Merge version_2 into main #3
@@ -1,76 +1,22 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Halant } from "next/font/google";
|
||||
import { Inter } from "next/font/google";
|
||||
import { Open_Sans } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { ServiceWrapper } from "@/components/ServiceWrapper";
|
||||
import Tag from "@/tag/Tag";
|
||||
|
||||
const halant = Halant({
|
||||
variable: "--font-halant",
|
||||
subsets: ["latin"],
|
||||
weight: ["300", "400", "500", "600", "700"],
|
||||
});
|
||||
|
||||
const inter = Inter({
|
||||
variable: "--font-inter",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const openSans = Open_Sans({
|
||||
variable: "--font-open-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "WinSite - Buy & Sell Ready-to-Launch Digital Products",
|
||||
description: "Launch your next digital project in minutes. Browse thousands of ready-made websites, SaaS tools, templates, and AI-generated assets. Skip building. Start winning.",
|
||||
keywords: "digital marketplace, ready-made websites, SaaS templates, AI tools, digital products, online business",
|
||||
metadataBase: new URL("https://winsite.com"),
|
||||
alternates: {
|
||||
canonical: "https://winsite.com",
|
||||
},
|
||||
openGraph: {
|
||||
title: "WinSite - Digital Products Marketplace",
|
||||
description: "The easiest place to buy and sell ready-to-launch digital products and AI-generated online businesses.",
|
||||
url: "https://winsite.com",
|
||||
siteName: "WinSite",
|
||||
type: "website",
|
||||
images: [
|
||||
{
|
||||
url: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AayI52Vzbi70YDU2rW87pd8RUc/a-modern-saas-landing-page-template-with-1772845779916-57149c31.png",
|
||||
alt: "WinSite Digital Marketplace Platform",
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: "WinSite - Launch Digital Projects in Minutes",
|
||||
description: "Buy ready-to-launch websites, tools, templates, and AI assets. Sell your digital products to thousands of entrepreneurs.",
|
||||
images: [
|
||||
"https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AayI52Vzbi70YDU2rW87pd8RUc/a-modern-saas-landing-page-template-with-1772845779916-57149c31.png",
|
||||
],
|
||||
},
|
||||
robots: {
|
||||
index: true,
|
||||
follow: true,
|
||||
},
|
||||
};
|
||||
title: "WinSite - Digital Marketplace", description: "A marketplace for ready-made websites, tools, templates, and digital businesses."};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
}) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<ServiceWrapper>
|
||||
<body
|
||||
className={`${halant.variable} ${inter.variable} ${openSans.variable} antialiased`}
|
||||
>
|
||||
<Tag />
|
||||
{children}
|
||||
|
||||
<html lang="en">
|
||||
<body className={inter.className}>
|
||||
{children}
|
||||
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
@@ -1438,7 +1384,6 @@ export default function RootLayout({
|
||||
}}
|
||||
/>
|
||||
</body>
|
||||
</ServiceWrapper>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import TestimonialCardTwo from "@/components/sections/testimonial/TestimonialCar
|
||||
import ContactSplit from "@/components/sections/contact/ContactSplit";
|
||||
import FooterBaseReveal from "@/components/sections/footer/FooterBaseReveal";
|
||||
import Link from "next/link";
|
||||
import { Zap, Star, Workflow, TrendingUp, Flame, Sparkles, Heart, Mail, Trophy, CheckCircle } from "lucide-react";
|
||||
import { Zap, Star, Workflow, TrendingUp, Flame, Sparkles, Heart, Mail, Trophy, CheckCircle, Filter, ShoppingCart, MessageCircle, Bookmark } from "lucide-react";
|
||||
|
||||
export default function HomePage() {
|
||||
const navItems = [
|
||||
@@ -290,4 +290,4 @@ export default function HomePage() {
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
352
src/app/profile/page.tsx
Normal file
352
src/app/profile/page.tsx
Normal file
@@ -0,0 +1,352 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
|
||||
import NavbarStyleApple from "@/components/navbar/NavbarStyleApple/NavbarStyleApple";
|
||||
import FooterBaseReveal from "@/components/sections/footer/FooterBaseReveal";
|
||||
import Input from "@/components/form/Input";
|
||||
import { User, Mail, Lock, MapPin, Phone, Save, LogOut, Settings, Heart, Download, TrendingUp } from "lucide-react";
|
||||
|
||||
export default function ProfilePage() {
|
||||
const navItems = [
|
||||
{ name: "Browse", id: "marketplace" },
|
||||
{ name: "How It Works", id: "how-it-works" },
|
||||
{ name: "Pricing", id: "pricing" },
|
||||
{ name: "Creators", id: "creators" },
|
||||
{ name: "Profile", id: "/profile" },
|
||||
{ name: "Contact", id: "contact" },
|
||||
];
|
||||
|
||||
// Profile state
|
||||
const [profile, setProfile] = useState({
|
||||
fullName: "John Doe", email: "john@example.com", phone: "+1 (555) 123-4567", location: "San Francisco, CA"});
|
||||
|
||||
// Account settings state
|
||||
const [settings, setSettings] = useState({
|
||||
twoFactorAuth: false,
|
||||
emailNotifications: true,
|
||||
marketingEmails: false,
|
||||
privateProfile: false,
|
||||
});
|
||||
|
||||
// Form state for editing
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [formData, setFormData] = useState(profile);
|
||||
|
||||
// Activity state
|
||||
const [activityData] = useState([
|
||||
{ id: 1, type: "purchase", title: "Purchased SaaS Landing Page", amount: "-$299", date: "2 hours ago" },
|
||||
{ id: 2, type: "sale", title: "Sold 3x Template Collection", amount: "+$450", date: "1 day ago" },
|
||||
{ id: 3, type: "download", title: "Downloaded Design System", amount: "-$99", date: "3 days ago" },
|
||||
{ id: 4, type: "purchase", title: "Purchased AI Tool", amount: "-$199", date: "1 week ago" },
|
||||
]);
|
||||
|
||||
// Wishlist state
|
||||
const [wishlistItems] = useState([
|
||||
{ id: 1, name: "Advanced E-commerce Template", price: "$399", category: "Templates" },
|
||||
{ id: 2, name: "AI Content Generator Pro", price: "$599", category: "AI Tools" },
|
||||
{ id: 3, name: "Premium SaaS Boilerplate", price: "$499", category: "SaaS" },
|
||||
]);
|
||||
|
||||
const handleProfileChange = (key: string, value: string) => {
|
||||
setFormData({ ...formData, [key]: value });
|
||||
};
|
||||
|
||||
const handleSaveProfile = () => {
|
||||
setProfile(formData);
|
||||
setIsEditing(false);
|
||||
};
|
||||
|
||||
const handleSettingChange = (key: string) => {
|
||||
setSettings({ ...settings, [key]: !settings[key] });
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeProvider
|
||||
defaultButtonVariant="elastic-effect"
|
||||
defaultTextAnimation="reveal-blur"
|
||||
borderRadius="soft"
|
||||
contentWidth="mediumSmall"
|
||||
sizing="mediumLargeSizeLargeTitles"
|
||||
background="aurora"
|
||||
cardStyle="glass-depth"
|
||||
primaryButtonStyle="double-inset"
|
||||
secondaryButtonStyle="solid"
|
||||
headingFontWeight="medium"
|
||||
>
|
||||
<div id="nav" data-section="nav">
|
||||
<NavbarStyleApple
|
||||
brandName="WinSite"
|
||||
navItems={navItems}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="min-h-screen pt-24 pb-20 px-4">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="mb-12">
|
||||
<h1 className="text-4xl font-bold mb-2">User Profile</h1>
|
||||
<p className="text-foreground/75">Manage your account settings and preferences</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{/* Left Sidebar - Stats */}
|
||||
<div className="lg:col-span-1">
|
||||
<div className="bg-card rounded-lg p-6 border border-accent/20 space-y-4">
|
||||
<div className="flex items-center justify-center w-16 h-16 bg-primary-cta rounded-full mx-auto mb-4">
|
||||
<User className="w-8 h-8 text-background" />
|
||||
</div>
|
||||
<h2 className="text-xl font-semibold text-center">{profile.fullName}</h2>
|
||||
<p className="text-sm text-foreground/60 text-center">{profile.email}</p>
|
||||
|
||||
<div className="pt-4 border-t border-accent/20">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="text-center">
|
||||
<p className="text-2xl font-bold text-primary-cta">12</p>
|
||||
<p className="text-xs text-foreground/60 mt-1">Purchases</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-2xl font-bold text-secondary-cta">$2.4K</p>
|
||||
<p className="text-xs text-foreground/60 mt-1">Total Spent</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button className="w-full mt-6 px-4 py-2 bg-secondary-cta/10 hover:bg-secondary-cta/20 rounded-lg text-sm font-medium transition flex items-center justify-center gap-2">
|
||||
<LogOut className="w-4 h-4" />
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="lg:col-span-2 space-y-8">
|
||||
{/* Profile Information Section */}
|
||||
<div className="bg-card rounded-lg p-8 border border-accent/20">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-2xl font-bold flex items-center gap-2">
|
||||
<User className="w-6 h-6 text-primary-cta" />
|
||||
Profile Information
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => setIsEditing(!isEditing)}
|
||||
className="px-4 py-2 bg-primary-cta hover:bg-primary-cta/90 text-background rounded-lg text-sm font-medium transition"
|
||||
>
|
||||
{isEditing ? "Cancel" : "Edit"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isEditing ? (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-2 block">Full Name</label>
|
||||
<Input
|
||||
value={formData.fullName}
|
||||
onChange={(val) => handleProfileChange("fullName", val)}
|
||||
placeholder="Full Name"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-2 block">Email</label>
|
||||
<Input
|
||||
value={formData.email}
|
||||
onChange={(val) => handleProfileChange("email", val)}
|
||||
type="email"
|
||||
placeholder="Email"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-2 block">Phone</label>
|
||||
<Input
|
||||
value={formData.phone}
|
||||
onChange={(val) => handleProfileChange("phone", val)}
|
||||
placeholder="Phone Number"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-2 block">Location</label>
|
||||
<Input
|
||||
value={formData.location}
|
||||
onChange={(val) => handleProfileChange("location", val)}
|
||||
placeholder="City, State"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleSaveProfile}
|
||||
className="w-full px-4 py-2 bg-primary-cta hover:bg-primary-cta/90 text-background rounded-lg font-medium transition flex items-center justify-center gap-2 mt-6"
|
||||
>
|
||||
<Save className="w-4 h-4" />
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<p className="text-sm text-foreground/60 mb-1">Full Name</p>
|
||||
<p className="font-medium">{profile.fullName}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-foreground/60 mb-1 flex items-center gap-2">
|
||||
<Mail className="w-4 h-4" />
|
||||
Email
|
||||
</p>
|
||||
<p className="font-medium">{profile.email}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-foreground/60 mb-1 flex items-center gap-2">
|
||||
<Phone className="w-4 h-4" />
|
||||
Phone
|
||||
</p>
|
||||
<p className="font-medium">{profile.phone}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-foreground/60 mb-1 flex items-center gap-2">
|
||||
<MapPin className="w-4 h-4" />
|
||||
Location
|
||||
</p>
|
||||
<p className="font-medium">{profile.location}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Account Settings Section */}
|
||||
<div className="bg-card rounded-lg p-8 border border-accent/20">
|
||||
<h2 className="text-2xl font-bold flex items-center gap-2 mb-6">
|
||||
<Settings className="w-6 h-6 text-primary-cta" />
|
||||
Account Settings
|
||||
</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
{[
|
||||
{ key: "twoFactorAuth", label: "Two-Factor Authentication", desc: "Add an extra layer of security" },
|
||||
{ key: "emailNotifications", label: "Email Notifications", desc: "Get updates on your activities" },
|
||||
{ key: "marketingEmails", label: "Marketing Emails", desc: "Receive promotional offers and updates" },
|
||||
{ key: "privateProfile", label: "Private Profile", desc: "Hide your profile from public view" },
|
||||
].map((item) => (
|
||||
<div key={item.key} className="flex items-center justify-between p-4 border border-accent/10 rounded-lg hover:bg-accent/5 transition">
|
||||
<div>
|
||||
<p className="font-medium">{item.label}</p>
|
||||
<p className="text-sm text-foreground/60">{item.desc}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleSettingChange(item.key)}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
||||
settings[item.key as keyof typeof settings] ? "bg-primary-cta" : "bg-accent/20"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-background transition-transform ${
|
||||
settings[item.key as keyof typeof settings] ? "translate-x-5" : "translate-x-1"
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recent Activity & Wishlist */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mt-8">
|
||||
{/* Recent Activity */}
|
||||
<div className="bg-card rounded-lg p-8 border border-accent/20">
|
||||
<h2 className="text-2xl font-bold flex items-center gap-2 mb-6">
|
||||
<TrendingUp className="w-6 h-6 text-primary-cta" />
|
||||
Recent Activity
|
||||
</h2>
|
||||
|
||||
<div className="space-y-3">
|
||||
{activityData.map((item) => (
|
||||
<div key={item.id} className="flex items-center justify-between p-3 border border-accent/10 rounded-lg hover:bg-accent/5 transition">
|
||||
<div className="flex items-center gap-3 flex-1">
|
||||
<div className="w-10 h-10 rounded-full bg-primary-cta/10 flex items-center justify-center">
|
||||
{item.type === "purchase" && <Download className="w-5 h-5 text-primary-cta" />}
|
||||
{item.type === "sale" && <TrendingUp className="w-5 h-5 text-secondary-cta" />}
|
||||
{item.type === "download" && <Download className="w-5 h-5 text-accent" />}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-medium text-sm">{item.title}</p>
|
||||
<p className="text-xs text-foreground/60">{item.date}</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className={`font-semibold text-sm ${
|
||||
item.amount.startsWith("+") ? "text-secondary-cta" : "text-foreground"
|
||||
}`}>
|
||||
{item.amount}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Wishlist */}
|
||||
<div className="bg-card rounded-lg p-8 border border-accent/20">
|
||||
<h2 className="text-2xl font-bold flex items-center gap-2 mb-6">
|
||||
<Heart className="w-6 h-6 text-primary-cta" />
|
||||
Wishlist ({wishlistItems.length})
|
||||
</h2>
|
||||
|
||||
<div className="space-y-3">
|
||||
{wishlistItems.map((item) => (
|
||||
<div key={item.id} className="flex items-center justify-between p-3 border border-accent/10 rounded-lg hover:bg-accent/5 transition">
|
||||
<div className="flex-1">
|
||||
<p className="font-medium text-sm">{item.name}</p>
|
||||
<p className="text-xs text-foreground/60">{item.category}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<p className="font-semibold text-sm text-primary-cta">{item.price}</p>
|
||||
<button className="px-3 py-1 bg-primary-cta/10 hover:bg-primary-cta/20 rounded text-xs font-medium transition">
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="footer" data-section="footer">
|
||||
<FooterBaseReveal
|
||||
columns={[
|
||||
{
|
||||
title: "Marketplace", items: [
|
||||
{ label: "Browse Assets", href: "/marketplace" },
|
||||
{ label: "Categories", href: "/marketplace" },
|
||||
{ label: "Trending", href: "#trending" },
|
||||
{ label: "New Releases", href: "/marketplace" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "For Creators", items: [
|
||||
{ label: "Become a Seller", href: "https://example.com/become-seller" },
|
||||
{ label: "Creator Dashboard", href: "/dashboard" },
|
||||
{ label: "Seller Guide", href: "#" },
|
||||
{ label: "Payout Policy", href: "#" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Company", items: [
|
||||
{ label: "About", href: "/about" },
|
||||
{ label: "Blog", href: "#" },
|
||||
{ label: "Contact", href: "/contact" },
|
||||
{ label: "Careers", href: "#" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Legal", items: [
|
||||
{ label: "Privacy Policy", href: "#" },
|
||||
{ label: "Terms of Service", href: "#" },
|
||||
{ label: "Cookie Policy", href: "#" },
|
||||
],
|
||||
},
|
||||
]}
|
||||
copyrightText="© 2025 WinSite. All rights reserved."
|
||||
/>
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
@@ -10,15 +10,15 @@
|
||||
--accent: #ffffff;
|
||||
--background-accent: #ffffff; */
|
||||
|
||||
--background: #fcf6ec;
|
||||
--card: #f3ede2;
|
||||
--foreground: #2e2521;
|
||||
--primary-cta: #ff8c42;
|
||||
--background: #0a0a0a;
|
||||
--card: #1a1a1a;
|
||||
--foreground: #ffffffe6;
|
||||
--primary-cta: #ffffff;
|
||||
--primary-cta-text: #ffffff;
|
||||
--secondary-cta: #ffffff;
|
||||
--secondary-cta: #1a1a1a;
|
||||
--secondary-cta-text: #2e2521;
|
||||
--accent: #b2a28b;
|
||||
--background-accent: #b2a28b;
|
||||
--accent: #737373;
|
||||
--background-accent: #737373;
|
||||
|
||||
/* text sizing - set by ThemeProvider */
|
||||
/* --text-2xs: clamp(0.465rem, 0.62vw, 0.62rem);
|
||||
|
||||
55
src/components/auth/GoogleLoginButton/GoogleLoginButton.tsx
Normal file
55
src/components/auth/GoogleLoginButton/GoogleLoginButton.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useAuth } from "@/providers/authProvider/AuthProvider";
|
||||
|
||||
interface GoogleLoginButtonProps {
|
||||
onSuccess?: () => void;
|
||||
onError?: (error: string) => void;
|
||||
text?: "signin_with" | "signup_with" | "signin" | "signup";
|
||||
size?: "large" | "medium" | "small";
|
||||
theme?: "outline" | "filled_blue" | "filled_black";
|
||||
locale?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function GoogleLoginButton({
|
||||
onSuccess,
|
||||
onError,
|
||||
text = "signin_with", size = "large", theme = "outline", locale = "en", className = ""}: GoogleLoginButtonProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const { login, isLoading } = useAuth();
|
||||
|
||||
useEffect(() => {
|
||||
const handleCredentialResponse = async (response: any) => {
|
||||
try {
|
||||
await login(response.credential);
|
||||
onSuccess?.();
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : "Login failed";
|
||||
onError?.(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof window !== "undefined" && window.google && containerRef.current) {
|
||||
window.google.accounts.id.renderButton(containerRef.current, {
|
||||
type: "standard", theme: theme,
|
||||
size: size,
|
||||
text: text,
|
||||
locale: locale,
|
||||
callback: handleCredentialResponse,
|
||||
});
|
||||
}
|
||||
}, [login, onSuccess, onError]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={className}
|
||||
style={{
|
||||
display: "flex", justifyContent: "center", pointerEvents: isLoading ? "none" : "auto", opacity: isLoading ? 0.6 : 1,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
"use client";
|
||||
|
||||
import { useAuth } from "@/providers/authProvider/AuthProvider";
|
||||
import { LogOut } from "lucide-react";
|
||||
|
||||
interface GoogleLogoutButtonProps {
|
||||
className?: string;
|
||||
textClassName?: string;
|
||||
iconClassName?: string;
|
||||
showIcon?: boolean;
|
||||
showText?: boolean;
|
||||
}
|
||||
|
||||
export default function GoogleLogoutButton({
|
||||
className = "", textClassName = "", iconClassName = "", showIcon = true,
|
||||
showText = true,
|
||||
}: GoogleLogoutButtonProps) {
|
||||
const { logout } = useAuth();
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={logout}
|
||||
className={className}
|
||||
aria-label="Sign out"
|
||||
>
|
||||
{showIcon && <LogOut className={iconClassName} />}
|
||||
{showText && <span className={textClassName}>Sign Out</span>}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
41
src/components/auth/UserProfile/UserProfile.tsx
Normal file
41
src/components/auth/UserProfile/UserProfile.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
"use client";
|
||||
|
||||
import { useAuth } from "@/providers/authProvider/AuthProvider";
|
||||
import Image from "next/image";
|
||||
|
||||
interface UserProfileProps {
|
||||
className?: string;
|
||||
nameClassName?: string;
|
||||
emailClassName?: string;
|
||||
imageClassName?: string;
|
||||
containerClassName?: string;
|
||||
}
|
||||
|
||||
export default function UserProfile({
|
||||
className = "", nameClassName = "", emailClassName = "", imageClassName = "", containerClassName = ""}: UserProfileProps) {
|
||||
const { user, isAuthenticated } = useAuth();
|
||||
|
||||
if (!isAuthenticated || !user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${containerClassName} flex items-center gap-3`}>
|
||||
{user.picture && (
|
||||
<div className={imageClassName}>
|
||||
<Image
|
||||
src={user.picture}
|
||||
alt={user.name}
|
||||
width={40}
|
||||
height={40}
|
||||
className="rounded-full"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className={className}>
|
||||
<p className={nameClassName}>{user.name}</p>
|
||||
<p className={emailClassName}>{user.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,148 +1,131 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback } from "react";
|
||||
import MobileMenu from "../mobileMenu/MobileMenu";
|
||||
import Button from "@/components/button/Button";
|
||||
import ButtonTextUnderline from "@/components/button/ButtonTextUnderline";
|
||||
import Logo from "../Logo";
|
||||
import { Plus } from "lucide-react";
|
||||
import { NavbarProps } from "@/types/navigation";
|
||||
import { useScrollState } from "./useScrollState";
|
||||
import { cls } from "@/lib/utils";
|
||||
import { getButtonProps } from "@/lib/buttonUtils";
|
||||
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||
import type { ButtonConfig } from "@/types/button";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { useAuth } from "@/providers/authProvider/AuthProvider";
|
||||
import GoogleLoginButton from "@/components/auth/GoogleLoginButton/GoogleLoginButton";
|
||||
import GoogleLogoutButton from "@/components/auth/GoogleLogoutButton/GoogleLogoutButton";
|
||||
import UserProfile from "@/components/auth/UserProfile/UserProfile";
|
||||
|
||||
const SCROLL_THRESHOLD = 50;
|
||||
|
||||
interface NavbarStyleAppleProps extends NavbarProps {
|
||||
button?: ButtonConfig;
|
||||
buttonClassName?: string;
|
||||
buttonTextClassName?: string;
|
||||
interface NavItem {
|
||||
name: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
const NavbarStyleApple = ({
|
||||
navItems,
|
||||
// logoSrc,
|
||||
// logoAlt = "",
|
||||
brandName = "Webild",
|
||||
button,
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
}: NavbarStyleAppleProps) => {
|
||||
const isScrolled = useScrollState(SCROLL_THRESHOLD);
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const theme = useTheme();
|
||||
interface NavbarStyleAppleProps {
|
||||
brandName?: string;
|
||||
navItems?: NavItem[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const handleMenuToggle = useCallback(() => {
|
||||
setMenuOpen((prev) => !prev);
|
||||
}, []);
|
||||
export default function NavbarStyleApple({
|
||||
brandName = "Webild", navItems = [],
|
||||
className = ""}: NavbarStyleAppleProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { isAuthenticated, user } = useAuth();
|
||||
|
||||
const handleMobileNavClick = useCallback(() => {
|
||||
setMenuOpen(false);
|
||||
}, []);
|
||||
|
||||
const getButtonConfigProps = () => {
|
||||
if (theme.defaultButtonVariant === "hover-bubble") {
|
||||
return { bgClassName: "w-full" };
|
||||
const handleNavClick = (id: string) => {
|
||||
if (id.startsWith("http")) {
|
||||
window.open(id, "_blank");
|
||||
} else {
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: "smooth" });
|
||||
}
|
||||
}
|
||||
if (theme.defaultButtonVariant === "icon-arrow") {
|
||||
return { className: "justify-between" };
|
||||
}
|
||||
return {};
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<nav
|
||||
className={cls(
|
||||
"fixed z-[1000] top-0 left-0 w-full transition-all duration-500 ease-in-out",
|
||||
isScrolled
|
||||
? "bg-background/80 backdrop-blur-sm h-15"
|
||||
: "bg-background/0 backdrop-blur-0 h-20"
|
||||
)}
|
||||
className={`fixed top-4 left-4 right-4 z-50 rounded-full bg-white/80 backdrop-blur-md border border-white/20 shadow-lg ${className}`}
|
||||
>
|
||||
<div className="relative flex items-center justify-between h-full w-content-width mx-auto">
|
||||
<div className="flex items-center transition-all duration-500 ease-in-out">
|
||||
<Logo brandName={brandName} href="/" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cls(
|
||||
"hidden md:flex items-center gap-6 transition-all duration-500 ease-in-out",
|
||||
button && "absolute left-1/2 -translate-x-1/2"
|
||||
)}
|
||||
role="navigation"
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
{/* Logo */}
|
||||
<Link
|
||||
href="/"
|
||||
className="text-xl font-bold text-foreground hover:opacity-80 transition-opacity"
|
||||
>
|
||||
{navItems.map((item, index) => (
|
||||
<ButtonTextUnderline
|
||||
key={index}
|
||||
text={item.name}
|
||||
href={item.id}
|
||||
className="!text-base"
|
||||
/>
|
||||
{brandName}
|
||||
</Link>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<div className="hidden md:flex items-center gap-8">
|
||||
{navItems.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => handleNavClick(item.id)}
|
||||
className="text-sm text-foreground/70 hover:text-foreground transition-colors"
|
||||
>
|
||||
{item.name}
|
||||
</button>
|
||||
))}
|
||||
{!button && null}
|
||||
</div>
|
||||
|
||||
{button && (
|
||||
<div className="hidden md:block">
|
||||
<Button
|
||||
{...getButtonProps(
|
||||
button,
|
||||
0,
|
||||
theme.defaultButtonVariant,
|
||||
buttonClassName,
|
||||
buttonTextClassName
|
||||
)}
|
||||
{/* Auth Section */}
|
||||
<div className="flex items-center gap-4">
|
||||
{isAuthenticated ? (
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="hidden sm:block">
|
||||
<UserProfile
|
||||
nameClassName="text-sm font-medium text-foreground"
|
||||
emailClassName="text-xs text-foreground/60"
|
||||
/>
|
||||
</div>
|
||||
<GoogleLogoutButton
|
||||
className="px-4 py-2 rounded-full bg-primary-cta text-white hover:opacity-90 transition-opacity text-sm font-medium"
|
||||
textClassName=""
|
||||
showIcon={false}
|
||||
showText={true}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<GoogleLoginButton
|
||||
text="signin_with"
|
||||
size="large"
|
||||
theme="outline"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu Button */}
|
||||
<button
|
||||
className="flex md:hidden shrink-0 h-8 aspect-square rounded-theme bg-foreground items-center justify-center cursor-pointer"
|
||||
onClick={handleMenuToggle}
|
||||
className="md:hidden p-2"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
aria-label="Toggle menu"
|
||||
aria-expanded={menuOpen}
|
||||
aria-controls="mobile-menu"
|
||||
>
|
||||
<Plus
|
||||
className={cls(
|
||||
"w-1/2 h-1/2 text-background transition-transform duration-300",
|
||||
menuOpen ? "rotate-45" : "rotate-0"
|
||||
)}
|
||||
strokeWidth={1.5}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<svg
|
||||
className="w-6 h-6"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M4 6h16M4 12h16M4 18h16"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<MobileMenu
|
||||
menuOpen={menuOpen}
|
||||
onMenuToggle={handleMenuToggle}
|
||||
navItems={navItems}
|
||||
onNavClick={handleMobileNavClick}
|
||||
>
|
||||
{button && (
|
||||
<Button
|
||||
{...getButtonProps(
|
||||
{
|
||||
...button,
|
||||
onClick: () => {
|
||||
button.onClick?.();
|
||||
setMenuOpen(false);
|
||||
},
|
||||
props: { ...button.props, ...getButtonConfigProps() }
|
||||
},
|
||||
0,
|
||||
theme.defaultButtonVariant,
|
||||
cls("w-full", buttonClassName),
|
||||
buttonTextClassName
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</MobileMenu>
|
||||
{/* Mobile Menu */}
|
||||
{isOpen && (
|
||||
<div className="md:hidden border-t border-white/20 px-6 py-4">
|
||||
<div className="flex flex-col gap-4">
|
||||
{navItems.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => handleNavClick(item.id)}
|
||||
className="text-sm text-foreground/70 hover:text-foreground transition-colors text-left"
|
||||
>
|
||||
{item.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
export default NavbarStyleApple;
|
||||
}
|
||||
|
||||
143
src/hooks/useUserSettings.ts
Normal file
143
src/hooks/useUserSettings.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
import {
|
||||
UserProfile,
|
||||
AccountSettings,
|
||||
ActivityLog,
|
||||
WishlistItem,
|
||||
defaultAccountSettings,
|
||||
UserSettingsManager,
|
||||
} from "@/utils/userSettings";
|
||||
|
||||
interface UseUserSettingsReturn {
|
||||
profile: UserProfile | null;
|
||||
settings: AccountSettings;
|
||||
activityLog: ActivityLog[];
|
||||
wishlist: WishlistItem[];
|
||||
updateProfile: (profile: Partial<UserProfile>) => Promise<void>;
|
||||
updateSettings: (settings: Partial<AccountSettings>) => void;
|
||||
addActivityLog: (activity: Omit<ActivityLog, "id" | "userId" | "timestamp">) => void;
|
||||
addToWishlist: (product: Omit<WishlistItem, "id" | "userId" | "addedAt">) => void;
|
||||
removeFromWishlist: (itemId: string) => void;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export function useUserSettings(userId?: string): UseUserSettingsReturn {
|
||||
const [profile, setProfile] = useState<UserProfile | null>(null);
|
||||
const [settings, setSettings] = useState<AccountSettings>(defaultAccountSettings);
|
||||
const [activityLog, setActivityLog] = useState<ActivityLog[]>([]);
|
||||
const [wishlist, setWishlist] = useState<WishlistItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Load user settings from localStorage (client-side)
|
||||
useEffect(() => {
|
||||
try {
|
||||
const savedProfile = localStorage.getItem(`user-profile-${userId}`);
|
||||
const savedSettings = localStorage.getItem(`user-settings-${userId}`);
|
||||
const savedActivity = localStorage.getItem(`user-activity-${userId}`);
|
||||
const savedWishlist = localStorage.getItem(`user-wishlist-${userId}`);
|
||||
|
||||
if (savedProfile) setProfile(JSON.parse(savedProfile));
|
||||
if (savedSettings) setSettings(JSON.parse(savedSettings));
|
||||
if (savedActivity) setActivityLog(JSON.parse(savedActivity));
|
||||
if (savedWishlist) setWishlist(JSON.parse(savedWishlist));
|
||||
} catch (err) {
|
||||
console.error("Failed to load user settings:", err);
|
||||
setError("Failed to load settings");
|
||||
}
|
||||
}, [userId]);
|
||||
|
||||
const updateProfile = useCallback(
|
||||
async (updatedProfile: Partial<UserProfile>) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Validate profile
|
||||
const errors = UserSettingsManager.validateProfile(updatedProfile);
|
||||
if (errors.length > 0) {
|
||||
throw new Error(errors.join(", "));
|
||||
}
|
||||
|
||||
// Sanitize profile
|
||||
const sanitized = UserSettingsManager.sanitizeProfile(updatedProfile);
|
||||
|
||||
// Update profile
|
||||
const newProfile = {
|
||||
...profile,
|
||||
...sanitized,
|
||||
updatedAt: new Date(),
|
||||
} as UserProfile;
|
||||
|
||||
setProfile(newProfile);
|
||||
localStorage.setItem(`user-profile-${userId}`, JSON.stringify(newProfile));
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : "Failed to update profile";
|
||||
setError(errorMessage);
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[profile, userId]
|
||||
);
|
||||
|
||||
const updateSettings = useCallback((updatedSettings: Partial<AccountSettings>) => {
|
||||
const newSettings = { ...settings, ...updatedSettings };
|
||||
setSettings(newSettings);
|
||||
localStorage.setItem(`user-settings-${userId}`, JSON.stringify(newSettings));
|
||||
}, [settings, userId]);
|
||||
|
||||
const addActivityLog = useCallback(
|
||||
(activity: Omit<ActivityLog, "id" | "userId" | "timestamp">) => {
|
||||
const newActivity: ActivityLog = {
|
||||
id: `activity-${Date.now()}`,
|
||||
userId: userId || "anonymous", ...activity,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
const newLog = [newActivity, ...activityLog].slice(0, 50); // Keep last 50 activities
|
||||
setActivityLog(newLog);
|
||||
localStorage.setItem(`user-activity-${userId}`, JSON.stringify(newLog));
|
||||
},
|
||||
[activityLog, userId]
|
||||
);
|
||||
|
||||
const addToWishlist = useCallback(
|
||||
(product: Omit<WishlistItem, "id" | "userId" | "addedAt">) => {
|
||||
const newWishlist = UserSettingsManager.addToWishlist(
|
||||
wishlist,
|
||||
userId || "anonymous", product
|
||||
);
|
||||
setWishlist(newWishlist);
|
||||
localStorage.setItem(`user-wishlist-${userId}`, JSON.stringify(newWishlist));
|
||||
},
|
||||
[wishlist, userId]
|
||||
);
|
||||
|
||||
const removeFromWishlist = useCallback(
|
||||
(itemId: string) => {
|
||||
const newWishlist = UserSettingsManager.removeFromWishlist(wishlist, itemId);
|
||||
setWishlist(newWishlist);
|
||||
localStorage.setItem(`user-wishlist-${userId}`, JSON.stringify(newWishlist));
|
||||
},
|
||||
[wishlist, userId]
|
||||
);
|
||||
|
||||
return {
|
||||
profile,
|
||||
settings,
|
||||
activityLog,
|
||||
wishlist,
|
||||
updateProfile,
|
||||
updateSettings,
|
||||
addActivityLog,
|
||||
addToWishlist,
|
||||
removeFromWishlist,
|
||||
loading,
|
||||
error,
|
||||
};
|
||||
}
|
||||
125
src/providers/authProvider/AuthProvider.tsx
Normal file
125
src/providers/authProvider/AuthProvider.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
"use client";
|
||||
|
||||
import React, { createContext, useContext, useState, useCallback, useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export interface GoogleUser {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
picture?: string;
|
||||
iat?: number;
|
||||
exp?: number;
|
||||
}
|
||||
|
||||
interface AuthContextType {
|
||||
user: GoogleUser | null;
|
||||
isLoading: boolean;
|
||||
isAuthenticated: boolean;
|
||||
login: (credential: string) => Promise<void>;
|
||||
logout: () => void;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
|
||||
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const [user, setUser] = useState<GoogleUser | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const router = useRouter();
|
||||
|
||||
// Restore session on mount
|
||||
useEffect(() => {
|
||||
const storedUser = localStorage.getItem("googleUser");
|
||||
if (storedUser) {
|
||||
try {
|
||||
setUser(JSON.parse(storedUser));
|
||||
} catch (e) {
|
||||
localStorage.removeItem("googleUser");
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const login = useCallback(async (credential: string) => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
// Decode JWT credential from Google
|
||||
const parts = credential.split(".");
|
||||
if (parts.length !== 3) {
|
||||
throw new Error("Invalid credential format");
|
||||
}
|
||||
|
||||
const decoded = JSON.parse(
|
||||
atob(parts[1].replace(/-/g, "+").replace(/_/g, "/"))
|
||||
);
|
||||
|
||||
const userData: GoogleUser = {
|
||||
id: decoded.sub,
|
||||
email: decoded.email,
|
||||
name: decoded.name,
|
||||
picture: decoded.picture,
|
||||
iat: decoded.iat,
|
||||
exp: decoded.exp,
|
||||
};
|
||||
|
||||
// Optional: Send to backend for verification
|
||||
// const response = await fetch('/api/auth/google', {
|
||||
// method: 'POST',
|
||||
// headers: { 'Content-Type': 'application/json' },
|
||||
// body: JSON.stringify({ credential })
|
||||
// });
|
||||
// if (!response.ok) throw new Error('Backend verification failed');
|
||||
|
||||
setUser(userData);
|
||||
localStorage.setItem("googleUser", JSON.stringify(userData));
|
||||
router.refresh();
|
||||
} catch (err) {
|
||||
const errorMessage =
|
||||
err instanceof Error ? err.message : "Login failed";
|
||||
setError(errorMessage);
|
||||
console.error("Login error:", err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [router]);
|
||||
|
||||
const logout = useCallback(() => {
|
||||
setUser(null);
|
||||
setError(null);
|
||||
localStorage.removeItem("googleUser");
|
||||
|
||||
// Sign out from Google
|
||||
if (typeof window !== "undefined" && window.google) {
|
||||
window.google.accounts.id.disableAutoSelect();
|
||||
}
|
||||
|
||||
router.refresh();
|
||||
}, [router]);
|
||||
|
||||
const value: AuthContextType = {
|
||||
user,
|
||||
isLoading,
|
||||
isAuthenticated: !!user,
|
||||
login,
|
||||
logout,
|
||||
error,
|
||||
};
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
}
|
||||
|
||||
export function useAuth() {
|
||||
const context = useContext(AuthContext);
|
||||
if (context === undefined) {
|
||||
throw new Error("useAuth must be used within an AuthProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
google?: any;
|
||||
}
|
||||
}
|
||||
60
src/providers/themeProvider/ThemeContext.tsx
Normal file
60
src/providers/themeProvider/ThemeContext.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
"use client";
|
||||
|
||||
import React, { createContext, useContext, useState, useEffect } from "react";
|
||||
|
||||
type ThemeContextType = {
|
||||
isDark: boolean;
|
||||
setIsDark: (isDark: boolean) => void;
|
||||
};
|
||||
|
||||
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||||
|
||||
export function ThemeContextProvider({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const [isDark, setIsDark] = useState(false);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
const stored = localStorage.getItem("theme-mode");
|
||||
if (stored === "dark") {
|
||||
setIsDark(true);
|
||||
document.documentElement.classList.add("dark");
|
||||
} else {
|
||||
setIsDark(false);
|
||||
document.documentElement.classList.remove("dark");
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (mounted) {
|
||||
localStorage.setItem("theme-mode", isDark ? "dark" : "light");
|
||||
if (isDark) {
|
||||
document.documentElement.classList.add("dark");
|
||||
} else {
|
||||
document.documentElement.classList.remove("dark");
|
||||
}
|
||||
}
|
||||
}, [isDark, mounted]);
|
||||
|
||||
if (!mounted) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ isDark, setIsDark }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useTheme() {
|
||||
const context = useContext(ThemeContext);
|
||||
if (context === undefined) {
|
||||
throw new Error("useTheme must be used within ThemeContextProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
201
src/utils/userSettings.ts
Normal file
201
src/utils/userSettings.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* User Settings Data Structure and Utilities
|
||||
* Manages user profile, account settings, preferences, and related operations
|
||||
*/
|
||||
|
||||
export interface UserProfile {
|
||||
id: string;
|
||||
fullName: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
location: string;
|
||||
avatar?: string;
|
||||
bio?: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface AccountSettings {
|
||||
twoFactorAuth: boolean;
|
||||
emailNotifications: boolean;
|
||||
marketingEmails: boolean;
|
||||
privateProfile: boolean;
|
||||
language?: string;
|
||||
timezone?: string;
|
||||
theme?: "light" | "dark" | "auto";
|
||||
}
|
||||
|
||||
export interface ActivityLog {
|
||||
id: string;
|
||||
userId: string;
|
||||
type: "purchase" | "sale" | "download" | "upload" | "comment" | "review";
|
||||
title: string;
|
||||
description?: string;
|
||||
amount?: number;
|
||||
timestamp: Date;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface WishlistItem {
|
||||
id: string;
|
||||
userId: string;
|
||||
productId: string;
|
||||
productName: string;
|
||||
productPrice: string;
|
||||
productCategory: string;
|
||||
addedAt: Date;
|
||||
}
|
||||
|
||||
export interface UserSettings {
|
||||
profile: UserProfile;
|
||||
accountSettings: AccountSettings;
|
||||
activityLog: ActivityLog[];
|
||||
wishlist: WishlistItem[];
|
||||
}
|
||||
|
||||
export interface UserPreferences {
|
||||
displayName: string;
|
||||
privacyLevel: "public" | "friends" | "private";
|
||||
notificationPreferences: {
|
||||
purchases: boolean;
|
||||
sales: boolean;
|
||||
reviews: boolean;
|
||||
messages: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Default account settings
|
||||
*/
|
||||
export const defaultAccountSettings: AccountSettings = {
|
||||
twoFactorAuth: false,
|
||||
emailNotifications: true,
|
||||
marketingEmails: false,
|
||||
privateProfile: false,
|
||||
language: "en", timezone: "UTC", theme: "auto"};
|
||||
|
||||
/**
|
||||
* User settings operations and utilities
|
||||
*/
|
||||
export class UserSettingsManager {
|
||||
/**
|
||||
* Validate user profile data
|
||||
*/
|
||||
static validateProfile(profile: Partial<UserProfile>): string[] {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (!profile.fullName || profile.fullName.trim().length < 2) {
|
||||
errors.push("Full name must be at least 2 characters");
|
||||
}
|
||||
|
||||
if (!profile.email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(profile.email)) {
|
||||
errors.push("Valid email is required");
|
||||
}
|
||||
|
||||
if (profile.phone && !/^[0-9\s\-\+\(\)]+$/.test(profile.phone)) {
|
||||
errors.push("Valid phone number is required");
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize profile data
|
||||
*/
|
||||
static sanitizeProfile(profile: Partial<UserProfile>): Partial<UserProfile> {
|
||||
return {
|
||||
fullName: profile.fullName?.trim() || "", email: profile.email?.trim().toLowerCase() || "", phone: profile.phone?.trim() || "", location: profile.location?.trim() || "", bio: profile.bio?.trim() || ""};
|
||||
}
|
||||
|
||||
/**
|
||||
* Format activity log entry
|
||||
*/
|
||||
static formatActivityLog(activity: ActivityLog): string {
|
||||
switch (activity.type) {
|
||||
case "purchase":
|
||||
return `Purchased ${activity.title}`;
|
||||
case "sale":
|
||||
return `Sold ${activity.title}`;
|
||||
case "download":
|
||||
return `Downloaded ${activity.title}`;
|
||||
case "upload":
|
||||
return `Uploaded ${activity.title}`;
|
||||
case "comment":
|
||||
return `Commented on ${activity.title}`;
|
||||
case "review":
|
||||
return `Reviewed ${activity.title}`;
|
||||
default:
|
||||
return activity.title;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user statistics from activity log
|
||||
*/
|
||||
static getUserStats(activityLog: ActivityLog[]): {
|
||||
totalPurchases: number;
|
||||
totalSales: number;
|
||||
totalSpent: number;
|
||||
totalEarned: number;
|
||||
} {
|
||||
return {
|
||||
totalPurchases: activityLog.filter((a) => a.type === "purchase").length,
|
||||
totalSales: activityLog.filter((a) => a.type === "sale").length,
|
||||
totalSpent: activityLog
|
||||
.filter((a) => a.type === "purchase")
|
||||
.reduce((sum, a) => sum + (a.amount || 0), 0),
|
||||
totalEarned: activityLog
|
||||
.filter((a) => a.type === "sale")
|
||||
.reduce((sum, a) => sum + (a.amount || 0), 0),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Add item to wishlist
|
||||
*/
|
||||
static addToWishlist(
|
||||
wishlist: WishlistItem[],
|
||||
userId: string,
|
||||
product: Omit<WishlistItem, "id" | "userId" | "addedAt">
|
||||
): WishlistItem[] {
|
||||
const newItem: WishlistItem = {
|
||||
id: `wishlist-${Date.now()}`,
|
||||
userId,
|
||||
...product,
|
||||
addedAt: new Date(),
|
||||
};
|
||||
|
||||
// Check if item already exists
|
||||
if (wishlist.some((item) => item.productId === product.productId)) {
|
||||
return wishlist;
|
||||
}
|
||||
|
||||
return [...wishlist, newItem];
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove item from wishlist
|
||||
*/
|
||||
static removeFromWishlist(
|
||||
wishlist: WishlistItem[],
|
||||
itemId: string
|
||||
): WishlistItem[] {
|
||||
return wishlist.filter((item) => item.id !== itemId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export user data
|
||||
*/
|
||||
static exportUserData(userSettings: UserSettings): string {
|
||||
return JSON.stringify(userSettings, null, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate account age in days
|
||||
*/
|
||||
static calculateAccountAge(createdAt: Date): number {
|
||||
const now = new Date();
|
||||
const diffTime = Math.abs(now.getTime() - createdAt.getTime());
|
||||
return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user