feat: release v0.2.0 profiles, chat, freeze, and bots
Ship the product pass on the v0.1.3 schema with additive migrations, a replica migration lock, and bot accounts kept out of people lists.
This commit is contained in:
@@ -2,17 +2,15 @@
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import dynamic from "next/dynamic";
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Flame, Heart, Sparkles, User as UserIcon } from "lucide-react";
|
||||
import { Flame, Heart, Sparkles } from "lucide-react";
|
||||
import { ProfileHeader } from "@/components/profile-header";
|
||||
import { NotFoundState, notFoundSentence } from "@/components/not-found-state";
|
||||
import { http, toUserError } from "@/lib/api";
|
||||
import { useAuth } from "@/lib/auth-store";
|
||||
import type { PublicProfile } from "@/lib/types";
|
||||
import { Panel } from "@/components/panel";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { UserAvatar, avatarPropsOf } from "@/components/ui/user-avatar";
|
||||
import { formatFaNumber, formatStreakLabel } from "@/lib/utils";
|
||||
|
||||
const AdminProfileManage = dynamic(
|
||||
@@ -20,12 +18,6 @@ const AdminProfileManage = dynamic(
|
||||
{ ssr: false },
|
||||
);
|
||||
|
||||
function roleLabel(role: string) {
|
||||
if (role === "mentor") return "منتور";
|
||||
if (role === "admin") return "ادمین";
|
||||
return "هنرجو";
|
||||
}
|
||||
|
||||
export default function PublicProfilePage() {
|
||||
const params = useParams<{ id: string }>();
|
||||
const id = Number(params.id);
|
||||
@@ -41,9 +33,12 @@ export default function PublicProfilePage() {
|
||||
|
||||
if (!enabled) {
|
||||
return (
|
||||
<div className="mx-auto max-w-2xl py-16 text-center text-sm text-muted-foreground">
|
||||
کاربر پیدا نشد.
|
||||
</div>
|
||||
<NotFoundState
|
||||
title="کاربر پیدا نشد"
|
||||
description="این شناسه در سامانه نیست."
|
||||
href="/unwrap"
|
||||
actionLabel="بازگشت به مرکز"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -62,14 +57,16 @@ export default function PublicProfilePage() {
|
||||
|
||||
if (profileQ.isError || !profileQ.data?.user) {
|
||||
return (
|
||||
<div className="mx-auto max-w-2xl space-y-4 py-16 text-center">
|
||||
<p className="text-sm font-bold text-destructive">
|
||||
{toUserError(profileQ.error, "کاربر پیدا نشد")}
|
||||
</p>
|
||||
<Button asChild variant="outline">
|
||||
<Link href="/unwrap">بازگشت</Link>
|
||||
</Button>
|
||||
</div>
|
||||
<NotFoundState
|
||||
title="کاربر پیدا نشد"
|
||||
description={notFoundSentence(
|
||||
toUserError(profileQ.error, ""),
|
||||
"این شناسه در سامانه نیست.",
|
||||
"کاربر پیدا نشد",
|
||||
)}
|
||||
href="/unwrap"
|
||||
actionLabel="بازگشت به مرکز"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -77,40 +74,55 @@ export default function PublicProfilePage() {
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-2xl space-y-6">
|
||||
<Panel
|
||||
title={person.name}
|
||||
description={roleLabel(person.role)}
|
||||
icon={<UserIcon className="h-5 w-5" />}
|
||||
tint="bg-primary/10 text-primary"
|
||||
>
|
||||
<div className="flex flex-col items-center gap-4 sm:flex-row sm:items-start">
|
||||
<UserAvatar name={person.name} className="h-20 w-20" {...avatarPropsOf(person)} />
|
||||
<div className="min-w-0 flex-1 space-y-2 text-center sm:text-start">
|
||||
<h1 className="text-2xl font-black tracking-tight">{person.name}</h1>
|
||||
<p className="font-latin text-sm font-bold" dir="ltr">
|
||||
@{person.username}
|
||||
</p>
|
||||
{person.isLocked && (
|
||||
<p className="text-xs font-bold text-destructive">حساب قفل است</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
<ProfileHeader
|
||||
person={{
|
||||
name: person.name,
|
||||
role: person.role,
|
||||
banner: person.banner,
|
||||
createdAt: person.private ? undefined : person.createdAt,
|
||||
avatarVariant: person.avatarVariant,
|
||||
avatarPalette: person.avatarPalette,
|
||||
avatarPhoto: person.avatarPhoto,
|
||||
isFrozen: person.isFrozen,
|
||||
isClosed: person.isClosed,
|
||||
}}
|
||||
secondary={person.private ? "این پروفایل خصوصی است" : undefined}
|
||||
/>
|
||||
{person.isLocked && <p className="text-xs font-bold text-destructive">حساب قفل است</p>}
|
||||
|
||||
{person.private ? (
|
||||
<p className="text-sm text-muted-foreground">جز نام و آواتار چیزی نمایش داده نمیشود.</p>
|
||||
) : (
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<Stat icon={<Sparkles className="h-4 w-4 text-gold" />} label="امتیاز" value={formatFaNumber(person.xp)} />
|
||||
<Stat icon={<Heart className="h-4 w-4 text-destructive" />} label="قلبها" value={formatFaNumber(person.hearts)} />
|
||||
{person.xp != null && <Stat icon={<Sparkles className="h-4 w-4 text-gold" />} label="امتیاز" value={formatFaNumber(person.xp)} />}
|
||||
{person.hearts != null && <Stat icon={<Heart className="h-4 w-4 text-destructive" />} label="قلبها" value={formatFaNumber(person.hearts)} />}
|
||||
{person.streakCurrent != null && (
|
||||
<Stat
|
||||
icon={<Flame className="h-4 w-4 text-accent" />}
|
||||
label="زنجیرهٔ فعلی"
|
||||
value={formatStreakLabel(person.streakCurrent)}
|
||||
/>
|
||||
)}
|
||||
{person.streakLongest != null && (
|
||||
<Stat
|
||||
icon={<Flame className="h-4 w-4 text-muted-foreground" />}
|
||||
label="بلندترین زنجیره"
|
||||
value={formatStreakLabel(person.streakLongest)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{(person.socialLinks ?? []).length > 0 && (
|
||||
<ul className="space-y-1 text-sm">
|
||||
{person.socialLinks!.map((l) => (
|
||||
<li key={l.url}>
|
||||
<a href={l.url} className="text-primary underline" target="_blank" rel="noreferrer">
|
||||
{l.name}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
{isAdmin && <AdminProfileManage userId={person.id} />}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user