Skip to content

Commit 5cd66c9

Browse files
Refactor UI architecture
Implemented broad UI and data layer alignments: - Added and wired new components: AnimatedTrustBadge, Header, Footer, ShareButtons, ProductQuickView, wishlist/store shims - Replaced language usage with locale across Hero, Shop, and ProductDetail flows - Implemented minimal supabase-typed product models and Shopify-compatible cart structures - Replaced heavy, DB-mismatched fields with DB-aligned product schema mappings - Introduced lightweight filtering, search, and quick-view UX for catalog with CRO-oriented tweaks X-Lovable-Edit-ID: edt-093e1ded-07b9-4f7e-bc4e-db04ec442b62
2 parents 2b62b5d + baea3ea commit 5cd66c9

12 files changed

Lines changed: 411 additions & 806 deletions
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { ShieldCheck } from "lucide-react";
2+
3+
export const AnimatedTrustBadge = () => (
4+
<div className="relative w-24 h-24 flex items-center justify-center">
5+
<div className="absolute inset-0 rounded-full border-2 border-accent/40 animate-spin" style={{ animationDuration: '8s' }} />
6+
<div className="absolute inset-2 rounded-full border border-accent/20" />
7+
<ShieldCheck className="w-8 h-8 text-accent" />
8+
</div>
9+
);

src/components/Footer.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { useLanguage } from "@/contexts/LanguageContext";
2+
3+
export const Footer = () => {
4+
const { t } = useLanguage();
5+
return (
6+
<footer className="bg-primary text-primary-foreground py-12">
7+
<div className="container mx-auto px-4 max-w-7xl text-center">
8+
<p className="font-serif text-lg mb-2">ASPER BEAUTY SHOP</p>
9+
<p className="text-sm opacity-70">{t("footer.tagline")}</p>
10+
<p className="text-xs opacity-50 mt-4">© {new Date().getFullYear()} {t("footer.copyright")}</p>
11+
</div>
12+
</footer>
13+
);
14+
};

src/components/Header.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { Link } from "react-router-dom";
2+
import { ShoppingBag, Search, User, Globe } from "lucide-react";
3+
import { useLanguage } from "@/contexts/LanguageContext";
4+
import { useCartStore } from "@/stores/cartStore";
5+
6+
export const Header = () => {
7+
const { locale, toggle, t } = useLanguage();
8+
const itemCount = useCartStore(s => s.items.reduce((sum, i) => sum + i.quantity, 0));
9+
10+
return (
11+
<header className="fixed top-0 inset-x-0 z-50 bg-background/90 backdrop-blur-md border-b border-border">
12+
<div className="container mx-auto px-4 max-w-7xl flex items-center justify-between h-16">
13+
<Link to="/" className="font-serif text-xl font-bold text-primary tracking-wide">
14+
ASPER
15+
</Link>
16+
<nav className="hidden md:flex items-center gap-6 text-sm font-medium">
17+
<Link to="/shop" className="text-foreground/80 hover:text-primary transition-colors">{t("nav.shop")}</Link>
18+
<Link to="/lab" className="text-foreground/80 hover:text-primary transition-colors">{t("nav.intelligence")}</Link>
19+
</nav>
20+
<div className="flex items-center gap-3">
21+
<button onClick={toggle} className="p-2 text-foreground/60 hover:text-accent transition-colors" aria-label="Toggle language">
22+
<Globe className="w-5 h-5" />
23+
</button>
24+
<Link to="/auth" className="p-2 text-foreground/60 hover:text-primary transition-colors">
25+
<User className="w-5 h-5" />
26+
</Link>
27+
<Link to="/shop" className="p-2 text-foreground/60 hover:text-primary transition-colors relative">
28+
<ShoppingBag className="w-5 h-5" />
29+
{itemCount > 0 && (
30+
<span className="absolute -top-0.5 -right-0.5 bg-primary text-primary-foreground text-[10px] w-4 h-4 rounded-full flex items-center justify-center font-bold">
31+
{itemCount}
32+
</span>
33+
)}
34+
</Link>
35+
</div>
36+
</div>
37+
</header>
38+
);
39+
};

