From de934b8035bff8be5f138940179a53ebdf0b041e Mon Sep 17 00:00:00 2001 From: owlinstack Date: Fri, 10 Jul 2026 15:44:47 +0200 Subject: [PATCH 01/10] feat: display pinned articles in a vertical list with discrete borders, and add a toggle button for mobile carousel view --- src/app/page.tsx | 21 +- src/components/article/PinnedArticles.tsx | 232 ++++++++++++++++++++++ src/lib/api.ts | 12 +- src/lib/types.ts | 1 + src/lib/validation.ts | 1 + 5 files changed, 264 insertions(+), 3 deletions(-) create mode 100644 src/components/article/PinnedArticles.tsx diff --git a/src/app/page.tsx b/src/app/page.tsx index 700d148..145dd82 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -3,6 +3,7 @@ import { Footer } from "@/components/layout/Footer"; import { Sidebar } from "@/components/layout/Sidebar"; import { Aside } from "@/components/layout/Aside"; import { ArticleGrid } from "@/components/article/ArticleGrid"; +import { PinnedArticles } from "@/components/article/PinnedArticles"; import { getArticles, getCategories, getTags } from "@/lib/api"; interface HomePageProps { @@ -22,16 +23,29 @@ export default async function HomePage({ searchParams }: HomePageProps) { const categoriesData = await getCategories(); const tagsData = await getTags(); - // We load all articles up to the current page on the server so "Load More" appends them + const isBrowsingAll = pageNum === 1 && !activeTag && activeCategory === "all"; + + // Charger les articles épinglés sur la page principale + let pinnedArticles: any[] = []; + if (isBrowsingAll) { + const pinnedArticlesData = await getArticles({ + page: 1, + pageSize: 10, + is_pinned: true, + }); + pinnedArticles = pinnedArticlesData.articles; + } + + // Charger le flux normal (exclut les épinglés sur l'accueil principale) const articlesData = await getArticles({ category: activeCategory, tag: activeTag, page: 1, pageSize: pageNum * 10, + is_pinned: isBrowsingAll ? false : undefined, }); const articles = articlesData.articles; - const isBrowsingAll = pageNum === 1 && !activeTag && activeCategory === "all"; const featuredArticle = isBrowsingAll ? (articles.find((a) => a.featured) ?? articles[0] ?? null) : null; @@ -58,6 +72,9 @@ export default async function HomePage({ searchParams }: HomePageProps) { {/* Article list — center column */}
+ {isBrowsingAll && pinnedArticles.length > 0 && ( + + )} + {/* Title Header with Collapse/Expand Button */} +
+
+ +

+ Articles épinglés +

+
+ + {/* Button shown on mobile only to toggle between vertical list and carousel */} + +
+ + {/* Grid or Horizontal scroll wrapper depending on layout state */} +
+ {articles.map((article) => ( +
+ + {/* Category */} + + {article.categories && article.categories.length > 0 ? article.categories.join(' / ') : article.category} + + + {/* Title */} +

+ {article.title} +

+ + {/* Excerpt */} +

+ {article.excerpt} +

+ + {/* Meta */} +

+ {formatDate(article.publishedAt)} + · + {article.readingTime} min de lecture +

+ +
+ ))} +
+ + + + ) +} diff --git a/src/lib/api.ts b/src/lib/api.ts index 7692494..4dfe5e4 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -69,6 +69,7 @@ Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu tags: ["typescript", "php", "api", "validation"], publishedAt: "2026-05-28", readingTime: 5, + is_pinned: true, }, { id: "01H7B3Q9N8472M6YV6N7R0G5Y2", @@ -87,6 +88,7 @@ Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deseru tags: ["css", "animation", "browser", "ux"], publishedAt: "2026-05-14", readingTime: 4, + is_pinned: true, }, { id: "01H7B3Q9N8472M6YV6N7R0G5Y3", @@ -780,8 +782,9 @@ export async function getArticles(params?: { tag?: string; page?: number; pageSize?: number; + is_pinned?: boolean; }): Promise { - const { category, tag, page = 1, pageSize = 10 } = params ?? {}; + const { category, tag, page = 1, pageSize = 10, is_pinned } = params ?? {}; const searchParams = new URLSearchParams({ page: String(page), @@ -789,6 +792,7 @@ export async function getArticles(params?: { }); if (category && category !== "all") searchParams.append("category", category); if (tag) searchParams.append("tag", tag); + if (is_pinned !== undefined) searchParams.append("is_pinned", String(is_pinned)); let filtered = [...MOCK_ARTICLES]; if (category && category !== "all") { @@ -797,6 +801,12 @@ export async function getArticles(params?: { if (tag) { filtered = filtered.filter((a) => a.tags.includes(tag)); } + if (is_pinned !== undefined) { + filtered = filtered.filter((a) => { + const pinVal = !!a.is_pinned; + return pinVal === is_pinned; + }); + } const start = (page - 1) * pageSize; const mockFallback: PaginatedArticles = { articles: filtered.slice(start, start + pageSize), diff --git a/src/lib/types.ts b/src/lib/types.ts index 2ca028c..ca8d44d 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -10,6 +10,7 @@ export interface Article { publishedAt: string | null readingTime: number // in minutes featured?: boolean | null + is_pinned?: boolean | null codeFile?: CodeFile | null codeFolder?: CodeFolder | null codeProject?: CodeProject | null diff --git a/src/lib/validation.ts b/src/lib/validation.ts index 7f1b295..a81059c 100644 --- a/src/lib/validation.ts +++ b/src/lib/validation.ts @@ -103,6 +103,7 @@ export const ArticleSchema = z.object({ publishedAt: DateStringSchema.nullable(), readingTime: z.number().nonnegative(), featured: z.boolean().optional().nullable(), + is_pinned: z.boolean().optional().nullable(), codeFile: CodeFileSchema.optional().nullable(), codeFolder: z.lazy(() => CodeFolderSchema).optional().nullable(), codeProject: CodeProjectSchema.optional().nullable(), From bbe1385716384163c8d839f5c47ec6a53b7970a5 Mon Sep 17 00:00:00 2001 From: owlinstack Date: Fri, 10 Jul 2026 15:48:43 +0200 Subject: [PATCH 02/10] style: center avatar and name on mobile screens in profile page --- src/app/profil/page.tsx | 48 ++++++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/src/app/profil/page.tsx b/src/app/profil/page.tsx index 4d90ad4..420ca2d 100644 --- a/src/app/profil/page.tsx +++ b/src/app/profil/page.tsx @@ -49,21 +49,14 @@ export default async function ProfilPage() { }} > {/* Hero name & Avatar */} -
+
{profile.avatarUrl && ( {profile.name} )} -

+

{profile.name}

@@ -331,6 +314,31 @@ export default async function ProfilPage() {