mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
fix(frontend/marketplace): comprehensive marketplace UI redesign (#12462)
## Summary <img width="600" height="964" alt="Screenshot_2026-03-19_at_00 07 52" src="https://github.com/user-attachments/assets/95c0430a-26a3-499b-8f6a-25b9715d3012" /> <img width="600" height="968" alt="Screenshot_2026-03-19_at_00 08 01" src="https://github.com/user-attachments/assets/d440c3b0-c247-4f13-bf82-a51ff2e50902" /> <img width="600" height="939" alt="Screenshot_2026-03-19_at_00 08 14" src="https://github.com/user-attachments/assets/f19be759-e102-4a95-9474-64f18bce60cf" />" <img width="600" height="953" alt="Screenshot_2026-03-19_at_00 08 24" src="https://github.com/user-attachments/assets/ba4fa644-3958-45e2-89e9-a6a4448c63c5" /> - Re-style and re-skin the Marketplace pages to look more "professional" ... - Move the `Give feedback` button to the header ## Test plan - [x] Verify marketplace page search bar matches Form text field styling - [x] Verify agent cards have padding and subtle border - [x] Verify hover/focus states work correctly - [x] Check responsive behavior at different breakpoints 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,33 +8,39 @@ import { Text } from "@/components/atoms/Text/Text";
|
||||
import { useJumpBackIn } from "./useJumpBackIn";
|
||||
|
||||
export function JumpBackIn() {
|
||||
const { agent, isLoading } = useJumpBackIn();
|
||||
const { execution, isLoading } = useJumpBackIn();
|
||||
|
||||
if (isLoading || !agent) {
|
||||
if (isLoading || !execution) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const href = execution.libraryAgentId
|
||||
? `/library/agents/${execution.libraryAgentId}?activeTab=runs&activeItem=${execution.id}`
|
||||
: "#";
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between rounded-large border border-zinc-200 bg-gradient-to-r from-zinc-50 to-white px-5 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-zinc-900">
|
||||
<Lightning size={18} weight="fill" className="text-white" />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<Text variant="small" className="text-zinc-500">
|
||||
Continue where you left off
|
||||
</Text>
|
||||
<Text variant="body-medium" className="text-zinc-900">
|
||||
{agent.name}
|
||||
</Text>
|
||||
<div className="rounded-large bg-gradient-to-r from-zinc-200 via-zinc-200/60 to-indigo-200/50 p-[1px]">
|
||||
<div className="flex items-center justify-between rounded-large bg-[#F6F7F8] px-5 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-zinc-900">
|
||||
<Lightning size={18} weight="fill" className="text-white" />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<Text variant="small" className="text-zinc-500">
|
||||
{execution.statusLabel} · {execution.duration}
|
||||
</Text>
|
||||
<Text variant="body-medium" className="text-zinc-900">
|
||||
{execution.agentName}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
<NextLink href={href}>
|
||||
<Button variant="secondary" size="small" className="gap-1.5">
|
||||
Jump Back In
|
||||
<ArrowRight size={16} />
|
||||
</Button>
|
||||
</NextLink>
|
||||
</div>
|
||||
<NextLink href={`/library/agents/${agent.id}`}>
|
||||
<Button variant="primary" size="small" className="gap-1.5">
|
||||
Jump Back In
|
||||
<ArrowRight size={16} />
|
||||
</Button>
|
||||
</NextLink>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,28 +1,82 @@
|
||||
"use client";
|
||||
|
||||
import { useGetV2ListLibraryAgents } from "@/app/api/__generated__/endpoints/library/library";
|
||||
import { useGetV1ListAllExecutions } from "@/app/api/__generated__/endpoints/graphs/graphs";
|
||||
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
|
||||
import { okData } from "@/app/api/helpers";
|
||||
import { useLibraryAgents } from "@/hooks/useLibraryAgents/useLibraryAgents";
|
||||
import { useMemo } from "react";
|
||||
|
||||
function isActive(status: AgentExecutionStatus) {
|
||||
return (
|
||||
status === AgentExecutionStatus.RUNNING ||
|
||||
status === AgentExecutionStatus.QUEUED ||
|
||||
status === AgentExecutionStatus.REVIEW
|
||||
);
|
||||
}
|
||||
|
||||
function formatDuration(startedAt: Date | string | null | undefined): string {
|
||||
if (!startedAt) return "";
|
||||
|
||||
const start = new Date(startedAt);
|
||||
if (isNaN(start.getTime())) return "";
|
||||
|
||||
const ms = Date.now() - start.getTime();
|
||||
if (ms < 0) return "";
|
||||
|
||||
const sec = Math.floor(ms / 1000);
|
||||
if (sec < 5) return "a few seconds";
|
||||
if (sec < 60) return `${sec}s`;
|
||||
const min = Math.floor(sec / 60);
|
||||
if (min < 60) return `${min}m ${sec % 60}s`;
|
||||
const hr = Math.floor(min / 60);
|
||||
return `${hr}h ${min % 60}m`;
|
||||
}
|
||||
|
||||
function getStatusLabel(status: AgentExecutionStatus) {
|
||||
if (status === AgentExecutionStatus.RUNNING) return "Running";
|
||||
if (status === AgentExecutionStatus.QUEUED) return "Queued";
|
||||
if (status === AgentExecutionStatus.REVIEW) return "Awaiting approval";
|
||||
return "";
|
||||
}
|
||||
|
||||
export function useJumpBackIn() {
|
||||
const { data, isLoading } = useGetV2ListLibraryAgents(
|
||||
{
|
||||
page: 1,
|
||||
page_size: 1,
|
||||
sort_by: "updatedAt",
|
||||
},
|
||||
{
|
||||
const { data: executions, isLoading: executionsLoading } =
|
||||
useGetV1ListAllExecutions({
|
||||
query: { select: okData },
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// The API doesn't include execution data by default (include_executions is
|
||||
// internal to the backend), so recent_executions is always empty here.
|
||||
// We use the most recently updated agent as the "jump back in" candidate
|
||||
// instead — updatedAt is the best available proxy for recent activity.
|
||||
const agent = data?.agents[0] ?? null;
|
||||
const { agentInfoMap, isRefreshing: agentsLoading } = useLibraryAgents();
|
||||
|
||||
const activeExecution = useMemo(() => {
|
||||
if (!executions) return null;
|
||||
|
||||
const active = executions
|
||||
.filter((e) => isActive(e.status))
|
||||
.sort((a, b) => {
|
||||
const aTime = a.started_at ? new Date(a.started_at).getTime() : 0;
|
||||
const bTime = b.started_at ? new Date(b.started_at).getTime() : 0;
|
||||
return bTime - aTime;
|
||||
});
|
||||
|
||||
return active[0] ?? null;
|
||||
}, [executions]);
|
||||
|
||||
const enriched = useMemo(() => {
|
||||
if (!activeExecution) return null;
|
||||
|
||||
const info = agentInfoMap.get(activeExecution.graph_id);
|
||||
return {
|
||||
id: activeExecution.id,
|
||||
agentName: info?.name ?? "Unknown Agent",
|
||||
libraryAgentId: info?.library_agent_id,
|
||||
status: activeExecution.status,
|
||||
statusLabel: getStatusLabel(activeExecution.status),
|
||||
duration: formatDuration(activeExecution.started_at),
|
||||
};
|
||||
}, [activeExecution, agentInfoMap]);
|
||||
|
||||
return {
|
||||
agent,
|
||||
isLoading,
|
||||
execution: enriched,
|
||||
isLoading: executionsLoading || agentsLoading,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { LibraryAgentSort } from "@/app/api/__generated__/models/libraryAgentSort";
|
||||
import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner";
|
||||
import { InfiniteScroll } from "@/components/contextual/InfiniteScroll/InfiniteScroll";
|
||||
import { LibraryActionSubHeader } from "../LibraryActionSubHeader/LibraryActionSubHeader";
|
||||
import { LibraryAgentCard } from "../LibraryAgentCard/LibraryAgentCard";
|
||||
import { LibraryFolder } from "../LibraryFolder/LibraryFolder";
|
||||
import { LibrarySubSection } from "../LibrarySubSection/LibrarySubSection";
|
||||
@@ -96,7 +95,8 @@ export function LibraryAgentList({
|
||||
const {
|
||||
isFavoritesTab,
|
||||
agentLoading,
|
||||
agentCount,
|
||||
allAgentsCount,
|
||||
favoritesCount,
|
||||
agents,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
@@ -120,19 +120,18 @@ export function LibraryAgentList({
|
||||
|
||||
return (
|
||||
<>
|
||||
<LibraryActionSubHeader
|
||||
agentCount={agentCount}
|
||||
setLibrarySort={setLibrarySort}
|
||||
/>
|
||||
{!selectedFolderId && (
|
||||
<LibrarySubSection
|
||||
tabs={tabs}
|
||||
activeTab={activeTab}
|
||||
onTabChange={onTabChange}
|
||||
allCount={allAgentsCount}
|
||||
favoritesCount={favoritesCount}
|
||||
setLibrarySort={setLibrarySort}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<div className="pt-4">
|
||||
{selectedFolderId && (
|
||||
<div className="mb-4 flex items-center gap-2">
|
||||
<button
|
||||
|
||||
@@ -209,6 +209,8 @@ export function useLibraryAgentList({
|
||||
isFavoritesTab,
|
||||
agentLoading,
|
||||
agentCount,
|
||||
allAgentsCount,
|
||||
favoritesCount: favoriteAgentsData.agentCount,
|
||||
agents,
|
||||
hasNextPage: agentsHasNextPage,
|
||||
isFetchingNextPage: agentsIsFetchingNextPage,
|
||||
|
||||
@@ -19,9 +19,11 @@ export function LibrarySortMenu({ setLibrarySort }: Props) {
|
||||
const { handleSortChange } = useLibrarySortMenu({ setLibrarySort });
|
||||
return (
|
||||
<div className="flex items-center" data-testid="sort-by-dropdown">
|
||||
<span className="hidden whitespace-nowrap sm:inline">sort by</span>
|
||||
<span className="hidden whitespace-nowrap text-sm sm:inline">
|
||||
sort by
|
||||
</span>
|
||||
<Select onValueChange={handleSortChange}>
|
||||
<SelectTrigger className="ml-1 w-fit space-x-1 border-none px-0 text-base underline underline-offset-4 shadow-none">
|
||||
<SelectTrigger className="ml-1 w-fit space-x-1 border-none px-0 text-sm underline underline-offset-4 shadow-none">
|
||||
<ArrowDownNarrowWideIcon className="h-4 w-4 sm:hidden" />
|
||||
<SelectValue placeholder="Last Modified" />
|
||||
</SelectTrigger>
|
||||
|
||||
@@ -4,17 +4,29 @@ import {
|
||||
TabsLineList,
|
||||
TabsLineTrigger,
|
||||
} from "@/components/molecules/TabsLine/TabsLine";
|
||||
import { LibraryAgentSort } from "@/app/api/__generated__/models/libraryAgentSort";
|
||||
import { useFavoriteAnimation } from "../../context/FavoriteAnimationContext";
|
||||
import { LibraryTab } from "../../types";
|
||||
import LibraryFolderCreationDialog from "../LibraryFolderCreationDialog/LibraryFolderCreationDialog";
|
||||
import { LibrarySortMenu } from "../LibrarySortMenu/LibrarySortMenu";
|
||||
|
||||
interface Props {
|
||||
tabs: LibraryTab[];
|
||||
activeTab: string;
|
||||
onTabChange: (tabId: string) => void;
|
||||
allCount: number;
|
||||
favoritesCount: number;
|
||||
setLibrarySort: (value: LibraryAgentSort) => void;
|
||||
}
|
||||
|
||||
export function LibrarySubSection({ tabs, activeTab, onTabChange }: Props) {
|
||||
export function LibrarySubSection({
|
||||
tabs,
|
||||
activeTab,
|
||||
onTabChange,
|
||||
allCount,
|
||||
favoritesCount,
|
||||
setLibrarySort,
|
||||
}: Props) {
|
||||
const { registerFavoritesTabRef } = useFavoriteAnimation();
|
||||
const favoritesRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
@@ -25,8 +37,21 @@ export function LibrarySubSection({ tabs, activeTab, onTabChange }: Props) {
|
||||
};
|
||||
}, [registerFavoritesTabRef]);
|
||||
|
||||
function getTabLabel(tab: LibraryTab) {
|
||||
if (tab.id === "all") {
|
||||
return `${tab.title} ${allCount}`;
|
||||
}
|
||||
if (tab.id === "favorites") {
|
||||
return favoritesCount > 0 ? `${tab.title} ${favoritesCount}` : tab.title;
|
||||
}
|
||||
return tab.title;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<span data-testid="agents-count" className="sr-only">
|
||||
{allCount}
|
||||
</span>
|
||||
<TabsLine value={activeTab} onValueChange={onTabChange}>
|
||||
<TabsLineList>
|
||||
{tabs.map((tab) => (
|
||||
@@ -35,14 +60,18 @@ export function LibrarySubSection({ tabs, activeTab, onTabChange }: Props) {
|
||||
value={tab.id}
|
||||
ref={tab.id === "favorites" ? favoritesRef : undefined}
|
||||
className="inline-flex items-center gap-1.5"
|
||||
disabled={tab.id === "favorites" && favoritesCount === 0}
|
||||
>
|
||||
<tab.icon size={16} />
|
||||
{tab.title}
|
||||
{getTabLabel(tab)}
|
||||
</TabsLineTrigger>
|
||||
))}
|
||||
</TabsLineList>
|
||||
</TabsLine>
|
||||
<LibraryFolderCreationDialog />
|
||||
<div className="hidden items-center gap-6 md:flex">
|
||||
<LibraryFolderCreationDialog />
|
||||
<LibrarySortMenu setLibrarySort={setLibrarySort} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -39,8 +39,8 @@ export default function LibraryPage() {
|
||||
onAnimationComplete={handleFavoriteAnimationComplete}
|
||||
>
|
||||
<main className="pt-160 container min-h-screen space-y-4 pb-20 pt-16 sm:px-8 md:px-12">
|
||||
<JumpBackIn />
|
||||
<LibraryActionHeader setSearchTerm={setSearchTerm} />
|
||||
<JumpBackIn />
|
||||
<LibraryAgentList
|
||||
searchTerm={searchTerm}
|
||||
librarySort={librarySort}
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
getGetV2ListLibraryAgentsQueryKey,
|
||||
useDeleteV2DeleteLibraryAgent,
|
||||
useGetV2ListLibraryAgents,
|
||||
usePostV2AddMarketplaceAgent,
|
||||
} from "@/app/api/__generated__/endpoints/library/library";
|
||||
import { getV2GetSpecificAgent } from "@/app/api/__generated__/endpoints/store/store";
|
||||
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
||||
import { LibraryAgentResponse } from "@/app/api/__generated__/models/libraryAgentResponse";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
|
||||
import { analytics } from "@/services/analytics";
|
||||
import { PlusIcon } from "@phosphor-icons/react";
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
|
||||
function UndoActions({
|
||||
libraryAgentID,
|
||||
libraryHref,
|
||||
onUndo,
|
||||
}: {
|
||||
libraryAgentID: string;
|
||||
libraryHref: string;
|
||||
onUndo: (id: string) => Promise<void>;
|
||||
}) {
|
||||
const [isUndoing, setIsUndoing] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="mt-6 flex items-center gap-2">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="small"
|
||||
as="NextLink"
|
||||
className="bg-neutral-200 text-zinc-900 hover:bg-neutral-300 hover:text-zinc-800"
|
||||
href={libraryHref}
|
||||
>
|
||||
Open agent
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="small"
|
||||
loading={isUndoing}
|
||||
className="border-none text-zinc-200 hover:bg-transparent hover:text-zinc-400"
|
||||
onClick={async () => {
|
||||
setIsUndoing(true);
|
||||
try {
|
||||
await onUndo(libraryAgentID);
|
||||
} finally {
|
||||
setIsUndoing(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isUndoing ? "Undoing..." : "Undo"}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface Props {
|
||||
creatorSlug: string;
|
||||
agentSlug: string;
|
||||
agentName: string;
|
||||
agentGraphID: string;
|
||||
className?: string;
|
||||
isInLibrary?: boolean;
|
||||
}
|
||||
|
||||
export function AddToLibraryButton({
|
||||
creatorSlug,
|
||||
agentSlug,
|
||||
agentName,
|
||||
agentGraphID,
|
||||
className,
|
||||
isInLibrary,
|
||||
}: Props) {
|
||||
const { isLoggedIn } = useSupabase();
|
||||
const { toast } = useToast();
|
||||
const queryClient = useQueryClient();
|
||||
const [justAdded, setJustAdded] = useState(false);
|
||||
|
||||
// Only fetch library list if isInLibrary wasn't provided by parent
|
||||
const { data: libraryAgents } = useGetV2ListLibraryAgents(undefined, {
|
||||
query: {
|
||||
enabled: isLoggedIn && isInLibrary === undefined,
|
||||
select: (res) =>
|
||||
res.status === 200 ? (res.data as LibraryAgentResponse) : undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: addToLibrary, isPending } =
|
||||
usePostV2AddMarketplaceAgent();
|
||||
|
||||
const { mutateAsync: removeFromLibrary } = useDeleteV2DeleteLibraryAgent();
|
||||
|
||||
if (!isLoggedIn) return null;
|
||||
if (justAdded) return null;
|
||||
|
||||
const isAlreadyInLibrary =
|
||||
isInLibrary ??
|
||||
libraryAgents?.agents?.some(
|
||||
(a: LibraryAgent) => a.graph_id === agentGraphID,
|
||||
);
|
||||
|
||||
if (isAlreadyInLibrary) return null;
|
||||
|
||||
async function handleClick(e: React.MouseEvent) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
const details = await getV2GetSpecificAgent(creatorSlug, agentSlug);
|
||||
|
||||
if (details.status !== 200) {
|
||||
throw new Error("Failed to fetch agent details");
|
||||
}
|
||||
|
||||
const { data: response } = await addToLibrary({
|
||||
data: {
|
||||
store_listing_version_id: details.data.store_listing_version_id,
|
||||
},
|
||||
});
|
||||
|
||||
const data = response as LibraryAgent;
|
||||
setJustAdded(true);
|
||||
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: getGetV2ListLibraryAgentsQueryKey(),
|
||||
});
|
||||
|
||||
analytics.sendDatafastEvent("add_to_library", {
|
||||
name: data.name,
|
||||
id: data.id,
|
||||
});
|
||||
|
||||
const addedToast = toast({
|
||||
title: `Agent ${agentName} added to your library.`,
|
||||
description: (
|
||||
<UndoActions
|
||||
libraryAgentID={data.id}
|
||||
libraryHref={`/library/agents/${data.id}`}
|
||||
onUndo={async (id) => {
|
||||
try {
|
||||
await removeFromLibrary({ libraryAgentId: id });
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: getGetV2ListLibraryAgentsQueryKey(),
|
||||
});
|
||||
setJustAdded(false);
|
||||
addedToast.dismiss();
|
||||
toast({
|
||||
title: "Action undone.",
|
||||
variant: "info",
|
||||
duration: 3000,
|
||||
});
|
||||
} catch (undoError) {
|
||||
Sentry.captureException(undoError);
|
||||
toast({
|
||||
title: "Failed to undo. Please try again.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
),
|
||||
dismissable: false,
|
||||
duration: 10000,
|
||||
});
|
||||
} catch (error) {
|
||||
Sentry.captureException(error);
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to add agent to library. Please try again.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="small"
|
||||
loading={isPending}
|
||||
leftIcon={<PlusIcon size={14} weight="bold" />}
|
||||
onClick={handleClick}
|
||||
className={`z-10 text-zinc-500 hover:border-transparent hover:bg-transparent hover:text-zinc-800 ${className ?? ""}`}
|
||||
aria-label={`Add ${agentName} to library`}
|
||||
>
|
||||
{isPending ? "Adding..." : "Add"}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Skeleton } from "@/components/atoms/Skeleton/Skeleton";
|
||||
import { Play } from "@phosphor-icons/react";
|
||||
import Image from "next/image";
|
||||
import { PlayIcon } from "@radix-ui/react-icons";
|
||||
import { Button } from "@/components/__legacy__/ui/button";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
getYouTubeVideoId,
|
||||
isValidVideoFile,
|
||||
@@ -16,19 +18,24 @@ interface AgentImageItemProps {
|
||||
handlePause: (index: number) => void;
|
||||
}
|
||||
|
||||
export const AgentImageItem: React.FC<AgentImageItemProps> = ({
|
||||
export function AgentImageItem({
|
||||
image,
|
||||
index,
|
||||
playingVideoIndex,
|
||||
handlePlay,
|
||||
handlePause,
|
||||
}) => {
|
||||
}: AgentImageItemProps) {
|
||||
const { videoRef } = useAgentImageItem({ playingVideoIndex, index });
|
||||
const isVideoFile = isValidVideoFile(image);
|
||||
const [imageLoaded, setImageLoaded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setImageLoaded(false);
|
||||
}, [image]);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="h-[15rem] overflow-hidden rounded-[26px] bg-[#a8a8a8] dark:bg-neutral-700 sm:h-[20rem] sm:w-full md:h-[25rem] lg:h-[30rem]">
|
||||
<div className="h-[15rem] overflow-hidden rounded-xl border border-neutral-100 bg-[#a8a8a8] sm:h-[20rem] sm:w-full md:h-[25rem] lg:h-[30rem]">
|
||||
{isValidVideoUrl(image) ? (
|
||||
getYouTubeVideoId(image) ? (
|
||||
<iframe
|
||||
@@ -60,12 +67,17 @@ export const AgentImageItem: React.FC<AgentImageItemProps> = ({
|
||||
)
|
||||
) : (
|
||||
<div className="relative h-full w-full">
|
||||
{!imageLoaded && (
|
||||
<Skeleton className="absolute inset-0 rounded-xl" />
|
||||
)}
|
||||
<Image
|
||||
src={image}
|
||||
alt="Image"
|
||||
fill
|
||||
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
|
||||
className="rounded-xl object-cover"
|
||||
onLoad={() => setImageLoaded(true)}
|
||||
onError={() => setImageLoaded(true)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -73,20 +85,25 @@ export const AgentImageItem: React.FC<AgentImageItemProps> = ({
|
||||
{isVideoFile && playingVideoIndex !== index && (
|
||||
<div className="absolute bottom-2 left-2 sm:bottom-3 sm:left-3 md:bottom-4 md:left-4 lg:bottom-[1.25rem] lg:left-[1.25rem]">
|
||||
<Button
|
||||
size="default"
|
||||
variant="secondary"
|
||||
size="large"
|
||||
onClick={() => {
|
||||
if (videoRef.current) {
|
||||
videoRef.current.play();
|
||||
}
|
||||
}}
|
||||
rightIcon={
|
||||
<Play
|
||||
size={20}
|
||||
weight="fill"
|
||||
className="text-black dark:text-neutral-200 sm:h-6 sm:w-6 md:h-7 md:w-7"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<span className="pr-1 text-sm font-medium leading-6 tracking-tight text-[#272727] dark:text-neutral-200 sm:pr-2 sm:text-base sm:leading-7 md:text-lg md:leading-8 lg:text-xl lg:leading-9">
|
||||
Play demo
|
||||
</span>
|
||||
<PlayIcon className="h-5 w-5 text-black dark:text-neutral-200 sm:h-6 sm:w-6 md:h-7 md:w-7" />
|
||||
Play demo
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,27 +1,90 @@
|
||||
"use client";
|
||||
|
||||
import { Skeleton } from "@/components/atoms/Skeleton/Skeleton";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useEffect, useState } from "react";
|
||||
import { AgentImageItem } from "../AgentImageItem/AgentImageItem";
|
||||
import { getYouTubeVideoId, isValidVideoUrl } from "../AgentImageItem/helpers";
|
||||
import { useAgentImage } from "./useAgentImage";
|
||||
|
||||
interface AgentImagesProps {
|
||||
images: string[];
|
||||
}
|
||||
|
||||
export const AgentImages: React.FC<AgentImagesProps> = ({ images }) => {
|
||||
export function AgentImages({ images }: AgentImagesProps) {
|
||||
const { playingVideoIndex, handlePlay, handlePause } = useAgentImage();
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
const [loadedThumbs, setLoadedThumbs] = useState<Set<number>>(new Set());
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedIndex((prev) => Math.max(0, Math.min(prev, images.length - 1)));
|
||||
}, [images.length]);
|
||||
|
||||
if (images.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="w-full overflow-y-auto bg-white px-2 dark:bg-transparent lg:w-[56.25rem]">
|
||||
<div className="space-y-4 sm:space-y-6 md:space-y-[1.875rem]">
|
||||
{images.map((image, index) => (
|
||||
<AgentImageItem
|
||||
key={index}
|
||||
image={image}
|
||||
index={index}
|
||||
playingVideoIndex={playingVideoIndex}
|
||||
handlePlay={handlePlay}
|
||||
handlePause={handlePause}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="w-full px-2 dark:bg-transparent lg:w-3/5 lg:flex-1">
|
||||
{/* Main preview */}
|
||||
<AgentImageItem
|
||||
image={images[selectedIndex]}
|
||||
index={selectedIndex}
|
||||
playingVideoIndex={playingVideoIndex}
|
||||
handlePlay={handlePlay}
|
||||
handlePause={handlePause}
|
||||
/>
|
||||
|
||||
{/* Thumbnails */}
|
||||
{images.length > 1 && (
|
||||
<div className="mt-3 flex gap-2 overflow-x-auto pb-1 sm:mt-4 sm:gap-3">
|
||||
{images.map((image, index) => {
|
||||
const isVideo = isValidVideoUrl(image);
|
||||
const youtubeId = isVideo ? getYouTubeVideoId(image) : null;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={index}
|
||||
type="button"
|
||||
onClick={() => setSelectedIndex(index)}
|
||||
className={cn(
|
||||
"relative h-16 w-24 shrink-0 overflow-hidden rounded-lg border transition-all sm:h-20 sm:w-32",
|
||||
selectedIndex === index
|
||||
? "border-violet-500"
|
||||
: "border-zinc-100 opacity-70 hover:opacity-100",
|
||||
)}
|
||||
>
|
||||
{(!isVideo || youtubeId) && !loadedThumbs.has(index) && (
|
||||
<Skeleton className="absolute inset-0 rounded-lg" />
|
||||
)}
|
||||
{youtubeId ? (
|
||||
<img
|
||||
src={`https://img.youtube.com/vi/${youtubeId}/mqdefault.jpg`}
|
||||
alt={`Thumbnail ${index + 1}`}
|
||||
loading="lazy"
|
||||
className="absolute inset-0 h-full w-full object-cover"
|
||||
onLoad={() =>
|
||||
setLoadedThumbs((prev) => new Set(prev).add(index))
|
||||
}
|
||||
/>
|
||||
) : isVideo ? (
|
||||
<div className="flex h-full w-full items-center justify-center bg-neutral-200 text-xs text-neutral-500">
|
||||
Video
|
||||
</div>
|
||||
) : (
|
||||
<img
|
||||
src={image}
|
||||
alt={`Thumbnail ${index + 1}`}
|
||||
loading="lazy"
|
||||
className="absolute inset-0 h-full w-full object-cover"
|
||||
onLoad={() =>
|
||||
setLoadedThumbs((prev) => new Set(prev).add(index))
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,28 +1,32 @@
|
||||
"use client";
|
||||
|
||||
import { StarRatingIcons } from "@/components/__legacy__/ui/icons";
|
||||
import { Separator } from "@/components/__legacy__/ui/separator";
|
||||
import Link from "next/link";
|
||||
import { User } from "@supabase/supabase-js";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { okData } from "@/app/api/helpers";
|
||||
import type { StoreAgentDetails } from "@/app/api/__generated__/models/storeAgentDetails";
|
||||
import { useGetV2GetSpecificAgent } from "@/app/api/__generated__/endpoints/store/store";
|
||||
import type { ChangelogEntry } from "@/app/api/__generated__/models/changelogEntry";
|
||||
import type { GetV2GetSpecificAgentParams } from "@/app/api/__generated__/models/getV2GetSpecificAgentParams";
|
||||
import { useAgentInfo } from "./useAgentInfo";
|
||||
import { useGetV2GetSpecificAgent } from "@/app/api/__generated__/endpoints/store/store";
|
||||
import type { StoreAgentDetails } from "@/app/api/__generated__/models/storeAgentDetails";
|
||||
import { okData } from "@/app/api/helpers";
|
||||
import Avatar, {
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/components/atoms/Avatar/Avatar";
|
||||
import { Badge } from "@/components/atoms/Badge/Badge";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { Dialog } from "@/components/molecules/Dialog/Dialog";
|
||||
import { formatTimeAgo } from "@/lib/utils/time";
|
||||
import * as React from "react";
|
||||
import Link from "next/link";
|
||||
import { FileArrowDownIcon, PlusIcon } from "@phosphor-icons/react";
|
||||
import { User } from "@supabase/supabase-js";
|
||||
import { useAgentInfo } from "./useAgentInfo";
|
||||
|
||||
interface AgentInfoProps {
|
||||
user: User | null;
|
||||
agentId: string;
|
||||
name: string;
|
||||
creator: string;
|
||||
creatorAvatar?: string;
|
||||
shortDescription: string;
|
||||
longDescription: string;
|
||||
rating: number;
|
||||
runs: number;
|
||||
categories: string[];
|
||||
lastUpdated: string;
|
||||
@@ -38,9 +42,9 @@ export const AgentInfo = ({
|
||||
agentId,
|
||||
name,
|
||||
creator,
|
||||
creatorAvatar,
|
||||
shortDescription,
|
||||
longDescription,
|
||||
rating,
|
||||
runs,
|
||||
categories,
|
||||
lastUpdated,
|
||||
@@ -57,9 +61,6 @@ export const AgentInfo = ({
|
||||
isAddingAgentToLibrary,
|
||||
} = useAgentInfo({ storeListingVersionId });
|
||||
|
||||
// State for expanding version list - start with 3, then show 3 more each time
|
||||
const [visibleVersionCount, setVisibleVersionCount] = React.useState(3);
|
||||
|
||||
// Get store agent data for version history
|
||||
const params: GetV2GetSpecificAgentParams = { include_changelog: true };
|
||||
const { data: storeAgentData } = useGetV2GetSpecificAgent(
|
||||
@@ -87,9 +88,6 @@ export const AgentInfo = ({
|
||||
}))
|
||||
: [];
|
||||
|
||||
const agentVersions = allVersions.slice(0, visibleVersionCount);
|
||||
const hasMoreVersions = allVersions.length > visibleVersionCount;
|
||||
|
||||
const renderVersionItem = (versionInfo: {
|
||||
version: number;
|
||||
isCurrentVersion: boolean;
|
||||
@@ -147,166 +145,176 @@ export const AgentInfo = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-[396px] px-4 sm:px-6 lg:w-[396px] lg:px-0">
|
||||
{/* Title */}
|
||||
<div
|
||||
data-testid="agent-title"
|
||||
className="mb-3 w-full font-poppins text-2xl font-medium leading-normal text-neutral-900 dark:text-neutral-100 sm:text-3xl lg:mb-4 lg:text-[35px] lg:leading-10"
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
<div className="w-full px-4 sm:px-6 lg:px-0">
|
||||
<div className="mb-8 rounded-2xl bg-gradient-to-r from-blue-200 to-indigo-200 p-[1px]">
|
||||
<div className="flex flex-col rounded-[calc(1rem-2px)] bg-gray-50 p-4">
|
||||
{/* Title */}
|
||||
<Text variant="h2" data-testid="agent-title" className="mb-3 w-full">
|
||||
{name}
|
||||
</Text>
|
||||
|
||||
{/* Creator */}
|
||||
<div className="mb-3 flex w-full items-center gap-1.5 lg:mb-4">
|
||||
<div className="text-base font-normal text-neutral-800 dark:text-neutral-200 sm:text-lg lg:text-xl">
|
||||
by
|
||||
</div>
|
||||
<Link
|
||||
data-testid={"agent-creator"}
|
||||
href={`/marketplace/creator/${encodeURIComponent(creator)}`}
|
||||
className="text-base font-medium text-neutral-800 hover:underline dark:text-neutral-200 sm:text-lg lg:text-xl"
|
||||
>
|
||||
{creator}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Short Description */}
|
||||
<div className="mb-4 line-clamp-2 w-full text-base font-normal leading-normal text-neutral-600 dark:text-neutral-300 sm:text-lg lg:mb-5 lg:text-xl lg:leading-7">
|
||||
{shortDescription}
|
||||
</div>
|
||||
|
||||
{/* Rating and Runs */}
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex items-center gap-1.5 sm:gap-2">
|
||||
<span className="whitespace-nowrap text-base font-semibold text-neutral-800 dark:text-neutral-200 sm:text-lg">
|
||||
{rating.toFixed(1)}
|
||||
</span>
|
||||
<div className="flex gap-0.5">{StarRatingIcons(rating)}</div>
|
||||
</div>
|
||||
<div className="whitespace-nowrap text-base font-semibold text-neutral-800 dark:text-neutral-200 sm:text-lg">
|
||||
{runs.toLocaleString()} runs
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Buttons */}
|
||||
{user && (
|
||||
<div className="mt-6 flex w-full gap-3 lg:mt-8">
|
||||
<button
|
||||
className={cn(
|
||||
"inline-flex min-w-24 items-center justify-center rounded-full bg-violet-600 px-4 py-3",
|
||||
"transition-colors duration-200 hover:bg-violet-500 disabled:bg-zinc-400",
|
||||
)}
|
||||
data-testid={"agent-add-library-button"}
|
||||
disabled={isAddingAgentToLibrary}
|
||||
onClick={() =>
|
||||
handleLibraryAction({
|
||||
isAddingAgentFirstTime: !isAgentAddedToLibrary,
|
||||
})
|
||||
}
|
||||
>
|
||||
<span className="justify-start font-sans text-sm font-medium leading-snug text-primary-foreground">
|
||||
{isAgentAddedToLibrary ? "See runs" : "Add to library"}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Download section */}
|
||||
<p className="mt-6 text-zinc-600 dark:text-zinc-400 lg:mt-12">
|
||||
Want to use this agent locally?{" "}
|
||||
<button
|
||||
className="underline"
|
||||
onClick={() => handleDownload(agentId, name)}
|
||||
disabled={isDownloadingAgent}
|
||||
data-testid="agent-download-button"
|
||||
>
|
||||
Download here.
|
||||
</button>
|
||||
</p>
|
||||
|
||||
{/* Separator */}
|
||||
<Separator className="my-7" />
|
||||
|
||||
{/* Agent Details Section */}
|
||||
<div className="flex w-full flex-col gap-4 lg:gap-6">
|
||||
{/* Description Section */}
|
||||
<div className="w-full">
|
||||
<div className="decoration-skip-ink-none mb-1.5 text-base font-medium leading-6 text-neutral-800 dark:text-neutral-200 sm:mb-2">
|
||||
Description
|
||||
</div>
|
||||
{/* Creator */}
|
||||
<div
|
||||
data-testid={"agent-description"}
|
||||
className="whitespace-pre-line text-base font-normal leading-6 text-neutral-600 dark:text-neutral-400"
|
||||
className="mb-3 flex w-full items-center gap-2 lg:mb-12"
|
||||
data-testid="agent-creator"
|
||||
>
|
||||
{longDescription}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Categories */}
|
||||
<div className="flex w-full flex-col gap-1.5 sm:gap-2">
|
||||
<div className="decoration-skip-ink-none mb-1.5 text-base font-medium leading-6 text-neutral-800 dark:text-neutral-200 sm:mb-2">
|
||||
Categories
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1.5 sm:gap-2">
|
||||
{categories.map((category, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="decoration-skip-ink-none whitespace-nowrap rounded-full border border-neutral-600 bg-white px-2 py-0.5 text-base font-normal leading-6 text-neutral-800 underline-offset-[from-font] dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-200 sm:px-[16px] sm:py-[10px]"
|
||||
>
|
||||
{category}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Version history */}
|
||||
<div className="flex w-full flex-col gap-1.5 sm:gap-2">
|
||||
<div className="decoration-skip-ink-none text-base font-medium leading-6 text-neutral-800 dark:text-neutral-200">
|
||||
Version history
|
||||
</div>
|
||||
<div className="decoration-skip-ink-none text-sm font-normal leading-6 text-neutral-600 underline-offset-[from-font] dark:text-neutral-400">
|
||||
Last updated {formatTimeAgo(lastUpdated)}
|
||||
</div>
|
||||
<div className="decoration-skip-ink-none text-xs text-neutral-600 dark:text-neutral-400 sm:text-sm">
|
||||
Version {version}.0
|
||||
<Avatar className="h-7 w-7 shrink-0">
|
||||
{creatorAvatar && (
|
||||
<AvatarImage src={creatorAvatar} alt={`${creator} avatar`} />
|
||||
)}
|
||||
<AvatarFallback size={28}>{creator.charAt(0)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<Text variant="body" className="text-md">
|
||||
by
|
||||
</Text>
|
||||
<Link
|
||||
href={`/marketplace/creator/${encodeURIComponent(creatorSlug ?? creator)}`}
|
||||
className="text-md font-medium hover:underline"
|
||||
>
|
||||
{creator}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Version List */}
|
||||
{agentVersions.length > 0 ? (
|
||||
<div className="mt-3">
|
||||
<div className="decoration-skip-ink-none mb-1.5 text-base font-medium leading-6 text-neutral-900 dark:text-neutral-200 sm:mb-2">
|
||||
Changelog
|
||||
</div>
|
||||
{agentVersions.map(renderVersionItem)}
|
||||
{hasMoreVersions && (
|
||||
<button
|
||||
onClick={() => setVisibleVersionCount((prev) => prev + 3)}
|
||||
className="mt-2 flex items-center gap-1 text-sm font-medium text-neutral-700 hover:text-neutral-700 dark:text-neutral-100 dark:hover:text-neutral-300"
|
||||
{/* Short Description */}
|
||||
<div className="mb-4 line-clamp-2 w-full text-base font-normal leading-normal text-neutral-600 dark:text-neutral-300 sm:text-lg lg:mb-5 lg:text-xl lg:leading-7">
|
||||
{shortDescription}
|
||||
</div>
|
||||
|
||||
{/* Buttons + Runs */}
|
||||
<div className="mt-6 flex w-full items-center justify-between lg:mt-8">
|
||||
<div className="flex gap-3">
|
||||
{user && (
|
||||
<Button
|
||||
variant="primary"
|
||||
className="group/add min-w-36 border-violet-600 bg-violet-600 transition-shadow duration-300 hover:border-violet-500 hover:bg-violet-500 hover:shadow-[0_0_20px_rgba(139,92,246,0.4)]"
|
||||
data-testid="agent-add-library-button"
|
||||
disabled={isAddingAgentToLibrary}
|
||||
loading={isAddingAgentToLibrary}
|
||||
leftIcon={
|
||||
!isAddingAgentToLibrary && !isAgentAddedToLibrary ? (
|
||||
<PlusIcon
|
||||
size={16}
|
||||
weight="bold"
|
||||
className="transition-transform duration-300 group-hover/add:rotate-90 group-hover/add:scale-125"
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
onClick={() =>
|
||||
handleLibraryAction({
|
||||
isAddingAgentFirstTime: !isAgentAddedToLibrary,
|
||||
})
|
||||
}
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M4 6l4 4 4-4"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
<span>Read more</span>
|
||||
</button>
|
||||
{isAddingAgentToLibrary
|
||||
? "Adding..."
|
||||
: isAgentAddedToLibrary
|
||||
? "See runs"
|
||||
: "Add to library"}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
loading={isDownloadingAgent}
|
||||
onClick={() => handleDownload(agentId, name)}
|
||||
data-testid="agent-download-button"
|
||||
>
|
||||
{!isDownloadingAgent && <FileArrowDownIcon size={18} />}
|
||||
{isDownloadingAgent ? "Downloading..." : "Download"}
|
||||
</Button>
|
||||
</div>
|
||||
<Text
|
||||
variant="small"
|
||||
className="mr-4 hidden whitespace-nowrap text-zinc-500 lg:block"
|
||||
>
|
||||
{runs === 0
|
||||
? "No runs"
|
||||
: `${runs.toLocaleString()} run${runs > 1 ? "s" : ""}`}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-8 flex flex-col gap-24">
|
||||
{/* Agent Details Section */}
|
||||
<div className="flex w-full flex-col gap-4 lg:gap-6">
|
||||
{/* Description Section */}
|
||||
<div className="mb-4 w-full">
|
||||
<Text variant="h5" className="mb-1.5 text-[1.3rem]">
|
||||
Description
|
||||
</Text>
|
||||
<Text
|
||||
variant="body"
|
||||
data-testid={"agent-description"}
|
||||
className="text-md whitespace-pre-line text-neutral-600"
|
||||
>
|
||||
{longDescription}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{/* Categories */}
|
||||
<div className="mb-4 flex w-full flex-col gap-1.5 sm:gap-2 md:px-2">
|
||||
<Text variant="h5" className="mb-1.5 text-[1.3rem]">
|
||||
Categories
|
||||
</Text>
|
||||
{categories.filter((c) => c.trim()).length > 0 ? (
|
||||
<div className="flex flex-wrap gap-1.5 sm:gap-2">
|
||||
{categories
|
||||
.filter((c) => c.trim())
|
||||
.map((category, index) => (
|
||||
<Badge
|
||||
variant="info"
|
||||
key={index}
|
||||
className="border border-purple-100 bg-purple-50 text-purple-800"
|
||||
>
|
||||
{category}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<Text variant="body" className="text-neutral-400">
|
||||
None
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Version history */}
|
||||
<div className="flex w-full flex-col gap-1.5 sm:gap-2 md:px-2">
|
||||
<div className="flex items-baseline justify-start">
|
||||
<Text variant="h5" className="text-[1.3rem]">
|
||||
Version
|
||||
</Text>
|
||||
{allVersions.length > 0 && (
|
||||
<Dialog
|
||||
title="Changelog"
|
||||
styling={{
|
||||
maxWidth: "30rem",
|
||||
}}
|
||||
>
|
||||
<Dialog.Trigger>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="small"
|
||||
className="text-violet-600 hover:text-violet-500"
|
||||
>
|
||||
(Changelog)
|
||||
</Button>
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Content>
|
||||
<div className="max-h-[60vh] space-y-4 overflow-y-auto p-4">
|
||||
{allVersions.map(renderVersionItem)}
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-xs text-neutral-600 dark:text-neutral-400 sm:text-sm">
|
||||
Version {version}.0
|
||||
<div className="flex w-full items-center justify-start gap-8">
|
||||
<Text variant="body" className="text-neutral-600">
|
||||
{version}.0
|
||||
</Text>
|
||||
<Text variant="body" className="text-neutral-600">
|
||||
Last updated {formatTimeAgo(lastUpdated)}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,26 +1,95 @@
|
||||
import { Skeleton } from "@/components/__legacy__/ui/skeleton";
|
||||
import { Skeleton } from "@/components/atoms/Skeleton/Skeleton";
|
||||
|
||||
export const AgentPageLoading = () => {
|
||||
export function AgentPageLoading() {
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-[1360px]">
|
||||
<main className="mt-5 px-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Skeleton className="h-4 w-24" />
|
||||
<span>/</span>
|
||||
<Skeleton className="h-4 w-32" />
|
||||
<span>/</span>
|
||||
<Skeleton className="h-4 w-40" />
|
||||
<main className="mt-5 px-4 pb-12">
|
||||
{/* Breadcrumbs */}
|
||||
<div className="mb-4 flex items-center justify-between px-4 md:!-mb-3">
|
||||
<Skeleton className="h-8 w-20 rounded-lg" />
|
||||
<div className="hidden items-center gap-2 md:flex">
|
||||
<Skeleton className="h-4 w-24" />
|
||||
<span className="text-zinc-300">/</span>
|
||||
<Skeleton className="h-4 w-28" />
|
||||
<span className="text-zinc-300">/</span>
|
||||
<Skeleton className="h-4 w-36" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 flex flex-col gap-8 md:flex-row">
|
||||
<div className="w-full max-w-sm">
|
||||
<Skeleton className="h-64 w-full rounded-lg" />
|
||||
{/* Main content: Info left + Images right */}
|
||||
<div className="mt-0 flex flex-col items-start gap-4 sm:mt-6 sm:gap-6 lg:mt-8 lg:flex-row lg:gap-12">
|
||||
{/* Left: Agent info panel */}
|
||||
<div className="w-full lg:w-2/5">
|
||||
<div className="rounded-2xl bg-gradient-to-r from-blue-100/50 to-indigo-100/50 p-[1px]">
|
||||
<div className="flex flex-col rounded-[calc(1rem-2px)] bg-gray-50 p-4">
|
||||
{/* Title */}
|
||||
<Skeleton className="mb-3 h-9 w-3/4" />
|
||||
|
||||
{/* Creator */}
|
||||
<div className="mb-3 flex items-center gap-2 lg:mb-12">
|
||||
<Skeleton className="h-7 w-7 shrink-0 rounded-full" />
|
||||
<Skeleton className="h-4 w-8" />
|
||||
<Skeleton className="h-4 w-24" />
|
||||
</div>
|
||||
|
||||
{/* Short description */}
|
||||
<Skeleton className="mb-4 h-5 w-full lg:mb-5" />
|
||||
<Skeleton className="mb-6 h-5 w-2/3" />
|
||||
|
||||
{/* Buttons */}
|
||||
<div className="flex gap-3">
|
||||
<Skeleton className="h-12 w-36 rounded-full" />
|
||||
<Skeleton className="h-12 w-28 rounded-full" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description section */}
|
||||
<div className="mt-8 space-y-3">
|
||||
<Skeleton className="h-6 w-28" />
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-3/4" />
|
||||
</div>
|
||||
|
||||
{/* Categories */}
|
||||
<div className="mt-6 space-y-3">
|
||||
<Skeleton className="h-6 w-24" />
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Skeleton className="h-7 w-20 rounded-full" />
|
||||
<Skeleton className="h-7 w-24 rounded-full" />
|
||||
<Skeleton className="h-7 w-16 rounded-full" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Skeleton className="aspect-video w-full rounded-lg" />
|
||||
|
||||
{/* Right: Image preview */}
|
||||
<div className="w-full px-2 lg:w-3/5 lg:flex-1">
|
||||
<Skeleton className="h-[15rem] w-full rounded-xl sm:h-[20rem] md:h-[25rem] lg:h-[30rem]" />
|
||||
|
||||
{/* Thumbnails */}
|
||||
<div className="mt-3 flex gap-2 sm:mt-4 sm:gap-3">
|
||||
<Skeleton className="h-16 w-24 shrink-0 rounded-lg sm:h-20 sm:w-32" />
|
||||
<Skeleton className="h-16 w-24 shrink-0 rounded-lg sm:h-20 sm:w-32" />
|
||||
<Skeleton className="h-16 w-24 shrink-0 rounded-lg sm:h-20 sm:w-32" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Related agents section */}
|
||||
<div className="my-6" />
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<Skeleton className="h-6 w-6" />
|
||||
<Skeleton className="h-6 w-48" />
|
||||
</div>
|
||||
<div className="hidden grid-cols-1 gap-6 md:grid md:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4">
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<Skeleton key={i} className="h-[25rem] w-full rounded-2xl" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { StoreAgent } from "@/app/api/__generated__/models/storeAgent";
|
||||
import {
|
||||
Carousel,
|
||||
CarouselContent,
|
||||
CarouselItem,
|
||||
} from "@/components/__legacy__/ui/carousel";
|
||||
import { useAgentsSection } from "./useAgentsSection";
|
||||
import { StoreAgent } from "@/app/api/__generated__/models/storeAgent";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { DotsNineIcon } from "@phosphor-icons/react";
|
||||
import { StoreCard } from "../StoreCard/StoreCard";
|
||||
import { useAgentsSection } from "./useAgentsSection";
|
||||
|
||||
export interface Agent {
|
||||
slug: string;
|
||||
@@ -21,62 +23,68 @@ export interface Agent {
|
||||
rating: number;
|
||||
}
|
||||
|
||||
interface AgentsSectionProps {
|
||||
sectionTitle: string;
|
||||
interface Props {
|
||||
sectionTitle?: string;
|
||||
agents: StoreAgent[];
|
||||
hideAvatars?: boolean;
|
||||
margin?: string;
|
||||
}
|
||||
|
||||
export const AgentsSection = ({
|
||||
export function AgentsSection({
|
||||
sectionTitle,
|
||||
agents: allAgents,
|
||||
hideAvatars = false,
|
||||
margin = "24px",
|
||||
}: AgentsSectionProps) => {
|
||||
// TODO: Update this when we have pagination and shifts to useAgentsSection
|
||||
}: Props) {
|
||||
const displayedAgents = allAgents;
|
||||
const { handleCardClick } = useAgentsSection();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<div className="w-full max-w-[1360px]">
|
||||
<h2
|
||||
style={{ marginBottom: margin }}
|
||||
className="font-poppins text-lg font-semibold text-[#282828] dark:text-neutral-200"
|
||||
>
|
||||
{sectionTitle}
|
||||
</h2>
|
||||
{!displayedAgents || displayedAgents.length === 0 ? (
|
||||
<div className="text-center text-gray-500 dark:text-gray-400">
|
||||
No agents found
|
||||
{sectionTitle ? (
|
||||
<div className="mb-8 flex flex-row items-center gap-2">
|
||||
<DotsNineIcon size={24} />
|
||||
<Text variant="h4">{sectionTitle}</Text>
|
||||
</div>
|
||||
) : null}
|
||||
{!displayedAgents || displayedAgents.length === 0 ? (
|
||||
<Text variant="body" className="text-center text-gray-500">
|
||||
No agents found
|
||||
</Text>
|
||||
) : (
|
||||
<>
|
||||
{/* Mobile Carousel View */}
|
||||
<Carousel
|
||||
className="md:hidden"
|
||||
className="-mx-4 md:hidden"
|
||||
opts={{
|
||||
loop: true,
|
||||
}}
|
||||
>
|
||||
<CarouselContent>
|
||||
{displayedAgents.map((agent, index) => (
|
||||
<CarouselItem key={index} className="min-w-64 max-w-71">
|
||||
<StoreCard
|
||||
agentName={agent.agent_name}
|
||||
agentImage={agent.agent_image}
|
||||
description={agent.description}
|
||||
runs={agent.runs}
|
||||
rating={agent.rating}
|
||||
avatarSrc={agent.creator_avatar}
|
||||
creatorName={agent.creator}
|
||||
hideAvatar={hideAvatars}
|
||||
onClick={() => handleCardClick(agent.creator, agent.slug)}
|
||||
/>
|
||||
</CarouselItem>
|
||||
))}
|
||||
</CarouselContent>
|
||||
<div className="relative">
|
||||
<CarouselContent className="px-4 pb-2">
|
||||
{displayedAgents.map((agent, index) => (
|
||||
<CarouselItem key={index} className="min-w-64 max-w-71">
|
||||
<StoreCard
|
||||
agentName={agent.agent_name}
|
||||
agentImage={agent.agent_image}
|
||||
description={agent.description}
|
||||
runs={agent.runs}
|
||||
rating={agent.rating}
|
||||
avatarSrc={agent.creator_avatar}
|
||||
creatorName={agent.creator}
|
||||
hideAvatar={hideAvatars}
|
||||
creatorSlug={agent.creator}
|
||||
agentSlug={agent.slug}
|
||||
agentGraphID={agent.agent_graph_id}
|
||||
onClick={() =>
|
||||
handleCardClick(agent.creator, agent.slug)
|
||||
}
|
||||
/>
|
||||
</CarouselItem>
|
||||
))}
|
||||
</CarouselContent>
|
||||
<div className="pointer-events-none absolute inset-y-0 left-0 w-8 bg-gradient-to-r from-[rgb(246,247,248)] to-transparent" />
|
||||
<div className="pointer-events-none absolute inset-y-0 right-0 w-8 bg-gradient-to-l from-[rgb(246,247,248)] to-transparent" />
|
||||
</div>
|
||||
</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">
|
||||
@@ -91,6 +99,9 @@ export const AgentsSection = ({
|
||||
avatarSrc={agent.creator_avatar}
|
||||
creatorName={agent.creator}
|
||||
hideAvatar={hideAvatars}
|
||||
creatorSlug={agent.creator}
|
||||
agentSlug={agent.slug}
|
||||
agentGraphID={agent.agent_graph_id}
|
||||
onClick={() => handleCardClick(agent.creator, agent.slug)}
|
||||
/>
|
||||
))}
|
||||
@@ -100,4 +111,4 @@ export const AgentsSection = ({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { PublishAgentModal } from "@/components/contextual/PublishAgentModal/PublishAgentModal";
|
||||
import * as React from "react";
|
||||
|
||||
interface BecomeACreatorProps {
|
||||
title?: string;
|
||||
@@ -10,17 +9,11 @@ interface BecomeACreatorProps {
|
||||
}
|
||||
|
||||
export function BecomeACreator({
|
||||
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",
|
||||
}: BecomeACreatorProps) {
|
||||
return (
|
||||
<div className="relative mx-auto h-auto min-h-[300px] w-full max-w-[1360px] md:min-h-[400px] lg:h-[459px]">
|
||||
{/* Title */}
|
||||
<h2 className="mb-[77px] font-poppins text-[18px] font-semibold leading-[28px] text-neutral-800 dark:text-neutral-200">
|
||||
{title}
|
||||
</h2>
|
||||
|
||||
<div className="relative mx-auto mt-16 h-auto min-h-[300px] w-full max-w-[1360px] md:min-h-[400px] lg:h-[459px]">
|
||||
{/* Content Container */}
|
||||
<div className="mx-auto w-full max-w-[900px] px-4 text-center md:px-6 lg:px-0">
|
||||
<h2 className="mb-6 text-center font-poppins text-[48px] font-semibold leading-[54px] tracking-[-0.012em] text-neutral-950 dark:text-neutral-50 md:mb-8 lg:mb-12">
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import Image from "next/image";
|
||||
"use client";
|
||||
|
||||
import Avatar, {
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/components/atoms/Avatar/Avatar";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { backgroundColor } from "./helper";
|
||||
|
||||
interface CreatorCardProps {
|
||||
interface Props {
|
||||
creatorName: string;
|
||||
creatorImage: string | null;
|
||||
bio: string;
|
||||
@@ -10,47 +16,44 @@ interface CreatorCardProps {
|
||||
index: number;
|
||||
}
|
||||
|
||||
export const CreatorCard = ({
|
||||
export function CreatorCard({
|
||||
creatorName,
|
||||
creatorImage,
|
||||
bio,
|
||||
agentsUploaded,
|
||||
onClick,
|
||||
index,
|
||||
}: CreatorCardProps) => {
|
||||
}: Props) {
|
||||
return (
|
||||
<div
|
||||
className={`h-[264px] w-full px-[18px] pb-5 pt-6 ${backgroundColor(index)} inline-flex cursor-pointer flex-col items-start justify-start gap-3.5 rounded-[26px] transition-all duration-200 hover:brightness-95`}
|
||||
<button
|
||||
type="button"
|
||||
className={`relative flex h-[16rem] w-full cursor-pointer flex-col items-start rounded-2xl border p-4 text-left shadow-md transition-all duration-300 hover:shadow-lg ${backgroundColor(index)}`}
|
||||
onClick={onClick}
|
||||
data-testid="creator-card"
|
||||
>
|
||||
<div className="relative h-[64px] w-[64px]">
|
||||
<div className="absolute inset-0 overflow-hidden rounded-full">
|
||||
{creatorImage ? (
|
||||
<Image
|
||||
src={creatorImage}
|
||||
alt={creatorName}
|
||||
width={64}
|
||||
height={64}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="h-full w-full bg-neutral-300 dark:bg-neutral-600" />
|
||||
)}
|
||||
{/* Avatar */}
|
||||
<Avatar className="h-14 w-14 shrink-0">
|
||||
{creatorImage && (
|
||||
<AvatarImage src={creatorImage} alt={`${creatorName} avatar`} />
|
||||
)}
|
||||
<AvatarFallback size={56}>{creatorName.charAt(0)}</AvatarFallback>
|
||||
</Avatar>
|
||||
|
||||
<div className="mt-3 flex w-full flex-1 flex-col">
|
||||
<Text variant="h4" className="leading-tight">
|
||||
{creatorName}
|
||||
</Text>
|
||||
<div className="mt-2 flex w-full flex-col">
|
||||
<Text variant="body" className="line-clamp-3 leading-normal">
|
||||
{bio}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<h3 className="font-poppins text-2xl font-semibold leading-tight text-neutral-900 dark:text-neutral-100">
|
||||
{creatorName}
|
||||
</h3>
|
||||
<p className="text-sm font-normal leading-normal text-neutral-600 dark:text-neutral-400">
|
||||
{bio}
|
||||
</p>
|
||||
<div className="text-lg font-semibold leading-7 text-neutral-800 dark:text-neutral-200">
|
||||
{agentsUploaded} agents
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Stats */}
|
||||
<Text variant="body" className="absolute bottom-4 left-4 text-zinc-500">
|
||||
{agentsUploaded} {agentsUploaded === 1 ? "agent" : "agents"}
|
||||
</Text>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
const BACKGROUND_COLORS = [
|
||||
"bg-amber-100 dark:bg-amber-800", // #fef3c7 / #92400e
|
||||
"bg-violet-100 dark:bg-violet-800", // #ede9fe / #5b21b6
|
||||
"bg-green-100 dark:bg-green-800", // #dcfce7 / #065f46
|
||||
"bg-blue-100 dark:bg-blue-800", // #dbeafe / #1e3a8a
|
||||
"bg-amber-50 border-amber-100/70",
|
||||
"bg-violet-50 border-violet-100/70",
|
||||
"bg-green-50 border-green-100/70",
|
||||
"bg-blue-50 border-blue-100/70",
|
||||
];
|
||||
|
||||
export const backgroundColor = (index: number) =>
|
||||
BACKGROUND_COLORS[index % BACKGROUND_COLORS.length];
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
import { getIconForSocial } from "@/components/__legacy__/ui/icons";
|
||||
import { Fragment } from "react";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import {
|
||||
FacebookLogo,
|
||||
GithubLogo,
|
||||
Globe,
|
||||
InstagramLogo,
|
||||
LinkedinLogo,
|
||||
TiktokLogo,
|
||||
XLogo,
|
||||
YoutubeLogo,
|
||||
} from "@phosphor-icons/react";
|
||||
|
||||
interface CreatorLinksProps {
|
||||
links: string[];
|
||||
@@ -21,39 +31,59 @@ function getHostnameFromURL(url: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
export const CreatorLinks = ({ links }: CreatorLinksProps) => {
|
||||
if (!links || links.length === 0) {
|
||||
return null;
|
||||
function getSocialIcon(url: string) {
|
||||
let host;
|
||||
try {
|
||||
host = new URL(normalizeURL(url)).hostname.toLowerCase();
|
||||
} catch {
|
||||
return <Globe className="h-4 w-4" />;
|
||||
}
|
||||
|
||||
const renderLinkButton = (url: string) => (
|
||||
<a
|
||||
href={normalizeURL(url)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex min-w-[200px] flex-1 items-center justify-between rounded-[34px] border border-neutral-600 px-5 py-3 dark:border-neutral-400"
|
||||
>
|
||||
<div className="text-base font-medium leading-normal text-neutral-800 dark:text-neutral-200">
|
||||
{getHostnameFromURL(url)}
|
||||
</div>
|
||||
<div className="relative h-6 w-6">
|
||||
{getIconForSocial(url, {
|
||||
className: "h-6 w-6 text-neutral-800 dark:text-neutral-200",
|
||||
})}
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
if (host === "facebook.com" || host.endsWith(".facebook.com")) {
|
||||
return <FacebookLogo className="h-4 w-4" />;
|
||||
} else if (
|
||||
host === "twitter.com" ||
|
||||
host.endsWith(".twitter.com") ||
|
||||
host === "x.com" ||
|
||||
host.endsWith(".x.com")
|
||||
) {
|
||||
return <XLogo className="h-4 w-4" />;
|
||||
} else if (host === "instagram.com" || host.endsWith(".instagram.com")) {
|
||||
return <InstagramLogo className="h-4 w-4" />;
|
||||
} else if (host === "linkedin.com" || host.endsWith(".linkedin.com")) {
|
||||
return <LinkedinLogo className="h-4 w-4" />;
|
||||
} else if (host === "github.com" || host.endsWith(".github.com")) {
|
||||
return <GithubLogo className="h-4 w-4" />;
|
||||
} else if (host === "youtube.com" || host.endsWith(".youtube.com")) {
|
||||
return <YoutubeLogo className="h-4 w-4" />;
|
||||
} else if (host === "tiktok.com" || host.endsWith(".tiktok.com")) {
|
||||
return <TiktokLogo className="h-4 w-4" />;
|
||||
}
|
||||
return <Globe className="h-4 w-4" />;
|
||||
}
|
||||
|
||||
export function CreatorLinks({ links }: CreatorLinksProps) {
|
||||
if (!links || links.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-start justify-start gap-4">
|
||||
<div className="text-base font-medium leading-normal text-neutral-800 dark:text-neutral-200">
|
||||
Other links
|
||||
</div>
|
||||
<div className="flex w-full flex-wrap gap-3">
|
||||
{links.map((link, index) => (
|
||||
<Fragment key={index}>{renderLinkButton(link)}</Fragment>
|
||||
<div className="flex flex-col items-start gap-3">
|
||||
<Text variant="h5">Links</Text>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{links.map((link) => (
|
||||
<Button
|
||||
key={link}
|
||||
variant="secondary"
|
||||
size="small"
|
||||
as="NextLink"
|
||||
href={normalizeURL(link)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
rightIcon={getSocialIcon(link)}
|
||||
>
|
||||
{getHostnameFromURL(link)}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,40 +1,86 @@
|
||||
import { Skeleton } from "@/components/__legacy__/ui/skeleton";
|
||||
import { Skeleton } from "@/components/atoms/Skeleton/Skeleton";
|
||||
|
||||
export const CreatorPageLoading = () => {
|
||||
export function CreatorPageLoading() {
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-[1360px]">
|
||||
<main className="mt-5 px-4">
|
||||
<Skeleton className="mb-4 h-6 w-40" />
|
||||
|
||||
<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">
|
||||
<Skeleton className="h-80 w-80 rounded-xl" />
|
||||
<div className="mt-4 space-y-2">
|
||||
<Skeleton className="h-6 w-80" />
|
||||
<Skeleton className="h-4 w-80" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-4">
|
||||
<Skeleton className="h-6 w-24" />
|
||||
<Skeleton className="h-8 w-full max-w-xl" />
|
||||
<Skeleton className="h-4 w-1/2" />
|
||||
<div className="flex gap-2">
|
||||
<Skeleton className="h-8 w-8 rounded-full" />
|
||||
<Skeleton className="h-8 w-8 rounded-full" />
|
||||
</div>
|
||||
<main className="mt-5 px-4 pb-12">
|
||||
{/* Breadcrumbs */}
|
||||
<div className="mb-4 flex items-center justify-between px-4 md:!-mb-3">
|
||||
<Skeleton className="h-8 w-20 rounded-lg" />
|
||||
<div className="hidden items-center gap-2 md:flex">
|
||||
<Skeleton className="h-4 w-24" />
|
||||
<span className="text-zinc-300">/</span>
|
||||
<Skeleton className="h-4 w-32" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8">
|
||||
<Skeleton className="mb-6 h-px w-full" />
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-3">
|
||||
{Array.from({ length: 3 }).map((_, i) => (
|
||||
<Skeleton key={i} className="h-32 w-full rounded-lg" />
|
||||
{/* Main content */}
|
||||
<div className="mt-0 flex flex-col items-start gap-4 sm:mt-6 sm:gap-6 lg:mt-8 lg:flex-row lg:gap-12">
|
||||
{/* Left: Creator info card */}
|
||||
<div className="w-full lg:w-2/5">
|
||||
<div className="w-full px-4 sm:px-6 lg:px-0">
|
||||
<div className="rounded-2xl bg-gradient-to-r from-blue-100/50 to-indigo-100/50 p-[1px]">
|
||||
<div className="flex flex-col rounded-[calc(1rem-2px)] bg-gray-50 p-4">
|
||||
{/* Avatar */}
|
||||
<Skeleton className="mb-4 h-20 w-20 rounded-full sm:h-24 sm:w-24" />
|
||||
|
||||
{/* Name */}
|
||||
<Skeleton className="mb-1 h-9 w-48" />
|
||||
|
||||
{/* Handle */}
|
||||
<Skeleton className="mb-4 h-5 w-28" />
|
||||
|
||||
{/* Description */}
|
||||
<div className="mb-6 space-y-2">
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-3/4" />
|
||||
</div>
|
||||
|
||||
{/* Categories */}
|
||||
<div className="mb-6">
|
||||
<Skeleton className="mb-2 h-5 w-28" />
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Skeleton className="h-7 w-20 rounded-full" />
|
||||
<Skeleton className="h-7 w-24 rounded-full" />
|
||||
<Skeleton className="h-7 w-16 rounded-full" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Links */}
|
||||
<Skeleton className="mb-2 h-5 w-14" />
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Skeleton className="h-9 w-28 rounded-full" />
|
||||
<Skeleton className="h-9 w-32 rounded-full" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right side spacer */}
|
||||
<div className="hidden lg:block lg:w-3/5" />
|
||||
</div>
|
||||
|
||||
{/* Agent grid section */}
|
||||
<div className="my-6" />
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<Skeleton className="h-6 w-6" />
|
||||
<Skeleton className="h-6 w-40" />
|
||||
</div>
|
||||
<div className="hidden grid-cols-1 gap-6 md:grid md:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4">
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<Skeleton key={i} className="h-[25rem] w-full rounded-2xl" />
|
||||
))}
|
||||
</div>
|
||||
{/* Mobile carousel placeholder */}
|
||||
<div className="flex gap-4 overflow-hidden md:hidden">
|
||||
<Skeleton className="h-[25rem] min-w-64 rounded-2xl" />
|
||||
<Skeleton className="h-[25rem] min-w-64 rounded-2xl" />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,74 +1,139 @@
|
||||
import Image from "next/image";
|
||||
import { StarRatingIcons } from "@/components/__legacy__/ui/icons";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/__legacy__/ui/card";
|
||||
import { useState } from "react";
|
||||
import { StoreAgent } from "@/app/api/__generated__/models/storeAgent";
|
||||
"use client";
|
||||
|
||||
interface FeaturedStoreCardProps {
|
||||
import { StoreAgent } from "@/app/api/__generated__/models/storeAgent";
|
||||
import Avatar, {
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/components/atoms/Avatar/Avatar";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/atoms/Tooltip/BaseTooltip";
|
||||
import { Skeleton } from "@/components/atoms/Skeleton/Skeleton";
|
||||
import Image from "next/image";
|
||||
import { useRef, useState } from "react";
|
||||
import { AddToLibraryButton } from "../AddToLibraryButton/AddToLibraryButton";
|
||||
|
||||
interface Props {
|
||||
agent: StoreAgent;
|
||||
backgroundColor: string;
|
||||
}
|
||||
|
||||
export const FeaturedAgentCard = ({
|
||||
agent,
|
||||
backgroundColor,
|
||||
}: FeaturedStoreCardProps) => {
|
||||
// TODO: Need to use group for hover
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
function getAccentTextClass(bg: string) {
|
||||
if (bg.includes("violet")) return "text-violet-500 hover:text-violet-800";
|
||||
if (bg.includes("blue")) return "text-blue-500 hover:text-blue-800";
|
||||
if (bg.includes("green")) return "text-green-500 hover:text-green-800";
|
||||
return "text-zinc-500 hover:text-zinc-800";
|
||||
}
|
||||
|
||||
export function FeaturedAgentCard({ agent, backgroundColor }: Props) {
|
||||
const [imageError, setImageError] = useState(false);
|
||||
const [imageLoaded, setImageLoaded] = useState(false);
|
||||
const titleRef = useRef<HTMLSpanElement>(null);
|
||||
const [isTitleTruncated, setIsTitleTruncated] = useState(false);
|
||||
|
||||
function checkTitleOverflow() {
|
||||
const el = titleRef.current;
|
||||
if (el) setIsTitleTruncated(el.scrollHeight > el.clientHeight);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
<div
|
||||
className={`relative flex h-[28rem] w-full max-w-md cursor-pointer flex-col items-start rounded-2xl p-4 shadow-md transition-all duration-300 hover:shadow-lg ${backgroundColor} border`}
|
||||
data-testid="featured-store-card"
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
className={`flex h-full flex-col ${backgroundColor} rounded-[1.5rem] border-none`}
|
||||
>
|
||||
<CardHeader>
|
||||
<CardTitle className="line-clamp-2 text-base sm:text-xl">
|
||||
{agent.agent_name}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-sm">
|
||||
By {agent.creator}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 p-4">
|
||||
<div className="relative aspect-[4/3] w-full overflow-hidden rounded-xl">
|
||||
<Image
|
||||
src={agent.agent_image || "/autogpt-logo-dark-bg.png"}
|
||||
alt={`${agent.agent_name} preview`}
|
||||
fill
|
||||
sizes="100%"
|
||||
className={`object-cover transition-opacity duration-200 ${
|
||||
isHovered ? "opacity-0" : "opacity-100"
|
||||
}`}
|
||||
{/* Image */}
|
||||
<div className="relative aspect-[2/1.2] w-full overflow-hidden rounded-xl md:aspect-[2.17/1]">
|
||||
{agent.agent_image && !imageError ? (
|
||||
<>
|
||||
{!imageLoaded && (
|
||||
<Skeleton className="absolute inset-0 rounded-xl" />
|
||||
)}
|
||||
<Image
|
||||
src={agent.agent_image}
|
||||
alt={`${agent.agent_name} preview image`}
|
||||
fill
|
||||
className="object-cover"
|
||||
onLoad={() => setImageLoaded(true)}
|
||||
onError={() => setImageError(true)}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="absolute inset-0 rounded-xl bg-violet-50" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-3 flex w-full flex-1 flex-col">
|
||||
{/* Agent Name and Creator */}
|
||||
<div className="flex w-full flex-col">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span
|
||||
ref={titleRef}
|
||||
onPointerEnter={checkTitleOverflow}
|
||||
className="line-clamp-2 block min-h-[2lh] min-w-0 leading-tight"
|
||||
>
|
||||
<Text variant="h4" as="span" className="leading-tight">
|
||||
{agent.agent_name}
|
||||
</Text>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
{isTitleTruncated && (
|
||||
<TooltipContent>
|
||||
<p>{agent.agent_name}</p>
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
{agent.creator && (
|
||||
<div className="mt-3 flex items-center gap-2">
|
||||
<Avatar className="h-6 w-6 shrink-0">
|
||||
{agent.creator_avatar && (
|
||||
<AvatarImage
|
||||
src={agent.creator_avatar}
|
||||
alt={`${agent.creator} creator avatar`}
|
||||
/>
|
||||
)}
|
||||
<AvatarFallback size={32}>
|
||||
{agent.creator.charAt(0)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<Text variant="body-medium" className="truncate">
|
||||
by {agent.creator}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="mt-2.5 flex w-full flex-col">
|
||||
<Text variant="body" className="line-clamp-3 leading-normal">
|
||||
{agent.description}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<Text variant="body" className="absolute bottom-4 left-4 text-zinc-500">
|
||||
{agent.runs === 0
|
||||
? "No runs"
|
||||
: `${(agent.runs ?? 0).toLocaleString()} runs`}
|
||||
</Text>
|
||||
{agent.creator && agent.slug && agent.agent_graph_id && (
|
||||
<div className="absolute bottom-2 right-0">
|
||||
<AddToLibraryButton
|
||||
creatorSlug={agent.creator}
|
||||
agentSlug={agent.slug}
|
||||
agentName={agent.agent_name}
|
||||
agentGraphID={agent.agent_graph_id}
|
||||
className={getAccentTextClass(backgroundColor)}
|
||||
/>
|
||||
<div
|
||||
className={`absolute inset-0 overflow-y-auto p-4 transition-opacity duration-200 ${
|
||||
isHovered ? "opacity-100" : "opacity-0"
|
||||
}`}
|
||||
>
|
||||
<CardDescription className="line-clamp-[6] text-xs sm:line-clamp-[8] sm:text-sm">
|
||||
{agent.description}
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="flex items-center justify-between">
|
||||
<div className="font-semibold">
|
||||
{agent.runs?.toLocaleString() ?? "0"} runs
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<p>{agent.rating.toFixed(1) ?? "0.0"}</p>
|
||||
{StarRatingIcons(agent.rating)}
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { CreatorDetails } from "@/app/api/__generated__/models/creatorDetails";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { UserCircleDashedIcon } from "@phosphor-icons/react";
|
||||
import { CreatorCard } from "../CreatorCard/CreatorCard";
|
||||
import { useFeaturedCreators } from "./useFeaturedCreators";
|
||||
import { CreatorDetails } from "@/app/api/__generated__/models/creatorDetails";
|
||||
|
||||
interface FeaturedCreatorsProps {
|
||||
title?: string;
|
||||
@@ -19,9 +21,10 @@ export const FeaturedCreators = ({
|
||||
return (
|
||||
<div className="flex w-full flex-col items-center justify-center">
|
||||
<div className="w-full max-w-[1360px]">
|
||||
<h2 className="mb-9 font-poppins text-lg font-semibold text-neutral-800 dark:text-neutral-200">
|
||||
{title}
|
||||
</h2>
|
||||
<div className="mb-8 flex flex-row items-center gap-2">
|
||||
<UserCircleDashedIcon size={24} />
|
||||
<Text variant="h4">{title}</Text>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4">
|
||||
{displayedCreators.map((creator, index) => (
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import { StoreAgent } from "@/app/api/__generated__/models/storeAgent";
|
||||
import {
|
||||
Carousel,
|
||||
CarouselContent,
|
||||
CarouselItem,
|
||||
CarouselPrevious,
|
||||
CarouselNext,
|
||||
CarouselIndicator,
|
||||
CarouselItem,
|
||||
CarouselNext,
|
||||
CarouselPrevious,
|
||||
} from "@/components/__legacy__/ui/carousel";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { SparkleIcon } from "@phosphor-icons/react";
|
||||
import Link from "next/link";
|
||||
import { useFeaturedSection } from "./useFeaturedSection";
|
||||
import { StoreAgent } from "@/app/api/__generated__/models/storeAgent";
|
||||
import { getBackgroundColor } from "./helper";
|
||||
import { FeaturedAgentCard } from "../FeaturedAgentCard/FeaturedAgentCard";
|
||||
import { useFeaturedSection } from "./useFeaturedSection";
|
||||
|
||||
const FEATURED_COLORS = [
|
||||
"bg-violet-50 border-violet-100/70",
|
||||
"bg-blue-50 border-blue-100/70",
|
||||
"bg-green-50 border-green-100/70",
|
||||
];
|
||||
|
||||
interface FeaturedSectionProps {
|
||||
featuredAgents: StoreAgent[];
|
||||
@@ -25,38 +32,52 @@ export const FeaturedSection = ({ featuredAgents }: FeaturedSectionProps) => {
|
||||
|
||||
return (
|
||||
<section className="w-full">
|
||||
<h2 className="mb-8 font-poppins text-2xl font-semibold leading-7 text-neutral-800 dark:text-neutral-200">
|
||||
Featured agents
|
||||
</h2>
|
||||
<div className="mb-8 flex flex-row items-center gap-2">
|
||||
<SparkleIcon size={24} />
|
||||
<Text variant="h4">Featured Agents</Text>
|
||||
</div>
|
||||
|
||||
<Carousel
|
||||
opts={{
|
||||
align: "center",
|
||||
containScroll: "trimSnaps",
|
||||
}}
|
||||
className="-mx-4"
|
||||
>
|
||||
<CarouselContent>
|
||||
{featuredAgents.map((agent, index) => (
|
||||
<CarouselItem
|
||||
key={index}
|
||||
className="h-[480px] md:basis-1/2 lg:basis-1/3"
|
||||
>
|
||||
<Link
|
||||
href={`/marketplace/agent/${encodeURIComponent(agent.creator)}/${encodeURIComponent(agent.slug)}`}
|
||||
className="block h-full"
|
||||
<div className="relative">
|
||||
<CarouselContent className="px-4">
|
||||
{featuredAgents.map((agent, index) => (
|
||||
<CarouselItem
|
||||
key={index}
|
||||
className="h-[480px] md:basis-1/2 lg:basis-1/3"
|
||||
>
|
||||
<FeaturedAgentCard
|
||||
agent={agent}
|
||||
backgroundColor={getBackgroundColor(index)}
|
||||
/>
|
||||
</Link>
|
||||
</CarouselItem>
|
||||
))}
|
||||
</CarouselContent>
|
||||
<div className="relative mt-4">
|
||||
<CarouselIndicator />
|
||||
<CarouselPrevious afterClick={handlePrevSlide} />
|
||||
<CarouselNext afterClick={handleNextSlide} />
|
||||
<Link
|
||||
href={`/marketplace/agent/${encodeURIComponent(agent.creator)}/${encodeURIComponent(agent.slug)}`}
|
||||
className="block h-full"
|
||||
>
|
||||
<FeaturedAgentCard
|
||||
agent={agent}
|
||||
backgroundColor={
|
||||
FEATURED_COLORS[index % FEATURED_COLORS.length]
|
||||
}
|
||||
/>
|
||||
</Link>
|
||||
</CarouselItem>
|
||||
))}
|
||||
</CarouselContent>
|
||||
<div className="pointer-events-none absolute inset-y-0 left-0 w-8 bg-gradient-to-r from-[rgb(246,247,248)] to-transparent" />
|
||||
<div className="pointer-events-none absolute inset-y-0 right-0 w-8 bg-gradient-to-l from-[rgb(246,247,248)] to-transparent" />
|
||||
</div>
|
||||
<div className="relative mt-2">
|
||||
<CarouselIndicator className="-mt-6 ml-8" />
|
||||
<CarouselPrevious
|
||||
afterClick={handlePrevSlide}
|
||||
className="right-14 h-10 w-10"
|
||||
/>
|
||||
<CarouselNext
|
||||
afterClick={handleNextSlide}
|
||||
className="right-2 h-10 w-10"
|
||||
/>
|
||||
</div>
|
||||
</Carousel>
|
||||
</section>
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
const BACKGROUND_COLORS = [
|
||||
"bg-violet-200 dark:bg-violet-800", // #ddd6fe / #5b21b6
|
||||
"bg-blue-200 dark:bg-blue-800", // #bfdbfe / #1e3a8a
|
||||
"bg-green-200 dark:bg-green-800", // #bbf7d0 / #065f46
|
||||
];
|
||||
|
||||
export const getBackgroundColor = (index: number) => {
|
||||
return BACKGROUND_COLORS[index % BACKGROUND_COLORS.length];
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Badge } from "@/components/__legacy__/ui/badge";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { MagnifyingGlass } from "@phosphor-icons/react";
|
||||
import { useFilterChips } from "./useFilterChips";
|
||||
|
||||
interface FilterChipsProps {
|
||||
@@ -9,32 +10,56 @@ interface FilterChipsProps {
|
||||
multiSelect?: boolean;
|
||||
}
|
||||
|
||||
// Some flaws in its logic
|
||||
// FRONTEND-TODO : This needs to be fixed
|
||||
export const FilterChips = ({
|
||||
export function FilterChips({
|
||||
badges,
|
||||
onFilterChange,
|
||||
multiSelect = true,
|
||||
}: FilterChipsProps) => {
|
||||
}: FilterChipsProps) {
|
||||
const { selectedFilters, handleBadgeClick } = useFilterChips({
|
||||
multiSelect,
|
||||
onFilterChange,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex h-auto min-h-8 flex-wrap items-center justify-center gap-3 lg:min-h-14 lg:justify-start lg:gap-5">
|
||||
{badges.map((badge) => (
|
||||
<Badge
|
||||
key={badge}
|
||||
variant={selectedFilters.includes(badge) ? "secondary" : "outline"}
|
||||
className="mb-2 flex cursor-pointer items-center justify-center gap-2 rounded-full border border-black/50 px-3 py-1 dark:border-white/50 lg:mb-3 lg:gap-2.5 lg:px-6 lg:py-2"
|
||||
onClick={() => handleBadgeClick(badge)}
|
||||
>
|
||||
<div className="text-sm font-light tracking-tight text-[#474747] dark:text-[#e0e0e0] lg:text-xl lg:font-medium lg:leading-9">
|
||||
{badge}
|
||||
</div>
|
||||
</Badge>
|
||||
))}
|
||||
<div className="flex h-auto min-h-8 flex-wrap items-center justify-center gap-3 lg:min-h-14 lg:justify-start">
|
||||
{badges.map((badge) => {
|
||||
const isSelected = selectedFilters.includes(badge);
|
||||
return (
|
||||
<button
|
||||
key={badge}
|
||||
type="button"
|
||||
onClick={() => handleBadgeClick(badge)}
|
||||
className={cn(
|
||||
"group relative inline-flex items-center gap-2 rounded-full p-[1px] text-sm font-medium transition-all hover:scale-[1.03] md:text-lg",
|
||||
isSelected
|
||||
? "bg-gradient-to-r from-blue-400 via-purple-400 to-indigo-400"
|
||||
: "bg-gradient-to-r from-blue-200 via-purple-200 to-indigo-200",
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1.5 rounded-full px-3 py-1.5 text-transparent transition-all md:gap-2 md:px-5 md:py-2",
|
||||
isSelected
|
||||
? "bg-purple-50 group-hover:bg-purple-100"
|
||||
: "bg-[rgb(246,247,248)] group-hover:bg-[rgb(236,237,238)]",
|
||||
)}
|
||||
>
|
||||
<MagnifyingGlass
|
||||
size={18}
|
||||
weight="regular"
|
||||
className={cn(
|
||||
isSelected
|
||||
? "text-purple-500"
|
||||
: "text-purple-300 group-hover:text-purple-400",
|
||||
)}
|
||||
/>
|
||||
<span className="bg-gradient-to-r from-blue-400 via-purple-400 to-indigo-400 bg-clip-text">
|
||||
{badge}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,37 +1,41 @@
|
||||
import { useState } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
interface useFilterChipsProps {
|
||||
onFilterChange?: (selectedFilters: string[]) => void;
|
||||
multiSelect?: boolean;
|
||||
}
|
||||
|
||||
export const useFilterChips = ({
|
||||
export function useFilterChips({
|
||||
onFilterChange,
|
||||
multiSelect,
|
||||
}: useFilterChipsProps) => {
|
||||
}: useFilterChipsProps) {
|
||||
const [selectedFilters, setSelectedFilters] = useState<string[]>([]);
|
||||
const pendingFilters = useRef<string[] | null>(null);
|
||||
|
||||
const handleBadgeClick = (badge: string) => {
|
||||
setSelectedFilters((prevFilters) => {
|
||||
let newFilters;
|
||||
if (multiSelect) {
|
||||
newFilters = prevFilters.includes(badge)
|
||||
? prevFilters.filter((filter) => filter !== badge)
|
||||
: [...prevFilters, badge];
|
||||
} else {
|
||||
newFilters = prevFilters.includes(badge) ? [] : [badge];
|
||||
}
|
||||
useEffect(() => {
|
||||
if (pendingFilters.current !== null) {
|
||||
onFilterChange?.(pendingFilters.current);
|
||||
pendingFilters.current = null;
|
||||
}
|
||||
}, [selectedFilters, onFilterChange]);
|
||||
|
||||
if (onFilterChange) {
|
||||
onFilterChange(newFilters);
|
||||
}
|
||||
const handleBadgeClick = useCallback(
|
||||
(badge: string) => {
|
||||
setSelectedFilters((prev) => {
|
||||
let next;
|
||||
if (multiSelect) {
|
||||
next = prev.includes(badge)
|
||||
? prev.filter((f) => f !== badge)
|
||||
: [...prev, badge];
|
||||
} else {
|
||||
next = prev.includes(badge) ? [] : [badge];
|
||||
}
|
||||
pendingFilters.current = next;
|
||||
return next;
|
||||
});
|
||||
},
|
||||
[multiSelect],
|
||||
);
|
||||
|
||||
return newFilters;
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
selectedFilters,
|
||||
handleBadgeClick,
|
||||
};
|
||||
};
|
||||
return { selectedFilters, handleBadgeClick };
|
||||
}
|
||||
|
||||
@@ -1,36 +1,46 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { FilterChips } from "../FilterChips/FilterChips";
|
||||
import { SearchBar } from "../SearchBar/SearchBar";
|
||||
import { useHeroSection } from "./useHeroSection";
|
||||
|
||||
const textStyles =
|
||||
"font-poppins text-[2.45rem] leading-[2.75rem] md:text-[3rem] font-semibold md:leading-[3.375rem]";
|
||||
|
||||
export const HeroSection = () => {
|
||||
const { onFilterChange, searchTerms } = useHeroSection();
|
||||
return (
|
||||
<div className="mb-2 mt-8 flex flex-col items-center justify-center px-4 sm:mb-4 sm:mt-12 sm:px-6 md:mb-6 md:mt-16 lg:my-24 lg:px-8 xl:my-16">
|
||||
<div className="mb-12 mt-8 flex flex-col items-center justify-center px-4 sm:mt-12 sm:px-6 md:mt-16 lg:my-24 lg:px-8 xl:my-16">
|
||||
<div className="w-full max-w-3xl lg:max-w-4xl xl:max-w-5xl">
|
||||
<div className="mb-4 text-center md:mb-8">
|
||||
<h1 className="text-center">
|
||||
<span className="font-poppins text-[48px] font-semibold leading-[54px] text-neutral-950 dark:text-neutral-50">
|
||||
<span
|
||||
className={cn(
|
||||
textStyles,
|
||||
"text-neutral-950 dark:text-neutral-50",
|
||||
)}
|
||||
>
|
||||
Explore AI agents built for{" "}
|
||||
</span>
|
||||
<span className="font-poppins text-[48px] font-semibold leading-[54px] text-violet-600">
|
||||
you
|
||||
</span>
|
||||
<span className={cn(textStyles, "text-violet-600")}>you</span>
|
||||
<br />
|
||||
<span className="font-poppins text-[48px] font-semibold leading-[54px] text-neutral-950 dark:text-neutral-50">
|
||||
<span
|
||||
className={cn(
|
||||
textStyles,
|
||||
"text-neutral-950 dark:text-neutral-50",
|
||||
)}
|
||||
>
|
||||
by the{" "}
|
||||
</span>
|
||||
<span className="font-poppins text-[48px] font-semibold leading-[54px] text-blue-500">
|
||||
community
|
||||
</span>
|
||||
<span className={cn(textStyles, "text-blue-500")}>community</span>
|
||||
</h1>
|
||||
</div>
|
||||
<h3 className="mb:text-2xl mb-6 text-center font-sans text-xl font-normal leading-loose text-neutral-700 dark:text-neutral-300 md:mb-12">
|
||||
<h3 className="mb-6 text-center font-sans text-lg font-normal leading-normal text-neutral-700 dark:text-neutral-300 md:mb-12 md:text-xl">
|
||||
Bringing you AI agents designed by thinkers from around the world
|
||||
</h3>
|
||||
<div className="mb-4 flex justify-center sm:mb-5">
|
||||
<SearchBar height="h-[74px]" />
|
||||
<div className="mb-4 flex w-full justify-center sm:mb-5">
|
||||
<SearchBar />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex justify-center">
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
"use client";
|
||||
import { okData } from "@/app/api/helpers";
|
||||
import { Separator } from "@/components/__legacy__/ui/separator";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Breadcrumbs } from "@/components/molecules/Breadcrumbs/Breadcrumbs";
|
||||
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
|
||||
import { okData } from "@/app/api/helpers";
|
||||
import { ArrowLeftIcon } from "@phosphor-icons/react";
|
||||
import { MarketplaceAgentPageParams } from "../../agent/[creator]/[slug]/page";
|
||||
import { AgentImages } from "../AgentImages/AgentImage";
|
||||
import { AgentInfo } from "../AgentInfo/AgentInfo";
|
||||
@@ -27,15 +29,7 @@ export function MainAgentPage({ params }: Props) {
|
||||
} = useMainAgentPage({ params });
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-[1360px]">
|
||||
<main className="px-4">
|
||||
<div className="flex h-[600px] items-center justify-center">
|
||||
<AgentPageLoading />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
return <AgentPageLoading />;
|
||||
}
|
||||
|
||||
if (hasError) {
|
||||
@@ -84,19 +78,33 @@ export function MainAgentPage({ params }: Props) {
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-[1360px]">
|
||||
<main className="mt-5 px-4">
|
||||
<Breadcrumbs items={breadcrumbs} />
|
||||
<main className="mt-5 px-4 pb-12">
|
||||
<div className="mb-4 flex items-center justify-between px-4 md:!-mb-3">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="small"
|
||||
as="NextLink"
|
||||
href="/marketplace"
|
||||
className="relative -left-2 lg:!-left-4"
|
||||
leftIcon={<ArrowLeftIcon size={16} />}
|
||||
>
|
||||
Go back
|
||||
</Button>
|
||||
<div className="hidden md:block">
|
||||
<Breadcrumbs items={breadcrumbs} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<div className="mt-0 flex flex-col items-start gap-4 sm:mt-6 sm:gap-6 lg:mt-8 lg:flex-row lg:gap-12">
|
||||
<div className="w-full lg:w-2/5">
|
||||
<AgentInfo
|
||||
user={user}
|
||||
agentId={agentData.active_version_id ?? "–"}
|
||||
name={agentData.agent_name ?? ""}
|
||||
creator={agentData.creator ?? ""}
|
||||
creatorAvatar={agentData.creator_avatar ?? ""}
|
||||
shortDescription={agentData.sub_heading ?? ""}
|
||||
longDescription={agentData.description ?? ""}
|
||||
rating={agentData.rating ?? 0}
|
||||
runs={agentData.runs ?? 0}
|
||||
categories={agentData.categories ?? []}
|
||||
lastUpdated={
|
||||
@@ -144,22 +152,20 @@ export function MainAgentPage({ params }: Props) {
|
||||
})()}
|
||||
/>
|
||||
</div>
|
||||
<Separator className="mb-[25px] mt-[60px]" />
|
||||
<Separator className="my-6 bg-transparent" />
|
||||
{otherAgents && (
|
||||
<AgentsSection
|
||||
margin="32px"
|
||||
agents={otherAgents.agents}
|
||||
sectionTitle={`Other agents by ${agentData.creator ?? ""}`}
|
||||
/>
|
||||
)}
|
||||
<Separator className="mb-[25px] mt-[60px]" />
|
||||
{similarAgents && (
|
||||
<Separator className="mb-[25px] mt-[60px] bg-transparent" />
|
||||
{similarAgents && similarAgents.agents.length > 0 ? (
|
||||
<AgentsSection
|
||||
margin="32px"
|
||||
agents={similarAgents.agents}
|
||||
sectionTitle="Similar agents"
|
||||
/>
|
||||
)}
|
||||
) : null}
|
||||
<BecomeACreator />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Separator } from "@/components/__legacy__/ui/separator";
|
||||
import { AgentsSection } from "../AgentsSection/AgentsSection";
|
||||
import { MarketplaceCreatorPageParams } from "../../creator/[creator]/page";
|
||||
import { Breadcrumbs } from "@/components/molecules/Breadcrumbs/Breadcrumbs";
|
||||
import { CreatorInfoCard } from "../CreatorInfoCard/CreatorInfoCard";
|
||||
import { CreatorLinks } from "../CreatorLinks/CreatorLinks";
|
||||
import { useMainCreatorPage } from "./useMainCreatorPage";
|
||||
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
|
||||
import { CreatorPageLoading } from "../CreatorPageLoading";
|
||||
|
||||
interface MainCreatorPageProps {
|
||||
params: MarketplaceCreatorPageParams;
|
||||
}
|
||||
|
||||
export const MainCreatorPage = ({ params }: MainCreatorPageProps) => {
|
||||
const { creatorAgents, creator, isLoading, hasError } = useMainCreatorPage({
|
||||
params,
|
||||
});
|
||||
|
||||
if (isLoading) return <CreatorPageLoading />;
|
||||
|
||||
if (hasError) {
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-[1360px]">
|
||||
<div className="flex min-h-[60vh] items-center justify-center">
|
||||
<ErrorCard
|
||||
isSuccess={false}
|
||||
responseError={{ message: "Failed to load creator data" }}
|
||||
context="creator page"
|
||||
onRetry={() => window.location.reload()}
|
||||
className="w-full max-w-md"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (creator)
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-[1360px]">
|
||||
<main className="mt-5 px-4">
|
||||
<Breadcrumbs
|
||||
items={[
|
||||
{ name: "Marketplace", 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="text-underline-position-from-font text-decoration-skip-none text-left font-poppins text-base font-medium leading-6">
|
||||
About
|
||||
</p>
|
||||
<div
|
||||
className="text-[48px] font-normal leading-[59px] text-neutral-900 dark:text-zinc-50"
|
||||
style={{ whiteSpace: "pre-line" }}
|
||||
data-testid="creator-description"
|
||||
>
|
||||
{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" />
|
||||
{creatorAgents && (
|
||||
<AgentsSection
|
||||
agents={creatorAgents.agents}
|
||||
hideAvatars={true}
|
||||
sectionTitle={`Agents by ${creator.name}`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,13 +1,13 @@
|
||||
"use client";
|
||||
import { Separator } from "@/components/__legacy__/ui/separator";
|
||||
import { FeaturedSection } from "../FeaturedSection/FeaturedSection";
|
||||
import { BecomeACreator } from "../BecomeACreator/BecomeACreator";
|
||||
import { HeroSection } from "../HeroSection/HeroSection";
|
||||
import { AgentsSection } from "../AgentsSection/AgentsSection";
|
||||
import { useMainMarketplacePage } from "./useMainMarketplacePage";
|
||||
import { FeaturedCreators } from "../FeaturedCreators/FeaturedCreators";
|
||||
import { MainMarketplacePageLoading } from "../MainMarketplacePageLoading";
|
||||
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
|
||||
import { AgentsSection } from "../AgentsSection/AgentsSection";
|
||||
import { BecomeACreator } from "../BecomeACreator/BecomeACreator";
|
||||
import { FeaturedCreators } from "../FeaturedCreators/FeaturedCreators";
|
||||
import { FeaturedSection } from "../FeaturedSection/FeaturedSection";
|
||||
import { HeroSection } from "../HeroSection/HeroSection";
|
||||
import { MainMarketplacePageLoading } from "../MainMarketplacePageLoading";
|
||||
import { useMainMarketplacePage } from "./useMainMarketplacePage";
|
||||
|
||||
export const MainMarkeplacePage = () => {
|
||||
const { featuredAgents, topAgents, featuredCreators, isLoading, hasError } =
|
||||
@@ -38,22 +38,22 @@ export const MainMarkeplacePage = () => {
|
||||
return (
|
||||
// FRONTEND-TODO : Need better state location, need to fetch creators and agents in their respective file, Can't do it right now because these files are used in some other pages of marketplace, will fix it when encounter with those pages
|
||||
<div className="mx-auto w-full max-w-[1360px]">
|
||||
<main className="px-4">
|
||||
<main className="px-4 pb-12">
|
||||
<HeroSection />
|
||||
{featuredAgents && (
|
||||
<FeaturedSection featuredAgents={featuredAgents.agents} />
|
||||
)}
|
||||
{/* 100px margin because our featured sections button are placed 40px below the container */}
|
||||
<Separator className="mb-6 mt-24" />
|
||||
<Separator className="mb-6 mt-24 bg-transparent" />
|
||||
|
||||
{topAgents && (
|
||||
<AgentsSection sectionTitle="Top Agents" agents={topAgents.agents} />
|
||||
<AgentsSection sectionTitle="All Agents" agents={topAgents.agents} />
|
||||
)}
|
||||
<Separator className="mb-[25px] mt-[60px]" />
|
||||
<Separator className="mb-[25px] mt-[60px] bg-transparent" />
|
||||
{featuredCreators && (
|
||||
<FeaturedCreators featuredCreators={featuredCreators.creators} />
|
||||
)}
|
||||
<Separator className="mb-[25px] mt-[60px]" />
|
||||
<Separator className="mb-[25px] mt-[60px] bg-transparent" />
|
||||
<BecomeACreator
|
||||
title="Become a Creator"
|
||||
description="Join our ever-growing community of hackers and tinkerers"
|
||||
|
||||
@@ -1,27 +1,62 @@
|
||||
import { Skeleton } from "@/components/__legacy__/ui/skeleton";
|
||||
import { Skeleton } from "@/components/atoms/Skeleton/Skeleton";
|
||||
|
||||
export const MainSearchResultPageLoading = () => {
|
||||
export function MainSearchResultPageLoading() {
|
||||
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">
|
||||
{/* Go back button */}
|
||||
<div className="mb-4 mt-5">
|
||||
<Skeleton className="h-9 w-24 rounded-full" />
|
||||
</div>
|
||||
|
||||
{/* Header: search term + search bar */}
|
||||
<div className="flex flex-col gap-4 md:flex-row md:items-center">
|
||||
<div className="flex-1">
|
||||
<Skeleton className="mb-2 h-5 w-32 bg-neutral-200 dark:bg-neutral-700" />
|
||||
<Skeleton className="h-8 w-64 bg-neutral-200 dark:bg-neutral-700" />
|
||||
<Skeleton className="mb-2 h-5 w-36" />
|
||||
<Skeleton className="h-8 w-56" />
|
||||
</div>
|
||||
<div className="flex-none">
|
||||
<Skeleton className="h-[60px] w-[439px] bg-neutral-200 dark:bg-neutral-700" />
|
||||
<Skeleton className="h-[2.75rem] w-full rounded-full md:w-[439px]" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-[36px] flex items-center justify-between">
|
||||
<Skeleton className="h-8 w-48 bg-neutral-200 dark:bg-neutral-700" />
|
||||
<Skeleton className="h-8 w-32 bg-neutral-200 dark:bg-neutral-700" />
|
||||
|
||||
{/* Filter chips + sort */}
|
||||
<div className="mt-6 flex flex-col gap-3 md:mt-9 md:flex-row md:items-center md:justify-between">
|
||||
<div className="flex gap-2">
|
||||
<Skeleton className="h-9 w-16 rounded-full" />
|
||||
<Skeleton className="h-9 w-20 rounded-full" />
|
||||
<Skeleton className="h-9 w-24 rounded-full" />
|
||||
</div>
|
||||
<Skeleton className="h-9 w-32 rounded-lg" />
|
||||
</div>
|
||||
<div className="mt-20 flex flex-col items-center justify-center">
|
||||
<Skeleton className="mb-4 h-6 w-40 bg-neutral-200 dark:bg-neutral-700" />
|
||||
<Skeleton className="h-6 w-80 bg-neutral-200 dark:bg-neutral-700" />
|
||||
|
||||
{/* Agent cards grid */}
|
||||
<div className="space-y-8 py-8">
|
||||
<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">
|
||||
{Array.from({ length: 8 }).map((_, i) => (
|
||||
<Skeleton key={i} className="h-[25rem] w-full rounded-2xl" />
|
||||
))}
|
||||
</div>
|
||||
{/* Mobile carousel placeholder */}
|
||||
<div className="flex gap-4 overflow-hidden md:hidden">
|
||||
<Skeleton className="h-[25rem] min-w-64 rounded-2xl" />
|
||||
<Skeleton className="h-[25rem] min-w-64 rounded-2xl" />
|
||||
</div>
|
||||
|
||||
{/* Separator */}
|
||||
<Skeleton className="h-px w-full" />
|
||||
|
||||
{/* Creator cards section */}
|
||||
<div className="space-y-6">
|
||||
<Skeleton className="h-6 w-24" />
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4">
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<Skeleton key={i} className="h-[16rem] w-full rounded-2xl" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,44 +1,40 @@
|
||||
"use client";
|
||||
|
||||
import { MagnifyingGlassIcon } from "@radix-ui/react-icons";
|
||||
import { MagnifyingGlass } from "@phosphor-icons/react";
|
||||
import { useSearchbar } from "./useSearchBar";
|
||||
|
||||
interface SearchBarProps {
|
||||
placeholder?: string;
|
||||
backgroundColor?: string;
|
||||
iconColor?: string;
|
||||
textColor?: string;
|
||||
placeholderColor?: string;
|
||||
width?: string;
|
||||
height?: string;
|
||||
}
|
||||
|
||||
export const SearchBar = ({
|
||||
export function SearchBar({
|
||||
placeholder = 'Search for tasks like "optimise SEO"',
|
||||
backgroundColor = "bg-neutral-100 dark:bg-neutral-800",
|
||||
iconColor = "text-[#646464] dark:text-neutral-400",
|
||||
textColor = "text-[#707070] dark:text-neutral-200",
|
||||
placeholderColor = "text-[#707070] dark:text-neutral-400",
|
||||
width = "w-9/10 lg:w-[56.25rem]",
|
||||
height = "h-[60px]",
|
||||
}: SearchBarProps) => {
|
||||
width = "w-full lg:w-[56.25rem]",
|
||||
height = "h-[3.8rem]",
|
||||
}: SearchBarProps) {
|
||||
const { handleSubmit, setSearchQuery, searchQuery } = useSearchbar();
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
data-testid="store-search-bar"
|
||||
className={`${width} ${height} px-4 pt-2 md:px-6 md:pt-1 ${backgroundColor} flex items-center justify-center gap-2 rounded-full md:gap-5`}
|
||||
className={`${width} ${height} flex items-center gap-3 rounded-full border border-zinc-200 bg-white px-4 shadow-none focus-within:border-zinc-400 focus-within:ring-1 focus-within:ring-zinc-400 focus-within:ring-offset-0`}
|
||||
>
|
||||
<MagnifyingGlassIcon className={`h-5 w-5 md:h-7 md:w-7 ${iconColor}`} />
|
||||
<MagnifyingGlass
|
||||
size={20}
|
||||
className="text-zinc-400 md:h-6 md:w-6"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
className={`flex-grow border-none bg-transparent ${textColor} font-sans text-lg font-normal leading-[2.25rem] tracking-tight md:text-xl placeholder:${placeholderColor} focus:outline-none`}
|
||||
className="flex-grow border-none bg-transparent text-base font-normal text-black placeholder:text-base placeholder:font-normal placeholder:text-zinc-400 focus:outline-none md:text-lg md:placeholder:text-lg"
|
||||
data-testid="store-search-input"
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
import Image from "next/image";
|
||||
"use client";
|
||||
|
||||
import { StarRatingIcons } from "@/components/__legacy__/ui/icons";
|
||||
import Avatar, {
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/components/atoms/Avatar/Avatar";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/atoms/Tooltip/BaseTooltip";
|
||||
import { Skeleton } from "@/components/atoms/Skeleton/Skeleton";
|
||||
import Image from "next/image";
|
||||
import { useRef, useState } from "react";
|
||||
import { AddToLibraryButton } from "../AddToLibraryButton/AddToLibraryButton";
|
||||
|
||||
interface StoreCardProps {
|
||||
interface Props {
|
||||
agentName: string;
|
||||
agentImage: string;
|
||||
description: string;
|
||||
@@ -15,9 +27,12 @@ interface StoreCardProps {
|
||||
avatarSrc: string;
|
||||
hideAvatar?: boolean;
|
||||
creatorName?: string;
|
||||
creatorSlug?: string;
|
||||
agentSlug?: string;
|
||||
agentGraphID?: string;
|
||||
}
|
||||
|
||||
export const StoreCard: React.FC<StoreCardProps> = ({
|
||||
export function StoreCard({
|
||||
agentName,
|
||||
agentImage,
|
||||
description,
|
||||
@@ -27,14 +42,27 @@ export const StoreCard: React.FC<StoreCardProps> = ({
|
||||
avatarSrc,
|
||||
hideAvatar = false,
|
||||
creatorName,
|
||||
}) => {
|
||||
creatorSlug,
|
||||
agentSlug,
|
||||
agentGraphID,
|
||||
}: Props) {
|
||||
const [imageError, setImageError] = useState(false);
|
||||
const [imageLoaded, setImageLoaded] = useState(false);
|
||||
const titleRef = useRef<HTMLSpanElement>(null);
|
||||
const [isTitleTruncated, setIsTitleTruncated] = useState(false);
|
||||
|
||||
function checkTitleOverflow() {
|
||||
const el = titleRef.current;
|
||||
if (el) setIsTitleTruncated(el.scrollHeight > el.clientHeight);
|
||||
}
|
||||
|
||||
const handleClick = () => {
|
||||
onClick();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex h-[27rem] w-full max-w-md cursor-pointer flex-col items-start rounded-3xl bg-background transition-all duration-300 hover:shadow-lg dark:hover:shadow-gray-700"
|
||||
className="relative flex h-[25rem] w-full max-w-md cursor-pointer flex-col items-start rounded-2xl border border-border/50 bg-background p-4 shadow-md transition-all duration-300 hover:shadow-lg"
|
||||
onClick={handleClick}
|
||||
data-testid="store-card"
|
||||
role="button"
|
||||
@@ -46,77 +74,110 @@ 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]">
|
||||
{agentImage && (
|
||||
<Image
|
||||
src={agentImage}
|
||||
alt={`${agentName} preview image`}
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
)}
|
||||
{!hideAvatar && (
|
||||
<div className="absolute bottom-4 left-4">
|
||||
<Avatar className="h-16 w-16">
|
||||
{avatarSrc && (
|
||||
<AvatarImage
|
||||
src={avatarSrc}
|
||||
alt={`${creatorName || agentName} creator avatar`}
|
||||
/>
|
||||
)}
|
||||
<AvatarFallback size={64}>
|
||||
{(creatorName || agentName).charAt(0)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
{/* First Section: Image */}
|
||||
<div className="relative aspect-[2/1.2] w-full overflow-hidden rounded-xl md:aspect-[2.17/1]">
|
||||
{agentImage && !imageError ? (
|
||||
<>
|
||||
{!imageLoaded && (
|
||||
<Skeleton className="absolute inset-0 rounded-xl" />
|
||||
)}
|
||||
<Image
|
||||
src={agentImage}
|
||||
alt={`${agentName} preview image`}
|
||||
fill
|
||||
className="object-cover"
|
||||
onLoad={() => setImageLoaded(true)}
|
||||
onError={() => setImageError(true)}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="absolute inset-0 rounded-xl bg-violet-50" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-3 flex w-full flex-1 flex-col px-4">
|
||||
<div className="mt-3 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">
|
||||
{agentName}
|
||||
</h3>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span
|
||||
ref={titleRef}
|
||||
onPointerEnter={checkTitleOverflow}
|
||||
className="line-clamp-2 block min-h-[2.7lh] min-w-0 leading-tight"
|
||||
>
|
||||
<Text
|
||||
variant="h4"
|
||||
as="span"
|
||||
className="text-xl leading-tight"
|
||||
>
|
||||
{agentName}
|
||||
</Text>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
{isTitleTruncated && (
|
||||
<TooltipContent>
|
||||
<p>{agentName}</p>
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
{!hideAvatar && creatorName && (
|
||||
<p className="mt-3 truncate font-sans text-xl font-normal text-neutral-600 dark:text-neutral-400">
|
||||
by {creatorName}
|
||||
</p>
|
||||
<div className="mb-4 mt-2 flex items-center gap-2">
|
||||
<Avatar className="h-6 w-6 shrink-0">
|
||||
{avatarSrc && (
|
||||
<AvatarImage
|
||||
src={avatarSrc}
|
||||
alt={`${creatorName} creator avatar`}
|
||||
/>
|
||||
)}
|
||||
<AvatarFallback size={32}>
|
||||
{creatorName.charAt(0)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<Text variant="body-medium" className="truncate">
|
||||
by {creatorName}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Third Section: Description */}
|
||||
<div className="mt-2.5 flex w-full flex-col">
|
||||
<p className="line-clamp-3 text-base font-normal leading-normal text-neutral-600 dark:text-neutral-400">
|
||||
<Text variant="body" className="line-clamp-3 leading-normal">
|
||||
{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="text-lg font-semibold text-neutral-800 dark:text-neutral-200">
|
||||
{runs.toLocaleString()} runs
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="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>
|
||||
</div>
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<Text variant="body" className="absolute bottom-4 left-4 text-zinc-500">
|
||||
{runs === 0 ? "No runs" : `${runs.toLocaleString()} runs`}
|
||||
</Text>
|
||||
{rating >= 1 && (
|
||||
<div className="absolute bottom-4 right-4 flex items-center gap-2">
|
||||
<span className="text-lg font-semibold text-neutral-800">
|
||||
{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>
|
||||
)}
|
||||
{creatorSlug && agentSlug && agentGraphID && (
|
||||
<div className="absolute bottom-2 right-0">
|
||||
<AddToLibraryButton
|
||||
creatorSlug={creatorSlug}
|
||||
agentSlug={agentSlug}
|
||||
agentName={agentName}
|
||||
agentGraphID={agentGraphID}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
"use client";
|
||||
|
||||
import { AgentsSection } from "@/app/(platform)/marketplace/components/AgentsSection/AgentsSection";
|
||||
import { CreatorLinks } from "@/app/(platform)/marketplace/components/CreatorLinks/CreatorLinks";
|
||||
import { CreatorPageLoading } from "@/app/(platform)/marketplace/components/CreatorPageLoading";
|
||||
import { MarketplaceCreatorPageParams } from "@/app/(platform)/marketplace/creator/[creator]/page";
|
||||
|
||||
import Avatar, {
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/components/atoms/Avatar/Avatar";
|
||||
import { Badge } from "@/components/atoms/Badge/Badge";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { Breadcrumbs } from "@/components/molecules/Breadcrumbs/Breadcrumbs";
|
||||
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
|
||||
import { ArrowLeftIcon } from "@phosphor-icons/react";
|
||||
import { useMainCreatorPage } from "./useMainCreatorPage";
|
||||
|
||||
interface Props {
|
||||
params: MarketplaceCreatorPageParams;
|
||||
}
|
||||
|
||||
export function MainCreatorPage({ params }: Props) {
|
||||
const { creatorAgents, creator, isLoading, hasError } = useMainCreatorPage({
|
||||
params,
|
||||
});
|
||||
|
||||
if (isLoading) return <CreatorPageLoading />;
|
||||
|
||||
if (hasError) {
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-[1360px]">
|
||||
<div className="flex min-h-[60vh] items-center justify-center">
|
||||
<ErrorCard
|
||||
isSuccess={false}
|
||||
responseError={{ message: "Failed to load creator data" }}
|
||||
context="creator page"
|
||||
onRetry={() => window.location.reload()}
|
||||
className="w-full max-w-md"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!creator) return null;
|
||||
|
||||
const breadcrumbs = [
|
||||
{ name: "Marketplace", link: "/marketplace" },
|
||||
{ name: creator.name, link: "#" },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-[1360px]">
|
||||
<main className="mt-5 px-4 pb-12">
|
||||
<div className="mb-4 flex items-center justify-between px-4 md:!-mb-3">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="small"
|
||||
as="NextLink"
|
||||
href="/marketplace"
|
||||
className="relative -left-2 lg:!-left-4"
|
||||
leftIcon={<ArrowLeftIcon size={16} />}
|
||||
>
|
||||
Go back
|
||||
</Button>
|
||||
<div className="hidden md:block">
|
||||
<Breadcrumbs items={breadcrumbs} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-0 flex flex-col items-start gap-4 sm:mt-6 sm:gap-6 lg:mt-8 lg:flex-row lg:gap-12">
|
||||
{/* Creator info - left side */}
|
||||
<div className="w-full lg:w-2/5">
|
||||
<div className="w-full px-4 sm:px-6 lg:px-0">
|
||||
<div className="max-w-[45rem] rounded-2xl bg-gradient-to-r from-blue-200 to-indigo-200 p-[1px]">
|
||||
<div className="flex flex-col rounded-[calc(1rem-2px)] bg-gray-50 p-4">
|
||||
{/* Avatar */}
|
||||
<Avatar className="mb-4 h-20 w-20 sm:h-24 sm:w-24">
|
||||
{creator.avatar_url && (
|
||||
<AvatarImage
|
||||
src={creator.avatar_url}
|
||||
alt={`${creator.name} avatar`}
|
||||
/>
|
||||
)}
|
||||
<AvatarFallback size={96}>
|
||||
{creator.name.charAt(0)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
|
||||
{/* Name */}
|
||||
<Text
|
||||
variant="h2"
|
||||
className="mb-1"
|
||||
data-testid="creator-title"
|
||||
>
|
||||
{creator.name}
|
||||
</Text>
|
||||
|
||||
{/* Handle */}
|
||||
<Text variant="body" className="mb-4 text-neutral-500">
|
||||
@{creator.username}
|
||||
</Text>
|
||||
|
||||
{/* Description */}
|
||||
<Text
|
||||
variant="body"
|
||||
className="mb-6 leading-relaxed text-neutral-600"
|
||||
data-testid="creator-description"
|
||||
>
|
||||
{creator.description}
|
||||
</Text>
|
||||
|
||||
{/* Categories */}
|
||||
{creator.top_categories.length > 0 && (
|
||||
<div className="mb-6">
|
||||
<Text variant="h5" className="mb-2">
|
||||
Top categories
|
||||
</Text>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{creator.top_categories.map((category, index) => (
|
||||
<Badge
|
||||
variant="info"
|
||||
key={index}
|
||||
className="border border-purple-100 bg-purple-50 text-purple-800"
|
||||
>
|
||||
{category}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Links */}
|
||||
<CreatorLinks links={creator.links} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right side - empty for now, keeps layout consistent */}
|
||||
<div className="hidden lg:block lg:w-3/5" />
|
||||
</div>
|
||||
|
||||
<div className="my-6" />
|
||||
|
||||
{creatorAgents && (
|
||||
<AgentsSection
|
||||
agents={creatorAgents.agents}
|
||||
hideAvatars
|
||||
sectionTitle={`Agents by ${creator.name}`}
|
||||
/>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
useGetV2ListStoreAgents,
|
||||
} from "@/app/api/__generated__/endpoints/store/store";
|
||||
import { StoreAgentsResponse } from "@/app/api/__generated__/models/storeAgentsResponse";
|
||||
import { MarketplaceCreatorPageParams } from "../../creator/[creator]/page";
|
||||
import { MarketplaceCreatorPageParams } from "@/app/(platform)/marketplace/creator/[creator]/page";
|
||||
import { CreatorDetails } from "@/app/api/__generated__/models/creatorDetails";
|
||||
|
||||
interface useMainCreatorPageProps {
|
||||
@@ -7,7 +7,7 @@ import { CreatorDetails } from "@/app/api/__generated__/models/creatorDetails";
|
||||
import { getQueryClient } from "@/lib/react-query/queryClient";
|
||||
import { dehydrate, HydrationBoundary } from "@tanstack/react-query";
|
||||
import { Metadata } from "next";
|
||||
import { MainCreatorPage } from "../../components/MainCreatorPage/MainCreatorPage";
|
||||
import { MainCreatorPage } from "./components/MainCreatorPage/MainCreatorPage";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { SearchBar } from "@/components/__legacy__/SearchBar";
|
||||
import { useMainSearchResultPage } from "./useMainSearchResultPage";
|
||||
import { GetV2ListStoreAgentsParams } from "@/app/api/__generated__/models/getV2ListStoreAgentsParams";
|
||||
import { SearchFilterChips } from "@/components/__legacy__/SearchFilterChips";
|
||||
import { SortDropdown } from "@/components/__legacy__/SortDropdown";
|
||||
import { AgentsSection } from "../AgentsSection/AgentsSection";
|
||||
import { Separator } from "@/components/__legacy__/ui/separator";
|
||||
import { FeaturedCreators } from "../FeaturedCreators/FeaturedCreators";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
|
||||
import { MainMarketplacePageLoading } from "../MainMarketplacePageLoading";
|
||||
import { GetV2ListStoreAgentsParams } from "@/app/api/__generated__/models/getV2ListStoreAgentsParams";
|
||||
import { ArrowLeftIcon } from "@phosphor-icons/react";
|
||||
import { AgentsSection } from "../../../components/AgentsSection/AgentsSection";
|
||||
import { FeaturedCreators } from "../../../components/FeaturedCreators/FeaturedCreators";
|
||||
import { MainSearchResultPageLoading } from "../../../components/MainSearchResultPageLoading";
|
||||
import { SearchBar } from "../../../components/SearchBar/SearchBar";
|
||||
import { useMainSearchResultPage } from "./useMainSearchResultPage";
|
||||
|
||||
type MarketplaceSearchSort = GetV2ListStoreAgentsParams["sorted_by"];
|
||||
|
||||
@@ -38,7 +40,7 @@ export const MainSearchResultPage = ({
|
||||
const hasError = isAgentsError || isCreatorsError;
|
||||
|
||||
if (isLoading) {
|
||||
return <MainMarketplacePageLoading />;
|
||||
return <MainSearchResultPageLoading />;
|
||||
}
|
||||
|
||||
if (hasError) {
|
||||
@@ -56,39 +58,49 @@ export const MainSearchResultPage = ({
|
||||
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="mb-4 mt-5">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
as="NextLink"
|
||||
href="/marketplace"
|
||||
leftIcon={<ArrowLeftIcon size={16} />}
|
||||
>
|
||||
Go back
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 md:flex-row md:items-center">
|
||||
<div className="flex-1">
|
||||
<h2 className="text-base font-medium leading-normal text-neutral-800 dark:text-neutral-200">
|
||||
Results for:
|
||||
Showing results for:
|
||||
</h2>
|
||||
<h1 className="font-poppins text-2xl font-semibold leading-[32px] text-neutral-800 dark:text-neutral-100">
|
||||
{searchTerm}
|
||||
"{searchTerm}"
|
||||
</h1>
|
||||
</div>
|
||||
<div className="flex-none">
|
||||
<SearchBar width="w-[439px]" height="h-[60px]" />
|
||||
<SearchBar width="w-full md:w-[439px]" height="h-[2.75rem]" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{totalCount > 0 ? (
|
||||
<>
|
||||
<div className="mt-[36px] flex items-center justify-between">
|
||||
<div className="mt-6 flex flex-col gap-3 md:mt-[36px] md:flex-row md:items-center md:justify-between">
|
||||
<SearchFilterChips
|
||||
totalCount={totalCount}
|
||||
agentsCount={agentsCount}
|
||||
creatorsCount={creatorsCount}
|
||||
onFilterChange={handleFilterChange}
|
||||
/>
|
||||
<SortDropdown onSort={handleSortChange} />
|
||||
<div className="mt-4 md:!mt-0">
|
||||
<SortDropdown onSort={handleSortChange} />
|
||||
</div>
|
||||
</div>
|
||||
{/* Content section */}
|
||||
<div className="min-h-[500px] max-w-[1440px] space-y-8 py-8">
|
||||
{showAgents && agentsCount > 0 && agents && (
|
||||
<div className="mt-[36px]">
|
||||
<AgentsSection agents={agents} sectionTitle="Agents" />
|
||||
</div>
|
||||
<AgentsSection agents={agents} />
|
||||
)}
|
||||
|
||||
{showAgents && agentsCount > 0 && creatorsCount > 0 && (
|
||||
<Separator />
|
||||
)}
|
||||
@@ -101,7 +113,7 @@ export const MainSearchResultPage = ({
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="mt-20 flex flex-col items-center justify-center">
|
||||
<div className="flex min-h-[60vh] 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>
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { use } from "react";
|
||||
import { MainSearchResultPage } from "../components/MainSearchResultPage/MainSearchResultPage";
|
||||
import { GetV2ListStoreAgentsParams } from "@/app/api/__generated__/models/getV2ListStoreAgentsParams";
|
||||
import { use } from "react";
|
||||
import { MainSearchResultPage } from "./components/MainSearchResultPage/MainSearchResultPage";
|
||||
|
||||
type MarketplaceSearchSort = GetV2ListStoreAgentsParams["sorted_by"];
|
||||
type MarketplaceSearchPageSearchParams = {
|
||||
|
||||
@@ -41,10 +41,13 @@ export const SearchFilterChips: React.FC<SearchFilterChipsProps> = ({
|
||||
<button
|
||||
key={filter.value}
|
||||
onClick={() => handleFilterClick(filter.value)}
|
||||
disabled={filter.value !== "all" && filter.count === 0}
|
||||
className={`flex items-center gap-2.5 rounded-[34px] px-5 py-2 ${
|
||||
selected === filter.value
|
||||
? "bg-neutral-800 text-white dark:bg-neutral-100 dark:text-neutral-900"
|
||||
: "border border-neutral-600 text-neutral-800 dark:border-neutral-400 dark:text-neutral-200"
|
||||
filter.value !== "all" && filter.count === 0
|
||||
? "cursor-not-allowed border border-neutral-200 text-neutral-300 dark:border-neutral-700 dark:text-neutral-600"
|
||||
: selected === filter.value
|
||||
? "bg-neutral-800 text-white dark:bg-neutral-100 dark:text-neutral-900"
|
||||
: "border border-neutral-600 text-neutral-800 dark:border-neutral-400 dark:text-neutral-200"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
// This file has been updated for the Store's "Featured Agent Section". If you want to add Carousel, keep these components in mind: CarouselIndicator, CarouselPrevious, and CarouselNext.
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import useEmblaCarousel, {
|
||||
type UseEmblaCarouselType,
|
||||
} from "embla-carousel-react";
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/__legacy__/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type CarouselApi = UseEmblaCarouselType[1];
|
||||
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
|
||||
@@ -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-2 w-[1.5rem] rounded-[39px] bg-neutral-800 transition-all duration-500 dark:bg-neutral-200"
|
||||
: "h-2 w-2 rounded-full bg-neutral-300 transition-all duration-500 dark:bg-neutral-600",
|
||||
"cursor-pointer",
|
||||
)}
|
||||
/>
|
||||
@@ -323,11 +323,11 @@ const CarouselIndicator = React.forwardRef<
|
||||
CarouselIndicator.displayName = "CarouselIndicator";
|
||||
|
||||
export {
|
||||
type CarouselApi,
|
||||
Carousel,
|
||||
CarouselContent,
|
||||
CarouselItem,
|
||||
CarouselIndicator,
|
||||
CarouselPrevious,
|
||||
CarouselItem,
|
||||
CarouselNext,
|
||||
CarouselPrevious,
|
||||
type CarouselApi,
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
|
||||
import { environment } from "@/services/environment";
|
||||
import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag";
|
||||
import { AccountMenu } from "./components/AccountMenu/AccountMenu";
|
||||
import { FeedbackButton } from "./components/FeedbackButton";
|
||||
import { AgentActivityDropdown } from "./components/AgentActivityDropdown/AgentActivityDropdown";
|
||||
import { LoginButton } from "./components/LoginButton";
|
||||
import { MobileNavBar } from "./components/MobileNavbar/MobileNavBar";
|
||||
@@ -62,7 +63,7 @@ export function Navbar() {
|
||||
<PreviewBanner branchName={previewBranchName} />
|
||||
) : null}
|
||||
<nav
|
||||
className="inline-flex w-full items-center border border-none bg-[#FAFAFA] p-3 backdrop-blur-[26px]"
|
||||
className="inline-flex w-full items-center bg-[#FAFAFA]/80 p-3 backdrop-blur-xl"
|
||||
style={{ height: NAVBAR_HEIGHT_PX }}
|
||||
>
|
||||
{/* Left section */}
|
||||
@@ -95,6 +96,7 @@ export function Navbar() {
|
||||
{isLoggedIn && !isSmallScreen ? (
|
||||
<div className="flex flex-1 items-center justify-end gap-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<FeedbackButton />
|
||||
<AgentActivityDropdown />
|
||||
{profile && <Wallet key={profile.username} />}
|
||||
<AccountMenu
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
"use client";
|
||||
|
||||
import { useTallyPopup } from "@/components/molecules/TallyPoup/useTallyPopup";
|
||||
import { ChatCircleDotsIcon } from "@phosphor-icons/react";
|
||||
|
||||
export function FeedbackButton() {
|
||||
const { state } = useTallyPopup();
|
||||
|
||||
if (state.isFormVisible) return null;
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="group inline-flex overflow-hidden rounded-full active:scale-[0.97] disabled:pointer-events-none disabled:opacity-50"
|
||||
data-tally-open="3yx2L0"
|
||||
data-tally-emoji-text="👋"
|
||||
data-tally-emoji-animation="wave"
|
||||
data-sentry-replay-id={state.sentryReplayId || "not-initialized"}
|
||||
data-sentry-replay-url={state.replayUrl || "not-initialized"}
|
||||
data-page-url={
|
||||
state.pageUrl ? state.pageUrl.split("?")[0] : "not-initialized"
|
||||
}
|
||||
data-is-authenticated={
|
||||
state.isAuthenticated === null
|
||||
? "unknown"
|
||||
: String(state.isAuthenticated)
|
||||
}
|
||||
>
|
||||
<div className="rounded-full bg-gradient-to-r from-indigo-100 to-indigo-300 to-zinc-400 p-[1px]">
|
||||
<div className="flex items-center gap-1.5 rounded-full bg-[#FAFAFA]/80 px-3 py-1.5 text-sm font-medium text-neutral-700 backdrop-blur-xl transition-colors duration-150 ease-out group-hover:bg-zinc-100/90">
|
||||
Give Feedback
|
||||
<ChatCircleDotsIcon size={16} />
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { IconLaptop } from "@/components/__legacy__/ui/icons";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag";
|
||||
import { ListChecksIcon } from "@phosphor-icons/react/dist/ssr";
|
||||
import { Laptop, ListChecksIcon } from "@phosphor-icons/react/dist/ssr";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Text } from "../../../atoms/Text/Text";
|
||||
@@ -13,7 +12,8 @@ import {
|
||||
MarketplaceIcon,
|
||||
} from "./MenuIcon/MenuIcon";
|
||||
|
||||
const iconWidthClass = "h-5 w-5";
|
||||
const iconBaseClass = "h-4 w-4 shrink-0";
|
||||
const iconNudgedClass = "relative bottom-[2px] h-4 w-4 shrink-0";
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
@@ -34,15 +34,15 @@ export function NavbarLink({ name, href }: Props) {
|
||||
<Link href={href} data-testid={`navbar-link-${name.toLowerCase()}`}>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-start gap-1 p-1 md:p-2",
|
||||
"flex items-center justify-start gap-2.5 p-1 md:p-2",
|
||||
isActive &&
|
||||
"rounded-small bg-neutral-800 py-1 pl-1 pr-1.5 transition-all duration-300 dark:bg-neutral-200 md:py-2 md:pl-2 md:pr-3",
|
||||
"rounded-small bg-neutral-800 py-1 pl-1 pr-1.5 transition-all duration-300 md:py-[0.7rem] md:pl-2 md:pr-3",
|
||||
)}
|
||||
>
|
||||
{href === "/marketplace" && (
|
||||
<div
|
||||
className={cn(
|
||||
iconWidthClass,
|
||||
iconNudgedClass,
|
||||
isActive && "text-white dark:text-black",
|
||||
)}
|
||||
>
|
||||
@@ -52,7 +52,7 @@ export function NavbarLink({ name, href }: Props) {
|
||||
{href === "/build" && (
|
||||
<div
|
||||
className={cn(
|
||||
iconWidthClass,
|
||||
iconNudgedClass,
|
||||
isActive && "text-white dark:text-black",
|
||||
)}
|
||||
>
|
||||
@@ -60,9 +60,9 @@ export function NavbarLink({ name, href }: Props) {
|
||||
</div>
|
||||
)}
|
||||
{href === "/monitor" && (
|
||||
<IconLaptop
|
||||
<Laptop
|
||||
className={cn(
|
||||
iconWidthClass,
|
||||
iconBaseClass,
|
||||
isActive && "text-white dark:text-black",
|
||||
)}
|
||||
/>
|
||||
@@ -70,7 +70,7 @@ export function NavbarLink({ name, href }: Props) {
|
||||
{href === "/copilot" && (
|
||||
<div
|
||||
className={cn(
|
||||
iconWidthClass,
|
||||
iconNudgedClass,
|
||||
isActive && "text-white dark:text-black",
|
||||
)}
|
||||
>
|
||||
@@ -81,14 +81,14 @@ export function NavbarLink({ name, href }: Props) {
|
||||
(isChatEnabled ? (
|
||||
<ListChecksIcon
|
||||
className={cn(
|
||||
iconWidthClass,
|
||||
"h-5 w-5 shrink-0",
|
||||
isActive && "text-white dark:text-black",
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className={cn(
|
||||
iconWidthClass,
|
||||
iconNudgedClass,
|
||||
isActive && "text-white dark:text-black",
|
||||
)}
|
||||
>
|
||||
@@ -98,7 +98,7 @@ export function NavbarLink({ name, href }: Props) {
|
||||
<Text
|
||||
variant="h5"
|
||||
className={cn(
|
||||
"hidden !font-poppins lg:block",
|
||||
"hidden !font-poppins leading-none lg:block",
|
||||
isActive ? "!text-white" : "!text-black",
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -19,12 +19,12 @@ export function Breadcrumbs({ items }: Props) {
|
||||
{item.link ? (
|
||||
<Link
|
||||
href={item.link}
|
||||
className="text-[0.75rem] font-[400] text-zinc-600 transition-colors hover:text-zinc-900 hover:no-underline"
|
||||
className="text-sm font-[400] text-zinc-600 transition-colors hover:text-zinc-900 hover:no-underline"
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="text-[0.75rem] font-[400] text-zinc-900">
|
||||
<span className="text-sm font-[400] text-zinc-900">
|
||||
{item.name}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -1,39 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { useTallyPopup } from "./useTallyPopup";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
|
||||
export function TallyPopupSimple() {
|
||||
const { state } = useTallyPopup();
|
||||
|
||||
if (state.isFormVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-1 right-0 z-20 hidden select-none items-center gap-4 p-3 transition-all duration-300 ease-in-out md:flex">
|
||||
<Button
|
||||
variant="primary"
|
||||
data-tally-open="3yx2L0"
|
||||
data-tally-emoji-text="👋"
|
||||
data-tally-emoji-animation="wave"
|
||||
data-sentry-replay-id={state.sentryReplayId || "not-initialized"}
|
||||
data-sentry-replay-url={state.replayUrl || "not-initialized"}
|
||||
data-user-agent={state.userAgent}
|
||||
data-page-url={state.pageUrl}
|
||||
data-is-authenticated={
|
||||
state.isAuthenticated === null
|
||||
? "unknown"
|
||||
: String(state.isAuthenticated)
|
||||
}
|
||||
data-email={state.userEmail || "not-authenticated"}
|
||||
className="mb-0 h-14 rounded-2xl bg-[rgba(65,65,64,1)] text-center font-sans text-lg font-medium leading-6"
|
||||
>
|
||||
Give Feedback
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
// Load the Tally script and set up event listeners
|
||||
useTallyPopup();
|
||||
return null;
|
||||
}
|
||||
|
||||
export default TallyPopupSimple;
|
||||
|
||||
@@ -80,7 +80,7 @@ export class MarketplacePage extends BasePage {
|
||||
|
||||
async getTopAgentsSection(page: Page) {
|
||||
const { getText } = getSelectors(page);
|
||||
return getText("Top Agents");
|
||||
return getText("All Agents");
|
||||
}
|
||||
|
||||
async getFeaturedCreatorsSection(page: Page) {
|
||||
|
||||
Reference in New Issue
Block a user