package models
import (
"encoding/json"
"time"
"github.com/google/uuid"
"theskyscape.com/repo/skykit"
)
// Deck represents a slide deck presentation
type Deck struct {
skykit.Model
OwnerID string
OwnerName string
OwnerHandle string
OwnerAvatar string
Title string
Description string
Content string // JSON slides/blocks
CoverImage string
IsPublished bool
PublishedAt time.Time
ViewCount int
}
// DeckContent represents the parsed JSON structure of a deck
type DeckContent struct {
Slides []Slide `json:"slides"`
Theme string `json:"theme"`
AspectRatio string `json:"aspectRatio"`
}
// Slide represents a single slide in the deck
type Slide struct {
ID string `json:"id"`
Blocks []Block `json:"blocks"`
Background *SlideBackground `json:"background,omitempty"`
}
// SlideBackground represents slide-level background styling
type SlideBackground struct {
Type string `json:"type,omitempty"` // color, image
Color string `json:"color,omitempty"` // CSS color value
ImageURL string `json:"imageURL,omitempty"` // Background image URL
Position string `json:"position,omitempty"` // cover, contain, center, etc.
Opacity string `json:"opacity,omitempty"` // 0-1 for overlay opacity
}
// Block represents a content block within a slide
type Block struct {
ID string `json:"id"`
Type string `json:"type"` // heading, text, code, image, list, quote, divider
Content string `json:"content,omitempty"` // For heading, text, code, quote (supports rich text HTML)
Level int `json:"level,omitempty"` // For heading (1-3)
Language string `json:"language,omitempty"` // For code
URL string `json:"url,omitempty"` // For image
Caption string `json:"caption,omitempty"` // For image
Style string `json:"style,omitempty"` // For list (bullet, number)
Items []string `json:"items,omitempty"` // For list
Author string `json:"author,omitempty"` // For quote
// Text styling
TextAlign string `json:"textAlign,omitempty"` // left, center, right, justify
TextColor string `json:"textColor,omitempty"` // CSS color value
FontSize string `json:"fontSize,omitempty"` // CSS size (e.g., "24px", "2rem")
// Box model styling
BoxStyle *BoxStyle `json:"boxStyle,omitempty"`
}
// BoxStyle contains CSS box model properties for a block
type BoxStyle struct {
// Dimensions
Width string `json:"width,omitempty"` // %, px, or "auto"
Height string `json:"height,omitempty"` // px or "auto"
// Spacing (CSS format: "10px" or "10px 20px 30px 40px")
Padding string `json:"padding,omitempty"`
Margin string `json:"margin,omitempty"`
// Background
BackgroundColor string `json:"backgroundColor,omitempty"`
BackgroundOpacity string `json:"backgroundOpacity,omitempty"` // 0-1
// Border
BorderWidth string `json:"borderWidth,omitempty"`
BorderStyle string `json:"borderStyle,omitempty"` // solid, dashed, dotted, none
BorderColor string `json:"borderColor,omitempty"`
BorderRadius string `json:"borderRadius,omitempty"`
}
// ParseContent returns the parsed DeckContent from JSON
func (d *Deck) ParseContent() (*DeckContent, error) {
if d.Content == "" {
return &DeckContent{
Slides: []Slide{},
Theme: "dark",
AspectRatio: "16:9",
}, nil
}
var content DeckContent
if err := json.Unmarshal([]byte(d.Content), &content); err != nil {
return nil, err
}
return &content, nil
}
// SetContent marshals the DeckContent to JSON and stores it
func (d *Deck) SetContent(content *DeckContent) error {
data, err := json.Marshal(content)
if err != nil {
return err
}
d.Content = string(data)
return nil
}
// CreateDeck creates a new deck with an initial empty slide
func CreateDeck(ownerID, ownerName, ownerHandle, ownerAvatar, title string) (*Deck, error) {
deck := &Deck{
OwnerID: ownerID,
OwnerName: ownerName,
OwnerHandle: ownerHandle,
OwnerAvatar: ownerAvatar,
Title: title,
}
// Create initial content with one empty slide
content := &DeckContent{
Slides: []Slide{
{
ID: generateID(),
Blocks: []Block{
{
ID: generateID(),
Type: "heading",
Content: title,
Level: 1,
},
},
},
},
Theme: "dark",
AspectRatio: "16:9",
}
if err := deck.SetContent(content); err != nil {
return nil, err
}
return Decks.Insert(deck)
}
// GetDeckByOwner retrieves a deck only if owned by the given user
func GetDeckByOwner(id, ownerID string) (*Deck, error) {
return Decks.First("WHERE ID = ? AND OwnerID = ?", id, ownerID)
}
// ListDecks returns all decks for a user ordered by UpdatedAt
func ListDecks(ownerID string) ([]*Deck, error) {
return Decks.Search("WHERE OwnerID = ? ORDER BY UpdatedAt DESC", ownerID)
}
// ListPublishedDecks returns published decks with pagination
func ListPublishedDecks(limit, offset int) ([]*Deck, error) {
return Decks.Search("WHERE IsPublished = true ORDER BY PublishedAt DESC LIMIT ? OFFSET ?", limit, offset)
}
// PublishDeck makes a deck public
func (d *Deck) Publish() error {
d.IsPublished = true
d.PublishedAt = time.Now()
return Decks.Update(d)
}
// UnpublishDeck makes a deck private
func (d *Deck) Unpublish() error {
d.IsPublished = false
return Decks.Update(d)
}
// IncrementViewCount increments the view count using atomic database operation
func (d *Deck) IncrementViewCount() {
go func() {
// Use database-level increment to avoid race conditions
DB.Exec("UPDATE decks SET ViewCount = ViewCount + 1 WHERE ID = ?", d.ID)
}()
}
// ForkDeck creates a copy of a deck for another user
func (d *Deck) Fork(ownerID, ownerName, ownerHandle, ownerAvatar string) (*Deck, error) {
fork := &Deck{
OwnerID: ownerID,
OwnerName: ownerName,
OwnerHandle: ownerHandle,
OwnerAvatar: ownerAvatar,
Title: d.Title + " (Fork)",
Description: d.Description,
Content: d.Content,
CoverImage: d.CoverImage,
}
return Decks.Insert(fork)
}
// generateID creates a UUID
func generateID() string {
return uuid.New().String()
}
// GetImageURLs returns all image URLs from block content
func (d *Deck) GetImageURLs() []string {
content, err := d.ParseContent()
if err != nil {
return nil
}
var urls []string
for _, slide := range content.Slides {
for _, block := range slide.Blocks {
if block.Type == "image" && block.URL != "" {
urls = append(urls, block.URL)
}
}
}
return urls
}
// GetImageURLsFromContent extracts image URLs from a content JSON string
func GetImageURLsFromContent(contentJSON string) []string {
if contentJSON == "" {
return nil
}
var content DeckContent
if err := json.Unmarshal([]byte(contentJSON), &content); err != nil {
return nil
}
var urls []string
for _, slide := range content.Slides {
for _, block := range slide.Blocks {
if block.Type == "image" && block.URL != "" {
urls = append(urls, block.URL)
}
}
}
return urls
}
Add rich text formatting and block styling to editor
Pitch decks to help promote Skyscape Apps