1
0
Fork 0

Merge branch 'joost' into plugins-scale

This commit is contained in:
Joost De Cock 2023-10-15 16:05:28 +02:00
commit d739e8f5bd
24466 changed files with 405611 additions and 707715 deletions

View 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} />

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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

View 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>
)
}

View 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

View 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

View 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>
)
}

View 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
}

View 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

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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)}&hellip;</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>
)
}

View 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>
)
}

View 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

View 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

View file

@ -0,0 +1,3 @@
import { AccountLinks } from './links.mjs'
export const AccountOverview = ({ app }) => <AccountLinks app={app} />

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

File diff suppressed because it is too large Load diff

View 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>
)

View 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>
)
}

View 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

View 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>
)
}

View 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 />
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)

View 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>
)
}

View file

@ -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">&raquo;</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
)
}

View 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>
)

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)

View 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>
)

View 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>
)
}

View 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>
)

View file

@ -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 }
}

View 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

View 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>
)
}

View file

@ -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>
)

View 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>
)
}

View 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>
</>
)
}

View 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>
)
}

View 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>
)
}

View 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' })

View 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>
</>
)
}

View 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"
/>
</>
)

View 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"
/>
</>
)

View 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"
/>
</>
)

View 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,
}

View 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}` })

File diff suppressed because one or more lines are too long

View 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"
/>
</>
)

View file

@ -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'])} />
}

View file

@ -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'])

View 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>
)
}

View 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

View 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

View 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

View 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

View 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

View 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: Закрити

View 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>
</>
)
}

View 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>
)

View file

@ -0,0 +1 @@
sponsors: bla bla

View file

@ -0,0 +1 @@
sponsors: bla bla

View file

@ -0,0 +1 @@
sponsors: bla bla

View file

@ -0,0 +1 @@
sponsors: bla bla

View 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>
)
}

View file

@ -0,0 +1 @@
sponsors: bla bla

View file

@ -0,0 +1 @@
sponsors: бла-бла-бла.

View 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'

View 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>
)
}

View 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'

View 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'

View 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>
</>
)
}

View 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'

View 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'

View 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 публікує анонімні заміри тіла як відкриті дані для наукових досліджень. Ви маєте право відмовитися від цього'

View 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

View 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>
)
}

File diff suppressed because one or more lines are too long

View file

@ -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}`}>

View 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