Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions apps/web/app/s/[subdomain]/dashboard/settings/billing/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export default function DashboardBillingSettingsPage() {
return (
<section className="p-6 md:p-10">
<div className="mb-6 w-full max-w-3xl">
<p className="text-xs font-semibold uppercase tracking-[0.12em] text-(--muted-2)">
Settings
</p>
<h1 className="mt-2 text-2xl font-semibold tracking-tight text-(--text)">
Billing
</h1>
<p className="mt-3 text-sm text-(--muted)">
Manage your plan, payment method, invoices, and billing details.
</p>
</div>

<div className="w-full max-w-3xl rounded-(--r-md) border border-(--border) bg-(--surface) p-6">
<p className="text-sm font-medium text-(--text)">Billing dashboard</p>
<p className="mt-2 text-sm text-(--muted)">
Billing controls will appear here next. For now, contact support to
update subscription details.
</p>
</div>
</section>
);
}
60 changes: 24 additions & 36 deletions apps/web/app/s/[subdomain]/dashboard/settings/page.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,6 @@
import Link from "next/link";

import { IconGlobe } from "@/components/icons/icon-globe";
import { IconPeople } from "@/components/icons/icon-people";
import { IconRoadmap } from "@/components/icons/icon-roadmap";

const settingsItems = [
{
href: "/dashboard/settings/custom-domain",
title: "Custom domain",
description: "Connect and verify your domain.",
icon: IconGlobe,
},
{
href: "/dashboard/settings/team",
title: "Team",
description: "Invite members and manage permissions.",
icon: IconPeople,
},
{
href: "/dashboard/settings/feedback-roadmap",
title: "Feedback & roadmap",
description: "Manage tags, boards, statuses, and public board behavior.",
icon: IconRoadmap,
},
];
import { settingsNavGroups } from "~/dashboard/lib/settings-nav";

