Merge branch 'joost' into plugins-scale
This commit is contained in:
commit
d739e8f5bd
24466 changed files with 405611 additions and 707715 deletions
65
sites/shared/components/accordion.mjs
Normal file
65
sites/shared/components/accordion.mjs
Normal file
|
@ -0,0 +1,65 @@
|
|||
// __SDEFILE__ - This file is a dependency for the stand-alone environment
|
||||
import { useState } from 'react'
|
||||
|
||||
/*
|
||||
* DaisyUI's accordion seems rather unreliable.
|
||||
* So instead, we handle this in React state
|
||||
*/
|
||||
const getProps = (isActive) => ({
|
||||
className: `p-2 px-4 rounded-lg bg-transparent shadow
|
||||
w-full mt-2 py-4 h-auto content-start text-left bg-opacity-20
|
||||
${isActive ? 'hover:bg-transparent' : 'hover:bg-secondary hover:bg-opacity-10'}`,
|
||||
})
|
||||
|
||||
const getSubProps = (isActive) => ({
|
||||
className: ` p-2 px-4 rounded bg-transparent w-full mt-2 py-4 h-auto
|
||||
content-start bg-secondary text-left bg-opacity-20
|
||||
${
|
||||
isActive
|
||||
? 'bg-opacity-100 hover:bg-transparent shadow'
|
||||
: 'hover:bg-opacity-10 hover:bg-secondary '
|
||||
}`,
|
||||
})
|
||||
|
||||
const components = {
|
||||
button: (props) => <button {...props}>{props.children}</button>,
|
||||
div: (props) => <div {...props}>{props.children}</div>,
|
||||
}
|
||||
|
||||
export const BaseAccordion = ({
|
||||
items, // Items in the accordion
|
||||
act, // Allows one to preset the active (opened) entry
|
||||
propsGetter = getProps, // Method to get the relevant props
|
||||
component = 'button',
|
||||
}) => {
|
||||
const [active, setActive] = useState(act)
|
||||
const Component = components[component]
|
||||
|
||||
return (
|
||||
<nav>
|
||||
{items
|
||||
.filter((item) => item[0])
|
||||
.map((item) =>
|
||||
active === item[2] ? (
|
||||
<div key={item[2]} {...propsGetter(true)}>
|
||||
<Component onClick={setActive} className="w-full hover:cursor-pointer">
|
||||
{item[0]}
|
||||
</Component>
|
||||
{item[1]}
|
||||
</div>
|
||||
) : (
|
||||
<Component
|
||||
key={item[2]}
|
||||
{...propsGetter(active === item[2])}
|
||||
onClick={() => setActive(item[2])}
|
||||
>
|
||||
{item[0]}
|
||||
</Component>
|
||||
)
|
||||
)}
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
export const SubAccordion = (props) => <BaseAccordion {...props} propsGetter={getSubProps} />
|
||||
export const Accordion = (props) => <BaseAccordion {...props} propsGetter={getProps} />
|
406
sites/shared/components/account/apikeys.mjs
Normal file
406
sites/shared/components/account/apikeys.mjs
Normal file
|
@ -0,0 +1,406 @@
|
|||
// Dependencies
|
||||
import { useState, useEffect, useContext } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { DateTime } from 'luxon'
|
||||
import { CopyToClipboard } from 'react-copy-to-clipboard'
|
||||
import { shortDate, formatNumber } from 'shared/utils.mjs'
|
||||
// Context
|
||||
import { LoadingStatusContext } from 'shared/context/loading-status-context.mjs'
|
||||
// Hooks
|
||||
import { useAccount } from 'shared/hooks/use-account.mjs'
|
||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
import { useRouter } from 'next/router'
|
||||
// Components
|
||||
import { BackToAccountButton, DisplayRow, NumberBullet } from './shared.mjs'
|
||||
import { Popout } from 'shared/components/popout/index.mjs'
|
||||
import { LeftIcon, PlusIcon, CopyIcon, RightIcon, TrashIcon } from 'shared/components/icons.mjs'
|
||||
import { PageLink, Link } from 'shared/components/link.mjs'
|
||||
import { StringInput, ListInput, FormControl } from 'shared/components/inputs.mjs'
|
||||
import { DynamicMdx } from 'shared/components/mdx/dynamic.mjs'
|
||||
|
||||
export const ns = ['account', 'status']
|
||||
|
||||
const ExpiryPicker = ({ t, expires, setExpires }) => {
|
||||
const router = useRouter()
|
||||
const { locale } = router
|
||||
const [months, setMonths] = useState(1)
|
||||
|
||||
// Run update when component mounts
|
||||
useEffect(() => update(months), [])
|
||||
|
||||
const update = (evt) => {
|
||||
const value = typeof evt === 'number' ? evt : evt.target.value
|
||||
setExpires(DateTime.now().plus({ months: value }))
|
||||
setMonths(value)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max={24}
|
||||
value={months}
|
||||
className="range range-secondary"
|
||||
onChange={update}
|
||||
/>
|
||||
</div>
|
||||
<Popout note compact>
|
||||
{t('keyExpiresDesc')}
|
||||
<b> {shortDate(locale, expires)}</b>
|
||||
</Popout>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const CopyInput = ({ text }) => {
|
||||
const { t } = useTranslation(['status'])
|
||||
const { setLoadingStatus } = useContext(LoadingStatusContext)
|
||||
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const showCopied = () => {
|
||||
setCopied(true)
|
||||
setLoadingStatus([true, t('copiedToClipboard'), true, true])
|
||||
window.setTimeout(() => setCopied(false), 2000)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flez-row gap-2 items-center w-full">
|
||||
<input
|
||||
readOnly
|
||||
value={text}
|
||||
className="input w-full input-bordered flex flex-row"
|
||||
type="text"
|
||||
/>
|
||||
<CopyToClipboard text={text} onCopy={showCopied}>
|
||||
<button className={`btn ${copied ? 'btn-success' : 'btn-secondary'}`}>
|
||||
<CopyIcon />
|
||||
</button>
|
||||
</CopyToClipboard>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const Apikey = ({ apikey }) => {
|
||||
const { t } = useTranslation(ns)
|
||||
const router = useRouter()
|
||||
const { locale } = router
|
||||
|
||||
return apikey ? (
|
||||
<div>
|
||||
<DisplayRow title={t('keyName')}>{apikey.name}</DisplayRow>
|
||||
<DisplayRow title={t('created')}>{shortDate(locale, apikey.createdAt)}</DisplayRow>
|
||||
<DisplayRow title={t('expires')}>{shortDate(locale, apikey.expiresAt)}</DisplayRow>
|
||||
<DisplayRow title="Key ID">{apikey.key}</DisplayRow>
|
||||
<div className="flex flex-row flex-wrap md:gap-2 md:items-center md:justify-between mt-8">
|
||||
<button
|
||||
className="w-full md:w-auto btn btn-secondary pr-6 flex flex-row items-center gap-2"
|
||||
onClick={() => router.push('/account/apikeys')}
|
||||
>
|
||||
<LeftIcon />
|
||||
{t('apikeys')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
|
||||
const ShowKey = ({ apikey, t, clear }) => {
|
||||
const router = useRouter()
|
||||
const { locale } = router
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Popout warning compact>
|
||||
{t('keySecretWarning')}
|
||||
</Popout>
|
||||
<DisplayRow title={t('keyName')}>{apikey.name}</DisplayRow>
|
||||
<DisplayRow title={t('created')}>{shortDate(locale, apikey.createdAt)}</DisplayRow>
|
||||
<DisplayRow title={t('created')}>{shortDate(locale, apikey.expiresAt)}</DisplayRow>
|
||||
<DisplayRow title="Key ID">
|
||||
<CopyInput text={apikey.key} />
|
||||
</DisplayRow>
|
||||
<DisplayRow title="Key Secret">
|
||||
<CopyInput text={apikey.secret} />
|
||||
</DisplayRow>
|
||||
<div className="flex flex-row flex-wrap md:gap-2 md:items-center md:justify-between mt-8">
|
||||
<button
|
||||
className="w-full md:w-auto btn btn-secondary pr-6 flex flex-row items-center gap-2"
|
||||
onClick={() => router.push('/account/apikeys')}
|
||||
>
|
||||
<LeftIcon />
|
||||
{t('apikeys')}
|
||||
</button>
|
||||
<button className="btn btn-primary w-full mt-2 md:w-auto md:mt-0" onClick={clear}>
|
||||
<PlusIcon />
|
||||
{t('newApikey')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const NewKey = ({ account, setGenerate, backend }) => {
|
||||
const [name, setName] = useState('')
|
||||
const [level, setLevel] = useState(1)
|
||||
const [expires, setExpires] = useState(Date.now())
|
||||
const [apikey, setApikey] = useState(false)
|
||||
const { setLoadingStatus } = useContext(LoadingStatusContext)
|
||||
const { t, i18n } = useTranslation(ns)
|
||||
const docs = {}
|
||||
for (const option of ['name', 'expiry', 'level']) {
|
||||
docs[option] = <DynamicMdx language={i18n.language} slug={`docs/site/apikeys/${option}`} />
|
||||
}
|
||||
|
||||
const levels = account.role === 'admin' ? [0, 1, 2, 3, 4, 5, 6, 7, 8] : [0, 1, 2, 3, 4]
|
||||
|
||||
const createKey = async () => {
|
||||
setLoadingStatus([true, 'processingUpdate'])
|
||||
const result = await backend.createApikey({
|
||||
name,
|
||||
level,
|
||||
expiresIn: Math.floor((expires.valueOf() - Date.now().valueOf()) / 1000),
|
||||
})
|
||||
if (result.success) {
|
||||
setLoadingStatus([true, 'nailedIt', true, true])
|
||||
setApikey(result.data.apikey)
|
||||
} else setLoadingStatus([true, 'backendError', true, false])
|
||||
}
|
||||
|
||||
const clear = () => {
|
||||
setApikey(false)
|
||||
setGenerate(false)
|
||||
setName('')
|
||||
setLevel(1)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{apikey ? (
|
||||
<ShowKey {...{ apikey, t, clear }} />
|
||||
) : (
|
||||
<>
|
||||
<StringInput
|
||||
id="apikey-name"
|
||||
label={t('keyName')}
|
||||
docs={docs.name}
|
||||
current={name}
|
||||
update={setName}
|
||||
valid={(val) => val.length > 0}
|
||||
placeholder={'Alicia Key'}
|
||||
/>
|
||||
<FormControl label={t('keyExpires')} docs={docs.expiry}>
|
||||
<ExpiryPicker {...{ t, expires, setExpires }} />
|
||||
</FormControl>
|
||||
<ListInput
|
||||
id="apikey-level"
|
||||
label={t('keyLevel')}
|
||||
docs={docs.level}
|
||||
list={levels.map((l) => ({
|
||||
val: l,
|
||||
label: (
|
||||
<div className="flex flex-row items-center w-full justify-between">
|
||||
<span>{t(`keyLevel${l}`)}</span>
|
||||
<NumberBullet nr={l} color="secondary" />
|
||||
</div>
|
||||
),
|
||||
}))}
|
||||
current={level}
|
||||
update={setLevel}
|
||||
/>
|
||||
<div className="flex flex-row gap-2 items-center w-full my-8">
|
||||
<button
|
||||
className="btn btn-primary capitalize w-full md:w-auto"
|
||||
disabled={name.length < 1}
|
||||
onClick={createKey}
|
||||
>
|
||||
{t('newApikey')}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Component for the 'new/apikey' page
|
||||
export const NewApikey = () => {
|
||||
// Hooks
|
||||
const { account } = useAccount()
|
||||
const backend = useBackend()
|
||||
|
||||
// State
|
||||
const [generate, setGenerate] = useState(false)
|
||||
const [added, setAdded] = useState(0)
|
||||
|
||||
// Helper method to force refresh
|
||||
const keyAdded = () => setAdded(added + 1)
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl xl:pl-4">
|
||||
<NewKey
|
||||
{...{
|
||||
account,
|
||||
generate,
|
||||
setGenerate,
|
||||
backend,
|
||||
keyAdded,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Component for the account/apikeys page
|
||||
export const Apikeys = () => {
|
||||
const router = useRouter()
|
||||
const { locale } = router
|
||||
|
||||
// Hooks
|
||||
const { account } = useAccount()
|
||||
const backend = useBackend()
|
||||
const { t } = useTranslation(ns)
|
||||
const { setLoadingStatus, LoadingProgress } = useContext(LoadingStatusContext)
|
||||
|
||||
// State
|
||||
const [keys, setKeys] = useState([])
|
||||
const [selected, setSelected] = useState({})
|
||||
const [refresh, setRefresh] = useState(0)
|
||||
|
||||
// Helper var to see how many are selected
|
||||
const selCount = Object.keys(selected).length
|
||||
|
||||
// Effects
|
||||
useEffect(() => {
|
||||
const getApikeys = async () => {
|
||||
const result = await backend.getApikeys()
|
||||
if (result.success) setKeys(result.data.apikeys)
|
||||
}
|
||||
getApikeys()
|
||||
}, [refresh])
|
||||
|
||||
// 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 === keys.length) setSelected({})
|
||||
else {
|
||||
const newSelected = {}
|
||||
for (const key of keys) newSelected[key.id] = 1
|
||||
setSelected(newSelected)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to delete one or more apikeys
|
||||
const removeSelectedApikeys = async () => {
|
||||
let i = 0
|
||||
for (const key in selected) {
|
||||
i++
|
||||
await backend.removeApikey(key)
|
||||
setLoadingStatus([
|
||||
true,
|
||||
<LoadingProgress val={i} max={selCount} msg={t('removingApikeys')} key="linter" />,
|
||||
])
|
||||
}
|
||||
setSelected({})
|
||||
setRefresh(refresh + 1)
|
||||
setLoadingStatus([true, 'nailedIt', true, true])
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl xl:pl-4">
|
||||
<p className="text-center md:text-right">
|
||||
<Link
|
||||
className="btn btn-primary capitalize w-full md:w-auto"
|
||||
bottom
|
||||
primary
|
||||
href="/new/apikey"
|
||||
>
|
||||
<PlusIcon />
|
||||
{t('newApikey')}
|
||||
</Link>
|
||||
</p>
|
||||
{selCount ? (
|
||||
<button className="btn btn-error" onClick={removeSelectedApikeys}>
|
||||
<TrashIcon /> {selCount} {t('apikeys')}
|
||||
</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={keys.length === selCount}
|
||||
/>
|
||||
</th>
|
||||
<th className="text-base-300 text-base">{t('keyName')}</th>
|
||||
<th className="text-base-300 text-base">
|
||||
<span className="hidden md:inline">{t('keyLevel')}</span>
|
||||
<span role="img" className="inline md:hidden">
|
||||
🔐
|
||||
</span>
|
||||
</th>
|
||||
<th className="text-base-300 text-base">{t('keyExpires')}</th>
|
||||
<th className="text-base-300 text-base hidden md:block">{t('apiCalls')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{keys.map((apikey, i) => (
|
||||
<tr key={i}>
|
||||
<td className="text-base font-medium">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selected[apikey.id] ? true : false}
|
||||
className="checkbox checkbox-secondary"
|
||||
onClick={() => toggleSelect(apikey.id)}
|
||||
/>
|
||||
</td>
|
||||
<td className="text-base font-medium">
|
||||
<PageLink href={`/account/apikeys/${apikey.id}`} txt={apikey.name} />
|
||||
</td>
|
||||
<td className="text-base font-medium">
|
||||
{apikey.level}
|
||||
<small className="hidden md:inline pl-2 text-base-300 italic">
|
||||
({t(`keyLevel${apikey.level}`)})
|
||||
</small>
|
||||
</td>
|
||||
<td className="text-base font-medium">
|
||||
{shortDate(locale, apikey.expiresAt, false)}
|
||||
</td>
|
||||
<td className="text-base font-medium hidden md:block">
|
||||
{formatNumber(apikey.calls)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<BackToAccountButton />
|
||||
{account.control < 5 ? (
|
||||
<Popout link>
|
||||
<h5>{t('keyDocsTitle')}</h5>
|
||||
<p>{t('keyDocsMsg')}</p>
|
||||
<p className="text-right">
|
||||
<a
|
||||
className="btn btn-secondary mt-2"
|
||||
href="https://freesewing.dev/reference/backend/apikeys"
|
||||
>
|
||||
FreeSewing.dev
|
||||
<RightIcon />
|
||||
</a>
|
||||
</p>
|
||||
</Popout>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
98
sites/shared/components/account/bio.mjs
Normal file
98
sites/shared/components/account/bio.mjs
Normal file
|
@ -0,0 +1,98 @@
|
|||
// Dependencies
|
||||
import { useState, useContext } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
// Context
|
||||
import { LoadingStatusContext } from 'shared/context/loading-status-context.mjs'
|
||||
// Hooks
|
||||
import { useAccount } from 'shared/hooks/use-account.mjs'
|
||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
// Components
|
||||
import { Icons, welcomeSteps, BackToAccountButton } from './shared.mjs'
|
||||
import { SaveSettingsButton } from 'shared/components/buttons/save-settings-button.mjs'
|
||||
import { ContinueButton } from 'shared/components/buttons/continue-button.mjs'
|
||||
import { MarkdownInput } from 'shared/components/inputs.mjs'
|
||||
import { DynamicMdx } from 'shared/components/mdx/dynamic.mjs'
|
||||
import { TipIcon } from 'shared/components/icons.mjs'
|
||||
|
||||
export const ns = ['account', 'status']
|
||||
|
||||
export const Tab = ({ id, activeTab, setActiveTab, t }) => (
|
||||
<button
|
||||
className={`text-xl font-bold capitalize tab tab-bordered grow
|
||||
${activeTab === id ? 'tab-active' : ''}`}
|
||||
onClick={() => setActiveTab(id)}
|
||||
>
|
||||
{t(id)}
|
||||
</button>
|
||||
)
|
||||
|
||||
export const BioSettings = ({ welcome = false }) => {
|
||||
// Hooks
|
||||
const { account, setAccount } = useAccount()
|
||||
const backend = useBackend()
|
||||
const { t, i18n } = useTranslation(ns)
|
||||
const { setLoadingStatus } = useContext(LoadingStatusContext)
|
||||
|
||||
// State
|
||||
const [bio, setBio] = useState(account.bio)
|
||||
|
||||
// Helper method to save bio
|
||||
const save = async () => {
|
||||
setLoadingStatus([true, 'processingUpdate'])
|
||||
const result = await backend.updateAccount({ bio })
|
||||
if (result.success) {
|
||||
setAccount(result.data.account)
|
||||
setLoadingStatus([true, 'settingsSaved', true, true])
|
||||
} else setLoadingStatus([true, 'backendError', true, false])
|
||||
}
|
||||
|
||||
// Next step in the onboarding
|
||||
const nextHref =
|
||||
welcomeSteps[account.control].length > 5
|
||||
? '/welcome/' + welcomeSteps[account.control][6]
|
||||
: '/docs/guide'
|
||||
|
||||
return (
|
||||
<div className="max-w-xl xl:pl-4">
|
||||
<MarkdownInput
|
||||
id="account-bio"
|
||||
label={t('bioTitle')}
|
||||
update={setBio}
|
||||
current={bio}
|
||||
placeholder={t('bioTitle')}
|
||||
docs={<DynamicMdx language={i18n.language} slug={`docs/site/account/bio`} />}
|
||||
labelBL={
|
||||
<span className="flex flex-row items-center gap-1">
|
||||
<TipIcon className="w-6 h-6 text-success" />
|
||||
{t('mdSupport')}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<SaveSettingsButton btnProps={{ onClick: save }} welcome={welcome} />
|
||||
{!welcome && <BackToAccountButton />}
|
||||
|
||||
{welcome ? (
|
||||
<>
|
||||
<ContinueButton btnProps={{ href: nextHref }} link />
|
||||
{welcomeSteps[account.control].length > 0 ? (
|
||||
<>
|
||||
<progress
|
||||
className="progress progress-primary w-full mt-12"
|
||||
value={600 / welcomeSteps[account.control].length}
|
||||
max="100"
|
||||
></progress>
|
||||
<span className="pt-4 text-sm font-bold opacity-50">
|
||||
6 / {welcomeSteps[account.control].length}
|
||||
</span>
|
||||
<Icons
|
||||
done={welcomeSteps[account.control].slice(0, 5)}
|
||||
todo={welcomeSteps[account.control].slice(6)}
|
||||
current="bio"
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
241
sites/shared/components/account/bookmarks.mjs
Normal file
241
sites/shared/components/account/bookmarks.mjs
Normal file
|
@ -0,0 +1,241 @@
|
|||
// Context
|
||||
import { LoadingStatusContext } from 'shared/context/loading-status-context.mjs'
|
||||
// Hooks
|
||||
import { useState, useEffect, Fragment, useContext } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
import { useRouter } from 'next/router'
|
||||
// Components
|
||||
import { BackToAccountButton } from './shared.mjs'
|
||||
import { PlusIcon, TrashIcon, LeftIcon } from 'shared/components/icons.mjs'
|
||||
import { PageLink, WebLink, Link } from 'shared/components/link.mjs'
|
||||
import { DisplayRow } from './shared.mjs'
|
||||
import { StringInput } from 'shared/components/inputs.mjs'
|
||||
import { DynamicMdx } from 'shared/components/mdx/dynamic.mjs'
|
||||
|
||||
export const ns = ['account', 'status']
|
||||
|
||||
export const types = ['design', 'pattern', 'set', 'cset', 'doc', 'custom']
|
||||
|
||||
export const Bookmark = ({ bookmark }) => {
|
||||
const { t } = useTranslation(ns)
|
||||
|
||||
return bookmark ? (
|
||||
<div>
|
||||
<DisplayRow title={t('title')}>{bookmark.title}</DisplayRow>
|
||||
<DisplayRow title={t('url')}>
|
||||
{bookmark.url.length > 30 ? bookmark.url.slice(0, 30) + '...' : bookmark.url}
|
||||
</DisplayRow>
|
||||
<DisplayRow title={t('type')}>{t(`${bookmark.type}Bookmark`)}</DisplayRow>
|
||||
<div className="flex flex-row flex-wrap md:gap-2 md:items-center md:justify-between mt-8">
|
||||
<Link
|
||||
href="/account/bookmarks"
|
||||
className="w-full md:w-auto btn btn-secondary pr-6 flex flex-row items-center gap-2"
|
||||
>
|
||||
<LeftIcon />
|
||||
{t('bookmarks')}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
|
||||
// Component for the 'new/apikey' page
|
||||
export const NewBookmark = () => {
|
||||
// Hooks
|
||||
const { setLoadingStatus } = useContext(LoadingStatusContext)
|
||||
const router = useRouter()
|
||||
const backend = useBackend()
|
||||
const { t, i18n } = useTranslation(ns)
|
||||
const docs = {}
|
||||
for (const option of ['title', 'location', 'type']) {
|
||||
docs[option] = <DynamicMdx language={i18n.language} slug={`docs/site/bookmarks/${option}`} />
|
||||
}
|
||||
|
||||
// State
|
||||
const [title, setTitle] = useState('')
|
||||
const [url, setUrl] = useState('')
|
||||
|
||||
const createBookmark = async () => {
|
||||
setLoadingStatus([true, 'processingUpdate'])
|
||||
const result = await backend.createBookmark({
|
||||
title,
|
||||
url,
|
||||
type: 'custom',
|
||||
})
|
||||
if (result.success) {
|
||||
setLoadingStatus([true, 'nailedIt', true, true])
|
||||
router.push('/account/bookmarks')
|
||||
} else setLoadingStatus([true, 'backendError', true, false])
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl xl:pl-4">
|
||||
<StringInput
|
||||
id="bookmark-title"
|
||||
label={t('title')}
|
||||
docs={docs.title}
|
||||
update={setTitle}
|
||||
current={title}
|
||||
valid={(val) => val.length > 0}
|
||||
placeholder={t('account')}
|
||||
/>
|
||||
<StringInput
|
||||
id="bookmark-url"
|
||||
label={t('location')}
|
||||
docs={docs.location}
|
||||
update={setUrl}
|
||||
current={url}
|
||||
valid={(val) => val.length > 0}
|
||||
placeholder={'https://freesewing.org/account'}
|
||||
/>
|
||||
<div className="flex flex-row gap-2 items-center w-full my-8">
|
||||
<button
|
||||
className="btn btn-primary grow capitalize"
|
||||
disabled={!(title.length > 0 && url.length > 0)}
|
||||
onClick={createBookmark}
|
||||
>
|
||||
{t('newBookmark')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Component for the account/bookmarks page
|
||||
export const Bookmarks = () => {
|
||||
// Hooks
|
||||
const backend = useBackend()
|
||||
const { t } = useTranslation(ns)
|
||||
const { setLoadingStatus, LoadingProgress } = useContext(LoadingStatusContext)
|
||||
|
||||
// State
|
||||
const [bookmarks, setBookmarks] = useState([])
|
||||
const [selected, setSelected] = useState({})
|
||||
const [refresh, setRefresh] = useState(0)
|
||||
|
||||
// Helper var to see how many are selected
|
||||
const selCount = Object.keys(selected).length
|
||||
|
||||
// Effects
|
||||
useEffect(() => {
|
||||
const getBookmarks = async () => {
|
||||
const result = await backend.getBookmarks()
|
||||
if (result.success) setBookmarks(result.data.bookmarks)
|
||||
}
|
||||
getBookmarks()
|
||||
}, [refresh])
|
||||
|
||||
// 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 === bookmarks.length) setSelected({})
|
||||
else {
|
||||
const newSelected = {}
|
||||
for (const bookmark of bookmarks) newSelected[bookmark.id] = 1
|
||||
setSelected(newSelected)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to delete one or more bookmarks
|
||||
const removeSelectedBookmarks = async () => {
|
||||
let i = 0
|
||||
for (const id in selected) {
|
||||
i++
|
||||
await backend.removeBookmark(id)
|
||||
setLoadingStatus([
|
||||
true,
|
||||
<LoadingProgress val={i} max={selCount} msg={t('removingBookmarks')} key="linter" />,
|
||||
])
|
||||
}
|
||||
setSelected({})
|
||||
setRefresh(refresh + 1)
|
||||
setLoadingStatus([true, 'nailedIt', true, true])
|
||||
}
|
||||
|
||||
const perType = {}
|
||||
for (const type of types) perType[type] = bookmarks.filter((b) => b.type === type)
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl xl:pl-4">
|
||||
<p className="text-center md:text-right">
|
||||
<Link
|
||||
className="btn btn-primary capitalize w-full md:w-auto"
|
||||
bottom
|
||||
primary
|
||||
href="/new/bookmark"
|
||||
>
|
||||
<PlusIcon />
|
||||
{t('newBookmark')}
|
||||
</Link>
|
||||
</p>
|
||||
{selCount ? (
|
||||
<button className="btn btn-error" onClick={removeSelectedBookmarks}>
|
||||
<TrashIcon /> {selCount} {t('bookmarks')}
|
||||
</button>
|
||||
) : null}
|
||||
{types.map((type) =>
|
||||
perType[type].length > 0 ? (
|
||||
<Fragment key={type}>
|
||||
<h2>{t(`${type}Bookmark`)}</h2>
|
||||
<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={bookmarks.length === selCount}
|
||||
/>
|
||||
</th>
|
||||
<th className="text-base-300 text-base">{t('title')}</th>
|
||||
<th className="text-base-300 text-base">{t('location')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{bookmarks
|
||||
.filter((bookmark) => bookmark.type === type)
|
||||
.map((bookmark, i) => (
|
||||
<tr key={i}>
|
||||
<td className="text-base font-medium">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selected[bookmark.id] ? true : false}
|
||||
className="checkbox checkbox-secondary"
|
||||
onClick={() => toggleSelect(bookmark.id)}
|
||||
/>
|
||||
</td>
|
||||
<td className="text-base font-medium">
|
||||
<PageLink href={`/account/bookmarks/${bookmark.id}`} txt={bookmark.title} />
|
||||
</td>
|
||||
<td className="text-base font-medium">
|
||||
<WebLink
|
||||
href={bookmark.url}
|
||||
txt={
|
||||
bookmark.url.length > 30
|
||||
? bookmark.url.slice(0, 30) + '...'
|
||||
: bookmark.url
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
<td className="text-base font-medium"></td>
|
||||
<td className="text-base font-medium hidden md:block"></td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</Fragment>
|
||||
) : null
|
||||
)}
|
||||
<BackToAccountButton />
|
||||
</div>
|
||||
)
|
||||
}
|
98
sites/shared/components/account/compare.mjs
Normal file
98
sites/shared/components/account/compare.mjs
Normal file
|
@ -0,0 +1,98 @@
|
|||
// Dependencies
|
||||
import { useState, useContext } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
// Context
|
||||
import { LoadingStatusContext } from 'shared/context/loading-status-context.mjs'
|
||||
// Hooks
|
||||
import { useAccount } from 'shared/hooks/use-account.mjs'
|
||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
// Components
|
||||
import { Icons, welcomeSteps, BackToAccountButton } from './shared.mjs'
|
||||
import { ContinueButton } from 'shared/components/buttons/continue-button.mjs'
|
||||
import { ListInput } from 'shared/components/inputs.mjs'
|
||||
import { OkIcon, NoIcon } from 'shared/components/icons.mjs'
|
||||
import { DynamicMdx } from 'shared/components/mdx/dynamic.mjs'
|
||||
|
||||
export const ns = ['account', 'status']
|
||||
|
||||
export const CompareSettings = ({ welcome = false }) => {
|
||||
// Hooks
|
||||
const { account, setAccount } = useAccount()
|
||||
const backend = useBackend()
|
||||
const { setLoadingStatus } = useContext(LoadingStatusContext)
|
||||
const { t, i18n } = useTranslation(ns)
|
||||
|
||||
// State
|
||||
const [selection, setSelection] = useState(account?.compare ? 'yes' : 'no')
|
||||
|
||||
// Helper method to update the account
|
||||
const update = async (val) => {
|
||||
if (val !== selection) {
|
||||
setLoadingStatus([true, 'processingUpdate'])
|
||||
const result = await backend.updateAccount({
|
||||
compare: val === 'yes' ? true : false,
|
||||
})
|
||||
if (result.success) {
|
||||
setLoadingStatus([true, 'settingsSaved', true, true])
|
||||
setAccount(result.data.account)
|
||||
setSelection(val)
|
||||
} else setLoadingStatus([true, 'backendError', true, true])
|
||||
}
|
||||
}
|
||||
|
||||
// Link to the next onboarding step
|
||||
const nextHref =
|
||||
welcomeSteps[account?.control].length > 3
|
||||
? '/welcome/' + welcomeSteps[account?.control][4]
|
||||
: '/docs/guide'
|
||||
|
||||
return (
|
||||
<div className="max-w-xl">
|
||||
<ListInput
|
||||
id="account-compare"
|
||||
label={t('compareTitle')}
|
||||
list={['yes', 'no'].map((val) => ({
|
||||
val,
|
||||
label: (
|
||||
<div className="flex flex-row items-center w-full justify-between">
|
||||
<span>{t(val === 'yes' ? 'compareYes' : 'compareNo')}</span>
|
||||
{val === 'yes' ? (
|
||||
<OkIcon className="w-8 h-8 text-success" stroke={4} />
|
||||
) : (
|
||||
<NoIcon className="w-8 h-8 text-error" stroke={3} />
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
desc: t(val === 'yes' ? 'compareYesd' : 'compareNod'),
|
||||
}))}
|
||||
current={selection}
|
||||
update={update}
|
||||
docs={<DynamicMdx language={i18n.language} slug={`docs/site/account/compare`} />}
|
||||
/>
|
||||
{welcome ? (
|
||||
<>
|
||||
<ContinueButton btnProps={{ href: nextHref }} link />
|
||||
{welcomeSteps[account?.control].length > 0 ? (
|
||||
<>
|
||||
<progress
|
||||
className="progress progress-primary w-full mt-12"
|
||||
value={400 / welcomeSteps[account?.control].length}
|
||||
max="100"
|
||||
></progress>
|
||||
<span className="pt-4 text-sm font-bold opacity-50">
|
||||
4 / {welcomeSteps[account?.control].length}
|
||||
</span>
|
||||
<Icons
|
||||
done={welcomeSteps[account?.control].slice(0, 3)}
|
||||
todo={welcomeSteps[account?.control].slice(4)}
|
||||
current="compare"
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<BackToAccountButton />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
116
sites/shared/components/account/consent.mjs
Normal file
116
sites/shared/components/account/consent.mjs
Normal file
|
@ -0,0 +1,116 @@
|
|||
// Dependencies
|
||||
import { useState, useContext } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { nsMerge } from 'shared/utils.mjs'
|
||||
// Context
|
||||
import { LoadingStatusContext } from 'shared/context/loading-status-context.mjs'
|
||||
// Hooks
|
||||
import { useAccount } from 'shared/hooks/use-account.mjs'
|
||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
// Components
|
||||
import Link from 'next/link'
|
||||
import { Popout } from 'shared/components/popout/index.mjs'
|
||||
import { BackToAccountButton } from './shared.mjs'
|
||||
import { SaveSettingsButton } from 'shared/components/buttons/save-settings-button.mjs'
|
||||
import { GdprAccountDetails, ns as gdprNs } from 'shared/components/gdpr/details.mjs'
|
||||
|
||||
export const ns = nsMerge(gdprNs, 'account', 'status')
|
||||
|
||||
const Checkbox = ({ value, setter, label, children = null }) => (
|
||||
<div
|
||||
className={`form-control p-4 hover:cursor-pointer rounded border-l-8 my-2
|
||||
${value ? 'border-success bg-success' : 'border-error bg-error'}
|
||||
bg-opacity-10 shadow`}
|
||||
onClick={() => setter(value ? false : true)}
|
||||
>
|
||||
<div className="form-control flex flex-row items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="checkbox"
|
||||
checked={value ? 'checked' : ''}
|
||||
onChange={() => setter(value ? false : true)}
|
||||
/>
|
||||
<span className="label-text">{label}</span>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
export const ConsentSettings = ({ title = false }) => {
|
||||
// Hooks
|
||||
const { account, setAccount, setToken } = useAccount()
|
||||
const backend = useBackend()
|
||||
const { setLoadingStatus } = useContext(LoadingStatusContext)
|
||||
const { t } = useTranslation(ns)
|
||||
|
||||
// State
|
||||
const [consent1, setConsent1] = useState(account?.consent > 0)
|
||||
const [consent2, setConsent2] = useState(account?.consent > 1)
|
||||
|
||||
// Helper method to update the account
|
||||
const update = async () => {
|
||||
let newConsent = 0
|
||||
if (consent1) newConsent = 1
|
||||
if (consent1 && consent2) newConsent = 2
|
||||
if (newConsent !== account.consent) {
|
||||
setLoadingStatus([true, 'processingUpdate'])
|
||||
const result = await backend.updateAccount({ consent: newConsent })
|
||||
if (result.data?.result === 'success') {
|
||||
setLoadingStatus([true, 'settingsSaved', true, true])
|
||||
setAccount(result.data.account)
|
||||
} else setLoadingStatus([true, 'backendError', true, true])
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to remove the account
|
||||
const removeAccount = async () => {
|
||||
setLoadingStatus([true, 'processingUpdate'])
|
||||
const result = await backend.removeAccount()
|
||||
if (result === true) setLoadingStatus([true, 'settingsSaved', true, true])
|
||||
else setLoadingStatus([true, 'backendError', true, true])
|
||||
setToken(null)
|
||||
setAccount({ username: false })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-xl xl:pl-4">
|
||||
{title ? <h2 className="text-4xl">{t('privacyMatters')}</h2> : null}
|
||||
<p>{t('compliant')}</p>
|
||||
<p>{t('consentWhyAnswer')}</p>
|
||||
<h5 className="mt-8">{t('accountQuestion')}</h5>
|
||||
<GdprAccountDetails />
|
||||
{consent1 ? (
|
||||
<Checkbox value={consent1} setter={setConsent1} label={t('yesIDo')} />
|
||||
) : (
|
||||
<button
|
||||
className="btn btn-primary btn-lg w-full mt-4"
|
||||
onClick={() => setConsent1(!consent1)}
|
||||
>
|
||||
{t('clickHere')}
|
||||
</button>
|
||||
)}
|
||||
{consent1 ? (
|
||||
<Checkbox value={consent2} setter={setConsent2} label={t('openDataQuestion')} />
|
||||
) : null}
|
||||
{consent1 && !consent2 ? <Popout note>{t('openDataInfo')}</Popout> : null}
|
||||
{!consent1 && <Popout warning>{t('noConsentNoAccount')}</Popout>}
|
||||
{consent1 ? (
|
||||
<SaveSettingsButton btnProps={{ onClick: update }} />
|
||||
) : (
|
||||
<SaveSettingsButton
|
||||
label={t('account:removeAccount')}
|
||||
btnProps={{
|
||||
onClick: removeAccount,
|
||||
className: 'btn mt-4 capitalize w-full btn-error',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<BackToAccountButton />
|
||||
<p className="text-center opacity-50 mt-12">
|
||||
<Link href="/docs/various/privacy" className="hover:text-secondary underline">
|
||||
FreeSewing Privacy Notice
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
105
sites/shared/components/account/control.mjs
Normal file
105
sites/shared/components/account/control.mjs
Normal file
|
@ -0,0 +1,105 @@
|
|||
// __SDEFILE__ - This file is a dependency for the stand-alone environment
|
||||
// Dependencies
|
||||
import { useState, useContext } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
// Context
|
||||
import { LoadingStatusContext } from 'shared/context/loading-status-context.mjs'
|
||||
// Hooks
|
||||
import { useAccount } from 'shared/hooks/use-account.mjs'
|
||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
// Components
|
||||
import { BackToAccountButton, Icons, welcomeSteps } from './shared.mjs'
|
||||
import { ContinueButton } from 'shared/components/buttons/continue-button.mjs'
|
||||
import { ListInput } from 'shared/components/inputs.mjs'
|
||||
import { ControlScore } from 'shared/components/control/score.mjs'
|
||||
import { DynamicMdx } from 'shared/components/mdx/dynamic.mjs'
|
||||
|
||||
export const ns = ['account', 'status']
|
||||
|
||||
/** state handlers for any input that changes the control setting */
|
||||
export const useControlState = () => {
|
||||
// Hooks
|
||||
const { account, setAccount, token } = useAccount()
|
||||
const backend = useBackend()
|
||||
const { setLoadingStatus } = useContext(LoadingStatusContext)
|
||||
|
||||
// State
|
||||
const [selection, setSelection] = useState(account.control)
|
||||
|
||||
// Method to update the control setting
|
||||
const update = async (control) => {
|
||||
if (control !== selection) {
|
||||
if (token) {
|
||||
setLoadingStatus([true, 'processingUpdate'])
|
||||
const result = await backend.updateAccount({ control })
|
||||
if (result.success) {
|
||||
setSelection(control)
|
||||
setAccount(result.data.account)
|
||||
setLoadingStatus([true, 'settingsSaved', true, true])
|
||||
} else setLoadingStatus([true, 'backendError', true, true])
|
||||
}
|
||||
//fallback for guest users
|
||||
else {
|
||||
setAccount({ ...account, control })
|
||||
setSelection(control)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { selection, update }
|
||||
}
|
||||
|
||||
export const ControlSettings = ({ welcome = false, noBack = false }) => {
|
||||
const { t, i18n } = useTranslation(ns)
|
||||
|
||||
const { selection, update } = useControlState()
|
||||
|
||||
// Helper to get the link to the next onboarding step
|
||||
const nextHref = welcome
|
||||
? welcomeSteps[selection].length > 1
|
||||
? '/welcome/' + welcomeSteps[selection][1]
|
||||
: '/docs/guide'
|
||||
: false
|
||||
|
||||
return (
|
||||
<div className="max-w-xl">
|
||||
<ListInput
|
||||
id="account-control"
|
||||
label={t('controlTitle')}
|
||||
list={[1, 2, 3, 4, 5].map((val) => ({
|
||||
val,
|
||||
label: (
|
||||
<div className="flex flex-row items-center w-full justify-between">
|
||||
<span>{t(`control${val}.t`)}</span>
|
||||
<ControlScore control={val} />
|
||||
</div>
|
||||
),
|
||||
desc: t(`control${val}.d`),
|
||||
}))}
|
||||
current={selection}
|
||||
update={update}
|
||||
docs={<DynamicMdx language={i18n.language} slug="docs/site/account/control" />}
|
||||
/>
|
||||
{welcome ? (
|
||||
<>
|
||||
<ContinueButton btnProps={{ href: nextHref }} link />
|
||||
{welcomeSteps[selection].length > 1 ? (
|
||||
<>
|
||||
<progress
|
||||
className="progress progress-primary w-full mt-12"
|
||||
value={100 / welcomeSteps[selection].length}
|
||||
max="100"
|
||||
></progress>
|
||||
<span className="pt-4 text-sm font-bold opacity-50">
|
||||
1 / {welcomeSteps[selection].length}
|
||||
</span>
|
||||
<Icons done={[]} todo={welcomeSteps[selection].slice(1)} current="" />
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
) : noBack ? null : (
|
||||
<BackToAccountButton />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
281
sites/shared/components/account/de.yaml
Normal file
281
sites/shared/components/account/de.yaml
Normal file
|
@ -0,0 +1,281 @@
|
|||
account: Account
|
||||
yourAccount: Dein Konto
|
||||
newPattern: Neues Schnittmuster
|
||||
newSet: Einen neuen Satz an Maßen erstellen
|
||||
links: Links
|
||||
info: Info
|
||||
settings: Einstellungen
|
||||
data: Daten
|
||||
sets: Maßnahmesets
|
||||
patterns: Schnittmuster
|
||||
actions: Aktionen
|
||||
created: Erstellt
|
||||
updated: Aktualisiert
|
||||
expires: Läuft ab
|
||||
yourProfile: Dein Profil
|
||||
yourPatterns: Deine Schnittmuster
|
||||
yourSets: Deine Maßeinheiten
|
||||
signOut: Abmelden
|
||||
politeOhCrap: Oh Purzelbäume
|
||||
bio: Über mich
|
||||
email: E-Mail Adresse
|
||||
img: Bild
|
||||
username: Benutzername
|
||||
compare: Metrikenvergleich
|
||||
consent: Zustimmung & Datenschutz
|
||||
control: Benutzererfahrung
|
||||
imperial: Einheiten
|
||||
units: Maßeinheiten
|
||||
apikeys: API-Schlüssel
|
||||
newsletter: Newsletter-Abonnement
|
||||
password: Passwort
|
||||
newPasswordPlaceholder: Gib dein neues Passwort hier ein
|
||||
passwordPlaceholder: Gib dein Passwort hier ein
|
||||
mfa: Zwei-Faktoren-Authentifizierung
|
||||
mfaTipTitle: Bitte erwäge die Aktivierung der Zwei-Faktor-Authentifizierung
|
||||
mfaTipMsg: Wir erzwingen keine Passwortrichtlinien, aber wir empfehlen dir, die Zwei-Faktor-Authentifizierung zu aktivieren, um dein FreeSewing-Konto sicher zu halten.
|
||||
mfaEnabled: Zwei-Faktoren-Authentifizierung ist aktiviert
|
||||
mfaDisabled: Die Zwei-Faktoren-Authentifizierung ist deaktiviert
|
||||
mfaSetup: Zwei-Faktoren-Authentifizierung einrichten
|
||||
mfaAdd: Füge FreeSewing zu deiner Authenticator App hinzu, indem du den QR-Code oben scannst.
|
||||
confirmWithPassword: Bitte gib dein Passwort ein, um diese Aktion zu bestätigen
|
||||
confirmWithMfa: Bitte gib einen Code aus deiner Authenticator App ein, um diese Aktion zu bestätigen
|
||||
enableMfa: Aktiviere die Zwei-Faktor-Authentifizierung
|
||||
disableMfa: Zwei-Faktoren-Authentifizierung deaktivieren
|
||||
language: Sprache
|
||||
developer: Entwickler
|
||||
design: Gestaltung
|
||||
patternMetadata: Pattern metadata
|
||||
clonePattern: Clone pattern
|
||||
updatePattern: Update pattern
|
||||
reload: Account neu laden
|
||||
export: Exportiere deine Daten
|
||||
exportMsg: Click below to export your personal data
|
||||
exportNote: The EU's General Data Protection Regulation (GDPR) ensures your so-called right to data portability — the right to obtain and reuse your personal data for your own purposes, or across different services.
|
||||
exportDownload: "Your data was exported and is available for download at the following location:"
|
||||
review: Überprüfe deine Einwilligungen
|
||||
restrict: Verarbeitung deiner Daten einschränken
|
||||
disable: Deaktiviere dein Konto
|
||||
remove: Entferne deinen Account
|
||||
proceedWithCaution: Bitte mit Vorsicht fortfahren
|
||||
restrictWarning: While no data will be removed, this will disable your account. Furthermore, you can not undo this on your own, but will have to contact support when you want to restore access to your account.
|
||||
noWayBack: There is no way back from this.
|
||||
mdSupport: Hier kannst du Markdown verwenden
|
||||
or: oder
|
||||
continue: Fortsetzen
|
||||
save: Speichern
|
||||
noThanks: Nein danke
|
||||
areYouCertain: Bist du sicher?
|
||||
delete: löschen
|
||||
#reload
|
||||
nailedIt: Geschafft
|
||||
gone: Puff. Verschwunden.
|
||||
reloadMsg1: Die in deinem Browser gespeicherten Daten können manchmal nicht mit den in unserem Backend gespeicherten Daten synchronisiert werden.
|
||||
reloadMsg2: Damit kannst du deine Kontodaten aus dem Backend neu laden. Es hat denselben Effekt wie das Abmelden und erneute Anmelden
|
||||
#bio
|
||||
bioTitle: Erzähl den Leuten ein bisschen was über dich
|
||||
bioPreview: Bio Vorschau
|
||||
bioPlaceholder: Ich mache Kleidung und Schuhe. Ich entwerfe Nähmuster. Ich schreibe Code. Ich betreibe [FreeSewing](http://freesewing.org)
|
||||
#compare
|
||||
compareTitle: Fühlst du dich wohl, wenn Messreihen verglichen werden?
|
||||
compareYes: Ja, falls es mir helfen kann
|
||||
compareYesd: |
|
||||
Gelegentlich zeigen wir, wie deine Messungen im Vergleich zu anderen Messungen abschneiden.
|
||||
So können wir mögliche Probleme in deinen Messungen oder Mustern erkennen.
|
||||
compareNo: Nein, niemals vergleichen
|
||||
compareNod: |
|
||||
Wir werden deine Maßangaben niemals mit anderen Maßangaben vergleichen.
|
||||
Das schränkt unsere Möglichkeiten ein, dich vor potenziellen Problemen in deinen Messsätzen oder Mustern zu warnen.
|
||||
#control
|
||||
showMore: Mehr zeigen
|
||||
control1.t: Halte es so einfach wie möglich
|
||||
control1.d: Blendet alle Funktionen außer den wichtigsten aus.
|
||||
control2.t: Halte es einfach, aber nicht zu einfach
|
||||
control2.d: Blendet die meisten Funktionen aus.
|
||||
control3.t: Balance zwischen Einfachheit und Leistung
|
||||
control3.d: Zeigt die meisten Funktionen an, aber nicht alle.
|
||||
control4.t: Gib mir alle Macht, aber beschütze mich
|
||||
control4.d: Zeigt alle Funktionen, hält Handläufe und Sicherheitschecks ein.
|
||||
control5.t: Geh mir aus dem Weg
|
||||
control5.d: Legt alle Funktionen frei, entfernt alle Handläufe und überprüft die Sicherheit.
|
||||
controlShowMore: Mehr Optionen anzeigen
|
||||
controlTitle: Welches Nutzererlebnis bevorzugst du?
|
||||
#img
|
||||
imgTitle: Wie wäre es mit einem Bild?
|
||||
imgDragAndDropImageHere: Ziehe ein Bild hierher und lege es ab
|
||||
imgPasteUrlHere: Paste an image location (url) here
|
||||
imgSelectImage: Ein Bild auswählen
|
||||
#newsletter
|
||||
newsletterTitle: Möchtest du den FreeSewing-Newsletter abonnieren?
|
||||
newsletterYes: Ja, ich möchte den Newsletter erhalten
|
||||
newsletterYesd: Alle 3 Monate erhältst du von uns eine E-Mail mit ehrlichen, gesunden Inhalten. Kein Tracking, keine Werbung, kein Blödsinn.
|
||||
newsletterNod: Du kannst deine Meinung später immer noch ändern. Aber bis du das tust, werden wir dir keine Newsletter schicken.
|
||||
#imperial
|
||||
metricUnits: Metrische Einheiten (cm)
|
||||
metricUnitsd: Wähle dies, wenn du Zentimeter gegenüber Zoll bevorzugst.
|
||||
imperialUnits: Imperiale Einheiten (inch)
|
||||
imperialUnitsd: Wähle diese Option, wenn du Zoll statt Zentimeter bevorzugst.
|
||||
unitsTitle: Welche Einheiten bevorzugst du?
|
||||
#username
|
||||
usernameTitle: Welchen Benutzernamen hättest du gerne?
|
||||
usernameAvailable: Username is available
|
||||
usernameNotAvailable: Benutzername ist nicht verfügbar
|
||||
#email
|
||||
emailTitle: Wo können wir dich erreichen, falls wir einen guten Grund dafür haben (z.B. wenn du dein Passwort vergessen hast)?
|
||||
oneMoreThing: Und zum Schluss
|
||||
oneMomentPlease: Einen Moment bitte
|
||||
emailChangeConfirmation: Wir haben eine E-Mail an deine neue Adresse geschickt, um diese Änderung zu bestätigen.
|
||||
vagueError: Etwas ist schief gelaufen und wir sind uns nicht sicher, wie wir damit umgehen sollen. Bitte versuche es noch einmal oder wende dich an einen Menschen, der dir hilft.
|
||||
#github
|
||||
githubTitle: Link your GitHub identity
|
||||
githubWhy1: Enter your GitHub username and email here and we will use them when interacting with GitHub on your behalf.
|
||||
githubWhy2: Note that both your GitHub username and email is public info. This merely allows us to make a link between your FreeSewing account and GitHub account.
|
||||
githubWhy3: For example, when you report a problem on this website, we can mention you so you will receive notifications when there is an update. For this, your username is sufficient.
|
||||
githubWhy4: When you submit a showcase post or make changed to our content, we can credit those commits to you if we have both your username and the email address you use on GitHub.
|
||||
tooComplex: If all of this in confusing, you don't have to provide this info. It's an advanced feature.
|
||||
#languge
|
||||
languageTitle: Welche Sprache bevorzugst du?
|
||||
#password
|
||||
passwordTitle: Etwas, das nur du weißt
|
||||
#api key
|
||||
newApikey: Erstelle einen neuen API-Schlüssel
|
||||
keyNewInfo: Erstelle einen neuen API-Schlüssel, um dich automatisch mit dem FreeSewing-Backend zu verbinden.
|
||||
keyName: Name des Schlüssels
|
||||
keyNameDesc: Ein eindeutiger Name für diesen API-Schlüssel. Nur für dich sichtbar.
|
||||
keyExpires: Schlüsselverfall
|
||||
keyExpiresDesc: "Der Schlüssel läuft am ab:"
|
||||
keyLevel: Schlüsselberechtigungsstufe
|
||||
keyLevel0: Nur authentifizieren
|
||||
keyLevel1: Lese den Zugang zu deinen eigenen Mustern und Messsätzen
|
||||
keyLevel2: Lesezugriff auf alle deine Kontodaten
|
||||
keyLevel3: Schreibzugang zu deinen eigenen Mustern und Messsätzen
|
||||
keyLevel4: Schreibzugriff auf alle deine Kontodaten
|
||||
keyLevel5: Lesezugriff auf Muster und Messreihen anderer Nutzer
|
||||
keyLevel6: Schreibzugriff auf Muster und Messreihen anderer Nutzer
|
||||
keyLevel7: Schreibzugriff auf alle Kontodaten von anderen Nutzern
|
||||
keyLevel8: Sich als anderer Benutzer ausgeben, voller Schreibzugriff auf alle Daten
|
||||
cancel: Abbrechen
|
||||
keySecretWarning: Das ist das einzige Mal, dass du das Schlüsselgeheimnis sehen kannst, also achte darauf, es zu kopieren.
|
||||
keyExpired: Dieser API-Schlüssel ist abgelaufen
|
||||
deleteKeyWarning: Das Entfernen eines API-Schlüssels kann nicht rückgängig gemacht werden.
|
||||
keyDocsTitle: Refer to FreeSewing.dev for documentation on using API keys (English only)
|
||||
keyDocsMsg: This is an advanced feature aimed at developers or anyone who wants to interact with our backend directly.
|
||||
apiCalls: API Calls
|
||||
#bookmarks
|
||||
newBookmark: Add a Bookmark
|
||||
bookmark: Bookmark
|
||||
bookmarks: Bookmarks
|
||||
type: Art
|
||||
location: Location
|
||||
title: Titel
|
||||
new: Neu
|
||||
designBookmark: Entwurf
|
||||
patternBookmark: Schnittmuster
|
||||
setBookmark: Maßnahmesets
|
||||
csetBookmark: Curated Measurements Sets
|
||||
docBookmark: Dokumentation
|
||||
customBookmark: Custom Bookmarks
|
||||
yourBookmarks: Your bookmarks
|
||||
bookmarkThisThing: Bookmark this { thing }
|
||||
page: Page
|
||||
#sets
|
||||
set: Measurements Set
|
||||
name: Name
|
||||
setNameDesc: Ein Name zur Identifizierung dieser Messreihe
|
||||
setNewInfo: Erstelle einen neuen Satz von Messungen, für die du dann Muster erstellen kannst.
|
||||
notes: Notizen
|
||||
setNotesDesc: Alle Notizen, die du zu dieser Messreihe aufbewahren möchtest
|
||||
description: Beschreibung
|
||||
deleteSetWarning: Das Entfernen einer Messreihe kann nicht rückgängig gemacht werden.
|
||||
image: Bild
|
||||
measies: Maße
|
||||
setUnitsMsgTitle: Diese Einstellung gilt nur für diesen Messsatz
|
||||
setUnitsMsgDesc: |
|
||||
Dies sind die Einheiten, die wir verwenden, wenn wir die Maße in diesem Set aktualisieren oder anzeigen.
|
||||
Überall sonst auf dieser Website verwenden wir die in deinem Konto eingestellten Einheiten.
|
||||
public: Öffentlich
|
||||
publicSet: Öffentliche Messungen eingestellt
|
||||
privateSet: Private Messungen eingestellt
|
||||
publicSetDesc: Andere dürfen diese Messungen nutzen, um Muster zu erstellen oder zu testen
|
||||
privateSetDesc: Diese Messungen können nicht von anderen Nutzern oder Besuchern verwendet werden
|
||||
permalink: Permalink
|
||||
editThing: '{thing} bearbeiten'
|
||||
saveThing: '{thing} speichern'
|
||||
filterByDesign: Filter by design
|
||||
noFilter: Do not filter
|
||||
filterByDesignDocs: If you have a specific design in mind, you can <b>filter by design</b> to only list those measurements that are required for this design.
|
||||
setLacksMeasiesForDesign: This set lacks measurements required for this pattern
|
||||
setHasMeasiesForDesign: This set has all measurements required for this pattern
|
||||
someSetsLacking: Some of your sets lack the measurments required to generate this pattern
|
||||
theseSetsReady: These sets have all required measurments to generate this pattern
|
||||
chooseSet: Bitte wähle einen Satz von Messungen
|
||||
patternForWhichSet: Für welchen Satz von Messungen sollten wir ein Muster erstellen?
|
||||
bookmarkedSets: Maßnahmesets, die du mit einem Lesezeichen versehen hast
|
||||
curatedSets: FreeSewing's curated measurements sets
|
||||
curatedSetsAbout: Von FreeSewing kuratierte Messreihen, die du nutzen kannst, um unsere Plattform oder deine Designs zu testen.
|
||||
curateCuratedSets: Curate our selection of curated measurements sets
|
||||
useThisSet: Verwende diesen Satz von Messungen
|
||||
ownSets: Your own measurements sets
|
||||
noOwnSets: You do not have any of your own measurements sets (yet)
|
||||
pleaseMtm: Because made-to-measure lies at the heart of what we do, we strongly suggest you take accurate measurements.
|
||||
noOwnSetsMsg: You can store your measurements as a measurements set, after which you can generate as many patterns as you want for them.
|
||||
measurements: Maße
|
||||
chooseASet: Choose a measurements set
|
||||
showImage: Show image
|
||||
suggestForCuration: Suggest for curation
|
||||
suggestCset: Suggest a measurements set for curation
|
||||
csetAllMeasies: To ensure curated measurements sets work for all designs, you need to provide a full set of measurements.
|
||||
csetMissing: Your measurements set is missing the following measurements
|
||||
allMeasiesAvailable: All measurements are available.
|
||||
csetHeightMsg1: To allow organizing and presenting our curated sets in a structured way, we organize them by height.
|
||||
csetImgMsg: Finally, we need a picture. Please refer to the documentation to see what makes a good picture for a curated measurements set.
|
||||
docs: Dokumentation
|
||||
csetNotesMsg: If you would like to add any notes, you can do so here.
|
||||
thankYouVeryMuch: Thank you very much
|
||||
csetSuggestedMsg: Your submission has been registered and will be processed by one of our curators.
|
||||
itIsAvailableAt: It is available at
|
||||
csetNameMsg: Each curated set has a name. You can suggest your own name or a pseudonym.
|
||||
patternNew: Ein neues Muster generieren
|
||||
patternNewInfo: Wähle ein Design aus, füge deine Maße hinzu und wir erstellen ein maßgeschneidertes Nähmuster für dich.
|
||||
designNew: Ein neues Design erstellen
|
||||
designNewInfo: FreeSewing-Designs sind kleine Bündel von JavaScript-Code, die Muster erzeugen. Es ist nicht schwer, eigene Designs zu erstellen, und wir haben eine ausführliche Anleitung für dich, damit du loslegen kannst.
|
||||
pluginNew: Ein neues Plugin erstellen
|
||||
pluginNewInfo: Die Funktionen von FreeSewing können mit Plugins erweitert werden. Es ist ganz einfach, ein Plugin zu erstellen, und wir haben eine Anleitung, die dich von Anfang bis Ende begleitet.
|
||||
showcaseNew: Create a new showcase post
|
||||
showcaseNewInfo: If you would like to share something you (or someone else) made based on our designs, you can start a draft showcase post here.
|
||||
blogNew: Create a new blog post
|
||||
blogNewInfo: If you would like to write on the FreeSewing blog, you can start a draft blog post here.
|
||||
csetNew: Suggest a new curated measurements set
|
||||
csetNewInfo: We curate a collection of vetted measurments sets that we use to test patterns. You can suggest a measurements set here.
|
||||
opackNew: Suggest a new options pack
|
||||
opackNewInfo: We curate a collection of vetted option packs for each of our designs. You can suggest your options here.
|
||||
newPopular: Most popular
|
||||
newShare: Share / Show
|
||||
newDev: Design / Develop
|
||||
generateANewThing: "Erstelle eine neue { thing }"
|
||||
website: Website
|
||||
linkedIdentities: Linked Identities
|
||||
websiteTitle: Do you have a website or other URL you'd like to add?
|
||||
platformTitle: Who are you on { platform }?
|
||||
platformWhy: We do not use this data in any way. This is only here so FreeSewing users can connect the dots across platforms.
|
||||
security: Security
|
||||
revealPassword: Reveal password
|
||||
hidePassword: Hide password
|
||||
#Design view
|
||||
designs: Entwurf
|
||||
code: Code
|
||||
aboutThing: About { thing }
|
||||
requiredMeasurements: Erforderliche Maße
|
||||
optionalMeasurements: Optionale Maße
|
||||
designOptions: Designoptionen
|
||||
parts: Parts
|
||||
plugins: Plugins
|
||||
specifications: Specifications
|
||||
visitShowcase: Visit showcase post
|
||||
examples: Beispiele
|
||||
noExamples: We currently do not have any examples for this design
|
||||
noExamplesMsg: We rely on the FreeSewing community to submit examples in our showcase posts.
|
||||
ownPublicPattern: This is the public view on one of your own patterns. For more options, access the private view.
|
||||
ownPrivatePattern: This is the private view on your pattern. The public view will work for you even when the pattern is private. It will only work for others when the pattern is public.
|
||||
privateView: Private view
|
||||
publicView: Public view
|
76
sites/shared/components/account/email.mjs
Normal file
76
sites/shared/components/account/email.mjs
Normal file
|
@ -0,0 +1,76 @@
|
|||
// Dependencies
|
||||
import { useState, useContext } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
// Context
|
||||
import { LoadingStatusContext } from 'shared/context/loading-status-context.mjs'
|
||||
// Hooks
|
||||
import { useAccount } from 'shared/hooks/use-account.mjs'
|
||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
// Verification methods
|
||||
import { validateEmail, validateTld } from 'shared/utils.mjs'
|
||||
// Components
|
||||
import { BackToAccountButton } from './shared.mjs'
|
||||
import { Popout } from 'shared/components/popout/index.mjs'
|
||||
import { EmailInput } from 'shared/components/inputs.mjs'
|
||||
import { DynamicMdx } from 'shared/components/mdx/dynamic.mjs'
|
||||
|
||||
export const ns = ['account', 'status']
|
||||
|
||||
export const EmailSettings = () => {
|
||||
// Hooks
|
||||
const { account, setAccount } = useAccount()
|
||||
const backend = useBackend()
|
||||
const { t, i18n } = useTranslation(ns)
|
||||
const { setLoadingStatus } = useContext(LoadingStatusContext)
|
||||
|
||||
// State
|
||||
const [email, setEmail] = useState(account.email)
|
||||
const [changed, setChanged] = useState(false)
|
||||
|
||||
// Helper method to update account
|
||||
const save = async () => {
|
||||
setLoadingStatus([true, 'processingUpdate'])
|
||||
const result = await backend.updateAccount({ email })
|
||||
if (result.success) {
|
||||
setAccount(result.data.account)
|
||||
setLoadingStatus([true, 'settingsSaved', true, true])
|
||||
} else setLoadingStatus([true, 'backendError', true, true])
|
||||
setChanged(true)
|
||||
}
|
||||
|
||||
// Is email valid?
|
||||
const valid = (validateEmail(email) && validateTld(email)) || false
|
||||
|
||||
return (
|
||||
<div className="max-w-xl">
|
||||
{changed ? (
|
||||
<Popout note>
|
||||
<h3>{t('oneMoreThing')}</h3>
|
||||
<p>{t('emailChangeConfirmation')}</p>
|
||||
</Popout>
|
||||
) : (
|
||||
<>
|
||||
<EmailInput
|
||||
id="account-email"
|
||||
label={t('account:email')}
|
||||
placeholder={t('account:email')}
|
||||
update={setEmail}
|
||||
labelBL={t('emailTitle')}
|
||||
current={email}
|
||||
original={account.email}
|
||||
valid={() => valid}
|
||||
docs={<DynamicMdx language={i18n.language} slug={`docs/site/account/email`} />}
|
||||
/>
|
||||
<button
|
||||
className="btn mt-4 btn-primary w-full"
|
||||
onClick={save}
|
||||
disabled={!valid || email.toLowerCase() === account.email}
|
||||
>
|
||||
{t('save')}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<BackToAccountButton />
|
||||
</div>
|
||||
)
|
||||
}
|
313
sites/shared/components/account/en.yaml
Normal file
313
sites/shared/components/account/en.yaml
Normal file
|
@ -0,0 +1,313 @@
|
|||
account: Account
|
||||
yourAccount: Your Account
|
||||
newPattern: New Pattern
|
||||
newSet: Create a new measurements set
|
||||
links: Links
|
||||
info: Info
|
||||
settings: Settings
|
||||
data: Data
|
||||
sets: Measurements Sets
|
||||
patterns: Patterns
|
||||
actions: Actions
|
||||
created: Created
|
||||
updated: Updated
|
||||
expires: Expires
|
||||
|
||||
yourProfile: Your Profile
|
||||
yourPatterns: Your Patterns
|
||||
yourSets: Your Measurements Sets
|
||||
signOut: Sign Out
|
||||
politeOhCrap: Oh fiddlesticks
|
||||
bio: Bio
|
||||
email: E-mail Address
|
||||
img: Image
|
||||
username: Username
|
||||
compare: Metricset Comparison
|
||||
consent: Consent & Privacy
|
||||
control: User Experience
|
||||
imperial: Units
|
||||
units: Units
|
||||
apikeys: API Keys
|
||||
newsletter: Newsletter Subscription
|
||||
password: Password
|
||||
newPasswordPlaceholder: Enter your new password here
|
||||
passwordPlaceholder: Enter your password here
|
||||
mfa: Two-Factor Authentication
|
||||
mfaTipTitle: Please consider enabling Two-Factor Authentication
|
||||
mfaTipMsg: We do not enforce a password policy, but we do recommend you enable Two-Factor Authentication to keep your FreeSewing account safe.
|
||||
mfaEnabled: Two-Factor Authentication is enabled
|
||||
mfaDisabled: Two-Factor Authentication is disabled
|
||||
mfaSetup: Set up Two-Factor Authentication
|
||||
mfaAdd: Add FreeSewing to your Authenticator App by scanning the QR code above.
|
||||
confirmWithPassword: Please enter your password to confirm this action
|
||||
confirmWithMfa: Please enter a code from your Authenticator App to confirm this action
|
||||
enableMfa: Enable Two-Factor Authentication
|
||||
disableMfa: Disable Two-Factor Authentication
|
||||
language: Language
|
||||
developer: Developer
|
||||
design: Design
|
||||
patternMetadata: Pattern metadata
|
||||
clonePattern: Clone pattern
|
||||
updatePattern: Update pattern
|
||||
|
||||
reload: Reload account
|
||||
export: Export your data
|
||||
exportMsg: Click below to export your personal data
|
||||
exportNote: The EU's General Data Protection Regulation (GDPR) ensures your so-called right to data portability — the right to obtain and reuse your personal data for your own purposes, or across different services.
|
||||
exportDownload: "Your data was exported and is available for download at the following location:"
|
||||
review: Review your consent
|
||||
restrict: Restrict processing of your data
|
||||
disable: Disable your account
|
||||
remove: Remove your account
|
||||
|
||||
proceedWithCaution: Proceed with caution
|
||||
restrictWarning: While no data will be removed, this will disable your account. Furthermore, you can not undo this on your own, but will have to contact support when you want to restore access to your account.
|
||||
noWayBack: There is no way back from this.
|
||||
|
||||
mdSupport: You can use markdown here
|
||||
or: or
|
||||
continue: Continue
|
||||
save: Save
|
||||
noThanks: No thanks
|
||||
areYouCertain: Are you certain?
|
||||
delete: Delete
|
||||
|
||||
# reload
|
||||
nailedIt: Nailed it
|
||||
gone: Poof. Gone.
|
||||
reloadMsg1: The data stored in your browser can sometimes get out of sync with the data stored in our backend.
|
||||
reloadMsg2: This lets you reload your account data from the backend. It has the same effect as signin out, and then signing in again
|
||||
|
||||
# bio
|
||||
bioTitle: Tell people a little bit about yourself
|
||||
bioPreview: Bio Preview
|
||||
bioPlaceholder: I make clothes and shoes. I design sewing patterns. I write code. I run [FreeSewing](http://freesewing.org)
|
||||
|
||||
# compare
|
||||
compareTitle: Are you comfortable with measurements sets being compared?
|
||||
compareYes: Yes, in case it may help me
|
||||
compareYesd: |
|
||||
We will occasionally show how your set of measurements compares to other measurements sets.
|
||||
This allows us to detect potential problems in your measurements or patterns.
|
||||
compareNo: No, never compare
|
||||
compareNod: |
|
||||
We will never compare your set of measurements to other measurements sets.
|
||||
This will limit our ability to warn you about potential problems in your measurements sets or patterns.
|
||||
|
||||
# control
|
||||
showMore: Show more
|
||||
control1.t: Keep it as simple as possible
|
||||
control1.d: Hides all but the most crucial features.
|
||||
control2.t: Keep it simple, but not too simple
|
||||
control2.d: Hides the majority of features.
|
||||
control3.t: Balance simplicity with power
|
||||
control3.d: Reveals the majority of features, but not all.
|
||||
control4.t: Give me all powers, but keep me safe
|
||||
control4.d: Reveals all features, keeps handrails and safety checks.
|
||||
control5.t: Get out of my way
|
||||
control5.d: Reveals all features, removes all handrails and safety checks.
|
||||
controlShowMore: Show more options
|
||||
controlTitle: Which user experience do you prefer?
|
||||
# img
|
||||
imgTitle: How about a picture?
|
||||
imgDragAndDropImageHere: Drag and drop an image here
|
||||
imgPasteUrlHere: Paste an image location (url) here
|
||||
imgSelectImage: Select an image
|
||||
|
||||
# newsletter
|
||||
newsletterTitle: Would you like to reveice the FreeSewing newsletter?
|
||||
newsletterYes: Yes, I would like to receive the newsletter
|
||||
newsletterYesd: Once every 3 months you'll receive an email from us with honest wholesome content. No tracking, no ads, no nonsense.
|
||||
newsletterNod: You can always change your mind later. But until you do, we will not send you any newsletters.
|
||||
|
||||
# imperial
|
||||
metricUnits: Metric units (cm)
|
||||
metricUnitsd: Pick this if you prefer centimeters over inches.
|
||||
imperialUnits: Imperial units (inch)
|
||||
imperialUnitsd: Pick this if you prefer inches over centimeters.
|
||||
unitsTitle: Which units do you prefer?
|
||||
|
||||
# username
|
||||
usernameTitle: What username would you like?
|
||||
usernameAvailable: Username is available
|
||||
usernameNotAvailable: Username is not available
|
||||
|
||||
# email
|
||||
emailTitle: Where can we reach you in case we have a good reason for it (like when you forgot your password)?
|
||||
oneMoreThing: One more thing
|
||||
oneMomentPlease: One moment please
|
||||
emailChangeConfirmation: We have sent an E-mail to your new address to confirm this change.
|
||||
vagueError: Something went wrong, and we're not certain how to handle it. Please try again, or involve a human being for assistance.
|
||||
|
||||
# github
|
||||
githubTitle: Link your GitHub identity
|
||||
githubWhy1: Enter your GitHub username and email here and we will use them when interacting with GitHub on your behalf.
|
||||
githubWhy2: Note that both your GitHub username and email is public info. This merely allows us to make a link between your FreeSewing account and GitHub account.
|
||||
githubWhy3 : For example, when you report a problem on this website, we can mention you so you will receive notifications when there is an update. For this, your username is sufficient.
|
||||
githubWhy4: When you submit a showcase post or make changed to our content, we can credit those commits to you if we have both your username and the email address you use on GitHub.
|
||||
tooComplex: If all of this in confusing, you don't have to provide this info. It's an advanced feature.
|
||||
|
||||
# languge
|
||||
languageTitle: Which language do you prefer?
|
||||
|
||||
# password
|
||||
passwordTitle: Something only you know
|
||||
|
||||
# api key
|
||||
newApikey: Generate a new API key
|
||||
keyNewInfo: Create a new API key to connect to the FreeSewing backend in an automated way.
|
||||
keyName: Key name
|
||||
keyNameDesc: A unique name for this API key. Only visible to you.
|
||||
keyExpires: Key expiration
|
||||
keyExpiresDesc: "The key will expire on:"
|
||||
keyLevel: Key permission level
|
||||
keyLevel0: Authenticate only
|
||||
keyLevel1: Read access to your own patterns and measurements sets
|
||||
keyLevel2: Read access to all your account data
|
||||
keyLevel3: Write access to your own patterns and measurements sets
|
||||
keyLevel4: Write access to all your account data
|
||||
keyLevel5: Read access to patterns and measurements sets of other users
|
||||
keyLevel6: Write access to patterns and measurements sets of other users
|
||||
keyLevel7: Write access to all account data of other users
|
||||
keyLevel8: Impersonate other users, full write access to all data
|
||||
cancel: Cancel
|
||||
keySecretWarning: This is the only time you can see the key secret, make sure to copy it.
|
||||
keyExpired: This API key has expired
|
||||
deleteKeyWarning: Removing an API key cannot be undone.
|
||||
keyDocsTitle: Refer to FreeSewing.dev for documentation on using API keys (English only)
|
||||
keyDocsMsg: This is an advanced feature aimed at developers or anyone who wants to interact with our backend directly.
|
||||
apiCalls: API Calls
|
||||
|
||||
# bookmarks
|
||||
newBookmark: Add a Bookmark
|
||||
bookmark: Bookmark
|
||||
bookmarks: Bookmarks
|
||||
type: Type
|
||||
location: Location
|
||||
title: Title
|
||||
new: New
|
||||
designBookmark: Designs
|
||||
patternBookmark: Patterns
|
||||
setBookmark: Measurements Sets
|
||||
csetBookmark: Curated Measurements Sets
|
||||
docBookmark: Documentation
|
||||
customBookmark: Custom Bookmarks
|
||||
yourBookmarks: Your bookmarks
|
||||
bookmarkThisThing: Bookmark this { thing }
|
||||
page: Page
|
||||
|
||||
# sets
|
||||
set: Measurements Set
|
||||
name: Name
|
||||
setNameDesc: A name to identify this measurements set
|
||||
setNewInfo: Create a new set of measurements which you can then use to generate patterns for.
|
||||
notes: Notes
|
||||
setNotesDesc: Any notes you'd like to keep regarding this measurements set
|
||||
description: Description
|
||||
deleteSetWarning: Removing a measurements set cannot be undone.
|
||||
image: Image
|
||||
measies: Measurements
|
||||
setUnitsMsgTitle: This settings only applies to this measurement set
|
||||
setUnitsMsgDesc: |
|
||||
These are the units we will use when updating or displaying the measurements in this set.
|
||||
Everywhere else on this website, we will use the units preference set in your account.
|
||||
public: Public
|
||||
publicSet: Public measurements set
|
||||
privateSet: Private measurements set
|
||||
publicSetDesc: Others are allowed to use these measurements to generate or test patterns
|
||||
privateSetDesc: These measurments cannot be used by other users or visitors
|
||||
permalink: Permalink
|
||||
editThing: Edit {thing}
|
||||
saveThing: Save {thing}
|
||||
filterByDesign: Filter by design
|
||||
noFilter: Do not filter
|
||||
filterByDesignDocs: If you have a specific design in mind, you can <b>filter by design</b> to only list those measurements that are required for this design.
|
||||
setLacksMeasiesForDesign: This set lacks measurements required for this pattern
|
||||
setHasMeasiesForDesign: This set has all measurements required for this pattern
|
||||
someSetsLacking: Some of these sets lack the measurments required to generate this pattern
|
||||
theseSetsReady: These sets have all required measurments to generate this pattern
|
||||
chooseSet: Please choose a set of measurements
|
||||
patternForWhichSet: Which set of measurements should we generate a pattern for?
|
||||
bookmarkedSets: Measurements sets you've bookmarked
|
||||
curatedSets: FreeSewing's curated measurements sets
|
||||
curatedSetsAbout: Sets of measurements curated by FreeSewing that you can use to test our platform, or your designs.
|
||||
curateCuratedSets: Curate our selection of curated measurements sets
|
||||
useThisSet: Use this set of measurements
|
||||
ownSets: Your own measurements sets
|
||||
noOwnSets: You do not have any of your own measurements sets (yet)
|
||||
pleaseMtm: Because made-to-measure lies at the heart of what we do, we strongly suggest you take accurate measurements.
|
||||
noOwnSetsMsg: You can store your measurements as a measurements set, after which you can generate as many patterns as you want for them.
|
||||
measurements: Measurements
|
||||
chooseASet: Choose a measurements set
|
||||
showImage: Show image
|
||||
suggestForCuration: Suggest for curation
|
||||
suggestCset: Suggest a measurements set for curation
|
||||
csetAllMeasies: To ensure curated measurements sets work for all designs, you need to provide a full set of measurements.
|
||||
csetMissing: Your measurements set is missing the following measurements
|
||||
allMeasiesAvailable: All measurements are available.
|
||||
csetHeightMsg1: To allow organizing and presenting our curated sets in a structured way, we organize them by height.
|
||||
csetImgMsg: Finally, we need a picture. Please refer to the documentation to see what makes a good picture for a curated measurements set.
|
||||
docs: Documentation
|
||||
csetNotesMsg: If you would like to add any notes, you can do so here.
|
||||
thankYouVeryMuch: Thank you very much
|
||||
csetSuggestedMsg: Your submission has been registered and will be processed by one of our curators.
|
||||
itIsAvailableAt: It is available at
|
||||
csetNameMsg: Each curated set has a name. You can suggest your own name or a pseudonym.
|
||||
|
||||
patternNew: Generate a new pattern
|
||||
patternNewInfo: Pick a design, add your measurements set, and we'll generate a made-to-measure sewing pattern for you.
|
||||
|
||||
designNew: Create a new design
|
||||
designNewInfo: FreeSewing designs are small bundles of JavaScript code that generate patterns. It's not hard to create your own designs, and we have a detailed tutorial to get you started.
|
||||
|
||||
pluginNew: Create a new plugin
|
||||
pluginNewInfo: FreeSewing's functionality can be further extended with plugins. Creating a plugin is easy, and we have a guide to take you from start to finish.
|
||||
|
||||
showcaseNew: Create a new showcase post
|
||||
showcaseNewInfo: If you would like to share something you (or someone else) made based on our designs, you can start a draft showcase post here.
|
||||
|
||||
blogNew: Create a new blog post
|
||||
blogNewInfo: If you would like to write on the FreeSewing blog, you can start a draft blog post here.
|
||||
|
||||
csetNew: Suggest a new curated measurements set
|
||||
csetNewInfo: We curate a collection of vetted measurments sets that we use to test patterns. You can suggest a measurements set here.
|
||||
|
||||
opackNew: Suggest a new options pack
|
||||
opackNewInfo: We curate a collection of vetted option packs for each of our designs. You can suggest your options here.
|
||||
|
||||
newPopular: Most popular
|
||||
newShare: Share / Show
|
||||
newDev: Design / Develop
|
||||
|
||||
generateANewThing: "Generate a new { thing }"
|
||||
|
||||
website: Website
|
||||
linkedIdentities: Linked Identities
|
||||
|
||||
websiteTitle: Do you have a website or other URL you'd like to add?
|
||||
platformTitle: Who are you on { platform }?
|
||||
platformWhy: We do not use this data in any way. This is only here so FreeSewing users can connect the dots across platforms.
|
||||
|
||||
security: Security
|
||||
revealPassword: Reveal password
|
||||
hidePassword: Hide password
|
||||
|
||||
# Design view
|
||||
designs: Designs
|
||||
code: Code
|
||||
aboutThing: About { thing }
|
||||
requiredMeasurements: Required measurements
|
||||
optionalMeasurements: Optional measurements
|
||||
designOptions: Design options
|
||||
parts: Parts
|
||||
plugins: Plugins
|
||||
specifications: Specifications
|
||||
visitShowcase: Visit showcase post
|
||||
examples: Examples
|
||||
noExamples: We currently do not have any examples for this design
|
||||
noExamplesMsg: We rely on the FreeSewing community to submit examples in our showcase posts.
|
||||
|
||||
ownPublicPattern: This is the public view on one of your own patterns. For more options, access the private view.
|
||||
ownPrivatePattern: This is the private view on your pattern. The public view will work for you even when the pattern is private. It will only work for others when the pattern is public.
|
||||
privateView: Private view
|
||||
publicView: Public view
|
281
sites/shared/components/account/es.yaml
Normal file
281
sites/shared/components/account/es.yaml
Normal file
|
@ -0,0 +1,281 @@
|
|||
account: Cuenta
|
||||
yourAccount: Tu cuenta
|
||||
newPattern: Nuevo patrón
|
||||
newSet: Crear un nuevo conjunto de medidas
|
||||
links: Enlaces
|
||||
info: Información
|
||||
settings: Ajustes
|
||||
data: Datos
|
||||
sets: Conjuntos de medidas
|
||||
patterns: Patrones
|
||||
actions: Acciones
|
||||
created: Creado
|
||||
updated: Actualizado
|
||||
expires: Caduca en
|
||||
yourProfile: Tu perfil
|
||||
yourPatterns: Tus patrones
|
||||
yourSets: Tus conjuntos de medidas
|
||||
signOut: Regístrate
|
||||
politeOhCrap: Oh fiddlesticks
|
||||
bio: Bio
|
||||
email: Dirección de correo electrónico
|
||||
img: Imagen
|
||||
username: Nombre de usuario
|
||||
compare: Comparación de conjuntos métricos
|
||||
consent: Consentimiento y privacidad
|
||||
control: Experiencia del usuario
|
||||
imperial: Unidades
|
||||
units: Unidades
|
||||
apikeys: Claves API
|
||||
newsletter: Suscripción al boletín
|
||||
password: Contraseña
|
||||
newPasswordPlaceholder: Introduce aquí tu nueva contraseña
|
||||
passwordPlaceholder: Introduce aquí tu contraseña
|
||||
mfa: Autenticación de dos factores
|
||||
mfaTipTitle: Considera la posibilidad de activar la autenticación de dos factores
|
||||
mfaTipMsg: No aplicamos una política de contraseñas, pero te recomendamos que habilites la autenticación de dos factores para mantener segura tu cuenta de FreeSewing.
|
||||
mfaEnabled: La autenticación de dos factores está activada
|
||||
mfaDisabled: La autenticación de dos factores está desactivada
|
||||
mfaSetup: Configura la autenticación de dos factores
|
||||
mfaAdd: Añade FreeSewing a tu aplicación Authenticator escaneando el código QR de arriba.
|
||||
confirmWithPassword: Introduce tu contraseña para confirmar esta acción
|
||||
confirmWithMfa: Introduce un código de tu aplicación Authenticator para confirmar esta acción
|
||||
enableMfa: Activar la autenticación de dos factores
|
||||
disableMfa: Desactivar la autenticación de dos factores
|
||||
language: Idioma
|
||||
developer: Desarrollador
|
||||
design: Diseño
|
||||
patternMetadata: Pattern metadata
|
||||
clonePattern: Clone pattern
|
||||
updatePattern: Update pattern
|
||||
reload: Recargar cuenta
|
||||
export: Exporta tus datos
|
||||
exportMsg: Click below to export your personal data
|
||||
exportNote: The EU's General Data Protection Regulation (GDPR) ensures your so-called right to data portability — the right to obtain and reuse your personal data for your own purposes, or across different services.
|
||||
exportDownload: "Your data was exported and is available for download at the following location:"
|
||||
review: Revisa tu consentimiento
|
||||
restrict: Restringir el procesamiento de sus datos
|
||||
disable: Desactivar tu cuenta
|
||||
remove: Elimina tu cuenta
|
||||
proceedWithCaution: Proceder con cautela
|
||||
restrictWarning: While no data will be removed, this will disable your account. Furthermore, you can not undo this on your own, but will have to contact support when you want to restore access to your account.
|
||||
noWayBack: There is no way back from this.
|
||||
mdSupport: Puedes utilizar markdown aquí
|
||||
or: o
|
||||
continue: Continuar
|
||||
save: Guardar
|
||||
noThanks: No, gracias
|
||||
areYouCertain: '¿Estás seguro?'
|
||||
delete: Borra
|
||||
#reload
|
||||
nailedIt: Clavado
|
||||
gone: Puf. Se ha ido.
|
||||
reloadMsg1: Los datos almacenados en tu navegador a veces pueden desincronizarse con los datos almacenados en nuestro backend.
|
||||
reloadMsg2: Esto te permite recargar los datos de tu cuenta desde el backend. Tiene el mismo efecto que cerrar sesión y volver a iniciarla.
|
||||
#bio
|
||||
bioTitle: Cuéntale a la gente un poco sobre ti
|
||||
bioPreview: Biografía
|
||||
bioPlaceholder: Hago ropa y zapatos. Diseño patrones de costura. Escribo código. Dirijo [FreeSewing](http://freesewing.org)
|
||||
#compare
|
||||
compareTitle: '¿Te sientes cómodo comparando conjuntos de medidas?'
|
||||
compareYes: Sí, por si puede ayudarme
|
||||
compareYesd: |
|
||||
De vez en cuando mostraremos cómo se compara tu conjunto de medidas con otros conjuntos de medidas.
|
||||
Esto nos permite detectar posibles problemas en tus medidas o patrones.
|
||||
compareNo: No, nunca compares
|
||||
compareNod: |
|
||||
Nunca compararemos tu conjunto de medidas con otros conjuntos de medidas.
|
||||
Esto limitará nuestra capacidad de advertirte sobre posibles problemas en tus conjuntos de medidas o patrones.
|
||||
#control
|
||||
showMore: Mostrar más
|
||||
control1.t: Hazlo lo más sencillo posible
|
||||
control1.d: Oculta todas las funciones excepto las más importantes.
|
||||
control2.t: Hazlo sencillo, pero no demasiado
|
||||
control2.d: Oculta la mayoría de las funciones.
|
||||
control3.t: Equilibra la sencillez con la potencia
|
||||
control3.d: Revela la mayoría de las funciones, pero no todas.
|
||||
control4.t: Dame todos los poderes, pero mantenme a salvo
|
||||
control4.d: Revela todas las características, conserva los pasamanos y las comprobaciones de seguridad.
|
||||
control5.t: Apártate de mi camino
|
||||
control5.d: Revela todas las características, quita todas las barandillas y comprueba la seguridad.
|
||||
controlShowMore: Mostrar más opciones
|
||||
controlTitle: '¿Qué experiencia de usuario prefieres?'
|
||||
#img
|
||||
imgTitle: '¿Qué tal una foto?'
|
||||
imgDragAndDropImageHere: Arrastra y suelta una imagen aquí
|
||||
imgPasteUrlHere: Paste an image location (url) here
|
||||
imgSelectImage: Selecciona una imagen
|
||||
#newsletter
|
||||
newsletterTitle: '¿Quieres recibir el boletín de FreeSewing?'
|
||||
newsletterYes: Sí, deseo recibir el boletín
|
||||
newsletterYesd: Una vez cada 3 meses recibirás un correo electrónico nuestro con contenido sano y honesto. Sin seguimiento, sin anuncios, sin tonterías.
|
||||
newsletterNod: Siempre puedes cambiar de opinión más adelante. Pero hasta que no lo hagas, no te enviaremos ningún boletín.
|
||||
#imperial
|
||||
metricUnits: Unidades métricas (cm)
|
||||
metricUnitsd: Elige esta opción si prefieres los centímetros a las pulgadas.
|
||||
imperialUnits: Unidades imperiales (pulgadas)
|
||||
imperialUnitsd: Elige esta opción si prefieres las pulgadas a los centímetros.
|
||||
unitsTitle: '¿Qué unidades prefieres?'
|
||||
#username
|
||||
usernameTitle: '¿Qué nombre de usuario te gustaría?'
|
||||
usernameAvailable: Username is available
|
||||
usernameNotAvailable: El nombre de usuario no está disponible
|
||||
#email
|
||||
emailTitle: '¿Dónde podemos localizarte en caso de que tengamos una buena razón para ello (como cuando olvidaste tu contraseña)?'
|
||||
oneMoreThing: Una cosa más
|
||||
oneMomentPlease: Un momento, por favor
|
||||
emailChangeConfirmation: Hemos enviado un correo electrónico a tu nueva dirección para confirmar este cambio.
|
||||
vagueError: Algo ha ido mal y no estamos seguros de cómo solucionarlo. Por favor, inténtalo de nuevo o pide ayuda a un ser humano.
|
||||
#github
|
||||
githubTitle: Link your GitHub identity
|
||||
githubWhy1: Enter your GitHub username and email here and we will use them when interacting with GitHub on your behalf.
|
||||
githubWhy2: Note that both your GitHub username and email is public info. This merely allows us to make a link between your FreeSewing account and GitHub account.
|
||||
githubWhy3: For example, when you report a problem on this website, we can mention you so you will receive notifications when there is an update. For this, your username is sufficient.
|
||||
githubWhy4: When you submit a showcase post or make changed to our content, we can credit those commits to you if we have both your username and the email address you use on GitHub.
|
||||
tooComplex: If all of this in confusing, you don't have to provide this info. It's an advanced feature.
|
||||
#languge
|
||||
languageTitle: '¿Qué lengua prefieres?'
|
||||
#password
|
||||
passwordTitle: Algo que sólo tú sabes
|
||||
#api key
|
||||
newApikey: Generar una nueva clave API
|
||||
keyNewInfo: Crea una nueva clave API para conectarte al backend de FreeSewing de forma automatizada.
|
||||
keyName: Nombre clave
|
||||
keyNameDesc: Un nombre único para esta clave API. Sólo visible para ti.
|
||||
keyExpires: Clave de caducidad
|
||||
keyExpiresDesc: "La clave caducará el:"
|
||||
keyLevel: Nivel de permiso clave
|
||||
keyLevel0: Autenticar sólo
|
||||
keyLevel1: Lee el acceso a tus propios patrones y conjuntos de medidas
|
||||
keyLevel2: Acceso de lectura a todos los datos de tu cuenta
|
||||
keyLevel3: Escribe el acceso a tus propios patrones y conjuntos de medidas
|
||||
keyLevel4: Acceso de escritura a todos los datos de tu cuenta
|
||||
keyLevel5: Acceso de lectura a patrones y conjuntos de medidas de otros usuarios
|
||||
keyLevel6: Acceso de escritura a patrones y conjuntos de medidas de otros usuarios
|
||||
keyLevel7: Acceso de escritura a todos los datos de la cuenta de otros usuarios
|
||||
keyLevel8: Hacerse pasar por otros usuarios, acceso total de escritura a todos los datos
|
||||
cancel: Cancelar
|
||||
keySecretWarning: Esta es la única vez que puedes ver la clave secreta, asegúrate de copiarla.
|
||||
keyExpired: Esta clave API ha caducado
|
||||
deleteKeyWarning: Eliminar una clave API no se puede deshacer.
|
||||
keyDocsTitle: Refer to FreeSewing.dev for documentation on using API keys (English only)
|
||||
keyDocsMsg: This is an advanced feature aimed at developers or anyone who wants to interact with our backend directly.
|
||||
apiCalls: API Calls
|
||||
#bookmarks
|
||||
newBookmark: Add a Bookmark
|
||||
bookmark: Bookmark
|
||||
bookmarks: Bookmarks
|
||||
type: Tipo
|
||||
location: Location
|
||||
title: Título
|
||||
new: Nuevo
|
||||
designBookmark: Diseños
|
||||
patternBookmark: Patrones
|
||||
setBookmark: Conjuntos de medidas
|
||||
csetBookmark: Curated Measurements Sets
|
||||
docBookmark: Documentación
|
||||
customBookmark: Custom Bookmarks
|
||||
yourBookmarks: Your bookmarks
|
||||
bookmarkThisThing: Bookmark this { thing }
|
||||
page: Page
|
||||
#sets
|
||||
set: Measurements Set
|
||||
name: Nombre
|
||||
setNameDesc: Un nombre para identificar este conjunto de medidas
|
||||
setNewInfo: Crea un nuevo conjunto de medidas que luego podrás utilizar para generar patrones.
|
||||
notes: Notas
|
||||
setNotesDesc: Alguna nota que quieras guardar sobre este conjunto de medidas
|
||||
description: Descripción
|
||||
deleteSetWarning: Eliminar un conjunto de medidas no se puede deshacer.
|
||||
image: Imagen
|
||||
measies: Medidas
|
||||
setUnitsMsgTitle: Esta configuración sólo se aplica a este conjunto de medidas
|
||||
setUnitsMsgDesc: |
|
||||
Estas son las unidades que utilizaremos cuando actualicemos o mostremos las medidas de este conjunto.
|
||||
En todas las demás partes de este sitio web, utilizaremos las unidades de preferencia establecidas en tu cuenta.
|
||||
public: Público
|
||||
publicSet: Conjunto de medidas públicas
|
||||
privateSet: Conjunto de medidas privadas
|
||||
publicSetDesc: A otros se les permite utilizar estas mediciones para generar o probar patrones
|
||||
privateSetDesc: Estas medidas no pueden ser utilizadas por otros usuarios o visitantes
|
||||
permalink: Permalink
|
||||
editThing: Editar {thing}
|
||||
saveThing: Guardar {thing}
|
||||
filterByDesign: Filter by design
|
||||
noFilter: Do not filter
|
||||
filterByDesignDocs: If you have a specific design in mind, you can <b>filter by design</b> to only list those measurements that are required for this design.
|
||||
setLacksMeasiesForDesign: This set lacks measurements required for this pattern
|
||||
setHasMeasiesForDesign: This set has all measurements required for this pattern
|
||||
someSetsLacking: Some of your sets lack the measurments required to generate this pattern
|
||||
theseSetsReady: These sets have all required measurments to generate this pattern
|
||||
chooseSet: Elige una serie de medidas
|
||||
patternForWhichSet: '¿Para qué conjunto de medidas debemos generar un patrón?'
|
||||
bookmarkedSets: Conjuntos de medidas que has marcado como favoritos
|
||||
curatedSets: FreeSewing's curated measurements sets
|
||||
curatedSetsAbout: Conjuntos de medidas curadas por FreeSewing que puedes utilizar para probar nuestra plataforma, o tus diseños.
|
||||
curateCuratedSets: Curate our selection of curated measurements sets
|
||||
useThisSet: Utiliza este conjunto de medidas
|
||||
ownSets: Your own measurements sets
|
||||
noOwnSets: You do not have any of your own measurements sets (yet)
|
||||
pleaseMtm: Because made-to-measure lies at the heart of what we do, we strongly suggest you take accurate measurements.
|
||||
noOwnSetsMsg: You can store your measurements as a measurements set, after which you can generate as many patterns as you want for them.
|
||||
measurements: Medidas
|
||||
chooseASet: Choose a measurements set
|
||||
showImage: Show image
|
||||
suggestForCuration: Suggest for curation
|
||||
suggestCset: Suggest a measurements set for curation
|
||||
csetAllMeasies: To ensure curated measurements sets work for all designs, you need to provide a full set of measurements.
|
||||
csetMissing: Your measurements set is missing the following measurements
|
||||
allMeasiesAvailable: All measurements are available.
|
||||
csetHeightMsg1: To allow organizing and presenting our curated sets in a structured way, we organize them by height.
|
||||
csetImgMsg: Finally, we need a picture. Please refer to the documentation to see what makes a good picture for a curated measurements set.
|
||||
docs: Documentación
|
||||
csetNotesMsg: If you would like to add any notes, you can do so here.
|
||||
thankYouVeryMuch: Thank you very much
|
||||
csetSuggestedMsg: Your submission has been registered and will be processed by one of our curators.
|
||||
itIsAvailableAt: It is available at
|
||||
csetNameMsg: Each curated set has a name. You can suggest your own name or a pseudonym.
|
||||
patternNew: Generar un nuevo patrón
|
||||
patternNewInfo: Elige un diseño, añade tu juego de medidas y generaremos un patrón de costura a medida para ti.
|
||||
designNew: Crear un nuevo diseño
|
||||
designNewInfo: Los diseños de FreeSewing son pequeños paquetes de código JavaScript que generan patrones. No es difícil crear tus propios diseños, y tenemos un tutorial detallado para que empieces.
|
||||
pluginNew: Crear un nuevo plugin
|
||||
pluginNewInfo: La funcionalidad de FreeSewing puede ampliarse aún más con plugins. Crear un plugin es fácil, y tenemos una guía que te llevará de principio a fin.
|
||||
showcaseNew: Create a new showcase post
|
||||
showcaseNewInfo: If you would like to share something you (or someone else) made based on our designs, you can start a draft showcase post here.
|
||||
blogNew: Create a new blog post
|
||||
blogNewInfo: If you would like to write on the FreeSewing blog, you can start a draft blog post here.
|
||||
csetNew: Suggest a new curated measurements set
|
||||
csetNewInfo: We curate a collection of vetted measurments sets that we use to test patterns. You can suggest a measurements set here.
|
||||
opackNew: Suggest a new options pack
|
||||
opackNewInfo: We curate a collection of vetted option packs for each of our designs. You can suggest your options here.
|
||||
newPopular: Most popular
|
||||
newShare: Share / Show
|
||||
newDev: Design / Develop
|
||||
generateANewThing: "Genera un nuevo { thing }"
|
||||
website: Website
|
||||
linkedIdentities: Linked Identities
|
||||
websiteTitle: Do you have a website or other URL you'd like to add?
|
||||
platformTitle: Who are you on { platform }?
|
||||
platformWhy: We do not use this data in any way. This is only here so FreeSewing users can connect the dots across platforms.
|
||||
security: Security
|
||||
revealPassword: Reveal password
|
||||
hidePassword: Hide password
|
||||
#Design view
|
||||
designs: Diseños
|
||||
code: Código
|
||||
aboutThing: About { thing }
|
||||
requiredMeasurements: Medidas requeridas
|
||||
optionalMeasurements: Medidas opcionales
|
||||
designOptions: Opciones de diseño
|
||||
parts: Parts
|
||||
plugins: Plugins
|
||||
specifications: Specifications
|
||||
visitShowcase: Visit showcase post
|
||||
examples: Ejemplos
|
||||
noExamples: We currently do not have any examples for this design
|
||||
noExamplesMsg: We rely on the FreeSewing community to submit examples in our showcase posts.
|
||||
ownPublicPattern: This is the public view on one of your own patterns. For more options, access the private view.
|
||||
ownPrivatePattern: This is the private view on your pattern. The public view will work for you even when the pattern is private. It will only work for others when the pattern is public.
|
||||
privateView: Private view
|
||||
publicView: Public view
|
49
sites/shared/components/account/export.mjs
Normal file
49
sites/shared/components/account/export.mjs
Normal file
|
@ -0,0 +1,49 @@
|
|||
// Context
|
||||
import { LoadingStatusContext } from 'shared/context/loading-status-context.mjs'
|
||||
// Hooks
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { useState, useContext } from 'react'
|
||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
// Components
|
||||
import { BackToAccountButton } from './shared.mjs'
|
||||
import { Popout } from 'shared/components/popout/index.mjs'
|
||||
import { WebLink } from 'shared/components/link.mjs'
|
||||
|
||||
export const ns = ['account', 'status']
|
||||
|
||||
export const ExportAccount = () => {
|
||||
// Hooks
|
||||
const backend = useBackend()
|
||||
const { t } = useTranslation(ns)
|
||||
const { setLoadingStatus } = useContext(LoadingStatusContext)
|
||||
|
||||
const [link, setLink] = useState()
|
||||
|
||||
// Helper method to export account
|
||||
const exportData = async () => {
|
||||
setLoadingStatus([true, 'processingUpdate'])
|
||||
const result = await backend.exportAccount()
|
||||
if (result.success) {
|
||||
setLink(result.data.data)
|
||||
setLoadingStatus([true, 'nailedIt', true, true])
|
||||
} else setLoadingStatus([true, 'backendError', true, false])
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-xl">
|
||||
{link ? (
|
||||
<Popout link>
|
||||
<h5>{t('exportDownload')}</h5>
|
||||
<p className="text-lg">
|
||||
<WebLink href={link} txt={link} />
|
||||
</p>
|
||||
</Popout>
|
||||
) : null}
|
||||
<p>{t('exportMsg')}</p>
|
||||
<button className="btn btn-primary capitalize w-full my-2" onClick={exportData}>
|
||||
{t('export')}
|
||||
</button>
|
||||
<BackToAccountButton />
|
||||
</div>
|
||||
)
|
||||
}
|
35
sites/shared/components/account/force-account-check.mjs
Normal file
35
sites/shared/components/account/force-account-check.mjs
Normal file
|
@ -0,0 +1,35 @@
|
|||
// Dependencies
|
||||
import { useState, useEffect } from 'react'
|
||||
// Hooks
|
||||
import { useAccount } from 'shared/hooks/use-account.mjs'
|
||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
|
||||
export const ForceAccountCheck = ({ trigger = null }) => {
|
||||
// Hooks
|
||||
const { account, setAccount, signOut } = useAccount()
|
||||
const backend = useBackend()
|
||||
|
||||
// State
|
||||
const [lastCheck, setLastCheck] = useState(Date.now())
|
||||
|
||||
// The actual check
|
||||
useEffect(() => {
|
||||
const age = Date.now() - lastCheck
|
||||
if (account.status && age < 500) {
|
||||
const checkAccount = async () => {
|
||||
const result = await backend.reloadAccount()
|
||||
if (result.success) {
|
||||
setAccount(result.data.account)
|
||||
} else {
|
||||
// Login expired. Logout user.
|
||||
signOut()
|
||||
}
|
||||
setLastCheck(Date.now())
|
||||
}
|
||||
checkAccount()
|
||||
}
|
||||
}, [trigger])
|
||||
|
||||
// Don't return anything. This is all about the useEffect hook.
|
||||
return null
|
||||
}
|
281
sites/shared/components/account/fr.yaml
Normal file
281
sites/shared/components/account/fr.yaml
Normal file
|
@ -0,0 +1,281 @@
|
|||
account: Compte
|
||||
yourAccount: Ton compte
|
||||
newPattern: Nouveau modèle
|
||||
newSet: Créer un nouveau jeu de mesures
|
||||
links: Liens
|
||||
info: Info
|
||||
settings: Paramètres
|
||||
data: Données
|
||||
sets: Jeux de mesures
|
||||
patterns: Patrons
|
||||
actions: Actions
|
||||
created: Créé
|
||||
updated: Mis à jour
|
||||
expires: Expire
|
||||
yourProfile: Ton profil
|
||||
yourPatterns: Tes modèles
|
||||
yourSets: Tes ensembles de mesures
|
||||
signOut: S'inscrire
|
||||
politeOhCrap: Oh, les baguettes
|
||||
bio: Bio
|
||||
email: Adresse électronique
|
||||
img: Image
|
||||
username: Nom d'utilisateur
|
||||
compare: Comparaison des ensembles de mesures
|
||||
consent: Consentement et protection de la vie privée
|
||||
control: Expérience de l'utilisateur
|
||||
imperial: Unité
|
||||
units: Unités
|
||||
apikeys: Clés API
|
||||
newsletter: Abonnement au bulletin d'information
|
||||
password: Mot de passe
|
||||
newPasswordPlaceholder: Entre ton nouveau mot de passe ici
|
||||
passwordPlaceholder: Saisis ton mot de passe ici
|
||||
mfa: Authentification à deux facteurs
|
||||
mfaTipTitle: Pense à activer l'authentification à deux facteurs.
|
||||
mfaTipMsg: Nous n'appliquons pas de politique en matière de mot de passe, mais nous te recommandons d'activer l'authentification à deux facteurs pour assurer la sécurité de ton compte FreeSewing.
|
||||
mfaEnabled: L'authentification à deux facteurs est activée
|
||||
mfaDisabled: L'authentification à deux facteurs est désactivée
|
||||
mfaSetup: Configurer l'authentification à deux facteurs
|
||||
mfaAdd: Ajoute FreeSewing à ton application Authenticator en scannant le code QR ci-dessus.
|
||||
confirmWithPassword: Saisis ton mot de passe pour confirmer cette action
|
||||
confirmWithMfa: Saisis un code de ton App Authenticator pour confirmer cette action.
|
||||
enableMfa: Activer l'authentification à deux facteurs
|
||||
disableMfa: Désactiver l'authentification à deux facteurs
|
||||
language: Langue
|
||||
developer: Développeur
|
||||
design: Design (conception)
|
||||
patternMetadata: Pattern metadata
|
||||
clonePattern: Clone pattern
|
||||
updatePattern: Update pattern
|
||||
reload: Recharger le compte
|
||||
export: Exportez vos données
|
||||
exportMsg: Click below to export your personal data
|
||||
exportNote: The EU's General Data Protection Regulation (GDPR) ensures your so-called right to data portability — the right to obtain and reuse your personal data for your own purposes, or across different services.
|
||||
exportDownload: "Your data was exported and is available for download at the following location:"
|
||||
review: Révisez votre consentement
|
||||
restrict: Restreindre le traitement de vos données
|
||||
disable: Désactive ton compte
|
||||
remove: Supprimer votre compte
|
||||
proceedWithCaution: Procédez avec précaution
|
||||
restrictWarning: While no data will be removed, this will disable your account. Furthermore, you can not undo this on your own, but will have to contact support when you want to restore access to your account.
|
||||
noWayBack: There is no way back from this.
|
||||
mdSupport: Tu peux utiliser markdown ici
|
||||
or: ou
|
||||
continue: Continuer
|
||||
save: Sauvegarder
|
||||
noThanks: Non merci
|
||||
areYouCertain: En es-tu certain ?
|
||||
delete: Effacer
|
||||
#reload
|
||||
nailedIt: C'est parfait
|
||||
gone: Pouf. Disparu.
|
||||
reloadMsg1: Les données stockées dans ton navigateur peuvent parfois se désynchroniser avec les données stockées dans notre backend.
|
||||
reloadMsg2: Cela te permet de recharger les données de ton compte à partir du backend. Cela a le même effet que de se déconnecter, puis de se reconnecter.
|
||||
#bio
|
||||
bioTitle: Parle aux gens un peu de toi
|
||||
bioPreview: Aperçu biologique
|
||||
bioPlaceholder: Je fabrique des vêtements et des chaussures. Je conçois des patrons de couture. J'écris du code. Je dirige [FreeSewing](http://freesewing.org)
|
||||
#compare
|
||||
compareTitle: Es-tu à l'aise avec la comparaison des ensembles de mesures ?
|
||||
compareYes: Oui, au cas où cela pourrait m'aider
|
||||
compareYesd: |
|
||||
Nous montrerons de temps en temps comment ton ensemble de mesures se compare à d'autres ensembles de mesures.
|
||||
Cela nous permet de détecter des problèmes potentiels dans tes mesures ou tes modèles.
|
||||
compareNo: Non, ne compare jamais
|
||||
compareNod: |
|
||||
Nous ne comparerons jamais ton jeu de mesures à d'autres jeux de mesures.
|
||||
Cela limitera notre capacité à t'avertir de problèmes potentiels dans tes ensembles de mesures ou tes patrons.
|
||||
#control
|
||||
showMore: Afficher plus
|
||||
control1.t: Fais en sorte que les choses soient aussi simples que possible
|
||||
control1.d: Cache toutes les caractéristiques sauf les plus cruciales.
|
||||
control2.t: Reste simple, mais pas trop
|
||||
control2.d: Cache la majorité des caractéristiques.
|
||||
control3.t: Équilibrer la simplicité et la puissance
|
||||
control3.d: Révèle la majorité des caractéristiques, mais pas toutes.
|
||||
control4.t: Donne-moi tous les pouvoirs, mais garde-moi en sécurité
|
||||
control4.d: Révèle toutes les caractéristiques, conserve les mains courantes et les contrôles de sécurité.
|
||||
control5.t: Pousse-toi de mon chemin
|
||||
control5.d: Révèle toutes les caractéristiques, enlève toutes les mains courantes et vérifie la sécurité.
|
||||
controlShowMore: Afficher plus d'options
|
||||
controlTitle: Quelle expérience utilisateur préfères-tu ?
|
||||
#img
|
||||
imgTitle: Que dirais-tu d'une photo ?
|
||||
imgDragAndDropImageHere: Glisse et dépose une image ici
|
||||
imgPasteUrlHere: Paste an image location (url) here
|
||||
imgSelectImage: Sélectionne une image
|
||||
#newsletter
|
||||
newsletterTitle: Veux-tu recevoir la lettre d'information de FreeSewing ?
|
||||
newsletterYes: Oui, je souhaite recevoir la lettre d'information
|
||||
newsletterYesd: Une fois tous les trois mois, tu recevras un courriel de notre part avec un contenu sain et honnête. Pas de suivi, pas de publicité, pas de bêtises.
|
||||
newsletterNod: Tu peux toujours changer d'avis plus tard. Mais tant que tu ne l'auras pas fait, nous ne t'enverrons pas de bulletin d'information.
|
||||
#imperial
|
||||
metricUnits: Unités métriques (cm)
|
||||
metricUnitsd: Choisis cette option si tu préfères les centimètres aux pouces.
|
||||
imperialUnits: Unités impériales (pouces)
|
||||
imperialUnitsd: Choisis cette option si tu préfères les pouces aux centimètres.
|
||||
unitsTitle: Quelles sont les unités que tu préfères ?
|
||||
#username
|
||||
usernameTitle: Quel nom d'utilisateur aimerais-tu ?
|
||||
usernameAvailable: Username is available
|
||||
usernameNotAvailable: Le nom d'utilisateur n'est pas disponible
|
||||
#email
|
||||
emailTitle: Où pouvons-nous te joindre au cas où nous aurions une bonne raison de le faire (comme lorsque tu as oublié ton mot de passe) ?
|
||||
oneMoreThing: Encore une chose
|
||||
oneMomentPlease: Veuillez patienter
|
||||
emailChangeConfirmation: Nous avons envoyé un e-mail à ta nouvelle adresse pour confirmer ce changement.
|
||||
vagueError: Quelque chose s'est mal passé, et nous ne sommes pas certains de la façon de le gérer. Essaie à nouveau, ou fais appel à un être humain pour obtenir de l'aide.
|
||||
#github
|
||||
githubTitle: Link your GitHub identity
|
||||
githubWhy1: Enter your GitHub username and email here and we will use them when interacting with GitHub on your behalf.
|
||||
githubWhy2: Note that both your GitHub username and email is public info. This merely allows us to make a link between your FreeSewing account and GitHub account.
|
||||
githubWhy3: For example, when you report a problem on this website, we can mention you so you will receive notifications when there is an update. For this, your username is sufficient.
|
||||
githubWhy4: When you submit a showcase post or make changed to our content, we can credit those commits to you if we have both your username and the email address you use on GitHub.
|
||||
tooComplex: If all of this in confusing, you don't have to provide this info. It's an advanced feature.
|
||||
#languge
|
||||
languageTitle: Quelle langue préfères-tu ?
|
||||
#password
|
||||
passwordTitle: Quelque chose que tu es le seul à savoir
|
||||
#api key
|
||||
newApikey: Générer une nouvelle clé API
|
||||
keyNewInfo: Crée une nouvelle clé API pour te connecter au backend de FreeSewing de manière automatisée.
|
||||
keyName: Nom de la clé
|
||||
keyNameDesc: Un nom unique pour cette clé API. Il n'est visible que par toi.
|
||||
keyExpires: Expiration des clés
|
||||
keyExpiresDesc: "La clé expirera le :"
|
||||
keyLevel: Niveau de permission de la clé
|
||||
keyLevel0: Authentifier seulement
|
||||
keyLevel1: Accède en lecture à tes propres patrons et ensembles de mesures
|
||||
keyLevel2: Accès en lecture à toutes les données de ton compte
|
||||
keyLevel3: Accède par écrit à tes propres patrons et ensembles de mesures
|
||||
keyLevel4: Accès en écriture à toutes les données de ton compte
|
||||
keyLevel5: Accès en lecture aux modèles et aux ensembles de mesures des autres utilisateurs
|
||||
keyLevel6: Accès par écrit aux modèles et aux ensembles de mesures d'autres utilisateurs
|
||||
keyLevel7: Accès en écriture à toutes les données de compte des autres utilisateurs
|
||||
keyLevel8: Se faire passer pour un autre utilisateur, accès en écriture à toutes les données.
|
||||
cancel: Annuler
|
||||
keySecretWarning: C'est le seul moment où tu peux voir le secret de la clé, assure-toi de le copier.
|
||||
keyExpired: Cette clé API a expiré
|
||||
deleteKeyWarning: La suppression d'une clé API ne peut pas être annulée.
|
||||
keyDocsTitle: Refer to FreeSewing.dev for documentation on using API keys (English only)
|
||||
keyDocsMsg: This is an advanced feature aimed at developers or anyone who wants to interact with our backend directly.
|
||||
apiCalls: API Calls
|
||||
#bookmarks
|
||||
newBookmark: Add a Bookmark
|
||||
bookmark: Bookmark
|
||||
bookmarks: Bookmarks
|
||||
type: Type
|
||||
location: Location
|
||||
title: Titre
|
||||
new: Nouveau
|
||||
designBookmark: Designs
|
||||
patternBookmark: Patrons
|
||||
setBookmark: Jeux de mesures
|
||||
csetBookmark: Curated Measurements Sets
|
||||
docBookmark: Documentation
|
||||
customBookmark: Custom Bookmarks
|
||||
yourBookmarks: Your bookmarks
|
||||
bookmarkThisThing: Bookmark this { thing }
|
||||
page: Page
|
||||
#sets
|
||||
set: Measurements Set
|
||||
name: Nom
|
||||
setNameDesc: Un nom pour identifier ce jeu de mesures
|
||||
setNewInfo: Crée un nouvel ensemble de mesures que tu pourras ensuite utiliser pour générer des motifs.
|
||||
notes: Remarques
|
||||
setNotesDesc: Toute note que tu souhaites conserver concernant cet ensemble de mesures.
|
||||
description: Description
|
||||
deleteSetWarning: La suppression d'un jeu de mesures ne peut pas être annulée.
|
||||
image: Image
|
||||
measies: Mensurations
|
||||
setUnitsMsgTitle: Ce réglage ne s'applique qu'à cet ensemble de mesures
|
||||
setUnitsMsgDesc: |
|
||||
Ce sont les unités que nous utiliserons lorsque nous mettrons à jour ou afficherons les mesures dans cet ensemble.
|
||||
Partout ailleurs sur ce site, nous utiliserons les préférences d'unités définies dans ton compte.
|
||||
public: Public
|
||||
publicSet: Les mesures publiques sont fixées
|
||||
privateSet: Ensemble de mesures privées
|
||||
publicSetDesc: D'autres personnes sont autorisées à utiliser ces mesures pour générer ou tester des modèles.
|
||||
privateSetDesc: Ces mesures ne peuvent pas être utilisées par d'autres utilisateurs ou visiteurs
|
||||
permalink: Lien permanent
|
||||
editThing: Modifier {thing}
|
||||
saveThing: Enregistrer {thing}
|
||||
filterByDesign: Filter by design
|
||||
noFilter: Do not filter
|
||||
filterByDesignDocs: If you have a specific design in mind, you can <b>filter by design</b> to only list those measurements that are required for this design.
|
||||
setLacksMeasiesForDesign: This set lacks measurements required for this pattern
|
||||
setHasMeasiesForDesign: This set has all measurements required for this pattern
|
||||
someSetsLacking: Some of your sets lack the measurments required to generate this pattern
|
||||
theseSetsReady: These sets have all required measurments to generate this pattern
|
||||
chooseSet: Choisis un ensemble de mesures
|
||||
patternForWhichSet: Pour quel ensemble de mesures devons-nous générer un modèle ?
|
||||
bookmarkedSets: Jeux de mesures que tu as mis en favoris
|
||||
curatedSets: FreeSewing's curated measurements sets
|
||||
curatedSetsAbout: Des ensembles de mesures curatées par FreeSewing que tu peux utiliser pour tester notre plateforme, ou tes créations.
|
||||
curateCuratedSets: Curate our selection of curated measurements sets
|
||||
useThisSet: Utilise cette série de mesures
|
||||
ownSets: Your own measurements sets
|
||||
noOwnSets: You do not have any of your own measurements sets (yet)
|
||||
pleaseMtm: Because made-to-measure lies at the heart of what we do, we strongly suggest you take accurate measurements.
|
||||
noOwnSetsMsg: You can store your measurements as a measurements set, after which you can generate as many patterns as you want for them.
|
||||
measurements: Mensurations
|
||||
chooseASet: Choose a measurements set
|
||||
showImage: Show image
|
||||
suggestForCuration: Suggest for curation
|
||||
suggestCset: Suggest a measurements set for curation
|
||||
csetAllMeasies: To ensure curated measurements sets work for all designs, you need to provide a full set of measurements.
|
||||
csetMissing: Your measurements set is missing the following measurements
|
||||
allMeasiesAvailable: All measurements are available.
|
||||
csetHeightMsg1: To allow organizing and presenting our curated sets in a structured way, we organize them by height.
|
||||
csetImgMsg: Finally, we need a picture. Please refer to the documentation to see what makes a good picture for a curated measurements set.
|
||||
docs: Documentation
|
||||
csetNotesMsg: If you would like to add any notes, you can do so here.
|
||||
thankYouVeryMuch: Thank you very much
|
||||
csetSuggestedMsg: Your submission has been registered and will be processed by one of our curators.
|
||||
itIsAvailableAt: It is available at
|
||||
csetNameMsg: Each curated set has a name. You can suggest your own name or a pseudonym.
|
||||
patternNew: Génère un nouveau modèle
|
||||
patternNewInfo: Choisis un modèle, ajoute tes mesures et nous créerons pour toi un patron de couture sur mesure.
|
||||
designNew: Créer un nouveau dessin
|
||||
designNewInfo: Les motifs FreeSewing sont de petits paquets de code JavaScript qui génèrent des motifs. Il n'est pas difficile de créer tes propres motifs, et nous avons un tutoriel détaillé pour te permettre de commencer.
|
||||
pluginNew: Créer un nouveau plugin
|
||||
pluginNewInfo: Les fonctionnalités de FreeSewing peuvent être étendues grâce à des plugins. Créer un plugin est facile, et nous avons un guide pour te guider du début à la fin.
|
||||
showcaseNew: Create a new showcase post
|
||||
showcaseNewInfo: If you would like to share something you (or someone else) made based on our designs, you can start a draft showcase post here.
|
||||
blogNew: Create a new blog post
|
||||
blogNewInfo: If you would like to write on the FreeSewing blog, you can start a draft blog post here.
|
||||
csetNew: Suggest a new curated measurements set
|
||||
csetNewInfo: We curate a collection of vetted measurments sets that we use to test patterns. You can suggest a measurements set here.
|
||||
opackNew: Suggest a new options pack
|
||||
opackNewInfo: We curate a collection of vetted option packs for each of our designs. You can suggest your options here.
|
||||
newPopular: Most popular
|
||||
newShare: Share / Show
|
||||
newDev: Design / Develop
|
||||
generateANewThing: "Génère un nouveau { thing }"
|
||||
website: Website
|
||||
linkedIdentities: Linked Identities
|
||||
websiteTitle: Do you have a website or other URL you'd like to add?
|
||||
platformTitle: Who are you on { platform }?
|
||||
platformWhy: We do not use this data in any way. This is only here so FreeSewing users can connect the dots across platforms.
|
||||
security: Security
|
||||
revealPassword: Reveal password
|
||||
hidePassword: Hide password
|
||||
#Design view
|
||||
designs: Designs
|
||||
code: Code
|
||||
aboutThing: About { thing }
|
||||
requiredMeasurements: Mensurations requises
|
||||
optionalMeasurements: Mesures optionnelles
|
||||
designOptions: Options de design
|
||||
parts: Parts
|
||||
plugins: Plugins
|
||||
specifications: Specifications
|
||||
visitShowcase: Visit showcase post
|
||||
examples: Exemples
|
||||
noExamples: We currently do not have any examples for this design
|
||||
noExamplesMsg: We rely on the FreeSewing community to submit examples in our showcase posts.
|
||||
ownPublicPattern: This is the public view on one of your own patterns. For more options, access the private view.
|
||||
ownPrivatePattern: This is the private view on your pattern. The public view will work for you even when the pattern is private. It will only work for others when the pattern is public.
|
||||
privateView: Private view
|
||||
publicView: Public view
|
62
sites/shared/components/account/github.mjs
Normal file
62
sites/shared/components/account/github.mjs
Normal file
|
@ -0,0 +1,62 @@
|
|||
// Context
|
||||
import { LoadingStatusContext } from 'shared/context/loading-status-context.mjs'
|
||||
// Hooks
|
||||
import { useState, useContext } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { useAccount } from 'shared/hooks/use-account.mjs'
|
||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
// Components
|
||||
import { BackToAccountButton } from './shared.mjs'
|
||||
import { SaveSettingsButton } from 'shared/components/buttons/save-settings-button.mjs'
|
||||
import { StringInput } from 'shared/components/inputs.mjs'
|
||||
import { DynamicMdx } from 'shared/components/mdx/dynamic.mjs'
|
||||
|
||||
export const ns = ['account', 'status']
|
||||
|
||||
export const GithubSettings = () => {
|
||||
// Hooks
|
||||
const { account, setAccount } = useAccount()
|
||||
const backend = useBackend()
|
||||
const { t, i18n } = useTranslation(ns)
|
||||
const { setLoadingStatus } = useContext(LoadingStatusContext)
|
||||
|
||||
// State
|
||||
const [githubUsername, setGithubUsername] = useState(account.data.githubUsername || '')
|
||||
const [githubEmail, setGithubEmail] = useState(account.data.githubEmail || '')
|
||||
|
||||
// Helper method to save changes
|
||||
const save = async () => {
|
||||
setLoadingStatus([true, 'processingUpdate'])
|
||||
const result = await backend.updateAccount({ data: { githubUsername, githubEmail } })
|
||||
if (result.success) {
|
||||
setAccount(result.data.account)
|
||||
setLoadingStatus([true, 'settingsSaved', true, true])
|
||||
} else setLoadingStatus([true, 'backendError', true, false])
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-xl">
|
||||
<h2 className="text-4xl">{t('githubTitle')}</h2>
|
||||
<StringInput
|
||||
id="account-github-email"
|
||||
label={t('email')}
|
||||
current={githubEmail}
|
||||
update={setGithubEmail}
|
||||
valid={(val) => val.length > 0}
|
||||
placeholder={'joostdecock'}
|
||||
docs={<DynamicMdx language={i18n.language} slug={`docs/site/account/github`} />}
|
||||
/>
|
||||
<StringInput
|
||||
id="account-github-username"
|
||||
label={t('username')}
|
||||
current={githubUsername}
|
||||
update={setGithubUsername}
|
||||
valid={(val) => val.length > 0}
|
||||
placeholder={'joost@joost.at'}
|
||||
docs={<DynamicMdx language={i18n.language} slug={`docs/site/account/github`} />}
|
||||
/>
|
||||
<SaveSettingsButton btnProps={{ onClick: save }} />
|
||||
<BackToAccountButton />
|
||||
</div>
|
||||
)
|
||||
}
|
88
sites/shared/components/account/img.mjs
Normal file
88
sites/shared/components/account/img.mjs
Normal file
|
@ -0,0 +1,88 @@
|
|||
// Dependencies
|
||||
import { cloudflareImageUrl } from 'shared/utils.mjs'
|
||||
// Context
|
||||
import { LoadingStatusContext } from 'shared/context/loading-status-context.mjs'
|
||||
// Hooks
|
||||
import { useState, useContext } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { useAccount } from 'shared/hooks/use-account.mjs'
|
||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
// Components
|
||||
import { Icons, welcomeSteps, BackToAccountButton } from './shared.mjs'
|
||||
import { ContinueButton } from 'shared/components/buttons/continue-button.mjs'
|
||||
import { SaveSettingsButton } from 'shared/components/buttons/save-settings-button.mjs'
|
||||
import { PassiveImageInput } from 'shared/components/inputs.mjs'
|
||||
import { DynamicMdx } from 'shared/components/mdx/dynamic.mjs'
|
||||
|
||||
export const ns = ['account', 'status']
|
||||
|
||||
export const ImgSettings = ({ welcome = false }) => {
|
||||
const { account, setAccount } = useAccount()
|
||||
const backend = useBackend()
|
||||
const { setLoadingStatus } = useContext(LoadingStatusContext)
|
||||
const { t, i18n } = useTranslation(ns)
|
||||
|
||||
const [img, setImg] = useState('')
|
||||
|
||||
const save = async () => {
|
||||
setLoadingStatus([true, 'processingUpdate'])
|
||||
const result = await backend.updateAccount({ img })
|
||||
if (result.success) {
|
||||
setAccount(result.data.account)
|
||||
setLoadingStatus([true, 'settingsSaved', true, true])
|
||||
} else setLoadingStatus([true, 'backendError', true, false])
|
||||
}
|
||||
|
||||
const nextHref = '/docs/guide'
|
||||
|
||||
return (
|
||||
<div className="max-w-xl">
|
||||
{!welcome || img !== false ? (
|
||||
<img
|
||||
alt="img"
|
||||
src={img || cloudflareImageUrl({ id: `user-${account.ihash}`, variant: 'public' })}
|
||||
className="shadow mb-4"
|
||||
/>
|
||||
) : null}
|
||||
<PassiveImageInput
|
||||
id="account-img"
|
||||
label={t('image')}
|
||||
placeholder={'image'}
|
||||
update={setImg}
|
||||
current={img}
|
||||
valid={(val) => val.length > 0}
|
||||
docs={<DynamicMdx language={i18n.language} slug={`docs/site/account/img`} />}
|
||||
/>
|
||||
{welcome ? (
|
||||
<>
|
||||
<button className={`btn btn-secondary mt-4 px-8`} onClick={save} disabled={!img}>
|
||||
{t('save')}
|
||||
</button>
|
||||
<ContinueButton btnProps={{ href: nextHref }} link />
|
||||
{welcomeSteps[account.control].length > 0 ? (
|
||||
<>
|
||||
<progress
|
||||
className="progress progress-primary w-full mt-12"
|
||||
value={700 / welcomeSteps[account.control].length}
|
||||
max="100"
|
||||
></progress>
|
||||
<span className="pt-4 text-sm font-bold opacity-50">
|
||||
7 / {welcomeSteps[account.control].length}
|
||||
</span>
|
||||
<Icons
|
||||
done={welcomeSteps[account.control].slice(0, 6)}
|
||||
todo={welcomeSteps[account.control].slice(7)}
|
||||
current="img"
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<SaveSettingsButton btnProps={{ onClick: save }} />
|
||||
<BackToAccountButton />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
90
sites/shared/components/account/imperial.mjs
Normal file
90
sites/shared/components/account/imperial.mjs
Normal file
|
@ -0,0 +1,90 @@
|
|||
// Context
|
||||
import { LoadingStatusContext } from 'shared/context/loading-status-context.mjs'
|
||||
// Hooks
|
||||
import { useState, useContext } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { useAccount } from 'shared/hooks/use-account.mjs'
|
||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
// Components
|
||||
import { Icons, welcomeSteps, BackToAccountButton, NumberBullet } from './shared.mjs'
|
||||
import { ContinueButton } from 'shared/components/buttons/continue-button.mjs'
|
||||
import { ListInput } from 'shared/components/inputs.mjs'
|
||||
import { DynamicMdx } from 'shared/components/mdx/dynamic.mjs'
|
||||
|
||||
export const ns = ['account', 'status']
|
||||
|
||||
export const ImperialSettings = ({ welcome = false }) => {
|
||||
// Hooks
|
||||
const { account, setAccount } = useAccount()
|
||||
const { setLoadingStatus } = useContext(LoadingStatusContext)
|
||||
const backend = useBackend()
|
||||
const { t, i18n } = useTranslation(ns)
|
||||
|
||||
// State
|
||||
const [selection, setSelection] = useState(account?.imperial === true ? 'imperial' : 'metric')
|
||||
|
||||
// Helper method to update account
|
||||
const update = async (val) => {
|
||||
if (val !== selection) {
|
||||
setLoadingStatus([true, 'processingUpdate'])
|
||||
const result = await backend.updateAccount({ imperial: val === 'imperial' ? true : false })
|
||||
if (result.success) {
|
||||
setAccount(result.data.account)
|
||||
setSelection(val)
|
||||
setLoadingStatus([true, 'settingsSaved', true, true])
|
||||
} else setLoadingStatus([true, 'backendError', true, true])
|
||||
}
|
||||
}
|
||||
|
||||
// Next step in the onboarding
|
||||
const nextHref =
|
||||
welcomeSteps[account?.control].length > 3
|
||||
? '/welcome/' + welcomeSteps[account?.control][3]
|
||||
: '/docs/guide'
|
||||
|
||||
return (
|
||||
<div className="max-w-xl">
|
||||
<ListInput
|
||||
id="account-units"
|
||||
label={t('unitsTitle')}
|
||||
list={['metric', 'imperial'].map((val) => ({
|
||||
val,
|
||||
label: (
|
||||
<div className="flex flex-row items-center w-full justify-between">
|
||||
<span>{t(`${val}Units`)}</span>
|
||||
<NumberBullet nr={val === 'imperial' ? '″' : 'cm'} color="secondary" />
|
||||
</div>
|
||||
),
|
||||
desc: t(`${val}Unitsd`),
|
||||
}))}
|
||||
current={selection}
|
||||
update={update}
|
||||
docs={<DynamicMdx language={i18n.language} slug={`docs/site/account/units`} />}
|
||||
/>
|
||||
{welcome ? (
|
||||
<>
|
||||
<ContinueButton btnProps={{ href: nextHref }} link />
|
||||
{welcomeSteps[account?.control].length > 0 ? (
|
||||
<>
|
||||
<progress
|
||||
className="progress progress-primary w-full mt-12"
|
||||
value={300 / welcomeSteps[account?.control].length}
|
||||
max="100"
|
||||
></progress>
|
||||
<span className="pt-4 text-sm font-bold opacity-50">
|
||||
3 / {welcomeSteps[account?.control].length}
|
||||
</span>
|
||||
<Icons
|
||||
done={welcomeSteps[account?.control].slice(0, 2)}
|
||||
todo={welcomeSteps[account?.control].slice(3)}
|
||||
current="units"
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<BackToAccountButton />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
66
sites/shared/components/account/language.mjs
Normal file
66
sites/shared/components/account/language.mjs
Normal file
|
@ -0,0 +1,66 @@
|
|||
// Context
|
||||
import { LoadingStatusContext } from 'shared/context/loading-status-context.mjs'
|
||||
// Hooks
|
||||
import { useState, useContext } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { useAccount } from 'shared/hooks/use-account.mjs'
|
||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
// Components
|
||||
import { BackToAccountButton, NumberBullet } from './shared.mjs'
|
||||
import { ListInput } from 'shared/components/inputs.mjs'
|
||||
import { DynamicMdx } from 'shared/components/mdx/dynamic.mjs'
|
||||
// Config
|
||||
import { siteConfig as conf } from 'site/site.config.mjs'
|
||||
|
||||
export const ns = ['account', 'locales', 'status']
|
||||
|
||||
export const LanguageSettings = () => {
|
||||
// Hooks
|
||||
const { account, setAccount } = useAccount()
|
||||
const { setLoadingStatus } = useContext(LoadingStatusContext)
|
||||
const backend = useBackend()
|
||||
const { t, i18n } = useTranslation(ns)
|
||||
|
||||
// State
|
||||
const [language, setLanguage] = useState(account.language || 'en')
|
||||
|
||||
// Helper method to update the account
|
||||
const update = async (lang) => {
|
||||
if (lang !== language) {
|
||||
setLoadingStatus([true, 'processingUpdate'])
|
||||
setLanguage(lang)
|
||||
const result = await backend.updateAccount({ language: lang })
|
||||
if (result.success) {
|
||||
setAccount(result.data.account)
|
||||
setLoadingStatus([true, 'settingsSaved', true, true])
|
||||
} else setLoadingStatus([true, 'backendError', true, true])
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-xl">
|
||||
<ListInput
|
||||
id="account-language"
|
||||
label={t('languageTitle')}
|
||||
list={conf.languages.map((val) => ({
|
||||
val,
|
||||
label: (
|
||||
<div className="flex flex-row items-center w-full justify-between">
|
||||
<span>
|
||||
{t(`locales:${val}`)}
|
||||
<span className="px-2 opacity-50">|</span>
|
||||
{t(`locales:${val}`, { lng: val })}
|
||||
</span>
|
||||
<NumberBullet nr={val} color="secondary" />
|
||||
</div>
|
||||
),
|
||||
desc: t('languageTitle', { lng: val }),
|
||||
}))}
|
||||
current={language}
|
||||
update={update}
|
||||
docs={<DynamicMdx language={i18n.language} slug={`docs/site/account/language`} />}
|
||||
/>
|
||||
<BackToAccountButton />
|
||||
</div>
|
||||
)
|
||||
}
|
278
sites/shared/components/account/links.mjs
Normal file
278
sites/shared/components/account/links.mjs
Normal file
|
@ -0,0 +1,278 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import { useAccount } from 'shared/hooks/use-account.mjs'
|
||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import Link from 'next/link'
|
||||
import { freeSewingConfig as conf, controlLevels } from 'shared/config/freesewing.config.mjs'
|
||||
import {
|
||||
MsetIcon,
|
||||
SignoutIcon,
|
||||
UserIcon,
|
||||
UnitsIcon,
|
||||
I18nIcon,
|
||||
ShowcaseIcon,
|
||||
ChatIcon,
|
||||
EmailIcon,
|
||||
KeyIcon,
|
||||
BookmarkIcon,
|
||||
CompareIcon,
|
||||
PrivacyIcon,
|
||||
ControlIcon,
|
||||
LockIcon,
|
||||
NewsletterIcon,
|
||||
ShieldIcon,
|
||||
FingerprintIcon,
|
||||
GitHubIcon,
|
||||
InstagramIcon,
|
||||
MastodonIcon,
|
||||
TwitterIcon,
|
||||
TwitchIcon,
|
||||
TikTokIcon,
|
||||
LinkIcon,
|
||||
TrashIcon,
|
||||
RedditIcon,
|
||||
ExportIcon,
|
||||
CloseIcon,
|
||||
ReloadIcon,
|
||||
NoIcon,
|
||||
PatternIcon,
|
||||
BoolYesIcon,
|
||||
BoolNoIcon,
|
||||
} from 'shared/components/icons.mjs'
|
||||
import { cloudflareImageUrl, capitalize } from 'shared/utils.mjs'
|
||||
import { ControlScore } from 'shared/components/control/score.mjs'
|
||||
|
||||
export const ns = ['account', 'i18n']
|
||||
|
||||
const itemIcons = {
|
||||
bookmarks: <BookmarkIcon />,
|
||||
sets: <MsetIcon />,
|
||||
patterns: <PatternIcon />,
|
||||
apikeys: <KeyIcon />,
|
||||
username: <UserIcon />,
|
||||
email: <EmailIcon />,
|
||||
bio: <ChatIcon />,
|
||||
img: <ShowcaseIcon />,
|
||||
language: <I18nIcon />,
|
||||
units: <UnitsIcon />,
|
||||
compare: <CompareIcon />,
|
||||
consent: <PrivacyIcon />,
|
||||
control: <ControlIcon />,
|
||||
mfa: <ShieldIcon />,
|
||||
newsletter: <NewsletterIcon />,
|
||||
password: <LockIcon />,
|
||||
github: <GitHubIcon />,
|
||||
instagram: <InstagramIcon />,
|
||||
mastodon: <MastodonIcon />,
|
||||
twitter: <TwitterIcon />,
|
||||
twitch: <TwitchIcon />,
|
||||
tiktok: <TikTokIcon />,
|
||||
website: <LinkIcon />,
|
||||
reddit: <RedditIcon />,
|
||||
}
|
||||
|
||||
const itemClasses = 'flex flex-row items-center justify-between bg-opacity-10 p-2 px-4 rounded mb-1'
|
||||
|
||||
const AccountLink = ({ href, title, children }) => (
|
||||
<Link
|
||||
className={`${itemClasses} hover:bg-secondary hover:bg-opacity-10 max-w-md`}
|
||||
href={href}
|
||||
title={title}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
)
|
||||
|
||||
const YesNo = ({ check }) => (check ? <BoolYesIcon /> : <BoolNoIcon />)
|
||||
|
||||
export const AccountLinks = () => {
|
||||
const { account, signOut, control } = useAccount()
|
||||
const { t } = useTranslation(ns)
|
||||
const backend = useBackend()
|
||||
|
||||
const [bookmarks, setBookmarks] = useState([])
|
||||
const [sets, setSets] = useState([])
|
||||
const [patterns, setPatterns] = useState([])
|
||||
const [apikeys, setApikeys] = useState([])
|
||||
|
||||
useEffect(() => {
|
||||
const getUserData = async () => {
|
||||
const result = await backend.getUserData(account.id)
|
||||
if (result.success) {
|
||||
setApikeys(result.data.data.apikeys)
|
||||
setBookmarks(result.data.data.bookmarks)
|
||||
setPatterns(result.data.data.patterns)
|
||||
setSets(result.data.data.sets)
|
||||
}
|
||||
}
|
||||
getUserData()
|
||||
}, [account.id])
|
||||
|
||||
const btnClasses = 'btn capitalize flex flex-row justify-between'
|
||||
|
||||
if (!account.username) return null
|
||||
|
||||
const itemPreviews = {
|
||||
apikeys: apikeys?.length || 0,
|
||||
bookmarks: bookmarks?.length || 0,
|
||||
sets: sets?.length || 0,
|
||||
patterns: patterns?.length || 0,
|
||||
username: account.username,
|
||||
email: account.email,
|
||||
bio: account.bio ? <span>{account.bio.slice(0, 15)}…</span> : '',
|
||||
img: (
|
||||
<img
|
||||
src={cloudflareImageUrl({ type: 'sq100', id: `user-${account.ihash}` })}
|
||||
className="w-8 h-8 aspect-square rounded-full shadow"
|
||||
/>
|
||||
),
|
||||
language: t(`i18n:${account.language}`),
|
||||
units: t(account.imperial ? 'imperialUnits' : 'metricUnits'),
|
||||
newsletter: <YesNo check={account.newsletter} />,
|
||||
compare: <YesNo check={account.compare} />,
|
||||
consent: <YesNo check={account.consent} />,
|
||||
control: <ControlScore control={account.control} />,
|
||||
github: account.data.githubUsername || account.data.githubEmail || <NoIcon />,
|
||||
password: account.passwordType === 'v3' ? <BoolYesIcon /> : <NoIcon />,
|
||||
mfa: <YesNo check={false} />,
|
||||
}
|
||||
for (const social of Object.keys(conf.account.fields.identities).filter((i) => i !== 'github'))
|
||||
itemPreviews[social] = account.data[social] || (
|
||||
<NoIcon className="text-base-content w-6 h-6" stroke={2} />
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-7xl">
|
||||
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4 mb-8">
|
||||
<div className="">
|
||||
<h4 className="my-2">{t('data')}</h4>
|
||||
{Object.keys(conf.account.fields.data).map((item) =>
|
||||
controlLevels[item] > control ? null : (
|
||||
<AccountLink href={`/account/${item}`} title={t(item)} key={item}>
|
||||
<div className="flex flex-row items-center gap-3 font-medium">
|
||||
{itemIcons[item]}
|
||||
{t(`your${capitalize(item)}`)}
|
||||
</div>
|
||||
<div className="">{itemPreviews[item]}</div>
|
||||
</AccountLink>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
{control > 1 && (
|
||||
<div className="">
|
||||
<h4 className="my-2">{t('info')}</h4>
|
||||
{Object.keys(conf.account.fields.info).map((item) =>
|
||||
controlLevels[item] > control ? null : (
|
||||
<AccountLink href={`/account/${item}`} title={t(item)} key={item}>
|
||||
<div className="flex flex-row items-center gap-3 font-medium">
|
||||
{itemIcons[item]}
|
||||
{t(item)}
|
||||
</div>
|
||||
<div className="">{itemPreviews[item]}</div>
|
||||
</AccountLink>
|
||||
)
|
||||
)}
|
||||
<div className={`${itemClasses} bg-neutral max-w-md`}>
|
||||
<div className="flex flex-row items-center gap-3 font-medium">
|
||||
<FingerprintIcon />
|
||||
<span>{t('userId')}</span>
|
||||
</div>
|
||||
<div className="">{account.id}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="">
|
||||
<h4 className="my-2">{t('settings')}</h4>
|
||||
{Object.keys(conf.account.fields.settings).map((item) =>
|
||||
controlLevels[item] > control ? null : (
|
||||
<AccountLink href={`/account/${item}`} title={t(item)} key={item}>
|
||||
<div className="flex flex-row items-center gap-3 font-medium">
|
||||
{itemIcons[item]}
|
||||
{t(item)}
|
||||
</div>
|
||||
<div className="">{itemPreviews[item]}</div>
|
||||
</AccountLink>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
{control > 2 && (
|
||||
<div className="">
|
||||
<h4 className="my-2">{t('linkedIdentities')}</h4>
|
||||
{Object.keys(conf.account.fields.identities).map((item) =>
|
||||
controlLevels[item] > control ? null : (
|
||||
<AccountLink href={`/account/${item}`} title={t(item)} key={item}>
|
||||
<div className="flex flex-row items-center gap-3 font-medium">
|
||||
{itemIcons[item]}
|
||||
{t(item)}
|
||||
</div>
|
||||
<div className="">{itemPreviews[item]}</div>
|
||||
</AccountLink>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{control > 1 && (
|
||||
<div className="">
|
||||
<h4 className="my-2">{t('security')}</h4>
|
||||
{Object.keys(conf.account.fields.security).map((item) =>
|
||||
controlLevels[item] > control ? null : (
|
||||
<AccountLink href={`/account/${item}`} title={t(item)} key={item}>
|
||||
<div className="flex flex-row items-center gap-3 font-medium">
|
||||
{itemIcons[item]}
|
||||
{t(item)}
|
||||
</div>
|
||||
<div className="">{itemPreviews[item]}</div>
|
||||
</AccountLink>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{control > 1 && (
|
||||
<div className="">
|
||||
<h4 className="my-2">{t('actions')}</h4>
|
||||
{control > 2 && (
|
||||
<AccountLink href={`/account/reload`} title={t('reload')}>
|
||||
<ReloadIcon />
|
||||
{t('reload')}
|
||||
</AccountLink>
|
||||
)}
|
||||
{control > 2 && (
|
||||
<AccountLink href={`/account/export`} title={t('export')}>
|
||||
<ExportIcon />
|
||||
{t('export')}
|
||||
</AccountLink>
|
||||
)}
|
||||
{control > 3 && (
|
||||
<AccountLink href={`/account/restrict`} title={t('restrict')}>
|
||||
<CloseIcon />
|
||||
{t('restrict')}
|
||||
</AccountLink>
|
||||
)}
|
||||
<AccountLink href={`/account/remove`} title={t('remove')}>
|
||||
<TrashIcon />
|
||||
{t('remove')}
|
||||
</AccountLink>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row flex-wrap gap-2 md:gap-4 justify-end">
|
||||
{control > 1 && (
|
||||
<Link className={`${btnClasses} btn-secondary md:w-64 w-full`} href="/profile">
|
||||
<UserIcon />
|
||||
{t('yourProfile')}
|
||||
</Link>
|
||||
)}
|
||||
<button className={`${btnClasses} btn-neutral md:w-64 w-full`} onClick={() => signOut()}>
|
||||
<SignoutIcon />
|
||||
{t('signOut')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
157
sites/shared/components/account/mfa.mjs
Normal file
157
sites/shared/components/account/mfa.mjs
Normal file
|
@ -0,0 +1,157 @@
|
|||
// Context
|
||||
import { LoadingStatusContext } from 'shared/context/loading-status-context.mjs'
|
||||
// Hooks
|
||||
import { useState, useContext } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { useAccount } from 'shared/hooks/use-account.mjs'
|
||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
// Components
|
||||
import { BackToAccountButton } from './shared.mjs'
|
||||
import { Popout } from 'shared/components/popout/index.mjs'
|
||||
import { Bullet } from 'shared/components/bullet.mjs'
|
||||
|
||||
export const ns = ['account']
|
||||
|
||||
const CodeInput = ({ code, setCode, t }) => (
|
||||
<input
|
||||
value={code}
|
||||
onChange={(evt) => setCode(evt.target.value)}
|
||||
className="input w-full text-4xl input-bordered input-lg flex flex-row text-center mb-8 tracking-widest"
|
||||
type="number"
|
||||
placeholder={t('000000')}
|
||||
/>
|
||||
)
|
||||
|
||||
export const MfaSettings = ({ title = false, welcome = false }) => {
|
||||
// Hooks
|
||||
const { account, setAccount } = useAccount()
|
||||
const backend = useBackend()
|
||||
const { t } = useTranslation(ns)
|
||||
const { setLoadingStatus } = useContext(LoadingStatusContext)
|
||||
|
||||
// State
|
||||
const [enable, setEnable] = useState(false)
|
||||
const [disable, setDisable] = useState(false)
|
||||
const [code, setCode] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
|
||||
// Helper method to enable MFA
|
||||
const enableMfa = async () => {
|
||||
setLoadingStatus([true, 'processingUpdate'])
|
||||
const result = await backend.enableMfa()
|
||||
if (result.success) {
|
||||
setEnable(result.data.mfa)
|
||||
setLoadingStatus([true, 'settingsSaved', true, true])
|
||||
} else setLoadingStatus([true, 'backendError', true, false])
|
||||
}
|
||||
|
||||
// Helper method to disable MFA
|
||||
const disableMfa = async () => {
|
||||
setLoadingStatus([true, 'processingUpdate'])
|
||||
const result = await backend.disableMfa({
|
||||
mfa: false,
|
||||
password,
|
||||
token: code,
|
||||
})
|
||||
if (result) {
|
||||
if (result.success) {
|
||||
setAccount(result.data.account)
|
||||
setLoadingStatus([true, 'settingsSaved', true, true])
|
||||
} else setLoadingStatus([true, 'backendError', true, false])
|
||||
setDisable(false)
|
||||
setEnable(false)
|
||||
setCode('')
|
||||
setPassword('')
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to confirm MFA
|
||||
const confirmMfa = async () => {
|
||||
setLoadingStatus([true, 'processingUpdate'])
|
||||
const result = await backend.confirmMfa({
|
||||
mfa: true,
|
||||
secret: enable.secret,
|
||||
token: code,
|
||||
})
|
||||
if (result.success) {
|
||||
setAccount(result.data.account)
|
||||
setLoadingStatus([true, 'settingsSaved', true, true])
|
||||
} else setLoadingStatus([true, 'backendError', true, false])
|
||||
setEnable(false)
|
||||
setCode('')
|
||||
}
|
||||
|
||||
// Figure out what title to use
|
||||
let titleText = account.mfaEnabled ? t('mfaEnabled') : t('mfaDisabled')
|
||||
if (enable) titleText = t('mfaSetup')
|
||||
|
||||
return (
|
||||
<div className="max-w-xl">
|
||||
{title ? <h2 className="text-4xl">{titleText}</h2> : null}
|
||||
{enable ? (
|
||||
<>
|
||||
<div className="flex flex-row items-center justify-center px-8 lg:px-36">
|
||||
<div dangerouslySetInnerHTML={{ __html: enable.qrcode }} />
|
||||
</div>
|
||||
<Bullet num="1">{t('mfaAdd')}</Bullet>
|
||||
<Bullet num="2">{t('confirmWithMfa')}</Bullet>
|
||||
<input
|
||||
value={code}
|
||||
onChange={(evt) => setCode(evt.target.value)}
|
||||
className="input w-64 m-auto text-4xl input-bordered input-lg flex flex-row text-center mb-8 tracking-widest"
|
||||
type="number"
|
||||
placeholder={t('000000')}
|
||||
/>
|
||||
<button className="btn btn-success btn-lg block w-full" onClick={confirmMfa}>
|
||||
{t('enableMfa')}
|
||||
</button>
|
||||
</>
|
||||
) : null}
|
||||
{disable ? (
|
||||
<div className="my-8">
|
||||
<Bullet num="1">
|
||||
<h5>{t('confirmWithPassword')}</h5>
|
||||
<input
|
||||
value={password}
|
||||
onChange={(evt) => setPassword(evt.target.value)}
|
||||
className="input w-full input-bordered flex flex-row"
|
||||
type="text"
|
||||
placeholder={t('passwordPlaceholder')}
|
||||
/>
|
||||
</Bullet>
|
||||
<Bullet num="2">
|
||||
<h5>{t('confirmWithMfa')}</h5>
|
||||
<CodeInput code={code} setCode={setCode} t={t} />
|
||||
</Bullet>
|
||||
<button
|
||||
className="btn btn-error btn-lg block w-full"
|
||||
onClick={disableMfa}
|
||||
disabled={code.length < 4 || password.length < 3}
|
||||
>
|
||||
{t('disableMfa')}
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="flex flex-row items-center mt-4">
|
||||
{account.mfaEnabled ? (
|
||||
disable ? null : (
|
||||
<button className="btn btn-primary block w-full" onClick={() => setDisable(true)}>
|
||||
{t('disableMfa')}
|
||||
</button>
|
||||
)
|
||||
) : enable ? null : (
|
||||
<div>
|
||||
<button className="btn btn-primary block w-full" onClick={enableMfa}>
|
||||
{t('mfaSetup')}
|
||||
</button>
|
||||
<Popout tip>
|
||||
<h5>{t('mfaTipTitle')}</h5>
|
||||
<p>{t('mfaTipMsg')}</p>
|
||||
</Popout>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!welcome && <BackToAccountButton />}
|
||||
</div>
|
||||
)
|
||||
}
|
96
sites/shared/components/account/newsletter.mjs
Normal file
96
sites/shared/components/account/newsletter.mjs
Normal file
|
@ -0,0 +1,96 @@
|
|||
// Context
|
||||
import { LoadingStatusContext } from 'shared/context/loading-status-context.mjs'
|
||||
// Hooks
|
||||
import { useState, useContext } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { useAccount } from 'shared/hooks/use-account.mjs'
|
||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
// Components
|
||||
import { BackToAccountButton, Icons, welcomeSteps } from './shared.mjs'
|
||||
import { ContinueButton } from 'shared/components/buttons/continue-button.mjs'
|
||||
import { ListInput } from 'shared/components/inputs.mjs'
|
||||
import { DynamicMdx } from 'shared/components/mdx/dynamic.mjs'
|
||||
import { OkIcon, NoIcon } from 'shared/components/icons.mjs'
|
||||
|
||||
export const ns = ['account', 'status']
|
||||
|
||||
export const NewsletterSettings = ({ welcome = false, bare = false }) => {
|
||||
// Hooks
|
||||
const { account, setAccount } = useAccount()
|
||||
const backend = useBackend()
|
||||
const { t, i18n } = useTranslation(ns)
|
||||
const { setLoadingStatus } = useContext(LoadingStatusContext)
|
||||
// State
|
||||
const [selection, setSelection] = useState(account?.newsletter ? 'yes' : 'no')
|
||||
|
||||
// Helper method to update account
|
||||
const update = async (val) => {
|
||||
if (val !== selection) {
|
||||
setLoadingStatus([true, 'processingUpdate'])
|
||||
const result = await backend.updateAccount({ newsletter: val === 'yes' ? true : false })
|
||||
if (result.success) {
|
||||
setAccount(result.data.account)
|
||||
setSelection(val)
|
||||
setLoadingStatus([true, 'settingsSaved', true, true])
|
||||
} else setLoadingStatus([true, 'backendError', true, true])
|
||||
}
|
||||
}
|
||||
|
||||
// Next step for onboarding
|
||||
const nextHref =
|
||||
welcomeSteps[account?.control].length > 2
|
||||
? '/welcome/' + welcomeSteps[account?.control][2]
|
||||
: '/docs/guide'
|
||||
|
||||
return (
|
||||
<div className="max-w-xl">
|
||||
<ListInput
|
||||
id="account-newsletter"
|
||||
label={t('newsletterTitle')}
|
||||
list={['yes', 'no'].map((val) => ({
|
||||
val,
|
||||
label: (
|
||||
<div className="flex flex-row items-center w-full justify-between">
|
||||
<span>{t(val === 'yes' ? 'newsletterYes' : 'noThanks')}</span>
|
||||
{val === 'yes' ? (
|
||||
<OkIcon className="w-8 h-8 text-success" stroke={4} />
|
||||
) : (
|
||||
<NoIcon className="w-8 h-8 text-error" stroke={3} />
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
desc: t(val === 'yes' ? 'newsletterYesd' : 'newsletterNod'),
|
||||
}))}
|
||||
current={selection}
|
||||
update={update}
|
||||
docs={<DynamicMdx language={i18n.language} slug={`account/site/account/newsletter`} />}
|
||||
/>
|
||||
{welcome ? (
|
||||
<>
|
||||
<ContinueButton btnProps={{ href: nextHref }} link />
|
||||
{welcomeSteps[account?.control].length > 0 ? (
|
||||
<>
|
||||
<progress
|
||||
className="progress progress-primary w-full mt-12"
|
||||
value={200 / welcomeSteps[account?.control].length}
|
||||
max="100"
|
||||
></progress>
|
||||
<span className="pt-4 text-sm font-bold opacity-50">
|
||||
2 / {welcomeSteps[account?.control].length}
|
||||
</span>
|
||||
<Icons
|
||||
done={welcomeSteps[account?.control].slice(0, 1)}
|
||||
todo={welcomeSteps[account?.control].slice(2)}
|
||||
current="newsletter"
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
) : bare ? null : (
|
||||
<BackToAccountButton />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NewsletterSettings
|
281
sites/shared/components/account/nl.yaml
Normal file
281
sites/shared/components/account/nl.yaml
Normal file
|
@ -0,0 +1,281 @@
|
|||
account: Account
|
||||
yourAccount: Jouw account
|
||||
newPattern: Nieuw patroon
|
||||
newSet: Start een nieuwe maten set
|
||||
links: Links
|
||||
info: Info
|
||||
settings: Instellingen
|
||||
data: Gegevens
|
||||
sets: Maten sets
|
||||
patterns: Patronen
|
||||
actions: Acties
|
||||
created: Aangemaakt
|
||||
updated: Bijgewerkt
|
||||
expires: Verloopt op
|
||||
yourProfile: Jouw profiel
|
||||
yourPatterns: Jouw patronen
|
||||
yourSets: Jouw maten sets
|
||||
signOut: Afmelden
|
||||
politeOhCrap: Jandorie
|
||||
bio: Bio
|
||||
email: E-mailadres
|
||||
img: Afbeelding
|
||||
username: Gebruikersnaam
|
||||
compare: Metricset Vergelijking
|
||||
consent: Privacy & Toestemming
|
||||
control: Gebruikerservaring
|
||||
imperial: Eenheden
|
||||
units: Eenheden
|
||||
apikeys: API-keys
|
||||
newsletter: Abonnement op nieuwsbrief
|
||||
password: Wachtwoord
|
||||
newPasswordPlaceholder: Voer hier je nieuwe wachtwoord in
|
||||
passwordPlaceholder: Voer hier je wachtwoord in
|
||||
mfa: Twee-Stappen Authenticatie
|
||||
mfaTipTitle: Overweeg om Twee-Stappen Authenticatie in te schakelen
|
||||
mfaTipMsg: We dringen je geen wachtwoordbeleid op, maar we raden je wel aan om Twee-Stappen Authenticatie in te schakelen om je FreeSewing account veilig te houden.
|
||||
mfaEnabled: Twee-Stappen Authenticatie is ingeschakeld
|
||||
mfaDisabled: Twee-Stappen Authenticatie is uitgeschakeld
|
||||
mfaSetup: Twee-Stappen Authenticatie instellen
|
||||
mfaAdd: Voeg FreeSewing toe aan je Authenticator App door de QR code hierboven te scannen.
|
||||
confirmWithPassword: Voer je wachtwoord in om deze actie te bevestigen
|
||||
confirmWithMfa: Voer een code in van je Authenticator App om deze actie te bevestigen
|
||||
enableMfa: Twee-Stappen Authenticatie inschakelen
|
||||
disableMfa: Twee-Stappen Authenticatie uitschakelen
|
||||
language: Taal
|
||||
developer: Ontwikkelaar
|
||||
design: Ontwerp
|
||||
patternMetadata: Pattern metadata
|
||||
clonePattern: Clone pattern
|
||||
updatePattern: Update pattern
|
||||
reload: Account herladen
|
||||
export: Exporteer je gegevens
|
||||
exportMsg: Click below to export your personal data
|
||||
exportNote: The EU's General Data Protection Regulation (GDPR) ensures your so-called right to data portability — the right to obtain and reuse your personal data for your own purposes, or across different services.
|
||||
exportDownload: "Your data was exported and is available for download at the following location:"
|
||||
review: Herzie je toestemmingen
|
||||
restrict: Beperk de verwerking van je gegevens
|
||||
disable: Je account desactiveren
|
||||
remove: Verwijder je account
|
||||
proceedWithCaution: Ga voorzichtig te werk
|
||||
restrictWarning: While no data will be removed, this will disable your account. Furthermore, you can not undo this on your own, but will have to contact support when you want to restore access to your account.
|
||||
noWayBack: There is no way back from this.
|
||||
mdSupport: Je kan hier markdown opmaak gebruiken
|
||||
or: of
|
||||
continue: Ga verder
|
||||
save: Opslaan
|
||||
noThanks: Dat hoeft niet
|
||||
areYouCertain: Weet je het zeker?
|
||||
delete: Verwijder
|
||||
#reload
|
||||
nailedIt: Dat is ook weer geregeld
|
||||
gone: Poef. Verdwenen.
|
||||
reloadMsg1: De gegevens die zijn opgeslagen in je browser kunnen soms gaan afwijken van de gegevens die zijn opgeslagen in onze backend.
|
||||
reloadMsg2: Hiermee kun je je accountgegevens synchroniseren met de backend. Het heeft hetzelfde effect als je afmelden en dan weer aanmelden
|
||||
#bio
|
||||
bioTitle: Vertel wat over jezelf
|
||||
bioPreview: Bio Voorbeeld
|
||||
bioPlaceholder: Ik maak kledij en schoenen. Ik ontwerp naaipatronen. Ik schrijf code. Ik beheer [FreeSewing](http://freesewing.org)
|
||||
#compare
|
||||
compareTitle: Voel je je ok als we maten gaan vergelijken?
|
||||
compareYes: Ja, voor zover het me kan helpen
|
||||
compareYesd: |
|
||||
We kunnen je tonen hoe jouw maten set zich verhoudt tot andere sets.
|
||||
Dit kan je helpen bij het opsporen van mogelijke foutjes bij het nemen van maten, of problemen in pratronen.
|
||||
compareNo: Nee, vergelijk nooit maten
|
||||
compareNod: |
|
||||
We zullen jouw maten sets nooit vergelijken met andere maten sets.
|
||||
Dit beperkt onze mogelijkheden om je te waarschuwen over mogelijke problemen in je maten sets of patronen.
|
||||
#control
|
||||
showMore: Toon meer
|
||||
control1.t: Houd het zo eenvoudig mogelijk
|
||||
control1.d: Toont alleen de meest essentiële functionaliteit.
|
||||
control2.t: Maak het eenvoudig, maar niet te eenvoudig
|
||||
control2.d: Verbergt de meeste functionaliteit.
|
||||
control3.t: Balanceer eenvoud met functionaliteit
|
||||
control3.d: Toont de meeste functionaliteit, maar niet de meest geavanceerde.
|
||||
control4.t: Geef me alle functionaliteit, maar hou het veilig
|
||||
control4.d: Onthult alle functionaliteit, met vangrails en veiligheidscontroles om vergissingen te vermijden.
|
||||
control5.t: Ik weet wat ik doe
|
||||
control5.d: Onthult alle functionaliteit, en verwijdert ook alle vangrails en veiligheidscontroles.
|
||||
controlShowMore: Meer opties tonen
|
||||
controlTitle: Welke gebruikerservaring heeft jouw voorkeur?
|
||||
#img
|
||||
imgTitle: Wat denk je van een leuke foto?
|
||||
imgDragAndDropImageHere: Sleep hier een afbeelding naartoe
|
||||
imgPasteUrlHere: Paste an image location (url) here
|
||||
imgSelectImage: Selecteer een afbeelding
|
||||
#newsletter
|
||||
newsletterTitle: Wil je de FreeSewing nieuwsbrief ontvangen?
|
||||
newsletterYes: Ja, ik wil graag de nieuwsbrief ontvangen
|
||||
newsletterYesd: Eens om de 3 maanden ontvang je van ons een e-mail met eerlijke, oprechte inhoud. Geen tracking, geen advertenties, geen onzin.
|
||||
newsletterNod: Je kunt later altijd van gedachten veranderen. Maar zolang je dat niet doet, sturen we je geen nieuwsbrieven.
|
||||
#imperial
|
||||
metricUnits: Metrische eenheden (cm)
|
||||
metricUnitsd: Kies deze optie als je de voorkeur geeft aan centimeters over duimen.
|
||||
imperialUnits: Imperiale (Engelse) eenheden (duim)
|
||||
imperialUnitsd: Kies deze optie als je de voorkeur geeft aan duimen boven centimeters.
|
||||
unitsTitle: Welke eenheden hebben jouw voorkeur?
|
||||
#username
|
||||
usernameTitle: Welke gebruikersnaam wil je?
|
||||
usernameAvailable: Username is available
|
||||
usernameNotAvailable: Deze gebruikersnaam is niet beschikbaar
|
||||
#email
|
||||
emailTitle: Waar kunnen we je bereiken als we daar een goede reden voor hebben (zoals wanneer je je wachtwoord bent vergeten)?
|
||||
oneMoreThing: En dan nog iets
|
||||
oneMomentPlease: Een ogenblikje alsjeblieft
|
||||
emailChangeConfirmation: We hebben een e-mail naar je nieuwe adres gestuurd om deze wijziging te bevestigen.
|
||||
vagueError: Er is iets fout gelopen en we weten niet meteen hoe we dit best oplossen. Probeer het opnieuw of contacteer ons voor assistentie.
|
||||
#github
|
||||
githubTitle: Link your GitHub identity
|
||||
githubWhy1: Enter your GitHub username and email here and we will use them when interacting with GitHub on your behalf.
|
||||
githubWhy2: Note that both your GitHub username and email is public info. This merely allows us to make a link between your FreeSewing account and GitHub account.
|
||||
githubWhy3: For example, when you report a problem on this website, we can mention you so you will receive notifications when there is an update. For this, your username is sufficient.
|
||||
githubWhy4: When you submit a showcase post or make changed to our content, we can credit those commits to you if we have both your username and the email address you use on GitHub.
|
||||
tooComplex: If all of this in confusing, you don't have to provide this info. It's an advanced feature.
|
||||
#languge
|
||||
languageTitle: Aan welke taal geef je de voorkeur?
|
||||
#password
|
||||
passwordTitle: Iets dat alleen jij kent
|
||||
#api key
|
||||
newApikey: Een nieuwe API-key aanmaken
|
||||
keyNewInfo: Maak een nieuwe API-key aan om verbinding te maken met de FreeSewing backend.
|
||||
keyName: Key naam
|
||||
keyNameDesc: Een unieke naam voor deze API key. Alleen zichtbaar voor jou.
|
||||
keyExpires: Key geldigheid
|
||||
keyExpiresDesc: "De sleutel verloopt op:"
|
||||
keyLevel: Key rechten
|
||||
keyLevel0: Alleen authenticatie
|
||||
keyLevel1: Lezen van je eigen patronen en maten sets
|
||||
keyLevel2: Lezen van al je accountgegevens
|
||||
keyLevel3: Lezen en schrijven van je eigen patronen en maten sets
|
||||
keyLevel4: Lezen en schrijven van al je accountgegevens
|
||||
keyLevel5: Lezen van patronen en maten sets van andere gebruikers
|
||||
keyLevel6: Lezen en schrijven van patronen en maten sets van andere gebruikers
|
||||
keyLevel7: Lezen en schrijven van alle accountgegevens van andere gebruikers
|
||||
keyLevel8: Zich voordoen als een andere gebruikers, volledige schrijftoegang tot alle gegevens
|
||||
cancel: Annuleren
|
||||
keySecretWarning: Dit is de enige keer dat je het sleutelgeheim kunt zien, zorg ervoor dat je het kopieert.
|
||||
keyExpired: Deze API-key is verlopen
|
||||
deleteKeyWarning: Het verwijderen van een API-key kan niet ongedaan worden gemaakt.
|
||||
keyDocsTitle: Refer to FreeSewing.dev for documentation on using API keys (English only)
|
||||
keyDocsMsg: This is an advanced feature aimed at developers or anyone who wants to interact with our backend directly.
|
||||
apiCalls: API Calls
|
||||
#bookmarks
|
||||
newBookmark: Add a Bookmark
|
||||
bookmark: Bookmark
|
||||
bookmarks: Bookmarks
|
||||
type: Type
|
||||
location: Location
|
||||
title: Titel
|
||||
new: Nieuw
|
||||
designBookmark: Collectie
|
||||
patternBookmark: Patronen
|
||||
setBookmark: Maten sets
|
||||
csetBookmark: Curated Measurements Sets
|
||||
docBookmark: Documentatie
|
||||
customBookmark: Custom Bookmarks
|
||||
yourBookmarks: Your bookmarks
|
||||
bookmarkThisThing: Bookmark this { thing }
|
||||
page: Page
|
||||
#sets
|
||||
set: Measurements Set
|
||||
name: Naam
|
||||
setNameDesc: Een naam om deze maten set te identificeren
|
||||
setNewInfo: Creëer een nieuwe maten set waar je vervolgens patronen voor kunt genereren.
|
||||
notes: Notities
|
||||
setNotesDesc: Notities die je wilt opslaan bij deze maten set
|
||||
description: Beschrijving
|
||||
deleteSetWarning: Het verwijderen van een maten set kan niet ongedaan worden gemaakt.
|
||||
image: Afbeelding
|
||||
measies: Maten
|
||||
setUnitsMsgTitle: Deze instellingen is alleen van toepassing op deze maten set
|
||||
setUnitsMsgDesc: |
|
||||
Dit zijn de eenheden die we gebruiken wanneer we de maten in deze set bijwerken of weergeven.
|
||||
Elders op deze website gebruiken we de eenheden die je in je account hebt ingesteld.
|
||||
public: Publiek
|
||||
publicSet: Publieke maten set
|
||||
privateSet: Privé maten set
|
||||
publicSetDesc: Anderen kunnen deze maten gebruiken om patronen aan te maken of te testen
|
||||
privateSetDesc: Deze maten mogen niet worden gebruikt door anderen om patronen aan te maken of te testen
|
||||
permalink: Link
|
||||
editThing: Bewerk {thing}
|
||||
saveThing: Bewaar {thing}
|
||||
filterByDesign: Filter by design
|
||||
noFilter: Do not filter
|
||||
filterByDesignDocs: If you have a specific design in mind, you can <b>filter by design</b> to only list those measurements that are required for this design.
|
||||
setLacksMeasiesForDesign: This set lacks measurements required for this pattern
|
||||
setHasMeasiesForDesign: This set has all measurements required for this pattern
|
||||
someSetsLacking: Some of your sets lack the measurments required to generate this pattern
|
||||
theseSetsReady: These sets have all required measurments to generate this pattern
|
||||
chooseSet: Kies een maten set
|
||||
patternForWhichSet: Voor welke maten set moeten we een patroon genereren?
|
||||
bookmarkedSets: Maten sets in je bladwijzers
|
||||
curatedSets: FreeSewing's curated measurements sets
|
||||
curatedSetsAbout: Maten sets samengesteld door FreeSewing die je kan gebruiken om ons platform of je ontwerpen te testen.
|
||||
curateCuratedSets: Curate our selection of curated measurements sets
|
||||
useThisSet: Gebruik deze maten set
|
||||
ownSets: Your own measurements sets
|
||||
noOwnSets: You do not have any of your own measurements sets (yet)
|
||||
pleaseMtm: Because made-to-measure lies at the heart of what we do, we strongly suggest you take accurate measurements.
|
||||
noOwnSetsMsg: You can store your measurements as a measurements set, after which you can generate as many patterns as you want for them.
|
||||
measurements: Maten
|
||||
chooseASet: Choose a measurements set
|
||||
showImage: Show image
|
||||
suggestForCuration: Suggest for curation
|
||||
suggestCset: Suggest a measurements set for curation
|
||||
csetAllMeasies: To ensure curated measurements sets work for all designs, you need to provide a full set of measurements.
|
||||
csetMissing: Your measurements set is missing the following measurements
|
||||
allMeasiesAvailable: All measurements are available.
|
||||
csetHeightMsg1: To allow organizing and presenting our curated sets in a structured way, we organize them by height.
|
||||
csetImgMsg: Finally, we need a picture. Please refer to the documentation to see what makes a good picture for a curated measurements set.
|
||||
docs: Documentatie
|
||||
csetNotesMsg: If you would like to add any notes, you can do so here.
|
||||
thankYouVeryMuch: Thank you very much
|
||||
csetSuggestedMsg: Your submission has been registered and will be processed by one of our curators.
|
||||
itIsAvailableAt: It is available at
|
||||
csetNameMsg: Each curated set has a name. You can suggest your own name or a pseudonym.
|
||||
patternNew: Een nieuw patroon genereren
|
||||
patternNewInfo: Kies een ontwerp, voeg je maten set toe en wij genereren een naaipatroon op maat voor je.
|
||||
designNew: Een nieuw ontwerp creëren
|
||||
designNewInfo: FreeSewing ontwerpen zijn bundeltjes JavaScript-code die patronen genereren. Je eigen ontwerpen maken is niet zo moeilijk en we hebben een gedetailleerde handleiding om je op weg te helpen.
|
||||
pluginNew: Een nieuwe plugin creëren
|
||||
pluginNewInfo: De functionaliteit van FreeSewing kan verder worden uitgebreid met plugins. Het maken van een plugin is eenvoudig en we hebben een gids die je van begin tot eind begeleidt.
|
||||
showcaseNew: Create a new showcase post
|
||||
showcaseNewInfo: If you would like to share something you (or someone else) made based on our designs, you can start a draft showcase post here.
|
||||
blogNew: Create a new blog post
|
||||
blogNewInfo: If you would like to write on the FreeSewing blog, you can start a draft blog post here.
|
||||
csetNew: Suggest a new curated measurements set
|
||||
csetNewInfo: We curate a collection of vetted measurments sets that we use to test patterns. You can suggest a measurements set here.
|
||||
opackNew: Suggest a new options pack
|
||||
opackNewInfo: We curate a collection of vetted option packs for each of our designs. You can suggest your options here.
|
||||
newPopular: Most popular
|
||||
newShare: Share / Show
|
||||
newDev: Design / Develop
|
||||
generateANewThing: "Genereer een nieuwe { thing }"
|
||||
website: Website
|
||||
linkedIdentities: Linked Identities
|
||||
websiteTitle: Do you have a website or other URL you'd like to add?
|
||||
platformTitle: Who are you on { platform }?
|
||||
platformWhy: We do not use this data in any way. This is only here so FreeSewing users can connect the dots across platforms.
|
||||
security: Security
|
||||
revealPassword: Reveal password
|
||||
hidePassword: Hide password
|
||||
#Design view
|
||||
designs: Designs
|
||||
code: Code
|
||||
aboutThing: About { thing }
|
||||
requiredMeasurements: Vereiste maten
|
||||
optionalMeasurements: Optionele maten
|
||||
designOptions: Design opties
|
||||
parts: Parts
|
||||
plugins: Plugins
|
||||
specifications: Specifications
|
||||
visitShowcase: Visit showcase post
|
||||
examples: Voorbeelden
|
||||
noExamples: We currently do not have any examples for this design
|
||||
noExamplesMsg: We rely on the FreeSewing community to submit examples in our showcase posts.
|
||||
ownPublicPattern: This is the public view on one of your own patterns. For more options, access the private view.
|
||||
ownPrivatePattern: This is the private view on your pattern. The public view will work for you even when the pattern is private. It will only work for others when the pattern is public.
|
||||
privateView: Private view
|
||||
publicView: Public view
|
3
sites/shared/components/account/overview.mjs
Normal file
3
sites/shared/components/account/overview.mjs
Normal file
|
@ -0,0 +1,3 @@
|
|||
import { AccountLinks } from './links.mjs'
|
||||
|
||||
export const AccountOverview = ({ app }) => <AccountLinks app={app} />
|
66
sites/shared/components/account/password.mjs
Normal file
66
sites/shared/components/account/password.mjs
Normal file
|
@ -0,0 +1,66 @@
|
|||
// Dependencies
|
||||
import { useState, useContext } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
// Context
|
||||
import { LoadingStatusContext } from 'shared/context/loading-status-context.mjs'
|
||||
// Hooks
|
||||
import { useAccount } from 'shared/hooks/use-account.mjs'
|
||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
// Components
|
||||
import Link from 'next/link'
|
||||
import { BackToAccountButton } from './shared.mjs'
|
||||
import { SaveSettingsButton } from 'shared/components/buttons/save-settings-button.mjs'
|
||||
import { Popout } from 'shared/components/popout/index.mjs'
|
||||
import { RightIcon } from 'shared/components/icons.mjs'
|
||||
import { PasswordInput } from 'shared/components/inputs.mjs'
|
||||
import { DynamicMdx } from 'shared/components/mdx/dynamic.mjs'
|
||||
|
||||
export const ns = ['account', 'status']
|
||||
|
||||
export const PasswordSettings = ({ welcome = false }) => {
|
||||
// Hooks
|
||||
const { account, setAccount } = useAccount()
|
||||
const backend = useBackend()
|
||||
const { t, i18n } = useTranslation(ns)
|
||||
const { setLoadingStatus } = useContext(LoadingStatusContext)
|
||||
|
||||
// State
|
||||
const [password, setPassword] = useState('')
|
||||
|
||||
// Helper method to save password to account
|
||||
const save = async () => {
|
||||
setLoadingStatus([true, 'processingUpdate'])
|
||||
const result = await backend.updateAccount({ password })
|
||||
if (result.success) {
|
||||
setAccount(result.data.account)
|
||||
setLoadingStatus([true, 'settingsSaved', true, true])
|
||||
} else setLoadingStatus([true, 'backendError', true, false])
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-xl">
|
||||
<PasswordInput
|
||||
id="account-password"
|
||||
label={t('passwordTitle')}
|
||||
current={password}
|
||||
update={setPassword}
|
||||
valid={(val) => val.length > 0}
|
||||
placeholder={t('passwordTitle')}
|
||||
docs={<DynamicMdx language={i18n.language} slug={`docs/site/account/password`} />}
|
||||
/>
|
||||
<SaveSettingsButton btnProps={{ onClick: save, disabled: password.length < 4 }} />
|
||||
{!welcome && <BackToAccountButton />}
|
||||
{!account.mfaEnabled && (
|
||||
<Popout tip>
|
||||
<h5>{t('mfaTipTitle')}</h5>
|
||||
<p>{t('mfaTipMsg')}</p>
|
||||
<p className="text-right m-0 pt-0">
|
||||
<Link className="btn btn-secondary btn-accent" href="/account/mfa">
|
||||
{t('mfa')} <RightIcon className="h-6 w-6 ml-2" />
|
||||
</Link>
|
||||
</p>
|
||||
</Popout>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
645
sites/shared/components/account/patterns.mjs
Normal file
645
sites/shared/components/account/patterns.mjs
Normal file
|
@ -0,0 +1,645 @@
|
|||
// Dependencies
|
||||
import { useState, useEffect, useContext } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import {
|
||||
capitalize,
|
||||
shortDate,
|
||||
cloudflareImageUrl,
|
||||
horFlexClasses,
|
||||
newPatternUrl,
|
||||
} from 'shared/utils.mjs'
|
||||
import { freeSewingConfig as conf, controlLevels } from 'shared/config/freesewing.config.mjs'
|
||||
// Context
|
||||
import { LoadingStatusContext } from 'shared/context/loading-status-context.mjs'
|
||||
// Hooks
|
||||
import { useAccount } from 'shared/hooks/use-account.mjs'
|
||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
import { useRouter } from 'next/router'
|
||||
// Context
|
||||
import { ModalContext } from 'shared/context/modal-context.mjs'
|
||||
// Components
|
||||
import { PageLink, Link, AnchorLink } from 'shared/components/link.mjs'
|
||||
import { BackToAccountButton } from './shared.mjs'
|
||||
import { Popout } from 'shared/components/popout/index.mjs'
|
||||
import {
|
||||
StringInput,
|
||||
MarkdownInput,
|
||||
PassiveImageInput,
|
||||
ListInput,
|
||||
} from 'shared/components/inputs.mjs'
|
||||
import {
|
||||
OkIcon,
|
||||
NoIcon,
|
||||
TrashIcon,
|
||||
PlusIcon,
|
||||
CameraIcon,
|
||||
EditIcon,
|
||||
ResetIcon,
|
||||
UploadIcon,
|
||||
FreeSewingIcon,
|
||||
CloneIcon,
|
||||
BoolYesIcon,
|
||||
BoolNoIcon,
|
||||
LockIcon,
|
||||
PatternIcon,
|
||||
} from 'shared/components/icons.mjs'
|
||||
import { DisplayRow } from './shared.mjs'
|
||||
import { ModalWrapper } from 'shared/components/wrappers/modal.mjs'
|
||||
import { Mdx } from 'shared/components/mdx/dynamic.mjs'
|
||||
import Timeago from 'react-timeago'
|
||||
import { TableWrapper } from 'shared/components/wrappers/table.mjs'
|
||||
import { PatternReactPreview } from 'shared/components/pattern/preview.mjs'
|
||||
import { Lightbox } from 'shared/components/lightbox.mjs'
|
||||
import { DynamicMdx } from 'shared/components/mdx/dynamic.mjs'
|
||||
|
||||
export const ns = ['account', 'patterns', 'status']
|
||||
|
||||
export const ShowPattern = ({ id }) => {
|
||||
// Hooks
|
||||
const { setLoadingStatus } = useContext(LoadingStatusContext)
|
||||
const backend = useBackend()
|
||||
const { t, i18n } = useTranslation(ns)
|
||||
|
||||
// State
|
||||
const [pattern, setPattern] = useState()
|
||||
const [isOwn, setIsOwn] = useState(false)
|
||||
|
||||
// Effect
|
||||
useEffect(() => {
|
||||
const getPattern = async () => {
|
||||
setLoadingStatus([true, t('backendLoadingStarted')])
|
||||
let result
|
||||
try {
|
||||
result = await backend.getPattern(id)
|
||||
console.log('first attempt', result)
|
||||
if (result.success) {
|
||||
setPattern(result.data.pattern)
|
||||
setIsOwn(true)
|
||||
setLoadingStatus([true, 'backendLoadingCompleted', true, true])
|
||||
} else {
|
||||
result = await backend.getPublicPattern(id)
|
||||
if (result.success) {
|
||||
setPattern({ ...result.data, public: true })
|
||||
setLoadingStatus([true, 'backendLoadingCompleted', true, true])
|
||||
} else setLoadingStatus([true, 'backendError', true, false])
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
setLoadingStatus([true, 'backendError', true, false])
|
||||
}
|
||||
}
|
||||
if (id) getPattern()
|
||||
}, [id])
|
||||
|
||||
if (!pattern) return <p>loading</p>
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-row flex-wrap gap-4 max-w-7xl w-full">
|
||||
<div className="max-w-lg grow w-full">
|
||||
<Lightbox buttonClasses="w-full" boxClasses="" modalProps={{ fullWidth: 1 }}>
|
||||
<PatternReactPreview {...pattern} />
|
||||
</Lightbox>
|
||||
</div>
|
||||
<div className="w-full md:max-w-lg">
|
||||
<DisplayRow title={t('name')}>{pattern.name}</DisplayRow>
|
||||
<DisplayRow title="#">{pattern.id}</DisplayRow>
|
||||
<DisplayRow title={t('account:publicView')}>
|
||||
<PageLink href={`/patterns/${pattern.id}`} txt={`/patterns/${pattern.id}`} />
|
||||
</DisplayRow>
|
||||
<DisplayRow title={t('account:privateView')}>
|
||||
<PageLink
|
||||
href={`/account/patterns/${pattern.id}`}
|
||||
txt={`/account/patterns/${pattern.id}`}
|
||||
/>
|
||||
</DisplayRow>
|
||||
<DisplayRow title={t('created')}>
|
||||
<Timeago date={pattern.createdAt} />
|
||||
<span className="px-2 opacity-50">|</span>
|
||||
{shortDate(i18n.language, pattern.createdAt, false)}
|
||||
</DisplayRow>
|
||||
<DisplayRow title={t('updated')}>
|
||||
<Timeago date={pattern.createdAt} />
|
||||
<span className="px-2 opacity-50">|</span>
|
||||
{shortDate(i18n.language, pattern.updatedAt, false)}
|
||||
</DisplayRow>
|
||||
<DisplayRow title={t('public')}>
|
||||
{pattern.public ? <BoolYesIcon /> : <BoolNoIcon />}
|
||||
</DisplayRow>
|
||||
<DisplayRow title={t('img')}>
|
||||
<Lightbox buttonClasses="mask mask-squircle w-36 h-35">
|
||||
<img src={cloudflareImageUrl({ id: pattern.img, variant: 'sq500' })} />
|
||||
</Lightbox>
|
||||
</DisplayRow>
|
||||
<Link
|
||||
href={newPatternUrl({ design: pattern.design, settings: pattern.settings })}
|
||||
className={`btn btn-primary ${horFlexClasses}`}
|
||||
>
|
||||
<CloneIcon /> {t('clonePattern')}
|
||||
</Link>
|
||||
{isOwn ? (
|
||||
<>
|
||||
<Popout tip noP>
|
||||
<p>{t('account:ownPublicPattern')}</p>
|
||||
<Link
|
||||
href={`/account/patterns/${pattern.id}/`}
|
||||
className={`btn btn-secondary ${horFlexClasses} mt-2`}
|
||||
>
|
||||
<LockIcon /> {t('account:privateView')}
|
||||
</Link>
|
||||
</Popout>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<h2>{t('account:notes')}</h2>
|
||||
{isOwn ? 'is own' : 'is not own'}
|
||||
<Mdx md={pattern.notes} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const Pattern = ({ id }) => {
|
||||
// Hooks
|
||||
const { account, control } = useAccount()
|
||||
const { setLoadingStatus } = useContext(LoadingStatusContext)
|
||||
const backend = useBackend()
|
||||
const { t, i18n } = useTranslation(ns)
|
||||
|
||||
// Context
|
||||
const { setModal } = useContext(ModalContext)
|
||||
|
||||
const [edit, setEdit] = useState(false)
|
||||
const [pattern, setPattern] = useState()
|
||||
// Set fields for editing
|
||||
const [name, setName] = useState(pattern?.name)
|
||||
const [image, setImage] = useState(pattern?.image)
|
||||
const [isPublic, setIsPublic] = useState(pattern?.public ? true : false)
|
||||
const [notes, setNotes] = useState(pattern?.notes || '')
|
||||
|
||||
// Effect
|
||||
useEffect(() => {
|
||||
const getPattern = async () => {
|
||||
setLoadingStatus([true, t('backendLoadingStarted')])
|
||||
const result = await backend.getPattern(id)
|
||||
if (result.success) {
|
||||
setPattern(result.data.pattern)
|
||||
setName(result.data.pattern.name)
|
||||
setImage(result.data.pattern.image)
|
||||
setIsPublic(result.data.pattern.public ? true : false)
|
||||
setNotes(result.data.pattern.notes)
|
||||
setLoadingStatus([true, 'backendLoadingCompleted', true, true])
|
||||
} else setLoadingStatus([true, 'backendError', true, false])
|
||||
}
|
||||
if (id) getPattern()
|
||||
}, [id])
|
||||
|
||||
const save = async () => {
|
||||
setLoadingStatus([true, 'gatheringInfo'])
|
||||
// Compile data
|
||||
const data = {}
|
||||
if (name || name !== pattern.name) data.name = name
|
||||
if (image || image !== pattern.image) data.img = image
|
||||
if (notes || notes !== pattern.notes) data.notes = notes
|
||||
if ([true, false].includes(isPublic) && isPublic !== pattern.public) data.public = isPublic
|
||||
setLoadingStatus([true, 'savingPattern'])
|
||||
const result = await backend.updatePattern(pattern.id, data)
|
||||
if (result.success) {
|
||||
setPattern(result.data.pattern)
|
||||
setEdit(false)
|
||||
setLoadingStatus([true, 'nailedIt', true, true])
|
||||
} else setLoadingStatus([true, 'backendError', true, false])
|
||||
}
|
||||
|
||||
if (!pattern) return null
|
||||
|
||||
const heading = (
|
||||
<>
|
||||
<div className="flex flex-wrap md:flex-nowrap flex-row gap-2 w-full">
|
||||
<div className="w-full md:w-96 shrink-0">
|
||||
<PatternCard pattern={pattern} size="md" />
|
||||
</div>
|
||||
<div className="flex flex-col justify-end gap-2 mb-2 grow">
|
||||
{account.control > 3 && pattern?.public ? (
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
<a
|
||||
className="badge badge-secondary font-bold badge-lg"
|
||||
href={`${conf.backend}/patterns/${pattern.id}.json`}
|
||||
>
|
||||
JSON
|
||||
</a>
|
||||
<a
|
||||
className="badge badge-success font-bold badge-lg"
|
||||
href={`${conf.backend}/patterns/${pattern.id}.yaml`}
|
||||
>
|
||||
YAML
|
||||
</a>
|
||||
</div>
|
||||
) : (
|
||||
<span></span>
|
||||
)}
|
||||
<button
|
||||
onClick={() =>
|
||||
setModal(
|
||||
<ModalWrapper flex="col" justify="top lg:justify-center" slideFrom="right">
|
||||
<img src={cloudflareImageUrl({ type: 'public', id: pattern.img })} />
|
||||
</ModalWrapper>
|
||||
)
|
||||
}
|
||||
className={`btn btn-secondary btn-outline ${horFlexClasses}`}
|
||||
>
|
||||
<CameraIcon />
|
||||
{t('showImage')}
|
||||
</button>
|
||||
{pattern.userId === account.id && (
|
||||
<>
|
||||
{edit ? (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setEdit(false)}
|
||||
className={`btn btn-primary btn-outline ${horFlexClasses}`}
|
||||
>
|
||||
<ResetIcon />
|
||||
{t('cancel')}
|
||||
</button>
|
||||
<button onClick={save} className={`btn btn-primary ${horFlexClasses}`}>
|
||||
<UploadIcon />
|
||||
{t('saveThing', { thing: t('account:pattern') })}
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Link
|
||||
href={`/account/patterns/${pattern.design}/${pattern.id}/edit`}
|
||||
className={`btn btn-primary btn-outline ${horFlexClasses}`}
|
||||
>
|
||||
<FreeSewingIcon /> {t('updatePattern')}
|
||||
</Link>
|
||||
<Link
|
||||
href={newPatternUrl({ design: pattern.design, settings: pattern.settings })}
|
||||
className={`btn btn-primary btn-outline ${horFlexClasses}`}
|
||||
>
|
||||
<CloneIcon /> {t('clonePattern')}
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => setEdit(true)}
|
||||
className={`btn btn-primary ${horFlexClasses}`}
|
||||
>
|
||||
<EditIcon /> {t('editThing', { thing: t('account:patternMetadata') })}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row flex-wrap gap-4 text-sm items-center justify-between mb-2"></div>
|
||||
</>
|
||||
)
|
||||
|
||||
if (!edit)
|
||||
return (
|
||||
<div className="max-w-2xl">
|
||||
{heading}
|
||||
<DisplayRow title={t('name')}>{pattern.name}</DisplayRow>
|
||||
{control >= controlLevels.sets.notes && (
|
||||
<DisplayRow title={t('notes')}>
|
||||
<Mdx md={pattern.notes} />
|
||||
</DisplayRow>
|
||||
)}
|
||||
{control >= controlLevels.patterns.public && (
|
||||
<>
|
||||
<DisplayRow title={t('public')}>
|
||||
{pattern.public ? <BoolYesIcon /> : <BoolNoIcon />}
|
||||
</DisplayRow>
|
||||
{pattern.public && (
|
||||
<DisplayRow title={t('permalink')}>
|
||||
<PageLink href={`/patterns/${pattern.id}`} txt={`/patterns/${pattern.id}`} />
|
||||
</DisplayRow>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{control >= controlLevels.sets.createdAt && (
|
||||
<DisplayRow title={t('created')}>
|
||||
<Timeago date={pattern.createdAt} />
|
||||
<span className="px-2 opacity-50">|</span>
|
||||
{shortDate(i18n.language, pattern.createdAt, false)}
|
||||
</DisplayRow>
|
||||
)}
|
||||
{control >= controlLevels.patterns.updatedAt && (
|
||||
<DisplayRow title={t('updated')}>
|
||||
<Timeago date={pattern.updatedAt} />
|
||||
<span className="px-2 opacity-50">|</span>
|
||||
{shortDate(i18n.language, pattern.updatedAt, false)}
|
||||
</DisplayRow>
|
||||
)}
|
||||
{control >= controlLevels.patterns.id && (
|
||||
<DisplayRow title={t('id')}>{pattern.id}</DisplayRow>
|
||||
)}
|
||||
<Popout tip noP>
|
||||
<p>{t('account:ownPrivatePattern')}</p>
|
||||
<Link className={`btn btn-secondary ${horFlexClasses}`} href={`/patterns/${pattern.id}`}>
|
||||
<PatternIcon />
|
||||
{t('account:publicView')}
|
||||
</Link>
|
||||
</Popout>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl">
|
||||
{heading}
|
||||
<ul className="list list-disc list-inside ml-4">
|
||||
<li>
|
||||
<AnchorLink id="name" txt={t('name')} />
|
||||
</li>
|
||||
{account.control >= conf.account.sets.img ? (
|
||||
<li>
|
||||
<AnchorLink id="image" txt={t('image')} />
|
||||
</li>
|
||||
) : null}
|
||||
{['public', 'units', 'notes'].map((id) =>
|
||||
account.control >= conf.account.sets[id] ? (
|
||||
<li key={id}>
|
||||
<AnchorLink id="units" txt={t(id)} />
|
||||
</li>
|
||||
) : null
|
||||
)}
|
||||
</ul>
|
||||
|
||||
{/* Name is always shown */}
|
||||
<span id="name"></span>
|
||||
<StringInput
|
||||
id="pattern-name"
|
||||
label={t('name')}
|
||||
update={setName}
|
||||
current={name}
|
||||
original={pattern.name}
|
||||
placeholder="Maurits Cornelis Escher"
|
||||
valid={(val) => val && val.length > 0}
|
||||
docs={<DynamicMdx language={i18n.language} slug="docs/site/patterns/name" />}
|
||||
/>
|
||||
|
||||
{/* img: Control level determines whether or not to show this */}
|
||||
<span id="image"></span>
|
||||
{account.control >= conf.account.sets.img ? (
|
||||
<PassiveImageInput
|
||||
id="pattern-img"
|
||||
label={t('image')}
|
||||
update={setImage}
|
||||
current={image}
|
||||
valid={(val) => val.length > 0}
|
||||
docs={<DynamicMdx language={i18n.language} slug="docs/site/patterns/image" />}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{/* public: Control level determines whether or not to show this */}
|
||||
<span id="public"></span>
|
||||
{account.control >= conf.account.patterns.public ? (
|
||||
<ListInput
|
||||
id="pattern-public"
|
||||
label={t('public')}
|
||||
update={setIsPublic}
|
||||
list={[
|
||||
{
|
||||
val: true,
|
||||
label: (
|
||||
<div className="flex flex-row items-center flex-wrap justify-between w-full">
|
||||
<span>{t('publicPattern')}</span>
|
||||
<OkIcon
|
||||
className="w-8 h-8 text-success bg-base-100 rounded-full p-1"
|
||||
stroke={4}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
desc: t('publicPatternDesc'),
|
||||
},
|
||||
{
|
||||
val: false,
|
||||
label: (
|
||||
<div className="flex flex-row items-center flex-wrap justify-between w-full">
|
||||
<span>{t('privatePattern')}</span>
|
||||
<NoIcon className="w-8 h-8 text-error bg-base-100 rounded-full p-1" stroke={3} />
|
||||
</div>
|
||||
),
|
||||
desc: t('privatePatternDesc'),
|
||||
},
|
||||
]}
|
||||
current={isPublic}
|
||||
docs={<DynamicMdx language={i18n.language} slug="docs/site/patterns/public" />}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{/* notes: Control level determines whether or not to show this */}
|
||||
<span id="notes"></span>
|
||||
{account.control >= conf.account.patterns.notes ? (
|
||||
<MarkdownInput
|
||||
id="pattern-notes"
|
||||
label={t('notes')}
|
||||
update={setNotes}
|
||||
current={notes}
|
||||
placeholder={t('mdSupport')}
|
||||
docs={<DynamicMdx language={i18n.language} slug="docs/site/patterns/notes" />}
|
||||
/>
|
||||
) : null}
|
||||
<button
|
||||
onClick={save}
|
||||
className="btn btn-primary btn-lg flex flex-row items-center gap-4 mx-auto mt-8"
|
||||
>
|
||||
<UploadIcon />
|
||||
{t('saveThing', { thing: t('account:pattern') })}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const PatternCard = ({
|
||||
pattern,
|
||||
href = false,
|
||||
onClick = false,
|
||||
useA = false,
|
||||
size = 'md',
|
||||
}) => {
|
||||
const sizes = {
|
||||
lg: 96,
|
||||
md: 52,
|
||||
sm: 36,
|
||||
xs: 20,
|
||||
}
|
||||
const s = sizes[size]
|
||||
|
||||
const wrapperProps = {
|
||||
className: `bg-base-300 w-full mb-2 mx-auto flex flex-col items-start text-center justify-center rounded shadow py-4 h-${s} w-${s}`,
|
||||
style: {
|
||||
backgroundImage: `url(${cloudflareImageUrl({ type: 'w1000', id: pattern.img })})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundPosition: '50%',
|
||||
},
|
||||
}
|
||||
if (pattern.img === 'default-avatar') wrapperProps.style.backgroundPosition = 'bottom right'
|
||||
|
||||
const inner = null
|
||||
|
||||
// Is it a button with an onClick handler?
|
||||
if (onClick)
|
||||
return (
|
||||
<button {...wrapperProps} onClick={onClick}>
|
||||
{inner}
|
||||
</button>
|
||||
)
|
||||
|
||||
// Returns a link to an internal page
|
||||
if (href && !useA)
|
||||
return (
|
||||
<Link {...wrapperProps} href={href}>
|
||||
{inner}
|
||||
</Link>
|
||||
)
|
||||
|
||||
// Returns a link to an external page
|
||||
if (href && useA)
|
||||
return (
|
||||
<a {...wrapperProps} href={href}>
|
||||
{inner}
|
||||
</a>
|
||||
)
|
||||
|
||||
// Returns a div
|
||||
return <div {...wrapperProps}>{inner}</div>
|
||||
}
|
||||
|
||||
// Component for the account/patterns page
|
||||
export const Patterns = () => {
|
||||
const router = useRouter()
|
||||
const { locale } = router
|
||||
|
||||
// Hooks
|
||||
const backend = useBackend()
|
||||
const { t } = useTranslation(ns)
|
||||
const { setLoadingStatus, LoadingProgress } = useContext(LoadingStatusContext)
|
||||
|
||||
// State
|
||||
const [patterns, setPatterns] = useState([])
|
||||
const [selected, setSelected] = useState({})
|
||||
const [refresh, setRefresh] = useState(0)
|
||||
|
||||
// Helper var to see how many are selected
|
||||
const selCount = Object.keys(selected).length
|
||||
|
||||
// Effects
|
||||
useEffect(() => {
|
||||
const getPatterns = async () => {
|
||||
const result = await backend.getPatterns()
|
||||
if (result.success) setPatterns(result.data.patterns)
|
||||
}
|
||||
getPatterns()
|
||||
}, [refresh])
|
||||
|
||||
// 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 === patterns.length) setSelected({})
|
||||
else {
|
||||
const newSelected = {}
|
||||
for (const pattern of patterns) newSelected[pattern.id] = 1
|
||||
setSelected(newSelected)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to delete one or more patterns
|
||||
const removeSelectedPatterns = async () => {
|
||||
let i = 0
|
||||
for (const pattern in selected) {
|
||||
i++
|
||||
await backend.removePattern(pattern)
|
||||
setLoadingStatus([
|
||||
true,
|
||||
<LoadingProgress val={i} max={selCount} msg={t('removingPatterns')} key="linter" />,
|
||||
])
|
||||
}
|
||||
setSelected({})
|
||||
setRefresh(refresh + 1)
|
||||
setLoadingStatus([true, 'nailedIt', true, true])
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl xl:pl-4">
|
||||
<p className="text-center md:text-right">
|
||||
<Link className="btn btn-primary capitalize w-full md:w-auto" href="/new/pattern">
|
||||
<PlusIcon />
|
||||
{t('patternNew')}
|
||||
</Link>
|
||||
</p>
|
||||
{selCount ? (
|
||||
<button className="btn btn-error" onClick={removeSelectedPatterns}>
|
||||
<TrashIcon /> {selCount} {t('patterns')}
|
||||
</button>
|
||||
) : null}
|
||||
<TableWrapper>
|
||||
<table className="table table-auto">
|
||||
<thead className="border border-base-300 border-b-2 border-t-0 border-x-0">
|
||||
<tr className="">
|
||||
<th className="">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="checkbox checkbox-secondary"
|
||||
onClick={toggleSelectAll}
|
||||
checked={patterns.length === selCount}
|
||||
/>
|
||||
</th>
|
||||
<th>#</th>
|
||||
<th>{t('account:img')}</th>
|
||||
<th>{t('account:name')}</th>
|
||||
<th>{t('account:design')}</th>
|
||||
<th>{t('account:createdAt')}</th>
|
||||
<th>{t('account:public')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{patterns.map((pattern, i) => (
|
||||
<tr key={i}>
|
||||
<td className="text-base font-medium">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selected[pattern.id] ? true : false}
|
||||
className="checkbox checkbox-secondary"
|
||||
onClick={() => toggleSelect(pattern.id)}
|
||||
/>
|
||||
</td>
|
||||
<td className="text-base font-medium">{pattern.id}</td>
|
||||
<td className="text-base font-medium">
|
||||
<PatternCard
|
||||
href={`/account/patterns/${pattern.id}`}
|
||||
pattern={pattern}
|
||||
size="xs"
|
||||
/>
|
||||
</td>
|
||||
<td className="text-base font-medium">
|
||||
<PageLink href={`/account/patterns/${pattern.id}`} txt={pattern.name} />
|
||||
</td>
|
||||
<td className="text-base font-medium">
|
||||
<PageLink href={`/designs/${pattern.design}`} txt={capitalize(pattern.design)} />
|
||||
</td>
|
||||
<td className="text-base font-medium">
|
||||
{shortDate(locale, pattern.createdAt, false)}
|
||||
</td>
|
||||
<td className="text-base font-medium">
|
||||
{pattern.public ? <BoolYesIcon /> : <BoolNoIcon />}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</TableWrapper>
|
||||
<BackToAccountButton />
|
||||
</div>
|
||||
)
|
||||
}
|
55
sites/shared/components/account/platform.mjs
Normal file
55
sites/shared/components/account/platform.mjs
Normal file
|
@ -0,0 +1,55 @@
|
|||
// Context
|
||||
import { LoadingStatusContext } from 'shared/context/loading-status-context.mjs'
|
||||
// Hooks
|
||||
import { useState, useContext } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { useAccount } from 'shared/hooks/use-account.mjs'
|
||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
// Components
|
||||
import { BackToAccountButton } from './shared.mjs'
|
||||
import { SaveSettingsButton } from 'shared/components/buttons/save-settings-button.mjs'
|
||||
import { StringInput } from 'shared/components/inputs.mjs'
|
||||
import { DynamicMdx } from 'shared/components/mdx/dynamic.mjs'
|
||||
|
||||
export const ns = ['account', 'status']
|
||||
|
||||
export const PlatformSettings = ({ platform }) => {
|
||||
// Hooks
|
||||
const { account, setAccount } = useAccount()
|
||||
const backend = useBackend()
|
||||
const { t, i18n } = useTranslation(ns)
|
||||
const { setLoadingStatus } = useContext(LoadingStatusContext)
|
||||
|
||||
// State
|
||||
const [platformId, setPlatformId] = useState(account.data[platform] || '')
|
||||
|
||||
// Helper method to save changes
|
||||
const save = async () => {
|
||||
setLoadingStatus([true, 'processingUpdate'])
|
||||
const data = { data: {} }
|
||||
data.data[platform] = platformId
|
||||
const result = await backend.updateAccount(data)
|
||||
if (result.success) {
|
||||
setAccount(result.data.account)
|
||||
setLoadingStatus([true, 'settingsSaved', true, true])
|
||||
} else setLoadingStatus([true, 'backendError', true, false])
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-xl">
|
||||
<StringInput
|
||||
id={`account-${platform}`}
|
||||
label={t(platform === 'website' ? 'account:websiteTitle' : 'account:platformTitle', {
|
||||
platform: platform,
|
||||
})}
|
||||
current={platformId}
|
||||
update={setPlatformId}
|
||||
valid={(val) => val.length > 0}
|
||||
placeholder={'joostdecock'}
|
||||
docs={<DynamicMdx language={i18n.language} slug={`docs/site/account/platform`} />}
|
||||
/>
|
||||
<SaveSettingsButton btnProps={{ onClick: save }} />
|
||||
<BackToAccountButton />
|
||||
</div>
|
||||
)
|
||||
}
|
54
sites/shared/components/account/profile.mjs
Normal file
54
sites/shared/components/account/profile.mjs
Normal file
|
@ -0,0 +1,54 @@
|
|||
// Hooks
|
||||
import { useAccount } from 'shared/hooks/use-account.mjs'
|
||||
// Components
|
||||
import Link from 'next/link'
|
||||
import { Mdx } from 'shared/components/mdx/dynamic.mjs'
|
||||
import { HeartIcon } from 'shared/components/icons.mjs'
|
||||
import { ModalWrapper } from 'shared/components/wrappers/modal.mjs'
|
||||
|
||||
export const ns = ['account']
|
||||
|
||||
export const Avatar = ({ img, app = false }) => (
|
||||
<div className={`mask mask-squircle bg-neutral z-10 ${app ? 'w-24' : 'w-full'}`}>
|
||||
<img
|
||||
src={img}
|
||||
onClick={
|
||||
app
|
||||
? () =>
|
||||
app.setModal(
|
||||
<ModalWrapper app={app}>
|
||||
<Avatar img={img} />
|
||||
</ModalWrapper>
|
||||
)
|
||||
: null
|
||||
}
|
||||
className={app ? 'hover:cursor-zoom-in' : 'hover:cursor-zoom-out'}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
export const AccountProfile = ({ app }) => {
|
||||
const { account } = useAccount()
|
||||
|
||||
if (!account) return null
|
||||
|
||||
return (
|
||||
<div className="my-8">
|
||||
<div className="flex flex-row w-full justify-center">
|
||||
<Avatar img={account.img} app={app} />
|
||||
{!account.patron ? (
|
||||
<Link href="/patrons/join" className="z-20">
|
||||
<HeartIcon className="w-12 h-12 -ml-8 mt-2 stroke-base-100 fill-accent" stroke={1} />
|
||||
</Link>
|
||||
) : null}
|
||||
</div>
|
||||
<h2 className="text-center">{account.username}</h2>
|
||||
<div className="flex flex-row">
|
||||
<div className="avatar -mt-6 -ml-8 flex flex-row items-end"></div>
|
||||
</div>
|
||||
<div className="max-w-full truncate">
|
||||
<Mdx md={account.bio} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
40
sites/shared/components/account/reload.mjs
Normal file
40
sites/shared/components/account/reload.mjs
Normal file
|
@ -0,0 +1,40 @@
|
|||
// Context
|
||||
import { LoadingStatusContext } from 'shared/context/loading-status-context.mjs'
|
||||
// Hooks
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { useContext } from 'react'
|
||||
import { useAccount } from 'shared/hooks/use-account.mjs'
|
||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
// Components
|
||||
import { BackToAccountButton } from './shared.mjs'
|
||||
|
||||
export const ns = ['account', 'status']
|
||||
|
||||
export const ReloadAccount = ({ title = false }) => {
|
||||
// Hooks
|
||||
const { setAccount } = useAccount()
|
||||
const backend = useBackend()
|
||||
const { t } = useTranslation(ns)
|
||||
const { setLoadingStatus } = useContext(LoadingStatusContext)
|
||||
|
||||
// Helper method to reload account
|
||||
const reload = async () => {
|
||||
setLoadingStatus([true, 'processingUpdate'])
|
||||
const result = await backend.reloadAccount()
|
||||
if (result.success) {
|
||||
setAccount(result.data.account)
|
||||
setLoadingStatus([true, 'nailedIt', true, true])
|
||||
} else setLoadingStatus([true, 'backendError', true, false])
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-xl">
|
||||
{title ? <h2>{t('reloadMsg1')}</h2> : null}
|
||||
<p>{t('reloadMsg2')}</p>
|
||||
<button className="btn btn-primary capitalize w-full my-2" onClick={reload}>
|
||||
{t('reload')}
|
||||
</button>
|
||||
<BackToAccountButton />
|
||||
</div>
|
||||
)
|
||||
}
|
42
sites/shared/components/account/remove.mjs
Normal file
42
sites/shared/components/account/remove.mjs
Normal file
|
@ -0,0 +1,42 @@
|
|||
// Context
|
||||
import { LoadingStatusContext } from 'shared/context/loading-status-context.mjs'
|
||||
// Hooks
|
||||
import { useContext } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { useAccount } from 'shared/hooks/use-account.mjs'
|
||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
// Components
|
||||
import { BackToAccountButton } from './shared.mjs'
|
||||
import { Popout } from 'shared/components/popout/index.mjs'
|
||||
|
||||
export const ns = ['account', 'status']
|
||||
|
||||
export const RemoveAccount = () => {
|
||||
// Hooks
|
||||
const { signOut } = useAccount()
|
||||
const backend = useBackend()
|
||||
const { t } = useTranslation(ns)
|
||||
const { setLoadingStatus } = useContext(LoadingStatusContext)
|
||||
|
||||
// Helper method to export account
|
||||
const removeAccount = async () => {
|
||||
setLoadingStatus([true, 'processingUpdate'])
|
||||
const result = await backend.removeAccount()
|
||||
if (result.success) {
|
||||
setLoadingStatus([true, 'nailedIt', true, true])
|
||||
signOut()
|
||||
} else setLoadingStatus([true, 'backendError', true, false])
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-xl">
|
||||
<Popout warning>
|
||||
<h3>{t('noWayBack')}</h3>
|
||||
<button className="btn btn-error capitalize w-full my-2" onClick={removeAccount}>
|
||||
{t('remove')}
|
||||
</button>
|
||||
</Popout>
|
||||
<BackToAccountButton />
|
||||
</div>
|
||||
)
|
||||
}
|
43
sites/shared/components/account/restrict.mjs
Normal file
43
sites/shared/components/account/restrict.mjs
Normal file
|
@ -0,0 +1,43 @@
|
|||
// Context
|
||||
import { LoadingStatusContext } from 'shared/context/loading-status-context.mjs'
|
||||
// Hooks
|
||||
import { useContext } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { useAccount } from 'shared/hooks/use-account.mjs'
|
||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
// Components
|
||||
import { BackToAccountButton } from './shared.mjs'
|
||||
import { Popout } from 'shared/components/popout/index.mjs'
|
||||
|
||||
export const ns = ['account', 'status']
|
||||
|
||||
export const RestrictAccount = () => {
|
||||
// Hooks
|
||||
const { signOut } = useAccount()
|
||||
const backend = useBackend()
|
||||
const { t } = useTranslation(ns)
|
||||
const { setLoadingStatus } = useContext(LoadingStatusContext)
|
||||
|
||||
// Helper method to export account
|
||||
const restrictAccount = async () => {
|
||||
setLoadingStatus([true, 'processingUpdate'])
|
||||
const result = await backend.restrictAccount()
|
||||
if (result.success) {
|
||||
setLoadingStatus([true, 'nailedIt', true, true])
|
||||
signOut()
|
||||
} else setLoadingStatus([true, 'backendError', true, false])
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-xl">
|
||||
<Popout warning>
|
||||
<h5>{t('proceedWithCaution')}</h5>
|
||||
<p className="text-lg">{t('restrictWarning')}</p>
|
||||
<button className="btn btn-error capitalize w-full my-2" onClick={restrictAccount}>
|
||||
{t('restrict')}
|
||||
</button>
|
||||
</Popout>
|
||||
<BackToAccountButton />
|
||||
</div>
|
||||
)
|
||||
}
|
28
sites/shared/components/account/role.mjs
Normal file
28
sites/shared/components/account/role.mjs
Normal file
|
@ -0,0 +1,28 @@
|
|||
export const ns = ['roles']
|
||||
|
||||
const colors = {
|
||||
user: 'primary',
|
||||
curator: 'secondary',
|
||||
bughunter: 'accent',
|
||||
support: 'warning',
|
||||
admin: 'error',
|
||||
}
|
||||
|
||||
export const AccountRole = ({ role }) => {
|
||||
const color = colors[role]
|
||||
|
||||
return (
|
||||
<span className={``}>
|
||||
<span
|
||||
className={`text-xs uppercase bg-${color} rounded-l-lg pl-1 font-bold text-base-100 border border-2 border-solid border-${color}`}
|
||||
>
|
||||
role
|
||||
</span>
|
||||
<span
|
||||
className={`text-xs uppercase bg-base-100 text-${color} rounded-r-lg px-1 font-bold border border-2 border-solid border-${color}`}
|
||||
>
|
||||
{role}
|
||||
</span>
|
||||
</span>
|
||||
)
|
||||
}
|
1113
sites/shared/components/account/sets.mjs
Normal file
1113
sites/shared/components/account/sets.mjs
Normal file
File diff suppressed because it is too large
Load diff
136
sites/shared/components/account/shared.mjs
Normal file
136
sites/shared/components/account/shared.mjs
Normal file
|
@ -0,0 +1,136 @@
|
|||
// __SDEFILE__ - This file is a dependency for the stand-alone environment
|
||||
import { Spinner } from 'shared/components/spinner.mjs'
|
||||
import Link from 'next/link'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import {
|
||||
CogIcon,
|
||||
FingerprintIcon as ControlIcon,
|
||||
NewsletterIcon,
|
||||
UnitsIcon,
|
||||
CompareIcon,
|
||||
LabelIcon,
|
||||
BioIcon,
|
||||
UserIcon,
|
||||
LeftIcon,
|
||||
OkIcon,
|
||||
NoIcon,
|
||||
} from 'shared/components/icons.mjs'
|
||||
|
||||
const btnClasses = {
|
||||
dflt:
|
||||
'btn w-full mt-2 btn-secondary ' +
|
||||
'flex flex-row flex-nowrap items-center gap-4 py-4 h-auto ' +
|
||||
'border border-secondary justify-start text-left bg-opacity-30',
|
||||
active:
|
||||
'btn-ghost bg-secondary hover:bg-secondary ' + 'hover:bg-opacity-30 hover:border-secondary',
|
||||
inactive:
|
||||
'hover:bg-opacity-20 hover:bg-secondary btn-ghost ' +
|
||||
'border border-secondary hover:border hover:border-secondary',
|
||||
}
|
||||
|
||||
export const NumberBullet = ({ nr, color = 'secondary' }) => (
|
||||
<span
|
||||
className={`p-2 w-8 h-8 flex flex-col items-center justify-center shrink-0 rounded-full text-center p-0 py-2 bg-${color} text-${color}-content border-2 border-base-100`}
|
||||
>
|
||||
{nr}
|
||||
</span>
|
||||
)
|
||||
|
||||
export const BackToAccountButton = ({ loading = false }) => {
|
||||
const { t } = useTranslation(['account'])
|
||||
|
||||
return (
|
||||
<Link className={`btn ${loading ? 'btn-accent' : 'btn-secondary'} mt-4 pr-6`} href="/account">
|
||||
<span className="flex flex-row items-center gap-2">
|
||||
{loading ? <Spinner /> : <LeftIcon />}
|
||||
{t('yourAccount')}
|
||||
</span>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export const Choice = ({
|
||||
val,
|
||||
update,
|
||||
current,
|
||||
children,
|
||||
bool = false,
|
||||
boolChoices = {
|
||||
yes: <OkIcon className="w-6 h-6 text-success shrink-0" stroke={4} />,
|
||||
no: <NoIcon className="w-6 h-6 text-error shrink-0" stroke={3} />,
|
||||
},
|
||||
}) => {
|
||||
const active = val === current
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`${btnClasses.dflt} ${active ? btnClasses.active : btnClasses.inactive}`}
|
||||
onClick={() => update(val)}
|
||||
>
|
||||
{bool ? boolChoices[val] : <NumberBullet nr={val} />}
|
||||
<div className={`normal-case text-base-content`}>{children}</div>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export const DoneIcon = ({ href }) => (
|
||||
<Link href={`/welcome/${href}`} className="text-success hover:text-secondary">
|
||||
<TopicIcon href={href} />
|
||||
</Link>
|
||||
)
|
||||
export const TodoIcon = ({ href }) => (
|
||||
<Link href={`/welcome/${href}`} className="text-secondary w-6 h-6 opacity-50 hover:opacity-100">
|
||||
<TopicIcon href={href} />
|
||||
</Link>
|
||||
)
|
||||
|
||||
const TopicIcon = (props) => {
|
||||
const Icon =
|
||||
props.href === '' || props.href === 'control'
|
||||
? ControlIcon
|
||||
: icons[props.href]
|
||||
? icons[props.href]
|
||||
: CogIcon
|
||||
|
||||
return <Icon {...props} />
|
||||
}
|
||||
|
||||
const DoingIcon = ({ href }) => <TopicIcon href={href} className="w-6 h-6 text-base-content" />
|
||||
|
||||
export const Icons = ({ done = [], todo = [], current = '' }) => (
|
||||
<div className="m-auto flex flex-row items-center justify-center gap-2">
|
||||
{done.map((href) => (
|
||||
<DoneIcon href={href} key={href} />
|
||||
))}
|
||||
<DoingIcon href={current} />
|
||||
{todo.map((href) => (
|
||||
<TodoIcon href={href} key={href} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
||||
const icons = {
|
||||
newsletter: NewsletterIcon,
|
||||
units: UnitsIcon,
|
||||
compare: CompareIcon,
|
||||
username: LabelIcon,
|
||||
bio: BioIcon,
|
||||
img: UserIcon,
|
||||
}
|
||||
|
||||
export const welcomeSteps = {
|
||||
1: [''],
|
||||
2: ['', 'newsletter', 'units'],
|
||||
3: ['', 'newsletter', 'units', 'compare', 'username'],
|
||||
4: ['', 'newsletter', 'units', 'compare', 'username', 'bio', 'img'],
|
||||
5: [''],
|
||||
}
|
||||
|
||||
export const DisplayRow = ({ title, children, keyWidth = 'w-24' }) => (
|
||||
<div className="flex flex-row flex-wrap items-center lg:gap-4 my-2 w-full">
|
||||
<div className={`${keyWidth} text-left md:text-right block md:inline font-bold pr-4 shrink-0`}>
|
||||
{title}
|
||||
</div>
|
||||
<div className="grow">{children}</div>
|
||||
</div>
|
||||
)
|
22
sites/shared/components/account/status.mjs
Normal file
22
sites/shared/components/account/status.mjs
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { freeSewingConfig } from 'shared/config/freesewing.config.mjs'
|
||||
|
||||
export const ns = ['status']
|
||||
|
||||
export const AccountStatus = ({ status }) => {
|
||||
const { name, color } = freeSewingConfig.statuses[status]
|
||||
|
||||
return (
|
||||
<span className={``}>
|
||||
<span
|
||||
className={`text-xs uppercase bg-${color} rounded-l-lg pl-1 font-bold text-base-100 border border-2 border-solid border-${color}`}
|
||||
>
|
||||
status
|
||||
</span>
|
||||
<span
|
||||
className={`text-xs uppercase bg-base-100 text-${color} rounded-r-lg px-1 font-bold border border-2 border-solid border-${color}`}
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
</span>
|
||||
)
|
||||
}
|
281
sites/shared/components/account/uk.yaml
Normal file
281
sites/shared/components/account/uk.yaml
Normal file
|
@ -0,0 +1,281 @@
|
|||
account: Обліковий запис
|
||||
yourAccount: Ваш обліковий запис
|
||||
newPattern: Новий візерунок
|
||||
newSet: Створіть новий набір вимірювань
|
||||
links: Посилання
|
||||
info: Інформація
|
||||
settings: Налаштування
|
||||
data: Дані
|
||||
sets: Набори для вимірювання
|
||||
patterns: Викрійки
|
||||
actions: Дії
|
||||
created: Створено
|
||||
updated: Оновлено
|
||||
expires: Закінчується
|
||||
yourProfile: Ваш профіль
|
||||
yourPatterns: Ваші візерунки
|
||||
yourSets: Ваші набори вимірювань
|
||||
signOut: Вийти
|
||||
politeOhCrap: Ох вже ці скрипки.
|
||||
bio: Про мене
|
||||
email: Адреса електронної пошти
|
||||
img: Зображення
|
||||
username: Ім’я користувача
|
||||
compare: Порівняння наборів метрик
|
||||
consent: Згода та конфіденційність
|
||||
control: Користувацький досвід
|
||||
imperial: Одиниці
|
||||
units: Одиниці вимірювання
|
||||
apikeys: Ключі API
|
||||
newsletter: Підписка на розсилку новин
|
||||
password: Пароль
|
||||
newPasswordPlaceholder: Введіть новий пароль тут
|
||||
passwordPlaceholder: Введіть свій пароль тут
|
||||
mfa: Двофакторна автентифікація
|
||||
mfaTipTitle: Будь ласка, розгляньте можливість увімкнення двофакторної автентифікації
|
||||
mfaTipMsg: Ми не впроваджуємо політику паролів, але рекомендуємо вам увімкнути двофакторну автентифікацію, щоб захистити ваш обліковий запис FreeSewing.
|
||||
mfaEnabled: Увімкнено двофакторну автентифікацію
|
||||
mfaDisabled: Двофакторну автентифікацію вимкнено
|
||||
mfaSetup: Налаштування двофакторної автентифікації
|
||||
mfaAdd: Додайте FreeSewing до свого додатку Authenticator, відсканувавши QR-код вище.
|
||||
confirmWithPassword: Будь ласка, введіть пароль для підтвердження цієї дії
|
||||
confirmWithMfa: Будь ласка, введіть код з вашого додатку Authenticator, щоб підтвердити цю дію
|
||||
enableMfa: Увімкнути двофакторну автентифікацію
|
||||
disableMfa: Вимкнути двофакторну автентифікацію
|
||||
language: Мова
|
||||
developer: Розробник
|
||||
design: Дизайн
|
||||
patternMetadata: Pattern metadata
|
||||
clonePattern: Clone pattern
|
||||
updatePattern: Update pattern
|
||||
reload: Перезавантажити обліковий запис
|
||||
export: Експортуйте Ваші дані
|
||||
exportMsg: Click below to export your personal data
|
||||
exportNote: The EU's General Data Protection Regulation (GDPR) ensures your so-called right to data portability — the right to obtain and reuse your personal data for your own purposes, or across different services.
|
||||
exportDownload: "Your data was exported and is available for download at the following location:"
|
||||
review: Переглянути вашу згоду
|
||||
restrict: Обмежити обробку ваших даних
|
||||
disable: Вимкніть свій обліковий запис
|
||||
remove: Видалення облікового запису
|
||||
proceedWithCaution: Продовжуйте з обережністю
|
||||
restrictWarning: While no data will be removed, this will disable your account. Furthermore, you can not undo this on your own, but will have to contact support when you want to restore access to your account.
|
||||
noWayBack: There is no way back from this.
|
||||
mdSupport: Скористатися знижкою можна тут
|
||||
or: або
|
||||
continue: Продовжити
|
||||
save: Зберегти
|
||||
noThanks: Ні, дякую.
|
||||
areYouCertain: Ти впевнений?
|
||||
delete: Видалити
|
||||
#reload
|
||||
nailedIt: Зрозумів.
|
||||
gone: Пуф. Зникла.
|
||||
reloadMsg1: Дані, що зберігаються у вашому браузері, іноді можуть не синхронізуватися з даними, що зберігаються в нашому бекенді.
|
||||
reloadMsg2: Це дозволяє перезавантажити дані вашого облікового запису з бекенду. Це має той самий ефект, що й вихід з системи, а потім повторний вхід
|
||||
#bio
|
||||
bioTitle: Розкажіть людям трохи про себе
|
||||
bioPreview: Попередній перегляд біографії
|
||||
bioPlaceholder: Виготовляю одяг та взуття. Розробляю викрійки одягу. Пишу код. Керую [FreeSewing](http://freesewing.org)
|
||||
#compare
|
||||
compareTitle: Чи комфортно вам, коли порівнюються набори вимірювань?
|
||||
compareYes: Так, якщо це може мені допомогти
|
||||
compareYesd: |
|
||||
Час від часу ми показуватимемо, як ваш набір вимірювань порівнюється з іншими наборами вимірювань.
|
||||
Це дозволяє нам виявити потенційні проблеми у ваших вимірах або лекалах.
|
||||
compareNo: Ні, ніколи не порівнюйте
|
||||
compareNod: |
|
||||
Ми ніколи не будемо порівнювати ваш набір вимірів з іншими наборами вимірів.
|
||||
Це обмежить нашу здатність попередити вас про потенційні проблеми у ваших наборах вимірів або лекалах.
|
||||
#control
|
||||
showMore: Показати більше
|
||||
control1.t: Зробіть це якомога простіше
|
||||
control1.d: Приховує всі, окрім найважливіших особливостей.
|
||||
control2.t: Нехай це буде просто, але не надто просто
|
||||
control2.d: Приховує більшість функцій.
|
||||
control3.t: Поєднуйте простоту з потужністю
|
||||
control3.d: Розкриває більшість функцій, але не всі.
|
||||
control4.t: Дай мені всі повноваження, але бережи мене
|
||||
control4.d: Розкриває всі функції, зберігає поручні та чеки безпеки.
|
||||
control5.t: Геть з дороги!
|
||||
control5.d: Відкриває всі функції, прибирає всі поручні та перевірки безпеки.
|
||||
controlShowMore: Показати більше варіантів
|
||||
controlTitle: Якому користувацькому досвіду ви надаєте перевагу?
|
||||
#img
|
||||
imgTitle: Як щодо фотографії?
|
||||
imgDragAndDropImageHere: Перетягніть зображення сюди
|
||||
imgPasteUrlHere: Paste an image location (url) here
|
||||
imgSelectImage: Виберіть зображення
|
||||
#newsletter
|
||||
newsletterTitle: Хочете переглянути розсилку новин FreeSewing?
|
||||
newsletterYes: Так, я хочу отримувати розсилку новин
|
||||
newsletterYesd: Раз на 3 місяці ви отримуватимете від нас лист із чесним та корисним контентом. Ніякого відстеження, ніякої реклами, ніякої нісенітниці.
|
||||
newsletterNod: Ви завжди можете передумати пізніше. Але поки ви цього не зробите, ми не будемо надсилати вам жодних розсилок.
|
||||
#imperial
|
||||
metricUnits: Метричні одиниці (см)
|
||||
metricUnitsd: Виберіть це, якщо ви віддаєте перевагу сантиметрам, а не дюймам.
|
||||
imperialUnits: Імперські одиниці (дюйм)
|
||||
imperialUnitsd: Виберіть цей параметр, якщо ви віддаєте перевагу дюймам, а не сантиметрам.
|
||||
unitsTitle: Яким одиницям ви віддаєте перевагу?
|
||||
#username
|
||||
usernameTitle: Яке ім'я користувача ви б хотіли?
|
||||
usernameAvailable: Username is available
|
||||
usernameNotAvailable: Ім'я користувача недоступне
|
||||
#email
|
||||
emailTitle: Де ми можемо зв'язатися з вами, якщо у нас буде на це поважна причина (наприклад, якщо ви забули свій пароль)?
|
||||
oneMoreThing: І ще одна річ
|
||||
oneMomentPlease: Будь ласка, зачекайте
|
||||
emailChangeConfirmation: Ми надіслали електронного листа на вашу нову адресу, щоб підтвердити цю зміну.
|
||||
vagueError: Щось пішло не так, і ми не знаємо, як це виправити. Будь ласка, спробуйте ще раз або залучіть людину для допомоги.
|
||||
#github
|
||||
githubTitle: Link your GitHub identity
|
||||
githubWhy1: Enter your GitHub username and email here and we will use them when interacting with GitHub on your behalf.
|
||||
githubWhy2: Note that both your GitHub username and email is public info. This merely allows us to make a link between your FreeSewing account and GitHub account.
|
||||
githubWhy3: For example, when you report a problem on this website, we can mention you so you will receive notifications when there is an update. For this, your username is sufficient.
|
||||
githubWhy4: When you submit a showcase post or make changed to our content, we can credit those commits to you if we have both your username and the email address you use on GitHub.
|
||||
tooComplex: If all of this in confusing, you don't have to provide this info. It's an advanced feature.
|
||||
#languge
|
||||
languageTitle: Якій мові ви віддаєте перевагу?
|
||||
#password
|
||||
passwordTitle: Щось, що знаєш тільки ти.
|
||||
#api key
|
||||
newApikey: Згенеруйте новий ключ API
|
||||
keyNewInfo: Створіть новий ключ API для автоматичного підключення до бекенду FreeSewing.
|
||||
keyName: Ключова назва
|
||||
keyNameDesc: Унікальне ім'я для цього ключа API. Видиме лише вам.
|
||||
keyExpires: Термін дії ключа
|
||||
keyExpiresDesc: "Термін дії ключа закінчується:"
|
||||
keyLevel: Рівень доступу до ключа
|
||||
keyLevel0: Тільки автентифікація
|
||||
keyLevel1: Доступ до власних лекал і наборів мірок
|
||||
keyLevel2: Доступ до всіх даних вашого облікового запису
|
||||
keyLevel3: Запишіть доступ до власних лекал і наборів мірок
|
||||
keyLevel4: Доступ на запис до всіх даних вашого облікового запису
|
||||
keyLevel5: Доступ до лекал і наборів вимірів інших користувачів
|
||||
keyLevel6: Запис доступу до лекал і наборів вимірів інших користувачів
|
||||
keyLevel7: Доступ на запис до всіх даних облікових записів інших користувачів
|
||||
keyLevel8: Видавати себе за інших користувачів, повний доступ на запис до всіх даних
|
||||
cancel: Скасувати
|
||||
keySecretWarning: Це єдиний раз, коли ви можете побачити секретний ключ, обов'язково скопіюйте його.
|
||||
keyExpired: Термін дії цього ключа API закінчився
|
||||
deleteKeyWarning: Видалення ключа API не можна скасувати.
|
||||
keyDocsTitle: Refer to FreeSewing.dev for documentation on using API keys (English only)
|
||||
keyDocsMsg: This is an advanced feature aimed at developers or anyone who wants to interact with our backend directly.
|
||||
apiCalls: API Calls
|
||||
#bookmarks
|
||||
newBookmark: Add a Bookmark
|
||||
bookmark: Bookmark
|
||||
bookmarks: Bookmarks
|
||||
type: Тип
|
||||
location: Location
|
||||
title: Title
|
||||
new: Новий
|
||||
designBookmark: Дизайни
|
||||
patternBookmark: Викрійки
|
||||
setBookmark: Набори для вимірювання
|
||||
csetBookmark: Curated Measurements Sets
|
||||
docBookmark: Документація
|
||||
customBookmark: Custom Bookmarks
|
||||
yourBookmarks: Your bookmarks
|
||||
bookmarkThisThing: Bookmark this { thing }
|
||||
page: Page
|
||||
#sets
|
||||
set: Measurements Set
|
||||
name: Назва
|
||||
setNameDesc: Ім'я для ідентифікації цього набору вимірювань
|
||||
setNewInfo: Створіть новий набір вимірів, який потім можна використовувати для створення лекал.
|
||||
notes: Нотатки
|
||||
setNotesDesc: Будь-які примітки, які ви хотіли б зберегти щодо цього набору вимірювань
|
||||
description: Опис
|
||||
deleteSetWarning: Видалення набору вимірювань не можна скасувати.
|
||||
image: Зображення
|
||||
measies: Вимірювання
|
||||
setUnitsMsgTitle: Ці налаштування застосовуються лише до цього набору вимірювань
|
||||
setUnitsMsgDesc: |
|
||||
Саме ці одиниці ми будемо використовувати при оновленні або відображенні вимірювань у цьому наборі.
|
||||
Всюди на цьому сайті ми будемо використовувати одиниці, встановлені у вашому обліковому записі.
|
||||
public: Громадськість
|
||||
publicSet: Публічний набір вимірювань
|
||||
privateSet: Набір приватних вимірів
|
||||
publicSetDesc: Інші можуть використовувати ці вимірювання для створення або тестування шаблонів
|
||||
privateSetDesc: Ці вимірювання не можуть бути використані іншими користувачами або відвідувачами
|
||||
permalink: Перманентне посилання
|
||||
editThing: Коригувати {thing}
|
||||
saveThing: Зберегти {thing}
|
||||
filterByDesign: Filter by design
|
||||
noFilter: Do not filter
|
||||
filterByDesignDocs: If you have a specific design in mind, you can <b>filter by design</b> to only list those measurements that are required for this design.
|
||||
setLacksMeasiesForDesign: This set lacks measurements required for this pattern
|
||||
setHasMeasiesForDesign: This set has all measurements required for this pattern
|
||||
someSetsLacking: Some of your sets lack the measurments required to generate this pattern
|
||||
theseSetsReady: These sets have all required measurments to generate this pattern
|
||||
chooseSet: Будь ласка, оберіть набір вимірів
|
||||
patternForWhichSet: Для якого набору вимірів ми повинні згенерувати викрійку?
|
||||
bookmarkedSets: Набори вимірювань, які ви додали до закладок
|
||||
curatedSets: FreeSewing's curated measurements sets
|
||||
curatedSetsAbout: Набори мірок, куратором яких є FreeSewing, які ви можете використовувати для тестування нашої платформи або ваших дизайнів.
|
||||
curateCuratedSets: Curate our selection of curated measurements sets
|
||||
useThisSet: Використовуйте цей набір вимірювань
|
||||
ownSets: Your own measurements sets
|
||||
noOwnSets: You do not have any of your own measurements sets (yet)
|
||||
pleaseMtm: Because made-to-measure lies at the heart of what we do, we strongly suggest you take accurate measurements.
|
||||
noOwnSetsMsg: You can store your measurements as a measurements set, after which you can generate as many patterns as you want for them.
|
||||
measurements: Заміри
|
||||
chooseASet: Choose a measurements set
|
||||
showImage: Show image
|
||||
suggestForCuration: Suggest for curation
|
||||
suggestCset: Suggest a measurements set for curation
|
||||
csetAllMeasies: To ensure curated measurements sets work for all designs, you need to provide a full set of measurements.
|
||||
csetMissing: Your measurements set is missing the following measurements
|
||||
allMeasiesAvailable: All measurements are available.
|
||||
csetHeightMsg1: To allow organizing and presenting our curated sets in a structured way, we organize them by height.
|
||||
csetImgMsg: Finally, we need a picture. Please refer to the documentation to see what makes a good picture for a curated measurements set.
|
||||
docs: Документація
|
||||
csetNotesMsg: If you would like to add any notes, you can do so here.
|
||||
thankYouVeryMuch: Thank you very much
|
||||
csetSuggestedMsg: Your submission has been registered and will be processed by one of our curators.
|
||||
itIsAvailableAt: It is available at
|
||||
csetNameMsg: Each curated set has a name. You can suggest your own name or a pseudonym.
|
||||
patternNew: Створіть новий шаблон
|
||||
patternNewInfo: Виберіть дизайн, додайте свої мірки, і ми створимо для вас викрійку, виготовлену за індивідуальними мірками.
|
||||
designNew: Створіть новий дизайн
|
||||
designNewInfo: Дизайни FreeSewing - це невеликі пакети коду JavaScript, які генерують візерунки. Створювати власні дизайни не складно, і ми маємо детальний підручник, щоб допомогти вам почати.
|
||||
pluginNew: Створіть новий плагін
|
||||
pluginNewInfo: Функціональність FreeSewing можна ще більше розширити за допомогою плагінів. Створити плагін дуже просто, і у нас є посібник, який проведе вас від початку до кінця.
|
||||
showcaseNew: Create a new showcase post
|
||||
showcaseNewInfo: If you would like to share something you (or someone else) made based on our designs, you can start a draft showcase post here.
|
||||
blogNew: Create a new blog post
|
||||
blogNewInfo: If you would like to write on the FreeSewing blog, you can start a draft blog post here.
|
||||
csetNew: Suggest a new curated measurements set
|
||||
csetNewInfo: We curate a collection of vetted measurments sets that we use to test patterns. You can suggest a measurements set here.
|
||||
opackNew: Suggest a new options pack
|
||||
opackNewInfo: We curate a collection of vetted option packs for each of our designs. You can suggest your options here.
|
||||
newPopular: Most popular
|
||||
newShare: Share / Show
|
||||
newDev: Design / Develop
|
||||
generateANewThing: "Створіть новий { thing }"
|
||||
website: Website
|
||||
linkedIdentities: Linked Identities
|
||||
websiteTitle: Do you have a website or other URL you'd like to add?
|
||||
platformTitle: Who are you on { platform }?
|
||||
platformWhy: We do not use this data in any way. This is only here so FreeSewing users can connect the dots across platforms.
|
||||
security: Security
|
||||
revealPassword: Reveal password
|
||||
hidePassword: Hide password
|
||||
#Design view
|
||||
designs: Дизайни
|
||||
code: Код
|
||||
aboutThing: About { thing }
|
||||
requiredMeasurements: Необхідні заміри
|
||||
optionalMeasurements: Додаткові заміри
|
||||
designOptions: Варіанти дизайну
|
||||
parts: Parts
|
||||
plugins: Plugins
|
||||
specifications: Specifications
|
||||
visitShowcase: Visit showcase post
|
||||
examples: Приклади
|
||||
noExamples: We currently do not have any examples for this design
|
||||
noExamplesMsg: We rely on the FreeSewing community to submit examples in our showcase posts.
|
||||
ownPublicPattern: This is the public view on one of your own patterns. For more options, access the private view.
|
||||
ownPrivatePattern: This is the private view on your pattern. The public view will work for you even when the pattern is private. It will only work for others when the pattern is public.
|
||||
privateView: Private view
|
||||
publicView: Public view
|
109
sites/shared/components/account/username.mjs
Normal file
109
sites/shared/components/account/username.mjs
Normal file
|
@ -0,0 +1,109 @@
|
|||
// Context
|
||||
import { LoadingStatusContext } from 'shared/context/loading-status-context.mjs'
|
||||
// Hooks
|
||||
import { useState, useContext } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { useAccount } from 'shared/hooks/use-account.mjs'
|
||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
// Components
|
||||
import { Icons, welcomeSteps, BackToAccountButton } from './shared.mjs'
|
||||
import { OkIcon, NoIcon } from 'shared/components/icons.mjs'
|
||||
import { ContinueButton } from 'shared/components/buttons/continue-button.mjs'
|
||||
import { StringInput } from 'shared/components/inputs.mjs'
|
||||
import { DynamicMdx } from 'shared/components/mdx/dynamic.mjs'
|
||||
|
||||
export const ns = ['account', 'status']
|
||||
|
||||
export const UsernameSettings = ({ welcome = false }) => {
|
||||
// Hooks
|
||||
const { account, setAccount } = useAccount()
|
||||
const backend = useBackend()
|
||||
const { setLoadingStatus } = useContext(LoadingStatusContext)
|
||||
const { t, i18n } = useTranslation(ns)
|
||||
const [username, setUsername] = useState(account.username)
|
||||
const [available, setAvailable] = useState(true)
|
||||
|
||||
const update = async (value) => {
|
||||
if (value !== username) {
|
||||
setUsername(value)
|
||||
const result = await backend.isUsernameAvailable(value)
|
||||
if (result?.response?.response?.status === 404) setAvailable(true)
|
||||
else setAvailable(false)
|
||||
}
|
||||
}
|
||||
|
||||
const save = async () => {
|
||||
setLoadingStatus([true, 'processingUpdate'])
|
||||
const result = await backend.updateAccount({ username })
|
||||
if (result.success) {
|
||||
setAccount(result.data.account)
|
||||
setLoadingStatus([true, 'settingsSaved', true, true])
|
||||
} else setLoadingStatus([true, 'backendError', true, true])
|
||||
}
|
||||
|
||||
const nextHref =
|
||||
welcomeSteps[account.control].length > 4
|
||||
? '/welcome/' + welcomeSteps[account.control][5]
|
||||
: '/docs/guide'
|
||||
|
||||
let btnClasses = 'btn mt-4 capitalize '
|
||||
if (welcome) btnClasses += 'w-64 btn-secondary'
|
||||
else btnClasses += 'w-full btn-primary'
|
||||
|
||||
return (
|
||||
<div className="max-w-xl">
|
||||
<StringInput
|
||||
id="account-username"
|
||||
label={t('usernameTitle')}
|
||||
current={username}
|
||||
update={update}
|
||||
valid={() => available}
|
||||
placeholder={'Sorcha Ni Dhubghaill'}
|
||||
labelBL={
|
||||
<span className="flex flex-row gap-1 items-center">
|
||||
{available ? (
|
||||
<>
|
||||
<OkIcon className="w-4 h-4 text-success" stroke={4} /> {t('usernameAvailable')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<NoIcon className="w-4 h-4 text-error" stroke={3} /> {t('usernameNotAvailable')}
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
}
|
||||
docs={<DynamicMdx language={i18n.language} slug={`docs/site/account/username`} />}
|
||||
/>
|
||||
<button className={btnClasses} disabled={!available} onClick={save}>
|
||||
<span className="flex flex-row items-center gap-2">
|
||||
{available ? t('save') : t('usernameNotAvailable')}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{welcome ? (
|
||||
<>
|
||||
<ContinueButton btnProps={{ href: nextHref }} link />
|
||||
{welcomeSteps[account.control].length > 0 ? (
|
||||
<>
|
||||
<progress
|
||||
className="progress progress-primary w-full mt-12"
|
||||
value={500 / welcomeSteps[account.control].length}
|
||||
max="100"
|
||||
></progress>
|
||||
<span className="pt-4 text-sm font-bold opacity-50">
|
||||
5 / {welcomeSteps[account.control].length}
|
||||
</span>
|
||||
<Icons
|
||||
done={welcomeSteps[account.control].slice(0, 4)}
|
||||
todo={welcomeSteps[account.control].slice(5)}
|
||||
current="username"
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<BackToAccountButton />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
201
sites/shared/components/admin.mjs
Normal file
201
sites/shared/components/admin.mjs
Normal file
|
@ -0,0 +1,201 @@
|
|||
// Depdendencies
|
||||
import { cloudflareConfig } from 'shared/config/cloudflare.mjs'
|
||||
import { freeSewingConfig } from 'shared/config/freesewing.config.mjs'
|
||||
// Context
|
||||
import { LoadingStatusContext } from 'shared/context/loading-status-context.mjs'
|
||||
// Hooks
|
||||
import { useState, useEffect, useContext } from 'react'
|
||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
import { useAccount } from 'shared/hooks/use-account.mjs'
|
||||
// Components
|
||||
import Link from 'next/link'
|
||||
import { Json } from 'shared/components/json.mjs'
|
||||
import { Mdx } from 'shared/components/mdx/dynamic.mjs'
|
||||
import { AccountRole } from 'shared/components/account/role.mjs'
|
||||
import { AccountStatus } from 'shared/components/account/status.mjs'
|
||||
import { Loading } from 'shared/components/spinner.mjs'
|
||||
|
||||
const roles = ['user', 'curator', 'bughunter', 'support', 'admin']
|
||||
|
||||
export const ImpersonateButton = ({ userId }) => {
|
||||
const backend = useBackend()
|
||||
const { setLoadingStatus } = useContext(LoadingStatusContext)
|
||||
const { impersonate } = useAccount()
|
||||
|
||||
if (!userId) return null
|
||||
|
||||
const impersonateUser = async () => {
|
||||
setLoadingStatus([true, 'status:contactingBackend'])
|
||||
const result = await backend.adminImpersonateUser(userId)
|
||||
if (result.success) {
|
||||
impersonate(result.data)
|
||||
setLoadingStatus([true, 'status:settingsSaved', true, true])
|
||||
} else setLoadingStatus([true, 'status:backendError', true, false])
|
||||
}
|
||||
|
||||
return (
|
||||
<button className="btn btn-primary" onClick={impersonateUser}>
|
||||
Impersonate
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export const Row = ({ title, val }) => (
|
||||
<tr className="py-1">
|
||||
<td className="text-sm px-2 text-right font-bold">{title}</td>
|
||||
<td className="text-sm">{val}</td>
|
||||
</tr>
|
||||
)
|
||||
|
||||
export const Hits = ({ results }) => (
|
||||
<>
|
||||
{results && results.username && results.username.length > 0 && (
|
||||
<>
|
||||
<h2>Results based on username</h2>
|
||||
{results.username.map((user) => (
|
||||
<User user={user} key={user.id} />
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{results && results.email && results.email.length > 0 && (
|
||||
<>
|
||||
<h2>Results based on E-mail address</h2>
|
||||
{results.email.map((user) => (
|
||||
<User user={user} key={user.id} />
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
export const ShowUser = ({ user, button = null }) => (
|
||||
<div className="flex flex-row w-full gap-4">
|
||||
<div
|
||||
className="w-52 h-52 bg-base-100 rounded-lg shadow shrink-0"
|
||||
style={{
|
||||
backgroundImage: `url(${cloudflareConfig.url}${user.img}/sq500)`,
|
||||
backgroundSize: 'cover',
|
||||
}}
|
||||
></div>
|
||||
<div className="w-full">
|
||||
<h6 className="flex flex-row items-center gap-2 flex-wrap">
|
||||
{user.username}
|
||||
<span className="font-light">|</span>
|
||||
<AccountRole role={user.role} />
|
||||
<span className="font-light">|</span>
|
||||
<AccountStatus status={user.status} />
|
||||
<span className="font-light">|</span>
|
||||
{user.id}
|
||||
</h6>
|
||||
<div className="flex flex-row flex-wrap gap-4 w-full">
|
||||
<div className="max-w-xs">
|
||||
<table>
|
||||
<tbody>
|
||||
<Row title="Email" val={user.email} />
|
||||
<Row title="Initial" val={user.initial} />
|
||||
<Row title="GitHub" val={user.github} />
|
||||
<Row title="MFA" val={user.mfaEnabled ? 'Yes' : 'No'} />
|
||||
<Row title="Passhash" val={user.passwordType} />
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="max-w-xs">
|
||||
<table>
|
||||
<tbody>
|
||||
<Row title="Patron" val={user.patron} />
|
||||
<Row title="Consent" val={user.consent} />
|
||||
<Row title="Control" val={user.control} />
|
||||
<Row title="Calls (jwt)" val={user.jwtCalls} />
|
||||
<Row title="Calls (key)" val={user.keyCalls} />
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="max-w-xs flex flex-col gap-2">{button}</div>
|
||||
</div>
|
||||
<div className="max-w-full truncate">
|
||||
<Mdx md={user.bio} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
export const User = ({ user }) => (
|
||||
<div className="my-8">
|
||||
<ShowUser
|
||||
user={user}
|
||||
button={
|
||||
<Link href={`/admin/user/${user.id}`} className="btn btn-primary">
|
||||
Manage user
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
export const ManageUser = ({ userId }) => {
|
||||
// Hooks
|
||||
const backend = useBackend()
|
||||
const { setLoadingStatus } = useContext(LoadingStatusContext)
|
||||
|
||||
// State
|
||||
const [user, setUser] = useState({})
|
||||
|
||||
// Effect
|
||||
useEffect(() => {
|
||||
const loadUser = async () => {
|
||||
const result = await backend.adminLoadUser(userId)
|
||||
if (result.success) setUser(result.data.user)
|
||||
}
|
||||
loadUser()
|
||||
}, [userId])
|
||||
|
||||
const updateUser = async (data) => {
|
||||
setLoadingStatus([true, 'status:contactingBackend'])
|
||||
const result = await backend.adminUpdateUser({ id: userId, data })
|
||||
if (result.success) {
|
||||
setLoadingStatus([true, 'status:settingsSaved', true, true])
|
||||
setUser(result.data.user)
|
||||
} else setLoadingStatus([true, 'status:backendError', true, false])
|
||||
}
|
||||
|
||||
return user.id ? (
|
||||
<div className="my-8">
|
||||
<ShowUser user={user} button={<ImpersonateButton userId={user.id} />} />
|
||||
<div className="flex flex-row flex-wrap gap-2 my-2">
|
||||
{roles.map((role) => (
|
||||
<button
|
||||
key={role}
|
||||
className="btn btn-primary btn-outline btn-sm"
|
||||
onClick={() => updateUser({ role })}
|
||||
disabled={role === user.role}
|
||||
>
|
||||
Assign {role} role
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-row flex-wrap gap-2 my-2">
|
||||
{user.mfaEnabled && (
|
||||
<button
|
||||
className="btn btn-warning btn-outline btn-sm"
|
||||
onClick={() => updateUser({ mfaEnabled: false })}
|
||||
>
|
||||
Disable MFA
|
||||
</button>
|
||||
)}
|
||||
{Object.keys(freeSewingConfig.statuses).map((status) => (
|
||||
<button
|
||||
key={status}
|
||||
className="btn btn-warning btn-outline btn-sm"
|
||||
onClick={() => updateUser({ status })}
|
||||
disabled={Number(status) === user.status}
|
||||
>
|
||||
Set {freeSewingConfig.statuses[status].name.toUpperCase()} status
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{user.id ? <Json js={user} /> : null}
|
||||
</div>
|
||||
) : (
|
||||
<Loading />
|
||||
)
|
||||
}
|
174
sites/shared/components/animations/freesewing.mjs
Normal file
174
sites/shared/components/animations/freesewing.mjs
Normal file
|
@ -0,0 +1,174 @@
|
|||
import { logoPath } from 'shared/components/logos/freesewing.mjs'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
||||
export const ns = ['commom']
|
||||
|
||||
const strokeScale = 0.7
|
||||
|
||||
const StrokedText = (props) => (
|
||||
<>
|
||||
<text
|
||||
{...props}
|
||||
strokeWidth={Number(props.strokeWidth) / 2}
|
||||
stroke="hsl(var(--b1))"
|
||||
fill="none"
|
||||
/>
|
||||
<text {...props} />
|
||||
</>
|
||||
)
|
||||
|
||||
export const FreeSewingAnimation = ({
|
||||
className = 'w-full', // Any classes to set on the SVG tag
|
||||
stroke = 1, // The stroke width of the initial colouring in of the logo
|
||||
duration = 10, // Duration of the complete animation in seconds
|
||||
}) => {
|
||||
const { t } = useTranslation(ns)
|
||||
|
||||
// Ensure duration is a number
|
||||
duration = Number(duration)
|
||||
// Ensure stroke is a number
|
||||
stroke = Number(stroke) * strokeScale
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="-1 0 27 27"
|
||||
strokeWidth={stroke}
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className + ' icon'}
|
||||
>
|
||||
<animat
|
||||
attributeName="viewBox"
|
||||
from="0 0 25 25"
|
||||
to="0 10 25 7"
|
||||
begin={`${duration * 0.18}s`}
|
||||
dur={`${duration * 0.03}s`}
|
||||
fill="freeze"
|
||||
/>
|
||||
<clipPath id="logo">
|
||||
<path d={logoPath} stroke="none" fill="none" transform="translate(-0.5,0)" />
|
||||
</clipPath>
|
||||
<g>
|
||||
<g>
|
||||
<path
|
||||
d="M 20 0 l -10,1 l 10 1 l -11 1 l 13 1 l -15 1 l 16 1 l -16 1 l 15 1 l -15 1 l 16 1 l -16 1 l 17 1 l-16 1 l15 1 l -18 1.3 l 19 0.2 l -15 1 l 6 0.5 l -5 0.5 l 5 0 l 9 -0.5 l -3 1.2 l 2 1 l -3 0 l 2 2 l -3 -0.5 l 1 2 l -3 -1 l1 2 l-3 -1 l -1.5 1.5 l-2 -1.5 l-3 1 l-5 -7 l 2 -2 l-3 -1 l 2 -2"
|
||||
clipPath="url(#logo)"
|
||||
strokeOpacity="1"
|
||||
strokeDasharray="330 400"
|
||||
>
|
||||
<animate
|
||||
attributeName="stroke-dashoffset"
|
||||
dur={`${duration * 0.333}s`}
|
||||
from="330"
|
||||
to="0"
|
||||
fill="freeze"
|
||||
/>
|
||||
<animate
|
||||
attributeName="stroke-width"
|
||||
begin={`${duration * 0.333}s`}
|
||||
dur={`${duration * 0.05}s`}
|
||||
from={stroke}
|
||||
to="3"
|
||||
fill="freeze"
|
||||
/>
|
||||
<animateTransfor
|
||||
attributeName="transform"
|
||||
attributeType="XLM"
|
||||
type="scale"
|
||||
begin={`${duration * 0.433}s`}
|
||||
dur={`${duration * 0.1}s`}
|
||||
from="1"
|
||||
to="0.45"
|
||||
fill="freeze"
|
||||
/>
|
||||
</path>
|
||||
<animateTransfor
|
||||
attributeName="transform"
|
||||
attributeType="XLM"
|
||||
type="translate"
|
||||
begin={`${duration * 0.433}s`}
|
||||
dur={`${duration * 0.1}s`}
|
||||
from="0,0"
|
||||
to="6.5,0"
|
||||
fill="freeze"
|
||||
/>
|
||||
</g>
|
||||
<StrokedText
|
||||
x="12.5"
|
||||
y="23"
|
||||
strokeWidth={stroke}
|
||||
stroke="none"
|
||||
fill="hsl(var(--p))"
|
||||
textAnchor="middle"
|
||||
opacity="0"
|
||||
style={{
|
||||
fontSize: 4.2,
|
||||
fontWeight: 'bold',
|
||||
letterSpacing: '-0.007rem',
|
||||
}}
|
||||
>
|
||||
FreeSewing
|
||||
<animate
|
||||
attributeName="opacity"
|
||||
begin={`${duration * 0.45}s`}
|
||||
dur={`${duration * 0.1}s`}
|
||||
from="0"
|
||||
to="1"
|
||||
fill="freeze"
|
||||
/>
|
||||
</StrokedText>
|
||||
<StrokedText
|
||||
x="1.666"
|
||||
y="24.5"
|
||||
stroke="none"
|
||||
strokeWidth={stroke}
|
||||
fill="hsl(var(--s))"
|
||||
textAnchor="start"
|
||||
opacity="0"
|
||||
style={{
|
||||
fontSize: 1.4,
|
||||
fontWeight: 'bold',
|
||||
letterSpacing: '-0.005rem',
|
||||
}}
|
||||
>
|
||||
{t('common:slogan1')}
|
||||
<animate
|
||||
attributeName="opacity"
|
||||
begin={`${duration * 0.6}s`}
|
||||
dur={`${duration * 0.05}s`}
|
||||
from="0"
|
||||
to="1"
|
||||
fill="freeze"
|
||||
/>
|
||||
</StrokedText>
|
||||
<StrokedText
|
||||
x="23.333"
|
||||
y="26"
|
||||
stroke="none"
|
||||
strokeWidth={stroke}
|
||||
fill="hsl(var(--a))"
|
||||
textAnchor="end"
|
||||
opacity="0"
|
||||
style={{
|
||||
fontSize: 1.4,
|
||||
fontWeight: 'bold',
|
||||
letterSpacing: '-0.005rem',
|
||||
}}
|
||||
>
|
||||
{t('common:slogan2')}
|
||||
<animate
|
||||
attributeName="opacity"
|
||||
begin={`${duration * 0.7}s`}
|
||||
dur={`${duration * 0.05}s`}
|
||||
from="0"
|
||||
to="1"
|
||||
fill="freeze"
|
||||
/>
|
||||
</StrokedText>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
160
sites/shared/components/animations/how-does-it-work.mjs
Normal file
160
sites/shared/components/animations/how-does-it-work.mjs
Normal file
|
@ -0,0 +1,160 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { AaronFront } from 'shared/components/designs/linedrawings/aaron.mjs'
|
||||
import { BruceFront, BruceBack } from 'shared/components/designs/linedrawings/bruce.mjs'
|
||||
import { SimonFront, SimonBack } from 'shared/components/designs/linedrawings/simon.mjs'
|
||||
import { WahidFront, WahidBack } from 'shared/components/designs/linedrawings/wahid.mjs'
|
||||
import { AlbertFront } from 'shared/components/designs/linedrawings/albert.mjs'
|
||||
|
||||
export const ns = ['homepage']
|
||||
|
||||
const lineDrawings = [
|
||||
<AaronFront key={1} className="h-72 md:h-96" />,
|
||||
<BruceBack key={2} className="h-72 md:h-96" />,
|
||||
<SimonBack key={3} className="h-72 md:h-96" />,
|
||||
<WahidFront key={4} className="h-72 md:h-96" />,
|
||||
<AlbertFront key={5} className="h-72 md:h-96" />,
|
||||
<BruceFront key={6} className="h-72 md:h-96" />,
|
||||
<SimonFront key={7} className="h-72 md:h-96" />,
|
||||
<WahidBack key={8} className="h-72 md:h-96" />,
|
||||
]
|
||||
|
||||
const patternTweaks = [
|
||||
<path
|
||||
key={1}
|
||||
d="M 0,121.4 L 0,705.1 L 253.46,705.1 C 253.46,474.02 281.12,307.05 281.12,307.05 C 187.15,307.05 128.12,163.24 163.07,19.43 L 119.46,8.83 C 92.11,121.4 92.11,121.4 0,121.4 z"
|
||||
/>,
|
||||
<path
|
||||
key={2}
|
||||
d="M 0,121.4 L 0,705.1 L 253.46,705.1 C 253.46,481 279.96,321 279.96,321 C 184.87,321 126.42,170.22 163.07,19.43 L 119.46,8.83 C 92.11,121.4 92.11,121.4 0,121.4 z"
|
||||
/>,
|
||||
<path
|
||||
key={3}
|
||||
d="M 0,121.4 L 0,705.1 L 253.46,705.1 C 253.46,481 273.47,321 273.47,321 C 181.62,321 126.42,170.22 163.07,19.43 L 119.46,8.83 C 92.11,121.4 92.11,121.4 0,121.4 z"
|
||||
/>,
|
||||
<path
|
||||
key={4}
|
||||
d="M 0,121.4 L 0,742.92 L 253.46,742.92 C 253.46,481 273.47,321 273.47,321 C 181.62,321 126.42,170.22 163.07,19.43 L 119.46,8.83 C 92.11,121.4 92.11,121.4 0,121.4 z"
|
||||
/>,
|
||||
<path
|
||||
key={5}
|
||||
d="M 0,121.4 L 0,742.92 L 253.46,742.92 C 253.46,481 273.47,321 273.47,321 C 181.62,321 126.42,170.22 163.07,19.43 L 119.46,8.83 C 95.69,106.65 80.04,121.4 0,121.4 z"
|
||||
/>,
|
||||
<path
|
||||
key={6}
|
||||
d="M 0,152.02 L 0,742.92 L 253.46,742.92 C 253.46,481 273.47,321 273.47,321 C 181.62,321 126.42,170.22 163.07,19.43 L 119.46,8.83 C 89.22,133.26 73.57,152.02 0,152.02 z"
|
||||
/>,
|
||||
<path
|
||||
key={7}
|
||||
d="M 0,152.02 L 0,742.92 L 253.46,742.92 C 253.46,481 273.47,321 273.47,321 C 183.55,321 130.16,170.66 166.7,20.31 L 123.1,9.71 C 93.04,133.38 76.92,152.02 0,152.02 z"
|
||||
/>,
|
||||
<path
|
||||
key={8}
|
||||
d="M 0,152.02 L 0,742.92 L 253.46,742.92 C 253.46,481 273.47,321 273.47,321 C 181.55,321 126.27,170.2 162.92,19.39 L 126.88,10.63 C 97.02,133.5 80.4,152.02 0,152.02 z"
|
||||
/>,
|
||||
]
|
||||
|
||||
const Pattern = ({ i }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="-300 -20 850 850"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="fill-primary h-72 md:h-96"
|
||||
strokeWidth="4"
|
||||
fillOpacity="0.25"
|
||||
>
|
||||
{patternTweaks[i]}
|
||||
</svg>
|
||||
)
|
||||
|
||||
const Nr = ({ nr }) => (
|
||||
<div className="absolute top-8 w-full -ml-20">
|
||||
<span className="bg-primary text-primary-content font-bold rounded-full w-12 h-12 flex items-center justify-center align-center m-auto text-3xl">
|
||||
{nr}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
const Title = ({ txt }) => (
|
||||
<div className="absolute top-28 left-0 w-full">
|
||||
<h3
|
||||
className="text-3xl -rotate-12 w-48 text-center m-auto"
|
||||
style={{
|
||||
textShadow:
|
||||
'1px 1px 1px hsl(var(--b1)), -1px -1px 1px hsl(var(--b1)), -1px 1px 1px hsl(var(--b1)), 1px -1px 1px hsl(var(--b1))',
|
||||
}}
|
||||
>
|
||||
{txt}
|
||||
</h3>
|
||||
</div>
|
||||
)
|
||||
|
||||
const slides = [0, 1, 2, 3, 4, 5, 6, 7]
|
||||
|
||||
export const HowDoesItWorkAnimation = () => {
|
||||
const { t } = useTranslation(ns)
|
||||
const [step, setStep] = useState(0)
|
||||
const [halfStep, setHalfStep] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
if (step > 6) setStep(0)
|
||||
else setStep(step + 1)
|
||||
if (halfStep > 7) setHalfStep(0)
|
||||
else setHalfStep(halfStep + 0.5)
|
||||
}, 800)
|
||||
}, [step])
|
||||
|
||||
return (
|
||||
<div className="flex flex-col md:grid md:grid-cols-3">
|
||||
<div className="relative w-full">
|
||||
<div className="relative h-72 md:h-96 overflow-hidden">
|
||||
{slides.map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`duration-700 ease-in-out transition-all ${
|
||||
step === i ? 'opacity-1' : 'opacity-0'
|
||||
} absolute top-0 text-center w-full`}
|
||||
>
|
||||
<div className="w-full flex flex-row items-center h-72 md:h-96 w-full justify-center">
|
||||
{lineDrawings[i]}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Nr nr={1} />
|
||||
<Title txt={t('pickAnyDesign')} />
|
||||
</div>
|
||||
<div className="relative w-full">
|
||||
<div className="relative h-72 md:h-96 overflow-hidden">
|
||||
{slides.map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`duration-700 ease-in-out transition-all ${
|
||||
Math.floor(halfStep) === i ? 'opacity-1' : 'opacity-0'
|
||||
} absolute top-0 text-center w-full`}
|
||||
>
|
||||
<div className="w-full flex flex-row items-center h-72 md:h-96 w-full justify-center">
|
||||
<img src={`/img/models/model-${i}.png`} className="h-72 md:h-96 shrink-0 px-8" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<Nr nr={2} />
|
||||
<Title txt={t('addASet')} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative w-full">
|
||||
<div className="relative h-96 overflow-hidden">
|
||||
<div className="w-full flex flex-row items-center h-72 md:h-96 w-full justify-center">
|
||||
<Pattern key={step} i={step} />
|
||||
</div>
|
||||
<Nr nr={3} />
|
||||
<Title txt={t('customizeYourPattern')} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
18
sites/shared/components/animations/loading-bar.mjs
Normal file
18
sites/shared/components/animations/loading-bar.mjs
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
|
||||
export const LoadingBar = ({ duration = 1000, color = 'primary' }) => {
|
||||
const [started, setStarted] = useState(false)
|
||||
useEffect(() => {
|
||||
setTimeout(() => setStarted(true), 100)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className={`w-full bg-base-200 rounded-full h-2.5 mb-4 bg-${color} bg-opacity-30`}>
|
||||
<div
|
||||
className={`bg-${color} h-2.5 rounded-full transition-all ${
|
||||
started ? 'w-full' : 'w-0'
|
||||
} duration-[${duration}ms]`}
|
||||
></div>
|
||||
</div>
|
||||
)
|
||||
}
|
36
sites/shared/components/base-layout.mjs
Normal file
36
sites/shared/components/base-layout.mjs
Normal file
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* The default full-page FreeSewing layout
|
||||
*/
|
||||
export const BaseLayout = ({ children = [] }) => (
|
||||
<div className="flex flex-row items-start w-full justify-between 2xl:px-36 xl:px-12 px-4 gap-0 lg:gap-4 xl:gap-8 3xl: gap-12">
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
/*
|
||||
* The left column of the default layout
|
||||
*/
|
||||
export const BaseLayoutLeft = ({ children = [] }) => (
|
||||
<div className="max-w-96 w-1/4 hidden lg:block shrink-0 my-8">{children}</div>
|
||||
)
|
||||
|
||||
/*
|
||||
* The right column of the default layout
|
||||
*/
|
||||
export const BaseLayoutRight = ({ children = [] }) => (
|
||||
<div className="max-w-96 w-1/4 hidden xl:block my-8">{children}</div>
|
||||
)
|
||||
|
||||
/*
|
||||
* The main column for prose (text like docs and so on)
|
||||
*/
|
||||
export const BaseLayoutProse = ({ children = [] }) => (
|
||||
<div className="grow w-full m-auto max-w-prose my-8">{children}</div>
|
||||
)
|
||||
|
||||
/*
|
||||
* The central column for wide content (no max-width)
|
||||
*/
|
||||
export const BaseLayoutWide = ({ children = [] }) => (
|
||||
<div className="grow w-full m-auto my-8 grow">{children}</div>
|
||||
)
|
73
sites/shared/components/bookmarks.mjs
Normal file
73
sites/shared/components/bookmarks.mjs
Normal file
|
@ -0,0 +1,73 @@
|
|||
// __SDEFILE__ - This file is a dependency for the stand-alone environment
|
||||
// Dependencies
|
||||
import { horFlexClasses, notEmpty } from 'shared/utils.mjs'
|
||||
// Hooks
|
||||
import { useContext, useState } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
// Context
|
||||
import { ModalContext } from 'shared/context/modal-context.mjs'
|
||||
import { LoadingStatusContext } from 'shared/context/loading-status-context.mjs'
|
||||
// Components
|
||||
import { ModalWrapper } from 'shared/components/wrappers/modal.mjs'
|
||||
import { BookmarkIcon } from 'shared/components/icons.mjs'
|
||||
import { StringInput } from 'shared/components/inputs.mjs'
|
||||
|
||||
export const ns = 'account'
|
||||
|
||||
export const CreateBookmark = ({ type, title, slug }) => {
|
||||
const backend = useBackend()
|
||||
const { t, i18n } = useTranslation(ns)
|
||||
const [name, setName] = useState(title)
|
||||
const { setLoadingStatus } = useContext(LoadingStatusContext)
|
||||
const { setModal } = useContext(ModalContext)
|
||||
|
||||
const url = i18n.language === 'en' ? `/${slug}` : `/${i18n.language}/${slug}`
|
||||
|
||||
const bookmark = async (evt) => {
|
||||
evt.stopPropagation()
|
||||
setLoadingStatus([true, 'status:contactingBackend'])
|
||||
const result = await backend.createBookmark({ type, title, url })
|
||||
if (result.success) {
|
||||
setLoadingStatus([true, 'status:nailedIt', true, true])
|
||||
setModal(false)
|
||||
} else setLoadingStatus([true, 'backendError', true, false])
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2>{t('account:bookmarkThisPage')}</h2>
|
||||
<StringInput
|
||||
label={t('account:title')}
|
||||
current={name}
|
||||
update={setName}
|
||||
valid={notEmpty}
|
||||
labelBL={url}
|
||||
/>
|
||||
<button className="btn btn-primary w-full mt-4" onClick={bookmark}>
|
||||
<span>{t('account:bookmarkThisPage')}</span>
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const BookmarkButton = ({ slug, type, title }) => {
|
||||
const { t } = useTranslation('account')
|
||||
const { setModal } = useContext(ModalContext)
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`btn btn-secondary btn-outline ${horFlexClasses}`}
|
||||
onClick={() =>
|
||||
setModal(
|
||||
<ModalWrapper flex="col" justify="top lg:justify-center" slideFrom="right">
|
||||
<CreateBookmark {...{ type, title, slug }} />
|
||||
</ModalWrapper>
|
||||
)
|
||||
}
|
||||
>
|
||||
<BookmarkIcon />
|
||||
<span>{t('account:bookmark')}</span>
|
||||
</button>
|
||||
)
|
||||
}
|
|
@ -1,32 +1,27 @@
|
|||
import { Fragment } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { FreeSewingIcon } from 'shared/components/icons.mjs'
|
||||
// __SDEFILE__ - This file is a dependency for the stand-alone environment
|
||||
import { HomeIcon, RightIcon } from 'shared/components/icons.mjs'
|
||||
import { Link, PageLink } from 'shared/components/link.mjs'
|
||||
|
||||
export const Breadcrumbs = ({ crumbs = [] }) =>
|
||||
crumbs ? (
|
||||
<ul className="flex flex-row flex-wrap gap-2 font-bold">
|
||||
<li>
|
||||
<Link href="/" title="FreeSewing" className="text-base-content">
|
||||
<FreeSewingIcon />
|
||||
export const Breadcrumbs = ({ crumbs, title }) => {
|
||||
if (!crumbs) return null
|
||||
|
||||
return (
|
||||
<ul className="flex flex-row flex-wrap">
|
||||
<li className="inline">
|
||||
<Link href="/" title="FreeSewing">
|
||||
<HomeIcon />
|
||||
</Link>
|
||||
</li>
|
||||
{crumbs.map((crumb) => (
|
||||
<Fragment key={crumb[1] + crumb[0]}>
|
||||
<li className="text-base-content px-2">»</li>
|
||||
<li>
|
||||
{crumb[1] ? (
|
||||
<Link
|
||||
href={crumb[1]}
|
||||
title={crumb[0]}
|
||||
className="text-secondary hover:text-secondary-focus"
|
||||
>
|
||||
{crumb[0]}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="text-base-content">{crumb[0]}</span>
|
||||
)}
|
||||
</li>
|
||||
</Fragment>
|
||||
{crumbs.map((crumb, i) => (
|
||||
<li key={crumb.s} className="flex flex-row flex-wrap items-center">
|
||||
<RightIcon className="w-4 h-4 opacity-50" stroke={3} />
|
||||
{i < crumbs.length - 1 ? (
|
||||
<PageLink href={`/${crumb.s}`} title={crumb.t} txt={crumb.t} />
|
||||
) : (
|
||||
<span className="font-medium">{title || crumb.t}</span>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null
|
||||
)
|
||||
}
|
||||
|
|
8
sites/shared/components/bullet.mjs
Normal file
8
sites/shared/components/bullet.mjs
Normal file
|
@ -0,0 +1,8 @@
|
|||
export const Bullet = ({ num, children }) => (
|
||||
<div className="flex flex-row items-center py-4 w-full gap-4">
|
||||
<span className="bg-secondary text-secondary-content rounded-full w-8 h-8 p-1 inline-block text-center font-bold mr-4 shrink-0">
|
||||
{num}
|
||||
</span>
|
||||
<div className="text-lg grow">{children}</div>
|
||||
</div>
|
||||
)
|
44
sites/shared/components/buttons/continue-button.mjs
Normal file
44
sites/shared/components/buttons/continue-button.mjs
Normal file
|
@ -0,0 +1,44 @@
|
|||
// __SDEFILE__ - This file is a dependency for the stand-alone environment
|
||||
// Hooks
|
||||
import { useContext } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
// Context
|
||||
import { LoadingStatusContext } from 'shared/context/loading-status-context.mjs'
|
||||
// Components
|
||||
import { Spinner } from 'shared/components/spinner.mjs'
|
||||
import Link from 'next/link'
|
||||
|
||||
export const ContinueButton = ({ btnProps = {}, link = false }) => {
|
||||
// Context
|
||||
const { loading } = useContext(LoadingStatusContext)
|
||||
|
||||
// Hooks
|
||||
const { t } = useTranslation(['account'])
|
||||
|
||||
let classes = 'btn mt-8 capitalize w-full '
|
||||
if (loading) classes += 'btn-accent '
|
||||
else classes += 'btn-primary '
|
||||
|
||||
const children = (
|
||||
<span className="flex flex-row items-center gap-2">
|
||||
{loading ? (
|
||||
<>
|
||||
<Spinner />
|
||||
<span>{t('processing')}</span>
|
||||
</>
|
||||
) : (
|
||||
<span>{t('continue')}</span>
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
|
||||
return link ? (
|
||||
<Link className={classes} tabIndex="-1" {...btnProps}>
|
||||
{children}
|
||||
</Link>
|
||||
) : (
|
||||
<button className={classes} tabIndex="-1" role="button" {...btnProps}>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
52
sites/shared/components/buttons/flex-button-text.mjs
Normal file
52
sites/shared/components/buttons/flex-button-text.mjs
Normal file
|
@ -0,0 +1,52 @@
|
|||
import { validateEmail, validateTld } from 'shared/utils.mjs'
|
||||
import { Spinner } from 'shared/components/spinner.mjs'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { EmailIcon, RightIcon, WarningIcon } from 'shared/components/icons.mjs'
|
||||
|
||||
export const FlexButtonText = ({ children }) => (
|
||||
<div className="flex flex-row items-center justify-between w-full">{children}</div>
|
||||
)
|
||||
|
||||
export const EmailValidButton = ({ email, app, validText, invalidText, btnProps = {} }) => {
|
||||
const { t } = useTranslation(['signup'])
|
||||
const emailValid = (validateEmail(email) && validateTld(email)) || false
|
||||
|
||||
return (
|
||||
<button
|
||||
style={{
|
||||
backgroundColor: emailValid ? '' : 'hsl(var(--wa) / var(--tw-border-opacity))',
|
||||
opacity: emailValid ? 1 : 0.8,
|
||||
}}
|
||||
className={`btn mt-4 capitalize w-full
|
||||
${emailValid ? (app.state.loading ? 'btn-accent' : 'btn-primary') : 'btn-warning'}`}
|
||||
tabIndex="-1"
|
||||
role="button"
|
||||
aria-disabled={!emailValid}
|
||||
{...btnProps}
|
||||
>
|
||||
<FlexButtonText>
|
||||
{emailValid ? (
|
||||
app.state.loading ? (
|
||||
<>
|
||||
<Spinner />
|
||||
<span>{t('processing')}</span>
|
||||
<Spinner />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<EmailIcon />
|
||||
<span>{validText}</span>
|
||||
<RightIcon />
|
||||
</>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<EmailIcon />
|
||||
<span>{invalidText}</span>
|
||||
<WarningIcon />
|
||||
</>
|
||||
)}
|
||||
</FlexButtonText>
|
||||
</button>
|
||||
)
|
||||
}
|
39
sites/shared/components/buttons/save-settings-button.mjs
Normal file
39
sites/shared/components/buttons/save-settings-button.mjs
Normal file
|
@ -0,0 +1,39 @@
|
|||
// Hooks
|
||||
import { useContext } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
// Context
|
||||
import { LoadingStatusContext } from 'shared/context/loading-status-context.mjs'
|
||||
// Components
|
||||
import { Spinner } from 'shared/components/spinner.mjs'
|
||||
|
||||
export const SaveSettingsButton = ({ btnProps = {}, welcome = false, label = false }) => {
|
||||
const { loading } = useContext(LoadingStatusContext)
|
||||
const { t } = useTranslation(['account'])
|
||||
let classes = 'btn mt-4 capitalize '
|
||||
if (welcome) {
|
||||
classes += 'w-64 '
|
||||
if (loading) classes += 'btn-accent '
|
||||
else classes += 'btn-secondary '
|
||||
} else {
|
||||
classes += 'w-full '
|
||||
if (loading) classes += 'btn-accent '
|
||||
else classes += 'btn-primary '
|
||||
}
|
||||
|
||||
return (
|
||||
<button className={classes} tabIndex="-1" role="button" {...btnProps}>
|
||||
<span className="flex flex-row items-center gap-2">
|
||||
{loading ? (
|
||||
<>
|
||||
<Spinner />
|
||||
<span>{t('processing')}</span>
|
||||
</>
|
||||
) : label ? (
|
||||
<span>{label}</span>
|
||||
) : (
|
||||
<span>{t('save')}</span>
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
}
|
15
sites/shared/components/card-link.mjs
Normal file
15
sites/shared/components/card-link.mjs
Normal file
|
@ -0,0 +1,15 @@
|
|||
import Link from 'next/link'
|
||||
|
||||
export const CardLink = ({ color = 'primary', href, title, text }) => (
|
||||
<div className={`p-1 bg-${color} rounded-none lg:rounded-xl lg:shadow flex flex-col`}>
|
||||
<Link
|
||||
href={href}
|
||||
className={`px-4 lg:px-8 py-10 rounded-none lg:rounded-lg block
|
||||
bg-base-100 text-base-content hover:bg-${color} hover:text-${color}-content
|
||||
transition-color duration-300 grow`}
|
||||
>
|
||||
<h2 className="mb-4 text-inherit">{title}</h2>
|
||||
<p className="font-medium text-inherit">{text}</p>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
29
sites/shared/components/choice-button.mjs
Normal file
29
sites/shared/components/choice-button.mjs
Normal file
|
@ -0,0 +1,29 @@
|
|||
export const ChoiceButton = ({
|
||||
title = '',
|
||||
onClick,
|
||||
children,
|
||||
icon = null,
|
||||
color = 'secondary',
|
||||
active = false,
|
||||
noMargin = false,
|
||||
}) => (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`
|
||||
flex flex-col flex-nowrap items-start justify-start gap-2 pt-2 pb-4 h-auto w-full ${
|
||||
noMargin ? '' : 'mt-3'
|
||||
}
|
||||
btn btn-${color} btn-ghost border border-${color}
|
||||
hover:bg-opacity-20 hover:bg-${color} hover:border hover:border-${color}
|
||||
${active ? 'bg-' + color + ' bg-opacity-20 border border-' + color : ''}
|
||||
`}
|
||||
>
|
||||
<h5 className="flex flex-row items-center justify-between w-full">
|
||||
<span>{title}</span>
|
||||
{icon}
|
||||
</h5>
|
||||
<div className={`normal-case text-base font-medium text-base-content text-left`}>
|
||||
{children}
|
||||
</div>
|
||||
</button>
|
||||
)
|
30
sites/shared/components/choice-link.mjs
Normal file
30
sites/shared/components/choice-link.mjs
Normal file
|
@ -0,0 +1,30 @@
|
|||
// __SDEFILE__ - This file is a dependency for the stand-alone environment
|
||||
import Link from 'next/link'
|
||||
|
||||
export const ChoiceLink = ({ title = '', href = '', children, icon = null }) => {
|
||||
const linkProps = {
|
||||
href,
|
||||
className: `flex flex-col flex-nowrap items-start justify-start gap-2 pt-2 pb-4 h-auto w-full mt-3
|
||||
btn btn-secondary btn-ghost border border-secondary
|
||||
hover:bg-opacity-20 hover:bg-secondary hover:border hover:border-secondary`,
|
||||
}
|
||||
|
||||
const content = (
|
||||
<>
|
||||
<h5 className="flex flex-row items-center justify-between w-full">
|
||||
<span>{title}</span>
|
||||
{icon}
|
||||
</h5>
|
||||
<div className={`normal-case text-base font-medium text-base-content text-left`}>
|
||||
{children}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
// Deal with external links
|
||||
return href.slice(0, 4).toLowerCase() === 'http' ? (
|
||||
<a {...linkProps}>{content}</a>
|
||||
) : (
|
||||
<Link {...linkProps}>{content}</Link>
|
||||
)
|
||||
}
|
21
sites/shared/components/code-box.mjs
Normal file
21
sites/shared/components/code-box.mjs
Normal file
|
@ -0,0 +1,21 @@
|
|||
// __SDEFILE__ - This file is a dependency for the stand-alone environment
|
||||
import { CopyToClipboard } from 'shared/components/copy-to-clipboard.mjs'
|
||||
|
||||
export const CodeBox = ({ code, title }) => (
|
||||
<div className="hljs my-4">
|
||||
<div
|
||||
className={`
|
||||
flex flex-row justify-between items-center
|
||||
text-xs font-medium text-success-content
|
||||
mt-1 border-b border-neutral-content border-opacity-25
|
||||
px-4 py-1 mb-2 lg:text-sm
|
||||
`}
|
||||
>
|
||||
<span>{title}</span>
|
||||
<CopyToClipboard text={code} />
|
||||
</div>
|
||||
<pre className="language-md hljs text-base lg:text-lg whitespace-break-spaces overflow-scroll pr-4">
|
||||
{code}
|
||||
</pre>
|
||||
</div>
|
||||
)
|
|
@ -1,44 +1,141 @@
|
|||
// __SDEFILE__ - This file is a dependency for the stand-alone environment
|
||||
import { useState } from 'react'
|
||||
import { DownIcon } from 'shared/components/icons.mjs'
|
||||
import Link from 'next/link'
|
||||
|
||||
export const Collapse = ({ title, children, valid = true, buttons = [], opened = false }) => {
|
||||
const OpenTitleButton = ({
|
||||
title,
|
||||
toggle,
|
||||
color = 'primary',
|
||||
openButtons = [],
|
||||
bottom = false,
|
||||
}) => (
|
||||
<div
|
||||
role="button"
|
||||
className={`flex flex-row items-center justify-between w-full ${
|
||||
bottom ? 'rounded-b-lg' : 'rounded-t-lg'
|
||||
}
|
||||
bg-${color} text-${color}-content px-4 py-1 text-lg font-medium`}
|
||||
onClick={toggle}
|
||||
>
|
||||
{<DownIcon className="rotate-180 w-6 h-6 mr-4" />}
|
||||
{!bottom && title}
|
||||
<div className="flex flex-row items-center gap-2 z-5">
|
||||
{openButtons}
|
||||
<button className="btn btn-ghost btn-xs px-0" onClick={toggle}></button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
export const Collapse = ({
|
||||
title,
|
||||
openTitle = false,
|
||||
children = [],
|
||||
buttons = [],
|
||||
top = true,
|
||||
bottom = false,
|
||||
color = 'primary',
|
||||
opened = false,
|
||||
toggle = false,
|
||||
toggleClasses = '',
|
||||
onClick = false,
|
||||
openButtons = null,
|
||||
className = '',
|
||||
}) => {
|
||||
const [open, setOpen] = useState(opened)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
${valid ? 'border-primary' : 'border-warning'}
|
||||
shadow my-4 border-solid border-l-[6px]
|
||||
border-r-0 border-t-0 border-b-0`}
|
||||
>
|
||||
const TitleBtn = ({ bottom }) =>
|
||||
open ? (
|
||||
<OpenTitleButton
|
||||
title={openTitle || title}
|
||||
toggle={() => setOpen(false)}
|
||||
{...{ color, openButtons, bottom }}
|
||||
/>
|
||||
) : null
|
||||
|
||||
return open ? (
|
||||
<div className={`shadow my-4 w-full mx-auto lg:mx-0 `}>
|
||||
{top ? <TitleBtn /> : null}
|
||||
<div
|
||||
className={`
|
||||
${valid ? `bg-primary bg-opacity-0` : `bg-warning bg-opacity-20`} ${
|
||||
open ? 'bg-opacity-100 min-h-0 text-primary-content' : ''
|
||||
}
|
||||
p-0 min-h-0 `}
|
||||
onClick={() => setOpen(!open)}
|
||||
className={`p-2 lg:p-4 border-solid border border-${color} ${
|
||||
!bottom ? 'rounded-b-lg' : ''
|
||||
} ${!top ? 'rounded-t-lg' : ''}`}
|
||||
>
|
||||
<div className="px-4 py-1 flex flex-row items-center justify-between hover:cursor-pointer">
|
||||
<div>
|
||||
<b>{title[0]}</b> <span className="font-normal font-lg">{title[1]}</span>
|
||||
</div>
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
{buttons}
|
||||
<DownIcon
|
||||
stroke={3}
|
||||
className={`w-6 h-6 transition-transform ${
|
||||
open ? 'text-primary-content rotate-180' : 'text-base-content'
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{open ? (
|
||||
<div className="bg-base-100 text-base-content px-4 py-2">
|
||||
<div className="pt-4">{children}</div>
|
||||
</div>
|
||||
) : null}
|
||||
{children}
|
||||
</div>
|
||||
{bottom ? <TitleBtn bottom /> : null}
|
||||
</div>
|
||||
) : (
|
||||
<div className={`flex flex-row gap-2 my-4 items-center bg-${color} bg-opacity-10 ${className}`}>
|
||||
<div
|
||||
className={`shadow border-solid border-l-[6px] border-r-0 border-t-0 border-b-0 border-${color} min-h-12
|
||||
grow flex flex-row gap-4 py-1 px-4 items-center justify-start hover:cursor-pointer hover:bg-${color} hover:bg-opacity-20`}
|
||||
onClick={onClick ? onClick : () => setOpen(true)}
|
||||
>
|
||||
<DownIcon /> {title}
|
||||
{toggle ? (
|
||||
<button onClick={() => setOpen(true)} className={toggleClasses}>
|
||||
{toggle}
|
||||
</button>
|
||||
) : (
|
||||
buttons
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const MimicCollapseLink = ({
|
||||
title,
|
||||
buttons = [],
|
||||
color = 'primary',
|
||||
href = '/',
|
||||
className = '',
|
||||
}) => (
|
||||
<Link className={`flex flex-row gap-2 my-4 items-center ${className}`} href={href}>
|
||||
<div
|
||||
className={`shadow border-solid border-l-[6px] border-r-0 border-t-0 border-b-0 border-${color} min-h-12
|
||||
grow flex flex-row gap-4 py-1 px-4 items-center justify-start hover:cursor-pointer hover:bg-${color} hover:bg-opacity-20`}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
{buttons}
|
||||
</Link>
|
||||
)
|
||||
|
||||
export const useCollapseButton = () => {
|
||||
// Shared state
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
// Method to allow closing the button
|
||||
const close = () => setOpen(false)
|
||||
|
||||
// The component
|
||||
const CollapseButton = ({
|
||||
title,
|
||||
openTitle = false,
|
||||
children = [],
|
||||
className = 'btn btn-lg btn-primary',
|
||||
top = true,
|
||||
bottom = false,
|
||||
color = 'primary',
|
||||
}) => {
|
||||
const titleBtn = open ? (
|
||||
<OpenTitleButton title={openTitle || title} toggle={() => setOpen(false)} color={color} />
|
||||
) : null
|
||||
|
||||
return open ? (
|
||||
<div className={`shadow border-solid border rounded-lg border-${color} mb-2 mt-4`}>
|
||||
{top ? titleBtn : null}
|
||||
<div className="p-4">{children}</div>
|
||||
{bottom ? titleBtn : null}
|
||||
</div>
|
||||
) : (
|
||||
<button className={className} onClick={() => setOpen(true)}>
|
||||
{title}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
return { CollapseButton, closeCollapseButton: close }
|
||||
}
|
||||
|
|
13
sites/shared/components/control/score.mjs
Normal file
13
sites/shared/components/control/score.mjs
Normal file
|
@ -0,0 +1,13 @@
|
|||
// __SDEFILE__ - This file is a dependency for the stand-alone environment
|
||||
import { BulletIcon } from 'shared/components/icons.mjs'
|
||||
|
||||
const scores = [1, 2, 3, 4, 5]
|
||||
|
||||
export const ControlScore = ({ control, color = 'base-content' }) =>
|
||||
control ? (
|
||||
<div className={`flex flex-row items-center text-${color}`}>
|
||||
{scores.map((score) => (
|
||||
<BulletIcon fill={control >= score ? true : false} className="w-6 h-6 -ml-1" key={score} />
|
||||
))}
|
||||
</div>
|
||||
) : null
|
28
sites/shared/components/control/tip.mjs
Normal file
28
sites/shared/components/control/tip.mjs
Normal file
|
@ -0,0 +1,28 @@
|
|||
// __SDEFILE__ - This file is a dependency for the stand-alone environment
|
||||
import { Popout } from 'shared/components/popout/index.mjs'
|
||||
import { PageLink } from 'shared/components/link.mjs'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { RightIcon } from 'shared/components/icons.mjs'
|
||||
|
||||
export const ns = ['docs']
|
||||
|
||||
export const ControlTip = () => {
|
||||
const { t } = useTranslation(ns)
|
||||
|
||||
return (
|
||||
<Popout note>
|
||||
<h5>{t('controltip.t')}</h5>
|
||||
<p dangerouslySetInnerHTML={{ __html: t('controltip.d1') }} />
|
||||
<p>
|
||||
{t('controltip.d2')}
|
||||
<br />
|
||||
{t('controltip.d3')}
|
||||
</p>
|
||||
<div className="flex flex-row gap-1 items-center">
|
||||
<PageLink href="/account/" txt={t('account')} />
|
||||
<RightIcon className="w-4 h-4" />
|
||||
<PageLink href="/account/control/" txt={t('controltip.t')} />
|
||||
</div>
|
||||
</Popout>
|
||||
)
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
// __SDEFILE__ - This file is a dependency for the stand-alone environment
|
||||
import ReactDOMServer from 'react-dom/server'
|
||||
import { useState } from 'react'
|
||||
import { CopyIcon } from 'shared/components/icons.mjs'
|
||||
import { CopyIcon, OkIcon } from 'shared/components/icons.mjs'
|
||||
import { CopyToClipboard as Copy } from 'react-copy-to-clipboard'
|
||||
|
||||
const strip = (html) =>
|
||||
|
@ -22,7 +23,11 @@ export const CopyToClipboard = ({ content }) => {
|
|||
return (
|
||||
<Copy text={text} onCopy={() => handleCopied(setCopied)}>
|
||||
<button className={copied ? 'text-success' : ''}>
|
||||
<CopyIcon className="w-5 h-5" />
|
||||
{copied ? (
|
||||
<OkIcon className="w-5 h-5 text-success-content bg-success rounded-full p-1" stroke={4} />
|
||||
) : (
|
||||
<CopyIcon className="w-5 h-5 text-inherit" />
|
||||
)}
|
||||
</button>
|
||||
</Copy>
|
||||
)
|
||||
|
|
536
sites/shared/components/curated-sets.mjs
Normal file
536
sites/shared/components/curated-sets.mjs
Normal file
|
@ -0,0 +1,536 @@
|
|||
// __SDEFILE__ - This file is a dependency for the stand-alone environment
|
||||
// Dependencies
|
||||
import { useState, useEffect, useContext } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { freeSewingConfig as conf, controlLevels } from 'shared/config/freesewing.config.mjs'
|
||||
import { siteConfig } from 'site/site.config.mjs'
|
||||
import {
|
||||
horFlexClasses,
|
||||
objUpdate,
|
||||
shortDate,
|
||||
cloudflareImageUrl,
|
||||
capitalize,
|
||||
notEmpty,
|
||||
} 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'
|
||||
// Hooks
|
||||
import { useAccount } from 'shared/hooks/use-account.mjs'
|
||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
// Context
|
||||
import { ModalContext } from 'shared/context/modal-context.mjs'
|
||||
import { LoadingStatusContext } from 'shared/context/loading-status-context.mjs'
|
||||
// Components
|
||||
import { DisplayRow } from './account/shared.mjs'
|
||||
import { ModalWrapper } from 'shared/components/wrappers/modal.mjs'
|
||||
import { Mdx } from 'shared/components/mdx/dynamic.mjs'
|
||||
import Timeago from 'react-timeago'
|
||||
import { MeasieVal } from './account/sets.mjs'
|
||||
import {
|
||||
TrashIcon,
|
||||
CameraIcon,
|
||||
UploadIcon,
|
||||
OkIcon,
|
||||
NoIcon,
|
||||
BoolNoIcon,
|
||||
BoolYesIcon,
|
||||
} from 'shared/components/icons.mjs'
|
||||
import { Link, PageLink } from 'shared/components/link.mjs'
|
||||
import {
|
||||
StringInput,
|
||||
PassiveImageInput,
|
||||
MarkdownInput,
|
||||
MeasieInput,
|
||||
DesignDropdown,
|
||||
ListInput,
|
||||
ns as inputNs,
|
||||
} from 'shared/components/inputs.mjs'
|
||||
|
||||
export const ns = ['account', 'patterns', 'status', 'measurements', 'sets', inputNs]
|
||||
|
||||
const SetLineup = ({ sets = [], href = false, onClick = false }) => (
|
||||
<div
|
||||
className={`w-full flex flex-row ${
|
||||
sets.length > 1 ? 'justify-start px-8' : 'justify-center'
|
||||
} overflow-x-scroll`}
|
||||
style={{
|
||||
backgroundImage: `url(/img/lineup-backdrop.svg)`,
|
||||
width: 'auto',
|
||||
backgroundSize: 'auto 100%',
|
||||
backgroundRepeat: 'repeat-x',
|
||||
}}
|
||||
>
|
||||
{sets.map((set) => {
|
||||
const props = {
|
||||
className: 'aspect-[1/3] w-auto h-96',
|
||||
style: {
|
||||
backgroundImage: `url(${cloudflareImageUrl({ id: set.img, type: 'lineup' })})`,
|
||||
width: 'auto',
|
||||
backgroundSize: 'contain',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundPosition: 'center',
|
||||
},
|
||||
}
|
||||
if (onClick) props.onClick = () => onClick(set)
|
||||
else if (typeof href === 'function') props.href = href(set.id)
|
||||
|
||||
if (onClick) return <button {...props} key={set.id}></button>
|
||||
else if (href) return <Link {...props} key={set.id}></Link>
|
||||
else return <div {...props} key={set.id}></div>
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
|
||||
const ShowCuratedSet = ({ cset }) => {
|
||||
const { control } = useAccount()
|
||||
const { t, i18n } = useTranslation(ns)
|
||||
const lang = i18n.language
|
||||
const { setModal } = useContext(ModalContext)
|
||||
|
||||
if (!cset) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2>{cset[`name${capitalize(lang)}`]}</h2>
|
||||
<div className="w-full">
|
||||
<SetLineup sets={[cset]} />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap flex-row gap-2 w-full mt-4 items-center justify-end">
|
||||
{control > 3 && (
|
||||
<>
|
||||
<a
|
||||
className="badge badge-secondary font-bold badge-lg"
|
||||
href={`${conf.backend}/curated-sets/${cset.id}.json`}
|
||||
>
|
||||
JSON
|
||||
</a>
|
||||
<a
|
||||
className="badge badge-success font-bold badge-lg"
|
||||
href={`${conf.backend}/curated-sets/${cset.id}.yaml`}
|
||||
>
|
||||
YAML
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
onClick={() =>
|
||||
setModal(
|
||||
<ModalWrapper flex="col" justify="top lg:justify-center" slideFrom="right">
|
||||
<img src={cloudflareImageUrl({ type: 'lineup', id: cset.img })} />
|
||||
</ModalWrapper>
|
||||
)
|
||||
}
|
||||
className={`btn btn-secondary btn-outline flex flex-row items-center justify-between w-48 grow-0`}
|
||||
>
|
||||
<CameraIcon />
|
||||
{t('showImage')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<h2>{t('data')}</h2>
|
||||
<DisplayRow title={t('name')}>{cset[`name${capitalize(lang)}`]}</DisplayRow>
|
||||
{control >= controlLevels.sets.notes && (
|
||||
<DisplayRow title={t('notes')}>
|
||||
<Mdx md={cset[`notes${capitalize(lang)}`]} />
|
||||
</DisplayRow>
|
||||
)}
|
||||
{control >= controlLevels.sets.createdAt && (
|
||||
<DisplayRow title={t('created')}>
|
||||
<Timeago date={cset.createdAt} />
|
||||
<span className="px-2 opacity-50">|</span>
|
||||
{shortDate(i18n.language, cset.createdAt, false)}
|
||||
</DisplayRow>
|
||||
)}
|
||||
{control >= controlLevels.sets.updatedAt && (
|
||||
<DisplayRow title={t('updated')}>
|
||||
<Timeago date={cset.updatedAt} />
|
||||
<span className="px-2 opacity-50">|</span>
|
||||
{shortDate(i18n.language, cset.updatedAt, false)}
|
||||
</DisplayRow>
|
||||
)}
|
||||
{control >= controlLevels.sets.id && <DisplayRow title={t('id')}>{cset.id}</DisplayRow>}
|
||||
|
||||
<h2>{t('measies')}</h2>
|
||||
{Object.entries(cset.measies).map(([m, val]) =>
|
||||
val > 0 ? (
|
||||
<DisplayRow title={<MeasieVal m={m} val={val} />} key={m}>
|
||||
<span className="font-medium">{t(m)}</span>
|
||||
</DisplayRow>
|
||||
) : null
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const CuratedSet = ({ id }) => {
|
||||
// Hooks
|
||||
const { setLoadingStatus } = useContext(LoadingStatusContext)
|
||||
const backend = useBackend()
|
||||
|
||||
// State
|
||||
const [cset, setCset] = useState()
|
||||
|
||||
// Effect
|
||||
useEffect(() => {
|
||||
const getCset = async () => {
|
||||
setLoadingStatus([true, 'status:contactingBackend'])
|
||||
const result = await backend.getCuratedSet(id)
|
||||
if (result.success) {
|
||||
setCset(result.data.curatedSet)
|
||||
setLoadingStatus([true, 'status:dataLoaded', true, true])
|
||||
} else setLoadingStatus([true, 'status:backendError', true, false])
|
||||
}
|
||||
if (id) getCset()
|
||||
}, [id])
|
||||
|
||||
if (!id || !cset) return null
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl">
|
||||
<ShowCuratedSet cset={cset} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Picker version
|
||||
export const CuratedSetPicker = (props) => <CuratedSets {...props} />
|
||||
|
||||
// Component for the curated-sets page
|
||||
export const CuratedSets = ({ href = false, clickHandler = false }) => {
|
||||
// Hooks
|
||||
const backend = useBackend()
|
||||
const { setLoadingStatus } = useContext(LoadingStatusContext)
|
||||
|
||||
// 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[set.id] = set
|
||||
setSets(allSets)
|
||||
setLoadingStatus([true, 'status:dataLoaded', true, true])
|
||||
} else setLoadingStatus([true, 'status:backendError', true, false])
|
||||
}
|
||||
getSets()
|
||||
}, [])
|
||||
|
||||
const lineupProps = {
|
||||
sets: Object.values(sets),
|
||||
}
|
||||
if (typeof href === 'function') lineupProps.href = href
|
||||
else lineupProps.onClick = clickHandler ? clickHandler : (set) => setSelected(set.id)
|
||||
|
||||
return (
|
||||
<div className="max-w-7xl xl:pl-4">
|
||||
<SetLineup {...lineupProps} />
|
||||
{selected && <ShowCuratedSet cset={sets[selected]} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 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 }) => {
|
||||
// Hooks
|
||||
const { account } = useAccount()
|
||||
const { setLoadingStatus } = useContext(LoadingStatusContext)
|
||||
const backend = useBackend()
|
||||
const { t } = useTranslation(ns)
|
||||
|
||||
const [filter, setFilter] = useState(false)
|
||||
const [cset, setCset] = useState()
|
||||
const [data, setData] = useState({})
|
||||
|
||||
// Effect
|
||||
useEffect(() => {
|
||||
const getCuratedSet = async () => {
|
||||
setLoadingStatus([true, t('backendLoadingStarted')])
|
||||
const result = await backend.getCuratedSet(id)
|
||||
if (result.success) {
|
||||
setCset(result.data.curatedSet)
|
||||
const initData = {
|
||||
img: result.data.curatedSet.img,
|
||||
published: result.data.curatedSet.published,
|
||||
measies: { ...result.data.curatedSet.measies },
|
||||
}
|
||||
for (const lang of siteConfig.languages) {
|
||||
let k = `name${capitalize(lang)}`
|
||||
initData[k] = result.data.curatedSet[k]
|
||||
k = `notes${capitalize(lang)}`
|
||||
initData[k] = result.data.curatedSet[k]
|
||||
}
|
||||
setData(initData)
|
||||
setLoadingStatus([true, 'backendLoadingCompleted', true, true])
|
||||
} else setLoadingStatus([true, 'backendError', true, false])
|
||||
}
|
||||
if (id) getCuratedSet()
|
||||
}, [id])
|
||||
|
||||
const filterMeasurements = () => {
|
||||
if (!filter) return measurements.map((m) => t(`measurements:${m}`) + `|${m}`).sort()
|
||||
else return designMeasurements[filter].map((m) => t(`measurements:${m}`) + `|${m}`).sort()
|
||||
}
|
||||
|
||||
if (!id || !cset) return <p>nope {id}</p> //null
|
||||
|
||||
const updateData = (path, val) => setData(objUpdate({ ...data }, path, val))
|
||||
|
||||
const save = async () => {
|
||||
setLoadingStatus([true, 'savingSet'])
|
||||
const changes = { measies: {} }
|
||||
for (const lang of siteConfig.languages) {
|
||||
let k = `name${capitalize(lang)}`
|
||||
if (data[k] !== cset[k]) changes[k] = data[k]
|
||||
k = `notes${capitalize(lang)}`
|
||||
if (data[k] !== cset[k]) changes[k] = data[k]
|
||||
}
|
||||
if (data.img !== cset.img) changes.img = data.img
|
||||
if (data.published !== cset.published) changes.published = data.published
|
||||
for (const m in data.measies) {
|
||||
if (data.measies[m] !== cset.measies[m]) changes.measies[m] = data.measies[m]
|
||||
}
|
||||
const result = await backend.updateCuratedSet(cset.id, changes)
|
||||
if (result.success) {
|
||||
setCset(result.data.curatedSet)
|
||||
setLoadingStatus([true, 'nailedIt', true, true])
|
||||
} else setLoadingStatus([true, 'backendError', true, false])
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl">
|
||||
<PageLink href={`/curated-sets/${id}`} txt={`/curated-sets/${id}`} />
|
||||
<ListInput
|
||||
label={t('curate:publshed')}
|
||||
update={(val) => updateData('published', val)}
|
||||
list={[
|
||||
{
|
||||
val: true,
|
||||
label: (
|
||||
<div className="flex flex-row items-center flex-wrap justify-between w-full">
|
||||
<span>{t('curate:published')}</span>
|
||||
<OkIcon className="w-8 h-8 text-success bg-base-100 rounded-full p-1" stroke={4} />
|
||||
</div>
|
||||
),
|
||||
desc: t('curate:publishedDesc'),
|
||||
},
|
||||
{
|
||||
val: false,
|
||||
label: (
|
||||
<div className="flex flex-row items-center flex-wrap justify-between w-full">
|
||||
<span>{t('curate:unpublished')}</span>
|
||||
<NoIcon className="w-8 h-8 text-error bg-base-100 rounded-full p-1" stroke={3} />
|
||||
</div>
|
||||
),
|
||||
desc: t('curate:unpublishedDesc'),
|
||||
},
|
||||
]}
|
||||
current={data.published}
|
||||
/>
|
||||
|
||||
<h2 id="measies">{t('measies')}</h2>
|
||||
<div className="bg-secondary px-4 pt-1 pb-4 rounded-lg shadow bg-opacity-10">
|
||||
<DesignDropdown
|
||||
update={setFilter}
|
||||
label={t('filterByDesign')}
|
||||
current={filter}
|
||||
firstOption={<option value="">{t('noFilter')}</option>}
|
||||
/>
|
||||
</div>
|
||||
{filterMeasurements().map((mplus) => {
|
||||
const [translated, m] = mplus.split('|')
|
||||
|
||||
return (
|
||||
<MeasieInput
|
||||
id={`measie-${m}`}
|
||||
key={m}
|
||||
m={m}
|
||||
imperial={account.imperial}
|
||||
label={translated}
|
||||
current={data.measies?.[m]}
|
||||
original={cset.measies?.[m]}
|
||||
update={(name, val) => updateData(['measies', m], val)}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
|
||||
<h2>{t('account:data')}</h2>
|
||||
<h3>{t('account:name')}</h3>
|
||||
|
||||
{/* Name is always shown */}
|
||||
{siteConfig.languages.map((lang) => {
|
||||
const key = `name${capitalize(lang)}`
|
||||
|
||||
return (
|
||||
<StringInput
|
||||
key={key}
|
||||
label={`${t('account:name')} (${lang.toUpperCase()})`}
|
||||
update={(val) => updateData(key, val)}
|
||||
current={data[key]}
|
||||
valid={notEmpty}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
|
||||
<h3>{t('account:notes')}</h3>
|
||||
|
||||
{/* notes: Control level determines whether or not to show this */}
|
||||
{siteConfig.languages.map((lang) => {
|
||||
const key = `notes${capitalize(lang)}`
|
||||
|
||||
return (
|
||||
<MarkdownInput
|
||||
key={lang}
|
||||
label={`${t('account:notes')} (${lang.toUpperCase()})`}
|
||||
update={(val) => updateData(key, val)}
|
||||
current={data[key]}
|
||||
placeholder={t('mdSupport')}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
|
||||
{/* img: Control level determines whether or not to show this */}
|
||||
<PassiveImageInput
|
||||
label={t('curate:img')}
|
||||
update={(val) => updateData('img', val)}
|
||||
current={data.img}
|
||||
valid={notEmpty}
|
||||
/>
|
||||
|
||||
<button onClick={save} className={`btn btn-primary btn-lg ${horFlexClasses} mt-8`}>
|
||||
<UploadIcon />
|
||||
{t('saveThing', { thing: t('curate:curatedSet') })}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
147
sites/shared/components/designs/design-picker.mjs
Normal file
147
sites/shared/components/designs/design-picker.mjs
Normal file
|
@ -0,0 +1,147 @@
|
|||
import { designs, tags, techniques } from 'shared/config/designs.mjs'
|
||||
import { Design, DesignLink, ns as designNs } from 'shared/components/designs/design.mjs'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { ShowcaseIcon, CisFemaleIcon, ResetIcon } from 'shared/components/icons.mjs'
|
||||
import { useAtom } from 'jotai'
|
||||
import { atomWithHash } from 'jotai-location'
|
||||
import { capitalize, objUpdate } from 'shared/utils.mjs'
|
||||
import { Difficulty } from 'shared/components/designs/difficulty.mjs'
|
||||
|
||||
export const ns = designNs
|
||||
|
||||
const filterAtom = atomWithHash('filter', { example: true })
|
||||
|
||||
export const useFilter = () => {
|
||||
return useAtom(filterAtom)
|
||||
}
|
||||
|
||||
export const DesignPicker = ({ linkTo = 'new', altLinkTo = 'docs' }) => {
|
||||
const { t } = useTranslation('designs')
|
||||
const [filter, setFilter] = useFilter()
|
||||
|
||||
// Need to sort designs by their translated title
|
||||
// let's also apply the filters while we're at it
|
||||
const translated = {}
|
||||
for (const d in designs) {
|
||||
let skip = false
|
||||
if (
|
||||
filter.tag &&
|
||||
filter.tag.filter((tag) => designs[d].tags.includes(tag)).length < filter.tag.length
|
||||
)
|
||||
skip = true
|
||||
if (
|
||||
filter.tech &&
|
||||
filter.tech.filter((tech) => designs[d].techniques.includes(tech)).length < filter.tech.length
|
||||
)
|
||||
skip = true
|
||||
if (filter.difficulty && filter.difficulty !== designs[d].difficulty) skip = true
|
||||
if (!skip) translated[t(`${d}.t`)] = d
|
||||
}
|
||||
|
||||
const updateFilter = (path, val) => {
|
||||
const newFilter = objUpdate({ ...filter }, path, val)
|
||||
setFilter(newFilter)
|
||||
}
|
||||
|
||||
const toggle = (type, val) => {
|
||||
const current = filter[type] || []
|
||||
const newSet = new Set(current)
|
||||
if (newSet.has(val)) newSet.delete(val)
|
||||
else newSet.add(val)
|
||||
updateFilter(type, [...newSet])
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="max-w-7xl m-auto">
|
||||
<div className="flex flex-row flex-wrap gap-1 justify-center font-medium">
|
||||
{Object.values(translated)
|
||||
.sort()
|
||||
.map((d) => (
|
||||
<DesignLink key={d} linkTo={linkTo} altLinkTo={altLinkTo} name={capitalize(d)} />
|
||||
))}
|
||||
</div>
|
||||
<h6 className="text-center mb-0 mt-4">
|
||||
Filters ({Object.keys(translated).length}/{Object.keys(designs).length})
|
||||
</h6>
|
||||
<div className="flex flex-row gap-1 items-center justify-center flex-wrap my-2">
|
||||
<b>{t('tags:tags')}:</b>
|
||||
{tags.map((tag) => (
|
||||
<button
|
||||
key={tag}
|
||||
className={`badge font-medium hover:shadow
|
||||
${
|
||||
filter?.tag && Array.isArray(filter.tag) && filter.tag.includes(tag)
|
||||
? 'badge badge-success hover:badge-error'
|
||||
: 'badge badge-primary hover:badge-success'
|
||||
}`}
|
||||
onClick={() => toggle('tag', tag)}
|
||||
>
|
||||
{t(`tags:${tag}`)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-row gap-1 items-center justify-center flex-wrap my-4">
|
||||
<b>{t('techniques:techniques')}:</b>
|
||||
{techniques.map((tech) => (
|
||||
<button
|
||||
key={tech}
|
||||
className={`badge font-medium hover:shadow
|
||||
${
|
||||
filter?.tech && Array.isArray(filter.tech) && filter.tech.includes(tech)
|
||||
? 'badge badge-success hover:badge-error'
|
||||
: 'badge badge-accent hover:badge-success'
|
||||
}`}
|
||||
onClick={() => toggle('tech', tech)}
|
||||
>
|
||||
{t(`techniques:${tech}`)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-row gap-2 items-center justify-center flex-wrap my-4">
|
||||
<b>{t('tags:difficulty')}:</b>
|
||||
{[1, 2, 3, 4, 5].map((score) => (
|
||||
<button
|
||||
onClick={() => updateFilter('difficulty', score)}
|
||||
key={score}
|
||||
className={`btn btn-sm ${
|
||||
filter.difficulty === score ? 'btn-secondary btn-outline' : 'btn-ghost'
|
||||
}`}
|
||||
>
|
||||
<Difficulty score={score} />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-row gap-4 items-center justify-center flex-wrap my-2">
|
||||
<button
|
||||
className="btn btn-secondary btn-outline"
|
||||
onClick={() => updateFilter('example', !filter.example)}
|
||||
>
|
||||
{filter.example ? <CisFemaleIcon /> : <ShowcaseIcon />}
|
||||
{filter.example ? t('tags:showLineDrawings') : t('tags:showExamples')}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary btn-outline"
|
||||
onClick={() => setFilter({ example: 1 })}
|
||||
>
|
||||
<ResetIcon />
|
||||
{t('tags:clearFilter')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-2 mt-4 justify-center sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5">
|
||||
{Object.keys(translated)
|
||||
.sort()
|
||||
.map((d) => (
|
||||
<Design
|
||||
name={translated[d]}
|
||||
key={d}
|
||||
linkTo={linkTo}
|
||||
altLinkTo={altLinkTo}
|
||||
lineDrawing={filter.example ? false : true}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
139
sites/shared/components/designs/design.mjs
Normal file
139
sites/shared/components/designs/design.mjs
Normal file
|
@ -0,0 +1,139 @@
|
|||
import Link from 'next/link'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { Difficulty } from 'shared/components/designs/difficulty.mjs'
|
||||
import { designs } from 'shared/config/designs.mjs'
|
||||
import { lineDrawings } from 'shared/components/designs/linedrawings/index.mjs'
|
||||
import { designImages } from 'shared/components/designs/examples.mjs'
|
||||
import { linkClasses } from 'shared/components/link.mjs'
|
||||
import { capitalize } from 'shared/utils.mjs'
|
||||
import { DocsIcon, NewPatternIcon } from 'shared/components/icons.mjs'
|
||||
|
||||
export const ns = ['designs', 'tags', 'techniques']
|
||||
|
||||
export const linkBuilders = {
|
||||
new: (design) => `/new/${design.toLowerCase()}`,
|
||||
docs: (design) => `/docs/designs/${design.toLowerCase()}`,
|
||||
}
|
||||
|
||||
export const DesignTechnique = ({ technique }) => {
|
||||
const { t } = useTranslation('techniques')
|
||||
|
||||
return (
|
||||
<Link
|
||||
className="badge badge-accent hover:badge-secondary hover:shadow font-medium"
|
||||
href={`/designs/techniques/${technique}`}
|
||||
>
|
||||
{t(`techniques:${technique}`)}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export const DesignTag = ({ tag }) => {
|
||||
const { t } = useTranslation(['tags'])
|
||||
|
||||
return (
|
||||
<Link
|
||||
className="badge badge-primary hover:badge-secondary hover:shadow font-medium"
|
||||
href={`/designs/tags/${tag}`}
|
||||
>
|
||||
{t(tag)}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export const DesignLink = ({ name, linkTo = 'new', className = linkClasses }) => (
|
||||
<Link href={linkBuilders[linkTo](name)} className={className}>
|
||||
{name}
|
||||
</Link>
|
||||
)
|
||||
|
||||
export const Design = ({ name, linkTo = 'new', altLinkTo = 'docs', lineDrawing = false }) => {
|
||||
const { t } = useTranslation(ns)
|
||||
|
||||
const LineDrawing =
|
||||
lineDrawing && lineDrawings[name]
|
||||
? lineDrawings[name]
|
||||
: ({ className }) => <div className={className}></div>
|
||||
const exampleImageUrl = designImages[name]
|
||||
? designImages[name]
|
||||
: 'https://images.pexels.com/photos/5626595/pexels-photo-5626595.jpeg?cs=srgb&dl=pexels-frida-toth-5626595.jpg&fm=jpg&w=640&h=427&_gl=1*vmxq7y*_ga*MTM0OTk5OTY4NS4xNjYxMjUyMjc0*_ga_8JE65Q40S6*MTY5NTU1NDc0Mi4yNS4xLjE2OTU1NTU1NjIuMC4wLjA.'
|
||||
|
||||
const bg = lineDrawing
|
||||
? {}
|
||||
: {
|
||||
backgroundImage: `url(${exampleImageUrl}`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center center',
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className={`flex flex-col flex-nowrap items-start justify-start gap-2 h-auto w-full
|
||||
btn btn-ghost border border-neutral p-0 border-b-none
|
||||
hover:border hover:border-secondary rounded-b-none
|
||||
relative`}
|
||||
style={bg}
|
||||
>
|
||||
<Link
|
||||
href={linkBuilders[linkTo](name)}
|
||||
className="w-full h-full before:absolute before:inset-y-0 before:inset-x-0"
|
||||
>
|
||||
<h5
|
||||
className={`flex flex-row items-center justify-between w-full w-full pt-2 px-4 rounded-t-lg m-0
|
||||
${lineDrawing ? '' : 'bg-neutral text-neutral-content bg-opacity-70'}`}
|
||||
>
|
||||
<span>{t(`designs:${name}.t`)}</span>
|
||||
<span className="flex flex-col items-end">
|
||||
<span className="text-xs font-medium opacity-70">{t('tags:difficulty')}</span>
|
||||
<Difficulty score={designs[name].difficulty} />
|
||||
</span>
|
||||
</h5>
|
||||
<div className={lineDrawing ? 'py-8' : 'py-24'}>
|
||||
<LineDrawing className="h-64 max-w-full m-auto my-4 text-base-content" />
|
||||
</div>
|
||||
</Link>
|
||||
<div
|
||||
className={`pt-0 m-0 -mt-2 text-center w-full
|
||||
${
|
||||
lineDrawing
|
||||
? 'bg-transparent text-base-content'
|
||||
: 'bg-neutral text-neutral-content bg-opacity-70'
|
||||
}`}
|
||||
>
|
||||
<p className={`normal-case text-inherit font-medium text-center grow m-0 px-2`}>
|
||||
{t(`designs:${name}.d`)}
|
||||
</p>
|
||||
<div className="flex flex-row flex-wrap gap-2 relative z-10 px-2 items-center justify-center">
|
||||
{designs[name].tags.map((tag) => (
|
||||
<DesignTag key={tag} tag={tag} />
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-row flex-wrap gap-2 relative z-10 p-2 items-center justify-center">
|
||||
{designs[name].techniques.map((technique) => (
|
||||
<DesignTechnique key={technique} technique={technique} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{altLinkTo ? (
|
||||
<Link
|
||||
className="btn btn-secondary w-full rounded-t-none flex flex-row items-center justify-between px-4"
|
||||
href={linkBuilders[altLinkTo](name)}
|
||||
>
|
||||
{altLinkTo === 'docs' ? (
|
||||
<>
|
||||
<DocsIcon />
|
||||
<span>{t('learnMoreAboutThing', { thing: capitalize(name) })}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<NewPatternIcon />
|
||||
<span>{t('newThingPattern', { thing: capitalize(name) })}</span>
|
||||
</>
|
||||
)}
|
||||
</Link>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
23
sites/shared/components/designs/difficulty.mjs
Normal file
23
sites/shared/components/designs/difficulty.mjs
Normal file
|
@ -0,0 +1,23 @@
|
|||
// __SDEFILE__ - This file is a dependency for the stand-alone environment
|
||||
import { CircleIcon as Icon } from 'shared/components/icons.mjs'
|
||||
import { useTheme } from 'shared/hooks/use-theme.mjs'
|
||||
|
||||
export const Circle = ({ off = false, color = 'warning' }) => (
|
||||
<Icon fill={!off} className={`w-4 h-4 text-${color}`} />
|
||||
)
|
||||
|
||||
const five = [0, 1, 2, 3, 4]
|
||||
|
||||
export const Difficulty = ({ score = 0, color = false }) => {
|
||||
const { rating } = useTheme()
|
||||
// Skip 0
|
||||
const colors = ['violet', ...rating]
|
||||
|
||||
return (
|
||||
<div className="flex flex-row">
|
||||
{five.map((i) => (
|
||||
<Circle key={i} color={color ? color : colors[score]} off={i < score ? false : true} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
55
sites/shared/components/designs/examples.mjs
Normal file
55
sites/shared/components/designs/examples.mjs
Normal file
|
@ -0,0 +1,55 @@
|
|||
import { cloudflareImageUrl } from 'shared/utils.mjs'
|
||||
|
||||
export const designExamples = {
|
||||
aaron: 'showcase-tight-aaron',
|
||||
albert: 'showcase-albert-by-wouter',
|
||||
bee: 'd0312c99-7b2f-4e37-c131-d626d34cd300',
|
||||
bella: 'showcase-bella-by-karen',
|
||||
benjamin: 'showcase-benjamin-by-anto',
|
||||
bent: 'showcase-linnen-jaeger-by-paul',
|
||||
bob: 'showcase-bob-bibs-by-duck',
|
||||
breanna: 'showcase-magicantace-breanna-sandy',
|
||||
//brian: '',
|
||||
bruce: 'showcase-nsfw-bruce',
|
||||
cathrin: 'showcase-green-cathrin',
|
||||
carlton: 'showcase-carlton-by-boris',
|
||||
carlita: 'showcase-quentin-carlita',
|
||||
charlie: 'showcase-charlie-by-joost',
|
||||
cornelius: 'showcase-cornelius-by-wouter',
|
||||
diana: 'showcase-diana-by-deby',
|
||||
florence: 'showcase-rowans-leaf-print-florence',
|
||||
florent: 'showcase-florent-by-enno',
|
||||
hi: 'showcase-hi-the-shark-has-our-hearts',
|
||||
holmes: 'showcase-a-modified-holmes',
|
||||
hortensia: 'showcase-hortensia-by-saber',
|
||||
huey: 'showcase-anneke-huey',
|
||||
hugo: 'showcase-husband-hugo',
|
||||
jaeger: 'showcase-jaeger-by-roberta',
|
||||
lucy: 'showcase-houseoflief-lucy',
|
||||
lunetius: 'showcase-lunetius-the-lacerna',
|
||||
//magde: '',
|
||||
//naomiwu: '',
|
||||
noble: 'showcase-a-casual-test-of-noble',
|
||||
octoplushy: 'showcase-meet-octoplushy',
|
||||
paco: 'showcase-paco-by-karen',
|
||||
penelope: 'showcase-pregnant-lady-penelope',
|
||||
sandy: 'showcase-sandy-by-anneke',
|
||||
shin: 'showcase-just-peachy-shin-bee',
|
||||
simon: 'showcase-simon-shirt-by-sewingdentist',
|
||||
//simone: '',
|
||||
sven: 'showcase-french-terry-sven',
|
||||
tamiko: 'showcase-a-tamiko-top',
|
||||
teagan: 'showcase-teagan-karen',
|
||||
tiberius: 'showcase-tiberius-the-tunica',
|
||||
titan: 'showcase-a-mock-up-of-titan-with-the-fit-to-knee-option-enabled',
|
||||
trayvon: 'showcase-liberty-trayvon',
|
||||
uma: 'showcase-lower-rise-ursula',
|
||||
wahid: 'showcase-sterling42-wahid',
|
||||
walburga: 'showcase-walburga-the-wappenrock',
|
||||
waralee: 'fde729f5-ea72-4af4-b798-331bbce04000',
|
||||
yuri: 'showcase-yuri-by-its-designer',
|
||||
}
|
||||
|
||||
export const designImages = {}
|
||||
for (const [design, id] of Object.entries(designExamples))
|
||||
designImages[design] = cloudflareImageUrl({ id, variant: 'sq500' })
|
330
sites/shared/components/designs/info.mjs
Normal file
330
sites/shared/components/designs/info.mjs
Normal file
|
@ -0,0 +1,330 @@
|
|||
// Dependencies
|
||||
import {
|
||||
nsMerge,
|
||||
capitalize,
|
||||
optionsMenuStructure,
|
||||
optionType,
|
||||
cloudflareImageUrl,
|
||||
} from 'shared/utils.mjs'
|
||||
import { designs } from 'shared/config/designs.mjs'
|
||||
import { examples } from 'site/components/design-examples.mjs'
|
||||
// Hooks
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { useDesign } from 'site/hooks/use-design.mjs'
|
||||
import { useContext, Fragment } from 'react'
|
||||
// Context
|
||||
import { ModalContext } from 'shared/context/modal-context.mjs'
|
||||
// Components
|
||||
import { ModalWrapper } from 'shared/components/wrappers/modal.mjs'
|
||||
import { lineDrawings } from 'shared/components/designs/linedrawings/index.mjs'
|
||||
import { ns as designNs } from 'shared/components/designs/design.mjs'
|
||||
import { Difficulty } from 'shared/components/designs/difficulty.mjs'
|
||||
import { PageLink, AnchorLink, Link } from 'shared/components/link.mjs'
|
||||
import { DocsLink, DocsTitle } from 'shared/components/mdx/docs-helpers.mjs'
|
||||
import { Popout } from 'shared/components/popout/index.mjs'
|
||||
import { NewPatternIcon } from 'shared/components/icons.mjs'
|
||||
|
||||
// Translation namespaces used on this page
|
||||
export const ns = nsMerge(
|
||||
designNs,
|
||||
'account',
|
||||
'tags',
|
||||
'techniques',
|
||||
'measurements',
|
||||
'workbench',
|
||||
'designs',
|
||||
'tags'
|
||||
)
|
||||
|
||||
const Option = ({ id, option, design }) =>
|
||||
optionType(option) === 'constant' ? null : (
|
||||
<li key={option.name}>
|
||||
<DocsLink site="org" slug={`docs/designs/${design}/options/${id.toLowerCase()}`} />
|
||||
</li>
|
||||
)
|
||||
|
||||
const OptionGroup = ({ id, group, t, design }) => (
|
||||
<li key={id}>
|
||||
<b>{t(`workbench:${id}`)}</b>
|
||||
<ul className="list list-inside list-disc pl-2">
|
||||
{Object.entries(group).map(([sid, entry]) =>
|
||||
entry.isGroup ? (
|
||||
<OptionGroup id={sid} key={sid} t={t} group={entry} desing={design} />
|
||||
) : (
|
||||
<Option key={sid} id={sid} option={entry} design={design} />
|
||||
)
|
||||
)}
|
||||
</ul>
|
||||
</li>
|
||||
)
|
||||
export const SimpleOptionsList = ({ options, t, design }) => {
|
||||
const structure = optionsMenuStructure(options, {})
|
||||
const output = []
|
||||
for (const [key, entry] of Object.entries(structure)) {
|
||||
const shared = { key, t, design, id: key }
|
||||
if (entry.isGroup) output.push(<OptionGroup {...shared} group={entry} />)
|
||||
else output.push(<Option {...shared} option={entry} />)
|
||||
}
|
||||
|
||||
return <ul className="list list-inside pl-2 list-disc">{output}</ul>
|
||||
}
|
||||
|
||||
export const DesignInfo = ({ design, docs = false, workbench = false }) => {
|
||||
const { setModal } = useContext(ModalContext)
|
||||
const { t, i18n } = useTranslation([...ns, design])
|
||||
const { language } = i18n
|
||||
const Design = useDesign(design)
|
||||
const config = Design.patternConfig
|
||||
|
||||
// Translate measurements
|
||||
const measies = { required: {}, optional: {} }
|
||||
if (config?.measurements) {
|
||||
for (const m of config.measurements) measies.required[m] = t(`measurements:${m}`)
|
||||
}
|
||||
if (config?.optionalMeasurements) {
|
||||
for (const m of config.optionalMeasurements) measies.optional[m] = t(`measurements:${m}`)
|
||||
}
|
||||
|
||||
// Linedrawing
|
||||
const LineDrawing = lineDrawings[design]
|
||||
? lineDrawings[design]
|
||||
: ({ className }) => <div className={className}></div>
|
||||
|
||||
// Docs content
|
||||
const docsContent = (
|
||||
<>
|
||||
<h2 id="docs">{t('account:docs')}</h2>
|
||||
<ul className="list list-disc list-inside pl-2">
|
||||
<li>
|
||||
<DocsLink site="org" slug={`docs/designs/${design}/cutting`} />
|
||||
</li>
|
||||
<li>
|
||||
<DocsLink site="org" slug={`docs/designs/${design}/instructions`} />
|
||||
</li>
|
||||
<li>
|
||||
<DocsLink site="org" slug={`docs/designs/${design}/needs`} />
|
||||
</li>
|
||||
<li>
|
||||
<DocsLink site="org" slug={`docs/designs/${design}/fabric`} />
|
||||
</li>
|
||||
<li>
|
||||
<DocsLink site="org" slug={`docs/designs/${design}/options`} />
|
||||
</li>
|
||||
<li>
|
||||
<DocsLink site="org" slug={`docs/designs/${design}/notes`} />
|
||||
</li>
|
||||
</ul>
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<h5 className="-mt-6 text-accent font-medium">#FreeSewing{capitalize(design)}</h5>
|
||||
<p className="text-xl">{t(`designs:${design}.d`)}</p>
|
||||
{workbench ? null : (
|
||||
<Link className="btn btn-primary btn items-center gap-8" href={`/new/${design}`}>
|
||||
<NewPatternIcon />
|
||||
{t('tags:newThingPattern', { thing: capitalize(design) })}
|
||||
</Link>
|
||||
)}
|
||||
{docs || workbench ? null : (
|
||||
<div className="flex flex-row flex-wrap gap-2 md:gap-4 items-center p-4 border rounded-lg bg-secondary bg-opacity-5 max-w-4xl">
|
||||
<b>Jump to:</b>
|
||||
<AnchorLink id="notes">
|
||||
<DocsTitle
|
||||
slug={`docs/designs/${design}/notes`}
|
||||
language={language}
|
||||
format={(t) => t.split(':').pop().trim()}
|
||||
/>
|
||||
</AnchorLink>
|
||||
{examples && <AnchorLink id="examples" txt={t('acount:examples')} />}
|
||||
{['needs', 'fabric'].map((page) => (
|
||||
<AnchorLink id={page} key={page}>
|
||||
<DocsTitle
|
||||
slug={`docs/designs/${design}/${page}`}
|
||||
language={language}
|
||||
format={(t) => t.split(':').pop().trim()}
|
||||
/>
|
||||
</AnchorLink>
|
||||
))}
|
||||
<AnchorLink id="docs" txt={t('account:docs')} />
|
||||
<AnchorLink id="specs" txt={t('account:specifications')} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={`mt-8 w-full ${docs ? '' : 'flex flex-row flex-wrap justify-between'}`}>
|
||||
<div className={`w-full max-w-2xl ${docs ? '' : 'md:w-2/3 pr-0 md:pr-8'}`}>
|
||||
<LineDrawing className="w-full text-base-content" />
|
||||
{docs ? null : (
|
||||
<>
|
||||
<h2 id="notes">
|
||||
<DocsTitle
|
||||
slug={`docs/designs/${design}/notes`}
|
||||
language={language}
|
||||
format={(t) => t.split(':').pop().trim()}
|
||||
/>
|
||||
</h2>
|
||||
</>
|
||||
)}
|
||||
{docs ? docsContent : null}
|
||||
{examples ? (
|
||||
<>
|
||||
<h2 id="examples">{t('account:examples')}</h2>
|
||||
{examples[design] ? (
|
||||
<div className="grid grid-cols-2 gap-2 md:grid-cols-3 lg:grid-cols-3">
|
||||
{examples[design].map((ex) => (
|
||||
<button
|
||||
key={ex}
|
||||
onClick={() =>
|
||||
setModal(
|
||||
<ModalWrapper
|
||||
flex="col"
|
||||
justify="top lg:justify-center"
|
||||
slideFrom="right"
|
||||
>
|
||||
<img
|
||||
className="w-full shadow rounded-lg"
|
||||
src={cloudflareImageUrl({ id: `showcase-${ex}`, variant: 'public' })}
|
||||
/>
|
||||
<p className="text-center">
|
||||
<PageLink href={`/showcase/${ex}`} txt={t('account:visitShowcase')} />
|
||||
</p>
|
||||
</ModalWrapper>
|
||||
)
|
||||
}
|
||||
>
|
||||
<img
|
||||
className="w-full shadow rounded-lg"
|
||||
src={cloudflareImageUrl({ id: `showcase-${ex}`, variant: 'sq500' })}
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<Popout note>
|
||||
<h5>{t('account:noExamples')}</h5>
|
||||
<p>{t('account:noExamplesMsg')}</p>
|
||||
<p className="text-right">
|
||||
<Link className="btn btn-primary" href="/new/showcase">
|
||||
{t('account:showcaseNew')}
|
||||
</Link>
|
||||
</p>
|
||||
</Popout>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
{docs
|
||||
? null
|
||||
: ['needs', 'fabric'].map((page) => (
|
||||
<Fragment key={page}>
|
||||
<h2 id={page}>
|
||||
<DocsTitle
|
||||
slug={`docs/designs/${design}/${page}`}
|
||||
language={language}
|
||||
format={(t) => t.split(':').pop().trim()}
|
||||
/>
|
||||
</h2>
|
||||
</Fragment>
|
||||
))}
|
||||
|
||||
{docs ? null : docsContent}
|
||||
</div>
|
||||
|
||||
<div className={`w-full ${docs ? '' : 'md:w-1/3'}`}>
|
||||
<h2 id="specs">{t('account:specifications')}</h2>
|
||||
|
||||
<h6 className="mt-4">{t('account:design')}</h6>
|
||||
<ul>
|
||||
{designs[design].design.map((person) => (
|
||||
<li key={person}>{person}</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<h6 className="mt-4">{t('account:code')}</h6>
|
||||
<ul>
|
||||
{designs[design].code.map((person) => (
|
||||
<li key={person}>{person}</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<h6 className="mt-4">{t('tags:difficulty')}</h6>
|
||||
<Difficulty score={designs[design].difficulty} />
|
||||
|
||||
<h6 className="mt-4">{t('tags:tags')}</h6>
|
||||
<div className="flex flex-row flex-wrap items-center gap-1">
|
||||
{designs[design].tags.map((tag) => (
|
||||
<span className="badge badge-primary font-medium" key={tag}>
|
||||
{t(`tags:${tag}`)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<h6 className="mt-4">{t('techniques:techniques')}</h6>
|
||||
<div className="flex flex-row flex-wrap items-center gap-1">
|
||||
{designs[design].techniques.map((tech) => (
|
||||
<span className="badge badge-accent font-medium" key={tech}>
|
||||
{t(`techniques:${tech}`)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{Object.keys(measies.required).length > 0 ? (
|
||||
<>
|
||||
<h6 className="mt-4">{t('account:requiredMeasurements')}</h6>
|
||||
<ul className="list list-disc list-inside pl-2">
|
||||
{Object.keys(measies.required)
|
||||
.sort()
|
||||
.map((m) => (
|
||||
<li key={m}>
|
||||
<PageLink
|
||||
href={`/docs/measurements/${m.toLowerCase()}`}
|
||||
txt={measies.required[m]}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{Object.keys(measies.optional).length > 0 ? (
|
||||
<>
|
||||
<h6 className="mt-4">{t('account:optionalMeasurements')}</h6>
|
||||
<ul className="list list-disc list-inside pl-2">
|
||||
{Object.keys(measies.optional)
|
||||
.sort()
|
||||
.map((m) => (
|
||||
<li key={m}>
|
||||
<PageLink
|
||||
href={`/docs/measurements/${m.toLowerCase()}`}
|
||||
txt={measies.optional[m]}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<h6 className="mt-4">{t('account:designOptions')}</h6>
|
||||
<SimpleOptionsList options={config.options} t={t} design={design} />
|
||||
|
||||
<h6 className="mt-4">{t('account:parts')}</h6>
|
||||
<ul className="list list-disc list-inside pl-2">
|
||||
{config.draftOrder.map((part) => (
|
||||
<li key={part}>{part}</li>
|
||||
))}
|
||||
</ul>
|
||||
{Object.keys(config.plugins).length > 0 ? (
|
||||
<>
|
||||
<h6 className="mt-4">{t('account:plugins')}</h6>
|
||||
<ul className="list list-disc list-inside pl-2">
|
||||
{Object.keys(config.plugins).map((plugin) => (
|
||||
<li key={plugin}>{plugin}</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
98
sites/shared/components/designs/linedrawings/aaron.mjs
Normal file
98
sites/shared/components/designs/linedrawings/aaron.mjs
Normal file
|
@ -0,0 +1,98 @@
|
|||
import { LineDrawingWrapper, thin, dashed } from './shared.mjs'
|
||||
|
||||
const strokeScale = 0.5
|
||||
|
||||
export const Aaron = ({
|
||||
className = 'w-64', // CSS classes to apply
|
||||
stroke = 1, // Stroke width to use
|
||||
}) => {
|
||||
// Normalize stroke across designs
|
||||
stroke = stroke * strokeScale
|
||||
|
||||
return (
|
||||
<LineDrawingWrapper viewBox="0 0 148 119" {...{ className, stroke }}>
|
||||
<Front stroke={stroke} />
|
||||
<Back stroke={stroke} />
|
||||
</LineDrawingWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
* React component for the front
|
||||
*/
|
||||
export const AaronFront = ({
|
||||
className = 'w-64', // CSS classes to apply
|
||||
stroke = 1, // Stroke width to use
|
||||
}) => {
|
||||
// Normalize stroke across designs
|
||||
stroke = stroke * strokeScale
|
||||
|
||||
return (
|
||||
<LineDrawingWrapper viewBox="0 0 74 119" {...{ className, stroke }}>
|
||||
<Front stroke={stroke} />
|
||||
</LineDrawingWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
* React component for the back
|
||||
*/
|
||||
export const AaronBack = ({
|
||||
className = 'w-64', // CSS classes to apply
|
||||
stroke = 1, // Stroke width to use
|
||||
}) => {
|
||||
// Normalize stroke across designs
|
||||
stroke = stroke * strokeScale
|
||||
|
||||
return (
|
||||
<LineDrawingWrapper viewBox="74 0 74 119" {...{ className, stroke }}>
|
||||
<Back stroke={stroke} />
|
||||
</LineDrawingWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
* SVG elements for the front
|
||||
*/
|
||||
export const Front = ({ stroke }) => (
|
||||
<>
|
||||
<path
|
||||
key="stitches"
|
||||
{...dashed(stroke)}
|
||||
{...thin(stroke)}
|
||||
d="m2.6292 110.88c0.63236 0.09 2.8764 0.43138 3.514 0.51605 10.485 1.4129 20.582 1.9817 30.207 1.9182h1.6774c9.6256 0.0635 19.719-0.50536 30.207-1.9182 1.0372-0.14023 2.069-0.28575 3.093-0.43921m-9.9665-107.51c-0.20109 1.8706-0.62209 5.9021-0.86022 8.63-0.46831 5.3208-0.5371 7.4242-0.55827 8.1518-0.12964 4.5614-0.17992 6.9003 0.55827 9.6811 0.4789 1.8018 1.053 3.8814 2.7861 6.1145 0.36248 0.46567 2.196 2.7728 5.5748 4.5852 0.75407 0.40481 1.8246 0.91658 2.4693 1.1012m-58.791-38.15c0.20109 1.8706 0.62209 5.7878 0.86022 8.5157 0.46831 5.3208 0.5371 7.4242 0.55827 8.1518 0.12964 4.5614 0.17991 6.9003-0.55827 9.6811-0.4789 1.8018-1.053 3.8814-2.7861 6.1145-0.36247 0.46567-2.196 2.7728-5.5748 4.5852-0.75406 0.40481-1.9396 0.90647-2.4873 1.1234m0.11863 70.162c0.63235 0.09 2.7735 0.37423 3.4111 0.45889 10.485 1.4129 20.582 1.9817 30.207 1.9182h1.6775c9.6255 0.0635 19.719-0.50535 30.207-1.9182 1.0372-0.14023 2.069-0.28575 3.093-0.4392m-15.352-109.64c-0.65352 4.5191-2.724 14.228-2.8802 14.77-0.0979 0.33602-0.12965 0.59531-0.30956 0.87842-0.25136 0.39952-0.58738 0.61912-0.77259 0.73289-9.0341 4.359-22.81 4.5807-30.194 0-0.1852-0.11641-0.52122-0.33337-0.77258-0.73289-0.17992-0.28575-0.21167-0.5424-0.30956-0.87842-0.17727-0.60589-2.659-12.398-2.995-14.716m36.052 2.1867c-9.6445 4.951-24.147 4.747-33.635 0"
|
||||
/>
|
||||
<path
|
||||
key="folds"
|
||||
opacity={0.3}
|
||||
d="m4.3282 54.821c0.10055 1.487 0.17728 2.9792 0.19844 4.4688 0.0185 1.4896-8e-3 2.9818-0.0926 4.4741-0.0953 1.4922-0.24342 2.9792-0.47361 4.4556-0.22489 1.4764-0.52916 2.9395-0.91281 4.3815 0.18256-1.4817 0.34925-2.958 0.50271-4.4344 0.14552-1.4764 0.27252-2.9554 0.35719-4.4344 0.0953-1.479 0.14552-2.9633 0.20902-4.4503l0.082-2.2304c0.045-0.74083 0.0714-1.487 0.12964-2.2304zm1.8494 50.21c4.7837-0.42069 9.652-0.26194 14.38 0.73025 2.3627 0.46831 4.6964 1.0345 7.0458 1.5134 2.3495 0.48948 4.7069 0.93398 7.0697 1.3652 2.3574 0.44714 4.7202 0.84402 7.1014 1.1139 2.3866 0.28046 4.7678 0.54769 7.1596 0.67469 2.3892 0.12965 4.789 0.0714 7.1755-0.15081 2.3892-0.20638 4.7598-0.59531 7.1067-1.1218-2.3151 0.64293-4.6884 1.0848-7.0776 1.3811-2.3918 0.27517-4.8075 0.381-7.2205 0.29898-2.413-0.0635-4.8075-0.34925-7.1967-0.60325-2.3945-0.25665-4.7784-0.64029-7.1438-1.0742-2.3627-0.44714-4.7228-0.90752-7.075-1.4155-2.3522-0.50535-4.6937-1.0583-7.03-1.5875-1.1695-0.25665-2.3442-0.48683-3.5295-0.65881-1.1827-0.17463-2.376-0.28575-3.5719-0.37571-2.3892-0.17992-4.7943-0.19579-7.194-0.09zm6.1172-19.913c1.7648-0.69585 3.5745-1.2859 5.4001-1.8098 1.8309-0.51329 3.6909-0.92604 5.5668-1.2515 1.8732-0.33602 3.7624-0.55827 5.6409-0.80433l5.6436-0.73554c3.7624-0.48684 7.538-0.87578 11.202-1.8045 1.8388-0.43921 3.6592-0.96573 5.4557-1.5663 0.89959-0.29633 1.7939-0.60854 2.6802-0.94191 0.88636-0.33602 1.7674-0.68527 2.6458-1.0451-1.7066 0.8308-3.4634 1.561-5.2414 2.2304-1.7859 0.65088-3.601 1.225-5.4398 1.7145-0.92075 0.24606-1.8442 0.47096-2.7808 0.65881-0.93663 0.18786-1.8759 0.34396-2.8152 0.48684-1.8785 0.28575-3.7597 0.5371-5.6409 0.78581l-5.6515 0.68792c-1.8838 0.22225-3.765 0.45772-5.6356 0.7276-1.8706 0.27517-3.728 0.64029-5.5642 1.1007-1.8415 0.4445-3.6618 0.9816-5.4663 1.5663zm-1.0663-12.306c0.62177 0.18521 1.2435 0.37306 1.86 0.56621 0.61912 0.1905 1.233 0.39158 1.86 0.55033 0.62442 0.17198 1.2435 0.3519 1.8732 0.50006 0.62706 0.15611 1.2515 0.3228 1.8838 0.45509 2.5188 0.5715 5.0615 1.0292 7.6226 1.3494 2.5612 0.32809 5.1356 0.52917 7.7179 0.61119 2.5823 0.0794 5.17 0.0582 7.7629-0.0503-2.5744 0.31221-5.17 0.47096-7.7682 0.46302-2.5982 3e-3 -5.1991-0.17727-7.7788-0.50006-2.5797-0.33338-5.1408-0.81492-7.6544-1.4737-0.62971-0.15611-1.2541-0.33602-1.8785-0.52123-0.62442-0.17727-1.2409-0.38365-1.8574-0.58473-0.61912-0.19579-1.2224-0.43656-1.8309-0.6641-0.60589-0.22755-1.2118-0.46303-1.8124-0.70115z"
|
||||
/>
|
||||
<path
|
||||
key="outline"
|
||||
d="m54.356 1.8383c-6.0877 5.9635-28.882 6.1419-34.769 0m18.224 114.83c9.6255 0.0635 19.719-0.50536 30.207-1.9182 1.1271-0.15081 2.2463-0.31221 3.3576-0.4789v-74.798c-0.50007-0.20903-1.1668-0.51859-1.9182-0.9578-2.9051-1.7066-4.482-3.8761-4.7942-4.3154-1.4896-2.1008-1.9844-4.0587-2.3971-5.7547-0.635-2.6167-0.59266-4.8154-0.47889-9.1096 0.0185-0.68527 0.0767-2.6644 0.47889-7.6729 0.26459-3.2835 0.52652-6.0034 0.71967-7.911l-8.6307-1.9182s-2.5453 13.29-2.7173 13.912c-0.09 0.32279-0.11642 0.56621-0.28046 0.83873-0.22754 0.381-0.53181 0.59002-0.6985 0.6985-6.8987 4.0814-21.715 3.6852-27.376 0-0.16668-0.11112-0.47095-0.3175-0.6985-0.6985-0.16139-0.27252-0.1905-0.51858-0.28045-0.83873-0.17198-0.62177-2.7173-13.912-2.7173-13.912l-8.6307 1.9182c0.19314 1.9076 0.45508 4.6276 0.71966 7.911 0.40217 5.0059 0.46038 6.985 0.4789 7.6729 0.11377 4.2942 0.1561 6.4929-0.4789 9.1096-0.41275 1.696-0.90752 3.6512-2.3971 5.7547-0.31221 0.43921-1.8891 2.6088-4.7942 4.3154-0.75142 0.44186-1.4182 0.74877-1.9182 0.9578v74.798c1.1112 0.16669 2.2304 0.32809 3.3576 0.4789 10.485 1.4129 20.582 1.9817 30.207 1.9182z"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
/*
|
||||
* SVG elements for the front
|
||||
*/
|
||||
const Back = ({ stroke }) => (
|
||||
<>
|
||||
<path
|
||||
key="outline"
|
||||
d="m93.73 1.8383-8.6307 1.9182c0.19315 1.9076 0.45508 4.6276 0.71967 7.911 0.40216 5.0059 0.46037 6.985 0.47889 7.6729 0.11377 4.2942 0.15611 6.4929-0.47889 9.1096-0.41275 1.696-0.90752 3.6512-2.3971 5.7547-0.31221 0.43921-1.8891 2.6088-4.7942 4.3154-0.75141 0.44186-1.4182 0.74877-1.9182 0.9578v74.798c1.1112 0.16669 2.2304 0.32809 3.3576 0.4789 10.485 1.4129 20.582 1.9817 30.207 1.9182h1.6774c9.6255 0.0635 19.719-0.50536 30.207-1.9182 1.1271-0.15081 2.2463-0.31221 3.3576-0.4789v-74.798c-0.50006-0.20903-1.1668-0.51859-1.9182-0.9578-2.9051-1.7066-4.482-3.8761-4.7942-4.3154-1.4896-2.1008-1.9844-4.0587-2.3971-5.7547-0.635-2.6167-0.59267-4.8154-0.4789-9.1096 0.0185-0.68527 0.0767-2.6644 0.4789-7.6729 0.26458-3.2835 0.52652-6.0034 0.71967-7.911l-8.6307-1.9182c-6.0877 5.9635-28.879 6.1419-34.766 0z"
|
||||
/>
|
||||
<path
|
||||
key="folds"
|
||||
opacity={0.3}
|
||||
d="m140.01 65.37c-0.23548 1.6722-0.55033 3.3338-0.89693 4.99-0.36248 1.651-0.78052 3.2914-1.2409 4.9212-0.23548 0.81227-0.48154 1.6219-0.73819 2.4289-0.27516 0.79904-0.53181 1.606-0.8255 2.4024-0.29368 0.79375-0.57414 1.5928-0.89164 2.3786l-0.46567 1.1827-0.49212 1.1695c-1.3494 3.1062-2.8787 6.133-4.5879 9.054-0.86783 1.4526-1.7595 2.8919-2.7067 4.2916-0.95779 1.3917-1.9368 2.7728-2.9766 4.1037 0.94456-1.4023 1.8759-2.8072 2.7596-4.2439 0.89959-1.4261 1.7462-2.884 2.5691-4.3524 0.80433-1.479 1.6087-2.958 2.3521-4.4715 0.37307-0.75406 0.73025-1.5161 1.0874-2.2807 0.34131-0.76993 0.6985-1.5319 1.0186-2.3098 1.3467-3.085 2.5056-6.2521 3.511-9.4668 0.50535-1.606 0.96308-3.2306 1.3785-4.863 0.42862-1.6351 0.79375-3.2835 1.1456-4.9345zm-58.735-15.105c0.73025 2.4553 1.4843 4.9001 2.3098 7.3237 0.80963 2.4262 1.6695 4.8366 2.585 7.2258 1.8283 4.7784 3.8338 9.4853 6.0563 14.089 2.1828 4.6249 4.617 9.1255 7.1808 13.549 2.5797 4.4185 5.3419 8.7286 8.2391 12.951-1.5399-2.0452-3.0083-4.146-4.4556-6.26-1.4235-2.1299-2.7993-4.2942-4.1328-6.4823-0.65617-1.1007-1.3229-2.196-1.9447-3.3179l-0.94456-1.6748-0.91811-1.6907c-0.62177-1.1218-1.1933-2.2675-1.7912-3.4026-0.5715-1.1483-1.1589-2.2886-1.7066-3.4475-0.54504-1.1615-1.0954-2.3178-1.6087-3.4925-0.25929-0.58737-0.52916-1.1668-0.78052-1.7568l-0.75406-1.7701-0.37571-0.88371-1.0795-2.6749c-0.47096-1.1933-0.91546-2.3945-1.36-3.5957-0.87048-2.4104-1.6907-4.8392-2.45-7.2866-0.74877-2.4527-1.4526-4.916-2.069-7.403z"
|
||||
/>
|
||||
<path
|
||||
key="stitches"
|
||||
{...dashed(stroke)}
|
||||
{...thin(stroke)}
|
||||
d="m145.43 110.89c-0.63235 0.09-2.4763 0.41995-3.1139 0.50462-10.485 1.4129-20.582 1.9817-30.207 1.9182h-1.6775c-9.6255 0.0635-19.719-0.50536-30.207-1.9182-1.0372-0.14023-2.4462-0.41148-3.4702-0.56494m10.051-107.27c0.20109 1.8706 0.57637 5.7878 0.8145 8.5157 0.46831 5.3208 0.5371 7.4242 0.55827 8.1518 0.12964 4.5614 0.17991 6.9003-0.55827 9.6811-0.4789 1.8018-1.053 3.8814-2.7861 6.1145-0.36248 0.46567-2.196 2.7728-5.5748 4.5852-0.75406 0.40481-1.9701 0.9085-2.5178 1.1254m58.774-38.288c-0.20108 1.8706-0.55351 5.9021-0.79163 8.63-0.46832 5.3208-0.53711 7.4242-0.55827 8.1518-0.12965 4.5614-0.17992 6.9003 0.55827 9.6811 0.47889 1.8018 1.053 3.8814 2.7861 6.1145 0.36248 0.46567 2.196 2.7728 5.5748 4.5852 0.75406 0.40481 1.7796 0.77502 2.3273 0.99197m2.3e-4 70.236c-0.63236 0.09-2.3963 0.43138-3.0339 0.51604-10.485 1.4129-20.582 1.9817-30.207 1.9182h-1.6774c-9.6256 0.0635-19.719-0.50535-30.207-1.9182-1.0372-0.14023-2.4805-0.37719-3.5045-0.53064m53.025-109.57c-4.9848 7.6282-32.798 7.553-37.46 0.0313"
|
||||
/>
|
||||
</>
|
||||
)
|
98
sites/shared/components/designs/linedrawings/albert.mjs
Normal file
98
sites/shared/components/designs/linedrawings/albert.mjs
Normal file
|
@ -0,0 +1,98 @@
|
|||
import { LineDrawingWrapper, thin, dashed } from './shared.mjs'
|
||||
|
||||
const strokeScale = 0.6
|
||||
|
||||
export const Albert = ({
|
||||
className = 'w-64', // CSS classes to apply
|
||||
stroke = 1, // Stroke width to use
|
||||
}) => {
|
||||
// Normalize stroke across designs
|
||||
stroke = stroke * strokeScale
|
||||
|
||||
return (
|
||||
<LineDrawingWrapper viewBox="0 0 128 141" {...{ className, stroke }}>
|
||||
<Front stroke={stroke} />
|
||||
<Back stroke={stroke} />
|
||||
</LineDrawingWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
* React component for the front
|
||||
*/
|
||||
export const AlbertFront = ({
|
||||
className = 'w-64', // CSS classes to apply
|
||||
stroke = 1, // Stroke width to use
|
||||
}) => {
|
||||
// Normalize stroke across designs
|
||||
stroke = stroke * strokeScale
|
||||
|
||||
return (
|
||||
<LineDrawingWrapper viewBox="0 0 64 141" {...{ className, stroke }}>
|
||||
<Front stroke={stroke} />
|
||||
</LineDrawingWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
* React component for the back
|
||||
*/
|
||||
export const AlbertBack = ({
|
||||
className = 'w-64', // CSS classes to apply
|
||||
stroke = 1, // Stroke width to use
|
||||
}) => {
|
||||
// Normalize stroke across designs
|
||||
stroke = stroke * strokeScale
|
||||
|
||||
return (
|
||||
<LineDrawingWrapper viewBox="64 0 64 141" {...{ className, stroke }}>
|
||||
<Back stroke={stroke} />
|
||||
</LineDrawingWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
* SVG elements for the front
|
||||
*/
|
||||
export const Front = ({ stroke }) => (
|
||||
<>
|
||||
<path
|
||||
key="stitches"
|
||||
{...dashed(stroke)}
|
||||
{...thin(stroke)}
|
||||
d="m 45.751762,25.686058 h -13.6234 -0.53181 -13.62604 M 1.3094621,132.6216 c 0.6694,0.17992 1.70392,0.44979 2.97657,0.74612 7.7972699,1.81769 14.1051699,2.20509 18.7882899,2.47761 1.84415,0.10848 5.37898,0.22225 8.43227,0.24871 h 0.65088 c 3.05329,-0.0265 6.58812,-0.14287 8.43227,-0.24871 4.68312,-0.27252 11.05165,-0.67204 18.84892,-2.48973 1.27,-0.29633 2.30716,-0.5662 2.97656,-0.74612 M 14.700182,74.292648 h 34.2609 m -3.36014,-49.58819 h -13.38527 -0.52123 -13.38527 M 1.2567821,133.40323 c 0.6694,0.17992 1.70392,0.44979 2.97656,0.74612 7.7972799,1.81769 14.1657999,2.21721 18.8489199,2.48973 1.84415,0.10848 5.37898,0.22225 8.43227,0.24871 h 0.65088 c 3.05329,-0.0265 6.58812,-0.14287 8.43227,-0.24871 4.68312,-0.27252 11.05164,-0.67204 18.84891,-2.48973 1.27,-0.29633 2.30717,-0.5662 2.97657,-0.74612 M 44.677552,23.249248 c 0.0926,1.29117 0.26723,3.92906 0.61913,6.12511 0.48154,2.98714 1.3626,8.24177 4.20952,14.3801 2.15371,4.64608 4.48204,7.747 5.19906,8.67833 1.20385,1.56369 3.13002,3.33904 4.01108,4.24921 m -39.6875,-33.43275 c -0.0926,1.29117 -0.26722,3.92906 -0.61912,6.12511 -0.48154,2.98714 -1.3626,8.24177 -4.20952,14.3801 -2.15371,4.64608 -4.4820399,7.747 -5.1990599,8.67833 -1.20386,1.56369 -3.13002,3.33904 -4.01109,4.24921 m 9.7471599,16.93065 h 34.2609 M 24.584442,2.756377 c 2.68565,-0.0603 3.34536,4.5240499 3.26972,9.273867 -0.0744,4.674224 -0.20054,9.477174 -0.15249,11.049694 M 24.806542,1.8705199 c 5.81722,-2.00006997 3.81977,17.9116981 4.28573,21.1945281 M 39.234022,2.733557 c -2.68564,-0.0603 -3.34535,4.5240499 -3.26972,9.273877 0.0744,4.674214 0.20054,9.477164 0.1525,11.049694 m 2.89512,-21.2094281 c -5.81722,-2.00006997 -3.81976,17.9117081 -4.28573,21.1945381 m 10.27284,0.0509 c -0.12302,-1.75154 -0.28706,-4.73869 -0.49484,-7.37394 -0.13669,-1.73567 -0.2898,-3.563944 -0.74363,-5.9002141 -0.1613,-0.82814 -0.41829,-2.013477 -0.99242,-3.442227 -0.36908,-0.92075 -0.72449,-1.78064 -1.48726,-2.70404 -0.74635,-0.92604 -1.46718,-1.547217 -2.45605,-1.825027 m 4.30146,21.3327581 c 0.0902,-1.77535 0.10389,-4.19365 0,-6.24152 -0.19685,-3.854987 -0.32261,-6.873884 -1.45719,-10.1891011 -0.1269,-0.56082 -0.59463,-2.23219 -2.46601,-4.305197 M 18.830412,22.968128 c 0.12302,-1.75154 0.28706,-4.73869 0.49484,-7.37394 0.1367,-1.73567 0.2898,-3.563934 0.74363,-5.9002041 0.1613,-0.82815 0.41829,-2.013477 0.99242,-3.442227 0.36908,-0.92075 0.72449,-1.78065 1.48726,-2.70404 0.74635,-0.92605 1.46718,-1.547217 2.45605,-1.825027 m -4.30146,21.3327481 c -0.0902,-1.77535 -0.10389,-4.19364 0,-6.24152 0.19685,-3.854987 0.32261,-6.8738741 1.45719,-10.1891011 0.1269,-0.56082 0.59463,-2.23219 2.46601,-4.305197"
|
||||
/>
|
||||
<path
|
||||
key="folds"
|
||||
opacity={0.3}
|
||||
d="m 58.242742,71.070038 c -0.65352,2.70404 -1.43669,5.37634 -2.29923,8.02217 -0.87048,2.64319 -1.87325,5.24669 -2.90248,7.82637 -1.01864,2.59027 -2.12196,5.1488 -3.34698,7.6544 -1.23296,2.50031 -2.58498,4.94771 -4.10369,7.289272 -3.05064,4.67783 -6.67014,8.96673 -10.69181,12.82435 -2.01348,1.92617 -4.12221,3.7518 -6.29973,5.48746 -2.1881,1.71979 -4.43441,3.3655 -6.75216,4.90273 2.24896,-1.63512 4.46087,-3.31787 6.58812,-5.10381 2.13784,-1.77271 4.20688,-3.62479 6.17802,-5.57742 1.96056,-1.96056 3.86292,-3.97933 5.62504,-6.11981 1.77007,-2.13254 3.40519,-4.37356 4.90802,-6.69925 1.52136,-2.315102 2.87603,-4.736042 4.11692,-7.215192 0.61648,-1.24089 1.19856,-2.50031 1.76477,-3.76766 0.55827,-1.27 1.10067,-2.54794 1.63777,-3.82852 2.17488,-5.11175 4.05871,-10.34257 5.57742,-15.69509 z m -0.68792,4.97152 c -0.49741,2.58763 -1.08479,5.15938 -1.70656,7.72055 -0.3175,1.28058 -0.65352,2.55587 -1.01071,3.82322 l -0.52652,1.90765 c -0.17727,0.635 -0.37306,1.26471 -0.55827,1.89706 -1.50283,5.05619 -3.26231,10.035652 -5.19642,14.946312 -1.98437,4.8895 -4.15395,9.70492 -6.54314,14.40657 -0.59531,1.17739 -1.21973,2.33891 -1.83356,3.50573 -0.63765,1.15358 -1.24884,2.32304 -1.9103,3.46339 l -0.98425,1.7145 -1.0107,1.70127 c -0.66411,1.14036 -1.37319,2.24896 -2.06375,3.37079 1.29645,-2.29393 2.61143,-4.57729 3.83116,-6.91091 0.635,-1.15359 1.22767,-2.33098 1.8415,-3.4925 0.59002,-1.1774 1.19327,-2.34686 1.76477,-3.53219 2.30982,-4.73075 4.47675,-9.53029 6.41615,-14.42244 1.97114,-4.87891 3.71739,-9.847792 5.2996,-14.866932 1.58486,-5.0218 2.97921,-10.1018 4.191,-15.23207 z m -41.9391,15.54427 c 2.59292,-1.03452 5.29696,-1.8362 8.06185,-2.30716 1.38113,-0.24871 2.76755,-0.42069 4.15132,-0.60854 l 4.15395,-0.53975 c 1.38377,-0.17463 2.77019,-0.34925 4.14602,-0.54505 0.69057,-0.0952 1.37584,-0.20108 2.05582,-0.32808 0.67733,-0.13229 1.35996,-0.28046 2.03464,-0.43921 2.70404,-0.64823 5.36046,-1.50812 7.94809,-2.55852 -2.50561,1.23825 -5.13557,2.23838 -7.84755,2.96069 -0.67733,0.18256 -1.35731,0.34925 -2.04787,0.49212 -0.69056,0.14023 -1.38113,0.2593 -2.07169,0.37042 -1.38377,0.21431 -2.77019,0.39158 -4.15396,0.57415 l -4.15925,0.51064 c -1.38641,0.15875 -2.77283,0.32015 -4.14866,0.51859 -1.37584,0.20372 -2.74373,0.46037 -4.09575,0.78581 -1.35467,0.30956 -2.69346,0.6985 -4.02696,1.11389 z m -0.78581,-9.05668 c 0.92339,0.24606 1.82298,0.55827 2.74902,0.78846 0.46037,0.1217 0.9181,0.25135 1.38112,0.35718 0.46302,0.11113 0.92075,0.23019 1.38642,0.32544 1.85473,0.4101 3.72269,0.74083 5.60652,0.97631 1.88119,0.24342 3.7756,0.39423 5.67267,0.46567 1.8997,0.0661 3.80206,0.0688 5.71235,0.0185 -1.89177,0.25665 -3.80471,0.39158 -5.71765,0.39423 -1.91558,0.0132 -3.83116,-0.11642 -5.73352,-0.35454 -1.8997,-0.24606 -3.78618,-0.6059 -5.63827,-1.10067 -0.46302,-0.11906 -0.92339,-0.25135 -1.38112,-0.39158 -0.46038,-0.13494 -0.91281,-0.2884 -1.36525,-0.44186 -0.45508,-0.15081 -0.89694,-0.33073 -1.34144,-0.50535 -0.44714,-0.17198 -0.889,-0.34925 -1.33085,-0.53181 z"
|
||||
/>
|
||||
<path
|
||||
key="outline"
|
||||
d="m 45.482092,23.144478 c -0.1285,-1.77271 -0.298,-4.79954 -0.51398,-7.46919 -0.14217,-1.75684 -0.30073,-3.608914 -0.77097,-5.974294 -0.16677,-0.83873 -0.55824,-2.36836 -1.02796,-3.4845571 -0.41794,-0.9931479 -0.88589,-1.9951999 -1.57745,-2.8214899 -0.59541,-0.711399 -0.85786,-0.978839 -2.152,-1.764739 -6.85497,-2.73578 -4.79698,15.18946 -5.25758,21.47559 m -15.95764,-0.002 c 0.1285,-1.77271 0.298,-4.79954 0.51398,-7.46919 0.14217,-1.75684 0.30073,-3.608914 0.77097,-5.9742941 0.16677,-0.83873 0.55824,-2.36836 1.02796,-3.484557 0.41794,-0.993148 0.88589,-1.9952 1.57745,-2.82149 0.59541,-0.711399 0.85786,-0.978839 2.152,-1.764739 6.85497,-2.73578 4.79698,15.1894601 5.25758,21.4755901 m 2.67006,116.664372 c 3.06388,-0.0265 6.61458,-0.14288 8.46402,-0.24871 4.699,-0.27252 11.09133,-0.67204 18.92036,-2.48973 1.27529,-0.29633 2.3151,-0.56621 2.98714,-0.74613 l -3.73327,-80.904292 c -0.889,-0.91281 -2.36659,-2.30596 -3.33284,-3.63628 -2.02333,-2.7857 -3.8287,-5.7492 -5.26509,-9.03832 -1.68787,-3.86498 -3.50612,-9.88126 -4.16947,-14.6028 -0.24494,-1.74344 -0.45301,-3.50075 -0.58327,-4.91868 -2.33203,-0.0485 -6.55702,-0.0408 -13.35108,-0.0408 h -0.52123 -13.38527 c -0.0926,1.29646 -0.26723,3.14854 -0.62177,5.35252 -0.48419,3.00038 -1.37054,8.27617 -4.23069,14.43831 -2.16693,4.66725 -4.5058499,7.77875 -5.2281599,8.71273 -1.2118,1.57163 -2.3495,2.82046 -3.23586,3.73327 l -3.73856,80.904292 c 0.67204,0.17992 1.71185,0.4498 2.98715,0.74613 7.8263699,1.81769 14.2186999,2.21721 18.9203499,2.48973 1.84944,0.10848 5.40015,0.22225 8.46402,0.24871 z m 16.80369,-66.116802 v -2.46592 h -16.95979 -0.34132 -16.95979 v 2.46592 18.80129 h 16.86984 0.52122 16.86984 z m -27.80553,-50.54427 c -0.0902,-1.77536 -0.10389,-4.19365 0,-6.24152 0.19684,-3.854987 0.3226,-6.8738841 1.45719,-10.1891111 0.48937,-1.4261 1.16319,-3.01332 2.79534,-5.317847 m 17.16437,21.7282281 c 0.0902,-1.77536 0.10389,-4.19365 0,-6.24152 -0.19684,-3.854987 -0.2083,-6.702432 -1.34288,-10.0176591 -0.48938,-1.4261 -1.23335,-3.10687 -2.86551,-5.411387 M 24.348072,3.098207 c 4.01259,-0.23034 2.83304,8.893627 3.02286,19.914291 M 39.547282,3.106097 c -4.01259,-0.23034 -2.83304,8.893617 -3.02286,19.914291"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
/*
|
||||
* SVG elements for the front
|
||||
*/
|
||||
const Back = ({ stroke }) => (
|
||||
<>
|
||||
<path
|
||||
key="stitches"
|
||||
{...dashed(stroke)}
|
||||
{...thin(stroke)}
|
||||
d="m 87.114972,2.2491977 c 0.92477,-0.244877 0.0726,-0.13752 1.41391,-0.229515 2.18052,0.350681 2.3819,3.092209 2.93839,4.91401 1.7545,8.1034703 1.69158,22.4431003 2.41018,30.1866003 0.17811,1.94939 0.56663,3.735 1.07695,5.34667 m 1.1626,3.00107 c 2.09578,4.54441 4.874578,7.11198 5.131268,7.33527 6.14785,5.49488 9.61454,5.4155 12.40621,6.7594 m -18.827578,-35.37351 2.11426,0.01 m -2.19717,-0.96012 2.27693,0.01 m 12.421728,0.95568 -10.463098,-0.002 m 10.393998,-0.9715 -10.345118,-0.002 m -16.76423,0.9755 10.4631,-0.002 m -10.394,-0.9715 10.34513,-0.002 M 87.424962,1.8582837 c 0.92477,-0.244877 0.11396,-0.162324 1.47179,-0.25432 2.12893,0.336711 2.77652,3.009088 3.40968,5.509323 1.80648,8.1034663 2.2748,22.0214263 2.79879,29.7649163 0.10656,1.16635 0.28845,2.27408 0.52668,3.32099 m 1.1626,3.74357 c 2.12198,5.34326 5.401588,8.37495 5.681528,8.61846 6.14786,5.49488 9.61455,5.41549 12.40622,6.7594 M 104.35377,1.8734467 c -0.92477,-0.244877 -0.11396,-0.162324 -1.47178,-0.25432 -2.12894,0.336712 -2.77652,3.009088 -3.409678,5.509324 -1.80649,8.1034723 -2.2748,22.0214223 -2.79879,29.7649123 -0.88644,9.70189 -6.98453,15.347 -7.37082,15.68302 -6.14785,5.49488 -9.61455,5.4155 -12.40621,6.7594 M 104.66377,2.2643607 c -0.92477,-0.244877 -0.0726,-0.13752 -1.41391,-0.229515 -2.18052,0.350681 -2.3819,3.092209 -2.93839,4.914011 -1.754508,8.1034663 -1.691578,22.4431063 -2.410178,30.1865963 -0.88644,9.70189 -6.98453,15.34699 -7.37082,15.68301 -6.14785,5.49488 -9.61455,5.4155 -12.40622,6.7594 m 28.917588,-37.8428 c -0.38808,-6.19847 -0.93413,-14.1569583 -3.48556,-19.2183453 m 1.24681,0.170932 c 1.68973,1.854406 2.12815,4.533185 2.27241,5.264885 0.9885,4.4603283 1.48778,9.1446983 1.82636,13.7729283 m -24.164958,-0.006 c 0.38808,-6.19847 0.93413,-14.1569523 3.48555,-19.2183383 m -1.2468,0.171361 c -1.68973,1.854407 -2.12816,4.533186 -2.27241,5.264886 -0.9885,4.4603273 -1.48779,9.1446913 -1.82636,13.7729313 m 0.17297,0.0661 c -0.0926,1.29116 -0.26723,3.92906 -0.61913,6.1251 -0.48154,2.98715 -1.3626,8.24177 -4.20952,14.38011 -2.1537,4.64608 -4.48204,7.747 -5.19906,8.67833 -1.20385,1.56369 -2.67741,2.87027 -3.55847,3.78044 m 39.234888,-32.96393 c 0.0926,1.29116 0.26723,3.92906 0.61912,6.1251 0.48154,2.98715 1.36261,8.24177 4.20952,14.38011 2.15371,4.64608 4.48204,7.747 5.19907,8.67833 1.20385,1.56369 2.68549,2.93492 3.56655,3.84509 m 0.31488,0.10514 c -0.0794,0.30956 -0.22225,0.75671 -0.49741,1.24354 -0.69057,1.21973 -1.74625,1.86796 -2.98715,2.48973 -1.28587,0.64558 -3.28083,1.51871 -5.97429,2.24102 m 1.15886,-1.3732 1.24356,79.660757 M 69.008412,54.910223 c 0.0794,0.30956 0.22225,0.75671 0.49742,1.24354 0.69056,1.21973 1.74625,1.86796 2.98714,2.48973 1.28588,0.64558 3.28084,1.51871 5.9743,2.24102 m -1.16152,-1.3732 -1.24354,79.660757 m 38.364248,-2.4068 c 2.51619,-0.26194 5.79438,-0.9525 9.21015,-2.73844 0.91281,-0.47625 1.61163,-0.88461 2.36041,-1.3979 m -11.60463,3.41841 c 2.51619,-0.26194 5.79438,-0.9525 9.21015,-2.73844 0.91281,-0.47625 1.74095,-0.9816 2.48973,-1.49489 m -11.73692,0.55298 h -18.237728 -0.62177 -18.23508 m 37.094578,0.79375 h -18.237728 -0.62177 -18.23508 m 0.0556,3.70152 c -2.51619,-0.26194 -5.79438,-0.9525 -9.21015,-2.73844 -0.91281,-0.47625 -1.74096,-0.9816 -2.48973,-1.49489 m 11.60463,3.41841 c -2.51619,-0.26194 -5.79438,-0.9525 -9.21015,-2.73844 -0.91281,-0.47625 -1.59548,-0.86845 -2.34425,-1.38174"
|
||||
/>
|
||||
<path
|
||||
key="folds"
|
||||
opacity={0.3}
|
||||
d="m 83.981182,24.959383 c 0.381,3.33905 0.82021,6.67015 1.37054,9.98273 0.52652,3.31788 1.16417,6.61194 1.85738,9.90071 0.68792,3.28613 1.37583,6.57225 2.21721,9.82134 0.4101,1.62718 0.87577,3.2385 1.35202,4.84452 0.49741,1.60073 1.00806,3.19616 1.58221,4.77043 1.11125,3.16177 2.39977,6.26005 3.83116,9.28952 0.72496,1.51078 1.47638,3.00832 2.27013,4.48469 0.80169,1.47109 1.616598,2.93688 2.476498,4.37621 1.7145,2.88396 3.55864,5.68854 5.51921,8.41375 1.94733,2.73315 4.02431,5.37633 6.17008,7.96131 -2.23838,-2.50825 -4.35769,-5.12233 -6.38175,-7.80785 -2.00819,-2.69875 -3.89467,-5.48746 -5.65944,-8.35554 -3.524248,-5.73617 -6.458478,-11.84275 -8.728598,-18.18746 -2.26484,-6.35 -3.79413,-12.93283 -5.05354,-19.53154 -0.64294,-3.29671 -1.23825,-6.61194 -1.7145,-9.9404 -0.45773,-3.3311 -0.84667,-6.67014 -1.10861,-10.02242 z m -0.003,6.33942 c 0.49477,6.55373 1.16945,13.08894 2.03729,19.59769 0.87048,6.50875 1.90764,12.99369 3.18558,19.43364 1.23561,6.4479 2.7305,12.84553 4.37356,19.20346 0.39952,1.59279 0.84667,3.17236 1.27,4.75986 0.45509,1.57691 0.88371,3.16177 1.35732,4.73604 0.89958,3.159117 1.8997,6.289137 2.8866,9.421807 -0.53975,-1.55046 -1.10067,-3.09562 -1.60867,-4.65931 l -0.77787,-2.33892 -0.74877,-2.349497 c -0.50271,-1.56633 -0.9525,-3.14589 -1.4314,-4.72017 -0.4445,-1.5822 -0.91546,-3.15647 -1.34144,-4.74662 l -0.64558,-2.38125 c -0.21431,-0.79375 -0.40481,-1.59279 -0.60854,-2.38919 -0.4101,-1.59279 -0.78581,-3.19352 -1.16152,-4.79425 -0.38629,-1.59808 -0.72231,-3.20939 -1.08479,-4.81277 -0.33867,-1.60866 -0.67469,-3.21733 -0.99748,-4.83129 -1.23561,-6.45848 -2.286,-12.95665 -3.06917,-19.48656 -0.20637,-1.62984 -0.37306,-3.26761 -0.54504,-4.90273 -0.17727,-1.63513 -0.33073,-3.27025 -0.46831,-4.90802 -0.25929,-3.26761 -0.48419,-6.54844 -0.62177,-9.83192 z"
|
||||
/>
|
||||
<path
|
||||
key="outline"
|
||||
d="m 105.10249,2.6876327 c -0.77226,-0.303159 -1.01296,-0.365982 -2.00516,-0.336891 -3.219362,0.763391 -3.927298,15.9920513 -4.597178,32.5001013 -1.27488,21.70817 -19.33703,24.19482 -19.97997,24.89861 m 30.868938,-37.96242 c -0.76683,-5.72922 -1.13835,-13.9627343 -3.40093,-18.0635323 -2.24026,-3.13116397 -3.42769,-2.634702 -4.67402,-1.74126 -2.645078,2.275017 -4.191878,15.6714923 -4.861768,32.1795423 -1.27487,21.70817 -19.33702,24.19482 -19.97996,24.89861 m 5.90537,-37.27336 c 0.76683,-5.72922 1.13835,-13.9627343 3.40092,-18.0635323 2.24027,-3.13116397 3.4277,-2.634702 4.67403,-1.74126 2.64507,2.275017 4.19188,15.6714923 4.86176,32.1795423 0.0993,1.69052 0.30037,3.26447 0.58711,4.73008 m 1.13702,4.18188 c 4.819118,13.53685 17.714388,15.39396 18.255828,15.98665 M 86.664442,2.6876277 c 0.77225,-0.303154 1.01295,-0.365977 2.00516,-0.336886 3.21936,0.763392 3.9273,15.9920513 4.59718,32.5001013 0.18007,3.06615 0.69504,5.74884 1.44853,8.09719 m 1.20249,3.07762 c 5.361998,11.48744 16.819248,13.16586 17.328938,13.7238 m -6.68039,-38.02327 c -0.17857,-5.64063 -0.68565,-11.626356 -2.2784,-16.4967743 -0.29898,-0.899583 -0.54504,-1.653646 -1.19062,-2.878667 M 85.200582,21.726183 c 0.17857,-5.64063 0.68564,-11.626356 2.27839,-16.4967743 0.29898,-0.899583 0.54505,-1.653646 1.19063,-2.878667 M 114.42943,135.54199 h -18.235078 -0.62177 -18.23773 m 45.370748,-81.766827 3.48456,81.401697 c -0.74877,0.51065 -1.57692,1.016 -2.48973,1.4949 -3.41577,1.78594 -6.69396,2.4765 -9.21014,2.73844 l -1.24355,-79.660747 c 2.69346,-0.72232 4.68842,-1.59544 5.9743,-2.24102 1.24089,-0.62177 2.29658,-1.27 2.98714,-2.48973 0.27517,-0.48684 0.42069,-0.93663 0.49742,-1.24354 -1.93675,-1.97644 -4.30742,-4.77044 -6.47171,-8.46403 -2.17752,-3.71475 -3.25702,-6.83154 -3.98198,-8.96143 -0.66939,-1.96321 -1.83885,-5.75998 -2.48973,-10.70504 -0.20373,-1.54782 -0.31221,-2.87073 -0.37306,-3.85763 H 99.157022 m -1.96784,0 h -0.97366 -0.66411 -0.94192 m -1.9513,0 h -10.28038 c -0.0609,0.98954 -0.16934,2.30981 -0.37307,3.85763 -0.65087,4.94241 -1.82033,8.74183 -2.48972,10.70504 -0.72761,2.12989 -1.80711,5.24668 -3.98198,8.96143 -2.16429,3.69359 -4.53496,6.48759 -6.47171,8.46403 0.0794,0.30956 0.22225,0.7567 0.49742,1.24354 0.69056,1.21973 1.74625,1.86796 2.98714,2.48973 1.28588,0.64558 3.28083,1.5187 5.97429,2.24102 l -1.24354,79.660747 c -2.51619,-0.26194 -5.79437,-0.9525 -9.21014,-2.73844 -0.91282,-0.47625 -1.74096,-0.98161 -2.48973,-1.4949 l 3.48456,-81.401697"
|
||||
/>
|
||||
</>
|
||||
)
|
98
sites/shared/components/designs/linedrawings/bruce.mjs
Normal file
98
sites/shared/components/designs/linedrawings/bruce.mjs
Normal file
|
@ -0,0 +1,98 @@
|
|||
import { LineDrawingWrapper, thin, dashed } from './shared.mjs'
|
||||
|
||||
const strokeScale = 0.6
|
||||
|
||||
export const Bruce = ({
|
||||
className = 'w-64', // CSS classes to apply
|
||||
stroke = 1, // Stroke width to use
|
||||
}) => {
|
||||
// Normalize stroke across designs
|
||||
stroke = stroke * strokeScale
|
||||
|
||||
return (
|
||||
<LineDrawingWrapper viewBox="0 0 202 78" {...{ className, stroke }}>
|
||||
<Front stroke={stroke} />
|
||||
<Back stroke={stroke} />
|
||||
</LineDrawingWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
* React component for the front
|
||||
*/
|
||||
export const BruceFront = ({
|
||||
className = 'w-64', // CSS classes to apply
|
||||
stroke = 1, // Stroke width to use
|
||||
}) => {
|
||||
// Normalize stroke across designs
|
||||
stroke = stroke * strokeScale
|
||||
|
||||
return (
|
||||
<LineDrawingWrapper viewBox="0 0 101 78" {...{ className, stroke }}>
|
||||
<Front stroke={stroke} />
|
||||
</LineDrawingWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
* React component for the back
|
||||
*/
|
||||
export const BruceBack = ({
|
||||
className = 'w-64', // CSS classes to apply
|
||||
stroke = 1, // Stroke width to use
|
||||
}) => {
|
||||
// Normalize stroke across designs
|
||||
stroke = stroke * strokeScale
|
||||
|
||||
return (
|
||||
<LineDrawingWrapper viewBox="101 0 101 78" {...{ className, stroke }}>
|
||||
<Back stroke={stroke} />
|
||||
</LineDrawingWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
* SVG elements for the front
|
||||
*/
|
||||
export const Front = ({ stroke }) => (
|
||||
<>
|
||||
<path
|
||||
key="stitches"
|
||||
{...dashed(stroke)}
|
||||
{...thin(stroke)}
|
||||
d="m 58.695139,70.692687 c 8.628062,-3.307291 21.476229,-6.974416 37.26127,-7.056437 1.169459,-0.0053 2.320396,0.0079 3.450167,0.03969 m -40.584437,8.244414 c 8.628062,-3.307292 21.476229,-6.974417 37.26127,-7.056437 1.169459,-0.0053 2.320396,0.0079 3.450167,0.03969 M 85.841343,10.706355 H 51.244426 50.979843 16.385573 M 85.841343,9.4390006 H 51.244426 50.979843 16.385573 M 43.531868,70.692687 C 34.903806,67.385396 22.055636,63.718271 6.2705959,63.63625 c -1.16946,-0.0053 -2.32039,0.0079 -3.45016,0.03969 M 43.404868,71.920354 C 34.776806,68.613062 21.928636,64.945937 6.1435959,64.863917 c -1.16946,-0.0053 -2.32039,0.0079 -3.45016,0.03969"
|
||||
/>
|
||||
<path
|
||||
key="seams"
|
||||
{...thin(stroke)}
|
||||
d="M 51.112206,59.5247 V 12.315022 m 15.566736,11.307707 c -0.195792,0.433917 -0.832116,1.537813 -1.14697,2.318334 -0.404812,1.000125 -1.317625,3.341688 -2.38125,7.408333 -0.619125,2.370667 -0.978958,4.135438 -1.322917,5.820833 -0.664104,3.264959 -1.124479,6.122459 -1.322916,7.408334 -0.431271,2.78077 -0.425979,3.214687 -0.79375,5.027083 -0.381,1.867958 -0.669396,3.288771 -1.322917,5.027083 -0.478896,1.275292 -1.096908,2.84136 -2.462158,4.815151 m 8.812158,-49.000567 c 1.156229,6.52198 2.301875,13.04925 3.439583,19.579166 2.151063,12.345459 4.796896,26.013833 6.879167,38.364583 M 35.517161,23.693373 c 0.195792,0.433917 0.860374,1.467169 1.175228,2.24769 0.404813,1.000125 1.317625,3.341688 2.38125,7.408333 0.619125,2.370667 0.978958,4.135438 1.322917,5.820833 0.664104,3.264959 1.124479,6.122459 1.322916,7.408334 0.431271,2.78077 0.42598,3.214687 0.79375,5.027083 0.381,1.867958 0.669396,3.288771 1.322917,5.027083 0.478896,1.275292 1.125166,2.84136 2.490416,4.815151 M 37.486139,12.447313 c -1.156229,6.52198 -2.301875,13.04925 -3.439583,19.579166 -2.151063,12.345459 -4.796896,26.013833 -6.879167,38.364583 m 34.517541,4.228042 c -0.66675,-2.368021 -1.54252,-5.275792 -2.500312,-7.929562 -0.230187,-0.635 -0.85725,-2.44475 -2.38125,-4.233334 -0.743479,-0.873125 -1.524,-1.785937 -2.910417,-2.38125 -1.055687,-0.452437 -2.021416,-0.529166 -2.645833,-0.529166 h -0.264583 c -0.624417,0 -1.590146,0.07673 -2.645834,0.529166 -1.386416,0.595313 -2.166937,1.508125 -2.910416,2.38125 -1.524,1.788584 -2.151063,3.598334 -2.38125,4.233334 -0.957792,2.65377 -1.833563,5.561541 -2.500313,7.929562"
|
||||
/>
|
||||
<path
|
||||
key="outline"
|
||||
d="m 100.19239,68.274396 c -1.07421,-0.01587 -2.159002,-0.01587 -3.251731,0 -13.446125,0.203729 -25.918583,2.905125 -37.26127,7.056437 M 86.169426,12.18273 h -34.925 -0.264583 -34.925 M 51.405868,2.2608555 h 33.572979 l 1.190625,9.9218745 c 0.357187,1.050396 0.709083,2.108729 1.058333,3.175 0,0 2.431521,7.440083 4.7625,16.139583 1.775354,6.627812 4.892146,19.647958 8.202085,36.777083 l -1.322918,0.79375 c -0.497417,0.20902 -1.222375,0.497416 -2.116667,0.79375 -1.987021,0.658812 -10.00125,2.344208 -15.345833,3.175 -9.136062,1.418166 -19.039417,2.182812 -21.695833,2.38125 -0.568854,-2.084917 -1.121833,-3.868209 -1.5875,-5.291667 0,0 -0.375708,-1.145646 -2.116667,-5.027083 C 55.829701,64.702521 55.469868,63.922 54.684056,63.247312 54.128431,62.768417 53.588681,62.540875 53.361139,62.453562 52.45891,62.109604 51.662514,62.141354 51.244472,62.188979 h -0.264583 c -0.418042,-0.04762 -1.214438,-0.07937 -2.116667,0.264583 -0.227541,0.08731 -0.767291,0.314855 -1.322916,0.79375 -0.785813,0.674688 -1.145646,1.455209 -1.322917,1.852084 -1.740958,3.881437 -2.116667,5.027083 -2.116667,5.027083 -0.465666,1.423458 -1.018645,3.20675 -1.5875,5.291667 -2.656416,-0.198438 -12.55977,-0.963084 -21.695836,-2.38125 -5.34458,-0.830792 -13.3588101,-2.516188 -15.3458301,-3.175 -0.89429,-0.296334 -1.61925,-0.58473 -2.11667,-0.79375 l -1.32291,-0.79375 c 3.30993,-17.129125 6.42673,-30.149271 8.2020801,-36.777083 2.33098,-8.6995 4.7625,-16.139583 4.7625,-16.139583 0.34925,-1.066271 0.70115,-2.124604 1.05833,-3.175 l 1.19063,-9.9218745 H 50.821139 Z M 2.0319759,68.274396 c 1.07421,-0.01587 2.159,-0.01587 3.25173,0 13.4461201,0.203729 25.9185791,2.905125 37.2612661,7.056437"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
/*
|
||||
* SVG elements for the back
|
||||
*/
|
||||
const Back = ({ stroke }) => (
|
||||
<>
|
||||
<path
|
||||
key="outline"
|
||||
d="m 102.30903,68.274396 c 1.07421,-0.01587 2.159,-0.01587 3.25173,0 13.44613,0.203729 25.91858,2.905125 37.26127,7.056437 M 116.3319,12.18273 h 34.925 0.26458 34.925 M 151.0982,2.2608555 h -33.57563 l -1.19062,9.9218745 c -0.35719,1.050396 -0.70909,2.108729 -1.05834,3.175 0,0 -2.43152,7.440083 -4.7625,16.139583 -1.77535,6.627812 -4.89214,19.647958 -8.20208,36.777083 l 1.32292,0.79375 c 0.49741,0.20902 1.22237,0.497416 2.11666,0.79375 1.98703,0.658812 10.00125,2.344208 15.34584,3.175 9.13606,1.418166 19.03941,2.182812 21.69583,2.38125 0.56885,-2.084917 1.12183,-3.868209 1.5875,-5.291667 0,0 0.37571,-1.145646 2.11667,-5.027083 0.17727,-0.396875 0.5371,-1.177396 1.32291,-1.852084 0.55563,-0.478895 1.09538,-0.706437 1.32292,-0.79375 0.90223,-0.343958 1.69863,-0.312208 2.11667,-0.264583 h 0.26458 c 0.41804,-0.04762 1.21444,-0.07937 2.11667,0.264583 0.22754,0.08731 0.76729,0.314855 1.32291,0.79375 0.78582,0.674688 1.14565,1.455209 1.32292,1.852084 1.74096,3.881437 2.11667,5.027083 2.11667,5.027083 0.46566,1.423458 1.01864,3.20675 1.5875,5.291667 2.65641,-0.198438 12.55977,-0.963084 21.69583,-2.38125 5.34458,-0.830792 13.35881,-2.516188 15.34583,-3.175 0.89429,-0.296334 1.61925,-0.58473 2.11667,-0.79375 l 1.32292,-0.79375 c -3.30994,-17.129125 -6.42673,-30.149271 -8.20209,-36.777083 -2.33098,-8.6995 -4.7625,-16.139583 -4.7625,-16.139583 -0.34925,-1.066271 -0.70114,-2.124604 -1.05833,-3.175 l -1.19062,-9.9218745 h -33.57298 z m 49.37125,66.0135405 c -1.07421,-0.01587 -2.159,-0.01587 -3.25173,0 -13.44613,0.203729 -25.91859,2.905125 -37.26127,7.056437"
|
||||
/>
|
||||
<path
|
||||
key="seams"
|
||||
{...thin(stroke)}
|
||||
d="m 179.0382,12.447313 c 1.11125,6.328834 2.21456,12.662959 3.30729,18.997083 2.06639,11.977687 4.61169,25.24125 6.61458,37.226875 m -48.14094,5.947833 c 0.66675,-2.368021 1.54253,-5.275792 2.50032,-7.929562 0.23018,-0.635 0.85725,-2.44475 2.38125,-4.233334 0.74348,-0.873125 1.524,-1.785937 2.91041,-2.38125 1.05569,-0.452437 2.02142,-0.529166 2.64584,-0.529166 h 0.26458 c 0.62442,0 1.59015,0.07673 2.64583,0.529166 1.38642,0.595313 2.16694,1.508125 2.91042,2.38125 1.524,1.788584 2.15106,3.598334 2.38125,4.233334 0.95779,2.65377 1.83356,5.561541 2.50031,7.929562 M 123.74028,12.447313 c -1.11125,6.328834 -2.21456,12.662959 -3.30729,18.997083 -2.0664,11.977687 -4.61169,25.24125 -6.61458,37.226875"
|
||||
/>
|
||||
<path
|
||||
key="stitches"
|
||||
{...dashed(stroke)}
|
||||
{...thin(stroke)}
|
||||
d="m 177.58299,12.050438 c 1.11125,6.402917 2.21456,12.811125 3.30994,19.221979 2.06904,12.117916 4.61433,25.540229 6.61723,37.663437 M 125.19549,12.050438 c -1.11125,6.402917 -2.21456,12.811125 -3.30994,19.221979 -2.06904,12.117916 -4.61433,25.540229 -6.61723,37.663437 m 28.54061,1.756833 c -8.62807,-3.307291 -21.47623,-6.974416 -37.26127,-7.056437 -1.16946,-0.0053 -2.3204,0.0079 -3.45017,0.03969 m 40.58444,8.244414 c -8.62807,-3.307292 -21.47623,-6.974417 -37.26127,-7.056437 -1.16946,-0.0053 -2.3204,0.0079 -3.45017,0.03969 m 56.00171,5.78908 c 8.62806,-3.307291 21.47623,-6.974416 37.26127,-7.056437 1.16946,-0.0053 2.32039,0.0079 3.45016,0.03969 m -40.58443,8.244414 c 8.62806,-3.307292 21.47623,-6.974417 37.26127,-7.056437 1.16946,-0.0053 2.32039,0.0079 3.45016,0.03969 M 116.66242,10.706355 h 34.59427 0.26458 34.59692 M 116.66242,9.4390006 h 34.59427 0.26458 34.59692"
|
||||
/>
|
||||
</>
|
||||
)
|
28
sites/shared/components/designs/linedrawings/index.mjs
Normal file
28
sites/shared/components/designs/linedrawings/index.mjs
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { Aaron, AaronFront, AaronBack } from 'shared/components/designs/linedrawings/aaron.mjs'
|
||||
import { Albert, AlbertFront } from 'shared/components/designs/linedrawings/albert.mjs'
|
||||
import { Bruce, BruceFront, BruceBack } from 'shared/components/designs/linedrawings/bruce.mjs'
|
||||
import { Simon, SimonFront, SimonBack } from 'shared/components/designs/linedrawings/simon.mjs'
|
||||
import { Wahid, WahidFront, WahidBack } from 'shared/components/designs/linedrawings/wahid.mjs'
|
||||
|
||||
export const lineDrawingsFront = {
|
||||
aaron: AaronFront,
|
||||
albert: AlbertFront,
|
||||
bruce: BruceFront,
|
||||
simon: SimonFront,
|
||||
wahid: WahidFront,
|
||||
}
|
||||
|
||||
export const lineDrawingsBack = {
|
||||
aaron: AaronBack,
|
||||
bruce: BruceBack,
|
||||
simon: SimonBack,
|
||||
wahid: WahidBack,
|
||||
}
|
||||
|
||||
export const lineDrawings = {
|
||||
aaron: Aaron,
|
||||
albert: Albert,
|
||||
bruce: Bruce,
|
||||
simon: Simon,
|
||||
wahid: Wahid,
|
||||
}
|
42
sites/shared/components/designs/linedrawings/shared.mjs
Normal file
42
sites/shared/components/designs/linedrawings/shared.mjs
Normal file
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* A React component to wrap SVG linedrawings for FreeSewing designs
|
||||
*
|
||||
* @param design {string} - The (lowercase) name of a FreeSewing design
|
||||
* @param className {string} - CSS classes to set on the svg tag
|
||||
*
|
||||
* @return LineDrawing as JSX
|
||||
*/
|
||||
export const LineDrawingWrapper = ({
|
||||
className = 'w-64', // CSS classes to apply
|
||||
viewBox = '0 0 100 100', // SVG viewBox
|
||||
stroke = 1, // Stroke to use
|
||||
children = [], // The actual linedrawing
|
||||
}) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox={viewBox}
|
||||
strokeWidth={stroke}
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className + ' linedrawing'}
|
||||
>
|
||||
{children}
|
||||
</svg>
|
||||
)
|
||||
|
||||
/*
|
||||
* Think stroke-width helper to ensure consistency across linedrawings
|
||||
*/
|
||||
export const thin = (stroke = 1) => ({ strokeWidth: stroke / 2 })
|
||||
|
||||
/*
|
||||
* Think stroke-width helper to ensure consistency across linedrawings
|
||||
*/
|
||||
export const veryThin = (stroke = 1) => ({ strokeWidth: stroke / 3 })
|
||||
|
||||
/*
|
||||
* Dashed stroke-dasharray helper to ensure consistency across linedrawings
|
||||
*/
|
||||
export const dashed = (stroke = 1) => ({ strokeDasharray: `${stroke * 1.2},${stroke * 0.8}` })
|
139
sites/shared/components/designs/linedrawings/simon.mjs
Normal file
139
sites/shared/components/designs/linedrawings/simon.mjs
Normal file
File diff suppressed because one or more lines are too long
116
sites/shared/components/designs/linedrawings/wahid.mjs
Normal file
116
sites/shared/components/designs/linedrawings/wahid.mjs
Normal file
|
@ -0,0 +1,116 @@
|
|||
import { LineDrawingWrapper, thin, dashed } from './shared.mjs'
|
||||
|
||||
const strokeScale = 0.4
|
||||
|
||||
export const Wahid = ({
|
||||
className = 'w-64', // CSS classes to apply
|
||||
stroke = 1, // Stroke width to use
|
||||
}) => {
|
||||
// Normalize stroke across designs
|
||||
stroke = stroke * strokeScale
|
||||
|
||||
return (
|
||||
<LineDrawingWrapper viewBox="0 0 162 126" {...{ className, stroke }}>
|
||||
<Front stroke={stroke} />
|
||||
<Back stroke={stroke} />
|
||||
</LineDrawingWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export const WahidFront = ({
|
||||
className = 'w-64', // CSS classes to apply
|
||||
stroke = 1, // Stroke width to use
|
||||
}) => {
|
||||
// Normalize stroke across designs
|
||||
stroke = stroke * strokeScale
|
||||
|
||||
return (
|
||||
<LineDrawingWrapper viewBox="0 0 81 126" {...{ className, stroke }}>
|
||||
<Front stroke={stroke} />
|
||||
</LineDrawingWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export const WahidBack = ({
|
||||
className = 'w-64', // CSS classes to apply
|
||||
stroke = 1, // Stroke width to use
|
||||
}) => {
|
||||
// Normalize stroke across designs
|
||||
stroke = stroke * strokeScale
|
||||
|
||||
return (
|
||||
<LineDrawingWrapper viewBox="82 0 81 126" {...{ className, stroke }}>
|
||||
<Back stroke={stroke} />
|
||||
</LineDrawingWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
* Always use an id for defs that is unique to the design because if we have
|
||||
* multiple linedrawings on the page, they share the same namespace and thus
|
||||
* IDs will collide
|
||||
*/
|
||||
const defs = (
|
||||
<defs>
|
||||
<g id="wahid-button">
|
||||
<circle cx="0" cy="0" r="1.8" strokeWidth="0.45" />
|
||||
<circle cx="-0.55" cy="-0.55" r="0.35" strokeWidth="0.25" />
|
||||
<circle cx="0.55" cy="-0.55" r="0.35" strokeWidth="0.25" />
|
||||
<circle cx="0.55" cy="0.55" r="0.35" strokeWidth="0.25" />
|
||||
<circle cx="-0.55" cy="0.55" r="0.35" strokeWidth="0.25" />
|
||||
</g>
|
||||
</defs>
|
||||
)
|
||||
|
||||
/*
|
||||
* React component for the front
|
||||
*/
|
||||
export const Front = ({ stroke }) => (
|
||||
<>
|
||||
{defs}
|
||||
<path
|
||||
key="darts"
|
||||
{...thin(stroke)}
|
||||
d="m 59.254131,58.572266 c -0.282849,14.103167 -0.248304,26.460675 0.06123,37.305715 m 0.08331,2.68639 c 0.146442,4.367849 0.339027,18.032999 0.574783,21.909619 M 30.210956,97.193711 H 6.6510187 m 44.2643213,0 H 74.364837 M 22.237874,58.572266 c 0.282849,14.103167 0.248304,26.460675 -0.06123,37.305715 m -0.08331,2.68639 c -0.146442,4.367849 -0.339027,18.032999 -0.574783,21.909619 M 30.245567,98.518921 H 6.6976497 v -2.64584 H 30.245567 Z M 50.8819,95.870781 h 23.547916 v 2.64583 H 50.8819 Z"
|
||||
/>
|
||||
<path
|
||||
key="stitches"
|
||||
{...dashed(stroke)}
|
||||
{...thin(stroke)}
|
||||
d="m 25.217291,3.134339 c 3.595687,1.275291 7.688791,2.174875 12.197291,2.248958 0.410104,0.0079 0.814917,0.0079 1.217084,0 h 1.801812 0.529167 1.801812 c 0.399521,0.0079 0.806979,0.0079 1.217083,0 4.5085,-0.07408 8.601605,-0.973667 12.197292,-2.248958"
|
||||
/>
|
||||
<path
|
||||
key="outline"
|
||||
d="m 41.970707,66.390917 c 0.0979,-0.0053 0.235479,-0.01587 0.399521,-0.0344 0.320146,-0.03175 0.484188,-0.05027 0.484188,-0.07673 0.0026,-0.02646 -0.150813,-0.07144 -0.862542,-0.150813 m 0.243417,20.476117 c 0.0979,-0.005 0.235479,-0.0159 0.39952,-0.0344 0.320146,-0.0317 0.484188,-0.0503 0.484188,-0.0767 0.0026,-0.0265 -0.150813,-0.0714 -0.862542,-0.15081 m -0.02117,-9.84518 c 0.0979,-0.005 0.235479,-0.01587 0.39952,-0.0344 0.320146,-0.03175 0.484188,-0.05027 0.484188,-0.07673 0.0026,-0.02646 -0.150813,-0.07144 -0.862542,-0.150813 m -0.28575,20.476113 c 0.0979,-0.005 0.235479,-0.0159 0.399521,-0.0344 0.320146,-0.0318 0.484188,-0.0503 0.484188,-0.0767 0.0026,-0.0264 -0.150813,-0.0714 -0.862542,-0.15081 m 0.243417,10.368989 c 0.0979,-0.005 0.235479,-0.0159 0.39952,-0.0344 0.320146,-0.0317 0.484188,-0.0503 0.484188,-0.0767 0.0026,-0.0265 -0.150813,-0.0714 -0.862542,-0.15081 m -0.28575,-50.273502 c 0.0979,-0.0053 0.235479,-0.01587 0.399521,-0.0344 0.320146,-0.03175 0.484188,-0.05027 0.484188,-0.07673 0.0026,-0.02646 -0.150813,-0.07144 -0.862542,-0.150812 m -1.280601,58.380324 -0.01583,3.18029 m -2.248956,-0.0159 h 4.529632 M 5.1089577,52.47913 v 9.887479 c 0.0053,0.844021 0.01323,2.450041 0,4.458229 -0.08202,12.453933 -0.8255,20.367623 -2.38125,43.333452 -0.06085,0.89694 -0.145521,2.15636 -0.248709,3.70417 M 25.217291,1.943714 9.6068737,8.02913 10.400624,11.733297 c 0.187854,1.063625 0.473604,2.725208 0.79375,4.7625 1.516063,9.681104 1.778,15.187083 1.852084,17.197916 0.22225,5.953125 -0.201084,7.839604 -0.529167,8.995834 -1.158875,4.071937 -3.2332083,6.373812 -3.7041673,6.879166 -1.336146,1.436688 -2.714625,2.352146 -3.704166,2.910417 1.09802,-0.833438 2.725208,-2.278063 3.96875,-4.497917 C 10.940374,44.660692 10.945666,41.39838 10.929791,39.77913 10.890101,35.707192 10.239228,20.768818 9.6068737,8.02913 M 40.695416,49.579296 c -1.764771,-2.989791 -3.6195,-6.439958 -5.423959,-10.326687 -2.137833,-4.606396 -3.341687,-7.985125 -5.027083,-12.7 0,0 -3.913187,-10.959041 -4.7625,-18.25625 -0.206375,-1.772708 -0.264583,-3.481916 -0.264583,-3.481916 -0.03969,-1.156229 -0.02646,-2.135188 0,-2.868083 3.595687,1.275291 7.688791,2.174875 12.197291,2.248958 0.410104,0.0079 0.814917,0.0079 1.217084,0 h 1.801812 0.529167 1.801812 c 0.399521,0.0079 0.806979,0.0079 1.217083,0 4.5085,-0.07408 8.601605,-0.973667 12.197292,-2.248958 0.02646,0.732895 0.03969,1.711854 0,2.868083 0,0 -0.05821,1.709208 -0.264583,3.481916 -0.849313,7.297209 -4.7625,18.25625 -4.7625,18.25625 -1.685396,4.714875 -2.88925,8.093604 -5.027084,12.7 -2.984499,6.434667 -6.11452,11.665479 -8.731249,15.610417 v 53.982934 l 9.405937,14.86165 c 1.695979,-0.33867 3.413125,-0.70909 5.146146,-1.11125 9.895416,-2.30188 18.896541,-5.34459 26.987499,-8.73125 m -38.237582,0.20637 -6.098646,9.63613 c -1.695979,-0.33867 -3.413125,-0.70909 -5.146146,-1.11125 -9.895417,-2.30188 -18.896541,-5.34459 -26.9875003,-8.73125 M 76.281873,52.47913 v 9.887479 c -0.0053,0.844021 -0.01323,2.450041 0,4.458229 0.08202,12.453933 0.8255,20.367623 2.38125,43.333452 0.06085,0.89694 0.145521,2.15636 0.248709,3.70417 M 56.17354,1.943714 71.783957,8.02913 l -0.79375,3.704167 c -0.187854,1.063625 -0.473604,2.725208 -0.79375,4.7625 -1.516063,9.681104 -1.778,15.187083 -1.852083,17.197916 -0.22225,5.953125 0.201083,7.839604 0.529166,8.995834 1.158875,4.071937 3.233208,6.373812 3.704167,6.879166 1.336146,1.436688 2.714625,2.352146 3.704166,2.910417 -1.09802,-0.833438 -2.725208,-2.278063 -3.96875,-4.497917 -1.862666,-3.320521 -1.867958,-6.582833 -1.852083,-8.202083 0.03969,-4.071938 0.690563,-19.010312 1.322917,-31.75"
|
||||
/>
|
||||
<use xlinkHref="#wahid-button" x="40" y="56.1" color="currentColor"></use>
|
||||
<use xlinkHref="#wahid-button" x="40" y="66.2" color="currentColor"></use>
|
||||
<use xlinkHref="#wahid-button" x="40.15" y="76.3" color="currentColor"></use>
|
||||
<use xlinkHref="#wahid-button" x="40.15" y="86.4" color="currentColor"></use>
|
||||
<use xlinkHref="#wahid-button" x="40.2" y="96.5" color="currentColor"></use>
|
||||
<use xlinkHref="#wahid-button" x="40.3" y="106.6" color="currentColor"></use>
|
||||
</>
|
||||
)
|
||||
|
||||
/*
|
||||
* React component for the back
|
||||
*/
|
||||
const Back = ({ stroke }) => (
|
||||
<>
|
||||
<path
|
||||
key="outline"
|
||||
d="m 157.23121,52.47913 v 9.887479 c -0.005,0.844021 -0.0132,2.450041 0,4.458229 0.082,12.453933 0.8255,20.367623 2.38125,43.333452 0.0608,0.89694 0.14552,2.15636 0.24871,3.70417 M 137.1229,1.943707 152.73331,8.029122 m 4.4979,44.450008 c -1.09802,-0.833438 -2.72521,-2.278063 -3.96875,-4.497917 -1.86267,-3.320521 -1.86796,-6.582833 -1.85208,-8.202083 0.0397,-4.071938 0.69056,-19.010312 1.32291,-31.75 M 137.12288,1.943714 c -3.59569,1.275291 -7.6888,2.174875 -12.1973,2.248958 -0.4101,0.0079 -0.81491,0.0079 -1.21708,0 h -1.80181 -0.52917 -1.80181 c -0.39952,0.0079 -0.80698,0.0079 -1.21708,0 -4.5085,-0.07408 -8.60161,-0.973667 -12.1973,-2.248958 m 18.05517,115.943056 3.683,5.81819 c 1.69598,-0.33867 3.41313,-0.70908 5.14615,-1.11125 9.89541,-2.30187 18.89654,-5.34458 26.9875,-8.73125 -7.9719,1.69333 -17.11325,3.09033 -27.24944,3.70417 -3.81529,0.23019 -7.48771,0.33602 -11.00667,0.33866 m -2.54793,-0.0185 -3.683,5.81819 c -1.69598,-0.33867 -3.41313,-0.70908 -5.14615,-1.11125 -9.89542,-2.30187 -18.896536,-5.34458 -26.987496,-8.73125 7.9719,1.69333 17.113246,3.09033 27.249436,3.70417 3.81529,0.23019 7.48771,0.33602 11.00666,0.33866 M 86.058294,52.47913 v 9.887479 c 0.005,0.844021 0.0132,2.450041 0,4.458229 -0.082,12.453933 -0.8255,20.367623 -2.38125,43.333452 -0.0609,0.89694 -0.14552,2.15636 -0.24871,3.70417 M 106.16665,1.943707 90.556244,8.029122 m -4.49795,44.450008 c 1.09802,-0.833438 2.72521,-2.278063 3.96875,-4.497917 1.86267,-3.320521 1.86796,-6.582833 1.85209,-8.202083 -0.0397,-4.071938 -0.69057,-19.010312 -1.32292,-31.75"
|
||||
/>
|
||||
<path
|
||||
key="stitches"
|
||||
{...thin(stroke)}
|
||||
{...dashed(stroke)}
|
||||
d="m 138.40611,2.446422 c -0.41011,0.248708 -0.83609,0.481542 -1.28059,0.690563 -4.36827,2.055812 -10.93787,2.227791 -12.19729,2.248958 -0.4101,0.0079 -0.81492,0.0079 -1.21708,0 h -1.80182 -0.52916 -1.80181 c -0.39953,0.0079 -0.80698,0.0079 -1.21709,0 -4.5085,-0.07408 -8.6016,-0.973667 -12.19729,-2.248958 -0.51065,-0.198438 -1.02129,-0.39423 -1.53458,-0.592667"
|
||||
/>
|
||||
<path
|
||||
key="darts"
|
||||
{...thin(stroke)}
|
||||
d="m 143.92015,47.170466 c 0.43464,22.690304 0.78099,45.327635 2.67748,68.895054 M 99.011784,47.170466 c -0.43464,22.690304 -0.78099,45.327635 -2.67748,68.895054 M 121.77706,4.192663 V 117.9635 m 19.12934,-1.0689 v 3.66977 m -38.36459,-3.66977 v 3.66977"
|
||||
/>
|
||||
</>
|
||||
)
|
|
@ -1,8 +0,0 @@
|
|||
import PageLink from './page-link.mjs'
|
||||
import get from 'lodash.get'
|
||||
import useApp from 'site/hooks/useApp.mjs'
|
||||
|
||||
export const DocsLink = ({ slug }) => {
|
||||
const app = useApp()
|
||||
return <PageLink href={slug} txt={get(app.navigation, [...slug.split('/'), '__title'])} />
|
||||
}
|
|
@ -1,7 +1,8 @@
|
|||
// __SDEFILE__ - This file is a dependency for the stand-alone environment
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { useState } from 'react'
|
||||
import { Robot } from 'shared/components/robot/index.mjs'
|
||||
import { Popout } from 'shared/components/popout.mjs'
|
||||
import { Popout } from 'shared/components/popout/index.mjs'
|
||||
|
||||
export const ErrorView = ({ children, inspectChildren }) => {
|
||||
const { t } = useTranslation(['errors'])
|
||||
|
|
36
sites/shared/components/errors/404.mjs
Normal file
36
sites/shared/components/errors/404.mjs
Normal file
|
@ -0,0 +1,36 @@
|
|||
import { useContext } from 'react'
|
||||
import { ModalContext } from 'shared/context/modal-context.mjs'
|
||||
import { Robot } from 'shared/components/robot/index.mjs'
|
||||
import { Popout } from 'shared/components/popout/index.mjs'
|
||||
import { MegaphoneIcon } from 'shared/components/icons.mjs'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { ModalProblemReport } from 'shared/components/modal/problem-report.mjs'
|
||||
|
||||
export const ns = ['errors']
|
||||
|
||||
export const Error404 = ({ err }) => {
|
||||
const { setModal } = useContext(ModalContext)
|
||||
const { t } = useTranslation(ns)
|
||||
|
||||
return (
|
||||
<Popout warning className="max-w-2xl m-auto">
|
||||
<div className="flex flex-col lg:flex-row gap-8 lg:items-center">
|
||||
<div className="w-48">
|
||||
<Robot pose="shrug2" embed className="w-full" />
|
||||
</div>
|
||||
<div>
|
||||
<h2>{t('errors:t404')}</h2>
|
||||
<h5>{t('errors:d404')}</h5>
|
||||
<p>{t('errors:m404')}</p>
|
||||
<button
|
||||
className="btn btn-neutral w-full flex flex-row justify-between"
|
||||
onClick={() => setModal(<ModalProblemReport {...err} />)}
|
||||
>
|
||||
<span>{t('errors:reportThis')}</span>
|
||||
<MegaphoneIcon />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Popout>
|
||||
)
|
||||
}
|
16
sites/shared/components/errors/de.yaml
Normal file
16
sites/shared/components/errors/de.yaml
Normal file
|
@ -0,0 +1,16 @@
|
|||
t404: Fehler 404
|
||||
d404: Was du suchst, kann nicht gefunden werden
|
||||
m404: Dieser Fehler bedeutet in der Regel, dass es kein Problem gibt, sondern dass die von dir angeforderten Informationen nicht verfügbar sind.
|
||||
something: Etwas ist schiefgelaufen
|
||||
reportThis: Dieses Problem melden
|
||||
newReport: Erstelle einen Problembericht
|
||||
privateReport.t: Erstelle einen privaten Problembericht
|
||||
privateReport.d: Der FreeSewing-Gemeinschaft werden keine Informationen über dieses Problem zur Verfügung gestellt.
|
||||
publicReport.t: Erstelle einen öffentlichen Problembericht
|
||||
publicReport.d: Einige Informationen über dieses Problem werden öffentlich zugänglich gemacht, um die Problemlösung zu erleichtern.
|
||||
leadId: Dies ist die eindeutige Kontext-ID deines Berichts
|
||||
reportCreated: Bericht erstellt
|
||||
leadIssue: Außerdem haben wir einen Eintrag auf GitHub erstellt, um dies zu verfolgen - dort kannst du auch deine eigenen Kommentare hinzufügen
|
||||
close: Schließen
|
||||
|
||||
|
16
sites/shared/components/errors/en.yaml
Normal file
16
sites/shared/components/errors/en.yaml
Normal file
|
@ -0,0 +1,16 @@
|
|||
t404: Error 404
|
||||
d404: What you are looking for cannot be found
|
||||
m404: This error typically means that there is no problem, but rather that the information you requested is not available.
|
||||
something: Something went wrong
|
||||
reportThis: Report this problem
|
||||
newReport: Generate a problem report
|
||||
privateReport.t: Generate a private problem report
|
||||
privateReport.d: No information about this problem will be made available to the wider FreeSewing community.
|
||||
publicReport.t: Generate a public problem report
|
||||
publicReport.d: Some information about this problem will be made available publicly to facilitate problem solving.
|
||||
leadId: This is the unique context ID of your report
|
||||
reportCreated: Report Created
|
||||
leadIssue: In addition, we have created an issue on GitHub to track this — this is also where you can add your own comments
|
||||
close: Close
|
||||
|
||||
|
16
sites/shared/components/errors/es.yaml
Normal file
16
sites/shared/components/errors/es.yaml
Normal file
|
@ -0,0 +1,16 @@
|
|||
t404: Error 404
|
||||
d404: No se encuentra lo que buscas
|
||||
m404: Este error suele significar que no hay ningún problema, sino que la información que solicitaste no está disponible.
|
||||
something: Algo salió mal
|
||||
reportThis: Informar de este problema
|
||||
newReport: Generar un informe de problema
|
||||
privateReport.t: Generar un informe privado del problema
|
||||
privateReport.d: No se pondrá a disposición de la comunidad de FreeSewing en general ninguna información sobre este problema.
|
||||
publicReport.t: Generar un informe público del problema
|
||||
publicReport.d: Parte de la información sobre este problema se hará pública para facilitar la resolución del problema.
|
||||
leadId: Este es el ID de contexto único de tu informe
|
||||
reportCreated: Informe creado
|
||||
leadIssue: Además, hemos creado una incidencia en GitHub para hacer un seguimiento de esto - aquí es también donde puedes añadir tus propios comentarios
|
||||
close: Cerrar
|
||||
|
||||
|
16
sites/shared/components/errors/fr.yaml
Normal file
16
sites/shared/components/errors/fr.yaml
Normal file
|
@ -0,0 +1,16 @@
|
|||
t404: Erreur 404
|
||||
d404: Ce que tu cherches est introuvable
|
||||
m404: Cette erreur signifie généralement qu'il n'y a pas de problème, mais plutôt que les informations que tu as demandées ne sont pas disponibles.
|
||||
something: Quelque chose s'est mal déroulé
|
||||
reportThis: Signaler ce problème
|
||||
newReport: Générer un rapport de problème
|
||||
privateReport.t: Génère un rapport de problème privé
|
||||
privateReport.d: Aucune information sur ce problème ne sera mise à la disposition de l'ensemble de la communauté FreeSewing.
|
||||
publicReport.t: Génère un rapport de problème public
|
||||
publicReport.d: Certaines informations sur ce problème seront rendues publiques pour faciliter la résolution du problème.
|
||||
leadId: Il s'agit de l'identifiant contextuel unique de ton rapport
|
||||
reportCreated: Rapport créé
|
||||
leadIssue: De plus, nous avons créé une question sur GitHub pour en assurer le suivi - c'est aussi là que tu peux ajouter tes propres commentaires
|
||||
close: Fermer
|
||||
|
||||
|
16
sites/shared/components/errors/nl.yaml
Normal file
16
sites/shared/components/errors/nl.yaml
Normal file
|
@ -0,0 +1,16 @@
|
|||
t404: Fout 404
|
||||
d404: Wat je zoekt kon niet worden gevonden
|
||||
m404: Deze fout betekent meestal dat er geen probleem is, maar dat de informatie die je hebt opgevraagd niet beschikbaar is.
|
||||
something: Er liep wat mis
|
||||
reportThis: Meld dit probleem
|
||||
newReport: Een probleemrapport genereren
|
||||
privateReport.t: Een persoonlijk probleemrapport genereren
|
||||
privateReport.d: Er wordt geen informatie over dit probleem beschikbaar gemaakt aan de bredere FreeSewing gemeenschap.
|
||||
publicReport.t: Genereer een publiek probleemrapport
|
||||
publicReport.d: Sommige informatie over dit probleem zal publiek beschikbaar zijn om het oplossen van problemen te vergemakkelijken.
|
||||
leadId: Dit is de unieke context-ID van je rapport
|
||||
reportCreated: Rapport aangemaakt
|
||||
leadIssue: Daarnaast hebben we een issue aangemaakt op GitHub om dit te volgen - daar kan je ook je eigen commentaar toevoegen
|
||||
close: Sluiten
|
||||
|
||||
|
16
sites/shared/components/errors/uk.yaml
Normal file
16
sites/shared/components/errors/uk.yaml
Normal file
|
@ -0,0 +1,16 @@
|
|||
t404: Помилка 404
|
||||
d404: Те, що ви шукаєте, не може бути знайдено
|
||||
m404: Ця помилка, як правило, означає, що проблеми немає, а скоріше, що інформація, яку ви запитували, недоступна.
|
||||
something: Щось пішло не так
|
||||
reportThis: Повідомте про цю проблему
|
||||
newReport: Створіть звіт про проблему
|
||||
privateReport.t: Створити приватний звіт про проблему
|
||||
privateReport.d: Жодна інформація про цю проблему не буде доступна широкій спільноті FreeSewing.
|
||||
publicReport.t: Створіть публічний звіт про проблему
|
||||
publicReport.d: Деяка інформація про цю проблему буде опублікована у відкритому доступі, щоб полегшити її вирішення.
|
||||
leadId: Це унікальний ідентифікатор контексту вашого звіту
|
||||
reportCreated: Звіт створено
|
||||
leadIssue: Крім того, ми створили тему на GitHub, щоб відстежувати це - тут ви також можете додавати власні коментарі
|
||||
close: Закрити
|
||||
|
||||
|
23
sites/shared/components/errors/vague.mjs
Normal file
23
sites/shared/components/errors/vague.mjs
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { Robot } from 'shared/components/robot/index.mjs'
|
||||
import Link from 'next/link'
|
||||
import { HelpIcon } from 'shared/components/icons.mjs'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
||||
export const ns = ['account']
|
||||
|
||||
export const VagueError = ({ noTitle = false }) => {
|
||||
const { t } = useTranslation('account')
|
||||
|
||||
return (
|
||||
<>
|
||||
{noTitle ? null : <h1>{t('account:politeOhCrap')}</h1>}
|
||||
<Robot pose="ohno" className="w-full" embed />
|
||||
<p className="mt-4 text-2xl">{t('account:vagueError')}</p>
|
||||
<div className="flex flex-row gap-4 items-center mt-4">
|
||||
<Link className="btn btn-primary btn-lg mt-4 pr-6" href="/support">
|
||||
<HelpIcon className="w-6 h-6 mr-4" /> {t('contactSupport')}
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
7
sites/shared/components/fingerprint.mjs
Normal file
7
sites/shared/components/fingerprint.mjs
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { FingerprintIcon } from 'shared/components/icons.mjs'
|
||||
|
||||
export const Fingerprint = ({ id }) => (
|
||||
<div className="text-sm font-medium badge badge-secondary mt-4 flex flex-row gap-2 px-3 py-3">
|
||||
<FingerprintIcon className="w-4 h-4" stroke={2} /> {id}
|
||||
</div>
|
||||
)
|
1
sites/shared/components/footer/de.yaml
Normal file
1
sites/shared/components/footer/de.yaml
Normal file
|
@ -0,0 +1 @@
|
|||
sponsors: bla bla
|
1
sites/shared/components/footer/en.yaml
Normal file
1
sites/shared/components/footer/en.yaml
Normal file
|
@ -0,0 +1 @@
|
|||
sponsors: bla bla
|
1
sites/shared/components/footer/es.yaml
Normal file
1
sites/shared/components/footer/es.yaml
Normal file
|
@ -0,0 +1 @@
|
|||
sponsors: bla bla
|
1
sites/shared/components/footer/fr.yaml
Normal file
1
sites/shared/components/footer/fr.yaml
Normal file
|
@ -0,0 +1 @@
|
|||
sponsors: bla bla
|
59
sites/shared/components/footer/index.mjs
Normal file
59
sites/shared/components/footer/index.mjs
Normal file
|
@ -0,0 +1,59 @@
|
|||
// __SDEFILE__ - This file is a dependency for the stand-alone environment
|
||||
// Dependencies
|
||||
import orderBy from 'lodash.orderby'
|
||||
import { NavigationContext } from 'shared/context/navigation-context.mjs'
|
||||
// Hooks
|
||||
import { useContext } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
// Components
|
||||
import Link from 'next/link'
|
||||
import { WordMark } from 'shared/components/wordmark.mjs'
|
||||
import { SocialIcons } from 'shared/components/social/icons.mjs'
|
||||
import { Sponsors, ns as sponsorsNs } from 'shared/components/sponsors/index.mjs'
|
||||
import { FreeSewingIcon } from 'shared/components/icons.mjs'
|
||||
|
||||
export const ns = ['common', ...sponsorsNs]
|
||||
|
||||
const onlyFooterLinks = (tree) => orderBy(tree, ['t'], ['asc']).filter((entry) => entry.f)
|
||||
|
||||
export const Footer = () => {
|
||||
// Grab siteNav from the navigation context
|
||||
const { siteNav } = useContext(NavigationContext)
|
||||
const { t } = useTranslation(ns)
|
||||
|
||||
return (
|
||||
<footer className="bg-neutral">
|
||||
<div className="w-full sm:w-auto flex flex-col gap-2 items-center justify-center pt-12">
|
||||
<FreeSewingIcon className="w-24 lg:w-40 m-auto m-auto text-neutral-content" />
|
||||
<div className="mt-4">
|
||||
<WordMark />
|
||||
</div>
|
||||
<p className="text-neutral-content text-normal leading-5 text-center -mt-2 opacity-70 font-normal">
|
||||
{t('common:slogan1')}
|
||||
<br />
|
||||
{t('common:slogan2')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="w-full max-w-xl text-center py-8 m-auto">
|
||||
<ul className="text-neutral-content list inline font-medium text-center">
|
||||
{onlyFooterLinks(siteNav).map((page) => (
|
||||
<li key={page.s} className="block lg:inline">
|
||||
<Link href={page.s} className="p-3 underline decoration-2 hover:decoration-4">
|
||||
{page.t}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="w-full sm:w-auto flex flex-row flex-wrap gap-6 lg:gap-8 items-center justify-center px-8 py-14">
|
||||
<SocialIcons />
|
||||
</div>
|
||||
|
||||
<div className="mt-8 py-8 px-8 flex flex-row gap-8 flex-wrap 2xl:flex-nowrap justify-around text-neutral-content py-10 border border-solid border-l-0 border-r-0 border-b-0 border-base-300">
|
||||
<Sponsors />
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
}
|
1
sites/shared/components/footer/nl.yaml
Normal file
1
sites/shared/components/footer/nl.yaml
Normal file
|
@ -0,0 +1 @@
|
|||
sponsors: bla bla
|
1
sites/shared/components/footer/uk.yaml
Normal file
1
sites/shared/components/footer/uk.yaml
Normal file
|
@ -0,0 +1 @@
|
|||
sponsors: бла-бла-бла.
|
38
sites/shared/components/gdpr/de.yaml
Normal file
38
sites/shared/components/gdpr/de.yaml
Normal file
|
@ -0,0 +1,38 @@
|
|||
clickHere: Klicke hier, um deine Zustimmung zu geben
|
||||
createAccount: Konto erstellen
|
||||
compliant: "FreeSewing respektiert deine Privatsphäre und deine Rechte. Wir halten uns an das strengste Datenschutz- und Sicherheitsgesetz der Welt: die General Data Protection Regulation (GDPR) der Europäischen Union (EU)."
|
||||
consent: Einwilligungen
|
||||
consentForAccountData: Consent for account data
|
||||
consentGiven: Einwilligung erteilt
|
||||
consentNotGiven: Einwilligung nicht erteilt
|
||||
consentWhyAnswer: Nach der DSGVO erfordert die Verarbeitung deiner personenbezogenen Daten eine detaillierte Zustimmung - mit anderen Worten, wir brauchen deine Erlaubnis für die verschiedenen Arten, wie wir deine Daten verarbeiten.
|
||||
createMyAccount: Meinen Account erstellen
|
||||
furtherReading: Weiterführende Informationen
|
||||
hideDetails: Details ausblenden
|
||||
noConsentNoAccount: This consent is required for a FreeSewing account.
|
||||
noConsentNoAccountCreation: Ohne diese Einwilligung können wir deinen Account nicht erstellen.
|
||||
noIDoNot: 'Nein, mache ich nicht'
|
||||
openDataInfo: Diese Daten werden verwendet, um die menschliche Form in all ihren Formen zu studieren und zu verstehen, sodass wir bessere Schnittmuster und besser passende Kleidungsstücke erhalten. Auch wenn diese Daten anonymisiert sind, hast du das Recht, dem zu widersprechen.
|
||||
openDataQuestion: Teile anonymisierte Maße als freie Daten (open data)
|
||||
privacyMatters: Datenschutz ist wichtig
|
||||
privacyNotice: FreeSewing Datenschutzhinweis
|
||||
processing: In Bearbeitung
|
||||
accountQuestion: Gibst du deine Einwilligung zur Verarbeitung deiner Modelldaten?
|
||||
accountShareAnswer: '<b>Nein</b>, niemals.'
|
||||
accountTimingAnswer: '<b>12 Monate</b> nach deinem letzten Login oder bis du <b>deinen Account entfernst</b> oder bis du diese Einwilligung <b>widerrufst</b>.'
|
||||
accountWarning: Durch den Widerruf dieser Einwilligung werden alle deine Daten entfernt. Es hat den gleichen Effekt wie das Entfernen deines Accounts.
|
||||
accountWhatAnswerOptional: 'Optional: A <b>picture</b>, <b>bio</b>, or <b>GitHub username</b>'
|
||||
accountWhatAnswer: 'Your <b>email address</b>, <b>username</b>, and <b>password</b>, and any <b>body measurements</b> you add to your account.'
|
||||
accountWhatQuestion: What is account data?
|
||||
accountWhyAnswer: 'Um <b>dich zu authentifizieren</b>, <b>dich bei Bedarf zu kontaktieren</b> und <b>maßgeschneiderte</b> Schnittmuster zu erstellen.'
|
||||
readMore: Weitere Informationen findest du in unserer Datenschutzerklärung.
|
||||
readRights: Lies mehr über deine Rechte für weitere Informationen.
|
||||
revokeConsent: Einwilligung widerrufen
|
||||
shareQuestion: Teilen wir sie mit anderen?
|
||||
showDetails: Details anzeigen
|
||||
timingQuestion: Für wie lange behalten wir sie?
|
||||
whatYouNeedToKnow: Was du wissen musst
|
||||
whyQuestion: Warum brauchen wir sie?
|
||||
yesIDoObject: 'Ja, ich widerspreche'
|
||||
yesIDo: 'Ja, das mache ich'
|
||||
openData: 'Hinweis: Freesewing veröffentlicht anonymisierte Maße als freie Daten (open data) für wissenschaftliche Forschung. Du hast das Recht, dem zu widersprechen'
|
30
sites/shared/components/gdpr/details.mjs
Normal file
30
sites/shared/components/gdpr/details.mjs
Normal file
|
@ -0,0 +1,30 @@
|
|||
// __SDEFILE__ - This file is a dependency for the stand-alone environment
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
||||
export const ns = ['gdpr']
|
||||
|
||||
export const GdprAccountDetails = () => {
|
||||
const { t } = useTranslation(ns)
|
||||
|
||||
return (
|
||||
<div className="border-l-4 ml-1 pl-4 my-2 opacity-80">
|
||||
<h6>{t('accountWhatQuestion')}</h6>
|
||||
<p
|
||||
dangerouslySetInnerHTML={{
|
||||
__html:
|
||||
t('accountWhatAnswer') +
|
||||
'<br /><em><small>' +
|
||||
t('accountWhatAnswerOptional') +
|
||||
'</small></em>',
|
||||
}}
|
||||
/>
|
||||
<h6>{t('whyQuestion')}</h6>
|
||||
<p dangerouslySetInnerHTML={{ __html: t('accountWhyAnswer') }} />
|
||||
<h6>{t('timingQuestion')}</h6>
|
||||
<p dangerouslySetInnerHTML={{ __html: t('accountTimingAnswer') }} />
|
||||
<h6>{t('shareQuestion')}</h6>
|
||||
<p dangerouslySetInnerHTML={{ __html: t('accountShareAnswer') }} />
|
||||
<p dangerouslySetInnerHTML={{ __html: t('openData') }} />
|
||||
</div>
|
||||
)
|
||||
}
|
38
sites/shared/components/gdpr/en.yaml
Normal file
38
sites/shared/components/gdpr/en.yaml
Normal file
|
@ -0,0 +1,38 @@
|
|||
clickHere: Click here to give your consent
|
||||
createAccount: Create account
|
||||
compliant: "FreeSewing respects your privacy and your rights. We adhere to the toughest privacy and security law in the world: the General Data Protection Regulation (GDPR) of the European Union (EU)."
|
||||
consent: Consent
|
||||
consentForAccountData: Consent for account data
|
||||
consentGiven: Consent given
|
||||
consentNotGiven: Consent not given
|
||||
consentWhyAnswer: Under the GDPR, processing of your personal data requires granular consent — in other words, we need your permission for the various ways we handle your data.
|
||||
createMyAccount: Create my account
|
||||
furtherReading: Further reading
|
||||
hideDetails: Hide details
|
||||
noConsentNoAccount: This consent is required for a FreeSewing account.
|
||||
noConsentNoAccountCreation: Without this consent, we cannot create a FreeSewing account.
|
||||
noIDoNot: 'No, I do not'
|
||||
openDataInfo: This data is used to study and understand the human form in all its shapes, so we can get better sewing patterns, and better fitting garments. Even though this data is anonymized, you have the right to object to this.
|
||||
openDataQuestion: Share anonymized measurements as open data
|
||||
privacyMatters: Privacy matters
|
||||
privacyNotice: FreeSewing Privacy Notice
|
||||
processing: Processing
|
||||
accountQuestion: Do you give your consent to process your account data?
|
||||
accountShareAnswer: '<b>No</b>, never.'
|
||||
accountTimingAnswer: '<b>12 months</b> after the last time your connected to our backend, or until you <b>remove</b> your account or <b>revoke</b> this consent.'
|
||||
accountWarning: Revoking this consent will trigger the removal of all of your data. It has the exact same affect as removing your account.
|
||||
accountWhatAnswerOptional: 'Optional: A <b>picture</b>, <b>bio</b>, or <b>GitHub username</b>'
|
||||
accountWhatAnswer: 'Your <b>email address</b>, <b>username</b>, and <b>password</b>, and any <b>body measurements</b> you add to your account.'
|
||||
accountWhatQuestion: What is account data?
|
||||
accountWhyAnswer: 'To <b>authenticate</b> you, <b>contact</b> you when needed, and generate <b>made-to-measure</b> sewing patterns.'
|
||||
readMore: For more information, please read our privacy notice.
|
||||
readRights: For more information, please read up on your rights.
|
||||
revokeConsent: Revoke consent
|
||||
shareQuestion: Do we share it with others?
|
||||
showDetails: Show details
|
||||
timingQuestion: How long do we keep it?
|
||||
whatYouNeedToKnow: What you need to know
|
||||
whyQuestion: Why do we need it?
|
||||
yesIDoObject: 'Yes, I do object'
|
||||
yesIDo: 'Yes, I do'
|
||||
openData: 'Note: Freesewing publishes anonymized measurements as open data for scientific research. You have the right to object to this'
|
38
sites/shared/components/gdpr/es.yaml
Normal file
38
sites/shared/components/gdpr/es.yaml
Normal file
|
@ -0,0 +1,38 @@
|
|||
clickHere: Haz clic aquí para dar tu consentimiento
|
||||
createAccount: Crear cuenta
|
||||
compliant: "FreeSewing respeta tu privacidad y tus derechos. Nos adherimos a la ley de privacidad y seguridad más estricta del mundo: el Reglamento General de Protección de Datos (RGPD) de la Unión Europea (UE)."
|
||||
consent: Consentimiento
|
||||
consentForAccountData: Consent for account data
|
||||
consentGiven: Consentimiento dado
|
||||
consentNotGiven: Consentimiento no dado
|
||||
consentWhyAnswer: Según el GDPR, el tratamiento de tus datos personales requiere un consentimiento granular, es decir, necesitamos tu permiso para las distintas formas en que tratamos tus datos.
|
||||
createMyAccount: Crea mi cuenta
|
||||
furtherReading: Lectura adicional
|
||||
hideDetails: Ocultar detalles
|
||||
noConsentNoAccount: This consent is required for a FreeSewing account.
|
||||
noConsentNoAccountCreation: Without this consent, we cannot create a FreeSewing account.
|
||||
noIDoNot: 'No, no lo hago'
|
||||
openDataInfo: Estos datos se utilizan para estudiar y comprender la forma humana en todas sus formas, para que podamos obtener mejores patrones de costura y que se ajusten mejor a las prendas. Aunque esta información es anónima, tiene derecho a objetar esto.
|
||||
openDataQuestion: Compartir mediciones anonimizadas como datos abiertos.
|
||||
privacyMatters: Cuestiones de privacidad
|
||||
privacyNotice: Aviso de privacidad de FreeSewing
|
||||
processing: Procesando
|
||||
accountQuestion: Do you give your consent to process your account data?
|
||||
accountShareAnswer: '<b>No</b>, nunca.'
|
||||
accountTimingAnswer: '<b>12 months</b> after the last time your connected to our backend, or until you <b>remove</b> your account or <b>revoke</b> this consent.'
|
||||
accountWarning: La revocación de este consentimiento activará la eliminación de todos sus datos. Tiene exactamente el mismo efecto que la eliminación de su cuenta.
|
||||
accountWhatAnswerOptional: 'Optional: A <b>picture</b>, <b>bio</b>, or <b>GitHub username</b>'
|
||||
accountWhatAnswer: 'Your <b>email address</b>, <b>username</b>, and <b>password</b>, and any <b>body measurements</b> you add to your account.'
|
||||
accountWhatQuestion: What is account data?
|
||||
accountWhyAnswer: 'To <b>authenticate</b> you, <b>contact</b> you when needed, and generate <b>made-to-measure</b> sewing patterns.'
|
||||
readMore: Para más información, lea nuestro aviso de privacidad.
|
||||
readRights: Para obtener más información, lea más sobre sus derechos.
|
||||
revokeConsent: Revocar consentimiento
|
||||
shareQuestion: '¿Lo compartimos con otros?'
|
||||
showDetails: Mostrar detalles
|
||||
timingQuestion: '¿Cuánto tiempo lo mantenemos?'
|
||||
whatYouNeedToKnow: Lo que necesitas saber
|
||||
whyQuestion: '¿Por qué la necesitamos?'
|
||||
yesIDoObject: 'Sí, me opongo'
|
||||
yesIDo: 'Sí, lo hago'
|
||||
openData: 'Nota: Freesewing publica mediciones anonimizadas como datos abiertos para la investigación científica. Tiene derecho a objetar esto'
|
85
sites/shared/components/gdpr/form.mjs
Normal file
85
sites/shared/components/gdpr/form.mjs
Normal file
|
@ -0,0 +1,85 @@
|
|||
// __SDEFILE__ - This file is a dependency for the stand-alone environment
|
||||
// Hooks
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
// Components
|
||||
import { Popout } from 'shared/components/popout/index.mjs'
|
||||
import { Link } from 'shared/components/link.mjs'
|
||||
import { GdprAccountDetails, ns as gdprNs } from 'shared/components/gdpr/details.mjs'
|
||||
|
||||
export const ns = ['gdpr', gdprNs]
|
||||
|
||||
const Checkbox = ({ value, setter, label, children = null }) => (
|
||||
<div
|
||||
className={`form-control p-4 hover:cursor-pointer rounded border-l-8 my-2
|
||||
${value ? 'border-success bg-success' : 'border-error bg-error'}
|
||||
bg-opacity-10 shadow`}
|
||||
onClick={() => setter(value ? false : true)}
|
||||
>
|
||||
<div className="form-control flex flex-row items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="checkbox"
|
||||
checked={value ? 'checked' : ''}
|
||||
onChange={() => setter(value ? false : true)}
|
||||
/>
|
||||
<span className="label-text">{label}</span>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
export const ConsentForm = ({ submit }) => {
|
||||
// State
|
||||
const [details, setDetails] = useState(false)
|
||||
const [consent1, setConsent1] = useState(false)
|
||||
const [consent2, setConsent2] = useState(false)
|
||||
|
||||
// Hooks
|
||||
const { t } = useTranslation(ns)
|
||||
|
||||
const giveConsent = () => {
|
||||
setConsent1(true)
|
||||
setConsent2(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1>{t('gdpr:privacyMatters')}</h1>
|
||||
<p>{t('gdpr:compliant')}</p>
|
||||
<p>{t('gdpr:consentWhyAnswer')}</p>
|
||||
<h5 className="mt-8">{t('gdpr:accountQuestion')}</h5>
|
||||
{details ? <GdprAccountDetails /> : null}
|
||||
{consent1 ? (
|
||||
<>
|
||||
<Checkbox value={consent1} setter={setConsent1} label={t('gdpr:yesIDo')} />
|
||||
<Checkbox value={consent2} setter={setConsent2} label={t('gdpr:openDataQuestion')} />
|
||||
</>
|
||||
) : (
|
||||
<button className="btn btn-primary btn-lg w-full mt-4" onClick={giveConsent}>
|
||||
{t('gdpr:clickHere')}
|
||||
</button>
|
||||
)}
|
||||
{consent1 && !consent2 ? <Popout note>{t('openDataInfo')}</Popout> : null}
|
||||
<p className="text-center">
|
||||
<button className="btn btn-neutral btn-ghost btn-sm" onClick={() => setDetails(!details)}>
|
||||
{t(details ? 'gdpr:hideDetails' : 'gdpr:showDetails')}
|
||||
</button>
|
||||
</p>
|
||||
{!consent1 && <Popout note>{t('gdpr:noConsentNoAccountCreation')}</Popout>}
|
||||
{consent1 && (
|
||||
<button
|
||||
onClick={() => submit({ consent1, consent2 })}
|
||||
className="btn btn-lg w-full mt-8 btn-primary"
|
||||
>
|
||||
<span>{t('gdpr:createAccount')}</span>
|
||||
</button>
|
||||
)}
|
||||
<p className="text-center opacity-50 mt-12">
|
||||
<Link href="/docs/various/privacy" className="hover:text-secondary underline">
|
||||
FreeSewing Privacy Notice
|
||||
</Link>
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
}
|
38
sites/shared/components/gdpr/fr.yaml
Normal file
38
sites/shared/components/gdpr/fr.yaml
Normal file
|
@ -0,0 +1,38 @@
|
|||
clickHere: Clique ici pour donner ton consentement
|
||||
createAccount: Créer un compte
|
||||
compliant: "FreeSewing respecte ta vie privée et tes droits. Nous adhérons à la loi la plus stricte au monde en matière de confidentialité et de sécurité : le Règlement général sur la protection des données (RGPD) de l'Union européenne (UE)."
|
||||
consent: Consentement
|
||||
consentForAccountData: Consent for account data
|
||||
consentGiven: Consentement donné
|
||||
consentNotGiven: Consentement non donné
|
||||
consentWhyAnswer: En vertu du GDPR, le traitement de tes données personnelles nécessite un consentement granulaire - en d'autres termes, nous avons besoin de ta permission pour les différentes façons dont nous traitons tes données.
|
||||
createMyAccount: Créer mon compte
|
||||
furtherReading: En lire plus
|
||||
hideDetails: Masquer les détails
|
||||
noConsentNoAccount: This consent is required for a FreeSewing account.
|
||||
noConsentNoAccountCreation: Without this consent, we cannot create a FreeSewing account.
|
||||
noIDoNot: 'Non, je ne le fais pas'
|
||||
openDataInfo: Ces données sont utilisées pour étudier et comprendre la forme humaine sous toutes ses formes, de sorte que nous puissions obtenir de meilleurs modèles de couture et des vêtements plus ajustés. Même si ces données sont anonymes, vous avez le droit de vous y opposer.
|
||||
openDataQuestion: Partager des mesures anonymisées sous forme de données ouvertes
|
||||
privacyMatters: Le respect de la vie privée
|
||||
privacyNotice: Avis de confidentialité de FreeSewing
|
||||
processing: Traitement en cours
|
||||
accountQuestion: Do you give your consent to process your account data?
|
||||
accountShareAnswer: '<b>Non</ b>, jamais.'
|
||||
accountTimingAnswer: '<b>12 months</b> after the last time your connected to our backend, or until you <b>remove</b> your account or <b>revoke</b> this consent.'
|
||||
accountWarning: Révoquer ce consentement entraînera la suppression de toutes vos données. Cela a exactement le même effet que de supprimer votre compte.
|
||||
accountWhatAnswerOptional: 'Optional: A <b>picture</b>, <b>bio</b>, or <b>GitHub username</b>'
|
||||
accountWhatAnswer: 'Your <b>email address</b>, <b>username</b>, and <b>password</b>, and any <b>body measurements</b> you add to your account.'
|
||||
accountWhatQuestion: What is account data?
|
||||
accountWhyAnswer: 'To <b>authenticate</b> you, <b>contact</b> you when needed, and generate <b>made-to-measure</b> sewing patterns.'
|
||||
readMore: Pour plus d'informations, veuillez lire notre politique de confidentialité.
|
||||
readRights: Pour plus d'informations, veuillez lire la page sur vos droits.
|
||||
revokeConsent: Révoquer le consentement
|
||||
shareQuestion: La partageons-nous avec les autres ?
|
||||
showDetails: Voir les détails
|
||||
timingQuestion: Combien de temps les gardons-nous ?
|
||||
whatYouNeedToKnow: Ce que vous devez savoir
|
||||
whyQuestion: Pourquoi en avons-nous besoin ?
|
||||
yesIDoObject: 'Oui, je m''y oppose'
|
||||
yesIDo: 'Oui, je le veux'
|
||||
openData: 'Note : Freesewing publie des mesures rendues anonymes en tant que données libres pour la recherche scientifique. Vous avez le droit de vous y opposer'
|
38
sites/shared/components/gdpr/nl.yaml
Normal file
38
sites/shared/components/gdpr/nl.yaml
Normal file
|
@ -0,0 +1,38 @@
|
|||
clickHere: Klik hier om toestemming te geven
|
||||
createAccount: Account aanmaken
|
||||
compliant: "FreeSewing respecteert je privacy en je rechten. We houden ons aan de strengste privacy- en beveiligingswet ter wereld: de General Data Protection Regulation (GDPR) van de Europese Unie (EU)."
|
||||
consent: Toestemming
|
||||
consentForAccountData: Consent for account data
|
||||
consentGiven: Toestemming gegeven
|
||||
consentNotGiven: Toestemming niet gegeven
|
||||
consentWhyAnswer: Onder de GDPR is voor de verwerking van je persoonlijke gegevens granulaire toestemming nodig - met andere woorden, we hebben je toestemming nodig voor de verschillende manieren waarop we je gegevens verwerken.
|
||||
createMyAccount: Maak mijn account aan
|
||||
furtherReading: Meer lezen
|
||||
hideDetails: Verberg details
|
||||
noConsentNoAccount: This consent is required for a FreeSewing account.
|
||||
noConsentNoAccountCreation: Without this consent, we cannot create a FreeSewing account.
|
||||
noIDoNot: 'Neen, ik geef geen toestemming'
|
||||
openDataInfo: Deze gegevens worden gebruikt om de menselijke vorm in al zijn vormen te bestuderen en te begrijpen, zodat we betere naaipatronen en beter passende kledingstukken kunnen ontwerpen. Hoewel deze gegevens anoniem zijn, hebt u het recht hiertegen bezwaar te maken.
|
||||
openDataQuestion: Deel geanonimiseerde maten als open data
|
||||
privacyMatters: Privacy is een recht
|
||||
privacyNotice: FreeSewing Privacy Verklaring
|
||||
processing: Verwerking
|
||||
accountQuestion: Do you give your consent to process your account data?
|
||||
accountShareAnswer: '<b>Nee</b>, nooit.'
|
||||
accountTimingAnswer: '<b>12 months</b> after the last time your connected to our backend, or until you <b>remove</b> your account or <b>revoke</b> this consent.'
|
||||
accountWarning: Als u deze toestemming intrekt, worden al je gegevens verwijderd. Het heeft precies hetzelfde effect als het verwijderen van je account.
|
||||
accountWhatAnswerOptional: 'Optional: A <b>picture</b>, <b>bio</b>, or <b>GitHub username</b>'
|
||||
accountWhatAnswer: 'Your <b>email address</b>, <b>username</b>, and <b>password</b>, and any <b>body measurements</b> you add to your account.'
|
||||
accountWhatQuestion: What is account data?
|
||||
accountWhyAnswer: 'To <b>authenticate</b> you, <b>contact</b> you when needed, and generate <b>made-to-measure</b> sewing patterns.'
|
||||
readMore: Lees onze privacyverklaring voor meer informatie.
|
||||
readRights: Voor meer informatie kan je alles lezen over je rechten en hoe we ze beschermen.
|
||||
revokeConsent: Toestemming intrekken
|
||||
shareQuestion: Delen we ze met anderen?
|
||||
showDetails: Toon details
|
||||
timingQuestion: Hoe lang houden we ze?
|
||||
whatYouNeedToKnow: Wat je moet weten
|
||||
whyQuestion: Waarom hebben we ze nodig?
|
||||
yesIDoObject: 'Ja, ik maak bezwaar'
|
||||
yesIDo: 'Ja, ik geef mijn toestemming'
|
||||
openData: 'Opmerking: FreeSewing publiceert geanonimmiseerde maten als open gegevens voor wetenschappelijk onderzoek. U heeft het recht om hier bezwaar tegen te maken'
|
38
sites/shared/components/gdpr/uk.yaml
Normal file
38
sites/shared/components/gdpr/uk.yaml
Normal file
|
@ -0,0 +1,38 @@
|
|||
clickHere: Натисніть тут, щоб дати свою згоду
|
||||
createAccount: Створити обліковий запис
|
||||
compliant: "FreeSewing поважає вашу конфіденційність і ваші права. Ми дотримуємося найсуворішого закону про конфіденційність і безпеку в світі: Загального регламенту про захист даних (GDPR) Європейського Союзу (ЄС)."
|
||||
consent: Згода
|
||||
consentForAccountData: Consent for account data
|
||||
consentGiven: Згода отримана
|
||||
consentNotGiven: Згода не отримана
|
||||
consentWhyAnswer: Відповідно до GDPR, обробка ваших персональних даних вимагає детальної згоди - іншими словами, нам потрібен ваш дозвіл на різні способи, якими ми обробляємо ваші дані.
|
||||
createMyAccount: Створити обліковий запис
|
||||
furtherReading: Подальше вивчення
|
||||
hideDetails: Приховати подробиці
|
||||
noConsentNoAccount: This consent is required for a FreeSewing account.
|
||||
noConsentNoAccountCreation: Without this consent, we cannot create a FreeSewing account.
|
||||
noIDoNot: 'Ні, не даю'
|
||||
openDataInfo: Ці дані використовуються для вивчення і розуміння людського тіла в усіх його формах. Це дозволяє нам покращити наші викрійки та створювати більш підходящий до тіла одяг. Незважаючи на те, що ці дані є анонімними, Ви маєте право не давати згоду на їх обробку.
|
||||
openDataQuestion: Поділитися замірами анонімно як відкритими даними
|
||||
privacyMatters: Питання конфіденційності
|
||||
privacyNotice: Повідомлення про конфіденційність FreeSewing
|
||||
processing: Обробляється
|
||||
accountQuestion: Do you give your consent to process your account data?
|
||||
accountShareAnswer: '<b>Ні</b>, ніколи.'
|
||||
accountTimingAnswer: '<b>12 months</b> after the last time your connected to our backend, or until you <b>remove</b> your account or <b>revoke</b> this consent.'
|
||||
accountWarning: Відкликання згоди видалить усі Ваші данні. Це має той же ефект, що і видалення облікового запису.
|
||||
accountWhatAnswerOptional: 'Optional: A <b>picture</b>, <b>bio</b>, or <b>GitHub username</b>'
|
||||
accountWhatAnswer: 'Your <b>email address</b>, <b>username</b>, and <b>password</b>, and any <b>body measurements</b> you add to your account.'
|
||||
accountWhatQuestion: What is account data?
|
||||
accountWhyAnswer: 'To <b>authenticate</b> you, <b>contact</b> you when needed, and generate <b>made-to-measure</b> sewing patterns.'
|
||||
readMore: Для отримання додаткової інформації, будь ласка, прочитайте наші умови конфіденційності.
|
||||
readRights: Для отримання додаткової інформації, будь ласка, прочитайте про Ваші права.
|
||||
revokeConsent: Відкликати згоду
|
||||
shareQuestion: Чи передаємо ми її іншим?
|
||||
showDetails: Показати подробиці
|
||||
timingQuestion: Скільки ми її зберігаємо?
|
||||
whatYouNeedToKnow: Що вам потрібно знати
|
||||
whyQuestion: Чому нам це потрібно?
|
||||
yesIDoObject: 'Ні, я проти'
|
||||
yesIDo: 'Так, я даю згоду'
|
||||
openData: 'Примітка: FreeSewing публікує анонімні заміри тіла як відкриті дані для наукових досліджень. Ви маєте право відмовитися від цього'
|
37
sites/shared/components/header.mjs
Normal file
37
sites/shared/components/header.mjs
Normal file
|
@ -0,0 +1,37 @@
|
|||
// __SDEFILE__ - This file is a dependency for the stand-alone environment
|
||||
import Link from 'next/link'
|
||||
|
||||
export const iconSize = 'h-8 w-8 md:h-10 md:w-10 lg:h-12 lg:w-12'
|
||||
|
||||
export const NavButton = ({
|
||||
href,
|
||||
label,
|
||||
color,
|
||||
children,
|
||||
onClick = false,
|
||||
extraClasses = '',
|
||||
active = false,
|
||||
}) => {
|
||||
const className =
|
||||
'border-0 px-1 lg:px-3 xl:px-4 text-base py-3 md:py-4 text-center flex flex-col items-center 2xl:w-36 ' +
|
||||
`hover:bg-${color} text-${color} hover:text-neutral grow xl:grow-0 relative ${extraClasses} ${
|
||||
active ? 'font-heavy' : ''
|
||||
}`
|
||||
const span = <span className="font-medium text-md hidden md:block md:pt-1 lg:pt-0">{label}</span>
|
||||
|
||||
return onClick ? (
|
||||
<button {...{ onClick, className }} title={label}>
|
||||
{children}
|
||||
{span}
|
||||
</button>
|
||||
) : (
|
||||
<Link {...{ href, className }} title={label}>
|
||||
{children}
|
||||
{span}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export const NavSpacer = () => (
|
||||
<div className="hidden xl:block text-base lg:text-4xl font-thin opacity-30 px-0.5 lg:px-2">|</div>
|
||||
)
|
File diff suppressed because one or more lines are too long
526
sites/shared/components/inputs.mjs
Normal file
526
sites/shared/components/inputs.mjs
Normal file
|
@ -0,0 +1,526 @@
|
|||
// __SDEFILE__ - This file is a dependency for the stand-alone environment
|
||||
// Dependencies
|
||||
import { cloudflareImageUrl } from 'shared/utils.mjs'
|
||||
import { collection } from 'site/hooks/use-design.mjs'
|
||||
// Context
|
||||
import { ModalContext } from 'shared/context/modal-context.mjs'
|
||||
import { LoadingStatusContext } from 'shared/context/loading-status-context.mjs'
|
||||
// Hooks
|
||||
import { useState, useCallback, useContext } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { useDropzone } from 'react-dropzone'
|
||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
// Components
|
||||
import { Mdx } from 'shared/components/mdx/dynamic.mjs'
|
||||
import { ResetIcon, DocsIcon, UploadIcon } from 'shared/components/icons.mjs'
|
||||
import { ModalWrapper } from 'shared/components/wrappers/modal.mjs'
|
||||
import { isDegreeMeasurement } from 'config/measurements.mjs'
|
||||
import { measurementAsMm, measurementAsUnits, parseDistanceInput } from 'shared/utils.mjs'
|
||||
import { Tabs, Tab } from 'shared/components/tabs.mjs'
|
||||
|
||||
export const ns = ['account', 'measurements', 'designs']
|
||||
|
||||
/*
|
||||
* Helper component to display a tab heading
|
||||
*/
|
||||
export const _Tab = ({
|
||||
id, // The tab ID
|
||||
label, // A label for the tab, if not set we'll use the ID
|
||||
activeTab, // Which tab (id) is active
|
||||
setActiveTab, // Method to set the active tab
|
||||
}) => (
|
||||
<button
|
||||
className={`text-lg font-bold capitalize tab tab-bordered grow
|
||||
${activeTab === id ? 'tab-active' : ''}`}
|
||||
onClick={() => setActiveTab(id)}
|
||||
>
|
||||
{label ? label : id}
|
||||
</button>
|
||||
)
|
||||
|
||||
/*
|
||||
* Helper component to wrap a form control with a label
|
||||
*/
|
||||
export const FormControl = ({
|
||||
label, // the (top-left) label
|
||||
children, // Children to go inside the form control
|
||||
docs = false, // Optional top-right label
|
||||
labelBL = false, // Optional bottom-left label
|
||||
labelBR = false, // Optional bottom-right label
|
||||
forId = false, // ID of the for element we are wrapping
|
||||
}) => {
|
||||
const { setModal } = useContext(ModalContext)
|
||||
|
||||
if (labelBR && !labelBL) labelBL = <span></span>
|
||||
|
||||
const topLabelChildren = (
|
||||
<>
|
||||
<span className="label-text text-lg font-bold mb-0 text-inherit">{label}</span>
|
||||
{docs ? (
|
||||
<span className="label-text-alt">
|
||||
<button
|
||||
className="btn btn-ghost btn-sm btn-circle hover:btn-secondary"
|
||||
onClick={() =>
|
||||
setModal(
|
||||
<ModalWrapper flex="col" justify="top lg:justify-center" slideFrom="right">
|
||||
<div className="mdx max-w-prose">{docs}</div>
|
||||
</ModalWrapper>
|
||||
)
|
||||
}
|
||||
>
|
||||
<DocsIcon />
|
||||
</button>
|
||||
</span>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
const bottomLabelChildren = (
|
||||
<>
|
||||
{labelBL ? <span className="label-text-alt">{labelBL}</span> : null}
|
||||
{labelBR ? <span className="label-text-alt">{labelBR}</span> : null}
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="form-control w-full mt-2">
|
||||
{forId ? (
|
||||
<label className="label pb-0" htmlFor={forId}>
|
||||
{topLabelChildren}
|
||||
</label>
|
||||
) : (
|
||||
<div className="label pb-0">{topLabelChildren}</div>
|
||||
)}
|
||||
{children}
|
||||
{labelBL || labelBR ? (
|
||||
forId ? (
|
||||
<label className="label" htmlFor={forId}>
|
||||
{bottomLabelChildren}
|
||||
</label>
|
||||
) : (
|
||||
<div className="label">{bottomLabelChildren}</div>
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
* Helper method to wrap content in a button
|
||||
*/
|
||||
export const ButtonFrame = ({
|
||||
children, // Children of the button
|
||||
onClick, // onClick handler
|
||||
active, // Whether or not to render the button as active/selected
|
||||
accordion = false, // Set this to true to not set a background color when active
|
||||
dense = false, // Use less padding
|
||||
}) => (
|
||||
<button
|
||||
className={`
|
||||
btn btn-ghost btn-secondary
|
||||
w-full ${dense ? 'mt-1 py-0 btn-sm' : 'mt-2 py-4 h-auto content-start'}
|
||||
border-2 border-secondary text-left bg-opacity-20
|
||||
${accordion ? 'hover:bg-transparent' : 'hover:bg-secondary hover:bg-opacity-10'}
|
||||
hover:border-secondary hover:border-solid hover:border-2
|
||||
${active ? 'border-solid' : 'border-dotted'}
|
||||
${active && !accordion ? 'bg-secondary' : 'bg-transparent'}
|
||||
`}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
|
||||
/*
|
||||
* Input for strings
|
||||
*/
|
||||
export const StringInput = ({
|
||||
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
|
||||
}) => (
|
||||
<FormControl {...{ label, labelBL, labelBR, docs }} forId={id}>
|
||||
<input
|
||||
id={id}
|
||||
type="text"
|
||||
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'
|
||||
}`}
|
||||
/>
|
||||
</FormControl>
|
||||
)
|
||||
|
||||
/*
|
||||
* Input for passwords
|
||||
*/
|
||||
export const PasswordInput = ({
|
||||
label, // Label to use
|
||||
update, // onChange handler
|
||||
valid, // Method that should return whether the value is valid or not
|
||||
current, // The current value
|
||||
placeholder = '¯\\_(ツ)_/¯', // The placeholder text
|
||||
docs = false, // Docs to load, if any
|
||||
id = '', // An id to tie the input to the label
|
||||
}) => {
|
||||
const { t } = useTranslation(['account'])
|
||||
const [reveal, setReveal] = useState(false)
|
||||
|
||||
return (
|
||||
<FormControl
|
||||
label={label}
|
||||
docs={docs}
|
||||
forId={id}
|
||||
labelBR={
|
||||
<button
|
||||
className="btn btn-primary btn-ghost btn-xs -mt-2"
|
||||
onClick={() => setReveal(!reveal)}
|
||||
>
|
||||
{reveal ? t('hidePassword') : t('revealPassword')}
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<input
|
||||
id={id}
|
||||
type={reveal ? 'text' : 'password'}
|
||||
placeholder={placeholder}
|
||||
value={current}
|
||||
onChange={(evt) => update(evt.target.value)}
|
||||
className={`input w-full input-bordered ${
|
||||
valid(current) ? 'input-success' : 'input-error'
|
||||
}`}
|
||||
/>
|
||||
</FormControl>
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
* Input for email addresses
|
||||
*/
|
||||
export const EmailInput = ({
|
||||
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
|
||||
}) => (
|
||||
<FormControl {...{ label, docs, labelBL, labelBR }} forId={id}>
|
||||
<input
|
||||
id={id}
|
||||
type="email"
|
||||
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'
|
||||
}`}
|
||||
/>
|
||||
</FormControl>
|
||||
)
|
||||
|
||||
/*
|
||||
* Dropdown for designs
|
||||
*/
|
||||
export const DesignDropdown = ({
|
||||
label, // Label to use
|
||||
update, // onChange handler
|
||||
current, // The current value
|
||||
docs = false, // Docs to load, if any
|
||||
firstOption = null, // Any first option to add in addition to designs
|
||||
id = '', // An id to tie the input to the label
|
||||
}) => {
|
||||
const { t } = useTranslation(['designs'])
|
||||
|
||||
return (
|
||||
<FormControl label={label} docs={docs} forId={id}>
|
||||
<select
|
||||
id={id}
|
||||
className="select select-bordered w-full"
|
||||
onChange={(evt) => update(evt.target.value)}
|
||||
value={current}
|
||||
>
|
||||
{firstOption}
|
||||
{collection.map((design) => (
|
||||
<option key={design} value={design}>
|
||||
{t(`${design}.t`)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</FormControl>
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
* Input for an image
|
||||
*/
|
||||
export const ImageInput = ({
|
||||
label, // The label
|
||||
update, // The onChange handler
|
||||
current, // The current value
|
||||
original, // The original value
|
||||
docs = false, // Docs to load, if any
|
||||
active = false, // Whether or not to upload images
|
||||
imgType = 'showcase', // The image type
|
||||
imgSubid, // The image sub-id
|
||||
imgSlug, // The image slug or other unique identifier to use in the image ID
|
||||
id = '', // An id to tie the input to the label
|
||||
}) => {
|
||||
const { t } = useTranslation(ns)
|
||||
const backend = useBackend()
|
||||
const { setLoadingStatus } = useContext(LoadingStatusContext)
|
||||
const [url, setUrl] = useState(false)
|
||||
const [uploadedId, setUploadedId] = useState(false)
|
||||
|
||||
const upload = async (img, fromUrl = false) => {
|
||||
setLoadingStatus([true, 'uploadingImage'])
|
||||
const data = {
|
||||
type: imgType,
|
||||
subId: imgSubid,
|
||||
slug: imgSlug,
|
||||
}
|
||||
if (fromUrl) data.url = img
|
||||
else data.img = img
|
||||
const result = await backend.uploadAnonImage(data)
|
||||
setLoadingStatus([true, 'allDone', true, true])
|
||||
if (result.success) {
|
||||
update(result.data.imgId)
|
||||
setUploadedId(result.data.imgId)
|
||||
} else setLoadingStatus([true, 'backendError', true, false])
|
||||
}
|
||||
|
||||
const onDrop = useCallback(
|
||||
(acceptedFiles) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = async () => {
|
||||
if (active) upload(reader.result)
|
||||
else update(reader.result)
|
||||
}
|
||||
acceptedFiles.forEach((file) => reader.readAsDataURL(file))
|
||||
},
|
||||
[current]
|
||||
)
|
||||
|
||||
const { getRootProps, getInputProps } = useDropzone({ onDrop })
|
||||
|
||||
if (current)
|
||||
return (
|
||||
<FormControl label={label} docs={docs}>
|
||||
<div
|
||||
className="bg-base-100 w-full h-36 mb-2 mx-auto flex flex-col items-center text-center justify-center"
|
||||
style={{
|
||||
backgroundImage: `url(${
|
||||
uploadedId ? cloudflareImageUrl({ type: 'public', id: uploadedId }) : current
|
||||
})`,
|
||||
backgroundSize: 'contain',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundPosition: '50%',
|
||||
}}
|
||||
>
|
||||
<button
|
||||
className="btn btn-neutral btn-circle opacity-50 hover:opacity-100"
|
||||
onClick={() => update(original)}
|
||||
>
|
||||
<ResetIcon />
|
||||
</button>
|
||||
</div>
|
||||
</FormControl>
|
||||
)
|
||||
|
||||
return (
|
||||
<FormControl label={label} docs={docs} forId={id}>
|
||||
<div
|
||||
{...getRootProps()}
|
||||
className={`
|
||||
flex rounded-lg w-full flex-col items-center justify-center
|
||||
lg:p-6 lg:border-4 lg:border-secondary lg:border-dashed
|
||||
`}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
<p className="hidden lg:block p-0 m-0">{t('imgDragAndDropImageHere')}</p>
|
||||
<p className="hidden lg:block p-0 my-2">{t('or')}</p>
|
||||
<button className={`btn btn-secondary btn-outline mt-4 px-8`}>{t('imgSelectImage')}</button>
|
||||
</div>
|
||||
<p className="p-0 my-2 text-center">{t('or')}</p>
|
||||
<div className="flex flex-row items-center">
|
||||
<input
|
||||
id={id}
|
||||
type="url"
|
||||
className="input input-secondary w-full input-bordered"
|
||||
placeholder={t('imgPasteUrlHere')}
|
||||
value={current}
|
||||
onChange={active ? (evt) => setUrl(evt.target.value) : (evt) => update(evt.target.value)}
|
||||
/>
|
||||
{active && (
|
||||
<button
|
||||
className="btn btn-secondary ml-2 capitalize"
|
||||
disabled={!url || url.length < 1}
|
||||
onClick={() => upload(url, true)}
|
||||
>
|
||||
<UploadIcon /> {t('upload')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</FormControl>
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
* Input for an image that is active (it does upload the image)
|
||||
*/
|
||||
export const ActiveImageInput = (props) => <ImageInput {...props} active={true} />
|
||||
|
||||
/*
|
||||
* Input for an image that is passive (it does not upload the image)
|
||||
*/
|
||||
export const PassiveImageInput = (props) => <ImageInput {...props} active={false} />
|
||||
|
||||
/*
|
||||
* Input for a list of things to pick from
|
||||
*/
|
||||
export const ListInput = ({
|
||||
update, // the onChange handler
|
||||
label, // The label
|
||||
list, // The list of items to present { val, label, desc }
|
||||
current, // The (value of the) current item
|
||||
docs = false, // Docs to load, if any
|
||||
}) => (
|
||||
<FormControl label={label} docs={docs}>
|
||||
{list.map((item, i) => (
|
||||
<ButtonFrame key={i} active={item.val === current} onClick={() => update(item.val)}>
|
||||
<div className="w-full flex flex-col gap-2">
|
||||
<div className="w-full text-lg leading-5">{item.label}</div>
|
||||
{item.desc ? (
|
||||
<div className="w-full text-normal font-normal normal-case pt-1 leading-5">
|
||||
{item.desc}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</ButtonFrame>
|
||||
))}
|
||||
</FormControl>
|
||||
)
|
||||
|
||||
/*
|
||||
* Input for markdown content
|
||||
*/
|
||||
export const MarkdownInput = ({
|
||||
label, // The label
|
||||
current, // The current value (markdown)
|
||||
update, // The onChange handler
|
||||
placeholder, // The placeholder content
|
||||
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
|
||||
}) => (
|
||||
<FormControl {...{ label, labelBL, labelBR, docs }} forId={id}>
|
||||
<Tabs tabs={['edit', 'preview']}>
|
||||
<Tab key="edit">
|
||||
<div className="flex flex-row items-center mt-4">
|
||||
<textarea
|
||||
id={id}
|
||||
rows="5"
|
||||
className="textarea textarea-bordered textarea-lg w-full"
|
||||
value={current}
|
||||
placeholder={placeholder}
|
||||
onChange={(evt) => update(evt.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab key="preview">
|
||||
<div className="flex flex-row items-center mt-4">
|
||||
<Mdx md={current} />
|
||||
</div>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</FormControl>
|
||||
)
|
||||
|
||||
export const MeasieInput = ({
|
||||
imperial, // True for imperial, False for metric
|
||||
m, // The measurement name
|
||||
original, // The original value
|
||||
update, // The onChange handler
|
||||
placeholder, // The placeholder content
|
||||
docs = false, // Docs to load, if any
|
||||
id = '', // An id to tie the input to the label
|
||||
}) => {
|
||||
const { t } = useTranslation(['measurements'])
|
||||
const isDegree = isDegreeMeasurement(m)
|
||||
const units = imperial ? 'imperial' : 'metric'
|
||||
|
||||
const [localVal, setLocalVal] = useState(
|
||||
typeof original === 'undefined'
|
||||
? original
|
||||
: isDegree
|
||||
? Number(original)
|
||||
: measurementAsUnits(original, units)
|
||||
)
|
||||
const [validatedVal, setValidatedVal] = useState(measurementAsUnits(original, units))
|
||||
const [valid, setValid] = useState(null)
|
||||
|
||||
// Update onChange
|
||||
const localUpdate = (newVal) => {
|
||||
setLocalVal(newVal)
|
||||
const parsedVal = isDegree ? Number(newVal) : parseDistanceInput(newVal, imperial)
|
||||
if (parsedVal) {
|
||||
update(m, isDegree ? parsedVal : measurementAsMm(parsedVal, units))
|
||||
setValid(true)
|
||||
setValidatedVal(parsedVal)
|
||||
} else setValid(false)
|
||||
}
|
||||
|
||||
if (!m) return null
|
||||
|
||||
// Various visual indicators for validating the input
|
||||
let inputClasses = 'input-secondary'
|
||||
let bottomLeftLabel = null
|
||||
if (valid === true) {
|
||||
inputClasses = 'input-success'
|
||||
const val = `${validatedVal}${isDegree ? '°' : imperial ? '"' : 'cm'}`
|
||||
bottomLeftLabel = <span className="font-medium text-base text-success -mt-2 block">{val}</span>
|
||||
} else if (valid === false) {
|
||||
inputClasses = 'input-error'
|
||||
bottomLeftLabel = (
|
||||
<span className="font-medium text-error text-base -mt-2 block">¯\_(ツ)_/¯</span>
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
* I'm on the fence here about using a text input rather than number
|
||||
* Obviously, number is the more correct option, but when the user enter
|
||||
* text, it won't fire an onChange event and thus they can enter text and it
|
||||
* will not be marked as invalid input.
|
||||
* See: https://github.com/facebook/react/issues/16554
|
||||
*/
|
||||
return (
|
||||
<FormControl
|
||||
label={t(m) + (isDegree ? ' (°)' : '')}
|
||||
docs={docs}
|
||||
forId={id}
|
||||
labelBL={bottomLeftLabel}
|
||||
>
|
||||
<input
|
||||
id={id}
|
||||
type="number"
|
||||
placeholder={placeholder}
|
||||
value={localVal}
|
||||
onChange={(evt) => localUpdate(evt.target.value)}
|
||||
className={`input w-full input-bordered ${inputClasses}`}
|
||||
/>
|
||||
</FormControl>
|
||||
)
|
||||
}
|
13
sites/shared/components/joost.mjs
Normal file
13
sites/shared/components/joost.mjs
Normal file
File diff suppressed because one or more lines are too long
|
@ -1,6 +1,9 @@
|
|||
import { useState } from 'react'
|
||||
import { useContext, useState } from 'react'
|
||||
import { ModalContext } from 'shared/context/modal-context.mjs'
|
||||
import { ModalWrapper } from 'shared/components/wrappers/modal.mjs'
|
||||
|
||||
export const Lightbox = ({ children }) => {
|
||||
export const Lightbox = ({ children, buttonClasses = '', boxClasses = false, modalProps = {} }) => {
|
||||
const { setModal } = useContext(ModalContext)
|
||||
const [box, setBox] = useState(false)
|
||||
|
||||
if (box)
|
||||
|
@ -26,8 +29,24 @@ export const Lightbox = ({ children }) => {
|
|||
)
|
||||
|
||||
return (
|
||||
<div onClick={() => setBox(!box)} className="hover:cursor-zoom-in">
|
||||
<button
|
||||
onClick={() =>
|
||||
setModal(
|
||||
<ModalWrapper
|
||||
flex="col"
|
||||
justify="top lg:justify-center"
|
||||
slideFrom="right"
|
||||
{...modalProps}
|
||||
>
|
||||
{boxClasses ? <div className={boxClasses}>{children}</div> : children}
|
||||
</ModalWrapper>
|
||||
)
|
||||
}
|
||||
className={buttonClasses}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
//<button onClick={() => setBox(!box)} className={`hover:cursor-zoom-in ${className}`}>
|
||||
|
|
47
sites/shared/components/link.mjs
Normal file
47
sites/shared/components/link.mjs
Normal file
|
@ -0,0 +1,47 @@
|
|||
// __SDEFILE__ - This file is a dependency for the stand-alone environment
|
||||
import Link from 'next/link'
|
||||
|
||||
const linkClasses =
|
||||
'underline decoration-2 hover:decoration-4 text-secondary hover:text-secondary-focus'
|
||||
|
||||
const AnchorLink = ({ id, txt = false, children }) => (
|
||||
<a href={`#${id}`} className={linkClasses} title={txt ? txt : ''}>
|
||||
{txt ? txt : children}
|
||||
</a>
|
||||
)
|
||||
|
||||
const PageLink = ({ href, txt = false, children }) => (
|
||||
<Link href={href} className={linkClasses} title={txt ? txt : ''}>
|
||||
{children ? children : txt}
|
||||
</Link>
|
||||
)
|
||||
|
||||
const WebLink = ({ href, txt = false, children }) => (
|
||||
<a href={href} className={linkClasses} title={txt ? txt : ''}>
|
||||
{children ? children : txt}
|
||||
</a>
|
||||
)
|
||||
|
||||
const CardLink = ({
|
||||
bg = 'bg-base-200',
|
||||
textColor = 'text-base-content',
|
||||
href,
|
||||
title,
|
||||
text,
|
||||
icon,
|
||||
}) => (
|
||||
<Link
|
||||
href={href}
|
||||
className={`px-8 ${bg} py-10 rounded-lg block ${textColor}
|
||||
hover:bg-secondary hover:bg-opacity-10 shadow-lg
|
||||
transition-color duration-300 grow`}
|
||||
>
|
||||
<h2 className="mb-4 text-inherit flex flex-row gap-4 justify-between items-center font-medium">
|
||||
{title}
|
||||
<span className="shrink-0">{icon}</span>
|
||||
</h2>
|
||||
<p className="font-medium text-inherit italic text-lg">{text}</p>
|
||||
</Link>
|
||||
)
|
||||
|
||||
export { linkClasses, Link, AnchorLink, PageLink, WebLink, CardLink }
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue