15 Commits

Author SHA1 Message Date
9fc1fc97fa feat(front-end): nav bar 2026-03-12 14:50:15 +01:00
7010a81596 feat(front-end): material page 2026-03-12 14:49:50 +01:00
96cfa91c67 Merge branch 'main' into dev
Some checks failed
PR Checks / prettier-autofix (pull_request) Successful in 8s
PR Checks / security-sast (pull_request) Successful in 29s
PR Checks / test-backend (pull_request) Successful in 28s
Build and Deploy / test-backend (push) Successful in 29s
PR Checks / test-frontend (pull_request) Successful in 1m3s
Build and Deploy / test-frontend (push) Successful in 1m6s
Build and Deploy / build-and-push (push) Failing after 1m25s
Build and Deploy / deploy (push) Has been skipped
2026-03-12 12:17:42 +01:00
669ace82c0 Merge remote-tracking branch 'origin/dev' into dev
Some checks failed
Build and Deploy / build-and-push (push) Has been cancelled
Build and Deploy / deploy (push) Has been cancelled
Build and Deploy / test-frontend (push) Has been cancelled
Build and Deploy / test-backend (push) Has been cancelled
PR Checks / prettier-autofix (pull_request) Successful in 13s
PR Checks / security-sast (pull_request) Successful in 30s
PR Checks / test-backend (pull_request) Successful in 27s
PR Checks / test-frontend (pull_request) Successful in 1m7s
2026-03-12 12:17:28 +01:00
93163ae6e8 fix(front-end): sitemap static 2026-03-12 12:17:13 +01:00
af2d506da1 Merge pull request 'dev' (#42) from dev into main
All checks were successful
Build and Deploy / test-backend (push) Successful in 27s
Build and Deploy / test-frontend (push) Successful in 59s
Build and Deploy / build-and-push (push) Successful in 15s
Build and Deploy / deploy (push) Successful in 8s
Reviewed-on: #42
2026-03-11 17:35:12 +01:00
637541994a Merge branch 'main' into dev
All checks were successful
Build and Deploy / test-backend (push) Successful in 27s
PR Checks / prettier-autofix (pull_request) Successful in 8s
Build and Deploy / test-frontend (push) Successful in 1m1s
PR Checks / security-sast (pull_request) Successful in 31s
PR Checks / test-backend (pull_request) Successful in 26s
Build and Deploy / build-and-push (push) Successful in 29s
PR Checks / test-frontend (pull_request) Successful in 1m3s
Build and Deploy / deploy (push) Successful in 22s
2026-03-11 17:32:53 +01:00
printcalc-ci
63cd4c4f5e style: apply prettier formatting 2026-03-11 16:31:15 +00:00
fd4104da39 fix(front-end): ssr i18n fix
All checks were successful
Build and Deploy / test-backend (push) Successful in 27s
Build and Deploy / test-frontend (push) Successful in 1m1s
Build and Deploy / build-and-push (push) Successful in 29s
Build and Deploy / deploy (push) Successful in 21s
PR Checks / prettier-autofix (pull_request) Successful in 11s
PR Checks / security-sast (pull_request) Successful in 30s
PR Checks / test-backend (pull_request) Successful in 27s
PR Checks / test-frontend (pull_request) Successful in 59s
2026-03-11 17:27:28 +01:00
5bb23fbcfa fix(front-end): ssr i18n fix
All checks were successful
Build and Deploy / test-backend (push) Successful in 26s
Build and Deploy / test-frontend (push) Successful in 1m0s
Build and Deploy / build-and-push (push) Successful in 26s
Build and Deploy / deploy (push) Successful in 19s
2026-03-11 17:23:32 +01:00
6a22c54e9f feat(front-end): ssr i18n fix
All checks were successful
Build and Deploy / test-backend (push) Successful in 26s
Build and Deploy / test-frontend (push) Successful in 1m3s
Build and Deploy / build-and-push (push) Successful in 30s
Build and Deploy / deploy (push) Successful in 19s
2026-03-11 17:19:26 +01:00
3ac3262e77 Merge remote-tracking branch 'origin/dev' into dev
All checks were successful
Build and Deploy / test-backend (push) Successful in 28s
Build and Deploy / test-frontend (push) Successful in 1m3s
Build and Deploy / build-and-push (push) Successful in 27s
Build and Deploy / deploy (push) Successful in 20s
# Conflicts:
#	frontend/src/app/app.config.ts
2026-03-11 17:10:06 +01:00
18ecc07240 feat(front-end): ssr i18n fix 2026-03-11 17:09:51 +01:00
cb468492b3 Merge pull request 'feat(front-end): ssr implementation' (#41) from feat/ssr into dev
All checks were successful
Build and Deploy / test-backend (push) Successful in 27s
Build and Deploy / build-and-push (push) Successful in 52s
Build and Deploy / deploy (push) Successful in 10s
Build and Deploy / test-frontend (push) Successful in 1m0s
Reviewed-on: #41
2026-03-11 16:59:21 +01:00
feee2b0bff Merge pull request 'dev' (#40) from dev into main
All checks were successful
Build and Deploy / test-backend (push) Successful in 27s
Build and Deploy / test-frontend (push) Successful in 1m3s
Build and Deploy / build-and-push (push) Successful in 15s
Build and Deploy / deploy (push) Successful in 7s
Reviewed-on: #40
2026-03-11 15:32:14 +01:00
15 changed files with 2399 additions and 103 deletions

View File

@@ -146,8 +146,22 @@ public class ShopSitemapService {
return;
}
for (String locLanguage : SUPPORTED_LANGUAGES) {
String locHref = hrefByLanguage.get(locLanguage);
if (locHref == null || locHref.isBlank()) {
continue;
}
appendLocalizedUrlEntry(xml, locHref, hrefByLanguage, defaultHref, lastmod);
}
}
private void appendLocalizedUrlEntry(StringBuilder xml,
String locHref,
Map<String, String> hrefByLanguage,
String defaultHref,
OffsetDateTime lastmod) {
xml.append(" <url>\n");
xml.append(" <loc>").append(xmlEscape(defaultHref)).append("</loc>\n");
xml.append(" <loc>").append(xmlEscape(locHref)).append("</loc>\n");
for (String language : SUPPORTED_LANGUAGES) {
String href = hrefByLanguage.get(language);

View File

@@ -89,10 +89,16 @@ class ShopSitemapServiceTest {
String xml = service.getShopSitemapXml();
assertTrue(xml.contains("<loc>https://3d-fab.ch/it/shop/accessori</loc>"));
assertTrue(xml.contains("<loc>https://3d-fab.ch/en/shop/accessori</loc>"));
assertTrue(xml.contains("<loc>https://3d-fab.ch/de/shop/accessori</loc>"));
assertTrue(xml.contains("<loc>https://3d-fab.ch/fr/shop/accessori</loc>"));
assertTrue(xml.contains("hreflang=\"en\" href=\"https://3d-fab.ch/en/shop/accessori\""));
assertFalse(xml.contains("https://3d-fab.ch/it/shop/bozza"));
assertTrue(xml.contains("<loc>https://3d-fab.ch/it/shop/p/123e4567-supporto-bici</loc>"));
assertTrue(xml.contains("<loc>https://3d-fab.ch/en/shop/p/123e4567-bike-holder</loc>"));
assertTrue(xml.contains("<loc>https://3d-fab.ch/de/shop/p/123e4567-fahrrad-halter</loc>"));
assertTrue(xml.contains("<loc>https://3d-fab.ch/fr/shop/p/123e4567-support-velo</loc>"));
assertTrue(xml.contains("hreflang=\"en\" href=\"https://3d-fab.ch/en/shop/p/123e4567-bike-holder\""));
assertTrue(xml.contains("hreflang=\"de\" href=\"https://3d-fab.ch/de/shop/p/123e4567-fahrrad-halter\""));
assertTrue(xml.contains("hreflang=\"x-default\" href=\"https://3d-fab.ch/it/shop/p/123e4567-supporto-bici\""));

View File

@@ -9,9 +9,9 @@ Disallow: /order
Disallow: /order/
Disallow: /*/order
Disallow: /*/order/
Disallow: /co
Disallow: /co$
Disallow: /co/
Disallow: /*/co
Disallow: /*/co$
Disallow: /*/co/
Disallow: /checkout
Disallow: /checkout/

View File

@@ -1,8 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset
xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xhtml="http://www.w3.org/1999/xhtml"
>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">
<url>
<loc>https://3d-fab.ch/it</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it" />
@@ -13,66 +10,119 @@
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://3d-fab.ch/en</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it" />
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://3d-fab.ch/de</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it" />
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://3d-fab.ch/fr</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it" />
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://3d-fab.ch/it/calculator/basic</loc>
<xhtml:link
rel="alternate"
hreflang="it"
href="https://3d-fab.ch/it/calculator/basic"
/>
<xhtml:link
rel="alternate"
hreflang="en"
href="https://3d-fab.ch/en/calculator/basic"
/>
<xhtml:link
rel="alternate"
hreflang="de"
href="https://3d-fab.ch/de/calculator/basic"
/>
<xhtml:link
rel="alternate"
hreflang="fr"
href="https://3d-fab.ch/fr/calculator/basic"
/>
<xhtml:link
rel="alternate"
hreflang="x-default"
href="https://3d-fab.ch/it/calculator/basic"
/>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/calculator/basic" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en/calculator/basic" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de/calculator/basic" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr/calculator/basic" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it/calculator/basic" />
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://3d-fab.ch/en/calculator/basic</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/calculator/basic" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en/calculator/basic" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de/calculator/basic" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr/calculator/basic" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it/calculator/basic" />
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://3d-fab.ch/de/calculator/basic</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/calculator/basic" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en/calculator/basic" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de/calculator/basic" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr/calculator/basic" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it/calculator/basic" />
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://3d-fab.ch/fr/calculator/basic</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/calculator/basic" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en/calculator/basic" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de/calculator/basic" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr/calculator/basic" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it/calculator/basic" />
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://3d-fab.ch/it/calculator/advanced</loc>
<xhtml:link
rel="alternate"
hreflang="it"
href="https://3d-fab.ch/it/calculator/advanced"
/>
<xhtml:link
rel="alternate"
hreflang="en"
href="https://3d-fab.ch/en/calculator/advanced"
/>
<xhtml:link
rel="alternate"
hreflang="de"
href="https://3d-fab.ch/de/calculator/advanced"
/>
<xhtml:link
rel="alternate"
hreflang="fr"
href="https://3d-fab.ch/fr/calculator/advanced"
/>
<xhtml:link
rel="alternate"
hreflang="x-default"
href="https://3d-fab.ch/it/calculator/advanced"
/>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/calculator/advanced" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en/calculator/advanced" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de/calculator/advanced" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr/calculator/advanced" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it/calculator/advanced" />
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://3d-fab.ch/en/calculator/advanced</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/calculator/advanced" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en/calculator/advanced" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de/calculator/advanced" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr/calculator/advanced" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it/calculator/advanced" />
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://3d-fab.ch/de/calculator/advanced</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/calculator/advanced" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en/calculator/advanced" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de/calculator/advanced" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr/calculator/advanced" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it/calculator/advanced" />
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://3d-fab.ch/fr/calculator/advanced</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/calculator/advanced" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en/calculator/advanced" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de/calculator/advanced" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr/calculator/advanced" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it/calculator/advanced" />
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://3d-fab.ch/it/shop</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/shop" />
@@ -83,64 +133,160 @@
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://3d-fab.ch/en/shop</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/shop" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en/shop" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de/shop" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr/shop" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it/shop" />
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://3d-fab.ch/de/shop</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/shop" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en/shop" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de/shop" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr/shop" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it/shop" />
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://3d-fab.ch/fr/shop</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/shop" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en/shop" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de/shop" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr/shop" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it/shop" />
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://3d-fab.ch/it/about</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/about" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en/about" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de/about" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr/about" />
<xhtml:link
rel="alternate"
hreflang="x-default"
href="https://3d-fab.ch/it/about"
/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it/about" />
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://3d-fab.ch/it/contact</loc>
<xhtml:link
rel="alternate"
hreflang="it"
href="https://3d-fab.ch/it/contact"
/>
<xhtml:link
rel="alternate"
hreflang="en"
href="https://3d-fab.ch/en/contact"
/>
<xhtml:link
rel="alternate"
hreflang="de"
href="https://3d-fab.ch/de/contact"
/>
<xhtml:link
rel="alternate"
hreflang="fr"
href="https://3d-fab.ch/fr/contact"
/>
<xhtml:link
rel="alternate"
hreflang="x-default"
href="https://3d-fab.ch/it/contact"
/>
<loc>https://3d-fab.ch/en/about</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/about" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en/about" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de/about" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr/about" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it/about" />
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://3d-fab.ch/de/about</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/about" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en/about" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de/about" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr/about" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it/about" />
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://3d-fab.ch/fr/about</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/about" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en/about" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de/about" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr/about" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it/about" />
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://3d-fab.ch/it/contact</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/contact" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en/contact" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de/contact" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr/contact" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it/contact" />
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://3d-fab.ch/en/contact</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/contact" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en/contact" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de/contact" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr/contact" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it/contact" />
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://3d-fab.ch/de/contact</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/contact" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en/contact" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de/contact" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr/contact" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it/contact" />
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://3d-fab.ch/fr/contact</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/contact" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en/contact" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de/contact" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr/contact" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it/contact" />
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://3d-fab.ch/it/privacy</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/privacy" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en/privacy" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de/privacy" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr/privacy" />
<xhtml:link
rel="alternate"
hreflang="x-default"
href="https://3d-fab.ch/it/privacy"
/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it/privacy" />
<changefreq>yearly</changefreq>
<priority>0.4</priority>
</url>
<url>
<loc>https://3d-fab.ch/en/privacy</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/privacy" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en/privacy" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de/privacy" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr/privacy" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it/privacy" />
<changefreq>yearly</changefreq>
<priority>0.4</priority>
</url>
<url>
<loc>https://3d-fab.ch/de/privacy</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/privacy" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en/privacy" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de/privacy" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr/privacy" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it/privacy" />
<changefreq>yearly</changefreq>
<priority>0.4</priority>
</url>
<url>
<loc>https://3d-fab.ch/fr/privacy</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/privacy" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en/privacy" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de/privacy" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr/privacy" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it/privacy" />
<changefreq>yearly</changefreq>
<priority>0.4</priority>
</url>
<url>
<loc>https://3d-fab.ch/it/terms</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/terms" />
@@ -151,4 +297,34 @@
<changefreq>yearly</changefreq>
<priority>0.4</priority>
</url>
<url>
<loc>https://3d-fab.ch/en/terms</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/terms" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en/terms" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de/terms" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr/terms" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it/terms" />
<changefreq>yearly</changefreq>
<priority>0.4</priority>
</url>
<url>
<loc>https://3d-fab.ch/de/terms</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/terms" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en/terms" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de/terms" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr/terms" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it/terms" />
<changefreq>yearly</changefreq>
<priority>0.4</priority>
</url>
<url>
<loc>https://3d-fab.ch/fr/terms</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/terms" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en/terms" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de/terms" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr/terms" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it/terms" />
<changefreq>yearly</changefreq>
<priority>0.4</priority>
</url>
</urlset>

View File

@@ -1,26 +1,48 @@
import {
ApplicationConfig,
provideAppInitializer,
provideZoneChangeDetection,
importProvidersFrom,
inject,
REQUEST,
} from '@angular/core';
import {
provideRouter,
withComponentInputBinding,
withInMemoryScrolling,
withViewTransitions,
Router,
} from '@angular/router';
import { routes } from './app.routes';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
import {
provideTranslateHttpLoader,
TranslateHttpLoader,
} from '@ngx-translate/http-loader';
TranslateLoader,
TranslateModule,
TranslateService,
} from '@ngx-translate/core';
import { adminAuthInterceptor } from './core/interceptors/admin-auth.interceptor';
import {
provideClientHydration,
withEventReplay,
} from '@angular/platform-browser';
import { serverOriginInterceptor } from './core/interceptors/server-origin.interceptor';
import { catchError, firstValueFrom, of } from 'rxjs';
import { StaticTranslateLoader } from './core/i18n/static-translate.loader';
type SupportedLang = 'it' | 'en' | 'de' | 'fr';
const SUPPORTED_LANGS: readonly SupportedLang[] = ['it', 'en', 'de', 'fr'];
function resolveLangFromUrl(url: string): SupportedLang {
const firstSegment = (url || '/')
.split('?')[0]
.split('#')[0]
.split('/')
.filter(Boolean)[0]
?.toLowerCase();
return SUPPORTED_LANGS.includes(firstSegment as SupportedLang)
? (firstSegment as SupportedLang)
: 'it';
}
export const appConfig: ApplicationConfig = {
providers: [
@@ -33,20 +55,44 @@ export const appConfig: ApplicationConfig = {
scrollPositionRestoration: 'top',
}),
),
provideHttpClient(withInterceptors([adminAuthInterceptor])),
provideTranslateHttpLoader({
prefix: './assets/i18n/',
suffix: '.json',
}),
provideHttpClient(
withInterceptors([serverOriginInterceptor, adminAuthInterceptor]),
),
importProvidersFrom(
TranslateModule.forRoot({
defaultLanguage: 'it',
loader: {
provide: TranslateLoader,
useClass: TranslateHttpLoader,
useClass: StaticTranslateLoader,
},
}),
),
provideAppInitializer(() => {
const translate = inject(TranslateService);
const router = inject(Router);
const request = inject(REQUEST, { optional: true }) as {
url?: string;
} | null;
translate.addLangs([...SUPPORTED_LANGS]);
translate.setDefaultLang('it');
const requestedUrl =
(typeof request?.url === 'string' && request.url) || router.url || '/';
const lang = resolveLangFromUrl(requestedUrl);
return firstValueFrom(
translate.use(lang).pipe(
catchError((error) => {
console.error('[i18n] Failed to preload language for SSR', {
lang,
requestedUrl,
error,
});
return of({});
}),
),
).then(() => undefined);
}),
provideClientHydration(withEventReplay()),
],
};

View File

@@ -0,0 +1,22 @@
import { Injectable } from '@angular/core';
import { TranslateLoader, TranslationObject } from '@ngx-translate/core';
import { Observable, of } from 'rxjs';
import de from '../../../assets/i18n/de.json';
import en from '../../../assets/i18n/en.json';
import fr from '../../../assets/i18n/fr.json';
import it from '../../../assets/i18n/it.json';
const TRANSLATIONS: Record<string, TranslationObject> = {
it: it as TranslationObject,
en: en as TranslationObject,
de: de as TranslationObject,
fr: fr as TranslationObject,
};
@Injectable()
export class StaticTranslateLoader implements TranslateLoader {
getTranslation(lang: string): Observable<TranslationObject> {
const normalized = String(lang || 'it').toLowerCase();
return of(TRANSLATIONS[normalized] ?? TRANSLATIONS['it']);
}
}

View File

@@ -0,0 +1,63 @@
import { HttpInterceptorFn } from '@angular/common/http';
import { inject, REQUEST } from '@angular/core';
type RequestLike = {
protocol?: string;
get?: (name: string) => string | undefined;
headers?: Record<string, string | string[] | undefined>;
};
function isAbsoluteUrl(url: string): boolean {
return /^[a-z][a-z\d+\-.]*:/i.test(url) || url.startsWith('//');
}
function firstHeaderValue(value: string | string[] | undefined): string | null {
if (Array.isArray(value)) {
return value[0] ?? null;
}
return typeof value === 'string' ? value : null;
}
function resolveOrigin(request: RequestLike | null): string | null {
if (!request) {
return null;
}
const host =
request.get?.('host') ??
firstHeaderValue(request.headers?.['host']) ??
firstHeaderValue(request.headers?.['x-forwarded-host']);
if (!host) {
return null;
}
const forwardedProtoRaw = firstHeaderValue(
request.headers?.['x-forwarded-proto'],
);
const forwardedProto = forwardedProtoRaw
?.split(',')
.map((part) => part.trim().toLowerCase())
.find(Boolean);
const protocol = forwardedProto || request.protocol || 'http';
return `${protocol}://${host}`;
}
function normalizeRelativePath(url: string): string {
const withoutDot = url.replace(/^\.\//, '');
return withoutDot.startsWith('/') ? withoutDot : `/${withoutDot}`;
}
export const serverOriginInterceptor: HttpInterceptorFn = (req, next) => {
if (isAbsoluteUrl(req.url)) {
return next(req);
}
const request = inject(REQUEST, { optional: true }) as RequestLike | null;
const origin = resolveOrigin(request);
if (!origin) {
return next(req);
}
const absoluteUrl = `${origin}${normalizeRelativePath(req.url)}`;
return next(req.clone({ url: absoluteUrl }));
};

View File

@@ -33,6 +33,12 @@
<a routerLink="/about" routerLinkActive="active" (click)="closeMenu()">{{
"NAV.ABOUT" | translate
}}</a>
<a
routerLink="/materials"
routerLinkActive="active"
(click)="closeMenu()"
>{{ "NAV.MATERIALS" | translate }}</a
>
<a
routerLink="/contact"
routerLinkActive="active"

View File

@@ -0,0 +1,397 @@
<main class="materials-page">
<section class="hero">
<div class="container hero-inner">
<p class="ui-eyebrow">Guida materiali</p>
<h1>Qualita e Materiali</h1>
<p class="hero-lead">
Confronta materiali in modo interattivo con radar chart, metriche tecniche,
vantaggi, limiti e fonti verificabili.
</p>
<p class="hero-note">
Seleziona fino a {{ maxCompareCount }} materiali: il grafico aggiorna i
punteggi in tempo reale.
</p>
</div>
</section>
<section class="selector-section">
<div class="container">
<h2>Selezione confronto</h2>
<div class="selector-grid" role="group" aria-label="Selezione materiali">
@for (material of materials; track trackMaterial($index, material)) {
<button
type="button"
class="selector-chip"
[class.is-selected]="isSelected(material.id)"
[disabled]="!canSelect(material.id)"
(click)="toggleMaterial(material.id)"
>
<span
class="selector-dot"
[style.background-color]="legendDotColor(material.id)"
></span>
<span>{{ material.name }}</span>
</button>
}
</div>
<p class="selector-help">
Nota: per l asse Economicita, un valore alto significa costo al kg piu
conveniente.
</p>
</div>
</section>
<section class="chart-section">
<div class="container chart-layout">
<article class="chart-card">
<header class="chart-header">
<h2>Radar chart comparativo</h2>
<p>
Punteggi normalizzati 0-100 su tutto il set materiali (min-max scaling).
</p>
</header>
<svg
class="radar-chart"
[attr.viewBox]="'0 0 ' + chartSize + ' ' + chartSize"
role="img"
aria-label="Radar chart materiali"
>
<g class="chart-rings">
@for (ring of ringPolygons(); track $index) {
<polygon [attr.points]="ring"></polygon>
}
</g>
<g class="chart-axes">
@for (axis of axisGuides(); track axis.id) {
<line
[attr.x1]="axis.fromX"
[attr.y1]="axis.fromY"
[attr.x2]="axis.x"
[attr.y2]="axis.y"
></line>
<text
[attr.x]="axis.labelX"
[attr.y]="axis.labelY"
[attr.text-anchor]="axis.labelAnchor"
>
{{ radarAxes[$index].label }}
</text>
}
</g>
<g class="chart-series">
@for (series of radarSeries(); track series.material.id) {
<polygon
class="series-shape"
[attr.points]="series.points"
[style.stroke]="series.color"
[style.fill]="series.fill"
[class.is-hovered]="hoveredMaterialId() === series.material.id"
(mouseenter)="setHoveredMaterial(series.material.id)"
(mouseleave)="setHoveredMaterial(null)"
></polygon>
@for (point of series.values; track point.axis.id) {
<circle
class="series-node"
[attr.cx]="point.x"
[attr.cy]="point.y"
r="4"
[style.fill]="series.color"
></circle>
}
}
</g>
</svg>
<div class="chart-legend">
@for (series of radarSeries(); track series.material.id) {
<button
type="button"
class="legend-item"
(mouseenter)="setHoveredMaterial(series.material.id)"
(mouseleave)="setHoveredMaterial(null)"
>
<span
class="legend-dot"
[style.background-color]="series.color"
></span>
<span>{{ series.material.name }}</span>
</button>
}
</div>
</article>
<article class="explain-card">
<h3>Spiegazione completa del radar</h3>
<p>
Ogni asse mostra una proprieta tecnica. Il valore 100 rappresenta la
miglior performance relativa nel dataset attuale; 0 la meno favorevole.
</p>
<ul>
@for (axis of radarAxes; track axis.id) {
<li>
<strong>{{ axis.label }}:</strong>
{{ axis.description }}
</li>
}
</ul>
<p>
La normalizzazione e calcolata su tutti i materiali mostrati in pagina.
Per leggibilita il radar usa un raggio minimo visivo: i valori minimi
restano i meno favorevoli, ma non collassano tutti nello stesso punto.
</p>
</article>
</div>
</section>
<section class="table-section">
<div class="container">
<h2>Tabella tecnica di confronto</h2>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Parametro</th>
@for (material of selectedMaterials(); track trackMaterial($index, material)) {
<th>{{ material.name }}</th>
}
</tr>
</thead>
<tbody>
@for (row of comparisonRows(); track row.label) {
<tr>
<th>{{ row.label }}</th>
@for (value of row.values; track $index) {
<td>{{ value }}</td>
}
</tr>
}
</tbody>
</table>
</div>
</div>
</section>
<section class="quality-section">
<div class="container">
<h2>Layer, ugello e infill: esempi pratici</h2>
<p class="quality-intro">
Questa sezione non e un calcolatore interattivo: spiega visivamente cosa
cambia su oggetti reali e come leggere i risultati del vostro calcolatore.
</p>
<div class="visual-guide-grid">
@for (guide of qualityVisualCards(); track trackVisualGuide($index, guide)) {
<article class="visual-guide-card">
<p class="visual-guide-category">{{ guide.category }}</p>
<h3>{{ guide.title }}</h3>
<div class="visual-guide-media">
@if (guide.image; as image) {
<picture>
@if (image.source.avifUrl) {
<source [srcset]="image.source.avifUrl" type="image/avif" />
}
@if (image.source.webpUrl) {
<source [srcset]="image.source.webpUrl" type="image/webp" />
}
<img
[src]="image.source.fallbackUrl"
[attr.alt]="image.altText || guide.title"
width="640"
height="420"
/>
</picture>
} @else {
<div class="media-fallback">
<span>
Nessuna immagine assegnata.
Carica asset backend con usageType
<code>MATERIALS_PAGE</code> e usageKey
<code>{{ guide.usageKey }}</code>.
</span>
</div>
}
</div>
<p><strong>Oggetto esempio:</strong> {{ guide.objectExample }}</p>
<p><strong>Meglio per:</strong> {{ guide.bestFor }}</p>
<p><strong>Limite:</strong> {{ guide.tradeoff }}</p>
<p class="visual-guide-calc">
<strong>Lettura nel calcolatore:</strong> {{ guide.calculatorRead }}
</p>
</article>
}
</div>
<article class="calculator-logic-card">
<h3>Come leggere il nostro calcolatore</h3>
<p>
Il calcolatore non sostituisce i profili slicer: serve a spiegare il
compromesso tra estetica, robustezza e tempi in modo coerente.
</p>
<div class="logic-table-wrap">
<table>
<thead>
<tr>
<th>Metrica</th>
<th>Cosa significa</th>
<th>Valore alto</th>
<th>Valore basso</th>
</tr>
</thead>
<tbody>
@for (rule of calculatorRules; track rule.metric) {
<tr>
<th>{{ rule.metric }}</th>
<td>{{ rule.whatItMeans }}</td>
<td>{{ rule.whenHigh }}</td>
<td>{{ rule.whenLow }}</td>
</tr>
}
</tbody>
</table>
</div>
</article>
<div class="quality-layout">
<article class="quality-card">
<h3>Regole rapide per l utente</h3>
<ul>
<li>
Layer basso e ugello piccolo migliorano i dettagli, ma aumentano i
tempi.
</li>
<li>
Infill e perimetri alti migliorano resistenza, ma aumentano tempo e
materiale.
</li>
<li>
Per pezzi estetici usa profili fini; per pezzi funzionali scegli setup
bilanciati o robusti.
</li>
</ul>
</article>
</div>
<div class="guides-grid">
@for (guide of qualityGuides; track trackGuide($index, guide)) {
<article class="guide-card">
<h3>{{ guide.title }}</h3>
<p><strong>Range consigliato:</strong> {{ guide.recommendation }}</p>
<p>{{ guide.explanation }}</p>
<p class="guide-effect">{{ guide.practicalEffect }}</p>
</article>
}
</div>
</div>
</section>
<section class="materials-section">
<div class="container">
<h2>Schede materiali: spiegazioni, pro/contro, fonti</h2>
<div class="materials-grid">
@for (card of selectedCards(); track card.material.id) {
<article class="material-card">
<header>
<h3>{{ card.material.name }}</h3>
<p>{{ card.material.summary }}</p>
</header>
<div class="material-media">
@if (card.image; as image) {
<picture>
@if (image.source.avifUrl) {
<source [srcset]="image.source.avifUrl" type="image/avif" />
}
@if (image.source.webpUrl) {
<source [srcset]="image.source.webpUrl" type="image/webp" />
}
<img
[src]="image.source.fallbackUrl"
[attr.alt]="image.altText || card.material.name"
width="640"
height="400"
/>
</picture>
} @else {
<div class="media-fallback">
<span>
Nessuna immagine assegnata.
Carica asset backend con usageType
<code>MATERIALS_PAGE</code> e usageKey
<code>material-{{ card.material.id }}</code>.
</span>
</div>
}
</div>
<div class="material-columns">
<div>
<h4>Vantaggi</h4>
<ul>
@for (item of card.material.pros; track $index) {
<li>{{ item }}</li>
}
</ul>
</div>
<div>
<h4>Limiti</h4>
<ul>
@for (item of card.material.cons; track $index) {
<li>{{ item }}</li>
}
</ul>
</div>
<div>
<h4>Ideale per</h4>
<ul>
@for (item of card.material.idealFor; track $index) {
<li>{{ item }}</li>
}
</ul>
</div>
</div>
<div class="source-list">
<h4>Fonti citate</h4>
<ul>
@for (source of card.material.sources; track trackSource($index, source)) {
<li>
<a [href]="source.url" target="_blank" rel="noopener noreferrer">
{{ source.label }}
</a>
<span class="source-kind">{{ source.kind }}</span>
</li>
}
</ul>
</div>
</article>
}
</div>
</div>
</section>
<section class="global-sources">
<div class="container">
<h2>Indice completo fonti</h2>
<p>
Tutti i link usati per metriche e descrizioni sono riportati qui in forma
centralizzata.
</p>
<ul class="global-source-list">
@for (source of allSources(); track trackSource($index, source)) {
<li>
<a [href]="source.url" target="_blank" rel="noopener noreferrer">
{{ source.label }}
</a>
<span>{{ source.kind }}</span>
</li>
}
</ul>
</div>
</section>
</main>

View File

@@ -0,0 +1,546 @@
.materials-page {
--materials-bg: #ffffff;
--materials-accent: #c23b22;
--materials-muted: #5f6771;
--materials-card: #ffffff;
background: var(--materials-bg);
color: var(--color-text-main);
}
.hero {
padding: 5rem 0 2.25rem;
position: relative;
overflow: hidden;
}
.hero::before {
content: '';
position: absolute;
inset: -40% -15% auto auto;
width: 420px;
height: 420px;
background: radial-gradient(circle, rgba(194, 59, 34, 0.08), transparent 70%);
pointer-events: none;
}
.hero-inner {
position: relative;
z-index: 1;
}
.hero h1 {
margin: 0.4rem 0 1rem;
font-size: clamp(2rem, 4vw, 3rem);
line-height: 1.05;
}
.hero-lead {
margin: 0;
max-width: 68ch;
font-size: 1.05rem;
color: var(--color-text-main);
}
.hero-note {
margin: 0.9rem 0 0;
color: var(--materials-muted);
}
.selector-section,
.chart-section,
.table-section,
.quality-section,
.materials-section,
.global-sources {
padding: 1.8rem 0;
}
.selector-grid {
margin-top: 1rem;
display: flex;
flex-wrap: wrap;
gap: 0.6rem;
}
.selector-chip {
border: 1px solid var(--color-border);
background: #fff;
color: var(--color-text-main);
border-radius: 999px;
padding: 0.5rem 0.9rem;
display: inline-flex;
align-items: center;
gap: 0.5rem;
font-weight: 600;
cursor: pointer;
transition:
transform 0.2s ease,
border-color 0.2s ease,
box-shadow 0.2s ease,
background-color 0.2s ease;
}
.selector-chip:hover:enabled {
transform: translateY(-1px);
border-color: var(--materials-accent);
box-shadow: 0 4px 12px rgb(16 24 32 / 0.12);
}
.selector-chip:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.selector-chip.is-selected {
border-color: var(--materials-accent);
background: #fff3ee;
}
.selector-dot {
width: 0.7rem;
height: 0.7rem;
border-radius: 50%;
border: 1px solid rgb(0 0 0 / 0.15);
display: inline-block;
}
.selector-help {
margin-top: 0.8rem;
color: var(--materials-muted);
}
.chart-layout {
display: grid;
gap: 1rem;
grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
align-items: start;
}
.chart-card,
.explain-card,
.material-card,
.table-wrap,
.quality-card,
.guide-card {
background: var(--materials-card);
border: 1px solid var(--color-border);
border-radius: 1rem;
box-shadow: 0 10px 28px rgb(16 24 32 / 0.05);
}
.chart-card {
padding: 1rem;
}
.chart-header h2 {
margin: 0;
}
.chart-header p {
margin: 0.5rem 0 0;
color: var(--materials-muted);
}
.radar-chart {
width: 100%;
max-width: 520px;
margin: 0 auto;
display: block;
}
.chart-rings polygon {
fill: none;
stroke: #d7d9de;
stroke-width: 1;
}
.chart-axes line {
stroke: #c3c8cf;
stroke-width: 1;
}
.chart-axes text {
font-size: 0.75rem;
fill: #4f5a66;
font-weight: 600;
}
.series-shape {
stroke-width: 2.2;
transition: filter 0.2s ease;
}
.series-shape.is-hovered {
filter: drop-shadow(0 3px 8px rgb(16 24 32 / 0.26));
}
.series-node {
stroke: #ffffff;
stroke-width: 1.2;
}
.chart-legend {
margin-top: 0.75rem;
display: flex;
flex-wrap: wrap;
gap: 0.6rem;
}
.legend-item {
border: 1px solid var(--color-border);
border-radius: 999px;
padding: 0.35rem 0.7rem;
background: #fff;
display: inline-flex;
gap: 0.45rem;
align-items: center;
font-weight: 600;
cursor: default;
}
.legend-dot {
width: 0.65rem;
height: 0.65rem;
border-radius: 50%;
display: inline-block;
}
.explain-card {
padding: 1rem;
}
.explain-card h3 {
margin: 0;
}
.explain-card p {
margin: 0.75rem 0;
color: var(--materials-muted);
}
.explain-card ul {
margin: 0;
padding-left: 1.1rem;
display: grid;
gap: 0.45rem;
}
.table-wrap {
overflow-x: auto;
}
.table-wrap table {
width: 100%;
border-collapse: collapse;
min-width: 760px;
}
.table-wrap th,
.table-wrap td {
padding: 0.6rem 0.75rem;
border-bottom: 1px solid var(--color-border);
text-align: left;
}
.table-wrap thead th {
background: #f8fafd;
font-size: 0.84rem;
letter-spacing: 0.01em;
}
.table-wrap tbody tr:hover {
background: #f8fbff;
}
.quality-intro {
margin: 0.4rem 0 0;
color: var(--materials-muted);
max-width: 72ch;
}
.visual-guide-grid {
margin-top: 1rem;
display: grid;
gap: 1rem;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
}
.visual-guide-card {
background: #fff;
border: 1px solid var(--color-border);
border-radius: 1rem;
box-shadow: 0 10px 28px rgb(16 24 32 / 0.05);
padding: 0.85rem;
display: grid;
gap: 0.55rem;
}
.visual-guide-category {
margin: 0;
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
color: #2563b8;
}
.visual-guide-card h3 {
margin: 0;
font-size: 1.02rem;
}
.visual-guide-media {
border: 1px solid var(--color-border);
border-radius: 0.75rem;
overflow: hidden;
background: #f7f8fb;
min-height: 170px;
}
.visual-guide-media img {
width: 100%;
height: 185px;
object-fit: cover;
display: block;
}
.visual-guide-card p {
margin: 0;
color: var(--materials-muted);
font-size: 0.92rem;
line-height: 1.42;
}
.visual-guide-calc {
margin-top: 0.2rem;
color: var(--color-text-main);
}
.calculator-logic-card {
margin-top: 1rem;
background: #fff;
border: 1px solid var(--color-border);
border-radius: 1rem;
box-shadow: 0 10px 28px rgb(16 24 32 / 0.05);
padding: 1rem;
}
.calculator-logic-card h3 {
margin: 0;
}
.calculator-logic-card p {
margin: 0.6rem 0 0;
color: var(--materials-muted);
}
.logic-table-wrap {
margin-top: 0.75rem;
overflow-x: auto;
}
.logic-table-wrap table {
width: 100%;
border-collapse: collapse;
min-width: 720px;
}
.logic-table-wrap th,
.logic-table-wrap td {
border-bottom: 1px solid var(--color-border);
text-align: left;
padding: 0.62rem 0.7rem;
}
.logic-table-wrap thead th {
background: #f8fafd;
font-size: 0.84rem;
letter-spacing: 0.01em;
}
.quality-layout {
margin-top: 1rem;
display: grid;
gap: 1rem;
grid-template-columns: 1fr;
}
.quality-card {
padding: 1rem;
}
.quality-card h3 {
margin: 0;
}
.quality-card ul {
margin: 0.7rem 0 0;
padding-left: 1rem;
display: grid;
gap: 0.45rem;
color: var(--materials-muted);
}
.guides-grid {
margin-top: 1rem;
display: grid;
gap: 0.8rem;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
}
.guide-card {
padding: 0.9rem;
}
.guide-card h3 {
margin: 0;
font-size: 1rem;
}
.guide-card p {
margin: 0.55rem 0 0;
color: var(--materials-muted);
}
.guide-effect {
color: var(--color-text-main);
font-weight: 500;
}
.materials-grid {
display: grid;
gap: 1rem;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
}
.material-card {
padding: 1rem;
}
.material-card h3 {
margin: 0;
font-size: 1.25rem;
}
.material-card header p {
margin: 0.55rem 0 0;
color: var(--materials-muted);
}
.material-media {
margin-top: 0.85rem;
border-radius: 0.75rem;
overflow: hidden;
border: 1px solid var(--color-border);
background: #f7f8fb;
min-height: 180px;
}
.material-media img {
width: 100%;
height: 220px;
object-fit: cover;
display: block;
}
.media-fallback {
min-height: 180px;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
color: var(--materials-muted);
text-align: center;
font-size: 0.9rem;
line-height: 1.45;
}
.material-columns {
margin-top: 0.9rem;
display: grid;
gap: 0.7rem;
}
.material-columns h4,
.source-list h4 {
margin: 0;
font-size: 0.95rem;
}
.material-columns ul,
.source-list ul,
.global-source-list {
margin: 0.45rem 0 0;
padding-left: 1rem;
display: grid;
gap: 0.35rem;
}
.source-list {
margin-top: 0.9rem;
padding-top: 0.9rem;
border-top: 1px solid var(--color-border);
}
.source-list li,
.global-source-list li {
display: flex;
gap: 0.5rem;
align-items: baseline;
justify-content: space-between;
}
.source-list a,
.global-source-list a {
color: #14409b;
word-break: break-word;
}
.source-kind {
color: var(--materials-muted);
font-size: 0.8rem;
white-space: nowrap;
}
.global-sources p {
color: var(--materials-muted);
}
.global-source-list {
background: #fff;
border: 1px solid var(--color-border);
border-radius: 0.9rem;
padding: 1rem 1rem 1rem 1.35rem;
}
@media (max-width: 1024px) {
.chart-layout {
grid-template-columns: 1fr;
}
.quality-layout {
grid-template-columns: 1fr;
}
}
@media (max-width: 640px) {
.hero {
padding-top: 4.2rem;
}
.chart-card,
.explain-card,
.material-card {
padding: 0.85rem;
}
.table-wrap table {
min-width: 640px;
}
.source-list li,
.global-source-list li {
flex-direction: column;
align-items: flex-start;
}
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 433 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 345 KiB

View File

@@ -19,6 +19,7 @@
href="https://fonts.googleapis.com/icon?family=Material+Icons"
rel="stylesheet"
/>
<meta name="msvalidate.01" content="5AF60C1471E1800B39DF4DBC3C709035" />
</head>
<body>
<app-root></app-root>

View File

@@ -14,6 +14,7 @@
"esModuleInterop": true,
"experimentalDecorators": true,
"moduleResolution": "bundler",
"resolveJsonModule": true,
"importHelpers": true,
"target": "ES2022",
"module": "ES2022"