Files
wanderer/db/util/subcategory_defaults.go
slothful-vassal 3b8f00fd26 feat: Refine trail category model (#1059)
* 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 <>
2026-06-29 03:16:26 +02:00

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
}