additional files

This commit is contained in:
2026-06-19 17:37:19 +00:00
parent 266c2451f1
commit 2d2b2d040b
25 changed files with 709 additions and 239 deletions
@@ -0,0 +1 @@
- [Angular Zoneless Fix](angular-zoneless-fix.md) — Added zone.js to fix async HTTP change detection
@@ -0,0 +1,12 @@
---
name: angular-zoneless-fix
description: Fixed Angular 21 zoneless default causing async HTTP callbacks to not trigger change detection
metadata:
type: project
---
Angular 21 defaults to zoneless execution. The project was missing zone.js entirely, so HTTP responses arrived but change detection never ran — components stayed stuck on loading states.
**Fix:** Added `import 'zone.js'` to [src/main.ts](src/main.ts) and installed zone.js as a dependency. This is the simpler fix over configuring provideZoneChangeDetection() manually.
Related: [[auto-seed-admin-bootstrap]]
+4 -2
View File
@@ -1,6 +1,8 @@
FROM python:3.12-alpine FROM python:3.12-alpine
RUN apk update && apk add git nodejs npm RUN apk update && apk add git nodejs npm bash
# Install Angular.js via NPM # Install Angular.js via NPM
RUN npm install -g @angular/cli@21.2.14 RUN npm install -g @angular/cli@21.2.14
RUN /bin/bash
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+18 -16
View File
@@ -17,23 +17,25 @@
<!-- Donation tiers --> <!-- Donation tiers -->
<div class="donate-tiers"> <div class="donate-tiers">
<h3>Choose Your Level of Support</h3> <h3>Choose Your Level of Support</h3>
@if (loading) { @if (tiers$ | async; as state) {
<div class="schedule-loading">Loading tiers&hellip;</div> @if (state.loading) {
} @else if (tiers.length === 0) { <div class="schedule-loading">Loading tiers&hellip;</div>
<div class="schedule-empty">No donation tiers available. Check back soon!</div> } @else if (state.tiers.length === 0) {
} @else { <div class="schedule-empty">No donation tiers available. Check back soon!</div>
<div class="tiers-grid"> } @else {
<div class="tier-card" *ngFor="let tier of tiers"> <div class="tiers-grid">
<div class="tier-icon">{{ tier.icon }}</div> <div class="tier-card" *ngFor="let tier of state.tiers">
<div class="tier-amount">${{ tier.amount }}</div> <div class="tier-icon">{{ tier.icon }}</div>
<div class="tier-period">per month</div> <div class="tier-amount">${{ tier.amount }}</div>
<h4>{{ tier.name }}</h4> <div class="tier-period">per month</div>
<ul class="tier-benefits"> <h4>{{ tier.name }}</h4>
<li *ngFor="let benefit of tier.benefits">{{ benefit }}</li> <ul class="tier-benefits">
</ul> <li *ngFor="let benefit of tier.benefits">{{ benefit }}</li>
<a href="#" class="btn btn-tier">Select {{ tier.name }}</a> </ul>
<a href="#" class="btn btn-tier">Select {{ tier.name }}</a>
</div>
</div> </div>
</div> }
} }
</div> </div>
+16 -19
View File
@@ -1,32 +1,29 @@
import { Component, inject, OnInit } from '@angular/core'; import { Component, inject } from '@angular/core';
import { NgFor } from '@angular/common'; import { AsyncPipe, NgFor } from '@angular/common';
import { map, Observable, startWith } from 'rxjs';
import { TierService } from '../services/tier.service'; import { TierService } from '../services/tier.service';
import { Tier } from '../interfaces/tier'; import { Tier } from '../interfaces/tier';
interface TiersState {
loading: boolean;
tiers: Tier[];
}
@Component({ @Component({
selector: 'app-donate', selector: 'app-donate',
standalone: true, standalone: true,
imports: [NgFor], imports: [AsyncPipe, NgFor],
templateUrl: './donate.component.html', templateUrl: './donate.component.html',
styleUrl: './donate.component.scss', styleUrl: './donate.component.scss',
}) })
export class DonateComponent implements OnInit { export class DonateComponent {
private tierService = inject(TierService); private tierService = inject(TierService);
tiers: Tier[] = []; /** Async pipe drives the template — guarantees change detection on every emission. */
loading = true; readonly tiers$: Observable<TiersState> =
this.tierService.getTiers().pipe(
ngOnInit(): void { map((tiers) => ({ loading: false, tiers })),
this.tierService.getTiers().subscribe({ startWith({ loading: true, tiers: [] }),
next: (data) => { );
this.tiers = data;
this.loading = false;
},
error: () => {
this.loading = false;
this.tiers = [];
},
});
}
} }
+28 -26
View File
@@ -10,35 +10,37 @@
</p> </p>
</div> </div>
@if (loading) { @if (events$ | async; as state) {
<div class="schedule-loading">Loading events&hellip;</div> @if (state.loading) {
} @else if (upcomingEvents.length === 0) { <div class="schedule-loading">Loading events&hellip;</div>
<div class="schedule-empty">No upcoming events — check back later!</div> } @else if (state.upcomingEvents.length === 0) {
} @else { <div class="schedule-empty">No upcoming events — check back later!</div>
<div class="events-list"> } @else {
<div class="event-card" *ngFor="let event of upcomingEvents"> <div class="events-list">
<div class="event-date"> <div class="event-card" *ngFor="let event of state.upcomingEvents">
<span class="event-month">{{ event.date | date:'MMM' }}</span> <div class="event-date">
<span class="event-day">{{ event.date | date:'d' }}</span> <span class="event-month">{{ event.date | date:'MMM' }}</span>
<span class="event-year">{{ event.date | date:'yyyy' }}</span> <span class="event-day">{{ event.date | date:'d' }}</span>
</div> <span class="event-year">{{ event.date | date:'yyyy' }}</span>
<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-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>
<div class="event-icon" aria-hidden="true">{{ event.icon }}</div>
</div> </div>
</div> }
} }
</div> </div>
</section> </section>
+16 -19
View File
@@ -1,32 +1,29 @@
import { Component, inject, OnInit } from '@angular/core'; import { Component, inject } from '@angular/core';
import { DatePipe, NgFor } from '@angular/common'; import { AsyncPipe, DatePipe, NgFor } from '@angular/common';
import { map, Observable, startWith } from 'rxjs';
import { EventService } from '../services/event.service'; import { EventService } from '../services/event.service';
import { Event } from '../interfaces/event'; import { Event } from '../interfaces/event';
interface EventsState {
loading: boolean;
upcomingEvents: Event[];
}
@Component({ @Component({
selector: 'app-events', selector: 'app-events',
standalone: true, standalone: true,
imports: [DatePipe, NgFor], imports: [AsyncPipe, DatePipe, NgFor],
templateUrl: './events.component.html', templateUrl: './events.component.html',
styleUrl: './events.component.scss', styleUrl: './events.component.scss',
}) })
export class EventsComponent implements OnInit { export class EventsComponent {
private eventService = inject(EventService); private eventService = inject(EventService);
upcomingEvents: Event[] = []; /** Async pipe drives the template — guarantees change detection on every emission. */
loading = true; readonly events$: Observable<EventsState> =
this.eventService.getEvents(true).pipe(
ngOnInit(): void { map((upcomingEvents) => ({ loading: false, upcomingEvents })),
this.eventService.getEvents(true).subscribe({ startWith({ loading: true, upcomingEvents: [] }),
next: (data) => { );
this.upcomingEvents = data;
this.loading = false;
},
error: () => {
this.loading = false;
this.upcomingEvents = [];
},
});
}
} }
+13
View File
@@ -21,7 +21,20 @@
<li><a routerLink="/schedule" routerLinkActive="active" (click)="closeMenu()">Schedule</a></li> <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="/events" routerLinkActive="active" (click)="closeMenu()">Events</a></li>
<li><a routerLink="/contact" routerLinkActive="active" (click)="closeMenu()">Contact</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> </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> <a routerLink="/donate" class="btn btn-donate-nav" (click)="closeMenu()">Donate Now</a>
</div> </div>
</div> </div>
+56
View File
@@ -99,6 +99,62 @@
background: rgba(232, 122, 46, 0.1); 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 { .btn-donate-nav {
+33 -4
View File
@@ -1,16 +1,40 @@
import { Component } from '@angular/core'; import { Component, DestroyRef, inject, OnInit } from '@angular/core';
import { RouterLink, RouterLinkActive } from '@angular/router'; 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({ @Component({
selector: 'app-navbar', selector: 'app-navbar',
standalone: true, standalone: true,
imports: [RouterLink, RouterLinkActive], imports: [CommonModule, RouterLink, RouterLinkActive],
templateUrl: './navbar.component.html', templateUrl: './navbar.component.html',
styleUrl: './navbar.component.scss', 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; 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 { toggleMenu(): void {
this.menuOpen = !this.menuOpen; this.menuOpen = !this.menuOpen;
} }
@@ -18,4 +42,9 @@ export class NavbarComponent {
closeMenu(): void { closeMenu(): void {
this.menuOpen = false; this.menuOpen = false;
} }
onLogout(): void {
this.auth.logout();
this.router.navigate(['/']);
}
} }
+28 -65
View File
@@ -2,79 +2,42 @@
<div class="container"> <div class="container">
<div class="schedule-header"> <div class="schedule-header">
<img src="svg/small-flower.svg" alt="" class="schedule-flower" aria-hidden="true"> <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"> <p class="section-intro">
All times are local (Mountain Time). Every show is free to listen to All times are local (Mountain Time). Every show is free to listen to
live at 98.7 FM or via our online stream. live at 98.7 FM or via our online stream.
</p> </p>
</div> </div>
@if (loading) { @if (schedule$ | async; as state) {
<div class="schedule-loading">Loading schedule&hellip;</div> @if (state.loading) {
} @else if (programs.length === 0) { <div class="schedule-loading">Loading schedule&hellip;</div>
<div class="schedule-empty">No programs scheduled. Check back soon!</div> } @else if (state.dayGroups.length === 0) {
} @else { <div class="schedule-empty">No programs scheduled. Check back soon!</div>
<div class="schedule-list"> } @else {
<div class="schedule-day-label">Monday Friday</div> @for (group of state.dayGroups; track group.day_label) {
<div class="schedule-table"> <div class="schedule-day-block">
<div *ngFor="let program of programs" class="schedule-row"> <div class="schedule-day-label">{{ group.day_label }}</div>
<div class="schedule-time">{{ program.time }}</div> <div class="schedule-table">
<div class="schedule-divider"></div> @for (program of group.programs; track program.id) {
<div class="schedule-details"> <div class="schedule-row">
<h4 class="schedule-title">{{ program.title }}</h4> <div class="schedule-time">{{ program.time }}</div>
<div class="schedule-meta"> <div class="schedule-divider"></div>
<span class="schedule-host"> <div class="schedule-details">
<span aria-hidden="true">&#x1F399;</span> {{ program.host }} <h4 class="schedule-title">{{ program.title }}</h4>
</span> <div class="schedule-meta">
<span class="schedule-genre">{{ program.genre }}</span> <span class="schedule-host">
</div> <span aria-hidden="true">&#x1F399;</span> {{ program.host }}
</span>
<span class="schedule-genre">{{ program.genre }}</span>
</div>
</div>
</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> </div>
</section> </section>
+10 -69
View File
@@ -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; max-width: 800px;
margin: 0 auto $spacing-3xl; margin: 0 auto $spacing-xl;
} }
.schedule-day-label { .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}) { @media (max-width: #{$bp-md - 1px}) {
.schedule-time { width: 80px; font-size: 0.85rem; } .schedule-time { width: 80px; font-size: 0.85rem; }
.weekend-grid { grid-template-columns: 1fr; }
} }
+40 -19
View File
@@ -1,32 +1,53 @@
import { Component, inject, OnInit } from '@angular/core'; import { Component, inject } from '@angular/core';
import { NgFor } from '@angular/common'; import { AsyncPipe, NgFor } from '@angular/common';
import { map, Observable, startWith } from 'rxjs';
import { ProgramService } from '../services/program.service'; import { ProgramService } from '../services/program.service';
import { Program } from '../interfaces/program'; 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({ @Component({
selector: 'app-schedule', selector: 'app-schedule',
standalone: true, standalone: true,
imports: [NgFor], imports: [AsyncPipe, NgFor],
templateUrl: './schedule.component.html', templateUrl: './schedule.component.html',
styleUrl: './schedule.component.scss', styleUrl: './schedule.component.scss',
}) })
export class ScheduleComponent implements OnInit { export class ScheduleComponent {
private programService = inject(ProgramService); private programService = inject(ProgramService);
programs: Program[] = []; /** Async pipe drives the template — guarantees change detection on every emission. */
loading = true; readonly schedule$: Observable<ScheduleState> =
this.programService.getPrograms().pipe(
ngOnInit(): void { map((programs) => groupByDay(programs)),
this.programService.getPrograms().subscribe({ map((dayGroups) => ({ loading: false, dayGroups })),
next: (data) => { startWith({ loading: true, dayGroups: [] }),
this.programs = data; );
this.loading = false;
},
error: () => {
this.loading = false;
this.programs = [];
},
});
}
} }
+434
View File
@@ -0,0 +1,434 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 800 600"
width="100%"
height="100%"
version="1.1"
id="svg31"
sodipodi:docname="studio.svg"
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview31"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="3.2479248"
inkscape:cx="300.03773"
inkscape:cy="329.44112"
inkscape:window-width="1920"
inkscape:window-height="943"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="host" />
<defs
id="defs6">
<linearGradient
id="bgGrad"
x1="0%"
y1="0%"
x2="100%"
y2="100%">
<stop
offset="0%"
stop-color="#1a252c"
id="stop1" />
<stop
offset="100%"
stop-color="#2c3e50"
id="stop2" />
</linearGradient>
<linearGradient
id="deskGrad"
x1="0%"
y1="0%"
x2="0%"
y2="100%">
<stop
offset="0%"
stop-color="#e67e22"
id="stop3" />
<stop
offset="100%"
stop-color="#d35400"
id="stop4" />
</linearGradient>
<pattern
id="foam"
width="40"
height="40"
patternUnits="userSpaceOnUse">
<rect
width="40"
height="40"
fill="#34495e"
id="rect4" />
<path
d="M 0 20 L 20 0 L 40 20 L 20 40 Z"
fill="#2c3e50"
id="path4" />
<path
d="M 10 20 L 20 10 L 30 20 L 20 30 Z"
fill="#22313f"
id="path5" />
</pattern>
<filter
id="glow"
x="-0.075"
y="-0.2"
width="1.15"
height="1.4">
<feGaussianBlur
stdDeviation="5"
result="blur"
id="feGaussianBlur5" />
<feMerge
id="feMerge6">
<feMergeNode
in="blur"
id="feMergeNode5" />
<feMergeNode
in="SourceGraphic"
id="feMergeNode6" />
</feMerge>
</filter>
</defs>
<rect
width="100%"
height="100%"
fill="url(#bgGrad)"
id="rect6" />
<rect
x="40"
y="40"
width="280"
height="280"
fill="url(#foam)"
rx="10"
id="rect7" />
<rect
x="340"
y="40"
width="120"
height="120"
fill="url(#foam)"
rx="10"
id="rect8" />
<rect
x="480"
y="60"
width="280"
height="200"
fill="#111"
rx="8"
id="rect9" />
<rect
x="490"
y="70"
width="260"
height="180"
fill="#3498db"
fill-opacity="0.2"
rx="4"
id="rect10" />
<path
d="M 490 70 L 650 70 L 550 250 L 490 250 Z"
fill="#ffffff"
fill-opacity="0.05"
id="path10" />
<g
transform="translate(60, 360)"
id="g12">
<rect
width="160"
height="60"
fill="#c0392b"
rx="8"
filter="url(#glow)"
id="rect11" />
<rect
width="160"
height="60"
fill="none"
stroke="#e74c3c"
stroke-width="4"
rx="8"
id="rect12" />
<text
x="80"
y="40"
fill="#ffffff"
font-family="Arial, sans-serif"
font-size="28"
font-weight="900"
text-anchor="middle"
letter-spacing="2"
id="text12">ON AIR</text>
</g>
<g
id="host">
<path
d="M 180 600 Q 180 430 300 420 Q 420 430 420 600 Z"
fill="#2980b9"
id="path12" />
<path
d="M 270 420 L 300 460 L 330 420 Z"
fill="#1f618d"
id="path13" />
<rect
x="280"
y="380"
width="40"
height="50"
fill="#e0ac69"
rx="10"
id="rect13" />
<ellipse
cx="300"
cy="330"
rx="60"
ry="70"
fill="#f5cba7"
id="ellipse13" />
<path
d="M 235 320 C 230 260 270 240 300 240 C 340 240 370 270 365 320 C 360 280 340 260 300 260 C 260 260 240 280 235 320 Z"
fill="#3e2723"
id="path14" />
<rect
x="220"
y="315"
width="20"
height="50"
fill="#111"
rx="8"
id="rect15" />
<rect
x="360"
y="315"
width="20"
height="50"
fill="#111"
rx="8"
id="rect16" />
<path
d="m 240.05929,305.84729 a 60,60 0 0 1 120,0"
fill="none"
stroke="#111111"
stroke-width="18"
stroke-linecap="round"
id="path16"
style="stroke-width:23;stroke-miterlimit:4.5;stroke-dasharray:none" />
</g>
<path
d="M -50 480 Q 400 440 850 480 L 850 650 L -50 650 Z"
fill="url(#deskGrad)"
id="path17" />
<path
d="M -50 480 Q 400 440 850 480"
fill="none"
stroke="#e67e22"
stroke-width="8"
id="path18" />
<g
transform="translate(480, 500) skewX(-15)"
id="g28">
<rect
width="260"
height="140"
fill="#2c3e50"
rx="5"
id="rect18" />
<rect
width="260"
height="140"
fill="none"
stroke="#1a252c"
stroke-width="4"
rx="5"
id="rect19" />
<line
x1="40"
y1="20"
x2="40"
y2="120"
stroke="#111"
stroke-width="6"
stroke-linecap="round"
id="line19" />
<rect
x="30"
y="80"
width="20"
height="15"
fill="#bdc3c7"
rx="3"
id="rect20" />
<circle
cx="40"
cy="40"
r="8"
fill="#e74c3c"
id="circle20" />
<line
x1="80"
y1="20"
x2="80"
y2="120"
stroke="#111"
stroke-width="6"
stroke-linecap="round"
id="line20" />
<rect
x="70"
y="50"
width="20"
height="15"
fill="#bdc3c7"
rx="3"
id="rect21" />
<circle
cx="80"
cy="30"
r="8"
fill="#f1c40f"
id="circle21" />
<line
x1="120"
y1="20"
x2="120"
y2="120"
stroke="#111"
stroke-width="6"
stroke-linecap="round"
id="line21" />
<rect
x="110"
y="90"
width="20"
height="15"
fill="#bdc3c7"
rx="3"
id="rect22" />
<circle
cx="120"
cy="60"
r="8"
fill="#3498db"
id="circle22" />
<rect
x="160"
y="20"
width="80"
height="60"
fill="#111"
rx="2"
id="rect23" />
<rect
x="165"
y="40"
width="10"
height="40"
fill="#2ecc71"
id="rect24" />
<rect
x="180"
y="30"
width="10"
height="50"
fill="#f1c40f"
id="rect25" />
<rect
x="195"
y="50"
width="10"
height="30"
fill="#2ecc71"
id="rect26" />
<rect
x="210"
y="25"
width="10"
height="55"
fill="#e74c3c"
id="rect27" />
<rect
x="225"
y="60"
width="10"
height="20"
fill="#2ecc71"
id="rect28" />
</g>
<g
id="microphone">
<path
d="M 600 650 L 550 400 L 400 360"
fill="none"
stroke="#7f8c8d"
stroke-width="16"
stroke-linejoin="round"
id="path28" />
<circle
cx="550"
cy="400"
r="12"
fill="#34495e"
id="circle28" />
<circle
cx="400"
cy="360"
r="10"
fill="#34495e"
id="circle29" />
<g
transform="translate(400, 360) rotate(-20)"
id="g31">
<path
d="M -20 -20 A 30 30 0 0 0 -20 20"
fill="none"
stroke="#95a5a6"
stroke-width="6"
id="path29" />
<rect
x="-40"
y="-15"
width="30"
height="30"
fill="#2c3e50"
rx="4"
id="rect29" />
<rect
x="-60"
y="-12"
width="20"
height="24"
fill="#bdc3c7"
rx="8"
id="rect30" />
<line
x1="-50"
y1="-12"
x2="-50"
y2="12"
stroke="#7f8c8d"
stroke-width="2"
id="line30" />
<line
x1="-55"
y1="0"
x2="-40"
y2="0"
stroke="#7f8c8d"
stroke-width="2"
id="line31" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.6 KiB