* feat: advanced trail categories * rename remote_category * fix merge issues * subcategories for plugins mapping * fix refresh * remove vertical trail filter category scrolling * cleanup * add confirm modal for disabling a category * Fix federation issues * fix review findings * redesign category settings page * remove prio badge * fix subcategory badge layout * optimize subcategory settings layout * further settings page layout optimization * fix subcategory icon position * fix * update docs --------- Co-authored-by: Christian Beutel <>
285 lines
7.8 KiB
Go
285 lines
7.8 KiB
Go
package util
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/pocketbase/pocketbase/core"
|
|
)
|
|
|
|
type defaultSubcategorySeed struct {
|
|
parentCategory string
|
|
name string
|
|
shortName string
|
|
badgeIcon string
|
|
translations map[string]CategoryTranslation
|
|
aliases []string
|
|
}
|
|
|
|
var defaultSubcategories = []defaultSubcategorySeed{
|
|
{parentCategory: "Biking", name: "MTB", shortName: "MTB", badgeIcon: "mountain"},
|
|
{parentCategory: "Biking", name: "Gravel", shortName: "GRVL"},
|
|
{
|
|
parentCategory: "Biking",
|
|
name: "Touring",
|
|
shortName: "TOUR",
|
|
aliases: []string{"Touring Bike", "City Bike"},
|
|
translations: subcategoryTranslations("Touring", "Tourenrad", "TOUR"),
|
|
},
|
|
{
|
|
parentCategory: "Biking",
|
|
name: "Road",
|
|
shortName: "ROAD",
|
|
badgeIcon: "grip-lines-vertical",
|
|
translations: subcategoryTranslations("Road", "Rennrad", "ROAD"),
|
|
},
|
|
{parentCategory: "Biking", name: "E-Bike", shortName: "EBIKE", badgeIcon: "bolt"},
|
|
{
|
|
parentCategory: "Hiking",
|
|
name: "Winter",
|
|
shortName: "WINT",
|
|
badgeIcon: "snowflake",
|
|
aliases: []string{"Winter Hiking"},
|
|
translations: map[string]CategoryTranslation{
|
|
"de": {Name: "Winterwandern", ShortName: "WINT"},
|
|
"en": {Name: "Winter", ShortName: "WINT"},
|
|
},
|
|
},
|
|
{
|
|
parentCategory: "Hiking",
|
|
name: "Alpine",
|
|
shortName: "ALP",
|
|
badgeIcon: "mountain",
|
|
aliases: []string{"Alpine Hiking"},
|
|
translations: subcategoryTranslations("Alpine", "Bergwandern", "ALP"),
|
|
},
|
|
{
|
|
parentCategory: "Hiking",
|
|
name: "Long-distance",
|
|
shortName: "LONG",
|
|
aliases: []string{"Long-distance Hiking"},
|
|
translations: subcategoryTranslations("Long-distance", "Fernwandern", "LONG"),
|
|
},
|
|
{
|
|
parentCategory: "Hiking",
|
|
name: "Snowshoeing",
|
|
shortName: "SNOW",
|
|
badgeIcon: "snowflake",
|
|
translations: subcategoryTranslations("Snowshoeing", "Schneeschuhwandern", "SNOW"),
|
|
},
|
|
{
|
|
parentCategory: "Hiking",
|
|
name: "Family",
|
|
shortName: "FAM",
|
|
badgeIcon: "child",
|
|
aliases: []string{"Family Hiking"},
|
|
translations: subcategoryTranslations("Family", "Familienwandern", "FAM"),
|
|
},
|
|
{
|
|
parentCategory: "Hiking",
|
|
name: "Pilgrimage",
|
|
shortName: "PILG",
|
|
badgeIcon: "cross",
|
|
translations: subcategoryTranslations("Pilgrimage", "Pilgern", "PILG"),
|
|
},
|
|
{
|
|
parentCategory: "Running",
|
|
name: "Trail",
|
|
shortName: "TRAIL",
|
|
aliases: []string{"Trail Running"},
|
|
translations: subcategoryTranslations("Trail", "Trailrunning", "TRAIL"),
|
|
},
|
|
{
|
|
parentCategory: "Running",
|
|
name: "Road",
|
|
shortName: "ROAD",
|
|
badgeIcon: "grip-lines-vertical",
|
|
aliases: []string{"Road Running"},
|
|
translations: subcategoryTranslations("Road", "Straßenlauf", "ROAD"),
|
|
},
|
|
{
|
|
parentCategory: "Skiing",
|
|
name: "Cross-country",
|
|
shortName: "NORD",
|
|
aliases: []string{"Cross-country Skiing"},
|
|
translations: subcategoryTranslations("Cross-country", "Langlauf", "NORD"),
|
|
},
|
|
{
|
|
parentCategory: "Skiing",
|
|
name: "Skating",
|
|
shortName: "SKATE",
|
|
translations: subcategoryTranslations("Skating", "Skating", "SKATE"),
|
|
},
|
|
{
|
|
parentCategory: "Skiing",
|
|
name: "Backcountry",
|
|
shortName: "BACK",
|
|
aliases: []string{"Backcountry Skiing"},
|
|
translations: subcategoryTranslations("Backcountry", "Skitour", "BACK"),
|
|
},
|
|
}
|
|
|
|
func subcategoryTranslations(en string, de string, shortName string) map[string]CategoryTranslation {
|
|
return map[string]CategoryTranslation{
|
|
"de": {Name: de, ShortName: shortName},
|
|
"en": {Name: en, ShortName: shortName},
|
|
}
|
|
}
|
|
|
|
func SeedDefaultSubcategories(app core.App) error {
|
|
subcategoriesCollection, err := app.FindCollectionByNameOrId("subcategories")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, seed := range defaultSubcategories {
|
|
categories, err := app.FindRecordsByFilter(
|
|
"categories",
|
|
"name = {:name}",
|
|
"",
|
|
1,
|
|
0,
|
|
map[string]any{"name": seed.parentCategory},
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(categories) == 0 {
|
|
continue
|
|
}
|
|
category := categories[0]
|
|
|
|
existing, err := app.FindRecordsByFilter(
|
|
"subcategories",
|
|
"category = {:category}",
|
|
"",
|
|
0,
|
|
0,
|
|
map[string]any{"category": category.Id},
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if existingRecord := findDefaultSubcategory(existing, seed.name, seed.aliases); existingRecord != nil {
|
|
changed, err := applyDefaultSubcategorySeed(existingRecord, seed)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if changed {
|
|
if err := app.Save(existingRecord); err != nil {
|
|
return fmt.Errorf("failed to update seeded subcategory %q: %w", seed.name, err)
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
|
|
record := core.NewRecord(subcategoriesCollection)
|
|
record.Set("category", category.Id)
|
|
record.Set("name", seed.name)
|
|
record.Set("short_name", seed.shortName)
|
|
if seed.badgeIcon != "" {
|
|
record.Set("badge_icon", seed.badgeIcon)
|
|
}
|
|
if len(seed.translations) > 0 {
|
|
record.Set("translations", seed.translations)
|
|
}
|
|
if err := app.Save(record); err != nil {
|
|
return fmt.Errorf("failed to seed subcategory %q: %w", seed.name, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func applyDefaultSubcategorySeed(record *core.Record, seed defaultSubcategorySeed) (bool, error) {
|
|
changed := false
|
|
|
|
if isDefaultSubcategoryAlias(record.GetString("name"), seed.aliases) {
|
|
record.Set("name", seed.name)
|
|
changed = true
|
|
}
|
|
|
|
if record.GetString("short_name") == "" && seed.shortName != "" {
|
|
record.Set("short_name", seed.shortName)
|
|
changed = true
|
|
}
|
|
|
|
if record.GetString("badge_icon") == "" && seed.badgeIcon != "" {
|
|
record.Set("badge_icon", seed.badgeIcon)
|
|
changed = true
|
|
}
|
|
|
|
if len(seed.translations) > 0 {
|
|
currentTranslations, err := ParseCategoryTranslations(record.Get("translations"))
|
|
if err != nil {
|
|
return false, fmt.Errorf("invalid existing translations for subcategory %q: %w", record.GetString("name"), err)
|
|
}
|
|
|
|
mergedTranslations, translationsChanged := mergeDefaultSubcategoryTranslations(seed.translations, currentTranslations)
|
|
if translationsChanged {
|
|
record.Set("translations", mergedTranslations)
|
|
changed = true
|
|
}
|
|
}
|
|
|
|
return changed, nil
|
|
}
|
|
|
|
func isDefaultSubcategoryAlias(name string, aliases []string) bool {
|
|
normalizedName := NormalizeCategoryName(name)
|
|
for _, alias := range aliases {
|
|
if normalizedName == NormalizeCategoryName(alias) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func mergeDefaultSubcategoryTranslations(staticTranslations map[string]CategoryTranslation, currentTranslations map[string]CategoryTranslation) (map[string]CategoryTranslation, bool) {
|
|
if currentTranslations == nil {
|
|
currentTranslations = map[string]CategoryTranslation{}
|
|
}
|
|
|
|
changed := false
|
|
for locale, staticTranslation := range staticTranslations {
|
|
translation := currentTranslations[locale]
|
|
localeChanged := false
|
|
if translation.Name == "" && staticTranslation.Name != "" {
|
|
translation.Name = staticTranslation.Name
|
|
localeChanged = true
|
|
}
|
|
if translation.ShortName == "" && staticTranslation.ShortName != "" {
|
|
translation.ShortName = staticTranslation.ShortName
|
|
localeChanged = true
|
|
}
|
|
if localeChanged {
|
|
changed = true
|
|
currentTranslations[locale] = translation
|
|
}
|
|
}
|
|
|
|
return currentTranslations, changed
|
|
}
|
|
|
|
func findDefaultSubcategory(records []*core.Record, name string, aliases []string) *core.Record {
|
|
normalizedName := NormalizeCategoryName(name)
|
|
for _, record := range records {
|
|
if NormalizeCategoryName(record.GetString("name")) == normalizedName {
|
|
return record
|
|
}
|
|
}
|
|
|
|
normalizedAliases := map[string]struct{}{}
|
|
for _, alias := range aliases {
|
|
normalizedAliases[NormalizeCategoryName(alias)] = struct{}{}
|
|
}
|
|
|
|
for _, record := range records {
|
|
if _, ok := normalizedAliases[NormalizeCategoryName(record.GetString("name"))]; ok {
|
|
return record
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|