From c8ec394774016becb8fd87ce207e10c42de4054a Mon Sep 17 00:00:00 2001 From: KartoffelChipss Date: Tue, 5 May 2026 22:32:49 +0200 Subject: [PATCH] Add app sidebar --- frontend/src/components/AppSidebar.tsx | 103 ++++-------------- frontend/src/components/NavUser.tsx | 47 ++++---- frontend/src/components/TopBar.tsx | 15 +++ frontend/src/components/ui/collapsible.tsx | 31 ++++++ frontend/src/components/ui/sidebar.tsx | 2 +- frontend/src/index.css | 12 +- frontend/src/pages/GitServers/GitServers.tsx | 15 ++- frontend/src/pages/Main/Main.tsx | 29 +++-- frontend/src/pages/NewSite/NewSite.tsx | 2 +- frontend/src/pages/Page.tsx | 19 ++-- .../src/pages/SiteOverview/SiteOverview.tsx | 4 +- frontend/src/pages/Users/Users.tsx | 13 +-- frontend/src/utils/siteColors.ts | 42 +++++++ 13 files changed, 190 insertions(+), 144 deletions(-) create mode 100644 frontend/src/components/TopBar.tsx create mode 100644 frontend/src/components/ui/collapsible.tsx create mode 100644 frontend/src/utils/siteColors.ts diff --git a/frontend/src/components/AppSidebar.tsx b/frontend/src/components/AppSidebar.tsx index d59e6f3..a467982 100644 --- a/frontend/src/components/AppSidebar.tsx +++ b/frontend/src/components/AppSidebar.tsx @@ -8,55 +8,40 @@ import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, - SidebarMenuSub, } from '@/components/ui/sidebar'; -import { - Home, - Library, - PanelsTopLeft, - PanelTop, - Search, - Users as UsersIcon, - Code2, -} from 'lucide-react'; +import { PanelsTopLeft, Users as UsersIcon, Code2 } from 'lucide-react'; import { Link, useLocation } from 'react-router'; -import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar'; -import { useTheme } from './theme-provider'; -import { getEffectiveTheme } from '../utils/effectiveTheme'; import { NavUser } from './NavUser'; +import { Logo } from './logo'; +import { Separator } from './ui/separator'; -const AppSidebar = () => { +interface AppSidebarProps { + userName?: string; + profilePictureUrl?: string; +} + +const AppSidebar = ({ userName, profilePictureUrl }: AppSidebarProps) => { const location = useLocation(); - const { theme } = useTheme(); - const effectiveTheme = getEffectiveTheme(theme); return ( - + - - - {'Q'} - -
- Quay - - Static site deployment - -
+ + +
+ - {/* Idk */} + {/* Sites */} @@ -66,37 +51,6 @@ const AppSidebar = () => { Sites - - - - - TensuraMap - - - - bifrost - - - - spaltoon3api - - - - homepage - - - - - - - - - Users - - { - {/* - - - - Library - - - - - - Search + + + Users - */} + - +
); diff --git a/frontend/src/components/NavUser.tsx b/frontend/src/components/NavUser.tsx index 4162b73..2675885 100644 --- a/frontend/src/components/NavUser.tsx +++ b/frontend/src/components/NavUser.tsx @@ -10,14 +10,17 @@ import { } from './ui/dropdown-menu'; import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar'; import { ChevronsUpDown, Laptop, LogOut, Moon, Sun } from 'lucide-react'; +import { Link } from 'react-router'; -export function NavUser() { +interface NavUserProps { + userName?: string; + profilePictureUrl?: string; +} + +export function NavUser({ userName, profilePictureUrl }: NavUserProps) { const { isMobile } = useSidebar(); const { theme, setTheme } = useTheme(); - const userName = 'Jan'; - const profilePictureUrl = '/api/v1/users/me/profile-image'; - return ( @@ -25,7 +28,7 @@ export function NavUser() { {userName - .split(' ') - .map((n) => n[0]) - .join('') - .toUpperCase()} + ? userName + .split(' ') + .map((n) => n[0]) + .join('') + .toUpperCase() + : 'T'}
@@ -61,10 +66,12 @@ export function NavUser() { /> {userName - .split(' ') - .map((n) => n[0]) - .join('') - .toUpperCase()} + ? userName + .split(' ') + .map((n) => n[0]) + .join('') + .toUpperCase() + : 'T'}
@@ -75,7 +82,7 @@ export function NavUser() { - + {theme === 'light' ? ( ) : theme === 'dark' ? ( @@ -107,13 +114,11 @@ export function NavUser() { - { - // TODO logout logic - }} - > - - Logout + + + + Logout + diff --git a/frontend/src/components/TopBar.tsx b/frontend/src/components/TopBar.tsx new file mode 100644 index 0000000..e44e8e0 --- /dev/null +++ b/frontend/src/components/TopBar.tsx @@ -0,0 +1,15 @@ +interface TopBarProps { + title: string; + button?: React.ReactNode; +} + +const TopBar = ({ title, button }: TopBarProps) => { + return ( +
+

{title}

+ {button} +
+ ); +}; + +export default TopBar; diff --git a/frontend/src/components/ui/collapsible.tsx b/frontend/src/components/ui/collapsible.tsx new file mode 100644 index 0000000..63fc8ef --- /dev/null +++ b/frontend/src/components/ui/collapsible.tsx @@ -0,0 +1,31 @@ +import { Collapsible as CollapsiblePrimitive } from "radix-ui" + +function Collapsible({ + ...props +}: React.ComponentProps) { + return +} + +function CollapsibleTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CollapsibleContent({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Collapsible, CollapsibleTrigger, CollapsibleContent } diff --git a/frontend/src/components/ui/sidebar.tsx b/frontend/src/components/ui/sidebar.tsx index 491e0fa..1f5365e 100644 --- a/frontend/src/components/ui/sidebar.tsx +++ b/frontend/src/components/ui/sidebar.tsx @@ -446,7 +446,7 @@ function SidebarMenuItem({ className, ...props }: React.ComponentProps<'li'>) { } const sidebarMenuButtonVariants = cva( - 'peer/menu-button group/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm ring-sidebar-ring outline-hidden transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-open:hover:bg-sidebar-accent data-open:hover:text-sidebar-accent-foreground data-[active=true]:bg-primary data-[active=true]:font-medium data-[active=true]:text-primary-foreground data-[active=true]:shadow-none data-[active=true]:hover:bg-primary data-[active=true]:hover:text-primary-foreground [&_svg]:size-4 [&_svg]:shrink-0 [&>span:last-child]:truncate', + 'peer/menu-button group/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm ring-sidebar-ring outline-hidden transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-open:hover:bg-sidebar-accent data-open:hover:text-sidebar-accent-foreground data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[active=true]:shadow-none data-[active=true]:hover:bg-sidebar-accent data-[active=true]:hover:text-sidebar-accent-foreground [&_svg]:size-4 [&_svg]:shrink-0 [&>span:last-child]:truncate', { variants: { variant: { diff --git a/frontend/src/index.css b/frontend/src/index.css index 4e4014f..c9c5099 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -40,10 +40,10 @@ --chart-5: oklch(0.3742 0.1844 263.942); --sidebar: oklch(0.9846 0.0017 247.8389); --sidebar-foreground: oklch(0.3211 0 0); - --sidebar-primary: oklch(0.6225 0.2041 259.9027); + --sidebar-primary: oklch(0.4 0 0); --sidebar-primary-foreground: oklch(1 0 0); - --sidebar-accent: oklch(0.951 0.0267 237.5723); - --sidebar-accent-foreground: oklch(0.3742 0.1844 263.942); + --sidebar-accent: oklch(0.9271 0.0075 260.7315); + --sidebar-accent-foreground: oklch(0.3211 0 0); --sidebar-border: oklch(0.9271 0.0075 260.7315); --sidebar-ring: oklch(0.6225 0.2041 259.9027); --font-sans: Inter, sans-serif; @@ -97,10 +97,10 @@ --chart-5: oklch(0.6225 0.2041 259.9027); --sidebar: oklch(0.2046 0 0); --sidebar-foreground: oklch(0.9219 0 0); - --sidebar-primary: oklch(0.6225 0.2041 259.9027); + --sidebar-primary: oklch(0.7 0 0); --sidebar-primary-foreground: oklch(1 0 0); - --sidebar-accent: oklch(0.6225 0.2041 259.9027); - --sidebar-accent-foreground: oklch(0.882 0.0588 253.9688); + --sidebar-accent: oklch(0.32 0 0); + --sidebar-accent-foreground: oklch(0.9219 0 0); --sidebar-border: oklch(0.3715 0 0); --sidebar-ring: oklch(0.6225 0.2041 259.9027); --font-sans: Inter, sans-serif; diff --git a/frontend/src/pages/GitServers/GitServers.tsx b/frontend/src/pages/GitServers/GitServers.tsx index 4f7bd3b..7cb27f5 100644 --- a/frontend/src/pages/GitServers/GitServers.tsx +++ b/frontend/src/pages/GitServers/GitServers.tsx @@ -34,6 +34,8 @@ import { SelectValue, } from '../../components/ui/select'; import GitServerTypeIcon from '../../components/GitServerTypeIcon'; +import TopBar from '../../components/TopBar'; +import { Card } from '../../components/ui/card'; const DeleteGitServerDialog = ({ gitServerId }: { gitServerId: string }) => { const [open, setOpen] = useState(false); @@ -50,7 +52,7 @@ const DeleteGitServerDialog = ({ gitServerId }: { gitServerId: string }) => { return ( - @@ -81,7 +83,7 @@ const DeleteGitServerDialog = ({ gitServerId }: { gitServerId: string }) => { const GitServerRow = ({ gitServer }: { gitServer: GitServer }) => { return ( -
+
@@ -95,7 +97,7 @@ const GitServerRow = ({ gitServer }: { gitServer: GitServer }) => {
-
+ ); }; @@ -328,7 +330,7 @@ const UpdateGitServerDialog = ({ gitServer }: { gitServer: GitServer }) => { return ( - @@ -438,10 +440,7 @@ export const GitServers = () => { return ( -
-

Git Servers

- -
+ } />
{isLoading && } diff --git a/frontend/src/pages/Main/Main.tsx b/frontend/src/pages/Main/Main.tsx index 38d37fc..2d609d7 100644 --- a/frontend/src/pages/Main/Main.tsx +++ b/frontend/src/pages/Main/Main.tsx @@ -18,12 +18,19 @@ import { Skeleton } from '../../components/ui/skeleton'; import { memo } from 'react'; import { useDeploymentsForSite } from '../../hooks/api/useDeploymentsForSite'; import { formatDateRelativeOrAbsolute } from '../../utils/dateTime'; +import TopBar from '../../components/TopBar'; +import { getSiteColor } from '../../utils/siteColors'; const SiteAvatar = ({ name, enabled }: { name: string; enabled: boolean }) => (
{name[0].toUpperCase()}
@@ -86,15 +93,17 @@ const MainPage = () => { return ( -
-

Sites

- -
+ + + + New site + + + } + />
{isLoading && } diff --git a/frontend/src/pages/NewSite/NewSite.tsx b/frontend/src/pages/NewSite/NewSite.tsx index bd778bd..1e6e187 100644 --- a/frontend/src/pages/NewSite/NewSite.tsx +++ b/frontend/src/pages/NewSite/NewSite.tsx @@ -148,7 +148,7 @@ const NewSite = () => { return (
-

New Site

+

New Site

Deploy a static site from a Git repository.

diff --git a/frontend/src/pages/Page.tsx b/frontend/src/pages/Page.tsx index 2a11ef7..332e622 100644 --- a/frontend/src/pages/Page.tsx +++ b/frontend/src/pages/Page.tsx @@ -1,10 +1,11 @@ import { useEffect, type PropsWithChildren } from 'react'; import { useNavigate } from 'react-router'; -import Navbar from '../components/navbar'; import { useCurrentUser } from '../hooks/api/useCurrentUser'; import type { User } from '../api/types/user'; import { getToken } from '../utils/credentials'; import Footer from '../components/Footer'; +import { SidebarProvider } from '../components/ui/sidebar'; +import AppSidebar from '../components/AppSidebar'; interface PageProps { title: string; @@ -37,13 +38,15 @@ const Page = ({ children, title, className, requireAuth = true }: PropsWithChild const userName = (user as User | undefined)?.name; return ( -
- -
- {children} -
-
-
+ + +
+
+ {children} +
+
+
+
); }; diff --git a/frontend/src/pages/SiteOverview/SiteOverview.tsx b/frontend/src/pages/SiteOverview/SiteOverview.tsx index ce4da2e..83842f4 100644 --- a/frontend/src/pages/SiteOverview/SiteOverview.tsx +++ b/frontend/src/pages/SiteOverview/SiteOverview.tsx @@ -29,7 +29,7 @@ const DEFAULT_TAB: TabValue = 'overview'; const SiteOverviewSkeleton = memo(() => ( <> -
+
@@ -130,7 +130,7 @@ const SiteOverview = () => { {site && !isLoadingSite && !siteError && ( <> -
+

{site.name}

diff --git a/frontend/src/pages/Users/Users.tsx b/frontend/src/pages/Users/Users.tsx index 1868986..56bc71f 100644 --- a/frontend/src/pages/Users/Users.tsx +++ b/frontend/src/pages/Users/Users.tsx @@ -33,6 +33,8 @@ import { SelectValue, } from '../../components/ui/select'; import { useDeleteUser } from '../../hooks/api/users/useDeleteUser'; +import TopBar from '../../components/TopBar'; +import { Card } from '../../components/ui/card'; const DeleteUserDialog = ({ userId }: { userId: string }) => { const [open, setOpen] = useState(false); @@ -49,7 +51,7 @@ const DeleteUserDialog = ({ userId }: { userId: string }) => { return ( - @@ -79,7 +81,7 @@ const DeleteUserDialog = ({ userId }: { userId: string }) => { const UserRow = ({ user }: { user: User }) => { return ( -
+ {user.name.charAt(0).toUpperCase()} @@ -90,7 +92,7 @@ const UserRow = ({ user }: { user: User }) => {
-
+ ); }; @@ -237,10 +239,7 @@ export const Users = () => { return ( -
-

Users

- -
+ } />
{loadingUsers && } diff --git a/frontend/src/utils/siteColors.ts b/frontend/src/utils/siteColors.ts new file mode 100644 index 0000000..0988b7b --- /dev/null +++ b/frontend/src/utils/siteColors.ts @@ -0,0 +1,42 @@ +function hashString(str: string): number { + let hash = 0; + for (let i = 0; i < str.length; i++) { + hash = (hash * 31 + str.charCodeAt(i)) >>> 0; + } + return hash; +} + +function hslToHex(h: number, s: number, l: number): string { + s /= 100; + l /= 100; + const a = s * Math.min(l, 1 - l); + const f = (n: number) => { + const k = (n + h / 30) % 12; + const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1); + return Math.round(255 * color) + .toString(16) + .padStart(2, '0'); + }; + return `#${f(0)}${f(8)}${f(4)}`; +} + +export function getSiteColor(name: string): { bg: string; text: string } { + const hash = hashString(name.trim().toLowerCase()); + + // Hue between 210 and 260 (blue to purple) + const hue = 210 + (hash % 50); + + const hash2 = hashString(hash.toString()); + const saturation = 35 + (hash2 % 30); // 35–65% + const lightness = 30 + (hash2 % 30); // 30–60% + + const bg = hslToHex(hue, saturation, lightness); + + const r = parseInt(bg.slice(1, 3), 16); + const g = parseInt(bg.slice(3, 5), 16); + const b = parseInt(bg.slice(5, 7), 16); + const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; + const text = luminance > 0.5 ? '#1a1a1a' : '#ffffff'; + + return { bg, text }; +}