Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion controllers/SiteController.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace app\controllers;

use yii\web\Controller;
use app\models\Post;

class SiteController extends Controller{
/**
Expand Down Expand Up @@ -31,6 +32,22 @@ public function actionIndex(){
* @return string
*/
public function actionTest1(){
return $this->render('test1');
$result = Post::getPaginatedPosts(6);
$error = null;
$posts = [];
$pagination = null;

if ($result === false) {
$error = 'Error loading posts from API. Please try again later.';
} else {
$posts = $result['posts'];
$pagination = $result['pagination'];
}

return $this->render('test1', [
'posts' => $posts,
'pagination' => $pagination,
'error' => $error
]);
}
}
85 changes: 85 additions & 0 deletions models/Post.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php

namespace app\models;

use yii\base\Model;
use yii\data\Pagination;

class Post extends Model
{
public $id;
public $userId;
public $title;
public $body;

/**
* Fetch posts from JSONPlaceholder API
* @return array|false
*/
public static function fetchPosts()
{
$url = 'https://jsonplaceholder.typicode.com/posts';

$context = stream_context_create([
'http' => [
'timeout' => 10,
'method' => 'GET',
'header' => 'Content-Type: application/json'
]
]);

$response = @file_get_contents($url, false, $context);

if ($response === false) {
return false;
}

$data = json_decode($response, true);

if (json_last_error() !== JSON_ERROR_NONE) {
return false;
}

$posts = [];
foreach ($data as $item) {
$post = new self();
$post->id = $item['id'];
$post->userId = $item['userId'];
$post->title = $item['title'];
$post->body = $item['body'];
$posts[] = $post;
}

return $posts;
}

/**
* Get paginated posts
* @param int $pageSize
* @return array|false
*/
public static function getPaginatedPosts($pageSize = 6)
{
$allPosts = self::fetchPosts();

if ($allPosts === false) {
return false;
}

$pagination = new Pagination([
'totalCount' => count($allPosts),
'pageSize' => $pageSize,
'pageSizeParam' => false,
'forcePageParam' => false,
]);

$offset = $pagination->offset;
$limit = $pagination->limit;
$posts = array_slice($allPosts, $offset, $limit);

return [
'posts' => $posts,
'pagination' => $pagination
];
}
}
62 changes: 61 additions & 1 deletion views/site/test1.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,67 @@

<div class="posts-list">
<div class="row mt-4">
<h2>Posts</h2>
<div class="col-md-12">
<h2>Posts</h2>

<?php if (isset($error) && $error): ?>
<div class="alert alert-danger" role="alert">
<?= $error ?>
</div>
<?php endif; ?>

<?php if (!empty($posts)): ?>
<div class="row">
<?php foreach ($posts as $post): ?>
<div class="col-md-6 col-lg-4 mb-4">
<div class="card h-100">
<div class="card-body">
<h5 class="card-title"><?= $post->title ?></h5>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The post title is displayed without HTML escaping. Since this content comes from an external API, it should be escaped to prevent potential XSS attacks.

Suggested change
<h5 class="card-title"><?= $post->title ?></h5>
<h5 class="card-title"><?= htmlspecialchars($post->title) ?></h5>

<p class="card-text"><?= $post->body ?></p>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The post body is displayed without HTML escaping. Since this content comes from an external API, it should be escaped to prevent potential XSS attacks.

Suggested change
<p class="card-text"><?= $post->body ?></p>
<p class="card-text"><?= htmlspecialchars($post->body) ?></p>

</div>
<div class="card-footer">
<small class="text-muted">
Post ID: <?= $post->id ?> | User ID: <?= $post->userId ?>
</small>
</div>
</div>
</div>
<?php endforeach; ?>
</div>

<?php if ($pagination && $pagination->pageCount > 1): ?>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pagination check should first verify if $pagination is set before checking its pageCount property to avoid potential PHP errors.

Suggested change
<?php if ($pagination && $pagination->pageCount > 1): ?>
<?php if (isset($pagination) && $pagination && $pagination->pageCount > 1): ?>

<div class="row mt-4">
<div class="col-md-12">
<nav aria-label="Posts pagination">
<?= \yii\widgets\LinkPager::widget([
'pagination' => $pagination,
'options' => ['class' => 'pagination justify-content-center'],
'linkOptions' => ['class' => 'page-link'],
'pageCssClass' => 'page-item',
'activePageCssClass' => 'active',
'disabledPageCssClass' => 'disabled',
'prevPageLabel' => '&laquo; Previous',
'nextPageLabel' => 'Next &raquo;',
'firstPageLabel' => '&laquo;&laquo; First',
'lastPageLabel' => 'Last &raquo;&raquo;',
]) ?>
</nav>

<div class="pagination-info text-center mt-3">
<small class="text-muted">
Showing <?= $pagination->offset + 1 ?> - <?= min($pagination->offset + $pagination->limit, $pagination->totalCount) ?>
of <?= $pagination->totalCount ?> posts
</small>
</div>
</div>
</div>
<?php endif; ?>
<?php else: ?>
<div class="alert alert-info" role="alert">
No posts available.
</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
100 changes: 100 additions & 0 deletions web/css/site.css
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,103 @@ main > .container {
margin-top: 5px;
color: #999;
}

.posts-list .card {
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
border: 1px solid #e0e0e0;
}

.posts-list .card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}

.posts-list .card-title {
font-size: 1.1rem;
font-weight: 600;
color: #333;
margin-bottom: 0.75rem;
line-height: 1.3;
}

.posts-list .card-text {
color: #666;
font-size: 0.9rem;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}

.posts-list .card-footer {
background-color: #f8f9fa;
border-top: 1px solid #e0e0e0;
padding: 0.75rem 1rem;
}

.posts-list .text-muted {
font-size: 0.8rem;
}

.site-test1 h2 {
color: #2c3e50;
margin-bottom: 1.5rem;
border-bottom: 2px solid #3498db;
padding-bottom: 0.5rem;
}

.site-test1 .alert {
margin-bottom: 1.5rem;
}

.pagination {
margin-bottom: 0;
}

.pagination .page-item .page-link {
color: #3498db;
border: 1px solid #dee2e6;
padding: 0.5rem 0.75rem;
margin: 0 2px;
border-radius: 4px;
transition: all 0.2s ease-in-out;
}

.pagination .page-item .page-link:hover {
color: #2c3e50;
background-color: #e9ecef;
border-color: #3498db;
transform: translateY(-1px);
}

.pagination .page-item.active .page-link {
background-color: #3498db;
border-color: #3498db;
color: white;
}

.pagination .page-item.disabled .page-link {
color: #6c757d;
background-color: #fff;
border-color: #dee2e6;
cursor: not-allowed;
}

.pagination .page-item.disabled .page-link:hover {
transform: none;
background-color: #fff;
border-color: #dee2e6;
}

.pagination-info {
color: #6c757d;
font-size: 0.9rem;
}

.pagination-info small {
background-color: #f8f9fa;
padding: 0.25rem 0.5rem;
border-radius: 3px;
border: 1px solid #e9ecef;
}