refactor(studio): CreateProjectModal module, re-export, stories
- Module create-project-modal: types, useCreateProjectModal, Header, Form, Footer, Skeleton, orchestrator CreateProjectModal - Re-export from projects/CreateProjectModal.tsx - Stories: Default, Loading (Skeleton); decorator min-h-layout-story Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
3ad473fde2
commit
946b6721b3
10 changed files with 281 additions and 135 deletions
|
|
@ -1,17 +1,27 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { CreateProjectModal } from './CreateProjectModal';
|
||||
import { CreateProjectModal, CreateProjectModalSkeleton } from './CreateProjectModal';
|
||||
import { fn } from '@storybook/test';
|
||||
|
||||
const meta: Meta<typeof CreateProjectModal> = {
|
||||
title: 'Components/Features/Studio/Projects/CreateProjectModal',
|
||||
component: CreateProjectModal,
|
||||
parameters: { layout: 'centered' },
|
||||
tags: ['autodocs'],
|
||||
args: { onClose: fn() },
|
||||
title: 'Components/Features/Studio/Projects/CreateProjectModal',
|
||||
component: CreateProjectModal,
|
||||
parameters: { layout: 'centered' },
|
||||
tags: ['autodocs'],
|
||||
args: { onClose: fn() },
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background min-h-layout-story flex items-center justify-center p-4">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = { name: 'Par défaut' };
|
||||
export const Creating: Story = { name: 'Création' };
|
||||
export const Default: Story = {};
|
||||
|
||||
export const Loading: Story = {
|
||||
render: () => <CreateProjectModalSkeleton />,
|
||||
};
|
||||
|
|
@ -1,127 +1,4 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Button } from '../../ui/button';
|
||||
import { Input } from '../../ui/input';
|
||||
import { X, Layers } from 'lucide-react';
|
||||
|
||||
interface CreateProjectModalProps {
|
||||
onClose: () => void;
|
||||
onCreate: (project: any) => void;
|
||||
}
|
||||
|
||||
export const CreateProjectModal: React.FC<CreateProjectModalProps> = ({
|
||||
onClose,
|
||||
onCreate,
|
||||
}) => {
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
daw: 'Ableton',
|
||||
bpm: '128',
|
||||
key: 'C Min',
|
||||
description: '',
|
||||
});
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!formData.name) return;
|
||||
onCreate({
|
||||
...formData,
|
||||
progress: 0,
|
||||
status: 'Idea',
|
||||
collaborators: [],
|
||||
modified: 'Just now',
|
||||
});
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4">
|
||||
<div
|
||||
className="absolute inset-0 bg-kodo-void/90 backdrop-blur-sm"
|
||||
onClick={onClose}
|
||||
></div>
|
||||
<div className="relative w-full max-w-lg bg-kodo-graphite border border-kodo-steel rounded-xl shadow-2xl animate-scaleIn overflow-hidden">
|
||||
<div className="p-4 border-b border-kodo-steel bg-kodo-ink flex justify-between items-center">
|
||||
<h3 className="font-bold text-white flex items-center gap-2">
|
||||
<Layers className="w-5 h-5 text-kodo-steel" /> New Project
|
||||
</h3>
|
||||
<button onClick={onClose}>
|
||||
<X className="w-5 h-5 text-kodo-content-dim hover:text-white" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-6 space-y-6">
|
||||
<Input
|
||||
label="Project Name"
|
||||
placeholder="e.g. Neon Genesis"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-kodo-content-dim uppercase mb-2">
|
||||
Primary Workstation (DAW)
|
||||
</label>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{['Ableton', 'FL Studio', 'Logic Pro'].map((daw) => (
|
||||
<button
|
||||
key={daw}
|
||||
onClick={() => setFormData({ ...formData, daw })}
|
||||
className={`p-4 rounded-lg border text-sm font-bold transition-all ${formData.daw === daw ? 'bg-kodo-cyan/10 border-kodo-cyan text-white' : 'bg-kodo-void border-kodo-steel text-kodo-content-dim hover:border-kodo-steel'}`}
|
||||
>
|
||||
{daw}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Input
|
||||
label="BPM"
|
||||
type="number"
|
||||
placeholder="128"
|
||||
value={formData.bpm}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, bpm: e.target.value })
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
label="Key"
|
||||
placeholder="C Minor"
|
||||
value={formData.key}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, key: e.target.value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-kodo-content-dim uppercase mb-2">
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
className="w-full bg-kodo-void border border-kodo-steel rounded-lg p-4 text-white focus:border-kodo-steel outline-none text-sm resize-none h-24"
|
||||
placeholder="Project goals, vibe, or reference tracks..."
|
||||
value={formData.description}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, description: e.target.value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 border-t border-kodo-steel bg-kodo-ink flex justify-end gap-4">
|
||||
<Button variant="ghost" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleSubmit}
|
||||
disabled={!formData.name}
|
||||
>
|
||||
Create Project
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export {
|
||||
CreateProjectModal,
|
||||
CreateProjectModalSkeleton,
|
||||
} from './create-project-modal';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
import React from 'react';
|
||||
import { useCreateProjectModal } from './useCreateProjectModal';
|
||||
import { CreateProjectModalHeader } from './CreateProjectModalHeader';
|
||||
import { CreateProjectModalForm } from './CreateProjectModalForm';
|
||||
import { CreateProjectModalFooter } from './CreateProjectModalFooter';
|
||||
import type { CreateProjectModalProps } from './types';
|
||||
|
||||
export function CreateProjectModal({ onClose, onCreate }: CreateProjectModalProps) {
|
||||
const { formData, updateField, handleSubmit, canSubmit } = useCreateProjectModal(onCreate);
|
||||
|
||||
const handleSubmitAndClose = () => {
|
||||
handleSubmit();
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4">
|
||||
<div
|
||||
className="absolute inset-0 bg-kodo-void/90 backdrop-blur-sm"
|
||||
onClick={onClose}
|
||||
aria-hidden
|
||||
/>
|
||||
<div className="relative w-full max-w-lg bg-kodo-graphite border border-kodo-steel rounded-xl shadow-2xl animate-scaleIn overflow-hidden">
|
||||
<CreateProjectModalHeader onClose={onClose} />
|
||||
<CreateProjectModalForm formData={formData} onFieldChange={updateField} />
|
||||
<CreateProjectModalFooter
|
||||
onClose={onClose}
|
||||
onSubmit={handleSubmitAndClose}
|
||||
canSubmit={canSubmit}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import { Button } from '@/components/ui/button';
|
||||
|
||||
interface CreateProjectModalFooterProps {
|
||||
onClose: () => void;
|
||||
onSubmit: () => void;
|
||||
canSubmit: boolean;
|
||||
}
|
||||
|
||||
export function CreateProjectModalFooter({
|
||||
onClose,
|
||||
onSubmit,
|
||||
canSubmit,
|
||||
}: CreateProjectModalFooterProps) {
|
||||
return (
|
||||
<div className="p-4 border-t border-kodo-steel bg-kodo-ink flex justify-end gap-4">
|
||||
<Button variant="ghost" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" onClick={onSubmit} disabled={!canSubmit}>
|
||||
Create Project
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
import { Input } from '@/components/ui/input';
|
||||
import { DAW_OPTIONS } from './types';
|
||||
import type { CreateProjectFormState } from './types';
|
||||
|
||||
interface CreateProjectModalFormProps {
|
||||
formData: CreateProjectFormState;
|
||||
onFieldChange: <K extends keyof CreateProjectFormState>(
|
||||
field: K,
|
||||
value: CreateProjectFormState[K],
|
||||
) => void;
|
||||
}
|
||||
|
||||
export function CreateProjectModalForm({
|
||||
formData,
|
||||
onFieldChange,
|
||||
}: CreateProjectModalFormProps) {
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<Input
|
||||
label="Project Name"
|
||||
placeholder="e.g. Neon Genesis"
|
||||
value={formData.name}
|
||||
onChange={(e) => onFieldChange('name', e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-kodo-content-dim uppercase mb-2">
|
||||
Primary Workstation (DAW)
|
||||
</label>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{DAW_OPTIONS.map((daw) => (
|
||||
<button
|
||||
key={daw}
|
||||
type="button"
|
||||
onClick={() => onFieldChange('daw', daw)}
|
||||
className={`p-4 rounded-lg border text-sm font-bold transition-all ${formData.daw === daw ? 'bg-kodo-cyan/10 border-kodo-cyan text-white' : 'bg-kodo-void border-kodo-steel text-kodo-content-dim hover:border-kodo-steel'}`}
|
||||
>
|
||||
{daw}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Input
|
||||
label="BPM"
|
||||
type="number"
|
||||
placeholder="128"
|
||||
value={formData.bpm}
|
||||
onChange={(e) => onFieldChange('bpm', e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
label="Key"
|
||||
placeholder="C Minor"
|
||||
value={formData.key}
|
||||
onChange={(e) => onFieldChange('key', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-kodo-content-dim uppercase mb-2">
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
className="w-full bg-kodo-void border border-kodo-steel rounded-lg p-4 text-white focus:border-kodo-steel outline-none text-sm resize-none h-24"
|
||||
placeholder="Project goals, vibe, or reference tracks..."
|
||||
value={formData.description}
|
||||
onChange={(e) => onFieldChange('description', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { X, Layers } from 'lucide-react';
|
||||
|
||||
interface CreateProjectModalHeaderProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function CreateProjectModalHeader({ onClose }: CreateProjectModalHeaderProps) {
|
||||
return (
|
||||
<div className="p-4 border-b border-kodo-steel bg-kodo-ink flex justify-between items-center">
|
||||
<h3 className="font-bold text-white flex items-center gap-2">
|
||||
<Layers className="w-5 h-5 text-kodo-steel" /> New Project
|
||||
</h3>
|
||||
<button type="button" onClick={onClose} aria-label="Close">
|
||||
<X className="w-5 h-5 text-kodo-content-dim hover:text-white" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
/**
|
||||
* Skeleton for CreateProjectModal — layout primitive, no arbitrary values.
|
||||
*/
|
||||
export function CreateProjectModalSkeleton() {
|
||||
return (
|
||||
<div className="relative w-full max-w-lg bg-kodo-graphite border border-kodo-steel rounded-xl overflow-hidden animate-pulse">
|
||||
<div className="p-4 border-b border-kodo-steel bg-kodo-ink flex justify-between items-center">
|
||||
<div className="h-6 w-32 rounded bg-muted" />
|
||||
<div className="h-5 w-5 rounded bg-muted" />
|
||||
</div>
|
||||
<div className="p-6 space-y-6">
|
||||
<div className="space-y-2">
|
||||
<div className="h-4 w-24 rounded bg-muted" />
|
||||
<div className="h-11 w-full rounded-lg bg-muted" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="h-4 w-40 rounded bg-muted" />
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="h-14 rounded-lg bg-muted" />
|
||||
<div className="h-14 rounded-lg bg-muted" />
|
||||
<div className="h-14 rounded-lg bg-muted" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="h-11 rounded-lg bg-muted" />
|
||||
<div className="h-11 rounded-lg bg-muted" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="h-4 w-24 rounded bg-muted" />
|
||||
<div className="h-24 w-full rounded-lg bg-muted" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 border-t border-kodo-steel bg-kodo-ink flex justify-end gap-4">
|
||||
<div className="h-10 w-20 rounded-lg bg-muted" />
|
||||
<div className="h-10 w-28 rounded-lg bg-muted" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
export { CreateProjectModal } from './CreateProjectModal';
|
||||
export { CreateProjectModalSkeleton } from './CreateProjectModalSkeleton';
|
||||
export { CreateProjectModalHeader } from './CreateProjectModalHeader';
|
||||
export { CreateProjectModalForm } from './CreateProjectModalForm';
|
||||
export { CreateProjectModalFooter } from './CreateProjectModalFooter';
|
||||
export { useCreateProjectModal } from './useCreateProjectModal';
|
||||
export { DAW_OPTIONS } from './types';
|
||||
export type {
|
||||
CreateProjectModalProps,
|
||||
CreateProjectFormState,
|
||||
CreateProjectFormPayload,
|
||||
} from './types';
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
export interface CreateProjectModalProps {
|
||||
onClose: () => void;
|
||||
onCreate: (project: CreateProjectFormPayload) => void;
|
||||
}
|
||||
|
||||
export interface CreateProjectFormState {
|
||||
name: string;
|
||||
daw: string;
|
||||
bpm: string;
|
||||
key: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface CreateProjectFormPayload extends CreateProjectFormState {
|
||||
progress: number;
|
||||
status: string;
|
||||
collaborators: number | unknown[];
|
||||
modified: string;
|
||||
}
|
||||
|
||||
export const DAW_OPTIONS = ['Ableton', 'FL Studio', 'Logic Pro'] as const;
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
import { useState, useCallback } from 'react';
|
||||
import type { CreateProjectFormState, CreateProjectFormPayload } from './types';
|
||||
|
||||
const INITIAL_STATE: CreateProjectFormState = {
|
||||
name: '',
|
||||
daw: 'Ableton',
|
||||
bpm: '128',
|
||||
key: 'C Min',
|
||||
description: '',
|
||||
};
|
||||
|
||||
export function useCreateProjectModal(onCreate: (project: CreateProjectFormPayload) => void) {
|
||||
const [formData, setFormData] = useState<CreateProjectFormState>(INITIAL_STATE);
|
||||
|
||||
const updateField = useCallback(<K extends keyof CreateProjectFormState>(
|
||||
field: K,
|
||||
value: CreateProjectFormState[K],
|
||||
) => {
|
||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||
}, []);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
if (!formData.name) return;
|
||||
const payload: CreateProjectFormPayload = {
|
||||
...formData,
|
||||
progress: 0,
|
||||
status: 'Idea',
|
||||
collaborators: [],
|
||||
modified: 'Just now',
|
||||
};
|
||||
onCreate(payload);
|
||||
}, [formData, onCreate]);
|
||||
|
||||
const canSubmit = !!formData.name.trim();
|
||||
|
||||
return { formData, updateField, handleSubmit, canSubmit };
|
||||
}
|
||||
Loading…
Reference in a new issue