Implemented user profile display in the chat interface: - Created UserMenu component with avatar, handle display, and dropdown - Added /api/user/profile endpoint to fetch user data from Bluesky - Added /api/auth/logout endpoint to clear auth cookie - Integrated UserMenu into chat page header - Shows user's ATproto avatar with initials fallback - Dropdown menu displays user info and logout option - Fixed JWT import to use verifySurrealJwt 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
100 lines
2.3 KiB
TypeScript
100 lines
2.3 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { Menu, Avatar, UnstyledButton, Group, Text } from '@mantine/core';
|
|
import { useRouter } from 'next/navigation';
|
|
|
|
interface UserProfile {
|
|
did: string;
|
|
handle: string;
|
|
displayName: string | null;
|
|
avatar: string | null;
|
|
}
|
|
|
|
export function UserMenu() {
|
|
const router = useRouter();
|
|
const [profile, setProfile] = useState<UserProfile | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
// Fetch user profile on mount
|
|
fetch('/api/user/profile')
|
|
.then((res) => res.json())
|
|
.then((data) => {
|
|
if (!data.error) {
|
|
setProfile(data);
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
console.error('Failed to fetch profile:', error);
|
|
})
|
|
.finally(() => {
|
|
setLoading(false);
|
|
});
|
|
}, []);
|
|
|
|
const handleLogout = async () => {
|
|
try {
|
|
await fetch('/api/auth/logout', { method: 'POST' });
|
|
router.push('/login');
|
|
} catch (error) {
|
|
console.error('Logout failed:', error);
|
|
}
|
|
};
|
|
|
|
if (loading || !profile) {
|
|
return (
|
|
<Avatar radius="xl" size="md" color="gray">
|
|
?
|
|
</Avatar>
|
|
);
|
|
}
|
|
|
|
// Get display name or handle
|
|
const displayText = profile.displayName || profile.handle;
|
|
// Get initials for fallback
|
|
const initials = profile.displayName
|
|
? profile.displayName
|
|
.split(' ')
|
|
.map((n) => n[0])
|
|
.join('')
|
|
.toUpperCase()
|
|
.slice(0, 2)
|
|
: profile.handle.slice(0, 2).toUpperCase();
|
|
|
|
return (
|
|
<Menu shadow="md" width={200} position="bottom-end">
|
|
<Menu.Target>
|
|
<UnstyledButton>
|
|
<Group gap="xs">
|
|
<Avatar
|
|
src={profile.avatar}
|
|
alt={displayText}
|
|
radius="xl"
|
|
size="md"
|
|
style={{ cursor: 'pointer' }}
|
|
>
|
|
{initials}
|
|
</Avatar>
|
|
</Group>
|
|
</UnstyledButton>
|
|
</Menu.Target>
|
|
|
|
<Menu.Dropdown>
|
|
<Menu.Label>
|
|
<Text size="xs" fw={600}>
|
|
{displayText}
|
|
</Text>
|
|
<Text size="xs" c="dimmed">
|
|
@{profile.handle}
|
|
</Text>
|
|
</Menu.Label>
|
|
<Menu.Divider />
|
|
<Menu.Item onClick={handleLogout} c="red">
|
|
Log out
|
|
</Menu.Item>
|
|
</Menu.Dropdown>
|
|
</Menu>
|
|
);
|
|
}
|