Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8394bc404e | |||
| a0930f5996 | |||
| c076a5848a | |||
| d19131c2f7 | |||
| 1b955b1bfb | |||
| c672c5a63e | |||
| d24529c85e | |||
| b6fc31affc | |||
| e6bb0fc375 | |||
| 019e0c3ee8 | |||
| 6e5a52cdd8 | |||
| 34951a96b7 | |||
| c134f15862 | |||
| e7c8d501b5 | |||
| 4e6e08fabe | |||
| b50e40f1c2 | |||
| a67212a4e1 | |||
| a18357792f | |||
| 57baf85e34 | |||
| abd419a9c5 | |||
| 23dbfc9bfd | |||
| 46b2da142f | |||
| c9edddb904 | |||
| 8ff41dbb6c | |||
| c1f33d717e | |||
| 7bec1e92d0 | |||
| 9a24891a8b |
361
src/app/page.tsx
361
src/app/page.tsx
@@ -34,77 +34,54 @@ export default function LandingPage() {
|
|||||||
<NavbarLayoutFloatingInline
|
<NavbarLayoutFloatingInline
|
||||||
navItems={[
|
navItems={[
|
||||||
{
|
{
|
||||||
name: "الرئيسية",
|
name: "Home", id: "hero"},
|
||||||
id: "hero",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "حولنا",
|
name: "About Us", id: "about"},
|
||||||
id: "about",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "الميزات",
|
name: "Features", id: "features"},
|
||||||
id: "features",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "الخدمات",
|
name: "Services", id: "products"},
|
||||||
id: "products",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "الأسعار",
|
name: "Pricing", id: "pricing"},
|
||||||
id: "pricing",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "آراء العملاء",
|
name: "Testimonials", id: "testimonials"},
|
||||||
id: "testimonials",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "الأسئلة الشائعة",
|
name: "FAQ", id: "faq"},
|
||||||
id: "faq",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "اتصل بنا",
|
name: "Contact Us", id: "contact"},
|
||||||
id: "contact",
|
|
||||||
},
|
|
||||||
]}
|
]}
|
||||||
logoSrc="https://webuild-dev.s3.eu-north-1.amazonaws.com/default/no-image.jpg?id=u8l15k"
|
logoSrc="https://webuild-dev.s3.eu-north-1.amazonaws.com/default/no-image.jpg?id=u8l15k"
|
||||||
logoAlt="Inwi Moneye Logo"
|
logoAlt="Inwi Moneye Logo"
|
||||||
brandName="Inwi Moneye"
|
brandName="Inwi Moneye"
|
||||||
button={{
|
button={{
|
||||||
text: "حمّل التطبيق",
|
text: "Download App", href: "#"}}
|
||||||
href: "#",
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="hero" data-section="hero">
|
<div id="hero" data-section="hero">
|
||||||
<HeroBillboardScroll
|
<HeroBillboardScroll
|
||||||
background={{
|
background={{
|
||||||
variant: "radial-gradient",
|
variant: "radial-gradient"}}
|
||||||
}}
|
title="Inwi Moneye: Your Comprehensive Digital Wallet"
|
||||||
title="Inwi Moneye: محفظتك الرقمية الشاملة"
|
description="Send money, pay bills, and manage your finances smartly and securely, both in Morocco and abroad. With Inwi Moneye, you control your financial future."
|
||||||
description="أرسل الأموال، ادفع الفواتير، وتحكّم في أموالك بذكاء وأمان، داخل المغرب وخارجه. مع Inwi Moneye، أنت تتحكم في مستقبلك المالي."
|
|
||||||
buttons={[
|
buttons={[
|
||||||
{
|
{
|
||||||
text: "حمّل التطبيق الآن",
|
text: "Download App Now", href: "#download"},
|
||||||
href: "#download",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
text: "اكتشف المزيد",
|
text: "Discover More", href: "#about"},
|
||||||
href: "#about",
|
|
||||||
},
|
|
||||||
]}
|
]}
|
||||||
imageSrc="http://img.b2bpic.net/free-photo/close-up-guy-with-ring-smartphone_23-2148450773.jpg"
|
imageSrc="http://img.b2bpic.net/free-photo/close-up-guy-with-ring-smartphone_23-2148450773.jpg"
|
||||||
imageAlt="تطبيق Inwi Moneye على الهاتف الذكي"
|
imageAlt="Inwi Moneye app on smartphone"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="about" data-section="about">
|
<div id="about" data-section="about">
|
||||||
<MediaAbout
|
<MediaAbout
|
||||||
useInvertedBackground={false}
|
useInvertedBackground={false}
|
||||||
title="عن Inwi Moneye: شريكك المالي الذكي"
|
title="About Inwi Moneye: Your Smart Financial Partner"
|
||||||
description="Inwi Moneye هو تطبيق المحفظة الإلكترونية المتطور من Inwi، المصمم لتبسيط معاملاتك المالية اليومية. يوفر لك حرية إرسال واستقبال الأموال، دفع فواتيرك، وشحن رصيدك بكل سهولة ويسر، مدعوماً بتقنيات الذكاء الاصطناعي لتقديم تجربة مصرفية ذكية ومخصصة تناسب احتياجاتك في المغرب وخارجه."
|
description="Inwi Moneye is Inwi's advanced e-wallet application, designed to simplify your daily financial transactions. It offers you the freedom to send and receive money, pay your bills, and top up your balance with ease and convenience, powered by AI technologies to provide a smart, personalized banking experience tailored to your needs in Morocco and beyond."
|
||||||
imageSrc="http://img.b2bpic.net/free-photo/advanced-technological-robot-interacting-with-money-finance_23-2151612658.jpg"
|
imageSrc="http://img.b2bpic.net/free-photo/advanced-technological-robot-interacting-with-money-finance_23-2151612658.jpg"
|
||||||
imageAlt="أشخاص يتفاعلون مع تطبيق محفظة إلكترونية"
|
imageAlt="People interacting with an e-wallet application"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -115,72 +92,40 @@ export default function LandingPage() {
|
|||||||
useInvertedBackground={true}
|
useInvertedBackground={true}
|
||||||
features={[
|
features={[
|
||||||
{
|
{
|
||||||
id: "f1",
|
id: "f1", label: "Fast & Secure Transfers", title: "Send & Receive Money", items: [
|
||||||
label: "تحويلات سريعة وآمنة",
|
"Instant transfers to friends and family", "Supported locally and internationally", "Low and transparent fees"],
|
||||||
title: "إرسال واستقبال الأموال",
|
|
||||||
items: [
|
|
||||||
"تحويلات فورية للأصدقاء والعائلة",
|
|
||||||
"مدعومة محلياً ودولياً",
|
|
||||||
"رسوم منخفضة وشفافة",
|
|
||||||
],
|
|
||||||
buttons: [
|
buttons: [
|
||||||
{
|
{
|
||||||
text: "تعلم المزيد",
|
text: "Learn More", href: "#"},
|
||||||
href: "#",
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "f2",
|
id: "f2", label: "Pay with a Tap", title: "Pay Bills & Top Up", items: [
|
||||||
label: "ادفع بضغطة زر",
|
"Pay Wi-Fi and internet bills", "Easily top up phone balance", "Various service payments"],
|
||||||
title: "دفع الفواتير وشحن الرصيد",
|
|
||||||
items: [
|
|
||||||
"دفع فواتير Wi-Fi والإنترنت",
|
|
||||||
"شحن رصيد الهاتف بسهولة",
|
|
||||||
"مدفوعات متنوعة للخدمات",
|
|
||||||
],
|
|
||||||
buttons: [
|
buttons: [
|
||||||
{
|
{
|
||||||
text: "اكتشف الفواتير",
|
text: "Explore Bills", href: "#"},
|
||||||
href: "#",
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "f3",
|
id: "f3", label: "Unified Management", title: "Link Bank Accounts", items: [
|
||||||
label: "إدارة موحدة",
|
"Link your wallet to your personal accounts", "Seamless transfers between banks and wallet", "Full control over your finances"],
|
||||||
title: "الربط بالحسابات البنكية",
|
|
||||||
items: [
|
|
||||||
"ربط محفظتك بحساباتك الشخصية",
|
|
||||||
"تحويلات سلسة بين البنوك والمحفظة",
|
|
||||||
"تحكم كامل في أموالك",
|
|
||||||
],
|
|
||||||
buttons: [
|
buttons: [
|
||||||
{
|
{
|
||||||
text: "ابدأ الربط",
|
text: "Start Linking", href: "#"},
|
||||||
href: "#",
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "f4",
|
id: "f4", label: "Your Financial Assistant", title: "AI-Powered Support", items: [
|
||||||
label: "مساعدك المالي",
|
"Personalized spending insights and suggestions", "Smart analysis of your transactions", "Better financial planning with AI assistance"],
|
||||||
title: "دعم الذكاء الاصطناعي",
|
|
||||||
items: [
|
|
||||||
"رؤى واقتراحات مخصصة للإنفاق",
|
|
||||||
"تحليل ذكي لمعاملاتك",
|
|
||||||
"تخطيط مالي أفضل بمساعدة AI",
|
|
||||||
],
|
|
||||||
buttons: [
|
buttons: [
|
||||||
{
|
{
|
||||||
text: "جرب AI",
|
text: "Try AI", href: "#"},
|
||||||
href: "#",
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
title="ميزات Inwi Moneye الرئيسية"
|
title="Key Features of Inwi Moneye"
|
||||||
description="استمتع بمجموعة واسعة من الخدمات المصممة لجعل إدارة أموالك أكثر كفاءة وأمانًا وذكاءً."
|
description="Enjoy a wide range of services designed to make managing your money more efficient, secure, and smart."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -191,37 +136,13 @@ export default function LandingPage() {
|
|||||||
useInvertedBackground={false}
|
useInvertedBackground={false}
|
||||||
metrics={[
|
metrics={[
|
||||||
{
|
{
|
||||||
id: "m1",
|
id: "m1", value: "5M+", title: "مستخدم نشط", description: "أكثر من خمسة ملايين مستخدم يثقون في Inwi Moneye.", imageSrc: "http://img.b2bpic.net/free-photo/group-friends-taking-selfie_53876-40336.jpg", imageAlt: "أيقونة مستخدمين"},
|
||||||
value: "5M+",
|
|
||||||
title: "مستخدم نشط",
|
|
||||||
description: "أكثر من خمسة ملايين مستخدم يثقون في Inwi Moneye.",
|
|
||||||
imageSrc: "http://img.b2bpic.net/free-photo/group-friends-taking-selfie_53876-40336.jpg",
|
|
||||||
imageAlt: "أيقونة مستخدمين",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: "m2",
|
id: "m2", value: "10M+", title: "معاملة شهرياً", description: "نحن نجري أكثر من 10 ملايين معاملة مالية كل شهر.", imageSrc: "http://img.b2bpic.net/free-vector/recycle-sign-with-dollar-sign-green-black_78370-9145.jpg", imageAlt: "أيقونة معاملات"},
|
||||||
value: "10M+",
|
|
||||||
title: "معاملة شهرياً",
|
|
||||||
description: "نحن نجري أكثر من 10 ملايين معاملة مالية كل شهر.",
|
|
||||||
imageSrc: "http://img.b2bpic.net/free-vector/recycle-sign-with-dollar-sign-green-black_78370-9145.jpg",
|
|
||||||
imageAlt: "أيقونة معاملات",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: "m3",
|
id: "m3", value: "20+", title: "دولة مدعومة", description: "تتواجد خدماتنا في أكثر من 20 دولة حول العالم.", imageSrc: "http://img.b2bpic.net/free-photo/earth-globe-internet-icon-sign-symbol-button-blue-speech-bubble-white-background-3d-rendering_56104-1178.jpg", imageAlt: "أيقونة دول"},
|
||||||
value: "20+",
|
|
||||||
title: "دولة مدعومة",
|
|
||||||
description: "تتواجد خدماتنا في أكثر من 20 دولة حول العالم.",
|
|
||||||
imageSrc: "http://img.b2bpic.net/free-photo/earth-globe-internet-icon-sign-symbol-button-blue-speech-bubble-white-background-3d-rendering_56104-1178.jpg",
|
|
||||||
imageAlt: "أيقونة دول",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: "m4",
|
id: "m4", value: "99.9%", title: "رضا العملاء", description: "معدل رضا عالٍ يعكس جودة خدماتنا ودعمنا.", imageSrc: "http://img.b2bpic.net/free-photo/arrangement-with-different-feelings_23-2148860250.jpg", imageAlt: "أيقونة رضا العملاء"},
|
||||||
value: "99.9%",
|
|
||||||
title: "رضا العملاء",
|
|
||||||
description: "معدل رضا عالٍ يعكس جودة خدماتنا ودعمنا.",
|
|
||||||
imageSrc: "http://img.b2bpic.net/free-photo/arrangement-with-different-feelings_23-2148860250.jpg",
|
|
||||||
imageAlt: "أيقونة رضا العملاء",
|
|
||||||
},
|
|
||||||
]}
|
]}
|
||||||
title="أرقامنا تتحدث عن نفسها"
|
title="أرقامنا تتحدث عن نفسها"
|
||||||
description="شاهد كيف يساهم Inwi Moneye في تمكين الآلاف يومياً من خلال خدماتنا الموثوقة والمنتشرة."
|
description="شاهد كيف يساهم Inwi Moneye في تمكين الآلاف يومياً من خلال خدماتنا الموثوقة والمنتشرة."
|
||||||
@@ -236,53 +157,17 @@ export default function LandingPage() {
|
|||||||
useInvertedBackground={true}
|
useInvertedBackground={true}
|
||||||
products={[
|
products={[
|
||||||
{
|
{
|
||||||
id: "p1",
|
id: "p1", name: "تحويل الأموال الدولي", price: "مجاني*", variant: "حدود عالية", imageSrc: "http://img.b2bpic.net/free-photo/business-person-futuristic-business-environment_23-2150970183.jpg", imageAlt: "تحويل أموال دولي"},
|
||||||
name: "تحويل الأموال الدولي",
|
|
||||||
price: "مجاني*",
|
|
||||||
variant: "حدود عالية",
|
|
||||||
imageSrc: "http://img.b2bpic.net/free-photo/business-person-futuristic-business-environment_23-2150970183.jpg",
|
|
||||||
imageAlt: "تحويل أموال دولي",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: "p2",
|
id: "p2", name: "دفع فواتير الإنترنت", price: "فوري", variant: "لكل الشبكات", imageSrc: "http://img.b2bpic.net/free-photo/joyous-person-delighted-online-shopping-carefully-typing-credit-card_482257-102475.jpg", imageAlt: "دفع فواتير الإنترنت"},
|
||||||
name: "دفع فواتير الإنترنت",
|
|
||||||
price: "فوري",
|
|
||||||
variant: "لكل الشبكات",
|
|
||||||
imageSrc: "http://img.b2bpic.net/free-photo/joyous-person-delighted-online-shopping-carefully-typing-credit-card_482257-102475.jpg",
|
|
||||||
imageAlt: "دفع فواتير الإنترنت",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: "p3",
|
id: "p3", name: "شحن رصيد الهاتف", price: "سهل", variant: "جميع المشغلين", imageSrc: "http://img.b2bpic.net/free-photo/young-handsome-man-holding-credit-card-looking-screen-his-smartphone-standing-blue-wall_141793-23007.jpg", imageAlt: "شحن رصيد الهاتف"},
|
||||||
name: "شحن رصيد الهاتف",
|
|
||||||
price: "سهل",
|
|
||||||
variant: "جميع المشغلين",
|
|
||||||
imageSrc: "http://img.b2bpic.net/free-photo/young-handsome-man-holding-credit-card-looking-screen-his-smartphone-standing-blue-wall_141793-23007.jpg",
|
|
||||||
imageAlt: "شحن رصيد الهاتف",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: "p4",
|
id: "p4", name: "محفظة AI الذكية", price: "حصري", variant: "تحليل مالي", imageSrc: "http://img.b2bpic.net/free-photo/man-using-digital-assistant-his-smartphone_23-2149107950.jpg", imageAlt: "محفظة AI الذكية"},
|
||||||
name: "محفظة AI الذكية",
|
|
||||||
price: "حصري",
|
|
||||||
variant: "تحليل مالي",
|
|
||||||
imageSrc: "http://img.b2bpic.net/free-photo/man-using-digital-assistant-his-smartphone_23-2149107950.jpg",
|
|
||||||
imageAlt: "محفظة AI الذكية",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: "p5",
|
id: "p5", name: "ربط الحسابات البنكية", price: "آمن", variant: "تكامل سلس", imageSrc: "http://img.b2bpic.net/free-vector/online-education-concept_23-2147507995.jpg", imageAlt: "ربط الحسابات البنكية"},
|
||||||
name: "ربط الحسابات البنكية",
|
|
||||||
price: "آمن",
|
|
||||||
variant: "تكامل سلس",
|
|
||||||
imageSrc: "http://img.b2bpic.net/free-vector/online-education-concept_23-2147507995.jpg",
|
|
||||||
imageAlt: "ربط الحسابات البنكية",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: "p6",
|
id: "p6", name: "مدفوعات التجار", price: "سريع", variant: "QR Code", imageSrc: "http://img.b2bpic.net/free-photo/person-paying-using-nfc-technology_23-2149893847.jpg", imageAlt: "مدفوعات التجار"},
|
||||||
name: "مدفوعات التجار",
|
|
||||||
price: "سريع",
|
|
||||||
variant: "QR Code",
|
|
||||||
imageSrc: "http://img.b2bpic.net/free-photo/person-paying-using-nfc-technology_23-2149893847.jpg",
|
|
||||||
imageAlt: "مدفوعات التجار",
|
|
||||||
},
|
|
||||||
]}
|
]}
|
||||||
title="خدماتنا المخصصة لك"
|
title="خدماتنا المخصصة لك"
|
||||||
description="اكتشف مجموعة حلول Inwi Moneye التي تلبي جميع احتياجاتك المالية وتجعل حياتك أسهل."
|
description="اكتشف مجموعة حلول Inwi Moneye التي تلبي جميع احتياجاتك المالية وتجعل حياتك أسهل."
|
||||||
@@ -296,60 +181,22 @@ export default function LandingPage() {
|
|||||||
useInvertedBackground={false}
|
useInvertedBackground={false}
|
||||||
plans={[
|
plans={[
|
||||||
{
|
{
|
||||||
id: "plan-basic",
|
id: "plan-basic", tag: "الاكثر شعبية", price: "مجاناً", period: "شهرياً", description: "للمستخدمين الجدد والذين يحتاجون للخدمات الأساسية اليومية.", button: {
|
||||||
tag: "الاكثر شعبية",
|
text: "ابدأ مجاناً", href: "#"},
|
||||||
price: "مجاناً",
|
featuresTitle: "الميزات الأساسية:", features: [
|
||||||
period: "شهرياً",
|
"تحويل الأموال محلياً", "دفع فواتير Wi-Fi والإنترنت", "شحن رصيد الهاتف", "دعم فني على مدار الساعة"],
|
||||||
description: "للمستخدمين الجدد والذين يحتاجون للخدمات الأساسية اليومية.",
|
|
||||||
button: {
|
|
||||||
text: "ابدأ مجاناً",
|
|
||||||
href: "#",
|
|
||||||
},
|
|
||||||
featuresTitle: "الميزات الأساسية:",
|
|
||||||
features: [
|
|
||||||
"تحويل الأموال محلياً",
|
|
||||||
"دفع فواتير Wi-Fi والإنترنت",
|
|
||||||
"شحن رصيد الهاتف",
|
|
||||||
"دعم فني على مدار الساعة",
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "plan-premium",
|
id: "plan-premium", tag: "موصى به", price: "49 درهم", period: "شهرياً", description: "للمستخدمين الذين يبحثون عن ميزات متقدمة ودعم أوسع.", button: {
|
||||||
tag: "موصى به",
|
text: "اشترك الآن", href: "#"},
|
||||||
price: "49 درهم",
|
featuresTitle: "الميزات المتقدمة:", features: [
|
||||||
period: "شهرياً",
|
"جميع ميزات الخطة الأساسية", "تحويل الأموال دولياً", "الربط بحسابات بنكية متعددة", "تقارير مالية مدعومة بالذكاء الاصطناعي", "أولوية في الدعم الفني"],
|
||||||
description: "للمستخدمين الذين يبحثون عن ميزات متقدمة ودعم أوسع.",
|
|
||||||
button: {
|
|
||||||
text: "اشترك الآن",
|
|
||||||
href: "#",
|
|
||||||
},
|
|
||||||
featuresTitle: "الميزات المتقدمة:",
|
|
||||||
features: [
|
|
||||||
"جميع ميزات الخطة الأساسية",
|
|
||||||
"تحويل الأموال دولياً",
|
|
||||||
"الربط بحسابات بنكية متعددة",
|
|
||||||
"تقارير مالية مدعومة بالذكاء الاصطناعي",
|
|
||||||
"أولوية في الدعم الفني",
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "plan-business",
|
id: "plan-business", tag: "للشركات", price: "199 درهم", period: "شهرياً", description: "حلول مالية متكاملة للشركات الصغيرة والمتوسطة.", button: {
|
||||||
tag: "للشركات",
|
text: "تواصل للمؤسسات", href: "#"},
|
||||||
price: "199 درهم",
|
featuresTitle: "حلول الأعمال:", features: [
|
||||||
period: "شهرياً",
|
"جميع ميزات الخطة المميزة", "مدفوعات جماعية للموظفين", "بوابات دفع مخصصة للتجار", "تحليلات مالية متقدمة للأعمال", "مدير حساب شخصي"],
|
||||||
description: "حلول مالية متكاملة للشركات الصغيرة والمتوسطة.",
|
|
||||||
button: {
|
|
||||||
text: "تواصل للمؤسسات",
|
|
||||||
href: "#",
|
|
||||||
},
|
|
||||||
featuresTitle: "حلول الأعمال:",
|
|
||||||
features: [
|
|
||||||
"جميع ميزات الخطة المميزة",
|
|
||||||
"مدفوعات جماعية للموظفين",
|
|
||||||
"بوابات دفع مخصصة للتجار",
|
|
||||||
"تحليلات مالية متقدمة للأعمال",
|
|
||||||
"مدير حساب شخصي",
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
title="خطط مرنة تناسب الجميع"
|
title="خطط مرنة تناسب الجميع"
|
||||||
@@ -364,53 +211,23 @@ export default function LandingPage() {
|
|||||||
useInvertedBackground={true}
|
useInvertedBackground={true}
|
||||||
testimonials={[
|
testimonials={[
|
||||||
{
|
{
|
||||||
id: "t1",
|
id: "t1", name: "أحمد المغربي", handle: "@AhmedMorocco", testimonial: "Inwi Moneye غيّر طريقة تعاملي مع الأموال. التحويلات سريعة وسهلة، ودفع الفواتير أصبح لا يتطلب أي مجهود. أوصي به بشدة!", rating: 5,
|
||||||
name: "أحمد المغربي",
|
imageSrc: "http://img.b2bpic.net/free-photo/close-up-smiley-man-library_23-2149204750.jpg", imageAlt: "أحمد المغربي"},
|
||||||
handle: "@AhmedMorocco",
|
|
||||||
testimonial: "Inwi Moneye غيّر طريقة تعاملي مع الأموال. التحويلات سريعة وسهلة، ودفع الفواتير أصبح لا يتطلب أي مجهود. أوصي به بشدة!",
|
|
||||||
rating: 5,
|
|
||||||
imageSrc: "http://img.b2bpic.net/free-photo/close-up-smiley-man-library_23-2149204750.jpg",
|
|
||||||
imageAlt: "أحمد المغربي",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: "t2",
|
id: "t2", name: "فاطمة الزهراء", handle: "@FatimaZahra", testimonial: "تطبيق رائع! خاصة ميزة ربط الحسابات البنكية ودعم الذكاء الاصطناعي الذي يقدم لي نصائح قيمة. أشعر أن أموالي تحت السيطرة.", rating: 5,
|
||||||
name: "فاطمة الزهراء",
|
imageSrc: "http://img.b2bpic.net/free-photo/powerful-woman-director-receives-project-updates-via-text-messages_482257-106601.jpg", imageAlt: "فاطمة الزهراء"},
|
||||||
handle: "@FatimaZahra",
|
|
||||||
testimonial: "تطبيق رائع! خاصة ميزة ربط الحسابات البنكية ودعم الذكاء الاصطناعي الذي يقدم لي نصائح قيمة. أشعر أن أموالي تحت السيطرة.",
|
|
||||||
rating: 5,
|
|
||||||
imageSrc: "http://img.b2bpic.net/free-photo/powerful-woman-director-receives-project-updates-via-text-messages_482257-106601.jpg",
|
|
||||||
imageAlt: "فاطمة الزهراء",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: "t3",
|
id: "t3", name: "يوسف بناني", handle: "@YoussefB", testimonial: "كنت أبحث عن حل لدفع فواتير الإنترنت وشحن رصيد هاتفي بسهولة. Inwi Moneye فاق توقعاتي بفضل سرعته وأمانه العالي.", rating: 5,
|
||||||
name: "يوسف بناني",
|
imageSrc: "http://img.b2bpic.net/free-photo/portrait-elegant-male-enjoying-coffee_23-2148673397.jpg", imageAlt: "يوسف بناني"},
|
||||||
handle: "@YoussefB",
|
|
||||||
testimonial: "كنت أبحث عن حل لدفع فواتير الإنترنت وشحن رصيد هاتفي بسهولة. Inwi Moneye فاق توقعاتي بفضل سرعته وأمانه العالي.",
|
|
||||||
rating: 5,
|
|
||||||
imageSrc: "http://img.b2bpic.net/free-photo/portrait-elegant-male-enjoying-coffee_23-2148673397.jpg",
|
|
||||||
imageAlt: "يوسف بناني",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: "t4",
|
id: "t4", name: "مريم علوي", handle: "@MaryamAlawi", testimonial: "خدمة عملاء ممتازة وتطبيق سهل الاستخدام. قمت بتحويل أموال دولياً عدة مرات ولم أواجه أي مشكلة. تجربة رائعة!", rating: 5,
|
||||||
name: "مريم علوي",
|
imageSrc: "http://img.b2bpic.net/free-photo/young-man-funny-expression_1194-3026.jpg", imageAlt: "مريم علوي"},
|
||||||
handle: "@MaryamAlawi",
|
|
||||||
testimonial: "خدمة عملاء ممتازة وتطبيق سهل الاستخدام. قمت بتحويل أموال دولياً عدة مرات ولم أواجه أي مشكلة. تجربة رائعة!",
|
|
||||||
rating: 5,
|
|
||||||
imageSrc: "http://img.b2bpic.net/free-photo/young-man-funny-expression_1194-3026.jpg",
|
|
||||||
imageAlt: "مريم علوي",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: "t5",
|
id: "t5", name: "خالد رشيدي", handle: "@KhalidRachidi", testimonial: "أحببت كيف يساعدني الذكاء الاصطناعي في تتبع إنفاقي وتقديم رؤى لتحسين ميزانيتي. إنه كأن لدي مساعد مالي شخصي.", rating: 5,
|
||||||
name: "خالد رشيدي",
|
imageSrc: "http://img.b2bpic.net/free-photo/middle-aged-people-asking-advice-assistance-regarding-pension-plans_482257-106912.jpg", imageAlt: "خالد رشيدي"},
|
||||||
handle: "@KhalidRachidi",
|
|
||||||
testimonial: "أحببت كيف يساعدني الذكاء الاصطناعي في تتبع إنفاقي وتقديم رؤى لتحسين ميزانيتي. إنه كأن لدي مساعد مالي شخصي.",
|
|
||||||
rating: 5,
|
|
||||||
imageSrc: "http://img.b2bpic.net/free-photo/middle-aged-people-asking-advice-assistance-regarding-pension-plans_482257-106912.jpg",
|
|
||||||
imageAlt: "خالد رشيدي",
|
|
||||||
},
|
|
||||||
]}
|
]}
|
||||||
showRating={true}
|
showRating={true}
|
||||||
title="ماذا يقول عملاؤنا؟"
|
title="ماذا يقول عملاؤنا?"
|
||||||
description="تجارب حقيقية من مستخدمي Inwi Moneye السعداء الذين يثقون بخدماتنا."
|
description="تجارب حقيقية من مستخدمي Inwi Moneye السعداء الذين يثقون بخدماتنا."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -421,30 +238,15 @@ export default function LandingPage() {
|
|||||||
useInvertedBackground={false}
|
useInvertedBackground={false}
|
||||||
faqs={[
|
faqs={[
|
||||||
{
|
{
|
||||||
id: "q1",
|
id: "q1", title: "كيف يمكنني تفعيل حساب Inwi Moneye الخاص بي؟", content: "لتفعيل حسابك، قم بتحميل التطبيق من متجر التطبيقات، ثم اتبع الخطوات لإدخال معلوماتك الشخصية والتحقق من هويتك. يستغرق الأمر بضع دقائق فقط."},
|
||||||
title: "كيف يمكنني تفعيل حساب Inwi Moneye الخاص بي؟",
|
|
||||||
content: "لتفعيل حسابك، قم بتحميل التطبيق من متجر التطبيقات، ثم اتبع الخطوات لإدخال معلوماتك الشخصية والتحقق من هويتك. يستغرق الأمر بضع دقائق فقط.",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: "q2",
|
id: "q2", title: "هل Inwi Moneye آمن للاستخدام؟", content: "نعم، نستخدم أحدث تقنيات التشفير والأمان لحماية بياناتك ومعاملاتك المالية. أمانك هو أولويتنا القصوى."},
|
||||||
title: "هل Inwi Moneye آمن للاستخدام؟",
|
|
||||||
content: "نعم، نستخدم أحدث تقنيات التشفير والأمان لحماية بياناتك ومعاملاتك المالية. أمانك هو أولويتنا القصوى.",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: "q3",
|
id: "q3", title: "ما هي رسوم التحويلات الدولية؟", content: "تختلف رسوم التحويلات الدولية حسب البلد والمبلغ. يمكنك مراجعة جدول الرسوم الشفاف داخل التطبيق قبل إتمام أي معاملة."},
|
||||||
title: "ما هي رسوم التحويلات الدولية؟",
|
|
||||||
content: "تختلف رسوم التحويلات الدولية حسب البلد والمبلغ. يمكنك مراجعة جدول الرسوم الشفاف داخل التطبيق قبل إتمام أي معاملة.",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: "q4",
|
id: "q4", title: "كيف يمكنني ربط حسابي البنكي؟", content: "اذهب إلى قسم 'إعدادات الحساب' في التطبيق، ثم اختر 'ربط حساب بنكي' واتبع التعليمات لإدخال تفاصيل حسابك بأمان."},
|
||||||
title: "كيف يمكنني ربط حسابي البنكي؟",
|
|
||||||
content: "اذهب إلى قسم 'إعدادات الحساب' في التطبيق، ثم اختر 'ربط حساب بنكي' واتبع التعليمات لإدخال تفاصيل حسابك بأمان.",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: "q5",
|
id: "q5", title: "ما هي ميزات الذكاء الاصطناعي؟", content: "يقدم لك الذكاء الاصطناعي رؤى مخصصة حول إنفاقك، ويقترح طرقاً لتوفير المال، وينبهك إلى الفواتير القادمة، ويساعدك في التخطيط المالي."},
|
||||||
title: "ما هي ميزات الذكاء الاصطناعي؟",
|
|
||||||
content: "يقدم لك الذكاء الاصطناعي رؤى مخصصة حول إنفاقك، ويقترح طرقاً لتوفير المال، وينبهك إلى الفواتير القادمة، ويساعدك في التخطيط المالي.",
|
|
||||||
},
|
|
||||||
]}
|
]}
|
||||||
title="الأسئلة الشائعة"
|
title="الأسئلة الشائعة"
|
||||||
description="إجابات على كل استفساراتك حول Inwi Moneye وكيف يمكنك الاستفادة القصوى من خدماتنا."
|
description="إجابات على كل استفساراتك حول Inwi Moneye وكيف يمكنك الاستفادة القصوى من خدماتنا."
|
||||||
@@ -456,18 +258,13 @@ export default function LandingPage() {
|
|||||||
<ContactText
|
<ContactText
|
||||||
useInvertedBackground={true}
|
useInvertedBackground={true}
|
||||||
background={{
|
background={{
|
||||||
variant: "cell-wave",
|
variant: "cell-wave"}}
|
||||||
}}
|
|
||||||
text="فريق دعم Inwi Moneye جاهز لمساعدتك في أي وقت. لا تتردد في الاتصال بنا لأي استفسارات أو دعم فني بخصوص محفظتك الرقمية. نحن هنا لخدمتك."
|
text="فريق دعم Inwi Moneye جاهز لمساعدتك في أي وقت. لا تتردد في الاتصال بنا لأي استفسارات أو دعم فني بخصوص محفظتك الرقمية. نحن هنا لخدمتك."
|
||||||
buttons={[
|
buttons={[
|
||||||
{
|
{
|
||||||
text: "أرسل رسالة",
|
text: "أرسل رسالة", href: "mailto:support@inwimoneye.com"},
|
||||||
href: "mailto:support@inwimoneye.com",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
text: "اتصل بالدعم",
|
text: "اتصل بالدعم", href: "tel:+212-XXX-XXXXXX"},
|
||||||
href: "tel:+212-XXX-XXXXXX",
|
|
||||||
},
|
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -479,19 +276,13 @@ export default function LandingPage() {
|
|||||||
socialLinks={[
|
socialLinks={[
|
||||||
{
|
{
|
||||||
icon: Facebook,
|
icon: Facebook,
|
||||||
href: "https://facebook.com/inwimoneye",
|
href: "https://facebook.com/inwimoneye", ariaLabel: "Facebook"},
|
||||||
ariaLabel: "Facebook",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
icon: Twitter,
|
icon: Twitter,
|
||||||
href: "https://twitter.com/inwimoneye",
|
href: "https://twitter.com/inwimoneye", ariaLabel: "Twitter"},
|
||||||
ariaLabel: "Twitter",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
icon: Linkedin,
|
icon: Linkedin,
|
||||||
href: "https://linkedin.com/company/inwimoneye",
|
href: "https://linkedin.com/company/inwimoneye", ariaLabel: "LinkedIn"},
|
||||||
ariaLabel: "LinkedIn",
|
|
||||||
},
|
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,15 +10,15 @@
|
|||||||
--accent: #ffffff;
|
--accent: #ffffff;
|
||||||
--background-accent: #ffffff; */
|
--background-accent: #ffffff; */
|
||||||
|
|
||||||
--background: #0a0a0a;
|
--background: #ffffff;
|
||||||
--card: #1a1a1a;
|
--card: #f9f9f9;
|
||||||
--foreground: #f5f5f5;
|
--foreground: #000612e6;
|
||||||
--primary-cta: #ffdf7d;
|
--primary-cta: #15479c;
|
||||||
--primary-cta-text: #0a0a0a;
|
--primary-cta-text: #ffffff;
|
||||||
--secondary-cta: #1a1a1a;
|
--secondary-cta: #f9f9f9;
|
||||||
--secondary-cta-text: #ffffff;
|
--secondary-cta-text: #ffffff;
|
||||||
--accent: #b8860b;
|
--accent: #e2e2e2;
|
||||||
--background-accent: #8b6914;
|
--background-accent: #c4c4c4;
|
||||||
|
|
||||||
/* text sizing - set by ThemeProvider */
|
/* text sizing - set by ThemeProvider */
|
||||||
/* --text-2xs: clamp(0.465rem, 0.62vw, 0.62rem);
|
/* --text-2xs: clamp(0.465rem, 0.62vw, 0.62rem);
|
||||||
|
|||||||
@@ -1,118 +1,88 @@
|
|||||||
import { useEffect, useState, useRef, RefObject } from "react";
|
import { useEffect } from 'react';
|
||||||
|
import { useMotionValue, useSpring, useTransform } from 'framer-motion';
|
||||||
|
import { Variants } from '@/types/AnimatePresence';
|
||||||
|
|
||||||
const MOBILE_BREAKPOINT = 768;
|
interface Depth3DAnimationProps {
|
||||||
const ANIMATION_SPEED = 0.05;
|
perspective?: number;
|
||||||
const ROTATION_SPEED = 0.1;
|
depthFactor?: number;
|
||||||
const MOUSE_MULTIPLIER = 0.5;
|
hoverScale?: number;
|
||||||
const ROTATION_MULTIPLIER = 0.25;
|
transition?: {
|
||||||
|
duration?: number;
|
||||||
interface UseDepth3DAnimationProps {
|
ease?: string;
|
||||||
itemRefs: RefObject<(HTMLElement | null)[]>;
|
};
|
||||||
containerRef: RefObject<HTMLDivElement | null>;
|
springOptions?: {
|
||||||
perspectiveRef?: RefObject<HTMLDivElement | null>;
|
stiffness?: number;
|
||||||
isEnabled: boolean;
|
damping?: number;
|
||||||
|
mass?: number;
|
||||||
|
};
|
||||||
|
rotationXRange?: number[];
|
||||||
|
rotationYRange?: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useDepth3DAnimation = ({
|
export const useDepth3DAnimation = ({
|
||||||
itemRefs,
|
perspective = 2000,
|
||||||
containerRef,
|
depthFactor = 20,
|
||||||
perspectiveRef,
|
hoverScale = 1.05,
|
||||||
isEnabled,
|
transition = { duration: 0.8, ease: [0.6, 0.01, -0.05, 0.9] },
|
||||||
}: UseDepth3DAnimationProps) => {
|
springOptions = { stiffness: 400, damping: 10, mass: 1 },
|
||||||
const [isMobile, setIsMobile] = useState(false);
|
rotationXRange = [-10, 10],
|
||||||
|
rotationYRange = [-10, 10],
|
||||||
|
}: Depth3DAnimationProps) => {
|
||||||
|
const x = useMotionValue(0);
|
||||||
|
const y = useMotionValue(0);
|
||||||
|
|
||||||
// Detect mobile viewport
|
const mouseXSpring = useSpring(x, springOptions);
|
||||||
useEffect(() => {
|
const mouseYSpring = useSpring(y, springOptions);
|
||||||
const checkMobile = () => {
|
|
||||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
const rotateX = useTransform(
|
||||||
|
mouseYSpring,
|
||||||
|
[-0.5, 0.5],
|
||||||
|
rotationXRange as Variants<number[]>,
|
||||||
|
);
|
||||||
|
const rotateY = useTransform(
|
||||||
|
mouseXSpring,
|
||||||
|
[-0.5, 0.5],
|
||||||
|
rotationYRange as Variants<number[]>,
|
||||||
|
);
|
||||||
|
const scale = useTransform(mouseXSpring, [-0.5, 0.5], [1, hoverScale]);
|
||||||
|
|
||||||
|
const handleMouseMove = (event: React.MouseEvent) => {
|
||||||
|
const rect = (event.target as HTMLElement).getBoundingClientRect();
|
||||||
|
const width = rect.width;
|
||||||
|
const height = rect.height;
|
||||||
|
const mouseX = event.clientX - rect.left;
|
||||||
|
const mouseY = event.clientY - rect.top;
|
||||||
|
const xPct = mouseX / width - 0.5;
|
||||||
|
const yPct = mouseY / height - 0.5;
|
||||||
|
x.set(xPct);
|
||||||
|
y.set(yPct);
|
||||||
};
|
};
|
||||||
|
|
||||||
checkMobile();
|
const handleMouseLeave = () => {
|
||||||
window.addEventListener("resize", checkMobile);
|
x.set(0);
|
||||||
|
y.set(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Cleanup function if needed, though Framer Motion handles many subscriptions internally.
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener("resize", checkMobile);
|
// No explicit cleanup for motion values in this simple case, but good to keep in mind.
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 3D mouse-tracking effect (desktop only)
|
return {
|
||||||
useEffect(() => {
|
style: {
|
||||||
if (!isEnabled || isMobile) return;
|
perspective: perspective + 'px',
|
||||||
|
transformStyle: 'preserve-3d',
|
||||||
let animationFrameId: number;
|
rotateX,
|
||||||
let isAnimating = true;
|
rotateY,
|
||||||
|
scale,
|
||||||
// Apply perspective to the perspective ref (grid) if provided, otherwise to container (section)
|
transition: transition as Variants<typeof transition>,
|
||||||
const perspectiveElement = perspectiveRef?.current || containerRef.current;
|
// The depth effect is implicitly handled by the perspective and rotation transform
|
||||||
if (perspectiveElement) {
|
// Additional translateZ can be added for more explicit depth if needed
|
||||||
perspectiveElement.style.perspective = "1200px";
|
translateZ: depthFactor + 'px',
|
||||||
perspectiveElement.style.transformStyle = "preserve-3d";
|
},
|
||||||
}
|
onMouseMove: handleMouseMove,
|
||||||
|
onMouseLeave: handleMouseLeave,
|
||||||
let mouseX = 0;
|
|
||||||
let mouseY = 0;
|
|
||||||
let isMouseInSection = false;
|
|
||||||
|
|
||||||
let currentX = 0;
|
|
||||||
let currentY = 0;
|
|
||||||
let currentRotationX = 0;
|
|
||||||
let currentRotationY = 0;
|
|
||||||
|
|
||||||
const handleMouseMove = (event: MouseEvent): void => {
|
|
||||||
if (containerRef.current) {
|
|
||||||
const rect = containerRef.current.getBoundingClientRect();
|
|
||||||
isMouseInSection =
|
|
||||||
event.clientX >= rect.left &&
|
|
||||||
event.clientX <= rect.right &&
|
|
||||||
event.clientY >= rect.top &&
|
|
||||||
event.clientY <= rect.bottom;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isMouseInSection) {
|
|
||||||
mouseX = (event.clientX / window.innerWidth) * 100 - 50;
|
|
||||||
mouseY = (event.clientY / window.innerHeight) * 100 - 50;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const animate = (): void => {
|
|
||||||
if (!isAnimating) return;
|
|
||||||
|
|
||||||
if (isMouseInSection) {
|
|
||||||
const distX = mouseX * MOUSE_MULTIPLIER - currentX;
|
|
||||||
const distY = mouseY * MOUSE_MULTIPLIER - currentY;
|
|
||||||
currentX += distX * ANIMATION_SPEED;
|
|
||||||
currentY += distY * ANIMATION_SPEED;
|
|
||||||
|
|
||||||
const distRotX = -mouseY * ROTATION_MULTIPLIER - currentRotationX;
|
|
||||||
const distRotY = mouseX * ROTATION_MULTIPLIER - currentRotationY;
|
|
||||||
currentRotationX += distRotX * ROTATION_SPEED;
|
|
||||||
currentRotationY += distRotY * ROTATION_SPEED;
|
|
||||||
} else {
|
|
||||||
currentX += -currentX * ANIMATION_SPEED;
|
|
||||||
currentY += -currentY * ANIMATION_SPEED;
|
|
||||||
currentRotationX += -currentRotationX * ROTATION_SPEED;
|
|
||||||
currentRotationY += -currentRotationY * ROTATION_SPEED;
|
|
||||||
}
|
|
||||||
|
|
||||||
itemRefs.current?.forEach((ref) => {
|
|
||||||
if (!ref) return;
|
|
||||||
ref.style.transform = `translate(${currentX}px, ${currentY}px) rotateX(${currentRotationX}deg) rotateY(${currentRotationY}deg)`;
|
|
||||||
});
|
|
||||||
|
|
||||||
animationFrameId = requestAnimationFrame(animate);
|
|
||||||
};
|
|
||||||
|
|
||||||
animate();
|
|
||||||
window.addEventListener("mousemove", handleMouseMove);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener("mousemove", handleMouseMove);
|
|
||||||
if (animationFrameId) {
|
|
||||||
cancelAnimationFrame(animationFrameId);
|
|
||||||
}
|
|
||||||
isAnimating = false;
|
|
||||||
};
|
|
||||||
}, [isEnabled, isMobile, itemRefs, containerRef]);
|
|
||||||
|
|
||||||
return { isMobile };
|
|
||||||
};
|
};
|
||||||
|
|||||||
1
src/config.ts
Normal file
1
src/config.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const config = {};
|
||||||
@@ -1,115 +1,34 @@
|
|||||||
"use client";
|
import { useState, useEffect } from 'react';
|
||||||
|
import { fetchProducts } from '@/lib/api/product';
|
||||||
|
|
||||||
import { useState, useMemo, useCallback } from "react";
|
interface UseProductCatalogProps {
|
||||||
import { useRouter } from "next/navigation";
|
initialProducts?: Product[];
|
||||||
import { useProducts } from "./useProducts";
|
category?: string;
|
||||||
import type { Product } from "@/lib/api/product";
|
limit?: number;
|
||||||
import type { CatalogProduct } from "@/components/ecommerce/productCatalog/ProductCatalogItem";
|
|
||||||
import type { ProductVariant } from "@/components/ecommerce/productDetail/ProductDetailCard";
|
|
||||||
|
|
||||||
export type SortOption = "Newest" | "Price: Low-High" | "Price: High-Low";
|
|
||||||
|
|
||||||
interface UseProductCatalogOptions {
|
|
||||||
basePath?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useProductCatalog(options: UseProductCatalogOptions = {}) {
|
export const useProductCatalog = ({ initialProducts, category, limit }: UseProductCatalogProps) => {
|
||||||
const { basePath = "/shop" } = options;
|
const [products, setProducts] = useState<Product[]>(initialProducts || []);
|
||||||
const router = useRouter();
|
const [loading, setLoading] = useState(false);
|
||||||
const { products: fetchedProducts, isLoading } = useProducts();
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const [search, setSearch] = useState("");
|
useEffect(() => {
|
||||||
const [category, setCategory] = useState("All");
|
const loadProducts = async () => {
|
||||||
const [sort, setSort] = useState<SortOption>("Newest");
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
const handleProductClick = useCallback((productId: string) => {
|
try {
|
||||||
router.push(`${basePath}/${productId}`);
|
const fetchedProducts = await fetchProducts({ category, limit });
|
||||||
}, [router, basePath]);
|
setProducts(fetchedProducts);
|
||||||
|
} catch (err) {
|
||||||
const catalogProducts: CatalogProduct[] = useMemo(() => {
|
setError('Failed to fetch products');
|
||||||
if (fetchedProducts.length === 0) return [];
|
|
||||||
|
|
||||||
return fetchedProducts.map((product) => ({
|
|
||||||
id: product.id,
|
|
||||||
name: product.name,
|
|
||||||
price: product.price,
|
|
||||||
imageSrc: product.imageSrc,
|
|
||||||
imageAlt: product.imageAlt || product.name,
|
|
||||||
rating: product.rating || 0,
|
|
||||||
reviewCount: product.reviewCount,
|
|
||||||
category: product.brand,
|
|
||||||
onProductClick: () => handleProductClick(product.id),
|
|
||||||
}));
|
|
||||||
}, [fetchedProducts, handleProductClick]);
|
|
||||||
|
|
||||||
const categories = useMemo(() => {
|
|
||||||
const categorySet = new Set<string>();
|
|
||||||
catalogProducts.forEach((product) => {
|
|
||||||
if (product.category) {
|
|
||||||
categorySet.add(product.category);
|
|
||||||
}
|
}
|
||||||
});
|
setLoading(false);
|
||||||
return Array.from(categorySet).sort();
|
|
||||||
}, [catalogProducts]);
|
|
||||||
|
|
||||||
const filteredProducts = useMemo(() => {
|
|
||||||
let result = catalogProducts;
|
|
||||||
|
|
||||||
if (search) {
|
|
||||||
const q = search.toLowerCase();
|
|
||||||
result = result.filter(
|
|
||||||
(p) =>
|
|
||||||
p.name.toLowerCase().includes(q) ||
|
|
||||||
(p.category?.toLowerCase().includes(q) ?? false)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (category !== "All") {
|
|
||||||
result = result.filter((p) => p.category === category);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sort === "Price: Low-High") {
|
|
||||||
result = [...result].sort(
|
|
||||||
(a, b) =>
|
|
||||||
parseFloat(a.price.replace("$", "").replace(",", "")) -
|
|
||||||
parseFloat(b.price.replace("$", "").replace(",", ""))
|
|
||||||
);
|
|
||||||
} else if (sort === "Price: High-Low") {
|
|
||||||
result = [...result].sort(
|
|
||||||
(a, b) =>
|
|
||||||
parseFloat(b.price.replace("$", "").replace(",", "")) -
|
|
||||||
parseFloat(a.price.replace("$", "").replace(",", ""))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}, [catalogProducts, search, category, sort]);
|
|
||||||
|
|
||||||
const filters: ProductVariant[] = useMemo(() => [
|
|
||||||
{
|
|
||||||
label: "Category",
|
|
||||||
options: ["All", ...categories],
|
|
||||||
selected: category,
|
|
||||||
onChange: setCategory,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Sort",
|
|
||||||
options: ["Newest", "Price: Low-High", "Price: High-Low"] as SortOption[],
|
|
||||||
selected: sort,
|
|
||||||
onChange: (value) => setSort(value as SortOption),
|
|
||||||
},
|
|
||||||
], [categories, category, sort]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
products: filteredProducts,
|
|
||||||
isLoading,
|
|
||||||
search,
|
|
||||||
setSearch,
|
|
||||||
category,
|
|
||||||
setCategory,
|
|
||||||
sort,
|
|
||||||
setSort,
|
|
||||||
filters,
|
|
||||||
categories,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!initialProducts || initialProducts.length === 0) {
|
||||||
|
loadProducts();
|
||||||
}
|
}
|
||||||
|
}, [initialProducts, category, limit]);
|
||||||
|
|
||||||
|
return { products, loading, error };
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,196 +1,32 @@
|
|||||||
"use client";
|
import { useState, useEffect } from 'react';
|
||||||
|
import { fetchProductById } from '@/lib/api/product';
|
||||||
|
|
||||||
import { useState, useMemo, useCallback } from "react";
|
interface UseProductDetailProps {
|
||||||
import { useProduct } from "./useProduct";
|
productId: string;
|
||||||
import type { Product } from "@/lib/api/product";
|
|
||||||
import type { ProductVariant } from "@/components/ecommerce/productDetail/ProductDetailCard";
|
|
||||||
import type { ExtendedCartItem } from "./useCart";
|
|
||||||
|
|
||||||
interface ProductImage {
|
|
||||||
src: string;
|
|
||||||
alt: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ProductMeta {
|
export const useProductDetail = ({ productId }: UseProductDetailProps) => {
|
||||||
salePrice?: string;
|
const [product, setProduct] = useState<Product | null>(null);
|
||||||
ribbon?: string;
|
const [loading, setLoading] = useState(false);
|
||||||
inventoryStatus?: string;
|
const [error, setError] = useState<string | null>(null);
|
||||||
inventoryQuantity?: number;
|
|
||||||
sku?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useProductDetail(productId: string) {
|
useEffect(() => {
|
||||||
const { product, isLoading, error } = useProduct(productId);
|
const loadProduct = async () => {
|
||||||
const [selectedQuantity, setSelectedQuantity] = useState(1);
|
setLoading(true);
|
||||||
const [selectedVariants, setSelectedVariants] = useState<Record<string, string>>({});
|
setError(null);
|
||||||
|
|
||||||
const images = useMemo<ProductImage[]>(() => {
|
|
||||||
if (!product) return [];
|
|
||||||
|
|
||||||
if (product.images && product.images.length > 0) {
|
|
||||||
return product.images.map((src, index) => ({
|
|
||||||
src,
|
|
||||||
alt: product.imageAlt || `${product.name} - Image ${index + 1}`,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
return [{
|
|
||||||
src: product.imageSrc,
|
|
||||||
alt: product.imageAlt || product.name,
|
|
||||||
}];
|
|
||||||
}, [product]);
|
|
||||||
|
|
||||||
const meta = useMemo<ProductMeta>(() => {
|
|
||||||
if (!product?.metadata) return {};
|
|
||||||
|
|
||||||
const metadata = product.metadata;
|
|
||||||
|
|
||||||
let salePrice: string | undefined;
|
|
||||||
const onSaleValue = metadata.onSale;
|
|
||||||
const onSale = String(onSaleValue) === "true" || onSaleValue === 1 || String(onSaleValue) === "1";
|
|
||||||
const salePriceValue = metadata.salePrice;
|
|
||||||
|
|
||||||
if (onSale && salePriceValue !== undefined && salePriceValue !== null) {
|
|
||||||
if (typeof salePriceValue === 'number') {
|
|
||||||
salePrice = `$${salePriceValue.toFixed(2)}`;
|
|
||||||
} else {
|
|
||||||
const salePriceStr = String(salePriceValue);
|
|
||||||
salePrice = salePriceStr.startsWith('$') ? salePriceStr : `$${salePriceStr}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let inventoryQuantity: number | undefined;
|
|
||||||
if (metadata.inventoryQuantity !== undefined) {
|
|
||||||
const qty = metadata.inventoryQuantity;
|
|
||||||
inventoryQuantity = typeof qty === 'number' ? qty : parseInt(String(qty), 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
salePrice,
|
|
||||||
ribbon: metadata.ribbon ? String(metadata.ribbon) : undefined,
|
|
||||||
inventoryStatus: metadata.inventoryStatus ? String(metadata.inventoryStatus) : undefined,
|
|
||||||
inventoryQuantity,
|
|
||||||
sku: metadata.sku ? String(metadata.sku) : undefined,
|
|
||||||
};
|
|
||||||
}, [product]);
|
|
||||||
|
|
||||||
const variants = useMemo<ProductVariant[]>(() => {
|
|
||||||
if (!product) return [];
|
|
||||||
|
|
||||||
const variantList: ProductVariant[] = [];
|
|
||||||
|
|
||||||
if (product.metadata?.variantOptions) {
|
|
||||||
try {
|
try {
|
||||||
const variantOptionsStr = String(product.metadata.variantOptions);
|
const fetchedProduct = await fetchProductById(productId);
|
||||||
const parsedOptions = JSON.parse(variantOptionsStr);
|
setProduct(fetchedProduct);
|
||||||
|
} catch (err) {
|
||||||
if (Array.isArray(parsedOptions)) {
|
setError('Failed to fetch product details');
|
||||||
parsedOptions.forEach((option: any) => {
|
|
||||||
if (option.name && option.values) {
|
|
||||||
const values = typeof option.values === 'string'
|
|
||||||
? option.values.split(',').map((v: string) => v.trim())
|
|
||||||
: Array.isArray(option.values)
|
|
||||||
? option.values.map((v: any) => String(v).trim())
|
|
||||||
: [String(option.values)];
|
|
||||||
|
|
||||||
if (values.length > 0) {
|
|
||||||
const optionLabel = option.name;
|
|
||||||
const currentSelected = selectedVariants[optionLabel] || values[0];
|
|
||||||
|
|
||||||
variantList.push({
|
|
||||||
label: optionLabel,
|
|
||||||
options: values,
|
|
||||||
selected: currentSelected,
|
|
||||||
onChange: (value) => {
|
|
||||||
setSelectedVariants((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[optionLabel]: value,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
setLoading(false);
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn("Failed to parse variantOptions:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (variantList.length === 0 && product.brand) {
|
|
||||||
variantList.push({
|
|
||||||
label: "Brand",
|
|
||||||
options: [product.brand],
|
|
||||||
selected: product.brand,
|
|
||||||
onChange: () => { },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (variantList.length === 0 && product.variant) {
|
|
||||||
const variantOptions = product.variant.includes('/')
|
|
||||||
? product.variant.split('/').map(v => v.trim())
|
|
||||||
: [product.variant];
|
|
||||||
|
|
||||||
const variantLabel = "Variant";
|
|
||||||
const currentSelected = selectedVariants[variantLabel] || variantOptions[0];
|
|
||||||
|
|
||||||
variantList.push({
|
|
||||||
label: variantLabel,
|
|
||||||
options: variantOptions,
|
|
||||||
selected: currentSelected,
|
|
||||||
onChange: (value) => {
|
|
||||||
setSelectedVariants((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[variantLabel]: value,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return variantList;
|
|
||||||
}, [product, selectedVariants]);
|
|
||||||
|
|
||||||
const quantityVariant = useMemo<ProductVariant>(() => ({
|
|
||||||
label: "Quantity",
|
|
||||||
options: Array.from({ length: 10 }, (_, i) => String(i + 1)),
|
|
||||||
selected: String(selectedQuantity),
|
|
||||||
onChange: (value) => setSelectedQuantity(parseInt(value, 10)),
|
|
||||||
}), [selectedQuantity]);
|
|
||||||
|
|
||||||
const createCartItem = useCallback((): ExtendedCartItem | null => {
|
|
||||||
if (!product) return null;
|
|
||||||
|
|
||||||
const variantStrings = Object.entries(selectedVariants).map(
|
|
||||||
([label, value]) => `${label}: ${value}`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (variantStrings.length === 0 && product.variant) {
|
|
||||||
variantStrings.push(`Variant: ${product.variant}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const variantId = Object.values(selectedVariants).join('-') || 'default';
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: `${product.id}-${variantId}-${selectedQuantity}`,
|
|
||||||
productId: product.id,
|
|
||||||
name: product.name,
|
|
||||||
variants: variantStrings,
|
|
||||||
price: product.price,
|
|
||||||
quantity: selectedQuantity,
|
|
||||||
imageSrc: product.imageSrc,
|
|
||||||
imageAlt: product.imageAlt || product.name,
|
|
||||||
};
|
};
|
||||||
}, [product, selectedVariants, selectedQuantity]);
|
|
||||||
|
|
||||||
return {
|
if (productId) {
|
||||||
product,
|
loadProduct();
|
||||||
isLoading,
|
|
||||||
error,
|
|
||||||
images,
|
|
||||||
meta,
|
|
||||||
variants,
|
|
||||||
quantityVariant,
|
|
||||||
selectedQuantity,
|
|
||||||
selectedVariants,
|
|
||||||
createCartItem,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
}, [productId]);
|
||||||
|
|
||||||
|
return { product, loading, error };
|
||||||
|
};
|
||||||
|
|||||||
27
src/i18n.ts
Normal file
27
src/i18n.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// @ts-expect-error Missing declaration file
|
||||||
|
import i18n from 'i18next';
|
||||||
|
// @ts-expect-error Missing declaration file
|
||||||
|
import Backend from 'i18next-http-backend';
|
||||||
|
// @ts-expect-error Missing declaration file
|
||||||
|
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||||
|
// @ts-expect-error Missing declaration file
|
||||||
|
import { initReactI18next } from 'react-i18next';
|
||||||
|
|
||||||
|
i18n
|
||||||
|
.use(Backend)
|
||||||
|
.use(LanguageDetector)
|
||||||
|
.use(initReactI18next)
|
||||||
|
.init({
|
||||||
|
fallbackLng: 'en',
|
||||||
|
debug: false,
|
||||||
|
interpolation: {
|
||||||
|
escapeValue: false,
|
||||||
|
},
|
||||||
|
backend: {
|
||||||
|
loadPath: '/locales/{{lng}}/{{ns}}.json',
|
||||||
|
},
|
||||||
|
ns: ['common', 'home'],
|
||||||
|
defaultNS: 'common',
|
||||||
|
});
|
||||||
|
|
||||||
|
export default i18n;
|
||||||
Reference in New Issue
Block a user