feat: implement core application structure, UI components, internationalization, and database seeding.

This commit is contained in:
2026-03-04 13:50:37 +01:00
parent 85642b4672
commit 5101f39ba0
49 changed files with 2732 additions and 980 deletions

View File

@@ -4,6 +4,7 @@
import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
import { WorkflowData, PlotNode, PlotConnection, PlotNodeType, Entity, EntityType } from '@/lib/types';
import { Plus, Trash2, ArrowRight, BookOpen, MessageCircle, Zap, Palette, Save, Link2 } from 'lucide-react';
import { useLanguage } from '@/providers/LanguageProvider';
interface StoryWorkflowProps {
data: WorkflowData;
@@ -24,8 +25,8 @@ const INITIAL_COLORS = [
'#f3e8ff', // Purple
];
const renderTextWithLinks = (text: string, entities: Entity[], onNavigate: (id: string) => void) => {
if (!text) return <span className="text-slate-400 italic">Description...</span>;
const renderTextWithLinks = (text: string, entities: Entity[], onNavigate: (id: string) => void, t: any) => {
if (!text) return <span className="text-slate-400 italic">{t('sw.desc_ph')}</span>;
const parts: (string | React.ReactNode)[] = [text];
@@ -45,7 +46,7 @@ const renderTextWithLinks = (text: string, entities: Entity[], onNavigate: (id:
key={`${entity.id}-${idx}`}
onClick={(e) => { e.stopPropagation(); onNavigate(entity.id); }}
className="text-indigo-600 hover:text-indigo-800 underline decoration-indigo-300 hover:decoration-indigo-600 cursor-pointer font-medium bg-indigo-50 px-0.5 rounded transition-all"
title={`Voir la fiche de ${entity.name}`}
title={t('sw.see_sheet') + entity.name}
>
{s}
</span>
@@ -92,12 +93,12 @@ const StoryNode = React.memo(({
onToggleColorPicker, onSaveColor, onNavigateToEntity,
onInputFocus, onInputCheckAutocomplete, onKeyDownInInput
}: StoryNodeProps) => {
const { t } = useLanguage();
const [showTypePicker, setShowTypePicker] = useState(false);
const richDescription = useMemo(() => {
return renderTextWithLinks(node.description, entities, onNavigateToEntity);
}, [node.description, entities, onNavigateToEntity]);
return renderTextWithLinks(node.description, entities, onNavigateToEntity, t);
}, [node.description, entities, onNavigateToEntity, t]);
return (
<div
@@ -170,7 +171,7 @@ const StoryNode = React.memo(({
onClick={() => onSaveColor(node.color || '#ffffff')}
className="text-[10px] font-bold text-indigo-600 hover:text-indigo-800 hover:underline flex-1 text-right"
>
+ SAUVER
{t('sw.save_color')}
</button>
</div>
</div>
@@ -181,7 +182,7 @@ const StoryNode = React.memo(({
{isEditing ? (
<textarea
className={`w-full h-full bg-white/70 resize-none outline-none text-xs leading-relaxed p-2 rounded border border-indigo-100 shadow-inner ${node.type === 'dialogue' ? 'font-mono text-slate-700' : 'text-slate-600'}`}
placeholder={node.type === 'dialogue' ? "Héros: Salut !\nGuide: ..." : "Résumé de l'intrigue..."}
placeholder={node.type === 'dialogue' ? t('sw.dialogue_ph') : t('sw.plot_ph')}
value={node.description}
onChange={(e) => onInputCheckAutocomplete(e, node.id, 'description')}
onKeyDown={(e) => onKeyDownInInput(e, node.id)}
@@ -204,21 +205,21 @@ const StoryNode = React.memo(({
<button
onClick={(e) => { e.stopPropagation(); onUpdate(node.id, { type: 'story' }); setShowTypePicker(false); }}
className={`p-1.5 rounded hover:bg-slate-100 ${node.type === 'story' ? 'bg-indigo-50 ring-1 ring-indigo-200' : ''}`}
title="Narration"
title={t('sw.type_story')}
>
<BookOpen size={14} className="text-slate-500" />
</button>
<button
onClick={(e) => { e.stopPropagation(); onUpdate(node.id, { type: 'action' }); setShowTypePicker(false); }}
className={`p-1.5 rounded hover:bg-amber-50 ${node.type === 'action' ? 'bg-amber-50 ring-1 ring-amber-200' : ''}`}
title="Action"
title={t('sw.type_action')}
>
<Zap size={14} className="text-amber-500" />
</button>
<button
onClick={(e) => { e.stopPropagation(); onUpdate(node.id, { type: 'dialogue' }); setShowTypePicker(false); }}
className={`p-1.5 rounded hover:bg-blue-50 ${node.type === 'dialogue' ? 'bg-blue-50 ring-1 ring-blue-200' : ''}`}
title="Dialogue"
title={t('sw.type_dialogue')}
>
<MessageCircle size={14} className="text-blue-500" />
</button>
@@ -267,6 +268,7 @@ interface SuggestionState {
}
const StoryWorkflow: React.FC<StoryWorkflowProps> = ({ data, onUpdate, entities, onNavigateToEntity }) => {
const { t } = useLanguage();
const containerRef = useRef<HTMLDivElement>(null);
const rafRef = useRef<number | null>(null);
@@ -569,7 +571,7 @@ const StoryWorkflow: React.FC<StoryWorkflowProps> = ({ data, onUpdate, entities,
id: `node-${Date.now()}`,
x,
y,
title: 'Nouvel événement',
title: t('sw.new_event'),
description: '',
color: INITIAL_COLORS[0],
type: 'story'
@@ -598,7 +600,7 @@ const StoryWorkflow: React.FC<StoryWorkflowProps> = ({ data, onUpdate, entities,
id: `node-${Date.now()}`,
x: scrollLeft + clientWidth / 2 - CARD_WIDTH / 2,
y: scrollTop + clientHeight / 2 - CARD_HEIGHT / 2,
title: 'Nouveau point d\'intrigue',
title: t('sw.new_plot_point'),
description: '',
color: INITIAL_COLORS[0],
type: 'story'
@@ -613,15 +615,15 @@ const StoryWorkflow: React.FC<StoryWorkflowProps> = ({ data, onUpdate, entities,
<div className="h-12 bg-theme-panel border-b border-theme-border flex items-center justify-between px-4 z-10 shadow-sm shrink-0 transition-colors duration-300">
<div className="flex items-center gap-2">
<button onClick={handleAddNodeCenter} className="flex items-center gap-1.5 px-3 py-1.5 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 text-xs font-bold transition-all shadow-md shadow-indigo-100">
<Plus size={14} /> AJOUTER NŒUD
<Plus size={14} /> {t('sw.add_node')}
</button>
<div className="w-px h-6 bg-theme-border mx-2" />
<div className="text-[10px] uppercase font-bold text-theme-muted tracking-wider">
{selectedNodeIds.size > 0 ? `${selectedNodeIds.size} SÉLECTIONNÉ(S)` : 'Double-cliquez sur le canvas pour créer'}
{selectedNodeIds.size > 0 ? `${selectedNodeIds.size} ${t('sw.selected')}` : t('sw.double_click_create')}
</div>
</div>
<div className="flex items-center gap-2">
<button onClick={handleDeleteSelected} disabled={selectedNodeIds.size === 0} className="p-2 text-red-500 hover:bg-red-500/10 rounded-lg disabled:opacity-30 transition-colors" title="Supprimer">
<button onClick={handleDeleteSelected} disabled={selectedNodeIds.size === 0} className="p-2 text-red-500 hover:bg-red-500/10 rounded-lg disabled:opacity-30 transition-colors" title={t('sw.delete')}>
<Trash2 size={16} />
</button>
</div>
@@ -698,7 +700,7 @@ const StoryWorkflow: React.FC<StoryWorkflowProps> = ({ data, onUpdate, entities,
{activeSuggestion && (
<div className="fixed z-50 bg-white rounded-xl shadow-2xl border border-indigo-100 w-64 max-h-48 overflow-y-auto" style={{ left: '50%', top: '50%', transform: 'translate(-50%, -50%)' }}>
<div className="px-3 py-2 bg-indigo-600 text-white text-[10px] font-black uppercase tracking-widest">
Insérer {activeSuggestion.trigger === '@' ? 'Personnage' : activeSuggestion.trigger === '#' ? 'Lieu' : 'Objet'}
{activeSuggestion.trigger === '@' ? t('sw.insert_char') : activeSuggestion.trigger === '#' ? t('sw.insert_loc') : t('sw.insert_obj')}
</div>
<div className="divide-y divide-slate-50">
{activeSuggestion.filteredEntities.length > 0 ? (
@@ -712,7 +714,7 @@ const StoryWorkflow: React.FC<StoryWorkflowProps> = ({ data, onUpdate, entities,
</button>
))
) : (
<div className="p-4 text-xs text-slate-400 italic text-center">Aucun résultat</div>
<div className="p-4 text-xs text-slate-400 italic text-center">{t('sw.no_result')}</div>
)}
</div>
</div>