working 3 tier approach

This commit is contained in:
2026-06-11 22:34:55 -07:00
parent 4482a30793
commit e5fef8b181
47 changed files with 1288 additions and 200 deletions
+6 -1
View File
@@ -1,8 +1,13 @@
import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [provideBrowserGlobalErrorListeners(), provideRouter(routes)],
providers: [
provideBrowserGlobalErrorListeners(),
provideRouter(routes),
provideHttpClient(),
],
};
+21 -15
View File
@@ -17,18 +17,24 @@
<!-- Donation tiers -->
<div class="donate-tiers">
<h3>Choose Your Level of Support</h3>
<div class="tiers-grid">
<div class="tier-card" *ngFor="let tier of tiers">
<div class="tier-icon">{{ tier.icon }}</div>
<div class="tier-amount">${{ tier.amount }}</div>
<div class="tier-period">per month</div>
<h4>{{ tier.name }}</h4>
<ul class="tier-benefits">
<li *ngFor="let benefit of tier.benefits">{{ benefit }}</li>
</ul>
<a href="#" class="btn btn-tier">Select {{ tier.name }}</a>
@if (loading) {
<div class="schedule-loading">Loading tiers&hellip;</div>
} @else if (tiers.length === 0) {
<div class="schedule-empty">No donation tiers available. Check back soon!</div>
} @else {
<div class="tiers-grid">
<div class="tier-card" *ngFor="let tier of tiers">
<div class="tier-icon">{{ tier.icon }}</div>
<div class="tier-amount">${{ tier.amount }}</div>
<div class="tier-period">per month</div>
<h4>{{ tier.name }}</h4>
<ul class="tier-benefits">
<li *ngFor="let benefit of tier.benefits">{{ benefit }}</li>
</ul>
<a href="#" class="btn btn-tier">Select {{ tier.name }}</a>
</div>
</div>
</div>
}
</div>
<!-- One-time donation -->
@@ -48,22 +54,22 @@
<div class="donate-why">
<div class="why-grid">
<div class="why-item">
<span class="why-icon">📻</span>
<span class="why-icon">&#x1F4FB;</span>
<strong>98.7 FM Signal</strong>
<p>Covering 3 mountain counties and the valleys between</p>
</div>
<div class="why-item">
<span class="why-icon">🎙️</span>
<span class="why-icon">&#x1F399;</span>
<strong>24/7 On-Air</strong>
<p>Over 15,000 hours of programming per year</p>
</div>
<div class="why-item">
<span class="why-icon">🌍</span>
<span class="why-icon">&#x1F30D;</span>
<strong>Worldwide Stream</strong>
<p>Accessible from every corner of the globe, free forever</p>
</div>
<div class="why-item">
<span class="why-icon">🎓</span>
<span class="why-icon">&#x1F393;</span>
<strong>Youth Programs</strong>
<p>Free radio training for 200+ local students each year</p>
</div>
+21 -53
View File
@@ -1,12 +1,8 @@
import { Component } from '@angular/core';
import { Component, inject, OnInit } from '@angular/core';
import { NgFor } from '@angular/common';
interface Tier {
name: string;
amount: number;
icon: string;
benefits: string[];
}
import { TierService } from '../services/tier.service';
import { Tier } from '../interfaces/tier';
@Component({
selector: 'app-donate',
@@ -15,50 +11,22 @@ interface Tier {
templateUrl: './donate.component.html',
styleUrl: './donate.component.scss',
})
export class DonateComponent {
readonly tiers: Tier[] = [
{
name: 'Seed',
amount: 5,
icon: '🌱',
benefits: [
'Digital thank-you card',
'Name listed on our website',
'Quarterly newsletter',
],
},
{
name: 'Stream',
amount: 15,
icon: '🎵',
benefits: [
'All Seed benefits',
'KMountain Flower pin',
'Priority show-request line',
'Annual station update call',
],
},
{
name: 'Ridge',
amount: 30,
icon: '🏔️',
benefits: [
'All Stream benefits',
'Official member patch',
'Exclusive show invitations',
'Annual station tour',
],
},
{
name: 'Summit',
amount: 60,
icon: '⭐',
benefits: [
'All Ridge benefits',
'Featured on the Summit Wall',
'Name on-air during anniversary special',
'Lifetime patron recognition',
],
},
];
export class DonateComponent implements OnInit {
private tierService = inject(TierService);
tiers: Tier[] = [];
loading = true;
ngOnInit(): void {
this.tierService.getTiers().subscribe({
next: (data) => {
this.tiers = data;
this.loading = false;
},
error: () => {
this.loading = false;
this.tiers = [];
},
});
}
}
+27 -17
View File
@@ -10,25 +10,35 @@
</p>
</div>
<div class="events-list">
<div class="event-card" *ngFor="let event of upcomingEvents">
<div class="event-date">
<span class="event-month">Jul</span>
<span class="event-day">12</span>
<span class="event-year">2026</span>
</div>
<div class="event-content">
<h3>{{ event.title }}</h3>
<p>{{ event.description }}</p>
<div class="event-meta">
<span class="event-location">
<span aria-hidden="true">📍</span> {{ event.location }}
</span>
<a href="#" class="event-rsvp">Learn More →</a>
@if (loading) {
<div class="schedule-loading">Loading events&hellip;</div>
} @else if (upcomingEvents.length === 0) {
<div class="schedule-empty">No upcoming events — check back later!</div>
} @else {
<div class="events-list">
<div class="event-card" *ngFor="let event of upcomingEvents">
<div class="event-date">
<span class="event-month">{{ event.date | date:'MMM' }}</span>
<span class="event-day">{{ event.date | date:'d' }}</span>
<span class="event-year">{{ event.date | date:'yyyy' }}</span>
</div>
<div class="event-content">
<h3>{{ event.title }}</h3>
<p>{{ event.description }}</p>
<div class="event-meta">
<span class="event-location">
<span aria-hidden="true">&#x1F4CD;</span> {{ event.location }}
</span>
@if (event.rsvp_url) {
<a [href]="event.rsvp_url" class="event-rsvp" target="_blank" rel="noopener">RSVP &rarr;</a>
} @else {
<a href="#" class="event-rsvp">Learn More &rarr;</a>
}
</div>
</div>
<div class="event-icon" aria-hidden="true">{{ event.icon }}</div>
</div>
<div class="event-icon" aria-hidden="true">{{ event.icon }}</div>
</div>
</div>
}
</div>
</section>
+23 -41
View File
@@ -1,50 +1,32 @@
import { Component } from '@angular/core';
import { NgFor } from '@angular/common';
import { Component, inject, OnInit } from '@angular/core';
import { DatePipe, NgFor } from '@angular/common';
interface Event {
date: string;
title: string;
description: string;
location: string;
icon: string;
}
import { EventService } from '../services/event.service';
import { Event } from '../interfaces/event';
@Component({
selector: 'app-events',
standalone: true,
imports: [NgFor],
imports: [DatePipe, NgFor],
templateUrl: './events.component.html',
styleUrl: './events.component.scss',
})
export class EventsComponent {
readonly upcomingEvents: Event[] = [
{
date: 'July 12, 2026',
title: 'Summer Sounds Festival',
description: 'A full day of live local music, community booths, and station meet-and-greet at the foothills park. Free admission — donations welcome.',
location: 'Foothills Community Park',
icon: '🎵',
},
{
date: 'August 5, 2026',
title: 'Annual Fundraiser Gala',
description: 'Our biggest fundraiser of the year! Enjoy dinner, silent auction, and performances from past guest artists.',
location: 'Mountain View Ballroom',
icon: '🎙️',
},
{
date: 'September 18, 2026',
title: 'Volunteer Open House',
description: 'Tour the studio, meet the team, and learn how you can help keep our free stream alive. New volunteers always welcome!',
location: 'KMTN Studio — 42 Pine St',
icon: '🤝',
},
{
date: 'October 30, 2026',
title: 'Harvest Moon Broadcast',
description: 'Special live broadcast from the harvest moon gathering. Featuring folk, bluegrass, and indigenous music.',
location: 'Ridgefield Amphitheater',
icon: '🌙',
},
];
export class EventsComponent implements OnInit {
private eventService = inject(EventService);
upcomingEvents: Event[] = [];
loading = true;
ngOnInit(): void {
this.eventService.getEvents(true).subscribe({
next: (data) => {
this.upcomingEvents = data;
this.loading = false;
},
error: () => {
this.loading = false;
this.upcomingEvents = [];
},
});
}
}
+10
View File
@@ -0,0 +1,10 @@
export interface Event {
id: number;
date: string; // YYYY-MM-DD from backend
title: string;
description: string;
location: string;
icon: string;
rsvp_url: string | null;
active: boolean;
}
+9
View File
@@ -0,0 +1,9 @@
export interface Program {
id: number;
day_of_week: number;
day_label: string;
time: string;
title: string;
host: string;
genre: string;
}
+8
View File
@@ -0,0 +1,8 @@
export interface Tier {
id: number;
name: string;
amount: number;
icon: string;
benefits: string[];
display_order: number;
}
+20 -14
View File
@@ -9,24 +9,30 @@
</p>
</div>
<div class="schedule-list">
<div class="schedule-day-label">Monday Friday</div>
<div class="schedule-table">
<div *ngFor="let program of programs" class="schedule-row">
<div class="schedule-time">{{ program.time }}</div>
<div class="schedule-divider"></div>
<div class="schedule-details">
<h4 class="schedule-title">{{ program.title }}</h4>
<div class="schedule-meta">
<span class="schedule-host">
<span aria-hidden="true">🎙️</span> {{ program.host }}
</span>
<span class="schedule-genre">{{ program.genre }}</span>
@if (loading) {
<div class="schedule-loading">Loading schedule&hellip;</div>
} @else if (programs.length === 0) {
<div class="schedule-empty">No programs scheduled. Check back soon!</div>
} @else {
<div class="schedule-list">
<div class="schedule-day-label">Monday Friday</div>
<div class="schedule-table">
<div *ngFor="let program of programs" class="schedule-row">
<div class="schedule-time">{{ program.time }}</div>
<div class="schedule-divider"></div>
<div class="schedule-details">
<h4 class="schedule-title">{{ program.title }}</h4>
<div class="schedule-meta">
<span class="schedule-host">
<span aria-hidden="true">&#x1F399;</span> {{ program.host }}
</span>
<span class="schedule-genre">{{ program.genre }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
}
<div class="schedule-weekend">
<div class="weekend-heading">
+21 -20
View File
@@ -1,12 +1,8 @@
import { Component } from '@angular/core';
import { Component, inject, OnInit } from '@angular/core';
import { NgFor } from '@angular/common';
interface Program {
time: string;
title: string;
host: string;
genre: string;
}
import { ProgramService } from '../services/program.service';
import { Program } from '../interfaces/program';
@Component({
selector: 'app-schedule',
@@ -15,17 +11,22 @@ interface Program {
templateUrl: './schedule.component.html',
styleUrl: './schedule.component.scss',
})
export class ScheduleComponent {
readonly programs: Program[] = [
{ time: '6:00 AM', title: 'Morning Mountain Mist', host: 'Diana Walsh', genre: 'Ambient / Nature' },
{ time: '8:00 AM', title: 'Community Roundup', host: 'Tom Breen', genre: 'News / Talk' },
{ time: '10:00 AM', title: 'Bluegrass Trails', host: 'Sarah Lynn', genre: 'Bluegrass' },
{ time: '12:00 PM', title: 'Lunchtime Jazz', host: 'Mike Darrow', genre: 'Jazz' },
{ time: '2:00 PM', title: 'Folk Roots Hour', host: 'Nadia Cole', genre: 'Folk' },
{ time: '4:00 PM', title: 'Afternoon Acoustics', host: 'Jen Reeves', genre: 'Acoustic' },
{ time: '6:00 PM', title: 'Evening Echoes', host: 'Carlos Mendez', genre: 'Indie / Alternative' },
{ time: '8:00 PM', title: 'Classical Mountains', host: 'Elena Cross', genre: 'Classical' },
{ time: '10:00 PM', title: 'Night Owl Session', host: 'DJ Kofi', genre: 'Electronic' },
{ time: '12:00 AM', title: 'Late Night Jazz', host: 'Mike Darrow', genre: 'Jazz' },
];
export class ScheduleComponent implements OnInit {
private programService = inject(ProgramService);
programs: Program[] = [];
loading = true;
ngOnInit(): void {
this.programService.getPrograms().subscribe({
next: (data) => {
this.programs = data;
this.loading = false;
},
error: () => {
this.loading = false;
this.programs = [];
},
});
}
}
+37
View File
@@ -0,0 +1,37 @@
import { Injectable, inject } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
import { environment } from '../../environments/environment';
import { Event } from '../interfaces/event';
@Injectable({
providedIn: 'root',
})
export class EventService {
private http = inject(HttpClient);
private baseUrl = `${environment.apiBaseUrl}/api/events`;
/** Fetch events, optionally filtered by active status. */
getEvents(active?: boolean): Observable<Event[]> {
let params = new HttpParams();
if (active !== undefined) params = params.set('active', String(active));
return this.http.get<Event[]>(this.baseUrl, { params });
}
getEvent(id: number): Observable<Event> {
return this.http.get<Event>(`${this.baseUrl}/${id}`);
}
createEvent(payload: Omit<Event, 'id'>): Observable<Event> {
return this.http.post<Event>(this.baseUrl, payload);
}
updateEvent(id: number, payload: Partial<Event>): Observable<Event> {
return this.http.put<Event>(`${this.baseUrl}/${id}`, payload);
}
deleteEvent(id: number): Observable<void> {
return this.http.delete<void>(`${this.baseUrl}/${id}`);
}
}
+37
View File
@@ -0,0 +1,37 @@
import { Injectable, inject } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
import { environment } from '../../environments/environment';
import { Program } from '../interfaces/program';
@Injectable({
providedIn: 'root',
})
export class ProgramService {
private http = inject(HttpClient);
private baseUrl = `${environment.apiBaseUrl}/api/programs`;
/** Fetch all programs, optionally filtered by day_of_week (1=Mon … 7=Sun). */
getPrograms(day?: number): Observable<Program[]> {
let params = new HttpParams();
if (day !== undefined) params = params.set('day', String(day));
return this.http.get<Program[]>(this.baseUrl, { params });
}
getProgram(id: number): Observable<Program> {
return this.http.get<Program>(`${this.baseUrl}/${id}`);
}
createProgram(payload: Omit<Program, 'id'>): Observable<Program> {
return this.http.post<Program>(this.baseUrl, payload);
}
updateProgram(id: number, payload: Partial<Program>): Observable<Program> {
return this.http.put<Program>(`${this.baseUrl}/${id}`, payload);
}
deleteProgram(id: number): Observable<void> {
return this.http.delete<void>(`${this.baseUrl}/${id}`);
}
}
+35
View File
@@ -0,0 +1,35 @@
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { environment } from '../../environments/environment';
import { Tier } from '../interfaces/tier';
@Injectable({
providedIn: 'root',
})
export class TierService {
private http = inject(HttpClient);
private baseUrl = `${environment.apiBaseUrl}/api/tiers`;
/** Fetch all tiers ordered by display_order. */
getTiers(): Observable<Tier[]> {
return this.http.get<Tier[]>(this.baseUrl);
}
getTier(id: number): Observable<Tier> {
return this.http.get<Tier>(`${this.baseUrl}/${id}`);
}
createTier(payload: Omit<Tier, 'id'>): Observable<Tier> {
return this.http.post<Tier>(this.baseUrl, payload);
}
updateTier(id: number, payload: Partial<Tier>): Observable<Tier> {
return this.http.put<Tier>(`${this.baseUrl}/${id}`, payload);
}
deleteTier(id: number): Observable<void> {
return this.http.delete<void>(`${this.baseUrl}/${id}`);
}
}
+5
View File
@@ -0,0 +1,5 @@
export const environment = {
production: true,
// In production the API is proxied through nginx at /api — no host needed.
apiBaseUrl: '',
};
+4
View File
@@ -0,0 +1,4 @@
export const environment = {
production: false,
apiBaseUrl: 'http://localhost:8000',
};