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:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user