Files
Compass/web/components/widgets/add-photos.tsx
2026-04-30 14:13:56 +02:00

168 lines
5.9 KiB
TypeScript

import {CheckCircleIcon} from '@heroicons/react/24/outline'
import {PlusIcon, XMarkIcon} from '@heroicons/react/24/solid'
import clsx from 'clsx'
import {buildArray} from 'common/util/array'
import {uniq} from 'lodash'
import Image from 'next/image'
import {useState} from 'react'
import toast from 'react-hot-toast'
import {Button} from 'web/components/buttons/button'
import {Col} from 'web/components/layout/col'
import {Row} from 'web/components/layout/row'
import {isVideo, uploadImage} from 'web/lib/firebase/storage'
import {useT} from 'web/lib/locale'
export const AddPhotosWidget = (props: {
username: string
image_descriptions: Record<string, string> | null
photo_urls: string[] | null
pinned_url: string | null
setPhotoUrls: (urls: string[]) => void
setPinnedUrl: (url: string) => void
setDescription: (url: string, description: string) => void
onUpload?: (uploading: boolean) => void
}) => {
const {
username,
photo_urls,
pinned_url,
setPhotoUrls,
setPinnedUrl,
setDescription,
image_descriptions,
onUpload,
} = props
const t = useT()
const [uploadingImages, setUploadingImages] = useState(false)
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files
if (!files) return
setUploadingImages(true)
onUpload?.(true)
// Convert files to an array and take only the first 6 files
const selectedFiles = Array.from(files).slice(0, 6)
const urls = await Promise.all(
selectedFiles.map((f) => uploadImage(username, f, 'love-images')),
).catch((e) => {
console.error(e)
toast.error(e)
return []
})
if (!pinned_url) setPinnedUrl(urls[0])
setPhotoUrls(uniq([...(photo_urls ?? []), ...urls]))
setUploadingImages(false)
onUpload?.(false)
}
return (
<Col className="gap-2">
<input
id="photo-upload"
type="file"
onChange={handleFileChange}
multiple // Allows multiple files to be selected
className={'hidden'}
disabled={uploadingImages}
/>
<Row className="flex-wrap gap-2">
<div className="relative" data-testid="profile-upload-photo">
<label
className={clsx(
'bg-canvas-50 hover:bg-ink-300 text-ink-0 dark:text-ink-500 hover:dark:text-ink-600 flex h-[200px] w-[200px] cursor-pointer flex-col items-center rounded-md transition-colors',
uploadingImages && 'opacity-50 cursor-not-allowed',
)}
htmlFor="photo-upload"
>
{uploadingImages ? (
<div className="mx-auto my-auto h-16 w-16 animate-spin rounded-full border-b-2 border-gray-500"></div>
) : (
<PlusIcon className="mx-auto my-auto h-16 w-16 text-gray-500" />
)}
</label>
</div>
{uniq(buildArray(pinned_url, photo_urls))?.map((url, index) => {
const isPinned = url === pinned_url
return (
<div
key={index}
className={clsx(
'relative cursor-pointer rounded-md border-2 p-2',
isPinned ? 'border-teal-500' : 'border-canvas-100',
'hover:border-teal-900',
)}
onClick={() => {
if (isPinned) return
setPhotoUrls(uniq(buildArray(pinned_url, photo_urls)))
setPinnedUrl(url)
}}
>
{isPinned && (
<div className={clsx(' absolute left-0 top-0 rounded-full')}>
<CheckCircleIcon className={' bg-canvas-50 h-6 w-6 rounded-full text-teal-500'} />
</div>
)}
<Button
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
const newUrls = (photo_urls ?? []).filter((u) => u !== url)
if (isPinned) setPinnedUrl(newUrls[0] ?? '')
setPhotoUrls(newUrls)
}}
color={'gray-outline'}
size={'2xs'}
className={clsx('bg-canvas-50 absolute right-0 top-0 !rounded-full !px-1 py-1')}
>
<XMarkIcon className={'h-4 w-4'} />
</Button>
{isVideo(url) ? (
<video
src={url}
width={80}
height={80}
className="h-[200px] w-[200px] object-cover"
autoPlay
muted
loop
playsInline
/>
) : (
<Image
src={url}
width={80}
height={80}
alt={`preview ${index}`}
className="h-[200px] w-[200px] object-cover"
/>
)}
<textarea
// stop click bubbling so clicking/focusing the input doesn't pin the image
onClick={(e: React.MouseEvent<HTMLTextAreaElement>) => e.stopPropagation()}
aria-label={`description for image ${index}`}
placeholder={t('add_photos.add_description', 'Add description')}
value={image_descriptions?.[url] ?? ''}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
e.stopPropagation()
const v = e.target.value
setDescription(url, v)
}}
rows={3}
className="mt-2 w-[200px] rounded border px-2 py-1 text-sm focus:outline-none bg-canvas-50 resize-none overflow-y-auto"
/>
</div>
)
})}
</Row>
{photo_urls?.length ? (
<span className={'text-ink-500 text-xs italic'}>
{t('add_photos.profile_picture_hint', 'The highlighted image is your profile picture')}
</span>
) : null}
</Col>
)
}