feat(front-end): new logo edited
Some checks failed
PR Checks / prettier-autofix (pull_request) Successful in 22s
PR Checks / security-sast (pull_request) Successful in 35s
PR Checks / test-backend (pull_request) Failing after 34s
PR Checks / test-frontend (pull_request) Successful in 1m1s

This commit is contained in:
2026-03-20 10:36:50 +01:00
parent 7e8c89ce45
commit e575021f53
18 changed files with 939 additions and 343 deletions

View File

@@ -2,6 +2,7 @@ package com.printcalculator.dto;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import java.util.UUID;
public record ShopProductDetailDto(
@@ -25,6 +26,8 @@ public record ShopProductDetailDto(
List<ShopProductVariantOptionDto> variants,
PublicMediaUsageDto primaryImage,
List<PublicMediaUsageDto> images,
ShopProductModelDto model3d
ShopProductModelDto model3d,
String publicPath,
Map<String, String> localizedPaths
) {
}

View File

@@ -1,6 +1,7 @@
package com.printcalculator.dto;
import java.math.BigDecimal;
import java.util.Map;
import java.util.UUID;
public record ShopProductSummaryDto(
@@ -15,6 +16,8 @@ public record ShopProductSummaryDto(
BigDecimal priceToChf,
ShopProductVariantOptionDto defaultVariant,
PublicMediaUsageDto primaryImage,
ShopProductModelDto model3d
ShopProductModelDto model3d,
String publicPath,
Map<String, String> localizedPaths
) {
}

View File

@@ -399,6 +399,7 @@ public class PublicShopCatalogService {
Map<String, String> variantColorHexByMaterialAndColor,
String language) {
List<PublicMediaUsageDto> images = productMediaBySlug.getOrDefault(productMediaUsageKey(entry.product()), List.of());
Map<String, String> localizedPaths = ShopPublicPathSupport.buildLocalizedProductPaths(entry.product());
return new ShopProductSummaryDto(
entry.product().getId(),
entry.product().getSlug(),
@@ -415,7 +416,9 @@ public class PublicShopCatalogService {
resolvePriceTo(entry.variants()),
toVariantDto(entry.defaultVariant(), entry.defaultVariant(), variantColorHexByMaterialAndColor, language),
selectPrimaryMedia(images),
toProductModelDto(entry)
toProductModelDto(entry),
localizedPaths.getOrDefault(normalizeLanguage(language), localizedPaths.get("it")),
localizedPaths
);
}
@@ -426,6 +429,7 @@ public class PublicShopCatalogService {
List<PublicMediaUsageDto> images = productMediaBySlug.getOrDefault(productMediaUsageKey(entry.product()), List.of());
String localizedSeoTitle = entry.product().getSeoTitleForLanguage(language);
String localizedSeoDescription = entry.product().getSeoDescriptionForLanguage(language);
Map<String, String> localizedPaths = ShopPublicPathSupport.buildLocalizedProductPaths(entry.product());
return new ShopProductDetailDto(
entry.product().getId(),
entry.product().getSlug(),
@@ -453,7 +457,9 @@ public class PublicShopCatalogService {
.toList(),
selectPrimaryMedia(images),
images,
toProductModelDto(entry)
toProductModelDto(entry),
localizedPaths.getOrDefault(normalizeLanguage(language), localizedPaths.get("it")),
localizedPaths
);
}
@@ -514,6 +520,22 @@ public class PublicShopCatalogService {
return raw;
}
private String normalizeLanguage(String language) {
String normalized = trimToNull(language);
if (normalized == null) {
return "it";
}
normalized = normalized.toLowerCase(Locale.ROOT);
int separatorIndex = normalized.indexOf('-');
if (separatorIndex > 0) {
normalized = normalized.substring(0, separatorIndex);
}
return switch (normalized) {
case "en", "de", "fr" -> normalized;
default -> "it";
};
}
private ShopProductModelDto toProductModelDto(ProductEntry entry) {
if (entry.modelAsset() == null) {
return null;

View File

@@ -0,0 +1,66 @@
package com.printcalculator.service.shop;
import com.printcalculator.entity.ShopProduct;
import java.text.Normalizer;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
final class ShopPublicPathSupport {
private static final String PRODUCT_ROUTE_PREFIX = "/shop/p/";
private ShopPublicPathSupport() {
}
static String buildProductPathSegment(ShopProduct product, String language) {
String localizedName = product.getNameForLanguage(language);
String idPrefix = productIdPrefix(product.getId());
String tail = firstNonBlank(slugify(localizedName), slugify(product.getSlug()), "product");
return idPrefix.isBlank() ? tail : idPrefix + "-" + tail;
}
static Map<String, String> buildLocalizedProductPaths(ShopProduct product) {
Map<String, String> localizedPaths = new LinkedHashMap<>();
for (String language : ShopProduct.SUPPORTED_LANGUAGES) {
localizedPaths.put(language, "/" + language + PRODUCT_ROUTE_PREFIX + buildProductPathSegment(product, language));
}
return localizedPaths;
}
static String productIdPrefix(UUID productId) {
if (productId == null) {
return "";
}
String raw = productId.toString().trim().toLowerCase(Locale.ROOT);
int dashIndex = raw.indexOf('-');
if (dashIndex > 0) {
return raw.substring(0, dashIndex);
}
return raw.length() >= 8 ? raw.substring(0, 8) : raw;
}
static String slugify(String rawValue) {
String safeValue = rawValue == null ? "" : rawValue;
String normalized = Normalizer.normalize(safeValue, Normalizer.Form.NFD)
.replaceAll("\\p{M}+", "")
.toLowerCase(Locale.ROOT)
.replaceAll("[^a-z0-9]+", "-")
.replaceAll("^-+|-+$", "")
.replaceAll("-{2,}", "-");
return normalized;
}
private static String firstNonBlank(String... values) {
if (values == null) {
return null;
}
for (String value : values) {
if (value != null && !value.isBlank()) {
return value;
}
}
return null;
}
}

View File

@@ -11,7 +11,6 @@ import org.springframework.transaction.annotation.Transactional;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.text.Normalizer;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
@@ -19,7 +18,6 @@ import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
@@ -31,6 +29,12 @@ public class ShopSitemapService {
private static final List<String> SUPPORTED_LANGUAGES = ShopProduct.SUPPORTED_LANGUAGES;
private static final String DEFAULT_LANGUAGE = "it";
private static final DateTimeFormatter LASTMOD_FORMATTER = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
private static final Map<String, String> HREFLANG_BY_LANGUAGE = Map.of(
"it", "it-CH",
"en", "en-CH",
"de", "de-CH",
"fr", "fr-CH"
);
private final ShopCategoryRepository shopCategoryRepository;
private final ShopProductRepository shopProductRepository;
@@ -130,7 +134,7 @@ public class ShopSitemapService {
Map<String, String> hrefByLanguage = new LinkedHashMap<>();
for (String language : SUPPORTED_LANGUAGES) {
String publicSegment = localizedProductPathSegment(product, language);
String publicSegment = ShopPublicPathSupport.buildProductPathSegment(product, language);
hrefByLanguage.put(language, frontendBaseUrl + "/" + language + "/shop/p/" + pathEncodeSegment(publicSegment));
}
@@ -169,7 +173,7 @@ public class ShopSitemapService {
continue;
}
xml.append(" <xhtml:link rel=\"alternate\" hreflang=\"")
.append(language)
.append(HREFLANG_BY_LANGUAGE.getOrDefault(language, language))
.append("\" href=\"")
.append(xmlEscape(href))
.append("\" />\n");
@@ -186,48 +190,6 @@ public class ShopSitemapService {
xml.append(" </url>\n");
}
private String localizedProductPathSegment(ShopProduct product, String language) {
String localizedName = product.getNameForLanguage(language);
String idPrefix = productIdPrefix(product.getId());
String tail = firstNonBlank(slugify(localizedName), slugify(product.getSlug()), "product");
return idPrefix.isBlank() ? tail : idPrefix + "-" + tail;
}
private String productIdPrefix(UUID productId) {
if (productId == null) {
return "";
}
String raw = productId.toString().trim().toLowerCase(Locale.ROOT);
int dashIndex = raw.indexOf('-');
if (dashIndex > 0) {
return raw.substring(0, dashIndex);
}
return raw.length() >= 8 ? raw.substring(0, 8) : raw;
}
static String slugify(String rawValue) {
String safeValue = rawValue == null ? "" : rawValue;
String normalized = Normalizer.normalize(safeValue, Normalizer.Form.NFD)
.replaceAll("\\p{M}+", "")
.toLowerCase(Locale.ROOT)
.replaceAll("[^a-z0-9]+", "-")
.replaceAll("^-+|-+$", "")
.replaceAll("-{2,}", "-");
return normalized;
}
private String firstNonBlank(String... values) {
if (values == null) {
return null;
}
for (String value : values) {
if (value != null && !value.isBlank()) {
return value;
}
}
return null;
}
private String pathEncodeSegment(String rawSegment) {
String safeSegment = rawSegment == null ? "" : rawSegment;
return URLEncoder.encode(safeSegment, StandardCharsets.UTF_8).replace("+", "%20");