Skip to content

Commit b76c9dd

Browse files
authored
feat: unify profile UI and tighten signup identity flows (#20)
* Unify profile UI and tighten signup identity flows * feat: enhance profile UI with gender and email visibility in the profile banner * Flatten nav and add admin forbidden flow * Remove unnecessary profile section * refactor: improve header navigation styles and remove unnecessary elements
1 parent f7a74fd commit b76c9dd

File tree

23 files changed

+700
-214
lines changed

23 files changed

+700
-214
lines changed

app/(protected)/layout.tsx

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,35 @@
1-
import { BookMarked, LibraryBig, Search } from "lucide-react";
21
import Link from "next/link";
32

43
import ProfileMenu from "@/components/auth/profile-menu";
4+
import {
5+
HeaderNav,
6+
type HeaderNavItem,
7+
} from "@/components/navigation/header-nav";
58
import { ReactQueryProvider } from "@/components/providers/react-query-provider";
6-
import { buttonStyles } from "@/components/ui/button";
79
import { requireCurrentUser } from "@/lib/auth/server";
810
import { getPublicUserIdentityLabel } from "@/lib/auth/users";
911
import { createMyShelvesHref } from "@/lib/shelves/view-paths";
1012

13+
const NAV_ITEMS: readonly HeaderNavItem[] = [
14+
{
15+
href: "/books/search",
16+
icon: "search",
17+
label: "Search",
18+
matchPrefix: "/books",
19+
},
20+
{
21+
href: "/clubs",
22+
icon: "libraryBig",
23+
label: "Clubs",
24+
},
25+
{
26+
href: createMyShelvesHref(),
27+
icon: "bookMarked",
28+
label: "Shelves",
29+
matchPrefix: "/me/shelves",
30+
},
31+
];
32+
1133
export default async function ProtectedLayout({ children }: Props.Layout) {
1234
const currentUser = await requireCurrentUser();
1335
const displayName = getPublicUserIdentityLabel(currentUser);
@@ -16,45 +38,25 @@ export default async function ProtectedLayout({ children }: Props.Layout) {
1638
<ReactQueryProvider>
1739
<div className="min-h-screen">
1840
<header className="relative z-40 overflow-visible border-b border-(--border) bg-(--surface-strong)/90 backdrop-blur">
19-
<div className="mx-auto flex w-full max-w-6xl flex-wrap items-center justify-between gap-3 px-4 py-4 sm:px-6 lg:px-8">
20-
<div className="flex items-center gap-2">
41+
<div className="mx-auto flex w-full max-w-6xl flex-wrap items-center justify-between gap-5 px-4 py-4 sm:px-6 lg:px-8">
42+
<div className="flex items-center gap-8">
2143
<Link
2244
href="/books/search"
2345
className="text-4xl leading-none text-foreground"
2446
style={{ fontFamily: '"Romanesco", cursive' }}
2547
>
2648
Book by Book
2749
</Link>
50+
<HeaderNav items={NAV_ITEMS} />
2851
</div>
2952

30-
<nav className="relative z-50 flex items-center gap-2">
31-
<Link
32-
href="/books/search"
33-
className={buttonStyles({ variant: "ghost", size: "sm" })}
34-
>
35-
<Search aria-hidden className="h-4 w-4 shrink-0" />
36-
Search
37-
</Link>
38-
<Link
39-
href="/clubs"
40-
className={buttonStyles({ variant: "ghost", size: "sm" })}
41-
>
42-
<LibraryBig aria-hidden className="h-4 w-4 shrink-0" />
43-
Clubs
44-
</Link>
45-
<Link
46-
href={createMyShelvesHref()}
47-
className={buttonStyles({ variant: "ghost", size: "sm" })}
48-
>
49-
<BookMarked aria-hidden className="h-4 w-4 shrink-0" />
50-
Shelves
51-
</Link>
53+
<div className="relative z-50 flex flex-wrap items-center justify-end gap-2">
5254
<ProfileMenu
5355
name={displayName}
5456
email={currentUser.email}
5557
imageUrl={currentUser.imageUrl}
5658
/>
57-
</nav>
59+
</div>
5860
</div>
5961
</header>
6062

app/(protected)/me/page.tsx

Lines changed: 98 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import Link from "next/link";
2-
import { ArrowRight } from "lucide-react";
2+
import { ArrowRight, BookOpen, Mail, MapPin, User } from "lucide-react";
33

44
import { UserAvatar } from "@/components/auth/user-avatar";
55
import { Badge } from "@/components/ui/badge";
@@ -40,87 +40,109 @@ export default async function MePage() {
4040

4141
return (
4242
<div className="space-y-6">
43-
<section className="space-y-3">
44-
<h1 className="text-3xl font-semibold sm:text-4xl">Profile</h1>
45-
<p className="text-(--muted)">
46-
Your account information used for Book by Book.
47-
</p>
48-
</section>
49-
5043
<Card className="border-2">
51-
<CardContent className="flex flex-col gap-6 p-6 sm:flex-row sm:items-center sm:p-8">
52-
<UserAvatar
53-
name={displayName}
54-
email={user.email}
55-
imageUrl={user.imageUrl}
56-
alt="Profile avatar"
57-
className="h-24 w-24 border border-(--border) bg-(--surface) text-2xl font-semibold text-foreground shadow-sm"
58-
/>
44+
<div className="h-24 bg-[radial-gradient(circle_at_top_left,rgba(15,97,82,0.18),transparent_48%),linear-gradient(135deg,rgba(255,246,222,0.85),rgba(214,236,230,0.7))]" />
45+
<CardContent className="relative flex flex-col gap-6 p-6 pt-0 sm:p-8 sm:pt-0">
46+
<div className="-mt-10 flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
47+
<div className="flex items-end gap-4">
48+
<UserAvatar
49+
name={displayName}
50+
email={user.email}
51+
imageUrl={user.imageUrl}
52+
alt="Profile avatar"
53+
fallbackVariant="person"
54+
className="h-24 w-24 border-4 border-(--surface-strong) bg-(--surface) text-2xl font-semibold text-foreground shadow-sm"
55+
/>
5956

60-
<div className="space-y-2">
61-
<h2 className="text-2xl font-semibold">{displayName}</h2>
62-
<p className="text-(--muted)">Nickname: {user.nickname}</p>
63-
{user.email ? (
64-
<p className="text-sm text-(--muted)">Connected email: {user.email}</p>
65-
) : null}
57+
<div className="space-y-2 pb-1">
58+
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-(--muted)">
59+
Reader profile
60+
</p>
61+
<h2 className="text-2xl font-semibold sm:text-3xl">
62+
{displayName}
63+
</h2>
64+
</div>
65+
</div>
66+
</div>
67+
68+
<div className="divide-y divide-(--border)/60">
69+
<div className="flex flex-col gap-3 py-3 sm:flex-row sm:items-start sm:gap-6">
70+
<div className="flex items-center gap-3 sm:w-44 sm:shrink-0">
71+
<span className="flex h-10 w-10 items-center justify-center rounded-full bg-(--surface) text-(--accent) shadow-sm">
72+
<Mail aria-hidden className="h-4 w-4" />
73+
</span>
74+
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-(--muted)">
75+
Email
76+
</p>
77+
</div>
78+
<p className="min-w-0 flex-1 break-all pt-1 text-sm font-medium text-foreground">
79+
{fallbackText(user.email, "Unavailable")}
80+
</p>
81+
</div>
82+
83+
<div className="flex flex-col gap-3 py-3 sm:flex-row sm:items-start sm:gap-6">
84+
<div className="flex items-center gap-3 sm:w-44 sm:shrink-0">
85+
<span className="flex h-10 w-10 items-center justify-center rounded-full bg-(--surface) text-(--accent) shadow-sm">
86+
<User aria-hidden className="h-4 w-4" />
87+
</span>
88+
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-(--muted)">
89+
Gender
90+
</p>
91+
</div>
92+
<p className="min-w-0 flex-1 pt-1 text-sm font-medium text-foreground">
93+
{formatGenderLabel(user.gender)}
94+
</p>
95+
</div>
96+
97+
<div className="flex flex-col gap-3 py-3 sm:flex-row sm:items-start sm:gap-6">
98+
<div className="flex items-center gap-3 sm:w-44 sm:shrink-0">
99+
<span className="flex h-10 w-10 items-center justify-center rounded-full bg-(--surface) text-(--accent) shadow-sm">
100+
<MapPin aria-hidden className="h-4 w-4" />
101+
</span>
102+
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-(--muted)">
103+
Country
104+
</p>
105+
</div>
106+
<div className="min-w-0 flex-1 pt-1">
107+
<p className="text-sm font-medium text-foreground">
108+
{countryName ?? fallbackText(user.countryCode, "Unavailable")}
109+
</p>
110+
{user.countryCode ? (
111+
<p className="mt-1 text-xs text-(--muted)">
112+
{user.countryCode}
113+
</p>
114+
) : null}
115+
</div>
116+
</div>
117+
118+
<div className="flex flex-col gap-3 py-3 sm:flex-row sm:items-start sm:gap-6">
119+
<div className="flex items-center gap-3 sm:w-44 sm:shrink-0">
120+
<span className="flex h-10 w-10 items-center justify-center rounded-full bg-(--surface) text-(--accent) shadow-sm">
121+
<BookOpen aria-hidden className="h-4 w-4" />
122+
</span>
123+
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-(--muted)">
124+
Favorite genres
125+
</p>
126+
</div>
127+
<div className="min-w-0 flex-1 pt-1">
128+
{user.favoriteGenres.length > 0 ? (
129+
<div className="flex flex-wrap gap-2">
130+
{user.favoriteGenres.map((genre) => (
131+
<Badge key={genre}>{getFavoriteGenreLabel(genre)}</Badge>
132+
))}
133+
</div>
134+
) : (
135+
<p className="text-sm text-(--muted)">
136+
No favorite genres saved.
137+
</p>
138+
)}
139+
</div>
140+
</div>
66141
</div>
67142
</CardContent>
68143
</Card>
69144

70-
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
71-
<Card>
72-
<CardHeader>
73-
<CardTitle className="text-lg">Gender</CardTitle>
74-
<CardDescription>Collected during signup</CardDescription>
75-
</CardHeader>
76-
<CardContent>
77-
<p className="text-sm font-medium">{formatGenderLabel(user.gender)}</p>
78-
</CardContent>
79-
</Card>
80-
81-
<Card>
82-
<CardHeader>
83-
<CardTitle className="text-lg">Country</CardTitle>
84-
<CardDescription>Reader location</CardDescription>
85-
</CardHeader>
86-
<CardContent className="space-y-1">
87-
<p className="text-sm font-medium">
88-
{countryName ?? fallbackText(user.countryCode, "Unavailable")}
89-
</p>
90-
{user.countryCode ? (
91-
<p className="text-xs text-(--muted)">{user.countryCode}</p>
92-
) : null}
93-
</CardContent>
94-
</Card>
95-
96-
<Card>
97-
<CardHeader>
98-
<CardTitle className="text-lg">Favorite genres</CardTitle>
99-
<CardDescription>Used for onboarding and recommendations later</CardDescription>
100-
</CardHeader>
101-
<CardContent className="flex flex-wrap gap-2">
102-
{user.favoriteGenres.length > 0 ? (
103-
user.favoriteGenres.map((genre) => (
104-
<Badge key={genre}>{getFavoriteGenreLabel(genre)}</Badge>
105-
))
106-
) : (
107-
<p className="text-sm text-(--muted)">No favorite genres saved.</p>
108-
)}
109-
</CardContent>
110-
</Card>
111-
112-
<Card>
113-
<CardHeader>
114-
<CardTitle className="text-lg">User ID</CardTitle>
115-
<CardDescription>Internal identifier</CardDescription>
116-
</CardHeader>
117-
<CardContent>
118-
<code className="break-all rounded-md bg-(--surface) px-2 py-1 text-xs">
119-
{fallbackText(user.id, "Unavailable")}
120-
</code>
121-
</CardContent>
122-
</Card>
123-
145+
<div className="grid gap-4 md:grid-cols-2">
124146
<Card className="group relative overflow-hidden border-2 transition-all duration-200 hover:-translate-y-1 hover:shadow-[0_18px_34px_rgba(42,32,18,0.12)] focus-within:-translate-y-1 focus-within:shadow-[0_18px_34px_rgba(42,32,18,0.12)]">
125147
<div className="pointer-events-none absolute inset-x-0 top-0 h-1 bg-linear-to-r from-(--accent)/70 via-[#cb8b39]/50 to-(--accent)/70" />
126148
<Link

app/admin/(protected)/invitation-codes/page.tsx

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export default async function AdminInvitationCodesPage({
6363
invitationCodes.map((invitationCode) => (
6464
<article
6565
key={invitationCode.id}
66-
className="rounded-2xl border border-(--border) bg-(--surface) p-5"
66+
className="flex h-full flex-col rounded-2xl border border-(--border) bg-(--surface) p-5"
6767
>
6868
<div className="flex flex-wrap items-start justify-between gap-4">
6969
<div className="space-y-2">
@@ -83,18 +83,6 @@ export default async function AdminInvitationCodesPage({
8383
: ""}
8484
</p>
8585
</div>
86-
87-
<form action={updateInvitationCodeStatusAction}>
88-
<input type="hidden" name="codeId" value={invitationCode.id} />
89-
<input
90-
type="hidden"
91-
name="isActive"
92-
value={invitationCode.isActive ? "false" : "true"}
93-
/>
94-
<Button type="submit" variant="secondary">
95-
{invitationCode.isActive ? "Deactivate" : "Activate"}
96-
</Button>
97-
</form>
9886
</div>
9987

10088
<dl className="mt-4 grid gap-4 text-sm text-(--muted) md:grid-cols-4">
@@ -145,6 +133,20 @@ export default async function AdminInvitationCodesPage({
145133
</ul>
146134
)}
147135
</div>
136+
137+
<div className="mt-auto flex justify-end border-t border-(--border) pt-4">
138+
<form action={updateInvitationCodeStatusAction}>
139+
<input type="hidden" name="codeId" value={invitationCode.id} />
140+
<input
141+
type="hidden"
142+
name="isActive"
143+
value={invitationCode.isActive ? "false" : "true"}
144+
/>
145+
<Button type="submit" variant="secondary">
146+
{invitationCode.isActive ? "Deactivate" : "Activate"}
147+
</Button>
148+
</form>
149+
</div>
148150
</article>
149151
))
150152
)}

0 commit comments

Comments
 (0)