ajout CGU CGV, bandeau cookie, font
This commit is contained in:
@@ -100,6 +100,7 @@ model Entity {
|
||||
id String @id @default(cuid())
|
||||
type String
|
||||
name String
|
||||
avatar String? @db.Text // Base64 image or URL
|
||||
description String @default("")
|
||||
details String @default("")
|
||||
storyContext String?
|
||||
|
||||
11
scripts/test-prisma.ts
Normal file
11
scripts/test-prisma.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
console.log(Object.keys(prisma.entity.fields));
|
||||
const e = await prisma.entity.findFirst();
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
main().catch(console.error).finally(() => prisma.$disconnect());
|
||||
@@ -30,6 +30,7 @@ export async function PUT(
|
||||
data: {
|
||||
...(body.name !== undefined && { name: body.name }),
|
||||
...(body.type !== undefined && { type: body.type }),
|
||||
...(body.avatar !== undefined && { avatar: body.avatar }),
|
||||
...(body.description !== undefined && { description: body.description }),
|
||||
...(body.details !== undefined && { details: body.details }),
|
||||
...(body.storyContext !== undefined && { storyContext: body.storyContext }),
|
||||
|
||||
@@ -24,6 +24,7 @@ export async function POST(request: NextRequest) {
|
||||
data: {
|
||||
type: body.type,
|
||||
name: body.name || 'Nouvelle entité',
|
||||
avatar: body.avatar || null,
|
||||
description: body.description || '',
|
||||
details: body.details || '',
|
||||
storyContext: body.storyContext || null,
|
||||
|
||||
@@ -27,10 +27,151 @@ export default function CGUPage() {
|
||||
</nav>
|
||||
|
||||
<main className="max-w-4xl mx-auto py-20 px-4 md:px-8">
|
||||
<h1 className="text-4xl md:text-5xl font-black text-slate-900 mb-8 tracking-tight">{t('legal.cgu_title')}</h1>
|
||||
<div className="bg-white p-6 sm:p-12 rounded-3xl shadow-xl border border-indigo-50 text-slate-600 leading-relaxed space-y-6">
|
||||
<p>{t('legal.cgu_content')}</p>
|
||||
<p><i>(Ceci est un document type en attente de la version finale par un conseiller juridique)</i></p>
|
||||
<h1 className="text-4xl md:text-5xl font-black text-slate-900 mb-8 tracking-tight">Conditions Générales d'Utilisation (CGU)</h1>
|
||||
<div className="bg-white p-6 sm:p-12 rounded-3xl shadow-xl border border-indigo-50 text-slate-600 leading-relaxed space-y-8">
|
||||
|
||||
<div>
|
||||
<p className="font-semibold text-slate-500 mb-6">Version en vigueur au : [Indiquer la date]</p>
|
||||
|
||||
<h2 className="text-xl font-bold text-slate-900 mb-4 uppercase tracking-wider">Préambule</h2>
|
||||
<p className="mb-4">
|
||||
La présente plateforme (ci-après "la Plateforme"), accessible à l'adresse
|
||||
<a href="https://pluu.me" className="text-blue-600 hover:underline mx-1">https://pluu.me</a>
|
||||
(ou <a href="https://www.youtube.com/watch?v=sllLX6wOpg4" target="_blank" rel="noreferrer" className="text-blue-600 hover:underline mx-1">lien original vidéo</a>),
|
||||
est éditée par [Nom de votre Société/Nom du Propriétaire]. La Plateforme propose un service de génération de contenus textuels assisté par l'intelligence artificielle (technologie Google Gemini) destiné à la création d'ebooks.
|
||||
</p>
|
||||
<p>
|
||||
Les présentes Conditions Générales d'Utilisation (CGU) ont pour objet de définir les règles d'accès et d'utilisation du service. Tout accès ou utilisation de la Plateforme suppose l'acceptation sans réserve de l'intégralité des présentes conditions.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-slate-900 mb-4 uppercase tracking-wider">Article 1 : Définitions</h2>
|
||||
<ul className="list-disc pl-5 space-y-2">
|
||||
<li><strong>Utilisateur :</strong> Toute personne physique ou morale accédant à la Plateforme.</li>
|
||||
<li><strong>Service :</strong> Ensemble des outils de génération de texte, de structuration et d'exportation d'ebooks mis à disposition.</li>
|
||||
<li><strong>Contenu Généré :</strong> Textes, plans ou documents produits par l'IA suite aux instructions de l'Utilisateur.</li>
|
||||
<li><strong>Prompt :</strong> Instructions textuelles saisies par l'Utilisateur pour diriger l'IA.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-slate-900 mb-4 uppercase tracking-wider">Article 2 : Accès et Inscription</h2>
|
||||
<p>
|
||||
L'accès à la Plateforme est réservé aux personnes majeures. Pour utiliser les services de génération, l'Utilisateur doit créer un compte. Il est responsable de la confidentialité de ses identifiants.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-slate-900 mb-4 uppercase tracking-wider">Article 3 : Conditions Financières (Abonnements et Crédits)</h2>
|
||||
<h3 className="font-bold text-slate-800 mt-4 mb-2">3.1 Tarifs</h3>
|
||||
<p className="mb-2">
|
||||
L'accès aux fonctionnalités de génération est soumis à une tarification consultable sur la page <Link href="/pricing" className="text-blue-600 hover:underline">Tarifs</Link>. Les prix sont exprimés en Euros TTC.
|
||||
</p>
|
||||
<ul className="list-disc pl-5 space-y-2 mb-4">
|
||||
<li><strong>Abonnements :</strong> Prélèvement périodique automatique.</li>
|
||||
<li><strong>Packs de crédits :</strong> Achat ponctuel pour un nombre défini de générations.</li>
|
||||
</ul>
|
||||
|
||||
<h3 className="font-bold text-slate-800 mt-4 mb-2">3.2 Paiement et Sécurité</h3>
|
||||
<p>
|
||||
Les paiements sont traités par un prestataire de paiement sécurisé. La Plateforme ne stocke aucune coordonnée bancaire.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-slate-900 mb-4 uppercase tracking-wider">Article 4 : Obligations de l'Utilisateur et Sécurité (Clause Stricte)</h2>
|
||||
<p className="mb-4">L'Utilisateur s'engage à utiliser le Service de manière licite.</p>
|
||||
|
||||
<h3 className="font-bold text-slate-800 mt-4 mb-2">4.1 Contenus Interdits</h3>
|
||||
<p className="mb-2">Il est formellement interdit d'utiliser la Plateforme pour générer des contenus :</p>
|
||||
<ul className="list-disc pl-5 space-y-2 mb-4">
|
||||
<li><strong>Pédopornographiques (CSAM) :</strong> Toute tentative de génération, requête ou diffusion de contenu lié à l'exploitation sexuelle des mineurs fera l'objet d'un bannissement immédiat sans préavis ni remboursement. Conformément à la loi, la Plateforme procédera à un signalement systématique auprès des autorités compétentes (Plateforme PHAROS).</li>
|
||||
<li><strong>Haineux et Discriminatoires :</strong> Incitation à la violence, au racisme, à l'antisémitisme, à l'homophobie ou toute forme de discrimination.</li>
|
||||
<li><strong>Illégaux :</strong> Apologie de crimes, terrorisme, ou violation de droits de propriété intellectuelle tiers.</li>
|
||||
</ul>
|
||||
|
||||
<h3 className="font-bold text-slate-800 mt-4 mb-2">4.2 Surveillance et Modération</h3>
|
||||
<p>
|
||||
La Plateforme utilise des algorithmes de filtrage en temps réel. En cas de violation répétée ou grave de cet article, la Plateforme se réserve le droit de suspendre le compte de l'Utilisateur de plein droit.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-slate-900 mb-4 uppercase tracking-wider">Article 5 : Propriété Intellectuelle</h2>
|
||||
<h3 className="font-bold text-slate-800 mt-4 mb-2">5.1 Droits de l'Utilisateur</h3>
|
||||
<p className="mb-2">
|
||||
La Plateforme concède à l'Utilisateur, sous réserve du paiement des frais éventuels, la pleine propriété des droits d'exploitation sur les ebooks générés.
|
||||
</p>
|
||||
<div className="bg-slate-50 p-4 rounded-xl border border-slate-200 tex-sm my-4">
|
||||
<strong>Note sur l'IA :</strong> L'Utilisateur est informé que la protection par le droit d'auteur des œuvres générées par IA peut varier selon les législations nationales et nécessite souvent une intervention créative humaine significative de la part de l'Utilisateur.
|
||||
</div>
|
||||
|
||||
<h3 className="font-bold text-slate-800 mt-4 mb-2">5.2 Droits de la Plateforme</h3>
|
||||
<p>
|
||||
L'interface, les algorithmes de connexion à l'API Gemini et l'identité visuelle du site restent la propriété exclusive de l'Éditeur.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-slate-900 mb-4 uppercase tracking-wider">Article 6 : Politique de "Zéro Stockage" et Confidentialité</h2>
|
||||
<h3 className="font-bold text-slate-800 mt-4 mb-2">6.1 Traitement Éphémère</h3>
|
||||
<p className="mb-2">
|
||||
La Plateforme applique une politique stricte de confidentialité. Aucune donnée de création (prompts, textes générés, ebooks en cours) n'est sauvegardée sur nos serveurs de manière permanente.
|
||||
</p>
|
||||
<p className="mb-2">
|
||||
Les données ne sont conservées que le temps de la session de travail pour permettre l'affichage et l'exportation.
|
||||
</p>
|
||||
<p className="mb-4">
|
||||
Quoi qu'il arrive, si l'Utilisateur décide de supprimer ses informations, son projet ou ferme sa session, toutes les données associées sont immédiatement et irréversiblement supprimées de nos bases de données.
|
||||
</p>
|
||||
|
||||
<h3 className="font-bold text-slate-800 mt-4 mb-2">6.2 Responsabilité de Sauvegarde</h3>
|
||||
<p>
|
||||
En raison de cette politique de non-conservation, il appartient exclusivement à l'Utilisateur de télécharger et de sauvegarder ses travaux (format PDF, EPUB, etc.) avant la clôture de sa session. La Plateforme ne pourra être tenue responsable d'une perte de données consécutive à une déconnexion ou une suppression volontaire.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-slate-900 mb-4 uppercase tracking-wider">Article 7 : Limitation de Responsabilité</h2>
|
||||
<h3 className="font-bold text-slate-800 mt-4 mb-2">7.1 Qualité du Contenu</h3>
|
||||
<p className="mb-4">
|
||||
Le Service utilise la technologie Google Gemini. L'Utilisateur accepte que l'IA puisse produire des informations inexactes, incomplètes ou biaisées ("hallucinations"). La Plateforme ne saurait être tenue responsable du contenu des ebooks ou de l'utilisation qui en est faite.
|
||||
</p>
|
||||
|
||||
<h3 className="font-bold text-slate-800 mt-4 mb-2">7.2 Disponibilité Technique</h3>
|
||||
<p>
|
||||
La Plateforme ne peut garantir une disponibilité ininterrompue du service, celle-ci dépendant de prestataires tiers (hébergement et API Google).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-slate-900 mb-4 uppercase tracking-wider">Article 8 : Protection des Données Personnelles (RGPD)</h2>
|
||||
<p>
|
||||
Les seules données conservées sont celles strictement nécessaires à la gestion du compte (email, facturation). L'Utilisateur dispose d'un droit d'accès, de rectification et de suppression totale de ses données personnelles sur simple demande à : <a href="mailto:rgpd@pluu.me" className="text-blue-600 hover:underline">rgpd@pluu.me</a>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-slate-900 mb-4 uppercase tracking-wider">Article 9 : Modification et Résiliation</h2>
|
||||
<h3 className="font-bold text-slate-800 mt-4 mb-2">9.1 Modification des CGU</h3>
|
||||
<p className="mb-4">
|
||||
La Plateforme se réserve le droit de modifier les présentes CGU à tout moment. L'Utilisateur sera informé de toute modification substantielle.
|
||||
</p>
|
||||
|
||||
<h3 className="font-bold text-slate-800 mt-4 mb-2">9.2 Résiliation</h3>
|
||||
<p>
|
||||
L'Utilisateur peut supprimer son compte à tout moment. Cette action entraîne la suppression immédiate de toutes ses données, conformément à l'Article 6.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-slate-900 mb-4 uppercase tracking-wider">Article 10 : Loi Applicable et Juridiction</h2>
|
||||
<p>
|
||||
Les présentes CGU sont soumises au droit français. Tout litige relatif à leur interprétation ou exécution relève de la compétence exclusive des tribunaux de [Ville].
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -27,10 +27,115 @@ export default function CGVPage() {
|
||||
</nav>
|
||||
|
||||
<main className="max-w-4xl mx-auto py-20 px-4 md:px-8">
|
||||
<h1 className="text-4xl md:text-5xl font-black text-slate-900 mb-8 tracking-tight">{t('legal.cgv_title')}</h1>
|
||||
<div className="bg-white p-6 sm:p-12 rounded-3xl shadow-xl border border-indigo-50 text-slate-600 leading-relaxed space-y-6">
|
||||
<p>{t('legal.cgv_content')}</p>
|
||||
<p><i>(Ceci est un document type en attente de la version finale par un conseiller juridique)</i></p>
|
||||
<h1 className="text-4xl md:text-5xl font-black text-slate-900 mb-8 tracking-tight">Conditions Générales de Vente (CGV)</h1>
|
||||
<div className="bg-white p-6 sm:p-12 rounded-3xl shadow-xl border border-indigo-50 text-slate-600 leading-relaxed space-y-8">
|
||||
|
||||
<div>
|
||||
<p className="font-semibold text-slate-500 mb-6">Version en vigueur au : [Indiquer la date]</p>
|
||||
|
||||
<h2 className="text-xl font-bold text-slate-900 mb-4 uppercase tracking-wider">Article 1 : Objet et Champ d'Application</h2>
|
||||
<p className="mb-4">
|
||||
Les présentes Conditions Générales de Vente (CGV) s'appliquent, sans restriction ni réserve, à tout achat des services de génération d'ebooks (ci-après "les Services") proposés par [Nom de votre Société/Nom du Propriétaire] (ci-après "le Vendeur") sur la plateforme
|
||||
<a href="https://pluu.me" className="text-blue-600 hover:underline mx-1">https://pluu.me</a>.
|
||||
</p>
|
||||
<p>
|
||||
Ces CGV encadrent les modalités de commande, de paiement, de livraison des crédits/abonnements et de gestion des éventuels litiges entre le Vendeur et l'Utilisateur (ci-après "le Client").
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-slate-900 mb-4 uppercase tracking-wider">Article 2 : Caractéristiques des Services</h2>
|
||||
<p className="mb-2">Le Vendeur propose des solutions de création d'ebooks assistées par l'intelligence artificielle Google Gemini. Les offres sont divisées en deux catégories :</p>
|
||||
<ul className="list-disc pl-5 space-y-2 mb-4">
|
||||
<li><strong>Packs de Crédits :</strong> Achat ponctuel d'un volume défini de générations de contenu.</li>
|
||||
<li><strong>Abonnements :</strong> Accès illimité ou plafonné aux services pour une durée déterminée (mensuelle ou annuelle) avec renouvellement automatique.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-slate-900 mb-4 uppercase tracking-wider">Article 3 : Commande et Validation</h2>
|
||||
<p>
|
||||
Le Client sélectionne l'offre de son choix sur la Plateforme. La commande est considérée comme définitive dès la validation du paiement par le prestataire tiers. Un e-mail de confirmation est envoyé au Client à l'adresse renseignée lors de la création du compte.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-slate-900 mb-4 uppercase tracking-wider">Article 4 : Conditions Financières</h2>
|
||||
|
||||
<h3 className="font-bold text-slate-800 mt-4 mb-2">4.1 Tarifs</h3>
|
||||
<p className="mb-4">
|
||||
Les prix sont indiqués en Euros et s'entendent toutes taxes comprises (TTC). Le Vendeur se réserve le droit de modifier ses tarifs à tout moment, mais les services seront facturés sur la base des tarifs en vigueur au moment de l'enregistrement de la commande.
|
||||
</p>
|
||||
|
||||
<h3 className="font-bold text-slate-800 mt-4 mb-2">4.2 Modalités de paiement (Stripe)</h3>
|
||||
<p className="mb-2">Le paiement s'effectue exclusivement par carte bancaire via le système de paiement sécurisé Stripe.</p>
|
||||
<ul className="list-disc pl-5 space-y-2 mb-4">
|
||||
<li>Le Vendeur n'a jamais accès aux coordonnées bancaires du Client.</li>
|
||||
<li>Stripe garantit la confidentialité et la sécurité des transactions grâce au protocole SSL et à la conformité PCI-DSS.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-slate-900 mb-4 uppercase tracking-wider">Article 5 : Gestion des Abonnements</h2>
|
||||
<h3 className="font-bold text-slate-800 mt-4 mb-2">5.1 Renouvellement automatique</h3>
|
||||
<p className="mb-4">
|
||||
Tout abonnement souscrit est à tacite reconduction. Le Client autorise Stripe à prélever le montant de l'abonnement à chaque échéance (mois ou année) sur la carte bancaire utilisée lors de l'achat initial.
|
||||
</p>
|
||||
|
||||
<h3 className="font-bold text-slate-800 mt-4 mb-2">5.2 Résiliation</h3>
|
||||
<p className="mb-2">Le Client peut résilier son abonnement à tout moment et sans frais depuis son espace personnel.</p>
|
||||
<ul className="list-disc pl-5 space-y-2 mb-4">
|
||||
<li>La résiliation interrompt le renouvellement automatique mais laisse l'accès aux services actif jusqu'à la fin de la période en cours.</li>
|
||||
<li>Aucun remboursement pro rata n'est effectué pour la période restant à courir entre la date de résiliation et la fin de l'échéance.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-slate-900 mb-4 uppercase tracking-wider">Article 6 : Droit de Rétractation (Produits Numériques)</h2>
|
||||
<p className="mb-4">
|
||||
Conformément à l'article L221-28 du Code de la consommation (ou législation équivalente selon le pays), le droit de rétractation de 14 jours ne s'applique pas aux contrats de fourniture de contenu numérique non fourni sur un support matériel dont l'exécution a commencé après accord préalable exprès du consommateur et renoncement exprès à son droit de rétractation.
|
||||
</p>
|
||||
<div className="bg-amber-50 text-amber-900 p-4 rounded-xl border border-amber-200 tex-sm my-4 font-medium">
|
||||
En procédant au paiement et en utilisant son premier crédit de génération, le Client accepte expressément l'exécution immédiate du service et renonce à son droit de rétractation. Aucun remboursement ne sera accordé une fois le service consommé.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-slate-900 mb-4 uppercase tracking-wider">Article 7 : Livraison et Accès</h2>
|
||||
<p>
|
||||
Les Services sont réputés "livrés" dès que les crédits sont ajoutés au compte du Client ou que l'accès premium est débloqué. En cas de problème technique empêchant l'accès immédiat, le Client doit contacter le support à l'adresse suivante : <a href="mailto:contact@pluume.fr" className="text-blue-600 hover:underline">[Email de contact]</a>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-slate-900 mb-4 uppercase tracking-wider">Article 8 : Garanties et Responsabilité Commerciale</h2>
|
||||
<h3 className="font-bold text-slate-800 mt-4 mb-2">8.1 Qualité de l'IA</h3>
|
||||
<p className="mb-4">
|
||||
Le Vendeur vend un accès à un outil de génération. Il ne garantit pas la qualité littéraire, l'exactitude des faits ou l'originalité absolue du texte produit par l'IA. Par conséquent, aucune demande de remboursement ne pourra être motivée par une "déception" quant au style ou à la pertinence des réponses de l'IA.
|
||||
</p>
|
||||
|
||||
<h3 className="font-bold text-slate-800 mt-4 mb-2">8.2 Continuité de service</h3>
|
||||
<p>
|
||||
Le Vendeur s'engage à faire ses meilleurs efforts pour maintenir l'accès au service, mais ne pourra être tenu responsable des pannes dues aux prestataires tiers (Stripe, API Google Gemini).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-slate-900 mb-4 uppercase tracking-wider">Article 9 : Politique de Suppression des Données</h2>
|
||||
<p className="mb-2">Comme stipulé dans les CGU, le Vendeur applique une politique de zéro stockage.</p>
|
||||
<ul className="list-disc pl-5 space-y-2 mb-4">
|
||||
<li>Si le Client supprime ses informations ou son compte, toutes les données de facturation sont archivées uniquement pour la durée légale fiscale, mais les contenus créés sont supprimés irréversiblement.</li>
|
||||
<li>Le Client ne pourra prétendre à un dédommagement en cas de perte de données suite à une suppression volontaire.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-slate-900 mb-4 uppercase tracking-wider">Article 10 : Litiges et Loi Applicable</h2>
|
||||
<p>
|
||||
Les présentes CGV sont soumises au droit [Pays]. En cas de litige, une solution amiable sera recherchée avant toute action judiciaire. À défaut d'accord, compétence exclusive est attribuée aux tribunaux de [Ville].
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
@theme {
|
||||
--font-sans: 'Inter', sans-serif;
|
||||
--font-serif: 'Merriweather', serif;
|
||||
--font-lora: 'Lora', serif;
|
||||
--font-garamond: 'EB Garamond', serif;
|
||||
--font-playfair: 'Playfair Display', serif;
|
||||
--color-paper: #fcfbf7;
|
||||
|
||||
/* Global Theme Colors */
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Inter, Merriweather } from "next/font/google";
|
||||
import { Inter, Merriweather, Lora, EB_Garamond, Playfair_Display } from "next/font/google";
|
||||
import Script from "next/script";
|
||||
import { AuthProvider } from "@/providers/AuthProvider";
|
||||
import { LanguageProvider } from "@/providers/LanguageProvider";
|
||||
import { CookieBanner } from "@/components/CookieBanner";
|
||||
import { Analytics } from "@/components/Analytics";
|
||||
import "./globals.css";
|
||||
|
||||
const inter = Inter({
|
||||
@@ -16,6 +18,21 @@ const merriweather = Merriweather({
|
||||
variable: "--font-serif",
|
||||
});
|
||||
|
||||
const lora = Lora({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-lora",
|
||||
});
|
||||
|
||||
const garamond = EB_Garamond({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-garamond",
|
||||
});
|
||||
|
||||
const playfair = Playfair_Display({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-playfair",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Pluume - Éditeur Intelligent",
|
||||
description: "Votre assistant éditorial intelligent propulsé par l'IA pour écrire votre prochain roman.",
|
||||
@@ -29,17 +46,13 @@ export default function RootLayout({
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<Script
|
||||
defer
|
||||
src="https://stats.kaelstudio.tech/script.js"
|
||||
data-website-id="bce265f0-c9d4-4542-813f-f3bd9bf151bc"
|
||||
strategy="afterInteractive"
|
||||
/>
|
||||
</head>
|
||||
<body className={`${inter.variable} ${merriweather.variable} font-sans h-screen overflow-x-hidden overflow-y-auto antialiased bg-theme-bg text-theme-text transition-colors duration-300`}>
|
||||
<body className={`${inter.variable} ${merriweather.variable} ${lora.variable} ${garamond.variable} ${playfair.variable} font-sans h-screen overflow-x-hidden overflow-y-auto antialiased bg-theme-bg text-theme-text transition-colors duration-300`}>
|
||||
<AuthProvider>
|
||||
<LanguageProvider>
|
||||
<Analytics />
|
||||
{children}
|
||||
<CookieBanner />
|
||||
</LanguageProvider>
|
||||
</AuthProvider>
|
||||
</body>
|
||||
|
||||
40
src/app/sitemap.xml/route.ts
Normal file
40
src/app/sitemap.xml/route.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
export async function GET() {
|
||||
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://pluume.fr';
|
||||
|
||||
// Dates can be dynamically fetched in a real scenario, here we use current date for static
|
||||
const currentDate = new Date().toISOString();
|
||||
|
||||
const staticRoutes = [
|
||||
'',
|
||||
'/features',
|
||||
'/pricing',
|
||||
'/login',
|
||||
'/signup',
|
||||
'/cgu',
|
||||
'/cgv',
|
||||
'/sitemap',
|
||||
];
|
||||
|
||||
const sitemapEntries = staticRoutes.map((route) => {
|
||||
const priority = route === '' ? '1.0' : '0.8';
|
||||
const changeFrequency = route === '' ? 'weekly' : 'monthly';
|
||||
return `
|
||||
<url>
|
||||
<loc>${baseUrl}${route}</loc>
|
||||
<lastmod>${currentDate}</lastmod>
|
||||
<changefreq>${changeFrequency}</changefreq>
|
||||
<priority>${priority}</priority>
|
||||
</url>`;
|
||||
}).join('');
|
||||
|
||||
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
${sitemapEntries}
|
||||
</urlset>`;
|
||||
|
||||
return new Response(xml, {
|
||||
headers: {
|
||||
'Content-Type': 'application/xml',
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,17 +1,110 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { useLanguage } from '@/providers/LanguageProvider';
|
||||
import { ArrowLeft, Book, Link as LinkIcon } from 'lucide-react';
|
||||
import { ArrowLeft, Book, Link as LinkIcon, ChevronDown, Shield, Home, Component, LogIn, Mail } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { LanguageSwitcher } from '@/components/LanguageSwitcher';
|
||||
|
||||
interface SitemapLink {
|
||||
title: string;
|
||||
href: string;
|
||||
icon: React.ElementType;
|
||||
}
|
||||
|
||||
interface SitemapCategory {
|
||||
id: string;
|
||||
title: string;
|
||||
color: string;
|
||||
icon: React.ElementType;
|
||||
links: SitemapLink[];
|
||||
}
|
||||
|
||||
const SitemapCard = ({ category, defaultOpen = false }: { category: SitemapCategory, defaultOpen?: boolean }) => {
|
||||
const [isOpen, setIsOpen] = useState(defaultOpen);
|
||||
const Icon = category.icon;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col rounded-xl shadow-sm border border-slate-200 bg-white transition-all hover:shadow-md overflow-hidden">
|
||||
{/* Colored Top Bar */}
|
||||
<div className="h-2 w-full" style={{ backgroundColor: category.color }} />
|
||||
|
||||
{/* Header (Toggle) */}
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="flex items-center justify-between p-4 w-full text-left bg-white hover:bg-slate-50 transition-colors focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-lg" style={{ backgroundColor: `${category.color}15`, color: category.color }}>
|
||||
<Icon size={20} />
|
||||
</div>
|
||||
<span className="font-bold text-slate-800 text-lg">{category.title}</span>
|
||||
</div>
|
||||
<div className={`p-1 rounded-full text-slate-400 hover:text-slate-600 transition-transform duration-300 ${isOpen ? 'rotate-180' : ''}`}>
|
||||
<ChevronDown size={20} />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Foldable Content */}
|
||||
<div
|
||||
className={`grid transition-all duration-300 ease-in-out ${isOpen ? 'grid-rows-[1fr] opacity-100' : 'grid-rows-[0fr] opacity-0'}`}
|
||||
>
|
||||
<div className="overflow-hidden">
|
||||
<div className="p-4 pt-0 border-t border-slate-100 bg-slate-50/50">
|
||||
<ul className="space-y-2 mt-3">
|
||||
{category.links.map((link, idx) => {
|
||||
const LinkIconComponent = link.icon || LinkIcon;
|
||||
return (
|
||||
<li key={idx}>
|
||||
<Link
|
||||
href={link.href}
|
||||
className="flex items-center gap-3 px-3 py-2 rounded-lg text-slate-600 hover:text-indigo-600 hover:bg-indigo-50/50 transition-all font-medium group"
|
||||
>
|
||||
<LinkIconComponent size={16} className="text-slate-400 group-hover:text-indigo-500 transition-colors" />
|
||||
{link.title}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default function SitemapPage() {
|
||||
const { t } = useLanguage();
|
||||
|
||||
const categories: SitemapCategory[] = [
|
||||
{
|
||||
id: 'general',
|
||||
title: 'Navigation Principale',
|
||||
color: '#3b82f6', // blue-500
|
||||
icon: Home,
|
||||
links: [
|
||||
{ title: 'Accueil', href: '/', icon: Home },
|
||||
{ title: 'Fonctionnalités', href: '/#features', icon: Component },
|
||||
{ title: 'Authentification', href: '/auth', icon: LogIn },
|
||||
{ title: 'Contact', href: 'mailto:contact@pluu.me', icon: Mail },
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'legal',
|
||||
title: 'Informations Légales',
|
||||
color: '#f59e0b', // amber-500
|
||||
icon: Shield,
|
||||
links: [
|
||||
{ title: t('legal.cgu_title') || 'Conditions Générales d\'Utilisation', href: '/cgu', icon: Book },
|
||||
{ title: t('legal.cgv_title') || 'Conditions Générales de Vente', href: '/cgv', icon: Book },
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#eef2ff] font-sans selection:bg-blue-200">
|
||||
<nav className="bg-white/80 backdrop-blur-md z-50 border-b border-indigo-100 px-8 h-16 flex items-center justify-between sticky top-0">
|
||||
<nav className="bg-white/80 backdrop-blur-md z-50 border-b border-indigo-100 px-4 md:px-8 h-16 flex items-center justify-between sticky top-0">
|
||||
<Link href="/" className="flex items-center gap-2 hover:opacity-80 transition-opacity">
|
||||
<div className="bg-blue-600 p-1.5 rounded-lg">
|
||||
<Book className="text-white" size={24} />
|
||||
@@ -26,36 +119,16 @@ export default function SitemapPage() {
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main className="max-w-4xl mx-auto py-20 px-8">
|
||||
<h1 className="text-4xl md:text-5xl font-black text-slate-900 mb-8 tracking-tight">{t('legal.sitemap_title')}</h1>
|
||||
<div className="bg-white p-8 sm:p-12 rounded-3xl shadow-xl border border-indigo-50">
|
||||
<ul className="space-y-4">
|
||||
<li>
|
||||
<Link href="/" className="flex items-center gap-3 text-lg font-bold text-slate-700 hover:text-blue-600 transition-colors">
|
||||
<LinkIcon size={18} className="text-slate-400" /> Accueil
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/auth" className="flex items-center gap-3 text-lg font-bold text-slate-700 hover:text-blue-600 transition-colors">
|
||||
<LinkIcon size={18} className="text-slate-400" /> Authentification
|
||||
</Link>
|
||||
</li>
|
||||
<li className="pt-4 mt-4 border-t border-slate-100">
|
||||
<span className="text-xs font-black uppercase text-slate-400 tracking-widest block mb-4">Légal</span>
|
||||
<ul className="space-y-4 pl-4">
|
||||
<li>
|
||||
<Link href="/cgu" className="flex items-center gap-3 text-base text-slate-600 hover:text-blue-600 transition-colors">
|
||||
<LinkIcon size={16} className="text-slate-400" /> {t('legal.cgu_title')}
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/cgv" className="flex items-center gap-3 text-base text-slate-600 hover:text-blue-600 transition-colors">
|
||||
<LinkIcon size={16} className="text-slate-400" /> {t('legal.cgv_title')}
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<main className="max-w-4xl mx-auto py-12 md:py-20 px-4 md:px-8">
|
||||
<div className="text-center mb-12">
|
||||
<h1 className="text-4xl md:text-5xl font-black text-slate-900 mb-4 tracking-tight">{t('legal.sitemap_title') || 'Plan du site'}</h1>
|
||||
<p className="text-slate-500 text-lg">Retrouvez facilement toutes les pages de l'application Pluume.</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{categories.map((cat, idx) => (
|
||||
<SitemapCard key={cat.id} category={cat} defaultOpen={true} />
|
||||
))}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
36
src/components/Analytics.tsx
Normal file
36
src/components/Analytics.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import Script from 'next/script';
|
||||
|
||||
export const Analytics = () => {
|
||||
const [hasConsent, setHasConsent] = useState(false);
|
||||
|
||||
const checkConsent = () => {
|
||||
const consent = localStorage.getItem('cookie-consent');
|
||||
setHasConsent(consent === 'accepted');
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Check initial state
|
||||
checkConsent();
|
||||
|
||||
// Listen for updates from CookieBanner
|
||||
window.addEventListener('cookie-consent-updated', checkConsent);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('cookie-consent-updated', checkConsent);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!hasConsent) return null;
|
||||
|
||||
return (
|
||||
<Script
|
||||
defer
|
||||
src="https://stats.kaelstudio.tech/script.js"
|
||||
data-website-id="bce265f0-c9d4-4542-813f-f3bd9bf151bc"
|
||||
strategy="afterInteractive"
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -16,6 +16,7 @@ const AuthPage: React.FC<AuthPageProps> = ({ onBack, onSuccess, initialMode = 's
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [formData, setFormData] = useState({ name: '', email: '', password: '' });
|
||||
const [error, setError] = useState('');
|
||||
const [cguAccepted, setCguAccepted] = useState(false);
|
||||
|
||||
// On récupère les fonctions de connexion directement du hook
|
||||
const { user, login, signup } = useAuthContext();
|
||||
@@ -156,6 +157,38 @@ const AuthPage: React.FC<AuthPageProps> = ({ onBack, onSuccess, initialMode = 's
|
||||
</div>
|
||||
)}
|
||||
|
||||
{mode === 'signup' && (
|
||||
<div className="pt-2 pb-1">
|
||||
<label className="flex items-start gap-3 cursor-pointer group">
|
||||
<div className="relative flex items-center mt-1 text-slate-900 font-bold mb-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
required
|
||||
checked={cguAccepted}
|
||||
onChange={(e) => setCguAccepted(e.target.checked)}
|
||||
className="peer shrink-0 appearance-none w-5 h-5 border-2 border-slate-300 rounded-md bg-white checked:bg-blue-600 checked:border-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-all cursor-pointer"
|
||||
/>
|
||||
<svg
|
||||
className="absolute w-5 h-5 pointer-events-none opacity-0 peer-checked:opacity-100 peer-checked:text-white transition-opacity text-white stroke-white fill-none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth="3"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-sm text-slate-600 font-medium leading-relaxed group-hover:text-slate-800 transition-colors">
|
||||
{t('auth.accept_cgu')}
|
||||
<a href="/cgu" target="_blank" rel="noopener noreferrer" className="text-blue-600 font-bold hover:underline hover:text-blue-700">
|
||||
{t('auth.cgu_link')}
|
||||
</a>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
data-umami-event={mode === 'signin' ? "Login" : mode === 'signup' ? "Signup" : "Send Reset"}
|
||||
|
||||
72
src/components/CookieBanner.tsx
Normal file
72
src/components/CookieBanner.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { X, ShieldCheck } from 'lucide-react';
|
||||
|
||||
export const CookieBanner = () => {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const consent = localStorage.getItem('cookie-consent');
|
||||
if (!consent) {
|
||||
setIsVisible(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleAccept = () => {
|
||||
localStorage.setItem('cookie-consent', 'accepted');
|
||||
setIsVisible(false);
|
||||
// Dispatch an event so Analytics component can react immediately without refresh
|
||||
window.dispatchEvent(new Event('cookie-consent-updated'));
|
||||
};
|
||||
|
||||
const handleDecline = () => {
|
||||
localStorage.setItem('cookie-consent', 'declined');
|
||||
setIsVisible(false);
|
||||
window.dispatchEvent(new Event('cookie-consent-updated'));
|
||||
};
|
||||
|
||||
if (!isVisible) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-0 left-0 right-0 z-[100] p-4 md:p-6 bg-slate-900/95 backdrop-blur shadow-2xl border-t border-slate-700/50 transform transition-transform duration-500 ease-out translate-y-0">
|
||||
<div className="max-w-7xl mx-auto flex flex-col md:flex-row items-start md:items-center justify-between gap-4">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="p-3 bg-blue-500/20 text-blue-400 rounded-full shrink-0">
|
||||
<ShieldCheck size={24} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-white font-bold text-lg mb-1">
|
||||
Nous respectons votre vie privée
|
||||
</h3>
|
||||
<p className="text-slate-300 text-sm leading-relaxed max-w-3xl">
|
||||
Nous utilisons des cookies (via Umami Analytics) exclusivement pour analyser le trafic de manière anonymisée et améliorer votre expérience sur l'application. Aucun parcours n'est lié à votre identité.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row w-full md:w-auto items-center gap-3 shrink-0">
|
||||
<button
|
||||
onClick={handleDecline}
|
||||
className="w-full sm:w-auto px-6 py-2.5 rounded-xl border border-slate-600 text-slate-300 font-semibold hover:bg-slate-800 transition-colors text-sm"
|
||||
>
|
||||
Continuer sans accepter
|
||||
</button>
|
||||
<button
|
||||
onClick={handleAccept}
|
||||
className="w-full sm:w-auto px-6 py-2.5 rounded-xl bg-blue-600 text-white font-bold shadow-lg shadow-blue-600/20 hover:bg-blue-500 hover:-translate-y-0.5 transition-all text-sm"
|
||||
>
|
||||
Accepter
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setIsVisible(false)}
|
||||
className="absolute top-4 right-4 md:hidden text-slate-400 p-2"
|
||||
aria-label="Fermer"
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -40,6 +40,10 @@ const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorProps>(({
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
|
||||
// Appearance State
|
||||
const [editorFont, setEditorFont] = useState('font-serif');
|
||||
const [showFontMenu, setShowFontMenu] = useState(false);
|
||||
|
||||
// Auto-Save State
|
||||
const [saveStatus, setSaveStatus] = useState<'saved_local' | 'saved_db' | 'saving' | 'unsaved'>('saved_db');
|
||||
const saveTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
@@ -198,6 +202,20 @@ const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorProps>(({
|
||||
}
|
||||
}, [initialContent, editorId]);
|
||||
|
||||
// Load saved font preference
|
||||
useEffect(() => {
|
||||
const savedFont = localStorage.getItem('rte_font_preference');
|
||||
if (savedFont) {
|
||||
setEditorFont(savedFont);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleFontChange = (fontClass: string) => {
|
||||
setEditorFont(fontClass);
|
||||
localStorage.setItem('rte_font_preference', fontClass);
|
||||
setShowFontMenu(false);
|
||||
};
|
||||
|
||||
// Flush pending save on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
@@ -405,6 +423,36 @@ const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorProps>(({
|
||||
<ToolbarButton icon={AlignRight} cmd="justifyRight" label="Aligner à droite" />
|
||||
<div className="w-px h-6 bg-slate-300 mx-1" />
|
||||
<ToolbarButton icon={List} cmd="insertUnorderedList" label="Liste" />
|
||||
<div className="w-px h-6 bg-slate-300 mx-1" />
|
||||
|
||||
{/* Font Selector */}
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setShowFontMenu(!showFontMenu)}
|
||||
className="flex items-center gap-2 px-2 py-1.5 text-xs font-semibold text-slate-600 hover:bg-slate-200 rounded transition-colors"
|
||||
title="Choisir la police d'écriture"
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="opacity-70">A</span>
|
||||
<span className="font-bold">Aa</span>
|
||||
</div>
|
||||
<ChevronDown size={14} className="opacity-50" />
|
||||
</button>
|
||||
|
||||
{showFontMenu && (
|
||||
<>
|
||||
<div className="fixed inset-0 z-40 bg-transparent" onClick={() => setShowFontMenu(false)} />
|
||||
<div className="absolute top-full mt-1 left-0 w-48 bg-white shadow-xl border border-slate-200 rounded-lg p-1.5 z-50 animate-in fade-in zoom-in-95 duration-100 flex flex-col gap-1">
|
||||
<div className="px-2 py-1 text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-1">Polices</div>
|
||||
<button onClick={() => handleFontChange('font-sans')} className={`text-left px-3 py-2 text-sm rounded transition-colors font-sans ${editorFont === 'font-sans' ? 'bg-indigo-50 text-indigo-700' : 'hover:bg-slate-50 text-slate-700'}`}>Classique Sans (Inter)</button>
|
||||
<button onClick={() => handleFontChange('font-serif')} className={`text-left px-3 py-2 text-sm rounded transition-colors font-serif ${editorFont === 'font-serif' ? 'bg-indigo-50 text-indigo-700' : 'hover:bg-slate-50 text-slate-700'}`}>Classique Serif (Merriweather)</button>
|
||||
<button onClick={() => handleFontChange('font-lora')} className={`text-left px-3 py-2 text-sm rounded transition-colors font-lora ${editorFont === 'font-lora' ? 'bg-indigo-50 text-indigo-700' : 'hover:bg-slate-50 text-slate-700'}`} style={{ fontFamily: 'var(--font-lora)' }}>Littéraire (Lora)</button>
|
||||
<button onClick={() => handleFontChange('font-garamond')} className={`text-left px-3 py-2 text-sm rounded transition-colors font-garamond ${editorFont === 'font-garamond' ? 'bg-indigo-50 text-indigo-700' : 'hover:bg-slate-50 text-slate-700'}`} style={{ fontFamily: 'var(--font-garamond)' }}>Ancienne (EB Garamond)</button>
|
||||
<button onClick={() => handleFontChange('font-playfair')} className={`text-left px-3 py-2 text-sm rounded transition-colors font-playfair ${editorFont === 'font-playfair' ? 'bg-indigo-50 text-indigo-700' : 'hover:bg-slate-50 text-slate-700'}`} style={{ fontFamily: 'var(--font-playfair)' }}>Élégante (Playfair)</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1" />
|
||||
|
||||
@@ -439,7 +487,7 @@ const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorProps>(({
|
||||
suppressContentEditableWarning
|
||||
spellCheck={true}
|
||||
lang="fr-FR"
|
||||
className="bg-theme-editor-bg shadow-sm w-[800px] min-h-[1000px] p-12 outline-none font-serif text-lg leading-relaxed text-theme-editor-text editor-content transition-colors duration-300"
|
||||
className={`bg-theme-editor-bg shadow-sm w-[800px] min-h-[1000px] p-12 outline-none text-lg leading-relaxed text-theme-editor-text editor-content transition-all duration-300 ${editorFont}`}
|
||||
onInput={handleInput}
|
||||
onBlur={() => { setIsFocused(false); saveSelection(); }}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import React, { useState, useMemo, useEffect } from 'react';
|
||||
import { useLanguage } from '@/providers/LanguageProvider';
|
||||
import { Entity, EntityType, CharacterAttributes, EntityTemplate, CustomFieldDefinition, CustomFieldType } from '@/lib/types';
|
||||
import { Plus, Trash2, Save, X, Sparkles, User, Activity, Brain, Ruler, Settings, Layout, List, ToggleLeft } from 'lucide-react';
|
||||
import { Plus, Trash2, Save, X, Sparkles, User, Activity, Brain, Ruler, Settings, Layout, List, ToggleLeft, Camera } from 'lucide-react';
|
||||
import { ENTITY_ICONS, ENTITY_COLORS, HAIR_COLORS, EYE_COLORS, ARCHETYPES } from '@/lib/constants';
|
||||
|
||||
interface WorldBuilderProps {
|
||||
@@ -146,6 +146,18 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
|
||||
}
|
||||
};
|
||||
|
||||
const handleAvatarUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file || !tempEntity) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
const base64 = event.target?.result as string;
|
||||
setTempEntity({ ...tempEntity, avatar: base64 });
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
};
|
||||
|
||||
// --- TEMPLATE ACTIONS ---
|
||||
|
||||
const addCustomField = (type: EntityType) => {
|
||||
@@ -614,8 +626,19 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
|
||||
className={`p-3 cursor-pointer hover:bg-blue-500/10 transition-colors flex justify-between group ${editingId === entity.id ? 'bg-blue-500/10 border-l-4 border-blue-500' : ''}`}
|
||||
>
|
||||
<div>
|
||||
<div className="font-medium text-theme-text">{entity.name}</div>
|
||||
<div className="text-xs text-theme-muted truncate">{entity.description}</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{entity.avatar ? (
|
||||
<img src={entity.avatar} alt={entity.name} className="w-8 h-8 rounded-full object-cover border border-theme-border shadow-sm" />
|
||||
) : (
|
||||
<div className={`w-8 h-8 rounded-full flex items-center justify-center font-bold text-xs ${ENTITY_COLORS[entity.type]} shadow-sm`}>
|
||||
{entity.name.substring(0, 2).toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<div className="font-medium text-theme-text">{entity.name}</div>
|
||||
<div className="text-xs text-theme-muted truncate">{entity.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleDelete(entity.id); }}
|
||||
@@ -651,25 +674,46 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-theme-text mb-1">{t('wb.name')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={tempEntity.name}
|
||||
onChange={e => setTempEntity({ ...tempEntity, name: e.target.value })}
|
||||
className="w-full p-2 bg-theme-bg border border-theme-border rounded focus:ring-2 focus:ring-blue-500 outline-none font-serif text-lg"
|
||||
placeholder={t('wb.name_ph')}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col md:flex-row gap-6">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<label className="relative group cursor-pointer">
|
||||
<input type="file" accept="image/*" onChange={handleAvatarUpload} className="hidden" />
|
||||
{tempEntity.avatar ? (
|
||||
<img src={tempEntity.avatar} alt="Avatar" className="w-24 h-24 rounded-2xl object-cover shadow-md border-2 border-theme-border group-hover:border-indigo-400 transition-all" />
|
||||
) : (
|
||||
<div className="w-24 h-24 rounded-2xl bg-slate-100 flex items-center justify-center border-2 border-dashed border-slate-300 group-hover:border-indigo-400 group-hover:bg-indigo-50 transition-all text-slate-400">
|
||||
<Camera size={32} />
|
||||
</div>
|
||||
)}
|
||||
<div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 rounded-2xl flex items-center justify-center transition-opacity">
|
||||
<Camera size={24} className="text-white" />
|
||||
</div>
|
||||
</label>
|
||||
<span className="text-xs font-semibold text-theme-muted text-center">Avatar / Plan</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-theme-text mb-1">{t('wb.short_desc')}</label>
|
||||
<textarea
|
||||
value={tempEntity.description}
|
||||
onChange={e => setTempEntity({ ...tempEntity, description: e.target.value })}
|
||||
className="w-full p-2 bg-theme-bg border border-theme-border rounded focus:ring-2 focus:ring-blue-500 outline-none text-sm h-20"
|
||||
placeholder={t('wb.short_desc_ph')}
|
||||
/>
|
||||
<div className="flex-1 space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-theme-text mb-1">{t('wb.name')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={tempEntity.name}
|
||||
onChange={e => setTempEntity({ ...tempEntity, name: e.target.value })}
|
||||
className="w-full p-2 bg-theme-bg border border-theme-border rounded focus:ring-2 focus:ring-blue-500 outline-none font-serif text-lg"
|
||||
placeholder={t('wb.name_ph')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-theme-text mb-1">{t('wb.short_desc')}</label>
|
||||
<textarea
|
||||
value={tempEntity.description}
|
||||
onChange={e => setTempEntity({ ...tempEntity, description: e.target.value })}
|
||||
className="w-full p-2 bg-theme-bg border border-theme-border rounded focus:ring-2 focus:ring-blue-500 outline-none text-sm h-20"
|
||||
placeholder={t('wb.short_desc_ph')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{tempEntity.type === EntityType.CHARACTER && renderCharacterEditor()}
|
||||
|
||||
@@ -89,6 +89,7 @@ export const useProjects = (user: UserProfile | null) => {
|
||||
id: e.id,
|
||||
type: e.type,
|
||||
name: e.name,
|
||||
avatar: e.avatar,
|
||||
description: e.description,
|
||||
details: e.details,
|
||||
storyContext: e.storyContext,
|
||||
@@ -264,6 +265,7 @@ export const useProjects = (user: UserProfile | null) => {
|
||||
projectId,
|
||||
type,
|
||||
name: initialData?.name || `Nouveau ${type}`,
|
||||
avatar: initialData?.avatar || undefined,
|
||||
description: initialData?.description || '',
|
||||
details: initialData?.details || '',
|
||||
attributes: initialData?.attributes || undefined,
|
||||
@@ -278,6 +280,7 @@ export const useProjects = (user: UserProfile | null) => {
|
||||
id: newEntity.id,
|
||||
type: newEntity.type,
|
||||
name: newEntity.name,
|
||||
avatar: newEntity.avatar,
|
||||
description: newEntity.description,
|
||||
details: newEntity.details,
|
||||
attributes: newEntity.attributes,
|
||||
|
||||
@@ -124,7 +124,7 @@ const api = {
|
||||
|
||||
// --- ENTITIES ---
|
||||
entities: {
|
||||
async create(data: { projectId: string; type: string; name?: string; description?: string; details?: string; attributes?: any; customValues?: any }) {
|
||||
async create(data: { projectId: string; type: string; name?: string; avatar?: string; description?: string; details?: string; attributes?: any; customValues?: any }) {
|
||||
return api.request<any>('/entities', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
|
||||
@@ -164,6 +164,8 @@ export const translations = {
|
||||
'auth.signup_link': "S'inscrire",
|
||||
'auth.signin_link': 'Se connecter',
|
||||
'auth.back_to_site': '← Revenir au site',
|
||||
'auth.accept_cgu': "J'ai lu et j'accepte les ",
|
||||
'auth.cgu_link': "Conditions Générales d'Utilisation",
|
||||
'auth.hero_title_part1': "L'endroit où vos",
|
||||
'auth.hero_title_part2': "histoires",
|
||||
'auth.hero_title_part3': "prennent vie.",
|
||||
@@ -495,6 +497,8 @@ export const translations = {
|
||||
'auth.signup_link': 'Sign up',
|
||||
'auth.signin_link': 'Log in',
|
||||
'auth.back_to_site': '← Back to site',
|
||||
'auth.accept_cgu': "I have read and agree to the ",
|
||||
'auth.cgu_link': "Terms of Service",
|
||||
'auth.hero_title_part1': "The place where your",
|
||||
'auth.hero_title_part2': "stories",
|
||||
'auth.hero_title_part3': "come to life.",
|
||||
@@ -826,6 +830,8 @@ export const translations = {
|
||||
'auth.signup_link': 'Regístrate',
|
||||
'auth.signin_link': 'Iniciar sesión',
|
||||
'auth.back_to_site': '← Volver al sitio',
|
||||
'auth.accept_cgu': "He leído y acepto los ",
|
||||
'auth.cgu_link': "Términos de Servicio",
|
||||
'auth.hero_title_part1': "El lugar donde tus",
|
||||
'auth.hero_title_part2': "historias",
|
||||
'auth.hero_title_part3': "cobran vida.",
|
||||
@@ -1157,6 +1163,8 @@ export const translations = {
|
||||
'auth.signup_link': 'Registrieren',
|
||||
'auth.signin_link': 'Anmelden',
|
||||
'auth.back_to_site': '← Zurück zur Website',
|
||||
'auth.accept_cgu': "Ich akzeptiere die ",
|
||||
'auth.cgu_link': "Nutzungsbedingungen",
|
||||
'auth.hero_title_part1': "Der Ort, an dem deine",
|
||||
'auth.hero_title_part2': "Geschichten",
|
||||
'auth.hero_title_part3': "zum Leben erwachen.",
|
||||
|
||||
@@ -41,6 +41,7 @@ export interface Entity {
|
||||
id: string;
|
||||
type: EntityType;
|
||||
name: string;
|
||||
avatar?: string;
|
||||
description: string;
|
||||
details: string;
|
||||
storyContext?: string;
|
||||
|
||||
Reference in New Issue
Block a user