├── .gitignore ├── README.md ├── index.html ├── package.json ├── pnpm-lock.yaml ├── public ├── nw.sqlite └── vite.svg ├── src ├── App.css ├── App.tsx ├── Customer.tsx ├── CustomersPage.tsx ├── DashboardPage.tsx ├── Documentation.tsx ├── Employees.tsx ├── EmployeesPage.tsx ├── LeftMenu.tsx ├── MainPage.tsx ├── MenuItem.tsx ├── Order.tsx ├── OrdersPage.tsx ├── Pagination.tsx ├── Product.tsx ├── ProductsPage.tsx ├── SearchPage.tsx ├── Supplier.tsx ├── SuppliersPage.tsx ├── assets │ └── react.svg ├── data │ └── schema.ts ├── hooks │ ├── useHover.ts │ ├── useOnClickOutside.ts │ └── usePagination.tsx ├── icons │ ├── ArrowDownIcon.tsx │ ├── Ballot.tsx │ ├── Cart.tsx │ ├── CheckboxChecked.tsx │ ├── CheckboxDefault.tsx │ ├── CustomersIcon.tsx │ ├── DashboardIcon.tsx │ ├── EmployeesIcon.tsx │ ├── HeaderArrowIcon.tsx │ ├── HomeIcon.tsx │ ├── InfoIcon.tsx │ ├── LinkIcon.tsx │ ├── MenuIcon.tsx │ ├── ProductsIcon.tsx │ ├── SearchIcon.tsx │ ├── SuppliersIcon.tsx │ └── nw.sqlite ├── index.css ├── main.tsx ├── pagination.scss ├── store │ ├── actions │ │ ├── common.ts │ │ ├── login.ts │ │ └── suppliers.ts │ ├── index.ts │ ├── reducers │ │ ├── Suppliers.ts │ │ └── auth.ts │ └── selectors │ │ ├── auth.ts │ │ └── suppliers.ts └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Drizzle ORM + sql.js 2 | 3 | This is a demo of the Northwind dataset, running on [Drizzle ORM](https://driz.li/orm) + [sql.js](https://github.com/sql-js/sql.js/) + [SQLite](https://www.sqlite.org/index.html) in your browser 4 | This dataset was sourced from [northwind-SQLite3](https://github.com/jpwhite3/northwind-SQLite3), UI design from [Cloudflare D1 example](https://northwind.d1sql.com). 5 | You can use the UI to explore Supplies, Orders, Customers, Employees and Products, or you can use search if you know what you're looking for. 6 | 7 | ```shell 8 | npm i 9 | npm run dev 10 | ``` 11 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sql-js", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@dicebear/avatars": "^4.10.5", 13 | "@dicebear/avatars-initials-sprites": "^4.10.5", 14 | "@reduxjs/toolkit": "^1.9.1", 15 | "axios": "^1.1.3", 16 | "buffer": "^6.0.3", 17 | "classnames": "^2.3.2", 18 | "cors": "^2.8.5", 19 | "date-fns": "^2.29.3", 20 | "dotenv": "^16.0.3", 21 | "drizzle-orm": "^0.23.8", 22 | "electron-debug": "^3.2.0", 23 | "electron-log": "^4.4.8", 24 | "electron-updater": "^5.2.3", 25 | "express": "^4.18.2", 26 | "history": "4.10.1", 27 | "immer-reducer": "^0.7.13", 28 | "pg": "^8.8.0", 29 | "react": "^18.2.0", 30 | "react-dom": "^18.2.0", 31 | "react-inlinesvg": "^3.0.1", 32 | "react-markdown": "^8.0.3", 33 | "react-redux": "^8.0.4", 34 | "react-router-dom": "^6.4.2", 35 | "react-scripts": "^5.0.1", 36 | "react-syntax-highlighter": "^15.5.0", 37 | "redux-thunk": "^2.4.1", 38 | "remark-gfm": "^3.0.1", 39 | "reselect": "^4.1.7", 40 | "sql.js": "^1.8.0", 41 | "styled-components": "^5.3.6" 42 | }, 43 | "devDependencies": { 44 | "@types/history": "4.7.8", 45 | "@types/react": "^18.0.26", 46 | "@types/react-dom": "^18.0.9", 47 | "@types/react-syntax-highlighter": "^15.5.6", 48 | "@types/sql.js": "^1.4.4", 49 | "@types/styled-components": "^5.1.26", 50 | "@vitejs/plugin-react": "^3.0.0", 51 | "sass": "^1.57.1", 52 | "typescript": "^4.9.3", 53 | "vite": "^4.0.0" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /public/nw.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drizzle-team/drizzle-sqljs/a1b0c40e80956c286a8abb964aff7a0374627674/public/nw.sqlite -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | /* 2 | * @NOTE: Prepend a `~` to css file paths that are in your node_modules 3 | * See https://github.com/webpack-contrib/sass-loader#imports 4 | */ 5 | body { 6 | position: relative; 7 | color: black; 8 | height: 100vh; 9 | font-size: 16px; 10 | margin: 0; 11 | background-color: rgba(249, 250, 251, 1); 12 | padding: 0; 13 | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji; 14 | } 15 | 16 | table { 17 | display: grid; 18 | grid-template-rows: 1.25rem 1fr; 19 | grid-template-columns: 1fr; 20 | height: 150px 21 | } 22 | 23 | tbody { 24 | height: 100%; 25 | overflow: auto; 26 | } 27 | 28 | tr { 29 | display: grid; 30 | grid-template-columns: repeat(5, 1fr) 31 | } 32 | 33 | td, th { 34 | border-top: 1px solid grey; 35 | } 36 | 37 | th { 38 | background: #F74B33; 39 | color: white 40 | } 41 | 42 | .container { 43 | height: 2.5em; 44 | position: relative; 45 | } 46 | 47 | .form__input { 48 | position: absolute; 49 | height: 100%; 50 | width: 530px; 51 | background: #fff; 52 | border: 2px solid #ccc; 53 | border-radius: 0.5em; 54 | outline: none; 55 | color: #000; 56 | padding-left: 1em; 57 | } 58 | 59 | label { 60 | position: absolute; 61 | background: #fff; 62 | color: #000; 63 | font-family: sans-serif; 64 | left: 1em; 65 | top: 0.75em; 66 | font-size: 16px; 67 | cursor: text; 68 | transition: top 350ms ease-in, font-size 350ms ease-in; 69 | } 70 | 71 | 72 | a { 73 | padding: 0 !important; 74 | margin: 0 !important; 75 | color: #2563eb !important; 76 | } 77 | 78 | button { 79 | background-color: white; 80 | font-size: 16px; 81 | padding: 5px 20px; 82 | border-radius: 5px; 83 | border: none; 84 | outline: none; 85 | appearance: none; 86 | box-shadow: 0px 8px 28px -6px rgba(24, 39, 75, 0.12), 87 | 0px 18px 88px -4px rgba(24, 39, 75, 0.14); 88 | transition: all ease-in 0.1s; 89 | cursor: pointer; 90 | opacity: 0.9; 91 | } 92 | 93 | button:hover { 94 | transform: scale(1.05); 95 | opacity: 1; 96 | } 97 | 98 | button:disabled { 99 | opacity: 0.5; 100 | cursor: not-allowed; 101 | } 102 | 103 | li { 104 | list-style: none; 105 | } 106 | 107 | a { 108 | text-decoration: none; 109 | height: fit-content; 110 | width: fit-content; 111 | margin: 10px; 112 | } 113 | 114 | a:hover { 115 | opacity: 1; 116 | text-decoration: none; 117 | } 118 | 119 | .Hello { 120 | display: flex; 121 | justify-content: center; 122 | align-items: center; 123 | margin: 20px 0; 124 | } 125 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import reactLogo from './assets/react.svg'; 3 | import './App.css'; 4 | import initSqlJs, { SqlValue } from 'sql.js'; 5 | import { drizzle } from 'drizzle-orm/sql-js'; 6 | import Home from "./LeftMenu"; 7 | import MainPage from "./MainPage"; 8 | import SuppliersPage from "./SuppliersPage"; 9 | import {MemoryRouter as Router, Route, Routes, useNavigate} from 'react-router-dom'; 10 | import Supplier from "./Supplier"; 11 | import DashboardPage from "./DashboardPage"; 12 | import {setLoadedFile} from "./store/actions/login"; 13 | import {details, employees, suppliers} from "./data/schema"; 14 | import {useDispatch, useSelector} from "react-redux"; 15 | import {selectLoadedFile} from "./store/selectors/auth"; 16 | import axios from "axios"; 17 | import ProductsPage from "./ProductsPage"; 18 | import Product from "./Product"; 19 | import OrdersPage from "./OrdersPage"; 20 | import Order from "./Order"; 21 | import EmployeesPage from "./EmployeesPage"; 22 | import Employees from "./Employees"; 23 | import CustomersPage from "./CustomersPage"; 24 | import Customer from "./Customer"; 25 | import Documentation from "./Documentation"; 26 | import SearchPage from "./SearchPage"; 27 | 28 | 29 | 30 | function App() { 31 | const [now, setNow] = useState(); 32 | const [file, setFile] = useState() 33 | const loadedFile = useSelector(selectLoadedFile); 34 | const dispatch = useDispatch(); 35 | const [db, setDb] = useState() 36 | 37 | useEffect(() => { 38 | (async () => { 39 | const sqlPromise = await initSqlJs({ 40 | // Required to load the wasm binary asynchronously. Of course, you can host it wherever you want 41 | // You can omit locateFile completely when running in node 42 | locateFile: (file) => `https://sql.js.org/dist/${file}`, 43 | }); 44 | // 45 | // const dataPromise = !loadedFile && await axios.get('https://therealyo-university.s3.eu-west-2.amazonaws.com/nw.sqlite', { 46 | // responseType: 'arraybuffer' 47 | // }).then((response) => { 48 | // dispatch(setLoadedFile(true)) 49 | // return response.data 50 | // }) 51 | // const [SQL, buf] = await Promise.all([sqlPromise, dataPromise]) 52 | // const db = new SQL.Database(new Uint8Array(buf)); 53 | function loadBinaryFile(path:any,success:any, error:any) { 54 | let xhr = new XMLHttpRequest(); 55 | xhr.open("GET", path, true); 56 | xhr.responseType = "arraybuffer"; 57 | xhr.onload = function() { 58 | let data = new Uint8Array(xhr.response); 59 | let arr = []; 60 | for(let i = 0; i != data.length; ++i) arr[i] = String.fromCharCode(data[i]); 61 | success(arr.join("")); 62 | }; 63 | xhr.send(); 64 | }; 65 | 66 | loadBinaryFile('./nw.sqlite', function(data:any){ 67 | let sqldb = new sqlPromise.Database(data); 68 | // Database is ready 69 | const database = drizzle(sqldb); 70 | const res = database.select().from(employees).all() 71 | setDb(database) 72 | 73 | }, function(error:any){ 74 | console.log(error) 75 | }); 76 | 77 | })().catch((e) => console.error(e)); 78 | 79 | }, []); 80 | 81 | 82 | return( 83 | <> 84 | 85 | 86 | 87 | } /> 88 | } /> 89 | } /> 90 | } /> 91 | } /> 92 | } /> 93 | } /> 94 | } /> 95 | } /> 96 | } /> 97 | } /> 98 | } /> 99 | } /> 100 | 101 | 102 | 103 | 104 | ) 105 | } 106 | 107 | export default App; 108 | -------------------------------------------------------------------------------- /src/Customer.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from 'react'; 2 | import styled from 'styled-components'; 3 | import {useNavigate, useParams} from 'react-router-dom'; 4 | import {useDispatch} from 'react-redux'; 5 | import Ballot from './icons/Ballot'; 6 | import {SQLJsDatabase} from "drizzle-orm/sql-js"; 7 | import {customers} from "./data/schema"; 8 | import {eq} from "drizzle-orm/expressions"; 9 | import {setQuery} from "./store/actions/login"; 10 | 11 | type CustomerType = { 12 | address: string; 13 | city: string; 14 | companyName: string; 15 | contactName: string; 16 | contactTitle: string; 17 | country: string; 18 | id: string; 19 | fax: string | null; 20 | phone: string; 21 | postalCode: string | null; 22 | region: string | null; 23 | }; 24 | type Props = { 25 | database: SQLJsDatabase 26 | } 27 | const Customer = ({database}: Props) => { 28 | const navigation = useNavigate(); 29 | const {id} = useParams(); 30 | const goBack = () => { 31 | navigation('/customers'); 32 | }; 33 | 34 | const [customerData, setCustomerData] = useState(null); 35 | const [queryArr, setQueryArr] = useState([]); 36 | const dispatch = useDispatch(); 37 | const [queryTime, setQueryTime] = useState([]); 38 | 39 | useEffect(() => { 40 | if (database && id) { 41 | const startTime = new Date().getTime(); 42 | const stmt = database.select().from(customers).where(eq(customers.id, id)).all(); 43 | const endTime = new Date().getTime(); 44 | setQueryTime([(endTime - startTime).toString()]); 45 | setQueryArr([...queryArr, database.select().from(customers).where(eq(customers.id, id)).toSQL().sql]); 46 | setCustomerData(stmt[0]); 47 | } 48 | }, [database, id]); 49 | 50 | useEffect(() => { 51 | if (customerData) { 52 | const obj = { 53 | query: queryArr, 54 | time: new Date().toISOString(), 55 | executeTime: queryTime, 56 | }; 57 | dispatch(setQuery(obj)); 58 | } 59 | }, [customerData, dispatch]); 60 | return ( 61 | 62 | {customerData ? ( 63 | <> 64 | 65 |
66 | 67 | Customer information 68 |
69 | 70 | 71 | 72 | 73 | Company Name 74 | 75 | 76 | {customerData.companyName} 77 | 78 | 79 | 80 | 81 | Contact Name 82 | 83 | 84 | {customerData.contactName} 85 | 86 | 87 | 88 | 89 | Contact Title 90 | 91 | 92 | {customerData.contactTitle} 93 | 94 | 95 | 96 | Address 97 | 98 | {customerData.address} 99 | 100 | 101 | 102 | City 103 | 104 | {customerData.city} 105 | 106 | 107 | 108 | 109 | 110 | Region 111 | 112 | {customerData.region} 113 | 114 | 115 | 116 | 117 | Postal Code 118 | 119 | 120 | {customerData.postalCode} 121 | 122 | 123 | 124 | Country 125 | 126 | {customerData.country} 127 | 128 | 129 | 130 | Phone 131 | 132 | {customerData.phone} 133 | 134 | 135 | 136 | 137 | 138 |
139 | Go back 140 |
141 | 142 | ) : ( 143 |
Loading customer...
144 | )} 145 |
146 | ); 147 | }; 148 | 149 | export default Customer; 150 | 151 | const Footer = styled.div` 152 | padding: 24px; 153 | background-color: #fff; 154 | border: 1px solid rgba(229, 231, 235, 1); 155 | border-top: none; 156 | `; 157 | 158 | const FooterButton = styled.div` 159 | color: white; 160 | background-color: #ef4444; 161 | border-radius: 0.25rem; 162 | width: 63px; 163 | padding: 12px 16px; 164 | display: flex; 165 | justify-content: center; 166 | align-items: center; 167 | cursor: pointer; 168 | `; 169 | 170 | const BodyContentLeftItem = styled.div` 171 | margin-bottom: 15px; 172 | `; 173 | const BodyContentLeftItemTitle = styled.div` 174 | font-size: 16px; 175 | font-weight: 700; 176 | color: black; 177 | margin-bottom: 10px; 178 | `; 179 | const BodyContentLeftItemValue = styled.div` 180 | color: black; 181 | `; 182 | 183 | const BodyContent = styled.div` 184 | padding: 24px; 185 | background-color: #fff; 186 | display: flex; 187 | `; 188 | 189 | const BodyContentLeft = styled.div` 190 | width: 50%; 191 | `; 192 | const BodyContentRight = styled.div` 193 | width: 50%; 194 | `; 195 | 196 | const Body = styled.div` 197 | border: 1px solid rgba(229, 231, 235, 1); 198 | `; 199 | 200 | const Wrapper = styled.div` 201 | padding: 24px; 202 | `; 203 | 204 | const Header = styled.div` 205 | display: flex; 206 | align-items: center; 207 | background-color: #fff; 208 | color: black; 209 | padding: 12px 16px; 210 | border-bottom: 1px solid rgba(229, 231, 235, 1); 211 | `; 212 | 213 | const HeaderTitle = styled.div` 214 | font-size: 16px; 215 | font-weight: 700; 216 | margin-left: 8px; 217 | `; 218 | -------------------------------------------------------------------------------- /src/CustomersPage.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from 'react'; 2 | import styled from 'styled-components'; 3 | import {Link} from 'react-router-dom'; 4 | import Svg from 'react-inlinesvg'; 5 | import {createAvatar} from '@dicebear/avatars'; 6 | import * as style from '@dicebear/avatars-initials-sprites'; 7 | import {useDispatch} from 'react-redux'; 8 | import HeaderArrowIcon from './icons/HeaderArrowIcon'; 9 | import {PaginationRow} from './OrdersPage'; 10 | import Pagination from './Pagination'; 11 | import {setQuery} from "./store/actions/login"; 12 | import {SQLJsDatabase} from "drizzle-orm/sql-js"; 13 | import {customers} from "./data/schema"; 14 | 15 | export type Customer = { 16 | address: string; 17 | city: string; 18 | companyName: string; 19 | contactName: string; 20 | contactTitle: string; 21 | country: string; 22 | id: string; 23 | fax: string | null; 24 | phone: string; 25 | postalCode: string | null; 26 | region: string | null; 27 | }; 28 | 29 | type Props = { 30 | database: SQLJsDatabase 31 | } 32 | 33 | const CustomersPage = ({database}: Props) => { 34 | const [customersData, setCustomersData] = useState([]); 35 | const [customersCount, setCustomersCount] = useState(null); 36 | const [currentPage, setCurrentPage] = useState(1); 37 | const dispatch = useDispatch(); 38 | const [queryArr, setQueryArr] = useState([]); 39 | const [queryTime, setQueryTime] = useState([]); 40 | 41 | useEffect(() => { 42 | if (database) { 43 | const startTime = new Date().getTime(); 44 | const stmt = database 45 | .select() 46 | .from(customers) 47 | .limit(20) 48 | .offset((currentPage - 1) * 20) 49 | .all(); 50 | const stmtCount = database.select().from(customers).all(); 51 | const endTime = new Date().getTime(); 52 | setQueryTime([(endTime - startTime).toString()]); 53 | setCustomersData(stmt); 54 | setCustomersCount(stmtCount.length); 55 | setQueryArr([...queryArr, database.select().from(customers).limit(20).offset((currentPage - 1) * 20).toSQL().sql]); 56 | } 57 | }, [currentPage]); 58 | 59 | useEffect(() => { 60 | if (customersData && customersData.length > 0) { 61 | const obj = { 62 | query: queryArr, 63 | time: new Date().toISOString(), 64 | executeTime: queryTime, 65 | }; 66 | dispatch(setQuery(obj)); 67 | } 68 | }, [customersData, dispatch]); 69 | return ( 70 | 71 | {customersData && customersCount ? ( 72 | <> 73 |
74 | Customers 75 | 76 |
77 | 78 | 79 | 80 | Company 81 | Contact 82 | Title 83 | City 84 | Country 85 | 86 | 87 | {customersData.map((customer: Customer, i) => { 88 | const svg = createAvatar(style, { 89 | seed: customer.contactName, 90 | // ... and other options 91 | }); 92 | if (i < 1) return; 93 | return ( 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | {customer.companyName} 103 | 104 | 105 | {customer.contactName} 106 | {customer.contactTitle} 107 | {customer.city} 108 | {customer.country} 109 | 110 | ); 111 | })} 112 | 113 | 114 | setCurrentPage(page)} 120 | /> 121 | 122 | 123 | Page: {currentPage} of {Math.ceil(customersCount / 20)} 124 | 125 | 126 | 127 |
128 | 129 | ) : ( 130 |
Loading Customers...
131 | )} 132 |
133 | ); 134 | }; 135 | 136 | export default CustomersPage; 137 | 138 | const PageCount = styled.div` 139 | font-size: 12.8px; 140 | `; 141 | 142 | const PaginationNumberWrapper = styled.div` 143 | cursor: pointer; 144 | display: flex; 145 | align-items: center; 146 | border: 1px solid rgba(243, 244, 246, 1); 147 | `; 148 | 149 | const PaginationNumber = styled.div<{ active: boolean }>` 150 | width: 7px; 151 | padding: 10px 16px; 152 | border: ${({active}) => 153 | active ? '1px solid rgba(209, 213, 219, 1)' : 'none'}; 154 | margin-right: 8px; 155 | `; 156 | 157 | const PaginationWrapper = styled.div` 158 | padding: 12px 24px; 159 | display: flex; 160 | align-items: center; 161 | justify-content: space-between; 162 | `; 163 | 164 | const Circle = styled.div` 165 | width: 24px; 166 | height: 24px; 167 | background-color: cadetblue; 168 | border-radius: 50%; 169 | overflow: hidden; 170 | color: white; 171 | font-size: 10px; 172 | display: flex; 173 | justify-content: center; 174 | align-items: center; 175 | `; 176 | 177 | const BodyIcon = styled.div` 178 | width: 5%; 179 | padding: 9px 12px; 180 | display: flex; 181 | align-items: center; 182 | justify-content: center; 183 | //border: 1px solid #000; 184 | `; 185 | const BodyCompany = styled.div` 186 | width: 30%; 187 | padding: 9px 12px; 188 | //border: 1px solid #000; 189 | `; 190 | const BodyContact = styled.div` 191 | width: 15%; 192 | padding: 9px 12px; 193 | //border: 1px solid #000; 194 | `; 195 | const BodyTitle = styled.div` 196 | width: 20%; 197 | padding: 9px 12px; 198 | //border: 1px solid #000; 199 | `; 200 | const BodyCity = styled.div` 201 | width: 15%; 202 | padding: 9px 12px; 203 | //border: 1px solid #000; 204 | `; 205 | const BodyCountry = styled.div` 206 | width: 13%; 207 | padding: 9px 12px; 208 | //border: 1px solid #000; 209 | `; 210 | 211 | const TableBody = styled.div` 212 | background-color: #fff; 213 | `; 214 | 215 | const TableRow = styled.div` 216 | width: 98%; 217 | display: flex; 218 | align-items: center; 219 | background-color: #f9fafb; 220 | 221 | &:hover { 222 | background-color: #f3f4f6; 223 | } 224 | 225 | &:hover:nth-child(even) { 226 | background-color: #f3f4f6; 227 | } 228 | 229 | &:nth-child(even) { 230 | background-color: #fff; 231 | } 232 | `; 233 | 234 | const Icon = styled.div` 235 | width: 5%; 236 | padding: 9px 12px; 237 | `; 238 | 239 | const Company = styled.div` 240 | width: 30%; 241 | font-size: 16px; 242 | font-weight: 700; 243 | padding: 9px 12px; 244 | `; 245 | const Contact = styled.div` 246 | width: 15%; 247 | font-size: 16px; 248 | padding: 9px 12px; 249 | font-weight: 700; 250 | `; 251 | const Title = styled.div` 252 | width: 20%; 253 | font-size: 16px; 254 | font-weight: 700; 255 | padding: 9px 12px; 256 | `; 257 | const City = styled.div` 258 | width: 15%; 259 | font-size: 16px; 260 | font-weight: 700; 261 | padding: 9px 12px; 262 | `; 263 | const Country = styled.div` 264 | width: 15%; 265 | font-size: 16px; 266 | font-weight: 700; 267 | padding: 9px 12px; 268 | `; 269 | 270 | const Table = styled.div``; 271 | 272 | const TableHeader = styled.div` 273 | width: 100%; 274 | display: flex; 275 | align-items: center; 276 | background-color: #fff; 277 | `; 278 | 279 | const Wrapper = styled.div` 280 | color: black; 281 | padding: 24px; 282 | border: 1px solid rgba(243, 244, 246, 1);; 283 | `; 284 | 285 | const Header = styled.div` 286 | padding: 12px 16px; 287 | display: flex; 288 | align-items: center; 289 | justify-content: space-between; 290 | background-color: #fff; 291 | border-bottom: 1px solid rgba(243, 244, 246, 1);; 292 | `; 293 | 294 | const HeaderTitle = styled.div` 295 | font-size: 16px; 296 | font-weight: 700; 297 | `; 298 | -------------------------------------------------------------------------------- /src/DashboardPage.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import styled from 'styled-components'; 3 | import { useSelector } from 'react-redux'; 4 | import {selectQuery} from "./store/selectors/auth"; 5 | 6 | const DashboardPage = () => { 7 | const query = useSelector(selectQuery); 8 | const [countSelect, setCountSelect] = useState(0); 9 | const [countSelectWhere, setCountSelectWhere] = useState(0); 10 | const [countSelectLeft, setCountSelectLeft] = useState(0); 11 | 12 | // find all left join in query 13 | useEffect(() => { 14 | query?.map((item: any) => { 15 | item.query.map((queryArr: any) => { 16 | if (queryArr.toLowerCase().includes('left join')) { 17 | setCountSelectLeft((prev) => prev + 1); 18 | } 19 | if (queryArr.toLowerCase().includes('where')) { 20 | setCountSelectWhere((prev) => prev + 1); 21 | } 22 | if (queryArr.toLowerCase().includes('select')) { 23 | setCountSelect((prev) => prev + 1); 24 | } 25 | }); 26 | }); 27 | }, [query]); 28 | 29 | return ( 30 | 31 | 32 | 33 | 34 | SQL Metrics 35 | Query count: {query?.length} 36 | Results count: {query?.length} 37 | # SELECT: {countSelect} 38 | # SELECT WHERE: {countSelectWhere} 39 | # SELECT LEFT JOIN: {countSelectLeft} 40 | 41 | 42 | 43 | Activity log 44 | 45 | Explore the app and see metrics here 46 | 47 | 48 | {query?.map((item: any) => 49 | item.query.map((itemQuery: any) => { 50 | return ( 51 | 52 |
53 | {item.time} 54 | , SQL,  55 | 56 | {item.executeTime ? item.executeTime[0] : ''}ms 57 | 58 |
59 | {itemQuery} 60 |
61 | ); 62 | }) 63 | )} 64 |
65 |
66 |
67 | ); 68 | }; 69 | 70 | export default DashboardPage; 71 | 72 | const LogString = styled.div` 73 | font-size: 14px; 74 | //line-height: 22px; 75 | line-height: 1.25rem; 76 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 77 | Liberation Mono, Courier New, monospace; 78 | `; 79 | 80 | const Log = styled.div` 81 | padding-top: 8px; 82 | `; 83 | 84 | const LogsInfo = styled.div` 85 | font-size: 12px; 86 | color: #9ca3af; 87 | `; 88 | 89 | const MainContentLogs = styled.div``; 90 | 91 | const MainContent = styled.div` 92 | padding-top: 24px; 93 | `; 94 | const MainContentTitle = styled.div` 95 | font-size: 20px; 96 | line-height: 30px; 97 | `; 98 | 99 | const MainContentSubTitle = styled.div` 100 | font-size: 12px; 101 | `; 102 | 103 | const Title = styled.div` 104 | font-size: 20px; 105 | line-height: 28px; 106 | `; 107 | const SubTitle = styled.div` 108 | font-size: 14px; 109 | line-height: 20px; 110 | `; 111 | 112 | const Wrapper = styled.div` 113 | padding: 48px; 114 | `; 115 | const TopContentWrapper = styled.div` 116 | display: flex; 117 | align-content: center; 118 | width: 100%; 119 | justify-content: space-between; 120 | `; 121 | const TopContentLeft = styled.div` 122 | width: 49%; 123 | `; 124 | const TopContentRight = styled.div` 125 | width: 49%; 126 | `; 127 | -------------------------------------------------------------------------------- /src/Documentation.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import ReactMarkdown from 'react-markdown'; 4 | import remarkGfm from 'remark-gfm'; 5 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; 6 | import { materialDark } from 'react-syntax-highlighter/dist/esm/styles/prism'; 7 | 8 | const Documentation = () => { 9 | const src = ` 10 | ### The Repository 11 | 12 | This repository stores a desktop copy of [Northwind Traders](https://northwind.d1sql.com/dash) made using electron, react, typescript. 13 | 14 | ### Install 15 | 16 | Clone the repo and install dependencies: 17 | 18 | \`\`\`bash 19 | git clone --depth 1 --branch main https://github.com/Nagrik/electron.git your-project-name 20 | cd your-project-name 21 | npm install 22 | \`\`\` 23 | 24 | ### Starting Development 25 | 26 | Start the app in the \`dev\` environment: 27 | 28 | \`\`\`bash 29 | npm start 30 | \`\`\` 31 | 32 | 39 | 40 | ### Docs 41 | 42 | You can use existing backend for this application with existing database or provide your own database either adding link to it in **.env** or setting link in dashboard page of the application. 43 | 44 | #### Own database 45 | 46 | You can use database with our backend **only if** your database is _PostgreSQL_ and have following tables 47 | 48 | \`\`\`sql 49 | CREATE TABLE IF NOT EXISTS Categories ( 50 | \t"CategoryID" INT PRIMARY KEY, 51 | \t"CategoryName" character varying(100), 52 | \t"Description" character varying(100) 53 | ); 54 | 55 | CREATE TABLE IF NOT EXISTS Customers ( 56 | \t"CustomerID" character varying(20) PRIMARY KEY, 57 | \t"CompanyName" character varying(100), 58 | \t"ContactName" character varying(100), 59 | \t"ContactTitle" character varying(100), 60 | \t"Address" character varying(100), 61 | \t"City" character varying(100), 62 | \t"Region" character varying(100), 63 | \t"PostalCode" character varying(100), 64 | \t"Country" character varying(100), 65 | \t"Phone" character varying(100), 66 | \t"Fax" character varying(100) 67 | ); 68 | 69 | CREATE TABLE IF NOT EXISTS EmployeeTerritories ( 70 | \t"EmployeeID" INT, 71 | \t"TerritoryID" INT 72 | ); 73 | 74 | CREATE TABLE IF NOT EXISTS Employees ( 75 | \t"EmployeeID" INT PRIMARY KEY, 76 | \t"LastName" character varying(100), 77 | \t"FirstName" character varying(100), 78 | \t"Title" character varying(100), 79 | \t"TitleOfCourtesy" character varying(100), 80 | \t"BirthDate" character varying(100), 81 | \t"HireDate" character varying(100), 82 | \t"Address" character varying(100), 83 | \t"City" character varying(100), 84 | \t"Region" character varying(100), 85 | \t"PostalCode" character varying(100), 86 | \t"Country" character varying(100), 87 | \t"HomePhone" character varying(100), 88 | \t"Extension" INT, 89 | \t"Notes" character varying(500), 90 | \t"ReportsTo" INT 91 | ); 92 | 93 | CREATE TABLE IF NOT EXISTS OrderDetails ( 94 | \t"id" SERIAL PRIMARY KEY, 95 | \t"OrderID" INT, 96 | \t"ProductID" INT, 97 | \t"UnitPrice" numeric, 98 | \t"Quantity" INT, 99 | \t"Discount" numeric 100 | ); 101 | 102 | CREATE TABLE IF NOT EXISTS Orders ( 103 | \t"OrderID" INT PRIMARY KEY, 104 | \t"CustomerID" character varying(5), 105 | \t"EmployeeID" INT, 106 | \t"OrderDate" character varying(100), 107 | \t"RequiredDate" character varying(100), 108 | \t"ShippedDate" character varying(100), 109 | \t"ShipVia" INT, 110 | \t"Freight" numeric, 111 | \t"ShipName" character varying(100), 112 | \t"ShipAddress" character varying(100), 113 | \t"ShipCity" character varying(100), 114 | \t"ShipRegion" character varying(100), 115 | \t"ShipPostalCode" character varying(100), 116 | \t"ShipCountry" character varying(100) 117 | ); 118 | 119 | CREATE TABLE IF NOT EXISTS Products ( 120 | \t"ProductID" INT PRIMARY KEY, 121 | \t"ProductName" character varying(100), 122 | \t"SupplierID" INT, 123 | \t"CategoryID" INT, 124 | \t"QuantityPerUnit" character varying(100), 125 | \t"UnitPrice" numeric, 126 | \t"UnitsInStock" INT, 127 | \t"UnitsOnOrder" INT, 128 | \t"ReorderLevel" INT, 129 | \t"Discontinued" INT 130 | ); 131 | 132 | CREATE TABLE IF NOT EXISTS Regions ( 133 | \t"RegionID" INT PRIMARY KEY, 134 | \t"RegionDescription" character varying(30) 135 | ); 136 | 137 | CREATE TABLE IF NOT EXISTS Shippers ( 138 | \t"ShipperID" INT PRIMARY KEY, 139 | \t"CompanyName" character varying(100), 140 | \t"Phone" character varying(50) 141 | ); 142 | 143 | CREATE TABLE IF NOT EXISTS Suppliers ( 144 | \t"SupplierID" INT PRIMARY KEY, 145 | \t"CompanyName" character varying(100), 146 | \t"ContactName" character varying(100), 147 | \t"ContactTitle" character varying(100), 148 | \t"Address" character varying(100), 149 | \t"City" character varying(100), 150 | \t"Region" character varying(100), 151 | \t"PostalCode" character varying(100), 152 | \t"Country" character varying(100), 153 | \t"Phone" character varying(100), 154 | \t"Fax" character varying(100), 155 | \t"HomePage" character varying(100) 156 | ); 157 | 158 | CREATE TABLE IF NOT EXISTS Territories ( 159 | \t"TerritoryID" character varying(10) PRIMARY KEY, 160 | \t"TerritoryDescription" character varying(50), 161 | \t"RegionID" INT 162 | ); 163 | 164 | ALTER TABLE customers 165 | ADD COLUMN customers_with_rankings tsvector; 166 | UPDATE customers SET customers_with_rankings = 167 | setweight(to_tsvector("CustomerID"), 'AA') || 168 | setweight(to_tsvector("CompanyName"), 'AB') || 169 | setweight(to_tsvector("ContactName"), 'AC') || 170 | setweight(to_tsvector("ContactTitle"), 'AD') || 171 | setweight(to_tsvector("Address"), 'BA'); 172 | 173 | ALTER TABLE products 174 | ADD COLUMN products_ranking tsvector; 175 | UPDATE products SET products_ranking = to_tsvector("ProductName"); 176 | \`\`\` 177 | 178 | #### Own API 179 | 180 | You can use your own API if has following endpoints: 181 | 182 | **queries** field is present on every response 183 | 184 | \`\`\`TEXT 185 | queries: Array[{ 186 | executionTime: number, 187 | select: number, 188 | selectWhere: number, 189 | selectJoin: number, 190 | query: string // SQL query executed to get response data 191 | }], 192 | \`\`\` 193 | ### Suppliers Page: 194 | \`\`\`TEXT 195 | GET http://your.own.api/suppliers?page=1 HTTP/1.1 196 | 197 | Response { 198 | queries, 199 | data: Array[ 200 | { count: number }, 201 | ...{ 202 | SupplierID: number; 203 | CompanyName: string; 204 | ContactName: string; 205 | ContactTitle: string; 206 | Address: string; 207 | City: string; 208 | Region: string; 209 | PostalCode: string; 210 | Country: string; 211 | Phone: string; 212 | Fax: string; 213 | HomePage: string; 214 | } 215 | ] 216 | } 217 | \`\`\` 218 | ### Supplier: 219 | \`\`\`TEXT 220 | GET http://your.own.api/supplier?id=1 HTTP/1.1 221 | 222 | Response { 223 | queries, 224 | data: [{ 225 | SupplierID: number; 226 | CompanyName: string; 227 | ContactName: string; 228 | ContactTitle: string; 229 | Address: string; 230 | City: string; 231 | Region: string; 232 | PostalCode: string; 233 | Country: string; 234 | Phone: string; 235 | Fax: string; 236 | HomePage: string; 237 | }] 238 | } 239 | 240 | \`\`\` 241 | ### Customers Page: 242 | \`\`\`TEXT 243 | GET http://your.own.api/customers?page=1 HTTP/1.1 244 | 245 | Response { 246 | queries, 247 | data: Array[ 248 | { count: number }, 249 | ...{ 250 | Address: string; 251 | City: string; 252 | CompanyName: string; 253 | ContactName: string; 254 | ContactTitle: string; 255 | Country: string; 256 | CustomerID: string; 257 | Fax: string; 258 | Phone: string; 259 | PostalCode: string; 260 | Region: string; 261 | } 262 | ] 263 | } 264 | \`\`\` 265 | ### Customer: 266 | \`\`\`TEXT 267 | GET http://your.own.api/customer?id=ALFKI HTTP/1.1 268 | 269 | Response { 270 | queries, 271 | data: [{ 272 | Address: string; 273 | City: string; 274 | CompanyName: string; 275 | ContactName: string; 276 | ContactTitle: string; 277 | Country: string; 278 | CustomerID: string; 279 | Fax: string; 280 | Phone: string; 281 | PostalCode: string; 282 | Region: string; 283 | }] 284 | } 285 | \`\`\` 286 | ### Search Page (search by customer name): 287 | \`\`\`TEXT 288 | GET http://your.own.api/searchCustomer?search=Alfred HTTP/1.1 289 | 290 | Response { 291 | queries, 292 | data: Array[...{ 293 | Address: string; 294 | City: string; 295 | CompanyName: string; 296 | ContactName: string; 297 | ContactTitle: string; 298 | Country: string; 299 | CustomerID: string; 300 | Fax: string; 301 | Phone: string; 302 | PostalCode: string; 303 | Region: string; 304 | }] 305 | } 306 | \`\`\` 307 | 308 | ### Products Page: 309 | \`\`\`TEXT 310 | GET http://your.own.api/products?page=1 HTTP/1.1 311 | 312 | Response { 313 | queries, 314 | data: Array[ 315 | { count: number }, 316 | ...{ 317 | CategoryID: number; 318 | Discontinued: number; 319 | ProductID: number; 320 | ProductName: string; 321 | QuantityPerUnit: string; 322 | ReorderLevel: number; 323 | Supplier: string; 324 | SupplierID: number; 325 | UnitPrice: number; 326 | UnitsInStock: number; 327 | UnitsOnOrder: number; 328 | } 329 | ] 330 | } 331 | \`\`\` 332 | 333 | ### Product: 334 | \`\`\`TEXT 335 | GET http://your.own.api/product?id=1 HTTP/1.1 336 | 337 | Response { 338 | queries, 339 | data: [{ 340 | CategoryID: number; 341 | Discontinued: number; 342 | ProductID: number; 343 | ProductName: string; 344 | QuantityPerUnit: string; 345 | ReorderLevel: number; 346 | Supplier: string; 347 | SupplierID: number; 348 | UnitPrice: number; 349 | UnitsInStock: number; 350 | UnitsOnOrder: number; 351 | }] 352 | } 353 | \`\`\` 354 | 355 | ### Search Page (search by product name): 356 | 357 | \`\`\`TEXT 358 | GET http://your.own.api/searchProduct?search=Chai HTTP/1.1 359 | 360 | Response { 361 | queries, 362 | data: Array[...{ 363 | CategoryID: number; 364 | Discontinued: number; 365 | ProductID: number; 366 | ProductName: string; 367 | QuantityPerUnit: string; 368 | ReorderLevel: number; 369 | Supplier: string; 370 | SupplierID: number; 371 | UnitPrice: number; 372 | UnitsInStock: number; 373 | UnitsOnOrder: number; 374 | }] 375 | } 376 | \`\`\` 377 | 378 | ### Employees Page: 379 | \`\`\`TEXT 380 | GET http://your.own.api/employees?page=1 HTTP/1.1 381 | 382 | Response { 383 | queries, 384 | data: Array[ 385 | { count: number }, 386 | ...{ 387 | Address: string; 388 | BirthDate: string; 389 | City: string; 390 | Country: string; 391 | EmployeeID: number; 392 | Extension: number; 393 | FirstName: string; 394 | HireDate: string; 395 | HomePhone: string; 396 | LastName: string; 397 | Notes: string; 398 | PostalCode: string; 399 | Region: string; 400 | ReportsTo: number; 401 | ReportsToName: string; 402 | Title: string; 403 | TitleOfCourtesy: string; 404 | } 405 | ] 406 | } 407 | \`\`\` 408 | 409 | ### Employee: 410 | 411 | \`\`\`TEXT 412 | GET http://your.own.api/employee?id=1 HTTP/1.1 413 | 414 | Response { 415 | queries, 416 | data: [{ 417 | Address: string; 418 | BirthDate: string; 419 | City: string; 420 | Country: string; 421 | EmployeeID: number; 422 | Extension: number; 423 | FirstName: string; 424 | HireDate: string; 425 | HomePhone: string; 426 | LastName: string; 427 | Notes: string; 428 | PostalCode: string; 429 | Region: string; 430 | ReportsTo: number; 431 | ReportsToName: string; 432 | Title: string; 433 | TitleOfCourtesy: string; 434 | }] 435 | } 436 | \`\`\` 437 | 438 | ### Orders Page: 439 | \`\`\`TEXT 440 | GET http://your.own.api/orders?page=1 HTTP/1.1 441 | 442 | Response { 443 | queries, 444 | data: Array[{ 445 | { count: number}, 446 | ...{ 447 | CustomerID: string; 448 | EmployeeID: number; 449 | Freight: number; 450 | OrderDate: string; 451 | OrderID: number; 452 | RequiredDate: string; 453 | ShipAddress: string; 454 | ShipCity: string; 455 | ShipCountry: string; 456 | ShipName: string; 457 | ShipPostalCode: string; 458 | ShipRegion: string; 459 | ShipVia: string; 460 | ShippedDate: string; 461 | TotalPrice: number; 462 | TotalQuantity: number; 463 | TotalDiscount: number; 464 | TotalProducts: number; 465 | } 466 | }] 467 | } 468 | \`\`\` 469 | 470 | ### Order Page: 471 | \`\`\`TEXT 472 | GET http://your.own.api/order?id=10248 HTTP/1.1 473 | 474 | Response { 475 | queries, 476 | data: [{ 477 | CustomerID: string; 478 | EmployeeID: number; 479 | Freight: number; 480 | OrderDate: string; 481 | OrderID: number; 482 | RequiredDate: string; 483 | ShipAddress: string; 484 | ShipCity: string; 485 | ShipCountry: string; 486 | ShipName: string; 487 | ShipPostalCode: string; 488 | ShipRegion: string; 489 | ShipVia: string; 490 | ShippedDate: string; 491 | TotalPrice: number; 492 | TotalQuantity: number; 493 | TotalDiscount: number; 494 | TotalProducts: number; 495 | Products: Array[...{ 496 | CategoryID: number; 497 | Discontinued: number; 498 | Discount: string; 499 | OrderID: number; 500 | OrderUnitPrice: string; 501 | ProductID: number; 502 | ProductName: string; 503 | ProductUnitPrice: string; 504 | Quantity: number; 505 | QuantityPerUnit: string; 506 | ReorderLevel: number; 507 | SupplierID: number; 508 | UnitsInStock: number; 509 | UnitsOnOrder: number; 510 | }]; 511 | }] 512 | } 513 | \`\`\` 514 | 515 | ### License 516 | 517 | MIT © [Electron React Boilerplate](https://github.com/electron-react-boilerplate)`; 518 | return ( 519 | 520 | 534 | ) : ( 535 | 536 | {children} 537 | 538 | ); 539 | }, 540 | }} 541 | /> 542 | 543 | ); 544 | }; 545 | 546 | export default Documentation; 547 | 548 | const Wrapper = styled.div` 549 | padding: 24px 48px; 550 | `; 551 | -------------------------------------------------------------------------------- /src/Employees.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import styled from 'styled-components'; 3 | import { Link, useNavigate, useParams } from 'react-router-dom'; 4 | import { useDispatch } from 'react-redux'; 5 | import Ballot from './icons/Ballot'; 6 | import {SQLJsDatabase} from "drizzle-orm/sql-js"; 7 | import {Employee} from "./EmployeesPage"; 8 | import {setQuery} from "./store/actions/login"; 9 | import {eq} from "drizzle-orm/expressions"; 10 | import {employees} from "./data/schema"; 11 | 12 | type Props = { 13 | database: SQLJsDatabase; 14 | } 15 | 16 | const Employees = ({database}: Props) => { 17 | const navigation = useNavigate(); 18 | const { id } = useParams(); 19 | const goBack = () => { 20 | navigation('/employees'); 21 | }; 22 | const [employeesData, setEmployeesData] = useState(null); 23 | const [reports, setReports] = useState(null); 24 | const [queryArr, setQueryArr] = useState([]); 25 | const [queryTime, setQueryTime] = useState([]); 26 | 27 | const dispatch = useDispatch(); 28 | 29 | useEffect(() => { 30 | if (employeesData) { 31 | const obj = { 32 | query: queryArr, 33 | time: new Date().toISOString(), 34 | executeTime: queryTime, 35 | }; 36 | dispatch(setQuery(obj)); 37 | } 38 | }, [employeesData, dispatch, queryArr]); 39 | 40 | useEffect(() => { 41 | if (database && id) { 42 | const startTime = new Date().getTime(); 43 | const stmt = database.select().from(employees).where(eq(employees.id, Number(id))).all(); 44 | const endTime = new Date().getTime(); 45 | setQueryArr([...queryArr, database.select().from(employees).where(eq(employees.id, Number(id))).toSQL().sql ]); 46 | setQueryTime([(endTime - startTime).toString()]); 47 | // @ts-ignore 48 | setEmployeesData(stmt[0]); 49 | } 50 | }, [id,database]); 51 | return ( 52 | 53 | {employeesData ? ( 54 | <> 55 | 56 |
57 | 58 | Product information 59 |
60 | 61 | 62 | 63 | Name 64 | 65 | {employeesData.firstName} {employeesData.lastName} 66 | 67 | 68 | 69 | Title 70 | 71 | {employeesData.title} 72 | 73 | 74 | 75 | 76 | Title Of Courtesy 77 | 78 | 79 | {employeesData.titleOfCourtesy} 80 | 81 | 82 | 83 | 84 | Birth Date 85 | 86 | 87 | {/* {employeesData.birthDate} */} 88 | 89 | 90 | 91 | Hire Date 92 | 93 | {/* {employeesData.hireDate} */} 94 | 95 | 96 | 97 | Address 98 | 99 | {employeesData.address} 100 | 101 | 102 | 103 | City 104 | 105 | {employeesData.city} 106 | 107 | 108 | 109 | 110 | 111 | 112 | Postal Code 113 | 114 | 115 | {employeesData.postalCode} 116 | 117 | 118 | 119 | Country 120 | 121 | {employeesData.country} 122 | 123 | 124 | 125 | 126 | Home Phone 127 | 128 | 129 | {employeesData.homePhone} 130 | 131 | 132 | 133 | Extension 134 | 135 | {employeesData.extension} 136 | 137 | 138 | 139 | Notes 140 | 141 | {employeesData.notes} 142 | 143 | 144 | 145 | 146 | Reports To 147 | 148 | 149 | 150 | {reports 151 | ? `${reports.firstName} ${reports.lastName}` 152 | : `${employeesData.firstName} ${employeesData.lastName}`} 153 | 154 | 155 | 156 | 157 | 158 | 159 |
160 | Go back 161 |
162 | 163 | ) : ( 164 |
Loading employees...
165 | )} 166 |
167 | ); 168 | }; 169 | 170 | export default Employees; 171 | 172 | const Footer = styled.div` 173 | padding: 24px; 174 | background-color: #fff; 175 | border: 1px solid rgba(229, 231, 235, 1); 176 | border-top: none; 177 | `; 178 | 179 | const FooterButton = styled.div` 180 | color: white; 181 | background-color: #ef4444; 182 | border-radius: 0.25rem; 183 | width: 63px; 184 | padding: 12px 16px; 185 | display: flex; 186 | justify-content: center; 187 | align-items: center; 188 | cursor: pointer; 189 | `; 190 | 191 | const BodyContentLeftItem = styled.div` 192 | margin-bottom: 15px; 193 | `; 194 | const BodyContentLeftItemTitle = styled.div` 195 | font-size: 16px; 196 | font-weight: 700; 197 | color: black; 198 | margin-bottom: 10px; 199 | `; 200 | const BodyContentLeftItemValue = styled.div` 201 | color: black; 202 | `; 203 | 204 | const BodyContent = styled.div` 205 | padding: 24px; 206 | background-color: #fff; 207 | display: flex; 208 | `; 209 | 210 | const BodyContentLeft = styled.div` 211 | width: 50%; 212 | `; 213 | const BodyContentRight = styled.div` 214 | width: 50%; 215 | `; 216 | 217 | const Body = styled.div` 218 | border: 1px solid rgba(229, 231, 235, 1); 219 | `; 220 | 221 | const Wrapper = styled.div` 222 | padding: 24px; 223 | `; 224 | 225 | const Header = styled.div` 226 | display: flex; 227 | align-items: center; 228 | background-color: #fff; 229 | color: black; 230 | padding: 12px 16px; 231 | border-bottom: 1px solid rgba(229, 231, 235, 1); 232 | `; 233 | 234 | const HeaderTitle = styled.div` 235 | font-size: 16px; 236 | font-weight: 700; 237 | margin-left: 8px; 238 | `; 239 | -------------------------------------------------------------------------------- /src/EmployeesPage.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from 'react'; 2 | import styled from 'styled-components'; 3 | import {useDispatch, useSelector} from 'react-redux'; 4 | import {Link} from 'react-router-dom'; 5 | import * as style from '@dicebear/avatars-initials-sprites'; 6 | import {createAvatar} from '@dicebear/avatars'; 7 | import Svg from 'react-inlinesvg'; 8 | import HeaderArrowIcon from './icons/HeaderArrowIcon'; 9 | import {PaginationRow} from './OrdersPage'; 10 | import Pagination from './Pagination'; 11 | import {setQuery} from "./store/actions/login"; 12 | import {selectQuery} from "./store/selectors/auth"; 13 | import {SQLJsDatabase} from "drizzle-orm/sql-js"; 14 | import {employees} from "./data/schema"; 15 | 16 | type Page = { 17 | EmployeeID: number; 18 | LastName: string; 19 | FirstName: string; 20 | Title: string; 21 | TitleOfCourtesy: string; 22 | BirthDate: string; 23 | HireDate: string; 24 | Address: string; 25 | City: string; 26 | Region: string; 27 | PostalCode: string; 28 | Country: string; 29 | HomePhone: string; 30 | Extension: number; 31 | Notes: string; 32 | ReportsTo: number; 33 | }; 34 | 35 | type Product = { 36 | queries: [ 37 | { 38 | query: string; 39 | metrics: { 40 | select: number; 41 | selectWhere: number; 42 | selectJoin: number; 43 | executionTime: number; 44 | }; 45 | }, 46 | { 47 | query: string; 48 | metrics: { 49 | select: number; 50 | selectWhere: number; 51 | selectJoin: number; 52 | executionTime: number; 53 | }; 54 | } 55 | ]; 56 | count: number; 57 | page: Array; 58 | }; 59 | 60 | export type Employee = { 61 | address: string; 62 | birthDate: string; 63 | city: string; 64 | country: string; 65 | id: number; 66 | extension: number; 67 | firstName: string; 68 | hireDate: string; 69 | homePhone: string; 70 | lastName: string; 71 | notes: string; 72 | postalCode: string; 73 | region: string; 74 | reportsTo: number; 75 | reportsToName: string; 76 | title: string; 77 | titleOfCourtesy: string; 78 | }; 79 | 80 | type Props = { 81 | database: SQLJsDatabase 82 | } 83 | 84 | const EmployeesPage = ({database}: Props) => { 85 | const [employeesData, setEmployeesData] = useState(null); 86 | const [employeesCount, setEmployeesCount] = useState(null); 87 | const [currentPage, setCurrentPage] = useState(1); 88 | const dispatch = useDispatch(); 89 | const [queryArr, setQueryArr] = useState([]); 90 | const [queryTime, setQueryTime] = useState([]); 91 | 92 | 93 | useEffect(() => { 94 | if (database) { 95 | const startTime = new Date().getTime(); 96 | const stmt = database 97 | .select() 98 | .from(employees) 99 | .limit(20) 100 | .offset((currentPage - 1) * 20) 101 | .all(); 102 | const stmtCount = database.select().from(employees).all(); 103 | const endTime = new Date().getTime(); 104 | setQueryTime([(endTime - startTime).toString()]); 105 | // @ts-ignore 106 | setEmployeesData(stmt); 107 | setQueryArr([...queryArr, database.select().from(employees).limit(20).offset((currentPage - 1) * 20).toSQL().sql]); 108 | // @ts-ignore 109 | setEmployeesCount(stmtCount.length); 110 | } 111 | }, [currentPage]); 112 | 113 | useEffect(() => { 114 | if (employeesData && employeesData.length > 0) { 115 | const obj = { 116 | query: queryArr, 117 | time: new Date().toISOString(), 118 | executeTime: queryTime, 119 | }; 120 | dispatch(setQuery(obj)); 121 | } 122 | }, [employeesData, dispatch]); 123 | return ( 124 | 125 | {employeesData && employeesCount ? ( 126 | <> 127 |
128 | Products 129 | 130 |
131 | 132 | 133 | 134 | Name 135 | Title 136 | City 137 | Phone 138 | Country 139 | 140 | 141 | {employeesData.map((product: Employee, i: number) => { 142 | const svg = createAvatar(style, { 143 | seed: product.firstName, 144 | }); 145 | if (i < 1) return; 146 | return ( 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | {product.firstName} {product.lastName} 156 | 157 | 158 | {product.title} 159 | {product.city} 160 | {product.homePhone} 161 | {product.country} 162 | 163 | ); 164 | })} 165 | 166 | 167 | setCurrentPage(page)} 173 | /> 174 | 175 | 176 | Page: {currentPage} of {Math.ceil(employeesCount / 20)} 177 | 178 | 179 | 180 |
181 | 182 | ) : ( 183 |
Loading employees...
184 | )} 185 |
186 | ); 187 | }; 188 | 189 | export default EmployeesPage; 190 | 191 | const Circle = styled.div` 192 | width: 24px; 193 | height: 24px; 194 | overflow: hidden; 195 | border-radius: 50%; 196 | color: white; 197 | font-size: 10px; 198 | display: flex; 199 | justify-content: center; 200 | align-items: center; 201 | `; 202 | 203 | const BodyIcon = styled.div` 204 | width: 5%; 205 | padding: 9px 12px; 206 | display: flex; 207 | align-items: center; 208 | justify-content: center; 209 | //border: 1px solid #000; 210 | `; 211 | const PageCount = styled.div` 212 | font-size: 12.8px; 213 | `; 214 | 215 | const PaginationNumberWrapper = styled.div` 216 | cursor: pointer; 217 | display: flex; 218 | align-items: center; 219 | border: 1px solid rgba(243, 244, 246, 1); 220 | `; 221 | 222 | const PaginationNumber = styled.div<{ active: boolean }>` 223 | width: 7px; 224 | padding: 10px 16px; 225 | border: ${({active}) => 226 | active ? '1px solid rgba(209, 213, 219, 1)' : 'none'}; 227 | margin-right: 8px; 228 | `; 229 | 230 | const PaginationWrapper = styled.div` 231 | padding: 12px 24px; 232 | display: flex; 233 | align-items: center; 234 | justify-content: space-between; 235 | `; 236 | 237 | const BodyCompany = styled.div` 238 | width: 25%; 239 | padding: 9px 12px; 240 | //border: 1px solid #000; 241 | `; 242 | const BodyContact = styled.div` 243 | width: 25%; 244 | padding: 9px 12px; 245 | //border: 1px solid #000; 246 | `; 247 | const BodyTitle = styled.div` 248 | width: 20%; 249 | padding: 9px 12px; 250 | //border: 1px solid #000; 251 | `; 252 | const BodyCity = styled.div` 253 | width: 20%; 254 | padding: 9px 12px; 255 | //border: 1px solid #000; 256 | `; 257 | const BodyCountry = styled.div` 258 | width: 8%; 259 | padding: 9px 12px; 260 | //border: 1px solid #000; 261 | `; 262 | 263 | const TableBody = styled.div` 264 | background-color: #fff; 265 | `; 266 | 267 | const TableRow = styled.div` 268 | width: 98%; 269 | display: flex; 270 | align-items: center; 271 | background-color: #f9fafb; 272 | 273 | &:hover { 274 | background-color: #f3f4f6; 275 | } 276 | 277 | &:hover:nth-child(even) { 278 | background-color: #f3f4f6; 279 | } 280 | 281 | &:nth-child(even) { 282 | background-color: #fff; 283 | } 284 | `; 285 | 286 | const Icon = styled.div` 287 | width: 5%; 288 | padding: 9px 12px; 289 | `; 290 | 291 | const Company = styled.div` 292 | width: 25%; 293 | font-size: 16px; 294 | font-weight: 700; 295 | padding: 9px 12px; 296 | `; 297 | const Contact = styled.div` 298 | width: 25%; 299 | font-size: 16px; 300 | padding: 9px 12px; 301 | font-weight: 700; 302 | `; 303 | const Title = styled.div` 304 | width: 20%; 305 | font-size: 16px; 306 | font-weight: 700; 307 | padding: 9px 12px; 308 | `; 309 | const City = styled.div` 310 | width: 20%; 311 | font-size: 16px; 312 | font-weight: 700; 313 | padding: 9px 12px; 314 | `; 315 | const Country = styled.div` 316 | width: 10%; 317 | font-size: 16px; 318 | font-weight: 700; 319 | padding: 9px 12px; 320 | `; 321 | 322 | const Table = styled.div``; 323 | 324 | const TableHeader = styled.div` 325 | width: 100%; 326 | display: flex; 327 | align-items: center; 328 | background-color: #fff; 329 | `; 330 | 331 | const Wrapper = styled.div` 332 | color: black; 333 | padding: 24px; 334 | border: 1px solid rgba(243, 244, 246, 1);; 335 | `; 336 | 337 | const Header = styled.div` 338 | padding: 12px 16px; 339 | display: flex; 340 | align-items: center; 341 | justify-content: space-between; 342 | background-color: #fff; 343 | border-bottom: 1px solid rgba(243, 244, 246, 1);; 344 | `; 345 | 346 | const HeaderTitle = styled.div` 347 | font-size: 16px; 348 | font-weight: 700; 349 | `; 350 | 351 | function setQueryResponseDashboard(obj: { query: any; time: string }): any { 352 | throw new Error('Function not implemented.'); 353 | } 354 | -------------------------------------------------------------------------------- /src/LeftMenu.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import styled from 'styled-components'; 3 | import { useLocation } from 'react-router-dom'; 4 | import MenuItem from './MenuItem'; 5 | import HomeIcon from './icons/HomeIcon'; 6 | import DashboardIcon from './icons/DashboardIcon'; 7 | import SuppliersIcon from './icons/SuppliersIcon'; 8 | import Cart from './icons/Cart'; 9 | import ProductsIcon from './icons/ProductsIcon'; 10 | import EmployeesIcon from './icons/EmployeesIcon'; 11 | import CustomersIcon from './icons/CustomersIcon'; 12 | import SearchIcon from './icons/SearchIcon'; 13 | import MenuIcon from './icons/MenuIcon'; 14 | import ArrowDownIcon from './icons/ArrowDownIcon'; 15 | import LinkIcon from './icons/LinkIcon'; 16 | import InfoIcon from './icons/InfoIcon'; 17 | 18 | const Home = ({ children }: any) => { 19 | const location = useLocation(); 20 | const [timer, setTimer] = useState( 21 | new Date().toLocaleTimeString('en-US', { 22 | hour12: false, 23 | hour: 'numeric', 24 | minute: 'numeric', 25 | second: 'numeric', 26 | }) 27 | ); 28 | const [isMenuOpened, setIsMenuOpened] = useState(false); 29 | const menuItemsFirst = [ 30 | { 31 | title: 'Home', 32 | icon: , 33 | route: '/', 34 | }, 35 | { 36 | title: 'Dashboard', 37 | icon: , 38 | route: '/dashboard', 39 | }, 40 | ]; 41 | 42 | const MenuItemsSecond = [ 43 | { 44 | title: 'Suppliers', 45 | icon: , 46 | route: '/suppliers', 47 | }, 48 | { 49 | title: 'Products', 50 | icon: , 51 | route: '/products', 52 | }, 53 | { 54 | title: 'Orders', 55 | icon: , 56 | route: '/orders', 57 | }, 58 | { 59 | title: 'Employees', 60 | icon: , 61 | route: '/employees', 62 | }, 63 | { 64 | title: 'Customers', 65 | icon: , 66 | route: '/customers', 67 | }, 68 | { 69 | title: 'Search', 70 | icon: , 71 | route: '/search', 72 | }, 73 | ]; 74 | setInterval(() => { 75 | const time = new Date().toLocaleTimeString('en-US', { 76 | hour12: false, 77 | hour: 'numeric', 78 | minute: 'numeric', 79 | second: 'numeric', 80 | }); 81 | setTimer(time); 82 | }, 1000); 83 | 84 | return ( 85 | 86 | 87 | 88 | 89 | Northwind Traders 90 | 91 | 92 | 93 | 94 | General 95 | {menuItemsFirst.map((item, index) => ( 96 | 103 | ))} 104 | BACKOFFICE 105 | {MenuItemsSecond.map((item, index) => ( 106 | 113 | ))} 114 | 115 | 116 | 117 | 118 | 119 |
{timer}
120 |
121 |
122 | {children} 123 |
124 |
125 | ); 126 | }; 127 | 128 | export default Home; 129 | 130 | const Margin = styled.div` 131 | margin-left: 240px; 132 | margin-top: 60px; 133 | `; 134 | 135 | const ContentWrapper = styled.div` 136 | display: flex; 137 | flex-direction: column; 138 | width: 100%; 139 | `; 140 | 141 | const LinkWrapper = styled.div` 142 | padding-right: 0.5rem; 143 | display: flex; 144 | align-items: center; 145 | `; 146 | 147 | const MenuOpened = styled.div` 148 | position: absolute; 149 | top: 60px; 150 | right: 0; 151 | background-color: #fff; 152 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), 153 | var(--tw-ring-shadow, 0 0 #0000), 0 4px 6px -1px rgba(0, 0, 0, 0.1), 154 | 0 2px 4px -1px rgba(0, 0, 0, 0.06); 155 | `; 156 | 157 | const MenuOpenedItem = styled.div` 158 | padding: 10.5px 12px; 159 | display: flex; 160 | align-items: center; 161 | min-width: 195px; 162 | font-size: 14px; 163 | cursor: pointer; 164 | `; 165 | 166 | const HeaderMenu = styled.div<{ isActive: boolean }>` 167 | display: flex; 168 | align-items: center; 169 | position: relative; 170 | color: ${({ isActive }) => (isActive ? '#3b82f6' : '#000')}; 171 | span { 172 | margin-left: 0.5rem; 173 | } 174 | svg { 175 | margin-left: 0.2rem; 176 | } 177 | cursor: pointer; 178 | `; 179 | 180 | const Wrapper = styled.div` 181 | display: flex; 182 | `; 183 | 184 | const RightMenuHeader = styled.div` 185 | font-size: 16px; 186 | color: black; 187 | margin-left: 12px; 188 | padding: 0 24px; 189 | display: flex; 190 | align-items: center; 191 | justify-content: space-between; 192 | `; 193 | 194 | const Right = styled.div` 195 | background-color: #fff; 196 | width: 100%; 197 | position: fixed; 198 | z-index: 3; 199 | padding: 17.5px 0; 200 | border-bottom: 1px solid rgba(229, 231, 235, 1); 201 | `; 202 | 203 | const LeftMenuBody = styled.div``; 204 | 205 | const BodyTitle = styled.div` 206 | padding: 0.75rem; 207 | font-size: 0.75rem; 208 | text-transform: uppercase; 209 | color: rgba(156, 163, 175, 1); 210 | `; 211 | 212 | const LeftMenu = styled.div` 213 | min-width: 15rem; 214 | max-width: 15rem; 215 | height: 100vh; 216 | position: fixed; 217 | z-index: 4; 218 | background-color: rgba(31, 41, 55, 1); 219 | `; 220 | 221 | const LeftMenuHeader = styled.div` 222 | height: 3.5rem; 223 | background-color: rgba(17, 24, 39, 1); 224 | display: flex; 225 | padding: 0 12px; 226 | align-items: center; 227 | `; 228 | 229 | const LeftMenuHeaderTitle = styled.div` 230 | color: white; 231 | span { 232 | font-weight: 900; 233 | } 234 | `; 235 | -------------------------------------------------------------------------------- /src/MainPage.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from 'react'; 2 | import styled from 'styled-components'; 3 | import { useNavigate } from 'react-router-dom'; 4 | import useOnClickOutside from './hooks/useOnClickOutside'; 5 | import initSqlJs from "sql.js"; 6 | 7 | 8 | type Props = { 9 | } 10 | 11 | const MainPage = ({}:Props) => { 12 | 13 | 14 | const nav = useNavigate(); 15 | 16 | return ( 17 | <> 18 | 19 | 20 | Northwind Traders example 21 | 22 | 23 | 24 | This is a demo of the Northwind dataset, running on{' '} 25 | Drizzle ORM + sql.js + SQLite in your browser 26 | 27 | 28 | Check out{' '} 29 | 30 | this source code on our GitHub 31 | 32 | 33 | 34 | This dataset was sourced from{' '} 35 | northwind-SQLite3, UI design from Cloudflare D1 example. 36 | 37 | 38 | You can use the UI to explore Supplies, Orders, Customers, 39 | Employees and Products, or you can use search if you know what 40 | you're looking for. 41 | 42 | 43 | 44 | image 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | {/* setFile(e.target.files[0])} />*/} 56 | 57 | 58 | 59 | 60 | 61 | ); 62 | }; 63 | 64 | export default MainPage; 65 | 66 | const Link = styled.span` 67 | color: #2f80ed; 68 | cursor: pointer; 69 | `; 70 | 71 | const Icon = styled.div` 72 | padding: 0 5px; 73 | display: flex; 74 | align-items: center; 75 | justify-content: center; 76 | `; 77 | 78 | const InputWrapperr = styled.div<{ active: boolean }>` 79 | display: flex; 80 | color: black; 81 | border: ${({ active }) => 82 | active ? '2px solid cadetblue' : '2px solid rgba(156, 163, 175, 1)'}; 83 | width: 400px; 84 | padding: 5px 0; 85 | border-radius: 0.25rem; 86 | margin-bottom: 12px; 87 | `; 88 | 89 | const Input = styled.input` 90 | border: none; 91 | padding: 5px 5px; 92 | font-size: 16px; 93 | width: 100%; 94 | 95 | &::placeholder { 96 | color: rgba(156, 163, 175, 1); 97 | font-size: 16px; 98 | } 99 | 100 | &:focus { 101 | outline: none; 102 | border: none; 103 | } 104 | `; 105 | 106 | const InfoBlockWrapper = styled.div` 107 | background-color: #fff; 108 | padding: 10px; 109 | position: absolute; 110 | top: -83px; 111 | cursor: default; 112 | left: -123px; 113 | z-index: 4; 114 | width: 320px; 115 | border-radius: 5px; 116 | box-shadow: 0px 8px 28px -6px rgba(24, 39, 75, 0.12), 117 | 0px 18px 88px -4px rgba(24, 39, 75, 0.14); 118 | `; 119 | 120 | const IconWrapper = styled.div` 121 | position: relative; 122 | display: flex; 123 | align-items: center; 124 | margin-right: 15px; 125 | cursor: pointer; 126 | `; 127 | 128 | const SaveButtonDB = styled.button<{ active: boolean }>` 129 | margin-top: 4px; 130 | margin-left: 15px; 131 | background-color: ${({ active }) => (active ? '#37b737' : '#fff')}; 132 | color: ${({ active }) => (active ? '#fff' : '#000')}; 133 | `; 134 | const SaveButtonDomain = styled.button<{ active: boolean; change?: boolean }>` 135 | margin-top: 4px; 136 | margin-left: 15px; 137 | background-color: ${({ active, change }) => 138 | // eslint-disable-next-line no-nested-ternary 139 | active && !change ? '#37b737' : change ? '#bf3945' : '#fff'}; 140 | color: ${({ active }) => (active ? '#fff' : '#000')}; 141 | `; 142 | 143 | const ParagraphWrapper = styled.div` 144 | display: flex; 145 | flex-direction: column; 146 | `; 147 | 148 | const InstructionParagraph = styled.p``; 149 | 150 | const InstructionWrapper = styled.div` 151 | display: flex; 152 | flex-direction: column; 153 | `; 154 | 155 | const Instruction = styled.div` 156 | padding-top: 35px; 157 | width: 100%; 158 | display: flex; 159 | flex-direction: column; 160 | `; 161 | 162 | const InstructionTitle = styled.div` 163 | display: flex; 164 | align-content: center; 165 | justify-content: center; 166 | font-size: 21px; 167 | `; 168 | 169 | const Container = styled.div``; 170 | 171 | const InputWrapper = styled.div` 172 | padding-top: 15px; 173 | display: flex; 174 | //width: 300px; 175 | align-content: center; 176 | `; 177 | 178 | const Label = styled.label` 179 | position: absolute; 180 | background: #121212; 181 | color: #fff; 182 | font-family: sans-serif; 183 | left: 1em; 184 | top: 0.75em; 185 | cursor: text; 186 | transition: top 350ms ease-in, font-size 350ms ease-in; 187 | `; 188 | 189 | const Button = styled.button``; 190 | 191 | const CheckBox = styled.div` 192 | padding-top: 5px; 193 | `; 194 | 195 | const Database = styled.div` 196 | display: flex; 197 | align-items: center; 198 | cursor: pointer; 199 | padding-left: 30px; 200 | 201 | &:nth-child(1) { 202 | padding-left: 0px; 203 | } 204 | `; 205 | 206 | const DatabasesWrapper = styled.div` 207 | display: flex; 208 | align-items: center; 209 | width: 100%; 210 | padding-top: 15px; 211 | `; 212 | 213 | const ChooseDBWrapper = styled.div` 214 | padding: 24px; 215 | `; 216 | const ChooseDBContentWrapper = styled.div` 217 | padding: 24px; 218 | `; 219 | 220 | const Paragraphs = styled.div` 221 | display: flex; 222 | flex-direction: column; 223 | `; 224 | 225 | const Image = styled.img<{ src: string }>` 226 | width: 24rem; 227 | `; 228 | 229 | const LeftContent = styled.div` 230 | display: flex; 231 | `; 232 | 233 | const Wrapper = styled.div` 234 | color: black; 235 | padding: 24px; 236 | `; 237 | 238 | const ContentWrapper = styled.div` 239 | padding: 24px; 240 | `; 241 | 242 | const Title = styled.div` 243 | font-size: 24px; 244 | `; 245 | 246 | const Subtitle = styled.div` 247 | font-size: 18px; 248 | color: #9ca3af; 249 | padding-top: 8px; 250 | `; 251 | 252 | const Paragraph = styled.div` 253 | padding-top: 16px; 254 | `; 255 | -------------------------------------------------------------------------------- /src/MenuItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useEffect } from 'react'; 2 | import styled from 'styled-components'; 3 | import { useNavigate } from 'react-router-dom'; 4 | 5 | type Props = { 6 | title: string; 7 | icon: React.ReactNode; 8 | route: string; 9 | active: boolean; 10 | }; 11 | const MenuItem: FC = ({ title, icon, route, active }) => { 12 | const navigation = useNavigate(); 13 | const goToRoute = () => { 14 | navigation(route); 15 | }; 16 | return ( 17 | 18 | {icon} 19 | {title} 20 | 21 | ); 22 | }; 23 | 24 | export default MenuItem; 25 | 26 | const Item = styled.div<{ active: boolean }>` 27 | display: flex; 28 | align-items: center; 29 | padding: 8px 0; 30 | cursor: pointer; 31 | background-color: ${({ active }) => 32 | active ? 'rgba(55,65,81,1)' : 'transparent'}; 33 | &:hover { 34 | background-color: rgba(55, 65, 81, 1); 35 | } 36 | `; 37 | 38 | const ItemTitle = styled.div` 39 | color: #d1d5db; 40 | font-size: 1rem; 41 | `; 42 | 43 | const IconWrapper = styled.div` 44 | display: flex; 45 | align-items: center; 46 | color: #d1d5db; 47 | padding: 0 0.75rem; 48 | `; 49 | -------------------------------------------------------------------------------- /src/Order.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import styled from 'styled-components'; 3 | import { Link, useNavigate, useParams } from 'react-router-dom'; 4 | import { useDispatch } from 'react-redux'; 5 | import { format } from 'date-fns'; 6 | import Ballot from './icons/Ballot'; 7 | import {SQLJsDatabase} from "drizzle-orm/sql-js"; 8 | import {details, orders, products, shipper} from "./data/schema"; 9 | import {sql} from "drizzle-orm"; 10 | import {eq} from "drizzle-orm/expressions"; 11 | import {OrderType} from "./OrdersPage"; 12 | import {setQuery} from "./store/actions/login"; 13 | 14 | type Product = { 15 | CategoryID: number; 16 | Discontinued: number; 17 | Discount: string; 18 | OrderID: number; 19 | OrderUnitPrice: string; 20 | ProductID: number; 21 | ProductName: string; 22 | ProductUnitPrice: string; 23 | Quantity: number; 24 | QuantityPerUnit: string; 25 | ReorderLevel: number; 26 | SupplierID: number; 27 | UnitsInStock: number; 28 | UnitsOnOrder: number; 29 | }; 30 | 31 | type OrderTypeTable = { 32 | discount: number; 33 | id: number; 34 | orderId: number; 35 | employeeId: number; 36 | orderUnitPrice: number; 37 | productId: number; 38 | productName: string; 39 | productUnitPrice: number; 40 | totalQuantity: number; 41 | unitPrice: number; 42 | totalPrice: number; 43 | }; 44 | 45 | type Props = { 46 | database: SQLJsDatabase; 47 | } 48 | 49 | const Order = ({database}:Props) => { 50 | const navigation = useNavigate(); 51 | const { id } = useParams(); 52 | const goBack = () => { 53 | navigation('/orders'); 54 | }; 55 | const [orderDataTable, setOrderDataTable] = useState( 56 | null 57 | ); 58 | const [orderData, setOrderData] = useState(null); 59 | const [totalQuantity, setTotalQuantity] = useState(0); 60 | const [totalDiscount, setTotalDiscount] = useState(0); 61 | const [queryArr, setQueryArr] = useState([]); 62 | const [queryTimeTable, setQueryTimeTable] = useState([]); 63 | const [queryTimeData, setQueryTimeData] = useState([]); 64 | 65 | const dispatch = useDispatch(); 66 | 67 | 68 | useEffect(() => { 69 | const startTime = new Date().getTime(); 70 | const stmtTable = database 71 | .select({ 72 | orderId: details.orderId, 73 | unitPrice: products.unitPrice, 74 | discount: details.discount, 75 | productId: products.id, 76 | productName: products.name, 77 | totalDiscount: sql`sum(${details.unitPrice} * ${details.quantity} * ${details.discount})`, 78 | totalPrice: sql`sum(${details.unitPrice} * ${details.quantity})`, 79 | totalQuantity: sql`sum(${details.quantity})`, 80 | totalProducts: sql`count(${details.orderId})`, 81 | }) 82 | .from(products) 83 | .leftJoin(details, eq(details.productId, products.id)) 84 | .where(eq(details.orderId, Number(id))) 85 | .all(); 86 | 87 | const stmtData = database 88 | .select({ 89 | orderId: orders.id, 90 | employeeId: orders.employeeId, 91 | orderDate: orders.orderDate, 92 | requiredDate: orders.requiredDate, 93 | shippedDate: orders.shippedDate, 94 | shipVia: orders.shipVia, 95 | freight: orders.freight, 96 | shipName: orders.shipName, 97 | shipAddress: orders.shipRegion, 98 | shipCity: orders.shipCity, 99 | shipRegion: orders.shipRegion, 100 | shipPostalCode: orders.shipPostalCode, 101 | shipCountry: orders.shipCountry, 102 | totalDiscount: sql`sum(${details.unitPrice} * ${details.quantity} * ${details.discount})`, 103 | totalPrice: sql`sum(${details.unitPrice} * ${details.quantity})`, 104 | totalQuantity: sql`sum(${details.quantity})`, 105 | totalProducts: sql`count(${details.orderId})`, 106 | }) 107 | .from(orders) 108 | .leftJoin(details, eq(orders.id, details.orderId)) 109 | .leftJoin(shipper, eq(orders.shipVia, shipper.id)) 110 | .where(eq(orders.id, Number(id))) 111 | .groupBy(orders.id, shipper.companyName) 112 | .all(); 113 | const endTime = new Date().getTime(); 114 | setQueryArr([...queryArr, database 115 | .select({ 116 | orderId: details.orderId, 117 | unitPrice: products.unitPrice, 118 | discount: details.discount, 119 | productId: products.id, 120 | productName: products.name, 121 | totalDiscount: sql`sum(${details.unitPrice} * ${details.quantity} * ${details.discount})`, 122 | totalPrice: sql`sum(${details.unitPrice} * ${details.quantity})`, 123 | totalQuantity: sql`sum(${details.quantity})`, 124 | totalProducts: sql`count(${details.orderId})`, 125 | }) 126 | .from(products) 127 | .leftJoin(details, eq(details.productId, products.id)) 128 | .where(eq(details.orderId, Number(id))).toSQL().sql]); 129 | 130 | setQueryArr([...queryArr, database 131 | .select({ 132 | orderId: orders.id, 133 | employeeId: orders.employeeId, 134 | orderDate: orders.orderDate, 135 | requiredDate: orders.requiredDate, 136 | shippedDate: orders.shippedDate, 137 | shipVia: orders.shipVia, 138 | freight: orders.freight, 139 | shipName: orders.shipName, 140 | shipAddress: orders.shipRegion, 141 | shipCity: orders.shipCity, 142 | shipRegion: orders.shipRegion, 143 | shipPostalCode: orders.shipPostalCode, 144 | shipCountry: orders.shipCountry, 145 | totalDiscount: sql`sum(${details.unitPrice} * ${details.quantity} * ${details.discount})`, 146 | totalPrice: sql`sum(${details.unitPrice} * ${details.quantity})`, 147 | totalQuantity: sql`sum(${details.quantity})`, 148 | totalProducts: sql`count(${details.orderId})`, 149 | }) 150 | .from(orders) 151 | .leftJoin(details, eq(orders.id, details.orderId)) 152 | .leftJoin(shipper, eq(orders.shipVia, shipper.id)) 153 | .where(eq(orders.id, Number(id))) 154 | .groupBy(orders.id, shipper.companyName).toSQL().sql]); 155 | setQueryTimeData([(endTime - startTime).toString()]); 156 | 157 | setOrderData(stmtData[0]); 158 | setOrderDataTable(stmtTable); 159 | }, [id]); 160 | 161 | 162 | useEffect(() => { 163 | 164 | if (orderData) { 165 | const obj = { 166 | query: queryArr, 167 | time: new Date().toISOString(), 168 | executeTime: queryTimeData, 169 | }; 170 | dispatch(setQuery(obj)); 171 | } 172 | if (orderDataTable) { 173 | const obj = { 174 | query: queryArr, 175 | time: new Date().toISOString(), 176 | executeTime: queryTimeData, 177 | }; 178 | dispatch(setQuery(obj)); 179 | } 180 | }, [orderData, dispatch, queryArr]); 181 | return ( 182 | 183 | {orderData && orderDataTable ? ( 184 | <> 185 | 186 |
187 | 188 | Supplier information 189 |
190 | 191 | 192 | 193 | 194 | Customer ID 195 | 196 | 197 | 198 | {orderData.employeeId} 199 | 200 | 201 | 202 | 203 | Ship Name 204 | 205 | {orderData.shipName} 206 | 207 | 208 | 209 | 210 | Total Products 211 | 212 | 213 | {orderData.products && orderData.products.length} 214 | 215 | 216 | 217 | 218 | Total Quantity 219 | 220 | 221 | {totalQuantity} 222 | 223 | 224 | 225 | 226 | Total Price 227 | 228 | 229 | ${orderData.totalPrice} 230 | 231 | 232 | 233 | 234 | Total Discount 235 | 236 | 237 | {totalDiscount}$ 238 | 239 | 240 | 241 | Ship Via 242 | 243 | {orderData.shipVia} 244 | 245 | 246 | 247 | Freight 248 | 249 | ${orderData.freight} 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | Order Date 258 | 259 | 260 | {format(new Date(orderData.orderDate), 'yyyy-LL-dd')} 261 | 262 | 263 | 264 | 265 | Required Date 266 | 267 | 268 | {format(new Date(orderData.requiredDate), 'yyyy-LL-dd')} 269 | 270 | 271 | 272 | 273 | Shipped Date 274 | 275 | 276 | {format(new Date(orderData.shippedDate), 'yyyy-LL-dd')} 277 | 278 | 279 | 280 | Ship City 281 | 282 | {orderData.shipCity} 283 | 284 | 285 | 286 | 287 | Ship Region 288 | 289 | 290 | {orderData.shipRegion} 291 | 292 | 293 | 294 | 295 | Ship Postal Code 296 | 297 | 298 | {orderData.shipPostalCode} 299 | 300 | 301 | 302 | 303 | Ship Country 304 | 305 | 306 | {orderData.shipCountry} 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | Products in Order 315 | 316 | 317 | Product 318 | Quantity 319 | Order Price 320 | Total Price 321 | Discount 322 | 323 | 324 | {orderDataTable && 325 | orderDataTable.map((product: OrderTypeTable) => ( 326 | 327 | 328 | 329 | {product.productName} 330 | 331 | 332 | 333 | {product.totalQuantity} 334 | 335 | 336 | ${product.unitPrice} 337 | 338 | 339 | {product.totalPrice} 340 | 341 | {product.discount}% 342 | 343 | ))} 344 | 345 |
346 |
347 |
348 | Go back 349 |
350 | 351 | ) : ( 352 |
Loading order...
353 | )} 354 |
355 | ); 356 | }; 357 | 358 | export default Order; 359 | 360 | const TableBody = styled.div``; 361 | const TableBodyRow = styled.div` 362 | border-left: 1px solid rgba(229, 231, 235, 1); 363 | border-right: 1px solid rgba(229, 231, 235, 1); 364 | display: flex; 365 | align-items: center; 366 | background-color: #f9fafb; 367 | 368 | &:hover { 369 | background-color: #f3f4f6; 370 | } 371 | 372 | &:hover:nth-child(even) { 373 | background-color: #f3f4f6; 374 | } 375 | 376 | &:nth-child(even) { 377 | background-color: #fff; 378 | } 379 | `; 380 | const TableBodyRowItem1 = styled.div` 381 | padding: 8px 12px; 382 | width: 46%; 383 | `; 384 | const TableBodyRowItem2 = styled.div` 385 | padding: 8px 12px; 386 | width: 14%; 387 | `; 388 | const TableBodyRowItem3 = styled.div` 389 | padding: 8px 12px; 390 | width: 19%; 391 | `; 392 | const TableBodyRowItem4 = styled.div` 393 | padding: 8px 12px; 394 | width: 18%; 395 | `; 396 | const TableBodyRowItem5 = styled.div` 397 | padding: 8px 12px; 398 | width: 15%; 399 | `; 400 | 401 | const Table = styled.div` 402 | color: black; 403 | `; 404 | const TableHeaderRow = styled.div` 405 | display: flex; 406 | font-size: 16px; 407 | font-weight: 700; 408 | align-items: center; 409 | border-left: 1px solid rgba(229, 231, 235, 1); 410 | border-right: 1px solid rgba(229, 231, 235, 1); 411 | `; 412 | const TableHeaderRowItem1 = styled.div` 413 | padding: 8px 12px; 414 | width: 46%; 415 | `; 416 | const TableHeaderRowItem2 = styled.div` 417 | padding: 8px 12px; 418 | width: 14%; 419 | `; 420 | const TableHeaderRowItem3 = styled.div` 421 | padding: 8px 12px; 422 | width: 19%; 423 | `; 424 | const TableHeaderRowItem4 = styled.div` 425 | padding: 8px 12px; 426 | width: 18%; 427 | `; 428 | const TableHeaderRowItem5 = styled.div` 429 | padding: 8px 12px; 430 | width: 15%; 431 | `; 432 | 433 | const TableWrapper = styled.div` 434 | background-color: #fff; 435 | `; 436 | 437 | const TableHeader = styled.div` 438 | color: black; 439 | font-size: 16px; 440 | padding: 12px 16px; 441 | border-left: 1px solid rgba(229, 231, 235, 1); 442 | border-right: 1px solid rgba(229, 231, 235, 1); 443 | border-bottom: 1px solid rgba(229, 231, 235, 1); 444 | font-weight: 700; 445 | `; 446 | 447 | const Footer = styled.div` 448 | padding: 24px; 449 | background-color: #fff; 450 | border: 1px solid rgba(229, 231, 235, 1); 451 | border-top: none; 452 | `; 453 | 454 | const FooterButton = styled.div` 455 | color: white; 456 | background-color: #ef4444; 457 | border-radius: 0.25rem; 458 | width: 63px; 459 | padding: 12px 16px; 460 | display: flex; 461 | justify-content: center; 462 | align-items: center; 463 | cursor: pointer; 464 | `; 465 | 466 | const BodyContentLeftItem = styled.div` 467 | margin-bottom: 15px; 468 | `; 469 | const BodyContentLeftItemTitle = styled.div` 470 | font-size: 16px; 471 | font-weight: 700; 472 | color: black; 473 | margin-bottom: 10px; 474 | `; 475 | const BodyContentLeftItemValue = styled.div` 476 | color: black; 477 | `; 478 | 479 | const BodyContent = styled.div` 480 | padding: 24px; 481 | background-color: #fff; 482 | display: flex; 483 | `; 484 | 485 | const BodyContentLeft = styled.div` 486 | width: 50%; 487 | `; 488 | const BodyContentRight = styled.div` 489 | width: 50%; 490 | `; 491 | 492 | const Body = styled.div` 493 | border: 1px solid rgba(229, 231, 235, 1); 494 | `; 495 | 496 | const Wrapper = styled.div` 497 | padding: 24px; 498 | `; 499 | 500 | const Header = styled.div` 501 | display: flex; 502 | align-items: center; 503 | background-color: #fff; 504 | color: black; 505 | padding: 12px 16px; 506 | border-bottom: 1px solid rgba(229, 231, 235, 1); 507 | `; 508 | 509 | const HeaderTitle = styled.div` 510 | font-size: 16px; 511 | font-weight: 700; 512 | margin-left: 8px; 513 | `; 514 | -------------------------------------------------------------------------------- /src/OrdersPage.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import styled from 'styled-components'; 3 | import { Link } from 'react-router-dom'; 4 | import { format } from 'date-fns'; 5 | import { useDispatch } from 'react-redux'; 6 | import HeaderArrowIcon from './icons/HeaderArrowIcon'; 7 | import Pagination from './Pagination'; 8 | import {SQLJsDatabase} from "drizzle-orm/sql-js"; 9 | import {details, orders} from "./data/schema"; 10 | import {sql} from "drizzle-orm"; 11 | import {asc, eq} from "drizzle-orm/expressions"; 12 | import {setQuery} from "./store/actions/login"; 13 | 14 | type Props = { 15 | database: SQLJsDatabase 16 | } 17 | 18 | 19 | export type OrderType = { 20 | customerId: string; 21 | employeeId: number; 22 | freight: number; 23 | orderDate: string; 24 | id: number; 25 | requiredDate: string; 26 | shipAddress: string; 27 | shipCity: string; 28 | shipCountry: string; 29 | shipName: string; 30 | shipPostalCode: string; 31 | shipRegion: string; 32 | shipVia: string; 33 | shippedDate: string; 34 | totalPrice: number; 35 | quantitySum: number; 36 | totalDiscount: number; 37 | productsCount: number; 38 | products?: Array; 39 | }; 40 | 41 | export type OrderProduct = { 42 | CategoryID: number; 43 | Discontinued: number; 44 | Discount: string; 45 | OrderID: number; 46 | OrderUnitPrice: string; 47 | ProductID: number; 48 | ProductName: string; 49 | ProductUnitPrice: string; 50 | Quantity: number; 51 | QuantityPerUnit: string; 52 | ReorderLevel: number; 53 | SupplierID: number; 54 | UnitsInStock: number; 55 | UnitsOnOrder: number; 56 | }; 57 | 58 | 59 | const OrdersPage = ({database}:Props) => { 60 | const [ordersData, setOrdersData] = useState< OrderType[] | null>(null); 61 | const [ordersCount, setOrdersCount] = useState< number| null>(null); 62 | const [currentPage, setCurrentPage] = useState(1); 63 | const [queryArr, setQueryArr] = useState([]); 64 | const [queryTime, setQueryTime] = useState([]); 65 | 66 | const dispatch = useDispatch(); 67 | 68 | useEffect(() => { 69 | if(database){ 70 | const startTime = new Date().getTime(); 71 | const stmt = database 72 | .select({ 73 | id: orders.id, 74 | shippedDate: orders.shippedDate, 75 | shipName: orders.shipName, 76 | shipCity: orders.shipCity, 77 | shipCountry: orders.shipCountry, 78 | productsCount: sql`count(${details.productId})`.as(), 79 | quantitySum: sql`sum(${details.quantity})`.as(), 80 | totalPrice: 81 | sql`sum(${details.quantity} * ${details.unitPrice})`.as(), 82 | }) 83 | .from(orders) 84 | .leftJoin(details, eq(orders.id, details.orderId)) 85 | .groupBy(orders.id) 86 | .orderBy(asc(orders.id)) 87 | .limit(20) 88 | .offset((currentPage - 1) * 20) 89 | .all(); 90 | const endTime = new Date().getTime(); 91 | setQueryArr([...queryArr, database 92 | .select({ 93 | id: orders.id, 94 | shippedDate: orders.shippedDate, 95 | shipName: orders.shipName, 96 | shipCity: orders.shipCity, 97 | shipCountry: orders.shipCountry, 98 | productsCount: sql`count(${details.productId})`, 99 | quantitySum: sql`sum(${details.quantity})`, 100 | totalPrice: 101 | sql`sum(${details.quantity} * ${details.unitPrice})`, 102 | }) 103 | .from(orders) 104 | .leftJoin(details, eq(orders.id, details.orderId)) 105 | .groupBy(orders.id) 106 | .orderBy(asc(orders.id)) 107 | .limit(20) 108 | .offset((currentPage - 1) * 20).toSQL().sql ]); 109 | // @ts-ignore 110 | setOrdersData(stmt); 111 | const stmtCount = database.select().from(orders).all(); 112 | setQueryTime([(endTime - startTime).toString()]); 113 | setOrdersCount(stmtCount.length) 114 | } 115 | }, [currentPage]); 116 | 117 | useEffect(() => { 118 | if (ordersData && ordersData.length > 0) { 119 | const obj = { 120 | query: queryArr, 121 | time: new Date().toISOString(), 122 | executeTime: queryTime, 123 | }; 124 | dispatch(setQuery(obj)); 125 | } 126 | }, [ordersData, dispatch, queryArr]); 127 | 128 | 129 | return ( 130 | 131 | {ordersData && ordersCount ? ( 132 | <> 133 |
134 | Orders 135 | 136 |
137 | 138 | 139 | Id 140 | Total Price 141 | Products 142 | Quantity 143 | Shipped 144 | Ship Name 145 | City 146 | Country 147 | 148 | 149 | {ordersData.map((order: OrderType) => ( 150 | 151 | 152 | {order.id} 153 | 154 | ${order.totalPrice.toFixed(0)} 155 | {order.productsCount} 156 | {order.quantitySum} 157 | 158 | {order.shippedDate 159 | ? format(new Date(order.shippedDate), 'yyyy-LL-dd') 160 | : ''} 161 | 162 | {order.shipName} 163 | {order.shipCity} 164 | {order.shipCountry} 165 | 166 | ))} 167 | 168 | 169 | setCurrentPage(page)} 175 | /> 176 | 177 | 178 | Page: {currentPage} of {Math.ceil(ordersCount / 20)} 179 | 180 | 181 | 182 |
183 | 184 | ) : ( 185 |
Loading orders...
186 | )} 187 |
188 | ); 189 | }; 190 | 191 | export default OrdersPage; 192 | 193 | export const PaginationRow = styled.div` 194 | display: flex; 195 | align-items: center; 196 | `; 197 | 198 | const PageCount = styled.div` 199 | font-size: 12.8px; 200 | `; 201 | 202 | export const PaginationNumberWrapper = styled.div` 203 | cursor: pointer; 204 | display: flex; 205 | align-items: center; 206 | border: 1px solid rgba(243, 244, 246, 1); 207 | `; 208 | 209 | export const PaginationNumber = styled.div<{ active: boolean }>` 210 | //width: 7px; 211 | padding: 10px 16px; 212 | border-radius: 0.25rem; 213 | margin-left: 0.25rem; 214 | margin-right: 0.25rem; 215 | cursor: pointer; 216 | border: ${({ active }) => 217 | active ? '1px solid rgba(209, 213, 219, 1)' : '1px solid #fff'}; 218 | //margin-right: 8px; 219 | :hover { 220 | border: 1px solid black; 221 | border-radius: 0.25rem; 222 | } 223 | `; 224 | 225 | export const PaginationWrapper = styled.div` 226 | padding: 12px 24px; 227 | display: flex; 228 | align-items: center; 229 | justify-content: space-between; 230 | border-top: 1px solid rgba(243, 244, 246, 1); 231 | `; 232 | 233 | const BodyCountry1 = styled.div` 234 | width: 12.25%; 235 | padding: 9px 12px; 236 | `; 237 | 238 | const BodyCompany = styled.div` 239 | width: 5.5%; 240 | padding: 9px 12px; 241 | //border: 1px solid #000; 242 | `; 243 | const BodyContact = styled.div` 244 | width: 9%; 245 | padding: 9px 12px; 246 | //border: 1px solid #000; 247 | `; 248 | const BodyTitle = styled.div` 249 | width: 9%; 250 | padding: 9px 12px; 251 | //border: 1px solid #000; 252 | `; 253 | const BodyCity = styled.div` 254 | width: 8%; 255 | 256 | padding: 9px 12px; 257 | //border: 1px solid #000; 258 | `; 259 | const BodyCountry = styled.div` 260 | width: 12.25%; 261 | 262 | padding: 9px 12px; 263 | //border: 1px solid #000; 264 | `; 265 | 266 | const TableBody = styled.div` 267 | background-color: #fff; 268 | `; 269 | 270 | const TableRow = styled.div` 271 | width: 98%; 272 | display: flex; 273 | align-items: center; 274 | background-color: #f9fafb; 275 | 276 | &:hover { 277 | background-color: #f3f4f6; 278 | } 279 | 280 | &:hover:nth-child(even) { 281 | background-color: #f3f4f6; 282 | } 283 | 284 | &:nth-child(even) { 285 | background-color: #fff; 286 | } 287 | `; 288 | 289 | const Icon = styled.div` 290 | width: 5%; 291 | padding: 9px 12px; 292 | `; 293 | 294 | const Company = styled.div` 295 | width: 5.5%; 296 | font-size: 16px; 297 | font-weight: 700; 298 | padding: 9px 12px; 299 | `; 300 | const Contact = styled.div` 301 | width: 9%; 302 | font-size: 16px; 303 | padding: 9px 12px; 304 | font-weight: 700; 305 | `; 306 | const Title = styled.div` 307 | width: 8%; 308 | font-size: 16px; 309 | font-weight: 700; 310 | padding: 9px 12px; 311 | `; 312 | const City = styled.div` 313 | width: 7.5%; 314 | font-size: 16px; 315 | font-weight: 700; 316 | padding: 9px 12px; 317 | `; 318 | const Country = styled.div` 319 | width: 12.5%; 320 | font-size: 16px; 321 | font-weight: 700; 322 | padding: 9px 12px; 323 | `; 324 | 325 | const Table = styled.div``; 326 | 327 | const TableHeader = styled.div` 328 | width: 100%; 329 | display: flex; 330 | align-items: center; 331 | background-color: #fff; 332 | `; 333 | 334 | const Wrapper = styled.div` 335 | color: black; 336 | padding: 24px; 337 | border: 1px solid rgba(243, 244, 246, 1); 338 | `; 339 | 340 | const Header = styled.div` 341 | padding: 12px 16px; 342 | display: flex; 343 | align-items: center; 344 | justify-content: space-between; 345 | background-color: #fff; 346 | border-bottom: 1px solid rgba(243, 244, 246, 1); ; 347 | `; 348 | 349 | const HeaderTitle = styled.div` 350 | font-size: 16px; 351 | font-weight: 700; 352 | `; 353 | -------------------------------------------------------------------------------- /src/Pagination.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classnames from 'classnames'; 3 | import './pagination.scss'; 4 | import { DOTS, usePagination } from './hooks/usePagination'; 5 | import { PaginationNumber } from './OrdersPage'; 6 | 7 | const Pagination = (props: any) => { 8 | const { 9 | onPageChange, 10 | totalCount, 11 | siblingCount, 12 | currentPage, 13 | pageSize, 14 | className, 15 | } = props; 16 | let p = 0; 17 | if (currentPage === 1) { 18 | p = 2; 19 | } else if (currentPage === 4) { 20 | p = 3.5; 21 | } else if (currentPage === 5) { 22 | p = 4; 23 | } else if (currentPage === 2) { 24 | p = 2.5; 25 | } else { 26 | p = 3; 27 | } 28 | const paginationRange = usePagination({ 29 | currentPage, 30 | totalCount, 31 | siblingCount: p, 32 | pageSize, 33 | }); 34 | 35 | // @ts-ignore 36 | if (currentPage === 0 || paginationRange.length < 2) { 37 | return null; 38 | } 39 | 40 | const onNext = () => { 41 | onPageChange(currentPage + 1); 42 | }; 43 | 44 | const onPrevious = () => { 45 | onPageChange(currentPage - 1); 46 | }; 47 | 48 | // @ts-ignore 49 | const lastPage = paginationRange[paginationRange.length - 1]; 50 | return ( 51 | <> 52 |
  • 58 | {paginationRange?.map((pageNumber) => { 59 | if (pageNumber === DOTS) { 60 | return
  • ; 61 | } 62 | 63 | return ( 64 | onPageChange(pageNumber)} 70 | > 71 | {pageNumber} 72 | 73 | ); 74 | })} 75 |
  • 81 | 82 | ); 83 | }; 84 | 85 | export default Pagination; 86 | -------------------------------------------------------------------------------- /src/Product.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from 'react'; 2 | import styled from 'styled-components'; 3 | import {useNavigate, useParams} from 'react-router-dom'; 4 | import {useDispatch} from 'react-redux'; 5 | import Ballot from './icons/Ballot'; 6 | import {setQuery} from "./store/actions/login"; 7 | import {SQLJsDatabase} from "drizzle-orm/sql-js"; 8 | import {products} from "./data/schema"; 9 | import {eq} from "drizzle-orm/expressions"; 10 | 11 | 12 | type SupplierData = { 13 | discontinued: number; 14 | id: number; 15 | name: string; 16 | quantityPerUnit: string; 17 | reorderLevel: number; 18 | supplier: string; 19 | unitPrice: number; 20 | unitsInStock: number; 21 | unitsOnOrder: number; 22 | supplierId: number; 23 | }; 24 | 25 | type Props = { 26 | database: SQLJsDatabase; 27 | } 28 | const Product = ({database}: Props) => { 29 | const navigation = useNavigate(); 30 | const {id} = useParams(); 31 | const goBack = () => { 32 | navigation('/products'); 33 | }; 34 | const [productData, setProductData] = useState(); 35 | const [queryArr, setQueryArr] = useState([]); 36 | const [queryTime, setQueryTime] = useState([]); 37 | 38 | const dispatch = useDispatch(); 39 | 40 | useEffect(() => { 41 | if (database) { 42 | const stmt = database.select().from(products).where(eq(products.id, Number(id))).all(); 43 | setQueryArr([...queryArr, database.select().from(products).where(eq(products.id, Number(id))).toSQL().sql]); 44 | // @ts-ignore 45 | setProductData(stmt[0]); 46 | } 47 | }, [id]); 48 | 49 | useEffect(() => { 50 | if (productData) { 51 | const obj = { 52 | query: queryArr, 53 | time: new Date().toISOString(), 54 | executeTime: queryTime, 55 | }; 56 | dispatch(setQuery(obj)); 57 | } 58 | }, [productData, dispatch, queryArr]); 59 | return ( 60 | 61 | {productData ? ( 62 | <> 63 | 64 |
    65 | 66 | Product information 67 |
    68 | 69 | 70 | 71 | 72 | Product Name 73 | 74 | 75 | {productData.name} 76 | 77 | 78 | 79 | Supplier 80 | 81 | {productData.supplier} 82 | 83 | 84 | 85 | 86 | Quantity Per Unit 87 | 88 | 89 | {productData.quantityPerUnit} 90 | 91 | 92 | 93 | 94 | Unit Price 95 | 96 | 97 | {productData.unitPrice} 98 | 99 | 100 | 101 | 102 | 103 | 104 | Units In Stock 105 | 106 | 107 | {productData.unitsInStock} 108 | 109 | 110 | 111 | 112 | Units In Order 113 | 114 | 115 | {productData.unitsOnOrder} 116 | 117 | 118 | 119 | 120 | Reorder Level 121 | 122 | 123 | {productData.reorderLevel} 124 | 125 | 126 | 127 | 128 | Discontinued 129 | 130 | 131 | {productData.discontinued} 132 | 133 | 134 | 135 | 136 | 137 |
    138 | Go back 139 |
    140 | 141 | ) : ( 142 |
    Loading product...
    143 | )} 144 |
    145 | ); 146 | }; 147 | 148 | export default Product; 149 | 150 | const Footer = styled.div` 151 | padding: 24px; 152 | background-color: #fff; 153 | border: 1px solid rgba(229, 231, 235, 1); 154 | border-top: none; 155 | `; 156 | 157 | const FooterButton = styled.div` 158 | color: white; 159 | background-color: #ef4444; 160 | border-radius: 0.25rem; 161 | width: 63px; 162 | padding: 12px 16px; 163 | display: flex; 164 | justify-content: center; 165 | align-items: center; 166 | cursor: pointer; 167 | `; 168 | 169 | const BodyContentLeftItem = styled.div` 170 | margin-bottom: 15px; 171 | `; 172 | const BodyContentLeftItemTitle = styled.div` 173 | font-size: 16px; 174 | font-weight: 700; 175 | color: black; 176 | margin-bottom: 10px; 177 | `; 178 | const BodyContentLeftItemValue = styled.div` 179 | color: black; 180 | `; 181 | 182 | const BodyContent = styled.div` 183 | padding: 24px; 184 | background-color: #fff; 185 | display: flex; 186 | `; 187 | 188 | const BodyContentLeft = styled.div` 189 | width: 50%; 190 | `; 191 | const BodyContentRight = styled.div` 192 | width: 50%; 193 | `; 194 | 195 | const Body = styled.div` 196 | border: 1px solid rgba(229, 231, 235, 1); 197 | `; 198 | 199 | const Wrapper = styled.div` 200 | padding: 24px; 201 | `; 202 | 203 | const Header = styled.div` 204 | display: flex; 205 | align-items: center; 206 | background-color: #fff; 207 | color: black; 208 | padding: 12px 16px; 209 | border-bottom: 1px solid rgba(229, 231, 235, 1); 210 | `; 211 | 212 | const HeaderTitle = styled.div` 213 | font-size: 16px; 214 | font-weight: 700; 215 | margin-left: 8px; 216 | `; 217 | -------------------------------------------------------------------------------- /src/ProductsPage.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import { Link } from 'react-router-dom'; 5 | import { useDispatch } from 'react-redux'; 6 | import HeaderArrowIcon from './icons/HeaderArrowIcon'; 7 | import { PaginationRow } from './OrdersPage'; 8 | import Pagination from './Pagination'; 9 | import {SQLJsDatabase} from "drizzle-orm/sql-js"; 10 | import {products} from "./data/schema"; 11 | import {setQuery} from "./store/actions/login"; 12 | 13 | export type Product = { 14 | discontinued: number; 15 | id: number; 16 | name: string; 17 | quantityPerUnit: string; 18 | reorderLevel: number; 19 | unitPrice: string; 20 | unitsInStock: number; 21 | unitsOnOrder: number; 22 | 23 | }; 24 | 25 | type Props = { 26 | database: SQLJsDatabase 27 | } 28 | 29 | const ProductsPage = ({database}:Props) => { 30 | const [productsData, setProductsData] = useState([]); 31 | const [productsCount, setProductsCount] = useState(null); 32 | const [currentPage, setCurrentPage] = useState(1); 33 | const dispatch = useDispatch(); 34 | const [queryArr, setQueryArr] = useState([]); 35 | const [queryTime, setQueryTime] = useState([]); 36 | 37 | useEffect(() => { 38 | if(database){ 39 | const startTime = new Date().getTime(); 40 | const stmt = database 41 | .select() 42 | .from(products) 43 | .limit(20) 44 | .offset((currentPage - 1) * 20) 45 | .all(); 46 | const stmtCount = database.select().from(products).all(); 47 | const endTime = new Date().getTime(); 48 | setQueryTime([(endTime - startTime).toString()]); 49 | setProductsData(stmt); 50 | setQueryArr([...queryArr, database.select().from(products).limit(20).offset((currentPage - 1) * 20).toSQL().sql ]); 51 | setProductsCount(stmtCount.length); 52 | } 53 | }, [currentPage, database]); 54 | 55 | useEffect(() => { 56 | if (productsData && productsData.length > 0) { 57 | const obj = { 58 | query: queryArr, 59 | time: new Date().toISOString(), 60 | executeTime: queryTime, 61 | }; 62 | dispatch(setQuery(obj)); 63 | } 64 | }, [productsData, dispatch, queryArr]); 65 | 66 | return ( 67 | 68 | {productsData && productsCount ? ( 69 | <> 70 |
    71 | Products 72 | 73 |
    74 | 75 | 76 | Name 77 | Qt per unit 78 | Price 79 | Stock 80 | Orders 81 | 82 | 83 | {productsData.map((product: Product, i: number) => { 84 | if (i < 1) return; 85 | return ( 86 | 87 | 88 | {product.name} 89 | 90 | {product.quantityPerUnit} 91 | ${product.unitPrice} 92 | {product.unitsInStock} 93 | {product.unitsOnOrder} 94 | 95 | ); 96 | })} 97 | 98 | 99 | setCurrentPage(page)} 105 | /> 106 | 107 | 108 | Page: {currentPage} of {Math.ceil(productsCount / 20)} 109 | 110 | 111 | 112 |
    113 | 114 | ) : ( 115 |
    Loading products...
    116 | )} 117 |
    118 | ); 119 | }; 120 | 121 | export default ProductsPage; 122 | const PageCount = styled.div` 123 | font-size: 12.8px; 124 | `; 125 | 126 | const PaginationNumberWrapper = styled.div` 127 | cursor: pointer; 128 | display: flex; 129 | align-items: center; 130 | border: 1px solid rgba(243, 244, 246, 1); 131 | `; 132 | 133 | const PaginationNumber = styled.div<{ active: boolean }>` 134 | width: 7px; 135 | padding: 10px 16px; 136 | border: ${({ active }) => 137 | active ? '1px solid rgba(209, 213, 219, 1)' : 'none'}; 138 | margin-right: 8px; 139 | `; 140 | 141 | const PaginationWrapper = styled.div` 142 | padding: 12px 24px; 143 | display: flex; 144 | align-items: center; 145 | justify-content: space-between; 146 | `; 147 | 148 | const BodyCompany = styled.div` 149 | width: 30%; 150 | padding: 9px 12px; 151 | //border: 1px solid #000; 152 | `; 153 | const BodyContact = styled.div` 154 | width: 20%; 155 | padding: 9px 12px; 156 | //border: 1px solid #000; 157 | `; 158 | const BodyTitle = styled.div` 159 | width: 30%; 160 | padding: 9px 12px; 161 | //border: 1px solid #000; 162 | `; 163 | const BodyCity = styled.div` 164 | width: 10%; 165 | padding: 9px 12px; 166 | //border: 1px solid #000; 167 | `; 168 | const BodyCountry = styled.div` 169 | width: 8%; 170 | padding: 9px 12px; 171 | //border: 1px solid #000; 172 | `; 173 | 174 | const TableBody = styled.div` 175 | background-color: #fff; 176 | `; 177 | 178 | const TableRow = styled.div` 179 | width: 98%; 180 | display: flex; 181 | align-items: center; 182 | background-color: #f9fafb; 183 | 184 | &:hover { 185 | background-color: #f3f4f6; 186 | } 187 | 188 | &:hover:nth-child(even) { 189 | background-color: #f3f4f6; 190 | } 191 | 192 | &:nth-child(even) { 193 | background-color: #fff; 194 | } 195 | `; 196 | 197 | const Icon = styled.div` 198 | width: 5%; 199 | padding: 9px 12px; 200 | `; 201 | 202 | const Company = styled.div` 203 | width: 30%; 204 | font-size: 16px; 205 | font-weight: 700; 206 | padding: 9px 12px; 207 | `; 208 | const Contact = styled.div` 209 | width: 20%; 210 | font-size: 16px; 211 | padding: 9px 12px; 212 | font-weight: 700; 213 | `; 214 | const Title = styled.div` 215 | width: 30%; 216 | font-size: 16px; 217 | font-weight: 700; 218 | padding: 9px 12px; 219 | `; 220 | const City = styled.div` 221 | width: 10%; 222 | font-size: 16px; 223 | font-weight: 700; 224 | padding: 9px 12px; 225 | `; 226 | const Country = styled.div` 227 | width: 10%; 228 | font-size: 16px; 229 | font-weight: 700; 230 | padding: 9px 12px; 231 | `; 232 | 233 | const Table = styled.div``; 234 | 235 | const TableHeader = styled.div` 236 | width: 100%; 237 | display: flex; 238 | align-items: center; 239 | background-color: #fff; 240 | `; 241 | 242 | const Wrapper = styled.div` 243 | color: black; 244 | padding: 24px; 245 | border: 1px solid rgba(243, 244, 246, 1); ; 246 | `; 247 | 248 | const Header = styled.div` 249 | padding: 12px 16px; 250 | display: flex; 251 | align-items: center; 252 | justify-content: space-between; 253 | background-color: #fff; 254 | border-bottom: 1px solid rgba(243, 244, 246, 1); ; 255 | `; 256 | 257 | const HeaderTitle = styled.div` 258 | font-size: 16px; 259 | font-weight: 700; 260 | `; 261 | 262 | function setQueryResponseDashboard(obj: { query: any; time: string }): any { 263 | throw new Error('Function not implemented.'); 264 | } 265 | -------------------------------------------------------------------------------- /src/SearchPage.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import styled from 'styled-components'; 3 | import { Link } from 'react-router-dom'; 4 | import { useDispatch } from 'react-redux'; 5 | import SearchIcon from './icons/SearchIcon'; 6 | import useOnClickOutside from './hooks/useOnClickOutside'; 7 | import {SQLJsDatabase} from "drizzle-orm/sql-js"; 8 | import {customers, products} from "./data/schema"; 9 | import {like} from "drizzle-orm/expressions"; 10 | import {Product} from "./ProductsPage"; 11 | import {setQuery} from "./store/actions/login"; 12 | import {Customer} from "./CustomersPage"; 13 | 14 | type Props = { 15 | database: SQLJsDatabase; 16 | } 17 | 18 | const SearchPage = ({database}:Props) => { 19 | const [inputActive, setInputActive] = useState(false); 20 | const [inputValue, setInputValue] = useState(''); 21 | const [queryArr, setQueryArr] = useState([]); 22 | const [queryTimeProduct, setQueryTimeProduct] = useState([]); 23 | const [queryTimeCustomer, setQueryTimeCustomer] = useState([]); 24 | const [searchResponseProducts, setSearchResponseProducts] = useState< 25 | Product[] | null 26 | >(null); 27 | const [searchResponseCustomer, setSearchResponseCustomer] = useState< 28 | Customer[] | null 29 | >(null); 30 | const [isProductsActive, setIsProductsActive] = useState(false); 31 | const [enterPressed, setEnterPressed] = useState(false); 32 | const inputRef = useOnClickOutside(() => { 33 | setInputActive(false); 34 | }); 35 | 36 | const dispatch = useDispatch(); 37 | 38 | const handleInput = (e: any) => { 39 | setInputValue(e.target.value); 40 | setEnterPressed(true); 41 | if (e.key === 'Enter') { 42 | if (isProductsActive) { 43 | const start = new Date().getTime(); 44 | const stmt = database 45 | .select() 46 | .from(products) 47 | .where(like(products.name, `%${inputValue}%`)) 48 | .all(); 49 | const end = new Date().getTime(); 50 | setQueryTimeProduct([(end - start).toString()]); 51 | setSearchResponseProducts(stmt); 52 | setQueryArr([...queryArr, database.select().from(products).where(like(products.name, `%${inputValue}%`)).toSQL().sql ]); 53 | } 54 | 55 | if (!isProductsActive) { 56 | const startTimeCustomer = new Date().getTime(); 57 | const stmt = database 58 | .select() 59 | .from(customers) 60 | .where(like(customers.contactName, `%${inputValue}%`)) 61 | .all(); 62 | const endTimeCustomer = new Date().getTime(); 63 | setQueryTimeCustomer([(endTimeCustomer - startTimeCustomer).toString()]); 64 | setSearchResponseCustomer(stmt); 65 | setQueryArr([...queryArr, database.select().from(customers).where(like(customers.contactName, `%${inputValue}%`)).toSQL().sql ]); 66 | } 67 | } 68 | }; 69 | 70 | useEffect(() => { 71 | if (isProductsActive) { 72 | const obj = { 73 | query: queryArr, 74 | time: new Date().toISOString(), 75 | executeTime: queryTimeProduct, 76 | }; 77 | dispatch(setQuery(obj)); 78 | } 79 | }, [dispatch, queryArr, searchResponseCustomer, searchResponseProducts]); 80 | 81 | useEffect(() => { 82 | if (!isProductsActive) { 83 | const obj = { 84 | query: queryArr, 85 | time: new Date().toISOString(), 86 | executeTime: queryTimeCustomer, 87 | }; 88 | dispatch(setQuery(obj)); 89 | } 90 | }, [dispatch, queryArr, searchResponseCustomer, searchResponseProducts]); 91 | 92 | 93 | return ( 94 | 95 | 96 | Search Database 97 | setInputActive(true)} 101 | > 102 | 103 | 104 | 105 | handleInput(e)} 108 | onKeyPress={(e) => handleInput(e)} 109 | /> 110 | 111 | Tables 112 | 113 | setIsProductsActive(true)}> 114 | {!isProductsActive ? ( 115 | 116 | ) : ( 117 | 118 | 119 | 120 | )} 121 | 122 | Products 123 | 124 | setIsProductsActive(false)}> 125 | {isProductsActive ? ( 126 | 127 | ) : ( 128 | 129 | 130 | 131 | )} 132 | Customers 133 | 134 | 135 | 136 | Search results 137 | 138 | {!isProductsActive && searchResponseCustomer 139 | ? searchResponseCustomer.map((product: any) => ( 140 | 141 | 142 | 143 | {product.contactName} 144 | 145 | 146 | 147 | #${product.id}, Contact: 148 | {product.contactName}, Title: ${product.contactTitle}, Phone: 149 | ${product.phone} 150 | 151 | 152 | )) 153 | : !isProductsActive && ( 154 | 155 | 156 | 157 | {enterPressed && 'No results'} 158 | 159 | 160 | 161 | )} 162 | {isProductsActive && searchResponseProducts 163 | ? searchResponseProducts.map((product: any) => ( 164 | 165 | 166 | {product.name} 167 | 168 | 169 | #${product.id}, Quantity Per Unit: ${product.quantityPerUnit}, 170 | Price: ${product.unitPrice}, Stock: ${product.unitsInStock} 171 | 172 | 173 | )) 174 | : isProductsActive && ( 175 | 176 | 177 | 178 | {enterPressed && 'No results'} 179 | 180 | 181 | 182 | )} 183 | 184 | 185 | ); 186 | }; 187 | 188 | export default SearchPage; 189 | 190 | const SearchResultMainSubtitle = styled.div` 191 | color: #9ca3af; 192 | font-size: 14px; 193 | `; 194 | const SearchResultMainTitle = styled.div` 195 | font-size: 16px; 196 | color: #2563eb; 197 | padding-bottom: 5px; 198 | `; 199 | 200 | const SearchResult = styled.div` 201 | padding-bottom: 8px; 202 | `; 203 | 204 | const SearchResultTitle = styled.div` 205 | margin-top: 24px; 206 | color: black; 207 | margin-bottom: 12px; 208 | font-weight: 700; 209 | `; 210 | 211 | const CircleActiveIcon = styled.div` 212 | width: 9px; 213 | height: 9px; 214 | border: 1px solid rgba(209, 213, 219, 1); 215 | border-radius: 50%; 216 | background-color: #fff; 217 | cursor: pointer; 218 | display: flex; 219 | align-items: center; 220 | justify-content: center; 221 | `; 222 | const CircleActive = styled.div` 223 | width: 20px; 224 | height: 20px; 225 | background-color: #3b82f6; 226 | border: 1px solid rgba(209, 213, 219, 1); 227 | border-radius: 50%; 228 | cursor: pointer; 229 | display: flex; 230 | align-items: center; 231 | justify-content: center; 232 | `; 233 | 234 | const Choice = styled.div` 235 | display: flex; 236 | align-items: center; 237 | margin-right: 8px; 238 | cursor: pointer; 239 | `; 240 | 241 | const TableTitle = styled.div` 242 | color: black; 243 | font-size: 16px; 244 | padding-left: 8px; 245 | `; 246 | const Circle = styled.div` 247 | width: 20px; 248 | height: 20px; 249 | border: 1px solid rgba(209, 213, 219, 1); 250 | border-radius: 50%; 251 | cursor: pointer; 252 | `; 253 | const TableWrapper = styled.div` 254 | display: flex; 255 | align-items: center; 256 | `; 257 | 258 | const Icon = styled.div` 259 | padding: 0 5px; 260 | display: flex; 261 | align-items: center; 262 | justify-content: center; 263 | `; 264 | 265 | const InputWrapper = styled.div<{ active: boolean }>` 266 | display: flex; 267 | color: black; 268 | border: ${({ active }) => 269 | active ? '2px solid cadetblue' : '2px solid rgba(156, 163, 175, 1)'}; 270 | width: 400px; 271 | padding: 5px 0; 272 | border-radius: 0.25rem; 273 | margin-bottom: 12px; 274 | `; 275 | 276 | const Input = styled.input` 277 | border: none; 278 | padding: 5px 5px; 279 | font-size: 16px; 280 | width: 400px; 281 | 282 | &::placeholder { 283 | color: rgba(156, 163, 175, 1); 284 | font-size: 16px; 285 | } 286 | 287 | &:focus { 288 | outline: none; 289 | border: none; 290 | } 291 | `; 292 | 293 | const ContentTitle = styled.div` 294 | font-size: 16px; 295 | font-weight: 700; 296 | color: black; 297 | margin-bottom: 12px; 298 | `; 299 | 300 | const Wrapper = styled.div` 301 | padding: 24px; 302 | `; 303 | 304 | const Content = styled.div` 305 | padding: 24px; 306 | background-color: #fff; 307 | `; 308 | -------------------------------------------------------------------------------- /src/Supplier.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from 'react'; 2 | import styled from 'styled-components'; 3 | import {useNavigate, useParams} from 'react-router-dom'; 4 | 5 | import {useDispatch} from 'react-redux'; 6 | import Ballot from './icons/Ballot'; 7 | import {SupplierType} from "./SuppliersPage"; 8 | import initSqlJs, {Database} from "sql.js"; 9 | import {setQuery} from "./store/actions/login"; 10 | import {eq} from "drizzle-orm/expressions"; 11 | import {suppliers} from "./data/schema"; 12 | import {SQLJsDatabase} from "drizzle-orm/sql-js"; 13 | 14 | 15 | type Props = { 16 | database: SQLJsDatabase 17 | } 18 | 19 | const Supplier = ({database}: Props) => { 20 | const navigation = useNavigate(); 21 | const {id} = useParams(); 22 | const goBack = () => { 23 | navigation('/suppliers'); 24 | }; 25 | const [supplierData, setSupplierData] = useState(null); 26 | const [queryArr, setQueryArr] = useState([]); 27 | const [queryTime, setQueryTime] = useState([]); 28 | 29 | const dispatch = useDispatch(); 30 | 31 | useEffect(() => { 32 | if (supplierData) { 33 | const obj = { 34 | query: queryArr, 35 | time: new Date().toISOString(), 36 | executeTime: queryTime, 37 | }; 38 | dispatch(setQuery(obj)); 39 | } 40 | }, [dispatch, queryArr, supplierData]); 41 | 42 | 43 | useEffect(() => { 44 | if(database && id) { 45 | const startTime = new Date().getTime(); 46 | const stmt = database.select().from(suppliers).where(eq(suppliers.id, Number(id))).all(); 47 | const endTime = new Date().getTime(); 48 | setQueryArr([...queryArr, database.select().from(suppliers).where(eq(suppliers.id, Number(id))).toSQL().sql ]); 49 | setQueryTime([(endTime - startTime).toString()]); 50 | setSupplierData(stmt[0]); 51 | } 52 | }, [database]); 53 | 54 | return ( 55 | 56 | {supplierData ? ( 57 | <> 58 | 59 |
    60 | 61 | Supplier information 62 |
    63 | 64 | 65 | 66 | 67 | Company Name 68 | 69 | 70 | {supplierData.companyName} 71 | 72 | 73 | 74 | 75 | Contact Name 76 | 77 | 78 | {supplierData.contactName} 79 | 80 | 81 | 82 | 83 | Contact Title 84 | 85 | 86 | {supplierData.contactTitle} 87 | 88 | 89 | 90 | Address 91 | 92 | {supplierData.address} 93 | 94 | 95 | 96 | City 97 | 98 | {supplierData.city} 99 | 100 | 101 | 102 | 103 | 104 | Region 105 | 106 | {supplierData.region} 107 | 108 | 109 | 110 | 111 | Postal Code 112 | 113 | 114 | {supplierData.postalCode} 115 | 116 | 117 | 118 | Country 119 | 120 | {supplierData.country} 121 | 122 | 123 | 124 | Phone 125 | 126 | {supplierData.phone} 127 | 128 | 129 | 130 | 131 | 132 |
    133 | Go back 134 |
    135 | 136 | ) : ( 137 |
    Loading supplier...
    138 | )} 139 |
    140 | ); 141 | }; 142 | 143 | export default Supplier; 144 | 145 | const Footer = styled.div` 146 | padding: 24px; 147 | background-color: #fff; 148 | border: 1px solid rgba(229, 231, 235, 1); 149 | border-top: none; 150 | `; 151 | 152 | const FooterButton = styled.div` 153 | color: white; 154 | background-color: #ef4444; 155 | border-radius: 0.25rem; 156 | width: 63px; 157 | padding: 12px 16px; 158 | display: flex; 159 | justify-content: center; 160 | align-items: center; 161 | cursor: pointer; 162 | `; 163 | 164 | const BodyContentLeftItem = styled.div` 165 | margin-bottom: 15px; 166 | `; 167 | const BodyContentLeftItemTitle = styled.div` 168 | font-size: 16px; 169 | font-weight: 700; 170 | color: black; 171 | margin-bottom: 10px; 172 | `; 173 | const BodyContentLeftItemValue = styled.div` 174 | color: black; 175 | `; 176 | 177 | const BodyContent = styled.div` 178 | padding: 24px; 179 | background-color: #fff; 180 | display: flex; 181 | `; 182 | 183 | const BodyContentLeft = styled.div` 184 | width: 50%; 185 | `; 186 | const BodyContentRight = styled.div` 187 | width: 50%; 188 | `; 189 | 190 | const Body = styled.div` 191 | border: 1px solid rgba(229, 231, 235, 1); 192 | `; 193 | 194 | const Wrapper = styled.div` 195 | padding: 24px; 196 | `; 197 | 198 | const Header = styled.div` 199 | display: flex; 200 | align-items: center; 201 | background-color: #fff; 202 | color: black; 203 | padding: 12px 16px; 204 | border-bottom: 1px solid rgba(229, 231, 235, 1); 205 | `; 206 | 207 | const HeaderTitle = styled.div` 208 | font-size: 16px; 209 | font-weight: 700; 210 | margin-left: 8px; 211 | `; 212 | -------------------------------------------------------------------------------- /src/SuppliersPage.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from 'react'; 2 | import {Link} from 'react-router-dom'; 3 | import styled from 'styled-components'; 4 | 5 | import {createAvatar} from '@dicebear/avatars'; 6 | import * as style from '@dicebear/avatars-initials-sprites'; 7 | import Svg from 'react-inlinesvg'; 8 | import {useDispatch, useSelector} from 'react-redux'; 9 | import HeaderArrowIcon from './icons/HeaderArrowIcon'; 10 | import Pagination from './Pagination'; 11 | import {PaginationRow, PaginationWrapper} from './OrdersPage'; 12 | import initSqlJs from "sql.js"; 13 | import {drizzle, SQLJsDatabase} from "drizzle-orm/sql-js"; 14 | import {suppliers} from "./data/schema"; 15 | import {setLoadedFile, setQuery} from "./store/actions/login"; 16 | import {selectLoadedFile} from "./store/selectors/auth"; 17 | 18 | type Props = { 19 | database: SQLJsDatabase; 20 | } 21 | 22 | export type SupplierType = { 23 | address: string; 24 | city: string; 25 | companyName: string; 26 | contactName: string; 27 | contactTitle: string; 28 | country: string; 29 | id: number; 30 | phone: string; 31 | postalCode: string; 32 | region: string | null; 33 | } 34 | const SuppliersPage = ({database}: Props) => { 35 | const [suppliersData, setSuppliersData] = useState([]); 36 | const [suppliersDataCount, setSuppliersDataCount] = useState(null); 37 | const [currentPage, setCurrentPage] = useState(1); 38 | const [queryArr, setQueryArr] = useState([]); 39 | const dispatch = useDispatch(); 40 | const [queryTime, setQueryTime] = useState([]); 41 | const loadedFile = useSelector(selectLoadedFile); 42 | 43 | useEffect(() => { 44 | if (database) { 45 | const startTime = new Date().getTime(); 46 | const stmtCount = database.select().from(suppliers).all() 47 | const stmt = database.select().from(suppliers).limit(20).offset((currentPage - 1) * 20).all(); 48 | const endTime = new Date().getTime(); 49 | setQueryTime([(endTime - startTime).toString()]); 50 | setQueryArr([...queryArr, database.select().from(suppliers).limit(20).offset((currentPage - 1) * 20).toSQL().sql ]); 51 | setSuppliersData(stmt); 52 | setSuppliersDataCount(stmtCount.length); 53 | 54 | } 55 | 56 | }, [currentPage, database]); 57 | 58 | 59 | useEffect(() => { 60 | if (suppliersData && suppliersData.length > 0) { 61 | const obj = { 62 | query: queryArr, 63 | time: new Date().toISOString(), 64 | executeTime: queryTime, 65 | }; 66 | dispatch(setQuery(obj)); 67 | } 68 | }, [suppliersData, dispatch]); 69 | return ( 70 | 71 | {suppliersData && suppliersDataCount ? ( 72 | <> 73 |
    74 | Suppliers 75 | 76 |
    77 | 78 | 79 | 80 | Company 81 | Contact 82 | Title 83 | City 84 | Country 85 | 86 | 87 | {suppliersData.map((supplier: SupplierType, index: number) => { 88 | const svg = createAvatar(style, { 89 | seed: supplier.contactName, 90 | // ... and other options 91 | }); 92 | if (index < 1) return; 93 | return ( 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | {supplier.companyName} 103 | 104 | 105 | 106 | {supplier.contactName} 107 | {supplier.contactTitle} 108 | {supplier.city} 109 | {supplier.country} 110 | 111 | ); 112 | })} 113 | 114 | 115 | setCurrentPage(page)} 121 | /> 122 | 123 | 124 | Page: {currentPage} of {Math.ceil(suppliersDataCount! / 20)} 125 | 126 | 127 | 128 |
    129 | 130 | ) : ( 131 |
    Loading suppliers...
    132 | )} 133 |
    134 | ); 135 | }; 136 | 137 | export default SuppliersPage; 138 | 139 | const PageCount = styled.div` 140 | font-size: 12.8px; 141 | `; 142 | 143 | const PaginationNumberWrapper = styled.div` 144 | cursor: pointer; 145 | display: flex; 146 | align-items: center; 147 | border: 1px solid rgba(243, 244, 246, 1); 148 | `; 149 | 150 | const PaginationNumber = styled.div<{ active: boolean }>` 151 | width: 7px; 152 | padding: 10px 16px; 153 | border: ${({active}) => 154 | active ? '1px solid rgba(209, 213, 219, 1)' : 'none'}; 155 | margin-right: 8px; 156 | `; 157 | 158 | const Circle = styled.div` 159 | width: 24px; 160 | height: 24px; 161 | overflow: hidden; 162 | background-color: cadetblue; 163 | border-radius: 50%; 164 | color: white; 165 | font-size: 10px; 166 | display: flex; 167 | justify-content: center; 168 | align-items: center; 169 | `; 170 | 171 | const BodyIcon = styled.div` 172 | width: 5%; 173 | padding: 9px 12px; 174 | display: flex; 175 | align-items: center; 176 | justify-content: center; 177 | //border: 1px solid #000; 178 | `; 179 | const BodyCompany = styled.div` 180 | width: 30%; 181 | padding: 9px 12px; 182 | //border: 1px solid #000; 183 | `; 184 | const BodyContact = styled.div` 185 | width: 15%; 186 | padding: 9px 12px; 187 | //border: 1px solid #000; 188 | `; 189 | const BodyTitle = styled.div` 190 | width: 20%; 191 | padding: 9px 12px; 192 | //border: 1px solid #000; 193 | `; 194 | const BodyCity = styled.div` 195 | width: 15%; 196 | padding: 9px 12px; 197 | //border: 1px solid #000; 198 | `; 199 | const BodyCountry = styled.div` 200 | width: 13%; 201 | padding: 9px 12px; 202 | //border: 1px solid #000; 203 | `; 204 | 205 | const TableBody = styled.div` 206 | background-color: #fff; 207 | `; 208 | 209 | const TableRow = styled.div` 210 | width: 98%; 211 | display: flex; 212 | align-items: center; 213 | background-color: #f9fafb; 214 | 215 | &:hover { 216 | background-color: #f3f4f6; 217 | } 218 | 219 | &:hover:nth-child(even) { 220 | background-color: #f3f4f6; 221 | } 222 | 223 | &:nth-child(even) { 224 | background-color: #fff; 225 | } 226 | `; 227 | 228 | const Icon = styled.div` 229 | width: 5%; 230 | padding: 9px 12px; 231 | `; 232 | 233 | const Company = styled.div` 234 | width: 30%; 235 | font-size: 16px; 236 | font-weight: 700; 237 | padding: 9px 12px; 238 | `; 239 | const Contact = styled.div` 240 | width: 15%; 241 | font-size: 16px; 242 | padding: 9px 12px; 243 | font-weight: 700; 244 | `; 245 | const Title = styled.div` 246 | width: 20%; 247 | font-size: 16px; 248 | font-weight: 700; 249 | padding: 9px 12px; 250 | `; 251 | const City = styled.div` 252 | width: 15%; 253 | font-size: 16px; 254 | font-weight: 700; 255 | padding: 9px 12px; 256 | `; 257 | const Country = styled.div` 258 | width: 15%; 259 | font-size: 16px; 260 | font-weight: 700; 261 | padding: 9px 12px; 262 | `; 263 | 264 | const Table = styled.div``; 265 | 266 | const TableHeader = styled.div` 267 | width: 100%; 268 | display: flex; 269 | align-items: center; 270 | background-color: #fff; 271 | `; 272 | 273 | const Wrapper = styled.div` 274 | color: black; 275 | padding: 24px; 276 | border: 1px solid rgba(243, 244, 246, 1);; 277 | `; 278 | 279 | const Header = styled.div` 280 | padding: 12px 16px; 281 | display: flex; 282 | align-items: center; 283 | justify-content: space-between; 284 | background-color: #fff; 285 | border-bottom: 1px solid rgba(243, 244, 246, 1);; 286 | `; 287 | 288 | const HeaderTitle = styled.div` 289 | font-size: 16px; 290 | font-weight: 700; 291 | `; 292 | -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/data/schema.ts: -------------------------------------------------------------------------------- 1 | import { InferModel } from 'drizzle-orm'; 2 | import { 3 | foreignKey, 4 | integer, 5 | numeric, 6 | sqliteTable, 7 | text, 8 | } from 'drizzle-orm/sqlite-core'; 9 | 10 | export const customers = sqliteTable('Customers', { 11 | id: text('CustomerID').primaryKey(), 12 | companyName: text('CompanyName').notNull(), 13 | contactName: text('ContactName').notNull(), 14 | contactTitle: text('ContactTitle').notNull(), 15 | address: text('Address').notNull(), 16 | city: text('City').notNull(), 17 | postalCode: text('PostalCode'), 18 | region: text('Region'), 19 | country: text('Country').notNull(), 20 | phone: text('Phone').notNull(), 21 | fax: text('Fax'), 22 | }); 23 | 24 | export type Customers = InferModel; 25 | 26 | export const employees = sqliteTable( 27 | 'Employees', 28 | { 29 | id: integer('EmployeeID').primaryKey(), 30 | lastName: text('LastName').notNull(), 31 | firstName: text('FirstName'), 32 | title: text('Title').notNull(), 33 | titleOfCourtesy: text('TitleOfCourtesy').notNull(), 34 | birthDate: integer('BirthDate', { mode: 'timestamp' }).notNull(), 35 | hireDate: integer('HireDate', { mode: 'timestamp' }).notNull(), 36 | address: text('Address').notNull(), 37 | city: text('City').notNull(), 38 | postalCode: text('PostalCode').notNull(), 39 | country: text('Country').notNull(), 40 | homePhone: text('HomePhone').notNull(), 41 | extension: integer('Extension').notNull(), 42 | notes: text('Notes').notNull(), 43 | reportsTo: integer('ReportsTo'), 44 | photoPath: text('PhotoPath'), 45 | }, 46 | (table) => ({ 47 | reportsToFk: foreignKey(() => ({ 48 | columns: [table.reportsTo], 49 | foreignColumns: [table.id], 50 | })), 51 | }) 52 | ); 53 | 54 | export type Employees = InferModel; 55 | 56 | export const orders = sqliteTable('Orders', { 57 | id: integer('OrderID').primaryKey(), 58 | orderDate: integer('OrderDate', { mode: 'timestamp' }).notNull(), 59 | requiredDate: integer('RequiredDate', { mode: 'timestamp' }).notNull(), 60 | shippedDate: integer('ShippedDate', { mode: 'number' }), 61 | shipVia: integer('ShipVia').notNull(), 62 | freight: numeric('Freight').notNull(), 63 | shipName: text('ShipName').notNull(), 64 | shipCity: text('ShipCity').notNull(), 65 | shipRegion: text('ShipRegion'), 66 | shipPostalCode: text('ShipPostalCode'), 67 | shipCountry: text('ShipCountry').notNull(), 68 | 69 | customerId: text('customerId') 70 | .notNull() 71 | .references(() => customers.id, { onDelete: 'cascade' }), 72 | 73 | employeeId: integer('employeeId') 74 | .notNull() 75 | .references(() => employees.id, { onDelete: 'cascade' }), 76 | }); 77 | 78 | export type Orders = InferModel; 79 | 80 | export const suppliers = sqliteTable('Suppliers', { 81 | id: integer('SupplierID').primaryKey({ autoIncrement: true }), 82 | companyName: text('CompanyName').notNull(), 83 | contactName: text('ContactName').notNull(), 84 | contactTitle: text('ContactTitle').notNull(), 85 | address: text('Address').notNull(), 86 | city: text('City').notNull(), 87 | region: text('Region'), 88 | postalCode: text('PostalCode').notNull(), 89 | country: text('Country').notNull(), 90 | phone: text('Phone').notNull(), 91 | }); 92 | 93 | export type Suppliers = InferModel; 94 | 95 | export const shipper = sqliteTable('Shippers', { 96 | id: integer('ShipperID').primaryKey({ autoIncrement: true }), 97 | companyName: text('CompanyName').notNull(), 98 | phone: text('Phone').notNull(), 99 | }); 100 | 101 | export type Shippers = InferModel; 102 | 103 | export const products = sqliteTable('Products', { 104 | id: integer('ProductID').primaryKey({ autoIncrement: true }), 105 | name: text('ProductName').notNull(), 106 | quantityPerUnit: text('QuantityPerUnit').notNull(), 107 | unitPrice: numeric('UnitPrice').notNull(), 108 | unitsInStock: integer('UnitsInStock').notNull(), 109 | unitsOnOrder: integer('UnitsOnOrder').notNull(), 110 | reorderLevel: integer('ReorderLevel').notNull(), 111 | discontinued: integer('Discontinued').notNull(), 112 | 113 | supplierId: integer('SupplierId') 114 | .notNull() 115 | .references(() => suppliers.id, { onDelete: 'cascade' }), 116 | }); 117 | 118 | export type Products = InferModel; 119 | 120 | export const details = sqliteTable('Order Details', { 121 | unitPrice: numeric('UnitPrice').notNull(), 122 | quantity: integer('Quantity').notNull(), 123 | discount: numeric('Discount').notNull(), 124 | 125 | orderId: integer('OrderID') 126 | .notNull() 127 | .references(() => orders.id, { onDelete: 'cascade' }), 128 | 129 | productId: integer('ProductID') 130 | .notNull() 131 | .references(() => products.id, { onDelete: 'cascade' }), 132 | }); 133 | 134 | export type Detail = InferModel; 135 | -------------------------------------------------------------------------------- /src/hooks/useHover.ts: -------------------------------------------------------------------------------- 1 | import { MutableRefObject, useEffect, useRef, useState } from 'react'; 2 | 3 | export default function useHover(): [MutableRefObject, boolean] { 4 | const [value, setValue] = useState(false); 5 | const ref: any = useRef(null); 6 | const handleMouseOver = (): void => setValue(true); 7 | const handleMouseOut = (): void => setValue(false); 8 | useEffect( 9 | () => { 10 | const node: any = ref.current; 11 | if (node) { 12 | node.addEventListener('mouseover', handleMouseOver); 13 | node.addEventListener('mouseout', handleMouseOut); 14 | return () => { 15 | node.removeEventListener('mouseover', handleMouseOver); 16 | node.removeEventListener('mouseout', handleMouseOut); 17 | }; 18 | } 19 | }, 20 | [ref.current] // Recall only if ref changes 21 | ); 22 | return [ref, value]; 23 | } 24 | -------------------------------------------------------------------------------- /src/hooks/useOnClickOutside.ts: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | 3 | export type Event = React.SyntheticEvent; 4 | export type Callback = () => void; 5 | export type Ref = HTMLDivElement; 6 | 7 | export default (callback: Callback) => { 8 | const containerRef = React.useRef(null); 9 | 10 | useEffect(() => { 11 | const listener = (e: Event) => { 12 | if ( 13 | containerRef.current && 14 | !containerRef.current.contains(e.target as HTMLDivElement) 15 | ) { 16 | callback(); 17 | } 18 | 19 | return null; 20 | }; 21 | 22 | document.body.addEventListener('click', listener as any); 23 | 24 | return () => { 25 | document.body.removeEventListener('click', listener as any); 26 | }; 27 | }, [callback]); 28 | 29 | return containerRef; 30 | }; 31 | -------------------------------------------------------------------------------- /src/hooks/usePagination.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react'; 2 | 3 | export const DOTS = '...'; 4 | 5 | const range = (start: number, end: number) => { 6 | const length = end - start + 1; 7 | return Array.from({ length }, (_, idx) => idx + start); 8 | }; 9 | 10 | export const usePagination = ({ 11 | totalCount, 12 | pageSize, 13 | siblingCount = 1, 14 | currentPage, 15 | }: any) => { 16 | const paginationRange = useMemo(() => { 17 | const totalPageCount = Math.ceil(totalCount / pageSize); 18 | 19 | // Pages count is determined as siblingCount + firstPage + lastPage + currentPage + 2*DOTS 20 | const totalPageNumbers = siblingCount + 5; 21 | 22 | /* 23 | If the number of pages is less than the page numbers we want to show in our 24 | paginationComponent, we return the range [1..totalPageCount] 25 | */ 26 | if (totalPageNumbers >= totalPageCount) { 27 | return range(1, totalPageCount); 28 | } 29 | 30 | const leftSiblingIndex = Math.max(currentPage - siblingCount, 1); 31 | const rightSiblingIndex = Math.min( 32 | currentPage + siblingCount + 3, 33 | totalPageCount 34 | ); 35 | 36 | /* 37 | We do not want to show dots if there is only one position left 38 | after/before the left/right page count as that would lead to a change if our Pagination 39 | component size which we do not want 40 | */ 41 | const shouldShowLeftDots = leftSiblingIndex > 2; 42 | const shouldShowRightDots = rightSiblingIndex < totalPageCount - 2; 43 | 44 | const firstPageIndex = 1; 45 | const lastPageIndex = totalPageCount; 46 | 47 | if (!shouldShowLeftDots && shouldShowRightDots) { 48 | const leftItemCount = 3 + 2 * siblingCount; 49 | const leftRange = range(1, leftItemCount); 50 | 51 | return [...leftRange, DOTS, totalPageCount]; 52 | } 53 | 54 | if (shouldShowLeftDots && !shouldShowRightDots) { 55 | const rightItemCount = 3 + 2 * siblingCount; 56 | const rightRange = range( 57 | totalPageCount - rightItemCount + 1, 58 | totalPageCount 59 | ); 60 | return [firstPageIndex, DOTS, ...rightRange]; 61 | } 62 | 63 | if (shouldShowLeftDots && shouldShowRightDots) { 64 | const middleRange = range(leftSiblingIndex, rightSiblingIndex); 65 | return [firstPageIndex, DOTS, ...middleRange, DOTS, lastPageIndex]; 66 | } 67 | }, [totalCount, pageSize, siblingCount, currentPage]); 68 | 69 | return paginationRange; 70 | }; 71 | -------------------------------------------------------------------------------- /src/icons/ArrowDownIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const ArrowDownIcon = () => { 4 | return ( 5 | 14 | 15 | 16 | ); 17 | }; 18 | 19 | export default ArrowDownIcon; 20 | -------------------------------------------------------------------------------- /src/icons/Ballot.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Ballot = () => { 4 | return ( 5 | 14 | 15 | 19 | 20 | ); 21 | }; 22 | 23 | export default Ballot; 24 | -------------------------------------------------------------------------------- /src/icons/Cart.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Cart = () => { 4 | return ( 5 | 14 | 15 | 16 | ); 17 | }; 18 | 19 | export default Cart; 20 | -------------------------------------------------------------------------------- /src/icons/CheckboxChecked.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const CheckboxChecked = () => { 4 | return ( 5 | 14 | 15 | 16 | 17 | ); 18 | }; 19 | 20 | export default CheckboxChecked; 21 | -------------------------------------------------------------------------------- /src/icons/CheckboxDefault.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const CheckboxDefault = () => { 4 | return ( 5 | 14 | 15 | 16 | ); 17 | }; 18 | 19 | export default CheckboxDefault; 20 | -------------------------------------------------------------------------------- /src/icons/CustomersIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const CustomersIcon = () => { 4 | return ( 5 | 14 | 15 | 19 | 20 | 21 | ); 22 | }; 23 | 24 | export default CustomersIcon; 25 | -------------------------------------------------------------------------------- /src/icons/DashboardIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const DashboardIcon = () => { 4 | return ( 5 | 14 | 15 | 16 | ); 17 | }; 18 | 19 | export default DashboardIcon; 20 | -------------------------------------------------------------------------------- /src/icons/EmployeesIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const EmployeesIcon = () => { 4 | return ( 5 | 14 | 15 | 16 | ); 17 | }; 18 | 19 | export default EmployeesIcon; 20 | -------------------------------------------------------------------------------- /src/icons/HeaderArrowIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const HeaderArrowIcon = () => { 4 | return ( 5 | 14 | 15 | 16 | 17 | ); 18 | }; 19 | 20 | export default HeaderArrowIcon; 21 | -------------------------------------------------------------------------------- /src/icons/HomeIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const HomeIcon = () => { 4 | return ( 5 | 14 | 15 | 16 | 17 | ); 18 | }; 19 | 20 | export default HomeIcon; 21 | -------------------------------------------------------------------------------- /src/icons/InfoIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const InfoIcon = () => { 4 | return ( 5 | 14 | 15 | 16 | 17 | ); 18 | }; 19 | 20 | export default InfoIcon; 21 | -------------------------------------------------------------------------------- /src/icons/LinkIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const LinkIcon = () => { 4 | return ( 5 | 16 | 17 | 18 | 19 | ); 20 | }; 21 | 22 | export default LinkIcon; 23 | -------------------------------------------------------------------------------- /src/icons/MenuIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const MenuIcon = () => { 4 | return ( 5 | 14 | 15 | 16 | ); 17 | }; 18 | 19 | export default MenuIcon; 20 | -------------------------------------------------------------------------------- /src/icons/ProductsIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const ProductsIcon = () => { 4 | return ( 5 | 14 | 15 | 16 | 17 | ); 18 | }; 19 | 20 | export default ProductsIcon; 21 | -------------------------------------------------------------------------------- /src/icons/SearchIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const SearchIcon = () => { 4 | return ( 5 | 14 | 15 | 16 | ); 17 | }; 18 | 19 | export default SearchIcon; 20 | -------------------------------------------------------------------------------- /src/icons/SuppliersIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const SuppliersIcon = () => { 4 | return ( 5 | 14 | 15 | 16 | ); 17 | }; 18 | 19 | export default SuppliersIcon; 20 | -------------------------------------------------------------------------------- /src/icons/nw.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drizzle-team/drizzle-sqljs/a1b0c40e80956c286a8abb964aff7a0374627674/src/icons/nw.sqlite -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 3 | font-size: 16px; 4 | line-height: 24px; 5 | font-weight: 400; 6 | 7 | color-scheme: light dark; 8 | color: rgba(255, 255, 255, 0.87); 9 | background-color: #242424; 10 | 11 | font-synthesis: none; 12 | text-rendering: optimizeLegibility; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | -webkit-text-size-adjust: 100%; 16 | } 17 | 18 | a { 19 | font-weight: 500; 20 | color: #646cff; 21 | text-decoration: inherit; 22 | } 23 | a:hover { 24 | color: #535bf2; 25 | } 26 | 27 | body { 28 | margin: 0; 29 | display: flex; 30 | place-items: center; 31 | min-width: 320px; 32 | min-height: 100vh; 33 | } 34 | 35 | h1 { 36 | font-size: 3.2em; 37 | line-height: 1.1; 38 | } 39 | 40 | button { 41 | border-radius: 8px; 42 | border: 1px solid transparent; 43 | padding: 0.6em 1.2em; 44 | font-size: 1em; 45 | font-weight: 500; 46 | font-family: inherit; 47 | background-color: #1a1a1a; 48 | cursor: pointer; 49 | transition: border-color 0.25s; 50 | } 51 | button:hover { 52 | border-color: #646cff; 53 | } 54 | button:focus, 55 | button:focus-visible { 56 | outline: 4px auto -webkit-focus-ring-color; 57 | } 58 | 59 | @media (prefers-color-scheme: light) { 60 | :root { 61 | color: #213547; 62 | background-color: #ffffff; 63 | } 64 | a:hover { 65 | color: #747bff; 66 | } 67 | button { 68 | background-color: #f9f9f9; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client'; 2 | import { Provider } from 'react-redux'; 3 | import App from "./App"; 4 | import store from "./store"; 5 | 6 | const container = document.getElementById('root')!; 7 | const root = createRoot(container); 8 | root.render( 9 | 10 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /src/pagination.scss: -------------------------------------------------------------------------------- 1 | .pagination-container { 2 | display: flex; 3 | list-style-type: none; 4 | 5 | .pagination-item { 6 | padding: 0 12px; 7 | height: 32px; 8 | text-align: center; 9 | margin: auto 4px; 10 | color: rgba(0, 0, 0, 0.87); 11 | display: flex; 12 | box-sizing: border-box; 13 | align-items: center; 14 | letter-spacing: 0.01071em; 15 | border-radius: 16px; 16 | line-height: 1.43; 17 | font-size: 13px; 18 | min-width: 32px; 19 | 20 | &.dots:hover { 21 | background-color: transparent; 22 | cursor: default; 23 | } 24 | &:hover { 25 | background-color: rgba(0, 0, 0, 0.04); 26 | cursor: pointer; 27 | } 28 | 29 | &.selected { 30 | background-color: rgba(0, 0, 0, 0.08); 31 | } 32 | 33 | .arrow { 34 | &::before { 35 | position: relative; 36 | /* top: 3pt; Uncomment this to lower the icons as requested in comments*/ 37 | content: ''; 38 | /* By using an em scale, the arrows will size with the font */ 39 | display: inline-block; 40 | width: 0.4em; 41 | height: 0.4em; 42 | border-right: 0.12em solid rgba(0, 0, 0, 0.87); 43 | border-top: 0.12em solid rgba(0, 0, 0, 0.87); 44 | } 45 | 46 | &.left { 47 | transform: rotate(-135deg) translate(-50%); 48 | } 49 | 50 | &.right { 51 | transform: rotate(45deg); 52 | } 53 | } 54 | 55 | &.disabled { 56 | pointer-events: none; 57 | 58 | .arrow::before { 59 | border-right: 0.12em solid rgba(0, 0, 0, 0.43); 60 | border-top: 0.12em solid rgba(0, 0, 0, 0.43); 61 | } 62 | 63 | &:hover { 64 | background-color: transparent; 65 | cursor: default; 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/store/actions/common.ts: -------------------------------------------------------------------------------- 1 | import { ThunkAction } from 'redux-thunk'; 2 | 3 | export type AsyncAction = any; 4 | -------------------------------------------------------------------------------- /src/store/actions/login.ts: -------------------------------------------------------------------------------- 1 | import { createActionCreators } from 'immer-reducer'; 2 | import { DashObjType, LoginReducer } from '../reducers/auth'; 3 | import { AsyncAction } from './common'; 4 | 5 | export const loginActions = createActionCreators(LoginReducer); 6 | 7 | export type LoginActions = ReturnType; 8 | 9 | export const setQuery = 10 | (response: DashObjType): AsyncAction => 11 | async (dispatch: any) => { 12 | try { 13 | dispatch(loginActions.setQueryResponseDashboard(response)); 14 | } catch (e) { 15 | // console.log(e); 16 | } 17 | }; 18 | 19 | export const setLoadedFile = 20 | (loadedFile: boolean): AsyncAction => 21 | async (dispatch: any) => { 22 | try { 23 | dispatch(loginActions.setLoadedFile(loadedFile)); 24 | } catch (e) { 25 | // console.log(e); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /src/store/actions/suppliers.ts: -------------------------------------------------------------------------------- 1 | import { createActionCreators } from 'immer-reducer'; 2 | import { AsyncAction } from './common'; 3 | import { SuppliersReducer } from '../reducers/Suppliers'; 4 | 5 | export const supplierAction = createActionCreators(SuppliersReducer); 6 | 7 | export type SupplierActions = ReturnType; 8 | 9 | export const setSuppliersAction = 10 | (response: any): AsyncAction => 11 | async (dispatch: any) => { 12 | try { 13 | dispatch(supplierAction.setSuppliers(response)); 14 | } catch (e) { 15 | // console.log(e); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createMemoryHistory } from 'history'; 2 | import { combineReducers, configureStore } from '@reduxjs/toolkit'; 3 | import loginReducer from './reducers/auth'; 4 | import suppliersReducer from './reducers/Suppliers'; 5 | 6 | export const history = createMemoryHistory(); 7 | 8 | // const rootReducer = combineReducers({ 9 | // router: connectRouter(history), 10 | // loginReducer, 11 | // }); 12 | 13 | const reducers = combineReducers({ 14 | loginReducer, 15 | suppliersReducer, 16 | }); 17 | 18 | export default configureStore({ 19 | reducer: reducers, 20 | }); 21 | 22 | export type State = ReturnType; 23 | -------------------------------------------------------------------------------- /src/store/reducers/Suppliers.ts: -------------------------------------------------------------------------------- 1 | import { createReducerFunction, ImmerReducer } from 'immer-reducer'; 2 | 3 | export type DashObjType = { 4 | query: Array; 5 | time: string; 6 | }; 7 | 8 | export interface SuppliersState { 9 | suppliers: any | null; 10 | } 11 | 12 | const initialState: SuppliersState = { 13 | suppliers: null, 14 | }; 15 | 16 | export class SuppliersReducer extends ImmerReducer { 17 | setSuppliers(suppliers: any) { 18 | this.draftState.suppliers = suppliers; 19 | } 20 | } 21 | 22 | export default createReducerFunction(SuppliersReducer, initialState); 23 | -------------------------------------------------------------------------------- /src/store/reducers/auth.ts: -------------------------------------------------------------------------------- 1 | import { createReducerFunction, ImmerReducer } from 'immer-reducer'; 2 | 3 | export type DashObjType = { 4 | query: Array; 5 | time: string; 6 | }; 7 | 8 | export interface LoginState { 9 | queryResponse: any | null; 10 | queryResponseDashboard: Array | null; 11 | loadedFile: boolean 12 | } 13 | 14 | const initialState: LoginState = { 15 | queryResponse: null, 16 | queryResponseDashboard: null, 17 | loadedFile: false 18 | }; 19 | 20 | export class LoginReducer extends ImmerReducer { 21 | setQueryResponse(queryResponse: any) { 22 | this.draftState.queryResponse = queryResponse; 23 | } 24 | 25 | setLoadedFile(loadedFile: boolean) { 26 | this.draftState.loadedFile = loadedFile; 27 | } 28 | 29 | setQueryResponseDashboard(queryResponseDashboard: DashObjType) { 30 | if (this.draftState.queryResponseDashboard) { 31 | this.draftState.queryResponseDashboard = [ 32 | ...this.draftState.queryResponseDashboard, 33 | queryResponseDashboard, 34 | ]; 35 | } else { 36 | this.draftState.queryResponseDashboard = [queryResponseDashboard]; 37 | } 38 | } 39 | 40 | 41 | clearQuery(queryResponse: any) { 42 | this.draftState.queryResponseDashboard = null; 43 | } 44 | } 45 | 46 | export default createReducerFunction(LoginReducer, initialState); 47 | -------------------------------------------------------------------------------- /src/store/selectors/auth.ts: -------------------------------------------------------------------------------- 1 | import { createSelector, Selector } from 'reselect'; 2 | import { State } from '../index'; 3 | import { DashObjType } from '../reducers/auth'; 4 | 5 | const selectLogin = (state: State) => state.loginReducer; 6 | 7 | export const selectQuery: Selector | null> = 8 | createSelector( 9 | selectLogin, 10 | ({ queryResponseDashboard }) => queryResponseDashboard 11 | ); 12 | 13 | export const selectLoadedFile: Selector = 14 | createSelector( 15 | selectLogin, 16 | ({ loadedFile }) => loadedFile 17 | ); 18 | -------------------------------------------------------------------------------- /src/store/selectors/suppliers.ts: -------------------------------------------------------------------------------- 1 | import { createSelector, Selector } from 'reselect'; 2 | import { State } from '../index'; 3 | 4 | const selectSupplier = (state: State) => state.suppliersReducer; 5 | 6 | export const selectSuppliers: Selector = 7 | createSelector(selectSupplier, ({ suppliers }) => suppliers); 8 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | assetsInclude: ['**/*.sqlite'] 8 | }) 9 | --------------------------------------------------------------------------------