Keep track of ICE and police locations in your city. Very much a work-in-progress and not ready yet, stay tuned I guess?

refactor and move db into its own features dir

aria.yuri.observer 2d020a7f d71eea23

verified
Changed files
+435 -36
apps
+1
apps/web/.env.example
··· 1 1 DATABASE_URL=file:local.db 2 + SEEDING_ENABLED=false
+2 -2
apps/web/drizzle.config.ts
··· 3 3 if (!process.env.DATABASE_URL) throw new Error('DATABASE_URL is not set'); 4 4 5 5 export default defineConfig({ 6 - schema: './src/lib/server/db/schema.ts', 6 + schema: './src/features/db/server/schema.ts', 7 7 dialect: 'sqlite', 8 8 dbCredentials: { url: process.env.DATABASE_URL }, 9 9 verbose: true, 10 - strict: true 10 + strict: true, 11 11 });
+148
apps/web/src/features/db/server/seed-data.ts
··· 1 + import type { InferSelectModel } from 'drizzle-orm'; 2 + import type { report } from './schema'; 3 + import { 4 + type InsertReportWithRelations, 5 + type InsertContextWithRelations, 6 + MediaType, 7 + ContextType, 8 + type InsertMedia, 9 + } from '../types'; 10 + 11 + export type SeedData = { 12 + reports: InsertReportWithRelations<InsertContextWithRelations>[]; 13 + }; 14 + 15 + const imageMedia = (media: Omit<InsertMedia, 'type'>) => { 16 + return { 17 + type: MediaType.image, 18 + ...media, 19 + }; 20 + }; 21 + 22 + export const seedData: SeedData = { 23 + reports: [ 24 + { 25 + context: [ 26 + { 27 + type: ContextType.root, 28 + text: 'ICE activity spotted in area', 29 + location: { 30 + latitude: '34.7724', 31 + longitude: '-84.9819', 32 + }, 33 + media: [ 34 + imageMedia({ 35 + url: 'https://plus.unsplash.com/premium_photo-1687157829884-fae305709c06?ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&q=80&w=900', 36 + altText: 37 + 'A cop car with its lights on and a city building in the background, out of focus.', 38 + }), 39 + imageMedia({ 40 + url: 'https://plus.unsplash.com/premium_photo-1686695196013-b0e9aef9cdff?ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTN8fHBvbGljZXxlbnwwfHwwfHx8MA%3D%3D&auto=format&fit=crop&q=60&w=900', 41 + altText: 42 + 'A cop with a stupidly smug face sitting on a motorcycle being useless and cruel.', 43 + }), 44 + imageMedia({ 45 + url: 'https://images.unsplash.com/photo-1652793806995-7bf3265e40b0?ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&q=80&w=1760', 46 + altText: 'Two Toronto police cars', 47 + }), 48 + imageMedia({ 49 + url: 'https://images.unsplash.com/photo-1590995891215-0336d27411de?ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&q=80&w=1740', 50 + altText: 51 + 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Hic, consequuntur incidunt non, fugiat debitis quas atque porro quia necessitatibus facere ut molestiae amet, a nisi temporibus unde sequi. At, nam.', 52 + }), 53 + imageMedia({ 54 + url: 'https://images.unsplash.com/photo-1591073214708-44d56a561981?ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&q=80&w=1740', 55 + altText: 56 + 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Hic, consequuntur incidunt non, fugiat debitis quas atque porro quia necessitatibus facere ut molestiae amet, a nisi temporibus unde sequi. At, nam.', 57 + }), 58 + imageMedia({ 59 + url: 'https://images.unsplash.com/photo-1520085401243-fa89fc9ff1b7?ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&q=80&w=1642', 60 + altText: 61 + 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Hic, consequuntur incidunt non, fugiat debitis quas atque porro quia necessitatibus facere ut molestiae amet, a nisi temporibus unde sequi. At, nam.', 62 + }), 63 + imageMedia({ 64 + url: 'https://images.unsplash.com/photo-1758405282251-26903f4b7fcb?ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&q=80&w=1740', 65 + altText: 66 + 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Hic, consequuntur incidunt non, fugiat debitis quas atque porro quia necessitatibus facere ut molestiae amet, a nisi temporibus unde sequi. At, nam.', 67 + }), 68 + imageMedia({ 69 + url: 'https://images.unsplash.com/photo-1758405282247-86deca3ecc87?ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&q=80&w=1740', 70 + altText: 71 + 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Hic, consequuntur incidunt non, fugiat debitis quas atque porro quia necessitatibus facere ut molestiae amet, a nisi temporibus unde sequi. At, nam.', 72 + }), 73 + imageMedia({ 74 + url: 'https://images.unsplash.com/photo-1686153957738-fed649408234?ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&q=80&w=1762', 75 + altText: 76 + 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Hic, consequuntur incidunt non, fugiat debitis quas atque porro quia necessitatibus facere ut molestiae amet, a nisi temporibus unde sequi. At, nam.', 77 + }), 78 + ], 79 + }, 80 + { 81 + type: ContextType.info, 82 + text: 'a protester was just kidnapped by ICE, please protect yourself and stay aware of your surroundings at all times in this area.', 83 + location: {}, 84 + media: [ 85 + imageMedia({ 86 + url: 'https://plus.unsplash.com/premium_photo-1683134562864-1670a96e3022?ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&q=80&w=1740', 87 + altText: 'person being detained by masked ICE thugs', 88 + }), 89 + ], 90 + }, 91 + ], 92 + }, 93 + { 94 + context: [ 95 + { 96 + type: ContextType.root, 97 + location: { 98 + latitude: '34.76803247376194', 99 + longitude: '-84.97822846789504', 100 + }, 101 + media: [ 102 + imageMedia({ 103 + url: 'https://images.unsplash.com/photo-1590995891215-0336d27411de?ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&q=80&w=1740', 104 + altText: 105 + 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Hic, consequuntur incidunt non, fugiat debitis quas atque porro quia necessitatibus facere ut molestiae amet, a nisi temporibus unde sequi. At, nam.', 106 + }), 107 + imageMedia({ 108 + url: 'https://images.unsplash.com/photo-1591073214708-44d56a561981?ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&q=80&w=1740', 109 + altText: 110 + 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Hic, consequuntur incidunt non, fugiat debitis quas atque porro quia necessitatibus facere ut molestiae amet, a nisi temporibus unde sequi. At, nam.', 111 + }), 112 + imageMedia({ 113 + url: 'https://images.unsplash.com/photo-1520085401243-fa89fc9ff1b7?ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&q=80&w=1642', 114 + altText: 115 + 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Hic, consequuntur incidunt non, fugiat debitis quas atque porro quia necessitatibus facere ut molestiae amet, a nisi temporibus unde sequi. At, nam.', 116 + }), 117 + imageMedia({ 118 + url: 'https://images.unsplash.com/photo-1758405282251-26903f4b7fcb?ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&q=80&w=1740', 119 + altText: 120 + 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Hic, consequuntur incidunt non, fugiat debitis quas atque porro quia necessitatibus facere ut molestiae amet, a nisi temporibus unde sequi. At, nam.', 121 + }), 122 + imageMedia({ 123 + url: 'https://images.unsplash.com/photo-1758405282247-86deca3ecc87?ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&q=80&w=1740', 124 + altText: 125 + 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Hic, consequuntur incidunt non, fugiat debitis quas atque porro quia necessitatibus facere ut molestiae amet, a nisi temporibus unde sequi. At, nam.', 126 + }), 127 + imageMedia({ 128 + url: 'https://images.unsplash.com/photo-1686153957738-fed649408234?ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&q=80&w=1762', 129 + altText: 130 + 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Hic, consequuntur incidunt non, fugiat debitis quas atque porro quia necessitatibus facere ut molestiae amet, a nisi temporibus unde sequi. At, nam.', 131 + }), 132 + ], 133 + }, 134 + { 135 + type: ContextType.info, 136 + text: 'a protester was just kidnapped by ICE, please protect yourself and stay aware of your surroundings at all times in this area.', 137 + location: {}, 138 + media: [ 139 + imageMedia({ 140 + url: 'https://plus.unsplash.com/premium_photo-1683134562864-1670a96e3022?ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&q=80&w=1740', 141 + altText: 'person being detained by masked ICE thugs', 142 + }), 143 + ], 144 + }, 145 + ], 146 + }, 147 + ], 148 + };
+67
apps/web/src/features/db/types/index.ts
··· 1 + import type { InferSelectModel, InferInsertModel } from 'drizzle-orm'; 2 + import type { report, context, location, media } from '../server/schema'; 3 + 4 + export type SelectedLocation = InferSelectModel<typeof location>; 5 + export type InsertLocation = InferInsertModel<typeof location>; 6 + 7 + export type SelectedMedia = InferSelectModel<typeof media> & { 8 + type: MediaType; 9 + }; 10 + 11 + export type InsertMedia = InferInsertModel<typeof media> & { 12 + type: MediaType; 13 + }; 14 + 15 + export type SelectedReport = InferSelectModel<typeof report>; 16 + export type InsertReport = InferInsertModel<typeof report>; 17 + 18 + export type SelectedContext = InferSelectModel<typeof context> & { 19 + type: ContextType; 20 + }; 21 + export type InsertContext = InferInsertModel<typeof context> & { 22 + type: ContextType; 23 + }; 24 + 25 + export type SelectedReportWithRelations<TContextType extends SelectedContext = SelectedContext> = 26 + SelectedReport & { 27 + context: TContextType[]; 28 + }; 29 + 30 + export type InsertReportWithRelations<TContextType extends InsertContext = InsertContext> = 31 + InsertReport & { 32 + context: TContextType[]; 33 + }; 34 + 35 + export type SelectedContextWithRelations< 36 + TMediaType extends SelectedMedia = SelectedMedia, 37 + TLocationType extends SelectedLocation = SelectedLocation, 38 + > = SelectedContext & { 39 + location?: TLocationType; 40 + media: TMediaType[]; 41 + }; 42 + 43 + export type InsertContextWithRelations< 44 + TMediaType extends InsertMedia = InsertMedia, 45 + TLocationType extends InsertLocation = InsertLocation, 46 + > = InsertContext & { 47 + location?: TLocationType; 48 + media: TMediaType[]; 49 + }; 50 + 51 + export enum MediaType { 52 + image = 'IMAGE', 53 + video = 'VIDEO', 54 + } 55 + 56 + export enum ContextType { 57 + /** The root context. There should only ever be one of these per report */ 58 + root = 'ROOT', 59 + /** Represents a confirmation by a user that the report is (still) valid */ 60 + confirm = 'CONFIRM', 61 + /** Represents a user's report that the location is now safe from ICE and police activity */ 62 + safe = 'SAFE', 63 + /** Used to add additional info/context to the report */ 64 + info = 'INFO', 65 + /** Represents a user's report that ICE/cops have moved to a new location */ 66 + moved = 'MOVED', 67 + }
+1 -4
apps/web/src/lib/api/reports/reports.remote.ts
··· 1 - import { json } from '@sveltejs/kit'; 2 - import type { GeoJSON, Feature, Geometry, GeoJsonProperties } from 'geojson'; 3 - 4 1 import { query } from '$app/server'; 5 - import { db } from '$lib/server/db'; 2 + import { db } from '$features/db/server'; 6 3 7 4 export const getReports = query(async () => { 8 5 const reports = await db.query.report.findMany({
+86
apps/web/src/lib/components/gallery/gallery.svelte
··· 1 + <script module lang="ts"> 2 + import type { Snippet } from 'svelte'; 3 + import type { SelectedPoint } from '$lib/components/pig-map.svelte'; 4 + export type Props = { 5 + media: SelectedPoint['context']['media']; 6 + active: number; 7 + open: boolean; 8 + }; 9 + </script> 10 + 11 + <script lang="ts"> 12 + import * as Dialog from '$lib/components/ui/dialog'; 13 + import * as Carousel from '$lib/components/ui/carousel'; 14 + import { Button } from '$lib/components/ui/button'; 15 + 16 + import { cn } from '$lib/utils/cn'; 17 + 18 + let { media = [], active = $bindable(0), open = $bindable(false) }: Props = $props(); 19 + </script> 20 + 21 + <Dialog.Root bind:open> 22 + <Dialog.Content> 23 + <div>hi</div> 24 + <Carousel.Root 25 + opts={{ 26 + align: 'start', 27 + }} 28 + class="h-full w-full" 29 + > 30 + <Carousel.Content> 31 + {#each media as mediaItem} 32 + {#if mediaItem.type === 'IMAGE'} 33 + <Carousel.Item> 34 + <Button variant="ghost" class="h-full w-full cursor-pointer p-0"> 35 + <img 36 + draggable={false} 37 + src={mediaItem.url} 38 + alt={mediaItem.altText} 39 + class="h-full w-full overflow-hidden rounded-md border object-cover transition-all duration-150" 40 + /> 41 + </Button> 42 + </Carousel.Item> 43 + {:else if mediaItem.type === 'VIDEO'} 44 + <Carousel.Item class={cn(' transition-all duration-150')}> 45 + <video src={mediaItem.url} class=" h-full w-full rounded-md border object-contain"> 46 + <track kind="captions" src={mediaItem.altText} srclang="en" label="English" /> 47 + </video> 48 + </Carousel.Item> 49 + {/if} 50 + {/each} 51 + </Carousel.Content> 52 + </Carousel.Root> 53 + </Dialog.Content> 54 + </Dialog.Root> 55 + <!-- <Carousel.Content> 56 + {#each media as mediaItem} 57 + {#if mediaItem.type === 'IMAGE'} 58 + <Carousel.Item 59 + > 60 + <Button variant="ghost" class="h-full w-full cursor-pointer p-0"> 61 + <img 62 + draggable={false} 63 + src={mediaItem.url} 64 + alt={mediaItem.altText} 65 + class="h-full w-full overflow-hidden rounded-md border object-cover transition-all duration-150" 66 + /> 67 + </Button> 68 + </Carousel.Item> 69 + {:else if mediaItem.type === 'VIDEO'} 70 + <Carousel.Item 71 + class={c 72 + n(' transition-all duration-150', { 73 + 'basis-1/3': !isCollapsed, 74 + 'basis-1/5': isCollapsed, 75 + })} 76 + > 77 + <video 78 + src={mediaItem.url} 79 + class=" h-full w-full rounded-md border object-contain" 80 + > 81 + <track kind="captions" src={media.altText} srclang="en" label="English" /> 82 + </video> 83 + </Carousel.Item> 84 + {/if} 85 + {/each} 86 + </Carousel.Content> -->
+3
apps/web/src/lib/components/gallery/index.ts
··· 1 + import Gallery from './gallery.svelte'; 2 + 3 + export { Gallery };
+15 -12
apps/web/src/lib/components/info-sheet.svelte
··· 27 27 const isMobile = new IsMobile(); 28 28 </script> 29 29 30 - <Drawer.Root 31 - dismissible={isMobile.current} 32 - bind:open={isSheetOpen} 33 - direction={isMobile.current ? 'bottom' : 'right'} 34 - {...props} 35 - > 36 - <Drawer.Content class={cn('', className)}> 37 - {#if !isMobile.current} 38 - <Drawer.Header class="flex flex-row"> 39 - <Drawer.Title class="flex-1 ">{title}</Drawer.Title> 30 + <Drawer.Root bind:open={isSheetOpen} direction={isMobile.current ? 'bottom' : 'right'} {...props}> 31 + <Drawer.Content 32 + class={cn( 33 + '', 34 + { 35 + 'm-4 rounded-md border shadow-md [&:after]:bg-transparent!': !isMobile.current, 36 + }, 37 + className 38 + )} 39 + > 40 + <Drawer.Header class="flex flex-row"> 41 + <Drawer.Title class="flex-1 ">{title}</Drawer.Title> 42 + {#if !isMobile.current} 40 43 <Button variant="ghost" size="icon" onclick={() => (isSheetOpen = false)}> 41 44 <XIcon /> 42 45 </Button> 43 - </Drawer.Header> 44 - {/if} 46 + {/if} 47 + </Drawer.Header> 45 48 <div class="w-full p-4"> 46 49 {@render children?.()} 47 50 </div>
+1 -1
apps/web/src/lib/components/ui/drawer/drawer.svelte
··· 1 1 <script lang="ts"> 2 - import { Drawer as DrawerPrimitive } from "vaul-svelte"; 2 + import { Drawer as DrawerPrimitive } from 'vaul-svelte'; 3 3 4 4 let { 5 5 shouldScaleBackground = true,
apps/web/src/lib/server/db/index.ts apps/web/src/features/db/server/index.ts
+1 -1
apps/web/src/lib/server/db/schema.ts apps/web/src/features/db/server/schema.ts
··· 51 51 id: integer('id').primaryKey({ 52 52 autoIncrement: true, 53 53 }), 54 - type: text('type').default(''), // 'CONFIRM' | 'SAFE' | 'INFO' | 'MOVED' | '' (empty for initial/root type) 54 + type: text('type').default(''), 55 55 reportId: integer('reportId').references(() => report.id), 56 56 locationId: integer('locationId').references(() => location.id), 57 57 createdAt: text('createdAt').notNull().default(Date.now().toString()),
+106 -16
apps/web/src/routes/+page.svelte
··· 12 12 import PigMap, { type SelectedPoint } from '$lib/components/pig-map.svelte'; 13 13 import { getReports } from '$lib/api/reports/reports.remote'; 14 14 import { cn } from '$lib/utils/cn'; 15 + import * as Dialog from '$lib/components/ui/dialog'; 16 + import Gallery from '$lib/components/gallery/gallery.svelte'; 17 + import { ContextType, MediaType } from '$features/db/types'; 15 18 16 19 let selectedPoint: SelectedPoint | null = $state(null); 17 - 18 - // let activeCollapsed: boolean = $state(false); 19 20 20 21 const formatTimestamp = (timestamp: string): { date: string; time: string } => { 21 22 console.log({ timestamp }); ··· 27 28 const getRootContext = () => { 28 29 if (!selectedPoint) return null; 29 30 const contexts = selectedPoint.report.context; 30 - 31 - const rootContextIndex = contexts.findIndex((c) => c.type === null); 31 + const rootContextIndex = contexts.findIndex((c) => c.type === ContextType.root); 32 32 return contexts[rootContextIndex]; 33 33 }; 34 - const sortContext = (contexts: SelectedPoint['context'][]): SelectedPoint['context'][] => { 35 - // extract type "null" context first, that's the "root" 36 - const rootContextIndex = contexts.findIndex((c) => c.type === null || c.type === 'ROOT'); 34 + 35 + const sortContext = ([ 36 + rootContext, 37 + ...contexts 38 + ]: SelectedPoint['context'][]): SelectedPoint['context'][] => { 39 + // const rootContextIndex = contexts.findIndex((c) => c.type === 'ROOT'); 37 40 38 41 // sort the rest by date 39 - const nonRootContexts = contexts 40 - .filter((c) => c.type !== null) 42 + const nonRootContexts = [...contexts] 43 + // .filter((c) => c.type !== null) 41 44 .sort((a, b) => { 42 45 return ( 43 46 new Date(parseInt(b.createdAt)).getTime() - new Date(parseInt(a.createdAt)).getTime() 44 47 ); 45 48 }); 46 49 47 - return [contexts[rootContextIndex], ...nonRootContexts]; 50 + return [rootContext, ...nonRootContexts]; 48 51 }; 49 52 50 53 let isSheetOpen = $state(false); 51 54 55 + let galleryState: { 56 + [key: string]: { 57 + open: boolean; 58 + active: number; 59 + }[]; 60 + } = $state({}); 61 + 52 62 $effect(() => { 53 63 selectedPoint; 54 64 untrack(() => { 55 65 isSheetOpen = !!selectedPoint?.context; 56 - // activeCollapsed = false; 57 66 }); 58 67 }); 68 + 69 + // const openGallery = (contextIndex: number, mediaIndex: number) => { 70 + // // if (!galleryState[contextIndex]) 71 + // galleryState[contextIndex] = { 72 + // active: mediaIndex, 73 + // open: true, 74 + // }; 75 + // }; 76 + 77 + const initializeGalleryState = () => { 78 + // console.log() 79 + // galleryState = 80 + // selectedPoint?.report.context.flatMap((ctx) => { 81 + // console.log('fff', ctx); 82 + // return ctx.media.map((mediaItem) => { 83 + // return { 84 + // open: false, 85 + // active: 0, 86 + // }; 87 + // }); 88 + // }) ?? []; 89 + 90 + console.log({ ...galleryState }); 91 + }; 92 + 93 + $effect(() => { 94 + selectedPoint; 95 + 96 + untrack(() => { 97 + initializeGalleryState(); 98 + }); 99 + }); 100 + 101 + // $inspect(selectedPoint); 59 102 </script> 60 103 61 104 {#await getReports() then reports} ··· 76 119 <!-- title={selectedPoint?.report.context[0].text ?? 'No context selected'} --> 77 120 {#if selectedPoint} 78 121 {@const rootContext = getRootContext()!} 122 + 79 123 <div class="@container space-y-2"> 80 - {#each sortContext(selectedPoint.report.context) as context} 124 + {#each sortContext(selectedPoint.report.context) as context, contextIndex} 81 125 {@const isRoot = context.id === rootContext.id} 82 126 {@const isSelected = context.id === selectedPoint.context.id} 83 127 {@const isCollapsed = !isSelected} 128 + <!-- 129 + <Gallery 130 + bind:open={galleryState[contextIndex].open} 131 + bind:active={galleryState[contextIndex].active} 132 + media={context.media} 133 + /> --> 84 134 <div 85 135 class={cn('animate-in space-y-2 rounded-md border p-2 shadow-md transition-all', { 86 136 'bg-card text-card-foreground': isCollapsed, ··· 140 190 </div> 141 191 142 192 {#if context?.media.length >= 1} 193 + <div 194 + class={cn('flex basis-1/6 gap-2 transition-all duration-150', { 195 + 'basis-1/4!': !isCollapsed, 196 + })} 197 + > 198 + {#each context?.media as media, mediaIndex} 199 + <button 200 + class={cn( 201 + 'aspect-square max-h-16 max-w-16 overflow-hidden rounded transition-all duration-95', 202 + { 203 + 'aspect-square': media.type === MediaType.image, 204 + 'max-h-1/3 max-w-1/3 basis-1/3': !isCollapsed && context.media.length <= 6, 205 + 'max-h-1/6 max-w-1/6 basis-1/6': !isCollapsed && context.media.length > 6, 206 + } 207 + )} 208 + > 209 + {#if media.type === MediaType.image} 210 + <img 211 + draggable={false} 212 + src={media.url} 213 + alt={media.altText} 214 + class="h-full w-full object-cover" 215 + /> 216 + {:else if media.type === MediaType.video} 217 + <video src={media.url} class="h-full w-full object-cover"> 218 + <track 219 + kind="captions" 220 + src={media.altText} 221 + srclang="en" 222 + label="English (Alt Text)" 223 + /> 224 + </video> 225 + {/if} 226 + </button> 227 + {/each} 228 + </div> 143 229 <Carousel.Root 144 230 opts={{ 145 231 align: 'start', 146 232 }} 147 233 class="w-full" 148 234 > 149 - <Carousel.Content> 150 - {#each context?.media as media} 235 + <!-- <Carousel.Content> 236 + {#each context?.media as media, mediaIndex} 151 237 {#if media.type === 'IMAGE'} 152 238 <Carousel.Item 153 239 class={cn('aspect-square transition-all duration-150', { ··· 155 241 'basis-1/5': isCollapsed, 156 242 })} 157 243 > 158 - <Button variant="ghost" class="h-full w-full cursor-pointer p-0"> 244 + <Button 245 + variant="ghost" 246 + class="h-full w-full cursor-pointer p-0" 247 + onclick={() => openGallery(contextIndex, mediaIndex)} 248 + > 159 249 <img 160 250 draggable={false} 161 251 src={media.url} ··· 180 270 </Carousel.Item> 181 271 {/if} 182 272 {/each} 183 - </Carousel.Content> 273 + </Carousel.Content> --> 184 274 </Carousel.Root> 185 275 {/if} 186 276 </div>
+4
apps/web/svelte.config.js
··· 8 8 preprocess: vitePreprocess(), 9 9 10 10 kit: { 11 + alias: { 12 + '$lib/*': './src/lib/*', 13 + '$features/*': './src/features/*', 14 + }, 11 15 experimental: { 12 16 remoteFunctions: true, 13 17 },