Redesign the frontend UI on twui

Rebuild every page on a shared set of twui-based primitives with a dense
ops-console look, WireGuard red accent and light/dark/system theming.

- Add design tokens (WireGuard red accent, zinc neutrals, theme-flipping
  semantic colours) and a no-flash theme cookie + toggle
- Add shared components: page header, panel, sub-nav, data table with
  mobile card list, stat tile, status dot, key-value list, copy button,
  inline alert, empty state, form section/actions, confirm dialog and
  toast providers
- New admin shell (sidebar, sticky header, mobile drawer) and a centered
  shell for auth and 404; strip the public template pages
- Unify adding clients at /admin/clients/add with a host picker; tabbed
  client detail (Overview / Rules / Config & QR) with config copy/download
- Re-run client setup after rule changes on the detail page so the
  firewall matches the DB
- Split Setup into status/maintenance/firewall and Settings into
  profile/password subpages
- Replace window.confirm/alert and page reloads with in-app dialogs,
  toasts and in-place list updates
- Fix nested rule form submitting the client form, prefilled signup
  passwords, hidden submit buttons, fields not updating after "next
  free" lookups, and TinyMCE white-on-white in dark mode

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-16 06:29:21 +01:00
co-authored by Claude Opus 5.5
parent e50b0405b0
commit cad1d32ae4
291 changed files with 6436 additions and 6612 deletions
@@ -0,0 +1,57 @@
import {
LayoutDashboard,
LogOut,
Server,
Settings,
Users,
Wrench,
type LucideIcon,
} from "lucide-react";
export type AdminNavItem = {
title: string;
url: string;
Icon: LucideIcon;
/** Only active on an exact path match */
strict?: boolean;
super_admin_only?: boolean;
};
export type AdminNavGroup = {
group: string;
items: AdminNavItem[];
};
export const AdminNav: AdminNavGroup[] = [
{
group: "Overview",
items: [
{ title: "Dashboard", url: "/admin", Icon: LayoutDashboard, strict: true },
],
},
{
group: "WireGuard",
items: [
{ title: "Hosts", url: "/admin/hosts", Icon: Server },
{ title: "Clients", url: "/admin/clients", Icon: Users },
],
},
{
group: "System",
items: [
{
title: "Setup",
url: "/admin/setup",
Icon: Wrench,
super_admin_only: true,
},
],
},
{
group: "Account",
items: [
{ title: "Settings", url: "/admin/settings", Icon: Settings },
{ title: "Sign out", url: "/auth/logout", Icon: LogOut },
],
},
];
+24 -23
View File
@@ -1,11 +1,11 @@
import React from "react";
import { Menu, X } from "lucide-react";
import AdminAsideLinks from "./admin-aside-links-dict";
import Row from "@/src/components/twui/layout/Row";
import LogoIcon from "@/src/components/general/logo-icon";
import Divider from "@/src/components/twui/layout/Divider";
import { twMerge } from "tailwind-merge";
import Button from "@/src/components/twui/layout/Button";
import Stack from "@/src/components/twui/layout/Stack";
import Logo from "@/src/components/general/logo";
import { twMerge } from "tailwind-merge";
import AdminNavigation from "./admin-nav";
export default function MobileNav() {
const [open, setOpen] = React.useState(false);
@@ -34,21 +34,23 @@ export default function MobileNav() {
return (
<div className="flex xl:hidden">
<button
<Button
ref={triggerRef}
type="button"
aria-label="Open menu"
title="Open menu"
variant="ghost"
color="gray"
aria-expanded={open}
className="cursor-pointer p-1"
className="p-1.5! -ml-1.5"
onClick={() => setOpen(true)}
>
<Menu size={28} />
</button>
<Menu size={20} />
</Button>
{open ? (
<>
<div
className="fixed inset-0 z-200 bg-dark/50"
className="fixed inset-0 z-200 bg-dark/60"
onClick={() => setOpen(false)}
aria-hidden
/>
@@ -57,36 +59,35 @@ export default function MobileNav() {
tabIndex={-1}
className={twMerge(
"fixed top-0 left-0 bottom-0 z-210",
"w-[min(280px,85vw)] bg-background-light dark:bg-background-dark",
"w-[min(272px,85vw)] bg-surface border-r border-line",
"flex flex-col shadow-xl outline-none",
)}
role="dialog"
aria-modal="true"
aria-label="Admin navigation"
>
<Row className="w-full justify-between items-center py-2 px-4 h-20 flex-nowrap">
<a href="/" onClick={() => setOpen(false)}>
<LogoIcon icon_size={40} />
</a>
<button
<Row className="w-full justify-between px-4 h-14 flex-nowrap border-b border-line">
<Logo />
<Button
type="button"
aria-label="Close menu"
className="cursor-pointer p-1"
title="Close menu"
variant="ghost"
color="gray"
className="p-1.5!"
onClick={() => setOpen(false)}
>
<X size={28} />
</button>
<X size={18} />
</Button>
</Row>
<Divider />
<Stack
className="w-full grow overflow-y-auto"
className="w-full grow overflow-y-auto px-2 py-4 items-stretch"
onClick={(e) => {
if ((e.target as HTMLElement).closest("a")) {
setOpen(false);
}
}}
>
<AdminAsideLinks />
<AdminNavigation />
</Stack>
</aside>
</>
@@ -1,102 +0,0 @@
import type { TWUILink } from "@/src/components/twui/elements/LinkList";
import LinkList from "@/src/components/twui/elements/LinkList";
import Stack from "@/src/components/twui/layout/Stack";
import { AppContext } from "@/src/pages/__root";
import { useContext } from "react";
import { LayoutDashboard, LogOut, Server, Settings, Users, Wrench } from "lucide-react";
const sectionLabel = (label: string) => (
<div className="px-3 pt-6 pb-1.5 text-[11.5px] font-semibold uppercase tracking-[0.08em] text-foreground-light/40 dark:text-foreground-dark/40">
{label}
</div>
);
const linkClassName =
"px-3 py-2 w-full rounded-[5px] no-underline! border-b-0! " +
"text-[14px] font-medium text-foreground-light/70 dark:text-foreground-dark/70 " +
"hover:text-foreground-light! dark:hover:text-foreground-dark! " +
"hover:bg-foreground-light/5 dark:hover:bg-foreground-dark/5 transition-colors";
const iconClassName = "shrink-0 opacity-60";
export default function AdminAsideLinks() {
const { pageProps } = useContext(AppContext);
const { user, user_types } = pageProps || {};
if (!user?.logged_in_status) {
return null;
}
const is_super_admin =
user_types?.find((t) => t.user_type === "super_admin") !== undefined;
const links: TWUILink[] = [
{ component: sectionLabel("Overview") },
{
title: "Dashboard",
url: "/admin",
strict: true,
icon: (
<LayoutDashboard
size={16}
strokeWidth={2}
className={iconClassName}
/>
),
},
{ component: sectionLabel("WireGuard") },
{
title: "Hosts",
url: "/admin/hosts",
icon: (
<Server size={16} strokeWidth={2} className={iconClassName} />
),
},
{
title: "Clients",
url: "/admin/clients",
icon: <Users size={16} strokeWidth={2} className={iconClassName} />,
},
{ component: sectionLabel("System") },
...(is_super_admin
? [
{
title: "Setup",
url: "/admin/setup",
icon: (
<Wrench
size={16}
strokeWidth={2}
className={iconClassName}
/>
),
},
]
: []),
{ component: sectionLabel("Account") },
{
title: "Settings",
url: "/admin/settings",
icon: (
<Settings size={16} strokeWidth={2} className={iconClassName} />
),
},
{
title: "Logout",
url: "/auth/logout",
icon: (
<LogOut size={16} strokeWidth={2} className={iconClassName} />
),
},
];
return (
<Stack className="w-full items-stretch">
<LinkList
links={links}
className="gap-0.5 flex-col items-start pb-6"
linkProps={{ className: linkClassName }}
/>
</Stack>
);
}
+41 -29
View File
@@ -1,29 +1,41 @@
import Row from "@/src/components/twui/layout/Row";
import Stack from "@/src/components/twui/layout/Stack";
import LogoIcon from "@/src/components/general/logo-icon";
import Divider from "@/src/components/twui/layout/Divider";
import AdminAsideLinks from "./admin-aside-links-dict";
import { twMerge } from "tailwind-merge";
type Props = {
className?: string;
};
export default function AdminAside({ className }: Props) {
return (
<aside className={twMerge("w-[220px] shrink-0", className)}>
<Stack className="w-full items-stretch gap-0! sticky top-0 h-screen overflow-y-auto">
<Row className="px-4 h-20 flex items-center">
<a href="/" className="no-underline! border-b-0!">
<LogoIcon icon_size={40} />
</a>
</Row>
<Divider />
<nav className="w-full grow">
<AdminAsideLinks />
</nav>
<Divider />
</Stack>
</aside>
);
}
import Row from "@/src/components/twui/layout/Row";
import Stack from "@/src/components/twui/layout/Stack";
import Logo from "@/src/components/general/logo";
import Link from "@/src/components/twui/layout/Link";
import { SiteData } from "@/src/data/site-data";
import { twMerge } from "tailwind-merge";
import AdminNavigation from "./admin-nav";
type Props = {
className?: string;
};
export default function AdminAside({ className }: Props) {
return (
<aside
className={twMerge(
"w-[232px] shrink-0 border-r border-line bg-surface",
className,
)}
>
<Stack className="w-full items-stretch gap-0 sticky top-0 h-screen">
<Row className="px-4 h-14 shrink-0 border-b border-line">
<Logo />
</Row>
<nav aria-label="Admin" className="w-full grow overflow-y-auto px-2 py-4">
<AdminNavigation />
</nav>
<Row className="px-4 h-11 shrink-0 border-t border-line">
<Link
href={SiteData["SiteURL"]}
target="_blank"
rel="noreferrer"
className="text-[13px] text-subtle! hover:text-fg! no-underline!"
>
Source &amp; docs
</Link>
</Row>
</Stack>
</aside>
);
}
@@ -0,0 +1,21 @@
import Stack from "@/src/components/twui/layout/Stack";
import SectionLabel from "@/src/components/general/section-label";
import type { AdminNavItem as AdminNavItemType } from "../(data)/admin-nav-dict";
import AdminNavItem from "./admin-nav-item";
type Props = {
group: string;
items: AdminNavItemType[];
pathname?: string;
};
export default function AdminNavGroup({ group, items, pathname }: Props) {
return (
<Stack className="w-full gap-0.5 items-stretch">
<SectionLabel className="px-2.5 pb-1">{group}</SectionLabel>
{items.map((item) => (
<AdminNavItem {...item} key={item.url} pathname={pathname} />
))}
</Stack>
);
}
@@ -0,0 +1,37 @@
import Link from "@/src/components/twui/layout/Link";
import Row from "@/src/components/twui/layout/Row";
import { twMerge } from "tailwind-merge";
import type { AdminNavItem as AdminNavItemType } from "../(data)/admin-nav-dict";
type Props = AdminNavItemType & {
pathname?: string;
};
export default function AdminNavItem({ title, url, Icon, strict, pathname }: Props) {
const active = pathname
? strict
? pathname == url
: pathname == url || pathname.startsWith(`${url}/`)
: false;
return (
<Link
href={url}
aria-current={active ? "page" : undefined}
className={twMerge(
"w-full px-2.5 py-1.5 rounded-default no-underline! text-[15px] font-medium",
"text-muted! hover:text-fg! hover:bg-gray dark:hover:bg-gray-dark transition-colors",
active && "text-fg! bg-gray dark:bg-gray-dark",
)}
>
<Row className="gap-2.5 flex-nowrap">
<Icon
size={15}
strokeWidth={2}
className={twMerge("shrink-0", active ? "text-primary dark:text-primary-dark-text" : "opacity-70")}
/>
{title}
</Row>
</Link>
);
}
@@ -0,0 +1,35 @@
import Stack from "@/src/components/twui/layout/Stack";
import { AppContext } from "@/src/pages/__root";
import { useContext } from "react";
import { AdminNav } from "../(data)/admin-nav-dict";
import AdminNavGroup from "./admin-nav-group";
export default function AdminNavigation() {
const { pageProps } = useContext(AppContext);
const { user, user_types, url } = pageProps || {};
if (!user?.logged_in_status) return null;
const is_super_admin = Boolean(
user_types?.find((t) => t.user_type === "super_admin"),
);
const groups = AdminNav.map((group) => ({
...group,
items: group.items.filter(
(item) => !item.super_admin_only || is_super_admin,
),
})).filter((group) => group.items.length);
return (
<Stack className="w-full gap-5 items-stretch">
{groups.map((group) => (
<AdminNavGroup
{...group}
key={group.group}
pathname={url?.pathname}
/>
))}
</Stack>
);
}
@@ -0,0 +1,24 @@
import Link from "@/src/components/twui/layout/Link";
import Row from "@/src/components/twui/layout/Row";
import type { LucideIcon } from "lucide-react";
type Props = {
title: string;
url: string;
Icon: LucideIcon;
};
export default function HeaderUserMenuItem({ title, url, Icon }: Props) {
return (
<Link
href={url}
role="menuitem"
className="w-full px-2.5 py-1.5 rounded-default-sm no-underline! text-[15px] text-fg! hover:bg-gray dark:hover:bg-gray-dark"
>
<Row className="gap-2.5 flex-nowrap">
<Icon size={14} className="opacity-70" />
{title}
</Row>
</Link>
);
}
@@ -0,0 +1,66 @@
import { useContext } from "react";
import Dropdown from "@/src/components/twui/elements/Dropdown";
import Img from "@/src/components/twui/layout/Img";
import Paper from "@/src/components/twui/elements/Paper";
import Stack from "@/src/components/twui/layout/Stack";
import Span from "@/src/components/twui/layout/Span";
import Button from "@/src/components/twui/layout/Button";
import MutedText from "@/src/components/general/muted-text";
import { AppContext } from "@/src/pages/__root";
import { LayoutDashboard, LogOut, Settings } from "lucide-react";
import HeaderUserMenuItem from "./header-user-menu-item";
const MenuItems = [
{ title: "Dashboard", url: "/admin", Icon: LayoutDashboard },
{ title: "Settings", url: "/admin/settings", Icon: Settings },
{ title: "Sign out", url: "/auth/logout", Icon: LogOut },
];
export default function HeaderUser() {
const { pageProps } = useContext(AppContext);
const { user } = pageProps || {};
if (!user?.logged_in_status) {
return null;
}
const full_name = [user.first_name, user.last_name].filter(Boolean).join(" ");
return (
<Dropdown
position="bottom-right"
target={
<Button
title="Account menu"
type="button"
variant="ghost"
color="gray"
aria-haspopup="menu"
className="p-0! rounded-full"
>
<Img
alt=""
src={user.image_thumbnail || "/icons/man.png"}
size={30}
circle
className="bg-gray dark:bg-gray-dark ring-1 ring-line"
/>
</Button>
}
>
<Paper role="menu" className="min-w-[220px] p-1! gap-0.5 shadow-lg rounded-default-lg">
<Stack className="gap-0 px-2.5 py-2 mb-1 border-b border-line w-full">
<Span className="text-[15px] font-medium text-fg truncate max-w-full">
{full_name || user.username}
</Span>
<MutedText text_size="xs" className="truncate max-w-full">
{user.email}
</MutedText>
</Stack>
{MenuItems.map((item) => (
<HeaderUserMenuItem {...item} key={item.url} />
))}
</Paper>
</Dropdown>
);
}
-32
View File
@@ -1,32 +0,0 @@
import Row from "@/src/components/twui/layout/Row";
import Span from "@/src/components/twui/layout/Span";
import { SiteData } from "@/src/data/site-data";
import { AppContext } from "@/src/pages/__root";
import type { AppContextObject } from "@/src/types";
import React from "react";
export default function Footer() {
const appContext: AppContextObject = React.useContext(AppContext);
const { user, appState } = appContext;
/**
* Render component
*/
return (
<footer className="w-full p-4 h-20">
<Row className="h-full">
<Span size="smaller" variant="faded">
Copyright © {new Date().getFullYear()}.{" "}
<a
href={SiteData["SiteURL"]}
target="_blank"
className="decoration-dotted underline"
>
{SiteData["SiteName"]}
</a>
. All rights reserved
</Span>
</Row>
</footer>
);
}
+43 -47
View File
@@ -1,47 +1,43 @@
import Row from "@/src/components/twui/layout/Row";
import HeaderUser from "../../main/(partials)/header-user";
import LogoIcon from "@/src/components/general/logo-icon";
import MobileNav from "../(partials)/MobileNav";
import { twMerge } from "tailwind-merge";
import Breadcrumbs from "@/src/components/twui/elements/Breadcrumbs";
import { useContext } from "react";
import { AppContext } from "@/src/pages/__root";
type Props = {};
export default function Header({}: Props) {
const { pageProps } = useContext(AppContext);
return (
<header className="h-20">
<Row
className={twMerge(
"w-full justify-between h-full py-4 px-4 xl:px-10",
"flex-nowrap",
)}
>
<Row className="flex-nowrap">
<LogoIcon
icon_size={60}
img_props={{ className: "flex xl:hidden" }}
/>
<MobileNav />
<Breadcrumbs
dividerProps={{
className: "-skew-10",
}}
currentLinkProps={{
className: "text-slate-400! pointer-events-none",
}}
pageUrl={pageProps?.url?.pathname}
backButton
skipHome
/>
</Row>
<Row className="flex-nowrap">
<HeaderUser />
</Row>
</Row>
</header>
);
}
import Row from "@/src/components/twui/layout/Row";
import Breadcrumbs from "@/src/components/twui/elements/Breadcrumbs";
import LogoIcon from "@/src/components/general/logo-icon";
import ThemeToggle from "@/src/components/general/theme/theme-toggle";
import { AppContext } from "@/src/pages/__root";
import { useContext } from "react";
import { ChevronRight } from "lucide-react";
import HeaderUser from "../(partials)/header-user";
import MobileNav from "../(partials)/MobileNav";
export default function Header() {
const { pageProps } = useContext(AppContext);
return (
<header className="sticky top-0 z-40 h-14 shrink-0 border-b border-line bg-surface/85 backdrop-blur">
<Row className="w-full h-full justify-between gap-3 px-4 sm:px-6 flex-nowrap">
<Row className="gap-2 flex-nowrap min-w-0">
<MobileNav />
<LogoIcon icon_size={24} className="xl:hidden md:hidden" />
<Row className="hidden md:flex min-w-0">
<Breadcrumbs
divider={
<ChevronRight size={13} className="text-subtle shrink-0 -mx-2" />
}
linkProps={{
className: "text-[15px] text-muted! hover:text-fg! no-underline!",
}}
currentLinkProps={{
className: "text-fg! pointer-events-none",
}}
pageUrl={pageProps?.url?.pathname}
skipHome
/>
</Row>
</Row>
<Row className="gap-2 flex-nowrap">
<ThemeToggle />
<HeaderUser />
</Row>
</Row>
</header>
);
}
+19 -40
View File
@@ -1,40 +1,19 @@
import React from "react";
import Header from "./(sections)/Header";
import Footer from "./(sections)/Footer";
import { AppContext } from "@/src/pages/__root";
import type { AppContextObject } from "@/src/types";
import Row from "@/src/components/twui/layout/Row";
import Stack from "@/src/components/twui/layout/Stack";
import LogoIcon from "@/src/components/general/logo-icon";
import Divider from "@/src/components/twui/layout/Divider";
import AdminAsideLinks from "./(partials)/admin-aside-links-dict";
import AdminAside from "./(partials)/admin-aside";
/* -------------------------------------------------------------------------- */
/* Main Layout Functional Component */
/* -------------------------------------------------------------------------- */
// Main Layout Component
export default function Layout({ children }: { children: React.ReactNode }) {
const appContext = React.useContext<AppContextObject>(AppContext);
const { user } = appContext;
return (
<>
<Row className="w-full items-stretch gap-0! flex-nowrap">
<AdminAside className="hidden xl:flex" />
<Divider vertical className="h-auto" />
<Stack className="grow items-stretch gap-0!">
<Header />
<Divider />
<main className="grow items-start" id="admin-main">
{children}
</main>
<Divider />
<Footer />
</Stack>
</Row>
<Divider />
</>
);
}
import React from "react";
import Row from "@/src/components/twui/layout/Row";
import Stack from "@/src/components/twui/layout/Stack";
import Header from "./(sections)/Header";
import AdminAside from "./(partials)/admin-aside";
export default function AdminLayout({ children }: { children: React.ReactNode }) {
return (
<Row className="w-full min-h-screen items-stretch gap-0 flex-nowrap">
<AdminAside className="hidden xl:block" />
<Stack className="grow min-w-0 items-stretch gap-0">
<Header />
<main className="grow" id="admin-main">
{children}
</main>
</Stack>
</Row>
);
}
+29
View File
@@ -0,0 +1,29 @@
import Card from "@/src/components/twui/elements/Card";
import Stack from "@/src/components/twui/layout/Stack";
import Logo from "@/src/components/general/logo";
import ThemeToggle from "@/src/components/general/theme/theme-toggle";
import type { PropsWithChildren } from "react";
import { twMerge } from "tailwind-merge";
type Props = PropsWithChildren & {
width?: "sm" | "md";
};
/** Auth pages, 404 and other pages outside the admin shell. */
export default function CenteredShell({ children, width = "sm" }: Props) {
return (
<Stack className="relative min-h-screen w-full items-center justify-center gap-6 px-4 py-12">
<ThemeToggle className="absolute top-4 right-4" />
<Logo icon_size={32} href="/" />
<Card
noHover
className={twMerge(
"w-full flex-col items-stretch gap-0 p-6 sm:p-8 shadow-sm",
width == "md" ? "max-w-[640px]" : "max-w-[400px]",
)}
>
{children}
</Card>
</Stack>
);
}
-95
View File
@@ -1,95 +0,0 @@
import React from "react";
import { Menu, X } from "lucide-react";
import Stack from "@/src/components/twui/layout/Stack";
import { MainLinks } from "@/src/dict/main-links-dict";
import LinkList from "@/src/components/twui/elements/LinkList";
import HeaderCTA from "./header-cta";
import Row from "@/src/components/twui/layout/Row";
import Logo from "@/src/components/general/logo";
import { twMerge } from "tailwind-merge";
export default function MobileNav() {
const [open, setOpen] = React.useState(false);
React.useEffect(() => {
if (!open) return;
const prev = document.body.style.overflow;
document.body.style.overflow = "hidden";
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") setOpen(false);
};
document.addEventListener("keydown", onKey);
return () => {
document.body.style.overflow = prev;
document.removeEventListener("keydown", onKey);
};
}, [open]);
return (
<div className="flex xl:hidden relative">
<button
type="button"
aria-label="Open menu"
aria-expanded={open}
className="cursor-pointer text-white p-1"
onClick={() => setOpen(true)}
>
<Menu size={30} />
</button>
{open ? (
<div
className={twMerge(
"fixed inset-0 z-200 flex flex-col",
"bg-dark text-white",
)}
role="dialog"
aria-modal="true"
aria-label="Mobile navigation"
onClick={(e) => {
if ((e.target as HTMLElement).closest("a")) {
setOpen(false);
}
}}
>
<Row className="w-full justify-between items-center px-4 py-6 flex-nowrap">
<Logo
icon_size={60}
text_wrapper_props={{
className: "max-[400px]:hidden",
}}
/>
<button
type="button"
aria-label="Close menu"
className="cursor-pointer text-white p-1"
onClick={() => setOpen(false)}
>
<X size={30} />
</button>
</Row>
<Stack className="grow w-full items-stretch px-6 py-4 gap-8! overflow-y-auto">
<nav className="w-full">
<LinkList
links={MainLinks}
className="flex-col items-stretch gap-1 w-full"
linkProps={{
className: twMerge(
"px-2 py-4 w-full text-xl border-0",
"text-white/80 hover:text-white",
),
}}
/>
</nav>
<HeaderCTA className="flex flex-col w-full items-stretch" />
</Stack>
</div>
) : null}
</div>
);
}
-28
View File
@@ -1,28 +0,0 @@
export default function NavList({ transparent }: { transparent?: boolean }) {
const className = transparent ? "light" : "";
/**
* Render component
*/
return (
<div className="gap-10 flex-col xl:flex-row">
<a className={className} href="/">
Home
</a>
<a className={className} href="/subsidiaries">
Subsidiaries
</a>
<a className={className} href="/about">
About
</a>
<a className={className} href="/events">
Events
</a>
<a className={className} href="/gallery">
Gallery
</a>
<a className={className} href="/contact">
Contact Us
</a>
</div>
);
}
@@ -1,17 +0,0 @@
import Row from "@/src/components/twui/layout/Row";
import Button from "@/src/components/twui/layout/Button";
import { twMerge } from "tailwind-merge";
type Props = {
className?: string;
};
export default function HeaderCTA({ className }: Props) {
return (
<Row className={twMerge("hidden xl:flex", className)}>
<Button title="Login" className="w-full" href="/auth/login">
Login
</Button>
</Row>
);
}
@@ -1,61 +0,0 @@
import { useContext } from "react";
import Row from "@/src/components/twui/layout/Row";
import { AppContext } from "@/src/pages/__root";
import Dropdown from "@/src/components/twui/elements/Dropdown";
import Img from "@/src/components/twui/layout/Img";
import Paper from "@/src/components/twui/elements/Paper";
import LinkList from "@/src/components/twui/elements/LinkList";
import Divider from "@/src/components/twui/layout/Divider";
export default function HeaderUser() {
const { pageProps } = useContext(AppContext);
const { user, user_types } = pageProps || {};
if (!user?.logged_in_status) {
return null;
}
return (
<Row className="">
<Dropdown
target={
<Img
alt="Logged in user image"
src={user.image_thumbnail ?? "/icons/man.png"}
size={45}
circle
className="bg-slate-400"
title={
user.first_name +
(user.last_name ? ` ${user.last_name}` : "")
}
/>
}
position="bottom-right"
>
<Paper className="min-w-[200px] px-0! py-2 header-user-dropdown">
<LinkList
links={[
{
title: "Dashboard",
url: "/admin",
},
{
component: <Divider />,
},
{
title: "Settings",
url: "/admin/settings",
},
{
title: "Logout",
url: "/auth/logout",
},
]}
className="flex-col items-stretch w-full"
/>
</Paper>
</Dropdown>
</Row>
);
}
-74
View File
@@ -1,74 +0,0 @@
import React from "react";
import { AppContext } from "@/src/pages/__root";
import type { AppContextObject } from "@/src/types";
import Divider from "@/src/components/twui/layout/Divider";
import Stack from "@/src/components/twui/layout/Stack";
import Section from "@/src/components/twui/layout/Section";
import Container from "@/src/components/twui/layout/Container";
import Row from "@/src/components/twui/layout/Row";
import Logo from "@/src/components/general/logo";
import Span from "@/src/components/twui/layout/Span";
import H4 from "@/src/components/twui/layout/H4";
import { SiteData } from "@/src/data/site-data";
export default function Footer() {
const appContext: AppContextObject = React.useContext(AppContext);
/**
* Render component
*/
return (
<footer className="bg-dark text-white">
<Section className="pt-20! pb-0!">
<Container>
<Stack className="w-full gap-10! items-center">
<Row className="w-full items-start justify-start xl:justify-between gap-20!">
<div className="flex flex-col items-start max-w-[380px] gap-4">
<Logo
text_props={{ className: "text-white" }}
white
/>
<Span className="opacity-70">
Wireguard UI is the ultimate dashboard for
managing raw Wireguard
</Span>
</div>
<Row className="gap-20! justify-start flex-wrap items-start">
<div className="flex items-start gap-10 justify-between flex-wrap xl:ml-auto">
<Stack>
<H4>Company</H4>
<Divider className="opacity-20" />
<a href="/about">Company</a>
<a href="/terms">
Terms and Conditions
</a>
<a href="/privacy">Privacy Policy</a>
</Stack>
</div>
</Row>
</Row>
<Divider className="opacity-20" />
<div
className="container px-4 sm:px-6 flex w-full items-start justify-between flex-wrap"
style={{
gap: "40px 100px",
}}
>
<span className="text-sm opacity-60">
Copyright {new Date().getFullYear()}{" "}
<a href={SiteData["SiteURL"]}>
{SiteData["SiteName"]}
</a>
. All rights reserved
</span>
</div>
</Stack>
</Container>
</Section>
<div className="w-full h-20 relative z-100 overflow-hidden mt-20"></div>
</footer>
);
}
-58
View File
@@ -1,58 +0,0 @@
import MobileNav from "../(partials)/MobileNav";
import Section from "@/src/components/twui/layout/Section";
import Container from "@/src/components/twui/layout/Container";
import Row from "@/src/components/twui/layout/Row";
import Logo from "@/src/components/general/logo";
import LinkList from "@/src/components/twui/elements/LinkList";
import { MainLinks } from "@/src/dict/main-links-dict";
import Divider from "@/src/components/twui/layout/Divider";
import HeaderCTA from "../(partials)/header-cta";
import HeaderUser from "../(partials)/header-user";
import { twMerge } from "tailwind-merge";
type Props = {
no_header_divider?: boolean;
};
export default function Header({ no_header_divider }: Props) {
/**
* Render component
*/
return (
<header className={"px-4 py-6 w-full z-100 -mb-[160px] relative"}>
<Section className="p-0! relative z-10">
<Container className="w-full justify-between">
<Row className="w-full items-center gap-10 justify-between flex-nowrap">
<Row className="flex xl:hidden">
<Logo
text_wrapper_props={{
className: "max-[400px]:hidden",
}}
/>
</Row>
<Row className="gap-5! hidden xl:flex">
<Logo />
{/* <Row className="hidden xl:flex gap-3!">
<Divider vertical className="-skew-12 h-10" />
<nav>
<LinkList
links={MainLinks}
className="gap-1"
/>
</nav>
</Row> */}
</Row>
<Row className="flex-nowrap">
<HeaderCTA />
<MobileNav />
<HeaderUser />
</Row>
</Row>
</Container>
</Section>
{no_header_divider ? null : <Divider className="mt-6 opacity-20" />}
</header>
);
}
-31
View File
@@ -1,31 +0,0 @@
import React from "react";
import Header from "./(sections)/Header";
import Footer from "./(sections)/Footer";
import { AppContext } from "@/src/pages/__root";
import Stack from "@/src/components/twui/layout/Stack";
type Props = {
children: React.ReactNode;
no_header_divider?: boolean;
blank?: boolean;
};
export default function Layout({
children,
no_header_divider,
blank,
}: Props): React.JSX.Element {
const {} = React.useContext(AppContext);
return (
<React.Fragment>
<Stack className="w-full items-stretch gap-0">
{blank ? null : (
<Header no_header_divider={no_header_divider} />
)}
<main>{children}</main>
{blank ? null : <Footer />}
</Stack>
</React.Fragment>
);
}