1
0
Fork 0

feat(org): Admin updates for curated sets

This commit is contained in:
Joost De Cock 2023-10-20 10:43:43 +02:00
parent a0754bf218
commit dba3c3731a
8 changed files with 176 additions and 12 deletions

1
.gitignore vendored
View file

@ -1,5 +1,6 @@
# See https://help.github.com/ignore-files/ for more about ignoring files.
dump
production.sqlite
# .env
.env

View file

@ -0,0 +1,46 @@
import { nsMerge } from 'shared/utils.mjs'
// Dependencies
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
// Hooks
import { useTranslation } from 'next-i18next'
// Components
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
import { AuthWrapper, ns as authNs } from 'shared/components/wrappers/auth/index.mjs'
import { EditCuratedSet, ns as csetNs } from 'shared/components/curated-sets.mjs'
// Translation namespaces used on this page
const ns = nsMerge(pageNs, authNs, csetNs, 'curate')
const EditCuratedSetPage = ({ page, id }) => {
const { t } = useTranslation(ns)
return (
<PageWrapper {...page} title={`${t('curate:set')}: ${id}`}>
<AuthWrapper requiredRole="curator">
<EditCuratedSet id={id} />
</AuthWrapper>
</PageWrapper>
)
}
export default EditCuratedSetPage
export async function getStaticProps({ locale, params }) {
return {
props: {
...(await serverSideTranslations(locale, ns)),
id: params.id,
page: {
locale,
path: ['curate', 'sets', params.id],
},
},
}
}
export async function getStaticPaths() {
return {
paths: [],
fallback: true,
}
}

View file

@ -0,0 +1,38 @@
// Dependencies
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import { nsMerge } from 'shared/utils.mjs'
// Hooks
import { useState } from 'react'
import { useTranslation } from 'next-i18next'
import { useBackend } from 'shared/hooks/use-backend.mjs'
// Components
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
import { AuthWrapper, ns as authNs } from 'shared/components/wrappers/auth/index.mjs'
import { Loading } from 'shared/components/spinner.mjs'
import { Hits } from 'shared/components/admin.mjs'
import { CuratedSetsList } from 'shared/components/curated-sets.mjs'
// Translation namespaces used on this page
const namespaces = nsMerge(pageNs, authNs)
const AdminPage = ({ page }) => (
<PageWrapper {...page} title="Manage Curated Sets">
<AuthWrapper requiredRole="admin">
<CuratedSetsList href={(id) => `/admin/cset/${id}`} />
</AuthWrapper>
</PageWrapper>
)
export default AdminPage
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, namespaces)),
page: {
locale,
path: ['admin', 'cset'],
},
},
}
}

View file

