diff --git a/src/app/download-cta/download-cta.component.html b/src/app/download-cta/download-cta.component.html new file mode 100644 index 0000000..c33e309 --- /dev/null +++ b/src/app/download-cta/download-cta.component.html @@ -0,0 +1,19 @@ +@if (config().play_store_url || config().app_store_embed_html) { +
+ Download our app +
+ @if (config().play_store_url) { + + @if (config().play_store_icon_url) { + Get it on Google Play + } @else { + Google Play + } + + } + @if (config().app_store_embed_html) { +
+ } +
+
+} diff --git a/src/app/download-cta/download-cta.component.scss b/src/app/download-cta/download-cta.component.scss new file mode 100644 index 0000000..e48b1f1 --- /dev/null +++ b/src/app/download-cta/download-cta.component.scss @@ -0,0 +1,85 @@ +@use '../../styles/variables' as *; + +// ── Download CTAs ─────────────────────────────────────────── + +.hero-download { + display: flex; + flex-direction: column; + align-items: center; + gap: $spacing-sm; + margin: $spacing-lg 0 $spacing-md; +} + +.download-label { + font-size: 0.9rem; + color: rgba(255, 255, 255, 0.65); + letter-spacing: 0.03em; +} + +.download-buttons { + display: flex; + gap: $spacing-md; + justify-content: center; + flex-wrap: wrap; +} + +// Both store badges are constrained to the same size (160x52) +.store-link { + display: inline-block; + text-decoration: none; +} + +.store-badge { + width: 160px; + height: 52px; + object-fit: contain; + background: rgba(255, 255, 255, 0.1); + border-radius: $radius-sm; + padding: 4px; + transition: opacity $transition-fast; + + &:hover { + opacity: 0.85; + } +} + +.store-text { + color: var(--color-white); + text-decoration: underline; + font-size: 0.95rem; + padding: $spacing-xs $spacing-sm; + border-radius: $radius-sm; + background: rgba(255, 255, 255, 0.1); + + &:hover { + background: rgba(255, 255, 255, 0.2); + } +} + +// Apple's embed HTML contains its own / — constrain it +.appstore-embed { + display: inline-flex; + align-items: center; + + // Apple's embeds typically wrap an around an + > a, + a { + display: inline-block; + text-decoration: none; + } + + img { + width: 160px; + height: 52px; + object-fit: contain; + display: block; + } + + // If the embed is an iframe (Apple's new style), constrain that too + iframe { + width: 160px; + height: 52px; + border: none; + border-radius: $radius-sm; + } +} diff --git a/src/app/download-cta/download-cta.component.ts b/src/app/download-cta/download-cta.component.ts new file mode 100644 index 0000000..69ad2d2 --- /dev/null +++ b/src/app/download-cta/download-cta.component.ts @@ -0,0 +1,20 @@ +import { Component, inject } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { StationConfigService } from '../services/station-config.service'; +import { resolveImageUrl } from '../services/app-config.service'; + +@Component({ + selector: 'app-download-cta', + standalone: true, + imports: [CommonModule], + templateUrl: './download-cta.component.html', + styleUrl: './download-cta.component.scss', +}) +export class DownloadCtaComponent { + readonly config = inject(StationConfigService).config; + + resolveImageUrl(url: string): string { + return resolveImageUrl(url); + } +} diff --git a/src/app/hero/hero.component.html b/src/app/hero/hero.component.html index c755a3d..8c189da 100644 --- a/src/app/hero/hero.component.html +++ b/src/app/hero/hero.component.html @@ -30,25 +30,7 @@ } View Schedule - @if (config().play_store_url || config().app_store_embed_html) { -
- Download our app -
- @if (config().play_store_url) { - - @if (config().play_store_icon_url) { - Get it on Google Play - } @else { - Google Play - } - - } - @if (config().app_store_embed_html) { -
- } -
-
- } +
Live on-air now — Stream free worldwide @@ -60,82 +42,7 @@ - - - @if (selectedUnderwriter()) { - @let uw = selectedUnderwriter()!; -
-
- -
- @if (uw.logo_url) { - - } -

{{ uw.name }}

-

{{ uw.description }}

- @if (uw.website_url) { - Visit Website - } -
-
-
- } -@if ((underwriters()).length >= 2) { -
-

{{ config().callsign }} is brought to you with the generosity of our underwriters.

- -
-} + diff --git a/src/app/hero/hero.component.scss b/src/app/hero/hero.component.scss index fa3a836..2885556 100644 --- a/src/app/hero/hero.component.scss +++ b/src/app/hero/hero.component.scss @@ -169,305 +169,6 @@ } } -// ── Underwriter Horizontal Carousel ───────────────────────── - -.underwriters-section { - background: $neutral-cream; - border-top: 2px solid $neutral-light; - border-bottom: 2px solid $neutral-light; -} - -.underwriters-label { - text-align: center; - padding: $spacing-lg $spacing-lg $spacing-md; - margin: 0; - font-family: $font-heading; - font-size: clamp(1rem, 2vw, 1.3rem); - font-weight: 600; - color: $neutral-dark; - letter-spacing: 0.02em; -} - -.underwriters-carousel { - width: 100%; - overflow: hidden; - padding: clamp($spacing-xl, 4vh, $spacing-3xl) 0; -} - -.carousel-track { - display: flex; - gap: clamp($spacing-xl, 4vw, $spacing-3xl); - width: max-content; - animation: scroll-carousel 45s linear infinite; - - &.paused { - animation-play-state: paused; - } -} - -@keyframes scroll-carousel { - from { - transform: translateX(0); - } - - to { - transform: translateX(-66.666%); - } -} - -.carousel-slide { - flex-shrink: 0; - background: none; - border: none; - padding: 0; - cursor: pointer; - -webkit-tap-highlight-color: transparent; - outline: none; - - &:focus-visible { - - .carousel-logo, - .carousel-badge { - outline: 2px solid $accent-orange; - outline-offset: 2px; - } - } -} - -.carousel-logo { - display: block; - width: clamp(180px, 12vw, 300px); - height: clamp(100px, 6.5vw, 170px); - object-fit: contain; - border-radius: $radius-md; - background: rgba(255, 255, 255, 0.95); - padding: clamp(10px, 1.2vw, 18px); - box-shadow: $shadow-md; - transition: transform $transition-fast, box-shadow $transition-fast; - - .carousel-slide:hover & { - transform: scale(1.08); - box-shadow: $shadow-lg; - } -} - -.carousel-badge { - display: flex; - align-items: center; - justify-content: center; - width: clamp(100px, 6.5vw, 170px); - height: clamp(100px, 6.5vw, 170px); - border-radius: $radius-md; - background: rgba(255, 255, 255, 0.95); - border: 2px solid $neutral-light; - box-shadow: $shadow-md; - font-family: $font-heading; - font-size: clamp(1.8rem, 3vw, 3rem); - font-weight: 700; - color: $primary-blue; - transition: transform $transition-fast, box-shadow $transition-fast; - - .carousel-slide:hover & { - transform: scale(1.08); - box-shadow: $shadow-lg; - } -} - -// ── Takeover Overlay ──────────────────────────────────────── - -.takeover-overlay { - position: fixed; - inset: 0; - z-index: $z-modal; - background: rgba($primary-blue-dark, 0.85); - display: flex; - align-items: center; - justify-content: center; - padding: $spacing-md; - @include fade-in; -} - -.takeover-dialog { - position: relative; - background: $neutral-white; - border-radius: $radius-xl; - padding: $spacing-lg $spacing-md; - max-width: 460px; - width: 100%; - box-shadow: $shadow-xl; - animation: takeover-slide-up 0.3s ease-out; -} - -@keyframes takeover-slide-up { - from { - opacity: 0; - transform: translateY(30px) scale(0.97); - } - - to { - opacity: 1; - transform: translateY(0) scale(1); - } -} - -.takeover-close { - position: absolute; - top: $spacing-xs; - right: $spacing-xs; - background: none; - border: none; - font-size: 1.4rem; - line-height: 1; - cursor: pointer; - color: $neutral-medium; - padding: $spacing-xs; - transition: color $transition-fast; - - &:hover { - color: $neutral-dark; - } -} - -.takeover-content { - text-align: left; - margin-top: $spacing-sm; -} - -.takeover-logo { - display: block; - width: clamp(150px, 30vw, 280px); - height: clamp(150px, 30vw, 280px); - margin: 0 auto $spacing-sm; - border-radius: $radius-lg; - object-fit: contain; - background: $neutral-cream; - border: 1px solid $neutral-light; -} - -.takeover-name { - font-family: $font-heading; - font-size: 1.3rem; - color: $primary-blue; - margin: 0 0 0.35rem; - text-align: left; -} - -.takeover-description { - font-size: 0.875rem; - color: $neutral-dark; - line-height: 1.6; - margin: 0 0 $spacing-md 0; - text-align: left; - max-height: 150px; - overflow-y: auto; - - &::-webkit-scrollbar { - width: 6px; - } - - &::-webkit-scrollbar-track { - background: $neutral-cream; - border-radius: 3px; - } - - &::-webkit-scrollbar-thumb { - background: $neutral-light; - border-radius: 3px; - } -} - -.takeover-visit { - @include button-style($accent-orange, $neutral-white, $accent-orange-dark); - font-size: 1rem; - padding: $spacing-sm $spacing-xl; - letter-spacing: 0.03em; - display: inline-block; -} - -// ── Download CTAs ─────────────────────────────────────────── - -.hero-download { - display: flex; - flex-direction: column; - align-items: center; - gap: $spacing-sm; - margin: $spacing-lg 0 $spacing-md; -} - -.download-label { - font-size: 0.9rem; - color: rgba(255, 255, 255, 0.65); - letter-spacing: 0.03em; -} - -.download-buttons { - display: flex; - gap: $spacing-md; - justify-content: center; - flex-wrap: wrap; -} - -// Both store badges are constrained to the same size (160x52) -.store-link { - display: inline-block; - text-decoration: none; -} - -.store-badge { - width: 160px; - height: 52px; - object-fit: contain; - background: rgba(255, 255, 255, 0.1); - border-radius: $radius-sm; - padding: 4px; - transition: opacity $transition-fast; - - &:hover { - opacity: 0.85; - } -} - -.store-text { - color: var(--color-white); - text-decoration: underline; - font-size: 0.95rem; - padding: $spacing-xs $spacing-sm; - border-radius: $radius-sm; - background: rgba(255, 255, 255, 0.1); - - &:hover { - background: rgba(255, 255, 255, 0.2); - } -} - -// Apple's embed HTML contains its own / — constrain it -.appstore-embed { - display: inline-flex; - align-items: center; - - // Apple's embeds typically wrap an around an - > a, - a { - display: inline-block; - text-decoration: none; - } - - img { - width: 160px; - height: 52px; - object-fit: contain; - display: block; - } - - // If the embed is an iframe (Apple's new style), constrain that too - iframe { - width: 160px; - height: 52px; - border: none; - border-radius: $radius-sm; - } -} - // ── Responsive ────────────────────────────────────────────── @media (max-width: #{$bp-md - 1px}) { @@ -492,8 +193,6 @@ flex-direction: column; align-items: center; } - - // Carousel clamp() handles responsive sizing automatically } @media (max-width: #{$bp-sm - 1px}) { @@ -504,4 +203,4 @@ .hero-line2 { font-size: 1.8rem; } -} \ No newline at end of file +} diff --git a/src/app/hero/hero.component.ts b/src/app/hero/hero.component.ts index 216b6f6..82c7492 100644 --- a/src/app/hero/hero.component.ts +++ b/src/app/hero/hero.component.ts @@ -1,62 +1,23 @@ -import { Component, inject, OnInit, signal } from '@angular/core'; +import { Component, inject } from '@angular/core'; import { RouterLink } from '@angular/router'; import { CommonModule } from '@angular/common'; -import { firstValueFrom } from 'rxjs'; import { StationConfigService } from '../services/station-config.service'; -import { UnderwriterService } from '../services/underwriter.service'; -import { Underwriter } from '../interfaces/underwriter'; +import { UnderwriterCarouselComponent } from '../underwriter-carousel/underwriter-carousel.component'; +import { DownloadCtaComponent } from '../download-cta/download-cta.component'; import { resolveImageUrl } from '../services/app-config.service'; @Component({ selector: 'app-hero', standalone: true, - imports: [RouterLink, CommonModule], + imports: [RouterLink, CommonModule, UnderwriterCarouselComponent, DownloadCtaComponent], templateUrl: './hero.component.html', styleUrl: './hero.component.scss', }) -export class HeroComponent implements OnInit { +export class HeroComponent { readonly config = inject(StationConfigService).config; - private underwriterService = inject(UnderwriterService); - - readonly underwriters = signal([]); - readonly selectedUnderwriter = signal(null); - - carouselPaused = false; - - ngOnInit(): void { - this.loadUnderwriters(); - } - - async loadUnderwriters(): Promise { - try { - const data = await firstValueFrom(this.underwriterService.getUnderwriters(true)); - this.underwriters.set(data); - } catch { - // Silently fail — carousel simply won't render - } - } resolveImageUrl(url: string): string { return resolveImageUrl(url); } - - pauseCarousel(): void { - this.carouselPaused = true; - } - - resumeCarousel(): void { - this.carouselPaused = false; - } - - openTakeover(uw: Underwriter): void { - this.selectedUnderwriter.set(uw); - this.carouselPaused = true; - } - - closeTakeover(): void { - this.selectedUnderwriter.set(null); - this.carouselPaused = false; - } - } diff --git a/src/app/schedule/schedule.component.scss b/src/app/schedule/schedule.component.scss index 380ce54..89cdb60 100644 --- a/src/app/schedule/schedule.component.scss +++ b/src/app/schedule/schedule.component.scss @@ -171,19 +171,28 @@ -webkit-box-orient: vertical; overflow: hidden; - // Standard line-clamp (Firefox 122+) — overrides display for supporting browsers - display: block; - line-clamp: 3; - transition: max-height $transition-slow ease, opacity $transition-normal; &.expanded { -webkit-line-clamp: unset; - line-clamp: unset; overflow: visible; } } +// Standard line-clamp (Firefox 122+) — overrides WebKit for supporting browsers +@supports (line-clamp: 3) { + .show-description { + display: block; + -webkit-line-clamp: unset; + -webkit-box-orient: unset; + line-clamp: 3; + + &.expanded { + line-clamp: unset; + } + } +} + // ── Meta row (host + genre) ──────────────────────────────── .show-meta { diff --git a/src/app/underwriter-carousel/underwriter-carousel.component.html b/src/app/underwriter-carousel/underwriter-carousel.component.html new file mode 100644 index 0000000..30820f7 --- /dev/null +++ b/src/app/underwriter-carousel/underwriter-carousel.component.html @@ -0,0 +1,75 @@ +@if ((underwriters()).length >= 2) { +
+

