Skip to content

Shop: storefront features, coupons, password reset - #57

Merged
bahman026 merged 40 commits into
mainfrom
shop
Aug 6, 2026
Merged

Shop: storefront features, coupons, password reset#57
bahman026 merged 40 commits into
mainfrom
shop

Conversation

@bahman026

Copy link
Copy Markdown
Owner

Brings main up to date with the shop branch — 40 commits since PR #55.

Breakdown: 29 feat, 6 docs, 2 test, 2 refactor, 1 fix.

🤖 Generated with Claude Code

bahman026 added 30 commits July 24, 2026 19:13
Replaces the free-text `position` field with a Select backed by a new
SliderPositionEnum (home-main, home-secondary, category-top,
product-side). The factory now derives its position from the enum, and
the table column shows the readable label. Prevents typos that would
silently break the storefront's slider lookup. Lang + db doc updated.
…stem

Slider: mirror the admin SliderPositionEnum in shop; rename
GetHeroSlides -> GetSliderByPosition(SliderPositionEnum) and have
HomeController pass HOME_MAIN, so the admin's assigned position and the
storefront lookup share one enum value instead of a hardcoded string.

Localization: move ALL server-side user-facing Persian out of PHP into
Laravel's lang system, default locale fa (APP_LOCALE=fa in .env,
.env.example and phpunit.xml; fallback_locale=en so framework strings
still resolve). Three lang/fa files:
- enums.php: every enum label() (keyed enums.<enum>.<case>)
- messages.php: flash/withErrors/abort strings, breadcrumbs, footer
  titles, home row titles, payment/result messages (payment.order_number
  takes an :id), deleted_product/guest_user fallbacks
- validation.php: standard Laravel rules + attributes, so AddressController
  drops its inline messages/attributes and every form gets Persian errors
Digit-normalization maps and code comments are intentionally left as-is.

Docs/AGENTS/tests updated; a HomePageTest case asserts a non-home-main
slider doesn't leak onto the hero.
Tags: new `tags` table + `attribute_tag` pivot, `Tag` model/factory/seeder
and `TagResource`. A tag is an SEO landing page for a category and/or
attribute filter, not a free-form product label (no `product_tag` pivot).
Deviations from the source schema: `category_id` is nullable and
attributes are many-to-many (so a tag can be category-only,
attribute-only, or both — at least one required, enforced in the form via
`requiredWithout`); SEO columns `title`/`description`/`no_index`/
`canonical` added to match categories/products; the single `attribute_id`
column and the user/seller `type` column are dropped (single-vendor).

Home sections: new `home_sections` table (not in the source schema) so
staff compose the storefront home page instead of it being hardcoded.
`HomeSectionTypeEnum` (slider/tags/categories/banners/products/brands),
a `config` JSON bag per type (`{"position": …}` for slider/banners,
`{"sort": …}` for product rows), `order` + `status`, and a
`HomeSectionResource` with a drag-to-reorder table whose form only shows
the fields the chosen type needs. `HomeSectionSeeder` seeds the current
hardcoded order. The storefront does not read this table yet.

Banner positions: `position` moves from a free-text input to a
`BannerPositionEnum` dropdown (home-top / home-middle / category-side),
mirrored in the shop so the assigned position and the storefront lookup
can't disagree; the table column and factory derive from the enum too.

Banner/slide `url` no longer requires an absolute URL — a leading `/`
internal path is accepted so they can link to tag pages.

SliderSeeder: create exactly one published slider per position (the
storefront takes the first published slider per position, so random
duplicates made the visible one nondeterministic) and delete via
`Slider::query()->delete()`, since Postgres refuses to TRUNCATE a table
that `slides` FK-references.
Tag pages: `GET /tags/{slug}` -> `TagController@show` + `Tags/Show.vue`.
A tag is an SEO landing page for a category and/or attribute filter, so
the page reuses the category machinery — `CollectCategoryIds` +
`GetCategoryProducts` + `GetCategoryFilters` — merging the tag's
attribute ids into the applied filters (OR within an attribute group,
AND across groups, same as the category page). For attribute-only tags
there is no category, so the controller passes an empty category-id list
and both shared actions now guard their category `whereIn` behind
`when($ids !== [])` ("no category constraint"); category pages always
pass ids, so they are unaffected. `app/Actions/Tag/BuildTagDetail`
(-> `TagDTO`, nullable category name/url) + `BuildTagBreadcrumbs`
(Home -> [Category …] -> Tag). SEO title/description/canonical/no_index
come from the tag; JSON-LD `BreadcrumbList`. Tests cover attribute
filtering, category-only, attribute-only (cross-category) and
multi-attribute AND.

Home: tags flagged `show_on_home` render as an image strip after the
category strip (`GetHomeTags` -> `tags` prop -> `Home/TagStrip.vue`),
each linking to its tag page.

Banners: `GetPromoBanners` becomes `GetBannersByPosition`, taking a
`BannerPositionEnum` mirrored from the admin enum, so the home grid asks
for `home-middle` instead of relying on an implicit convention — the
same pattern already used for slider positions.

Docs: TAGS.md rewritten as built; `home_sections` documented in the db
doc and flagged in both roadmaps as admin-only (the storefront still
renders a hardcoded home layout and does not read the table yet).
@bahman026
bahman026 merged commit 8421848 into main Aug 6, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant