mirror of
https://github.com/IT4Change/ohmyform-ui.git
synced 2025-12-13 09:45:50 +00:00
update form handling
This commit is contained in:
parent
ac03ca3250
commit
ec0f6e9572
@ -22,3 +22,7 @@
|
|||||||
color: #1890ff;
|
color: #1890ff;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ant-spin-nested-loading > div > .ant-spin {
|
||||||
|
max-height: unset;
|
||||||
|
}
|
||||||
|
|||||||
54
components/auth/footer.tsx
Normal file
54
components/auth/footer.tsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import {Button} from 'antd'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export const AuthFooter: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Link href={'/admin'}>
|
||||||
|
<Button
|
||||||
|
type={'link'}
|
||||||
|
ghost
|
||||||
|
>
|
||||||
|
Admin
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<Link href={'/login'}>
|
||||||
|
<Button
|
||||||
|
type={'link'}
|
||||||
|
ghost
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<Link href={'/register'}>
|
||||||
|
<Button
|
||||||
|
type={'link'}
|
||||||
|
ghost
|
||||||
|
>
|
||||||
|
Register
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type={'link'}
|
||||||
|
target={'_blank'}
|
||||||
|
ghost
|
||||||
|
href={'https://www.ohmyform.com'}
|
||||||
|
style={{
|
||||||
|
float: 'right',
|
||||||
|
color: '#FFF'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
© OhMyForm
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
19
components/auth/layout.tsx
Normal file
19
components/auth/layout.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import {Layout, Spin} from 'antd'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
loading?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AuthLayout: React.FC<Props> = props => {
|
||||||
|
return (
|
||||||
|
<Spin spinning={props.loading}>
|
||||||
|
<Layout style={{
|
||||||
|
height: '100vh',
|
||||||
|
background: '#437fdc'
|
||||||
|
}}>
|
||||||
|
{props.children}
|
||||||
|
</Layout>
|
||||||
|
</Spin>
|
||||||
|
)
|
||||||
|
}
|
||||||
27
components/clean.input.ts
Normal file
27
components/clean.input.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
|
||||||
|
const omitDeepArrayWalk = (arr, key) => {
|
||||||
|
return arr.map((val) => {
|
||||||
|
if (Array.isArray(val)) return omitDeepArrayWalk(val, key)
|
||||||
|
else if (typeof val === 'object') return omitDeep(val, key)
|
||||||
|
return val
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const omitDeep = (obj: any, key: string | number): any => {
|
||||||
|
const keys: Array<any> = Object.keys(obj);
|
||||||
|
const newObj: any = {};
|
||||||
|
keys.forEach((i: any) => {
|
||||||
|
if (i !== key) {
|
||||||
|
const val: any = obj[i];
|
||||||
|
if (val instanceof Date) newObj[i] = val;
|
||||||
|
else if (Array.isArray(val)) newObj[i] = omitDeepArrayWalk(val, key);
|
||||||
|
else if (typeof val === 'object' && val !== null) newObj[i] = omitDeep(val, key);
|
||||||
|
else newObj[i] = val;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return newObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const cleanInput = <T>(obj: T): T => {
|
||||||
|
return omitDeep(obj, '__typename')
|
||||||
|
}
|
||||||
16
components/error.page.tsx
Normal file
16
components/error.page.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export const ErrorPage: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
height: '100vh',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
}}>
|
||||||
|
<h1>ERROR</h1>
|
||||||
|
<p>there was an error with your request</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
55
components/form/admin/base.data.tab.tsx
Normal file
55
components/form/admin/base.data.tab.tsx
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import {Form, Input, Select, Switch, Tabs} from 'antd'
|
||||||
|
import {TabPaneProps} from 'antd/lib/tabs'
|
||||||
|
import React from 'react'
|
||||||
|
import {languages} from '../../../i18n'
|
||||||
|
|
||||||
|
export const BaseDataTab: React.FC<TabPaneProps> = props => {
|
||||||
|
return (
|
||||||
|
<Tabs.TabPane {...props}>
|
||||||
|
<Form.Item
|
||||||
|
label="Is Live"
|
||||||
|
name={['form', 'isLive']}
|
||||||
|
valuePropName={'checked'}
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="Title"
|
||||||
|
name={['form', 'title']}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: 'Please provide a Title',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="Language"
|
||||||
|
name={['form', 'language']}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: 'Please select a Language',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Select>
|
||||||
|
{languages.map(language => <Select.Option value={language} key={language}>{language.toUpperCase()}</Select.Option> )}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="Show Footer"
|
||||||
|
name={['form', 'showFooter']}
|
||||||
|
valuePropName={'checked'}
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
</Tabs.TabPane>
|
||||||
|
)
|
||||||
|
}
|
||||||
29
components/form/admin/design.tab.tsx
Normal file
29
components/form/admin/design.tab.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import {Form, Input, Tabs} from 'antd'
|
||||||
|
import {TabPaneProps} from 'antd/lib/tabs'
|
||||||
|
import React from 'react'
|
||||||
|
import {InputColor} from '../../input/color'
|
||||||
|
|
||||||
|
export const DesignTab: React.FC<TabPaneProps> = props => {
|
||||||
|
return (
|
||||||
|
<Tabs.TabPane {...props}>
|
||||||
|
<Form.Item
|
||||||
|
label="Font"
|
||||||
|
name={['form', 'design', 'font']}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
{[
|
||||||
|
{name: 'backgroundColor', label: 'Background Color'},
|
||||||
|
{name: 'questionColor', label: 'Question Color'},
|
||||||
|
{name: 'answerColor', label: 'Answer Color'},
|
||||||
|
{name: 'buttonColor', label: 'Button Color'},
|
||||||
|
{name: 'buttonTextColor', label: 'Button Text Color'},
|
||||||
|
].map(({label, name}) => (
|
||||||
|
<Form.Item key={name} label={label} name={['form', 'design', 'colors', name]}>
|
||||||
|
<InputColor />
|
||||||
|
</Form.Item>
|
||||||
|
))}
|
||||||
|
</Tabs.TabPane>
|
||||||
|
)
|
||||||
|
}
|
||||||
98
components/form/admin/end.page.tab.tsx
Normal file
98
components/form/admin/end.page.tab.tsx
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import {DeleteOutlined, PlusOutlined} from '@ant-design/icons/lib'
|
||||||
|
import {Button, Card, Form, Input, Switch, Tabs} from 'antd'
|
||||||
|
import {TabPaneProps} from 'antd/lib/tabs'
|
||||||
|
import React from 'react'
|
||||||
|
import {InputColor} from '../../input/color'
|
||||||
|
|
||||||
|
export const EndPageTab: React.FC<TabPaneProps> = props => {
|
||||||
|
return (
|
||||||
|
<Tabs.TabPane {...props}>
|
||||||
|
<Form.Item
|
||||||
|
label={'Show'}
|
||||||
|
name={['form', 'endPage', 'show']}
|
||||||
|
valuePropName={'checked'}
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={'Title'}
|
||||||
|
name={['form', 'endPage', 'title']}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={'Paragraph'}
|
||||||
|
name={['form', 'endPage', 'paragraph']}
|
||||||
|
>
|
||||||
|
<Input.TextArea autoSize />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={'Continue Button Text'}
|
||||||
|
name={['form', 'endPage', 'buttonText']}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.List
|
||||||
|
name={['form', 'endPage', 'buttons']}
|
||||||
|
>
|
||||||
|
{(fields, { add, remove }) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{fields.map((field, index) => (
|
||||||
|
<Form.Item
|
||||||
|
wrapperCol={{
|
||||||
|
sm: { offset: index === 0 ? 0 : 6 },
|
||||||
|
}}
|
||||||
|
label={index === 0 ? 'Buttons' : ''}
|
||||||
|
key={field.key}
|
||||||
|
>
|
||||||
|
<Card
|
||||||
|
actions={[
|
||||||
|
<DeleteOutlined key={'delete'} onClick={() => remove(index)} />
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Form.Item label={'Url'} name={[field.key, 'url']} labelCol={{ span: 6 }}>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={'Action'} name={[field.key, 'action']} labelCol={{ span: 6 }}>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={'Text'} name={[field.key, 'text']} labelCol={{ span: 6 }}>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={'Background Color'} name={[field.key, 'bgColor']} labelCol={{ span: 6 }}>
|
||||||
|
<InputColor />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={'Color'} name={[field.key, 'color']} labelCol={{ span: 6 }}>
|
||||||
|
<InputColor />
|
||||||
|
</Form.Item>
|
||||||
|
</Card>
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
<Form.Item
|
||||||
|
wrapperCol={{
|
||||||
|
sm: { offset: 6 },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type="dashed"
|
||||||
|
onClick={() => {
|
||||||
|
add();
|
||||||
|
}}
|
||||||
|
style={{ width: '60%' }}
|
||||||
|
>
|
||||||
|
<PlusOutlined /> Add Button
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</Form.List>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
)
|
||||||
|
}
|
||||||
130
components/form/admin/field.card.tsx
Normal file
130
components/form/admin/field.card.tsx
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import {DeleteOutlined} from '@ant-design/icons/lib'
|
||||||
|
import {Button, Card, Checkbox, Form, Input, Popconfirm, Tag} from 'antd'
|
||||||
|
import {FormInstance} from 'antd/lib/form'
|
||||||
|
import {FieldData} from 'rc-field-form/lib/interface'
|
||||||
|
import React, {useEffect, useState} from 'react'
|
||||||
|
import {AdminFormFieldFragment} from '../../../graphql/fragment/admin.form.fragment'
|
||||||
|
import {DateType} from './types/date.type'
|
||||||
|
import {DropdownType} from './types/dropdown.type'
|
||||||
|
import {EmailType} from './types/email.type'
|
||||||
|
import {HiddenType} from './types/hidden.type'
|
||||||
|
import {LinkType} from './types/link.type'
|
||||||
|
import {NumberType} from './types/number.type'
|
||||||
|
import {RadioType} from './types/radio.type'
|
||||||
|
import {RatingType} from './types/rating.type'
|
||||||
|
import {TextType} from './types/text.type'
|
||||||
|
import {TextareaType} from './types/textarea.type'
|
||||||
|
import {YesNoType} from './types/yes_no.type'
|
||||||
|
|
||||||
|
export const availableTypes = {
|
||||||
|
'textfield': TextType,
|
||||||
|
'date': DateType,
|
||||||
|
'email': EmailType,
|
||||||
|
'textarea': TextareaType,
|
||||||
|
'link': LinkType,
|
||||||
|
'dropdown': DropdownType,
|
||||||
|
'rating': RatingType,
|
||||||
|
'radio': RadioType,
|
||||||
|
'hidden': HiddenType,
|
||||||
|
'yes_no': YesNoType,
|
||||||
|
'number': NumberType,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
form: FormInstance
|
||||||
|
fields: AdminFormFieldFragment[]
|
||||||
|
onChangeFields: (fields: AdminFormFieldFragment[]) => any
|
||||||
|
field: FieldData
|
||||||
|
remove: (index: number) => void
|
||||||
|
index: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FieldCard: React.FC<Props> = props => {
|
||||||
|
const {
|
||||||
|
form,
|
||||||
|
field,
|
||||||
|
fields,
|
||||||
|
onChangeFields,
|
||||||
|
remove,
|
||||||
|
index,
|
||||||
|
} = props
|
||||||
|
|
||||||
|
const type = form.getFieldValue(['form', 'fields', field.name as string, 'type'])
|
||||||
|
const TypeComponent: React.FC<any> = availableTypes[type] || TextType
|
||||||
|
|
||||||
|
const [nextTitle, setNextTitle] = useState(form.getFieldValue(['form', 'fields', field.name as string, 'title']))
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const id = setTimeout(() => {
|
||||||
|
console.log('update fields')
|
||||||
|
onChangeFields(fields.map((field, i) => {
|
||||||
|
if (i === index) {
|
||||||
|
return {
|
||||||
|
...field,
|
||||||
|
title: nextTitle,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return field
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}, 500)
|
||||||
|
|
||||||
|
return () => clearTimeout(id)
|
||||||
|
}, [nextTitle])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
title={nextTitle}
|
||||||
|
type={'inner'}
|
||||||
|
extra={(
|
||||||
|
<div>
|
||||||
|
<Tag color={'blue'}>{type}</Tag>
|
||||||
|
<Popconfirm
|
||||||
|
placement={'left'}
|
||||||
|
title={'Really remove this field? Check that it is not referenced anywhere!'}
|
||||||
|
okText={'Delete Field'}
|
||||||
|
okButtonProps={{ danger: true }}
|
||||||
|
onConfirm={() => {
|
||||||
|
remove(index)
|
||||||
|
onChangeFields(fields.filter((e, i) => i !== index))
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button danger><DeleteOutlined /></Button>
|
||||||
|
</Popconfirm>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
actions={[
|
||||||
|
<DeleteOutlined key={'delete'} onClick={() => remove(index)} />
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Form.Item name={[field.name as string, 'type']} noStyle><Input type={'hidden'} /></Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={'Title'}
|
||||||
|
name={[field.name as string, 'title']}
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: 'Title is required' }
|
||||||
|
]}
|
||||||
|
labelCol={{ span: 6 }}
|
||||||
|
>
|
||||||
|
<Input onChange={e => setNextTitle(e.target.value)}/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={'Description'}
|
||||||
|
name={[field.name as string, 'description']}
|
||||||
|
labelCol={{ span: 6 }}
|
||||||
|
>
|
||||||
|
<Input.TextArea autoSize />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={'Required'}
|
||||||
|
name={[field.name as string, 'required']}
|
||||||
|
labelCol={{ span: 6 }}
|
||||||
|
valuePropName={'checked'}
|
||||||
|
>
|
||||||
|
<Checkbox />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<TypeComponent field={field} />
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
101
components/form/admin/fields.tab.tsx
Normal file
101
components/form/admin/fields.tab.tsx
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import {PlusOutlined} from '@ant-design/icons/lib'
|
||||||
|
import {Button, Form, Select, Space, Tabs} from 'antd'
|
||||||
|
import {FormInstance} from 'antd/lib/form'
|
||||||
|
import {TabPaneProps} from 'antd/lib/tabs'
|
||||||
|
import React, {useCallback, useState} from 'react'
|
||||||
|
import {AdminFormFieldFragment} from '../../../graphql/fragment/admin.form.fragment'
|
||||||
|
import {availableTypes, FieldCard} from './field.card'
|
||||||
|
|
||||||
|
interface Props extends TabPaneProps {
|
||||||
|
form: FormInstance
|
||||||
|
fields: AdminFormFieldFragment[]
|
||||||
|
onChangeFields: (fields: AdminFormFieldFragment[]) => any
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FieldsTab: React.FC<Props> = props => {
|
||||||
|
const [nextType, setNextType] = useState('textfield')
|
||||||
|
|
||||||
|
const renderType = useCallback((field, index, remove) => {
|
||||||
|
return (
|
||||||
|
<FieldCard
|
||||||
|
form={props.form}
|
||||||
|
field={field}
|
||||||
|
index={index}
|
||||||
|
remove={remove}
|
||||||
|
fields={props.fields}
|
||||||
|
onChangeFields={props.onChangeFields}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}, [props.fields])
|
||||||
|
|
||||||
|
const addField = useCallback((add, index) => {
|
||||||
|
return (
|
||||||
|
<Form.Item
|
||||||
|
wrapperCol={{span: 24}}
|
||||||
|
>
|
||||||
|
<Space
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Select value={nextType} onChange={e => setNextType(e)} style={{ minWidth: 200 }}>
|
||||||
|
{Object.keys(availableTypes).map(type => <Select.Option value={type} key={type}>{type}</Select.Option> )}
|
||||||
|
</Select>
|
||||||
|
<Button
|
||||||
|
type="dashed"
|
||||||
|
onClick={() => {
|
||||||
|
const defaults: AdminFormFieldFragment = {
|
||||||
|
id: `NEW-${Date.now()}`,
|
||||||
|
type: nextType,
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
|
required: false,
|
||||||
|
value: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
add(defaults)
|
||||||
|
const next = [...props.fields]
|
||||||
|
next.splice(index, 0, defaults)
|
||||||
|
props.onChangeFields(next)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PlusOutlined /> Add Field
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}, [props.fields, nextType])
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tabs.TabPane {...props}>
|
||||||
|
|
||||||
|
<Form.List
|
||||||
|
name={['form', 'fields']}
|
||||||
|
>
|
||||||
|
{(fields, { add, remove, move }) => {
|
||||||
|
const addAndMove = (index) => (defaults) => {
|
||||||
|
add(defaults)
|
||||||
|
move(fields.length, index)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{addField(addAndMove(0), 0)}
|
||||||
|
{fields.map((field, index) => (
|
||||||
|
<div key={field.key}>
|
||||||
|
<Form.Item wrapperCol={{ span: 24 }}>
|
||||||
|
{renderType(field, index, remove)}
|
||||||
|
</Form.Item>
|
||||||
|
{addField(addAndMove(index + 1), index + 1)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</Form.List>
|
||||||
|
|
||||||
|
</Tabs.TabPane>
|
||||||
|
)
|
||||||
|
}
|
||||||
107
components/form/admin/respondent.notifications.tab.tsx
Normal file
107
components/form/admin/respondent.notifications.tab.tsx
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import {Form, Input, Select, Switch, Tabs} from 'antd'
|
||||||
|
import {FormInstance} from 'antd/lib/form'
|
||||||
|
import {TabPaneProps} from 'antd/lib/tabs'
|
||||||
|
import React, {useEffect, useState} from 'react'
|
||||||
|
import {AdminFormFieldFragment} from '../../../graphql/fragment/admin.form.fragment'
|
||||||
|
|
||||||
|
interface Props extends TabPaneProps {
|
||||||
|
form: FormInstance
|
||||||
|
fields: AdminFormFieldFragment[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RespondentNotificationsTab: React.FC<Props> = props => {
|
||||||
|
const [enabled, setEnabled] = useState<boolean>()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const next = props.form.getFieldValue(['form', 'respondentNotifications', 'enabled'])
|
||||||
|
|
||||||
|
if (next !== enabled) {
|
||||||
|
setEnabled(next)
|
||||||
|
}
|
||||||
|
}, [props.form.getFieldValue(['form', 'respondentNotifications', 'enabled'])])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
props.form.validateFields([
|
||||||
|
['form', 'respondentNotifications', 'subject'],
|
||||||
|
['form', 'respondentNotifications', 'htmlTemplate'],
|
||||||
|
['form', 'respondentNotifications', 'toField'],
|
||||||
|
])
|
||||||
|
}, [enabled])
|
||||||
|
|
||||||
|
const groups = {}
|
||||||
|
|
||||||
|
props.fields.forEach(field => {
|
||||||
|
if (!groups[field.type]) {
|
||||||
|
groups[field.type] = []
|
||||||
|
}
|
||||||
|
groups[field.type].push(field)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tabs.TabPane {...props}>
|
||||||
|
<Form.Item
|
||||||
|
label={'Enabled'}
|
||||||
|
name={['form', 'respondentNotifications', 'enabled']}
|
||||||
|
valuePropName={'checked'}
|
||||||
|
>
|
||||||
|
<Switch onChange={e => setEnabled(e.valueOf())} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={'Subject'}
|
||||||
|
name={['form', 'respondentNotifications', 'subject']}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: enabled,
|
||||||
|
message: 'Please provide a Subject',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={'HTML Template'}
|
||||||
|
name={['form', 'respondentNotifications', 'htmlTemplate']}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: enabled,
|
||||||
|
message: 'Please provide a Template',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input.TextArea autoSize />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={'Email Field'}
|
||||||
|
name={['form', 'respondentNotifications', 'toField']}
|
||||||
|
extra={'Field with Email for receipt'}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: enabled,
|
||||||
|
message: 'Please provide a Email Field',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Select>
|
||||||
|
{Object.keys(groups).map(key => (
|
||||||
|
<Select.OptGroup label={key.toUpperCase()} key={key}>
|
||||||
|
{groups[key].map(field => (
|
||||||
|
<Select.Option value={field.id} key={field.id}>{field.title}</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select.OptGroup>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={'Sender Email'}
|
||||||
|
name={['form', 'respondentNotifications', 'fromEmail']}
|
||||||
|
extra={'Make sure your mailserver can send from this email'}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
)
|
||||||
|
}
|
||||||
99
components/form/admin/self.notifications.tab.tsx
Normal file
99
components/form/admin/self.notifications.tab.tsx
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import {Form, Input, Select, Switch, Tabs} from 'antd'
|
||||||
|
import {FormInstance} from 'antd/lib/form'
|
||||||
|
import {TabPaneProps} from 'antd/lib/tabs'
|
||||||
|
import React, {useEffect, useState} from 'react'
|
||||||
|
import {AdminFormFieldFragment} from '../../../graphql/fragment/admin.form.fragment'
|
||||||
|
|
||||||
|
interface Props extends TabPaneProps {
|
||||||
|
form: FormInstance
|
||||||
|
fields: AdminFormFieldFragment[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SelfNotificationsTab: React.FC<Props> = props => {
|
||||||
|
const [enabled, setEnabled] = useState<boolean>()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const next = props.form.getFieldValue(['form', 'selfNotifications', 'enabled'])
|
||||||
|
|
||||||
|
if (next !== enabled) {
|
||||||
|
setEnabled(next)
|
||||||
|
}
|
||||||
|
}, [props.form.getFieldValue(['form', 'selfNotifications', 'enabled'])])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
props.form.validateFields([
|
||||||
|
['form', 'selfNotifications', 'subject'],
|
||||||
|
['form', 'selfNotifications', 'htmlTemplate'],
|
||||||
|
])
|
||||||
|
}, [enabled])
|
||||||
|
|
||||||
|
const groups = {}
|
||||||
|
props.fields.forEach(field => {
|
||||||
|
if (!groups[field.type]) {
|
||||||
|
groups[field.type] = []
|
||||||
|
}
|
||||||
|
groups[field.type].push(field)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tabs.TabPane {...props}>
|
||||||
|
<Form.Item
|
||||||
|
label={'Enabled'}
|
||||||
|
name={['form', 'selfNotifications', 'enabled']}
|
||||||
|
valuePropName={'checked'}
|
||||||
|
>
|
||||||
|
<Switch onChange={e => setEnabled(e.valueOf())} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={'Subject'}
|
||||||
|
name={['form', 'selfNotifications', 'subject']}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: enabled,
|
||||||
|
message: 'Please provide a Subject',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={'HTML Template'}
|
||||||
|
name={['form', 'selfNotifications', 'htmlTemplate']}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: enabled,
|
||||||
|
message: 'Please provide a Template',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input.TextArea autoSize />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={'Email Field'}
|
||||||
|
name={['form', 'selfNotifications', 'fromField']}
|
||||||
|
extra={'Field with Email, will set the Reply-To header'}
|
||||||
|
>
|
||||||
|
<Select>
|
||||||
|
{Object.keys(groups).map(key => (
|
||||||
|
<Select.OptGroup label={key.toUpperCase()} key={key}>
|
||||||
|
{groups[key].map(field => (
|
||||||
|
<Select.Option value={field.id} key={field.id}>{field.title}</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select.OptGroup>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={'Your Email'}
|
||||||
|
name={['form', 'selfNotifications', 'toEmail']}
|
||||||
|
extra={'If not set will send to the admin of the form'}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
)
|
||||||
|
}
|
||||||
98
components/form/admin/start.page.tab.tsx
Normal file
98
components/form/admin/start.page.tab.tsx
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import {DeleteOutlined, PlusOutlined} from '@ant-design/icons/lib'
|
||||||
|
import {Button, Card, Form, Input, Switch, Tabs} from 'antd'
|
||||||
|
import {TabPaneProps} from 'antd/lib/tabs'
|
||||||
|
import React from 'react'
|
||||||
|
import {InputColor} from '../../input/color'
|
||||||
|
|
||||||
|
export const StartPageTab: React.FC<TabPaneProps> = props => {
|
||||||
|
return (
|
||||||
|
<Tabs.TabPane {...props}>
|
||||||
|
<Form.Item
|
||||||
|
label={'Show'}
|
||||||
|
name={['form', 'startPage', 'show']}
|
||||||
|
valuePropName={'checked'}
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={'Title'}
|
||||||
|
name={['form', 'startPage', 'title']}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={'Paragraph'}
|
||||||
|
name={['form', 'startPage', 'paragraph']}
|
||||||
|
>
|
||||||
|
<Input.TextArea autoSize />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={'Continue Button Text'}
|
||||||
|
name={['form', 'startPage', 'buttonText']}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.List
|
||||||
|
name={['form', 'startPage', 'buttons']}
|
||||||
|
>
|
||||||
|
{(fields, { add, remove }) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{fields.map((field, index) => (
|
||||||
|
<Form.Item
|
||||||
|
wrapperCol={{
|
||||||
|
sm: { offset: index === 0 ? 0 : 6 },
|
||||||
|
}}
|
||||||
|
label={index === 0 ? 'Buttons' : ''}
|
||||||
|
key={field.key}
|
||||||
|
>
|
||||||
|
<Card
|
||||||
|
actions={[
|
||||||
|
<DeleteOutlined key={'delete'} onClick={() => remove(index)} />
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Form.Item label={'Url'} name={[field.key, 'url']} labelCol={{ span: 6 }}>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={'Action'} name={[field.key, 'action']} labelCol={{ span: 6 }}>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={'Text'} name={[field.key, 'text']} labelCol={{ span: 6 }}>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={'Background Color'} name={[field.key, 'bgColor']} labelCol={{ span: 6 }}>
|
||||||
|
<InputColor />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={'Color'} name={[field.key, 'color']} labelCol={{ span: 6 }}>
|
||||||
|
<InputColor />
|
||||||
|
</Form.Item>
|
||||||
|
</Card>
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
<Form.Item
|
||||||
|
wrapperCol={{
|
||||||
|
sm: { offset: 6 },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type="dashed"
|
||||||
|
onClick={() => {
|
||||||
|
add();
|
||||||
|
}}
|
||||||
|
style={{ width: '60%' }}
|
||||||
|
>
|
||||||
|
<PlusOutlined /> Add Button
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</Form.List>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
)
|
||||||
|
}
|
||||||
34
components/form/admin/types/date.type.tsx
Normal file
34
components/form/admin/types/date.type.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import {Form, Input} from 'antd'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
field: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DateType: React.FC<Props> = props => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Form.Item
|
||||||
|
label={'Default Date'}
|
||||||
|
name={[props.field.name, 'value']}
|
||||||
|
labelCol={{ span: 6 }}
|
||||||
|
>
|
||||||
|
<Input type={'date'} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={'Min Date'}
|
||||||
|
name={[props.field.name, 'value']}
|
||||||
|
labelCol={{ span: 6 }}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={'Max Date'}
|
||||||
|
name={[props.field.name, 'value']}
|
||||||
|
labelCol={{ span: 6 }}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
21
components/form/admin/types/dropdown.type.tsx
Normal file
21
components/form/admin/types/dropdown.type.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import {Form, Input} from 'antd'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
field: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DropdownType: React.FC<Props> = props => {
|
||||||
|
// TODO add dropdown options
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Form.Item
|
||||||
|
label={'Default Value'}
|
||||||
|
name={[props.field.name, 'value']}
|
||||||
|
labelCol={{ span: 6 }}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
23
components/form/admin/types/email.type.tsx
Normal file
23
components/form/admin/types/email.type.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import {Form, Input} from 'antd'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
field: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EmailType: React.FC<Props> = props => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Form.Item
|
||||||
|
label={'Default Email'}
|
||||||
|
name={[props.field.name, 'value']}
|
||||||
|
rules={[
|
||||||
|
{ type: 'email', message: 'Must be a valid email' }
|
||||||
|
]}
|
||||||
|
labelCol={{ span: 6 }}
|
||||||
|
>
|
||||||
|
<Input type={'email'} />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
20
components/form/admin/types/hidden.type.tsx
Normal file
20
components/form/admin/types/hidden.type.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import {Form, Input} from 'antd'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
field: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HiddenType: React.FC<Props> = props => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Form.Item
|
||||||
|
label={'Default Value'}
|
||||||
|
name={[props.field.name, 'value']}
|
||||||
|
labelCol={{ span: 6 }}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
23
components/form/admin/types/link.type.tsx
Normal file
23
components/form/admin/types/link.type.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import {Form, Input} from 'antd'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
field: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LinkType: React.FC<Props> = props => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Form.Item
|
||||||
|
label={'Default Link'}
|
||||||
|
name={[props.field.name, 'value']}
|
||||||
|
rules={[
|
||||||
|
{ type: 'url', message: 'Must be a valid URL' }
|
||||||
|
]}
|
||||||
|
labelCol={{ span: 6 }}
|
||||||
|
>
|
||||||
|
<Input type={'url'} />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
20
components/form/admin/types/number.type.tsx
Normal file
20
components/form/admin/types/number.type.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import {Form, InputNumber} from 'antd'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
field: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NumberType: React.FC<Props> = props => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Form.Item
|
||||||
|
label={'Default Number'}
|
||||||
|
name={[props.field.name, 'value']}
|
||||||
|
labelCol={{ span: 6 }}
|
||||||
|
>
|
||||||
|
<InputNumber />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
22
components/form/admin/types/radio.type.tsx
Normal file
22
components/form/admin/types/radio.type.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import {Form, Input} from 'antd'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
field: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RadioType: React.FC<Props> = props => {
|
||||||
|
// TODO Add radio support
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Form.Item
|
||||||
|
label={'Default Value'}
|
||||||
|
name={[props.field.name, 'value']}
|
||||||
|
labelCol={{ span: 6 }}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
22
components/form/admin/types/rating.type.tsx
Normal file
22
components/form/admin/types/rating.type.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import {Form, Input} from 'antd'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
field: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RatingType: React.FC<Props> = props => {
|
||||||
|
// TODO add ratings
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Form.Item
|
||||||
|
label={'Default Value'}
|
||||||
|
name={[props.field.name, 'value']}
|
||||||
|
labelCol={{ span: 6 }}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
20
components/form/admin/types/text.type.tsx
Normal file
20
components/form/admin/types/text.type.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import {Form, Input} from 'antd'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
field: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TextType: React.FC<Props> = props => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Form.Item
|
||||||
|
label={'Default Value'}
|
||||||
|
name={[props.field.name, 'value']}
|
||||||
|
labelCol={{ span: 6 }}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
20
components/form/admin/types/textarea.type.tsx
Normal file
20
components/form/admin/types/textarea.type.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import {Form, Input} from 'antd'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
field: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TextareaType: React.FC<Props> = props => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Form.Item
|
||||||
|
label={'Default Value'}
|
||||||
|
name={[props.field.name, 'value']}
|
||||||
|
labelCol={{ span: 6 }}
|
||||||
|
>
|
||||||
|
<Input.TextArea autoSize />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
21
components/form/admin/types/yes_no.type.tsx
Normal file
21
components/form/admin/types/yes_no.type.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import {Form, Input} from 'antd'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
field: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export const YesNoType: React.FC<Props> = props => {
|
||||||
|
// TODO add switch
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Form.Item
|
||||||
|
label={'Default Value'}
|
||||||
|
name={[props.field.name, 'value']}
|
||||||
|
labelCol={{ span: 6 }}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
22
components/form/is.live.tsx
Normal file
22
components/form/is.live.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import {CheckCircleOutlined, CloseCircleOutlined} from '@ant-design/icons/lib'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
isLive: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FormIsLive: React.FC<Props> = props => {
|
||||||
|
if (props.isLive) {
|
||||||
|
return (
|
||||||
|
<CheckCircleOutlined style={{
|
||||||
|
color: 'green'
|
||||||
|
}} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CloseCircleOutlined style={{
|
||||||
|
color: 'red'
|
||||||
|
}} />
|
||||||
|
)
|
||||||
|
}
|
||||||
38
components/input/color.tsx
Normal file
38
components/input/color.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import React, {useEffect} from 'react'
|
||||||
|
import {BlockPicker} from 'react-color'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
value?: string
|
||||||
|
onChange?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InputColor: React.FC<Props> = props => {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!props.value) {
|
||||||
|
props.onChange('#FFF')
|
||||||
|
}
|
||||||
|
}, [props.value])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BlockPicker
|
||||||
|
triangle={'hide'}
|
||||||
|
width={'100%'}
|
||||||
|
color={props.value}
|
||||||
|
onChange={e => props.onChange(e.hex)}
|
||||||
|
styles={{
|
||||||
|
default: {
|
||||||
|
card: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
display: 'flex',
|
||||||
|
boxShadow: 'none'
|
||||||
|
},
|
||||||
|
head: {
|
||||||
|
flex: 1,
|
||||||
|
borderRadius: 6,
|
||||||
|
height: 'auto',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
21
components/loading.page.tsx
Normal file
21
components/loading.page.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import {Spin} from 'antd'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
message?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LoadingPage: React.FC<Props> = props => {
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
height: '100vh',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
}}>
|
||||||
|
<Spin size="large"/>
|
||||||
|
{props.message}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -15,26 +15,33 @@ export const sideMenu: SideMenuElement[] = [
|
|||||||
{
|
{
|
||||||
key: 'home',
|
key: 'home',
|
||||||
name: 'Home',
|
name: 'Home',
|
||||||
href: '/',
|
href: '/admin',
|
||||||
icon: <HomeOutlined />,
|
icon: <HomeOutlined />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'communication',
|
key: 'public',
|
||||||
name: 'Communication',
|
name: 'Forms',
|
||||||
|
group: true,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
key: 'forms',
|
||||||
|
name: 'Forms',
|
||||||
|
href: '/admin/forms',
|
||||||
|
icon: <MessageOutlined />,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'administration',
|
||||||
|
name: 'Administration',
|
||||||
group: true,
|
group: true,
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
key: 'users',
|
key: 'users',
|
||||||
name: 'Users',
|
name: 'Users',
|
||||||
href: '/users',
|
href: '/admin/users',
|
||||||
icon: <TeamOutlined />,
|
icon: <TeamOutlined />,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: 'chats',
|
|
||||||
name: 'Chats',
|
|
||||||
href: '/chats',
|
|
||||||
icon: <MessageOutlined />,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@ -139,9 +139,7 @@ const Structure: FunctionComponent<Props> = (props) => {
|
|||||||
onClick: () => setSidebar(!sidebar),
|
onClick: () => setSidebar(!sidebar),
|
||||||
})}
|
})}
|
||||||
|
|
||||||
<img src={require('assets/images/logo_white_small.png')} height={30} style={{marginRight: 16}} alt={'RecTag'} />
|
<img src={require('assets/images/logo_white_small.png')} height={30} style={{marginRight: 16}} alt={'OhMyForm'} />
|
||||||
|
|
||||||
{publicRuntimeConfig.area.toUpperCase()}
|
|
||||||
</div>
|
</div>
|
||||||
<div style={{float: 'right', display: 'flex', height: '100%'}}>
|
<div style={{float: 'right', display: 'flex', height: '100%'}}>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
|
|||||||
@ -1,9 +1,14 @@
|
|||||||
import {Spin} from 'antd'
|
import {useQuery} from '@apollo/react-hooks'
|
||||||
import {AxiosRequestConfig} from 'axios'
|
import {AxiosRequestConfig} from 'axios'
|
||||||
import getConfig from 'next/config'
|
import {useRouter} from 'next/router'
|
||||||
import React from 'react'
|
import React, {useEffect, useState} from 'react'
|
||||||
|
import {ME_QUERY, MeQueryData} from '../graphql/query/me.query'
|
||||||
|
import {LoadingPage} from './loading.page'
|
||||||
|
|
||||||
const { publicRuntimeConfig } = getConfig()
|
export const setAuth = (access, refresh) => {
|
||||||
|
localStorage.setItem('access', access)
|
||||||
|
localStorage.setItem('refresh', refresh)
|
||||||
|
}
|
||||||
|
|
||||||
export const authConfig = async (config: AxiosRequestConfig = {}): Promise<AxiosRequestConfig> => {
|
export const authConfig = async (config: AxiosRequestConfig = {}): Promise<AxiosRequestConfig> => {
|
||||||
if (!config.headers) {
|
if (!config.headers) {
|
||||||
@ -11,7 +16,12 @@ export const authConfig = async (config: AxiosRequestConfig = {}): Promise<Axios
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// TODO config.headers.Authorization = `Bearer ${session.getAccessToken().getJwtToken()}`
|
const token = localStorage.getItem('access')
|
||||||
|
// TODO check for validity / use refresh token
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
@ -19,39 +29,47 @@ export const authConfig = async (config: AxiosRequestConfig = {}): Promise<Axios
|
|||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
export const withAuth = (Component): React.FC => {
|
export const withAuth = (Component, roles: string[] = []): React.FC => {
|
||||||
return props => {
|
return props => {
|
||||||
const [signedIn, setSignedIn] = React.useState(false);
|
const router = useRouter()
|
||||||
const [loading, setLoading] = React.useState(true);
|
const [access, setAccess] = useState(false)
|
||||||
|
const {loading, data, error} = useQuery<MeQueryData>(ME_QUERY)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!error) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
React.useEffect(() => {
|
localStorage.clear()
|
||||||
(async () => {
|
const path = router.asPath || router.pathname
|
||||||
try {
|
localStorage.setItem('redirect', path)
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(false)
|
router.push('/login')
|
||||||
})();
|
}, [error])
|
||||||
}, []);
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!data || roles.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = roles
|
||||||
|
.map(role => data.me.roles.includes(role))
|
||||||
|
.filter(p => p)
|
||||||
|
.length > 0
|
||||||
|
|
||||||
|
setAccess(next)
|
||||||
|
|
||||||
|
if (!next) {
|
||||||
|
router.push('/')
|
||||||
|
}
|
||||||
|
}, [data])
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return <LoadingPage message={'Loading Credentials'} />
|
||||||
<div style={{
|
|
||||||
height: '100vh',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
}}>
|
|
||||||
<Spin size="large"/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!signedIn) {
|
if (!access) {
|
||||||
// TODO
|
return <LoadingPage message={'Checking Credentials'} />
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Component {...props} />
|
return <Component {...props} />
|
||||||
|
|||||||
143
graphql/fragment/admin.form.fragment.ts
Normal file
143
graphql/fragment/admin.form.fragment.ts
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
import {gql} from 'apollo-boost'
|
||||||
|
|
||||||
|
export interface AdminFormPageFragment {
|
||||||
|
show: boolean
|
||||||
|
title?: string
|
||||||
|
paragraph?: string
|
||||||
|
buttonText?: string
|
||||||
|
buttons: {
|
||||||
|
url?: string
|
||||||
|
action?: string
|
||||||
|
text?: string
|
||||||
|
bgColor?: string
|
||||||
|
color?: string
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminFormFieldFragment {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
type: string
|
||||||
|
description: string
|
||||||
|
required: boolean
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminFormFragment {
|
||||||
|
id?: string
|
||||||
|
title: string
|
||||||
|
created: string
|
||||||
|
lastModified?: string
|
||||||
|
language: string
|
||||||
|
showFooter: boolean
|
||||||
|
isLive: boolean
|
||||||
|
fields: AdminFormFieldFragment[]
|
||||||
|
selfNotifications: {
|
||||||
|
enabled: boolean
|
||||||
|
subject?: string
|
||||||
|
htmlTemplate?: string
|
||||||
|
fromField?: string
|
||||||
|
toEmail?: string
|
||||||
|
}
|
||||||
|
respondentNotifications: {
|
||||||
|
enabled: boolean
|
||||||
|
subject?: string
|
||||||
|
htmlTemplate?: string
|
||||||
|
toField?: string
|
||||||
|
fromEmail?: string
|
||||||
|
}
|
||||||
|
design: {
|
||||||
|
colors: {
|
||||||
|
backgroundColor: string
|
||||||
|
questionColor: string
|
||||||
|
answerColor: string
|
||||||
|
buttonColor: string
|
||||||
|
buttonTextColor: string
|
||||||
|
}
|
||||||
|
font?: string
|
||||||
|
}
|
||||||
|
startPage: AdminFormPageFragment
|
||||||
|
endPage: AdminFormPageFragment
|
||||||
|
admin: {
|
||||||
|
id: string
|
||||||
|
username: string
|
||||||
|
email: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ADMIN_FORM_FRAGMENT = gql`
|
||||||
|
fragment AdminForm on Form {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
created
|
||||||
|
lastModified
|
||||||
|
language
|
||||||
|
showFooter
|
||||||
|
isLive
|
||||||
|
|
||||||
|
fields {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
type
|
||||||
|
description
|
||||||
|
required
|
||||||
|
value
|
||||||
|
}
|
||||||
|
|
||||||
|
selfNotifications {
|
||||||
|
enabled
|
||||||
|
subject
|
||||||
|
htmlTemplate
|
||||||
|
fromField
|
||||||
|
toEmail
|
||||||
|
}
|
||||||
|
respondentNotifications {
|
||||||
|
enabled
|
||||||
|
subject
|
||||||
|
htmlTemplate
|
||||||
|
toField
|
||||||
|
fromEmail
|
||||||
|
}
|
||||||
|
design {
|
||||||
|
colors {
|
||||||
|
backgroundColor
|
||||||
|
questionColor
|
||||||
|
answerColor
|
||||||
|
buttonColor
|
||||||
|
buttonTextColor
|
||||||
|
}
|
||||||
|
font
|
||||||
|
}
|
||||||
|
startPage {
|
||||||
|
show
|
||||||
|
title
|
||||||
|
paragraph
|
||||||
|
buttonText
|
||||||
|
buttons {
|
||||||
|
url
|
||||||
|
action
|
||||||
|
text
|
||||||
|
bgColor
|
||||||
|
color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
endPage {
|
||||||
|
show
|
||||||
|
title
|
||||||
|
paragraph
|
||||||
|
buttonText
|
||||||
|
buttons {
|
||||||
|
url
|
||||||
|
action
|
||||||
|
text
|
||||||
|
bgColor
|
||||||
|
color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
admin {
|
||||||
|
id
|
||||||
|
username
|
||||||
|
email
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
20
graphql/mutation/admin.form.create.mutation.ts
Normal file
20
graphql/mutation/admin.form.create.mutation.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import {gql} from 'apollo-boost'
|
||||||
|
import {ADMIN_FORM_FRAGMENT, AdminFormFragment} from '../fragment/admin.form.fragment'
|
||||||
|
|
||||||
|
export interface AdminFormCreateMutationData {
|
||||||
|
form: AdminFormFragment
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminFormCreateMutationVariables {
|
||||||
|
form: AdminFormFragment
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ADMIN_FORM_CREATE_MUTATION = gql`
|
||||||
|
mutation update($$form: FormCreateInput!) {
|
||||||
|
form: createForm(form: $form) {
|
||||||
|
...AdminForm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
${ADMIN_FORM_FRAGMENT}
|
||||||
|
`
|
||||||
20
graphql/mutation/admin.form.update.mutation.ts
Normal file
20
graphql/mutation/admin.form.update.mutation.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import {gql} from 'apollo-boost'
|
||||||
|
import {ADMIN_FORM_FRAGMENT, AdminFormFragment} from '../fragment/admin.form.fragment'
|
||||||
|
|
||||||
|
export interface AdminFormUpdateMutationData {
|
||||||
|
form: AdminFormFragment
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminFormUpdateMutationVariables {
|
||||||
|
form: AdminFormFragment
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ADMIN_FORM_UPDATE_MUTATION = gql`
|
||||||
|
mutation update($form: FormUpdateInput!) {
|
||||||
|
form: updateForm(form: $form) {
|
||||||
|
...AdminForm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
${ADMIN_FORM_FRAGMENT}
|
||||||
|
`
|
||||||
10
graphql/mutation/login.mutation.ts
Normal file
10
graphql/mutation/login.mutation.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import {gql} from 'apollo-boost'
|
||||||
|
|
||||||
|
export const LOGIN_MUTATION = gql`
|
||||||
|
mutation login($username: String!, $password: String!) {
|
||||||
|
tokens: authLogin(username: $username, password: $password) {
|
||||||
|
access: accessToken
|
||||||
|
refresh: refreshToken
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
10
graphql/mutation/register.mutation.ts
Normal file
10
graphql/mutation/register.mutation.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import {gql} from 'apollo-boost'
|
||||||
|
|
||||||
|
export const REGISTER_MUTATION = gql`
|
||||||
|
mutation register($user: UserCreateInput!) {
|
||||||
|
tokens: authRegister(user: $user) {
|
||||||
|
access: accessToken
|
||||||
|
refresh: refreshToken
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
20
graphql/query/admin.form.query.ts
Normal file
20
graphql/query/admin.form.query.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import {gql} from 'apollo-boost'
|
||||||
|
import {ADMIN_FORM_FRAGMENT, AdminFormFragment} from '../fragment/admin.form.fragment'
|
||||||
|
|
||||||
|
export interface AdminFormQueryData {
|
||||||
|
form: AdminFormFragment
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminFormQueryVariables {
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ADMIN_FORM_QUERY = gql`
|
||||||
|
query form($id: ID!){
|
||||||
|
form:getFormById(id: $id) {
|
||||||
|
...AdminForm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
${ADMIN_FORM_FRAGMENT}
|
||||||
|
`
|
||||||
18
graphql/query/me.query.ts
Normal file
18
graphql/query/me.query.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import {gql} from 'apollo-boost'
|
||||||
|
|
||||||
|
export interface MeQueryData {
|
||||||
|
me: {
|
||||||
|
id: string
|
||||||
|
|
||||||
|
roles: string[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ME_QUERY = gql`
|
||||||
|
query {
|
||||||
|
me {
|
||||||
|
id
|
||||||
|
roles
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
53
graphql/query/pager.form.query.ts
Normal file
53
graphql/query/pager.form.query.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import {gql} from 'apollo-boost'
|
||||||
|
|
||||||
|
export interface PagerFormEntryQueryData {
|
||||||
|
id: string
|
||||||
|
created: string
|
||||||
|
lastModified?: string
|
||||||
|
title: string
|
||||||
|
isLive: boolean
|
||||||
|
language: string
|
||||||
|
admin: {
|
||||||
|
id: string
|
||||||
|
email: string
|
||||||
|
username: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PagerFormQueryData {
|
||||||
|
pager: {
|
||||||
|
entries: PagerFormEntryQueryData[]
|
||||||
|
|
||||||
|
total: number
|
||||||
|
limit: number
|
||||||
|
start: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PagerFormQueryVariables {
|
||||||
|
start?: number
|
||||||
|
limit?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PAGER_FORM_QUERY = gql`
|
||||||
|
query pager($start: Int, $limit: Int){
|
||||||
|
pager: listForms(start: $start, limit: $limit) {
|
||||||
|
entries {
|
||||||
|
id
|
||||||
|
created
|
||||||
|
lastModified
|
||||||
|
title
|
||||||
|
isLive
|
||||||
|
language
|
||||||
|
admin {
|
||||||
|
id
|
||||||
|
email
|
||||||
|
username
|
||||||
|
}
|
||||||
|
}
|
||||||
|
total
|
||||||
|
limit
|
||||||
|
start
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
@ -1,9 +1,10 @@
|
|||||||
const withImages = require('next-images')
|
const withImages = require('next-images')
|
||||||
const module = require('./package.json')
|
const p = require('./package.json')
|
||||||
|
|
||||||
const version = module.version;
|
const version = p.version;
|
||||||
|
|
||||||
module.exports = withImages({
|
module.exports = withImages({
|
||||||
|
|
||||||
publicRuntimeConfig: {
|
publicRuntimeConfig: {
|
||||||
endpoint: process.env.API_HOST || '/graphql',
|
endpoint: process.env.API_HOST || '/graphql',
|
||||||
version,
|
version,
|
||||||
|
|||||||
12
package.json
12
package.json
@ -3,20 +3,28 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"start:dev": "next dev -p 4000",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"export": "next build && next export",
|
"export": "next build && next export",
|
||||||
"start": "next start"
|
"start": "next start -p 4010",
|
||||||
|
"server": "node server.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons": "^4.1.0",
|
"@ant-design/icons": "^4.1.0",
|
||||||
|
"@apollo/react-hooks": "^3.1.5",
|
||||||
|
"@lifeomic/axios-fetch": "^1.4.2",
|
||||||
"antd": "^4.2.2",
|
"antd": "^4.2.2",
|
||||||
|
"apollo-boost": "^0.4.9",
|
||||||
"axios": "^0.19.2",
|
"axios": "^0.19.2",
|
||||||
"dayjs": "^1.8.27",
|
"dayjs": "^1.8.27",
|
||||||
|
"express": "^4.17.1",
|
||||||
|
"graphql": "^15.0.0",
|
||||||
|
"http-proxy-middleware": "^1.0.4",
|
||||||
"next": "9.4.0",
|
"next": "9.4.0",
|
||||||
"next-images": "^1.4.0",
|
"next-images": "^1.4.0",
|
||||||
"next-redux-wrapper": "^6.0.0",
|
"next-redux-wrapper": "^6.0.0",
|
||||||
"react": "16.13.1",
|
"react": "16.13.1",
|
||||||
|
"react-color": "^2.18.1",
|
||||||
"react-dom": "16.13.1",
|
"react-dom": "16.13.1",
|
||||||
"react-icons": "^3.10.0",
|
"react-icons": "^3.10.0",
|
||||||
"react-redux": "^7.2.0",
|
"react-redux": "^7.2.0",
|
||||||
|
|||||||
@ -1,23 +1,36 @@
|
|||||||
|
import {ApolloProvider} from '@apollo/react-common'
|
||||||
|
import {buildAxiosFetch} from '@lifeomic/axios-fetch'
|
||||||
import 'antd/dist/antd.css'
|
import 'antd/dist/antd.css'
|
||||||
|
import ApolloClient from 'apollo-boost'
|
||||||
import 'assets/global.scss'
|
import 'assets/global.scss'
|
||||||
import 'assets/variables.scss'
|
import 'assets/variables.scss'
|
||||||
|
import axios from 'axios'
|
||||||
import {AppProps} from 'next/app'
|
import {AppProps} from 'next/app'
|
||||||
import getConfig from 'next/config'
|
import getConfig from 'next/config'
|
||||||
import Head from 'next/head'
|
import Head from 'next/head'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {wrapper} from 'store'
|
import {wrapper} from 'store'
|
||||||
|
import {authConfig} from '../components/with.auth'
|
||||||
|
|
||||||
const { publicRuntimeConfig } = getConfig()
|
const { publicRuntimeConfig } = getConfig()
|
||||||
|
|
||||||
|
const client = new ApolloClient({
|
||||||
|
uri: publicRuntimeConfig.endpoint,
|
||||||
|
fetch: buildAxiosFetch(axios),
|
||||||
|
request: async (operation): Promise<void> => {
|
||||||
|
operation.setContext(await authConfig())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const App: React.FC<AppProps> = ({ Component, pageProps }) => {
|
const App: React.FC<AppProps> = ({ Component, pageProps }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<ApolloProvider client={client}>
|
||||||
<Head>
|
<Head>
|
||||||
<title>OhMyForm</title>
|
<title>OhMyForm</title>
|
||||||
<meta name="theme-color" content={'#4182e4'} />
|
<meta name="theme-color" content={'#4182e4'} />
|
||||||
</Head>
|
</Head>
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
</>
|
</ApolloProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
134
pages/admin/forms/[id]/index.tsx
Normal file
134
pages/admin/forms/[id]/index.tsx
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
import {useMutation, useQuery} from '@apollo/react-hooks'
|
||||||
|
import {Button, Form, Input, message, Tabs} from 'antd'
|
||||||
|
import {useForm} from 'antd/lib/form/Form'
|
||||||
|
import {NextPage} from 'next'
|
||||||
|
import {useRouter} from 'next/router'
|
||||||
|
import React, {useState} from 'react'
|
||||||
|
import {cleanInput} from '../../../../components/clean.input'
|
||||||
|
import {BaseDataTab} from '../../../../components/form/admin/base.data.tab'
|
||||||
|
import {DesignTab} from '../../../../components/form/admin/design.tab'
|
||||||
|
import {EndPageTab} from '../../../../components/form/admin/end.page.tab'
|
||||||
|
import {FieldsTab} from '../../../../components/form/admin/fields.tab'
|
||||||
|
import {RespondentNotificationsTab} from '../../../../components/form/admin/respondent.notifications.tab'
|
||||||
|
import {SelfNotificationsTab} from '../../../../components/form/admin/self.notifications.tab'
|
||||||
|
import {StartPageTab} from '../../../../components/form/admin/start.page.tab'
|
||||||
|
import Structure from '../../../../components/structure'
|
||||||
|
import {withAuth} from '../../../../components/with.auth'
|
||||||
|
import {AdminFormFieldFragment} from '../../../../graphql/fragment/admin.form.fragment'
|
||||||
|
import {
|
||||||
|
ADMIN_FORM_UPDATE_MUTATION,
|
||||||
|
AdminFormUpdateMutationData,
|
||||||
|
AdminFormUpdateMutationVariables
|
||||||
|
} from '../../../../graphql/mutation/admin.form.update.mutation'
|
||||||
|
import {ADMIN_FORM_QUERY, AdminFormQueryData, AdminFormQueryVariables} from '../../../../graphql/query/admin.form.query'
|
||||||
|
|
||||||
|
const Index: NextPage = () => {
|
||||||
|
const router = useRouter()
|
||||||
|
const [form] = useForm()
|
||||||
|
const [saving, setSaving] = useState(false)
|
||||||
|
const [fields, setFields] = useState<AdminFormFieldFragment[]>([])
|
||||||
|
const [update] = useMutation<AdminFormUpdateMutationData, AdminFormUpdateMutationVariables>(ADMIN_FORM_UPDATE_MUTATION)
|
||||||
|
|
||||||
|
const {data, loading, error} = useQuery<AdminFormQueryData, AdminFormQueryVariables>(ADMIN_FORM_QUERY, {
|
||||||
|
variables: {
|
||||||
|
id: router.query.id as string
|
||||||
|
},
|
||||||
|
onCompleted: next => {
|
||||||
|
form.setFieldsValue(next)
|
||||||
|
setFields(next.form.fields)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const save = async (formData: AdminFormQueryData) => {
|
||||||
|
setSaving(true)
|
||||||
|
console.log('try to save form!', formData)
|
||||||
|
|
||||||
|
formData.form.fields = formData.form.fields.filter(e => e && e.type)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const next = (await update({
|
||||||
|
variables: cleanInput(formData),
|
||||||
|
})).data
|
||||||
|
|
||||||
|
form.setFieldsValue(next)
|
||||||
|
setFields(next.form.fields)
|
||||||
|
|
||||||
|
message.success('Form Updated')
|
||||||
|
} catch (e) {
|
||||||
|
console.error('failed to save', e)
|
||||||
|
message.error('Could not save Form')
|
||||||
|
}
|
||||||
|
|
||||||
|
setSaving(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Structure
|
||||||
|
loading={loading || saving}
|
||||||
|
title={loading ? 'Loading Form' : `Edit Form "${data.form.title}"`}
|
||||||
|
selected={'forms'}
|
||||||
|
breadcrumbs={[
|
||||||
|
{ href: '/admin', name: 'Home' },
|
||||||
|
{ href: '/admin/forms', name: 'Form' },
|
||||||
|
]}
|
||||||
|
extra={[
|
||||||
|
<Button
|
||||||
|
key={'save'}
|
||||||
|
onClick={form.submit}
|
||||||
|
type={'primary'}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
|
style={{paddingTop: 0}}
|
||||||
|
>
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
onFinish={save}
|
||||||
|
onFinishFailed={errors => {
|
||||||
|
message.error('Required fields are missing')
|
||||||
|
}}
|
||||||
|
labelCol={{
|
||||||
|
xs: { span: 24 },
|
||||||
|
sm: { span: 6 },
|
||||||
|
}}
|
||||||
|
wrapperCol={{
|
||||||
|
xs: { span: 24 },
|
||||||
|
sm: { span: 18 },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Form.Item noStyle name={['form', 'id']}><Input type={'hidden'} /></Form.Item>
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<FieldsTab
|
||||||
|
key={'fields'}
|
||||||
|
tab={'Fields'}
|
||||||
|
fields={fields}
|
||||||
|
onChangeFields={setFields}
|
||||||
|
form={form}
|
||||||
|
/>
|
||||||
|
<BaseDataTab key={'base_data'} tab={'Base Data'} />
|
||||||
|
<DesignTab key={'design'} tab={'Design'} />
|
||||||
|
<SelfNotificationsTab
|
||||||
|
key={'self_notifications'}
|
||||||
|
tab={'Self Notifications'}
|
||||||
|
fields={fields}
|
||||||
|
form={form}
|
||||||
|
/>
|
||||||
|
<RespondentNotificationsTab
|
||||||
|
key={'respondent_notifications'}
|
||||||
|
tab={'Respondent Notifications'}
|
||||||
|
fields={fields}
|
||||||
|
form={form}
|
||||||
|
/>
|
||||||
|
<StartPageTab key={'start_page'} tab={'Start Page'} />
|
||||||
|
<EndPageTab key={'end_page'} tab={'End Page'} />
|
||||||
|
</Tabs>
|
||||||
|
</Form>
|
||||||
|
</Structure>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withAuth(Index, ['admin'])
|
||||||
135
pages/admin/forms/index.tsx
Normal file
135
pages/admin/forms/index.tsx
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import {DeleteOutlined, EditOutlined, GlobalOutlined} from '@ant-design/icons/lib'
|
||||||
|
import {useQuery} from '@apollo/react-hooks'
|
||||||
|
import {Button, Popconfirm, Space, Table, Tooltip} from 'antd'
|
||||||
|
import {PaginationProps} from 'antd/es/pagination'
|
||||||
|
import {NextPage} from 'next'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import React, {useState} from 'react'
|
||||||
|
import {DateTime} from '../../../components/date.time'
|
||||||
|
import {FormIsLive} from '../../../components/form/is.live'
|
||||||
|
import Structure from '../../../components/structure'
|
||||||
|
import {TimeAgo} from '../../../components/time.ago'
|
||||||
|
import {withAuth} from '../../../components/with.auth'
|
||||||
|
import {
|
||||||
|
PAGER_FORM_QUERY,
|
||||||
|
PagerFormEntryQueryData,
|
||||||
|
PagerFormQueryData,
|
||||||
|
PagerFormQueryVariables
|
||||||
|
} from '../../../graphql/query/pager.form.query'
|
||||||
|
|
||||||
|
const Index: NextPage = () => {
|
||||||
|
const [pagination, setPagination] = useState<PaginationProps>({
|
||||||
|
pageSize: 25,
|
||||||
|
})
|
||||||
|
const [entries, setEntries] = useState<PagerFormEntryQueryData[]>()
|
||||||
|
// TODO limit forms if user is only admin!
|
||||||
|
const {loading, refetch} = useQuery<PagerFormQueryData, PagerFormQueryVariables>(PAGER_FORM_QUERY, {
|
||||||
|
variables: {
|
||||||
|
limit: pagination.pageSize,
|
||||||
|
start: pagination.current * pagination.pageSize || 0
|
||||||
|
},
|
||||||
|
onCompleted: ({pager}) => {
|
||||||
|
setPagination({
|
||||||
|
...pagination,
|
||||||
|
total: pager.total,
|
||||||
|
})
|
||||||
|
setEntries(pager.entries)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const deleteForm = async (form) => {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: 'Live',
|
||||||
|
dataIndex: 'isLive',
|
||||||
|
render: live => <FormIsLive isLive={live} />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Title',
|
||||||
|
dataIndex: 'title',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Owner',
|
||||||
|
dataIndex: 'admin',
|
||||||
|
render: user => (
|
||||||
|
<Link href={'/admin/users/[id]'} as={`/admin/users/${user.id}`}>
|
||||||
|
<Tooltip title={user.email}>
|
||||||
|
<Button type={'dashed'}>{user.username}</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Language',
|
||||||
|
dataIndex: 'language',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Created',
|
||||||
|
dataIndex: 'created',
|
||||||
|
render: date => <DateTime date={date} />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Last Change',
|
||||||
|
dataIndex: 'lastModified',
|
||||||
|
render: date => <TimeAgo date={date} />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
render: row => {
|
||||||
|
return (
|
||||||
|
<Space>
|
||||||
|
<Link
|
||||||
|
href={'/admin/forms/[id]'}
|
||||||
|
as={`/admin/forms/${row.id}`}
|
||||||
|
>
|
||||||
|
<Button type={'primary'}><EditOutlined /></Button>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Popconfirm
|
||||||
|
title="Are you sure delete this form?"
|
||||||
|
onConfirm={deleteForm}
|
||||||
|
okText={'Delete now!'}
|
||||||
|
>
|
||||||
|
<Button danger><DeleteOutlined /></Button>
|
||||||
|
</Popconfirm>
|
||||||
|
|
||||||
|
<Tooltip title={row.isLive ? null : 'Not Public accessible!'}>
|
||||||
|
<Button
|
||||||
|
href={`/form/${row.id}`}
|
||||||
|
target={'_blank'}
|
||||||
|
>
|
||||||
|
<GlobalOutlined />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</Space>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Structure
|
||||||
|
title={'Forms'}
|
||||||
|
selected={'forms'}
|
||||||
|
loading={loading}
|
||||||
|
breadcrumbs={[
|
||||||
|
{ href: '/admin', name: 'Home' },
|
||||||
|
]}
|
||||||
|
padded={false}
|
||||||
|
>
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
dataSource={entries}
|
||||||
|
rowKey={'id'}
|
||||||
|
pagination={pagination}
|
||||||
|
onChange={next => {
|
||||||
|
setPagination(pagination)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Structure>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withAuth(Index, ['admin'])
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
import {NextPage} from 'next'
|
||||||
|
import React from 'react'
|
||||||
|
import Structure from '../../components/structure'
|
||||||
|
import {withAuth} from '../../components/with.auth'
|
||||||
|
|
||||||
|
const Index: NextPage = () => {
|
||||||
|
return (
|
||||||
|
<Structure
|
||||||
|
title={'Home'}
|
||||||
|
>
|
||||||
|
ok!
|
||||||
|
</Structure>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withAuth(Index, ['admin'])
|
||||||
20
pages/admin/users/[id]/index.tsx
Normal file
20
pages/admin/users/[id]/index.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import {NextPage} from 'next'
|
||||||
|
import React from 'react'
|
||||||
|
import Structure from '../../../../components/structure'
|
||||||
|
import {withAuth} from '../../../../components/with.auth'
|
||||||
|
|
||||||
|
const Index: NextPage = () => {
|
||||||
|
return (
|
||||||
|
<Structure
|
||||||
|
title={'Edit User'}
|
||||||
|
breadcrumbs={[
|
||||||
|
{ href: '/admin', name: 'Home' },
|
||||||
|
{ href: '/admin/users', name: 'Users' },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
ok!
|
||||||
|
</Structure>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withAuth(Index, ['admin'])
|
||||||
19
pages/admin/users/index.tsx
Normal file
19
pages/admin/users/index.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import {NextPage} from 'next'
|
||||||
|
import React from 'react'
|
||||||
|
import Structure from '../../../components/structure'
|
||||||
|
import {withAuth} from '../../../components/with.auth'
|
||||||
|
|
||||||
|
const Index: NextPage = () => {
|
||||||
|
return (
|
||||||
|
<Structure
|
||||||
|
title={'Users'}
|
||||||
|
breadcrumbs={[
|
||||||
|
{ href: '/admin', name: 'Home' },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
ok!
|
||||||
|
</Structure>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withAuth(Index, ['admin'])
|
||||||
11
pages/form/[id]/index.tsx
Normal file
11
pages/form/[id]/index.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import {NextPage} from 'next'
|
||||||
|
import React from 'react'
|
||||||
|
import {ErrorPage} from '../../../components/error.page'
|
||||||
|
|
||||||
|
const Index: NextPage = () => {
|
||||||
|
return (
|
||||||
|
<ErrorPage />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Index
|
||||||
10
pages/form/index.tsx
Normal file
10
pages/form/index.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import {NextPage} from 'next'
|
||||||
|
import React from 'react'
|
||||||
|
import {ErrorPage} from '../../components/error.page'
|
||||||
|
|
||||||
|
const Index: NextPage = () => {
|
||||||
|
return (
|
||||||
|
<ErrorPage />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default Index
|
||||||
@ -1,18 +1,27 @@
|
|||||||
import {Alert} from 'antd'
|
import {Layout} from 'antd'
|
||||||
import {withAuth} from 'components/with.auth'
|
|
||||||
import {NextPage} from 'next'
|
import {NextPage} from 'next'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import Structure from '../components/structure'
|
import {AuthFooter} from '../components/auth/footer'
|
||||||
|
|
||||||
const Index: NextPage = () => {
|
const Index: NextPage = () => {
|
||||||
return (
|
return (
|
||||||
<Structure
|
<Layout style={{
|
||||||
selected={'home'}
|
height: '100vh',
|
||||||
title={'Home'}
|
background: '#437fdc'
|
||||||
>
|
}}>
|
||||||
<Alert message={"Hi"}/>
|
<img
|
||||||
</Structure>
|
style={{
|
||||||
|
margin: 'auto',
|
||||||
|
maxWidth: '90%',
|
||||||
|
width: 500,
|
||||||
|
textAlign: 'center',
|
||||||
|
}}
|
||||||
|
src={require('../assets/images/logo_white.png')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<AuthFooter />
|
||||||
|
</Layout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withAuth(Index)
|
export default Index
|
||||||
|
|||||||
26
pages/login/confirm/[code].tsx
Normal file
26
pages/login/confirm/[code].tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import {Alert} from 'antd'
|
||||||
|
import {NextPage} from 'next'
|
||||||
|
import React from 'react'
|
||||||
|
import {AuthFooter} from '../../../components/auth/footer'
|
||||||
|
import {AuthLayout} from '../../../components/auth/layout'
|
||||||
|
|
||||||
|
const Index: NextPage = () => {
|
||||||
|
return (
|
||||||
|
<AuthLayout>
|
||||||
|
<Alert
|
||||||
|
style={{
|
||||||
|
margin: 'auto',
|
||||||
|
maxWidth: '90%',
|
||||||
|
width: 500,
|
||||||
|
textAlign: 'center',
|
||||||
|
}}
|
||||||
|
type={'warning'}
|
||||||
|
message={'Work in Progress'}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<AuthFooter />
|
||||||
|
</AuthLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Index
|
||||||
132
pages/login/index.tsx
Normal file
132
pages/login/index.tsx
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
import {useMutation} from '@apollo/react-hooks'
|
||||||
|
import {Button, Form, Input, message} from 'antd'
|
||||||
|
import {useForm} from 'antd/lib/form/Form'
|
||||||
|
import {NextPage} from 'next'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import {useRouter} from 'next/router'
|
||||||
|
import React, {useState} from 'react'
|
||||||
|
import {AuthFooter} from '../../components/auth/footer'
|
||||||
|
import {AuthLayout} from '../../components/auth/layout'
|
||||||
|
import {setAuth} from '../../components/with.auth'
|
||||||
|
import {LOGIN_MUTATION} from '../../graphql/mutation/login.mutation'
|
||||||
|
|
||||||
|
const Index: NextPage = () => {
|
||||||
|
const [form] = useForm()
|
||||||
|
const router = useRouter()
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [login] = useMutation(LOGIN_MUTATION)
|
||||||
|
|
||||||
|
const finish = async (data) => {
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
const result = await login({
|
||||||
|
variables: data,
|
||||||
|
})
|
||||||
|
|
||||||
|
await setAuth(
|
||||||
|
result.data.tokens.access,
|
||||||
|
result.data.tokens.refresh
|
||||||
|
)
|
||||||
|
|
||||||
|
message.success('Welcome back!')
|
||||||
|
|
||||||
|
router.push('/admin')
|
||||||
|
} catch (e) {
|
||||||
|
message.error('username / password are invalid')
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const failed = () => {
|
||||||
|
message.error('mandatory fields missing')
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthLayout loading={loading}>
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
name="login"
|
||||||
|
onFinish={finish}
|
||||||
|
onFinishFailed={failed}
|
||||||
|
style={{
|
||||||
|
margin: 'auto',
|
||||||
|
maxWidth: '95%',
|
||||||
|
width: 400,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={require('../../assets/images/logo_white_small.png')}
|
||||||
|
alt={'OhMyForm'}
|
||||||
|
style={{
|
||||||
|
display: 'block',
|
||||||
|
width: '70%',
|
||||||
|
marginLeft: 'auto',
|
||||||
|
marginRight: 'auto',
|
||||||
|
marginBottom: 16,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="username"
|
||||||
|
rules={[{ required: true, message: 'Please input your username!' }]}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
size="large"
|
||||||
|
placeholder="Username"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="password"
|
||||||
|
rules={[{ required: true, message: 'Please input your password!' }]}
|
||||||
|
>
|
||||||
|
<Input.Password
|
||||||
|
size="large"
|
||||||
|
placeholder={'Password'}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item>
|
||||||
|
<Button
|
||||||
|
size="large"
|
||||||
|
type="primary"
|
||||||
|
htmlType="submit"
|
||||||
|
block
|
||||||
|
>
|
||||||
|
Login Now
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Button.Group
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
justifyContent: 'center',
|
||||||
|
display: 'flex',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Link href={'/register'}>
|
||||||
|
<Button
|
||||||
|
type={'link'}
|
||||||
|
ghost
|
||||||
|
>
|
||||||
|
Create Account
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<Link href={'/login/recover'}>
|
||||||
|
<Button
|
||||||
|
type={'link'}
|
||||||
|
ghost
|
||||||
|
>
|
||||||
|
Lost password
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</Button.Group>
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
<AuthFooter />
|
||||||
|
</AuthLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Index
|
||||||
26
pages/login/recover.tsx
Normal file
26
pages/login/recover.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import {Alert} from 'antd'
|
||||||
|
import {NextPage} from 'next'
|
||||||
|
import React from 'react'
|
||||||
|
import {AuthFooter} from '../../components/auth/footer'
|
||||||
|
import {AuthLayout} from '../../components/auth/layout'
|
||||||
|
|
||||||
|
const Recover: NextPage = () => {
|
||||||
|
return (
|
||||||
|
<AuthLayout>
|
||||||
|
<Alert
|
||||||
|
style={{
|
||||||
|
margin: 'auto',
|
||||||
|
maxWidth: '90%',
|
||||||
|
width: 500,
|
||||||
|
textAlign: 'center',
|
||||||
|
}}
|
||||||
|
type={'warning'}
|
||||||
|
message={'Work in Progress'}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<AuthFooter />
|
||||||
|
</AuthLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Recover
|
||||||
143
pages/register.tsx
Normal file
143
pages/register.tsx
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
import {useMutation} from '@apollo/react-hooks'
|
||||||
|
import {Button, Form, Input, message} from 'antd'
|
||||||
|
import {useForm} from 'antd/lib/form/Form'
|
||||||
|
import {NextPage} from 'next'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import {useRouter} from 'next/router'
|
||||||
|
import React, {useState} from 'react'
|
||||||
|
import {AuthFooter} from '../components/auth/footer'
|
||||||
|
import {AuthLayout} from '../components/auth/layout'
|
||||||
|
import {setAuth} from '../components/with.auth'
|
||||||
|
import {REGISTER_MUTATION} from '../graphql/mutation/register.mutation'
|
||||||
|
|
||||||
|
const Register: NextPage = () => {
|
||||||
|
const [form] = useForm()
|
||||||
|
const router = useRouter()
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
const [register] = useMutation(REGISTER_MUTATION)
|
||||||
|
|
||||||
|
const finish = async (data) => {
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await register({
|
||||||
|
variables: {
|
||||||
|
user: data
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await setAuth(
|
||||||
|
result.data.tokens.access,
|
||||||
|
result.data.tokens.refresh
|
||||||
|
)
|
||||||
|
|
||||||
|
message.success('Welcome, please also confirm your email')
|
||||||
|
|
||||||
|
router.push('/')
|
||||||
|
} catch (e) {
|
||||||
|
message.error('Some data already in use!')
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const failed = () => {
|
||||||
|
message.error('mandatory fields missing')
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthLayout loading={loading}>
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
name="login"
|
||||||
|
onFinish={finish}
|
||||||
|
onFinishFailed={failed}
|
||||||
|
style={{
|
||||||
|
margin: 'auto',
|
||||||
|
maxWidth: '95%',
|
||||||
|
width: 400,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={require('../assets/images/logo_white_small.png')}
|
||||||
|
alt={'OhMyForm'}
|
||||||
|
style={{
|
||||||
|
display: 'block',
|
||||||
|
width: '70%',
|
||||||
|
marginLeft: 'auto',
|
||||||
|
marginRight: 'auto',
|
||||||
|
marginBottom: 16,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="username"
|
||||||
|
rules={[{ required: true, message: 'Please input your username!' }]}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
size="large"
|
||||||
|
placeholder="Username"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="email"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: 'Please input your email!' },
|
||||||
|
{ type: 'email', message: 'Must be a valid email!' }
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
size="large"
|
||||||
|
placeholder="Email"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="password"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: 'Please input your password!' },
|
||||||
|
{ min: 5, message: 'Must be longer than or equal to 5 characters!' },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input.Password
|
||||||
|
size="large"
|
||||||
|
placeholder={'Password'}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item>
|
||||||
|
<Button
|
||||||
|
size="large"
|
||||||
|
type="primary"
|
||||||
|
htmlType="submit"
|
||||||
|
block
|
||||||
|
>
|
||||||
|
Register Now
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Button.Group
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
justifyContent: 'center',
|
||||||
|
display: 'flex',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Link href={'/login'}>
|
||||||
|
<Button
|
||||||
|
type={'link'}
|
||||||
|
ghost
|
||||||
|
>
|
||||||
|
Have an account? Go to login
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</Button.Group>
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
<AuthFooter />
|
||||||
|
</AuthLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Register
|
||||||
22
store/auth/index.ts
Normal file
22
store/auth/index.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import redux, {Reducer} from 'redux'
|
||||||
|
|
||||||
|
export interface AuthState {
|
||||||
|
authenticated?: boolean
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActionTypes = 'AUTH_INIT' | 'AUTH_LOGOUT' | 'AUTH_UPDATE_SETTINGS';
|
||||||
|
type Action = redux.Action<ActionTypes> & redux.AnyAction
|
||||||
|
|
||||||
|
export const actionTypes: {[key: string]: ActionTypes} = {
|
||||||
|
INIT: 'AUTH_INIT',
|
||||||
|
LOGOUT: 'AUTH_LOGOUT',
|
||||||
|
UPDATE_SETTINGS: 'AUTH_UPDATE_SETTINGS',
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialState: AuthState = {
|
||||||
|
}
|
||||||
|
|
||||||
|
export const auth: Reducer<AuthState, Action> = (state = initialState, action: Action): AuthState => {
|
||||||
|
return state
|
||||||
|
}
|
||||||
@ -2,31 +2,30 @@ import {createWrapper, HYDRATE, MakeStore} from 'next-redux-wrapper'
|
|||||||
import {AnyAction, applyMiddleware, combineReducers, createStore} from 'redux'
|
import {AnyAction, applyMiddleware, combineReducers, createStore} from 'redux'
|
||||||
import {composeWithDevTools} from 'redux-devtools-extension'
|
import {composeWithDevTools} from 'redux-devtools-extension'
|
||||||
import thunkMiddleware from 'redux-thunk'
|
import thunkMiddleware from 'redux-thunk'
|
||||||
|
import {auth, AuthState} from './auth'
|
||||||
|
|
||||||
|
|
||||||
export interface State {
|
export interface State {
|
||||||
|
auth: AuthState
|
||||||
}
|
}
|
||||||
|
|
||||||
const state = {}
|
|
||||||
|
|
||||||
const root = (state: State, action: AnyAction) => {
|
const root = (state: State, action: AnyAction) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case HYDRATE:
|
case HYDRATE:
|
||||||
return {...state, ...action.payload};
|
return {...state, ...action.payload};
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const combined = combineReducers({
|
||||||
|
auth
|
||||||
|
})
|
||||||
|
|
||||||
|
return combined(state, action);
|
||||||
};
|
};
|
||||||
|
|
||||||
const makeStore: MakeStore<State> = (context) => {
|
const makeStore: MakeStore<State> = (context) => {
|
||||||
return createStore(
|
return createStore(
|
||||||
(state, action): State => {
|
root,
|
||||||
const simple = combineReducers({
|
undefined,
|
||||||
// TODO add child reducers
|
|
||||||
})
|
|
||||||
|
|
||||||
return root(simple, action)
|
|
||||||
},
|
|
||||||
{},
|
|
||||||
composeWithDevTools(applyMiddleware(
|
composeWithDevTools(applyMiddleware(
|
||||||
thunkMiddleware,
|
thunkMiddleware,
|
||||||
))
|
))
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user