mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-05-09 23:54:57 -04:00
314 lines
12 KiB
TypeScript
314 lines
12 KiB
TypeScript
import {XMarkIcon} from '@heroicons/react/24/outline'
|
|
import clsx from 'clsx'
|
|
import {DisplayUser} from 'common/api/user-types'
|
|
import {FilterFields} from 'common/filters'
|
|
import {formatFilters, locationType} from 'common/filters-format'
|
|
import {User} from 'common/user'
|
|
import {Star, Users} from 'lucide-react'
|
|
import Link from 'next/link'
|
|
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 {Modal, MODAL_CLASS, SCROLLABLE_MODAL_CLASS} from 'web/components/layout/modal'
|
|
import {Row} from 'web/components/layout/row'
|
|
import {Avatar} from 'web/components/widgets/avatar'
|
|
import {BookmarkedSearchesType} from 'web/hooks/use-bookmarked-searches'
|
|
import {useChoicesContext} from 'web/hooks/use-choices'
|
|
import {useMeasurementSystem} from 'web/hooks/use-measurement-system'
|
|
import {useUser} from 'web/hooks/use-user'
|
|
import {api} from 'web/lib/api'
|
|
import {useT} from 'web/lib/locale'
|
|
import {deleteBookmarkedSearch} from 'web/lib/supabase/searches'
|
|
|
|
export function BookmarkSearchButton(props: {
|
|
bookmarkedSearches: BookmarkedSearchesType[]
|
|
refreshBookmarkedSearches: () => void
|
|
open: boolean
|
|
setOpen: (checked: boolean) => void
|
|
}) {
|
|
const {bookmarkedSearches, refreshBookmarkedSearches, open, setOpen} = props
|
|
const user = useUser()
|
|
|
|
const t = useT()
|
|
if (!user) return null
|
|
return (
|
|
<>
|
|
<button
|
|
onClick={() => setOpen(true)}
|
|
// color="gray-outline"
|
|
// size={'xs'}
|
|
className={
|
|
'rounded-xl bg-canvas-50 text-xs border-canvas-300 flex items-center gap-1.5 border px-3 py-2 text-ink-500 transition-colors hover:border-primary-400 hover:bg-primary-50'
|
|
}
|
|
>
|
|
<Star className="h-4 w-4 mr-1 hidden sm:flex" />{' '}
|
|
{t('saved_searches.button', 'Saved Searches')}
|
|
</button>
|
|
<ButtonModal
|
|
open={open}
|
|
setOpen={setOpen}
|
|
user={user}
|
|
bookmarkedSearches={bookmarkedSearches}
|
|
refreshBookmarkedSearches={refreshBookmarkedSearches}
|
|
/>
|
|
</>
|
|
)
|
|
}
|
|
|
|
export function ResetFiltersButton(props: {clearFilters: () => void}) {
|
|
const {clearFilters} = props
|
|
const t = useT()
|
|
return (
|
|
<>
|
|
<Button onClick={clearFilters} color="gray-outline" size={'xs'}>
|
|
{t('filter.reset', 'Reset filters')}
|
|
</Button>
|
|
</>
|
|
)
|
|
}
|
|
|
|
function ButtonModal(props: {
|
|
open: boolean
|
|
setOpen: (open: boolean) => void
|
|
user: User
|
|
bookmarkedSearches: BookmarkedSearchesType[]
|
|
refreshBookmarkedSearches: () => void
|
|
}) {
|
|
const {open, setOpen, bookmarkedSearches, refreshBookmarkedSearches} = props
|
|
const t = useT()
|
|
const choicesIdsToLabels = useChoicesContext()
|
|
const {measurementSystem} = useMeasurementSystem()
|
|
return (
|
|
<Modal
|
|
open={open}
|
|
setOpen={setOpen}
|
|
onClose={() => {
|
|
refreshBookmarkedSearches()
|
|
}}
|
|
size={'lg'}
|
|
>
|
|
<Col className={MODAL_CLASS}>
|
|
<h3 className="font-cormorant text-2xl font-medium text-ink-900 mb-4">
|
|
{t('saved_searches.title', 'Saved Searches')}
|
|
</h3>
|
|
{bookmarkedSearches?.length ? (
|
|
<>
|
|
<p className="text-ink-500 text-sm mb-4">
|
|
{t(
|
|
'saved_searches.notification_note',
|
|
"We'll notify you daily when new people match your searches below.",
|
|
)}
|
|
</p>
|
|
<Col className={clsx('divide-y divide-canvas-200 w-full pr-2', SCROLLABLE_MODAL_CLASS)}>
|
|
{(bookmarkedSearches || []).map((search) => (
|
|
<div key={search.id} className="py-3 first:pt-0 last:pb-0">
|
|
<div className="bg-canvas-0 border border-canvas-200 rounded-xl p-3 transition-all hover:border-primary-300 hover:shadow-sm">
|
|
<Row className="items-center justify-between gap-3">
|
|
<div className="flex-1">
|
|
<div className="font-medium text-ink-900 text-sm leading-relaxed">
|
|
{formatFilters(
|
|
search.search_filters as Partial<FilterFields>,
|
|
search.location as locationType,
|
|
choicesIdsToLabels,
|
|
measurementSystem,
|
|
t,
|
|
)?.join(' • ')}
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={async () => {
|
|
await deleteBookmarkedSearch(search.id)
|
|
refreshBookmarkedSearches()
|
|
}}
|
|
className="inline-flex items-center justify-center h-8 w-8 rounded-lg border border-canvas-300 bg-canvas-0 text-ink-500 hover:border-red-300 hover:bg-red-50 hover:text-red-600 transition-all focus:outline-none focus:ring-2 focus:ring-red-400 focus:ring-offset-1"
|
|
>
|
|
<XMarkIcon className="h-4 w-4" />
|
|
</button>
|
|
</Row>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</Col>
|
|
</>
|
|
) : (
|
|
<div className="text-center py-8">
|
|
<div className="text-ink-500 text-sm mb-2">You haven't saved any search.</div>
|
|
<div className="text-ink-300 text-xs">
|
|
{t(
|
|
'saved_searches.empty_state',
|
|
"To save one, click on Get Notified and we'll notify you daily when new people match it.",
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
{/*<BookmarkSearchContent*/}
|
|
{/* total={bookmarkedSearches.length}*/}
|
|
{/* compatibilityQuestion={bookmarkedSearches[questionIndex]}*/}
|
|
{/* user={user}*/}
|
|
{/* onSubmit={() => {*/}
|
|
{/* setOpen(false)*/}
|
|
{/* }}*/}
|
|
{/* isLastQuestion={questionIndex === bookmarkedSearches.length - 1}*/}
|
|
{/* onNext={() => {*/}
|
|
{/* if (questionIndex === bookmarkedSearches.length - 1) {*/}
|
|
{/* setOpen(false)*/}
|
|
{/* } else {*/}
|
|
{/* setQuestionIndex(questionIndex + 1)*/}
|
|
{/* }*/}
|
|
{/* }}*/}
|
|
{/*/>*/}
|
|
</Col>
|
|
</Modal>
|
|
)
|
|
}
|
|
|
|
export function BookmarkStarButton(props: {
|
|
starredUsers: DisplayUser[]
|
|
refreshStars: () => void
|
|
open: boolean
|
|
setOpen: (checked: boolean) => void
|
|
}) {
|
|
const {starredUsers, refreshStars, open, setOpen} = props
|
|
const user = useUser()
|
|
|
|
const t = useT()
|
|
if (!user) return null
|
|
return (
|
|
<>
|
|
<button
|
|
onClick={() => setOpen(true)}
|
|
color="gray-outline"
|
|
// size={'xs'}
|
|
className={
|
|
'rounded-xl bg-canvas-50 text-xs border-canvas-300 flex items-center gap-1.5 border px-3 py-2 text-ink-500 transition-colors hover:border-primary-400 hover:bg-primary-50'
|
|
}
|
|
>
|
|
<Users className="h-4 w-4 mr-1 hidden sm:flex" /> {t('saved_people.button', 'Saved People')}
|
|
</button>
|
|
<StarModal
|
|
open={open}
|
|
setOpen={setOpen}
|
|
user={user}
|
|
starredUsers={starredUsers}
|
|
refreshStars={refreshStars}
|
|
/>
|
|
</>
|
|
)
|
|
}
|
|
|
|
function StarModal(props: {
|
|
open: boolean
|
|
setOpen: (open: boolean) => void
|
|
user: User
|
|
starredUsers: DisplayUser[]
|
|
refreshStars: () => void
|
|
}) {
|
|
const {open, setOpen, starredUsers, refreshStars} = props
|
|
// Track items being optimistically removed so we can hide them immediately
|
|
const [removingIds, setRemovingIds] = useState<Set<string>>(new Set())
|
|
|
|
const t = useT()
|
|
const visibleUsers = (starredUsers || []).filter((u) => !removingIds.has(u.id))
|
|
|
|
return (
|
|
<Modal
|
|
open={open}
|
|
setOpen={setOpen}
|
|
// onClose={() => {
|
|
// refreshBookmarkedSearches()
|
|
// }}
|
|
>
|
|
<Col className={MODAL_CLASS}>
|
|
<h3 className="font-cormorant text-2xl font-medium text-ink-900 mb-4">
|
|
{t('saved_people.title', 'Saved People')}
|
|
</h3>
|
|
{visibleUsers?.length ? (
|
|
<>
|
|
<p className="text-ink-500 text-sm mb-4">
|
|
{t('saved_people.list_header', 'Here are the people you saved:')}
|
|
</p>
|
|
<Col className={clsx('divide-y divide-canvas-200 w-full pr-2', SCROLLABLE_MODAL_CLASS)}>
|
|
{visibleUsers.map((u) => (
|
|
<div key={u.id} className="py-3 first:pt-0 last:pb-0">
|
|
<div className="bg-canvas-0 border border-canvas-200 rounded-xl p-3 transition-all hover:border-primary-300 hover:shadow-sm">
|
|
<Row className="items-center justify-between gap-3">
|
|
<Link className="flex-1 group" href={'/' + u.username}>
|
|
<Row className="items-center gap-3">
|
|
<div className="relative">
|
|
<Avatar
|
|
size="md"
|
|
username={u.username}
|
|
avatarUrl={u.avatarUrl ?? undefined}
|
|
/>
|
|
</div>
|
|
<Col className="flex-1">
|
|
<div className="font-medium text-ink-900 group-hover:text-primary-600 transition-colors">
|
|
{u.name}
|
|
</div>
|
|
<div className="text-ink-500 text-sm">@{u.username}</div>
|
|
</Col>
|
|
</Row>
|
|
</Link>
|
|
<button
|
|
onClick={() => {
|
|
// Optimistically remove the user from the list
|
|
setRemovingIds((prev) => new Set(prev).add(u.id))
|
|
// Fire the API call without blocking UI
|
|
api('star-profile', {
|
|
targetUserId: u.id,
|
|
remove: true,
|
|
})
|
|
.then(() => {
|
|
// Sync with server state
|
|
refreshStars()
|
|
})
|
|
.catch(() => {
|
|
toast.error("Couldn't remove saved profile. Please try again.")
|
|
// Revert optimistic removal on failure
|
|
setRemovingIds((prev) => {
|
|
const next = new Set(prev)
|
|
next.delete(u.id)
|
|
return next
|
|
})
|
|
})
|
|
}}
|
|
className="inline-flex items-center justify-center h-8 w-8 rounded-lg border border-canvas-300 bg-canvas-0 text-ink-500 hover:border-red-300 hover:bg-red-50 hover:text-red-600 transition-all focus:outline-none focus:ring-2 focus:ring-red-400 focus:ring-offset-1"
|
|
>
|
|
<XMarkIcon className="h-4 w-4" />
|
|
</button>
|
|
</Row>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</Col>
|
|
</>
|
|
) : (
|
|
<div className="text-center py-8">
|
|
<div className="text-ink-500 text-sm mb-2">You haven't saved any profile.</div>
|
|
<div className="text-ink-300 text-xs">
|
|
To save one, click on the star on their profile page.
|
|
</div>
|
|
</div>
|
|
)}
|
|
{/*<BookmarkSearchContent*/}
|
|
{/* total={bookmarkedSearches.length}*/}
|
|
{/* compatibilityQuestion={bookmarkedSearches[questionIndex]}*/}
|
|
{/* user={user}*/}
|
|
{/* onSubmit={() => {*/}
|
|
{/* setOpen(false)*/}
|
|
{/* }}*/}
|
|
{/* isLastQuestion={questionIndex === bookmarkedSearches.length - 1}*/}
|
|
{/* onNext={() => {*/}
|
|
{/* if (questionIndex === bookmarkedSearches.length - 1) {*/}
|
|
{/* setOpen(false)*/}
|
|
{/* } else {*/}
|
|
{/* setQuestionIndex(questionIndex + 1)*/}
|
|
{/* }*/}
|
|
{/* }}*/}
|
|
{/*/>*/}
|
|
</Col>
|
|
</Modal>
|
|
)
|
|
}
|