transforms landing, about and schedule page content to data driven

This commit is contained in:
2026-07-01 03:24:30 +00:00
parent 2ba6204c3d
commit d2ab2240d5
16 changed files with 90 additions and 23 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
+6
View File
@@ -51,6 +51,12 @@ def _migrate_add_missing_columns(sync_conn):
("station_config", "play_store_icon_url", sa.String(500), ""),
("station_config", "play_store_url", sa.String(500), ""),
("station_config", "app_store_embed_html", sa.Text, ""),
# Page content columns
("station_config", "hero_subtitle", sa.Text, ""),
("station_config", "about_intro", sa.Text, ""),
("station_config", "about_donation_text", sa.Text, ""),
("station_config", "schedule_intro", sa.Text, ""),
("station_config", "events_intro", sa.Text, ""),
# Color theme columns
("station_config", "color_primary", sa.String(7), "#1a3a5c"),
("station_config", "color_primary_light", sa.String(7), "#2a5a8c"),
+7
View File
@@ -106,6 +106,13 @@ class StationConfig(Base):
play_store_url = Column(String(500), nullable=False, default="")
app_store_embed_html = Column(Text, nullable=False, default="")
# Page content (freeform text, admin-editable)
hero_subtitle = Column(Text, nullable=False, default="")
about_intro = Column(Text, nullable=False, default="")
about_donation_text = Column(Text, nullable=False, default="")
schedule_intro = Column(Text, nullable=False, default="")
events_intro = Column(Text, nullable=False, default="")
# Color theme — hex strings, e.g. "#1a3a5c"
# server_default is required so PostgreSQL emits a proper DEFAULT in ALTER TABLE DDL
color_primary = Column(String(7), nullable=False, server_default=text("'#1a3a5c'"), default="#1a3a5c")
+12
View File
@@ -78,6 +78,12 @@ class StationConfigResponse(BaseModel):
play_store_icon_url: str
play_store_url: str
app_store_embed_html: str
# Page content
hero_subtitle: str
about_intro: str
about_donation_text: str
schedule_intro: str
events_intro: str
# Color theme
color_primary: str
color_primary_light: str
@@ -119,6 +125,12 @@ class StationConfigUpdate(BaseModel):
play_store_icon_url: Optional[str] = None
play_store_url: Optional[str] = None
app_store_embed_html: Optional[str] = None
# Page content
hero_subtitle: Optional[str] = None
about_intro: Optional[str] = None
about_donation_text: Optional[str] = None
schedule_intro: Optional[str] = None
events_intro: Optional[str] = None
# Color theme
color_primary: Optional[str] = None
color_primary_light: Optional[str] = None
+6
View File
@@ -217,6 +217,12 @@ STATION_CONFIG: dict = {
"play_store_icon_url": "",
"play_store_url": "",
"app_store_embed_html": "",
# Page content
"hero_subtitle": "Your community-powered, nonprofit radio station — bringing music, stories, and connection to the mountain towns and beyond since 1987.",
"about_intro": "For nearly four decades, KMountain Flower Radio has been the voice of our community — a nonprofit, listener-supported radio station dedicated to music, local news, and the stories that bind mountain towns together.",
"about_donation_text": "As a 501(c)(3) nonprofit, every donation keeps our airwaves free and our signal strong. Members receive exclusive perks and the satisfaction of knowing they helped make this possible.",
"schedule_intro": "All times are local (Mountain Time). Every show is free to listen to live at 98.7 FM or via our online stream.",
"events_intro": "Come meet the team, enjoy live music, and support public radio in person. All events are open to the community — friends and family of listeners welcome.",
# Color theme defaults (match _variables.scss)
"color_primary": "#1a3a5c",
"color_primary_light": "#2a5a8c",
+2 -10
View File
@@ -4,11 +4,7 @@
<div class="about-intro">
<img [src]="resolveImageUrl(config().hero_icon_url)" alt="" class="about-flower-decor" aria-hidden="true">
<h2>About <span class="text-accent">{{ config().name_primary }} {{ config().name_secondary }}</span></h2>
<p class="section-intro">
For nearly four decades, {{ config().name_primary }} {{ config().name_secondary }} has been the voice of our
community — a nonprofit, listener-supported radio station dedicated to
music, local news, and the stories that bind mountain towns together.
</p>
<p class="section-intro">{{ config().about_intro }}</p>
</div>
<!-- Community Highlights Grid -->
@@ -80,11 +76,7 @@
<img [src]="resolveImageUrl(config().logo_url)" alt="">
</div>
<h3>Become a Member</h3>
<p class="section-intro">
As a {{ config().nonprofit_type }} nonprofit, every donation keeps our airwaves free and our
signal strong. Members receive exclusive perks and the satisfaction of
knowing they helped make this possible.
</p>
<p class="section-intro">{{ config().about_donation_text }}</p>
<a [href]="config().donation_url" target="_blank" rel="noopener noreferrer" class="btn">Join the Station</a>
</div>
}
@@ -184,6 +184,31 @@
</div>
</div>
<!-- Page Content -->
<div class="form-section">
<h3>Page Content</h3>
<div class="form-group">
<label for="hero_subtitle">Hero Subtitle</label>
<textarea id="hero_subtitle" [(ngModel)]="hero_subtitle" name="hero_subtitle" rows="3"></textarea>
</div>
<div class="form-group">
<label for="about_intro">About Page Intro</label>
<textarea id="about_intro" [(ngModel)]="about_intro" name="about_intro" rows="3"></textarea>
</div>
<div class="form-group">
<label for="about_donation_text">About Page — Donation CTA</label>
<textarea id="about_donation_text" [(ngModel)]="about_donation_text" name="about_donation_text" rows="3"></textarea>
</div>
<div class="form-group">
<label for="schedule_intro">Schedule Page Intro</label>
<textarea id="schedule_intro" [(ngModel)]="schedule_intro" name="schedule_intro" rows="2"></textarea>
</div>
<div class="form-group">
<label for="events_intro">Events Page Intro</label>
<textarea id="events_intro" [(ngModel)]="events_intro" name="events_intro" rows="2"></textarea>
</div>
</div>
<div class="form-actions">
<button type="button" class="btn btn-cancel" (click)="onClose()">Cancel</button>
<button type="submit" class="btn btn-save" [disabled]="saving">
@@ -43,6 +43,11 @@ export class AdminStationFormComponent implements OnChanges {
play_store_icon_url = '';
play_store_url = '';
app_store_embed_html = '';
hero_subtitle = '';
about_intro = '';
about_donation_text = '';
schedule_intro = '';
events_intro = '';
loading = false;
saving = false;
@@ -79,6 +84,11 @@ export class AdminStationFormComponent implements OnChanges {
this.play_store_icon_url = config.play_store_icon_url;
this.play_store_url = config.play_store_url;
this.app_store_embed_html = config.app_store_embed_html;
this.hero_subtitle = config.hero_subtitle;
this.about_intro = config.about_intro;
this.about_donation_text = config.about_donation_text;
this.schedule_intro = config.schedule_intro;
this.events_intro = config.events_intro;
}
/** Reset form with current service values. */
@@ -170,6 +180,11 @@ export class AdminStationFormComponent implements OnChanges {
play_store_icon_url: this.play_store_icon_url,
play_store_url: this.play_store_url,
app_store_embed_html: this.app_store_embed_html,
hero_subtitle: this.hero_subtitle,
about_intro: this.about_intro,
about_donation_text: this.about_donation_text,
schedule_intro: this.schedule_intro,
events_intro: this.events_intro,
};
try {
+1 -5
View File
@@ -3,11 +3,7 @@
<div class="events-header">
<img src="svg/small-flower.svg" alt="" class="events-flower" aria-hidden="true">
<h2>Upcoming <span class="text-accent">Events</span></h2>
<p class="section-intro">
Come meet the team, enjoy live music, and support public radio in
person. All events are open to the community — friends and family of
listeners welcome.
</p>
<p class="section-intro">{{ config().events_intro }}</p>
</div>
@if (events$ | async; as state) {
+2
View File
@@ -3,6 +3,7 @@ import { AsyncPipe, DatePipe, NgFor } from '@angular/common';
import { map, Observable, startWith } from 'rxjs';
import { EventService } from '../services/event.service';
import { StationConfigService } from '../services/station-config.service';
import { Event } from '../interfaces/event';
interface EventsState {
@@ -19,6 +20,7 @@ interface EventsState {
})
export class EventsComponent {
private eventService = inject(EventService);
readonly config = inject(StationConfigService).config;
/** Async pipe drives the template — guarantees change detection on every emission. */
readonly events$: Observable<EventsState> =
+1 -4
View File
@@ -17,10 +17,7 @@
<span class="hero-line1">{{ config().name_primary }}</span>
<span class="hero-line2 text-accent">{{ config().name_secondary }}</span>
</h1>
<p class="hero-subtitle">
Your community-powered, nonprofit radio station — bringing music,
stories, and connection to the mountain towns and beyond since {{ config().founded_year }}.
</p>
<p class="hero-subtitle">{{ config().hero_subtitle }}</p>
<p class="hero-subtitle hero-tagline">
{{ config().tagline }}
</p>
+6
View File
@@ -23,6 +23,12 @@ export interface StationConfig {
play_store_icon_url: string;
play_store_url: string;
app_store_embed_html: string;
// Page content
hero_subtitle: string;
about_intro: string;
about_donation_text: string;
schedule_intro: string;
events_intro: string;
// Color theme
color_primary: string;
color_primary_light: string;
+1 -4
View File
@@ -3,10 +3,7 @@
<div class="schedule-header">
<img [src]="resolveImageUrl(config().logo_url)" alt="" class="schedule-flower" aria-hidden="true">
<h2>Program <span class="text-accent">Schedule</span></h2>
<p class="section-intro">
All times are local (Mountain Time). Every show is free to listen to
live at {{ config().frequency }} or via our online stream.
</p>
<p class="section-intro">{{ config().schedule_intro }}</p>
</div>
@if (schedule$ | async; as state) {
@@ -32,6 +32,12 @@ const DEFAULT_CONFIG: StationConfig = {
play_store_icon_url: '',
play_store_url: '',
app_store_embed_html: '',
// Page content
hero_subtitle: 'Your community-powered, nonprofit radio station — bringing music, stories, and connection to the mountain towns and beyond since 1987.',
about_intro: 'For nearly four decades, KMountain Flower Radio has been the voice of our community — a nonprofit, listener-supported radio station dedicated to music, local news, and the stories that bind mountain towns together.',
about_donation_text: 'As a 501(c)(3) nonprofit, every donation keeps our airwaves free and our signal strong. Members receive exclusive perks and the satisfaction of knowing they helped make this possible.',
schedule_intro: 'All times are local (Mountain Time). Every show is free to listen to live at 98.7 FM or via our online stream.',
events_intro: 'Come meet the team, enjoy live music, and support public radio in person. All events are open to the community — friends and family of listeners welcome.',
// Color theme defaults (match _variables.scss)
color_primary: '#1a3a5c',
color_primary_light: '#2a5a8c',