Merge version_2 into main #3

Merged
bender merged 12 commits from version_2 into main 2026-03-07 01:58:49 +00:00
12 changed files with 1129 additions and 194 deletions

View File

@@ -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>
);
}
}

View File

@@ -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
View 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>
);
}

View File

@@ -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);

View 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,
}}
/>
);
}

View File

@@ -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>
);
}

View 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>
);
}

View File

@@ -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;
}

View 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,
};
}

View 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;
}
}

View 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
View 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));
}
}