diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..aeea612 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,72 @@ +# ARCHITECTURE + +## Platform principles + +- One codebase, unlimited tenants. No tenant-specific implementation code in the frontend. +- Tenant behavior is controlled entirely by configuration loaded at bootstrap (`GET /bootstrap`, tenant resolved server-side by domain). +- Prefer configuration over conditionals, composition over inheritance. +- Authentication, payment, and authorization contracts/behavior are frozen and must not be redesigned as part of platform work (`docs/architecture/foundation/adr/ADR-010-backward-compatibility-for-auth-payment-authorization.md`). +- No circular dependencies; shared/UI layers are feature-agnostic. + +These rules are enforced, not aspirational — see `docs/architecture/foundation/README.md` and the ADR set below, plus `npm run arch:check` (import-boundary + circular-dependency checks). + +## Architecture Decision Records (source of truth — read directly, do not treat this file as a paraphrase) + +All under `docs/architecture/foundation/adr/`: + +- **ADR-001** — platform model (multi-tenant, config-driven). +- **ADR-002** — layered feature architecture. +- **ADR-003** — import boundaries and dependency direction. +- **ADR-004** — configuration bootstrap and provider abstraction. +- **ADR-005** — dynamic page/section/widget rendering. +- **ADR-006** — UI component purity and container/facade pattern. +- **ADR-007** — state management and facade boundaries. +- **ADR-008** — theme engine and design-token runtime. +- **ADR-009** — feature flags and capability guards. +- **ADR-010** — backward compatibility for auth/payment/authorization. + +Companion standards docs (also `docs/architecture/foundation/`, kept as-is, enforced): `Coding-Standards.md`, `Naming-Conventions.md`, `Dependency-Rules.md`, `Folder-Blueprint.md`, `Import-Boundary-Matrix.md`, `State-Management-Standards.md`, `Configuration-Standards.md`, `Component-Standards.md`, `Service-Standards.md`. + +## Layered architecture + +``` +Component (container) --> Facade --> Domain Service --> Repository/Provider --> Mock | API +``` + +- **Container/page components** own routing, orchestration, and DI of a facade. They hold no business logic. +- **Presentational components** are `@Input()`/`@Output()`-only: no `HttpClient`, no storage, no environment access, no facade injection (ADR-006). The Project Editor's *sections* (`features/project-editor/sections/*`) are an accepted exception — they are container/section components, not shared presentational UI, so they may inject the facade directly (see `docs/EDITOR.md`). +- **Facades** (`facades/**`, or feature-local `facade/`) are the only thing components talk to. They expose signals/observables and imperative methods; they compose one or more domain services (ADR-007). +- **Domain services** (`core//*.service.ts`) convert backend DTOs into domain models via a **mapper**, and expose domain-shaped methods. DTOs never leak past the mapper boundary. +- **Repositories/providers** are swappable via injection tokens (e.g. `PRODUCT_DATA_PROVIDER`, `CATEGORY_REPOSITORY`, `BACKOFFICE_DATA_PROVIDER`, `ADMIN_DASHBOARD_METRICS_GATEWAY`) so mock and real-API implementations can be swapped without touching facades or components — the same pattern used throughout `core/`, `features/admin/*`, and `features/backoffice/*`. + +## Bootstrap / configuration engine + +- `ConfigService` loads `BootstrapConfig` (see `docs/BOOTSTRAP.md`) once at startup; `PlatformRuntimeService` applies it (theme, branding, runtime state) and can `reloadFromBootstrap()` for in-memory preview without a full page reload. +- The bootstrap is the single source of truth for pages, sections, widgets, theme, navigation, footer, static pages, and feature flags (ADR-004). +- The Project Editor mutates an in-memory draft of the same `BootstrapConfig` — there is no parallel editor-only model. + +## Dynamic page / section / widget rendering (ADR-005) + +Render pipeline: `page config -> section engine -> section renderer -> widget host -> registered widget component`. + +- **Section Engine** (`dynamic-renderer/section-engine/section-engine.service.ts`) builds an ordered page render model from `PageConfig.sections`, applying `order`, `layout` (`SectionLayoutConfig.strategy`: `stack | grid | hero | carousel | split`), and `visibility` (desktop/tablet/mobile). +- **Page Renderer** (`dynamic-renderer/page-renderer/page-renderer.service.ts`) delegates to the Section Engine. +- **Widget Host** (`dynamic-renderer/widget-host/widget-host.service.ts`) resolves each widget's component via the **Widget Manifest** (`widgets/registry/widget-manifest.service.ts`, `widgets/contracts/widget-manifest.contract.ts`) and its data via the **Data Source Resolver** (`widgets/resolvers/data-source-resolver.service.ts`), which delegates to `CategoryFacade`/`ProductFacade` — widgets never call APIs directly. +- Widgets receive only `{ section config, resolved data }` as inputs; they render presentation only, never fetch or mutate. +- Unknown/unregistered widget types render a safe fallback; this is also surfaced in `features/diagnostics` (dev-only, route `/__diagnostics`). +- `dynamic-page-layout.component.ts` (`layouts/containers/`) is the top-level container that composes Section Engine output using `PlatformLayoutConfig.type` (`default | sidebar-left | carousel-home | minimal`). + +## Theme engine (ADR-008) + +- `ThemeConfig` (`shared/models/config/theme.model.ts`): `themeId`, `mode` (`light | dark | system`), `palette` (12 semantic colors), `typography`, `spacing`, `borderRadiusScale`, `shadows`, `iconSet`. +- Applied as CSS custom properties at runtime; components/widgets consume tokens, never hardcoded brand colors. +- Three tenant theme stylesheets live under `src/styles/themes/*.theme.scss` — see `docs/FRONTEND.md` for the CSS custom property convention. + +## Feature flags / capability guards (ADR-009) + +- `bootstrap.featureFlags` (typed) plus the broader `bootstrap.features` (`MarketplaceFeaturesConfig`) surface for UI-facing toggles (wishlist, compare, reviews, recommendations, search history, etc.). +- Feature resolution falls back across older config surfaces to preserve behavior as the flag model evolved across sprints — see `docs/BOOTSTRAP.md` for the full field list. + +## Diagnostics (dev-only) + +`features/diagnostics/` (route `/__diagnostics`, excluded from production) validates bootstrap structure (missing fields, unknown widget types, duplicate ids, unknown layout values, missing translations) and runtime health (widget render failures, missing datasources), scored 0-100. Useful when investigating a bootstrap authored by the Project Editor. diff --git a/docs/BACKEND.md b/docs/BACKEND.md new file mode 100644 index 0000000..9547b6a --- /dev/null +++ b/docs/BACKEND.md @@ -0,0 +1,157 @@ +# BACKEND + +This is the punch list for backend engineers. Every item below is a frontend behavior that is currently **mocked or local-only**, with the exact gap, the endpoint(s) needed, and which frontend files change once the endpoint exists. Read this file instead of diffing the whole repo against `main`. + +General contract rules (apply to everything below): tenant is resolved by request host, never a path/query param (`docs/ARCHITECTURE.md`); bootstrap/config responses must never include secrets; existing auth/payment/authorization contracts are frozen (ADR-010) — nothing here changes them. + +## 1. Auth — admin authorization gap (highest priority, security-relevant) + +**Current frontend behavior:** Admin login and customer login share **one** Telegram QR/session backend (`{authApiUrl}/users/sessions`) and **one** UI component (`TelegramLoginComponent`, `mode: 'customer' | 'admin'`). The only client-side difference is *where the resulting session id is stored*: `webSessionID` cookie (`SameSite=Lax`) for customers vs. `adminSessionID` cookie (`SameSite=Strict`) for admin, via separate `AuthService` / `AdminAuthService`. Admin API calls attach `AdminWebSessionID` via `adminAuthHeadersInterceptor` (`core/admin-auth/admin-auth-headers.interceptor.ts`). + +**The gap:** because both flows hit the identical Telegram session endpoint, the backend has **no concept of "this is an admin session"** at the moment the QR is scanned — it's an ordinary Telegram user session, indistinguishable from a customer's. The frontend only decides where to *store* the resulting id; it cannot and does not decide whether that Telegram user is actually allowed to act as an admin. Any Telegram user who completes the QR flow while the admin login UI is showing gets an `adminSessionID`. + +**What's needed:** server-side authorization check keyed off the session id (or a dedicated admin-scoped token) at the point admin API calls arrive with `AdminWebSessionID` — reject non-admin users. This must be enforced server-side; nothing on the frontend can substitute for it. + +**Frontend files that would change:** `src/app/core/admin-auth/admin-auth.service.ts`, `admin-auth-headers.interceptor.ts`, `admin-auth.guard.ts` — only if the auth response shape changes (e.g. a role claim to check client-side in addition to the server-side enforcement). + +**Also prepared, not wired:** `core/admin-auth/ed25519-verification.model.ts` defines `Ed25519VerificationService` (challenge/verify) for a future non-Telegram admin auth path. Current binding (`NoopEd25519VerificationService`, in `app.config.ts`) throws rather than silently accepting — safe to leave wired until a real challenge/verify endpoint exists. + +## 2. Bootstrap draft / publish persistence (second-highest priority) + +**Current frontend behavior:** The Project Editor (`docs/EDITOR.md`) edits the same `BootstrapConfig` the storefront consumes. +- Load: `GET /bootstrap` (existing, tenant by host). +- Save: in-memory + `localStorage` only (`ProjectEditorDraftStorageService`, key `projectEditor.draftBootstrap.v1`, scoped by `tenant.id`). Reload in another browser/tab and it's gone. +- Publish: applies the bootstrap in-memory via `PlatformRuntimeService.reloadFromBootstrap()` and flips a local `status` flag. **No backend call.** + +**Endpoints needed** (none exist yet; shapes are contracts, adjust to house conventions): + +``` +GET /builder/bootstrap/draft + -> tenant's current draft BootstrapConfig. 404/empty if none (draft = published). + +PUT /builder/bootstrap/draft + body: BootstrapConfig + -> persists the draft for this tenant. Does not affect GET /bootstrap (storefront-facing). + +POST /builder/bootstrap/publish + body: BootstrapConfig (or none, if publish always promotes the stored draft) + -> validates, then makes this BootstrapConfig what GET /bootstrap returns. Only endpoint + that affects the live storefront. + +POST /builder/bootstrap/validate (optional) + body: BootstrapConfig + -> { code: string, message: string }[], mirroring ProjectValidationIssue shape client-side. +``` + +Tenant identity: resolved by request host, same as every other endpoint — no `projectId` path param. One domain = one tenant = one draft = one published bootstrap. + +**Don't duplicate client validation, enforce it server-side too.** `ProjectValidator` (`features/project-editor/services/project-validator.service.ts`) blocks Publish client-side on: missing `branding.logoUrl`, empty `localization.supportedLocales`, invalid `tenant.websiteBaseUrl` (must be `http(s)://...`), duplicate static-page identifiers (slug, falling back to `route`), empty homepage sections, a homepage widget with no `type`, duplicate header nav links, non-hex `theme.palette` values, missing translations for a supported locale, and unknown layout/section-layout enum values. A malicious or stale client can bypass all of this — if `POST /builder/bootstrap/publish` is a trust boundary, re-run equivalent checks server-side. + +**Frontend files that would change:** `features/project-editor/facade/project-editor.facade.ts` (replace local save/publish with the new endpoints, keep the same public method signatures), `services/project-editor-draft-storage.service.ts` (becomes a fallback/offline cache rather than primary store). + +## 3. Static pages / `slug` vs `route` inconsistency + +**Gap:** `StaticPageConfig` requires `slug: string`, but at least one real bootstrap fixture (`src/assets/mock/bootstrap/bootstrap.json`) only populates `route` (e.g. `/about-us`) and leaves `slug` undefined. The frontend's duplicate-detection was patched to fall back to `route` when `slug` is empty, but the underlying data inconsistency remains. + +**Needed:** decide whether `slug` is backend-required or auto-derived from `route`, so both sides agree on one source of truth going forward. + +**Frontend files:** `shared/models/config/static-page.model.ts`, `features/content-management/services/*` (`ContentPageService` normalization), `features/project-editor/services/project-validator.service.ts`. + +## 4. Static pages / CMS persistence + +**Current frontend behavior:** `staticPages` are edited client-side in the Project Editor (create/delete page, toggle footer/header/sitemap visibility, edit slug/icon/order/translations/HTML) and only ever written into the in-memory/`localStorage` bootstrap draft above (item 2) — no dedicated backend for CMS content exists. + +**Endpoints needed:** +``` +GET /builder/content-pages +PUT /builder/content-pages +POST /builder/content-pages/import +GET /builder/content-pages/export +POST /builder/content-pages/validate +``` +Backend must also support content moderation/validation on publish (disallow dangerous tags/attributes) and revision history — the frontend only sanitizes at render time, not at authoring time (`MarketplaceHtmlEditorComponent` emits raw HTML with no sanitization by design; sanitization is a storefront-render concern, not an authoring concern). + +**Frontend files:** `features/content-management/facade/*`, `services/*`, `components/html-editor/*`, `features/project-editor/sections/footer-section.component.ts` (static page list editing today lives partly here too). + +## 5. Categories + +**Current behavior:** `GET /category` (existing) mapped through `CategoryDto -> CategoryMapper -> Category` domain model, exposed via `CategoryFacade`. No editor/CRUD surface — `admin/categories` is a routing placeholder (`BackofficeComingSoonPageComponent`). + +**Gap:** no admin write path (create/update/delete/reorder categories) exists anywhere in this codebase. + +**Needed:** category CRUD endpoints and, ideally, a bulk reorder/visibility endpoint (category `priority`/`visible` are already read fields — see `docs/BOOTSTRAP.md`/domain report history). + +**Frontend files:** would need a new `features/admin/categories/` module mirroring `features/admin/products/` (gateway interface + local/API gateway + facade + pages), plus wiring `/​:lang/backoffice/categories` off the current coming-soon placeholder in `app.routes.ts`. + +## 6. Products + +**Current behavior:** `features/admin/products/` is fully built (list + editor pages) against `AdminProductsLocalGateway` (swappable via an injection token, same pattern as everywhere else) — i.e. it's ready for a real API gateway, but one has never been implemented. + +**Needed:** product CRUD endpoints matching the existing storefront product contract (`itemID`, `name`, `price`, `currency`, `categoryID`, `visible`, `discount`, `images`, `badges`, `media`, `specificationGroups`, `variantOptions`, `relatedCollections` — see the Product Engagement / Product Experience 2.0 fields folded from prior sprint reports). + +**Frontend files:** implement `AdminProductsApiGateway` alongside the existing `AdminProductsLocalGateway` and rebind the injection token — `features/admin/products/pages/*` and the facade do not change. + +## 7. Orders / revenue (does not exist at all) + +**Current behavior:** no backend or local data model for orders or revenue exists anywhere in the codebase. `features/backoffice/orders` is an empty placeholder. The Admin Dashboard's Orders and Revenue cards intentionally render a `pending-backend` state ("Awaiting backend integration") rather than fabricated numbers or a generic empty state. + +**Needed:** an order domain (creation, lifecycle, line items, totals) and revenue aggregation, plus endpoints to back a dashboard summary (see item 8) and an admin orders list/detail UI. + +**Frontend files:** `features/admin/dashboard/facade/admin-dashboard.facade.ts` (card status computation), a new `features/admin/orders/` module once the domain exists. + +## 8. Dashboard metrics + +**Current behavior:** `AdminDashboardMetricsGateway` (token `ADMIN_DASHBOARD_METRICS_GATEWAY`) defaults to `AdminDashboardMetricsLocalGateway`, which composes `BackofficeDataService.loadCategories()/loadProducts()` client-side into counts. Everything else on the dashboard (marketplace status, theme, languages, last publish/save, bootstrap version, active layout, enabled widgets, system health) is derived from `ProjectEditorFacade` state, not a metrics endpoint. + +**Needed:** a `/builder/dashboard/summary`-style endpoint returning real-time counts and trend deltas. + +**Frontend files:** implement `AdminDashboardMetricsGateway` (real API version) and rebind the token in DI config — `AdminDashboardFacade` and all dashboard card components are unaffected (`docs/ADMIN.md` has the full architecture). + +## 9. Recent Activity (admin dashboard) + +**Current behavior:** `AdminDashboardHistoryService` is `localStorage`-backed, scoped per tenant (`adminDashboard.activityHistory.v1`) — same limitation pattern as the draft storage in item 2. It will never show another editor's activity. + +**Needed:** a real audit-log endpoint (who changed what, when) that multiple admin sessions can read. + +**Frontend files:** `features/admin/dashboard/services/admin-dashboard-history.service.ts`, `facade/admin-dashboard.facade.ts`. + +## 10. Translations + +**Current behavior:** all UI strings are static, compiled into the three locale files (`src/app/i18n/{en,ru,hy}.ts`) via the `Translations` interface (`translations.ts`) and served by `TranslateService`/`TranslatePipe`. There is no backend-editable translation surface — bootstrap-level translatable content (nav labels, static-page translations, footer copyright) is edited per-tenant through the Project Editor and stored in the bootstrap itself (see item 2), which is separate from the compiled UI-chrome strings. + +**Gap:** UI-chrome strings (button labels, section titles, validation messages) require a frontend deploy to change or add a locale — there's no backend-driven UI-string catalog. This may be acceptable (frontend chrome vs. tenant content are different concerns) but is worth an explicit decision if a backend team expects to control all copy. + +**Frontend files:** `src/app/i18n/*` (if this becomes backend-driven, it's a new i18n loading mechanism, a larger change than any other item here). + +## 11. Search / autocomplete / trending (future-ready, not urgent) + +**Current behavior:** in-memory products/categories/tags power autocomplete and suggestions (`features/search/services/search-autocomplete.service.ts`); trending returns `null` (UI hides gracefully); search history is `LocalSearchHistoryRepository` (guest) with a `BackendSearchHistoryRepository` placeholder already defined but unimplemented. + +**Endpoints that would let this go live without frontend changes:** +``` +GET /search/suggestions?q={term} +GET /catalog/filters?category={id}&q={term} +``` +Plus, if authenticated history/wishlist/compare/saved-searches sync is wanted: `GET/POST/DELETE /me/wishlist`, `/me/compare`, `/me/saved-searches`, `/me/recently-viewed`. + +**Frontend files:** `features/search/services/search-autocomplete.service.ts`, `search-trending.service.ts`, `search-history.repository.ts` (swap `LocalSearchHistoryRepository` for `BackendSearchHistoryRepository`) — facade/UI unaffected by design. + +## 12. Product engagement (rating/reviews/questions) — partially real, confirm contract + +**Current behavior:** frontend already expects these endpoints and renders against them: `GET /products/{id}/rating`, `GET /products/{id}/reviews?page&pageSize`, `GET /products/{id}/questions?page&pageSize`, `POST /products/{id}/reviews` (rating/title/text/anonymous), `POST /products/{id}/questions` (text/anonymous). If these already exist, this is a contract-confirmation item, not new work; if not, they block the Reviews/Questions UI (`productPage.reviews`/`questions` config, already toggleable per tenant). + +**Frontend files:** none, if the contract matches what's documented in `docs/BOOTSTRAP.md`'s Product Engagement notes. + +--- + +## Known reliability issues + +### Production 502/504 Bad Gateway on refresh / back-navigation + +**Symptom:** production hits intermittent `502`/`504 Bad Gateway` responses specifically on page refresh and browser back-navigation. + +**Root cause (investigation so far):** `environment.production.ts` points the frontend at the backend API via **absolute URLs directly** (`apiUrl: 'https://api.dexarmarket.ru:445'`, `authApiUrl: 'https://users.vitanova.network:456'`), bypassing this repo's `nginx.conf` entirely — that config only proxies `/api` for the `lovero.store` tenant, not `dexarmarket.ru`. So the 502/504 originates from **that backend API's own reverse proxy** (ports 445/456, a separate server not in this repo), not from anything this repo controls. + +Refresh and back-navigation both re-fire session-check and bootstrap-load calls on mount (`AdminAuthService.checkSession()`, `ConfigService.loadBootstrap()`, `TelegramSessionApiService`), which is the likely trigger if that backend's app server or reverse proxy is crashing, overloaded, or misconfigured on those specific endpoints. + +**This is not fixable from this repo.** It needs DevOps/backend investigation on the reverse proxy and app server fronting `api.dexarmarket.ru:445` and `users.vitanova.network:456` — check upstream health, timeout settings, and concurrent-connection handling around session-check and bootstrap endpoints. diff --git a/docs/BOOTSTRAP.md b/docs/BOOTSTRAP.md new file mode 100644 index 0000000..4c13ff5 --- /dev/null +++ b/docs/BOOTSTRAP.md @@ -0,0 +1,153 @@ +# BOOTSTRAP + +The `BootstrapConfig` model (`src/app/shared/models/config/bootstrap-config.model.ts`) is the single JSON contract that drives the entire storefront for a tenant. `ConfigService` loads it once at startup via `GET /bootstrap` (tenant resolved server-side by request host); `PlatformRuntimeService` applies it and can re-apply an edited in-memory copy for preview. + +## Top-level shape + +```ts +interface BootstrapConfig { + schemaVersion: string; + generatedAt: string; + tenant: TenantConfig; // required + branding: BrandingConfig; // required + theme: ThemeConfig; // required + company: CompanyConfig; // required + featureFlags: FeatureFlagsConfig; // required + features?: MarketplaceFeaturesConfig; // optional, centralized feature toggles + apiEndpoints: ApiEndpointsConfig; // required + localization: LocalizationConfig; // required + seo: SeoConfig; // required + permissions: PermissionsConfig; // required + header?: HeaderConfig; + catalog?: CatalogConfig; + layout?: PlatformLayoutConfig; + navigation: NavigationConfig; // required + footer?: FooterConfig; + productPage?: ProductPageConfig; + userExperience?: UserExperienceConfig; + pages: PageConfig[]; // required + staticPages?: StaticPagesConfig; + widgetRegistry?: WidgetRegistryConfig; +} +``` + +## Field-by-field + +| Key | Meaning | +|---|---| +| `schemaVersion` | Contract version. Breaking changes require a bump; frontend must stay compatible within a minor line. | +| `generatedAt` | Payload generation timestamp. | +| `tenant` | `id`, `slug`, `host`, `name`, `defaultLocale`, `supportedLocales`, `defaultCurrency`, `websiteBaseUrl`, etc. Tenant is resolved by domain only — never re-derived on the frontend. | +| `branding` | `logoUrl`, `logoCompactUrl`, `faviconUrl`, `brandName`. | +| `theme` | `themeId`, `mode` (`light\|dark\|system`), `palette` (12 semantic colors), `typography`, `spacing`, `borderRadiusScale`, `shadows`, `iconSet`. See `docs/ARCHITECTURE.md` theme engine section. | +| `company` | Legal/contact info used in the footer: `companyName`, `address`, `contacts.phone`/`email`. | +| `featureFlags` | Simple boolean toggles: `wishlist`, `compare`, `reviews`, `comments`, `recommendations`, etc. | +| `features` | Newer centralized feature surface (`MarketplaceFeaturesConfig`) — wishlist/compare/reviews/comments/questions/recommendations/recentlyViewed/searchHistory/recentlySearched/ratings/share/brands/manufacturers/availability/discounts/badges. Resolvers fall back to `featureFlags`/`productPage`/`userExperience`/`catalog` for older bootstraps. | +| `apiEndpoints` | Public endpoint map the frontend's API layer reads (base URLs, paths, timeouts). Never contains secrets. | +| `localization` | `defaultLocale`, `supportedLocales`, optional `currencyByLocale`. | +| `seo` | `default.title`/`default.description` plus per-page SEO overrides. | +| `permissions` | Roles/permissions for admin surfaces (currently minimal; see `docs/BACKEND.md`). | +| `header` | Boolean toggles: `showLogo`, `showSearch`, `showCategories`, `showLanguages`, `showCart`, `showProfile`, `showWishlist`, `showCompare`, `showRegion`. | +| `catalog` | UI/feature config only (no product data) — see Catalog Config below. | +| `layout` | `PlatformLayoutConfig`: `{ type: 'default'|'sidebar-left'|'carousel-home'|'minimal', options?: Record }`. Global page-chrome mode. | +| `navigation` | `header[]` / `footer[]` link arrays: `id`, label (translatable), `route`, `order`, `visible`. | +| `footer` | Payment icons, social links, copyright (per-locale), static-page references. | +| `productPage` | Feature config only for the product detail page — rating/reviews/questions/tabs/relatedProducts/actions enablement, pagination size, mode. No review/question *data* lives here. | +| `userExperience` | Feature config only for wishlist/compare/recentlyViewed/share/savedSearches — flags and limits, never user-specific lists. | +| `pages` | Array of `PageConfig`: `id`, `key`, `route.path`, `sections: SectionConfig[]`. | +| `staticPages` | CMS-style informational/legal pages — see `staticPages` below. | +| `widgetRegistry` | Pointer/metadata for the widget manifest (see `docs/ARCHITECTURE.md` widget engine). | + +### Section config (`shared/models/config/section.model.ts`) + +```ts +type SectionLayoutStrategy = 'stack' | 'grid' | 'hero' | 'carousel' | 'split'; + +interface SectionConfig { + id: string; type: string; order: number; + layout?: { strategy?: SectionLayoutStrategy; columns?: number; gap?: string; align?: 'start'|'center'|'end'|'stretch' }; + visibility?: { desktop?: boolean; tablet?: boolean; mobile?: boolean }; + widgets: WidgetConfig[]; + featureFlag?: string; + visible?: boolean; +} +``` + +### Widget config (`shared/models/config/widget.model.ts`) + +Each widget has `id`, `type`, `version`, optional `title`/`subtitle`/`order`/`padding`/`visibility`/`animation`/`style`/`permissions`/`actions`/`featureFlag`/`visible`, and `props: Record` (widget-specific). Typed editors exist in the Project Editor for `hero`, `categories`, `product-collection`; everything else edits `props` as raw JSON (see `docs/EDITOR.md`). + +### Catalog Config (`shared/models/config/catalog-config.model.ts`) + +```ts +type CatalogLayoutModeConfig = 'grid'|'large-grid'|'compact-grid'|'grid-2'|'grid-3'|'grid-4'|'compact'|'list'; +type CatalogNavigationModeConfig = 'default'|'left-category-navigation'|'mega-category-layout'|'top-category-carousel'; +type CatalogLoadingStrategy = 'pagination'|'loadMore'|'infiniteScroll'; +``` +Plus `defaultSort`/`availableSorts` (`relevance|latest|price_asc|price_desc|rating|popular|discount`), `enabledFilters: string[]`, and `show*`/`*Enabled` booleans (breadcrumbs, category banner, subcategory chips, ratings, discounts, availability, suggestions, search history). This is feature-configuration only — no product/filter *data* lives in bootstrap. + +### `staticPages` + +Each entry: `id`, `slug`, `title`, `showInHeader`, `showInFooter`, `showInSitemap`, `icon`, `order`, `visibility`, `requiresAuthentication`, `footerGroup`, `translations[locale] = { title, html, seo }`. Rendered dynamically (no hardcoded page list); HTML is sanitized on render. **Known inconsistency:** the model requires `slug`, but the mock bootstrap only populates `route` for some pages — the frontend's duplicate-slug validator falls back to `route` when `slug` is empty (see `docs/BACKEND.md`). + +## Representative example (trimmed) + +```json +{ + "schemaVersion": "2.1.0", + "generatedAt": "2026-07-05T10:30:00Z", + "tenant": { + "id": "tenant-dexar-ru", "slug": "dexar-ru", "host": "dexarmarket.ru", + "name": "Dexar Market", "defaultLocale": "ru", "supportedLocales": ["ru", "en", "hy"], + "defaultCurrency": "RUB", "websiteBaseUrl": "https://dexarmarket.ru" + }, + "branding": { "logoUrl": "/assets/brand/logo.svg", "faviconUrl": "/assets/brand/favicon.ico", "brandName": "Dexar Market" }, + "theme": { + "themeId": "dexar-light", "mode": "light", + "palette": { "primary": "#2F6E5D", "secondary": "#8FA9A2", "backgroundPrimary": "#FFFFFF", "textPrimary": "#1F322D" }, + "typography": { "primaryFontFamily": "DM Sans, sans-serif", "baseFontSize": 16 } + }, + "layout": { "type": "default" }, + "catalog": { "layout": "grid-4", "navigationMode": "default", "defaultSort": "relevance", "showRatings": true }, + "navigation": { + "header": [ { "id": "nav-home", "label": "Home", "route": "/", "order": 1, "visible": true } ], + "footer": [ { "id": "nav-privacy", "label": "Privacy", "route": "/privacy-policy", "order": 1, "visible": true } ] + }, + "pages": [ + { + "id": "page-home", "key": "home", "route": { "path": "/", "exact": true }, + "sections": [ + { + "id": "home-hero", "type": "hero", "order": 1, + "layout": { "strategy": "hero" }, + "widgets": [ { "id": "w-hero", "type": "hero", "version": "1.0.0", "props": { "title": "Welcome", "layout": "full-bleed" } } ] + }, + { + "id": "home-categories", "type": "categories", "order": 2, + "layout": { "strategy": "grid", "columns": 4 }, + "widgets": [ { "id": "w-categories", "type": "categories", "version": "1.0.0", "props": { "columns": 4 } } ] + } + ] + } + ], + "staticPages": [ + { "id": "static-about", "slug": "about", "title": { "en": "About Us" }, "showInFooter": true, "showInHeader": false, "translations": { "en": { "html": "

