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}