Files
ketopath/apps/web/src/app/[locale]/profile/page.tsx
T
lucianoandClaude Opus 4.7 d467e212cb feat(ui): editorial off-grid restyling — landing + tutte le pagine app
Direzione visiva "asimmetrico/off-grid" applicata in modo coerente:
titoli mega che sbordano dal viewport (bleed-left), chapter markers
giganti in stroke trasparente come art di sfondo, eyebrow verticale
sul bordo destro, mesh-blob fluttuanti, cursor-glow che segue il mouse.

- Landing marketing rifatta da zero: hero cinema con mockup ruotato in
  absolute, marquee infinito di keyword, stats sparse a quote diverse,
  bento di feature ruotate, mockup browser 3D che sborda a destra,
  CTA finale dark con "?" gigante.
- Dashboard loggata: greeting bleed-left, hero card "/plan" ruotata,
  nav asimmetrica con chapter markers per ogni voce.
- /plan, /onboarding, /profile, /tracking, /fasting, /shopping: stesso
  trattamento chrome (titolo bleed, chapter marker, atmosfera mesh-blob),
  contenuti funzionali invariati.

Componenti nuovi:
- CursorGlow (client) — alone radiale che segue il mouse, easing morbido
- EditorialHeader (server) — header riusabile con chapter+eyebrow+title
- EditorialAtmosphere (server) — preset di mesh-blob per pagina

CSS utilities: text-mega, text-chapter, text-stroke, bleed-left/right,
btn-gradient, glow-ring, tilt-3d, animate-marquee, animate-float-{x,y,z},
animate-ring-trace. Tutte rispettano prefers-reduced-motion.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 16:43:47 +02:00

132 lines
4.7 KiB
TypeScript

import { prisma } from '@ketopath/db';
import { redirect } from 'next/navigation';
import { useTranslations } from 'next-intl';
import { setRequestLocale } from 'next-intl/server';
import { CursorGlow } from '@/components/cursor-glow';
import { Masthead } from '@/components/masthead';
import { getServerSession } from '@/lib/auth';
import { fetchAchievements } from './achievements-actions';
import { AchievementsPanel } from './achievements-panel';
import { fetchProfile } from './actions';
import { DataPanel } from './data-panel';
import { fetchNotificationConfig } from './notifications-actions';
import { NotificationsPanel } from './notifications-panel';
import { fetchPreferences } from './preferences-actions';
import { PreferencesSection } from './preferences-section';
import { ProfileForm } from './profile-form';
export default async function ProfilePage({ params: { locale } }: { params: { locale: string } }) {
setRequestLocale(locale);
const session = await getServerSession();
if (!session?.user) redirect('/sign-in');
const user = await prisma.user.findUnique({
where: { id: session.user.id },
select: { disclaimerAcceptedAt: true },
});
if (!user?.disclaimerAcceptedAt) redirect('/welcome');
const profile = (await fetchProfile()) as Parameters<typeof ProfileForm>[0]['initial'];
const notifications = await fetchNotificationConfig();
const preferences = await fetchPreferences();
const achievements = await fetchAchievements();
return (
<ProfilePageContent
initial={profile}
notifications={notifications}
preferences={preferences}
achievements={achievements}
/>
);
}
function ProfilePageContent({
initial,
notifications,
preferences,
achievements,
}: {
initial: Parameters<typeof ProfileForm>[0]['initial'];
notifications: Awaited<ReturnType<typeof fetchNotificationConfig>>;
preferences: Awaited<ReturnType<typeof fetchPreferences>>;
achievements: Awaited<ReturnType<typeof fetchAchievements>>;
}) {
const t = useTranslations('Profile');
return (
<div className="relative">
<div className="mx-auto min-h-screen max-w-7xl px-6 sm:px-10">
<Masthead issueLabel="N. 04 — Profilo" />
<main className="relative overflow-hidden pb-24 pt-10 sm:pt-12">
<CursorGlow color="hsl(var(--oliva))" size={520} />
<div
aria-hidden
className="mesh-blob mesh-blob--oliva animate-float-y -right-32 top-12 h-[28rem] w-[28rem] opacity-50"
/>
<div
aria-hidden
className="mesh-blob mesh-blob--oro animate-float-x -left-24 top-72 h-72 w-72 opacity-35"
/>
<span
aria-hidden
className="font-display text-stroke-thin text-chapter pointer-events-none absolute -right-4 -top-12 select-none italic"
>
IV
</span>
<div
aria-hidden
className="pointer-events-none absolute right-0 top-32 hidden md:block"
style={{ writingMode: 'vertical-rl' }}
>
<p className="text-ink-soft font-mono text-xs uppercase tracking-[0.4em]">
Capitolo IV — Profilo
</p>
</div>
<h1 className="font-display text-mega bleed-left animate-fade-up relative mt-8 font-medium [animation-delay:120ms]">
<span className="text-ink block">
{t('title')}
<span className="text-pomodoro">.</span>
</span>
</h1>
<p className="font-display text-ink-soft animate-fade-up relative mt-10 max-w-2xl text-2xl italic leading-snug [animation-delay:240ms] md:ml-[16.66%]">
{t('subtitle')}
</p>
<div className="rule animate-rule-in my-12 [animation-delay:360ms]" />
<div className="animate-fade-up [animation-delay:300ms]">
<ProfileForm initial={initial} />
</div>
<div className="rule animate-rule-in my-16 [animation-delay:420ms]" />
<div className="animate-fade-up [animation-delay:480ms]">
<PreferencesSection initial={preferences} />
</div>
<div className="rule animate-rule-in my-16 [animation-delay:600ms]" />
<div className="animate-fade-up [animation-delay:660ms]">
<NotificationsPanel initial={notifications} />
</div>
<div className="rule animate-rule-in my-16 [animation-delay:780ms]" />
<div className="animate-fade-up [animation-delay:840ms]">
<AchievementsPanel unlocked={achievements} />
</div>
<div className="rule animate-rule-in my-16 [animation-delay:900ms]" />
<div className="animate-fade-up [animation-delay:960ms]">
<DataPanel />
</div>
</main>
</div>
</div>
);
}