feat(org): Load curated sets in workbench
This commit is contained in:
parent
50ccfe1e67
commit
5abbc559b9
4 changed files with 143 additions and 113 deletions
|
@ -7,7 +7,7 @@ import { useTranslation } from 'next-i18next'
|
||||||
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
|
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
|
||||||
import { ns as authNs } from 'shared/components/wrappers/auth/index.mjs'
|
import { ns as authNs } from 'shared/components/wrappers/auth/index.mjs'
|
||||||
import { AuthWrapper } from 'shared/components/wrappers/auth/index.mjs'
|
import { AuthWrapper } from 'shared/components/wrappers/auth/index.mjs'
|
||||||
import { CuratedSets } from 'shared/components/curated-sets.mjs'
|
import { CuratedSetsList } from 'shared/components/curated-sets.mjs'
|
||||||
import { CsetSubmissions } from 'shared/components/submissions/index.mjs'
|
import { CsetSubmissions } from 'shared/components/submissions/index.mjs'
|
||||||
|
|
||||||
// Translation namespaces used on this page
|
// Translation namespaces used on this page
|
||||||
|
@ -29,7 +29,7 @@ const CuratorPage = ({ page }) => {
|
||||||
<h2>{t('curate:suggestedSets')}</h2>
|
<h2>{t('curate:suggestedSets')}</h2>
|
||||||
<CsetSubmissions />
|
<CsetSubmissions />
|
||||||
<h2>{t('curate:sets')}</h2>
|
<h2>{t('curate:sets')}</h2>
|
||||||
<CuratedSets href={(id) => `/curate/sets/${id}`} />
|
<CuratedSetsList href={(id) => `/curate/sets/${id}`} />
|
||||||
</div>
|
</div>
|
||||||
</AuthWrapper>
|
</AuthWrapper>
|
||||||
</PageWrapper>
|
</PageWrapper>
|
||||||
|
|
|
@ -934,112 +934,6 @@ export const UserSetPicker = ({ design, t, href, clickHandler, size = 'lg' }) =>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CuratedSetPicker = ({ design, href, clickHandler, size }) => {
|
|
||||||
// Hooks
|
|
||||||
const backend = useBackend()
|
|
||||||
const { t, i18n } = useTranslation('sets')
|
|
||||||
const { language } = i18n
|
|
||||||
const { control } = useAccount()
|
|
||||||
|
|
||||||
// State
|
|
||||||
const [curatedSets, setCuratedSets] = useState([])
|
|
||||||
const [filter, setFilter] = useState([])
|
|
||||||
const [tags, setTags] = useState([])
|
|
||||||
|
|
||||||
// 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()
|
|
||||||
}, [backend, language])
|
|
||||||
|
|
||||||
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)}`
|
|
||||||
if (set[key]) 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()
|
|
||||||
|
|
||||||
// Need to sort designs by their translated title
|
|
||||||
const translated = {}
|
|
||||||
for (const d of list) translated[t(`${d}.t`)] = d
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<h3 id="curatedsets">{t('account:curatedSets')}</h3>
|
|
||||||
<V3Wip />
|
|
||||||
{tags.map((tag) => (
|
|
||||||
<Tag onClick={() => addFilter(tag)} tag={tag} key={tag}>
|
|
||||||
{tag}
|
|
||||||
</Tag>
|
|
||||||
))}
|
|
||||||
<div className="my-2 p-2 px-4 border rounded-lg bg-secondary bg-opacity-10 max-w-xl">
|
|
||||||
<div className="flex flex-row items-center justify-between gap-2">
|
|
||||||
<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>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 2xl:grid-cols-3 gap-2">
|
|
||||||
{orderBy(list, ['name'], ['asc']).map((set) => (
|
|
||||||
<MsetButton
|
|
||||||
key={set.id}
|
|
||||||
{...{ set, control, design }}
|
|
||||||
href={href}
|
|
||||||
onClick={clickHandler}
|
|
||||||
requiredMeasies={measurements[design]}
|
|
||||||
language={i18n.language}
|
|
||||||
size={size}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const BookmarkedSetPicker = ({ design, clickHandler, t, size, href }) => {
|
export const BookmarkedSetPicker = ({ design, clickHandler, t, size, href }) => {
|
||||||
// Hooks
|
// Hooks
|
||||||
const { account, control } = useAccount()
|
const { account, control } = useAccount()
|
||||||
|
|
|
@ -26,7 +26,15 @@ import { ModalWrapper } from 'shared/components/wrappers/modal.mjs'
|
||||||
import Markdown from 'react-markdown'
|
import Markdown from 'react-markdown'
|
||||||
import Timeago from 'react-timeago'
|
import Timeago from 'react-timeago'
|
||||||
import { MeasieVal } from './account/sets.mjs'
|
import { MeasieVal } from './account/sets.mjs'
|
||||||
import { CameraIcon, UploadIcon, OkIcon, NoIcon } from 'shared/components/icons.mjs'
|
import {
|
||||||
|
TrashIcon,
|
||||||
|
CameraIcon,
|
||||||
|
UploadIcon,
|
||||||
|
OkIcon,
|
||||||
|
NoIcon,
|
||||||
|
BoolNoIcon,
|
||||||
|
BoolYesIcon,
|
||||||
|
} from 'shared/components/icons.mjs'
|
||||||
import { Link, PageLink } from 'shared/components/link.mjs'
|
import { Link, PageLink } from 'shared/components/link.mjs'
|
||||||
import {
|
import {
|
||||||
StringInput,
|
StringInput,
|
||||||
|
@ -63,7 +71,7 @@ const SetLineup = ({ sets = [], href = false, onClick = false }) => (
|
||||||
backgroundPosition: 'center',
|
backgroundPosition: 'center',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if (onClick) props.onClick = () => onClick(set.id)
|
if (onClick) props.onClick = () => onClick(set)
|
||||||
else if (typeof href === 'function') props.href = href(set.id)
|
else if (typeof href === 'function') props.href = href(set.id)
|
||||||
|
|
||||||
if (onClick) return <button {...props} key={set.id}></button>
|
if (onClick) return <button {...props} key={set.id}></button>
|
||||||
|
@ -185,8 +193,11 @@ export const CuratedSet = ({ id }) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Picker version
|
||||||
|
export const CuratedSetPicker = (props) => <CuratedSets {...props} />
|
||||||
|
|
||||||
// Component for the curated-sets page
|
// Component for the curated-sets page
|
||||||
export const CuratedSets = ({ href = false }) => {
|
export const CuratedSets = ({ href = false, clickHandler = false }) => {
|
||||||
// Hooks
|
// Hooks
|
||||||
const backend = useBackend()
|
const backend = useBackend()
|
||||||
const { setLoadingStatus } = useContext(LoadingStatusContext)
|
const { setLoadingStatus } = useContext(LoadingStatusContext)
|
||||||
|
@ -214,7 +225,7 @@ export const CuratedSets = ({ href = false }) => {
|
||||||
sets: Object.values(sets),
|
sets: Object.values(sets),
|
||||||
}
|
}
|
||||||
if (typeof href === 'function') lineupProps.href = href
|
if (typeof href === 'function') lineupProps.href = href
|
||||||
else lineupProps.onClick = setSelected
|
else lineupProps.onClick = clickHandler ? clickHandler : (set) => setSelected(set.id)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-7xl xl:pl-4">
|
<div className="max-w-7xl xl:pl-4">
|
||||||
|
@ -224,6 +235,131 @@ export const CuratedSets = ({ href = false }) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Component for the maintaining the list of curated-sets
|
||||||
|
export const CuratedSetsList = ({ href = false }) => {
|
||||||
|
// Hooks
|
||||||
|
const { t } = useTranslation(ns)
|
||||||
|
const backend = useBackend()
|
||||||
|
const { setLoadingStatus, LoadingProgress } = useContext(LoadingStatusContext)
|
||||||
|
const [refresh, setRefresh] = useState(0)
|
||||||
|
|
||||||
|
// State
|
||||||
|
const [sets, setSets] = useState([])
|
||||||
|
const [selected, setSelected] = useState(false)
|
||||||
|
|
||||||
|
// Effects
|
||||||
|
useEffect(() => {
|
||||||
|
const getSets = async () => {
|
||||||
|
setLoadingStatus([true, 'contactingBackend'])
|
||||||
|
const result = await backend.getCuratedSets()
|
||||||
|
if (result.success) {
|
||||||
|
const allSets = []
|
||||||
|
for (const set of result.data.curatedSets) allSets.push(set)
|
||||||
|
setSets(allSets)
|
||||||
|
setLoadingStatus([true, 'status:dataLoaded', true, true])
|
||||||
|
} else setLoadingStatus([true, 'status:backendError', true, false])
|
||||||
|
}
|
||||||
|
getSets()
|
||||||
|
}, [refresh])
|
||||||
|
|
||||||
|
// Helper var to see how many are selected
|
||||||
|
const selCount = Object.keys(selected).length
|
||||||
|
|
||||||
|
// Helper method to toggle single selection
|
||||||
|
const toggleSelect = (id) => {
|
||||||
|
const newSelected = { ...selected }
|
||||||
|
if (newSelected[id]) delete newSelected[id]
|
||||||
|
else newSelected[id] = 1
|
||||||
|
setSelected(newSelected)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper method to toggle select all
|
||||||
|
const toggleSelectAll = () => {
|
||||||
|
if (selCount === selected.length) setSelected({})
|
||||||
|
else {
|
||||||
|
const newSelected = {}
|
||||||
|
for (const set of selected) newSelected[set.id] = 1
|
||||||
|
setSelected(newSelected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to delete one or more sets
|
||||||
|
const removeSelected = async () => {
|
||||||
|
let i = 0
|
||||||
|
for (const key in selected) {
|
||||||
|
i++
|
||||||
|
await backend.removeCuratedMeasurementsSet(key)
|
||||||
|
setLoadingStatus([
|
||||||
|
true,
|
||||||
|
<LoadingProgress val={i} max={selCount} msg={t('removingRecords')} key="linter" />,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
setSelected({})
|
||||||
|
setRefresh(refresh + 1)
|
||||||
|
setLoadingStatus([true, 'nailedIt', true, true])
|
||||||
|
}
|
||||||
|
|
||||||
|
const lineupProps = {
|
||||||
|
sets: Object.values(sets),
|
||||||
|
}
|
||||||
|
if (typeof href === 'function') lineupProps.href = href
|
||||||
|
else lineupProps.onClick = setSelected
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-7xl xl:pl-4">
|
||||||
|
{selCount ? (
|
||||||
|
<button className="btn btn-error" onClick={removeSelected}>
|
||||||
|
<TrashIcon /> {selCount} {t('curate:sets')}
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
<table className="table table-auto">
|
||||||
|
<thead className="border border-base-300 border-b-2 border-t-0 border-x-0">
|
||||||
|
<tr className="b">
|
||||||
|
<th className="text-base-300 text-base">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="checkbox checkbox-secondary"
|
||||||
|
onClick={toggleSelectAll}
|
||||||
|
checked={selected.length === selCount}
|
||||||
|
/>
|
||||||
|
</th>
|
||||||
|
<th className="text-base-300 text-base">{t('curate:id')}</th>
|
||||||
|
<th className="text-base-300 text-base">{t('curate:img')}</th>
|
||||||
|
<th className="text-base-300 text-base">{t('curate:name')}</th>
|
||||||
|
<th className="text-base-300 text-base">{t('curate:published')}</th>
|
||||||
|
<th className="text-base-300 text-base">{t('curate:createdAt')}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{sets.map((set) => (
|
||||||
|
<tr key={set.id}>
|
||||||
|
<td className="text-base font-medium">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={selected[set.id] ? true : false}
|
||||||
|
className="checkbox checkbox-secondary"
|
||||||
|
onClick={() => toggleSelect(set.id)}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>{set.id}</td>
|
||||||
|
<td>
|
||||||
|
<img
|
||||||
|
src={cloudflareImageUrl({ id: set.img, variant: 'sq100' })}
|
||||||
|
className="mask mask-squircle w-12 h-12"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>{set.nameEn}</td>
|
||||||
|
<td>{set.published ? <BoolYesIcon /> : <BoolNoIcon />}</td>
|
||||||
|
<td>{set.createdAt}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<SetLineup {...lineupProps} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export const EditCuratedSet = ({ id }) => {
|
export const EditCuratedSet = ({ id }) => {
|
||||||
// Hooks
|
// Hooks
|
||||||
const { account } = useAccount()
|
const { account } = useAccount()
|
||||||
|
|
|
@ -9,9 +9,9 @@ import { useTranslation } from 'next-i18next'
|
||||||
import {
|
import {
|
||||||
UserSetPicker,
|
UserSetPicker,
|
||||||
BookmarkedSetPicker,
|
BookmarkedSetPicker,
|
||||||
CuratedSetPicker,
|
|
||||||
ns as setsNs,
|
ns as setsNs,
|
||||||
} from 'shared/components/account/sets.mjs'
|
} from 'shared/components/account/sets.mjs'
|
||||||
|
import { CuratedSetPicker } from 'shared/components/curated-sets.mjs'
|
||||||
import { MeasiesEditor } from './editor.mjs'
|
import { MeasiesEditor } from './editor.mjs'
|
||||||
import { Popout } from 'shared/components/popout/index.mjs'
|
import { Popout } from 'shared/components/popout/index.mjs'
|
||||||
import { Accordion } from 'shared/components/accordion.mjs'
|
import { Accordion } from 'shared/components/accordion.mjs'
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue