From a6f60a5a380e40a9069d4d5ec140690d267e1e3f Mon Sep 17 00:00:00 2001 From: KartoffelChips <104089082+KartoffelChipss@users.noreply.github.com> Date: Sun, 5 Apr 2026 17:55:58 +0200 Subject: [PATCH] Added dashboard page layout & sites overview --- backend/app/handlers/site.go | 3 + backend/app/models/site.go | 1 + backend/internal/database/init_sqlite.go | 1 + backend/internal/database/site_sqlite.go | 18 +- frontend/package.json | 1 + frontend/pnpm-lock.yaml | 77 ++ frontend/src/api/index.ts | 7 + frontend/src/api/sites.api.ts | 24 + frontend/src/api/types/site.ts | 46 ++ frontend/src/components/AppSidebar.tsx | 115 +++ frontend/src/components/NavUser.tsx | 123 ++++ frontend/src/components/logo.tsx | 19 + frontend/src/components/nav-menu.tsx | 36 + frontend/src/components/navbar.tsx | 44 ++ frontend/src/components/navigation-sheet.tsx | 26 + frontend/src/components/ui/avatar.tsx | 110 +++ frontend/src/components/ui/badge.tsx | 49 ++ frontend/src/components/ui/breadcrumb.tsx | 122 ++++ frontend/src/components/ui/button.tsx | 117 ++- frontend/src/components/ui/card.tsx | 103 +++ frontend/src/components/ui/dropdown-menu.tsx | 267 +++++++ frontend/src/components/ui/empty.tsx | 104 +++ frontend/src/components/ui/input.tsx | 19 + .../src/components/ui/navigation-menu.tsx | 164 +++++ frontend/src/components/ui/separator.tsx | 27 + frontend/src/components/ui/sheet.tsx | 145 ++++ frontend/src/components/ui/sidebar.tsx | 673 ++++++++++++++++++ frontend/src/components/ui/skeleton.tsx | 13 + frontend/src/components/ui/spinner.tsx | 10 + frontend/src/components/ui/tooltip.tsx | 57 ++ frontend/src/hooks/api/useSites.ts | 10 + frontend/src/hooks/use-mobile.ts | 19 + frontend/src/index.css | 58 +- frontend/src/main.tsx | 3 +- frontend/src/pages/Main/Main.tsx | 136 +++- frontend/src/pages/Page.tsx | 24 + frontend/src/utils/effectiveTheme.ts | 9 + frontend/vite.config.ts | 18 +- 38 files changed, 2687 insertions(+), 111 deletions(-) create mode 100644 frontend/src/api/index.ts create mode 100644 frontend/src/api/sites.api.ts create mode 100644 frontend/src/api/types/site.ts create mode 100644 frontend/src/components/AppSidebar.tsx create mode 100644 frontend/src/components/NavUser.tsx create mode 100644 frontend/src/components/logo.tsx create mode 100644 frontend/src/components/nav-menu.tsx create mode 100644 frontend/src/components/navbar.tsx create mode 100644 frontend/src/components/navigation-sheet.tsx create mode 100644 frontend/src/components/ui/avatar.tsx create mode 100644 frontend/src/components/ui/badge.tsx create mode 100644 frontend/src/components/ui/breadcrumb.tsx create mode 100644 frontend/src/components/ui/card.tsx create mode 100644 frontend/src/components/ui/dropdown-menu.tsx create mode 100644 frontend/src/components/ui/empty.tsx create mode 100644 frontend/src/components/ui/input.tsx create mode 100644 frontend/src/components/ui/navigation-menu.tsx create mode 100644 frontend/src/components/ui/separator.tsx create mode 100644 frontend/src/components/ui/sheet.tsx create mode 100644 frontend/src/components/ui/sidebar.tsx create mode 100644 frontend/src/components/ui/skeleton.tsx create mode 100644 frontend/src/components/ui/spinner.tsx create mode 100644 frontend/src/components/ui/tooltip.tsx create mode 100644 frontend/src/hooks/api/useSites.ts create mode 100644 frontend/src/hooks/use-mobile.ts create mode 100644 frontend/src/pages/Page.tsx create mode 100644 frontend/src/utils/effectiveTheme.ts diff --git a/backend/app/handlers/site.go b/backend/app/handlers/site.go index dd6055d..7338055 100644 --- a/backend/app/handlers/site.go +++ b/backend/app/handlers/site.go @@ -89,6 +89,9 @@ func validateIncomingSite(site *models.Site) error { if site == nil { return errors.New("site is required") } + if site.Name == "" { + return errors.New("site name is required") + } if site.GitServer == "" { return errors.New("git server required") } diff --git a/backend/app/models/site.go b/backend/app/models/site.go index 6ac0a32..dfd1de9 100644 --- a/backend/app/models/site.go +++ b/backend/app/models/site.go @@ -23,6 +23,7 @@ type CustomHeaders struct { type Site struct { ID string `json:"id"` + Name string `json:"name"` GitServer string `json:"git_server"` Owner string `json:"owner"` Repository string `json:"repository"` diff --git a/backend/internal/database/init_sqlite.go b/backend/internal/database/init_sqlite.go index c8221f2..d1de047 100644 --- a/backend/internal/database/init_sqlite.go +++ b/backend/internal/database/init_sqlite.go @@ -9,6 +9,7 @@ func InitializeSQLite(db *sql.DB) error { _, err := db.Exec(` CREATE TABLE IF NOT EXISTS sites ( id TEXT PRIMARY KEY, + name TEXT NOT NULL, git_server TEXT NOT NULL, owner TEXT NOT NULL, repository TEXT NOT NULL, diff --git a/backend/internal/database/site_sqlite.go b/backend/internal/database/site_sqlite.go index d838785..9ff03a6 100644 --- a/backend/internal/database/site_sqlite.go +++ b/backend/internal/database/site_sqlite.go @@ -25,7 +25,7 @@ var _ repository.SiteRepository = (*SQLiteSiteRepository)(nil) func (r *SQLiteSiteRepository) GetSite(id string) (*models.Site, error) { row := r.db.QueryRow(` - SELECT id, git_server, owner, repository, branch, domain, deploy_token, enabled, not_found_file + SELECT id, name, git_server, owner, repository, branch, domain, deploy_token, enabled, not_found_file FROM sites WHERE id = ?`, id) s, err := scanSite(row) @@ -41,7 +41,7 @@ func (r *SQLiteSiteRepository) GetSite(id string) (*models.Site, error) { func (r *SQLiteSiteRepository) GetSiteByDomain(domain string) (*models.Site, error) { row := r.db.QueryRow(` - SELECT id, git_server, owner, repository, branch, domain, deploy_token, enabled, not_found_file + SELECT id, name, git_server, owner, repository, branch, domain, deploy_token, enabled, not_found_file FROM sites WHERE domain = ?`, domain) s, err := scanSite(row) @@ -60,7 +60,7 @@ func (r *SQLiteSiteRepository) GetSiteByDomain(domain string) (*models.Site, err func (r *SQLiteSiteRepository) ListSites() ([]models.Site, error) { rows, err := r.db.Query(` - SELECT id, git_server, owner, repository, branch, domain, deploy_token, enabled, not_found_file + SELECT id, name, git_server, owner, repository, branch, domain, deploy_token, enabled, not_found_file FROM sites`) if err != nil { return nil, fmt.Errorf("list sites: %w", err) @@ -98,9 +98,9 @@ func (r *SQLiteSiteRepository) CreateSite(s *models.Site) error { s.ID = uuid.NewString() _, err = tx.Exec(` - INSERT INTO sites (id, git_server, owner, repository, branch, domain, deploy_token, enabled, not_found_file) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, - s.ID, s.GitServer, s.Owner, s.Repository, s.Branch, + INSERT INTO sites (id, name, git_server, owner, repository, branch, domain, deploy_token, enabled, not_found_file) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + s.ID, s.Name, s.GitServer, s.Owner, s.Repository, s.Branch, s.Domain, s.DeployToken, s.Enabled, s.NotFoundFile, ) if err != nil { @@ -130,9 +130,9 @@ func (r *SQLiteSiteRepository) UpdateSite(s *models.Site) error { defer tx.Rollback() _, err = tx.Exec(` - UPDATE sites SET git_server=?, owner=?, repository=?, branch=?, domain=?, + UPDATE sites SET name=?, git_server=?, owner=?, repository=?, branch=?, domain=?, deploy_token=?, enabled=?, not_found_file=? WHERE id=?`, - s.GitServer, s.Owner, s.Repository, s.Branch, s.Domain, + s.Name, s.GitServer, s.Owner, s.Repository, s.Branch, s.Domain, s.DeployToken, s.Enabled, s.NotFoundFile, s.ID, ) if err != nil { @@ -333,7 +333,7 @@ func scanSite(s scanner) (*models.Site, error) { var site models.Site var enabled int err := s.Scan( - &site.ID, &site.GitServer, &site.Owner, &site.Repository, + &site.ID, &site.Name, &site.GitServer, &site.Owner, &site.Repository, &site.Branch, &site.Domain, &site.DeployToken, &enabled, &site.NotFoundFile, ) if err != nil { diff --git a/frontend/package.json b/frontend/package.json index 63ea5d0..42e8e3e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@fontsource-variable/geist": "^5.2.8", + "@radix-ui/react-visually-hidden": "^1.2.4", "@tailwindcss/vite": "^4.2.2", "@tanstack/react-query": "^5.96.2", "class-variance-authority": "^0.7.1", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 5307a88..663108e 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@fontsource-variable/geist': specifier: ^5.2.8 version: 5.2.8 + '@radix-ui/react-visually-hidden': + specifier: ^1.2.4 + version: 1.2.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@tailwindcss/vite': specifier: ^4.2.2 version: 4.2.2(vite@8.0.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.12.1)(jiti@2.6.1)) @@ -825,6 +828,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-primitive@2.1.4': + resolution: {integrity: sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-progress@1.1.7': resolution: {integrity: sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==} peerDependencies: @@ -925,6 +941,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-slot@1.2.4': + resolution: {integrity: sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-switch@1.2.6': resolution: {integrity: sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==} peerDependencies: @@ -1110,6 +1135,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-visually-hidden@1.2.4': + resolution: {integrity: sha512-kaeiyGCe844dkb9AVF+rb4yTyb1LiLN/e3es3nLiRyN4dC8AduBYPMnnNlDjX2VDOcvDEiPnRNMJeWCfsX0txg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} @@ -1148,36 +1186,42 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': resolution: {integrity: sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': resolution: {integrity: sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': resolution: {integrity: sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] + libc: [glibc] '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': resolution: {integrity: sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': resolution: {integrity: sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [musl] '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': resolution: {integrity: sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==} @@ -1253,24 +1297,28 @@ packages: engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.2.2': resolution: {integrity: sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.2.2': resolution: {integrity: sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.2.2': resolution: {integrity: sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.2.2': resolution: {integrity: sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==} @@ -2231,24 +2279,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} @@ -3888,6 +3940,15 @@ snapshots: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@radix-ui/react-primitive@2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-slot': 1.2.4(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@radix-ui/react-progress@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) @@ -4014,6 +4075,13 @@ snapshots: optionalDependencies: '@types/react': 19.2.14 + '@radix-ui/react-slot@1.2.4(@types/react@19.2.14)(react@19.2.4)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + '@radix-ui/react-switch@1.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/primitive': 1.1.3 @@ -4196,6 +4264,15 @@ snapshots: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@radix-ui/react-visually-hidden@1.2.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@radix-ui/rect@1.1.1': {} '@rolldown/binding-android-arm64@1.0.0-rc.12': diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts new file mode 100644 index 0000000..7a81129 --- /dev/null +++ b/frontend/src/api/index.ts @@ -0,0 +1,7 @@ +export const API_BASE_URL = '/api/v1'; + +export const makeApiUrl = (endpoint: string) => { + const base = API_BASE_URL.endsWith('/') ? API_BASE_URL.slice(0, -1) : API_BASE_URL; + const path = endpoint.startsWith('/') ? endpoint : `/${endpoint}`; + return `${base}${path}`; +}; diff --git a/frontend/src/api/sites.api.ts b/frontend/src/api/sites.api.ts new file mode 100644 index 0000000..f60089a --- /dev/null +++ b/frontend/src/api/sites.api.ts @@ -0,0 +1,24 @@ +import { makeApiUrl } from '.'; + +export const getSites = async () => { + const response = await fetch(makeApiUrl('/sites'), { + method: 'GET', + }); + if (response.status === 404) { + return { sites: [], total: 0 }; + } + if (!response.ok) { + throw new Error('Failed to fetch sites'); + } + return response.json(); +}; + +export const getSite = async (id: string) => { + const response = await fetch(makeApiUrl(`/sites/${id}`), { + method: 'GET', + }); + if (!response.ok) { + throw new Error('Failed to fetch site'); + } + return response.json(); +}; diff --git a/frontend/src/api/types/site.ts b/frontend/src/api/types/site.ts new file mode 100644 index 0000000..a9d4248 --- /dev/null +++ b/frontend/src/api/types/site.ts @@ -0,0 +1,46 @@ +export interface ForwardRule { + id: string; + source: string; + destination: string; + status_code: number; + regex: boolean; +} + +export interface Header { + id: string; + key: string; + value: string; +} + +export interface CustomHeaders { + id: string; + source: string; + regex: boolean; + headers: Header[]; +} + +export interface Site { + id: string; + name: string; + git_server: string; + owner: string; + repository: string; + branch: string; + domain: string; + deploy_token: string; + enabled: boolean; + spa: boolean; + not_found_file: string; + forward_rules: ForwardRule[]; + custom_headers: CustomHeaders[]; +} + +export interface GetAllSitesResponse { + sites: Site[]; + total: number; +} + +export interface CreateSiteResponse { + site: Site; + raw_deploy_token: string; +} diff --git a/frontend/src/components/AppSidebar.tsx b/frontend/src/components/AppSidebar.tsx new file mode 100644 index 0000000..39400ce --- /dev/null +++ b/frontend/src/components/AppSidebar.tsx @@ -0,0 +1,115 @@ +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarGroup, + SidebarGroupContent, + SidebarHeader, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarMenuSub, +} from '@/components/ui/sidebar'; +import { Home, Library, PanelsTopLeft, PanelTop, Search } 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'; + +const AppSidebar = () => { + const location = useLocation(); + const { theme } = useTheme(); + const effectiveTheme = getEffectiveTheme(theme); + + return ( + + + + + + + {'Q'} + +
+ Quay + + Static site deployment + +
+
+
+
+ + + {/* Idk */} + + + + + + + Sites + + + + + + + TensuraMap + + + + bifrost + + + + spaltoon3api + + + + homepage + + + + + {/* + + + + Library + + + + + + + + Search + + + */} + + + + + + + +
+ ); +}; + +export default AppSidebar; diff --git a/frontend/src/components/NavUser.tsx b/frontend/src/components/NavUser.tsx new file mode 100644 index 0000000..4162b73 --- /dev/null +++ b/frontend/src/components/NavUser.tsx @@ -0,0 +1,123 @@ +import { useTheme } from './theme-provider'; +import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, useSidebar } from './ui/sidebar'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from './ui/dropdown-menu'; +import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar'; +import { ChevronsUpDown, Laptop, LogOut, Moon, Sun } from 'lucide-react'; + +export function NavUser() { + const { isMobile } = useSidebar(); + const { theme, setTheme } = useTheme(); + + const userName = 'Jan'; + const profilePictureUrl = '/api/v1/users/me/profile-image'; + + return ( + + + + + + + + + {userName + .split(' ') + .map((n) => n[0]) + .join('') + .toUpperCase()} + + +
+ {userName} +
+ +
+
+ + +
+ + + + {userName + .split(' ') + .map((n) => n[0]) + .join('') + .toUpperCase()} + + +
+ {userName} +
+
+
+ + + + + {theme === 'light' ? ( + + ) : theme === 'dark' ? ( + + ) : ( + + )} + Theme + + + + setTheme('light')}> + + Light + + setTheme('dark')}> + + Dark + + setTheme('system')}> + + System + + + + + { + // TODO logout logic + }} + > + + Logout + +
+
+
+
+ ); +} diff --git a/frontend/src/components/logo.tsx b/frontend/src/components/logo.tsx new file mode 100644 index 0000000..0a8946f --- /dev/null +++ b/frontend/src/components/logo.tsx @@ -0,0 +1,19 @@ +export const Logo = () => ( + + + + +); diff --git a/frontend/src/components/nav-menu.tsx b/frontend/src/components/nav-menu.tsx new file mode 100644 index 0000000..e2f5f93 --- /dev/null +++ b/frontend/src/components/nav-menu.tsx @@ -0,0 +1,36 @@ +import type { ComponentProps } from 'react'; +import { + NavigationMenu, + NavigationMenuItem, + NavigationMenuLink, + NavigationMenuList, + navigationMenuTriggerStyle, +} from '@/components/ui/navigation-menu'; +import { Link } from 'react-router'; + +export const NavMenu = (props: ComponentProps) => ( + + + + + Home + + + + + Blog + + + + + About + + + + + Contact Us + + + + +); diff --git a/frontend/src/components/navbar.tsx b/frontend/src/components/navbar.tsx new file mode 100644 index 0000000..73a8f0e --- /dev/null +++ b/frontend/src/components/navbar.tsx @@ -0,0 +1,44 @@ +import { Logo } from '@/components/logo'; +import { NavMenu } from '@/components/nav-menu'; +import { NavigationSheet } from '@/components/navigation-sheet'; +import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar'; + +interface NavbarProps { + userName?: string; + profilePictureUrl?: string; +} + +const Navbar = ({ userName, profilePictureUrl }: NavbarProps) => { + return ( + + ); +}; + +export default Navbar; diff --git a/frontend/src/components/navigation-sheet.tsx b/frontend/src/components/navigation-sheet.tsx new file mode 100644 index 0000000..776bd6c --- /dev/null +++ b/frontend/src/components/navigation-sheet.tsx @@ -0,0 +1,26 @@ +import { VisuallyHidden } from '@radix-ui/react-visually-hidden'; +import { Menu } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Sheet, SheetContent, SheetTitle, SheetTrigger } from '@/components/ui/sheet'; +import { Logo } from '@/components/logo'; +import { NavMenu } from '@/components/nav-menu'; + +export const NavigationSheet = () => { + return ( + + + Navigation Menu + + + + + + + + + + + ); +}; diff --git a/frontend/src/components/ui/avatar.tsx b/frontend/src/components/ui/avatar.tsx new file mode 100644 index 0000000..99f3ed2 --- /dev/null +++ b/frontend/src/components/ui/avatar.tsx @@ -0,0 +1,110 @@ +import * as React from "react" +import { Avatar as AvatarPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function Avatar({ + className, + size = "default", + ...props +}: React.ComponentProps & { + size?: "default" | "sm" | "lg" +}) { + return ( + + ) +} + +function AvatarImage({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarFallback({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) { + return ( + svg]:hidden", + "group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2", + "group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2", + className + )} + {...props} + /> + ) +} + +function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AvatarGroupCount({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3", + className + )} + {...props} + /> + ) +} + +export { + Avatar, + AvatarImage, + AvatarFallback, + AvatarGroup, + AvatarGroupCount, + AvatarBadge, +} diff --git a/frontend/src/components/ui/badge.tsx b/frontend/src/components/ui/badge.tsx new file mode 100644 index 0000000..cacff11 --- /dev/null +++ b/frontend/src/components/ui/badge.tsx @@ -0,0 +1,49 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { Slot } from "radix-ui" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80", + secondary: + "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80", + destructive: + "bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20", + outline: + "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground", + ghost: + "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50", + link: "text-primary underline-offset-4 hover:underline", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Badge({ + className, + variant = "default", + asChild = false, + ...props +}: React.ComponentProps<"span"> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot.Root : "span" + + return ( + + ) +} + +export { Badge, badgeVariants } diff --git a/frontend/src/components/ui/breadcrumb.tsx b/frontend/src/components/ui/breadcrumb.tsx new file mode 100644 index 0000000..db9afc0 --- /dev/null +++ b/frontend/src/components/ui/breadcrumb.tsx @@ -0,0 +1,122 @@ +import * as React from "react" +import { Slot } from "radix-ui" + +import { cn } from "@/lib/utils" +import { ChevronRightIcon, MoreHorizontalIcon } from "lucide-react" + +function Breadcrumb({ className, ...props }: React.ComponentProps<"nav">) { + return ( +