CI Pipeline / backend-test (push) Successful in 10m9s
CI Pipeline / frontend-build (push) Successful in 30s
CI Pipeline / backend-lint (push) Successful in 9s
CI Pipeline / frontend-test (push) Successful in 30s
CI Pipeline / backend-test (pull_request) Successful in 10m30s
CI Pipeline / frontend-test (pull_request) Successful in 25s
CI Pipeline / frontend-build (pull_request) Successful in 41s
CI Pipeline / backend-lint (pull_request) Successful in 9s
CI Pipeline / quality-gate (push) Successful in 3s
CI Pipeline / quality-gate (pull_request) Successful in 3s
- Add <link rel="preload"> for hero background image (LCP element) in index.html - Remove blocking APP_INITIALIZER for config.json and station config loading - Config loads now fire asynchronously in App.ngOnInit() (browser only) - SSR/prerender renders instantly with default config — no network wait - Improves LCP, Time to First Byte, and initial paint for SEO
110 lines
4.1 KiB
TypeScript
110 lines
4.1 KiB
TypeScript
import { Injectable, inject, signal } from '@angular/core';
|
|
import { HttpClient } from '@angular/common/http';
|
|
import { firstValueFrom } from 'rxjs';
|
|
|
|
import { StationConfig } from '../interfaces/station-config';
|
|
import { getAppConfig } from './app-config.service';
|
|
import { ThemeService } from './theme.service';
|
|
|
|
/**
|
|
* Hardcoded defaults — used when the API is unavailable (dev fallback).
|
|
* Rendered immediately on the initial paint so the page is never blank.
|
|
*/
|
|
const DEFAULT_CONFIG: StationConfig = {
|
|
id: 0,
|
|
key: 'default',
|
|
callsign: 'KMTN',
|
|
name_primary: 'KMountain',
|
|
name_secondary: 'Flower Radio',
|
|
tagline: 'Community Radio — Est. 1987',
|
|
frequency: '98.7 FM',
|
|
founded_year: 1987,
|
|
nonprofit_type: '501(c)(3)',
|
|
ein: '84-XXXXXXX',
|
|
logo_url: 'svg/small-flower.svg',
|
|
hero_background_url: 'svg/mountain-landscape.svg',
|
|
hero_icon_url: 'svg/orange-flower.svg',
|
|
hero_divider_url: 'svg/mountain-divider.svg',
|
|
address: '42 Pine Street\nMountain View, CO 80424',
|
|
phone: '(970) 555-0198',
|
|
email: 'hello@kmountainflower.org',
|
|
website: 'kmountainflower.org',
|
|
donation_url: '',
|
|
stream_url: '',
|
|
stream_metadata_url: '',
|
|
play_store_icon_url: '',
|
|
play_store_url: '',
|
|
app_store_embed_html: '',
|
|
// Social media
|
|
social_facebook: '',
|
|
social_twitter: '',
|
|
social_instagram: '',
|
|
social_youtube: '',
|
|
// 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',
|
|
color_primary_dark: '#0e2440',
|
|
color_primary_muted: '#3a6a9c',
|
|
color_accent: '#e87a2e',
|
|
color_accent_light: '#f09a4e',
|
|
color_accent_dark: '#c85a1e',
|
|
color_accent_muted: '#f0a86a',
|
|
color_background: '#faf6f0',
|
|
color_text: '#3a3632',
|
|
color_success: '#4a9e4f',
|
|
color_danger: '#c83030',
|
|
color_info: '#3a8abf',
|
|
color_white: '#ffffff',
|
|
color_light: '#f0ebe3',
|
|
color_medium: '#8a8580',
|
|
color_black: '#1a1816',
|
|
};
|
|
|
|
@Injectable({
|
|
providedIn: 'root',
|
|
})
|
|
export class StationConfigService {
|
|
private http = inject(HttpClient);
|
|
private themeService = inject(ThemeService);
|
|
private baseUrl = `${getAppConfig().apiBaseUrl}/api/station-config`;
|
|
|
|
readonly config = signal<StationConfig>(DEFAULT_CONFIG);
|
|
|
|
/** Load station config from the API. Falls back to defaults on error. */
|
|
async load(): Promise<void> {
|
|
try {
|
|
const data = await firstValueFrom(this.http.get<StationConfig>(this.baseUrl));
|
|
this.config.set(data);
|
|
this.themeService.apply(data);
|
|
} catch {
|
|
// Keep the default config if the API is unavailable.
|
|
console.warn('StationConfigService: Failed to load config, using defaults.');
|
|
}
|
|
}
|
|
|
|
/** Update station config (admin only). */
|
|
async updateConfig(payload: Partial<StationConfig>): Promise<void> {
|
|
const data = await firstValueFrom(
|
|
this.http.put<StationConfig>(this.baseUrl, payload),
|
|
);
|
|
this.config.set(data);
|
|
this.themeService.apply(data);
|
|
}
|
|
|
|
/** Upload an image file for station branding (admin only). Returns the relative URL. */
|
|
async uploadImage(file: File): Promise<string> {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
const response = await firstValueFrom(
|
|
this.http.post<{ url: string }>(`${getAppConfig().apiBaseUrl}/api/storage/upload`, formData),
|
|
);
|
|
return response.url;
|
|
}
|
|
}
|