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

Configure Feed

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

woag ui

+152 -64
+33 -13
src/components/Header.svelte
··· 7 7 import PopupMenu from './ui/PopupMenu.svelte'; 8 8 import { navItems } from '$lib/nav'; 9 9 import { page } from '$app/state'; 10 + import { onMount } from 'svelte'; 11 + import { ads } from '$lib/ads'; 10 12 11 13 async function handleLogout() { 12 14 if ($session) { ··· 36 38 }, 37 39 enabled: !!$session 38 40 })); 41 + 42 + function handleChange(event: Event) { 43 + const adsEnabled = (event.target as HTMLInputElement).checked; 44 + ads.toggle(adsEnabled); 45 + } 39 46 </script> 40 47 41 48 <div ··· 47 54 <div class="flex items-center gap-2"> 48 55 <PopupMenu 49 56 wrapperClass="relative" 50 - menuClass="absolute top-full right-0 z-50 w-48 rounded-md border border-ctp-surface1 bg-ctp-base px-1.5 py-2 shadow-lg" 57 + menuClass="absolute top-full right-0 z-50 w-48 rounded-md border border-ctp-surface1 bg-ctp-base p-1.5 shadow-lg" 51 58 > 52 59 <svelte:fragment slot="trigger" let:toggle> 53 60 <Button ··· 97 104 {/each} 98 105 </div> 99 106 <div class="my-2 border-t border-ctp-surface1"></div> 100 - <Button 101 - variant="danger" 102 - fullWidth={true} 103 - size="sm" 104 - on:click={handleLogout} 105 - className="justify-start py-1.5 rounded-sm" 106 - > 107 - <LogOut size={18} class="mr-3" /> 108 - <span>Logout</span> 109 - </Button> 110 - </svelte:fragment> 107 + <div> 108 + <div class="flex items-center gap-2 px-2 py-1"> 109 + <input 110 + type="checkbox" 111 + id="ads-toggle" 112 + class="cursor-pointer rounded accent-ctp-sapphire" 113 + bind:checked={$ads} 114 + on:change={handleChange} 115 + /> 116 + <label for="ads-toggle" class="cursor-pointer text-ctp-text">Enable ads</label> 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></svelte:fragment 130 + > 111 131 </PopupMenu> 112 132 <PopupMenu 113 133 wrapperClass="relative" 114 - menuClass="absolute top-full right-0 z-50 w-40 rounded-md border border-ctp-surface1 bg-ctp-base px-1.5 py-2 shadow-lg" 134 + menuClass="absolute top-full right-0 z-50 w-40 rounded-md border border-ctp-surface1 bg-ctp-base p-1.5 shadow-lg" 115 135 > 116 136 <svelte:fragment slot="trigger" let:toggle> 117 137 <Button
+10 -18
src/components/view/Actions.svelte
··· 1 1 <script lang="ts"> 2 - import { CheckIcon, LoaderCircle } from 'lucide-svelte'; 2 + import { LoaderCircle } from 'lucide-svelte'; 3 3 import TextField from '../ui/TextField.svelte'; 4 4 import Button from '../ui/Button.svelte'; 5 - import { inputAccentColor } from '$lib/stores/ui'; 6 - import { accentClasses } from '$lib/utils/accent'; 5 + import Checkbox from '../ui/Checkbox.svelte'; 7 6 import { createQuery } from '@tanstack/svelte-query'; 8 7 import { moderationConfigQueryOptions } from '$lib/queries/moderation'; 9 8 import { session } from '$lib/stores/auth'; ··· 81 80 <div class="flex flex-wrap gap-2 rounded-lg"> 82 81 {#each labelerConfig.data?.labels as label} 83 82 {@const isSelected = selectedLabelVals.has(label.val)} 84 - <button 85 - type="button" 86 - class={`${isSelected ? 'text-ctp-text' : 'text-ctp-subtext0'} group flex cursor-pointer items-center gap-2 rounded-lg border border-ctp-surface1 bg-ctp-surface0 px-2.5 py-1.5 text-sm transition-colors hover:border-ctp-surface2 hover:bg-ctp-surface0 hover:text-ctp-text`} 87 - onclick={() => onToggleLabel(label.val)} 83 + <div 84 + class={`rounded-lg border border-ctp-surface1 bg-ctp-surface0 px-2.5 py-1.5 transition-colors hover:border-ctp-surface2`} 88 85 > 89 - <span 90 - class={`flex h-4 w-4 items-center justify-center rounded-sm text-[10px] ${ 91 - isSelected 92 - ? `${accentClasses[$inputAccentColor].bg} text-ctp-base` 93 - : 'bg-ctp-surface1 text-transparent group-hover:text-ctp-subtext1' 94 - }`} 95 - > 96 - <CheckIcon class="h-3 w-3" /> 97 - </span> 98 - <span class="font-medium">{label.name}</span> 99 - </button> 86 + <Checkbox 87 + checked={isSelected} 88 + on:change={() => onToggleLabel(label.val)} 89 + label={label.name} 90 + /> 91 + </div> 100 92 {/each} 101 93 </div> 102 94 <TextField
+92 -30
src/components/view/Events.svelte
··· 1 1 <script lang="ts"> 2 2 import { formatDate } from '$lib/time'; 3 - import type { ToolsOzoneModerationDefs, ToolsOzoneModerationQueryEvents } from '@atproto/api'; 3 + import { 4 + AtUri, 5 + ComAtprotoRepoStrongRef, 6 + type ToolsOzoneModerationDefs, 7 + type ToolsOzoneModerationQueryEvents 8 + } from '@atproto/api'; 9 + import { isRepoRef } from '@atproto/api/dist/client/types/com/atproto/admin/defs'; 4 10 import { 5 11 isModEventLabel, 6 12 isModEventTag, ··· 11 17 CreateQueryResult, 12 18 InfiniteData 13 19 } from '@tanstack/svelte-query'; 14 - import { LoaderCircle } from 'lucide-svelte'; 20 + import { 21 + CheckIcon, 22 + CircleCheck, 23 + CircleUserRound, 24 + ClipboardIcon, 25 + ExternalLink, 26 + FlagIcon, 27 + HammerIcon, 28 + LoaderCircle, 29 + StampIcon, 30 + TagIcon, 31 + ToolboxIcon 32 + } from 'lucide-svelte'; 15 33 16 - let { eventsQuery, onUserClick, elevation } = $props<{ 34 + let { eventsQuery, onUserClick, onPostClick, elevation, showInfo } = $props<{ 17 35 eventsQuery: 18 36 | CreateQueryResult<ToolsOzoneModerationQueryEvents.OutputSchema, Error> 19 37 | CreateInfiniteQueryResult< ··· 21 39 Error 22 40 >; 23 41 elevation?: 'normal' | 'lower'; 42 + showInfo?: boolean; 24 43 onUserClick?: (did: string) => void; 44 + onPostClick?: (uri: AtUri) => void; 25 45 }>(); 26 46 const events = $derived.by(() => { 27 - if ('pages' in eventsQuery.data) { 47 + if (eventsQuery?.data != null && 'pages' in eventsQuery?.data) { 28 48 // infinite query data structure 29 49 const pages = eventsQuery.data.pages as ToolsOzoneModerationQueryEvents.OutputSchema[]; 30 50 return pages.flatMap((page) => (Array.isArray(page.events) ? page.events : [])); ··· 35 55 } 36 56 }); 37 57 38 - function eventName(event: ToolsOzoneModerationDefs.ModEventView): string { 39 - switch (event.event.$type) { 40 - case 'tools.ozone.moderation.defs#modEventAcknowledge': 41 - return 'Acknowledged'; 42 - case 'tools.ozone.moderation.defs#modEventLabel': 43 - return 'Labeled'; 44 - case 'tools.ozone.moderation.defs#modEventTag': 45 - return 'Tagged'; 46 - case 'tools.ozone.moderation.defs#modEventReport': 47 - return 'Reported'; 48 - default: 49 - return event.event.$type; 50 - } 51 - } 58 + const iconSize = 15; 52 59 </script> 53 60 54 61 <div> ··· 68 75 <div class="space-y-2"> 69 76 {#each events as event} 70 77 <div 71 - class={`rounded-lg border ${elevation == 'lower' ? 'border-ctp-surface1' : 'border-ctp-surface2'} p-2`} 78 + class={`rounded-lg border ${elevation == 'lower' ? 'border-ctp-surface1' : 'border-ctp-surface2'} p-3`} 72 79 > 73 - <div class="flex items-center justify-between gap-2"> 74 - <p class="text-sm font-medium text-ctp-text">{eventName(event)}</p> 80 + <div class="mb-1 flex items-center justify-between gap-2"> 81 + <div class="flex text-base text-ctp-text"> 82 + <p class="flex items-center gap-1 font-medium"> 83 + {#if event.event.$type === 'tools.ozone.moderation.defs#modEventAcknowledge'} 84 + <CircleCheck size={iconSize} class="text-ctp-green" /> Acknowledged 85 + {:else if event.event.$type === 'tools.ozone.moderation.defs#modEventLabel'} 86 + <ClipboardIcon size={iconSize} class="text-ctp-mauve" /> Labeled 87 + {:else if event.event.$type === 'tools.ozone.moderation.defs#modEventTag'} 88 + <TagIcon size={iconSize} class="text-ctp-sapphire" /> Tagged 89 + {:else if event.event.$type === 'tools.ozone.moderation.defs#modEventReport'} 90 + <FlagIcon size={iconSize} class="text-ctp-red" /> Reported 91 + {:else} 92 + {event.event.$type} 93 + {/if} 94 + </p> 95 + {#if showInfo} 96 + {@const subject = event.subject} 97 + {#if ComAtprotoRepoStrongRef.isMain(subject)} 98 + <button 99 + onclick={() => onPostClick?.(new AtUri(subject.uri))} 100 + class="ml-1 inline-flex cursor-pointer items-center text-ctp-blue hover:underline" 101 + > 102 + <p>post</p> 103 + <ExternalLink size={15} class="ml-1" /> 104 + </button> 105 + <p class="mx-1">created by</p> 106 + <button 107 + onclick={() => onUserClick?.(new AtUri(subject.uri).host)} 108 + class="inline-flex cursor-pointer items-center px-0 py-0 text-ctp-blue hover:underline" 109 + > 110 + @{event.subjectHandle} 111 + <ExternalLink size={15} class="ml-1" /> 112 + </button> 113 + {:else if isRepoRef(subject)} 114 + <button 115 + onclick={() => onUserClick?.(subject.did)} 116 + class="ml-1 inline-flex cursor-pointer items-center px-0 py-0 text-ctp-blue hover:underline" 117 + > 118 + @{event.subjectHandle} 119 + <ExternalLink size={15} class="ml-1" /> 120 + </button> 121 + {/if} 122 + {/if} 123 + </div> 75 124 <p class="text-xs text-ctp-subtext0">{formatDate(event.createdAt)}</p> 76 125 </div> 77 126 {#if isModEventLabel(event.event)} ··· 103 152 </div> 104 153 {:else if isModEventReport(event.event)} 105 154 <div class="space-y-1 py-1"> 106 - <p class="w-fit rounded-md bg-ctp-surface0 p-1 text-xs text-ctp-text"> 107 - {event.event.reportType.replace('com.atproto.moderation.defs#reason', '')} 155 + <p 156 + class={`w-fit rounded-md border ${elevation == 'lower' ? 'border-ctp-surface1' : 'border-ctp-surface2'} px-2 py-1 text-xs text-ctp-text`} 157 + > 158 + {event.event.reportType 159 + .replace('com.atproto.moderation.defs#reason', '') 160 + .replace('tools.ozone.report.defs#reason', '')} 108 161 </p> 109 162 <p class="text-sm text-ctp-text">Reason: {event.event.comment}</p> 110 163 </div> 111 164 {/if} 112 165 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> 166 + <div class="mt-1 gap-1 text-xs text-ctp-subtext0"> 167 + <p class="flex items-center gap-1"> 168 + <CircleUserRound size={13} class="text-ctp-text" /> by 169 + <button 170 + class={onUserClick ? `cursor-pointer text-ctp-blue hover:underline` : ''} 171 + onclick={() => onUserClick?.(event.createdBy)} 172 + disabled={!onUserClick}>@{event.creatorHandle}</button 173 + > 174 + </p> 175 + {#if event.modTool != null} 176 + <p class="mt-1 flex items-center gap-1"> 177 + <HammerIcon size={13} class="text-ctp-text" /> using 178 + <span class="text-ctp-text">{event.modTool.name}</span> 179 + </p> 180 + {/if} 181 + </div> 120 182 </div> 121 183 {/each} 122 184 </div>
-1
src/components/view/Post.svelte
··· 223 223 <div class="hidden gap-3 md:grid md:grid-cols-2"> 224 224 {@render postPanel()} 225 225 <EventsContainer> 226 - <h3 class="text-md mb-2 text-ctp-text">Moderation Events</h3> 227 226 <Events {eventsQuery} /> 228 227 </EventsContainer> 229 228 </div>
+5
src/routes/+page.svelte
··· 21 21 import Post from '$components/view/Post.svelte'; 22 22 import { page } from '$app/state'; 23 23 import Sidebar from '$components/Sidebar.svelte'; 24 + import Advertisement from '$components/Advertisement.svelte'; 25 + import { ads } from '$lib/ads'; 26 + import { onMount } from 'svelte'; 27 + import AdvertisementOverlay from '$components/AdvertisementOverlay.svelte'; 24 28 25 29 let selectedDid: string | null = null; 26 30 let selectedPostUri: AtUri | null = null; ··· 94 98 <Post isOpen={true} uri={selectedPostUri} onClose={() => (selectedPostUri = null)} /> 95 99 {/key} 96 100 {/if} 101 + <AdvertisementOverlay /> 97 102 <div class="mx-auto max-w-2xl"> 98 103 {#if reportsQuery.isLoading} 99 104 <div class="flex min-h-screen items-center justify-center">
+9 -1
src/routes/events/+page.svelte
··· 19 19 import Sidebar from '$components/Sidebar.svelte'; 20 20 import UserModal from '$components/view/User.svelte'; 21 21 import Events from '$components/view/Events.svelte'; 22 + import AdvertisementOverlay from '$components/AdvertisementOverlay.svelte'; 22 23 23 24 let selectedDid: string | null = null; 24 25 let selectedPostUri: AtUri | null = null; ··· 84 85 <PostModal isOpen={true} uri={selectedPostUri} onClose={() => (selectedPostUri = null)} /> 85 86 {/key} 86 87 {/if} 88 + <AdvertisementOverlay /> 87 89 <div class="mx-auto max-w-2xl"> 88 90 <div class="flex items-center gap-2 pb-5 text-ctp-subtext0"> 89 91 <ScrollIcon size={16} /> 90 92 <h2 class="text-md font-medium">Recent Events</h2> 91 93 </div> 92 - <Events {eventsQuery} elevation="lower" onUserClick={(did) => (selectedDid = did)} /> 94 + <Events 95 + {eventsQuery} 96 + elevation="lower" 97 + onUserClick={(did) => (selectedDid = did)} 98 + onPostClick={(uri) => (selectedPostUri = uri)} 99 + showInfo={true} 100 + /> 93 101 <div use:infiniteScroll> 94 102 {#if eventsQuery.isFetchingNextPage} 95 103 <div class="flex items-center justify-center p-4">
+3 -1
src/routes/login/+page.svelte
··· 5 5 import { Agent, AtpAgent } from '@atproto/api'; 6 6 import Button from '../../components/ui/Button.svelte'; 7 7 import Input from '../../components/ui/Input.svelte'; 8 - import { CatIcon, OrbitIcon } from 'lucide-svelte'; 8 + import { CatIcon, OrbitIcon, XIcon } from 'lucide-svelte'; 9 + import Advertisement from '$components/Advertisement.svelte'; 9 10 10 11 let handleInput = $state(''); 11 12 let passwordInput = $state(''); ··· 179 180 </button> 180 181 {/if} 181 182 </div> 183 + <Advertisement className="mt-5" /> 182 184 </div> 183 185 </div>