From e756fbaf854666e40089cddf93abbf9f156249f8 Mon Sep 17 00:00:00 2001 From: "s.esmaeili" Date: Wed, 16 Sep 2026 01:15:16 +0330 Subject: [PATCH] Redesign orders/cart/search flows and fix RTL/auth edge cases in user-app - Fix silent redirect-to-home for unauthenticated users on protected routes; now redirects to login with a return path - Restyle OrderCard, SearchInput, Filters, DropDown, and product card image sizing/centering to match the app's LESS-token design language - Fix ant-design Slider handle placement/drag under RTL and seed a valid default range so handles actually render - Fix chip-row edge padding and top spacing across Orders/Cart/Search lists - Add pull-to-refresh to the dashboard and fix its header/padding to match the fixed PageBar pattern - Remove the floating WhatsApp button from index.html --- apps/admin-panel/vite.config.ts | 1 + apps/api/prisma/gen-product-images.js | 59 +++++ apps/api/prisma/insert-mock-products.js | 118 +++++++++ apps/user-app/index.html | 35 --- apps/user-app/src/components/Spinner.vue | 5 +- .../src/components/home/ShowcaseCard.vue | 8 +- .../src/components/product/CartItem.vue | 20 +- .../src/components/product/OrderCard.vue | 114 ++++---- .../src/components/product/ProductCard.vue | 8 +- .../src/components/search/Filters.vue | 246 +++++++++++++++--- .../src/components/search/SearchInput.vue | 82 +++++- apps/user-app/src/components/ui/DropDown.vue | 74 +++++- .../user-app/src/components/ui/Navigation.vue | 232 +++++++---------- apps/user-app/src/components/ui/PageBar.vue | 4 +- apps/user-app/src/router/index.ts | 3 +- apps/user-app/src/stores/navStore.ts | 19 +- apps/user-app/src/views/CartView.vue | 21 +- apps/user-app/src/views/HomeView.vue | 52 ++-- apps/user-app/src/views/NewestView.vue | 2 +- .../src/views/search/CollectionView.vue | 2 +- apps/user-app/src/views/search/SearchView.vue | 39 ++- .../user-app/src/views/user/DashboardView.vue | 21 +- .../user-app/src/views/user/FavoritesView.vue | 2 +- apps/user-app/src/views/user/LoginView.vue | 8 +- .../src/views/user/NotificationsView.vue | 51 ++++ apps/user-app/src/views/user/OrdersView.vue | 19 +- apps/user-app/src/views/user/ProfileView.vue | 2 +- apps/user-app/src/views/user/RegisterView.vue | 2 +- .../src/views/user/TransactionsView.vue | 2 +- apps/user-app/vite.config.ts | 4 +- 30 files changed, 890 insertions(+), 365 deletions(-) create mode 100644 apps/api/prisma/gen-product-images.js create mode 100644 apps/api/prisma/insert-mock-products.js create mode 100644 apps/user-app/src/views/user/NotificationsView.vue diff --git a/apps/admin-panel/vite.config.ts b/apps/admin-panel/vite.config.ts index 6e156b0..1d603ef 100644 --- a/apps/admin-panel/vite.config.ts +++ b/apps/admin-panel/vite.config.ts @@ -42,6 +42,7 @@ export default defineConfig(({ mode }) => { sourcemap: false, // legacy: productionSourceMap: false }, server: { + host: '0.0.0.0', port: 5174, proxy: { '/api': 'http://localhost:3000', diff --git a/apps/api/prisma/gen-product-images.js b/apps/api/prisma/gen-product-images.js new file mode 100644 index 0000000..d8181c3 --- /dev/null +++ b/apps/api/prisma/gen-product-images.js @@ -0,0 +1,59 @@ +// Generates distinct SVG placeholder images per product category, so the 200 +// mock products render varied artwork instead of one shared no-items.png icon. +// Not photography — an honest stand-in, styled to the app's own gold/cream +// palette, with a distinct glyph + background tint per category so the grid +// reads as varied rather than one repeated tile. +const fs = require('fs'); +const path = require('path'); + +const OUT_DIR = path.join(__dirname, '../../user-app/public/img/products'); +fs.mkdirSync(OUT_DIR, { recursive: true }); + +// category slug -> [glyph path (viewBox 0 0 64 64), accent color] +const GLYPHS = { + rings: { + accent: '#B08D57', + path: '', + }, + bracelets: { + accent: '#8D6A46', + path: '', + }, + necklaces: { + accent: '#C9A24B', + path: '', + }, + earrings: { + accent: '#A67C52', + path: '', + }, + coins: { + accent: '#D4AF37', + path: 'Au', + }, +}; + +// A handful of warm background tints so 200 tiles don't look identical even +// within one category. +const BG_TINTS = ['#F6F3EF', '#F3EADC', '#FAF5EC', '#F0E6D6', '#F8F1E6', '#EFE4D3']; + +function svgFor(categorySlug, variantIndex) { + const g = GLYPHS[categorySlug] || GLYPHS.rings; + const bg = BG_TINTS[variantIndex % BG_TINTS.length]; + return ` + + + ${g.path} + +`; +} + +let written = 0; +for (const slug of Object.keys(GLYPHS)) { + for (let i = 0; i < 6; i++) { + const file = path.join(OUT_DIR, `${slug}-${i}.svg`); + fs.writeFileSync(file, svgFor(slug, i)); + written++; + } +} +console.log(`wrote ${written} placeholder SVGs to ${OUT_DIR}`); diff --git a/apps/api/prisma/insert-mock-products.js b/apps/api/prisma/insert-mock-products.js new file mode 100644 index 0000000..5e250ab --- /dev/null +++ b/apps/api/prisma/insert-mock-products.js @@ -0,0 +1,118 @@ +const fs = require('fs'); +const path = require('path'); + +const seedPath = path.join(__dirname, 'seed.ts'); +let content = fs.readFileSync(seedPath, 'utf-8'); + +const marker = ' imageCount += 1;\n }\n\n // \u2500\u2500 Featured collection'; +if (!content.includes(marker)) { + console.error('MARKER NOT FOUND'); + process.exit(1); +} + +const insertion = ` imageCount += 1; + } + + // \u2500\u2500 200 mock products for a realistic-looking catalogue \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 + // These are generated, not hand-authored like the 5 above (whose ids are + // referenced elsewhere in this seed, e.g. by orders \u2014 left untouched). + // Images: real product photography does not exist in this project, so each + // mock product cycles through 6 generated SVG placeholders per category + // (apps/user-app/public/img/products/-N.svg), giving the catalogue + // grid genuine visual variety instead of one repeated icon. + const MOCK_CATEGORIES = [ + { slug: 'rings', id: ringsCategory.id, nouns: ['انگشتر نگین‌دار', 'انگشتر سولیتر', 'انگشتر آویزدار', 'انگشتر پهن', 'انگشتر ظریف', 'انگشتر رینگی'] }, + { slug: 'bracelets', id: braceletsCategory.id, nouns: ['دستبند زنجیری', 'دستبند پاندول', 'دستبند بافت', 'دستبند تنیس', 'دستبند پلاک‌دار', 'دستبند چرمی طلاکوب'] }, + { slug: 'necklaces', id: necklacesCategory.id, nouns: ['گردنبند آویز قلب', 'گردنبند ماه و ستاره', 'گردنبند ظریف', 'گردنبند رولو', 'گردنبند صلیب', 'گردنبند اسم'] }, + { slug: 'earrings', id: earringsCategory.id, nouns: ['گوشواره حلقه‌ای', 'گوشواره آویز', 'گوشواره میخی', 'گوشواره پروانه‌ای', 'گوشواره مرواریدی', 'گوشواره هندسی'] }, + { slug: 'coins', id: coinsCategory.id, nouns: ['ربع سکه بهار آزادی', 'نیم سکه بهار آزادی', 'سکه امامی', 'سکه پارسیان', 'شمش طلای ۱۰ گرمی', 'شمش طلای ۵ گرمی'] }, + ]; + const MOCK_ADJECTIVES = ['کلاسیک', 'مدرن', 'مینیمال', 'لوکس', 'دست‌ساز', 'ظریف', 'خاص', 'شیک', 'سنتی', 'رزگلد']; + const MOCK_BRANDS = [brandHenza.id, brandArtisan.id]; + + const MOCK_PRODUCT_COUNT = 200; + let mockImageCount = 0; + for (let i = 0; i < MOCK_PRODUCT_COUNT; i++) { + const cat = MOCK_CATEGORIES[i % MOCK_CATEGORIES.length]; + const noun = cat.nouns[i % cat.nouns.length]; + const adjective = MOCK_ADJECTIVES[i % MOCK_ADJECTIVES.length]; + const brandId = MOCK_BRANDS[i % MOCK_BRANDS.length]; + const productId = \`mock-product-\${i + 1}\`; + const title = \`\${noun} \${adjective} \${i + 1}\`; + // Deterministic but varied numeric fields \u2014 no randomness, so a re-seed + // is stable and diffable. + const baseWeight = 1.5 + ((i * 37) % 250) / 20; // ~1.5g .. ~14g + const wagePercent = (6 + ((i * 13) % 12)).toFixed(1); // 6.0% .. 17.9% + const qty = (i * 7) % 15; // 0..14, so some are genuinely out of stock + const isPreOrder = i % 9 === 0; + + const product = await prisma.product.upsert({ + where: { id: productId }, + update: {}, + create: { + id: productId, + title, + description: \`\${noun} با طراحی \${adjective}، مناسب استفاده روزمره یا هدیه.\`, + categoryId: cat.id, + brandId, + productCode: \`P-\${String(i + 1).padStart(4, '0')}\`, + }, + }); + + const variantId = \`\${productId}-variant-1\`; + await prisma.productVariant.upsert({ + where: { id: variantId }, + update: {}, + create: { + id: variantId, + productId: product.id, + title: 'استاندارد', + sku: \`MOCK-\${String(i + 1).padStart(4, '0')}\`, + rashWeightGrams: baseWeight.toFixed(3), + defaultWagePercent: wagePercent, + qty, + isPreOrder, + }, + }); + + // A second variant on ~1 in 4 products, so the "\u062a\u0646\u0648\u0639 \u0648\u0632\u0646\u06cc" chip row has + // something real to show on more than the 5 hand-authored products. + if (i % 4 === 0) { + const variant2Id = \`\${productId}-variant-2\`; + await prisma.productVariant.upsert({ + where: { id: variant2Id }, + update: {}, + create: { + id: variant2Id, + productId: product.id, + title: 'سایز بزرگ', + sku: \`MOCK-\${String(i + 1).padStart(4, '0')}-L\`, + rashWeightGrams: (baseWeight * 1.35).toFixed(3), + defaultWagePercent: wagePercent, + qty: (qty + 3) % 15, + isPreOrder, + }, + }); + } + + const imgIndex = i % 6; + const imageUrl = \`/img/products/\${cat.slug}-\${imgIndex}.svg\`; + await prisma.productImage.upsert({ + where: { id: \`\${productId}-img-1\` }, + update: { url: imageUrl }, + create: { + id: \`\${productId}-img-1\`, + productId: product.id, + url: imageUrl, + sortOrder: 0, + }, + }); + mockImageCount += 1; + } + imageCount += mockImageCount; + + // \u2500\u2500 Featured collection`; + +content = content.replace(marker, insertion); +fs.writeFileSync(seedPath, content, 'utf-8'); +console.log('inserted mock product generator into seed.ts'); diff --git a/apps/user-app/index.html b/apps/user-app/index.html index def503c..2e6cf22 100644 --- a/apps/user-app/index.html +++ b/apps/user-app/index.html @@ -9,45 +9,10 @@ طلای هنزا -
- - پشتیبانی واتساپ - diff --git a/apps/user-app/src/components/Spinner.vue b/apps/user-app/src/components/Spinner.vue index d33caec..b9dd57d 100644 --- a/apps/user-app/src/components/Spinner.vue +++ b/apps/user-app/src/components/Spinner.vue @@ -1,6 +1,6 @@