405 lines
13 KiB
JavaScript
405 lines
13 KiB
JavaScript
![]() |
// Dependencies
|
||
|
import orderBy from 'lodash/orderBy.js'
|
||
|
import {
|
||
|
cloudflareImageUrl,
|
||
|
capitalize,
|
||
|
shortDate,
|
||
|
horFlexClasses,
|
||
|
newPatternUrl,
|
||
|
} from '@freesewing/utils'
|
||
|
import { urls, control as controlConfig } from '@freesewing/config'
|
||
|
|
||
|
// Context
|
||
|
import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus'
|
||
|
import { ModalContext } from '@freesewing/react/context/Modal'
|
||
|
|
||
|
// Hooks
|
||
|
import React, { useState, useEffect, useContext } from 'react'
|
||
|
import { useAccount } from '@freesewing/react/hooks/useAccount'
|
||
|
import { useBackend } from '@freesewing/react/hooks/useBackend'
|
||
|
import { useSelection } from '@freesewing/react/hooks/useSelection'
|
||
|
|
||
|
// Components
|
||
|
import Markdown from 'react-markdown'
|
||
|
import {
|
||
|
StringInput,
|
||
|
MarkdownInput,
|
||
|
PassiveImageInput,
|
||
|
ListInput,
|
||
|
} from '@freesewing/react/components/Input'
|
||
|
import { Link as WebLink, AnchorLink } from '@freesewing/react/components/Link'
|
||
|
import {
|
||
|
BoolNoIcon,
|
||
|
BoolYesIcon,
|
||
|
CloneIcon,
|
||
|
EditIcon,
|
||
|
FreeSewingIcon,
|
||
|
OkIcon,
|
||
|
NoIcon,
|
||
|
PatternIcon,
|
||
|
ShowcaseIcon,
|
||
|
ResetIcon,
|
||
|
UploadIcon,
|
||
|
} from '@freesewing/react/components/Icon'
|
||
|
import { DisplayRow } from './shared.mjs'
|
||
|
import { TimeAgo } from '@freesewing/react/components/Time'
|
||
|
import { Popout } from '@freesewing/react/components/Popout'
|
||
|
import { ModalWrapper } from '@freesewing/react/components/Modal'
|
||
|
import { KeyVal } from '@freesewing/react/components/KeyVal'
|
||
|
|
||
|
export const Pattern = ({ id, Link }) => {
|
||
|
if (!Link) Link = WebLink
|
||
|
// Hooks
|
||
|
const { account, control } = useAccount()
|
||
|
const { setLoadingStatus } = useContext(LoadingStatusContext)
|
||
|
const backend = useBackend()
|
||
|
|
||
|
// 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, 'Loading pattern from backend'])
|
||
|
const [status, body] = await backend.getPattern(id)
|
||
|
if (status === 200) {
|
||
|
setPattern(body.pattern)
|
||
|
setName(body.pattern.name)
|
||
|
setImage(body.pattern.image)
|
||
|
setIsPublic(body.pattern.public ? true : false)
|
||
|
setNotes(body.pattern.notes)
|
||
|
setLoadingStatus([true, 'Loaded pattern', true, true])
|
||
|
} else setLoadingStatus([true, 'An error occured. Please report this', true, false])
|
||
|
}
|
||
|
if (id) getPattern()
|
||
|
}, [id])
|
||
|
|
||
|
const save = async () => {
|
||
|
setLoadingStatus([true, 'Gathering info'])
|
||
|
// 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, 'Saving pattern'])
|
||
|
const [status, body] = await backend.updatePattern(pattern.id, data)
|
||
|
if (status === 200 && body.result === 'success') {
|
||
|
setPattern(body.pattern)
|
||
|
setEdit(false)
|
||
|
setLoadingStatus([true, 'Nailed it', true, true])
|
||
|
} else setLoadingStatus([true, 'An error occured. Please report this.', true, false])
|
||
|
}
|
||
|
|
||
|
const clone = async () => {
|
||
|
setLoadingStatus([true, 'Cloning pattern'])
|
||
|
// Compile data
|
||
|
const data = { ...pattern }
|
||
|
delete data.id
|
||
|
delete data.createdAt
|
||
|
delete data.data
|
||
|
delete data.userId
|
||
|
delete data.img
|
||
|
data.settings = JSON.parse(data.settings)
|
||
|
const [status, body] = await backend.createPattern(data)
|
||
|
if (status === 201 && body.result === 'created') {
|
||
|
setLoadingStatus([true, 'Loading newly created pattern', true, true])
|
||
|
window.location = `/account/data/patterns/pattern?id=${body.pattern.id}`
|
||
|
} else setLoadingStatus([true, 'We failed to create this pattern', true, false])
|
||
|
}
|
||
|
|
||
|
const togglePublic = async () => {
|
||
|
setLoadingStatus([true, 'Updating pattern'])
|
||
|
// Compile data
|
||
|
const data = { public: !pattern.public }
|
||
|
const [status, body] = await backend.updatePattern(pattern.id, data)
|
||
|
if (status === 200 && body.result === 'success') {
|
||
|
setPattern(body.pattern)
|
||
|
setLoadingStatus([true, 'Nailed it', true, true])
|
||
|
} else setLoadingStatus([true, 'An error occured. Please report this.', true, false])
|
||
|
}
|
||
|
if (!pattern) return null
|
||
|
|
||
|
const header = (
|
||
|
<PatternHeader {...{ pattern, Link, account, setModal, setEdit, togglePublic, save, clone }} />
|
||
|
)
|
||
|
|
||
|
if (!edit)
|
||
|
return (
|
||
|
<div className="tw-w-full">
|
||
|
{pattern.public ? (
|
||
|
<Popout note>
|
||
|
<h5>This is the private view of your pattern</h5>
|
||
|
<p>
|
||
|
Everyone can access the public view since this is a public pattern.
|
||
|
<br />
|
||
|
But only you can access this private view.
|
||
|
</p>
|
||
|
<p className="tw-text-right">
|
||
|
<Link
|
||
|
className={`tw-daisy-btn tw-daisy-btn-secondary hover:tw-text-secondary-content hover:tw-no-underline`}
|
||
|
href={`/pattern?id=${pattern.id}`}
|
||
|
>
|
||
|
<PatternIcon />
|
||
|
Public View
|
||
|
</Link>
|
||
|
</p>
|
||
|
</Popout>
|
||
|
) : null}
|
||
|
{header}
|
||
|
{control >= controlConfig.account.patterns.notes && (
|
||
|
<>
|
||
|
<h3>Notes</h3>
|
||
|
<Markdown>{pattern.notes}</Markdown>
|
||
|
</>
|
||
|
)}
|
||
|
</div>
|
||
|
)
|
||
|
|
||
|
return (
|
||
|
<div className="tw-w-full">
|
||
|
<h2>Edit pattern {pattern.name}</h2>
|
||
|
|
||
|
{/* Name is always shown */}
|
||
|
<span id="name"></span>
|
||
|
<StringInput
|
||
|
id="pattern-name"
|
||
|
label="Name"
|
||
|
update={setName}
|
||
|
current={name}
|
||
|
original={pattern.name}
|
||
|
placeholder="Maurits Cornelis Escher"
|
||
|
valid={(val) => val && val.length > 0}
|
||
|
/>
|
||
|
|
||
|
{/* img: Control level determines whether or not to show this */}
|
||
|
<span id="image"></span>
|
||
|
{account.control >= controlConfig.account.sets.img ? (
|
||
|
<PassiveImageInput
|
||
|
id="pattern-img"
|
||
|
label="Image"
|
||
|
update={setImage}
|
||
|
current={image}
|
||
|
valid={(val) => val.length > 0}
|
||
|
/>
|
||
|
) : null}
|
||
|
|
||
|
{/* public: Control level determines whether or not to show this */}
|
||
|
<span id="public"></span>
|
||
|
{account.control >= controlConfig.account.patterns.public ? (
|
||
|
<ListInput
|
||
|
id="pattern-public"
|
||
|
label="Public"
|
||
|
update={setIsPublic}
|
||
|
list={[
|
||
|
{
|
||
|
val: true,
|
||
|
label: (
|
||
|
<div className="tw-flex tw-flex-row tw-items-center tw-flex-wrap tw-justify-between tw-w-full">
|
||
|
<span>Public Pattern</span>
|
||
|
<OkIcon
|
||
|
className="tw-w-8 tw-h-8 tw-text-success tw-bg-base-100 tw-rounded-full tw-p-1"
|
||
|
stroke={4}
|
||
|
/>
|
||
|
</div>
|
||
|
),
|
||
|
desc: 'Public patterns can be shared with other FreeSewing users',
|
||
|
},
|
||
|
{
|
||
|
val: false,
|
||
|
label: (
|
||
|
<div className="tw-flex tw-flex-row tw-items-center tw-flex-wrap tw-justify-between tw-w-full">
|
||
|
<span>Private Pattern</span>
|
||
|
<NoIcon
|
||
|
className="tw-w-8 tw-h-8 tw-text-error tw-bg-base-100 tw-rounded-full tw-p-1"
|
||
|
stroke={3}
|
||
|
/>
|
||
|
</div>
|
||
|
),
|
||
|
desc: 'Private patterns are yours and yours alone',
|
||
|
},
|
||
|
]}
|
||
|
current={isPublic}
|
||
|
/>
|
||
|
) : null}
|
||
|
|
||
|
{/* notes: Control level determines whether or not to show this */}
|
||
|
<span id="notes"></span>
|
||
|
{account.control >= controlConfig.account.patterns.notes ? (
|
||
|
<MarkdownInput id="pattern-notes" label="Notes" update={setNotes} current={notes} />
|
||
|
) : null}
|
||
|
<div className="tw-flex tw-flex-row tw-items-center tw-align-end tw-gap-2 tw-mt-8">
|
||
|
<button
|
||
|
onClick={() => setEdit(false)}
|
||
|
className={`tw-daisy-btn tw-daisy-btn-primary tw-daisy-btn-outline`}
|
||
|
>
|
||
|
<ResetIcon />
|
||
|
Cancel
|
||
|
</button>
|
||
|
<button onClick={save} className="tw-daisy-btn tw-daisy-btn-primary tw-grow">
|
||
|
<UploadIcon />
|
||
|
Save Pattern
|
||
|
</button>
|
||
|
</div>
|
||
|
</div>
|
||
|
)
|
||
|
}
|
||
|
|
||
|
export const PatternCard = ({
|
||
|
pattern,
|
||
|
href = false,
|
||
|
onClick = false,
|
||
|
useA = false,
|
||
|
size = 'md',
|
||
|
Link = false,
|
||
|
}) => {
|
||
|
if (!Link) Link = WebLink
|
||
|
const sizes = {
|
||
|
lg: 96,
|
||
|
md: 52,
|
||
|
sm: 36,
|
||
|
xs: 20,
|
||
|
}
|
||
|
const s = sizes[size]
|
||
|
|
||
|
const wrapperProps = {
|
||
|
className: `tw-bg-base-300 tw-w-full tw-mb-2 tw-mx-auto tw-flex tw-flex-col tw-items-start tw-text-center tw-justify-center tw-rounded tw-shadow tw-py-4 tw-w-${s} tw-aspect-square`,
|
||
|
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>
|
||
|
}
|
||
|
|
||
|
const BadgeLink = ({ label, href }) => (
|
||
|
<a
|
||
|
href={href}
|
||
|
className="tw-daisy-badge tw-daisy-badge-secondary tw-font-bold tw-daisy-badge-lg hover:tw-text-secondary-content hover:tw-no-underline"
|
||
|
>
|
||
|
{label}
|
||
|
</a>
|
||
|
)
|
||
|
|
||
|
/**
|
||
|
* Helper component to show the pattern title, image, and various buttons
|
||
|
*/
|
||
|
const PatternHeader = ({
|
||
|
pattern,
|
||
|
Link,
|
||
|
account,
|
||
|
setModal,
|
||
|
setEdit,
|
||
|
togglePublic,
|
||
|
save,
|
||
|
clone,
|
||
|
}) => (
|
||
|
<>
|
||
|
<h2>{pattern.name}</h2>
|
||
|
<div className="tw-flex tw-flex-row tw-flex-wrap tw-gap-2 tw-text-sm tw-items-center tw-mb-2">
|
||
|
<KeyVal k="ID" val={pattern.id} color="secondary" />
|
||
|
<KeyVal k="Created" val={<TimeAgo iso={pattern.createdAt} />} color="secondary" />
|
||
|
<KeyVal k="Updated" val={<TimeAgo iso={pattern.updatedAt} />} color="secondary" />
|
||
|
<KeyVal k="Public" val={pattern.public ? 'yes' : 'no'} color="secondary" />
|
||
|
</div>
|
||
|
<div className="tw-flex tw-flex-wrap md:tw-flex-nowrap tw-flex-row tw-gap-2 tw-w-full">
|
||
|
<div className="tw-w-full md:tw-w-96 tw-shrink-0">
|
||
|
<PatternCard pattern={pattern} size="md" Link={Link} />
|
||
|
</div>
|
||
|
<div className="tw-flex tw-flex-col tw-justify-end tw-gap-2 tw-mb-2 tw-grow">
|
||
|
{account.control > 3 && (pattern?.public || pattern.userId === account.id) ? (
|
||
|
<div className="tw-flex tw-flex-row tw-gap-2 tw-items-center">
|
||
|
<BadgeLink label="JSON" href={`${urls.backend}/patterns/${pattern.id}.json`} />
|
||
|
<BadgeLink label="YAML" href={`${urls.backend}/patterns/${pattern.id}.yaml`} />
|
||
|
</div>
|
||
|
) : (
|
||
|
<span></span>
|
||
|
)}
|
||
|
<button
|
||
|
onClick={() =>
|
||
|
setModal(
|
||
|
<ModalWrapper flex="col" justify="top lg:tw-justify-center" slideFrom="right">
|
||
|
<img src={cloudflareImageUrl({ type: 'public', id: pattern.img })} />
|
||
|
</ModalWrapper>
|
||
|
)
|
||
|
}
|
||
|
className={`tw-daisy-btn tw-daisy-btn-secondary tw-daisy-btn-outline ${horFlexClasses}`}
|
||
|
>
|
||
|
<ShowcaseIcon />
|
||
|
Show Image
|
||
|
</button>
|
||
|
{account.control > 3 ? (
|
||
|
<button
|
||
|
onClick={() => togglePublic()}
|
||
|
className={`tw-daisy-btn tw-daisy-btn-${pattern.public ? 'error' : 'success'} tw-daisy-btn-outline ${horFlexClasses} hover:tw-text-${pattern.public ? 'error' : 'success'}-content`}
|
||
|
>
|
||
|
{pattern.public ? <BoolNoIcon /> : <BoolYesIcon />}
|
||
|
Make pattern {pattern.public ? 'private' : 'public'}
|
||
|
</button>
|
||
|
) : null}
|
||
|
{pattern.userId === account.id && (
|
||
|
<>
|
||
|
<Link
|
||
|
href={`/account/patterns/${pattern.design}/edit?id=${pattern.id}`}
|
||
|
className={`tw-daisy-btn tw-daisy-btn-primary tw-daisy-btn-outline ${horFlexClasses}`}
|
||
|
>
|
||
|
<FreeSewingIcon /> Update Pattern
|
||
|
</Link>
|
||
|
<button
|
||
|
className={`tw-daisy-btn tw-daisy-btn-primary tw-daisy-btn-outline ${horFlexClasses}`}
|
||
|
onClick={clone}
|
||
|
>
|
||
|
<CloneIcon /> Clone Pattern
|
||
|
</button>
|
||
|
<button
|
||
|
onClick={() => setEdit(true)}
|
||
|
className={`tw-daisy-btn tw-daisy-btn-primary ${horFlexClasses}`}
|
||
|
>
|
||
|
<EditIcon /> Edit Pattern Metadata
|
||
|
</button>
|
||
|
</>
|
||
|
)}
|
||
|
</div>
|
||
|
</div>
|
||
|
</>
|
||
|
)
|