feat: Hardcover→Directus book sync + fix empty states for projects/books
- Add POST /api/n8n/hardcover/sync-books — n8n calls this after detecting
finished books in Hardcover. Authenticates via N8N_SECRET_TOKEN/N8N_API_KEY,
deduplicates by hardcover_id, creates new book_reviews entries in Directus.
- Add getBookReviewByHardcoverId() + createBookReview() to lib/directus.ts.
Check uses GraphQL filter; create uses Directus REST POST /items/book_reviews.
- ReadBooks: replace silent return null with a visible empty state so the
section stays visible with a hint until the n8n sync populates it.
- Projects: add "No projects yet." placeholder instead of blank grid when
both Directus and PostgreSQL return no data.
- Add home.about.readBooks.empty i18n key (EN + DE).
n8n workflow setup:
Schedule → HTTP Hardcover GraphQL (books_read) → Code (transform) →
POST /api/n8n/hardcover/sync-books with array of { hardcover_id, title,
author, image, rating, finished_at }
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -993,3 +993,83 @@ export async function getSnippets(limit = 10, featured?: boolean): Promise<Snipp
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Hardcover → Directus sync helpers ───────────────────────────────────────
|
||||
|
||||
export interface BookReviewCreate {
|
||||
hardcover_id: string;
|
||||
book_title: string;
|
||||
book_author: string;
|
||||
book_image?: string;
|
||||
rating?: number;
|
||||
finished_at?: string;
|
||||
status: 'published' | 'draft';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a book review already exists in Directus by Hardcover ID.
|
||||
* Used for deduplication during sync.
|
||||
*/
|
||||
export async function getBookReviewByHardcoverId(
|
||||
hardcoverId: string
|
||||
): Promise<{ id: string } | null> {
|
||||
const query = `
|
||||
query {
|
||||
book_reviews(
|
||||
filter: { hardcover_id: { _eq: "${hardcoverId}" } }
|
||||
limit: 1
|
||||
) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
try {
|
||||
const result = await directusRequest<{ book_reviews: Array<{ id: string }> }>(
|
||||
'',
|
||||
{ body: { query } }
|
||||
);
|
||||
const item = result?.book_reviews?.[0];
|
||||
return item ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new book review in the Directus book_reviews collection via REST API.
|
||||
* Returns the created item id, or null on failure.
|
||||
*/
|
||||
export async function createBookReview(
|
||||
data: BookReviewCreate
|
||||
): Promise<{ id: string } | null> {
|
||||
if (!DIRECTUS_TOKEN) return null;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${DIRECTUS_URL}/items/book_reviews`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${DIRECTUS_TOKEN}`,
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.error(`Directus create book_review failed ${response.status}:`, text.slice(0, 200));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
return result?.data?.id ? { id: result.data.id } : null;
|
||||
} catch (error) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.error('createBookReview error:', error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user