feat(back-end front-end): upgrade to the order componen instead of payment and order-confirmed
All checks were successful
Build, Test and Deploy / test-backend (push) Successful in 39s
Build, Test and Deploy / build-and-push (push) Successful in 1m3s
Build, Test and Deploy / deploy (push) Successful in 9s

This commit is contained in:
2026-02-24 08:44:42 +01:00
parent c1652798b4
commit 699a968875
21 changed files with 717 additions and 761 deletions

View File

@@ -30,16 +30,12 @@ export const routes: Routes = [
loadComponent: () => import('./features/checkout/checkout.component').then(m => m.CheckoutComponent)
},
{
path: 'payment/:orderId',
loadComponent: () => import('./features/payment/payment.component').then(m => m.PaymentComponent)
path: 'order/:orderId',
loadComponent: () => import('./features/order/order.component').then(m => m.OrderComponent)
},
{
path: 'ordine/:orderId',
loadComponent: () => import('./features/payment/payment.component').then(m => m.PaymentComponent)
},
{
path: 'order-confirmed/:orderId',
loadComponent: () => import('./features/order-confirmed/order-confirmed.component').then(m => m.OrderConfirmedComponent)
path: 'co/:orderId',
loadComponent: () => import('./features/order/order.component').then(m => m.OrderComponent)
},
{
path: '',

View File

@@ -193,8 +193,7 @@ export class CheckoutComponent implements OnInit {
this.quoteService.createOrder(this.sessionId, orderRequest).subscribe({
next: (order) => {
console.log('Order created', order);
this.router.navigate(['/payment', order.id]);
this.router.navigate(['/order', order.id]);
},
error: (err) => {
console.error('Order creation failed', err);

View File

@@ -28,7 +28,6 @@
</p>
<ul class="calculator-list">
<li>{{ 'HOME.SEC_CALC_LIST_1' | translate }}</li>
<li>{{ 'HOME.SEC_CALC_LIST_2' | translate }}</li>
</ul>
</div>
<app-card class="quote-card">

View File

@@ -1,52 +0,0 @@
<div class="container hero">
<h1>{{ 'ORDER_CONFIRMED.TITLE' | translate }}</h1>
<p class="subtitle">{{ 'ORDER_CONFIRMED.SUBTITLE' | translate }}</p>
</div>
<div class="container">
<div class="confirmation-layout" *ngIf="order() as o">
<app-card class="status-card">
<div class="status-badge">{{ o.status === 'SHIPPED' ? ('TRACKING.STEP_SHIPPED' | translate) : ('ORDER_CONFIRMED.STATUS' | translate) }}</div>
<h2>{{ 'ORDER_CONFIRMED.HEADING' | translate }}</h2>
<p class="order-ref" *ngIf="orderNumber">
{{ 'ORDER_CONFIRMED.ORDER_REF' | translate }}: <strong>#{{ orderNumber }}</strong>
</p>
<div class="status-timeline">
<div class="timeline-step"
[class.active]="o.status === 'PENDING_PAYMENT' && o.paymentStatus !== 'REPORTED'"
[class.completed]="o.paymentStatus === 'REPORTED' || o.status !== 'PENDING_PAYMENT'">
<div class="circle">1</div>
<div class="label">{{ 'TRACKING.STEP_PENDING' | translate }}</div>
</div>
<div class="timeline-step"
[class.active]="o.paymentStatus === 'REPORTED' && o.status === 'PENDING_PAYMENT'"
[class.completed]="o.status === 'PAID' || o.status === 'IN_PRODUCTION' || o.status === 'SHIPPED' || o.status === 'COMPLETED'">
<div class="circle">2</div>
<div class="label">{{ 'TRACKING.STEP_REPORTED' | translate }}</div>
</div>
<div class="timeline-step"
[class.active]="o.status === 'PAID' || o.status === 'IN_PRODUCTION'"
[class.completed]="o.status === 'SHIPPED' || o.status === 'COMPLETED'">
<div class="circle">3</div>
<div class="label">{{ 'TRACKING.STEP_PRODUCTION' | translate }}</div>
</div>
<div class="timeline-step"
[class.active]="o.status === 'SHIPPED'"
[class.completed]="o.status === 'COMPLETED'">
<div class="circle">4</div>
<div class="label">{{ 'TRACKING.STEP_SHIPPED' | translate }}</div>
</div>
</div>
<div class="message-block">
<p>{{ 'ORDER_CONFIRMED.PROCESSING_TEXT' | translate }}</p>
<p>{{ 'ORDER_CONFIRMED.EMAIL_TEXT' | translate }}</p>
</div>
<div class="actions">
<app-button (click)="goHome()">{{ 'ORDER_CONFIRMED.BACK_HOME' | translate }}</app-button>
</div>
</app-card>
</div>
</div>

View File

@@ -1,159 +0,0 @@
.hero {
padding: var(--space-12) 0 var(--space-8);
text-align: center;
h1 {
font-size: 2.4rem;
margin-bottom: var(--space-2);
}
}
.subtitle {
font-size: 1.1rem;
color: var(--color-text-muted);
max-width: 720px;
margin: 0 auto;
}
.confirmation-layout {
max-width: 760px;
margin: 0 auto var(--space-12);
}
.status-badge {
display: inline-block;
padding: 0.35rem 0.65rem;
border-radius: 999px;
background: #eef8f0;
color: #136f2d;
font-weight: 700;
font-size: 0.85rem;
margin-bottom: var(--space-4);
}
h2 {
margin: 0 0 var(--space-3);
}
.order-ref {
margin: 0 0 var(--space-4);
color: var(--color-text-muted);
}
.message-block {
background: var(--color-neutral-100);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: var(--space-5);
margin-bottom: var(--space-6);
p {
margin: 0;
line-height: 1.45;
}
p + p {
margin-top: var(--space-3);
}
}
.actions {
max-width: 320px;
}
.status-timeline {
display: flex;
justify-content: space-between;
margin-bottom: var(--space-6);
position: relative;
&::before {
content: '';
position: absolute;
top: 15px;
left: 20px;
right: 20px;
height: 2px;
background: var(--color-border);
z-index: 1;
}
}
.timeline-step {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
z-index: 2;
flex: 1;
text-align: center;
.circle {
width: 32px;
height: 32px;
border-radius: 50%;
background: var(--color-neutral-100);
border: 2px solid var(--color-border);
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
margin-bottom: var(--space-2);
color: var(--color-text-muted);
transition: all 0.3s ease;
}
.label {
font-size: 0.85rem;
color: var(--color-text-muted);
font-weight: 500;
}
&.active {
.circle {
border-color: var(--color-primary);
background: var(--color-primary-light);
color: var(--color-primary);
}
.label {
color: var(--color-text);
font-weight: 600;
}
}
&.completed {
.circle {
background: var(--color-primary);
border-color: var(--color-primary);
color: white;
}
.label {
color: var(--color-text);
}
}
}
@media (max-width: 600px) {
.status-timeline {
flex-direction: column;
align-items: flex-start;
gap: var(--space-4);
&::before {
top: 10px;
bottom: 10px;
left: 15px;
width: 2px;
height: auto;
}
.timeline-step {
flex-direction: row;
gap: var(--space-3);
.circle {
margin-bottom: 0;
}
}
}
}

View File

@@ -1,50 +0,0 @@
import { Component, OnInit, inject, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ActivatedRoute, Router } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { AppButtonComponent } from '../../shared/components/app-button/app-button.component';
import { AppCardComponent } from '../../shared/components/app-card/app-card.component';
import { QuoteEstimatorService } from '../calculator/services/quote-estimator.service';
@Component({
selector: 'app-order-confirmed',
standalone: true,
imports: [CommonModule, TranslateModule, AppButtonComponent, AppCardComponent],
templateUrl: './order-confirmed.component.html',
styleUrl: './order-confirmed.component.scss'
})
export class OrderConfirmedComponent implements OnInit {
private route = inject(ActivatedRoute);
private router = inject(Router);
private quoteService = inject(QuoteEstimatorService);
orderId: string | null = null;
orderNumber: string | null = null;
order = signal<any>(null);
ngOnInit(): void {
this.orderId = this.route.snapshot.paramMap.get('orderId');
if (!this.orderId) {
return;
}
this.orderNumber = this.extractOrderNumber(this.orderId);
this.quoteService.getOrder(this.orderId).subscribe({
next: (order) => {
this.order.set(order);
this.orderNumber = order?.orderNumber ?? this.orderNumber;
},
error: () => {
// Keep fallback derived from UUID when API is unavailable.
}
});
}
goHome(): void {
this.router.navigate(['/']);
}
private extractOrderNumber(orderId: string): string {
return orderId.split('-')[0];
}
}

View File

@@ -0,0 +1,151 @@
<div class="container hero">
<h1>
{{ 'TRACKING.TITLE' | translate }}
<ng-container *ngIf="order()">
<br/><span class="order-id-title">#{{ getDisplayOrderNumber(order()) }}</span>
</ng-container>
</h1>
<p class="subtitle">{{ 'TRACKING.SUBTITLE' | translate }}</p>
</div>
<div class="container">
<ng-container *ngIf="order() as o">
<div class="status-timeline mb-6">
<div class="timeline-step"
[class.active]="o.status === 'PENDING_PAYMENT' && o.paymentStatus !== 'REPORTED'"
[class.completed]="o.paymentStatus === 'REPORTED' || o.status !== 'PENDING_PAYMENT'">
<div class="circle">1</div>
<div class="label">{{ 'TRACKING.STEP_PENDING' | translate }}</div>
</div>
<div class="timeline-step"
[class.active]="o.paymentStatus === 'REPORTED' && o.status === 'PENDING_PAYMENT'"
[class.completed]="o.status === 'PAID' || o.status === 'IN_PRODUCTION' || o.status === 'SHIPPED' || o.status === 'COMPLETED'">
<div class="circle">2</div>
<div class="label">{{ 'TRACKING.STEP_REPORTED' | translate }}</div>
</div>
<div class="timeline-step"
[class.active]="o.status === 'PAID' || o.status === 'IN_PRODUCTION'"
[class.completed]="o.status === 'SHIPPED' || o.status === 'COMPLETED'">
<div class="circle">3</div>
<div class="label">{{ 'TRACKING.STEP_PRODUCTION' | translate }}</div>
</div>
<div class="timeline-step"
[class.active]="o.status === 'SHIPPED'"
[class.completed]="o.status === 'COMPLETED'">
<div class="circle">4</div>
<div class="label">{{ 'TRACKING.STEP_SHIPPED' | translate }}</div>
</div>
</div>
<ng-container *ngIf="o.status === 'PENDING_PAYMENT'">
<app-card class="mb-6 status-reported-card" *ngIf="o.paymentStatus === 'REPORTED'">
<div class="status-content text-center">
<h3>{{ 'PAYMENT.STATUS_REPORTED_TITLE' | translate }}</h3>
<p>{{ 'PAYMENT.STATUS_REPORTED_DESC' | translate }}</p>
</div>
</app-card>
<div class="payment-layout">
<div class="payment-main">
<app-card class="mb-6">
<div class="card-header-simple">
<h3>{{ 'PAYMENT.METHOD' | translate }}</h3>
</div>
<div class="payment-selection">
<div class="methods-grid">
<div class="type-option" [class.selected]="selectedPaymentMethod === 'twint'" (click)="selectPayment('twint')">
<span class="method-name">{{ 'PAYMENT.METHOD_TWINT' | translate }}</span>
</div>
<div class="type-option" [class.selected]="selectedPaymentMethod === 'bill'" (click)="selectPayment('bill')">
<span class="method-name">{{ 'PAYMENT.METHOD_BANK' | translate }}</span>
</div>
</div>
</div>
<div class="payment-details fade-in text-center" *ngIf="selectedPaymentMethod === 'twint'">
<div class="details-header">
<h4>{{ 'PAYMENT.TWINT_TITLE' | translate }}</h4>
</div>
<div class="qr-placeholder">
<img *ngIf="twintQrUrl()" class="twint-qr" [src]="getTwintQrUrl()" (error)="onTwintQrError()" alt="TWINT payment QR" />
<p>{{ 'PAYMENT.TWINT_DESC' | translate }}</p>
<p class="billing-hint">{{ 'PAYMENT.BILLING_INFO_HINT' | translate }}</p>
<div class="twint-mobile-action">
<app-button (click)="openTwintPayment()" [fullWidth]="true">
{{ 'PAYMENT.TWINT_OPEN' | translate }}
</app-button>
</div>
<p class="amount">{{ 'PAYMENT.TOTAL' | translate }}: {{ o.totalChf | currency:'CHF' }}</p>
</div>
</div>
<div class="payment-details fade-in" *ngIf="selectedPaymentMethod === 'bill'">
<div class="details-header">
<h4>{{ 'PAYMENT.BANK_TITLE' | translate }}</h4>
</div>
<div class="bank-details">
<p><strong>{{ 'PAYMENT.BANK_OWNER' | translate }}:</strong> Küng, Joe</p>
<p><strong>{{ 'PAYMENT.BANK_IBAN' | translate }}:</strong> CH74 0900 0000 1548 2158 1</p>
<p><strong>{{ 'PAYMENT.BANK_REF' | translate }}:</strong> {{ getDisplayOrderNumber(o) }}</p>
<p class="billing-hint">{{ 'PAYMENT.BILLING_INFO_HINT' | translate }}</p>
<div class="qr-bill-actions">
<app-button (click)="downloadInvoice()">
{{ 'PAYMENT.DOWNLOAD_QR' | translate }}
</app-button>
</div>
</div>
</div>
<div class="actions">
<app-button variant="outline" (click)="completeOrder()" [disabled]="!selectedPaymentMethod || o.paymentStatus === 'REPORTED'" [fullWidth]="true">
{{ o.paymentStatus === 'REPORTED' ? ('PAYMENT.IN_VERIFICATION' | translate) : ('PAYMENT.CONFIRM' | translate) }}
</app-button>
</div>
</app-card>
</div>
<div class="payment-summary">
<app-card class="sticky-card">
<div class="card-header-simple">
<h3>{{ 'PAYMENT.SUMMARY_TITLE' | translate }}</h3>
<p class="order-id">#{{ getDisplayOrderNumber(o) }}</p>
</div>
<div class="summary-totals">
<div class="total-row">
<span>{{ 'PAYMENT.SUBTOTAL' | translate }}</span>
<span>{{ o.subtotalChf | currency:'CHF' }}</span>
</div>
<div class="total-row">
<span>{{ 'PAYMENT.SHIPPING' | translate }}</span>
<span>{{ o.shippingCostChf | currency:'CHF' }}</span>
</div>
<div class="total-row">
<span>{{ 'PAYMENT.SETUP_FEE' | translate }}</span>
<span>{{ o.setupCostChf | currency:'CHF' }}</span>
</div>
<div class="grand-total-row">
<span>{{ 'PAYMENT.TOTAL' | translate }}</span>
<span>{{ o.totalChf | currency:'CHF' }}</span>
</div>
</div>
</app-card>
</div>
</div>
</ng-container>
</ng-container>
<div *ngIf="loading()" class="loading-state">
<app-card>
<p>{{ 'PAYMENT.LOADING' | translate }}</p>
</app-card>
</div>
<div *ngIf="error()" class="error-message">
<app-card>
<p>{{ error() }}</p>
</app-card>
</div>
</div>

View File

@@ -234,3 +234,101 @@
margin-top: var(--space-12);
text-align: center;
}
.status-timeline {
display: flex;
justify-content: space-between;
margin-bottom: var(--space-8);
position: relative;
/* padding: var(--space-6); */ /* Removed if it was here to match non-card layout */
&::before {
content: '';
position: absolute;
top: 15px;
left: 12.5%;
right: 12.5%;
height: 2px;
background: var(--color-border);
z-index: 1;
}
}
.timeline-step {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
z-index: 2;
flex: 1;
text-align: center;
.circle {
width: 32px;
height: 32px;
border-radius: 50%;
background: var(--color-neutral-100);
border: 2px solid var(--color-border);
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
margin-bottom: var(--space-2);
color: var(--color-text-muted);
transition: all 0.3s ease;
}
.label {
font-size: 0.85rem;
color: var(--color-text-muted);
font-weight: 500;
}
&.active {
.circle {
border-color: var(--color-brand);
background: var(--color-bg);
color: var(--color-brand);
}
.label {
color: var(--color-text);
font-weight: 600;
}
}
&.completed {
.circle {
background: var(--color-brand);
border-color: var(--color-brand);
color: white;
}
.label {
color: var(--color-text);
}
}
}
@media (max-width: 600px) {
.status-timeline {
flex-direction: column;
align-items: flex-start;
gap: var(--space-4);
&::before {
top: 10px;
bottom: 10px;
left: 15px;
width: 2px;
height: auto;
}
.timeline-step {
flex-direction: row;
gap: var(--space-3);
.circle {
margin-bottom: 0;
}
}
}
}

View File

@@ -8,19 +8,19 @@ import { TranslateModule } from '@ngx-translate/core';
import { environment } from '../../../environments/environment';
@Component({
selector: 'app-payment',
selector: 'app-order',
standalone: true,
imports: [CommonModule, AppButtonComponent, AppCardComponent, TranslateModule],
templateUrl: './payment.component.html',
styleUrl: './payment.component.scss'
templateUrl: './order.component.html',
styleUrl: './order.component.scss'
})
export class PaymentComponent implements OnInit {
export class OrderComponent implements OnInit {
private route = inject(ActivatedRoute);
private router = inject(Router);
private quoteService = inject(QuoteEstimatorService);
orderId: string | null = null;
selectedPaymentMethod: 'twint' | 'bill' | null = null;
selectedPaymentMethod: 'twint' | 'bill' | null = 'twint';
order = signal<any>(null);
loading = signal(true);
error = signal<string | null>(null);

View File

@@ -1,129 +0,0 @@
<div class="container hero">
<h1>{{ 'PAYMENT.TITLE' | translate }}</h1>
<p class="subtitle">{{ 'CHECKOUT.SUBTITLE' | translate }}</p>
</div>
<div class="container">
<div class="payment-layout" *ngIf="order() as o">
<div class="payment-main">
<app-card class="mb-6 status-reported-card" *ngIf="o.paymentStatus === 'REPORTED'">
<div class="status-content text-center">
<h3>{{ 'PAYMENT.STATUS_REPORTED_TITLE' | translate }}</h3>
<p>{{ 'PAYMENT.STATUS_REPORTED_DESC' | translate }}</p>
</div>
</app-card>
<app-card class="mb-6">
<div class="card-header-simple">
<h3>{{ 'PAYMENT.METHOD' | translate }}</h3>
</div>
<div class="payment-selection">
<div class="methods-grid">
<div
class="type-option"
[class.selected]="selectedPaymentMethod === 'twint'"
(click)="selectPayment('twint')">
<span class="method-name">{{ 'PAYMENT.METHOD_TWINT' | translate }}</span>
</div>
<div
class="type-option"
[class.selected]="selectedPaymentMethod === 'bill'"
(click)="selectPayment('bill')">
<span class="method-name">{{ 'PAYMENT.METHOD_BANK' | translate }}</span>
</div>
</div>
</div>
<div class="payment-details fade-in text-center" *ngIf="selectedPaymentMethod === 'twint'">
<div class="details-header">
<h4>{{ 'PAYMENT.TWINT_TITLE' | translate }}</h4>
</div>
<div class="qr-placeholder">
<img
*ngIf="twintQrUrl()"
class="twint-qr"
[src]="getTwintQrUrl()"
(error)="onTwintQrError()"
alt="TWINT payment QR" />
<p>{{ 'PAYMENT.TWINT_DESC' | translate }}</p>
<p class="billing-hint">{{ 'PAYMENT.BILLING_INFO_HINT' | translate }}</p>
<div class="twint-mobile-action">
<app-button variant="outline" (click)="openTwintPayment()" [fullWidth]="true">
{{ 'PAYMENT.TWINT_OPEN' | translate }}
</app-button>
</div>
<p class="amount">{{ 'PAYMENT.TOTAL' | translate }}: {{ o.totalChf | currency:'CHF' }}</p>
</div>
</div>
<div class="payment-details fade-in" *ngIf="selectedPaymentMethod === 'bill'">
<div class="details-header">
<h4>{{ 'PAYMENT.BANK_TITLE' | translate }}</h4>
</div>
<div class="bank-details">
<p><strong>{{ 'PAYMENT.BANK_OWNER' | translate }}:</strong> 3D Fab Switzerland</p>
<p><strong>{{ 'PAYMENT.BANK_IBAN' | translate }}:</strong> CH98 0000 0000 0000 0000 0</p>
<p><strong>{{ 'PAYMENT.BANK_REF' | translate }}:</strong> {{ getDisplayOrderNumber(o) }}</p>
<p class="billing-hint">{{ 'PAYMENT.BILLING_INFO_HINT' | translate }}</p>
<div class="qr-bill-actions">
<app-button variant="outline" (click)="downloadInvoice()">
{{ 'PAYMENT.DOWNLOAD_QR' | translate }}
</app-button>
</div>
</div>
</div>
<div class="actions">
<app-button
(click)="completeOrder()"
[disabled]="!selectedPaymentMethod || o.paymentStatus === 'REPORTED'"
[fullWidth]="true">
{{ o.paymentStatus === 'REPORTED' ? ('PAYMENT.IN_VERIFICATION' | translate) : ('PAYMENT.CONFIRM' | translate) }}
</app-button>
</div>
</app-card>
</div>
<div class="payment-summary">
<app-card class="sticky-card">
<div class="card-header-simple">
<h3>{{ 'PAYMENT.SUMMARY_TITLE' | translate }}</h3>
<p class="order-id">#{{ getDisplayOrderNumber(o) }}</p>
</div>
<div class="summary-totals">
<div class="total-row">
<span>{{ 'PAYMENT.SUBTOTAL' | translate }}</span>
<span>{{ o.subtotalChf | currency:'CHF' }}</span>
</div>
<div class="total-row">
<span>{{ 'PAYMENT.SHIPPING' | translate }}</span>
<span>{{ o.shippingCostChf | currency:'CHF' }}</span>
</div>
<div class="total-row">
<span>{{ 'PAYMENT.SETUP_FEE' | translate }}</span>
<span>{{ o.setupCostChf | currency:'CHF' }}</span>
</div>
<div class="grand-total-row">
<span>{{ 'PAYMENT.TOTAL' | translate }}</span>
<span>{{ o.totalChf | currency:'CHF' }}</span>
</div>
</div>
</app-card>
</div>
</div>
<div *ngIf="loading()" class="loading-state">
<app-card>
<p>{{ 'PAYMENT.LOADING' | translate }}</p>
</app-card>
</div>
<div *ngIf="error()" class="error-message">
<app-card>
<p>{{ error() }}</p>
</app-card>
</div>
</div>

View File

@@ -1,12 +1,18 @@
<div class="container hero">
<h1>{{ 'SHOP.TITLE' | translate }}</h1>
<p class="subtitle">{{ 'SHOP.SUBTITLE' | translate }}</p>
</div>
<section class="wip-section">
<div class="container">
<div class="wip-card">
<p class="wip-eyebrow">{{ 'SHOP.WIP_EYEBROW' | translate }}</p>
<h1>{{ 'SHOP.WIP_TITLE' | translate }}</h1>
<p class="wip-subtitle">{{ 'SHOP.WIP_SUBTITLE' | translate }}</p>
<div class="container">
<div class="grid">
@for (product of products(); track product.id) {
<app-product-card [product]="product"></app-product-card>
}
<div class="wip-actions">
<app-button variant="primary" routerLink="/calculator/basic">
{{ 'SHOP.WIP_CTA_CALC' | translate }}
</app-button>
</div>
<p class="wip-return-later">{{ 'SHOP.WIP_RETURN_LATER' | translate }}</p>
<p class="wip-note">{{ 'SHOP.WIP_NOTE' | translate }}</p>
</div>
</div>
</div>
</section>

View File

@@ -1,7 +1,72 @@
.hero { padding: var(--space-8) 0; text-align: center; }
.subtitle { color: var(--color-text-muted); margin-bottom: var(--space-8); }
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: var(--space-6);
.wip-section {
position: relative;
padding: var(--space-12) 0;
background-color: var(--color-bg);
}
.wip-card {
max-width: 760px;
margin: 0 auto;
padding: clamp(1.4rem, 3vw, 2.4rem);
border: 1px solid var(--color-border);
border-radius: var(--radius-xl);
background: rgba(255, 255, 255, 0.95);
box-shadow: var(--shadow-lg);
text-align: center;
}
.wip-eyebrow {
display: inline-block;
margin-bottom: var(--space-3);
padding: 0.3rem 0.7rem;
border-radius: 999px;
border: 1px solid rgba(16, 24, 32, 0.14);
font-size: 0.78rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--color-secondary-600);
background: rgba(250, 207, 10, 0.28);
}
h1 {
font-size: clamp(1.7rem, 4vw, 2.5rem);
margin-bottom: var(--space-4);
color: var(--color-text);
}
.wip-subtitle {
max-width: 60ch;
margin: 0 auto var(--space-8);
color: var(--color-text-muted);
}
.wip-actions {
display: flex;
gap: var(--space-4);
justify-content: center;
flex-wrap: wrap;
}
.wip-note {
margin: var(--space-4) auto 0;
max-width: 62ch;
font-size: 0.95rem;
color: var(--color-secondary-600);
}
.wip-return-later {
margin: var(--space-6) 0 0;
font-weight: 600;
color: var(--color-secondary-600);
}
@media (max-width: 640px) {
.wip-section {
padding: var(--space-10) 0;
}
.wip-actions {
flex-direction: column;
align-items: stretch;
}
}

View File

@@ -1,22 +1,14 @@
import { Component, signal } from '@angular/core';
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterLink } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { ShopService, Product } from './services/shop.service';
import { ProductCardComponent } from './components/product-card/product-card.component';
import { AppButtonComponent } from '../../shared/components/app-button/app-button.component';
@Component({
selector: 'app-shop-page',
standalone: true,
imports: [CommonModule, TranslateModule, ProductCardComponent],
imports: [CommonModule, RouterLink, TranslateModule, AppButtonComponent],
templateUrl: './shop-page.component.html',
styleUrl: './shop-page.component.scss'
})
export class ShopPageComponent {
products = signal<Product[]>([]);
constructor(private shopService: ShopService) {
this.shopService.getProducts().subscribe(data => {
this.products.set(data);
});
}
}
export class ShopPageComponent {}

View File

@@ -73,8 +73,15 @@
"SHOP": {
"TITLE": "Technical solutions",
"SUBTITLE": "Ready-made products solving practical problems",
"WIP_EYEBROW": "Work in progress",
"WIP_TITLE": "Shop under construction",
"WIP_SUBTITLE": "We are building a curated technical shop with products that are genuinely useful and field-tested.",
"WIP_CTA_CALC": "Check our calculator",
"WIP_RETURN_LATER": "Come back soon",
"WIP_NOTE": "We care about doing this right. In the meantime, you can get instant pricing and lead time from our calculator.",
"ADD_CART": "Add to Cart",
"BACK": "Back to Shop"
"BACK": "Back to Shop",
"NOT_FOUND": "Product not found."
},
"ABOUT": {
"TITLE": "About Us",
@@ -188,7 +195,7 @@
"BANK_REF": "Reference",
"BILLING_INFO_HINT": "Add the same information used in billing.",
"DOWNLOAD_QR": "Download QR-Invoice (PDF)",
"CONFIRM": "Confirm Order",
"CONFIRM": "I have completed the payment",
"SUMMARY_TITLE": "Order Summary",
"SUBTOTAL": "Subtotal",
"SHIPPING": "Shipping",
@@ -197,13 +204,15 @@
"LOADING": "Loading order details...",
"METHOD_TWINT": "TWINT",
"METHOD_BANK": "Bank Transfer / QR",
"STATUS_REPORTED_TITLE": "Payment Reported",
"STATUS_REPORTED_DESC": "We are verifying your transaction. Your order will move to production as soon as the payment is confirmed.",
"IN_VERIFICATION": "Verifying Payment"
"STATUS_REPORTED_TITLE": "Order in progress",
"STATUS_REPORTED_DESC": "We have registered your operation. Your order will soon move to production.",
"IN_VERIFICATION": "Payment Reported"
},
"TRACKING": {
"TITLE": "Order Status",
"SUBTITLE": "Check the status of your order and manage the payment if necessary.",
"STEP_PENDING": "Pending",
"STEP_REPORTED": "Verifying",
"STEP_REPORTED": "Received",
"STEP_PRODUCTION": "Production",
"STEP_SHIPPED": "Shipped"
},

View File

@@ -15,14 +15,13 @@
"HERO_EYEBROW": "Stampa 3D tecnica per aziende, freelance e maker",
"HERO_TITLE": "Prezzo e tempi in pochi secondi.<br>Dal file 3D al pezzo finito.",
"HERO_LEAD": "Il calcolatore più avanzato per le tue stampe 3D: precisione assoluta e zero sorprese.",
"HERO_SUBTITLE": "Realizziamo pezzi unici e produzioni in serie. Se hai il file, il preventivo è istantaneo. Se devi ancora crearlo, il nostro team di design lo progetterà per te.",
"HERO_SUBTITLE": "Offriamo anche servizi di cad, per pezzi personalizzati!",
"BTN_CALCULATE": "Calcola Preventivo",
"BTN_SHOP": "Vai allo shop",
"BTN_CONTACT": "Parla con noi",
"SEC_CALC_TITLE": "Preventivo immediato in pochi secondi",
"SEC_CALC_SUBTITLE": "Nessuna registrazione necessaria. Non salviamo il tuo file fino al momento dell'ordine e la stima è effettuata tramite vero slicing.",
"SEC_CALC_TITLE": "Prezzo corretto in pochi secondi",
"SEC_CALC_SUBTITLE": "Nessuna registrazione necessaria. La stima è effettuata tramite vero slicing.",
"SEC_CALC_LIST_1": "Formati supportati: STL, 3MF, STEP, OBJ",
"SEC_CALC_LIST_2": "Qualità: bozza, standard, alta definizione",
"CARD_CALC_EYEBROW": "Calcolo automatico",
"CARD_CALC_TITLE": "Prezzo e tempi in un click",
"CARD_CALC_TAG": "Senza registrazione",
@@ -137,6 +136,12 @@
"SHOP": {
"TITLE": "Soluzioni tecniche",
"SUBTITLE": "Prodotti pronti che risolvono problemi pratici",
"WIP_EYEBROW": "Work in progress",
"WIP_TITLE": "Shop in allestimento",
"WIP_SUBTITLE": "Stiamo preparando uno shop con prodotti selezionati e funzionalità di creazione automatica!",
"WIP_CTA_CALC": "Vai al calcolatore",
"WIP_RETURN_LATER": "Torna tra un po'",
"WIP_NOTE": "Ci teniamo a fare le cose fatte bene: nel frattempo puoi calcolare subito prezzo e tempi di un file 3d con il nostro calcolatore.",
"ADD_CART": "Aggiungi al Carrello",
"BACK": "Torna allo Shop",
"NOT_FOUND": "Prodotto non trovato."
@@ -259,7 +264,7 @@
"BANK_REF": "Riferimento",
"BILLING_INFO_HINT": "Aggiungi le informazioni uguali a quelle della fatturazione.",
"DOWNLOAD_QR": "Scarica QR-Fattura (PDF)",
"CONFIRM": "Conferma Ordine",
"CONFIRM": "Ho completato il pagamento",
"SUMMARY_TITLE": "Riepilogo Ordine",
"SUBTOTAL": "Subtotale",
"SHIPPING": "Spedizione",
@@ -268,11 +273,13 @@
"LOADING": "Caricamento dettagli ordine...",
"METHOD_TWINT": "TWINT",
"METHOD_BANK": "Fattura QR / Bonifico",
"STATUS_REPORTED_TITLE": "Abbiamo ricevuto la tua segnalazione",
"STATUS_REPORTED_DESC": "Stiamo verificando la transazione. Il tuo ordine passerà in produzione non appena l'accredito sarà confermato.",
"IN_VERIFICATION": "Pagamento in verifica"
"STATUS_REPORTED_TITLE": "Ordine in lavorazione",
"STATUS_REPORTED_DESC": "Abbiamo registrato la tua operazione. L'ordine entrerà a breve in produzione.",
"IN_VERIFICATION": "Pagamento Segnalato"
},
"TRACKING": {
"TITLE": "Stato dell'Ordine",
"SUBTITLE": "Consulta lo stato del tuo ordine e gestisci il pagamento se necessario.",
"STEP_PENDING": "In attesa",
"STEP_REPORTED": "In verifica",
"STEP_PRODUCTION": "In Produzione",