Retires Support/donation/tiers page in favor of simple configurable link

This commit is contained in:
2026-06-23 00:30:47 +00:00
parent 86fbfa56bf
commit 9b95bef9c6
34 changed files with 124 additions and 924 deletions
+3 -1
View File
@@ -74,6 +74,7 @@
}
<!-- Donation CTA -->
@if (config().donation_url) {
<div class="about-cta">
<div class="about-flower-divider" aria-hidden="true">
<img [src]="resolveImageUrl(config().logo_url)" alt="">
@@ -84,7 +85,8 @@
signal strong. Members receive exclusive perks and the satisfaction of
knowing they helped make this possible.
</p>
<a routerLink="/donate" class="btn">Join the Station</a>
<a [href]="config().donation_url" target="_blank" rel="noopener noreferrer" class="btn">Join the Station</a>
</div>
}
</div>
</section>
+1 -2
View File
@@ -1,5 +1,4 @@
import { Component, inject } from '@angular/core';
import { RouterLink } from '@angular/router';
import { AsyncPipe } from '@angular/common';
import { BehaviorSubject } from 'rxjs';
@@ -15,7 +14,7 @@ import { resolveImageUrl } from '../services/app-config.service';
@Component({
selector: 'app-about',
standalone: true,
imports: [RouterLink, AsyncPipe],
imports: [AsyncPipe],
templateUrl: './about.component.html',
styleUrl: './about.component.scss',
})
@@ -130,6 +130,15 @@
</div>
</div>
<!-- Donation -->
<div class="form-section">
<h3>Donation</h3>
<div class="form-group">
<label for="donation_url">External Donation URL</label>
<input id="donation_url" type="url" [(ngModel)]="donation_url" name="donation_url" placeholder="https://example.com/donate">
</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">
@@ -37,6 +37,7 @@ export class AdminStationFormComponent implements OnChanges {
phone = '';
email = '';
website = '';
donation_url = '';
loading = false;
saving = false;
@@ -67,6 +68,7 @@ export class AdminStationFormComponent implements OnChanges {
this.phone = config.phone;
this.email = config.email;
this.website = config.website;
this.donation_url = config.donation_url;
}
/** Reset form with current service values. */
@@ -152,6 +154,7 @@ export class AdminStationFormComponent implements OnChanges {
phone: this.phone,
email: this.email,
website: this.website,
donation_url: this.donation_url,
};
try {
@@ -1,50 +0,0 @@
<div class="form-overlay" (click)="onClose()">
<div class="form-dialog" (click)="$event.stopPropagation()">
<div class="form-header">
<h2>{{ id ? 'Edit Tier' : 'Add Tier' }}</h2>
<button type="button" class="btn-close" (click)="onClose()">&times;</button>
</div>
@if (error) {
<div class="form-error" role="alert">{{ error }}</div>
}
<form (ngSubmit)="onSubmit()" class="admin-form">
<div class="form-row">
<div class="form-group">
<label for="name">Name</label>
<input id="name" type="text" [(ngModel)]="name" name="name" placeholder="Tier name" required [class.is-invalid]="submitted && !name">
</div>
<div class="form-group">
<label for="amount">Amount ($)</label>
<input id="amount" type="number" step="0.01" min="0" [(ngModel)]="amount" name="amount" required [class.is-invalid]="submitted && amount <= 0">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="icon">Icon</label>
<input id="icon" type="text" [(ngModel)]="icon" name="icon" placeholder="⭐" maxlength="10">
</div>
<div class="form-group">
<label for="display_order">Display Order</label>
<input id="display_order" type="number" min="1" [(ngModel)]="display_order" name="display_order" required>
</div>
</div>
<div class="form-group">
<label for="benefits">Benefits (comma-separated)</label>
<textarea id="benefits" [(ngModel)]="benefits_text" name="benefits" rows="3" placeholder="Benefit 1, Benefit 2, Benefit 3"></textarea>
</div>
<div class="form-actions">
<button type="button" class="btn btn-cancel" (click)="onClose()">Cancel</button>
<button type="submit" class="btn btn-save" [disabled]="loading">
{{ loading ? 'Saving…' : (id ? 'Update' : 'Create') }}
</button>
</div>
</form>
</div>
</div>
@@ -1,129 +0,0 @@
@use '../../styles/variables' as *;
@use '../../styles/mixins' as *;
.form-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
@include flex-center;
z-index: $z-modal;
@include fade-in;
}
.form-dialog {
background: $neutral-white;
border-radius: $radius-lg;
padding: $spacing-xl;
width: 90%;
max-width: 560px;
box-shadow: $shadow-xl;
}
.form-header {
@include flex-between;
margin-bottom: $spacing-lg;
h2 {
font-family: $font-heading;
color: $primary-blue;
margin: 0;
font-size: 1.4rem;
}
}
.btn-close {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: $neutral-medium;
line-height: 1;
padding: $spacing-xs;
&:hover {
color: $neutral-dark;
}
}
.form-error {
background: rgba($danger-red, 0.1);
color: $danger-red;
padding: $spacing-sm $spacing-md;
border-radius: $radius-md;
font-size: 0.875rem;
margin-bottom: $spacing-md;
text-align: center;
}
.admin-form {
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: $spacing-md;
}
.form-group {
margin-bottom: $spacing-md;
label {
display: block;
font-size: 0.875rem;
font-weight: 600;
color: $neutral-dark;
margin-bottom: $spacing-xs;
}
input, select, textarea {
width: 100%;
padding: $spacing-sm $spacing-md;
border: 1px solid $neutral-light;
border-radius: $radius-md;
font-size: 0.95rem;
transition: border-color $transition-fast;
box-sizing: border-box;
&:focus {
outline: none;
border-color: $primary-blue;
box-shadow: 0 0 0 3px rgba($primary-blue, 0.15);
}
&.is-invalid {
border-color: $danger-red;
box-shadow: 0 0 0 3px rgba($danger-red, 0.15);
}
}
}
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: $spacing-sm;
margin-top: $spacing-md;
}
.btn-save {
@include button-style($primary-blue, $neutral-white, $primary-blue-light);
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
.btn-cancel {
background: $neutral-light;
color: $neutral-dark;
border: none;
border-radius: $radius-md;
padding: $spacing-sm $spacing-lg;
font-size: 0.95rem;
cursor: pointer;
transition: background $transition-fast;
&:hover {
background: $neutral-medium;
color: $neutral-white;
}
}
-109
View File
@@ -1,109 +0,0 @@
import { Component, EventEmitter, inject, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { firstValueFrom } from 'rxjs';
import { Tier } from '../interfaces/tier';
import { TierService } from '../services/tier.service';
@Component({
selector: 'app-admin-tier-form',
standalone: true,
imports: [CommonModule, FormsModule],
templateUrl: './admin-tier-form.component.html',
styleUrl: './admin-tier-form.component.scss',
})
export class AdminTierFormComponent implements OnChanges {
private tierService = inject(TierService);
@Input() editItem: Tier | null = null;
@Output() saved = new EventEmitter<void>();
@Output() closed = new EventEmitter<void>();
id: number | null = null;
name = '';
amount = 0;
icon = '⭐';
benefits_text = '';
display_order = 1;
loading = false;
error = '';
submitted = false;
ngOnChanges(changes: SimpleChanges): void {
if (changes['editItem'] && this.editItem) {
this.edit(this.editItem);
}
}
edit(tier: Tier): void {
this.id = tier.id;
this.name = tier.name;
this.amount = tier.amount;
this.icon = tier.icon;
this.benefits_text = tier.benefits.join(', ');
this.display_order = tier.display_order;
}
formatError(err: any): string {
if (err?.error?.detail) return err.error.detail;
if (err?.error?.message) return err.error.message;
if (err?.message) return err.message;
return 'Failed to save. Please try again.';
}
reset(): void {
this.id = null;
this.name = '';
this.amount = 0;
this.icon = '⭐';
this.benefits_text = '';
this.display_order = 1;
this.error = '';
this.submitted = false;
}
onClose(): void {
this.closed.emit();
this.reset();
}
async onSubmit(): Promise<void> {
this.submitted = true;
if (!this.name || this.amount <= 0) {
this.error = 'Please fill in all required fields.';
return;
}
this.loading = true;
this.error = '';
const benefits = this.benefits_text
.split(',')
.map((b) => b.trim())
.filter((b) => b.length > 0);
const payload = {
name: this.name,
amount: this.amount,
icon: this.icon,
benefits: benefits,
display_order: this.display_order,
};
try {
if (this.id) {
await firstValueFrom(this.tierService.updateTier(this.id, payload));
} else {
await firstValueFrom(this.tierService.createTier(payload));
}
this.saved.emit();
this.reset();
} catch (err: any) {
this.error = this.formatError(err);
} finally {
this.loading = false;
}
}
}
+5 -49
View File
@@ -2,7 +2,7 @@
<div class="container">
<div class="admin-header">
<h1>Admin Dashboard</h1>
<p>Manage shows, events, tiers, history, team, and community content</p>
<p>Manage shows, events, history, team, and community content</p>
</div>
<!-- Tabs -->
@@ -17,11 +17,6 @@
[class.active]="activeTab() === 'events'"
(click)="activeTab.set('events')"
>Events</button>
<button
class="tab-btn"
[class.active]="activeTab() === 'tiers'"
(click)="activeTab.set('tiers')"
>Tiers</button>
<button
class="tab-btn"
[class.active]="activeTab() === 'history'"
@@ -130,46 +125,6 @@
}
}
<!-- Tiers Tab -->
@if (activeTab() === 'tiers') {
@if (tiers$ | async; as tiers) {
<div class="tab-content">
<div class="tab-toolbar">
<h2>Donation Tiers ({{ tiers.length }})</h2>
<button class="btn btn-add" (click)="openTierForm()">+ Add Tier</button>
</div>
<table class="admin-table">
<thead>
<tr>
<th>Order</th>
<th>Name</th>
<th>Amount</th>
<th>Benefits</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@for (tier of tiers; track tier.id) {
<tr>
<td>{{ tier.display_order }}</td>
<td>{{ tier.icon }} {{ tier.name }}</td>
<td>${{ tier.amount.toFixed(2) }}</td>
<td class="benefits-cell">{{ tier.benefits.join(', ') }}</td>
<td class="actions">
<button class="btn-icon btn-edit" (click)="editTier(tier)">Edit</button>
<button class="btn-icon btn-delete" (click)="deleteTier(tier.id)">Delete</button>
</td>
</tr>
} @empty {
<tr><td colspan="5" class="empty-row">No tiers yet. Click "Add Tier" to create one.</td></tr>
}
</tbody>
</table>
</div>
}
}
<!-- History Tab -->
@if (activeTab() === 'history') {
@if (historyEntries$ | async; as entries) {
@@ -336,6 +291,10 @@
<span class="config-label">Website</span>
<span class="config-value">{{ config.website }}</span>
</div>
<div class="config-row">
<span class="config-label">Donation URL</span>
<span class="config-value">{{ config.donation_url || '(not set)' }}</span>
</div>
</div>
</div>
}
@@ -349,9 +308,6 @@
@if (showEventForm) {
<app-admin-event-form [editItem]="editingEvent" (saved)="onEventSaved()" (closed)="onEventClosed()" />
}
@if (showTierForm) {
<app-admin-tier-form [editItem]="editingTier" (saved)="onTierSaved()" (closed)="onTierClosed()" />
}
@if (showStationForm) {
<app-admin-station-form [editItem]="stationConfig$ | async" (saved)="onStationSaved()" (closed)="onStationClosed()" />
}
+1 -44
View File
@@ -5,27 +5,24 @@ import { BehaviorSubject, firstValueFrom } from 'rxjs';
import { Show } from '../interfaces/show';
import { Event } from '../interfaces/event';
import { Tier } from '../interfaces/tier';
import { StationConfig } from '../interfaces/station-config';
import { HistoryEntry } from '../interfaces/history-entry';
import { TeamMember } from '../interfaces/team-member';
import { CommunityHighlight } from '../interfaces/community-highlight';
import { ShowService } from '../services/show.service';
import { EventService } from '../services/event.service';
import { TierService } from '../services/tier.service';
import { StationConfigService } from '../services/station-config.service';
import { HistoryEntryService } from '../services/history-entry.service';
import { TeamMemberService } from '../services/team-member.service';
import { CommunityHighlightService } from '../services/community-highlight.service';
import { AdminShowFormComponent } from './admin-show-form.component';
import { AdminEventFormComponent } from './admin-event-form.component';
import { AdminTierFormComponent } from './admin-tier-form.component';
import { AdminStationFormComponent } from './admin-station-form.component';
import { AdminHistoryFormComponent } from './admin-history-form.component';
import { AdminTeamFormComponent } from './admin-team-form.component';
import { AdminCommunityFormComponent } from './admin-community-form.component';
type TabKey = 'shows' | 'events' | 'tiers' | 'station' | 'history' | 'team' | 'community';
type TabKey = 'shows' | 'events' | 'station' | 'history' | 'team' | 'community';
@Component({
selector: 'app-admin',
@@ -36,7 +33,6 @@ type TabKey = 'shows' | 'events' | 'tiers' | 'station' | 'history' | 'team' | 'c
FormsModule,
AdminShowFormComponent,
AdminEventFormComponent,
AdminTierFormComponent,
AdminStationFormComponent,
AdminHistoryFormComponent,
AdminTeamFormComponent,
@@ -48,7 +44,6 @@ type TabKey = 'shows' | 'events' | 'tiers' | 'station' | 'history' | 'team' | 'c
export class AdminComponent implements OnInit {
private showService = inject(ShowService);
private eventService = inject(EventService);
private tierService = inject(TierService);
private stationConfigService = inject(StationConfigService);
private historyEntryService = inject(HistoryEntryService);
private teamMemberService = inject(TeamMemberService);
@@ -59,7 +54,6 @@ export class AdminComponent implements OnInit {
/** Item currently being edited (set by editXxx, cleared on save/close). */
editingShow: Show | null = null;
editingEvent: Event | null = null;
editingTier: Tier | null = null;
editingHistory: HistoryEntry | null = null;
editingTeam: TeamMember | null = null;
editingCommunity: CommunityHighlight | null = null;
@@ -67,7 +61,6 @@ export class AdminComponent implements OnInit {
/** BehaviorSubjects guarantee change detection via the async pipe — start empty, populated on load. */
readonly shows$ = new BehaviorSubject<Show[]>([]);
readonly events$ = new BehaviorSubject<Event[]>([]);
readonly tiers$ = new BehaviorSubject<Tier[]>([]);
readonly stationConfig$ = new BehaviorSubject<StationConfig | null>(null);
readonly historyEntries$ = new BehaviorSubject<HistoryEntry[]>([]);
readonly teamMembers$ = new BehaviorSubject<TeamMember[]>([]);
@@ -75,7 +68,6 @@ export class AdminComponent implements OnInit {
showShowForm = false;
showEventForm = false;
showTierForm = false;
showStationForm = false;
showHistoryForm = false;
showTeamForm = false;
@@ -95,11 +87,6 @@ export class AdminComponent implements OnInit {
this.events$.next(data);
}
async reloadTiers(): Promise<void> {
const data = await firstValueFrom(this.tierService.getTiers());
this.tiers$.next(data);
}
async reloadStationConfig(): Promise<void> {
await this.stationConfigService.load();
this.stationConfig$.next(this.stationConfigService.config());
@@ -124,7 +111,6 @@ export class AdminComponent implements OnInit {
await Promise.all([
this.reloadShows(),
this.reloadEvents(),
this.reloadTiers(),
this.reloadStationConfig(),
this.reloadHistoryEntries(),
this.reloadTeamMembers(),
@@ -190,35 +176,6 @@ export class AdminComponent implements OnInit {
await this.reloadEvents();
}
// ── Tier CRUD ───────────────────────────────────────────
openTierForm(): void {
this.editingTier = null;
this.showTierForm = true;
}
editTier(tier: Tier): void {
this.editingTier = tier;
this.showTierForm = true;
}
onTierSaved(): void {
this.reloadTiers();
this.showTierForm = false;
this.editingTier = null;
}
onTierClosed(): void {
this.showTierForm = false;
this.editingTier = null;
}
async deleteTier(id: number): Promise<void> {
if (!confirm('Delete this tier?')) return;
await firstValueFrom(this.tierService.deleteTier(id));
await this.reloadTiers();
}
// ── Station Config ──────────────────────────────────────
openStationForm(): void {
-5
View File
@@ -18,11 +18,6 @@ export const routes: Routes = [
loadComponent: () =>
import('./schedule/schedule.component').then((m) => m.ScheduleComponent),
},
{
path: 'donate',
loadComponent: () =>
import('./donate/donate.component').then((m) => m.DonateComponent),
},
{
path: 'events',
loadComponent: () =>
-1
View File
@@ -27,7 +27,6 @@
<option value="" disabled selected>Choose a topic</option>
<option value="general">General Inquiry</option>
<option value="volunteer">Volunteer</option>
<option value="donation">Donation Question</option>
<option value="advertising">Community Announcement</option>
<option value="song">Song Request</option>
<option value="technical">Technical Support</option>
-81
View File
@@ -1,81 +0,0 @@
<section class="donate">
<!-- Hero banner -->
<div class="donate-banner">
<div class="donate-banner-overlay">
<div class="container donate-banner-content">
<img [src]="resolveImageUrl(config().hero_icon_url)" alt="" class="donate-flower-hero" aria-hidden="true">
<h2>Keep the Music <span class="text-accent">Free</span></h2>
<p class="donate-subtitle">
{{ config().name_primary }} {{ config().name_secondary }} Radio is 100% listener-funded and independent.
Your gift keeps our signal strong and our airwaves free for everyone.
</p>
</div>
</div>
</div>
<div class="container">
<!-- Donation tiers -->
<div class="donate-tiers">
<h3>Choose Your Level of Support</h3>
@if (tiers$ | async; as state) {
@if (state.loading) {
<div class="schedule-loading">Loading tiers&hellip;</div>
} @else if (state.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 state.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>
<!-- One-time donation -->
<div class="donate-one-time">
<div class="donate-flower-divider" aria-hidden="true">
<img [src]="resolveImageUrl(config().logo_url)" alt="">
</div>
<h3>Or Make a One-Time Gift</h3>
<p class="section-intro">
Every dollar matters — from $5 to $5,000. No amount is too small to make
a difference. All donations are tax-deductible (EIN: {{ config().ein }}).
</p>
<a href="#" class="btn btn-lg">Give Any Amount</a>
</div>
<!-- Why donate -->
<div class="donate-why">
<div class="why-grid">
<div class="why-item">
<span class="why-icon">&#x1F4FB;</span>
<strong>{{ config().frequency }} Signal</strong>
<p>Covering 3 mountain counties and the valleys between</p>
</div>
<div class="why-item">
<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">&#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">&#x1F393;</span>
<strong>Youth Programs</strong>
<p>Free radio training for 200+ local students each year</p>
</div>
</div>
</div>
</div>
</section>
-197
View File
@@ -1,197 +0,0 @@
@use '../../styles/variables' as *;
@use '../../styles/mixins' as *;
.donate {
background: var(--color-cream);
}
// Banner
.donate-banner {
position: relative;
margin-bottom: $spacing-3xl;
}
.donate-banner-overlay {
background: $mixed-gradient;
padding: $spacing-3xl $spacing-lg;
text-align: center;
}
.donate-banner-content {
position: relative;
z-index: 1;
}
.donate-flower-hero {
width: 80px;
height: 80px;
margin: 0 auto $spacing-md;
opacity: 0.7;
animation: float 4s ease-in-out infinite;
}
.donate-banner h2 {
color: var(--color-white);
font-size: 2.4rem;
margin-bottom: $spacing-md;
}
.donate-subtitle {
color: rgba(255, 255, 255, 0.85);
font-size: 1.1rem;
max-width: 550px;
margin: 0 auto;
line-height: 1.7;
}
// Tiers
.donate-tiers {
text-align: center;
margin-bottom: $spacing-3xl;
h3 {
font-size: 1.8rem;
margin-bottom: $spacing-xl;
}
}
.tiers-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: $spacing-lg;
margin-bottom: $spacing-xl;
}
.tier-card {
@include card-style;
padding: $spacing-xl;
text-align: center;
background: var(--color-white);
border-top: 4px solid var(--color-accent);
.tier-icon {
font-size: 2rem;
margin-bottom: $spacing-sm;
}
.tier-amount {
font-size: 2.8rem;
font-weight: 700;
color: var(--color-accent);
line-height: 1;
}
.tier-period {
font-size: 0.85rem;
color: var(--color-medium);
margin-bottom: $spacing-md;
}
h4 {
font-size: 1.2rem;
color: var(--color-primary);
margin-bottom: $spacing-md;
}
.tier-benefits {
list-style: none;
text-align: left;
margin-bottom: $spacing-lg;
li {
padding: $spacing-xs 0;
font-size: 0.9rem;
color: var(--color-dark);
border-bottom: 1px solid var(--color-light);
&::before {
content: '';
color: $success-green;
font-weight: 700;
}
}
}
.btn-tier {
@include button-style($accent-orange, $neutral-white, $accent-orange-dark);
display: inline-block;
width: 100%;
text-align: center;
}
}
// One-time
.donate-one-time {
text-align: center;
padding: $spacing-2xl $spacing-lg;
background: var(--color-primary);
border-radius: $radius-xl;
margin-bottom: $spacing-3xl;
.donate-flower-divider {
width: 40px;
height: 40px;
margin: 0 auto $spacing-md;
img {
width: 100%;
filter: brightness(1.2);
}
}
h3 {
color: var(--color-white);
font-size: 1.6rem;
margin-bottom: $spacing-sm;
}
.section-intro {
color: rgba(255, 255, 255, 0.8);
margin-bottom: $spacing-lg;
}
.btn-lg {
@include button-style($accent-orange, $neutral-white, $accent-orange-dark);
font-size: 1.2rem;
padding: $spacing-md $spacing-2xl;
}
}
// Why
.donate-why {
padding-bottom: $spacing-xl;
}
.why-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: $spacing-lg;
.why-item {
text-align: center;
padding: $spacing-lg;
.why-icon {
display: block;
font-size: 2.5rem;
margin-bottom: $spacing-sm;
}
strong {
display: block;
color: var(--color-primary);
margin-bottom: $spacing-xs;
font-size: 1.05rem;
}
p {
font-size: 0.9rem;
color: var(--color-medium);
}
}
}
@media (max-width: #{$bp-md - 1px}) {
.donate-banner h2 { font-size: 1.8rem; }
.tiers-grid { grid-template-columns: 1fr; max-width: 320px; margin-left: auto; margin-right: auto; }
}
-36
View File
@@ -1,36 +0,0 @@
import { Component, inject } from '@angular/core';
import { AsyncPipe, NgFor } from '@angular/common';
import { map, Observable, startWith } from 'rxjs';
import { TierService } from '../services/tier.service';
import { Tier } from '../interfaces/tier';
import { StationConfigService } from '../services/station-config.service';
import { resolveImageUrl } from '../services/app-config.service';
interface TiersState {
loading: boolean;
tiers: Tier[];
}
@Component({
selector: 'app-donate',
standalone: true,
imports: [AsyncPipe, NgFor],
templateUrl: './donate.component.html',
styleUrl: './donate.component.scss',
})
export class DonateComponent {
private tierService = inject(TierService);
readonly config = inject(StationConfigService).config;
resolveImageUrl(url: string): string {
return resolveImageUrl(url);
}
/** Async pipe drives the template — guarantees change detection on every emission. */
readonly tiers$: Observable<TiersState> =
this.tierService.getTiers().pipe(
map((tiers) => ({ loading: false, tiers })),
startWith({ loading: true, tiers: [] }),
);
}
+6 -2
View File
@@ -23,14 +23,18 @@
<li><a routerLink="/about">About Us</a></li>
<li><a routerLink="/schedule">Schedule</a></li>
<li><a routerLink="/events">Events</a></li>
<li><a routerLink="/donate">Donate</a></li>
@if (config().donation_url) {
<li><a [href]="config().donation_url" target="_blank" rel="noopener noreferrer">Donate</a></li>
}
<li><a routerLink="/contact">Contact</a></li>
</ul>
</div>
<div class="footer-col">
<h4>Support</h4>
<ul>
<li><a routerLink="/donate">Become a Member</a></li>
@if (config().donation_url) {
<li><a [href]="config().donation_url" target="_blank" rel="noopener noreferrer">Become a Member</a></li>
}
<li><a href="#">Volunteer</a></li>
<li><a href="#">Corporate Giving</a></li>
<li><a href="#">Planned Giving</a></li>
+3 -1
View File
@@ -25,7 +25,9 @@
{{ config().tagline }}
</p>
<div class="hero-actions">
<a routerLink="/donate" class="btn btn-hero-primary">Support the Station</a>
@if (config().donation_url) {
<a [href]="config().donation_url" target="_blank" rel="noopener noreferrer" class="btn btn-hero-primary">Support the Station</a>
}
<a routerLink="/schedule" class="btn btn-hero-secondary">View Schedule</a>
</div>
<div class="hero-listeners">
+1
View File
@@ -17,4 +17,5 @@ export interface StationConfig {
phone: string;
email: string;
website: string;
donation_url: string;
}
-8
View File
@@ -1,8 +0,0 @@
export interface Tier {
id: number;
name: string;
amount: number;
icon: string;
benefits: string[];
display_order: number;
}
+3 -1
View File
@@ -34,7 +34,9 @@
<a routerLink="/login" class="btn-sign-in" (click)="closeMenu()">Sign In</a>
}
</div>
<a routerLink="/donate" class="btn btn-donate-nav" (click)="closeMenu()">Donate Now</a>
@if (config().donation_url) {
<a [href]="config().donation_url" target="_blank" rel="noopener noreferrer" class="btn btn-donate-nav" (click)="closeMenu()">Donate Now</a>
}
</div>
</div>
</nav>
@@ -25,6 +25,7 @@ const DEFAULT_CONFIG: StationConfig = {
phone: '(970) 555-0198',
email: 'hello@kmountainflower.org',
website: 'kmountainflower.org',
donation_url: '',
};
@Injectable({
-35
View File
@@ -1,35 +0,0 @@
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Tier } from '../interfaces/tier';
import { getAppConfig } from './app-config.service';
@Injectable({
providedIn: 'root',
})
export class TierService {
private http = inject(HttpClient);
private baseUrl = `${getAppConfig().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}`);
}
}