Transitions UI to react.js #1
@@ -0,0 +1,36 @@
|
|||||||
|
type RadioControlsProps = {
|
||||||
|
status: string;
|
||||||
|
isPlaying: boolean;
|
||||||
|
onPlay: () => void | Promise<void>;
|
||||||
|
onStop: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function RadioControls({ status, isPlaying, onPlay, onStop }: RadioControlsProps) {
|
||||||
|
return (
|
||||||
|
<section className="radio-controls" aria-label="Radio playback controls">
|
||||||
|
<p className="radio-controls__status" aria-live="polite">
|
||||||
|
{status}
|
||||||
|
</p>
|
||||||
|
<div className="radio-controls__buttons">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="playpause"
|
||||||
|
onClick={() => void onPlay()}
|
||||||
|
disabled={isPlaying}
|
||||||
|
aria-label="Start live stream"
|
||||||
|
>
|
||||||
|
▶️
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="playpause"
|
||||||
|
onClick={onStop}
|
||||||
|
disabled={!isPlaying}
|
||||||
|
aria-label="Stop live stream"
|
||||||
|
>
|
||||||
|
⏹️
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
type WebsitePanelProps = {
|
||||||
|
label: string;
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function WebsitePanel({ label, url }: WebsitePanelProps) {
|
||||||
|
return (
|
||||||
|
<section className="website-panel" aria-label={label}>
|
||||||
|
<header className="website-panel__header">
|
||||||
|
<p className="website-panel__label">{label}</p>
|
||||||
|
<p className="website-panel__url" title={url}>
|
||||||
|
{url}
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
<div className="website-panel__content">
|
||||||
|
<iframe
|
||||||
|
className="website-panel__frame"
|
||||||
|
src={url}
|
||||||
|
title={label}
|
||||||
|
loading="lazy"
|
||||||
|
referrerPolicy="strict-origin-when-cross-origin"
|
||||||
|
sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
export type SiteConfig = {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SITES: SiteConfig[] = [
|
||||||
|
{
|
||||||
|
id: 'kryz-home',
|
||||||
|
label: 'Station Website',
|
||||||
|
url: 'https://kryzradio.org/',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const ACTIVE_SITE = SITES[0];
|
||||||
+111
-50
@@ -2,11 +2,14 @@
|
|||||||
--bg-1: #f8f2e6;
|
--bg-1: #f8f2e6;
|
||||||
--bg-2: #dce9f4;
|
--bg-2: #dce9f4;
|
||||||
--card: #fff9f0;
|
--card: #fff9f0;
|
||||||
|
--panel: #f9f8f3;
|
||||||
|
--panel-line: #d8d1c4;
|
||||||
--ink: #1f2633;
|
--ink: #1f2633;
|
||||||
--muted: #586173;
|
--muted: #586173;
|
||||||
--primary: #0a5f94;
|
--primary: #0a5f94;
|
||||||
--primary-press: #084d79;
|
--primary-press: #084d79;
|
||||||
--disabled: #9fa9b8;
|
--disabled: #9fa9b8;
|
||||||
|
--controls-height: 10dvh;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
@@ -16,7 +19,7 @@
|
|||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
min-height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@@ -25,56 +28,106 @@ body {
|
|||||||
background:
|
background:
|
||||||
radial-gradient(circle at 16% 18%, rgba(255, 255, 255, 0.85) 0%, rgba(255, 255, 255, 0) 44%),
|
radial-gradient(circle at 16% 18%, rgba(255, 255, 255, 0.85) 0%, rgba(255, 255, 255, 0) 44%),
|
||||||
linear-gradient(140deg, var(--bg-1), var(--bg-2));
|
linear-gradient(140deg, var(--bg-1), var(--bg-2));
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-shell {
|
.home-layout {
|
||||||
min-height: 100vh;
|
height: 100dvh;
|
||||||
display: grid;
|
width: 100%;
|
||||||
place-items: center;
|
padding: 1rem;
|
||||||
padding: 1.25rem;
|
padding-bottom: calc(var(--controls-height) + 1rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-card {
|
.website-zone {
|
||||||
width: min(30rem, 100%);
|
height: calc(100dvh - var(--controls-height) - 1rem);
|
||||||
border-radius: 1.25rem;
|
max-height: 90dvh;
|
||||||
background: var(--card);
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.website-panel {
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 1rem;
|
||||||
|
border: 1px solid var(--panel-line);
|
||||||
|
background: var(--panel);
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 1.5rem 3rem rgba(18, 50, 73, 0.17),
|
0 1.2rem 2.2rem rgba(18, 50, 73, 0.12),
|
||||||
inset 0 0.1rem 0.25rem rgba(255, 255, 255, 0.6);
|
inset 0 0.1rem 0.2rem rgba(255, 255, 255, 0.7);
|
||||||
padding: 1.4rem;
|
display: grid;
|
||||||
text-align: center;
|
grid-template-rows: auto 1fr;
|
||||||
animation: card-enter 420ms ease-out;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.eyebrow {
|
.website-panel__header {
|
||||||
|
border-bottom: 1px solid var(--panel-line);
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
background: linear-gradient(180deg, rgba(255, 255, 255, 0.7), rgba(247, 242, 233, 0.9));
|
||||||
|
}
|
||||||
|
|
||||||
|
.website-panel__label {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.14em;
|
letter-spacing: 0.12em;
|
||||||
|
font-size: 0.68rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-size: 0.72rem;
|
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
.website-panel__url {
|
||||||
margin: 0.4rem 0 0.6rem;
|
margin: 0.25rem 0 0;
|
||||||
font-size: clamp(2rem, 8vw, 2.8rem);
|
font-size: clamp(0.82rem, 2.2vw, 0.96rem);
|
||||||
line-height: 1.1;
|
color: var(--ink);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status {
|
.website-panel__content {
|
||||||
margin: 0 0 1.2rem;
|
min-height: 0;
|
||||||
color: var(--muted);
|
|
||||||
font-size: 1rem;
|
|
||||||
min-height: 1.3rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.controls {
|
.website-panel__frame {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: none;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-controls-wrap {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
height: var(--controls-height);
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
padding-bottom: calc(0.5rem + env(safe-area-inset-bottom));
|
||||||
|
border-top: 1px solid rgba(28, 41, 62, 0.16);
|
||||||
|
background: linear-gradient(180deg, rgba(255, 250, 242, 0.95), rgba(239, 235, 225, 0.98));
|
||||||
|
backdrop-filter: blur(7px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-controls {
|
||||||
|
height: 100%;
|
||||||
|
max-width: 48rem;
|
||||||
|
margin: 0 auto;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.75rem;
|
gap: 0.55rem;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr auto;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
.radio-controls__status {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: clamp(0.8rem, 2.2vw, 1rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-controls__buttons {
|
||||||
|
display: grid;
|
||||||
|
grid-auto-flow: column;
|
||||||
|
gap: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-controls button {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 0.7rem;
|
border-radius: 0.7rem;
|
||||||
@@ -87,39 +140,47 @@ button {
|
|||||||
transition: transform 120ms ease, background 120ms ease;
|
transition: transform 120ms ease, background 120ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
button.playpause {
|
.radio-controls button.playpause {
|
||||||
width: 100%;
|
width: clamp(2.9rem, 9vw, 4rem);
|
||||||
aspect-ratio: 1;
|
aspect-ratio: 1 / 1;
|
||||||
font-size: clamp(2rem, 25vw, 6em);
|
font-size: clamp(1.15rem, 4vw, 1.7rem);
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:hover:not(:disabled) {
|
.radio-controls button:hover:not(:disabled) {
|
||||||
background: var(--primary-press);
|
background: var(--primary-press);
|
||||||
}
|
}
|
||||||
|
|
||||||
button:active:not(:disabled) {
|
.radio-controls button:active:not(:disabled) {
|
||||||
transform: translateY(1px);
|
transform: translateY(1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
button:disabled {
|
.radio-controls button:disabled {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
background: var(--disabled);
|
background: var(--disabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes card-enter {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(10px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 560px) {
|
@media (max-width: 560px) {
|
||||||
.controls {
|
.home-layout {
|
||||||
|
padding: 0.65rem;
|
||||||
|
padding-bottom: calc(var(--controls-height) + 0.65rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.website-zone {
|
||||||
|
height: calc(100dvh - var(--controls-height) - 0.65rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-controls-wrap {
|
||||||
|
padding-left: 0.65rem;
|
||||||
|
padding-right: 0.65rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-controls {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
gap: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-controls__buttons {
|
||||||
|
justify-content: start;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@ export default function RootLayout({
|
|||||||
<head>
|
<head>
|
||||||
<meta
|
<meta
|
||||||
httpEquiv="Content-Security-Policy"
|
httpEquiv="Content-Security-Policy"
|
||||||
content="default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; media-src 'self' https://kryz.out.airtime.pro"
|
content="default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; media-src 'self' https://kryz.out.airtime.pro; frame-src 'self' https:; child-src 'self' https:"
|
||||||
/>
|
/>
|
||||||
</head>
|
</head>
|
||||||
<body>{children}</body>
|
<body>{children}</body>
|
||||||
|
|||||||
+14
-15
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { AudioPlayer, CapacitorWindow, STREAM_URL } from './AudioPlayer';
|
import { AudioPlayer, CapacitorWindow, STREAM_URL } from './AudioPlayer';
|
||||||
|
import { RadioControls } from './components/RadioControls';
|
||||||
|
import { WebsitePanel } from './components/WebsitePanel';
|
||||||
|
import { ACTIVE_SITE } from './config/sites';
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
const audioRef = useRef<HTMLAudioElement | null>(null);
|
const audioRef = useRef<HTMLAudioElement | null>(null);
|
||||||
@@ -39,25 +42,21 @@ export default function HomePage() {
|
|||||||
}, [player, backgroundMode]);
|
}, [player, backgroundMode]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="page-shell">
|
<main className="home-layout">
|
||||||
<section className="player-card" aria-label="KRYZ live stream player">
|
<section className="website-zone">
|
||||||
<p className="eyebrow">KRYZ Go!</p>
|
<WebsitePanel label={ACTIVE_SITE.label} url={ACTIVE_SITE.url} />
|
||||||
<h1>Live Radio</h1>
|
</section>
|
||||||
<p className="status" aria-live="polite">
|
|
||||||
{status}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="controls">
|
<div className="radio-controls-wrap">
|
||||||
<button type="button" className='playpause' onClick={() => void player.play(backgroundMode)} disabled={isPlaying}>
|
<RadioControls
|
||||||
▶️
|
status={status}
|
||||||
</button>
|
isPlaying={isPlaying}
|
||||||
<button type="button" className='playpause' onClick={() => player.stop()} disabled={!isPlaying}>
|
onPlay={() => player.play(backgroundMode)}
|
||||||
⏹️
|
onStop={() => player.stop()}
|
||||||
</button>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<audio ref={audioRef} preload="none" />
|
<audio ref={audioRef} preload="none" />
|
||||||
</section>
|
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
import "./.next/types/routes.d.ts";
|
import "./.next/dev/types/routes.d.ts";
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|||||||
Reference in New Issue
Block a user