feat: Add user profile menu with logout functionality
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>
This commit is contained in:
99
components/UserMenu.tsx
Normal file
99
components/UserMenu.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user