(feat): admin layout
This commit is contained in:
parent
cf8e29caf0
commit
b4683bb70e
@ -4,6 +4,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bootstrap": "5.1.3",
|
"bootstrap": "5.1.3",
|
||||||
|
"classnames": "^2.3.1",
|
||||||
"history": "5.3.0",
|
"history": "5.3.0",
|
||||||
"merge": "^2.1.1",
|
"merge": "^2.1.1",
|
||||||
"react": "18.0.0",
|
"react": "18.0.0",
|
||||||
|
|||||||
@ -3,48 +3,50 @@ import { Route, Routes } from 'react-router'
|
|||||||
|
|
||||||
//Redux
|
//Redux
|
||||||
import { useSelector, useDispatch } from 'react-redux'
|
import { useSelector, useDispatch } from 'react-redux'
|
||||||
import { actionCreators as settingsActionCreators, ISettingsState, IRoute } from './store/reducers/Settings'
|
import { actionCreators as settingsActionCreators } from './store/reducers/Settings'
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import { DynamicLayout } from './layouts'
|
import { DynamicLayout } from './layouts'
|
||||||
import { DynamicPage } from './pages'
|
import { DynamicPage } from './pages'
|
||||||
|
import { IReduxState, IRoute } from './interfaces'
|
||||||
interface IRouteProp {
|
interface IRouteProp {
|
||||||
path: string,
|
path: string,
|
||||||
element?: JSX.Element
|
element?: JSX.Element
|
||||||
}
|
}
|
||||||
|
|
||||||
const NestedRoutes = (routes: IRoute[], tag: string) => {
|
const NestedRoutes = (routes: IRoute[], tag: string | undefined = undefined) => {
|
||||||
|
|
||||||
|
if(!Array.isArray(routes)) return
|
||||||
|
|
||||||
return routes.map((route: IRoute, index: number) => {
|
return routes.map((route: IRoute, index: number) => {
|
||||||
const props: IRouteProp = {
|
const routeProps: IRouteProp = {
|
||||||
path: route.path
|
path: route.path
|
||||||
}
|
}
|
||||||
|
|
||||||
if (route.component) {
|
if (route.component) {
|
||||||
const page = <DynamicPage tag={route.component} />
|
const page = <DynamicPage tag={route.component} />
|
||||||
props.element = tag ? <DynamicLayout tag={tag}>{page}</DynamicLayout> : page
|
routeProps.element = tag ? <DynamicLayout tag={tag}>{page}</DynamicLayout> : page
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Route key={index} { ...props }>{route.childRoutes ? NestedRoutes(route.childRoutes, tag) : ''}</Route>
|
return <Route key={index} { ...routeProps }>{Array.isArray(route.childRoutes) ? NestedRoutes(route.childRoutes, tag) : ''}</Route>
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IReduxState {
|
|
||||||
settings: ISettingsState
|
|
||||||
}
|
|
||||||
|
|
||||||
const App: FC = () => {
|
const App: FC = () => {
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const { routes } = useSelector((state: IReduxState) => state.settings)
|
const { routes, serviceRoutes } = useSelector((state: IReduxState) => state.settings)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(settingsActionCreators.requestSettings())
|
dispatch(settingsActionCreators.requestSettings())
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
{routes.length > 0 ? <Routes>
|
<Routes>
|
||||||
{NestedRoutes(routes, 'PublicLayout')}
|
{NestedRoutes(routes, 'AdminLayout')}
|
||||||
</Routes> : ''}
|
{NestedRoutes(serviceRoutes)}
|
||||||
|
</Routes>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
46
clientapp/src/components/FeatherIcons/FeatherIcon.tsx
Normal file
46
clientapp/src/components/FeatherIcons/FeatherIcon.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import React, { FC } from 'react'
|
||||||
|
import { IconInner } from './IconInner'
|
||||||
|
|
||||||
|
interface IFeatherIcon {
|
||||||
|
icon: string,
|
||||||
|
size?: string | number,
|
||||||
|
className?: string,
|
||||||
|
fill?: string,
|
||||||
|
otherProps?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Feather icon
|
||||||
|
* otherProps spread will be removed in version 1.
|
||||||
|
* @param {icon} icon name that matches from feathericons
|
||||||
|
* @returns FeatherIcon react component
|
||||||
|
*/
|
||||||
|
const FeatherIcon : FC<IFeatherIcon> = (props : IFeatherIcon) => {
|
||||||
|
|
||||||
|
const { icon, size = 24, className = '', fill = 'none', otherProps } = props
|
||||||
|
|
||||||
|
if (!icon) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill={fill}
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
className={`feather feather-${icon} ${className}`}
|
||||||
|
{...otherProps}
|
||||||
|
>
|
||||||
|
<IconInner icon={icon} />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
FeatherIcon
|
||||||
|
}
|
||||||
37
clientapp/src/components/FeatherIcons/IconInner.tsx
Normal file
37
clientapp/src/components/FeatherIcons/IconInner.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import React, { FC } from 'react'
|
||||||
|
// for now this icons json is generated via the build script from latest feather
|
||||||
|
// TODO: automatically generate this JSON via this repo's build script
|
||||||
|
import icons from './icons.json'
|
||||||
|
|
||||||
|
const createMarkup = (markup: string) => {
|
||||||
|
// we dont sanitize markup
|
||||||
|
// since icons.json is maintained within the package before build
|
||||||
|
return { __html: markup }
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IIconInner {
|
||||||
|
icon: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const IconInner : FC<IIconInner> = (props : IIconInner) => {
|
||||||
|
const { icon } = props
|
||||||
|
|
||||||
|
|
||||||
|
// icons are based on generated icons.json from feather lib
|
||||||
|
interface IIcons {
|
||||||
|
[key: string]: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const iconMarkup = (icons as IIcons)[icon]
|
||||||
|
|
||||||
|
if (iconMarkup) {
|
||||||
|
// i didnt want to use dangerouslySetInnerHTML
|
||||||
|
// but this way I can just use the JSON to spit out SVG.
|
||||||
|
return <g dangerouslySetInnerHTML={createMarkup(iconMarkup)} />
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
IconInner
|
||||||
|
}
|
||||||
1
clientapp/src/components/FeatherIcons/icons.json
Normal file
1
clientapp/src/components/FeatherIcons/icons.json
Normal file
File diff suppressed because one or more lines are too long
1
clientapp/src/components/FeatherIcons/index.tsx
Normal file
1
clientapp/src/components/FeatherIcons/index.tsx
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { FeatherIcon } from './FeatherIcon'
|
||||||
23
clientapp/src/interfaces/index.ts
Normal file
23
clientapp/src/interfaces/index.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { ISettingsState } from "../store/reducers/Settings"
|
||||||
|
|
||||||
|
export interface IReduxState {
|
||||||
|
settings: ISettingsState
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRoute {
|
||||||
|
path: string,
|
||||||
|
component?: string,
|
||||||
|
childRoutes?: IRoute[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISubMenuItem {
|
||||||
|
icon?: string,
|
||||||
|
title: string,
|
||||||
|
target?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IMenuItem extends ISubMenuItem {
|
||||||
|
items?: ISubMenuItem []
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
56
clientapp/src/layouts/admin/NavMenu/index.tsx
Normal file
56
clientapp/src/layouts/admin/NavMenu/index.tsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import React, { FC, useState } from 'react'
|
||||||
|
import { Button, Collapse, Navbar, NavbarBrand, NavbarToggler, NavItem, NavLink } from 'reactstrap'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import { FeatherIcon } from '../../../components/FeatherIcons'
|
||||||
|
import { IMenuItem, IReduxState } from '../../../interfaces'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
|
||||||
|
interface INavMenu {
|
||||||
|
toggleSidebar: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const NavMenu : FC<INavMenu> = (props: INavMenu) => {
|
||||||
|
let { topMenu = [] } = useSelector((state: IReduxState) => state.settings)
|
||||||
|
|
||||||
|
const { toggleSidebar } = props
|
||||||
|
|
||||||
|
const [state, hookState] = useState({
|
||||||
|
isOpen: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const toggle = () => {
|
||||||
|
hookState({
|
||||||
|
isOpen: !state.isOpen
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Navbar className="navbar-expand-sm navbar-toggleable-sm fixed-top border-bottom box-shadow mb-3" light>
|
||||||
|
|
||||||
|
<Button color="light" onClick={toggleSidebar}>
|
||||||
|
<FeatherIcon icon="align-left" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<NavbarBrand href="/">reactstrap</ NavbarBrand>
|
||||||
|
|
||||||
|
<NavbarToggler onClick={toggle} className="mr-2"/>
|
||||||
|
|
||||||
|
<Collapse className="d-sm-inline-flex flex-sm-row-reverse" isOpen={state.isOpen} navbar>
|
||||||
|
<ul className="navbar-nav flex-grow">
|
||||||
|
{topMenu.map((item: IMenuItem, index: number) => {
|
||||||
|
return <NavItem key={index}>
|
||||||
|
<NavLink tag={Link} className="text-dark" to={item.target}>
|
||||||
|
{item.icon ? <FeatherIcon icon={item.icon}/> : ''}
|
||||||
|
{item.title}
|
||||||
|
</NavLink>
|
||||||
|
</NavItem>
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</Collapse>
|
||||||
|
|
||||||
|
|
||||||
|
</Navbar>
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
NavMenu
|
||||||
|
}
|
||||||
69
clientapp/src/layouts/admin/SideMenu/index.tsx
Normal file
69
clientapp/src/layouts/admin/SideMenu/index.tsx
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import React, { FC, useState } from 'react'
|
||||||
|
|
||||||
|
import classNames from 'classnames'
|
||||||
|
import { Collapse, Nav, NavItem, NavLink } from 'reactstrap'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
|
||||||
|
import { FeatherIcon } from '../../../components/FeatherIcons'
|
||||||
|
import { IMenuItem, IReduxState, ISubMenuItem } from '../../../interfaces'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
|
||||||
|
|
||||||
|
interface ISubMenu {
|
||||||
|
icon?: string,
|
||||||
|
title: string,
|
||||||
|
items: ISubMenuItem []
|
||||||
|
}
|
||||||
|
|
||||||
|
const SubMenu : FC<ISubMenu> = (props: ISubMenu) => {
|
||||||
|
const { icon, title, items } = props
|
||||||
|
|
||||||
|
const [collapsed, setCollapsed] = useState(true)
|
||||||
|
const toggle = () => setCollapsed(!collapsed)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<NavItem onClick={toggle} className={classNames({ "menu-open": !collapsed })}>
|
||||||
|
<NavLink className="dropdown-toggle">
|
||||||
|
{icon ? <FeatherIcon icon={icon}/> : ''}
|
||||||
|
<span className="link-title">{title}</span>
|
||||||
|
</NavLink>
|
||||||
|
</NavItem>
|
||||||
|
<Collapse isOpen={!collapsed} navbar className={classNames("items-menu", { "mb-1": !collapsed })}>
|
||||||
|
{items.map((item: ISubMenuItem, index: number) => (
|
||||||
|
<NavItem key={index} className="pl-4">
|
||||||
|
<NavLink tag={Link} to={item.target}>
|
||||||
|
{item.icon ? <FeatherIcon icon={item.icon}/> : ''}
|
||||||
|
<span className="link-title">{item.title}</span>
|
||||||
|
</NavLink>
|
||||||
|
</NavItem>
|
||||||
|
))}
|
||||||
|
</Collapse>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const SideMenu : FC = () => {
|
||||||
|
let { sideMenu = [] } = useSelector((state: IReduxState) => state.settings)
|
||||||
|
|
||||||
|
return <div className="side-menu">
|
||||||
|
<Nav vertical className="list-unstyled pb-3">
|
||||||
|
{sideMenu.map((item: IMenuItem, index: number) => {
|
||||||
|
if(item.items) {
|
||||||
|
return <SubMenu key={index} icon={item.icon} title={item.title} items={item.items} />
|
||||||
|
}
|
||||||
|
|
||||||
|
return <NavItem key={index}>
|
||||||
|
<NavLink tag={Link} to={item.target}>
|
||||||
|
{item.icon ? <FeatherIcon icon={item.icon}/> : ''}
|
||||||
|
<span className="link-title">{item.title}</span>
|
||||||
|
</NavLink>
|
||||||
|
</NavItem>
|
||||||
|
})}
|
||||||
|
</Nav>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
SideMenu
|
||||||
|
}
|
||||||
@ -1,15 +1,34 @@
|
|||||||
import React, { FC } from 'react'
|
import React, { FC, useState } from 'react'
|
||||||
|
import classNames from 'classnames'
|
||||||
import { Container } from 'reactstrap'
|
import { Container } from 'reactstrap'
|
||||||
|
|
||||||
|
import { NavMenu } from './NavMenu'
|
||||||
|
import { SideMenu } from './SideMenu'
|
||||||
|
|
||||||
import { ILayout } from '../interfaces'
|
import { ILayout } from '../interfaces'
|
||||||
|
|
||||||
import 'bootstrap/dist/css/bootstrap.min.css'
|
import 'bootstrap/dist/css/bootstrap.min.css'
|
||||||
|
import './scss/style.scss'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const AdminLayout: FC<ILayout> = ({ children = null }) => {
|
const AdminLayout: FC<ILayout> = ({ children = null }) => {
|
||||||
|
|
||||||
|
const [sidebarIsOpen, setSidebar] = useState(true)
|
||||||
|
const toggleSidebar = () => setSidebar(!sidebarIsOpen)
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<Container>
|
<NavMenu toggleSidebar={toggleSidebar} />
|
||||||
{children}
|
<div className="wrapper">
|
||||||
</Container>
|
<div className={classNames("sidebar", { "is-open": sidebarIsOpen })}>
|
||||||
|
<SideMenu />
|
||||||
|
</div>
|
||||||
|
<Container fluid className={classNames("content", { "is-open": sidebarIsOpen }) }>
|
||||||
|
{children}
|
||||||
|
</Container>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
170
clientapp/src/layouts/admin/scss/style.scss
Normal file
170
clientapp/src/layouts/admin/scss/style.scss
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
//colors
|
||||||
|
$color_science_blue_approx: #0366d6;
|
||||||
|
$color_cerise_approx: #e01a76;
|
||||||
|
$white: #fff;
|
||||||
|
$color_denim_approx: #1b6ec2;
|
||||||
|
$color_fun_blue_approx: #1861ac;
|
||||||
|
$black_5: rgba(0, 0, 0, .05);
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: $color_science_blue_approx;
|
||||||
|
&.navbar-brand {
|
||||||
|
white-space: normal;
|
||||||
|
text-align: center;
|
||||||
|
//Instead of the line below you could use @include word-break($value)
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
color: $color_cerise_approx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
color: $white;
|
||||||
|
background-color: $color_denim_approx;
|
||||||
|
border-color: $color_fun_blue_approx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-shadow {
|
||||||
|
//Instead of the line below you could use @include box-shadow($shadow-1, $shadow-2, $shadow-3, $shadow-4, $shadow-5, $shadow-6, $shadow-7, $shadow-8, $shadow-9, $shadow-10)
|
||||||
|
box-shadow: 0 .25rem .75rem $black_5;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media(min-width: 768px) {
|
||||||
|
html {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
padding-top: 57px;
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
position: fixed;
|
||||||
|
|
||||||
|
background: #7386d5;
|
||||||
|
color: #fff;
|
||||||
|
|
||||||
|
min-width: 250px;
|
||||||
|
max-width: 250px;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
margin-left: -250px;
|
||||||
|
transition: all 0.5s;
|
||||||
|
&.is-open {
|
||||||
|
margin-left: 0;
|
||||||
|
transition: 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
a,
|
||||||
|
a:hover,
|
||||||
|
a:focus {
|
||||||
|
cursor: pointer;
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
.link-title {
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-menu {
|
||||||
|
|
||||||
|
.menu-title {
|
||||||
|
color: #fff;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
&:hover {
|
||||||
|
color: #7386d5;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-toggle {
|
||||||
|
&::after {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Collabsable menu nav item */
|
||||||
|
.menu-open {
|
||||||
|
background: #6d7fcc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Collapsable menu items container */
|
||||||
|
.items-menu {
|
||||||
|
color: #fff;
|
||||||
|
background: #6d7fcc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
transition: all 0.5s;
|
||||||
|
|
||||||
|
&.is-open {
|
||||||
|
padding-left: 255px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@media(max-width: 768px) {
|
||||||
|
.wrapper {
|
||||||
|
padding-top: 51px;
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
&.is-open {
|
||||||
|
min-width: 60px;
|
||||||
|
max-width: 60px;
|
||||||
|
|
||||||
|
margin-left: 0;
|
||||||
|
transition: all 0.5s, height 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-menu {
|
||||||
|
.menu-title {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
.link-title {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.dropdown-toggle {
|
||||||
|
&::after {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
transition: all 0.5s;
|
||||||
|
|
||||||
|
&.is-open {
|
||||||
|
padding-left: 65px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,10 +1,10 @@
|
|||||||
import React, { FC } from 'react'
|
import React, { FC } from 'react'
|
||||||
|
|
||||||
import { ILayout } from './interfaces'
|
|
||||||
|
|
||||||
import { PublicLayout } from './public'
|
import { PublicLayout } from './public'
|
||||||
import { AdminLayout } from './admin'
|
import { AdminLayout } from './admin'
|
||||||
|
|
||||||
|
import { ILayout } from './interfaces'
|
||||||
|
|
||||||
interface ILayouts {
|
interface ILayouts {
|
||||||
[key: string]: React.FC<ILayout>;
|
[key: string]: React.FC<ILayout>;
|
||||||
}
|
}
|
||||||
@ -14,7 +14,7 @@ const layouts: ILayouts = {
|
|||||||
AdminLayout
|
AdminLayout
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IDynamicLayout {
|
interface IDynamicLayout {
|
||||||
tag: string,
|
tag: string,
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import { ISettingsState } from "../store/reducers/Settings"
|
||||||
|
|
||||||
export interface ILayout {
|
export interface ILayout {
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
}
|
}
|
||||||
@ -15,7 +15,7 @@ const NavMenu = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return <header>
|
return <header>
|
||||||
<Navbar className="navbar-expand-sm navbar-toggleable-sm border-bottom box-shadow mb-3" light>
|
<Navbar className="navbar-expand-sm navbar-toggleable-sm fixed-top border-bottom box-shadow mb-3" light>
|
||||||
|
|
||||||
<NavbarBrand tag={Link} to="/">react-redux-template</NavbarBrand>
|
<NavbarBrand tag={Link} to="/">react-redux-template</NavbarBrand>
|
||||||
<NavbarToggler onClick={toggle} className="mr-2"/>
|
<NavbarToggler onClick={toggle} className="mr-2"/>
|
||||||
|
|||||||
70
clientapp/src/pages/Signin/index.tsx
Normal file
70
clientapp/src/pages/Signin/index.tsx
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import React, { useState } from "react"
|
||||||
|
import { Link } from "react-router-dom"
|
||||||
|
import { Button, Container, Form, FormGroup, Input, Label } from "reactstrap"
|
||||||
|
|
||||||
|
import './scss/style.scss'
|
||||||
|
|
||||||
|
interface IStateProp {
|
||||||
|
[key: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState extends IStateProp {
|
||||||
|
username: string,
|
||||||
|
password: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const Signin = () => {
|
||||||
|
|
||||||
|
const [state, hookState] = useState<IState>({
|
||||||
|
username: '',
|
||||||
|
password: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const setState = (props: IStateProp) => {
|
||||||
|
const newState = { ...state }
|
||||||
|
Object.keys(props).forEach(key => newState[key] = props[key])
|
||||||
|
hookState(newState)
|
||||||
|
return newState
|
||||||
|
}
|
||||||
|
|
||||||
|
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const { name, value } = e.target
|
||||||
|
setState({ [name]: value })
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Container className="container">
|
||||||
|
<h2>Sign In</h2>
|
||||||
|
<Form className="form">
|
||||||
|
<FormGroup>
|
||||||
|
<Label for="username">Username</Label>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
name="username"
|
||||||
|
id="username"
|
||||||
|
placeholder="username or email..."
|
||||||
|
value={state.username}
|
||||||
|
onChange={onChange} />
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup>
|
||||||
|
<Label for="password">Password</Label>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
id="password"
|
||||||
|
placeholder="password..."
|
||||||
|
value={state.password}
|
||||||
|
onChange={onChange} />
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup>
|
||||||
|
Dont have an account yet? Please <Link to="/signup">Signup</Link>.
|
||||||
|
</FormGroup>
|
||||||
|
<Button>Submit</Button>
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
|
||||||
|
</Container>
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Signin
|
||||||
|
}
|
||||||
19
clientapp/src/pages/Signin/scss/style.scss
Normal file
19
clientapp/src/pages/Signin/scss/style.scss
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
.container {
|
||||||
|
border: 2px solid #d3d3d3;
|
||||||
|
border-radius: .5em;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
margin-top: 100px;
|
||||||
|
padding: 1em;
|
||||||
|
text-align: left;
|
||||||
|
width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
117
clientapp/src/pages/Signup/index.tsx
Normal file
117
clientapp/src/pages/Signup/index.tsx
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import React, { useState } from "react"
|
||||||
|
import { Button, Container, Form, FormGroup, Input, Label } from "reactstrap"
|
||||||
|
|
||||||
|
import './scss/style.scss'
|
||||||
|
|
||||||
|
interface IStateProp {
|
||||||
|
[key: string]: string | boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState extends IStateProp {
|
||||||
|
username: string,
|
||||||
|
|
||||||
|
email: string,
|
||||||
|
reEmail: string,
|
||||||
|
|
||||||
|
password: string,
|
||||||
|
rePassword: string,
|
||||||
|
|
||||||
|
tnc: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const Signup = () => {
|
||||||
|
|
||||||
|
const [state, hookState] = useState<IState>({
|
||||||
|
username: '',
|
||||||
|
|
||||||
|
email: '',
|
||||||
|
reEmail : '',
|
||||||
|
|
||||||
|
password: '',
|
||||||
|
rePassword: '',
|
||||||
|
|
||||||
|
tnc: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const setState = (props: IStateProp) => {
|
||||||
|
const newState = { ...state }
|
||||||
|
Object.keys(props).forEach(key => newState[key] = props[key])
|
||||||
|
hookState(newState)
|
||||||
|
return newState
|
||||||
|
}
|
||||||
|
|
||||||
|
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const { name, value } = e.target
|
||||||
|
setState({ [name]: value })
|
||||||
|
}
|
||||||
|
|
||||||
|
const onTogle = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const { name } = e.target
|
||||||
|
setState({ [name]: !state[name] })
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Container className="container">
|
||||||
|
<h2>Sign Up</h2>
|
||||||
|
<Form className="form">
|
||||||
|
<FormGroup>
|
||||||
|
<Label for="username">Username</Label>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
name="username"
|
||||||
|
id="username"
|
||||||
|
placeholder="username..."
|
||||||
|
value={state.username}
|
||||||
|
onChange={onChange} />
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup>
|
||||||
|
<Label for="email">Email</Label>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
name="email"
|
||||||
|
id="email"
|
||||||
|
placeholder="email..."
|
||||||
|
value={state.email}
|
||||||
|
onChange={onChange} />
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup>
|
||||||
|
<Label for="reEmail">Repeat email</Label>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
name="reEmail"
|
||||||
|
id="reEmail"
|
||||||
|
placeholder="repeat email..."
|
||||||
|
value={state.reEmail}
|
||||||
|
onChange={onChange} />
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup>
|
||||||
|
<Label for="password">Password</Label>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
id="password"
|
||||||
|
placeholder="password..."
|
||||||
|
value={state.password}
|
||||||
|
onChange={onChange} />
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup>
|
||||||
|
<Label for="rePassword">Repeat password</Label>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
name="rePassword"
|
||||||
|
id="rePassword"
|
||||||
|
placeholder="repeat password..."
|
||||||
|
value={state.rePassword}
|
||||||
|
onChange={onChange} />
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup check>
|
||||||
|
<Input type="checkbox" name="tnc" id="tnc" checked={state.tnc} onChange={onTogle}/>
|
||||||
|
<Label check>Accept Terms And Conditions</Label>
|
||||||
|
</FormGroup>
|
||||||
|
<Button>Submit</Button>
|
||||||
|
</Form>
|
||||||
|
</Container>
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Signup
|
||||||
|
}
|
||||||
19
clientapp/src/pages/Signup/scss/style.scss
Normal file
19
clientapp/src/pages/Signup/scss/style.scss
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
.container {
|
||||||
|
border: 2px solid #d3d3d3;
|
||||||
|
border-radius: .5em;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
margin-top: 100px;
|
||||||
|
padding: 1em;
|
||||||
|
text-align: left;
|
||||||
|
width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
@ -3,15 +3,19 @@ import React, { FC } from 'react'
|
|||||||
import { Home } from './Home'
|
import { Home } from './Home'
|
||||||
import { Counter } from './Counter'
|
import { Counter } from './Counter'
|
||||||
import { FetchData } from './FetchData'
|
import { FetchData } from './FetchData'
|
||||||
|
import { Signin } from './Signin'
|
||||||
|
import { Signup } from './Signup'
|
||||||
|
|
||||||
interface IPages {
|
interface IPages {
|
||||||
[key: string]: React.FC<any>;
|
[key: string]: React.FC<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pages: IPages = {
|
const pages: IPages = {
|
||||||
Home: Home,
|
Home,
|
||||||
Counter: Counter,
|
Counter,
|
||||||
FetchData: FetchData
|
FetchData,
|
||||||
|
Signin,
|
||||||
|
Signup
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IDynamicPage {
|
export interface IDynamicPage {
|
||||||
|
|||||||
@ -1,14 +1,12 @@
|
|||||||
import { Action, Reducer } from 'redux'
|
import { Action, Reducer } from 'redux'
|
||||||
import { AppThunkAction } from '../'
|
import { AppThunkAction } from '../'
|
||||||
|
import { IMenuItem, IRoute } from '../../interfaces'
|
||||||
export interface IRoute {
|
|
||||||
path: string,
|
|
||||||
component?: string,
|
|
||||||
childRoutes?: IRoute[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ISettingsState {
|
export interface ISettingsState {
|
||||||
routes: IRoute[],
|
routes: IRoute [],
|
||||||
|
serviceRoutes: IRoute [],
|
||||||
|
sideMenu?: IMenuItem [],
|
||||||
|
topMenu?: IMenuItem [],
|
||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -18,38 +16,137 @@ interface RequestSettingsAction {
|
|||||||
|
|
||||||
interface ReceiveSettingsAction {
|
interface ReceiveSettingsAction {
|
||||||
type: 'RECEIVE_SETTINGS',
|
type: 'RECEIVE_SETTINGS',
|
||||||
routes: IRoute[]
|
routes: IRoute [],
|
||||||
|
serviceRoutes: IRoute [],
|
||||||
|
sideMenu?: IMenuItem [],
|
||||||
|
topMenu?: IMenuItem [],
|
||||||
}
|
}
|
||||||
|
|
||||||
type KnownAction = RequestSettingsAction | ReceiveSettingsAction;
|
type KnownAction = RequestSettingsAction | ReceiveSettingsAction;
|
||||||
|
|
||||||
export const actionCreators = {
|
export const actionCreators = {
|
||||||
requestSettings: (): AppThunkAction<KnownAction> => (dispatch, getState) => {
|
requestSettings: (): AppThunkAction<KnownAction> => (dispatch, getState) => {
|
||||||
const appState = getState()
|
|
||||||
console.log(appState)
|
dispatch({ type: 'REQUEST_SETTINGS' })
|
||||||
|
|
||||||
const routes = [
|
const appState = getState()
|
||||||
|
|
||||||
|
const routes : IRoute[] = [
|
||||||
{ path: "/", component: "Home" },
|
{ path: "/", component: "Home" },
|
||||||
|
{ path: "/home", component: "Home" },
|
||||||
{ path: "/counter", component: "Counter" },
|
{ path: "/counter", component: "Counter" },
|
||||||
{ path: "/fetch-data",
|
{ path: "/fetch-data", component: "FetchData",
|
||||||
component: "FetchData",
|
|
||||||
childRoutes: [
|
childRoutes: [
|
||||||
{
|
{
|
||||||
path: ":startDateIndex",
|
path: ":startDateIndex",
|
||||||
component: "FetchData"
|
component: "FetchData"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
const serviceRoutes : IRoute[] = [
|
||||||
|
{ path: "/signin", component: "Signin" },
|
||||||
|
{ path: "/signup", component: "Signup" }
|
||||||
|
]
|
||||||
|
|
||||||
|
const sideMenu : IMenuItem [] = [
|
||||||
|
{
|
||||||
|
icon: "activity",
|
||||||
|
title: "Home",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
icon: "activity",
|
||||||
|
title: "Home 1",
|
||||||
|
target: "/Home-1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "activity",
|
||||||
|
title: "Home 2",
|
||||||
|
target: "/Home-2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "activity",
|
||||||
|
title: "Home 3",
|
||||||
|
target: "/Home-3",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
icon: "info",
|
||||||
|
title: "About",
|
||||||
|
target: "/about"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
icon: "activity",
|
||||||
|
title: "Page",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
icon: "activity",
|
||||||
|
title: "Page 1",
|
||||||
|
target: "/Page-1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "activity",
|
||||||
|
title: "Page 2",
|
||||||
|
target: "/Page-2",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
icon: "alert-triangle",
|
||||||
|
title: "Faq",
|
||||||
|
target: "/faq"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
icon: "phone-call",
|
||||||
|
title: "Contact",
|
||||||
|
target: "/contact"
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const topMenu : IMenuItem [] = [
|
||||||
|
{
|
||||||
|
icon: "",
|
||||||
|
title: "Home",
|
||||||
|
target: "/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "",
|
||||||
|
title: "Counter",
|
||||||
|
target: "/counter",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "",
|
||||||
|
title: "Fetch data",
|
||||||
|
target: "fetch-data"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "",
|
||||||
|
title: "Signin",
|
||||||
|
target: "/signin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "",
|
||||||
|
title: "Signout",
|
||||||
|
target: "/signout"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
dispatch({ type: 'RECEIVE_SETTINGS', routes })
|
dispatch({ type: 'RECEIVE_SETTINGS', routes, serviceRoutes, sideMenu, topMenu })
|
||||||
|
|
||||||
dispatch({ type: 'REQUEST_SETTINGS' })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const unloadedState: ISettingsState = {
|
const unloadedState: ISettingsState = {
|
||||||
routes: [],
|
routes: [],
|
||||||
|
serviceRoutes: [],
|
||||||
|
sideMenu: [],
|
||||||
|
topMenu: [],
|
||||||
isLoading: false
|
isLoading: false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,12 +160,18 @@ export const reducer: Reducer<ISettingsState> = (state: ISettingsState | undefin
|
|||||||
case 'REQUEST_SETTINGS':
|
case 'REQUEST_SETTINGS':
|
||||||
return {
|
return {
|
||||||
routes: state.routes,
|
routes: state.routes,
|
||||||
|
serviceRoutes: state.serviceRoutes,
|
||||||
|
sideMenu: state.sideMenu,
|
||||||
|
topMenu: state.topMenu,
|
||||||
isLoading: true
|
isLoading: true
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'RECEIVE_SETTINGS':
|
case 'RECEIVE_SETTINGS':
|
||||||
return {
|
return {
|
||||||
routes: action.routes,
|
routes: action.routes,
|
||||||
|
serviceRoutes: action.serviceRoutes,
|
||||||
|
sideMenu: action.sideMenu,
|
||||||
|
topMenu: action.topMenu,
|
||||||
isLoading: false
|
isLoading: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2820,7 +2820,7 @@ cjs-module-lexer@^1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz#9f84ba3244a512f3a54e5277e8eef4c489864e40"
|
resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz#9f84ba3244a512f3a54e5277e8eef4c489864e40"
|
||||||
integrity sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==
|
integrity sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==
|
||||||
|
|
||||||
classnames@^2.2.3:
|
classnames@^2.2.3, classnames@^2.3.1:
|
||||||
version "2.3.1"
|
version "2.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e"
|
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e"
|
||||||
integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==
|
integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user