diff --git a/.prettierrc b/.prettierrc index 30f01c1..1eed9cb 100644 --- a/.prettierrc +++ b/.prettierrc @@ -19,7 +19,8 @@ { "files": ["packages/frontend/**/*"], "options": { - "plugins": ["prettier-plugin-tailwindcss"] + "plugins": ["prettier-plugin-tailwindcss"], + "tailwindPreserveWhitespace": true } } ] diff --git a/packages/frontend/eslint.config.js b/packages/frontend/eslint.config.js index a768ca9..6f771b7 100644 --- a/packages/frontend/eslint.config.js +++ b/packages/frontend/eslint.config.js @@ -18,6 +18,7 @@ export default tseslint.config( 'react-refresh': reactRefresh }, rules: { + '@typescript-eslint/no-unused-vars': 'off', ...reactHooks.configs.recommended.rules, 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }] } diff --git a/packages/frontend/generate-react-cli.json b/packages/frontend/generate-react-cli.json index 65a29c9..6558d41 100644 --- a/packages/frontend/generate-react-cli.json +++ b/packages/frontend/generate-react-cli.json @@ -1,16 +1,16 @@ { - "usesTypeScript": true, - "usesStyledComponents": false, - "usesCssModule": true, - "cssPreprocessor": "scss", - "testLibrary": "Testing Library", - "component": { - "default": { - "path": "src/components", - "withStyle": true, - "withTest": true, - "withStory": true, - "withLazy": true + "usesTypeScript": true, + "usesStyledComponents": false, + "usesCssModule": true, + "cssPreprocessor": "scss", + "testLibrary": "Testing Library", + "component": { + "default": { + "path": "src/components", + "withStyle": false, + "withTest": true, + "withStory": true, + "withLazy": true + } } - } -} \ No newline at end of file +} diff --git a/packages/frontend/index.html b/packages/frontend/index.html index 72f3ebe..a286b74 100644 --- a/packages/frontend/index.html +++ b/packages/frontend/index.html @@ -3,11 +3,11 @@ - + TACOS -
+
diff --git a/packages/frontend/package.json b/packages/frontend/package.json index eaca4ea..3ccdef4 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -3,6 +3,8 @@ "generate-react-cli": "^8.4.8", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-icons": "^5.3.0", + "react-router-dom": "^6.27.0", "stylelint": "^16.9.0", "zod": "^3.23.8" }, diff --git a/packages/frontend/public/logo.png b/packages/frontend/public/logo.png new file mode 100644 index 0000000..cb16ecb Binary files /dev/null and b/packages/frontend/public/logo.png differ diff --git a/packages/frontend/public/react.svg b/packages/frontend/public/react.svg deleted file mode 100644 index 6c87de9..0000000 --- a/packages/frontend/public/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/frontend/public/vite.svg b/packages/frontend/public/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/packages/frontend/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/frontend/src/components/App/App.css b/packages/frontend/src/components/App/App.css index e3204a2..e69de29 100644 --- a/packages/frontend/src/components/App/App.css +++ b/packages/frontend/src/components/App/App.css @@ -1,45 +0,0 @@ -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} - -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} - -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} diff --git a/packages/frontend/src/components/App/App.tsx b/packages/frontend/src/components/App/App.tsx index 179502f..8f5d231 100644 --- a/packages/frontend/src/components/App/App.tsx +++ b/packages/frontend/src/components/App/App.tsx @@ -1,34 +1,39 @@ -import { useState } from 'react' +import { useState, useEffect } from 'react' +import { Routes, Route } from 'react-router-dom' import './App.css' +import MobileBar from '../MobileBar/MobileBar' +import Header from '../Header/Header' +//import SideBarVertical from '../SideBarVertical/SideBarVertical' + +import HomePage from '../Pages/HomePage/HomePage' +import DevicesPage from '../Pages/DevicesPage/DevicesPage' +import TerminalsPage from '../Pages/TerminalsPage/TerminalsPage' +import LogsPage from '../Pages/LogsPage/LogsPage' export function App() { - const [count, setCount] = useState(0) + const [isMobile, setIsMobile] = useState(false) + + const handleReize = () => { + setIsMobile(window.matchMedia('(max-width: 640px)').matches) + } + + useEffect(() => { + handleReize() + window.addEventListener('resize', handleReize) + return () => window.removeEventListener('resize', handleReize) + }, []) return ( - <> -
-
- - Vite logo - -
-
- - React logo - -
-
-

- TACOS -

-
- -

- Edit src/App.tsx and save to test HMR -

-
-

Click on the Vite and React logos to learn more

