style: convert remaining ad-hoc inline cards to shared SurfaceCard
Introduce components/surface-card.tsx (Card + CardBody) mirroring the .content-card / SectionCard token set, and use it on the (site) pages that still hand-rolled card markup: me, search, and verify. This puts every public page on one of the shared card components (ContentCard, SectionCard, or SurfaceCard) for consistent radius/shadow/border.
This commit is contained in:
1 parent
a3e3356ea7
commit
ed6eaf33a0
4 files changed
+73
-117
No files matched your search
+16
-62
@@ -7,6 +7,7 @@ import { claimReferral } from "@/actions/referral";
|
|||||||
|
|
||||||
import { Reveal } from "@/components/motion-reveal";
|
import { Reveal } from "@/components/motion-reveal";
|
||||||
import { UserAvatarThumbnail } from "@/components/shared/user-avatar-thumbnail";
|
import { UserAvatarThumbnail } from "@/components/shared/user-avatar-thumbnail";
|
||||||
|
import { SurfaceCard } from "@/components/surface-card";
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
import {
|
import {
|
||||||
db,
|
db,
|
||||||
@@ -382,14 +383,9 @@ export default async function MePage({
|
|||||||
{ value: friendCount.toLocaleString(), label: "Friends" },
|
{ value: friendCount.toLocaleString(), label: "Friends" },
|
||||||
{ value: lastLoginDate, label: "Last login" },
|
{ value: lastLoginDate, label: "Last login" },
|
||||||
].map((s) => (
|
].map((s) => (
|
||||||
<div
|
<SurfaceCard
|
||||||
key={s.label}
|
key={s.label}
|
||||||
className="relative rounded-2xl border p-5 text-center transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg"
|
className="relative p-5 text-center transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg"
|
||||||
style={{
|
|
||||||
background: "var(--color-surface)",
|
|
||||||
borderColor:
|
|
||||||
"color-mix(in srgb, var(--color-primary) 12%, transparent)",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="text-xl md:text-2xl font-black tracking-tight mb-0.5"
|
className="text-xl md:text-2xl font-black tracking-tight mb-0.5"
|
||||||
@@ -403,20 +399,13 @@ export default async function MePage({
|
|||||||
>
|
>
|
||||||
{s.label}
|
{s.label}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</SurfaceCard>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
|
|
||||||
<Reveal delay={0.15}>
|
<Reveal delay={0.15}>
|
||||||
<div
|
<SurfaceCard className="p-5">
|
||||||
className="rounded-2xl border p-5"
|
|
||||||
style={{
|
|
||||||
background: "var(--color-surface)",
|
|
||||||
borderColor:
|
|
||||||
"color-mix(in srgb, var(--color-primary) 12%, transparent)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<div>
|
<div>
|
||||||
<h2
|
<h2
|
||||||
@@ -497,18 +486,11 @@ export default async function MePage({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</SurfaceCard>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
|
|
||||||
<Reveal delay={0.2}>
|
<Reveal delay={0.2}>
|
||||||
<div
|
<SurfaceCard className="p-5">
|
||||||
className="rounded-2xl border p-5"
|
|
||||||
style={{
|
|
||||||
background: "var(--color-surface)",
|
|
||||||
borderColor:
|
|
||||||
"color-mix(in srgb, var(--color-primary) 12%, transparent)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<h2
|
<h2
|
||||||
className="text-lg font-black tracking-tight"
|
className="text-lg font-black tracking-tight"
|
||||||
@@ -592,19 +574,12 @@ export default async function MePage({
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</SurfaceCard>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
|
|
||||||
{recentRooms.length > 0 && (
|
{recentRooms.length > 0 && (
|
||||||
<Reveal delay={0.25}>
|
<Reveal delay={0.25}>
|
||||||
<div
|
<SurfaceCard className="p-5">
|
||||||
className="rounded-2xl border p-5"
|
|
||||||
style={{
|
|
||||||
background: "var(--color-surface)",
|
|
||||||
borderColor:
|
|
||||||
"color-mix(in srgb, var(--color-primary) 12%, transparent)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<div>
|
<div>
|
||||||
<h2
|
<h2
|
||||||
@@ -656,20 +631,13 @@ export default async function MePage({
|
|||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</SurfaceCard>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{badges.length > 0 && (
|
{badges.length > 0 && (
|
||||||
<Reveal delay={0.3}>
|
<Reveal delay={0.3}>
|
||||||
<div
|
<SurfaceCard className="p-5">
|
||||||
className="rounded-2xl border p-5"
|
|
||||||
style={{
|
|
||||||
background: "var(--color-surface)",
|
|
||||||
borderColor:
|
|
||||||
"color-mix(in srgb, var(--color-primary) 12%, transparent)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<h2
|
<h2
|
||||||
className="text-lg font-black tracking-tight"
|
className="text-lg font-black tracking-tight"
|
||||||
@@ -697,19 +665,12 @@ export default async function MePage({
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</SurfaceCard>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Reveal delay={0.35}>
|
<Reveal delay={0.35}>
|
||||||
<div
|
<SurfaceCard className="p-5">
|
||||||
className="rounded-2xl border p-5"
|
|
||||||
style={{
|
|
||||||
background: "var(--color-surface)",
|
|
||||||
borderColor:
|
|
||||||
"color-mix(in srgb, var(--color-primary) 12%, transparent)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<h2
|
<h2
|
||||||
className="text-lg font-black tracking-tight mb-4"
|
className="text-lg font-black tracking-tight mb-4"
|
||||||
style={{ color: "var(--color-text-readable)" }}
|
style={{ color: "var(--color-text-readable)" }}
|
||||||
@@ -750,20 +711,13 @@ export default async function MePage({
|
|||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</SurfaceCard>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} catch {
|
} catch {
|
||||||
content = (
|
content = (
|
||||||
<div
|
<SurfaceCard className="p-6 text-center">
|
||||||
className="rounded-2xl border p-6 text-center"
|
|
||||||
style={{
|
|
||||||
background: "var(--color-surface)",
|
|
||||||
borderColor:
|
|
||||||
"color-mix(in srgb, var(--color-text-muted) 10%, transparent)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<p
|
<p
|
||||||
className="text-sm font-bold"
|
className="text-sm font-bold"
|
||||||
style={{ color: "var(--color-text-muted)" }}
|
style={{ color: "var(--color-text-muted)" }}
|
||||||
@@ -771,7 +725,7 @@ export default async function MePage({
|
|||||||
We couldn't load your dashboard right now. Please try again
|
We couldn't load your dashboard right now. Please try again
|
||||||
shortly.
|
shortly.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</SurfaceCard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { and, eq, like } from "drizzle-orm";
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { UserAvatarThumbnail } from "@/components/shared/user-avatar-thumbnail";
|
import { UserAvatarThumbnail } from "@/components/shared/user-avatar-thumbnail";
|
||||||
|
import { SurfaceCard } from "@/components/surface-card";
|
||||||
import { db, Rooms, User } from "@/lib/db";
|
import { db, Rooms, User } from "@/lib/db";
|
||||||
|
|
||||||
export const metadata: Metadata = { title: "Search" };
|
export const metadata: Metadata = { title: "Search" };
|
||||||
@@ -18,21 +19,14 @@ export default async function SearchPage({
|
|||||||
|
|
||||||
if (!query) {
|
if (!query) {
|
||||||
return (
|
return (
|
||||||
<div
|
<SurfaceCard className="p-10 text-center">
|
||||||
className="rounded-2xl border p-10 text-center"
|
|
||||||
style={{
|
|
||||||
background: "var(--color-surface)",
|
|
||||||
borderColor:
|
|
||||||
"color-mix(in srgb, var(--color-text-muted) 10%, transparent)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<p
|
<p
|
||||||
className="text-sm font-bold"
|
className="text-sm font-bold"
|
||||||
style={{ color: "var(--color-text-muted)" }}
|
style={{ color: "var(--color-text-muted)" }}
|
||||||
>
|
>
|
||||||
Enter a search term to find users.
|
Enter a search term to find users.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</SurfaceCard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,14 +60,7 @@ export default async function SearchPage({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<div
|
<SurfaceCard className="p-4">
|
||||||
className="rounded-2xl border p-4"
|
|
||||||
style={{
|
|
||||||
background: "var(--color-surface)",
|
|
||||||
borderColor:
|
|
||||||
"color-mix(in srgb, var(--color-text-muted) 10%, transparent)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<form action="/search" method="GET" className="flex gap-2">
|
<form action="/search" method="GET" className="flex gap-2">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -99,7 +86,7 @@ export default async function SearchPage({
|
|||||||
Search
|
Search
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</SurfaceCard>
|
||||||
|
|
||||||
<p className="text-sm" style={{ color: "var(--color-text-muted)" }}>
|
<p className="text-sm" style={{ color: "var(--color-text-muted)" }}>
|
||||||
Results for "{query}" — {users.length} user
|
Results for "{query}" — {users.length} user
|
||||||
@@ -109,14 +96,7 @@ export default async function SearchPage({
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
{users.length > 0 && (
|
{users.length > 0 && (
|
||||||
<div
|
<SurfaceCard className="p-5">
|
||||||
className="rounded-2xl border p-5"
|
|
||||||
style={{
|
|
||||||
background: "var(--color-surface)",
|
|
||||||
borderColor:
|
|
||||||
"color-mix(in srgb, var(--color-text-muted) 10%, transparent)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<h2
|
<h2
|
||||||
className="text-lg font-black tracking-tight mb-4"
|
className="text-lg font-black tracking-tight mb-4"
|
||||||
style={{ color: "var(--color-text-readable)" }}
|
style={{ color: "var(--color-text-readable)" }}
|
||||||
@@ -155,18 +135,11 @@ export default async function SearchPage({
|
|||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</SurfaceCard>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{rooms.length > 0 && (
|
{rooms.length > 0 && (
|
||||||
<div
|
<SurfaceCard className="p-5">
|
||||||
className="rounded-2xl border p-5"
|
|
||||||
style={{
|
|
||||||
background: "var(--color-surface)",
|
|
||||||
borderColor:
|
|
||||||
"color-mix(in srgb, var(--color-text-muted) 10%, transparent)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<h2
|
<h2
|
||||||
className="text-lg font-black tracking-tight mb-4"
|
className="text-lg font-black tracking-tight mb-4"
|
||||||
style={{ color: "var(--color-text-readable)" }}
|
style={{ color: "var(--color-text-readable)" }}
|
||||||
@@ -201,25 +174,18 @@ export default async function SearchPage({
|
|||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</SurfaceCard>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{users.length === 0 && rooms.length === 0 && (
|
{users.length === 0 && rooms.length === 0 && (
|
||||||
<div
|
<SurfaceCard className="p-10 text-center">
|
||||||
className="rounded-2xl border p-10 text-center"
|
|
||||||
style={{
|
|
||||||
background: "var(--color-surface)",
|
|
||||||
borderColor:
|
|
||||||
"color-mix(in srgb, var(--color-text-muted) 10%, transparent)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<p
|
<p
|
||||||
className="text-sm font-bold"
|
className="text-sm font-bold"
|
||||||
style={{ color: "var(--color-text-muted)" }}
|
style={{ color: "var(--color-text-muted)" }}
|
||||||
>
|
>
|
||||||
No users or rooms found for "{query}".
|
No users or rooms found for "{query}".
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</SurfaceCard>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { CheckCircle2, Clock, 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 { SurfaceCard } from "@/components/surface-card";
|
||||||
import { db, User } from "@/lib/db";
|
import { db, User } from "@/lib/db";
|
||||||
|
|
||||||
type Status = "verified" | "already" | "invalid" | "unavailable";
|
type Status = "verified" | "already" | "invalid" | "unavailable";
|
||||||
@@ -30,15 +31,7 @@ function StatusCard({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="flex min-h-[60vh] items-center justify-center px-4">
|
<main className="flex min-h-[60vh] items-center justify-center px-4">
|
||||||
<div
|
<SurfaceCard className="w-full max-w-md">
|
||||||
className="w-full max-w-md overflow-hidden rounded-2xl border"
|
|
||||||
style={{
|
|
||||||
borderColor:
|
|
||||||
"color-mix(in srgb, var(--color-text-muted) 12%, transparent)",
|
|
||||||
backgroundColor: "var(--color-surface)",
|
|
||||||
boxShadow: "var(--shadow-card)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
className="flex items-center gap-2.5 border-b px-5 py-4"
|
className="flex items-center gap-2.5 border-b px-5 py-4"
|
||||||
style={{
|
style={{
|
||||||
@@ -81,7 +74,7 @@ function StatusCard({
|
|||||||
</p>
|
</p>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</SurfaceCard>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import type { CSSProperties, ReactNode } from "react";
|
||||||
|
|
||||||
|
const CARD_BORDER =
|
||||||
|
"color-mix(in srgb, var(--color-text-muted) 12%, transparent)";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public-site surface card. Mirrors the CSS .content-card / SectionCard visual
|
||||||
|
* language (--radius-lg corners, --shadow-card, hairline border) so every page
|
||||||
|
* that opts out of ContentCard still shares one consistent component.
|
||||||
|
*/
|
||||||
|
export function SurfaceCard({
|
||||||
|
className = "",
|
||||||
|
style,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
className?: string;
|
||||||
|
style?: CSSProperties;
|
||||||
|
children: ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`overflow-hidden rounded-2xl border bg-[var(--color-surface)] ${className}`}
|
||||||
|
style={{
|
||||||
|
borderColor: CARD_BORDER,
|
||||||
|
boxShadow: "var(--shadow-card)",
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Padded body for a SurfaceCard. Override padding via className. */
|
||||||
|
export function SurfaceCardBody({
|
||||||
|
className = "p-5",
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
className?: string;
|
||||||
|
children: ReactNode;
|
||||||
|
}) {
|
||||||
|
return <div className={className}>{children}</div>;
|
||||||
|
}
|
||||||
Reference in new issue
Block a user