Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ed7ed6ec7e | |||
| 8a951092b0 | |||
| 2a830e6c24 | |||
| cf7a8842c5 | |||
| 3af1d9d9f5 | |||
| bcfd01265a | |||
| 0b064fdd08 | |||
| f99e0d80eb | |||
| 8f5ace10d9 | |||
| f44c9c40b3 | |||
| cff8aa84d5 | |||
| 49f4f4a6a3 | |||
| a545a8edbe | |||
| d6c2f75b2c | |||
| 5a69f33498 | |||
| 21a4b6a64a | |||
| b97270b14e | |||
| 70cbb9bb7b | |||
| 91b67973ba | |||
| b25ee3ade6 | |||
| 60f09ad641 | |||
| e44ead1e99 | |||
| fba1a578bc | |||
| 004f008065 | |||
| 9887b9eded | |||
| 2fa8fa3b0d |
72
src/app/api/upload-video/route.ts
Normal file
72
src/app/api/upload-video/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,66 +1,15 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
|
||||
import NavbarStyleCentered from '@/components/navbar/NavbarStyleCentered/NavbarStyleCentered';
|
||||
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 { 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";
|
||||
}
|
||||
import { Star, Users, Heart, Mail } from "lucide-react";
|
||||
|
||||
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"
|
||||
@@ -80,256 +29,140 @@ export default function ChefProfilePage() {
|
||||
{ name: "Home", id: "/" },
|
||||
{ name: "About", id: "about" },
|
||||
{ name: "Products", id: "products" },
|
||||
{ name: "Features", id: "features" },
|
||||
{ name: "Reviews", id: "testimonials" },
|
||||
{ name: "Contact", id: "contact" }
|
||||
]}
|
||||
button={{ text: "Shop Now", href: "/" }}
|
||||
brandName="Lumière Skin"
|
||||
button={{ text: "Follow Chef", href: "#" }}
|
||||
brandName="Chef Profile"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="hero" data-section="hero">
|
||||
<div id="chef-info" data-section="chef-info">
|
||||
<HeroLogoBillboard
|
||||
logoText="Chef Profile & Video Management"
|
||||
description="Manage and showcase your video content. Track performance, edit details, and grow your audience."
|
||||
logoText="Chef Marcus"
|
||||
description="Award-winning culinary expert with 15+ years of experience in modern gastronomy. Specializing in innovative techniques and sustainable ingredient sourcing."
|
||||
buttons={[
|
||||
{ text: "Upload New Video", href: "#" },
|
||||
{ text: "Back to Home", href: "/" }
|
||||
{ text: "View Video History", href: "#videos" },
|
||||
{ text: "Subscribe", href: "#" }
|
||||
]}
|
||||
background={{ variant: "sparkles-gradient" }}
|
||||
imageSrc="http://img.b2bpic.net/free-photo/woman-applying-moisturizer-her-beauty-routine_23-2150166464.jpg"
|
||||
imageAlt="Chef profile"
|
||||
imageSrc="http://img.b2bpic.net/free-photo/portrait-successful-man-chef-wearing-hat-standing-against-blurred-kitchen_23-2148381812.jpg"
|
||||
imageAlt="Chef Marcus Professional Portrait"
|
||||
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>
|
||||
<div id="stats" data-section="stats">
|
||||
<MetricCardThree
|
||||
metrics={[
|
||||
{ id: "1", icon: Users, title: "Followers", value: "245.8K" },
|
||||
{ id: "2", icon: Heart, title: "Engagement", value: "98.5%" },
|
||||
{ id: "3", icon: Star, title: "Chef Rating", value: "4.9/5" },
|
||||
{ id: "4", icon: Mail, title: "Videos", value: "156" }
|
||||
]}
|
||||
title="Chef Performance"
|
||||
description="Outstanding metrics showcasing chef excellence and community engagement."
|
||||
textboxLayout="default"
|
||||
animationType="slide-up"
|
||||
useInvertedBackground={false}
|
||||
/>
|
||||
</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>
|
||||
<div id="videos" data-section="videos">
|
||||
<TeamCardOne
|
||||
members={[
|
||||
{
|
||||
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"
|
||||
},
|
||||
{
|
||||
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"
|
||||
},
|
||||
{
|
||||
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"
|
||||
},
|
||||
{
|
||||
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"
|
||||
}
|
||||
]}
|
||||
title="Recent Video History"
|
||||
description="Explore Chef Marcus's latest cooking tutorials and technique demonstrations."
|
||||
gridVariant="two-columns-alternating-heights"
|
||||
animationType="slide-up"
|
||||
textboxLayout="default"
|
||||
useInvertedBackground={false}
|
||||
/>
|
||||
</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="ratings" data-section="ratings">
|
||||
<TestimonialCardThirteen
|
||||
testimonials={[
|
||||
{
|
||||
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,
|
||||
icon: Star
|
||||
},
|
||||
{
|
||||
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
|
||||
},
|
||||
{
|
||||
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,
|
||||
icon: Star
|
||||
},
|
||||
{
|
||||
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,
|
||||
icon: Star
|
||||
}
|
||||
]}
|
||||
showRating={true}
|
||||
title="Community Ratings & Reviews"
|
||||
description="See what followers love most about Chef Marcus's content and expertise."
|
||||
textboxLayout="default"
|
||||
animationType="slide-up"
|
||||
useInvertedBackground={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="footer" data-section="footer">
|
||||
<FooterBaseCard
|
||||
logoText="Lumière Skin"
|
||||
logoText="Chef Marcus"
|
||||
columns={[
|
||||
{
|
||||
title: "Shop", items: [
|
||||
{ label: "All Products", href: "/#products" },
|
||||
{ label: "Skincare Routine", href: "/#products" },
|
||||
{ label: "Collections", href: "/#products" },
|
||||
{ label: "Gift Sets", href: "/#products" }
|
||||
title: "Content", items: [
|
||||
{ label: "Cooking Tutorials", href: "#videos" },
|
||||
{ label: "Recipe Collections", href: "#" },
|
||||
{ label: "Technique Guides", href: "#" },
|
||||
{ label: "Live Streams", href: "#" }
|
||||
]
|
||||
},
|
||||
{
|
||||
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: "About", items: [
|
||||
{ label: "Bio", href: "#chef-info" },
|
||||
{ label: "Achievements", href: "#" },
|
||||
{ label: "Experience", href: "#" },
|
||||
{ label: "Philosophy", href: "#" }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Connect", items: [
|
||||
{ label: "YouTube", href: "https://youtube.com" },
|
||||
{ label: "Instagram", href: "https://instagram.com" },
|
||||
{ label: "Facebook", href: "https://facebook.com" },
|
||||
{ label: "Pinterest", href: "https://pinterest.com" },
|
||||
{ label: "TikTok", href: "https://tiktok.com" }
|
||||
{ label: "TikTok", href: "https://tiktok.com" },
|
||||
{ label: "Twitter", href: "https://twitter.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>
|
||||
</ThemeProvider>
|
||||
|
||||
263
src/app/dashboard/page.tsx
Normal file
263
src/app/dashboard/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +1,17 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { Inter } from "next/font/google";
|
||||
|
||||
const inter = Inter({
|
||||
variable: "--font-inter", subsets: ["latin"],
|
||||
const geist = Geist({
|
||||
variable: "--font-geist-sans", subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono", subsets: ["latin"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Food Homes", description: "Discover premium food delivery and catering services"};
|
||||
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({
|
||||
children,
|
||||
@@ -16,7 +20,7 @@ export default function RootLayout({
|
||||
}) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body className={`${inter.variable} antialiased`}>
|
||||
<body className={`${geist.variable} ${geistMono.variable} antialiased`}>
|
||||
{children}
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
|
||||
import NavbarStyleCentered from '@/components/navbar/NavbarStyleCentered/NavbarStyleCentered';
|
||||
@@ -33,24 +33,25 @@ export default function LandingPage() {
|
||||
{ name: "Products", id: "products" },
|
||||
{ name: "Features", id: "features" },
|
||||
{ name: "Reviews", id: "testimonials" },
|
||||
{ name: "Contact", id: "contact" }
|
||||
{ name: "Contact", id: "contact" },
|
||||
{ name: "Dashboard", id: "/dashboard" }
|
||||
]}
|
||||
button={{ text: "Shop Now", href: "products" }}
|
||||
brandName="Food Homes"
|
||||
brandName="Lumière Skin"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="hero" data-section="hero">
|
||||
<HeroLogoBillboard
|
||||
logoText="Food Homes"
|
||||
description="Delicious, freshly prepared meals delivered to your door. Discover premium culinary creations crafted for your table."
|
||||
logoText="Lumière Skin"
|
||||
description="Radiant, healthy skin starts with premium, science-backed skincare. Discover our luxurious collection crafted for your most beautiful glow."
|
||||
buttons={[
|
||||
{ text: "Explore Menu", href: "products" },
|
||||
{ text: "Explore Collection", href: "products" },
|
||||
{ text: "Learn Our Story", href: "about" }
|
||||
]}
|
||||
background={{ variant: "sparkles-gradient" }}
|
||||
imageSrc="http://img.b2bpic.net/free-photo/model-career-kit-still-life-flat-lay_23-2150218023.jpg"
|
||||
imageAlt="Premium food delivery"
|
||||
imageAlt="Premium skincare collection"
|
||||
mediaAnimation="slide-up"
|
||||
frameStyle="card"
|
||||
buttonAnimation="blur-reveal"
|
||||
@@ -62,7 +63,7 @@ export default function LandingPage() {
|
||||
heading={[
|
||||
{ type: "text", content: "Crafted with" },
|
||||
{ type: "image", src: "http://img.b2bpic.net/free-photo/people-working-elegant-cozy-office-space_23-2149548692.jpg", alt: "Brand heritage" },
|
||||
{ type: "text", content: "passion for exceptional food" }
|
||||
{ type: "text", content: "integrity and innovation" }
|
||||
]}
|
||||
buttons={[
|
||||
{ text: "Discover Our Mission", href: "#contact" }
|
||||
@@ -76,17 +77,17 @@ export default function LandingPage() {
|
||||
<ProductCardOne
|
||||
products={[
|
||||
{
|
||||
id: "1", name: "Grilled Salmon Bowl", price: "$18", imageSrc: "http://img.b2bpic.net/free-photo/make-up-concept-with-pink-nail-polish_23-2149030370.jpg", imageAlt: "Grilled Salmon Bowl"
|
||||
id: "1", name: "Hydra Essence Moisturizer", price: "$68", imageSrc: "http://img.b2bpic.net/free-photo/make-up-concept-with-pink-nail-polish_23-2149030370.jpg", imageAlt: "Hydra Essence Moisturizer"
|
||||
},
|
||||
{
|
||||
id: "2", name: "Vegetarian Delight", price: "$14", imageSrc: "http://img.b2bpic.net/free-photo/flat-lay-hands-holding-body-care-product-wooden-background_23-2148241876.jpg", imageAlt: "Vegetarian Delight"
|
||||
id: "2", name: "Luminous Glow Serum", price: "$82", imageSrc: "http://img.b2bpic.net/free-photo/flat-lay-hands-holding-body-care-product-wooden-background_23-2148241876.jpg", imageAlt: "Luminous Glow Serum"
|
||||
},
|
||||
{
|
||||
id: "3", name: "Classic Pasta Primavera", price: "$12", imageSrc: "http://img.b2bpic.net/free-photo/product-branding-packaging_23-2150965833.jpg", imageAlt: "Classic Pasta Primavera"
|
||||
id: "3", name: "Pure Silk Cleanser", price: "$45", imageSrc: "http://img.b2bpic.net/free-photo/product-branding-packaging_23-2150965833.jpg", imageAlt: "Pure Silk Cleanser"
|
||||
}
|
||||
]}
|
||||
title="Our Featured Menu"
|
||||
description="Each meal is carefully prepared with fresh, locally-sourced ingredients. From classic favorites to innovative creations, we have something for every palate."
|
||||
title="Our Signature Collection"
|
||||
description="Each product is thoughtfully formulated to deliver visible results. From cleansing to hydration, we've got your skincare routine covered."
|
||||
gridVariant="three-columns-all-equal-width"
|
||||
animationType="slide-up"
|
||||
textboxLayout="default"
|
||||
@@ -96,21 +97,21 @@ export default function LandingPage() {
|
||||
|
||||
<div id="features" data-section="features">
|
||||
<FeatureBento
|
||||
title="Why Choose Food Homes"
|
||||
description="We combine premium ingredients with culinary expertise to create meals that delight your senses and nourish your body."
|
||||
title="Why Choose Lumière Skin"
|
||||
description="We combine nature's finest ingredients with cutting-edge skincare science to create products that truly transform your skin."
|
||||
features={[
|
||||
{
|
||||
title: "Fresh Ingredients", description: "Sourced daily from trusted local suppliers and farms", bentoComponent: "globe"
|
||||
title: "Natural Ingredients", description: "Sourced from organic farms and botanical gardens worldwide", bentoComponent: "globe"
|
||||
},
|
||||
{
|
||||
title: "Expert Chefs", description: "Experienced culinary professionals preparing every meal", bentoComponent: "icon-info-cards", items: [
|
||||
{ icon: CheckCircle, label: "Award-Winning", value: "Chef Team" },
|
||||
{ icon: Shield, label: "Food Safe", value: "Certified" },
|
||||
{ icon: Sparkles, label: "Quality", value: "Guaranteed" }
|
||||
title: "Dermatologist Tested", description: "Clinically proven formulas safe for all skin types", bentoComponent: "icon-info-cards", items: [
|
||||
{ icon: CheckCircle, label: "Hypoallergenic", value: "100%" },
|
||||
{ icon: Shield, label: "Cruelty-Free", value: "Certified" },
|
||||
{ icon: Sparkles, label: "Effective", value: "Proven" }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Quick Delivery", description: "Hot, fresh meals delivered within minutes of preparation", bentoComponent: "animated-bar-chart"
|
||||
title: "Visible Results", description: "See radiant, glowing skin in just weeks of consistent use", bentoComponent: "animated-bar-chart"
|
||||
}
|
||||
]}
|
||||
animationType="slide-up"
|
||||
@@ -121,9 +122,9 @@ export default function LandingPage() {
|
||||
|
||||
<div id="testimonials" data-section="testimonials">
|
||||
<TestimonialCardFifteen
|
||||
testimonial="Food Homes has revolutionized how I eat. The meals are absolutely delicious, beautifully presented, and always delivered fresh. I've never felt better about the quality of my meals."
|
||||
testimonial="Lumière Skin has completely transformed my skincare routine. My skin feels nourished, looks radiant, and I've never felt more confident. These products are worth every penny."
|
||||
rating={5}
|
||||
author="James Anderson, Food Enthusiast"
|
||||
author="Sarah Mitchell, Beauty Enthusiast"
|
||||
avatars={[
|
||||
{ src: "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", alt: "Customer 1" },
|
||||
{ src: "http://img.b2bpic.net/free-photo/young-beautiful-woman-having-online-meeting_23-2149116347.jpg", alt: "Customer 2" },
|
||||
@@ -138,30 +139,30 @@ export default function LandingPage() {
|
||||
|
||||
<div id="faq" data-section="faq">
|
||||
<FaqSplitMedia
|
||||
title="Frequently Asked Questions"
|
||||
description="Find answers to common questions about our meals, delivery, and ordering process."
|
||||
title="Skincare Questions Answered"
|
||||
description="Find answers to common questions about our products, ingredients, and how to get the best results for your skin."
|
||||
faqs={[
|
||||
{
|
||||
id: "1", title: "How fresh are the meals?", content: "All our meals are prepared fresh daily using ingredients sourced that morning. We prepare orders to specification and deliver within 30 minutes of completion to ensure maximum freshness and optimal temperature."
|
||||
id: "1", title: "How long before I see results?", content: "Most customers notice improvements in skin texture and hydration within 2-3 weeks of consistent use. Visible changes in brightness and clarity typically appear within 4-6 weeks. For best results, use our complete routine morning and night."
|
||||
},
|
||||
{
|
||||
id: "2", title: "Do you accommodate dietary restrictions?", content: "Absolutely! We offer vegetarian, vegan, gluten-free, and keto options. Our chefs are trained to handle allergies and specific dietary needs. Contact us with your requirements, and we'll create the perfect meal for you."
|
||||
id: "2", title: "Are your products suitable for sensitive skin?", content: "Yes! All Lumière Skin products are hypoallergenic, dermatologist-tested, and free from harsh chemicals. We recommend starting with our gentle cleanser and moisturizer. If you have specific concerns, our beauty experts are happy to recommend personalized solutions."
|
||||
},
|
||||
{
|
||||
id: "3", title: "What makes your ingredients special?", content: "We partner exclusively with local, sustainable farms and suppliers who share our commitment to quality. Every ingredient is hand-selected for freshness, flavor, and nutritional value. We never compromise on quality."
|
||||
id: "3", title: "What makes your ingredients special?", content: "We source premium, organic ingredients from trusted suppliers worldwide. Our formulas combine traditional botanicals with advanced skincare science, ensuring maximum efficacy without compromise. Every ingredient is chosen for its proven benefits."
|
||||
},
|
||||
{
|
||||
id: "4", title: "What's your satisfaction guarantee?", content: "We stand behind every meal. If you're not completely satisfied, we'll replace your order or provide a full refund, no questions asked. Your satisfaction is our highest priority."
|
||||
id: "4", title: "Do you offer a satisfaction guarantee?", content: "Absolutely! We're confident you'll love our products. If you're not completely satisfied within 60 days, we offer a full refund, no questions asked. Your skin's health and happiness are our priority."
|
||||
},
|
||||
{
|
||||
id: "5", title: "Do you offer catering services?", content: "Yes! We offer full catering services for events, corporate functions, and special occasions. Our team can customize menus to match your preferences and guest count. Contact our catering department for details."
|
||||
id: "5", title: "Are your products cruelty-free and vegan?", content: "Yes! All Lumière Skin products are certified cruelty-free and we never test on animals. Many of our formulas are also vegan, though some contain beeswax for their nourishing properties. Check individual product pages for specific details."
|
||||
},
|
||||
{
|
||||
id: "6", title: "How should I store leftover meals?", content: "Store meals in airtight containers in the refrigerator at 40°F or below. Most meals stay fresh for 3-4 days. For best flavor and texture, we recommend enjoying meals within 24 hours. Reheat gently to preserve quality."
|
||||
id: "6", title: "How should I store my skincare products?", content: "Store all products in a cool, dry place away from direct sunlight. Our serums and creams are best kept at room temperature. Avoid storing in bathrooms with high humidity when possible, as moisture can affect product integrity."
|
||||
}
|
||||
]}
|
||||
imageSrc="http://img.b2bpic.net/free-photo/woman-applying-moisturizer-her-beauty-routine_23-2150166464.jpg"
|
||||
imageAlt="Gourmet meal preparation"
|
||||
imageAlt="Skincare consultation"
|
||||
mediaAnimation="slide-up"
|
||||
mediaPosition="left"
|
||||
textboxLayout="default"
|
||||
@@ -174,8 +175,8 @@ export default function LandingPage() {
|
||||
<ContactCenter
|
||||
tag="Newsletter"
|
||||
tagIcon={Mail}
|
||||
title="Join the Food Homes Family"
|
||||
description="Subscribe to receive exclusive recipes, special menu previews, and delivery promotions. Be the first to know about new culinary creations."
|
||||
title="Join Our Skincare Community"
|
||||
description="Subscribe to receive exclusive skincare tips, product launches, and special offers. Become part of the Lumière Skin family."
|
||||
background={{ variant: "sparkles-gradient" }}
|
||||
useInvertedBackground={false}
|
||||
inputPlaceholder="Enter your email"
|
||||
@@ -186,14 +187,14 @@ export default function LandingPage() {
|
||||
|
||||
<div id="footer" data-section="footer">
|
||||
<FooterBaseCard
|
||||
logoText="Food Homes"
|
||||
logoText="Lumière Skin"
|
||||
columns={[
|
||||
{
|
||||
title: "Menu", items: [
|
||||
{ label: "All Items", href: "#products" },
|
||||
{ label: "Specials", href: "#products" },
|
||||
{ label: "Meal Plans", href: "#products" },
|
||||
{ label: "Catering", href: "#products" }
|
||||
title: "Shop", items: [
|
||||
{ label: "All Products", href: "#products" },
|
||||
{ label: "Skincare Routine", href: "#products" },
|
||||
{ label: "Collections", href: "#products" },
|
||||
{ label: "Gift Sets", href: "#products" }
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -208,7 +209,7 @@ export default function LandingPage() {
|
||||
title: "Support", items: [
|
||||
{ label: "Contact Us", href: "#contact" },
|
||||
{ label: "FAQ", href: "#faq" },
|
||||
{ label: "Delivery Info", href: "#" },
|
||||
{ label: "Shipping Info", href: "#" },
|
||||
{ label: "Returns", href: "#" }
|
||||
]
|
||||
},
|
||||
@@ -216,12 +217,12 @@ export default function LandingPage() {
|
||||
title: "Connect", items: [
|
||||
{ label: "Instagram", href: "https://instagram.com" },
|
||||
{ label: "Facebook", href: "https://facebook.com" },
|
||||
{ label: "Twitter", href: "https://twitter.com" },
|
||||
{ label: "Pinterest", href: "https://pinterest.com" },
|
||||
{ label: "TikTok", href: "https://tiktok.com" }
|
||||
]
|
||||
}
|
||||
]}
|
||||
copyrightText="© 2025 Food Homes. All rights reserved."
|
||||
copyrightText="© 2025 Lumière Skin. All rights reserved."
|
||||
/>
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
|
||||
@@ -1,178 +1,111 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
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;
|
||||
}
|
||||
import { Upload, AlertCircle, CheckCircle, Video } from "lucide-react";
|
||||
|
||||
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 [file, setFile] = useState<File | null>(null);
|
||||
const [isDragActive, setIsDragActive] = useState(false);
|
||||
const [uploadStatus, setUploadStatus] = useState<"idle" | "uploading" | "success" | "error">("idle");
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
const [videoTitle, setVideoTitle] = useState("");
|
||||
const [videoDescription, setVideoDescription] = useState("");
|
||||
|
||||
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 ALLOWED_FORMATS = ["video/mp4", "video/mpeg", "video/quicktime", "video/x-msvideo"];
|
||||
|
||||
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.' };
|
||||
const handleDrag = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
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> => {
|
||||
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 validateFile = (selectedFile: File): boolean => {
|
||||
if (selectedFile.size > MAX_FILE_SIZE) {
|
||||
setErrorMessage("File size exceeds 500MB limit");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ALLOWED_FORMATS.includes(selectedFile.type)) {
|
||||
setErrorMessage("Invalid file format. Please upload MP4, MPEG, MOV, or AVI");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleFileSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = e.currentTarget.files;
|
||||
if (!files) return;
|
||||
const handleDrop = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragActive(false);
|
||||
|
||||
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 droppedFiles = e.dataTransfer.files;
|
||||
if (droppedFiles && droppedFiles.length > 0) {
|
||||
const selectedFile = droppedFiles[0];
|
||||
if (validateFile(selectedFile)) {
|
||||
setFile(selectedFile);
|
||||
setErrorMessage("");
|
||||
setUploadStatus("idle");
|
||||
}
|
||||
|
||||
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 handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files && e.target.files.length > 0) {
|
||||
const selectedFile = e.target.files[0];
|
||||
if (validateFile(selectedFile)) {
|
||||
setFile(selectedFile);
|
||||
setErrorMessage("");
|
||||
setUploadStatus("idle");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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.');
|
||||
const handleUpload = async () => {
|
||||
if (!file || !videoTitle.trim()) {
|
||||
setErrorMessage("Please select a video and enter a title");
|
||||
return;
|
||||
}
|
||||
|
||||
const id = `video-${index}`;
|
||||
setUploadStatus((prev) => ({ ...prev, [id]: 'uploading' }));
|
||||
setUploadProgress((prev) => ({ ...prev, [id]: 0 }));
|
||||
setUploadStatus("uploading");
|
||||
setErrorMessage("");
|
||||
|
||||
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);
|
||||
formData.append("file", file);
|
||||
formData.append("title", videoTitle);
|
||||
formData.append("description", videoDescription);
|
||||
|
||||
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) }));
|
||||
}
|
||||
const response = await fetch("/api/upload-video", {
|
||||
method: "POST", body: formData,
|
||||
});
|
||||
|
||||
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' }));
|
||||
}
|
||||
});
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.message || "Upload failed");
|
||||
}
|
||||
|
||||
xhr.addEventListener('error', () => {
|
||||
setUploadStatus((prev) => ({ ...prev, [id]: 'error' }));
|
||||
});
|
||||
setUploadStatus("success");
|
||||
setFile(null);
|
||||
setVideoTitle("");
|
||||
setVideoDescription("");
|
||||
|
||||
xhr.open('POST', '/api/videos/upload');
|
||||
xhr.send(formData);
|
||||
setTimeout(() => {
|
||||
setUploadStatus("idle");
|
||||
}, 3000);
|
||||
} 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 (
|
||||
<ThemeProvider
|
||||
defaultButtonVariant="hover-bubble"
|
||||
@@ -189,11 +122,11 @@ export default function VideoUploadPage() {
|
||||
<div id="nav" data-section="nav">
|
||||
<NavbarStyleCentered
|
||||
navItems={[
|
||||
{ name: "Home", id: "/" },
|
||||
{ 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: "/" }}
|
||||
@@ -201,164 +134,146 @@ export default function VideoUploadPage() {
|
||||
/>
|
||||
</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 className="min-h-screen bg-gradient-to-b from-background to-background/50 py-16 px-4">
|
||||
<div className="max-w-2xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="text-center mb-12">
|
||||
<div className="flex items-center justify-center mb-4">
|
||||
<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 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>
|
||||
{/* Upload Area */}
|
||||
<div className="bg-card rounded-lg p-8 mb-8 border border-accent/20">
|
||||
<div
|
||||
onDragEnter={handleDrag}
|
||||
onDragLeave={handleDrag}
|
||||
onDragOver={handleDrag}
|
||||
onDrop={handleDrop}
|
||||
className={`border-2 border-dashed rounded-lg p-12 text-center cursor-pointer transition-colors ${
|
||||
isDragActive
|
||||
? "border-primary-cta bg-primary-cta/5"
|
||||
: "border-accent/30 hover:border-accent/50"
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
multiple
|
||||
accept="video/mp4,video/quicktime,video/x-msvideo,video/webm"
|
||||
accept="video/*"
|
||||
onChange={handleFileSelect}
|
||||
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>
|
||||
|
||||
{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;
|
||||
{/* Form Fields */}
|
||||
<div className="bg-card rounded-lg p-8 mb-8 border border-accent/20">
|
||||
<div className="mb-6">
|
||||
<label className="block text-foreground font-semibold mb-2">
|
||||
Video Title *
|
||||
</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 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="mb-6">
|
||||
<label className="block text-foreground font-semibold mb-2">
|
||||
Video Description
|
||||
</label>
|
||||
<textarea
|
||||
value={videoDescription}
|
||||
onChange={(e) => setVideoDescription(e.target.value)}
|
||||
placeholder="Describe your recipe, ingredients, and cooking process..."
|
||||
rows={5}
|
||||
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 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>
|
||||
)}
|
||||
{/* Status Messages */}
|
||||
{errorMessage && (
|
||||
<div className="flex items-center gap-3 p-4 bg-red-500/10 border border-red-500/30 rounded-lg mb-6">
|
||||
<AlertCircle className="w-5 h-5 text-red-500 flex-shrink-0" />
|
||||
<p className="text-red-600 font-medium">{errorMessage}</p>
|
||||
</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>
|
||||
{uploadStatus === "success" && (
|
||||
<div className="flex items-center gap-3 p-4 bg-green-500/10 border border-green-500/30 rounded-lg mb-6">
|
||||
<CheckCircle className="w-5 h-5 text-green-500 flex-shrink-0" />
|
||||
<p className="text-green-600 font-medium">Video uploaded successfully!</p>
|
||||
</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 id="footer" data-section="footer" className="mt-20">
|
||||
<div id="footer" data-section="footer">
|
||||
<FooterBaseCard
|
||||
logoText="Lumière Skin"
|
||||
columns={[
|
||||
|
||||
@@ -1,11 +1,66 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useMemo } from "react";
|
||||
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
|
||||
import NavbarStyleCentered from '@/components/navbar/NavbarStyleCentered/NavbarStyleCentered';
|
||||
import BlogCardTwo from '@/components/sections/blog/BlogCardTwo';
|
||||
import HeroLogoBillboard from '@/components/sections/hero/HeroLogoBillboard';
|
||||
import FooterBaseCard from '@/components/sections/footer/FooterBaseCard';
|
||||
import { Play, X } from "lucide-react";
|
||||
|
||||
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() {
|
||||
const [selectedVideo, setSelectedVideo] = useState<Video | null>(null);
|
||||
const [cuisineFilter, setCuisineFilter] = useState<string>("All");
|
||||
const [chefFilter, setChefFilter] = useState<string>("All");
|
||||
|
||||
const cuisines = ["All", ...Array.from(new Set(videos.map(v => v.cuisine)))];
|
||||
const chefs = ["All", ...Array.from(new Set(videos.map(v => v.chef)))];
|
||||
|
||||
const filteredVideos = useMemo(() => {
|
||||
return videos.filter(video => {
|
||||
const cuisineMatch = cuisineFilter === "All" || video.cuisine === cuisineFilter;
|
||||
const chefMatch = chefFilter === "All" || video.chef === chefFilter;
|
||||
return cuisineMatch && chefMatch;
|
||||
});
|
||||
}, [cuisineFilter, chefFilter]);
|
||||
|
||||
return (
|
||||
<ThemeProvider
|
||||
defaultButtonVariant="hover-bubble"
|
||||
@@ -26,60 +81,185 @@ export default function VideosPage() {
|
||||
{ name: "Products", id: "products" },
|
||||
{ name: "Features", id: "features" },
|
||||
{ name: "Reviews", id: "testimonials" },
|
||||
{ name: "Videos", id: "videos" },
|
||||
{ name: "Contact", id: "contact" }
|
||||
]}
|
||||
button={{ text: "Shop Now", href: "products" }}
|
||||
brandName="Food Homes"
|
||||
button={{ text: "Shop Now", href: "/" }}
|
||||
brandName="Lumière Skin"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="blog" data-section="blog">
|
||||
<BlogCardTwo
|
||||
blogs={[
|
||||
{
|
||||
id: "1", category: "Recipe", title: "How to Prepare the Perfect Grilled Salmon", excerpt: "Learn the secrets to preparing restaurant-quality grilled salmon at home with our step-by-step guide.", imageSrc: "http://img.b2bpic.net/free-photo/make-up-concept-with-pink-nail-polish_23-2149030370.jpg", imageAlt: "Grilled Salmon", authorName: "Chef Maria", 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: "15 Jan 2025"
|
||||
},
|
||||
{
|
||||
id: "2", category: "Nutrition", title: "The Benefits of Eating Fresh, Local Ingredients", excerpt: "Discover why locally-sourced ingredients are better for your health and the environment.", imageSrc: "http://img.b2bpic.net/free-photo/flat-lay-hands-holding-body-care-product-wooden-background_23-2148241876.jpg", imageAlt: "Fresh Ingredients", authorName: "Dr. James", authorAvatar: "http://img.b2bpic.net/free-photo/young-beautiful-woman-having-online-meeting_23-2149116347.jpg", date: "12 Jan 2025"
|
||||
},
|
||||
{
|
||||
id: "3", category: "Lifestyle", title: "Meal Planning Tips for Busy Professionals", excerpt: "Simplify your week with our practical meal planning strategies that save time and reduce stress.", imageSrc: "http://img.b2bpic.net/free-photo/product-branding-packaging_23-2150965833.jpg", imageAlt: "Meal Planning", authorName: "Sarah Chen", authorAvatar: "http://img.b2bpic.net/free-photo/young-women-polishing-nails-bed_23-2147770248.jpg", date: "10 Jan 2025"
|
||||
}
|
||||
<div id="hero" data-section="hero">
|
||||
<HeroLogoBillboard
|
||||
logoText="Skincare Videos"
|
||||
description="Expert tutorials, product guides, and educational content to help you master your skincare routine."
|
||||
buttons={[
|
||||
{ text: "Back to Home", href: "/" },
|
||||
{ text: "Shop Collection", href: "/" }
|
||||
]}
|
||||
title="Featured Food Articles"
|
||||
description="Read our latest insights about food, nutrition, and culinary experiences."
|
||||
animationType="slide-up"
|
||||
textboxLayout="default"
|
||||
useInvertedBackground={false}
|
||||
carouselMode="buttons"
|
||||
background={{ variant: "sparkles-gradient" }}
|
||||
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 id="videos" data-section="videos" className="py-20">
|
||||
<div className="w-full max-w-7xl mx-auto px-6">
|
||||
{/* Filters */}
|
||||
<div className="mb-12">
|
||||
<div className="grid md:grid-cols-2 gap-8">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">Filter by Category</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{cuisines.map(cuisine => (
|
||||
<button
|
||||
key={cuisine}
|
||||
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>
|
||||
<h3 className="text-lg font-semibold mb-4">Filter by Expert</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{chefs.map(chef => (
|
||||
<button
|
||||
key={chef}
|
||||
onClick={() => setChefFilter(chef)}
|
||||
className={`px-4 py-2 rounded-lg transition-all ${
|
||||
chefFilter === chef
|
||||
? "bg-gradient-to-r from-pink-500 to-rose-500 text-white"
|
||||
: "bg-gray-200 text-gray-700 hover:bg-gray-300"
|
||||
}`}
|
||||
>
|
||||
{chef}
|
||||
</button>
|
||||
))}
|
||||
</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>
|
||||
|
||||
{/* Video Player Modal */}
|
||||
{selectedVideo && (
|
||||
<div className="fixed inset-0 bg-black/80 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-xl overflow-hidden max-w-4xl w-full">
|
||||
<div className="flex justify-between items-center p-6 border-b">
|
||||
<h2 className="text-xl font-semibold">{selectedVideo.title}</h2>
|
||||
<button
|
||||
onClick={() => setSelectedVideo(null)}
|
||||
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
||||
>
|
||||
<X className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="aspect-video bg-black">
|
||||
<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 id="footer" data-section="footer">
|
||||
<FooterBaseCard
|
||||
logoText="Food Homes"
|
||||
logoText="Lumière Skin"
|
||||
columns={[
|
||||
{
|
||||
title: "Menu", items: [
|
||||
{ label: "All Items", href: "#products" },
|
||||
{ label: "Specials", href: "#products" },
|
||||
{ label: "Meal Plans", href: "#products" },
|
||||
{ label: "Catering", href: "#products" }
|
||||
title: "Shop", items: [
|
||||
{ label: "All Products", href: "/" },
|
||||
{ label: "Skincare Routine", href: "/" },
|
||||
{ label: "Collections", href: "/" },
|
||||
{ label: "Gift Sets", href: "/" }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Company", items: [
|
||||
{ label: "About Us", href: "#about" },
|
||||
{ label: "Our Story", href: "#about" },
|
||||
{ label: "About Us", href: "/" },
|
||||
{ label: "Our Story", href: "/" },
|
||||
{ label: "Sustainability", href: "#" },
|
||||
{ label: "Careers", href: "#" }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Support", items: [
|
||||
{ label: "Contact Us", href: "#contact" },
|
||||
{ label: "FAQ", href: "#faq" },
|
||||
{ label: "Delivery Info", href: "#" },
|
||||
{ label: "Contact Us", href: "/" },
|
||||
{ label: "FAQ", href: "/" },
|
||||
{ label: "Shipping Info", href: "#" },
|
||||
{ label: "Returns", href: "#" }
|
||||
]
|
||||
},
|
||||
@@ -87,12 +267,12 @@ export default function VideosPage() {
|
||||
title: "Connect", items: [
|
||||
{ label: "Instagram", href: "https://instagram.com" },
|
||||
{ label: "Facebook", href: "https://facebook.com" },
|
||||
{ label: "Twitter", href: "https://twitter.com" },
|
||||
{ label: "Pinterest", href: "https://pinterest.com" },
|
||||
{ label: "TikTok", href: "https://tiktok.com" }
|
||||
]
|
||||
}
|
||||
]}
|
||||
copyrightText="© 2025 Food Homes. All rights reserved."
|
||||
copyrightText="© 2025 Lumière Skin. All rights reserved."
|
||||
/>
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
|
||||
Reference in New Issue
Block a user