1
0
Fork 0
freesewing/sites/shared/components/support.mjs
2023-09-29 16:01:27 +02:00

313 lines
9 KiB
JavaScript

// __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',
]
const userCard = (id) =>
`[![User ${id}](${config.backend}/users/${id}/card)](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(type)}</span>
{icons[type]}
</div>
<div className="w-full text-normal font-normal normal-case pt-1 leading-5 grow">
{t(`${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('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('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('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('title')}
update={setTitle}
current={title}
valid={(val) => val.length > 10}
docs={
<div className="max-w-prose">
<h2>{t('title')}</h2>
<p>{t('titleDocs1')}</p>
<p>{t('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('designDocs1')}</p>
</div>
}
/>
)}
<MarkdownInput
id="support-body"
label={t('description')}
update={setBody}
current={body}
valid={(val) => val.length > 10}
docs={
<div className="max-w-prose">
<h2>{t('description')}</h2>
<p>{t('descriptionDocs1')}</p>
</div>
}
/>
{Object.keys(images).map((key) => {
const markup =
'![Uploaded image](' + cloudflareImageUrl({ id: images[key], variant: 'public' }) + ')'
return (
<Fragment key={key}>
<ActiveImageInput
id={`support-img-${key}`}
label={`${t('image')} ${key}`}
update={(val) => setSingleImage(key, val)}
current={images[key]}
valid={(val) => val.length > 1}
docs={
<div className="max-w-prose">
<h2>{t('image')}</h2>
<p>{t('imageDocs1')}</p>
</div>
}
imgType="support"
imgSubid={key}
imgSlug={Date.now()}
/>
{images[key] && (
<Popout tip>
<span>{t('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('addToDescription')}
</button>
</p>
</Popout>
)}
</Fragment>
)
})}
{Object.keys(images).length < 9 && (
<button className="btn btn-secondary mt-2" onClick={addImage}>
{t('addImage')}
</button>
)}
<button
className="btn btn-primary block btn-lg mx-auto mt-8"
disabled={title.length < 10}
onClick={submit}
>
{t('submitSupportRequest')}
</button>
</div>
)
}