diff --git a/frontend/.env.local.example b/frontend/.env.local.example index 600de8d..4784ecb 100644 --- a/frontend/.env.local.example +++ b/frontend/.env.local.example @@ -1 +1 @@ -NEXT_PUBLIC_API_URL=http://localhost:8000 +NEXT_PUBLIC_API_URL=http://localhost:8000/api diff --git a/frontend/src/app/news/[id]/not-found.tsx b/frontend/src/app/news/[id]/not-found.tsx new file mode 100644 index 0000000..f48255d --- /dev/null +++ b/frontend/src/app/news/[id]/not-found.tsx @@ -0,0 +1,19 @@ +import Link from "next/link"; + +export default function NewsNotFound() { + return ( +
+

Article Not Found

+

+ The article you are looking for may have been removed or is not + available yet. +

+ + Back to All News + +
+ ); +} diff --git a/frontend/src/app/news/[id]/page.tsx b/frontend/src/app/news/[id]/page.tsx index 9b7c530..b65aea4 100644 --- a/frontend/src/app/news/[id]/page.tsx +++ b/frontend/src/app/news/[id]/page.tsx @@ -1,5 +1,6 @@ import { getNewsById } from "@/lib/api"; import { format } from "date-fns"; +import Image from "next/image"; import { notFound } from "next/navigation"; export default async function NewsDetailPage({ @@ -29,15 +30,16 @@ export default async function NewsDetailPage({ )} {article.image_url && ( -
- + {article.title}
)} -
+

{article.body}

diff --git a/frontend/src/app/news/page.tsx b/frontend/src/app/news/page.tsx index ee46140..766056b 100644 --- a/frontend/src/app/news/page.tsx +++ b/frontend/src/app/news/page.tsx @@ -1,6 +1,6 @@ import { Card } from "@/components/ui/card"; +import type { NewsArticle } from "@/lib/api"; import { getNews } from "@/lib/api"; -import type { NewsArticle } from "@/lib/mock/news"; import { format } from "date-fns"; import Image from "next/image"; import Link from "next/link"; @@ -16,8 +16,9 @@ export default async function NewsPage({ Array.isArray(pageParam) ? pageParam[0] : pageParam || "1", 10, ); + const safePage = Number.isFinite(page) && page > 0 ? page : 1; const pageSize = 10; - const newsData: NewsArticle[] = await getNews(page, pageSize); + const newsData: NewsArticle[] = await getNews(safePage, pageSize); const hasNextPage = newsData.length === pageSize; return ( @@ -47,9 +48,9 @@ export default async function NewsPage({
- {page > 1 ? ( + {safePage > 1 ? ( Previous @@ -60,11 +61,11 @@ export default async function NewsPage({
)} - Page {page} + Page {safePage} {hasNextPage ? ( Next diff --git a/frontend/src/components/home/RecentNews.tsx b/frontend/src/components/home/RecentNews.tsx index 90a5ceb..9deaf40 100644 --- a/frontend/src/components/home/RecentNews.tsx +++ b/frontend/src/components/home/RecentNews.tsx @@ -1,8 +1,9 @@ import { Card } from "@/components/ui/card"; +import type { NewsArticle } from "@/lib/api"; import { getNews } from "@/lib/api"; -import Link from "next/link"; -import Image from "next/image"; import { format } from "date-fns"; +import Image from "next/image"; +import Link from "next/link"; export default async function RecentNews() { const newsData = await getNews(1, 3); @@ -11,7 +12,7 @@ export default async function RecentNews() {

Recent News

- {newsData.map((article: any) => ( + {newsData.map((article: NewsArticle) => (
@@ -43,4 +44,4 @@ export default async function RecentNews() {
); -} \ No newline at end of file +} diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 151ef75..9383124 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -3,36 +3,119 @@ */ export const API_BASE_URL = - process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"; + process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000/api"; -export async function fetchAPI(endpoint: string, options?: RequestInit) { +class ApiError extends Error { + status: number; + + constructor(status: number, message: string) { + super(message); + this.name = "ApiError"; + this.status = status; + } +} + +interface NewsApiArticle { + id: number | string; + title: string; + summary?: string; + description?: string; + body: string; + author_name?: string | null; + author?: string | null; + image_url: string | null; + date_published: string; + date_last_edited?: string | null; + date_edited?: string | null; +} + +interface PaginatedResponse { + items: T[]; + total: number; + page: number; + limit: number; +} + +export interface NewsArticle { + id: string; + title: string; + description: string; + body: string; + author: string | null; + image_url: string | null; + date_published: string; + date_edited: string | null; +} + +function mapNewsArticle(article: NewsApiArticle): NewsArticle { + return { + id: String(article.id), + title: article.title, + description: article.summary ?? article.description ?? "", + body: article.body, + author: article.author_name ?? article.author ?? null, + image_url: article.image_url, + date_published: article.date_published, + date_edited: article.date_last_edited ?? article.date_edited ?? null, + }; +} + +function sortMostRecentFirst(articles: NewsArticle[]): NewsArticle[] { + return [...articles].sort( + (a, b) => + new Date(b.date_published).getTime() - + new Date(a.date_published).getTime(), + ); +} + +export async function fetchAPI( + endpoint: string, + options?: RequestInit, +): Promise { const url = `${API_BASE_URL}${endpoint}`; const response = await fetch(url, options); if (!response.ok) { - throw new Error(`API error: ${response.statusText}`); + throw new ApiError(response.status, `API error: ${response.statusText}`); } - return response.json(); + return response.json() as Promise; } -export async function getNews(page: number = 1, limit: number = 10) { +export async function getNews( + page: number = 1, + limit: number = 10, +): Promise { try { - const data = await fetchAPI(`/news?page=${page}&limit=${limit}`); - return data; - } catch { + const data = await fetchAPI>( + `/news?page=${page}&limit=${limit}`, + ); + return sortMostRecentFirst(data.items.map(mapNewsArticle)); + } catch (error) { + if (error instanceof ApiError && error.status < 500) { + throw error; + } + console.warn("[API] Backend unavailable — using mock news data"); const { mockNews } = await import("@/lib/mock/news"); const start = (page - 1) * limit; - return mockNews.slice(start, start + limit); + return sortMostRecentFirst(mockNews).slice(start, start + limit); } } -export async function getNewsById(id: string) { +export async function getNewsById(id: string): Promise { try { - const data = await fetchAPI(`/news/${id}`); - return data; - } catch { + const data = await fetchAPI(`/news/${id}`); + return mapNewsArticle(data); + } catch (error) { + if (error instanceof ApiError && error.status === 404) { + return null; + } + + if (error instanceof ApiError && error.status < 500) { + throw error; + } + console.warn("[API] Backend unavailable — using mock news article"); const { mockNews } = await import("@/lib/mock/news"); return mockNews.find((article) => article.id === id) ?? null;