resolve installation issue on vercel sandbox + refine search feature with instruction capabilities

This commit is contained in:
Developers Digest
2025-09-09 15:58:36 -04:00
parent 599e209888
commit 8687860a47
8 changed files with 760 additions and 533 deletions
+333 -113
View File
@@ -47,6 +47,8 @@ export default function HomePage() {
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 router = useRouter();
// Simple URL validation
@@ -170,10 +172,12 @@ export default function HomePage() {
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',
@@ -184,6 +188,7 @@ export default function HomePage() {
if (response.ok) {
const data = await response.json();
setSearchResults(data.results || []);
setShowSearchTiles(true);
}
} catch (error) {
console.error('Search error:', error);
@@ -272,80 +277,132 @@ export default function HomePage() {
>
<div className="p-16 flex gap-12 items-center w-full relative bg-white rounded-20">
{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>
{/* 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>
</>
) : (
// 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>
<>
{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="Enter URL or search term..."
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={isURL(url) ? 'Scrape Site' : 'Search'}
disabled={isSearching}
/>
</div>
</>
)}
<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="Enter URL or search term..."
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)) {
setShowSearchTiles(true);
}
}}
onBlur={() => {
setTimeout(() => setShowSearchTiles(false), 200);
}}
/>
<div
onClick={(e) => {
e.preventDefault();
if (!isSearching) {
handleSubmit();
}
}}
className={isSearching ? 'pointer-events-none' : ''}
>
<HeroInputSubmitButton
dirty={url.length > 0}
buttonText={isURL(url) ? 'Scrape Site' : 'Search'}
disabled={isSearching}
/>
</div>
</div>
@@ -430,61 +487,42 @@ export default function HomePage() {
<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 skeletons
// 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="flex gap-12 py-4 px-8">
{[0, 1, 2, 3, 4].map((index) => (
<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-24 overflow-hidden border-2 border-gray-200/30 bg-white relative"
style={{
animation: `fadeIn 0.5s ease-out forwards`,
animationDelay: `${index * 100}ms`,
opacity: 0
}}
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 */}
<div className="absolute top-0 left-0 right-0 h-8 bg-gray-100 border-b border-gray-200/50 flex items-center px-3 gap-2">
<div className="flex gap-1">
<div className="w-2 h-2 rounded-full bg-gray-300" />
<div className="w-2 h-2 rounded-full bg-gray-300" />
<div className="w-2 h-2 rounded-full bg-gray-300" />
{/* 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-4 bg-gray-200 rounded-sm mx-4" />
<div className="flex-1 h-8 bg-gray-200 rounded-md mx-6 animate-pulse" />
</div>
{/* Content skeleton */}
<div className="p-4 mt-8">
<div className="h-6 bg-gray-200 rounded w-3/4 mb-3" />
<div className="h-4 bg-gray-150 rounded w-full mb-2" />
<div className="h-4 bg-gray-150 rounded w-5/6 mb-2" />
<div className="h-4 bg-gray-150 rounded w-4/6" />
{/* 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>
{/* Loading text */}
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
<div className="bg-white/95 backdrop-blur-sm rounded-full px-6 py-3 shadow-lg border border-gray-200">
<div className="flex items-center gap-3">
<div className="flex gap-1">
<div className="w-2 h-2 bg-orange-500 rounded-full animate-bounce" style={{ animationDelay: '0ms' }} />
<div className="w-2 h-2 bg-orange-500 rounded-full animate-bounce" style={{ animationDelay: '150ms' }} />
<div className="w-2 h-2 bg-orange-500 rounded-full animate-bounce" style={{ animationDelay: '300ms' }} />
</div>
<span className="text-sm font-medium text-gray-700">Searching for sites...</span>
</div>
</div>
</div>
</div>
) : searchResults.length > 0 ? (
// Actual results
@@ -496,11 +534,173 @@ export default function HomePage() {
<div className="carousel-container absolute left-0 flex gap-12 py-4">
{/* Duplicate results for infinite scroll */}
{[...searchResults, ...searchResults].map((result, index) => (
<button
<div
key={`${result.url}-${index}`}
onClick={() => handleSubmit(result)}
className="flex-shrink-0 w-[400px] h-[240px] rounded-24 overflow-hidden border-2 border-gray-200/50 hover:border-orange-500 transition-all duration-300 hover:shadow-2xl hover:scale-[1.02] cursor-pointer bg-white"
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 ? (
<img
src={result.screenshot}
@@ -509,9 +709,29 @@ export default function HomePage() {
loading="lazy"
/>
) : (
<div className="w-full h-full bg-gradient-to-br from-gray-100 to-gray-50" />
<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>
)}
</button>
</div>
))}
</div>
</div>