prod setup
This commit is contained in:
78
src/components/admin/forms/ExperiencesForm.tsx
Normal file
78
src/components/admin/forms/ExperiencesForm.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { ExperiencesSchema, type Experiences } from '../../../content/schemas/experiences';
|
||||
import { FormShell } from '../FormShell';
|
||||
import { Section } from '../Section';
|
||||
import { TextField } from '../fields/TextField';
|
||||
import { TextAreaField } from '../fields/TextAreaField';
|
||||
import { SelectField } from '../fields/SelectField';
|
||||
import { ToggleField } from '../fields/ToggleField';
|
||||
import { ChipsField } from '../fields/ChipsField';
|
||||
import { ImageUploadField } from '../fields/ImageUploadField';
|
||||
import { ArrayEditor } from '../fields/ArrayEditor';
|
||||
|
||||
type Props = { defaultValues: Experiences };
|
||||
|
||||
const TYPE_OPTIONS = [
|
||||
{ value: 'cdi', label: 'CDI' },
|
||||
{ value: 'cdd', label: 'CDD' },
|
||||
{ value: 'stage', label: 'Stage' },
|
||||
{ value: 'alternance', label: 'Alternance' },
|
||||
{ value: 'freelance', label: 'Freelance' },
|
||||
];
|
||||
|
||||
export function ExperiencesForm({ defaultValues }: Props) {
|
||||
return (
|
||||
<FormShell<Experiences>
|
||||
section="experiences"
|
||||
defaultValues={defaultValues as never}
|
||||
resolver={zodResolver(ExperiencesSchema) as never}
|
||||
>
|
||||
<Section title="Expériences" subtitle="// setlist">
|
||||
<ArrayEditor
|
||||
name="items"
|
||||
label="Postes"
|
||||
newItem={() =>
|
||||
({
|
||||
id: '',
|
||||
company: '',
|
||||
role: '',
|
||||
period: '',
|
||||
duration: '',
|
||||
description: '',
|
||||
missions: [],
|
||||
stack: [],
|
||||
type: 'cdi' as const,
|
||||
current: false,
|
||||
}) as never
|
||||
}
|
||||
itemLabel={(i) => `track #${String(i + 1).padStart(2, '0')}`}
|
||||
>
|
||||
{({ namePrefix }) => (
|
||||
<>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||
<TextField name={`${namePrefix}.id`} label="ID" hint="Slug unique" />
|
||||
<TextField name={`${namePrefix}.company`} label="Entreprise" />
|
||||
<TextField name={`${namePrefix}.role`} label="Rôle" />
|
||||
</div>
|
||||
<ImageUploadField name={`${namePrefix}.logo`} label="Logo" folder="company" />
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4 items-end">
|
||||
<TextField name={`${namePrefix}.period`} label="Période" placeholder="Janv 2020 — Now" />
|
||||
<TextField name={`${namePrefix}.duration`} label="Durée" placeholder="4 ans" />
|
||||
<SelectField name={`${namePrefix}.type`} label="Type" options={TYPE_OPTIONS} />
|
||||
</div>
|
||||
<TextAreaField name={`${namePrefix}.description`} label="Description" rows={3} />
|
||||
<ChipsField name={`${namePrefix}.missions`} label="Missions" hint="Une par chip" />
|
||||
<ChipsField name={`${namePrefix}.stack`} label="Stack" hint="Technos utilisées" />
|
||||
<ToggleField name={`${namePrefix}.current`} label="Poste en cours (NOW PLAYING)" />
|
||||
</>
|
||||
)}
|
||||
</ArrayEditor>
|
||||
</Section>
|
||||
|
||||
<Section title="Compétences globales" subtitle="// patch bay">
|
||||
<ChipsField name="skills.technical" label="Techniques" />
|
||||
<ChipsField name="skills.soft" label="Transverses / soft" />
|
||||
</Section>
|
||||
</FormShell>
|
||||
);
|
||||
}
|
||||
55
src/components/admin/forms/FormationsForm.tsx
Normal file
55
src/components/admin/forms/FormationsForm.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { FormationsSchema, type Formations } from '../../../content/schemas/formations';
|
||||
import { FormShell } from '../FormShell';
|
||||
import { Section } from '../Section';
|
||||
import { TextField } from '../fields/TextField';
|
||||
import { TextAreaField } from '../fields/TextAreaField';
|
||||
import { ArrayEditor } from '../fields/ArrayEditor';
|
||||
|
||||
type Props = { defaultValues: Formations };
|
||||
|
||||
export function FormationsForm({ defaultValues }: Props) {
|
||||
return (
|
||||
<FormShell<Formations>
|
||||
section="formations"
|
||||
defaultValues={defaultValues as never}
|
||||
resolver={zodResolver(FormationsSchema) as never}
|
||||
>
|
||||
<Section title="Formations" subtitle="// edu system">
|
||||
<ArrayEditor
|
||||
name="items"
|
||||
label="Cursus"
|
||||
newItem={() =>
|
||||
({
|
||||
id: '',
|
||||
title: '',
|
||||
school: '',
|
||||
period: '',
|
||||
}) as never
|
||||
}
|
||||
itemLabel={(i) => `ticket #${String(i + 1).padStart(3, '0')}`}
|
||||
>
|
||||
{({ namePrefix }) => (
|
||||
<>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<TextField name={`${namePrefix}.id`} label="ID" hint="Slug unique" />
|
||||
<TextField name={`${namePrefix}.period`} label="Période" placeholder="2018 — 2020" />
|
||||
</div>
|
||||
<TextField name={`${namePrefix}.title`} label="Titre / diplôme" />
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<TextField name={`${namePrefix}.school`} label="École" />
|
||||
<TextField
|
||||
name={`${namePrefix}.schoolUrl`}
|
||||
label="Site école"
|
||||
type="url"
|
||||
placeholder="https://…"
|
||||
/>
|
||||
</div>
|
||||
<TextAreaField name={`${namePrefix}.description`} label="Description" rows={3} />
|
||||
</>
|
||||
)}
|
||||
</ArrayEditor>
|
||||
</Section>
|
||||
</FormShell>
|
||||
);
|
||||
}
|
||||
51
src/components/admin/forms/InterestsForm.tsx
Normal file
51
src/components/admin/forms/InterestsForm.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { InterestsSchema, type Interests } from '../../../content/schemas/interests';
|
||||
import { FormShell } from '../FormShell';
|
||||
import { Section } from '../Section';
|
||||
import { TextField } from '../fields/TextField';
|
||||
import { SelectField } from '../fields/SelectField';
|
||||
import { EmojiField } from '../fields/EmojiField';
|
||||
import { ArrayEditor } from '../fields/ArrayEditor';
|
||||
|
||||
type Props = { defaultValues: Interests };
|
||||
|
||||
const COLOR_OPTIONS = [
|
||||
{ value: 'cyan', label: '◉ cyan' },
|
||||
{ value: 'magenta', label: '◉ magenta' },
|
||||
{ value: 'violet', label: '◉ violet' },
|
||||
{ value: 'ice', label: '◉ ice' },
|
||||
];
|
||||
|
||||
export function InterestsForm({ defaultValues }: Props) {
|
||||
return (
|
||||
<FormShell<Interests>
|
||||
section="interests"
|
||||
defaultValues={defaultValues as never}
|
||||
resolver={zodResolver(InterestsSchema) as never}
|
||||
>
|
||||
<Section title="Centres d'intérêt" subtitle="// off decks">
|
||||
<ArrayEditor
|
||||
name="items"
|
||||
label="Items"
|
||||
newItem={() =>
|
||||
({ id: '', label: '', icon: '🎧', color: 'cyan' as const }) as never
|
||||
}
|
||||
itemLabel={(i) => `pad #${String(i + 1).padStart(2, '0')}`}
|
||||
>
|
||||
{({ namePrefix }) => (
|
||||
<>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<TextField name={`${namePrefix}.id`} label="ID" hint="Slug unique" />
|
||||
<TextField name={`${namePrefix}.label`} label="Label" />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<EmojiField name={`${namePrefix}.icon`} label="Icône" />
|
||||
<SelectField name={`${namePrefix}.color`} label="Couleur" options={COLOR_OPTIONS} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</ArrayEditor>
|
||||
</Section>
|
||||
</FormShell>
|
||||
);
|
||||
}
|
||||
107
src/components/admin/forms/ProfileForm.tsx
Normal file
107
src/components/admin/forms/ProfileForm.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { ProfileSchema, type Profile } from '../../../content/schemas/profile';
|
||||
import { FormShell } from '../FormShell';
|
||||
import { Section } from '../Section';
|
||||
import { TextField } from '../fields/TextField';
|
||||
import { TextAreaField } from '../fields/TextAreaField';
|
||||
import { SelectField } from '../fields/SelectField';
|
||||
import { ToggleField } from '../fields/ToggleField';
|
||||
import { ChipsField } from '../fields/ChipsField';
|
||||
import { ImageUploadField } from '../fields/ImageUploadField';
|
||||
import { FileUploadField } from '../fields/FileUploadField';
|
||||
import { ArrayEditor } from '../fields/ArrayEditor';
|
||||
|
||||
type Props = { defaultValues: Profile };
|
||||
|
||||
export function ProfileForm({ defaultValues }: Props) {
|
||||
return (
|
||||
<FormShell<Profile>
|
||||
section="profile"
|
||||
defaultValues={defaultValues as never}
|
||||
resolver={zodResolver(ProfileSchema) as never}
|
||||
>
|
||||
<Section title="Identité" subtitle="// owner">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<TextField name="name" label="Nom" />
|
||||
<TextField name="title" label="Titre" />
|
||||
</div>
|
||||
<TextField name="tagline" label="Tagline" hint="Phrase courte affichée sous le nom" />
|
||||
<TextAreaField name="bio" label="Bio" rows={5} />
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 items-end">
|
||||
<ToggleField name="available" label="Disponible" hint="Affiche le badge ON AIR" />
|
||||
<TextField name="availableLabel" label="Texte du badge" hint="ex : Disponible — Nantes" />
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section title="Hero" subtitle="// stage">
|
||||
<ImageUploadField name="hero.photo" label="Photo principale" folder="pp" />
|
||||
<ChipsField
|
||||
name="hero.roles"
|
||||
label="Rôles affichés"
|
||||
hint="Mots-clés morphés dans le hero (ex : dev, DJ, dev)"
|
||||
/>
|
||||
<ArrayEditor
|
||||
name="hero.photos"
|
||||
label="Galerie hero"
|
||||
hint="Photos additionnelles (slideshow / mosaïque)"
|
||||
newItem={() => ''}
|
||||
itemLabel={(i) => `photo #${String(i + 1).padStart(2, '0')}`}
|
||||
>
|
||||
{({ namePrefix }) => (
|
||||
<ImageUploadField name={namePrefix} label="URL" folder="pp" />
|
||||
)}
|
||||
</ArrayEditor>
|
||||
<ArrayEditor
|
||||
name="hero.cta"
|
||||
label="Boutons CTA"
|
||||
hint="Actions principales sous le hero"
|
||||
newItem={() => ({ label: '', href: '', variant: 'primary' as const, external: false })}
|
||||
itemLabel={(i) => `cta #${String(i + 1).padStart(2, '0')}`}
|
||||
>
|
||||
{({ namePrefix }) => (
|
||||
<>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<TextField name={`${namePrefix}.label`} label="Label" />
|
||||
<TextField name={`${namePrefix}.href`} label="Lien" placeholder="/projets ou https://…" />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 items-end">
|
||||
<SelectField
|
||||
name={`${namePrefix}.variant`}
|
||||
label="Style"
|
||||
options={[
|
||||
{ value: 'primary', label: 'Primary (acid)' },
|
||||
{ value: 'ghost', label: 'Ghost (cyan)' },
|
||||
]}
|
||||
/>
|
||||
<ToggleField
|
||||
name={`${namePrefix}.external`}
|
||||
label="Externe (target=_blank)"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</ArrayEditor>
|
||||
</Section>
|
||||
|
||||
<Section title="Réseaux" subtitle="// patch bay">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<TextField name="socials.email" label="Email" type="email" placeholder="hello@example.com" />
|
||||
<TextField name="socials.github" label="GitHub" type="url" placeholder="https://github.com/…" />
|
||||
<TextField name="socials.gitea" label="Gitea" type="url" />
|
||||
<TextField name="socials.linkedin" label="LinkedIn" type="url" />
|
||||
<TextField name="socials.website" label="Site personnel" type="url" />
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section title="CV" subtitle="// dossier">
|
||||
<FileUploadField
|
||||
name="cv"
|
||||
label="Fichier PDF du CV"
|
||||
folder="cv"
|
||||
kind="document"
|
||||
hint="Glisser-déposer un PDF (max 8 Mo)"
|
||||
/>
|
||||
</Section>
|
||||
</FormShell>
|
||||
);
|
||||
}
|
||||
79
src/components/admin/forms/ProjectsForm.tsx
Normal file
79
src/components/admin/forms/ProjectsForm.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { ProjectsSchema, type Projects } from '../../../content/schemas/projects';
|
||||
import { FormShell } from '../FormShell';
|
||||
import { Section } from '../Section';
|
||||
import { TextField } from '../fields/TextField';
|
||||
import { TextAreaField } from '../fields/TextAreaField';
|
||||
import { SelectField } from '../fields/SelectField';
|
||||
import { ToggleField } from '../fields/ToggleField';
|
||||
import { ChipsField } from '../fields/ChipsField';
|
||||
import { ImageUploadField } from '../fields/ImageUploadField';
|
||||
import { ArrayEditor } from '../fields/ArrayEditor';
|
||||
|
||||
type Props = { defaultValues: Projects };
|
||||
|
||||
const CATEGORY_OPTIONS = [
|
||||
{ value: 'web', label: 'Web' },
|
||||
{ value: 'mobile', label: 'Mobile' },
|
||||
{ value: 'bot', label: 'Bot' },
|
||||
{ value: 'infra', label: 'Infra' },
|
||||
{ value: 'other', label: 'Autre' },
|
||||
];
|
||||
|
||||
const STATUS_OPTIONS = [
|
||||
{ value: 'live', label: 'Live' },
|
||||
{ value: 'archived', label: 'Archived' },
|
||||
{ value: 'private', label: 'Private' },
|
||||
{ value: 'wip', label: 'WIP' },
|
||||
];
|
||||
|
||||
export function ProjectsForm({ defaultValues }: Props) {
|
||||
return (
|
||||
<FormShell<Projects>
|
||||
section="projects"
|
||||
defaultValues={defaultValues as never}
|
||||
resolver={zodResolver(ProjectsSchema) as never}
|
||||
>
|
||||
<Section title="Projets" subtitle="// crate">
|
||||
<ArrayEditor
|
||||
name="items"
|
||||
label="Projets"
|
||||
newItem={() =>
|
||||
({
|
||||
id: '',
|
||||
title: '',
|
||||
description: '',
|
||||
category: 'web' as const,
|
||||
status: 'live' as const,
|
||||
stack: [],
|
||||
featured: false,
|
||||
}) as never
|
||||
}
|
||||
itemLabel={(i) => `skeud #${String(i + 1).padStart(2, '0')}`}
|
||||
>
|
||||
{({ namePrefix }) => (
|
||||
<>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<TextField name={`${namePrefix}.id`} label="ID" hint="Slug unique" />
|
||||
<TextField name={`${namePrefix}.title`} label="Titre" />
|
||||
</div>
|
||||
<TextField name={`${namePrefix}.subtitle`} label="Sous-titre" />
|
||||
<TextAreaField name={`${namePrefix}.description`} label="Description" rows={3} />
|
||||
<ImageUploadField name={`${namePrefix}.image`} label="Pochette" folder="projet" />
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||
<SelectField name={`${namePrefix}.category`} label="Catégorie" options={CATEGORY_OPTIONS} />
|
||||
<SelectField name={`${namePrefix}.status`} label="Statut" options={STATUS_OPTIONS} />
|
||||
<ToggleField name={`${namePrefix}.featured`} label="Mis en avant" />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<TextField name={`${namePrefix}.url`} label="URL live" type="url" />
|
||||
<TextField name={`${namePrefix}.repoUrl`} label="URL repo" type="url" />
|
||||
</div>
|
||||
<ChipsField name={`${namePrefix}.stack`} label="Stack" />
|
||||
</>
|
||||
)}
|
||||
</ArrayEditor>
|
||||
</Section>
|
||||
</FormShell>
|
||||
);
|
||||
}
|
||||
39
src/components/admin/forms/SiteForm.tsx
Normal file
39
src/components/admin/forms/SiteForm.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { SiteSchema, type Site } from '../../../content/schemas/site';
|
||||
import { FormShell } from '../FormShell';
|
||||
import { Section } from '../Section';
|
||||
import { TextField } from '../fields/TextField';
|
||||
import { TextAreaField } from '../fields/TextAreaField';
|
||||
import { ToggleField } from '../fields/ToggleField';
|
||||
import { ChipsField } from '../fields/ChipsField';
|
||||
import { ImageUploadField } from '../fields/ImageUploadField';
|
||||
|
||||
type Props = { defaultValues: Site };
|
||||
|
||||
export function SiteForm({ defaultValues }: Props) {
|
||||
return (
|
||||
<FormShell<Site>
|
||||
section="site"
|
||||
defaultValues={defaultValues as never}
|
||||
resolver={zodResolver(SiteSchema) as never}
|
||||
>
|
||||
<Section title="Méta" subtitle="// SEO">
|
||||
<TextField name="title" label="Titre du site" />
|
||||
<TextAreaField name="description" label="Description" rows={3} />
|
||||
<TextField name="url" label="URL canonique" type="url" />
|
||||
<ChipsField name="keywords" label="Keywords" hint="Mots-clés SEO" />
|
||||
<ImageUploadField name="ogImage" label="OG image" folder="" hint="Affichée lors d'un partage social" />
|
||||
<TextField name="twitter" label="Handle Twitter / X" placeholder="@johanleroy" />
|
||||
</Section>
|
||||
|
||||
<Section title="Features" subtitle="// flags">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<ToggleField name="features.hero3D" label="Hero 3D" />
|
||||
<ToggleField name="features.smoothScroll" label="Smooth scroll (Lenis)" />
|
||||
<ToggleField name="features.customCursor" label="Curseur custom" />
|
||||
<ToggleField name="features.partyMode" label="Party mode (Konami)" />
|
||||
</div>
|
||||
</Section>
|
||||
</FormShell>
|
||||
);
|
||||
}
|
||||
74
src/components/admin/forms/SkillsForm.tsx
Normal file
74
src/components/admin/forms/SkillsForm.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { SkillsSchema, type Skills } from '../../../content/schemas/skills';
|
||||
import { FormShell } from '../FormShell';
|
||||
import { Section } from '../Section';
|
||||
import { TextField } from '../fields/TextField';
|
||||
import { SelectField } from '../fields/SelectField';
|
||||
import { EmojiField } from '../fields/EmojiField';
|
||||
import { ArrayEditor } from '../fields/ArrayEditor';
|
||||
|
||||
type Props = { defaultValues: Skills };
|
||||
|
||||
const LEVEL_OPTIONS = [
|
||||
{ value: 'primary', label: 'Primary' },
|
||||
{ value: 'secondary', label: 'Secondary' },
|
||||
];
|
||||
|
||||
function CategoryArray({ root, label, subtitle }: { root: 'primary' | 'secondary'; label: string; subtitle?: string }) {
|
||||
return (
|
||||
<Section title={label} subtitle={subtitle}>
|
||||
<ArrayEditor
|
||||
name={root}
|
||||
label="Catégories"
|
||||
newItem={() => ({ id: '', label: '', items: [] as { name: string; icon: string; level: 'primary' | 'secondary' }[] })}
|
||||
itemLabel={(i) => `catégorie #${String(i + 1).padStart(2, '0')}`}
|
||||
>
|
||||
{({ namePrefix }) => (
|
||||
<>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<TextField name={`${namePrefix}.id`} label="ID" hint="Slug interne, ex : front, devops" />
|
||||
<TextField name={`${namePrefix}.label`} label="Label affiché" />
|
||||
</div>
|
||||
<ArrayEditor
|
||||
name={`${namePrefix}.items`}
|
||||
label="Skills"
|
||||
newItem={() => ({ name: '', icon: '⚡', level: 'primary' as const })}
|
||||
itemLabel={(i) => `skill #${String(i + 1).padStart(2, '0')}`}
|
||||
>
|
||||
{({ namePrefix: skillName }) => (
|
||||
<>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<TextField name={`${skillName}.name`} label="Nom" />
|
||||
<SelectField
|
||||
name={`${skillName}.level`}
|
||||
label="Niveau"
|
||||
options={LEVEL_OPTIONS}
|
||||
/>
|
||||
</div>
|
||||
<EmojiField
|
||||
name={`${skillName}.icon`}
|
||||
label="Icône"
|
||||
hint="Emoji ou caractère court (slug iconify supporté)"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</ArrayEditor>
|
||||
</>
|
||||
)}
|
||||
</ArrayEditor>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
export function SkillsForm({ defaultValues }: Props) {
|
||||
return (
|
||||
<FormShell<Skills>
|
||||
section="skills"
|
||||
defaultValues={defaultValues as never}
|
||||
resolver={zodResolver(SkillsSchema) as never}
|
||||
>
|
||||
<CategoryArray root="primary" label="Stack primaire" subtitle="// rack 1" />
|
||||
<CategoryArray root="secondary" label="Stack secondaire" subtitle="// rack 2" />
|
||||
</FormShell>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user