Skip to content

Commit eb91e5e

Browse files
committed
feat: enhance BlogPostPage and GameDetailPage with related projects and improved API queries
1 parent b541a9a commit eb91e5e

4 files changed

Lines changed: 117 additions & 21 deletions

File tree

index.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
<meta property="og:type" content="website" />
1313
<meta property="og:url" content="https://chrisjogos.com" />
1414
<meta name="twitter:card" content="summary_large_image" />
15+
<meta name="darkreader" content="no-darken">
16+
<meta name="darkreader-lock">
1517

1618
<link rel="stylesheet" href="/assets/css/all.min.css">
1719
<link rel="preconnect" href="https://fonts.googleapis.com">

src/pages/BlogPostPage.jsx

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { getReadingTime, extractToc } from '../utils/textUtils'
88
import { getAssetUrl, baseURL } from '../utils'
99
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
1010
import { dracula } from 'react-syntax-highlighter/dist/esm/styles/prism';
11+
import ProjectCard from '../components/ProjectCard'
1112

1213
// Component for rendering code blocks with syntax highlighting
1314
const CodeBlock = ({ node, inline, className, children, ...props }) => {
@@ -38,7 +39,22 @@ function BlogPostPage() {
3839

3940
useEffect(() => {
4041
const fetchPost = async () => {
41-
const API_URL = `${baseURL}/items/blog_posts/${slug}?fields=id,title,date_published,content,cover_image.id,cover_image.type`
42+
43+
// Fields required by ProjectCard component on the related project object
44+
const PROJECT_CARD_REQUIRED_FIELDS = [
45+
'id', 'status', 'release_date', 'engine', 'project_type',
46+
'card_image.id', 'card_image.type',
47+
'translations.*',
48+
'tags.tags_id',
49+
];
50+
51+
// Prefix all required fields with 'related_projects.projects_id.'
52+
const RELATED_PROJECT_FIELDS = PROJECT_CARD_REQUIRED_FIELDS
53+
.map(field => `related_projects.projects_id.${field}`)
54+
.join(',');
55+
56+
const API_URL = `${baseURL}/items/blog_posts/${slug}?fields=id,title,date_published,content,cover_image.id,cover_image.type,${RELATED_PROJECT_FIELDS}`
57+
4258
try {
4359
setLoading(true);
4460
const response = await fetch(API_URL);
@@ -92,6 +108,21 @@ function BlogPostPage() {
92108
? post.content.substring(0, 155).replace(/(\r\n|\n|\r|#|!|\[|\]|\*)/gm, " ").trim() + "..."
93109
: "Read this post on ChrisJogos blog.";
94110

111+
// Extract and filter related projects based on environment status requirements
112+
const relatedProjects = (post.related_projects || [])
113+
.map(link => link.projects_id)
114+
.filter(project => {
115+
if (!project) return false;
116+
const status = project.status;
117+
118+
if (import.meta.env.DEV) {
119+
return status === 'published' || status === 'draft';
120+
} else {
121+
return status === 'published';
122+
}
123+
});
124+
125+
95126
return (
96127
<div className="blog-post-layout">
97128

@@ -145,6 +176,18 @@ function BlogPostPage() {
145176
</ReactMarkdown>
146177
</div>
147178

179+
{/* Related Projects Section */}
180+
{relatedProjects.length > 0 && (
181+
<div className="github-readme-box">
182+
<h3>Related Projects</h3>
183+
<div className="game-grid">
184+
{relatedProjects.map((project) => (
185+
<ProjectCard key={project.id} project={project} />
186+
))}
187+
</div>
188+
</div>
189+
)}
190+
148191
</article>
149192

150193
<aside className="blog-post-sidebar-container">

src/pages/GameDetailPage.jsx

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { useState, useEffect } from 'react'
22
import { useParams, Link, useNavigate } from 'react-router-dom'
3-
import { baseURL, fieldsQuery, getHashedColor, getAssetUrl } from '../utils'
3+
import { baseURL, fieldsQuery, getHashedColor, getAssetUrl, formatDate } from '../utils'
44
import ScreenshotGallery from '../components/ScreenshotGallery'
55
import DownloadButton from '../components/DownloadButton'
66
import ReactMarkdown from 'react-markdown'
@@ -37,7 +37,6 @@ const CodeBlock = ({ node, inline, className, children, ...props }) => {
3737
);
3838
};
3939

40-
4140
function GameDetailPage() {
4241
const { projectId } = useParams()
4342
const [project, setProject] = useState(null);
@@ -92,7 +91,7 @@ function GameDetailPage() {
9291
const cardImageId = project.card_image?.id;
9392
const cardImageType = project.card_image?.type;
9493
const imageUrl = getAssetUrl(cardImageId, 800, '', cardImageType);
95-
94+
9695
const title = translation.title || 'Title Not Available'; // Define title for meta tags
9796

9897
const description = translation.synopsis
@@ -114,18 +113,21 @@ function GameDetailPage() {
114113

115114
const trailerEmbedUrl = getEmbedUrl(project.trailer_url);
116115

116+
// Filter only published related posts
117+
const relatedPosts = project.related_posts?.filter(post => post.post_id.status === 'published') || [];
118+
117119
return (
118120
<div className="page-content game-detail-page fade-in">
119121
{/* SEO META TAGS */}
120122
<title>{`${title} - ChrisJogos`}</title>
121123
<meta name="description" content={description} />
122-
124+
123125
{/* Open Graph Tags */}
124126
<meta property="og:title" content={title} />
125127
<meta property="og:description" content={description} />
126128
<meta property="og:type" content="article" />
127129
{imageUrl && <meta property="og:image" content={imageUrl} />}
128-
130+
129131
{/* Twitter Cards */}
130132
<meta name="twitter:card" content="summary_large_image" />
131133
<meta name="twitter:title" content={title} />
@@ -168,6 +170,40 @@ function GameDetailPage() {
168170
</ReactMarkdown>
169171
</div>
170172

173+
{/* Related Posts Section */}
174+
{relatedPosts.length > 0 && (
175+
<div className="github-readme-box">
176+
<h3>Related Articles</h3>
177+
<div className="blog-post-grid">
178+
{relatedPosts.map((post) => {
179+
const postImageUrl = post.post_id.cover_image
180+
? getAssetUrl(post.post_id.cover_image.id, 400, 'height=225&fit=cover', post.post_id.cover_image.type)
181+
: null;
182+
183+
return (
184+
<Link
185+
key={post.post_id.id}
186+
to={`/blog/${post.post_id.id}`}
187+
className="blog-post-card"
188+
>
189+
<div className="blog-post-image-container">
190+
{postImageUrl ? (
191+
<img src={postImageUrl} alt={`Cover image of ${post.post_id.title}`} />
192+
) : (
193+
<div className="blog-post-image-placeholder"></div>
194+
)}
195+
</div>
196+
<div className="blog-post-content">
197+
<h4>{post.post_id.title}</h4>
198+
<span className="blog-post-date">{formatDate(post.post_id.date_published)}</span>
199+
</div>
200+
</Link>
201+
);
202+
})}
203+
</div>
204+
</div>
205+
)}
206+
171207
</div>
172208

173209
<aside className="game-detail-sidebar">

src/utils.js

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export const baseURL = "https://cms.chrisjogos.com";
22

3-
const baseQuery = "fields=*,translations.*,tags.tags_id,tags_translations.*,card_image.id,card_image.type,genres.genres_id,steam_id,trailer_url,web_version_url,screenshots.directus_files_id.id,screenshots.directus_files_id.type";
3+
const baseQuery = "fields=*,translations.*,tags.tags_id,tags_translations.*,card_image.id,card_image.type,genres.genres_id,steam_id,trailer_url,web_version_url,screenshots.directus_files_id.id,screenshots.directus_files_id.type,related_posts.post_id.id,related_posts.post_id.title,related_posts.post_id.date_published,related_posts.post_id.cover_image.id,related_posts.post_id.cover_image.type,related_posts.post_id.status";
44

55
const filter = import.meta.env.DEV
66
? "filter[status][_in]=published,draft"
@@ -11,41 +11,56 @@ export const fieldsQuery = `${baseQuery}&${filter}`;
1111
const DEFAULT_IMAGE_WIDTH = 800;
1212

1313
/**
14-
* Retorna a URL otimizada de um asset do Directus, aplicando WEBP e limite de largura por padrão.
15-
* @param {string} id ID do asset.
16-
* @param {number} [width=800] Largura máxima.
17-
* @param {string} [options=''] Parâmetros adicionais (ex: 'height=120&fit=cover').
18-
* @param {string} [mimeType=''] O MIME type do arquivo (ex: 'image/gif').
19-
* @returns {string | null} URL otimizada.
14+
* Returns an optimized asset URL from Directus, applying WEBP format and width limit by default.
15+
* @param {string} id Asset ID.
16+
* @param {number} [width=800] Maximum width.
17+
* @param {string} [options=''] Additional parameters (e.g., 'height=120&fit=cover').
18+
* @param {string} [mimeType=''] The file MIME type (e.g., 'image/gif').
19+
* @returns {string | null} Optimized URL.
2020
*/
2121
export const getAssetUrl = (id, width = DEFAULT_IMAGE_WIDTH, options = '', mimeType = '') => {
2222
if (!id) return null;
23-
24-
// Se for GIF, retorna a URL original sem otimização
23+
24+
// If it's a GIF, return the original URL without optimization
2525
if (mimeType.toLowerCase() === 'image/gif') {
2626
return `${baseURL}/assets/${id}`;
2727
}
28-
28+
2929
let params = `?format=webp&width=${width}`;
3030
if (options) {
3131
params += `&${options}`;
3232
}
33-
33+
3434
return `${baseURL}/assets/${id}${params}`;
3535
};
3636

37+
/**
38+
* Formats a date string to a readable format.
39+
* @param {string} dateString Date string to format.
40+
* @returns {string} Formatted date.
41+
*/
42+
export const formatDate = (dateString) => {
43+
if (!dateString) return '';
44+
const date = new Date(dateString);
45+
return date.toLocaleDateString('en-US', {
46+
year: 'numeric',
47+
month: 'long',
48+
day: 'numeric'
49+
});
50+
}
51+
3752
export function getHashedColor(tagString) {
3853
let hash = 0;
3954
if (tagString.length === 0) return 'hsl(0, 0%, 30%)';
4055

4156
for (let i = 0; i < tagString.length; i++) {
4257
const char = tagString.charCodeAt(i);
4358
hash = (hash << 5) - hash + char;
44-
hash = hash & hash; // Converte para 32bit integer
59+
hash = hash & hash; // Convert to 32bit integer
4560
}
46-
61+
4762
const hue = Math.abs(hash) % 360;
48-
// Usamos 60% de saturação e 35% de luminosidade
49-
// É uma cor escura, mas colorida, perfeita para texto claro por cima
63+
// Use 60% saturation and 35% lightness
64+
// It's a dark but colorful color, perfect for light text on top
5065
return `hsl(${hue}, 60%, 35%)`;
5166
}

0 commit comments

Comments
 (0)