additional files
This commit is contained in:
@@ -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]]
|
||||
@@ -1,6 +1,8 @@
|
||||
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
|
||||
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.
@@ -17,13 +17,14 @@
|
||||
<!-- Donation tiers -->
|
||||
<div class="donate-tiers">
|
||||
<h3>Choose Your Level of Support</h3>
|
||||
@if (loading) {
|
||||
@if (tiers$ | async; as state) {
|
||||
@if (state.loading) {
|
||||
<div class="schedule-loading">Loading tiers…</div>
|
||||
} @else if (tiers.length === 0) {
|
||||
} @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 tiers">
|
||||
<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>
|
||||
@@ -35,6 +36,7 @@
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- One-time donation -->
|
||||
|
||||
@@ -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,13 +10,14 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@if (loading) {
|
||||
@if (events$ | async; as state) {
|
||||
@if (state.loading) {
|
||||
<div class="schedule-loading">Loading events…</div>
|
||||
} @else if (upcomingEvents.length === 0) {
|
||||
} @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 upcomingEvents">
|
||||
<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>
|
||||
@@ -40,5 +41,6 @@
|
||||
</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,22 +2,25 @@
|
||||
<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) {
|
||||
@if (schedule$ | async; as state) {
|
||||
@if (state.loading) {
|
||||
<div class="schedule-loading">Loading schedule…</div>
|
||||
} @else if (programs.length === 0) {
|
||||
} @else if (state.dayGroups.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>
|
||||
@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">
|
||||
<div *ngFor="let program of programs" class="schedule-row">
|
||||
@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">
|
||||
@@ -30,51 +33,11 @@
|
||||
</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: [] }),
|
||||
);
|
||||
}
|
||||
|
||||
+434
@@ -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 |
Reference in New Issue
Block a user