feat(ST-507): add email input to settings and surface in user menu
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5ed3645ecb
commit
0dc907b75c
5 changed files with 29 additions and 10 deletions
|
|
@ -17,7 +17,7 @@ export default async function AppLayout({ children }: { children: React.ReactNod
|
||||||
const [user, userRoles] = await Promise.all([
|
const [user, userRoles] = await Promise.all([
|
||||||
prisma.user.findUnique({
|
prisma.user.findUnique({
|
||||||
where: { id: session.userId },
|
where: { id: session.userId },
|
||||||
select: { username: true, bio: true },
|
select: { username: true, email: true },
|
||||||
}),
|
}),
|
||||||
prisma.userRole.findMany({
|
prisma.userRole.findMany({
|
||||||
where: { user_id: session.userId },
|
where: { user_id: session.userId },
|
||||||
|
|
@ -40,7 +40,7 @@ export default async function AppLayout({ children }: { children: React.ReactNod
|
||||||
roles={roles}
|
roles={roles}
|
||||||
userId={session.userId}
|
userId={session.userId}
|
||||||
username={user.username}
|
username={user.username}
|
||||||
bio={user.bio}
|
email={user.email}
|
||||||
/>
|
/>
|
||||||
<MinWidthBanner />
|
<MinWidthBanner />
|
||||||
<main id="main-content" className="flex-1 flex flex-col overflow-y-auto min-h-0">
|
<main id="main-content" className="flex-1 flex flex-col overflow-y-auto min-h-0">
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ export default async function SettingsPage() {
|
||||||
const [user, userRoles, ownedProducts, memberships] = await Promise.all([
|
const [user, userRoles, ownedProducts, memberships] = await Promise.all([
|
||||||
prisma.user.findUnique({
|
prisma.user.findUnique({
|
||||||
where: { id: session.userId },
|
where: { id: session.userId },
|
||||||
select: { username: true, bio: true, bio_detail: true, avatar_data: true, updated_at: true },
|
select: { username: true, email: true, bio: true, bio_detail: true, avatar_data: true, updated_at: true },
|
||||||
}),
|
}),
|
||||||
prisma.userRole.findMany({ where: { user_id: session.userId } }),
|
prisma.userRole.findMany({ where: { user_id: session.userId } }),
|
||||||
prisma.product.findMany({
|
prisma.product.findMany({
|
||||||
|
|
@ -58,6 +58,7 @@ export default async function SettingsPage() {
|
||||||
<p className="text-sm text-muted-foreground">Niet beschikbaar in demo-modus.</p>
|
<p className="text-sm text-muted-foreground">Niet beschikbaar in demo-modus.</p>
|
||||||
) : (
|
) : (
|
||||||
<ProfileEditor
|
<ProfileEditor
|
||||||
|
email={user?.email ?? null}
|
||||||
bio={user?.bio ?? null}
|
bio={user?.bio ?? null}
|
||||||
bioDetail={user?.bio_detail ?? null}
|
bioDetail={user?.bio_detail ?? null}
|
||||||
hasAvatar={!!user?.avatar_data}
|
hasAvatar={!!user?.avatar_data}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { Textarea } from '@/components/ui/textarea'
|
||||||
import { updateProfileAction } from '@/actions/profile'
|
import { updateProfileAction } from '@/actions/profile'
|
||||||
|
|
||||||
interface ProfileEditorProps {
|
interface ProfileEditorProps {
|
||||||
|
email: string | null
|
||||||
bio: string | null
|
bio: string | null
|
||||||
bioDetail: string | null
|
bioDetail: string | null
|
||||||
hasAvatar: boolean
|
hasAvatar: boolean
|
||||||
|
|
@ -17,7 +18,7 @@ interface ProfileEditorProps {
|
||||||
const ALLOWED = ['image/jpeg', 'image/png', 'image/webp']
|
const ALLOWED = ['image/jpeg', 'image/png', 'image/webp']
|
||||||
const MAX_BYTES = 12 * 1024 * 1024
|
const MAX_BYTES = 12 * 1024 * 1024
|
||||||
|
|
||||||
export function ProfileEditor({ bio, bioDetail, hasAvatar, avatarVersion }: ProfileEditorProps) {
|
export function ProfileEditor({ email, bio, bioDetail, hasAvatar, avatarVersion }: ProfileEditorProps) {
|
||||||
const [state, formAction, isPending] = useActionState(updateProfileAction, null)
|
const [state, formAction, isPending] = useActionState(updateProfileAction, null)
|
||||||
const [avatarSrc, setAvatarSrc] = useState<string | null>(
|
const [avatarSrc, setAvatarSrc] = useState<string | null>(
|
||||||
hasAvatar ? `/api/profile/avatar?v=${avatarVersion}` : null
|
hasAvatar ? `/api/profile/avatar?v=${avatarVersion}` : null
|
||||||
|
|
@ -107,6 +108,23 @@ export function ProfileEditor({ bio, bioDetail, hasAvatar, avatarVersion }: Prof
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form action={formAction} className="space-y-4">
|
<form action={formAction} className="space-y-4">
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<label htmlFor="email" className="text-xs font-medium text-foreground">
|
||||||
|
E-mailadres
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
type="email"
|
||||||
|
defaultValue={email ?? ''}
|
||||||
|
placeholder="jij@voorbeeld.nl"
|
||||||
|
maxLength={254}
|
||||||
|
disabled={isPending}
|
||||||
|
autoComplete="email"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">Optioneel — wordt getoond in je accountmenu</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<label htmlFor="bio" className="text-xs font-medium text-foreground">
|
<label htmlFor="bio" className="text-xs font-medium text-foreground">
|
||||||
Korte omschrijving
|
Korte omschrijving
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,10 @@ interface NavBarProps {
|
||||||
roles: string[]
|
roles: string[]
|
||||||
userId: string
|
userId: string
|
||||||
username: string
|
username: string
|
||||||
bio: string | null
|
email: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NavBar({ isDemo, roles, userId, username, bio }: NavBarProps) {
|
export function NavBar({ isDemo, roles, userId, username, email }: NavBarProps) {
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
const currentProduct = useProductStore(s => s.currentProduct)
|
const currentProduct = useProductStore(s => s.currentProduct)
|
||||||
|
|
||||||
|
|
@ -97,7 +97,7 @@ export function NavBar({ isDemo, roles, userId, username, bio }: NavBarProps) {
|
||||||
|
|
||||||
{/* Rechts: account-menu */}
|
{/* Rechts: account-menu */}
|
||||||
<div className="flex items-center gap-2 flex-1 justify-end">
|
<div className="flex items-center gap-2 flex-1 justify-end">
|
||||||
<UserMenu userId={userId} username={username} bio={bio} roles={roles} />
|
<UserMenu userId={userId} username={username} email={email} roles={roles} />
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -23,14 +23,14 @@ const ROLE_LABELS: Record<string, string> = {
|
||||||
interface UserMenuProps {
|
interface UserMenuProps {
|
||||||
userId: string
|
userId: string
|
||||||
username: string
|
username: string
|
||||||
bio: string | null
|
email: string | null
|
||||||
roles: string[]
|
roles: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UserMenu({ userId, username, bio, roles }: UserMenuProps) {
|
export function UserMenu({ userId, username, email, roles }: UserMenuProps) {
|
||||||
const initials = username.slice(0, 2).toUpperCase()
|
const initials = username.slice(0, 2).toUpperCase()
|
||||||
const roleLabels = roles.map((r) => ROLE_LABELS[r]).filter(Boolean)
|
const roleLabels = roles.map((r) => ROLE_LABELS[r]).filter(Boolean)
|
||||||
const subtitle = bio?.trim() ? bio.trim() : 'Lokaal account'
|
const subtitle = email?.trim() ? email.trim() : 'Lokaal account'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue