mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
fix all components of marketplace and profile according to new design
This commit is contained in:
@@ -9,7 +9,7 @@ export default function PlatformLayout({ children }: { children: ReactNode }) {
|
||||
links={[
|
||||
{
|
||||
name: "Home",
|
||||
href: "/home",
|
||||
href: "/library",
|
||||
},
|
||||
{
|
||||
name: "Marketplace",
|
||||
@@ -63,7 +63,7 @@ export default function PlatformLayout({ children }: { children: ReactNode }) {
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<main>{children}</main>
|
||||
<main className="w-full pt-8">{children}</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -54,52 +54,46 @@ export default async function Page({
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-screen max-w-[1360px]">
|
||||
<main className="mt-5 px-4">
|
||||
<BreadCrumbs items={breadcrumbs} />
|
||||
<main className="mt-9 px-10">
|
||||
<BreadCrumbs items={breadcrumbs} />
|
||||
|
||||
<div className="mt-4 flex flex-col items-start gap-4 sm:mt-6 sm:gap-6 md:mt-8 md:flex-row md:gap-8">
|
||||
<div className="w-full md:w-auto md:shrink-0">
|
||||
<AgentInfo
|
||||
name={agent.agent_name}
|
||||
creator={agent.creator}
|
||||
shortDescription={agent.sub_heading}
|
||||
longDescription={agent.description}
|
||||
rating={agent.rating}
|
||||
runs={agent.runs}
|
||||
categories={agent.categories}
|
||||
lastUpdated={agent.updated_at}
|
||||
version={agent.versions[agent.versions.length - 1]}
|
||||
storeListingVersionId={agent.store_listing_version_id}
|
||||
/>
|
||||
</div>
|
||||
<AgentImages
|
||||
images={
|
||||
agent.agent_video
|
||||
? [agent.agent_video, ...agent.agent_image]
|
||||
: agent.agent_image
|
||||
}
|
||||
<div className="mt-4 flex flex-col items-start gap-4 sm:mt-6 sm:gap-6 md:mt-8 md:flex-row md:gap-8">
|
||||
<div className="w-full md:w-auto md:shrink-0">
|
||||
<AgentInfo
|
||||
name={agent.agent_name}
|
||||
creator={agent.creator}
|
||||
shortDescription={agent.sub_heading}
|
||||
longDescription={agent.description}
|
||||
rating={agent.rating}
|
||||
runs={agent.runs}
|
||||
categories={agent.categories}
|
||||
lastUpdated={agent.updated_at}
|
||||
version={agent.versions[agent.versions.length - 1]}
|
||||
storeListingVersionId={agent.store_listing_version_id}
|
||||
/>
|
||||
</div>
|
||||
<Separator className="mb-[25px] mt-[60px]" />
|
||||
<AgentsSection
|
||||
margin="32px"
|
||||
agents={otherAgents.agents}
|
||||
sectionTitle={`Other agents by ${agent.creator}`}
|
||||
<AgentImages
|
||||
images={
|
||||
agent.agent_video
|
||||
? [agent.agent_video, ...agent.agent_image]
|
||||
: agent.agent_image
|
||||
}
|
||||
/>
|
||||
<Separator className="mb-[25px] mt-[60px]" />
|
||||
<AgentsSection
|
||||
margin="32px"
|
||||
agents={similarAgents.agents}
|
||||
sectionTitle="Similar agents"
|
||||
/>
|
||||
<Separator className="mb-[25px] mt-[60px]" />
|
||||
<BecomeACreator
|
||||
title="Become a Creator"
|
||||
description="Join our ever-growing community of hackers and tinkerers"
|
||||
buttonText="Become a Creator"
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
<Separator className="mb-[25px] mt-[60px]" />
|
||||
<AgentsSection
|
||||
margin="32px"
|
||||
agents={otherAgents.agents}
|
||||
sectionTitle={`Other agents by ${agent.creator}`}
|
||||
/>
|
||||
<Separator className="mb-[25px] mt-[60px]" />
|
||||
<AgentsSection
|
||||
margin="32px"
|
||||
agents={similarAgents.agents}
|
||||
sectionTitle="Similar agents"
|
||||
/>
|
||||
<Separator className="mb-[25px] mt-[60px]" />
|
||||
<BecomeACreator title="Become a Creator" buttonText="Become a Creator" />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -44,50 +44,48 @@ export default async function Page({
|
||||
const creatorAgents = await api.getStoreAgents({ creator: params.creator });
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-screen max-w-[1360px]">
|
||||
<main className="mt-5 px-4">
|
||||
<BreadCrumbs
|
||||
items={[
|
||||
{ name: "Store", link: "/marketplace" },
|
||||
{ name: creator.name, link: "#" },
|
||||
]}
|
||||
/>
|
||||
<main className="px-10">
|
||||
<BreadCrumbs
|
||||
items={[
|
||||
{ name: "Store", link: "/marketplace" },
|
||||
{ name: creator.name, link: "#" },
|
||||
]}
|
||||
/>
|
||||
|
||||
<div className="mt-4 flex flex-col items-start gap-4 sm:mt-6 sm:gap-6 md:mt-8 md:flex-row md:gap-8">
|
||||
<div className="w-full md:w-auto md:shrink-0">
|
||||
<CreatorInfoCard
|
||||
username={creator.name}
|
||||
handle={creator.username}
|
||||
avatarSrc={creator.avatar_url}
|
||||
categories={creator.top_categories}
|
||||
averageRating={creator.agent_rating}
|
||||
totalRuns={creator.agent_runs}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-4 sm:gap-6 md:gap-8">
|
||||
<p className="font-geist text-underline-position-from-font text-decoration-skip-none text-left text-base font-medium leading-6">
|
||||
About
|
||||
</p>
|
||||
<div
|
||||
className="font-poppins text-[48px] font-normal leading-[59px] text-neutral-900 dark:text-zinc-50"
|
||||
style={{ whiteSpace: "pre-line" }}
|
||||
>
|
||||
{creator.description}
|
||||
</div>
|
||||
|
||||
<CreatorLinks links={creator.links} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-8 sm:mt-12 md:mt-16 lg:pb-[58px]">
|
||||
<Separator className="mb-6 bg-gray-200" />
|
||||
<AgentsSection
|
||||
agents={creatorAgents.agents}
|
||||
hideAvatars={true}
|
||||
sectionTitle={`Agents by ${creator.name}`}
|
||||
<div className="mt-4 flex flex-col items-start gap-4 sm:mt-6 sm:gap-6 md:mt-8 md:flex-row md:gap-8">
|
||||
<div>
|
||||
<CreatorInfoCard
|
||||
username={creator.name}
|
||||
handle={creator.username}
|
||||
avatarSrc={creator.avatar_url}
|
||||
categories={creator.top_categories}
|
||||
averageRating={creator.agent_rating}
|
||||
totalRuns={creator.agent_runs}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<div className="flex-1 space-y-7">
|
||||
<div>
|
||||
<p className="font-sans text-base font-medium text-zinc-800">
|
||||
About
|
||||
</p>
|
||||
<h1 className="font-poppins text-4xl font-normal leading-[3.25rem] text-zinc-800">
|
||||
{creator.description}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<CreatorLinks links={creator.links} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 sm:mt-12 md:mt-16 lg:pb-[58px]">
|
||||
<Separator className="mb-6 bg-gray-200" />
|
||||
<AgentsSection
|
||||
agents={creatorAgents.agents}
|
||||
hideAvatars={true}
|
||||
sectionTitle={`Agents by ${creator.name}`}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
} catch (error) {
|
||||
return (
|
||||
|
||||
@@ -149,11 +149,13 @@ export default async function Page({}: {}) {
|
||||
const { featuredAgents, topAgents, featuredCreators } = await getStoreData();
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-screen max-w-[1360px] overflow-x-hidden">
|
||||
<main>
|
||||
<main>
|
||||
<section className="px-10">
|
||||
<HeroSection />
|
||||
<FeaturedSection featuredAgents={featuredAgents.agents} />
|
||||
{/* 100px margin because our featured sections button are placed 40px below the container */}
|
||||
</section>
|
||||
<FeaturedSection featuredAgents={featuredAgents.agents} />
|
||||
{/* 100px margin because our featured sections button are placed 40px below the container */}
|
||||
<section className="px-10">
|
||||
<Separator className="mb-6 mt-24" />
|
||||
<AgentsSection
|
||||
sectionTitle="Top Agents"
|
||||
@@ -166,10 +168,9 @@ export default async function Page({}: {}) {
|
||||
<Separator className="mb-[25px] mt-[60px]" />
|
||||
<BecomeACreator
|
||||
title="Become a Creator"
|
||||
description="Join our ever-growing community of hackers and tinkerers"
|
||||
buttonText="Become a Creator"
|
||||
buttonText="Upload your agent"
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -116,67 +116,67 @@ function SearchResults({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="mx-auto min-h-screen max-w-[1440px] px-10 lg:min-w-[1440px]">
|
||||
<div className="mt-8 flex items-center">
|
||||
<div className="flex-1">
|
||||
<h2 className="font-geist text-base font-medium leading-normal text-neutral-800 dark:text-neutral-200">
|
||||
Results for:
|
||||
</h2>
|
||||
<h1 className="font-poppins text-2xl font-semibold leading-[32px] text-neutral-800 dark:text-neutral-100">
|
||||
{searchTerm}
|
||||
</h1>
|
||||
</div>
|
||||
<div className="flex-none">
|
||||
<SearchBar width="w-[439px]" height="h-[60px]" />
|
||||
</div>
|
||||
<div className="px-10">
|
||||
<div className="mt-8 flex items-center">
|
||||
<div className="flex-1">
|
||||
<h2 className="font-geist text-base font-medium leading-normal text-neutral-800 dark:text-neutral-200">
|
||||
Results for:
|
||||
</h2>
|
||||
<h1 className="font-poppins text-2xl font-semibold leading-[32px] text-neutral-800 dark:text-neutral-100">
|
||||
{searchTerm}
|
||||
</h1>
|
||||
</div>
|
||||
<div className="flex-none">
|
||||
<SearchBar width="w-[439px]" height="h-[60px]" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="mt-20 flex flex-col items-center justify-center">
|
||||
<p className="text-neutral-500 dark:text-neutral-400">Loading...</p>
|
||||
{isLoading ? (
|
||||
<div className="mt-20 flex flex-col items-center justify-center">
|
||||
<p className="text-neutral-500 dark:text-neutral-400">Loading...</p>
|
||||
</div>
|
||||
) : totalCount > 0 ? (
|
||||
<>
|
||||
<div className="mt-[36px] flex items-center justify-between">
|
||||
<SearchFilterChips
|
||||
totalCount={totalCount}
|
||||
agentsCount={agentsCount}
|
||||
creatorsCount={creatorsCount}
|
||||
onFilterChange={handleFilterChange}
|
||||
/>
|
||||
<SortDropdown onSort={handleSortChange} />
|
||||
</div>
|
||||
) : totalCount > 0 ? (
|
||||
<>
|
||||
<div className="mt-[36px] flex items-center justify-between">
|
||||
<SearchFilterChips
|
||||
totalCount={totalCount}
|
||||
agentsCount={agentsCount}
|
||||
creatorsCount={creatorsCount}
|
||||
onFilterChange={handleFilterChange}
|
||||
/>
|
||||
<SortDropdown onSort={handleSortChange} />
|
||||
</div>
|
||||
{/* Content section */}
|
||||
<div className="min-h-[500px] max-w-[1440px]">
|
||||
{showAgents && agentsCount > 0 && (
|
||||
<div className="mt-[36px]">
|
||||
<AgentsSection agents={agents} sectionTitle="Agents" />
|
||||
</div>
|
||||
)}
|
||||
{/* Content section */}
|
||||
<div className="">
|
||||
{showAgents && agentsCount > 0 && (
|
||||
<div className="mt-[36px]">
|
||||
<AgentsSection agents={agents} sectionTitle="Agents" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showAgents && agentsCount > 0 && creatorsCount > 0 && (
|
||||
<Separator />
|
||||
)}
|
||||
{showCreators && creatorsCount > 0 && (
|
||||
{showAgents && agentsCount > 0 && creatorsCount > 0 && (
|
||||
<Separator />
|
||||
)}
|
||||
{showCreators && creatorsCount > 0 && (
|
||||
<div className="mt-[36px]">
|
||||
<FeaturedCreators
|
||||
featuredCreators={creators}
|
||||
title="Creators"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="mt-20 flex flex-col items-center justify-center">
|
||||
<h3 className="mb-2 text-xl font-medium text-neutral-600 dark:text-neutral-300">
|
||||
No results found
|
||||
</h3>
|
||||
<p className="text-neutral-500 dark:text-neutral-400">
|
||||
Try adjusting your search terms or filters
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="mt-20 flex flex-col items-center justify-center">
|
||||
<h3 className="mb-2 text-xl font-medium text-neutral-600 dark:text-neutral-300">
|
||||
No results found
|
||||
</h3>
|
||||
<p className="text-neutral-500 dark:text-neutral-400">
|
||||
Try adjusting your search terms or filters
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from "@/lib/autogpt-server-api/types";
|
||||
import useSupabase from "@/hooks/useSupabase";
|
||||
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||
import AutogptButton from "@/components/agptui/AutogptButton";
|
||||
|
||||
export default function Page({}: {}) {
|
||||
const { supabase } = useSupabase();
|
||||
@@ -64,70 +65,71 @@ export default function Page({}: {}) {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<main className="flex-1 py-8">
|
||||
{/* Header Section */}
|
||||
<div className="mb-8 flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
|
||||
<div className="space-y-6">
|
||||
<h1 className="text-4xl font-medium text-neutral-900 dark:text-neutral-100">
|
||||
Agent dashboard
|
||||
</h1>
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-xl font-medium text-neutral-900 dark:text-neutral-100">
|
||||
<main className="flex-1 space-y-7.5 pb-8">
|
||||
{/* Title */}
|
||||
<h1 className="font-poppins text-[1.75rem] font-medium leading-[2.5rem] text-zinc-800">
|
||||
Agent dashboard
|
||||
</h1>
|
||||
|
||||
{/* Content */}
|
||||
<section className="space-y-8">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="font-poppins text-base font-medium text-zinc-800">
|
||||
Submit a New Agent
|
||||
</h2>
|
||||
<p className="text-sm text-[#707070] dark:text-neutral-400">
|
||||
|
||||
<p className="font-sans text-base font-normal text-zinc-600">
|
||||
Select from the list of agents you currently have, or upload from
|
||||
your local machine.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<PublishAgentPopout
|
||||
trigger={
|
||||
<Button
|
||||
onClick={onOpenPopout}
|
||||
className="h-9 rounded-full bg-black px-4 text-sm font-medium text-white hover:bg-neutral-700 dark:hover:bg-neutral-600"
|
||||
>
|
||||
Submit agent
|
||||
</Button>
|
||||
}
|
||||
openPopout={openPopout}
|
||||
inputStep={popoutStep}
|
||||
submissionData={submissionData}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Separator className="mb-8" />
|
||||
|
||||
{/* Agents Section */}
|
||||
<div>
|
||||
<h2 className="mb-4 text-xl font-bold text-neutral-900 dark:text-neutral-100">
|
||||
Your uploaded agents
|
||||
</h2>
|
||||
{submissions && (
|
||||
<AgentTable
|
||||
agents={
|
||||
submissions?.submissions.map((submission, index) => ({
|
||||
id: index,
|
||||
agent_id: submission.agent_id,
|
||||
agent_version: submission.agent_version,
|
||||
sub_heading: submission.sub_heading,
|
||||
date_submitted: submission.date_submitted,
|
||||
agentName: submission.name,
|
||||
description: submission.description,
|
||||
imageSrc: submission.image_urls || [""],
|
||||
dateSubmitted: new Date(
|
||||
submission.date_submitted,
|
||||
).toLocaleDateString(),
|
||||
status: submission.status.toLowerCase() as StatusType,
|
||||
runs: submission.runs,
|
||||
rating: submission.rating,
|
||||
})) || []
|
||||
<PublishAgentPopout
|
||||
trigger={
|
||||
<AutogptButton onClick={onOpenPopout} variant="outline">
|
||||
Add to Library
|
||||
</AutogptButton>
|
||||
}
|
||||
onEditSubmission={onEditSubmission}
|
||||
onDeleteSubmission={onDeleteSubmission}
|
||||
openPopout={openPopout}
|
||||
inputStep={popoutStep}
|
||||
submissionData={submissionData}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator className="bg-neutral-300" />
|
||||
|
||||
<div className="space-y-3">
|
||||
<h2 className="font-poppins text-base font-medium text-zinc-800">
|
||||
Your uploaded agents
|
||||
</h2>
|
||||
|
||||
{submissions && (
|
||||
<AgentTable
|
||||
agents={
|
||||
submissions?.submissions.map((submission, index) => ({
|
||||
id: index,
|
||||
agent_id: submission.agent_id,
|
||||
agent_version: submission.agent_version,
|
||||
sub_heading: submission.sub_heading,
|
||||
date_submitted: submission.date_submitted,
|
||||
agentName: submission.name,
|
||||
description: submission.description,
|
||||
imageSrc: submission.image_urls || [""],
|
||||
dateSubmitted: new Date(
|
||||
submission.date_submitted,
|
||||
).toLocaleDateString(),
|
||||
status: submission.status.toLowerCase() as StatusType,
|
||||
runs: submission.runs,
|
||||
rating: submission.rating,
|
||||
})) || []
|
||||
}
|
||||
onEditSubmission={onEditSubmission}
|
||||
onDeleteSubmission={onDeleteSubmission}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -149,7 +149,7 @@ export default function PrivatePage() {
|
||||
: [];
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-3xl md:py-8">
|
||||
<div className="mx-auto max-w-3xl md:pb-8">
|
||||
<h2 className="mb-4 text-lg">Connections & Credentials</h2>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
|
||||
@@ -16,45 +16,45 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
{
|
||||
text: "Creator Dashboard",
|
||||
href: "/profile/dashboard",
|
||||
icon: <IconDashboardLayout className="h-6 w-6" />,
|
||||
icon: <IconDashboardLayout className="h-6 w-6 stroke-[1.25px]" />,
|
||||
},
|
||||
...(process.env.NEXT_PUBLIC_SHOW_BILLING_PAGE === "true"
|
||||
? [
|
||||
{
|
||||
text: "Billing",
|
||||
href: "/profile/credits",
|
||||
icon: <IconCoin className="h-6 w-6" />,
|
||||
icon: <IconCoin className="h-6 w-6 stroke-[1.25px]" />,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
text: "Integrations",
|
||||
href: "/profile/integrations",
|
||||
icon: <IconIntegrations className="h-6 w-6" />,
|
||||
icon: <IconIntegrations className="h-6 w-6 stroke-[1.25px]" />,
|
||||
},
|
||||
{
|
||||
text: "API Keys",
|
||||
href: "/profile/api_keys",
|
||||
icon: <KeyIcon className="h-6 w-6" />,
|
||||
icon: <KeyIcon className="h-6 w-6 stroke-[1.25px]" />,
|
||||
},
|
||||
{
|
||||
text: "Profile",
|
||||
href: "/profile",
|
||||
icon: <IconProfile className="h-6 w-6" />,
|
||||
icon: <IconProfile className="h-6 w-6 stroke-[1.25px]" />,
|
||||
},
|
||||
{
|
||||
text: "Settings",
|
||||
href: "/profile/settings",
|
||||
icon: <IconSliders className="h-6 w-6" />,
|
||||
icon: <IconSliders className="h-6 w-6 stroke-[1.25px]" />,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen w-screen max-w-[1360px] flex-col lg:flex-row">
|
||||
<div className="flex flex-row gap-14 px-4 pr-10">
|
||||
<Sidebar linkGroups={sidebarLinkGroups} />
|
||||
<div className="flex-1 pl-4">{children}</div>
|
||||
<div className="flex-1">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,10 @@ export default async function Page({}: {}) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center px-4">
|
||||
<div className="space-y-6 pb-10">
|
||||
<h1 className="font-poppins text-[1.75rem] font-medium leading-[2.5rem] text-zinc-500">
|
||||
Profile
|
||||
</h1>
|
||||
<ProfileInfoForm profile={profile as CreatorDetails} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -19,13 +19,12 @@ export default async function SettingsPage() {
|
||||
const preferences = await getUserPreferences();
|
||||
|
||||
return (
|
||||
<div className="container max-w-2xl space-y-6 py-10">
|
||||
<div>
|
||||
<h3 className="text-lg font-medium">My account</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Manage your account settings and preferences.
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-6 pb-10">
|
||||
{/* Title */}
|
||||
<h1 className="font-poppins text-[1.75rem] font-medium leading-[2.5rem] text-zinc-500">
|
||||
Settings
|
||||
</h1>
|
||||
|
||||
<SettingsForm user={user} preferences={preferences} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -38,7 +38,7 @@ export default async function RootLayout({
|
||||
>
|
||||
<body
|
||||
className={cn(
|
||||
"bg-neutral-50 antialiased transition-colors",
|
||||
"mx-auto w-screen max-w-[1500px] overflow-x-hidden bg-white antialiased transition-colors",
|
||||
inter.className,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -11,6 +11,8 @@ import { useToast } from "@/components/ui/use-toast";
|
||||
import useSupabase from "@/hooks/useSupabase";
|
||||
import { DownloadIcon, LoaderIcon } from "lucide-react";
|
||||
import { useOnboarding } from "../onboarding/onboarding-provider";
|
||||
import AutogptButton from "./AutogptButton";
|
||||
import { Chip } from "./Chip";
|
||||
interface AgentInfoProps {
|
||||
name: string;
|
||||
creator: string;
|
||||
@@ -97,37 +99,33 @@ export const AgentInfo: React.FC<AgentInfoProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-[25rem] space-y-11">
|
||||
<div className="w-full max-w-[27rem] space-y-7">
|
||||
{/* Top part */}
|
||||
<div>
|
||||
<h2 className="mb-3 font-poppins text-4xl font-medium text-neutral-900">
|
||||
{name}
|
||||
</h2>
|
||||
<div className="mb-7 flex w-full items-center gap-1.5 font-sans">
|
||||
<p className="text-xl font-normal text-neutral-800">by</p>
|
||||
<Link
|
||||
href={`/marketplace/creator/${encodeURIComponent(creator)}`}
|
||||
className="text-xl font-medium text-neutral-800 hover:underline"
|
||||
>
|
||||
{creator}
|
||||
</Link>
|
||||
<div className="space-y-14">
|
||||
{/* Agent name */}
|
||||
<div>
|
||||
<h2 className="font-poppins text-[1.75rem] font-medium leading-[2.5rem] text-zinc-800">
|
||||
{name}
|
||||
</h2>
|
||||
|
||||
{/* Creator name */}
|
||||
<div className="mb-7 flex w-full items-center gap-1.5 font-sans">
|
||||
<p className="text-base font-normal text-zinc-800">by</p>
|
||||
<Link
|
||||
href={`/marketplace/creator/${encodeURIComponent(creator)}`}
|
||||
className="text-base font-medium text-zinc-800 hover:underline"
|
||||
>
|
||||
{creator}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="mb-7 line-clamp-2 font-sans text-xl font-normal text-neutral-600">
|
||||
{shortDescription}
|
||||
</p>
|
||||
|
||||
<div className="mb-14 w-full">
|
||||
{/* Download and run button */}
|
||||
<div className="w-full">
|
||||
{user ? (
|
||||
<button
|
||||
onClick={handleAddToLibrary}
|
||||
className="inline-flex w-full items-center justify-center gap-2 rounded-[38px] bg-violet-600 px-4 py-3 transition-colors hover:bg-violet-700 sm:w-auto sm:gap-2.5 sm:px-5 sm:py-3.5 lg:px-6 lg:py-4"
|
||||
>
|
||||
<IconPlay className="h-5 w-5 text-white sm:h-5 sm:w-5 lg:h-6 lg:w-6" />
|
||||
<span className="font-poppins text-base font-medium text-neutral-50 sm:text-lg">
|
||||
Add To Library
|
||||
</span>
|
||||
</button>
|
||||
<AutogptButton variant={"secondary"} onClick={handleAddToLibrary}>
|
||||
Add To Library
|
||||
</AutogptButton>
|
||||
) : (
|
||||
<button
|
||||
onClick={handleDownloadToLibrary}
|
||||
@@ -149,47 +147,42 @@ export const AgentInfo: React.FC<AgentInfoProps> = ({
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex w-full items-center justify-between">
|
||||
{/* Runs and ratings */}
|
||||
<div className="flex w-full items-center gap-10">
|
||||
<div className="flex items-center gap-1.5 sm:gap-2">
|
||||
<span className="font-sans text-lg font-semibold text-neutral-800">
|
||||
<span className="font-sans text-base font-medium text-zinc-800">
|
||||
{rating.toFixed(1)}
|
||||
</span>
|
||||
<div className="flex gap-0.5">{StarRatingIcons(rating)}</div>
|
||||
</div>
|
||||
<div className="font-sans text-lg font-semibold text-neutral-800">
|
||||
<div className="font-sans text-base font-medium text-zinc-800">
|
||||
{runs.toLocaleString()} runs
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Separator */}
|
||||
<Separator className="mb-4 lg:mb-[44px]" />
|
||||
<Separator className="bg-neutral-300" />
|
||||
|
||||
{/* Bottom part */}
|
||||
<div className="space-y-9">
|
||||
<div className="space-y-2.5">
|
||||
<p className="font-sans text-base font-medium text-neutral-800">
|
||||
<p className="font-sans text-base font-medium text-zinc-800">
|
||||
Description
|
||||
</p>
|
||||
<p className="whitespace-pre-line font-sans text-base font-normal text-neutral-600">
|
||||
<p className="whitespace-pre-line font-sans text-base font-normal text-zinc-600">
|
||||
{longDescription}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Categories */}
|
||||
<div className="space-y-2.5">
|
||||
<p className="font-sans text-base font-medium text-neutral-800">
|
||||
<p className="font-sans text-base font-medium text-zinc-800">
|
||||
Categories
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2.5">
|
||||
{categories.map((category, index) => (
|
||||
<p
|
||||
key={index}
|
||||
className="rounded-full border border-neutral-600 bg-white px-5 py-3 font-sans text-base font-normal text-neutral-800"
|
||||
>
|
||||
{category}
|
||||
</p>
|
||||
<Chip key={index}>{category}</Chip>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -197,16 +190,18 @@ export const AgentInfo: React.FC<AgentInfoProps> = ({
|
||||
{/* TODO : Rating Agent */}
|
||||
|
||||
{/* Version History */}
|
||||
<div className="flex flex-col gap-2.5">
|
||||
<p className="font-base font-sans font-medium text-neutral-800">
|
||||
<div className="space-y-2.5">
|
||||
<p className="font-sans text-base font-medium text-zinc-800">
|
||||
Version history
|
||||
</p>
|
||||
<p className="font-sans text-base font-normal text-neutral-600">
|
||||
Last updated {lastUpdated}
|
||||
</p>
|
||||
<p className="font-sans text-base font-normal text-neutral-600">
|
||||
Version {version}
|
||||
</p>
|
||||
<div className="space-y-1.5">
|
||||
<p className="font-sans text-base font-normal text-zinc-600">
|
||||
Last updated {lastUpdated}
|
||||
</p>
|
||||
<p className="font-sans text-base font-normal text-zinc-600">
|
||||
Version {version}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -49,7 +49,7 @@ export const AgentTable: React.FC<AgentTableProps> = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="w-full border-t border-neutral-300">
|
||||
{/* Table for desktop view */}
|
||||
<div className="hidden md:block">
|
||||
<Table>
|
||||
@@ -106,7 +106,7 @@ export const AgentTable: React.FC<AgentTableProps> = ({
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={7} className="py-4 text-center">
|
||||
<span className="font-sans text-base text-neutral-600 dark:text-neutral-400">
|
||||
<span className="font-sans text-base text-zinc-600 dark:text-neutral-400">
|
||||
No agents available. Create your first agent to get started!
|
||||
</span>
|
||||
</TableCell>
|
||||
|
||||
@@ -126,3 +126,10 @@ export const Link: Story = {
|
||||
variant: "link",
|
||||
},
|
||||
};
|
||||
|
||||
export const Loading: Story = {
|
||||
args: {
|
||||
children: "Loading Button",
|
||||
isLoading: true,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PlayIcon } from "lucide-react";
|
||||
import { PlayIcon, Loader2Icon } from "lucide-react";
|
||||
import { Button } from "../ui/button";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -15,7 +15,7 @@ const buttonVariants = cva("inline-flex items-center justify-center", {
|
||||
outline:
|
||||
"bg-white text-zinc-800 hover:bg-zinc-100 border border-zinc-700 disabled:bg-white disabled:border-zinc-300 disabled:text-zinc-300",
|
||||
ghost:
|
||||
"bg-transparent text-zinc-800 hover:bg-zinc-100 disabled:text-zinc-300 border-none",
|
||||
"bg-transparent text-zinc-800 hover:bg-zinc-100 disabled:text-zinc-300 disabled:bg-transparent border-none",
|
||||
link: "bg-transparent text-zinc-800 hover:underline border-none hover:bg-transparent",
|
||||
},
|
||||
},
|
||||
@@ -28,6 +28,8 @@ interface AutogptButtonProps extends VariantProps<typeof buttonVariants> {
|
||||
icon?: boolean;
|
||||
children: React.ReactNode;
|
||||
isDisabled?: boolean;
|
||||
isLoading?: boolean;
|
||||
[key: string]: any; // Allow any additional props
|
||||
}
|
||||
|
||||
const AutogptButton = ({
|
||||
@@ -35,6 +37,8 @@ const AutogptButton = ({
|
||||
children,
|
||||
variant,
|
||||
isDisabled = false,
|
||||
isLoading = false,
|
||||
...props
|
||||
}: AutogptButtonProps) => {
|
||||
return (
|
||||
<Button
|
||||
@@ -42,10 +46,17 @@ const AutogptButton = ({
|
||||
"h-12 space-x-1.5 rounded-[3rem] px-4 py-3 shadow-none",
|
||||
buttonVariants({ variant }),
|
||||
isDisabled && "bg-red-500",
|
||||
isLoading && "bg-[#3F3F4680] text-white",
|
||||
)}
|
||||
disabled={isDisabled}
|
||||
disabled={isDisabled || isLoading}
|
||||
variant={variant}
|
||||
{...props}
|
||||
>
|
||||
{icon && <PlayIcon className="h-5 w-5" />}
|
||||
{isLoading ? (
|
||||
<Loader2Icon className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
icon && <PlayIcon className="h-5 w-5" />
|
||||
)}
|
||||
<p className="font-sans text-sm font-medium">{children}</p>
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Meta, StoryObj } from "@storybook/react";
|
||||
import AutogptInput from "./AutogptInput";
|
||||
|
||||
const meta: Meta<typeof AutogptInput> = {
|
||||
title: "new/AutogptInput",
|
||||
component: AutogptInput,
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="flex items-center justify-center bg-[#E1E1E1] p-4">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
tags: ["autodocs"],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof AutogptInput>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
label: "Email",
|
||||
placeholder: "Type something...",
|
||||
onChange: () => {
|
||||
console.log("It's working");
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const IsDisabled: Story = {
|
||||
args: {
|
||||
label: "Email",
|
||||
placeholder: "Type something...",
|
||||
isDisabled: true,
|
||||
onChange: () => {
|
||||
console.log("It's working");
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
import React from "react";
|
||||
import { Input } from "../ui/input";
|
||||
import { Label } from "../ui/label";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface AutogptInputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
className?: string;
|
||||
label?: string;
|
||||
isDisabled?: boolean;
|
||||
}
|
||||
|
||||
const AutogptInput: React.FC<AutogptInputProps> = ({
|
||||
label,
|
||||
isDisabled,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-2", isDisabled && "opacity-50")}>
|
||||
{label && (
|
||||
<Label className="font-sans text-sm font-medium leading-[1.4rem]">
|
||||
{label}
|
||||
</Label>
|
||||
)}
|
||||
<Input
|
||||
{...props}
|
||||
disabled={isDisabled}
|
||||
className={cn(
|
||||
"m-0 h-10 w-full rounded-3xl border border-zinc-300 bg-white py-2 pl-4 font-sans text-base font-normal text-zinc-800 shadow-none outline-none placeholder:text-zinc-400 focus:border-2 focus:border-[#CBD5E1] focus:shadow-none focus:ring-0",
|
||||
props.className,
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AutogptInput;
|
||||
@@ -2,16 +2,15 @@
|
||||
|
||||
import * as React from "react";
|
||||
import { PublishAgentPopout } from "./composite/PublishAgentPopout";
|
||||
import AutogptButton from "./AutogptButton";
|
||||
interface BecomeACreatorProps {
|
||||
title?: string;
|
||||
description?: string;
|
||||
buttonText?: string;
|
||||
onButtonClick?: () => void;
|
||||
}
|
||||
|
||||
export const BecomeACreator: React.FC<BecomeACreatorProps> = ({
|
||||
title = "Become a creator",
|
||||
description = "Join a community where your AI creations can inspire, engage, and be downloaded by users around the world.",
|
||||
buttonText = "Upload your agent",
|
||||
onButtonClick,
|
||||
}) => {
|
||||
@@ -22,32 +21,28 @@ export const BecomeACreator: React.FC<BecomeACreatorProps> = ({
|
||||
return (
|
||||
<div className="mb-18 w-full sm:mb-36 md:mb-72">
|
||||
{/* Title */}
|
||||
<h2 className="mb-18 font-poppins text-lg font-semibold text-neutral-800">
|
||||
<h2 className="mb-18 text-base font-medium text-zinc-500 dark:text-zinc-200">
|
||||
{title}
|
||||
</h2>
|
||||
|
||||
{/* Content Container */}
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<h2 className="mb-9 text-center font-poppins text-3xl font-semibold leading-[3rem] text-neutral-950 md:text-5xl">
|
||||
<h2 className="mb-9 text-center font-poppins text-3xl font-semibold leading-[3.5rem] text-neutral-950 md:text-[2.75rem]">
|
||||
Build AI agents and share
|
||||
<br />
|
||||
your vision
|
||||
<span className="text-violet-600"> your </span>
|
||||
vision
|
||||
</h2>
|
||||
|
||||
<p className="mb-12 text-center font-sans text-lg font-normal text-neutral-700 md:text-2xl">
|
||||
{description}
|
||||
<p className="mb-12 text-center font-sans text-lg font-normal text-zinc-600">
|
||||
Join a community where your AI creations can inspire, engage, <br />{" "}
|
||||
and be downloaded by users around the world.
|
||||
</p>
|
||||
|
||||
<PublishAgentPopout
|
||||
trigger={
|
||||
<button
|
||||
onClick={handleButtonClick}
|
||||
className="inline-flex h-[48px] cursor-pointer items-center justify-center rounded-[38px] bg-neutral-800 px-8 py-3 transition-colors hover:bg-neutral-700 dark:bg-neutral-700 dark:hover:bg-neutral-600 md:h-[56px] md:px-10 md:py-4 lg:h-[68px] lg:px-12 lg:py-5"
|
||||
>
|
||||
<span className="whitespace-nowrap font-poppins text-base font-medium leading-normal text-neutral-50 md:text-lg md:leading-relaxed lg:text-xl lg:leading-7">
|
||||
{buttonText}
|
||||
</span>
|
||||
</button>
|
||||
<AutogptButton onClick={handleButtonClick}>
|
||||
{buttonText}
|
||||
</AutogptButton>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { Chip } from "./Chip";
|
||||
|
||||
const meta: Meta<typeof Chip> = {
|
||||
component: Chip,
|
||||
title: "new/BasicBadge",
|
||||
argTypes: {
|
||||
children: {
|
||||
control: "text",
|
||||
description: "The content of the badge",
|
||||
},
|
||||
},
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Chip>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
children: "Marketing",
|
||||
},
|
||||
};
|
||||
22
autogpt_platform/frontend/src/components/agptui/Chip.tsx
Normal file
22
autogpt_platform/frontend/src/components/agptui/Chip.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import React from "react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function Chip({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<Badge
|
||||
className={cn(
|
||||
"rounded-[30px] border border-zinc-400 bg-white px-3.5 py-2 font-sans text-base font-normal text-zinc-600 shadow-none hover:border-zinc-500 hover:bg-zinc-100",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { CreatorCard } from "./CreatorCard";
|
||||
import { userEvent, within, expect } from "@storybook/test";
|
||||
|
||||
const meta = {
|
||||
title: "AGPT UI/Creator Card",
|
||||
title: "new/Creator Card",
|
||||
component: CreatorCard,
|
||||
decorators: [
|
||||
(Story) => (
|
||||
|
||||
@@ -29,7 +29,7 @@ export const CreatorCard: React.FC<CreatorCardProps> = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`h-64 w-full px-5 pb-5 pt-6 md:w-80 ${backgroundColor} inline-flex cursor-pointer flex-col items-start justify-start gap-3.5 rounded-[1.5rem] transition-all duration-200 hover:brightness-95`}
|
||||
className={`aspect-square w-80 space-y-4 rounded-3xl bg-amber-100 p-5 pt-6 hover:cursor-pointer hover:bg-amber-200`}
|
||||
onClick={onClick}
|
||||
data-testid="creator-card"
|
||||
>
|
||||
@@ -37,25 +37,26 @@ export const CreatorCard: React.FC<CreatorCardProps> = ({
|
||||
<Image
|
||||
src={creatorImage}
|
||||
alt={creatorName}
|
||||
width={80}
|
||||
height={80}
|
||||
className="h-20 w-20 rounded-full"
|
||||
width={84}
|
||||
height={84}
|
||||
className="rounded-full"
|
||||
priority
|
||||
/>
|
||||
) : (
|
||||
<div className="h-20 w-20 rounded-full bg-neutral-300 dark:bg-neutral-600" />
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<h3 className="line-clamp-1 font-poppins text-2xl font-semibold text-neutral-800 dark:text-neutral-100">
|
||||
<div className="flex h-36 flex-col gap-2">
|
||||
<h3 className="line-clamp-1 font-poppins text-3xl font-medium text-zinc-800 dark:text-neutral-100">
|
||||
{creatorName}
|
||||
</h3>
|
||||
<p className="line-clamp-2 font-sans text-base font-normal text-neutral-800 dark:text-neutral-400">
|
||||
<p className="line-clamp-3 font-sans text-base font-normal text-zinc-600 dark:text-neutral-400">
|
||||
{bio}
|
||||
</p>
|
||||
<div className="font-sans text-lg font-semibold text-neutral-800 dark:text-neutral-200">
|
||||
{agentsUploaded} agents
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="font-sans text-sm font-medium text-zinc-800 dark:text-neutral-200">
|
||||
{agentsUploaded} agents
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { CreatorInfoCard } from "./CreatorInfoCard";
|
||||
|
||||
const meta = {
|
||||
title: "AGPT UI/Creator Info Card",
|
||||
title: "new/Creator Info Card",
|
||||
component: CreatorInfoCard,
|
||||
decorators: [
|
||||
(Story) => (
|
||||
|
||||
@@ -2,6 +2,7 @@ import * as React from "react";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { StarRatingIcons } from "@/components/ui/icons";
|
||||
import { Separator } from "../ui/separator";
|
||||
import { Chip } from "./Chip";
|
||||
|
||||
interface CreatorInfoCardProps {
|
||||
username: string;
|
||||
@@ -22,95 +23,84 @@ export const CreatorInfoCard: React.FC<CreatorInfoCardProps> = ({
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className="inline-flex h-auto min-h-[32rem] w-full max-w-[440px] flex-col items-start justify-between overflow-hidden rounded-[26px] bg-violet-100 p-4 dark:bg-violet-900 sm:min-h-[40rem] sm:w-[440px] sm:p-6"
|
||||
className="h-auto w-full max-w-md space-y-6 overflow-hidden rounded-[26px] bg-violet-100 px-5 pb-7 pt-6 dark:bg-violet-900 sm:w-[27rem]"
|
||||
role="article"
|
||||
aria-label={`Creator profile for ${username}`}
|
||||
>
|
||||
{/* Avatar + Basic Info */}
|
||||
<div className="flex w-full flex-col items-start justify-start gap-3.5">
|
||||
<Avatar className="h-[100px] w-[100px] sm:h-[130px] sm:w-[130px]">
|
||||
<Avatar className="h-[100px] w-[100px]">
|
||||
<AvatarImage
|
||||
width={130}
|
||||
height={130}
|
||||
width={100}
|
||||
height={100}
|
||||
src={avatarSrc}
|
||||
alt={`${username}'s avatar`}
|
||||
/>
|
||||
<AvatarFallback
|
||||
size={130}
|
||||
className="h-[100px] w-[100px] sm:h-[130px] sm:w-[130px]"
|
||||
>
|
||||
<AvatarFallback size={100} className="h-[100px] w-[100px]">
|
||||
{username.charAt(0)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex w-full flex-col items-start justify-start gap-1.5">
|
||||
<div className="w-full font-poppins text-2xl font-medium text-neutral-900 dark:text-neutral-100 sm:text-4xl">
|
||||
<div className="w-full font-poppins text-3xl font-medium text-zinc-800 dark:text-zinc-100">
|
||||
{username}
|
||||
</div>
|
||||
<div className="w-full font-sans text-lg font-normal text-neutral-800 dark:text-neutral-200 sm:text-xl">
|
||||
<div className="w-full font-sans text-base font-normal text-zinc-800 dark:text-zinc-200">
|
||||
@{handle}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="my-4 flex w-full flex-col items-start justify-start gap-6 sm:gap-12">
|
||||
{/* Categories */}
|
||||
<div className="flex w-full flex-col items-start justify-start gap-3">
|
||||
<Separator className="bg-neutral-700" />
|
||||
<div className="flex flex-col items-start justify-start gap-2.5">
|
||||
<div className="w-full font-sans text-sm font-medium text-neutral-800 dark:text-neutral-200 sm:text-base">
|
||||
Top categories
|
||||
<Separator className="bg-zinc-300" />
|
||||
|
||||
<div className="flex flex-col items-start justify-start gap-2.5">
|
||||
<div className="w-full font-sans text-sm font-medium text-zinc-800 dark:text-zinc-200 sm:text-base">
|
||||
Top categories
|
||||
</div>
|
||||
<div
|
||||
className="flex flex-wrap items-center gap-2.5"
|
||||
role="list"
|
||||
aria-label="Categories"
|
||||
>
|
||||
{categories.map((category, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center justify-center gap-2.5"
|
||||
role="listitem"
|
||||
>
|
||||
<Chip className="bg-transparent">{category}</Chip>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator className="bg-zinc-300" />
|
||||
|
||||
<div className="flex w-full flex-col items-center justify-between gap-4 sm:flex-row sm:gap-0">
|
||||
{/* Average Rating */}
|
||||
<div className="flex w-full flex-col items-start justify-start gap-2.5">
|
||||
<div className="w-full font-sans text-sm font-medium leading-normal text-zinc-800 dark:text-zinc-200 sm:text-base">
|
||||
Average rating
|
||||
</div>
|
||||
<div className="inline-flex items-center gap-2">
|
||||
<div className="font-sans text-sm font-medium text-zinc-800 dark:text-zinc-200">
|
||||
{averageRating.toFixed(1)}
|
||||
</div>
|
||||
<div
|
||||
className="flex flex-wrap items-center gap-2.5"
|
||||
role="list"
|
||||
aria-label="Categories"
|
||||
className="flex items-center gap-px"
|
||||
role="img"
|
||||
aria-label={`Rating: ${averageRating} out of 5 stars`}
|
||||
>
|
||||
{categories.map((category, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center justify-center gap-2.5 rounded-[34px] border border-neutral-600 px-4 py-3 dark:border-neutral-400"
|
||||
role="listitem"
|
||||
>
|
||||
<div className="font-sans text-sm font-normal text-neutral-800 dark:text-neutral-200 sm:text-base">
|
||||
{category}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{StarRatingIcons(averageRating)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Ratings */}
|
||||
<div className="flex w-full flex-col items-start justify-start gap-3">
|
||||
<Separator className="bg-neutral-700" />
|
||||
<div className="flex w-full flex-col items-center justify-between gap-4 sm:flex-row sm:gap-0">
|
||||
{/* Average Rating */}
|
||||
<div className="flex w-full flex-col items-start justify-start gap-2.5">
|
||||
<div className="w-full font-sans text-sm font-medium leading-normal text-neutral-800 dark:text-neutral-200 sm:text-base">
|
||||
Average rating
|
||||
</div>
|
||||
<div className="inline-flex items-center gap-2">
|
||||
<div className="font-sans text-base font-semibold text-neutral-800 dark:text-neutral-200 sm:text-lg">
|
||||
{averageRating.toFixed(1)}
|
||||
</div>
|
||||
<div
|
||||
className="flex items-center gap-px"
|
||||
role="img"
|
||||
aria-label={`Rating: ${averageRating} out of 5 stars`}
|
||||
>
|
||||
{StarRatingIcons(averageRating)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Number of runs */}
|
||||
<div className="flex w-full flex-col items-start justify-start gap-2.5">
|
||||
<div className="w-full font-sans text-sm font-medium leading-normal text-neutral-800 dark:text-neutral-200 sm:text-base">
|
||||
Number of runs
|
||||
</div>
|
||||
<div className="font-sans text-sm font-semibold text-neutral-800 dark:text-neutral-200 sm:text-lg">
|
||||
{new Intl.NumberFormat().format(totalRuns)} runs
|
||||
</div>
|
||||
</div>
|
||||
{/* Number of runs */}
|
||||
<div className="flex w-full flex-col items-start justify-start gap-2.5">
|
||||
<div className="w-full font-sans text-sm font-medium leading-normal text-zinc-800 dark:text-zinc-200 sm:text-base">
|
||||
Number of runs
|
||||
</div>
|
||||
<div className="font-sans text-sm font-medium text-zinc-800 dark:text-zinc-200 sm:text-base">
|
||||
{new Intl.NumberFormat().format(totalRuns)} runs
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import CreatorLink from "./CreatorLink";
|
||||
|
||||
const meta: Meta<typeof CreatorLink> = {
|
||||
title: "new/CreatorLink",
|
||||
component: CreatorLink,
|
||||
tags: ["autodocs"],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="flex items-center justify-center p-4">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
argTypes: {
|
||||
href: { control: "text" },
|
||||
children: { control: "text" },
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof CreatorLink>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
href: "https://linkedin.com",
|
||||
children: "View Creator Profile",
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
import { FC } from "react";
|
||||
import Link from "next/link";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ExternalLink } from "lucide-react";
|
||||
|
||||
interface CreatorLinkProps {
|
||||
href: string;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
key?: number;
|
||||
}
|
||||
|
||||
const CreatorLink: FC<CreatorLinkProps> = ({
|
||||
href,
|
||||
children,
|
||||
className,
|
||||
key,
|
||||
}) => {
|
||||
return (
|
||||
<Link
|
||||
key={key}
|
||||
href={href}
|
||||
className={cn(
|
||||
"flex h-12 w-full min-w-80 max-w-md items-center justify-between rounded-[34px] border border-neutral-600 bg-transparent px-5 py-3",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<p className="font-sans text-base font-medium text-neutral-800">
|
||||
{children}
|
||||
</p>
|
||||
<ExternalLink className="h-5 w-5 stroke-[1.5px]" />
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreatorLink;
|
||||
@@ -1,6 +1,5 @@
|
||||
import * as React from "react";
|
||||
import { getIconForSocial } from "@/components/ui/icons";
|
||||
import Link from "next/link";
|
||||
import CreatorLink from "./CreatorLink";
|
||||
|
||||
interface CreatorLinksProps {
|
||||
links: string[];
|
||||
@@ -11,32 +10,16 @@ export const CreatorLinks: React.FC<CreatorLinksProps> = ({ links }) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const renderLinkButton = (url: string) => (
|
||||
<Link
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-between rounded-[34px] border border-neutral-600 px-5 py-3 dark:border-neutral-400"
|
||||
>
|
||||
<div className="font-sans text-base font-medium text-neutral-800 dark:text-neutral-200">
|
||||
{new URL(url).hostname.replace("www.", "")}
|
||||
</div>
|
||||
<div className="relative h-6 w-6">
|
||||
{getIconForSocial(url, {
|
||||
className: "h-6 w-6 text-neutral-800 dark:text-neutral-200",
|
||||
})}
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col items-start justify-start gap-4">
|
||||
<div className="font-sans text-base font-medium text-neutral-800 dark:text-neutral-200">
|
||||
<div className="space-y-4">
|
||||
<div className="font-sans text-base font-medium text-zinc-800">
|
||||
Other links
|
||||
</div>
|
||||
<div className="grid w-full grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
{links.map((link, index) => (
|
||||
<React.Fragment key={index}>{renderLinkButton(link)}</React.Fragment>
|
||||
<CreatorLink href={link} key={index}>
|
||||
{new URL(link).hostname.replace("www.", "").replace(".com", "")}
|
||||
</CreatorLink>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -33,7 +33,7 @@ export const FeaturedAgentCard: React.FC<FeaturedStoreCardProps> = ({
|
||||
<CardTitle className="line-clamp-3 font-poppins text-3xl font-medium text-zinc-800">
|
||||
{agent.agent_name}
|
||||
</CardTitle>
|
||||
<CardDescription className="line-clamp-1 font-sans text-lg font-normal text-zinc-800">
|
||||
<CardDescription className="line-clamp-1 font-sans text-base font-normal text-zinc-800">
|
||||
By {agent.creator}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
@@ -65,7 +65,7 @@ export const FeaturedAgentCard: React.FC<FeaturedStoreCardProps> = ({
|
||||
>
|
||||
<CardDescription
|
||||
data-testid="agent-description"
|
||||
className="line-clamp-6 font-sans text-sm text-neutral-800"
|
||||
className="line-clamp-6 font-sans text-sm text-zinc-600"
|
||||
>
|
||||
{agent.description}
|
||||
</CardDescription>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import * as React from "react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Chip } from "./Chip";
|
||||
|
||||
interface FilterChipsProps {
|
||||
badges: string[];
|
||||
@@ -38,17 +39,13 @@ export const FilterChips: React.FC<FilterChipsProps> = ({
|
||||
return (
|
||||
<div className="flex flex-wrap items-center justify-center gap-3">
|
||||
{badges.map((badge) => (
|
||||
<Badge
|
||||
<div
|
||||
data-testid="filter-chip"
|
||||
key={badge}
|
||||
variant={selectedFilters.includes(badge) ? "secondary" : "outline"}
|
||||
className="rounded-[2rem] border border-neutral-600 px-5 py-3 hover:cursor-pointer hover:bg-neutral-200"
|
||||
onClick={() => handleBadgeClick(badge)}
|
||||
>
|
||||
<p className="font-sans text-base font-normal text-neutral-800 md:text-xl">
|
||||
{badge}
|
||||
</p>
|
||||
</Badge>
|
||||
<Chip className="hover:cursor-pointer">{badge}</Chip>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -17,7 +17,7 @@ interface NavbarLinkProps {
|
||||
const icons = {
|
||||
"/marketplace": IconShoppingCart,
|
||||
"/build": IconBoxes,
|
||||
"/monitor": IconLaptop,
|
||||
"/library": IconLibrary,
|
||||
"/home": IconLibrary,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { useState } from "react";
|
||||
import { useState, useRef } from "react";
|
||||
|
||||
import Image from "next/image";
|
||||
|
||||
@@ -14,12 +14,15 @@ import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||
import { Input } from "../ui/input";
|
||||
import { Label } from "../ui/label";
|
||||
import { Textarea } from "../ui/textarea";
|
||||
import AutogptButton from "./AutogptButton";
|
||||
import AutogptInput from "./AutogptInput";
|
||||
|
||||
export const ProfileInfoForm = ({ profile }: { profile: CreatorDetails }) => {
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [profileData, setProfileData] = useState(profile);
|
||||
const { supabase } = useSupabase();
|
||||
const api = useBackendAPI();
|
||||
const editPhotoRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const submitForm = async () => {
|
||||
try {
|
||||
@@ -101,168 +104,156 @@ export const ProfileInfoForm = ({ profile }: { profile: CreatorDetails }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="md:min-w-md w-full px-4 sm:px-8">
|
||||
<h1 className="mb-6 font-poppins text-4xl font-medium text-neutral-900">
|
||||
Profile
|
||||
</h1>
|
||||
<div className="mb-8 sm:mb-12">
|
||||
<div className="mb-8 flex flex-col items-center gap-4 sm:flex-row">
|
||||
<div className="flex h-[6.25rem] w-[6.25rem] items-center justify-center rounded-full bg-[#DADADA]">
|
||||
{profileData.avatar_url ? (
|
||||
<Image
|
||||
src={profileData.avatar_url}
|
||||
alt="Profile"
|
||||
fill
|
||||
className="rounded-full"
|
||||
/>
|
||||
) : (
|
||||
<IconPersonFill className="h-10 w-10 text-[#7e7e7e]" />
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
ref={editPhotoRef}
|
||||
onChange={async (e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
await handleImageUpload(file);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<AutogptButton onClick={() => editPhotoRef.current?.click()}>
|
||||
Edit photo
|
||||
</AutogptButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-8 sm:mb-12">
|
||||
<div className="mb-8 flex flex-col items-center gap-4 sm:flex-row sm:items-start">
|
||||
<div className="relative h-[130px] w-[130px] rounded-full bg-[#d9d9d9] dark:bg-[#333333]">
|
||||
{profileData.avatar_url ? (
|
||||
<Image
|
||||
src={profileData.avatar_url}
|
||||
alt="Profile"
|
||||
fill
|
||||
className="rounded-full"
|
||||
/>
|
||||
) : (
|
||||
<IconPersonFill className="absolute left-[30px] top-[24px] h-[77.80px] w-[70.63px] text-[#7e7e7e] dark:text-[#999999]" />
|
||||
)}
|
||||
</div>
|
||||
<Label className="mt-11 inline-flex h-[43px] items-center justify-center rounded-[22px] bg-[#15171A] px-6 py-2 font-sans text-sm font-normal text-white transition-colors hover:cursor-pointer hover:bg-[#2D2F34] dark:bg-white dark:text-[#15171A] dark:hover:bg-[#E5E5E5]">
|
||||
<Input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
onChange={async (e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
await handleImageUpload(file);
|
||||
}
|
||||
<form className="space-y-10" onSubmit={submitForm}>
|
||||
{/* Top section */}
|
||||
<section className="max-w-3xl space-y-6">
|
||||
<AutogptInput
|
||||
label="Display name"
|
||||
type="text"
|
||||
name="displayName"
|
||||
defaultValue={profileData.name}
|
||||
placeholder="Enter your display name"
|
||||
className="h-11 w-full rounded-full border border-[#E2E8F0] px-4 py-2.5 font-inter text-base font-normal text-[#7e7e7e] outline-none"
|
||||
onChange={(e) => {
|
||||
const newProfileData = {
|
||||
...profileData,
|
||||
name: e.target.value,
|
||||
};
|
||||
setProfileData(newProfileData);
|
||||
}}
|
||||
/>
|
||||
|
||||
<AutogptInput
|
||||
label="Handle"
|
||||
type="text"
|
||||
name="handle"
|
||||
defaultValue={profileData.username}
|
||||
placeholder="@username"
|
||||
className="h-11 w-full rounded-full border border-[#E2E8F0] px-4 py-2.5 font-inter text-base font-normal text-[#7e7e7e] outline-none"
|
||||
onChange={(e) => {
|
||||
const newProfileData = {
|
||||
...profileData,
|
||||
username: e.target.value,
|
||||
};
|
||||
setProfileData(newProfileData);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="w-full space-y-1.5">
|
||||
<Label className="font-sans text-sm font-medium leading-[1.4rem]">
|
||||
Bio
|
||||
</Label>
|
||||
<Textarea
|
||||
name="bio"
|
||||
defaultValue={profileData.description}
|
||||
placeholder="Tell us about yourself..."
|
||||
className="m-0 h-10 min-h-56 w-full resize-none rounded-3xl border border-zinc-300 bg-white py-2 pl-4 font-sans text-base font-normal text-zinc-800 shadow-none outline-none placeholder:text-zinc-400 focus:border-2 focus:border-[#CBD5E1] focus:shadow-none focus:ring-0"
|
||||
onChange={(e) => {
|
||||
const newProfileData = {
|
||||
...profileData,
|
||||
description: e.target.value,
|
||||
};
|
||||
setProfileData(newProfileData);
|
||||
}}
|
||||
/>
|
||||
Edit photo
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<form className="space-y-8" onSubmit={submitForm}>
|
||||
{/* Top section */}
|
||||
<section className="space-y-6">
|
||||
<div className="w-full space-y-1.5">
|
||||
<Label className="block font-sans text-base font-medium text-[#020617]">
|
||||
Display name
|
||||
</Label>
|
||||
<Input
|
||||
type="text"
|
||||
name="displayName"
|
||||
defaultValue={profileData.name}
|
||||
placeholder="Enter your display name"
|
||||
className="h-11 w-full rounded-full border border-[#E2E8F0] px-4 py-2.5 font-inter text-base font-normal text-[#7e7e7e] outline-none"
|
||||
onChange={(e) => {
|
||||
const newProfileData = {
|
||||
...profileData,
|
||||
name: e.target.value,
|
||||
};
|
||||
setProfileData(newProfileData);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Separator className="bg-neutral-300" />
|
||||
|
||||
<div className="w-full space-y-1.5">
|
||||
<Label className="block font-sans text-base font-medium text-[#020617]">
|
||||
Handle
|
||||
</Label>
|
||||
<Input
|
||||
type="text"
|
||||
name="handle"
|
||||
defaultValue={profileData.username}
|
||||
placeholder="@username"
|
||||
className="h-11 w-full rounded-full border border-[#E2E8F0] px-4 py-2.5 font-inter text-base font-normal text-[#7e7e7e] outline-none"
|
||||
onChange={(e) => {
|
||||
const newProfileData = {
|
||||
...profileData,
|
||||
username: e.target.value,
|
||||
};
|
||||
setProfileData(newProfileData);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="w-full space-y-1.5">
|
||||
<Label className="block font-sans text-base font-medium text-[#020617]">
|
||||
Bio
|
||||
</Label>
|
||||
<Textarea
|
||||
name="bio"
|
||||
defaultValue={profileData.description}
|
||||
placeholder="Tell us about yourself..."
|
||||
className="min-h-56 w-full resize-none rounded-[1rem] border border-[#E2E8F0] px-4 py-2.5 font-inter text-base font-normal text-[#7e7e7e] outline-none"
|
||||
onChange={(e) => {
|
||||
const newProfileData = {
|
||||
...profileData,
|
||||
description: e.target.value,
|
||||
};
|
||||
setProfileData(newProfileData);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Separator className="bg-neutral-300" />
|
||||
|
||||
{/* mid section */}
|
||||
<section className="mb-8 space-y-6">
|
||||
<h2 className="font-poppins text-lg font-semibold text-neutral-500">
|
||||
{/* mid section */}
|
||||
<section className="mb-8 max-w-3xl space-y-6">
|
||||
<div>
|
||||
<h2 className="font-poppins text-base font-medium text-neutral-900">
|
||||
Your links
|
||||
</h2>
|
||||
<p className="font-sans text-base font-medium text-[#020617]">
|
||||
<p className="font-sans text-sm font-normal text-zinc-800">
|
||||
You can display up to 5 links on your profile
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 sm:space-y-6">
|
||||
{[1, 2, 3, 4, 5].map((linkNum) => {
|
||||
const link = profileData.links[linkNum - 1];
|
||||
return (
|
||||
<div key={linkNum} className="w-full space-y-1.5">
|
||||
<Label className="block font-sans text-base font-medium text-[#020617]">
|
||||
Link {linkNum}
|
||||
</Label>
|
||||
<Input
|
||||
type="text"
|
||||
name={`link${linkNum}`}
|
||||
placeholder="https://"
|
||||
defaultValue={link || ""}
|
||||
className="h-11 w-full rounded-full border border-[#E2E8F0] px-4 py-2.5 font-inter text-base font-normal text-[#7e7e7e] outline-none"
|
||||
onChange={(e) => {
|
||||
const newLinks = [...profileData.links];
|
||||
newLinks[linkNum - 1] = e.target.value;
|
||||
const newProfileData = {
|
||||
...profileData,
|
||||
links: newLinks,
|
||||
};
|
||||
setProfileData(newProfileData);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
<div className="space-y-4 sm:space-y-6">
|
||||
{[1, 2, 3, 4, 5].map((linkNum) => {
|
||||
const link = profileData.links[linkNum - 1];
|
||||
return (
|
||||
<AutogptInput
|
||||
key={linkNum}
|
||||
label={`Link ${linkNum}`}
|
||||
type="text"
|
||||
name={`link${linkNum}`}
|
||||
placeholder="https://"
|
||||
defaultValue={link || ""}
|
||||
className="h-11 w-full rounded-full border border-[#E2E8F0] px-4 py-2.5 font-inter text-base font-normal text-[#7e7e7e] outline-none"
|
||||
onChange={(e) => {
|
||||
const newLinks = [...profileData.links];
|
||||
newLinks[linkNum - 1] = e.target.value;
|
||||
const newProfileData = {
|
||||
...profileData,
|
||||
links: newLinks,
|
||||
};
|
||||
setProfileData(newProfileData);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* buttons */}
|
||||
<section className="flex h-[50px] items-center justify-end gap-3 py-8">
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
className="h-[50px] rounded-[35px] bg-neutral-200 px-6 py-3 font-sans text-base font-medium text-neutral-800 transition-colors hover:bg-neutral-300 dark:border-neutral-700 dark:bg-neutral-700 dark:text-neutral-200 dark:hover:border-neutral-600 dark:hover:bg-neutral-600"
|
||||
onClick={() => {
|
||||
setProfileData(profile);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="h-[50px] rounded-[35px] bg-neutral-800 px-6 py-3 font-sans text-base font-medium text-white transition-colors hover:bg-neutral-900 dark:bg-neutral-200 dark:text-neutral-900 dark:hover:bg-neutral-100"
|
||||
onClick={submitForm}
|
||||
>
|
||||
{isSubmitting ? "Saving..." : "Save changes"}
|
||||
</Button>
|
||||
</section>
|
||||
</form>
|
||||
</div>
|
||||
{/* buttons */}
|
||||
<section className="flex h-[50px] items-center justify-end gap-3 py-8">
|
||||
<AutogptButton
|
||||
type="button"
|
||||
variant="secondary"
|
||||
className="h-[50px] rounded-[35px] bg-neutral-200 px-6 py-3 font-sans text-base font-medium text-neutral-800 transition-colors hover:bg-neutral-300 dark:border-neutral-700 dark:bg-neutral-700 dark:text-neutral-200 dark:hover:border-neutral-600 dark:hover:bg-neutral-600"
|
||||
onClick={() => {
|
||||
setProfileData(profile);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</AutogptButton>
|
||||
<AutogptButton
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="h-[50px] rounded-[35px] bg-neutral-800 px-6 py-3 font-sans text-base font-medium text-white transition-colors hover:bg-neutral-900 dark:bg-neutral-200 dark:text-neutral-900 dark:hover:bg-neutral-100"
|
||||
onClick={submitForm}
|
||||
>
|
||||
{isSubmitting ? "Saving..." : "Save changes"}
|
||||
</AutogptButton>
|
||||
</section>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
"use client";
|
||||
import * as React from "react";
|
||||
import Link from "next/link";
|
||||
import { Button } from "./Button";
|
||||
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
|
||||
import { Menu } from "lucide-react";
|
||||
import { IconDashboardLayout } from "../ui/icons";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
export interface SidebarLink {
|
||||
text: string;
|
||||
@@ -28,54 +27,32 @@ const getDefaultIconForLink = () => {
|
||||
export const Sidebar: React.FC<SidebarProps> = ({ linkGroups }) => {
|
||||
// Extract all links from linkGroups
|
||||
const allLinks = linkGroups.flatMap((group) => group.links);
|
||||
const pathname = usePathname();
|
||||
|
||||
// Function to render link items
|
||||
const renderLinks = () => {
|
||||
return allLinks.map((link, index) => (
|
||||
<Link
|
||||
key={`${link.href}-${index}`}
|
||||
href={link.href}
|
||||
className="inline-flex w-full items-center gap-2.5 rounded-xl px-3 py-3 text-neutral-800 hover:bg-neutral-800 hover:text-white dark:text-neutral-200 dark:hover:bg-neutral-700 dark:hover:text-white"
|
||||
>
|
||||
{link.icon || getDefaultIconForLink()}
|
||||
<div className="p-ui-medium text-base font-medium leading-normal">
|
||||
{link.text}
|
||||
</div>
|
||||
</Link>
|
||||
));
|
||||
return allLinks.map((link, index) => {
|
||||
const isActive = pathname === link.href;
|
||||
return (
|
||||
<Link
|
||||
key={`${link.href}-${index}`}
|
||||
href={link.href}
|
||||
className={`inline-flex w-full items-center gap-2.5 rounded-xl px-3 py-3 ${
|
||||
isActive
|
||||
? "bg-neutral-800 text-white dark:bg-neutral-700 dark:text-white"
|
||||
: "text-neutral-800 hover:bg-neutral-800 hover:text-white dark:text-neutral-200 dark:hover:bg-neutral-700 dark:hover:text-white"
|
||||
}`}
|
||||
>
|
||||
{link.icon || getDefaultIconForLink()}
|
||||
<p className="font-sans text-base font-medium">{link.text}</p>
|
||||
</Link>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button
|
||||
aria-label="Open sidebar menu"
|
||||
className="fixed left-4 top-4 z-50 flex h-14 w-14 items-center justify-center rounded-lg border border-neutral-500 bg-neutral-200 hover:bg-gray-200/50 dark:border-neutral-700 dark:bg-neutral-800 dark:hover:bg-gray-700/50 md:block lg:hidden"
|
||||
>
|
||||
<Menu className="h-8 w-8 stroke-black dark:stroke-white" />
|
||||
<span className="sr-only">Open sidebar menu</span>
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent
|
||||
side="left"
|
||||
className="z-50 w-[280px] border-none p-0 dark:bg-neutral-900 sm:w-[280px]"
|
||||
>
|
||||
<div className="h-full w-full rounded-2xl bg-zinc-200 dark:bg-zinc-800">
|
||||
<div className="inline-flex h-[264px] flex-col items-start justify-start gap-6 p-3">
|
||||
{renderLinks()}
|
||||
</div>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
|
||||
<div className="relative hidden h-[912px] w-[234px] border-none lg:block">
|
||||
<div className="h-full w-full rounded-2xl bg-zinc-200 dark:bg-zinc-800">
|
||||
<div className="inline-flex h-[264px] flex-col items-start justify-start gap-6 p-3">
|
||||
{renderLinks()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
<div className="sticky top-24 flex h-[calc(100vh-7rem)] w-60 flex-col gap-6 rounded-[1rem] bg-zinc-200 p-3">
|
||||
{renderLinks()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,12 +3,12 @@ import { StoreCard } from "./StoreCard";
|
||||
import { userEvent, within, expect } from "@storybook/test";
|
||||
|
||||
const meta = {
|
||||
title: "AGPT UI/StoreCard",
|
||||
title: "new/StoreCard",
|
||||
component: StoreCard,
|
||||
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="flex items-center justify-center p-4">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
|
||||
@@ -32,7 +32,7 @@ export const StoreCard: React.FC<StoreCardProps> = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex h-[27rem] w-full max-w-md cursor-pointer flex-col items-start rounded-3xl transition-all duration-300 dark:bg-transparent dark:hover:shadow-gray-700"
|
||||
className="w-full min-w-80 max-w-md space-y-2 rounded-3xl bg-white p-2 pb-3 hover:bg-gray-50"
|
||||
onClick={handleClick}
|
||||
data-testid="store-card"
|
||||
role="button"
|
||||
@@ -45,7 +45,7 @@ export const StoreCard: React.FC<StoreCardProps> = ({
|
||||
}}
|
||||
>
|
||||
{/* First Section: Image with Avatar */}
|
||||
<div className="relative aspect-[2/1.2] w-full overflow-hidden rounded-3xl md:aspect-[2.17/1]">
|
||||
<div className="relative aspect-[2/1.2] w-full overflow-hidden rounded-3xl md:aspect-[1.78/1]">
|
||||
{agentImage && (
|
||||
<Image
|
||||
src={agentImage}
|
||||
@@ -56,7 +56,7 @@ export const StoreCard: React.FC<StoreCardProps> = ({
|
||||
/>
|
||||
)}
|
||||
{!hideAvatar && (
|
||||
<div className="absolute bottom-4 left-4">
|
||||
<div className="absolute bottom-4 left-4 rounded-full border border-zinc-200">
|
||||
<Avatar className="h-16 w-16">
|
||||
{avatarSrc && (
|
||||
<AvatarImage
|
||||
@@ -72,46 +72,40 @@ export const StoreCard: React.FC<StoreCardProps> = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-3 flex w-full flex-1 flex-col">
|
||||
<div className="flex w-full flex-1 flex-col">
|
||||
{/* Second Section: Agent Name and Creator Name */}
|
||||
<div className="flex w-full flex-col">
|
||||
<h3 className="line-clamp-2 font-poppins text-2xl font-semibold text-[#272727] dark:text-neutral-100">
|
||||
<div className="flex w-full flex-col px-1.5">
|
||||
<h3 className="line-clamp-2 h-12 font-sans text-base font-medium text-zinc-800 dark:text-neutral-100">
|
||||
{agentName}
|
||||
</h3>
|
||||
{!hideAvatar && creatorName && (
|
||||
<p className="mt-3 truncate font-sans text-xl font-normal text-neutral-600 dark:text-neutral-400">
|
||||
<p className="truncate font-sans text-sm font-normal text-zinc-600 dark:text-neutral-400">
|
||||
by {creatorName}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Third Section: Description */}
|
||||
<div className="mt-2.5 flex w-full flex-col">
|
||||
<p className="line-clamp-3 font-sans text-base font-normal text-neutral-600 dark:text-neutral-400">
|
||||
<div className="flex h-18 w-full flex-col px-1.5 pt-2">
|
||||
<p className="line-clamp-3 font-sans text-sm font-normal text-zinc-500 dark:text-neutral-400">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex-grow" />
|
||||
{/* Spacer to push stats to bottom */}
|
||||
|
||||
{/* Fourth Section: Stats Row - aligned to bottom */}
|
||||
<div className="mt-5 w-full">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="font-sans text-lg font-semibold text-neutral-800 dark:text-neutral-200">
|
||||
{runs.toLocaleString()} runs
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-sans text-lg font-semibold text-neutral-800 dark:text-neutral-200">
|
||||
{rating.toFixed(1)}
|
||||
</span>
|
||||
<div
|
||||
className="inline-flex items-center"
|
||||
role="img"
|
||||
aria-label={`Rating: ${rating.toFixed(1)} out of 5 stars`}
|
||||
>
|
||||
{StarRatingIcons(rating)}
|
||||
</div>
|
||||
<div className="mt-2.5 flex items-center justify-between px-1.5 pt-2">
|
||||
<div className="font-sans text-sm font-medium text-zinc-800 dark:text-neutral-200">
|
||||
{runs.toLocaleString()} runs
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-sans text-sm font-medium text-zinc-800 dark:text-neutral-200">
|
||||
{rating.toFixed(1)}
|
||||
</span>
|
||||
<div
|
||||
className="inline-flex items-center"
|
||||
role="img"
|
||||
aria-label={`Rating: ${rating.toFixed(1)} out of 5 stars`}
|
||||
>
|
||||
{StarRatingIcons(rating)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Agent, AgentsSection } from "./AgentsSection";
|
||||
import { userEvent, within, expect } from "@storybook/test";
|
||||
|
||||
const meta = {
|
||||
title: "AGPT UI/Composite/Agents Section",
|
||||
title: "new/Composite/Agents Section",
|
||||
component: AgentsSection,
|
||||
|
||||
decorators: [
|
||||
|
||||
@@ -33,6 +33,7 @@ export const AgentsSection: React.FC<AgentsSectionProps> = ({
|
||||
sectionTitle,
|
||||
agents: allAgents,
|
||||
hideAvatars = false,
|
||||
className,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
|
||||
@@ -46,10 +47,12 @@ export const AgentsSection: React.FC<AgentsSectionProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col items-center justify-center">
|
||||
<div
|
||||
className={`flex w-full flex-col items-center justify-center ${className}`}
|
||||
>
|
||||
<div className="w-full">
|
||||
<div
|
||||
className={`mb-9 pl-4 font-poppins text-lg font-semibold text-neutral-800 dark:text-neutral-200 md:pl-0`}
|
||||
className={`mb-9 pl-4 text-base font-medium text-zinc-500 dark:text-zinc-200 md:pl-0`}
|
||||
>
|
||||
{sectionTitle}
|
||||
</div>
|
||||
@@ -85,7 +88,7 @@ export const AgentsSection: React.FC<AgentsSectionProps> = ({
|
||||
</CarouselContent>
|
||||
</Carousel>
|
||||
|
||||
<div className="hidden grid-cols-1 place-items-center gap-6 md:grid md:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4">
|
||||
<div className="hidden grid-cols-1 place-items-center gap-5 md:grid md:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4">
|
||||
{displayedAgents.map((agent, index) => (
|
||||
<StoreCard
|
||||
key={index}
|
||||
|
||||
@@ -33,7 +33,7 @@ export const FeaturedCreators: React.FC<FeaturedCreatorsProps> = ({
|
||||
return (
|
||||
<div className="flex w-full flex-col items-center justify-center">
|
||||
<div className="w-full">
|
||||
<h2 className="mb-9 font-poppins text-lg font-semibold text-neutral-800 dark:text-neutral-200">
|
||||
<h2 className="mb-9 text-base font-medium text-zinc-500 dark:text-zinc-200">
|
||||
{title}
|
||||
</h2>
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ export const FeaturedSection: React.FC<FeaturedSectionProps> = ({
|
||||
|
||||
return (
|
||||
<section className="w-full space-y-8">
|
||||
<h2 className="font-poppins text-lg font-semibold text-neutral-800">
|
||||
<h2 className="pl-10 font-poppins text-base font-medium text-zinc-500">
|
||||
Featured agents
|
||||
</h2>
|
||||
|
||||
@@ -61,7 +61,7 @@ export const FeaturedSection: React.FC<FeaturedSectionProps> = ({
|
||||
{featuredAgents.map((agent, index) => (
|
||||
<CarouselItem
|
||||
key={index}
|
||||
className={`w-fit flex-none ${index === featuredAgents.length - 1 ? "mr-4" : ""}`}
|
||||
className={`w-fit flex-none ${index === featuredAgents.length - 1 ? "mr-4" : ""} ${index === 0 ? "pl-14" : ""}`}
|
||||
>
|
||||
<Link
|
||||
href={`/marketplace/agent/${encodeURIComponent(agent.creator)}/${encodeURIComponent(agent.slug)}`}
|
||||
@@ -75,7 +75,7 @@ export const FeaturedSection: React.FC<FeaturedSectionProps> = ({
|
||||
</CarouselItem>
|
||||
))}
|
||||
</CarouselContent>
|
||||
<div className="relative mt-4">
|
||||
<div className="relative mt-4 px-10">
|
||||
<CarouselIndicator />
|
||||
<CarouselPrevious
|
||||
afterClick={handlePrevSlide}
|
||||
|
||||
@@ -23,7 +23,7 @@ export const HeroSection: React.FC = () => {
|
||||
return (
|
||||
<div className="mx-auto flex w-[90%] flex-col items-center justify-center pb-12 pt-16 md:w-full md:pb-28 md:pt-32">
|
||||
{/* Title */}
|
||||
<h1 className="mb-4 text-center font-poppins text-2xl font-semibold leading-8 text-neutral-950 md:mb-9 md:text-5xl md:leading-[3rem]">
|
||||
<h1 className="mb-4 text-center font-poppins text-2xl font-semibold leading-8 text-zinc-900 md:mb-9 md:text-[2.75rem] md:leading-[3.5rem]">
|
||||
<span>Explore AI agents built for </span>
|
||||
<span className="text-violet-600">you</span>
|
||||
<br />
|
||||
@@ -32,7 +32,7 @@ export const HeroSection: React.FC = () => {
|
||||
</h1>
|
||||
|
||||
{/* Description */}
|
||||
<h3 className="mb-6 text-center font-sans text-lg text-neutral-700 md:mb-12 md:text-2xl">
|
||||
<h3 className="mb-6 text-center font-sans text-lg text-zinc-600 md:mb-12 md:text-xl">
|
||||
Bringing you AI agents designed by thinkers from around the world
|
||||
</h3>
|
||||
|
||||
|
||||
@@ -24,6 +24,8 @@ import {
|
||||
NotificationPreference,
|
||||
NotificationPreferenceDTO,
|
||||
} from "@/lib/autogpt-server-api";
|
||||
import AutogptInput from "@/components/agptui/AutogptInput";
|
||||
import AutogptButton from "@/components/agptui/AutogptButton";
|
||||
|
||||
const formSchema = z
|
||||
.object({
|
||||
@@ -119,10 +121,10 @@ export default function SettingsForm({ user, preferences }: SettingsFormProps) {
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="flex flex-col gap-8"
|
||||
className="flex flex-col gap-10"
|
||||
>
|
||||
{/* Account Settings Section */}
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex max-w-3xl flex-col gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
@@ -130,7 +132,7 @@ export default function SettingsForm({ user, preferences }: SettingsFormProps) {
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} type="email" />
|
||||
<AutogptInput {...field} type="email" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -144,7 +146,7 @@ export default function SettingsForm({ user, preferences }: SettingsFormProps) {
|
||||
<FormItem>
|
||||
<FormLabel>New Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
<AutogptInput
|
||||
{...field}
|
||||
type="password"
|
||||
placeholder="************"
|
||||
@@ -162,7 +164,7 @@ export default function SettingsForm({ user, preferences }: SettingsFormProps) {
|
||||
<FormItem>
|
||||
<FormLabel>Confirm New Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
<AutogptInput
|
||||
{...field}
|
||||
type="password"
|
||||
placeholder="************"
|
||||
@@ -174,11 +176,13 @@ export default function SettingsForm({ user, preferences }: SettingsFormProps) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
<Separator className="bg-neutral-300" />
|
||||
|
||||
{/* Notifications Section */}
|
||||
<div className="flex flex-col gap-6">
|
||||
<h3 className="text-lg font-medium">Notifications</h3>
|
||||
<div className="flex max-w-3xl flex-col gap-6">
|
||||
<h3 className="font-poppins text-base font-medium text-neutral-900">
|
||||
Notifications
|
||||
</h3>
|
||||
|
||||
{/* Agent Notifications */}
|
||||
<div className="flex flex-col gap-4">
|
||||
@@ -379,22 +383,25 @@ export default function SettingsForm({ user, preferences }: SettingsFormProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator className="bg-neutral-300" />
|
||||
|
||||
{/* Form Actions */}
|
||||
<div className="flex justify-end gap-4">
|
||||
<Button
|
||||
<AutogptButton
|
||||
variant="outline"
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
disabled={form.formState.isSubmitting}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
</AutogptButton>
|
||||
<AutogptButton
|
||||
variant={"default"}
|
||||
type="submit"
|
||||
disabled={form.formState.isSubmitting || !form.formState.isDirty}
|
||||
>
|
||||
{form.formState.isSubmitting ? "Saving..." : "Save changes"}
|
||||
</Button>
|
||||
</AutogptButton>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
@@ -211,9 +211,9 @@ const CarouselPrevious = React.forwardRef<
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
"pointer absolute h-[52px] w-[52px] rounded-full",
|
||||
"pointer absolute h-10 w-10 rounded-full border border-zinc-700 bg-white text-zinc-800 hover:bg-zinc-800 hover:text-white",
|
||||
orientation === "horizontal"
|
||||
? "right-20 top-0"
|
||||
? "right-24 top-0"
|
||||
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||
className,
|
||||
)}
|
||||
@@ -226,7 +226,7 @@ const CarouselPrevious = React.forwardRef<
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<ChevronLeft className="h-8 w-8" strokeWidth={1.25} />
|
||||
<ChevronLeft className="h-5 w-5" strokeWidth={1.25} />
|
||||
<span className="sr-only">Previous slide</span>
|
||||
</Button>
|
||||
);
|
||||
@@ -257,9 +257,9 @@ const CarouselNext = React.forwardRef<
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
"absolute h-[52px] w-[52px] rounded-full",
|
||||
"order absolute h-10 w-10 rounded-full border-zinc-700 bg-white text-zinc-800 hover:bg-zinc-800 hover:text-white",
|
||||
orientation === "horizontal"
|
||||
? "right-4 top-0"
|
||||
? "right-12 top-0"
|
||||
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||
className,
|
||||
)}
|
||||
@@ -267,7 +267,7 @@ const CarouselNext = React.forwardRef<
|
||||
onClick={handleClick}
|
||||
{...props}
|
||||
>
|
||||
<ChevronRight className="h-8 w-8" strokeWidth={1.25} />
|
||||
<ChevronRight className="h-5 w-5" strokeWidth={1.25} />
|
||||
<span className="sr-only">Next slide</span>
|
||||
</Button>
|
||||
);
|
||||
@@ -311,8 +311,8 @@ const CarouselIndicator = React.forwardRef<
|
||||
onClick={() => scrollTo(index)}
|
||||
className={cn(
|
||||
selectedIndex === index
|
||||
? "h-3 w-[52px] rounded-[39px] bg-neutral-800 transition-all duration-500 dark:bg-neutral-200"
|
||||
: "h-3 w-3 rounded-full bg-neutral-300 transition-all duration-500 dark:bg-neutral-600",
|
||||
? "h-3 w-[52px] rounded-[39px] bg-zinc-600 transition-all duration-500 dark:bg-neutral-200"
|
||||
: "h-3 w-3 rounded-full bg-zinc-300 transition-all duration-500 dark:bg-neutral-600",
|
||||
"cursor-pointer",
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -58,7 +58,7 @@ const TableRow = React.forwardRef<
|
||||
<tr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-b transition-colors hover:bg-neutral-100/50 data-[state=selected]:bg-neutral-100 dark:hover:bg-neutral-800/50 dark:data-[state=selected]:bg-neutral-800",
|
||||
"border-b border-neutral-300 transition-colors hover:bg-neutral-100/50 data-[state=selected]:bg-neutral-100 dark:hover:bg-neutral-800/50 dark:data-[state=selected]:bg-neutral-800",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
Reference in New Issue
Block a user