├── .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 | 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 |
17 |
23 |
24 | ) 25 | 26 | const AcImg = () => ( 27 |
28 |
34 |
35 | ) 36 | 37 | const TvImg = () => ( 38 |
39 |
45 |
46 | ) 47 | 48 | const HeaterImg = () => ( 49 |
50 |
56 |
57 | ) 58 | const SpeakersImg = () => ( 59 |
60 |
66 |
67 | ) 68 | 69 | const DehumidifierImg = () => ( 70 |
71 |
77 |
78 | ) 79 | 80 | const GenericImg = () => ( 81 |
82 |
88 |
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 | {/* Air Conditioner */} 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 |
{props.isConnected ? : }
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 | 110 | 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 | 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 |
    6 |
    7 |
    8 | {children} 9 |
    10 |
    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 | 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 |
    12 | 29 |
    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 | 43 | 46 | 47 | 48 | )} 49 |
    50 |
    51 |
    52 |
    53 | 54 |
    55 |
    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 | Speaker 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 | Speaker 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 | 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 | --------------------------------------------------------------------------------