+2
-2
apps/web/drizzle.config.ts
+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
+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
+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
-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
+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
+3
apps/web/src/lib/components/gallery/index.ts
+15
-12
apps/web/src/lib/components/info-sheet.svelte
+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
apps/web/src/lib/components/ui/drawer/drawer.svelte
apps/web/src/lib/server/db/index.ts
apps/web/src/features/db/server/index.ts
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
+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
+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>