Redesign home, login, register pages with magic-hotel dark aesthetic
Local Build and Deploy / deploy (push) Successful in 51s

This commit is contained in:
openhands committed 2026-07-16 20:06:16 +02:00
1 parent d4ee1dc4a3
commit 9889131b03
7 files changed
+611 -430

No files matched your search

+45 -41
View File
@@ -1,8 +1,7 @@
import { KeyRound, Lock } from "lucide-react";
import { KeyRound, Mail } from "lucide-react";
import Link from "next/link";
import { getTranslations } from "next-intl/server";
import { requestReset } from "@/actions/password-reset";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
export default async function ForgotPage({
searchParams,
@@ -13,63 +12,68 @@ export default async function ForgotPage({
const { sent } = await searchParams;
return (
<div className="flex min-h-[60vh] items-center justify-center px-4 py-12">
<div className="flex min-h-[70vh] items-center justify-center px-4 py-12">
<div className="w-full max-w-md animate-fade-in">
<div className="text-center mb-8">
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-gradient-to-br from-[var(--color-primary)]/20 to-[var(--color-primary)]/5 shadow-lg">
<KeyRound className="h-8 w-8 text-[var(--color-primary)]" />
</div>
<h1 className="text-2xl font-extrabold tracking-tight m-0">
{t("title")}
</h1>
<p className="mt-1.5 text-sm text-[var(--color-text-muted-readable)]">
{t("subtitle")}
</p>
</div>
<div className="relative overflow-hidden rounded-3xl border border-white/[0.06] bg-[#09090b] shadow-2xl p-8">
<div className="absolute inset-0 bg-gradient-to-br from-[#5AB4C6]/5 via-transparent to-transparent" />
<div className="absolute -top-40 -right-40 w-[400px] h-[400px] bg-[#5AB4C6]/5 rounded-full blur-[120px]" />
<div className="relative">
<div className="text-center mb-8">
<div className="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-2xl bg-[#5AB4C6]/10 border border-[#5AB4C6]/20">
<KeyRound className="h-6 w-6 text-[#5AB4C6]" />
</div>
<h1 className="text-2xl font-black text-white tracking-tight m-0">
{t("title")}
</h1>
<p className="mt-1.5 text-sm text-white/40">
{t("subtitle")}
</p>
</div>
<Card className="border-[var(--border-subtle)] shadow-elevated">
<CardHeader className="px-6 pt-6 pb-0">
<CardTitle className="text-base font-bold">
{sent ? "Email sent" : "Reset your password"}
</CardTitle>
<CardDescription>
{sent ? t("sentNotice") : "Enter your email to receive a reset link"}
</CardDescription>
</CardHeader>
<CardContent className="p-6">
{sent ? (
<div className="rounded-xl bg-green-50 dark:bg-green-950/30 border border-green-200 dark:border-green-900 px-4 py-6 text-center">
<p className="text-sm font-medium text-green-600 dark:text-green-400 m-0">
<div className="rounded-xl bg-green-500/10 border border-green-500/20 px-4 py-6 text-center">
<p className="text-sm font-bold text-green-400 m-0">
{t("sentNotice")}
</p>
</div>
) : (
<form action={requestReset} className="space-y-4">
<div className="relative">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-[var(--color-text-muted)]" />
<input
name="email"
type="email"
placeholder={t("emailPlaceholder")}
autoComplete="email"
required
className="w-full rounded-xl border-2 border-[var(--input-border-color)] bg-[var(--color-background)] pl-10 pr-4 py-2.5 text-sm transition-all duration-200 focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/20"
/>
<div>
<label className="block text-gray-500 text-[10px] font-black uppercase tracking-widest mb-1.5 ml-1">
Email
</label>
<div className="relative group">
<div className="absolute left-3.5 top-1/2 -translate-y-1/2 text-gray-600 group-focus-within:text-[#5AB4C6] transition-colors">
<Mail size={15} />
</div>
<input
name="email"
type="email"
placeholder={t("emailPlaceholder")}
autoComplete="email"
required
className="w-full pl-10 pr-4 py-3 bg-white/5 border border-white/10 rounded-lg focus:outline-none focus:border-[#5AB4C6]/50 focus:bg-[#5AB4C6]/5 text-sm text-white placeholder-gray-500 transition-all font-medium"
/>
</div>
</div>
<button type="submit" className="btn btn-primary w-full justify-center py-3 text-sm font-bold rounded-xl">
<button
type="submit"
className="w-full flex items-center justify-center gap-2 bg-[#5AB4C6] hover:bg-[#5AB4C6]/90 text-white font-black text-xs py-3.5 rounded-lg transition-all active:scale-[0.98] uppercase tracking-widest shadow-lg shadow-[#5AB4C6]/20"
>
{t("sendResetLink")}
</button>
</form>
)}
<div className="mt-6 text-center">
<p className="text-sm text-[var(--color-text-muted-readable)]">
<Link href="/login" className="font-bold text-[var(--color-primary)] hover:underline">
<p className="text-sm text-white/40">
<Link href="/login" className="font-bold text-[#5AB4C6] hover:underline">
{t("backToLogin")}
</Link>
</p>
</div>
</CardContent>
</Card>
</div>
</div>
</div>
</div>
);
+152 -128
View File
@@ -1,13 +1,11 @@
"use client";
import { KeyRound, Lock, Mail } from "lucide-react";
import { Eye, EyeOff, Lock, Mail, User } from "lucide-react";
import Link from "next/link";
import { signIn } from "next-auth/react";
import { useTranslations } from "next-intl";
import { type FormEvent, useState } from "react";
import { precheckLogin } from "@/actions/auth-precheck";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
export default function LoginPage() {
const t = useTranslations("pages.login");
@@ -17,6 +15,7 @@ export default function LoginPage() {
const [needs2fa, setNeeds2fa] = useState(false);
const [error, setError] = useState<string | null>(null);
const [pending, setPending] = useState(false);
const [showPassword, setShowPassword] = useState(false);
async function onSubmit(e: FormEvent) {
e.preventDefault();
@@ -55,79 +54,107 @@ export default function LoginPage() {
return (
<div className="flex min-h-[70vh] items-center justify-center px-4 py-12">
<div className="w-full max-w-md animate-fade-in">
<div className="text-center mb-8">
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-gradient-to-br from-[var(--color-primary)]/20 to-[var(--color-primary)]/5 shadow-lg">
<Lock className="h-8 w-8 text-[var(--color-primary)]" />
</div>
<h1 className="text-2xl font-extrabold tracking-tight m-0">
{t("title")}
</h1>
<p className="mt-1.5 text-sm text-[var(--color-text-muted-readable)]">
{needs2fa ? t("subtitle2fa") : t("subtitle")}
</p>
</div>
<div
className="relative overflow-hidden rounded-3xl border border-white/[0.06] bg-[#09090b] shadow-2xl p-8"
>
<div className="absolute inset-0 bg-gradient-to-br from-[#5AB4C6]/5 via-transparent to-transparent" />
<div className="absolute -top-40 -right-40 w-[400px] h-[400px] bg-[#5AB4C6]/5 rounded-full blur-[120px]" />
<Card className="border-[var(--border-subtle)] shadow-elevated">
<CardHeader className="px-6 pt-6 pb-0">
<CardTitle className="text-base font-bold">
{needs2fa ? t("subtitle2fa") : t("subtitle")}
</CardTitle>
<CardDescription>
{needs2fa ? "Enter your authentication code" : "Sign in to your account"}
</CardDescription>
</CardHeader>
<CardContent className="p-6">
<form onSubmit={onSubmit} className="space-y-4">
<div className="space-y-3">
<div className="relative">
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-[var(--color-text-muted)]" />
<input
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder={t("usernamePlaceholder")}
autoComplete="username"
disabled={needs2fa}
className="w-full rounded-xl border-2 border-[var(--input-border-color)] bg-[var(--color-background)] px-10 py-2.5 text-sm transition-all duration-200 focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/20"
/>
</div>
<div className="relative">
<KeyRound className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-[var(--color-text-muted)]" />
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder={t("passwordPlaceholder")}
autoComplete="current-password"
disabled={needs2fa}
className="w-full rounded-xl border-2 border-[var(--input-border-color)] bg-[var(--color-background)] px-10 py-2.5 text-sm transition-all duration-200 focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/20"
/>
</div>
{needs2fa ? (
<input
value={code}
onChange={(e) => setCode(e.target.value)}
placeholder={t("codePlaceholder")}
inputMode="numeric"
autoComplete="one-time-code"
autoFocus
className="w-full rounded-xl border-2 border-[var(--input-border-color)] bg-[var(--color-background)] px-4 py-2.5 text-sm transition-all duration-200 focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/20"
/>
) : null}
<div className="relative">
{/* Header */}
<div className="text-center mb-8">
<div className="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-2xl bg-[#5AB4C6]/10 border border-[#5AB4C6]/20">
<Lock className="h-6 w-6 text-[#5AB4C6]" />
</div>
<h1 className="text-2xl font-black text-white tracking-tight m-0">
{t("title")}
</h1>
<p className="mt-1.5 text-sm text-white/40">
{needs2fa ? t("subtitle2fa") : t("subtitle")}
</p>
</div>
{error ? (
<div className="rounded-xl bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-900 px-4 py-3">
<p className="text-sm font-medium text-red-600 dark:text-red-400 text-center m-0">
<form onSubmit={onSubmit} className="space-y-4">
{error && (
<div className="rounded-xl bg-red-500/10 border border-red-500/20 px-4 py-3">
<p className="text-xs font-bold text-red-400 text-center m-0">
{error}
</p>
</div>
) : null}
)}
<Button
<div className="space-y-3.5">
<div>
<label className="block text-gray-500 text-[10px] font-black uppercase tracking-widest mb-1.5 ml-1">
Username
</label>
<div className="relative group">
<div className="absolute left-3.5 top-1/2 -translate-y-1/2 text-gray-600 group-focus-within:text-[#5AB4C6] transition-colors">
<User size={15} />
</div>
<input
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder={t("usernamePlaceholder")}
autoComplete="username"
disabled={needs2fa}
className="w-full pl-10 pr-4 py-3 bg-white/5 border border-white/10 rounded-lg focus:outline-none focus:border-[#5AB4C6]/50 focus:bg-[#5AB4C6]/5 text-sm text-white placeholder-gray-500 transition-all font-medium"
/>
</div>
</div>
<div>
<label className="block text-gray-500 text-[10px] font-black uppercase tracking-widest mb-1.5 ml-1">
Password
</label>
<div className="relative group">
<div className="absolute left-3.5 top-1/2 -translate-y-1/2 text-gray-600 group-focus-within:text-[#5AB4C6] transition-colors">
<Lock size={15} />
</div>
<input
type={showPassword ? "text" : "password"}
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder={t("passwordPlaceholder")}
autoComplete="current-password"
disabled={needs2fa}
className="w-full pl-10 pr-10 py-3 bg-white/5 border border-white/10 rounded-lg focus:outline-none focus:border-[#5AB4C6]/50 focus:bg-[#5AB4C6]/5 text-sm text-white placeholder-gray-500 transition-all font-medium"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3.5 top-1/2 -translate-y-1/2 text-gray-500 hover:text-white transition-colors"
>
{showPassword ? <EyeOff size={15} /> : <Eye size={15} />}
</button>
</div>
</div>
{needs2fa && (
<div>
<label className="block text-gray-500 text-[10px] font-black uppercase tracking-widest mb-1.5 ml-1">
2FA Code
</label>
<div className="relative group">
<div className="absolute left-3.5 top-1/2 -translate-y-1/2 text-gray-600 group-focus-within:text-[#5AB4C6] transition-colors">
<Mail size={15} />
</div>
<input
value={code}
onChange={(e) => setCode(e.target.value)}
placeholder={t("codePlaceholder")}
inputMode="numeric"
autoComplete="one-time-code"
autoFocus
className="w-full pl-10 pr-4 py-3 bg-white/5 border border-white/10 rounded-lg focus:outline-none focus:border-[#5AB4C6]/50 focus:bg-[#5AB4C6]/5 text-sm text-white placeholder-gray-500 transition-all font-medium"
/>
</div>
</div>
)}
</div>
<button
type="submit"
disabled={pending}
size="lg"
className="w-full font-bold text-base h-12 rounded-xl"
className="w-full flex items-center justify-center gap-2 bg-[#5AB4C6] hover:bg-[#5AB4C6]/90 text-white font-black text-xs py-3.5 rounded-lg transition-all disabled:opacity-50 active:scale-[0.98] uppercase tracking-widest shadow-lg shadow-[#5AB4C6]/20"
>
{pending ? (
<span className="flex items-center justify-center gap-2">
@@ -138,68 +165,65 @@ export default function LoginPage() {
{t("pleaseWait")}
</span>
) : needs2fa ? t("verify") : t("signIn")}
</Button>
</button>
{/* Divider */}
{!needs2fa && (
<>
<div className="relative my-4">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-white/[0.06]" />
</div>
<div className="relative flex justify-center text-[10px] uppercase tracking-widest font-black">
<span className="bg-[#09090b] px-3 text-white/30">
{t("or")}
</span>
</div>
</div>
<div className="grid gap-2">
<button
type="button"
onClick={() => signIn("discord", { callbackUrl: "/" })}
className="w-full flex items-center justify-center gap-2 bg-white/5 hover:bg-white/10 text-white font-black text-xs py-3 rounded-lg transition-all active:scale-[0.98] uppercase tracking-widest border border-white/10"
>
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
<path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189z" />
</svg>
{t("continueWithDiscord")}
</button>
<button
type="button"
onClick={() => signIn("google", { callbackUrl: "/" })}
className="w-full flex items-center justify-center gap-2 bg-white/5 hover:bg-white/10 text-white font-black text-xs py-3 rounded-lg transition-all active:scale-[0.98] uppercase tracking-widest border border-white/10"
>
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
<path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 01-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z" fill="#4285F4" />
<path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853" />
<path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05" />
<path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335" />
</svg>
{t("continueWithGoogle")}
</button>
</div>
</>
)}
<div className="pt-2 text-center">
<p className="text-sm text-white/40">
{t("noAccount")}{" "}
<Link href="/register" className="font-bold text-[#5AB4C6] hover:underline">
{t("createOne")}
</Link>
<span className="mx-2 text-white/10">·</span>
<Link href="/forgot" className="font-bold text-[#5AB4C6] hover:underline">
{t("forgotPassword")}
</Link>
</p>
</div>
</form>
{!needs2fa ? (
<>
<div className="relative my-6">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-[var(--border-subtle)]" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-[var(--color-card)] px-3 text-[var(--color-text-muted-readable)] font-bold">
{t("or")}
</span>
</div>
</div>
<div className="grid gap-3">
<Button
type="button"
variant="outline"
size="lg"
className="w-full font-semibold h-11 rounded-xl"
onClick={() => signIn("discord", { callbackUrl: "/" })}
>
<svg className="h-5 w-5" viewBox="0 0 24 24" fill="currentColor">
<path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189z" />
</svg>
{t("continueWithDiscord")}
</Button>
<Button
type="button"
variant="outline"
size="lg"
className="w-full font-semibold h-11 rounded-xl"
onClick={() => signIn("google", { callbackUrl: "/" })}
>
<svg className="h-5 w-5" viewBox="0 0 24 24" fill="currentColor">
<path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 01-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z" fill="#4285F4" />
<path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853" />
<path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05" />
<path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335" />
</svg>
{t("continueWithGoogle")}
</Button>
</div>
</>
) : null}
<div className="mt-6 text-center">
<p className="text-sm text-[var(--color-text-muted-readable)]">
{t("noAccount")}{" "}
<Link href="/register" className="font-bold text-[var(--color-primary)] hover:underline">
{t("createOne")}
</Link>
<span className="mx-2 text-[var(--border-subtle)]">·</span>
<Link href="/forgot" className="font-bold text-[var(--color-primary)] hover:underline">
{t("forgotPassword")}
</Link>
</p>
</div>
</CardContent>
</Card>
</div>
</div>
</div>
</div>
);
+1 -1
View File
@@ -20,7 +20,7 @@ export default async function RegisterPage({
return (
<main className="flex min-h-[calc(100vh-200px)] items-center justify-center px-4 py-8">
<div className="w-full">
<div className="w-full max-w-lg">
<h1 className="sr-only">{t("title")}</h1>
<RegisterForm
hotelName={hotelName ?? "Atom"}
+85 -66
View File
@@ -1,5 +1,7 @@
"use client";
import { LogIn, UserPlus } from "lucide-react";
import Link from "next/link";
import { signIn } from "next-auth/react";
import { type FormEvent, useState } from "react";
import { precheckLogin } from "@/actions/auth-precheck";
@@ -47,86 +49,103 @@ export function HomeLoginForm() {
}
return (
<form onSubmit={onSubmit} className="flex flex-col gap-4">
<div className="space-y-3">
<form onSubmit={onSubmit} className="space-y-4">
{error && (
<div className="rounded-lg bg-red-500/10 border border-red-500/20 px-4 py-2.5">
<p className="text-xs font-bold text-red-400 text-center m-0">
{error}
</p>
</div>
)}
<div className="space-y-3.5">
<div>
<label className="block text-xs font-bold uppercase tracking-wider text-[var(--color-text-muted-readable)] mb-1.5">
<label className="block text-gray-500 text-[10px] font-black uppercase tracking-widest mb-1.5 ml-1">
Username
</label>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="Enter your username"
className="w-full rounded-lg border-2 px-3 py-2.5 text-sm transition-all duration-200 bg-[var(--color-background)]"
style={{ borderColor: "var(--input-border-color)" }}
required
disabled={needs2fa}
/>
</div>
<div>
<label className="block text-xs font-bold uppercase tracking-wider text-[var(--color-text-muted-readable)] mb-1.5">
Password
</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Enter your password"
className="w-full rounded-lg border-2 px-3 py-2.5 text-sm transition-all duration-200 bg-[var(--color-background)]"
style={{ borderColor: "var(--input-border-color)" }}
required
disabled={needs2fa}
/>
</div>
{needs2fa ? (
<div>
<label className="block text-xs font-bold uppercase tracking-wider text-[var(--color-text-muted-readable)] mb-1.5">
2FA Code
</label>
<div className="relative group">
<div className="absolute left-3.5 top-1/2 -translate-y-1/2 text-gray-600 group-focus-within:text-[#5AB4C6] transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" />
<circle cx="12" cy="7" r="4" />
</svg>
</div>
<input
type="text"
value={code}
onChange={(e) => setCode(e.target.value)}
placeholder="Enter your 2FA code"
inputMode="numeric"
className="w-full rounded-lg border-2 px-3 py-2.5 text-sm transition-all duration-200 bg-[var(--color-background)]"
style={{ borderColor: "var(--input-border-color)" }}
autoFocus
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="Username"
className="w-full pl-10 pr-4 py-3 bg-white/5 border border-white/10 rounded-lg focus:outline-none focus:border-[#5AB4C6]/50 focus:bg-[#5AB4C6]/5 text-sm text-white placeholder-gray-500 transition-all font-medium"
required
disabled={needs2fa}
/>
</div>
) : null}
</div>
<div>
<label className="block text-gray-500 text-[10px] font-black uppercase tracking-widest mb-1.5 ml-1">
Password
</label>
<div className="relative group">
<div className="absolute left-3.5 top-1/2 -translate-y-1/2 text-gray-600 group-focus-within:text-[#5AB4C6] transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect width="18" height="11" x="3" y="11" rx="2" ry="2" />
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
</svg>
</div>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
className="w-full pl-10 pr-4 py-3 bg-white/5 border border-white/10 rounded-lg focus:outline-none focus:border-[#5AB4C6]/50 focus:bg-[#5AB4C6]/5 text-sm text-white placeholder-gray-500 transition-all font-medium"
required
disabled={needs2fa}
/>
</div>
</div>
{needs2fa && (
<div>
<label className="block text-gray-500 text-[10px] font-black uppercase tracking-widest mb-1.5 ml-1">
2FA Code
</label>
<div className="relative group">
<div className="absolute left-3.5 top-1/2 -translate-y-1/2 text-gray-600 group-focus-within:text-[#5AB4C6] transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect width="18" height="11" x="3" y="11" rx="2" ry="2" />
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
</svg>
</div>
<input
type="text"
value={code}
onChange={(e) => setCode(e.target.value)}
placeholder="Enter your 2FA code"
inputMode="numeric"
autoFocus
className="w-full pl-10 pr-4 py-3 bg-white/5 border border-white/10 rounded-lg focus:outline-none focus:border-[#5AB4C6]/50 focus:bg-[#5AB4C6]/5 text-sm text-white placeholder-gray-500 transition-all font-medium"
/>
</div>
</div>
)}
</div>
{error ? (
<p className="m-0 text-center text-sm font-medium text-red-500 dark:text-red-400">
{error}
</p>
) : null}
<button
type="submit"
disabled={pending}
className="w-full cursor-pointer rounded-xl px-4 py-3 text-sm font-bold tracking-wide text-white transition-all duration-200 hover:opacity-90 active:scale-[0.98] disabled:opacity-50"
style={{
background: "linear-gradient(135deg, var(--button-color), color-mix(in srgb, var(--button-color) 80%, #000000))",
boxShadow: "0 4px 16px color-mix(in srgb, var(--button-color) 30%, transparent)",
}}
className="w-full group relative flex items-center justify-center gap-2 bg-[#5AB4C6] hover:bg-[#5AB4C6]/90 text-white font-black text-xs py-3.5 rounded-lg transition-all disabled:opacity-50 active:scale-[0.98] uppercase tracking-widest"
>
{pending ? (
<span className="flex items-center justify-center gap-2">
<svg className="animate-spin h-4 w-4" viewBox="0 0 24 24" fill="none">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
Please wait...
</span>
) : needs2fa ? (
"Verify"
) : (
"Login"
)}
<LogIn size={14} />
{pending ? "Please wait..." : needs2fa ? "Verify" : "Login"}
</button>
<div className="pt-4 border-t border-white/5 text-center">
<Link href="/register">
<div className="flex items-center justify-center gap-2 w-full py-3 rounded-lg bg-white/5 border border-white/10 text-white text-[11px] font-black hover:bg-white/10 transition-all active:scale-95 uppercase tracking-wider">
<UserPlus size={14} className="text-[#5AB4C6]" />
Create account
</div>
</Link>
</div>
</form>
);
}
+118 -97
View File
@@ -1,13 +1,10 @@
"use client";
import { Eye, EyeOff, Lock, Mail, User, UserPlus } from "lucide-react";
import Image from "next/image";
import { Eye, EyeOff, Lock, Mail, User } from "lucide-react";
import Link from "next/link";
import { useTranslations } from "next-intl";
import { useActionState, useState } from "react";
import { register } from "@/actions/register";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
interface RegisterFormProps {
hotelName: string;
@@ -23,100 +20,130 @@ export function RegisterForm({ hotelName, captcha, error }: RegisterFormProps) {
const [showPassword, setShowPassword] = useState(false);
return (
<div className="mx-auto w-full max-w-lg animate-fade-in">
<div className="text-center mb-8">
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-gradient-to-br from-[var(--color-primary)]/20 to-[var(--color-primary)]/5 shadow-lg">
<UserPlus className="h-8 w-8 text-[var(--color-primary)]" />
</div>
<h1 className="text-2xl font-extrabold tracking-tight m-0">
{t("title")}
</h1>
<p className="mt-1.5 text-sm text-[var(--color-text-muted-readable)]">
{t("subtitle", { hotel: hotelName })}
</p>
</div>
<div className="animate-fade-in">
<div
className="relative overflow-hidden rounded-3xl border border-white/[0.06] bg-[#09090b] shadow-2xl p-8 md:p-10"
>
<div className="absolute inset-0 bg-gradient-to-br from-[#5AB4C6]/5 via-transparent to-transparent" />
<div className="absolute -top-40 -right-40 w-[500px] h-[500px] bg-[#5AB4C6]/5 rounded-full blur-[120px]" />
<Card className="border-[var(--border-subtle)] shadow-elevated">
<CardHeader className="px-6 pt-6 pb-0">
<CardTitle className="text-base font-bold">
Create your account
</CardTitle>
<CardDescription>
Fill in the details below to join {hotelName}
</CardDescription>
</CardHeader>
<CardContent className="p-6">
<form action={formAction} className="space-y-5">
<div className="relative">
{/* Header */}
<div className="text-center mb-8">
<div className="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-2xl bg-[#5AB4C6]/10 border border-[#5AB4C6]/20">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#5AB4C6" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4" />
<polyline points="10 17 15 12 10 7" />
<line x1="15" x2="3" y1="12" y2="12" />
</svg>
</div>
<h1 className="text-2xl font-black text-white tracking-tight m-0">
{t("title")}
</h1>
<p className="mt-1.5 text-sm text-white/40">
{t("subtitle", { hotel: hotelName })}
</p>
</div>
<form action={formAction} className="space-y-5 max-w-md mx-auto">
{(error || serverError) && (
<div className="rounded-xl bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-900 px-4 py-3">
<p className="text-sm font-medium text-red-600 dark:text-red-400 text-center m-0">
<div className="rounded-xl bg-red-500/10 border border-red-500/20 px-4 py-3">
<p className="text-xs font-bold text-red-400 text-center m-0">
{error || serverError}
</p>
</div>
)}
<div className="grid gap-4 sm:grid-cols-2">
<div className="relative">
<User className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-[var(--color-text-muted)]" />
<input
id="username"
name="username"
type="text"
placeholder={t("usernamePlaceholder")}
autoComplete="username"
className="w-full rounded-xl border-2 border-[var(--input-border-color)] bg-[var(--color-background)] pl-10 pr-4 py-2.5 text-sm transition-all duration-200 focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/20"
required
/>
<div>
<label className="block text-gray-500 text-[10px] font-black uppercase tracking-widest mb-1.5 ml-1">
Username
</label>
<div className="relative group">
<div className="absolute left-3.5 top-1/2 -translate-y-1/2 text-gray-600 group-focus-within:text-[#5AB4C6] transition-colors">
<User size={15} />
</div>
<input
id="username"
name="username"
type="text"
placeholder={t("usernamePlaceholder")}
autoComplete="username"
className="w-full pl-10 pr-4 py-3 bg-white/5 border border-white/10 rounded-lg focus:outline-none focus:border-[#5AB4C6]/50 focus:bg-[#5AB4C6]/5 text-sm text-white placeholder-gray-500 transition-all font-medium"
required
/>
</div>
</div>
<div className="relative">
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-[var(--color-text-muted)]" />
<input
id="mail"
name="mail"
type="email"
placeholder={t("emailPlaceholder")}
autoComplete="email"
className="w-full rounded-xl border-2 border-[var(--input-border-color)] bg-[var(--color-background)] pl-10 pr-4 py-2.5 text-sm transition-all duration-200 focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/20"
/>
<div>
<label className="block text-gray-500 text-[10px] font-black uppercase tracking-widest mb-1.5 ml-1">
Email
</label>
<div className="relative group">
<div className="absolute left-3.5 top-1/2 -translate-y-1/2 text-gray-600 group-focus-within:text-[#5AB4C6] transition-colors">
<Mail size={15} />
</div>
<input
id="mail"
name="mail"
type="email"
placeholder={t("emailPlaceholder")}
autoComplete="email"
className="w-full pl-10 pr-4 py-3 bg-white/5 border border-white/10 rounded-lg focus:outline-none focus:border-[#5AB4C6]/50 focus:bg-[#5AB4C6]/5 text-sm text-white placeholder-gray-500 transition-all font-medium"
/>
</div>
</div>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<div className="relative">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-[var(--color-text-muted)]" />
<input
id="password"
name="password"
type={showPassword ? "text" : "password"}
placeholder={t("passwordPlaceholder")}
autoComplete="new-password"
className="w-full rounded-xl border-2 border-[var(--input-border-color)] bg-[var(--color-background)] pl-10 pr-10 py-2.5 text-sm transition-all duration-200 focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/20"
required
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-[var(--color-text-muted)] hover:text-[var(--color-text-readable)]"
>
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
<div>
<label className="block text-gray-500 text-[10px] font-black uppercase tracking-widest mb-1.5 ml-1">
Password
</label>
<div className="relative group">
<div className="absolute left-3.5 top-1/2 -translate-y-1/2 text-gray-600 group-focus-within:text-[#5AB4C6] transition-colors">
<Lock size={15} />
</div>
<input
id="password"
name="password"
type={showPassword ? "text" : "password"}
placeholder={t("passwordPlaceholder")}
autoComplete="new-password"
className="w-full pl-10 pr-10 py-3 bg-white/5 border border-white/10 rounded-lg focus:outline-none focus:border-[#5AB4C6]/50 focus:bg-[#5AB4C6]/5 text-sm text-white placeholder-gray-500 transition-all font-medium"
required
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3.5 top-1/2 -translate-y-1/2 text-gray-500 hover:text-white transition-colors"
>
{showPassword ? <EyeOff size={15} /> : <Eye size={15} />}
</button>
</div>
</div>
<div className="relative">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-[var(--color-text-muted)]" />
<input
id="password_confirmation"
name="password_confirmation"
type="password"
placeholder={t("confirmPasswordPlaceholder")}
autoComplete="new-password"
className="w-full rounded-xl border-2 border-[var(--input-border-color)] bg-[var(--color-background)] pl-10 pr-4 py-2.5 text-sm transition-all duration-200 focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/20"
required
/>
<div>
<label className="block text-gray-500 text-[10px] font-black uppercase tracking-widest mb-1.5 ml-1">
Confirm
</label>
<div className="relative group">
<div className="absolute left-3.5 top-1/2 -translate-y-1/2 text-gray-600 group-focus-within:text-[#5AB4C6] transition-colors">
<Lock size={15} />
</div>
<input
id="password_confirmation"
name="password_confirmation"
type="password"
placeholder={t("confirmPasswordPlaceholder")}
autoComplete="new-password"
className="w-full pl-10 pr-4 py-3 bg-white/5 border border-white/10 rounded-lg focus:outline-none focus:border-[#5AB4C6]/50 focus:bg-[#5AB4C6]/5 text-sm text-white placeholder-gray-500 transition-all font-medium"
required
/>
</div>
</div>
</div>
{/* Terms */}
<div className="flex items-start gap-3 rounded-xl border border-[var(--border-subtle)] bg-[var(--color-background)] p-4">
<div className="flex items-start gap-3 rounded-xl border border-white/10 bg-white/[0.03] p-4">
<button
type="button"
role="checkbox"
@@ -124,12 +151,8 @@ export function RegisterForm({ hotelName, captcha, error }: RegisterFormProps) {
onClick={() => setTermsAccepted(!termsAccepted)}
className="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-md border-2 transition-all"
style={{
borderColor: termsAccepted
? "var(--color-primary)"
: "var(--input-border-color)",
backgroundColor: termsAccepted
? "var(--color-primary)"
: "transparent",
borderColor: termsAccepted ? "#5AB4C6" : "rgba(255,255,255,0.15)",
backgroundColor: termsAccepted ? "#5AB4C6" : "transparent",
}}
>
{termsAccepted && (
@@ -140,8 +163,7 @@ export function RegisterForm({ hotelName, captcha, error }: RegisterFormProps) {
</button>
<input type="hidden" name="terms" value={termsAccepted ? "1" : ""} />
<label
className="text-sm cursor-pointer select-none"
style={{ color: "var(--color-text-readable)" }}
className="text-sm cursor-pointer select-none text-white/60"
onClick={() => setTermsAccepted(!termsAccepted)}
>
{t("termsAccept", { hotel: hotelName })}
@@ -160,11 +182,10 @@ export function RegisterForm({ hotelName, captcha, error }: RegisterFormProps) {
</div>
)}
<Button
<button
type="submit"
disabled={isPending}
size="lg"
className="w-full font-bold text-base h-12 rounded-xl"
className="w-full flex items-center justify-center gap-2 bg-[#5AB4C6] hover:bg-[#5AB4C6]/90 text-white font-black text-xs py-3.5 rounded-lg transition-all disabled:opacity-50 active:scale-[0.98] uppercase tracking-widest shadow-lg shadow-[#5AB4C6]/20"
>
{isPending ? (
<span className="flex items-center justify-center gap-2">
@@ -175,19 +196,19 @@ export function RegisterForm({ hotelName, captcha, error }: RegisterFormProps) {
{t("creatingAccount")}
</span>
) : t("createAccount")}
</Button>
</button>
<div className="text-center">
<p className="text-sm text-[var(--color-text-muted-readable)]">
<div className="text-center pt-2">
<p className="text-sm text-white/40">
{t("alreadyHaveAccount")}{" "}
<Link href="/login" className="font-bold text-[var(--color-primary)] hover:underline">
<Link href="/login" className="font-bold text-[#5AB4C6] hover:underline">
{t("signIn")}
</Link>
</p>
</div>
</form>
</CardContent>
</Card>
</div>
</div>
</div>
);
}
+146 -62
View File
@@ -1,9 +1,8 @@
import { ArrowRight, Lock, Newspaper, Sparkles, Users } from "lucide-react";
import { ArrowRight, Newspaper, Users } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { getTranslations } from "next-intl/server";
import { HomeLoginForm } from "@/components/auth/home-login-form";
import { Button } from "@/components/ui/button";
import { avatarImageUrl, excerpt } from "@/lib/format";
import { prisma } from "@/lib/prisma";
import { siteSettings } from "@/lib/services/site-settings";
@@ -41,71 +40,131 @@ export default async function GuestView() {
} catch {}
return (
<div className="flex flex-col gap-8">
{/* Hero Banner */}
<div className="animate-fade-in relative overflow-hidden rounded-2xl border border-[var(--border-subtle)] bg-gradient-to-br from-[var(--color-primary)]/10 via-[var(--color-surface)] to-[var(--color-accent)]/5 shadow-elevated">
<div className="absolute -top-20 -right-20 h-64 w-64 rounded-full opacity-[0.08] blur-[100px]" style={{ background: "var(--color-primary)" }} />
<div className="absolute -bottom-20 -left-20 h-48 w-48 rounded-full opacity-[0.06] blur-[80px]" style={{ background: "var(--color-accent)" }} />
<div className="relative flex flex-col items-center gap-6 px-8 py-10 text-center md:flex-row md:text-left">
<div className="flex-1">
<div className="mb-2 inline-flex items-center gap-1.5 rounded-full border border-[var(--border-subtle)] bg-[var(--color-surface)] px-3 py-1 text-xs font-bold uppercase tracking-wider text-[var(--color-text-muted-readable)]">
<Sparkles className="h-3 w-3 text-[var(--color-primary)]" />
Welcome to {hotelName}
<div className="flex flex-col gap-10">
{/* Hero Section */}
<div className="relative overflow-hidden rounded-3xl bg-[#09090b] border border-white/[0.06] shadow-2xl">
<div className="absolute inset-0 bg-gradient-to-br from-[#5AB4C6]/5 via-transparent to-transparent" />
<div className="absolute top-0 left-1/4 w-[600px] h-[600px] bg-[#5AB4C6]/5 rounded-full blur-[120px]" />
<div className="absolute -bottom-20 -right-20 w-80 h-80 bg-white/[0.03] rounded-full blur-[80px]" />
<div className="relative flex flex-col items-center gap-8 px-6 py-10 md:flex-row md:px-10 md:py-12">
{/* Left: Welcome */}
<div className="flex-1 text-center md:text-left">
<div className="inline-flex items-center gap-2 rounded-full border border-[#5AB4C6]/20 bg-[#5AB4C6]/10 px-4 py-1.5 mb-5">
<span className="relative flex h-2 w-2">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-[#5AB4C6] opacity-80" />
<span className="relative inline-flex h-2 w-2 rounded-full bg-[#5AB4C6]" />
</span>
<span className="text-[10px] font-black uppercase tracking-widest text-[#5AB4C6]">
Online
</span>
</div>
<h1 className="m-0 text-3xl font-extrabold tracking-tight md:text-4xl">
Join the <span className="text-[var(--color-primary)]">Community</span>
<h1 className="m-0 text-3xl font-black tracking-tight text-white md:text-4xl">
Welcome to{" "}
<span className="text-[#5AB4C6]">{hotelName}</span>
</h1>
<p className="mt-2 max-w-md text-sm text-[var(--color-text-muted-readable)]">
Connect with friends, explore the hotel, and create unforgettable memories. Your adventure starts here.
<p className="mt-3 max-w-md text-sm text-white/50 leading-relaxed">
Connect with friends, explore the hotel, and create unforgettable
memories. Your adventure starts here.
</p>
<div className="mt-5 flex flex-wrap gap-3">
<div className="mt-7 flex flex-wrap gap-3">
<Link href="/register">
<Button size="lg" className="font-bold gap-2 rounded-xl px-8 h-12 shadow-glow">
Get Started <ArrowRight className="h-4 w-4" />
</Button>
<div className="inline-flex items-center gap-2 bg-[#5AB4C6] hover:bg-[#5AB4C6]/90 text-white font-black text-xs py-3.5 px-7 rounded-lg transition-all active:scale-[0.98] uppercase tracking-widest shadow-lg shadow-[#5AB4C6]/20">
Get Started <ArrowRight size={14} />
</div>
</Link>
<Link href="/news">
<Button variant="outline" size="lg" className="font-bold rounded-xl px-8 h-12">
<div className="inline-flex items-center gap-2 bg-white/5 hover:bg-white/10 text-white font-black text-xs py-3.5 px-7 rounded-lg transition-all active:scale-[0.98] uppercase tracking-widest border border-white/10">
Latest News
</Button>
</div>
</Link>
</div>
{/* Stats */}
<div className="mt-8 flex gap-6">
<div>
<p className="text-lg font-black text-white m-0">
{articles.length}
</p>
<p className="text-[10px] font-black uppercase tracking-widest text-white/30 m-0 mt-0.5">
Articles
</p>
</div>
<div>
<p className="text-lg font-black text-white m-0">
{recentUsers.length}+
</p>
<p className="text-[10px] font-black uppercase tracking-widest text-white/30 m-0 mt-0.5">
Active Users
</p>
</div>
</div>
</div>
<div className="shrink-0">
<div className="w-full max-w-xs rounded-2xl border border-[var(--border-subtle)] bg-[var(--color-surface)] shadow-elevated md:w-72">
<div className="rounded-t-2xl bg-gradient-to-r from-[var(--color-primary)]/10 to-[var(--color-primary)]/5 px-5 py-3 border-b border-[var(--border-subtle)]">
<h3 className="m-0 text-sm font-bold flex items-center gap-2">
<Lock className="h-4 w-4 text-[var(--color-primary)]" />
{/* Right: Login Form */}
<div className="w-full max-w-sm shrink-0">
<div
className="rounded-2xl border border-white/10 p-6"
style={{
background:
"linear-gradient(135deg, rgba(255,255,255,0.05) 0%, rgba(255,255,255,0.02) 100%)",
backdropFilter: "blur(32px)",
WebkitBackdropFilter: "blur(32px)",
}}
>
<div className="flex items-center gap-2.5 mb-5">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-[#5AB4C6]/10">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="#5AB4C6"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect width="18" height="11" x="3" y="11" rx="2" ry="2" />
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
</svg>
</div>
<span className="text-sm font-black text-white tracking-wide">
Sign In
</h3>
</div>
<div className="p-4">
<HomeLoginForm />
</span>
</div>
<HomeLoginForm />
</div>
</div>
</div>
</div>
{/* Articles + Sidebar */}
<div className="grid gap-6 lg:grid-cols-3">
{/* Latest News */}
<div className="lg:col-span-2 animate-slide-up">
<div className="mb-4 flex items-center justify-between">
<div className="mb-5 flex items-center justify-between">
<div className="flex items-center gap-2.5">
<div className="flex h-9 w-9 items-center justify-center rounded-xl bg-gradient-to-br from-[var(--color-primary)]/20 to-[var(--color-primary)]/5">
<Newspaper className="h-4 w-4 text-[var(--color-primary)]" />
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-[#5AB4C6]/10">
<Newspaper className="h-4 w-4 text-[#5AB4C6]" />
</div>
<h2 className="m-0 text-lg font-extrabold">{t("latestNews")}</h2>
<h2 className="m-0 text-sm font-black text-white uppercase tracking-wider">
{t("latestNews")}
</h2>
</div>
<Link href="/news" className="text-xs font-bold text-[var(--color-primary)] hover:underline flex items-center gap-1">
<Link
href="/news"
className="text-[10px] font-black uppercase tracking-widest text-[#5AB4C6] hover:underline flex items-center gap-1"
>
View all <ArrowRight className="h-3 w-3" />
</Link>
</div>
{articles.length === 0 ? (
<div className="flex flex-col items-center justify-center rounded-2xl border border-dashed border-[var(--border-subtle)] bg-[var(--color-surface)] px-6 py-16 text-center">
<Newspaper className="mb-3 h-10 w-10 text-[var(--color-text-muted)] opacity-40" />
<p className="text-sm text-[var(--color-text-muted-readable)] m-0">{t("noArticles")}</p>
<div className="flex flex-col items-center justify-center rounded-2xl border border-dashed border-white/10 bg-white/[0.03] px-6 py-16 text-center">
<Newspaper className="mb-3 h-10 w-10 text-white/20" />
<p className="text-sm text-white/40 m-0">{t("noArticles")}</p>
</div>
) : (
<div className="grid gap-4 sm:grid-cols-2">
@@ -113,7 +172,7 @@ export default async function GuestView() {
<Link
key={a.slug}
href={`/news/${a.slug}`}
className="group relative overflow-hidden rounded-2xl border border-[var(--border-subtle)] bg-[var(--color-surface)] transition-all duration-300 hover:shadow-card-hover hover:-translate-y-1"
className="group relative overflow-hidden rounded-2xl border border-white/10 bg-white/[0.03] transition-all duration-300 hover:bg-white/[0.06] hover:-translate-y-1"
style={{ animationDelay: `${i * 100}ms` }}
>
<div className="relative aspect-[16/9] overflow-hidden">
@@ -124,13 +183,13 @@ export default async function GuestView() {
className="object-cover transition-all duration-500 group-hover:scale-110"
unoptimized
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/70 via-black/20 to-transparent" />
<div className="absolute inset-0 bg-gradient-to-t from-[#09090b] via-black/20 to-transparent" />
<div className="absolute bottom-0 left-0 right-0 p-4">
<h3 className="m-0 text-base font-bold text-white text-shadow-sm truncate">
<h3 className="m-0 text-base font-bold text-white truncate">
{a.title}
</h3>
{a.shortStory && (
<p className="mt-1 truncate text-xs text-white/80 text-shadow-sm">
<p className="mt-1 truncate text-xs text-white/60">
{excerpt(a.shortStory, 80)}
</p>
)}
@@ -142,7 +201,7 @@ export default async function GuestView() {
<Link
key={a.slug}
href={`/news/${a.slug}`}
className="group flex items-center gap-3 rounded-xl border border-[var(--border-subtle)] bg-[var(--color-surface)] p-3 transition-all duration-200 hover:shadow-card-hover hover:-translate-y-0.5"
className="group flex items-center gap-3 rounded-xl border border-white/10 bg-white/[0.03] p-3 transition-all duration-200 hover:bg-white/[0.06] hover:-translate-y-0.5"
>
<div className="relative h-16 w-16 shrink-0 overflow-hidden rounded-lg">
<Image
@@ -154,14 +213,16 @@ export default async function GuestView() {
/>
</div>
<div className="min-w-0 flex-1">
<h4 className="m-0 text-sm font-bold truncate">{a.title}</h4>
<h4 className="m-0 text-sm font-bold text-white truncate">
{a.title}
</h4>
{a.shortStory && (
<p className="m-0 mt-0.5 truncate text-xs text-[var(--color-text-muted-readable)]">
<p className="m-0 mt-0.5 truncate text-xs text-white/40">
{excerpt(a.shortStory, 60)}
</p>
)}
</div>
<ArrowRight className="h-4 w-4 shrink-0 text-[var(--color-text-muted)] opacity-0 -translate-x-2 transition-all duration-200 group-hover:opacity-100 group-hover:translate-x-0" />
<ArrowRight className="h-4 w-4 shrink-0 text-white/20 opacity-0 -translate-x-2 transition-all duration-200 group-hover:opacity-100 group-hover:translate-x-0" />
</Link>
))}
</div>
@@ -169,13 +230,20 @@ export default async function GuestView() {
</div>
{/* Sidebar */}
<div className="animate-slide-up space-y-6" style={{ animationDelay: "150ms" }}>
<div
className="animate-slide-up space-y-6"
style={{ animationDelay: "150ms" }}
>
{/* Recent Users */}
{recentUsers.length > 0 && (
<div className="rounded-2xl border border-[var(--border-subtle)] bg-[var(--color-surface)] overflow-hidden">
<div className="flex items-center gap-2.5 bg-gradient-to-r from-[var(--color-primary)]/10 to-[var(--color-primary)]/5 px-5 py-3 border-b border-[var(--border-subtle)]">
<Users className="h-4 w-4 text-[var(--color-primary)]" />
<h3 className="m-0 text-sm font-bold">Recent Users</h3>
<div className="rounded-2xl border border-white/10 bg-white/[0.03] overflow-hidden">
<div className="flex items-center gap-2.5 px-5 py-3.5 border-b border-white/[0.06]">
<div className="flex h-7 w-7 items-center justify-center rounded-lg bg-[#5AB4C6]/10">
<Users className="h-3.5 w-3.5 text-[#5AB4C6]" />
</div>
<h3 className="m-0 text-[11px] font-black text-white uppercase tracking-wider">
Recent Users
</h3>
</div>
<div className="grid grid-cols-4 gap-2 p-4">
{recentUsers.map((u) => (
@@ -184,7 +252,7 @@ export default async function GuestView() {
href={`/u/${u.username}`}
className="group relative flex items-center justify-center transition-all duration-200 hover:scale-110"
>
<div className="relative h-14 w-14 overflow-hidden rounded-xl border-2 border-transparent bg-[var(--color-background)] transition-all duration-200 group-hover:border-[var(--color-primary)]/30 group-hover:shadow-lg">
<div className="relative h-14 w-14 overflow-hidden rounded-xl border-2 border-transparent bg-white/5 transition-all duration-200 group-hover:border-[#5AB4C6]/30 group-hover:shadow-lg">
<Image
src={avatarImageUrl(imager, u.look, {
headOnly: true,
@@ -203,7 +271,7 @@ export default async function GuestView() {
unoptimized
/>
</div>
<span className="absolute -bottom-1 left-1/2 -translate-x-1/2 whitespace-nowrap rounded-full bg-[var(--color-surface)] px-2 py-0.5 text-[0.55rem] font-bold text-[var(--color-text-muted)] opacity-0 shadow-lg transition-all duration-200 group-hover:opacity-100">
<span className="absolute -bottom-1 left-1/2 -translate-x-1/2 whitespace-nowrap rounded-full bg-[#09090b] px-2 py-0.5 text-[0.55rem] font-bold text-white/60 opacity-0 shadow-lg transition-all duration-200 group-hover:opacity-100 border border-white/10">
{u.username}
</span>
</Link>
@@ -214,19 +282,35 @@ export default async function GuestView() {
{/* Quick Register CTA */}
<Link href="/register" className="group block">
<div className="relative overflow-hidden rounded-2xl bg-gradient-to-br from-[var(--color-primary)] to-[var(--color-primary)]/80 px-6 py-8 text-center transition-all duration-300 hover:shadow-glow hover:-translate-y-1">
<div className="absolute -top-10 -right-10 h-32 w-32 rounded-full bg-white/10 blur-3xl" />
<div className="absolute -bottom-10 -left-10 h-24 w-24 rounded-full bg-white/10 blur-3xl" />
<div className="relative overflow-hidden rounded-2xl border border-white/10 bg-white/[0.03] px-6 py-8 text-center transition-all duration-300 hover:bg-white/[0.06] hover:-translate-y-1">
<div className="absolute -top-10 -right-10 h-32 w-32 rounded-full bg-[#5AB4C6]/10 blur-3xl" />
<div className="absolute -bottom-10 -left-10 h-24 w-24 rounded-full bg-[#5AB4C6]/10 blur-3xl" />
<div className="relative">
<Sparkles className="mx-auto h-8 w-8 text-white/80" />
<h3 className="mt-3 text-xl font-extrabold text-white">
<div className="mx-auto flex h-10 w-10 items-center justify-center rounded-xl bg-[#5AB4C6]/10">
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="#5AB4C6"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4" />
<polyline points="10 17 15 12 10 7" />
<line x1="15" x2="3" y1="12" y2="12" />
</svg>
</div>
<h3 className="mt-4 text-lg font-black text-white">
Join {hotelName} Today
</h3>
<p className="mt-1 text-sm text-white/80">
<p className="mt-1 text-xs text-white/40">
Create your free account and start your adventure!
</p>
<div className="mt-4 inline-flex items-center gap-2 rounded-xl bg-white/20 px-6 py-2.5 text-sm font-bold text-white backdrop-blur-sm transition-all duration-200 group-hover:bg-white/30">
Register Now <ArrowRight className="h-4 w-4" />
<div className="mt-5 inline-flex items-center gap-2 bg-[#5AB4C6] hover:bg-[#5AB4C6]/90 text-white font-black text-xs py-3 px-6 rounded-lg transition-all active:scale-[0.98] uppercase tracking-widest shadow-lg shadow-[#5AB4C6]/20">
Register Now <ArrowRight className="h-3.5 w-3.5" />
</div>
</div>
</div>
+64 -35
View File
@@ -1,4 +1,4 @@
import { ArrowRight, Gamepad2, Newspaper, Sparkles, Users } from "lucide-react";
import { ArrowRight, Gamepad2, Newspaper, Users } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { getTranslations } from "next-intl/server";
@@ -58,13 +58,13 @@ export default async function UserView({
{/* Main Content */}
<div className="flex w-full flex-col gap-6 lg:w-3/4">
{/* Hero/Welcome Banner */}
<div className="animate-fade-in relative overflow-hidden rounded-2xl border border-[var(--border-subtle)] bg-gradient-to-br from-[var(--color-primary)]/10 via-[var(--color-surface)] to-[var(--color-accent)]/5 shadow-elevated">
<div className="absolute -top-10 -right-10 h-48 w-48 rounded-full opacity-[0.1] blur-[80px]" style={{ background: "var(--color-primary)" }} />
<div className="absolute -bottom-10 -left-10 h-36 w-36 rounded-full opacity-[0.06] blur-[60px]" style={{ background: "var(--color-accent)" }} />
<div className="relative overflow-hidden rounded-3xl border border-white/[0.06] bg-[#09090b] shadow-2xl">
<div className="absolute inset-0 bg-gradient-to-br from-[#5AB4C6]/5 via-transparent to-transparent" />
<div className="absolute -top-20 -right-20 w-64 h-64 bg-[#5AB4C6]/5 rounded-full blur-[100px]" />
<div className="relative flex flex-col items-center gap-6 px-6 py-8 md:flex-row md:px-8">
<div className="shrink-0">
<div className="relative">
<div className="rounded-2xl bg-gradient-to-br from-[var(--color-primary)]/20 to-[var(--color-primary)]/5 p-1 shadow-lg">
<div className="rounded-2xl bg-white/5 border border-white/10 p-1 shadow-lg">
<Image
src={avatarImageUrl(imager, look, { size: "l", direction: 2 })}
alt={username}
@@ -75,29 +75,33 @@ export default async function UserView({
unoptimized
/>
</div>
<div className="absolute -bottom-2 -right-2 flex h-7 w-7 items-center justify-center rounded-full bg-green-500 shadow-lg ring-2 ring-[var(--color-surface)]">
<div className="absolute -bottom-2 -right-2 flex h-7 w-7 items-center justify-center rounded-full bg-green-500 shadow-lg ring-2 ring-[#09090b]">
<span className="h-3 w-3 rounded-full bg-green-200 animate-ping absolute" />
<span className="h-3 w-3 rounded-full bg-white relative" />
</div>
</div>
</div>
<div className="flex-1 text-center md:text-left">
<h1 className="m-0 text-2xl font-extrabold tracking-tight">
Welcome back, <span className="text-[var(--color-primary)]">{username}</span>
<h1 className="m-0 text-2xl font-black tracking-tight text-white">
Welcome back,{" "}
<span className="text-[#5AB4C6]">{username}</span>
</h1>
<p className="mt-1 text-sm text-[var(--color-text-muted-readable)]">
<p className="mt-1 text-sm text-white/50">
{motto || "Happy to see you again!"}
</p>
<div className="mt-4 flex flex-wrap justify-center gap-3 md:justify-start">
<Link href="/client" target="_blank" rel="noopener noreferrer">
<div className="inline-flex items-center gap-2 rounded-xl bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-primary)]/80 px-6 py-2.5 text-sm font-bold text-[var(--button-text-color-readable)] shadow-lg shadow-[var(--color-primary)]/30 transition-all duration-200 hover:scale-105 hover:shadow-glow active:scale-95">
<Gamepad2 size={18} />
<div className="inline-flex items-center gap-2 bg-[#5AB4C6] hover:bg-[#5AB4C6]/90 text-white font-black text-xs py-3 px-6 rounded-lg transition-all active:scale-[0.98] uppercase tracking-widest shadow-lg shadow-[#5AB4C6]/20">
<Gamepad2 size={15} />
{t("enterHotel", { hotelName })}
<ArrowRight size={16} className="transition-transform group-hover:translate-x-0.5" />
<ArrowRight
size={14}
className="transition-transform group-hover:translate-x-0.5"
/>
</div>
</Link>
<Link href={`/u/${username}`}>
<div className="inline-flex items-center gap-2 rounded-xl border-2 border-[var(--border-subtle)] bg-[var(--color-surface)] px-5 py-2.5 text-sm font-bold text-[var(--color-text-readable)] transition-all duration-200 hover:border-[var(--color-primary)]/30 hover:shadow-md">
<div className="inline-flex items-center gap-2 bg-white/5 hover:bg-white/10 text-white font-black text-xs py-3 px-6 rounded-lg transition-all active:scale-[0.98] uppercase tracking-widest border border-white/10">
View Profile
</div>
</Link>
@@ -107,18 +111,20 @@ export default async function UserView({
</div>
{/* Online Friends Bar */}
<div className="animate-slide-up rounded-2xl border border-[var(--border-subtle)] bg-[var(--color-surface)] p-1">
<div className="rounded-2xl border border-white/[0.06] bg-white/[0.03] p-1">
<div className="flex flex-col gap-2 lg:flex-row lg:items-center">
<div className="flex shrink-0 items-center gap-2 rounded-xl bg-gradient-to-r from-[var(--color-primary)]/10 to-[var(--color-primary)]/5 px-4 py-2.5">
<div className="relative flex h-2.5 w-2.5">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-400 opacity-75" />
<span className="relative inline-flex h-2.5 w-2.5 rounded-full bg-green-500" />
<div className="flex shrink-0 items-center gap-2 px-4 py-2.5">
<div className="relative flex h-2 w-2">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-[#5AB4C6] opacity-80" />
<span className="relative inline-flex h-2 w-2 rounded-full bg-[#5AB4C6]" />
</div>
<span className="text-xs font-bold text-[var(--color-text-readable)]">{t("onlineFriends")}</span>
<span className="text-[10px] font-black uppercase tracking-widest text-white/60">
{t("onlineFriends")}
</span>
</div>
<div className="flex flex-wrap items-center gap-2 px-3 py-1">
{onlineFriends.length === 0 ? (
<p className="m-0 text-xs font-medium text-[var(--color-text-muted-readable)]">
<p className="m-0 text-xs font-medium text-white/40">
{t("noFriendsOnline")}
</p>
) : (
@@ -128,7 +134,7 @@ export default async function UserView({
href={`/u/${f.username}`}
className="group relative transition-all duration-200 hover:scale-110"
>
<div className="overflow-hidden rounded-xl border-2 border-transparent bg-[var(--color-background)] transition-all group-hover:border-green-500/30">
<div className="overflow-hidden rounded-xl border-2 border-transparent bg-white/5 transition-all group-hover:border-[#5AB4C6]/30">
<Image
src={avatarImageUrl(imager, f.look, { headOnly: true })}
alt={f.username}
@@ -139,7 +145,7 @@ export default async function UserView({
unoptimized
/>
</div>
<span className="absolute -bottom-1 left-1/2 -translate-x-1/2 whitespace-nowrap rounded-full bg-[var(--color-surface)] px-2 py-0.5 text-[0.5rem] font-bold text-[var(--color-text-muted)] opacity-0 shadow-lg transition-all group-hover:opacity-100">
<span className="absolute -bottom-1 left-1/2 -translate-x-1/2 whitespace-nowrap rounded-full bg-[#09090b] px-2 py-0.5 text-[0.5rem] font-bold text-white/60 opacity-0 shadow-lg transition-all group-hover:opacity-100 border border-white/10">
{f.username}
</span>
</Link>
@@ -153,10 +159,14 @@ export default async function UserView({
{/* Sidebar */}
<div className="flex w-full flex-col gap-6 lg:w-1/4">
{latestArticle ? (
<div className="animate-slide-up overflow-hidden rounded-2xl border border-[var(--border-subtle)] bg-[var(--color-surface)] shadow-card transition-all duration-200 hover:shadow-card-hover">
<div className="flex items-center gap-2 bg-gradient-to-r from-[var(--color-primary)]/10 to-[var(--color-primary)]/5 px-4 py-3 border-b border-[var(--border-subtle)]">
<Newspaper className="h-4 w-4 text-[var(--color-primary)]" />
<h2 className="m-0 text-sm font-bold">{t("latestArticle")}</h2>
<div className="overflow-hidden rounded-2xl border border-white/[0.06] bg-white/[0.03] transition-all duration-200 hover:bg-white/[0.06]">
<div className="flex items-center gap-2 px-4 py-3 border-b border-white/[0.06]">
<div className="flex h-6 w-6 items-center justify-center rounded-lg bg-[#5AB4C6]/10">
<Newspaper className="h-3 w-3 text-[#5AB4C6]" />
</div>
<h2 className="m-0 text-[11px] font-black text-white uppercase tracking-wider">
{t("latestArticle")}
</h2>
</div>
<Link href={`/news/${latestArticle.slug}`} className="group block">
<div className="relative aspect-[16/9] overflow-hidden">
@@ -167,16 +177,18 @@ export default async function UserView({
className="object-cover transition-transform duration-500 group-hover:scale-110"
unoptimized
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100" />
<div className="absolute inset-0 bg-gradient-to-t from-[#09090b] via-black/20 to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100" />
</div>
<div className="p-4">
<h3 className="text-sm font-bold leading-tight">{latestArticle.title}</h3>
<h3 className="text-sm font-bold text-white leading-tight">
{latestArticle.title}
</h3>
{latestArticle.shortStory && (
<p className="mt-1.5 text-xs text-[var(--color-text-muted-readable)] leading-relaxed">
<p className="mt-1.5 text-xs text-white/40 leading-relaxed">
{excerpt(latestArticle.shortStory, 100)}
</p>
)}
<div className="mt-3 flex items-center gap-1 text-xs font-bold text-[var(--color-primary)]">
<div className="mt-3 flex items-center gap-1 text-[10px] font-black uppercase tracking-widest text-[#5AB4C6]">
Read more <ArrowRight size={12} />
</div>
</div>
@@ -185,13 +197,30 @@ export default async function UserView({
) : null}
{/* Quick Stats */}
<div className="animate-slide-up rounded-2xl border border-[var(--border-subtle)] bg-gradient-to-br from-[var(--color-primary)]/10 to-[var(--color-accent)]/5 p-5 text-center" style={{ animationDelay: "100ms" }}>
<Sparkles className="mx-auto h-6 w-6 text-[var(--color-primary)]" />
<p className="mt-2 text-sm font-bold text-[var(--color-text-readable)]">
<div className="rounded-2xl border border-white/[0.06] bg-white/[0.03] p-5 text-center">
<div className="mx-auto flex h-10 w-10 items-center justify-center rounded-xl bg-[#5AB4C6]/10">
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="#5AB4C6"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4" />
<polyline points="10 17 15 12 10 7" />
<line x1="15" x2="3" y1="12" y2="12" />
</svg>
</div>
<p className="mt-3 text-sm font-black text-white tracking-wide">
Explore the Hotel
</p>
<p className="mt-1 text-xs text-[var(--color-text-muted-readable)]">
Check out the latest news, visit the marketplace, or chat with friends!
<p className="mt-1 text-xs text-white/40">
Check out the latest news, visit the marketplace, or chat with
friends!
</p>
</div>
</div>