diff --git a/backend/src/db/db.config.js b/backend/src/db/db.config.js index df25d2d..8f37ad8 100644 --- a/backend/src/db/db.config.js +++ b/backend/src/db/db.config.js @@ -1,4 +1,4 @@ - +require('dotenv').config(); module.exports = { production: { @@ -12,11 +12,12 @@ module.exports = { seederStorage: 'sequelize', }, development: { - username: 'postgres', dialect: 'postgres', - password: '', - database: 'db_malaysian_mini_market_site', - host: process.env.DB_HOST || 'localhost', + username: process.env.DB_USER, + password: process.env.DB_PASS, + database: process.env.DB_NAME, + host: process.env.DB_HOST, + port: process.env.DB_PORT, logging: console.log, seederStorage: 'sequelize', }, @@ -30,4 +31,4 @@ module.exports = { logging: console.log, seederStorage: 'sequelize', } -}; +}; \ No newline at end of file diff --git a/backend/src/db/migrations/1771699492941-public-permissions.js b/backend/src/db/migrations/1771699492941-public-permissions.js index a73938f..8d21dc5 100644 --- a/backend/src/db/migrations/1771699492941-public-permissions.js +++ b/backend/src/db/migrations/1771699492941-public-permissions.js @@ -22,7 +22,8 @@ module.exports = { "hero_translations", "languages", "site_assets", - "file" + "file", + "shops" ]; const [permissions] = await queryInterface.sequelize.query( @@ -42,6 +43,10 @@ module.exports = { })); if (rolesPermissions.length > 0) { + // First, delete existing to avoid duplicates if re-run + await queryInterface.bulkDelete("rolesPermissionsPermissions", { + roles_permissionsId: publicRoleId + }); await queryInterface.bulkInsert("rolesPermissionsPermissions", rolesPermissions); } }, @@ -49,4 +54,4 @@ module.exports = { async down() { // Optional: remove the permissions }, -}; \ No newline at end of file +}; diff --git a/backend/src/db/seeders/20260221000000-minimarket-sample.js b/backend/src/db/seeders/20260221000000-minimarket-sample.js index 338ba0d..8e475e5 100644 --- a/backend/src/db/seeders/20260221000000-minimarket-sample.js +++ b/backend/src/db/seeders/20260221000000-minimarket-sample.js @@ -14,34 +14,71 @@ module.exports = { const langZhId = uuid(); const langTaId = uuid(); - // Ensure languages exist or fetch them - // For simplicity, I'll just insert everything needed - - await queryInterface.bulkInsert("languages", [ + const shopId = uuid(); + + // Check if languages exist before inserting + const [existingLangs] = await queryInterface.sequelize.query('SELECT id, code FROM "languages"'); + const existingCodes = existingLangs.map(l => l.code); + + const languagesToInsert = [ { id: langEnId, name: "English", code: "en", createdAt, updatedAt }, { id: langMsId, name: "Bahasa Melayu", code: "ms", createdAt, updatedAt }, { id: langZhId, name: "Mandarin", code: "zh", createdAt, updatedAt }, { id: langTaId, name: "Tamil", code: "ta", createdAt, updatedAt }, + ].filter(l => !existingCodes.includes(l.code)); + + if (languagesToInsert.length > 0) { + await queryInterface.bulkInsert("languages", languagesToInsert); + } + + // Get the actual IDs for translations + const [allLangs] = await queryInterface.sequelize.query('SELECT id, code FROM "languages"'); + const langMap = allLangs.reduce((acc, l) => ({ ...acc, [l.code]: l.id }), {}); + + await queryInterface.bulkInsert("shops", [ + { + id: shopId, + name: "Super Star Store", + tagline: "Your Neighbourhood Choice in Ipoh", + primary_color_hex: "#22c55e", + secondary_color_hex: "#fbbf24", + whatsapp_number: "60125749390", + phone_number: "012-574 9390", + email: "superstarstore@gmail.com", + address_line_1: "1-25, Laluan Menglembu Impiana 8", + address_line_2: "Taman Menglembu Impiana Adril", + postcode: "31450", + city: "Ipoh", + state: "Perak", + country: "Malaysia", + latitude: 4.5670, + longitude: 101.0450, + google_maps_embed_url: "https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d3976.99!2d101.04!3d4.56!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x0%3A0x0!2zNMKwMzMnMzguMiJOIDEwMcKwMDInNDIuMCJF!5e0!3m2!1sen!2smy!4v1700000000000!5m2!1sen!2smy", + opening_hours_text: "Closed · Opens 8:30 am", + is_published: true, + createdAt, + updatedAt + } ]); await queryInterface.bulkInsert("product_categories", [ - { id: catDrinkId, code: "Drinks", createdAt, updatedAt }, - { id: catSnackId, code: "Snacks", createdAt, updatedAt }, - { id: catGroceryId, code: "Groceries", createdAt, updatedAt }, + { id: catDrinkId, code: "Drinks", shopId, createdAt, updatedAt }, + { id: catSnackId, code: "Snacks", shopId, createdAt, updatedAt }, + { id: catGroceryId, code: "Groceries", shopId, createdAt, updatedAt }, ]); await queryInterface.bulkInsert("category_translations", [ - { id: uuid(), name: "Minuman", languageId: langMsId, categoryId: catDrinkId, createdAt, updatedAt }, - { id: uuid(), name: "饮料", languageId: langZhId, categoryId: catDrinkId, createdAt, updatedAt }, - { id: uuid(), name: "பானங்கள்", languageId: langTaId, categoryId: catDrinkId, createdAt, updatedAt }, + { id: uuid(), name: "Minuman", languageId: langMap["ms"], categoryId: catDrinkId, createdAt, updatedAt }, + { id: uuid(), name: "饮料", languageId: langMap["zh"], categoryId: catDrinkId, createdAt, updatedAt }, + { id: uuid(), name: "பானங்கள்", languageId: langMap["ta"], categoryId: catDrinkId, createdAt, updatedAt }, - { id: uuid(), name: "Snek", languageId: langMsId, categoryId: catSnackId, createdAt, updatedAt }, - { id: uuid(), name: "零食", languageId: langZhId, categoryId: catSnackId, createdAt, updatedAt }, - { id: uuid(), name: "சிற்றுண்டி", languageId: langTaId, categoryId: catSnackId, createdAt, updatedAt }, + { id: uuid(), name: "Snek", languageId: langMap["ms"], categoryId: catSnackId, createdAt, updatedAt }, + { id: uuid(), name: "零食", languageId: langMap["zh"], categoryId: catSnackId, createdAt, updatedAt }, + { id: uuid(), name: "சிற்றுண்டி", languageId: langMap["ta"], categoryId: catSnackId, createdAt, updatedAt }, - { id: uuid(), name: "Runcit", languageId: langMsId, categoryId: catGroceryId, createdAt, updatedAt }, - { id: uuid(), name: "杂货", languageId: langZhId, categoryId: catGroceryId, createdAt, updatedAt }, - { id: uuid(), name: "மளிகை", languageId: langTaId, categoryId: catGroceryId, createdAt, updatedAt }, + { id: uuid(), name: "Runcit", languageId: langMap["ms"], categoryId: catGroceryId, createdAt, updatedAt }, + { id: uuid(), name: "杂货", languageId: langMap["zh"], categoryId: catGroceryId, createdAt, updatedAt }, + { id: uuid(), name: "மளிகை", languageId: langMap["ta"], categoryId: catGroceryId, createdAt, updatedAt }, ]); const prod1Id = uuid(); @@ -57,6 +94,7 @@ module.exports = { stock_quantity: 100, status: "active", categoryId: catDrinkId, + shopId, createdAt, updatedAt }, { @@ -66,6 +104,7 @@ module.exports = { stock_quantity: 50, status: "active", categoryId: catSnackId, + shopId, createdAt, updatedAt }, { @@ -75,36 +114,38 @@ module.exports = { stock_quantity: 200, status: "active", categoryId: catGroceryId, + shopId, createdAt, updatedAt }, ]); await queryInterface.bulkInsert("product_translations", [ - { id: uuid(), name: "Milo Can 240ml", languageId: langEnId, productId: prod1Id, createdAt, updatedAt }, - { id: uuid(), name: "Tin Milo 240ml", languageId: langMsId, productId: prod1Id, createdAt, updatedAt }, - { id: uuid(), name: "美禄罐装 240ml", languageId: langZhId, productId: prod1Id, createdAt, updatedAt }, - { id: uuid(), name: "மைலோ டின் 240மி", languageId: langTaId, productId: prod1Id, createdAt, updatedAt }, + { id: uuid(), name: "Milo Can 240ml", languageId: langMap["en"], productId: prod1Id, createdAt, updatedAt }, + { id: uuid(), name: "Tin Milo 240ml", languageId: langMap["ms"], productId: prod1Id, createdAt, updatedAt }, + { id: uuid(), name: "美禄罐装 240ml", languageId: langMap["zh"], productId: prod1Id, createdAt, updatedAt }, + { id: uuid(), name: "மைலோ டின் 240மி", languageId: langMap["ta"], productId: prod1Id, createdAt, updatedAt }, - { id: uuid(), name: "Mamee Monster Snack", languageId: langEnId, productId: prod2Id, createdAt, updatedAt }, - { id: uuid(), name: "Snek Mamee Monster", languageId: langMsId, productId: prod2Id, createdAt, updatedAt }, - { id: uuid(), name: "妈咪怪兽零食", languageId: langZhId, productId: prod2Id, createdAt, updatedAt }, - { id: uuid(), name: "மாமி மான்ஸ்டர் சிற்றுண்டி", languageId: langTaId, productId: prod2Id, createdAt, updatedAt }, + { id: uuid(), name: "Mamee Monster Snack", languageId: langMap["en"], productId: prod2Id, createdAt, updatedAt }, + { id: uuid(), name: "Snek Mamee Monster", languageId: langMap["ms"], productId: prod2Id, createdAt, updatedAt }, + { id: uuid(), name: "妈咪怪兽零食", languageId: langMap["zh"], productId: prod2Id, createdAt, updatedAt }, + { id: uuid(), name: "மாமி மான்ஸ்டர் சிற்றுண்டி", languageId: langMap["ta"], productId: prod2Id, createdAt, updatedAt }, - { id: uuid(), name: "Maggi Curry Noodles 5-Pack", languageId: langEnId, productId: prod3Id, createdAt, updatedAt }, - { id: uuid(), name: "Maggi Kari 5-Pek", languageId: langMsId, productId: prod3Id, createdAt, updatedAt }, - { id: uuid(), name: "美极咖喱面 5包入", languageId: langZhId, productId: prod3Id, createdAt, updatedAt }, - { id: uuid(), name: "மேகி கறி நூடுல்ஸ் 5-பேக்", languageId: langTaId, productId: prod3Id, createdAt, updatedAt }, + { id: uuid(), name: "Maggi Curry Noodles 5-Pack", languageId: langMap["en"], productId: prod3Id, createdAt, updatedAt }, + { id: uuid(), name: "Maggi Kari 5-Pek", languageId: langMap["ms"], productId: prod3Id, createdAt, updatedAt }, + { id: uuid(), name: "美极咖喱面 5包入", languageId: langMap["zh"], productId: prod3Id, createdAt, updatedAt }, + { id: uuid(), name: "மேகி கறி நூடுல்ஸ் 5-பேக்", languageId: langMap["ta"], productId: prod3Id, createdAt, updatedAt }, ]); const promoId = uuid(); await queryInterface.bulkInsert("promotions", [ - { id: promoId, title: "Ramadan Special Sale", status: "active", createdAt, updatedAt } + { id: promoId, promotion_type: "general", is_active: true, shopId, createdAt, updatedAt } ]); await queryInterface.bulkInsert("promotion_translations", [ - { id: uuid(), title: "Jualan Khas Ramadan", languageId: langMsId, promotionId: promoId, createdAt, updatedAt }, - { id: uuid(), title: "斋戒月特别大减价", languageId: langZhId, promotionId: promoId, createdAt, updatedAt }, - { id: uuid(), title: "ரம்ஜான் சிறப்பு விற்பனை", languageId: langTaId, promotionId: promoId, createdAt, updatedAt }, + { id: uuid(), title: "Ramadan Special Sale", languageId: langMap["en"], promotionId: promoId, createdAt, updatedAt }, + { id: uuid(), title: "Jualan Khas Ramadan", languageId: langMap["ms"], promotionId: promoId, createdAt, updatedAt }, + { id: uuid(), title: "斋戒月特别大减价", languageId: langMap["zh"], promotionId: promoId, createdAt, updatedAt }, + { id: uuid(), title: "ரம்ஜான் சிறப்பு விற்பனை", languageId: langMap["ta"], promotionId: promoId, createdAt, updatedAt }, ]); }, diff --git a/frontend/src/components/MiniMarket/ContactSection.tsx b/frontend/src/components/MiniMarket/ContactSection.tsx index 02caca9..d261435 100644 --- a/frontend/src/components/MiniMarket/ContactSection.tsx +++ b/frontend/src/components/MiniMarket/ContactSection.tsx @@ -1,99 +1,112 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; -import { mdiPhone, mdiWhatsapp, mdiMapMarker, mdiClock } from '@mdi/js'; +import { mdiPhone, mdiWhatsapp, mdiMapMarker, mdiClock, mdiEarth } from '@mdi/js'; import BaseIcon from '../BaseIcon'; import BaseButton from '../BaseButton'; -const ContactSection: React.FC = () => { +interface ContactSectionProps { + shop?: any; +} + +const ContactSection: React.FC = ({ shop }) => { const { t } = useTranslation(); + const shopName = shop?.name || 'Super Star Store'; return ( -
+
-
-
-
-

- {t('miniMarket.contactTitle', { defaultValue: 'Visit Super Star Store' })} +
+
+
+
+
+ +
+ Location +
+

+ Visit Our Retail Spot

-

+

{t('miniMarket.contactSubtitle', { defaultValue: 'We are conveniently located in Taman Menglembu Impiana Adril. Stop by today or reach out via WhatsApp.' })}

-
-
-
- +
+
+
+
-
-

{t('miniMarket.ourLocation', { defaultValue: 'Our Location' })}

-

- 1-25, Laluan Menglembu Impiana 8,
- Taman Menglembu Impiana Adril,
- 31450 Ipoh, Perak +

+

{t('miniMarket.ourLocation', { defaultValue: 'Physical Hub' })}

+

+ {shop?.address_line_1 || '1-25, Laluan Menglembu Impiana 8'},
+ {shop?.address_line_2 || 'Taman Menglembu Impiana Adril'},
+ {shop?.postcode || '31450'} {shop?.city || 'Ipoh'}, {shop?.state || 'Perak'}

-
-
- +
+
+
-
-

{t('miniMarket.callUs', { defaultValue: 'Call Us' })}

-

+60 12-574 9390

+
+

{t('miniMarket.callUs', { defaultValue: 'Direct Line' })}

+

{shop?.phone_number || '+60 12-574 9390'}

-
-
- +
+
+
-
-

{t('miniMarket.whatsappChat', { defaultValue: 'WhatsApp Chat' })}

-

- {t('miniMarket.orderNowViaWA', { defaultValue: 'Chat with us for orders & inquiries.' })} -

-
- +
+
+

{t('miniMarket.whatsappChat', { defaultValue: 'WhatsApp Orders' })}

+

+ {t('miniMarket.orderNowViaWA', { defaultValue: 'Quickest way to check stock or place an order for pick-up.' })} +

+
-
-
- +
+
+
-
-

{t('miniMarket.openingHours', { defaultValue: 'Opening Hours' })}

-

{t('miniMarket.monSat', { defaultValue: 'Daily: 8:30 AM - 10:00 PM' })}

+
+

{t('miniMarket.openingHours', { defaultValue: 'Business Hours' })}

+

{shop?.opening_hours_text || 'Daily: 8:30 AM - 10:00 PM'}

-
-
-
+
+ {/* Unique Graphic: Border Animation */} +
+
+ + {/* Dark Mode Map Overlay */} +
@@ -102,4 +115,4 @@ const ContactSection: React.FC = () => { ); }; -export default ContactSection; \ No newline at end of file +export default ContactSection; diff --git a/frontend/src/components/MiniMarket/Hero.tsx b/frontend/src/components/MiniMarket/Hero.tsx index 0127b70..119c2e6 100644 --- a/frontend/src/components/MiniMarket/Hero.tsx +++ b/frontend/src/components/MiniMarket/Hero.tsx @@ -1,71 +1,108 @@ import React from 'react'; import BaseButton from '../BaseButton'; -import { mdiWhatsapp, mdiStar } from '@mdi/js'; +import { mdiWhatsapp, mdiStar, mdiCircleSlice8 } from '@mdi/js'; import { useTranslation } from 'react-i18next'; import BaseIcon from '../BaseIcon'; -const Hero: React.FC = () => { +interface HeroProps { + shop?: any; +} + +const Hero: React.FC = ({ shop }) => { const { t } = useTranslation(); + const shopName = shop?.name || 'Super Star Store'; + const tagline = shop?.tagline || 'Your Neighbourhood Choice in Ipoh'; return ( -
-
-
-
-
- {t('miniMarket.freshLocalFriendly', { defaultValue: 'Super Star Store - Ipoh' })} +
+ {/* Unique Background Graphics */} +
+ + + +
+
+
+ + {/* Animated SVG Graphic Elements */} +
+ +
+
+ +
+ +
+
+
+
+
+ {shopName} • IPOH +
+
+ + 4.37 + Rating +
-
- - 4.37 + +

+ {tagline.split(' ').map((word, i) => ( + + {word} + + ))} +

+ +

+ {t('miniMarket.heroSubtitle', { defaultValue: 'Bringing you the freshest groceries, household essentials, and local delights right at your doorstep in Taman Menglembu Impiana Adril.' })} +

+ +
+ + +
+ +
+ {[ + { label: 'Open Daily', value: '8:30 AM+' }, + { label: 'Delivery', value: 'WhatsApp' }, + { label: 'Service', value: 'Local' } + ].map((stat, i) => ( +
+
{stat.label}
+
{stat.value}
+
+ ))}
-

- {t('miniMarket.heroTitle', { defaultValue: 'Your Neighbourhood Choice in Ipoh' })} -

-

- {t('miniMarket.heroSubtitle', { defaultValue: 'Bringing you the freshest groceries, household essentials, and local delights right at your doorstep in Taman Menglembu Impiana Adril.' })} -

-
- - -
-
-
- - {t('miniMarket.openDaily', { defaultValue: 'Open Daily' })} + +
+
+
+ {shopName} +
+
+
+
Featured Store
+
{shopName}
+
+
-
- - {t('miniMarket.bestPrices', { defaultValue: 'Friendly Service' })} -
-
- - {t('miniMarket.fastOrder', { defaultValue: 'Fast Order via WhatsApp' })} -
-
-
-
-
-
-
- Super Star Store
@@ -73,4 +110,4 @@ const Hero: React.FC = () => { ); }; -export default Hero; +export default Hero; \ No newline at end of file diff --git a/frontend/src/components/MiniMarket/ProductCatalogue.tsx b/frontend/src/components/MiniMarket/ProductCatalogue.tsx index a0e11fd..654890d 100644 --- a/frontend/src/components/MiniMarket/ProductCatalogue.tsx +++ b/frontend/src/components/MiniMarket/ProductCatalogue.tsx @@ -2,13 +2,18 @@ import React, { useEffect, useState } from 'react'; import axios from 'axios'; import { useTranslation } from 'react-i18next'; import BaseButton from '../BaseButton'; -import { mdiWhatsapp, mdiCartOutline } from '@mdi/js'; +import { mdiWhatsapp, mdiShoppingOutline, mdiFilterOutline } from '@mdi/js'; +import BaseIcon from '../BaseIcon'; -const ProductCatalogue: React.FC = () => { +interface ProductCatalogueProps { + shop?: any; +} + +const ProductCatalogue: React.FC = ({ shop }) => { const { t, i18n } = useTranslation(); - const [categories, setCategories] = useState([]); - const [products, setProducts] = useState([]); - const [selectedCategory, setSelectedCategory] = useState(null); + const [categories, setCategories] = useState([]); + const [products, setProducts] = useState([]); + const [selectedCategory, setSelectedCategory] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { @@ -33,118 +38,129 @@ const ProductCatalogue: React.FC = () => { ? products.filter(p => p.categoryId === selectedCategory) : products; - const getTranslation = (item, entity) => { + const getTranslation = (item: any, entity: string) => { if (!item) return ''; const translationArr = item[`${entity}_translations_${entity}`] || []; - const translation = translationArr.find(tr => tr.language?.code === i18n.language) - || translationArr.find(tr => tr.language?.code === 'en') + const translation = translationArr.find((tr: any) => tr.language?.code === i18n.language) + || translationArr.find((tr: any) => tr.language?.code === 'en') || translationArr[0]; return translation?.name || item.name || item.code || item.title || t(`miniMarket.unnamed${entity.charAt(0).toUpperCase() + entity.slice(1)}`); }; - if (loading) return
{t('miniMarket.loadingProducts', { defaultValue: 'Loading products...' })}
; + if (loading) return
{t('miniMarket.loadingCatalogue', { defaultValue: 'Syncing Catalogue...' })}
; + + const shopName = shop?.name || 'Super Star Store'; return ( -
+
-
-
-

- {t('miniMarket.ourCatalogue', { defaultValue: 'Explore Our Catalogue' })} +
+
+
+
+ +
+ Inventory +
+

+ Shop Essential Items

-

- {t('miniMarket.catalogueSubtitle', { defaultValue: 'Fresh items and daily essentials available for pick-up or WhatsApp order.' })} -

+
+ +
+
+ + Filter By +
+ + {categories.map((cat) => ( + + ))}
- {/* Categories Filter */} -
- - {categories.map((cat) => ( - - ))} -
- {/* Product Grid */} -
+
{filteredProducts.length > 0 ? ( filteredProducts.map((product) => ( -
-
+
+
{getTranslation(product, {product.compare_at_price_rm > product.price_rm && ( -
- {t('miniMarket.onSale', { defaultValue: 'SALE' })} -
- )} - {product.stock_quantity <= 0 && ( -
- - {t('miniMarket.outOfStock', { defaultValue: 'Out of Stock' })} - +
+ Offer
)} +
-
-
-
- {getTranslation(product.category, 'product_category')} + +
+
+
+ {getTranslation(product.product_categories_product, 'product_category')}
-

+

{getTranslation(product, 'product')}

-
+ +
{product.compare_at_price_rm > product.price_rm && ( - + RM {Number(product.compare_at_price_rm).toFixed(2)} )} - - RM {Number(product.price_rm).toFixed(2)} + + RM {Number(product.price_rm).toFixed(2)}
+
+ + {/* Hover Graphic Element */} +
)) ) : ( -
- {t('miniMarket.noProducts', { defaultValue: 'No products found in this category.' })} +
+
+ +
+ + {t('miniMarket.noProducts', { defaultValue: 'Empty inventory for this category.' })} +
)}
@@ -153,4 +169,4 @@ const ProductCatalogue: React.FC = () => { ); }; -export default ProductCatalogue; \ No newline at end of file +export default ProductCatalogue; diff --git a/frontend/src/components/MiniMarket/PromotionsBanner.tsx b/frontend/src/components/MiniMarket/PromotionsBanner.tsx index 00d36ff..0b87025 100644 --- a/frontend/src/components/MiniMarket/PromotionsBanner.tsx +++ b/frontend/src/components/MiniMarket/PromotionsBanner.tsx @@ -2,18 +2,23 @@ import React, { useEffect, useState } from 'react'; import axios from 'axios'; import { useTranslation } from 'react-i18next'; import BaseButton from '../BaseButton'; -import { mdiArrowRight } from '@mdi/js'; +import { mdiArrowRight, mdiFire, mdiShoppingOutline } from '@mdi/js'; +import BaseIcon from '../BaseIcon'; -const PromotionsBanner: React.FC = () => { +interface PromotionsBannerProps { + shop?: any; +} + +const PromotionsBanner: React.FC = ({ shop }) => { const { t, i18n } = useTranslation(); - const [promotions, setPromotions] = useState([]); + const [promotions, setPromotions] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { const fetchPromotions = async () => { try { const res = await axios.get('/promotions'); - setPromotions(res.data.rows.filter(p => p.status === 'active')); + setPromotions(res.data.rows.filter((p: any) => p.is_active)); } catch (err) { console.error("Failed to fetch promotions", err); } finally { @@ -23,47 +28,83 @@ const PromotionsBanner: React.FC = () => { fetchPromotions(); }, []); - const getTranslation = (item, entity) => { + const getTranslation = (item: any, entity: string) => { if (!item) return ''; - const translation = item[`${entity}_translations_${entity}`]?.find(tr => tr.language?.code === i18n.language) - || item[`${entity}_translations_${entity}`]?.find(tr => tr.language?.code === 'en') - || item[`${entity}_translations_${entity}`]?.[0]; + const translationArr = item[`${entity}_translations_${entity}`]; + if (!translationArr) return item.name || item.title || ''; + + const translation = translationArr.find((tr: any) => tr.language?.code === i18n.language) + || translationArr.find((tr: any) => tr.language?.code === 'en') + || translationArr[0]; + return translation?.name || translation?.title || item.name || item.title || t(`miniMarket.unnamed${entity.charAt(0).toUpperCase() + entity.slice(1)}`); }; if (loading || promotions.length === 0) return null; + const promo = promotions[0]; + return ( -
-
-
- +
+ {/* Unique Graphic: Rotating Text / Stripes */} +
+
+ PROMO • DEALS • OFFERS • {shop?.name?.toUpperCase() || 'SUPER STAR STORE'} • PROMO • DEALS • OFFERS +
+
+
-
-
-
- {t('miniMarket.hotDeals', { defaultValue: 'Limited Time Offer' })} +
+
+
+
+ +
+ + {t('miniMarket.exclusiveOffer', { defaultValue: 'Exclusive Member Offer' })} + +
+ +
+

+ {getTranslation(promo, 'promotion')} +

+

+ {t('miniMarket.promoSubtitle', { defaultValue: 'Grab these deals before they are gone forever!' })} +

+
+ +
+
+
+ Free Delivery +
+
+
+ Low Prices +
+
+
+ Fresh Stock +
-

- {getTranslation(promotions[0], 'promotion')} -

-

- {t('miniMarket.promoText', { defaultValue: 'Check out our latest deals and save more on your daily essentials today!' })} -

-
+ +
+ + {/* Unique Decorative Bar */} +
); }; -export default PromotionsBanner; +export default PromotionsBanner; \ No newline at end of file diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx index 0b5363b..5e54ba3 100644 --- a/frontend/src/pages/index.tsx +++ b/frontend/src/pages/index.tsx @@ -12,17 +12,27 @@ import LanguageSwitcher from '../components/LanguageSwitcher'; import { useTranslation } from 'react-i18next'; import { mdiStoreOutline, mdiMenu, mdiClose } from '@mdi/js'; import BaseIcon from '../components/BaseIcon'; +import axios from 'axios'; export default function Home() { const { t } = useTranslation(); const [isMenuOpen, setIsMenuOpen] = useState(false); const [isScrolled, setIsScrolled] = useState(false); + const [shop, setShop] = useState(null); useEffect(() => { const handleScroll = () => { setIsScrolled(window.scrollY > 20); }; window.addEventListener('scroll', handleScroll); + + // Fetch shop data + axios.get('/shops').then(res => { + if (res.data && res.data.rows && res.data.rows.length > 0) { + setShop(res.data.rows[0]); + } + }).catch(err => console.error('Error fetching shop:', err)); + return () => window.removeEventListener('scroll', handleScroll); }, []); @@ -32,27 +42,32 @@ export default function Home() { { label: t('nav.contact', { defaultValue: 'Contact Us' }), href: '#contact' }, ]; + const shopName = shop?.name || 'Super Star Store'; + return ( -
+
- {getPageTitle('Super Star Store - Ipoh')} - + {getPageTitle(`${shopName} - Ipoh`)} + {/* Header */} -
-
+
{/* Logo */} - -
- + +
+ +
+
+ + Super Star Store + + Convenience • Quality
- - Super Star Store - {/* Desktop Nav */} @@ -61,24 +76,24 @@ export default function Home() { {link.label} ))} -
+
- {t('auth.login', { defaultValue: 'Admin Login' })} + {t('auth.login', { defaultValue: 'Admin' })} {/* Mobile Toggle */} +
+ {navLinks.map((link) => ( + setIsMenuOpen(false)} + className="text-4xl font-black uppercase tracking-tighter hover:text-yellow-400 transition-colors" + > + {link.label} + + ))} +
+ + + {t('auth.login', { defaultValue: 'Admin Login' })} + +
+
)}
- - - - + + +
+
+ +
+
{/* Footer */} -