diff --git a/react-ui/src/components/devices/view/device.view.tsx b/react-ui/src/components/devices/view/device.view.tsx index 518c12af44e2a92da48d5d749c6bb0302ad14dac..4ef0073278b1685222ea05f5a5d3f183aac82a32 100755 --- a/react-ui/src/components/devices/view/device.view.tsx +++ b/react-ui/src/components/devices/view/device.view.tsx @@ -16,7 +16,7 @@ const DeviceView = () => { <Container fluid> <Row> <Col lg={5} sm={12}> - <Container className='bg-white rounded c-box'> + <Container className='bg-white c-box'> <Row> <Col sm={12} className='mt-4'><h3 className='text-black-50'>{t('device.title')}</h3></Col> </Row> @@ -40,7 +40,7 @@ const DeviceView = () => { </Container> </Col> <Col xs={12} lg={7} className='mt-5 mt-lg-0'> - <Container className='bg-white rounded c-box'> + <Container className='bg-white c-box'> <Row> <Col xs={12} className='mt-4'> <Nav className='justify-content-around'> diff --git a/react-ui/src/routes.tsx b/react-ui/src/routes.tsx index 532d03fbf0d02924b8fb8e58b9aefed1ae2ab964..a476feaaedfd8922a1841bd6f877792416856f13 100755 --- a/react-ui/src/routes.tsx +++ b/react-ui/src/routes.tsx @@ -1,5 +1,6 @@ import { BasicLayout } from "@layout/basic.layout"; import { ProtectedLayout } from "@layout/protected.layout/protected.layout"; +import DelayedRender, { SplashScreen } from "@utils/loading-fallback"; import { lazy, Suspense } from 'react'; import { createBrowserRouter, createRoutesFromElements, Navigate, Route } from "react-router-dom"; @@ -10,17 +11,16 @@ export const LOGIN_URL = '/login'; const DeviceView = lazy(() => import('./components/devices/view/device.view')); const LoginLayout = lazy(() => import('./components/login/layouts/login.layout')); -// Loading fallback component -const LoadingFallback = () => <div>Loading...</div>; - export const router = createBrowserRouter( createRoutesFromElements( <Route element={<BasicLayout />}> <Route path={LOGIN_URL} element={ - <Suspense fallback={<LoadingFallback />}> - <LoginLayout /> + <Suspense fallback={null}> + <DelayedRender> + <LoginLayout /> + </DelayedRender> </Suspense> } /> @@ -28,9 +28,16 @@ export const router = createBrowserRouter( <Route path={DEVICE_URL} element={ - <Suspense fallback={<LoadingFallback />}> - <DeviceView /> - </Suspense> + <DelayedRender + loading={{ + minimumLoadingTime: 1000, + component: SplashScreen + }} + > + <Suspense fallback={null}> + <DeviceView /> + </Suspense> + </DelayedRender> } /> <Route @@ -38,6 +45,6 @@ export const router = createBrowserRouter( element={<Navigate to={DEVICE_URL} replace={true} />} /> </Route> - </Route> + </Route > ) ); \ No newline at end of file diff --git a/react-ui/src/shared/layouts/protected.layout/protected.layout.scss b/react-ui/src/shared/layouts/protected.layout/protected.layout.scss index 07b38d5a73d1102bebf4e4b0f82e8cc2bc10a799..713e634814717755dc2f0fdca1343bc1e3c2844f 100755 --- a/react-ui/src/shared/layouts/protected.layout/protected.layout.scss +++ b/react-ui/src/shared/layouts/protected.layout/protected.layout.scss @@ -1,7 +1,5 @@ @import "/src/shared/style/colors.scss"; -$sidebar-width: 4.5em; - .head-links { text-decoration: none; color: map-get($theme-colors, dark); @@ -19,11 +17,46 @@ $sidebar-width: 4.5em; } } -.sidebar { - width: $sidebar-width; - height: 100vh; -} +// Add these styles to your protected.layout.scss +nav { + border-radius: 0 0 $border-radius $border-radius; + box-shadow: + 0px 4px 8px mix(map-get($theme-colors, "primary"), map-get($theme-colors, "dark"), 35%), + 0px 2px 4px mix(map-get($theme-colors, "primary"), map-get($theme-colors, "dark"), 20%); + + .head-links { + text-decoration: none; + color: map-get($theme-colors, "dark"); + padding: 8px 16px; + margin: 0 4px; + border-radius: 12px; + transition: all 0.2s ease; -.main-content { - margin-left: $sidebar-width; + &:hover { + background-color: map-get($theme-colors, "bg-primary"); + } + + &.active { + color: map-get($theme-colors, "primary"); + background-color: map-get($theme-colors, "primary::hover"); + } + } + + .dropdown-menu { + border-radius: $border-radius; + box-shadow: + 0px 4px 8px mix(map-get($theme-colors, "primary"), map-get($theme-colors, "dark"), 35%), + 0px 2px 4px mix(map-get($theme-colors, "primary"), map-get($theme-colors, "dark"), 20%); + border: none; + padding: 8px; + + .dropdown-item { + border-radius: 8px; + padding: 8px 16px; + + &:hover { + background-color: map-get($theme-colors, "bg-primary"); + } + } + } } diff --git a/react-ui/src/shared/layouts/protected.layout/protected.layout.tsx b/react-ui/src/shared/layouts/protected.layout/protected.layout.tsx index 0f0e8161ebb8c105f0796736d47b01ef92c2fa5d..b1d70b823b2cf6dce87eefedebb9e5902cbf34e9 100755 --- a/react-ui/src/shared/layouts/protected.layout/protected.layout.tsx +++ b/react-ui/src/shared/layouts/protected.layout/protected.layout.tsx @@ -69,14 +69,6 @@ export const ProtectedLayout = () => { } ); - const VerticalSidebar = () => { - return ( - <div className="d-flex fixed-top flex-column flex-shrink-0 bg-white sidebar justify-content-end border-end border-dark py-3 z-2"> - <FontAwesomeIcon className="clickable icon" icon={faRightFromBracket} onClick={logout} size="2x" /> - </div> - ) - } - const HorizontalNavbar = () => { return ( <nav className="bg-white border-bottom border-dark py-2 d-flex align-items-center z-3 position-relative"> @@ -87,15 +79,18 @@ export const ProtectedLayout = () => { <Dropdown className="ms-auto px-3"> <Dropdown.Toggle as={UserIconToggle}> - <FontAwesomeIcon icon={faCircleUser} className="icon clickable" /> + <FontAwesomeIcon icon={faCircleUser} className="clickable" size="2x" /> </Dropdown.Toggle> <Dropdown.Menu as={UserIconMenu}> <Dropdown.Item eventKey="1">{user?.name}</Dropdown.Item> <hr /> - <Dropdown.Item eventKey="1"> + <Dropdown.Item eventKey="2"> <Link className="text-decoration-none text-reset" to="/">{t('protected.link.settings')}</Link> </Dropdown.Item> + <Dropdown.Item eventKey="3" onClick={logout}> + <Link className="text-decoration-none text-reset" to="/"><FontAwesomeIcon className="clickable" icon={faRightFromBracket} />{t('protected.link.settings')}</Link> + </Dropdown.Item> </Dropdown.Menu> </Dropdown> </nav> @@ -106,10 +101,7 @@ export const ProtectedLayout = () => { <div> <MenuProvider> {HorizontalNavbar()} - {VerticalSidebar()} - <div className='main-content'> - <Outlet /> - </div> + <Outlet /> </MenuProvider> </div> ) diff --git a/react-ui/src/shared/style/box.scss b/react-ui/src/shared/style/box.scss index bd75fb00a6c1574584c85afafe6a91d8b858deb4..934861a2bc12db2aee1490f0cfa40b342919d59d 100755 --- a/react-ui/src/shared/style/box.scss +++ b/react-ui/src/shared/style/box.scss @@ -1,26 +1,30 @@ -@import './colors.scss'; +@import "./colors.scss"; $box-padding: 10px; $border-radius: 20px; - +$border-width: 2px; .c-box { padding: $box-padding; background-color: white; - box-shadow: 0px 4px 4px rgba(0,0,0, .35); + position: relative; border-radius: $border-radius; + + background: + linear-gradient(white, white) padding-box, + linear-gradient( + 180deg, + rgba(map-get($theme-colors, "primary"), 0.3) 0%, + rgba(map-get($theme-colors, "primary"), 0.1) 100% + ) + border-box; + border: $border-width solid transparent; + + box-shadow: 0px 1px 2px rgba(map-get($theme-colors, "dark"), 0.12); } .abstract-box { padding: 16px $box-padding; - font-size: .90em; + font-size: 0.9em; border-radius: calc($border-radius / 2); } - - -// @each $color, $value in $theme-colors { -// .#{$color}-box { -// @extend .abstract-box; -// background-color: $value !important; -// } -// } diff --git a/react-ui/src/shared/style/colors.scss b/react-ui/src/shared/style/colors.scss index 749af9e8eb8e86b0169c7f2a652dc0bada992181..d91ffb44e5fac78ba0bcd1acf5a9f49f3baf7b33 100755 --- a/react-ui/src/shared/style/colors.scss +++ b/react-ui/src/shared/style/colors.scss @@ -1,11 +1,11 @@ $theme-colors: ( - 'primary': #b350e0, - 'primary::hover': #ddaff3af, - 'bg-primary': #E1E1E1, - 'danger': #ffdcdc, - 'warning': #dbd116, - 'dark': #595959, - 'black': #000000, + "primary": #b350e0, + "primary::hover": #ddaff3af, + "bg-primary": #ededed, + "danger": #ffdcdc, + "warning": #dbd116, + "dark": #595959, + "black": #000000 ); - -@import '/node_modules/bootstrap/scss/bootstrap'; + +@import "/node_modules/bootstrap/scss/bootstrap"; diff --git a/react-ui/src/shared/style/utils.scss b/react-ui/src/shared/style/utils.scss index d8be654f7e67fc11d9626c2a52f5744af0dfbb79..d6f34d301416ac7aeb82aa740cd150e95efbd6c5 100755 --- a/react-ui/src/shared/style/utils.scss +++ b/react-ui/src/shared/style/utils.scss @@ -7,7 +7,3 @@ cursor: pointer; } } - -.icon { - font-size: 1.75em; -} diff --git a/react-ui/src/shared/utils/loading-fallback.tsx b/react-ui/src/shared/utils/loading-fallback.tsx new file mode 100644 index 0000000000000000000000000000000000000000..d1c6daa4aa369ca157dbae653c6dd29f6f99d99a --- /dev/null +++ b/react-ui/src/shared/utils/loading-fallback.tsx @@ -0,0 +1,117 @@ +import React, { useEffect, useState } from 'react'; +import { Col, Container, Row } from 'react-bootstrap'; +import logo from '/public/logo.png'; + +interface DelayedRenderProps { + children: React.ReactNode; + loading?: { + minimumLoadingTime: number; + component: () => JSX.Element + } +} + +export const SplashScreen = () => { + const [dots, setDots] = useState(''); + + useEffect(() => { + const dotsInterval = setInterval(() => { + setDots(prev => prev.length >= 3 ? '' : prev + '.'); + }, 500); + + return () => clearInterval(dotsInterval); + }, []); + + return ( + <div className="splash-screen-overlay"> + <Container fluid className="h-100 d-flex align-items-center justify-content-center bg-bg-primary"> + <Row> + <Col className="text-center"> + <div className="loading-bounce mb-4"> + <img + src={logo} + alt="Logo" + className="img-fluid" + style={{ width: '120px', height: '120px', objectFit: 'contain' }} + /> + </div> + <div className="loading-text"> + <span className="h4 text-secondary">Loading</span> + <span className="h4 text-secondary dots-width">{dots}</span> + </div> + </Col> + </Row> + </Container> + + <style> + {` + .splash-screen-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: #f8f9fa; + z-index: 0; + display: flex; + align-items: center; + justify-content: center; + } + + .loading-bounce { + animation: bounce 1s infinite; + } + + @keyframes bounce { + 0%, 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-20px); + } + } + + .loading-text { + display: flex; + justify-content: center; + align-items: center; + } + + .dots-width { + min-width: 24px; + text-align: left; + margin-left: 2px; + } + `} + </style> + </div> + ); +}; + +export const DelayedRender: React.FC<DelayedRenderProps> = ({ + children, + loading +}) => { + const [shouldRender, setShouldRender] = useState(false); + + useEffect(() => { + if (!loading) { + setShouldRender(true); + return; + } + + const timer = setTimeout(() => { + setShouldRender(true); + }, loading.minimumLoadingTime); + + return () => clearTimeout(timer); + }, [loading]); + + if (!shouldRender && loading) { + const LoadingComponent = loading.component; + return <LoadingComponent />; + } + + return <>{children}</>; +}; + +export default DelayedRender; \ No newline at end of file