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.

events page and pwetty ui

+171 -27
+1 -1
src/components/Sidebar.svelte
··· 56 56 ></button> 57 57 {/if} 58 58 59 - <div class=""> 59 + <div class="mr-5 h-full border-r border-ctp-surface1 p-4"> 60 60 <nav class="gap-1 p-4"> 61 61 <div class="relative"> 62 62 <Button
+35 -18
src/components/view/Events.svelte
··· 6 6 isModEventTag, 7 7 isModEventReport 8 8 } from '@atproto/api/dist/client/types/tools/ozone/moderation/defs'; 9 - import type { CreateQueryResult } from '@tanstack/svelte-query'; 9 + import type { 10 + CreateInfiniteQueryResult, 11 + CreateQueryResult, 12 + InfiniteData 13 + } from '@tanstack/svelte-query'; 10 14 import { LoaderCircle } from 'lucide-svelte'; 11 15 12 - let { eventsQuery } = $props<{ 13 - eventsQuery: CreateQueryResult<ToolsOzoneModerationQueryEvents.OutputSchema, Error>; 16 + let { eventsQuery, onUserClick, elevation } = $props<{ 17 + eventsQuery: 18 + | CreateQueryResult<ToolsOzoneModerationQueryEvents.OutputSchema, Error> 19 + | CreateInfiniteQueryResult< 20 + InfiniteData<ToolsOzoneModerationQueryEvents.OutputSchema, unknown>, 21 + Error 22 + >; 23 + elevation?: 'normal' | 'lower'; 24 + onUserClick?: (did: string) => void; 14 25 }>(); 15 26 const events = $derived.by(() => { 16 - const data = eventsQuery.data as 17 - | { events?: ToolsOzoneModerationDefs.ModEventView[] } 18 - | undefined; 19 - return Array.isArray(data?.events) ? data.events : []; 27 + if ('pages' in eventsQuery.data) { 28 + // infinite query data structure 29 + const pages = eventsQuery.data.pages as ToolsOzoneModerationQueryEvents.OutputSchema[]; 30 + return pages.flatMap((page) => (Array.isArray(page.events) ? page.events : [])); 31 + } else { 32 + // regular query data structure 33 + const data = eventsQuery.data as ToolsOzoneModerationQueryEvents.OutputSchema | undefined; 34 + return Array.isArray(data?.events) ? data.events : []; 35 + } 20 36 }); 21 37 22 38 function eventName(event: ToolsOzoneModerationDefs.ModEventView): string { ··· 35 51 } 36 52 </script> 37 53 38 - <div 39 - class=" 40 - overflow-y-auto rounded-lg border border-ctp-surface1 bg-ctp-surface0 41 - p-3 42 - md:max-h-[60vh] 43 - " 44 - > 45 - <h3 class="text-md mb-2 text-ctp-text">Moderation Events</h3> 46 - 54 + <div> 47 55 {#if eventsQuery.isLoading} 48 56 <div class="flex items-center justify-center p-10"> 49 57 <LoaderCircle class="h-8 w-8 animate-spin text-ctp-text" /> ··· 59 67 {:else} 60 68 <div class="space-y-2"> 61 69 {#each events as event} 62 - <div class="rounded-lg border border-ctp-surface1 bg-ctp-base p-2"> 70 + <div 71 + class={`rounded-lg border ${elevation == 'lower' ? 'border-ctp-surface1' : 'border-ctp-surface2'} p-2`} 72 + > 63 73 <div class="flex items-center justify-between gap-2"> 64 74 <p class="text-sm font-medium text-ctp-text">{eventName(event)}</p> 65 75 <p class="text-xs text-ctp-subtext0">{formatDate(event.createdAt)}</p> ··· 99 109 <p class="text-sm text-ctp-text">Reason: {event.event.comment}</p> 100 110 </div> 101 111 {/if} 102 - <p class="text-xs text-ctp-subtext1">by @{event.creatorHandle}</p> 112 + 113 + <p class="text-xs text-ctp-subtext0"> 114 + by <button 115 + class={onUserClick ? `cursor-pointer text-ctp-blue hover:underline` : ''} 116 + onclick={() => onUserClick?.(event.createdBy)} 117 + disabled={!onUserClick}>@{event.creatorHandle}</button 118 + > 119 + </p> 103 120 </div> 104 121 {/each} 105 122 </div>
+10
src/components/view/EventsContainer.svelte
··· 1 + <div 2 + class=" 3 + overflow-y-auto rounded-lg border border-ctp-surface1 bg-ctp-surface0 4 + p-3 5 + md:max-h-[60vh] 6 + " 7 + > 8 + <h3 class="text-md mb-2 text-ctp-text">Moderation Events</h3> 9 + <slot /> 10 + </div>
+8 -2
src/components/view/Post.svelte
··· 10 10 import Events from './Events.svelte'; 11 11 import Actions from './Actions.svelte'; 12 12 import type { RecordViewDetail } from '@atproto/api/dist/client/types/tools/ozone/moderation/defs'; 13 + import EventsContainer from './EventsContainer.svelte'; 13 14 14 15 let { 15 16 isOpen = false, ··· 213 214 {#if activePanel === 'post'} 214 215 {@render postPanel()} 215 216 {:else} 216 - <Events {eventsQuery} /> 217 + <EventsContainer> 218 + <Events {eventsQuery} /> 219 + </EventsContainer> 217 220 {/if} 218 221 </div> 219 222 220 223 <div class="hidden gap-3 md:grid md:grid-cols-2"> 221 224 {@render postPanel()} 222 - <Events {eventsQuery} /> 225 + <EventsContainer> 226 + <h3 class="text-md mb-2 text-ctp-text">Moderation Events</h3> 227 + <Events {eventsQuery} /> 228 + </EventsContainer> 223 229 </div> 224 230 </div> 225 231 </Modal>
+8 -2
src/components/view/User.svelte
··· 9 9 import Bluesky from '$lib/assets/bluesky.svelte'; 10 10 import Events from '$components/view/Events.svelte'; 11 11 import Actions from '$components/view/Actions.svelte'; 12 + import EventsContainer from './EventsContainer.svelte'; 12 13 13 14 let { 14 15 isOpen = false, ··· 199 200 {#if activePanel === 'profile'} 200 201 {@render profilePanel()} 201 202 {:else} 202 - <Events {eventsQuery} /> 203 + <EventsContainer> 204 + <Events {eventsQuery} /> 205 + </EventsContainer> 203 206 {/if} 204 207 </div> 205 208 206 209 <div class="hidden gap-3 md:grid md:grid-cols-2"> 207 210 {@render profilePanel()} 208 - <Events {eventsQuery} /> 211 + 212 + <EventsContainer> 213 + <Events {eventsQuery} /> 214 + </EventsContainer> 209 215 </div> 210 216 </div> 211 217 </Modal>
+8 -4
src/routes/+page.svelte
··· 6 6 TriangleAlert, 7 7 CheckIcon, 8 8 CircleCheck, 9 - Scale 9 + Scale, 10 + ShieldAlertIcon 10 11 } from 'lucide-svelte'; 11 12 import { createInfiniteQuery, createQuery, QueryClient } from '@tanstack/svelte-query'; 12 13 import { AtUri, ComAtprotoRepoStrongRef } from '@atproto/api'; ··· 93 94 <Post isOpen={true} uri={selectedPostUri} onClose={() => (selectedPostUri = null)} /> 94 95 {/key} 95 96 {/if} 96 - <div class="mx-auto max-w-2xl"> 97 + <div class="max-w-2xl"> 97 98 {#if reportsQuery.isLoading} 98 99 <div class="flex min-h-screen items-center justify-center"> 99 100 <div class="text-center"> ··· 106 107 <p>{reportsQuery.error?.message}</p> 107 108 </div> 108 109 {:else if reportsQuery.data} 109 - <h2 class="pb-5 text-xl font-medium text-ctp-text">Recent Reports</h2> 110 + <div class="flex items-center gap-2 pb-5 text-ctp-subtext0"> 111 + <ShieldAlertIcon size={16} /> 112 + <h2 class="text-md font-medium">Recent Reports</h2> 113 + </div> 110 114 <ul class="space-y-4 text-ctp-text"> 111 115 {#each reportsQuery.data.pages as page} 112 116 {#each page.subjectStatuses as report} 113 117 {@const subject = report.subject} 114 - <li class="rounded-lg border border-ctp-surface1 bg-ctp-surface0 p-4"> 118 + <li class="rounded-lg border border-ctp-surface1 p-4"> 115 119 <div class="flex items-center justify-between"> 116 120 <div class="flex items-center gap-2"> 117 121 {#if report.appealed}
+101
src/routes/events/+page.svelte
··· 1 + <script lang="ts"> 2 + import { session, type AuthStore } from '$lib/stores/auth'; 3 + import { 4 + LoaderCircle, 5 + ExternalLink, 6 + TriangleAlert, 7 + CheckIcon, 8 + CircleCheck, 9 + Scale, 10 + ScrollIcon 11 + } from 'lucide-svelte'; 12 + import { createInfiniteQuery, createQuery, QueryClient } from '@tanstack/svelte-query'; 13 + import { AtUri, ComAtprotoRepoStrongRef } from '@atproto/api'; 14 + import type { Report } from '$lib/types'; 15 + import { isRepoRef } from '@atproto/api/dist/client/types/com/atproto/admin/defs'; 16 + import { formatDate } from '$lib/time'; 17 + import PostModal from '$components/view/Post.svelte'; 18 + import { page } from '$app/state'; 19 + import Sidebar from '$components/Sidebar.svelte'; 20 + import UserModal from '$components/view/User.svelte'; 21 + import Events from '$components/view/Events.svelte'; 22 + 23 + let selectedDid: string | null = null; 24 + let selectedPostUri: AtUri | null = null; 25 + 26 + // switch to infinite query so we can page with the cursor returned by the API 27 + const eventsQuery = createInfiniteQuery(() => ({ 28 + queryKey: ['events_fullscreen'], 29 + queryFn: async ({ pageParam }) => { 30 + if (!$session) { 31 + throw new Error('No active session'); 32 + } 33 + const statuses = await $session.agent.tools.ozone.moderation.queryEvents({ 34 + limit: 10, 35 + sortDirection: 'desc', 36 + // pageParam will be the cursor string if present 37 + cursor: pageParam == '' ? undefined : pageParam 38 + }); 39 + 40 + return statuses.data; 41 + }, 42 + initialPageParam: '', 43 + getNextPageParam: (lastPage) => lastPage.cursor ?? undefined, 44 + retry(failureCount, error) { 45 + console.log('Fetch events error:', error); 46 + return failureCount < 3; 47 + }, 48 + enabled: !!$session 49 + })); 50 + 51 + function infiniteScroll(node: HTMLElement) { 52 + const observer = new IntersectionObserver( 53 + (entries) => { 54 + if ( 55 + entries[0].isIntersecting && 56 + eventsQuery.hasNextPage && 57 + !eventsQuery.isFetchingNextPage 58 + ) { 59 + eventsQuery.fetchNextPage(); 60 + } 61 + }, 62 + { 63 + rootMargin: '200px' 64 + } 65 + ); 66 + 67 + observer.observe(node); 68 + 69 + return { 70 + destroy() { 71 + observer.disconnect(); 72 + } 73 + }; 74 + } 75 + </script> 76 + 77 + <div class="min-h-screen w-full bg-ctp-base p-4"> 78 + {#if selectedDid} 79 + {#key selectedDid} 80 + <UserModal isOpen={true} did={selectedDid} onClose={() => (selectedDid = null)} /> 81 + {/key} 82 + {:else if selectedPostUri} 83 + {#key selectedPostUri} 84 + <PostModal isOpen={true} uri={selectedPostUri} onClose={() => (selectedPostUri = null)} /> 85 + {/key} 86 + {/if} 87 + <div class="max-w-2xl"> 88 + <div class="flex items-center gap-2 pb-5 text-ctp-subtext0"> 89 + <ScrollIcon size={16} /> 90 + <h2 class="text-md font-medium">Recent Events</h2> 91 + </div> 92 + <Events {eventsQuery} elevation="lower" onUserClick={(did) => (selectedDid = did)} /> 93 + <div use:infiniteScroll> 94 + {#if eventsQuery.isFetchingNextPage} 95 + <div class="flex items-center justify-center p-4"> 96 + <LoaderCircle class="h-6 w-6 animate-spin text-ctp-text" /> 97 + </div> 98 + {/if} 99 + </div> 100 + </div> 101 + </div>