mirror of
https://github.com/IT4Change/ohmyform-ui.git
synced 2026-01-20 19:31:17 +00:00
add form layouts
This commit is contained in:
parent
de1180d547
commit
d5dff46816
@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||
- add environment list in [doc](doc/environment.md)
|
||||
- show error message on homepage in case there is a problem with api connection
|
||||
- new slider field type
|
||||
- new card layout for forms
|
||||
|
||||
### Changed
|
||||
|
||||
@ -26,6 +27,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||
- links at the bottom for new users
|
||||
- fixes for hide contrib setting
|
||||
- fix problem with node-prune on production build
|
||||
- translation for prev / continue during form submission
|
||||
|
||||
### Security
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Form, Input, Tabs } from 'antd'
|
||||
import { Form, Input, Select, Tabs } from 'antd'
|
||||
import { TabPaneProps } from 'antd/lib/tabs'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -12,6 +12,20 @@ export const DesignTab: React.FC<TabPaneProps> = (props) => {
|
||||
<Form.Item label={t('form:design.font')} name={['form', 'design', 'font']}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label={t('form:design.layouts')} name={['form', 'design', 'layout']}>
|
||||
<Select
|
||||
options={[
|
||||
{
|
||||
value: null,
|
||||
label: t('form:design.layout.slider'),
|
||||
},
|
||||
{
|
||||
value: 'card',
|
||||
label: t('form:design.layout.card'),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{['background', 'question', 'answer', 'button', 'buttonActive', 'buttonText'].map((name) => (
|
||||
<Form.Item
|
||||
|
||||
63
components/form/layouts/card/field.tsx
Normal file
63
components/form/layouts/card/field.tsx
Normal file
@ -0,0 +1,63 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
FormPublicDesignFragment,
|
||||
FormPublicFieldFragment,
|
||||
} from '../../../../graphql/fragment/form.public.fragment'
|
||||
import { StyledH1 } from '../../../styled/h1'
|
||||
import { StyledMarkdown } from '../../../styled/markdown'
|
||||
import { useRouter } from '../../../use.router'
|
||||
import { fieldTypes } from '../../types'
|
||||
import { TextType } from '../../types/text.type'
|
||||
import { FieldTypeProps } from '../../types/type.props'
|
||||
|
||||
interface Props {
|
||||
field: FormPublicFieldFragment
|
||||
design: FormPublicDesignFragment
|
||||
}
|
||||
|
||||
export const Field: React.FC<Props> = ({ field, design, ...props }) => {
|
||||
const router = useRouter()
|
||||
|
||||
const FieldInput: React.FC<FieldTypeProps> = fieldTypes[field.type] || TextType
|
||||
|
||||
const getUrlDefault = (): string => {
|
||||
if (router.query[field.id]) {
|
||||
return router.query[field.id] as string
|
||||
}
|
||||
|
||||
if (router.query[field.slug]) {
|
||||
return router.query[field.slug] as string
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
padding: 32,
|
||||
justifyContent: 'flex-end',
|
||||
}}
|
||||
>
|
||||
<StyledH1 design={design} type={'question'}>
|
||||
{field.title}
|
||||
</StyledH1>
|
||||
{field.description && (
|
||||
<StyledMarkdown design={design} type={'question'} source={field.description} />
|
||||
)}
|
||||
|
||||
<FieldInput design={design} field={field} urlValue={getUrlDefault()} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
0
components/form/layouts/card/index.module.scss
Normal file
0
components/form/layouts/card/index.module.scss
Normal file
139
components/form/layouts/card/index.tsx
Normal file
139
components/form/layouts/card/index.tsx
Normal file
@ -0,0 +1,139 @@
|
||||
import { Card, Form, message, Modal, Spin } from 'antd'
|
||||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
import { Omf } from '../../../omf'
|
||||
import { StyledButton } from '../../../styled/button'
|
||||
import { darken, lighten } from '../../../styled/color.change'
|
||||
import { LayoutProps } from '../layout.props'
|
||||
import { Field } from './field'
|
||||
import { Page } from './page'
|
||||
|
||||
type Step = 'start' | 'form' | 'end'
|
||||
|
||||
const MyCard = styled.div<{ background: string }>`
|
||||
background: ${(props) => darken(props.background, 10)};
|
||||
height: 100%;
|
||||
min-height: 100vh;
|
||||
|
||||
padding: 32px;
|
||||
|
||||
.ant-card {
|
||||
background: ${(props) => props.background};
|
||||
border-color: ${(props) => lighten(props.background, 40)};
|
||||
width: 800px;
|
||||
margin: auto;
|
||||
max-width: 90%;
|
||||
}
|
||||
`
|
||||
|
||||
export const CardLayout: React.FC<LayoutProps> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
const [form] = Form.useForm()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [step, setStep] = useState<Step>(props.form.startPage.show ? 'start' : 'form')
|
||||
|
||||
const { design, startPage, endPage, fields } = props.form
|
||||
const { setField } = props.submission
|
||||
|
||||
const finish = async (data: { [key: number]: unknown }) => {
|
||||
console.log('data', data)
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
// save fields
|
||||
await Promise.all(Object.keys(data).map((fieldId) => setField(fieldId, data[fieldId])))
|
||||
|
||||
await props.submission.finish()
|
||||
|
||||
if (endPage.show) {
|
||||
setStep('end')
|
||||
} else {
|
||||
Modal.success({
|
||||
content: t('form:submitted'),
|
||||
okText: t('from:restart'),
|
||||
onOk: () => {
|
||||
window.location.reload()
|
||||
},
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
void message.error({
|
||||
content: 'Error saving Input',
|
||||
})
|
||||
}
|
||||
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
const render = () => {
|
||||
switch (step) {
|
||||
case 'start':
|
||||
return <Page page={startPage} design={design} next={() => setStep('form')} />
|
||||
|
||||
case 'form':
|
||||
return (
|
||||
<Card>
|
||||
<Form form={form} onFinish={finish}>
|
||||
{fields.map((field, i) => {
|
||||
if (field.type === 'hidden') {
|
||||
return null
|
||||
}
|
||||
|
||||
return <Field key={field.id} field={field} design={design} />
|
||||
})}
|
||||
<div
|
||||
style={{
|
||||
padding: 32,
|
||||
display: 'flex',
|
||||
}}
|
||||
>
|
||||
{startPage.show && (
|
||||
<StyledButton
|
||||
background={design.colors.button}
|
||||
color={design.colors.buttonText}
|
||||
highlight={design.colors.buttonActive}
|
||||
onClick={() => setStep('start')}
|
||||
>
|
||||
{t('form:previous')}
|
||||
</StyledButton>
|
||||
)}
|
||||
|
||||
<div style={{ flex: 1 }} />
|
||||
|
||||
<StyledButton
|
||||
background={design.colors.button}
|
||||
color={design.colors.buttonText}
|
||||
highlight={design.colors.buttonActive}
|
||||
size={'large'}
|
||||
onClick={form.submit}
|
||||
>
|
||||
{t('form:next')}
|
||||
</StyledButton>
|
||||
</div>
|
||||
</Form>
|
||||
</Card>
|
||||
)
|
||||
|
||||
case 'end':
|
||||
return (
|
||||
<Page
|
||||
page={endPage}
|
||||
design={design}
|
||||
next={() => {
|
||||
window.location.reload()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<MyCard background={design.colors.background}>
|
||||
<Omf />
|
||||
|
||||
<Spin spinning={loading}>{render()}</Spin>
|
||||
</MyCard>
|
||||
)
|
||||
}
|
||||
65
components/form/layouts/card/page.tsx
Normal file
65
components/form/layouts/card/page.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
import { Card } from 'antd'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
FormPublicDesignFragment,
|
||||
FormPublicPageFragment,
|
||||
} from '../../../../graphql/fragment/form.public.fragment'
|
||||
import { StyledButton } from '../../../styled/button'
|
||||
import { StyledH1 } from '../../../styled/h1'
|
||||
import { StyledMarkdown } from '../../../styled/markdown'
|
||||
import { PageButtons } from '../page.buttons'
|
||||
|
||||
interface Props {
|
||||
page: FormPublicPageFragment
|
||||
design: FormPublicDesignFragment
|
||||
|
||||
next?: () => void
|
||||
prev?: () => void
|
||||
}
|
||||
|
||||
export const Page: React.FC<Props> = ({ design, page, next, prev }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<StyledH1 design={design} type={'question'}>
|
||||
{page.title}
|
||||
</StyledH1>
|
||||
<StyledMarkdown design={design} type={'question'} source={page.paragraph} />
|
||||
|
||||
<div
|
||||
style={{
|
||||
padding: 32,
|
||||
display: 'flex',
|
||||
}}
|
||||
>
|
||||
{prev && (
|
||||
<StyledButton
|
||||
background={design.colors.button}
|
||||
color={design.colors.buttonText}
|
||||
highlight={design.colors.buttonActive}
|
||||
onClick={prev}
|
||||
>
|
||||
{t('form:restart')}
|
||||
</StyledButton>
|
||||
)}
|
||||
<PageButtons buttons={page.buttons} />
|
||||
|
||||
<div style={{ flex: 1 }} />
|
||||
|
||||
{next && (
|
||||
<StyledButton
|
||||
background={design.colors.button}
|
||||
color={design.colors.buttonText}
|
||||
highlight={design.colors.buttonActive}
|
||||
size={'large'}
|
||||
onClick={next}
|
||||
>
|
||||
{page.buttonText || t('form:continue')}
|
||||
</StyledButton>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
7
components/form/layouts/layout.props.ts
Normal file
7
components/form/layouts/layout.props.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { FormPublicFragment, } from '../../../graphql/fragment/form.public.fragment'
|
||||
import { Submission } from '../../use.submission'
|
||||
|
||||
export interface LayoutProps {
|
||||
form: FormPublicFragment
|
||||
submission: Submission
|
||||
}
|
||||
34
components/form/layouts/page.buttons.tsx
Normal file
34
components/form/layouts/page.buttons.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import { Space } from 'antd'
|
||||
import React from 'react'
|
||||
import { FormPublicPageButtonFragment } from '../../../graphql/fragment/form.public.fragment'
|
||||
import { StyledButton } from '../../styled/button'
|
||||
|
||||
interface Props {
|
||||
buttons: FormPublicPageButtonFragment[]
|
||||
}
|
||||
|
||||
export const PageButtons: React.FC<Props> = ({ buttons }) => {
|
||||
if (buttons.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Space>
|
||||
{buttons.map((button, key) => {
|
||||
return (
|
||||
<StyledButton
|
||||
background={button.bgColor}
|
||||
color={button.color}
|
||||
highlight={button.activeColor}
|
||||
key={key}
|
||||
href={button.url}
|
||||
target={'_blank'}
|
||||
rel={'noreferrer'}
|
||||
>
|
||||
{button.text}
|
||||
</StyledButton>
|
||||
)
|
||||
})}
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
@ -1,17 +1,18 @@
|
||||
import { Form, message } from 'antd'
|
||||
import { useForm } from 'antd/lib/form/Form'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
FormPublicDesignFragment,
|
||||
FormPublicFieldFragment,
|
||||
} from '../../graphql/fragment/form.public.fragment'
|
||||
import { StyledButton } from '../styled/button'
|
||||
import { StyledH1 } from '../styled/h1'
|
||||
import { StyledMarkdown } from '../styled/markdown'
|
||||
import { useRouter } from '../use.router'
|
||||
import { fieldTypes } from './types'
|
||||
import { TextType } from './types/text.type'
|
||||
import { FieldTypeProps } from './types/type.props'
|
||||
} from '../../../../graphql/fragment/form.public.fragment'
|
||||
import { StyledButton } from '../../../styled/button'
|
||||
import { StyledH1 } from '../../../styled/h1'
|
||||
import { StyledMarkdown } from '../../../styled/markdown'
|
||||
import { useRouter } from '../../../use.router'
|
||||
import { fieldTypes } from '../../types'
|
||||
import { TextType } from '../../types/text.type'
|
||||
import { FieldTypeProps } from '../../types/type.props'
|
||||
|
||||
interface Props {
|
||||
field: FormPublicFieldFragment
|
||||
@ -26,6 +27,7 @@ interface Props {
|
||||
export const Field: React.FC<Props> = ({ field, save, design, next, prev, ...props }) => {
|
||||
const [form] = useForm()
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const FieldInput: React.FC<FieldTypeProps> = fieldTypes[field.type] || TextType
|
||||
|
||||
@ -92,7 +94,7 @@ export const Field: React.FC<Props> = ({ field, save, design, next, prev, ...pro
|
||||
highlight={design.colors.buttonActive}
|
||||
onClick={prev}
|
||||
>
|
||||
{'Previous'}
|
||||
{t('form:previous')}
|
||||
</StyledButton>
|
||||
|
||||
<div style={{ flex: 1 }} />
|
||||
@ -104,7 +106,7 @@ export const Field: React.FC<Props> = ({ field, save, design, next, prev, ...pro
|
||||
size={'large'}
|
||||
onClick={form.submit}
|
||||
>
|
||||
{'Next'}
|
||||
{t('form:next')}
|
||||
</StyledButton>
|
||||
</div>
|
||||
</Form>
|
||||
97
components/form/layouts/slider/index.tsx
Normal file
97
components/form/layouts/slider/index.tsx
Normal file
@ -0,0 +1,97 @@
|
||||
import { Modal } from 'antd'
|
||||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Swiper from 'react-id-swiper'
|
||||
import { ReactIdSwiperProps } from 'react-id-swiper/lib/types'
|
||||
import * as OriginalSwiper from 'swiper'
|
||||
import { Omf } from '../../../omf'
|
||||
import { LayoutProps } from '../layout.props'
|
||||
import { Field } from './field'
|
||||
import { FormPage } from './page'
|
||||
|
||||
export const SliderLayout: React.FC<LayoutProps> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
const [swiper, setSwiper] = useState<OriginalSwiper.default>(null)
|
||||
|
||||
const { design, startPage, endPage, fields } = props.form
|
||||
const { finish, setField } = props.submission
|
||||
|
||||
const goNext = () => {
|
||||
if (!swiper) return
|
||||
|
||||
swiper.allowSlideNext = true
|
||||
swiper.slideNext()
|
||||
swiper.allowSlideNext = false
|
||||
}
|
||||
const goPrev = () => swiper && swiper.slidePrev()
|
||||
|
||||
const swiperConfig: ReactIdSwiperProps = {
|
||||
direction: 'vertical',
|
||||
allowSlideNext: false,
|
||||
allowSlidePrev: true,
|
||||
noSwiping: true,
|
||||
updateOnWindowResize: true,
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
background: design.colors.background,
|
||||
}}
|
||||
>
|
||||
<Omf />
|
||||
{/* eslint-disable-next-line @typescript-eslint/no-unsafe-member-access */}
|
||||
<Swiper {...swiperConfig} ref={(element) => element && setSwiper((element as any).swiper)}>
|
||||
{[
|
||||
startPage.show ? (
|
||||
<FormPage key={'start'} page={startPage} design={design} next={goNext} prev={goPrev} />
|
||||
) : undefined,
|
||||
...fields
|
||||
.map((field, i) => {
|
||||
if (field.type === 'hidden') {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Field
|
||||
key={field.id}
|
||||
field={field}
|
||||
design={design}
|
||||
save={async (values: { [key: string]: unknown }) => {
|
||||
await setField(field.id, values[field.id])
|
||||
|
||||
if (fields.length === i + 1) {
|
||||
finish()
|
||||
}
|
||||
}}
|
||||
next={() => {
|
||||
if (fields.length === i + 1) {
|
||||
// prevent going back!
|
||||
swiper.allowSlidePrev = true
|
||||
|
||||
if (!endPage.show) {
|
||||
Modal.success({
|
||||
content: t('form:submitted'),
|
||||
okText: t('from:restart'),
|
||||
onOk: () => {
|
||||
window.location.reload()
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
goNext()
|
||||
}}
|
||||
prev={goPrev}
|
||||
/>
|
||||
)
|
||||
})
|
||||
.filter((e) => e !== null),
|
||||
endPage.show ? (
|
||||
<FormPage key={'end'} page={endPage} design={design} next={finish} prev={goPrev} />
|
||||
) : undefined,
|
||||
].filter((e) => !!e)}
|
||||
</Swiper>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
0
components/form/layouts/slider/page.module.scss
Normal file
0
components/form/layouts/slider/page.module.scss
Normal file
@ -1,16 +1,16 @@
|
||||
import { Space } from 'antd'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
FormPublicDesignFragment,
|
||||
FormPublicPageFragment,
|
||||
} from '../../graphql/fragment/form.public.fragment'
|
||||
import { StyledButton } from '../styled/button'
|
||||
import { StyledH1 } from '../styled/h1'
|
||||
import { StyledMarkdown } from '../styled/markdown'
|
||||
} from '../../../../graphql/fragment/form.public.fragment'
|
||||
import { StyledButton } from '../../../styled/button'
|
||||
import { StyledH1 } from '../../../styled/h1'
|
||||
import { StyledMarkdown } from '../../../styled/markdown'
|
||||
import { PageButtons } from '../page.buttons'
|
||||
import scss from './page.module.scss'
|
||||
|
||||
interface Props {
|
||||
type: 'start' | 'end'
|
||||
page: FormPublicPageFragment
|
||||
design: FormPublicDesignFragment
|
||||
className?: string
|
||||
@ -20,6 +20,8 @@ interface Props {
|
||||
}
|
||||
|
||||
export const FormPage: React.FC<Props> = ({ page, design, next, prev, className, ...props }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (!page.show) {
|
||||
return null
|
||||
}
|
||||
@ -38,26 +40,17 @@ export const FormPage: React.FC<Props> = ({ page, design, next, prev, className,
|
||||
display: 'flex',
|
||||
}}
|
||||
>
|
||||
{prev && <div />}
|
||||
{page.buttons.length > 0 && (
|
||||
<Space>
|
||||
{page.buttons.map((button, key) => {
|
||||
return (
|
||||
<StyledButton
|
||||
background={button.bgColor}
|
||||
color={button.color}
|
||||
highlight={button.activeColor}
|
||||
key={key}
|
||||
href={button.url}
|
||||
target={'_blank'}
|
||||
rel={'noreferrer'}
|
||||
>
|
||||
{button.text}
|
||||
</StyledButton>
|
||||
)
|
||||
})}
|
||||
</Space>
|
||||
{prev && (
|
||||
<StyledButton
|
||||
background={design.colors.button}
|
||||
color={design.colors.buttonText}
|
||||
highlight={design.colors.buttonActive}
|
||||
onClick={prev}
|
||||
>
|
||||
{t('form:previous')}
|
||||
</StyledButton>
|
||||
)}
|
||||
<PageButtons buttons={page.buttons} />
|
||||
|
||||
<div style={{ flex: 1 }} />
|
||||
|
||||
@ -68,7 +61,7 @@ export const FormPage: React.FC<Props> = ({ page, design, next, prev, className,
|
||||
size={'large'}
|
||||
onClick={next}
|
||||
>
|
||||
{page.buttonText || 'Continue'}
|
||||
{page.buttonText || t('form:continue')}
|
||||
</StyledButton>
|
||||
</div>
|
||||
</div>
|
||||
@ -1,19 +0,0 @@
|
||||
.main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
padding: 16px;
|
||||
|
||||
@media (max-width: 600px) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,10 +11,10 @@ import {
|
||||
SubmissionStartMutationVariables,
|
||||
} from '../graphql/mutation/submission.start.mutation'
|
||||
|
||||
interface Submission {
|
||||
export interface Submission {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
setField: (fieldId: string, data: unknown) => Promise<void>
|
||||
finish: () => void
|
||||
finish: () => Promise<void>
|
||||
}
|
||||
|
||||
export const useSubmission = (formId: string): Submission => {
|
||||
@ -70,8 +70,10 @@ export const useSubmission = (formId: string): Submission => {
|
||||
[submission]
|
||||
)
|
||||
|
||||
const finish = useCallback(() => {
|
||||
const finish = useCallback(async () => {
|
||||
console.log('finish submission!!', formId)
|
||||
|
||||
await Promise.resolve()
|
||||
}, [submission])
|
||||
|
||||
return {
|
||||
|
||||
@ -96,6 +96,7 @@ export interface FormFragment {
|
||||
buttonText: string
|
||||
}
|
||||
font?: string
|
||||
layout?: string
|
||||
}
|
||||
startPage: FormPageFragment
|
||||
endPage: FormPageFragment
|
||||
@ -176,6 +177,7 @@ export const FORM_FRAGMENT = gql`
|
||||
buttonText
|
||||
}
|
||||
font
|
||||
layout
|
||||
}
|
||||
startPage {
|
||||
id
|
||||
|
||||
@ -1,20 +1,22 @@
|
||||
import { gql } from '@apollo/client/core'
|
||||
|
||||
export interface FormPublicPageButtonFragment {
|
||||
id: string
|
||||
url?: string
|
||||
action?: string
|
||||
text?: string
|
||||
bgColor?: string
|
||||
activeColor?: string
|
||||
color?: string
|
||||
}
|
||||
|
||||
export interface FormPublicPageFragment {
|
||||
id: string
|
||||
show: boolean
|
||||
title?: string
|
||||
paragraph?: string
|
||||
buttonText?: string
|
||||
buttons: {
|
||||
id: string
|
||||
url?: string
|
||||
action?: string
|
||||
text?: string
|
||||
bgColor?: string
|
||||
activeColor?: string
|
||||
color?: string
|
||||
}[]
|
||||
buttons: FormPublicPageButtonFragment[]
|
||||
}
|
||||
|
||||
export interface FormPublicFieldOptionFragment {
|
||||
@ -62,6 +64,7 @@ export interface FormPublicDesignFragment {
|
||||
buttonText: string
|
||||
}
|
||||
font?: string
|
||||
layout?: string
|
||||
}
|
||||
|
||||
export interface FormPublicFragment {
|
||||
@ -134,6 +137,7 @@ export const FORM_PUBLIC_FRAGMENT = gql`
|
||||
buttonText
|
||||
}
|
||||
font
|
||||
layout
|
||||
}
|
||||
|
||||
startPage {
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
"baseDataTab": "Base Data",
|
||||
"building": "Building Form",
|
||||
"confirmDelete": "Do you really want to delete this form with all submissions?",
|
||||
"continue": "Continue",
|
||||
"create": "Create new form",
|
||||
"created": "Form Created",
|
||||
"createNow": "Save",
|
||||
@ -24,6 +25,11 @@
|
||||
"buttonText": "Button Text Color",
|
||||
"question": "Question Color"
|
||||
},
|
||||
"layout": {
|
||||
"card": "Card",
|
||||
"slider": "Slider"
|
||||
},
|
||||
"layouts": "Layout",
|
||||
"font": "Font"
|
||||
},
|
||||
"designTab": "Design",
|
||||
@ -52,7 +58,9 @@
|
||||
"loading": "Loading Form",
|
||||
"mange": "Edit Form \"{{title}}\"",
|
||||
"new": "New Form",
|
||||
"next": "Next",
|
||||
"notifications": {
|
||||
"add": "Add Notification",
|
||||
"enabled": "Enabled",
|
||||
"fromEmail": "Sender Email",
|
||||
"fromEmailInfo": "Make sure your mailserver can send from this email",
|
||||
@ -66,6 +74,8 @@
|
||||
"toField": "Email Field",
|
||||
"toFieldInfo": "Field with Email for receipt"
|
||||
},
|
||||
"notificationsTab": "Notifications",
|
||||
"previous": "Previous",
|
||||
"restart": "Restart Form",
|
||||
"row": {
|
||||
"admin": "Owner",
|
||||
|
||||
@ -1,29 +1,22 @@
|
||||
import { Modal } from 'antd'
|
||||
import { ErrorPage } from 'components/error.page'
|
||||
import { Field } from 'components/form/field'
|
||||
import { FormPage } from 'components/form/page'
|
||||
import { LoadingPage } from 'components/loading.page'
|
||||
import { NextPage } from 'next'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import React, { useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Swiper from 'react-id-swiper'
|
||||
import { ReactIdSwiperProps } from 'react-id-swiper/lib/types'
|
||||
import * as OriginalSwiper from 'swiper'
|
||||
import { Omf } from '../../../components/omf'
|
||||
import { CardLayout } from '../../../components/form/layouts/card'
|
||||
import { SliderLayout } from '../../../components/form/layouts/slider'
|
||||
import { useSubmission } from '../../../components/use.submission'
|
||||
import { useFormPublicQuery } from '../../../graphql/query/form.public.query'
|
||||
|
||||
const Index: NextPage = () => {
|
||||
const { t, i18n } = useTranslation()
|
||||
const router = useRouter()
|
||||
const id = router.query.id as string
|
||||
const [swiper, setSwiper] = useState<OriginalSwiper.default>(null)
|
||||
const submission = useSubmission(id)
|
||||
const submission = useSubmission(router.query.id as string)
|
||||
|
||||
const { loading, data, error } = useFormPublicQuery({
|
||||
variables: {
|
||||
id,
|
||||
id: router.query.id as string,
|
||||
},
|
||||
})
|
||||
|
||||
@ -34,6 +27,7 @@ const Index: NextPage = () => {
|
||||
}
|
||||
|
||||
if (i18n.language !== data.form.language) {
|
||||
// TODO prompt for language change if is not a match!
|
||||
i18n
|
||||
.changeLanguage(data.form.language)
|
||||
.catch((e: Error) => console.error('failed to change language', e))
|
||||
@ -48,100 +42,14 @@ const Index: NextPage = () => {
|
||||
return <ErrorPage />
|
||||
}
|
||||
|
||||
const design = data.form.design
|
||||
switch (data.form.design.layout) {
|
||||
case 'card':
|
||||
return <CardLayout form={data.form} submission={submission} />
|
||||
|
||||
const goNext = () => {
|
||||
if (!swiper) return
|
||||
|
||||
swiper.allowSlideNext = true
|
||||
swiper.slideNext()
|
||||
swiper.allowSlideNext = false
|
||||
case 'slider':
|
||||
default:
|
||||
return <SliderLayout form={data.form} submission={submission} />
|
||||
}
|
||||
const goPrev = () => swiper && swiper.slidePrev()
|
||||
|
||||
const swiperConfig: ReactIdSwiperProps = {
|
||||
direction: 'vertical',
|
||||
allowSlideNext: false,
|
||||
allowSlidePrev: true,
|
||||
noSwiping: true,
|
||||
updateOnWindowResize: true,
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
background: design.colors.background,
|
||||
}}
|
||||
>
|
||||
<Omf />
|
||||
{/* eslint-disable-next-line @typescript-eslint/no-unsafe-member-access */}
|
||||
<Swiper {...swiperConfig} ref={(element) => element && setSwiper((element as any).swiper)}>
|
||||
{[
|
||||
data.form.startPage.show ? (
|
||||
<FormPage
|
||||
key={'start'}
|
||||
type={'start'}
|
||||
page={data.form.startPage}
|
||||
design={design}
|
||||
next={goNext}
|
||||
prev={goPrev}
|
||||
/>
|
||||
) : undefined,
|
||||
...data.form.fields
|
||||
.map((field, i) => {
|
||||
if (field.type === 'hidden') {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Field
|
||||
key={field.id}
|
||||
field={field}
|
||||
design={design}
|
||||
save={async (values: { [key: string]: unknown }) => {
|
||||
await submission.setField(field.id, values[field.id])
|
||||
|
||||
if (data.form.fields.length === i + 1) {
|
||||
submission.finish()
|
||||
}
|
||||
}}
|
||||
next={() => {
|
||||
if (data.form.fields.length === i + 1) {
|
||||
// prevent going back!
|
||||
swiper.allowSlidePrev = true
|
||||
|
||||
if (!data.form.endPage.show) {
|
||||
Modal.success({
|
||||
content: t('form:submitted'),
|
||||
okText: t('from:restart'),
|
||||
onOk: () => {
|
||||
window.location.reload()
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
goNext()
|
||||
}}
|
||||
prev={goPrev}
|
||||
/>
|
||||
)
|
||||
})
|
||||
.filter((e) => e !== null),
|
||||
data.form.endPage.show ? (
|
||||
<FormPage
|
||||
key={'end'}
|
||||
type={'end'}
|
||||
page={data.form.endPage}
|
||||
design={design}
|
||||
next={submission.finish}
|
||||
prev={goPrev}
|
||||
/>
|
||||
) : undefined,
|
||||
].filter((e) => !!e)}
|
||||
</Swiper>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Index
|
||||
|
||||
@ -36,6 +36,7 @@ type Deleted {
|
||||
type Design {
|
||||
colors: Colors!
|
||||
font: String
|
||||
layout: String
|
||||
}
|
||||
|
||||
type Device {
|
||||
@ -283,6 +284,7 @@ input ColorsInput {
|
||||
input DesignInput {
|
||||
colors: ColorsInput!
|
||||
font: String
|
||||
layout: String
|
||||
}
|
||||
|
||||
input DeviceInput {
|
||||
@ -294,6 +296,7 @@ input DeviceInput {
|
||||
input FormCreateInput {
|
||||
isLive: Boolean
|
||||
language: String!
|
||||
layout: String
|
||||
showFooter: Boolean
|
||||
title: String!
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user