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 && (
-
-
![]()
+
)}
-
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;