add blog management system with admin tools

This commit is contained in:
2025-07-11 14:02:58 +02:00
parent b10493629c
commit 5b1827809c
15 changed files with 885 additions and 201 deletions

View File

@@ -1,200 +0,0 @@
'use client'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { LogIn, LogOut, Menu, Moon, Settings, Sun, User, X } from 'lucide-react'
import { signIn, signOut, useSession } from 'next-auth/react'
import { useTheme } from 'next-themes'
import Link from 'next/link'
import { useState } from 'react'
export function MainHeader() {
const { setTheme, theme } = useTheme()
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
const { data: session, status } = useSession()
const isAuthenticated = status === 'authenticated'
return (
<header className='sticky top-0 z-40 border-gray-200 border-b bg-white dark:border-zinc-800 dark:bg-zinc-900'>
<div className='container mx-auto px-4'>
<div className='flex h-16 items-center justify-between'>
{/* Logo and Brand */}
<div className='flex items-center'>
<Link href='/' className='flex items-center gap-2'>
<span className='hidden font-bold text-xl sm:inline-block'>
Balatro Multiplayer
</span>
</Link>
</div>
{/* Desktop Navigation */}
<nav className='hidden items-center space-x-6 md:flex'>
<Link
href='/'
className='font-medium text-gray-700 text-sm transition-colors hover:text-violet-500 dark:text-zinc-300 dark:hover:text-violet-400'
>
Home
</Link>
<Link
href='/leaderboards'
className='font-medium text-gray-700 text-sm transition-colors hover:text-violet-500 dark:text-zinc-300 dark:hover:text-violet-400'
>
Leaderboards
</Link>
{/*<Link*/}
{/* href='/matches'*/}
{/* className='font-medium text-gray-700 text-sm transition-colors hover:text-violet-500 dark:text-zinc-300 dark:hover:text-violet-400'*/}
{/*>*/}
{/* Matches*/}
{/*</Link>*/}
<Link
href='/about'
className='font-medium text-gray-700 text-sm transition-colors hover:text-violet-500 dark:text-zinc-300 dark:hover:text-violet-400'
>
About
</Link>
</nav>
{/* Actions: Theme Toggle, Sign In/User Menu */}
<div className='flex items-center gap-2'>
{/* Theme Toggle */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant='ghost' size='icon' className='h-9 w-9'>
<Sun className='dark:-rotate-90 h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:scale-0' />
<Moon className='absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100' />
<span className='sr-only'>Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuItem onClick={() => setTheme('light')}>
Light
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('dark')}>
Dark
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('system')}>
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{/* Sign In Button or User Menu */}
{isAuthenticated && session?.user && session.user.name ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant='ghost'
className='relative h-9 w-9 rounded-full'
>
<Avatar className='h-9 w-9'>
<AvatarImage
src={session.user.image ?? ''}
alt={session.user.name}
/>
<AvatarFallback className='bg-violet-50 text-violet-600 dark:bg-violet-900/50 dark:text-violet-300'>
{session.user.name?.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className='w-56' align='end' forceMount>
<div className='flex items-center justify-start gap-2 p-2'>
<div className='flex flex-col space-y-1 leading-none'>
<p className='font-medium'>{session.user.name}</p>
</div>
</div>
<DropdownMenuItem asChild>
<Link
href={`/players/${session.user.discord_id}`}
className='flex w-full items-center'
>
<User className='mr-2 h-4 w-4' />
<span>Profile</span>
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link href='/settings' className='flex w-full items-center'>
<Settings className='mr-2 h-4 w-4' />
<span>Settings</span>
</Link>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => signOut()}>
<LogOut className='mr-2 h-4 w-4' />
<span>Log out</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
) : (
<Button
variant='default'
size='sm'
className='bg-violet-600 hover:bg-violet-700 dark:text-zinc-100'
onClick={() => signIn('discord')}
>
<LogIn className='mr-2 h-4 w-4' />
Sign In
</Button>
)}
{/* Mobile Menu Toggle */}
<Button
variant='ghost'
size='icon'
className='h-9 w-9 md:hidden'
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
>
{mobileMenuOpen ? (
<X className='h-5 w-5' />
) : (
<Menu className='h-5 w-5' />
)}
<span className='sr-only'>Toggle menu</span>
</Button>
</div>
</div>
</div>
{/* Mobile Navigation */}
{mobileMenuOpen && (
<div className='border-gray-200 border-t px-4 py-4 md:hidden dark:border-zinc-800'>
<nav className='flex flex-col space-y-4'>
<Link
href='/'
className='font-medium text-gray-700 text-sm transition-colors hover:text-violet-500 dark:text-zinc-300 dark:hover:text-violet-400'
onClick={() => setMobileMenuOpen(false)}
>
Home
</Link>
<Link
href='/leaderboards'
className='font-medium text-gray-700 text-sm transition-colors hover:text-violet-500 dark:text-zinc-300 dark:hover:text-violet-400'
onClick={() => setMobileMenuOpen(false)}
>
Leaderboards
</Link>
{/*<Link*/}
{/* href='/matches'*/}
{/* className='font-medium text-gray-700 text-sm transition-colors hover:text-violet-500 dark:text-zinc-300 dark:hover:text-violet-400'*/}
{/* onClick={() => setMobileMenuOpen(false)}*/}
{/*>*/}
{/* Matches*/}
{/*</Link>*/}
<Link
href='/about'
className='font-medium text-gray-700 text-sm transition-colors hover:text-violet-500 dark:text-zinc-300 dark:hover:text-violet-400'
onClick={() => setMobileMenuOpen(false)}
>
About
</Link>
</nav>
</div>
)}
</header>
)
}

