├── .eslintrc.json ├── .github └── windmill-dashboard-thumbnail.jpg ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── context ├── SidebarContext.tsx └── ThemeContext.tsx ├── example ├── components │ ├── AccessibleNavigationAnnouncer.tsx │ ├── CTA.tsx │ ├── Cards │ │ └── InfoCard.tsx │ ├── Chart │ │ ├── ChartCard.tsx │ │ └── ChartLegend.tsx │ ├── Header.tsx │ ├── Loader │ │ ├── Loader.module.css │ │ └── Loader.tsx │ ├── RoundIcon.tsx │ ├── Sidebar │ │ ├── DesktopSidebar.tsx │ │ ├── MobileSidebar.tsx │ │ ├── SidebarContent.tsx │ │ ├── SidebarSubmenu.tsx │ │ └── index.tsx │ ├── ThemedSuspense.tsx │ └── Typography │ │ ├── PageTitle.tsx │ │ └── SectionTitle.tsx └── containers │ ├── Layout.tsx │ └── Main.tsx ├── icons ├── bell.svg ├── buttons.svg ├── cards.svg ├── cart.svg ├── charts.svg ├── chat.svg ├── dropdown.svg ├── edit.svg ├── forbidden.svg ├── forms.svg ├── github.svg ├── heart.svg ├── home.svg ├── index.ts ├── mail.svg ├── menu.svg ├── modals.svg ├── money.svg ├── moon.svg ├── outlineCog.svg ├── outlineLogout.svg ├── outlinePerson.svg ├── pages.svg ├── people.svg ├── search.svg ├── sun.svg ├── tables.svg ├── trash.svg └── twitter.svg ├── next-env.d.ts ├── next.config.js ├── package.json ├── pages ├── _app.tsx ├── api │ └── hello.ts ├── example │ ├── 404.tsx │ ├── blank.tsx │ ├── buttons.tsx │ ├── cards.tsx │ ├── charts.tsx │ ├── create-account.tsx │ ├── forgot-password.tsx │ ├── forms.tsx │ ├── index.tsx │ ├── login.tsx │ ├── modals.tsx │ └── tables.tsx └── index.tsx ├── postcss.config.js ├── public ├── assets │ └── img │ │ ├── create-account-office-dark.jpeg │ │ ├── create-account-office.jpeg │ │ ├── forgot-password-office-dark.jpeg │ │ ├── forgot-password-office.jpeg │ │ ├── login-office-dark.jpeg │ │ └── login-office.jpeg ├── favicon.ico └── vercel.svg ├── routes └── sidebar.tsx ├── styles ├── Home.module.css └── globals.css ├── tailwind.config.js ├── tsconfig.json ├── utils └── demo │ ├── chartsData.ts │ └── tableData.ts └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.github/windmill-dashboard-thumbnail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roketid/windmill-dashboard-nextjs-typescript/4b26d05839141df2cfb38936842ec48e6be8d610/.github/windmill-dashboard-thumbnail.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "editor.formatOnSave": true, 4 | "editor.formatOnPaste": true, 5 | "editor.tabSize": 2 6 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Estevan Maito 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |

Windmill Dashboard Next.js Typescript

3 | 4 | Windmill Dashboard React 5 |
6 |

