polish: typewriter SSR fix, Community link label, verify premium cards, loading skeleton
Deploy / release (push) Successful in 8s
Deploy / deploy (push) Skipped

This commit is contained in:
openhands committed 2026-07-24 14:49:07 +02:00
1 parent fc42a1c138
commit 12e4a07e48
26 files changed
+153 -39

No files matched your search

+1 -1
View File
@@ -406,7 +406,7 @@ export default async function Home() {
</h3> </h3>
</div> </div>
<Link href="/community" className="text-xs font-bold text-white/80 hover:text-white hover:underline"> <Link href="/community" className="text-xs font-bold text-white/80 hover:text-white hover:underline">
{tp("allNews")} {tp("allUsers")}
</Link> </Link>
</div> </div>
<div className="p-4"> <div className="p-4">
+97 -24
View File
@@ -1,13 +1,68 @@
import { CheckCircle2, Clock, Link2, MailX } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { isValidVerificationToken } from "@/actions/email-verify"; import { isValidVerificationToken } from "@/actions/email-verify";
import { ContentCard, EmptyState } from "@/components/public/ui";
import { prisma } from "@/lib/prisma"; import { prisma } from "@/lib/prisma";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
type Status = "verified" | "already" | "invalid" | "unavailable"; 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({ export default async function VerifyPage({
searchParams, 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 ( return (
<main style={{ maxWidth: 480, margin: "2rem auto" }}> <>
{status === "verified" && ( {status === "verified" && (
<ContentCard <StatusCard
icon="✅" icon={<CheckCircle2 size={22} />}
title={t("verifiedTitle")} title={t("verifiedTitle")}
subtitle={t("verifiedSubtitle")} subtitle={t("verifiedSubtitle")}
color="green"
> >
<p className="muted" style={{ marginTop: 0 }}> <p className="text-sm" style={{ color: "var(--color-text-muted)" }}>
{t("verifiedBody")} {t("verifiedBody")}
</p> </p>
<Link href="/login" className="btn btn-primary"> <Link href="/login" className={linkClass} style={linkStyle}>
{t("goToLogin")} {t("goToLogin")}
</Link> </Link>
</ContentCard> </StatusCard>
)} )}
{status === "already" && ( {status === "already" && (
<ContentCard <StatusCard
icon="✅" icon={<CheckCircle2 size={22} />}
title={t("alreadyTitle")} title={t("alreadyTitle")}
subtitle={t("alreadySubtitle")} subtitle={t("alreadySubtitle")}
color="yellow"
> >
<p className="muted" style={{ marginTop: 0 }}> <p className="text-sm" style={{ color: "var(--color-text-muted)" }}>
{t("alreadyBody")} {t("alreadyBody")}
</p> </p>
<Link href="/login" className="btn btn-primary"> <Link href="/login" className={linkClass} style={linkStyle}>
{t("goToLogin")} {t("goToLogin")}
</Link> </Link>
</ContentCard> </StatusCard>
)} )}
{status === "unavailable" && ( {status === "unavailable" && (
<ContentCard <StatusCard
icon="⏳" icon={<Clock size={22} />}
title={t("unavailableTitle")} title={t("unavailableTitle")}
subtitle={t("unavailableSubtitle")} subtitle={t("unavailableSubtitle")}
color="blue"
> >
<p className="muted" style={{ margin: 0 }}> <p className="text-sm" style={{ color: "var(--color-text-muted)" }}>
{t("unavailableBody")} {t("unavailableBody")}
</p> </p>
</ContentCard> </StatusCard>
)} )}
{status === "invalid" && ( {status === "invalid" && (
<ContentCard <StatusCard
icon="📧" icon={<MailX size={22} />}
title={t("invalidTitle")} title={t("invalidTitle")}
subtitle={t("invalidSubtitle")} subtitle={t("invalidSubtitle")}
color="red"
>
<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)",
}}
> >
<EmptyState icon="🔗">{t("invalidBody")}</EmptyState>
<div style={{ textAlign: "center" }}>
<Link href="/login" className="btn btn-outline">
{t("backToLogin")} {t("backToLogin")}
</Link> </Link>
</div> </StatusCard>
</ContentCard>
)} )}
</main> </>
); );
} }
+24 -6
View File
@@ -1,15 +1,33 @@
export default function RootLoading() { export default function RootLoading() {
return ( return (
<div style={{ maxWidth: 1280, margin: "0 auto", padding: "1.5rem 1rem" }}>
<div className="flex flex-col gap-10 pb-14">
<div <div
style={{ display: "flex", justifyContent: "center", padding: "4rem 0" }} className="h-16 animate-pulse rounded-xl"
>
<div
className="animate-spin w-8 h-8 border-4 rounded-full"
style={{ style={{
borderColor: "var(--color-primary)", background: "color-mix(in srgb, var(--color-text-muted) 8%, transparent)",
borderTopColor: "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>
<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>
); );
} }
+9 -8
View File
@@ -10,13 +10,13 @@ export function TypewriterText({
text: string; text: string;
className?: string; className?: string;
}) { }) {
const [displayed, setDisplayed] = useState(""); const [displayed, setDisplayed] = useState(text[0] ?? "");
const [done, setDone] = useState(false); const [done, setDone] = useState(text.length <= 1);
useEffect(() => { useEffect(() => {
setDisplayed(""); setDisplayed(text[0] ?? "");
setDone(false); setDone(text.length <= 1);
let i = 0; let i = 1;
const id = setInterval(() => { const id = setInterval(() => {
if (i < text.length) { if (i < text.length) {
setDisplayed(text.slice(0, i + 1)); setDisplayed(text.slice(0, i + 1));
@@ -30,13 +30,14 @@ export function TypewriterText({
}, [text]); }, [text]);
return ( return (
<>
<span className="sr-only">{text}</span>
<motion.span <motion.span
className={`${className ?? ""} ${!done ? "typing-cursor" : ""}`} className={`${className ?? ""} ${!done ? "typing-cursor" : ""}`}
initial={{ opacity: 0 }} aria-hidden
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
> >
{displayed} {displayed}
</motion.span> </motion.span>
</>
); );
} }
+1
View File
@@ -189,6 +189,7 @@
"newsTitle": "Последни новини", "newsTitle": "Последни новини",
"newsSubtitle": "Току що от хотела", "newsSubtitle": "Току що от хотела",
"allNews": "Всички новини →", "allNews": "Всички новини →",
"allUsers": "Общност →",
"noArticles": "Все още няма статии.", "noArticles": "Все още няма статии.",
"noPhotos": "Все още няма снимки.", "noPhotos": "Все още няма снимки.",
"noUsers": "Няма потребители онлайн", "noUsers": "Няма потребители онлайн",
+1
View File
@@ -189,6 +189,7 @@
"newsTitle": "Nejnovější zprávy", "newsTitle": "Nejnovější zprávy",
"newsSubtitle": "Čerstvé z hotelu", "newsSubtitle": "Čerstvé z hotelu",
"allNews": "Všechny novinky →", "allNews": "Všechny novinky →",
"allUsers": "Komunita →",
"noArticles": "Zatím žádné články.", "noArticles": "Zatím žádné články.",
"noPhotos": "Zatím žádné fotky.", "noPhotos": "Zatím žádné fotky.",
"noUsers": "Žádní uživatelé online", "noUsers": "Žádní uživatelé online",
+1
View File
@@ -189,6 +189,7 @@
"newsTitle": "Seneste nyt", "newsTitle": "Seneste nyt",
"newsSubtitle": "Frisk fra hotellet", "newsSubtitle": "Frisk fra hotellet",
"allNews": "Alle nyheder →", "allNews": "Alle nyheder →",
"allUsers": "Fællesskab →",
"noArticles": "Ingen artikler endnu.", "noArticles": "Ingen artikler endnu.",
"noPhotos": "Ingen billeder endnu.", "noPhotos": "Ingen billeder endnu.",
"noUsers": "Ingen brugere online", "noUsers": "Ingen brugere online",
+1
View File
@@ -189,6 +189,7 @@
"newsTitle": "Neueste Nachrichten", "newsTitle": "Neueste Nachrichten",
"newsSubtitle": "Frisch aus dem Hotel", "newsSubtitle": "Frisch aus dem Hotel",
"allNews": "Alle Nachrichten →", "allNews": "Alle Nachrichten →",
"allUsers": "Community →",
"noArticles": "Noch keine Artikel.", "noArticles": "Noch keine Artikel.",
"noPhotos": "Noch keine Fotos.", "noPhotos": "Noch keine Fotos.",
"noUsers": "Keine Benutzer online", "noUsers": "Keine Benutzer online",
+1
View File
@@ -189,6 +189,7 @@
"newsTitle": "Τελευταία νέα", "newsTitle": "Τελευταία νέα",
"newsSubtitle": "Φρέσκο από το ξενοδοχείο", "newsSubtitle": "Φρέσκο από το ξενοδοχείο",
"allNews": "Όλα τα νέα →", "allNews": "Όλα τα νέα →",
"allUsers": "Κοινότητα →",
"noArticles": "Δεν υπάρχουν ακόμη άρθρα.", "noArticles": "Δεν υπάρχουν ακόμη άρθρα.",
"noPhotos": "Δεν υπάρχουν ακόμα φωτογραφίες.", "noPhotos": "Δεν υπάρχουν ακόμα φωτογραφίες.",
"noUsers": "Δεν υπάρχουν χρήστες στο διαδίκτυο", "noUsers": "Δεν υπάρχουν χρήστες στο διαδίκτυο",
+1
View File
@@ -232,6 +232,7 @@
"newsTitle": "Latest news", "newsTitle": "Latest news",
"newsSubtitle": "Fresh from the hotel", "newsSubtitle": "Fresh from the hotel",
"allNews": "All news →", "allNews": "All news →",
"allUsers": "Community →",
"noArticles": "No articles yet.", "noArticles": "No articles yet.",
"noPhotos": "No photos yet.", "noPhotos": "No photos yet.",
"noUsers": "No users online", "noUsers": "No users online",
+1
View File
@@ -189,6 +189,7 @@
"newsTitle": "Últimas noticias", "newsTitle": "Últimas noticias",
"newsSubtitle": "Lo último del hotel", "newsSubtitle": "Lo último del hotel",
"allNews": "Todas las noticias →", "allNews": "Todas las noticias →",
"allUsers": "Comunidad →",
"noArticles": "Aún no hay artículos.", "noArticles": "Aún no hay artículos.",
"noPhotos": "Aún no hay fotos.", "noPhotos": "Aún no hay fotos.",
"noUsers": "No hay usuarios en línea", "noUsers": "No hay usuarios en línea",
+1
View File
@@ -189,6 +189,7 @@
"newsTitle": "Dernières actualités", "newsTitle": "Dernières actualités",
"newsSubtitle": "Fraîchement sorties de l'hôtel", "newsSubtitle": "Fraîchement sorties de l'hôtel",
"allNews": "Toutes les actualités →", "allNews": "Toutes les actualités →",
"allUsers": "Communauté →",
"noArticles": "Pas encore d'articles.", "noArticles": "Pas encore d'articles.",
"noPhotos": "Pas encore de photos.", "noPhotos": "Pas encore de photos.",
"noUsers": "Aucun utilisateur en ligne", "noUsers": "Aucun utilisateur en ligne",
+1
View File
@@ -189,6 +189,7 @@
"newsTitle": "Najnovije vijesti", "newsTitle": "Najnovije vijesti",
"newsSubtitle": "Svježe iz hotela", "newsSubtitle": "Svježe iz hotela",
"allNews": "Sve novosti →", "allNews": "Sve novosti →",
"allUsers": "Zajednica →",
"noArticles": "Još nema članaka.", "noArticles": "Još nema članaka.",
"noPhotos": "Još nema fotografija.", "noPhotos": "Još nema fotografija.",
"noUsers": "Nema korisnika na mreži", "noUsers": "Nema korisnika na mreži",
+1
View File
@@ -189,6 +189,7 @@
"newsTitle": "Friss hírek", "newsTitle": "Friss hírek",
"newsSubtitle": "Frissen a szállodából", "newsSubtitle": "Frissen a szállodából",
"allNews": "Minden hír →", "allNews": "Minden hír →",
"allUsers": "Közösség →",
"noArticles": "Még nincsenek cikkek.", "noArticles": "Még nincsenek cikkek.",
"noPhotos": "Még nincsenek fényképek.", "noPhotos": "Még nincsenek fényképek.",
"noUsers": "Nincsenek online felhasználók", "noUsers": "Nincsenek online felhasználók",
+1
View File
@@ -232,6 +232,7 @@
"newsTitle": "Ultime notizie", "newsTitle": "Ultime notizie",
"newsSubtitle": "Appena arrivate dall'hotel", "newsSubtitle": "Appena arrivate dall'hotel",
"allNews": "Tutte le notizie →", "allNews": "Tutte le notizie →",
"allUsers": "Community →",
"noArticles": "Ancora nessun articolo.", "noArticles": "Ancora nessun articolo.",
"noPhotos": "Ancora nessuna foto.", "noPhotos": "Ancora nessuna foto.",
"noUsers": "Nessun utente online", "noUsers": "Nessun utente online",
+1
View File
@@ -232,6 +232,7 @@
"newsTitle": "Laatste nieuws", "newsTitle": "Laatste nieuws",
"newsSubtitle": "Vers van het hotel", "newsSubtitle": "Vers van het hotel",
"allNews": "Al het nieuws →", "allNews": "Al het nieuws →",
"allUsers": "Community →",
"noArticles": "Nog geen artikelen.", "noArticles": "Nog geen artikelen.",
"noPhotos": "Nog geen foto's.", "noPhotos": "Nog geen foto's.",
"noUsers": "Geen gebruikers online", "noUsers": "Geen gebruikers online",
+1
View File
@@ -189,6 +189,7 @@
"newsTitle": "Siste nytt", "newsTitle": "Siste nytt",
"newsSubtitle": "Frisk fra hotellet", "newsSubtitle": "Frisk fra hotellet",
"allNews": "Alle nyheter →", "allNews": "Alle nyheter →",
"allUsers": "Fellesskap →",
"noArticles": "Ingen artikler ennå.", "noArticles": "Ingen artikler ennå.",
"noPhotos": "Ingen bilder ennå.", "noPhotos": "Ingen bilder ennå.",
"noUsers": "Ingen brukere online", "noUsers": "Ingen brukere online",
+1
View File
@@ -189,6 +189,7 @@
"newsTitle": "Najnowsze wiadomości", "newsTitle": "Najnowsze wiadomości",
"newsSubtitle": "Świeżo z hotelu", "newsSubtitle": "Świeżo z hotelu",
"allNews": "Wszystkie aktualności →", "allNews": "Wszystkie aktualności →",
"allUsers": "Społeczność →",
"noArticles": "Nie ma jeszcze artykułów.", "noArticles": "Nie ma jeszcze artykułów.",
"noPhotos": "Nie ma jeszcze zdjęć.", "noPhotos": "Nie ma jeszcze zdjęć.",
"noUsers": "Brak użytkowników online", "noUsers": "Brak użytkowników online",
+1
View File
@@ -189,6 +189,7 @@
"newsTitle": "Últimas notícias", "newsTitle": "Últimas notícias",
"newsSubtitle": "Fresco do hotel", "newsSubtitle": "Fresco do hotel",
"allNews": "Todas as notícias →", "allNews": "Todas as notícias →",
"allUsers": "Comunidade →",
"noArticles": "Nenhum artigo ainda.", "noArticles": "Nenhum artigo ainda.",
"noPhotos": "Ainda não há fotos.", "noPhotos": "Ainda não há fotos.",
"noUsers": "Nenhum usuário on-line", "noUsers": "Nenhum usuário on-line",
+1
View File
@@ -189,6 +189,7 @@
"newsTitle": "Ultimele știri", "newsTitle": "Ultimele știri",
"newsSubtitle": "Proaspăt de la hotel", "newsSubtitle": "Proaspăt de la hotel",
"allNews": "Toate știrile →", "allNews": "Toate știrile →",
"allUsers": "Comunitate →",
"noArticles": "Încă nu există articole.", "noArticles": "Încă nu există articole.",
"noPhotos": "Nicio fotografie încă.", "noPhotos": "Nicio fotografie încă.",
"noUsers": "Niciun utilizator online", "noUsers": "Niciun utilizator online",
+1
View File
@@ -189,6 +189,7 @@
"newsTitle": "Последние новости", "newsTitle": "Последние новости",
"newsSubtitle": "Только что из отеля", "newsSubtitle": "Только что из отеля",
"allNews": "Все новости →", "allNews": "Все новости →",
"allUsers": "Сообщество →",
"noArticles": "Статей пока нет.", "noArticles": "Статей пока нет.",
"noPhotos": "Фотографий пока нет.", "noPhotos": "Фотографий пока нет.",
"noUsers": "Нет пользователей в сети", "noUsers": "Нет пользователей в сети",
+1
View File
@@ -189,6 +189,7 @@
"newsTitle": "Najnovšie správy", "newsTitle": "Najnovšie správy",
"newsSubtitle": "Čerstvé z hotela", "newsSubtitle": "Čerstvé z hotela",
"allNews": "Všetky novinky →", "allNews": "Všetky novinky →",
"allUsers": "Komunita →",
"noArticles": "Zatiaľ žiadne články.", "noArticles": "Zatiaľ žiadne články.",
"noPhotos": "Zatiaľ žiadne fotky.", "noPhotos": "Zatiaľ žiadne fotky.",
"noUsers": "Žiadni používatelia online", "noUsers": "Žiadni používatelia online",
+1
View File
@@ -189,6 +189,7 @@
"newsTitle": "Најновије вести", "newsTitle": "Најновије вести",
"newsSubtitle": "Свеже из хотела", "newsSubtitle": "Свеже из хотела",
"allNews": "Све вести →", "allNews": "Све вести →",
"allUsers": "Заједница →",
"noArticles": "Још нема чланака.", "noArticles": "Још нема чланака.",
"noPhotos": "Још нема фотографија.", "noPhotos": "Још нема фотографија.",
"noUsers": "Нема корисника на мрежи", "noUsers": "Нема корисника на мрежи",
+1
View File
@@ -189,6 +189,7 @@
"newsTitle": "Senaste nyheterna", "newsTitle": "Senaste nyheterna",
"newsSubtitle": "Färskt från hotellet", "newsSubtitle": "Färskt från hotellet",
"allNews": "Alla nyheter →", "allNews": "Alla nyheter →",
"allUsers": "Community →",
"noArticles": "Inga artiklar ännu.", "noArticles": "Inga artiklar ännu.",
"noPhotos": "Inga foton än.", "noPhotos": "Inga foton än.",
"noUsers": "Inga användare online", "noUsers": "Inga användare online",
+1
View File
@@ -189,6 +189,7 @@
"newsTitle": "En son haberler", "newsTitle": "En son haberler",
"newsSubtitle": "Otelden yeni çıkmış", "newsSubtitle": "Otelden yeni çıkmış",
"allNews": "Tüm haberler →", "allNews": "Tüm haberler →",
"allUsers": "Topluluk →",
"noArticles": "Henüz makale yok.", "noArticles": "Henüz makale yok.",
"noPhotos": "Henüz fotoğraf yok.", "noPhotos": "Henüz fotoğraf yok.",
"noUsers": "Çevrimiçi kullanıcı yok", "noUsers": "Çevrimiçi kullanıcı yok",
+1
View File
@@ -189,6 +189,7 @@
"newsTitle": "Останні новини", "newsTitle": "Останні новини",
"newsSubtitle": "Щойно з готелю", "newsSubtitle": "Щойно з готелю",
"allNews": "Всі новини →", "allNews": "Всі новини →",
"allUsers": "Спільнота →",
"noArticles": "Ще немає статей.", "noArticles": "Ще немає статей.",
"noPhotos": "Фото ще немає.", "noPhotos": "Фото ще немає.",
"noUsers": "Немає користувачів онлайн", "noUsers": "Немає користувачів онлайн",