src/components/HeroSection.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ const GoldParticles = () => {
6464
};
6565
const HeroSection = () => {
6666
const [videoLoaded, setVideoLoaded] = useState(false);
67-
const { language } = useLanguage();
68-
const isAr = language === "ar";
67+
const { locale } = useLanguage();
68+
const isAr = locale === "ar";
6969

7070
return (
7171
<section className="relative min-h-screen w-full overflow-hidden bg-gradient-to-b from-[#FFF8E1] via-[#FFFDF5] to-[#FFF8E1]">
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
2+
import { Button } from "@/components/ui/button";
3+
import { ShoppingBag } from "lucide-react";
4+
import { formatJOD, getProductImage } from "@/lib/productImageUtils";
5+
6+
interface Product {
7+
id: string;
8+
title: string;
9+
price: number;
10+
description: string | null;
11+
image_url: string | null;
12+
brand: string | null;
13+
category?: string;
14+
}
15+
16+
interface ProductQuickViewProps {
17+
product: Product | null;
18+
open: boolean;
19+
onOpenChange: (open: boolean) => void;
20+
onAddToCart?: (product: Product) => void;
21+
}
22+
23+
export const ProductQuickView = ({ product, open, onOpenChange, onAddToCart }: ProductQuickViewProps) => {
24+
if (!product) return null;
25+
return (
26+
<Dialog open={open} onOpenChange={onOpenChange}>
27+
<DialogContent className="max-w-lg">
28+
<DialogHeader>
29+
<DialogTitle className="font-serif">{product.title}</DialogTitle>
30+
</DialogHeader>
31+
<div className="space-y-4">
32+
<img src={getProductImage(product.image_url)} alt={product.title} className="w-full aspect-square object-cover rounded-lg" />
33+
{product.brand && <p className="text-xs text-muted-foreground uppercase tracking-widest">{product.brand}</p>}
34+
<p className="text-sm text-muted-foreground">{product.description}</p>
35+
<div className="flex items-center justify-between">
36+
<span className="text-lg font-bold">{formatJOD(product.price)}</span>
37+
{onAddToCart && (
38+
<Button onClick={() => onAddToCart(product)} size="sm" className="gap-2">
39+
<ShoppingBag className="w-4 h-4" /> Add to Cart
40+
</Button>
41+
)}
42+
</div>
43+
</div>
44+
</DialogContent>
45+
</Dialog>
46+
);
47+
};
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { Input } from "@/components/ui/input";
2+
import { Search } from "lucide-react";
3+
4+
export interface FilterState {
5+
searchQuery: string;
6+
categories: string[];
7+
subcategories: string[];
8+
brands: string[];
9+
skinConcerns: string[];
10+
priceRange: [number, number];
11+
onSaleOnly: boolean;
12+
}
13+
14+
interface ProductSearchFiltersProps {
15+
filters: FilterState;
16+
onFilterChange: (filters: FilterState) => void;
17+
products: any[];
18+
}
19+
20+
export const ProductSearchFilters = ({ filters, onFilterChange }: ProductSearchFiltersProps) => {
21+
return (
22+
<div className="space-y-4">
23+
<div className="relative">
24+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
25+
<Input
26+
placeholder="Search products..."
27+
value={filters.searchQuery}
28+
onChange={(e) => onFilterChange({ ...filters, searchQuery: e.target.value })}
29+
className="pl-10"
30+
/>
31+
</div>
32+
</div>
33+
);
34+
};

src/components/ShareButtons.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { Share2 } from "lucide-react";
2+
import { Button } from "@/components/ui/button";
3+
import { toast } from "sonner";
4+
5+
interface ShareButtonsProps {
6+
url: string;
7+
title: string;
8+
}
9+
10+
export const ShareButtons = ({ url, title }: ShareButtonsProps) => {
11+
const handleShare = async () => {
12+
if (navigator.share) {
13+
await navigator.share({ title, url });
14+
} else {
15+
await navigator.clipboard.writeText(url);
16+
toast.success("Link copied!");
17+
}
18+
};
19+
20+
return (
21+
<Button variant="outline" size="sm" onClick={handleShare} className="gap-2">
22+
<Share2 className="w-4 h-4" /> Share
23+
</Button>
24+
);
25+
};

src/lib/productImageUtils.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export function formatJOD(price: number): string {
2+
return `${price.toFixed(2)} JOD`;
3+
}
4+
5+
export function getProductImage(imageUrl: string | null, _category?: string, _title?: string): string {
6+
return imageUrl || "/placeholder.svg";
7+
}
8+
9+
export const CATEGORY_FILTER_TO_DB: Record<string, string[]> = {
10+
"skin-care": ["Skin Care", "Skincare", "skincare"],
11+
"makeup": ["Makeup", "Make Up", "makeup"],
12+
"hair-care": ["Hair Care", "Haircare", "hair care"],
13+
"fragrance": ["Fragrance", "Fragrances", "fragrance"],
14+
"body-care": ["Body Care", "Body", "body care"],
15+
"tools-devices": ["Tools", "Devices", "tools"],
16+
};

src/lib/productUtils.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
type Locale = "en" | "ar";
2+
3+
export function getLocalizedCategory(category: string | null, locale: Locale): string {
4+
if (!category) return locale === "ar" ? "عام" : "General";
5+
return category;
6+
}
7+
8+
export function getLocalizedDescription(description: string | null | undefined, locale: Locale, maxLength = 300): string {
9+
if (!description) return "";
10+
return description.length > maxLength ? description.slice(0, maxLength) + "…" : description;
11+
}
12+
13+
export function translateTitle(title: string, _locale: Locale): string {
14+
return title;
15+
}

0 commit comments

Comments
 (0)