{{ config().callsign }} is brought to you with the generosity of our underwriters.

+ +
+} + + +@if (selectedUnderwriter()) { + @let uw = selectedUnderwriter()!; +
+ +
+} diff --git a/src/app/underwriter-carousel/underwriter-carousel.component.scss b/src/app/underwriter-carousel/underwriter-carousel.component.scss new file mode 100644 index 0000000..4a32977 --- /dev/null +++ b/src/app/underwriter-carousel/underwriter-carousel.component.scss @@ -0,0 +1,217 @@ +@use '../../styles/variables' as *; +@use '../../styles/mixins' as *; + +// ── Underwriter Horizontal Carousel ───────────────────────── + +.underwriters-section { + background: $neutral-cream; + border-top: 2px solid $neutral-light; + border-bottom: 2px solid $neutral-light; +} + +.underwriters-label { + text-align: center; + padding: $spacing-lg $spacing-lg $spacing-md; + margin: 0; + font-family: $font-heading; + font-size: clamp(1rem, 2vw, 1.3rem); + font-weight: 600; + color: $neutral-dark; + letter-spacing: 0.02em; +} + +.underwriters-carousel { + width: 100%; + overflow: hidden; + padding: clamp($spacing-xl, 4vh, $spacing-3xl) 0; +} + +.carousel-track { + display: flex; + gap: clamp($spacing-xl, 4vw, $spacing-3xl); + width: max-content; + animation: scroll-carousel 45s linear infinite; + + &.paused { + animation-play-state: paused; + } +} + +@keyframes scroll-carousel { + from { + transform: translateX(0); + } + + to { + transform: translateX(-66.666%); + } +} + +.carousel-slide { + flex-shrink: 0; + background: none; + border: none; + padding: 0; + cursor: pointer; + -webkit-tap-highlight-color: transparent; + outline: none; + + &:focus-visible { + + .carousel-logo, + .carousel-badge { + outline: 2px solid $accent-orange; + outline-offset: 2px; + } + } +} + +.carousel-logo { + display: block; + width: clamp(180px, 12vw, 300px); + height: clamp(100px, 6.5vw, 170px); + object-fit: contain; + border-radius: $radius-md; + background: rgba(255, 255, 255, 0.95); + padding: clamp(10px, 1.2vw, 18px); + box-shadow: $shadow-md; + transition: transform $transition-fast, box-shadow $transition-fast; + + .carousel-slide:hover & { + transform: scale(1.08); + box-shadow: $shadow-lg; + } +} + +.carousel-badge { + display: flex; + align-items: center; + justify-content: center; + width: clamp(100px, 6.5vw, 170px); + height: clamp(100px, 6.5vw, 170px); + border-radius: $radius-md; + background: rgba(255, 255, 255, 0.95); + border: 2px solid $neutral-light; + box-shadow: $shadow-md; + font-family: $font-heading; + font-size: clamp(1.8rem, 3vw, 3rem); + font-weight: 700; + color: $primary-blue; + transition: transform $transition-fast, box-shadow $transition-fast; + + .carousel-slide:hover & { + transform: scale(1.08); + box-shadow: $shadow-lg; + } +} + +// ── Takeover Overlay ──────────────────────────────────────── + +.takeover-overlay { + position: fixed; + inset: 0; + z-index: $z-modal; + background: rgba($primary-blue-dark, 0.85); + display: flex; + align-items: center; + justify-content: center; + padding: $spacing-md; + @include fade-in; +} + +.takeover-dialog { + position: relative; + background: $neutral-white; + border-radius: $radius-xl; + padding: $spacing-lg $spacing-md; + max-width: 460px; + width: 100%; + box-shadow: $shadow-xl; + animation: takeover-slide-up 0.3s ease-out; +} + +@keyframes takeover-slide-up { + from { + opacity: 0; + transform: translateY(30px) scale(0.97); + } + + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +.takeover-close { + position: absolute; + top: $spacing-xs; + right: $spacing-xs; + background: none; + border: none; + font-size: 1.4rem; + line-height: 1; + cursor: pointer; + color: $neutral-medium; + padding: $spacing-xs; + transition: color $transition-fast; + + &:hover { + color: $neutral-dark; + } +} + +.takeover-content { + text-align: left; + margin-top: $spacing-sm; +} + +.takeover-logo { + display: block; + width: clamp(150px, 30vw, 280px); + height: clamp(150px, 30vw, 280px); + margin: 0 auto $spacing-sm; + border-radius: $radius-lg; + object-fit: contain; + background: $neutral-cream; + border: 1px solid $neutral-light; +} + +.takeover-name { + font-family: $font-heading; + font-size: 1.3rem; + color: $primary-blue; + margin: 0 0 0.35rem; + text-align: left; +} + +.takeover-description { + font-size: 0.875rem; + color: $neutral-dark; + line-height: 1.6; + margin: 0 0 $spacing-md 0; + text-align: left; + max-height: 150px; + overflow-y: auto; + + &::-webkit-scrollbar { + width: 6px; + } + + &::-webkit-scrollbar-track { + background: $neutral-cream; + border-radius: 3px; + } + + &::-webkit-scrollbar-thumb { + background: $neutral-light; + border-radius: 3px; + } +} + +.takeover-visit { + @include button-style($accent-orange, $neutral-white, $accent-orange-dark); + font-size: 1rem; + padding: $spacing-sm $spacing-xl; + letter-spacing: 0.03em; + display: inline-block; +} diff --git a/src/app/underwriter-carousel/underwriter-carousel.component.ts b/src/app/underwriter-carousel/underwriter-carousel.component.ts new file mode 100644 index 0000000..703741d --- /dev/null +++ b/src/app/underwriter-carousel/underwriter-carousel.component.ts @@ -0,0 +1,60 @@ +import { Component, inject, OnInit, signal } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { firstValueFrom } from 'rxjs'; + +import { StationConfigService } from '../services/station-config.service'; +import { UnderwriterService } from '../services/underwriter.service'; +import { Underwriter } from '../interfaces/underwriter'; +import { resolveImageUrl } from '../services/app-config.service'; + +@Component({ + selector: 'app-underwriter-carousel', + standalone: true, + imports: [CommonModule], + templateUrl: './underwriter-carousel.component.html', + styleUrl: './underwriter-carousel.component.scss', +}) +export class UnderwriterCarouselComponent implements OnInit { + readonly config = inject(StationConfigService).config; + private underwriterService = inject(UnderwriterService); + + readonly underwriters = signal([]); + readonly selectedUnderwriter = signal(null); + + carouselPaused = false; + + ngOnInit(): void { + this.loadUnderwriters(); + } + + async loadUnderwriters(): Promise { + try { + const data = await firstValueFrom(this.underwriterService.getUnderwriters(true)); + this.underwriters.set(data); + } catch { + // Silently fail — carousel simply won't render + } + } + + resolveImageUrl(url: string): string { + return resolveImageUrl(url); + } + + pauseCarousel(): void { + this.carouselPaused = true; + } + + resumeCarousel(): void { + this.carouselPaused = false; + } + + openTakeover(uw: Underwriter): void { + this.selectedUnderwriter.set(uw); + this.carouselPaused = true; + } + + closeTakeover(): void { + this.selectedUnderwriter.set(null); + this.carouselPaused = false; + } +}