Compare commits
1 Commits
feat/calcu
...
3cbcec5f53
| Author | SHA1 | Date | |
|---|---|---|---|
| 3cbcec5f53 |
@@ -22,30 +22,10 @@
|
||||
</div>
|
||||
|
||||
<div class="col social">
|
||||
<div class="social-link-row">
|
||||
<span class="social-name">Joe Küng:</span>
|
||||
<a
|
||||
class="social-icon-link"
|
||||
href="https://www.linkedin.com/in/joe-k%C3%BCng-31831828b/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Joe Küng LinkedIn"
|
||||
>
|
||||
<span class="social-icon-linkedin" aria-hidden="true"></span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="social-link-row">
|
||||
<span class="social-name">Matteo Caletti:</span>
|
||||
<a
|
||||
class="social-icon-link"
|
||||
href="https://www.linkedin.com/in/matteo-caletti-94291a3b6/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Matteo Caletti LinkedIn"
|
||||
>
|
||||
<span class="social-icon-linkedin" aria-hidden="true"></span>
|
||||
</a>
|
||||
</div>
|
||||
<!-- Social Placeholders -->
|
||||
<div class="social-icon"></div>
|
||||
<div class="social-icon"></div>
|
||||
<div class="social-icon"></div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -66,69 +66,11 @@
|
||||
|
||||
.social {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.social-link-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.social-name {
|
||||
color: var(--color-neutral-200);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.social-icon-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
.social-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background-color: var(--color-neutral-800);
|
||||
border-radius: 50%;
|
||||
background-color: var(--color-neutral-50);
|
||||
color: #0a66c2;
|
||||
transition:
|
||||
transform 0.2s ease,
|
||||
background-color 0.2s ease,
|
||||
color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: #0a66c2;
|
||||
color: var(--color-neutral-50);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--color-secondary-500);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.social-icon-linkedin {
|
||||
display: block;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
background-color: currentColor;
|
||||
mask-image: url("/assets/images/SVG/linkedin-svgrepo-com.svg");
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-size: contain;
|
||||
-webkit-mask-image: url("/assets/images/SVG/linkedin-svgrepo-com.svg");
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
-webkit-mask-position: center;
|
||||
-webkit-mask-size: contain;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.social {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.social-link-row {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
>{{ "NAV.HOME" | translate }}</a
|
||||
>
|
||||
<a
|
||||
[routerLink]="langService.localizedPath('/calculator')"
|
||||
[routerLink]="langService.localizedPath('/calculator/basic')"
|
||||
routerLinkActive="active"
|
||||
[routerLinkActiveOptions]="{ exact: false }"
|
||||
(click)="closeMenu()"
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
<div class="container ui-simple-hero">
|
||||
<h1 class="ui-simple-hero__title">
|
||||
{{ modeContentKey("TITLE") | translate }}
|
||||
</h1>
|
||||
<p class="ui-simple-hero__subtitle">
|
||||
{{ modeContentKey("SUBTITLE") | translate }}
|
||||
</p>
|
||||
<h1 class="ui-simple-hero__title">{{ "CALC.TITLE" | translate }}</h1>
|
||||
<p class="ui-simple-hero__subtitle">{{ "CALC.SUBTITLE" | translate }}</p>
|
||||
|
||||
@if (error()) {
|
||||
<app-alert type="error">{{ errorKey() | translate }}</app-alert>
|
||||
@@ -103,84 +99,4 @@
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container calculator-guides">
|
||||
<section class="calculator-guide">
|
||||
<div class="model-source-section">
|
||||
<div class="model-source-intro">
|
||||
<p class="guide-kicker">
|
||||
{{ "CALC.MODEL_SOURCES.KICKER" | translate }}
|
||||
</p>
|
||||
<h2>{{ "CALC.MODEL_SOURCES.TITLE" | translate }}</h2>
|
||||
<p>{{ "CALC.MODEL_SOURCES.TEXT" | translate }}</p>
|
||||
</div>
|
||||
|
||||
<div class="model-source-links">
|
||||
<div class="model-source-group">
|
||||
<p class="model-source-group__label">
|
||||
{{ "CALC.MODEL_SOURCES.FAVORITES_TITLE" | translate }}
|
||||
</p>
|
||||
@for (source of favoriteModelSources; track source.id) {
|
||||
<a
|
||||
class="model-source-link model-source-link--favorite"
|
||||
[class.model-source-link--printables]="
|
||||
source.id === 'PRINTABLES'
|
||||
"
|
||||
[class.model-source-link--makerworld]="
|
||||
source.id === 'MAKERWORLD'
|
||||
"
|
||||
[href]="source.url"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<span class="model-source-link__name">{{ source.label }}</span>
|
||||
<span class="model-source-link__description">{{
|
||||
modelSourceDescriptionKey(source.id) | translate
|
||||
}}</span>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="model-source-group">
|
||||
<p class="model-source-group__label">
|
||||
{{ "CALC.MODEL_SOURCES.OTHERS_TITLE" | translate }}
|
||||
</p>
|
||||
<div class="model-source-compact-list">
|
||||
@for (source of otherModelSources; track source.id) {
|
||||
<a
|
||||
class="model-source-link model-source-link--compact"
|
||||
[href]="source.url"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<span class="model-source-link__name">{{
|
||||
source.label
|
||||
}}</span>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="calculator-guide">
|
||||
<app-card class="guide-card">
|
||||
<div class="guide-header guide-header--compact">
|
||||
<p class="guide-kicker">{{ "CALC.FAQ.KICKER" | translate }}</p>
|
||||
<h2>{{ "CALC.FAQ.TITLE" | translate }}</h2>
|
||||
<p>{{ "CALC.FAQ.SUBTITLE" | translate }}</p>
|
||||
</div>
|
||||
|
||||
<div class="faq-list">
|
||||
@for (faqId of faqIds; track faqId) {
|
||||
<details class="faq-item">
|
||||
<summary>{{ faqKey(faqId, "Q") | translate }}</summary>
|
||||
<p>{{ faqKey(faqId, "A") | translate }}</p>
|
||||
</details>
|
||||
}
|
||||
</div>
|
||||
</app-card>
|
||||
</section>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -53,8 +53,8 @@
|
||||
padding: 8px 16px;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
font-size: 0.92rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-muted);
|
||||
transition: all 0.2s ease;
|
||||
user-select: none;
|
||||
@@ -134,257 +134,3 @@
|
||||
--brand-animation-scale-mobile: 0.84;
|
||||
--brand-animation-loader-loop-duration: 2.65s;
|
||||
}
|
||||
|
||||
.calculator-guides {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-6);
|
||||
margin-top: var(--space-8);
|
||||
padding-bottom: var(--space-10);
|
||||
}
|
||||
|
||||
.calculator-guide {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.guide-header {
|
||||
max-width: 48rem;
|
||||
|
||||
h2 {
|
||||
margin: 0 0 var(--space-3);
|
||||
font-size: clamp(1.4rem, 2vw, 1.9rem);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: var(--color-text-muted);
|
||||
line-height: 1.65;
|
||||
}
|
||||
}
|
||||
|
||||
.guide-header--compact {
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
.guide-kicker {
|
||||
margin: 0 0 var(--space-2);
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.76rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.guide-card {
|
||||
h3 {
|
||||
margin: 0 0 var(--space-3);
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: var(--color-text-muted);
|
||||
line-height: 1.65;
|
||||
}
|
||||
}
|
||||
|
||||
.model-source-section {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: var(--space-6);
|
||||
padding: var(--space-2) 0;
|
||||
|
||||
@media (min-width: 960px) {
|
||||
grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
|
||||
align-items: start;
|
||||
}
|
||||
}
|
||||
|
||||
.model-source-intro {
|
||||
max-width: 34rem;
|
||||
|
||||
h2 {
|
||||
margin: 0 0 var(--space-3);
|
||||
font-size: clamp(1.35rem, 2vw, 1.8rem);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: var(--color-text-muted);
|
||||
line-height: 1.7;
|
||||
}
|
||||
}
|
||||
|
||||
.model-source-links {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-5);
|
||||
}
|
||||
|
||||
.model-source-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.model-source-group__label {
|
||||
margin: 0 0 var(--space-1);
|
||||
color: var(--color-text);
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.model-source-link {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.15rem;
|
||||
padding: var(--space-3) 0;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
transition:
|
||||
color 0.2s ease,
|
||||
border-color 0.2s ease,
|
||||
transform 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-text);
|
||||
border-color: rgba(250, 207, 10, 0.7);
|
||||
transform: translateX(2px);
|
||||
}
|
||||
}
|
||||
|
||||
.model-source-link--favorite {
|
||||
--favorite-accent: var(--color-brand);
|
||||
padding: var(--space-3) var(--space-4);
|
||||
border: 1px solid
|
||||
color-mix(in srgb, var(--favorite-accent) 38%, var(--color-border));
|
||||
border-radius: var(--radius-md);
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--favorite-accent) 12%,
|
||||
var(--color-bg-card)
|
||||
);
|
||||
box-shadow: inset 0 0 0 1px
|
||||
color-mix(in srgb, var(--favorite-accent) 16%, #fff);
|
||||
|
||||
&:hover {
|
||||
color: var(--color-text);
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--favorite-accent) 18%,
|
||||
var(--color-bg-card)
|
||||
);
|
||||
border-color: color-mix(
|
||||
in srgb,
|
||||
var(--favorite-accent) 70%,
|
||||
var(--color-border)
|
||||
);
|
||||
transform: translateX(2px);
|
||||
}
|
||||
}
|
||||
|
||||
.model-source-link--printables {
|
||||
--favorite-accent: #f1872a;
|
||||
}
|
||||
|
||||
.model-source-link--makerworld {
|
||||
--favorite-accent: #00b140;
|
||||
}
|
||||
|
||||
.model-source-link__name {
|
||||
position: relative;
|
||||
width: fit-content;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.model-source-link--favorite .model-source-link__name::after {
|
||||
content: " ->";
|
||||
position: static;
|
||||
font-weight: 600;
|
||||
color: var(--favorite-accent);
|
||||
}
|
||||
|
||||
.model-source-link--favorite .model-source-link__name {
|
||||
text-decoration: underline;
|
||||
text-decoration-thickness: 2px;
|
||||
text-decoration-color: var(--favorite-accent);
|
||||
text-underline-offset: 0.2em;
|
||||
}
|
||||
|
||||
.model-source-link__description {
|
||||
color: var(--color-text-muted);
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.model-source-compact-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-2) var(--space-4);
|
||||
}
|
||||
|
||||
.model-source-link--compact {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
transform: none;
|
||||
|
||||
&:hover {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
.model-source-link--compact .model-source-link__name {
|
||||
font-size: 0.92rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-muted);
|
||||
text-decoration: underline;
|
||||
text-decoration-thickness: 1px;
|
||||
text-decoration-color: currentColor;
|
||||
}
|
||||
|
||||
.model-source-link--compact .model-source-link__name::after {
|
||||
content: " ->";
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.faq-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.faq-item {
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-surface-muted);
|
||||
padding: var(--space-4);
|
||||
|
||||
summary {
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: var(--space-3) 0 0;
|
||||
color: var(--color-text-muted);
|
||||
line-height: 1.65;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,7 @@ import { of } from 'rxjs';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { CalculatorPageComponent } from './calculator-page.component';
|
||||
import {
|
||||
PendingCalculatorDraft,
|
||||
QuoteEstimatorService,
|
||||
QuoteRequest,
|
||||
QuoteResult,
|
||||
} from './services/quote-estimator.service';
|
||||
import { LanguageService } from '../../core/services/language.service';
|
||||
@@ -33,54 +31,15 @@ describe('CalculatorPageComponent', () => {
|
||||
notes,
|
||||
});
|
||||
|
||||
const createDraftRequest = (): QuoteRequest => ({
|
||||
items: [
|
||||
{
|
||||
file: new File(['mesh'], 'part-a.stl', { type: 'model/stl' }),
|
||||
quantity: 2,
|
||||
material: 'PLA',
|
||||
quality: 'standard',
|
||||
color: 'Black',
|
||||
supportEnabled: true,
|
||||
infillDensity: 15,
|
||||
infillPattern: 'grid',
|
||||
layerHeight: 0.2,
|
||||
nozzleDiameter: 0.4,
|
||||
},
|
||||
],
|
||||
material: 'PLA',
|
||||
quality: 'standard',
|
||||
notes: 'draft note',
|
||||
infillDensity: 15,
|
||||
infillPattern: 'grid',
|
||||
supportEnabled: true,
|
||||
layerHeight: 0.2,
|
||||
nozzleDiameter: 0.4,
|
||||
mode: 'easy',
|
||||
});
|
||||
|
||||
function createComponent() {
|
||||
const estimator = jasmine.createSpyObj<QuoteEstimatorService>(
|
||||
'QuoteEstimatorService',
|
||||
[
|
||||
'updateLineItem',
|
||||
'getQuoteSession',
|
||||
'mapSessionToQuoteResult',
|
||||
'setPendingCalculatorDraft',
|
||||
'consumePendingCalculatorDraft',
|
||||
],
|
||||
['updateLineItem', 'getQuoteSession', 'mapSessionToQuoteResult'],
|
||||
);
|
||||
const router = jasmine.createSpyObj<Router>('Router', ['navigate']);
|
||||
const route = {
|
||||
data: of({}),
|
||||
queryParams: of({}),
|
||||
snapshot: {
|
||||
routeConfig: { path: 'basic' },
|
||||
queryParams: {},
|
||||
queryParamMap: {
|
||||
get: () => null,
|
||||
},
|
||||
},
|
||||
} as unknown as ActivatedRoute;
|
||||
const languageService = jasmine.createSpyObj<LanguageService>(
|
||||
'LanguageService',
|
||||
@@ -96,25 +55,13 @@ describe('CalculatorPageComponent', () => {
|
||||
|
||||
const uploadForm = jasmine.createSpyObj<UploadFormComponent>(
|
||||
'UploadFormComponent',
|
||||
[
|
||||
'updateItemQuantityByIndex',
|
||||
'updateItemQuantityByName',
|
||||
'getCurrentRequestDraft',
|
||||
'restoreRequestDraft',
|
||||
],
|
||||
['updateItemQuantityByIndex', 'updateItemQuantityByName'],
|
||||
);
|
||||
uploadForm.sameSettingsForAll = jasmine
|
||||
.createSpy('sameSettingsForAll')
|
||||
.and.returnValue(true) as any;
|
||||
uploadForm.selectedFile = jasmine
|
||||
.createSpy('selectedFile')
|
||||
.and.returnValue(null) as any;
|
||||
component.uploadForm = uploadForm;
|
||||
|
||||
return {
|
||||
component,
|
||||
estimator,
|
||||
route,
|
||||
uploadForm,
|
||||
};
|
||||
}
|
||||
@@ -162,80 +109,4 @@ describe('CalculatorPageComponent', () => {
|
||||
expect(component.result()?.notes).toBe('persisted notes');
|
||||
expect(component.result()?.items[0].quantity).toBe(1);
|
||||
});
|
||||
|
||||
it('builds mode-specific content keys', () => {
|
||||
const { component } = createComponent();
|
||||
|
||||
component.mode.set('easy');
|
||||
expect(component.modeContentKey('TITLE')).toBe('CALC.MODES.BASIC.TITLE');
|
||||
|
||||
component.mode.set('advanced');
|
||||
expect(component.modeContentKey('TITLE')).toBe('CALC.MODES.ADVANCED.TITLE');
|
||||
});
|
||||
|
||||
it('exposes the expected external model sources and faq entries', () => {
|
||||
const { component } = createComponent();
|
||||
|
||||
expect(component.favoriteModelSources.map((entry) => entry.id)).toEqual([
|
||||
'PRINTABLES',
|
||||
'MAKERWORLD',
|
||||
]);
|
||||
expect(component.otherModelSources.map((entry) => entry.id)).toEqual([
|
||||
'THINGIVERSE',
|
||||
'THANGS',
|
||||
'CULTS3D',
|
||||
'YEGGI',
|
||||
]);
|
||||
expect(component.modelSources.map((entry) => entry.id)).toEqual([
|
||||
'PRINTABLES',
|
||||
'MAKERWORLD',
|
||||
'THINGIVERSE',
|
||||
'THANGS',
|
||||
'CULTS3D',
|
||||
'YEGGI',
|
||||
]);
|
||||
expect(component.faqIds).toEqual([
|
||||
'FILES',
|
||||
'MODE',
|
||||
'NO_MODEL',
|
||||
'PRICE',
|
||||
'BEFORE_UPLOAD',
|
||||
]);
|
||||
});
|
||||
|
||||
it('stores the current draft before switching mode without a session', () => {
|
||||
const { component, estimator, uploadForm } = createComponent();
|
||||
const draftRequest = createDraftRequest();
|
||||
uploadForm.getCurrentRequestDraft.and.returnValue(draftRequest);
|
||||
(uploadForm.sameSettingsForAll as jasmine.Spy).and.returnValue(false);
|
||||
(uploadForm.selectedFile as jasmine.Spy).and.returnValue(
|
||||
draftRequest.items[0].file,
|
||||
);
|
||||
|
||||
component.switchMode('advanced');
|
||||
|
||||
expect(estimator.setPendingCalculatorDraft).toHaveBeenCalledWith({
|
||||
request: draftRequest,
|
||||
sameSettingsForAll: false,
|
||||
selectedFileName: 'part-a.stl',
|
||||
});
|
||||
});
|
||||
|
||||
it('restores a pending draft after view init when there is no session', () => {
|
||||
const { component, estimator, uploadForm } = createComponent();
|
||||
const draftRequest = createDraftRequest();
|
||||
const pendingDraft: PendingCalculatorDraft = {
|
||||
request: draftRequest,
|
||||
sameSettingsForAll: true,
|
||||
selectedFileName: 'part-a.stl',
|
||||
};
|
||||
estimator.consumePendingCalculatorDraft.and.returnValue(pendingDraft);
|
||||
|
||||
component.ngAfterViewInit();
|
||||
|
||||
expect(uploadForm.restoreRequestDraft).toHaveBeenCalledWith(draftRequest, {
|
||||
sameSettingsForAll: true,
|
||||
selectedFileName: 'part-a.stl',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
AfterViewInit,
|
||||
Component,
|
||||
computed,
|
||||
signal,
|
||||
@@ -22,7 +21,6 @@ import { BrandAnimationLogoComponent } from '../../shared/components/brand-anima
|
||||
import { UploadFormComponent } from './components/upload-form/upload-form.component';
|
||||
import { QuoteResultComponent } from './components/quote-result/quote-result.component';
|
||||
import {
|
||||
PendingCalculatorDraft,
|
||||
QuoteRequest,
|
||||
QuoteResult,
|
||||
QuoteEstimatorService,
|
||||
@@ -59,7 +57,7 @@ type TrackedPrintSettings = {
|
||||
templateUrl: './calculator-page.component.html',
|
||||
styleUrl: './calculator-page.component.scss',
|
||||
})
|
||||
export class CalculatorPageComponent implements OnInit, AfterViewInit {
|
||||
export class CalculatorPageComponent implements OnInit {
|
||||
private readonly isBrowser: boolean;
|
||||
mode = signal<'easy' | 'advanced'>('easy');
|
||||
step = signal<'upload' | 'quote' | 'details' | 'success'>('upload');
|
||||
@@ -73,57 +71,6 @@ export class CalculatorPageComponent implements OnInit, AfterViewInit {
|
||||
isZeroQuoteError = computed(
|
||||
() => this.error() && this.errorKey() === 'CALC.ERROR_ZERO_PRICE',
|
||||
);
|
||||
readonly faqIds = [
|
||||
'FILES',
|
||||
'MODE',
|
||||
'NO_MODEL',
|
||||
'PRICE',
|
||||
'BEFORE_UPLOAD',
|
||||
] as const;
|
||||
readonly modelSources = [
|
||||
{
|
||||
id: 'PRINTABLES',
|
||||
label: 'Printables',
|
||||
url: 'https://www.printables.com',
|
||||
},
|
||||
{
|
||||
id: 'MAKERWORLD',
|
||||
label: 'MakerWorld',
|
||||
url: 'https://makerworld.com',
|
||||
},
|
||||
{
|
||||
id: 'THINGIVERSE',
|
||||
label: 'Thingiverse',
|
||||
url: 'https://www.thingiverse.com',
|
||||
},
|
||||
{
|
||||
id: 'THANGS',
|
||||
label: 'Thangs',
|
||||
url: 'https://thangs.com',
|
||||
},
|
||||
{
|
||||
id: 'CULTS3D',
|
||||
label: 'Cults3D',
|
||||
url: 'https://cults3d.com',
|
||||
},
|
||||
{
|
||||
id: 'YEGGI',
|
||||
label: 'Yeggi',
|
||||
url: 'https://www.yeggi.com',
|
||||
},
|
||||
] as const;
|
||||
readonly favoriteModelSourceIds = ['PRINTABLES', 'MAKERWORLD'] as const;
|
||||
readonly favoriteModelSources = this.modelSources.filter((source) =>
|
||||
this.favoriteModelSourceIds.includes(
|
||||
source.id as (typeof this.favoriteModelSourceIds)[number],
|
||||
),
|
||||
);
|
||||
readonly otherModelSources = this.modelSources.filter(
|
||||
(source) =>
|
||||
!this.favoriteModelSourceIds.includes(
|
||||
source.id as (typeof this.favoriteModelSourceIds)[number],
|
||||
),
|
||||
);
|
||||
|
||||
orderSuccess = signal(false);
|
||||
requiresRecalculation = signal(false);
|
||||
@@ -168,31 +115,6 @@ export class CalculatorPageComponent implements OnInit, AfterViewInit {
|
||||
});
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
const pendingDraft = this.estimator.consumePendingCalculatorDraft();
|
||||
if (!pendingDraft || this.currentSessionId()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.uploadForm?.restoreRequestDraft(pendingDraft.request, {
|
||||
sameSettingsForAll: pendingDraft.sameSettingsForAll,
|
||||
selectedFileName: pendingDraft.selectedFileName,
|
||||
});
|
||||
}
|
||||
|
||||
modeContentKey(field: string): string {
|
||||
const modeKey = this.mode() === 'easy' ? 'BASIC' : 'ADVANCED';
|
||||
return `CALC.MODES.${modeKey}.${field}`;
|
||||
}
|
||||
|
||||
modelSourceDescriptionKey(id: string): string {
|
||||
return `CALC.MODEL_SOURCES.ITEMS.${id}`;
|
||||
}
|
||||
|
||||
faqKey(id: string, field: string): string {
|
||||
return `CALC.FAQ.ITEMS.${id}.${field}`;
|
||||
}
|
||||
|
||||
loadSession(sessionId: string) {
|
||||
this.loading.set(true);
|
||||
this.estimator.getQuoteSession(sessionId).subscribe({
|
||||
@@ -611,7 +533,7 @@ export class CalculatorPageComponent implements OnInit, AfterViewInit {
|
||||
if (this.cadSessionLocked()) return;
|
||||
|
||||
const targetPath = nextMode === 'easy' ? 'basic' : 'advanced';
|
||||
const currentPath = this.route.snapshot?.routeConfig?.path;
|
||||
const currentPath = this.route.snapshot.routeConfig?.path;
|
||||
|
||||
this.mode.set(nextMode);
|
||||
|
||||
@@ -619,54 +541,12 @@ export class CalculatorPageComponent implements OnInit, AfterViewInit {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.currentSessionId()) {
|
||||
this.persistPendingDraftForModeSwitch();
|
||||
}
|
||||
|
||||
this.router.navigate(['..', targetPath], {
|
||||
relativeTo: this.route,
|
||||
queryParamsHandling: 'preserve',
|
||||
});
|
||||
}
|
||||
|
||||
private currentSessionId(): string | null {
|
||||
const fromResult = this.result()?.sessionId;
|
||||
if (fromResult) {
|
||||
return fromResult;
|
||||
}
|
||||
|
||||
const snapshot = this.route.snapshot;
|
||||
const fromQueryParamMap = snapshot?.queryParamMap?.get?.('session');
|
||||
if (fromQueryParamMap) {
|
||||
return fromQueryParamMap;
|
||||
}
|
||||
|
||||
const fromQueryParams = snapshot?.queryParams?.['session'];
|
||||
return typeof fromQueryParams === 'string' && fromQueryParams.length > 0
|
||||
? fromQueryParams
|
||||
: null;
|
||||
}
|
||||
|
||||
private persistPendingDraftForModeSwitch(): void {
|
||||
if (!this.uploadForm) {
|
||||
this.estimator.setPendingCalculatorDraft(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const request = this.uploadForm.getCurrentRequestDraft();
|
||||
if (!request.items.length) {
|
||||
this.estimator.setPendingCalculatorDraft(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const draft: PendingCalculatorDraft = {
|
||||
request,
|
||||
sameSettingsForAll: this.uploadForm.sameSettingsForAll(),
|
||||
selectedFileName: this.uploadForm.selectedFile()?.name ?? null,
|
||||
};
|
||||
this.estimator.setPendingCalculatorDraft(draft);
|
||||
}
|
||||
|
||||
private toTrackedSettingsFromRequest(
|
||||
req: QuoteRequest,
|
||||
): TrackedPrintSettings {
|
||||
|
||||
@@ -573,17 +573,12 @@ export class UploadFormComponent implements OnInit {
|
||||
|
||||
const patch: any = {};
|
||||
if (settings.materialCode) patch.material = settings.materialCode;
|
||||
if (settings.quality) {
|
||||
patch.quality = this.normalizeQualityValue(settings.quality);
|
||||
}
|
||||
|
||||
const layer = Number(settings.layerHeightMm);
|
||||
if (Number.isFinite(layer)) {
|
||||
patch.layerHeight = layer;
|
||||
if (!patch.quality) {
|
||||
patch.quality =
|
||||
layer >= 0.24 ? 'draft' : layer <= 0.12 ? 'extra_fine' : 'standard';
|
||||
}
|
||||
patch.quality =
|
||||
layer >= 0.24 ? 'draft' : layer <= 0.12 ? 'extra_fine' : 'standard';
|
||||
}
|
||||
|
||||
const nozzle = Number(settings.nozzleDiameterMm);
|
||||
@@ -711,69 +706,6 @@ export class UploadFormComponent implements OnInit {
|
||||
this.emitItemSettingsDiffChange();
|
||||
}
|
||||
|
||||
restoreRequestDraft(
|
||||
request: QuoteRequest,
|
||||
options?: {
|
||||
sameSettingsForAll?: boolean;
|
||||
selectedFileName?: string | null;
|
||||
},
|
||||
) {
|
||||
if (!request?.items?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setFiles(request.items.map((item) => item.file));
|
||||
this.patchSettings({
|
||||
materialCode: request.material,
|
||||
quality: request.quality,
|
||||
layerHeightMm: request.layerHeight,
|
||||
nozzleDiameterMm: request.nozzleDiameter,
|
||||
infillPercent: request.infillDensity,
|
||||
infillPattern: request.infillPattern,
|
||||
supportsEnabled: request.supportEnabled,
|
||||
notes: request.notes,
|
||||
});
|
||||
|
||||
const sameSettingsForAll =
|
||||
this.mode() === 'advanced' ? (options?.sameSettingsForAll ?? true) : true;
|
||||
this.onSameSettingsToggle(sameSettingsForAll);
|
||||
|
||||
request.items.forEach((item, index) => {
|
||||
this.updateItemQuantityByIndex(index, Number(item.quantity || 1));
|
||||
this.setItemPrintSettingsByIndex(index, {
|
||||
material: item.material ?? request.material,
|
||||
quality: item.quality ?? request.quality,
|
||||
nozzleDiameter: item.nozzleDiameter ?? request.nozzleDiameter,
|
||||
layerHeight: item.layerHeight ?? request.layerHeight,
|
||||
infillDensity: item.infillDensity ?? request.infillDensity,
|
||||
infillPattern: item.infillPattern ?? request.infillPattern,
|
||||
supportEnabled: item.supportEnabled ?? request.supportEnabled,
|
||||
});
|
||||
|
||||
if (item.color) {
|
||||
this.updateItemColor(index, {
|
||||
colorName: item.color,
|
||||
filamentVariantId: item.filamentVariantId,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const selectedFileName = this.normalizeFileName(
|
||||
options?.selectedFileName ?? '',
|
||||
);
|
||||
const target =
|
||||
this.items().find(
|
||||
(item) => this.normalizeFileName(item.file.name) === selectedFileName,
|
||||
) ?? this.items()[this.items().length - 1];
|
||||
|
||||
if (target) {
|
||||
this.selectFile(target.file);
|
||||
}
|
||||
|
||||
this.emitPrintSettingsChange();
|
||||
this.emitItemSettingsDiffChange();
|
||||
}
|
||||
|
||||
getCurrentRequestDraft(): QuoteRequest {
|
||||
const defaults = this.getCurrentGlobalItemDefaults();
|
||||
|
||||
|
||||
@@ -30,12 +30,6 @@ export interface QuoteRequest {
|
||||
mode: 'easy' | 'advanced';
|
||||
}
|
||||
|
||||
export interface PendingCalculatorDraft {
|
||||
request: QuoteRequest;
|
||||
sameSettingsForAll: boolean;
|
||||
selectedFileName?: string | null;
|
||||
}
|
||||
|
||||
export interface QuoteItem {
|
||||
id?: string;
|
||||
fileName: string;
|
||||
@@ -136,7 +130,6 @@ export class QuoteEstimatorService {
|
||||
files: File[];
|
||||
message: string;
|
||||
} | null>(null);
|
||||
private pendingCalculatorDraft = signal<PendingCalculatorDraft | null>(null);
|
||||
|
||||
getOptions(): Observable<OptionsResponse> {
|
||||
const headers: any = {};
|
||||
@@ -348,16 +341,6 @@ export class QuoteEstimatorService {
|
||||
return data;
|
||||
}
|
||||
|
||||
setPendingCalculatorDraft(data: PendingCalculatorDraft | null) {
|
||||
this.pendingCalculatorDraft.set(data);
|
||||
}
|
||||
|
||||
consumePendingCalculatorDraft(): PendingCalculatorDraft | null {
|
||||
const data = this.pendingCalculatorDraft();
|
||||
this.pendingCalculatorDraft.set(null);
|
||||
return data;
|
||||
}
|
||||
|
||||
getLineItemContent(
|
||||
sessionId: string,
|
||||
lineItemId: string,
|
||||
|
||||
@@ -53,12 +53,12 @@
|
||||
"TITLE": "3D-Druck-Angebotsrechner | 3D fab",
|
||||
"DESCRIPTION": "Laden Sie Ihre 3D-Datei hoch und erhalten Sie Preis und Lieferzeit in Sekunden mit echtem Slicing.",
|
||||
"BASIC": {
|
||||
"TITLE": "Schnelles 3D-Druck-Angebot fuer fertige Dateien | 3D fab",
|
||||
"DESCRIPTION": "Laden Sie eine fertige STL- oder 3MF-Datei hoch und erhalten Sie in Sekunden einen passenden 3D-Druck-Preis mit dem Basis-Rechner."
|
||||
"TITLE": "Einfacher 3D-Druck-Rechner | 3D fab",
|
||||
"DESCRIPTION": "Berechnen Sie den Preis Ihres 3D-Drucks schnell mit dem Basis-Workflow."
|
||||
},
|
||||
"ADVANCED": {
|
||||
"TITLE": "Praezises 3D-Druck-Angebot mit Material und Einstellungen | 3D fab",
|
||||
"DESCRIPTION": "Waehlen Sie Material, Schichthoehe, Stuetzstrukturen und Infill fuer ein genaueres 3D-Druck-Angebot."
|
||||
"TITLE": "Erweiterter 3D-Druck-Rechner | 3D fab",
|
||||
"DESCRIPTION": "Konfigurieren Sie erweiterte Druckparameter und erhalten Sie ein präzises Angebot mit echtem Slicing."
|
||||
}
|
||||
},
|
||||
"SHOP": {
|
||||
@@ -108,84 +108,11 @@
|
||||
"CALC": {
|
||||
"TITLE": "3D-Angebot berechnen",
|
||||
"SUBTITLE": "Laden Sie Ihre 3D-Datei (STL, 3MF) hoch, stellen Sie Qualität und Farbe ein und berechnen Sie sofort Preis und Lieferzeit.",
|
||||
"EYEBROW": "3D-Druck-Angebot online",
|
||||
"CTA_START": "Jetzt starten",
|
||||
"BUSINESS": "Unternehmen",
|
||||
"PRIVATE": "Privat",
|
||||
"MODE_EASY": "Basis",
|
||||
"MODE_ADVANCED": "Erweitert",
|
||||
"WHEN_TO_USE_LABEL": "Wann verwenden:",
|
||||
"MODES": {
|
||||
"BASIC": {
|
||||
"TOGGLE_HINT": "Fuer fertige Dateien",
|
||||
"TITLE": "Schnelles Angebot fuer fertige 3D-Dateien",
|
||||
"SUBTITLE": "Der Basis-Modus ist fuer Nutzer gedacht, die in Sekunden einen passenden Preis erhalten wollen, ohne technische Druckparameter im Detail zu verwalten.",
|
||||
"UPLOAD_HELP": "Laden Sie eine bereits exportierte STL- oder 3MF-Datei hoch, waehlen Sie Material und Qualitaet und erhalten Sie sofort eine klare Preis- und Zeitabschaetzung.",
|
||||
"WHEN_TO_USE": "Verwenden Sie Basis, wenn Ihr Modell fertig ist und Sie vor allem ein schnelles, einfaches Angebot brauchen."
|
||||
},
|
||||
"ADVANCED": {
|
||||
"TOGGLE_HINT": "Mehr Kontrolle ueber Druck und Material",
|
||||
"TITLE": "Genaueres Angebot mit Material und Einstellungen",
|
||||
"SUBTITLE": "Der erweiterte Modus ist fuer Nutzer gedacht, die durch relevante Druckeinstellungen eine genauere Schaetzung erhalten wollen.",
|
||||
"UPLOAD_HELP": "Laden Sie Ihre 3D-Datei hoch und stellen Sie Material, Schichthoehe, Stuetzstrukturen, Infill und Duese ein, um naeher an die spaetere Druckkonfiguration zu kommen.",
|
||||
"WHEN_TO_USE": "Verwenden Sie Erweitert, wenn Sie Materialien oder Einstellungen vergleichen und mehr Kontrolle ueber das Ergebnis wollen."
|
||||
}
|
||||
},
|
||||
"COMPARE_KICKER": "Schnelle Auswahl",
|
||||
"COMPARE_TITLE": "Basis oder Erweitert?",
|
||||
"COMPARE_SUBTITLE": "Die beiden Seiten sind fuer unterschiedliche Anwendungsfaelle gedacht. Starten Sie mit Basis fuer Geschwindigkeit und wechseln Sie zu Erweitert, wenn Material oder Druckeinstellungen wichtig werden.",
|
||||
"COMPARE": {
|
||||
"BASIC": {
|
||||
"TITLE": "Basis: schnelles Angebot",
|
||||
"TEXT": "Ideal, wenn Sie bereits eine fertige Datei haben und schnell einen passenden Preis moechten, ohne technische Druckdetails einzustellen."
|
||||
},
|
||||
"ADVANCED": {
|
||||
"TITLE": "Erweitert: genaueres Angebot",
|
||||
"TEXT": "Ideal, wenn Sie Material, Schichthoehe, Stuetzstrukturen, Infill und weitere Einstellungen waehlen moechten, die das Endergebnis staerker beeinflussen."
|
||||
}
|
||||
},
|
||||
"MODEL_SOURCES": {
|
||||
"KICKER": "3D-Modelle",
|
||||
"TITLE": "Wo Sie 3D-Modelle zum Hochladen finden",
|
||||
"TEXT": "Wenn Sie noch keine Datei haben, koennen Sie auf diesen Plattformen starten. Pruefen Sie vor dem Hochladen immer Lizenz, Abmessungen und Modellqualitaet.",
|
||||
"FAVORITES_TITLE": "Unsere Favoriten",
|
||||
"OTHERS_TITLE": "Weitere",
|
||||
"ITEMS": {
|
||||
"PRINTABLES": "Grosses Verzeichnis druckbarer Modelle aus der Maker-Community.",
|
||||
"MAKERWORLD": "Repository mit druckfertigen Modellen, besonders nuetzlich fuer Maker-Projekte.",
|
||||
"THINGIVERSE": "Langjaehriges Archiv mit sehr vielen kostenlosen Modellen in verschiedenen Kategorien.",
|
||||
"THANGS": "3D-Suchmaschine und Repository mit verwandten Versionen und Sammlungen.",
|
||||
"CULTS3D": "Marktplatz mit kostenlosen und kostenpflichtigen Modellen fuer viele Einsatzzwecke.",
|
||||
"YEGGI": "Suchmaschine, die 3D-Modelle aus verschiedenen Seiten zusammenfuehrt."
|
||||
}
|
||||
},
|
||||
"FAQ": {
|
||||
"KICKER": "Hauefige Fragen",
|
||||
"TITLE": "Mini-FAQ",
|
||||
"SUBTITLE": "Kurze Antworten auf die Fragen, die nicht-technische Nutzer am haeufigsten ausbremsen.",
|
||||
"ITEMS": {
|
||||
"FILES": {
|
||||
"Q": "Welche Dateien kann ich hochladen?",
|
||||
"A": "Sie koennen STL- oder 3MF-Dateien hochladen. Wenn Sie ein Modell von einer externen Plattform herunterladen, stellen Sie sicher, dass es sich wirklich um eine 3D-Datei und nicht nur um Bilder oder Dokumentation handelt."
|
||||
},
|
||||
"MODE": {
|
||||
"Q": "Wie waehle ich zwischen Basis und Erweitert?",
|
||||
"A": "Waehlen Sie Basis fuer ein schnelles Angebot zu einer fertigen Datei. Waehlen Sie Erweitert, wenn Sie Materialien vergleichen oder Einstellungen aendern moechten, die Zeit, Gewicht und Preis beeinflussen."
|
||||
},
|
||||
"NO_MODEL": {
|
||||
"Q": "Was, wenn ich noch kein 3D-Modell habe?",
|
||||
"A": "Sie koennen auf den oben genannten Plattformen suchen oder uns kontaktieren, wenn Sie Hilfe bei CAD-Konstruktion oder Anpassungen brauchen."
|
||||
},
|
||||
"PRICE": {
|
||||
"Q": "Ist der angezeigte Preis schon verlaesslich?",
|
||||
"A": "Es handelt sich um eine automatische Schaetzung auf Basis Ihrer Datei und der gewaehlten Einstellungen. Der erweiterte Modus ist genauer, weil mehr Druckparameter beruecksichtigt werden."
|
||||
},
|
||||
"BEFORE_UPLOAD": {
|
||||
"Q": "Was sollte ich vor dem Hochladen pruefen?",
|
||||
"A": "Pruefen Sie, ob das Modell den richtigen Massstab hat, die Datei nicht beschaedigt ist und die Geometrie wirklich dem finalen Bauteil entspricht, das Sie drucken moechten."
|
||||
}
|
||||
}
|
||||
},
|
||||
"UPLOAD_LABEL": "Ziehen Sie Ihre 3D-Datei hierher",
|
||||
"UPLOAD_SUB": "Wir unterstützen STL, 3MF bis 50MB",
|
||||
"MATERIAL": "Material",
|
||||
|
||||
@@ -53,12 +53,12 @@
|
||||
"TITLE": "3D Printing Quote Calculator | 3D fab",
|
||||
"DESCRIPTION": "Upload your 3D file and get price and lead time in seconds with real slicing.",
|
||||
"BASIC": {
|
||||
"TITLE": "Fast 3D printing quote for ready files | 3D fab",
|
||||
"DESCRIPTION": "Upload a ready STL or 3MF file and get a correct 3D printing price in seconds with the basic calculator."
|
||||
"TITLE": "Basic 3D Printing Calculator | 3D fab",
|
||||
"DESCRIPTION": "Quickly estimate the price of your 3D print with the basic workflow."
|
||||
},
|
||||
"ADVANCED": {
|
||||
"TITLE": "Precise 3D printing quote with materials and settings | 3D fab",
|
||||
"DESCRIPTION": "Set material, layer height, supports and infill to get a more precise 3D printing quote."
|
||||
"TITLE": "Advanced 3D Printing Calculator | 3D fab",
|
||||
"DESCRIPTION": "Configure advanced print settings and get a precise quote based on real slicing."
|
||||
}
|
||||
},
|
||||
"SHOP": {
|
||||
@@ -108,84 +108,11 @@
|
||||
"CALC": {
|
||||
"TITLE": "3D Print Calculator",
|
||||
"SUBTITLE": "Upload your 3D file (STL, 3MF) and get an instant estimate of costs and print time.",
|
||||
"EYEBROW": "Online 3D printing quote",
|
||||
"CTA_START": "Start Now",
|
||||
"BUSINESS": "Business",
|
||||
"PRIVATE": "Private",
|
||||
"MODE_EASY": "Quick",
|
||||
"MODE_ADVANCED": "Advanced",
|
||||
"WHEN_TO_USE_LABEL": "When to use it:",
|
||||
"MODES": {
|
||||
"BASIC": {
|
||||
"TOGGLE_HINT": "For ready-to-print files",
|
||||
"TITLE": "Fast quote for ready 3D files",
|
||||
"SUBTITLE": "Basic mode is for users who want a correct price in seconds without dealing with advanced technical settings.",
|
||||
"UPLOAD_HELP": "Upload an STL or 3MF file that is already exported, choose material and quality, and get a clear price and lead-time estimate right away.",
|
||||
"WHEN_TO_USE": "Use Basic if your model is already final and you mainly need a simple, fast quote."
|
||||
},
|
||||
"ADVANCED": {
|
||||
"TOGGLE_HINT": "More control over print and material",
|
||||
"TITLE": "More precise quote with materials and settings",
|
||||
"SUBTITLE": "Advanced mode is for users who want a more precise estimate by adjusting relevant print settings.",
|
||||
"UPLOAD_HELP": "Upload your 3D file and set material, layer height, supports, infill and nozzle size to get closer to the final print configuration.",
|
||||
"WHEN_TO_USE": "Use Advanced if you want to compare materials or settings and need more control over the outcome."
|
||||
}
|
||||
},
|
||||
"COMPARE_KICKER": "Quick choice",
|
||||
"COMPARE_TITLE": "Basic or Advanced?",
|
||||
"COMPARE_SUBTITLE": "The two pages cover different use cases. Start with Basic for speed, then switch to Advanced when material or print settings matter.",
|
||||
"COMPARE": {
|
||||
"BASIC": {
|
||||
"TITLE": "Basic: fast quote",
|
||||
"TEXT": "Best if you already have a ready file and want a correct price quickly, without going into technical print details."
|
||||
},
|
||||
"ADVANCED": {
|
||||
"TITLE": "Advanced: more precise quote",
|
||||
"TEXT": "Best if you want to choose material, layer height, supports, infill and other settings that have more impact on the final result."
|
||||
}
|
||||
},
|
||||
"MODEL_SOURCES": {
|
||||
"KICKER": "3D models",
|
||||
"TITLE": "Where to find 3D models to upload",
|
||||
"TEXT": "If you do not have a file yet, start from these platforms. Always check license, dimensions and model quality before uploading it to the calculator.",
|
||||
"FAVORITES_TITLE": "Our favorites",
|
||||
"OTHERS_TITLE": "Others",
|
||||
"ITEMS": {
|
||||
"PRINTABLES": "Large catalog of printable models shared by the maker community.",
|
||||
"MAKERWORLD": "Repository of ready-to-print models, especially useful for maker projects.",
|
||||
"THINGIVERSE": "Long-running archive with a very large number of free models in many categories.",
|
||||
"THANGS": "3D search engine and repository with related versions and collections.",
|
||||
"CULTS3D": "Marketplace with both free and paid models across many use cases.",
|
||||
"YEGGI": "Search engine that aggregates 3D models from different sites."
|
||||
}
|
||||
},
|
||||
"FAQ": {
|
||||
"KICKER": "Common questions",
|
||||
"TITLE": "Mini FAQ",
|
||||
"SUBTITLE": "Quick answers to the questions that most often block non-technical users.",
|
||||
"ITEMS": {
|
||||
"FILES": {
|
||||
"Q": "Which files can I upload?",
|
||||
"A": "You can upload STL or 3MF files. If you download a model from an external portal, make sure it is an actual 3D file and not just images or documentation."
|
||||
},
|
||||
"MODE": {
|
||||
"Q": "How do I choose between Basic and Advanced?",
|
||||
"A": "Choose Basic if you want a fast quote for a ready file. Choose Advanced if you need to compare materials or change settings that affect time, weight and price."
|
||||
},
|
||||
"NO_MODEL": {
|
||||
"Q": "What if I do not have a 3D model yet?",
|
||||
"A": "You can search on the platforms listed above or contact us if you need help with CAD design or adaptation."
|
||||
},
|
||||
"PRICE": {
|
||||
"Q": "Is the shown price already reliable?",
|
||||
"A": "It is an automatic estimate based on your file and the selected settings. Advanced mode is more precise because it accounts for more print parameters."
|
||||
},
|
||||
"BEFORE_UPLOAD": {
|
||||
"Q": "What should I check before uploading?",
|
||||
"A": "Check that the model has the correct scale, that the file is not corrupted and that the geometry really matches the final part you want to print."
|
||||
}
|
||||
}
|
||||
},
|
||||
"UPLOAD_LABEL": "Drag your 3D file here",
|
||||
"UPLOAD_SUB": "Supports STL, 3MF up to 50MB",
|
||||
"MATERIAL": "Material",
|
||||
|
||||
@@ -27,12 +27,12 @@
|
||||
"TITLE": "Calculateur de devis impression 3D | 3D fab",
|
||||
"DESCRIPTION": "Chargez votre fichier 3D et obtenez prix et délais en quelques secondes avec un vrai slicing.",
|
||||
"BASIC": {
|
||||
"TITLE": "Devis impression 3D rapide pour fichiers prets | 3D fab",
|
||||
"DESCRIPTION": "Chargez un fichier STL ou 3MF deja pret et obtenez en quelques secondes un prix d impression 3D fiable avec le calculateur de base."
|
||||
"TITLE": "Calculateur impression 3D de base | 3D fab",
|
||||
"DESCRIPTION": "Calculez rapidement le prix de votre impression 3D avec le parcours de base."
|
||||
},
|
||||
"ADVANCED": {
|
||||
"TITLE": "Devis impression 3D precis avec materiaux et reglages | 3D fab",
|
||||
"DESCRIPTION": "Definissez le materiau, la hauteur de couche, les supports et le remplissage pour obtenir un devis d impression 3D plus precis."
|
||||
"TITLE": "Calculateur impression 3D avancé | 3D fab",
|
||||
"DESCRIPTION": "Configurez des paramètres avancés et obtenez un devis précis basé sur un vrai slicing."
|
||||
}
|
||||
},
|
||||
"SHOP": {
|
||||
@@ -140,84 +140,11 @@
|
||||
"CALC": {
|
||||
"TITLE": "Calculer un devis 3D",
|
||||
"SUBTITLE": "Chargez votre fichier 3D (STL, 3MF), réglez la qualité et la couleur puis calculez immédiatement prix et délais.",
|
||||
"EYEBROW": "Devis impression 3D en ligne",
|
||||
"CTA_START": "Commencer maintenant",
|
||||
"BUSINESS": "Entreprises",
|
||||
"PRIVATE": "Particuliers",
|
||||
"MODE_EASY": "Base",
|
||||
"MODE_ADVANCED": "Avancée",
|
||||
"WHEN_TO_USE_LABEL": "Quand l'utiliser :",
|
||||
"MODES": {
|
||||
"BASIC": {
|
||||
"TOGGLE_HINT": "Pour fichiers deja prets",
|
||||
"TITLE": "Devis rapide pour fichiers 3D deja prets",
|
||||
"SUBTITLE": "Le mode Base est pense pour les utilisateurs qui veulent un prix fiable en quelques secondes sans gerer des reglages techniques avances.",
|
||||
"UPLOAD_HELP": "Chargez un fichier STL ou 3MF deja exporte, choisissez le materiau et la qualite, puis obtenez tout de suite une estimation claire du prix et du delai.",
|
||||
"WHEN_TO_USE": "Utilisez Base si votre modele est deja finalise et que vous voulez surtout un devis simple et rapide."
|
||||
},
|
||||
"ADVANCED": {
|
||||
"TOGGLE_HINT": "Plus de controle sur l impression",
|
||||
"TITLE": "Devis plus precis avec materiaux et reglages",
|
||||
"SUBTITLE": "Le mode avance est pense pour les utilisateurs qui veulent une estimation plus precise en ajustant les parametres d impression importants.",
|
||||
"UPLOAD_HELP": "Chargez votre fichier 3D et reglez le materiau, la hauteur de couche, les supports, le remplissage et la buse pour vous rapprocher de la configuration finale.",
|
||||
"WHEN_TO_USE": "Utilisez Avancee si vous voulez comparer des materiaux ou des reglages et garder plus de controle sur le resultat."
|
||||
}
|
||||
},
|
||||
"COMPARE_KICKER": "Choix rapide",
|
||||
"COMPARE_TITLE": "Base ou Avancee ?",
|
||||
"COMPARE_SUBTITLE": "Les deux pages repondent a des besoins differents. Commencez par Base pour aller vite, puis passez a Avancee quand le materiau ou les reglages d impression deviennent importants.",
|
||||
"COMPARE": {
|
||||
"BASIC": {
|
||||
"TITLE": "Base : devis rapide",
|
||||
"TEXT": "Ideal si vous avez deja un fichier pret et que vous voulez recevoir rapidement un prix fiable, sans entrer dans les details techniques."
|
||||
},
|
||||
"ADVANCED": {
|
||||
"TITLE": "Avancee : devis plus precis",
|
||||
"TEXT": "Ideal si vous voulez choisir le materiau, la hauteur de couche, les supports, le remplissage et d autres reglages qui influencent davantage le resultat final."
|
||||
}
|
||||
},
|
||||
"MODEL_SOURCES": {
|
||||
"KICKER": "Modeles 3D",
|
||||
"TITLE": "Ou trouver des modeles 3D a charger",
|
||||
"TEXT": "Si vous n avez pas encore de fichier, vous pouvez commencer par ces plateformes. Verifiez toujours la licence, les dimensions et la qualite du modele avant de le charger dans le calculateur.",
|
||||
"FAVORITES_TITLE": "Nos preferes",
|
||||
"OTHERS_TITLE": "Autres",
|
||||
"ITEMS": {
|
||||
"PRINTABLES": "Grand catalogue de modeles imprimables partages par la communaute maker.",
|
||||
"MAKERWORLD": "Repository de modeles prets a imprimer, utile surtout pour les projets maker.",
|
||||
"THINGIVERSE": "Archive historique avec un tres grand nombre de modeles gratuits dans de nombreuses categories.",
|
||||
"THANGS": "Moteur de recherche et repository 3D avec versions et collections associees.",
|
||||
"CULTS3D": "Marketplace avec des modeles gratuits et payants pour des usages tres varies.",
|
||||
"YEGGI": "Moteur de recherche qui agrège des modeles 3D depuis plusieurs sites."
|
||||
}
|
||||
},
|
||||
"FAQ": {
|
||||
"KICKER": "Questions frequentes",
|
||||
"TITLE": "Mini FAQ",
|
||||
"SUBTITLE": "Des reponses rapides aux questions qui bloquent le plus souvent les utilisateurs non techniques.",
|
||||
"ITEMS": {
|
||||
"FILES": {
|
||||
"Q": "Quels fichiers puis-je charger ?",
|
||||
"A": "Vous pouvez charger des fichiers STL ou 3MF. Si vous telechargez un modele depuis un portail externe, verifiez qu il s agit bien d un vrai fichier 3D et pas seulement d images ou de documentation."
|
||||
},
|
||||
"MODE": {
|
||||
"Q": "Comment choisir entre Base et Avancee ?",
|
||||
"A": "Choisissez Base si vous voulez un devis rapide pour un fichier deja pret. Choisissez Avancee si vous devez comparer des materiaux ou modifier des reglages qui influencent le temps, le poids et le prix."
|
||||
},
|
||||
"NO_MODEL": {
|
||||
"Q": "Et si je n ai pas encore de modele 3D ?",
|
||||
"A": "Vous pouvez chercher sur les plateformes ci-dessus ou nous contacter si vous avez besoin d aide pour la conception ou l adaptation CAD."
|
||||
},
|
||||
"PRICE": {
|
||||
"Q": "Le prix affiche est-il deja fiable ?",
|
||||
"A": "C est une estimation automatique basee sur votre fichier et les reglages choisis. Le mode Avancee est plus precis car il tient compte d un plus grand nombre de parametres d impression."
|
||||
},
|
||||
"BEFORE_UPLOAD": {
|
||||
"Q": "Que faut-il verifier avant le chargement ?",
|
||||
"A": "Verifiez que le modele a la bonne echelle, que le fichier n est pas corrompu et que la geometrie correspond bien a la piece finale que vous voulez imprimer."
|
||||
}
|
||||
}
|
||||
},
|
||||
"UPLOAD_LABEL": "Glissez votre fichier 3D ici",
|
||||
"UPLOAD_SUB": "Nous prenons en charge STL, 3MF jusqu'à 50MB",
|
||||
"MATERIAL": "Matériau",
|
||||
|
||||
@@ -27,12 +27,12 @@
|
||||
"TITLE": "Calcolatore preventivo stampa 3D | 3D fab",
|
||||
"DESCRIPTION": "Carica il file 3D e ottieni prezzo e tempi in pochi secondi con slicing reale.",
|
||||
"BASIC": {
|
||||
"TITLE": "Preventivo stampa 3D veloce da file pronti | 3D fab",
|
||||
"DESCRIPTION": "Carica un file STL o 3MF gia pronto e ricevi un prezzo corretto in pochi secondi con il calcolatore base."
|
||||
"TITLE": "Calcolatore stampa 3D base | 3D fab",
|
||||
"DESCRIPTION": "Calcola rapidamente il prezzo della tua stampa 3D in modalita base."
|
||||
},
|
||||
"ADVANCED": {
|
||||
"TITLE": "Preventivo stampa 3D preciso con materiali e impostazioni | 3D fab",
|
||||
"DESCRIPTION": "Configura materiale, layer, supporti e riempimento per ottenere un preventivo stampa 3D piu preciso."
|
||||
"TITLE": "Calcolatore stampa 3D avanzato | 3D fab",
|
||||
"DESCRIPTION": "Configura parametri avanzati e ottieni un preventivo preciso con slicing reale."
|
||||
}
|
||||
},
|
||||
"SHOP": {
|
||||
@@ -140,84 +140,11 @@
|
||||
"CALC": {
|
||||
"TITLE": "Calcola Preventivo 3D",
|
||||
"SUBTITLE": "Carica il tuo file 3D (STL, 3MF), imposta la qualità, il colore e calcola immediatamente prezzo e tempi.",
|
||||
"EYEBROW": "Preventivo stampa 3D online",
|
||||
"CTA_START": "Inizia Ora",
|
||||
"BUSINESS": "Aziende",
|
||||
"PRIVATE": "Privati",
|
||||
"MODE_EASY": "Base",
|
||||
"MODE_ADVANCED": "Avanzata",
|
||||
"WHEN_TO_USE_LABEL": "Quando usarla:",
|
||||
"MODES": {
|
||||
"BASIC": {
|
||||
"TOGGLE_HINT": "Per file gia pronti",
|
||||
"TITLE": "Preventivo veloce per file 3D gia pronti",
|
||||
"SUBTITLE": "La modalita Base e pensata per chi vuole un prezzo corretto in pochi secondi senza gestire parametri tecnici avanzati.",
|
||||
"UPLOAD_HELP": "Carica un file STL o 3MF gia esportato, scegli materiale e qualita, poi ricevi subito una stima chiara di prezzo e tempo.",
|
||||
"WHEN_TO_USE": "Usa Base se il modello e pronto e ti serve soprattutto un preventivo rapido e semplice."
|
||||
},
|
||||
"ADVANCED": {
|
||||
"TOGGLE_HINT": "Piu controllo su stampa e materiale",
|
||||
"TITLE": "Preventivo piu preciso con materiali e impostazioni",
|
||||
"SUBTITLE": "La modalita Avanzata e pensata per chi vuole una stima piu precisa regolando parametri di stampa rilevanti.",
|
||||
"UPLOAD_HELP": "Carica il file 3D e imposta materiale, layer, supporti, riempimento e ugello per avvicinare di piu il preventivo alla configurazione finale.",
|
||||
"WHEN_TO_USE": "Usa Avanzata se devi confrontare materiali o impostazioni e vuoi piu controllo sul risultato."
|
||||
}
|
||||
},
|
||||
"COMPARE_KICKER": "Scelta rapida",
|
||||
"COMPARE_TITLE": "Basic o Advanced?",
|
||||
"COMPARE_SUBTITLE": "Le due pagine servono a casi d uso diversi. Parti da Base per fare presto, passa ad Avanzata quando il materiale o le impostazioni fanno la differenza.",
|
||||
"COMPARE": {
|
||||
"BASIC": {
|
||||
"TITLE": "Base: preventivo veloce",
|
||||
"TEXT": "Ideale se hai un file gia pronto e vuoi ricevere un prezzo corretto in modo semplice, senza entrare nei dettagli tecnici."
|
||||
},
|
||||
"ADVANCED": {
|
||||
"TITLE": "Avanzata: preventivo piu preciso",
|
||||
"TEXT": "Ideale se vuoi scegliere materiale, layer, supporti, riempimento e altre impostazioni che incidono di piu sul risultato finale."
|
||||
}
|
||||
},
|
||||
"MODEL_SOURCES": {
|
||||
"KICKER": "Modelli 3D",
|
||||
"TITLE": "Dove trovare modelli 3D da caricare",
|
||||
"TEXT": "Se non hai ancora un file, puoi partire da queste piattaforme. Controlla sempre licenza, dimensioni e qualita del modello prima di caricarlo nel calcolatore.",
|
||||
"FAVORITES_TITLE": "I nostri preferiti",
|
||||
"OTHERS_TITLE": "Altri",
|
||||
"ITEMS": {
|
||||
"PRINTABLES": "Ampio catalogo di modelli stampabili condivisi dalla community.",
|
||||
"MAKERWORLD": "Repository con modelli pronti alla stampa, utile soprattutto in ambito maker.",
|
||||
"THINGIVERSE": "Archivio storico con moltissimi modelli gratuiti in diverse categorie.",
|
||||
"THANGS": "Motore di ricerca e repository 3D con raccolte e versioni correlate.",
|
||||
"CULTS3D": "Marketplace con modelli gratuiti e a pagamento per progetti molto diversi.",
|
||||
"YEGGI": "Motore di ricerca che aggrega modelli 3D da siti differenti."
|
||||
}
|
||||
},
|
||||
"FAQ": {
|
||||
"KICKER": "Dubbi comuni",
|
||||
"TITLE": "Mini FAQ",
|
||||
"SUBTITLE": "Le risposte rapide alle domande che bloccano piu spesso chi non usa abitualmente la stampa 3D.",
|
||||
"ITEMS": {
|
||||
"FILES": {
|
||||
"Q": "Che file posso caricare?",
|
||||
"A": "Puoi caricare file STL o 3MF. Se scarichi un modello da un portale esterno, verifica che sia gia un file 3D pronto e non solo immagini o documentazione."
|
||||
},
|
||||
"MODE": {
|
||||
"Q": "Come scelgo tra Base e Avanzata?",
|
||||
"A": "Scegli Base se vuoi un preventivo rapido per un file gia pronto. Scegli Avanzata se devi confrontare materiali o modificare impostazioni che cambiano tempo, peso e costo."
|
||||
},
|
||||
"NO_MODEL": {
|
||||
"Q": "E se non ho ancora il modello 3D?",
|
||||
"A": "Puoi cercarlo nei portali suggeriti qui sopra oppure contattarci se ti serve aiuto con progettazione o adattamento CAD."
|
||||
},
|
||||
"PRICE": {
|
||||
"Q": "Il prezzo mostrato e gia affidabile?",
|
||||
"A": "E una stima automatica basata sul file e sulle impostazioni selezionate. In modalita Avanzata la stima e piu precisa perche considera piu parametri di stampa."
|
||||
},
|
||||
"BEFORE_UPLOAD": {
|
||||
"Q": "Cosa conviene controllare prima del caricamento?",
|
||||
"A": "Controlla che il modello abbia la scala corretta, che il file non sia corrotto e che la geometria sia davvero quella finale che vuoi stampare."
|
||||
}
|
||||
}
|
||||
},
|
||||
"UPLOAD_LABEL": "Trascina il tuo file 3D qui",
|
||||
"UPLOAD_SUB": "Supportiamo STL, 3MF fino a 50MB",
|
||||
"MATERIAL": "Materiale",
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 382 382" xml:space="preserve">
|
||||
<path style="fill:#0077B7;" d="M347.445,0H34.555C15.471,0,0,15.471,0,34.555v312.889C0,366.529,15.471,382,34.555,382h312.889
|
||||
C366.529,382,382,366.529,382,347.444V34.555C382,15.471,366.529,0,347.445,0z M118.207,329.844c0,5.554-4.502,10.056-10.056,10.056
|
||||
H65.345c-5.554,0-10.056-4.502-10.056-10.056V150.403c0-5.554,4.502-10.056,10.056-10.056h42.806
|
||||
c5.554,0,10.056,4.502,10.056,10.056V329.844z M86.748,123.432c-22.459,0-40.666-18.207-40.666-40.666S64.289,42.1,86.748,42.1
|
||||
s40.666,18.207,40.666,40.666S109.208,123.432,86.748,123.432z M341.91,330.654c0,5.106-4.14,9.246-9.246,9.246H286.73
|
||||
c-5.106,0-9.246-4.14-9.246-9.246v-84.168c0-12.556,3.683-55.021-32.813-55.021c-28.309,0-34.051,29.066-35.204,42.11v97.079
|
||||
c0,5.106-4.139,9.246-9.246,9.246h-44.426c-5.106,0-9.246-4.14-9.246-9.246V149.593c0-5.106,4.14-9.246,9.246-9.246h44.426
|
||||
c5.106,0,9.246,4.14,9.246,9.246v15.655c10.497-15.753,26.097-27.912,59.312-27.912c73.552,0,73.131,68.716,73.131,106.472
|
||||
L341.91,330.654L341.91,330.654z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
Reference in New Issue
Block a user