polish: typewriter SSR fix, Community link label, verify premium cards, loading skeleton
This commit is contained in:
1 parent
fc42a1c138
commit
12e4a07e48
26 files changed
+166
-52
No files matched your search
@@ -406,7 +406,7 @@ export default async function Home() {
|
||||
</h3>
|
||||
</div>
|
||||
<Link href="/community" className="text-xs font-bold text-white/80 hover:text-white hover:underline">
|
||||
{tp("allNews")}
|
||||
{tp("allUsers")}
|
||||
</Link>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
|
||||
+100
-27
@@ -1,13 +1,68 @@
|
||||
import { CheckCircle2, Clock, Link2, MailX } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { isValidVerificationToken } from "@/actions/email-verify";
|
||||
import { ContentCard, EmptyState } from "@/components/public/ui";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
type Status = "verified" | "already" | "invalid" | "unavailable";
|
||||
|
||||
function StatusCard({
|
||||
icon,
|
||||
title,
|
||||
subtitle,
|
||||
children,
|
||||
color,
|
||||
}: {
|
||||
icon: React.ReactNode;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
children: React.ReactNode;
|
||||
color: string;
|
||||
}) {
|
||||
const gradientMap: Record<string, string> = {
|
||||
green: "linear-gradient(140deg, #0A2F1A 0%, #0F3D22 20%, #154C2A 40%, #1B5A32 60%, #20683A 75%, #1A5A30 90%, #144826 100%)",
|
||||
yellow: "linear-gradient(140deg, #3A2F0A 0%, #4A3D0F 20%, #5A4C15 40%, #6A5A1B 60%, #7A6820 75%, #6A5A1A 90%, #5A4814 100%)",
|
||||
red: "linear-gradient(140deg, #3A0F0A 0%, #4A1A0F 20%, #5A2515 40%, #6A301B 60%, #7A3B20 75%, #6A301A 90%, #5A2514 100%)",
|
||||
blue: "linear-gradient(140deg, #0A1A3A 0%, #0F254A 20%, #15305A 40%, #1B3B6A 60%, #20467A 75%, #1A3B6A 90%, #14305A 100%)",
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="flex min-h-[60vh] items-center justify-center px-4">
|
||||
<div
|
||||
className="w-full max-w-md overflow-hidden rounded-xl border"
|
||||
style={{
|
||||
borderColor: "color-mix(in srgb, var(--color-text-muted) 12%, transparent)",
|
||||
backgroundColor: "var(--color-surface)",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="flex items-center gap-2.5 px-5 py-4"
|
||||
style={{ background: gradientMap[color] ?? gradientMap.blue }}
|
||||
>
|
||||
<span className="text-white">{icon}</span>
|
||||
<h1 className="text-sm font-bold text-white">{title}</h1>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-4 px-6 py-8 text-center">
|
||||
<div
|
||||
className="flex h-16 w-16 items-center justify-center rounded-full"
|
||||
style={{
|
||||
background: "color-mix(in srgb, var(--color-primary) 12%, transparent)",
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
<p className="text-sm font-semibold" style={{ color: "var(--color-text-muted)" }}>
|
||||
{subtitle}
|
||||
</p>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export default async function VerifyPage({
|
||||
searchParams,
|
||||
}: {
|
||||
@@ -47,64 +102,82 @@ export default async function VerifyPage({
|
||||
}
|
||||
}
|
||||
|
||||
const linkClass =
|
||||
"btn-shine inline-flex items-center gap-2 font-extrabold text-sm px-6 py-3 rounded-xl transition-all duration-200 hover:scale-[1.02] active:scale-95 shadow-lg";
|
||||
const linkStyle = {
|
||||
background: "var(--color-primary)",
|
||||
color: "var(--color-primary-foreground)",
|
||||
boxShadow: "0 4px 20px color-mix(in srgb, var(--color-primary) 30%, transparent)",
|
||||
};
|
||||
|
||||
return (
|
||||
<main style={{ maxWidth: 480, margin: "2rem auto" }}>
|
||||
<>
|
||||
{status === "verified" && (
|
||||
<ContentCard
|
||||
icon="✅"
|
||||
<StatusCard
|
||||
icon={<CheckCircle2 size={22} />}
|
||||
title={t("verifiedTitle")}
|
||||
subtitle={t("verifiedSubtitle")}
|
||||
color="green"
|
||||
>
|
||||
<p className="muted" style={{ marginTop: 0 }}>
|
||||
<p className="text-sm" style={{ color: "var(--color-text-muted)" }}>
|
||||
{t("verifiedBody")}
|
||||
</p>
|
||||
<Link href="/login" className="btn btn-primary">
|
||||
<Link href="/login" className={linkClass} style={linkStyle}>
|
||||
{t("goToLogin")}
|
||||
</Link>
|
||||
</ContentCard>
|
||||
</StatusCard>
|
||||
)}
|
||||
|
||||
{status === "already" && (
|
||||
<ContentCard
|
||||
icon="✅"
|
||||
<StatusCard
|
||||
icon={<CheckCircle2 size={22} />}
|
||||
title={t("alreadyTitle")}
|
||||
subtitle={t("alreadySubtitle")}
|
||||
color="yellow"
|
||||
>
|
||||
<p className="muted" style={{ marginTop: 0 }}>
|
||||
<p className="text-sm" style={{ color: "var(--color-text-muted)" }}>
|
||||
{t("alreadyBody")}
|
||||
</p>
|
||||
<Link href="/login" className="btn btn-primary">
|
||||
<Link href="/login" className={linkClass} style={linkStyle}>
|
||||
{t("goToLogin")}
|
||||
</Link>
|
||||
</ContentCard>
|
||||
</StatusCard>
|
||||
)}
|
||||
|
||||
{status === "unavailable" && (
|
||||
<ContentCard
|
||||
icon="⏳"
|
||||
<StatusCard
|
||||
icon={<Clock size={22} />}
|
||||
title={t("unavailableTitle")}
|
||||
subtitle={t("unavailableSubtitle")}
|
||||
color="blue"
|
||||
>
|
||||
<p className="muted" style={{ margin: 0 }}>
|
||||
<p className="text-sm" style={{ color: "var(--color-text-muted)" }}>
|
||||
{t("unavailableBody")}
|
||||
</p>
|
||||
</ContentCard>
|
||||
</StatusCard>
|
||||
)}
|
||||
|
||||
{status === "invalid" && (
|
||||
<ContentCard
|
||||
icon="📧"
|
||||
<StatusCard
|
||||
icon={<MailX size={22} />}
|
||||
title={t("invalidTitle")}
|
||||
subtitle={t("invalidSubtitle")}
|
||||
color="red"
|
||||
>
|
||||
<EmptyState icon="🔗">{t("invalidBody")}</EmptyState>
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<Link href="/login" className="btn btn-outline">
|
||||
{t("backToLogin")}
|
||||
</Link>
|
||||
</div>
|
||||
</ContentCard>
|
||||
<p className="text-sm" style={{ color: "var(--color-text-muted)" }}>
|
||||
{t("invalidBody")}
|
||||
</p>
|
||||
<Link href="/login" className={`${linkClass} !shadow-none`}
|
||||
style={{
|
||||
background: "transparent",
|
||||
color: "var(--color-text-readable)",
|
||||
border: "2px solid color-mix(in srgb, var(--color-text-muted) 20%, transparent)",
|
||||
}}
|
||||
>
|
||||
{t("backToLogin")}
|
||||
</Link>
|
||||
</StatusCard>
|
||||
)}
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
+29
-11
@@ -1,15 +1,33 @@
|
||||
export default function RootLoading() {
|
||||
return (
|
||||
<div
|
||||
style={{ display: "flex", justifyContent: "center", padding: "4rem 0" }}
|
||||
>
|
||||
<div
|
||||
className="animate-spin w-8 h-8 border-4 rounded-full"
|
||||
style={{
|
||||
borderColor: "var(--color-primary)",
|
||||
borderTopColor: "transparent",
|
||||
}}
|
||||
/>
|
||||
<div style={{ maxWidth: 1280, margin: "0 auto", padding: "1.5rem 1rem" }}>
|
||||
<div className="flex flex-col gap-10 pb-14">
|
||||
<div
|
||||
className="h-16 animate-pulse rounded-xl"
|
||||
style={{
|
||||
background: "color-mix(in srgb, var(--color-text-muted) 8%, transparent)",
|
||||
}}
|
||||
/>
|
||||
<div className="relative overflow-hidden rounded-2xl" style={{ height: 280 }}>
|
||||
<div
|
||||
className="h-full w-full animate-pulse"
|
||||
style={{
|
||||
background: "color-mix(in srgb, var(--color-text-muted) 6%, transparent)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="h-32 animate-pulse rounded-xl"
|
||||
style={{
|
||||
background: "color-mix(in srgb, var(--color-text-muted) 8%, transparent)",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user