export default function DashboardSettingsPage() {
return (
Expand All @@ -40,19 +17,30 @@ export default function DashboardSettingsPage() {
</p>
</div>

<div className="grid w-full max-w-4xl gap-3 md:grid-cols-2">
{settingsItems.map((item) => (
<Link
key={item.href}
href={item.href}
className="rounded-(--r-md) border border-(--border) bg-(--surface) p-4 transition-colors hover:border-(--text)/20 hover:bg-black/2"
>
<item.icon className="size-5 text-(--text)" />
<p className="mt-3 text-sm font-semibold text-(--text)">
{item.title}
<div className="w-full max-w-4xl space-y-7">
{settingsNavGroups.map((group) => (
<section key={group.id}>
<p className="px-1 text-xs font-medium text-(--muted)">
{group.label}
</p>
<p className="mt-1 text-sm text-(--muted)">{item.description}</p>
</Link>
<div className="mt-2 grid gap-3 md:grid-cols-2">
{group.items.map((item) => (
<Link
key={item.href}
href={item.href}
className="rounded-(--r-md) border border-(--border) bg-(--surface) p-4 transition-colors hover:border-(--text)/20 hover:bg-black/2"
>
<item.icon className="size-5 text-(--text)" />
<p className="mt-3 text-sm font-semibold text-(--text)">
{item.label}
</p>
<p className="mt-1 text-sm text-(--muted)">
{item.description}
</p>
</Link>
))}
</div>
</section>
))}
</div>
</section>
Expand Down
29 changes: 29 additions & 0 deletions apps/web/components/icons/icon-billing.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import * as React from "react";
import type { SVGProps } from "react";

type IconBillingProps = SVGProps<SVGSVGElement> & {
color?: string;
};

export function IconBilling({
color = "currentColor",
...props
}: IconBillingProps) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
viewBox="0 0 24 24"
fill="none"
{...props}
>
<path
fill={color}
fillRule="evenodd"
clipRule="evenodd"
d="M5 5C4.44772 5 4 5.44772 4 6C4 6.55228 4.44772 7 5 7H8H18C20.2091 7 22 8.79086 22 11V18C22 20.2091 20.2091 22 18 22H5C3.34315 22 2 20.6569 2 19V6C2 4.34315 3.34315 3 5 3H19C19.5523 3 20 3.44772 20 4C20 4.55228 19.5523 5 19 5H5ZM16.5 11C14.567 11 13 12.567 13 14.5C13 16.433 14.567 18 16.5 18H19C19.5523 18 20 17.5523 20 17C20 16.4477 19.5523 16 19 16H16.5C15.6716 16 15 15.3284 15 14.5C15 13.6716 15.6716 13 16.5 13H19C19.5523 13 20 12.5523 20 12C20 11.4477 19.5523 11 19 11H16.5Z"
/>
</svg>
);
}
29 changes: 29 additions & 0 deletions apps/web/components/icons/icon-book-bookmark.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import * as React from "react";
import type { SVGProps } from "react";

type IconBookBookmarkProps = SVGProps<SVGSVGElement> & {
color?: string;
};

export function IconBookBookmark({
color = "currentColor",
...props
}: IconBookBookmarkProps) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
viewBox="0 0 24 24"
fill="none"
{...props}
>
<path
fill={color}
fillRule="evenodd"
clipRule="evenodd"
d="M18 2C19.1046 2 20 2.89543 20 4L20 16C20 17.1046 19.1046 18 18 18H7C6.44772 18 6 18.4477 6 19C6 19.5523 6.44772 20 7 20H18.5C19.0523 20 19.5 20.4477 19.5 21C19.5 21.5523 19.0523 22 18.5 22H7C5.34315 22 4 20.6569 4 19V5C4 3.34315 5.34315 2 7 2H18ZM11 5C11 4.44772 11.4477 4 12 4H17C17.5523 4 18 4.44772 18 5V8.93248C18 9.78032 17.0111 10.2435 16.3598 9.7007L14.5 8.15085L12.6402 9.7007C11.9889 10.2435 11 9.78032 11 8.93248V5Z"
/>
</svg>
);
}
29 changes: 29 additions & 0 deletions apps/web/components/icons/icon-chevron-left.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import * as React from "react";
import type { SVGProps } from "react";

type IconChevronLeftProps = SVGProps<SVGSVGElement> & {
color?: string;
};

export function IconChevronLeft({
color = "currentColor",
...props
}: IconChevronLeftProps) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
viewBox="0 0 24 24"
fill="none"
{...props}
>
<path
fill={color}
fillRule="evenodd"
clipRule="evenodd"
d="M16.7071 3.29289C17.0976 3.68342 17.0976 4.31658 16.7071 4.70711L9.41421 12L16.7071 19.2929C17.0976 19.6834 17.0976 20.3166 16.7071 20.7071C16.3166 21.0976 15.6834 21.0976 15.2929 20.7071L7.29289 12.7071C7.10536 12.5196 7 12.2652 7 12C7 11.7348 7.10536 11.4804 7.29289 11.2929L15.2929 3.29289C15.6834 2.90237 16.3166 2.90237 16.7071 3.29289Z"
/>
</svg>
);
}
44 changes: 31 additions & 13 deletions apps/web/features/dashboard/components/dashboard-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import { Menu01Icon } from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import { AnimatePresence, motion } from "motion/react";
import { useState } from "react";
import { usePathname } from "next/navigation";

import { Button } from "@/components/ui/button";
import { DashboardSidebar } from "~/dashboard/components/dashboard-sidebar";
import { SettingsSidebar } from "~/dashboard/components/settings-sidebar";

