10 Commits

Author SHA1 Message Date
8a951092b0 Update src/app/layout.tsx 2026-03-08 08:40:46 +00:00
cff8aa84d5 Update src/app/videos/page.tsx 2026-03-08 08:38:27 +00:00
49f4f4a6a3 Update src/app/video-upload/page.tsx 2026-03-08 08:38:27 +00:00
a545a8edbe Update src/app/page.tsx 2026-03-08 08:38:26 +00:00
d6c2f75b2c Update src/app/layout.tsx 2026-03-08 08:38:25 +00:00
5a69f33498 Add src/app/dashboard/page.tsx 2026-03-08 08:38:25 +00:00
21a4b6a64a Update src/app/chef-profile/page.tsx 2026-03-08 08:38:24 +00:00
b97270b14e Add src/app/api/upload-video/route.ts 2026-03-08 08:38:23 +00:00
9887b9eded Update src/app/videos/page.tsx 2026-03-08 08:35:50 +00:00
2fa8fa3b0d Update src/app/layout.tsx 2026-03-08 08:35:49 +00:00
7 changed files with 855 additions and 676 deletions

View File

@@ -0,0 +1,72 @@
import { NextRequest, NextResponse } from 'next/server';
const MAX_FILE_SIZE = 500 * 1024 * 1024; // 500MB
const ALLOWED_FORMATS = ['video/mp4', 'video/mpeg', 'video/quicktime', 'video/x-msvideo'];
export async function POST(request: NextRequest) {
try {
const formData = await request.formData();
const file = formData.get('file') as File;
const title = formData.get('title') as string;
const description = formData.get('description') as string;
// Validation
if (!file) {
return NextResponse.json(
{ message: 'No file provided' },
{ status: 400 }
);
}
if (!title || title.trim().length === 0) {
return NextResponse.json(
{ message: 'Video title is required' },
{ status: 400 }
);
}
if (file.size > MAX_FILE_SIZE) {
return NextResponse.json(
{ message: `File size exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB limit` },
{ status: 413 }
);
}
if (!ALLOWED_FORMATS.includes(file.type)) {
return NextResponse.json(
{ message: 'Invalid file format. Supported: MP4, MPEG, MOV, AVI' },
{ status: 415 }
);
}
// TODO: Implement actual file storage
// This could be:
// 1. Cloud storage (AWS S3, Google Cloud Storage, etc.)
// 2. Local file system (with proper permissions)
// 3. Database blob storage
// 4. CDN integration
// For now, return success response
return NextResponse.json(
{
success: true,
message: 'Video uploaded successfully',
video: {
title,
description,
fileName: file.name,
fileSize: file.size,
mimeType: file.type,
uploadedAt: new Date().toISOString(),
},
},
{ status: 201 }
);
} catch (error) {
console.error('Video upload error:', error);
return NextResponse.json(
{ message: 'Internal server error' },
{ status: 500 }
);
}
}

View File

