├── .eslintrc.cjs
├── .gitignore
├── LICENSE
├── README.md
├── index.html
├── package.json
├── postcss.config.js
├── public
└── logo.svg
├── src
├── App.jsx
├── api
│ └── api.js
├── assets
│ ├── icons
│ │ ├── active.svg
│ │ ├── airremote-logo-short.svg
│ │ ├── airremote-logo.svg
│ │ ├── apple-logo.svg
│ │ ├── arrow-right.svg
│ │ ├── automation-color.svg
│ │ ├── automations.svg
│ │ ├── avatar.svg
│ │ ├── circle-button-1.svg
│ │ ├── circle-button.svg
│ │ ├── cloud-network.svg
│ │ ├── controller.svg
│ │ ├── cpu.svg
│ │ ├── cross-white.svg
│ │ ├── device-coloured.svg
│ │ ├── edit-pencil.svg
│ │ ├── engineering.svg
│ │ ├── esp.svg
│ │ ├── facebook-logo.svg
│ │ ├── github-logo.svg
│ │ ├── github-mark.svg
│ │ ├── google-logo.svg
│ │ ├── green-circle.svg
│ │ ├── home.svg
│ │ ├── inactive.svg
│ │ ├── left-arrow.svg
│ │ ├── logoipsum-262.svg
│ │ ├── microcontroller.svg
│ │ ├── money.svg
│ │ ├── plus-button-white.svg
│ │ ├── plus-button.svg
│ │ ├── plus-sign.svg
│ │ ├── power.svg
│ │ ├── red-circle.svg
│ │ ├── remote-access.svg
│ │ ├── remote-colored.svg
│ │ ├── remote-control.svg
│ │ ├── remote-teal.svg
│ │ ├── remote.svg
│ │ ├── round-blue-button.svg
│ │ ├── schedule.svg
│ │ └── woman-2.svg
│ ├── imgs
│ │ ├── air-conditioner-white.png
│ │ ├── air-remote-demo-short.gif
│ │ ├── air-remote-logo.jpg
│ │ ├── air-remote-wifi-setup.gif
│ │ ├── air-white-side.png
│ │ ├── airconditioner-black-side.png
│ │ ├── airconditioner.png
│ │ ├── automations.gif
│ │ ├── dehumidifier-black.png
│ │ ├── dehumidifier-white.png
│ │ ├── demo.gif
│ │ ├── device-esp32.png
│ │ ├── device-espressif.png
│ │ ├── esp32.png
│ │ ├── fan.png
│ │ ├── generic-device-black.png
│ │ ├── generic-device-blue.png
│ │ ├── heater-black-side.png
│ │ ├── heater-black.png
│ │ ├── heater.png
│ │ ├── heater2.png
│ │ ├── login.gif
│ │ ├── logo-black.png
│ │ ├── logo-white.png
│ │ ├── logo.png
│ │ ├── microcontroller3.png
│ │ ├── pcb-sketch.png
│ │ ├── rearrange.gif
│ │ ├── rgb-strip.png
│ │ ├── short-air-remote-logo.png
│ │ ├── speakers-black-2.png
│ │ ├── speakers-black.png
│ │ ├── speakers.png
│ │ ├── tv-gray.png
│ │ ├── tv.png
│ │ ├── tvbox.png
│ │ ├── universal-device.png
│ │ └── wifi-setup.gif
│ └── lotties
│ │ ├── checkbox-animation-2.json
│ │ ├── checkbox-animation-full.json
│ │ ├── checkbox-animation-light.json
│ │ ├── checkbox-animation.json
│ │ ├── checkbox-custom.json
│ │ ├── error-animation.json
│ │ ├── green-spinner.json
│ │ ├── purple-spinner.json
│ │ ├── walmart-spinner.json
│ │ └── x-animation.json
├── components
│ ├── EmptyTabs.jsx
│ ├── EmptyTiles.jsx
│ ├── InfoBar.jsx
│ ├── ModalAddAutomation.jsx
│ ├── ModalAddButton.jsx
│ ├── ModalAddDevice.jsx
│ ├── ModalAddRemote.jsx
│ ├── ModalAddUser.jsx
│ ├── ModalError.jsx
│ ├── NavigationBar.jsx
│ ├── NoticeBox.jsx
│ ├── RemoteButton.jsx
│ ├── Sidebar.jsx
│ ├── TileAutomation.jsx
│ ├── TileDelete.jsx
│ ├── TileDevice.jsx
│ ├── TileGrid.jsx
│ ├── TileList.jsx
│ ├── TileRemote.jsx
│ ├── TileRemoteButton.jsx
│ ├── Toolbar.jsx
│ └── TopToolbar.jsx
├── configs
│ └── config.js
├── contexts
│ ├── AuthContext.jsx
│ ├── DraggingContext.jsx
│ └── EditModeContext.jsx
├── hooks
│ ├── useDelete.js
│ ├── useError.js
│ ├── useFetch.js
│ ├── useFetchMemo.js
│ ├── useKeepAlive.js
│ └── usePost.js
├── index.css
├── main.jsx
├── pages
│ ├── Automations.jsx
│ ├── Dashboard.jsx
│ ├── Devices.jsx
│ ├── Loading.jsx
│ ├── Login.jsx
│ ├── OAuth2Callback.jsx
│ ├── RemoteButtons.jsx
│ ├── Remotes.jsx
│ └── SignUp.jsx
└── services
│ ├── authenticate.js
│ ├── oauth2.js
│ └── websocket.js
├── tailwind.config.js
├── tmp
├── acm_validation_config.json
├── caching_policy.json
├── cloudfront_distribution_config.json
├── origin_access_control_config.json
├── route_53_record.json
└── s3_bucket_policy.json
└── vite.config.js
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: { browser: true, es2020: true },
4 | extends: [
5 | 'eslint:recommended',
6 | 'plugin:react/recommended',
7 | 'plugin:react/jsx-runtime',
8 | 'plugin:react-hooks/recommended',
9 | ],
10 | ignorePatterns: ['dist', '.eslintrc.cjs'],
11 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
12 | settings: { react: { version: '18.2' } },
13 | plugins: ['react-refresh'],
14 | rules: {
15 | 'react/jsx-no-target-blank': 'off',
16 | 'react-refresh/only-export-components': [
17 | 'warn',
18 | { allowConstantExport: true },
19 | ],
20 | },
21 | }
22 |
--------------------------------------------------------------------------------
/.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 | .env
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | AirRemote
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "air-remote",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "@dnd-kit/core": "^6.1.0",
14 | "@dnd-kit/sortable": "^8.0.0",
15 | "@nextui-org/modal": "^2.0.39",
16 | "@nextui-org/react": "^2.4.6",
17 | "amazon-cognito-identity-js": "^6.3.12",
18 | "axios": "^1.7.7",
19 | "crypto-js": "^4.2.0",
20 | "framer-motion": "^11.3.21",
21 | "lottie-react": "^2.4.0",
22 | "lucide-react": "^0.424.0",
23 | "react": "^18.3.1",
24 | "react-dom": "^18.3.1",
25 | "react-router-dom": "^6.26.0",
26 | "tailwindcss": "^3.4.7",
27 | "vite-plugin-svgr": "^4.2.0"
28 | },
29 | "devDependencies": {
30 | "@types/react": "^18.3.3",
31 | "@types/react-dom": "^18.3.0",
32 | "@vitejs/plugin-basic-ssl": "^1.1.0",
33 | "@vitejs/plugin-react": "^4.3.1",
34 | "autoprefixer": "^10.4.20",
35 | "eslint": "^8.57.0",
36 | "eslint-plugin-react": "^7.34.3",
37 | "eslint-plugin-react-hooks": "^4.6.2",
38 | "eslint-plugin-react-refresh": "^0.4.7",
39 | "postcss": "^8.4.40",
40 | "vite": "^5.3.4"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/App.jsx:
--------------------------------------------------------------------------------
1 | import React, {useEffect, useState} from 'react';
2 |
3 | import { BrowserRouter, Route, Routes, Link, Navigate, Outlet } from 'react-router-dom';
4 | import { Sidebar, SidebarItem } from './components/Sidebar';
5 | import {NavigationBar, NavigationBarItem} from './components/NavigationBar';
6 | import { LayoutDashboard, Usb, CalendarCog } from 'lucide-react';
7 | import config from "./configs/config";
8 | import Remote2 from './assets/icons/remote.svg?react';
9 |
10 | import Remote from './assets/icons/remote-access.svg?react';
11 | import Automations from './pages/Automations';
12 | import Devices from './pages/Devices';
13 | import Remotes from './pages/Remotes';
14 | import Loading from './pages/Loading';
15 | import RemoteButtons from './pages/RemoteButtons';
16 | import Dashboard from './pages/Dashboard';
17 | import SignUp from './pages/SignUp';
18 | import Login from './pages/Login';
19 | import OAuth2Callback from './pages/OAuth2Callback';
20 | import { useLocation, useNavigate } from 'react-router-dom';
21 | import { AuthProvider, useAuth } from './contexts/AuthContext';
22 | import useKeepAlive from './hooks/useKeepAlive';
23 | function App() {
24 | const apiUrl = config.apiUrl;
25 | const baseUrl = config.baseUrl;
26 | const authUrl = config.authUrl;
27 |
28 | useKeepAlive(`${apiUrl}/keep-alive`, 6, 15000); // Keep alive data endpoint instances
29 | useKeepAlive(`${baseUrl}/wss/keep-alive`, 1, 15000); // Keep alive websocket handler instance
30 | useKeepAlive(`${authUrl}/keep-alive`, 1, 10000); // Keep alive refresh token handler instance
31 |
32 | const location = useLocation();
33 | const navigate = useNavigate();
34 | return (
35 |
36 |
37 |
38 | {/* Authenticated Routes */}
39 | }>
40 | } />
41 |
42 |
43 | }>
44 | } />
45 |
46 |
47 | }>
48 | } />
49 |
50 |
51 | }>
52 | } />
53 |
54 |
55 | }>
56 | } />
57 |
58 |
59 | {/* Unauthenticated Routes */}
60 | }>
61 | } />
62 |
63 | }>
64 | } />
65 |
66 |
67 | }>
68 | }/>
69 |
70 |
71 |
72 |
73 |
74 | )
75 | }
76 |
77 | const Navigation = ({children}) => (
78 | <>
79 |
80 | } text="Dashboard" to="/"/>
81 | } text="Remotes" to="/remotes" />
82 | } text="Devices" to="/devices"/>
83 | } text="Automations" to="/automations"/>
84 |
85 | {children}
86 |
87 | } text="Dashboard" to="/"/>
88 | } text="Remotes" to="/remotes"/>
89 | } text="Devices" to="/devices"/>
90 | } text="Automations" to="/automations"/>
91 |
92 |
93 | >
94 | )
95 |
96 | const PrivateRoute = () => {
97 | const { isAuthenticated, refreshLoading } = useAuth();
98 |
99 | return refreshLoading ?
100 |
101 | :
102 | isAuthenticated ?
103 | :
104 |
105 | };
106 |
107 | const PublicRoute = ({ children }) => {
108 | const { isAuthenticated } = useAuth();
109 |
110 | return !isAuthenticated ? : ;
111 | };
112 |
113 | export default App;
--------------------------------------------------------------------------------
/src/api/api.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import config from "../configs/config";
3 |
4 | const api = axios.create({
5 | baseURL: config.baseUrl,
6 | })
7 |
8 | export default api;
--------------------------------------------------------------------------------
/src/assets/icons/active.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/airremote-logo-short.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/assets/icons/apple-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/assets/icons/arrow-right.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/src/assets/icons/automation-color.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/automations.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/src/assets/icons/avatar.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
11 |
12 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/src/assets/icons/circle-button-1.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/assets/icons/cloud-network.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/controller.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/src/assets/icons/cpu.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
44 |
45 |
46 |
47 |
48 |
55 |
56 |
57 |
58 |
59 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
--------------------------------------------------------------------------------
/src/assets/icons/cross-white.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/src/assets/icons/device-coloured.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
37 |
38 |
40 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
69 |
--------------------------------------------------------------------------------
/src/assets/icons/edit-pencil.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
pencil_line
7 |
--------------------------------------------------------------------------------
/src/assets/icons/esp.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/facebook-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
15 |
17 |
20 |
23 |
24 |
25 |
34 |
39 |
41 |
44 |
47 |
51 |
52 |
55 |
59 |
60 |
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/src/assets/icons/github-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | github [#142]
6 | Created with Sketch.
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/assets/icons/github-mark.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/google-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/icons/green-circle.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/icons/home.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/inactive.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/left-arrow.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/src/assets/icons/logoipsum-262.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/microcontroller.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/plus-button-white.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/src/assets/icons/plus-button.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
11 |
--------------------------------------------------------------------------------
/src/assets/icons/plus-sign.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/power.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/red-circle.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/icons/remote-access.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/remote-control.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/remote-teal.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/assets/icons/remote.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
8 |
11 |
15 |
19 |
23 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/src/assets/icons/schedule.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/woman-2.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/imgs/air-conditioner-white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jugeekuz/AirRemote-Frontend/a15a93e84fb15fdf169406779aae7b2138a09f77/src/assets/imgs/air-conditioner-white.png
--------------------------------------------------------------------------------
/src/assets/imgs/air-remote-demo-short.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jugeekuz/AirRemote-Frontend/a15a93e84fb15fdf169406779aae7b2138a09f77/src/assets/imgs/air-remote-demo-short.gif
--------------------------------------------------------------------------------
/src/assets/imgs/air-remote-logo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jugeekuz/AirRemote-Frontend/a15a93e84fb15fdf169406779aae7b2138a09f77/src/assets/imgs/air-remote-logo.jpg
--------------------------------------------------------------------------------
/src/assets/imgs/air-remote-wifi-setup.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jugeekuz/AirRemote-Frontend/a15a93e84fb15fdf169406779aae7b2138a09f77/src/assets/imgs/air-remote-wifi-setup.gif
--------------------------------------------------------------------------------
/src/assets/imgs/air-white-side.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jugeekuz/AirRemote-Frontend/a15a93e84fb15fdf169406779aae7b2138a09f77/src/assets/imgs/air-white-side.png
--------------------------------------------------------------------------------
/src/assets/imgs/airconditioner-black-side.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jugeekuz/AirRemote-Frontend/a15a93e84fb15fdf169406779aae7b2138a09f77/src/assets/imgs/airconditioner-black-side.png
--------------------------------------------------------------------------------
/src/assets/imgs/airconditioner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jugeekuz/AirRemote-Frontend/a15a93e84fb15fdf169406779aae7b2138a09f77/src/assets/imgs/airconditioner.png
--------------------------------------------------------------------------------
/src/assets/imgs/automations.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jugeekuz/AirRemote-Frontend/a15a93e84fb15fdf169406779aae7b2138a09f77/src/assets/imgs/automations.gif
--------------------------------------------------------------------------------
/src/assets/imgs/dehumidifier-black.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jugeekuz/AirRemote-Frontend/a15a93e84fb15fdf169406779aae7b2138a09f77/src/assets/imgs/dehumidifier-black.png
--------------------------------------------------------------------------------
/src/assets/imgs/dehumidifier-white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jugeekuz/AirRemote-Frontend/a15a93e84fb15fdf169406779aae7b2138a09f77/src/assets/imgs/dehumidifier-white.png
--------------------------------------------------------------------------------
/src/assets/imgs/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jugeekuz/AirRemote-Frontend/a15a93e84fb15fdf169406779aae7b2138a09f77/src/assets/imgs/demo.gif
--------------------------------------------------------------------------------
/src/assets/imgs/device-esp32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jugeekuz/AirRemote-Frontend/a15a93e84fb15fdf169406779aae7b2138a09f77/src/assets/imgs/device-esp32.png
--------------------------------------------------------------------------------
/src/assets/imgs/device-espressif.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jugeekuz/AirRemote-Frontend/a15a93e84fb15fdf169406779aae7b2138a09f77/src/assets/imgs/device-espressif.png
--------------------------------------------------------------------------------
/src/assets/imgs/esp32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jugeekuz/AirRemote-Frontend/a15a93e84fb15fdf169406779aae7b2138a09f77/src/assets/imgs/esp32.png
--------------------------------------------------------------------------------
/src/assets/imgs/fan.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jugeekuz/AirRemote-Frontend/a15a93e84fb15fdf169406779aae7b2138a09f77/src/assets/imgs/fan.png
--------------------------------------------------------------------------------
/src/assets/imgs/generic-device-black.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jugeekuz/AirRemote-Frontend/a15a93e84fb15fdf169406779aae7b2138a09f77/src/assets/imgs/generic-device-black.png
--------------------------------------------------------------------------------
/src/assets/imgs/generic-device-blue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jugeekuz/AirRemote-Frontend/a15a93e84fb15fdf169406779aae7b2138a09f77/src/assets/imgs/generic-device-blue.png
--------------------------------------------------------------------------------
/src/assets/imgs/heater-black-side.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jugeekuz/AirRemote-Frontend/a15a93e84fb15fdf169406779aae7b2138a09f77/src/assets/imgs/heater-black-side.png
--------------------------------------------------------------------------------
/src/assets/imgs/heater-black.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jugeekuz/AirRemote-Frontend/a15a93e84fb15fdf169406779aae7b2138a09f77/src/assets/imgs/heater-black.png
--------------------------------------------------------------------------------
/src/assets/imgs/heater.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jugeekuz/AirRemote-Frontend/a15a93e84fb15fdf169406779aae7b2138a09f77/src/assets/imgs/heater.png
--------------------------------------------------------------------------------
/src/assets/imgs/heater2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jugeekuz/AirRemote-Frontend/a15a93e84fb15fdf169406779aae7b2138a09f77/src/assets/imgs/heater2.png
--------------------------------------------------------------------------------
/src/assets/imgs/login.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jugeekuz/AirRemote-Frontend/a15a93e84fb15fdf169406779aae7b2138a09f77/src/assets/imgs/login.gif
--------------------------------------------------------------------------------
/src/assets/imgs/logo-black.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jugeekuz/AirRemote-Frontend/a15a93e84fb15fdf169406779aae7b2138a09f77/src/assets/imgs/logo-black.png
--------------------------------------------------------------------------------
/src/assets/imgs/logo-white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jugeekuz/AirRemote-Frontend/a15a93e84fb15fdf169406779aae7b2138a09f77/src/assets/imgs/logo-white.png
--------------------------------------------------------------------------------
/src/assets/imgs/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jugeekuz/AirRemote-Frontend/a15a93e84fb15fdf169406779aae7b2138a09f77/src/assets/imgs/logo.png
--------------------------------------------------------------------------------
/src/assets/imgs/microcontroller3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jugeekuz/AirRemote-Frontend/a15a93e84fb15fdf169406779aae7b2138a09f77/src/assets/imgs/microcontroller3.png
--------------------------------------------------------------------------------
/src/assets/imgs/pcb-sketch.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jugeekuz/AirRemote-Frontend/a15a93e84fb15fdf169406779aae7b2138a09f77/src/assets/imgs/pcb-sketch.png
--------------------------------------------------------------------------------
/src/assets/imgs/rearrange.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jugeekuz/AirRemote-Frontend/a15a93e84fb15fdf169406779aae7b2138a09f77/src/assets/imgs/rearrange.gif
--------------------------------------------------------------------------------
/src/assets/imgs/rgb-strip.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jugeekuz/AirRemote-Frontend/a15a93e84fb15fdf169406779aae7b2138a09f77/src/assets/imgs/rgb-strip.png
--------------------------------------------------------------------------------
/src/assets/imgs/short-air-remote-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jugeekuz/AirRemote-Frontend/a15a93e84fb15fdf169406779aae7b2138a09f77/src/assets/imgs/short-air-remote-logo.png
--------------------------------------------------------------------------------
/src/assets/imgs/speakers-black-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jugeekuz/AirRemote-Frontend/a15a93e84fb15fdf169406779aae7b2138a09f77/src/assets/imgs/speakers-black-2.png
--------------------------------------------------------------------------------
/src/assets/imgs/speakers-black.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jugeekuz/AirRemote-Frontend/a15a93e84fb15fdf169406779aae7b2138a09f77/src/assets/imgs/speakers-black.png
--------------------------------------------------------------------------------
/src/assets/imgs/speakers.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jugeekuz/AirRemote-Frontend/a15a93e84fb15fdf169406779aae7b2138a09f77/src/assets/imgs/speakers.png
--------------------------------------------------------------------------------
/src/assets/imgs/tv-gray.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jugeekuz/AirRemote-Frontend/a15a93e84fb15fdf169406779aae7b2138a09f77/src/assets/imgs/tv-gray.png
--------------------------------------------------------------------------------
/src/assets/imgs/tv.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jugeekuz/AirRemote-Frontend/a15a93e84fb15fdf169406779aae7b2138a09f77/src/assets/imgs/tv.png
--------------------------------------------------------------------------------
/src/assets/imgs/tvbox.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jugeekuz/AirRemote-Frontend/a15a93e84fb15fdf169406779aae7b2138a09f77/src/assets/imgs/tvbox.png
--------------------------------------------------------------------------------
/src/assets/imgs/universal-device.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jugeekuz/AirRemote-Frontend/a15a93e84fb15fdf169406779aae7b2138a09f77/src/assets/imgs/universal-device.png
--------------------------------------------------------------------------------
/src/assets/imgs/wifi-setup.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jugeekuz/AirRemote-Frontend/a15a93e84fb15fdf169406779aae7b2138a09f77/src/assets/imgs/wifi-setup.gif
--------------------------------------------------------------------------------
/src/assets/lotties/checkbox-animation.json:
--------------------------------------------------------------------------------
1 | {"v":"5.6.6","fr":60,"ip":0,"op":72,"w":400,"h":400,"nm":"check-ok","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"check","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[200,204,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[90,90,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-39.75,3.25],[-19.5,25],[31,-24.5]],"o":[[-39.75,3.25],[-16.5,25],[38.5,-32.5]],"v":[[-39.75,3.25],[-18,25],[34.75,-28.5]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":14,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.233],"y":[0.983]},"o":{"x":[0.615],"y":[0.027]},"t":20,"s":[0]},{"t":45,"s":[99.491]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":20,"op":286,"st":20,"bm":0,"completed":true},{"ddd":0,"ind":2,"ty":4,"nm":"rounder","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[197.336,199.336,0],"ix":2},"a":{"a":0,"k":[12,30,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.863,0.863,0.333],"y":[0.014,0.014,0]},"t":0,"s":[0,0,100]},{"t":20,"s":[73.018,73.018,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[253.328,253.328],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false,"_render":true},{"ty":"fl","c":{"a":0,"k":[0.1176,0.4588,0.1333,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false,"_render":true},{"ty":"tr","p":{"a":0,"k":[15.359,31.031],"ix":2},"a":{"a":0,"k":[0.695,0.367],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform","_render":true}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false,"_render":true}],"ip":0,"op":302,"st":0,"bm":0,"completed":true},{"ddd":0,"ind":3,"ty":4,"nm":"outline 2","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":-35,"s":[0]},{"i":{"x":[0.771],"y":[-11.591]},"o":{"x":[0.154],"y":[0]},"t":-27,"s":[0]},{"i":{"x":[0.725],"y":[1]},"o":{"x":[0.389],"y":[0.01]},"t":16,"s":[0]},{"t":18,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[197.336,199.336,0],"ix":2},"a":{"a":0,"k":[12,30,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.475,0.475,0.667],"y":[0.535,0.535,-24.786]},"o":{"x":[0.464,0.464,0.333],"y":[0,0,0]},"t":-5,"s":[0,0,100]},{"i":{"x":[0.533,0.533,0.667],"y":[0.999,0.999,1]},"o":{"x":[0.223,0.223,0.333],"y":[0.376,0.376,29.15]},"t":18,"s":[66.563,66.563,100]},{"t":44,"s":[106,106,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[253.328,253.328],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false,"_render":true},{"ty":"st","c":{"a":0,"k":[0.1176,0.4588,0.1333,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.811],"y":[1]},"o":{"x":[0.296],"y":[0]},"t":13,"s":[18]},{"t":44,"s":[0]}],"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false,"_render":true},{"ty":"tr","p":{"a":0,"k":[15.359,31.031],"ix":2},"a":{"a":0,"k":[0.695,0.367],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform","_render":true}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false,"_render":true},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false,"_render":true}],"ip":-11,"op":153,"st":-51,"bm":0,"completed":true}],"markers":[],"__complete":true}
--------------------------------------------------------------------------------
/src/assets/lotties/checkbox-custom.json:
--------------------------------------------------------------------------------
1 | {"nm":"Main Scene","ddd":0,"h":500,"w":500,"meta":{"g":"@lottiefiles/creator 1.24.1"},"layers":[{"ty":0,"nm":" Success Checkmark","sr":1,"st":0,"op":40,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[41,37]},"s":{"a":0,"k":[661.4965,669.9699]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[256.615,229.9009]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"w":80,"h":80,"refId":"precomp_Success Checkmark_0815ef68-281b-40f2-acbb-ce64377fcbc1","ind":1}],"v":"5.7.0","fr":30,"op":40,"ip":0,"assets":[{"nm":"Success Checkmark","id":"precomp_Success Checkmark_0815ef68-281b-40f2-acbb-ce64377fcbc1","layers":[{"ty":4,"nm":"Check Mark","sr":1,"st":0,"op":40,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[-1.313,6,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[40,40,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Shape 1","ix":1,"cix":2,"np":3,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-15.75,8],[-8,16],[13.125,-4]]},"ix":2}},{"ty":"tm","bm":0,"hd":false,"mn":"ADBE Vector Filter - Trim","nm":"Trim Paths 1","ix":2,"e":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":0,"ix":3},"s":{"a":1,"k":[{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[0],"t":25},{"s":[100],"t":33}],"ix":1},"m":1},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":2,"lj":2,"ml":1,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":3,"ix":5},"c":{"a":0,"k":[1,1,1],"ix":3}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":1},{"ty":4,"nm":"Circle Flash","sr":1,"st":0,"op":40,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[0,0,100],"t":25},{"s":[100,100,100],"t":30}],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[40,40,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[0],"t":25},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[98],"t":30},{"s":[0],"t":38}],"ix":11}},"shapes":[{"ty":"el","bm":0,"hd":false,"mn":"ADBE Vector Shape - Ellipse","nm":"Ellipse Path 1","d":1,"p":{"a":0,"k":[0,0],"ix":3},"s":{"a":0,"k":[64,64],"ix":2}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[0.5294,0.9608,0.4471],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}}],"ind":2},{"ty":4,"nm":"Circle Stroke","sr":1,"st":0,"op":40,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[100,100,100],"t":16},{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[80,80,100],"t":22},{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[120,120,100],"t":25},{"s":[100,100,100],"t":29}],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[39.022,39.022,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Ellipse 1","ix":1,"cix":2,"np":3,"it":[{"ty":"el","bm":0,"hd":false,"mn":"ADBE Vector Shape - Ellipse","nm":"Ellipse Path 1","d":1,"p":{"a":0,"k":[0,0],"ix":3},"s":{"a":0,"k":[60,60],"ix":2}},{"ty":"tm","bm":0,"hd":false,"mn":"ADBE Vector Filter - Trim","nm":"Trim Paths 1","ix":2,"e":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":0,"ix":3},"s":{"a":1,"k":[{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[0],"t":0},{"s":[100],"t":16}],"ix":1},"m":1},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":2,"lj":2,"ml":1,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":3,"ix":5},"c":{"a":0,"k":[0.4275,0.8,0.3569],"ix":3}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[0.978,0.978],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":3},{"ty":4,"nm":"Circle Green Fill","sr":1,"st":0,"op":40,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[0,0,100],"t":21},{"s":[100,100,100],"t":28}],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[40,40,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[0],"t":21},{"s":[98],"t":28}],"ix":11}},"shapes":[{"ty":"el","bm":0,"hd":false,"mn":"ADBE Vector Shape - Ellipse","nm":"Ellipse Path 1","d":1,"p":{"a":0,"k":[0,0],"ix":3},"s":{"a":0,"k":[64,64],"ix":2}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[0.4275,0.8,0.3569],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}}],"ind":4}]}]}
--------------------------------------------------------------------------------
/src/assets/lotties/error-animation.json:
--------------------------------------------------------------------------------
1 | {"nm":"Error","ddd":0,"h":400,"w":400,"meta":{"g":"@lottiefiles/toolkit-js 0.33.2"},"layers":[{"ty":4,"nm":"POINT","sr":1,"st":1.00000004073083,"op":901.000036698482,"ip":1.00000004073083,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[0,70,0],"ix":1},"s":{"a":1,"k":[{"o":{"x":0.178,"y":0},"i":{"x":0.585,"y":0.754},"s":[0,0,100],"t":15},{"o":{"x":0.348,"y":-1.651},"i":{"x":0.701,"y":1},"s":[120,120,100],"t":18},{"s":[100,100,100],"t":22.0000008960784}],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[200,269.5,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Ellipse 1","ix":1,"cix":2,"np":3,"it":[{"ty":"el","bm":0,"hd":false,"mn":"ADBE Vector Shape - Ellipse","nm":"Ellipse Path 1","d":1,"p":{"a":0,"k":[0,0],"ix":3},"s":{"a":0,"k":[400,400],"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"c":{"a":0,"k":[1,1,1],"ix":3}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[1,1,1],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[8.39,8.39],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[0,70.168],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":1},{"ty":4,"nm":"Ex","sr":1,"st":1.00000004073083,"op":901.000036698482,"ip":1.00000004073083,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[6,-16,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[200.275,203,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Shape 1","ix":1,"cix":2,"np":3,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[0,-31.833]],"o":[[0,0],[0,0]],"v":[[6,-96],[6,-0.5]]},"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":2,"lj":1,"ml":4,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":32,"ix":5},"c":{"a":0,"k":[1,1,1],"ix":3}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[0.9216,0.0471,0.0471],"ix":4},"r":1,"o":{"a":0,"k":0,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[90.821,90.821],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]},{"ty":"tm","bm":0,"hd":false,"mn":"ADBE Vector Filter - Trim","nm":"Trim Paths 1","ix":2,"e":{"a":1,"k":[{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[0],"t":9},{"s":[100],"t":15.0000006109625}],"ix":2},"o":{"a":0,"k":0,"ix":3},"s":{"a":0,"k":0,"ix":1},"m":1}],"ind":2},{"ty":4,"nm":"MAIN 3","sr":1,"st":0,"op":900.000036657751,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[0,0,100],"t":0},{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[115,115,100],"t":6},{"o":{"x":0.167,"y":0},"i":{"x":0.833,"y":1},"s":[85,85,100],"t":12},{"s":[90,90,100],"t":16.0000006516934}],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[200,200,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Ellipse 1","ix":1,"cix":2,"np":3,"it":[{"ty":"el","bm":0,"hd":false,"mn":"ADBE Vector Shape - Ellipse","nm":"Ellipse Path 1","d":1,"p":{"a":0,"k":[0,0],"ix":3},"s":{"a":0,"k":[400,400],"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"c":{"a":0,"k":[1,1,1],"ix":3}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[0.9216,0.0471,0.0471],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[72.319,72.319],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":3}],"v":"5.6.5","fr":30,"op":30.0000012219251,"ip":0,"assets":[]}
--------------------------------------------------------------------------------
/src/assets/lotties/green-spinner.json:
--------------------------------------------------------------------------------
1 | {"v":"5.1.8","fr":29.9700012207031,"ip":0,"op":60.0000024438501,"w":100,"h":100,"nm":"Spinner","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"spinner Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2},"a":{"a":0,"k":[160,284,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[36,19.812],[19.882,-36],[-36,-19.882],[-19.882,36]],"o":[[36,-19.882],[-19.882,-36],[-36,19.882],[19.882,36]],"v":[[36,0],[0,-36],[-36,0],[0,36]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false,"_render":true},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"n":["0p667_1_0p167_0p167"],"t":0,"s":[100],"e":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"n":["0p833_1_0p167_0"],"t":29,"s":[0],"e":[0]},{"t":51.0000020772726}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"n":["0p833_0p833_0p333_0"],"t":20,"s":[100],"e":[1]},{"t":60.0000024438501}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false,"_render":true},{"ty":"st","c":{"a":0,"k":[0.1176,0.4588,0.1333,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5},"lc":2,"lj":1,"ml":10,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false,"_render":true},{"ty":"tr","p":{"a":0,"k":[160,284],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"n":["0p667_1_0p167_0p167"],"t":0,"s":[-90],"e":[270]},{"t":59.0000024031193}],"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform","_render":true}],"nm":"Group 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false,"_render":true}],"ip":0,"op":60.0000024438501,"st":0,"bm":0,"completed":true}],"markers":[],"__complete":true}
--------------------------------------------------------------------------------
/src/assets/lotties/purple-spinner.json:
--------------------------------------------------------------------------------
1 | {"v":"5.1.8","fr":29.9700012207031,"ip":0,"op":60.0000024438501,"w":100,"h":100,"nm":"Spinner","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"spinner Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2},"a":{"a":0,"k":[160,284,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[36,19.812],[19.882,-36],[-36,-19.882],[-19.882,36]],"o":[[36,-19.882],[-19.882,-36],[-36,19.882],[19.882,36]],"v":[[36,0],[0,-36],[-36,0],[0,36]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false,"_render":true},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"n":["0p667_1_0p167_0p167"],"t":0,"s":[100],"e":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"n":["0p833_1_0p167_0"],"t":29,"s":[0],"e":[0]},{"t":51.0000020772726}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"n":["0p833_0p833_0p333_0"],"t":20,"s":[100],"e":[1]},{"t":60.0000024438501}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false,"_render":true},{"ty":"st","c":{"a":0,"k":[0.4588,0.1137,0.8431,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5},"lc":2,"lj":1,"ml":10,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false,"_render":true},{"ty":"tr","p":{"a":0,"k":[160,284],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"n":["0p667_1_0p167_0p167"],"t":0,"s":[-90],"e":[270]},{"t":59.0000024031193}],"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform","_render":true}],"nm":"Group 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false,"_render":true}],"ip":0,"op":60.0000024438501,"st":0,"bm":0,"completed":true}],"markers":[],"__complete":true}
--------------------------------------------------------------------------------
/src/assets/lotties/walmart-spinner.json:
--------------------------------------------------------------------------------
1 | {"nm":"background/Busy Six Spoke","ddd":0,"h":150,"w":150,"meta":{"g":"@lottiefiles/toolkit-js 0.33.2"},"layers":[{"ty":4,"nm":"spinner 6 slow to fast","sr":1,"st":0,"op":241,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[63,70.5,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[75,75,0],"ix":2},"r":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[0],"t":0},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[180],"t":119},{"s":[720],"t":239}],"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Group 1","ix":1,"cix":2,"np":2,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":true,"i":[[-2.063,3.572],[0,0],[-3.572,-2.063],[0,0],[2.062,-3.572],[3.572,2.063],[0,0]],"o":[[0,0],[2.063,-3.572],[0,0],[3.572,2.062],[-2.063,3.572],[0,0],[-3.572,-2.062]],"v":[[-19.052,-11],[-19.052,-11],[-8.807,-13.745],[16.308,0.755],[19.053,11],[8.808,13.745],[-16.307,-0.755]]},"ix":2}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[0.2784,0.3961,0.9294],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[21.365,46.25],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]},{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Group 2","ix":2,"cix":2,"np":2,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":true,"i":[[-2.063,3.572],[0,0],[-3.572,-2.063],[0,0],[2.063,-3.572],[3.573,2.063],[0,0]],"o":[[0,0],[2.063,-3.572],[0,0],[3.573,2.062],[-2.062,3.572],[0,0],[-3.572,-2.062]],"v":[[-19.052,-11],[-19.052,-11],[-8.807,-13.745],[16.307,0.755],[19.052,11],[8.807,13.745],[-16.307,-0.755]]},"ix":2}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[0.2784,0.3961,0.9294],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[104.504,94.25],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]},{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Group 3","ix":3,"cix":2,"np":2,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":true,"i":[[-2.063,-3.572],[0,0],[3.572,-2.062],[0,0],[2.062,3.572],[-3.573,2.062],[0,0]],"o":[[0,0],[2.063,3.572],[0,0],[-3.573,2.063],[-2.063,-3.572],[0,0],[3.572,-2.063]],"v":[[19.053,-11],[19.053,-11],[16.307,-0.755],[-8.808,13.745],[-19.053,11],[-16.308,0.755],[8.807,-13.745]]},"ix":2}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[0.2784,0.3961,0.9294],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[104.504,46.25],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]},{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Group 4","ix":4,"cix":2,"np":2,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":true,"i":[[-2.063,-3.572],[0,0],[3.572,-2.062],[0,0],[2.063,3.572],[-3.572,2.062],[0,0]],"o":[[0,0],[2.063,3.572],[0,0],[-3.572,2.063],[-2.062,-3.572],[0,0],[3.572,-2.063]],"v":[[19.052,-11],[19.052,-11],[16.307,-0.755],[-8.808,13.745],[-19.053,11],[-16.308,0.755],[8.807,-13.745]]},"ix":2}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[0.2784,0.3961,0.9294],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[21.366,94.25],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]},{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Group 5","ix":5,"cix":2,"np":2,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":true,"i":[[4.125,0],[0,0],[0,4.125],[0,0],[-4.125,0],[0,-4.125],[0,0]],"o":[[0,0],[-4.125,0],[0,0],[0,-4.125],[4.125,0],[0,0],[0,4.125]],"v":[[0,22],[0,22],[-7.5,14.5],[-7.5,-14.5],[0,-22],[7.5,-14.5],[7.5,14.5]]},"ix":2}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[0.2784,0.3961,0.9294],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[62.935,118.25],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]},{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Group 6","ix":6,"cix":2,"np":2,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":true,"i":[[4.125,0],[0,0],[0,4.125],[0,0],[-4.125,0],[0,-4.125],[0,0]],"o":[[0,0],[-4.125,0],[0,0],[0,-4.125],[4.125,0],[0,0],[0,4.125]],"v":[[0,22],[0,22],[-7.5,14.5],[-7.5,-14.5],[0,-22],[7.5,-14.5],[7.5,14.5]]},"ix":2}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[0.2784,0.3961,0.9294],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[62.935,22.25],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":1}],"v":"5.1.1","fr":60,"op":240,"ip":0,"assets":[]}
--------------------------------------------------------------------------------
/src/components/EmptyTabs.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Button } from '@nextui-org/react'
3 | import { useNavigate } from 'react-router-dom'
4 | const EmptyTabs = ({text, link, textLink}) => {
5 | const navigate = useNavigate();
6 | return (
7 |
8 |
9 | {text}
10 | Click the button to get started.
11 | navigate(link)}>
12 | {textLink}
13 |
14 |
15 |
16 |
17 | )
18 | }
19 |
20 | export default EmptyTabs
--------------------------------------------------------------------------------
/src/components/EmptyTiles.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { CircleFadingPlus } from "lucide-react";
3 | const EmptyTiles = ({text}) => {
4 | return (
5 |
6 |
7 | {text}
8 | Click the icon to get started.
9 |
10 |
11 |
12 | )
13 | }
14 |
15 | export default EmptyTiles
--------------------------------------------------------------------------------
/src/components/InfoBar.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Active from '../assets/icons/active.svg?react';
3 | import Inactive from '../assets/icons/inactive.svg?react';
4 |
5 | import rgbStripImg from '../assets/imgs/rgb-strip.png'
6 | import acImg from '../assets/imgs/airconditioner-black-side.png';
7 | import tvImg from '../assets/imgs/tv-gray.png'
8 | import heaterImg from '../assets/imgs/heater-black.png'
9 | import speakerImg from '../assets/imgs/speakers-black.png'
10 | import dehumidiferImg from '../assets/imgs/dehumidifier-black.png'
11 | import genericImg from '../assets/imgs/generic-device-blue.png'
12 |
13 | const InfoBar = (props) => {
14 |
15 | const RgbImg = () => (
16 |
24 | )
25 |
26 | const AcImg = () => (
27 |
35 | )
36 |
37 | const TvImg = () => (
38 |
46 | )
47 |
48 | const HeaterImg = () => (
49 |
57 | )
58 | const SpeakersImg = () => (
59 |
67 | )
68 |
69 | const DehumidifierImg = () => (
70 |
78 | )
79 |
80 | const GenericImg = () => (
81 |
89 | )
90 |
91 | const renderImg = {
92 | "Generic Device": ,
93 | "Air Conditioner": ,
94 | "Dehumidifier": ,
95 | "RGB Lights": ,
96 | "Audio System": ,
97 | "Heater": ,
98 | "Smart TV":
99 | }
100 | return (
101 |
102 |
106 |
107 | {/* Top Container */}
108 |
113 |
114 | {/*
*/}
115 | {renderImg[props.category]}
116 |
117 |
118 |
122 | {props.remoteName}
123 |
124 |
127 | {props.category}
128 |
129 |
130 |
131 |
132 | {/* Divider */}
133 |
134 |
135 | {/* Bottom Container */}
136 |
141 |
142 |
145 | {props.deviceName}
146 |
147 |
150 | {props.macAddress}
151 |
152 |
153 | {/* */}
154 |
155 |
158 | {props.isConnected ? "Connected" : "Disconnected"}
159 |
160 |
161 |
162 |
163 |
164 |
165 | );
166 | };
167 |
168 | export default InfoBar;
169 |
--------------------------------------------------------------------------------
/src/components/ModalAddUser.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useMemo, useEffect, useRef } from "react";
2 | import { CircleFadingPlus } from "lucide-react";
3 | import {
4 | Modal,
5 | ModalContent,
6 | ModalHeader,
7 | ModalBody,
8 | ModalFooter,
9 | Button,
10 | useDisclosure,
11 | Select,
12 | SelectItem,
13 | Progress,
14 | Divider,
15 | Input,
16 | } from "@nextui-org/react";
17 |
18 | import { useParams } from "react-router-dom"
19 |
20 | import useError from "../hooks/useError";
21 | import usePost from "../hooks/usePost";
22 |
23 | import ModalError from "./ModalError";
24 | import config from "../configs/config";
25 |
26 | export const ModalAddUser = ({toggleOpen, setToggleOpen}) => {
27 | const authUrl = config.authUrl;
28 | const { postItem, success: userAddSuccess, error: userAddError, data: userAddData } = usePost(`${authUrl}/registeruser`);
29 |
30 | const [userEmail, setUserEmail] = useState("");
31 |
32 | const {isOpen, onOpen, onClose, onOpenChange} = useDisclosure();
33 |
34 | const attributes = useError("");
35 | const {error, setError} = attributes;
36 |
37 | const isInvalid = useMemo(() => {
38 | if (userEmail === "") return false;
39 |
40 | const regex = /^[\w\.-]+@[a-zA-Z\d\.-]+\.[a-zA-Z]{2,}$/;
41 |
42 | return regex.test(userEmail) ? false : true;
43 | }, [userEmail]);
44 |
45 | useEffect(() => {
46 | if (!userAddError) return;
47 | attributes.setError(userAddError);
48 | },[userAddError])
49 |
50 | useEffect(() => {
51 | onClose();
52 | },[error])
53 |
54 | // Restart the steps of the modal if the user closes it
55 | useEffect(()=>{
56 | if (isOpen) return;
57 | setUserEmail("");
58 | setToggleOpen(false);
59 | },[isOpen])
60 |
61 | useEffect(() => {
62 | onClose();
63 | setUserEmail("");
64 | },[userAddSuccess])
65 |
66 | useEffect(() => {
67 | if (toggleOpen) onOpen();
68 | },[toggleOpen])
69 |
70 | const addUser = () => {
71 | if (userEmail === "") return;
72 | const item = {
73 | "userEmail": userEmail
74 | }
75 | postItem(item);
76 | onClose();
77 | }
78 |
79 | return (
80 | <>
81 |
82 |
83 | {(onClose) => (
84 | <>
85 |
86 | Add new user
87 |
88 |
89 |
90 |
91 |
92 | Provide permission to user to access the app
93 |
94 |
104 |
105 |
106 |
107 |
108 | Close
109 |
110 |
111 | Add User
112 |
113 |
114 | >
115 |
116 | )}
117 |
118 |
119 |
120 | >
121 | );
122 |
123 | }
124 |
125 | export default ModalAddUser;
--------------------------------------------------------------------------------
/src/components/ModalError.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button, useDisclosure} from "@nextui-org/react";
3 |
4 | const ModalError = ({ isOpen, onOpenChange, error }) => {
5 |
6 | return (
7 |
8 |
9 | {(onClose) => (
10 | <>
11 | Error
12 |
13 |
14 | {error}
15 |
16 |
17 |
18 |
19 | OK
20 |
21 |
22 | >
23 | )}
24 |
25 |
26 | );
27 | };
28 |
29 | export default ModalError;
30 |
--------------------------------------------------------------------------------
/src/components/NavigationBar.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useLocation, Link } from 'react-router-dom';
3 |
4 | import { LayoutDashboard, House, Cpu, Usb, CalendarCog, Wifi } from 'lucide-react';
5 |
6 | import Remote from '../assets/icons/remote.svg?react';
7 | import Control from '../assets/icons/controller.svg?react';
8 | import Cloud from '../assets/icons/cloud-network.svg?react';
9 |
10 |
11 | export const NavigationBar = (props) => {
12 |
13 | return (
14 |
15 |
16 | {props.children}
17 |
18 |
19 | )
20 | }
21 |
22 | export const NavigationBarItem = (props) => {
23 | const location = useLocation();
24 | const isActive = (location.pathname.includes(props.to) && props.to !== "/") ||
25 | (location.pathname === "/" && props.to === "/");
26 |
27 | return (
28 |
29 |
30 | {props.icon}
31 | {props.text}
32 |
33 |
34 | )
35 | }
36 |
37 |
38 |
--------------------------------------------------------------------------------
/src/components/NoticeBox.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { BadgeAlert } from 'lucide-react'
3 | const NoticeBox = ({children}) => {
4 | return (
5 |
11 |
12 | )
13 | }
14 |
15 | export default NoticeBox
--------------------------------------------------------------------------------
/src/components/RemoteButton.jsx:
--------------------------------------------------------------------------------
1 | import React, {useState, useEffect, useRef} from "react";
2 | import { Power, Check, } from "lucide-react";
3 | import {Spinner} from "@nextui-org/react";
4 | import {Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button, useDisclosure} from "@nextui-org/react";
5 |
6 | import config from "../configs/config";
7 | import { wsHandler } from "../services/websocket";
8 |
9 | const RemoteButton = (props) => {
10 | const responseTimeout = 22000;
11 | const successAnimationTimeout = 800;
12 | const wsUrl = config.wssUrl;
13 |
14 | const ws_payload = {
15 | "action": "cmd",
16 | "cmd": "execute",
17 | "remoteName": props.remoteName,
18 | "buttonName": props.buttonName
19 | }
20 |
21 | const successResponseFormat = (data) => {
22 | const response = JSON.parse(data);
23 | return (response.action === "ack");
24 | };
25 |
26 | const {isOpen, onOpen, onOpenChange} = useDisclosure();
27 | const [loadingState, setLoadingState] = useState('idle');
28 | const errorMessage = useRef("Unexpected Error Occured.");
29 |
30 | const setErrorMessage = (msg) => (errorMessage.current = msg);
31 |
32 | const handlePress = () => {
33 | setLoadingState('loading');
34 | wsHandler(wsUrl, ws_payload, responseTimeout, successResponseFormat, setLoadingState, setErrorMessage);
35 | }
36 |
37 | useEffect(() => {
38 | if (loadingState === 'error') {
39 | setLoadingState('idle');
40 | onOpen(); // Open the modal when loading is 'error'
41 | }
42 | }, [loadingState, onOpen]);
43 |
44 | useEffect(() => {
45 | if (loadingState !== 'success') return;
46 | setTimeout(() => setLoadingState('idle'), successAnimationTimeout);
47 | },[loadingState])
48 |
49 | const renderButton = () => {
50 | switch (loadingState) {
51 | case 'loading':
52 | return ;
53 | case 'success':
54 | return ;
55 | default:
56 | return ;
57 | }
58 | }
59 |
60 | return (
61 | <>
62 | {/* Modal displaying error information */}
63 |
64 |
65 | {(onClose) => (
66 | <>
67 | Error
68 |
69 |
70 | {errorMessage.current}
71 |
72 |
73 |
74 |
75 | Close
76 |
77 |
78 | >
79 | )}
80 |
81 |
82 |
90 | {
91 | renderButton()
92 | }
93 |
94 |
95 |
96 | >
97 | );
98 | };
99 |
100 | const CheckBoxAnimation = ({durationSeconds}) => (
101 |
102 |
{/* Tailwind class for width */}
103 |
112 |
113 |
114 |
140 |
141 | )
142 |
143 | export default RemoteButton;
144 |
--------------------------------------------------------------------------------
/src/components/Sidebar.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useContext, createContext } from "react";
2 | import { useLocation, Link } from 'react-router-dom';
3 | import { ChevronFirst, ChevronLast, MoreVertical, MoonStar } from "lucide-react";
4 | import Logo from '../assets/icons/airremote-logo.svg?react';
5 | const SidebarContext = createContext();
6 | export const Sidebar = (props) => {
7 | const [expanded, setExpanded] = useState(false);
8 |
9 | return (
10 | // w-16 mr-3 z-10
11 |
30 | )
31 | }
32 |
33 | export const SidebarItem = (props) => {
34 | const { expanded } = useContext(SidebarContext);
35 | const location = useLocation();
36 | const isActive = (location.pathname.includes(props.to) && props.to !== "/") ||
37 | (location.pathname === "/" && props.to === "/");
38 |
39 | return (
40 |
41 |
49 | {props.icon}
50 | {props.text}
51 | {props.alert &&
}
52 |
53 |
54 |
55 | )
56 | }
--------------------------------------------------------------------------------
/src/components/TileDelete.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { X, Minus } from 'lucide-react';
3 | import { useParams } from 'react-router-dom';
4 | import {Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button, useDisclosure} from "@nextui-org/react";
5 |
6 | import useDelete from '../hooks/useDelete';
7 | import useError from '../hooks/useError';
8 |
9 | import ModalError from './ModalError';
10 | const TileDelete = ({url, refetch, position}) => {
11 | const { remoteName } = useParams();
12 | const attributes = useError("");
13 |
14 | const { success, loading , error , refetch: deleteRefetch } = useDelete(url);
15 | const {isOpen, onOpen, onClose, onOpenChange} = useDisclosure();
16 |
17 | const onPressYes = () => {
18 | deleteRefetch().then(refetch);
19 | onClose();
20 | }
21 |
22 | useEffect(() => {
23 | if (!error) return;
24 | attributes.setError(error);
25 | },[error])
26 |
27 | return (
28 | <>
29 |
30 |
31 | {(onClose) => (
32 | <>
33 | Confirmation
34 |
35 |
36 | Are you sure you want to delete this item?
37 |
38 |
39 |
40 |
41 | Yes
42 |
43 |
44 | No
45 |
46 |
47 | >
48 | )}
49 |
50 |
51 |
56 |
57 | >
58 | );
59 | };
60 |
61 | export default TileDelete;
62 |
--------------------------------------------------------------------------------
/src/components/TileDevice.jsx:
--------------------------------------------------------------------------------
1 | import React, { useContext, useEffect, useState } from "react";
2 | import {
3 | useSortable
4 | } from "@dnd-kit/sortable";
5 |
6 | import { Grip } from "lucide-react";
7 | import { CSS } from '@dnd-kit/utilities';
8 | import { Tooltip } from "@nextui-org/react";
9 | import { EditModeContext } from '../contexts/EditModeContext';
10 | import { DraggingContext } from '../contexts/DraggingContext';
11 |
12 | import TileDelete from "./TileDelete";
13 |
14 | import config from '../configs/config';
15 | import esp32Img from '../assets/imgs/microcontroller3.png'
16 | import Active from '../assets/icons/active.svg?react';
17 | import Inactive from '../assets/icons/inactive.svg?react';
18 |
19 | export const TileDevice = ({id, item, isConnected, refetch}) => {
20 | const apiUrl = config.apiUrl;
21 | const UNDEFINED_MAC_ADDRESS = "FF:FF:FF:FF:FF:FF"
22 | const { editMode } = useContext(EditModeContext);
23 | const { dragging } = useContext(DraggingContext);
24 | const [isOpenTooltip, setIsOpenTooltip] = useState(false)
25 |
26 | useEffect(() => {
27 | if (!isOpenTooltip) return;
28 | setTimeout(() => {
29 | setIsOpenTooltip(false);
30 | }, 2000)
31 | },[isOpenTooltip])
32 |
33 | const {
34 | attributes,
35 | listeners,
36 | setNodeRef,
37 | transform,
38 | transition,
39 | isDragging
40 | } = useSortable({ id: `${id}` });
41 |
42 | const style = {
43 | transform: CSS.Translate.toString(transform),
44 | transition,
45 | zIndex: isDragging ? "100": "auto",
46 | opacity: isDragging ? 0.3 : 1
47 | }
48 |
49 | const getRandomAnimationDelay = () => {
50 | const delay = Math.random() * 0.133;
51 | return {animationDelay: `${delay}s`};
52 | };
53 |
54 | const listenersOnState = editMode ? { ...listeners } : {};
55 |
56 | const IotImg = () =>
62 |
63 | const IotImg3 = ({className, img}) => {
64 | return (
65 |
66 |
71 |
72 | )
73 | }
74 | return (
75 | <>
76 |
81 | {
82 | item?.macAddress === UNDEFINED_MAC_ADDRESS?
83 |
84 | setIsOpenTooltip(true)}
86 | onMouseLeave={() => setIsOpenTooltip(false)}
87 | onMouseDown={() => setIsOpenTooltip(!isOpenTooltip)}
88 | className="flex justify-center items-center absolute top-0 right-3 -translate-y-1/2 rounded-full bg-red-500 w-[25px] h-[25px]">
89 | !
90 |
91 |
92 | : null
93 |
94 | }
95 |
96 |
97 |
98 |
107 |
108 |
109 |
110 |
111 |
112 |
{item.deviceName}
113 |
114 | {isConnected ?
115 | <>
116 |
Connected
117 |
118 | >
119 | :
120 | <>
121 |
Disconnected
122 |
123 | >
124 | }
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 | {editMode?
: null}
137 |
138 |
139 |
140 | >
141 | );
142 | };
143 |
144 | export default TileDevice;
--------------------------------------------------------------------------------
/src/components/TileGrid.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState, useContext } from "react";
2 | import {
3 | DndContext,
4 | closestCenter,
5 | KeyboardSensor,
6 | PointerSensor,
7 | MouseSensor,
8 | TouchSensor,
9 | useSensor,
10 | useSensors,
11 | DragOverlay,
12 | } from "@dnd-kit/core";
13 |
14 | import {
15 | arrayMove,
16 | SortableContext,
17 | sortableKeyboardCoordinates,
18 | rectSortingStrategy,
19 | useSortable
20 | } from "@dnd-kit/sortable";
21 |
22 | import { CSS } from '@dnd-kit/utilities';
23 |
24 | import { DraggingContext } from "../contexts/DraggingContext";
25 |
26 | export const TileGrid = ({itemOrder, children, onOrderChange, size}) => {
27 | const [activeId, setActiveId] = useState(null);
28 | const { setDragging } = useContext(DraggingContext);
29 | const [childrenOrder, setChildrenOrder] = useState(
30 | itemOrder ||
31 | Array.from({ length: React.Children.count(children) }, (_, index) => index)
32 | );
33 |
34 | const sensors = useSensors(
35 | useSensor(MouseSensor, {
36 | activationConstraint: {
37 | distance: 8,
38 | },
39 | }),
40 | useSensor(TouchSensor, {
41 | activationConstraint: {
42 | delay: 300,
43 | tolerance: 8,
44 | },
45 | }),
46 | useSensor(KeyboardSensor, {
47 | coordinateGetter: sortableKeyboardCoordinates,
48 | }),
49 | );
50 |
51 | useEffect(() => {
52 | if (!onOrderChange) return;
53 | onOrderChange(childrenOrder);
54 | },[childrenOrder])
55 |
56 | const handleDragStart = (event) => {
57 | setActiveId(event.active.id);
58 | setDragging(true);
59 | };
60 |
61 | const handleDragEnd = (event) => {
62 | setActiveId(null);
63 | setDragging(false);
64 |
65 | const { active, over } = event;
66 |
67 | if (active.id !== over.id) {
68 | const oldIndex = childrenOrder.indexOf(Number(active.id));
69 | const newIndex = childrenOrder.indexOf(Number(over.id));
70 | setChildrenOrder(arrayMove(childrenOrder, oldIndex, newIndex));
71 | }
72 | };
73 |
74 | return (
75 |
81 |
89 |
`${id}`)} strategy={rectSortingStrategy}>
90 | {size && childrenOrder.map((orderIndex) => children[orderIndex])}
91 |
92 | {activeId ? {children[activeId]}
: null}
93 |
94 |
95 |
96 |
97 | )
98 | }
99 |
--------------------------------------------------------------------------------
/src/components/TileList.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState, useContext } from "react";
2 | import {
3 | DndContext,
4 | closestCenter,
5 | KeyboardSensor,
6 | PointerSensor,
7 | MouseSensor,
8 | TouchSensor,
9 | useSensor,
10 | useSensors,
11 | DragOverlay,
12 | } from "@dnd-kit/core";
13 |
14 | import {
15 | arrayMove,
16 | SortableContext,
17 | sortableKeyboardCoordinates,
18 | rectSortingStrategy,
19 | useSortable
20 | } from "@dnd-kit/sortable";
21 |
22 | import { CSS } from '@dnd-kit/utilities';
23 |
24 | import { DraggingContext } from "../contexts/DraggingContext";
25 |
26 | export const TileList = ({itemOrder, children, onOrderChange, size}) => {
27 | const [activeId, setActiveId] = useState(null);
28 | const { setDragging } = useContext(DraggingContext);
29 | const [childrenOrder, setChildrenOrder] = useState(
30 | itemOrder ||
31 | Array.from({ length: React.Children.count(children) }, (_, index) => index)
32 | );
33 |
34 | useEffect(() => {
35 | if (!onOrderChange) return;
36 | onOrderChange(childrenOrder);
37 | },[childrenOrder])
38 |
39 | const sensors = useSensors(
40 | useSensor(MouseSensor, {
41 | activationConstraint: {
42 | distance: 8,
43 | },
44 | }),
45 | useSensor(TouchSensor, {
46 | activationConstraint: {
47 | delay: 300,
48 | tolerance: 8,
49 | },
50 | }),
51 | useSensor(KeyboardSensor, {
52 | coordinateGetter: sortableKeyboardCoordinates,
53 | }),
54 | );
55 |
56 | const handleDragStart = (event) => {
57 | setActiveId(event.active.id);
58 | setDragging(true);
59 | };
60 |
61 | const handleDragEnd = (event) => {
62 | setActiveId(null);
63 | setDragging(false);
64 |
65 | const { active, over } = event;
66 |
67 | if (active.id !== over.id) {
68 | const oldIndex = childrenOrder.indexOf(Number(active.id));
69 | const newIndex = childrenOrder.indexOf(Number(over.id));
70 | setChildrenOrder(arrayMove(childrenOrder, oldIndex, newIndex));
71 | }
72 | };
73 |
74 | return (
75 |
81 |
89 |
`${id}`)} strategy={rectSortingStrategy}>
90 | {size && childrenOrder.map((orderIndex) => children[orderIndex])}
91 |
92 | {activeId ? {children[activeId]}
: null}
93 |
94 |
95 |
96 |
97 | )
98 | }
99 | export default TileList;
--------------------------------------------------------------------------------
/src/components/TileRemote.jsx:
--------------------------------------------------------------------------------
1 | import React, { useContext, useEffect, useState } from "react";
2 | import { useNavigate, useLocation } from "react-router-dom";
3 | import {
4 | useSortable
5 | } from "@dnd-kit/sortable";
6 |
7 | import { Grip } from "lucide-react";
8 | import { CSS } from '@dnd-kit/utilities';
9 |
10 | import { EditModeContext } from '../contexts/EditModeContext';
11 | import { DraggingContext } from '../contexts/DraggingContext';
12 |
13 | import RemoteButton from "./RemoteButton";
14 | import TileDelete from "./TileDelete";
15 |
16 | import config from '../configs/config';
17 | import dehumidifierImg from '../assets/imgs/dehumidifier-white.png';
18 | import uniDeviceImg from '../assets/imgs/universal-device.png'
19 | import rgbStripImg from '../assets/imgs/rgb-strip.png'
20 | import tvImg from '../assets/imgs/tv.png'
21 | import airconditionerImg from '../assets/imgs/air-white-side.png'
22 | import heaterImg from '../assets/imgs/heater2.png'
23 | import speakerImg from '../assets/imgs/speakers.png'
24 | import tvBoxImg from '../assets/imgs/tvbox.png'
25 | import Active from '../assets/icons/active.svg?react';
26 | import Inactive from '../assets/icons/inactive.svg?react';
27 |
28 | import { ChevronRight } from "lucide-react";
29 | export const TileRemote = ({id, item, isConnected, refetch}) => {
30 | const apiUrl = config.apiUrl;
31 | const { editMode } = useContext(EditModeContext);
32 | const { dragging } = useContext(DraggingContext);
33 | const navigate = useNavigate();
34 | const location = useLocation();
35 | const devices = ["Air Conditioner", "Dehumidifier", "Smart TV", "Heater", "Audio System", "RGB Lights", "Generic Device"]
36 | const [deviceType, setDeviceType] = useState(item.category);
37 |
38 | const {
39 | attributes,
40 | listeners,
41 | setNodeRef,
42 | transform,
43 | transition,
44 | isDragging
45 | } = useSortable({ id: `${id}` });
46 |
47 | const style = {
48 | transform: CSS.Translate.toString(transform),
49 | transition,
50 | zIndex: isDragging ? "100": "auto",
51 | opacity: isDragging ? 0.3 : 1
52 | }
53 |
54 | const getRandomAnimationDelay = () => {
55 | const delay = Math.random() * 0.133; // Delay between 0 and 2 seconds
56 | return {animationDelay: `${delay}s`};
57 | };
58 |
59 | const listenersOnState = editMode ? { ...listeners } : {};
60 |
61 |
62 | const AirConditionerImg = () =>
68 |
69 |
70 |
71 | const TileImg = ({className, img}) => {
72 | return (
73 |
74 |
79 |
80 | )
81 | }
82 | const mapping = {
83 | "Air Conditioner": ,
84 | "Dehumidifier": ,
85 | "Smart TV": ,
86 | "Heater": ,
87 | "Audio System": ,
88 | "RGB Lights": ,
89 | "Generic Device":
90 | }
91 |
92 | return (
93 | <>
94 |
99 |
100 |
101 |
102 |
111 |
112 |
113 |
114 |
115 |
116 |
{item.remoteName}
117 |
118 |
{deviceType}
119 | {isConnected
120 | ?
121 | :
122 | }
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 | {mapping[deviceType]}
131 |
132 |
133 |
134 | navigate(`/remotes/${item.remoteName}`)} className="opacity-95" color={"#374151"} size={28}/>
135 |
136 |
137 |
138 |
139 |
140 | {editMode?
: null}
141 |
142 |
143 |
144 | >
145 | );
146 | };
147 |
148 |
149 | export default TileRemote;
--------------------------------------------------------------------------------
/src/components/TileRemoteButton.jsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from "react";
2 |
3 | import {
4 | useSortable
5 | } from "@dnd-kit/sortable";
6 |
7 | import { Grip } from "lucide-react";
8 | import { CSS } from '@dnd-kit/utilities';
9 |
10 | import { EditModeContext } from '../contexts/EditModeContext';
11 | import { DraggingContext } from '../contexts/DraggingContext';
12 |
13 | import config from '../configs/config'
14 | import RemoteButton from "./RemoteButton";
15 | import TileDelete from "./TileDelete";
16 |
17 | export const TileRemoteButton = (props) => {
18 | const apiUrl = config.apiUrl;
19 | const { editMode } = useContext(EditModeContext);
20 | const { dragging } = useContext(DraggingContext);
21 |
22 | const {
23 | attributes,
24 | listeners,
25 | setNodeRef,
26 | transform,
27 | transition,
28 | isDragging
29 | } = useSortable({ id: `${props.id}` });
30 |
31 | const style = {
32 | transform: CSS.Translate.toString(transform),
33 | transition,
34 | zIndex: isDragging ? "100": "auto",
35 | opacity: isDragging ? 0.3 : 1
36 | }
37 |
38 | const getRandomAnimationDelay = () => {
39 | const delay = Math.random() * 0.133; // Delay between 0 and 2 seconds
40 | return {animationDelay: `${delay}s`};
41 | };
42 |
43 | const listenersOnState = editMode ? { ...listeners } : {};
44 | return (
45 | <>
46 |
51 |
52 |
53 |
54 |
55 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 | {props.state ? "State" : "Stateless"}
72 |
73 | {props.item.buttonName}
74 |
75 |
76 |
77 |
78 |
79 | {editMode?
: null}
80 |
81 |
82 |
83 | >
84 | );
85 | };
86 |
--------------------------------------------------------------------------------
/src/components/Toolbar.jsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from "react";
2 | import { Pencil, CircleFadingPlus } from "lucide-react";
3 |
4 | import { EditModeContext } from '../contexts/EditModeContext';
5 | const Toolbar = ({children}) => {
6 |
7 | const { toggleEditMode } = useContext(EditModeContext);
8 |
9 | return (
10 | <>
11 |
12 |
13 |
14 | {/* */}
15 | {children}
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | >
24 | );
25 | };
26 | export default Toolbar;
--------------------------------------------------------------------------------
/src/components/TopToolbar.jsx:
--------------------------------------------------------------------------------
1 | import React, {useState} from "react";
2 | import { ArrowLeft, Settings, LogOut, UserPlus } from "lucide-react";
3 | import {Dropdown, DropdownTrigger, DropdownMenu, DropdownItem, Button, Avatar} from "@nextui-org/react";
4 | import UserLogo from '../assets/icons/avatar.svg?react';
5 | import AppLogo from '../assets/icons/airremote-logo-short.svg?react';
6 | import { useNavigate, useLocation } from 'react-router-dom';
7 | import { logout } from "../services/authenticate";
8 | import { useAuth } from "../contexts/AuthContext";
9 | import logoImg from '../assets/imgs/logo.png'
10 | import ModalAddUser from "./ModalAddUser";
11 | const TopToolbar = () => {
12 | const navigate = useNavigate();
13 | const location = useLocation();
14 | const { setToken } = useAuth();
15 | const isNested = location.pathname.split('/').filter(Boolean).length > 1;
16 | const [toggleOpen, setToggleOpen] = useState(false);
17 | return (
18 |
19 | { isNested?
20 |
navigate(-1)} className="flex justify-center items-center w-8 h-8 rounded-md bg-gray-50 cursor-pointer">
21 | :
22 |
navigate('/')}
25 | />
26 | }
27 |
28 |
29 | {/*
30 |
31 |
*/}
32 |
33 |
34 |
38 |
39 |
40 |
41 |
42 |
43 | }
46 | onClick={() => {
47 | setToggleOpen(true);
48 | }}
49 | >
50 | Invite a friend
51 |
52 | {
57 | logout();
58 | setToken(null);
59 | }}
60 | startContent={}
61 | >
62 | Logout
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | )
71 | };
72 |
73 | export default TopToolbar;
74 |
--------------------------------------------------------------------------------
/src/configs/config.js:
--------------------------------------------------------------------------------
1 | const config = {
2 | wssUrl: `${import.meta.env.VITE_WSS_URL}?deviceType=client`,
3 | baseUrl: `${import.meta.env.VITE_BASE_URL}`,
4 | apiUrl: `${import.meta.env.VITE_API_URL}`,
5 | authUrl: `${import.meta.env.VITE_AUTH_URL}`,
6 | redirectUri: `${window.location.origin}/oauth2/callback`,
7 | appClientId: `${import.meta.env.VITE_APP_CLIENT_ID}`,
8 | cognitoDomain: `${import.meta.env.VITE_COGNITO_DOMAIN}`,
9 | }
10 |
11 | export default config;
--------------------------------------------------------------------------------
/src/contexts/AuthContext.jsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, useState, useEffect, useContext, useLayoutEffect, useRef } from 'react';
2 | import { authenticate, refresh } from '../services/authenticate';
3 | import config from '../configs/config';
4 | import api from '../api/api';
5 | const AuthContext = createContext();
6 |
7 | export const useAuth = () => {
8 | const authContext = useContext(AuthContext);
9 |
10 | if (!authContext) {
11 | throw new Error('useAuth must be used within an AuthProvider')
12 | }
13 |
14 | return authContext;
15 | }
16 |
17 | export const AuthProvider = ({ children }) => {
18 | const authUrl = config.authUrl;
19 | const [token, setToken] = useState();
20 | const [refreshLoading, setRefreshLoading] = useState(true);
21 | const isAuthenticated = !!token;
22 | const username = useRef("User");
23 | const decodeIdToken = (token) => {
24 | try {
25 | if (!token) return;
26 | const base64Url = token.split('.')[1];
27 | const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
28 | const jsonPayload = decodeURIComponent(
29 | atob(base64)
30 | .split('')
31 | .map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
32 | .join('')
33 | );
34 |
35 | return JSON.parse(jsonPayload);
36 | } catch (error) {
37 | console.error("Invalid token", error);
38 | return null;
39 | }
40 | }
41 |
42 |
43 | useEffect(() => {
44 | const tokenPayload = decodeIdToken(token);
45 | username.current = tokenPayload ? tokenPayload.nickname : "User";
46 | }, [token])
47 |
48 | useLayoutEffect(() => {
49 | refresh()
50 | .then((res) => {
51 | setToken(res.data.id_token);
52 | })
53 | .catch((res) => setToken(null))
54 | .finally(() => setRefreshLoading(false))
55 | },[])
56 |
57 | useLayoutEffect(() => {
58 | const authInterceptor = api.interceptors.request.use((config) => {
59 | config.headers.Authorization =
60 | !config._retry && token
61 | ? `Bearer ${token}`
62 | :config.headers.Authorization;
63 |
64 | return config;
65 | })
66 | return () => {
67 | api.interceptors.request.eject(authInterceptor);
68 | }
69 | }, [token])
70 |
71 | useLayoutEffect(() => {
72 | const refreshInterceptor = api.interceptors.response.use(
73 | (response) => response,
74 | async (error) => {
75 | const originalRequest = error.config;
76 | if (
77 | error?.response?.status === 401 &&
78 | (
79 | error?.response?.data?.message === 'Unauthorized' ||
80 | error?.response?.data?.message === 'The incoming token has expired'
81 | )
82 | ) {
83 | try {
84 |
85 | const response = await refresh();
86 | setToken(response.data.id_token);
87 |
88 | originalRequest.headers.Authorization = `Bearer ${response.data.id_token}`;
89 | originalRequest._retry = true;
90 |
91 | return api(originalRequest);
92 | } catch {
93 | setToken(null)
94 | }
95 | }
96 |
97 | return Promise.reject(error);
98 | }
99 | )
100 | return () => {
101 | api.interceptors.request.eject(refreshInterceptor);
102 | }
103 | }, [])
104 |
105 | return (
106 |
107 | {children}
108 |
109 | );
110 | };
111 |
--------------------------------------------------------------------------------
/src/contexts/DraggingContext.jsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, useState } from 'react';
2 |
3 | export const DraggingContext = createContext();
4 |
5 | export const DraggingProvider = ({ children }) => {
6 | const [dragging, setDragging] = useState(false);
7 |
8 | return (
9 |
10 | {children}
11 |
12 | );
13 | };
14 |
--------------------------------------------------------------------------------
/src/contexts/EditModeContext.jsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, useState } from 'react';
2 |
3 | export const EditModeContext = createContext();
4 |
5 | export const EditModeProvider = ({ children }) => {
6 | const [editMode, setEditMode] = useState(false);
7 |
8 | const toggleEditMode = () => {
9 | setEditMode(prevMode => !prevMode);
10 | };
11 |
12 | return (
13 |
14 | {children}
15 |
16 | );
17 | };
18 |
--------------------------------------------------------------------------------
/src/hooks/useDelete.js:
--------------------------------------------------------------------------------
1 | import { useState, useCallback } from "react";
2 | import api from "../api/api";
3 |
4 | const useDelete = (url) => {
5 | const [loading, setLoading] = useState(false);
6 | const [error, setError] = useState(null);
7 | const [success, setSuccess] = useState(false);
8 |
9 | const deleteItem = useCallback(async () => {
10 | setLoading(true);
11 | setError(null);
12 | setSuccess(false);
13 |
14 | try {
15 | const response = await api({
16 | method: "DELETE",
17 | url: url,
18 | headers: {
19 | 'Content-Type': 'application/json',
20 | },
21 | });
22 | setSuccess(true);
23 | } catch (e) {
24 | setError(e.response ? e.response.statusText : e.message);
25 | } finally {
26 | setLoading(false);
27 | }
28 | }, [url]);
29 |
30 | return { success, loading, error, refetch: deleteItem };
31 | };
32 |
33 | export default useDelete;
34 |
--------------------------------------------------------------------------------
/src/hooks/useError.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import { useDisclosure } from "@nextui-org/react";
3 | const useError = (err) => {
4 | const [error, setError] = useState(err || "");
5 | const {isOpen, onOpen, onClose, onOpenChange} = useDisclosure();
6 |
7 | useEffect(() => {
8 | if (error === "") return;
9 |
10 | onOpen();
11 | },[error]);
12 |
13 | return { error, setError, isOpen, onOpen, onClose, onOpenChange };
14 | }
15 |
16 | export default useError;
17 |
--------------------------------------------------------------------------------
/src/hooks/useFetch.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import api from '../api/api';
3 |
4 | const useFetch = (url) => {
5 | const [data, setData] = useState(null);
6 | const [isLoading, setIsLoading] = useState(true);
7 | const [error, setError] = useState(null);
8 |
9 | useEffect(() => {
10 | const fetchData = async () => {
11 | setIsLoading(true);
12 | setError(null);
13 |
14 | try {
15 | const response = await api({
16 | method: "GET",
17 | url: url,
18 | headers: {
19 | 'Content-Type': 'application/json',
20 | },
21 | })
22 | setData(response.data.body);
23 | setIsLoading(false);
24 | } catch (err) {
25 | setIsLoading(false);
26 | setError(err.response ? err.response.statusText : err.message);
27 | }
28 | };
29 |
30 | fetchData();
31 | }, [url]);
32 |
33 | return { data, isLoading, error };
34 | };
35 |
36 | export default useFetch;
--------------------------------------------------------------------------------
/src/hooks/useFetchMemo.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useCallback } from "react";
2 | import api from "../api/api";
3 |
4 | const useFetchMemo = (url) => {
5 | const [data, setData] = useState(null);
6 | const [loading, setLoading] = useState(true);
7 | const [error, setError] = useState(null);
8 |
9 | const fetchData = useCallback(async () => {
10 | if (!url) return;
11 |
12 | setLoading(true);
13 | setError(null);
14 |
15 | try {
16 | const response = await api({
17 | method: "GET",
18 | url: url,
19 | headers: {
20 | 'Content-Type': 'application/json',
21 | },
22 | });
23 | setData(response.data.body);
24 | } catch (error) {
25 | setError(error.response ? error.response.statusText : error.message);
26 | } finally {
27 | setLoading(false);
28 | }
29 | }, [url]);
30 |
31 | useEffect(() => {
32 | fetchData();
33 | }, [fetchData]);
34 |
35 | return { data, loading, error, refetch: fetchData };
36 | };
37 |
38 | export default useFetchMemo;
--------------------------------------------------------------------------------
/src/hooks/useKeepAlive.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from "react";
2 |
3 | // The hook is meant to keep `numInstances` lambdas concurrently active / warm
4 | // by pinging them every `interval` ms, when the tab is active
5 | const useKeepAlive = (url, numInstances, interval) => {
6 | const intervalIdRef = useRef(null);
7 |
8 | const sendParallelRequests = async () => {
9 | try {
10 | const requests = Array.from({ length: numInstances }, () =>
11 | fetch(url, { method: "POST" })
12 | );
13 | await Promise.all(requests);
14 | } catch (error) {
15 | console.error("Error in keep-alive requests:", error);
16 | }
17 | };
18 |
19 | useEffect(() => {
20 | const startPinging = () => {
21 | if (!intervalIdRef.current) {
22 | sendParallelRequests();
23 | intervalIdRef.current = setInterval(sendParallelRequests, interval);
24 | }
25 | };
26 |
27 | const stopPinging = () => {
28 | if (intervalIdRef.current) {
29 | clearInterval(intervalIdRef.current);
30 | intervalIdRef.current = null;
31 | }
32 | };
33 |
34 | const handleVisibilityChange = () => {
35 | if (document.visibilityState === "visible") {
36 | startPinging();
37 | } else {
38 | stopPinging();
39 | }
40 | };
41 |
42 | document.addEventListener("visibilitychange", handleVisibilityChange);
43 |
44 | if (document.visibilityState === "visible") {
45 | startPinging();
46 | }
47 |
48 | return () => {
49 | stopPinging();
50 | document.removeEventListener("visibilitychange", handleVisibilityChange);
51 | };
52 | }, [url, numInstances, interval]);
53 |
54 | return null;
55 | };
56 | export default useKeepAlive;
--------------------------------------------------------------------------------
/src/hooks/usePost.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import api from "../api/api";
3 |
4 | const usePost = (url) => {
5 | const [success, setSuccess] = useState(false);
6 | const [error, setError] = useState(null);
7 | const [data, setData] = useState(null);
8 |
9 | const postItem = async (item) => {
10 | try {
11 | const response = await api({
12 | method: "POST",
13 | url: url,
14 | headers: {
15 | 'Content-Type': 'application/json',
16 | },
17 | data: item,
18 | });
19 |
20 | setSuccess(true);
21 | setData(response.data);
22 | return response.data;
23 |
24 | } catch (e) {
25 | setError(e.response ? e.response.statusText : e.message);
26 | console.error("Failed to post data:", e);
27 | } finally {
28 | setSuccess(false);
29 | }
30 | };
31 |
32 | return { postItem, success, error, data };
33 | };
34 |
35 | export default usePost;
36 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap');
2 |
3 | @tailwind base;
4 | @tailwind components;
5 | @tailwind utilities;
6 |
7 | @keyframes custom-ping {
8 | 75%, 100% {
9 | transform: scale(2);
10 | opacity: 0;
11 | }
12 | }
13 |
14 | .animate-custom-ping {
15 | animation: custom-ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
16 | }
17 |
18 | .remove-child-border > div {
19 | @apply !border-0;
20 | }
--------------------------------------------------------------------------------
/src/main.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom/client'
3 | import App from './App.jsx'
4 | import { BrowserRouter } from 'react-router-dom';
5 | import {NextUIProvider} from '@nextui-org/react'
6 | import './index.css'
7 |
8 | ReactDOM.createRoot(document.getElementById('root')).render(
9 |
10 |
11 |
12 |
13 |
14 |
15 | ,
16 | )
17 |
--------------------------------------------------------------------------------
/src/pages/Automations.jsx:
--------------------------------------------------------------------------------
1 | import React, {useEffect, useState, useRef, useContext} from "react";
2 | import config from "../configs/config";
3 | import api from "../api/api";
4 | import useError from "../hooks/useError";
5 | import useFetchMemo from "../hooks/useFetchMemo";
6 |
7 | import { EditModeProvider, EditModeContext } from "../contexts/EditModeContext";
8 | import { DraggingProvider } from "../contexts/DraggingContext";
9 |
10 | import ModalError from "../components/ModalError";
11 | import TopToolbar from "../components/TopToolbar";
12 | import Toolbar from "../components/Toolbar";
13 | import NoticeBox from "../components/NoticeBox";
14 | import ModalAddAutomation from "../components/ModalAddAutomation";
15 | import { TileList } from "../components/TileList";
16 | import { TileAutomation } from "../components/TileAutomation";
17 | import EmptyTiles from "../components/EmptyTiles";
18 | const Automations = () => {
19 | const apiUrl = config.apiUrl;
20 | const attributes = useError("");
21 |
22 | const isCleaned = useRef(false);
23 | const { data, loading, error, refetch } = useFetchMemo(`${apiUrl}/automations`);
24 | const [itemOrder, setItemOrder] = useState([]); // This represents which item is currently in the i-th position. If order[i] = 3 then the (originally) 4th item is in the (i+1)-th position
25 | const originalOrderIndex = useRef([]); // Same as below but original values.
26 | const newOrderIndex = useRef([]); // This represents in which position the item[i] should go. If item[i] = 3 then the i-th item should go to t
27 |
28 | useEffect(() => {
29 | if (!data || isCleaned.current) return;
30 | api({
31 | method: "POST",
32 | url: "/api/automations/clean",
33 | headers: {
34 | 'Content-Type': 'application/json',
35 | },
36 | })
37 | .then(() => {
38 | isCleaned.current = true;
39 | })
40 | .catch(err => {
41 | console.error("Error in cleaning automation:", err.message);
42 | });
43 |
44 | }, [data]);
45 |
46 | useEffect(() => {
47 | if (!error) return;
48 | attributes.setError(error);
49 | },[error])
50 |
51 | useEffect(() => {
52 | if (!data) return;
53 | /*
54 | We're receiving for each item an orderIndex meaning that the item in position i should move to item.orderIndex
55 | We're looping through all items and initializing an array `newOrder` with each item from position i representing
56 | the item that had item.orderIndex == i.
57 | The reverse has to be done when saving the changes to the database
58 | */
59 |
60 | const originalOrder = new Array(data.length);
61 | const newOrder = new Array(data.length);
62 |
63 | data.forEach((item, index) => {
64 | const new_order = parseInt(item.orderIndex);
65 | originalOrder[index] = parseInt(item.orderIndex);
66 | newOrder[new_order] = index;
67 | })
68 |
69 | originalOrderIndex.current = originalOrder;
70 | setItemOrder(newOrder);
71 | }, [data])
72 |
73 | // This is a workaround to rerender this component
74 | // If this component is only dependent on `data`, then because of shallow comparison it won't rerender
75 | const Grid = ({length, data, itemOrder}) => {
76 | const { editMode } = useContext(EditModeContext);
77 |
78 | useEffect(() => {
79 | if (editMode) return;
80 | if (newOrderIndex.current.length === 0) return;
81 |
82 | const orderChanged = !(newOrderIndex.current.every((element, index) => element === originalOrderIndex.current[index]));
83 | if (!orderChanged) return;
84 | console.log(newOrderIndex.current)
85 | const response = api({
86 | method: "POST",
87 | url: `${apiUrl}/automations/sort`,
88 | headers: {
89 | 'Content-Type': 'application/json',
90 | },
91 | data: {"newOrder": newOrderIndex.current},
92 | });
93 |
94 | response
95 | .catch((error => attributes.setError(error.message)));
96 | }, [editMode])
97 |
98 | const onOrderChange = (newItemOrder) => {
99 | const orderChanged = !(itemOrder.every((element, index) => element === newItemOrder[index]));
100 | if (!orderChanged) return;
101 | const orderChanges = new Array(data.length);
102 | newItemOrder.forEach((item, index) => {
103 | orderChanges[item] = index;
104 | })
105 |
106 | newOrderIndex.current = orderChanges;
107 |
108 | setItemOrder(newItemOrder)
109 | }
110 | return
111 |
112 | {
113 | data.map((item, index) => )
114 | }
115 |
116 |
117 | }
118 |
119 | return (
120 | <>
121 |
122 |
123 |
124 |
125 |
126 | {/* Secondary Toolbar */}
127 |
128 |
129 |
130 | Your Automations
131 |
132 |
133 |
134 | {data ? data.length : 0}
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 | Make sure devices are connected, otherwise the automation won't run.
146 |
147 |
148 | {data && data?.length > 0 ?
149 |
150 | :
151 | }
152 |
153 |
154 |
155 | >
156 | );
157 | };
158 |
159 |
160 |
161 | export default Automations;
162 |
--------------------------------------------------------------------------------
/src/pages/Devices.jsx:
--------------------------------------------------------------------------------
1 | import React, {useEffect, useRef, useState, useContext} from "react";
2 | import config from "../configs/config";
3 |
4 | import useFetchMemo from "../hooks/useFetchMemo";
5 | import useError from "../hooks/useError";
6 | import api from "../api/api";
7 | import { EditModeProvider, EditModeContext } from "../contexts/EditModeContext";
8 | import { DraggingProvider } from "../contexts/DraggingContext";
9 |
10 | import TopToolbar from '../components/TopToolbar';
11 | import EmptyTiles from "../components/EmptyTiles";
12 | import Toolbar from "../components/Toolbar";
13 | import ModalAddDevice from "../components/ModalAddDevice";
14 | import { TileGrid } from '../components/TileGrid';
15 | import { TileDevice } from '../components/TileDevice';
16 | import ModalError from "../components/ModalError";
17 | import NoticeBox from "../components/NoticeBox";
18 | const Devices = () => {
19 | const apiUrl = config.apiUrl;
20 |
21 | const { data: deviceData, loading: deviceLoading, error: deviceError, refetch: deviceRefetch } = useFetchMemo(`${apiUrl}/devices`);
22 | const [itemOrder, setItemOrder] = useState([]); // This represents which item is currently in the i-th position. If order[i] = 3 then the (originally) 4th item is in the (i+1)-th position
23 | const originalOrderIndex = useRef([]); // Same as below but original values.
24 | const newOrderIndex = useRef([]); // This represents in which position the item[i] should go. If item[i] = 3 then the i-th item should go to the 4th position.
25 | const attributes = useError("");
26 |
27 | useEffect(() => {
28 | if (!deviceError) return;
29 | attributes.setError(deviceError);
30 | },[deviceError])
31 |
32 |
33 | useEffect(() => {
34 | if (!deviceData) return;
35 | /*
36 | We're receiving for each item an orderIndex meaning that the item in position i should move to item.orderIndex
37 | We're looping through all items and initializing an array `newOrder` with each item from position i representing
38 | the item that had item.orderIndex == i.
39 | The reverse has to be done when saving the changes to the database
40 | */
41 | const originalOrder = new Array(deviceData.length);
42 | const newOrder = new Array(deviceData.length);
43 |
44 | deviceData.forEach((item, index) => {
45 | const new_order = parseInt(item.orderIndex);
46 | originalOrder[index] = parseInt(item.orderIndex);
47 | newOrder[new_order] = index;
48 | })
49 |
50 | originalOrderIndex.current = originalOrder;
51 | setItemOrder(newOrder);
52 | }, [deviceData])
53 |
54 |
55 | const Grid = ({ length, deviceData, itemOrder }) => {
56 | const { editMode } = useContext(EditModeContext);
57 |
58 | useEffect(() => {
59 | if (editMode) return;
60 | if (newOrderIndex.current.length === 0) return;
61 |
62 | const orderChanged = !(newOrderIndex.current.every((element, index) => element === originalOrderIndex.current[index]));
63 | if (!orderChanged) return;
64 |
65 | const response = api({
66 | method: "POST",
67 | url: `${apiUrl}/devices/sort`,
68 | headers: {
69 | 'Content-Type': 'application/json',
70 | },
71 | data: {"newOrder": newOrderIndex.current},
72 | });
73 |
74 | response
75 | .catch((error => attributes.setError(error.message)));
76 | }, [editMode])
77 |
78 | const onOrderChange = (newItemOrder) => {
79 | const orderChanged = !(itemOrder.every((element, index) => element === newItemOrder[index]));
80 | if (!orderChanged) return;
81 | const orderChanges = new Array(deviceData.length);
82 | newItemOrder.forEach((item, index) => {
83 | orderChanges[item] = index;
84 | })
85 |
86 | newOrderIndex.current = orderChanges;
87 |
88 | setItemOrder(newItemOrder)
89 | }
90 | return
91 |
92 | {
93 | deviceData.map((device, index) => {
94 | const isConnected = !!device && !!device.connectionId;
95 | return
96 | })
97 | }
98 |
99 |
100 | }
101 |
102 | return (<>
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 | Your Devices
111 |
112 |
113 |
114 | {/* Buttons Count */}
115 | {deviceData ? deviceData.length : 0}
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 | If the device loses connection it will take up to 10 minutes to show up as disconnected.
129 |
130 | {deviceData && deviceData?.length > 0?
131 |
132 | :
133 | }
134 |
135 |
136 |
137 | >
138 | );
139 | };
140 |
141 | export default Devices;
142 |
--------------------------------------------------------------------------------
/src/pages/Loading.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Spinner } from '@nextui-org/react';
3 | const Loading = ({text}) => {
4 | return (
5 |
6 |
7 |
8 | {text}
9 |
10 |
11 | )
12 | }
13 |
14 | export default Loading
--------------------------------------------------------------------------------
/src/pages/OAuth2Callback.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { useNavigate } from 'react-router-dom';
3 | import { Spinner } from '@nextui-org/react';
4 | import { useAuth } from '../contexts/AuthContext';
5 | import config from '../configs/config';
6 | import axios from 'axios';
7 |
8 | const OAuth2Callback = () => {
9 | const navigate = useNavigate();
10 | const { token, setToken } = useAuth();
11 |
12 | useEffect(() => {
13 | const fetchToken = async () => {
14 | const urlParams = new URLSearchParams(window.location.search);
15 | const code = urlParams.get('code');
16 | const state = urlParams.get('state');
17 |
18 | if (!code || !state) {
19 | alert('Authorization code not found!');
20 | navigate('/');
21 | return;
22 | }
23 |
24 | try {
25 | const tokenUrl = `${config.authUrl}/oauth2/token`;
26 | const response = await axios.post(tokenUrl, { code, state }, {
27 | headers: {
28 | 'Content-Type': 'application/json',
29 | },
30 | });
31 |
32 | const data = response.data;
33 |
34 | if (!data?.id_token) {
35 | const errorMessage = data?.message || 'Error while receiving credentials.';
36 | alert(errorMessage);
37 | navigate('/');
38 | return;
39 | }
40 |
41 | setToken(data.id_token);
42 | } catch (error) {
43 | const errorMessage = error.response?.data?.message || 'An error occurred while fetching the token.';
44 | console.error(error);
45 | alert(errorMessage);
46 | navigate('/');
47 | }
48 | };
49 |
50 | fetchToken();
51 | }, [navigate, setToken]);
52 |
53 | useEffect(() => {
54 | if (token) {
55 | navigate('/');
56 | }
57 | }, [token, navigate]);
58 |
59 | return (
60 |
61 |
62 |
63 | );
64 | };
65 |
66 | export default OAuth2Callback;
67 |
--------------------------------------------------------------------------------
/src/pages/RemoteButtons.jsx:
--------------------------------------------------------------------------------
1 | import React, { useContext, useEffect, useState, useRef } from "react";
2 |
3 | import { useParams } from "react-router-dom"
4 |
5 | import config from "../configs/config";
6 |
7 | import useFetchMemo from "../hooks/useFetchMemo";
8 | import useError from "../hooks/useError";
9 |
10 | import { EditModeProvider, EditModeContext } from "../contexts/EditModeContext";
11 | import { DraggingProvider } from "../contexts/DraggingContext";
12 | import api from "../api/api";
13 |
14 | import EmptyTiles from "../components/EmptyTiles";
15 | import TopToolbar from '../components/TopToolbar';
16 | import InfoBar from '../components/InfoBar';
17 | import Toolbar from "../components/Toolbar";
18 | import ModalAddButton from "../components/ModalAddButton";
19 | import { TileGrid } from '../components/TileGrid';
20 | import { TileRemoteButton } from '../components/TileRemoteButton';
21 | import ModalError from "../components/ModalError";
22 |
23 | const RemoteButtons = () => {
24 | const apiUrl = config.apiUrl;
25 | const { remoteName } = useParams();
26 | const attributes = useError("");
27 | const [macAddress, setMacAddress] = useState(null);
28 | const [buttonsLength, setButtonsLength] = useState(0);
29 | const [isConnected, setIsConnected] = useState(false);
30 | const { data: remoteData, loading, error: remoteError, refetch: remoteRefetch } = useFetchMemo(`${apiUrl}/remotes/${remoteName}`);
31 | const { data: deviceData, loading: deviceLoading, error: deviceError, refetch: deviceRefetch } = useFetchMemo(macAddress ? `${apiUrl}/devices/${macAddress}` : null);
32 | const [itemOrder, setItemOrder] = useState(null); // This represents which item is currently in the i-th position. If order[i] = 3 then the (originally) 4th item is in the (i+1)-th position
33 | const originalOrder = useRef([]);
34 |
35 |
36 | useEffect(() => {
37 | if (!remoteData?.macAddress) return;
38 | setMacAddress(remoteData.macAddress);
39 | originalOrder.current = Array.from({ length: remoteData.buttons.length }, (_, i) => i);
40 | deviceRefetch();
41 | if (remoteData?.buttons.length > 0) {
42 | setButtonsLength(remoteData.buttons.length)
43 | }
44 | }, [remoteData]);
45 |
46 | useEffect(() => {
47 | if (!deviceData?.connectionId) return;
48 | setIsConnected(deviceData?.connectionId ? true : false);
49 | }, [deviceData])
50 |
51 | useEffect(() => {
52 | if (!remoteError) return;
53 | attributes.setError(remoteError);
54 | },[remoteError])
55 |
56 | useEffect(() => {
57 | if (!deviceError) return;
58 | attributes.setError(deviceError);
59 | },[deviceError])
60 |
61 | const Grid = ({ length, itemOrder, buttons, data, remoteName }) => {
62 | const { editMode } = useContext(EditModeContext);
63 |
64 | useEffect(() => {
65 | if (editMode) return;
66 | if (!itemOrder) return;
67 | if(originalOrder.current.length == 0 || itemOrder?.length == 0) return;
68 | const orderChanged = !(originalOrder.current.every((element, index) => element === itemOrder[index]));
69 | if (!orderChanged) return;
70 |
71 | const response = api({
72 | method: "POST",
73 | url: `${apiUrl}/remotes/${remoteName}/buttons/sort`,
74 | headers: {
75 | 'Content-Type': 'application/json',
76 | },
77 | data: {"newOrder": itemOrder},
78 | });
79 |
80 | response
81 | .catch((error => attributes.setError(error.message)));
82 | }, [editMode])
83 |
84 | const onOrderChange = (newItemOrder) => {
85 | setItemOrder(newItemOrder)
86 | }
87 | return (data &&
88 |
89 |
90 | {
91 | buttons.map((item, index) =>
92 |
93 | )
94 | }
95 |
96 | )
97 | }
98 |
99 | return (
100 | <>
101 |
102 |
103 | {/* Top Toolbar */}
104 |
105 |
106 |
107 | {/* Info Bar */}
108 |
115 |
116 | {/* Secondary Toolbar */}
117 |
118 |
119 |
120 | Remote Buttons
121 |
122 |
123 |
124 | {/* Buttons Count */}
125 | {remoteData?.buttons && remoteData.buttons.length}
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 | {/* Buttons Grid */}
136 | {remoteData && remoteData?.buttons && remoteData?.buttons.length > 0
137 | ?
138 |
139 | :
140 | }
141 |
142 |
143 |
144 | >
145 | );
146 | };
147 |
148 | export default RemoteButtons;
149 |
--------------------------------------------------------------------------------
/src/pages/Remotes.jsx:
--------------------------------------------------------------------------------
1 | import React, {useEffect, useContext, useRef, useState} from "react";
2 | import config from "../configs/config";
3 |
4 | import useFetchMemo from "../hooks/useFetchMemo";
5 | import useError from "../hooks/useError";
6 | import api from "../api/api";
7 |
8 | import { EditModeProvider, EditModeContext } from "../contexts/EditModeContext";
9 | import { DraggingProvider } from "../contexts/DraggingContext";
10 |
11 | import Toolbar from "../components/Toolbar";
12 | import TopToolbar from "../components/TopToolbar";
13 | import ModalError from "../components/ModalError";
14 | import ModalAddRemote from "../components/ModalAddRemote";
15 | import EmptyTiles from "../components/EmptyTiles";
16 | import { TileGrid } from "../components/TileGrid";
17 | import { TileRemote } from "../components/TileRemote";
18 | import NoticeBox from "../components/NoticeBox";
19 | const Remotes = () => {
20 | const apiUrl = config.apiUrl;
21 |
22 | const { data, loading, error, refetch } = useFetchMemo(`${apiUrl}/remotes`);
23 | const [itemOrder, setItemOrder] = useState([]); // This represents which item is currently in the i-th position. If order[i] = 3 then the (originally) 4th item is in the (i+1)-th position
24 | const originalOrderIndex = useRef([]); // Same as below but original values.
25 | const newOrderIndex = useRef([]); // This represents in which position the item[i] should go. If item[i] = 3 then the i-th item should go to the 4th position.
26 | const { data: deviceData, loading: deviceLoading, error: deviceError, refetch: deviceRefetch } = useFetchMemo(`${apiUrl}/devices`);
27 | const attributes = useError("");
28 |
29 | useEffect(() => {
30 | if (!error) return;
31 | attributes.setError(error);
32 | },[error])
33 |
34 | useEffect(() => {
35 | if (!deviceError) return;
36 | attributes.setError(deviceError);
37 | },[deviceError])
38 |
39 | useEffect(() => {
40 | if (!data) return;
41 | /*
42 | We're receiving for each item an orderIndex meaning that the item in position i should move to item.orderIndex
43 | We're looping through all items and initializing an array `newOrder` with each item from position i representing
44 | the item that had item.orderIndex == i.
45 | The reverse has to be done when saving the changes to the database in `onOrderChange`
46 | */
47 | const originalOrder = new Array(data.length);
48 | const newOrder = new Array(data.length);
49 | data.forEach((item, index) => {
50 | const new_order = parseInt(item.orderIndex);
51 | originalOrder[index] = parseInt(item.orderIndex);
52 | newOrder[new_order] = index;
53 | })
54 | originalOrderIndex.current = originalOrder;
55 | setItemOrder(newOrder);
56 | }, [data])
57 |
58 |
59 | const Grid = ({ data, length, deviceData, itemOrder }) => {
60 | const { editMode } = useContext(EditModeContext);
61 |
62 | useEffect(() => {
63 | if (editMode) return;
64 | if (newOrderIndex.current.length === 0) return;
65 | const orderChanged = !(newOrderIndex.current.every((element, index) => element === originalOrderIndex.current[index]));
66 | if (!orderChanged) return;
67 |
68 | const response = api({
69 | method: "POST",
70 | url: `${apiUrl}/remotes/sort`,
71 | headers: {
72 | 'Content-Type': 'application/json',
73 | },
74 | data: {"newOrder": newOrderIndex.current},
75 | });
76 |
77 | response
78 | .catch((error => attributes.setError(error.message)));
79 | }, [editMode])
80 |
81 | const onOrderChange = (newItemOrder) => {
82 | const orderChanged = !(itemOrder.every((element, index) => element === newItemOrder[index]));
83 | if (!orderChanged) return;
84 | const orderChanges = new Array(data.length);
85 | newItemOrder.forEach((item, index) => {
86 | orderChanges[item] = index;
87 | })
88 |
89 | newOrderIndex.current = orderChanges;
90 |
91 | setItemOrder(newItemOrder);
92 | }
93 |
94 | return
95 |
96 | {
97 | data.map((item, index) => {
98 | const device = deviceData ? deviceData.find((dev) => dev.macAddress == item.macAddress) : null;
99 | const isConnected = !!device && !!device.connectionId;
100 | return
101 | })
102 | }
103 |
104 |
105 | }
106 | return (
107 | <>
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 | Your remotes
116 |
117 |
118 |
119 | {/* Buttons Count */}
120 | {data ? data.length : 0}
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 | If the device loses connection it will take up to 10 minutes to show up as disconnected.
133 |
134 |
135 | {data && data?.length > 0 ?
136 |
137 | :
138 | }
139 |
140 |
141 |
142 | >);
143 | };
144 |
145 | export default Remotes;
146 |
--------------------------------------------------------------------------------
/src/services/authenticate.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import config from '../configs/config';
3 | export const authenticate = async (email, password)=>{
4 | const loginUrl = `${config.authUrl}/login`
5 | try {
6 | const payload = {
7 | email: email,
8 | password: password,
9 | };
10 |
11 | const response = await axios.post(loginUrl, payload, {
12 | withCredentials: true,
13 | headers: {
14 | 'Content-Type': 'application/json',
15 | },
16 | });
17 | return response;
18 | } catch (error) {
19 | throw error;
20 | }
21 | };
22 |
23 | export const logout = async () => {
24 | const logoutUrl = `${config.authUrl}/logout`
25 | try {
26 | const response = await axios.post(logoutUrl, null, {
27 | withCredentials: true,
28 | headers: {
29 | 'Content-Type': 'application/json',
30 | },
31 | });
32 | return response;
33 | } catch (error) {
34 | console.error('Logout failed:', error.response ? error.response.data : error.message);
35 | }
36 |
37 | };
38 |
39 | export const signup = async (username, email, password) => {
40 | const signupUrl = `${config.authUrl}/signup`
41 | const item = {
42 | nickname: username,
43 | email: email,
44 | password: password
45 | }
46 | try {
47 | const response = await axios.post(signupUrl, item, {
48 | headers: {
49 | 'Content-Type': 'application/json',
50 | }
51 | });
52 |
53 | return response;
54 | } catch (error) {
55 | throw error;
56 | }
57 | }
58 |
59 | export const refresh = async () => {
60 | const refreshUrl = `${config.authUrl}/refresh-token`;
61 | try {
62 | const response = await axios.post(refreshUrl, {}, {
63 | withCredentials: true,
64 | headers: {
65 | 'Content-Type': 'application/json',
66 | }
67 | })
68 | return response;
69 | } catch (error) {
70 | throw error;
71 | }
72 | }
--------------------------------------------------------------------------------
/src/services/oauth2.js:
--------------------------------------------------------------------------------
1 | import crypto from 'crypto-js';
2 | import config from '../configs/config';
3 |
4 | export const initiateOAuthFlow = (identity_provider) => {
5 | const state = crypto.lib.WordArray.random(16).toString();
6 | const authorizeParams = new URLSearchParams({
7 | response_type: 'code',
8 | client_id: config.appClientId,
9 | redirect_uri: config.redirectUri,
10 | state: state,
11 | identity_provider: identity_provider,
12 | scope: 'profile email openid',
13 | });
14 | window.location.href = `${config.cognitoDomain}/oauth2/authorize?${authorizeParams.toString()}`;
15 | }
16 |
17 | export default initiateOAuthFlow;
--------------------------------------------------------------------------------
/src/services/websocket.js:
--------------------------------------------------------------------------------
1 | import api from '../api/api'
2 | import config from '../configs/config'
3 | export const wsHandler = async (endpoint, message, timeout, successResponseFormat, setStatus, setError) => {
4 | let socket;
5 | let timeoutId;
6 | const websocketJwtURL = `${config.apiUrl}/websocketJwt`;
7 | try{
8 | const response = await api({
9 | method: "GET",
10 | url: websocketJwtURL,
11 | headers: {
12 | 'Content-Type': 'application/json',
13 | },
14 | });
15 |
16 | const data = JSON.parse(response.data.body);
17 |
18 | socket = new WebSocket(`${endpoint}&jwt=${data.jwt}`);
19 |
20 | socket.onopen = () => {
21 | socket.send(JSON.stringify(message));
22 | timeoutId = setTimeout(() => {
23 | setStatus('error');
24 | setError("Timeout reached without response from remote end.");
25 | socket.close();
26 | }, timeout);
27 | };
28 |
29 | socket.onmessage = (event) => {
30 | clearTimeout(timeoutId);
31 | if(successResponseFormat(event.data)){
32 | setStatus('success');
33 | } else {
34 | event?.data && (setError(event.data));
35 | setStatus('error');
36 | const response = JSON.parse(event.data);
37 | }
38 | socket.close();
39 | }
40 |
41 | socket.onerror = () => {
42 | clearTimeout(timeoutId);
43 | setStatus('error');
44 | setError("Unexpected error occured during websocket connection with remote end.");
45 | socket.close();
46 | }
47 | } catch (e) {
48 | console.log(`Received unexpected error during websocket connection : ${e}`)
49 | }
50 | return;
51 | }
52 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | const { nextui } = require("@nextui-org/react");
2 | /** @type {import('tailwindcss').Config} */
3 |
4 | module.exports ={
5 | content: [
6 | "./index.html",
7 | "./src/**/*.{js,ts,jsx,tsx}",
8 | "./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}"
9 | ],
10 | theme: {
11 | screens: {
12 | 'xs': '480px',
13 | 'sm': '640px',
14 | 'md': '768px',
15 | 'lg': '1024px',
16 | 'xl': '1280px',
17 | '2xl': '1920px',
18 | '3xl': '2600px',
19 | },
20 | extend: {
21 | fontFamily: {
22 | roboto: ['Roboto', 'sans-serif'],
23 | poppins: ['Poppins', 'sans-serif'],
24 | fredoka: ['Fredoka', 'sans-serif'],
25 | playfair: ['Playfair Display', 'serif'],
26 | },
27 | scale: {
28 | '110': '1.10',
29 | '105': '1.05',
30 | },
31 | gridTemplateColumns: {
32 | '14': 'repeat(14, minmax(0, 1fr))',
33 | '15': 'repeat(15, minmax(0, 1fr))',
34 | '16': 'repeat(16, minmax(0, 1fr))',
35 | '17': 'repeat(17, minmax(0, 1fr))',
36 | '18': 'repeat(18, minmax(0, 1fr))',
37 | '19': 'repeat(19, minmax(0, 1fr))',
38 | '20': 'repeat(20, minmax(0, 1fr))',
39 | },
40 | colors:{
41 | primary: {
42 | light: '#ffffff',
43 | DEFAULT: '#000000',
44 | dark: '#000000',
45 | },
46 | secondary: {
47 | light: '#ffffff',
48 | DEFAULT: '#343434',
49 | dark: '#343434',
50 | },
51 | },
52 | height: {
53 | '100dvh': '100dvh'
54 | },
55 | keyframes: {
56 | pop: {
57 | '0%': { transform: 'scale(1)' },
58 | '30%': { transform: 'scale(1.2)' },
59 | '50%': { transform: 'scale(1.1)' },
60 | '65%': { transform: 'scale(1.2)' },
61 | '100%': { transform: 'scale(1)' },
62 | },
63 | shake: {
64 | '0%': {
65 | transform: 'rotate(1deg)',
66 | },
67 | '50%': {
68 | transform: 'rotate(-1.5deg)',
69 | },
70 | '100%': {
71 | transform: 'rotate(1deg)',
72 | }
73 | },
74 | shakeSm: {
75 | '0%': {
76 | transform: 'rotate(0.5deg)',
77 | },
78 | '50%': {
79 | transform: 'rotate(-0.75deg)',
80 | },
81 | '100%': {
82 | transform: 'rotate(0.5deg)',
83 | }
84 | },
85 | },
86 | animation: {
87 | pop: 'pop 0.9s ease-in-out infinite',
88 | shake: 'shake 0.2s infinite ease-in-out',
89 | shakeSm: 'shakeSm 0.2s infinite ease-in-out',
90 | },
91 | },
92 | },
93 | darkMode: "class",
94 | plugins: [nextui()]
95 | }
96 |
97 |
--------------------------------------------------------------------------------
/tmp/acm_validation_config.json:
--------------------------------------------------------------------------------
1 | {
2 | "Changes": [
3 | {
4 | "Action": "UPSERT",
5 | "ResourceRecordSet": {
6 | "Name": "" ,
7 | "Type": "CNAME",
8 | "TTL": 300,
9 | "ResourceRecords": [{"Value": ""}]
10 | }
11 | }
12 | ]
13 | }
--------------------------------------------------------------------------------
/tmp/caching_policy.json:
--------------------------------------------------------------------------------
1 | {
2 | "Name": "AirRemoteCustomCachePolicy",
3 | "DefaultTTL": 86400,
4 | "MinTTL": 0,
5 | "MaxTTL": 31536000,
6 | "ParametersInCacheKeyAndForwardedToOrigin": {
7 | "EnableAcceptEncodingGzip": true,
8 | "EnableAcceptEncodingBrotli": true,
9 | "HeadersConfig": {
10 | "HeaderBehavior": "none"
11 | },
12 | "CookiesConfig": {
13 | "CookieBehavior": "all"
14 | },
15 | "QueryStringsConfig": {
16 | "QueryStringBehavior": "all"
17 | }
18 | }
19 | }
--------------------------------------------------------------------------------
/tmp/cloudfront_distribution_config.json:
--------------------------------------------------------------------------------
1 | {
2 | "CallerReference": "'CLOUDFRONT_AIRREMOTE_DISTRIBUTION'",
3 | "Comment": "Air Remote CloudFront Distribution",
4 | "Enabled": true,
5 | "Aliases": {
6 | "Quantity": 1,
7 | "Items": [""]
8 | },
9 | "Origins": {
10 | "Quantity": 1,
11 | "Items": [
12 | {
13 | "Id": "air-remote-origin",
14 | "DomainName": ".s3.eu-central-1.amazonaws.com",
15 | "OriginAccessControlId": "",
16 | "S3OriginConfig": {
17 | "OriginAccessIdentity": ""
18 | }
19 | }
20 | ]
21 | },
22 | "DefaultCacheBehavior": {
23 | "TargetOriginId": "air-remote-origin",
24 | "ViewerProtocolPolicy": "redirect-to-https",
25 | "AllowedMethods": {
26 | "Quantity": 2,
27 | "Items": ["GET", "HEAD"]
28 | },
29 | "CachePolicyId": ""
30 | },
31 | "ViewerCertificate": {
32 | "ACMCertificateArn": "",
33 | "SSLSupportMethod": "sni-only",
34 | "MinimumProtocolVersion": "TLSv1.2_2021"
35 | },
36 | "Restrictions": {
37 | "GeoRestriction": {
38 | "RestrictionType": "none",
39 | "Quantity": 0
40 | }
41 | },
42 | "CustomErrorResponses": {
43 | "Quantity": 2,
44 | "Items": [
45 | {
46 | "ErrorCode": 403,
47 | "ResponsePagePath": "/index.html",
48 | "ResponseCode": "200",
49 | "ErrorCachingMinTTL": 300
50 | },
51 | {
52 | "ErrorCode": 404,
53 | "ResponsePagePath": "/index.html",
54 | "ResponseCode": "200",
55 | "ErrorCachingMinTTL": 300
56 | }
57 | ]
58 | },
59 | "HttpVersion": "http2",
60 | "IsIPV6Enabled": true,
61 | "DefaultRootObject": "index.html",
62 | "WebACLId": ""
63 | }
--------------------------------------------------------------------------------
/tmp/origin_access_control_config.json:
--------------------------------------------------------------------------------
1 | {
2 | "Name": "air-remote-access-control",
3 | "Description": "OAC for AirRemote bucket",
4 | "SigningProtocol": "sigv4",
5 | "SigningBehavior": "always",
6 | "OriginAccessControlOriginType": "s3"
7 | }
--------------------------------------------------------------------------------
/tmp/route_53_record.json:
--------------------------------------------------------------------------------
1 | {
2 | "Comment": "A record for CloudFront distribution",
3 | "Changes": [
4 | {
5 | "Action": "UPSERT",
6 | "ResourceRecordSet": {
7 | "Name": "",
8 | "Type": "A",
9 | "AliasTarget": {
10 | "HostedZoneId": "Z2FDTNDATAQYW2",
11 | "DNSName": "",
12 | "EvaluateTargetHealth": false
13 | }
14 | }
15 | }
16 | ]
17 | }
--------------------------------------------------------------------------------
/tmp/s3_bucket_policy.json:
--------------------------------------------------------------------------------
1 | {
2 | "Version": "2008-10-17",
3 | "Id": "PolicyForCloudFrontPrivateContent",
4 | "Statement": [
5 | {
6 | "Sid": "AllowCloudFrontServicePrincipal",
7 | "Effect": "Allow",
8 | "Principal": {
9 | "Service": "cloudfront.amazonaws.com"
10 | },
11 | "Action": "s3:GetObject",
12 | "Resource": "arn:aws:s3:::/*",
13 | "Condition": {
14 | "StringEquals": {
15 | "AWS:SourceArn": ""
16 | }
17 | }
18 | }
19 | ]
20 | }
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 | import svgr from 'vite-plugin-svgr'
4 | // https://vitejs.dev/config/
5 | import basicSsl from '@vitejs/plugin-basic-ssl';
6 | export default defineConfig({
7 | server: {
8 | port: 3000,
9 | host: '0.0.0.0',
10 | https: true
11 | },
12 | define: {
13 | global: {},
14 | 'process.env': {}
15 | },
16 | plugins: [react(),
17 | svgr({
18 | svgrOptions: {
19 |
20 | },
21 | }),
22 | basicSsl()],
23 | })
24 |
--------------------------------------------------------------------------------