dev #6

Merged
JoeKung merged 3 commits from dev into int 2026-02-10 19:05:41 +01:00
27 changed files with 337 additions and 26 deletions

View File

@@ -19,7 +19,7 @@ RUN apt-get update && apt-get install -y \
libglib2.0-0 \ libglib2.0-0 \
libgtk-3-0 \ libgtk-3-0 \
libdbus-1-3 \ libdbus-1-3 \
libwebkit2gtk-4.1-0 \ libwebkit2gtk-4.0-37 \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Install OrcaSlicer # Install OrcaSlicer

View File

@@ -1,5 +1,6 @@
plugins { plugins {
id 'java' id 'java'
id 'application'
id 'org.springframework.boot' version '3.4.1' id 'org.springframework.boot' version '3.4.1'
id 'io.spring.dependency-management' version '1.1.7' id 'io.spring.dependency-management' version '1.1.7'
} }
@@ -13,12 +14,18 @@ java {
} }
} }
application {
mainClass = 'com.printcalculator.BackendApplication'
}
repositories { repositories {
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'org.postgresql:postgresql'
developmentOnly 'org.springframework.boot:spring-boot-devtools' developmentOnly 'org.springframework.boot:spring-boot-devtools'
testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher' testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
@@ -27,3 +34,11 @@ dependencies {
tasks.named('test') { tasks.named('test') {
useJUnitPlatform() useJUnitPlatform()
} }
tasks.named('bootRun') {
args = ["--spring.profiles.active=local"]
}
application {
applicationDefaultJvmArgs = ["-Dspring.profiles.active=local"]
}

View File

@@ -0,0 +1,20 @@
package com.printcalculator.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@Profile("local")
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(false);
}
}

View File

@@ -13,16 +13,15 @@ import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
@RestController @RestController
@CrossOrigin(origins = "*") // Allow all for development
public class QuoteController { public class QuoteController {
private final SlicerService slicerService; private final SlicerService slicerService;
private final QuoteCalculator quoteCalculator; private final QuoteCalculator quoteCalculator;
// Defaults // Defaults (using aliases defined in ProfileManager)
private static final String DEFAULT_MACHINE = "Bambu_Lab_A1_machine"; private static final String DEFAULT_MACHINE = "bambu_a1";
private static final String DEFAULT_FILAMENT = "Bambu_PLA_Basic"; private static final String DEFAULT_FILAMENT = "pla_basic";
private static final String DEFAULT_PROCESS = "Bambu_Process_0.20_Standard"; private static final String DEFAULT_PROCESS = "standard";
public QuoteController(SlicerService slicerService, QuoteCalculator quoteCalculator) { public QuoteController(SlicerService slicerService, QuoteCalculator quoteCalculator) {
this.slicerService = slicerService; this.slicerService = slicerService;
@@ -32,12 +31,24 @@ public class QuoteController {
@PostMapping("/api/quote") @PostMapping("/api/quote")
public ResponseEntity<QuoteResult> calculateQuote( public ResponseEntity<QuoteResult> calculateQuote(
@RequestParam("file") MultipartFile file, @RequestParam("file") MultipartFile file,
@RequestParam(value = "machine", defaultValue = DEFAULT_MACHINE) String machine, @RequestParam(value = "machine", required = false, defaultValue = DEFAULT_MACHINE) String machine,
@RequestParam(value = "filament", defaultValue = DEFAULT_FILAMENT) String filament, @RequestParam(value = "filament", required = false, defaultValue = DEFAULT_FILAMENT) String filament,
@RequestParam(value = "process", defaultValue = DEFAULT_PROCESS) String process @RequestParam(value = "process", required = false) String process,
@RequestParam(value = "quality", required = false) String quality
) throws IOException { ) throws IOException {
return processRequest(file, machine, filament, process); // Frontend sends 'quality', backend expects 'process'.
// If process is missing, try quality. If both missing, use default.
String actualProcess = process;
if (actualProcess == null || actualProcess.isEmpty()) {
if (quality != null && !quality.isEmpty()) {
actualProcess = quality;
} else {
actualProcess = DEFAULT_PROCESS;
}
}
return processRequest(file, machine, filament, actualProcess);
} }
@PostMapping("/calculate/stl") @PostMapping("/calculate/stl")

View File

@@ -13,9 +13,13 @@ import java.util.regex.Pattern;
@Service @Service
public class GCodeParser { public class GCodeParser {
private static final Pattern TIME_PATTERN = Pattern.compile("estimated printing time = (.*)"); // OrcaSlicer/BambuStudio format
private static final Pattern FILAMENT_G_PATTERN = Pattern.compile("filament used \\[g\\] = (.*)"); // ; estimated printing time = 1h 2m 3s
private static final Pattern FILAMENT_MM_PATTERN = Pattern.compile("filament used \\[mm\\] = (.*)"); // ; filament used [g] = 12.34
// ; filament used [mm] = 1234.56
private static final Pattern TIME_PATTERN = Pattern.compile(";\\s*estimated printing time\\s*=\\s*(.*)");
private static final Pattern FILAMENT_G_PATTERN = Pattern.compile(";\\s*filament used \\[g\\]\\s*=\\s*(.*)");
private static final Pattern FILAMENT_MM_PATTERN = Pattern.compile(";\\s*filament used \\[mm\\]\\s*=\\s*(.*)");
public PrintStats parse(File gcodeFile) throws IOException { public PrintStats parse(File gcodeFile) throws IOException {
long seconds = 0; long seconds = 0;
@@ -25,9 +29,9 @@ public class GCodeParser {
try (BufferedReader reader = new BufferedReader(new FileReader(gcodeFile))) { try (BufferedReader reader = new BufferedReader(new FileReader(gcodeFile))) {
String line; String line;
// Scan first 500 lines for efficiency // Scan first 5000 lines for efficiency (metadata might be further down)
int count = 0; int count = 0;
while ((line = reader.readLine()) != null && count < 500) { while ((line = reader.readLine()) != null && count < 5000) {
line = line.trim(); line = line.trim();
if (!line.startsWith(";")) { if (!line.startsWith(";")) {
count++; count++;

View File

@@ -14,6 +14,8 @@ import java.util.Iterator;
import java.util.Optional; import java.util.Optional;
import java.util.logging.Logger; import java.util.logging.Logger;
import java.util.stream.Stream; import java.util.stream.Stream;
import java.util.Map;
import java.util.HashMap;
@Service @Service
public class ProfileManager { public class ProfileManager {
@@ -22,9 +24,31 @@ public class ProfileManager {
private final String profilesRoot; private final String profilesRoot;
private final ObjectMapper mapper; private final ObjectMapper mapper;
private final Map<String, String> profileAliases;
public ProfileManager(@Value("${profiles.root:profiles}") String profilesRoot, ObjectMapper mapper) { public ProfileManager(@Value("${profiles.root:profiles}") String profilesRoot, ObjectMapper mapper) {
this.profilesRoot = profilesRoot; this.profilesRoot = profilesRoot;
this.mapper = mapper; this.mapper = mapper;
this.profileAliases = new HashMap<>();
initializeAliases();
}
private void initializeAliases() {
// Machine Aliases
profileAliases.put("bambu_a1", "Bambu Lab A1 0.4 nozzle");
// Material Aliases
profileAliases.put("pla_basic", "Bambu PLA Basic @BBL A1");
profileAliases.put("petg_basic", "Bambu PETG Basic @BBL A1");
profileAliases.put("tpu_95a", "Bambu TPU 95A @BBL A1");
// Quality/Process Aliases
profileAliases.put("draft", "0.24mm Draft @BBL A1");
profileAliases.put("standard", "0.20mm Standard @BBL A1"); // or 0.20mm Standard @BBL A1
profileAliases.put("extra_fine", "0.08mm High Quality @BBL A1");
// Additional aliases from error logs
profileAliases.put("Bambu_Process_0.20_Standard", "0.20mm Standard @BBL A1");
} }
public ObjectNode getMergedProfile(String profileName, String type) throws IOException { public ObjectNode getMergedProfile(String profileName, String type) throws IOException {
@@ -36,9 +60,12 @@ public class ProfileManager {
} }
private Path findProfileFile(String name, String type) { private Path findProfileFile(String name, String type) {
// Check aliases first
String resolvedName = profileAliases.getOrDefault(name, name);
// Simple search: look for name.json in the profiles_root recursively // Simple search: look for name.json in the profiles_root recursively
// Type could be "machine", "process", "filament" to narrow down, but for now global search // Type could be "machine", "process", "filament" to narrow down, but for now global search
String filename = name.endsWith(".json") ? name : name + ".json"; String filename = resolvedName.endsWith(".json") ? resolvedName : resolvedName + ".json";
try (Stream<Path> stream = Files.walk(Paths.get(profilesRoot))) { try (Stream<Path> stream = Files.walk(Paths.get(profilesRoot))) {
Optional<Path> found = stream Optional<Path> found = stream

View File

@@ -52,8 +52,8 @@ public class QuoteCalculator {
); );
List<String> notes = new ArrayList<>(); List<String> notes = new ArrayList<>();
notes.add("Generated via Dynamic Slicer (Java Backend)"); // notes.add("Generated via Dynamic Slicer (Java Backend)");
return new QuoteResult(totalPrice, "EUR", stats, breakdown, notes); return new QuoteResult(totalPrice, "CHF", stats, breakdown, notes);
} }
} }

View File

@@ -55,12 +55,16 @@ public class SlicerService {
// 3. Build Command // 3. Build Command
// --load-settings "machine.json;process.json" --load-filaments "filament.json" // --load-settings "machine.json;process.json" --load-filaments "filament.json"
String settingsArg = mFile.getAbsolutePath() + ";" + pFile.getAbsolutePath();
List<String> command = new ArrayList<>(); List<String> command = new ArrayList<>();
command.add(slicerPath); command.add(slicerPath);
// Load machine settings
command.add("--load-settings"); command.add("--load-settings");
command.add(settingsArg); command.add(mFile.getAbsolutePath());
// Load process settings
command.add("--load-settings");
command.add(pFile.getAbsolutePath());
command.add("--load-filaments"); command.add("--load-filaments");
command.add(fFile.getAbsolutePath()); command.add(fFile.getAbsolutePath());
command.add("--ensure-on-bed"); command.add("--ensure-on-bed");

View File

@@ -1,6 +1,14 @@
spring.application.name=backend spring.application.name=backend
server.port=8000 server.port=8000
# Database Configuration
spring.datasource.url=${DB_URL:jdbc:postgresql://localhost:5432/printcalc}
spring.datasource.username=${DB_USERNAME:printcalc}
spring.datasource.password=${DB_PASSWORD:printcalc_secret}
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
# Slicer Configuration # Slicer Configuration
# Map SLICER_PATH env var if present (default to /opt/orcaslicer/AppRun or Mac path) # Map SLICER_PATH env var if present (default to /opt/orcaslicer/AppRun or Mac path)
slicer.path=${SLICER_PATH:/Applications/OrcaSlicer.app/Contents/MacOS/OrcaSlicer} slicer.path=${SLICER_PATH:/Applications/OrcaSlicer.app/Contents/MacOS/OrcaSlicer}

View File

@@ -15,7 +15,7 @@ services:
- MARKUP_PERCENT=${MARKUP_PERCENT} - MARKUP_PERCENT=${MARKUP_PERCENT}
- TEMP_DIR=/app/temp - TEMP_DIR=/app/temp
- PROFILES_DIR=/app/profiles - PROFILES_DIR=/app/profiles
restart: unless-stopped restart: always
volumes: volumes:
- backend_profiles_${ENV}:/app/profiles - backend_profiles_${ENV}:/app/profiles
@@ -26,7 +26,7 @@ services:
- "${FRONTEND_PORT}:8008" - "${FRONTEND_PORT}:8008"
depends_on: depends_on:
- backend - backend
restart: unless-stopped restart: always
volumes: volumes:
backend_profiles_prod: backend_profiles_prod:

View File

@@ -25,4 +25,21 @@ services:
- "80:80" - "80:80"
depends_on: depends_on:
- backend - backend
- db
restart: unless-stopped restart: unless-stopped
db:
image: postgres:15-alpine
container_name: print-calculator-db
environment:
- POSTGRES_USER=printcalc
- POSTGRES_PASSWORD=printcalc_secret
- POSTGRES_DB=printcalc
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
restart: unless-stopped
volumes:
postgres_data:

View File

@@ -70,6 +70,17 @@
"optimization": false, "optimization": false,
"extractLicenses": false, "extractLicenses": false,
"sourceMap": true "sourceMap": true
},
"local": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.local.ts"
}
],
"optimization": false,
"extractLicenses": false,
"sourceMap": true
} }
}, },
@@ -83,6 +94,9 @@
}, },
"development": { "development": {
"buildTarget": "frontend:build:development" "buildTarget": "frontend:build:development"
},
"local": {
"buildTarget": "frontend:build:local"
} }
}, },
"defaultConfiguration": "development" "defaultConfiguration": "development"

View File

@@ -24,6 +24,10 @@ export const routes: Routes = [
{ {
path: 'contact', path: 'contact',
loadChildren: () => import('./features/contact/contact.routes').then(m => m.CONTACT_ROUTES) loadChildren: () => import('./features/contact/contact.routes').then(m => m.CONTACT_ROUTES)
},
{
path: '',
loadChildren: () => import('./features/legal/legal.routes').then(m => m.LEGAL_ROUTES)
} }
] ]
} }

View File

@@ -4,11 +4,11 @@
<span class="brand">3D fab</span> <span class="brand">3D fab</span>
<p class="copyright">&copy; 2026 3D fab.</p> <p class="copyright">&copy; 2026 3D fab.</p>
</div> </div>
<div class="col links"> <div class="col links">
<a routerLink="/privacy">{{ 'FOOTER.PRIVACY' | translate }}</a> <a routerLink="/privacy">{{ 'FOOTER.PRIVACY' | translate }}</a>
<a routerLink="/terms">{{ 'FOOTER.TERMS' | translate }}</a> <a routerLink="/terms">{{ 'FOOTER.TERMS' | translate }}</a>
<a routerLink="/about">{{ 'FOOTER.CONTACT' | translate }}</a> <a routerLink="/contact">{{ 'FOOTER.CONTACT' | translate }}</a>
</div> </div>
<div class="col social"> <div class="col social">

View File

@@ -11,6 +11,7 @@
formControlName="name" formControlName="name"
label="USER_DETAILS.NAME" label="USER_DETAILS.NAME"
placeholder="USER_DETAILS.NAME_PLACEHOLDER" placeholder="USER_DETAILS.NAME_PLACEHOLDER"
[required]="true"
[error]="form.get('name')?.invalid && form.get('name')?.touched ? ('COMMON.REQUIRED' | translate) : null"> [error]="form.get('name')?.invalid && form.get('name')?.touched ? ('COMMON.REQUIRED' | translate) : null">
</app-input> </app-input>
</div> </div>
@@ -19,6 +20,7 @@
formControlName="surname" formControlName="surname"
label="USER_DETAILS.SURNAME" label="USER_DETAILS.SURNAME"
placeholder="USER_DETAILS.SURNAME_PLACEHOLDER" placeholder="USER_DETAILS.SURNAME_PLACEHOLDER"
[required]="true"
[error]="form.get('surname')?.invalid && form.get('surname')?.touched ? ('COMMON.REQUIRED' | translate) : null"> [error]="form.get('surname')?.invalid && form.get('surname')?.touched ? ('COMMON.REQUIRED' | translate) : null">
</app-input> </app-input>
</div> </div>
@@ -32,6 +34,7 @@
label="USER_DETAILS.EMAIL" label="USER_DETAILS.EMAIL"
type="email" type="email"
placeholder="USER_DETAILS.EMAIL_PLACEHOLDER" placeholder="USER_DETAILS.EMAIL_PLACEHOLDER"
[required]="true"
[error]="form.get('email')?.invalid && form.get('email')?.touched ? ('COMMON.INVALID_EMAIL' | translate) : null"> [error]="form.get('email')?.invalid && form.get('email')?.touched ? ('COMMON.INVALID_EMAIL' | translate) : null">
</app-input> </app-input>
</div> </div>
@@ -41,6 +44,7 @@
label="USER_DETAILS.PHONE" label="USER_DETAILS.PHONE"
type="tel" type="tel"
placeholder="USER_DETAILS.PHONE_PLACEHOLDER" placeholder="USER_DETAILS.PHONE_PLACEHOLDER"
[required]="true"
[error]="form.get('phone')?.invalid && form.get('phone')?.touched ? ('COMMON.REQUIRED' | translate) : null"> [error]="form.get('phone')?.invalid && form.get('phone')?.touched ? ('COMMON.REQUIRED' | translate) : null">
</app-input> </app-input>
</div> </div>
@@ -51,6 +55,7 @@
formControlName="address" formControlName="address"
label="USER_DETAILS.ADDRESS" label="USER_DETAILS.ADDRESS"
placeholder="USER_DETAILS.ADDRESS_PLACEHOLDER" placeholder="USER_DETAILS.ADDRESS_PLACEHOLDER"
[required]="true"
[error]="form.get('address')?.invalid && form.get('address')?.touched ? ('COMMON.REQUIRED' | translate) : null"> [error]="form.get('address')?.invalid && form.get('address')?.touched ? ('COMMON.REQUIRED' | translate) : null">
</app-input> </app-input>
@@ -61,6 +66,7 @@
formControlName="zip" formControlName="zip"
label="USER_DETAILS.ZIP" label="USER_DETAILS.ZIP"
placeholder="USER_DETAILS.ZIP_PLACEHOLDER" placeholder="USER_DETAILS.ZIP_PLACEHOLDER"
[required]="true"
[error]="form.get('zip')?.invalid && form.get('zip')?.touched ? ('COMMON.REQUIRED' | translate) : null"> [error]="form.get('zip')?.invalid && form.get('zip')?.touched ? ('COMMON.REQUIRED' | translate) : null">
</app-input> </app-input>
</div> </div>
@@ -69,6 +75,7 @@
formControlName="city" formControlName="city"
label="USER_DETAILS.CITY" label="USER_DETAILS.CITY"
placeholder="USER_DETAILS.CITY_PLACEHOLDER" placeholder="USER_DETAILS.CITY_PLACEHOLDER"
[required]="true"
[error]="form.get('city')?.invalid && form.get('city')?.touched ? ('COMMON.REQUIRED' | translate) : null"> [error]="form.get('city')?.invalid && form.get('city')?.touched ? ('COMMON.REQUIRED' | translate) : null">
</app-input> </app-input>
</div> </div>

View File

@@ -0,0 +1,12 @@
import { Routes } from '@angular/router';
export const LEGAL_ROUTES: Routes = [
{
path: 'privacy',
loadComponent: () => import('./privacy/privacy.component').then(m => m.PrivacyComponent)
},
{
path: 'terms',
loadComponent: () => import('./terms/terms.component').then(m => m.TermsComponent)
}
];

View File

@@ -0,0 +1,17 @@
<section class="legal-page">
<div class="container narrow">
<h1>{{ 'LEGAL.PRIVACY_TITLE' | translate }}</h1>
<div class="content">
<p class="intro">{{ 'LEGAL.LAST_UPDATE' | translate }}: February 2026</p>
<h2>{{ 'LEGAL.PRIVACY.SECTION_1' | translate }}</h2>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
<h2>{{ 'LEGAL.PRIVACY.SECTION_2' | translate }}</h2>
<p>Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<h2>{{ 'LEGAL.PRIVACY.SECTION_3' | translate }}</h2>
<p>Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra, est eros bibendum elit, nec luctus magna felis sollicitudin mauris.</p>
</div>
</div>
</section>

View File

@@ -0,0 +1,37 @@
.legal-page {
padding: 6rem 0;
min-height: 70vh;
.narrow {
max-width: 800px;
margin: 0 auto;
}
h1 {
font-size: 3rem;
margin-bottom: 3rem;
color: var(--color-text-main);
}
h2 {
font-size: 1.5rem;
margin-top: 2.5rem;
margin-bottom: 1rem;
color: var(--color-text-main);
}
.intro {
color: var(--color-text-muted);
font-size: 0.9rem;
margin-bottom: 2rem;
}
.content {
line-height: 1.8;
color: var(--color-text-main);
p {
margin-bottom: 1.5rem;
}
}
}

View File

@@ -0,0 +1,11 @@
import { Component } from '@angular/core';
import { TranslateModule } from '@ngx-translate/core';
@Component({
selector: 'app-privacy',
standalone: true,
imports: [TranslateModule],
templateUrl: './privacy.component.html',
styleUrl: './privacy.component.scss'
})
export class PrivacyComponent {}

View File

@@ -0,0 +1,18 @@
<section class="legal-page">
<div class="container narrow">
<h1>{{ 'LEGAL.TERMS_TITLE' | translate }}</h1>
<div class="content">
<p class="intro">{{ 'LEGAL.LAST_UPDATE' | translate }}: February 2026</p>
<h2>{{ 'LEGAL.TERMS.SECTION_1' | translate }}</h2>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
<h2>{{ 'LEGAL.TERMS.SECTION_2' | translate }}</h2>
<p>Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<h2>{{ 'LEGAL.TERMS.SECTION_3' | translate }}</h2>
<p>I prodotti personalizzati e realizzati su misura tramite stampa 3D non sono soggetti al diritto di recesso, a meno di difetti di fabbricazione evidenti o errori rispetto al file fornito.</p>
<p>In caso di problemi, vi preghiamo di contattarci entro 14 giorni dalla ricezione per valutare una sostituzione o un rimborso parziale.</p>
</div>
</div>
</section>

View File

@@ -0,0 +1,37 @@
.legal-page {
padding: 6rem 0;
min-height: 70vh;
.narrow {
max-width: 800px;
margin: 0 auto;
}
h1 {
font-size: 3rem;
margin-bottom: 3rem;
color: var(--color-text-main);
}
h2 {
font-size: 1.5rem;
margin-top: 2.5rem;
margin-bottom: 1rem;
color: var(--color-text-main);
}
.intro {
color: var(--color-text-muted);
font-size: 0.9rem;
margin-bottom: 2rem;
}
.content {
line-height: 1.8;
color: var(--color-text-main);
p {
margin-bottom: 1.5rem;
}
}
}

View File

@@ -0,0 +1,11 @@
import { Component } from '@angular/core';
import { TranslateModule } from '@ngx-translate/core';
@Component({
selector: 'app-terms',
standalone: true,
imports: [TranslateModule],
templateUrl: './terms.component.html',
styleUrl: './terms.component.scss'
})
export class TermsComponent {}

View File

@@ -1,5 +1,10 @@
<div class="form-group"> <div class="form-group">
@if (label()) { <label [for]="id()">{{ label() }}</label> } @if (label()) {
<label [for]="id()">
{{ label() }}
@if (required()) { <span class="required-mark">*</span> }
</label>
}
<input <input
[id]="id()" [id]="id()"
[type]="type()" [type]="type()"

View File

@@ -1,5 +1,6 @@
.form-group { display: flex; flex-direction: column; margin-bottom: var(--space-4); } .form-group { display: flex; flex-direction: column; margin-bottom: var(--space-4); }
label { font-size: 0.875rem; font-weight: 500; margin-bottom: var(--space-2); color: var(--color-text); } label { font-size: 0.875rem; font-weight: 500; margin-bottom: var(--space-2); color: var(--color-text); }
.required-mark { color: var(--color-danger-500); margin-left: 2px; }
.form-control { .form-control {
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem;
border: 1px solid var(--color-border); border: 1px solid var(--color-border);

View File

@@ -22,6 +22,7 @@ export class AppInputComponent implements ControlValueAccessor {
type = input<string>('text'); type = input<string>('text');
placeholder = input<string>(''); placeholder = input<string>('');
error = input<string | null>(null); error = input<string | null>(null);
required = input<boolean>(false);
value: string = ''; value: string = '';
disabled = false; disabled = false;

View File

@@ -103,6 +103,21 @@
"ADDRESS_BIENNE": "Bienne Office, Switzerland", "ADDRESS_BIENNE": "Bienne Office, Switzerland",
"CONTACT_US": "Contact Us" "CONTACT_US": "Contact Us"
}, },
"LEGAL": {
"PRIVACY_TITLE": "Privacy Policy",
"TERMS_TITLE": "Terms and Conditions",
"LAST_UPDATE": "Last update",
"PRIVACY": {
"SECTION_1": "1. Data Collection",
"SECTION_2": "2. Purpose of Processing",
"SECTION_3": "3. Cookies and Tracking"
},
"TERMS": {
"SECTION_1": "1. Terms of Use",
"SECTION_2": "2. Orders and Payments",
"SECTION_3": "3. Refunds and Returns"
}
},
"CONTACT": { "CONTACT": {
"TITLE": "Contact Us", "TITLE": "Contact Us",
"SEND": "Send Message", "SEND": "Send Message",

View File

@@ -82,6 +82,21 @@
"ADDRESS_BIENNE": "Sede Bienne, Svizzera", "ADDRESS_BIENNE": "Sede Bienne, Svizzera",
"CONTACT_US": "Contattaci" "CONTACT_US": "Contattaci"
}, },
"LEGAL": {
"PRIVACY_TITLE": "Privacy Policy",
"TERMS_TITLE": "Termini e Condizioni",
"LAST_UPDATE": "Ultimo aggiornamento",
"PRIVACY": {
"SECTION_1": "1. Raccolta dei Dati",
"SECTION_2": "2. Finalità del Trattamento",
"SECTION_3": "3. Cookie e Tracciamento"
},
"TERMS": {
"SECTION_1": "1. Condizioni d'Uso",
"SECTION_2": "2. Ordini e Pagamenti",
"SECTION_3": "3. Rimborsi e Resi"
}
},
"CONTACT": { "CONTACT": {
"TITLE": "Contattaci", "TITLE": "Contattaci",
"SEND": "Invia Messaggio", "SEND": "Invia Messaggio",