feat(frontend): new navbar design (#10341)

## Changes 🏗️

<img width="900" height="327" alt="Screenshot 2025-07-10 at 20 12 38"
src="https://github.com/user-attachments/assets/044f00ed-7e05-46b7-a821-ce1cb0ee9298"
/>
<br /><br />

Navbar updated to look pretty from the new designs:
- the logo is now centred instead of on the left
- menu items have been updated to a smaller font-size and less radius
- icons have been updated

I also generated the API files ( _sorry for the noise_ ). I had to do
some border-radius and button updates on the atoms/tokens for it to look
good.

## Checklist 📋

## For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
  - [x] Login/logout
  - [x] The new navbar looks good across screens 

## For configuration changes

No config changes
This commit is contained in:
Ubbe
2025-07-10 22:06:12 +04:00
committed by GitHub
parent 22c76eab61
commit 2fda8dfd32
32 changed files with 923 additions and 1516 deletions

View File

@@ -1,12 +1,13 @@
"use client"; "use client";
import { useSearchParams } from "next/navigation";
import { GraphID } from "@/lib/autogpt-server-api/types";
import FlowEditor from "@/components/Flow"; import FlowEditor from "@/components/Flow";
import { useOnboarding } from "@/components/onboarding/onboarding-provider"; import { useOnboarding } from "@/components/onboarding/onboarding-provider";
import { useEffect } from "react"; import LoadingBox from "@/components/ui/loading";
import { GraphID } from "@/lib/autogpt-server-api/types";
import { useSearchParams } from "next/navigation";
import { Suspense, useEffect } from "react";
export default function BuilderPage() { function BuilderContent() {
const query = useSearchParams(); const query = useSearchParams();
const { completeStep } = useOnboarding(); const { completeStep } = useOnboarding();
@@ -15,12 +16,20 @@ export default function BuilderPage() {
}, [completeStep]); }, [completeStep]);
const _graphVersion = query.get("flowVersion"); const _graphVersion = query.get("flowVersion");
const graphVersion = _graphVersion ? parseInt(_graphVersion) : undefined const graphVersion = _graphVersion ? parseInt(_graphVersion) : undefined;
return ( return (
<FlowEditor <FlowEditor
className="flow-container" className="flow-container"
flowID={query.get("flowID") as GraphID | null ?? undefined} flowID={(query.get("flowID") as GraphID | null) ?? undefined}
flowVersion={graphVersion} flowVersion={graphVersion}
/> />
); );
} }
export default function BuilderPage() {
return (
<Suspense fallback={<LoadingBox className="h-[80vh]" />}>
<BuilderContent />
</Suspense>
);
}

View File

