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 <>
This commit is contained in:
284
db/util/subcategory_defaults.go
Normal file
284
db/util/subcategory_defaults.go
Normal file
@@ -0,0 +1,284 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user