package domain import ( "database/sql" "encoding/json" "time" "gorm.io/datatypes" ) // TimelineCategory defines the category of a timeline event type TimelineCategory string const ( TimelineCategoryPolitical TimelineCategory = "political" TimelineCategoryMilitary TimelineCategory = "military" TimelineCategoryEconomic TimelineCategory = "economic" TimelineCategoryCultural TimelineCategory = "cultural" TimelineCategorySocial TimelineCategory = "social" TimelineCategoryNatural TimelineCategory = "natural" TimelineCategoryInfrastructure TimelineCategory = "infrastructure" TimelineCategoryCriminal TimelineCategory = "criminal" ) // TimelineKind defines the kind/type of timeline event type TimelineKind string const ( TimelineKindHistorical TimelineKind = "historical" TimelineKindLegend TimelineKind = "legend" TimelineKindMixed TimelineKind = "mixed" ) // IsValidTimelineCategory checks if a category value is valid func IsValidTimelineCategory(category TimelineCategory) bool { validCategories := map[TimelineCategory]bool{ TimelineCategoryPolitical: true, TimelineCategoryMilitary: true, TimelineCategoryEconomic: true, TimelineCategoryCultural: true, TimelineCategorySocial: true, TimelineCategoryNatural: true, TimelineCategoryInfrastructure: true, TimelineCategoryCriminal: true, } return validCategories[category] } // IsValidTimelineKind checks if a kind value is valid func IsValidTimelineKind(kind TimelineKind) bool { validKinds := map[TimelineKind]bool{ TimelineKindHistorical: true, TimelineKindLegend: true, TimelineKindMixed: true, } return validKinds[kind] } // TimelineItem represents a historical event or period that can be displayed in various contexts type TimelineItem struct { ID string `gorm:"primaryKey;type:varchar(50)" json:"id"` Title string `gorm:"type:varchar(255);not null" json:"title"` Content string `gorm:"type:text;not null" json:"content"` Summary string `gorm:"type:text" json:"summary"` // Short 1-2 sentence description ImageURL string `gorm:"type:text" json:"image_url"` IconName string `gorm:"type:varchar(50);not null" json:"icon_name"` Order int `gorm:"not null" json:"order"` Heritage sql.NullBool `json:"heritage"` // Whether this item is for heritage page display (nullable to allow false) // Time range TimeFrom *time.Time `gorm:"type:timestamp" json:"time_from"` // Start date/time TimeTo *time.Time `gorm:"type:timestamp" json:"time_to"` // End date/time // Categorization Category TimelineCategory `gorm:"type:varchar(50)" json:"category"` // political | military | economic | cultural | social | natural | infrastructure | criminal Kind TimelineKind `gorm:"type:varchar(20)" json:"kind"` // historical | legend | mixed IsHistorical sql.NullBool `json:"is_historical"` // Whether this is a verified historical event (nullable to allow false) // Importance level (1-10, higher = more important) Importance int `gorm:"default:1" json:"importance"` // Related data as JSON arrays // Store arrays as JSONB in Postgres. Use datatypes.JSON so GORM will // correctly write/read JSONB columns. MarshalJSON will decode these // into native []string when producing API responses. Locations datatypes.JSON `gorm:"type:jsonb;default:'[]'::jsonb" json:"locations"` Actors datatypes.JSON `gorm:"type:jsonb;default:'[]'::jsonb" json:"actors"` Related datatypes.JSON `gorm:"type:jsonb;default:'[]'::jsonb" json:"related"` Tags datatypes.JSON `gorm:"type:jsonb;default:'[]'::jsonb" json:"tags"` CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` } // TableName specifies the table name for TimelineItem func (TimelineItem) TableName() string { return "timeline_items" } // GetEntityType implements Localizable interface func (t *TimelineItem) GetEntityType() string { return "timeline_item" } // GetEntityID implements Localizable interface func (t *TimelineItem) GetEntityID() string { return t.ID } // MarshalJSON implements custom JSON marshaling for TimelineItem func (t TimelineItem) MarshalJSON() ([]byte, error) { // Create a map for JSON serialization result := map[string]interface{}{ "id": t.ID, "title": t.Title, "content": t.Content, "summary": t.Summary, "image_url": t.ImageURL, "icon_name": t.IconName, "order": t.Order, "time_from": t.TimeFrom, "time_to": t.TimeTo, "category": t.Category, "kind": t.Kind, "importance": t.Importance, "locations": t.Locations, "actors": t.Actors, "related": t.Related, "tags": t.Tags, "created_at": t.CreatedAt, "updated_at": t.UpdatedAt, } // Handle nullable boolean fields if t.Heritage.Valid { result["heritage"] = t.Heritage.Bool } else { result["heritage"] = nil } if t.IsHistorical.Valid { result["is_historical"] = t.IsHistorical.Bool } else { result["is_historical"] = nil } // Ensure stored JSON fields are valid JSON arrays. When using datatypes.JSON // we should decode the raw bytes into Go slices for consistent API output. // Attempt to decode locations/actors/related/tags into []string and set // the result to the decoded slice; on failure fall back to raw JSON value. var list []string if len(t.Locations) > 0 { if err := json.Unmarshal(t.Locations, &list); err == nil { result["locations"] = list } else { result["locations"] = t.Locations } } if len(t.Actors) > 0 { if err := json.Unmarshal(t.Actors, &list); err == nil { result["actors"] = list } else { result["actors"] = t.Actors } } if len(t.Related) > 0 { if err := json.Unmarshal(t.Related, &list); err == nil { result["related"] = list } else { result["related"] = t.Related } } if len(t.Tags) > 0 { if err := json.Unmarshal(t.Tags, &list); err == nil { result["tags"] = list } else { result["tags"] = t.Tags } } return json.Marshal(result) }