diff --git a/apps/web/package.json b/apps/web/package.json index f84c0e5c..a2ef1782 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -36,6 +36,7 @@ "react": "^19.1.0", "react-aria-components": "^1.8.0", "react-dom": "^19.1.0", + "react-stately": "^3.39.0", "tailwind-merge": "^3.3.0", "tailwind-variants": "^1.0.0", "tailwindcss": "^4.1.5", @@ -55,6 +56,7 @@ "@storybook/react": "^8.6.12", "@storybook/react-vite": "^8.6.12", "@storybook/test": "^8.6.12", + "@storybook/theming": "^8.6.14", "@svgr/core": "^8.1.0", "@svgr/plugin-jsx": "^8.1.0", "@tanstack/router-plugin": "^1.120.2", diff --git a/apps/web/pnpm-lock.yaml b/apps/web/pnpm-lock.yaml index ccc42f3f..2c17d3d5 100644 --- a/apps/web/pnpm-lock.yaml +++ b/apps/web/pnpm-lock.yaml @@ -38,6 +38,9 @@ importers: react-dom: specifier: ^19.1.0 version: 19.1.0(react@19.1.0) + react-stately: + specifier: ^3.39.0 + version: 3.39.0(react@19.1.0) tailwind-merge: specifier: ^3.3.0 version: 3.3.1 @@ -90,6 +93,9 @@ importers: '@storybook/test': specifier: ^8.6.12 version: 8.6.14(storybook@8.6.14(prettier@3.5.3)) + '@storybook/theming': + specifier: ^8.6.14 + version: 8.6.14(storybook@8.6.14(prettier@3.5.3)) '@svgr/core': specifier: ^8.1.0 version: 8.1.0(typescript@5.8.3) diff --git a/apps/web/src/components/AppShell/NavLink.tsx b/apps/web/src/components/AppShell/NavLink.tsx new file mode 100644 index 00000000..d005d48e --- /dev/null +++ b/apps/web/src/components/AppShell/NavLink.tsx @@ -0,0 +1,110 @@ +import type { PropsWithChildren, ReactNode } from "react"; +import TablerChevronRight from "~icons/tabler/chevron-right"; +import { tv } from "tailwind-variants"; +import { useToggleState } from "react-stately"; +import { cn } from "@/utils/cn"; +import { Button as RAC_Button } from "react-aria-components"; +import { Link } from "@tanstack/react-router"; + +const navLink = tv({ + base: "px-3 py-2.5 rounded-sm text-sm flex flex-row items-center justify-between w-full cursor-pointer transition-none select-none text-navlink-text", + variants: { + active: { + true: "bg-navlink-bg-active font-medium", + false: "bg-navlink-bg-inactive font-normal hover:scale-101", + }, + }, +}); + +interface NavLinkProps { + href?: string; + label: string; + description?: string; + leftSection?: ReactNode; + rightSection?: ReactNode; + active?: boolean; + initialExpanded?: boolean; +} + +const NavLink = ({ + href, + label, + description, + leftSection, + rightSection, + active = false, + initialExpanded = false, + children, +}: PropsWithChildren) => { + const isExpandable = !!children; + + const toggleState = useToggleState({ defaultSelected: initialExpanded }); + const toggle = toggleState.toggle; + + return ( +
+ {isExpandable ? ( + +
+ {leftSection && ( + + {leftSection} + + )} +
+ {label} + {description && ( + + {description} + + )} +
+
+ + + + +
+ ) : ( + +
+ {leftSection && ( + + {leftSection} + + )} +
+ {label} + {description && ( + + {description} + + )} +
+
+ {rightSection && ( + + {rightSection} + + )} + + )} + + {/* Expanded children links */} +
+ {isExpandable && toggleState.isSelected && ( +
{children}
+ )} +
+
+ ); +}; + +NavLink.displayName = "NavLink"; + +export { NavLink }; diff --git a/apps/web/src/components/AppShell/stories/NavLink.stories.tsx b/apps/web/src/components/AppShell/stories/NavLink.stories.tsx new file mode 100644 index 00000000..15cfe999 --- /dev/null +++ b/apps/web/src/components/AppShell/stories/NavLink.stories.tsx @@ -0,0 +1,84 @@ +/* This is bugged out brother */ +import type { Meta, StoryObj } from "@storybook/react"; +import TablerLayoutCollage from "~icons/tabler/layout-collage"; +import { NavLink } from "../NavLink"; +import { + createRootRouteWithContext, + createRouter, + RouterProvider, +} from "@tanstack/react-router"; +import type { ReactNode } from "react"; + +// Define a dummy root component that wraps Story +function withTanstackRouter(StoryComponent: () => ReactNode) { + const RootRoute = createRootRouteWithContext()({ + component: StoryComponent, + }); + + const router = createRouter({ + routeTree: RootRoute, + }); + + return ; +} + +const meta = { + component: NavLink, + title: "AppShell/NavLink", + tags: ["autodocs"], + decorators: [(Story) => withTanstackRouter(Story)], + argTypes: { + href: { + control: { type: "text" }, + description: "The URL the link points to", + defaultValue: "https://core.apidocumentation.com", + }, + label: { + control: { type: "text" }, + description: "The text displayed in the link", + defaultValue: "Dashboard", + }, + active: { + control: { type: "boolean" }, + description: "Indicates if the link is active", + defaultValue: false, + }, + }, + args: { + href: "https://core.apidocumentation.com", + label: "Dashboard", + active: true, + leftSection: ( + + ), + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + label: "Dashboard", + href: "https://core.apidocumentation.com", + }, +}; + +export const Expandable: Story = { + args: { + label: "Mother", + children: ( + <> + + + + ), + }, +}; + +export const WithDescription: Story = { + args: { + label: "Resume Review", + description: "67/67 (100%) resumes reviewed.", + }, +}; diff --git a/apps/web/src/index.css b/apps/web/src/index.css index b1bf9bbe..f2b2715d 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -18,6 +18,6 @@ body, } /* https://stackoverflow.com/questions/68400074/how-to-apply-transition-effects-when-switching-from-light-mode-to-dark-in-tailwi */ -body.enable-theme-transition * { - @apply transition-colors duration-500; +body.enable-theme-transition { + @apply transition-colors duration-300; } diff --git a/apps/web/src/routes/_protected/community.tsx b/apps/web/src/routes/_protected/community.tsx new file mode 100644 index 00000000..d9d0dbf0 --- /dev/null +++ b/apps/web/src/routes/_protected/community.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/_protected/community")({ + component: RouteComponent, +}); + +function RouteComponent() { + return
Hello this is the community tab!
; +} diff --git a/apps/web/src/routes/_protected/layout.tsx b/apps/web/src/routes/_protected/layout.tsx index efd0724e..5ae03849 100644 --- a/apps/web/src/routes/_protected/layout.tsx +++ b/apps/web/src/routes/_protected/layout.tsx @@ -1,4 +1,13 @@ -import { createFileRoute, Outlet, redirect } from "@tanstack/react-router"; +import { NavLink } from "@/components/AppShell/NavLink"; +import { + createFileRoute, + Outlet, + redirect, + useLocation, +} from "@tanstack/react-router"; +import TablerLayoutCollage from "~icons/tabler/layout-collage"; +import TablerBooks from "~icons/tabler/books"; +import TablerSocial from "~icons/tabler/social"; // This layout component performs authentication checks before the user can access protected pages export const Route = createFileRoute("/_protected")({ @@ -34,5 +43,59 @@ export const Route = createFileRoute("/_protected")({ }); function RouteComponent() { - return ; + const pathname = useLocation({ select: (loc) => loc.pathname }); + + return ( +
+
+