About Us

" } } } + ] +} +``` + +## How `ConfigService` / `PlatformRuntimeService` consume it + +1. `ConfigService.loadBootstrap()` fetches (or, in mock mode, reads local JSON under `src/assets/mock/bootstrap/`) and parses `BootstrapConfig`. +2. `PlatformRuntimeService` applies theme tokens as CSS variables, sets branding, and exposes the parsed pages/navigation/footer/static-pages to the rest of the app. +3. Section Engine / Widget Host render pages from `bootstrap.pages` on route match (see `docs/ARCHITECTURE.md`). +4. `PlatformRuntimeService.reloadFromBootstrap(next)` re-applies an entire new `BootstrapConfig` in-memory — this is what the Project Editor's Publish action (and Preview) use, without a full browser reload. + +## How the Project Editor edits it, and draft/publish/preview + +The Project Editor (`docs/EDITOR.md`) edits an in-memory copy of the exact same `BootstrapConfig` — there is no parallel editor model or DTO translation layer. Today: + +- **Load**: `GET /bootstrap` (same endpoint the storefront uses). +- **Save**: in-memory snapshot only, persisted to `localStorage` as a draft (`projectEditor.draftBootstrap.v1`, scoped by `tenant.id`) so it survives reloads on the same browser. +- **Publish**: runs `ProjectValidator`, then calls `PlatformRuntimeService.reloadFromBootstrap()` for live in-memory preview and flips a local `status` flag — **no backend call happens**. This is the largest gap covered in `docs/BACKEND.md`. +- **Preview**: same in-memory re-apply mechanism, without marking the state published. diff --git a/docs/Backend-Handoff-Sprint16.md b/docs/Backend-Handoff-Sprint16.md deleted file mode 100644 index 77d9ceb..0000000 --- a/docs/Backend-Handoff-Sprint16.md +++ /dev/null @@ -1,90 +0,0 @@ -# Backend Handoff — Sprint 16 (Project Editor) - -For backend devs picking up work after the Sprint 16 frontend editor. Frontend -is done; this documents what backend still needs to build for the editor to -be real (not just an in-browser demo). - -## What exists today (frontend-only) - -The Project Editor (`/edit/:section`, or `/{lang}/edit/:section`) edits the -same `BootstrapConfig` the storefront consumes — no parallel model. Today: - -- **Load:** `GET /bootstrap` (existing, tenant resolved by request host). -- **Save:** in-memory only. `ProjectEditorFacade.save()` just snapshots the - current draft as "last saved" in the browser tab. Nothing is persisted. - Reload the page, or open the editor in another tab/browser, and it's gone. -- **Publish:** `ProjectEditorFacade.publish()` applies the bootstrap - in-memory via `PlatformRuntimeService.reloadFromBootstrap` (for live - preview) and flips a local `status` flag to `'published'`. It does not - call any backend endpoint. Nothing is persisted. - -This is fine for demoing the editor UI to one person in one browser tab. It -is not usable as a real per-tenant admin panel yet — that's this handoff. - -## Endpoints backend needs to add - -None of these exist yet. Suggested shapes (adjust to match your existing API -conventions — these are contracts, not prescriptions): - -``` -GET /builder/bootstrap/draft - -> returns the tenant's current draft BootstrapConfig (may differ from - the published one). 404/empty if no draft exists yet (draft = published). - -PUT /builder/bootstrap/draft - body: BootstrapConfig - -> persists the draft for this tenant. Does not affect what GET /bootstrap - (storefront-facing) returns. - -POST /builder/bootstrap/publish - body: BootstrapConfig (or no body, if publish always promotes the - current stored draft) - -> validates, then makes this BootstrapConfig the one GET /bootstrap - returns for this tenant. This is the only endpoint that affects the - live storefront. - -POST /builder/bootstrap/validate (optional — validation already runs - client-side via ProjectValidator, but a - server-side check prevents a stale/ - bypassed client from publishing garbage) - body: BootstrapConfig - -> returns the same shape as the client's ProjectValidationIssue[]: - { code: string, message: string }[] -``` - -Tenant identity: same as every other endpoint in this platform — resolved by -request host, not a `projectId` path param (see `docs/backend-platform/ -tenant-resolution.md`). There is no multi-project-per-domain concept; each -domain is one tenant with one draft and one published bootstrap. - -## What the frontend already validates (don't duplicate logic, just enforce it) - -`ProjectValidator` (`src/app/features/project-editor/services/ -project-validator.service.ts`) blocks Publish client-side on: -missing `branding.logoUrl`, empty `localization.supportedLocales`, invalid -`tenant.websiteBaseUrl` (must be `http(s)://...`), duplicate static-page -identifiers (slug, falling back to route), empty homepage sections, a -homepage widget with no `type`, duplicate header nav links, and any -non-hex-string `theme.palette` value. A malicious or buggy client could -bypass all of this — if `POST /builder/bootstrap/publish` is meant to be a -trust boundary, re-run equivalent checks server-side before accepting. - -## Static pages: `slug` vs `route` - -Heads up for whoever owns `StaticPageConfig`: the model requires `slug: -string`, but at least one real bootstrap in this repo -(`src/assets/mock/bootstrap/bootstrap.json`) only populates `route` (e.g. -`/about-us`) and leaves `slug` undefined. The frontend's duplicate-detection -was patched to fall back to `route` when `slug` is empty -(`project-validator.service.ts`), but the underlying data inconsistency is -still there. Worth deciding whether `slug` should be backend-required/ -auto-derived from `route` going forward, so both frontend and backend agree -on one source of truth. - -## Not in scope for this handoff (already tracked separately) - -- Separate admin app/deployment (`admin.` subdomain) — storefront and - editor still ship in one Angular build today. -- A pre-existing, unrelated crash in `ContentPageService.normalizeSlug` - against legacy-shaped static-page fixture data (frontend bug, not a - backend concern). diff --git a/docs/Catalog-Module-Report.md b/docs/Catalog-Module-Report.md deleted file mode 100644 index 35c3ec8..0000000 --- a/docs/Catalog-Module-Report.md +++ /dev/null @@ -1,357 +0,0 @@ -# Catalog Module Report - -## Scope - -Sprint 5 added a Catalog Module on the frozen platform architecture. No backend APIs, authentication, payment, or bootstrap contracts were changed. - -The catalog uses existing domain boundaries: - -- Category data: `CategoryFacade` -> `CategoryService` -> Category Repository -> existing `GET /category` -- Product data: `ProductFacade` -> `ProductDataService` -> Product Provider -> existing product/category item endpoints - -## Implemented Module - -### Catalog Container - -- `src/app/features/website/catalog/containers/catalog-container.component.ts` -- `src/app/features/website/catalog/containers/catalog-container.component.html` -- `src/app/features/website/catalog/containers/catalog-container.component.scss` - -Responsibilities implemented: - -- Reads the route category id. -- Requests category data through `CategoryFacade` only. -- Requests product data through `ProductFacade` only. -- Determines whether the current category has child categories. -- Renders category grid when child categories exist. -- Renders product grid when no child categories exist. -- Supports root catalog entry with root categories. -- Handles loading, empty, and error states. -- Cancels prior category/product data subscriptions when the route changes. - -No `HttpClient`, backend DTO, auth, payment, bootstrap, or tenant-specific logic is used in the container. - -### Category Grid - -- `src/app/features/website/catalog/components/category-grid/category-grid.component.ts` -- `src/app/features/website/catalog/components/category-grid/category-grid.component.html` -- `src/app/features/website/catalog/components/category-grid/category-grid.component.scss` - -Reusable category grid implemented with: - -- Input: `Category[]` -- Output: selected `Category` -- Responsive grid layout -- Domain model only -- No data fetching -- No backend DTOs - -### Product Grid - -- `src/app/features/website/catalog/components/product-grid/product-grid.component.ts` -- `src/app/features/website/catalog/components/product-grid/product-grid.component.html` -- `src/app/features/website/catalog/components/product-grid/product-grid.component.scss` - -Reusable product grid implemented with: - -- Input: `Product[]` -- Output: selected `Product` -- Output: add-to-cart payload -- Output: product preview id -- Responsive grid layout -- Uses existing reusable product card -- No `HttpClient` -- No backend DTOs - -### Product Card Compatibility - -- `src/app/components/product-card/product-card.component.ts` -- `src/app/components/product-card/product-card.component.html` - -Updated the reusable product card to depend on the Product Domain type and added an explicit selected output. - -The product card remains input/output-only and does not use services, storage, `HttpClient`, or environment configuration. It displays image, title, price, discount, badges, and stock. - -### Catalog State - -- `src/app/features/website/catalog/models/catalog-state.model.ts` - -Prepared future state architecture for: - -- Category -- Search -- Sort -- Price range -- Attributes -- Pagination -- Filters - -Backend filtering was intentionally not implemented in this sprint. - -## Navigation - -Updated routes in `src/app/app.routes.ts`: - -- `/catalog` -- `/catalog/:id` - -Both routes load the same catalog container. Legacy category URLs redirect to the catalog route: - -- `/category/:id` -> `/catalog/:id` -- `/category/:id/items` -> `/catalog/:id` - -Home category links now point to `/catalog/:id`. - -## Unlimited Category Depth - -Unlimited nesting is supported by the Category Domain tree utilities from Sprint 4. The catalog container does not assume a fixed depth. For any category id, it asks `CategoryFacade.getChildren(categoryId)`: - -- if children exist, it renders the category grid -- if no children exist, it loads the product grid - -This same decision repeats for every category route depth. - -## Localization - -Added catalog translations in: - -- `src/app/i18n/en.ts` -- `src/app/i18n/ru.ts` -- `src/app/i18n/hy.ts` -- `src/app/i18n/translations.ts` - -## Validation - -Completed checks: - -- Unlimited category depth is supported through facade child lookup and recursive category domain tree utilities. -- Product grid is reusable and consumes `Product[]`. -- Category grid is reusable and consumes `Category[]`. -- Catalog components use domain models only. -- Catalog data requests go through facades only. -- DTOs remain isolated outside the catalog module. -- Catalog module has no `HttpClient` usage. -- Product card has no services, storage, `HttpClient`, or environment usage. -- Authentication was not modified. -- Payment was not modified. -- Bootstrap contracts were not modified. -- Backend APIs were not modified. - -Build validation passed: - -```bash -npm run build -``` - -## Stop Point - -Catalog Module implementation is complete for Sprint 5. Stop here for approval before starting the next module or any Builder/Backoffice work. - -## Sprint 10.2 Catalog UX Polish - -Sprint 10.2 improves catalog UX and responsiveness without changing facades, business logic, bootstrap flow, runtime architecture, routing, authentication, or payment. - -### Empty State Behavior - -Two separate states are now rendered in the catalog container: - -- Empty category state (`rawProducts.length === 0`): - - hides filter/sort/layout/result controls - - shows dedicated empty category component with icon, category context, friendly message, and "Browse Categories" action -- Filtered empty state (`rawProducts.length > 0 && products.length === 0`): - - shows "no filter match" message - - provides "Clear Filters" action - - keeps filter access available (sidebar on desktop, drawer trigger on tablet/mobile) - -### Mobile Filter Drawer - -- Desktop keeps visible sticky sidebar filters. -- Tablet and mobile switch to a drawer-based filter UI. -- Drawer includes filter groups, Reset, and Apply actions. -- Apply closes the drawer. -- Accessibility: - - drawer uses dialog semantics (`role="dialog"`, `aria-modal="true"`) - - focus trap is enabled while drawer is open - - `Esc` closes the drawer - -### Mobile Sort - -- Desktop keeps dropdown sort control. -- Tablet keeps compact dropdown with drawer-based filters. -- Mobile opens a bottom-sheet sort modal. -- Supported mobile sort options: - - Recommended - - Newest - - Price Low -> High - - Price High -> Low - - Rating - - Popularity - -### Responsive Grid Modes and Toolbar - -- Grid selector uses icon buttons and keeps active-state highlighting. -- Mobile sticky toolbar added with quick actions: - - Filters - - Sort - - Grid cycle -- Grid cycle rotates through supported layouts while preserving existing layout architecture. - -### Responsive Spacing and Overflow - -Catalog spacing and controls were polished for desktop/tablet/mobile: - -- filter/input/button spacing -- sort/reset row behavior (single row on desktop, stacked naturally on mobile) -- card and grid spacing -- search block spacing -- drawer/sheet interaction surfaces -- horizontal overflow prevention - -## Sprint UI Polish (Visual Only) - -This sprint applies visual and responsive UX polish only. No facade contracts, business logic, API contracts, runtime/bootstrap architecture, or widget contracts were changed. - -### Desktop Layout - -- Catalog products section uses a cleaner two-column structure with consistent spacing tokens (8/12/16/24/32). -- Filters panel remains sticky on desktop and uses collapsible sections with smooth expand/collapse animation. -- Product cards keep consistent image height/aspect ratio and improved vertical rhythm between image/title/rating/price/actions. -- Product action controls are vertical floating circles in the image top-right zone with fixed spacing and no overlap. - -### Tablet Layout - -- Sidebar filters transition into drawer interaction for better content width. -- Sort and layout controls retain consistent sizing and spacing. -- Grid/list results avoid horizontal overflow and preserve button/input containment. - -### Mobile Layout - -- Permanent sidebar is hidden. -- Sticky toolbar provides three entry points: Filters, Sort, Grid. -- Filters open in drawer form with scrollable content and fixed bottom actions. -- Sort and Grid open bottom-sheet style dialogs. -- Focus states and keyboard dismissal (`Esc`) are preserved for all overlays. - -### Grid Types - -- `grid` -- `large-grid` -- `compact-grid` -- `list` - -All grid switch icons are normalized in size and selected state is visually highlighted. - -### Filter Drawer and Sections - -- Filter groups (Price, Availability, Rating, Brand, etc.) are collapsible. -- Range inputs are stacked vertically (`From`, `To`) with full-width controls and 12px+ spacing. -- Slider remains below price inputs for predictable scan order. - -### Product Card Anatomy - -- Image area: square ratio, `object-fit: contain`, padded image content. -- Status elements (discount/stock/badges) positioned to avoid action collisions. -- Actions: top-right vertical controls with equal circular dimensions. -- Content: title, optional description, rating, pricing, stock indicator, CTA. - -### Animations - -- Card hover: subtle elevation + `translateY(-2px)`. -- Button/selector transitions: ~180-200ms. -- Filter group expand/collapse: smooth height/opacity transition. -- Drawer and sheet overlays: subtle slide/fade entrance. - -### Empty and Loading States - -- Empty results state keeps friendly message and action while hiding non-essential catalog controls when no products are rendered. -- Skeletons for cards/results keep stable heights to reduce layout shift. - -### Accessibility Notes - -- Added/standardized visible `:focus-visible` outlines for interactive elements. -- Product quick action controls now expose aria labels. -- Modal/drawer interactions continue to use dialog semantics and focus trap. - -### Localization and Accessibility - -- New strings for empty states, drawer/sheet UI, and toolbar were added to all languages: - - `src/app/i18n/en.ts` - - `src/app/i18n/ru.ts` - - `src/app/i18n/hy.ts` - - `src/app/i18n/translations.ts` -- No hardcoded catalog UX strings were introduced for Sprint 10.2 additions. - -## Sprint 11 Search & Discovery Engine - -Sprint 11 introduces a reusable, backend-driven Search and Discovery architecture while preserving platform boundaries and existing domain models. - -### Search Domain Models - -Core search models were added under: - -- `src/app/core/search/models/search.model.ts` -- `src/app/core/search/models/search-state.model.ts` - -Model coverage includes: - -- `SearchQuery` -- `SearchResult` -- `FilterGroup` -- `FilterOption` -- `SortOption` -- `SearchSuggestion` -- `SearchHistory` -- `SearchState` - -### Search Entry Point - -- `src/app/facades/platform/search.facade.ts` - -`SearchFacade` is now the search orchestration entry point for the catalog UX and provides: - -- backend catalog loading bridge for search query payloads -- metadata-driven sort option generation -- metadata-driven dynamic filter group generation -- live suggestions generation -- in-memory filter metadata memoization -- reusable filtering, sorting, and pagination helpers -- query param serialization/deserialization for URL synchronization - -### History Service - -- `src/app/core/search/services/search-history.service.ts` - -Search history moved to a reusable core service with: - -- recent search tracking -- popular search support -- clear/reset support - -### Catalog UI Integration - -Catalog UI now consumes Search domain metadata and state: - -- `src/app/features/website/catalog/containers/catalog-container.component.ts` -- `src/app/features/website/catalog/components/search-box/search-box.component.ts` -- `src/app/features/website/catalog/components/filters-panel/filters-panel.component.ts` -- `src/app/features/website/catalog/components/sorting-control/sorting-control.component.ts` - -Implemented behaviors: - -- live suggestions -- recent + popular searches -- keyboard navigation in search box (up/down/enter/escape) -- clear search action -- dynamic filters for checkbox, radio, toggle, range, slider, color, size, rating, availability -- URL query synchronization with `SearchState` -- page reload restore from query params - -### Validation - -Completed validation for Sprint 11 integration: - -- `npm run build` passes successfully -- facades remain the UI data boundary -- no authentication changes -- no payment changes -- no bootstrap/runtime contract changes diff --git a/docs/Catalog-UX-Architecture.md b/docs/Catalog-UX-Architecture.md deleted file mode 100644 index 3f0a17b..0000000 --- a/docs/Catalog-UX-Architecture.md +++ /dev/null @@ -1,104 +0,0 @@ -# Catalog UX, Navigation and Loading Strategies - Sprint 16 - -## Scope - -Sprint 16 improves catalog UX without introducing marketplace-specific logic. - -Areas covered: -- empty category behavior -- root navigation consistency -- multiple loading strategies -- grid selector completion -- mobile catalog behavior -- future slug routing preparation -- reusable catalog states -- centralized feature flags -- project skills documentation - -## Empty Category Behavior - -Catalog now distinguishes three category outcomes: -- subcategories exist: show category browser -- products exist: show product list -- neither exist: show dedicated catalog empty state - -Empty category state belongs to catalog surface, not product grid. - -## Root Navigation - -`All Categories` always routes to `/catalog` and shows category browser. - -Continue-browsing restoration no longer hijacks this root navigation path. - -## Loading Strategies - -Configured via `catalog.loadingStrategy`: -- `pagination` -- `loadMore` -- `infiniteScroll` - -Single product list component remains source of truth. Strategy changes only affect controls and page-windowing. - -## Grid System - -Supported layouts: -- `grid-2` -- `grid-3` -- `grid-4` -- `list` -- `compact` - -User preference persists locally. Bootstrap default still seeds first render. - -Legacy layout aliases normalize to new modes for backward compatibility. - -## Mobile Behavior - -Mobile catalog uses: -- filter drawer -- sort popup sheet -- grid popup sheet - -Inline filter density is avoided. - -## Breadcrumb and Slug Preparation - -Current URLs remain ID-based. - -Routing layer now tolerates future slug-like category tokens by resolving them to internal IDs without changing current public contract. - -## Centralized Features - -`bootstrap.features` is new central toggle surface for UI features such as: -- wishlist -- compare -- reviews -- comments -- questions -- recommendations -- recentlyViewed -- searchHistory -- recentlySearched -- ratings -- share -- brands -- manufacturers -- availability -- discounts -- badges - -Feature resolver falls back to older config surfaces to preserve behavior. - -## Project Skills - -Added repo skills: -- `.agents/skills/marketplace-architecture/SKILL.md` -- `.agents/skills/ui-standards/SKILL.md` -- `.agents/skills/backend-contract/SKILL.md` - -## Future Work - -- true backend paging for load-more/infinite strategies -- offline-aware cached catalog data -- explicit slug field on categories -- admin editing surface for centralized feature toggles diff --git a/docs/Category-Domain-Report.md b/docs/Category-Domain-Report.md deleted file mode 100644 index cadd6ab..0000000 --- a/docs/Category-Domain-Report.md +++ /dev/null @@ -1,155 +0,0 @@ -# Category Domain Report - -## Scope - -Sprint 4 added a complete Category Domain on top of the existing backend API contract. Backend endpoints and payload names were not changed. - -Existing backend category fields remain isolated as DTO input: - -- `categoryID` -- `parentID` -- `name` -- `icon` -- `priority` -- `visible` -- `categoriesCount` -- `itemCount` -- `names[]` - -The UI now consumes category domain models rather than backend-shaped category responses. - -## Implemented Files - -### DTO - -- `src/app/core/categories/dto/category.dto.ts` - -Defines `CategoryDto` and `CategoryNameDto` for existing backend category payloads. Compatibility fields for current mock/API variants are accepted only at the DTO boundary. - -### Domain Model - -- `src/app/core/categories/models/category-domain.model.ts` - -Frontend category model exposes: - -- `id` -- `parentId` -- `title` -- `icon` -- `priority` -- `visible` -- `itemCount` -- `children[]` -- `translations` - -No backend category naming is required by category UI consumers. - -### Mapper - -- `src/app/core/categories/mappers/category.mapper.ts` - -Maps backend DTOs into domain categories, including: - -- backend id normalization -- parent id normalization -- title fallback selection -- `names[]` to `translations` -- nested DTO flattening -- visible-category filtering -- priority sorting -- duplicate id de-duplication - -### Tree Utilities - -- `src/app/core/categories/utils/category-tree.utils.ts` - -Supports: - -- flat list to tree -- unlimited nesting -- parent lookup -- children lookup -- breadcrumb generation -- leaf detection -- tree flattening for future lazy-loading compatibility - -### Repository Abstraction - -- `src/app/core/categories/repositories/category.repository.ts` -- `src/app/core/categories/repositories/api-category.repository.ts` -- `src/app/core/categories/category-repository.token.ts` - -`CategoryRepository` returns DTOs from the existing `GET /category` API. The injection token uses the existing runtime provider strategy and remains compatible with both mock and API modes. Mock mode continues to work through the existing mock-data interceptor. - -### Category Service - -- `src/app/core/categories/category.service.ts` - -Converts repository DTOs through the mapper and exposes domain methods: - -- all categories -- category tree -- root categories -- category by id -- children -- parent -- breadcrumb -- leaf detection - -### Category Facade - -- `src/app/facades/platform/category.facade.ts` - -Exposes observable streams and state for: - -- all categories -- category tree -- root categories -- category by id -- selected category -- breadcrumb -- children - -### Product Compatibility - -- `src/app/core/products/models/product-domain.model.ts` -- `src/app/core/products/providers/api-product-data.provider.ts` - -`ProductFacade.getCategories()` now resolves through `CategoryService`, so compatibility category access also returns the new category domain model. - -## UI Migration - -Updated category-facing UI consumers: - -- `src/app/pages/home/home.component.ts` -- `src/app/pages/home/home.component.html` -- `src/app/pages/category/subcategories.component.ts` -- `src/app/pages/category/subcategories.component.html` -- `src/app/pages/category/category.component.ts` - -The home page and subcategory page now consume `CategoryFacade` and `Category` domain models. Category route item loading still uses the existing product facade for product lists, without changing product/payment/auth contracts. - -## Validation - -Completed checks: - -- DTOs are isolated under `core/categories/dto`. -- Category mapper exists and is the only category DTO-to-domain conversion point. -- Category UI uses `CategoryFacade` and category domain models. -- Category backend field names are contained to the category DTO/mapper boundary and compatibility internals. -- Components do not use `HttpClient` for category data. -- No authentication changes were made. -- No payment changes were made. -- No bootstrap contract changes were made. -- No backend API contract changes were made. -- Mock/API compatibility is preserved through the repository token and existing mock interceptor. - -Build validation passed: - -```bash -npm run build -``` - -## Stop Point - -Category Domain implementation is complete for Sprint 4. Stop here for approval before starting the next domain or any Builder/Backoffice work. diff --git a/docs/Content-Management.md b/docs/Content-Management.md deleted file mode 100644 index 61d2c58..0000000 --- a/docs/Content-Management.md +++ /dev/null @@ -1,154 +0,0 @@ -# Static Pages / CMS - Sprint 14 - -## Scope - -Sprint 14 adds configuration-driven CMS support for unlimited marketplace static pages. - -Out of scope: -- backend save endpoints -- marketplace-specific content logic -- hardcoded page names - -## Feature Architecture - -```text -src/app/features/content-management/ - facade/ - services/ - models/ - pages/ - components/ -``` - -Main pieces: -- `ContentPageService` normalizes bootstrap static page data -- `ContentManagementFacade` exposes normalized pages and validation helpers -- `StaticPagesEditorComponent` integrates CMS editing into Project Editor - -## Bootstrap Structure - -Static pages live under `bootstrap.staticPages`. - -Supported properties per page: -- `id` -- `slug` -- `title` -- `showInFooter` -- `showInHeader` -- `showInSitemap` -- `icon` -- `order` -- `visibility` -- `requiresAuthentication` -- `footerGroup` -- `translations` -- `html` -- `seo` - -## Translation Model - -Content can be stored in `translations[locale]`: -- `title` -- `html` -- `seo` - -Legacy structures remain supported through normalization. - -## Dynamic Routing - -Frontend uses dynamic static page resolution instead of manual page registration. - -Current route surface: -- `/:lang/:staticPath` -- legacy compatibility: `/:lang/page/:key` - -Resolver maps route slug to bootstrap-configured page. - -## Header Generation - -Pages with `showInHeader: true` render in header navigation. - -No hardcoded page list required. - -## Footer Generation - -Footer can render grouped static pages from bootstrap flags and metadata. - -Supported grouping: -- Company -n- Customer -- Legal -- Support -- Social - -Social links come from footer bootstrap config. - -## Rendering - -`StaticPageComponent` now: -- resolves content by slug or id -- reloads content when language changes -- updates document title/meta -- supports RTL-ready `dir` switching -- renders backend HTML safely through sanitization - -## Editor Integration - -Project Editor now includes `Static Pages` section. - -Supported actions: -- create page -- delete page -- enable footer/header/sitemap visibility -- edit slug -- edit icon -- edit order -- edit translations -- edit HTML - -## Validation - -Current validation prevents: -- duplicate slugs -- empty titles - -## Future Backend Endpoints - -Recommended future endpoints: -- `GET /builder/content-pages` -- `PUT /builder/content-pages` -- `POST /builder/content-pages/import` -- `GET /builder/content-pages/export` -- `POST /builder/content-pages/validate` - -## Example Bootstrap - -```json -{ - "staticPages": { - "about": { - "id": "about", - "slug": "about", - "title": { "en": "About Us", "ru": "О нас" }, - "showInFooter": true, - "showInHeader": true, - "showInSitemap": true, - "icon": "info", - "order": 1, - "requiresAuthentication": false, - "footerGroup": "Company", - "translations": { - "en": { - "title": "About Us", - "html": "

