mirror of
https://github.com/R0m1k3/Menu.git
synced 2026-10-16 11:46:20 +02:00
feat: initialize project with Next.js, Prisma, and admin dashboard functionality
This commit is contained in:
commit
b73aa8616b
37 files changed
+8711
No files matched your search
@@ -0,0 +1,78 @@
|
||||
'use server';
|
||||
|
||||
import { prisma } from '@/lib/db';
|
||||
import { verifyPassword } from '@/lib/auth';
|
||||
import { cookies } from 'next/headers';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
/**
|
||||
* Handle admin login.
|
||||
* @param formData - The form data containing the password.
|
||||
*/
|
||||
export async function loginAdmin(formData: FormData) {
|
||||
const password = formData.get('password') as string;
|
||||
|
||||
const config = await prisma.config.findUnique({
|
||||
where: { id: 'admin_password' },
|
||||
});
|
||||
|
||||
if (!config) {
|
||||
redirect('/setup');
|
||||
}
|
||||
|
||||
const isValid = await verifyPassword(password, config.value);
|
||||
|
||||
if (isValid) {
|
||||
const cookieStore = await cookies();
|
||||
cookieStore.set('admin_auth', 'true', {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
maxAge: 60 * 60 * 24, // 24 hours
|
||||
path: '/',
|
||||
});
|
||||
redirect('/admin');
|
||||
} else {
|
||||
throw new Error('Mot de passe incorrect.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout admin.
|
||||
*/
|
||||
export async function logoutAdmin() {
|
||||
const cookieStore = await cookies();
|
||||
cookieStore.delete('admin_auth');
|
||||
redirect('/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Is authenticated check.
|
||||
*/
|
||||
export async function isAuthenticated() {
|
||||
const cookieStore = await cookies();
|
||||
return cookieStore.has('admin_auth');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new menu button.
|
||||
*/
|
||||
export async function addButton(formData: FormData) {
|
||||
if (!(await isAuthenticated())) throw new Error('Non autorisé');
|
||||
|
||||
const name = formData.get('name') as string;
|
||||
const url = formData.get('url') as string;
|
||||
|
||||
if (!name || !url) throw new Error('Champs requis manquants.');
|
||||
|
||||
await prisma.button.create({
|
||||
data: { name, url, order: 0 },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a menu button.
|
||||
*/
|
||||
export async function deleteButton(id: number) {
|
||||
if (!(await isAuthenticated())) throw new Error('Non autorisé');
|
||||
await prisma.button.delete({ where: { id } });
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
'use server';
|
||||
|
||||
import { prisma } from '@/lib/db';
|
||||
import { hashPassword } from '@/lib/auth';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
/**
|
||||
* Save the initial admin password.
|
||||
* @param formData - The form data containing the password.
|
||||
*/
|
||||
export async function setupAdmin(formData: FormData) {
|
||||
const password = formData.get('password') as string;
|
||||
const confirm = formData.get('confirm') as string;
|
||||
|
||||
if (!password || password !== confirm) {
|
||||
throw new Error('Les mots de passe ne correspondent pas.');
|
||||
}
|
||||
|
||||
// Check if already setup
|
||||
const existing = await prisma.config.findUnique({
|
||||
where: { id: 'admin_password' },
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
throw new Error('La configuration a déjà été effectuée.');
|
||||
}
|
||||
|
||||
const hashedPassword = await hashPassword(password);
|
||||
|
||||
await prisma.config.create({
|
||||
data: {
|
||||
id: 'admin_password',
|
||||
value: hashedPassword,
|
||||
},
|
||||
});
|
||||
|
||||
redirect('/admin');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the application is configured.
|
||||
*/
|
||||
export async function isConfigured() {
|
||||
const config = await prisma.config.findUnique({
|
||||
where: { id: 'admin_password' },
|
||||
});
|
||||
return !!config;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { GlassCard } from '@/components/GlassCard';
|
||||
import { loginAdmin } from '@/app/actions/admin';
|
||||
|
||||
export default function AdminLoginPage() {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const formData = new FormData(e.currentTarget);
|
||||
try {
|
||||
await loginAdmin(formData);
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Une erreur est survenue.');
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container" style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh' }}>
|
||||
<GlassCard style={{ width: '100%', maxWidth: '400px' }}>
|
||||
<h1 style={{ marginBottom: '2rem', fontSize: '1.8rem' }}>Accès Admin</h1>
|
||||
|
||||
<form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
|
||||
<div>
|
||||
<label style={{ display: 'block', marginBottom: '0.5rem', fontSize: '0.9rem', fontWeight: 600 }}>Mot de passe</label>
|
||||
<input name="password" type="password" required placeholder="••••••••" />
|
||||
</div>
|
||||
|
||||
{error && <p style={{ color: 'var(--error)', fontSize: '0.85rem', textAlign: 'center' }}>{error}</p>}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
style={{
|
||||
background: 'var(--accent)',
|
||||
color: 'white',
|
||||
padding: '1rem',
|
||||
borderRadius: '12px',
|
||||
fontWeight: 600,
|
||||
marginTop: '1rem',
|
||||
opacity: loading ? 0.7 : 1
|
||||
}}
|
||||
>
|
||||
{loading ? 'Connexion...' : 'Se connecter'}
|
||||
</button>
|
||||
</form>
|
||||
</GlassCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import { prisma } from '@/lib/db';
|
||||
import { isAuthenticated, logoutAdmin, addButton, deleteButton } from '@/app/actions/admin';
|
||||
import { redirect } from 'next/navigation';
|
||||
import { GlassCard } from '@/components/GlassCard';
|
||||
import { Trash2, LogOut, PlusCircle, Globe } from 'lucide-react';
|
||||
|
||||
export default async function AdminPage() {
|
||||
const auth = await isAuthenticated();
|
||||
|
||||
if (!auth) {
|
||||
redirect('/admin/login');
|
||||
}
|
||||
|
||||
const buttons = await prisma.button.findMany({
|
||||
orderBy: { order: 'asc' },
|
||||
});
|
||||
|
||||
return (
|
||||
<main className="container">
|
||||
<header style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '3rem' }}>
|
||||
<h1 style={{ marginBottom: 0, fontSize: '2rem' }}>Panel d'Administration</h1>
|
||||
<form action={logoutAdmin}>
|
||||
<button type="submit" style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', background: 'transparent', color: 'var(--foreground)', opacity: 0.7 }}>
|
||||
<LogOut size={20} /> Déconnexion
|
||||
</button>
|
||||
</form>
|
||||
</header>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(300px, 1fr) 2fr', gap: '3rem', alignItems: 'start' }}>
|
||||
{/* Form to add button */}
|
||||
<GlassCard>
|
||||
<h2 style={{ fontSize: '1.25rem', marginBottom: '1.5rem', display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||
<PlusCircle size={20} color="var(--accent)" /> Ajouter un bouton
|
||||
</h2>
|
||||
<form action={addButton} style={{ display: 'flex', flexDirection: 'column', gap: '1.2rem' }}>
|
||||
<div>
|
||||
<label style={{ display: 'block', marginBottom: '0.4rem', fontSize: '0.85rem', fontWeight: 600 }}>Nom de l'application</label>
|
||||
<input name="name" type="text" placeholder="Ex: Slack, Jira..." required />
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'block', marginBottom: '0.4rem', fontSize: '0.85rem', fontWeight: 600 }}>Lien (URL)</label>
|
||||
<input name="url" type="url" placeholder="https://..." required />
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
style={{ background: 'var(--accent)', color: 'white', padding: '0.8rem', borderRadius: '10px', fontWeight: 600, marginTop: '0.5rem' }}
|
||||
>
|
||||
Ajouter au menu
|
||||
</button>
|
||||
</form>
|
||||
</GlassCard>
|
||||
|
||||
{/* List of buttons */}
|
||||
<div>
|
||||
<h2 style={{ fontSize: '1.25rem', marginBottom: '1.5rem' }}>Boutons configurés</h2>
|
||||
{buttons.length === 0 ? (
|
||||
<p style={{ opacity: 0.5 }}>Aucun bouton configuré pour le moment.</p>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||
{buttons.map((btn: any) => (
|
||||
<GlassCard key={btn.id} style={{ padding: '1.2rem', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
|
||||
<div style={{ background: 'var(--accent)', padding: '0.5rem', borderRadius: '8px', color: 'white' }}>
|
||||
<Globe size={18} />
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontWeight: 600 }}>{btn.name}</div>
|
||||
<div style={{ fontSize: '0.8rem', opacity: 0.6 }}>{btn.url}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form action={async () => {
|
||||
'use server';
|
||||
await deleteButton(btn.id);
|
||||
}}>
|
||||
<button
|
||||
type="submit"
|
||||
style={{ background: 'transparent', color: 'var(--error)', padding: '0.5rem', borderRadius: '8px' }}
|
||||
>
|
||||
<Trash2 size={20} />
|
||||
</button>
|
||||
</form>
|
||||
</GlassCard>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
+105
@@ -0,0 +1,105 @@
|
||||
:root {
|
||||
/* Common */
|
||||
--font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
--transition-smooth: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
/* Light Theme */
|
||||
--background: #f5f5f7;
|
||||
--foreground: #1d1d1f;
|
||||
--glass-bg: rgba(255, 255, 255, 0.7);
|
||||
--glass-border: rgba(255, 255, 255, 0.3);
|
||||
--card-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.07);
|
||||
--accent: #0071e3;
|
||||
--error: #ff3b30;
|
||||
--bg-gradient: radial-gradient(circle at 0% 0%, #fff 0%, #f5f5f7 100%);
|
||||
}
|
||||
|
||||
[data-theme='dark'] {
|
||||
/* Dark Theme */
|
||||
--background: #000000;
|
||||
--foreground: #f5f5f7;
|
||||
--glass-bg: rgba(28, 28, 30, 0.7);
|
||||
--glass-border: rgba(255, 255, 255, 0.1);
|
||||
--card-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.4);
|
||||
--accent: #0a84ff;
|
||||
--error: #ff453a;
|
||||
--bg-gradient: radial-gradient(circle at 100% 100%, #1c1c1e 0%, #000 100%);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--bg-gradient);
|
||||
background-attachment: fixed;
|
||||
color: var(--foreground);
|
||||
font-family: var(--font-family);
|
||||
min-height: 100vh;
|
||||
transition: var(--transition-smooth);
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Custom Scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(125, 125, 125, 0.3);
|
||||
border-radius: 10px;
|
||||
border: 3px solid transparent;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(125, 125, 125, 0.5);
|
||||
}
|
||||
|
||||
.glass {
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid var(--glass-border);
|
||||
box-shadow: var(--card-shadow);
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.022em;
|
||||
margin-bottom: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
button, .button {
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-family: inherit;
|
||||
transition: var(--transition-smooth);
|
||||
}
|
||||
|
||||
input {
|
||||
background: var(--glass-bg);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: 12px;
|
||||
padding: 0.8rem 1rem;
|
||||
color: var(--foreground);
|
||||
font-family: inherit;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: 2px solid var(--accent);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { Inter } from 'next/font/google';
|
||||
import './globals.css';
|
||||
import { ThemeProvider } from '@/components/ThemeProvider';
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] });
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Menu Professional',
|
||||
description: 'Industrial dashboard for SaaS applications',
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="fr" suppressHydrationWarning>
|
||||
<body className={inter.className}>
|
||||
<ThemeProvider
|
||||
attribute="data-theme"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
.page {
|
||||
--background: #fafafa;
|
||||
--foreground: #fff;
|
||||
|
||||
--text-primary: #000;
|
||||
--text-secondary: #666;
|
||||
|
||||
--button-primary-hover: #383838;
|
||||
--button-secondary-hover: #f2f2f2;
|
||||
--button-secondary-border: #ebebeb;
|
||||
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: var(--font-geist-sans);
|
||||
background-color: var(--background);
|
||||
}
|
||||
|
||||
.main {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
background-color: var(--foreground);
|
||||
padding: 120px 60px;
|
||||
}
|
||||
|
||||
.intro {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
text-align: left;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.intro h1 {
|
||||
max-width: 320px;
|
||||
font-size: 40px;
|
||||
font-weight: 600;
|
||||
line-height: 48px;
|
||||
letter-spacing: -2.4px;
|
||||
text-wrap: balance;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.intro p {
|
||||
max-width: 440px;
|
||||
font-size: 18px;
|
||||
line-height: 32px;
|
||||
text-wrap: balance;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.intro a {
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.ctas {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
max-width: 440px;
|
||||
gap: 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.ctas a {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
padding: 0 16px;
|
||||
border-radius: 128px;
|
||||
border: 1px solid transparent;
|
||||
transition: 0.2s;
|
||||
cursor: pointer;
|
||||
width: fit-content;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
a.primary {
|
||||
background: var(--text-primary);
|
||||
color: var(--background);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
a.secondary {
|
||||
border-color: var(--button-secondary-border);
|
||||
}
|
||||
|
||||
/* Enable hover only on non-touch devices */
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
a.primary:hover {
|
||||
background: var(--button-primary-hover);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
a.secondary:hover {
|
||||
background: var(--button-secondary-hover);
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.main {
|
||||
padding: 48px 24px;
|
||||
}
|
||||
|
||||
.intro {
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.intro h1 {
|
||||
font-size: 32px;
|
||||
line-height: 40px;
|
||||
letter-spacing: -1.92px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.logo {
|
||||
filter: invert();
|
||||
}
|
||||
|
||||
.page {
|
||||
--background: #000;
|
||||
--foreground: #000;
|
||||
|
||||
--text-primary: #ededed;
|
||||
--text-secondary: #999;
|
||||
|
||||
--button-primary-hover: #ccc;
|
||||
--button-secondary-hover: #1a1a1a;
|
||||
--button-secondary-border: #1a1a1a;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { prisma } from '@/lib/db';
|
||||
import { isConfigured } from '@/app/actions/setup';
|
||||
import { redirect } from 'next/navigation';
|
||||
import { MenuButton } from '@/components/MenuButton';
|
||||
import { ThemeToggle } from '@/components/ThemeToggle';
|
||||
|
||||
export default async function Home() {
|
||||
const configured = await isConfigured();
|
||||
|
||||
if (!configured) {
|
||||
redirect('/setup');
|
||||
}
|
||||
|
||||
const buttons = await prisma.button.findMany({
|
||||
orderBy: { order: 'asc' },
|
||||
});
|
||||
|
||||
return (
|
||||
<main className="container">
|
||||
<header style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '4rem' }}>
|
||||
<h1 style={{ marginBottom: 0 }}>Application Menu</h1>
|
||||
<ThemeToggle />
|
||||
</header>
|
||||
|
||||
{buttons.length === 0 ? (
|
||||
<div style={{ textAlign: 'center', marginTop: '4rem' }}>
|
||||
<p style={{ opacity: 0.6 }}>Aucune application configurée. Rendez-vous dans le panel admin.</p>
|
||||
<a href="/admin" style={{ color: 'var(--accent)', marginTop: '1rem', display: 'inline-block' }}>Accéder à l'administration</a>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(240px, 1fr))',
|
||||
gap: '2.5rem'
|
||||
}}>
|
||||
{buttons.map((btn: any) => (
|
||||
<MenuButton
|
||||
key={btn.id}
|
||||
name={btn.name}
|
||||
url={btn.url}
|
||||
iconName="ExternalLink"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { GlassCard } from '@/components/GlassCard';
|
||||
import { setupAdmin } from '@/app/actions/setup';
|
||||
|
||||
export default function SetupPage() {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const formData = new FormData(e.currentTarget);
|
||||
try {
|
||||
await setupAdmin(formData);
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Une erreur est survenue.');
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container" style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh' }}>
|
||||
<GlassCard style={{ width: '100%', maxWidth: '450px' }}>
|
||||
<h1 style={{ marginBottom: '1rem', fontSize: '1.8rem' }}>Configuration Initiale</h1>
|
||||
<p style={{ textAlign: 'center', marginBottom: '2rem', color: 'var(--foreground)', opacity: 0.8 }}>
|
||||
Définissez le mot de passe maître pour accéder au panel d'administration.
|
||||
</p>
|
||||
|
||||
<form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
|
||||
<div>
|
||||
<label style={{ display: 'block', marginBottom: '0.5rem', fontSize: '0.9rem', fontWeight: 600 }}>Mot de passe Admin</label>
|
||||
<input name="password" type="password" required placeholder="••••••••" />
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'block', marginBottom: '0.5rem', fontSize: '0.9rem', fontWeight: 600 }}>Confirmer le mot de passe</label>
|
||||
<input name="confirm" type="password" required placeholder="••••••••" />
|
||||
</div>
|
||||
|
||||
{error && <p style={{ color: 'var(--error)', fontSize: '0.85rem', textAlign: 'center' }}>{error}</p>}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
style={{
|
||||
background: 'var(--accent)',
|
||||
color: 'white',
|
||||
padding: '1rem',
|
||||
borderRadius: '12px',
|
||||
fontWeight: 600,
|
||||
marginTop: '1rem',
|
||||
opacity: loading ? 0.7 : 1
|
||||
}}
|
||||
>
|
||||
{loading ? 'Configuration...' : 'Finaliser la configuration'}
|
||||
</button>
|
||||
</form>
|
||||
</GlassCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user