This commit is contained in:
Michael Schramm 2020-05-22 21:18:48 +02:00
commit ac03ca3250
21 changed files with 685 additions and 0 deletions

31
.gitignore vendored Normal file
View File

@ -0,0 +1,31 @@
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local
# development environments
/.idea

30
README.md Normal file
View File

@ -0,0 +1,30 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/zeit/next.js/tree/canary/packages/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/zeit/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/import?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

24
assets/global.scss Normal file
View File

@ -0,0 +1,24 @@
@import "variables";
:root {
--backgroundColor: #{$background-color};
--primaryColor: #{$primary-color};
--textColorSecondary: #{$text-color-secondary};
--amplify-primary-color: #{$primary-color};
--amplify-primary-tint: #{lighten($primary-color, 0.1)};
--amplify-primary-shade: #{$primary-color};
}
.sidebar-toggle {
font-size: 18px;
line-height: 64px;
padding: 0 24px;
cursor: pointer;
transition: color 0.3s;
color: #FFF;
&:hover {
color: #1890ff;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

3
assets/variables.scss Normal file
View File

@ -0,0 +1,3 @@
$background-color: #f7f7f7;
$primary-color: #4182e4;
$text-color-secondary: rgba(0, 0, 0, 0.45);

20
components/date.time.tsx Normal file
View File

@ -0,0 +1,20 @@
import dayjs from 'dayjs'
import React from 'react'
interface Props {
date: string
hideTime?: boolean
}
export const DateTime: React.FC<Props> = props => {
const format = props.hideTime ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm'
return (
<div style={{
display: 'inline-block'
}}>
{dayjs(props.date).format(format)}
</div>
)
}

40
components/sidemenu.tsx Normal file
View File

@ -0,0 +1,40 @@
import {HomeOutlined, MessageOutlined, TeamOutlined} from '@ant-design/icons'
import React from 'react'
export interface SideMenuElement {
items?: SideMenuElement[]
key: string
name: string
group?: boolean
href?: string
icon?: any
}
export const sideMenu: SideMenuElement[] = [
{
key: 'home',
name: 'Home',
href: '/',
icon: <HomeOutlined />,
},
{
key: 'communication',
name: 'Communication',
group: true,
items: [
{
key: 'users',
name: 'Users',
href: '/users',
icon: <TeamOutlined />,
},
{
key: 'chats',
name: 'Chats',
href: '/chats',
icon: <MessageOutlined />,
},
],
},
]

270
components/structure.tsx Normal file
View File

@ -0,0 +1,270 @@
import {CaretDownOutlined, UserOutlined} from '@ant-design/icons'
import {MenuFoldOutlined, MenuUnfoldOutlined} from '@ant-design/icons/lib'
import {Dropdown, Layout, Menu, PageHeader, Spin, Tag} from 'antd'
import getConfig from 'next/config'
import Link from 'next/link'
import {useRouter} from 'next/router'
import React, {FunctionComponent} from 'react'
import {sideMenu, SideMenuElement} from './sidemenu'
import {useWindowSize} from './use.window.size'
const { publicRuntimeConfig } = getConfig()
const { SubMenu, ItemGroup } = Menu
const { Header, Content, Sider } = Layout
interface BreadcrumbEntry {
name: string
href?: string
as?: string
}
interface Props {
loading?: boolean
padded?: boolean
style?: any
selected?: string
breadcrumbs?: BreadcrumbEntry[]
title?: string
subTitle?: string
extra?: any[]
}
const Structure: FunctionComponent<Props> = (props) => {
const size = useWindowSize()
const [userMenu, setUserMenu] = React.useState(false)
const [open, setOpen] = React.useState<string[]>()
const [selected, setSelected] = React.useState<string[]>()
const [sidebar, setSidebar] = React.useState(size.width < 700)
const router = useRouter()
React.useEffect(() => {
if (sidebar !== size.width < 700) {
setSidebar(size.width < 700)
}
}, [size.width])
React.useEffect(() => {
if (props.selected) {
const parts = props.selected.split('.')
const last = parts.pop()
if (parts.length > 0) {
setOpen(parts)
}
setSelected([last])
}
}, [props.selected])
const buildMenu = (data: SideMenuElement[]): JSX.Element[] => {
return data.map((element): JSX.Element => {
if (element.items && element.items.length > 0) {
if (element.group) {
return (
<ItemGroup
key={element.key}
title={(
<div style={{
textTransform: 'uppercase',
paddingTop: 16,
fontWeight: 'bold',
color: '#444'
}}>
{element.icon}
{element.name}
</div>
)}
>
{buildMenu(element.items)}
</ItemGroup>
)
}
return (
<SubMenu
key={element.key}
title={
<span>
{element.icon}
{element.name}
</span>
}
>
{buildMenu(element.items)}
</SubMenu>
)
}
return (
<Menu.Item
onClick={(): void => {
if (element.href) {
router.push(element.href)
}
}}
key={element.key}
>
{element.icon}
{element.name}
</Menu.Item>
)
})
}
const signOut = async (): Promise<void> => {
// TODO sign out
}
return (
<Layout style={{ height: '100vh' }}>
<Header
style={{
paddingLeft: 0,
}}
>
<div style={{
float: 'left',
color: '#FFF',
fontSize: 14,
marginRight: 26,
fontWeight: 'bold'
}}>
{React.createElement(sidebar ? MenuUnfoldOutlined : MenuFoldOutlined, {
className: 'sidebar-toggle',
onClick: () => setSidebar(!sidebar),
})}
<img src={require('assets/images/logo_white_small.png')} height={30} style={{marginRight: 16}} alt={'RecTag'} />
{publicRuntimeConfig.area.toUpperCase()}
</div>
<div style={{float: 'right', display: 'flex', height: '100%'}}>
<Dropdown
overlay={(
<Menu>
<Menu.Item onClick={(): void => console.log('profile??')}>Profile</Menu.Item>
<Menu.Divider/>
<Menu.Item onClick={signOut}>Logout</Menu.Item>
</Menu>
)}
onVisibleChange={setUserMenu}
visible={userMenu}
>
<a style={{
color: '#FFF',
alignItems: 'center',
display: 'inline-flex',
}}>
<UserOutlined style={{fontSize: 24}} />
<CaretDownOutlined />
</a>
</Dropdown>
</div>
</Header>
<Layout style={{
height: '100%',
}}>
<Sider
collapsed={sidebar}
trigger={null}
collapsedWidth={0}
breakpoint={'xs'}
width={200}
style={{
background: '#fff',
maxHeight: '100%',
overflow: 'auto',
}}
>
<Menu
mode="inline"
defaultSelectedKeys={['1']}
selectedKeys={selected}
onSelect={(s): void => setSelected(s.keyPath)}
openKeys={open}
onOpenChange={(open): void => setOpen(open)}
>
{buildMenu(sideMenu)}
</Menu>
<Menu
mode="inline"
selectable={false}
>
<Menu.Item
style={{
marginTop: 40,
}}
>
Version: <Tag color="gold">{publicRuntimeConfig.version}</Tag>
</Menu.Item>
</Menu>
</Sider>
<Layout style={{ padding: '0 24px 24px' }}>
{props.title && (
<PageHeader
title={props.title}
subTitle={props.subTitle}
extra={props.extra}
breadcrumb={{
routes: [
...(props.breadcrumbs || []).map(b => ({
breadcrumbName: b.name,
path: ''
})),
{
breadcrumbName: props.title,
path: ''
}
],
params: props.breadcrumbs,
itemRender: (route, params: BreadcrumbEntry[], routes, paths) => {
if (routes.indexOf(route) === routes.length - 1) {
return <span>{route.breadcrumbName}</span>
}
const entry = params[routes.indexOf(route)]
return (
<Link
href={entry.href}
as={entry.as || entry.href}
>
<a>
{entry.name}
</a>
</Link>
)
}}}
/>
)}
<Spin
spinning={!!props.loading}
>
<Content
style={{
background: props.padded ? '#fff' : null,
padding: props.padded ? 24 : 0,
...props.style
}}
>
{props.children}
</Content>
</Spin>
</Layout>
</Layout>
</Layout>
)
}
Structure.defaultProps = {
padded: true,
style: {},
}
export default Structure

23
components/time.ago.tsx Normal file
View File

@ -0,0 +1,23 @@
import {Tooltip} from 'antd'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import React from 'react'
dayjs.extend(relativeTime)
interface Props {
date: string
}
export const TimeAgo: React.FC<Props> = props => {
const date = dayjs(props.date)
return (
<Tooltip title={date.format('YYYY-MM-DD HH:mm:ss')}>
<div style={{
display: 'inline-block'
}}>
{date.fromNow()}
</div>
</Tooltip>
)
}

View File

@ -0,0 +1,29 @@
import {useEffect, useState} from 'react'
export const useWindowSize = () => {
const isClient = typeof window === 'object';
function getSize() {
return {
width: isClient ? window.innerWidth : undefined,
height: isClient ? window.innerHeight : undefined
};
}
const [windowSize, setWindowSize] = useState(getSize);
useEffect(() => {
if (!isClient) {
return;
}
function handleResize() {
setWindowSize(getSize());
}
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []); // Empty array ensures that effect is only run on mount and unmount
return windowSize;
}

59
components/with.auth.tsx Normal file
View File

@ -0,0 +1,59 @@
import {Spin} from 'antd'
import {AxiosRequestConfig} from 'axios'
import getConfig from 'next/config'
import React from 'react'
const { publicRuntimeConfig } = getConfig()
export const authConfig = async (config: AxiosRequestConfig = {}): Promise<AxiosRequestConfig> => {
if (!config.headers) {
config.headers = {}
}
try {
// TODO config.headers.Authorization = `Bearer ${session.getAccessToken().getJwtToken()}`
} catch (e) {
return config
}
return config
}
export const withAuth = (Component): React.FC => {
return props => {
const [signedIn, setSignedIn] = React.useState(false);
const [loading, setLoading] = React.useState(true);
React.useEffect(() => {
(async () => {
try {
} catch (err) {
console.error(err);
}
setLoading(false)
})();
}, []);
if (loading) {
return (
<div style={{
height: '100vh',
justifyContent: 'center',
alignItems: 'center',
display: 'flex',
flexDirection: 'column',
}}>
<Spin size="large"/>
</div>
)
}
if (!signedIn) {
// TODO
}
return <Component {...props} />
};
}

2
next-env.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
/// <reference types="next" />
/// <reference types="next/types/global" />

11
next.config.js Normal file
View File

@ -0,0 +1,11 @@
const withImages = require('next-images')
const module = require('./package.json')
const version = module.version;
module.exports = withImages({
publicRuntimeConfig: {
endpoint: process.env.API_HOST || '/graphql',
version,
}
})

33
package.json Normal file
View File

@ -0,0 +1,33 @@
{
"name": "ohmyform-react",
"version": "0.1.0",
"license": "MIT",
"scripts": {
"dev": "next dev",
"build": "next build",
"export": "next build && next export",
"start": "next start"
},
"dependencies": {
"@ant-design/icons": "^4.1.0",
"antd": "^4.2.2",
"axios": "^0.19.2",
"dayjs": "^1.8.27",
"next": "9.4.0",
"next-images": "^1.4.0",
"next-redux-wrapper": "^6.0.0",
"react": "16.13.1",
"react-dom": "16.13.1",
"react-icons": "^3.10.0",
"react-redux": "^7.2.0",
"redux": "^4.0.5",
"redux-devtools-extension": "^2.13.8",
"redux-thunk": "^2.3.0",
"sass": "^1.26.5"
},
"devDependencies": {
"@types/node": "^14.0.1",
"@types/react": "^16.9.35",
"typescript": "^3.9.2"
}
}

25
pages/_app.tsx Normal file
View File

@ -0,0 +1,25 @@
import 'antd/dist/antd.css'
import 'assets/global.scss'
import 'assets/variables.scss'
import {AppProps} from 'next/app'
import getConfig from 'next/config'
import Head from 'next/head'
import React from 'react'
import {wrapper} from 'store'
const { publicRuntimeConfig } = getConfig()
const App: React.FC<AppProps> = ({ Component, pageProps }) => {
return (
<>
<Head>
<title>OhMyForm</title>
<meta name="theme-color" content={'#4182e4'} />
</Head>
<Component {...pageProps} />
</>
)
}
export default wrapper.withRedux(App)
// export default App

0
pages/admin/index.tsx Normal file
View File

18
pages/index.tsx Normal file
View File

@ -0,0 +1,18 @@
import {Alert} from 'antd'
import {withAuth} from 'components/with.auth'
import {NextPage} from 'next'
import React from 'react'
import Structure from '../components/structure'
const Index: NextPage = () => {
return (
<Structure
selected={'home'}
title={'Home'}
>
<Alert message={"Hi"}/>
</Structure>
)
}
export default withAuth(Index)

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

37
store/index.ts Normal file
View File

@ -0,0 +1,37 @@
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'
export interface State {
}
const state = {}
const root = (state: State, action: AnyAction) => {
switch (action.type) {
case HYDRATE:
return {...state, ...action.payload};
default:
return state;
}
};
const makeStore: MakeStore<State> = (context) => {
return createStore(
(state, action): State => {
const simple = combineReducers({
// TODO add child reducers
})
return root(simple, action)
},
{},
composeWithDevTools(applyMiddleware(
thunkMiddleware,
))
)
}
export const wrapper = createWrapper<State>(makeStore, {debug: true});

30
tsconfig.json Normal file
View File

@ -0,0 +1,30 @@
{
"compilerOptions": {
"baseUrl": ".",
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve"
},
"exclude": [
"node_modules"
],
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx"
]
}