Add curator Movement links config with proof URLs and admin-gated delete votes.
Persist lineage in PostgreSQL, seed documented edges, and keep timeline/tree consumers API-backed with a static fallback. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
2b4cdc5a9f
commit
1b760faf0d
36 files changed
+2051
-43
No files matched your search
@@ -773,6 +773,81 @@ export const api = {
|
||||
);
|
||||
},
|
||||
|
||||
listMovementLinks: (params: { q?: string; limit?: number; offset?: number } = {}) => {
|
||||
const qs = new URLSearchParams();
|
||||
if (params.q) qs.set('q', params.q);
|
||||
if (params.limit) qs.set('limit', String(params.limit));
|
||||
if (params.offset) qs.set('offset', String(params.offset));
|
||||
const q = qs.toString();
|
||||
return fetchJson<{ items: MovementLineageLinkItem[]; total: number }>(
|
||||
`${API}/movement-links${q ? `?${q}` : ''}`,
|
||||
);
|
||||
},
|
||||
|
||||
createMovementLink: (payload: {
|
||||
sourceMovementId: number;
|
||||
targetMovementId: number;
|
||||
proofUrl: string;
|
||||
proofLabel?: string;
|
||||
linkYearApprox?: number | null;
|
||||
notes?: string;
|
||||
}) =>
|
||||
fetch(`${API}/movement-links`, {
|
||||
...fetchCredentials,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
}).then(async (res) => {
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => ({}));
|
||||
throw new Error(body.error || `Create failed: ${res.status}`);
|
||||
}
|
||||
return res.json() as Promise<{ item: MovementLineageLinkItem }>;
|
||||
}),
|
||||
|
||||
listMovementLinkDeleteVotes: (id: number) =>
|
||||
fetchJson<{ linkId: number; votes: MovementLineageDeleteVote[] }>(
|
||||
`${API}/movement-links/${id}/delete-votes`,
|
||||
),
|
||||
|
||||
voteDeleteMovementLink: (id: number, reason: string) =>
|
||||
fetch(`${API}/movement-links/${id}/delete-votes`, {
|
||||
...fetchCredentials,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ reason }),
|
||||
}).then(async (res) => {
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => ({}));
|
||||
throw new Error(body.error || `Vote failed: ${res.status}`);
|
||||
}
|
||||
return res.json() as Promise<{ vote: MovementLineageDeleteVote }>;
|
||||
}),
|
||||
|
||||
withdrawDeleteMovementLinkVote: (id: number) =>
|
||||
fetch(`${API}/movement-links/${id}/delete-votes/me`, {
|
||||
...fetchCredentials,
|
||||
method: 'DELETE',
|
||||
}).then(async (res) => {
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => ({}));
|
||||
throw new Error(body.error || `Withdraw failed: ${res.status}`);
|
||||
}
|
||||
return res.json() as Promise<{ ok: boolean; id: number; linkId: number }>;
|
||||
}),
|
||||
|
||||
deleteMovementLink: (id: number) =>
|
||||
fetch(`${API}/movement-links/${id}`, {
|
||||
...fetchCredentials,
|
||||
method: 'DELETE',
|
||||
}).then(async (res) => {
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => ({}));
|
||||
throw new Error(body.error || `Delete failed: ${res.status}`);
|
||||
}
|
||||
return res.json() as Promise<{ ok: boolean; id: number }>;
|
||||
}),
|
||||
|
||||
getInfluenceGraph: (params: { artistId?: number; paintingId?: number }) => {
|
||||
const qs = new URLSearchParams();
|
||||
if (params.artistId) qs.set('artistId', String(params.artistId));
|
||||
@@ -1032,6 +1107,33 @@ export interface InfluenceEdgeItem {
|
||||
updatedAt: string | null;
|
||||
}
|
||||
|
||||
export interface MovementLineageLinkItem {
|
||||
id: number;
|
||||
source_movement_id: number;
|
||||
target_movement_id: number;
|
||||
link_year_approx: number | null;
|
||||
proof_url: string;
|
||||
proof_label: string | null;
|
||||
notes: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
source_name: string;
|
||||
source_start_year: number;
|
||||
target_name: string;
|
||||
target_start_year: number;
|
||||
delete_vote_count?: number;
|
||||
viewer_has_delete_vote?: boolean;
|
||||
}
|
||||
|
||||
export interface MovementLineageDeleteVote {
|
||||
id: number;
|
||||
link_id: number;
|
||||
user_id: number;
|
||||
reason: string;
|
||||
created_at: string;
|
||||
username?: string;
|
||||
}
|
||||
|
||||
export interface InfluenceGraph {
|
||||
focus: { artistId: number; paintingId: number | null; label: string };
|
||||
nodes: Array<{ id: string; type: string; label: string; focus?: boolean; artistId?: number; paintingId?: number; movementId?: number }>;
|
||||
|
||||
Reference in new issue
Block a user