Files
open-lovable/app/page.tsx
T
Developers Digest 836b085f75 continue re-design
2025-09-05 13:06:17 -04:00

261 lines
12 KiB
TypeScript

"use client";
import Link from "next/link";
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"
export default function HomePage() {
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 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());
};
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 = () => {
if (!url.trim()) {
toast.error("Please enter a URL");
return;
}
// Store the configuration in sessionStorage
sessionStorage.setItem('targetUrl', url);
sessionStorage.setItem('selectedStyle', selectedStyle);
sessionStorage.setItem('selectedModel', selectedModel);
sessionStorage.setItem('autoStart', 'true'); // Set flag to auto-start generation
// Redirect to the generation interface
router.push('/generation');
};
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">
<HomeHeroBadge />
<HomeHeroTitle />
<p className="text-center text-body-large">
Re-imagine any website, in seconds.
</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"
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-16 flex gap-12 items-center w-full relative">
<Globe />
<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="example.com"
type="text"
value={url}
onChange={(e) => {
setUrl(e.target.value);
setIsValidUrl(validateUrl(e.target.value));
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
handleSubmit();
}
}}
/>
<div
onClick={(e) => {
e.preventDefault();
handleSubmit();
}}
>
<HeroInputSubmitButton dirty={url.length > 0} />
</div>
</div>
{/* Options Section - Only show when valid URL */}
<div className={`overflow-hidden transition-all duration-500 ease-in-out ${
isValidUrl ? 'max-h-[200px] opacity-100' : 'max-h-0 opacity-0'
}`}>
<div className="p-[28px]">
<div className="border-t border-gray-100 bg-white">
{/* Style Selector */}
<div className={`mb-2 pt-4 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-1">
{styles.map((style, index) => (
<button
key={style.id}
onClick={() => setSelectedStyle(style.id)}
className={`
py-2.5 px-2 rounded text-[10px] font-medium border transition-all text-center
${selectedStyle === style.id
? 'border-orange-500 bg-orange-50 text-orange-900'
: 'border-gray-200 hover:border-gray-300 bg-white text-gray-700'
}
${isValidUrl ? 'opacity-100' : 'opacity-0'}
`}
style={{
transitionDelay: `${150 + index * 30}ms`,
transition: 'all 0.3s ease-in-out'
}}
>
{style.name}
</button>
))}
</div>
</div>
{/* Model Selector Dropdown and Additional Instructions */}
<div className={`flex 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-[10px] 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"
>
{models.map((model) => (
<option key={model.id} value={model.id}>
{model.name}
</option>
))}
</select>
{/* Additional Instructions */}
<input
type="text"
className="flex-1 px-3 py-2.5 text-[10px] 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>
</div>
</HeaderProvider>
);
}