@ -10,6 +10,7 @@ import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
import { AuthWrapper, ns as authNs } from 'shared/components/wrappers/auth/index.mjs'
import { Loading } from 'shared/components/spinner.mjs'
import { Hits } from 'shared/components/admin.mjs'
import { PageLink } from 'shared/components/link.mjs'
// Translation namespaces used on this page
const namespaces = nsMerge(pageNs, authNs)
@ -39,6 +40,10 @@ const AdminPage = ({ page }) => {
return (
<PageWrapper {...page} title="Administration">
<AuthWrapper requiredRole="admin">
<p>
Other admin links:
<PageLink href="/admin/cset" txt="Curated measurement sets" />
</p>
<h5>Search users</h5>
<input
autoFocus

View file

@ -14,7 +14,7 @@ import {
} from 'shared/utils.mjs'
import { measurements } from 'config/measurements.mjs'
import { measurements as designMeasurements } from 'shared/prebuild/data/design-measurements.mjs'
//import orderBy from 'lodash.orderby'
import orderBy from 'lodash.orderby'
// Hooks
import { useAccount } from 'shared/hooks/use-account.mjs'
import { useBackend } from 'shared/hooks/use-backend.mjs'
@ -44,6 +44,7 @@ import {
MeasieInput,
DesignDropdown,
ListInput,
NumberInput,
ns as inputNs,
} from 'shared/components/inputs.mjs'
@ -65,7 +66,7 @@ const SetLineup = ({ sets = [], href = false, onClick = false }) => (
const props = {
className: 'aspect-[1/3] w-auto h-96',
style: {
backgroundImage: `url(${cloudflareImageUrl({ id: set.img, type: 'lineup' })})`,
backgroundImage: `url(${cloudflareImageUrl({ id: `cset-${set.id}`, type: 'lineup' })})`,
width: 'auto',
backgroundSize: 'contain',
backgroundRepeat: 'no-repeat',
@ -118,7 +119,7 @@ const ShowCuratedSet = ({ cset }) => {
onClick={() =>
setModal(
<ModalWrapper flex="col" justify="top lg:justify-center" slideFrom="right">
<img src={cloudflareImageUrl({ type: 'lineup', id: cset.img })} />
<img src={cloudflareImageUrl({ type: 'lineup', id: `cset-${cset.id}` })} />
</ModalWrapper>
)
}
@ -130,7 +131,10 @@ const ShowCuratedSet = ({ cset }) => {
</div>
<h2>{t('data')}</h2>
<DisplayRow title={t('name')}>{cset[`name${capitalize(lang)}`]}</DisplayRow>
<DisplayRow title={t('name')}>
<PageLink href={`/curated-sets/${cset.id}`} txt={cset[`name${capitalize(lang)}`]} />
</DisplayRow>
<DisplayRow title={t('height')}>{cset.height}cm</DisplayRow>
{control >= controlLevels.sets.notes && (
<DisplayRow title={t('notes')}>
<Mdx md={cset[`notes${capitalize(lang)}`]} />
@ -198,7 +202,7 @@ export const CuratedSet = ({ id }) => {
export const CuratedSetPicker = (props) => <CuratedSets {...props} />
// Component for the curated-sets page
export const CuratedSets = ({ href = false, clickHandler = false }) => {
export const CuratedSets = ({ href = false, clickHandler = false, published = true }) => {
// Hooks
const backend = useBackend()
const { setLoadingStatus } = useContext(LoadingStatusContext)
@ -214,7 +218,9 @@ export const CuratedSets = ({ href = false, clickHandler = false }) => {
const result = await backend.getCuratedSets()
if (result.success) {
const allSets = {}
for (const set of result.data.curatedSets) allSets[set.id] = set
for (const set of result.data.curatedSets) {
if (!published || set.published) allSets[set.id] = set
}
setSets(allSets)
setLoadingStatus([true, 'status:dataLoaded', true, true])
} else setLoadingStatus([true, 'status:backendError', true, false])
@ -223,7 +229,7 @@ export const CuratedSets = ({ href = false, clickHandler = false }) => {
}, [])
const lineupProps = {
sets: Object.values(sets),
sets: orderBy(sets, 'height', 'asc'),
}
if (typeof href === 'function') lineupProps.href = href
else lineupProps.onClick = clickHandler ? clickHandler : (set) => setSelected(set.id)
@ -236,7 +242,7 @@ export const CuratedSets = ({ href = false, clickHandler = false }) => {
)
}
// Component for the maintaining the list of curated-sets
// Component for the maintaining the list of curated-sets
export const CuratedSetsList = ({ href = false }) => {
// Hooks
const { t } = useTranslation(ns)
@ -328,6 +334,7 @@ export const CuratedSetsList = ({ href = false }) => {
<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:height')}</th>
<th className="text-base-300 text-base">{t('curate:createdAt')}</th>
</tr>
</thead>
@ -342,15 +349,23 @@ export const CuratedSetsList = ({ href = false }) => {
onClick={() => toggleSelect(set.id)}
/>
</td>
<td>{set.id}</td>
<td>
<PageLink href={typeof href === 'function' ? href(set.id) : href} txt={set.id} />
</td>
<td>
<img
src={cloudflareImageUrl({ id: set.img, variant: 'sq100' })}
src={cloudflareImageUrl({ id: `cset-${set.id}`, variant: 'sq100' })}
className="mask mask-squircle w-12 h-12"
/>
</td>
<td>{set.nameEn}</td>
<td>
<PageLink
href={typeof href === 'function' ? href(set.id) : href}
txt={set.nameEn}
/>
</td>
<td>{set.published ? <BoolYesIcon /> : <BoolNoIcon />}</td>
<td>{set.height}cm</td>
<td>{set.createdAt}</td>
</tr>
))}
@ -415,6 +430,7 @@ export const EditCuratedSet = ({ id }) => {
k = `notes${capitalize(lang)}`
if (data[k] !== cset[k]) changes[k] = data[k]
}
if (data.height !== cset.height) changes.height = Number(data.height)
if (data.img !== cset.img) changes.img = data.img
if (data.published !== cset.published) changes.published = data.published
for (const m in data.measies) {
@ -431,7 +447,7 @@ export const EditCuratedSet = ({ id }) => {
<div className="max-w-2xl">
<PageLink href={`/curated-sets/${id}`} txt={`/curated-sets/${id}`} />
<ListInput
label={t('curate:publshed')}
label={t('curate:published')}
update={(val) => updateData('published', val)}
list={[
{
@ -458,6 +474,17 @@ export const EditCuratedSet = ({ id }) => {
current={data.published}
/>
<NumberInput
min={42}
max={215}
step={1}
key="height"
label="Height"
update={(val) => updateData('height', val)}
current={Number(data.height)}
valid={notEmpty}
/>
<h2 id="measies">{t('measies')}</h2>
<div className="bg-secondary px-4 pt-1 pb-4 rounded-lg shadow bg-opacity-10">
<DesignDropdown

View file

@ -130,6 +130,39 @@ export const ButtonFrame = ({
</button>
)
/*
* Input for integers
*/
export const NumberInput = ({
label, // Label to use
update, // onChange handler
valid, // Method that should return whether the value is valid or not
current, // The current value
original, // The original value
placeholder, // The placeholder text
docs = false, // Docs to load, if any
id = '', // An id to tie the input to the label
labelBL = false, // Bottom-Left label
labelBR = false, // Bottom-Right label
max = 0,
min = 220,
step = 1,
}) => (
<FormControl {...{ label, labelBL, labelBR, docs }} forId={id}>
<input
id={id}
type="number"
placeholder={placeholder}
value={current}
onChange={(evt) => update(evt.target.value)}
className={`input w-full input-bordered ${
current === original ? 'input-secondary' : valid(current) ? 'input-success' : 'input-error'
}`}
{...{ max, min, step }}
/>
</FormControl>
)
/*
* Input for strings
*/

View file

@ -28,6 +28,7 @@ sets: Your Measurements Sets
patterns: Your Patterns
curate: Curate
curateSets: Curate Sets
curatedSets: Curated Measurements Sets
code: Code
patternsAbout: Lists the patterns that you have stored in your FreeSewing account
setsAbout: Lists the measurements sets that you have stored in your FreeSewing account

View file

@ -112,6 +112,11 @@ export const extendSiteNav = async (siteNav, lang) => {
t: t('sections:admin'),
_: 1,
s: 'admin',
cset: {
t: 'Curated Measurement Sets',
s: 'admin/cset',
_: 1,
},
}
// Add account
@ -161,6 +166,14 @@ export const extendSiteNav = async (siteNav, lang) => {
t: t('yourProfile'),
}
// Add curated measurements sets
siteNav['curated-sets'] = {
m: 1,
s: 'curated-sets',
t: t('sections:curatedSets'),
n: 1,
}
// Add translation
siteNav.translation = {
s: 'translation',