beatufitull front end for ozone modration ,, wit catpucoin and ebergarden !
ozone moderation
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

add mobile menu !! woag

+203 -149
+75 -44
src/components/Header.svelte
··· 2 2 import { session } from '$lib/stores/auth'; 3 3 import { accentColours, inputAccentColor, type InputAccentColor } from '$lib/stores/ui'; 4 4 import { createQuery } from '@tanstack/svelte-query'; 5 - import { Cat, LogOut, Paintbrush, TriangleAlert } from 'lucide-svelte'; 5 + import { Cat, LogOut, MenuIcon, Paintbrush, TriangleAlert } from 'lucide-svelte'; 6 6 import Button from './ui/Button.svelte'; 7 - 8 - let menuOpen = false; 7 + import PopupMenu from './ui/PopupMenu.svelte'; 8 + import { navItems } from '$lib/nav'; 9 + import { page } from '$app/state'; 9 10 10 11 async function handleLogout() { 11 12 if ($session) { ··· 14 15 localStorage.removeItem('meowzone-password-session'); 15 16 session.set(null); 16 17 window.location.href = '/login'; 17 - } 18 - 19 - function toggleMenu() { 20 - menuOpen = !menuOpen; 21 - } 22 - 23 - function closeMenu() { 24 - menuOpen = false; 25 18 } 26 19 27 20 function setInputAccentColor(color: InputAccentColor) { 28 21 inputAccentColor.set(color); 29 22 } 30 23 24 + function isActive(href: string) { 25 + return page.url.pathname === href; 26 + } 27 + 31 28 const profileQuery = createQuery(() => ({ 32 29 queryKey: ['profile'], 33 30 queryFn: async () => { ··· 41 38 })); 42 39 </script> 43 40 44 - {#if menuOpen} 45 - <button type="button" class="fixed inset-0 z-40" on:click={closeMenu} aria-label="Close menu" 46 - ></button> 47 - {/if} 48 - 49 41 <div 50 42 class="mx-auto mb-4 flex max-w-2xl items-center justify-between rounded-xl border border-ctp-surface1 px-2 py-2" 51 43 > 52 44 <a class="ml-3 cursor-pointer" href="/"> 53 45 <Cat class="text-ctp-subtext1 hover:text-ctp-text" /> 54 46 </a> 55 - <div class="relative"> 56 - <Button 57 - variant="ghost" 58 - size="icon" 59 - on:click={toggleMenu} 60 - className="items-center" 61 - aria-label="Menu" 47 + <div class="flex items-center gap-2"> 48 + <PopupMenu 49 + wrapperClass="relative" 50 + menuClass="absolute top-full right-0 z-50 w-48 rounded-md border border-ctp-surface1 bg-ctp-surface0 px-1.5 py-2 shadow-lg" 62 51 > 63 - <div class="size-8"> 64 - {#if profileQuery.isLoading} 65 - <div class="size-8 rounded-full bg-ctp-surface1"></div> 66 - {:else if profileQuery.isError || !profileQuery.data} 67 - <div class="size-8 rounded-full bg-ctp-red/20"> 68 - <TriangleAlert class="size-full p-2 text-ctp-red" /> 52 + <svelte:fragment slot="trigger" let:toggle> 53 + <Button 54 + variant="ghost" 55 + size="icon" 56 + on:click={toggle} 57 + className="items-center size-10" 58 + aria-label="Menu" 59 + > 60 + <div class="size-8"> 61 + {#if profileQuery.isLoading} 62 + <div class="size-8 rounded-full bg-ctp-surface1"></div> 63 + {:else if profileQuery.isError || !profileQuery.data} 64 + <div class="size-8 rounded-full bg-ctp-red/20"> 65 + <TriangleAlert class="size-full p-2 text-ctp-red" /> 66 + </div> 67 + {:else if profileQuery.data && $session} 68 + <img 69 + src={profileQuery.data.avatar} 70 + alt={profileQuery.data.displayName || profileQuery.data.handle} 71 + class="size-8 rounded-full" 72 + /> 73 + {/if} 69 74 </div> 70 - {:else if profileQuery.data && $session} 71 - <img 72 - src={profileQuery.data.avatar} 73 - alt={profileQuery.data.displayName || profileQuery.data.handle} 74 - class="size-8 rounded-full" 75 - /> 76 - {/if} 77 - </div> 78 - </Button> 75 + </Button> 76 + </svelte:fragment> 79 77 80 - {#if menuOpen} 81 - <div 82 - class="absolute top-full right-0 z-50 w-48 rounded-md border border-ctp-surface1 bg-ctp-surface0 px-1.5 py-2 shadow-lg" 83 - > 78 + <svelte:fragment slot="content"> 84 79 <div class="mb-1 flex items-center gap-2 px-2 py-1 text-xs text-ctp-subtext0"> 85 80 <Paintbrush size={16} /> 86 81 <span>Theme</span> ··· 112 107 <LogOut size={18} class="mr-3" /> 113 108 <span>Logout</span> 114 109 </Button> 115 - </div> 116 - {/if} 110 + </svelte:fragment> 111 + </PopupMenu> 112 + <PopupMenu 113 + wrapperClass="relative" 114 + menuClass="absolute top-full right-0 z-50 w-40 rounded-md border border-ctp-surface1 bg-ctp-surface0 px-1.5 py-2 shadow-lg" 115 + > 116 + <svelte:fragment slot="trigger" let:toggle> 117 + <Button 118 + variant="ghost" 119 + size="icon" 120 + on:click={toggle} 121 + className="items-center size-10" 122 + aria-label="Menu" 123 + > 124 + <MenuIcon class="size-5" /> 125 + </Button> 126 + </svelte:fragment> 127 + 128 + <svelte:fragment slot="content"> 129 + {#each navItems as item} 130 + {@const Icon = item.icon} 131 + 132 + <a href={item.href}> 133 + <Button 134 + variant="ghost" 135 + fullWidth={true} 136 + size="icon" 137 + className="justify-start rounded-md" 138 + > 139 + <span class="grid size-6 place-items-center"> 140 + <Icon size={16} /> 141 + </span> 142 + <span>{item.label}</span> 143 + </Button> 144 + </a> 145 + {/each} 146 + </svelte:fragment> 147 + </PopupMenu> 117 148 </div> 118 149 </div>
+98 -104
src/components/Sidebar.svelte
··· 2 2 import { session } from '$lib/stores/auth'; 3 3 import { accentColours, inputAccentColor, type InputAccentColor } from '$lib/stores/ui'; 4 4 import { createQuery } from '@tanstack/svelte-query'; 5 - import { 6 - Cat, 7 - ClipboardIcon, 8 - LogOut, 9 - Paintbrush, 10 - Scroll, 11 - ShieldAlert, 12 - TriangleAlert 13 - } from 'lucide-svelte'; 5 + import { CheckIcon, LogOut, Paintbrush, TriangleAlert } from 'lucide-svelte'; 14 6 import Button from './ui/Button.svelte'; 15 - import type { ComponentType } from 'svelte'; 16 7 import { navItems } from '$lib/nav'; 17 - 18 - let menuOpen = false; 8 + import { page } from '$app/stores'; 9 + import PopupMenu from './ui/PopupMenu.svelte'; 19 10 20 11 async function handleLogout() { 21 12 if ($session) { ··· 26 17 window.location.href = '/login'; 27 18 } 28 19 29 - function toggleMenu() { 30 - menuOpen = !menuOpen; 31 - } 32 - 33 - function closeMenu() { 34 - menuOpen = false; 20 + function isActive(href: string) { 21 + return $page.url.pathname === href; 35 22 } 36 23 37 24 function setInputAccentColor(color: InputAccentColor) { ··· 51 38 })); 52 39 </script> 53 40 54 - {#if menuOpen} 55 - <button type="button" class="fixed inset-0 z-40" on:click={closeMenu} aria-label="Close menu" 56 - ></button> 57 - {/if} 58 - 59 - <div class="mr-5 h-full border-r border-ctp-surface1 p-4"> 60 - <nav class="gap-1 p-4"> 61 - <div class="relative"> 62 - <Button 63 - variant="ghost" 64 - size="icon" 65 - on:click={toggleMenu} 66 - className="items-center" 67 - aria-label="Menu" 68 - > 69 - <div class="size-8"> 70 - {#if profileQuery.isLoading} 71 - <div class="size-8 rounded-full bg-ctp-surface1"></div> 72 - {:else if profileQuery.isError || !profileQuery.data} 73 - <div class="size-8 rounded-full bg-ctp-red/20"> 74 - <TriangleAlert class="size-full p-2 text-ctp-red" /> 41 + <aside class="mr-5 h-full w-64 border-r border-ctp-surface1 px-3 py-4"> 42 + <nav class="flex h-full flex-col gap-3 px-2"> 43 + <PopupMenu 44 + wrapperClass="relative" 45 + menuClass="absolute top-[calc(100%+0.5rem)] right-0 z-50 w-56 rounded-lg border border-ctp-surface1 bg-ctp-surface0 p-1.5" 46 + > 47 + <svelte:fragment slot="trigger" let:toggle> 48 + <button 49 + on:click={toggle} 50 + class="w-full cursor-pointer rounded-lg px-3 py-2.5 text-left hover:bg-ctp-surface0" 51 + aria-label="Menu" 52 + > 53 + <div class="flex items-center gap-3"> 54 + <div class="size-9 shrink-0"> 55 + {#if profileQuery.isLoading} 56 + <div class="size-9 animate-pulse rounded-full bg-ctp-surface1"></div> 57 + {:else if profileQuery.isError || !profileQuery.data} 58 + <div class="size-9 rounded-full bg-ctp-red/20"> 59 + <TriangleAlert class="size-full p-2 text-ctp-red" /> 60 + </div> 61 + {:else if profileQuery.data && $session} 62 + <img 63 + src={profileQuery.data.avatar} 64 + alt={profileQuery.data.displayName || profileQuery.data.handle} 65 + class="size-9 rounded-full object-cover" 66 + /> 67 + {/if} 68 + </div> 69 + <div class="min-w-0 flex-1"> 70 + <h2 class="truncate text-sm font-medium text-ctp-text"> 71 + {profileQuery.data?.displayName || profileQuery.data?.handle || 'Loading...'} 72 + </h2> 73 + <p class="truncate text-xs text-ctp-subtext0"> 74 + @{profileQuery.data?.handle || 'unknown'} 75 + </p> 75 76 </div> 76 - {:else if profileQuery.data && $session} 77 - <img 78 - src={profileQuery.data.avatar} 79 - alt={profileQuery.data.displayName || profileQuery.data.handle} 80 - class="size-8 rounded-full" 81 - /> 82 - {/if} 77 + </div> 78 + </button> 79 + </svelte:fragment> 80 + 81 + <svelte:fragment slot="content"> 82 + <div class="mb-1 flex items-center gap-2 px-2 py-1 text-xs font-medium text-ctp-subtext0"> 83 + <Paintbrush size={14} /> 84 + <span>Accent color</span> 83 85 </div> 84 - <div class="ml-1 flex flex-col items-start text-left text-sm"> 85 - <h2 class="truncate text-sm font-medium text-ctp-text"> 86 - {profileQuery.data?.displayName || profileQuery.data?.handle} 87 - </h2> 88 - <p class="truncate text-xs text-ctp-subtext0"> 89 - @{profileQuery.data?.handle} 90 - </p> 86 + <div class="flex gap-2 px-2 py-1"> 87 + {#each accentColours as colour} 88 + <button 89 + type="button" 90 + class="grid size-5 cursor-pointer place-items-center rounded-full transition-opacity hover:opacity-80" 91 + class:bg-ctp-sapphire={colour === 'sapphire'} 92 + class:bg-ctp-mauve={colour === 'mauve'} 93 + class:bg-ctp-lavender={colour === 'lavender'} 94 + on:click={() => setInputAccentColor(colour)} 95 + aria-label={`Set input accent color to ${colour}`} 96 + > 97 + {#if $inputAccentColor === colour} 98 + <CheckIcon size={14} class="text-ctp-crust" /> 99 + {/if} 100 + </button> 101 + {/each} 91 102 </div> 92 - </Button> 103 + <div class="my-2 border-t border-ctp-surface1"></div> 104 + <Button 105 + variant="danger" 106 + fullWidth={true} 107 + size="sm" 108 + on:click={handleLogout} 109 + className="justify-start rounded-md py-1.5" 110 + > 111 + <LogOut size={16} class="mr-2" /> 112 + <span>Logout</span> 113 + </Button> 114 + </svelte:fragment> 115 + </PopupMenu> 116 + 117 + <div class="flex flex-col gap-1.5"> 118 + {#each navItems as item} 119 + {@const Icon = item.icon} 93 120 94 - {#if menuOpen} 95 - <div 96 - class="absolute top-full right-0 z-50 w-48 rounded-md border border-ctp-surface1 bg-ctp-surface0 px-1.5 py-2 shadow-lg" 121 + <a 122 + href={item.href} 123 + class="group flex items-center gap-2.5 rounded-md px-3 py-2 text-sm" 124 + class:bg-ctp-surface0={isActive(item.href)} 125 + class:text-ctp-text={isActive(item.href)} 126 + class:font-medium={isActive(item.href)} 127 + class:text-ctp-subtext0={!isActive(item.href)} 128 + class:hover:bg-ctp-surface0={!isActive(item.href)} 129 + class:hover:bg-ctp-surface1={isActive(item.href)} 130 + class:hover:text-ctp-text={!isActive(item.href)} 97 131 > 98 - <div class="mb-1 flex items-center gap-2 px-2 py-1 text-xs text-ctp-subtext0"> 99 - <Paintbrush size={16} /> 100 - <span>Theme</span> 101 - </div> 102 - <div class="flex gap-2 px-2 py-1"> 103 - {#each accentColours as colour} 104 - <button 105 - class="size-5 cursor-pointer rounded-full hover:opacity-80" 106 - class:bg-ctp-sapphire={colour === 'sapphire'} 107 - class:bg-ctp-mauve={colour === 'mauve'} 108 - class:bg-ctp-lavender={colour === 'lavender'} 109 - class:ring-2={$inputAccentColor === colour} 110 - class:ring-ctp-overlay1={$inputAccentColor === colour} 111 - class:text-ctp-subtext0={$inputAccentColor !== colour} 112 - on:click={() => setInputAccentColor(colour)} 113 - aria-label={`Set input accent color to ${colour}`} 114 - > 115 - </button> 116 - {/each} 117 - </div> 118 - <div class="my-2 border-t border-ctp-surface1"></div> 119 - <Button 120 - variant="danger" 121 - fullWidth={true} 122 - size="sm" 123 - on:click={handleLogout} 124 - className="justify-start py-1.5 rounded-sm" 125 - > 126 - <LogOut size={18} class="mr-3" /> 127 - <span>Logout</span> 128 - </Button> 129 - </div> 130 - {/if} 132 + <span class="grid size-6 place-items-center"> 133 + <Icon size={16} /> 134 + </span> 135 + <span>{item.label}</span> 136 + </a> 137 + {/each} 131 138 </div> 132 - <div class="py-1"></div> 133 - {#each navItems as item} 134 - {@const Icon = item.icon} 135 - 136 - <a 137 - href={item.href} 138 - class="flex items-center gap-2 rounded-lg px-3 py-2 text-sm text-ctp-subtext0 139 - transition-colors hover:bg-ctp-surface0 hover:text-ctp-text" 140 - > 141 - <span><Icon size={16} /></span> 142 - <span>{item.label}</span> 143 - </a> 144 - {/each} 145 139 </nav> 146 - </div> 140 + </aside>
+29
src/components/ui/PopupMenu.svelte
··· 1 + <script lang="ts"> 2 + export let wrapperClass = 'relative'; 3 + export let menuClass = ''; 4 + export let overlayClass = 'fixed inset-0 z-40'; 5 + 6 + let open = false; 7 + 8 + function toggle() { 9 + open = !open; 10 + } 11 + 12 + function close() { 13 + open = false; 14 + } 15 + </script> 16 + 17 + {#if open} 18 + <button type="button" class={overlayClass} on:click={close} aria-label="Close menu"></button> 19 + {/if} 20 + 21 + <div class={wrapperClass}> 22 + <slot name="trigger" {open} {toggle} {close}></slot> 23 + 24 + {#if open} 25 + <div class={menuClass}> 26 + <slot name="content" {close}></slot> 27 + </div> 28 + {/if} 29 + </div>
+1 -1
src/routes/+page.svelte
··· 115 115 {#each reportsQuery.data.pages as page} 116 116 {#each page.subjectStatuses as report} 117 117 {@const subject = report.subject} 118 - <li class="rounded-lg border border-ctp-surface1 p-4"> 118 + <li class="rounded-lg border border-ctp-surface0 p-4"> 119 119 <div class="flex items-center justify-between"> 120 120 <div class="flex items-center gap-2"> 121 121 {#if report.appealed}