feat(auth): implement all 16 homepage/login/register review items
Gitea Actions Runner Test / test-job (push) Successful in 1s
CI / check (push) Successful in 33s
CI / tests-integration (push) Successful in 1m42s
CI / tests-unit (push) Successful in 1m47s
CI / tests-ui (push) Successful in 2m30s
CI / preflight (push) Skipped
CI / deploy (push) Failing after 2m56s
Gitea Actions Runner Test / test-job (push) Successful in 1s
CI / check (push) Successful in 33s
CI / tests-integration (push) Successful in 1m42s
CI / tests-unit (push) Successful in 1m47s
CI / tests-ui (push) Successful in 2m30s
CI / preflight (push) Skipped
CI / deploy (push) Failing after 2m56s
- add countArticles() (published-only, mirrors news-list) and warm total_articles - localize homepage metadata; bind articleCount to both stats; unique photo alts - drop duplicate news date and the mascot preload priorities - extract shared AuthPageFrame/AuthUsersCards used by /login and /register - login: localized noindex metadata, session redirect via safeRedirectPath, ?from passthrough from proxy, unified auth roster cache keys, registered notice - register: localized metadata, session redirect to /me, unified cache keys - add resend-verification flow on /verify with rate-limited non-enumerable action - add safeRedirectPath() with unit tests - register form: live requirements checklist + password mismatch guard - login form: unverified state with resend-link CTA - honour prefers-reduced-motion in TypewriterText - add 6 translations across all 25 locales
This commit is contained in:
1 parent
8561c3f85e
commit
3933214953
43 files changed
+1037
-490
No files matched your search
+22
-24
@@ -26,6 +26,7 @@ import { resolveHotelName } from "@/lib/hotel-name";
|
||||
import { captchaConfig } from "@/lib/services/captcha";
|
||||
import { getNewsList } from "@/lib/services/news-list";
|
||||
import {
|
||||
countArticles,
|
||||
countPhotos,
|
||||
countRooms,
|
||||
countUsers,
|
||||
@@ -42,17 +43,17 @@ import {
|
||||
ICON_NAV_ME,
|
||||
} from "@/lib/site-icons";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Home",
|
||||
description:
|
||||
"An online virtual world where you can create your own avatar, make friends, chat, and build your own rooms.",
|
||||
openGraph: {
|
||||
title: "Home",
|
||||
description:
|
||||
"An online virtual world where you can create your own avatar, make friends, chat, and build your own rooms.",
|
||||
type: "website",
|
||||
},
|
||||
};
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const tn = await getTranslations("nav");
|
||||
const tp = await getTranslations("pages.home");
|
||||
const title = tn("home");
|
||||
const description = tp("welcomeBody");
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
openGraph: { title, description, type: "website" },
|
||||
};
|
||||
}
|
||||
|
||||
// ── Local visual language (theme-aware, no big white blocks) ──────────
|
||||
const PANEL_BG =
|
||||
@@ -177,6 +178,7 @@ async function getHotelData() {
|
||||
users,
|
||||
rooms,
|
||||
totalPhotos,
|
||||
articleCount,
|
||||
articles,
|
||||
recentUsers,
|
||||
recentPhotos,
|
||||
@@ -197,6 +199,9 @@ async function getHotelData() {
|
||||
cached("total_photos", 300_000, countPhotos, { staleMs: 300000 }).catch(
|
||||
publicReadFailure("home.photos-count"),
|
||||
),
|
||||
cached("total_articles", 300_000, countArticles, {
|
||||
staleMs: 300000,
|
||||
}).catch(publicReadFailure("home.articles-count")),
|
||||
getNewsList(4, { throwOnError: true }).catch(
|
||||
publicReadFailure("home.news"),
|
||||
),
|
||||
@@ -230,6 +235,7 @@ async function getHotelData() {
|
||||
users,
|
||||
rooms,
|
||||
totalPhotos,
|
||||
articleCount,
|
||||
articles,
|
||||
recentUsers,
|
||||
recentPhotos,
|
||||
@@ -251,6 +257,7 @@ export default async function Home() {
|
||||
users,
|
||||
rooms,
|
||||
totalPhotos,
|
||||
articleCount,
|
||||
articles,
|
||||
recentUsers,
|
||||
recentPhotos,
|
||||
@@ -344,12 +351,6 @@ export default async function Home() {
|
||||
>
|
||||
{a.title}
|
||||
</h3>
|
||||
<p
|
||||
className="mt-0.5 text-[10px] sm:text-[11px] font-semibold opacity-80"
|
||||
style={{ color: "#cbd5e1" }}
|
||||
>
|
||||
{formatDate(a.createdAt, "date", "")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
@@ -373,7 +374,7 @@ export default async function Home() {
|
||||
color: "var(--color-primary)",
|
||||
},
|
||||
{
|
||||
value: articles?.length ?? null,
|
||||
value: articleCount,
|
||||
label: tp("statsArticles"),
|
||||
color: "var(--color-accent)",
|
||||
},
|
||||
@@ -699,7 +700,6 @@ export default async function Home() {
|
||||
alt=""
|
||||
width={96}
|
||||
height={128}
|
||||
priority
|
||||
unoptimized
|
||||
className="relative animate-float object-contain drop-shadow-2xl"
|
||||
/>
|
||||
@@ -856,7 +856,7 @@ export default async function Home() {
|
||||
icon: ICON_CAMERA,
|
||||
},
|
||||
{
|
||||
value: articles?.length ?? null,
|
||||
value: articleCount,
|
||||
label: tp("statsArticles"),
|
||||
icon: ICON_ARTICLE,
|
||||
},
|
||||
@@ -1025,12 +1025,10 @@ export default async function Home() {
|
||||
<Reveal className="mx-auto w-full max-w-7xl">
|
||||
<GlassPanel title={tp("recentPhotos")} icon={ICON_CAMERA}>
|
||||
<div className="mx-auto grid max-w-5xl grid-cols-2 gap-3 sm:grid-cols-4">
|
||||
{recentPhotos.slice(0, 4).map((p) => (
|
||||
{recentPhotos.slice(0, 4).map((p, i) => (
|
||||
<Link
|
||||
key={p.id}
|
||||
href="/photos"
|
||||
title=""
|
||||
aria-label={tp("recentPhotos")}
|
||||
className="block aspect-[4/3] overflow-hidden rounded-2xl border transition-all duration-300 ease-out hover:scale-[1.03] hover:shadow-xl hover:ring-2 hover:ring-[color-mix(in_srgb,var(--color-primary)_45%,transparent)]"
|
||||
style={{
|
||||
borderColor:
|
||||
@@ -1039,7 +1037,7 @@ export default async function Home() {
|
||||
>
|
||||
<Image
|
||||
src={p.url}
|
||||
alt={tp("recentPhotos")}
|
||||
alt={`${tp("recentPhotos")} ${i + 1}`}
|
||||
width={280}
|
||||
height={210}
|
||||
className="h-full w-full object-cover"
|
||||
|
||||
Reference in new issue
Block a user