refactor(ui): unify low-level controls and expand design system
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 2m57s
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 2m57s
This commit is contained in:
@@ -4,7 +4,7 @@ import { useState } from 'react';
|
|||||||
import { signIn } from 'next-auth/react';
|
import { signIn } from 'next-auth/react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { RocketIcon } from '@/components/ui';
|
import { Button, Input, RocketIcon } from '@/components/ui';
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -62,42 +62,32 @@ export default function LoginPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label htmlFor="email" className="mb-2 block text-sm font-medium text-foreground">
|
<Input
|
||||||
Email
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="email"
|
id="email"
|
||||||
name="email"
|
name="email"
|
||||||
type="email"
|
type="email"
|
||||||
|
label="Email"
|
||||||
required
|
required
|
||||||
autoComplete="email"
|
autoComplete="email"
|
||||||
className="w-full rounded-lg border border-input-border bg-input px-4 py-2.5 text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
|
|
||||||
placeholder="vous@exemple.com"
|
placeholder="vous@exemple.com"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<label htmlFor="password" className="mb-2 block text-sm font-medium text-foreground">
|
<Input
|
||||||
Mot de passe
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="password"
|
id="password"
|
||||||
name="password"
|
name="password"
|
||||||
type="password"
|
type="password"
|
||||||
|
label="Mot de passe"
|
||||||
required
|
required
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
className="w-full rounded-lg border border-input-border bg-input px-4 py-2.5 text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
|
|
||||||
placeholder="••••••••"
|
placeholder="••••••••"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<Button type="submit" disabled={loading} loading={loading} className="w-full">
|
||||||
type="submit"
|
|
||||||
disabled={loading}
|
|
||||||
className="w-full rounded-lg bg-primary px-4 py-2.5 font-semibold text-primary-foreground transition-colors hover:bg-primary-hover disabled:cursor-not-allowed disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{loading ? 'Connexion...' : 'Se connecter'}
|
{loading ? 'Connexion...' : 'Se connecter'}
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
<p className="mt-6 text-center text-sm text-muted">
|
<p className="mt-6 text-center text-sm text-muted">
|
||||||
Pas encore de compte ?{' '}
|
Pas encore de compte ?{' '}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useState } from 'react';
|
|||||||
import { signIn } from 'next-auth/react';
|
import { signIn } from 'next-auth/react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { RocketIcon } from '@/components/ui';
|
import { Button, Input, RocketIcon } from '@/components/ui';
|
||||||
|
|
||||||
export default function RegisterPage() {
|
export default function RegisterPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -91,74 +91,55 @@ export default function RegisterPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label htmlFor="name" className="mb-2 block text-sm font-medium text-foreground">
|
<Input
|
||||||
Nom
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="name"
|
id="name"
|
||||||
name="name"
|
name="name"
|
||||||
type="text"
|
type="text"
|
||||||
|
label="Nom"
|
||||||
autoComplete="name"
|
autoComplete="name"
|
||||||
className="w-full rounded-lg border border-input-border bg-input px-4 py-2.5 text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
|
|
||||||
placeholder="Jean Dupont"
|
placeholder="Jean Dupont"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label htmlFor="email" className="mb-2 block text-sm font-medium text-foreground">
|
<Input
|
||||||
Email
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="email"
|
id="email"
|
||||||
name="email"
|
name="email"
|
||||||
type="email"
|
type="email"
|
||||||
|
label="Email"
|
||||||
required
|
required
|
||||||
autoComplete="email"
|
autoComplete="email"
|
||||||
className="w-full rounded-lg border border-input-border bg-input px-4 py-2.5 text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
|
|
||||||
placeholder="vous@exemple.com"
|
placeholder="vous@exemple.com"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label htmlFor="password" className="mb-2 block text-sm font-medium text-foreground">
|
<Input
|
||||||
Mot de passe
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="password"
|
id="password"
|
||||||
name="password"
|
name="password"
|
||||||
type="password"
|
type="password"
|
||||||
|
label="Mot de passe"
|
||||||
required
|
required
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
className="w-full rounded-lg border border-input-border bg-input px-4 py-2.5 text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
|
|
||||||
placeholder="••••••••"
|
placeholder="••••••••"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<label
|
<Input
|
||||||
htmlFor="confirmPassword"
|
|
||||||
className="mb-2 block text-sm font-medium text-foreground"
|
|
||||||
>
|
|
||||||
Confirmer le mot de passe
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="confirmPassword"
|
id="confirmPassword"
|
||||||
name="confirmPassword"
|
name="confirmPassword"
|
||||||
type="password"
|
type="password"
|
||||||
|
label="Confirmer le mot de passe"
|
||||||
required
|
required
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
className="w-full rounded-lg border border-input-border bg-input px-4 py-2.5 text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
|
|
||||||
placeholder="••••••••"
|
placeholder="••••••••"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<Button type="submit" disabled={loading} loading={loading} className="w-full">
|
||||||
type="submit"
|
|
||||||
disabled={loading}
|
|
||||||
className="w-full rounded-lg bg-primary px-4 py-2.5 font-semibold text-primary-foreground transition-colors hover:bg-primary-hover disabled:cursor-not-allowed disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{loading ? 'Création...' : 'Créer mon compte'}
|
{loading ? 'Création...' : 'Créer mon compte'}
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
<p className="mt-6 text-center text-sm text-muted">
|
<p className="mt-6 text-center text-sm text-muted">
|
||||||
Déjà un compte ?{' '}
|
Déjà un compte ?{' '}
|
||||||
|
|||||||
525
src/app/design-system/page.tsx
Normal file
525
src/app/design-system/page.tsx
Normal file
@@ -0,0 +1,525 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
Badge,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardFooter,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
CollaboratorDisplay,
|
||||||
|
DateInput,
|
||||||
|
Disclosure,
|
||||||
|
DropdownMenu,
|
||||||
|
EditableGifMoodTitle,
|
||||||
|
EditableMotivatorTitle,
|
||||||
|
EditableSessionTitle,
|
||||||
|
EditableTitle,
|
||||||
|
EditableWeatherTitle,
|
||||||
|
EditableWeeklyCheckInTitle,
|
||||||
|
EditableYearReviewTitle,
|
||||||
|
InlineFormActions,
|
||||||
|
Input,
|
||||||
|
IconCheck,
|
||||||
|
IconClose,
|
||||||
|
IconDuplicate,
|
||||||
|
IconEdit,
|
||||||
|
IconButton,
|
||||||
|
IconPlus,
|
||||||
|
IconTrash,
|
||||||
|
Modal,
|
||||||
|
ModalFooter,
|
||||||
|
PageHeader,
|
||||||
|
ParticipantInput,
|
||||||
|
RocketIcon,
|
||||||
|
Select,
|
||||||
|
SegmentedControl,
|
||||||
|
SessionPageHeader,
|
||||||
|
Textarea,
|
||||||
|
ToggleGroup,
|
||||||
|
FormField,
|
||||||
|
NumberInput,
|
||||||
|
} from '@/components/ui';
|
||||||
|
|
||||||
|
const BUTTON_VARIANTS = [
|
||||||
|
'primary',
|
||||||
|
'secondary',
|
||||||
|
'outline',
|
||||||
|
'ghost',
|
||||||
|
'destructive',
|
||||||
|
'brand',
|
||||||
|
] as const;
|
||||||
|
const BUTTON_SIZES = ['sm', 'md', 'lg'] as const;
|
||||||
|
|
||||||
|
const BADGE_VARIANTS = [
|
||||||
|
'default',
|
||||||
|
'primary',
|
||||||
|
'strength',
|
||||||
|
'weakness',
|
||||||
|
'opportunity',
|
||||||
|
'threat',
|
||||||
|
'success',
|
||||||
|
'warning',
|
||||||
|
'destructive',
|
||||||
|
'accent',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const SELECT_OPTIONS = [
|
||||||
|
{ value: 'editor', label: 'Editeur' },
|
||||||
|
{ value: 'viewer', label: 'Lecteur' },
|
||||||
|
{ value: 'admin', label: 'Admin' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const SECTION_LINKS = [
|
||||||
|
{ id: 'buttons', label: 'Buttons' },
|
||||||
|
{ id: 'badges', label: 'Badges' },
|
||||||
|
{ id: 'icon-button', label: 'IconButton' },
|
||||||
|
{ id: 'form-inputs', label: 'Form Inputs' },
|
||||||
|
{ id: 'select-toggle', label: 'Select & Toggle' },
|
||||||
|
{ id: 'form-field', label: 'FormField / Date / Number' },
|
||||||
|
{ id: 'cards', label: 'Cards' },
|
||||||
|
{ id: 'avatars', label: 'Avatar & Collaborators' },
|
||||||
|
{ id: 'disclosure-dropdown', label: 'Disclosure & Dropdown' },
|
||||||
|
{ id: 'menu', label: 'Menu' },
|
||||||
|
{ id: 'editable-titles', label: 'Editable Titles' },
|
||||||
|
{ id: 'session-header', label: 'Session Header' },
|
||||||
|
{ id: 'participant-input', label: 'ParticipantInput' },
|
||||||
|
{ id: 'icons', label: 'Icons' },
|
||||||
|
{ id: 'modal', label: 'Modal' },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export default function DesignSystemPage() {
|
||||||
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
|
const [toggleValue, setToggleValue] = useState<'cards' | 'table' | 'list'>('cards');
|
||||||
|
const [selectMd, setSelectMd] = useState('editor');
|
||||||
|
const [selectSm, setSelectSm] = useState('viewer');
|
||||||
|
const [selectXs, setSelectXs] = useState('admin');
|
||||||
|
const [selectLg, setSelectLg] = useState('editor');
|
||||||
|
const [menuCount, setMenuCount] = useState(0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="mx-auto max-w-7xl px-4 py-8">
|
||||||
|
<PageHeader
|
||||||
|
emoji="🎨"
|
||||||
|
title="Design System"
|
||||||
|
subtitle="Guide visuel des composants UI et de leurs variantes"
|
||||||
|
actions={
|
||||||
|
<Button variant="brand" size="sm">
|
||||||
|
Action principale
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="grid items-start gap-8" style={{ gridTemplateColumns: '240px minmax(0, 1fr)' }}>
|
||||||
|
<aside>
|
||||||
|
<Card className="sticky top-20 p-4">
|
||||||
|
<p className="mb-3 text-sm font-medium text-foreground">Menu de la page</p>
|
||||||
|
<nav className="flex flex-col gap-1.5">
|
||||||
|
{SECTION_LINKS.map((section) => (
|
||||||
|
<a
|
||||||
|
key={section.id}
|
||||||
|
href={`#${section.id}`}
|
||||||
|
className="rounded-md px-2.5 py-1.5 text-sm text-muted transition-colors hover:bg-card-hover hover:text-foreground"
|
||||||
|
>
|
||||||
|
{section.label}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</Card>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<div className="space-y-8">
|
||||||
|
<Card id="buttons" className="p-6">
|
||||||
|
<h2 className="mb-4 text-xl font-semibold text-foreground">Buttons</h2>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{BUTTON_SIZES.map((size) => (
|
||||||
|
<div key={size} className="flex flex-wrap items-center gap-3">
|
||||||
|
<span className="w-12 text-xs uppercase tracking-wide text-muted">{size}</span>
|
||||||
|
{BUTTON_VARIANTS.map((variant) => (
|
||||||
|
<Button key={`${size}-${variant}`} variant={variant} size={size}>
|
||||||
|
{variant}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className="flex flex-wrap items-center gap-3 border-t border-border pt-4">
|
||||||
|
<Button loading>Chargement</Button>
|
||||||
|
<Button disabled>Desactive</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card id="badges" className="p-6">
|
||||||
|
<h2 className="mb-4 text-xl font-semibold text-foreground">Badges</h2>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{BADGE_VARIANTS.map((variant) => (
|
||||||
|
<Badge key={variant} variant={variant}>
|
||||||
|
{variant}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card id="icon-button" className="p-6">
|
||||||
|
<h2 className="mb-4 text-xl font-semibold text-foreground">IconButton</h2>
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
<IconButton icon={<IconEdit />} label="Edit" />
|
||||||
|
<IconButton icon={<IconDuplicate />} label="Duplicate" variant="primary" />
|
||||||
|
<IconButton icon={<IconTrash />} label="Delete" variant="destructive" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card id="form-inputs" className="p-6">
|
||||||
|
<h2 className="mb-4 text-xl font-semibold text-foreground">Form Inputs</h2>
|
||||||
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
|
<Input label="Input standard" placeholder="Votre texte" />
|
||||||
|
<Input label="Input avec erreur" defaultValue="Valeur invalide" error="Champ invalide" />
|
||||||
|
<Textarea label="Textarea standard" placeholder="Votre description" rows={3} />
|
||||||
|
<Textarea
|
||||||
|
label="Textarea avec erreur"
|
||||||
|
defaultValue="Texte"
|
||||||
|
rows={3}
|
||||||
|
error="Description trop courte"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card id="select-toggle" className="p-6">
|
||||||
|
<h2 className="mb-4 text-xl font-semibold text-foreground">Select & ToggleGroup</h2>
|
||||||
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Select
|
||||||
|
label="Select XS"
|
||||||
|
size="xs"
|
||||||
|
value={selectXs}
|
||||||
|
onChange={(e) => setSelectXs(e.target.value)}
|
||||||
|
options={SELECT_OPTIONS}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
label="Select SM"
|
||||||
|
size="sm"
|
||||||
|
value={selectSm}
|
||||||
|
onChange={(e) => setSelectSm(e.target.value)}
|
||||||
|
options={SELECT_OPTIONS}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
label="Select MD"
|
||||||
|
size="md"
|
||||||
|
value={selectMd}
|
||||||
|
onChange={(e) => setSelectMd(e.target.value)}
|
||||||
|
options={SELECT_OPTIONS}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
label="Select LG"
|
||||||
|
size="lg"
|
||||||
|
value={selectLg}
|
||||||
|
onChange={(e) => setSelectLg(e.target.value)}
|
||||||
|
options={SELECT_OPTIONS}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<p className="text-sm font-medium text-foreground">Toggle group</p>
|
||||||
|
<ToggleGroup
|
||||||
|
value={toggleValue}
|
||||||
|
onChange={setToggleValue}
|
||||||
|
options={[
|
||||||
|
{ value: 'cards', label: 'Cards' },
|
||||||
|
{ value: 'table', label: 'Table' },
|
||||||
|
{ value: 'list', label: 'List' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<p className="text-sm text-muted">Valeur active: {toggleValue}</p>
|
||||||
|
<p className="pt-2 text-sm font-medium text-foreground">Segmented control</p>
|
||||||
|
<SegmentedControl
|
||||||
|
value={toggleValue}
|
||||||
|
onChange={setToggleValue}
|
||||||
|
options={[
|
||||||
|
{ value: 'cards', label: 'Cards' },
|
||||||
|
{ value: 'table', label: 'Table' },
|
||||||
|
{ value: 'list', label: 'List' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card id="form-field" className="p-6">
|
||||||
|
<h2 className="mb-4 text-xl font-semibold text-foreground">FormField / Date / Number</h2>
|
||||||
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
|
<FormField label="FormField">
|
||||||
|
<Input placeholder="Control custom" />
|
||||||
|
</FormField>
|
||||||
|
<DateInput label="DateInput" defaultValue="2026-03-03" />
|
||||||
|
<NumberInput label="NumberInput" defaultValue={42} />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card id="cards" className="p-6">
|
||||||
|
<h2 className="mb-4 text-xl font-semibold text-foreground">Cards & Header blocks</h2>
|
||||||
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
|
<Card hover>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Card title</CardTitle>
|
||||||
|
<CardDescription>Description secondaire</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-sm text-muted">Contenu principal de la card.</p>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter className="justify-end">
|
||||||
|
<Button size="sm" variant="outline">
|
||||||
|
Annuler
|
||||||
|
</Button>
|
||||||
|
<Button size="sm">Valider</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-4">
|
||||||
|
<h3 className="mb-3 font-medium text-foreground">Inline actions</h3>
|
||||||
|
<Input placeholder="Exemple inline" className="mb-2" />
|
||||||
|
<InlineFormActions
|
||||||
|
onCancel={() => {}}
|
||||||
|
onSubmit={() => {}}
|
||||||
|
isPending={false}
|
||||||
|
submitLabel="Ajouter"
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card id="avatars" className="p-6">
|
||||||
|
<h2 className="mb-4 text-xl font-semibold text-foreground">Avatar & Collaborators</h2>
|
||||||
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Avatar email="jane.doe@example.com" name="Jane Doe" size={40} />
|
||||||
|
<Avatar email="john.smith@example.com" name="John Smith" size={32} />
|
||||||
|
<Avatar email="team@example.com" size={24} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<CollaboratorDisplay
|
||||||
|
collaborator={{
|
||||||
|
raw: 'Jane Doe',
|
||||||
|
matchedUser: {
|
||||||
|
id: '1',
|
||||||
|
email: 'jane.doe@example.com',
|
||||||
|
name: 'Jane Doe',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
showEmail
|
||||||
|
/>
|
||||||
|
<CollaboratorDisplay
|
||||||
|
collaborator={{
|
||||||
|
raw: 'Intervenant externe',
|
||||||
|
matchedUser: null,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card id="disclosure-dropdown" className="p-6">
|
||||||
|
<h2 className="mb-4 text-xl font-semibold text-foreground">Disclosure & Dropdown</h2>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Disclosure title="Panneau pliable" subtitle="Composant Disclosure">
|
||||||
|
<p className="text-sm text-muted">Contenu du panneau.</p>
|
||||||
|
</Disclosure>
|
||||||
|
<DropdownMenu
|
||||||
|
panelClassName="mt-2 w-56 rounded-lg border border-border bg-card p-2 shadow-lg"
|
||||||
|
trigger={({ open, toggle }) => (
|
||||||
|
<Button type="button" variant="outline" onClick={toggle}>
|
||||||
|
Menu demo {open ? '▲' : '▼'}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{({ close }) => (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
setMenuCount((prev) => prev + 1);
|
||||||
|
close();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Incrementer ({menuCount})
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card id="menu" className="p-6">
|
||||||
|
<h2 className="mb-4 text-xl font-semibold text-foreground">Menu</h2>
|
||||||
|
<DropdownMenu
|
||||||
|
panelClassName="mt-2 w-64 overflow-hidden rounded-lg border border-border bg-card py-1 shadow-lg"
|
||||||
|
trigger={({ open, toggle }) => (
|
||||||
|
<Button type="button" variant="outline" onClick={toggle}>
|
||||||
|
Ouvrir le menu {open ? '▲' : '▼'}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{({ close }) => (
|
||||||
|
<>
|
||||||
|
<div className="border-b border-border px-4 py-2">
|
||||||
|
<p className="text-xs text-muted">MENU DE DEMO</p>
|
||||||
|
<p className="text-sm font-medium text-foreground">Navigation rapide</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={close}
|
||||||
|
className="block w-full px-4 py-2 text-left text-sm text-foreground hover:bg-card-hover"
|
||||||
|
>
|
||||||
|
👤 Mon profil
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={close}
|
||||||
|
className="block w-full px-4 py-2 text-left text-sm text-foreground hover:bg-card-hover"
|
||||||
|
>
|
||||||
|
👥 Equipes
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={close}
|
||||||
|
className="block w-full px-4 py-2 text-left text-sm text-destructive hover:bg-card-hover"
|
||||||
|
>
|
||||||
|
Se deconnecter
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DropdownMenu>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card id="editable-titles" className="p-6">
|
||||||
|
<h2 className="mb-4 text-xl font-semibold text-foreground">Editable Titles</h2>
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<p className="mb-2 text-sm font-medium text-foreground">EditableTitle (base)</p>
|
||||||
|
<EditableTitle
|
||||||
|
sessionId="demo-editable-title"
|
||||||
|
initialTitle="Titre modifiable (cliquez pour tester)"
|
||||||
|
canEdit
|
||||||
|
onUpdate={async () => ({ success: true })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
|
<EditableSessionTitle
|
||||||
|
sessionId="demo-session-title"
|
||||||
|
initialTitle="Session title wrapper"
|
||||||
|
canEdit={false}
|
||||||
|
/>
|
||||||
|
<EditableMotivatorTitle
|
||||||
|
sessionId="demo-motivator-title"
|
||||||
|
initialTitle="Motivator title wrapper"
|
||||||
|
canEdit={false}
|
||||||
|
/>
|
||||||
|
<EditableYearReviewTitle
|
||||||
|
sessionId="demo-year-review-title"
|
||||||
|
initialTitle="Year review title wrapper"
|
||||||
|
canEdit={false}
|
||||||
|
/>
|
||||||
|
<EditableWeatherTitle
|
||||||
|
sessionId="demo-weather-title"
|
||||||
|
initialTitle="Weather title wrapper"
|
||||||
|
canEdit={false}
|
||||||
|
/>
|
||||||
|
<EditableWeeklyCheckInTitle
|
||||||
|
sessionId="demo-weekly-checkin-title"
|
||||||
|
initialTitle="Weekly check-in title wrapper"
|
||||||
|
canEdit={false}
|
||||||
|
/>
|
||||||
|
<EditableGifMoodTitle
|
||||||
|
sessionId="demo-gif-mood-title"
|
||||||
|
initialTitle="Gif mood title wrapper"
|
||||||
|
canEdit={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card id="session-header" className="p-6">
|
||||||
|
<h2 className="mb-4 text-xl font-semibold text-foreground">Session Header</h2>
|
||||||
|
<SessionPageHeader
|
||||||
|
workshopType="swot"
|
||||||
|
sessionId="demo-session"
|
||||||
|
sessionTitle="Atelier de demonstration"
|
||||||
|
isOwner={true}
|
||||||
|
canEdit={false}
|
||||||
|
ownerUser={{ name: 'Jane Doe', email: 'jane.doe@example.com' }}
|
||||||
|
date={new Date()}
|
||||||
|
collaborator={{
|
||||||
|
raw: 'Jane Doe',
|
||||||
|
matchedUser: {
|
||||||
|
id: '1',
|
||||||
|
email: 'jane.doe@example.com',
|
||||||
|
name: 'Jane Doe',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
badges={<Badge variant="primary">DEMO</Badge>}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card id="participant-input" className="p-6">
|
||||||
|
<h2 className="mb-4 text-xl font-semibold text-foreground">ParticipantInput</h2>
|
||||||
|
<ParticipantInput name="participant" />
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card id="icons" className="p-6">
|
||||||
|
<h2 className="mb-4 text-xl font-semibold text-foreground">Icons</h2>
|
||||||
|
<div className="flex flex-wrap items-center gap-4 text-foreground">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<IconEdit />
|
||||||
|
<span className="text-sm text-muted">Edit</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<IconTrash />
|
||||||
|
<span className="text-sm text-muted">Trash</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<IconDuplicate />
|
||||||
|
<span className="text-sm text-muted">Duplicate</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<IconPlus />
|
||||||
|
<span className="text-sm text-muted">Plus</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<IconCheck />
|
||||||
|
<span className="text-sm text-muted">Check</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<IconClose />
|
||||||
|
<span className="text-sm text-muted">Close</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RocketIcon className="h-5 w-5" />
|
||||||
|
<span className="text-sm text-muted">Rocket</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card id="modal" className="p-6">
|
||||||
|
<h2 className="mb-4 text-xl font-semibold text-foreground">Modal</h2>
|
||||||
|
<Button onClick={() => setModalOpen(true)}>Ouvrir la popup</Button>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Modal isOpen={modalOpen} onClose={() => setModalOpen(false)} title="Exemple de popup" size="md">
|
||||||
|
<p className="text-sm text-muted">
|
||||||
|
Ceci est un exemple de modal avec ses actions standardisees.
|
||||||
|
</p>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button variant="outline" onClick={() => setModalOpen(false)}>
|
||||||
|
Annuler
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => setModalOpen(false)}>Confirmer</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</Modal>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
CardDescription,
|
CardDescription,
|
||||||
CardContent,
|
CardContent,
|
||||||
Button,
|
Button,
|
||||||
|
DateInput,
|
||||||
Input,
|
Input,
|
||||||
} from '@/components/ui';
|
} from '@/components/ui';
|
||||||
import { createGifMoodSession } from '@/actions/gif-mood';
|
import { createGifMoodSession } from '@/actions/gif-mood';
|
||||||
@@ -78,20 +79,14 @@ export default function NewGifMoodPage() {
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div>
|
<DateInput
|
||||||
<label htmlFor="date" className="block text-sm font-medium text-foreground mb-1">
|
|
||||||
Date
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="date"
|
id="date"
|
||||||
name="date"
|
name="date"
|
||||||
type="date"
|
label="Date"
|
||||||
value={selectedDate}
|
value={selectedDate}
|
||||||
onChange={(e) => setSelectedDate(e.target.value)}
|
onChange={(e) => setSelectedDate(e.target.value)}
|
||||||
required
|
required
|
||||||
className="w-full rounded-lg border border-border bg-input px-3 py-2 text-foreground outline-none focus:border-primary focus:ring-2 focus:ring-primary/20"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="rounded-lg border border-border bg-card-hover p-4">
|
<div className="rounded-lg border border-border bg-card-hover p-4">
|
||||||
<h3 className="font-medium text-foreground mb-2">Comment ça marche ?</h3>
|
<h3 className="font-medium text-foreground mb-2">Comment ça marche ?</h3>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { auth } from '@/lib/auth';
|
|||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { getUserOKRs } from '@/services/okrs';
|
import { getUserOKRs } from '@/services/okrs';
|
||||||
import { Card, PageHeader } from '@/components/ui';
|
import { Card, PageHeader, getButtonClassName } from '@/components/ui';
|
||||||
import { ObjectivesList } from '@/components/okrs/ObjectivesList';
|
import { ObjectivesList } from '@/components/okrs/ObjectivesList';
|
||||||
import { comparePeriods } from '@/lib/okr-utils';
|
import { comparePeriods } from '@/lib/okr-utils';
|
||||||
|
|
||||||
@@ -46,10 +46,11 @@ export default async function ObjectivesPage() {
|
|||||||
Vous n'avez pas encore d'OKR défini. Contactez un administrateur d'équipe
|
Vous n'avez pas encore d'OKR défini. Contactez un administrateur d'équipe
|
||||||
pour en créer.
|
pour en créer.
|
||||||
</p>
|
</p>
|
||||||
<Link href="/teams">
|
<Link
|
||||||
<span className="inline-block rounded-lg bg-[var(--purple)] px-4 py-2 text-white hover:opacity-90">
|
href="/teams"
|
||||||
|
className={getButtonClassName({ variant: 'brand' })}
|
||||||
|
>
|
||||||
Voir mes équipes
|
Voir mes équipes
|
||||||
</span>
|
|
||||||
</Link>
|
</Link>
|
||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import { getButtonClassName } from '@/components/ui';
|
||||||
import { WORKSHOPS, getSessionsTabUrl } from '@/lib/workshops';
|
import { WORKSHOPS, getSessionsTabUrl } from '@/lib/workshops';
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
@@ -888,14 +889,16 @@ function WorkshopCard({
|
|||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<Link
|
<Link
|
||||||
href={newHref}
|
href={newHref}
|
||||||
className="flex-1 rounded-lg px-4 py-2.5 text-center font-medium text-white transition-colors"
|
className={getButtonClassName({
|
||||||
|
className: 'flex-1 border-transparent text-white',
|
||||||
|
})}
|
||||||
style={{ backgroundColor: accentColor }}
|
style={{ backgroundColor: accentColor }}
|
||||||
>
|
>
|
||||||
Démarrer
|
Démarrer
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href={href}
|
href={href}
|
||||||
className="rounded-lg border border-border px-4 py-2.5 font-medium text-foreground transition-colors hover:bg-card-hover"
|
className={getButtonClassName({ variant: 'outline' })}
|
||||||
>
|
>
|
||||||
Mes sessions
|
Mes sessions
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -39,29 +39,20 @@ export function PasswordForm() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
htmlFor="currentPassword"
|
|
||||||
className="mb-1.5 block text-sm font-medium text-foreground"
|
|
||||||
>
|
|
||||||
Mot de passe actuel
|
|
||||||
</label>
|
|
||||||
<Input
|
<Input
|
||||||
id="currentPassword"
|
id="currentPassword"
|
||||||
type="password"
|
type="password"
|
||||||
|
label="Mot de passe actuel"
|
||||||
value={currentPassword}
|
value={currentPassword}
|
||||||
onChange={(e) => setCurrentPassword(e.target.value)}
|
onChange={(e) => setCurrentPassword(e.target.value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="newPassword" className="mb-1.5 block text-sm font-medium text-foreground">
|
|
||||||
Nouveau mot de passe
|
|
||||||
</label>
|
|
||||||
<Input
|
<Input
|
||||||
id="newPassword"
|
id="newPassword"
|
||||||
type="password"
|
type="password"
|
||||||
|
label="Nouveau mot de passe"
|
||||||
value={newPassword}
|
value={newPassword}
|
||||||
onChange={(e) => setNewPassword(e.target.value)}
|
onChange={(e) => setNewPassword(e.target.value)}
|
||||||
required
|
required
|
||||||
@@ -71,15 +62,10 @@ export function PasswordForm() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label
|
|
||||||
htmlFor="confirmPassword"
|
|
||||||
className="mb-1.5 block text-sm font-medium text-foreground"
|
|
||||||
>
|
|
||||||
Confirmer le nouveau mot de passe
|
|
||||||
</label>
|
|
||||||
<Input
|
<Input
|
||||||
id="confirmPassword"
|
id="confirmPassword"
|
||||||
type="password"
|
type="password"
|
||||||
|
label="Confirmer le nouveau mot de passe"
|
||||||
value={confirmPassword}
|
value={confirmPassword}
|
||||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
required
|
required
|
||||||
|
|||||||
@@ -39,31 +39,23 @@ export function ProfileForm({ initialData }: ProfileFormProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
<div>
|
|
||||||
<label htmlFor="name" className="mb-1.5 block text-sm font-medium text-foreground">
|
|
||||||
Nom
|
|
||||||
</label>
|
|
||||||
<Input
|
<Input
|
||||||
id="name"
|
id="name"
|
||||||
type="text"
|
type="text"
|
||||||
|
label="Nom"
|
||||||
value={name}
|
value={name}
|
||||||
onChange={(e) => setName(e.target.value)}
|
onChange={(e) => setName(e.target.value)}
|
||||||
placeholder="Votre nom"
|
placeholder="Votre nom"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label htmlFor="email" className="mb-1.5 block text-sm font-medium text-foreground">
|
|
||||||
Email
|
|
||||||
</label>
|
|
||||||
<Input
|
<Input
|
||||||
id="email"
|
id="email"
|
||||||
type="email"
|
type="email"
|
||||||
|
label="Email"
|
||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
{message && (
|
{message && (
|
||||||
<p
|
<p
|
||||||
|
|||||||
@@ -1,25 +1,15 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useRef } from 'react';
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Button } from '@/components/ui';
|
import { Button, DropdownMenu } from '@/components/ui';
|
||||||
import { WORKSHOPS } from '@/lib/workshops';
|
import { WORKSHOPS } from '@/lib/workshops';
|
||||||
import { useClickOutside } from '@/hooks/useClickOutside';
|
|
||||||
|
|
||||||
export function NewWorkshopDropdown() {
|
export function NewWorkshopDropdown() {
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
|
||||||
useClickOutside(containerRef, () => setOpen(false), open);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={containerRef} className="relative">
|
<DropdownMenu
|
||||||
<Button
|
panelClassName="absolute right-0 z-20 mt-2 w-60 rounded-xl border border-border bg-card py-1.5 shadow-lg"
|
||||||
type="button"
|
trigger={({ open, toggle }) => (
|
||||||
variant="primary"
|
<Button type="button" variant="primary" size="sm" onClick={toggle} className="gap-1.5">
|
||||||
size="sm"
|
|
||||||
onClick={() => setOpen(!open)}
|
|
||||||
className="gap-1.5"
|
|
||||||
>
|
|
||||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||||
</svg>
|
</svg>
|
||||||
@@ -33,14 +23,16 @@ export function NewWorkshopDropdown() {
|
|||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||||
</svg>
|
</svg>
|
||||||
</Button>
|
</Button>
|
||||||
{open && (
|
)}
|
||||||
<div className="absolute right-0 z-20 mt-2 w-60 rounded-xl border border-border bg-card py-1.5 shadow-lg">
|
>
|
||||||
|
{({ close }) => (
|
||||||
|
<>
|
||||||
{WORKSHOPS.map((w) => (
|
{WORKSHOPS.map((w) => (
|
||||||
<Link
|
<Link
|
||||||
key={w.id}
|
key={w.id}
|
||||||
href={w.newPath}
|
href={w.newPath}
|
||||||
className="flex items-center gap-3 px-4 py-2.5 text-sm text-foreground hover:bg-card-hover transition-colors"
|
className="flex items-center gap-3 px-4 py-2.5 text-sm text-foreground hover:bg-card-hover transition-colors"
|
||||||
onClick={() => setOpen(false)}
|
onClick={close}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="flex h-8 w-8 items-center justify-center rounded-lg text-base flex-shrink-0"
|
className="flex h-8 w-8 items-center justify-center rounded-lg text-base flex-shrink-0"
|
||||||
@@ -54,8 +46,8 @@ export function NewWorkshopDropdown() {
|
|||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</DropdownMenu>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,16 @@
|
|||||||
|
|
||||||
import { useState, useTransition } from 'react';
|
import { useState, useTransition } from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Button, Modal, ModalFooter, Input, CollaboratorDisplay } from '@/components/ui';
|
import {
|
||||||
|
Button,
|
||||||
|
Modal,
|
||||||
|
ModalFooter,
|
||||||
|
Input,
|
||||||
|
CollaboratorDisplay,
|
||||||
|
IconButton,
|
||||||
|
IconEdit,
|
||||||
|
IconTrash,
|
||||||
|
} from '@/components/ui';
|
||||||
import { deleteSwotSession, updateSwotSession } from '@/actions/session';
|
import { deleteSwotSession, updateSwotSession } from '@/actions/session';
|
||||||
import { deleteMotivatorSession, updateMotivatorSession } from '@/actions/moving-motivators';
|
import { deleteMotivatorSession, updateMotivatorSession } from '@/actions/moving-motivators';
|
||||||
import { deleteYearReviewSession, updateYearReviewSession } from '@/actions/year-review';
|
import { deleteYearReviewSession, updateYearReviewSession } from '@/actions/year-review';
|
||||||
@@ -211,25 +220,21 @@ export function SessionCard({
|
|||||||
<>
|
<>
|
||||||
{(session.isOwner || session.role === 'EDITOR' || session.isTeamCollab) && (
|
{(session.isOwner || session.role === 'EDITOR' || session.isTeamCollab) && (
|
||||||
<div className={`absolute flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity z-20 ${view === 'table' ? 'top-1/2 -translate-y-1/2 right-3' : 'top-2.5 right-2.5'}`}>
|
<div className={`absolute flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity z-20 ${view === 'table' ? 'top-1/2 -translate-y-1/2 right-3' : 'top-2.5 right-2.5'}`}>
|
||||||
<button
|
<IconButton
|
||||||
onClick={(e) => { e.preventDefault(); e.stopPropagation(); openEditModal(); }}
|
onClick={(e) => { e.preventDefault(); e.stopPropagation(); openEditModal(); }}
|
||||||
className="p-1.5 rounded-lg bg-card border border-border text-muted hover:text-primary hover:bg-primary/5 shadow-sm"
|
label="Modifier"
|
||||||
title="Modifier"
|
icon={<IconEdit />}
|
||||||
>
|
variant="primary"
|
||||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
className="bg-card shadow-sm"
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
/>
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
{(session.isOwner || session.isTeamCollab) && (
|
{(session.isOwner || session.isTeamCollab) && (
|
||||||
<button
|
<IconButton
|
||||||
onClick={(e) => { e.preventDefault(); e.stopPropagation(); setShowDeleteModal(true); }}
|
onClick={(e) => { e.preventDefault(); e.stopPropagation(); setShowDeleteModal(true); }}
|
||||||
className="p-1.5 rounded-lg bg-card border border-border text-muted hover:text-destructive hover:bg-destructive/5 shadow-sm"
|
label="Supprimer"
|
||||||
title="Supprimer"
|
icon={<IconTrash />}
|
||||||
>
|
variant="destructive"
|
||||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
className="bg-card shadow-sm"
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
/>
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -247,19 +252,15 @@ export function SessionCard({
|
|||||||
|
|
||||||
<Modal isOpen={showEditModal} onClose={() => setShowEditModal(false)} title="Modifier l'atelier" size="sm">
|
<Modal isOpen={showEditModal} onClose={() => setShowEditModal(false)} title="Modifier l'atelier" size="sm">
|
||||||
<form onSubmit={(e) => { e.preventDefault(); handleEdit(); }} className="space-y-4">
|
<form onSubmit={(e) => { e.preventDefault(); handleEdit(); }} className="space-y-4">
|
||||||
|
<Input id="edit-title" label="Titre" value={editTitle} onChange={(e) => setEditTitle(e.target.value)} placeholder="Titre de l'atelier" required />
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="edit-title" className="block text-sm font-medium text-foreground mb-1">Titre</label>
|
|
||||||
<Input id="edit-title" value={editTitle} onChange={(e) => setEditTitle(e.target.value)} placeholder="Titre de l'atelier" required />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label htmlFor="edit-participant" className="block text-sm font-medium text-foreground mb-1">{workshop.participantLabel}</label>
|
|
||||||
{!isWeather && !isGifMood && (
|
{!isWeather && !isGifMood && (
|
||||||
<Input id="edit-participant" value={editParticipant} onChange={(e) => setEditParticipant(e.target.value)}
|
<Input id="edit-participant" label={workshop.participantLabel} value={editParticipant} onChange={(e) => setEditParticipant(e.target.value)}
|
||||||
placeholder={isSwot ? 'Nom du collaborateur' : 'Nom du participant'} required />
|
placeholder={isSwot ? 'Nom du collaborateur' : 'Nom du participant'} required />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button type="button" variant="ghost" onClick={() => setShowEditModal(false)} disabled={isPending}>Annuler</Button>
|
<Button type="button" variant="outline" onClick={() => setShowEditModal(false)} disabled={isPending}>Annuler</Button>
|
||||||
<Button type="submit" disabled={isPending || !editTitle.trim() || (!isWeather && !isGifMood && !editParticipant.trim())}>
|
<Button type="submit" disabled={isPending || !editTitle.trim() || (!isWeather && !isGifMood && !editParticipant.trim())}>
|
||||||
{isPending ? 'Enregistrement...' : 'Enregistrer'}
|
{isPending ? 'Enregistrement...' : 'Enregistrer'}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -272,7 +273,7 @@ export function SessionCard({
|
|||||||
<p className="text-muted">Êtes-vous sûr de vouloir supprimer <strong className="text-foreground">"{session.title}"</strong> ?</p>
|
<p className="text-muted">Êtes-vous sûr de vouloir supprimer <strong className="text-foreground">"{session.title}"</strong> ?</p>
|
||||||
<p className="text-sm text-destructive">Cette action est irréversible. Toutes les données seront perdues.</p>
|
<p className="text-sm text-destructive">Cette action est irréversible. Toutes les données seront perdues.</p>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button variant="ghost" onClick={() => setShowDeleteModal(false)} disabled={isPending}>Annuler</Button>
|
<Button variant="outline" onClick={() => setShowDeleteModal(false)} disabled={isPending}>Annuler</Button>
|
||||||
<Button variant="destructive" onClick={handleDelete} disabled={isPending}>{isPending ? 'Suppression...' : 'Supprimer'}</Button>
|
<Button variant="destructive" onClick={handleDelete} disabled={isPending}>{isPending ? 'Suppression...' : 'Supprimer'}</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export default async function TeamDetailPage({ params }: TeamDetailPageProps) {
|
|||||||
isAdmin ? (
|
isAdmin ? (
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Link href={`/teams/${id}/okrs/new`}>
|
<Link href={`/teams/${id}/okrs/new`}>
|
||||||
<Button className="bg-[var(--purple)] text-white hover:opacity-90 border-transparent">
|
<Button variant="brand" size="sm">
|
||||||
Définir un OKR
|
Définir un OKR
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export default function NewTeamPage() {
|
|||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
className="bg-[var(--purple)] text-white hover:opacity-90 border-transparent"
|
variant="brand"
|
||||||
>
|
>
|
||||||
{submitting ? 'Création...' : "Créer l'équipe"}
|
{submitting ? 'Création...' : "Créer l'équipe"}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export default async function TeamsPage() {
|
|||||||
subtitle={`${teams.length} équipe${teams.length !== 1 ? 's' : ''} · Collaborez et définissez vos OKRs`}
|
subtitle={`${teams.length} équipe${teams.length !== 1 ? 's' : ''} · Collaborez et définissez vos OKRs`}
|
||||||
actions={
|
actions={
|
||||||
<Link href="/teams/new">
|
<Link href="/teams/new">
|
||||||
<Button className="bg-[var(--purple)] text-white hover:opacity-90 border-transparent">
|
<Button variant="brand" size="sm">
|
||||||
Créer une équipe
|
Créer une équipe
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
@@ -44,7 +44,7 @@ export default async function TeamsPage() {
|
|||||||
Créez votre première équipe pour commencer à définir des OKRs
|
Créez votre première équipe pour commencer à définir des OKRs
|
||||||
</div>
|
</div>
|
||||||
<Link href="/teams/new" className="mt-6">
|
<Link href="/teams/new" className="mt-6">
|
||||||
<Button className="!bg-[var(--purple)] !text-white hover:!bg-[var(--purple)]/90">
|
<Button variant="brand">
|
||||||
Créer une équipe
|
Créer une équipe
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
CardDescription,
|
CardDescription,
|
||||||
CardContent,
|
CardContent,
|
||||||
Button,
|
Button,
|
||||||
|
DateInput,
|
||||||
Input,
|
Input,
|
||||||
} from '@/components/ui';
|
} from '@/components/ui';
|
||||||
import { createWeatherSession } from '@/actions/weather';
|
import { createWeatherSession } from '@/actions/weather';
|
||||||
@@ -93,20 +94,14 @@ export default function NewWeatherPage() {
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div>
|
<DateInput
|
||||||
<label htmlFor="date" className="block text-sm font-medium text-foreground mb-1">
|
|
||||||
Date de la météo
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="date"
|
id="date"
|
||||||
name="date"
|
name="date"
|
||||||
type="date"
|
label="Date de la météo"
|
||||||
value={selectedDate}
|
value={selectedDate}
|
||||||
onChange={handleDateChange}
|
onChange={handleDateChange}
|
||||||
required
|
required
|
||||||
className="w-full rounded-lg border border-border bg-input px-3 py-2 text-foreground outline-none focus:border-primary focus:ring-2 focus:ring-primary/20"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="rounded-lg border border-border bg-card-hover p-4">
|
<div className="rounded-lg border border-border bg-card-hover p-4">
|
||||||
<h3 className="font-medium text-foreground mb-2">Comment ça marche ?</h3>
|
<h3 className="font-medium text-foreground mb-2">Comment ça marche ?</h3>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
CardDescription,
|
CardDescription,
|
||||||
CardContent,
|
CardContent,
|
||||||
Button,
|
Button,
|
||||||
|
DateInput,
|
||||||
Input,
|
Input,
|
||||||
ParticipantInput,
|
ParticipantInput,
|
||||||
} from '@/components/ui';
|
} from '@/components/ui';
|
||||||
@@ -98,20 +99,14 @@ export default function NewWeeklyCheckInPage() {
|
|||||||
|
|
||||||
<ParticipantInput name="participant" required />
|
<ParticipantInput name="participant" required />
|
||||||
|
|
||||||
<div>
|
<DateInput
|
||||||
<label htmlFor="date" className="block text-sm font-medium text-foreground mb-1">
|
|
||||||
Date du check-in
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="date"
|
id="date"
|
||||||
name="date"
|
name="date"
|
||||||
type="date"
|
label="Date du check-in"
|
||||||
value={selectedDate}
|
value={selectedDate}
|
||||||
onChange={handleDateChange}
|
onChange={handleDateChange}
|
||||||
required
|
required
|
||||||
className="w-full rounded-lg border border-border bg-input px-3 py-2 text-foreground outline-none focus:border-primary focus:ring-2 focus:ring-primary/20"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="rounded-lg border border-border bg-card-hover p-4">
|
<div className="rounded-lg border border-border bg-card-hover p-4">
|
||||||
<h3 className="font-medium text-foreground mb-2">Comment ça marche ?</h3>
|
<h3 className="font-medium text-foreground mb-2">Comment ça marche ?</h3>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
CardDescription,
|
CardDescription,
|
||||||
CardContent,
|
CardContent,
|
||||||
Button,
|
Button,
|
||||||
|
NumberInput,
|
||||||
Input,
|
Input,
|
||||||
ParticipantInput,
|
ParticipantInput,
|
||||||
} from '@/components/ui';
|
} from '@/components/ui';
|
||||||
@@ -79,21 +80,15 @@ export default function NewYearReviewPage() {
|
|||||||
|
|
||||||
<ParticipantInput name="participant" required />
|
<ParticipantInput name="participant" required />
|
||||||
|
|
||||||
<div>
|
<NumberInput
|
||||||
<label htmlFor="year" className="block text-sm font-medium text-foreground mb-1">
|
|
||||||
Année du bilan
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="year"
|
id="year"
|
||||||
name="year"
|
name="year"
|
||||||
type="number"
|
label="Année du bilan"
|
||||||
min="2000"
|
min="2000"
|
||||||
max="2100"
|
max="2100"
|
||||||
defaultValue={currentYear}
|
defaultValue={currentYear}
|
||||||
required
|
required
|
||||||
className="w-full rounded-lg border border-border bg-input px-3 py-2 text-foreground outline-none focus:border-primary focus:ring-2 focus:ring-primary/20"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="rounded-lg border border-border bg-card-hover p-4">
|
<div className="rounded-lg border border-border bg-card-hover p-4">
|
||||||
<h3 className="font-medium text-foreground mb-2">Comment ça marche ?</h3>
|
<h3 className="font-medium text-foreground mb-2">Comment ça marche ?</h3>
|
||||||
|
|||||||
@@ -2,12 +2,17 @@
|
|||||||
|
|
||||||
import { useState, useTransition } from 'react';
|
import { useState, useTransition } from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Modal } from '@/components/ui/Modal';
|
import {
|
||||||
import { Input } from '@/components/ui/Input';
|
Modal,
|
||||||
import { Button } from '@/components/ui/Button';
|
Input,
|
||||||
import { Badge } from '@/components/ui/Badge';
|
Button,
|
||||||
import { Avatar } from '@/components/ui/Avatar';
|
Badge,
|
||||||
import { Select } from '@/components/ui/Select';
|
Avatar,
|
||||||
|
Select,
|
||||||
|
SegmentedControl,
|
||||||
|
IconButton,
|
||||||
|
IconTrash,
|
||||||
|
} from '@/components/ui';
|
||||||
import { getTeamMembersForShare, type TeamWithMembers, type Share } from '@/lib/share-utils';
|
import { getTeamMembersForShare, type TeamWithMembers, type Share } from '@/lib/share-utils';
|
||||||
import type { ShareRole } from '@prisma/client';
|
import type { ShareRole } from '@prisma/client';
|
||||||
|
|
||||||
@@ -117,24 +122,20 @@ export function ShareModal({
|
|||||||
|
|
||||||
{isOwner && (
|
{isOwner && (
|
||||||
<form onSubmit={handleShare} className="space-y-4">
|
<form onSubmit={handleShare} className="space-y-4">
|
||||||
<div className="flex gap-2 border-b border-border pb-3 flex-wrap">
|
<div className="border-b border-border pb-3">
|
||||||
{tabs.map((tab) => (
|
<SegmentedControl
|
||||||
<button
|
value={shareType}
|
||||||
key={tab.value}
|
onChange={(value) => {
|
||||||
type="button"
|
setShareType(value);
|
||||||
onClick={() => {
|
|
||||||
setShareType(tab.value);
|
|
||||||
resetForm();
|
resetForm();
|
||||||
}}
|
}}
|
||||||
className={`flex-1 min-w-0 rounded-lg px-4 py-2 text-sm font-medium transition-colors ${
|
fullWidth
|
||||||
shareType === tab.value
|
className="flex w-full gap-2 border-0 bg-transparent p-0"
|
||||||
? 'bg-primary text-primary-foreground'
|
options={tabs.map((tab) => ({
|
||||||
: 'bg-card-hover text-muted hover:text-foreground'
|
value: tab.value,
|
||||||
}`}
|
label: `${tab.icon} ${tab.label}`,
|
||||||
>
|
}))}
|
||||||
{tab.icon} {tab.label}
|
/>
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{shareType === 'email' && (
|
{shareType === 'email' && (
|
||||||
@@ -271,25 +272,13 @@ export function ShareModal({
|
|||||||
{share.role === 'EDITOR' ? 'Éditeur' : 'Lecteur'}
|
{share.role === 'EDITOR' ? 'Éditeur' : 'Lecteur'}
|
||||||
</Badge>
|
</Badge>
|
||||||
{isOwner && (
|
{isOwner && (
|
||||||
<button
|
<IconButton
|
||||||
|
icon={<IconTrash className="h-4 w-4" />}
|
||||||
|
label="Retirer l'accès"
|
||||||
|
variant="destructive"
|
||||||
onClick={() => handleRemove(share.user.id)}
|
onClick={() => handleRemove(share.user.id)}
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
className="rounded p-1 text-muted hover:bg-destructive/10 hover:text-destructive"
|
|
||||||
title="Retirer l'accès"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
className="h-4 w-4"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
d="M8.75 1A2.75 2.75 0 006 3.75v.443c-.795.077-1.584.176-2.365.298a.75.75 0 10.23 1.482l.149-.022.841 10.518A2.75 2.75 0 007.596 19h4.807a2.75 2.75 0 002.742-2.53l.841-10.52.149.023a.75.75 0 00.23-1.482A41.03 41.03 0 0014 4.193V3.75A2.75 2.75 0 0011.25 1h-2.5zM10 4c.84 0 1.673.025 2.5.075V3.75c0-.69-.56-1.25-1.25-1.25h-2.5c-.69 0-1.25.56-1.25 1.25v.325C8.327 4.025 9.16 4 10 4zM8.58 7.72a.75.75 0 00-1.5.06l.3 7.5a.75.75 0 101.5-.06l-.3-7.5zm4.34.06a.75.75 0 10-1.5-.06l-.3 7.5a.75.75 0 101.5.06l.3-7.5z"
|
|
||||||
clipRule="evenodd"
|
|
||||||
/>
|
/>
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { auth } from '@/lib/auth';
|
import { auth } from '@/lib/auth';
|
||||||
import { RocketIcon } from '@/components/ui';
|
import { RocketIcon, getButtonClassName } from '@/components/ui';
|
||||||
import { ThemeToggle } from './ThemeToggle';
|
import { ThemeToggle } from './ThemeToggle';
|
||||||
import { UserMenu } from './UserMenu';
|
import { UserMenu } from './UserMenu';
|
||||||
import { WorkshopsDropdown } from './WorkshopsDropdown';
|
import { WorkshopsDropdown } from './WorkshopsDropdown';
|
||||||
@@ -36,7 +36,7 @@ export async function Header() {
|
|||||||
) : (
|
) : (
|
||||||
<Link
|
<Link
|
||||||
href="/login"
|
href="/login"
|
||||||
className="flex h-9 items-center rounded-lg bg-primary px-4 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary-hover"
|
className={getButtonClassName({ size: 'sm' })}
|
||||||
>
|
>
|
||||||
Connexion
|
Connexion
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { usePathname } from 'next/navigation';
|
|||||||
|
|
||||||
export function NavLinks() {
|
export function NavLinks() {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
const isDev = process.env.NODE_ENV !== 'production';
|
||||||
|
|
||||||
const isActiveLink = (path: string) => pathname.startsWith(path);
|
const isActiveLink = (path: string) => pathname.startsWith(path);
|
||||||
|
|
||||||
@@ -38,6 +39,17 @@ export function NavLinks() {
|
|||||||
>
|
>
|
||||||
👥 Équipes
|
👥 Équipes
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
|
{isDev && (
|
||||||
|
<Link
|
||||||
|
href="/design-system"
|
||||||
|
className={`text-sm font-medium transition-colors ${
|
||||||
|
isActiveLink('/design-system') ? 'text-primary' : 'text-muted hover:text-foreground'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
🎨 UI Guide
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,7 @@
|
|||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { signOut } from 'next-auth/react';
|
import { signOut } from 'next-auth/react';
|
||||||
import { useState, useRef } from 'react';
|
import { Avatar, DropdownMenu } from '@/components/ui';
|
||||||
import { Avatar } from '@/components/ui';
|
|
||||||
import { useClickOutside } from '@/hooks/useClickOutside';
|
|
||||||
|
|
||||||
interface UserMenuProps {
|
interface UserMenuProps {
|
||||||
userName: string | null | undefined;
|
userName: string | null | undefined;
|
||||||
@@ -12,14 +10,12 @@ interface UserMenuProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function UserMenu({ userName, userEmail }: UserMenuProps) {
|
export function UserMenu({ userName, userEmail }: UserMenuProps) {
|
||||||
const [menuOpen, setMenuOpen] = useState(false);
|
|
||||||
const userMenuRef = useRef<HTMLDivElement>(null);
|
|
||||||
useClickOutside(userMenuRef, () => setMenuOpen(false), menuOpen);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={userMenuRef} className="relative">
|
<DropdownMenu
|
||||||
|
panelClassName="absolute right-0 z-20 mt-2 w-48 rounded-lg border border-border bg-card py-1 shadow-lg"
|
||||||
|
trigger={({ open, toggle }) => (
|
||||||
<button
|
<button
|
||||||
onClick={() => setMenuOpen(!menuOpen)}
|
onClick={toggle}
|
||||||
className="flex h-9 items-center gap-2 rounded-lg border border-border bg-card pl-1.5 pr-3 transition-colors hover:bg-card-hover"
|
className="flex h-9 items-center gap-2 rounded-lg border border-border bg-card pl-1.5 pr-3 transition-colors hover:bg-card-hover"
|
||||||
>
|
>
|
||||||
<Avatar email={userEmail} name={userName} size={24} />
|
<Avatar email={userEmail} name={userName} size={24} />
|
||||||
@@ -27,36 +23,32 @@ export function UserMenu({ userName, userEmail }: UserMenuProps) {
|
|||||||
{userName || userEmail.split('@')[0]}
|
{userName || userEmail.split('@')[0]}
|
||||||
</span>
|
</span>
|
||||||
<svg
|
<svg
|
||||||
className={`h-4 w-4 text-muted transition-transform ${menuOpen ? 'rotate-180' : ''}`}
|
className={`h-4 w-4 text-muted transition-transform ${open ? 'rotate-180' : ''}`}
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
>
|
>
|
||||||
<path
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M19 9l-7 7-7-7"
|
|
||||||
/>
|
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
{menuOpen && (
|
>
|
||||||
<div className="absolute right-0 z-20 mt-2 w-48 rounded-lg border border-border bg-card py-1 shadow-lg">
|
{({ close }) => (
|
||||||
|
<>
|
||||||
<div className="border-b border-border px-4 py-2">
|
<div className="border-b border-border px-4 py-2">
|
||||||
<p className="text-xs text-muted">Connecté en tant que</p>
|
<p className="text-xs text-muted">Connecté en tant que</p>
|
||||||
<p className="truncate text-sm font-medium text-foreground">{userEmail}</p>
|
<p className="truncate text-sm font-medium text-foreground">{userEmail}</p>
|
||||||
</div>
|
</div>
|
||||||
<Link
|
<Link
|
||||||
href="/profile"
|
href="/profile"
|
||||||
onClick={() => setMenuOpen(false)}
|
onClick={close}
|
||||||
className="block w-full px-4 py-2 text-left text-sm text-foreground hover:bg-card-hover"
|
className="block w-full px-4 py-2 text-left text-sm text-foreground hover:bg-card-hover"
|
||||||
>
|
>
|
||||||
👤 Mon Profil
|
👤 Mon Profil
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href="/users"
|
href="/users"
|
||||||
onClick={() => setMenuOpen(false)}
|
onClick={close}
|
||||||
className="block w-full px-4 py-2 text-left text-sm text-foreground hover:bg-card-hover"
|
className="block w-full px-4 py-2 text-left text-sm text-foreground hover:bg-card-hover"
|
||||||
>
|
>
|
||||||
👥 Utilisateurs
|
👥 Utilisateurs
|
||||||
@@ -67,8 +59,8 @@ export function UserMenu({ userName, userEmail }: UserMenuProps) {
|
|||||||
>
|
>
|
||||||
Se déconnecter
|
Se déconnecter
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</DropdownMenu>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,22 +2,20 @@
|
|||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
import { useState, useRef } from 'react';
|
import { DropdownMenu } from '@/components/ui';
|
||||||
import { WORKSHOPS } from '@/lib/workshops';
|
import { WORKSHOPS } from '@/lib/workshops';
|
||||||
import { useClickOutside } from '@/hooks/useClickOutside';
|
|
||||||
|
|
||||||
export function WorkshopsDropdown() {
|
export function WorkshopsDropdown() {
|
||||||
const [workshopsOpen, setWorkshopsOpen] = useState(false);
|
|
||||||
const workshopsDropdownRef = useRef<HTMLDivElement>(null);
|
|
||||||
useClickOutside(workshopsDropdownRef, () => setWorkshopsOpen(false), workshopsOpen);
|
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
||||||
const isActiveLink = (path: string) => pathname.startsWith(path);
|
const isActiveLink = (path: string) => pathname.startsWith(path);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative" ref={workshopsDropdownRef}>
|
<DropdownMenu
|
||||||
|
panelClassName="absolute left-0 z-20 mt-2 w-56 rounded-lg border border-border bg-card py-1 shadow-lg"
|
||||||
|
trigger={({ open, toggle }) => (
|
||||||
<button
|
<button
|
||||||
onClick={() => setWorkshopsOpen(!workshopsOpen)}
|
onClick={toggle}
|
||||||
className={`flex items-center gap-1 text-sm font-medium transition-colors ${
|
className={`flex items-center gap-1 text-sm font-medium transition-colors ${
|
||||||
WORKSHOPS.some((w) => isActiveLink(w.path))
|
WORKSHOPS.some((w) => isActiveLink(w.path))
|
||||||
? 'text-primary'
|
? 'text-primary'
|
||||||
@@ -26,28 +24,24 @@ export function WorkshopsDropdown() {
|
|||||||
>
|
>
|
||||||
Nouvel atelier
|
Nouvel atelier
|
||||||
<svg
|
<svg
|
||||||
className={`h-4 w-4 transition-transform ${workshopsOpen ? 'rotate-180' : ''}`}
|
className={`h-4 w-4 transition-transform ${open ? 'rotate-180' : ''}`}
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
>
|
>
|
||||||
<path
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M19 9l-7 7-7-7"
|
|
||||||
/>
|
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
{workshopsOpen && (
|
>
|
||||||
<div className="absolute left-0 z-20 mt-2 w-56 rounded-lg border border-border bg-card py-1 shadow-lg">
|
{({ close }) => (
|
||||||
|
<>
|
||||||
{WORKSHOPS.map((w) => (
|
{WORKSHOPS.map((w) => (
|
||||||
<Link
|
<Link
|
||||||
key={w.id}
|
key={w.id}
|
||||||
href={w.newPath}
|
href={w.newPath}
|
||||||
className="flex items-center gap-3 px-4 py-2.5 text-sm text-foreground hover:bg-card-hover"
|
className="flex items-center gap-3 px-4 py-2.5 text-sm text-foreground hover:bg-card-hover"
|
||||||
onClick={() => setWorkshopsOpen(false)}
|
onClick={close}
|
||||||
>
|
>
|
||||||
<span className="text-lg">{w.icon}</span>
|
<span className="text-lg">{w.icon}</span>
|
||||||
<div>
|
<div>
|
||||||
@@ -56,8 +50,8 @@ export function WorkshopsDropdown() {
|
|||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</DropdownMenu>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
arrayMove,
|
arrayMove,
|
||||||
} from '@dnd-kit/sortable';
|
} from '@dnd-kit/sortable';
|
||||||
import type { MotivatorCard as MotivatorCardType } from '@/lib/types';
|
import type { MotivatorCard as MotivatorCardType } from '@/lib/types';
|
||||||
|
import { Button } from '@/components/ui';
|
||||||
import { MotivatorCard } from './MotivatorCard';
|
import { MotivatorCard } from './MotivatorCard';
|
||||||
import { MotivatorSummary } from './MotivatorSummary';
|
import { MotivatorSummary } from './MotivatorSummary';
|
||||||
import { InfluenceZone } from './InfluenceZone';
|
import { InfluenceZone } from './InfluenceZone';
|
||||||
@@ -167,12 +168,9 @@ export function MotivatorBoard({ sessionId, cards: initialCards, canEdit }: Moti
|
|||||||
|
|
||||||
{/* Next button */}
|
{/* Next button */}
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<button
|
<Button onClick={nextStep} className="px-6">
|
||||||
onClick={nextStep}
|
|
||||||
className="px-6 py-2 bg-primary text-primary-foreground rounded-lg font-medium hover:bg-primary/90 transition-colors"
|
|
||||||
>
|
|
||||||
Suivant →
|
Suivant →
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -197,18 +195,12 @@ export function MotivatorBoard({ sessionId, cards: initialCards, canEdit }: Moti
|
|||||||
|
|
||||||
{/* Navigation buttons */}
|
{/* Navigation buttons */}
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<button
|
<Button onClick={prevStep} variant="outline" className="px-6">
|
||||||
onClick={prevStep}
|
|
||||||
className="px-6 py-2 border border-border rounded-lg font-medium hover:bg-card transition-colors"
|
|
||||||
>
|
|
||||||
← Retour
|
← Retour
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button onClick={nextStep} className="px-6">
|
||||||
onClick={nextStep}
|
|
||||||
className="px-6 py-2 bg-primary text-primary-foreground rounded-lg font-medium hover:bg-primary/90 transition-colors"
|
|
||||||
>
|
|
||||||
Voir le récapitulatif →
|
Voir le récapitulatif →
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -226,12 +218,9 @@ export function MotivatorBoard({ sessionId, cards: initialCards, canEdit }: Moti
|
|||||||
|
|
||||||
{/* Navigation buttons */}
|
{/* Navigation buttons */}
|
||||||
<div className="flex justify-start">
|
<div className="flex justify-start">
|
||||||
<button
|
<Button onClick={prevStep} variant="outline" className="px-6">
|
||||||
onClick={prevStep}
|
|
||||||
className="px-6 py-2 border border-border rounded-lg font-medium hover:bg-card transition-colors"
|
|
||||||
>
|
|
||||||
← Modifier
|
← Modifier
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -3,8 +3,7 @@
|
|||||||
import { useTransition } from 'react';
|
import { useTransition } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui';
|
import { Badge, Card, CardContent, CardHeader, CardTitle, IconButton, IconTrash } from '@/components/ui';
|
||||||
import { Badge } from '@/components/ui';
|
|
||||||
import { getGravatarUrl } from '@/lib/gravatar';
|
import { getGravatarUrl } from '@/lib/gravatar';
|
||||||
import type { OKR, KeyResult, OKRStatus, KeyResultStatus } from '@/lib/types';
|
import type { OKR, KeyResult, OKRStatus, KeyResultStatus } from '@/lib/types';
|
||||||
import { OKR_STATUS_LABELS, KEY_RESULT_STATUS_LABELS } from '@/lib/types';
|
import { OKR_STATUS_LABELS, KEY_RESULT_STATUS_LABELS } from '@/lib/types';
|
||||||
@@ -143,32 +142,20 @@ export function OKRCard({ okr, teamId, isAdmin = false, compact = false }: OKRCa
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5 flex-shrink-0 relative z-10">
|
<div className="flex items-center gap-1.5 flex-shrink-0 relative z-10">
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<button
|
<IconButton
|
||||||
|
icon={<IconTrash className="h-3 w-3" />}
|
||||||
|
label="Supprimer l'OKR"
|
||||||
|
variant="destructive"
|
||||||
|
size="xs"
|
||||||
onClick={handleDelete}
|
onClick={handleDelete}
|
||||||
className="h-5 w-5 p-0 flex items-center justify-center rounded hover:bg-destructive/10 transition-colors flex-shrink-0"
|
className="flex-shrink-0"
|
||||||
style={{
|
style={{
|
||||||
color: 'var(--destructive)',
|
color: 'var(--destructive)',
|
||||||
border: '1px solid color-mix(in srgb, var(--destructive) 40%, transparent)',
|
border: '1px solid color-mix(in srgb, var(--destructive) 40%, transparent)',
|
||||||
backgroundColor: 'color-mix(in srgb, var(--destructive) 5%, transparent)',
|
backgroundColor: 'color-mix(in srgb, var(--destructive) 5%, transparent)',
|
||||||
}}
|
}}
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
title="Supprimer l'OKR"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
className="h-3 w-3"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2.5}
|
|
||||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
|
||||||
/>
|
/>
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
)}
|
)}
|
||||||
<Badge
|
<Badge
|
||||||
style={{
|
style={{
|
||||||
@@ -255,32 +242,20 @@ export function OKRCard({ okr, teamId, isAdmin = false, compact = false }: OKRCa
|
|||||||
{/* Action Zone */}
|
{/* Action Zone */}
|
||||||
<div className="flex items-center gap-2 flex-shrink-0 relative z-10">
|
<div className="flex items-center gap-2 flex-shrink-0 relative z-10">
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<button
|
<IconButton
|
||||||
|
icon={<IconTrash className="h-4 w-4" />}
|
||||||
|
label="Supprimer l'OKR"
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
onClick={handleDelete}
|
onClick={handleDelete}
|
||||||
className="h-6 w-6 p-0 flex items-center justify-center rounded hover:bg-destructive/10 transition-colors flex-shrink-0"
|
className="flex-shrink-0"
|
||||||
style={{
|
style={{
|
||||||
color: 'var(--destructive)',
|
color: 'var(--destructive)',
|
||||||
border: '1px solid color-mix(in srgb, var(--destructive) 40%, transparent)',
|
border: '1px solid color-mix(in srgb, var(--destructive) 40%, transparent)',
|
||||||
backgroundColor: 'color-mix(in srgb, var(--destructive) 5%, transparent)',
|
backgroundColor: 'color-mix(in srgb, var(--destructive) 5%, transparent)',
|
||||||
}}
|
}}
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
title="Supprimer l'OKR"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
className="h-4 w-4"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2.5}
|
|
||||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
|
||||||
/>
|
/>
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
)}
|
)}
|
||||||
<Badge
|
<Badge
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -478,7 +478,7 @@ export function OKRForm({ teamMembers, onSubmit, onCancel, initialData }: OKRFor
|
|||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
className="bg-[var(--purple)] text-white hover:opacity-90 border-transparent"
|
variant="brand"
|
||||||
>
|
>
|
||||||
{submitting
|
{submitting
|
||||||
? initialData?.teamMemberId
|
? initialData?.teamMemberId
|
||||||
|
|||||||
@@ -2,7 +2,18 @@
|
|||||||
|
|
||||||
import { useState, useTransition } from 'react';
|
import { useState, useTransition } from 'react';
|
||||||
import type { SwotItem, Action, ActionLink, SwotCategory } from '@prisma/client';
|
import type { SwotItem, Action, ActionLink, SwotCategory } from '@prisma/client';
|
||||||
import { Button, Badge, Modal, ModalFooter, Input, Textarea, Select } from '@/components/ui';
|
import {
|
||||||
|
Button,
|
||||||
|
Badge,
|
||||||
|
Modal,
|
||||||
|
ModalFooter,
|
||||||
|
Input,
|
||||||
|
Textarea,
|
||||||
|
Select,
|
||||||
|
IconButton,
|
||||||
|
IconEdit,
|
||||||
|
IconTrash,
|
||||||
|
} from '@/components/ui';
|
||||||
import { createAction, updateAction, deleteAction } from '@/actions/swot';
|
import { createAction, updateAction, deleteAction } from '@/actions/swot';
|
||||||
|
|
||||||
type ActionWithLinks = Action & {
|
type ActionWithLinks = Action & {
|
||||||
@@ -202,44 +213,19 @@ export function ActionPanel({
|
|||||||
<div className="flex items-start justify-between gap-2">
|
<div className="flex items-start justify-between gap-2">
|
||||||
<h3 className="font-medium text-foreground line-clamp-2">{action.title}</h3>
|
<h3 className="font-medium text-foreground line-clamp-2">{action.title}</h3>
|
||||||
<div className="flex shrink-0 items-center gap-1">
|
<div className="flex shrink-0 items-center gap-1">
|
||||||
<button
|
<IconButton
|
||||||
|
icon={<IconEdit />}
|
||||||
|
label="Modifier"
|
||||||
onClick={() => openEditModal(action)}
|
onClick={() => openEditModal(action)}
|
||||||
className="rounded p-1 text-muted opacity-0 transition-opacity hover:bg-card-hover hover:text-foreground group-hover:opacity-100"
|
className="opacity-0 transition-opacity group-hover:opacity-100"
|
||||||
aria-label="Modifier"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
className="h-3.5 w-3.5"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"
|
|
||||||
/>
|
/>
|
||||||
</svg>
|
<IconButton
|
||||||
</button>
|
icon={<IconTrash />}
|
||||||
<button
|
label="Supprimer"
|
||||||
|
variant="destructive"
|
||||||
onClick={() => handleDelete(action.id)}
|
onClick={() => handleDelete(action.id)}
|
||||||
className="rounded p-1 text-muted opacity-0 transition-opacity hover:bg-destructive/10 hover:text-destructive group-hover:opacity-100"
|
className="opacity-0 transition-opacity group-hover:opacity-100"
|
||||||
aria-label="Supprimer"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
className="h-3.5 w-3.5"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
|
||||||
/>
|
/>
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -377,12 +363,14 @@ export function ActionPanel({
|
|||||||
<label className="mb-2 block text-sm font-medium text-foreground">Priorité</label>
|
<label className="mb-2 block text-sm font-medium text-foreground">Priorité</label>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{priorityLabels.map((label, index) => (
|
{priorityLabels.map((label, index) => (
|
||||||
<button
|
<Button
|
||||||
key={index}
|
key={index}
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
onClick={() => setPriority(index)}
|
onClick={() => setPriority(index)}
|
||||||
className={`
|
className={`
|
||||||
flex-1 rounded-lg border px-3 py-2 text-sm font-medium transition-colors
|
flex-1
|
||||||
${
|
${
|
||||||
priority === index
|
priority === index
|
||||||
? index === 2
|
? index === 2
|
||||||
@@ -395,17 +383,17 @@ export function ActionPanel({
|
|||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button type="button" variant="outline" onClick={closeModal}>
|
<Button type="button" variant="outline" size="sm" onClick={closeModal}>
|
||||||
Annuler
|
Annuler
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" loading={isPending}>
|
<Button type="submit" size="sm" loading={isPending}>
|
||||||
{editingAction ? 'Enregistrer' : "Créer l'action"}
|
{editingAction ? 'Enregistrer' : "Créer l'action"}
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type { SwotItem, Action, ActionLink, SwotCategory } from '@prisma/client'
|
|||||||
import { SwotQuadrant } from './SwotQuadrant';
|
import { SwotQuadrant } from './SwotQuadrant';
|
||||||
import { SwotCard } from './SwotCard';
|
import { SwotCard } from './SwotCard';
|
||||||
import { ActionPanel } from './ActionPanel';
|
import { ActionPanel } from './ActionPanel';
|
||||||
|
import { Button } from '@/components/ui';
|
||||||
import { moveSwotItem } from '@/actions/swot';
|
import { moveSwotItem } from '@/actions/swot';
|
||||||
|
|
||||||
type ActionWithLinks = Action & {
|
type ActionWithLinks = Action & {
|
||||||
@@ -94,12 +95,9 @@ export function SwotBoard({ sessionId, items, actions }: SwotBoardProps) {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<Button onClick={exitLinkMode} variant="outline" size="sm">
|
||||||
onClick={exitLinkMode}
|
|
||||||
className="rounded-lg border border-border bg-card px-4 py-2 text-sm font-medium hover:bg-card-hover"
|
|
||||||
>
|
|
||||||
Annuler
|
Annuler
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { forwardRef, memo, useState, useTransition } from 'react';
|
import { forwardRef, memo, useState, useTransition } from 'react';
|
||||||
import type { SwotItem, SwotCategory } from '@prisma/client';
|
import type { SwotItem, SwotCategory } from '@prisma/client';
|
||||||
import { updateSwotItem, deleteSwotItem, duplicateSwotItem } from '@/actions/swot';
|
import { updateSwotItem, deleteSwotItem, duplicateSwotItem } from '@/actions/swot';
|
||||||
import { IconEdit, IconTrash, IconDuplicate, IconCheck } from '@/components/ui';
|
import { IconEdit, IconTrash, IconDuplicate, IconCheck, IconButton } from '@/components/ui';
|
||||||
|
|
||||||
interface SwotCardProps {
|
interface SwotCardProps {
|
||||||
item: SwotItem;
|
item: SwotItem;
|
||||||
@@ -114,30 +114,32 @@ export const SwotCard = memo(
|
|||||||
{/* Actions (visible on hover) */}
|
{/* Actions (visible on hover) */}
|
||||||
{!linkMode && (
|
{!linkMode && (
|
||||||
<div className="absolute right-1 top-1 flex gap-0.5 opacity-0 transition-opacity group-hover:opacity-100">
|
<div className="absolute right-1 top-1 flex gap-0.5 opacity-0 transition-opacity group-hover:opacity-100">
|
||||||
<button
|
<IconButton
|
||||||
|
icon={<IconEdit />}
|
||||||
|
label="Modifier"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setIsEditing(true);
|
setIsEditing(true);
|
||||||
}}
|
}}
|
||||||
className="rounded p-1 text-muted hover:bg-card-hover hover:text-foreground"
|
/>
|
||||||
aria-label="Modifier"
|
<IconButton
|
||||||
>
|
icon={<IconDuplicate />}
|
||||||
<IconEdit />
|
label="Dupliquer"
|
||||||
</button>
|
variant="primary"
|
||||||
<button
|
onClick={(e) => {
|
||||||
onClick={(e) => { e.stopPropagation(); handleDuplicate(); }}
|
e.stopPropagation();
|
||||||
className="rounded p-1 text-muted hover:bg-primary/10 hover:text-primary"
|
handleDuplicate();
|
||||||
aria-label="Dupliquer"
|
}}
|
||||||
>
|
/>
|
||||||
<IconDuplicate />
|
<IconButton
|
||||||
</button>
|
icon={<IconTrash />}
|
||||||
<button
|
label="Supprimer"
|
||||||
onClick={(e) => { e.stopPropagation(); handleDelete(); }}
|
variant="destructive"
|
||||||
className="rounded p-1 text-muted hover:bg-destructive/10 hover:text-destructive"
|
onClick={(e) => {
|
||||||
aria-label="Supprimer"
|
e.stopPropagation();
|
||||||
>
|
handleDelete();
|
||||||
<IconTrash />
|
}}
|
||||||
</button>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { forwardRef, useState, useTransition, useRef, ReactNode } from 'react';
|
|||||||
import type { SwotCategory } from '@prisma/client';
|
import type { SwotCategory } from '@prisma/client';
|
||||||
import { createSwotItem } from '@/actions/swot';
|
import { createSwotItem } from '@/actions/swot';
|
||||||
import { QuadrantHelpPanel } from './QuadrantHelp';
|
import { QuadrantHelpPanel } from './QuadrantHelp';
|
||||||
import { IconPlus, InlineFormActions } from '@/components/ui';
|
import { IconPlus, InlineAddItem } from '@/components/ui';
|
||||||
|
|
||||||
interface SwotQuadrantProps {
|
interface SwotQuadrantProps {
|
||||||
category: SwotCategory;
|
category: SwotCategory;
|
||||||
@@ -66,16 +66,6 @@ export const SwotQuadrant = forwardRef<HTMLDivElement, SwotQuadrantProps>(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleKeyDown(e: React.KeyboardEvent) {
|
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
|
||||||
e.preventDefault();
|
|
||||||
handleAdd();
|
|
||||||
} else if (e.key === 'Escape') {
|
|
||||||
setIsAdding(false);
|
|
||||||
setNewContent('');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@@ -129,32 +119,18 @@ export const SwotQuadrant = forwardRef<HTMLDivElement, SwotQuadrantProps>(
|
|||||||
|
|
||||||
{/* Add Form */}
|
{/* Add Form */}
|
||||||
{isAdding && (
|
{isAdding && (
|
||||||
<div className="rounded-lg border border-border bg-card p-2 shadow-sm">
|
<InlineAddItem
|
||||||
<textarea
|
|
||||||
autoFocus
|
|
||||||
value={newContent}
|
value={newContent}
|
||||||
onChange={(e) => setNewContent(e.target.value)}
|
onChange={setNewContent}
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
onBlur={(e) => {
|
|
||||||
// Don't trigger on blur if clicking on a button
|
|
||||||
if (!e.currentTarget.contains(e.relatedTarget as Node)) {
|
|
||||||
handleAdd();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
placeholder="Décrivez cet élément..."
|
|
||||||
className="w-full resize-none rounded border-0 bg-transparent p-1 text-sm text-foreground placeholder:text-muted focus:outline-none focus:ring-0"
|
|
||||||
rows={2}
|
|
||||||
disabled={isPending}
|
|
||||||
/>
|
|
||||||
<InlineFormActions
|
|
||||||
onCancel={() => { setIsAdding(false); setNewContent(''); }}
|
|
||||||
onSubmit={handleAdd}
|
onSubmit={handleAdd}
|
||||||
|
onCancel={() => {
|
||||||
|
setIsAdding(false);
|
||||||
|
setNewContent('');
|
||||||
|
}}
|
||||||
isPending={isPending}
|
isPending={isPending}
|
||||||
disabled={!newContent.trim()}
|
placeholder="Décrivez cet élément..."
|
||||||
submitColorClass={`${styles.text} hover:bg-white/50`}
|
submitColorClass={`${styles.text} hover:bg-white/50`}
|
||||||
className="mt-1"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ export function AddMemberModal({
|
|||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!selectedUserId || loading}
|
disabled={!selectedUserId || loading}
|
||||||
className="bg-[var(--purple)] text-white hover:opacity-90 border-transparent"
|
variant="brand"
|
||||||
>
|
>
|
||||||
{loading ? 'Ajout...' : 'Ajouter'}
|
{loading ? 'Ajout...' : 'Ajouter'}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -42,7 +42,11 @@ export function DeleteTeamButton({ teamId, teamName }: DeleteTeamButtonProps) {
|
|||||||
<Button
|
<Button
|
||||||
onClick={() => setShowModal(true)}
|
onClick={() => setShowModal(true)}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="text-destructive border-destructive hover:bg-destructive/10"
|
size="sm"
|
||||||
|
style={{
|
||||||
|
color: 'var(--destructive)',
|
||||||
|
borderColor: 'var(--destructive)',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Supprimer l'équipe
|
Supprimer l'équipe
|
||||||
</Button>
|
</Button>
|
||||||
@@ -63,7 +67,7 @@ export function DeleteTeamButton({ teamId, teamName }: DeleteTeamButtonProps) {
|
|||||||
supprimés.
|
supprimés.
|
||||||
</p>
|
</p>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button variant="ghost" onClick={() => setShowModal(false)} disabled={isPending}>
|
<Button variant="outline" onClick={() => setShowModal(false)} disabled={isPending}>
|
||||||
Annuler
|
Annuler
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="destructive" onClick={handleDelete} disabled={isPending}>
|
<Button variant="destructive" onClick={handleDelete} disabled={isPending}>
|
||||||
|
|||||||
@@ -75,10 +75,7 @@ export function MembersList({ members, teamId, isAdmin, onMemberUpdate }: Member
|
|||||||
<div className="mb-4 flex items-center justify-between">
|
<div className="mb-4 flex items-center justify-between">
|
||||||
<h3 className="text-lg font-semibold text-foreground">Membres ({members.length})</h3>
|
<h3 className="text-lg font-semibold text-foreground">Membres ({members.length})</h3>
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<Button
|
<Button onClick={() => setAddMemberOpen(true)} variant="brand" size="sm">
|
||||||
onClick={() => setAddMemberOpen(true)}
|
|
||||||
className="bg-[var(--purple)] text-white hover:opacity-90 border-transparent"
|
|
||||||
>
|
|
||||||
Ajouter un membre
|
Ajouter un membre
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { forwardRef, ButtonHTMLAttributes } from 'react';
|
import { forwardRef, ButtonHTMLAttributes } from 'react';
|
||||||
|
|
||||||
type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost' | 'destructive';
|
type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost' | 'destructive' | 'brand';
|
||||||
type ButtonSize = 'sm' | 'md' | 'lg';
|
type ButtonSize = 'sm' | 'md' | 'lg';
|
||||||
|
|
||||||
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
@@ -15,6 +15,7 @@ const variantStyles: Record<ButtonVariant, string> = {
|
|||||||
outline: 'bg-transparent text-foreground hover:bg-card-hover border-border',
|
outline: 'bg-transparent text-foreground hover:bg-card-hover border-border',
|
||||||
ghost: 'bg-transparent text-foreground hover:bg-card-hover border-transparent',
|
ghost: 'bg-transparent text-foreground hover:bg-card-hover border-transparent',
|
||||||
destructive: 'bg-destructive text-white hover:bg-destructive/90 border-transparent',
|
destructive: 'bg-destructive text-white hover:bg-destructive/90 border-transparent',
|
||||||
|
brand: 'bg-[var(--purple)] text-white hover:opacity-90 border-transparent',
|
||||||
};
|
};
|
||||||
|
|
||||||
const sizeStyles: Record<ButtonSize, string> = {
|
const sizeStyles: Record<ButtonSize, string> = {
|
||||||
@@ -23,6 +24,28 @@ const sizeStyles: Record<ButtonSize, string> = {
|
|||||||
lg: 'h-12 px-6 text-base gap-2',
|
lg: 'h-12 px-6 text-base gap-2',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface GetButtonClassNameOptions {
|
||||||
|
variant?: ButtonVariant;
|
||||||
|
size?: ButtonSize;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getButtonClassName({
|
||||||
|
variant = 'primary',
|
||||||
|
size = 'md',
|
||||||
|
className = '',
|
||||||
|
}: GetButtonClassNameOptions = {}) {
|
||||||
|
return `
|
||||||
|
inline-flex items-center justify-center rounded-lg border font-medium
|
||||||
|
transition-colors focus-visible:outline-none focus-visible:ring-2
|
||||||
|
focus-visible:ring-ring focus-visible:ring-offset-2
|
||||||
|
disabled:pointer-events-none disabled:opacity-50
|
||||||
|
${variantStyles[variant]}
|
||||||
|
${sizeStyles[size]}
|
||||||
|
${className}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
(
|
(
|
||||||
{ className = '', variant = 'primary', size = 'md', loading, disabled, children, ...props },
|
{ className = '', variant = 'primary', size = 'md', loading, disabled, children, ...props },
|
||||||
@@ -32,15 +55,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|||||||
<button
|
<button
|
||||||
ref={ref}
|
ref={ref}
|
||||||
disabled={disabled || loading}
|
disabled={disabled || loading}
|
||||||
className={`
|
className={getButtonClassName({ variant, size, className })}
|
||||||
inline-flex items-center justify-center rounded-lg border font-medium
|
|
||||||
transition-colors focus-visible:outline-none focus-visible:ring-2
|
|
||||||
focus-visible:ring-ring focus-visible:ring-offset-2
|
|
||||||
disabled:pointer-events-none disabled:opacity-50
|
|
||||||
${variantStyles[variant]}
|
|
||||||
${sizeStyles[size]}
|
|
||||||
${className}
|
|
||||||
`}
|
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{loading && (
|
{loading && (
|
||||||
|
|||||||
52
src/components/ui/Disclosure.tsx
Normal file
52
src/components/ui/Disclosure.tsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { ReactNode, useId, useState } from 'react';
|
||||||
|
|
||||||
|
interface DisclosureProps {
|
||||||
|
title: ReactNode;
|
||||||
|
subtitle?: ReactNode;
|
||||||
|
defaultOpen?: boolean;
|
||||||
|
className?: string;
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Disclosure({
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
defaultOpen = false,
|
||||||
|
className = '',
|
||||||
|
children,
|
||||||
|
}: DisclosureProps) {
|
||||||
|
const [isOpen, setIsOpen] = useState(defaultOpen);
|
||||||
|
const contentId = useId();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`rounded-lg border border-border bg-card-hover ${className}`}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsOpen((prev) => !prev)}
|
||||||
|
aria-expanded={isOpen}
|
||||||
|
aria-controls={contentId}
|
||||||
|
className="flex w-full items-center justify-between px-4 py-2.5 text-left transition-colors hover:bg-card"
|
||||||
|
>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="text-sm font-semibold text-foreground">{title}</div>
|
||||||
|
{subtitle && <div className="text-xs text-muted">{subtitle}</div>}
|
||||||
|
</div>
|
||||||
|
<svg
|
||||||
|
className={`h-4 w-4 shrink-0 text-muted transition-transform ${isOpen ? 'rotate-180' : ''}`}
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{isOpen && (
|
||||||
|
<div id={contentId} className="border-t border-border px-4 py-3">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
29
src/components/ui/DropdownMenu.tsx
Normal file
29
src/components/ui/DropdownMenu.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { ReactNode, useRef, useState } from 'react';
|
||||||
|
import { useClickOutside } from '@/hooks/useClickOutside';
|
||||||
|
|
||||||
|
interface DropdownMenuProps {
|
||||||
|
trigger: (options: { open: boolean; toggle: () => void }) => ReactNode;
|
||||||
|
children: (options: { close: () => void }) => ReactNode;
|
||||||
|
panelClassName?: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DropdownMenu({
|
||||||
|
trigger,
|
||||||
|
children,
|
||||||
|
panelClassName = '',
|
||||||
|
className = '',
|
||||||
|
}: DropdownMenuProps) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
useClickOutside(ref, () => setOpen(false), open);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={ref} className={`relative ${className}`}>
|
||||||
|
{trigger({ open, toggle: () => setOpen((prev) => !prev) })}
|
||||||
|
{open && <div className={panelClassName}>{children({ close: () => setOpen(false) })}</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
67
src/components/ui/FormField.tsx
Normal file
67
src/components/ui/FormField.tsx
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { ChangeEventHandler, ReactNode } from 'react';
|
||||||
|
import { Input } from './Input';
|
||||||
|
|
||||||
|
interface FormFieldProps {
|
||||||
|
id?: string;
|
||||||
|
label?: string;
|
||||||
|
error?: string;
|
||||||
|
hint?: string;
|
||||||
|
className?: string;
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FormField({ id, label, error, hint, className = '', children }: FormFieldProps) {
|
||||||
|
return (
|
||||||
|
<div className={className || 'w-full'}>
|
||||||
|
{label && (
|
||||||
|
<label htmlFor={id} className="mb-2 block text-sm font-medium text-foreground">
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
{children}
|
||||||
|
{hint && !error && <p className="mt-1 text-xs text-muted">{hint}</p>}
|
||||||
|
{error && <p className="mt-1.5 text-sm text-destructive">{error}</p>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type BaseInputProps = {
|
||||||
|
id?: string;
|
||||||
|
name?: string;
|
||||||
|
value?: string | number;
|
||||||
|
defaultValue?: string | number;
|
||||||
|
onChange?: ChangeEventHandler<HTMLInputElement>;
|
||||||
|
required?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
min?: string | number;
|
||||||
|
max?: string | number;
|
||||||
|
step?: string | number;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface DateInputProps extends BaseInputProps {
|
||||||
|
label?: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DateInput({ label, error, className = '', ...props }: DateInputProps) {
|
||||||
|
return (
|
||||||
|
<FormField id={props.id} label={label} error={error}>
|
||||||
|
<Input type="date" className={className} {...props} />
|
||||||
|
</FormField>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NumberInputProps extends BaseInputProps {
|
||||||
|
label?: string;
|
||||||
|
error?: string;
|
||||||
|
hint?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NumberInput({ label, error, hint, className = '', ...props }: NumberInputProps) {
|
||||||
|
return (
|
||||||
|
<FormField id={props.id} label={label} error={error} hint={hint}>
|
||||||
|
<Input type="number" className={className} {...props} />
|
||||||
|
</FormField>
|
||||||
|
);
|
||||||
|
}
|
||||||
53
src/components/ui/IconButton.tsx
Normal file
53
src/components/ui/IconButton.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { ButtonHTMLAttributes, ReactNode } from 'react';
|
||||||
|
|
||||||
|
type IconButtonVariant = 'default' | 'primary' | 'destructive' | 'ghost';
|
||||||
|
type IconButtonSize = 'xs' | 'sm' | 'md';
|
||||||
|
|
||||||
|
interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
|
icon: ReactNode;
|
||||||
|
label: string;
|
||||||
|
variant?: IconButtonVariant;
|
||||||
|
size?: IconButtonSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
const variantStyles: Record<IconButtonVariant, string> = {
|
||||||
|
default: 'text-muted hover:bg-card-hover hover:text-foreground border-transparent',
|
||||||
|
primary: 'text-muted hover:bg-primary/10 hover:text-primary border-transparent',
|
||||||
|
destructive:
|
||||||
|
'text-muted hover:bg-destructive/10 hover:text-destructive border-transparent',
|
||||||
|
ghost: 'text-muted hover:text-foreground border-transparent',
|
||||||
|
};
|
||||||
|
|
||||||
|
const sizeStyles: Record<IconButtonSize, string> = {
|
||||||
|
xs: 'h-6 w-6',
|
||||||
|
sm: 'h-7 w-7',
|
||||||
|
md: 'h-8 w-8',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function IconButton({
|
||||||
|
icon,
|
||||||
|
label,
|
||||||
|
variant = 'default',
|
||||||
|
size = 'sm',
|
||||||
|
className = '',
|
||||||
|
type = 'button',
|
||||||
|
...props
|
||||||
|
}: IconButtonProps) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type={type}
|
||||||
|
aria-label={label}
|
||||||
|
title={label}
|
||||||
|
className={`
|
||||||
|
inline-flex items-center justify-center rounded-md border
|
||||||
|
transition-colors disabled:pointer-events-none disabled:opacity-50
|
||||||
|
${sizeStyles[size]}
|
||||||
|
${variantStyles[variant]}
|
||||||
|
${className}
|
||||||
|
`}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
59
src/components/ui/InlineAddItem.tsx
Normal file
59
src/components/ui/InlineAddItem.tsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { ReactNode } from 'react';
|
||||||
|
import { InlineFormActions } from './InlineFormActions';
|
||||||
|
|
||||||
|
interface InlineAddItemProps {
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
onSubmit: () => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
isPending: boolean;
|
||||||
|
placeholder?: string;
|
||||||
|
rows?: number;
|
||||||
|
extra?: ReactNode;
|
||||||
|
submitColorClass?: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function InlineAddItem({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
onSubmit,
|
||||||
|
onCancel,
|
||||||
|
isPending,
|
||||||
|
placeholder,
|
||||||
|
rows = 2,
|
||||||
|
extra,
|
||||||
|
submitColorClass,
|
||||||
|
className = '',
|
||||||
|
}: InlineAddItemProps) {
|
||||||
|
return (
|
||||||
|
<div className={`rounded-lg border border-border bg-card p-2 shadow-sm ${className}`}>
|
||||||
|
<textarea
|
||||||
|
autoFocus
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
onSubmit();
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
onCancel();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder={placeholder}
|
||||||
|
className="w-full resize-none rounded border-0 bg-transparent p-1 text-sm text-foreground placeholder:text-muted focus:outline-none focus:ring-0"
|
||||||
|
rows={rows}
|
||||||
|
disabled={isPending}
|
||||||
|
/>
|
||||||
|
{extra}
|
||||||
|
<InlineFormActions
|
||||||
|
onCancel={onCancel}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
isPending={isPending}
|
||||||
|
disabled={!value.trim()}
|
||||||
|
submitColorClass={submitColorClass}
|
||||||
|
className="mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
68
src/components/ui/InlineEditor.tsx
Normal file
68
src/components/ui/InlineEditor.tsx
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
|
interface InlineEditorProps {
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
onSave: () => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
isPending: boolean;
|
||||||
|
placeholder?: string;
|
||||||
|
rows?: number;
|
||||||
|
submitLabel?: string;
|
||||||
|
footer?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function InlineEditor({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
onSave,
|
||||||
|
onCancel,
|
||||||
|
isPending,
|
||||||
|
placeholder,
|
||||||
|
rows = 2,
|
||||||
|
submitLabel = 'Enregistrer',
|
||||||
|
footer,
|
||||||
|
}: InlineEditorProps) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<textarea
|
||||||
|
autoFocus
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey && !e.ctrlKey && !e.metaKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
onSave();
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
onCancel();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="w-full resize-none rounded border-0 bg-transparent p-0 text-sm text-foreground focus:outline-none focus:ring-0"
|
||||||
|
rows={rows}
|
||||||
|
disabled={isPending}
|
||||||
|
placeholder={placeholder}
|
||||||
|
/>
|
||||||
|
{footer}
|
||||||
|
{!footer && (
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="rounded px-2 py-1 text-xs text-muted hover:bg-card-hover"
|
||||||
|
onClick={onCancel}
|
||||||
|
disabled={isPending}
|
||||||
|
>
|
||||||
|
Annuler
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="rounded px-2 py-1 text-xs font-medium text-primary hover:bg-primary/10"
|
||||||
|
onClick={onSave}
|
||||||
|
disabled={isPending || !value.trim()}
|
||||||
|
>
|
||||||
|
{submitLabel}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Button } from './Button';
|
||||||
|
|
||||||
interface InlineFormActionsProps {
|
interface InlineFormActionsProps {
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
onSubmit: () => void;
|
onSubmit: () => void;
|
||||||
@@ -19,21 +21,27 @@ export function InlineFormActions({
|
|||||||
}: InlineFormActionsProps) {
|
}: InlineFormActionsProps) {
|
||||||
return (
|
return (
|
||||||
<div className={`flex justify-end gap-1 ${className}`}>
|
<div className={`flex justify-end gap-1 ${className}`}>
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
onClick={onCancel}
|
onClick={onCancel}
|
||||||
className="rounded px-2 py-1 text-xs text-muted hover:bg-card-hover"
|
className="h-7 px-2 text-xs text-muted"
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
>
|
>
|
||||||
Annuler
|
Annuler
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
onMouseDown={(e) => e.preventDefault()}
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
onClick={onSubmit}
|
onClick={onSubmit}
|
||||||
disabled={isPending || disabled}
|
disabled={isPending || disabled}
|
||||||
className={`rounded px-2 py-1 text-xs font-medium disabled:opacity-50 ${submitColorClass}`}
|
className={`h-7 px-2 text-xs ${submitColorClass}`}
|
||||||
>
|
>
|
||||||
{isPending ? '...' : submitLabel}
|
{isPending ? '...' : submitLabel}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
59
src/components/ui/SegmentedControl.tsx
Normal file
59
src/components/ui/SegmentedControl.tsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
|
export interface SegmentedOption<T extends string> {
|
||||||
|
value: T;
|
||||||
|
label: ReactNode;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SegmentedControlProps<T extends string> {
|
||||||
|
value: T;
|
||||||
|
options: SegmentedOption<T>[];
|
||||||
|
onChange: (value: T) => void;
|
||||||
|
size?: 'sm' | 'md';
|
||||||
|
fullWidth?: boolean;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sizeStyles = {
|
||||||
|
sm: 'px-3 py-1.5 text-sm',
|
||||||
|
md: 'px-4 py-2 text-sm',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export function SegmentedControl<T extends string>({
|
||||||
|
value,
|
||||||
|
options,
|
||||||
|
onChange,
|
||||||
|
size = 'md',
|
||||||
|
fullWidth = false,
|
||||||
|
className = '',
|
||||||
|
}: SegmentedControlProps<T>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`inline-flex items-center gap-1 rounded-lg border border-border bg-card p-1 ${className}`}
|
||||||
|
>
|
||||||
|
{options.map((option) => (
|
||||||
|
<button
|
||||||
|
key={option.value}
|
||||||
|
type="button"
|
||||||
|
disabled={option.disabled}
|
||||||
|
onClick={() => onChange(option.value)}
|
||||||
|
className={`
|
||||||
|
rounded-md font-medium transition-colors disabled:pointer-events-none disabled:opacity-50
|
||||||
|
${sizeStyles[size]}
|
||||||
|
${fullWidth ? 'min-w-0 flex-1' : ''}
|
||||||
|
${
|
||||||
|
value === option.value
|
||||||
|
? 'bg-primary text-primary-foreground'
|
||||||
|
: 'text-muted hover:bg-card-hover hover:text-foreground'
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
export { Avatar } from './Avatar';
|
export { Avatar } from './Avatar';
|
||||||
export { Badge } from './Badge';
|
export { Badge } from './Badge';
|
||||||
export { Button } from './Button';
|
export { Button, getButtonClassName } from './Button';
|
||||||
export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from './Card';
|
export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from './Card';
|
||||||
export { CollaboratorDisplay } from './CollaboratorDisplay';
|
export { CollaboratorDisplay } from './CollaboratorDisplay';
|
||||||
|
export { Disclosure } from './Disclosure';
|
||||||
|
export { DropdownMenu } from './DropdownMenu';
|
||||||
export { EditableTitle } from './EditableTitle';
|
export { EditableTitle } from './EditableTitle';
|
||||||
export {
|
export {
|
||||||
EditableSessionTitle,
|
EditableSessionTitle,
|
||||||
@@ -13,6 +15,9 @@ export {
|
|||||||
EditableGifMoodTitle,
|
EditableGifMoodTitle,
|
||||||
} from './EditableTitles';
|
} from './EditableTitles';
|
||||||
export { IconEdit, IconTrash, IconDuplicate, IconPlus, IconCheck, IconClose } from './Icons';
|
export { IconEdit, IconTrash, IconDuplicate, IconPlus, IconCheck, IconClose } from './Icons';
|
||||||
|
export { IconButton } from './IconButton';
|
||||||
|
export { InlineAddItem } from './InlineAddItem';
|
||||||
|
export { InlineEditor } from './InlineEditor';
|
||||||
export { InlineFormActions } from './InlineFormActions';
|
export { InlineFormActions } from './InlineFormActions';
|
||||||
export { PageHeader } from './PageHeader';
|
export { PageHeader } from './PageHeader';
|
||||||
export { SessionPageHeader } from './SessionPageHeader';
|
export { SessionPageHeader } from './SessionPageHeader';
|
||||||
@@ -22,6 +27,8 @@ export { Modal, ModalFooter } from './Modal';
|
|||||||
export { RocketIcon } from './RocketIcon';
|
export { RocketIcon } from './RocketIcon';
|
||||||
export { Select } from './Select';
|
export { Select } from './Select';
|
||||||
export type { SelectOption } from './Select';
|
export type { SelectOption } from './Select';
|
||||||
|
export { SegmentedControl } from './SegmentedControl';
|
||||||
export { Textarea } from './Textarea';
|
export { Textarea } from './Textarea';
|
||||||
export { ToggleGroup } from './ToggleGroup';
|
export { ToggleGroup } from './ToggleGroup';
|
||||||
export type { ToggleOption } from './ToggleGroup';
|
export type { ToggleOption } from './ToggleGroup';
|
||||||
|
export { FormField, DateInput, NumberInput } from './FormField';
|
||||||
|
|||||||
@@ -1,58 +1,36 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { Disclosure } from '@/components/ui';
|
||||||
|
|
||||||
export function WeatherInfoPanel() {
|
export function WeatherInfoPanel() {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-6 rounded-lg border border-border bg-card-hover">
|
<Disclosure title="Les 4 axes de la météo personnelle" className="mb-6">
|
||||||
<button
|
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
|
||||||
className="w-full flex items-center justify-between px-4 py-2.5 text-left transition-colors hover:bg-card"
|
|
||||||
>
|
|
||||||
<h3 className="text-sm font-semibold text-foreground">
|
|
||||||
Les 4 axes de la météo personnelle
|
|
||||||
</h3>
|
|
||||||
<svg
|
|
||||||
className={`h-4 w-4 text-muted transition-transform ${isOpen ? 'rotate-180' : ''}`}
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
{isOpen && (
|
|
||||||
<div className="border-t border-border px-4 py-3">
|
|
||||||
<div className="grid gap-2.5 sm:grid-cols-2 lg:grid-cols-4">
|
<div className="grid gap-2.5 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-medium text-foreground mb-0.5">☀️ Performance</p>
|
<p className="mb-0.5 text-xs font-medium text-foreground">☀️ Performance</p>
|
||||||
<p className="text-xs text-muted leading-relaxed">
|
<p className="text-xs leading-relaxed text-muted">
|
||||||
Votre performance personnelle et l'atteinte de vos objectifs
|
Votre performance personnelle et l'atteinte de vos objectifs
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-medium text-foreground mb-0.5">😊 Moral</p>
|
<p className="mb-0.5 text-xs font-medium text-foreground">😊 Moral</p>
|
||||||
<p className="text-xs text-muted leading-relaxed">
|
<p className="text-xs leading-relaxed text-muted">
|
||||||
Votre moral actuel et votre ressenti
|
Votre moral actuel et votre ressenti
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-medium text-foreground mb-0.5">🌊 Flux</p>
|
<p className="mb-0.5 text-xs font-medium text-foreground">🌊 Flux</p>
|
||||||
<p className="text-xs text-muted leading-relaxed">
|
<p className="text-xs leading-relaxed text-muted">
|
||||||
Votre flux de travail personnel et les blocages éventuels
|
Votre flux de travail personnel et les blocages éventuels
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-medium text-foreground mb-0.5">💎 Création de valeur</p>
|
<p className="mb-0.5 text-xs font-medium text-foreground">💎 Création de valeur</p>
|
||||||
<p className="text-xs text-muted leading-relaxed">
|
<p className="text-xs leading-relaxed text-muted">
|
||||||
Votre création de valeur et votre apport
|
Votre création de valeur et votre apport
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Disclosure>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { Disclosure } from '@/components/ui';
|
||||||
import type { WeatherHistoryPoint } from '@/services/weather';
|
import type { WeatherHistoryPoint } from '@/services/weather';
|
||||||
|
|
||||||
interface WeatherTrendChartProps {
|
interface WeatherTrendChartProps {
|
||||||
@@ -63,7 +64,6 @@ function buildPath(
|
|||||||
const Y_TICKS = [1, 5, 10, 14, 19];
|
const Y_TICKS = [1, 5, 10, 14, 19];
|
||||||
|
|
||||||
export function WeatherTrendChart({ data, currentSessionId }: WeatherTrendChartProps) {
|
export function WeatherTrendChart({ data, currentSessionId }: WeatherTrendChartProps) {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const [hoveredIdx, setHoveredIdx] = useState<number | null>(null);
|
const [hoveredIdx, setHoveredIdx] = useState<number | null>(null);
|
||||||
|
|
||||||
if (data.length < 2) return null;
|
if (data.length < 2) return null;
|
||||||
@@ -71,27 +71,16 @@ export function WeatherTrendChart({ data, currentSessionId }: WeatherTrendChartP
|
|||||||
const hoveredPt = hoveredIdx !== null ? data[hoveredIdx] : null;
|
const hoveredPt = hoveredIdx !== null ? data[hoveredIdx] : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-6 rounded-lg border border-border bg-card-hover">
|
<Disclosure
|
||||||
<button
|
className="mb-6"
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
title={
|
||||||
className="w-full flex items-center justify-between px-4 py-2.5 text-left transition-colors hover:bg-card"
|
<>
|
||||||
>
|
|
||||||
<h3 className="text-sm font-semibold text-foreground">
|
|
||||||
Évolution dans le temps
|
Évolution dans le temps
|
||||||
<span className="ml-2 text-xs font-normal text-muted">{data.length} sessions</span>
|
<span className="ml-2 text-xs font-normal text-muted">{data.length} sessions</span>
|
||||||
</h3>
|
</>
|
||||||
<svg
|
}
|
||||||
className={`h-4 w-4 text-muted transition-transform ${isOpen ? 'rotate-180' : ''}`}
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
>
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
<div className="py-1">
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{isOpen && (
|
|
||||||
<div className="border-t border-border px-4 py-4">
|
|
||||||
{/* Legend */}
|
{/* Legend */}
|
||||||
<div className="flex flex-wrap gap-4 mb-3">
|
<div className="flex flex-wrap gap-4 mb-3">
|
||||||
{INDICATORS.map((ind) => (
|
{INDICATORS.map((ind) => (
|
||||||
@@ -301,7 +290,6 @@ export function WeatherTrendChart({ data, currentSessionId }: WeatherTrendChartP
|
|||||||
Score 1 = ☀️ (meilleur) · Score 19 = dégradé
|
Score 1 = ☀️ (meilleur) · Score 19 = dégradé
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</Disclosure>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { forwardRef, memo, useState, useTransition } from 'react';
|
|||||||
import type { WeeklyCheckInItem } from '@prisma/client';
|
import type { WeeklyCheckInItem } from '@prisma/client';
|
||||||
import { updateWeeklyCheckInItem, deleteWeeklyCheckInItem } from '@/actions/weekly-checkin';
|
import { updateWeeklyCheckInItem, deleteWeeklyCheckInItem } from '@/actions/weekly-checkin';
|
||||||
import { WEEKLY_CHECK_IN_BY_CATEGORY, EMOTION_BY_TYPE } from '@/lib/types';
|
import { WEEKLY_CHECK_IN_BY_CATEGORY, EMOTION_BY_TYPE } from '@/lib/types';
|
||||||
import { IconEdit, IconTrash, InlineFormActions } from '@/components/ui';
|
import { IconEdit, IconTrash, IconButton, InlineFormActions } from '@/components/ui';
|
||||||
import { Select } from '@/components/ui/Select';
|
import { Select } from '@/components/ui/Select';
|
||||||
|
|
||||||
interface WeeklyCheckInCardProps {
|
interface WeeklyCheckInCardProps {
|
||||||
@@ -144,23 +144,23 @@ export const WeeklyCheckInCard = memo(
|
|||||||
|
|
||||||
{/* Actions (visible on hover) */}
|
{/* Actions (visible on hover) */}
|
||||||
<div className="absolute right-1 top-1 flex gap-0.5 opacity-0 transition-opacity group-hover:opacity-100">
|
<div className="absolute right-1 top-1 flex gap-0.5 opacity-0 transition-opacity group-hover:opacity-100">
|
||||||
<button
|
<IconButton
|
||||||
|
icon={<IconEdit />}
|
||||||
|
label="Modifier"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setIsEditing(true);
|
setIsEditing(true);
|
||||||
}}
|
}}
|
||||||
className="rounded p-1 text-muted hover:bg-card-hover hover:text-foreground"
|
/>
|
||||||
aria-label="Modifier"
|
<IconButton
|
||||||
>
|
icon={<IconTrash />}
|
||||||
<IconEdit />
|
label="Supprimer"
|
||||||
</button>
|
variant="destructive"
|
||||||
<button
|
onClick={(e) => {
|
||||||
onClick={(e) => { e.stopPropagation(); handleDelete(); }}
|
e.stopPropagation();
|
||||||
className="rounded p-1 text-muted hover:bg-destructive/10 hover:text-destructive"
|
handleDelete();
|
||||||
aria-label="Supprimer"
|
}}
|
||||||
>
|
/>
|
||||||
<IconTrash />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { forwardRef, useState, useTransition, useRef, ReactNode } from 'react';
|
|||||||
import type { WeeklyCheckInCategory } from '@prisma/client';
|
import type { WeeklyCheckInCategory } from '@prisma/client';
|
||||||
import { createWeeklyCheckInItem } from '@/actions/weekly-checkin';
|
import { createWeeklyCheckInItem } from '@/actions/weekly-checkin';
|
||||||
import { WEEKLY_CHECK_IN_BY_CATEGORY, EMOTION_BY_TYPE } from '@/lib/types';
|
import { WEEKLY_CHECK_IN_BY_CATEGORY, EMOTION_BY_TYPE } from '@/lib/types';
|
||||||
import { IconPlus, InlineFormActions } from '@/components/ui';
|
import { IconPlus, InlineAddItem } from '@/components/ui';
|
||||||
import { Select } from '@/components/ui/Select';
|
import { Select } from '@/components/ui/Select';
|
||||||
|
|
||||||
interface WeeklyCheckInSectionProps {
|
interface WeeklyCheckInSectionProps {
|
||||||
@@ -44,17 +44,6 @@ export const WeeklyCheckInSection = forwardRef<HTMLDivElement, WeeklyCheckInSect
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleKeyDown(e: React.KeyboardEvent) {
|
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
|
||||||
e.preventDefault();
|
|
||||||
handleAdd();
|
|
||||||
} else if (e.key === 'Escape') {
|
|
||||||
setIsAdding(false);
|
|
||||||
setNewContent('');
|
|
||||||
setNewEmotion('NONE');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@@ -93,53 +82,31 @@ export const WeeklyCheckInSection = forwardRef<HTMLDivElement, WeeklyCheckInSect
|
|||||||
|
|
||||||
{/* Add Form */}
|
{/* Add Form */}
|
||||||
{isAdding && (
|
{isAdding && (
|
||||||
<div
|
<InlineAddItem
|
||||||
className="rounded-lg border border-border bg-card p-2 shadow-sm"
|
value={newContent}
|
||||||
onBlur={(e) => {
|
onChange={setNewContent}
|
||||||
// Don't close if focus moves to another element in this container
|
onSubmit={handleAdd}
|
||||||
const currentTarget = e.currentTarget;
|
onCancel={() => {
|
||||||
const relatedTarget = e.relatedTarget as Node | null;
|
|
||||||
if (relatedTarget && currentTarget.contains(relatedTarget)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Only add on blur if content is not empty
|
|
||||||
if (newContent.trim()) {
|
|
||||||
handleAdd();
|
|
||||||
} else {
|
|
||||||
setIsAdding(false);
|
setIsAdding(false);
|
||||||
setNewContent('');
|
setNewContent('');
|
||||||
setNewEmotion('NONE');
|
setNewEmotion('NONE');
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
isPending={isPending}
|
||||||
<textarea
|
|
||||||
autoFocus
|
|
||||||
value={newContent}
|
|
||||||
onChange={(e) => setNewContent(e.target.value)}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
placeholder={`Décrivez ${config.title.toLowerCase()}...`}
|
placeholder={`Décrivez ${config.title.toLowerCase()}...`}
|
||||||
className="w-full resize-none rounded border-0 bg-transparent p-1 text-sm text-foreground placeholder:text-muted focus:outline-none focus:ring-0"
|
extra={
|
||||||
rows={2}
|
<div className="mt-2">
|
||||||
disabled={isPending}
|
|
||||||
/>
|
|
||||||
<div className="mt-2 flex items-center justify-between gap-2">
|
|
||||||
<Select
|
<Select
|
||||||
value={newEmotion}
|
value={newEmotion}
|
||||||
onChange={(e) => setNewEmotion(e.target.value as typeof newEmotion)}
|
onChange={(e) => setNewEmotion(e.target.value as typeof newEmotion)}
|
||||||
className="text-xs flex-1"
|
className="text-xs"
|
||||||
options={Object.values(EMOTION_BY_TYPE).map((em) => ({
|
options={Object.values(EMOTION_BY_TYPE).map((em) => ({
|
||||||
value: em.emotion,
|
value: em.emotion,
|
||||||
label: `${em.icon} ${em.label}`,
|
label: `${em.icon} ${em.label}`,
|
||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
<InlineFormActions
|
</div>
|
||||||
onCancel={() => { setIsAdding(false); setNewContent(''); setNewEmotion('NONE'); }}
|
}
|
||||||
onSubmit={handleAdd}
|
|
||||||
isPending={isPending}
|
|
||||||
disabled={!newContent.trim()}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { forwardRef, memo, useState, useTransition } from 'react';
|
|||||||
import type { YearReviewItem } from '@prisma/client';
|
import type { YearReviewItem } from '@prisma/client';
|
||||||
import { updateYearReviewItem, deleteYearReviewItem } from '@/actions/year-review';
|
import { updateYearReviewItem, deleteYearReviewItem } from '@/actions/year-review';
|
||||||
import { YEAR_REVIEW_BY_CATEGORY } from '@/lib/types';
|
import { YEAR_REVIEW_BY_CATEGORY } from '@/lib/types';
|
||||||
import { IconEdit, IconTrash } from '@/components/ui';
|
import { IconEdit, IconTrash, IconButton } from '@/components/ui';
|
||||||
|
|
||||||
interface YearReviewCardProps {
|
interface YearReviewCardProps {
|
||||||
item: YearReviewItem;
|
item: YearReviewItem;
|
||||||
@@ -88,23 +88,23 @@ export const YearReviewCard = memo(
|
|||||||
|
|
||||||
{/* Actions (visible on hover) */}
|
{/* Actions (visible on hover) */}
|
||||||
<div className="absolute right-1 top-1 flex gap-0.5 opacity-0 transition-opacity group-hover:opacity-100">
|
<div className="absolute right-1 top-1 flex gap-0.5 opacity-0 transition-opacity group-hover:opacity-100">
|
||||||
<button
|
<IconButton
|
||||||
|
icon={<IconEdit />}
|
||||||
|
label="Modifier"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setIsEditing(true);
|
setIsEditing(true);
|
||||||
}}
|
}}
|
||||||
className="rounded p-1 text-muted hover:bg-card-hover hover:text-foreground"
|
/>
|
||||||
aria-label="Modifier"
|
<IconButton
|
||||||
>
|
icon={<IconTrash />}
|
||||||
<IconEdit />
|
label="Supprimer"
|
||||||
</button>
|
variant="destructive"
|
||||||
<button
|
onClick={(e) => {
|
||||||
onClick={(e) => { e.stopPropagation(); handleDelete(); }}
|
e.stopPropagation();
|
||||||
className="rounded p-1 text-muted hover:bg-destructive/10 hover:text-destructive"
|
handleDelete();
|
||||||
aria-label="Supprimer"
|
}}
|
||||||
>
|
/>
|
||||||
<IconTrash />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { forwardRef, useState, useTransition, useRef, ReactNode } from 'react';
|
|||||||
import type { YearReviewCategory } from '@prisma/client';
|
import type { YearReviewCategory } from '@prisma/client';
|
||||||
import { createYearReviewItem } from '@/actions/year-review';
|
import { createYearReviewItem } from '@/actions/year-review';
|
||||||
import { YEAR_REVIEW_BY_CATEGORY } from '@/lib/types';
|
import { YEAR_REVIEW_BY_CATEGORY } from '@/lib/types';
|
||||||
import { IconPlus, InlineFormActions } from '@/components/ui';
|
import { IconPlus, InlineAddItem } from '@/components/ui';
|
||||||
|
|
||||||
interface YearReviewSectionProps {
|
interface YearReviewSectionProps {
|
||||||
category: YearReviewCategory;
|
category: YearReviewCategory;
|
||||||
@@ -40,16 +40,6 @@ export const YearReviewSection = forwardRef<HTMLDivElement, YearReviewSectionPro
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleKeyDown(e: React.KeyboardEvent) {
|
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
|
||||||
e.preventDefault();
|
|
||||||
handleAdd();
|
|
||||||
} else if (e.key === 'Escape') {
|
|
||||||
setIsAdding(false);
|
|
||||||
setNewContent('');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@@ -88,31 +78,17 @@ export const YearReviewSection = forwardRef<HTMLDivElement, YearReviewSectionPro
|
|||||||
|
|
||||||
{/* Add Form */}
|
{/* Add Form */}
|
||||||
{isAdding && (
|
{isAdding && (
|
||||||
<div className="rounded-lg border border-border bg-card p-2 shadow-sm">
|
<InlineAddItem
|
||||||
<textarea
|
|
||||||
autoFocus
|
|
||||||
value={newContent}
|
value={newContent}
|
||||||
onChange={(e) => setNewContent(e.target.value)}
|
onChange={setNewContent}
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
onBlur={(e) => {
|
|
||||||
// Don't trigger on blur if clicking on a button
|
|
||||||
if (!e.currentTarget.contains(e.relatedTarget as Node)) {
|
|
||||||
handleAdd();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
placeholder={`Décrivez ${config.title.toLowerCase()}...`}
|
|
||||||
className="w-full resize-none rounded border-0 bg-transparent p-1 text-sm text-foreground placeholder:text-muted focus:outline-none focus:ring-0"
|
|
||||||
rows={2}
|
|
||||||
disabled={isPending}
|
|
||||||
/>
|
|
||||||
<InlineFormActions
|
|
||||||
onCancel={() => { setIsAdding(false); setNewContent(''); }}
|
|
||||||
onSubmit={handleAdd}
|
onSubmit={handleAdd}
|
||||||
|
onCancel={() => {
|
||||||
|
setIsAdding(false);
|
||||||
|
setNewContent('');
|
||||||
|
}}
|
||||||
isPending={isPending}
|
isPending={isPending}
|
||||||
disabled={!newContent.trim()}
|
placeholder={`Décrivez ${config.title.toLowerCase()}...`}
|
||||||
className="mt-1"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user