From 264fac60bd438b96265407da18c0757ca07332af Mon Sep 17 00:00:00 2001 From: Jeremie Zitti Date: Wed, 22 Jul 2026 14:23:02 +0100 Subject: [PATCH] feat(projects): admin CRUD page for flagships + OSS partners MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wire /admin/projects endpoints (list, create, patch, archive) into the admin panel with filters (flagship / curated / partnership level / archived), create + edit modal, and a sidebar entry. All content management — including OSS partner curation and flagship stewardship — now goes through the admin UI rather than seed scripts. --- src/lib/api/admin.ts | 44 +++ src/lib/types/index.ts | 76 ++++ src/routes/+layout.svelte | 2 + src/routes/projects/+page.svelte | 602 +++++++++++++++++++++++++++++++ 4 files changed, 724 insertions(+) create mode 100644 src/routes/projects/+page.svelte diff --git a/src/lib/api/admin.ts b/src/lib/api/admin.ts index 5382709..b4133d1 100644 --- a/src/lib/api/admin.ts +++ b/src/lib/api/admin.ts @@ -4,6 +4,11 @@ import type { ChallengeDifficulty, ChallengeMode, ChallengeTone, + ProjectListItem, + ProjectListFilters, + ProjectDetail, + ProjectCreateBody, + ProjectPatchBody, Report, ReportStatus, ReportTargetType, @@ -357,6 +362,45 @@ export const adminApi = { return api.post>('/admin/leaderboards/rebuild'); }, + // --- Admin projects (content-strategy §4, annexes E + F) --- + + listAdminProjects(filters?: ProjectListFilters) { + const params = new URLSearchParams(); + if (filters?.is_flagship !== undefined) params.set('is_flagship', String(filters.is_flagship)); + if (filters?.curated_by_admin !== undefined) + params.set('curated_by_admin', String(filters.curated_by_admin)); + if (filters?.partnership_level !== undefined) + params.set('partnership_level', String(filters.partnership_level)); + if (filters?.include_archived) params.set('include_archived', 'true'); + if (filters?.page) params.set('page', String(filters.page)); + if (filters?.per_page) params.set('per_page', String(filters.per_page)); + const qs = params.toString(); + return api.get< + ApiPaginatedResponse + >(`/admin/projects${qs ? `?${qs}` : ''}`); + }, + + getAdminProject(slug: string) { + return api.get>(`/admin/projects/${slug}`); + }, + + createAdminProject(data: ProjectCreateBody) { + return api.post>('/admin/projects', data); + }, + + patchAdminProject(slug: string, data: ProjectPatchBody) { + return api.patch>( + `/admin/projects/${slug}`, + data + ); + }, + + archiveAdminProject(slug: string) { + return api.delete>( + `/admin/projects/${slug}` + ); + }, + // --- Community --- communityReview() { diff --git a/src/lib/types/index.ts b/src/lib/types/index.ts index cffc65f..7d3874d 100644 --- a/src/lib/types/index.ts +++ b/src/lib/types/index.ts @@ -734,3 +734,79 @@ export const ERROR_CODES = { CONTACT_BLOCKED: 'CONTACT_BLOCKED', CONVERSATION_CLOSED: 'CONVERSATION_CLOSED' } as const; + +// ─── Admin projects — content-strategy-2027-2028 §4 + annexes E, F ─────────── + +export type ProjectOwnerType = 'user' | 'guild'; +export type PartnershipLevel = 1 | 2 | 3; + +/** Row returned by `GET /admin/projects` list endpoint. */ +export interface ProjectListItem { + id: string; + slug: string; + name: string; + description: string | null; + repo_url: string | null; + is_flagship: boolean; + curated_by_admin: boolean; + skilluv_partnership_level: PartnershipLevel | null; + flagship_steward_user_id: string | null; + created_at: string; + archived_at: string | null; +} + +/** Full row returned by `GET /admin/projects/{slug}`. */ +export interface ProjectDetail extends ProjectListItem { + demo_url: string | null; + tech_stack: string[]; + is_oss: boolean; + looking_for_contributors: boolean; + owner_type: ProjectOwnerType; + owner_id: string; + skilluv_editorial_notes: string | null; + updated_at: string; +} + +/** Payload for `POST /admin/projects`. */ +export interface ProjectCreateBody { + slug: string; + name: string; + description?: string | null; + repo_url?: string | null; + demo_url?: string | null; + tech_stack?: string[]; + is_oss?: boolean; + looking_for_contributors?: boolean; + owner_type: ProjectOwnerType; + owner_id: string; + curated_by_admin?: boolean; + is_flagship?: boolean; + flagship_steward_user_id?: string | null; + skilluv_partnership_level?: PartnershipLevel | null; + skilluv_editorial_notes?: string | null; +} + +/** Payload for `PATCH /admin/projects/{slug}`. All fields optional. */ +export interface ProjectPatchBody { + name?: string; + description?: string | null; + repo_url?: string | null; + demo_url?: string | null; + tech_stack?: string[]; + is_oss?: boolean; + looking_for_contributors?: boolean; + curated_by_admin?: boolean; + is_flagship?: boolean; + flagship_steward_user_id?: string | null; + skilluv_partnership_level?: PartnershipLevel | null; + skilluv_editorial_notes?: string | null; +} + +export interface ProjectListFilters { + is_flagship?: boolean; + curated_by_admin?: boolean; + partnership_level?: PartnershipLevel; + include_archived?: boolean; + page?: number; + per_page?: number; +} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 4c6e6bc..5622199 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -22,6 +22,7 @@ BookMarked, Briefcase, Network, + FolderGit2, LogOut } from '@lucide/svelte'; @@ -45,6 +46,7 @@ { href: '/reports', label: i18n.t('admin.reports.title'), icon: Flag }, { href: '/fraud', label: i18n.t('admin.nav.fraud'), icon: Fingerprint }, { href: '/challenges', label: i18n.t('admin.challenges.title'), icon: Code2 }, + { href: '/projects', label: 'Projets', icon: FolderGit2 }, { href: '/community', label: i18n.t('admin.community.title'), icon: Star }, { href: '/catalog', label: i18n.t('admin.nav.catalog'), icon: BookMarked }, { href: '/skills', label: i18n.t('admin.nav.skills'), icon: Network }, diff --git a/src/routes/projects/+page.svelte b/src/routes/projects/+page.svelte new file mode 100644 index 0000000..e19506d --- /dev/null +++ b/src/routes/projects/+page.svelte @@ -0,0 +1,602 @@ + + + + Projets — Admin Skilluv + + +
+
+
+

Projets

+

+ Gestion des partenaires OSS curés, des flagships Skilluv et des projets communautaires + modérés. +

+
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ + + {#if loading} + + {:else if projects.length === 0} +
+

Aucun projet ne correspond aux filtres.

+
+ {:else} +
+ + + + + + + + + + + + + {#each projects as p (p.id)} + + + + + + + + + {/each} + +
NomTypeRepoNiveauCréé le + Actions +
+
{p.name}
+
{p.slug}
+ {#if p.description} +
{p.description}
+ {/if} +
+
+ {#if p.is_flagship} + Flagship + {/if} + {#if p.curated_by_admin} + Curated + {/if} + {#if !p.is_flagship && !p.curated_by_admin} + User + {/if} + {#if p.archived_at} + Archivé + {/if} +
+
+ {#if p.repo_url} + + {p.repo_url.replace('https://github.com/', '')} + + {:else} + + {/if} + + {#if p.skilluv_partnership_level} + Niveau {p.skilluv_partnership_level} + {:else} + + {/if} + + {new Date(p.created_at).toLocaleDateString('fr-FR')} + +
+ + {#if !p.archived_at} + + {/if} +
+
+
+ +
+ + {total} projet{total > 1 ? 's' : ''} au total + +
+ + +
+
+ {/if} +
+ + + (showForm = false)} +> + {#snippet children()} +
+ {#if !editing} +
+ + +

Minuscules, chiffres, tirets. Immuable après création.

+
+ {/if} +
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+ + {#if !editing} +
+
+ + +
+
+ + +
+
+ {/if} + +
+ + + + +
+ + {#if form.is_flagship} +
+ + +
+ {/if} + +
+ + +
+ +
+ + +
+ +
+ + +
+
+ {/snippet} +