Paper2Code / app /page.tsx
AUXteam's picture
Upload folder using huggingface_hub
e6b89d5 verified
"use client";
import Link from "next/link";
import Image from "next/image";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { appConfig } from '@/config/app.config';
import { toast } from "sonner";
// Import shared components
import { Connector } from "@/components/shared/layout/curvy-rect";
import HeroFlame from "@/components/shared/effects/flame/hero-flame";
import AsciiExplosion from "@/components/shared/effects/flame/ascii-explosion";
import { HeaderProvider } from "@/components/shared/header/HeaderContext";
// Import hero section components
import HomeHeroBackground from "@/components/app/(home)/sections/hero/Background/Background";
import { BackgroundOuterPiece } from "@/components/app/(home)/sections/hero/Background/BackgroundOuterPiece";
import HomeHeroBadge from "@/components/app/(home)/sections/hero/Badge/Badge";
import HomeHeroPixi from "@/components/app/(home)/sections/hero/Pixi/Pixi";
import HomeHeroTitle from "@/components/app/(home)/sections/hero/Title/Title";
import HeroInputSubmitButton from "@/components/app/(home)/sections/hero-input/Button/Button";
// import Globe from "@/components/app/(home)/sections/hero-input/_svg/Globe";
// Import header components
import HeaderBrandKit from "@/components/shared/header/BrandKit/BrandKit";
import HeaderWrapper from "@/components/shared/header/Wrapper/Wrapper";
import HeaderDropdownWrapper from "@/components/shared/header/Dropdown/Wrapper/Wrapper";
import GithubIcon from "@/components/shared/header/Github/_svg/GithubIcon";
import ButtonUI from "@/components/ui/shadcn/button"
interface SearchResult {
url: string;
title: string;
description: string;
screenshot: string | null;
markdown: string;
}
export default function HomePage() {
const [mode, setMode] = useState<'website' | 'paper'>('website');
const [url, setUrl] = useState<string>("");
const [selectedStyle, setSelectedStyle] = useState<string>("1");
const [selectedModel, setSelectedModel] = useState<string>(appConfig.ai.defaultModel);
const [isValidUrl, setIsValidUrl] = useState<boolean>(false);
const [showSearchTiles, setShowSearchTiles] = useState<boolean>(false);
const [searchResults, setSearchResults] = useState<SearchResult[]>([]);
const [isSearching, setIsSearching] = useState<boolean>(false);
const [hasSearched, setHasSearched] = useState<boolean>(false);
const [isFadingOut, setIsFadingOut] = useState<boolean>(false);
const [showSelectMessage, setShowSelectMessage] = useState<boolean>(false);
const [showInstructionsForIndex, setShowInstructionsForIndex] = useState<number | null>(null);
const [additionalInstructions, setAdditionalInstructions] = useState<string>('');
const [extendBrandStyles, setExtendBrandStyles] = useState<boolean>(false);
const router = useRouter();
// Simple URL validation
const validateUrl = (urlString: string) => {
if (!urlString) return false;
// Basic URL pattern - accepts domains with or without protocol
const urlPattern = /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/;
return urlPattern.test(urlString.toLowerCase());
};
// Check if input is a URL (contains a dot)
const isURL = (str: string): boolean => {
const urlPattern = /^(https?:\/\/)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(\/.*)?$/;
return urlPattern.test(str.trim());
};
const styles = [
{ id: "1", name: "Glassmorphism", description: "Frosted glass effect" },
{ id: "2", name: "Neumorphism", description: "Soft 3D shadows" },
{ id: "3", name: "Brutalism", description: "Bold and raw" },
{ id: "4", name: "Minimalist", description: "Clean and simple" },
{ id: "5", name: "Dark Mode", description: "Dark theme design" },
{ id: "6", name: "Gradient Rich", description: "Vibrant gradients" },
{ id: "7", name: "3D Depth", description: "Dimensional layers" },
{ id: "8", name: "Retro Wave", description: "80s inspired" },
];
const models = appConfig.ai.availableModels.map(model => ({
id: model,
name: appConfig.ai.modelDisplayNames[model] || model,
}));
const handleSubmit = async (selectedResult?: SearchResult) => {
const inputValue = url.trim();
if (!inputValue) {
toast.error(mode === 'website' ? "Please enter a URL or search term" : "Please enter an Arxiv link or paper query");
return;
}
if (mode === 'paper') {
toast.info("Transforming paper to code...");
try {
const response = await fetch('/api/submit-job', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
arxivUrl: inputValue,
repo: 'AUXteam/Paper2Code-Jobs', // Default repo for paper jobs
paperName: inputValue.split('/').pop() || 'paper'
})
});
const data = await response.json();
if (data.success) {
toast.success("Job submitted! Code will be pushed to GitHub.");
if (data.githubUrl) {
window.open(data.githubUrl, '_blank');
}
} else {
toast.error(data.error || "Failed to submit job");
}
} catch (error) {
toast.error("An error occurred while submitting the paper");
}
return;
}
// Validate brand extension mode requirements
if (extendBrandStyles && isURL(inputValue) && !additionalInstructions.trim()) {
toast.error("Please describe what you want to build with this brand's styles");
return;
}
// If it's a search result being selected, fade out and redirect
if (selectedResult) {
setIsFadingOut(true);
// Wait for fade animation
setTimeout(() => {
sessionStorage.setItem('targetUrl', selectedResult.url);
sessionStorage.setItem('selectedStyle', selectedStyle);
sessionStorage.setItem('selectedModel', selectedModel);
sessionStorage.setItem('autoStart', 'true');
if (selectedResult.markdown) {
sessionStorage.setItem('siteMarkdown', selectedResult.markdown);
}
router.push('/generation');
}, 500);
return;
}
// If it's a URL, check if we're extending brand styles or cloning
if (isURL(inputValue)) {
if (extendBrandStyles) {
// Brand extension mode - extract brand styles and use them with the prompt
sessionStorage.setItem('targetUrl', inputValue);
sessionStorage.setItem('selectedModel', selectedModel);
sessionStorage.setItem('autoStart', 'true');
sessionStorage.setItem('brandExtensionMode', 'true');
sessionStorage.setItem('brandExtensionPrompt', additionalInstructions || '');
router.push('/generation');
} else {
// Normal clone mode
sessionStorage.setItem('targetUrl', inputValue);
sessionStorage.setItem('selectedStyle', selectedStyle);
sessionStorage.setItem('selectedModel', selectedModel);
sessionStorage.setItem('autoStart', 'true');
router.push('/generation');
}
} else {
// It's a search term, fade out if results exist, then search
if (hasSearched && searchResults.length > 0) {
setIsFadingOut(true);
setTimeout(async () => {
setSearchResults([]);
setIsFadingOut(false);
setShowSelectMessage(true);
// Perform new search
await performSearch(inputValue);
setHasSearched(true);
setShowSearchTiles(true);
setShowSelectMessage(false);
// Smooth scroll to carousel
setTimeout(() => {
const carouselSection = document.querySelector('.carousel-section');
if (carouselSection) {
carouselSection.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, 300);
}, 500);
} else {
// First search, no fade needed
setShowSelectMessage(true);
setIsSearching(true);
setHasSearched(true);
setShowSearchTiles(true);
// Scroll to carousel area immediately
setTimeout(() => {
const carouselSection = document.querySelector('.carousel-section');
if (carouselSection) {
carouselSection.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, 100);
await performSearch(inputValue);
setShowSelectMessage(false);
setIsSearching(false);
// Smooth scroll to carousel
setTimeout(() => {
const carouselSection = document.querySelector('.carousel-section');
if (carouselSection) {
carouselSection.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, 300);
}
}
};
// Perform search when user types
const performSearch = async (searchQuery: string) => {
if (!searchQuery.trim() || isURL(searchQuery)) {
setSearchResults([]);
setShowSearchTiles(false);
return;
}
setIsSearching(true);
setShowSearchTiles(true);
try {
const response = await fetch('/api/search', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: searchQuery }),
});
if (response.ok) {
const data = await response.json();
setSearchResults(data.results || []);
setShowSearchTiles(true);
}
} catch (error) {
console.error('Search error:', error);
} finally {
setIsSearching(false);
}
};
return (
<HeaderProvider>
<div className="min-h-screen bg-background-base">
{/* Header/Navigation Section */}
<HeaderDropdownWrapper />
<div className="sticky top-0 left-0 w-full z-[101] bg-background-base header">
<div className="absolute top-0 cmw-container border-x border-border-faint h-full pointer-events-none" />
<div className="h-1 bg-border-faint w-full left-0 -bottom-1 absolute" />
<div className="cmw-container absolute h-full pointer-events-none top-0">
<Connector className="absolute -left-[10.5px] -bottom-11" />
<Connector className="absolute -right-[10.5px] -bottom-11" />
</div>
<HeaderWrapper>
<div className="max-w-[900px] mx-auto w-full flex justify-between items-center">
<div className="flex gap-24 items-center">
<HeaderBrandKit />
</div>
<div className="flex gap-8">
<a
className="contents"
href="https://github.com/mendableai/open-lovable"
target="_blank"
>
<ButtonUI variant="tertiary">
<GithubIcon />
Use this Template
</ButtonUI>
</a>
</div>
</div>
</HeaderWrapper>
</div>
{/* Hero Section */}
<section className="overflow-x-clip" id="home-hero">
<div className="pt-28 lg:pt-254 lg:-mt-100 pb-115 relative" id="hero-content">
<HomeHeroPixi />
<HeroFlame />
<BackgroundOuterPiece />
<HomeHeroBackground />
<div className="relative container px-16">
<div className="flex justify-center mb-12">
<div className="bg-black-alpha-4 p-4 rounded-12 flex gap-4">
<button
onClick={() => setMode('website')}
className={`px-16 py-8 rounded-8 text-label-medium transition-all ${mode === 'website' ? 'bg-white shadow-sm text-accent-black' : 'text-black-alpha-48 hover:text-black-alpha-72'}`}
>
Website2Code
</button>
<button
onClick={() => setMode('paper')}
className={`px-16 py-8 rounded-8 text-label-medium transition-all ${mode === 'paper' ? 'bg-white shadow-sm text-accent-black' : 'text-black-alpha-48 hover:text-black-alpha-72'}`}
>
Paper2Code
</button>
</div>
</div>
<HomeHeroBadge />
<HomeHeroTitle />
<p className="text-center text-body-large">
{mode === 'website'
? "Clone brand format or re-imagine any website, in seconds."
: "Transform research papers into functional Python or React code."}
</p>
<Link
className="bg-black-alpha-4 hover:bg-black-alpha-6 rounded-6 px-8 lg:px-6 text-label-large h-30 lg:h-24 block mt-8 mx-auto w-max gap-4 transition-all"
href="#"
onClick={(e) => e.preventDefault()}
>
Powered by Firecrawl.
</Link>
</div>
</div>
{/* Mini Playground Input */}
<div className="container lg:contents !p-16 relative -mt-90">
<div className="absolute top-0 left-[calc(50%-50vw)] w-screen h-1 bg-border-faint lg:hidden" />
<div className="absolute bottom-0 left-[calc(50%-50vw)] w-screen h-1 bg-border-faint lg:hidden" />
<Connector className="-top-10 -left-[10.5px] lg:hidden" />
<Connector className="-top-10 -right-[10.5px] lg:hidden" />
<Connector className="-bottom-10 -left-[10.5px] lg:hidden" />
<Connector className="-bottom-10 -right-[10.5px] lg:hidden" />
{/* Hero Input Component */}
<div className="max-w-552 mx-auto z-[11] lg:z-[2]">
<div className="rounded-20 -mt-30 lg:-mt-30">
<div
className="bg-white rounded-20 relative z-10"
style={{
boxShadow:
"0px 0px 44px 0px rgba(0, 0, 0, 0.02), 0px 88px 56px -20px rgba(0, 0, 0, 0.03), 0px 56px 56px -20px rgba(0, 0, 0, 0.02), 0px 32px 32px -20px rgba(0, 0, 0, 0.03), 0px 16px 24px -12px rgba(0, 0, 0, 0.03), 0px 0px 0px 1px rgba(0, 0, 0, 0.05), 0px 0px 0px 10px #F9F9F9",
}}
>
<div className="p-[28px] flex gap-12 items-center w-full relative bg-white rounded-20">
{/* Show different UI when search results are displayed */}
{hasSearched && searchResults.length > 0 && !isFadingOut ? (
<>
{/* Selection mode icon */}
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="opacity-40 flex-shrink-0"
>
<rect x="2" y="4" width="7" height="5" rx="1" stroke="currentColor" strokeWidth="1.5"/>
<rect x="11" y="4" width="7" height="5" rx="1" stroke="currentColor" strokeWidth="1.5"/>
<rect x="2" y="11" width="7" height="5" rx="1" stroke="currentColor" strokeWidth="1.5"/>
<rect x="11" y="11" width="7" height="5" rx="1" stroke="currentColor" strokeWidth="1.5"/>
</svg>
{/* Selection message */}
<div className="flex-1 text-body-input text-accent-black">
Select which site to clone from the results below
</div>
{/* Search again button */}
<button
onClick={(e) => {
e.preventDefault();
setIsFadingOut(true);
setTimeout(() => {
setSearchResults([]);
setHasSearched(false);
setShowSearchTiles(false);
setIsFadingOut(false);
setUrl('');
}, 500);
}}
className="button relative rounded-10 px-12 py-8 text-label-medium font-medium flex items-center justify-center gap-6 bg-gray-100 hover:bg-gray-200 text-gray-700 active:scale-[0.995] transition-all"
>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="opacity-60"
>
<path d="M14 14L10 10M11 6.5C11 9 9 11 6.5 11C4 11 2 9 2 6.5C2 4 4 2 6.5 2C9 2 11 4 11 6.5Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
</svg>
<span>Search Again</span>
</button>
</>
) : (
<>
{isURL(url) ? (
// Scrape icon for URLs
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="opacity-40 flex-shrink-0"
>
<rect x="3" y="3" width="14" height="14" rx="2" stroke="currentColor" strokeWidth="1.5"/>
<path d="M7 10L9 12L13 8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
) : (
// Search icon for search terms
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="opacity-40 flex-shrink-0"
>
<circle cx="8.5" cy="8.5" r="5.5" stroke="currentColor" strokeWidth="1.5"/>
<path d="M12.5 12.5L16.5 16.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
</svg>
)}
<input
className="flex-1 bg-transparent text-body-input text-accent-black placeholder:text-black-alpha-48 focus:outline-none focus:ring-0 focus:border-transparent"
placeholder={mode === 'website' ? "Enter URL or search term..." : "Enter Arxiv link or reference..."}
type="text"
value={url}
disabled={isSearching}
onChange={(e) => {
const value = e.target.value;
setUrl(value);
setIsValidUrl(validateUrl(value));
// Reset search state when input changes
if (value.trim() === "") {
setShowSearchTiles(false);
setHasSearched(false);
setSearchResults([]);
}
}}
onKeyDown={(e) => {
if (e.key === "Enter" && !isSearching) {
e.preventDefault();
handleSubmit();
}
}}
onFocus={() => {
if (url.trim() && !isURL(url) && searchResults.length > 0) {
setShowSearchTiles(true);
}
}}
/>
<div
onClick={(e) => {
e.preventDefault();
if (!isSearching) {
handleSubmit();
}
}}
className={isSearching ? 'pointer-events-none' : ''}
>
<HeroInputSubmitButton
dirty={url.length > 0}
buttonText={mode === 'website' ? (isURL(url) ? 'Scrape Site' : 'Search') : 'Submit Paper'}
disabled={isSearching}
/>
</div>
</>
)}
</div>
{/* Options Section - Only show when valid URL and in website mode */}
<div className={`overflow-hidden transition-all duration-500 ease-in-out ${
mode === 'website' && isValidUrl ? (extendBrandStyles ? 'max-h-[400px]' : 'max-h-[300px]') + ' opacity-100' : 'max-h-0 opacity-0'
}`}>
<div className="px-[28px] pt-0 pb-[28px]">
<div className="border-t border-gray-100 bg-white">
{/* Extend Brand Styles Toggle */}
<div className={`transition-all duration-300 transform ${
isValidUrl ? 'translate-y-0 opacity-100' : '-translate-y-2 opacity-0'
}`} style={{ transitionDelay: '50ms' }}>
<div className="py-8 grid grid-cols-2 items-center gap-12 group cursor-pointer" onClick={() => setExtendBrandStyles(!extendBrandStyles)}>
<div className="flex select-none">
<div className="flex lg-max:flex-col whitespace-nowrap flex-wrap min-w-0 gap-8 lg:justify-between flex-1">
<div className="text-xs font-medium text-black-alpha-72 transition-all group-hover:text-accent-black relative">
Extend brand styles
</div>
</div>
</div>
<div className="flex justify-end">
<button
className="transition-all relative rounded-full group bg-black-alpha-10"
type="button"
onClick={(e) => {
e.stopPropagation();
setExtendBrandStyles(!extendBrandStyles);
}}
style={{
width: '50px',
height: '20px',
boxShadow: 'rgba(0, 0, 0, 0.02) 0px 6px 12px 0px inset, rgba(0, 0, 0, 0.02) 0px 0.75px 0.75px 0px inset, rgba(0, 0, 0, 0.04) 0px 0.25px 0.25px 0px inset'
}}
>
<div
className={`overlay transition-opacity ${extendBrandStyles ? 'opacity-100' : 'opacity-0'}`}
style={{ background: 'color(display-p3 0.9059 0.3294 0.0784)', backgroundColor: '#FA4500' }}
/>
<div
className="top-[2px] left-[2px] transition-all absolute rounded-full bg-accent-white"
style={{
width: '28px',
height: '16px',
boxShadow: 'rgba(0, 0, 0, 0.06) 0px 6px 12px -3px, rgba(0, 0, 0, 0.06) 0px 3px 6px -1px, rgba(0, 0, 0, 0.04) 0px 1px 2px 0px, rgba(0, 0, 0, 0.08) 0px 0.5px 0.5px 0px',
transform: extendBrandStyles ? 'translateX(16px)' : 'none'
}}
/>
</button>
</div>
</div>
</div>
{/* Brand Extension Prompt - Show when toggle is enabled */}
{extendBrandStyles && (
<div className="pb-10 transition-all duration-300 opacity-100">
<textarea
value={additionalInstructions}
onChange={(e) => setAdditionalInstructions(e.target.value)}
placeholder="Describe the new functionality you want to build using this brand's styles..."
className="w-full px-4 py-10 text-xs font-medium text-gray-700 bg-gray-50 rounded border border-gray-200 focus:border-orange-500 focus:outline-none focus:ring-1 focus:ring-orange-500 placeholder:text-gray-400 min-h-[80px] resize-none"
/>
</div>
)}
{/* Style Selector - Hide when brand extension mode is enabled */}
{!extendBrandStyles && (
<div className={`mb-2 transition-all duration-300 transform ${
isValidUrl ? 'translate-y-0 opacity-100' : '-translate-y-2 opacity-0'
}`} style={{ transitionDelay: '100ms' }}>
<div className="grid grid-cols-4 gap-2">
{styles.map((style, index) => (
<button
key={style.id}
onClick={() => setSelectedStyle(style.id)}
className={`
${selectedStyle === style.id
? 'bg-heat-100 hover:bg-heat-200 flex items-center justify-center button relative text-label-medium button-primary group/button rounded-10 p-8 text-accent-white active:scale-[0.995] border-0'
: 'border-gray-200 hover:border-gray-300 bg-white text-gray-700 py-3.5 px-4 rounded text-xs font-medium border text-center'
}
transition-all
${isValidUrl ? 'opacity-100' : 'opacity-0'}
`}
style={{
transitionDelay: `${150 + index * 30}ms`,
transition: 'all 0.3s ease-in-out'
}}
>
{selectedStyle === style.id && (
<div className="button-background absolute inset-0 rounded-10 pointer-events-none" />
)}
<span className={selectedStyle === style.id ? 'relative' : ''}>
{style.name}
</span>
</button>
))}
</div>
</div>
)}
{/* Model Selector Dropdown and Additional Instructions */}
<div className={`flex items-center gap-3 mt-2 pb-4 transition-all duration-300 transform ${
isValidUrl ? 'translate-y-0 opacity-100' : '-translate-y-2 opacity-0'
}`} style={{ transitionDelay: '400ms' }}>
{/* Model Dropdown */}
<select
value={selectedModel}
onChange={(e) => setSelectedModel(e.target.value)}
className={`px-3 py-2.5 text-xs font-medium text-gray-700 bg-white rounded border border-gray-200 focus:border-orange-500 focus:outline-none focus:ring-1 focus:ring-orange-500 ${extendBrandStyles ? 'flex-1' : ''}`}
>
{models.map((model) => (
<option key={model.id} value={model.id}>
{model.name}
</option>
))}
</select>
{/* Additional Instructions - Hidden when extend brand styles is enabled */}
{!extendBrandStyles && (
<input
type="text"
className="flex-1 px-3 py-2.5 text-xs font-medium text-gray-700 bg-gray-50 rounded border border-gray-200 focus:border-orange-500 focus:outline-none focus:ring-1 focus:ring-orange-500 placeholder:text-gray-400"
placeholder="Additional instructions (optional)"
onChange={(e) => sessionStorage.setItem('additionalInstructions', e.target.value)}
/>
)}
</div>
</div>
</div>
</div>
</div>
<div className="h-248 top-84 cw-768 pointer-events-none absolute overflow-clip -z-10">
<AsciiExplosion className="-top-200" />
</div>
</div>
</div>
</div>
</section>
{/* Full-width oval carousel section */}
{showSearchTiles && hasSearched && (
<section className={`carousel-section relative w-full overflow-hidden mt-32 mb-32 transition-opacity duration-500 ${
isFadingOut ? 'opacity-0' : 'opacity-100'
}`}>
<div className="absolute inset-0 bg-gradient-to-b from-gray-50/50 to-white rounded-[50%] transform scale-x-150 -translate-y-24" />
{isSearching ? (
// Loading state with animated scrolling skeletons
<div className="relative h-[250px] overflow-hidden">
{/* Edge fade overlays */}
<div className="absolute left-0 top-0 bottom-0 w-[120px] z-20 pointer-events-none" style={{background: 'linear-gradient(to right, white 0%, white 20%, transparent 100%)'}} />
<div className="absolute right-0 top-0 bottom-0 w-[120px] z-20 pointer-events-none" style={{background: 'linear-gradient(to left, white 0%, white 20%, transparent 100%)'}} />
<div className="carousel-container absolute left-0 flex gap-12 py-4">
{/* Duplicate skeleton tiles for continuous scroll */}
{[...Array(10), ...Array(10)].map((_, index) => (
<div
key={`loading-${index}`}
className="flex-shrink-0 w-[400px] h-[240px] rounded-lg overflow-hidden border-2 border-gray-200/30 bg-white relative"
>
<div className="absolute inset-0 skeleton-shimmer">
<div className="absolute inset-0 bg-gradient-to-r from-gray-100 via-gray-50 to-gray-100 skeleton-gradient" />
</div>
{/* Fake browser UI - 5x bigger */}
<div className="absolute top-0 left-0 right-0 h-40 bg-gray-100 border-b border-gray-200/50 flex items-center px-6 gap-4">
<div className="flex gap-3">
<div className="w-5 h-5 rounded-full bg-gray-300 animate-pulse" />
<div className="w-5 h-5 rounded-full bg-gray-300 animate-pulse" style={{ animationDelay: '0.1s' }} />
<div className="w-5 h-5 rounded-full bg-gray-300 animate-pulse" style={{ animationDelay: '0.2s' }} />
</div>
<div className="flex-1 h-8 bg-gray-200 rounded-md mx-6 animate-pulse" />
</div>
{/* Content skeleton - positioned just below nav bar */}
<div className="absolute top-44 left-4 right-4">
<div className="h-3 bg-gray-200 rounded w-3/4 mb-2 animate-pulse" />
<div className="h-3 bg-gray-150 rounded w-1/2 mb-2 animate-pulse" style={{ animationDelay: '0.2s' }} />
<div className="h-3 bg-gray-150 rounded w-2/3 animate-pulse" style={{ animationDelay: '0.3s' }} />
</div>
</div>
))}
</div>
</div>
) : searchResults.length > 0 ? (
// Actual results
<div className="relative h-[250px] overflow-hidden">
{/* Edge fade overlays */}
<div className="absolute left-0 top-0 bottom-0 w-[120px] z-20 pointer-events-none" style={{background: 'linear-gradient(to right, white 0%, white 20%, transparent 100%)'}} />
<div className="absolute right-0 top-0 bottom-0 w-[120px] z-20 pointer-events-none" style={{background: 'linear-gradient(to left, white 0%, white 20%, transparent 100%)'}} />
<div className="carousel-container absolute left-0 flex gap-12 py-4">
{/* Duplicate results for infinite scroll */}
{[...searchResults, ...searchResults].map((result, index) => (
<div
key={`${result.url}-${index}`}
className="group flex-shrink-0 w-[400px] h-[240px] rounded-lg overflow-hidden border-2 border-gray-200/50 transition-all duration-300 hover:shadow-2xl bg-white relative"
onMouseLeave={() => {
if (showInstructionsForIndex === index) {
setShowInstructionsForIndex(null);
setAdditionalInstructions('');
}
}}
>
{/* Hover overlay with clone buttons or instructions input */}
<div className="absolute inset-0 bg-gradient-to-t from-black/90 via-black/50 to-black/30 opacity-0 group-hover:opacity-100 transition-opacity duration-300 z-10 flex flex-col items-center justify-center p-6">
{showInstructionsForIndex === index ? (
/* Instructions input view - matching main input style exactly */
<div className="w-full max-w-[380px]">
<div className="bg-white rounded-20" style={{
boxShadow: "0px 0px 44px 0px rgba(0, 0, 0, 0.02), 0px 88px 56px -20px rgba(0, 0, 0, 0.03), 0px 56px 56px -20px rgba(0, 0, 0, 0.02), 0px 32px 32px -20px rgba(0, 0, 0, 0.03), 0px 16px 24px -12px rgba(0, 0, 0, 0.03), 0px 0px 0px 1px rgba(0, 0, 0, 0.05)"
}}>
{/* Input area matching main search */}
<div className="p-16 flex gap-12 items-start w-full relative">
{/* Instructions icon */}
<div className="mt-2 flex-shrink-0">
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="opacity-40"
>
<path d="M5 5H15M5 10H15M5 15H10" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
</svg>
</div>
<textarea
value={additionalInstructions}
onChange={(e) => setAdditionalInstructions(e.target.value)}
placeholder="Describe your customizations..."
className="flex-1 bg-transparent text-body-input text-accent-black placeholder:text-black-alpha-48 focus:outline-none focus:ring-0 focus:border-transparent resize-none min-h-[60px]"
autoFocus
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => {
if (e.key === 'Escape') {
e.stopPropagation();
setShowInstructionsForIndex(null);
setAdditionalInstructions('');
}
}}
/>
</div>
{/* Divider */}
<div className="border-t border-black-alpha-5" />
{/* Buttons area matching main style */}
<div className="p-10 flex justify-between items-center">
<button
onClick={(e) => {
e.stopPropagation();
setShowInstructionsForIndex(null);
setAdditionalInstructions('');
}}
className="button relative rounded-10 px-8 py-8 text-label-medium font-medium flex items-center justify-center bg-black-alpha-4 hover:bg-black-alpha-6 text-black-alpha-48 active:scale-[0.995] transition-all"
>
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M12 5L7 10L12 15" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</button>
<button
onClick={(e) => {
e.stopPropagation();
if (additionalInstructions.trim()) {
sessionStorage.setItem('additionalInstructions', additionalInstructions);
handleSubmit(result);
}
}}
disabled={!additionalInstructions.trim()}
className={`
button relative rounded-10 px-8 py-8 text-label-medium font-medium
flex items-center justify-center gap-6
${additionalInstructions.trim()
? 'button-primary text-accent-white active:scale-[0.995]'
: 'bg-black-alpha-4 text-black-alpha-24 cursor-not-allowed'
}
`}
>
{additionalInstructions.trim() && <div className="button-background absolute inset-0 rounded-10 pointer-events-none" />}
<span className="px-6 relative">Apply & Clone</span>
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="relative"
>
<path d="M11.6667 4.79163L16.875 9.99994M16.875 9.99994L11.6667 15.2083M16.875 9.99994H3.125" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5"/>
</svg>
</button>
</div>
</div>
</div>
) : (
/* Default buttons view */
<>
<div className="text-white text-center mb-3">
<p className="text-base font-semibold mb-0.5">{result.title}</p>
<p className="text-[11px] opacity-80">Choose how to clone this site</p>
</div>
<div className="flex gap-3 justify-center">
{/* Instant Clone Button - Orange/Heat style */}
<button
onClick={(e) => {
e.stopPropagation();
handleSubmit(result);
}}
className="bg-orange-500 hover:bg-orange-600 flex items-center justify-center button relative text-label-medium button-primary group/button rounded-10 p-8 gap-2 text-white active:scale-[0.995]"
>
<div className="button-background absolute inset-0 rounded-10 pointer-events-none" />
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="relative"
>
<path d="M11.6667 4.79163L16.875 9.99994M16.875 9.99994L11.6667 15.2083M16.875 9.99994H3.125" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5"/>
</svg>
<span className="px-6 relative">Instant Clone</span>
</button>
{/* Instructions Button - Gray style */}
<button
onClick={(e) => {
e.stopPropagation();
setShowInstructionsForIndex(index);
setAdditionalInstructions('');
}}
className="bg-gray-100 hover:bg-gray-200 flex items-center justify-center button relative text-label-medium rounded-10 p-8 gap-2 text-gray-700 active:scale-[0.995]"
>
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="opacity-60"
>
<path d="M5 5H15M5 10H15M5 15H10" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
<path d="M14 14L16 16L14 18" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
<span className="px-6">Add Instructions</span>
</button>
</div>
</>
)}
</div>
{result.screenshot ? (
<div className="relative w-full h-full">
<Image
src={result.screenshot}
alt={result.title}
fill
className="object-cover object-top"
loading="lazy"
/>
</div>
) : (
<div className="w-full h-full bg-gradient-to-br from-gray-100 to-gray-50 flex items-center justify-center">
<div className="text-center">
<div className="w-16 h-16 rounded-full bg-gray-200 mx-auto mb-3 flex items-center justify-center">
<svg
width="32"
height="32"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="text-gray-400"
>
<rect x="3" y="3" width="18" height="18" rx="2" stroke="currentColor" strokeWidth="1.5"/>
<path d="M3 9H21" stroke="currentColor" strokeWidth="1.5"/>
<circle cx="6" cy="6" r="1" fill="currentColor"/>
<circle cx="9" cy="6" r="1" fill="currentColor"/>
<circle cx="12" cy="6" r="1" fill="currentColor"/>
</svg>
</div>
<p className="text-gray-500 text-sm font-medium">{result.title}</p>
</div>
</div>
)}
</div>
))}
</div>
</div>
) : (
// No results state
<div className="relative h-[250px] flex items-center justify-center">
<div className="text-center">
<div className="mb-4">
<svg className="w-16 h-16 mx-auto text-gray-300" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
<p className="text-gray-500 text-lg">No results found</p>
<p className="text-gray-400 text-sm mt-1">Try a different search term</p>
</div>
</div>
)}
</section>
)}
</div>
<style jsx>{`
@keyframes infiniteScroll {
from {
transform: translateX(0);
}
to {
transform: translateX(-50%);
}
}
@keyframes shimmer {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.carousel-container {
animation: infiniteScroll 30s linear infinite;
}
.carousel-container:hover {
animation-play-state: paused;
}
.skeleton-shimmer {
position: relative;
overflow: hidden;
}
.skeleton-gradient {
animation: shimmer 2s infinite;
}
`}</style>
</HeaderProvider>
);
}