feat(08-03): FavoritesWidget frontend + API client + page.tsx wiring (GREEN)

- favorites-api.ts: FavoriteLink type + fetchFavorites/createFavorite/updateFavorite/deleteFavorite
  all use credentials: include and API_URL/favorites
- favorites-widget.tsx: list/grid view, inline add/edit/delete in edit mode,
  icon + letter fallback, rel=noreferrer + target=_blank, no dangerouslySetInnerHTML (T-08-07)
- useEffect deps fixed to [instanceId] only — excludes t() to prevent re-fetch on each render
- page.tsx: wireFavoritesWidget(FavoritesWidget) wired
- Full test suite: 81/81 pass (17 test files)
- Web TypeScript: clean
This commit is contained in:
2026-07-01 10:34:11 +02:00
parent 758d246e98
commit cc6f5ae893
4 changed files with 579 additions and 21 deletions
+81
View File
@@ -0,0 +1,81 @@
/**
* Favorites API client functions.
* Mirrors the NestJS FavoritesController routes (08-03).
* All calls use credentials: 'include' for cookie-based auth.
*/
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
export interface FavoriteLink {
id: string;
widgetId: string;
title: string;
url: string;
iconUrl: string | null;
position: number;
}
/**
* Fetch all favorite links for a specific widget instance.
* The widgetId parameter scopes the query to the correct widget (Pitfall 3).
*/
export async function fetchFavorites(widgetId: string): Promise<FavoriteLink[]> {
const res = await fetch(
`${API_URL}/favorites?widgetId=${encodeURIComponent(widgetId)}`,
{ credentials: 'include' },
);
if (!res.ok) throw new Error('Failed to fetch favorites');
return res.json();
}
/**
* Create a new favorite link.
* Server-side icon discovery runs automatically if iconUrl is not provided.
*/
export async function createFavorite(payload: {
widgetId: string;
title: string;
url: string;
iconUrl?: string;
}): Promise<FavoriteLink> {
const res = await fetch(`${API_URL}/favorites`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(payload),
});
if (!res.ok) throw new Error('Failed to create favorite');
return res.json();
}
/**
* Update an existing favorite link.
* Pass iconUrl: null to clear a stored icon.
*/
export async function updateFavorite(
id: string,
payload: Partial<{ title: string; url: string; iconUrl: string | null; position: number }>,
): Promise<FavoriteLink> {
const res = await fetch(`${API_URL}/favorites/${encodeURIComponent(id)}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(payload),
});
if (!res.ok) throw new Error('Failed to update favorite');
return res.json();
}
/**
* Delete a favorite link by id.
*/
export async function deleteFavorite(id: string): Promise<void> {
const res = await fetch(`${API_URL}/favorites/${encodeURIComponent(id)}`, {
method: 'DELETE',
credentials: 'include',
});
if (!res.ok) throw new Error('Failed to delete favorite');
}