@@ -1,67 +1,10 @@
import { Navbar } from "@/components/layout/Navbar/Navbar";
import { ReactNode } from "react"; import { ReactNode } from "react";
import { Navbar } from "@/components/agptui/Navbar";
import { IconType } from "@/components/ui/icons";
export default function PlatformLayout({ children }: { children: ReactNode }) { export default function PlatformLayout({ children }: { children: ReactNode }) {
return ( return (
<> <>
<Navbar <Navbar />
links={[
{
name: "Marketplace",
href: "/marketplace",
},
{
name: "Library",
href: "/library",
},
{
name: "Build",
href: "/build",
},
]}
menuItemGroups={[
{
items: [
{
icon: IconType.Edit,
text: "Edit profile",
href: "/profile",
},
],
},
{
items: [
{
icon: IconType.LayoutDashboard,
text: "Creator Dashboard",
href: "/profile/dashboard",
},
{
icon: IconType.UploadCloud,
text: "Publish an agent",
},
],
},
{
items: [
{
icon: IconType.Settings,
text: "Settings",
href: "/profile/settings",
},
],
},
{
items: [
{
icon: IconType.LogOut,
text: "Log out",
},
],
},
]}
/>
<main>{children}</main> <main>{children}</main>
</> </>
); );

View File

@@ -1,15 +1,15 @@
"use client"; "use client";
import Link from "next/link"; import Link from "next/link";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { import {
ArrowBottomRightIcon, ArrowBottomRightIcon,
QuestionMarkCircledIcon, QuestionMarkCircledIcon,
} from "@radix-ui/react-icons"; } from "@radix-ui/react-icons";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { LibraryPageStateProvider } from "./components/state-provider";
import LibraryActionHeader from "./components/LibraryActionHeader/LibraryActionHeader"; import LibraryActionHeader from "./components/LibraryActionHeader/LibraryActionHeader";
import LibraryAgentList from "./components/LibraryAgentList/LibraryAgentList"; import LibraryAgentList from "./components/LibraryAgentList/LibraryAgentList";
import { LibraryPageStateProvider } from "./components/state-provider";
/** /**
* LibraryPage Component * LibraryPage Component
@@ -17,7 +17,7 @@ import LibraryAgentList from "./components/LibraryAgentList/LibraryAgentList";
*/ */
export default function LibraryPage() { export default function LibraryPage() {
return ( return (
<main className="container min-h-screen space-y-4 pb-20 sm:px-8 md:px-12"> <main className="pt-160 container min-h-screen space-y-4 pb-20 pt-16 sm:px-8 md:px-12">
<LibraryPageStateProvider> <LibraryPageStateProvider>
<LibraryActionHeader /> <LibraryActionHeader />
<LibraryAgentList /> <LibraryAgentList />

View File

@@ -6,12 +6,12 @@
* OpenAPI spec version: 0.1 * OpenAPI spec version: 0.1
*/ */
import type { CredentialsMetaInputTitle } from "./credentialsMetaInputTitle"; import type { CredentialsMetaInputTitle } from "./credentialsMetaInputTitle";
import type { ProviderName } from "./providerName";
import type { CredentialsMetaInputType } from "./credentialsMetaInputType"; import type { CredentialsMetaInputType } from "./credentialsMetaInputType";
export interface CredentialsMetaInput { export interface CredentialsMetaInput {
id: string; id: string;
title?: CredentialsMetaInputTitle; title?: CredentialsMetaInputTitle;
/** Provider name for integrations. Can be any string value, including custom provider names. */ provider: ProviderName;
provider: string;
type: CredentialsMetaInputType; type: CredentialsMetaInputType;
} }

View File

@@ -5,12 +5,12 @@
* This server is used to execute agents that are created by the AutoGPT system. * This server is used to execute agents that are created by the AutoGPT system.
* OpenAPI spec version: 0.1 * OpenAPI spec version: 0.1
*/ */
import type { ProviderName } from "./providerName";
import type { LibraryAgentTriggerInfoConfigSchema } from "./libraryAgentTriggerInfoConfigSchema"; import type { LibraryAgentTriggerInfoConfigSchema } from "./libraryAgentTriggerInfoConfigSchema";
import type { LibraryAgentTriggerInfoCredentialsInputName } from "./libraryAgentTriggerInfoCredentialsInputName"; import type { LibraryAgentTriggerInfoCredentialsInputName } from "./libraryAgentTriggerInfoCredentialsInputName";
export interface LibraryAgentTriggerInfo { export interface LibraryAgentTriggerInfo {
/** Provider name for integrations. Can be any string value, including custom provider names. */ provider: ProviderName;
provider: string;
/** Input schema for the trigger block */ /** Input schema for the trigger block */
config_schema: LibraryAgentTriggerInfoConfigSchema; config_schema: LibraryAgentTriggerInfoConfigSchema;
credentials_input_name: LibraryAgentTriggerInfoCredentialsInputName; credentials_input_name: LibraryAgentTriggerInfoCredentialsInputName;

View File

@@ -0,0 +1,53 @@
/**
* Generated by orval v7.10.0 🍺
* Do not edit manually.
* AutoGPT Agent Server
* This server is used to execute agents that are created by the AutoGPT system.
* OpenAPI spec version: 0.1
*/
export type ProviderName = (typeof ProviderName)[keyof typeof ProviderName];
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const ProviderName = {
aiml_api: "aiml_api",
anthropic: "anthropic",
apollo: "apollo",
compass: "compass",
discord: "discord",
d_id: "d_id",
e2b: "e2b",
exa: "exa",
fal: "fal",
generic_webhook: "generic_webhook",
github: "github",
google: "google",
google_maps: "google_maps",
groq: "groq",
http: "http",
hubspot: "hubspot",
ideogram: "ideogram",
jina: "jina",
linear: "linear",
llama_api: "llama_api",
medium: "medium",
mem0: "mem0",
notion: "notion",
nvidia: "nvidia",
ollama: "ollama",
openai: "openai",
openweathermap: "openweathermap",
open_router: "open_router",
pinecone: "pinecone",
reddit: "reddit",
replicate: "replicate",
revid: "revid",
screenshotone: "screenshotone",
slant3d: "slant3d",
smartlead: "smartlead",
smtp: "smtp",
twitter: "twitter",
todoist: "todoist",
unreal_speech: "unreal_speech",
zerobounce: "zerobounce",
} as const;

View File

@@ -5,13 +5,13 @@
* This server is used to execute agents that are created by the AutoGPT system. * This server is used to execute agents that are created by the AutoGPT system.
* OpenAPI spec version: 0.1 * OpenAPI spec version: 0.1
*/ */
import type { ProviderName } from "./providerName";
import type { WebhookConfig } from "./webhookConfig"; import type { WebhookConfig } from "./webhookConfig";
export interface Webhook { export interface Webhook {
id?: string; id?: string;
user_id: string; user_id: string;
/** Provider name for integrations. Can be any string value, including custom provider names. */ provider: ProviderName;
provider: string;
credentials_id: string; credentials_id: string;
webhook_type: string; webhook_type: string;
resource: string; resource: string;

View File

@@ -18,9 +18,8 @@
"in": "path", "in": "path",
"required": true, "required": true,
"schema": { "schema": {
"type": "string", "$ref": "#/components/schemas/ProviderName",
"title": "The provider to initiate an OAuth flow for", "title": "The provider to initiate an OAuth flow for"
"description": "Provider name for integrations. Can be any string value, including custom provider names."
} }
}, },
{ {
@@ -65,9 +64,8 @@
"in": "path", "in": "path",
"required": true, "required": true,
"schema": { "schema": {
"type": "string", "$ref": "#/components/schemas/ProviderName",
"title": "The target provider for this OAuth exchange", "title": "The target provider for this OAuth exchange"
"description": "Provider name for integrations. Can be any string value, including custom provider names."
} }
} }
], ],
@@ -135,9 +133,8 @@
"in": "path", "in": "path",
"required": true, "required": true,
"schema": { "schema": {
"type": "string", "$ref": "#/components/schemas/ProviderName",
"title": "The provider to list credentials for", "title": "The provider to list credentials for"
"description": "Provider name for integrations. Can be any string value, including custom provider names."
} }
} }
], ],
@@ -176,9 +173,8 @@
"in": "path", "in": "path",
"required": true, "required": true,
"schema": { "schema": {
"type": "string", "$ref": "#/components/schemas/ProviderName",
"title": "The provider to create credentials for", "title": "The provider to create credentials for"
"description": "Provider name for integrations. Can be any string value, including custom provider names."
} }
} }
], ],
@@ -257,9 +253,8 @@
"in": "path", "in": "path",
"required": true, "required": true,
"schema": { "schema": {
"type": "string", "$ref": "#/components/schemas/ProviderName",
"title": "The provider to retrieve credentials for", "title": "The provider to retrieve credentials for"
"description": "Provider name for integrations. Can be any string value, including custom provider names."
} }
}, },
{ {
@@ -320,9 +315,8 @@
"in": "path", "in": "path",
"required": true, "required": true,
"schema": { "schema": {
"type": "string", "$ref": "#/components/schemas/ProviderName",
"title": "The provider to delete credentials for", "title": "The provider to delete credentials for"
"description": "Provider name for integrations. Can be any string value, including custom provider names."
} }
}, },
{ {
@@ -386,9 +380,8 @@
"in": "path", "in": "path",
"required": true, "required": true,
"schema": { "schema": {
"type": "string", "$ref": "#/components/schemas/ProviderName",
"title": "Provider where the webhook was registered", "title": "Provider where the webhook was registered"
"description": "Provider name for integrations. Can be any string value, including custom provider names."
} }
}, },
{ {
@@ -443,86 +436,6 @@
} }
} }
}, },
"/api/integrations/providers": {
"get": {
"tags": ["v1", "integrations"],
"summary": "List Providers",
"description": "Get a list of all available provider names.\n\nReturns both statically defined providers (from ProviderName enum)\nand dynamically registered providers (from SDK decorators).\n\nNote: The complete list of provider names is also available as a constant\nin the generated TypeScript client via PROVIDER_NAMES.",
"operationId": "getV1ListProviders",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"items": { "type": "string" },
"type": "array",
"title": "Response Getv1Listproviders"
}
}
}
}
}
}
},
"/api/integrations/providers/names": {
"get": {
"tags": ["v1", "integrations"],
"summary": "Get Provider Names",
"description": "Get all provider names in a structured format.\n\nThis endpoint is specifically designed to expose the provider names\nin the OpenAPI schema so that code generators like Orval can create\nappropriate TypeScript constants.",
"operationId": "getV1GetProviderNames",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProviderNamesResponse"
}
}
}
}
}
}
},
"/api/integrations/providers/constants": {
"get": {
"tags": ["v1", "integrations"],
"summary": "Get Provider Constants",
"description": "Get provider names as constants.\n\nThis endpoint returns a model with provider names as constants,\nspecifically designed for OpenAPI code generation tools to create\nTypeScript constants.",
"operationId": "getV1GetProviderConstants",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ProviderConstants" }
}
}
}
}
}
},
"/api/integrations/providers/enum-example": {
"get": {
"tags": ["v1", "integrations"],
"summary": "Get Provider Enum Example",
"description": "Example endpoint that uses the CompleteProviderNames enum.\n\nThis endpoint exists to ensure that the CompleteProviderNames enum is included\nin the OpenAPI schema, which will cause Orval to generate it as a\nTypeScript enum/constant.",
"operationId": "getV1GetProviderEnumExample",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProviderEnumResponse"
}
}
}
}
}
}
},
"/api/analytics/log_raw_metric": { "/api/analytics/log_raw_metric": {
"post": { "post": {
"tags": ["v1", "analytics"], "tags": ["v1", "analytics"],
@@ -3373,6 +3286,48 @@
} }
} }
}, },
"/api/library/agents/by-graph/{graph_id}": {
"get": {
"tags": ["v2", "library", "private"],
"summary": "Get Library Agent By Graph Id",
"operationId": "getV2GetLibraryAgentByGraphId",
"parameters": [
{
"name": "graph_id",
"in": "path",
"required": true,
"schema": { "type": "string", "title": "Graph Id" }
},
{
"name": "version",
"in": "query",
"required": false,
"schema": {
"anyOf": [{ "type": "integer" }, { "type": "null" }],
"title": "Version"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/LibraryAgent" }
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/HTTPValidationError" }
}
}
}
}
}
},
"/api/library/agents/marketplace/{store_listing_version_id}": { "/api/library/agents/marketplace/{store_listing_version_id}": {
"get": { "get": {
"tags": ["v2", "library", "private", "store, library"], "tags": ["v2", "library", "private", "store, library"],
@@ -4146,11 +4101,7 @@
"anyOf": [{ "type": "string" }, { "type": "null" }], "anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Title" "title": "Title"
}, },
"provider": { "provider": { "$ref": "#/components/schemas/ProviderName" },
"type": "string",
"title": "Provider",
"description": "Provider name for integrations. Can be any string value, including custom provider names."
},
"type": { "type": {
"type": "string", "type": "string",
"enum": ["api_key", "oauth2", "user_password", "host_scoped"], "enum": ["api_key", "oauth2", "user_password", "host_scoped"],
@@ -4160,8 +4111,8 @@
"type": "object", "type": "object",
"required": ["id", "provider", "type"], "required": ["id", "provider", "type"],
"title": "CredentialsMetaInput", "title": "CredentialsMetaInput",
"credentials_provider": ["string"], "credentials_provider": [],
"credentials_types": ["api_key", "oauth2", "user_password"] "credentials_types": []
}, },
"CredentialsMetaResponse": { "CredentialsMetaResponse": {
"properties": { "properties": {
@@ -4869,11 +4820,7 @@
}, },
"LibraryAgentTriggerInfo": { "LibraryAgentTriggerInfo": {
"properties": { "properties": {
"provider": { "provider": { "$ref": "#/components/schemas/ProviderName" },
"type": "string",
"title": "Provider",
"description": "Provider name for integrations. Can be any string value, including custom provider names."
},
"config_schema": { "config_schema": {
"additionalProperties": true, "additionalProperties": true,
"type": "object", "type": "object",
@@ -5288,6 +5235,7 @@
"AGENT_INPUT", "AGENT_INPUT",
"CONGRATS", "CONGRATS",
"GET_RESULTS", "GET_RESULTS",
"RUN_AGENTS",
"MARKETPLACE_VISIT", "MARKETPLACE_VISIT",
"MARKETPLACE_ADD_AGENT", "MARKETPLACE_ADD_AGENT",
"MARKETPLACE_RUN_AGENT", "MARKETPLACE_RUN_AGENT",
@@ -5675,44 +5623,51 @@
"required": ["name", "username", "description", "links"], "required": ["name", "username", "description", "links"],
"title": "ProfileDetails" "title": "ProfileDetails"
}, },
"ProviderConstants": { "ProviderName": {
"properties": { "type": "string",
"PROVIDER_NAMES": { "enum": [
"additionalProperties": { "type": "string" }, "aiml_api",
"type": "object", "anthropic",
"title": "Provider Names", "apollo",
"description": "All available provider names as a constant mapping" "compass",
} "discord",
}, "d_id",
"type": "object", "e2b",
"title": "ProviderConstants", "exa",
"description": "Model that exposes all provider names as a constant in the OpenAPI schema.\nThis is designed to be converted by Orval into a TypeScript constant." "fal",
}, "generic_webhook",
"ProviderEnumResponse": { "github",
"properties": { "google",
"provider": { "google_maps",
"type": "string", "groq",
"title": "Provider", "http",
"description": "A provider name from the complete list of providers" "hubspot",
} "ideogram",
}, "jina",
"type": "object", "linear",
"required": ["provider"], "llama_api",
"title": "ProviderEnumResponse", "medium",
"description": "Response containing a provider from the enum." "mem0",
}, "notion",
"ProviderNamesResponse": { "nvidia",
"properties": { "ollama",
"providers": { "openai",
"items": { "type": "string" }, "openweathermap",
"type": "array", "open_router",
"title": "Providers", "pinecone",
"description": "List of all available provider names" "reddit",
} "replicate",
}, "revid",
"type": "object", "screenshotone",
"title": "ProviderNamesResponse", "slant3d",
"description": "Response containing list of all provider names." "smartlead",
"smtp",
"twitter",
"todoist",
"unreal_speech",
"zerobounce"
],
"title": "ProviderName"
}, },
"RefundRequest": { "RefundRequest": {
"properties": { "properties": {
@@ -6486,11 +6441,7 @@
"properties": { "properties": {
"id": { "type": "string", "title": "Id" }, "id": { "type": "string", "title": "Id" },
"user_id": { "type": "string", "title": "User Id" }, "user_id": { "type": "string", "title": "User Id" },
"provider": { "provider": { "$ref": "#/components/schemas/ProviderName" },
"type": "string",
"title": "Provider",
"description": "Provider name for integrations. Can be any string value, including custom provider names."
},
"credentials_id": { "type": "string", "title": "Credentials Id" }, "credentials_id": { "type": "string", "title": "Credentials Id" },
"webhook_type": { "type": "string", "title": "Webhook Type" }, "webhook_type": { "type": "string", "title": "Webhook Type" },
"resource": { "type": "string", "title": "Resource" }, "resource": { "type": "string", "title": "Resource" },

View File

@@ -1,131 +0,0 @@
import * as React from "react";
import Link from "next/link";
import { ProfilePopoutMenu } from "./ProfilePopoutMenu";
import { IconType, IconLogIn, IconAutoGPTLogo } from "@/components/ui/icons";
import { MobileNavBar } from "./MobileNavBar";
import { Button } from "./Button";
import Wallet from "./Wallet";
import { ProfileDetails } from "@/lib/autogpt-server-api/types";
import { NavbarLink } from "./NavbarLink";
import BackendAPI from "@/lib/autogpt-server-api";
import { getServerUser } from "@/lib/supabase/server/getServerUser";
// Disable theme toggle for now
// import { ThemeToggle } from "./ThemeToggle";
interface NavLink {
name: string;
href: string;
}
interface NavbarProps {
links: NavLink[];
menuItemGroups: {
groupName?: string;
items: {
icon: IconType;
text: string;
href?: string;
onClick?: () => void;
}[];
}[];
}
async function getProfileData() {
const api = new BackendAPI();
const profile = await Promise.resolve(api.getStoreProfile());
return profile;
}
export const Navbar = async ({ links, menuItemGroups }: NavbarProps) => {
const { user } = await getServerUser();
const isLoggedIn = user !== null;
let profile: ProfileDetails | null = null;
if (isLoggedIn) {
profile = await getProfileData();
}
return (
<>
<nav className="sticky top-0 z-40 mx-[16px] hidden h-16 items-center justify-between rounded-bl-2xl rounded-br-2xl border border-white/50 bg-white/5 py-3 pl-6 pr-3 backdrop-blur-[26px] dark:border-gray-700 dark:bg-gray-900 md:inline-flex">
<div className="flex items-center gap-11">
<div className="relative h-10 w-[88.87px]">
<IconAutoGPTLogo className="h-full w-full" />
</div>
{links.map((link) => (
<NavbarLink key={link.name} name={link.name} href={link.href} />
))}
</div>
{/* Profile section */}
<div className="flex items-center gap-4">
{isLoggedIn ? (
<div className="flex items-center gap-4">
{profile && <Wallet />}
<ProfilePopoutMenu
menuItemGroups={menuItemGroups}
userName={profile?.username}
userEmail={profile?.name}
avatarSrc={profile?.avatar_url}
/>
</div>
) : (
<Link href="/login">
<Button
size="sm"
className="flex items-center justify-end space-x-2"
>
<IconLogIn className="h-5 h-[48px] w-5" />
<span>Log In</span>
</Button>
</Link>
)}
{/* <ThemeToggle /> */}
</div>
</nav>
{/* Mobile Navbar - Adjust positioning */}
<>
{isLoggedIn ? (
<div className="fixed right-4 top-4 z-50">
<MobileNavBar
userName={profile?.username}
menuItemGroups={[
{
groupName: "Navigation",
items: links.map((link) => ({
icon:
link.name === "Marketplace"
? IconType.Marketplace
: link.name === "Library"
? IconType.Library
: link.name === "Build"
? IconType.Builder
: link.name === "Monitor"
? IconType.Library
: IconType.LayoutDashboard,
text: link.name,
href: link.href,
})),
},
...menuItemGroups,
]}
userEmail={profile?.name}
avatarSrc={profile?.avatar_url}
/>
</div>
) : (
<Link
href="/login"
className="fixed right-4 top-4 z-50 mt-4 inline-flex h-8 items-center justify-end rounded-lg pr-4 md:hidden"
>
<Button size="sm" className="flex items-center space-x-2">
<IconLogIn className="h-5 w-5" />
<span>Log In</span>
</Button>
</Link>
)}
</>
</>
);
};

View File

@@ -1,66 +0,0 @@
"use client";
import Link from "next/link";
import {
IconShoppingCart,
IconBoxes,
IconLibrary,
IconLaptop,
} from "@/components/ui/icons";
import { usePathname } from "next/navigation";
interface NavbarLinkProps {
name: string;
href: string;
}
export const NavbarLink = ({ name, href }: NavbarLinkProps) => {
const pathname = usePathname();
const parts = pathname.split("/");
const activeLink = "/" + (parts.length > 2 ? parts[2] : parts[1]);
return (
<Link
href={href}
data-testid={`navbar-link-${name.toLowerCase()}`}
className="font-poppins text-[20px] leading-[28px]"
>
<div
className={`h-[48px] px-5 py-4 ${
activeLink === href
? "rounded-2xl bg-neutral-800 dark:bg-neutral-200"
: ""
} flex items-center justify-start gap-3`}
>
{href === "/marketplace" && (
<IconShoppingCart
className={`h-6 w-6 ${activeLink === href ? "text-white dark:text-black" : ""}`}
/>
)}
{href === "/build" && (
<IconBoxes
className={`h-6 w-6 ${activeLink === href ? "text-white dark:text-black" : ""}`}
/>
)}
{href === "/monitor" && (
<IconLaptop
className={`h-6 w-6 ${activeLink === href ? "text-white dark:text-black" : ""}`}
/>
)}
{href === "/library" && (
<IconLibrary
className={`h-6 w-6 ${activeLink === href ? "text-white dark:text-black" : ""}`}
/>
)}
<div
className={`hidden font-poppins text-[20px] font-medium leading-[28px] lg:block ${
activeLink === href
? "text-neutral-50 dark:text-neutral-900"
: "text-neutral-900 dark:text-neutral-50"
}`}
>
{name}
</div>
</div>
</Link>
);
};

View File

@@ -109,7 +109,7 @@ export default function Wallet() {
<button <button
ref={walletRef} ref={walletRef}
className={cn( className={cn(
"relative flex items-center gap-1 rounded-md bg-zinc-200 px-3 py-2 text-sm transition-colors duration-200 hover:bg-zinc-300", "relative flex items-center gap-1 rounded-md bg-zinc-50 px-3 py-2 text-sm",
)} )}
onClick={onWalletOpen} onClick={onWalletOpen}
> >

View File

@@ -56,7 +56,7 @@ const meta: Meta<typeof Button> = {
}; };
export default meta; export default meta;
type Story = StoryObj<typeof meta>; type Story = StoryObj<typeof Button>;
// Basic variants // Basic variants
export const Primary: Story = { export const Primary: Story = {

View File

@@ -1,82 +1,78 @@
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { CircleNotchIcon } from "@phosphor-icons/react/dist/ssr"; import { CircleNotchIcon } from "@phosphor-icons/react/dist/ssr";
import { cva, type VariantProps } from "class-variance-authority"; import Link, { type LinkProps } from "next/link";
import React from "react"; import React from "react";
import { ButtonProps, extendedButtonVariants } from "./helpers";
// Extended button variants based on our design system export function Button(props: ButtonProps) {
const extendedButtonVariants = cva( const {
"inline-flex items-center justify-center whitespace-nowrap font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-neutral-950 disabled:pointer-events-none disabled:opacity-50 font-['Geist'] leading-snug border", className,
{ variant,
variants: { size,
variant: { loading = false,
primary: leftIcon,
"bg-zinc-800 border-zinc-800 text-white hover:bg-zinc-900 hover:border-zinc-900 rounded-full disabled:text-white disabled:bg-zinc-200 disabled:border-zinc-200 disabled:opacity-1", rightIcon,
secondary: children,
"bg-zinc-100 border-zinc-100 text-black hover:bg-zinc-300 hover:border-zinc-300 rounded-full disabled:text-zinc-300 disabled:bg-zinc-50 disabled:border-zinc-50 disabled:opacity-1", as = "button",
destructive: ...restProps
"bg-red-500 border-red-500 text-white hover:bg-red-600 hover:border-red-600 rounded-full disabled:text-white disabled:bg-zinc-200 disabled:border-zinc-200 disabled:opacity-1", } = props;
outline:
"bg-transparent border-zinc-700 text-black hover:bg-zinc-100 hover:border-zinc-700 rounded-full disabled:border-zinc-200 disabled:text-zinc-200 disabled:opacity-1",
ghost:
"bg-transparent border-transparent text-black hover:bg-zinc-50 hover:border-zinc-50 rounded-full disabled:text-zinc-200 disabled:opacity-1",
icon: "bg-white text-black border border-zinc-600 hover:bg-zinc-100 rounded-[96px] disabled:opacity-1",
},
size: {
small: "px-3 py-2 text-sm gap-1.5 h-[2.25rem]",
large: "min-w-20 px-4 py-3 text-sm gap-2",
icon: "p-3",
},
},
defaultVariants: {
variant: "primary",
size: "large",
},
},
);
export interface ButtonProps const disabled = "disabled" in props ? props.disabled : false;
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof extendedButtonVariants> {
loading?: boolean;
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
asChild?: boolean;
}
function Button({
className,
variant,
size,
loading = false,
leftIcon,
rightIcon,
children,
disabled,
...props
}: ButtonProps) {
const isDisabled = disabled; const isDisabled = disabled;
const buttonContent = (
<>
{loading && (
<CircleNotchIcon className="h-4 w-4 animate-spin" weight="bold" />
)}
{!loading && leftIcon}
{children}
{!loading && rightIcon}
</>
);
if (loading) { if (loading) {
return variant === "ghost" ? ( const loadingClassName =
<button variant === "ghost"
? cn(
extendedButtonVariants({ variant, size, className }),
"pointer-events-none",
)
: cn(
extendedButtonVariants({ variant: "primary", size, className }),
"pointer-events-none border-zinc-500 bg-zinc-500 text-white",
);
return as === "NextLink" ? (
<Link
{...(restProps as LinkProps)}
className={loadingClassName}
aria-disabled="true"
>
<CircleNotchIcon className="h-4 w-4 animate-spin" weight="bold" />
{children}
</Link>
) : (
<button className={loadingClassName} disabled>
<CircleNotchIcon className="h-4 w-4 animate-spin" weight="bold" />
{children}
</button>
);
}
if (as === "NextLink") {
return (
<Link
{...(restProps as LinkProps)}
className={cn( className={cn(
extendedButtonVariants({ variant, size, className }), extendedButtonVariants({ variant, size, className }),
"pointer-events-none", loading && "pointer-events-none",
isDisabled && "pointer-events-none opacity-50",
)} )}
aria-disabled={isDisabled}
> >
<CircleNotchIcon className="h-4 w-4 animate-spin" weight="bold" /> {buttonContent}
{children} </Link>
</button>
) : (
<button
className={cn(
extendedButtonVariants({ variant: "primary", size, className }),
"pointer-events-none border-zinc-500 bg-zinc-500 text-white",
)}
>
<CircleNotchIcon className="h-4 w-4 animate-spin" weight="bold" />
{children}
</button>
); );
} }
@@ -87,18 +83,9 @@ function Button({
loading && "pointer-events-none", loading && "pointer-events-none",
)} )}
disabled={isDisabled} disabled={isDisabled}
{...props} {...(restProps as React.ButtonHTMLAttributes<HTMLButtonElement>)}
> >
{loading && ( {buttonContent}
<CircleNotchIcon className="h-4 w-4 animate-spin" weight="bold" />
)}
{!loading && leftIcon}
{children}
{!loading && rightIcon}
</button> </button>
); );
} }
Button.displayName = "Button";
export { Button, extendedButtonVariants };

View File

@@ -0,0 +1,55 @@
import { cva, VariantProps } from "class-variance-authority";
import { LinkProps } from "next/link";
// Extended button variants based on our design system
export const extendedButtonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-neutral-950 disabled:pointer-events-none disabled:opacity-50 font-sans leading-snug border",
{
variants: {
variant: {
primary:
"bg-zinc-800 border-zinc-800 text-white hover:bg-zinc-900 hover:border-zinc-900 rounded-full disabled:text-white disabled:bg-zinc-200 disabled:border-zinc-200 disabled:opacity-1",
secondary:
"bg-zinc-100 border-zinc-100 text-black hover:bg-zinc-300 hover:border-zinc-300 rounded-full disabled:text-zinc-300 disabled:bg-zinc-50 disabled:border-zinc-50 disabled:opacity-1",
destructive:
"bg-red-500 border-red-500 text-white hover:bg-red-600 hover:border-red-600 rounded-full disabled:text-white disabled:bg-zinc-200 disabled:border-zinc-200 disabled:opacity-1",
outline:
"bg-transparent border-zinc-700 text-black hover:bg-zinc-100 hover:border-zinc-700 rounded-full disabled:border-zinc-200 disabled:text-zinc-200 disabled:opacity-1",
ghost:
"bg-transparent border-transparent text-black hover:bg-zinc-50 hover:border-zinc-50 rounded-full disabled:text-zinc-200 disabled:opacity-1",
icon: "bg-white text-black border border-zinc-600 hover:bg-zinc-100 rounded-[96px] disabled:opacity-1",
},
size: {
small: "px-3 py-2 text-sm gap-1.5 h-[2.25rem]",
large: "min-w-20 px-4 py-3 text-sm gap-2",
icon: "p-3",
},
},
defaultVariants: {
variant: "primary",
size: "large",
},
},
);
type BaseButtonProps = {
loading?: boolean;
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
asChild?: boolean;
} & VariantProps<typeof extendedButtonVariants>;
type ButtonAsButton = BaseButtonProps &
React.ButtonHTMLAttributes<HTMLButtonElement> & {
as?: "button";
href?: never;
};
type ButtonAsLink = BaseButtonProps &
Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, keyof LinkProps> &
LinkProps & {
as: "NextLink";
disabled?: boolean;
};
export type ButtonProps = ButtonAsButton | ButtonAsLink;

View File

@@ -0,0 +1,84 @@
import { IconAutoGPTLogo, IconType } from "@/components/ui/icons";
import Wallet from "../../agptui/Wallet";
import { AccountMenu } from "./components/AccountMenu/AccountMenu";
import { LoginButton } from "./components/LoginButton";
import { MobileNavBar } from "./components/MobileNavbar/MobileNavBar";
import { NavbarLink } from "./components/NavbarLink";
import { accountMenuItems, loggedInLinks, loggedOutLinks } from "./helpers";
import { getNavbarAccountData } from "./data";
export async function Navbar() {
const { profile, isLoggedIn } = await getNavbarAccountData();
return (
<>
<nav className="sticky top-0 z-40 hidden h-16 items-center rounded-bl-2xl rounded-br-2xl border border-white/50 bg-[#f3f4f6]/20 p-3 backdrop-blur-[26px] md:inline-flex">
{/* Left section */}
<div className="flex flex-1 items-center gap-6">
{isLoggedIn
? loggedInLinks.map((link) => (
<NavbarLink key={link.name} name={link.name} href={link.href} />
))
: loggedOutLinks.map((link) => (
<NavbarLink key={link.name} name={link.name} href={link.href} />
))}
</div>
{/* Centered logo */}
<div className="absolute left-1/2 top-1/2 h-10 w-[88.87px] -translate-x-1/2 -translate-y-1/2">
<IconAutoGPTLogo className="h-full w-full" />
</div>
{/* Right section */}
<div className="flex flex-1 items-center justify-end gap-4">
{isLoggedIn ? (
<div className="flex items-center gap-4">
{profile && <Wallet />}
<AccountMenu
userName={profile?.username}
userEmail={profile?.name}
avatarSrc={profile?.avatar_url ?? ""}
menuItemGroups={accountMenuItems}
/>
</div>
) : (
<LoginButton />
)}
{/* <ThemeToggle /> */}
</div>
</nav>
{/* Mobile Navbar - Adjust positioning */}
<>
{isLoggedIn ? (
<div className="fixed right-4 top-4 z-50">
<MobileNavBar
userName={profile?.username}
menuItemGroups={[
{
groupName: "Navigation",
items: loggedInLinks.map((link) => ({
icon:
link.name === "Marketplace"
? IconType.Marketplace
: link.name === "Library"
? IconType.Library
: link.name === "Build"
? IconType.Builder
: link.name === "Monitor"
? IconType.Library
: IconType.LayoutDashboard,
text: link.name,
href: link.href,
})),
},
...accountMenuItems,
]}
userEmail={profile?.name}
avatarSrc={profile?.avatar_url ?? ""}
/>
</div>
) : null}
</>
</>
);
}

View File

@@ -1,74 +1,31 @@
import * as React from "react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { import {
Popover, Popover,
PopoverTrigger,
PopoverContent, PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"; } from "@/components/ui/popover";
import {
IconType,
IconEdit,
IconLayoutDashboard,
IconUploadCloud,
IconSettings,
IconLogOut,
IconRefresh,
IconMarketplace,
IconLibrary,
IconBuilder,
} from "../ui/icons";
import Link from "next/link"; import Link from "next/link";
import { ProfilePopoutMenuLogoutButton } from "./ProfilePopoutMenuLogoutButton"; import * as React from "react";
import { PublishAgentPopout } from "./composite/PublishAgentPopout"; import { PublishAgentPopout } from "../../../../agptui/composite/PublishAgentPopout";
import { getAccountMenuOptionIcon, MenuItemGroup } from "../../helpers";
import { AccountLogoutOption } from "./components/AccountLogoutOption";
interface ProfilePopoutMenuProps { interface Props {
userName?: string; userName?: string;
userEmail?: string; userEmail?: string;
avatarSrc?: string; avatarSrc?: string;
hideNavBarUsername?: boolean; hideNavBarUsername?: boolean;
menuItemGroups: { menuItemGroups: MenuItemGroup[];
groupName?: string;
items: {
icon: IconType;
text: string;
href?: string;
onClick?: () => void;
}[];
}[];
} }
export function ProfilePopoutMenu({ export function AccountMenu({
userName, userName,
userEmail, userEmail,
avatarSrc, avatarSrc,
menuItemGroups, menuItemGroups,
}: ProfilePopoutMenuProps) { }: Props) {
const popupId = React.useId(); const popupId = React.useId();
const getIcon = (icon: IconType) => {
const iconClass = "w-6 h-6";
switch (icon) {
case IconType.LayoutDashboard:
return <IconLayoutDashboard className={iconClass} />;
case IconType.UploadCloud:
return <IconUploadCloud className={iconClass} />;
case IconType.Edit:
return <IconEdit className={iconClass} />;
case IconType.Settings:
return <IconSettings className={iconClass} />;
case IconType.LogOut:
return <IconLogOut className={iconClass} />;
case IconType.Marketplace:
return <IconMarketplace className={iconClass} />;
case IconType.Library:
return <IconLibrary className={iconClass} />;
case IconType.Builder:
return <IconBuilder className={iconClass} />;
default:
return <IconRefresh className={iconClass} />;
}
};
return ( return (
<Popover> <Popover>
<PopoverTrigger asChild> <PopoverTrigger asChild>
@@ -127,7 +84,7 @@ export function ProfilePopoutMenu({
className="inline-flex w-full items-center justify-start gap-2.5" className="inline-flex w-full items-center justify-start gap-2.5"
> >
<div className="relative h-6 w-6"> <div className="relative h-6 w-6">
{getIcon(item.icon)} {getAccountMenuOptionIcon(item.icon)}
</div> </div>
<div className="font-sans text-base font-medium leading-normal text-neutral-800 dark:text-neutral-200"> <div className="font-sans text-base font-medium leading-normal text-neutral-800 dark:text-neutral-200">
{item.text} {item.text}
@@ -135,7 +92,7 @@ export function ProfilePopoutMenu({
</Link> </Link>
); );
} else if (item.text === "Log out") { } else if (item.text === "Log out") {
return <ProfilePopoutMenuLogoutButton key={itemIndex} />; return <AccountLogoutOption key={itemIndex} />;
} else if (item.text === "Publish an agent") { } else if (item.text === "Publish an agent") {
return ( return (
<PublishAgentPopout <PublishAgentPopout
@@ -143,7 +100,7 @@ export function ProfilePopoutMenu({
trigger={ trigger={
<div className="inline-flex w-full items-center justify-start gap-2.5"> <div className="inline-flex w-full items-center justify-start gap-2.5">
<div className="relative h-6 w-6"> <div className="relative h-6 w-6">
{getIcon(item.icon)} {getAccountMenuOptionIcon(item.icon)}
</div> </div>
<div className="font-sans text-base font-medium leading-normal text-neutral-800 dark:text-neutral-200"> <div className="font-sans text-base font-medium leading-normal text-neutral-800 dark:text-neutral-200">
{item.text} {item.text}
@@ -165,7 +122,7 @@ export function ProfilePopoutMenu({
tabIndex={0} tabIndex={0}
> >
<div className="relative h-6 w-6"> <div className="relative h-6 w-6">
{getIcon(item.icon)} {getAccountMenuOptionIcon(item.icon)}
</div> </div>
<div className="font-sans text-base font-medium leading-normal text-neutral-800 dark:text-neutral-200"> <div className="font-sans text-base font-medium leading-normal text-neutral-800 dark:text-neutral-200">
{item.text} {item.text}

View File

@@ -1,14 +1,14 @@
"use client"; "use client";
import { IconLogOut } from "@/components/ui/icons"; import { IconLogOut } from "@/components/ui/icons";
import { LoadingSpinner } from "@/components/ui/loading";
import { useSupabase } from "@/lib/supabase/hooks/useSupabase"; import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import * as Sentry from "@sentry/nextjs"; import * as Sentry from "@sentry/nextjs";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useTransition } from "react"; import { useTransition } from "react";
import { LoadingSpinner } from "../ui/loading"; import { toast } from "@/components/molecules/Toast/use-toast";
import { toast } from "../molecules/Toast/use-toast";
export function ProfilePopoutMenuLogoutButton() { export function AccountLogoutOption() {
const router = useRouter(); const router = useRouter();
const [isPending, startTransition] = useTransition(); const [isPending, startTransition] = useTransition();
const supabase = useSupabase(); const supabase = useSupabase();

View File

@@ -0,0 +1,29 @@
"use client";
import { Button } from "@/components/atoms/Button/Button";
import { SignInIcon } from "@phosphor-icons/react/dist/ssr";
import { usePathname, useRouter } from "next/navigation";
export function LoginButton() {
const router = useRouter();
const pathname = usePathname();
const isLoginPage = pathname.includes("/login");
if (isLoginPage) return null;
function handleLogin() {
router.push("/login");
}
return (
<Button
onClick={handleLogin}
size="small"
className="flex items-center justify-end space-x-2"
leftIcon={<SignInIcon className="h-5 w-5" />}
variant="secondary"
>
Log In
</Button>
);
}

View File

@@ -1,45 +1,26 @@
"use client"; "use client";
import * as React from "react"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import Link from "next/link"; import { Button } from "@/components/ui/button";
import { import {
Popover, Popover,
PopoverTrigger,
PopoverContent, PopoverContent,
PopoverPortal, PopoverPortal,
PopoverTrigger,
} from "@/components/ui/popover"; } from "@/components/ui/popover";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import {
IconType,
IconMenu,
IconChevronUp,
IconEdit,
IconLayoutDashboard,
IconUploadCloud,
IconSettings,
IconLogOut,
IconMarketplace,
IconLibrary,
IconBuilder,
} from "../ui/icons";
import { AnimatePresence, motion } from "framer-motion"; import { AnimatePresence, motion } from "framer-motion";
import { Button } from "@/components/ui/button";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import * as React from "react";
import { IconChevronUp, IconMenu } from "../../../../ui/icons";
import { MenuItemGroup } from "../../helpers";
import { MobileNavbarMenuItem } from "./components/MobileNavbarMenuItem";
interface MobileNavBarProps { interface MobileNavBarProps {
userName?: string; userName?: string;
userEmail?: string; userEmail?: string;
avatarSrc?: string; avatarSrc?: string;
menuItemGroups: { menuItemGroups: MenuItemGroup[];
groupName?: string;
items: {
icon: IconType;
text: string;
href?: string;
onClick?: () => void;
}[];
}[];
} }
const Overlay = React.forwardRef<HTMLDivElement, { children: React.ReactNode }>( const Overlay = React.forwardRef<HTMLDivElement, { children: React.ReactNode }>(
@@ -49,76 +30,15 @@ const Overlay = React.forwardRef<HTMLDivElement, { children: React.ReactNode }>(
</div> </div>
), ),
); );
Overlay.displayName = "Overlay"; Overlay.displayName = "Overlay";
const PopoutMenuItem: React.FC<{ export function MobileNavBar({
icon: IconType;
isActive: boolean;
text: React.ReactNode;
href?: string;
onClick?: () => void;
}> = ({ icon, isActive, text, href, onClick }) => {
const getIcon = (iconType: IconType) => {
const iconClass = "w-6 h-6 relative";
switch (iconType) {
case IconType.Marketplace:
return <IconMarketplace className={iconClass} />;
case IconType.Library:
return <IconLibrary className={iconClass} />;
case IconType.Builder:
return <IconBuilder className={iconClass} />;
case IconType.Edit:
return <IconEdit className={iconClass} />;
case IconType.LayoutDashboard:
return <IconLayoutDashboard className={iconClass} />;
case IconType.UploadCloud:
return <IconUploadCloud className={iconClass} />;
case IconType.Settings:
return <IconSettings className={iconClass} />;
case IconType.LogOut:
return <IconLogOut className={iconClass} />;
default:
return null;
}
};
const content = (
<div className="inline-flex w-full items-center justify-start gap-4 hover:rounded hover:bg-[#e0e0e0] dark:hover:bg-[#3a3a3a]">
{getIcon(icon)}
<div className="relative">
<div
className={`font-sans text-base font-normal leading-7 text-[#474747] dark:text-[#cfcfcf] ${isActive ? "font-semibold text-[#272727] dark:text-[#ffffff]" : "text-[#474747] dark:text-[#cfcfcf]"}`}
>
{text}
</div>
{isActive && (
<div className="absolute bottom-[-4px] left-0 h-[2px] w-full bg-[#272727] dark:bg-[#ffffff]"></div>
)}
</div>
</div>
);
if (onClick)
return (
<div className="w-full" onClick={onClick}>
{content}
</div>
);
if (href)
return (
<Link href={href} className="w-full">
{content}
</Link>
);
return content;
};
export const MobileNavBar: React.FC<MobileNavBarProps> = ({
userName, userName,
userEmail, userEmail,
avatarSrc, avatarSrc,
menuItemGroups, menuItemGroups,
}) => { }: MobileNavBarProps) {
const [isOpen, setIsOpen] = React.useState(false); const [isOpen, setIsOpen] = React.useState(false);
const pathname = usePathname(); const pathname = usePathname();
const parts = pathname.split("/"); const parts = pathname.split("/");
@@ -173,7 +93,7 @@ export const MobileNavBar: React.FC<MobileNavBarProps> = ({
{menuItemGroups.map((group, groupIndex) => ( {menuItemGroups.map((group, groupIndex) => (
<React.Fragment key={groupIndex}> <React.Fragment key={groupIndex}>
{group.items.map((item, itemIndex) => ( {group.items.map((item, itemIndex) => (
<PopoutMenuItem <MobileNavbarMenuItem
key={itemIndex} key={itemIndex}
icon={item.icon} icon={item.icon}
isActive={item.href === activeLink} isActive={item.href === activeLink}
@@ -194,4 +114,4 @@ export const MobileNavBar: React.FC<MobileNavBarProps> = ({
</AnimatePresence> </AnimatePresence>
</Popover> </Popover>
); );
}; }

View File

@@ -0,0 +1,55 @@
import { IconType } from "@/components/ui/icons";
import { cn } from "@/lib/utils";
import Link from "next/link";
import { getAccountMenuOptionIcon } from "../../../helpers";
interface Props {
icon: IconType;
isActive: boolean;
text: string;
href?: string;
onClick?: () => void;
}
export function MobileNavbarMenuItem({
icon,
isActive,
text,
href,
onClick,
}: Props) {
const content = (
<div className="inline-flex w-full items-center justify-start gap-4 hover:rounded hover:bg-[#e0e0e0] dark:hover:bg-[#3a3a3a]">
{getAccountMenuOptionIcon(icon)}
<div className="relative">
<div
className={cn(
"font-sans text-base font-normal leading-7",
isActive
? "font-semibold text-[#272727] dark:text-[#ffffff]"
: "text-[#474747] dark:text-[#cfcfcf]",
)}
>
{text}
</div>
{isActive && (
<div className="absolute bottom-[-4px] left-0 h-[2px] w-full bg-[#272727] dark:bg-[#ffffff]"></div>
)}
</div>
</div>
);
if (onClick)
return (
<div className="w-full" onClick={onClick}>
{content}
</div>
);
if (href)
return (
<Link href={href} className="w-full">
{content}
</Link>
);
return content;
}

View File

@@ -0,0 +1,68 @@
"use client";
import { IconLaptop } from "@/components/ui/icons";
import { cn } from "@/lib/utils";
import {
CubeIcon,
HouseIcon,
StorefrontIcon,
} from "@phosphor-icons/react/dist/ssr";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { Text } from "../../../atoms/Text/Text";
interface Props {
name: string;
href: string;
}
export function NavbarLink({ name, href }: Props) {
const pathname = usePathname();
const isActive = pathname.includes(href);
return (
<Link
href={href}
data-testid={`navbar-link-${name.toLowerCase()}`}
className="font-poppins text-[20px] leading-[28px]"
>
<div
className={cn(
"flex items-center justify-start gap-1 p-2",
isActive &&
"rounded-small bg-neutral-800 p-2 transition-all duration-300 dark:bg-neutral-200",
)}
>
{href === "/marketplace" && (
<StorefrontIcon
className={cn("h-6 w-6", isActive && "text-white dark:text-black")}
/>
)}
{href === "/build" && (
<CubeIcon
className={cn("h-6 w-6", isActive && "text-white dark:text-black")}
/>
)}
{href === "/monitor" && (
<IconLaptop
className={cn("h-6 w-6", isActive && "text-white dark:text-black")}
/>
)}
{href === "/library" && (
<HouseIcon
className={cn("h-6 w-6", isActive && "text-white dark:text-black")}
/>
)}
<Text
variant="body-medium"
className={cn(
"hidden lg:block",
isActive ? "!text-white" : "!text-black",
)}
>
{name}
</Text>
</div>
</Link>
);
}

View File

@@ -0,0 +1,21 @@
import { IconAutoGPTLogo } from "@/components/ui/icons";
import { Skeleton } from "@/components/ui/skeleton";
export function NavbarLoading() {
return (
<nav className="sticky top-0 z-40 hidden h-16 items-center rounded-bl-2xl rounded-br-2xl border border-white/50 bg-white/5 p-3 backdrop-blur-[26px] md:inline-flex">
<div className="flex flex-1 items-center gap-6">
<Skeleton className="h-4 w-20 bg-white/20" />
<Skeleton className="h-4 w-16 bg-white/20" />
<Skeleton className="h-4 w-12 bg-white/20" />
</div>
<div className="absolute left-1/2 top-1/2 h-10 w-[88.87px] -translate-x-1/2 -translate-y-1/2">
<IconAutoGPTLogo className="h-full w-full" />
</div>
<div className="flex flex-1 items-center justify-end gap-4">
<Skeleton className="h-8 w-8 rounded-full bg-white/20" />
<Skeleton className="h-8 w-8 rounded-full bg-white/20" />
</div>
</nav>
);
}

View File

@@ -0,0 +1,29 @@
import { getV2GetUserProfile } from "@/app/api/__generated__/endpoints/store/store";
import { getServerUser } from "@/lib/supabase/server/getServerUser";
export async function getNavbarAccountData() {
const { user } = await getServerUser();
const isLoggedIn = Boolean(user);
if (!isLoggedIn) {
return {
profile: null,
isLoggedIn,
};
}
let profile = null;
try {
const profileResponse = await getV2GetUserProfile();
profile = profileResponse.data || null;
} catch (error) {
console.error("Error fetching profile:", error);
profile = null;
}
return {
profile,
isLoggedIn,
};
}

View File

@@ -0,0 +1,115 @@
import {
IconBuilder,
IconEdit,
IconLayoutDashboard,
IconLibrary,
IconLogOut,
IconMarketplace,
IconRefresh,
IconSettings,
IconType,
IconUploadCloud,
} from "@/components/ui/icons";
type Link = {
name: string;
href: string;
};
export const loggedInLinks: Link[] = [
{
name: "Marketplace",
href: "/marketplace",
},
{
name: "Library",
href: "/library",
},
{
name: "Build",
href: "/build",
},
];
export const loggedOutLinks: Link[] = [
{
name: "Marketplace",
href: "/marketplace",
},
];
export type MenuItemGroup = {
groupName?: string;
items: {
icon: IconType;
text: string;
href?: string;
onClick?: () => void;
}[];
};
export const accountMenuItems: MenuItemGroup[] = [
{
items: [
{
icon: IconType.Edit,
text: "Edit profile",
href: "/profile",
},
],
},
{
items: [
{
icon: IconType.LayoutDashboard,
text: "Creator Dashboard",
href: "/profile/dashboard",
},
{
icon: IconType.UploadCloud,
text: "Publish an agent",
},
],
},
{
items: [
{
icon: IconType.Settings,
text: "Settings",
href: "/profile/settings",
},
],
},
{
items: [
{
icon: IconType.LogOut,
text: "Log out",
},
],
},
];
export function getAccountMenuOptionIcon(icon: IconType) {
const iconClass = "w-6 h-6";
switch (icon) {
case IconType.LayoutDashboard:
return <IconLayoutDashboard className={iconClass} />;
case IconType.UploadCloud:
return <IconUploadCloud className={iconClass} />;
case IconType.Edit:
return <IconEdit className={iconClass} />;
case IconType.Settings:
return <IconSettings className={iconClass} />;
case IconType.LogOut:
return <IconLogOut className={iconClass} />;
case IconType.Marketplace:
return <IconMarketplace className={iconClass} />;
case IconType.Library:
return <IconLibrary className={iconClass} />;
case IconType.Builder:
return <IconBuilder className={iconClass} />;
default:
return <IconRefresh className={iconClass} />;
}
}

View File

@@ -71,6 +71,30 @@ export const colors = {
800: "#0c5a29", 800: "#0c5a29",
900: "#09441f", 900: "#09441f",
}, },
purple: {
50: "#f1ebfe",
100: "#d5c0fc",
200: "#c0a1fa",
300: "#a476f8",
400: "#925cf7",
500: "#7733f5",
600: "#6c2edf",
700: "#5424ae",
800: "#411c87",
900: "#321567",
},
pink: {
50: "#fdedf5",
100: "#f9c6df",
200: "#f6abd0",
300: "#f284bb",
400: "#f06dad",
500: "#ec4899",
600: "#d7428b",
700: "#a8336d",
800: "#822854",
900: "#631e40",
},
// Special semantic colors // Special semantic colors
white: "#fefefe", white: "#fefefe",

View File

@@ -14,54 +14,54 @@ const meta: Meta = {
export default meta; export default meta;
// Border radius scale data based on Figma design tokens // Border radius scale data based on Figma design tokens
// Custom naming convention: xs, s, m, l, xl, 2xl, full // Custom naming convention: xsmall, small, medium, large, xlarge, 2xlarge, full
const borderRadiusScale = [ const borderRadiusScale = [
{ {
name: "xs", name: "xsmall",
value: "0.25rem", value: "0.25rem",
rem: "0.25rem", rem: "0.25rem",
px: "4px", px: "4px",
class: "rounded-xs", class: "rounded-xsmall",
description: "Extra small - for subtle rounding", description: "Extra small - for subtle rounding",
}, },
{ {
name: "s", name: "small",
value: "0.5rem", value: "0.5rem",
rem: "0.5rem", rem: "0.5rem",
px: "8px", px: "8px",
class: "rounded-s", class: "rounded-small",
description: "Small - for cards and containers", description: "Small - for cards and containers",
}, },
{ {
name: "m", name: "medium",
value: "0.75rem", value: "0.75rem",
rem: "0.75rem", rem: "0.75rem",
px: "12px", px: "12px",
class: "rounded-m", class: "rounded-medium",
description: "Medium - for buttons and inputs", description: "Medium - for buttons and inputs",
}, },
{ {
name: "l", name: "large",
value: "1rem", value: "1rem",
rem: "1rem", rem: "1rem",
px: "16px", px: "16px",
class: "rounded-l", class: "rounded-large",
description: "Large - for panels and modals", description: "Large - for panels and modals",
}, },
{ {
name: "xl", name: "xlarge",
value: "1.25rem", value: "1.25rem",
rem: "1.25rem", rem: "1.25rem",
px: "20px", px: "20px",
class: "rounded-xl", class: "rounded-xlarge",
description: "Extra large - for hero sections", description: "Extra large - for hero sections",
}, },
{ {
name: "2xl", name: "2xlarge",
value: "1.5rem", value: "1.5rem",
rem: "1.5rem", rem: "1.5rem",
px: "24px", px: "24px",
class: "rounded-2xl", class: "rounded-2xlarge",
description: "2X large - for major containers", description: "2X large - for major containers",
}, },
{ {
@@ -84,10 +84,11 @@ export function AllVariants() {
Border Radius Border Radius
</Text> </Text>
<Text variant="large" className="text-zinc-600"> <Text variant="large" className="text-zinc-600">
Our border radius system uses a simplified naming convention (xs, s, Our border radius system uses a descriptive naming convention
m, l, xl, 2xl, full) based on our Figma design tokens. This creates (xsmall, small, medium, large, xlarge, 2xlarge, full) based on our
visual hierarchy and maintains design consistency across all Figma design tokens. This creates visual hierarchy and maintains
components. design consistency across all components while avoiding conflicts
with Tailwind&apos;s built-in classes.
</Text> </Text>
</div> </div>
@@ -137,9 +138,10 @@ export function AllVariants() {
</div> </div>
<Text variant="body" className="mb-4 text-zinc-600"> <Text variant="body" className="mb-4 text-zinc-600">
We use a custom border radius system based on our Figma design We use a custom border radius system based on our Figma design
tokens, with simplified naming (xs, s, m, l, xl, 2xl, full) that tokens, with descriptive naming (xsmall, small, medium, large,
provides consistent radius values optimized for our design xlarge, 2xlarge, full) that provides consistent radius values
system. optimized for our design system while avoiding conflicts with
Tailwind&apos;s built-in classes.
</Text> </Text>
</div> </div>
</div> </div>
@@ -188,7 +190,8 @@ export function AllVariants() {
<Text variant="body" className="mb-6 text-zinc-600"> <Text variant="body" className="mb-6 text-zinc-600">
All border radius values from our Figma design tokens. Each value All border radius values from our Figma design tokens. Each value
can be applied to all corners or specific corners/sides using our can be applied to all corners or specific corners/sides using our
simplified naming convention. descriptive naming convention (xsmall, small, medium, large, xlarge,
2xlarge, full).
</Text> </Text>
</div> </div>
@@ -232,30 +235,30 @@ export function AllVariants() {
<StoryCode <StoryCode
code={`// Border radius examples - Design System Tokens code={`// Border radius examples - Design System Tokens
<div className="rounded-xs">Extra small rounding (4px)</div> <div className="rounded-xsmall">Extra small rounding (4px)</div>
<div className="rounded-s">Small rounding (8px)</div> <div className="rounded-small">Small rounding (8px)</div>
<div className="rounded-m">Medium rounding (12px)</div> <div className="rounded-medium">Medium rounding (12px)</div>
<div className="rounded-l">Large rounding (16px)</div> <div className="rounded-large">Large rounding (16px)</div>
<div className="rounded-xl">Extra large rounding (20px)</div> <div className="rounded-xlarge">Extra large rounding (20px)</div>
<div className="rounded-2xl">2X large rounding (24px)</div> <div className="rounded-2xlarge">2X large rounding (24px)</div>
<div className="rounded-full">Pill buttons (circular)</div> <div className="rounded-full">Pill buttons (circular)</div>
// Directional rounding (works with all sizes) // Directional rounding (works with all sizes)
<div className="rounded-t-m">Top corners only</div> <div className="rounded-t-medium">Top corners only</div>
<div className="rounded-r-m">Right corners only</div> <div className="rounded-r-medium">Right corners only</div>
<div className="rounded-b-m">Bottom corners only</div> <div className="rounded-b-medium">Bottom corners only</div>
<div className="rounded-l-m">Left corners only</div> <div className="rounded-l-medium">Left corners only</div>
// Individual corners // Individual corners
<div className="rounded-tl-m">Top-left corner</div> <div className="rounded-tl-medium">Top-left corner</div>
<div className="rounded-tr-m">Top-right corner</div> <div className="rounded-tr-medium">Top-right corner</div>
<div className="rounded-bl-m">Bottom-left corner</div> <div className="rounded-bl-medium">Bottom-left corner</div>
<div className="rounded-br-m">Bottom-right corner</div> <div className="rounded-br-medium">Bottom-right corner</div>
// Usage recommendations // Usage recommendations
<button className="rounded-full">Pill Button</button> <button className="rounded-full">Pill Button</button>
<div className="rounded-m">Card Container</div> <div className="rounded-medium">Card Container</div>
<input className="rounded-s">Input Field</input>`} <input className="rounded-small">Input Field</input>`}
/> />
</div> </div>
</div> </div>

View File

@@ -36,6 +36,8 @@ const colorCategories = Object.entries(colors)
orange: "Warnings, notifications, and secondary call-to-actions", orange: "Warnings, notifications, and secondary call-to-actions",
yellow: "Highlights, cautions, and attention-grabbing elements", yellow: "Highlights, cautions, and attention-grabbing elements",
green: "Success states, confirmations, and positive actions", green: "Success states, confirmations, and positive actions",
purple: "Brand accents, premium features, and creative elements",
pink: "Highlights, special promotions, and playful interactions",
}; };
return { return {
@@ -312,6 +314,8 @@ export function AllVariants() {
<div className="bg-green-50 border-green-200 text-green-800">Success</div> <div className="bg-green-50 border-green-200 text-green-800">Success</div>
<div className="bg-red-50 border-red-200 text-red-800">Error</div> <div className="bg-red-50 border-red-200 text-red-800">Error</div>
<div className="bg-yellow-50 border-yellow-200 text-yellow-800">Warning</div> <div className="bg-yellow-50 border-yellow-200 text-yellow-800">Warning</div>
<div className="bg-purple-50 border-purple-200 text-purple-800">Premium</div>
<div className="bg-pink-50 border-pink-200 text-pink-800">Special</div>
// ❌ INCORRECT - Don't use these // ❌ INCORRECT - Don't use these
<div className="bg-blue-500 text-purple-600">❌ Not approved</div> <div className="bg-blue-500 text-purple-600">❌ Not approved</div>

View File

@@ -11,12 +11,12 @@ const ScrollArea = React.forwardRef<
>(({ className, children, ...props }, ref) => ( >(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root <ScrollAreaPrimitive.Root
ref={ref} ref={ref}
className={cn("relative overflow-hidden", className)} className={cn("relative", className)}
{...props} {...props}
> >
<ScrollAreaPrimitive.Viewport <ScrollAreaPrimitive.Viewport
className="h-full w-full rounded-[inherit]" className="h-full w-full rounded-[inherit]"
style={{ overflow: "scroll" }} style={{ overflowX: "hidden", overflowY: "scroll" }}
> >
{children} {children}
</ScrollAreaPrimitive.Viewport> </ScrollAreaPrimitive.Viewport>

View File

@@ -166,6 +166,8 @@ export async function serverLogout(options: ServerLogoutOptions = {}) {
scope: options.globalLogout ? "global" : "local", scope: options.globalLogout ? "global" : "local",
}); });
revalidatePath("/");
if (error) { if (error) {
console.error("Error logging out:", error); console.error("Error logging out:", error);
} }

View File

@@ -24,6 +24,7 @@ export function useSupabase() {
const [isUserLoading, setIsUserLoading] = useState(true); const [isUserLoading, setIsUserLoading] = useState(true);
const lastValidationRef = useRef<number>(0); const lastValidationRef = useRef<number>(0);
const isValidatingRef = useRef(false); const isValidatingRef = useRef(false);
const isLoggedIn = Boolean(user);
const supabase = useMemo(() => { const supabase = useMemo(() => {
try { try {
@@ -50,7 +51,9 @@ export function useSupabase() {
await serverLogout(options); await serverLogout(options);
} catch (error) { } catch (error) {
console.error("Error logging out:", error); console.error("Error logging out:", error);
router.push("/login"); } finally {
setUser(null);
router.refresh();
} }
} }
@@ -162,9 +165,9 @@ export function useSupabase() {
}, []); }, []);
return { return {
supabase, // Available for non-auth operations like real-time subscriptions
user, user,
isLoggedIn: !isUserLoading ? !!user : null, supabase, // Available for non-auth operations like real-time subscriptions
isLoggedIn,
isUserLoading, isUserLoading,
logOut, logOut,
validateSession: validateSessionServer, validateSession: validateSessionServer,

View File

@@ -73,56 +73,56 @@ const config = {
}, },
spacing: { spacing: {
// Tailwind spacing + custom sizes // Tailwind spacing + custom sizes
0: "0rem", 0: "0rem", // 0px
0.5: "0.125rem", 0.5: "0.125rem", // 2px
1: "0.25rem", 1: "0.25rem", // 4px
1.5: "0.375rem", 1.5: "0.375rem", // 6px
2: "0.5rem", 2: "0.5rem", // 8px
2.5: "0.625rem", 2.5: "0.625rem", // 10px
3: "0.75rem", 3: "0.75rem", // 12px
3.5: "0.875rem", 3.5: "0.875rem", // 14px
4: "1rem", 4: "1rem", // 16px
5: "1.25rem", 5: "1.25rem", // 20px
6: "1.5rem", 6: "1.5rem", // 24px
7: "1.75rem", 7: "1.75rem", // 28px
7.5: "1.875rem", 7.5: "1.875rem", // 30px
8: "2rem", 8: "2rem", // 32px
8.5: "2.125rem", 8.5: "2.125rem", // 34px
9: "2.25rem", 9: "2.25rem", // 36px
10: "2.5rem", 10: "2.5rem", // 40px
11: "2.75rem", 11: "2.75rem", // 44px
12: "3rem", 12: "3rem", // 48px
14: "3.5rem", 14: "3.5rem", // 56px
16: "4rem", 16: "4rem", // 64px
18: "4.5rem", 18: "4.5rem", // 72px
20: "5rem", 20: "5rem", // 80px
24: "6rem", 24: "6rem", // 96px
28: "7rem", 28: "7rem", // 112px
32: "8rem", 32: "8rem", // 128px
36: "9rem", 36: "9rem", // 144px
40: "10rem", 40: "10rem", // 160px
44: "11rem", 44: "11rem", // 176px
48: "12rem", 48: "12rem", // 192px
52: "13rem", 52: "13rem", // 208px
56: "14rem", 56: "14rem", // 224px
60: "15rem", 60: "15rem", // 240px
64: "16rem", 64: "16rem", // 256px
68: "17rem", 68: "17rem", // 272px
70: "17.5rem", 70: "17.5rem", // 280px
71: "17.75rem", 71: "17.75rem", // 284px
72: "18rem", 72: "18rem", // 288px
76: "19rem", 76: "19rem", // 304px
80: "20rem", 80: "20rem", // 320px
96: "24rem", 96: "24rem", // 384px
}, },
borderRadius: { borderRadius: {
// Design system border radius tokens from Figma // Design system border radius tokens from Figma
xs: "0.25rem", // 4px xsmall: "0.25rem", // 4px
s: "0.5rem", // 8px small: "0.5rem", // 8px
m: "0.75rem", // 12px medium: "0.75rem", // 12px
l: "1rem", // 16px large: "1rem", // 16px
xl: "1.25rem", // 20px xlarge: "1.25rem", // 20px
"2xl": "1.5rem", // 24px "2xlarge": "1.5rem", // 24px
full: "9999px", // For pill buttons full: "9999px", // For pill buttons
// Legacy values - kept for backward compatibility // Legacy values - kept for backward compatibility