7 | 8 | See the [Original Project by @estevanmaito](https://github.com/estevanmaito/windmill-dashboard-react/) 9 | 10 | With help from other contributors : 11 | - [Typescript version by @neutralboy](https://github.com/neutralboy/windmill-dashboard-react-ts) 12 | - [Nextjs version by @Aldhanekaa](https://github.com/Aldhanekaa/windmill-dashboard-nextjs) 13 | 14 | 15 | 🚀 [See it live](https://windmill-dashboard-nextjs-typescript.vercel.app/example) 16 | 17 | This is not a template. This is a complete application, built on top of React, with all tiny details taken care of so you just need to bring the data to feed it. 18 | 19 | Accessibility is a priority in my projects and I think it should be in yours too, so this was developed listening to real screen readers, focus traps and keyboard navigation are available everywhere. 20 | 21 | ## 📦 Features 22 | 23 | - 🦮 Throughly accessible (developed using screen readers) 24 | - 🌗 Dark theme enabled (load even different images based on theme) 25 | - 🧩 Multiple (custom) components 26 | - ⚡ Code splitting 27 | - Tailwind CSS 28 | - [Windmill React UI](https://windmillui.com/react-ui) 29 | - Heroicons 30 | - Chart.js 31 | 32 | ## 📚 Docs 33 | 34 | ### General components 35 | 36 | Windmill Dashboard React is built on top of [Windmill React UI](https://windmillui.com/react-ui). You will find the documentation for every small component there. 37 | 38 | ### Example Boilerplate 39 | 40 | All components and containers are saved in folder [example](example) 41 | 42 | ### Routing 43 | 44 | Change default redirecting when hit the `/` or `home` in file [next.config.js](next.config.js) 45 | ```js 46 | async redirects() { 47 | return [ 48 | { 49 | source: '/', 50 | destination: '/example/login', 51 | permanent: false, 52 | }, 53 | ] 54 | } 55 | ``` 56 | 57 | #### Sidebar routes 58 | 59 | To configure sidebar menus, see file ([routes/sidebar.tsx](routes/sidebar.tsx)). 60 | 61 | These are the routes that will show in the sidebar. They expect three properties: 62 | 63 | - `path`: the destination; 64 | - `name`: the name to be shown; 65 | - `icon`: an icon to illustrate the item 66 | 67 | Item that are used as dropdowns, like the Pages option, don't need a `path`, but expect a `routes` array of objects with `path` and `name`: 68 | 69 | ```js 70 | // sidebar.js 71 | { 72 | path: '/example/tables', 73 | icon: 'TablesIcon', 74 | name: 'Tables', 75 | }, 76 | { 77 | icon: 'PagesIcon', // <-- this is used as a submenu, so no path 78 | name: 'Pages', 79 | routes: [ 80 | // submenu 81 | { 82 | path: '/example/login', 83 | name: 'Login', // <-- these don't have icons 84 | }, 85 | { 86 | path: '/example/create-account', 87 | name: 'Create account', 88 | }, 89 | ``` 90 | 91 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 92 | 93 | ## Getting Started 94 | 95 | First, install dependencies : 96 | ```bash 97 | npm install 98 | # or 99 | yarn install 100 | ``` 101 | 102 | then, you can run the development server: 103 | 104 | ```bash 105 | npm run dev 106 | # or 107 | yarn dev 108 | ``` 109 | 110 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 111 | 112 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 113 | 114 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. 115 | 116 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 117 | 118 | ## Learn More 119 | 120 | To learn more about Next.js, take a look at the following resources: 121 | 122 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 123 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 124 | 125 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 126 | 127 | ## Deploy on Vercel 128 | 129 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 130 | 131 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 132 | -------------------------------------------------------------------------------- /context/SidebarContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useLayoutEffect, useMemo, useState } from 'react' 2 | 3 | // create context 4 | 5 | interface IScrollY { 6 | id: string | null 7 | position: number 8 | } 9 | interface ISidebarContext { 10 | isSidebarOpen: boolean 11 | scrollY: IScrollY 12 | closeSidebar: () => void 13 | toggleSidebar: () => void 14 | saveScroll: (el: HTMLElement | null) => void 15 | } 16 | 17 | const SidebarContext = React.createContext( 18 | { 19 | isSidebarOpen: false, 20 | scrollY: {id: null, position: 0}, 21 | closeSidebar: () => {}, 22 | toggleSidebar: () => {}, 23 | saveScroll: (el: HTMLElement | null) => {} 24 | } 25 | ); 26 | 27 | interface ISidebarPovider{ children: React.ReactNode } 28 | 29 | export const SidebarProvider = ({ children }: ISidebarPovider) => { 30 | const [isSidebarOpen, setIsSidebarOpen] = useState(false) 31 | 32 | function toggleSidebar() { 33 | setIsSidebarOpen(!isSidebarOpen) 34 | } 35 | 36 | function closeSidebar() { 37 | setIsSidebarOpen(false) 38 | } 39 | 40 | const defaultScrollY = useMemo(() => { 41 | return {id: null, position: 0} 42 | }, []) 43 | 44 | const storageScrollY = useCallback(() => { 45 | return JSON.parse(localStorage.getItem('sidebarScrollY') || JSON.stringify(defaultScrollY)) 46 | }, [defaultScrollY]) 47 | 48 | const [scrollY, setScrollY] = useState( 49 | process.browser ? storageScrollY() : defaultScrollY 50 | ) 51 | 52 | function saveScroll(el: HTMLElement | null) { 53 | const id = el?.id || null 54 | const position = el?.scrollTop || 0 55 | setScrollY({id, position}) 56 | } 57 | 58 | useEffect(() => { 59 | if (process.browser) { 60 | localStorage.setItem('sidebarScrollY', JSON.stringify(scrollY)) 61 | } 62 | }, [scrollY]) 63 | 64 | useLayoutEffect(() => { 65 | if (process.browser) { 66 | const { id, position } = storageScrollY() 67 | document.getElementById(id)?.scrollTo(0, position) 68 | 69 | if (isSidebarOpen) { 70 | document.getElementById(id)?.scrollTo(0, position) 71 | } 72 | } 73 | }, [scrollY, storageScrollY, isSidebarOpen]) 74 | 75 | const context = { 76 | isSidebarOpen, 77 | scrollY, 78 | toggleSidebar, 79 | closeSidebar, 80 | saveScroll, 81 | } 82 | 83 | return {children} 84 | } 85 | 86 | export default SidebarContext 87 | -------------------------------------------------------------------------------- /context/ThemeContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef, useLayoutEffect } from 'react' 2 | 3 | /** 4 | * Saves the old theme for future use 5 | * @param {string} theme - Name of curent theme 6 | * @return {string} previousTheme 7 | */ 8 | function usePrevious(theme: string) { 9 | const ref = useRef() 10 | useEffect(() => { 11 | ref.current = theme 12 | }) 13 | return ref.current 14 | } 15 | 16 | /** 17 | * Gets user preferences from local storage 18 | * @param {string} key - localStorage key 19 | * @return {array} getter and setter for user preferred theme 20 | */ 21 | function useStorageTheme(key: string): [string, React.Dispatch>]{ 22 | const userPreference = 23 | !!window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches 24 | 25 | const [theme, setTheme] = useState( 26 | // use stored theme fallback to user preference 27 | localStorage.getItem(key) || userPreference 28 | ) 29 | 30 | // update stored theme 31 | useEffect(() => { 32 | localStorage.setItem(key.toString(), theme.toString()) 33 | }, [theme, key]) 34 | 35 | return [theme.toString(), setTheme] 36 | } 37 | 38 | interface IThemeContext{ 39 | theme: string | React.Dispatch> 40 | toggleTheme: () => void 41 | } 42 | 43 | // create context 44 | export const ThemeContext = React.createContext({ theme: "", toggleTheme: () => {} }) 45 | 46 | interface IThemeProvider{ 47 | children: React.ReactNode 48 | } 49 | 50 | // create context provider 51 | export const ThemeProvider = ({ children }: IThemeProvider) => { 52 | const [theme, setTheme] = useStorageTheme('theme') 53 | 54 | // update root element class on theme change 55 | const oldTheme = usePrevious(theme.toString()) 56 | useLayoutEffect(() => { 57 | document.documentElement.classList.remove(`theme-${oldTheme}`) 58 | document.documentElement.classList.add(`theme-${theme}`) 59 | }, [theme, oldTheme]) 60 | 61 | function toggleTheme() { 62 | 63 | if (theme === 'light'){ 64 | setTheme('dark') 65 | } 66 | else{ 67 | setTheme('light') 68 | } 69 | } 70 | 71 | const context = { 72 | theme, 73 | toggleTheme, 74 | } 75 | 76 | return {children} 77 | } -------------------------------------------------------------------------------- /example/components/AccessibleNavigationAnnouncer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react' 2 | import { useRouter } from 'next/router' 3 | 4 | function AccessibleNavigationAnnouncer() { 5 | const [message, setMessage] = useState('') 6 | const router = useRouter() 7 | 8 | useEffect(() => { 9 | // ignore the / 10 | if (router.pathname.slice(1)) { 11 | // make sure navigation has occurred and screen reader is ready 12 | setTimeout(() => setMessage(`Navigated to ${router.pathname.slice(1)} page.`), 500) 13 | } else { 14 | setMessage('') 15 | } 16 | }, [router]) 17 | 18 | return ( 19 | 20 | {message} 21 | 22 | ) 23 | } 24 | 25 | export default AccessibleNavigationAnnouncer 26 | -------------------------------------------------------------------------------- /example/components/CTA.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | function CTA() { 4 | return ( 5 | 9 |
10 | 11 | 12 | 13 | Star this project on GitHub 14 |
15 | 16 | View more 17 | 18 |
19 | ) 20 | } 21 | 22 | export default CTA 23 | -------------------------------------------------------------------------------- /example/components/Cards/InfoCard.tsx: -------------------------------------------------------------------------------- 1 | import { ReactSVGElement } from 'react' 2 | import { Card, CardBody } from '@roketid/windmill-react-ui' 3 | 4 | interface IInfoCard{ 5 | title: string 6 | value: string 7 | children?: ReactSVGElement 8 | } 9 | 10 | function InfoCard({ title, value, children }: IInfoCard) { 11 | return ( 12 | 13 | 14 | {children} 15 |
16 |

{title}

17 |

{value}

18 |
19 |
20 |
21 | ) 22 | } 23 | 24 | export default InfoCard 25 | -------------------------------------------------------------------------------- /example/components/Chart/ChartCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | interface IChart{ 4 | children: React.ReactNode 5 | title: string 6 | } 7 | 8 | function Chart({ children, title }: IChart) { 9 | return ( 10 |
11 |

{title}

12 | {children} 13 |
14 | ) 15 | } 16 | 17 | export default Chart 18 | -------------------------------------------------------------------------------- /example/components/Chart/ChartLegend.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import {ILegends} from 'utils/demo/chartsData' 4 | 5 | interface IChartLegend{ 6 | legends: ILegends[] 7 | } 8 | 9 | function ChartLegend({ legends }: IChartLegend) { 10 | return ( 11 |
12 | {legends.map((legend) => ( 13 |
14 | 15 | {legend.title} 16 |
17 | ))} 18 |
19 | ) 20 | } 21 | 22 | export default ChartLegend 23 | -------------------------------------------------------------------------------- /example/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useState } from 'react' 2 | import SidebarContext from 'context/SidebarContext' 3 | import { 4 | SearchIcon, 5 | MoonIcon, 6 | SunIcon, 7 | BellIcon, 8 | MenuIcon, 9 | OutlinePersonIcon, 10 | OutlineCogIcon, 11 | OutlineLogoutIcon, 12 | } from 'icons' 13 | import { Avatar, Badge, Input, Dropdown, DropdownItem, WindmillContext } from '@roketid/windmill-react-ui' 14 | 15 | function Header() { 16 | const { mode, toggleMode } = useContext(WindmillContext) 17 | const { toggleSidebar } = useContext(SidebarContext) 18 | 19 | const [isNotificationsMenuOpen, setIsNotificationsMenuOpen] = useState(false) 20 | const [isProfileMenuOpen, setIsProfileMenuOpen] = useState(false) 21 | 22 | function handleNotificationsClick() { 23 | setIsNotificationsMenuOpen(!isNotificationsMenuOpen) 24 | } 25 | 26 | function handleProfileClick() { 27 | setIsProfileMenuOpen(!isProfileMenuOpen) 28 | } 29 | 30 | return ( 31 |
32 |
33 | {/* */} 34 | 41 | {/* */} 42 |
43 |
44 |
45 |
47 | 52 |
53 |
54 |
    55 | {/* */} 56 |
  • 57 | 68 |
  • 69 | {/* */} 70 |
  • 71 | 84 | 85 | setIsNotificationsMenuOpen(false)} 89 | > 90 | 91 | Messages 92 | 13 93 | 94 | 95 | Sales 96 | 2 97 | 98 | alert('Alerts!')}> 99 | Alerts 100 | 101 | 102 |
  • 103 | {/* */} 104 |
  • 105 | 118 | setIsProfileMenuOpen(false)} 122 | > 123 | 124 | 127 | 128 | 131 | alert('Log out!')}> 132 | 135 | 136 |
  • 137 |
