grain.social is a photo sharing platform built on atproto.
1import { PhotoView } from "$lexicon/types/social/grain/photo/defs.ts";
2import { $Typed } from "$lexicon/util.ts";
3import { AtUri } from "@atproto/syntax";
4import { Button } from "./Button.tsx";
5import { Dialog } from "./Dialog.tsx";
6
7export function LibraryPhotoSelectDialog({
8 galleryUri,
9 photos,
10}: Readonly<{
11 galleryUri: string;
12 photos: $Typed<PhotoView>[];
13}>) {
14 const rkey = new AtUri(galleryUri).rkey;
15 return (
16 <Dialog id="photo-select-dialog" class="z-101">
17 <Dialog.Content class="flex flex-col gap-4">
18 <Dialog.X class="fill-zinc-950 dark:fill-zinc-50" />
19 <Dialog.Title>My library</Dialog.Title>
20
21 <form
22 id="photo-select-form"
23 hx-put={`/actions/gallery/${rkey}/add-photos`}
24 hx-target="#dialog-target"
25 hx-swap="innerHTML"
26 class="flex-1 overflow-y-auto"
27 >
28 {photos.length
29 ? (
30 <div class="grid grid-cols-3 gap-2">
31 {photos.map((photo) => (
32 <PhotoItem
33 key={photo.cid}
34 photo={photo}
35 />
36 ))}
37 </div>
38 )
39 : (
40 <div class="flex justify-center items-center my-30 h-full">
41 <p>No photos yet.</p>
42 </div>
43 )}
44 </form>
45
46 <div
47 id="photo-select-overlay"
48 class="w-full bg-white dark:bg-zinc-900 flex justify-between items-center z-102"
49 _="on load set my.count to 0"
50 >
51 <span id="selected-count">0 selected</span>
52 <Button
53 type="submit"
54 form="photo-select-form"
55 variant="primary"
56 >
57 Add to gallery
58 </Button>
59 </div>
60
61 <Dialog.Close variant="secondary" class="w-full">Close</Dialog.Close>
62 </Dialog.Content>
63 </Dialog>
64 );
65}
66
67export function LibaryPhotoSelectDialogButton({ galleryUri }: Readonly<{
68 galleryUri: string;
69}>) {
70 const rkey = new AtUri(galleryUri).rkey;
71 return (
72 <Button
73 type="button"
74 variant="secondary"
75 hx-get={`/dialogs/gallery/${rkey}/library`}
76 hx-trigger="click"
77 hx-target="#dialog-target"
78 hx-swap="innerHTML"
79 >
80 <i class="fa-solid fa-plus mr-2" />
81 Add from library
82 </Button>
83 );
84}
85
86export function PhotoItem({
87 photo,
88}: Readonly<{
89 photo: PhotoView;
90}>) {
91 return (
92 <button
93 type="button"
94 class="group relative aspect-square cursor-pointer"
95 _="
96 on click
97 set checkbox to me.querySelector('input[type=checkbox]')
98 set checkbox.checked to not checkbox.checked
99 trigger change on checkbox
100 "
101 >
102 <input
103 type="checkbox"
104 name="photoUri"
105 value={photo.uri}
106 class="peer absolute top-2 left-2 z-30 w-5 h-5 accent-sky-600"
107 _="
108 on change
109 set checkedCount to my.closest('form') or document
110 then set checkedInputs to checkedCount.querySelectorAll('input[type=checkbox]:checked')
111 then set count to checkedInputs.length
112 then set #selected-count's innerText to `${count} selected`"
113 />
114
115 <img
116 src={photo.fullsize}
117 alt={photo.alt}
118 loading="lazy"
119 class="w-full h-full object-cover transition-opacity duration-200 peer-checked:opacity-50"
120 />
121 </button>
122 );
123}