Skip to content

Commit 8635dad

Browse files
Wire Mom & Baby to Shopify API
Replace placeholder data with real Shopify Storefront data for Mom & Baby phases. Added Shopify queries, data fetching with React Query, and dynamic product rendering per lifecycle phase, including loading states and RTL language support. X-Lovable-Edit-ID: edt-0a99402f-5346-429c-a510-937a50515d22
2 parents ffefcdf + 500a645 commit 8635dad

1 file changed

Lines changed: 132 additions & 98 deletions

File tree

src/components/mom-baby/LifecycleSection.tsx

Lines changed: 132 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,36 @@
11
import { useLanguage } from "@/contexts/LanguageContext";
22
import type { LifecyclePhase } from "@/pages/MomBaby";
33
import { motion, AnimatePresence } from "framer-motion";
4-
import { Heart, Sparkles, Baby, ShoppingBag, ArrowRight } from "lucide-react";
4+
import { Heart, Sparkles, Baby, ShoppingBag, ArrowRight, Loader2 } from "lucide-react";
55
import { cn } from "@/lib/utils";
6+
import { useQuery } from "@tanstack/react-query";
7+
import { fetchProducts, normalizePrice, type ShopifyProduct } from "@/lib/shopify";
8+
import { Link } from "react-router-dom";
69

