additional files
This commit is contained in:
@@ -17,23 +17,25 @@
|
||||
<!-- Donation tiers -->
|
||||
<div class="donate-tiers">
|
||||
<h3>Choose Your Level of Support</h3>
|
||||
@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>
|
||||
@if (tiers$ | async; as state) {
|
||||
@if (state.loading) {
|
||||
<div class="schedule-loading">Loading tiers…</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>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,32 +1,29 @@
|
||||
import { Component, inject, OnInit } from '@angular/core';
|
||||
import { NgFor } from '@angular/common';
|
||||
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';
|
||||
|
||||
interface TiersState {
|
||||
loading: boolean;
|
||||
tiers: Tier[];
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-donate',
|
||||
standalone: true,
|
||||
imports: [NgFor],
|
||||
imports: [AsyncPipe, NgFor],
|
||||
templateUrl: './donate.component.html',
|
||||
styleUrl: './donate.component.scss',
|
||||
})
|
||||
export class DonateComponent implements OnInit {
|
||||
export class DonateComponent {
|
||||
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 = [];
|
||||
},
|
||||
});
|
||||
}
|
||||
/** 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: [] }),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,35 +10,37 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@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>
|
||||
}
|
||||
@if (events$ | async; as state) {
|
||||
@if (state.loading) {
|
||||
<div class="schedule-loading">Loading events…</div>
|
||||
} @else if (state.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 state.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,32 +1,29 @@
|
||||
import { Component, inject, OnInit } from '@angular/core';
|
||||
import { DatePipe, NgFor } from '@angular/common';
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { AsyncPipe, DatePipe, NgFor } from '@angular/common';
|
||||
import { map, Observable, startWith } from 'rxjs';
|
||||
|
||||
import { EventService } from '../services/event.service';
|
||||
import { Event } from '../interfaces/event';
|
||||
|
||||
interface EventsState {
|
||||
loading: boolean;
|
||||
upcomingEvents: Event[];
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-events',
|
||||
standalone: true,
|
||||
imports: [DatePipe, NgFor],
|
||||
imports: [AsyncPipe, DatePipe, NgFor],
|
||||
templateUrl: './events.component.html',
|
||||
styleUrl: './events.component.scss',
|
||||
})
|
||||
export class EventsComponent implements OnInit {
|
||||
export class EventsComponent {
|
||||
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 = [];
|
||||
},
|
||||
});
|
||||
}
|
||||
/** Async pipe drives the template — guarantees change detection on every emission. */
|
||||
readonly events$: Observable<EventsState> =
|
||||
this.eventService.getEvents(true).pipe(
|
||||
map((upcomingEvents) => ({ loading: false, upcomingEvents })),
|
||||
startWith({ loading: true, upcomingEvents: [] }),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,20 @@
|
||||
<li><a routerLink="/schedule" routerLinkActive="active" (click)="closeMenu()">Schedule</a></li>
|
||||
<li><a routerLink="/events" routerLinkActive="active" (click)="closeMenu()">Events</a></li>
|
||||
<li><a routerLink="/contact" routerLinkActive="active" (click)="closeMenu()">Contact</a></li>
|
||||
@if (isAdmin) {
|
||||
<li><a routerLink="/admin" routerLinkActive="active" (click)="closeMenu()" class="nav-admin">Admin</a></li>
|
||||
}
|
||||
</ul>
|
||||
<div class="nav-auth">
|
||||
@if (isLoggedIn) {
|
||||
<div class="user-menu">
|
||||
<span class="user-name">{{ displayName }}</span>
|
||||
<button class="btn-logout" (click)="onLogout()">Sign Out</button>
|
||||
</div>
|
||||
} @else {
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -99,6 +99,62 @@
|
||||
background: rgba(232, 122, 46, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.nav-admin {
|
||||
color: var(--color-accent-light);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
// Auth section in navbar
|
||||
.nav-auth {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $spacing-sm;
|
||||
margin-left: $spacing-sm;
|
||||
}
|
||||
|
||||
.user-menu {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $spacing-sm;
|
||||
|
||||
.user-name {
|
||||
color: var(--color-light);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-logout {
|
||||
background: none;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
color: var(--color-light);
|
||||
padding: $spacing-xs $spacing-sm;
|
||||
border-radius: $radius-sm;
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
transition: border-color $transition-fast, color $transition-fast;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--color-accent);
|
||||
color: var(--color-accent-light);
|
||||
}
|
||||
}
|
||||
|
||||
.btn-sign-in {
|
||||
color: var(--color-accent-light);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
padding: $spacing-xs $spacing-sm;
|
||||
border-radius: $radius-sm;
|
||||
transition: color $transition-fast, background $transition-fast;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-accent);
|
||||
background: rgba(232, 122, 46, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.btn-donate-nav {
|
||||
|
||||
@@ -1,16 +1,40 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { RouterLink, RouterLinkActive } from '@angular/router';
|
||||
import { Component, DestroyRef, inject, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterLink, RouterLinkActive, Router } from '@angular/router';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
|
||||
import { AuthService } from '../services/auth.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-navbar',
|
||||
standalone: true,
|
||||
imports: [RouterLink, RouterLinkActive],
|
||||
imports: [CommonModule, RouterLink, RouterLinkActive],
|
||||
templateUrl: './navbar.component.html',
|
||||
styleUrl: './navbar.component.scss',
|
||||
})
|
||||
export class NavbarComponent {
|
||||
export class NavbarComponent implements OnInit {
|
||||
private auth = inject(AuthService);
|
||||
private router = inject(Router);
|
||||
private destroyRef = inject(DestroyRef);
|
||||
|
||||
menuOpen = false;
|
||||
|
||||
isLoggedIn = false;
|
||||
isAdmin = false;
|
||||
displayName = '';
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to auth state — drives all three properties from a single source.
|
||||
// isLoggedIn and isAdmin are derived from the user object, not from separate
|
||||
// checks (localStorage vs BehaviorSubject), eliminating a race condition where
|
||||
// the token exists but the /me call hasn't returned yet.
|
||||
this.auth.authState$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((user) => {
|
||||
this.isLoggedIn = !!user;
|
||||
this.isAdmin = user?.is_admin ?? false;
|
||||
this.displayName = user?.display_name ?? '';
|
||||
});
|
||||
}
|
||||
|
||||
toggleMenu(): void {
|
||||
this.menuOpen = !this.menuOpen;
|
||||
}
|
||||
@@ -18,4 +42,9 @@ export class NavbarComponent {
|
||||
closeMenu(): void {
|
||||
this.menuOpen = false;
|
||||
}
|
||||
|
||||
onLogout(): void {
|
||||
this.auth.logout();
|
||||
this.router.navigate(['/']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,79 +2,42 @@
|
||||
<div class="container">
|
||||
<div class="schedule-header">
|
||||
<img src="svg/small-flower.svg" alt="" class="schedule-flower" aria-hidden="true">
|
||||
<h2>Today's <span class="text-accent">Program Schedule</span></h2>
|
||||
<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 98.7 FM or via our online stream.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@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>
|
||||
@if (schedule$ | async; as state) {
|
||||
@if (state.loading) {
|
||||
<div class="schedule-loading">Loading schedule…</div>
|
||||
} @else if (state.dayGroups.length === 0) {
|
||||
<div class="schedule-empty">No programs scheduled. Check back soon!</div>
|
||||
} @else {
|
||||
@for (group of state.dayGroups; track group.day_label) {
|
||||
<div class="schedule-day-block">
|
||||
<div class="schedule-day-label">{{ group.day_label }}</div>
|
||||
<div class="schedule-table">
|
||||
@for (program of group.programs; track program.id) {
|
||||
<div 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>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
<div class="schedule-weekend">
|
||||
<div class="weekend-heading">
|
||||
<img src="svg/small-flower.svg" alt="" class="schedule-flower-sm" aria-hidden="true">
|
||||
<h3>Weekend Programming</h3>
|
||||
</div>
|
||||
<div class="weekend-grid">
|
||||
<div class="weekend-card">
|
||||
<div class="card-day">Saturday</div>
|
||||
<div class="card-programs">
|
||||
<div class="card-item">
|
||||
<strong>9:00 AM</strong> — Country Roads Hour
|
||||
</div>
|
||||
<div class="card-item">
|
||||
<strong>11:00 AM</strong> — Community Hour (call-ins welcome!)
|
||||
</div>
|
||||
<div class="card-item">
|
||||
<strong>2:00 PM</strong> — Afternoon Bluegrass
|
||||
</div>
|
||||
<div class="card-item">
|
||||
<strong>4:00 PM</strong> — Record Request Hour
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="weekend-card">
|
||||
<div class="card-day">Sunday</div>
|
||||
<div class="card-programs">
|
||||
<div class="card-item">
|
||||
<strong>10:00 AM</strong> — Sunday Morning Hymns & Folk
|
||||
</div>
|
||||
<div class="card-item">
|
||||
<strong>12:00 PM</strong> — Gospel & Roots
|
||||
</div>
|
||||
<div class="card-item">
|
||||
<strong>3:00 PM</strong> — The Long Wave (ambient / nature sounds)
|
||||
</div>
|
||||
<div class="card-item">
|
||||
<strong>6:00 PM</strong> — Sunday Sunset Jazz
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -18,9 +18,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
.schedule-list {
|
||||
.schedule-loading,
|
||||
.schedule-empty {
|
||||
text-align: center;
|
||||
padding: $spacing-3xl $spacing-md;
|
||||
color: var(--color-medium);
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.schedule-day-block {
|
||||
max-width: 800px;
|
||||
margin: 0 auto $spacing-3xl;
|
||||
margin: 0 auto $spacing-xl;
|
||||
}
|
||||
|
||||
.schedule-day-label {
|
||||
@@ -101,73 +109,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Weekend
|
||||
.schedule-weekend {
|
||||
margin-top: $spacing-xl;
|
||||
}
|
||||
|
||||
.weekend-heading {
|
||||
text-align: center;
|
||||
margin-bottom: $spacing-lg;
|
||||
|
||||
.schedule-flower-sm {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
margin: 0 auto $spacing-sm;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.6rem;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.weekend-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: $spacing-lg;
|
||||
}
|
||||
|
||||
.weekend-card {
|
||||
@include card-style;
|
||||
padding: $spacing-xl;
|
||||
background: var(--color-white);
|
||||
|
||||
.card-day {
|
||||
font-family: var(--font-heading);
|
||||
font-size: 1.4rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-accent);
|
||||
margin-bottom: $spacing-md;
|
||||
padding-bottom: $spacing-sm;
|
||||
border-bottom: 2px solid var(--color-light);
|
||||
}
|
||||
|
||||
.card-programs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $spacing-sm;
|
||||
}
|
||||
|
||||
.card-item {
|
||||
font-size: 0.95rem;
|
||||
color: var(--color-dark);
|
||||
padding: $spacing-sm;
|
||||
border-radius: $radius-sm;
|
||||
transition: background $transition-fast;
|
||||
|
||||
strong {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--color-cream);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: #{$bp-md - 1px}) {
|
||||
.schedule-time { width: 80px; font-size: 0.85rem; }
|
||||
.weekend-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
@@ -1,32 +1,53 @@
|
||||
import { Component, inject, OnInit } from '@angular/core';
|
||||
import { NgFor } from '@angular/common';
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { AsyncPipe, NgFor } from '@angular/common';
|
||||
import { map, Observable, startWith } from 'rxjs';
|
||||
|
||||
import { ProgramService } from '../services/program.service';
|
||||
import { Program } from '../interfaces/program';
|
||||
|
||||
interface DayGroup {
|
||||
day_label: string;
|
||||
day_of_week: number;
|
||||
programs: Program[];
|
||||
}
|
||||
|
||||
interface ScheduleState {
|
||||
loading: boolean;
|
||||
dayGroups: DayGroup[];
|
||||
}
|
||||
|
||||
function groupByDay(programs: Program[]): DayGroup[] {
|
||||
const map = new Map<string, DayGroup>();
|
||||
|
||||
for (const p of programs) {
|
||||
if (!map.has(p.day_label)) {
|
||||
map.set(p.day_label, {
|
||||
day_label: p.day_label,
|
||||
day_of_week: p.day_of_week,
|
||||
programs: [],
|
||||
});
|
||||
}
|
||||
map.get(p.day_label)!.programs.push(p);
|
||||
}
|
||||
|
||||
return Array.from(map.values()).sort((a, b) => a.day_of_week - b.day_of_week);
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-schedule',
|
||||
standalone: true,
|
||||
imports: [NgFor],
|
||||
imports: [AsyncPipe, NgFor],
|
||||
templateUrl: './schedule.component.html',
|
||||
styleUrl: './schedule.component.scss',
|
||||
})
|
||||
export class ScheduleComponent implements OnInit {
|
||||
export class ScheduleComponent {
|
||||
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 = [];
|
||||
},
|
||||
});
|
||||
}
|
||||
/** Async pipe drives the template — guarantees change detection on every emission. */
|
||||
readonly schedule$: Observable<ScheduleState> =
|
||||
this.programService.getPrograms().pipe(
|
||||
map((programs) => groupByDay(programs)),
|
||||
map((dayGroups) => ({ loading: false, dayGroups })),
|
||||
startWith({ loading: true, dayGroups: [] }),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user