transforms landing, about and schedule page content to data driven
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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"),
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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> =
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user