login-seiten neu designt, homepage neu designt. "so funktioniert's" ergänzt und Demo-Seite hinzugefügt. Paketansicht in mobile verbessert.
This commit is contained in:
@@ -8,11 +8,11 @@ import { useLocalizedRoutes } from '@/hooks/useLocalizedRoutes';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
|
||||
import { Sheet, SheetClose, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion';
|
||||
import { Sun, Moon, Menu, X, ChevronRight } from 'lucide-react';
|
||||
import { Sun, Moon, Menu, X, Languages, UserRound } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
NavigationMenu,
|
||||
@@ -31,10 +31,15 @@ const Header: React.FC = () => {
|
||||
const { localizedPath } = useLocalizedRoutes();
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
|
||||
const toggleTheme = () => {
|
||||
const newAppearance = appearance === 'dark' ? 'light' : 'dark';
|
||||
updateAppearance(newAppearance);
|
||||
const setTheme = useCallback((mode: 'light' | 'dark') => {
|
||||
if (appearance !== mode) {
|
||||
updateAppearance(mode);
|
||||
}
|
||||
setMobileMenuOpen(false);
|
||||
}, [appearance, updateAppearance]);
|
||||
|
||||
const toggleTheme = () => {
|
||||
setTheme(appearance === 'dark' ? 'light' : 'dark');
|
||||
};
|
||||
|
||||
const handleLanguageChange = useCallback(async (value: string) => {
|
||||
@@ -68,49 +73,61 @@ const Header: React.FC = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const navItems = useMemo(() => ([
|
||||
{
|
||||
key: 'home',
|
||||
label: t('header.home', 'Home'),
|
||||
href: localizedPath('/'),
|
||||
},
|
||||
{
|
||||
key: 'packages',
|
||||
label: t('header.packages', 'Pakete'),
|
||||
href: localizedPath('/packages'),
|
||||
},
|
||||
{
|
||||
key: 'blog',
|
||||
label: t('header.blog', 'Blog'),
|
||||
href: localizedPath('/blog'),
|
||||
},
|
||||
{
|
||||
key: 'occasions',
|
||||
label: t('header.occasions.label', 'Anlässe'),
|
||||
children: [
|
||||
{
|
||||
key: 'wedding',
|
||||
label: t('header.occasions.wedding', 'Hochzeit'),
|
||||
href: localizedPath('/anlaesse/hochzeit'),
|
||||
},
|
||||
{
|
||||
key: 'birthday',
|
||||
label: t('header.occasions.birthday', 'Geburtstag'),
|
||||
href: localizedPath('/anlaesse/geburtstag'),
|
||||
},
|
||||
{
|
||||
key: 'corporate',
|
||||
label: t('header.occasions.corporate', 'Firmenevent'),
|
||||
href: localizedPath('/anlaesse/firmenevent'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'contact',
|
||||
label: t('header.contact', 'Kontakt'),
|
||||
href: localizedPath('/kontakt'),
|
||||
},
|
||||
]), [localizedPath, t]);
|
||||
const ctaHref = localizedPath('/demo');
|
||||
|
||||
const navItems = useMemo(() => {
|
||||
const homeHref = localizedPath('/');
|
||||
const howItWorksHref = localizedPath('/so-funktionierts');
|
||||
|
||||
return [
|
||||
{
|
||||
key: 'howItWorks',
|
||||
label: t('header.how_it_works', "So geht's"),
|
||||
href: howItWorksHref,
|
||||
},
|
||||
{
|
||||
key: 'packages',
|
||||
label: t('header.packages', 'Pakete'),
|
||||
href: localizedPath('/packages'),
|
||||
},
|
||||
{
|
||||
key: 'occasions',
|
||||
label: t('header.occasions.label', 'Anlässe'),
|
||||
children: [
|
||||
{
|
||||
key: 'wedding',
|
||||
label: t('header.occasions.wedding', 'Hochzeiten'),
|
||||
href: localizedPath('/anlaesse/hochzeit'),
|
||||
},
|
||||
{
|
||||
key: 'birthday',
|
||||
label: t('header.occasions.birthday', 'Geburtstage'),
|
||||
href: localizedPath('/anlaesse/geburtstag'),
|
||||
},
|
||||
{
|
||||
key: 'corporate',
|
||||
label: t('header.occasions.corporate', 'Firmenfeiern'),
|
||||
href: localizedPath('/anlaesse/firmenevent'),
|
||||
},
|
||||
{
|
||||
key: 'confirmation',
|
||||
label: t('header.occasions.confirmation', 'Konfirmation/Jugendweihe'),
|
||||
href: localizedPath('/anlaesse/konfirmation'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'blog',
|
||||
label: t('header.blog', 'Blog'),
|
||||
href: localizedPath('/blog'),
|
||||
},
|
||||
{
|
||||
key: 'contact',
|
||||
label: t('header.contact', 'Kontakt'),
|
||||
href: localizedPath('/kontakt'),
|
||||
},
|
||||
];
|
||||
}, [localizedPath, t]);
|
||||
|
||||
const handleNavSelect = useCallback(() => setMobileMenuOpen(false), []);
|
||||
|
||||
@@ -125,25 +142,27 @@ const Header: React.FC = () => {
|
||||
</span>
|
||||
</Link>
|
||||
<NavigationMenu className="hidden lg:flex flex-1 justify-center" viewport={false}>
|
||||
<NavigationMenuList className="gap-2">
|
||||
<NavigationMenuList className="gap-1.5">
|
||||
{navItems.map((item) => (
|
||||
<NavigationMenuItem key={item.key}>
|
||||
{item.children ? (
|
||||
<>
|
||||
<NavigationMenuTrigger className="bg-transparent text-gray-700 hover:text-pink-600 hover:bg-pink-50 dark:text-gray-300 dark:hover:text-pink-400 dark:hover:bg-pink-950/20 font-sans-marketing !text-lg font-medium">
|
||||
<NavigationMenuTrigger className="bg-transparent px-3 py-1.5 !text-lg font-medium text-gray-700 hover:text-pink-600 hover:bg-pink-50 dark:text-gray-300 dark:hover:text-pink-400 dark:hover:bg-pink-950/20 font-sans-marketing">
|
||||
{item.label}
|
||||
</NavigationMenuTrigger>
|
||||
<NavigationMenuContent className="min-w-[220px] rounded-md border bg-popover p-3 shadow-lg">
|
||||
<NavigationMenuContent className="min-w-[260px] rounded-md border bg-popover p-3 shadow-lg">
|
||||
<ul className="flex flex-col gap-1">
|
||||
{item.children.map((child) => (
|
||||
<li key={child.key}>
|
||||
<NavigationMenuLink asChild>
|
||||
<Link
|
||||
href={child.href}
|
||||
className="flex items-center justify-between rounded-md px-3 py-2 !text-lg font-medium text-gray-700 transition hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-900/60 font-sans-marketing"
|
||||
className="flex w-full items-center rounded-md px-3 py-2 text-sm font-medium text-gray-700 transition hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-900/60 font-sans-marketing"
|
||||
>
|
||||
{child.label}
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
<span aria-hidden className="mr-2 flex h-4 w-4 shrink-0 items-center justify-center text-muted-foreground">
|
||||
›
|
||||
</span>
|
||||
<span className="whitespace-nowrap">{child.label}</span>
|
||||
</Link>
|
||||
</NavigationMenuLink>
|
||||
</li>
|
||||
@@ -157,7 +176,7 @@ const Header: React.FC = () => {
|
||||
href={item.href}
|
||||
className={cn(
|
||||
navigationMenuTriggerStyle(),
|
||||
"bg-transparent !text-lg font-medium text-gray-700 hover:bg-pink-50 hover:text-pink-600 dark:text-gray-300 dark:hover:bg-pink-950/20 dark:hover:text-pink-400 font-sans-marketing"
|
||||
"bg-transparent px-3 py-1.5 !text-lg font-medium text-gray-700 hover:bg-pink-50 hover:text-pink-600 dark:text-gray-300 dark:hover:bg-pink-950/20 dark:hover:text-pink-400 font-sans-marketing"
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
@@ -168,26 +187,49 @@ const Header: React.FC = () => {
|
||||
))}
|
||||
</NavigationMenuList>
|
||||
</NavigationMenu>
|
||||
<div className="hidden lg:flex items-center space-x-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={toggleTheme}
|
||||
className="h-8 w-8"
|
||||
>
|
||||
<Sun className={cn("h-4 w-4", appearance === "dark" && "hidden")} />
|
||||
<Moon className={cn("h-4 w-4", appearance !== "dark" && "hidden")} />
|
||||
<span className="sr-only">Theme Toggle</span>
|
||||
<div className="hidden lg:flex items-center space-x-2">
|
||||
<Button asChild size="sm" className="bg-[#FF5F87] hover:bg-[#ff4674] text-white shadow-md shadow-rose-500/20">
|
||||
<Link href={ctaHref} className="font-sans-marketing font-semibold px-3">
|
||||
{t('header.cta', 'Jetzt ausprobieren')}
|
||||
</Link>
|
||||
</Button>
|
||||
<Select value={i18n.language || 'de'} onValueChange={handleLanguageChange}>
|
||||
<SelectTrigger className="w-[70px] h-8">
|
||||
<SelectValue placeholder={t('common.ui.language_select')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="de">DE</SelectItem>
|
||||
<SelectItem value="en">EN</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
aria-label={t('header.utility', 'Darstellung und Sprache öffnen')}
|
||||
>
|
||||
<Languages className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-52" align="end" forceMount>
|
||||
<DropdownMenuLabel className="font-sans-marketing text-xs uppercase tracking-wide text-muted-foreground">
|
||||
{t('header.appearance', 'Darstellung')}
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuItem onClick={() => setTheme('light')} className="font-sans-marketing">
|
||||
<Sun className="mr-2 h-4 w-4" />
|
||||
{t('header.appearance_light', 'Hell')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme('dark')} className="font-sans-marketing">
|
||||
<Moon className="mr-2 h-4 w-4" />
|
||||
{t('header.appearance_dark', 'Dunkel')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel className="font-sans-marketing text-xs uppercase tracking-wide text-muted-foreground">
|
||||
{t('header.language', 'Sprache')}
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuRadioGroup value={i18n.language || 'de'} onValueChange={handleLanguageChange}>
|
||||
<DropdownMenuRadioItem value="de" className="font-sans-marketing">
|
||||
Deutsch
|
||||
</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value="en" className="font-sans-marketing">
|
||||
English
|
||||
</DropdownMenuRadioItem>
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
{auth.user ? (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
@@ -223,14 +265,16 @@ const Header: React.FC = () => {
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : (
|
||||
<>
|
||||
<Button asChild variant="ghost" size="icon" className="h-8 w-8">
|
||||
<Link
|
||||
href={localizedPath('/login')}
|
||||
className="text-gray-700 hover:text-pink-600 dark:text-gray-300 dark:hover:text-pink-400 font-medium transition-colors duration-200 font-sans-marketing"
|
||||
className="flex items-center justify-center text-gray-700 hover:text-pink-600 dark:text-gray-300 dark:hover:text-pink-400"
|
||||
aria-label={t('header.login')}
|
||||
>
|
||||
{t('header.login')}
|
||||
<UserRound className="h-4 w-4" />
|
||||
<span className="sr-only">{t('header.login')}</span>
|
||||
</Link>
|
||||
</>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center lg:hidden">
|
||||
@@ -279,11 +323,11 @@ const Header: React.FC = () => {
|
||||
<SheetClose asChild key={child.key}>
|
||||
<Link
|
||||
href={child.href}
|
||||
className="flex items-center justify-between rounded-md border border-transparent px-3 py-2 text-base font-medium text-gray-700 transition hover:border-gray-200 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-900/60 font-sans-marketing"
|
||||
className="flex w-full items-center rounded-md border border-transparent px-3 py-2 text-sm font-medium text-gray-700 transition hover:border-gray-200 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-900/60 font-sans-marketing"
|
||||
onClick={handleNavSelect}
|
||||
>
|
||||
<span aria-hidden className="mr-2 text-muted-foreground">›</span>
|
||||
<span>{child.label}</span>
|
||||
<ChevronRight className="h-4 w-4 text-muted-foreground" />
|
||||
</Link>
|
||||
</SheetClose>
|
||||
))}
|
||||
@@ -305,6 +349,15 @@ const Header: React.FC = () => {
|
||||
</nav>
|
||||
<Separator />
|
||||
<div className="flex flex-col gap-4">
|
||||
<SheetClose asChild>
|
||||
<Link
|
||||
href={ctaHref}
|
||||
className="rounded-full bg-[#FF5F87] px-4 py-3 text-center text-base font-semibold text-white shadow-md shadow-rose-500/20 transition hover:bg-[#ff4674] font-sans-marketing"
|
||||
onClick={handleNavSelect}
|
||||
>
|
||||
{t('header.cta', 'Jetzt ausprobieren')}
|
||||
</Link>
|
||||
</SheetClose>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium text-muted-foreground">Darstellung</span>
|
||||
<Button
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import AppLogoIcon from '@/components/app-logo-icon';
|
||||
import { home } from '@/routes';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { home, packages } from '@/routes';
|
||||
import { Link } from '@inertiajs/react';
|
||||
import { Sparkles, Camera, ShieldCheck } from 'lucide-react';
|
||||
import { type PropsWithChildren } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface AuthLayoutProps {
|
||||
name?: string;
|
||||
@@ -10,24 +13,105 @@ interface AuthLayoutProps {
|
||||
}
|
||||
|
||||
export default function AuthSimpleLayout({ children, title, description }: PropsWithChildren<AuthLayoutProps>) {
|
||||
return (
|
||||
<div className="flex min-h-svh flex-col items-center justify-center gap-6 bg-background p-6 md:p-10">
|
||||
<div className="w-full max-w-sm">
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<Link href={home()} className="flex flex-col items-center gap-2 font-medium">
|
||||
<div className="mb-1 flex h-9 w-9 items-center justify-center rounded-md">
|
||||
<AppLogoIcon className="size-9 fill-current text-[var(--foreground)] dark:text-white" />
|
||||
</div>
|
||||
<span className="sr-only">{title}</span>
|
||||
</Link>
|
||||
const { t } = useTranslation('auth');
|
||||
|
||||
<div className="space-y-2 text-center">
|
||||
<h1 className="text-xl font-medium">{title}</h1>
|
||||
<p className="text-center text-sm text-muted-foreground">{description}</p>
|
||||
const highlights = [
|
||||
{
|
||||
icon: Sparkles,
|
||||
title: t('login.highlights.moments', 'Momente in Echtzeit teilen'),
|
||||
description: t('login.highlights.moments_description', 'Uploads landen sofort in der Event-Galerie – ohne App-Download.'),
|
||||
},
|
||||
{
|
||||
icon: Camera,
|
||||
title: t('login.highlights.branding', 'Branding & Slideshows, die begeistern'),
|
||||
description: t('login.highlights.branding_description', 'Konfiguriere Slideshow, Wasserzeichen und Aufgaben für dein Event.'),
|
||||
},
|
||||
{
|
||||
icon: ShieldCheck,
|
||||
title: t('login.highlights.privacy', 'Sicherer Zugang über Tokens'),
|
||||
description: t('login.highlights.privacy_description', 'Eventzugänge bleiben geschützt – DSGVO-konform mit Join Tokens.'),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="relative min-h-svh overflow-hidden bg-slate-950">
|
||||
<div
|
||||
aria-hidden
|
||||
className="absolute inset-0 bg-[radial-gradient(ellipse_at_top,_rgba(255,137,170,0.35),_transparent_60%),radial-gradient(ellipse_at_bottom,_rgba(99,102,241,0.35),_transparent_55%)] blur-3xl"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-gray-950 via-gray-950/70 to-[#1a0f1f]" aria-hidden />
|
||||
|
||||
<div className="relative z-10 flex min-h-svh items-center justify-center px-4 py-12 sm:px-6 lg:px-8">
|
||||
<div className="w-full max-w-5xl">
|
||||
<div className="grid overflow-hidden rounded-3xl border border-white/15 bg-white/95 shadow-2xl shadow-fuchsia-500/10 backdrop-blur-2xl dark:border-gray-800/70 dark:bg-gray-950/85 md:grid-cols-[1.08fr_1fr]">
|
||||
<div className="relative hidden flex-col justify-between gap-10 overflow-hidden bg-gradient-to-br from-[#1d1937] via-[#2a1134] to-[#ff5f87] p-10 text-white md:flex">
|
||||
<div aria-hidden className="pointer-events-none absolute inset-0 opacity-45">
|
||||
<div className="absolute -inset-20 bg-[radial-gradient(circle_at_top,_rgba(255,255,255,0.4),_transparent_55%),radial-gradient(circle_at_bottom_left,_rgba(236,72,153,0.35),_transparent_60%)]" />
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 flex flex-col gap-6">
|
||||
<div className="flex items-center gap-3 text-xs font-semibold uppercase tracking-[0.45em] text-white/70">
|
||||
<Sparkles className="h-4 w-4" aria-hidden />
|
||||
<span className="font-sans-marketing">{t('login.hero_tagline', 'Event-Tech mit Herz')}</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h2 className="font-display text-3xl leading-tight sm:text-4xl">
|
||||
{t('login.hero_heading', 'Willkommen zurück bei Fotospiel')}
|
||||
</h2>
|
||||
<p className="text-sm text-white/80 sm:text-base">
|
||||
{t('login.hero_subheading', 'Verwalte Events, Galerien und Gästelisten in einem liebevoll gestalteten Dashboard.')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ul className="space-y-4">
|
||||
{highlights.map(({ icon: Icon, title: highlightTitle, description: highlightDescription }) => (
|
||||
<li key={highlightTitle} className="flex items-start gap-3">
|
||||
<span className="mt-1 flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-white/15 backdrop-blur">
|
||||
<Icon className="h-4 w-4" aria-hidden />
|
||||
</span>
|
||||
<span className="space-y-1">
|
||||
<p className="text-sm font-semibold tracking-tight sm:text-base">{highlightTitle}</p>
|
||||
<p className="text-xs text-white/70 sm:text-sm">{highlightDescription}</p>
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 flex items-center justify-between gap-4 rounded-2xl border border-white/15 bg-white/10 p-4 text-xs text-white/80 sm:text-sm">
|
||||
<div className="space-y-1">
|
||||
<p className="font-semibold">{t('login.hero_footer.headline', 'Noch kein Account?')}</p>
|
||||
<p>{t('login.hero_footer.subline', 'Entdecke unsere Packages und erlebe Fotospiel live.')}</p>
|
||||
</div>
|
||||
<Button asChild variant="secondary" className="h-10 rounded-full bg-white px-5 text-sm font-semibold text-gray-900 shadow-md shadow-white/30 transition hover:bg-white/90">
|
||||
<Link href={packages()}>{t('login.hero_footer.cta', 'Packages entdecken')}</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative bg-white/95 px-6 py-10 sm:px-10 dark:bg-gray-950/90">
|
||||
<div className="absolute inset-x-0 top-0 h-1 bg-gradient-to-r from-pink-400 via-fuchsia-400 to-sky-400" aria-hidden />
|
||||
<div className="relative z-10 flex flex-col gap-8">
|
||||
<div className="flex flex-col items-center gap-4 text-center">
|
||||
<Link href={home()} className="group flex flex-col items-center gap-3 font-medium">
|
||||
<span className="flex h-12 w-12 items-center justify-center rounded-2xl bg-gradient-to-br from-[#ff8ab4] to-[#a855f7] shadow-lg shadow-pink-400/40 transition duration-300 group-hover:scale-105">
|
||||
<AppLogoIcon className="size-8 fill-white" aria-hidden />
|
||||
</span>
|
||||
<span className="text-2xl font-semibold font-display text-gray-900 dark:text-white">Fotospiel</span>
|
||||
<span className="sr-only">{title}</span>
|
||||
</Link>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-2xl font-semibold tracking-tight text-gray-900 dark:text-white sm:text-3xl">{title}</h1>
|
||||
<p className="text-sm text-muted-foreground sm:text-base">{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user