7-
interface PhaseData {
10+
interface PhaseConfig {
811
id: LifecyclePhase;
912
en: string;
1013
ar: string;
1114
icon: typeof Heart;
1215
color: string;
13-
categories: { en: string; ar: string; count: number }[];
14-
featured: { en: string; ar: string; brand: string; price: string }[];
16+
/** Shopify search query to fetch products for this phase */
17+
shopifyQuery: string;
18+
categories: { en: string; ar: string; shopifyQuery: string }[];
1519
}
1620

17-
const phasesData: PhaseData[] = [
21+
const phasesConfig: PhaseConfig[] = [
1822
{
1923
id: "before-birth",
2024
en: "Before Birth",
2125
ar: "قبل الولادة",
2226
icon: Heart,
2327
color: "text-rose-clay",
28+
shopifyQuery: "product_type:Stretch Mark OR product_type:Supplements OR tag:pregnancy OR tag:prenatal OR (tag:baby AND tag:skincare)",
2429
categories: [
25-
{ en: "Supplements & Fertility", ar: "المكملات والخصوبة", count: 24 },
26-
{ en: "Stretch Mark Prevention", ar: "الوقاية من التمدد", count: 18 },
27-
{ en: "Pregnancy-Safe Skincare", ar: "عناية آمنة للحمل", count: 32 },
28-
{ en: "Hair & Scalp Care", ar: "العناية بالشعر", count: 12 },
29-
],
30-
featured: [
31-
{ en: "Natalben Supra Pregnancy", ar: "ناتالبن سوبرا للحمل", brand: "Natalben", price: "21.73" },
32-
{ en: "Mustela Stretch Marks Cream", ar: "كريم علامات التمدد", brand: "Mustela", price: "18.50" },
33-
{ en: "A-Derma Exomega Control", ar: "إكزوميغا كونترول", brand: "A-Derma", price: "15.20" },
30+
{ en: "Stretch Mark Prevention", ar: "الوقاية من التمدد", shopifyQuery: "product_type:Stretch Mark" },
31+
{ en: "Pregnancy-Safe Skincare", ar: "عناية آمنة للحمل", shopifyQuery: "tag:pregnancy AND tag:skincare" },
32+
{ en: "Hair & Scalp Care", ar: "العناية بالشعر", shopifyQuery: "tag:pregnancy AND product_type:Shampoo" },
33+
{ en: "Supplements & Fertility", ar: "المكملات والخصوبة", shopifyQuery: "product_type:Supplements OR tag:prenatal" },
3434
],
3535
},
3636
{
@@ -39,16 +39,12 @@ const phasesData: PhaseData[] = [
3939
ar: "بعد الولادة",
4040
icon: Sparkles,
4141
color: "text-accent",
42+
shopifyQuery: "product_type:Breast Pump OR tag:breastfeeding OR tag:nursing OR product_type:Nursing",
4243
categories: [
43-
{ en: "Breast Pumps & Accessories", ar: "مضخات الثدي والإكسسوارات", count: 28 },
44-
{ en: "Milk Storage", ar: "تخزين الحليب", count: 15 },
45-
{ en: "Nipple Care", ar: "العناية بالحلمات", count: 9 },
46-
{ en: "Body Recovery", ar: "استعادة الجسم", count: 22 },
47-
],
48-
featured: [
49-
{ en: "Medela Swing Maxi Double", ar: "مضخة مديلا سوينج ماكسي", brand: "Medela", price: "189.00" },
50-
{ en: "Barral MotherProtect Oil", ar: "زيت باريل للأم", brand: "Barral", price: "12.40" },
51-
{ en: "Lierac Body Sculpt Gel", ar: "جل شد الجسم ليراك", brand: "Lierac", price: "28.90" },
44+
{ en: "Breast Pumps & Accessories", ar: "مضخات الثدي والإكسسوارات", shopifyQuery: "product_type:Breast Pump" },
45+
{ en: "Nursing Accessories", ar: "ملحقات الرضاعة", shopifyQuery: "tag:nursing" },
46+
{ en: "Nipple Care", ar: "العناية بالحلمات", shopifyQuery: "tag:nipple" },
47+
{ en: "Body Recovery", ar: "استعادة الجسم", shopifyQuery: "tag:postpartum" },
5248
],
5349
},
5450
{
@@ -57,16 +53,12 @@ const phasesData: PhaseData[] = [
5753
ar: "السنوات الأولى",
5854
icon: Baby,
5955
color: "text-primary",
56+
shopifyQuery: "product_type:Baby Powder OR product_type:Baby Oil OR product_type:Baby Shampoo OR product_type:Baby Cream OR product_type:Baby Lotion OR product_type:Baby Wash OR product_type:Baby Towel OR product_type:Baby Clothes",
6057
categories: [
61-
{ en: "Feeding & Accessories", ar: "الإطعام والإكسسوارات", count: 190 },
62-
{ en: "Diaper Changing", ar: "تغيير الحفاض", count: 59 },
63-
{ en: "Bath & Hygiene", ar: "الاستحمام والنظافة", count: 34 },
64-
{ en: "Oral Health", ar: "الصحة الفموية", count: 6 },
65-
],
66-
featured: [
67-
{ en: "Mustela Stelatopia+ Cream", ar: "كريم ستيلاتوبيا+", brand: "Mustela", price: "22.80" },
68-
{ en: "Bioderma ABCDerm Gel", ar: "جل أبيسيدرم بيوديرما", brand: "Bioderma", price: "14.50" },
69-
{ en: "Isdin Nutratopic Pro-AMP", ar: "نيوتراتوبيك برو", brand: "Isdin", price: "19.30" },
58+
{ en: "Bath & Hygiene", ar: "الاستحمام والنظافة", shopifyQuery: "product_type:Baby Shampoo OR product_type:Baby Wash OR product_type:Baby Towel" },
59+
{ en: "Skin Care", ar: "العناية بالبشرة", shopifyQuery: "product_type:Baby Cream OR product_type:Baby Lotion OR product_type:Baby Oil" },
60+
{ en: "Diaper Changing", ar: "تغيير الحفاض", shopifyQuery: "product_type:Baby Powder" },
61+
{ en: "Clothing", ar: "الملابس", shopifyQuery: "product_type:Baby Clothes" },
7062
],
7163
},
7264
{
@@ -75,30 +67,127 @@ const phasesData: PhaseData[] = [
7567
ar: "أساسيات الأمومة",
7668
icon: ShoppingBag,
7769
color: "text-burgundy",
70+
shopifyQuery: "product_type:Baby Carrier OR product_type:Baby Stroller OR product_type:Baby Gift Set OR product_type:Baby Bag OR tag:maternity",
7871
categories: [
79-
{ en: "Hospital Bags & Baskets", ar: "حقائب المستشفى", count: 12 },
80-
{ en: "Thermometers & Monitors", ar: "موازين الحرارة والمراقبة", count: 13 },
81-
{ en: "Gift Sets & Bundles", ar: "أطقم الهدايا", count: 18 },
82-
{ en: "Nebulizers & Respiratory", ar: "أجهزة الاستنشاق", count: 5 },
83-
],
84-
featured: [
85-
{ en: "Mustela Essential Basket", ar: "سلة مستيلا الأساسية", brand: "Mustela", price: "45.00" },
86-
{ en: "Owlet Dream Sock Monitor", ar: "جوارب أولت الذكية", brand: "Owlet", price: "299.00" },
87-
{ en: "Braun No Touch Thermometer", ar: "ميزان حرارة براون", brand: "Braun", price: "52.00" },
72+
{ en: "Carriers & Strollers", ar: "الحاملات والعربات", shopifyQuery: "product_type:Baby Carrier OR product_type:Baby Stroller" },
73+
{ en: "Gift Sets & Bundles", ar: "أطقم الهدايا", shopifyQuery: "product_type:Baby Gift Set" },
74+
{ en: "Bags & Travel", ar: "حقائب السفر", shopifyQuery: "product_type:Baby Bag" },
75+
{ en: "Thermometers & Monitors", ar: "موازين الحرارة والمراقبة", shopifyQuery: "tag:thermometer OR tag:monitor" },
8876
],
8977
},
9078
];
9179

80+
function usePhaseProducts(phase: PhaseConfig, enabled: boolean) {
81+
return useQuery({
82+
queryKey: ["mom-baby-phase", phase.id],
83+
queryFn: () => fetchProducts(6, phase.shopifyQuery),
84+
enabled,
85+
staleTime: 5 * 60 * 1000,
86+
});
87+
}
88+
9289
interface Props {
9390
activePhase: LifecyclePhase;
9491
activeConcern: string | null;
9592
}
9693

94+
function PhaseSection({ phase, isAr }: { phase: PhaseConfig; isAr: boolean }) {
95+
const { data, isLoading } = usePhaseProducts(phase, true);
96+
const products = data?.products || [];
97+
98+
return (
99+
<div>
100+
{/* Phase header */}
101+
<div className="flex items-center gap-3 mb-6">
102+
<phase.icon className={cn("w-6 h-6", phase.color)} />
103+
<h2 className="font-heading text-2xl md:text-3xl text-foreground">
104+
{isAr ? phase.ar : phase.en}
105+
</h2>
106+
</div>
107+
108+
{/* Categories grid */}
109+
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-8">
110+
{phase.categories.map((cat) => (
111+
<Link
112+
key={cat.en}
113+
to={`/products?q=${encodeURIComponent(cat.shopifyQuery)}`}
114+
className="group rounded-xl border border-border bg-card p-4 text-start hover:border-accent/50 hover:shadow-warm transition-all duration-300"
115+
>
116+
<span className="block text-sm font-body font-medium text-foreground group-hover:text-primary transition-colors">
117+
{isAr ? cat.ar : cat.en}
118+
</span>
119+
</Link>
120+
))}
121+
</div>
122+
123+
{/* Featured products — real Shopify data */}
124+
{isLoading ? (
125+
<div className="flex items-center justify-center py-12">
126+
<Loader2 className="w-6 h-6 animate-spin text-muted-foreground" />
127+
</div>
128+
) : products.length === 0 ? (
129+
<p className="text-sm text-muted-foreground text-center py-8">
130+
{isAr ? "لا توجد منتجات حالياً" : "No products available yet"}
131+
</p>
132+
) : (
133+
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
134+
{products.slice(0, 3).map((p: ShopifyProduct) => {
135+
const product = p.node;
136+
const imageUrl = product.images.edges[0]?.node.url;
137+
const price = normalizePrice(product.priceRange.minVariantPrice.amount);
138+
const currency = product.priceRange.minVariantPrice.currencyCode;
139+
140+
return (
141+
<Link
142+
key={product.id}
143+
to={`/product/${product.handle}`}
144+
className="product-card-hover group rounded-xl border border-border bg-card p-5 cursor-pointer"
145+
>
146+
{/* Product image */}
147+
<div className="w-full aspect-square rounded-lg bg-muted/50 mb-4 overflow-hidden flex items-center justify-center">
148+
{imageUrl ? (
149+
<img
150+
src={imageUrl}
151+
alt={product.title}
152+
className="w-full h-full object-contain"
153+
loading="lazy"
154+
/>
155+
) : (
156+
<phase.icon className="w-10 h-10 text-muted-foreground/30" />
157+
)}
158+
</div>
159+
<p className="text-[10px] font-body uppercase tracking-widest text-accent mb-1">
160+
{product.vendor}
161+
</p>
162+
<h3 className="text-sm font-body font-medium text-foreground mb-2 line-clamp-2">
163+
{product.title}
164+
</h3>
165+
<div className="flex items-center justify-between">
166+
<span className="text-base font-heading font-bold text-primary">
167+
{price.toFixed(2)} {currency === "JOD" ? "JD" : currency}
168+
</span>
169+
<span className="inline-flex items-center gap-1 text-xs text-accent opacity-0 group-hover:opacity-100 transition-opacity">
170+
{isAr ? "عرض" : "View"}
171+
<ArrowRight className="w-3 h-3" />
172+
</span>
173+
</div>
174+
</Link>
175+
);
176+
})}
177+
</div>
178+
)}
179+
</div>
180+
);
181+
}
182+
97183
export default function LifecycleSection({ activePhase, activeConcern }: Props) {
98184
const { locale } = useLanguage();
99185
const isAr = locale === "ar";
100186

101-
const visible = activePhase === "all" ? phasesData : phasesData.filter((p) => p.id === activePhase);
187+
const visible =
188+
activePhase === "all"
189+
? phasesConfig
190+
: phasesConfig.filter((p) => p.id === activePhase);
102191

103192
return (
104193
<section className="py-12">
@@ -113,62 +202,7 @@ export default function LifecycleSection({ activePhase, activeConcern }: Props)
113202
className="space-y-16"
114203
>
115204
{visible.map((phase) => (
116-
<div key={phase.id}>
117-
{/* Phase header */}
118-
<div className="flex items-center gap-3 mb-6">
119-
<phase.icon className={cn("w-6 h-6", phase.color)} />
120-
<h2 className="font-heading text-2xl md:text-3xl text-foreground">
121-
{isAr ? phase.ar : phase.en}
122-
</h2>
123-
</div>
124-
125-
{/* Categories grid */}
126-
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-8">
127-
{phase.categories.map((cat) => (
128-
<button
129-
key={cat.en}
130-
className="group rounded-xl border border-border bg-card p-4 text-start hover:border-accent/50 hover:shadow-warm transition-all duration-300"
131-
>
132-
<span className="block text-sm font-body font-medium text-foreground group-hover:text-primary transition-colors">
133-
{isAr ? cat.ar : cat.en}
134-
</span>
135-
<span className="text-xs text-muted-foreground mt-1 block">
136-
{cat.count} {isAr ? "منتج" : "products"}
137-
</span>
138-
</button>
139-
))}
140-
</div>
141-
142-
{/* Featured products */}
143-
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
144-
{phase.featured.map((product) => (
145-
<div
146-
key={product.en}
147-
className="product-card-hover group rounded-xl border border-border bg-card p-5 cursor-pointer"
148-
>
149-
{/* Placeholder image area */}
150-
<div className="w-full aspect-square rounded-lg bg-muted/50 mb-4 flex items-center justify-center">
151-
<phase.icon className="w-10 h-10 text-muted-foreground/30" />
152-
</div>
153-
<p className="text-[10px] font-body uppercase tracking-widest text-accent mb-1">
154-
{product.brand}
155-
</p>
156-
<h3 className="text-sm font-body font-medium text-foreground mb-2 line-clamp-2">
157-
{isAr ? product.ar : product.en}
158-
</h3>
159-
<div className="flex items-center justify-between">
160-
<span className="text-base font-heading font-bold text-primary">
161-
${product.price}
162-
</span>
163-
<span className="inline-flex items-center gap-1 text-xs text-accent opacity-0 group-hover:opacity-100 transition-opacity">
164-
{isAr ? "عرض" : "View"}
165-
<ArrowRight className="w-3 h-3" />
166-
</span>
167-
</div>
168-
</div>
169-
))}
170-
</div>
171-
</div>
205+
<PhaseSection key={phase.id} phase={phase} isAr={isAr} />
172206
))}
173207
</motion.div>
174208
</AnimatePresence>

0 commit comments

Comments
 (0)