resolve installation issue on vercel sandbox + refine search feature with instruction capabilities
This commit is contained in:
+333
-113
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user