mobile responsive

This commit is contained in:
gosangam
2026-10-16 15:45:29 +05:30
parent 1e551764e1
commit b194d4aa03
21 changed files with 249 additions and 138 deletions
+5 -4
View File
@@ -17,6 +17,7 @@ import { Can } from "@/components/staff-context";
import { toast } from "sonner";
import { broadcasts, type Broadcast } from "@/lib/api";
import { compressImage } from "@/lib/compress-image";
import { Clamp } from "@/components/ui/clamp";
export default function BroadcastsPage() {
const [items, setItems] = useState<Broadcast[]>([]);
@@ -256,12 +257,12 @@ function BroadcastRow({
return (
<li className="flex items-start gap-3 p-3">
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium text-foreground">
<Clamp className="text-sm font-medium text-foreground">
{item.title}
</p>
<p className="mt-0.5 line-clamp-2 text-xs text-muted-foreground">
</Clamp>
<Clamp lines={2} className="mt-0.5 text-xs text-muted-foreground">
{item.body}
</p>
</Clamp>
<p className="mt-1 text-xs text-muted-foreground">
{live
? `Published · dismissed by ${item.seen_count}`
+11 -8
View File
@@ -6,6 +6,7 @@ import { usePagedList } from "@/lib/use-paged-list";
import { ListShell, EmptyRow } from "@/components/list-shell";
import { Table, THead, TR, TH, TD } from "@/components/ui/table";
import { fmtDate } from "@/lib/utils";
import { Clamp } from "@/components/ui/clamp";
export default function CollectionsPage() {
const [q, setQ] = useState("");
@@ -40,18 +41,20 @@ export default function CollectionsPage() {
list.items.map((c) => (
<TR key={c.id}>
<TD className="max-w-md">
<div className="font-medium">{c.title}</div>
<Clamp lines={2} className="font-medium">
{c.title}
</Clamp>
{c.description ? (
<div className="mt-0.5 line-clamp-1 text-xs text-muted-foreground">
<Clamp className="mt-0.5 text-xs text-muted-foreground">
{c.description}
</div>
</Clamp>
) : null}
</TD>
<TD className="whitespace-nowrap">
<div className="font-medium">{c.owner_name}</div>
<div className="text-xs text-muted-foreground">
@{c.owner_handle}
</div>
<TD className="max-w-[14rem]">
<Clamp className="font-medium">{c.owner_name}</Clamp>
<Clamp className="text-xs text-muted-foreground">
{`@${c.owner_handle}`}
</Clamp>
</TD>
<TD>{c.item_count}</TD>
<TD>
+12 -9
View File
@@ -13,6 +13,7 @@ import { useConfirm } from "@/components/confirm";
import { RichTextarea } from "@/components/ui/rich-textarea";
import { RichText } from "@/lib/rich-text";
import { cn, fmtDate } from "@/lib/utils";
import { Clamp } from "@/components/ui/clamp";
const STATUSES = ["all", "open", "resolved"] as const;
type Status = (typeof STATUSES)[number];
@@ -87,7 +88,9 @@ export default function FeedbackPage() {
</div>
}
>
<Table>
{/* Phone: each field stacked, label above value, so a long message
gets the card's full width. Desktop: ordinary table rows. */}
<Table card="stacked">
<THead>
<TR>
<TH>From</TH>
@@ -103,18 +106,18 @@ export default function FeedbackPage() {
) : (
list.items.map((f) => (
<TR key={f.id}>
<TD className="whitespace-nowrap">
<TD className="max-w-[14rem]">
{f.author_handle ? (
<>
<div className="font-medium">{f.author_name}</div>
<div className="text-xs text-muted-foreground">
@{f.author_handle}
</div>
<Clamp className="font-medium">{f.author_name}</Clamp>
<Clamp className="text-xs text-muted-foreground">
{`@${f.author_handle}`}
</Clamp>
</>
) : (
<div className="text-xs text-muted-foreground">
<Clamp className="text-xs text-muted-foreground">
{f.contact_email ?? "Anonymous"}
</div>
</Clamp>
)}
</TD>
<TD className="max-w-md">
@@ -134,7 +137,7 @@ export default function FeedbackPage() {
</TD>
<TD className="whitespace-nowrap">{fmtDate(f.created_at)}</TD>
<TD className="text-right">
<div className="flex flex-wrap justify-end gap-2">
<div className="flex flex-wrap justify-start gap-2 md:justify-end">
<Can perm="feedback:resolve">
{!f.resolved_at && !f.acknowledged_at ? (
<Button
+3 -1
View File
@@ -109,7 +109,9 @@ export default function DashboardLayout({
</p>
</header>
<main className="min-w-0 flex-1 px-4 py-5 sm:px-6 sm:py-6 lg:px-10">
{/* break-words: a safety net so no unbroken string (a URL, a long
handle) can push a page sideways. Labels use <Clamp> on top. */}
<main className="min-w-0 flex-1 break-words px-4 py-5 sm:px-6 sm:py-6 lg:px-10">
<div className="mx-auto max-w-6xl">
<StaffProvider value={staffMe}>
<ConfirmProvider>
+15 -10
View File
@@ -9,6 +9,7 @@ import { Table, THead, TR, TH, TD } from "@/components/ui/table";
import { Button } from "@/components/ui/button";
import { useConfirm } from "@/components/confirm";
import { fmtDate } from "@/lib/utils";
import { Clamp } from "@/components/ui/clamp";
export default function PostsPage() {
const [q, setQ] = useState("");
@@ -67,25 +68,29 @@ export default function PostsPage() {
list.items.map((p) => (
<TR key={p.id} className={p.deleted_at ? "opacity-60" : ""}>
<TD className="max-w-md">
<div className="line-clamp-2">{p.caption || "—"}</div>
<div className="mt-0.5 flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
{p.source_title}
<Clamp lines={2}>{p.caption || "—"}</Clamp>
<div className="mt-0.5 flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 text-xs text-muted-foreground">
{p.source_title ? (
<Clamp lines={2} className="max-w-full">
{p.source_title}
</Clamp>
) : null}
{p.deleted_at ? (
<span className="rounded bg-destructive/10 px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-destructive">
<span className="rounded bg-destructive/10 px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-destructive whitespace-nowrap">
Deleted
</span>
) : p.is_hidden ? (
<span className="rounded bg-muted px-1.5 py-0.5 text-[10px] uppercase tracking-wide">
<span className="rounded bg-muted px-1.5 py-0.5 text-[10px] uppercase tracking-wide whitespace-nowrap">
Hidden
</span>
) : null}
</div>
</TD>
<TD className="whitespace-nowrap">
<div className="font-medium">{p.author_name}</div>
<div className="text-xs text-muted-foreground">
@{p.author_handle}
</div>
<TD className="max-w-[14rem]">
<Clamp className="font-medium">{p.author_name}</Clamp>
<Clamp className="text-xs text-muted-foreground">
{`@${p.author_handle}`}
</Clamp>
</TD>
<TD className="whitespace-nowrap text-xs text-muted-foreground">
{p.likes_count} likes · {p.comments_count} comments
+12 -5
View File
@@ -12,6 +12,7 @@ import { cn } from "@/lib/utils";
import { QuizEditor } from "../quiz-editor";
import { KIND_LABEL } from "../labels";
import { Participants, Responses } from "./results";
import { Clamp } from "@/components/ui/clamp";
type Tab = "edit" | "participants" | "responses";
@@ -105,7 +106,7 @@ export default function QuizDetailPage() {
return (
<div className="space-y-6">
<header className="flex flex-wrap items-start justify-between gap-4">
<div>
<div className="min-w-0 flex-1 basis-64">
<Link
href="/quizzes"
className="text-xs text-muted-foreground hover:text-foreground"
@@ -113,9 +114,15 @@ export default function QuizDetailPage() {
← Quizzes
</Link>
<h1 className="mt-1 text-xl font-semibold text-foreground">
{quiz.title}
<Clamp lines={2}>{quiz.title}</Clamp>
</h1>
<p className="mt-1 text-sm text-muted-foreground">
<Clamp
lines={2}
className="mt-1 text-sm text-muted-foreground"
title={[quiz.target_title, quiz.target_path]
.filter(Boolean)
.join(" — ")}
>
{KIND_LABEL[quiz.target_kind]}
{quiz.target_title && ` · ${quiz.target_title}`}
{quiz.target_path && (
@@ -123,9 +130,9 @@ export default function QuizDetailPage() {
({quiz.target_path})
</span>
)}
</p>
</Clamp>
</div>
<div className="flex gap-2">
<div className="flex shrink-0 gap-2">
<Can perm="quizzes:write">
<Button
variant={quiz.published_at ? "outline" : "primary"}
+9 -10
View File
@@ -29,6 +29,7 @@ import { Table, THead, TH, TR, TD } from "@/components/ui/table";
import { RichText } from "@/lib/rich-text";
import { cn, fmtDate } from "@/lib/utils";
import { fmtMs } from "../labels";
import { Clamp } from "@/components/ui/clamp";
const errMsg = (err: unknown, fallback: string) =>
err instanceof AdminError ? err.message : fallback;
@@ -110,7 +111,7 @@ export function Participants({ quizId }: { quizId: string }) {
<button
type="button"
onClick={() => view(p)}
className="flex items-center gap-3 text-left hover:underline"
className="flex min-w-0 max-w-full items-center gap-3 text-left hover:underline"
>
<Avatar
src={p.avatar_url ?? null}
@@ -118,12 +119,10 @@ export function Participants({ quizId }: { quizId: string }) {
className="size-8"
/>
<span className="min-w-0">
<span className="block truncate text-sm font-medium">
{p.name}
</span>
<span className="block truncate text-xs text-muted-foreground">
@{p.handle}
</span>
<Clamp className="text-sm font-medium">{p.name}</Clamp>
<Clamp className="text-xs text-muted-foreground">
{`@${p.handle}`}
</Clamp>
</span>
</button>
</TD>
@@ -184,7 +183,7 @@ export function Participants({ quizId }: { quizId: string }) {
{detail.items.map((it, i) => (
<div key={it.id ?? i} className="space-y-1.5">
<div className="flex items-start justify-between gap-3 text-sm">
<p className="font-medium">
<p className="min-w-0 font-medium">
{i + 1}. <RichText text={it.prompt} />
</p>
<span className="shrink-0 text-xs tabular-nums text-muted-foreground">
@@ -251,10 +250,10 @@ export function Responses({ quizId }: { quizId: string }) {
return (
<li key={q.id} className="rounded-xl border border-border p-4">
<div className="flex items-start justify-between gap-4">
<p className="text-sm font-medium">
<p className="min-w-0 text-sm font-medium">
{i + 1}. <RichText text={q.prompt} />
{q.multi && (
<span className="ml-2 rounded bg-muted px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
<span className="ml-2 rounded bg-muted px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-muted-foreground whitespace-nowrap">
Multiple answers
</span>
)}
+7 -6
View File
@@ -13,6 +13,7 @@ import { Table, THead, TH, TR, TD } from "@/components/ui/table";
import { Can } from "@/components/staff-context";
import { fmtDate } from "@/lib/utils";
import { KIND_LABEL } from "./labels";
import { Clamp } from "@/components/ui/clamp";
export default function QuizzesPage() {
const [list, setList] = useState<Quiz[] | null>(null);
@@ -68,9 +69,9 @@ export default function QuizzesPage() {
<TD>
<Link
href={`/quizzes/${q.id}`}
className="font-medium text-foreground hover:underline"
className="block font-medium text-foreground hover:underline"
>
{q.title}
<Clamp lines={2}>{q.title}</Clamp>
</Link>
<p className="text-xs text-muted-foreground">
Updated {fmtDate(q.updated_at)}
@@ -79,13 +80,13 @@ export default function QuizzesPage() {
<TD>
<p className="text-sm">{KIND_LABEL[q.target_kind]}</p>
{q.target_title && (
<p className="truncate text-xs text-muted-foreground">
<Clamp className="text-xs text-muted-foreground">
{q.target_title}
</p>
</Clamp>
)}
</TD>
<TD>{q.question_count}</TD>
<TD>
<TD className="[overflow-wrap:normal]">
{q.completed_count ?? 0} finished
<span className="text-muted-foreground">
{" "}
@@ -94,7 +95,7 @@ export default function QuizzesPage() {
</TD>
<TD>
{q.published_at ? (
<span className="rounded bg-primary/10 px-2 py-0.5 text-xs text-primary">
<span className="rounded bg-primary/10 px-2 py-0.5 text-xs text-primary whitespace-nowrap">
Published
</span>
) : (
+14 -11
View File
@@ -13,6 +13,7 @@ import { RichTextarea } from "@/components/ui/rich-textarea";
import { RichText } from "@/lib/rich-text";
import { useConfirm } from "@/components/confirm";
import { cn, fmtDate } from "@/lib/utils";
import { Clamp } from "@/components/ui/clamp";
const TYPES: { key: ReportType; label: string }[] = [
{ key: "post", label: "Posts" },
@@ -195,18 +196,16 @@ export default function ReportsPage() {
<TR key={r.id} className={resolved ? "opacity-70" : ""}>
<TD className="max-w-xs align-top">
{type === "user" ? (
<span className="font-medium">
<Clamp className="font-medium">
{r.target_text ? `@${r.target_text}` : "—"}
</span>
</Clamp>
) : (
<>
<div className="line-clamp-2">
{r.target_text || "—"}
</div>
<Clamp lines={2}>{r.target_text || "—"}</Clamp>
{r.target_author_handle ? (
<div className="mt-0.5 text-xs text-muted-foreground">
by @{r.target_author_handle}
</div>
<Clamp className="mt-0.5 text-xs text-muted-foreground">
{`by @${r.target_author_handle}`}
</Clamp>
) : null}
</>
)}
@@ -221,11 +220,15 @@ export default function ReportsPage() {
{r.category}
</div>
{r.reason ? (
<div className="mt-0.5 line-clamp-2">{r.reason}</div>
<Clamp lines={2} className="mt-0.5">
{r.reason}
</Clamp>
) : null}
</TD>
<TD className="whitespace-nowrap align-top text-xs text-muted-foreground">
{r.reporter_handle ? `@${r.reporter_handle}` : "—"}
<TD className="max-w-[12rem] align-top text-xs text-muted-foreground">
<Clamp>
{r.reporter_handle ? `@${r.reporter_handle}` : "—"}
</Clamp>
</TD>
<TD className="whitespace-nowrap align-top">
{fmtDate(r.created_at)}
+3 -2
View File
@@ -17,6 +17,7 @@ import Image from "next/image";
import { toast } from "sonner";
import { rewardGifts, type GiftableReward } from "@/lib/api";
import { compressImage } from "@/lib/compress-image";
import { Clamp } from "@/components/ui/clamp";
export default function RewardGiftsPage() {
const [catalog, setCatalog] = useState<GiftableReward[] | null>(null);
@@ -123,9 +124,9 @@ export default function RewardGiftsPage() {
unoptimized
/>
</div>
<p className="line-clamp-2 text-sm font-medium text-foreground">
<Clamp lines={2} className="w-full text-sm font-medium text-foreground">
{r.name}
</p>
</Clamp>
<div className="mt-auto w-full space-y-1.5">
<Can perm="rewards:send">
<button
+3 -3
View File
@@ -6,6 +6,7 @@ import { usePagedList } from "@/lib/use-paged-list";
import { ListShell, EmptyRow } from "@/components/list-shell";
import { Table, THead, TR, TH, TD } from "@/components/ui/table";
import { fmtDate } from "@/lib/utils";
import { Clamp } from "@/components/ui/clamp";
export default function ShortUrlsPage() {
// ── Stats ────────────────────────────────────────────────────────────────
@@ -137,10 +138,9 @@ export default function ShortUrlsPage() {
href={s.long_url}
target="_blank"
rel="noopener noreferrer"
className="line-clamp-1 break-all text-primary hover:underline"
title={s.long_url}
className="block text-primary hover:underline"
>
{s.long_url}
<Clamp>{s.long_url}</Clamp>
</a>
</TD>
<TD className="whitespace-nowrap text-muted-foreground">
+1 -1
View File
@@ -174,7 +174,7 @@ export function Sidebar({
<div className="px-3 pb-2 text-xs text-muted-foreground">
<p className="truncate font-medium text-foreground">{me.name}</p>
<p className="truncate">@{me.handle}</p>
<p className="mt-1 inline-block rounded bg-muted px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
<p className="mt-1 inline-block rounded bg-muted px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-muted-foreground whitespace-nowrap">
{staffMe.role_name}
</p>
</div>
+9 -6
View File
@@ -15,6 +15,7 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Modal } from "@/components/ui/modal";
import { useConfirm } from "@/components/confirm";
import { Clamp } from "@/components/ui/clamp";
const errMsg = (err: unknown, fallback: string) =>
err instanceof AdminError ? err.message : fallback;
@@ -129,18 +130,20 @@ export function RolesTab() {
<li key={r.id} className="rounded-xl border border-border p-4">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<p className="text-sm font-medium text-foreground">
{r.name}{" "}
<div className="flex min-w-0 items-center gap-2">
<Clamp className="text-sm font-medium text-foreground">
{r.name}
</Clamp>
{r.is_system && (
<span className="ml-1 rounded bg-muted px-1.5 py-0.5 text-[10px] uppercase tracking-wide text-muted-foreground">
<span className="shrink-0 rounded bg-muted px-1.5 py-0.5 text-[10px] uppercase tracking-wide text-muted-foreground whitespace-nowrap">
System
</span>
)}
</p>
</div>
{r.description && (
<p className="mt-0.5 text-xs text-muted-foreground">
<Clamp lines={2} className="mt-0.5 text-xs text-muted-foreground">
{r.description}
</p>
</Clamp>
)}
<p className="mt-1 text-xs text-muted-foreground">
{r.member_count} {r.member_count === 1 ? "member" : "members"}
+16 -10
View File
@@ -14,6 +14,7 @@ import { Input } from "@/components/ui/input";
import { Avatar } from "@/components/ui/avatar";
import { Table, THead, TH, TR, TD } from "@/components/ui/table";
import { useConfirm } from "@/components/confirm";
import { Clamp } from "@/components/ui/clamp";
const errMsg = (err: unknown, fallback: string) =>
err instanceof AdminError ? err.message : fallback;
@@ -132,7 +133,9 @@ export function StaffTab() {
<select
value={grantRole}
onChange={(e) => setGrantRole(e.target.value)}
className="h-10 rounded-lg border border-input bg-card px-3 text-sm"
// A <select> sizes itself to its longest option; a long custom
// role name must not widen the form past the screen.
className="h-10 min-w-0 max-w-full flex-1 truncate rounded-lg border border-input bg-card px-3 text-sm sm:max-w-xs sm:flex-none"
>
{roles.map((r) => (
<option key={r.id} value={r.id}>
@@ -164,22 +167,25 @@ export function StaffTab() {
return (
<TR key={m.user_id}>
<TD>
<div className="flex items-center gap-3">
<div className="flex min-w-0 items-center gap-3">
<Avatar
src={m.avatar_url ?? null}
name={m.name}
className="size-8"
/>
<div className="min-w-0">
<p className="truncate text-sm font-medium text-foreground">
<Clamp
className="text-sm font-medium text-foreground"
title={m.name}
>
{m.name}{" "}
{self && (
<span className="text-muted-foreground">(you)</span>
)}
</p>
<p className="truncate text-xs text-muted-foreground">
@{m.handle}
</p>
</Clamp>
<Clamp className="text-xs text-muted-foreground">
{`@${m.handle}`}
</Clamp>
</div>
</div>
</TD>
@@ -188,7 +194,7 @@ export function StaffTab() {
access, so a superadmin can't demote themselves into
a locked-out panel by accident. */}
{self ? (
<span className="text-sm">{m.role_name}</span>
<Clamp className="text-sm">{m.role_name}</Clamp>
) : (
<select
value={m.role_id}
@@ -200,7 +206,7 @@ export function StaffTab() {
"Role changed",
)
}
className="h-8 rounded-md border border-input bg-card px-2 text-xs"
className="h-8 w-full max-w-[14rem] truncate rounded-md border border-input bg-card px-2 text-xs"
>
{roles.map((r) => (
<option key={r.id} value={r.id}>
@@ -212,7 +218,7 @@ export function StaffTab() {
</TD>
<TD>
{m.blocked_at ? (
<span className="rounded bg-destructive/10 px-2 py-0.5 text-xs text-destructive">
<span className="rounded bg-destructive/10 px-2 py-0.5 text-xs text-destructive whitespace-nowrap">
Blocked
</span>
) : (
+10 -6
View File
@@ -21,6 +21,7 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Modal } from "@/components/ui/modal";
import { cn } from "@/lib/utils";
import { Clamp } from "@/components/ui/clamp";
/** "Vṛndāvana · 1972" — whichever halves we know. */
function placeYear(d: AdminDisciple): string | null {
@@ -138,13 +139,13 @@ export function DiscipleLinkModal({
<div className="text-xs text-muted-foreground">
Currently linked to
</div>
<div className="truncate text-sm font-medium">
<Clamp className="text-sm font-medium">
{user.disciple_name ?? (
<span className="text-muted-foreground">
unknown disciple ({user.disciple_id.slice(0, 8)}…)
</span>
)}
</div>
</Clamp>
</div>
<Button
variant="outline"
@@ -200,16 +201,19 @@ export function DiscipleLinkModal({
)}
>
<span className="min-w-0">
<span className="block truncate text-sm font-medium">
<Clamp
className="text-sm font-medium"
title={`${d.initiated_name} ${d.suffix ?? ""}`.trim()}
>
{d.initiated_name}{" "}
<span className="font-normal text-muted-foreground">
{d.suffix}
</span>
</span>
</Clamp>
{sub ? (
<span className="block truncate text-xs text-muted-foreground">
<Clamp className="text-xs text-muted-foreground">
{sub}
</span>
</Clamp>
) : null}
</span>
<span className="shrink-0 text-xs">
+14 -15
View File
@@ -23,6 +23,7 @@ import { RichTextarea } from "@/components/ui/rich-textarea";
import { fmtDate } from "@/lib/utils";
import { DiscipleLinkModal } from "./disciple-link-modal";
import { useStaff } from "@/components/staff-context";
import { Clamp } from "@/components/ui/clamp";
export default function UsersPage() {
const { can } = useStaff();
@@ -138,20 +139,20 @@ export default function UsersPage() {
list.items.map((u) => (
<TR key={u.id}>
<TD>
<div className="flex items-center gap-3">
<div className="flex min-w-0 items-center gap-3">
<Avatar src={u.avatar_url} name={u.name} />
<div className="min-w-0">
<div className="flex items-center gap-2">
<span className="font-medium">{u.name}</span>
<div className="flex min-w-0 items-center gap-2">
<Clamp className="font-medium">{u.name}</Clamp>
{u.banned_at ? (
<span className="rounded bg-destructive/10 px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-destructive">
<span className="shrink-0 rounded bg-destructive/10 px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-destructive whitespace-nowrap">
Suspended
</span>
) : null}
</div>
<div className="text-xs text-muted-foreground">
@{u.handle}
</div>
<Clamp className="text-xs text-muted-foreground">
{`@${u.handle}`}
</Clamp>
{u.disciple_id ? (
<div
className="mt-1 inline-flex max-w-full items-center gap-1 rounded bg-primary/10 px-1.5 py-0.5 text-[10px] font-medium text-primary"
@@ -161,9 +162,7 @@ export default function UsersPage() {
: "Linked to an unknown disciple record"
}
>
<span className="truncate">
{u.disciple_name ?? "Direct disciple"}
</span>
<Clamp>{u.disciple_name ?? "Direct disciple"}</Clamp>
</div>
) : null}
</div>
@@ -171,11 +170,11 @@ export default function UsersPage() {
</TD>
<TD>
<div className="space-y-1.5">
<div>
{u.email ?? (
<span className="text-muted-foreground">—</span>
)}
</div>
{u.email ? (
<Clamp>{u.email}</Clamp>
) : (
<span className="text-muted-foreground">—</span>
)}
<AuthIcons user={u} />
</div>
</TD>
+10 -12
View File
@@ -22,6 +22,7 @@ import {
type VaniCandidate,
type VaniPick,
} from "@/lib/api";
import { Clamp } from "@/components/ui/clamp";
/** Today in UTC — picks are keyed on a UTC calendar day. */
function todayUTC(): string {
@@ -200,12 +201,10 @@ export default function VaniPage() {
className="flex items-center gap-3 px-3 py-2.5"
>
<div className="min-w-0 flex-1">
<p className="truncate text-sm text-foreground">
{c.title}
</p>
<p className="truncate text-xs text-muted-foreground">
{vaniKindFor(c.collection)} · {c.snippet}
</p>
<Clamp className="text-sm text-foreground">{c.title}</Clamp>
<Clamp className="text-xs text-muted-foreground">
{`${vaniKindFor(c.collection)} · ${c.snippet}`}
</Clamp>
</div>
<button
type="button"
@@ -240,13 +239,12 @@ export default function VaniPage() {
{p.day}
</span>
<div className="min-w-0 flex-1">
<p className="truncate text-sm text-foreground">
<Clamp className="text-sm text-foreground">
{p.payload.title}
</p>
<p className="truncate text-xs text-muted-foreground">
{p.payload.label}
{p.payload.book_title ? ` · ${p.payload.book_title}` : ""}
</p>
</Clamp>
<Clamp className="text-xs text-muted-foreground">
{`${p.payload.label}${p.payload.book_title ? ` · ${p.payload.book_title}` : ""}`}
</Clamp>
</div>
<Can perm="vani:write">
<button