206 lines
6.2 KiB
JavaScript
206 lines
6.2 KiB
JavaScript
// Dependencies
|
|
import { capitalize } from 'shared/utils.mjs'
|
|
import { siteConfig } from 'site/site.config.mjs'
|
|
// Context
|
|
import { LoadingStatusContext } from 'shared/context/loading-status-context.mjs'
|
|
import { ModalContext } from 'shared/context/modal-context.mjs'
|
|
// Hooks
|
|
import { useState, useEffect, useContext } from 'react'
|
|
import { useTranslation } from 'next-i18next'
|
|
import { useAccount } from 'shared/hooks/use-account.mjs'
|
|
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
|
import { useToast } from 'shared/hooks/use-toast.mjs'
|
|
// Components
|
|
import Link from 'next/link'
|
|
import { Collapse } from 'shared/components/collapse.mjs'
|
|
import { TrashIcon, EditIcon, FilterIcon } from 'shared/components/icons.mjs'
|
|
import { ModalWrapper } from 'shared/components/wrappers/modal.mjs'
|
|
import Timeago from 'react-timeago'
|
|
import { Tag } from 'shared/components/tag.mjs'
|
|
|
|
export const ns = ['toast', 'curate', 'sets', 'account']
|
|
|
|
export const Row = ({ title, children }) => (
|
|
<div className="flex flex-row flex-wrap items-center lg:gap-4 my-2">
|
|
<div className="w-24 text-left md:text-right block md:inline font-bold pr-4">{title}</div>
|
|
<div className="grow">{children}</div>
|
|
</div>
|
|
)
|
|
|
|
const CuratedSet = ({ set, account, t, setLoadingStatus, backend, refresh, toast, language }) => {
|
|
const { setModal } = useContext(ModalContext)
|
|
|
|
const remove = async () => {
|
|
setLoadingStatus([true, 'status:contactingBackend'])
|
|
const result = await backend.removeCuratedMeasurementsSet(set.id)
|
|
if (result) setLoadingStatus([true, 'status:settingsSaved', true, true])
|
|
else setLoadingStatus([true, 'status:backendError', true, false])
|
|
// This just forces a refresh of the list from the server
|
|
// We obviously did not add a key here, but rather removed one
|
|
refresh()
|
|
}
|
|
|
|
const removeModal = () => {
|
|
setModal(
|
|
<ModalWrapper slideFrom="top">
|
|
<h2>{t('curate:areYouCertain')}</h2>
|
|
<p>{t('curate:deleteCuratedItemWarning')}</p>
|
|
<p className="flex flex-row gap-4 items-center justify-center">
|
|
<button className="btn btn-neutral btn-outline px-8">{t('cancel')}</button>
|
|
<button className="btn btn-error px-8" onClick={remove}>
|
|
{t('delete')}
|
|
</button>
|
|
</p>
|
|
</ModalWrapper>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<Collapse
|
|
title={set[`name${capitalize(language)}`]}
|
|
primary
|
|
buttons={[
|
|
<Link
|
|
key="edit"
|
|
className="btn btn-secondary hover:text-secondary-content border-0"
|
|
href={`/curate/sets/${set.id}`}
|
|
>
|
|
<EditIcon key="button1" />
|
|
</Link>,
|
|
<button
|
|
key="rm"
|
|
className="btn btn-error hover:text-error-content border-0"
|
|
onClick={account.control > 4 ? remove : removeModal}
|
|
>
|
|
<TrashIcon key="button2" />
|
|
</button>,
|
|
]}
|
|
>
|
|
<img src={set.img} />
|
|
<Row title="ID">{set.id}</Row>
|
|
<Row title={t('account:created')}>
|
|
<Timeago date={set.createdAt} />
|
|
</Row>
|
|
<Row title={t('account:updated')}>
|
|
<Timeago date={set.updatedAt} />
|
|
</Row>
|
|
{siteConfig.languages
|
|
.sort()
|
|
.map((lang) => capitalize(lang))
|
|
.map((lang) => (
|
|
<Row
|
|
title={
|
|
<>
|
|
<span className="uppercase">{lang}</span> {t('name')}
|
|
</>
|
|
}
|
|
key={`name${lang}`}
|
|
>
|
|
{set[`name${lang}`]}
|
|
</Row>
|
|
))}
|
|
<Link href={`/curate/sets/${set.id}`} className="btn btn-secondary w-full">
|
|
edit
|
|
</Link>
|
|
</Collapse>
|
|
)
|
|
}
|
|
|
|
export const CurateSets = () => {
|
|
// Context
|
|
const { setLoadingStatus } = useContext(LoadingStatusContext)
|
|
|
|
// Hooks
|
|
const { account } = useAccount()
|
|
const backend = useBackend()
|
|
const { t, i18n } = useTranslation('sets', 'curate', 'toast', 'account')
|
|
const { language } = i18n
|
|
const toast = useToast()
|
|
|
|
// State
|
|
const [curatedSets, setCuratedSets] = useState([])
|
|
const [filter, setFilter] = useState([])
|
|
const [tags, setTags] = useState([])
|
|
const [reload, setReload] = useState(0)
|
|
|
|
// Force a refresh
|
|
const refresh = () => setReload(reload + 1)
|
|
|
|
// Effects
|
|
useEffect(() => {
|
|
const getCuratedSets = async () => {
|
|
const result = await backend.getCuratedSets()
|
|
if (result.success) {
|
|
const all = []
|
|
const allTags = new Set()
|
|
for (const set of result.data.curatedSets) {
|
|
all.push(set)
|
|
for (const tag of set[`tags${capitalize(language)}`]) allTags.add(tag)
|
|
}
|
|
setCuratedSets(all)
|
|
setTags([...allTags])
|
|
}
|
|
}
|
|
getCuratedSets()
|
|
}, [reload])
|
|
|
|
const addFilter = (tag) => {
|
|
const newFilter = [...filter, tag]
|
|
setFilter(newFilter)
|
|
}
|
|
|
|
const removeFilter = (tag) => {
|
|
const newFilter = filter.filter((t) => t !== tag)
|
|
setFilter(newFilter)
|
|
}
|
|
|
|
const applyFilter = () => {
|
|
const newList = new Set()
|
|
for (const set of curatedSets) {
|
|
const setTags = []
|
|
for (const lang of siteConfig.languages) {
|
|
const key = `tags${capitalize(lang)}`
|
|
setTags.push(...set[key])
|
|
}
|
|
let match = 0
|
|
for (const tag of filter) {
|
|
if (setTags.includes(tag)) match++
|
|
}
|
|
if (match === filter.length) newList.add(set)
|
|
}
|
|
|
|
return [...newList]
|
|
}
|
|
|
|
const list = applyFilter()
|
|
|
|
return (
|
|
<div className="max-w-xl xl:pl-4">
|
|
{tags.map((tag) => (
|
|
<Tag onClick={() => addFilter(tag)} key={tag}>
|
|
{tag}
|
|
</Tag>
|
|
))}
|
|
<div className="flex flex-row items-center justify-between gap-2 my-2 p-2 px-4 border rounded-lg bg-secondary bg-opacity-10">
|
|
<FilterIcon className="w-6 h-6 text-secondary" />
|
|
<span>
|
|
{list.length} / {curatedSets.length}
|
|
</span>
|
|
<button onClick={() => setFilter([])} className="btn btn-secondary btn-sm">
|
|
clear
|
|
</button>
|
|
</div>
|
|
{filter.map((tag) => (
|
|
<Tag onClick={() => removeFilter(tag)} color="success" hoverColor="error" key={tag}>
|
|
{tag}
|
|
</Tag>
|
|
))}
|
|
{list.map((set) => (
|
|
<CuratedSet
|
|
key={set.id}
|
|
{...{ set, account, t, setLoadingStatus, backend, refresh, toast, language }}
|
|
/>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|