SwampHacks

+
+ +
+ + + {/* Main content */} +
+ +
+
+
+ ); } diff --git a/apps/web/src/routes/_protected/resources/programming.tsx b/apps/web/src/routes/_protected/resources/programming.tsx new file mode 100644 index 00000000..477d6485 --- /dev/null +++ b/apps/web/src/routes/_protected/resources/programming.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/_protected/resources/programming")({ + component: RouteComponent, +}); + +function RouteComponent() { + return
Hello "/_protected/resources/programming"!
; +} diff --git a/apps/web/src/routes/_protected/resources/sponsors.tsx b/apps/web/src/routes/_protected/resources/sponsors.tsx new file mode 100644 index 00000000..ed094ab0 --- /dev/null +++ b/apps/web/src/routes/_protected/resources/sponsors.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/_protected/resources/sponsors")({ + component: RouteComponent, +}); + +function RouteComponent() { + return
Hello "/_protected/resources/sponsors"!
; +} diff --git a/apps/web/src/theme.css b/apps/web/src/theme.css index c42452d2..99566bbe 100644 --- a/apps/web/src/theme.css +++ b/apps/web/src/theme.css @@ -8,6 +8,14 @@ --text-main: var(--color-zinc-900); --text-secondary: var(--color-zinc-600); + /* NAVBAR */ + --navlink-bg-inactive: transparent; + --navlink-bg-active: var(--color-neutral-300); + + /* NAVBAR TEXT */ + --navlink-text: var(--text-main); + --navlink-secondary-text: var(--color-neutral-500); + /* BUTTONS */ --button-primary: var(--color-blue-500); --button-primary-hover: var(--color-blue-700); @@ -120,6 +128,14 @@ --text-main: var(--color-zinc-100); --text-secondary: var(--color-zinc-400); + /* NAVBAR */ + --navlink-bg-inactive: transparent; + --navlink-bg-active: var(--surface); + + /* NAVBAR TEXT */ + --navlink-text: var(--text-main); + --navlink-secondary-text: var(--color-neutral-500); + /* BUTTONS */ --button-primary: var(--color-blue-500); --button-primary-hover: var(--color-blue-700); @@ -263,6 +279,11 @@ --color-text-main: var(--text-main); --color-text-secondary: var(--text-secondary); + --color-navlink-bg-inactive: var(--navlink-bg-inactive); + --color-navlink-bg-active: var(--navlink-bg-active); + --color-navlink-text: var(--navlink-text); + --color-navlink-secondary-text: var(--navlink-secondary-text); + --color-button-primary: var(--button-primary); --color-button-primary-hover: var(--button-primary-hover); --color-button-primary-disabled: var(--button-primary-disabled);