diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..8c40783 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,26 @@ +node_modules +npm-debug.log* +.npm +.env +.env.local +.env.development.local +.env.test.local +.env.production.local +.git +.gitignore +README.md +.eslintcache +.nyc_output +coverage +.DS_Store +*.log +build +dist +.cache +.vscode +.idea +*.swp +*.swo +.dockerignore +Dockerfile* +docker-compose*.yml \ No newline at end of file diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..4010b1c --- /dev/null +++ b/.env.development @@ -0,0 +1,3 @@ +VITE_API_BASE_URL=https://api.vitrown.abrino.cloud +VITE_API_SOCKET_BASE_URL=wss://api.vitrown.abrino.cloud +VITE_SUPPORT_PHONE=+989223740993 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..7105031 --- /dev/null +++ b/.env.example @@ -0,0 +1,23 @@ +# API Configuration +# K8s Dev: api.vitrown.abrino.cloud | Production: api.vitrown.com +VITE_API_BASE_URL=https://api.vitrown.abrino.cloud +VITE_API_SOCKET_BASE_URL=wss://api.vitrown.abrino.cloud +VITE_API_URL=https://api.vitrown.abrino.cloud + +# Application Environment +VITE_APP_VERSION_TYPE=development +NODE_ENV=development + +# Site Configuration +# K8s Dev: vitrown.abrino.cloud | Production: vitrown.com +VITE_SITE_URL=https://vitrown.abrino.cloud +VITE_DOMAIN_URL=https://vitrown.abrino.cloud + +# Session Secret (REQUIRED - Change in production!) +REMIX_SECRET=your-secret-key-here + +# Optional Build Configuration +VITE_BUILD_TARGET=development + +# Support Configuration +VITE_SUPPORT_PHONE=+989223740993 diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..83f2bcf --- /dev/null +++ b/.env.production @@ -0,0 +1,21 @@ +# Production Environment Configuration +NODE_ENV=production + +# API Configuration +VITE_API_BASE_URL=https://api.prod.vitrown.com +VITE_API_SOCKET_BASE_URL=wss://api.prod.vitrown.com +VITE_API_URL=https://api.prod.vitrown.com + +# Application Environment +VITE_APP_VERSION_TYPE=production + +# Site Configuration +VITE_SITE_URL=https://vitrown.com +VITE_DOMAIN_URL=https://vitrown.com + +# Session Secret (REQUIRED - Set via secrets in CI/CD) +REMIX_SECRET=change-me-in-production + +# Build Configuration +VITE_BUILD_TARGET=production +VITE_SUPPORT_PHONE=+989223740993 \ No newline at end of file diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 0000000..a804be1 --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,81 @@ +/** @type {import('eslint').Linter.Config} */ +module.exports = { + root: true, + parserOptions: { + ecmaVersion: "latest", + sourceType: "module", + ecmaFeatures: { + jsx: true, + }, + }, + env: { + browser: true, + commonjs: true, + es6: true, + }, + ignorePatterns: ["!**/.server", "!**/.client"], + + // Base config + extends: ["eslint:recommended"], + + overrides: [ + // React + { + files: ["**/*.{js,jsx,ts,tsx}"], + plugins: ["react", "jsx-a11y"], + extends: [ + "plugin:react/recommended", + "plugin:react/jsx-runtime", + "plugin:react-hooks/recommended", + "plugin:jsx-a11y/recommended", + ], + settings: { + react: { + version: "detect", + }, + formComponents: ["Form"], + linkComponents: [ + { name: "Link", linkAttribute: "to" }, + { name: "NavLink", linkAttribute: "to" }, + ], + "import/resolver": { + typescript: {}, + }, + }, + }, + + // Typescript + { + files: ["**/*.{ts,tsx}"], + plugins: ["@typescript-eslint", "import"], + parser: "@typescript-eslint/parser", + settings: { + "import/internal-regex": "^~/", + "import/resolver": { + node: { + extensions: [".ts", ".tsx"], + }, + typescript: { + alwaysTryTypes: true, + }, + }, + }, + extends: [ + "plugin:@typescript-eslint/recommended", + "plugin:import/recommended", + "plugin:import/typescript", + ], + rules: { + "@typescript-eslint/no-unused-vars": "warn", // Change from error to warning + }, + }, + + // Node + { + files: [".eslintrc.cjs"], + env: { + node: true, + }, + }, + ], +}; diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a452068 --- /dev/null +++ b/.gitignore @@ -0,0 +1,111 @@ +# Dependencies +node_modules/ +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# Build outputs +/build +/dist +/.next/ +/out/ + +# Environment files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# IDE and editor files +.vscode/ +.idea/ +.cursor/ +.claude/ +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* +lerna-debug.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +/coverage +*.lcov + +# Dependency directories +jspm_packages/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Temporary folders +tmp/ +temp/ + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Vercel +.vercel + +# Turbo +.turbo + +# Local Netlify folder +.netlify diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 0000000..29809ca --- /dev/null +++ b/DEPLOYMENT.md @@ -0,0 +1,190 @@ +# Production Deployment Guide + +## Overview + +This project uses a **Blue-Green deployment** strategy with **automatic rollback** capabilities to ensure zero-downtime deployments and quick recovery from failures. + +## Architecture + +### Blue-Green Deployment +- **Blue Service**: Currently serving production traffic +- **Green Service**: New deployment being tested +- **Traffic Switch**: Instant switchover via Traefik labels +- **Automatic Rollback**: Health check failures trigger immediate rollback + +### Key Features +- ✅ **Zero-downtime deployments** +- ✅ **Automatic health checks** +- ✅ **Instant rollback on failure** +- ✅ **Backup system with retention** +- ✅ **Build verification before traffic switch** +- ✅ **Resource limits and security** + +## Deployment Process + +### Automatic Deployment (Recommended) + +1. **Create Release Tag**: + ```bash + git tag 1.0.1-release + git push origin 1.0.1-release + ``` + +2. **Monitor Progress**: + - GitHub Actions: `https://github.com/sotoontech/Vitron-Front/actions` + - Server logs: `ssh root@prod-server 'docker-compose -f /opt/front-prod/Vitron-Front/docker-compose.yml logs -f'` + +### Deployment Steps (Automatic) + +1. **Backup**: Current deployment tagged as backup +2. **Build**: New version built in parallel (green/blue) +3. **Health Check**: 10 attempts over 100 seconds +4. **Traffic Switch**: Instant switchover via Traefik +5. **Verification**: Final check on live domain +6. **Cleanup**: Old deployment stopped and cleaned + +## Manual Operations + +### Emergency Rollback + +```bash +# SSH to production server +ssh root@prod-server + +# Go to project directory +cd /opt/front-prod/Vitron-Front + +# Rollback to latest backup +./scripts/rollback.sh + +# Or rollback to specific backup +./scripts/rollback.sh vitron-frontend-backup:20240906_150230 +``` + +### Check Status + +```bash +# Check running containers +docker ps | grep vitron + +# Check available backups +docker images | grep vitron-frontend-backup + +# Check service health +docker exec vitron-frontend-blue wget --spider http://localhost:3000/ +curl -I https://vitrown.com/ + +# Check logs +docker-compose logs -f vitron-frontend-blue +docker-compose logs -f vitron-frontend-green +``` + +### Manual Deployment + +```bash +# SSH to server +ssh root@prod-server +cd /opt/front-prod/Vitron-Front + +# Pull latest changes +git fetch --all +git checkout main +git pull origin main + +# Deploy manually +docker-compose -f docker-compose.yml up -d --build +``` + +## Environment Configuration + +### Required GitHub Secrets +- `PROD_SSH_HOST`: Production server IP/hostname +- `PROD_SSH_KEY`: SSH private key for server access +- `PROD_REMIX_SECRET`: Strong random string for session encryption + +### Environment Variables +```bash +NODE_ENV=production +VITE_API_BASE_URL=https://api.prod.vitrown.com +VITE_API_SOCKET_BASE_URL=wss://api.prod.vitrown.com +VITE_API_URL=https://api.prod.vitrown.com +VITE_SITE_URL=https://vitrown.com +VITE_DOMAIN_URL=https://vitrown.com +VITE_APP_VERSION_TYPE=production +REMIX_SECRET=[FROM_GITHUB_SECRETS] +``` + +## Troubleshooting + +### Common Issues + +1. **Build Failed** + ```bash + # Check build logs + docker-compose logs vitron-frontend-[color] + + # Manual build test + docker-compose build vitron-frontend-blue + ``` + +2. **Health Check Failed** + ```bash + # Check container status + docker exec vitron-frontend-[color] ps aux + + # Check internal health + docker exec vitron-frontend-[color] wget --spider http://localhost:3000/ + + # Check logs + docker exec vitron-frontend-[color] tail -f /var/log/nginx/error.log + ``` + +3. **Rollback Issues** + ```bash + # List available backups + docker images | grep backup + + # Manual container start + docker run -d --name manual-rollback vitron-frontend-backup:20240906_150230 + ``` + +### Health Check Endpoints +- Internal: `http://localhost:3000/` (container health) +- External: `https://vitrown.com/` (public endpoint) +- API: `https://api.prod.vitrown.com/api/health/` (backend health) + +## Security Features + +- **Non-root user**: Containers run as `remix` user (UID 1001) +- **Security headers**: X-Frame-Options, X-Content-Type-Options, etc. +- **HTTPS enforcement**: Automatic redirect from HTTP +- **Resource limits**: Memory limits prevent resource exhaustion +- **Network isolation**: Services run in isolated Docker network + +## Monitoring + +### Key Metrics to Monitor +- Container health status +- Response time on health checks +- Memory usage (limit: 512MB) +- Disk space (Docker images) +- SSL certificate expiry + +### Log Locations +- Application logs: `docker-compose logs` +- Traefik logs: Check proxy network logs +- System logs: `/var/log/syslog` on server + +## Backup Strategy + +- **Automatic**: Every deployment creates timestamped backup +- **Retention**: Last 3 backups kept automatically +- **Manual backup**: `docker tag vitron-frontend-blue vitron-frontend-backup:manual-$(date +%Y%m%d_%H%M%S)` + +## Performance Optimizations + +- **Multi-stage build**: Smaller production images +- **Asset optimization**: Terser minification, tree shaking +- **Caching**: Docker layer caching, npm ci +- **Resource limits**: Prevents memory leaks +- **Health checks**: Quick failure detection \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ded1151 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,53 @@ +# Multi-stage build for production +FROM node:20-alpine AS builder + +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install ALL dependencies (including dev) for build +RUN npm ci && npm cache clean --force + +# Copy source code +COPY . . + +# Clear any existing build artifacts and build fresh +RUN rm -rf build node_modules/.vite node_modules/.cache + +# Build the application +RUN NODE_ENV=production npm run build + +# Production stage +FROM node:20-alpine AS production + +WORKDIR /app + +# Install only production dependencies +COPY package*.json ./ +RUN npm ci --omit=dev && npm cache clean --force + +# Copy built application from builder stage +COPY --from=builder /app/build ./build +COPY --from=builder /app/public ./public + +# Create non-root user for security +RUN addgroup -g 1001 -S nodejs && \ + adduser -S remix -u 1001 + +# Change ownership of app directory +RUN chown -R remix:nodejs /app +USER remix + +# Set production environment +ENV NODE_ENV=production + +# Expose port +EXPOSE 3000 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:3000/ || exit 1 + +# Start production server +CMD ["npm", "start"] \ No newline at end of file diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 0000000..69bedaa --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,20 @@ +# Development Dockerfile +FROM node:20-alpine + +WORKDIR /app + +# Install dependencies +COPY package*.json ./ +RUN npm ci + +# Copy source code +COPY . . + +# Set development environment +ENV NODE_ENV=development + +# Expose port +EXPOSE 5173 + +# Start development server +CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"] \ No newline at end of file diff --git a/app/assets/data/consts.tsx b/app/assets/data/consts.tsx new file mode 100644 index 0000000..5c2ad7b --- /dev/null +++ b/app/assets/data/consts.tsx @@ -0,0 +1,390 @@ +export const years = [ + ...Array(104) + .fill(1) + .map((_, number) => { + return { + value: 1404 - number, + label: JSON.stringify(1404 - number), + }; + }), +]; +export const days = [ + ...Array(31) + .fill(1) + .map((_, number) => { + return { + value: number + 1, + label: JSON.stringify(number + 1), + }; + }), +]; +export const months = [ + { + value: 1, + label: "فروردین", + }, + { + value: 2, + label: "اردیبهشت", + }, + { + value: 3, + label: "خرداد", + }, + { + value: 4, + label: "تیر", + }, + { + value: 5, + label: "مرداد", + }, + { + value: 6, + label: "شهریور", + }, + { + value: 7, + label: "مهر", + }, + { + value: 8, + label: "آبان", + }, + { + value: 9, + label: "آذر", + }, + { + value: 10, + label: "دی", + }, + { + value: 11, + label: "بهمن", + }, + { + value: 12, + label: "اسفند", + }, +]; +export const SearchSuggestionsData = [ + // Products - Apparel - Men + { type: "product", title: "پیراهن مردانه آستین بلند" }, + { type: "product", title: "پیراهن مردانه آستین کوتاه" }, + { type: "product", title: "پیراهن مردانه رسمی" }, + { type: "product", title: "پیراهن مردانه اسپرت" }, + { type: "product", title: "شلوار مردانه جین" }, + { type: "product", title: "شلوار مردانه کتان" }, + { type: "product", title: "شلوار مردانه پارچه ای" }, + { type: "product", title: "شلوارک مردانه" }, + { type: "product", title: "کت تک مردانه" }, + { type: "product", title: "کت و شلوار مردانه دامادی" }, + { type: "product", title: "جلیقه مردانه" }, + { type: "product", title: "تیشرت مردانه یقه گرد" }, + { type: "product", title: "تیشرت مردانه یقه هفت" }, + { type: "product", title: "پولوشرت مردانه" }, + { type: "product", title: "هودی مردانه" }, + { type: "product", title: "سویشرت مردانه" }, + { type: "product", title: "زیرپوش مردانه" }, + { type: "product", title: "لباس خواب مردانه" }, + { type: "product", title: "جوراب کالج مردانه" }, + { type: "product", title: "جوراب ساق بلند مردانه" }, + { type: "product", title: "ست لباس ورزشی مردانه" }, + { type: "product", title: "شلوار ورزشی مردانه" }, + { type: "product", title: "کاپشن چرم مردانه" }, + { type: "product", title: "کاپشن زمستانه مردانه" }, + { type: "product", title: "بارانی مردانه" }, + { type: "product", title: "پالتو فوتر مردانه" }, + { type: "product", title: "ژاکت بافتنی مردانه" }, + { type: "product", title: "پلیور یقه اسکی مردانه" }, + + // Products - Apparel - Women + { type: "product", title: "مانتو مجلسی" }, + { type: "product", title: "مانتو اسپرت" }, + { type: "product", title: "مانتو اداری" }, + { type: "product", title: "مانتو کتی" }, + { type: "product", title: "شومیز حریر" }, + { type: "product", title: "بلوز زنانه" }, + { type: "product", title: "تونیک زنانه" }, + { type: "product", title: "سارافون زنانه" }, + { type: "product", title: "شلوار جین زنانه" }, + { type: "product", title: "شلوار پارچه ای زنانه" }, + { type: "product", title: "لگینگ زنانه" }, + { type: "product", title: "دامن کوتاه" }, + { type: "product", title: "دامن بلند" }, + { type: "product", title: "دامن مجلسی" }, + { type: "product", title: "پیراهن مجلسی زنانه" }, + { type: "product", title: "پیراهن ساحلی" }, + { type: "product", title: "لباس شب زنانه" }, + { type: "product", title: "سوتین" }, + { type: "product", title: "شورت زنانه" }, + { type: "product", title: "لباس خواب زنانه" }, + { type: "product", title: "گن زنانه" }, + { type: "product", title: "جوراب شلواری" }, + { type: "product", title: "جوراب نازک زنانه" }, + { type: "product", title: "ست لباس ورزشی زنانه" }, + { type: "product", title: "تاپ ورزشی زنانه" }, + { type: "product", title: "شلوارک ورزشی زنانه" }, + { type: "product", title: "کاپشن چرم زنانه" }, + { type: "product", title: "کاپشن زمستانه زنانه" }, + { type: "product", title: "بارانی زنانه" }, + { type: "product", title: "پالتو فوتر زنانه" }, + { type: "product", title: "پانچو" }, + { type: "product", title: "وست زنانه" }, + { type: "product", title: "روپوش پزشکی" }, + { type: "product", title: "لباس فرم اداری زنانه" }, + + // Products - Apparel - Children & Baby + { type: "product", title: "لباس نوزادی دخترانه" }, + { type: "product", title: "لباس نوزادی پسرانه" }, + { type: "product", title: "بادی نوزاد" }, + { type: "product", title: "رامپر نوزاد" }, + { type: "product", title: "ست بیمارستانی نوزاد" }, + { type: "product", title: "پیشبند نوزاد" }, + { type: "product", title: "کلاه نوزادی" }, + { type: "product", title: "تیشرت بچگانه" }, + { type: "product", title: "شلوار بچگانه" }, + { type: "product", title: "پیراهن دخترانه" }, + { type: "product", title: "سارافون دخترانه" }, + { type: "product", title: "لباس مجلسی دخترانه" }, + { type: "product", title: "کاپشن بچگانه" }, + { type: "product", title: "لباس شنا بچگانه" }, + + // Products - General Apparel + { type: "product", title: "کلاه کپ" }, + { type: "product", title: "کلاه بافتنی" }, + { type: "product", title: "شال گردن پشمی" }, + { type: "product", title: "شال گردن نخی" }, + { type: "product", title: "دستکش چرمی" }, + { type: "product", title: "دستکش بافتنی" }, + { type: "product", title: "لباس محلی" }, + { type: "product", title: "لباس سنتی" }, + + // Products - Shoes - Men + { type: "product", title: "کفش چرم مردانه" }, + { type: "product", title: "کالج مردانه" }, + { type: "product", title: "کفش ورزشی مردانه (رانینگ، فوتبال، بسکتبال)" }, + { type: "product", title: "نیم بوت مردانه" }, + { type: "product", title: "صندل طبیعت گردی مردانه" }, + { type: "product", title: "دمپایی مردانه" }, + + // Products - Shoes - Women + { type: "product", title: "کفش چرم زنانه" }, + { type: "product", title: "کالج زنانه" }, + { type: "product", title: "کفش تخت زنانه" }, + { type: "product", title: "کفش لژدار زنانه" }, + { type: "product", title: "کفش ورزشی زنانه (فیتنس، یوگا)" }, + { type: "product", title: "نیم بوت زنانه" }, + { type: "product", title: "صندل مجلسی زنانه" }, + { type: "product", title: "دمپایی زنانه" }, + { type: "product", title: "پاپوش زنانه" }, + + // Products - Shoes - Children + { type: "product", title: "کفش نوزادی" }, + { type: "product", title: "کتانی بچگانه" }, + { type: "product", title: "صندل بچگانه" }, + { type: "product", title: "بوت بچگانه" }, + + // Products - Bags - Women + { type: "product", title: "کیف مجلسی زنانه" }, + { type: "product", title: "کیف کتابی زنانه (کلاچ)" }, + { type: "product", title: "کیف کمری زنانه" }, + { type: "product", title: "ساک ورزشی زنانه" }, + + // Products - Bags - Men + { type: "product", title: "کیف سامسونت مردانه" }, + { type: "product", title: "کیف کمری مردانه" }, + { type: "product", title: "ساک ورزشی مردانه" }, + { type: "product", title: "کیف لپتاپ" }, + + // Products - Bags - General + { type: "product", title: "کوله پشتی مدرسه" }, + { type: "product", title: "کوله پشتی کوهنوردی" }, + { type: "product", title: "ساک خرید" }, + + // Products - Accessories - General + { type: "product", title: "عینک طبی" }, + { type: "product", title: "بند عینک" }, + { type: "product", title: "دکمه سردست" }, + { type: "product", title: "سنجاق سینه" }, + { type: "product", title: "سنجاق کراوات" }, + { type: "product", title: "گیره مو" }, + { type: "product", title: "تل مو" }, + { type: "product", title: "تاج عروس" }, + { type: "product", title: "پاپوش" }, + { type: "product", title: "ساسبند (بند شلوار)" }, + + // Shops - General Clothing & Department Stores + { + type: "shop", + id: "1", + title: "فروشگاه پوشاک خانواده", + image: + "https://images.unsplash.com/photo-1441986300917-64674bd600d8?w=500&auto=format&fit=crop", + }, + { + type: "shop", + id: "2", + title: "فروشگاه زنجیره ای پوشاک", + image: + "https://images.unsplash.com/photo-1483985988355-763728e1935b?w=500&auto=format&fit=crop", + }, + { + type: "shop", + id: "3", + title: "مرکز خرید پوشاک", + image: + "https://images.unsplash.com/photo-1555529669-e6910eb125b6?w=500&auto=format&fit=crop", + }, + { + type: "shop", + id: "4", + title: "پاساژ لباس", + image: + "https://images.unsplash.com/photo-1591085686350-798c0f9faa7f?w=500&auto=format&fit=crop", + }, + { + type: "shop", + id: "5", + title: "اوت لت پوشاک", + image: + "https://images.unsplash.com/photo-1560243563-062bfc001d68?w=500&auto=format&fit=crop", + }, + + // Shops - Specific Apparel Types + { + type: "shop", + id: "6", + title: "فروشگاه لباس زیر", + image: + "https://images.unsplash.com/photo-1603217041454-99e679540093?w=500&auto=format&fit=crop", + }, + { + type: "shop", + id: "7", + title: "فروشگاه لباس خواب", + image: + "https://images.unsplash.com/photo-1585519105003-c89177b34699?w=500&auto=format&fit=crop", + }, + { + type: "shop", + id: "8", + title: "فروشگاه جوراب", + image: + "https://images.unsplash.com/photo-1613690150715-070f407fff94?w=500&auto=format&fit=crop", + }, + { + type: "shop", + id: "9", + title: "فروشگاه تخصصی مانتو", + image: + "https://images.unsplash.com/photo-1595965781570-496f87b1d3c0?w=500&auto=format&fit=crop", + }, + { + type: "shop", + title: "فروشگاه کت و شلوار", + image: + "https://images.unsplash.com/photo-1509319117193-57bab727e09d?w=500&auto=format&fit=crop", + }, + { + type: "shop", + id: "10", + title: "فروشگاه لباس کودک و نوزاد", + image: + "https://images.unsplash.com/photo-1520923642038-b42e596efc03?w=500&auto=format&fit=crop", + }, + { + type: "shop", + id: "11", + title: "فروشگاه لباس بارداری", + image: + "https://images.unsplash.com/photo-1581027912560-1a0db3e9983f?w=500&auto=format&fit=crop", + }, + { + type: "shop", + id: "12", + title: "فروشگاه لباس سایز بزرگ", + image: + "https://images.unsplash.com/photo-1607008028064-325720103504?w=500&auto=format&fit=crop", + }, + { + type: "shop", + id: "13", + title: "فروشگاه لباس فرم و کار", + image: + "https://images.unsplash.com/photo-1529338296731-c1290a08ccc4?w=500&auto=format&fit=crop", + }, + + // Shops - Accessories & More + { + type: "shop", + id: "14", + title: "فروشگاه زیورآلات و بدلیجات", + image: + "https://images.unsplash.com/photo-1610041321327-07ac523e5804?w=500&auto=format&fit=crop", + }, + { + type: "shop", + id: "15", + title: "گالری شال و روسری", + image: + "https://images.unsplash.com/photo-1563059269-3a38d1ae50a1?w=500&auto=format&fit=crop", + }, + { + type: "shop", + id: "16", + title: "فروشگاه تخصصی کلاه", + image: + "https://images.unsplash.com/photo-1534215754734-18e1d14349ae?w=500&auto=format&fit=crop", + }, + { + type: "shop", + id: "17", + title: "فروشگاه چمدان و کیف سفر", + image: + "https://images.unsplash.com/photo-1581403341094-189d3f0f8f15?w=500&auto=format&fit=crop", + }, + { + type: "shop", + id: "18", + title: "خیاطی و مزون شخصی دوزی", + image: + "https://images.unsplash.com/photo-1579084806155-11518b23ac96?w=500&auto=format&fit=crop", + }, + { + type: "shop", + id: "19", + title: "خشکشویی و خدمات لباس", + image: + "https://images.unsplash.com/photo-1580828343791-9fe69e70f611?w=500&auto=format&fit=crop", + }, + { + type: "shop", + id: "20", + title: "فروشگاه پارچه", + image: + "https://images.unsplash.com/photo-1549061528-434734449753?w=500&auto=format&fit=crop", + }, + { + type: "shop", + id: "21", + title: "نمایندگی برندهای معروف پوشاک (مثال: زارا، ال سی وایکیکی)", + image: + "https://images.unsplash.com/photo-1551488831-00ddcb6ac6bd?w=500&auto=format&fit=crop", + }, + { + type: "shop", + id: "22", + title: "فروشگاه آنلاین مد و پوشاک", + image: + "https://images.unsplash.com/photo-1462392246756-320a5667a952?w=500&auto=format&fit=crop", + }, + { + type: "shop", + id: "23", + title: "بازار سنتی پوشاک", + image: + "https://images.unsplash.com/photo-1542359649-31e03cdde49b?w=500&auto=format&fit=crop", + }, +]; diff --git a/app/assets/icons/cart/basket.svg b/app/assets/icons/cart/basket.svg new file mode 100644 index 0000000..12814a7 --- /dev/null +++ b/app/assets/icons/cart/basket.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/assets/icons/cart/failure_pay.svg b/app/assets/icons/cart/failure_pay.svg new file mode 100644 index 0000000..ec9751f --- /dev/null +++ b/app/assets/icons/cart/failure_pay.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/app/assets/icons/cart/sucess_pay.svg b/app/assets/icons/cart/sucess_pay.svg new file mode 100644 index 0000000..59a85ad --- /dev/null +++ b/app/assets/icons/cart/sucess_pay.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/icons/filter-dark.png b/app/assets/icons/filter-dark.png new file mode 100644 index 0000000..cbf38f2 Binary files /dev/null and b/app/assets/icons/filter-dark.png differ diff --git a/app/assets/icons/filter.png b/app/assets/icons/filter.png new file mode 100644 index 0000000..7d85446 Binary files /dev/null and b/app/assets/icons/filter.png differ diff --git a/app/assets/icons/login/verify-banner.svg b/app/assets/icons/login/verify-banner.svg new file mode 100644 index 0000000..81896e1 --- /dev/null +++ b/app/assets/icons/login/verify-banner.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/assets/icons/navbar/buy-filled.svg b/app/assets/icons/navbar/buy-filled.svg new file mode 100644 index 0000000..e306ac3 --- /dev/null +++ b/app/assets/icons/navbar/buy-filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/icons/navbar/buy-outline.svg b/app/assets/icons/navbar/buy-outline.svg new file mode 100644 index 0000000..c9a1b68 --- /dev/null +++ b/app/assets/icons/navbar/buy-outline.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/icons/navbar/document-filled.svg b/app/assets/icons/navbar/document-filled.svg new file mode 100644 index 0000000..37bca70 --- /dev/null +++ b/app/assets/icons/navbar/document-filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/icons/navbar/document-outline.svg b/app/assets/icons/navbar/document-outline.svg new file mode 100644 index 0000000..0d78584 --- /dev/null +++ b/app/assets/icons/navbar/document-outline.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/icons/navbar/home-filled.svg b/app/assets/icons/navbar/home-filled.svg new file mode 100644 index 0000000..c07c9ab --- /dev/null +++ b/app/assets/icons/navbar/home-filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/icons/navbar/home-outline.svg b/app/assets/icons/navbar/home-outline.svg new file mode 100644 index 0000000..243c5db --- /dev/null +++ b/app/assets/icons/navbar/home-outline.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/icons/navbar/products-filled.svg b/app/assets/icons/navbar/products-filled.svg new file mode 100644 index 0000000..601368a --- /dev/null +++ b/app/assets/icons/navbar/products-filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/icons/navbar/products-outline.svg b/app/assets/icons/navbar/products-outline.svg new file mode 100644 index 0000000..8add69b --- /dev/null +++ b/app/assets/icons/navbar/products-outline.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/icons/navbar/profile-filled.svg b/app/assets/icons/navbar/profile-filled.svg new file mode 100644 index 0000000..405e110 --- /dev/null +++ b/app/assets/icons/navbar/profile-filled.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/assets/icons/navbar/profile-outline.svg b/app/assets/icons/navbar/profile-outline.svg new file mode 100644 index 0000000..00fe4d7 --- /dev/null +++ b/app/assets/icons/navbar/profile-outline.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/assets/icons/navbar/search-filled.svg b/app/assets/icons/navbar/search-filled.svg new file mode 100644 index 0000000..96fd1df --- /dev/null +++ b/app/assets/icons/navbar/search-filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/icons/navbar/search-outline.svg b/app/assets/icons/navbar/search-outline.svg new file mode 100644 index 0000000..1e0171d --- /dev/null +++ b/app/assets/icons/navbar/search-outline.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/assets/icons/navbar/setting-filled.svg b/app/assets/icons/navbar/setting-filled.svg new file mode 100644 index 0000000..a599674 --- /dev/null +++ b/app/assets/icons/navbar/setting-filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/icons/navbar/setting-outline.svg b/app/assets/icons/navbar/setting-outline.svg new file mode 100644 index 0000000..2535c7e --- /dev/null +++ b/app/assets/icons/navbar/setting-outline.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/assets/icons/product-dark.png b/app/assets/icons/product-dark.png new file mode 100644 index 0000000..6cc5e0d Binary files /dev/null and b/app/assets/icons/product-dark.png differ diff --git a/app/assets/icons/product.png b/app/assets/icons/product.png new file mode 100644 index 0000000..0602f2e Binary files /dev/null and b/app/assets/icons/product.png differ diff --git a/app/assets/icons/product/discount-red.svg b/app/assets/icons/product/discount-red.svg new file mode 100644 index 0000000..885b4b8 --- /dev/null +++ b/app/assets/icons/product/discount-red.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/icons/product/longArrow.svg b/app/assets/icons/product/longArrow.svg new file mode 100644 index 0000000..5a1f8b7 --- /dev/null +++ b/app/assets/icons/product/longArrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/icons/product/tomanBlack.svg b/app/assets/icons/product/tomanBlack.svg new file mode 100644 index 0000000..923b5e8 --- /dev/null +++ b/app/assets/icons/product/tomanBlack.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/icons/product/tomanGray.svg b/app/assets/icons/product/tomanGray.svg new file mode 100644 index 0000000..413725d --- /dev/null +++ b/app/assets/icons/product/tomanGray.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/icons/profile/bookmark/bookmark.svg b/app/assets/icons/profile/bookmark/bookmark.svg new file mode 100644 index 0000000..59bb997 --- /dev/null +++ b/app/assets/icons/profile/bookmark/bookmark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/assets/icons/profile/location/location.svg b/app/assets/icons/profile/location/location.svg new file mode 100644 index 0000000..854f6bf --- /dev/null +++ b/app/assets/icons/profile/location/location.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/assets/icons/profile/orders/paper.svg b/app/assets/icons/profile/orders/paper.svg new file mode 100644 index 0000000..c38583d --- /dev/null +++ b/app/assets/icons/profile/orders/paper.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/app/assets/icons/profile/threads/messageBox.svg b/app/assets/icons/profile/threads/messageBox.svg new file mode 100644 index 0000000..8c2eb49 --- /dev/null +++ b/app/assets/icons/profile/threads/messageBox.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/assets/icons/sellerPlaceholder-dark.svg b/app/assets/icons/sellerPlaceholder-dark.svg new file mode 100644 index 0000000..6111972 --- /dev/null +++ b/app/assets/icons/sellerPlaceholder-dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/assets/icons/sellerPlaceholder.svg b/app/assets/icons/sellerPlaceholder.svg new file mode 100644 index 0000000..13db3af --- /dev/null +++ b/app/assets/icons/sellerPlaceholder.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/assets/logo/SVG-01.svg b/app/assets/logo/SVG-01.svg new file mode 100644 index 0000000..bc49678 --- /dev/null +++ b/app/assets/logo/SVG-01.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/assets/logo/SVG-02.svg b/app/assets/logo/SVG-02.svg new file mode 100644 index 0000000..06dedd2 --- /dev/null +++ b/app/assets/logo/SVG-02.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/assets/logo/SVG-03.svg b/app/assets/logo/SVG-03.svg new file mode 100644 index 0000000..e697026 --- /dev/null +++ b/app/assets/logo/SVG-03.svg @@ -0,0 +1,17 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/assets/logo/SVG-04.svg b/app/assets/logo/SVG-04.svg new file mode 100644 index 0000000..42cb505 --- /dev/null +++ b/app/assets/logo/SVG-04.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/assets/logo/SVG-05.svg b/app/assets/logo/SVG-05.svg new file mode 100644 index 0000000..7399592 --- /dev/null +++ b/app/assets/logo/SVG-05.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/assets/logo/SVG-06.svg b/app/assets/logo/SVG-06.svg new file mode 100644 index 0000000..b5fbe4a --- /dev/null +++ b/app/assets/logo/SVG-06.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/assets/logo/SVG-07.svg b/app/assets/logo/SVG-07.svg new file mode 100644 index 0000000..8b2cf1a --- /dev/null +++ b/app/assets/logo/SVG-07.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/auth.server.ts b/app/auth.server.ts new file mode 100644 index 0000000..e5ddd1a --- /dev/null +++ b/app/auth.server.ts @@ -0,0 +1,21 @@ +import { Authenticator } from "remix-auth"; +import { FormStrategy } from "remix-auth-form"; +import { sessionStorage } from "./sessions.server"; + +export const authenticator = new Authenticator(sessionStorage); + +// login +authenticator.use( + new FormStrategy(async ({ form }) => { + const phoneNumber = form.get("phoneNumber") as string; + const access_token = form.get("access_token") as string; + const refresh_token = form.get("refresh_token") as string; + return { + phoneNumber: phoneNumber?.toString(), + access_token: access_token?.toString(), + refresh_token: refresh_token?.toString(), + loggedIn: true, + }; + }), + "vitrown-login" +); diff --git a/app/components/AIPromoteModal.tsx b/app/components/AIPromoteModal.tsx new file mode 100644 index 0000000..91abea4 --- /dev/null +++ b/app/components/AIPromoteModal.tsx @@ -0,0 +1,585 @@ +import { useState, useRef, useEffect } from "react"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogOverlay, +} from "./ui/dialog"; +import { Button } from "./ui/button"; +import { + Sparkles, + Upload, + ImageIcon, + X, + Download, + RotateCcw, + CheckCircle, + Share2, +} from "lucide-react"; +import { useRootData, useAuthToken } from "../hooks/use-root-data"; +import { LoginRequiredDialog } from "./LoginRequiredDialog"; +import { useMutation } from "@tanstack/react-query"; +import { getApiBaseUrl } from "~/utils/api-config"; + +interface AIPromoteModalProps { + isOpen: boolean; + onClose: () => void; + collectionId?: string; + productId?: string; +} + +export default function AIPromoteModal({ + isOpen, + onClose, + collectionId, + productId, +}: AIPromoteModalProps) { + const isProductMode = !!productId; + const { user } = useRootData(); + const token = useAuthToken(); + const [selectedImage, setSelectedImage] = useState(null); + const [imagePreview, setImagePreview] = useState(null); + const [resultImage, setResultImage] = useState(null); + const [isUploading, setIsUploading] = useState(false); + const [isLoginDialogOpen, setIsLoginDialogOpen] = useState(false); + const [loadingStep, setLoadingStep] = useState(0); + const fileInputRef = useRef(null); + + // Loading steps messages + const loadingSteps = [ + { text: "در حال آماده‌سازی لباس...", icon: "👗" }, + { text: "در حال اندازه‌گیری سایز شما...", icon: "📏" }, + { text: "در حال دوختن لباس به اندازه شما...", icon: "✂️" }, + { text: "در حال اتو کردن و مرتب کردن...", icon: "✨" }, + { text: "در حال پرو کردن روی شما...", icon: "🎯" }, + { text: "تقریباً آماده شده یخورده هم صبر کن...", icon: "⏳" }, + ]; + + // Cycle through loading steps + useEffect(() => { + if (!isUploading) { + setLoadingStep(0); + return; + } + + const interval = setInterval(() => { + setLoadingStep((prev) => { + // Stay on last step if we've gone through all + if (prev >= loadingSteps.length - 1) return prev; + return prev + 1; + }); + }, 4000); + + return () => clearInterval(interval); + }, [isUploading, loadingSteps.length]); + + // AI Try-on mutation for collection + const tryOnCollectionMutation = useMutation({ + mutationFn: async ({ + image, + collectionId, + }: { + image: Blob; + collectionId: string; + }) => { + if (!token) throw new Error("Authentication required"); + const baseUrl = getApiBaseUrl(); + + const formData = new FormData(); + formData.append("collection_id", collectionId); + formData.append("image", image); + + const response = await fetch(`${baseUrl}/api/aifit/v1/try-on/`, { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + }, + body: formData, + }); + + if (!response.ok) { + throw new Error(`API error: ${response.status}`); + } + + // Response is binary PNG, convert to blob URL + const blob = await response.blob(); + return URL.createObjectURL(blob); + }, + }); + + // AI Try-on mutation for product + const tryOnProductMutation = useMutation({ + mutationFn: async ({ + image, + productId, + }: { + image: Blob; + productId: string; + }) => { + if (!token) throw new Error("Authentication required"); + const baseUrl = getApiBaseUrl(); + + const formData = new FormData(); + formData.append("product_id", productId); + formData.append("image", image); + + // AI processing can take a while, set 2 minute timeout + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 120000); + + try { + const response = await fetch( + `${baseUrl}/api/aifit/v1/try-on/product/`, + { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + }, + body: formData, + signal: controller.signal, + } + ); + + clearTimeout(timeoutId); + + if (!response.ok) { + const errorText = await response.text().catch(() => ""); + console.error("API error response:", errorText); + throw new Error(`API error: ${response.status}`); + } + + // Response is binary PNG, convert to blob URL + const blob = await response.blob(); + console.log("Received blob:", blob.type, blob.size); + return URL.createObjectURL(blob); + } catch (error) { + clearTimeout(timeoutId); + if (error instanceof Error && error.name === "AbortError") { + throw new Error( + "درخواست زمان زیادی طول کشید. لطفا دوباره تلاش کنید." + ); + } + throw error; + } + }, + }); + + // Image Upload Handler + const handleImageSelect = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + // Validate file type + if (!file.type.startsWith("image/")) { + alert("لطفا یک فایل تصویری انتخاب کنید"); + return; + } + + // Validate file size (max 10MB) + if (file.size > 10 * 1024 * 1024) { + alert("حجم فایل نباید بیشتر از 10 مگابایت باشد"); + return; + } + + setSelectedImage(file); + + // Create preview + const reader = new FileReader(); + reader.onloadend = () => { + setImagePreview(reader.result as string); + }; + reader.readAsDataURL(file); + } + }; + + // Handle login required - close AI modal first, then show login + const handleLoginRequired = () => { + onClose(); // Close AI modal first + // Small delay to let the modal close animation complete + setTimeout(() => { + setIsLoginDialogOpen(true); + }, 150); + }; + + // Handle AI Promote Submit + const handleAIPromote = async () => { + // Check if user is logged in + if (!user) { + handleLoginRequired(); + return; + } + + if (!selectedImage) { + alert("لطفا ابتدا عکس خود را آپلود کنید"); + return; + } + + setIsUploading(true); + + try { + console.log("Sending AI try-on request..."); + + let result; + + if (isProductMode && productId) { + // Product try-on + result = await tryOnProductMutation.mutateAsync({ + image: selectedImage, + productId, + }); + } else if (collectionId) { + // Collection try-on + result = await tryOnCollectionMutation.mutateAsync({ + image: selectedImage, + collectionId, + }); + } else { + throw new Error("No product or collection ID provided"); + } + + console.log("AI try-on completed successfully", result); + + // Result is already a blob URL from the mutation + setResultImage(result); + + // Reset state + // handleModalClose(); + } catch (error) { + console.error("Error in AI try-on:", error); + alert("متاسفانه خطایی رخ داد. لطفا دوباره تلاش کنید."); + } finally { + setIsUploading(false); + } + }; + + // Reset modal state when closed + const handleModalClose = () => { + setSelectedImage(null); + setImagePreview(null); + setResultImage(null); + setIsUploading(false); + onClose(); + }; + + // Reset to try again + const handleTryAgain = () => { + setResultImage(null); + setSelectedImage(null); + setImagePreview(null); + }; + + // Download result image + const handleDownload = () => { + if (!resultImage) return; + const link = document.createElement("a"); + link.href = resultImage; + link.download = `ai-tryon-${Date.now()}.png`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }; + + // Share result image + const handleShare = async () => { + if (!resultImage) return; + + try { + // Convert blob URL to actual blob for sharing + const response = await fetch(resultImage); + const blob = await response.blob(); + const file = new File([blob], `ai-tryon-${Date.now()}.png`, { + type: "image/png", + }); + + // Build product URL for sharing + const siteUrl = + typeof window !== "undefined" + ? window.location.origin + : "https://vitrown.com"; + const productUrl = productId + ? `${siteUrl}/product/${productId}` + : siteUrl; + const shareText = `ببین این لباس رو تن من چطور شده! 👗✨\n\nاین محصول رو از ویترون ببین:\n${productUrl}`; + + // Check if Web Share API is available and supports sharing files + if (navigator.share && navigator.canShare?.({ files: [file] })) { + await navigator.share({ + title: "پرو کردن با هوش مصنوعی - ویترون", + text: shareText, + files: [file], + }); + } else { + // Fallback: Copy image to clipboard or show message + try { + await navigator.clipboard.write([ + new ClipboardItem({ "image/png": blob }), + ]); + alert( + "تصویر در کلیپ‌بورد کپی شد! حالا میتونی توی هر اپلیکیشنی Paste کنی." + ); + } catch { + // If clipboard also fails, just download + handleDownload(); + alert("تصویر دانلود شد. میتونی از گالری به اشتراک بذاری."); + } + } + } catch (error) { + console.error("Error sharing:", error); + } + }; + + return ( + <> + {/* Login Required Dialog - outside of main dialog to avoid overlap */} + + + + + + {/* Close Button */} + + + + + + {isProductMode + ? "پرو کردن محصول با هوش مصنوعی" + : "پرو کردن کالکشن با هوش مصنوعی"} + + + + + {isProductMode + ? "برای اینکه AI بتواند این محصول رو روی بدن شما نمایش بده، لطفا یک عکس قدی از خود را آپلود کنید." + : "برای اینکه AI بتواد این کالکشن رو روی بدن شما نمایش بده، لطفا یک عکس قدی از خود را آپلود کنید."} + + + +
+ {resultImage ? ( + // Result View +
+ {/* Success Badge */} +
+ +

+ پرو کردن با موفقیت انجام شد! +

+
+ + {/* Result Image */} +
+ AI Try-on Result +
+ + {/* Action Buttons */} +
+ {/* Share Button - Full Width */} + + + {/* Download & Try Again */} +
+ + +
+
+
+ ) : ( + // Upload View + <> + {/* Image Upload Area */} +
+ + + {imagePreview ? ( + // Image Preview +
+ Preview + +
+ ) : ( + // Upload Button + + )} +
+ + {/* Action Button or Loading State */} + {isUploading ? ( + // Loading State - replaces button and info boxes +
+ {/* Animated Icon */} +
+ {loadingSteps[loadingStep].icon} +
+ + {/* Loading Text */} +

+ {loadingSteps[loadingStep].text} +

+ + {/* Progress Dots */} +
+ {loadingSteps.map((_, index) => ( +
+ ))} +
+ + {/* Step Counter */} +

+ مرحله {loadingStep + 1} از {loadingSteps.length} +

+
+ ) : ( + // Normal State - Button and Info boxes + <> + + + {/* Info Box */} +
+ +

+ نکته: برای + بهترین نتیجه، عکس باید در نورپردازی مناسب و با زمینه + ساده باشه. AI با استفاده از این عکس،{" "} + {isProductMode ? "این محصول" : "محصولات کالکشن"} رو روی + بدن شما نمایش میده. +

+
+ + {/* Privacy Notice */} +
+ + + +

+ + حریم خصوصی شما مهمه: + {" "} + عکس‌های شما هیچ‌جا ذخیره نمیشه +

+
+ + )} + + )} +
+ +
+ + ); +} diff --git a/app/components/AppInput.tsx b/app/components/AppInput.tsx new file mode 100644 index 0000000..0e58399 --- /dev/null +++ b/app/components/AppInput.tsx @@ -0,0 +1,77 @@ +import { memo } from "react"; +import { Input } from "./ui/input"; +import { Skeleton } from "./ui/skeleton"; +import { LucideIcon } from "lucide-react"; + +const AppInput = memo(function AppInput({ + inputValue, + inputOnChange, + inputIcon, + inputPlaceholder, + inputDir, + disabled, + type, + inputTitle, + loading, // New prop + onKeyDown, + className, + inputRef, // New prop to forward ref to input + inputOnFocus, + inputOnBlur, + inputTitleFontSize, + iconProps, // New prop for icon customization +}: { + inputValue: string; + inputOnChange: (e: React.ChangeEvent) => void; + inputIcon?: LucideIcon | string; + inputPlaceholder: string; + type?: string; + inputDir?: string; + disabled?: boolean; + inputTitle: string; + loading?: boolean; // New prop + onKeyDown?: (e: React.KeyboardEvent) => void; + className?: string; + inputRef?: React.RefObject; // New prop type + inputOnFocus?: (e: React.FocusEvent) => void; + inputOnBlur?: (e: React.FocusEvent) => void; + inputTitleFontSize?: string; + iconProps?: { + size?: number; + color?: string; + strokeWidth?: number; + className?: string; + }; // New prop for icon customization +}) { + return ( +
+

+ {inputTitle} +

+ {loading ? ( + // Display skeleton when loading + ) : ( + + )} +
+ ); +}); + +export default AppInput; diff --git a/app/components/AppSelectBox.tsx b/app/components/AppSelectBox.tsx new file mode 100644 index 0000000..79ac6fb --- /dev/null +++ b/app/components/AppSelectBox.tsx @@ -0,0 +1,112 @@ +import { Check, Loader } from "lucide-react"; + +import { cn } from "../lib/utils"; +import { Button } from "../components/ui/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "../components/ui/command"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "../components/ui/popover"; +import { useState } from "react"; + +interface Item { + value: string | number; + label: string; +} + +interface AppSelectBoxProps { + items: Item[]; + placeholder?: string; + haveSearchbar?: boolean; + value: string; + setValue: React.Dispatch>; + loading?: boolean; // New loading prop + width?: string; // New width prop + disabled?: boolean; + title?: string; // Add title prop +} + +export default function AppSelectBox({ + haveSearchbar, + items, + placeholder, + value, + setValue, + loading = false, // Default to false + width = "w-[105px]", // Default width + disabled = false, + title, +}: AppSelectBoxProps) { + const [open, setOpen] = useState(false); + + return ( +
+ {title && ( +

{title}

+ )} + + + + + {!loading && ( // Only show dropdown content when not loading + + + {haveSearchbar && ( + + )} + + آیتمی وجود نداره + + {items.map((item: Item) => ( + { + setValue( + item.value === value ? "" : item.value.toString() + ); + setOpen(false); + }} + > + {item.label} + + + ))} + + + + + )} + +
+ ); +} diff --git a/app/components/AppTextArea.tsx b/app/components/AppTextArea.tsx new file mode 100644 index 0000000..622b23c --- /dev/null +++ b/app/components/AppTextArea.tsx @@ -0,0 +1,39 @@ +import { Skeleton } from "./ui/skeleton"; + +export default function AppTextArea({ + inputValue, + inputOnChange, + inputPlaceholder, + inputDir, + disabled, + inputTitle, + loading, +}: { + inputValue: string; + inputOnChange: (e: React.ChangeEvent) => void; + inputPlaceholder: string; + inputDir?: string; + disabled?: boolean; + inputTitle: string; + loading?: boolean; +}) { + return ( +
+

+ {inputTitle} +

+ {loading ? ( + + ) : ( +