working 3 tier approach
This commit is contained in:
@@ -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(),
|
||||
],
|
||||
};
|
||||
|
||||
@@ -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…</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">📻</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">🎙</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">🌍</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">🎓</span>
|
||||
<strong>Youth Programs</strong>
|
||||
<p>Free radio training for 200+ local students each year</p>
|
||||
</div>
|
||||
|
||||
@@ -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 = [];
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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…</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">📍</span> {{ event.location }}
|
||||
</span>
|
||||
@if (event.rsvp_url) {
|
||||
<a [href]="event.rsvp_url" class="event-rsvp" target="_blank" rel="noopener">RSVP →</a>
|
||||
} @else {
|
||||
<a href="#" class="event-rsvp">Learn More →</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>
|
||||
|
||||
@@ -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 = [];
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export interface Program {
|
||||
id: number;
|
||||
day_of_week: number;
|
||||
day_label: string;
|
||||
time: string;
|
||||
title: string;
|
||||
host: string;
|
||||
genre: string;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export interface Tier {
|
||||
id: number;
|
||||
name: string;
|
||||
amount: number;
|
||||
icon: string;
|
||||
benefits: string[];
|
||||
display_order: number;
|
||||
}
|
||||
@@ -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…</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">🎙</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">
|
||||
|
||||
@@ -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 = [];
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export const environment = {
|
||||
production: true,
|
||||
// In production the API is proxied through nginx at /api — no host needed.
|
||||
apiBaseUrl: '',
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export const environment = {
|
||||
production: false,
|
||||
apiBaseUrl: 'http://localhost:8000',
|
||||
};
|
||||
Reference in New Issue
Block a user