// Dependencies import orderBy from 'lodash/orderBy.js' import { cloudflareImageUrl, capitalize, shortDate, horFlexClasses, newPatternUrl, patternUrlFromState, } 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' /** * A component to manage a pattern in the user's account data * * @component * @param {object} props - All component props * @param {number} props.id - The pattern ID to load * @param {React.Component} props.Link - A framework specific Link component for client-side routing * @returns {JSX.Element} */ 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.name += ' (clone)' 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 tw:hover:text-secondary-content tw:hover: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> ) } /** * A component to display a card representing a pattern in the user's account data. * * This is a pure render component, you have to pass in the data. * * @component * @param {object} props - All component props * @param {React.Component} props.Link - A framework specific Link component for client-side routing * @param {string} [props.href = false] - An optional URL the pattern card should link to * @param {function} [props.onClick = false] - An optional onClick handler * @param {object} props.pattern - An object holding the pattern data * @param {string} [props.size = 'md'] - The size, one of lg, md, sm, or xs * @param {boolean} [props.useA = false] - Whether to use an a tag of Link component when passing in a href prop * @returns {JSX.Element} */ export const PatternCard = ({ pattern, href = false, onClick = false, useA = false, size = 'md', Link = false, }) => { if (!Link) Link = WebLink const sizes = { lg: 96, md: 48, 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 tw:hover:text-secondary-content tw:hover:cursor-pointer" > <span className="tw:text-secondary-content hover:tw:decoration-0">{label}</span> </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 tw:md:flex-nowrap tw:flex-row tw:gap-2 tw:w-full"> <div className="tw:w-full tw:md: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 tw:lg: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={patternUrlFromState(pattern, true)} 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> </> )