diff --git a/app/globals.css b/app/globals.css index cca20f1..3fec6c7 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,44 +1,43 @@ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); :root { - --font: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + --font: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; --ease: cubic-bezier(0.4, 0, 0.2, 1); - --duration: 280ms; + --duration: 260ms; - /* Light */ - --bg-base: #f2f2f7; - --fg: #1c1c1e; - --fg-secondary: #6c6c70; - --glass-bg: rgba(255, 255, 255, 0.62); - --glass-border: rgba(255, 255, 255, 0.8); - --glass-shadow: 0 8px 40px rgba(0, 0, 0, 0.08), 0 1px 0 rgba(255,255,255,0.9) inset; - --glass-hover-shadow: 0 20px 60px rgba(0, 0, 0, 0.12), 0 1px 0 rgba(255,255,255,0.9) inset; - --accent: #007AFF; - --accent-glow: rgba(0, 122, 255, 0.25); - --error: #FF3B30; - --success: #34C759; + /* ─── Dark (Default: Purple → Wine) ─── */ + --bg-from: #3a0f3c; + --bg-to: #7b1535; + --fg: #ffffff; + --fg-secondary: rgba(255, 255, 255, 0.55); - --orb-1: radial-gradient(ellipse at 20% 20%, rgba(0, 122, 255, 0.15) 0%, transparent 60%); - --orb-2: radial-gradient(ellipse at 80% 80%, rgba(88, 86, 214, 0.12) 0%, transparent 60%); - --orb-3: radial-gradient(ellipse at 60% 10%, rgba(52, 199, 89, 0.08) 0%, transparent 50%); + --card-bg: rgba(255, 255, 255, 0.10); + --card-border: rgba(255, 255, 255, 0.12); + --card-shadow: 0 4px 24px rgba(0, 0, 0, 0.25); + --card-hover-shadow: 0 12px 40px rgba(0, 0, 0, 0.4); + --card-hover-bg: rgba(255, 255, 255, 0.15); + + --accent: #ffffff; + --accent-btn: rgba(255, 255, 255, 0.18); + --error: #FF6B7A; } -[data-theme='dark'] { - --bg-base: #000000; - --fg: #f5f5f7; - --fg-secondary: #8e8e93; - --glass-bg: rgba(28, 28, 30, 0.72); - --glass-border: rgba(255, 255, 255, 0.08); - --glass-shadow: 0 8px 40px rgba(0, 0, 0, 0.4), 0 1px 0 rgba(255,255,255,0.05) inset; - --glass-hover-shadow: 0 20px 60px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(0, 122, 255, 0.3); - --accent: #0A84FF; - --accent-glow: rgba(10, 132, 255, 0.35); - --error: #FF453A; - --success: #30D158; +[data-theme='light'] { + /* ─── Light: Lavender → Pink ─── */ + --bg-from: #e8d5f5; + --bg-to: #f5c2d0; + --fg: #1c1c2e; + --fg-secondary: rgba(28, 28, 46, 0.55); - --orb-1: radial-gradient(ellipse at 20% 20%, rgba(10, 132, 255, 0.18) 0%, transparent 60%); - --orb-2: radial-gradient(ellipse at 80% 80%, rgba(94, 92, 230, 0.18) 0%, transparent 60%); - --orb-3: radial-gradient(ellipse at 60% 10%, rgba(48, 209, 88, 0.08) 0%, transparent 50%); + --card-bg: rgba(255, 255, 255, 0.45); + --card-border: rgba(255, 255, 255, 0.8); + --card-shadow: 0 4px 24px rgba(100, 30, 60, 0.10); + --card-hover-shadow: 0 12px 40px rgba(100, 30, 60, 0.18); + --card-hover-bg: rgba(255, 255, 255, 0.65); + + --accent: #1c1c2e; + --accent-btn: rgba(28, 28, 46, 0.12); + --error: #e0334e; } *, *::before, *::after { @@ -47,127 +46,91 @@ padding: 0; } -html { +html, body { height: 100%; } body { min-height: 100vh; - background-color: var(--bg-base); - color: var(--fg); font-family: var(--font); + color: var(--fg); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; overflow-x: hidden; - transition: background-color var(--duration) var(--ease), color var(--duration) var(--ease); + background: linear-gradient(145deg, var(--bg-from) 0%, var(--bg-to) 100%); + background-attachment: fixed; + transition: background var(--duration) var(--ease), color var(--duration) var(--ease); } -/* Ambient background orbs */ -body::before { - content: ''; - position: fixed; - inset: 0; - background: var(--orb-1), var(--orb-2), var(--orb-3); - pointer-events: none; - z-index: 0; -} +a { text-decoration: none; color: inherit; } +button { cursor: pointer; border: none; outline: none; font-family: var(--font); } -#__next, main { - position: relative; - z-index: 1; -} - -/* Scrollbar */ -::-webkit-scrollbar { width: 8px; } +/* ── Scrollbar ── */ +::-webkit-scrollbar { width: 6px; } ::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-thumb { - background: rgba(128, 128, 128, 0.25); + background: rgba(255,255,255,0.2); border-radius: 10px; - border: 2px solid transparent; - background-clip: padding-box; -} -::-webkit-scrollbar-thumb:hover { background: rgba(128, 128, 128, 0.4); background-clip: padding-box; } - -/* Glass utility */ -.glass { - background: var(--glass-bg); - backdrop-filter: blur(24px) saturate(1.8); - -webkit-backdrop-filter: blur(24px) saturate(1.8); - border: 1px solid var(--glass-border); - box-shadow: var(--glass-shadow); - border-radius: 20px; } -/* Page layout */ -.page-container { - max-width: 1400px; - margin: 0 auto; - padding: 2rem; - min-height: 100vh; -} - -/* Forms */ +/* ── Input ── */ input[type="text"], input[type="password"], input[type="url"], input[type="email"] { width: 100%; padding: 0.85rem 1rem; - background: var(--glass-bg); - border: 1px solid var(--glass-border); + background: var(--card-bg); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + border: 1px solid var(--card-border); border-radius: 12px; color: var(--fg); font-family: var(--font); font-size: 0.95rem; - transition: all var(--duration) var(--ease); - backdrop-filter: blur(12px); outline: none; + transition: all var(--duration) var(--ease); } input:focus { - border-color: var(--accent); - box-shadow: 0 0 0 3px var(--accent-glow); + border-color: rgba(255,255,255,0.5); + box-shadow: 0 0 0 3px rgba(255,255,255,0.10); } -input::placeholder { - color: var(--fg-secondary); -} +input::placeholder { color: var(--fg-secondary); } -button { - cursor: pointer; - border: none; - outline: none; - font-family: var(--font); +/* ── Glass card ── */ +.glass-card { + background: var(--card-bg); + backdrop-filter: blur(24px) saturate(1.4); + -webkit-backdrop-filter: blur(24px) saturate(1.4); + border: 1px solid var(--card-border); + box-shadow: var(--card-shadow); + border-radius: 20px; transition: all var(--duration) var(--ease); } -a { - text-decoration: none; - color: inherit; -} - -/* Btn primary */ +/* ── Btn primary ── */ .btn-primary { - background: var(--accent); - color: white; - padding: 0.85rem 1.5rem; - border-radius: 12px; - font-size: 0.95rem; + background: rgba(255,255,255,0.18); + color: var(--fg); + border: 1px solid rgba(255,255,255,0.25); + padding: 0.75rem 1.4rem; + border-radius: 50px; + font-size: 0.88rem; font-weight: 600; letter-spacing: -0.01em; + backdrop-filter: blur(12px); + transition: all var(--duration) var(--ease); } .btn-primary:hover { - filter: brightness(1.1); + background: rgba(255,255,255,0.28); transform: translateY(-1px); - box-shadow: 0 8px 20px var(--accent-glow); -} - -.btn-primary:active { - transform: translateY(0); + box-shadow: 0 4px 16px rgba(0,0,0,0.2); } .btn-primary:disabled { - opacity: 0.6; + opacity: 0.5; pointer-events: none; } diff --git a/app/layout.tsx b/app/layout.tsx index d3ec84a..268449f 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -20,8 +20,8 @@ export default function RootLayout({ {children} diff --git a/app/page.tsx b/app/page.tsx index c775df5..f257845 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -10,113 +10,88 @@ export const dynamic = 'force-dynamic'; export default async function Home() { const configured = await isConfigured(); - - if (!configured) { - redirect('/setup'); - } + if (!configured) redirect('/setup'); const buttons = await prisma.button.findMany({ orderBy: { order: 'asc' }, }); return ( -
- {/* Header */} +
+ {/* Top bar */}
-
-

- Applications -

- {buttons.length > 0 && ( -

- {buttons.length} {buttons.length > 1 ? 'liens configurés' : 'lien configuré'} -

- )} -
- -
- - - - -
+ + + +
- {/* Button grid */} - {buttons.length === 0 ? ( -
-
- 🔗 -
-
-

Aucune application

-

- Ajoutez vos premiers liens depuis le panel d'administration. + {/* Center content */} +

+ {buttons.length === 0 ? ( +
+
🔗
+

+ Aucune application configurée

+

+ Ajoutez vos premiers raccourcis depuis l'administration. +

+ + Configurer → +
- - Configurer → - -
- ) : ( -
- {buttons.map((btn: any) => ( - - ))} -
- )} + {buttons.map((btn: any) => ( + + ))} +
+ )} +
); } diff --git a/components/MenuButton.module.css b/components/MenuButton.module.css index 55e12ec..172dabf 100644 --- a/components/MenuButton.module.css +++ b/components/MenuButton.module.css @@ -4,60 +4,53 @@ align-items: center; justify-content: center; gap: 1rem; - padding: 2rem 1.5rem; + padding: 2rem 1.25rem 1.75rem; text-decoration: none; color: var(--fg); - min-height: 180px; + width: 160px; + aspect-ratio: 1 / 1; cursor: pointer; + background: var(--card-bg); + backdrop-filter: blur(24px) saturate(1.4); + -webkit-backdrop-filter: blur(24px) saturate(1.4); + border: 1px solid var(--card-border); + box-shadow: var(--card-shadow); + border-radius: 22px; + transition: all 240ms cubic-bezier(0.4, 0, 0.2, 1); position: relative; overflow: hidden; } -/* Glow accent on hover */ -.menuButton::after { - content: ''; - position: absolute; - inset: 0; - border-radius: inherit; - background: radial-gradient(ellipse at center, var(--accent-glow) 0%, transparent 70%); - opacity: 0; - transition: opacity 280ms cubic-bezier(0.4, 0, 0.2, 1); - pointer-events: none; +.menuButton:hover { + background: var(--card-hover-bg); + box-shadow: var(--card-hover-shadow); + transform: translateY(-5px) scale(1.02); } -.menuButton:hover::after { - opacity: 1; +.menuButton:active { + transform: scale(0.97); + transition-duration: 80ms; } -/* Favicon wrapper */ +/* Favicon */ .faviconWrapper { - width: 64px; - height: 64px; - border-radius: 16px; - background: var(--glass-bg); - border: 1px solid var(--glass-border); + width: 56px; + height: 56px; + border-radius: 14px; display: flex; align-items: center; justify-content: center; overflow: hidden; - transition: transform 280ms cubic-bezier(0.4, 0, 0.2, 1), box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); - flex-shrink: 0; - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08); -} - -.menuButton:hover .faviconWrapper { - transform: scale(1.08); - box-shadow: 0 8px 24px var(--accent-glow); } .favicon { - width: 40px; - height: 40px; + width: 48px; + height: 48px; object-fit: contain; } .faviconFallback { - color: var(--accent); + color: rgba(255,255,255,0.7); display: flex; align-items: center; justify-content: center; @@ -65,29 +58,13 @@ /* Label */ .label { - font-size: 0.95rem; + font-size: 0.82rem; font-weight: 600; - letter-spacing: -0.02em; text-align: center; - line-height: 1.3; - max-width: 160px; + letter-spacing: -0.01em; + color: var(--fg); + max-width: 130px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } - -/* Arrow indicator */ -.arrowIcon { - position: absolute; - top: 12px; - right: 14px; - color: var(--fg-secondary); - opacity: 0; - transform: translate(-4px, 4px); - transition: all 280ms cubic-bezier(0.4, 0, 0.2, 1); -} - -.menuButton:hover .arrowIcon { - opacity: 1; - transform: translate(0, 0); -} diff --git a/components/MenuButton.tsx b/components/MenuButton.tsx index eb616a2..45ed8c0 100644 --- a/components/MenuButton.tsx +++ b/components/MenuButton.tsx @@ -3,8 +3,7 @@ import React, { useState } from 'react'; import Link from 'next/link'; import Image from 'next/image'; -import { ArrowUpRight, Globe } from 'lucide-react'; -import { GlassCard } from './GlassCard'; +import { Globe } from 'lucide-react'; import styles from './MenuButton.module.css'; interface MenuButtonProps { @@ -12,22 +11,14 @@ interface MenuButtonProps { url: string; } -/** - * Extract base domain from URL for favicon fetch. - */ function getDomain(url: string): string { try { - const { hostname } = new URL(url); - return hostname; + return new URL(url).hostname; } catch { return url; } } -/** - * MenuButton component displaying a favicon and app name. - * Favicons are fetched from Google's favicon CDN. - */ export const MenuButton: React.FC = ({ name, url }) => { const domain = getDomain(url); const faviconUrl = `https://www.google.com/s2/favicons?domain=${domain}&sz=64`; @@ -35,29 +26,24 @@ export const MenuButton: React.FC = ({ name, url }) => { return ( - - - -
- {!faviconError ? ( - {`${name} setFaviconError(true)} - unoptimized - /> - ) : ( -
- -
- )} -
- - {name} -
+
+ {!faviconError ? ( + {`${name} setFaviconError(true)} + unoptimized + /> + ) : ( +
+ +
+ )} +
+ {name} ); }; diff --git a/components/ThemeToggle.tsx b/components/ThemeToggle.tsx index e5c269d..b746709 100644 --- a/components/ThemeToggle.tsx +++ b/components/ThemeToggle.tsx @@ -8,37 +8,32 @@ export function ThemeToggle() { const { theme, setTheme } = useTheme(); const [mounted, setMounted] = React.useState(false); - React.useEffect(() => { - setMounted(true); - }, []); + React.useEffect(() => setMounted(true), []); - if (!mounted) { - return
; - } + if (!mounted) return
; return ( );