update form handling

This commit is contained in:
Michael Schramm 2020-05-29 16:27:45 +02:00
parent ac03ca3250
commit ec0f6e9572
56 changed files with 2281 additions and 71 deletions

View File

@ -22,3 +22,7 @@
color: #1890ff;
}
}
.ant-spin-nested-loading > div > .ant-spin {
max-height: unset;
}

View 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'
}}
>
&copy; OhMyForm
</Button>
</div>
)
}

View 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
View 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
View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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'
}} />
)
}

View 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',
},
},
}}
/>
)
}

View 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>
)
}

View File

@ -15,26 +15,33 @@ export const sideMenu: SideMenuElement[] = [
{
key: 'home',
name: 'Home',
href: '/',
href: '/admin',
icon: <HomeOutlined />,
},
{
key: 'communication',
name: 'Communication',
key: 'public',
name: 'Forms',
group: true,
items: [
{
key: 'forms',
name: 'Forms',
href: '/admin/forms',
icon: <MessageOutlined />,
},
]
},
{
key: 'administration',
name: 'Administration',
group: true,
items: [
{
key: 'users',
name: 'Users',
href: '/users',
href: '/admin/users',
icon: <TeamOutlined />,
},
{
key: 'chats',
name: 'Chats',
href: '/chats',
icon: <MessageOutlined />,
},
],
},
]

View File

@ -139,9 +139,7 @@ const Structure: FunctionComponent<Props> = (props) => {
onClick: () => setSidebar(!sidebar),
})}
<img src={require('assets/images/logo_white_small.png')} height={30} style={{marginRight: 16}} alt={'RecTag'} />
{publicRuntimeConfig.area.toUpperCase()}
<img src={require('assets/images/logo_white_small.png')} height={30} style={{marginRight: 16}} alt={'OhMyForm'} />
</div>
<div style={{float: 'right', display: 'flex', height: '100%'}}>
<Dropdown

View File

@ -1,9 +1,14 @@
import {Spin} from 'antd'
import {useQuery} from '@apollo/react-hooks'
import {AxiosRequestConfig} from 'axios'
import getConfig from 'next/config'
import React from 'react'
import {useRouter} from 'next/router'
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> => {
if (!config.headers) {
@ -11,7 +16,12 @@ export const authConfig = async (config: AxiosRequestConfig = {}): Promise<Axios
}
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) {
return config
}
@ -19,39 +29,47 @@ export const authConfig = async (config: AxiosRequestConfig = {}): Promise<Axios
return config
}
export const withAuth = (Component): React.FC => {
export const withAuth = (Component, roles: string[] = []): React.FC => {
return props => {
const [signedIn, setSignedIn] = React.useState(false);
const [loading, setLoading] = React.useState(true);
const router = useRouter()
const [access, setAccess] = useState(false)
const {loading, data, error} = useQuery<MeQueryData>(ME_QUERY)
useEffect(() => {
if (!error) {
return
}
React.useEffect(() => {
(async () => {
try {
} catch (err) {
console.error(err);
}
localStorage.clear()
const path = router.asPath || router.pathname
localStorage.setItem('redirect', path)
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) {
return (
<div style={{
height: '100vh',
justifyContent: 'center',
alignItems: 'center',
display: 'flex',
flexDirection: 'column',
}}>
<Spin size="large"/>
</div>
)
return <LoadingPage message={'Loading Credentials'} />
}
if (!signedIn) {
// TODO
if (!access) {
return <LoadingPage message={'Checking Credentials'} />
}
return <Component {...props} />

View 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
}
}
`

View 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}
`

View 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}
`

View 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
}
}
`

View 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
}
}
`

View 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
View 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
}
}
`

View 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
}
}
`

5
i18n.ts Normal file
View File

@ -0,0 +1,5 @@
export const languages = [
'de',
'en',
]

View File

@ -1,9 +1,10 @@
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({
publicRuntimeConfig: {
endpoint: process.env.API_HOST || '/graphql',
version,

View File

@ -3,20 +3,28 @@
"version": "0.1.0",
"license": "MIT",
"scripts": {
"dev": "next dev",
"start:dev": "next dev -p 4000",
"build": "next build",
"export": "next build && next export",
"start": "next start"
"start": "next start -p 4010",
"server": "node server.js"
},
"dependencies": {
"@ant-design/icons": "^4.1.0",
"@apollo/react-hooks": "^3.1.5",
"@lifeomic/axios-fetch": "^1.4.2",
"antd": "^4.2.2",
"apollo-boost": "^0.4.9",
"axios": "^0.19.2",
"dayjs": "^1.8.27",
"express": "^4.17.1",
"graphql": "^15.0.0",
"http-proxy-middleware": "^1.0.4",
"next": "9.4.0",
"next-images": "^1.4.0",
"next-redux-wrapper": "^6.0.0",
"react": "16.13.1",
"react-color": "^2.18.1",
"react-dom": "16.13.1",
"react-icons": "^3.10.0",
"react-redux": "^7.2.0",

View File

@ -1,23 +1,36 @@
import {ApolloProvider} from '@apollo/react-common'
import {buildAxiosFetch} from '@lifeomic/axios-fetch'
import 'antd/dist/antd.css'
import ApolloClient from 'apollo-boost'
import 'assets/global.scss'
import 'assets/variables.scss'
import axios from 'axios'
import {AppProps} from 'next/app'
import getConfig from 'next/config'
import Head from 'next/head'
import React from 'react'
import {wrapper} from 'store'
import {authConfig} from '../components/with.auth'
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 }) => {
return (
<>
<ApolloProvider client={client}>
<Head>
<title>OhMyForm</title>
<meta name="theme-color" content={'#4182e4'} />
</Head>
<Component {...pageProps} />
</>
</ApolloProvider>
)
}

View 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
View 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'])

View File

@ -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'])

View 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'])

View 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
View 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
View 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

View File

@ -1,18 +1,27 @@
import {Alert} from 'antd'
import {withAuth} from 'components/with.auth'
import {Layout} from 'antd'
import {NextPage} from 'next'
import React from 'react'
import Structure from '../components/structure'
import {AuthFooter} from '../components/auth/footer'
const Index: NextPage = () => {
return (
<Structure
selected={'home'}
title={'Home'}
>
<Alert message={"Hi"}/>
</Structure>
<Layout style={{
height: '100vh',
background: '#437fdc'
}}>
<img
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

View 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
View 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
View 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
View 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
View 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
}

View File

@ -2,31 +2,30 @@ import {createWrapper, HYDRATE, MakeStore} from 'next-redux-wrapper'
import {AnyAction, applyMiddleware, combineReducers, createStore} from 'redux'
import {composeWithDevTools} from 'redux-devtools-extension'
import thunkMiddleware from 'redux-thunk'
import {auth, AuthState} from './auth'
export interface State {
auth: AuthState
}
const state = {}
const root = (state: State, action: AnyAction) => {
switch (action.type) {
case HYDRATE:
return {...state, ...action.payload};
default:
return state;
}
const combined = combineReducers({
auth
})
return combined(state, action);
};
const makeStore: MakeStore<State> = (context) => {
return createStore(
(state, action): State => {
const simple = combineReducers({
// TODO add child reducers
})
return root(simple, action)
},
{},
root,
undefined,
composeWithDevTools(applyMiddleware(
thunkMiddleware,
))