diff --git a/assets/global.scss b/assets/global.scss
index 5bbb37b..5f2fadd 100644
--- a/assets/global.scss
+++ b/assets/global.scss
@@ -22,3 +22,7 @@
color: #1890ff;
}
}
+
+.ant-spin-nested-loading > div > .ant-spin {
+ max-height: unset;
+}
diff --git a/components/auth/footer.tsx b/components/auth/footer.tsx
new file mode 100644
index 0000000..febe80c
--- /dev/null
+++ b/components/auth/footer.tsx
@@ -0,0 +1,54 @@
+import {Button} from 'antd'
+import Link from 'next/link'
+import React from 'react'
+
+export const AuthFooter: React.FC = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/components/auth/layout.tsx b/components/auth/layout.tsx
new file mode 100644
index 0000000..ae9e3fa
--- /dev/null
+++ b/components/auth/layout.tsx
@@ -0,0 +1,19 @@
+import {Layout, Spin} from 'antd'
+import React from 'react'
+
+interface Props {
+ loading?: boolean
+}
+
+export const AuthLayout: React.FC = props => {
+ return (
+
+
+ {props.children}
+
+
+ )
+}
diff --git a/components/clean.input.ts b/components/clean.input.ts
new file mode 100644
index 0000000..40fc4d7
--- /dev/null
+++ b/components/clean.input.ts
@@ -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 = 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 = (obj: T): T => {
+ return omitDeep(obj, '__typename')
+}
diff --git a/components/error.page.tsx b/components/error.page.tsx
new file mode 100644
index 0000000..1fb4604
--- /dev/null
+++ b/components/error.page.tsx
@@ -0,0 +1,16 @@
+import React from 'react'
+
+export const ErrorPage: React.FC = () => {
+ return (
+
+
ERROR
+
there was an error with your request
+
+ )
+}
diff --git a/components/form/admin/base.data.tab.tsx b/components/form/admin/base.data.tab.tsx
new file mode 100644
index 0000000..c4c884e
--- /dev/null
+++ b/components/form/admin/base.data.tab.tsx
@@ -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 = props => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/components/form/admin/design.tab.tsx b/components/form/admin/design.tab.tsx
new file mode 100644
index 0000000..93ac569
--- /dev/null
+++ b/components/form/admin/design.tab.tsx
@@ -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 = props => {
+ return (
+
+
+
+
+
+ {[
+ {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}) => (
+
+
+
+ ))}
+
+ )
+}
diff --git a/components/form/admin/end.page.tab.tsx b/components/form/admin/end.page.tab.tsx
new file mode 100644
index 0000000..3e61806
--- /dev/null
+++ b/components/form/admin/end.page.tab.tsx
@@ -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 = props => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {(fields, { add, remove }) => {
+ return (
+
+ )
+ }}
+
+
+ )
+}
diff --git a/components/form/admin/field.card.tsx b/components/form/admin/field.card.tsx
new file mode 100644
index 0000000..fb81039
--- /dev/null
+++ b/components/form/admin/field.card.tsx
@@ -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 => {
+ const {
+ form,
+ field,
+ fields,
+ onChangeFields,
+ remove,
+ index,
+ } = props
+
+ const type = form.getFieldValue(['form', 'fields', field.name as string, 'type'])
+ const TypeComponent: React.FC = 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 (
+
+ {type}
+ {
+ remove(index)
+ onChangeFields(fields.filter((e, i) => i !== index))
+ }}
+ >
+
+
+
+ )}
+ actions={[
+ remove(index)} />
+ ]}
+ >
+
+
+ setNextTitle(e.target.value)}/>
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/components/form/admin/fields.tab.tsx b/components/form/admin/fields.tab.tsx
new file mode 100644
index 0000000..c218d35
--- /dev/null
+++ b/components/form/admin/fields.tab.tsx
@@ -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 => {
+ const [nextType, setNextType] = useState('textfield')
+
+ const renderType = useCallback((field, index, remove) => {
+ return (
+
+ )
+ }, [props.fields])
+
+ const addField = useCallback((add, index) => {
+ return (
+
+
+
+
+
+
+ )
+ }, [props.fields, nextType])
+
+
+ return (
+
+
+
+ {(fields, { add, remove, move }) => {
+ const addAndMove = (index) => (defaults) => {
+ add(defaults)
+ move(fields.length, index)
+ }
+
+ return (
+
+ {addField(addAndMove(0), 0)}
+ {fields.map((field, index) => (
+
+
+ {renderType(field, index, remove)}
+
+ {addField(addAndMove(index + 1), index + 1)}
+
+ ))}
+
+ )
+ }}
+
+
+
+ )
+}
diff --git a/components/form/admin/respondent.notifications.tab.tsx b/components/form/admin/respondent.notifications.tab.tsx
new file mode 100644
index 0000000..fd15dab
--- /dev/null
+++ b/components/form/admin/respondent.notifications.tab.tsx
@@ -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 => {
+ const [enabled, setEnabled] = useState()
+
+ 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 (
+
+
+ setEnabled(e.valueOf())} />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/components/form/admin/self.notifications.tab.tsx b/components/form/admin/self.notifications.tab.tsx
new file mode 100644
index 0000000..f625d12
--- /dev/null
+++ b/components/form/admin/self.notifications.tab.tsx
@@ -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 => {
+ const [enabled, setEnabled] = useState()
+
+ 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 (
+
+
+ setEnabled(e.valueOf())} />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/components/form/admin/start.page.tab.tsx b/components/form/admin/start.page.tab.tsx
new file mode 100644
index 0000000..981e076
--- /dev/null
+++ b/components/form/admin/start.page.tab.tsx
@@ -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 = props => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {(fields, { add, remove }) => {
+ return (
+
+ )
+ }}
+
+
+ )
+}
diff --git a/components/form/admin/types/date.type.tsx b/components/form/admin/types/date.type.tsx
new file mode 100644
index 0000000..99630b4
--- /dev/null
+++ b/components/form/admin/types/date.type.tsx
@@ -0,0 +1,34 @@
+import {Form, Input} from 'antd'
+import React from 'react'
+
+interface Props {
+ field: any
+}
+
+export const DateType: React.FC = props => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/components/form/admin/types/dropdown.type.tsx b/components/form/admin/types/dropdown.type.tsx
new file mode 100644
index 0000000..660a6ae
--- /dev/null
+++ b/components/form/admin/types/dropdown.type.tsx
@@ -0,0 +1,21 @@
+import {Form, Input} from 'antd'
+import React from 'react'
+
+interface Props {
+ field: any
+}
+
+export const DropdownType: React.FC = props => {
+ // TODO add dropdown options
+ return (
+
+
+
+
+
+ )
+}
diff --git a/components/form/admin/types/email.type.tsx b/components/form/admin/types/email.type.tsx
new file mode 100644
index 0000000..76d623d
--- /dev/null
+++ b/components/form/admin/types/email.type.tsx
@@ -0,0 +1,23 @@
+import {Form, Input} from 'antd'
+import React from 'react'
+
+interface Props {
+ field: any
+}
+
+export const EmailType: React.FC = props => {
+ return (
+
+
+
+
+
+ )
+}
diff --git a/components/form/admin/types/hidden.type.tsx b/components/form/admin/types/hidden.type.tsx
new file mode 100644
index 0000000..f7d16ab
--- /dev/null
+++ b/components/form/admin/types/hidden.type.tsx
@@ -0,0 +1,20 @@
+import {Form, Input} from 'antd'
+import React from 'react'
+
+interface Props {
+ field: any
+}
+
+export const HiddenType: React.FC = props => {
+ return (
+
+
+
+
+
+ )
+}
diff --git a/components/form/admin/types/link.type.tsx b/components/form/admin/types/link.type.tsx
new file mode 100644
index 0000000..4f2570f
--- /dev/null
+++ b/components/form/admin/types/link.type.tsx
@@ -0,0 +1,23 @@
+import {Form, Input} from 'antd'
+import React from 'react'
+
+interface Props {
+ field: any
+}
+
+export const LinkType: React.FC = props => {
+ return (
+
+
+
+
+
+ )
+}
diff --git a/components/form/admin/types/number.type.tsx b/components/form/admin/types/number.type.tsx
new file mode 100644
index 0000000..2019b98
--- /dev/null
+++ b/components/form/admin/types/number.type.tsx
@@ -0,0 +1,20 @@
+import {Form, InputNumber} from 'antd'
+import React from 'react'
+
+interface Props {
+ field: any
+}
+
+export const NumberType: React.FC = props => {
+ return (
+
+
+
+
+
+ )
+}
diff --git a/components/form/admin/types/radio.type.tsx b/components/form/admin/types/radio.type.tsx
new file mode 100644
index 0000000..6e0da7e
--- /dev/null
+++ b/components/form/admin/types/radio.type.tsx
@@ -0,0 +1,22 @@
+import {Form, Input} from 'antd'
+import React from 'react'
+
+interface Props {
+ field: any
+}
+
+export const RadioType: React.FC = props => {
+ // TODO Add radio support
+
+ return (
+
+
+
+
+
+ )
+}
diff --git a/components/form/admin/types/rating.type.tsx b/components/form/admin/types/rating.type.tsx
new file mode 100644
index 0000000..5131d8f
--- /dev/null
+++ b/components/form/admin/types/rating.type.tsx
@@ -0,0 +1,22 @@
+import {Form, Input} from 'antd'
+import React from 'react'
+
+interface Props {
+ field: any
+}
+
+export const RatingType: React.FC = props => {
+ // TODO add ratings
+
+ return (
+
+
+
+
+
+ )
+}
diff --git a/components/form/admin/types/text.type.tsx b/components/form/admin/types/text.type.tsx
new file mode 100644
index 0000000..9a41b65
--- /dev/null
+++ b/components/form/admin/types/text.type.tsx
@@ -0,0 +1,20 @@
+import {Form, Input} from 'antd'
+import React from 'react'
+
+interface Props {
+ field: any
+}
+
+export const TextType: React.FC = props => {
+ return (
+
+
+
+
+
+ )
+}
diff --git a/components/form/admin/types/textarea.type.tsx b/components/form/admin/types/textarea.type.tsx
new file mode 100644
index 0000000..e4ef04a
--- /dev/null
+++ b/components/form/admin/types/textarea.type.tsx
@@ -0,0 +1,20 @@
+import {Form, Input} from 'antd'
+import React from 'react'
+
+interface Props {
+ field: any
+}
+
+export const TextareaType: React.FC = props => {
+ return (
+
+
+
+
+
+ )
+}
diff --git a/components/form/admin/types/yes_no.type.tsx b/components/form/admin/types/yes_no.type.tsx
new file mode 100644
index 0000000..4dc6436
--- /dev/null
+++ b/components/form/admin/types/yes_no.type.tsx
@@ -0,0 +1,21 @@
+import {Form, Input} from 'antd'
+import React from 'react'
+
+interface Props {
+ field: any
+}
+
+export const YesNoType: React.FC = props => {
+ // TODO add switch
+ return (
+
+
+
+
+
+ )
+}
diff --git a/components/form/is.live.tsx b/components/form/is.live.tsx
new file mode 100644
index 0000000..51aebd7
--- /dev/null
+++ b/components/form/is.live.tsx
@@ -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 => {
+ if (props.isLive) {
+ return (
+
+ )
+ }
+
+ return (
+
+ )
+}
diff --git a/components/input/color.tsx b/components/input/color.tsx
new file mode 100644
index 0000000..a909be4
--- /dev/null
+++ b/components/input/color.tsx
@@ -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 => {
+ useEffect(() => {
+ if (!props.value) {
+ props.onChange('#FFF')
+ }
+ }, [props.value])
+
+ return (
+ props.onChange(e.hex)}
+ styles={{
+ default: {
+ card: {
+ flexDirection: 'row',
+ display: 'flex',
+ boxShadow: 'none'
+ },
+ head: {
+ flex: 1,
+ borderRadius: 6,
+ height: 'auto',
+ },
+ },
+ }}
+ />
+ )
+}
diff --git a/components/loading.page.tsx b/components/loading.page.tsx
new file mode 100644
index 0000000..798121d
--- /dev/null
+++ b/components/loading.page.tsx
@@ -0,0 +1,21 @@
+import {Spin} from 'antd'
+import React from 'react'
+
+interface Props {
+ message?: string
+}
+
+export const LoadingPage: React.FC = props => {
+ return (
+
+
+ {props.message}
+
+ )
+}
diff --git a/components/sidemenu.tsx b/components/sidemenu.tsx
index 39588e0..d362f97 100644
--- a/components/sidemenu.tsx
+++ b/components/sidemenu.tsx
@@ -15,26 +15,33 @@ export const sideMenu: SideMenuElement[] = [
{
key: 'home',
name: 'Home',
- href: '/',
+ href: '/admin',
icon: ,
},
{
- key: 'communication',
- name: 'Communication',
+ key: 'public',
+ name: 'Forms',
+ group: true,
+ items: [
+ {
+ key: 'forms',
+ name: 'Forms',
+ href: '/admin/forms',
+ icon: ,
+ },
+ ]
+ },
+ {
+ key: 'administration',
+ name: 'Administration',
group: true,
items: [
{
key: 'users',
name: 'Users',
- href: '/users',
+ href: '/admin/users',
icon: ,
},
- {
- key: 'chats',
- name: 'Chats',
- href: '/chats',
- icon: ,
- },
],
},
]
diff --git a/components/structure.tsx b/components/structure.tsx
index 847144f..f8c13c7 100644
--- a/components/structure.tsx
+++ b/components/structure.tsx
@@ -139,9 +139,7 @@ const Structure: FunctionComponent = (props) => {
onClick: () => setSidebar(!sidebar),
})}
-
-
- {publicRuntimeConfig.area.toUpperCase()}
+
{
+ localStorage.setItem('access', access)
+ localStorage.setItem('refresh', refresh)
+}
export const authConfig = async (config: AxiosRequestConfig = {}): Promise => {
if (!config.headers) {
@@ -11,7 +16,12 @@ export const authConfig = async (config: AxiosRequestConfig = {}): Promise {
+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(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 (
-
-
-
- )
+ return
}
- if (!signedIn) {
- // TODO
+ if (!access) {
+ return
}
return
diff --git a/graphql/fragment/admin.form.fragment.ts b/graphql/fragment/admin.form.fragment.ts
new file mode 100644
index 0000000..70760ee
--- /dev/null
+++ b/graphql/fragment/admin.form.fragment.ts
@@ -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
+ }
+ }
+`
diff --git a/graphql/mutation/admin.form.create.mutation.ts b/graphql/mutation/admin.form.create.mutation.ts
new file mode 100644
index 0000000..15d43f6
--- /dev/null
+++ b/graphql/mutation/admin.form.create.mutation.ts
@@ -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}
+`
diff --git a/graphql/mutation/admin.form.update.mutation.ts b/graphql/mutation/admin.form.update.mutation.ts
new file mode 100644
index 0000000..a133140
--- /dev/null
+++ b/graphql/mutation/admin.form.update.mutation.ts
@@ -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}
+`
diff --git a/graphql/mutation/login.mutation.ts b/graphql/mutation/login.mutation.ts
new file mode 100644
index 0000000..182699f
--- /dev/null
+++ b/graphql/mutation/login.mutation.ts
@@ -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
+ }
+ }
+`
diff --git a/graphql/mutation/register.mutation.ts b/graphql/mutation/register.mutation.ts
new file mode 100644
index 0000000..a4b447e
--- /dev/null
+++ b/graphql/mutation/register.mutation.ts
@@ -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
+ }
+ }
+`
diff --git a/graphql/query/admin.form.query.ts b/graphql/query/admin.form.query.ts
new file mode 100644
index 0000000..ca713bc
--- /dev/null
+++ b/graphql/query/admin.form.query.ts
@@ -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}
+`
diff --git a/graphql/query/me.query.ts b/graphql/query/me.query.ts
new file mode 100644
index 0000000..cecc368
--- /dev/null
+++ b/graphql/query/me.query.ts
@@ -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
+ }
+ }
+`
diff --git a/graphql/query/pager.form.query.ts b/graphql/query/pager.form.query.ts
new file mode 100644
index 0000000..5de469d
--- /dev/null
+++ b/graphql/query/pager.form.query.ts
@@ -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
+ }
+ }
+`
diff --git a/i18n.ts b/i18n.ts
new file mode 100644
index 0000000..addd5bc
--- /dev/null
+++ b/i18n.ts
@@ -0,0 +1,5 @@
+
+export const languages = [
+ 'de',
+ 'en',
+]
diff --git a/next.config.js b/next.config.js
index 6240b81..50816b5 100644
--- a/next.config.js
+++ b/next.config.js
@@ -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,
diff --git a/package.json b/package.json
index 7509a69..3e9621a 100644
--- a/package.json
+++ b/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",
diff --git a/pages/_app.tsx b/pages/_app.tsx
index 40bf010..30d9d45 100644
--- a/pages/_app.tsx
+++ b/pages/_app.tsx
@@ -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 => {
+ operation.setContext(await authConfig())
+ }
+})
+
const App: React.FC = ({ Component, pageProps }) => {
return (
- <>
+
OhMyForm
- >
+
)
}
diff --git a/pages/admin/forms/[id]/index.tsx b/pages/admin/forms/[id]/index.tsx
new file mode 100644
index 0000000..ae9b7af
--- /dev/null
+++ b/pages/admin/forms/[id]/index.tsx
@@ -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([])
+ const [update] = useMutation(ADMIN_FORM_UPDATE_MUTATION)
+
+ const {data, loading, error} = useQuery(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 (
+
+ Save
+ ,
+ ]}
+ style={{paddingTop: 0}}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export default withAuth(Index, ['admin'])
diff --git a/pages/admin/forms/index.tsx b/pages/admin/forms/index.tsx
new file mode 100644
index 0000000..8b4c6a3
--- /dev/null
+++ b/pages/admin/forms/index.tsx
@@ -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({
+ pageSize: 25,
+ })
+ const [entries, setEntries] = useState()
+ // TODO limit forms if user is only admin!
+ const {loading, refetch} = useQuery(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 =>
+ },
+ {
+ title: 'Title',
+ dataIndex: 'title',
+ },
+ {
+ title: 'Owner',
+ dataIndex: 'admin',
+ render: user => (
+
+
+
+
+
+ )
+ },
+ {
+ title: 'Language',
+ dataIndex: 'language',
+ },
+ {
+ title: 'Created',
+ dataIndex: 'created',
+ render: date =>
+ },
+ {
+ title: 'Last Change',
+ dataIndex: 'lastModified',
+ render: date =>
+ },
+ {
+ render: row => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+ }
+ },
+ ]
+
+ return (
+
+ {
+ setPagination(pagination)
+ }}
+ />
+
+ )
+}
+
+export default withAuth(Index, ['admin'])
diff --git a/pages/admin/index.tsx b/pages/admin/index.tsx
index e69de29..d926f8e 100644
--- a/pages/admin/index.tsx
+++ b/pages/admin/index.tsx
@@ -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 (
+
+ ok!
+
+ )
+}
+
+export default withAuth(Index, ['admin'])
diff --git a/pages/admin/users/[id]/index.tsx b/pages/admin/users/[id]/index.tsx
new file mode 100644
index 0000000..87a2da0
--- /dev/null
+++ b/pages/admin/users/[id]/index.tsx
@@ -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 (
+
+ ok!
+
+ )
+}
+
+export default withAuth(Index, ['admin'])
diff --git a/pages/admin/users/index.tsx b/pages/admin/users/index.tsx
new file mode 100644
index 0000000..68e7d56
--- /dev/null
+++ b/pages/admin/users/index.tsx
@@ -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 (
+
+ ok!
+
+ )
+}
+
+export default withAuth(Index, ['admin'])
diff --git a/pages/form/[id]/index.tsx b/pages/form/[id]/index.tsx
new file mode 100644
index 0000000..ffcccd9
--- /dev/null
+++ b/pages/form/[id]/index.tsx
@@ -0,0 +1,11 @@
+import {NextPage} from 'next'
+import React from 'react'
+import {ErrorPage} from '../../../components/error.page'
+
+const Index: NextPage = () => {
+ return (
+
+ )
+}
+
+export default Index
diff --git a/pages/form/index.tsx b/pages/form/index.tsx
new file mode 100644
index 0000000..ec2d948
--- /dev/null
+++ b/pages/form/index.tsx
@@ -0,0 +1,10 @@
+import {NextPage} from 'next'
+import React from 'react'
+import {ErrorPage} from '../../components/error.page'
+
+const Index: NextPage = () => {
+ return (
+
+ )
+}
+export default Index
diff --git a/pages/index.tsx b/pages/index.tsx
index cf4dde6..b24eb9a 100644
--- a/pages/index.tsx
+++ b/pages/index.tsx
@@ -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 (
-
-
-
+
+
+
+
+
)
}
-export default withAuth(Index)
+export default Index
diff --git a/pages/login/confirm/[code].tsx b/pages/login/confirm/[code].tsx
new file mode 100644
index 0000000..8000ecf
--- /dev/null
+++ b/pages/login/confirm/[code].tsx
@@ -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 (
+
+
+
+
+
+ )
+}
+
+export default Index
diff --git a/pages/login/index.tsx b/pages/login/index.tsx
new file mode 100644
index 0000000..140005c
--- /dev/null
+++ b/pages/login/index.tsx
@@ -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 (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export default Index
diff --git a/pages/login/recover.tsx b/pages/login/recover.tsx
new file mode 100644
index 0000000..b413a95
--- /dev/null
+++ b/pages/login/recover.tsx
@@ -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 (
+
+
+
+
+
+ )
+}
+
+export default Recover
diff --git a/pages/register.tsx b/pages/register.tsx
new file mode 100644
index 0000000..4cd12ae
--- /dev/null
+++ b/pages/register.tsx
@@ -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 (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export default Register
diff --git a/store/auth/index.ts b/store/auth/index.ts
new file mode 100644
index 0000000..5b8aa2d
--- /dev/null
+++ b/store/auth/index.ts
@@ -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 & 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 = (state = initialState, action: Action): AuthState => {
+ return state
+}
diff --git a/store/index.ts b/store/index.ts
index 5dea0d4..adc72ea 100644
--- a/store/index.ts
+++ b/store/index.ts
@@ -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 = (context) => {
return createStore(
- (state, action): State => {
- const simple = combineReducers({
- // TODO add child reducers
- })
-
- return root(simple, action)
- },
- {},
+ root,
+ undefined,
composeWithDevTools(applyMiddleware(
thunkMiddleware,
))