From 4f8d76f79700d88c3974b352854beb2eec724dff Mon Sep 17 00:00:00 2001 From: MartinBraquet Date: Thu, 4 Dec 2025 19:49:38 +0100 Subject: [PATCH] Add description below profile images --- backend/supabase/profiles.sql | 1 + common/src/api/zod-types.ts | 1 + common/src/supabase/schema.ts | 3 + web/components/optional-profile-form.tsx | 7 + web/components/profile-carousel.tsx | 256 +++++++++++------------ web/components/widgets/add-photos.tsx | 49 +++-- 6 files changed, 172 insertions(+), 145 deletions(-) diff --git a/backend/supabase/profiles.sql b/backend/supabase/profiles.sql index 23db9e2..cac05d8 100644 --- a/backend/supabase/profiles.sql +++ b/backend/supabase/profiles.sql @@ -25,6 +25,7 @@ CREATE TABLE IF NOT EXISTS profiles ( has_kids INTEGER, height_in_inches INTEGER, id BIGINT GENERATED ALWAYS AS IDENTITY NOT NULL, + image_descriptions jsonb, is_smoker BOOLEAN, diet TEXT[], last_modification_time TIMESTAMPTZ DEFAULT now() NOT NULL, diff --git a/common/src/api/zod-types.ts b/common/src/api/zod-types.ts index 9b6dc71..42de723 100644 --- a/common/src/api/zod-types.ts +++ b/common/src/api/zod-types.ts @@ -103,6 +103,7 @@ const optionalProfilesSchema = z.object({ twitter: z.string().optional().nullable(), university: z.string().optional().nullable(), website: z.string().optional().nullable(), + image_descriptions: z.any().optional().nullable(), }) export const combinedProfileSchema = diff --git a/common/src/supabase/schema.ts b/common/src/supabase/schema.ts index c9ffbbf..c49afab 100644 --- a/common/src/supabase/schema.ts +++ b/common/src/supabase/schema.ts @@ -758,6 +758,7 @@ export type Database = { has_kids: number | null height_in_inches: number | null id: number + image_descriptions: Json | null is_smoker: boolean | null languages: string[] | null last_modification_time: string @@ -812,6 +813,7 @@ export type Database = { has_kids?: number | null height_in_inches?: number | null id?: number + image_descriptions?: Json | null is_smoker?: boolean | null languages?: string[] | null last_modification_time?: string @@ -866,6 +868,7 @@ export type Database = { has_kids?: number | null height_in_inches?: number | null id?: number + image_descriptions?: Json | null is_smoker?: boolean | null languages?: string[] | null last_modification_time?: string diff --git a/web/components/optional-profile-form.tsx b/web/components/optional-profile-form.tsx index 15b3bca..3cd27bd 100644 --- a/web/components/optional-profile-form.tsx +++ b/web/components/optional-profile-form.tsx @@ -694,6 +694,13 @@ export const OptionalProfileUserForm = (props: { pinned_url={profile.pinned_url} setPhotoUrls={(urls) => setProfile('photo_urls', urls)} setPinnedUrl={(url) => setProfile('pinned_url', url)} + setDescription={(url, description) => + setProfile("image_descriptions", { + ...(profile?.image_descriptions as Record ?? {}), + [url]: description, + }) + } + image_descriptions={profile.image_descriptions as Record} /> diff --git a/web/components/profile-carousel.tsx b/web/components/profile-carousel.tsx index 8409931..d6f2ea2 100644 --- a/web/components/profile-carousel.tsx +++ b/web/components/profile-carousel.tsx @@ -1,46 +1,39 @@ import {useState} from 'react' -import clsx from 'clsx' import Image from 'next/image' import {buildArray} from 'common/util/array' import {Carousel} from 'web/components/widgets/carousel' -import {Modal, MODAL_CLASS} from 'web/components/layout/modal' +import {Modal} from 'web/components/layout/modal' import {Col} from 'web/components/layout/col' import {SignUpButton} from './nav/sidebar' import {Profile} from 'common/profiles/profile' -import {Button} from 'web/components/buttons/button' -import {updateProfile} from 'web/lib/api' -import {Row} from 'web/components/layout/row' import {useUser} from 'web/hooks/use-user' -import {PlusIcon} from '@heroicons/react/solid' -import {EditablePhotoGrid} from './widgets/editable-photo-grid' -import {AddPhotosWidget} from './widgets/add-photos' export default function ProfileCarousel(props: { profile: Profile, refreshProfile: () => void, }) { - const {profile, refreshProfile} = props + const {profile} = props const photoNums = profile.photo_urls ? profile.photo_urls.length : 0 const [lightboxUrl, setLightboxUrl] = useState('') const [lightboxOpen, setLightboxOpen] = useState(false) - const [isEditMode, setIsEditMode] = useState(false) - const [addPhotosOpen, setAddPhotosOpen] = useState(false) + // const [isEditMode, setIsEditMode] = useState(false) + // const [addPhotosOpen, setAddPhotosOpen] = useState(false) - const [pinnedUrl, setPinnedUrl] = useState(profile.pinned_url) - const [photoUrls, setPhotoUrls] = useState(profile.photo_urls ?? []) + // const [pinnedUrl, setPinnedUrl] = useState(profile.pinned_url) + // const [photoUrls, setPhotoUrls] = useState(profile.photo_urls ?? []) const currentUser = useUser() - const isCurrentUser = currentUser?.id === profile.user_id + // const isCurrentUser = currentUser?.id === profile.user_id - const handleSaveChanges = async () => { - await updateProfile({ - pinned_url: pinnedUrl ?? undefined, - photo_urls: photoUrls, - }) - setIsEditMode(false) - refreshProfile() - } + // const handleSaveChanges = async () => { + // await updateProfile({ + // pinned_url: pinnedUrl ?? undefined, + // photo_urls: photoUrls, + // }) + // setIsEditMode(false) + // refreshProfile() + // } if (!currentUser && profile.visibility !== 'public') { return ( @@ -78,82 +71,83 @@ export default function ProfileCarousel(props: { return ( <> -
- {isCurrentUser && !isEditMode && ( - - )} - {isCurrentUser && isEditMode && ( - - - - - )} -
+ {/*
*/} + {/* {isCurrentUser && !isEditMode && (*/} + {/* setIsEditMode(true)}*/} + {/* color="gray-outline"*/} + {/* size="sm"*/} + {/* >*/} + {/* Edit photos*/} + {/* */} + {/* )}*/} + {/* {isCurrentUser && isEditMode && (*/} + {/* */} + {/* {*/} + {/* // TODO this is stale if you've saved*/} + {/* setPhotoUrls(profile.photo_urls ?? [])*/} + {/* setPinnedUrl(profile.pinned_url)*/} + {/* setIsEditMode(false)*/} + {/* }}*/} + {/* color="gray-outline"*/} + {/* size="sm"*/} + {/* >*/} + {/* Cancel*/} + {/* */} + {/* */} + {/* */} + {/* )}*/} + {/*
*/} - {isEditMode ? ( - - { - const newPinnedUrl = newOrder[0] - const newPhotoUrls = newOrder.filter( - (url) => url !== newPinnedUrl - ) - setPinnedUrl(newPinnedUrl) - setPhotoUrls(newPhotoUrls) - }} - onDelete={(url) => { - if (url === pinnedUrl) { - const newPhotos = photoUrls.filter((u) => u !== url) - setPinnedUrl(newPhotos[0] ?? null) - setPhotoUrls(newPhotos.slice(1)) - } else { - setPhotoUrls(photoUrls.filter((u) => u !== url)) - } - }} - onSetProfilePic={(url) => { - if (url === pinnedUrl) return - setPinnedUrl(url) - setPhotoUrls( - [...photoUrls.filter((u) => u !== url), pinnedUrl].filter( - Boolean - ) as string[] - ) - }} - /> - - - ) : ( - - {buildArray(profile.pinned_url, profile.photo_urls).map((url, i) => ( -
+ {/*{isEditMode ? (*/} + {/* */} + {/* {*/} + {/* const newPinnedUrl = newOrder[0]*/} + {/* const newPhotoUrls = newOrder.filter(*/} + {/* (url) => url !== newPinnedUrl*/} + {/* )*/} + {/* setPinnedUrl(newPinnedUrl)*/} + {/* setPhotoUrls(newPhotoUrls)*/} + {/* }}*/} + {/* onDelete={(url) => {*/} + {/* if (url === pinnedUrl) {*/} + {/* const newPhotos = photoUrls.filter((u) => u !== url)*/} + {/* setPinnedUrl(newPhotos[0] ?? null)*/} + {/* setPhotoUrls(newPhotos.slice(1))*/} + {/* } else {*/} + {/* setPhotoUrls(photoUrls.filter((u) => u !== url))*/} + {/* }*/} + {/* }}*/} + {/* onSetProfilePic={(url) => {*/} + {/* if (url === pinnedUrl) return*/} + {/* setPinnedUrl(url)*/} + {/* setPhotoUrls(*/} + {/* [...photoUrls.filter((u) => u !== url), pinnedUrl].filter(*/} + {/* Boolean*/} + {/* ) as string[]*/} + {/* )*/} + {/* }}*/} + {/* />*/} + {/* setAddPhotosOpen(true)}*/} + {/* color="gray-outline"*/} + {/* size="sm"*/} + {/* className="self-start"*/} + {/* >*/} + {/* */} + {/* Add photos*/} + {/* */} + {/* */} + {/*) : (*/} + + {buildArray(profile.pinned_url, profile.photo_urls).map((url, i) => ( + +
- ))} - {isCurrentUser && (profile.photo_urls?.length ?? 0) > 1 && ( - - )} -
- )} +

+ {(profile.image_descriptions as Record)?.[url]} +

+ + ))} + {/*{isCurrentUser && (profile.photo_urls?.length ?? 0) > 1 && (*/} + {/* setAddPhotosOpen(true)}*/} + {/* >*/} + {/* */} + {/* */} + {/*)}*/} + + {/* )}*/} - {isCurrentUser && ( - - - - - - - - - )} + {/*{isCurrentUser && (*/} + {/* */} + {/* */} + {/* */} + {/* */} + {/* setAddPhotosOpen(false)}*/} + {/* >*/} + {/* Done*/} + {/* */} + {/* */} + {/* */} + {/* */} + {/*)}*/} ) } diff --git a/web/components/widgets/add-photos.tsx b/web/components/widgets/add-photos.tsx index 2af640f..905d40c 100644 --- a/web/components/widgets/add-photos.tsx +++ b/web/components/widgets/add-photos.tsx @@ -1,29 +1,31 @@ -import { CheckCircleIcon } from '@heroicons/react/outline' -import { XIcon } from '@heroicons/react/solid' +import {CheckCircleIcon} from '@heroicons/react/outline' +import {PlusIcon, XIcon} from '@heroicons/react/solid' import Image from 'next/image' -import { uniq } from 'lodash' -import { useState } from 'react' +import {uniq} from 'lodash' +import {useState} from 'react' import clsx from 'clsx' -import { Col } from 'web/components/layout/col' -import { Button } from 'web/components/buttons/button' -import { uploadImage } from 'web/lib/firebase/storage' -import { buildArray } from 'common/util/array' -import { Row } from 'web/components/layout/row' -import { User } from 'common/user' -import { PlusIcon } from '@heroicons/react/solid' +import {Col} from 'web/components/layout/col' +import {Button} from 'web/components/buttons/button' +import {uploadImage} from 'web/lib/firebase/storage' +import {buildArray} from 'common/util/array' +import {Row} from 'web/components/layout/row' +import {User} from 'common/user' export const AddPhotosWidget = (props: { user: User + image_descriptions: Record | null photo_urls: string[] | null pinned_url: string | null setPhotoUrls: (urls: string[]) => void setPinnedUrl: (url: string) => void + setDescription: (url: string, description: string) => void }) => { - const { user, photo_urls, pinned_url, setPhotoUrls, setPinnedUrl } = props + const {user, photo_urls, pinned_url, setPhotoUrls, setPinnedUrl, setDescription, image_descriptions} = props const [uploadingImages, setUploadingImages] = useState(false) + const handleFileChange = async (e: React.ChangeEvent) => { const files = e.target.files if (!files) return @@ -56,11 +58,11 @@ export const AddPhotosWidget = (props: { {uniq(buildArray(pinned_url, photo_urls))?.map((url, index) => { const isPinned = url === pinned_url @@ -100,14 +102,29 @@ export const AddPhotosWidget = (props: { 'bg-canvas-0 absolute right-0 top-0 !rounded-full !px-1 py-1' )} > - + {`preview + +