Build a digital signage system with remote content management, scheduling, and multi-screen display
* Preview is for reference only. Actual results may vary depending on the AI model, variable values, and tools used.
You are a senior full-stack engineer specialising in digital signage solutions for Malaysian SMEs (restaurants, retail shops, offices). Generate a complete, production-ready Digital Signage & Announcement Board system based on the following specification.
Company Name: {{company_name}}
Number of Screens: {{screen_count}}
Content Types: {{content_types}}
Default Slide Duration: {{default_duration}} seconds
═══════════════════════════════
1. ROLE & CONTEXT
═══════════════════════════════
Build a web-based digital signage platform that lets a Malaysian SME owner manage what plays on every TV or monitor in their business — promotions, menus, announcements, and branded content — from a single web dashboard. Non-technical staff must be able to upload an image and get it showing on a screen within 3 minutes. The system must keep playing even when the internet is temporarily unavailable.
═══════════════════════════════
2. TECH STACK
═══════════════════════════════
- Framework: Next.js 14 (App Router) + TypeScript
- Styling: Tailwind CSS + shadcn/ui (New York style, light theme)
- Backend / Database: Supabase (Postgres + Storage + Realtime + RLS)
- File Storage: Supabase Storage bucket (media-library)
- Real-time sync: Supabase Realtime channels (instant slide updates)
- Drag-and-drop: @dnd-kit/sortable
- File uploads: react-dropzone
- Deployment: Vercel
═══════════════════════════════
3. DATABASE SCHEMA (full SQL)
═══════════════════════════════
Generate complete CREATE TABLE statements with column comments, constraints, and indexes for every table below.
Table: screens
- id uuid PRIMARY KEY DEFAULT gen_random_uuid()
- name text NOT NULL -- e.g. 'Lobby TV'
- location text -- e.g. 'Ground Floor Entrance'
- resolution text DEFAULT '1920x1080'
- orientation text DEFAULT 'landscape' CHECK (orientation IN ('landscape','portrait'))
- registration_code text UNIQUE NOT NULL -- 6-char uppercase alphanumeric
- is_registered boolean DEFAULT false
- last_seen_at timestamptz
- status text DEFAULT 'offline' CHECK (status IN ('online','offline','error'))
- created_at timestamptz DEFAULT now()
- Indexes: registration_code, status
Table: playlists
- id uuid PRIMARY KEY DEFAULT gen_random_uuid()
- name text NOT NULL
- screen_id uuid REFERENCES screens(id) ON DELETE CASCADE
- is_active boolean DEFAULT false
- created_at timestamptz DEFAULT now()
- Index: screen_id
Table: slides
- id uuid PRIMARY KEY DEFAULT gen_random_uuid()
- playlist_id uuid REFERENCES playlists(id) ON DELETE CASCADE
- content_type text NOT NULL CHECK (content_type IN ('image','video','html','text','menu','template'))
- content_url text -- Supabase Storage public URL
- html_content text -- for HTML slide type
- text_content text -- for plain text announcement
- text_overlay jsonb -- {text, position, font_size, color, bg_color}
- duration_seconds integer DEFAULT 10 CHECK (duration_seconds > 0)
- sort_order integer DEFAULT 0
- schedule_type text DEFAULT 'always' CHECK (schedule_type IN ('always','weekdays','weekend','date_range','time_range','custom'))
- schedule_config jsonb -- {days:[], start_date, end_date, start_time, end_time}
- is_active boolean DEFAULT true
- created_at timestamptz DEFAULT now()
- Indexes: playlist_id, sort_order, is_active
Table: media_library
- id uuid PRIMARY KEY DEFAULT gen_random_uuid()
- file_name text NOT NULL
- file_url text NOT NULL
- file_type text NOT NULL CHECK (file_type IN ('image','video'))
- file_size_bytes bigint
- thumbnail_url text
- uploaded_by text
- created_at timestamptz DEFAULT now()
Table: announcement_overrides
- id uuid PRIMARY KEY DEFAULT gen_random_uuid()
- title text NOT NULL
- message text NOT NULL
- priority integer DEFAULT 1 -- higher number = higher priority
- target_screens uuid[] -- NULL means broadcast to all screens
- background_color text DEFAULT '#DC2626'
- text_color text DEFAULT '#FFFFFF'
- starts_at timestamptz DEFAULT now()
- ends_at timestamptz NOT NULL
- is_active boolean DEFAULT true
- created_at timestamptz DEFAULT now()
- Indexes: is_active, ends_at
Table: screen_heartbeats (retain last 24 hours only)
- id uuid PRIMARY KEY DEFAULT gen_random_uuid()
- screen_id uuid REFERENCES screens(id) ON DELETE CASCADE
- recorded_at timestamptz DEFAULT now()
- ip_address text
- Indexes: screen_id, recorded_at
Enable RLS on all tables:
- anon role: SELECT only active/published records
- authenticated role (admin): full CRUD
- All RLS policies must use (select auth.uid()) — never bare auth.uid() — to prevent per-row function overhead
═══════════════════════════════
4. CORE FEATURES
═══════════════════════════════
4.1 Screen Registration & Pairing
- Admin dashboard generates a unique 6-character registration code
- TV opens /display?code=ABC123 — system auto-pairs the screen and begins playback
- On successful pairing, screen enters fullscreen mode (Fullscreen API) and hides browser chrome
- Registration code is single-use; invalidated immediately after pairing
- Admin can regenerate a new code for any screen at any time
4.2 Media Content Management
- Upload images (JPG/PNG/GIF/WebP, max 10 MB) and videos (MP4, max 200 MB) to Supabase Storage
- Auto-generate video thumbnails using canvas frame extraction
- Media library grid view with search and file-type filter
- Drag a media item onto the playlist to create a new slide instantly
4.3 Playlist Builder
- Drag-and-drop slide reordering using @dnd-kit/sortable
- Per-slide duration setting (5–300 seconds)
- Slide types: Image, Video, HTML (custom code), Text Announcement, Menu Board, Template
- Text overlay tool: 9-point grid positioning, font size, text color, background color
- Live 16:9 preview pane updates as you edit
4.4 Slide Scheduling
- Always — plays at all times
- Weekdays — Monday through Friday only
- Weekends — Saturday and Sunday only
- Date Range — set start and end dates (ideal for promotions)
- Time Range — specify daily time window (e.g. Breakfast Menu 07:00–10:30)
- Custom — choose specific weekdays plus optional time window
- Out-of-schedule slides are silently skipped; no error is shown on screen
4.5 Emergency Announcement Overrides
- Admin publishes a scrolling banner or full-screen overlay message
- Choose background color: Red (alert), Yellow (notice), Green (info)
- Set auto-expiry time (minimum 5 minutes, maximum 7 days)
- Pushed instantly to all matching screens via Supabase Realtime
- Supports targeting a subset of screens or broadcasting to all
4.6 Multi-Screen Management
- Screen grid view: online status indicator, currently playing slide title, last heartbeat timestamp
- Each screen binds to its own playlist (e.g. Menu Board vs. Lobby Promo Screen)
- Remote refresh: send Realtime command to reload the display page
- Bulk action: assign a new playlist to multiple screens simultaneously
4.7 Display Player Page (/display)
- Full-screen auto-advancing slideshow with configurable fade/slide transitions
- Sends heartbeat POST /api/heartbeat every 60 seconds
- Caches playlist JSON and media URLs in IndexedDB for offline resilience
- Polls Supabase every 30 seconds for announcement overrides and playlist changes
- Subscribes to Supabase Realtime channel for instant updates
- Evaluates schedule rules client-side on every slide transition
- Shows clock widget in corner when no slides are scheduled for current time
4.8 Built-in Slide Templates
Provide 4 ready-to-use templates with editable fields:
1. Menu Board — dark background, dish name + price grid, 2-column layout
2. Promotion Banner — yellow background, large headline, optional countdown timer
3. Company Announcement — white background, logo area, body text
4. Clock & Weather Widget — real-time clock + Malaysia weather via OpenWeatherMap API
═══════════════════════════════
5. ADMIN DASHBOARD — PAGE STRUCTURE
═══════════════════════════════
Generate all pages and components listed below:
/admin — Dashboard
- Stat cards: Online Screens / Total, Active Playlists, Media Uploads This Month, Active Announcements
- Screen status grid: name, online dot, currently playing title, last heartbeat time
- Upcoming schedule changes in next 7 days (slides going live or expiring)
/admin/screens — Screen Management
- Screen list + Add Screen button (generates registration code shown in a modal)
- Screen detail side sheet: name, location, resolution, bound playlist selector, remote refresh button
- Offline warning badge on screens silent for more than 5 minutes
/admin/playlists — Playlist Management
- Playlist list: screen name, slide count, total duration
- Playlist editor: left panel drag-and-drop slide sequence, right panel live preview
/admin/slides/[id] — Slide Editor
- Content type selector (Image / Video / HTML / Text / Template)
- Text overlay designer (9-point positioning, font size, color pickers)
- Schedule configurator (calendar + time range picker)
- 16:9 live preview pane
/admin/media — Media Library
- Drag-and-drop upload zone + click-to-browse
- Image/video grid with search and type filter
- Bulk delete with reference-check warning
/admin/announcements — Emergency Announcements
- Create announcement form: title, message, color theme, target screens, expiry datetime
- Active announcements list with early-termination button
═══════════════════════════════
6. UI / UX SPECIFICATIONS
═══════════════════════════════
- Primary palette: dark navy nav (#0F172A) + white content area
- Accent: Yellow (#FCD34D) for primary action buttons and online status indicators
- Error / offline: Red (#DC2626); Success / online: Green (#16A34A)
- Card padding: 24px; grid gap: 16px
- Typography: Inter (Latin) + Noto Sans SC (Chinese characters)
- Admin dashboard minimum width: 1280px; display player: full-screen 1920x1080
- Use shadcn/ui components throughout: Button, Input, Select, Badge, Card, Table, Sheet, Dialog, Tabs, Separator, Skeleton, Tooltip
- All conditional class merging via cn() utility
- Loading states: Skeleton components while data fetches
- Empty states: friendly illustration + action button
═══════════════════════════════
7. BUSINESS RULES & BACKGROUND JOBS
═══════════════════════════════
1. Heartbeat: display page POSTs to /api/heartbeat every 60 s; server updates last_seen_at and sets status = 'online'
2. Offline detection: if last_seen_at is more than 5 minutes ago, a Supabase cron job (pg_cron) sets status = 'offline'; dashboard shows red warning badge
3. Announcement priority: higher priority integer wins; ties broken by starts_at ascending
4. Offline resilience: display page stores full playlist + media URLs in IndexedDB; continues playback on network loss
5. Schedule evaluation: computed client-side on every slide advance using current device time; out-of-schedule slides are skipped without error
6. Playlist hot-swap: when admin changes a screen's active playlist, Realtime notifies the display; transition happens after the current slide finishes
7. Registration code security: code invalidated on first successful pairing; admin regenerates via dashboard
8. Media deletion guard: before deleting a media file, query slides table for references; warn admin and require confirmation before proceeding
9. Heartbeat log retention: delete screen_heartbeats rows older than 24 hours via pg_cron daily job
10. Content caching: display page pre-fetches and caches all media assets for the active playlist on load
═══════════════════════════════
8. API ROUTES
═══════════════════════════════
POST /api/register — screen pairing (receives code, returns screen_id)
POST /api/heartbeat — update last_seen_at for a screen
GET /api/playlist/[id] — return active playlist + slides for a screen_id
GET /api/announcements/[id] — active announcement overrides for a screen
POST /api/media/upload — upload file to Supabase Storage, insert media_library row
═══════════════════════════════
9. IMPLEMENTATION ORDER
═══════════════════════════════
Generate code in this sequence so each phase is independently runnable:
Phase 1 — Database SQL migrations + Supabase Storage bucket configuration + RLS policies
Phase 2 — /display full-screen player page (heartbeat + IndexedDB caching + schedule evaluation)
Phase 3 — Admin layout + /admin/screens page (registration code generation + screen grid)
Phase 4 — /admin/playlists playlist editor (drag-and-drop slide ordering + live preview)
Phase 5 — /admin/media media library (upload + grid + delete guard)
Phase 6 — /admin/announcements emergency override system + Supabase Realtime push
Phase 7 — /admin dashboard stats + schedule timeline
Every file must be complete — no truncation, no placeholder comments saying 'add logic here'. Include the file path as a comment at the top of each code block.