mirror of
https://github.com/IT4Change/ohmyform-ui.git
synced 2025-12-13 01:35:51 +00:00
update form handling
This commit is contained in:
parent
ac03ca3250
commit
ec0f6e9572
@ -22,3 +22,7 @@
|
||||
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',
|
||||
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 />,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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} />
|
||||
|
||||
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 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,
|
||||
|
||||
12
package.json
12
package.json
@ -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",
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
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 {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
|
||||
|
||||
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 {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,
|
||||
))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user