Merge version_2 into main #2
337
src/app/chef-profile/page.tsx
Normal file
337
src/app/chef-profile/page.tsx
Normal file
@@ -0,0 +1,337 @@
|
||||
"use client"
|
||||
|
||||
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
|
||||
import NavbarStyleCentered from '@/components/navbar/NavbarStyleCentered/NavbarStyleCentered';
|
||||
import HeroLogoBillboard from '@/components/sections/hero/HeroLogoBillboard';
|
||||
import FooterBaseCard from '@/components/sections/footer/FooterBaseCard';
|
||||
import { useState } from "react";
|
||||
import { Upload, Edit2, Trash2, Play, Pause, MoreVertical } from "lucide-react";
|
||||
|
||||
interface Video {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
thumbnail: string;
|
||||
duration: string;
|
||||
uploadDate: string;
|
||||
views: number;
|
||||
status: "published" | "draft" | "archived";
|
||||
}
|
||||
|
||||
export default function ChefProfilePage() {
|
||||
const [videos, setVideos] = useState<Video[]>([
|
||||
{
|
||||
id: "1", title: "Signature Skincare Routine", description: "Learn our 5-step skincare routine for glowing skin", thumbnail: "http://img.b2bpic.net/free-photo/model-career-kit-still-life-flat-lay_23-2150218023.jpg", duration: "12:34", uploadDate: "Jan 15, 2025", views: 2543,
|
||||
status: "published"
|
||||
},
|
||||
{
|
||||
id: "2", title: "Ingredient Spotlight: Natural Botanicals", description: "Deep dive into our premium organic ingredients", thumbnail: "http://img.b2bpic.net/free-photo/flat-lay-hands-holding-body-care-product-wooden-background_23-2148241876.jpg", duration: "8:45", uploadDate: "Jan 12, 2025", views: 1876,
|
||||
status: "published"
|
||||
},
|
||||
{
|
||||
id: "3", title: "Product Application Techniques", description: "Master the art of applying skincare products", thumbnail: "http://img.b2bpic.net/free-photo/product-branding-packaging_23-2150965833.jpg", duration: "15:22", uploadDate: "Jan 10, 2025", views: 3421,
|
||||
status: "published"
|
||||
},
|
||||
{
|
||||
id: "4", title: "Winter Skincare Tips", description: "Keep your skin glowing during cold months", thumbnail: "http://img.b2bpic.net/free-photo/woman-applying-moisturizer-her-beauty-routine_23-2150166464.jpg", duration: "10:15", uploadDate: "Jan 8, 2025", views: 0,
|
||||
status: "draft"
|
||||
}
|
||||
]);
|
||||
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
const [editData, setEditData] = useState({ title: "", description: "" });
|
||||
|
||||
const handleEdit = (video: Video) => {
|
||||
setEditingId(video.id);
|
||||
setEditData({ title: video.title, description: video.description });
|
||||
};
|
||||
|
||||
const handleSaveEdit = (id: string) => {
|
||||
setVideos(videos.map(v =>
|
||||
v.id === id ? { ...v, title: editData.title, description: editData.description } : v
|
||||
));
|
||||
setEditingId(null);
|
||||
};
|
||||
|
||||
const handleDelete = (id: string) => {
|
||||
setVideos(videos.filter(v => v.id !== id));
|
||||
};
|
||||
|
||||
const publishedVideos = videos.filter(v => v.status === "published");
|
||||
const draftVideos = videos.filter(v => v.status === "draft");
|
||||
const totalViews = videos.reduce((sum, v) => sum + v.views, 0);
|
||||
|
||||
return (
|
||||
<ThemeProvider
|
||||
defaultButtonVariant="hover-bubble"
|
||||
defaultTextAnimation="reveal-blur"
|
||||
borderRadius="rounded"
|
||||
contentWidth="small"
|
||||
sizing="largeSmallSizeLargeTitles"
|
||||
background="circleGradient"
|
||||
cardStyle="gradient-bordered"
|
||||
primaryButtonStyle="double-inset"
|
||||
secondaryButtonStyle="solid"
|
||||
headingFontWeight="extrabold"
|
||||
>
|
||||
<div id="nav" data-section="nav">
|
||||
<NavbarStyleCentered
|
||||
navItems={[
|
||||
{ name: "Home", id: "/" },
|
||||
{ name: "About", id: "about" },
|
||||
{ name: "Products", id: "products" },
|
||||
{ name: "Contact", id: "contact" }
|
||||
]}
|
||||
button={{ text: "Shop Now", href: "/" }}
|
||||
brandName="Lumière Skin"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="hero" data-section="hero">
|
||||
<HeroLogoBillboard
|
||||
logoText="Chef Profile & Video Management"
|
||||
description="Manage and showcase your video content. Track performance, edit details, and grow your audience."
|
||||
buttons={[
|
||||
{ text: "Upload New Video", href: "#" },
|
||||
{ text: "Back to Home", href: "/" }
|
||||
]}
|
||||
background={{ variant: "sparkles-gradient" }}
|
||||
imageSrc="http://img.b2bpic.net/free-photo/woman-applying-moisturizer-her-beauty-routine_23-2150166464.jpg"
|
||||
imageAlt="Chef profile"
|
||||
mediaAnimation="slide-up"
|
||||
frameStyle="card"
|
||||
buttonAnimation="blur-reveal"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<section className="py-20 px-4">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Stats Section */}
|
||||
<div className="mb-12 grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div className="p-6 rounded-lg bg-card border border-card">
|
||||
<h3 className="text-sm font-semibold text-foreground/60 mb-2">Total Videos</h3>
|
||||
<p className="text-4xl font-bold text-foreground">{videos.length}</p>
|
||||
</div>
|
||||
<div className="p-6 rounded-lg bg-card border border-card">
|
||||
<h3 className="text-sm font-semibold text-foreground/60 mb-2">Total Views</h3>
|
||||
<p className="text-4xl font-bold text-foreground">{totalViews.toLocaleString()}</p>
|
||||
</div>
|
||||
<div className="p-6 rounded-lg bg-card border border-card">
|
||||
<h3 className="text-sm font-semibold text-foreground/60 mb-2">Published</h3>
|
||||
<p className="text-4xl font-bold text-foreground">{publishedVideos.length}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Published Videos */}
|
||||
<div className="mb-12">
|
||||
<h2 className="text-2xl font-bold text-foreground mb-6">Published Videos</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{publishedVideos.map(video => (
|
||||
<div key={video.id} className="rounded-lg overflow-hidden bg-card border border-card hover:border-primary-cta/50 transition-all">
|
||||
{/* Thumbnail */}
|
||||
<div className="relative aspect-video overflow-hidden bg-background">
|
||||
<img
|
||||
src={video.thumbnail}
|
||||
alt={video.title}
|
||||
className="w-full h-full object-cover hover:scale-105 transition-transform"
|
||||
/>
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-black/30 opacity-0 hover:opacity-100 transition-opacity">
|
||||
<Play className="w-12 h-12 text-white fill-white" />
|
||||
</div>
|
||||
<div className="absolute bottom-2 right-2 bg-black/70 px-2 py-1 rounded text-xs text-white">
|
||||
{video.duration}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-4">
|
||||
{editingId === video.id ? (
|
||||
<div className="space-y-3">
|
||||
<input
|
||||
type="text"
|
||||
value={editData.title}
|
||||
onChange={(e) => setEditData({...editData, title: e.target.value})}
|
||||
className="w-full bg-background border border-foreground/20 rounded px-2 py-1 text-foreground text-sm"
|
||||
/>
|
||||
<textarea
|
||||
value={editData.description}
|
||||
onChange={(e) => setEditData({...editData, description: e.target.value})}
|
||||
className="w-full bg-background border border-foreground/20 rounded px-2 py-1 text-foreground text-xs resize-none h-16"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => handleSaveEdit(video.id)}
|
||||
className="flex-1 bg-primary-cta text-background px-3 py-1 rounded text-sm font-medium hover:opacity-90"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setEditingId(null)}
|
||||
className="flex-1 bg-secondary-cta text-foreground px-3 py-1 rounded text-sm font-medium hover:opacity-90"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<h3 className="font-semibold text-foreground mb-1 line-clamp-2">{video.title}</h3>
|
||||
<p className="text-xs text-foreground/60 mb-3 line-clamp-2">{video.description}</p>
|
||||
<div className="flex justify-between items-center mb-3 text-xs text-foreground/60">
|
||||
<span>{video.uploadDate}</span>
|
||||
<span>{video.views.toLocaleString()} views</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => handleEdit(video)}
|
||||
className="flex-1 flex items-center justify-center gap-1 bg-secondary-cta text-foreground px-2 py-1 rounded text-xs font-medium hover:opacity-90 transition-opacity"
|
||||
>
|
||||
<Edit2 className="w-3 h-3" />
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(video.id)}
|
||||
className="flex-1 flex items-center justify-center gap-1 bg-accent/20 text-accent px-2 py-1 rounded text-xs font-medium hover:opacity-90 transition-opacity"
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Draft Videos */}
|
||||
{draftVideos.length > 0 && (
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-foreground mb-6">Draft Videos</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{draftVideos.map(video => (
|
||||
<div key={video.id} className="rounded-lg overflow-hidden bg-card border border-card border-dashed opacity-75 hover:border-primary-cta/50 transition-all">
|
||||
{/* Thumbnail */}
|
||||
<div className="relative aspect-video overflow-hidden bg-background">
|
||||
<img
|
||||
src={video.thumbnail}
|
||||
alt={video.title}
|
||||
className="w-full h-full object-cover opacity-75"
|
||||
/>
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-black/50">
|
||||
<span className="text-white font-semibold">DRAFT</span>
|
||||
</div>
|
||||
<div className="absolute bottom-2 right-2 bg-black/70 px-2 py-1 rounded text-xs text-white">
|
||||
{video.duration}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-4">
|
||||
{editingId === video.id ? (
|
||||
<div className="space-y-3">
|
||||
<input
|
||||
type="text"
|
||||
value={editData.title}
|
||||
onChange={(e) => setEditData({...editData, title: e.target.value})}
|
||||
className="w-full bg-background border border-foreground/20 rounded px-2 py-1 text-foreground text-sm"
|
||||
/>
|
||||
<textarea
|
||||
value={editData.description}
|
||||
onChange={(e) => setEditData({...editData, description: e.target.value})}
|
||||
className="w-full bg-background border border-foreground/20 rounded px-2 py-1 text-foreground text-xs resize-none h-16"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => handleSaveEdit(video.id)}
|
||||
className="flex-1 bg-primary-cta text-background px-3 py-1 rounded text-sm font-medium hover:opacity-90"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setEditingId(null)}
|
||||
className="flex-1 bg-secondary-cta text-foreground px-3 py-1 rounded text-sm font-medium hover:opacity-90"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<h3 className="font-semibold text-foreground mb-1 line-clamp-2">{video.title}</h3>
|
||||
<p className="text-xs text-foreground/60 mb-3 line-clamp-2">{video.description}</p>
|
||||
<div className="flex justify-between items-center mb-3 text-xs text-foreground/60">
|
||||
<span>{video.uploadDate}</span>
|
||||
<span>Not published</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => handleEdit(video)}
|
||||
className="flex-1 flex items-center justify-center gap-1 bg-secondary-cta text-foreground px-2 py-1 rounded text-xs font-medium hover:opacity-90 transition-opacity"
|
||||
>
|
||||
<Edit2 className="w-3 h-3" />
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(video.id)}
|
||||
className="flex-1 flex items-center justify-center gap-1 bg-accent/20 text-accent px-2 py-1 rounded text-xs font-medium hover:opacity-90 transition-opacity"
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div id="footer" data-section="footer">
|
||||
<FooterBaseCard
|
||||
logoText="Lumière Skin"
|
||||
columns={[
|
||||
{
|
||||
title: "Shop", items: [
|
||||
{ label: "All Products", href: "/#products" },
|
||||
{ label: "Skincare Routine", href: "/#products" },
|
||||
{ label: "Collections", href: "/#products" },
|
||||
{ label: "Gift Sets", href: "/#products" }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Company", items: [
|
||||
{ label: "About Us", href: "/#about" },
|
||||
{ label: "Our Story", href: "/#about" },
|
||||
{ label: "Sustainability", href: "#" },
|
||||
{ label: "Careers", href: "#" }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Support", items: [
|
||||
{ label: "Contact Us", href: "/#contact" },
|
||||
{ label: "FAQ", href: "/#faq" },
|
||||
{ label: "Shipping Info", href: "#" },
|
||||
{ label: "Returns", href: "#" }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Connect", items: [
|
||||
{ label: "Instagram", href: "https://instagram.com" },
|
||||
{ label: "Facebook", href: "https://facebook.com" },
|
||||
{ label: "Pinterest", href: "https://pinterest.com" },
|
||||
{ label: "TikTok", href: "https://tiktok.com" }
|
||||
]
|
||||
}
|
||||
]}
|
||||
copyrightText="© 2025 Lumière Skin. All rights reserved."
|
||||
/>
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,59 +1,38 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Halant } from "next/font/google";
|
||||
import { Inter } from "next/font/google";
|
||||
import { Raleway } from "next/font/google";
|
||||
import { Poppins } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { ServiceWrapper } from "@/components/ServiceWrapper";
|
||||
import Tag from "@/tag/Tag";
|
||||
import { ServiceWrapper } from "@/providers/ServiceWrapper";
|
||||
import { Tag } from "@/components/common/Tag";
|
||||
|
||||
const halant = Halant({
|
||||
variable: "--font-halant", subsets: ["latin"],
|
||||
weight: ["300", "400", "500", "600", "700"],
|
||||
});
|
||||
|
||||
const inter = Inter({
|
||||
variable: "--font-inter", subsets: ["latin"],
|
||||
});
|
||||
|
||||
const raleway = Raleway({
|
||||
variable: "--font-raleway", subsets: ["latin"],
|
||||
const poppins = Poppins({
|
||||
variable: "--font-poppins", subsets: ["latin"],
|
||||
weight: ["100", "200", "300", "400", "500", "600", "700", "800", "900"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Lumière Skin | Premium Skincare & Beauty Products", description: "Discover luxurious, science-backed skincare products. Natural ingredients meet dermatology expertise for radiant, healthy skin. Shop our signature collection today.", keywords: "skincare, beauty products, organic skincare, luxury cosmetics, dermatologist tested, natural ingredients, skincare routine", metadataBase: new URL("https://lumiereskin.com"),
|
||||
alternates: {
|
||||
canonical: "https://lumiereskin.com"
|
||||
},
|
||||
openGraph: {
|
||||
title: "Lumière Skin | Premium Skincare Collection", description: "Transform your skin with premium, science-backed skincare. Crafted from organic ingredients for visible results.", url: "https://lumiereskin.com", siteName: "Lumière Skin", type: "website", images: [
|
||||
{
|
||||
url: "http://img.b2bpic.net/free-photo/model-career-kit-still-life-flat-lay_23-2150218023.jpg", alt: "Premium skincare collection"
|
||||
}
|
||||
]
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image", title: "Lumière Skin | Premium Skincare", description: "Discover radiant skin with luxury, dermatologist-tested skincare products.", images: ["http://img.b2bpic.net/free-photo/model-career-kit-still-life-flat-lay_23-2150218023.jpg"]
|
||||
},
|
||||
robots: {
|
||||
index: true,
|
||||
follow: true
|
||||
}
|
||||
};
|
||||
title: "Lumière Skin - Premium Skincare", description: "Radiant, healthy skin starts with premium, science-backed skincare. Discover our luxurious collection crafted for your most beautiful glow."};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
}) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<ServiceWrapper>
|
||||
<body
|
||||
className={`${halant.variable} ${inter.variable} ${raleway.variable} antialiased`}
|
||||
>
|
||||
<body className={`${poppins.variable} antialiased`}>
|
||||
<ServiceWrapper>
|
||||
<Tag />
|
||||
{children}
|
||||
|
||||
</ServiceWrapper>
|
||||
<script
|
||||
async
|
||||
src="https://cdn.jsdelivr.net/npm/gsap@3.12.2/dist/gsap.min.js"
|
||||
/>
|
||||
<script
|
||||
async
|
||||
src="https://cdn.jsdelivr.net/npm/gsap@3.12.2/dist/ScrollTrigger.min.js"
|
||||
/>
|
||||
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
@@ -1421,7 +1400,6 @@ export default function RootLayout({
|
||||
}}
|
||||
/>
|
||||
</body>
|
||||
</ServiceWrapper>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ export default function LandingPage() {
|
||||
{ name: "Products", id: "products" },
|
||||
{ name: "Features", id: "features" },
|
||||
{ name: "Reviews", id: "testimonials" },
|
||||
{ name: "Chef Profile", id: "/chef-profile" },
|
||||
{ name: "Contact", id: "contact" }
|
||||
]}
|
||||
button={{ text: "Shop Now", href: "products" }}
|
||||
|
||||
403
src/app/video-upload/page.tsx
Normal file
403
src/app/video-upload/page.tsx
Normal file
@@ -0,0 +1,403 @@
|
||||
"use client"
|
||||
|
||||
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
|
||||
import NavbarStyleCentered from '@/components/navbar/NavbarStyleCentered/NavbarStyleCentered';
|
||||
import FooterBaseCard from '@/components/sections/footer/FooterBaseCard';
|
||||
import { useState } from "react";
|
||||
import { Upload, CheckCircle, AlertCircle, X } from "lucide-react";
|
||||
|
||||
interface VideoFile {
|
||||
file: File;
|
||||
preview: string;
|
||||
title: string;
|
||||
description: string;
|
||||
chefName: string;
|
||||
duration: number | null;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export default function VideoUploadPage() {
|
||||
const [videos, setVideos] = useState<VideoFile[]>([]);
|
||||
const [uploadProgress, setUploadProgress] = useState<Record<string, number>>({});
|
||||
const [uploadStatus, setUploadStatus] = useState<Record<string, 'success' | 'error' | 'uploading'>>({});
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
|
||||
const MAX_FILE_SIZE = 500 * 1024 * 1024; // 500MB
|
||||
const ALLOWED_FORMATS = ['video/mp4', 'video/quicktime', 'video/x-msvideo', 'video/webm'];
|
||||
const MAX_DURATION = 30 * 60; // 30 minutes in seconds
|
||||
|
||||
const validateVideo = (file: File): { valid: boolean; error?: string } => {
|
||||
if (!ALLOWED_FORMATS.includes(file.type)) {
|
||||
return { valid: false, error: 'Invalid file format. Please upload MP4, MOV, AVI, or WebM.' };
|
||||
}
|
||||
if (file.size > MAX_FILE_SIZE) {
|
||||
return { valid: false, error: 'File size exceeds 500MB limit.' };
|
||||
}
|
||||
return { valid: true };
|
||||
};
|
||||
|
||||
const getVideoDuration = (file: File): Promise<number> => {
|
||||
return new Promise((resolve) => {
|
||||
const video = document.createElement('video');
|
||||
const blob = URL.createObjectURL(file);
|
||||
video.src = blob;
|
||||
video.onloadedmetadata = () => {
|
||||
URL.revokeObjectURL(blob);
|
||||
resolve(video.duration);
|
||||
};
|
||||
video.onerror = () => {
|
||||
URL.revokeObjectURL(blob);
|
||||
resolve(0);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const handleFileSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = e.currentTarget.files;
|
||||
if (!files) return;
|
||||
|
||||
const newVideos: VideoFile[] = [];
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
const validation = validateVideo(file);
|
||||
|
||||
if (!validation.valid) {
|
||||
alert(`${file.name}: ${validation.error}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const duration = await getVideoDuration(file);
|
||||
|
||||
if (duration > MAX_DURATION) {
|
||||
alert(`${file.name}: Video exceeds 30-minute limit (${Math.round(duration / 60)} minutes).`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const preview = URL.createObjectURL(file);
|
||||
newVideos.push({
|
||||
file,
|
||||
preview,
|
||||
title: '',
|
||||
description: '',
|
||||
chefName: '',
|
||||
duration,
|
||||
size: file.size,
|
||||
});
|
||||
}
|
||||
|
||||
setVideos([...videos, ...newVideos]);
|
||||
};
|
||||
|
||||
const updateVideo = (index: number, field: string, value: string) => {
|
||||
const updatedVideos = [...videos];
|
||||
updatedVideos[index] = { ...updatedVideos[index], [field]: value };
|
||||
setVideos(updatedVideos);
|
||||
};
|
||||
|
||||
const removeVideo = (index: number) => {
|
||||
const updatedVideos = videos.filter((_, i) => i !== index);
|
||||
URL.revokeObjectURL(videos[index].preview);
|
||||
setVideos(updatedVideos);
|
||||
};
|
||||
|
||||
const formatFileSize = (bytes: number): string => {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
const formatDuration = (seconds: number | null): string => {
|
||||
if (!seconds) return '0:00';
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = Math.floor(seconds % 60);
|
||||
return `${mins}:${secs < 10 ? '0' : ''}${secs}`;
|
||||
};
|
||||
|
||||
const handleUpload = async (index: number) => {
|
||||
const video = videos[index];
|
||||
|
||||
if (!video.title || !video.chefName) {
|
||||
alert('Please fill in title and chef name.');
|
||||
return;
|
||||
}
|
||||
|
||||
const id = `video-${index}`;
|
||||
setUploadStatus((prev) => ({ ...prev, [id]: 'uploading' }));
|
||||
setUploadProgress((prev) => ({ ...prev, [id]: 0 }));
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', video.file);
|
||||
formData.append('title', video.title);
|
||||
formData.append('description', video.description);
|
||||
formData.append('chefName', video.chefName);
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.upload.addEventListener('progress', (e) => {
|
||||
if (e.lengthComputable) {
|
||||
const percentComplete = (e.loaded / e.total) * 100;
|
||||
setUploadProgress((prev) => ({ ...prev, [id]: Math.round(percentComplete) }));
|
||||
}
|
||||
});
|
||||
|
||||
xhr.addEventListener('load', () => {
|
||||
if (xhr.status === 200 || xhr.status === 201) {
|
||||
setUploadStatus((prev) => ({ ...prev, [id]: 'success' }));
|
||||
setUploadProgress((prev) => ({ ...prev, [id]: 100 }));
|
||||
} else {
|
||||
setUploadStatus((prev) => ({ ...prev, [id]: 'error' }));
|
||||
}
|
||||
});
|
||||
|
||||
xhr.addEventListener('error', () => {
|
||||
setUploadStatus((prev) => ({ ...prev, [id]: 'error' }));
|
||||
});
|
||||
|
||||
xhr.open('POST', '/api/videos/upload');
|
||||
xhr.send(formData);
|
||||
} catch (error) {
|
||||
setUploadStatus((prev) => ({ ...prev, [id]: 'error' }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleUploadAll = async () => {
|
||||
setIsProcessing(true);
|
||||
for (let i = 0; i < videos.length; i++) {
|
||||
await handleUpload(i);
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
}
|
||||
setIsProcessing(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeProvider
|
||||
defaultButtonVariant="hover-bubble"
|
||||
defaultTextAnimation="reveal-blur"
|
||||
borderRadius="rounded"
|
||||
contentWidth="small"
|
||||
sizing="largeSmallSizeLargeTitles"
|
||||
background="circleGradient"
|
||||
cardStyle="gradient-bordered"
|
||||
primaryButtonStyle="double-inset"
|
||||
secondaryButtonStyle="solid"
|
||||
headingFontWeight="extrabold"
|
||||
>
|
||||
<div id="nav" data-section="nav">
|
||||
<NavbarStyleCentered
|
||||
navItems={[
|
||||
{ name: "About", id: "about" },
|
||||
{ name: "Products", id: "products" },
|
||||
{ name: "Features", id: "features" },
|
||||
{ name: "Reviews", id: "testimonials" },
|
||||
{ name: "Upload Video", id: "/video-upload" },
|
||||
{ name: "Contact", id: "contact" }
|
||||
]}
|
||||
button={{ text: "Shop Now", href: "/" }}
|
||||
brandName="Lumière Skin"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="video-upload" data-section="video-upload" className="w-full py-20 px-4 md:px-8">
|
||||
<div className="w-full max-w-4xl mx-auto">
|
||||
<div className="mb-12 text-center">
|
||||
<h1 className="text-4xl md:text-5xl font-bold mb-4 text-foreground">Share Your Cooking Video</h1>
|
||||
<p className="text-lg text-foreground/75 mb-2">Help our community by uploading your favorite cooking recipe or technique videos</p>
|
||||
<p className="text-sm text-foreground/50">Maximum file size: 500MB | Maximum duration: 30 minutes | Formats: MP4, MOV, AVI, WebM</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-card rounded-lg border border-primary-cta/20 p-8 mb-8">
|
||||
<label className="block mb-6">
|
||||
<div className="flex items-center justify-center w-full h-48 border-2 border-dashed border-primary-cta/30 rounded-lg cursor-pointer hover:border-primary-cta/50 transition">
|
||||
<div className="flex flex-col items-center justify-center pt-5 pb-6">
|
||||
<Upload className="w-12 h-12 text-primary-cta mb-2" />
|
||||
<p className="text-sm text-foreground/75"><span className="font-semibold">Click to upload</span> or drag and drop</p>
|
||||
<p className="text-xs text-foreground/50 mt-1">MP4, MOV, AVI or WebM</p>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="file"
|
||||
multiple
|
||||
accept="video/mp4,video/quicktime,video/x-msvideo,video/webm"
|
||||
onChange={handleFileSelect}
|
||||
className="hidden"
|
||||
disabled={isProcessing}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{videos.length > 0 && (
|
||||
<div className="space-y-6">
|
||||
{videos.map((video, index) => {
|
||||
const id = `video-${index}`;
|
||||
const status = uploadStatus[id];
|
||||
const progress = uploadProgress[id] || 0;
|
||||
|
||||
return (
|
||||
<div key={index} className="bg-card rounded-lg border border-primary-cta/20 p-6 overflow-hidden">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||
<div className="md:col-span-1">
|
||||
<video
|
||||
src={video.preview}
|
||||
className="w-full h-32 object-cover rounded-lg bg-background"
|
||||
/>
|
||||
<p className="text-xs text-foreground/50 mt-2">Duration: {formatDuration(video.duration)}</p>
|
||||
<p className="text-xs text-foreground/50">Size: {formatFileSize(video.size)}</p>
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-3">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-1">Video Title</label>
|
||||
<input
|
||||
type="text"
|
||||
value={video.title}
|
||||
onChange={(e) => updateVideo(index, 'title', e.target.value)}
|
||||
placeholder="Enter video title"
|
||||
className="w-full px-3 py-2 border border-primary-cta/20 rounded-lg bg-background text-foreground placeholder-foreground/50 focus:outline-none focus:border-primary-cta"
|
||||
disabled={status === 'uploading' || status === 'success'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-1">Chef Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={video.chefName}
|
||||
onChange={(e) => updateVideo(index, 'chefName', e.target.value)}
|
||||
placeholder="Your name"
|
||||
className="w-full px-3 py-2 border border-primary-cta/20 rounded-lg bg-background text-foreground placeholder-foreground/50 focus:outline-none focus:border-primary-cta"
|
||||
disabled={status === 'uploading' || status === 'success'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-1">Description (Optional)</label>
|
||||
<textarea
|
||||
value={video.description}
|
||||
onChange={(e) => updateVideo(index, 'description', e.target.value)}
|
||||
placeholder="Add a description for your video"
|
||||
rows={3}
|
||||
className="w-full px-3 py-2 border border-primary-cta/20 rounded-lg bg-background text-foreground placeholder-foreground/50 focus:outline-none focus:border-primary-cta resize-none"
|
||||
disabled={status === 'uploading' || status === 'success'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{status === 'uploading' && (
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-sm text-foreground/75">Uploading...</span>
|
||||
<span className="text-sm font-semibold text-primary-cta">{progress}%</span>
|
||||
</div>
|
||||
<div className="w-full h-2 bg-background rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-primary-cta transition-all duration-300"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{status === 'success' && (
|
||||
<div className="flex items-center gap-2 p-3 rounded-lg bg-green-500/10 border border-green-500/20">
|
||||
<CheckCircle className="w-5 h-5 text-green-500" />
|
||||
<span className="text-sm text-green-600">Upload successful!</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{status === 'error' && (
|
||||
<div className="flex items-center gap-2 p-3 rounded-lg bg-red-500/10 border border-red-500/20">
|
||||
<AlertCircle className="w-5 h-5 text-red-500" />
|
||||
<span className="text-sm text-red-600">Upload failed. Please try again.</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-3 pt-2">
|
||||
<button
|
||||
onClick={() => handleUpload(index)}
|
||||
disabled={status === 'uploading' || status === 'success' || isProcessing}
|
||||
className="flex-1 px-4 py-2 bg-primary-cta text-white rounded-lg font-medium hover:opacity-90 disabled:opacity-50 transition"
|
||||
>
|
||||
{status === 'success' ? 'Uploaded' : status === 'error' ? 'Retry' : 'Upload'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => removeVideo(index)}
|
||||
disabled={status === 'uploading' || status === 'success'}
|
||||
className="px-4 py-2 bg-red-500/10 text-red-600 rounded-lg font-medium hover:bg-red-500/20 disabled:opacity-50 transition"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{videos.some((_, i) => !uploadStatus[`video-${i}`] || uploadStatus[`video-${i}`] === 'error') && (
|
||||
<button
|
||||
onClick={handleUploadAll}
|
||||
disabled={isProcessing}
|
||||
className="w-full px-6 py-3 bg-primary-cta text-white rounded-lg font-semibold hover:opacity-90 disabled:opacity-50 transition"
|
||||
>
|
||||
{isProcessing ? 'Uploading All Videos...' : 'Upload All Videos'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{videos.length === 0 && (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-foreground/50">No videos selected yet. Upload your cooking videos to get started!</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="footer" data-section="footer" className="mt-20">
|
||||
<FooterBaseCard
|
||||
logoText="Lumière Skin"
|
||||
columns={[
|
||||
{
|
||||
title: "Shop", items: [
|
||||
{ label: "All Products", href: "#products" },
|
||||
{ label: "Skincare Routine", href: "#products" },
|
||||
{ label: "Collections", href: "#products" },
|
||||
{ label: "Gift Sets", href: "#products" }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Company", items: [
|
||||
{ label: "About Us", href: "#about" },
|
||||
{ label: "Our Story", href: "#about" },
|
||||
{ label: "Sustainability", href: "#" },
|
||||
{ label: "Careers", href: "#" }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Support", items: [
|
||||
{ label: "Contact Us", href: "#contact" },
|
||||
{ label: "FAQ", href: "#faq" },
|
||||
{ label: "Shipping Info", href: "#" },
|
||||
{ label: "Returns", href: "#" }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Connect", items: [
|
||||
{ label: "Instagram", href: "https://instagram.com" },
|
||||
{ label: "Facebook", href: "https://facebook.com" },
|
||||
{ label: "Pinterest", href: "https://pinterest.com" },
|
||||
{ label: "TikTok", href: "https://tiktok.com" }
|
||||
]
|
||||
}
|
||||
]}
|
||||
copyrightText="© 2025 Lumière Skin. All rights reserved."
|
||||
/>
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
185
src/app/videos/page.tsx
Normal file
185
src/app/videos/page.tsx
Normal file
@@ -0,0 +1,185 @@
|
||||
"use client"
|
||||
|
||||
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
|
||||
import NavbarStyleCentered from '@/components/navbar/NavbarStyleCentered/NavbarStyleCentered';
|
||||
import HeroOverlay from '@/components/sections/hero/HeroOverlay';
|
||||
import BlogCardThree from '@/components/sections/blog/BlogCardThree';
|
||||
import FooterBaseCard from '@/components/sections/footer/FooterBaseCard';
|
||||
import { useState } from 'react';
|
||||
import { Search } from 'lucide-react';
|
||||
import Input from '@/components/form/Input';
|
||||
|
||||
export default function VideosPage() {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [selectedCategory, setSelectedCategory] = useState('all');
|
||||
|
||||
const allVideos = [
|
||||
{
|
||||
id: "1", category: "French Cuisine", title: "Classic French Techniques", excerpt: "Learn the essential knife skills and techniques used by professional chefs in French cuisine. Master julienne, brunoise, and chiffonade cuts.", imageSrc: "http://img.b2bpic.net/free-photo/chef-preparing-dish_23-2148241892.jpg", imageAlt: "Chef preparing French dish", authorName: "Chef Marie Dubois", authorAvatar: "http://img.b2bpic.net/free-photo/woman-smiles-toothily-keeps-palms-pressed-together-looks-away-joyfully-wears-casual-t-shirt-isolated-beige-copy-space-away-positive-emotions-concept_273609-56517.jpg", date: "Jan 20, 2025"
|
||||
},
|
||||
{
|
||||
id: "2", category: "Mediterranean", title: "Mediterranean Summer Salad", excerpt: "Discover how to prepare fresh, vibrant Mediterranean dishes with seasonal ingredients. Fresh vegetables, feta, and olive oil combinations.", imageSrc: "http://img.b2bpic.net/free-photo/food-photography-salad_23-2148241893.jpg", imageAlt: "Mediterranean salad", authorName: "Chef Antonio Rivera", authorAvatar: "http://img.b2bpic.net/free-photo/young-beautiful-woman-having-online-meeting_23-2149116347.jpg", date: "Jan 18, 2025"
|
||||
},
|
||||
{
|
||||
id: "3", category: "Baking", title: "Artisan Bread Making", excerpt: "Master the art of baking traditional sourdough bread with proper fermentation techniques. Create perfect crusts and fluffy interiors.", imageSrc: "http://img.b2bpic.net/free-photo/baking-bread_23-2148241894.jpg", imageAlt: "Artisan bread", authorName: "Chef Sophie Laurent", authorAvatar: "http://img.b2bpic.net/free-photo/young-women-polishing-nails-bed_23-2147770248.jpg", date: "Jan 15, 2025"
|
||||
},
|
||||
{
|
||||
id: "4", category: "Asian Cuisine", title: "Wok Mastery for Beginners", excerpt: "Learn the fundamentals of wok cooking including proper heat management and ingredient timing for authentic Asian flavors.", imageSrc: "http://img.b2bpic.net/free-photo/cooking-asian-food_23-2148241895.jpg", imageAlt: "Asian wok cooking", authorName: "Chef Lin Chen", authorAvatar: "http://img.b2bpic.net/free-photo/close-perfection-closeup-peaceful-relaxed-redhead-happy-woman-closed-eyes-pure-delighted-smile-sh_1258-139766.jpg", date: "Jan 12, 2025"
|
||||
},
|
||||
{
|
||||
id: "5", category: "Desserts", title: "Chocolate Tempering Secrets", excerpt: "Unlock the secrets of professional chocolate tempering for glossy, smooth finishes on your desserts and confections.", imageSrc: "http://img.b2bpic.net/free-photo/chocolate-dessert_23-2148241896.jpg", imageAlt: "Chocolate tempering", authorName: "Chef Laurent Petit", authorAvatar: "http://img.b2bpic.net/free-photo/woman-smiles-toothily-keeps-palms-pressed-together-looks-away-joyfully-wears-casual-t-shirt-isolated-beige-copy-space-away-positive-emotions-concept_273609-56517.jpg", date: "Jan 10, 2025"
|
||||
},
|
||||
{
|
||||
id: "6", category: "French Cuisine", title: "Perfect Béarnaise Sauce", excerpt: "Create the classic French béarnaise sauce from scratch with proper emulsification techniques and flavor balancing.", imageSrc: "http://img.b2bpic.net/free-photo/sauce-preparation_23-2148241897.jpg", imageAlt: "Béarnaise sauce", authorName: "Chef Marie Dubois", authorAvatar: "http://img.b2bpic.net/free-photo/woman-smiles-toothily-keeps-palms-pressed-together-looks-away-joyfully-wears-casual-t-shirt-isolated-beige-copy-space-away-positive-emotions-concept_273609-56517.jpg", date: "Jan 8, 2025"
|
||||
}
|
||||
];
|
||||
|
||||
const categories = ['all', 'French Cuisine', 'Mediterranean', 'Baking', 'Asian Cuisine', 'Desserts'];
|
||||
|
||||
const filteredVideos = allVideos.filter(video => {
|
||||
const matchesSearch = video.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
video.excerpt.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
video.authorName.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
const matchesCategory = selectedCategory === 'all' || video.category === selectedCategory;
|
||||
return matchesSearch && matchesCategory;
|
||||
});
|
||||
|
||||
return (
|
||||
<ThemeProvider
|
||||
defaultButtonVariant="hover-bubble"
|
||||
defaultTextAnimation="reveal-blur"
|
||||
borderRadius="rounded"
|
||||
contentWidth="small"
|
||||
sizing="largeSmallSizeLargeTitles"
|
||||
background="circleGradient"
|
||||
cardStyle="gradient-bordered"
|
||||
primaryButtonStyle="double-inset"
|
||||
secondaryButtonStyle="solid"
|
||||
headingFontWeight="extrabold"
|
||||
>
|
||||
<div id="nav" data-section="nav">
|
||||
<NavbarStyleCentered
|
||||
navItems={[
|
||||
{ name: "Home", id: "/" },
|
||||
{ name: "About", id: "#about" },
|
||||
{ name: "Products", id: "#products" },
|
||||
{ name: "Features", id: "#features" },
|
||||
{ name: "Contact", id: "#contact" }
|
||||
]}
|
||||
button={{ text: "Shop Now", href: "/" }}
|
||||
brandName="Lumière Skin"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="hero" data-section="hero">
|
||||
<HeroOverlay
|
||||
title="Cooking Videos & Tutorials"
|
||||
description="Explore our collection of culinary expertise from world-class chefs. Learn techniques, recipes, and cooking secrets."
|
||||
tag="Video Gallery"
|
||||
textPosition="center"
|
||||
showDimOverlay={true}
|
||||
buttons={[
|
||||
{ text: "Browse Videos", href: "#videos" }
|
||||
]}
|
||||
imageSrc="http://img.b2bpic.net/free-photo/chef-preparing-dish_23-2148241892.jpg"
|
||||
imageAlt="Professional chef cooking"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="filters" data-section="filters" className="relative py-20 px-4">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="mb-8">
|
||||
<h2 className="text-3xl font-bold mb-4">Search & Filter Videos</h2>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex gap-2">
|
||||
<Search className="w-5 h-5 opacity-50 self-center" />
|
||||
<Input
|
||||
value={searchQuery}
|
||||
onChange={setSearchQuery}
|
||||
placeholder="Search by title, chef name, or topic..."
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{categories.map(category => (
|
||||
<button
|
||||
key={category}
|
||||
onClick={() => setSelectedCategory(category)}
|
||||
className={`px-4 py-2 rounded-full transition ${
|
||||
selectedCategory === category
|
||||
? 'bg-primary-cta text-white'
|
||||
: 'bg-secondary-cta/30 hover:bg-secondary-cta/50'
|
||||
}`}
|
||||
>
|
||||
{category.charAt(0).toUpperCase() + category.slice(1)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-sm opacity-75">Found {filteredVideos.length} video{filteredVideos.length !== 1 ? 's' : ''}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="videos" data-section="videos">
|
||||
{filteredVideos.length > 0 ? (
|
||||
<BlogCardThree
|
||||
blogs={filteredVideos}
|
||||
title="Featured Cooking Videos"
|
||||
description="Watch expert chefs share their culinary techniques and recipes in our video gallery."
|
||||
gridVariant="three-columns-all-equal-width"
|
||||
animationType="slide-up"
|
||||
textboxLayout="default"
|
||||
useInvertedBackground={false}
|
||||
tag="Videos"
|
||||
/>
|
||||
) : (
|
||||
<div className="py-20 text-center">
|
||||
<p className="text-xl opacity-75">No videos found matching your search. Try different keywords or categories.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div id="footer" data-section="footer">
|
||||
<FooterBaseCard
|
||||
logoText="Lumière Skin"
|
||||
columns={[
|
||||
{
|
||||
title: "Shop", items: [
|
||||
{ label: "All Products", href: "/#products" },
|
||||
{ label: "Skincare Routine", href: "/#products" },
|
||||
{ label: "Collections", href: "/#products" },
|
||||
{ label: "Gift Sets", href: "/#products" }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Company", items: [
|
||||
{ label: "About Us", href: "/#about" },
|
||||
{ label: "Our Story", href: "/#about" },
|
||||
{ label: "Sustainability", href: "#" },
|
||||
{ label: "Careers", href: "#" }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Support", items: [
|
||||
{ label: "Contact Us", href: "/#contact" },
|
||||
{ label: "FAQ", href: "/#faq" },
|
||||
{ label: "Shipping Info", href: "#" },
|
||||
{ label: "Returns", href: "#" }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Connect", items: [
|
||||
{ label: "Instagram", href: "https://instagram.com" },
|
||||
{ label: "Facebook", href: "https://facebook.com" },
|
||||
{ label: "Pinterest", href: "https://pinterest.com" },
|
||||
{ label: "TikTok", href: "https://tiktok.com" }
|
||||
]
|
||||
}
|
||||
]}
|
||||
copyrightText="© 2025 Lumière Skin. All rights reserved."
|
||||
/>
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user