- +
+
+ {isMobile ? : } + + } /> + } /> + } /> + } /> + +
) } diff --git a/packages/frontend/src/components/Header/DarkModeToggle/DarkModeToggle.stories.tsx b/packages/frontend/src/components/Header/DarkModeToggle/DarkModeToggle.stories.tsx new file mode 100644 index 0000000..5701a8c --- /dev/null +++ b/packages/frontend/src/components/Header/DarkModeToggle/DarkModeToggle.stories.tsx @@ -0,0 +1,12 @@ +/* eslint-disable */ +import DarkModeToggle from './DarkModeToggle'; + +export default { + title: "DarkModeToggle", +}; + +export const Default = () => ; + +Default.story = { + name: 'default', +}; diff --git a/packages/frontend/src/components/Header/DarkModeToggle/DarkModeToggle.tsx b/packages/frontend/src/components/Header/DarkModeToggle/DarkModeToggle.tsx new file mode 100644 index 0000000..3ea0c28 --- /dev/null +++ b/packages/frontend/src/components/Header/DarkModeToggle/DarkModeToggle.tsx @@ -0,0 +1,41 @@ +import { FC, useState, useEffect } from 'react' +import { FaMoon, FaS, FaSun } from 'react-icons/fa6' + +const DarkModeToggle: FC = () => { + const DM_KEY = 'dark-mode-key' + const [isDarkMode, setIsDarkMode] = useState(() => { + const savedValue = localStorage.getItem(DM_KEY) + return savedValue ? JSON.parse(savedValue) : true + }) + + const handleToggle = () => { + setIsDarkMode(!isDarkMode) + if (!isDarkMode) { + document.getElementById('root')?.classList.add('dark') + document.getElementById('root')?.classList.remove('light') + } else { + document.getElementById('root')?.classList.add('light') + document.getElementById('root')?.classList.remove('dark') + } + } + + useEffect(() => { + localStorage.setItem(DM_KEY, JSON.stringify(isDarkMode)) + }, [isDarkMode]) + + return ( +
+
+ {isDarkMode ? : } +
+
+ ) +} + +export default DarkModeToggle diff --git a/packages/frontend/src/components/Header/Header.stories.tsx b/packages/frontend/src/components/Header/Header.stories.tsx new file mode 100644 index 0000000..0cfcaf8 --- /dev/null +++ b/packages/frontend/src/components/Header/Header.stories.tsx @@ -0,0 +1,12 @@ +/* eslint-disable */ +import Header from './Header'; + +export default { + title: "Header", +}; + +export const Default = () =>
; + +Default.story = { + name: 'default', +}; diff --git a/packages/frontend/src/components/Header/Header.tsx b/packages/frontend/src/components/Header/Header.tsx new file mode 100644 index 0000000..21ef7cc --- /dev/null +++ b/packages/frontend/src/components/Header/Header.tsx @@ -0,0 +1,19 @@ +import { FC } from 'react' +import DarkModeToggle from './DarkModeToggle/DarkModeToggle' +import VHS from './logo.png' + +const Header: FC = () => { + return ( +
+
+
+ VHS +
TACOS
+
+ +
+
+ ) +} + +export default Header diff --git a/packages/frontend/src/components/Header/Subheader.tsx b/packages/frontend/src/components/Header/Subheader.tsx new file mode 100644 index 0000000..72faa08 --- /dev/null +++ b/packages/frontend/src/components/Header/Subheader.tsx @@ -0,0 +1,17 @@ +import { FC } from 'react' + +interface SubheaderProps { + text: string +} + +const Subheader: FC = (props) => { + return ( +
+
+
{props.text}
+
+
+ ) +} + +export default Subheader diff --git a/packages/frontend/src/components/Header/logo.png b/packages/frontend/src/components/Header/logo.png new file mode 100644 index 0000000..cb16ecb Binary files /dev/null and b/packages/frontend/src/components/Header/logo.png differ diff --git a/packages/frontend/src/components/MobileBar/MobileBar.css b/packages/frontend/src/components/MobileBar/MobileBar.css new file mode 100644 index 0000000..470cfd1 --- /dev/null +++ b/packages/frontend/src/components/MobileBar/MobileBar.css @@ -0,0 +1,29 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer components { + .navbar { + @apply bg-navbar-body text-navbar-text fixed bottom-0 + left-0 flex w-screen translate-y-0 flex-col + transition-all duration-300; + } + + .navbar-collapsed { + @apply translate-y-full; + } + + .navbar-button { + @apply bg-navbar-grad-dark flex size-[74px] + flex-col items-center justify-center + rounded-xl bg-gradient-to-tr pt-1 shadow-lg; + } + + .navbar-icon { + @apply mb-1; + } + + .navbar-icon-text { + @apply font-teko text-lg font-medium; + } +} diff --git a/packages/frontend/src/components/MobileBar/MobileBar.stories.tsx b/packages/frontend/src/components/MobileBar/MobileBar.stories.tsx new file mode 100644 index 0000000..3d210a1 --- /dev/null +++ b/packages/frontend/src/components/MobileBar/MobileBar.stories.tsx @@ -0,0 +1,12 @@ +/* eslint-disable */ +import MobileBar from './MobileBar'; + +export default { + title: "MobileBar", +}; + +export const Default = () => ; + +Default.story = { + name: 'default', +}; diff --git a/packages/frontend/src/components/MobileBar/MobileBar.tsx b/packages/frontend/src/components/MobileBar/MobileBar.tsx new file mode 100644 index 0000000..3c323f3 --- /dev/null +++ b/packages/frontend/src/components/MobileBar/MobileBar.tsx @@ -0,0 +1,70 @@ +import { FC, useState, useEffect } from 'react' +import { FaDesktop } from 'react-icons/fa' +import { FaHouseChimney, FaCodepen, FaClipboardList } from 'react-icons/fa6' +import MobileBarIcon from './MobileBarIcon/MobileBarIcon' +import './MobileBar.css' + +const MobileBar: FC = () => { + const [isVisible, setIsVisible] = useState(true) + const [lastScrollY, setLastScrollY] = useState(0) + + const SideBarIcons = [ + { + icon: , + text: 'Home', + path: '/' + }, + { + icon: , + text: 'Devices', + path: '/devices' + }, + { + icon: , + text: 'Terminals', + path: '/terminals' + }, + { + icon: , + text: 'Logs', + path: '/logs' + } + ] + + useEffect(() => { + const handleScroll = () => { + const curScrollY = window.scrollY + const maxScrollY = document.documentElement.scrollHeight - window.innerHeight + + if (curScrollY >= maxScrollY || curScrollY <= 0) { + return + } + + if (curScrollY > lastScrollY && curScrollY > 100) { + setIsVisible(false) + } else if (curScrollY < lastScrollY) { + setIsVisible(true) + } + + setLastScrollY(curScrollY) + } + + window.addEventListener('scroll', handleScroll) + + return () => { + window.removeEventListener('scroll', handleScroll) + } + }, [lastScrollY]) + + return ( +
+
+ {SideBarIcons.map((i) => ( + + ))} +
+
+ ) +} + +export default MobileBar diff --git a/packages/frontend/src/components/MobileBar/MobileBarIcon/MobileBarIcon.stories.tsx b/packages/frontend/src/components/MobileBar/MobileBarIcon/MobileBarIcon.stories.tsx new file mode 100644 index 0000000..395e82f --- /dev/null +++ b/packages/frontend/src/components/MobileBar/MobileBarIcon/MobileBarIcon.stories.tsx @@ -0,0 +1,12 @@ +/* eslint-disable */ +import MobileBarIcon from './MobileBarIcon' + +export default { + title: 'MobileBarIcon' +} + +export const Default = () => + +Default.story = { + name: 'default' +} diff --git a/packages/frontend/src/components/MobileBar/MobileBarIcon/MobileBarIcon.tsx b/packages/frontend/src/components/MobileBar/MobileBarIcon/MobileBarIcon.tsx new file mode 100644 index 0000000..384a470 --- /dev/null +++ b/packages/frontend/src/components/MobileBar/MobileBarIcon/MobileBarIcon.tsx @@ -0,0 +1,27 @@ +import React, { FC } from 'react' +import { useNavigate } from 'react-router-dom' + +interface MobileBarIconProps { + icon: React.ReactNode + text: string + path: string +} + +const MobileBarIcon: FC = ({ icon, text, path }) => { + const navigate = useNavigate() + const isActive = location.pathname === path + + const handleClick = () => { + navigate(path) + console.log(location.pathname) + } + + return ( +
+
{icon}
+
{text}
+
+ ) +} + +export default MobileBarIcon diff --git a/packages/frontend/src/components/Pages/DevicesPage/Devices/Device/Device.stories.tsx b/packages/frontend/src/components/Pages/DevicesPage/Devices/Device/Device.stories.tsx new file mode 100644 index 0000000..4eac618 --- /dev/null +++ b/packages/frontend/src/components/Pages/DevicesPage/Devices/Device/Device.stories.tsx @@ -0,0 +1,12 @@ +/* eslint-disable */ +import Device from './Device'; + +export default { + title: "Device", +}; + +export const Default = () => ; + +Default.story = { + name: 'default', +}; diff --git a/packages/frontend/src/components/Pages/DevicesPage/Devices/Device/Device.tsx b/packages/frontend/src/components/Pages/DevicesPage/Devices/Device/Device.tsx new file mode 100644 index 0000000..68d1ce7 --- /dev/null +++ b/packages/frontend/src/components/Pages/DevicesPage/Devices/Device/Device.tsx @@ -0,0 +1,37 @@ +import { FC, useState } from 'react' +import DeviceDescription from './DeviceDescription/DeviceDescription' +import DeviceRoles from './DeviceRoles/DeviceRoles' +import DeviceButton from './DeviceButton/DeviceButton' + +interface DeviceProps { + onDelete: (id: string) => void + onArm: (id: string) => void + name: string + role: string + state: boolean + seen: string + id: string +} + +const Device: FC = (props) => { + const [selectedRole, setSelectedRole] = useState(props.role) + + return ( +
+
{props.name}
+ + +
+ State: + {props.state ? 'Armed' : 'Unarmed'} +
+
+ Last Seen: + {props.seen} +
+ +
+ ) +} + +export default Device diff --git a/packages/frontend/src/components/Pages/DevicesPage/Devices/Device/DeviceButton/DeviceButton.stories.tsx b/packages/frontend/src/components/Pages/DevicesPage/Devices/Device/DeviceButton/DeviceButton.stories.tsx new file mode 100644 index 0000000..40b51f6 --- /dev/null +++ b/packages/frontend/src/components/Pages/DevicesPage/Devices/Device/DeviceButton/DeviceButton.stories.tsx @@ -0,0 +1,12 @@ +/* eslint-disable */ +import DeviceButton from './DeviceButton'; + +export default { + title: "DeviceButton", +}; + +export const Default = () => ; + +Default.story = { + name: 'default', +}; diff --git a/packages/frontend/src/components/Pages/DevicesPage/Devices/Device/DeviceButton/DeviceButton.tsx b/packages/frontend/src/components/Pages/DevicesPage/Devices/Device/DeviceButton/DeviceButton.tsx new file mode 100644 index 0000000..00d1562 --- /dev/null +++ b/packages/frontend/src/components/Pages/DevicesPage/Devices/Device/DeviceButton/DeviceButton.tsx @@ -0,0 +1,30 @@ +import { FC, useState } from 'react' + +interface DeviceButtonProps { + onDelete: (id: string) => void + onArm: (id: string) => void + id: string +} + +const DeviceButton: FC = (props) => { + const [isArmed, setIsArmed] = useState(false) + + return ( +
+ + +
+ ) +} + +export default DeviceButton diff --git a/packages/frontend/src/components/Pages/DevicesPage/Devices/Device/DeviceDescription/DeviceDescription.stories.tsx b/packages/frontend/src/components/Pages/DevicesPage/Devices/Device/DeviceDescription/DeviceDescription.stories.tsx new file mode 100644 index 0000000..e94a21f --- /dev/null +++ b/packages/frontend/src/components/Pages/DevicesPage/Devices/Device/DeviceDescription/DeviceDescription.stories.tsx @@ -0,0 +1,12 @@ +/* eslint-disable */ +import DeviceDescription from './DeviceDescription'; + +export default { + title: "DeviceDescription", +}; + +export const Default = () => ; + +Default.story = { + name: 'default', +}; diff --git a/packages/frontend/src/components/Pages/DevicesPage/Devices/Device/DeviceDescription/DeviceDescription.tsx b/packages/frontend/src/components/Pages/DevicesPage/Devices/Device/DeviceDescription/DeviceDescription.tsx new file mode 100644 index 0000000..5e2ba73 --- /dev/null +++ b/packages/frontend/src/components/Pages/DevicesPage/Devices/Device/DeviceDescription/DeviceDescription.tsx @@ -0,0 +1,39 @@ +import { FC, useState } from 'react' + +interface DeviceDescriptionProps { + id: string +} + +const DeviceDescriptionForm: FC = (props) => { + const [isEdit, setIsEdit] = useState(true) + + const handleClick = () => { + setIsEdit(!isEdit) + } + + return ( + <> +
+ +
+ + +
+
+ + ) +} + +export default DeviceDescriptionForm diff --git a/packages/frontend/src/components/Pages/DevicesPage/Devices/Device/DeviceRoles/DeviceRoles.stories.tsx b/packages/frontend/src/components/Pages/DevicesPage/Devices/Device/DeviceRoles/DeviceRoles.stories.tsx new file mode 100644 index 0000000..cc4103b --- /dev/null +++ b/packages/frontend/src/components/Pages/DevicesPage/Devices/Device/DeviceRoles/DeviceRoles.stories.tsx @@ -0,0 +1,12 @@ +/* eslint-disable */ +import DeviceRoles from './DeviceRoles'; + +export default { + title: "DeviceRoles", +}; + +export const Default = () => ; + +Default.story = { + name: 'default', +}; diff --git a/packages/frontend/src/components/Pages/DevicesPage/Devices/Device/DeviceRoles/DeviceRoles.tsx b/packages/frontend/src/components/Pages/DevicesPage/Devices/Device/DeviceRoles/DeviceRoles.tsx new file mode 100644 index 0000000..161a986 --- /dev/null +++ b/packages/frontend/src/components/Pages/DevicesPage/Devices/Device/DeviceRoles/DeviceRoles.tsx @@ -0,0 +1,40 @@ +import { FC } from 'react' + +interface DeviceRolesProps { + onRoleChange: (role: string) => void + selectedRole: string + id: string +} + +const DeviceRoles: FC = (props) => { + const roles = [ + 'tool:main:lazer-cutter', + 'tool:metal:cnc', + 'tool:metal:mill', + 'tool:metal:lathe', + 'tool:wood:cnc', + 'tool:wood:jointer-planer', + 'tool:wood:table-saw' + ] + + return ( +
+ Roles: + +
+ ) +} + +export default DeviceRoles diff --git a/packages/frontend/src/components/Pages/DevicesPage/Devices/Devices.stories.tsx b/packages/frontend/src/components/Pages/DevicesPage/Devices/Devices.stories.tsx new file mode 100644 index 0000000..fe7caa0 --- /dev/null +++ b/packages/frontend/src/components/Pages/DevicesPage/Devices/Devices.stories.tsx @@ -0,0 +1,12 @@ +/* eslint-disable */ +import Devices from './Devices'; + +export default { + title: "Devices", +}; + +export const Default = () => ; + +Default.story = { + name: 'default', +}; diff --git a/packages/frontend/src/components/Pages/DevicesPage/Devices/Devices.tsx b/packages/frontend/src/components/Pages/DevicesPage/Devices/Devices.tsx new file mode 100644 index 0000000..737c1c3 --- /dev/null +++ b/packages/frontend/src/components/Pages/DevicesPage/Devices/Devices.tsx @@ -0,0 +1,59 @@ +import Device from './Device/Device' +import { useState, useEffect } from 'react' + +const DeviceContainers = [ + { name: 'Lazer Cutter', role: 'tool:main:lazer-cutter', state: false, seen: '2 Years Ago', id: '' }, + { name: 'Lathe', role: 'tool:metal:lathe', state: false, seen: '2 Years Ago', id: '' }, + { name: 'Metal CNC', role: 'tool:metal:cnc', state: false, seen: '2 Years Ago', id: '' }, + { name: 'Mill', role: 'tool:metal:mill', state: false, seen: '2 Years Ago', id: '' }, + { name: 'Wood CNC', role: 'tool:wood:cnc', state: false, seen: '2 Years Ago', id: '' }, + { name: 'Jointer-Planer', role: 'tool:wood:jointer-planer', state: false, seen: '2 Years Ago', id: '' }, + { name: 'Table Saw', role: 'tool:wood:table-saw', state: false, seen: '2 Years Ago', id: '' } +] + +export default function Devices() { + const [deviceIDs, setDevicesIDs] = useState([]) + + const handleDelete = (id: string) => { + setDevicesIDs((prevDeviceIDs) => prevDeviceIDs.filter((deviceID) => deviceID !== id)) + } + + const handleArm = (id: string) => { + DeviceContainers.forEach((i) => { + if (i.id === id) { + i.state = !i.state + } + }) + } + + const addDeviceID = (id: string) => { + setDevicesIDs((prevDeviceIDs) => [...prevDeviceIDs, id]) + } + + useEffect(() => { + DeviceContainers.forEach((i) => { + i.id = crypto.randomUUID() + addDeviceID(i.id) + }) + }, []) + + return ( +
+ {DeviceContainers.map((device) => + deviceIDs.includes(device.id) ? ( + + ) : null + )} +
+
+ ) +} diff --git a/packages/frontend/src/components/Pages/DevicesPage/DevicesPage.stories.tsx b/packages/frontend/src/components/Pages/DevicesPage/DevicesPage.stories.tsx new file mode 100644 index 0000000..25d801f --- /dev/null +++ b/packages/frontend/src/components/Pages/DevicesPage/DevicesPage.stories.tsx @@ -0,0 +1,12 @@ +/* eslint-disable */ +import DevicesPage from './DevicesPage'; + +export default { + title: "DevicesPage", +}; + +export const Default = () => ; + +Default.story = { + name: 'default', +}; diff --git a/packages/frontend/src/components/Pages/DevicesPage/DevicesPage.tsx b/packages/frontend/src/components/Pages/DevicesPage/DevicesPage.tsx new file mode 100644 index 0000000..6238a4b --- /dev/null +++ b/packages/frontend/src/components/Pages/DevicesPage/DevicesPage.tsx @@ -0,0 +1,14 @@ +import { FC } from 'react' +import Devices from './Devices/Devices' +import Subheader from '../../Header/Subheader' + +const DevicesPage: FC = () => { + return ( +
+ + +
+ ) +} + +export default DevicesPage diff --git a/packages/frontend/src/components/Pages/HomePage/CondensedDevices/CondensedDevice/CondensedDevice.tsx b/packages/frontend/src/components/Pages/HomePage/CondensedDevices/CondensedDevice/CondensedDevice.tsx new file mode 100644 index 0000000..78d07ba --- /dev/null +++ b/packages/frontend/src/components/Pages/HomePage/CondensedDevices/CondensedDevice/CondensedDevice.tsx @@ -0,0 +1,39 @@ +import { FC, useState } from 'react' + +interface CondensedDeviceProps { + name: string + role: string + state: boolean + seen: string + id: string +} + +const CondensedDevice: FC = (props) => { + const [isArmed, setIsArmed] = useState(false) + + return ( +
+
{props.name}
+
+ State: + {props.state ? 'Armed' : 'Unarmed'} +
+
+ Last Seen: + {props.seen} +
+
+ +
+
+ ) +} + +export default CondensedDevice diff --git a/packages/frontend/src/components/Pages/HomePage/CondensedDevices/CondensedDevices.tsx b/packages/frontend/src/components/Pages/HomePage/CondensedDevices/CondensedDevices.tsx new file mode 100644 index 0000000..6a0a863 --- /dev/null +++ b/packages/frontend/src/components/Pages/HomePage/CondensedDevices/CondensedDevices.tsx @@ -0,0 +1,32 @@ +import { FC } from 'react' +import CondensedDevice from './CondensedDevice/CondensedDevice' + +const CondensedDeviceContainers = [ + { name: 'Lazer Cutter', role: 'tool:main:lazer-cutter', state: false, seen: '2 Years Ago', id: '' }, + { name: 'Metal CNC', role: 'tool:metal:cnc', state: false, seen: '2 Years Ago', id: '' }, + { name: 'Mill', role: 'tool:metal:mill', state: false, seen: '2 Years Ago', id: '' }, + { name: 'Lathe', role: 'tool:metal:lathe', state: false, seen: '2 Years Ago', id: '' }, + { name: 'Wood CNC', role: 'tool:wood:cnc', state: false, seen: '2 Years Ago', id: '' }, + { name: 'Jointer-Planer', role: 'tool:wood:jointer-planer', state: false, seen: '2 Years Ago', id: '' }, + { name: 'Table Saw', role: 'tool:wood:table-saw', state: false, seen: '2 Years Ago', id: '' } +] + +const CondensedDevices: FC = () => { + return ( +
+ {CondensedDeviceContainers.map((device) => ( + + ))} +
+
+ ) +} + +export default CondensedDevices diff --git a/packages/frontend/src/components/Pages/HomePage/HomePage.stories.tsx b/packages/frontend/src/components/Pages/HomePage/HomePage.stories.tsx new file mode 100644 index 0000000..c8486b9 --- /dev/null +++ b/packages/frontend/src/components/Pages/HomePage/HomePage.stories.tsx @@ -0,0 +1,11 @@ +import HomePage from './HomePage' + +export default { + title: 'HomePage' +} + +export const Default = () => + +Default.story = { + name: 'default' +} diff --git a/packages/frontend/src/components/Pages/HomePage/HomePage.tsx b/packages/frontend/src/components/Pages/HomePage/HomePage.tsx new file mode 100644 index 0000000..7bdab7b --- /dev/null +++ b/packages/frontend/src/components/Pages/HomePage/HomePage.tsx @@ -0,0 +1,14 @@ +import { FC } from 'react' +import Subheader from '../../Header/Subheader' +import CondensedDevices from './CondensedDevices/CondensedDevices' + +const HomePage: FC = () => { + return ( +
+ + +
+ ) +} + +export default HomePage diff --git a/packages/frontend/src/components/Pages/LogsPage/LogsPage.stories.tsx b/packages/frontend/src/components/Pages/LogsPage/LogsPage.stories.tsx new file mode 100644 index 0000000..6df0772 --- /dev/null +++ b/packages/frontend/src/components/Pages/LogsPage/LogsPage.stories.tsx @@ -0,0 +1,12 @@ +/* eslint-disable */ +import LogsPage from './LogsPage'; + +export default { + title: "LogsPage", +}; + +export const Default = () => ; + +Default.story = { + name: 'default', +}; diff --git a/packages/frontend/src/components/Pages/LogsPage/LogsPage.tsx b/packages/frontend/src/components/Pages/LogsPage/LogsPage.tsx new file mode 100644 index 0000000..f62ff0d --- /dev/null +++ b/packages/frontend/src/components/Pages/LogsPage/LogsPage.tsx @@ -0,0 +1,12 @@ +import { FC } from 'react' +import Subheader from '../../Header/Subheader' + +const LogsPage: FC = () => { + return ( +
+ +
+ ) +} + +export default LogsPage diff --git a/packages/frontend/src/components/Pages/TerminalsPage/Terminals/Terminal/Terminal.tsx b/packages/frontend/src/components/Pages/TerminalsPage/Terminals/Terminal/Terminal.tsx new file mode 100644 index 0000000..65a6b8b --- /dev/null +++ b/packages/frontend/src/components/Pages/TerminalsPage/Terminals/Terminal/Terminal.tsx @@ -0,0 +1,63 @@ +import { FC, useState } from 'react' +import TerminalField from './TerminalField/TerminalField' +import TerminalTargets from './TerminalTargets/TerminalTargets' + +interface TerminalProps { + onDelete: (id: string) => void + onArm?: (id: string) => void + name: string + role: string + state: boolean + seen: string + id: string + windowWidth: number +} + +const Terminal: FC = ({ onDelete, onArm, name, role, state, seen, id, windowWidth }) => { + const [selectedRole, setSelectedRole] = useState(role) + const [enabled, setEnabled] = useState(false) + + return ( +
+
{name}
+ +
+ +
+ { + setEnabled(!enabled) + }} + /> +
{enabled ? 'Enabled' : 'Disabled'}
+
+
+ +
+ Secure: + {state ? 'Secured' : 'Unsecured'} +
+ +
+ Last Seen: + {seen} +
+
+ +
+
+ ) +} + +export default Terminal diff --git a/packages/frontend/src/components/Pages/TerminalsPage/Terminals/Terminal/TerminalField/TerminalField.tsx b/packages/frontend/src/components/Pages/TerminalsPage/Terminals/Terminal/TerminalField/TerminalField.tsx new file mode 100644 index 0000000..951677e --- /dev/null +++ b/packages/frontend/src/components/Pages/TerminalsPage/Terminals/Terminal/TerminalField/TerminalField.tsx @@ -0,0 +1,43 @@ +import { FC, useState } from 'react' + +interface TerminalFieldProps { + label: string + id: string + type: string + windowWidth: number +} + +const OFFSET = 56 + +const TerminalField: FC = ({ label, id, type, windowWidth }) => { + const [isEdit, setIsEdit] = useState(true) + const [innerWidth, setInnerWidth] = useState(windowWidth - OFFSET) + + const handleClick = () => { + setIsEdit(!isEdit) + } + + return ( +
+ +
+ + +
+
+ ) +} + +export default TerminalField diff --git a/packages/frontend/src/components/Pages/TerminalsPage/Terminals/Terminal/TerminalTargets/TerminalTargets.tsx b/packages/frontend/src/components/Pages/TerminalsPage/Terminals/Terminal/TerminalTargets/TerminalTargets.tsx new file mode 100644 index 0000000..ed828af --- /dev/null +++ b/packages/frontend/src/components/Pages/TerminalsPage/Terminals/Terminal/TerminalTargets/TerminalTargets.tsx @@ -0,0 +1,40 @@ +import { FC } from 'react' + +interface TerminalTargetsProps { + onRoleChange: (role: string) => void + selectedRole: string + id: string +} + +const TerminalTargets: FC = (props) => { + const roles = [ + 'tool:main:lazer-cutter', + 'tool:metal:cnc', + 'tool:metal:mill', + 'tool:metal:lathe', + 'tool:wood:cnc', + 'tool:wood:jointer-planer', + 'tool:wood:table-saw' + ] + + return ( +
+ Target: + +
+ ) +} + +export default TerminalTargets diff --git a/packages/frontend/src/components/Pages/TerminalsPage/Terminals/Terminals.tsx b/packages/frontend/src/components/Pages/TerminalsPage/Terminals/Terminals.tsx new file mode 100644 index 0000000..4f766e9 --- /dev/null +++ b/packages/frontend/src/components/Pages/TerminalsPage/Terminals/Terminals.tsx @@ -0,0 +1,59 @@ +import { FC, useState, useEffect } from 'react' +import Terminal from './Terminal/Terminal' + +const DeviceContainers = [ + { name: 'Terminal 1', role: 'tool:main:lazer-cutter', state: true, seen: '2 Years Ago', id: '' }, + { name: 'Terminal 2', role: 'tool:metal:lathe', state: true, seen: '2 Years Ago', id: '' }, + { name: 'Terminal 3', role: 'tool:metal:cnc', state: true, seen: '2 Years Ago', id: '' }, + { name: 'Terminal 4', role: 'tool:metal:mill', state: true, seen: '2 Years Ago', id: '' }, + { name: 'Terminal 5', role: 'tool:wood:cnc', state: true, seen: '2 Years Ago', id: '' } +] + +const Terminals: FC = () => { + const [deviceIDs, setDevicesIDs] = useState([]) + const [windowWidth, setWindowWidth] = useState(window.innerWidth) + + const handleDelete = (id: string) => { + setDevicesIDs((prevDeviceIDs) => prevDeviceIDs.filter((deviceID) => deviceID !== id)) + } + + const addDeviceID = (id: string) => { + setDevicesIDs((prevDeviceIDs) => [...prevDeviceIDs, id]) + } + + const updateWidth = () => { + setWindowWidth(window.innerWidth) + } + + useEffect(() => { + DeviceContainers.forEach((i) => { + i.id = crypto.randomUUID() + addDeviceID(i.id) + }) + + window.addEventListener('resize', updateWidth) + return () => window.removeEventListener('resize', updateWidth) + }, []) + + return ( +
+ {DeviceContainers.map((device) => + deviceIDs.includes(device.id) ? ( + + ) : null + )} +
+
+ ) +} + +export default Terminals diff --git a/packages/frontend/src/components/Pages/TerminalsPage/TerminalsPage.stories.tsx b/packages/frontend/src/components/Pages/TerminalsPage/TerminalsPage.stories.tsx new file mode 100644 index 0000000..b4c70af --- /dev/null +++ b/packages/frontend/src/components/Pages/TerminalsPage/TerminalsPage.stories.tsx @@ -0,0 +1,12 @@ +/* eslint-disable */ +import TerminalsPage from './TerminalsPage'; + +export default { + title: "TerminalsPage", +}; + +export const Default = () => ; + +Default.story = { + name: 'default', +}; diff --git a/packages/frontend/src/components/Pages/TerminalsPage/TerminalsPage.tsx b/packages/frontend/src/components/Pages/TerminalsPage/TerminalsPage.tsx new file mode 100644 index 0000000..4288a8a --- /dev/null +++ b/packages/frontend/src/components/Pages/TerminalsPage/TerminalsPage.tsx @@ -0,0 +1,14 @@ +import { FC } from 'react' +import Subheader from '../../Header/Subheader' +import Devices from './Terminals/Terminals' + +const TerminalsPage: FC = () => { + return ( +
+ + +
+ ) +} + +export default TerminalsPage diff --git a/packages/frontend/src/components/SideBarVertical/SideBarIcon/SideBarIcon.stories.tsx b/packages/frontend/src/components/SideBarVertical/SideBarIcon/SideBarIcon.stories.tsx new file mode 100644 index 0000000..bb08b8d --- /dev/null +++ b/packages/frontend/src/components/SideBarVertical/SideBarIcon/SideBarIcon.stories.tsx @@ -0,0 +1,12 @@ +/* eslint-disable */ +import SideBarIcon from './SideBarIcon' + +export default { + title: 'SideBarIcon' +} + +export const Default = () => + +Default.story = { + name: 'default' +} diff --git a/packages/frontend/src/components/SideBarVertical/SideBarIcon/SideBarIcon.tsx b/packages/frontend/src/components/SideBarVertical/SideBarIcon/SideBarIcon.tsx new file mode 100644 index 0000000..ae98d2c --- /dev/null +++ b/packages/frontend/src/components/SideBarVertical/SideBarIcon/SideBarIcon.tsx @@ -0,0 +1,18 @@ +import React, { FC } from 'react' + +interface SideBarIconProps { + icon?: React.ReactNode + text?: string +} + +const SideBarIcon: FC = (props) => ( +
+
+ {props.icon} + {props.text} + {props.text} +
+
+) + +export default SideBarIcon diff --git a/packages/frontend/src/components/SideBarVertical/SideBarVertical.css b/packages/frontend/src/components/SideBarVertical/SideBarVertical.css new file mode 100644 index 0000000..a1b27ef --- /dev/null +++ b/packages/frontend/src/components/SideBarVertical/SideBarVertical.css @@ -0,0 +1,60 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer components { + .sidebar-v { + @apply fixed left-0 top-0 m-0 flex + h-screen w-[236px] flex-col bg-gray-900 + text-white shadow-lg transition-all delay-150 duration-300 ease-linear; + } + + .sidebar-v-collapsed { + @apply w-[114px]; + } + + .sidebar-v-header { + @apply font-teko ml-4 mt-[11.5px] min-h-[60px] bg-gradient-to-r + from-yellow-600 via-orange-600 to-red-600 bg-clip-text + text-6xl font-semibold leading-[60px] + text-transparent transition-all delay-150 duration-[350ms] ease-linear; + } + + .sidebar-v-collapsed .sidebar-v-header { + @apply text-2xl duration-[300ms] ease-linear; + } + + .sidebar-v-icon { + @apply relative mb-6 ml-[18px] flex + h-10 w-10 cursor-pointer items-center justify-center + rounded-[15px] bg-gradient-to-r from-yellow-600 + via-orange-600 to-red-600 transition-all duration-300 ease-linear + hover:rounded-[10px]; + } + + .sidebar-v-tooltip { + @apply absolute left-28 m-2 w-auto min-w-max origin-left + scale-0 rounded-md bg-gray-900 p-2 text-lg font-bold + text-white shadow-md transition-all duration-100; + } + + .sidebar-v-collapsed .sidebar-v-icon:hover .sidebar-v-tooltip { + @apply scale-100; + } + + .sidebar-v-icon-text { + @apply font-teko absolute left-16 min-w-max text-2xl font-medium + opacity-100 transition-opacity delay-[300ms] duration-[450ms]; + } + + .sidebar-v-collapsed .sidebar-v-icon-text { + @apply opacity-0 transition-opacity delay-0 duration-200; + } + + .sidebar-v-expand-button { + @apply absolute left-full top-1/2 flex h-6 w-6 -translate-x-3/4 + cursor-pointer items-center justify-center rounded-[10px] + bg-gradient-to-r from-yellow-600 via-orange-600 to-red-600 + transition-all delay-100 duration-300 ease-linear hover:rounded-md; + } +} diff --git a/packages/frontend/src/components/SideBarVertical/SideBarVertical.stories.tsx b/packages/frontend/src/components/SideBarVertical/SideBarVertical.stories.tsx new file mode 100644 index 0000000..99370ef --- /dev/null +++ b/packages/frontend/src/components/SideBarVertical/SideBarVertical.stories.tsx @@ -0,0 +1,12 @@ +/* eslint-disable */ +import SideBarVertical from './SideBarVertical'; + +export default { + title: "SideBarVertical", +}; + +export const Default = () => ; + +Default.story = { + name: 'default', +}; diff --git a/packages/frontend/src/components/SideBarVertical/SideBarVertical.tsx b/packages/frontend/src/components/SideBarVertical/SideBarVertical.tsx new file mode 100644 index 0000000..1b13a4f --- /dev/null +++ b/packages/frontend/src/components/SideBarVertical/SideBarVertical.tsx @@ -0,0 +1,36 @@ +import { useState } from 'react' +import { FaAngleLeft, FaAngleRight, FaHome, FaTools, FaDesktop, FaClipboardList } from 'react-icons/fa' +import SideBarIcon from './SideBarIcon/SideBarIcon' +import './SideBarVertical.css' + +const SideBarIcons = [ + { icon: , text: 'Dashboard' }, + { icon: , text: 'Devices' }, + { icon: , text: 'Terminals' }, + { + icon: , + text: 'Logs' + } +] + +export default function SideBarVertical() { + const [isExpanded, setIsExpanded] = useState(true) + return ( +
+
TACOS
+
+ {SideBarIcons.map((i) => ( + + ))} +
+
{ + setIsExpanded(!isExpanded) + }} + className='sidebar-v-expand-button' + > + {isExpanded ? : } +
+
+ ) +} diff --git a/packages/frontend/src/index.css b/packages/frontend/src/index.css index cc457eb..a967383 100644 --- a/packages/frontend/src/index.css +++ b/packages/frontend/src/index.css @@ -1,75 +1,49 @@ +@import url('https://fonts.googleapis.com/css2?family=Teko:wght@300..700&display=swap'); + @tailwind base; @tailwind components; @tailwind utilities; -:root { - font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; - color-scheme: light dark; - color: rgb(255 255 255 / 87%); - background-color: #242424; - font-synthesis: none; - text-rendering: optimizelegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} - -a:hover { - color: #535bf2; -} - body { - margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; -} - -h1 { - font-size: 3.2em; - line-height: 1.1; -} - -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} - -button:hover { - border-color: #646cff; -} - -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; -} - -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #fff; - } - - a:hover { - color: #747bff; - } - - button { - background-color: #f9f9f9; - } + background-color: rgb(24, 24, 27); +} + +.light { + --header-body: 220 220 220; + --header-text: 26 26 26; + --header-toggle: 0 0 0; + + --navbar-body: 245 245 245; + --navbar-text: 0 0 0; + --navbar-grad-light: 212 212 212; + --navbar-grad-dark: 232 232 232; + + --body: 238 239 240; + --card: 200 200 200; + --card-text-primary: 0 0 0; + --card-text-secondary: 0 0 0; + --card-edit: 220 220 220; + --card-delete: 239 68 68; + --card-arm: 123 204 22; + --card-disarm: 249 115 22; +} + +.dark { + --header-body: 24 24 27; + --header-text: 255 255 255; + --header-toggle: 0 0 0; + + --navbar-body: 9 9 11; + --navbar-text: 255 255 255; + --navbar-grad-light: 39 39 42; + --navbar-grad-dark: 24 24 27; + + --body: 15 16 17; + --card: 43 43 50; + --card-text-primary: 255 255 255; + --card-text-secondary: 255 255 255; + --card-edit: 112 112 116; + --card-delete: 239 68 68; + --card-arm: 123 204 22; + --card-disarm: 249 115 22; } diff --git a/packages/frontend/src/main.tsx b/packages/frontend/src/main.tsx index 383fd59..d92cabb 100644 --- a/packages/frontend/src/main.tsx +++ b/packages/frontend/src/main.tsx @@ -1,10 +1,13 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' +import { BrowserRouter } from 'react-router-dom' import App from './components/App/App' import './index.css' createRoot(document.getElementById('root')!).render( - + + + ) diff --git a/packages/frontend/tailwind.config.js b/packages/frontend/tailwind.config.js index 8f9f6b3..2a216d0 100644 --- a/packages/frontend/tailwind.config.js +++ b/packages/frontend/tailwind.config.js @@ -1,9 +1,32 @@ /** @type {import('tailwindcss').Config} */ +const colors = require('tailwindcss/colors') + export default { content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], - darkMode: ['class', '[data-theme="dark"]'], theme: { - extend: {} + extend: { + fontFamily: { + teko: ['Teko', 'sans-serif'] + }, + colors: { + 'header-body': 'rgba(var(--header-body))', + 'header-text': 'rgba(var(--header-text))', + + 'navbar-body': 'rgba(var(--navbar-body))', + 'navbar-text': 'rgba(var(--navbar-text))', + 'navbar-grad-light': 'rgba(var(--navbar-grad-light))', + 'navbar-grad-dark': 'rgba(var(--navbar-grad-dark))', + + body: 'rgba(var(--body))', + card: 'rgba(var(--card))', + 'card-text-primary': 'rgba(var(--card-text-primary))', + 'card-text-secondary': 'rgba(var(--card-text-secondary))', + 'card-edit': 'rgba(var(--card-edit))', + 'card-delete': 'rgba(var(--card-delete))', + 'card-arm': 'rgba(var(--card-arm))', + 'card-disarm': 'rgba(var(--card-disarm))', + } + } }, plugins: [] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0e22552..b05e795 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -148,6 +148,12 @@ importers: react-dom: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) + react-icons: + specifier: ^5.3.0 + version: 5.3.0(react@18.3.1) + react-router-dom: + specifier: ^6.27.0 + version: 6.27.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) stylelint: specifier: ^16.9.0 version: 16.9.0(typescript@5.5.3) @@ -1431,6 +1437,10 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@remix-run/router@1.20.0': + resolution: {integrity: sha512-mUnk8rPJBI9loFDZ+YzPGdeniYK+FTmRD1TMCz7ev2SNIozyKKpnGgsxO34u6Z4z/t0ITuu7voi/AshfsGsgFg==} + engines: {node: '>=14.0.0'} + '@rollup/pluginutils@5.1.2': resolution: {integrity: sha512-/FIdS3PyZ39bjZlwqFnWqCOVnW7o963LtKMwQOD0NhQqw22gSr2YY1afu3FxRip4ZCZNsD5jq6Aaz6QV3D/Njw==} engines: {node: '>=14.0.0'} @@ -4665,6 +4675,11 @@ packages: react: ^0.14.8 || ^15.0.1 || ^16.0.0 || ^17.0.1 || ^18.0.0 react-dom: ^0.14.8 || ^15.0.1 || ^16.0.0 || ^17.0.1 || ^18.0.0 + react-icons@5.3.0: + resolution: {integrity: sha512-DnUk8aFbTyQPSkCfF8dbX6kQjXA9DktMeJqfjrg6cK9vwQVMxmcA3BfP4QoiztVmEHtwlTgLFsPuH2NskKT6eg==} + peerDependencies: + react: '*' + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -4677,6 +4692,19 @@ packages: react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + react-router-dom@6.27.0: + resolution: {integrity: sha512-+bvtFWMC0DgAFrfKXKG9Fc+BcXWRUO1aJIihbB79xaeq0v5UzfvnM5houGUm1Y461WVRcgAQ+Clh5rdb1eCx4g==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + + react-router@6.27.0: + resolution: {integrity: sha512-YA+HGZXz4jaAkVoYBE98VQl+nVzI+cVI2Oj/06F5ZM+0u3TgedN9Y9kmMRo2mnkSK2nCpNQn0DVob4HCsY/WLw==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: '>=16.8' + react@18.3.1: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} @@ -6950,6 +6978,8 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@remix-run/router@1.20.0': {} + '@rollup/pluginutils@5.1.2(rollup@4.22.4)': dependencies: '@types/estree': 1.0.6 @@ -10586,6 +10616,10 @@ snapshots: react-dom: 18.3.1(react@18.3.1) react-is: 18.1.0 + react-icons@5.3.0(react@18.3.1): + dependencies: + react: 18.3.1 + react-is@16.13.1: {} react-is@17.0.2: {} @@ -10594,6 +10628,18 @@ snapshots: react-is@18.3.1: {} + react-router-dom@6.27.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@remix-run/router': 1.20.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-router: 6.27.0(react@18.3.1) + + react-router@6.27.0(react@18.3.1): + dependencies: + '@remix-run/router': 1.20.0 + react: 18.3.1 + react@18.3.1: dependencies: loose-envify: 1.4.0