@@ -1,66 +1,15 @@
"use client" "use client";
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider"; import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
import NavbarStyleCentered from '@/components/navbar/NavbarStyleCentered/NavbarStyleCentered'; import NavbarStyleCentered from '@/components/navbar/NavbarStyleCentered/NavbarStyleCentered';
import HeroLogoBillboard from '@/components/sections/hero/HeroLogoBillboard'; import HeroLogoBillboard from '@/components/sections/hero/HeroLogoBillboard';
import TeamCardOne from '@/components/sections/team/TeamCardOne';
import MetricCardThree from '@/components/sections/metrics/MetricCardThree';
import TestimonialCardThirteen from '@/components/sections/testimonial/TestimonialCardThirteen';
import FooterBaseCard from '@/components/sections/footer/FooterBaseCard'; import FooterBaseCard from '@/components/sections/footer/FooterBaseCard';
import { useState } from "react"; import { Star, Users, Heart, Mail } from "lucide-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() { 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 ( return (
<ThemeProvider <ThemeProvider
defaultButtonVariant="hover-bubble" defaultButtonVariant="hover-bubble"
@@ -80,256 +29,140 @@ export default function ChefProfilePage() {
{ name: "Home", id: "/" }, { name: "Home", id: "/" },
{ name: "About", id: "about" }, { name: "About", id: "about" },
{ name: "Products", id: "products" }, { name: "Products", id: "products" },
{ name: "Features", id: "features" },
{ name: "Reviews", id: "testimonials" },
{ name: "Contact", id: "contact" } { name: "Contact", id: "contact" }
]} ]}
button={{ text: "Shop Now", href: "/" }} button={{ text: "Follow Chef", href: "#" }}
brandName="Lumière Skin" brandName="Chef Profile"
/> />
</div> </div>
<div id="hero" data-section="hero"> <div id="chef-info" data-section="chef-info">
<HeroLogoBillboard <HeroLogoBillboard
logoText="Chef Profile & Video Management" logoText="Chef Marcus"
description="Manage and showcase your video content. Track performance, edit details, and grow your audience." description="Award-winning culinary expert with 15+ years of experience in modern gastronomy. Specializing in innovative techniques and sustainable ingredient sourcing."
buttons={[ buttons={[
{ text: "Upload New Video", href: "#" }, { text: "View Video History", href: "#videos" },
{ text: "Back to Home", href: "/" } { text: "Subscribe", href: "#" }
]} ]}
background={{ variant: "sparkles-gradient" }} background={{ variant: "sparkles-gradient" }}
imageSrc="http://img.b2bpic.net/free-photo/woman-applying-moisturizer-her-beauty-routine_23-2150166464.jpg" imageSrc="http://img.b2bpic.net/free-photo/portrait-successful-man-chef-wearing-hat-standing-against-blurred-kitchen_23-2148381812.jpg"
imageAlt="Chef profile" imageAlt="Chef Marcus Professional Portrait"
mediaAnimation="slide-up" mediaAnimation="slide-up"
frameStyle="card" frameStyle="card"
buttonAnimation="blur-reveal" buttonAnimation="blur-reveal"
/> />
</div> </div>
<section className="py-20 px-4"> <div id="stats" data-section="stats">
<div className="max-w-7xl mx-auto"> <MetricCardThree
{/* Stats Section */} metrics={[
<div className="mb-12 grid grid-cols-1 md:grid-cols-3 gap-6"> { id: "1", icon: Users, title: "Followers", value: "245.8K" },
<div className="p-6 rounded-lg bg-card border border-card"> { id: "2", icon: Heart, title: "Engagement", value: "98.5%" },
<h3 className="text-sm font-semibold text-foreground/60 mb-2">Total Videos</h3> { id: "3", icon: Star, title: "Chef Rating", value: "4.9/5" },
<p className="text-4xl font-bold text-foreground">{videos.length}</p> { id: "4", icon: Mail, title: "Videos", value: "156" }
</div> ]}
<div className="p-6 rounded-lg bg-card border border-card"> title="Chef Performance"
<h3 className="text-sm font-semibold text-foreground/60 mb-2">Total Views</h3> description="Outstanding metrics showcasing chef excellence and community engagement."
<p className="text-4xl font-bold text-foreground">{totalViews.toLocaleString()}</p> textboxLayout="default"
</div> animationType="slide-up"
<div className="p-6 rounded-lg bg-card border border-card"> useInvertedBackground={false}
<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>
</div>
{/* Published Videos */} <div id="videos" data-section="videos">
<div className="mb-12"> <TeamCardOne
<h2 className="text-2xl font-bold text-foreground mb-6">Published Videos</h2> members={[
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> {
{publishedVideos.map(video => ( id: "1", name: "Mastering Sauces", role: "Cooking Technique", imageSrc: "http://img.b2bpic.net/free-photo/professional-chef-preparing-delicious-food-kitchen-restaurant_23-2150887345.jpg", imageAlt: "Mastering Sauces Tutorial"
<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"> id: "2", name: "Plating Perfection", role: "Presentation Guide", imageSrc: "http://img.b2bpic.net/free-photo/chef-preparing-gourmet-dish-plating_23-2150887201.jpg", imageAlt: "Plating Perfection Workshop"
<img },
src={video.thumbnail} {
alt={video.title} id: "3", name: "Farm to Table", role: "Sustainability Focus", imageSrc: "http://img.b2bpic.net/free-photo/fresh-vegetables-farm-market_23-2150887102.jpg", imageAlt: "Farm to Table Preparation"
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"> id: "4", name: "Molecular Gastronomy", role: "Advanced Techniques", imageSrc: "http://img.b2bpic.net/free-photo/innovative-cooking-laboratory-style_23-2150887456.jpg", imageAlt: "Molecular Gastronomy Demo"
<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"> title="Recent Video History"
{video.duration} description="Explore Chef Marcus's latest cooking tutorials and technique demonstrations."
</div> gridVariant="two-columns-alternating-heights"
</div> animationType="slide-up"
textboxLayout="default"
useInvertedBackground={false}
/>
</div>
{/* Content */} <div id="ratings" data-section="ratings">
<div className="p-4"> <TestimonialCardThirteen
{editingId === video.id ? ( testimonials={[
<div className="space-y-3"> {
<input id: "1", name: "Emma Wilson", handle: "@emmachef", testimonial: "Chef Marcus's tutorials completely transformed my cooking skills. His attention to detail and passion for food is truly inspiring!", rating: 5,
type="text" icon: Star
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" id: "2", name: "James Chen", handle: "@jameskit", testimonial: "The molecular gastronomy series is mind-blowing. Professional, educational, and entertaining. Highly recommend!", rating: 5,
/> icon: Star
<textarea },
value={editData.description} {
onChange={(e) => setEditData({...editData, description: e.target.value})} id: "3", name: "Sarah Johnson", handle: "@foodlover_sarah", testimonial: "I've learned more from Chef Marcus in two weeks than I did in a year of other cooking channels. Pure expertise!", rating: 5,
className="w-full bg-background border border-foreground/20 rounded px-2 py-1 text-foreground text-xs resize-none h-16" icon: Star
/> },
<div className="flex gap-2"> {
<button id: "4", name: "Michael Rodriguez", handle: "@mikecooking", testimonial: "The farm-to-table episode changed how I think about ingredient sourcing. Best culinary content creator out there.", rating: 5,
onClick={() => handleSaveEdit(video.id)} icon: Star
className="flex-1 bg-primary-cta text-background px-3 py-1 rounded text-sm font-medium hover:opacity-90" }
> ]}
Save showRating={true}
</button> title="Community Ratings & Reviews"
<button description="See what followers love most about Chef Marcus's content and expertise."
onClick={() => setEditingId(null)} textboxLayout="default"
className="flex-1 bg-secondary-cta text-foreground px-3 py-1 rounded text-sm font-medium hover:opacity-90" animationType="slide-up"
> useInvertedBackground={false}
Cancel />
</button> </div>
</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"> <div id="footer" data-section="footer">
<FooterBaseCard <FooterBaseCard
logoText="Lumière Skin" logoText="Chef Marcus"
columns={[ columns={[
{ {
title: "Shop", items: [ title: "Content", items: [
{ label: "All Products", href: "/#products" }, { label: "Cooking Tutorials", href: "#videos" },
{ label: "Skincare Routine", href: "/#products" }, { label: "Recipe Collections", href: "#" },
{ label: "Collections", href: "/#products" }, { label: "Technique Guides", href: "#" },
{ label: "Gift Sets", href: "/#products" } { label: "Live Streams", href: "#" }
] ]
}, },
{ {
title: "Company", items: [ title: "About", items: [
{ label: "About Us", href: "/#about" }, { label: "Bio", href: "#chef-info" },
{ label: "Our Story", href: "/#about" }, { label: "Achievements", href: "#" },
{ label: "Sustainability", href: "#" }, { label: "Experience", href: "#" },
{ label: "Careers", href: "#" } { label: "Philosophy", href: "#" }
]
},
{
title: "Support", items: [
{ label: "Contact Us", href: "/#contact" },
{ label: "FAQ", href: "/#faq" },
{ label: "Shipping Info", href: "#" },
{ label: "Returns", href: "#" }
] ]
}, },
{ {
title: "Connect", items: [ title: "Connect", items: [
{ label: "YouTube", href: "https://youtube.com" },
{ label: "Instagram", href: "https://instagram.com" }, { label: "Instagram", href: "https://instagram.com" },
{ label: "Facebook", href: "https://facebook.com" }, { label: "TikTok", href: "https://tiktok.com" },
{ label: "Pinterest", href: "https://pinterest.com" }, { label: "Twitter", href: "https://twitter.com" }
{ label: "TikTok", href: "https://tiktok.com" } ]
},
{
title: "Resources", items: [
{ label: "Merchandise", href: "#" },
{ label: "Cookbooks", href: "#" },
{ label: "Collaborations", href: "#" },
{ label: "Contact", href: "#" }
] ]
} }
]} ]}
copyrightText="© 2025 Lumière Skin. All rights reserved." copyrightText="© 2025 Chef Marcus. All rights reserved. Culinary Excellence Worldwide."
/> />
</div> </div>
</ThemeProvider> </ThemeProvider>

263
src/app/dashboard/page.tsx Normal file
View File

@@ -0,0 +1,263 @@
"use client";
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
import NavbarStyleCentered from '@/components/navbar/NavbarStyleCentered/NavbarStyleCentered';
import HeroLogoBillboard from '@/components/sections/hero/HeroLogoBillboard';
import FeatureBento from '@/components/sections/feature/FeatureBento';
import FooterBaseCard from '@/components/sections/footer/FooterBaseCard';
import { Video, Upload, Edit3, Trash2, Eye, Settings } from "lucide-react";
import { useState } from "react";
interface DashboardVideo {
id: string;
title: string;
thumbnail: string;
duration: string;
uploadDate: string;
views: string;
status: 'published' | 'draft' | 'processing';
}
export default function Dashboard() {
const [videos, setVideos] = useState<DashboardVideo[]>([
{
id: '1',
title: 'Michelin-Star Pasta Carbonara Masterclass',
thumbnail: 'http://img.b2bpic.net/free-photo/woman-applying-moisturizer-her-beauty-routine_23-2150166464.jpg',
duration: '12:45',
uploadDate: 'Jan 15, 2025',
views: '2.4K',
status: 'published'
},
{
id: '2',
title: 'Advanced Plating Techniques for Fine Dining',
thumbnail: 'http://img.b2bpic.net/free-photo/model-career-kit-still-life-flat-lay_23-2150218023.jpg',
duration: '18:30',
uploadDate: 'Jan 12, 2025',
views: '1.8K',
status: 'published'
},
{
id: '3',
title: 'Sustainable Sourcing for Restaurant Kitchens',
thumbnail: 'http://img.b2bpic.net/free-photo/people-working-elegant-cozy-office-space_23-2149548692.jpg',
duration: '15:20',
uploadDate: 'Jan 10, 2025',
views: '892',
status: 'draft'
}
]);
const handleEdit = (id: string) => {
console.log('Edit video:', id);
};
const handleDelete = (id: string) => {
setVideos(videos.filter(v => v.id !== id));
};
const handleUpload = () => {
console.log('Upload new video');
};
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: "My Videos", id: "/dashboard" },
{ name: "Analytics", id: "#" },
{ name: "Settings", id: "#" },
{ name: "Support", id: "#" }
]}
button={{ text: "Upload Video", href: "#" }}
brandName="Chef Studio"
/>
</div>
<div id="hero" data-section="hero">
<HeroLogoBillboard
logoText="Chef Studio"
description="Manage your culinary videos with professional tools. Upload, edit, and share your cooking expertise with the world."
buttons={[
{ text: "Upload New Video", onClick: handleUpload },
{ text: "View Analytics", href: "#" }
]}
background={{ variant: "sparkles-gradient" }}
imageSrc="http://img.b2bpic.net/free-photo/model-career-kit-still-life-flat-lay_23-2150218023.jpg"
imageAlt="Chef Studio Dashboard"
mediaAnimation="slide-up"
frameStyle="card"
buttonAnimation="blur-reveal"
/>
</div>
<div id="features" data-section="features">
<FeatureBento
title="Powerful Video Management Tools"
description="Everything you need to create, manage, and grow your culinary content library."
features={[
{
title: "Upload & Organize", description: "Upload videos in multiple formats with automatic processing and organization", bentoComponent: "icon-info-cards", items: [
{ icon: Upload, label: "Formats", value: "4K Ready" },
{ icon: Video, label: "Duration", value: "Unlimited" },
{ icon: Settings, label: "Quality", value: "Auto-Optimize" }
]
},
{
title: "Edit & Customize", description: "Built-in editing tools to trim, add captions, and personalize your videos", bentoComponent: "icon-info-cards", items: [
{ icon: Edit3, label: "Trim", value: "Frame-Precise" },
{ icon: Video, label: "Effects", value: "50+ Built-in" },
{ icon: Eye, label: "Preview", value: "Real-Time" }
]
},
{
title: "Track Analytics", description: "Monitor views, engagement, and audience growth with detailed metrics", bentoComponent: "animated-bar-chart"
},
{
title: "Publish & Share", description: "Share your videos across multiple platforms with one click", bentoComponent: "icon-info-cards", items: [
{ icon: Video, label: "Platforms", value: "10+" },
{ icon: Settings, label: "Scheduling", value: "Auto-Post" },
{ icon: Eye, label: "Analytics", value: "Real-Time" }
]
}
]}
animationType="slide-up"
textboxLayout="default"
useInvertedBackground={false}
/>
</div>
<div id="videos" data-section="videos" className="py-20">
<div className="container mx-auto px-4 max-w-6xl">
<div className="mb-12">
<h2 className="text-4xl font-bold mb-4">Your Video Library</h2>
<p className="text-lg text-gray-600">Manage and monitor your uploaded culinary content</p>
</div>
<div className="grid gap-6">
{videos.map((video) => (
<div
key={video.id}
className="bg-white rounded-lg shadow-lg overflow-hidden hover:shadow-xl transition-shadow"
>
<div className="flex flex-col md:flex-row">
{/* Thumbnail */}
<div className="md:w-48 flex-shrink-0 relative group">
<img
src={video.thumbnail}
alt={video.title}
className="w-full h-48 object-cover"
/>
<div className="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-50 transition-all flex items-center justify-center">
<Video className="w-12 h-12 text-white opacity-0 group-hover:opacity-100 transition-opacity" />
</div>
<div className="absolute bottom-2 right-2 bg-black bg-opacity-75 text-white text-sm px-2 py-1 rounded">
{video.duration}
</div>
</div>
{/* Content */}
<div className="flex-1 p-6 flex flex-col justify-between">
<div>
<div className="flex items-start justify-between mb-2">
<h3 className="text-xl font-bold">{video.title}</h3>
<span
className={`px-3 py-1 rounded-full text-xs font-semibold ${
video.status === 'published'
? 'bg-green-100 text-green-800'
: video.status === 'draft'
? 'bg-yellow-100 text-yellow-800'
: 'bg-blue-100 text-blue-800'
}`}
>
{video.status.charAt(0).toUpperCase() + video.status.slice(1)}
</span>
</div>
<div className="flex gap-6 text-sm text-gray-600">
<span>📅 {video.uploadDate}</span>
<span>👁 {video.views} views</span>
</div>
</div>
{/* Actions */}
<div className="flex gap-2 mt-4">
<button
onClick={() => handleEdit(video.id)}
className="flex items-center gap-2 px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
>
<Edit3 size={16} />
Edit
</button>
<button
onClick={() => handleDelete(video.id)}
className="flex items-center gap-2 px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors"
>
<Trash2 size={16} />
Delete
</button>
</div>
</div>
</div>
</div>
))}
</div>
</div>
</div>
<div id="footer" data-section="footer">
<FooterBaseCard
logoText="Chef Studio"
columns={[
{
title: "Dashboard", items: [
{ label: "My Videos", href: "/dashboard" },
{ label: "Analytics", href: "#" },
{ label: "Settings", href: "#" },
{ label: "Upload", href: "#" }
]
},
{
title: "Resources", items: [
{ label: "Help Center", href: "#" },
{ label: "Guidelines", href: "#" },
{ label: "Best Practices", href: "#" },
{ label: "FAQ", href: "#" }
]
},
{
title: "Account", items: [
{ label: "Profile", href: "#" },
{ label: "Preferences", href: "#" },
{ label: "Billing", href: "#" },
{ label: "Logout", href: "#" }
]
},
{
title: "Connect", items: [
{ label: "Twitter", href: "https://twitter.com" },
{ label: "Instagram", href: "https://instagram.com" },
{ label: "YouTube", href: "https://youtube.com" },
{ label: "TikTok", href: "https://tiktok.com" }
]
}
]}
copyrightText="© 2025 Chef Studio. All rights reserved."
/>
</div>
</ThemeProvider>
);
}

View File

@@ -1,16 +1,17 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { Poppins } from "next/font/google"; import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css"; import "./globals.css";
import { ServiceWrapper } from "@/providers/ServiceWrapper";
import { Tag } from "@/components/common/Tag";
const poppins = Poppins({ const geist = Geist({
variable: "--font-poppins", subsets: ["latin"], variable: "--font-geist-sans", subsets: ["latin"],
weight: ["100", "200", "300", "400", "500", "600", "700", "800", "900"], });
const geistMono = Geist_Mono({
variable: "--font-geist-mono", subsets: ["latin"],
}); });
export const metadata: Metadata = { export const metadata: Metadata = {
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."}; title: "Video Management Dashboard | Chef Studio", description: "Professional video management dashboard for chefs to upload, edit, and manage their culinary content."};
export default function RootLayout({ export default function RootLayout({
children, children,
@@ -19,18 +20,18 @@ export default function RootLayout({
}) { }) {
return ( return (
<html lang="en" suppressHydrationWarning> <html lang="en" suppressHydrationWarning>
<body className={`${poppins.variable} antialiased`}> <body className={`${geist.variable} ${geistMono.variable} antialiased`}>
<ServiceWrapper> {children}
<Tag />
{children}
</ServiceWrapper>
<script <script
async dangerouslySetInnerHTML={{
src="https://cdn.jsdelivr.net/npm/gsap@3.12.2/dist/gsap.min.js" __html: `
/> if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
<script document.documentElement.classList.add('dark')
async } else {
src="https://cdn.jsdelivr.net/npm/gsap@3.12.2/dist/ScrollTrigger.min.js" document.documentElement.classList.remove('dark')
}
`,
}}
/> />
<script <script

View File

@@ -1,4 +1,4 @@
"use client" "use client";
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider"; import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
import NavbarStyleCentered from '@/components/navbar/NavbarStyleCentered/NavbarStyleCentered'; import NavbarStyleCentered from '@/components/navbar/NavbarStyleCentered/NavbarStyleCentered';
@@ -33,8 +33,8 @@ export default function LandingPage() {
{ name: "Products", id: "products" }, { name: "Products", id: "products" },
{ name: "Features", id: "features" }, { name: "Features", id: "features" },
{ name: "Reviews", id: "testimonials" }, { name: "Reviews", id: "testimonials" },
{ name: "Chef Profile", id: "/chef-profile" }, { name: "Contact", id: "contact" },
{ name: "Contact", id: "contact" } { name: "Dashboard", id: "/dashboard" }
]} ]}
button={{ text: "Shop Now", href: "products" }} button={{ text: "Shop Now", href: "products" }}
brandName="Lumière Skin" brandName="Lumière Skin"

View File

@@ -1,178 +1,111 @@
"use client" "use client";
import { useState } from "react";
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider"; import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
import NavbarStyleCentered from '@/components/navbar/NavbarStyleCentered/NavbarStyleCentered'; import NavbarStyleCentered from '@/components/navbar/NavbarStyleCentered/NavbarStyleCentered';
import FooterBaseCard from '@/components/sections/footer/FooterBaseCard'; import FooterBaseCard from '@/components/sections/footer/FooterBaseCard';
import { useState } from "react"; import { Upload, AlertCircle, CheckCircle, Video } from "lucide-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() { export default function VideoUploadPage() {
const [videos, setVideos] = useState<VideoFile[]>([]); const [file, setFile] = useState<File | null>(null);
const [uploadProgress, setUploadProgress] = useState<Record<string, number>>({}); const [isDragActive, setIsDragActive] = useState(false);
const [uploadStatus, setUploadStatus] = useState<Record<string, 'success' | 'error' | 'uploading'>>({}); const [uploadStatus, setUploadStatus] = useState<"idle" | "uploading" | "success" | "error">("idle");
const [isProcessing, setIsProcessing] = useState(false); const [errorMessage, setErrorMessage] = useState("");
const [videoTitle, setVideoTitle] = useState("");
const [videoDescription, setVideoDescription] = useState("");
const MAX_FILE_SIZE = 500 * 1024 * 1024; // 500MB const MAX_FILE_SIZE = 500 * 1024 * 1024; // 500MB
const ALLOWED_FORMATS = ['video/mp4', 'video/quicktime', 'video/x-msvideo', 'video/webm']; const ALLOWED_FORMATS = ["video/mp4", "video/mpeg", "video/quicktime", "video/x-msvideo"];
const MAX_DURATION = 30 * 60; // 30 minutes in seconds
const validateVideo = (file: File): { valid: boolean; error?: string } => { const handleDrag = (e: React.DragEvent) => {
if (!ALLOWED_FORMATS.includes(file.type)) { e.preventDefault();
return { valid: false, error: 'Invalid file format. Please upload MP4, MOV, AVI, or WebM.' }; e.stopPropagation();
if (e.type === "dragenter" || e.type === "dragover") {
setIsDragActive(true);
} else if (e.type === "dragleave") {
setIsDragActive(false);
} }
if (file.size > MAX_FILE_SIZE) {
return { valid: false, error: 'File size exceeds 500MB limit.' };
}
return { valid: true };
}; };
const getVideoDuration = (file: File): Promise<number> => { const validateFile = (selectedFile: File): boolean => {
return new Promise((resolve) => { if (selectedFile.size > MAX_FILE_SIZE) {
const video = document.createElement('video'); setErrorMessage("File size exceeds 500MB limit");
const blob = URL.createObjectURL(file); return false;
video.src = blob; }
video.onloadedmetadata = () => {
URL.revokeObjectURL(blob); if (!ALLOWED_FORMATS.includes(selectedFile.type)) {
resolve(video.duration); setErrorMessage("Invalid file format. Please upload MP4, MPEG, MOV, or AVI");
}; return false;
video.onerror = () => { }
URL.revokeObjectURL(blob);
resolve(0); return true;
};
});
}; };
const handleFileSelect = async (e: React.ChangeEvent<HTMLInputElement>) => { const handleDrop = (e: React.DragEvent) => {
const files = e.currentTarget.files; e.preventDefault();
if (!files) return; e.stopPropagation();
setIsDragActive(false);
const newVideos: VideoFile[] = []; const droppedFiles = e.dataTransfer.files;
if (droppedFiles && droppedFiles.length > 0) {
for (let i = 0; i < files.length; i++) { const selectedFile = droppedFiles[0];
const file = files[i]; if (validateFile(selectedFile)) {
const validation = validateVideo(file); setFile(selectedFile);
setErrorMessage("");
if (!validation.valid) { setUploadStatus("idle");
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 handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const updatedVideos = [...videos]; if (e.target.files && e.target.files.length > 0) {
updatedVideos[index] = { ...updatedVideos[index], [field]: value }; const selectedFile = e.target.files[0];
setVideos(updatedVideos); if (validateFile(selectedFile)) {
setFile(selectedFile);
setErrorMessage("");
setUploadStatus("idle");
}
}
}; };
const removeVideo = (index: number) => { const handleUpload = async () => {
const updatedVideos = videos.filter((_, i) => i !== index); if (!file || !videoTitle.trim()) {
URL.revokeObjectURL(videos[index].preview); setErrorMessage("Please select a video and enter a title");
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; return;
} }
const id = `video-${index}`; setUploadStatus("uploading");
setUploadStatus((prev) => ({ ...prev, [id]: 'uploading' })); setErrorMessage("");
setUploadProgress((prev) => ({ ...prev, [id]: 0 }));
try { try {
const formData = new FormData(); const formData = new FormData();
formData.append('file', video.file); formData.append("file", file);
formData.append('title', video.title); formData.append("title", videoTitle);
formData.append('description', video.description); formData.append("description", videoDescription);
formData.append('chefName', video.chefName);
const xhr = new XMLHttpRequest(); const response = await fetch("/api/upload-video", {
method: "POST", body: formData,
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 (!response.ok) {
if (xhr.status === 200 || xhr.status === 201) { const error = await response.json();
setUploadStatus((prev) => ({ ...prev, [id]: 'success' })); throw new Error(error.message || "Upload failed");
setUploadProgress((prev) => ({ ...prev, [id]: 100 })); }
} else {
setUploadStatus((prev) => ({ ...prev, [id]: 'error' }));
}
});
xhr.addEventListener('error', () => { setUploadStatus("success");
setUploadStatus((prev) => ({ ...prev, [id]: 'error' })); setFile(null);
}); setVideoTitle("");
setVideoDescription("");
xhr.open('POST', '/api/videos/upload'); setTimeout(() => {
xhr.send(formData); setUploadStatus("idle");
}, 3000);
} catch (error) { } catch (error) {
setUploadStatus((prev) => ({ ...prev, [id]: 'error' })); setUploadStatus("error");
setErrorMessage(error instanceof Error ? error.message : "Upload failed");
} }
}; };
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 ( return (
<ThemeProvider <ThemeProvider
defaultButtonVariant="hover-bubble" defaultButtonVariant="hover-bubble"
@@ -189,11 +122,11 @@ export default function VideoUploadPage() {
<div id="nav" data-section="nav"> <div id="nav" data-section="nav">
<NavbarStyleCentered <NavbarStyleCentered
navItems={[ navItems={[
{ name: "Home", id: "/" },
{ name: "About", id: "about" }, { name: "About", id: "about" },
{ name: "Products", id: "products" }, { name: "Products", id: "products" },
{ name: "Features", id: "features" }, { name: "Features", id: "features" },
{ name: "Reviews", id: "testimonials" }, { name: "Reviews", id: "testimonials" },
{ name: "Upload Video", id: "/video-upload" },
{ name: "Contact", id: "contact" } { name: "Contact", id: "contact" }
]} ]}
button={{ text: "Shop Now", href: "/" }} button={{ text: "Shop Now", href: "/" }}
@@ -201,164 +134,146 @@ export default function VideoUploadPage() {
/> />
</div> </div>
<div id="video-upload" data-section="video-upload" className="w-full py-20 px-4 md:px-8"> <div className="min-h-screen bg-gradient-to-b from-background to-background/50 py-16 px-4">
<div className="w-full max-w-4xl mx-auto"> <div className="max-w-2xl mx-auto">
<div className="mb-12 text-center"> {/* Header */}
<h1 className="text-4xl md:text-5xl font-bold mb-4 text-foreground">Share Your Cooking Video</h1> <div className="text-center mb-12">
<p className="text-lg text-foreground/75 mb-2">Help our community by uploading your favorite cooking recipe or technique videos</p> <div className="flex items-center justify-center mb-4">
<p className="text-sm text-foreground/50">Maximum file size: 500MB | Maximum duration: 30 minutes | Formats: MP4, MOV, AVI, WebM</p> <Video className="w-12 h-12 text-primary-cta" />
</div>
<h1 className="text-4xl md:text-5xl font-bold text-foreground mb-4">
Submit Your Cooking Video
</h1>
<p className="text-lg text-foreground/70">
Share your culinary creations and showcase your chef skills with our community
</p>
</div> </div>
<div className="bg-card rounded-lg border border-primary-cta/20 p-8 mb-8"> {/* Upload Area */}
<label className="block mb-6"> <div className="bg-card rounded-lg p-8 mb-8 border border-accent/20">
<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
<div className="flex flex-col items-center justify-center pt-5 pb-6"> onDragEnter={handleDrag}
<Upload className="w-12 h-12 text-primary-cta mb-2" /> onDragLeave={handleDrag}
<p className="text-sm text-foreground/75"><span className="font-semibold">Click to upload</span> or drag and drop</p> onDragOver={handleDrag}
<p className="text-xs text-foreground/50 mt-1">MP4, MOV, AVI or WebM</p> onDrop={handleDrop}
</div> className={`border-2 border-dashed rounded-lg p-12 text-center cursor-pointer transition-colors ${
</div> isDragActive
? "border-primary-cta bg-primary-cta/5"
: "border-accent/30 hover:border-accent/50"
}`}
>
<input <input
type="file" type="file"
multiple accept="video/*"
accept="video/mp4,video/quicktime,video/x-msvideo,video/webm"
onChange={handleFileSelect} onChange={handleFileSelect}
className="hidden" className="hidden"
disabled={isProcessing} id="video-input"
/> />
</label> <label htmlFor="video-input" className="cursor-pointer">
<Upload className="w-12 h-12 text-accent mx-auto mb-4" />
<p className="text-xl font-semibold text-foreground mb-2">
Drag and drop your video here
</p>
<p className="text-foreground/60 mb-4">
or click to browse your files
</p>
<p className="text-sm text-foreground/50">
Supported formats: MP4, MPEG, MOV, AVI (Max 500MB)
</p>
</label>
</div>
{/* Selected File Info */}
{file && (
<div className="mt-6 p-4 bg-background rounded-lg border border-accent/20">
<p className="text-foreground font-medium"> File selected: {file.name}</p>
<p className="text-foreground/60 text-sm mt-1">
Size: {(file.size / 1024 / 1024).toFixed(2)} MB
</p>
</div>
)}
</div> </div>
{videos.length > 0 && ( {/* Form Fields */}
<div className="space-y-6"> <div className="bg-card rounded-lg p-8 mb-8 border border-accent/20">
{videos.map((video, index) => { <div className="mb-6">
const id = `video-${index}`; <label className="block text-foreground font-semibold mb-2">
const status = uploadStatus[id]; Video Title *
const progress = uploadProgress[id] || 0; </label>
<input
type="text"
value={videoTitle}
onChange={(e) => setVideoTitle(e.target.value)}
placeholder="e.g., Classic French Coq au Vin"
className="w-full px-4 py-3 bg-background border border-accent/20 rounded-lg text-foreground placeholder-foreground/40 focus:outline-none focus:border-primary-cta focus:ring-2 focus:ring-primary-cta/20"
/>
</div>
return ( <div className="mb-6">
<div key={index} className="bg-card rounded-lg border border-primary-cta/20 p-6 overflow-hidden"> <label className="block text-foreground font-semibold mb-2">
<div className="grid grid-cols-1 md:grid-cols-4 gap-6"> Video Description
<div className="md:col-span-1"> </label>
<video <textarea
src={video.preview} value={videoDescription}
className="w-full h-32 object-cover rounded-lg bg-background" onChange={(e) => setVideoDescription(e.target.value)}
/> placeholder="Describe your recipe, ingredients, and cooking process..."
<p className="text-xs text-foreground/50 mt-2">Duration: {formatDuration(video.duration)}</p> rows={5}
<p className="text-xs text-foreground/50">Size: {formatFileSize(video.size)}</p> className="w-full px-4 py-3 bg-background border border-accent/20 rounded-lg text-foreground placeholder-foreground/40 focus:outline-none focus:border-primary-cta focus:ring-2 focus:ring-primary-cta/20"
</div> />
</div>
</div>
<div className="md:col-span-3"> {/* Status Messages */}
<div className="space-y-4"> {errorMessage && (
<div> <div className="flex items-center gap-3 p-4 bg-red-500/10 border border-red-500/30 rounded-lg mb-6">
<label className="block text-sm font-medium text-foreground mb-1">Video Title</label> <AlertCircle className="w-5 h-5 text-red-500 flex-shrink-0" />
<input <p className="text-red-600 font-medium">{errorMessage}</p>
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> </div>
)} )}
{videos.length === 0 && ( {uploadStatus === "success" && (
<div className="text-center py-12"> <div className="flex items-center gap-3 p-4 bg-green-500/10 border border-green-500/30 rounded-lg mb-6">
<p className="text-foreground/50">No videos selected yet. Upload your cooking videos to get started!</p> <CheckCircle className="w-5 h-5 text-green-500 flex-shrink-0" />
<p className="text-green-600 font-medium">Video uploaded successfully!</p>
</div> </div>
)} )}
{/* Upload Button */}
<button
onClick={handleUpload}
disabled={!file || uploadStatus === "uploading"}
className={`w-full py-4 px-6 rounded-lg font-semibold text-lg transition-all ${
uploadStatus === "uploading"
? "bg-primary-cta/50 text-white cursor-not-allowed"
: !file
? "bg-foreground/10 text-foreground/50 cursor-not-allowed"
: "bg-primary-cta text-white hover:bg-primary-cta/90 active:scale-95"
}`}
>
{uploadStatus === "uploading"
? "Uploading..."
: uploadStatus === "success"
? "Upload Complete!"
: "Upload Video"}
</button>
{/* Guidelines */}
<div className="mt-12 p-6 bg-background rounded-lg border border-accent/20">
<h3 className="text-lg font-semibold text-foreground mb-4">Upload Guidelines</h3>
<ul className="space-y-2 text-foreground/70">
<li> Videos must be between 30 seconds and 10 minutes long</li>
<li> Use high-quality video (1080p or higher recommended)</li>
<li> Ensure good lighting and clear audio</li>
<li> Title should be descriptive and engaging</li>
<li> Include ingredients and cooking instructions in the description</li>
<li> Respect copyright and intellectual property rights</li>
<li> Videos are subject to moderation before publishing</li>
</ul>
</div>
</div> </div>
</div> </div>
<div id="footer" data-section="footer" className="mt-20"> <div id="footer" data-section="footer">
<FooterBaseCard <FooterBaseCard
logoText="Lumière Skin" logoText="Lumière Skin"
columns={[ columns={[

View File

@@ -1,48 +1,65 @@
"use client" "use client"
import { useState, useMemo } from "react";
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider"; import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
import NavbarStyleCentered from '@/components/navbar/NavbarStyleCentered/NavbarStyleCentered'; import NavbarStyleCentered from '@/components/navbar/NavbarStyleCentered/NavbarStyleCentered';
import HeroOverlay from '@/components/sections/hero/HeroOverlay'; import HeroLogoBillboard from '@/components/sections/hero/HeroLogoBillboard';
import BlogCardThree from '@/components/sections/blog/BlogCardThree';
import FooterBaseCard from '@/components/sections/footer/FooterBaseCard'; import FooterBaseCard from '@/components/sections/footer/FooterBaseCard';
import { useState } from 'react'; import { Play, X } from "lucide-react";
import { Search } from 'lucide-react';
import Input from '@/components/form/Input'; interface Video {
id: string;
title: string;
description: string;
thumbnail: string;
videoUrl: string;
cuisine: string;
chef: string;
duration: string;
}
const videos: Video[] = [
{
id: "1", title: "Luxurious Night Skincare Routine", description: "Learn the perfect evening skincare regimen for deep hydration and overnight repair.", thumbnail: "http://img.b2bpic.net/free-photo/woman-applying-moisturizer-her-beauty-routine_23-2150166464.jpg", videoUrl: "https://commondatastorage.googleapis.com/gtv-videos-library/sample/ElephantsDream.mp4", cuisine: "Skincare Tutorial", chef: "Dr. Sarah Chen", duration: "12:45"
},
{
id: "2", title: "Anti-Aging Serum Application Guide", description: "Master the technique for maximum anti-aging benefits with our premium serum.", thumbnail: "http://img.b2bpic.net/free-photo/make-up-concept-with-pink-nail-polish_23-2149030370.jpg", videoUrl: "https://commondatastorage.googleapis.com/gtv-videos-library/sample/BigBuckBunny.mp4", cuisine: "Product Guide", chef: "Elena Rodriguez", duration: "8:30"
},
{
id: "3", title: "Hydration Secrets for Dry Skin", description: "Discover professional techniques to combat dryness and achieve radiant skin.", thumbnail: "http://img.b2bpic.net/free-photo/flat-lay-hands-holding-body-care-product-wooden-background_23-2148241876.jpg", videoUrl: "https://commondatastorage.googleapis.com/gtv-videos-library/sample/ForBiggerBlazes.mp4", cuisine: "Skincare Tutorial", chef: "Dr. James Wilson", duration: "15:20"
},
{
id: "4", title: "Natural Ingredients Deep Dive", description: "Explore the science behind our carefully selected natural ingredients.", thumbnail: "http://img.b2bpic.net/free-photo/product-branding-packaging_23-2150965833.jpg", videoUrl: "https://commondatastorage.googleapis.com/gtv-videos-library/sample/ForBiggerJoyrides.mp4", cuisine: "Educational", chef: "Dr. Lisa Martinez", duration: "18:10"
},
{
id: "5", title: "Minimalist Skincare Routine", description: "Less is more: build an effective skincare routine with just essentials.", thumbnail: "http://img.b2bpic.net/free-photo/model-career-kit-still-life-flat-lay_23-2150218023.jpg", videoUrl: "https://commondatastorage.googleapis.com/gtv-videos-library/sample/ForBiggerEscapes.mp4", cuisine: "Skincare Tutorial", chef: "Emma Thompson", duration: "10:15"
},
{
id: "6", title: "Acne Treatment Protocol", description: "Professional advice on treating acne-prone skin with our product line.", thumbnail: "http://img.b2bpic.net/free-photo/close-perfection-closeup-peaceful-relaxed-redhead-happy-woman-closed-eyes-pure-delighted-smile-sh_1258-139766.jpg", videoUrl: "https://commondatastorage.googleapis.com/gtv-videos-library/sample/ElephantsDream.mp4", cuisine: "Product Guide", chef: "Dr. Michael Lee", duration: "14:05"
},
{
id: "7", title: "Sensitive Skin Solutions", description: "Gentle approaches to care for sensitive and reactive skin types.", thumbnail: "http://img.b2bpic.net/free-photo/young-beautiful-woman-having-online-meeting_23-2149116347.jpg", videoUrl: "https://commondatastorage.googleapis.com/gtv-videos-library/sample/BigBuckBunny.mp4", cuisine: "Skincare Tutorial", chef: "Dr. Sophie Brown", duration: "11:40"
},
{
id: "8", title: "Seasonal Skincare Adjustments", description: "How to adapt your skincare routine for different seasons and climates.", thumbnail: "http://img.b2bpic.net/free-photo/young-women-polishing-nails-bed_23-2147770248.jpg", videoUrl: "https://commondatastorage.googleapis.com/gtv-videos-library/sample/ForBiggerBlazes.mp4", cuisine: "Educational", chef: "Dr. Amanda White", duration: "16:25"
}
];
export default function VideosPage() { export default function VideosPage() {
const [searchQuery, setSearchQuery] = useState(''); const [selectedVideo, setSelectedVideo] = useState<Video | null>(null);
const [selectedCategory, setSelectedCategory] = useState('all'); const [cuisineFilter, setCuisineFilter] = useState<string>("All");
const [chefFilter, setChefFilter] = useState<string>("All");
const allVideos = [ const cuisines = ["All", ...Array.from(new Set(videos.map(v => v.cuisine)))];
{ const chefs = ["All", ...Array.from(new Set(videos.map(v => v.chef)))];
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 = useMemo(() => {
return videos.filter(video => {
const filteredVideos = allVideos.filter(video => { const cuisineMatch = cuisineFilter === "All" || video.cuisine === cuisineFilter;
const matchesSearch = video.title.toLowerCase().includes(searchQuery.toLowerCase()) || const chefMatch = chefFilter === "All" || video.chef === chefFilter;
video.excerpt.toLowerCase().includes(searchQuery.toLowerCase()) || return cuisineMatch && chefMatch;
video.authorName.toLowerCase().includes(searchQuery.toLowerCase()); });
const matchesCategory = selectedCategory === 'all' || video.category === selectedCategory; }, [cuisineFilter, chefFilter]);
return matchesSearch && matchesCategory;
});
return ( return (
<ThemeProvider <ThemeProvider
@@ -60,11 +77,12 @@ export default function VideosPage() {
<div id="nav" data-section="nav"> <div id="nav" data-section="nav">
<NavbarStyleCentered <NavbarStyleCentered
navItems={[ navItems={[
{ name: "Home", id: "/" }, { name: "About", id: "about" },
{ name: "About", id: "#about" }, { name: "Products", id: "products" },
{ name: "Products", id: "#products" }, { name: "Features", id: "features" },
{ name: "Features", id: "#features" }, { name: "Reviews", id: "testimonials" },
{ name: "Contact", id: "#contact" } { name: "Videos", id: "videos" },
{ name: "Contact", id: "contact" }
]} ]}
button={{ text: "Shop Now", href: "/" }} button={{ text: "Shop Now", href: "/" }}
brandName="Lumière Skin" brandName="Lumière Skin"
@@ -72,73 +90,150 @@ export default function VideosPage() {
</div> </div>
<div id="hero" data-section="hero"> <div id="hero" data-section="hero">
<HeroOverlay <HeroLogoBillboard
title="Cooking Videos & Tutorials" logoText="Skincare Videos"
description="Explore our collection of culinary expertise from world-class chefs. Learn techniques, recipes, and cooking secrets." description="Expert tutorials, product guides, and educational content to help you master your skincare routine."
tag="Video Gallery"
textPosition="center"
showDimOverlay={true}
buttons={[ buttons={[
{ text: "Browse Videos", href: "#videos" } { text: "Back to Home", href: "/" },
{ text: "Shop Collection", href: "/" }
]} ]}
imageSrc="http://img.b2bpic.net/free-photo/chef-preparing-dish_23-2148241892.jpg" background={{ variant: "sparkles-gradient" }}
imageAlt="Professional chef cooking" imageSrc="http://img.b2bpic.net/free-photo/woman-applying-moisturizer-her-beauty-routine_23-2150166464.jpg"
imageAlt="Video Gallery"
mediaAnimation="slide-up"
frameStyle="card"
buttonAnimation="blur-reveal"
/> />
</div> </div>
<div id="filters" data-section="filters" className="relative py-20 px-4"> <div id="videos" data-section="videos" className="py-20">
<div className="max-w-4xl mx-auto"> <div className="w-full max-w-7xl mx-auto px-6">
<div className="mb-8"> {/* Filters */}
<h2 className="text-3xl font-bold mb-4">Search & Filter Videos</h2> <div className="mb-12">
<div className="flex flex-col gap-4"> <div className="grid md:grid-cols-2 gap-8">
<div className="flex gap-2"> <div>
<Search className="w-5 h-5 opacity-50 self-center" /> <h3 className="text-lg font-semibold mb-4">Filter by Category</h3>
<Input <div className="flex flex-wrap gap-2">
value={searchQuery} {cuisines.map(cuisine => (
onChange={setSearchQuery} <button
placeholder="Search by title, chef name, or topic..." key={cuisine}
type="text" onClick={() => setCuisineFilter(cuisine)}
/> className={`px-4 py-2 rounded-lg transition-all ${
cuisineFilter === cuisine
? "bg-gradient-to-r from-pink-500 to-rose-500 text-white"
: "bg-gray-200 text-gray-700 hover:bg-gray-300"
}`}
>
{cuisine}
</button>
))}
</div>
</div> </div>
<div className="flex flex-wrap gap-2"> <div>
{categories.map(category => ( <h3 className="text-lg font-semibold mb-4">Filter by Expert</h3>
<button <div className="flex flex-wrap gap-2">
key={category} {chefs.map(chef => (
onClick={() => setSelectedCategory(category)} <button
className={`px-4 py-2 rounded-full transition ${ key={chef}
selectedCategory === category onClick={() => setChefFilter(chef)}
? 'bg-primary-cta text-white' className={`px-4 py-2 rounded-lg transition-all ${
: 'bg-secondary-cta/30 hover:bg-secondary-cta/50' chefFilter === chef
}`} ? "bg-gradient-to-r from-pink-500 to-rose-500 text-white"
> : "bg-gray-200 text-gray-700 hover:bg-gray-300"
{category.charAt(0).toUpperCase() + category.slice(1)} }`}
</button> >
))} {chef}
</button>
))}
</div>
</div> </div>
<p className="text-sm opacity-75">Found {filteredVideos.length} video{filteredVideos.length !== 1 ? 's' : ''}</p>
</div> </div>
</div> </div>
{/* Video Grid */}
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6 mb-12">
{filteredVideos.map(video => (
<div
key={video.id}
className="group cursor-pointer rounded-xl overflow-hidden bg-white shadow-lg hover:shadow-xl transition-shadow"
onClick={() => setSelectedVideo(video)}
>
<div className="relative aspect-video bg-gray-900 overflow-hidden">
<img
src={video.thumbnail}
alt={video.title}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
/>
<div className="absolute inset-0 bg-black/40 flex items-center justify-center group-hover:bg-black/50 transition-colors">
<Play className="w-16 h-16 text-white fill-white" />
</div>
<div className="absolute top-2 right-2 bg-black/70 px-3 py-1 rounded text-white text-sm">
{video.duration}
</div>
</div>
<div className="p-4">
<h3 className="font-semibold text-gray-900 mb-2 line-clamp-2 group-hover:text-pink-600 transition-colors">
{video.title}
</h3>
<p className="text-sm text-gray-600 mb-3 line-clamp-2">{video.description}</p>
<div className="flex flex-wrap gap-2 mb-3">
<span className="text-xs bg-pink-100 text-pink-700 px-2 py-1 rounded">
{video.cuisine}
</span>
<span className="text-xs bg-blue-100 text-blue-700 px-2 py-1 rounded">
{video.chef}
</span>
</div>
</div>
</div>
))}
</div>
{filteredVideos.length === 0 && (
<div className="text-center py-12">
<p className="text-gray-600 text-lg">No videos found with the selected filters.</p>
</div>
)}
</div> </div>
</div> </div>
<div id="videos" data-section="videos"> {/* Video Player Modal */}
{filteredVideos.length > 0 ? ( {selectedVideo && (
<BlogCardThree <div className="fixed inset-0 bg-black/80 flex items-center justify-center z-50 p-4">
blogs={filteredVideos} <div className="bg-white rounded-xl overflow-hidden max-w-4xl w-full">
title="Featured Cooking Videos" <div className="flex justify-between items-center p-6 border-b">
description="Watch expert chefs share their culinary techniques and recipes in our video gallery." <h2 className="text-xl font-semibold">{selectedVideo.title}</h2>
gridVariant="three-columns-all-equal-width" <button
animationType="slide-up" onClick={() => setSelectedVideo(null)}
textboxLayout="default" className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
useInvertedBackground={false} >
tag="Videos" <X className="w-6 h-6" />
/> </button>
) : ( </div>
<div className="py-20 text-center"> <div className="aspect-video bg-black">
<p className="text-xl opacity-75">No videos found matching your search. Try different keywords or categories.</p> <video
src={selectedVideo.videoUrl}
controls
autoPlay
className="w-full h-full"
/>
</div>
<div className="p-6">
<p className="text-gray-700 mb-4">{selectedVideo.description}</p>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<p className="text-gray-600 font-medium">Category</p>
<p className="text-gray-900">{selectedVideo.cuisine}</p>
</div>
<div>
<p className="text-gray-600 font-medium">Expert</p>
<p className="text-gray-900">{selectedVideo.chef}</p>
</div>
</div>
</div>
</div> </div>
)} </div>
</div> )}
<div id="footer" data-section="footer"> <div id="footer" data-section="footer">
<FooterBaseCard <FooterBaseCard
@@ -146,24 +241,24 @@ export default function VideosPage() {
columns={[ columns={[
{ {
title: "Shop", items: [ title: "Shop", items: [
{ label: "All Products", href: "/#products" }, { label: "All Products", href: "/" },
{ label: "Skincare Routine", href: "/#products" }, { label: "Skincare Routine", href: "/" },
{ label: "Collections", href: "/#products" }, { label: "Collections", href: "/" },
{ label: "Gift Sets", href: "/#products" } { label: "Gift Sets", href: "/" }
] ]
}, },
{ {
title: "Company", items: [ title: "Company", items: [
{ label: "About Us", href: "/#about" }, { label: "About Us", href: "/" },
{ label: "Our Story", href: "/#about" }, { label: "Our Story", href: "/" },
{ label: "Sustainability", href: "#" }, { label: "Sustainability", href: "#" },
{ label: "Careers", href: "#" } { label: "Careers", href: "#" }
] ]
}, },
{ {
title: "Support", items: [ title: "Support", items: [
{ label: "Contact Us", href: "/#contact" }, { label: "Contact Us", href: "/" },
{ label: "FAQ", href: "/#faq" }, { label: "FAQ", href: "/" },
{ label: "Shipping Info", href: "#" }, { label: "Shipping Info", href: "#" },
{ label: "Returns", href: "#" } { label: "Returns", href: "#" }
] ]