feat: Phase 1 - Admin CMS with Links Management

Backend (updates-api):
- Add links table schema with categories, visibility, sorting
- Implement full CRUD API for links (/v1/links, /v1/admin/links)
- Create admin UI for links management (/admin/links)
- Add navigation between Updates and Links admin pages
- Add comprehensive tests for links API (7/7 passing)

Frontend (Next.js):
- Create API client for fetching links from updates-api
- Add LinksSection component with category icons
- Integrate links into homepage (between Notes and CTA)
- Add i18n entries for links section (DE/EN)
- ISR caching: 60s for links, 5min for updates

Features:
- Links organized by category (external/internal/resource/tool)
- Visibility toggle (show/hide on homepage)
- Sort order support
- Optional icons and descriptions
- Responsive grid layout (1/2/3 columns)

Testing:
- All existing tests pass
- New links API tests cover CRUD + validation
- Production build successful

Phase 1 Complete: Links management ready for deployment
Next: Phase 2 (Markdown editor + media upload)
This commit is contained in:
Jan Wagner
2026-08-22 16:56:00 +02:00
parent 1f37edfa46
commit 97944875ca
12 changed files with 1533 additions and 1 deletions
+101
View File
@@ -0,0 +1,101 @@
// API client for updates-api service
const UPDATES_API_URL = process.env.UPDATES_API_URL || 'http://localhost:8080';
export type Link = {
id: number;
title: string;
url: string;
category: 'external' | 'internal' | 'resource' | 'tool';
description: string | null;
icon: string | null;
sort_order: number;
};
export type Update = {
id: number;
slug: string;
product: 'batchmaker' | 'standalone';
title: string;
summary: string;
body_markdown: string;
published_at: string;
created_at: string;
updated_at: string;
link_url: string | null;
};
/**
* Fetch visible links from the updates-api
* Used for displaying links on the homepage
*/
export async function fetchLinks(): Promise<Link[]> {
try {
const res = await fetch(`${UPDATES_API_URL}/v1/links`, {
next: { revalidate: 60 }, // ISR: Cache 60s
});
if (!res.ok) {
console.error('Failed to fetch links:', res.status, res.statusText);
return [];
}
const data = await res.json();
return data.data || [];
} catch (error) {
console.error('Error fetching links:', error);
return [];
}
}
/**
* Fetch recent product updates
* @param product Filter by product (optional)
* @param limit Max number of updates (default: 5)
*/
export async function fetchUpdates(
product?: 'batchmaker' | 'standalone',
limit: number = 5
): Promise<Update[]> {
try {
const params = new URLSearchParams();
if (product) params.set('product', product);
params.set('limit', String(limit));
const res = await fetch(
`${UPDATES_API_URL}/v1/updates?${params}`,
{ next: { revalidate: 300 } } // ISR: Cache 5min
);
if (!res.ok) {
console.error('Failed to fetch updates:', res.status, res.statusText);
return [];
}
const data = await res.json();
return data.data || [];
} catch (error) {
console.error('Error fetching updates:', error);
return [];
}
}
/**
* Fetch a single update by slug
*/
export async function fetchUpdateBySlug(slug: string): Promise<Update | null> {
try {
const res = await fetch(`${UPDATES_API_URL}/v1/updates/${slug}`, {
next: { revalidate: 300 },
});
if (!res.ok) {
return null;
}
const data = await res.json();
return data.data || null;
} catch (error) {
console.error('Error fetching update:', error);
return null;
}
}