chore: remove confirmed dead code
Dead code sweep verified manually against app.routes.ts, DI registries, and
cross-repo grep for every candidate (per prior false-positive incident with
knip on pages/**). Deleted only what has zero reachable reference:
Auth (unregistered, comment-only mention):
- core/auth/guards/ed25519-auth.guard.ts - ed25519AuthGuard never imported;
only mentioned inside a doc-comment in admin-login-page.component.ts.
- core/auth/guards/permission.guard.ts - permissionGuard never imported.
- core/auth/interceptors/auth.interceptor.ts - authInterceptor not present
in app.config.ts's withInterceptors([...]) list; not imported elsewhere.
Search feature:
- features/search/services/search-analytics.service.ts - SearchAnalyticsService
never imported outside its own file.
- features/search/components/empty-results/* - app-search-empty-results
selector never used in any template; search-bar.component.html implements
its own inline @if (noResults) empty state instead.
Content management:
- features/content-management/pages/content-management-page.component.ts -
thin wrapper around StaticPagesEditorComponent with zero route pointing at
it in app.routes.ts. The rest of features/content-management/* (facade,
static-pages-editor, page-editor, etc.) remains: it is used by
project-editor and stays.
Backoffice CRUD scaffolding (re-verified the UI-COMPOSITION-REVIEW.md claim
independently): app.routes.ts backoffice section only loads
features/admin/{dashboard,products,categories,transactions,orders,customers,
moderation,users,monitoring,analytics} and features/backoffice/media. Grepped
every other backoffice/* folder for cross-references - none found.
- features/backoffice/{categories,customers,inventory,orders,products,settings}
- each contained only a placeholder .gitkeep from the original scaffold
commit (b957112); no real components were ever added, so this is not the
"duplicate implementation" the prior doc described, just unused scaffold
dirs. Removing corrects that doc's premise.
- features/backoffice/shared/backoffice-coming-soon-page.component.* - only
consumer would have been those scaffold dirs; unreferenced elsewhere.
- assets/mock/backoffice/{customers,orders}/list.json - mock data with no
corresponding fetch call; BackofficeDataProvider only exposes
loadProducts()/loadCategories(), backed by the products/categories mock
files, which are kept.
Dead shared barrels/models (no importer anywhere in src/app):
- shared/index.ts, shared/models/index.ts, shared/types/index.ts - unused
re-export barrels.
- shared/models/domain/index.ts + user-preferences.model.ts (whole domain/
subfolder) - UserPreferences interface has zero consumers.
Storefront pages (pages/public/platform-home.component.ts) - PlatformHomeComponent
has no route in app.routes.ts and is not imported anywhere; distinct from the
pages/category, pages/search, pages/info/**, pages/legal/**, pages/item-detail
components which ARE routed and were correctly left untouched.
Verification: npx tsc --noEmit -p tsconfig.app.json clean after each batch;
npm run build succeeded (pre-existing initial-bundle-budget warning only,
unrelated to this change).
This commit is contained in:
@@ -1,25 +0,0 @@
|
|||||||
import { inject } from '@angular/core';
|
|
||||||
import { CanActivateFn, Router } from '@angular/router';
|
|
||||||
import { SessionService } from '../services/session.service';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Guards routes under the Ed25519 JWT flow. Not wired onto any live route
|
|
||||||
* yet (see docs/AUTH.md cutover plan) - `adminAuthGuard`
|
|
||||||
* (`core/admin-auth/admin-auth.guard.ts`) remains the active guard for
|
|
||||||
* `/backoffice` and `/edit` until the backend ships the challenge/verify
|
|
||||||
* endpoints this depends on.
|
|
||||||
*/
|
|
||||||
export const ed25519AuthGuard: CanActivateFn = () => {
|
|
||||||
const session = inject(SessionService);
|
|
||||||
const router = inject(Router);
|
|
||||||
|
|
||||||
if (session.isAuthenticated()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (session.status() === 'expired') {
|
|
||||||
return router.parseUrl('/admin-login/error/session-expired');
|
|
||||||
}
|
|
||||||
|
|
||||||
return router.parseUrl('/admin-login');
|
|
||||||
};
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { inject } from '@angular/core';
|
|
||||||
import { CanActivateFn, Router } from '@angular/router';
|
|
||||||
import { Permission } from '../models/permission.model';
|
|
||||||
import { PermissionService } from '../services/permission.service';
|
|
||||||
import { SessionService } from '../services/session.service';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Factory guard: `permissionGuard('users.manage')` in a route's
|
|
||||||
* `canActivate`. Composes with `ed25519AuthGuard` - route to this only after
|
|
||||||
* confirming authentication, since an unauthenticated user has no role and
|
|
||||||
* would otherwise always be routed to `forbidden` instead of the login page.
|
|
||||||
*/
|
|
||||||
export function permissionGuard(required: Permission): CanActivateFn {
|
|
||||||
return () => {
|
|
||||||
const session = inject(SessionService);
|
|
||||||
const permissions = inject(PermissionService);
|
|
||||||
const router = inject(Router);
|
|
||||||
|
|
||||||
if (!session.isAuthenticated()) {
|
|
||||||
return router.parseUrl('/admin-login');
|
|
||||||
}
|
|
||||||
|
|
||||||
return permissions.has(required) ? true : router.parseUrl('/admin-login/error/forbidden');
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import { HttpErrorResponse, HttpInterceptorFn } from '@angular/common/http';
|
|
||||||
import { inject } from '@angular/core';
|
|
||||||
import { Router } from '@angular/router';
|
|
||||||
import { catchError, switchMap, throwError } from 'rxjs';
|
|
||||||
import { SessionService } from '../services/session.service';
|
|
||||||
import { AuthService } from '../services/auth.service';
|
|
||||||
|
|
||||||
/** Paths gated by the Ed25519 JWT once it is the live admin auth mechanism. Kept identical to adminAuthHeadersInterceptor's list for consistency. */
|
|
||||||
const ADMIN_GATED_PATH_SEGMENTS = ['/admin/', '/backoffice/', '/builder/', '/media/'];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attaches `Authorization: Bearer <jwt>` to admin API requests and, on a 401,
|
|
||||||
* attempts a single silent refresh-then-retry before giving up and routing
|
|
||||||
* to the session-expired screen. Not registered in app.config.ts yet - this
|
|
||||||
* activates once the Ed25519 flow replaces (or runs alongside)
|
|
||||||
* adminAuthHeadersInterceptor; see docs/AUTH.md for the cutover plan.
|
|
||||||
*/
|
|
||||||
export const authInterceptor: HttpInterceptorFn = (req, next) => {
|
|
||||||
const isAdminRequest = ADMIN_GATED_PATH_SEGMENTS.some(segment => req.url.includes(segment));
|
|
||||||
if (!isAdminRequest) {
|
|
||||||
return next(req);
|
|
||||||
}
|
|
||||||
|
|
||||||
const session = inject(SessionService);
|
|
||||||
const auth = inject(AuthService);
|
|
||||||
const router = inject(Router);
|
|
||||||
|
|
||||||
const token = session.token();
|
|
||||||
const authedReq = token ? req.clone({ headers: req.headers.set('Authorization', `Bearer ${token}`) }) : req;
|
|
||||||
|
|
||||||
return next(authedReq).pipe(
|
|
||||||
catchError((error: unknown) => {
|
|
||||||
if (!(error instanceof HttpErrorResponse) || error.status !== 401 || !session.getRefreshToken()) {
|
|
||||||
return throwError(() => error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return auth.refresh().pipe(
|
|
||||||
switchMap(refreshed => next(req.clone({ headers: req.headers.set('Authorization', `Bearer ${refreshed.token}`) }))),
|
|
||||||
catchError(refreshError => {
|
|
||||||
router.navigate(['/admin-login/error', 'session-expired']);
|
|
||||||
return throwError(() => refreshError);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<div class="coming-soon">
|
|
||||||
<h1 class="coming-soon__title">{{ titleKey | translate }}</h1>
|
|
||||||
<p class="coming-soon__description">{{ 'dashboard.comingSoonDescription' | translate }}</p>
|
|
||||||
<a class="coming-soon__link" [routerLink]="['../dashboard']">{{ 'dashboard.backToDashboard' | translate }}</a>
|
|
||||||
</div>
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
.coming-soon {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 12px;
|
|
||||||
max-width: 640px;
|
|
||||||
margin: 40px auto;
|
|
||||||
padding: 24px;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.coming-soon__title {
|
|
||||||
margin: 0;
|
|
||||||
font-size: var(--font-size-3xl, 1.5rem);
|
|
||||||
font-weight: 800;
|
|
||||||
color: var(--text-primary, #1e3c38);
|
|
||||||
}
|
|
||||||
|
|
||||||
.coming-soon__description {
|
|
||||||
margin: 0;
|
|
||||||
color: var(--text-secondary, #667a77);
|
|
||||||
}
|
|
||||||
|
|
||||||
.coming-soon__link {
|
|
||||||
color: var(--primary-color, #497671);
|
|
||||||
font-weight: var(--font-weight-semibold, 600);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.coming-soon__link:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { ActivatedRoute, RouterLink } from '@angular/router';
|
|
||||||
import { TranslatePipe } from '../../../i18n/translate.pipe';
|
|
||||||
|
|
||||||
/** Landing page for backoffice sections not built yet (categories, static pages, transactions, orders, media). Route `data.titleKey` sets the section name; falls back to the generic "coming soon" title. */
|
|
||||||
@Component({
|
|
||||||
selector: 'app-backoffice-coming-soon-page',
|
|
||||||
standalone: true,
|
|
||||||
imports: [CommonModule, RouterLink, TranslatePipe],
|
|
||||||
templateUrl: './backoffice-coming-soon-page.component.html',
|
|
||||||
styleUrls: ['./backoffice-coming-soon-page.component.scss'],
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
||||||
})
|
|
||||||
export class BackofficeComingSoonPageComponent {
|
|
||||||
private readonly route = inject(ActivatedRoute);
|
|
||||||
|
|
||||||
readonly titleKey: string = this.route.snapshot.data['titleKey'] ?? 'dashboard.comingSoonTitle';
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
|
||||||
import { StaticPagesEditorComponent } from '../components/static-pages-editor.component';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-content-management-page',
|
|
||||||
standalone: true,
|
|
||||||
imports: [StaticPagesEditorComponent],
|
|
||||||
template: `<app-static-pages-editor />`,
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
|
||||||
})
|
|
||||||
export class ContentManagementPageComponent {}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
<section class="search-empty-results card" [attr.aria-label]="'search.emptyResultsAria' | translate">
|
|
||||||
<h3>{{ 'search.noResults' | translate }}</h3>
|
|
||||||
|
|
||||||
<button type="button" class="reset-btn" (click)="resetFilters.emit()">{{ 'catalog.clearFilters' | translate }}</button>
|
|
||||||
|
|
||||||
@if (popularCategories.length > 0) {
|
|
||||||
<div class="block">
|
|
||||||
<strong>{{ 'search.popularCategories' | translate }}</strong>
|
|
||||||
<div class="chip-list">
|
|
||||||
@for (category of popularCategories; track category.id) {
|
|
||||||
<button type="button" class="chip" (click)="popularCategorySelected.emit(category.id)">{{ category.label }}</button>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (popularSearches.length > 0) {
|
|
||||||
<div class="block">
|
|
||||||
<strong>{{ 'catalog.popularSearchesTitle' | translate }}</strong>
|
|
||||||
<div class="chip-list">
|
|
||||||
@for (item of popularSearches; track $index) {
|
|
||||||
<button type="button" class="chip" (click)="popularSearchSelected.emit(item)">{{ item }}</button>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (recommendedProducts.length > 0) {
|
|
||||||
<div class="block">
|
|
||||||
<strong>{{ 'search.recommendedProducts' | translate }}</strong>
|
|
||||||
<div class="product-grid">
|
|
||||||
@for (product of recommendedProducts; track product.itemID) {
|
|
||||||
<app-product-card
|
|
||||||
[item]="product"
|
|
||||||
[title]="product.name"
|
|
||||||
appearance="compact"
|
|
||||||
[addToCartLabel]="'catalog.addToCart' | translate"
|
|
||||||
(selected)="productSelected.emit(product)"
|
|
||||||
(addToCart)="productAddToCart.emit({ product, event: $event.event })" />
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</section>
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
.search-empty-results {
|
|
||||||
display: grid;
|
|
||||||
gap: 14px;
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reset-btn {
|
|
||||||
justify-self: start;
|
|
||||||
min-height: 40px;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
background: var(--bg-primary);
|
|
||||||
color: var(--text-primary);
|
|
||||||
padding: 0 14px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-weight: var(--font-weight-bold, 700);
|
|
||||||
}
|
|
||||||
|
|
||||||
.block {
|
|
||||||
display: grid;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chip-list {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chip {
|
|
||||||
min-height: 36px;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: var(--radius-full, 999px);
|
|
||||||
background: var(--bg-primary);
|
|
||||||
color: var(--text-secondary);
|
|
||||||
padding: 0 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
|
|
||||||
import { Product } from '../../../../core/products/models/product-domain.model';
|
|
||||||
import { ProductCardComponent } from '../../../../components/product-card/product-card.component';
|
|
||||||
import { TranslatePipe } from '../../../../i18n/translate.pipe';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-search-empty-results',
|
|
||||||
standalone: true,
|
|
||||||
imports: [ProductCardComponent, TranslatePipe],
|
|
||||||
templateUrl: './search-empty-results.component.html',
|
|
||||||
styleUrls: ['./search-empty-results.component.scss'],
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
|
||||||
})
|
|
||||||
export class SearchEmptyResultsComponent {
|
|
||||||
@Input() popularCategories: Array<{ id: string | number; label: string }> = [];
|
|
||||||
@Input() popularSearches: string[] = [];
|
|
||||||
@Input() recommendedProducts: Product[] = [];
|
|
||||||
|
|
||||||
@Output() resetFilters = new EventEmitter<void>();
|
|
||||||
@Output() popularCategorySelected = new EventEmitter<string | number>();
|
|
||||||
@Output() popularSearchSelected = new EventEmitter<string>();
|
|
||||||
@Output() productSelected = new EventEmitter<Product>();
|
|
||||||
@Output() productAddToCart = new EventEmitter<{ product: Product; event: Event }>();
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { SearchAnalyticsEvent } from '../models/search.model';
|
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
|
||||||
export class SearchAnalyticsService {
|
|
||||||
buildEvent(input: {
|
|
||||||
query: string;
|
|
||||||
tenant: string;
|
|
||||||
language: string;
|
|
||||||
resultCount: number;
|
|
||||||
timestamp?: string;
|
|
||||||
}): SearchAnalyticsEvent {
|
|
||||||
return {
|
|
||||||
query: input.query,
|
|
||||||
tenant: input.tenant,
|
|
||||||
language: input.language,
|
|
||||||
timestamp: input.timestamp ?? new Date().toISOString(),
|
|
||||||
resultCount: input.resultCount,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';
|
|
||||||
import { Router } from '@angular/router';
|
|
||||||
import { DynamicPageLayoutComponent } from '../../layouts/containers/dynamic-page-layout.component';
|
|
||||||
import { PageRenderModel } from '../../dynamic-renderer/page-renderer/page-renderer.model';
|
|
||||||
import { WebsiteRuntimeFacade } from '../../facades/website/website-runtime.facade';
|
|
||||||
import { TranslatePipe } from '../../i18n/translate.pipe';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-platform-home',
|
|
||||||
standalone: true,
|
|
||||||
imports: [DynamicPageLayoutComponent, TranslatePipe],
|
|
||||||
template: `
|
|
||||||
@if (loading()) {
|
|
||||||
<section class="platform-loading section">
|
|
||||||
<div class="page-container">{{ 'platform.loadingPage' | translate }}</div>
|
|
||||||
</section>
|
|
||||||
} @else if (model()) {
|
|
||||||
<app-dynamic-page-layout [model]="model()" />
|
|
||||||
} @else {
|
|
||||||
<section class="platform-empty section">
|
|
||||||
<div class="page-container">{{ 'platform.emptyConfig' | translate }}</div>
|
|
||||||
</section>
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
styles: [
|
|
||||||
`
|
|
||||||
.platform-loading, .platform-empty { padding: 2rem; color: var(--text-secondary, #667a77); }
|
|
||||||
`
|
|
||||||
],
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
|
||||||
})
|
|
||||||
export class PlatformHomeComponent {
|
|
||||||
readonly model = signal<PageRenderModel | null>(null);
|
|
||||||
readonly loading = signal(true);
|
|
||||||
private readonly router = inject(Router);
|
|
||||||
|
|
||||||
constructor(private readonly websiteRuntime: WebsiteRuntimeFacade) {
|
|
||||||
this.websiteRuntime.getPageRenderModelForUrl(this.router.url).subscribe({
|
|
||||||
next: (model) => {
|
|
||||||
this.model.set(model);
|
|
||||||
this.loading.set(false);
|
|
||||||
},
|
|
||||||
error: () => {
|
|
||||||
this.model.set(null);
|
|
||||||
this.loading.set(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export * from './models';
|
|
||||||
export * from './types';
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './user-preferences.model';
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
export interface UserPreferences {
|
|
||||||
locale: string;
|
|
||||||
currency: string;
|
|
||||||
themeMode: 'light' | 'dark' | 'system';
|
|
||||||
compactMode?: boolean;
|
|
||||||
wishlistEnabled?: boolean;
|
|
||||||
notificationsEnabled?: boolean;
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export * as ConfigModels from './config';
|
|
||||||
export * as DomainModels from './domain';
|
|
||||||
export * as UiModels from './ui';
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './primitive.types';
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
[]
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
[]
|
|
||||||
Reference in New Issue
Block a user