feat(org): Admin updates for curated sets
This commit is contained in:
parent
a0754bf218
commit
dba3c3731a
8 changed files with 176 additions and 12 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,5 +1,6 @@
|
||||||
# See https://help.github.com/ignore-files/ for more about ignoring files.
|
# See https://help.github.com/ignore-files/ for more about ignoring files.
|
||||||
dump
|
dump
|
||||||
|
production.sqlite
|
||||||
|
|
||||||
# .env
|
# .env
|
||||||
.env
|
.env
|
||||||
|
|
46
sites/org/pages/admin/cset/[id].mjs
Normal file
46
sites/org/pages/admin/cset/[id].mjs
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
38
sites/org/pages/admin/cset/index.mjs
Normal file
38
sites/org/pages/admin/cset/index.mjs
Normal 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'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 { AuthWrapper, ns as authNs } from 'shared/components/wrappers/auth/index.mjs'
|
||||||
import { Loading } from 'shared/components/spinner.mjs'
|
import { Loading } from 'shared/components/spinner.mjs'
|
||||||
import { Hits } from 'shared/components/admin.mjs'
|
import { Hits } from 'shared/components/admin.mjs'
|
||||||
|
import { PageLink } from 'shared/components/link.mjs'
|
||||||
|
|
||||||
// Translation namespaces used on this page
|
// Translation namespaces used on this page
|
||||||
const namespaces = nsMerge(pageNs, authNs)
|
const namespaces = nsMerge(pageNs, authNs)
|
||||||
|
@ -39,6 +40,10 @@ const AdminPage = ({ page }) => {
|
||||||
return (
|
return (
|
||||||
<PageWrapper {...page} title="Administration">
|
<PageWrapper {...page} title="Administration">
|
||||||
<AuthWrapper requiredRole="admin">
|
<AuthWrapper requiredRole="admin">
|
||||||
|
<p>
|
||||||
|
Other admin links:
|
||||||
|
<PageLink href="/admin/cset" txt="Curated measurement sets" />
|
||||||
|
</p>
|
||||||
<h5>Search users</h5>
|
<h5>Search users</h5>
|
||||||
<input
|
<input
|
||||||
autoFocus
|
autoFocus
|
||||||
|
|
|
@ -14,7 +14,7 @@ import {
|
||||||
} from 'shared/utils.mjs'
|
} from 'shared/utils.mjs'
|
||||||
import { measurements } from 'config/measurements.mjs'
|
import { measurements } from 'config/measurements.mjs'
|
||||||
import { measurements as designMeasurements } from 'shared/prebuild/data/design-measurements.mjs'
|
import { measurements as designMeasurements } from 'shared/prebuild/data/design-measurements.mjs'
|
||||||
//import orderBy from 'lodash.orderby'
|
import orderBy from 'lodash.orderby'
|
||||||
// Hooks
|
// Hooks
|
||||||
import { useAccount } from 'shared/hooks/use-account.mjs'
|
import { useAccount } from 'shared/hooks/use-account.mjs'
|
||||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||||
|
@ -44,6 +44,7 @@ import {
|
||||||
MeasieInput,
|
MeasieInput,
|
||||||
DesignDropdown,
|
DesignDropdown,
|
||||||
ListInput,
|
ListInput,
|
||||||
|
NumberInput,
|
||||||
ns as inputNs,
|
ns as inputNs,
|
||||||
} from 'shared/components/inputs.mjs'
|
} from 'shared/components/inputs.mjs'
|
||||||
|
|
||||||
|
@ -65,7 +66,7 @@ const SetLineup = ({ sets = [], href = false, onClick = false }) => (
|
||||||
const props = {
|
const props = {
|
||||||
className: 'aspect-[1/3] w-auto h-96',
|
className: 'aspect-[1/3] w-auto h-96',
|
||||||
style: {
|
style: {
|
||||||
backgroundImage: `url(${cloudflareImageUrl({ id: set.img, type: 'lineup' })})`,
|
backgroundImage: `url(${cloudflareImageUrl({ id: `cset-${set.id}`, type: 'lineup' })})`,
|
||||||
width: 'auto',
|
width: 'auto',
|
||||||
backgroundSize: 'contain',
|
backgroundSize: 'contain',
|
||||||
backgroundRepeat: 'no-repeat',
|
backgroundRepeat: 'no-repeat',
|
||||||
|
@ -118,7 +119,7 @@ const ShowCuratedSet = ({ cset }) => {
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setModal(
|
setModal(
|
||||||
<ModalWrapper flex="col" justify="top lg:justify-center" slideFrom="right">
|
<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>
|
</ModalWrapper>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -130,7 +131,10 @@ const ShowCuratedSet = ({ cset }) => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2>{t('data')}</h2>
|
<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 && (
|
{control >= controlLevels.sets.notes && (
|
||||||
<DisplayRow title={t('notes')}>
|
<DisplayRow title={t('notes')}>
|
||||||
<Mdx md={cset[`notes${capitalize(lang)}`]} />
|
<Mdx md={cset[`notes${capitalize(lang)}`]} />
|
||||||
|
@ -198,7 +202,7 @@ export const CuratedSet = ({ id }) => {
|
||||||
export const CuratedSetPicker = (props) => <CuratedSets {...props} />
|
export const CuratedSetPicker = (props) => <CuratedSets {...props} />
|
||||||
|
|
||||||
// Component for the curated-sets page
|
// Component for the curated-sets page
|
||||||
export const CuratedSets = ({ href = false, clickHandler = false }) => {
|
export const CuratedSets = ({ href = false, clickHandler = false, published = true }) => {
|
||||||
// Hooks
|
// Hooks
|
||||||
const backend = useBackend()
|
const backend = useBackend()
|
||||||
const { setLoadingStatus } = useContext(LoadingStatusContext)
|
const { setLoadingStatus } = useContext(LoadingStatusContext)
|
||||||
|
@ -214,7 +218,9 @@ export const CuratedSets = ({ href = false, clickHandler = false }) => {
|
||||||
const result = await backend.getCuratedSets()
|
const result = await backend.getCuratedSets()
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const allSets = {}
|
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)
|
setSets(allSets)
|
||||||
setLoadingStatus([true, 'status:dataLoaded', true, true])
|
setLoadingStatus([true, 'status:dataLoaded', true, true])
|
||||||
} else setLoadingStatus([true, 'status:backendError', true, false])
|
} else setLoadingStatus([true, 'status:backendError', true, false])
|
||||||
|
@ -223,7 +229,7 @@ export const CuratedSets = ({ href = false, clickHandler = false }) => {
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const lineupProps = {
|
const lineupProps = {
|
||||||
sets: Object.values(sets),
|
sets: orderBy(sets, 'height', 'asc'),
|
||||||
}
|
}
|
||||||
if (typeof href === 'function') lineupProps.href = href
|
if (typeof href === 'function') lineupProps.href = href
|
||||||
else lineupProps.onClick = clickHandler ? clickHandler : (set) => setSelected(set.id)
|
else lineupProps.onClick = clickHandler ? clickHandler : (set) => setSelected(set.id)
|
||||||
|
@ -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:img')}</th>
|
||||||
<th className="text-base-300 text-base">{t('curate:name')}</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:published')}</th>
|
||||||
|
<th className="text-base-300 text-base">{t('curate:height')}</th>
|
||||||
<th className="text-base-300 text-base">{t('curate:createdAt')}</th>
|
<th className="text-base-300 text-base">{t('curate:createdAt')}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
@ -342,15 +349,23 @@ export const CuratedSetsList = ({ href = false }) => {
|
||||||
onClick={() => toggleSelect(set.id)}
|
onClick={() => toggleSelect(set.id)}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>{set.id}</td>
|
<td>
|
||||||
|
<PageLink href={typeof href === 'function' ? href(set.id) : href} txt={set.id} />
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<img
|
<img
|
||||||
src={cloudflareImageUrl({ id: set.img, variant: 'sq100' })}
|
src={cloudflareImageUrl({ id: `cset-${set.id}`, variant: 'sq100' })}
|
||||||
className="mask mask-squircle w-12 h-12"
|
className="mask mask-squircle w-12 h-12"
|
||||||
/>
|
/>
|
||||||
</td>
|
</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.published ? <BoolYesIcon /> : <BoolNoIcon />}</td>
|
||||||
|
<td>{set.height}cm</td>
|
||||||
<td>{set.createdAt}</td>
|
<td>{set.createdAt}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
@ -415,6 +430,7 @@ export const EditCuratedSet = ({ id }) => {
|
||||||
k = `notes${capitalize(lang)}`
|
k = `notes${capitalize(lang)}`
|
||||||
if (data[k] !== cset[k]) changes[k] = data[k]
|
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.img !== cset.img) changes.img = data.img
|
||||||
if (data.published !== cset.published) changes.published = data.published
|
if (data.published !== cset.published) changes.published = data.published
|
||||||
for (const m in data.measies) {
|
for (const m in data.measies) {
|
||||||
|
@ -431,7 +447,7 @@ export const EditCuratedSet = ({ id }) => {
|
||||||
<div className="max-w-2xl">
|
<div className="max-w-2xl">
|
||||||
<PageLink href={`/curated-sets/${id}`} txt={`/curated-sets/${id}`} />
|
<PageLink href={`/curated-sets/${id}`} txt={`/curated-sets/${id}`} />
|
||||||
<ListInput
|
<ListInput
|
||||||
label={t('curate:publshed')}
|
label={t('curate:published')}
|
||||||
update={(val) => updateData('published', val)}
|
update={(val) => updateData('published', val)}
|
||||||
list={[
|
list={[
|
||||||
{
|
{
|
||||||
|
@ -458,6 +474,17 @@ export const EditCuratedSet = ({ id }) => {
|
||||||
current={data.published}
|
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>
|
<h2 id="measies">{t('measies')}</h2>
|
||||||
<div className="bg-secondary px-4 pt-1 pb-4 rounded-lg shadow bg-opacity-10">
|
<div className="bg-secondary px-4 pt-1 pb-4 rounded-lg shadow bg-opacity-10">
|
||||||
<DesignDropdown
|
<DesignDropdown
|
||||||
|
|
|
@ -130,6 +130,39 @@ export const ButtonFrame = ({
|
||||||
</button>
|
</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
|
* Input for strings
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -28,6 +28,7 @@ sets: Your Measurements Sets
|
||||||
patterns: Your Patterns
|
patterns: Your Patterns
|
||||||
curate: Curate
|
curate: Curate
|
||||||
curateSets: Curate Sets
|
curateSets: Curate Sets
|
||||||
|
curatedSets: Curated Measurements Sets
|
||||||
code: Code
|
code: Code
|
||||||
patternsAbout: Lists the patterns that you have stored in your FreeSewing account
|
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
|
setsAbout: Lists the measurements sets that you have stored in your FreeSewing account
|
||||||
|
|
|
@ -112,6 +112,11 @@ export const extendSiteNav = async (siteNav, lang) => {
|
||||||
t: t('sections:admin'),
|
t: t('sections:admin'),
|
||||||
_: 1,
|
_: 1,
|
||||||
s: 'admin',
|
s: 'admin',
|
||||||
|
cset: {
|
||||||
|
t: 'Curated Measurement Sets',
|
||||||
|
s: 'admin/cset',
|
||||||
|
_: 1,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add account
|
// Add account
|
||||||
|
@ -161,6 +166,14 @@ export const extendSiteNav = async (siteNav, lang) => {
|
||||||
t: t('yourProfile'),
|
t: t('yourProfile'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add curated measurements sets
|
||||||
|
siteNav['curated-sets'] = {
|
||||||
|
m: 1,
|
||||||
|
s: 'curated-sets',
|
||||||
|
t: t('sections:curatedSets'),
|
||||||
|
n: 1,
|
||||||
|
}
|
||||||
|
|
||||||
// Add translation
|
// Add translation
|
||||||
siteNav.translation = {
|
siteNav.translation = {
|
||||||
s: 'translation',
|
s: 'translation',
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue