Commit Graph

74 Commits

Author SHA1 Message Date
31bc9f3c7b fix(push): include underlying error text in subscribe failure toast
Sellers running from an installed PWA still hit failures inside subscribe
(iOS 16.4+ PWA has its own quirks even when standalone), and a generic
"لطفاً چند لحظه بعد دوباره تلاش کنید" doesn't tell whoever is helping
them what actually broke. The catch handler now surfaces the underlying
Error.message (e.g. "timeout: pushManager.subscribe", "NotAllowedError")
in the toast description so field debugging works over screenshots.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-19 16:22:50 +00:00
c4e0f2f89f fix(push): timeout every await + iOS-not-PWA early exit so subscribe never wedges
The subscribe/disable flow awaited several browser APIs that on certain
devices — most reliably iOS Safari in a non-installed PWA — return a
Promise that never resolves or rejects (requestPermission, serviceWorker
.ready, pushManager.subscribe). When any of them hung, the try/finally
never ran, so `busy` stayed true forever: the dashboard nudge button
stuck at "..." and the settings toggle refused to flip.

Every awaited step is now wrapped in withTimeout(15s). Failures surface
as toasts instead of silent hangs (subscribe success, permission denied,
generic error). iOS Safari without the site installed to home screen —
where Web Push physically cannot work — short-circuits with an
add-to-home-screen instruction before we ever try, so we don't burn a
timeout waiting for a doomed subscribe().

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-19 15:33:04 +00:00
225161249e feat(chat+push): image attachments, day-labeled inbox with reply state, sub self-heal
- MessageInput: paperclip button (team-only, controlled by teamUserIds
  from ThreadDetail) uploads through the media service and hands the URL
  to the WebSocket; Enter now inserts a newline — sending is button-only
  since accidental Enter-sends were noisy for staff replies.
- MessageBubble: renders image messages inline (tap to open), shows the
  coworker's username at the top of team-side bubbles so staff can tell
  who typed what.
- MessageList / ThreadChatPage: any message from a store team member
  (owner or active staff) renders on the "our" side of the bubble even
  for a viewer who isn't the sender, using team_user_ids from
  ThreadDetail. Delete option stays gated to the actual sender.
- ThreadsPage: inbox now shows smart-relative timestamps
  (today HH:MM / دیروز HH:MM / N روز پیش / Persian date), plus a
  reply-state icon per row (red bell = customer waiting, blue double-check
  = you replied and they've seen it, nothing = you replied not yet seen).
  Image-only last messages preview as "📷 تصویر".
- Push notifications: on mount we re-POST the current PushSubscription so
  a browser-rotated sub or a backend row deleted after a 410 auto-heals.
- useWebSocket: SendMessagePayload and NewMessageEvent carry image_url so
  the server relay can round-trip images.
- API types: MessageList.imageUrl, LastMessage.imageUrl,
  ThreadList.lastMessageFromTeam/otherPartySeenLast, ThreadDetail.teamUserIds.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-19 13:22:44 +00:00
56fcce35c8 feat(product-detail): ProductDescription section + strict discount guard + category types
- app/components/product/ProductDescription.tsx: new below-fold section
  with a "توضیحات محصول" heading, whitespace-pre-line so multi-line seller
  copy (Instagram scrapes, victorwear WooCommerce imports) renders with
  its bullet-list line breaks preserved. Renders on both mobile and
  desktop; nothing shown when the product has no description.
- app/components/product/ProductInfo.tsx: removed the collapsed
  single-<p> mobile description; the new section replaces it.
- app/components/product/ProductDetailView.tsx: pricingOf now hides the
  discount when discount_price >= price (was only hiding when equal),
  so a stale row where the discount price accidentally matches or
  exceeds the base can never render as a fake promo. Mounts
  ProductDescription just above the mobile Contact button and reviews.
- app/components/product/index.ts: export ProductDescription.
- src/api/types/models/index.ts: ProductCategory now carries readonly
  parentId / parentName so the two-step category picker can build the
  parent→sub tree from a flat category list without an extra fetch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-18 13:19:27 +00:00
83b93fe523 feat(seller-editor): 9x9 color grid, mandatory size, two-step category, robust image + discount save
- app/components/store/ColorSelector.tsx: renders as a 9-column grid sorted
  by info.sortOrder. Compact aspect-square swatches; Persian name lives in
  title/aria-label so 81 swatches breathe on mobile. Checkmark ink flips by
  swatch luminance so it stays visible on both سفید and مشکی.
- app/components/store/SizeSelector.tsx + ProductVariantDrawer.tsx: dropped
  the "بدون مشخص کردن سایز" toggle now that "FreeSize" exists in the
  palette. Every product must pick a size (or FreeSize) on save.
- app/routes/store.add.manual.$storeId.tsx:
    - two-step category picker (parent → sub) driven by category.parentId,
      with edit-mode pre-fill deriving the parent from the sub's parentId.
    - getFinalImageUrls now uploads in parallel with concurrency 3, retries
      each image once, persists successful uploads to state as they land so
      a resubmit only retries the still-failing ones. If every upload fails
      we refuse to submit rather than sending an empty gallery.
    - only ship discountPrice when it's a REAL discount (finalNum > 0 and
      < priceNum) — otherwise pass null so the backend clears any stale
      value. Closes the "phantom discount" hole where PricingForm's
      "no discount" radio still shipped the previous typed number.
    - isStep1Valid requires every variant to have a real (non-"unselected")
      size, matching the new mandatory-size rule.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-18 13:19:13 +00:00
6a300b8760 fix(store): don't bounce owners home, stop cross-user myStores cache
- app/hooks/useStoreOwnership.ts: the old redirect logic sent owners to /
  the moment useMyStores briefly resolved to []. That empty array came from
  useMyStores's dead `if (!token) return []` inside the queryFn, so any
  transient auth blip left the store page thinking the user has no stores.
  Now compares usernames case-insensitively, only redirects while the query
  is settled (!isLoading && !isFetching && !isError), and never bounces to
  home on empty — worst case the seller sees an empty dashboard rather than
  a silent kick-out.
- app/requestHandler/use-seller-hooks.ts: removed the unreachable
  `if (!token) return []` in useMyStores and added the token to the
  queryKey so a login switch stops serving the previous user's cached list.
- app/routes/store.$storeId._index.tsx: mount StorePushPromptCard at the
  top of the store dashboard.
- app/routes/store.$storeId.tsx: sidebar "پیام‌ها" badge reads
  badges.sellerChat[storeId] (per-store) instead of the buyer-side
  badges.chat, matching the badge-serializer split.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-18 13:18:23 +00:00
0b18438a97 fix(notif+auth): camelCase badge fields, seller push prompt, only 401 signs out
- app/components/MyLayout.tsx / app/components/home/HomePageTopBar.tsx /
  app/routes/profile._index.tsx: the BadgeCounts type was declared with
  snake_case fields (order_updates, seller_orders) while the API's global
  camelCase renderer sends orderUpdates / sellerOrders on the wire — every
  read went to undefined, so the "buyer activity" dot on the profile icon
  and the order-badge on the store dashboard were silently stuck at 0.
  Fixed by renaming the interface to match the wire and adding sellerChat
  (per-store roll-up) so the store dashboard sidebar reads
  badges.sellerChat[storeId] instead of the buyer-side chat count.
- app/hooks/useBadgeCounts.ts: interface + EMPTY constant updated to
  camelCase + new sellerChat map.
- app/utils/api-error-handler.ts: 403 no longer force-redirects to /logout.
  It used to force-log-out on ANY 403/401, so an approved-but-unbadged
  owner (Ehsan / Noelabel) whose store dashboard fired the Instagram-tab
  query got a 403 → forced logout → the store page vanished mid-load
  and the seller landed on the home screen convinced they'd been kicked
  off their own store. Only 401 signs the session out now.
- app/components/store/StorePushPromptCard.tsx: new dashboard-level card
  that nudges the store owner/staff to enable browser push. Unlike the
  global buyer banner it's not dismissible-forever; "بعداً یادآوری کن"
  hides for 24h and it comes back until the user actually subscribes.
  Handles the permission="denied" path with an in-browser-settings hint.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-18 13:18:08 +00:00
7a385ba18e fix(pwa): top safe-area inset on products page + owner product carousel
Two more iOS-notch overlaps:
- Seller محصولات page: top container now carries pt-[env(safe-area-inset-top)]
  on mobile (lg:pt-0) so the heading drops below the notch.
- Owner single-product view: the carousel's ⋮/back buttons sat at top-3 (under
  the notch); offset them by calc(env(safe-area-inset-top,0px)+12px), matching
  the buyer back button. Only the owner buttons (product detail, top of screen)
  use this — grid tiles don't pass isOwner.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 15:16:28 +03:30
2cf1224107 change(size-chart): centimeter only (drop cm/inch toggle)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 15:09:01 +03:30
7867db425e feat(chat): share the product into the chat + fix empty-thread preview
- Product-page chat buttons pass the product id when starting a thread, so it's
  shared into the conversation as a card the seller can see (and tap through).
- MessageBubble renders the product card (image, title, price → product page);
  card-only messages no longer render an empty bubble.
- Inbox preview shows 🛍 <product> instead of "بدون پیام" for product-card
  threads. ChatProductCard type + product on message/last-message types.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 15:09:01 +03:30
d694040bf7 fix(pwa): apply top safe-area inset to page headers (iOS notch)
MyLayout only pads the bottom inset globally; each top bar must add its own
top inset. Several inline page headers omitted it, so their content slid
under the notch in the iOS standalone PWA (home was fine — its bar has it).

Wrap the back-button headers the same way ProfilePagesHeader already does
(outer sticky element carries pt-[env(safe-area-inset-top)]; inner relative
bar holds the content) so the absolute back arrow stays out of the notch.
Fixed: /profile hub, /discounts, /collections, /collection/:id, /sellers,
seller store index, and financial transactions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 13:21:23 +03:30
d4316381d5 feat(seller): size chart editor + buyer size guide (راهنمای سایز)
Sellers fill a size grid (rows = sizes, columns = measurements) in the
product create/edit wizard; buyers see it via a راهنمای سایز dialog.

- SizeChartSection editor: rows seeded from the product's sizes, columns
  seeded from category-aware defaults, category-specific quick-add chips
  (tops→دور سینه/سرشانه/آستین, pants→دور کمر/دور باسن/فاق, shoes→سایز اروپایی,
  bags→طول/عرض/ارتفاع, …), custom columns, cm/inch toggle, scrollable grid
- required-if-the-product-has-sizes gate on publish (with per-size hint);
  optional otherwise so accessories/one-size and imports aren't blocked
- buyer SizeGuideDialog + راهنمای سایز link in the size selector header
- SizeChart types on ProductDetail / FullProductCreate / FullProductUpdate

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 13:21:23 +03:30
05cbbd8d5e chore(store): update product title placeholder example
"کفش آدیداس" (Adidas shoes) name-checked a third-party brand — swap for
"تیشرت بیسیک" (basic t-shirt), a neutral generic example.
2026-07-09 20:12:30 +00:00
72e4baab10 fix(seller): make inline price editor obviously tappable on touch
The pencil hint was hover-only, so on phones the price read as plain text.
Style the display as an editable field (subtle border + grey fill) with an
always-visible pencil, and drop the negative margins that fought the border.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 12:51:36 +03:30
5c8e19001c feat(seller): inline quick-edit for product & variant prices
Sellers can tap a price on the products page to edit it inline (Enter to
save / Escape to cancel), mirroring the existing stock quick-edit.

