Merge version_2 into main #1
@@ -9,9 +9,30 @@ import MetricCardOne from "@/components/sections/metrics/MetricCardOne";
|
||||
import FaqDouble from "@/components/sections/faq/FaqDouble";
|
||||
import ContactFaq from "@/components/sections/contact/ContactFaq";
|
||||
import FooterBaseReveal from "@/components/sections/footer/FooterBaseReveal";
|
||||
import VirtualTryOnUpload from "@/components/virtualTryOn/VirtualTryOnUpload";
|
||||
import VirtualTryOnPreview from "@/components/virtualTryOn/VirtualTryOnPreview";
|
||||
import { Clock, Package, Sparkles, Target, Zap } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function LandingPage() {
|
||||
const [userPhoto, setUserPhoto] = useState<string | null>(null);
|
||||
const [clothesPhoto, setClothesPhoto] = useState<string | null>(null);
|
||||
const [showPreview, setShowPreview] = useState(false);
|
||||
|
||||
const handleUserPhotoUpload = (photo: string) => {
|
||||
setUserPhoto(photo);
|
||||
};
|
||||
|
||||
const handleClothesPhotoUpload = (photo: string) => {
|
||||
setClothesPhoto(photo);
|
||||
};
|
||||
|
||||
const handleStartTryOn = () => {
|
||||
if (userPhoto && clothesPhoto) {
|
||||
setShowPreview(true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeProvider
|
||||
defaultButtonVariant="directional-hover"
|
||||
@@ -30,10 +51,11 @@ export default function LandingPage() {
|
||||
navItems={[
|
||||
{ name: "Features", id: "features" },
|
||||
{ name: "How It Works", id: "about" },
|
||||
{ name: "Try-On", id: "tryon" },
|
||||
{ name: "FAQ", id: "faq" },
|
||||
{ name: "Contact", id: "contact" },
|
||||
]}
|
||||
button={{ text: "Try Now", href: "hero" }}
|
||||
button={{ text: "Try Now", href: "tryon" }}
|
||||
brandName="VirtualTryOn"
|
||||
/>
|
||||
</div>
|
||||
@@ -43,8 +65,8 @@ export default function LandingPage() {
|
||||
logoText="VIRTUAL TRY ON"
|
||||
description="Upload your photo and clothes to instantly see how outfits look on you. No more guessing – visualize your style in seconds with AI-powered virtual fitting."
|
||||
buttons={[
|
||||
{ text: "Start Try-On", href: "#features" },
|
||||
{ text: "Learn More", href: "#about" },
|
||||
{ text: "Start Try-On", href: "tryon" },
|
||||
{ text: "Learn More", href: "about" },
|
||||
]}
|
||||
slides={[
|
||||
{
|
||||
@@ -100,12 +122,41 @@ export default function LandingPage() {
|
||||
title="Revolutionary AI-Powered Virtual Fitting Technology. Say goodbye to sizing anxiety and hello to confident shopping."
|
||||
useInvertedBackground={true}
|
||||
buttons={[
|
||||
{ text: "Get Started Free", href: "#contact" },
|
||||
{ text: "View Demo", href: "#features" },
|
||||
{ text: "Get Started Free", href: "tryon" },
|
||||
{ text: "View Demo", href: "features" },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="tryon" data-section="tryon">
|
||||
<div className="w-full py-20 px-4 md:px-8 bg-gradient-to-b from-transparent to-background/50">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
{!showPreview ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<VirtualTryOnUpload
|
||||
title="Upload Your Photo"
|
||||
description="Select a clear full-body photo for accurate measurements"
|
||||
onPhotoUpload={handleUserPhotoUpload}
|
||||
uploadType="user"
|
||||
/>
|
||||
<VirtualTryOnUpload
|
||||
title="Upload Clothing Photo"
|
||||
description="Choose the clothes you want to try on"
|
||||
onPhotoUpload={handleClothesPhotoUpload}
|
||||
uploadType="clothes"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<VirtualTryOnPreview
|
||||
userPhoto={userPhoto}
|
||||
clothesPhoto={clothesPhoto}
|
||||
onBack={() => setShowPreview(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="metrics" data-section="metrics">
|
||||
<MetricCardOne
|
||||
metrics={[
|
||||
@@ -170,7 +221,7 @@ export default function LandingPage() {
|
||||
]}
|
||||
ctaTitle="Ready to Transform Your Shopping?"
|
||||
ctaDescription="Start your virtual try-on journey today. Get instant access to our AI-powered fitting technology."
|
||||
ctaButton={{ text: "Launch App", href: "#hero" }}
|
||||
ctaButton={{ text: "Launch App", href: "tryon" }}
|
||||
ctaIcon={Sparkles}
|
||||
animationType="slide-up"
|
||||
accordionAnimationType="smooth"
|
||||
@@ -183,9 +234,9 @@ export default function LandingPage() {
|
||||
columns={[
|
||||
{
|
||||
title: "Product", items: [
|
||||
{ label: "Features", href: "#features" },
|
||||
{ label: "How It Works", href: "#about" },
|
||||
{ label: "Pricing", href: "#" },
|
||||
{ label: "Features", href: "features" },
|
||||
{ label: "How It Works", href: "about" },
|
||||
{ label: "Try-On", href: "tryon" },
|
||||
{ label: "Blog", href: "#" },
|
||||
],
|
||||
},
|
||||
@@ -194,7 +245,7 @@ export default function LandingPage() {
|
||||
{ label: "About Us", href: "#" },
|
||||
{ label: "Careers", href: "#" },
|
||||
{ label: "Press", href: "#" },
|
||||
{ label: "Contact", href: "#contact" },
|
||||
{ label: "Contact", href: "contact" },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -211,4 +262,4 @@ export default function LandingPage() {
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
193
src/components/virtualTryOn/VirtualTryOnPreview.tsx
Normal file
193
src/components/virtualTryOn/VirtualTryOnPreview.tsx
Normal file
@@ -0,0 +1,193 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { ArrowLeft, Download, Share2, RotateCcw } from "lucide-react";
|
||||
|
||||
interface VirtualTryOnPreviewProps {
|
||||
userPhoto: string | null;
|
||||
clothesPhoto: string | null;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
export default function VirtualTryOnPreview({
|
||||
userPhoto,
|
||||
clothesPhoto,
|
||||
onBack,
|
||||
}: VirtualTryOnPreviewProps) {
|
||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||
const [isProcessing, setIsProcessing] = useState(true);
|
||||
const [selectedFit, setSelectedFit] = useState<"tight" | "normal" | "loose">("normal");
|
||||
|
||||
useEffect(() => {
|
||||
// Simulate AI processing time
|
||||
const timer = setTimeout(() => {
|
||||
setIsProcessing(false);
|
||||
// Create a simple composite preview
|
||||
if (userPhoto && clothesPhoto) {
|
||||
// In a real implementation, this would use server-side image processing
|
||||
setPreviewImage(userPhoto);
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [userPhoto, clothesPhoto]);
|
||||
|
||||
const handleDownload = () => {
|
||||
if (previewImage) {
|
||||
const link = document.createElement("a");
|
||||
link.href = previewImage;
|
||||
link.download = "virtual-tryon-result.png";
|
||||
link.click();
|
||||
}
|
||||
};
|
||||
|
||||
const handleShare = () => {
|
||||
if (navigator.share && previewImage) {
|
||||
navigator.share({
|
||||
title: "My Virtual Try-On", text: "Check out how I look in these clothes!"});
|
||||
} else {
|
||||
// Fallback: copy to clipboard
|
||||
const url = window.location.href;
|
||||
navigator.clipboard.writeText(url);
|
||||
alert("Link copied to clipboard!");
|
||||
}
|
||||
};
|
||||
|
||||
const handleFitChange = (fit: "tight" | "normal" | "loose") => {
|
||||
setSelectedFit(fit);
|
||||
setIsProcessing(true);
|
||||
setTimeout(() => setIsProcessing(false), 1500);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
{/* Header */}
|
||||
<div className="mb-8 flex items-center justify-between">
|
||||
<h2 className="text-2xl md:text-3xl font-semibold text-foreground">Your Virtual Try-On</h2>
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="flex items-center gap-2 px-4 py-2 rounded-lg border border-foreground/20 hover:border-foreground/40 hover:bg-foreground/5 transition-all text-foreground text-sm"
|
||||
aria-label="Go back"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
<span>Back</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Main Preview Container */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{/* Preview Image */}
|
||||
<div className="lg:col-span-2">
|
||||
<div className="relative w-full aspect-video rounded-xl overflow-hidden border-2 border-foreground/20 bg-gradient-to-br from-foreground/5 to-background/20">
|
||||
{isProcessing ? (
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center gap-4 bg-background/40 backdrop-blur-sm">
|
||||
<div className="w-16 h-16 rounded-full border-4 border-foreground/20 border-t-primary-cta animate-spin"></div>
|
||||
<p className="text-foreground text-center text-sm md:text-base">
|
||||
Processing your virtual try-on with AI technology...
|
||||
</p>
|
||||
</div>
|
||||
) : previewImage ? (
|
||||
<>
|
||||
<img
|
||||
src={previewImage}
|
||||
alt="Virtual try-on preview"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-transparent to-background/10"></div>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{/* Details Below Preview */}
|
||||
<div className="mt-6 grid grid-cols-2 gap-4">
|
||||
<div className="p-4 rounded-lg bg-foreground/5 border border-foreground/10">
|
||||
<p className="text-xs text-foreground/60 uppercase tracking-wide mb-2">Your Photo</p>
|
||||
<div className="w-full aspect-square rounded-lg overflow-hidden border border-foreground/20 bg-foreground/5">
|
||||
{userPhoto && (
|
||||
<img
|
||||
src={userPhoto}
|
||||
alt="Your photo"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 rounded-lg bg-foreground/5 border border-foreground/10">
|
||||
<p className="text-xs text-foreground/60 uppercase tracking-wide mb-2">Clothes</p>
|
||||
<div className="w-full aspect-square rounded-lg overflow-hidden border border-foreground/20 bg-foreground/5">
|
||||
{clothesPhoto && (
|
||||
<img
|
||||
src={clothesPhoto}
|
||||
alt="Clothes photo"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Controls Panel */}
|
||||
<div className="flex flex-col gap-6">
|
||||
{/* Fit Selector */}
|
||||
<div className="p-6 rounded-xl bg-foreground/5 border border-foreground/10">
|
||||
<h3 className="text-sm font-semibold text-foreground mb-4 uppercase tracking-wide">
|
||||
Fit Preference
|
||||
</h3>
|
||||
<div className="flex flex-col gap-3">
|
||||
{(["tight", "normal", "loose"] as const).map((fit) => (
|
||||
<button
|
||||
key={fit}
|
||||
onClick={() => handleFitChange(fit)}
|
||||
className={`px-4 py-3 rounded-lg transition-all text-sm font-medium capitalize border-2 ${
|
||||
selectedFit === fit
|
||||
? "border-primary-cta bg-primary-cta/10 text-foreground"
|
||||
: "border-foreground/20 bg-background text-foreground/70 hover:border-foreground/40"
|
||||
}`}
|
||||
>
|
||||
{fit}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="space-y-3">
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
className="w-full px-4 py-3 rounded-lg bg-primary-cta text-background font-medium flex items-center justify-center gap-2 hover:opacity-90 transition-opacity"
|
||||
aria-label="Download result"
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
<span>Download</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={handleShare}
|
||||
className="w-full px-4 py-3 rounded-lg bg-foreground/10 border border-foreground/20 text-foreground font-medium flex items-center justify-center gap-2 hover:border-foreground/40 hover:bg-foreground/15 transition-all"
|
||||
aria-label="Share result"
|
||||
>
|
||||
<Share2 className="w-4 h-4" />
|
||||
<span>Share</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="w-full px-4 py-3 rounded-lg bg-foreground/10 border border-foreground/20 text-foreground font-medium flex items-center justify-center gap-2 hover:border-foreground/40 hover:bg-foreground/15 transition-all"
|
||||
aria-label="Try again"
|
||||
>
|
||||
<RotateCcw className="w-4 h-4" />
|
||||
<span>Try Again</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Info Box */}
|
||||
<div className="p-4 rounded-lg bg-background-accent/40 border border-foreground/10">
|
||||
<p className="text-xs text-foreground/70 leading-relaxed">
|
||||
<span className="font-semibold text-foreground block mb-2">Accuracy:</span>
|
||||
This preview is powered by advanced AI. The actual fit may vary based on fabric properties and your exact measurements.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
136
src/components/virtualTryOn/VirtualTryOnUpload.tsx
Normal file
136
src/components/virtualTryOn/VirtualTryOnUpload.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useRef } from "react";
|
||||
import { Upload, X } from "lucide-react";
|
||||
|
||||
interface VirtualTryOnUploadProps {
|
||||
title: string;
|
||||
description: string;
|
||||
onPhotoUpload: (photo: string) => void;
|
||||
uploadType: "user" | "clothes";
|
||||
}
|
||||
|
||||
export default function VirtualTryOnUpload({
|
||||
title,
|
||||
description,
|
||||
onPhotoUpload,
|
||||
uploadType,
|
||||
}: VirtualTryOnUploadProps) {
|
||||
const [preview, setPreview] = useState<string | null>(null);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleFileSelect = (file: File) => {
|
||||
if (file && file.type.startsWith("image/")) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const result = e.target?.result as string;
|
||||
setPreview(result);
|
||||
onPhotoUpload(result);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragOver = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(true);
|
||||
};
|
||||
|
||||
const handleDragLeave = () => {
|
||||
setIsDragging(false);
|
||||
};
|
||||
|
||||
const handleDrop = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(false);
|
||||
const files = e.dataTransfer.files;
|
||||
if (files && files[0]) {
|
||||
handleFileSelect(files[0]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = e.target.files;
|
||||
if (files && files[0]) {
|
||||
handleFileSelect(files[0]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemovePhoto = () => {
|
||||
setPreview(null);
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
fileInputRef.current?.click();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="mb-6">
|
||||
<h3 className="text-xl md:text-2xl font-semibold mb-2 text-foreground">
|
||||
{title}
|
||||
</h3>
|
||||
<p className="text-sm md:text-base text-foreground/70">{description}</p>
|
||||
</div>
|
||||
|
||||
{!preview ? (
|
||||
<div
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
onClick={handleClick}
|
||||
className={`relative w-full aspect-video rounded-xl border-2 border-dashed transition-all cursor-pointer flex flex-col items-center justify-center gap-4 p-8 ${
|
||||
isDragging
|
||||
? "border-primary-cta bg-primary-cta/10"
|
||||
: "border-foreground/20 bg-foreground/5 hover:border-foreground/40 hover:bg-foreground/10"
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleFileInputChange}
|
||||
className="hidden"
|
||||
aria-label={`Upload ${uploadType} photo`}
|
||||
/>
|
||||
<div className="text-center">
|
||||
<Upload className="w-12 h-12 md:w-16 md:h-16 mx-auto mb-4 text-primary-cta opacity-70" />
|
||||
<p className="text-base md:text-lg font-medium text-foreground mb-2">
|
||||
{isDragging ? "Drop your photo here" : "Drag & drop or click to upload"}
|
||||
</p>
|
||||
<p className="text-sm text-foreground/60">PNG, JPG, GIF up to 10MB</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative w-full aspect-video rounded-xl overflow-hidden border-2 border-foreground/20 bg-foreground/5">
|
||||
<img
|
||||
src={preview}
|
||||
alt={`${uploadType} photo preview`}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
<button
|
||||
onClick={handleRemovePhoto}
|
||||
className="absolute top-3 right-3 p-2 bg-background/80 hover:bg-background rounded-lg transition-colors border border-foreground/20 flex items-center justify-center"
|
||||
aria-label="Remove photo"
|
||||
>
|
||||
<X className="w-5 h-5 text-foreground" />
|
||||
</button>
|
||||
<div className="absolute bottom-3 left-3 right-3 flex items-center gap-2">
|
||||
<div className="flex-1 h-1 bg-foreground/20 rounded-full overflow-hidden">
|
||||
<div className="h-full w-1/2 bg-primary-cta rounded-full"></div>
|
||||
</div>
|
||||
<span className="text-xs font-medium text-foreground/70">Processing...</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-4 p-4 bg-background-accent/40 rounded-lg border border-foreground/10">
|
||||
<p className="text-xs md:text-sm text-foreground/70">
|
||||
<span className="font-semibold text-foreground">Pro tip:</span> For best results, ensure good
|
||||
lighting, wear fitted clothes, and stand in a neutral pose.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user