// __SDEFILE__ - This file is a dependency for the stand-alone environment import { freeSewingConfig as config } from 'shared/config/freesewing.config.mjs' // Context import { LoadingStatusContext } from 'shared/context/loading-status-context.mjs' // Hooks import { useTranslation } from 'next-i18next' import { useState, Fragment, useContext } from 'react' import { useAccount } from 'shared/hooks/use-account.mjs' import { useBackend } from 'shared/hooks/use-backend.mjs' // Components import { Popout } from 'shared/components/popout/index.mjs' import { HeartIcon, ChatIcon, BugIcon, SettingsIcon, DocsIcon, LockIcon, KeyIcon, DesignIcon, HelpIcon, LeftIcon, } from 'shared/components/icons.mjs' import { ActiveImageInput, ButtonFrame, DesignDropdown, StringInput, MarkdownInput, } from 'shared/components/inputs.mjs' import { cloudflareImageUrl } from 'shared/utils.mjs' import { CodeBox } from 'shared/components/code-box.mjs' import { WebLink } from 'shared/components/link.mjs' // Translation namespaces used on this page export const ns = ['support', 'designs', 'account', 'status'] const types = [ 'bugReport', 'designIssue', 'accountIssue', 'docsUpdate', 'question', 'featureRequest', 'security', 'patronSponsor', 'other', ] export const userCard = (id) => `[](https://next.freesewing.org/users/${id})` const templates = { bugReport: { title: ({ title }) => `[bug]: ${title}`, labels: () => [':bug: bug'], }, designIssue: { title: ({ title, design }) => `[${design}]: ${title}`, labels: ({ design }) => [`:shirt: ${design}`], }, accountIssue: { title: ({ title }) => `[accounts]: ${title}`, labels: () => ['account'], }, docsUpdate: { title: ({ title }) => `[docs]: ${title}`, labels: () => [':book: documentation'], }, question: { title: ({ title }) => title, labels: () => [], }, featureRequest: { title: ({ title }) => `[featureRequest]: ${title}`, labels: () => [':gem: enhancement'], }, security: { title: ({ title }) => `[security]: ${title}`, labels: () => ['security'], }, patronSponsor: { title: ({ title }) => `[patrons]: ${title}`, labels: () => ['impacts-patron'], }, other: { title: ({ title }) => `[other]: ${title}`, labels: () => [], }, } const commonLabels = [':robot: robot', 'needs-triage'] const iconProps = { className: 'w-8 h-8 shrink-0' } const icons = { bugReport: <BugIcon {...iconProps} />, featureRequest: <SettingsIcon {...iconProps} />, docsUpdate: <DocsIcon {...iconProps} />, security: <LockIcon className="w-8 h-8 shrink-0 text-warning" />, question: <ChatIcon {...iconProps} />, other: <HelpIcon {...iconProps} />, accountIssue: <KeyIcon {...iconProps} />, designIssue: <DesignIcon {...iconProps} />, patronSponsor: <HeartIcon className="w-8 h-8 shrink-0 text-error" fill />, } const SupportType = ({ type, active, t, update }) => ( <ButtonFrame key={type} active={active} onClick={() => update(type)}> <div className="w-full flex flex-col gap-2"> <div className="w-full text-lg leading-5 flex flex-row items-center justify-between grow-0"> <span>{t(`support:${type}`)}</span> {icons[type]} </div> <div className="w-full text-normal font-normal normal-case pt-1 leading-5 grow"> {t(`support:${type}Desc`)} </div> </div> </ButtonFrame> ) export const SupportForm = () => { const { t } = useTranslation(ns) const { setLoadingStatus } = useContext(LoadingStatusContext) const { account } = useAccount() const backend = useBackend() const [type, setType] = useState(false) const [title, setTitle] = useState('') const [design, setDesign] = useState('') const [body, setBody] = useState('') const [images, setImages] = useState({}) const [issue, setIssue] = useState(false) const [discussion, setDiscussion] = useState(false) const addImage = () => { const id = Object.keys(images).length + 1 const newImages = { ...images } newImages[id] = null setImages(newImages) } const setSingleImage = (id, img) => { const newImages = { ...images } newImages[id] = img setImages(newImages) } const clear = () => { setIssue(false) setDiscussion(false) setType(false) setTitle('') setDesign('') setBody('') setImages({}) } const submit = async () => { setLoadingStatus([true, 'gatheringInfo']) const templateData = { title, design } const issueData = { title: templates[type].title(templateData), body: `${body}\n\n${userCard(account.id || false)}`, labels: [...commonLabels, ...templates[type].labels(templateData)], } setLoadingStatus([true, 'submittingData']) const result = type === 'question' ? await backend.createDiscussion(issueData) : await backend.createIssue(issueData) if (result.success) { setLoadingStatus([true, 'settingsSaved', true, true]) if (type === 'question') setDiscussion(result.data.discussion.data.createDiscussion.discussion.url) else setIssue(result.data.issue) } else setLoadingStatus([true, 'backendError', true, false]) } if (issue || discussion) return ( <div className="w-full max-w-xl"> <h5>{t('support:requestCreated')}</h5> <p> We have created your request, you can find it here: <br /> <WebLink href={discussion ? discussion : issue.html_url} txt={discussion ? discussion : issue.html_url} /> </p> <button className="btn btn-secondary" onClick={clear}> <LeftIcon /> {t('support:back')} </button> </div> ) if (!type) return ( <> <div className="grid grid-cols-3 gap-2"> {types.map((_type) => ( <SupportType type={_type} active={type === _type} update={setType} t={t} key={_type} /> ))} </div> <p className="text-right"> <WebLink txt={t('support:useGitHubInstead')} href="https://github.com/freesewing/freesewing/issues/new/choose" /> </p> </> ) return ( <div className="w-full max-w-4xl mx-auto"> <SupportType type={type} active={true} update={() => setType(false)} t={t} /> <StringInput id="support-title" label={t('support:title')} update={setTitle} current={title} valid={(val) => val.length > 10} docs={ <div className="max-w-prose"> <h2>{t('support:title')}</h2> <p>{t('support:titleDocs1')}</p> <p>{t('support:titleDocs2')}</p> </div> } /> {type === 'designIssue' && ( <DesignDropdown firstOption={<option val="">Not related to a design</option>} label={t('design')} update={setDesign} current={design} valid={(val) => val.length > 1} docs={ <div className="max-w-prose"> <h2>{t('design')}</h2> <p>{t('support:designDocs1')}</p> </div> } /> )} <MarkdownInput id="support-body" label={t('support:description')} update={setBody} current={body} valid={(val) => val.length > 10} docs={ <div className="max-w-prose"> <h2>{t('support:description')}</h2> <p>{t('support:descriptionDocs1')}</p> </div> } /> {Object.keys(images).map((key) => { const markup = ' + ')' return ( <Fragment key={key}> <ActiveImageInput id={`support-img-${key}`} label={`${t('support:image')} ${key}`} update={(val) => setSingleImage(key, val)} current={images[key]} valid={(val) => val.length > 1} docs={ <div className="max-w-prose"> <h2>{t('support:image')}</h2> <p>{t('support:imageDocs1')}</p> </div> } imgType="support" imgSubid={key} imgSlug={Date.now()} /> {images[key] && ( <Popout tip> <span>{t('support:addImageToMd')}:</span> <CodeBox code={markup} title="MarkDown" /> <p className="text-right -mt-5"> <button className="btn btn-sm btn-accent" onClick={() => setBody(body + '\n\n' + markup)} > {t('support:addToDescription')} </button> </p> </Popout> )} </Fragment> ) })} {Object.keys(images).length < 9 && ( <button className="btn btn-secondary mt-2" onClick={addImage}> {t('support:addImage')} </button> )} <button className="btn btn-primary block btn-lg mx-auto mt-8" disabled={title.length < 10} onClick={submit} > {t('support:submitSupportRequest')} </button> </div> ) }