deck.go
go
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
}
72f0edf

Add rich text formatting and block styling to editor

Connor McCutcheon
@connor
0 stars

Pitch decks to help promote Skyscape Apps

Sign in to comment Sign In