mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-02-07 04:21:01 -05:00
Add description below profile images
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<string, string> ?? {}),
|
||||
[url]: description,
|
||||
})
|
||||
}
|
||||
image_descriptions={profile.image_descriptions as Record<string, string>}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
|
||||
@@ -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<string | null>(profile.pinned_url)
|
||||
const [photoUrls, setPhotoUrls] = useState<string[]>(profile.photo_urls ?? [])
|
||||
// const [pinnedUrl, setPinnedUrl] = useState<string | null>(profile.pinned_url)
|
||||
// const [photoUrls, setPhotoUrls] = useState<string[]>(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 (
|
||||
<>
|
||||
<div className="flex gap-2 self-end">
|
||||
{isCurrentUser && !isEditMode && (
|
||||
<Button
|
||||
onClick={() => setIsEditMode(true)}
|
||||
color="gray-outline"
|
||||
size="sm"
|
||||
>
|
||||
Edit photos
|
||||
</Button>
|
||||
)}
|
||||
{isCurrentUser && isEditMode && (
|
||||
<Row className="gap-2">
|
||||
<Button
|
||||
onClick={() => {
|
||||
// TODO this is stale if you've saved
|
||||
setPhotoUrls(profile.photo_urls ?? [])
|
||||
setPinnedUrl(profile.pinned_url)
|
||||
setIsEditMode(false)
|
||||
}}
|
||||
color="gray-outline"
|
||||
size="sm"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSaveChanges} size="sm">
|
||||
Save changes
|
||||
</Button>
|
||||
</Row>
|
||||
)}
|
||||
</div>
|
||||
{/*<div className="flex gap-2 self-end">*/}
|
||||
{/* {isCurrentUser && !isEditMode && (*/}
|
||||
{/* <Button*/}
|
||||
{/* onClick={() => setIsEditMode(true)}*/}
|
||||
{/* color="gray-outline"*/}
|
||||
{/* size="sm"*/}
|
||||
{/* >*/}
|
||||
{/* Edit photos*/}
|
||||
{/* </Button>*/}
|
||||
{/* )}*/}
|
||||
{/* {isCurrentUser && isEditMode && (*/}
|
||||
{/* <Row className="gap-2">*/}
|
||||
{/* <Button*/}
|
||||
{/* onClick={() => {*/}
|
||||
{/* // TODO this is stale if you've saved*/}
|
||||
{/* setPhotoUrls(profile.photo_urls ?? [])*/}
|
||||
{/* setPinnedUrl(profile.pinned_url)*/}
|
||||
{/* setIsEditMode(false)*/}
|
||||
{/* }}*/}
|
||||
{/* color="gray-outline"*/}
|
||||
{/* size="sm"*/}
|
||||
{/* >*/}
|
||||
{/* Cancel*/}
|
||||
{/* </Button>*/}
|
||||
{/* <Button onClick={handleSaveChanges} size="sm">*/}
|
||||
{/* Save changes*/}
|
||||
{/* </Button>*/}
|
||||
{/* </Row>*/}
|
||||
{/* )}*/}
|
||||
{/*</div>*/}
|
||||
|
||||
{isEditMode ? (
|
||||
<Col className="gap-4">
|
||||
<EditablePhotoGrid
|
||||
photos={buildArray(pinnedUrl, photoUrls)}
|
||||
onReorder={(newOrder) => {
|
||||
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[]
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => setAddPhotosOpen(true)}
|
||||
color="gray-outline"
|
||||
size="sm"
|
||||
className="self-start"
|
||||
>
|
||||
<PlusIcon className="mr-1 h-5 w-5"/>
|
||||
Add photos
|
||||
</Button>
|
||||
</Col>
|
||||
) : (
|
||||
<Carousel>
|
||||
{buildArray(profile.pinned_url, profile.photo_urls).map((url, i) => (
|
||||
<div key={url} className="h-[300px] w-[300px] flex-none snap-start">
|
||||
{/*{isEditMode ? (*/}
|
||||
{/* <Col className="gap-4">*/}
|
||||
{/* <EditablePhotoGrid*/}
|
||||
{/* photos={buildArray(pinnedUrl, photoUrls)}*/}
|
||||
{/* onReorder={(newOrder) => {*/}
|
||||
{/* 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[]*/}
|
||||
{/* )*/}
|
||||
{/* }}*/}
|
||||
{/* />*/}
|
||||
{/* <Button*/}
|
||||
{/* onClick={() => setAddPhotosOpen(true)}*/}
|
||||
{/* color="gray-outline"*/}
|
||||
{/* size="sm"*/}
|
||||
{/* className="self-start"*/}
|
||||
{/* >*/}
|
||||
{/* <PlusIcon className="mr-1 h-5 w-5"/>*/}
|
||||
{/* Add photos*/}
|
||||
{/* </Button>*/}
|
||||
{/* </Col>*/}
|
||||
{/*) : (*/}
|
||||
<Carousel>
|
||||
{buildArray(profile.pinned_url, profile.photo_urls).map((url, i) => (
|
||||
<Col key={url}>
|
||||
<div className="h-[300px] w-[300px] flex-none snap-start">
|
||||
<Image
|
||||
priority={i < 3}
|
||||
src={url}
|
||||
@@ -168,43 +162,47 @@ export default function ProfileCarousel(props: {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{isCurrentUser && (profile.photo_urls?.length ?? 0) > 1 && (
|
||||
<button
|
||||
className="bg-ink-200 text-ink-0 group flex h-[300px] w-[300px] flex-none cursor-pointer snap-start items-center justify-center rounded ease-in-out"
|
||||
onClick={() => setAddPhotosOpen(true)}
|
||||
>
|
||||
<PlusIcon className="w-20 transition-all group-hover:w-24"/>
|
||||
</button>
|
||||
)}
|
||||
</Carousel>
|
||||
)}
|
||||
<p className="mt-2 px-4 py-1 text-sm w-[300px] whitespace-pre-wrap">
|
||||
{(profile.image_descriptions as Record<string, string>)?.[url]}
|
||||
</p>
|
||||
</Col>
|
||||
))}
|
||||
{/*{isCurrentUser && (profile.photo_urls?.length ?? 0) > 1 && (*/}
|
||||
{/* <button*/}
|
||||
{/* className="bg-ink-200 text-ink-0 group flex h-[300px] w-[300px] flex-none cursor-pointer snap-start items-center justify-center rounded ease-in-out"*/}
|
||||
{/* onClick={() => setAddPhotosOpen(true)}*/}
|
||||
{/* >*/}
|
||||
{/* <PlusIcon className="w-20 transition-all group-hover:w-24"/>*/}
|
||||
{/* </button>*/}
|
||||
{/*)}*/}
|
||||
</Carousel>
|
||||
{/* )}*/}
|
||||
|
||||
<Modal open={lightboxOpen} setOpen={setLightboxOpen}>
|
||||
<Image src={lightboxUrl} width={1000} height={1000} alt=""/>
|
||||
</Modal>
|
||||
|
||||
{isCurrentUser && (
|
||||
<Modal open={addPhotosOpen} setOpen={setAddPhotosOpen}>
|
||||
<Col className={clsx(MODAL_CLASS)}>
|
||||
<AddPhotosWidget
|
||||
user={currentUser}
|
||||
photo_urls={photoUrls}
|
||||
pinned_url={pinnedUrl}
|
||||
setPhotoUrls={setPhotoUrls}
|
||||
setPinnedUrl={setPinnedUrl}
|
||||
/>
|
||||
<Row className="gap-4 self-end">
|
||||
<Button
|
||||
color="gray-outline"
|
||||
onClick={() => setAddPhotosOpen(false)}
|
||||
>
|
||||
Done
|
||||
</Button>
|
||||
</Row>
|
||||
</Col>
|
||||
</Modal>
|
||||
)}
|
||||
{/*{isCurrentUser && (*/}
|
||||
{/* <Modal open={addPhotosOpen} setOpen={setAddPhotosOpen}>*/}
|
||||
{/* <Col className={clsx(MODAL_CLASS)}>*/}
|
||||
{/* <AddPhotosWidget*/}
|
||||
{/* user={currentUser}*/}
|
||||
{/* photo_urls={photoUrls}*/}
|
||||
{/* pinned_url={pinnedUrl}*/}
|
||||
{/* setPhotoUrls={setPhotoUrls}*/}
|
||||
{/* setPinnedUrl={setPinnedUrl}*/}
|
||||
{/* />*/}
|
||||
{/* <Row className="gap-4 self-end">*/}
|
||||
{/* <Button*/}
|
||||
{/* color="gray-outline"*/}
|
||||
{/* onClick={() => setAddPhotosOpen(false)}*/}
|
||||
{/* >*/}
|
||||
{/* Done*/}
|
||||
{/* </Button>*/}
|
||||
{/* </Row>*/}
|
||||
{/* </Col>*/}
|
||||
{/* </Modal>*/}
|
||||
{/*)}*/}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<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
|
||||
}) => {
|
||||
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<HTMLInputElement>) => {
|
||||
const files = e.target.files
|
||||
if (!files) return
|
||||
@@ -56,11 +58,11 @@ export const AddPhotosWidget = (props: {
|
||||
<Row className="flex-wrap gap-2">
|
||||
<label
|
||||
className={clsx(
|
||||
'bg-canvas-50 hover:bg-ink-300 text-ink-0 dark:text-ink-500 hover:dark:text-ink-600 flex h-[100px] w-[100px] cursor-pointer flex-col items-center rounded-md transition-colors'
|
||||
'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'
|
||||
)}
|
||||
htmlFor="photo-upload"
|
||||
>
|
||||
<PlusIcon className=" mx-auto my-auto h-16 w-16 text-gray-500" />
|
||||
<PlusIcon className=" mx-auto my-auto h-16 w-16 text-gray-500"/>
|
||||
</label>
|
||||
{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'
|
||||
)}
|
||||
>
|
||||
<XIcon className={'h-4 w-4'} />
|
||||
<XIcon className={'h-4 w-4'}/>
|
||||
</Button>
|
||||
<Image
|
||||
src={url}
|
||||
width={80}
|
||||
height={80}
|
||||
alt={`preview ${index}`}
|
||||
className="h-20 w-20 object-cover"
|
||||
className="h-[200px] w-[200px] object-cover"
|
||||
/>
|
||||
|
||||
<textarea
|
||||
// stop click bubbling so clicking/focusing the input doesn't pin the image
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
aria-label={`description for image ${index}`}
|
||||
placeholder="Add description"
|
||||
value={image_descriptions?.[url] ?? ''}
|
||||
onChange={(e) => {
|
||||
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>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user