The seller dashboard is the most complex Angular feature in the classified website — it combines a listing management table, real-time status updates, a multi-step creation wizard, and seller analytics. The CreateListingWizard is a multi-step reactive form that validates each step before proceeding, maintains state across steps (including uploaded photos), and submits the complete listing as a single command when the seller clicks “Publish.”
Seller Dashboard Components
// ── MyListingsComponent — seller's listing management table ───────────────
@Component({
selector: 'app-my-listings',
standalone: true,
imports: [MatTableModule, MatButtonModule, MatChipsModule,
MatSelectModule, MatPaginatorModule, RouterLink, DatePipe],
template: `
<!-- Statistics panel ────────────────────────────────────────────────── -->
<div class="stats-row">
@for (stat of stats(); track stat.label) {
<div class="stat-card">
<span class="stat-value">{{ stat.value }}</span>
<span class="stat-label">{{ stat.label }}</span>
</div>
}
</div>
<!-- Action bar ──────────────────────────────────────────────────────── -->
<div class="actions-bar">
<button mat-raised-button color="primary"
routerLink="/my-listings/new"
data-cy="create-listing-btn">
+ New Listing
</button>
@if (selection.hasValue()) {
<button mat-button (click)="onBulkExpire()">
Expire Selected ({{ selection.selected.length }})
</button>
}
</div>
<!-- Listings table ──────────────────────────────────────────────────── -->
<mat-table [dataSource]="listings()" data-cy="listings-table">
<!-- Checkbox column ───────────────────────────────────────────────── -->
<ng-container matColumnDef="select">
<mat-header-cell *matHeaderCellDef>
<mat-checkbox (change)="toggleAll($event)"
[checked]="allSelected()"></mat-checkbox>
</mat-header-cell>
<mat-cell *matCellDef="let row">
<mat-checkbox [checked]="selection.isSelected(row)"
(change)="selection.toggle(row)"></mat-checkbox>
</mat-cell>
</ng-container>
<!-- Title ─────────────────────────────────────────────────────────── -->
<ng-container matColumnDef="title">
<mat-header-cell *matHeaderCellDef>Title</mat-header-cell>
<mat-cell *matCellDef="let listing" data-cy="listing-title">
<a [routerLink]="['/listings', listing.id]">{{ listing.title }}</a>
</mat-cell>
</ng-container>
<!-- Status badge ──────────────────────────────────────────────────── -->
<ng-container matColumnDef="status">
<mat-header-cell *matHeaderCellDef>Status</mat-header-cell>
<mat-cell *matCellDef="let listing">
<mat-chip [color]="statusColour(listing.status)">
{{ listing.status }}
</mat-chip>
</mat-cell>
</ng-container>
<!-- Actions ───────────────────────────────────────────────────────── -->
<ng-container matColumnDef="actions">
<mat-header-cell *matHeaderCellDef></mat-header-cell>
<mat-cell *matCellDef="let listing">
@switch (listing.status) {
@case ('Draft') {
<button mat-button (click)="onPublish(listing.id)">Publish</button>
}
@case ('Expired') {
<button mat-button (click)="onRenew(listing.id)">Renew</button>
}
@case ('Active') {
<button mat-button (click)="onMarkSold(listing.id)">Mark Sold</button>
}
}
<button mat-icon-button [routerLink]="['/my-listings', listing.id, 'edit']">
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button color="warn"
(click)="onDelete(listing.id)">
<mat-icon>delete</mat-icon>
</button>
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns"
[class.pending]="row.status === 'PendingReview'"></mat-row>
</mat-table>
<mat-paginator [pageSize]="10" (page)="onPageChange($event)" />
`,
})
export class MyListingsComponent implements OnInit {
private listingsApi = inject(ListingsApiService);
private router = inject(Router);
private notify = inject(ToastService);
listings = signal<MyListingDto[]>([]);
stats = signal<StatCard[]>([]);
loading = signal(true);
selection = new SelectionModel<MyListingDto>(true, []);
displayedColumns = ['select', 'title', 'price', 'status', 'views',
'contacts', 'publishedAt', 'expiresAt', 'actions'];
allSelected = computed(() =>
this.listings().length > 0 &&
this.selection.selected.length === this.listings().length);
statusColour = (status: string): string => ({
Active: 'primary',
PendingReview: 'accent',
Expired: 'warn',
Draft: '',
Rejected: 'warn',
}[status] ?? '');
ngOnInit() {
this.loadListings();
this.loadStats();
}
private loadListings(page = 1): void {
this.listingsApi.getMyListings(page).pipe(
finalize(() => this.loading.set(false)),
).subscribe(result => this.listings.set(result.items));
}
onPublish(id: string): void {
this.listingsApi.publish(id).subscribe(() => {
this.notify.success('Listing published!');
this.loadListings();
});
}
}
@switch block in the actions column renders different action buttons based on the listing’s status — only draft listings show “Publish,” only expired listings show “Renew,” only active listings show “Mark Sold.” This prevents impossible actions (publishing an already-active listing) at the UI level, complementing the domain invariant checks in the backend. The UI provides a clear affordance; the backend enforces the rule.SelectionModel from @angular/cdk/collections manages the checkboxes in the table without custom state management. It tracks which rows are selected (selection.isSelected(row)), provides toggle (selection.toggle(row)), select all (selection.select(...all)), and clear (selection.clear()) operations. Use it for any Angular Material table that needs row selection — it integrates with mat-checkbox cleanly and provides the hasValue() check for conditional action buttons.Common Mistakes
Mistake 1 — Allowing impossible status transitions in the UI (confusing UX)
❌ Wrong — “Publish” button shown for Active listings; user clicks it; backend returns 400; confusing error with no context.
✅ Correct — @switch (listing.status) only shows relevant actions; impossible actions never offered; clean UX.
Mistake 2 — No confirmation for irreversible actions (accidental data loss)
❌ Wrong — single-click delete or mark-as-sold; user fat-fingers the button; listing gone; no undo.
✅ Correct — MatDialog confirmation for delete and mark-sold; user must explicitly confirm before irreversible action.