View File

@@ -0,0 +1,97 @@
'use client'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Textarea } from '@/components/ui/textarea'
import { useEffect, useState } from 'react'
import ReactMarkdown from 'react-markdown'
interface MarkdownEditorProps {
value: string
onChange: (value: string) => void
placeholder?: string
minHeight?: string
}
export function MarkdownEditor({
value,
onChange,
placeholder = 'Write your content here...',
minHeight = '400px',
}: MarkdownEditorProps) {
const [activeTab, setActiveTab] = useState<string>('write')
const [previewContent, setPreviewContent] = useState<string>(value)
// Always update preview content when value changes
useEffect(() => {
setPreviewContent(value)
}, [value])
return (
<div className='w-full'>
{/* Tabs are only visible on mobile */}
<div className='md:hidden'>
<Tabs
defaultValue='write'
value={activeTab}
onValueChange={setActiveTab}
className='w-full'
>
<TabsList className='grid w-full grid-cols-2'>
<TabsTrigger value='write'>Write</TabsTrigger>
<TabsTrigger value='preview'>Preview</TabsTrigger>
</TabsList>
<TabsContent value='write' className='mt-2'>
<Textarea
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
className='min-h-[400px] font-mono'
style={{ minHeight }}
/>
</TabsContent>
<TabsContent value='preview' className='mt-2'>
<div
className='prose prose-sm dark:prose-invert w-full max-w-none rounded-md border p-4'
style={{ minHeight }}
>
{previewContent ? (
<ReactMarkdown>{previewContent}</ReactMarkdown>
) : (
<p className='text-muted-foreground'>Nothing to preview</p>
)}
</div>
</TabsContent>
</Tabs>
</div>
{/* Side-by-side layout for desktop */}
<div className='hidden md:grid md:grid-cols-2 md:gap-4'>
<div className='w-full'>
<div className='mb-2 font-medium'>Write</div>
<Textarea
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
className='min-h-[400px] font-mono'
style={{ minHeight }}
/>
</div>
<div className='w-full'>
<div className='mb-2 font-medium'>Preview</div>
<div
className='prose prose-sm dark:prose-invert w-full max-w-none rounded-md border p-4'
style={{ minHeight }}
>
{previewContent ? (
<ReactMarkdown>{previewContent}</ReactMarkdown>
) : (
<p className='text-muted-foreground'>Nothing to preview</p>
)}
</div>
</div>
</div>
</div>
)
}