├── .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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
16 | );
17 | };
18 |
19 | export default ArrowDownIcon;
20 |
--------------------------------------------------------------------------------
/src/icons/Ballot.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Ballot = () => {
4 | return (
5 |
20 | );
21 | };
22 |
23 | export default Ballot;
24 |
--------------------------------------------------------------------------------
/src/icons/Cart.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Cart = () => {
4 | return (
5 |
16 | );
17 | };
18 |
19 | export default Cart;
20 |
--------------------------------------------------------------------------------
/src/icons/CheckboxChecked.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const CheckboxChecked = () => {
4 | return (
5 |
17 | );
18 | };
19 |
20 | export default CheckboxChecked;
21 |
--------------------------------------------------------------------------------
/src/icons/CheckboxDefault.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const CheckboxDefault = () => {
4 | return (
5 |
16 | );
17 | };
18 |
19 | export default CheckboxDefault;
20 |
--------------------------------------------------------------------------------
/src/icons/CustomersIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const CustomersIcon = () => {
4 | return (
5 |
21 | );
22 | };
23 |
24 | export default CustomersIcon;
25 |
--------------------------------------------------------------------------------
/src/icons/DashboardIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const DashboardIcon = () => {
4 | return (
5 |
16 | );
17 | };
18 |
19 | export default DashboardIcon;
20 |
--------------------------------------------------------------------------------
/src/icons/EmployeesIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const EmployeesIcon = () => {
4 | return (
5 |
16 | );
17 | };
18 |
19 | export default EmployeesIcon;
20 |
--------------------------------------------------------------------------------
/src/icons/HeaderArrowIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const HeaderArrowIcon = () => {
4 | return (
5 |
17 | );
18 | };
19 |
20 | export default HeaderArrowIcon;
21 |
--------------------------------------------------------------------------------
/src/icons/HomeIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const HomeIcon = () => {
4 | return (
5 |
17 | );
18 | };
19 |
20 | export default HomeIcon;
21 |
--------------------------------------------------------------------------------
/src/icons/InfoIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const InfoIcon = () => {
4 | return (
5 |
17 | );
18 | };
19 |
20 | export default InfoIcon;
21 |
--------------------------------------------------------------------------------
/src/icons/LinkIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const LinkIcon = () => {
4 | return (
5 |
19 | );
20 | };
21 |
22 | export default LinkIcon;
23 |
--------------------------------------------------------------------------------
/src/icons/MenuIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const MenuIcon = () => {
4 | return (
5 |
16 | );
17 | };
18 |
19 | export default MenuIcon;
20 |
--------------------------------------------------------------------------------
/src/icons/ProductsIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const ProductsIcon = () => {
4 | return (
5 |
17 | );
18 | };
19 |
20 | export default ProductsIcon;
21 |
--------------------------------------------------------------------------------
/src/icons/SearchIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const SearchIcon = () => {
4 | return (
5 |
16 | );
17 | };
18 |
19 | export default SearchIcon;
20 |
--------------------------------------------------------------------------------
/src/icons/SuppliersIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const SuppliersIcon = () => {
4 | return (
5 |
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 |
--------------------------------------------------------------------------------
]