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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 8 additions & 13 deletions app/hub/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,6 @@ export default function HubPage() {
debouncedSearchQuery,
]);

const topFeatured = featuredWorkflows[0];
const carouselWorkflows = featuredWorkflows.slice(1);

useEffect(() => {
const fetchWorkflows = async (): Promise<void> => {
try {
Expand Down Expand Up @@ -161,25 +158,23 @@ export default function HubPage() {
className="pointer-events-auto fixed inset-0 overflow-y-auto bg-sidebar [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
ref={scrollContainerRef}
>
{/* Fixed dot pattern behind */}
<div className="pointer-events-none fixed inset-0 [background-image:radial-gradient(rgb(148_163_184_/_0.15)_1px,transparent_1px)] [background-size:24px_24px]" />
{/* Fixed gradient overlay - fades as you scroll */}
<div
className="pointer-events-none fixed inset-x-0 top-0 h-[80vh] bg-gradient-to-b from-60% from-sidebar to-transparent"
ref={gradientRef}
/>
<div className="container relative mx-auto px-4 py-4 pt-28 pb-12">
{/* start custom KeeperHub code */}
{isLoading ? (
<p className="text-muted-foreground">Loading workflows...</p>
) : (
<>
<HubHero topWorkflow={topFeatured} />
<HubHero />

<FeaturedCarousel workflows={carouselWorkflows} />
<div className="relative right-1/2 left-1/2 -mr-[50vw] -ml-[50vw] w-screen bg-sidebar">
<div className="bg-white/[0.03] py-12">
<div className="container mx-auto px-4">
<FeaturedCarousel workflows={featuredWorkflows} />
</div>
</div>
</div>

<div className="relative right-1/2 left-1/2 -mr-[50vw] -ml-[50vw] w-screen">
<hr className="border-border" />
<div className="bg-sidebar px-4 pt-8 pb-12">
<div className="container mx-auto">
<h2 className="mb-8 font-bold text-2xl">
Expand Down
60 changes: 44 additions & 16 deletions keeperhub/components/hub/featured-carousel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,26 @@

import { ChevronLeft, ChevronRight, Eye } from "lucide-react";
import { useRouter } from "next/navigation";
import { useCallback, useRef, useState } from "react";
import { useCallback, useMemo, useRef, useState } from "react";
import { toast } from "sonner";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { api, type SavedWorkflow } from "@/lib/api-client";
import { authClient, useSession } from "@/lib/auth-client";
import { WorkflowMiniMap } from "./workflow-mini-map";
import { WorkflowNodeIcons } from "./workflow-node-icons";

type FeaturedCarouselProps = {
workflows: SavedWorkflow[];
Expand All @@ -28,6 +33,23 @@ export function FeaturedCarousel({ workflows }: FeaturedCarouselProps) {
const [duplicatingIds, setDuplicatingIds] = useState<Set<string>>(new Set());
const scrollRef = useRef<HTMLDivElement>(null);

const arrowVisibility = useMemo((): string => {
const count = workflows.length;
if (count > 4) {
return "flex";
}
if (count > 3) {
return "flex lg:hidden";
}
if (count > 2) {
return "flex md:hidden";
}
if (count > 1) {
return "flex sm:hidden";
}
return "hidden";
}, [workflows.length]);

const scroll = useCallback((direction: "left" | "right") => {
const container = scrollRef.current;
if (!container) {
Expand Down Expand Up @@ -77,10 +99,10 @@ export function FeaturedCarousel({ workflows }: FeaturedCarouselProps) {
}

return (
<section className="mb-16">
<section className="relative z-10">
<div className="mb-6 flex items-center justify-between">
<h2 className="font-bold text-3xl">Featured</h2>
<div className="flex gap-2">
<div className={`gap-2 ${arrowVisibility}`}>
<Button
aria-label="Scroll left"
onClick={() => scroll("left")}
Expand Down Expand Up @@ -109,10 +131,10 @@ export function FeaturedCarousel({ workflows }: FeaturedCarouselProps) {

return (
<Card
className="flex w-[320px] shrink-0 flex-col overflow-hidden bg-sidebar"
className="flex w-[320px] shrink-0 flex-col gap-0 overflow-hidden border-none bg-sidebar py-0"
key={workflow.id}
>
<div className="relative -mt-6 flex aspect-video w-full items-center justify-center overflow-hidden">
<div className="relative flex aspect-video w-full items-center justify-center overflow-hidden px-8">
<WorkflowMiniMap
edges={workflow.edges}
height={160}
Expand All @@ -121,14 +143,14 @@ export function FeaturedCarousel({ workflows }: FeaturedCarouselProps) {
/>
{workflow.category && (
<Badge
className="absolute top-3 right-3 rounded-full bg-sidebar"
className="absolute top-3 right-3 rounded-full border-none bg-[#09fd671a] px-3 py-1 text-[#09fd67]"
variant="outline"
>
{workflow.category}
</Badge>
)}
</div>
<CardHeader>
<CardHeader className="pb-4">
{workflow.protocol && (
<p className="font-medium text-muted-foreground text-xs uppercase tracking-wider">
{workflow.protocol}
Expand All @@ -140,9 +162,10 @@ export function FeaturedCarousel({ workflows }: FeaturedCarouselProps) {
{workflow.description}
</CardDescription>
)}
<WorkflowNodeIcons nodes={workflow.nodes} />
</CardHeader>
<CardContent className="flex-1" />
<CardFooter className="gap-2">
<div className="flex-1" />
<CardFooter className="gap-2 pb-4">
<Button
className="flex-1"
disabled={isDuplicating}
Expand All @@ -151,12 +174,17 @@ export function FeaturedCarousel({ workflows }: FeaturedCarouselProps) {
>
{isDuplicating ? "Duplicating..." : "Use Template"}
</Button>
<Button
onClick={() => router.push(`/workflows/${workflow.id}`)}
variant="outline"
>
<Eye className="size-4" />
</Button>
<Tooltip>
<TooltipTrigger asChild>
<Button
onClick={() => router.push(`/workflows/${workflow.id}`)}
variant="outline"
>
<Eye className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="top">View Template</TooltipContent>
</Tooltip>
</CardFooter>
</Card>
);
Expand Down
126 changes: 18 additions & 108 deletions keeperhub/components/hub/hub-hero.tsx
Original file line number Diff line number Diff line change
@@ -1,120 +1,30 @@
"use client";

import { Eye } from "lucide-react";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { toast } from "sonner";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { api, type SavedWorkflow } from "@/lib/api-client";
import { authClient, useSession } from "@/lib/auth-client";
import { WorkflowMiniMap } from "./workflow-mini-map";

type HubHeroProps = {
topWorkflow: SavedWorkflow | undefined;
};

export function HubHero({ topWorkflow }: HubHeroProps) {
const router = useRouter();
const { data: session } = useSession();
const [isDuplicating, setIsDuplicating] = useState(false);

const handleDuplicate = async (workflowId: string): Promise<void> => {
if (isDuplicating) {
return;
}

setIsDuplicating(true);

try {
if (!session?.user) {
await authClient.signIn.anonymous();
await new Promise((resolve) => setTimeout(resolve, 100));
}

const duplicated = await api.workflow.duplicate(workflowId);
toast.success("Workflow duplicated successfully");
router.push(`/workflows/${duplicated.id}`);
} catch (error) {
console.error("Failed to duplicate workflow:", error);
toast.error(
error instanceof Error ? error.message : "Failed to duplicate workflow"
);
} finally {
setIsDuplicating(false);
}
};
import Image from "next/image";

export function HubHero() {
return (
<div className="mb-16 grid items-center gap-12 pt-8 lg:grid-cols-2">
<div>
<div className="relative grid items-center gap-12 overflow-visible pt-8 lg:grid-cols-2">
<div className="relative z-10">
<h1 className="mb-4 font-bold text-5xl tracking-tight">
KeeperHub Web3 Automation
KeeperHub Web3 Templates
</h1>
<p className="max-w-lg text-lg text-muted-foreground">
<p className="mb-12 max-w-lg text-muted-foreground text-xl">
Browse ready-made workflow templates and community automations.
Duplicate any template to get started in seconds.
</p>
</div>

{topWorkflow && (
<Card className="flex flex-col overflow-hidden bg-sidebar">
<div className="relative -mt-6 flex w-full items-center justify-center overflow-hidden [aspect-ratio:32/9]">
<WorkflowMiniMap
edges={topWorkflow.edges}
height={120}
nodes={topWorkflow.nodes}
width={480}
/>
{topWorkflow.category && (
<Badge
className="absolute top-3 right-3 rounded-full bg-sidebar"
variant="outline"
>
{topWorkflow.category}
</Badge>
)}
</div>
<CardHeader>
{topWorkflow.protocol && (
<p className="font-medium text-muted-foreground text-xs uppercase tracking-wider">
{topWorkflow.protocol}
</p>
)}
<CardTitle className="line-clamp-2">{topWorkflow.name}</CardTitle>
{topWorkflow.description && (
<CardDescription className="line-clamp-2">
{topWorkflow.description}
</CardDescription>
)}
</CardHeader>
<CardContent className="flex-1" />
<CardFooter className="gap-2">
<Button
className="flex-1"
disabled={isDuplicating}
onClick={() => handleDuplicate(topWorkflow.id)}
variant="default"
>
{isDuplicating ? "Duplicating..." : "Use Template"}
</Button>
<Button
onClick={() => router.push(`/workflows/${topWorkflow.id}`)}
variant="outline"
>
<Eye className="size-4" />
</Button>
</CardFooter>
</Card>
)}
<div className="relative hidden lg:block">
<div className="absolute top-1/2 left-1/2 -z-10 h-[150%] w-[150%] -translate-x-1/2 -translate-y-1/2 rounded-full bg-[radial-gradient(circle,#243548_0%,transparent_70%)]" />
<Image
alt=""
height={400}
priority
src="/hub-graphic.png"
width={700}
/>
</div>

<div className="pointer-events-none absolute inset-x-0 bottom-0 hidden h-2/3 bg-[linear-gradient(to_top,oklch(0.2101_0.0318_264.66)_0%,oklch(0.2101_0.0318_264.66)_10%,transparent_100%)] lg:block" />
</div>
);
}
Loading