About

Company story

" - } - }, - "seo": { - "title": { "en": "About Us" }, - "description": { "en": "About our marketplace" }, - "canonical": "/about" - } - } - } -} -``` diff --git a/docs/Diagnostics.md b/docs/Diagnostics.md deleted file mode 100644 index 7fc750e..0000000 --- a/docs/Diagnostics.md +++ /dev/null @@ -1,127 +0,0 @@ -# Marketplace Diagnostics & Health Engine - Sprint 14 - -## Scope - -Sprint 14 adds a development-only diagnostics feature for marketplace configuration and runtime health. - -Constraints respected: -- No runtime behavior changes -- No business logic changes -- No authentication changes -- No payment changes - -## Architecture - -```text -src/app/features/diagnostics/ - components/ - diagnostics-page.component.* - services/ - diagnostics-logger.service.ts - models/ - diagnostics.model.ts - validators/ - bootstrap-diagnostics.validator.ts - runtime-diagnostics.validator.ts - diagnostics-health-score.util.ts - facade/ - diagnostics.facade.ts -``` - -## Health Checks - -Current checks cover: -- bootstrap loaded -- tenant resolved -- runtime initialized -- theme loaded -- widget manifest loaded -- section engine reachable -- configuration engine initialized -- translations available -- required assets health - -## Bootstrap Validation - -Current validator detects: -- missing required properties -- unknown widget types -- duplicate page/section/widget ids -- unknown layout values -- invalid feature flags -- broken page definitions -- invalid navigation targets -- missing translations in navigation label keys -- missing branding media - -## Runtime Validation - -Current validator detects: -- missing datasource declarations for widgets that support them -- widget rendering failures from runtime diagnostics stream -- broken route expectations -- configuration fallback usage in local development -- failed image loading where DOM can observe it -- missing optional data - -## Severity Model - -Each entry includes: -- code -- severity -- title -- description -- affected component -- suggested resolution - -Levels: -- info -- warning -- error -- critical - -## Health Score - -Simple weighted score: -- critical: -15 -- error: -8 -- warning: -3 -- info: 0 - -Minimum 0, maximum 100. - -## Developer Page - -Development-only route: -- `/__diagnostics` - -Page shows: -- summary -- health score -- passed checks -- warnings/errors/critical counts -- detailed diagnostics entries - -## Logging Abstraction - -`DiagnosticsLoggerService` stores diagnostic entries in memory. - -`RuntimeDiagnosticsService` now keeps unknown widget events in memory for diagnostics consumption only. - -No external logging integration in this sprint. - -## Extension Points - -Future additions can plug into: -- new validators under `validators/` -- new runtime event collectors -- remote diagnostics export service -- monitoring integrations (Sentry, Datadog, Grafana, OpenTelemetry) -- asset/network checks -- widget render timing checks - -## Performance - -Production route excluded. - -Diagnostics work runs only on diagnostics page access in development mode. diff --git a/docs/EDITOR.md b/docs/EDITOR.md new file mode 100644 index 0000000..efe32c0 --- /dev/null +++ b/docs/EDITOR.md @@ -0,0 +1,70 @@ +# EDITOR (Project Editor) + +Replaces the old `docs/Project-Editor.md` (content merged in below and extended with the Sprint 19 field-description/dropdown work). + +The Project Editor (`src/app/features/project-editor/`) edits the tenant's `BootstrapConfig` (`docs/BOOTSTRAP.md`) directly — no parallel model. It is out of scope for products, categories, orders, or analytics management (those live under `features/admin/*`/`features/backoffice/*`, see `docs/ADMIN.md`). + +``` +src/app/features/project-editor/ + pages/ route container + sections/ one component per editor tab (see below) + components/ shared editor UI (save bar, HTML editor) + models/ ProjectEditorState, EDITOR_SECTION_BOOTSTRAP_KEYS + services/ ProjectValidator, ProjectEditorDraftStorageService, LocaleSyncService + facade/ ProjectEditorFacade +``` + +Route: `/edit/:section` or `/{lang}/edit/:section`. + +## Facade + +`ProjectEditorFacade` exposes: `loadBootstrap()`, `updateBootstrap(updater)`, `exportBootstrap()`, `importBootstrap()`, `preview()`, `save()`, `publish()`, plus signals `bootstrap`, `status` (`draft|published`), `dirty`, `lastSavedAt`, `lastPublishedAt`, `validationIssues`, `homepageWidgets`, `homepagePage`. Components in `sections/*` inject this facade directly (an accepted exception to the presentational-component rule, per ADR-006 — these are container/section components, not shared UI). + +## Sections + +| Section | Component | Covers | +|---|---|---| +| General | `general-section` | marketplace name, domain, description, default/supported languages | +| Branding | `branding-section` | logo, small logo, favicon, marketplace title | +| Theme | `theme-section` | palette colors, theme mode, site layout mode | +| Header | `header-section` | logo/search/categories/languages/cart/profile/wishlist/compare/region toggles | +| Footer | `footer-section` | company info, address, phone, email, copyright, payment icons, social links, static pages list | +| Homepage | `homepage-section` | homepage section list: visibility, order (drag-and-drop), layout strategy, columns | +| Widgets | `widgets-section` | homepage widget configuration — typed editors for hero/categories/product-collection, JSON fallback for everything else | +| Marketplace Features | `features-section` | feature flags, catalog navigation mode, search suggestions/history, recently viewed, reviews/questions/recommendations | +| Languages | `languages-section` | add/remove supported locale, set default locale; syncs translation keys across static pages and nav labels via `LocaleSyncService` | +| Navigation | `navigation-section` | header nav: add/remove/reorder/edit label/URL/visibility. Flat footer nav: same. Grouped (column-based) footer nav is read-only here — edit via Footer tab. | +| Preview | `preview-section` | export/import JSON, in-memory runtime preview without full reload | + +## Save / publish / draft / reset model + +- **Save**: `save()` snapshots the current in-memory bootstrap as "last saved" (`lastSavedAt`). `ProjectEditorDraftStorageService` persists the full draft to `localStorage` (`projectEditor.draftBootstrap.v1`, scoped by `tenant.id`) on every `updateBootstrap()`, `save()`, and `publish()` call. +- **Publish**: runs `ProjectValidator`; if clean, calls `PlatformRuntimeService.reloadFromBootstrap()`, sets `status = 'published'`, sets `lastPublishedAt`, and becomes the new `originalBootstrap` baseline used by reset. +- **Draft restore**: on `loadBootstrap()`, if a stored draft exists for the same tenant it loads instead of the fresh fetch, and `draftRestored` is set (shown as a dismissible banner in the save bar). +- **Reset section**: reverts one section's bootstrap keys (per `EDITOR_SECTION_BOOTSTRAP_KEYS` in `models/project-editor.model.ts`) to `originalBootstrap`. Confirmation required. +- **Reset draft**: reverts the entire bootstrap to `originalBootstrap` and clears the persisted local draft. Confirmation required. +- **Per-field reset is not implemented** — no per-field default registry exists; only section- and project-level reset. +- **No backend persistence exists for any of this today** — see `docs/BACKEND.md` item 2 for the endpoints needed. + +## Validation + +`ProjectValidator` (`services/project-validator.service.ts`) runs on every save-bar render and blocks Publish (not Save) on: missing `branding.logoUrl`, no supported locales, invalid `tenant.websiteBaseUrl`, duplicate static-page slugs (falls back to `route`), empty homepage, a homepage widget with no `type`, duplicate header nav links, invalid theme colors, missing translations for a supported locale, and layout/section-layout values outside the known enums (`PlatformLayoutType`, `SectionLayoutStrategy`). + +## Admin Authentication (QR reuse) + +Admin login shares the exact same Telegram QR/session backend and `TelegramLoginComponent` as customer login (`mode: 'admin'` vs `'customer'`) — only the cookie name/`SameSite` policy, token storage keys, and guard differ. **Backend gap:** because both flows hit the same session endpoint, the backend cannot distinguish an admin scan from a customer scan today — real admin authorization must be enforced server-side. Full detail: `docs/BACKEND.md` item 1. + +## Field-description / dropdown UX (Sprint 19+) + +Every field across the 10 editor section templates now carries a one-line, i18n'd description under its label explaining what it does in plain language (all new copy routed through `TranslateService`/`TranslatePipe`, added to `Translations` + `en.ts`/`ru.ts`/`hy.ts` following the existing `builder.*` key pattern — see `src/app/i18n/translations.ts`). + +**Converted from free-text `` to ``, which is the correct native widget), company/address/phone/email, copyright, payment icons/social links (JSON-ish textarea), homepage section `columns` (a number, not an enum), widget-specific props (`hero`/`categories`/`product-collection` typed fields like layout/height/overlay/autoplay/cardsPerRow — these are widget `props` strings/booleans, not modeled as TypeScript unions anywhere, so they stay free text/checkbox with a description rather than a fabricated enum), navigation link label/URL, and the widget JSON fallback textarea for any widget type without a dedicated editor. These are genuinely open-ended or already have the correct native input type; converting them to ` - - - -
- @for (code of locales(); track code) { -
-
-

{{ code }}

-
- @if (code === defaultLocale()) { - {{ 'builder.defaultLanguageLabel' | translate }} - } @else { - - - } -
-
-
- } -
- -``` - -- [ ] **Step 5: Register the tab** - -In `src/app/features/project-editor/pages/project-editor-page.component.ts`, add the import and register it in `imports`: - -```typescript -import { ProjectEditorLanguagesSectionComponent } from '../sections/languages-section.component'; -``` - -Add `ProjectEditorLanguagesSectionComponent` to the `imports` array (after `ProjectEditorFeaturesSectionComponent`), and add `'languages'` to `KNOWN_SECTIONS`: - -```typescript -const KNOWN_SECTIONS: ProjectEditorSectionId[] = [ - 'general', 'branding', 'theme', 'header', 'footer', 'homepage', 'widgets', 'static-pages', 'features', 'languages', 'preview' -]; -``` - -In `src/app/features/project-editor/pages/project-editor-page.component.html`, add a case inside the `@switch` (after the `features` case): - -```html - @case ('languages') { } -``` - -In `src/app/features/project-editor/components/project-editor-nav.component.ts`, add to `sections` (after `features`, before `preview`): - -```typescript - { id: 'languages', label: 'builder.languagesTab' }, -``` - -- [ ] **Step 6: i18n keys** - -In `src/app/i18n/translations.ts`, inside the `builder: { ... }` interface block, add (after `livePreview: string;`): - -```typescript - languagesTab: string; - addLanguage: string; - removeLanguage: string; - setDefaultLanguage: string; - defaultLanguageLabel: string; -``` - -In `src/app/i18n/en.ts`, inside `builder: { ... }`, add: - -```typescript - languagesTab: 'Languages', - addLanguage: 'Add Language', - removeLanguage: 'Remove', - setDefaultLanguage: 'Set as Default', - defaultLanguageLabel: 'Default', -``` - -In `src/app/i18n/ru.ts`, inside `builder: { ... }`, add: - -```typescript - languagesTab: 'Языки', - addLanguage: 'Добавить язык', - removeLanguage: 'Удалить', - setDefaultLanguage: 'Сделать языком по умолчанию', - defaultLanguageLabel: 'По умолчанию', -``` - -In `src/app/i18n/hy.ts`, inside `builder: { ... }`, add: - -```typescript - languagesTab: 'Լեզուներ', - addLanguage: 'Ավելացնել լեզու', - removeLanguage: 'Հեռացնել', - setDefaultLanguage: 'Դարձնել լռելյայն', - defaultLanguageLabel: 'Լռելյայն', -``` - -- [ ] **Step 7: Manual verification** - -Run `ng serve`, open `/edit/languages`. Confirm current locales (en, ru, hy) list with `en` marked default. Type `de` and click Add Language — confirm `de` appears in the list. Click "Set as Default" on `de` — confirm it now shows as default and `en` shows the remove/set-default buttons instead. Go to `/edit/preview`, click the export/refresh action, and inspect the exported JSON textarea: confirm `localization.supportedLocales` includes `"de"` and `localization.defaultLocale` is `"de"`, and (if any static pages exist) each static page's `translations` object now has a `"de"` key with `{ "title": "", "html": "" }`. Go back to `/edit/languages` and remove `de` — confirm it disappears from the list and from the exported JSON's `translations` objects. Attempt to remove the current default locale — confirm nothing happens (no button is shown for the default row). - -- [ ] **Step 8: Commit** - -```bash -git add src/app/features/project-editor/services/locale-sync.service.ts src/app/features/project-editor/sections/languages-section.component.ts src/app/features/project-editor/sections/languages-section.component.html src/app/features/project-editor/models/project-editor.model.ts src/app/features/project-editor/facade/project-editor.facade.ts src/app/features/project-editor/pages/project-editor-page.component.ts src/app/features/project-editor/pages/project-editor-page.component.html src/app/features/project-editor/components/project-editor-nav.component.ts src/app/i18n/translations.ts src/app/i18n/en.ts src/app/i18n/ru.ts src/app/i18n/hy.ts -git commit -m "feat(project-editor): add Languages tab with generic locale sync" -``` - ---- - -### Task 3: Static pages editor uses supported locales dynamically - -**Files:** -- Modify: `src/app/features/content-management/components/static-pages-editor.component.ts` -- Modify: `src/app/features/content-management/components/static-pages-editor.component.html` - -**Interfaces:** -- Consumes: `ProjectEditorFacade.bootstrap` (existing), `bootstrap.localization.supportedLocales: string[]` (existing field). -- Produces: `locales: Signal` computed on the component, consumed by its own template only. - -- [ ] **Step 1: Add a `locales` computed** - -In `src/app/features/content-management/components/static-pages-editor.component.ts`, add after the existing `readonly validation = ...` line: - -```typescript - readonly locales = computed(() => this.bootstrap()?.localization.supportedLocales ?? ['en']); -``` - -- [ ] **Step 2: Use it in the template instead of the hardcoded array** - -In `src/app/features/content-management/components/static-pages-editor.component.html`, change: - -```html - @for (locale of ['en','ru','hy']; track locale) { -``` - -to: - -```html - @for (locale of locales(); track locale) { -``` - -- [ ] **Step 3: Manual verification** - -With the dev server running, go to `/edit/languages` and add `de`. Go to `/edit/static-pages` — confirm every page card now shows a fourth translation row for `de` (title input + HTML field) in addition to en/ru/hy. Remove `de` on the Languages tab, return to Static Pages, confirm the `de` row is gone. - -- [ ] **Step 4: Commit** - -```bash -git add src/app/features/content-management/components/static-pages-editor.component.ts src/app/features/content-management/components/static-pages-editor.component.html -git commit -m "fix(content-management): static pages editor reads supported locales instead of hardcoding en/ru/hy" -``` - ---- - -### Task 4: Navigation tab - -**Files:** -- Modify: `src/app/features/project-editor/facade/project-editor.facade.ts` -- Modify: `src/app/features/project-editor/models/project-editor.model.ts` -- Create: `src/app/features/project-editor/sections/navigation-section.component.ts` -- Create: `src/app/features/project-editor/sections/navigation-section.component.html` -- Modify: `src/app/features/project-editor/pages/project-editor-page.component.ts` -- Modify: `src/app/features/project-editor/pages/project-editor-page.component.html` -- Modify: `src/app/features/project-editor/components/project-editor-nav.component.ts` -- Modify: `src/app/i18n/translations.ts`, `en.ts`, `ru.ts`, `hy.ts` - -**Interfaces:** -- Consumes: `NavigationItemConfig` from `src/app/shared/models/config` (`id`, `label`, `route`, `order`, `visible`, `children`). -- Produces: `ProjectEditorFacade.addNavLink(target)`, `.removeNavLink(target, id)`, `.updateNavLink(target, id, patch)`, `.reorderNavLink(target, id, direction)` — no other task depends on these, but they follow the same `target: 'header' | 'footer'` shape a future Sidebar tab could reuse. - -- [ ] **Step 1: Facade actions** - -In `src/app/features/project-editor/facade/project-editor.facade.ts`, add the import: - -```typescript -import { NavigationItemConfig } from '../../../shared/models/config'; -``` - -Add these methods after `setDefaultLocale` (from Task 2), before `private normalize`: - -```typescript - addNavLink(target: 'header' | 'footer'): void { - this.updateBootstrap(current => { - const list = current.navigation[target]; - if (!this.isFlatNavList(list)) { - return current; - } - const items = list as NavigationItemConfig[]; - const newItem: NavigationItemConfig = { - id: `nav-${Date.now()}`, - label: 'New link', - route: '/', - order: items.length + 1, - visible: true, - }; - return { ...current, navigation: { ...current.navigation, [target]: [...items, newItem] } }; - }); - } - - removeNavLink(target: 'header' | 'footer', id: string): void { - this.updateBootstrap(current => { - const list = current.navigation[target]; - if (!this.isFlatNavList(list)) { - return current; - } - return { - ...current, - navigation: { ...current.navigation, [target]: (list as NavigationItemConfig[]).filter(item => item.id !== id) }, - }; - }); - } - - updateNavLink(target: 'header' | 'footer', id: string, patch: Partial): void { - this.updateBootstrap(current => { - const list = current.navigation[target]; - if (!this.isFlatNavList(list)) { - return current; - } - return { - ...current, - navigation: { - ...current.navigation, - [target]: (list as NavigationItemConfig[]).map(item => (item.id !== id ? item : { ...item, ...patch })), - }, - }; - }); - } - - reorderNavLink(target: 'header' | 'footer', id: string, direction: -1 | 1): void { - this.updateBootstrap(current => { - const list = current.navigation[target]; - if (!this.isFlatNavList(list)) { - return current; - } - const items = [...(list as NavigationItemConfig[])].sort((a, b) => (a.order ?? 0) - (b.order ?? 0)); - const index = items.findIndex(item => item.id === id); - const nextIndex = index + direction; - if (index < 0 || nextIndex < 0 || nextIndex >= items.length) { - return current; - } - const tmp = items[index]; - items[index] = items[nextIndex]; - items[nextIndex] = tmp; - return { - ...current, - navigation: { ...current.navigation, [target]: items.map((item, order) => ({ ...item, order: order + 1 })) }, - }; - }); - } - - private isFlatNavList(list: NavigationItemConfig[] | { items: unknown }[]): list is NavigationItemConfig[] { - return list.length === 0 || !('items' in list[0]); - } -``` - -- [ ] **Step 2: Add the section id** - -In `src/app/features/project-editor/models/project-editor.model.ts`, add `'navigation'` to the `ProjectEditorSectionId` union (next to `'languages'`): - -```typescript -export type ProjectEditorSectionId = - | 'general' - | 'branding' - | 'theme' - | 'header' - | 'footer' - | 'homepage' - | 'widgets' - | 'static-pages' - | 'features' - | 'languages' - | 'navigation' - | 'preview'; -``` - -- [ ] **Step 3: Navigation tab component** - -Create `src/app/features/project-editor/sections/navigation-section.component.ts`: - -```typescript -import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core'; -import { FormsModule } from '@angular/forms'; -import { ProjectEditorFacade } from '../facade/project-editor.facade'; -import { TranslatePipe } from '../../../i18n/translate.pipe'; -import { NavigationItemConfig } from '../../../shared/models/config'; - -@Component({ - selector: 'app-project-editor-navigation-section', - standalone: true, - imports: [FormsModule, TranslatePipe], - templateUrl: './navigation-section.component.html', - styleUrls: ['./section.shared.scss'], - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class ProjectEditorNavigationSectionComponent { - private readonly facade = inject(ProjectEditorFacade); - readonly bootstrap = this.facade.bootstrap; - - readonly headerLinks = computed(() => this.sorted(this.bootstrap()?.navigation.header ?? [])); - readonly footerLinks = computed(() => { - const footer = this.bootstrap()?.navigation.footer ?? []; - if (footer.length === 0) { - return []; - } - return 'items' in footer[0] ? null : this.sorted(footer as NavigationItemConfig[]); - }); - - labelOf(item: NavigationItemConfig): string { - if (typeof item.label === 'string' || !item.label) { - return item.label ?? ''; - } - const defaultLocale = this.bootstrap()?.localization.defaultLocale ?? 'en'; - return item.label[defaultLocale] ?? Object.values(item.label)[0] ?? ''; - } - - addLink(target: 'header' | 'footer'): void { - this.facade.addNavLink(target); - } - - removeLink(target: 'header' | 'footer', id: string): void { - this.facade.removeNavLink(target, id); - } - - move(target: 'header' | 'footer', id: string, direction: -1 | 1): void { - this.facade.reorderNavLink(target, id, direction); - } - - updateLabel(target: 'header' | 'footer', id: string, value: string): void { - this.facade.updateNavLink(target, id, { label: value }); - } - - updateRoute(target: 'header' | 'footer', id: string, value: string): void { - this.facade.updateNavLink(target, id, { route: value }); - } - - updateVisible(target: 'header' | 'footer', id: string, value: boolean): void { - this.facade.updateNavLink(target, id, { visible: value }); - } - - private sorted(items: NavigationItemConfig[]): NavigationItemConfig[] { - return [...items].sort((a, b) => (a.order ?? 0) - (b.order ?? 0)); - } -} -``` - -Create `src/app/features/project-editor/sections/navigation-section.component.html`: - -```html -
-
-

{{ 'builder.navigationHeader' | translate }}

- -
- -
- @for (item of headerLinks(); track item.id) { -
-
-

{{ labelOf(item) }}

-
- - - -
-
-
- - - -
-
- } -
- -
-

{{ 'builder.navigationFooter' | translate }}

- @if (footerLinks(); as footer) { - - } -
- - @if (footerLinks(); as footer) { -
- @for (item of footer; track item.id) { -
-
-

{{ labelOf(item) }}

-
- - - -
-
-
- - - -
-
- } -
- } @else { -

{{ 'builder.navigationFooterGrouped' | translate }}

- } -
-``` - -- [ ] **Step 4: Register the tab** - -In `src/app/features/project-editor/pages/project-editor-page.component.ts`, add the import, add to `imports`, and add `'navigation'` to `KNOWN_SECTIONS`: - -```typescript -import { ProjectEditorNavigationSectionComponent } from '../sections/navigation-section.component'; -``` - -```typescript -const KNOWN_SECTIONS: ProjectEditorSectionId[] = [ - 'general', 'branding', 'theme', 'header', 'footer', 'homepage', 'widgets', 'static-pages', 'features', 'languages', 'navigation', 'preview' -]; -``` - -In `src/app/features/project-editor/pages/project-editor-page.component.html`, add the case (after `languages`): - -```html - @case ('navigation') { } -``` - -In `src/app/features/project-editor/components/project-editor-nav.component.ts`, add (after `languages`, before `preview`): - -```typescript - { id: 'navigation', label: 'builder.navigationTab' }, -``` - -- [ ] **Step 5: i18n keys** - -In `src/app/i18n/translations.ts`, add to the `builder` interface (after the Task 2 keys): - -```typescript - navigationTab: string; - navigationHeader: string; - navigationFooter: string; - navigationFooterGrouped: string; - addLink: string; - removeLink: string; - linkLabel: string; - linkUrl: string; -``` - -In `src/app/i18n/en.ts`, add: - -```typescript - navigationTab: 'Navigation', - navigationHeader: 'Header Navigation', - navigationFooter: 'Footer Navigation', - navigationFooterGrouped: 'Footer navigation uses grouped columns and is not editable here yet — edit via the Footer tab.', - addLink: 'Add Link', - removeLink: 'Remove', - linkLabel: 'Label', - linkUrl: 'URL', -``` - -In `src/app/i18n/ru.ts`, add: - -```typescript - navigationTab: 'Навигация', - navigationHeader: 'Навигация в шапке', - navigationFooter: 'Навигация в подвале', - navigationFooterGrouped: 'Навигация подвала сгруппирована по колонкам и пока недоступна для редактирования здесь — используйте вкладку "Подвал".', - addLink: 'Добавить ссылку', - removeLink: 'Удалить', - linkLabel: 'Название', - linkUrl: 'URL', -``` - -In `src/app/i18n/hy.ts`, add: - -```typescript - navigationTab: 'Նավիգացիա', - navigationHeader: 'Վերին նավիգացիա', - navigationFooter: 'Ստորին նավիգացիա', - navigationFooterGrouped: 'Ստորին նավիգացիան խմբավորված է սյուներով և դեռ խմբագրելի չէ այստեղ. օգտագործեք «Footer» ներդիրը։', - addLink: 'Ավելացնել հղում', - removeLink: 'Հեռացնել', - linkLabel: 'Պիտակ', - linkUrl: 'URL', -``` - -- [ ] **Step 6: Manual verification** - -Run `ng serve`, open `/edit/navigation`. Confirm the existing header nav items list, sorted by order. Click "Add Link" under Header Navigation — confirm a "New link" row appears at the bottom. Edit its label and URL — confirm the values persist (check via `/edit/preview` export JSON: `navigation.header` contains the new item with your edited `label`/`route`). Click ↑ on the new item — confirm it moves up one position and the exported JSON's `order` values update accordingly. Click "Remove" — confirm it disappears. If `navigation.footer` in this bootstrap is grouped (`FooterNavigationGroupConfig[]`), confirm the footer section shows the "not editable here yet" message instead of a broken list. - -- [ ] **Step 7: Commit** - -```bash -git add src/app/features/project-editor/facade/project-editor.facade.ts src/app/features/project-editor/models/project-editor.model.ts src/app/features/project-editor/sections/navigation-section.component.ts src/app/features/project-editor/sections/navigation-section.component.html src/app/features/project-editor/pages/project-editor-page.component.ts src/app/features/project-editor/pages/project-editor-page.component.html src/app/features/project-editor/components/project-editor-nav.component.ts src/app/i18n/translations.ts src/app/i18n/en.ts src/app/i18n/ru.ts src/app/i18n/hy.ts -git commit -m "feat(project-editor): add Navigation tab for header/flat footer nav" -``` - ---- - -### Task 5: Reusable rich HTML editor component - -**Files:** -- Create: `src/app/features/project-editor/components/html-editor/marketplace-html-editor.component.ts` -- Create: `src/app/features/project-editor/components/html-editor/marketplace-html-editor.component.html` -- Create: `src/app/features/project-editor/components/html-editor/marketplace-html-editor.component.scss` - -**Interfaces:** -- Consumes: nothing outside Angular core/browser APIs (`document.execCommand`, native `contentEditable`). -- Produces: `MarketplaceHtmlEditorComponent` with `@Input() html: string`, `@Output() htmlChange: EventEmitter` — consumed by Task 6. - -- [ ] **Step 1: Component** - -Create `src/app/features/project-editor/components/html-editor/marketplace-html-editor.component.ts`: - -```typescript -import { ChangeDetectionStrategy, Component, ElementRef, EventEmitter, Input, OnChanges, Output, SimpleChanges, ViewChild, signal } from '@angular/core'; - -export interface HtmlEditorToolbarCommand { - id: string; - label: string; - command: string; - value?: string; -} - -export const HTML_EDITOR_TOOLBAR: HtmlEditorToolbarCommand[] = [ - { id: 'bold', label: 'B', command: 'bold' }, - { id: 'italic', label: 'I', command: 'italic' }, - { id: 'underline', label: 'U', command: 'underline' }, - { id: 'h2', label: 'H2', command: 'formatBlock', value: 'H2' }, - { id: 'h3', label: 'H3', command: 'formatBlock', value: 'H3' }, - { id: 'ul', label: 'List', command: 'insertUnorderedList' }, - { id: 'ol', label: '1,2,3', command: 'insertOrderedList' }, - { id: 'link', label: 'Link', command: 'createLink' }, - { id: 'image', label: 'Image', command: 'insertImage' }, - { id: 'table', label: 'Table', command: 'insertHTML', value: '
  
' }, -]; - -@Component({ - selector: 'app-marketplace-html-editor', - standalone: true, - imports: [], - templateUrl: './marketplace-html-editor.component.html', - styleUrls: ['./marketplace-html-editor.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class MarketplaceHtmlEditorComponent implements OnChanges { - @Input() html = ''; - @Output() htmlChange = new EventEmitter(); - @ViewChild('surface', { static: true }) surface!: ElementRef; - - readonly toolbar = HTML_EDITOR_TOOLBAR; - readonly showCode = signal(false); - readonly codeValue = signal(''); - - ngOnChanges(changes: SimpleChanges): void { - if (changes['html'] && this.surface && this.surface.nativeElement.innerHTML !== (this.html || '')) { - this.surface.nativeElement.innerHTML = this.html || ''; - } - } - - runCommand(item: HtmlEditorToolbarCommand): void { - this.surface.nativeElement.focus(); - if (item.command === 'createLink') { - const url = window.prompt('URL'); - if (!url) { - return; - } - document.execCommand('createLink', false, url); - } else if (item.command === 'insertImage') { - const url = window.prompt('Image URL'); - if (!url) { - return; - } - document.execCommand('insertImage', false, url); - } else { - document.execCommand(item.command, false, item.value); - } - this.emitChange(); - } - - onInput(): void { - this.emitChange(); - } - - toggleCode(): void { - if (!this.showCode()) { - this.codeValue.set(this.surface.nativeElement.innerHTML); - this.showCode.set(true); - return; - } - this.surface.nativeElement.innerHTML = this.codeValue(); - this.showCode.set(false); - this.emitChange(); - } - - updateCode(value: string): void { - this.codeValue.set(value); - } - - private emitChange(): void { - this.htmlChange.emit(this.surface.nativeElement.innerHTML); - } -} -``` - -- [ ] **Step 2: Template** - -Create `src/app/features/project-editor/components/html-editor/marketplace-html-editor.component.html`: - -```html -
-
- @for (item of toolbar; track item.id) { - - } - -
- - @if (showCode()) { - - } @else { -
- } -
-``` - -- [ ] **Step 3: Styles** - -Create `src/app/features/project-editor/components/html-editor/marketplace-html-editor.component.scss`: - -```scss -.html-editor { - display: flex; - flex-direction: column; - gap: 0.5rem; -} - -.html-editor-toolbar { - display: flex; - flex-wrap: wrap; - gap: 0.25rem; -} - -.html-editor-surface { - min-height: 160px; - border: 1px solid var(--border, #ccc); - border-radius: 4px; - padding: 0.5rem; - overflow-y: auto; -} - -.html-editor-code { - min-height: 160px; - font-family: monospace; - border: 1px solid var(--border, #ccc); - border-radius: 4px; - padding: 0.5rem; -} -``` - -- [ ] **Step 4: Manual verification** - -This component isn't wired into a page yet — verify it compiles: run `npx tsc -p tsconfig.app.json --noEmit`. Expected: no new errors referencing `marketplace-html-editor.component.ts`. (Full interactive verification happens in Task 6 once it's mounted somewhere.) - -- [ ] **Step 5: Commit** - -```bash -git add src/app/features/project-editor/components/html-editor/ -git commit -m "feat(project-editor): add reusable contentEditable HTML editor component" -``` - ---- - -### Task 6: Replace the static-pages ` - -``` - -with: - -```html - -``` - -- [ ] **Step 3: Manual verification** - -Run `ng serve`, open `/edit/static-pages`, create a page. Confirm each locale's HTML field is now the toolbar editor, not a plain textarea. Click Bold, type text — confirm it renders bold in the editing surface. Click "Code", confirm the raw HTML (e.g. `...`) shows in a textarea, edit it, click "Code" again (now labeled "Preview") — confirm the surface re-renders your manual HTML edit. Go to `/edit/preview`, export JSON, confirm the page's `translations..html` contains the exact HTML you produced (bold tags etc.) — proving nothing sanitized it in the editor. - -- [ ] **Step 4: Commit** - -```bash -git add src/app/features/content-management/components/static-pages-editor.component.ts src/app/features/content-management/components/static-pages-editor.component.html -git commit -m "feat(content-management): use rich HTML editor for static page content" -``` - ---- - -### Task 7: `ProjectValidator` - -**Files:** -- Create: `src/app/features/project-editor/services/project-validator.service.ts` -- Modify: `src/app/i18n/translations.ts`, `en.ts`, `ru.ts`, `hy.ts` - -**Interfaces:** -- Consumes: `BootstrapConfig` (existing). -- Produces: `ProjectValidationIssue { code: string; message: string }`, `ProjectValidator.validate(bootstrap: BootstrapConfig): ProjectValidationIssue[]` — consumed by Task 8's facade `validationIssues` computed and Task 9's save bar. - -- [ ] **Step 1: Service** - -Create `src/app/features/project-editor/services/project-validator.service.ts`: - -```typescript -import { Injectable } from '@angular/core'; -import { BootstrapConfig } from '../../../shared/models/config'; - -export interface ProjectValidationIssue { - code: string; - message: string; -} - -const HEX_COLOR = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/; -const HTTP_URL = /^https?:\/\/\S+$/; - -@Injectable({ providedIn: 'root' }) -export class ProjectValidator { - validate(bootstrap: BootstrapConfig): ProjectValidationIssue[] { - return [ - ...this.brandingIssues(bootstrap), - ...this.languageIssues(bootstrap), - ...this.urlIssues(bootstrap), - ...this.duplicateSlugIssues(bootstrap), - ...this.homepageIssues(bootstrap), - ...this.navigationIssues(bootstrap), - ...this.colorIssues(bootstrap), - ]; - } - - private brandingIssues(bootstrap: BootstrapConfig): ProjectValidationIssue[] { - return bootstrap.branding.logoUrl ? [] : [{ code: 'missing-logo', message: 'builder.validationMissingLogo' }]; - } - - private languageIssues(bootstrap: BootstrapConfig): ProjectValidationIssue[] { - return bootstrap.localization.supportedLocales.length > 0 ? [] : [{ code: 'no-languages', message: 'builder.validationNoLanguages' }]; - } - - private urlIssues(bootstrap: BootstrapConfig): ProjectValidationIssue[] { - const url = bootstrap.tenant.websiteBaseUrl; - return !url || HTTP_URL.test(url) ? [] : [{ code: 'invalid-url', message: 'builder.validationInvalidUrl' }]; - } - - private duplicateSlugIssues(bootstrap: BootstrapConfig): ProjectValidationIssue[] { - const staticPages = bootstrap.staticPages; - if (!staticPages || Array.isArray(staticPages)) { - return []; - } - const slugs = Object.values(staticPages).map(page => page.slug); - const hasDuplicates = slugs.some((slug, index) => slugs.indexOf(slug) !== index); - return hasDuplicates ? [{ code: 'duplicate-slugs', message: 'builder.validationDuplicateSlugs' }] : []; - } - - private homepageIssues(bootstrap: BootstrapConfig): ProjectValidationIssue[] { - const homePage = bootstrap.pages.find(page => page.key === 'home' || page.route.path === '/'); - if (!homePage || homePage.sections.length === 0) { - return [{ code: 'empty-homepage', message: 'builder.validationEmptyHomepage' }]; - } - const hasMissingWidgetType = homePage.sections.some(section => section.widgets.some(widget => !widget.type?.trim())); - return hasMissingWidgetType ? [{ code: 'missing-widget', message: 'builder.validationMissingWidget' }] : []; - } - - private navigationIssues(bootstrap: BootstrapConfig): ProjectValidationIssue[] { - const keyOf = (item: { label?: string | Record; route?: string }): string => - `${typeof item.label === 'string' ? item.label : JSON.stringify(item.label ?? {})}|${item.route ?? ''}`; - const keys = bootstrap.navigation.header.map(keyOf); - const hasDuplicates = keys.some((key, index) => keys.indexOf(key) !== index); - return hasDuplicates ? [{ code: 'duplicate-nav-links', message: 'builder.validationDuplicateNavLinks' }] : []; - } - - private colorIssues(bootstrap: BootstrapConfig): ProjectValidationIssue[] { - const invalid = Object.values(bootstrap.theme.palette).some(value => !HEX_COLOR.test(value)); - return invalid ? [{ code: 'invalid-colors', message: 'builder.validationInvalidColors' }] : []; - } -} -``` - -- [ ] **Step 2: i18n keys** - -In `src/app/i18n/translations.ts`, add to the `builder` interface: - -```typescript - validationMissingLogo: string; - validationNoLanguages: string; - validationInvalidUrl: string; - validationDuplicateSlugs: string; - validationEmptyHomepage: string; - validationMissingWidget: string; - validationDuplicateNavLinks: string; - validationInvalidColors: string; -``` - -In `src/app/i18n/en.ts`, add: - -```typescript - validationMissingLogo: 'Branding is missing a logo.', - validationNoLanguages: 'No languages are configured.', - validationInvalidUrl: 'The marketplace URL is invalid.', - validationDuplicateSlugs: 'Two or more static pages share the same slug.', - validationEmptyHomepage: 'The homepage has no sections.', - validationMissingWidget: 'A homepage section has a widget with no type.', - validationDuplicateNavLinks: 'Two or more header navigation links are duplicates.', - validationInvalidColors: 'One or more theme colors are not valid hex colors.', -``` - -In `src/app/i18n/ru.ts`, add: - -```typescript - validationMissingLogo: 'В брендинге отсутствует логотип.', - validationNoLanguages: 'Не настроены языки.', - validationInvalidUrl: 'Некорректный URL маркетплейса.', - validationDuplicateSlugs: 'Две или более статические страницы имеют одинаковый slug.', - validationEmptyHomepage: 'На главной странице нет секций.', - validationMissingWidget: 'В секции главной страницы есть виджет без типа.', - validationDuplicateNavLinks: 'Две или более ссылки в навигации шапки дублируются.', - validationInvalidColors: 'Один или несколько цветов темы указаны некорректно.', -``` - -In `src/app/i18n/hy.ts`, add: - -```typescript - validationMissingLogo: 'Բրենդինգում բացակայում է լոգոն։', - validationNoLanguages: 'Կարգավորված լեզուներ չկան։', - validationInvalidUrl: 'Մարքեթփլեյսի URL-ը սխալ է։', - validationDuplicateSlugs: 'Երկու կամ ավելի ստատիկ էջեր ունեն նույն slug-ը։', - validationEmptyHomepage: 'Գլխավոր էջում սեկցիաներ չկան։', - validationMissingWidget: 'Գլխավոր էջի սեկցիաներից մեկն ունի վիջեթ առանց տիպի։', - validationDuplicateNavLinks: 'Վերնագրի նավիգացիայում կան կրկնվող հղումներ։', - validationInvalidColors: 'Թեմայի գույներից մեկը կամ մի քանիսը վավեր hex գույն չեն։', -``` - -- [ ] **Step 3: Manual verification** - -This service isn't wired into the UI yet. Verify it compiles: `npx tsc -p tsconfig.app.json --noEmit`. Expected: no new errors. (Full interactive verification happens in Task 9.) - -- [ ] **Step 4: Commit** - -```bash -git add src/app/features/project-editor/services/project-validator.service.ts src/app/i18n/translations.ts src/app/i18n/en.ts src/app/i18n/ru.ts src/app/i18n/hy.ts -git commit -m "feat(project-editor): add ProjectValidator with MVP validation rules" -``` - ---- - -### Task 8: Draft/Publish state and dirty tracking on the facade - -**Files:** -- Modify: `src/app/features/project-editor/models/project-editor.model.ts` -- Modify: `src/app/features/project-editor/facade/project-editor.facade.ts` - -**Interfaces:** -- Consumes: `ProjectValidator.validate` (Task 7), `PlatformRuntimeService.reloadFromBootstrap` (existing, `src/app/core/runtime/platform-runtime.service.ts:60`). -- Produces: `ProjectEditorFacade.status: Signal<'draft' | 'published'>`, `.dirty: Signal`, `.validationIssues: Signal`, `.save(): void`, `.publish(): boolean` — consumed by Task 9 (save bar) and Task 10 (dirty guard). - -- [ ] **Step 1: Extend the state shape** - -In `src/app/features/project-editor/models/project-editor.model.ts`, change `ProjectEditorState`: - -```typescript -export interface ProjectEditorState { - bootstrap: BootstrapConfig | null; - importError: string | null; - activeSection: ProjectEditorSectionId; - status: 'draft' | 'published'; - lastSavedBootstrap: BootstrapConfig | null; -} -``` - -- [ ] **Step 2: Update the facade** - -In `src/app/features/project-editor/facade/project-editor.facade.ts`, add imports: - -```typescript -import { PlatformRuntimeService } from '../../../core/runtime/platform-runtime.service'; -import { ProjectValidator } from '../services/project-validator.service'; -``` - -Add injected services (with the others at the top of the class): - -```typescript - private readonly runtime = inject(PlatformRuntimeService); - private readonly validator = inject(ProjectValidator); -``` - -Update the initial state: - -```typescript - private readonly state = signal({ - bootstrap: null, - importError: null, - activeSection: 'general', - status: 'draft', - lastSavedBootstrap: null, - }); -``` - -Add these computed signals (after `homepageWidgets`): - -```typescript - readonly status = computed(() => this.state().status); - readonly validationIssues = computed(() => { - const current = this.bootstrap(); - return current ? this.validator.validate(current) : []; - }); - readonly dirty = computed(() => { - const current = this.bootstrap(); - if (!current) { - return false; - } - return JSON.stringify(current) !== JSON.stringify(this.state().lastSavedBootstrap); - }); -``` - -Update `loadBootstrap()` to seed `lastSavedBootstrap`: - -```typescript - loadBootstrap(): void { - this.configService.loadBootstrap(true).pipe(take(1)).subscribe({ - next: config => { - const normalized = this.normalize(JSON.parse(JSON.stringify(config)) as BootstrapConfig); - this.state.update(current => ({ ...current, bootstrap: normalized, importError: null, lastSavedBootstrap: normalized, status: 'draft' })); - }, - error: () => this.state.update(current => ({ ...current, bootstrap: null, importError: 'builder.importError' })), - }); - } -``` - -Add `save()` and `publish()` after `setDefaultLocale` / nav actions (from Tasks 2 and 4): - -```typescript - save(): void { - const current = this.state().bootstrap; - if (!current) { - return; - } - this.state.update(state => ({ ...state, lastSavedBootstrap: JSON.parse(JSON.stringify(current)) })); - } - - publish(): boolean { - const current = this.state().bootstrap; - if (!current || this.validationIssues().length > 0) { - return false; - } - this.runtime.reloadFromBootstrap(current); - this.state.update(state => ({ - ...state, - status: 'published', - lastSavedBootstrap: JSON.parse(JSON.stringify(current)), - })); - return true; - } -``` - -- [ ] **Step 3: Manual verification** - -This isn't wired to any button yet. Verify it compiles: `npx tsc -p tsconfig.app.json --noEmit`. Expected: no new errors — in particular no error about `PlatformRuntimeService` not having `reloadFromBootstrap` (it does, confirmed at `src/app/core/runtime/platform-runtime.service.ts:60`). Full interactive verification happens in Task 9. - -- [ ] **Step 4: Commit** - -```bash -git add src/app/features/project-editor/models/project-editor.model.ts src/app/features/project-editor/facade/project-editor.facade.ts -git commit -m "feat(project-editor): add draft/publish status, dirty tracking, save/publish to facade" -``` - ---- - -### Task 9: Sticky Save/Publish bar - -**Files:** -- Create: `src/app/features/project-editor/components/save-bar/project-editor-save-bar.component.ts` -- Create: `src/app/features/project-editor/components/save-bar/project-editor-save-bar.component.html` -- Create: `src/app/features/project-editor/components/save-bar/project-editor-save-bar.component.scss` -- Modify: `src/app/features/project-editor/pages/project-editor-page.component.ts` -- Modify: `src/app/features/project-editor/pages/project-editor-page.component.html` -- Modify: `src/app/i18n/translations.ts`, `en.ts`, `ru.ts`, `hy.ts` - -**Interfaces:** -- Consumes: `ProjectEditorFacade.dirty`, `.status`, `.validationIssues`, `.save()`, `.publish()` (Task 8). -- Produces: nothing consumed elsewhere. - -- [ ] **Step 1: Component** - -Create `src/app/features/project-editor/components/save-bar/project-editor-save-bar.component.ts`: - -```typescript -import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; -import { TranslatePipe } from '../../../../i18n/translate.pipe'; -import { ProjectEditorFacade } from '../../facade/project-editor.facade'; - -@Component({ - selector: 'app-project-editor-save-bar', - standalone: true, - imports: [TranslatePipe], - templateUrl: './project-editor-save-bar.component.html', - styleUrls: ['./project-editor-save-bar.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class ProjectEditorSaveBarComponent { - private readonly facade = inject(ProjectEditorFacade); - readonly dirty = this.facade.dirty; - readonly status = this.facade.status; - readonly issues = this.facade.validationIssues; - - save(): void { - this.facade.save(); - } - - publish(): void { - this.facade.publish(); - } -} -``` - -- [ ] **Step 2: Template** - -Create `src/app/features/project-editor/components/save-bar/project-editor-save-bar.component.html`: - -```html -
-
- {{ (status() === 'published' ? 'builder.statusPublished' : 'builder.statusDraft') | translate }} - @if (dirty()) { - {{ 'builder.unsavedChanges' | translate }} - } - @if (issues().length > 0) { -
    - @for (issue of issues(); track issue.code) { -
  • {{ issue.message | translate }}
  • - } -
- } -
-
- - -
-
-``` - -- [ ] **Step 3: Styles** - -Create `src/app/features/project-editor/components/save-bar/project-editor-save-bar.component.scss`: - -```scss -.project-editor-save-bar { - position: sticky; - bottom: 0; - display: flex; - justify-content: space-between; - align-items: flex-start; - gap: 1rem; - padding: 0.75rem 1rem; - background: var(--surface, #fff); - border-top: 1px solid var(--border, #ddd); - z-index: 5; -} - -.project-editor-save-bar-dirty { - color: var(--warning, #b45309); - margin-left: 0.5rem; -} - -.project-editor-save-bar-issues { - margin: 0.25rem 0 0; - padding-left: 1.25rem; - color: var(--danger, #b91c1c); -} - -.project-editor-save-bar-actions button + button { - margin-left: 0.5rem; -} -``` - -- [ ] **Step 4: Mount it in the page** - -In `src/app/features/project-editor/pages/project-editor-page.component.ts`, add the import and register in `imports`: - -```typescript -import { ProjectEditorSaveBarComponent } from '../components/save-bar/project-editor-save-bar.component'; -``` - -In `src/app/features/project-editor/pages/project-editor-page.component.html`, add the save bar after the closing `` of `project-editor-stack` (still inside the `@else` block, as a sibling after the stack section): - -```html - -``` - -- [ ] **Step 5: i18n keys** - -In `src/app/i18n/translations.ts`, add to the `builder` interface: - -```typescript - statusDraft: string; - statusPublished: string; - unsavedChanges: string; - save: string; - publish: string; -``` - -In `src/app/i18n/en.ts`, add: - -```typescript - statusDraft: 'Draft', - statusPublished: 'Published', - unsavedChanges: 'Unsaved changes', - save: 'Save', - publish: 'Publish', -``` - -In `src/app/i18n/ru.ts`, add: - -```typescript - statusDraft: 'Черновик', - statusPublished: 'Опубликовано', - unsavedChanges: 'Есть несохранённые изменения', - save: 'Сохранить', - publish: 'Опубликовать', -``` - -In `src/app/i18n/hy.ts`, add: - -```typescript - statusDraft: 'Սևագիր', - statusPublished: 'Հրապարակված', - unsavedChanges: 'Չպահված փոփոխություններ', - save: 'Պահպանել', - publish: 'Հրապարակել', -``` - -- [ ] **Step 6: Manual verification** - -Run `ng serve`, open `/edit/general`. Confirm the sticky bar at the bottom shows "Draft" and no "Unsaved changes" label initially. Change the marketplace name — confirm "Unsaved changes" appears immediately. If your test bootstrap has no `branding.logoUrl`, confirm a validation message ("Branding is missing a logo.") appears in the bar and the Publish button is disabled; set a logo URL on the Branding tab and confirm the message disappears and Publish becomes enabled. Click Save — confirm "Unsaved changes" disappears (status stays Draft). Click Publish — confirm status flips to "Published". - -- [ ] **Step 7: Commit** - -```bash -git add src/app/features/project-editor/components/save-bar/ src/app/features/project-editor/pages/project-editor-page.component.ts src/app/features/project-editor/pages/project-editor-page.component.html src/app/i18n/translations.ts src/app/i18n/en.ts src/app/i18n/ru.ts src/app/i18n/hy.ts -git commit -m "feat(project-editor): add sticky save/publish bar with validation summary" -``` - ---- - -### Task 10: Warn before losing unsaved changes - -**Files:** -- Create: `src/app/features/project-editor/guards/project-editor-dirty.guard.ts` -- Modify: `src/app/app.routes.ts` -- Modify: `src/app/features/project-editor/pages/project-editor-page.component.ts` - -**Interfaces:** -- Consumes: `ProjectEditorFacade.dirty` (Task 8). -- Produces: `projectEditorDirtyGuard: CanDeactivateFn`, wired as `canDeactivate` on the `edit/:section` route. - -- [ ] **Step 1: Guard** - -Create `src/app/features/project-editor/guards/project-editor-dirty.guard.ts`: - -```typescript -import { inject } from '@angular/core'; -import { CanDeactivateFn } from '@angular/router'; -import { ProjectEditorFacade } from '../facade/project-editor.facade'; -import { ProjectEditorPageComponent } from '../pages/project-editor-page.component'; - -export const projectEditorDirtyGuard: CanDeactivateFn = () => { - const facade = inject(ProjectEditorFacade); - if (!facade.dirty()) { - return true; - } - return window.confirm('You have unsaved changes. Leave anyway?'); -}; -``` - -- [ ] **Step 2: Wire it into the route** - -In `src/app/app.routes.ts`, update the `edit/:section` route added in Task 1: - -```typescript - { - path: 'edit/:section', - loadComponent: () => import('./features/project-editor/pages/project-editor-page.component').then(m => m.ProjectEditorPageComponent), - canDeactivate: [() => import('./features/project-editor/guards/project-editor-dirty.guard').then(m => m.projectEditorDirtyGuard())] - }, -``` - -- [ ] **Step 3: Warn on tab/window close** - -In `src/app/features/project-editor/pages/project-editor-page.component.ts`, add `HostListener` import and a listener method: - -```typescript -import { ChangeDetectionStrategy, Component, HostListener, effect, inject } from '@angular/core'; -``` - -Add inside the class body: - -```typescript - @HostListener('window:beforeunload', ['$event']) - warnBeforeUnload(event: BeforeUnloadEvent): void { - if (this.facade.dirty()) { - event.preventDefault(); - event.returnValue = ''; - } - } -``` - -- [ ] **Step 4: Manual verification** - -Run `ng serve`, open `/edit/general`, change the marketplace name (dirty state is now true). Try to navigate to `/catalog` via the URL bar or a link — confirm a native "Leave site?" confirm dialog appears; cancel it and confirm you're still on the editor. Confirm it, and confirm you land on `/catalog`. Reload the editor, make a change, then try closing/reloading the tab — confirm the browser's native "leave site" prompt appears (exact wording is browser-controlled, not customizable — this is expected `beforeunload` behavior). - -- [ ] **Step 5: Commit** - -```bash -git add src/app/features/project-editor/guards/project-editor-dirty.guard.ts src/app/app.routes.ts src/app/features/project-editor/pages/project-editor-page.component.ts -git commit -m "feat(project-editor): warn before leaving with unsaved changes" -``` - ---- - -### Task 11: Documentation - -**Files:** -- Modify: `docs/Project-Editor.md` -- Create: `docs/context/features/project-editor/FACTS.jsonl` - -**Interfaces:** -- Consumes: nothing (documentation only). -- Produces: nothing consumed by code — this is the "every new feature must include documentation" deliverable from the platform's coding rules and this plan's Global Constraints. - -- [ ] **Step 1: Update `docs/Project-Editor.md`** - -Replace the `## Supported Sections` list item order and add new subsections after `Preview`, and replace `## Future Backend Endpoints` with a section documenting the current client-side draft/publish behavior and the still-needed backend contract. Insert after the existing `- Preview` bullet block (before `## Preview Strategy`): - -```markdown -- Languages (Sprint 16) - - add/remove supported locale - - set default locale - - generically syncs translation keys across static pages and navigation labels (`LocaleSyncService`) -- Navigation (Sprint 16) - - header navigation: add/remove/reorder/edit label/URL/visibility - - flat footer navigation: same actions - - grouped footer navigation (column-based) is read-only in this tab for now -``` - -Replace the `## Future Backend Endpoints` section with: - -```markdown -## Draft / Publish (Sprint 16) - -There is still no backend draft/publish API. This sprint models it client-side -in `ProjectEditorFacade`: -- `status: 'draft' | 'published'` and `dirty` (diffed against the - last-saved snapshot) live in facade state. -- `save()` snapshots the current in-memory bootstrap as "last saved" (no - network call yet). -- `publish()` runs `ProjectValidator`, and if there are no issues, applies - the bootstrap via `PlatformRuntimeService.reloadFromBootstrap` and marks - status `published`. - -**Backend gap, not yet implemented:** real persistence needs -`PUT /builder/bootstrap/draft` and `POST /builder/bootstrap/publish` -endpoints so drafts/publishes survive a reload and are shared across editors. - -## Validation - -`ProjectValidator` (`services/project-validator.service.ts`) runs on every -render of the save bar: missing logo, no languages, invalid marketplace URL, -duplicate static-page slugs, empty homepage, a homepage widget with no -`type`, duplicate header navigation links, invalid theme colors. Publish is -blocked while any issue is present; Save is not. - -## Rich HTML editing - -Static page HTML is edited via `MarketplaceHtmlEditorComponent` -(`components/html-editor/`), a `contentEditable` + toolbar component with no -external dependency. It emits raw HTML on every change and never sanitizes — -sanitization remains a storefront-render concern. -``` - -- [ ] **Step 2: Fact pack** - -Create the directory and file `docs/context/features/project-editor/FACTS.jsonl`: - -``` -{"id":"PE-20260713T010000Z-0001","subject":"project-editor-routing","predicate":"is","object":"flat routes under /edit/:section (no projectId — a project is the domain-resolved tenant); /builder and /project-editor redirect to /edit/general","src":["docs/superpowers/specs/2026-07-13-marketplace-project-editor-sprint16-design.md","src/app/app.routes.ts"],"status":"active","kind":"decision","updated_at":"2026-07-13T01:00:00Z","confidence":"high","tags":["project-editor","routing"]} -{"id":"PE-20260713T010000Z-0002","subject":"locale-sync","predicate":"is-implemented-by","object":"LocaleSyncService, which generically adds/removes a locale key across static page translations and navigation labels without per-field hardcoding","src":["src/app/features/project-editor/services/locale-sync.service.ts"],"status":"active","kind":"implemented","confidence":"high","updated_at":"2026-07-13T01:00:00Z","tags":["project-editor","i18n"]} -{"id":"PE-20260713T010000Z-0003","subject":"draft-publish-flow","predicate":"is","object":"client-side only (ProjectEditorFacade.status/dirty/save/publish) because no backend draft/publish endpoint exists yet; PUT /builder/bootstrap/draft and POST /builder/bootstrap/publish are the documented backend gap","src":["docs/Project-Editor.md","src/app/features/project-editor/facade/project-editor.facade.ts"],"status":"active","kind":"constraint","confidence":"high","updated_at":"2026-07-13T01:00:00Z","tags":["project-editor","backend-gap"]} -{"id":"PE-20260713T010000Z-0004","subject":"html-editing","predicate":"uses","object":"MarketplaceHtmlEditorComponent, a contentEditable + toolbar component with no external rich-text dependency; emits raw HTML, never sanitizes during editing","src":["src/app/features/project-editor/components/html-editor/marketplace-html-editor.component.ts"],"status":"active","kind":"decision","confidence":"high","updated_at":"2026-07-13T01:00:00Z","tags":["project-editor","html-editor"]} -{"id":"PE-20260713T010000Z-0005","subject":"navigation-tab","predicate":"supports","object":"header navigation and flat-list footer navigation (add/remove/reorder/edit); grouped-column footer navigation is read-only until a future sprint","src":["src/app/features/project-editor/sections/navigation-section.component.ts"],"status":"active","kind":"constraint","confidence":"high","updated_at":"2026-07-13T01:00:00Z","tags":["project-editor","navigation"]} -``` - -- [ ] **Step 3: Manual verification** - -Read both files back to confirm they render/parse: `cat docs/Project-Editor.md` (visually check the new sections landed in the right place) and validate the JSONL parses one-object-per-line: run `node -e "require('fs').readFileSync('docs/context/features/project-editor/FACTS.jsonl','utf8').trim().split('\n').forEach(l => JSON.parse(l))"` — expected: no output (no thrown error) means every line is valid JSON. - -- [ ] **Step 4: Commit** - -```bash -git add docs/Project-Editor.md docs/context/features/project-editor/FACTS.jsonl -git commit -m "docs(project-editor): document Sprint 16 tabs, draft/publish gap, and validation rules" -``` - ---- - -## Follow-ups (explicitly out of scope, do not fold into the above) - -- No test runner exists in this repo (`package.json` has none). Recommend a follow-up to add Karma+Jasmine or Jest so this feature (and everything else) gets real automated tests. -- Real backend `PUT /builder/bootstrap/draft` / `POST /builder/bootstrap/publish` endpoints. -- A genuinely separate Angular app/deployment for `admin.marketplace.com`, replacing this sprint's same-build flat routes. -- Grouped (column-based) footer navigation editing. diff --git a/docs/superpowers/specs/2026-07-13-marketplace-project-editor-sprint16-design.md b/docs/superpowers/specs/2026-07-13-marketplace-project-editor-sprint16-design.md deleted file mode 100644 index 7420360..0000000 --- a/docs/superpowers/specs/2026-07-13-marketplace-project-editor-sprint16-design.md +++ /dev/null @@ -1,175 +0,0 @@ -# Sprint 16 — Marketplace Project Editor MVP - -Status: approved for planning -Date: 2026-07-13 -Related: [ADR-0001](../../context/adrs/ADR-0001-marketplace-platform-vision.md), [Project-Editor.md](../../Project-Editor.md) - -## Goal - -A client can open the editor for their marketplace, edit settings, save a -draft, preview, and publish — without touching JSON by hand. The editor edits -the same `BootstrapConfig` the storefront consumes. No parallel/duplicate -configuration model is introduced anywhere in this work. - -## Current state (as of this sprint) - -A working editor already exists at `/builder` -(`src/app/features/project-editor/`): `ProjectEditorPageComponent` + signal-based -`ProjectEditorFacade` (no NgRx), sections for general, branding, theme, header, -footer, homepage (already has drag-and-drop reordering), widgets, -static-pages (via `StaticPagesEditorComponent`), features, preview. Static -page HTML is edited via a plain `