This commit is contained in:
Tomáš Dvořák
2025-10-16 13:32:05 +02:00
commit 12cba639b9
663 changed files with 168914 additions and 0 deletions
+206
View File
@@ -0,0 +1,206 @@
package database
import (
"log"
"fotbal-club/internal/models"
"fotbal-club/pkg/utils"
"gorm.io/gorm"
)
// MigrateDB runs database migrations
func MigrateDB(db *gorm.DB) error {
// Enable UUID extension
err := db.Exec(`
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
`).Error
if err != nil {
return err
}
// Drop existing tables if they exist
err = db.Migrator().DropTable(
&models.User{},
&models.Article{},
&models.Category{},
&models.Team{},
&models.Player{},
&models.Sponsor{},
&models.Settings{},
&models.MatchOverride{},
&models.TeamLogoOverride{},
&models.ContactMessage{},
&models.PasswordReset{},
&models.ArticleMatchLink{},
&models.ScoreboardState{},
&models.ContactCategory{},
&models.Contact{},
&models.PageElementConfig{},
)
if err != nil {
return err
}
// Auto migrate all models
err = db.AutoMigrate(
&models.User{},
&models.Article{},
&models.Category{},
&models.Team{},
&models.Player{},
&models.Sponsor{},
&models.Settings{},
&models.MatchOverride{},
&models.TeamLogoOverride{},
&models.ContactMessage{},
&models.PasswordReset{},
&models.ArticleMatchLink{},
&models.ScoreboardState{},
&models.ContactCategory{},
&models.Contact{},
&models.PageElementConfig{},
)
if err != nil {
return err
}
// Add indexes
err = db.Exec(`
CREATE INDEX IF NOT EXISTS idx_users_email ON users (email);
CREATE INDEX IF NOT EXISTS idx_articles_category_id ON articles (category_id);
CREATE INDEX IF NOT EXISTS idx_articles_author_id ON articles (author_id);
CREATE INDEX IF NOT EXISTS idx_password_resets_user_id ON password_resets (user_id);
CREATE UNIQUE INDEX IF NOT EXISTS ux_password_resets_token ON password_resets (token);
`).Error
if err != nil {
return err
}
log.Println("Database migration completed successfully")
return nil
}
// SeedDB populates the database with initial data
func SeedDB(db *gorm.DB) error {
// Check if we already have data
var count int64
db.Model(&models.User{}).Count(&count)
if count > 0 {
log.Println("Database already seeded, skipping...")
return nil
}
// Hash the admin password
hashedPassword, err := utils.HashPassword("admin123")
if err != nil {
return err
}
// Create default admin user
admin := models.User{
Email: "admin@fotbalclub.cz",
Password: hashedPassword,
FirstName: "Admin",
LastName: "User",
Role: "admin",
}
if err := db.Create(&admin).Error; err != nil {
return err
}
// Create default test user
hashedPassword, err = utils.HashPassword("user123")
if err != nil {
return err
}
user := models.User{
Email: "user@fotbalclub.cz",
Password: hashedPassword,
FirstName: "Test",
LastName: "User",
Role: "user",
}
if err := db.Create(&user).Error; err != nil {
return err
}
// Create default categories
categories := []models.Category{
{Name: "Novinky", Description: "Aktuální novinky z klubu"},
{Name: "Zápasy", Description: "Zápasy a výsledky"},
{Name: "Tabulka", Description: "Soutěžní tabulka"},
{Name: "Tiskové zprávy", Description: "Oficiální tiskové zprávy"},
}
for _, category := range categories {
if err := db.FirstOrCreate(&category, models.Category{Name: category.Name}).Error; err != nil {
return err
}
}
// Create sample team
team := models.Team{
Name: "FK Fotbal Club",
ShortName: "FKFC",
Description: "Hlavní tým FK Fotbal Clubu",
IsActive: true,
}
if err := db.Create(&team).Error; err != nil {
return err
}
// Create sample players
players := []models.Player{
{
FirstName: "Jan",
LastName: "Novák",
Position: "Brankář",
JerseyNumber: 1,
TeamID: team.ID,
IsActive: true,
},
{
FirstName: "Petr",
LastName: "Svoboda",
Position: "Obránce",
JerseyNumber: 5,
TeamID: team.ID,
IsActive: true,
},
}
for _, player := range players {
if err := db.Create(&player).Error; err != nil {
return err
}
}
// Create sample sponsors
sponsors := []models.Sponsor{
{
Name: "Hlavní partner",
WebsiteURL: "https://example.com",
IsActive: true,
},
{
Name: "Oficiální výstroj",
WebsiteURL: "https://sportovni-vyrobce.cz",
IsActive: true,
},
}
for _, sponsor := range sponsors {
if err := db.Create(&sponsor).Error; err != nil {
return err
}
}
log.Println("Database seeding completed successfully")
return nil
}
@@ -0,0 +1,4 @@
-- +goose Down
-- SQL in this section is executed when the migration is rolled back
DROP TABLE IF EXISTS club_info CASCADE;
DROP TABLE IF EXISTS setup_info CASCADE;
@@ -0,0 +1,33 @@
-- +goose Up
-- SQL in this section is executed when the migration is applied
CREATE TABLE IF NOT EXISTS setup_info (
id SERIAL PRIMARY KEY,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMP WITH TIME ZONE,
status VARCHAR(20) NOT NULL DEFAULT 'pending',
skipped_at TIMESTAMP WITH TIME ZONE,
completed_at TIMESTAMP WITH TIME ZONE,
smtp_configured BOOLEAN NOT NULL DEFAULT false,
club_imported BOOLEAN NOT NULL DEFAULT false
);
CREATE TABLE IF NOT EXISTS club_info (
id SERIAL PRIMARY KEY,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMP WITH TIME ZONE,
setup_info_id INTEGER NOT NULL REFERENCES setup_info(id) ON DELETE CASCADE,
facr_club_id VARCHAR(255) UNIQUE NOT NULL,
name VARCHAR(255) NOT NULL,
short_name VARCHAR(100),
logo_url TEXT,
primary_color VARCHAR(7),
secondary_color VARCHAR(7),
text_color VARCHAR(7)
);
-- Create initial setup info record if it doesn't exist
INSERT INTO setup_info (id, status, created_at, updated_at)
SELECT 1, 'pending', NOW(), NOW()
WHERE NOT EXISTS (SELECT 1 FROM setup_info WHERE id = 1);
@@ -0,0 +1,18 @@
-- Remove contact/location fields from settings
ALTER TABLE settings DROP COLUMN IF EXISTS show_map_on_homepage;
ALTER TABLE settings DROP COLUMN IF EXISTS map_style;
ALTER TABLE settings DROP COLUMN IF EXISTS map_zoom_level;
ALTER TABLE settings DROP COLUMN IF EXISTS location_longitude;
ALTER TABLE settings DROP COLUMN IF EXISTS location_latitude;
ALTER TABLE settings DROP COLUMN IF EXISTS contact_email;
ALTER TABLE settings DROP COLUMN IF EXISTS contact_phone;
ALTER TABLE settings DROP COLUMN IF EXISTS contact_country;
ALTER TABLE settings DROP COLUMN IF EXISTS contact_zip;
ALTER TABLE settings DROP COLUMN IF EXISTS contact_city;
ALTER TABLE settings DROP COLUMN IF EXISTS contact_address;
-- Drop contacts table
DROP TABLE IF EXISTS contacts;
-- Drop contact_categories table
DROP TABLE IF EXISTS contact_categories;
@@ -0,0 +1,48 @@
-- Create contact_categories table
CREATE TABLE IF NOT EXISTS contact_categories (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL UNIQUE,
description TEXT,
display_order INT DEFAULT 0,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
CREATE INDEX idx_contact_categories_display_order ON contact_categories(display_order);
CREATE INDEX idx_contact_categories_is_active ON contact_categories(is_active);
-- Create contacts table
CREATE TABLE IF NOT EXISTS contacts (
id SERIAL PRIMARY KEY,
category_id INT REFERENCES contact_categories(id) ON DELETE SET NULL,
name VARCHAR(255) NOT NULL,
position VARCHAR(255),
email VARCHAR(255),
phone VARCHAR(100),
image_url VARCHAR(500),
description TEXT,
display_order INT DEFAULT 0,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
CREATE INDEX idx_contacts_category_id ON contacts(category_id);
CREATE INDEX idx_contacts_display_order ON contacts(display_order);
CREATE INDEX idx_contacts_is_active ON contacts(is_active);
-- Add contact/location fields to settings table
ALTER TABLE settings ADD COLUMN IF NOT EXISTS contact_address VARCHAR(500);
ALTER TABLE settings ADD COLUMN IF NOT EXISTS contact_city VARCHAR(255);
ALTER TABLE settings ADD COLUMN IF NOT EXISTS contact_zip VARCHAR(20);
ALTER TABLE settings ADD COLUMN IF NOT EXISTS contact_country VARCHAR(100);
ALTER TABLE settings ADD COLUMN IF NOT EXISTS contact_phone VARCHAR(100);
ALTER TABLE settings ADD COLUMN IF NOT EXISTS contact_email VARCHAR(255);
ALTER TABLE settings ADD COLUMN IF NOT EXISTS location_latitude DECIMAL(10, 8);
ALTER TABLE settings ADD COLUMN IF NOT EXISTS location_longitude DECIMAL(11, 8);
ALTER TABLE settings ADD COLUMN IF NOT EXISTS map_zoom_level INT DEFAULT 15;
ALTER TABLE settings ADD COLUMN IF NOT EXISTS map_style VARCHAR(255);
ALTER TABLE settings ADD COLUMN IF NOT EXISTS show_map_on_homepage BOOLEAN DEFAULT FALSE;
@@ -0,0 +1,9 @@
-- Rollback visitor_events table changes
DROP INDEX IF EXISTS idx_visitor_events_page_path;
DROP INDEX IF EXISTS idx_visitor_events_created_at;
DROP INDEX IF EXISTS idx_visitor_events_event_type_created_at;
ALTER TABLE visitor_events DROP COLUMN IF EXISTS page_path;
ALTER TABLE visitor_events DROP COLUMN IF EXISTS page_name;
ALTER TABLE visitor_events DROP COLUMN IF EXISTS data;
@@ -0,0 +1,13 @@
-- Add new fields to visitor_events table for better analytics tracking
ALTER TABLE visitor_events ADD COLUMN IF NOT EXISTS page_path VARCHAR(512);
ALTER TABLE visitor_events ADD COLUMN IF NOT EXISTS page_name VARCHAR(512);
ALTER TABLE visitor_events ADD COLUMN IF NOT EXISTS data JSONB;
-- Create indexes for better query performance
CREATE INDEX IF NOT EXISTS idx_visitor_events_page_path ON visitor_events(page_path);
CREATE INDEX IF NOT EXISTS idx_visitor_events_created_at ON visitor_events(created_at);
CREATE INDEX IF NOT EXISTS idx_visitor_events_event_type_created_at ON visitor_events(event_type, created_at);
-- Copy existing 'page' data to 'page_path' if page_path is empty
UPDATE visitor_events SET page_path = page WHERE page_path IS NULL OR page_path = '';
@@ -0,0 +1,11 @@
-- Drop file tracking tables
DROP INDEX IF EXISTS idx_file_usages_unique;
DROP INDEX IF EXISTS idx_file_usages_entity;
DROP INDEX IF EXISTS idx_file_usages_file_id;
DROP TABLE IF EXISTS file_usages;
DROP INDEX IF EXISTS idx_uploaded_files_deleted_at;
DROP INDEX IF EXISTS idx_uploaded_files_created_at;
DROP INDEX IF EXISTS idx_uploaded_files_uploaded_by;
DROP INDEX IF EXISTS idx_uploaded_files_file_path;
DROP TABLE IF EXISTS uploaded_files;
@@ -0,0 +1,35 @@
-- Create uploaded_files table to track all uploaded files
CREATE TABLE IF NOT EXISTS uploaded_files (
id BIGSERIAL PRIMARY KEY,
filename VARCHAR(255) NOT NULL,
file_path VARCHAR(500) NOT NULL UNIQUE,
file_url VARCHAR(500) NOT NULL,
file_size BIGINT NOT NULL DEFAULT 0,
mime_type VARCHAR(100),
uploaded_by_id BIGINT REFERENCES users(id) ON DELETE SET NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP WITH TIME ZONE
);
-- Create index for faster lookups
CREATE INDEX IF NOT EXISTS idx_uploaded_files_file_path ON uploaded_files(file_path);
CREATE INDEX IF NOT EXISTS idx_uploaded_files_uploaded_by ON uploaded_files(uploaded_by_id);
CREATE INDEX IF NOT EXISTS idx_uploaded_files_created_at ON uploaded_files(created_at);
CREATE INDEX IF NOT EXISTS idx_uploaded_files_deleted_at ON uploaded_files(deleted_at);
-- Create file_usages table to track where files are used
CREATE TABLE IF NOT EXISTS file_usages (
id BIGSERIAL PRIMARY KEY,
file_id BIGINT NOT NULL REFERENCES uploaded_files(id) ON DELETE CASCADE,
entity_type VARCHAR(100) NOT NULL, -- e.g., 'article', 'player', 'sponsor', 'event', 'contact', 'settings'
entity_id BIGINT NOT NULL,
field_name VARCHAR(100), -- e.g., 'image_url', 'logo_url', 'attachments'
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- Create indexes for file usage tracking
CREATE INDEX IF NOT EXISTS idx_file_usages_file_id ON file_usages(file_id);
CREATE INDEX IF NOT EXISTS idx_file_usages_entity ON file_usages(entity_type, entity_id);
CREATE UNIQUE INDEX IF NOT EXISTS idx_file_usages_unique ON file_usages(file_id, entity_type, entity_id, field_name);
@@ -0,0 +1 @@
DROP TABLE IF EXISTS page_element_configs;
@@ -0,0 +1,17 @@
-- Create page_element_configs table for storing visual element variant preferences
CREATE TABLE IF NOT EXISTS page_element_configs (
id SERIAL PRIMARY KEY,
page_type VARCHAR(50) NOT NULL,
element_name VARCHAR(100) NOT NULL,
variant VARCHAR(50) NOT NULL,
visible BOOLEAN DEFAULT true,
display_order INTEGER DEFAULT 0,
settings JSONB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP,
UNIQUE(page_type, element_name)
);
CREATE INDEX idx_page_element_configs_page_type ON page_element_configs(page_type);
CREATE INDEX idx_page_element_configs_element_name ON page_element_configs(element_name);
@@ -0,0 +1,25 @@
-- Drop triggers
DROP TRIGGER IF EXISTS trigger_polls_updated_at ON polls;
DROP TRIGGER IF EXISTS trigger_poll_options_updated_at ON poll_options;
DROP FUNCTION IF EXISTS update_polls_updated_at();
-- Drop indexes
DROP INDEX IF EXISTS idx_polls_status;
DROP INDEX IF EXISTS idx_polls_featured;
DROP INDEX IF EXISTS idx_polls_start_date;
DROP INDEX IF EXISTS idx_polls_end_date;
DROP INDEX IF EXISTS idx_polls_deleted_at;
DROP INDEX IF EXISTS idx_poll_options_poll_id;
DROP INDEX IF EXISTS idx_poll_options_display_order;
DROP INDEX IF EXISTS idx_poll_votes_poll_id;
DROP INDEX IF EXISTS idx_poll_votes_option_id;
DROP INDEX IF EXISTS idx_poll_votes_user_id;
DROP INDEX IF EXISTS idx_poll_votes_ip_hash;
DROP INDEX IF EXISTS idx_poll_votes_session_token;
-- Drop tables (in reverse order of dependencies)
DROP TABLE IF EXISTS poll_votes;
DROP TABLE IF EXISTS poll_options;
DROP TABLE IF EXISTS polls;
@@ -0,0 +1,85 @@
-- Create polls table
CREATE TABLE IF NOT EXISTS polls (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
description TEXT,
type VARCHAR(50) NOT NULL DEFAULT 'single',
status VARCHAR(20) NOT NULL DEFAULT 'draft',
start_date TIMESTAMP,
end_date TIMESTAMP,
allow_multiple BOOLEAN DEFAULT FALSE,
max_choices INTEGER DEFAULT 1,
show_results VARCHAR(20) DEFAULT 'after_vote',
require_auth BOOLEAN DEFAULT FALSE,
allow_guest_vote BOOLEAN DEFAULT TRUE,
featured BOOLEAN DEFAULT FALSE,
category_id INTEGER REFERENCES categories(id) ON DELETE SET NULL,
related_match_id INTEGER,
image_url VARCHAR(500),
total_votes INTEGER DEFAULT 0,
created_by INTEGER REFERENCES users(id) ON DELETE SET NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- Create poll_options table
CREATE TABLE IF NOT EXISTS poll_options (
id SERIAL PRIMARY KEY,
poll_id INTEGER NOT NULL REFERENCES polls(id) ON DELETE CASCADE,
text VARCHAR(255) NOT NULL,
description TEXT,
image_url VARCHAR(500),
display_order INTEGER DEFAULT 0,
vote_count INTEGER DEFAULT 0,
player_id INTEGER REFERENCES players(id) ON DELETE SET NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Create poll_votes table
CREATE TABLE IF NOT EXISTS poll_votes (
id SERIAL PRIMARY KEY,
poll_id INTEGER NOT NULL REFERENCES polls(id) ON DELETE CASCADE,
option_id INTEGER NOT NULL REFERENCES poll_options(id) ON DELETE CASCADE,
user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
ip_hash VARCHAR(64),
user_agent VARCHAR(500),
session_token VARCHAR(100),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Create indexes for performance
CREATE INDEX IF NOT EXISTS idx_polls_status ON polls(status);
CREATE INDEX IF NOT EXISTS idx_polls_featured ON polls(featured);
CREATE INDEX IF NOT EXISTS idx_polls_start_date ON polls(start_date);
CREATE INDEX IF NOT EXISTS idx_polls_end_date ON polls(end_date);
CREATE INDEX IF NOT EXISTS idx_polls_deleted_at ON polls(deleted_at);
CREATE INDEX IF NOT EXISTS idx_poll_options_poll_id ON poll_options(poll_id);
CREATE INDEX IF NOT EXISTS idx_poll_options_display_order ON poll_options(display_order);
CREATE INDEX IF NOT EXISTS idx_poll_votes_poll_id ON poll_votes(poll_id);
CREATE INDEX IF NOT EXISTS idx_poll_votes_option_id ON poll_votes(option_id);
CREATE INDEX IF NOT EXISTS idx_poll_votes_user_id ON poll_votes(user_id);
CREATE INDEX IF NOT EXISTS idx_poll_votes_ip_hash ON poll_votes(ip_hash);
CREATE INDEX IF NOT EXISTS idx_poll_votes_session_token ON poll_votes(session_token);
-- Add trigger for updated_at
CREATE OR REPLACE FUNCTION update_polls_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trigger_polls_updated_at
BEFORE UPDATE ON polls
FOR EACH ROW
EXECUTE FUNCTION update_polls_updated_at();
CREATE TRIGGER trigger_poll_options_updated_at
BEFORE UPDATE ON poll_options
FOR EACH ROW
EXECUTE FUNCTION update_polls_updated_at();
@@ -0,0 +1,4 @@
-- Remove visibility and display order columns
DROP INDEX IF EXISTS idx_page_element_configs_display_order;
ALTER TABLE page_element_configs DROP COLUMN IF EXISTS visible;
ALTER TABLE page_element_configs DROP COLUMN IF EXISTS display_order;
@@ -0,0 +1,6 @@
-- Add visibility and display order columns to page_element_configs
ALTER TABLE page_element_configs ADD COLUMN IF NOT EXISTS visible BOOLEAN DEFAULT true;
ALTER TABLE page_element_configs ADD COLUMN IF NOT EXISTS display_order INTEGER DEFAULT 0;
-- Create index on display_order for faster sorting
CREATE INDEX IF NOT EXISTS idx_page_element_configs_display_order ON page_element_configs(display_order);
@@ -0,0 +1,9 @@
-- Drop indexes
DROP INDEX IF EXISTS idx_polls_related_article_id;
DROP INDEX IF EXISTS idx_polls_related_event_id;
DROP INDEX IF EXISTS idx_polls_related_video_url;
-- Remove relationship columns
ALTER TABLE polls DROP COLUMN IF EXISTS related_article_id;
ALTER TABLE polls DROP COLUMN IF EXISTS related_event_id;
ALTER TABLE polls DROP COLUMN IF EXISTS related_video_url;
@@ -0,0 +1,14 @@
-- Add relationship fields to polls table
ALTER TABLE polls ADD COLUMN IF NOT EXISTS related_article_id INTEGER REFERENCES articles(id) ON DELETE SET NULL;
ALTER TABLE polls ADD COLUMN IF NOT EXISTS related_event_id INTEGER REFERENCES events(id) ON DELETE SET NULL;
ALTER TABLE polls ADD COLUMN IF NOT EXISTS related_video_url VARCHAR(500);
-- Create indexes for better query performance
CREATE INDEX IF NOT EXISTS idx_polls_related_article_id ON polls(related_article_id);
CREATE INDEX IF NOT EXISTS idx_polls_related_event_id ON polls(related_event_id);
CREATE INDEX IF NOT EXISTS idx_polls_related_video_url ON polls(related_video_url);
-- Add comments for documentation
COMMENT ON COLUMN polls.related_article_id IS 'Link poll to a blog article';
COMMENT ON COLUMN polls.related_event_id IS 'Link poll to an event/activity';
COMMENT ON COLUMN polls.related_video_url IS 'Link poll to a YouTube video URL or ID';
@@ -0,0 +1,4 @@
-- +goose Down
-- Remove font configuration fields from settings table
ALTER TABLE settings DROP COLUMN IF EXISTS font_heading;
ALTER TABLE settings DROP COLUMN IF EXISTS font_body;
@@ -0,0 +1,8 @@
-- +goose Up
-- Add font configuration fields to settings table
ALTER TABLE settings ADD COLUMN IF NOT EXISTS font_heading VARCHAR(255) DEFAULT '';
ALTER TABLE settings ADD COLUMN IF NOT EXISTS font_body VARCHAR(255) DEFAULT '';
-- Set default values for existing records
UPDATE settings SET font_heading = 'Inter' WHERE font_heading IS NULL OR font_heading = '';
UPDATE settings SET font_body = 'Inter' WHERE font_body IS NULL OR font_body = '';
@@ -0,0 +1,2 @@
-- Remove finished_match_display_days column from settings table
ALTER TABLE settings DROP COLUMN IF EXISTS finished_match_display_days;
@@ -0,0 +1,2 @@
-- Add finished_match_display_days column to settings table
ALTER TABLE settings ADD COLUMN IF NOT EXISTS finished_match_display_days INTEGER DEFAULT 2;
@@ -0,0 +1,58 @@
-- Rollback performance indexes
-- Articles indexes
DROP INDEX IF EXISTS idx_articles_published_date;
DROP INDEX IF EXISTS idx_articles_slug;
DROP INDEX IF EXISTS idx_articles_category;
DROP INDEX IF EXISTS idx_articles_author;
DROP INDEX IF EXISTS idx_articles_featured;
DROP INDEX IF EXISTS idx_articles_search;
DROP INDEX IF EXISTS idx_articles_category_published;
DROP INDEX IF EXISTS idx_articles_author_published;
DROP INDEX IF EXISTS idx_articles_published_only;
DROP INDEX IF EXISTS idx_articles_unpublished_only;
-- Players indexes
DROP INDEX IF EXISTS idx_players_team;
DROP INDEX IF EXISTS idx_players_position;
DROP INDEX IF EXISTS idx_players_active;
DROP INDEX IF EXISTS idx_players_name;
-- Users indexes
DROP INDEX IF EXISTS idx_users_email;
DROP INDEX IF EXISTS idx_users_role;
DROP INDEX IF EXISTS idx_users_created;
-- Categories indexes
DROP INDEX IF EXISTS idx_categories_name;
-- Newsletter indexes
DROP INDEX IF EXISTS idx_newsletter_email;
DROP INDEX IF EXISTS idx_newsletter_active;
-- Contact messages indexes
DROP INDEX IF EXISTS idx_contact_messages_created;
DROP INDEX IF EXISTS idx_contact_messages_read;
-- Polls indexes
DROP INDEX IF EXISTS idx_polls_active;
DROP INDEX IF EXISTS idx_poll_votes_poll;
DROP INDEX IF EXISTS idx_poll_votes_user;
-- Match overrides indexes
DROP INDEX IF EXISTS idx_match_overrides_external;
-- Team logo overrides indexes
DROP INDEX IF EXISTS idx_team_logo_external;
-- Uploaded files indexes
DROP INDEX IF EXISTS idx_uploaded_files_user;
DROP INDEX IF EXISTS idx_uploaded_files_created;
-- Competition aliases indexes
DROP INDEX IF EXISTS idx_competition_aliases_code;
DROP INDEX IF EXISTS idx_competition_aliases_order;
-- Clothing indexes
DROP INDEX IF EXISTS idx_clothing_category;
DROP INDEX IF EXISTS idx_clothing_available;
@@ -0,0 +1,82 @@
-- Performance indexes for optimal query performance
-- Run this migration to achieve 10/10 database performance
-- Articles table indexes
CREATE INDEX IF NOT EXISTS idx_articles_published_date ON articles(published, published_at DESC) WHERE published = true;
CREATE INDEX IF NOT EXISTS idx_articles_slug ON articles(slug) WHERE slug IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_articles_category ON articles(category_id) WHERE category_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_articles_author ON articles(author_id);
CREATE INDEX IF NOT EXISTS idx_articles_featured ON articles(featured, published_at DESC) WHERE featured = true AND published = true;
CREATE INDEX IF NOT EXISTS idx_articles_search ON articles USING gin(to_tsvector('english', title || ' ' || COALESCE(content, '')));
-- Players table indexes
CREATE INDEX IF NOT EXISTS idx_players_team ON players(team_id) WHERE team_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_players_position ON players(position) WHERE position IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_players_active ON players(is_active) WHERE is_active = true;
CREATE INDEX IF NOT EXISTS idx_players_name ON players(name);
-- Users table indexes
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
CREATE INDEX IF NOT EXISTS idx_users_role ON users(role);
CREATE INDEX IF NOT EXISTS idx_users_created ON users(created_at DESC);
-- Categories table indexes
CREATE INDEX IF NOT EXISTS idx_categories_name ON categories(name);
-- Newsletter subscriptions indexes
CREATE INDEX IF NOT EXISTS idx_newsletter_email ON newsletter_subscriptions(email);
CREATE INDEX IF NOT EXISTS idx_newsletter_active ON newsletter_subscriptions(is_active) WHERE is_active = true;
-- Contact messages indexes
CREATE INDEX IF NOT EXISTS idx_contact_messages_created ON contact_messages(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_contact_messages_read ON contact_messages(is_read) WHERE is_read = false;
-- Polls indexes
CREATE INDEX IF NOT EXISTS idx_polls_active ON polls(is_active, end_date) WHERE is_active = true;
CREATE INDEX IF NOT EXISTS idx_poll_votes_poll ON poll_votes(poll_id);
CREATE INDEX IF NOT EXISTS idx_poll_votes_user ON poll_votes(user_identifier);
-- Match overrides indexes
CREATE INDEX IF NOT EXISTS idx_match_overrides_external ON match_overrides(external_match_id);
-- Team logo overrides indexes
CREATE INDEX IF NOT EXISTS idx_team_logo_external ON team_logo_overrides(external_team_id);
-- Uploaded files indexes
CREATE INDEX IF NOT EXISTS idx_uploaded_files_user ON uploaded_files(uploaded_by_id) WHERE uploaded_by_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_uploaded_files_created ON uploaded_files(created_at DESC);
-- Settings table (only one row, no index needed)
-- Competition aliases indexes
CREATE INDEX IF NOT EXISTS idx_competition_aliases_code ON competition_aliases(code);
CREATE INDEX IF NOT EXISTS idx_competition_aliases_order ON competition_aliases(display_order) WHERE display_order > 0;
-- Clothing items indexes
CREATE INDEX IF NOT EXISTS idx_clothing_category ON clothing(category) WHERE category IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_clothing_available ON clothing(is_available) WHERE is_available = true;
-- Composite indexes for common queries
CREATE INDEX IF NOT EXISTS idx_articles_category_published ON articles(category_id, published, published_at DESC) WHERE published = true;
CREATE INDEX IF NOT EXISTS idx_articles_author_published ON articles(author_id, published, published_at DESC) WHERE published = true;
-- Partial indexes for better performance on filtered queries
CREATE INDEX IF NOT EXISTS idx_articles_published_only ON articles(published_at DESC) WHERE published = true;
CREATE INDEX IF NOT EXISTS idx_articles_unpublished_only ON articles(created_at DESC) WHERE published = false;
-- VACUUM ANALYZE to update statistics
VACUUM ANALYZE articles;
VACUUM ANALYZE players;
VACUUM ANALYZE users;
VACUUM ANALYZE categories;
VACUUM ANALYZE newsletter_subscriptions;
VACUUM ANALYZE contact_messages;
VACUUM ANALYZE polls;
VACUUM ANALYZE poll_options;
VACUUM ANALYZE poll_votes;
-- Add comments for documentation
COMMENT ON INDEX idx_articles_published_date IS 'Optimizes published articles listing';
COMMENT ON INDEX idx_articles_search IS 'Full-text search on articles';
COMMENT ON INDEX idx_players_team IS 'Player queries by team';
COMMENT ON INDEX idx_newsletter_active IS 'Active subscriber queries';
@@ -0,0 +1,17 @@
-- Drop triggers first to avoid dependency issues
DROP TRIGGER IF EXISTS update_contact_messages_updated_at ON contact_messages;
DROP TRIGGER IF EXISTS update_newsletter_subscriptions_updated_at ON newsletter_subscriptions;
-- Drop the function
DROP FUNCTION IF EXISTS update_updated_at_column();
-- Drop indexes
DROP INDEX IF EXISTS idx_contact_messages_email;
DROP INDEX IF EXISTS idx_contact_messages_created_at;
DROP INDEX IF EXISTS idx_contact_messages_is_read;
DROP INDEX IF EXISTS idx_newsletter_subscriptions_email;
DROP INDEX IF EXISTS idx_newsletter_subscriptions_is_active;
-- Drop tables
DROP TABLE IF EXISTS contact_messages CASCADE;
DROP TABLE IF EXISTS newsletter_subscriptions CASCADE;
@@ -0,0 +1,61 @@
-- Create contact_messages table
CREATE TABLE IF NOT EXISTS contact_messages (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL,
subject VARCHAR(255) NOT NULL,
message TEXT NOT NULL,
ip_address VARCHAR(45),
user_agent TEXT,
is_read BOOLEAN DEFAULT FALSE,
read_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMP WITH TIME ZONE
);
-- Create index for faster lookups on email and created_at
CREATE INDEX IF NOT EXISTS idx_contact_messages_email ON contact_messages(email);
CREATE INDEX IF NOT EXISTS idx_contact_messages_created_at ON contact_messages(created_at);
CREATE INDEX IF NOT EXISTS idx_contact_messages_is_read ON contact_messages(is_read);
-- Create newsletter_subscriptions table
CREATE TABLE IF NOT EXISTS newsletter_subscriptions (
id SERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMP WITH TIME ZONE
);
-- Create index for faster lookups on email and is_active
CREATE INDEX IF NOT EXISTS idx_newsletter_subscriptions_email ON newsletter_subscriptions(email);
CREATE INDEX IF NOT EXISTS idx_newsletter_subscriptions_is_active ON newsletter_subscriptions(is_active);
-- Create a function to update the updated_at column
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Create triggers to update updated_at columns
DO $$
BEGIN
-- For contact_messages
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'update_contact_messages_updated_at') THEN
CREATE TRIGGER update_contact_messages_updated_at
BEFORE UPDATE ON contact_messages
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
END IF;
-- For newsletter_subscriptions
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'update_newsletter_subscriptions_updated_at') THEN
CREATE TRIGGER update_newsletter_subscriptions_updated_at
BEFORE UPDATE ON newsletter_subscriptions
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
END IF;
END$$;
@@ -0,0 +1,5 @@
-- Remove verification code columns from password_resets table
ALTER TABLE password_resets
DROP COLUMN IF EXISTS verification_code,
DROP COLUMN IF EXISTS verification_code_expires_at,
DROP COLUMN IF EXISTS verification_attempts;
@@ -0,0 +1,5 @@
-- Add verification code columns to password_resets table
ALTER TABLE password_resets
ADD COLUMN verification_code VARCHAR(6) DEFAULT NULL,
ADD COLUMN verification_code_expires_at TIMESTAMP WITH TIME ZONE DEFAULT NULL,
ADD COLUMN verification_attempts INT NOT NULL DEFAULT 0;
@@ -0,0 +1,3 @@
-- Remove source column and its index
DROP INDEX IF EXISTS idx_contact_messages_source;
ALTER TABLE contact_messages DROP COLUMN IF EXISTS source;
@@ -0,0 +1,8 @@
-- Add source column to contact_messages to differentiate between contact page and sponsor page
ALTER TABLE contact_messages ADD COLUMN IF NOT EXISTS source VARCHAR(50) DEFAULT 'contact';
-- Create index for faster filtering by source
CREATE INDEX IF NOT EXISTS idx_contact_messages_source ON contact_messages(source);
-- Add comment for clarity
COMMENT ON COLUMN contact_messages.source IS 'Source of the message: contact, sponsor, etc.';
@@ -0,0 +1,6 @@
-- Add preferences JSONB column to newsletter_subscriptions
ALTER TABLE newsletter_subscriptions
ADD COLUMN IF NOT EXISTS preferences JSONB DEFAULT '{}'::jsonb;
-- Update trigger exists from previous migration to keep updated_at behavior (no-op here)
@@ -0,0 +1,11 @@
-- Add SMTP configuration columns to settings table
ALTER TABLE settings
ADD COLUMN IF NOT EXISTS smtp_host VARCHAR(255) NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS smtp_port INTEGER NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS smtp_user VARCHAR(255) NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS smtp_password TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS smtp_from VARCHAR(255) NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS smtp_from_name VARCHAR(255) NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS smtp_encryption VARCHAR(16) NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS smtp_auth BOOLEAN NOT NULL DEFAULT FALSE,
ADD COLUMN IF NOT EXISTS smtp_skip_verify BOOLEAN NOT NULL DEFAULT FALSE;
@@ -0,0 +1,4 @@
-- Add optional email and phone columns to players
ALTER TABLE players
ADD COLUMN IF NOT EXISTS email VARCHAR(255),
ADD COLUMN IF NOT EXISTS phone VARCHAR(50);
@@ -0,0 +1,6 @@
-- Add 'featured' flag to articles
ALTER TABLE articles
ADD COLUMN IF NOT EXISTS featured BOOLEAN NOT NULL DEFAULT FALSE;
-- Optional: index to query featured quickly together with published
CREATE INDEX IF NOT EXISTS idx_articles_featured_published ON articles (featured, published);
@@ -0,0 +1,3 @@
DROP TABLE IF EXISTS blog_notifications CASCADE;
DROP TABLE IF EXISTS match_notifications CASCADE;
DROP TABLE IF EXISTS newsletter_sent_log CASCADE;
@@ -0,0 +1,40 @@
-- Add newsletter sent tracking table
CREATE TABLE IF NOT EXISTS newsletter_sent_log (
id SERIAL PRIMARY KEY,
newsletter_type VARCHAR(50) NOT NULL, -- weekly|match_reminder|match_result|blog_release
subject VARCHAR(500),
content_ids TEXT, -- JSON array of article/match IDs included
recipients_count INT DEFAULT 0,
sent_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_newsletter_sent_log_type ON newsletter_sent_log(newsletter_type);
CREATE INDEX IF NOT EXISTS idx_newsletter_sent_log_sent_at ON newsletter_sent_log(sent_at);
-- Add match notification tracking to prevent duplicate alerts
CREATE TABLE IF NOT EXISTS match_notifications (
id SERIAL PRIMARY KEY,
match_id VARCHAR(255) NOT NULL, -- External FACR match ID
notification_type VARCHAR(50) NOT NULL, -- reminder_48h|reminder_day|result
sent_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
recipients_count INT DEFAULT 0,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
UNIQUE(match_id, notification_type)
);
CREATE INDEX IF NOT EXISTS idx_match_notifications_match_id ON match_notifications(match_id);
CREATE INDEX IF NOT EXISTS idx_match_notifications_sent_at ON match_notifications(sent_at);
-- Add blog notification tracking to prevent duplicate alerts
CREATE TABLE IF NOT EXISTS blog_notifications (
id SERIAL PRIMARY KEY,
article_id INT NOT NULL,
sent_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
recipients_count INT DEFAULT 0,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
UNIQUE(article_id)
);
CREATE INDEX IF NOT EXISTS idx_blog_notifications_article_id ON blog_notifications(article_id);
CREATE INDEX IF NOT EXISTS idx_blog_notifications_sent_at ON blog_notifications(sent_at);
@@ -0,0 +1,3 @@
-- Remove display_order column and index
DROP INDEX IF EXISTS idx_competition_aliases_display_order;
ALTER TABLE competition_aliases DROP COLUMN IF EXISTS display_order;
@@ -0,0 +1,5 @@
-- Add display_order column to competition_aliases table for custom sorting
ALTER TABLE competition_aliases ADD COLUMN IF NOT EXISTS display_order INTEGER DEFAULT 0;
-- Create index for better query performance
CREATE INDEX IF NOT EXISTS idx_competition_aliases_display_order ON competition_aliases(display_order);
@@ -0,0 +1,14 @@
-- Rollback contact and map fields from settings table
ALTER TABLE settings
DROP COLUMN IF EXISTS contact_address,
DROP COLUMN IF EXISTS contact_city,
DROP COLUMN IF EXISTS contact_zip,
DROP COLUMN IF EXISTS contact_country,
DROP COLUMN IF EXISTS contact_phone,
DROP COLUMN IF EXISTS contact_email,
DROP COLUMN IF EXISTS location_latitude,
DROP COLUMN IF EXISTS location_longitude,
DROP COLUMN IF EXISTS map_zoom_level,
DROP COLUMN IF EXISTS map_style,
DROP COLUMN IF EXISTS show_map_on_homepage;
@@ -0,0 +1,28 @@
-- Add contact and map fields to settings table
-- These fields enable location display and contact information on the website
ALTER TABLE settings
ADD COLUMN IF NOT EXISTS contact_address VARCHAR(255),
ADD COLUMN IF NOT EXISTS contact_city VARCHAR(100),
ADD COLUMN IF NOT EXISTS contact_zip VARCHAR(20),
ADD COLUMN IF NOT EXISTS contact_country VARCHAR(100),
ADD COLUMN IF NOT EXISTS contact_phone VARCHAR(50),
ADD COLUMN IF NOT EXISTS contact_email VARCHAR(255),
ADD COLUMN IF NOT EXISTS location_latitude DOUBLE PRECISION,
ADD COLUMN IF NOT EXISTS location_longitude DOUBLE PRECISION,
ADD COLUMN IF NOT EXISTS map_zoom_level INT DEFAULT 15,
ADD COLUMN IF NOT EXISTS map_style VARCHAR(50),
ADD COLUMN IF NOT EXISTS show_map_on_homepage BOOLEAN DEFAULT FALSE;
-- Add comments for documentation
COMMENT ON COLUMN settings.contact_address IS 'Street address of the club/stadium';
COMMENT ON COLUMN settings.contact_city IS 'City name';
COMMENT ON COLUMN settings.contact_zip IS 'Postal/ZIP code';
COMMENT ON COLUMN settings.contact_country IS 'Country name';
COMMENT ON COLUMN settings.contact_phone IS 'Contact phone number';
COMMENT ON COLUMN settings.contact_email IS 'Contact email address';
COMMENT ON COLUMN settings.location_latitude IS 'Geographic latitude for map display';
COMMENT ON COLUMN settings.location_longitude IS 'Geographic longitude for map display';
COMMENT ON COLUMN settings.map_zoom_level IS 'Default zoom level for map (1-20)';
COMMENT ON COLUMN settings.map_style IS 'Map style: default, dark, or satellite';
COMMENT ON COLUMN settings.show_map_on_homepage IS 'Whether to display map on homepage';
@@ -0,0 +1,5 @@
ALTER TABLE articles
DROP COLUMN IF EXISTS youtube_video_thumbnail,
DROP COLUMN IF EXISTS youtube_video_url,
DROP COLUMN IF EXISTS youtube_video_title,
DROP COLUMN IF EXISTS youtube_video_id;
@@ -0,0 +1,5 @@
ALTER TABLE articles
ADD COLUMN youtube_video_id VARCHAR(64),
ADD COLUMN youtube_video_title TEXT,
ADD COLUMN youtube_video_url TEXT,
ADD COLUMN youtube_video_thumbnail TEXT;
@@ -0,0 +1,5 @@
ALTER TABLE articles
DROP COLUMN IF EXISTS gallery_photo_ids,
DROP COLUMN IF EXISTS gallery_album_url,
DROP COLUMN IF EXISTS gallery_album_id,
DROP COLUMN IF EXISTS og_image_url;
@@ -0,0 +1,5 @@
ALTER TABLE articles
ADD COLUMN IF NOT EXISTS og_image_url TEXT,
ADD COLUMN IF NOT EXISTS gallery_album_id TEXT,
ADD COLUMN IF NOT EXISTS gallery_album_url TEXT,
ADD COLUMN IF NOT EXISTS gallery_photo_ids TEXT;
@@ -0,0 +1,6 @@
-- Remove custom page and navigation fields from settings
ALTER TABLE settings
DROP COLUMN IF EXISTS custom_nav_json,
DROP COLUMN IF EXISTS show_about_in_nav,
DROP COLUMN IF EXISTS about_html;
@@ -0,0 +1,10 @@
-- Add custom page and navigation fields to settings
ALTER TABLE settings
ADD COLUMN IF NOT EXISTS about_html TEXT,
ADD COLUMN IF NOT EXISTS show_about_in_nav BOOLEAN DEFAULT TRUE,
ADD COLUMN IF NOT EXISTS custom_nav_json TEXT;
COMMENT ON COLUMN settings.about_html IS 'Rich HTML content for the unified custom club page';
COMMENT ON COLUMN settings.show_about_in_nav IS 'Controls whether the custom club page appears in main navigation';
COMMENT ON COLUMN settings.custom_nav_json IS 'Serialized custom navigation links managed via admin settings';
@@ -0,0 +1,22 @@
-- +goose Down
-- SQL in this section is executed when the migration is rolled back
-- Drop tables
DROP TABLE IF EXISTS social_links;
DROP TABLE IF EXISTS navigation_items;
-- Remove settings columns
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'settings') THEN
IF EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'settings' AND column_name = 'custom_navigation_enabled') THEN
ALTER TABLE settings DROP COLUMN custom_navigation_enabled;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'settings' AND column_name = 'show_social_in_nav') THEN
ALTER TABLE settings DROP COLUMN show_social_in_nav;
END IF;
END IF;
END $$;
@@ -0,0 +1,100 @@
-- +goose Up
-- SQL in this section is executed when the migration is applied
-- Create navigation_items table for managing navigation
CREATE TABLE IF NOT EXISTS navigation_items (
id SERIAL PRIMARY KEY,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMP WITH TIME ZONE,
-- Basic info
label VARCHAR(255) NOT NULL,
url TEXT,
icon VARCHAR(100),
-- Type: internal, external, dropdown, page
type VARCHAR(50) NOT NULL DEFAULT 'internal',
-- Page reference (for type=page): links to existing pages
page_type VARCHAR(100), -- e.g., 'blog', 'about', 'calendar', 'players', etc.
page_id INTEGER, -- optional reference to specific content ID
-- Visibility and display
visible BOOLEAN NOT NULL DEFAULT true,
display_order INTEGER NOT NULL DEFAULT 0,
-- Parent for dropdown menus
parent_id INTEGER REFERENCES navigation_items(id) ON DELETE CASCADE,
-- Target
target VARCHAR(20) DEFAULT '_self', -- _self, _blank
-- Styling
css_class VARCHAR(255),
-- Permissions
requires_auth BOOLEAN DEFAULT false,
requires_admin BOOLEAN DEFAULT false
);
-- Create index for ordering and parent relationships
CREATE INDEX idx_navigation_items_order ON navigation_items(display_order);
CREATE INDEX idx_navigation_items_parent ON navigation_items(parent_id);
CREATE INDEX idx_navigation_items_visible ON navigation_items(visible);
-- Create social_links table for managing social media
CREATE TABLE IF NOT EXISTS social_links (
id SERIAL PRIMARY KEY,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMP WITH TIME ZONE,
platform VARCHAR(50) NOT NULL, -- facebook, instagram, youtube, twitter, tiktok, etc.
url TEXT NOT NULL,
display_order INTEGER NOT NULL DEFAULT 0,
visible BOOLEAN NOT NULL DEFAULT true,
icon VARCHAR(100) -- optional custom icon
);
CREATE INDEX idx_social_links_order ON social_links(display_order);
CREATE INDEX idx_social_links_visible ON social_links(visible);
-- Insert default navigation items if table is empty
INSERT INTO navigation_items (label, type, page_type, display_order, visible)
SELECT * FROM (VALUES
('Domů', 'page', 'home', 0, true),
('O klubu', 'page', 'about', 1, true),
('Kalendář', 'page', 'calendar', 2, true),
('Zápasy', 'page', 'matches', 3, true),
('Aktivity', 'page', 'activities', 4, true),
('Hráči', 'page', 'players', 5, true),
('Tabulky', 'page', 'tables', 6, true),
('Články', 'page', 'blog', 7, true),
('Videa', 'page', 'videos', 8, true),
('Fotogalerie', 'page', 'gallery', 9, true),
('Sponzoři', 'page', 'sponsors', 10, true),
('Kontakt', 'page', 'contact', 11, true)
) AS default_nav(label, type, page_type, display_order, visible)
WHERE NOT EXISTS (SELECT 1 FROM navigation_items);
-- Migrate existing social links from settings if they exist
-- This will be handled in application code as we don't want to directly access settings table
-- Add navigation settings to settings table if not exists
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'settings') THEN
-- Add column for navigation customization enabled
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'settings' AND column_name = 'custom_navigation_enabled') THEN
ALTER TABLE settings ADD COLUMN custom_navigation_enabled BOOLEAN DEFAULT false;
END IF;
-- Add column for showing social links in nav
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'settings' AND column_name = 'show_social_in_nav') THEN
ALTER TABLE settings ADD COLUMN show_social_in_nav BOOLEAN DEFAULT true;
END IF;
END IF;
END $$;
@@ -0,0 +1,5 @@
-- Drop clothing table
DROP INDEX IF EXISTS idx_clothing_created_at;
DROP INDEX IF EXISTS idx_clothing_display_order;
DROP INDEX IF EXISTS idx_clothing_active;
DROP TABLE IF EXISTS clothing;
@@ -0,0 +1,20 @@
-- Create clothing table for merchandising items
CREATE TABLE IF NOT EXISTS clothing (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
description TEXT,
price DECIMAL(10, 2),
currency VARCHAR(10) DEFAULT '',
image_url VARCHAR(500),
url VARCHAR(500),
is_active BOOLEAN DEFAULT true,
display_order INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- Create index for active items and ordering
CREATE INDEX idx_clothing_active ON clothing(is_active);
CREATE INDEX idx_clothing_display_order ON clothing(display_order);
CREATE INDEX idx_clothing_created_at ON clothing(created_at);