- new reusable InlinePriceEditor (optimistic display, Persian/Latin digit
  input, thousands grouping, loading + toast feedback, a11y labels)
- base price editable on each product row (mobile + desktop column)
- per-variant price editable beside stock in the expanded panel; empty
  clears the override and inherits the base price ("پیش‌فرض: <base>")
- useUpdateProductBasePrice + useUpdateVariantPrice hooks; extend generated
  API client + types for the two new endpoints

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 12:41:04 +03:30
16389fdd85 fix(desktop): lay out dashboard sub-pages + form pages for wide screens
- HeaderWithSupport is now lg:hidden (all its callers are desktop-full-width
  store/profile pages), removing the stray mobile back+support bar on desktop.
- Constrain + title the previously mobile-only pages so their content doesn't
  stretch edge-to-edge on desktop: store edit/create form (StoreForm), add
  product, shipping method, financial sub-pages (cash-funds/reports/
  transactions), order detail, instagram auth; and profile add/edit address,
  set detail, and payment result. Forms use a narrow centered column, lists/
  details a wider one. Transactions keeps its filter button (header restyled,
  not hidden).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 12:06:13 +03:30
a67e81ad55 fix(desktop): constrain profile sub-pages so they don't stretch on desktop
These pages are desktop-enabled (they get the DesktopHeader via the /profile
allowlist) but had no desktop layout, so their content stretched edge-to-edge on
wide screens. Add a centered max-width container + a desktop title to the main
profile sub-pages (bookmarks, following, orders, settings, wallet, addresses,
sets). Grids use a wider max-width; single-column lists a narrower one.
ProfilePagesHeader is already lg:hidden, so the mobile header still disappears.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 11:53:28 +03:30
0a482c70fa fix(discounts): enable desktop chrome for /discounts
/discounts was missing from MyLayout's isDesktopReady allowlist, so on desktop it
stayed in the narrow mobile column with no desktop header/footer. Add it so it
gets the full-width desktop canvas; the page's lg:max-w container + Mondrian
4-column masonry then render like /sellers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 11:41:09 +03:30
b371c50d23 fix(pwa): add apple-mobile-web-app-capable so iOS applies safe-area insets
On iOS, apple-mobile-web-app-status-bar-style: black-translucent has no effect
unless apple-mobile-web-app-capable=yes is also set. Without it the installed
PWA never went edge-to-edge, so env(safe-area-inset-top) resolved to 0 and the
header padding did nothing — content ran under the notch. Android was unaffected
(it honors mobile-web-app-capable, which was already present).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 11:35:12 +03:30
8b51c3252a feat(search): show sellers and collections in search results
Render a فروشگاه‌ها row (store cards with logo via SellerLogo, linking to the
store) and a کالکشن‌ها row (reusing HorizontalCollectionsList) above the product
grid on the search page. Sellers/collections read off the first page (they don't
paginate); products paginate as before. Types extended (SellerSearchResult.logo,
SearchResponse.collections). Degrades gracefully on older backends.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 10:44:52 +03:30
fea80647fa feat(discounts): full تخفیف‌ها page with infinite scroll
Add a complete /discounts page (MondrianProductList grid, infinite scroll via
useGetHomeDiscounts) and link the home page "تخفیف‌ها" section to it with a
«مشاهده همه» link, mirroring the برترین فروشگاه‌ها → /sellers pattern.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 14:00:44 +03:30
877b01f483 feat(desktop-header): surface "ورود به فروشگاه من" CTA for sellers and staff
Mobile profile page already has this action, but the desktop chrome had
no way for a seller or staff member to jump into a store they manage —
they'd have to type the URL. Add a compact "فروشگاه من" pill in the
header action row, gated on useMyStores() so it only renders for users
who actually manage a store. Selection mirrors the mobile flow: owned
first, then the first store they staff.
2026-07-07 10:57:14 +00:00
0d0da636fd fix(seller-team): call /api/seller/v1/ (not /api/seller/) for team endpoints
my-stores, members list/invite/remove all mount under /api/seller/v1/ in the
Django backend, so my-stores and every team-management fetch was 404'ing. That
made useMyStores throw silently, useStoreOwnership fall back to role=null, and
the setting page's `!ownerOnly || role === "owner"` filter drop every
owner-only tile (edit-store, team, financial-dashboard, shipping-method,
instagram-sync). Owners could log into their own store and see none of the
owner-gated menu items.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-06 12:48:36 +00:00
ba8e839959 Merge pull request 'feat(seller): multi-user seller dashboard (owner + staff)' (#2) from feat/seller-team into main 2026-07-01 14:04:19 +00:00
bb44ebf0e5 fix(seller-team): guard owner-only routes + robust useMyStores
Review fixes:
- useMyStores throws on a failed response instead of resolving to [] — a
  transient 401/blip no longer looks like "no stores" and bounces an owner off
  their own dashboard (query stays in error/retry, keeps prior data).
- Add useRequireOwner() guard and apply it to the owner-only routes
  (financial-dashboard + its cash-funds/reports/transactions sub-routes and
  shipping-method) so staff who hit those URLs directly are redirected to the
  dashboard. (Financial data was already backend-protected; this is the matching
  client-side gate.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 17:04:06 +03:30
a642fc0fae feat(seller-team): multi-user dashboard — store switcher, role gating, team UI
- useMyStores + team hooks (invite by phone / list / remove) in use-seller-hooks.
- useStoreOwnership rewritten to allow staff (not just the owner) via my-stores;
  exposes `role` so callers can gate owner-only UI, and no longer redirects
  members away from a store they help manage.
- Store settings: owner-only items (financial, shipping, edit store, instagram
  sync, team) hidden from staff; new "مدیریت اعضای تیم" entry (owner-only).
- New /store/:storeId/team page: invite by phone + members list with pending/
  active status + remove (owner-only, redirects staff). MyLayout keeps store mode
  on the team route.
- StoreForm edit is owner-only (redirects staff). Profile "enter my store" now
  routes owners to their store and staff to the store they manage.
- Chat already split earlier: seller inbox vs personal buyer inbox.

Depends on the seller-team backend; ships on this branch, not main.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 16:43:53 +03:30
3502168a8b feat(seller-team): split buyer inbox vs store inbox in chat UI
Seller dashboard threads (type="store") now fetch the store's inbox via
useGetStoreThreads(storeId) -> /threads/list/?store_id=; the profile threads page
(type="user") keeps the personal buyer inbox. Depends on the seller-team backend
(store-aware threads + store_id-by-username), so it ships on this branch, not main.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 16:00:23 +03:30
e60a03ac65 feat(seller-tiles): remove seller name overlay on desktop grid
The homepage seller tiles now show just the shop image on all breakpoints.
The desktop-only bottom gradient + name label was making some brand images
harder to read; the shop click target is unchanged and username is still
available on hover-tap navigation to /seller/<username>.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-01 09:15:42 +00:00
660e01c3c3 feat(seller): customizable profile banner + seller page polish
- Banner editor in store edit: color swatches + custom color picker, or
  image/GIF upload with a 1600x500 dimension hint. Renders the chosen banner
  (image cover / solid color) on the public seller page, falling back to the
  default gradient.
- Sellers list: drop the desktop name overlay (match mobile, image-only tiles).
- Seller page mobile header: chat moved top-left under the banner, search+filter
  beside the follow button, followers/product counts removed (rating kept),
  compacted so products fill more of the screen, circular logo, thinner action row.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 12:00:23 +03:30
bb10907463 feat(product): record product views for recommendations
Fire a fire-and-forget view event when an authenticated, non-owner user
opens a product page (deduped per product), POSTing to the new
/api/engagement/v1/views/ endpoint. Silent on failure so tracking never
disrupts browsing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 13:57:04 +03:30
0b874dbd57 feat(address-map): click-to-select + "use my current location"
- Click-to-select instead of geocoding on every move: pan the map freely
  (drag), then tap to drop the pin — reverse geocodes that point ONCE.
  Saves Map.ir reverse tokens. Shows a "tap to select" hint until a point
  is chosen; a draggable-style red marker marks the selection (and the
  existing point in edit mode, without re-geocoding).
- "موقعیت فعلی من" button: uses the device geolocation to fly to and
  select the current position (with an error toast if denied/unsupported).
- removed the old center-pin + moveend auto-geocode and the unused
  user-location-found event/pulse effect.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 16:43:55 +03:30
0f617b45c9 feat(profile): show which area a notification is for (chat / orders)
The bottom-nav profile badge sums unread chat + order updates, but the
profile page gave no breakdown. Wire useBadgeCounts into the mobile
account hub: the پیام‌ها (messages) menu item shows the chat unread count,
and the سفارش‌ها quick action shows the order-updates count. Adds a badge
slot to the menu item content.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 16:18:43 +03:30
68786787b4 fix(address-map): RTL label rendering + use address_compact
- register MapLibre's RTL text plugin (@mapbox/mapbox-gl-rtl-text) so
  Persian/Arabic map labels render correctly instead of reversed/broken.
  Guarded via getRTLTextPluginStatus so it's set only once.
- reverse geocoding now prefers Map.ir's address_compact (shorter, cleaner
  address) over the full address.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 02:38:34 +03:30
3c7ee5b24c feat(address-map): MapLibre GL + Map.ir vector map & Persian reverse geocoding
Replace the broken-looking Leaflet/OSM address picker with a MapLibre GL
map using Map.ir vector tiles, plus Map.ir reverse geocoding for fast
coordinates -> Persian address.

- InteractiveMap: drop react-leaflet/leaflet; init a MapLibre map (Map.ir
  style via transformRequest x-api-key), a NavigationControl, and a fixed
  center pin — the map moves under the pin and the centre is reverse-
  geocoded on moveend (debounced). Wrapped dir=ltr so the map renders
  correctly inside the RTL page (fixes the scattered-tiles bug). Selected
  address shown in an overlay chip; city select / geolocation fly the map.
- reverse geocoding: Nominatim -> Map.ir (https://map.ir/reverse), which
  returns proper Persian addresses (Nominatim was weak for Iran and its
  TOS forbids production use).
- default map centre fixed to Tehran (matched the "تهران" selector).
- key via VITE_MAPIR_KEY (documented in .env.example; real key in env).
- deps: add maplibre-gl, remove leaflet/react-leaflet/@types/leaflet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 01:20:37 +03:30
668649d540 feat(profile): mobile account hub redesign
Rework the mobile profile to match the design: an avatar + name + phone
header with an edit button, a wallet gradient card (live balance from
useServiceGetWallet + افزایش موجودی → /profile/wallet), a 4-up quick-action
grid (orders / bookmarks / following[badge] / addresses), a «حساب من»
section label, the theme toggle, and a trimmed menu list (items surfaced
above are dropped from the list). Desktop profile (sidebar + welcome
panel) is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 23:41:36 +03:30
254fed4a99 feat(cart): mockup-style item rows + separate-shipping note
- CartItem: compact row — image + title + رنگ/سایز attribute chips
  (circular color dot) + a bottom row with the stepper and the effective
  price plus struck original. Drops the verbose label:value rows and the
  product-code line. Shared with the desktop detail column (verified).
- cart list: add a green «ارسال جداگانه» badge per seller-cart, matching
  the mockup's per-seller grouping.

Keeps the existing list -> detail checkout flow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 23:16:42 +03:30
e6574b1e9e feat(seller): mobile brand header redesign (cover + overlapping logo)
Bring the seller page mobile header in line with the design / desktop:
replace the plain top bar + centered logo with a cover banner, a floating
circular back button (safe-area aware), an overlapping rounded logo, the
store name + verified badge + @handle, a clean borderless stats row
(محصول / دنبال‌کننده / امتیاز), the description, and follow + message-icon
actions. Search bar and product masonry unchanged. Desktop untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 21:44:20 +03:30
252d3f8f50 feat(search/explore): home-style product tiles with seller brand chip
Bring the home product tile to search and explore: seller brand chip
(SellerChip) plus price/discount overlay, instead of a bare image.

- search: map discountPercent/discountPrice/basePrice + sellerName from
  ProductSearchResult (sellerName is already returned, so the chip shows
  now with the initial-letter fallback).
- explore: map sellerName/sellerLogo/sellerUsername (and add them to the
  Product type); these populate once the backend ProductSerializer change
  ships.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 17:09:58 +03:30
11656c2996 feat(home): seller brand chip (logo + name) on product tiles
Show the seller's brand on the home product cards, like the design:
a small logo circle + name overlaid above the title.

- new SellerChip component: renders nothing without a name; falls back to
  the seller's initial when no logo is present (e.g. before the backend
  ships seller_logo). seller_name is already returned today, so the name
  shows immediately.
- thread sellerName/sellerLogo/sellerUsername through ProductHomepage type,
  the home discounts + for-you mappings, and the MondrianProductList /
  HorizontalProductList card interfaces + overlays.

Only renders where seller data is mapped (home), so seller-page and
store-dashboard tiles are unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 16:38:44 +03:30
692d385d09 feat(product): mobile redesign — brand below image, floating back, circular colors
Per design feedback, restructure the mobile product page (desktop layout
unchanged except color swatches):
- remove the top seller bar; the seller/brand (logo + name + ارسال پیام)
  now sits in a row below the gallery.
- add a floating circular back button over the image (safe-area aware).
- move the rating out of the action row into a prominent body row, shown
  only when the average rating is > 0.
- color swatches are now circles with a selection ring (shared with
  desktop for consistency) instead of squares.
- make the افزودن به سبد خرید button full-width/taller with a compact
  price beside it.
- the action row keeps save / share / add-to-collection / try-on.
- the red discount icon is kept as-is.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 15:06:54 +03:30
b45e247dda Revert "feat(home): clean discount pill on product cards"
Restore the original red discount-icon badge on product cards per
preference; revert the ٪N pill restyle.

This reverts commit eefad53.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 14:23:42 +03:30
eefad5387c feat(home): clean discount pill on product cards
Restyle the discount badge to the redesign's pill: ٪ + rounded Persian
digits on a translucent white rounded-full chip, no icon. Drops the raw
decimal percent (e.g. "12.7" -> "٪۱۳") and the discount-red icon import.
Applied in both the discounts rail and the for-you masonry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 14:08:32 +03:30
f8289898bc feat(home): search-first mobile top bar (redesign)
Replace the mobile home scroll-tab bar with the redesign's search-first
chrome: logo + ویترون wordmark, notification (bell) and cart (bag) icon
buttons with unread dot / count badges, and a prominent tap-to-search
field that opens /explore. Mobile only (stays inside lg:hidden), so the
desktop DesktopHeader is unaffected. Keeps the existing pinar font and
color tokens; preserves the safe-area-inset top padding.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 13:58:46 +03:30
bac93568f5 feat(upload): convert HEIC/HEIF to JPEG on the client before preview/upload
Most browsers (everything but Safari/WebKit) can't decode HEIC in <img> or
canvas, so picking an iPhone HEIC photo broke the preview and the crop editor,
and could reach the backend in a format it couldn't process. Convert HEIC/HEIF
to JPEG right after selection via heic2any (dynamically imported, browser-only).

- new util app/utils/convert-heic.ts: isHeic() detection (MIME or extension
  when iOS reports no type) + convertHeicToJpeg()/convertHeicFiles(); never
  throws — falls back to the original file on failure.
- wired into the product image picker (store.add.manual) and the store
  logo picker (StoreForm).
- adds heic2any dependency.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 16:43:30 +03:30
641abf711c fix(upload): preserve resolution when cropping product images
The crop canvas was sized to cropArea.width/height, which are the editor's
on-screen (CSS) pixels — a few hundred px. Phone photos were therefore
downscaled to that size before upload, so cropped product images came out
blurry (most visibly on iPhone, whose source photos are large).

Size the canvas to the crop region in the source image's native pixels
(adjustedCropWidth/Height, already computed) and draw 1:1. Bumped JPEG
quality 0.9 -> 0.92. The backend still caps the final image to 1600px webp,
so output size stays bounded.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 16:27:28 +03:30
940cc3e859 fix(store): always revalidate public storefront product list
The seller store page (useGetSellerProducts) inherited the global 30s
staleTime with refetchOnWindowFocus disabled, so after a seller edited a
price the store tiles could keep showing the pre-edit price while the
(SSR) single-product page already showed the new one — a list-vs-detail
mismatch. The backend serves both from live DB and is consistent.

Set staleTime: 0 + refetchOnMount/refetchOnWindowFocus on the storefront
query so it revalidates on every visit and tab refocus, keeping the list
prices current.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 20:06:34 +00:00
272362cfdf fix(product): detail page price now matches list tiles + fresher feeds
Single-product page showed basePrice with no discount while list tiles
showed the cheapest variant's discounted price. Cause: the detail view
defaulted selectedColor/selectedSize to the first in-stock color and the
first in-stock size independently — a combo that often isn't a real
variant, so no variant matched and it fell back to basePrice / no sale.

- ProductDetailView: derive a `defaultVariant` (cheapest in-stock variant
  by final price, == the tile's min/discount price) and use it for the
  initial selection, the initial displayed price, and the fallback when no
  exact color/size combo is matched. A shared `pricingOf` helper applies
  the same discount rule everywhere. Detail price now matches the tile.
- use-home-hooks: drop staleTime 5m -> 60s on the For You / discounts /
  top-sellers feeds so price/discount edits show up promptly in lists.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 19:55:11 +00:00
a405f6ffc4 feat(desktop): add search autocomplete to desktop header
The desktop header search only navigated on submit — the autocomplete
suggestions dropdown existed on mobile (explore SearchSection) but was
never wired into the desktop chrome. Reuse the same useSearchAutocomplete
hook (debounced) and render a suggestions dropdown under the search box:
product titles navigate to /search, sellers navigate to /seller/<id>.
Closes on outside-click / Escape / selection.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 19:23:12 +00:00
501cf28b9a fix(auth): 6-digit OTP in LoginRequiredDialog
The login-required popup still rendered a 4-slot OTP (maxLength=4, gated on
length===4) while the main login page already used 6. Users who triggered
login via the dialog saw the old 4-digit code. Match login.tsx: 6 slots,
length===6, same slot sizing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 17:53:01 +03:30
b4d5f8d8db fix(ios): respect safe-area insets so chrome clears the notch/home bar
Content was drawing under the iOS status bar and home indicator because
the app uses viewport-fit=cover + black-translucent but the fixed/sticky
chrome didn't reserve the safe areas.

- bottom nav (MyLayout): move the 50px row into an inner box and let the
  bar extend by env(safe-area-inset-bottom) so icons sit above the home
  indicator (previously the fixed height clipped the inset padding).
- top headers gain env(safe-area-inset-top): HomePageTopBar, ExploreTopBar,
  SearchTopBar, CartHeader, ChatHeader, and wrapper-padded HeaderWithSupport
  / ProfilePagesHeader (so their absolutely-positioned buttons shift too).
- chat input pads bottom by the inset; chat scroll area height accounts for
  the taller header/input.
- _index.tsx no longer overrides the viewport meta (it was dropping
  viewport-fit=cover on the home page); the global one in root.tsx wins.

env(safe-area-inset-*) is 0 on non-notch devices, so desktop/Android render
identically — only iOS gains the padding.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 13:42:51 +03:30