diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ef8b103..282bb11 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -98,8 +98,8 @@ jobs: push: false tags: web-ui:test - cypress-dashboard: - name: Cypress - Dashboard + cypress-home: + name: Cypress - Home runs-on: ubuntu-latest steps: @@ -118,7 +118,7 @@ jobs: run: npx wait-on http://localhost:5173 - name: Run Cypress - run: npx cypress run --spec "cypress/e2e/dashboard/**/*.cy.ts" + run: npx cypress run --spec "cypress/e2e/home/**/*.cy.ts" cypress-events: name: Cypress - Events @@ -140,29 +140,7 @@ jobs: run: npx wait-on http://localhost:5173 - name: Run Cypress - run: npx cypress run --spec "cypress/e2e/dashboard/**/*.cy.ts" - - cypress-insights: - name: Cypress - Insights - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - name: Install dependencies - run: npm ci - - - name: Build Docker image - run: docker build -t web-ui:test . - - - name: Start container - run: docker run -d --name webui -p 5173:80 web-ui:test - - - name: Wait for app - run: npx wait-on http://localhost:5173 - - - name: Run Cypress - run: npx cypress run --spec "cypress/e2e/dashboard/**/*.cy.ts" + run: npx cypress run --spec "cypress/e2e/events/**/*.cy.ts" cypress-organizations: name: Cypress - Organizations @@ -184,10 +162,10 @@ jobs: run: npx wait-on http://localhost:5173 - name: Run Cypress - run: npx cypress run --spec "cypress/e2e/dashboard/**/*.cy.ts" + run: npx cypress run --spec "cypress/e2e/organizations/**/*.cy.ts" - cypress-user: - name: Cypress - User + cypress-profile: + name: Cypress - Profile runs-on: ubuntu-latest steps: @@ -206,4 +184,4 @@ jobs: run: npx wait-on http://localhost:5173 - name: Run Cypress - run: npx cypress run --spec "cypress/e2e/dashboard/**/*.cy.ts" + run: npx cypress run --spec "cypress/e2e/profile/**/*.cy.ts" diff --git a/cypress/e2e/events/events.cy.ts b/cypress/e2e/events/events.cy.ts index 336c993..46e8baf 100644 --- a/cypress/e2e/events/events.cy.ts +++ b/cypress/e2e/events/events.cy.ts @@ -2,6 +2,6 @@ describe('Event Page', () => { it('displays the correct main title', () => { cy.visit('localhost:5173/events'); - cy.get('h1').should('contain', 'My Events'); + cy.get('h1').should('contain', 'Events'); }); }); diff --git a/cypress/e2e/dashboard/dashboard.cy.ts b/cypress/e2e/home/home.cy.ts similarity index 52% rename from cypress/e2e/dashboard/dashboard.cy.ts rename to cypress/e2e/home/home.cy.ts index b8957e8..1a9848f 100644 --- a/cypress/e2e/dashboard/dashboard.cy.ts +++ b/cypress/e2e/home/home.cy.ts @@ -1,7 +1,7 @@ -describe('Dashboard Page', () => { +describe('Home Page', () => { it('displays the correct main title', () => { cy.visit('localhost:5173'); - cy.get('h1').should('contain', 'My Dashboard'); + cy.get('h1').should('contain', 'Home'); }); }); diff --git a/cypress/e2e/insights/insights.cy.ts b/cypress/e2e/insights/insights.cy.ts deleted file mode 100644 index 0a989b7..0000000 --- a/cypress/e2e/insights/insights.cy.ts +++ /dev/null @@ -1,7 +0,0 @@ -describe('Insights Page', () => { - it('displays the correct main title', () => { - cy.visit('localhost:5173/insights'); - - cy.get('h1').should('contain', 'Insights'); - }); -}); diff --git a/cypress/e2e/organizations/organizations.cy.ts b/cypress/e2e/organizations/organizations.cy.ts index 874a36a..6597df7 100644 --- a/cypress/e2e/organizations/organizations.cy.ts +++ b/cypress/e2e/organizations/organizations.cy.ts @@ -2,6 +2,6 @@ describe('Organizations Page', () => { it('displays the correct main title', () => { cy.visit('localhost:5173/organizations'); - cy.get('h1').should('contain', 'My Organizations'); + cy.get('h1').should('contain', 'Organizations'); }); }); diff --git a/cypress/e2e/profile/profile.cy.ts b/cypress/e2e/profile/profile.cy.ts index e0adb53..b10553c 100644 --- a/cypress/e2e/profile/profile.cy.ts +++ b/cypress/e2e/profile/profile.cy.ts @@ -2,6 +2,6 @@ describe('Profile Page', () => { it('displays the correct main title', () => { cy.visit('localhost:5173/profile'); - cy.get('h1').should('contain', 'My Profile'); + cy.get('h1').should('contain', 'Profile'); }); }); diff --git a/src/App.tsx b/src/App.tsx index 2bbf2a4..9bbc7ad 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,7 +3,6 @@ import { ThemeProvider } from './contexts/ThemeContext'; import AppLayout from './components/AppLayout.tsx'; import Home from './pages/Home.tsx'; import Events from './pages/Events.tsx'; -import Insights from './pages/Insights.tsx'; import Organizations from './pages/Organizations.tsx'; import Testing from './pages/Testing.tsx'; // temp page import Profile from './pages/Profile.tsx'; @@ -17,12 +16,11 @@ function App() { }> + } /> } /> } /> - } /> } /> } /> - } /> diff --git a/src/components/AppLayout.tsx b/src/components/AppLayout.tsx index 5e57c80..d665fa8 100644 --- a/src/components/AppLayout.tsx +++ b/src/components/AppLayout.tsx @@ -1,24 +1,15 @@ /* Layout that each page will follow */ import '../css/app-layout.css'; import { Outlet } from 'react-router-dom'; -import { useCollapseSidebar } from '../hooks/useCollapseSidebar'; -import Sidebar from './sidebar/Sidebar.tsx'; +import Navbar from './navbar/Navbar.tsx'; export default function AppLayout() { - const [collapsed, setCollapsed] = useCollapseSidebar('sidebar-collapsed', false); - - const toggleSidebar = () => { - setCollapsed((prev) => !prev); - }; - return (
-
- -
- -
+ +
+
); diff --git a/src/components/navbar/Navbar.tsx b/src/components/navbar/Navbar.tsx new file mode 100644 index 0000000..1a28a92 --- /dev/null +++ b/src/components/navbar/Navbar.tsx @@ -0,0 +1,62 @@ +import { useLayoutEffect, useRef, useState, useEffect } from 'react'; +import { useLocation } from 'react-router-dom'; +import NavbarLink from './NavbarLink'; +import '../../css/navbar.css'; + +type IndicatorState = { left: number; width: number }; + +export default function Navbar() { + const navbarRef = useRef(null); + const location = useLocation(); + const [indicator, setIndicator] = useState({ left: 0, width: 0 }); + + const computeIndicator = () => { + const navbarEl = navbarRef.current; + if (!navbarEl) return; + + const activeEl = navbarEl.querySelector('.navlink.active-navlink') as HTMLElement | null; + if (!activeEl) return; + + const navRect = navbarEl.getBoundingClientRect(); + const activeRect = activeEl.getBoundingClientRect(); + + const next = { + left: activeRect.left - navRect.left, + width: activeRect.width, + }; + + // Avoid useless state updates + setIndicator((prev) => (prev.left === next.left && prev.width === next.width ? prev : next)); + }; + + // Run when the route changes (active link changes). + // useLayoutEffect helps avoid a visible "jump" on first render. + useLayoutEffect(() => { + // Let the DOM apply the active class first, then measure. + requestAnimationFrame(computeIndicator); + }, [location.pathname]); + + // Also update on resize + useEffect(() => { + const onResize = () => computeIndicator(); + window.addEventListener('resize', onResize); + return () => window.removeEventListener('resize', onResize); + }, []); + + return ( +
+
+ + + + + +
+ ); +} diff --git a/src/components/navbar/NavbarLink.tsx b/src/components/navbar/NavbarLink.tsx new file mode 100644 index 0000000..4ace121 --- /dev/null +++ b/src/components/navbar/NavbarLink.tsx @@ -0,0 +1,14 @@ +import { NavLink } from 'react-router-dom'; + +type NavbarLinkProps = { + label: string; + nav: string; +}; + +export default function NavbarLink({ label, nav }: NavbarLinkProps) { + return ( + `navlink ${isActive ? 'active-navlink' : ''}`}> + {label} + + ); +} diff --git a/src/components/sidebar/Sidebar.tsx b/src/components/sidebar/Sidebar.tsx deleted file mode 100644 index 47e3581..0000000 --- a/src/components/sidebar/Sidebar.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import '../../css/sidebar.css'; -import '../../css/login.css'; -import SidebarLink from './SidebarLink.tsx'; -import SidebarProfile from './SidebarProfile.tsx'; -import SidebarWorkspace from './SidebarWorkspace.tsx'; - -import { - AdjustmentsHorizontalIcon, - Bars3Icon, - HomeIcon, - StarIcon, - CalendarDaysIcon, - UserGroupIcon, - UserCircleIcon, -} from '@heroicons/react/24/solid'; - -type SidebarProps = { - collapsed: boolean; - onToggle: () => void; -}; - -export default function Sidebar({ collapsed, onToggle }: SidebarProps) { - return ( - <> -
-
-
- -
-
- } nav="/" /> - } nav="/events" /> - } nav="/insights" /> - } nav="/organizations" /> - } nav="/testing" /> -
- - } name="Kevin Smith" email="smithk@rpi.edu" /> - -
- - ); -} diff --git a/src/components/sidebar/SidebarGroup.tsx b/src/components/sidebar/SidebarGroup.tsx deleted file mode 100644 index 9821c9f..0000000 --- a/src/components/sidebar/SidebarGroup.tsx +++ /dev/null @@ -1,43 +0,0 @@ -/* - -*** NOT IN USE *** -Groups links allowing for a nested structure -on the Sidebar. - -*/ - -import { ChevronRightIcon, ChevronDownIcon } from '@heroicons/react/24/solid'; -import { useState, useEffect } from 'react'; - -type SidebarGroupProps = { - label: string; - icon: React.ReactNode; - children: React.ReactNode; - collapsed: boolean; -}; - -export default function SidebarGroup({ label, icon, children, collapsed }: SidebarGroupProps) { - const [open, setOpen] = useState(true); - const Icon = open ? ChevronDownIcon : ChevronRightIcon; - - // This logic assumes the user wants groups back open when sidebar is toggled. - useEffect(() => { - if (collapsed) { - // eslint-disable-next-line react-hooks/set-state-in-effect - setOpen(false); - } else { - setOpen(true); - } - }, [collapsed]); - - return ( -
-
setOpen((prev) => !prev)}> - {icon} - {label} - -
- {open &&
{children}
} -
- ); -} diff --git a/src/components/sidebar/SidebarLink.tsx b/src/components/sidebar/SidebarLink.tsx deleted file mode 100644 index a2c7981..0000000 --- a/src/components/sidebar/SidebarLink.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { NavLink } from 'react-router-dom'; - -type SidebarLinkProps = { - label: string; - icon: React.ReactNode; - nav: string; -}; - -export default function SidebarLink({ label, icon, nav }: SidebarLinkProps) { - return ( - `sidebar-link ${isActive ? 'active-link' : ''}`}> - {icon} - {label} - - ); -} diff --git a/src/components/sidebar/SidebarProfile.tsx b/src/components/sidebar/SidebarProfile.tsx deleted file mode 100644 index 5757d79..0000000 --- a/src/components/sidebar/SidebarProfile.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { useNavigate } from 'react-router-dom'; - -type SidebarLinkProps = { - icon: React.ReactNode; - name: string; - email: string; -}; - -export default function SidebarProfile({ icon, name, email }: SidebarLinkProps) { - const navigate = useNavigate(); - return ( -
navigate('/profile')}> - {icon} -
- {name} - {email} -
-
- ); -} diff --git a/src/components/sidebar/SidebarWorkspace.tsx b/src/components/sidebar/SidebarWorkspace.tsx deleted file mode 100644 index 5fbddce..0000000 --- a/src/components/sidebar/SidebarWorkspace.tsx +++ /dev/null @@ -1,31 +0,0 @@ -/* - -Currently only used as a standard div, but supports -a toggle for viewing links. - -*/ - -import React from 'react'; -import { useState } from 'react'; - -import { ChevronRightIcon, ChevronDownIcon } from '@heroicons/react/24/solid'; - -interface SidebarWorkspaceProps { - workspace?: string; - children: React.ReactNode; -} - -export default function SidebarWorkspace({ workspace, children }: SidebarWorkspaceProps) { - const [open, setOpen] = useState(true); - const Icon = open ? ChevronDownIcon : ChevronRightIcon; - - return ( -
-
setOpen((prev) => !prev)} className={`${workspace ? 'workspace-header' : 'hide'}`}> - - {workspace} -
- {open && children} -
- ); -} diff --git a/src/css/app-layout.css b/src/css/app-layout.css index 51c6cb1..2fc949e 100644 --- a/src/css/app-layout.css +++ b/src/css/app-layout.css @@ -3,13 +3,7 @@ width: 100vw; display: flex; flex-direction: column; -} - -.app-sidebar-content-container { - height: 100%; - display: flex; - flex-direction: row; - flex: 1; + align-items: center; } .app-content { diff --git a/src/css/navbar.css b/src/css/navbar.css new file mode 100644 index 0000000..db244f5 --- /dev/null +++ b/src/css/navbar.css @@ -0,0 +1,39 @@ +.navbar { + position: relative; + display: inline-flex; + gap: 0.5rem; + background: #e6e6e6; + border-radius: 999px; + width: fit-content; + margin: 40px auto 0; +} + +.navlink { + position: relative; + z-index: 2; + padding: 0.5rem 1rem; + border-radius: 999px; + text-decoration: none; + color: #444; +} + +.navlink:hover { + background: rgb(0 0 0 / 5%); +} + +.active-navlink { + color: #111; +} + +.nav-indicator { + position: absolute; + z-index: 1; + left: 0; + height: 100%; + border-radius: 999px; + background: #d0d0d0; + transition: + transform 0.28s cubic-bezier(0.4, 0, 0.2, 1), + width 0.28s cubic-bezier(0.4, 0, 0.2, 1); + will-change: transform, width; +} diff --git a/src/css/sidebar.css b/src/css/sidebar.css deleted file mode 100644 index d6d9656..0000000 --- a/src/css/sidebar.css +++ /dev/null @@ -1,205 +0,0 @@ -/* Styles for Sidebar.tsx and SidebarButton.tsx */ - -/* Main Sidebar */ -.sidebar { - display: flex; - flex-direction: column; - width: 16rem; - background: var(--sidebar-bg); - padding: 0.5rem; - transition: width 500ms ease; - box-shadow: 3px 3px 6px rgb(0 0 0 / 15%); -} - -.collapsed { - width: 3.45rem; -} - -.sidebar-header { - display: flex; - flex-direction: row; - width: 100%; - padding: 0.75rem 0; - justify-content: center; -} - -.toggle-spacer { - width: 13.75rem; - transition: width 500ms ease; -} - -.sidebar.collapsed .toggle-spacer { - width: 0; -} - -.collapse-toggle { - background: none; - border: none; - color: var(--standard-text); - cursor: pointer; -} - -.collapse-toggle:focus-visible { - outline: none; -} - -.collapse-toggle svg { - height: 1.25rem; - width: 1.25rem; -} - -.sidebar-title { - flex-grow: 1; - font-size: 1.5rem; -} - -.sidebar.collapsed .sidebar-title { - display: none; -} - -/* Sidebar Workspace */ - -.sidebar-workspace { - display: flex; - flex-direction: column; - gap: 0.4rem; -} - -.sidebar-workspace:not(:last-child) { - margin-bottom: 1rem; -} - -.sidebar-workspace:last-child { - margin-top: auto; -} - -.workspace-icon { - height: 0.85rem; - width: 0.85rem; - margin-left: 0.2rem; - flex-shrink: 0; -} - -.workspace-text { - font-size: 0.85rem; -} - -/* Sidebar Links */ - -.sidebar-link, -.workspace-header { - display: flex; - flex-direction: row; - align-items: center; - gap: 0.7rem; - justify-content: flex-start; - width: 100%; - border-radius: 0.5rem; - padding: 0.4rem 0.6rem; - cursor: pointer; - text-decoration: none; - color: var(--standard-text); - overflow: hidden; - white-space: nowrap; - transition: none; -} - -.sidebar-link:hover { - background: var(--sidebar-link-hover-bg); -} - -.active-link { - background: var(--sidebar-active-link-bg) !important; -} - -.sidebar-link-label { - font-size: 1rem; -} - -.sidebar-link-icon { - width: 1.25rem; - height: 1.25rem; - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; -} - -.sidebar-link-icon svg { - width: 100%; - height: 100%; -} - -/* Sidebar Groups */ - -.sidebar-group-header { - display: flex; - flex-direction: row; - align-items: center; - gap: 0.7rem; - justify-content: flex-start; - width: 100%; - border-radius: 0.5rem; - padding: 0.4rem 0.6rem; - cursor: pointer; - text-decoration: none; - color: var(--standard-text); - overflow: hidden; - white-space: nowrap; - transition: none; -} - -.group-collapse-icon { - margin-left: auto; - height: 1rem; - width: 1rem; -} - -.sidebar-group-children { - display: flex; - flex-direction: column; - border-left: 1px solid var(--standard-text); - margin-left: 1.225rem; - padding-left: 1rem; -} - -/* Sidebar Profile */ - -.sidebar-profile-icon { - width: 2rem; - height: 2rem; - flex-shrink: 0; - transition: - width 300ms ease, - height 300ms ease; -} - -.sidebar.collapsed .sidebar-profile-icon { - width: 1.25rem; - height: 1.25rem; -} - -.sidebar-profile-content { - display: flex; - flex-direction: column; -} - -.sidebar-profile-email { - font-size: 0.75rem; -} - -/* Transitions */ - -.sidebar-label-transition { - max-width: 200px; - overflow: hidden; - white-space: nowrap; - transition: - max-width 300ms ease, - opacity 150ms ease 50ms; -} - -.sidebar.collapsed .sidebar-label-transition { - max-width: 0; - opacity: 0; -} diff --git a/src/hooks/.gitkeep b/src/hooks/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/hooks/useCollapseSidebar.ts b/src/hooks/useCollapseSidebar.ts deleted file mode 100644 index ae0f7bb..0000000 --- a/src/hooks/useCollapseSidebar.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { useEffect, useState } from 'react'; - -export function useCollapseSidebar( - key: string, - defaultValue: boolean -): [boolean, React.Dispatch>] { - const [value, setValue] = useState(() => { - if (typeof window === 'undefined') { - return defaultValue; - } - - const stored = localStorage.getItem(key); - return stored !== null ? stored === 'true' : defaultValue; - }); - - useEffect(() => { - if (typeof window !== 'undefined') { - localStorage.setItem(key, String(value)); - } - }, [key, value]); - - return [value, setValue]; -} diff --git a/src/pages/Events.tsx b/src/pages/Events.tsx index aedddef..d12f79d 100644 --- a/src/pages/Events.tsx +++ b/src/pages/Events.tsx @@ -2,7 +2,7 @@ export default function Events() { return ( <>
-

My Events

+

Events

); diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 17565c7..7758cfa 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -2,7 +2,7 @@ export default function Home() { return ( <>
-

My Dashboard

+

Home

); diff --git a/src/pages/Insights.tsx b/src/pages/Insights.tsx deleted file mode 100644 index 101080e..0000000 --- a/src/pages/Insights.tsx +++ /dev/null @@ -1,9 +0,0 @@ -export default function Insights() { - return ( - <> -
-

Insights

-
- - ); -} diff --git a/src/pages/Organizations.tsx b/src/pages/Organizations.tsx index 905f43a..229f108 100644 --- a/src/pages/Organizations.tsx +++ b/src/pages/Organizations.tsx @@ -2,7 +2,7 @@ export default function Organizations() { return ( <>
-

My Organizations

+

Organizations

); diff --git a/src/pages/Profile.tsx b/src/pages/Profile.tsx index 4730ccc..b089692 100644 --- a/src/pages/Profile.tsx +++ b/src/pages/Profile.tsx @@ -2,7 +2,7 @@ export default function Profile() { return ( <>
-

My Profile

+

Profile

);