export interface DashboardWorkspaceOption {
connectedDomain?: string | null;
Expand Down Expand Up @@ -41,19 +43,28 @@ function DashboardShellRoot({
user,
}: DashboardShellProps) {
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
const pathname = usePathname();
const normalizedPath = pathname.replace(/^\/s\/[^/]+/, "");
const isSettingsRoute =
normalizedPath === "/dashboard/settings" ||
normalizedPath.startsWith("/dashboard/settings/");

return (
<div
className="h-screen overflow-hidden bg-(--bg) text-(--text)"
data-ui-theme="agency-dashboard"
>
<div className="flex h-full overflow-hidden">
<DashboardSidebar
className="hidden md:block"
organizationName={organizationName}
workspaces={workspaces}
user={user}
/>
{isSettingsRoute ? (
<SettingsSidebar className="hidden md:block" />
) : (
<DashboardSidebar
className="hidden md:block"
organizationName={organizationName}
workspaces={workspaces}
user={user}
/>
)}

<div className="flex h-full min-h-0 flex-1 flex-col overflow-hidden">
<header className="sticky top-0 z-20 flex h-14 items-center border-b border-(--border) bg-(--bg)/95 px-4 backdrop-blur md:hidden">
Expand Down Expand Up @@ -95,13 +106,20 @@ function DashboardShellRoot({
exit={{ x: -28, opacity: 0 }}
transition={{ duration: 0.2, ease: "easeOut" }}
>
<DashboardSidebar
className="h-full"
onNavigate={() => setMobileSidebarOpen(false)}
organizationName={organizationName}
workspaces={workspaces}
user={user}
/>
{isSettingsRoute ? (
<SettingsSidebar
className="h-full"
onNavigate={() => setMobileSidebarOpen(false)}
/>
) : (
<DashboardSidebar
className="h-full"
onNavigate={() => setMobileSidebarOpen(false)}
organizationName={organizationName}
workspaces={workspaces}
user={user}
/>
)}
</motion.div>
</>
) : null}
Expand Down
94 changes: 94 additions & 0 deletions apps/web/features/dashboard/components/settings-sidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
"use client";

import Link from "next/link";
import { usePathname } from "next/navigation";

import { IconChevronLeft } from "@/components/icons/icon-chevron-left";
import { cn } from "@/lib/utils";
import { settingsNavGroups } from "~/dashboard/lib/settings-nav";

interface SettingsSidebarProps {
className?: string;
onNavigate?: () => void;
}

function isActive(pathname: string, href: string) {
const normalizedPath = pathname.replace(/^\/s\/[^/]+/, "");
return normalizedPath === href || normalizedPath.startsWith(`${href}/`);
}

export function SettingsSidebar({
className,
onNavigate,
}: SettingsSidebarProps) {
const pathname = usePathname();

return (
<aside
className={cn(
"h-screen w-72 shrink-0 border-r border-(--sidebar-border) bg-(--sidebar) text-(--sidebar-foreground)",
className,
)}
>
<div className="flex h-full flex-col">
<div className="px-3 py-3">
<Link
href="/dashboard"
onClick={onNavigate}
className="inline-flex items-center gap-2 rounded-md px-2 py-1 text-sm font-medium text-(--muted) transition-colors hover:text-(--text)"
>
<IconChevronLeft aria-hidden className="size-4" />
<span>Back to app</span>
</Link>
</div>

<div className="flex-1 overflow-y-auto px-2 py-4">
{settingsNavGroups.map((group, index) => (
<div key={group.id} className={cn(index > 0 && "mt-5")}>
<p className="px-2 text-xs font-medium text-(--muted)">
{group.label}
</p>
<nav className="mt-2 space-y-0.5">
{group.items.map((item) => {
const active = isActive(pathname, item.href);

return (
<Link
key={item.id}
href={item.href}
onClick={onNavigate}
aria-current={active ? "page" : undefined}
className={cn(
"group/item relative flex h-8 items-center gap-2 rounded-lg border border-transparent pl-3 pr-2 text-[0.86rem] font-medium text-(--muted) outline-none transition-[color,background-color,border-color,transform]",
"hover:border-transparent hover:bg-black/7 hover:text-(--text)",
"focus-visible:ring-2 focus-visible:ring-(--sidebar-ring) focus-visible:ring-offset-2 focus-visible:ring-offset-(--sidebar)",
active &&
"border-(--border) bg-(--surface) text-(--text) shadow-[0_1px_0_rgba(17,18,20,0.05)] hover:border-(--border) hover:bg-black/6 hover:text-(--text)",
)}
>
{active ? (
<span
className="absolute -left-2 top-1/2 h-8 w-[2px] -translate-y-1/2 rounded-br-full rounded-tr-full border border-l-0 bg-primary [corner-shape:superellipse(0.3)]"
style={{
borderColor:
"color-mix(in oklab, var(--primary) 90%, black)",
}}
/>
) : null}
<item.icon
className={cn("size-[17px]", active && "text-(--text)")}
/>
<span className="min-w-0 flex-1 truncate">
{item.label}
</span>
</Link>
);
})}
</nav>
</div>
))}
</div>
</div>
</aside>
);
}
3 changes: 2 additions & 1 deletion apps/web/features/dashboard/dashboard-nav.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ComponentType, SVGProps } from "react";

import { IconAssistant } from "@/components/icons/icon-assistant";
import { IconBookBookmark } from "@/components/icons/icon-book-bookmark";
import { IconHelp } from "@/components/icons/icon-help";
import { IconInbox } from "@/components/icons/icon-inbox";
import { IconMap } from "@/components/icons/icon-map";
Expand Down Expand Up @@ -74,7 +75,7 @@ export const feedbackNavItems: DashboardNavItem[] = [
id: "changelog",
href: "/dashboard/changelog",
iconType: "svg",
icon: IconMap,
icon: IconBookBookmark,
label: "Changelog",
},
{
Expand Down
Loading