138 |
139 |
140 | ) 141 | } 142 | 143 | export default Header 144 | -------------------------------------------------------------------------------- /example/components/Loader/Loader.module.css: -------------------------------------------------------------------------------- 1 | .LoaderDots div { 2 | animation-timing-function: cubic-bezier(0, 1, 1, 0); 3 | } 4 | .LoaderDots div:nth-child(1) { 5 | left: 8px; 6 | animation: LoaderDots1 0.6s infinite; 7 | } 8 | .LoaderDots div:nth-child(2) { 9 | left: 8px; 10 | animation: LoaderDots2 0.6s infinite; 11 | } 12 | .LoaderDots div:nth-child(3) { 13 | left: 32px; 14 | animation: LoaderDots2 0.6s infinite; 15 | } 16 | .LoaderDots div:nth-child(4) { 17 | left: 56px; 18 | animation: LoaderDots3 0.6s infinite; 19 | } 20 | @keyframes LoaderDots1 { 21 | 0% { 22 | transform: scale(0); 23 | } 24 | 100% { 25 | transform: scale(1); 26 | } 27 | } 28 | @keyframes LoaderDots3 { 29 | 0% { 30 | transform: scale(1); 31 | } 32 | 100% { 33 | transform: scale(0); 34 | } 35 | } 36 | @keyframes LoaderDots2 { 37 | 0% { 38 | transform: translate(0, 0); 39 | } 40 | 100% { 41 | transform: translate(24px, 0); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /example/components/Loader/Loader.tsx: -------------------------------------------------------------------------------- 1 | import styles from './Loader.module.css' 2 | 3 | function Loader() { 4 | return ( 5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | Please wait... 16 |
17 |
18 |
19 |
20 | ) 21 | } 22 | 23 | export default Loader 24 | -------------------------------------------------------------------------------- /example/components/RoundIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import classNames from 'classnames' 3 | 4 | interface IRoundIcon{ 5 | icon: any 6 | className: string 7 | iconColorClass: string 8 | bgColorClass: string 9 | } 10 | 11 | function RoundIcon({ 12 | icon, 13 | iconColorClass = 'text-purple-600 dark:text-purple-100', 14 | bgColorClass = 'bg-purple-100 dark:bg-purple-600', 15 | className 16 | }: IRoundIcon) { 17 | const baseStyle = 'p-3 rounded-full' 18 | const cls = classNames(baseStyle, iconColorClass, bgColorClass, className) 19 | return( 20 |
21 | {/* */} 22 |
23 | ) 24 | } 25 | 26 | export default RoundIcon 27 | -------------------------------------------------------------------------------- /example/components/Sidebar/DesktopSidebar.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useRef } from 'react' 2 | import SidebarContext from 'context/SidebarContext' 3 | import SidebarContent from './SidebarContent' 4 | 5 | function DesktopSidebar() { 6 | const sidebarRef = useRef(null) 7 | const { saveScroll } = useContext(SidebarContext) 8 | 9 | const linkClickedHandler = () => { 10 | saveScroll(sidebarRef.current) 11 | } 12 | 13 | return ( 14 | 21 | ) 22 | } 23 | 24 | export default DesktopSidebar 25 | -------------------------------------------------------------------------------- /example/components/Sidebar/MobileSidebar.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useRef } from 'react' 2 | import { Transition, Backdrop } from '@roketid/windmill-react-ui' 3 | import SidebarContext from 'context/SidebarContext' 4 | import SidebarContent from './SidebarContent' 5 | 6 | 7 | function MobileSidebar() { 8 | const sidebarRef = useRef(null) 9 | const { isSidebarOpen, closeSidebar, saveScroll } = useContext(SidebarContext) 10 | 11 | const linkClickedHandler = () => { 12 | saveScroll(sidebarRef.current) 13 | } 14 | 15 | return ( 16 | 17 | <> 18 | 26 | 27 | 28 | 29 | 37 | 44 | 45 | 46 | 47 | ) 48 | } 49 | 50 | export default MobileSidebar 51 | -------------------------------------------------------------------------------- /example/components/Sidebar/SidebarContent.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import routes, { routeIsActive } from 'routes/sidebar' 3 | import * as Icons from 'icons' 4 | import { IIcon } from 'icons' 5 | import SidebarSubmenu from './SidebarSubmenu' 6 | import { Button } from '@roketid/windmill-react-ui' 7 | import { useRouter } from 'next/router' 8 | 9 | function Icon({ icon, ...props }: IIcon){ 10 | // @ts-ignore 11 | const Icon= Icons[icon] 12 | return 13 | } 14 | 15 | interface ISidebarContent{ 16 | linkClicked: () => void 17 | } 18 | 19 | function SidebarContent({ linkClicked }: ISidebarContent) { 20 | const { pathname } = useRouter(); 21 | const appName = process.env.NEXT_PUBLIC_APP_NAME 22 | 23 | return ( 24 |
25 | 26 | 33 | 34 | 71 |
72 | 78 |
79 |
80 | ) 81 | } 82 | 83 | export default SidebarContent -------------------------------------------------------------------------------- /example/components/Sidebar/SidebarSubmenu.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext } from 'react' 2 | import Link from 'next/link' 3 | import { useRouter } from 'next/router' 4 | import { DropdownIcon, IIcon } from 'icons' 5 | import * as Icons from 'icons' 6 | import { Transition } from '@roketid/windmill-react-ui' 7 | import { IRoute, routeIsActive } from 'routes/sidebar' 8 | import SidebarContext from 'context/SidebarContext' 9 | 10 | function Icon({ icon, ...props }: IIcon) { 11 | // @ts-ignore 12 | const _Icon = Icons[icon] 13 | return <_Icon {...props} /> 14 | } 15 | 16 | interface ISidebarSubmenu { 17 | route: IRoute 18 | linkClicked: () => void 19 | } 20 | 21 | function SidebarSubmenu({ route, linkClicked }: ISidebarSubmenu) { 22 | const { pathname } = useRouter() 23 | const { saveScroll } = useContext(SidebarContext) 24 | 25 | const [isDropdownMenuOpen, setIsDropdownMenuOpen] = useState( 26 | route.routes 27 | ? route.routes.filter((r) => { 28 | return routeIsActive(pathname, r) 29 | }).length > 0 30 | : false 31 | ) 32 | 33 | function handleDropdownMenuClick() { 34 | setIsDropdownMenuOpen(!isDropdownMenuOpen) 35 | } 36 | 37 | return ( 38 |
  • 39 | {isDropdownMenuOpen && ( 40 | 44 | )} 45 | 61 | 70 |
      74 | { 75 | route.routes && route.routes.map((r) => ( 76 |
    • 80 | 84 | 92 | {r.name} 93 | 94 | 95 |
    • 96 | )) 97 | } 98 |
    99 |
    100 |
  • 101 | ) 102 | } 103 | 104 | export default SidebarSubmenu 105 | -------------------------------------------------------------------------------- /example/components/Sidebar/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import DesktopSidebar from './DesktopSidebar' 3 | import MobileSidebar from './MobileSidebar' 4 | 5 | function Sidebar() { 6 | return ( 7 | <> 8 | 9 | 10 | 11 | ) 12 | } 13 | 14 | export default Sidebar 15 | -------------------------------------------------------------------------------- /example/components/ThemedSuspense.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | function ThemedSuspense() { 4 | return ( 5 |
    6 | Loading... 7 |
    8 | ) 9 | } 10 | 11 | export default ThemedSuspense 12 | -------------------------------------------------------------------------------- /example/components/Typography/PageTitle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | interface IPageTitle{ 4 | children: React.ReactNode 5 | } 6 | 7 | function PageTitle({ children }: IPageTitle) { 8 | return ( 9 |

    {children}

    10 | ) 11 | } 12 | 13 | export default PageTitle 14 | -------------------------------------------------------------------------------- /example/components/Typography/SectionTitle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | interface ISectionTitle{ 4 | children: React.ReactNode 5 | } 6 | 7 | function SectionTitle({ children }: ISectionTitle) { 8 | return

    {children}

    9 | } 10 | 11 | export default SectionTitle 12 | -------------------------------------------------------------------------------- /example/containers/Layout.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react' 2 | import SidebarContext, { SidebarProvider } from 'context/SidebarContext' 3 | import Sidebar from 'example/components/Sidebar' 4 | import Header from 'example/components/Header' 5 | import Main from './Main' 6 | 7 | interface ILayout{ 8 | children: React.ReactNode 9 | } 10 | 11 | function Layout({ children }: ILayout) { 12 | const { isSidebarOpen } = useContext(SidebarContext) 13 | 14 | return 15 |
    18 | 19 |
    20 |
    21 |
    22 | {children} 23 |
    24 |
    25 |
    26 |
    27 | } 28 | 29 | export default Layout -------------------------------------------------------------------------------- /example/containers/Main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | interface IMain{ 4 | children: React.ReactNode 5 | } 6 | 7 | function Main({ children }: IMain) { 8 | return ( 9 |
    10 |
    {children}
    11 |
    12 | ); 13 | } 14 | 15 | export default Main 16 | -------------------------------------------------------------------------------- /icons/bell.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /icons/buttons.svg: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /icons/cards.svg: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /icons/cart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /icons/charts.svg: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /icons/chat.svg: -------------------------------------------------------------------------------- 1 | 2 | 7 | -------------------------------------------------------------------------------- /icons/dropdown.svg: -------------------------------------------------------------------------------- 1 | 2 | 7 | -------------------------------------------------------------------------------- /icons/edit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /icons/forbidden.svg: -------------------------------------------------------------------------------- 1 | 2 | 7 | -------------------------------------------------------------------------------- /icons/forms.svg: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /icons/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icons/heart.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icons/home.svg: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /icons/index.ts: -------------------------------------------------------------------------------- 1 | import ButtonsIcon from './buttons.svg' 2 | import CardsIcon from './cards.svg' 3 | import ChartsIcon from './charts.svg' 4 | import FormsIcon from './forms.svg' 5 | import HomeIcon from './home.svg' 6 | import ModalsIcon from './modals.svg' 7 | import PagesIcon from './pages.svg' 8 | import TablesIcon from './tables.svg' 9 | import HeartIcon from './heart.svg' 10 | import EditIcon from './edit.svg' 11 | import TrashIcon from './trash.svg' 12 | import ForbiddenIcon from './forbidden.svg' 13 | import GithubIcon from './github.svg' 14 | import TwitterIcon from './twitter.svg' 15 | import MailIcon from './mail.svg' 16 | import CartIcon from './cart.svg' 17 | import ChatIcon from './chat.svg' 18 | import MoneyIcon from './money.svg' 19 | import PeopleIcon from './people.svg' 20 | import SearchIcon from './search.svg' 21 | import MoonIcon from './moon.svg' 22 | import SunIcon from './sun.svg' 23 | import BellIcon from './bell.svg' 24 | import MenuIcon from './menu.svg' 25 | import DropdownIcon from './dropdown.svg' 26 | import OutlinePersonIcon from './outlinePerson.svg' 27 | import OutlineCogIcon from './outlineCog.svg' 28 | import OutlineLogoutIcon from './outlineLogout.svg' 29 | 30 | export { 31 | ButtonsIcon, 32 | CardsIcon, 33 | ChartsIcon, 34 | FormsIcon, 35 | HomeIcon, 36 | ModalsIcon, 37 | PagesIcon, 38 | TablesIcon, 39 | HeartIcon, 40 | EditIcon, 41 | TrashIcon, 42 | ForbiddenIcon, 43 | GithubIcon, 44 | TwitterIcon, 45 | MailIcon, 46 | CartIcon, 47 | ChatIcon, 48 | MoneyIcon, 49 | PeopleIcon, 50 | SearchIcon, 51 | MoonIcon, 52 | SunIcon, 53 | BellIcon, 54 | MenuIcon, 55 | DropdownIcon, 56 | OutlinePersonIcon, 57 | OutlineCogIcon, 58 | OutlineLogoutIcon, 59 | } 60 | 61 | interface IIcon{ 62 | icon: string 63 | [key: string]: string | undefined 64 | } 65 | 66 | export type { 67 | IIcon 68 | }; -------------------------------------------------------------------------------- /icons/mail.svg: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /icons/menu.svg: -------------------------------------------------------------------------------- 1 | 2 | 7 | -------------------------------------------------------------------------------- /icons/modals.svg: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /icons/money.svg: -------------------------------------------------------------------------------- 1 | 2 | 7 | -------------------------------------------------------------------------------- /icons/moon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /icons/outlineCog.svg: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /icons/outlineLogout.svg: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /icons/outlinePerson.svg: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /icons/pages.svg: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /icons/people.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /icons/search.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icons/sun.svg: -------------------------------------------------------------------------------- 1 | 2 | 7 | -------------------------------------------------------------------------------- /icons/tables.svg: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /icons/trash.svg: -------------------------------------------------------------------------------- 1 | 5 | 10 | -------------------------------------------------------------------------------- /icons/twitter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | // NOTE: This file should not be edited 6 | // see https://nextjs.org/docs/basic-features/typescript for more information. 7 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | module.exports = { 3 | reactStrictMode: false, 4 | webpack(config) { 5 | config.module.rules.push({ 6 | test: /\.svg$/i, 7 | issuer: /\.[jt]sx?$/, 8 | use: ['@svgr/webpack'], 9 | }) 10 | 11 | return config 12 | }, 13 | async redirects() { 14 | return [ 15 | { 16 | source: '/', 17 | destination: '/example/login', 18 | permanent: false, 19 | }, 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "windmill-dashboard-nextjs-typescript", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "chart.js": "^3.6.1", 13 | "classnames": "^2.3.1", 14 | "next": "11.1.2", 15 | "react": "17.0.2", 16 | "react-chartjs-2": "^4.0.0", 17 | "react-dom": "17.0.2" 18 | }, 19 | "devDependencies": { 20 | "@roketid/windmill-react-ui": "^0.1.1", 21 | "@svgr/webpack": "^6.1.0", 22 | "@types/react": "17.0.21", 23 | "autoprefixer": "^10.4.2", 24 | "cssnano": "^5.0.12", 25 | "eslint": "7.32.0", 26 | "eslint-config-next": "11.1.2", 27 | "postcss": "^8.4.6", 28 | "tailwindcss": "^3.0.22", 29 | "typescript": "4.4.3" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '../styles/globals.css' 2 | import 'tailwindcss/tailwind.css'; 3 | 4 | import React from 'react' 5 | import { Windmill } from '@roketid/windmill-react-ui' 6 | import type { AppProps } from 'next/app' 7 | 8 | function MyApp({ Component, pageProps }: AppProps) { 9 | // suppress useLayoutEffect warnings when running outside a browser 10 | if (!process.browser) React.useLayoutEffect = React.useEffect; 11 | 12 | return ( 13 | 14 | 15 | 16 | ) 17 | } 18 | export default MyApp 19 | -------------------------------------------------------------------------------- /pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from 'next' 3 | 4 | type Data = { 5 | name: string 6 | } 7 | 8 | export default function handler( 9 | req: NextApiRequest, 10 | res: NextApiResponse 11 | ) { 12 | res.status(200).json({ name: 'John Doe' }) 13 | } 14 | -------------------------------------------------------------------------------- /pages/example/404.tsx: -------------------------------------------------------------------------------- 1 | import Layout from 'example/containers/Layout' 2 | import { ForbiddenIcon } from 'icons' 3 | 4 | function Page404() { 5 | return ( 6 | 7 |
    8 |
    18 |
    19 | ) 20 | } 21 | 22 | export default Page404 23 | -------------------------------------------------------------------------------- /pages/example/blank.tsx: -------------------------------------------------------------------------------- 1 | import PageTitle from 'example/components/Typography/PageTitle' 2 | import Layout from 'example/containers/Layout' 3 | 4 | function Blank() { 5 | return ( 6 | 7 | Blank 8 | 9 | ) 10 | } 11 | 12 | export default Blank 13 | -------------------------------------------------------------------------------- /pages/example/buttons.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Button } from '@roketid/windmill-react-ui' 3 | import PageTitle from 'example/components/Typography/PageTitle' 4 | import SectionTitle from 'example/components/Typography/SectionTitle' 5 | import CTA from 'example/components/CTA' 6 | import Layout from 'example/containers/Layout' 7 | import { HeartIcon, EditIcon } from 'icons' 8 | 9 | function Buttons() { 10 | return ( 11 | 12 | Buttons 13 | 14 | 15 | 16 | Primary 17 |
    18 |
    19 | 20 |
    21 | 22 |
    23 | 24 |
    25 | 26 |
    27 | 28 |
    29 | 30 | {/*
    31 | 34 |
    */} 35 | 36 |
    37 | 38 |
    39 | 40 |
    41 | 42 |
    43 |
    44 | 45 | Outline 46 |
    47 |
    48 | 51 |
    52 | 53 |
    54 | 57 |
    58 | 59 |
    60 | 61 |
    62 | 63 | {/*
    64 | 67 |
    */} 68 | 69 |
    70 | 73 |
    74 | 75 |
    76 | 79 |
    80 |
    81 | 82 | Link 83 |
    84 |
    85 | 88 |
    89 | 90 |
    91 | 94 |
    95 | 96 |
    97 | 98 |
    99 | 100 | {/*
    101 | 104 |
    */} 105 | 106 |
    107 | 110 |
    111 | 112 |
    113 | 116 |
    117 |
    118 | 119 | Icons 120 |
    121 |
    122 | {/* @ts-ignore */} 123 | 126 |
    127 | 128 |
    129 | {/* @ts-ignore */} 130 | 133 |
    134 | 135 |
    136 | {/* @ts-ignore */} 137 |
    139 | 140 |
    141 | {/* @ts-ignore */} 142 |
    144 | 145 |
    146 | {/* @ts-ignore */} 147 |
    149 |
    150 | {/* @ts-ignore */} 151 |
    153 |
    154 |
    155 | ) 156 | } 157 | 158 | export default Buttons 159 | -------------------------------------------------------------------------------- /pages/example/cards.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { Card, CardBody } from '@roketid/windmill-react-ui' 4 | import PageTitle from 'example/components/Typography/PageTitle' 5 | import SectionTitle from 'example/components/Typography/SectionTitle' 6 | import CTA from 'example/components/CTA' 7 | import InfoCard from 'example/components/Cards/InfoCard' 8 | import RoundIcon from 'example/components/RoundIcon' 9 | import Layout from 'example/containers/Layout' 10 | import { CartIcon, ChatIcon, MoneyIcon, PeopleIcon } from 'icons' 11 | 12 | function Cards() { 13 | return ( 14 | 15 | Cards 16 | 17 | 18 | 19 | Big section cards 20 | 21 | 22 | 23 |

    24 | Large, full width sections goes here 25 |

    26 |
    27 |
    28 | 29 | Responsive cards 30 | 31 |
    32 | 33 | {/* @ts-ignore */} 34 | 40 | 41 | 42 | 43 | {/* @ts-ignore */} 44 | 50 | 51 | 52 | 53 | {/* @ts-ignore */} 54 | 60 | 61 | 62 | 63 | {/* @ts-ignore */} 64 | 70 | 71 |
    72 | 73 | Cards with title 74 | 75 |
    76 | 77 | 78 |

    Revenue

    79 |

    80 | Lorem ipsum dolor sit, amet consectetur adipisicing elit. Fuga, cum commodi a omnis 81 | numquam quod? Totam exercitationem quos hic ipsam at qui cum numquam, sed amet 82 | ratione! Ratione, nihil dolorum. 83 |

    84 |
    85 |
    86 | 87 | 88 | 89 |

    Colored card

    90 |

    91 | Lorem ipsum dolor sit, amet consectetur adipisicing elit. Fuga, cum commodi a omnis 92 | numquam quod? Totam exercitationem quos hic ipsam at qui cum numquam, sed amet 93 | ratione! Ratione, nihil dolorum. 94 |

    95 |
    96 |
    97 |
    98 |
    99 | ) 100 | } 101 | 102 | export default Cards 103 | -------------------------------------------------------------------------------- /pages/example/charts.tsx: -------------------------------------------------------------------------------- 1 | import { Doughnut, Line, Bar } from 'react-chartjs-2' 2 | import ChartCard from 'example/components/Chart/ChartCard' 3 | import ChartLegend from 'example/components/Chart/ChartLegend' 4 | import PageTitle from 'example/components/Typography/PageTitle' 5 | import Layout from 'example/containers/Layout' 6 | import { 7 | doughnutOptions, 8 | lineOptions, 9 | barOptions, 10 | doughnutLegends, 11 | lineLegends, 12 | barLegends, 13 | } from 'utils/demo/chartsData' 14 | import { 15 | Chart, 16 | ArcElement, 17 | BarElement, 18 | CategoryScale, 19 | LinearScale, 20 | PointElement, 21 | LineElement, 22 | Title, 23 | Tooltip, 24 | Legend, 25 | } from 'chart.js' 26 | 27 | function Charts() { 28 | Chart.register( 29 | ArcElement, 30 | BarElement, 31 | CategoryScale, 32 | LinearScale, 33 | PointElement, 34 | LineElement, 35 | Title, 36 | Tooltip, 37 | Legend 38 | ) 39 | 40 | return ( 41 | 42 | Charts 43 | 44 |
    45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 |
    60 |
    61 | ) 62 | } 63 | 64 | export default Charts 65 | -------------------------------------------------------------------------------- /pages/example/create-account.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | import Link from 'next/link' 3 | import Image from 'next/image' 4 | 5 | import { GithubIcon, TwitterIcon } from 'icons' 6 | import { Input, Label, Button, WindmillContext } from '@roketid/windmill-react-ui' 7 | 8 | function CrateAccount() { 9 | const { mode } = useContext(WindmillContext) 10 | const imgSource = mode === 'dark' ? '/assets/img/create-account-office-dark.jpeg' : '/assets/img/create-account-office.jpeg' 11 | 12 | return ( 13 |
    14 |
    15 |
    16 |
    17 | 24 |
    25 |
    26 |
    27 |

    28 | Create account 29 |

    30 | 34 | 38 | 42 | 43 | 49 | 50 | 54 | 57 | 58 | 59 |
    60 | 61 | 65 | 69 | 70 |

    71 | 72 | 75 | Already have an account? Login 76 | 77 | 78 |

    79 |
    80 |
    81 |
    82 |
    83 |
    84 | ) 85 | } 86 | 87 | export default CrateAccount 88 | -------------------------------------------------------------------------------- /pages/example/forgot-password.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react' 2 | import Image from 'next/image' 3 | import Link from 'next/link' 4 | 5 | import { Label, Input, Button, WindmillContext } from '@roketid/windmill-react-ui' 6 | 7 | function ForgotPassword() { 8 | const { mode } = useContext(WindmillContext) 9 | const imgSource = mode === 'dark' ? '/assets/img/forgot-password-office-dark.jpeg' : '/assets/img/forgot-password-office.jpeg' 10 | 11 | return ( 12 |
    13 |
    14 |
    15 |
    16 | 23 |
    24 |
    25 |
    26 |

    27 | Forgot password 28 |

    29 | 30 | 34 | 35 | 36 | 39 | 40 |
    41 |
    42 |
    43 |
    44 |
    45 | ) 46 | } 47 | 48 | export default ForgotPassword 49 | -------------------------------------------------------------------------------- /pages/example/forms.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { Input, HelperText, Label, Select, Textarea } from '@roketid/windmill-react-ui' 4 | import CTA from 'example/components/CTA' 5 | import PageTitle from 'example/components/Typography/PageTitle' 6 | import SectionTitle from 'example/components/Typography/SectionTitle' 7 | 8 | import Layout from 'example/containers/Layout' 9 | import { MailIcon } from 'icons' 10 | 11 | function Forms() { 12 | return ( 13 | 14 | Forms 15 | 16 | Elements 17 | 18 |
    19 | 23 | 24 | 28 | 29 |
    30 | {/* TODO: Check if this label is accessible, or fallback */} 31 | {/* Account Type */} 32 | 33 |
    34 | 38 | 42 | 46 |
    47 |
    48 | 49 | 58 | 59 | 69 | 70 |