Compare commits
15 Commits
feat/ssr
...
9fc1fc97fa
| Author | SHA1 | Date | |
|---|---|---|---|
| 9fc1fc97fa | |||
| 7010a81596 | |||
| 96cfa91c67 | |||
| 669ace82c0 | |||
| 93163ae6e8 | |||
| af2d506da1 | |||
| 637541994a | |||
|
|
63cd4c4f5e | ||
| fd4104da39 | |||
| 5bb23fbcfa | |||
| 6a22c54e9f | |||
| 3ac3262e77 | |||
| 18ecc07240 | |||
| cb468492b3 | |||
| feee2b0bff |
@@ -146,8 +146,22 @@ public class ShopSitemapService {
|
|||||||
return;
|
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(" <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) {
|
for (String language : SUPPORTED_LANGUAGES) {
|
||||||
String href = hrefByLanguage.get(language);
|
String href = hrefByLanguage.get(language);
|
||||||
|
|||||||
@@ -89,10 +89,16 @@ class ShopSitemapServiceTest {
|
|||||||
String xml = service.getShopSitemapXml();
|
String xml = service.getShopSitemapXml();
|
||||||
|
|
||||||
assertTrue(xml.contains("<loc>https://3d-fab.ch/it/shop/accessori</loc>"));
|
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\""));
|
assertTrue(xml.contains("hreflang=\"en\" href=\"https://3d-fab.ch/en/shop/accessori\""));
|
||||||
assertFalse(xml.contains("https://3d-fab.ch/it/shop/bozza"));
|
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/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=\"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=\"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\""));
|
assertTrue(xml.contains("hreflang=\"x-default\" href=\"https://3d-fab.ch/it/shop/p/123e4567-supporto-bici\""));
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ Disallow: /order
|
|||||||
Disallow: /order/
|
Disallow: /order/
|
||||||
Disallow: /*/order
|
Disallow: /*/order
|
||||||
Disallow: /*/order/
|
Disallow: /*/order/
|
||||||
Disallow: /co
|
Disallow: /co$
|
||||||
Disallow: /co/
|
Disallow: /co/
|
||||||
Disallow: /*/co
|
Disallow: /*/co$
|
||||||
Disallow: /*/co/
|
Disallow: /*/co/
|
||||||
Disallow: /checkout
|
Disallow: /checkout
|
||||||
Disallow: /checkout/
|
Disallow: /checkout/
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<urlset
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">
|
||||||
xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
|
|
||||||
xmlns:xhtml="http://www.w3.org/1999/xhtml"
|
|
||||||
>
|
|
||||||
<url>
|
<url>
|
||||||
<loc>https://3d-fab.ch/it</loc>
|
<loc>https://3d-fab.ch/it</loc>
|
||||||
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it" />
|
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it" />
|
||||||
@@ -13,66 +10,119 @@
|
|||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>1.0</priority>
|
<priority>1.0</priority>
|
||||||
</url>
|
</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>
|
<url>
|
||||||
<loc>https://3d-fab.ch/it/calculator/basic</loc>
|
<loc>https://3d-fab.ch/it/calculator/basic</loc>
|
||||||
<xhtml:link
|
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/calculator/basic" />
|
||||||
rel="alternate"
|
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en/calculator/basic" />
|
||||||
hreflang="it"
|
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de/calculator/basic" />
|
||||||
href="https://3d-fab.ch/it/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="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>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.9</priority>
|
<priority>0.9</priority>
|
||||||
</url>
|
</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>
|
<url>
|
||||||
<loc>https://3d-fab.ch/it/calculator/advanced</loc>
|
<loc>https://3d-fab.ch/it/calculator/advanced</loc>
|
||||||
<xhtml:link
|
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/calculator/advanced" />
|
||||||
rel="alternate"
|
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en/calculator/advanced" />
|
||||||
hreflang="it"
|
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de/calculator/advanced" />
|
||||||
href="https://3d-fab.ch/it/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="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>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.8</priority>
|
<priority>0.8</priority>
|
||||||
</url>
|
</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>
|
<url>
|
||||||
<loc>https://3d-fab.ch/it/shop</loc>
|
<loc>https://3d-fab.ch/it/shop</loc>
|
||||||
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/shop" />
|
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/shop" />
|
||||||
@@ -83,64 +133,160 @@
|
|||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.8</priority>
|
<priority>0.8</priority>
|
||||||
</url>
|
</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>
|
<url>
|
||||||
<loc>https://3d-fab.ch/it/about</loc>
|
<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="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="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="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="fr" href="https://3d-fab.ch/fr/about" />
|
||||||
<xhtml:link
|
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it/about" />
|
||||||
rel="alternate"
|
|
||||||
hreflang="x-default"
|
|
||||||
href="https://3d-fab.ch/it/about"
|
|
||||||
/>
|
|
||||||
<changefreq>monthly</changefreq>
|
<changefreq>monthly</changefreq>
|
||||||
<priority>0.7</priority>
|
<priority>0.7</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://3d-fab.ch/it/contact</loc>
|
<loc>https://3d-fab.ch/en/about</loc>
|
||||||
<xhtml:link
|
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/about" />
|
||||||
rel="alternate"
|
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en/about" />
|
||||||
hreflang="it"
|
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de/about" />
|
||||||
href="https://3d-fab.ch/it/contact"
|
<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="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>
|
<changefreq>monthly</changefreq>
|
||||||
<priority>0.7</priority>
|
<priority>0.7</priority>
|
||||||
</url>
|
</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>
|
<url>
|
||||||
<loc>https://3d-fab.ch/it/privacy</loc>
|
<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="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="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="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="fr" href="https://3d-fab.ch/fr/privacy" />
|
||||||
<xhtml:link
|
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it/privacy" />
|
||||||
rel="alternate"
|
|
||||||
hreflang="x-default"
|
|
||||||
href="https://3d-fab.ch/it/privacy"
|
|
||||||
/>
|
|
||||||
<changefreq>yearly</changefreq>
|
<changefreq>yearly</changefreq>
|
||||||
<priority>0.4</priority>
|
<priority>0.4</priority>
|
||||||
</url>
|
</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>
|
<url>
|
||||||
<loc>https://3d-fab.ch/it/terms</loc>
|
<loc>https://3d-fab.ch/it/terms</loc>
|
||||||
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/terms" />
|
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/terms" />
|
||||||
@@ -151,4 +297,34 @@
|
|||||||
<changefreq>yearly</changefreq>
|
<changefreq>yearly</changefreq>
|
||||||
<priority>0.4</priority>
|
<priority>0.4</priority>
|
||||||
</url>
|
</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>
|
</urlset>
|
||||||
|
|||||||
@@ -1,26 +1,48 @@
|
|||||||
import {
|
import {
|
||||||
ApplicationConfig,
|
ApplicationConfig,
|
||||||
|
provideAppInitializer,
|
||||||
provideZoneChangeDetection,
|
provideZoneChangeDetection,
|
||||||
importProvidersFrom,
|
importProvidersFrom,
|
||||||
|
inject,
|
||||||
|
REQUEST,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import {
|
import {
|
||||||
provideRouter,
|
provideRouter,
|
||||||
withComponentInputBinding,
|
withComponentInputBinding,
|
||||||
withInMemoryScrolling,
|
withInMemoryScrolling,
|
||||||
withViewTransitions,
|
withViewTransitions,
|
||||||
|
Router,
|
||||||
} from '@angular/router';
|
} from '@angular/router';
|
||||||
import { routes } from './app.routes';
|
import { routes } from './app.routes';
|
||||||
import { provideHttpClient, withInterceptors } from '@angular/common/http';
|
import { provideHttpClient, withInterceptors } from '@angular/common/http';
|
||||||
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
|
|
||||||
import {
|
import {
|
||||||
provideTranslateHttpLoader,
|
TranslateLoader,
|
||||||
TranslateHttpLoader,
|
TranslateModule,
|
||||||
} from '@ngx-translate/http-loader';
|
TranslateService,
|
||||||
|
} from '@ngx-translate/core';
|
||||||
import { adminAuthInterceptor } from './core/interceptors/admin-auth.interceptor';
|
import { adminAuthInterceptor } from './core/interceptors/admin-auth.interceptor';
|
||||||
import {
|
import {
|
||||||
provideClientHydration,
|
provideClientHydration,
|
||||||
withEventReplay,
|
withEventReplay,
|
||||||
} from '@angular/platform-browser';
|
} 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 = {
|
export const appConfig: ApplicationConfig = {
|
||||||
providers: [
|
providers: [
|
||||||
@@ -33,20 +55,44 @@ export const appConfig: ApplicationConfig = {
|
|||||||
scrollPositionRestoration: 'top',
|
scrollPositionRestoration: 'top',
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
provideHttpClient(withInterceptors([adminAuthInterceptor])),
|
provideHttpClient(
|
||||||
provideTranslateHttpLoader({
|
withInterceptors([serverOriginInterceptor, adminAuthInterceptor]),
|
||||||
prefix: './assets/i18n/',
|
),
|
||||||
suffix: '.json',
|
|
||||||
}),
|
|
||||||
importProvidersFrom(
|
importProvidersFrom(
|
||||||
TranslateModule.forRoot({
|
TranslateModule.forRoot({
|
||||||
defaultLanguage: 'it',
|
defaultLanguage: 'it',
|
||||||
loader: {
|
loader: {
|
||||||
provide: TranslateLoader,
|
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()),
|
provideClientHydration(withEventReplay()),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
22
frontend/src/app/core/i18n/static-translate.loader.ts
Normal file
22
frontend/src/app/core/i18n/static-translate.loader.ts
Normal 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']);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 }));
|
||||||
|
};
|
||||||
@@ -33,6 +33,12 @@
|
|||||||
<a routerLink="/about" routerLinkActive="active" (click)="closeMenu()">{{
|
<a routerLink="/about" routerLinkActive="active" (click)="closeMenu()">{{
|
||||||
"NAV.ABOUT" | translate
|
"NAV.ABOUT" | translate
|
||||||
}}</a>
|
}}</a>
|
||||||
|
<a
|
||||||
|
routerLink="/materials"
|
||||||
|
routerLinkActive="active"
|
||||||
|
(click)="closeMenu()"
|
||||||
|
>{{ "NAV.MATERIALS" | translate }}</a
|
||||||
|
>
|
||||||
<a
|
<a
|
||||||
routerLink="/contact"
|
routerLink="/contact"
|
||||||
routerLinkActive="active"
|
routerLinkActive="active"
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
1018
frontend/src/app/features/materials/materials-page.component.ts
Normal file
1018
frontend/src/app/features/materials/materials-page.component.ts
Normal file
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 |
@@ -19,6 +19,7 @@
|
|||||||
href="https://fonts.googleapis.com/icon?family=Material+Icons"
|
href="https://fonts.googleapis.com/icon?family=Material+Icons"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
/>
|
/>
|
||||||
|
<meta name="msvalidate.01" content="5AF60C1471E1800B39DF4DBC3C709035" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<app-root></app-root>
|
<app-root></app-root>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
"importHelpers": true,
|
"importHelpers": true,
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"module": "ES2022"
|
"module": "ES2022"
|
||||||
|
|||||||
Reference in New Issue
Block a user