// Dependencies
import { atomWithHash } from 'jotai-location'
import {
about,
collection,
tags,
techniques,
designers,
developers,
examples,
measurements,
requiredMeasurements,
optionalMeasurements,
} from '@freesewing/collection'
import { capitalize, linkClasses, mutateObject } from '@freesewing/utils'
import { measurements as measurementsTranslations } from '@freesewing/i18n'
// Context
import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus'
import { ModalContext } from '@freesewing/react/context/Modal'
// Hooks
import React, { useState, useContext, Fragment } from 'react'
import { useAtom } from 'jotai'
// Components
import { Link as WebLink, AnchorLink } from '@freesewing/react/components/Link'
import {
CircleIcon,
CisFemaleIcon,
DocsIcon,
FilterIcon,
HeartIcon,
NewPatternIcon,
ResetIcon,
ShowcaseIcon,
} from '@freesewing/react/components/Icon'
import {
lineDrawingsFront as lineDrawings,
lineDrawingsBack,
} from '@freesewing/react/components/LineDrawing'
import { IconButton } from '@freesewing/react/components/Button'
import { ModalWrapper } from '@freesewing/react/components/Modal'
import { KeyVal } from '@freesewing/react/components/KeyVal'
import { CopyToClipboard as Copy } from 'react-copy-to-clipboard'
const filterAtom = atomWithHash('filter', { example: true })
export const useFilter = () => {
return useAtom(filterAtom)
}
/**
* React component to show the FreeSewing collection and pick a design
*
* @param {object} props - All React props
* @param {function} Link - An optional framework specific Link component for client-side routing
*/
export const Collection = ({ Link = false, linkTo = 'about' }) => {
if (!Link) Link = WebLink
// State
const [filter, setFilter] = useFilter()
const [showFilters, setShowFilters] = useState(false)
/*
* Apply filter
*/
const filtered = {}
for (const d of collection) {
let skip = false
if (
filter.tag &&
filter.tag.filter((tag) => about[d].tags.includes(tag)).length < filter.tag.length
)
skip = true
if (
filter.tech &&
filter.tech.filter((tech) => about[d].techniques.includes(tech)).length < filter.tech.length
)
skip = true
if (filter.difficulty && filter.difficulty !== about[d].difficulty) skip = true
if (!skip) filtered[d] = d
}
const updateFilter = (path, val) => {
// Allow clicking the same difficulty to remove it as a filter
if (path === 'difficulty' && val === filter.difficulty) val = 'unset'
const newFilter = mutateObject({ ...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 (
<>
{Object.keys(filtered)
.sort()
.map((d) => (
{d}
))}
{showFilters ? (
<>
Filters ({Object.keys(filtered).length}/{collection.length})
Tags:
{tags.map((tag) => (
toggle('tag', tag)}
>
{tag}
))}
Techniques
{techniques.sort().map((tech) => (
toggle('tech', tech)}
>
{tech}
))}
Difficulty:
{[1, 2, 3, 4, 5].map((score) => (
updateFilter('difficulty', score)}
key={score}
className={`tw-daisy-btn tw-daisy-btn-sm ${
filter.difficulty === score
? 'tw-daisy-btn-secondary tw-daisy-btn-outline'
: 'tw-daisy-btn-ghost'
}`}
>
))}
updateFilter('example', !filter.example)}
>
{filter.example ? : }
{filter.example ? 'Show Line Drawings' : 'Show Examples'}
setFilter({ example: 1 })}
>
Clear Filter
setShowFilters(false)}
>
Hide Filters
>
) : (
updateFilter('example', !filter.example)}
>
{filter.example ? : }
{filter.example ? 'Show Line Drawings' : 'Show Examples'}
setShowFilters(true)}
>
Show Filters
)}
{Object.keys(filtered)
.sort()
.map((d) => (
))}
>
)
}
/*
* A helper component to show a design technique
*
* @param {object} props - All React props
* @param {function} props.Link - A Link component, typically specific to the framework for client-side routing
* @param {string} props.technique - The technique name/id
*/
const Technique = ({ Link = WebLink, technique }) => (
{technique}
)
/*
* A helper component to show a design tag
*
* @param {object} props - All React props
* @param {function} props.Link - A Link component, typically specific to the framework for client-side routing
* @param {string} props.tag - The tag name/id
*/
const Tag = ({ Link = WebLink, technique }) => (
{tag}
)
const DesignCard = ({ name, lineDrawing = false, linkTo, Link }) => {
if (!Link) Link = WebLink
const LineDrawing =
lineDrawing && lineDrawings[name]
? lineDrawings[name]
: ({ className }) =>
const exampleImageUrl = examples.href[name] ? examples.href[name] : noExample
const bg = { aspectRatio: '1/1.4' }
if (!lineDrawing) {
bg.backgroundImage = `url(${exampleImageUrl}`
bg.backgroundSize = 'cover'
bg.backgroundPosition = 'center center'
}
return (
{about[name].name}
{lineDrawing ? (
) : (
)}
)
}
/*
* A helper component to show difficulety of a design
*
* @param {object} props - All React props
* @param {number} props.score - The difficulty score of the design (1-5)
*/
const Difficulty = ({ score = 0, className = '' }) => (
{[0, 1, 2, 3, 4].map((i) => (
))}
)
const linkBuilders = {
new: (design) => `/-/?d=${design.toLowerCase()}`,
docs: (design) => `/docs/designs/${design.toLowerCase()}/`,
about: (design) => `/designs/${design.toLowerCase()}/`,
}
const noExample =
'https://images.pexels.com/photos/5626595/pexels-photo-5626595.jpeg?cs=srgb&fm=jpg&w=640&h=427'
/**
* React component to show info about a FreeSewing design
*
* @param {object} props - All React props
* @param {string} design - The name/id of the design
* @param {function} Link - An optional framework specific Link component for client-side routing
*/
export const DesignInfo = ({ Link = false, design = false, noDocsLink = false }) => {
if (!Link) Link = WebLink
// State
const [back, setBack] = useState(false)
// Context
const { setModal, clearModal } = useContext(ModalContext)
const { setLoadingStatus } = useContext(LoadingStatusContext)
if (!design) return null
if (['teagan', 'trayvon'].includes(design))
return (
We are not rendering the design info for {design} because of a bug we are still working on.
)
// Line drawings
const LineDrawing = lineDrawings[design] || []
const LineDrawingBack = lineDrawingsBack[design] || []
const hasBack = !Array.isArray(LineDrawingBack)
// Make sure these always hold arrays, that way we can just map() over them in the JSX output
const codeBy = Array.isArray(about[design].code) ? about[design].code : [about[design].code]
const designBy = Array.isArray(about[design].design)
? about[design].design
: [about[design].design]
const tags = about[design].tags || []
const techniques = about[design].techniques || []
const colors = {
1: 'success',
2: 'success',
3: 'warning',
4: 'warning',
5: 'error',
}
const makeButton = (
New {capitalize(design)} pattern
)
const buttons = noDocsLink ? (
makeButton
) : (
Documentation
{makeButton}
)
return (
<>
{buttons}
{back ? : }
{Array.isArray(LineDrawingBack) ? null : (
setBack(!back)}
>
{back ? 'Front' : 'Back'} view
)}
Description
{about[design].description}
By
{codeBy.map((code) => (
))}
{designBy.map((code) => (
))}
Difficulty
{optionalMeasurements[design].length > 0 ? (
<>
Optional Measurements
{optionalMeasurements[design].map((m, i) => (
{measurementsTranslations[m]}
{i < optionalMeasurements[design].length - 1 ? , : null}
))}
>
) : null}
{requiredMeasurements[design].length > 0 ? (
<>
Required Measurements
{requiredMeasurements[design].map((m, i) => (
{measurementsTranslations[m]}
{i < requiredMeasurements[design].length - 1 ? , : null}
))}
>
) : null}
Tags
{tags.map((tag) => (
{tag}
))}
Techniques
{techniques.map((tech) => (
{tech}
))}
Examples
{buttons}
>
)
}
const SharingIsCaring = ({ design }) => (
<>
Use #FreeSewing{capitalize(design)} to facilitate discovery
Please use the{' '}
#FreeSewing{capitalize(design)}
{' '}
hashtag when discussing FreeSewing's {capitalize(design)} pattern online.
Doing so can help others discover your post, which really is a win-win.
If you like, you can copy the hashtag below:
>
)
/*
// __SDEFILE__ - This file is a dependency for the stand-alone environment
// Dependencies
import {
nsMerge,
capitalize,
optionsMenuStructure,
optionType,
cloudflareImageUrl,
horFlexClasses,
} 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 { 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, DocsIcon } from 'shared/components/icons.mjs'
import { DynamicMdx } from 'shared/components/mdx/dynamic.mjs'
// Translation namespaces used on this page
export const ns = nsMerge(
'account',
'tags',
'techniques',
'measurements',
'workbench',
'designs',
'tags'
)
const Option = ({ id, option, design }) =>
optionType(option) === 'constant' ? null : (
)
const OptionGroup = ({ id, group, t, design }) => (
{t(`workbench:${id}`)}
{Object.entries(group).map(([sid, entry]) =>
entry.isGroup ? (
) : (
)
)}
)
export const SimpleOptionsList = ({ options, t, design }) => {
const structure = optionsMenuStructure(options, {}, true)
const output = []
for (const [key, entry] of Object.entries(structure)) {
const shared = { key, t, design, id: key }
if (entry.isGroup) output.push( )
else output.push( )
}
return
}
export const DesignInfo = ({ design, docs = false, workbench = false, modal = 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 }) =>
// Docs content
const docsContent = (
<>
{t('account:docs')}
{Object.keys(config.options).length > 0 ? (
) : null}
>
)
return (
<>
#FreeSewing{capitalize(design)}
{t(`designs:${design}.d`)}
{workbench ? null : (
{t('tags:newThingPattern', { thing: capitalize(design) })}
)}
{docs ? null : (
{t('account:docs')}
)}
{docs || workbench || modal ? null : (
Jump to:
t.split(':').pop().trim()}
/>
{examples &&
}
{['needs', 'fabric'].map((page) => (
t.split(':').pop().trim()}
/>
))}
)}
{docs ? null : (
<>
t.split(':').pop().trim()}
/>
>
)}
{docs ? docsContent : null}
{examples ? (
<>
{t('account:examples')}
{examples[design] ? (
{examples[design].map((ex) => (
setModal(
)
}
>
))}
) : (
{t('account:noExamples')}
{t('account:noExamplesMsg')}
{t('account:showcaseNew')}
)}
>
) : null}
{docs
? null
: ['needs', 'fabric'].map((page) => (
t.split(':').pop().trim()}
/>
))}
{docs ? null : docsContent}
{workbench ? null : (
{t('tags:newThingPattern', { thing: capitalize(design) })}
)}
{docs ? null : (
{t('account:docs')}
)}
{t('account:specifications')}
{t('account:design')}
{designs[design].design.map((person) => (
{person}
))}
{t('account:code')}
{designs[design].code.map((person) => (
{person}
))}
{t('tags:difficulty')}
{t('tags:tags')}
{designs[design].tags.map((tag) => (
{t(`tags:${tag}`)}
))}
{t('techniques:techniques')}
{designs[design].techniques.map((tech) => (
{t(`techniques:${tech}`)}
))}
{Object.keys(measies.required).length > 0 ? (
<>
{t('account:requiredMeasurements')}
{Object.keys(measies.required)
.sort()
.map((m) => (
))}
>
) : null}
{Object.keys(measies.optional).length > 0 ? (
<>
{t('account:optionalMeasurements')}
{Object.keys(measies.optional)
.sort()
.map((m) => (
))}
>
) : null}
{Object.keys(config.options).length > 0 ? (
<>
{t('account:designOptions')}
>
) : null}
{t('account:parts')}
{config.draftOrder.map((part) => (
{part}
))}
{Object.keys(config.plugins).length > 0 ? (
<>
{t('account:plugins')}
{Object.keys(config.plugins).map((plugin) => (
{plugin}
))}
>
) : null}
>
)
}
*/