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 side bar

+190 -13
+4 -5
src/components/Header.svelte
··· 46 46 {/if} 47 47 48 48 <div 49 - class="mx-auto mb-4 flex max-w-2xl items-center justify-between rounded-xl border border-ctp-surface1 bg-ctp-surface0 px-2 py-2" 49 + class="mx-auto mb-4 flex max-w-2xl items-center justify-between rounded-xl border border-ctp-surface1 px-2 py-2" 50 50 > 51 - <div class="ml-3 flex cursor-pointer items-center"> 52 - <Cat class="mr-4 text-ctp-text" /> 53 - <h1 class="text-xl font-bold text-ctp-text">meowzone</h1> 54 - </div> 51 + <a class="ml-3 cursor-pointer" href="/"> 52 + <Cat class="text-ctp-subtext1 hover:text-ctp-text" /> 53 + </a> 55 54 <div class="relative"> 56 55 <Button 57 56 variant="ghost"
+145
src/components/Sidebar.svelte
··· 1 + <script lang="ts"> 2 + import { session } from '$lib/stores/auth'; 3 + import { accentColours, inputAccentColor, type InputAccentColor } from '$lib/stores/ui'; 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'; 14 + import Button from './ui/Button.svelte'; 15 + import type { ComponentType } from 'svelte'; 16 + import { navItems } from '$lib/nav'; 17 + 18 + let menuOpen = false; 19 + 20 + async function handleLogout() { 21 + if ($session) { 22 + await $session.session.session.signOut(); 23 + } 24 + session.set(null); 25 + window.location.href = '/login'; 26 + } 27 + 28 + function toggleMenu() { 29 + menuOpen = !menuOpen; 30 + } 31 + 32 + function closeMenu() { 33 + menuOpen = false; 34 + } 35 + 36 + function setInputAccentColor(color: InputAccentColor) { 37 + inputAccentColor.set(color); 38 + } 39 + 40 + const profileQuery = createQuery(() => ({ 41 + queryKey: ['profile'], 42 + queryFn: async () => { 43 + if (!$session) { 44 + throw new Error('No active session'); 45 + } 46 + const profile = await $session.agent.getProfile({ actor: $session.agent.did! }); 47 + return profile.data; 48 + }, 49 + enabled: !!$session 50 + })); 51 + </script> 52 + 53 + {#if menuOpen} 54 + <button type="button" class="fixed inset-0 z-40" on:click={closeMenu} aria-label="Close menu" 55 + ></button> 56 + {/if} 57 + 58 + <div class=""> 59 + <nav class="gap-1 p-4"> 60 + <div class="relative"> 61 + <Button 62 + variant="ghost" 63 + size="icon" 64 + on:click={toggleMenu} 65 + className="items-center" 66 + aria-label="Menu" 67 + > 68 + <div class="size-8"> 69 + {#if profileQuery.isLoading} 70 + <div class="size-8 rounded-full bg-ctp-surface1"></div> 71 + {:else if profileQuery.isError || !profileQuery.data} 72 + <div class="size-8 rounded-full bg-ctp-red/20"> 73 + <TriangleAlert class="size-full p-2 text-ctp-red" /> 74 + </div> 75 + {:else if profileQuery.data && $session} 76 + <img 77 + src={profileQuery.data.avatar} 78 + alt={profileQuery.data.displayName || profileQuery.data.handle} 79 + class="size-8 rounded-full" 80 + /> 81 + {/if} 82 + </div> 83 + <div class="ml-1 flex flex-col items-start text-left text-sm"> 84 + <h2 class="truncate text-sm font-medium text-ctp-text"> 85 + {profileQuery.data?.displayName || profileQuery.data?.handle} 86 + </h2> 87 + <p class="truncate text-xs text-ctp-subtext0"> 88 + @{profileQuery.data?.handle} 89 + </p> 90 + </div> 91 + </Button> 92 + 93 + {#if menuOpen} 94 + <div 95 + 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" 96 + > 97 + <div class="mb-1 flex items-center gap-2 px-2 py-1 text-xs text-ctp-subtext0"> 98 + <Paintbrush size={16} /> 99 + <span>Theme</span> 100 + </div> 101 + <div class="flex gap-2 px-2 py-1"> 102 + {#each accentColours as colour} 103 + <button 104 + class="size-5 cursor-pointer rounded-full hover:opacity-80" 105 + class:bg-ctp-sapphire={colour === 'sapphire'} 106 + class:bg-ctp-mauve={colour === 'mauve'} 107 + class:bg-ctp-lavender={colour === 'lavender'} 108 + class:ring-2={$inputAccentColor === colour} 109 + class:ring-ctp-overlay1={$inputAccentColor === colour} 110 + class:text-ctp-subtext0={$inputAccentColor !== colour} 111 + on:click={() => setInputAccentColor(colour)} 112 + aria-label={`Set input accent color to ${colour}`} 113 + > 114 + </button> 115 + {/each} 116 + </div> 117 + <div class="my-2 border-t border-ctp-surface1"></div> 118 + <Button 119 + variant="danger" 120 + fullWidth={true} 121 + size="sm" 122 + on:click={handleLogout} 123 + className="justify-start py-1.5 rounded-sm" 124 + > 125 + <LogOut size={18} class="mr-3" /> 126 + <span>Logout</span> 127 + </Button> 128 + </div> 129 + {/if} 130 + </div> 131 + <div class="py-1"></div> 132 + {#each navItems as item} 133 + {@const Icon = item.icon} 134 + 135 + <a 136 + href={item.href} 137 + class="flex items-center gap-2 rounded-lg px-3 py-2 text-sm text-ctp-subtext0 138 + transition-colors hover:bg-ctp-surface0 hover:text-ctp-text" 139 + > 140 + <span><Icon size={16} /></span> 141 + <span>{item.label}</span> 142 + </a> 143 + {/each} 144 + </nav> 145 + </div>
+13
src/lib/nav.ts
··· 1 + import { ScrollIcon, ShieldAlertIcon } from "lucide-svelte"; 2 + import type { ComponentType } from "svelte"; 3 + 4 + export type NavItem = { 5 + href: string; 6 + label: string; 7 + icon: ComponentType; 8 + }; 9 + 10 + export const navItems: NavItem[] = [ 11 + { href: '/', icon: ShieldAlertIcon, label: 'Reports' }, 12 + { href: '/events', icon: ScrollIcon, label: 'Events' } 13 + ];
+13 -3
src/routes/+layout.svelte
··· 11 11 import { createQuery, QueryClient, QueryClientProvider } from '@tanstack/svelte-query'; 12 12 import LabelerSetup from '../components/LabelerSetup.svelte'; 13 13 import { moderationConfigQueryOptions } from '$lib/queries/moderation'; 14 + import Sidebar from '$components/Sidebar.svelte'; 15 + import Header from '$components/Header.svelte'; 14 16 15 17 let { children } = $props(); 16 18 let isInitialized = $state(false); ··· 66 68 {#if labelerConfig.isLoading} 67 69 <div class="flex min-h-screen items-center justify-center"> 68 70 <div class="text-center"> 69 - <LoaderCircle class="mx-auto mb-4 h-8 w-8 animate-spin text-ctp-text" /> 71 + <LoaderCircle class="mx-auto mb-4 h-8 w-8 animate-spin text-ctp-subtext0" /> 70 72 </div> 71 73 </div> 72 74 {:else if !labelerConfig.data?.plcLabeler || !labelerConfig.data?.moderatorRecord} 73 75 <LabelerSetup /> 74 76 {:else if !redirecting} 75 - {@render children()} 77 + <div class="w-full bg-ctp-base md:flex"> 78 + <div class="hidden md:block"> 79 + <Sidebar /> 80 + </div> 81 + <div class="px-4 pt-4 md:hidden"> 82 + <Header /> 83 + </div> 84 + {@render children()} 85 + </div> 76 86 {:else} 77 87 <div class="flex min-h-screen items-center justify-center"> 78 88 <div class="text-center"> 79 - <LoaderCircle class="mx-auto mb-4 h-8 w-8 animate-spin text-ctp-text" /> 89 + <LoaderCircle class="mx-auto mb-4 h-8 w-8 animate-spin text-ctp-subtext0" /> 80 90 </div> 81 91 </div> 82 92 {/if}
+15 -5
src/routes/+page.svelte
··· 1 1 <script lang="ts"> 2 2 import { session, type AuthStore } from '$lib/stores/auth'; 3 - import { LoaderCircle, ExternalLink, TriangleAlert, CheckIcon, CircleCheck } from 'lucide-svelte'; 3 + import { 4 + LoaderCircle, 5 + ExternalLink, 6 + TriangleAlert, 7 + CheckIcon, 8 + CircleCheck, 9 + Scale 10 + } from 'lucide-svelte'; 4 11 import { createInfiniteQuery, createQuery, QueryClient } from '@tanstack/svelte-query'; 5 12 import { AtUri, ComAtprotoRepoStrongRef } from '@atproto/api'; 6 13 import Header from '../components/Header.svelte'; ··· 11 18 import { isRepoRef } from '@atproto/api/dist/client/types/com/atproto/admin/defs'; 12 19 import { formatDate } from '$lib/time'; 13 20 import Post from '$components/view/Post.svelte'; 21 + import { page } from '$app/state'; 22 + import Sidebar from '$components/Sidebar.svelte'; 14 23 15 24 let selectedDid: string | null = null; 16 25 let selectedPostUri: AtUri | null = null; ··· 74 83 } 75 84 </script> 76 85 77 - <div class="min-h-screen bg-ctp-base p-4"> 78 - <Header></Header> 86 + <div class="min-h-screen w-full bg-ctp-base p-4"> 79 87 {#if selectedDid} 80 88 {#key selectedDid} 81 89 <UserModal isOpen={true} did={selectedDid} onClose={() => (selectedDid = null)} /> ··· 98 106 <p>{reportsQuery.error?.message}</p> 99 107 </div> 100 108 {:else if reportsQuery.data} 101 - <h2 class="pb-5 text-xl font-bold text-ctp-text">Recent Reports</h2> 109 + <h2 class="pb-5 text-xl font-medium text-ctp-text">Recent Reports</h2> 102 110 <ul class="space-y-4 text-ctp-text"> 103 111 {#each reportsQuery.data.pages as page} 104 112 {#each page.subjectStatuses as report} ··· 106 114 <li class="rounded-lg border border-ctp-surface1 bg-ctp-surface0 p-4"> 107 115 <div class="flex items-center justify-between"> 108 116 <div class="flex items-center gap-2"> 109 - {#if report.reviewState == 'tools.ozone.moderation.defs#reviewClosed'} 117 + {#if report.appealed} 118 + <Scale class="size-5 text-ctp-yellow" /> 119 + {:else if report.reviewState == 'tools.ozone.moderation.defs#reviewClosed'} 110 120 <CircleCheck class="size-5 text-ctp-green" /> 111 121 {:else} 112 122 <TriangleAlert class="size-5 text-ctp-yellow" />