chore: add colors and update UI

This commit is contained in:
Lluis Agusti
2025-07-02 21:09:25 +04:00
parent 255ee5d684
commit 42f51379a8
13 changed files with 823 additions and 29 deletions

View File

@@ -1,35 +1,26 @@
"use client";
import { IconAutoGPTLogo, IconType } from "@/components/ui/icons";
import { ProfileDetails } from "@/lib/autogpt-server-api/types";
import Wallet from "../../agptui/Wallet";
import { AccountMenu } from "./components/AccountMenu/AccountMenu";
import { AgentNotifications } from "./components/AgentNotifications/AgentNotifications";
import { LoginButton } from "./components/LoginButton";
import { MobileNavBar } from "./components/MobileNavbar/MobileNavBar";
import { NavbarLink } from "./components/NavbarLink";
import BackendAPI from "@/lib/autogpt-server-api";
import { getServerUser } from "@/lib/supabase/server/getServerUser";
import { LoginButton } from "./components/LoginButton";
import { NavbarLoading } from "./components/NavbarLoading";
import { accountMenuItems, loggedInLinks, loggedOutLinks } from "./helpers";
import { useNavbar } from "./useNavbar";
async function getProfileData() {
const api = new BackendAPI();
const profile = await Promise.resolve(api.getStoreProfile());
export function Navbar() {
const { isLoggedIn, profile, isLoading } = useNavbar();
return profile;
}
export async function Navbar() {
const { user } = await getServerUser();
const isLoggedIn = user !== null;
let profile: ProfileDetails | null = null;
if (isLoggedIn) {
profile = await getProfileData();
if (isLoading) {
return <NavbarLoading />;
}
return (
<>
<nav className="sticky top-0 z-40 mx-[16px] hidden h-16 items-center rounded-bl-2xl rounded-br-2xl border border-white/50 bg-white/5 p-3 backdrop-blur-[26px] dark:border-gray-700 dark:bg-gray-900 md:inline-flex">
<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">
{/* Left section */}
<div className="flex flex-1 items-center gap-6">
{isLoggedIn
@@ -50,11 +41,12 @@ export async function Navbar() {
<div className="flex flex-1 items-center justify-end gap-4">
{isLoggedIn ? (
<div className="flex items-center gap-4">
<AgentNotifications />
{profile && <Wallet />}
<AccountMenu
userName={profile?.username}
userEmail={profile?.name}
avatarSrc={profile?.avatar_url}
avatarSrc={profile?.avatar_url ?? ""}
menuItemGroups={accountMenuItems}
/>
</div>
@@ -91,7 +83,7 @@ export async function Navbar() {
...accountMenuItems,
]}
userEmail={profile?.name}
avatarSrc={profile?.avatar_url}
avatarSrc={profile?.avatar_url ?? ""}
/>
</div>
) : null}

View File

@@ -0,0 +1,52 @@
"use client";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Bell } from "@phosphor-icons/react";
import { NotificationDropdown } from "./components/NotificationDropdown";
import { formatNotificationCount } from "./helpers";
import { useAgentNotifications } from "./useAgentNotifications";
export function AgentNotifications() {
const { activeExecutions, recentCompletions, recentFailures, isConnected } =
useAgentNotifications();
return (
<Popover>
<PopoverTrigger asChild>
<button
className="relative rounded-lg p-2 transition-colors hover:bg-white/10"
title="Agent Activity"
>
<Bell size={22} className="text-black" />
{/* Running Agents Badge */}
{activeExecutions.length > 0 && (
<div className="absolute -right-1 -top-1 flex h-5 w-5 items-center justify-center rounded-full bg-blue-500 text-xs font-medium text-white">
{formatNotificationCount(activeExecutions.length)}
{/* Rotating Spinner */}
<div className="absolute -inset-1 animate-spin rounded-full border-2 border-transparent border-t-blue-300" />
</div>
)}
{/* Connection Status Indicator - only show when no running agents */}
{!isConnected && activeExecutions.length === 0 && (
<div className="absolute -right-1 -top-1 h-2 w-2 rounded-full bg-yellow-500" />
)}
</button>
</PopoverTrigger>
<PopoverContent className="w-80 p-0" align="center" sideOffset={8}>
<NotificationDropdown
activeExecutions={activeExecutions}
recentCompletions={recentCompletions}
recentFailures={recentFailures}
/>
</PopoverContent>
</Popover>
);
}

View File

@@ -0,0 +1,81 @@
"use client";
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
import { Text } from "@/components/atoms/Text/Text";
import { Bell } from "@phosphor-icons/react";
import { AgentExecutionWithInfo } from "../helpers";
import { NotificationItem } from "./NotificationItem";
interface NotificationDropdownProps {
activeExecutions: AgentExecutionWithInfo[];
recentCompletions: AgentExecutionWithInfo[];
recentFailures: AgentExecutionWithInfo[];
}
export function NotificationDropdown({
activeExecutions,
recentCompletions,
recentFailures,
}: NotificationDropdownProps) {
// Combine and sort all executions - running/queued at top, then by most recent
function getSortedExecutions() {
const allExecutions = [
...activeExecutions.map((e) => ({ ...e, type: "running" as const })),
...recentCompletions.map((e) => ({ ...e, type: "completed" as const })),
...recentFailures.map((e) => ({ ...e, type: "failed" as const })),
];
return allExecutions.sort((a, b) => {
// Running/queued always at top
const aIsActive =
a.status === AgentExecutionStatus.RUNNING ||
a.status === AgentExecutionStatus.QUEUED;
const bIsActive =
b.status === AgentExecutionStatus.RUNNING ||
b.status === AgentExecutionStatus.QUEUED;
if (aIsActive && !bIsActive) return -1;
if (!aIsActive && bIsActive) return 1;
// Within same category, sort by most recent
const aTime = aIsActive ? a.started_at : a.ended_at;
const bTime = bIsActive ? b.started_at : b.ended_at;
if (!aTime || !bTime) return 0;
return new Date(bTime).getTime() - new Date(aTime).getTime();
});
}
const sortedExecutions = getSortedExecutions();
return (
<div>
{/* Header */}
<div className="px-4 pt-4">
<Text variant="body-medium" className="font-semibold text-gray-900">
Agent Activity
</Text>
</div>
{/* Content */}
<div className="max-h-96 overflow-y-auto">
{sortedExecutions.length > 0 ? (
<div className="p-2">
{sortedExecutions.map((execution) => (
<NotificationItem
key={execution.id}
execution={execution}
type={execution.type}
/>
))}
</div>
) : (
<div className="p-8 text-center text-gray-500">
<Bell size={32} className="mx-auto mb-2 opacity-50" />
<p>No recent activity</p>
</div>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,88 @@
"use client";
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
import { Text } from "@/components/atoms/Text/Text";
import {
CheckCircle,
CircleNotchIcon,
Clock,
WarningOctagonIcon,
} from "@phosphor-icons/react";
import type { AgentExecutionWithInfo } from "../helpers";
import {
formatTimeAgo,
getExecutionDuration,
getStatusColorClass,
} from "../helpers";
interface NotificationItemProps {
execution: AgentExecutionWithInfo;
type: "running" | "completed" | "failed";
}
export function NotificationItem({ execution, type }: NotificationItemProps) {
function getStatusIcon() {
switch (type) {
case "running":
return execution.status === AgentExecutionStatus.QUEUED ? (
<Clock size={16} className="text-purple-500" />
) : (
<CircleNotchIcon
size={16}
className="animate-spin text-purple-500"
weight="bold"
/>
);
case "completed":
return (
<CheckCircle size={16} weight="fill" className="text-purple-500" />
);
case "failed":
return <WarningOctagonIcon size={16} className="text-purple-500" />;
default:
return null;
}
}
function getTimeDisplay() {
if (type === "running") {
const timeAgo = formatTimeAgo(execution.started_at.toString());
return `Started ${timeAgo}, ${getExecutionDuration(execution)} running`;
}
if (execution.ended_at) {
const timeAgo = formatTimeAgo(execution.ended_at.toString());
return type === "completed"
? `Completed ${timeAgo}`
: `Failed ${timeAgo}`;
}
return "Unknown";
}
return (
<div className="cursor-pointer border-b border-slate-50 px-2 py-3 transition-colors hover:bg-gray-50">
{/* Icon + Agent Name */}
<div className="flex items-center space-x-3">
{getStatusIcon()}
<Text variant="body-medium" className="truncate text-gray-900">
{execution.agent_name}
</Text>
</div>
{/* Agent Message - Indented */}
<div className="ml-7">
{execution.agent_description ? (
<Text variant="body" className={`${getStatusColorClass(execution)}`}>
{execution.agent_description}
</Text>
) : null}
{/* Time - Indented */}
<Text variant="small" className="pt-2 !text-zinc-500">
{getTimeDisplay()}
</Text>
</div>
</div>
);
}

View File

@@ -0,0 +1,26 @@
"use client";
import { ReactNode } from "react";
interface NotificationSectionProps {
title: string;
count: number;
colorClass: string;
children: ReactNode;
}
export function NotificationSection({
title,
count,
colorClass,
children,
}: NotificationSectionProps) {
return (
<div className="border-b border-gray-100 p-4 dark:border-gray-700">
<h4 className={`mb-2 text-sm font-medium ${colorClass}`}>
{title} ({count})
</h4>
<div className="space-y-2">{children}</div>
</div>
);
}

View File

@@ -0,0 +1,326 @@
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
import { GraphExecutionMeta as GeneratedGraphExecutionMeta } from "@/app/api/__generated__/models/graphExecutionMeta";
import { MyAgent } from "@/app/api/__generated__/models/myAgent";
import type { GraphExecution } from "@/lib/autogpt-server-api/types";
export function formatTimeAgo(dateStr: string): string {
const date = new Date(dateStr);
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffMins = Math.floor(diffMs / 60000);
if (diffMins < 1) return "Just now";
if (diffMins < 60) return `${diffMins}m ago`;
const diffHours = Math.floor(diffMins / 60);
if (diffHours < 24) return `${diffHours}h ago`;
const diffDays = Math.floor(diffHours / 24);
return `${diffDays}d ago`;
}
export function getStatusDisplayText(
execution: GeneratedGraphExecutionMeta,
): string {
switch (execution.status) {
case AgentExecutionStatus.QUEUED:
return "Queued";
case AgentExecutionStatus.RUNNING:
return "Running";
case AgentExecutionStatus.COMPLETED:
return "Completed";
case AgentExecutionStatus.FAILED:
return "Failed";
case AgentExecutionStatus.TERMINATED:
return "Stopped";
case AgentExecutionStatus.INCOMPLETE:
return "Incomplete";
default:
return execution.status;
}
}
export function getStatusColorClass(
execution: GeneratedGraphExecutionMeta,
): string {
switch (execution.status) {
case AgentExecutionStatus.QUEUED:
return "text-yellow-600";
case AgentExecutionStatus.RUNNING:
return "text-blue-600";
case AgentExecutionStatus.COMPLETED:
return "text-green-600";
case AgentExecutionStatus.FAILED:
case AgentExecutionStatus.TERMINATED:
return "text-red-600";
case AgentExecutionStatus.INCOMPLETE:
return "text-gray-600";
default:
return "text-gray-600";
}
}
export function truncateGraphId(graphId: string, length: number = 8): string {
return `${graphId.slice(0, length)}...`;
}
export function getExecutionDuration(
execution: GeneratedGraphExecutionMeta,
): string {
if (!execution.started_at) return "Unknown";
const start = new Date(execution.started_at);
const end = execution.ended_at ? new Date(execution.ended_at) : new Date();
const durationMs = end.getTime() - start.getTime();
const durationSec = Math.floor(durationMs / 1000);
if (durationSec < 60) return `${durationSec}s`;
const durationMin = Math.floor(durationSec / 60);
if (durationMin < 60) return `${durationMin}m ${durationSec % 60}s`;
const durationHr = Math.floor(durationMin / 60);
return `${durationHr}h ${durationMin % 60}m`;
}
export function shouldShowNotificationBadge(totalCount: number): boolean {
return totalCount > 0;
}
export function formatNotificationCount(count: number): string {
if (count > 99) return "99+";
return count.toString();
}
export interface AgentExecutionWithInfo extends GeneratedGraphExecutionMeta {
agent_name: string;
agent_description: string;
}
export interface NotificationState {
activeExecutions: AgentExecutionWithInfo[];
recentCompletions: AgentExecutionWithInfo[];
recentFailures: AgentExecutionWithInfo[];
totalCount: number;
}
export function createAgentInfoMap(
agents: MyAgent[],
): Map<string, { name: string; description: string }> {
const agentMap = new Map<string, { name: string; description: string }>();
agents.forEach((agent) => {
agentMap.set(agent.agent_id, {
name: agent.agent_name,
description: agent.description,
});
});
return agentMap;
}
export function convertLegacyExecutionToGenerated(
execution: GraphExecution,
): GeneratedGraphExecutionMeta {
return {
id: execution.id,
user_id: execution.user_id,
graph_id: execution.graph_id,
graph_version: execution.graph_version,
preset_id: execution.preset_id,
status: execution.status as AgentExecutionStatus,
started_at: execution.started_at.toISOString(),
ended_at: execution.ended_at.toISOString(),
stats: execution.stats || {
cost: 0,
duration: 0,
duration_cpu_only: 0,
node_exec_time: 0,
node_exec_time_cpu_only: 0,
node_exec_count: 0,
},
};
}
export function enrichExecutionWithAgentInfo(
execution: GeneratedGraphExecutionMeta,
agentInfoMap: Map<string, { name: string; description: string }>,
): AgentExecutionWithInfo {
const agentInfo = agentInfoMap.get(execution.graph_id);
return {
...execution,
agent_name: agentInfo?.name || `Graph ${execution.graph_id.slice(0, 8)}...`,
agent_description: agentInfo?.description ?? "",
};
}
export function isActiveExecution(
execution: GeneratedGraphExecutionMeta,
): boolean {
const status = execution.status;
return (
status === AgentExecutionStatus.RUNNING ||
status === AgentExecutionStatus.QUEUED
);
}
export function isRecentCompletion(
execution: GeneratedGraphExecutionMeta,
thirtyMinutesAgo: Date,
): boolean {
const status = execution.status;
return (
status === AgentExecutionStatus.COMPLETED &&
!!execution.ended_at &&
new Date(execution.ended_at) > thirtyMinutesAgo
);
}
export function isRecentFailure(
execution: GeneratedGraphExecutionMeta,
thirtyMinutesAgo: Date,
): boolean {
const status = execution.status;
return (
(status === AgentExecutionStatus.FAILED ||
status === AgentExecutionStatus.TERMINATED) &&
!!execution.ended_at &&
new Date(execution.ended_at) > thirtyMinutesAgo
);
}
export function isRecentNotification(
execution: AgentExecutionWithInfo,
thirtyMinutesAgo: Date,
): boolean {
return execution.ended_at
? new Date(execution.ended_at) > thirtyMinutesAgo
: false;
}
export function categorizeExecutions(
executions: GeneratedGraphExecutionMeta[],
agentInfoMap: Map<string, { name: string; description: string }>,
): NotificationState {
const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
const enrichedExecutions = executions.map((execution) =>
enrichExecutionWithAgentInfo(execution, agentInfoMap),
);
const activeExecutions = enrichedExecutions
.filter(isActiveExecution)
.slice(0, 10);
const recentCompletions = enrichedExecutions
.filter((execution) => isRecentCompletion(execution, twentyFourHoursAgo))
.slice(0, 10);
const recentFailures = enrichedExecutions
.filter((execution) => isRecentFailure(execution, twentyFourHoursAgo))
.slice(0, 10);
return {
activeExecutions,
recentCompletions,
recentFailures,
totalCount:
activeExecutions.length +
recentCompletions.length +
recentFailures.length,
};
}
export function removeExecutionFromAllCategories(
state: NotificationState,
executionId: string,
): NotificationState {
return {
activeExecutions: state.activeExecutions.filter(
(e) => e.id !== executionId,
),
recentCompletions: state.recentCompletions.filter(
(e) => e.id !== executionId,
),
recentFailures: state.recentFailures.filter((e) => e.id !== executionId),
totalCount: state.totalCount, // Will be recalculated later
};
}
export function addExecutionToCategory(
state: NotificationState,
execution: AgentExecutionWithInfo,
): NotificationState {
const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
const newState = { ...state };
if (isActiveExecution(execution)) {
newState.activeExecutions = [execution, ...newState.activeExecutions].slice(
0,
10,
);
} else if (isRecentCompletion(execution, twentyFourHoursAgo)) {
newState.recentCompletions = [
execution,
...newState.recentCompletions,
].slice(0, 10);
} else if (isRecentFailure(execution, twentyFourHoursAgo)) {
newState.recentFailures = [execution, ...newState.recentFailures].slice(
0,
10,
);
}
return newState;
}
export function cleanupOldNotifications(
state: NotificationState,
): NotificationState {
const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
return {
...state,
recentCompletions: state.recentCompletions.filter((e) =>
isRecentNotification(e, twentyFourHoursAgo),
),
recentFailures: state.recentFailures.filter((e) =>
isRecentNotification(e, twentyFourHoursAgo),
),
};
}
export function calculateTotalCount(
state: NotificationState,
): NotificationState {
return {
...state,
totalCount:
state.activeExecutions.length +
state.recentCompletions.length +
state.recentFailures.length,
};
}
export function handleExecutionUpdate(
currentState: NotificationState,
execution: GraphExecution,
agentInfoMap: Map<string, { name: string; description: string }>,
): NotificationState {
// Convert and enrich the execution
const convertedExecution = convertLegacyExecutionToGenerated(execution);
const enrichedExecution = enrichExecutionWithAgentInfo(
convertedExecution,
agentInfoMap,
);
// Remove from all categories first
let newState = removeExecutionFromAllCategories(currentState, execution.id);
// Add to appropriate category
newState = addExecutionToCategory(newState, enrichedExecution);
// Clean up old notifications
newState = cleanupOldNotifications(newState);
// Recalculate total count
newState = calculateTotalCount(newState);
return newState;
}

View File

@@ -0,0 +1,156 @@
import { useGetV1GetAllExecutions } from "@/app/api/__generated__/endpoints/graphs/graphs";
import { useGetV2GetMyAgents } from "@/app/api/__generated__/endpoints/store/store";
import BackendAPI from "@/lib/autogpt-server-api/client";
import type { GraphExecution } from "@/lib/autogpt-server-api/types";
import { useCallback, useEffect, useState } from "react";
import {
NotificationState,
categorizeExecutions,
createAgentInfoMap,
handleExecutionUpdate,
} from "./helpers";
export function useAgentNotifications() {
const [api] = useState(() => new BackendAPI());
const [notifications, setNotifications] = useState<NotificationState>({
activeExecutions: [],
recentCompletions: [],
recentFailures: [],
totalCount: 0,
});
const [isConnected, setIsConnected] = useState(false);
const [agentInfoMap, setAgentInfoMap] = useState<
Map<string, { name: string; description: string }>
>(new Map());
// Get library agents using generated hook
const {
data: myAgentsResponse,
isLoading: isAgentsLoading,
error: agentsError,
} = useGetV2GetMyAgents({
query: {
enabled: true,
},
});
// Get all executions using generated hook
const {
data: executionsResponse,
isLoading: isExecutionsLoading,
error: executionsError,
} = useGetV1GetAllExecutions({
query: {
enabled: true,
},
});
// Update agent info map when library agents data changes
useEffect(() => {
if (myAgentsResponse?.data?.agents) {
console.log(
"[AgentNotifications] Processing library agents:",
myAgentsResponse.data.agents.length,
);
const agentMap = createAgentInfoMap(myAgentsResponse.data.agents);
console.log(
"[AgentNotifications] Agent info map created:",
agentMap.size,
"agents",
);
setAgentInfoMap(agentMap);
}
}, [myAgentsResponse]);
// Handle real-time execution updates
const handleExecutionEvent = useCallback(
(execution: GraphExecution) => {
console.log(
"[AgentNotifications] Received graph execution event:",
execution,
);
setNotifications((currentState) =>
handleExecutionUpdate(currentState, execution, agentInfoMap),
);
},
[agentInfoMap],
);
// Process initial execution state when data loads
useEffect(() => {
if (
executionsResponse?.data &&
!isExecutionsLoading &&
agentInfoMap.size > 0
) {
console.log(
"[AgentNotifications] Processing executions:",
executionsResponse.data.length,
);
const newNotifications = categorizeExecutions(
executionsResponse.data,
agentInfoMap,
);
console.log("[AgentNotifications] Processed notifications:", {
active: newNotifications.activeExecutions.length,
completed: newNotifications.recentCompletions.length,
failed: newNotifications.recentFailures.length,
});
setNotifications(newNotifications);
}
}, [executionsResponse, isExecutionsLoading, agentInfoMap]);
// Initialize WebSocket connection for real-time updates
useEffect(() => {
const connectHandler = api.onWebSocketConnect(() => {
console.log(
"[AgentNotifications] WebSocket connected - setting up execution subscriptions",
);
setIsConnected(true);
// Subscribe to graph executions for all user agents
if (myAgentsResponse?.data?.agents) {
myAgentsResponse.data.agents.forEach((agent) => {
api
.subscribeToGraphExecutions(agent.agent_id as any)
.catch((error) => {
console.error(
`[AgentNotifications] Failed to subscribe to graph ${agent.agent_id}:`,
error,
);
});
});
}
});
const disconnectHandler = api.onWebSocketDisconnect(() => {
console.log("[AgentNotifications] WebSocket disconnected");
setIsConnected(false);
});
const messageHandler = api.onWebSocketMessage(
"graph_execution_event",
handleExecutionEvent,
);
console.log("[AgentNotifications] Starting WebSocket connection...");
api.connectWebSocket();
return () => {
connectHandler();
disconnectHandler();
messageHandler();
api.disconnectWebSocket();
};
}, [api, handleExecutionEvent, myAgentsResponse]);
return {
...notifications,
isConnected,
isLoading: isAgentsLoading || isExecutionsLoading,
error: agentsError || executionsError,
};
}

View File

@@ -18,9 +18,7 @@ interface Props {
export function NavbarLink({ name, href }: Props) {
const pathname = usePathname();
const parts = pathname.split("/");
const activeLink = "/" + (parts.length > 2 ? parts[2] : parts[1]);
const isActive = activeLink === href;
const isActive = pathname.includes(href);
return (
<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,26 @@
import { useGetV2GetUserProfile } from "@/app/api/__generated__/endpoints/store/store";
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
export function useNavbar() {
const { isLoggedIn, isUserLoading } = useSupabase();
const {
data: profileResponse,
isLoading: isProfileLoading,
error: profileError,
} = useGetV2GetUserProfile({
query: {
enabled: isLoggedIn === true,
},
});
const profile = profileResponse?.data || null;
const isLoading = isUserLoading || (isLoggedIn && isProfileLoading);
return {
isLoggedIn,
profile,
isLoading,
profileError,
};
}

View File

@@ -71,6 +71,30 @@ export const colors = {
800: "#0c5a29",
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
white: "#fefefe",

View File

@@ -36,6 +36,8 @@ const colorCategories = Object.entries(colors)
orange: "Warnings, notifications, and secondary call-to-actions",
yellow: "Highlights, cautions, and attention-grabbing elements",
green: "Success states, confirmations, and positive actions",
purple: "Brand accents, premium features, and creative elements",
pink: "Highlights, special promotions, and playful interactions",
};
return {
@@ -312,6 +314,8 @@ export function AllVariants() {
<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-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
<div className="bg-blue-500 text-purple-600">❌ Not approved</div>

View File

@@ -1,7 +1,7 @@
"use client";
import * as React from "react";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import * as React from "react";
import { cn } from "@/lib/utils";
@@ -23,7 +23,7 @@ const PopoverContent = React.forwardRef<
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border border-neutral-200 bg-white p-4 text-neutral-950 shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-neutral-800 dark:bg-neutral-950 dark:text-neutral-50",
"rounded-medium z-50 w-72 border border-neutral-200 bg-white p-4 text-neutral-950 shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-neutral-800 dark:bg-neutral-950 dark:text-neutral-50",
className,
)}
{...props}
@@ -34,8 +34,8 @@ PopoverContent.displayName = PopoverPrimitive.Content.displayName;
export {
Popover,
PopoverTrigger,
PopoverContent,
PopoverAnchor,
PopoverContent,
PopoverPortal,
PopoverTrigger,
};