all other page restyling
All checks were successful
CI / Maven unit tests (push) Successful in 13s
CI / Build and push images (push) Has been skipped
CI / Deploy to server (push) Has been skipped

This commit is contained in:
2026-08-16 16:02:49 +01:00
parent 20693df628
commit 512ebf4957
20 changed files with 180 additions and 157 deletions

View File

@@ -7,8 +7,8 @@ type Props = {
const Selector = ({ children, label }: Props) => (
<div className="flex items-center gap-2">
<label className="w-full text-sm text-gray-600">{label}</label>
<div className="w-px h-5 bg-gray-200 mx-2" />
<label className="w-full text-sm text-text-secondary">{label}</label>
<div className="w-px h-5 text-text-secondary mx-2" />
{children}
</div>
);

View File

@@ -76,8 +76,8 @@ const Topbar = ({
{user.name}
{isStreaming && (
<span className="relative flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-500 opacity-75" />
<span className="relative inline-flex rounded-full h-2 w-2 bg-red-500" />
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-[#B5432E] opacity-75" />
<span className="relative inline-flex rounded-full h-2 w-2 bg-[#B5432E]" />
</span>
)}
</span>
@@ -86,7 +86,7 @@ const Topbar = ({
onClick={() => navigate("/profile")} />
<DropdownItem item="Logout"
onClick={() => handleLogout()}
className={"text-red-500 font-medium"} />
className={"text-[#B5432E] font-medium"} />
</Dropdown>
</div>
) :

View File

@@ -1,14 +0,0 @@
import React from "react";
type Props = React.ButtonHTMLAttributes<HTMLButtonElement>;
const BlueButton: React.FC<Props> = ({ className = "", ...props }) => {
return (
<button
className={`bg-primary text-white font-medium rounded-lg hover:bg-primary-pressed transition-colors duration-150 h-10 px-4 ${className}`}
{...props}
/>
);
};
export default BlueButton;

View File

@@ -6,7 +6,7 @@ type Props = React.ButtonHTMLAttributes<HTMLButtonElement>;
const MenuButton: React.FC<Props> = ({ className = "", ...props }) => {
return (
<button
className={clsx("p-2 rounded-md text-gray-600 hover:bg-gray-100 hover:text-gray-900 transition-colors duration-150", className)}
className={clsx("p-2 rounded-md text-text-secondary hover:bg-hover hover:text-text-primary transition-colors duration-150", className)}
{...props}
/>
);

View File

@@ -0,0 +1,14 @@
import React from "react";
type Props = React.ButtonHTMLAttributes<HTMLButtonElement>;
const PrimaryButton: React.FC<Props> = ({ className = "", ...props }) => {
return (
<button
className={`bg-[#A85A38] text-[#FDFBF7] font-body text-sm rounded-lg hover:bg-[#94492A] disabled:bg-[#C9AE9C] transition-colors duration-150 h-10 px-4 ${className}`}
{...props}
/>
);
};
export default PrimaryButton;

View File

@@ -8,7 +8,7 @@ type Props = {
className?: string;
};
const inputClass = "border border-gray-300 bg-white rounded-md w-full p-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-colors";
const inputClass = "border border-hairline bg-card rounded-md w-full p-2 text-sm focus:outline-none focus:ring-2 focus:ring-muted transition-colors";
export default function ConfigBox({ setMetadata, className }: Props) {
const updateRes = (e: React.ChangeEvent<HTMLSelectElement>) => {
@@ -26,7 +26,7 @@ export default function ConfigBox({ setMetadata, className }: Props) {
return (
<div className={clsx("flex flex-col gap-3 p-6", className)}>
<h2 className="text-sm font-semibold text-gray-800 mb-1">Export Settings</h2>
<h2 className="text-sm text-text-primary font-semibold mb-1">Export Settings</h2>
<Selector label="Resolution">
<select id="resolution" name="resolution" defaultValue="1280,720"

View File

@@ -1,4 +1,4 @@
import BlueButton from "../buttons/BlueButton.tsx";
import PrimaryButton from "../buttons/PrimaryButton.tsx";
import React from "react";
type props = {
@@ -24,24 +24,24 @@ const Spinner = () => (
const ExportWidget = ({dataSend, handleDownload, downloadable, progress, uploading}: props) => {
return (
<div className={"flex flex-col gap-3"}>
<BlueButton
<PrimaryButton
onClick={dataSend}
disabled={uploading}
className={uploading ? "opacity-60 cursor-not-allowed flex items-center justify-center gap-2" : ""}
>
{uploading && <Spinner />}
{uploading ? "Uploading..." : "Export"}
</BlueButton>
</PrimaryButton>
<div className="h-10 flex items-center">
{ downloadable ?
(<BlueButton className="w-full" onClick={handleDownload}>
(<PrimaryButton className="w-full" onClick={handleDownload}>
Download
</BlueButton>)
</PrimaryButton>)
:(
<progress
value={progress}
className="w-full h-1.5 rounded bg-gray-200">
className="w-full h-1.5 rounded bg-fields">
</progress>
)
}

View File

@@ -9,12 +9,12 @@ type Props = {
const MetadataBox = ({ setMetadata, className }: Props) => (
<div className={clsx("flex flex-col content-between p-6 gap-2", className)}>
<p className="w-full font-semibold text-gray-800 text-sm">Title</p>
<p className="w-full font-body text-text-strong font-bold text-sm">Title</p>
<input
type="text"
placeholder="Enter title"
onChange={(e) => setMetadata(prev => ({ ...prev, title: e.target.value }))}
className="border border-gray-300 bg-white rounded-md w-full p-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-colors"
className="border border-hairline bg-fields rounded-md w-full p-2 text-sm focus:outline-none focus:ring-2 focus:ring-muted transition-colors"
/>
</div>
);

View File

@@ -62,11 +62,11 @@ export default function Playbar({ video, videoMetadata, className }: Props) {
</div>
<button
onClick={togglePlay}
className="hover:bg-gray-100 rounded-full p-1.5 transition-colors duration-150 text-gray-700"
className="hover:bg-hover rounded-full p-1.5 transition-colors duration-150 text-text-body"
>
{isPlaying ? <Pause size={24} /> : <Play size={24} />}
</button>
<span>
<span className="font-data text-sm text-text-secondary tabular-nums">
{formatTime(video?.currentTime ?? 0)} / {formatTime(videoMetadata.duration ?? 0)}
</span>
</div>

View File

@@ -70,7 +70,7 @@ const VideoCard = ({ id, title, duration, createdAt, onEdit, onDelete, className
className="w-full aspect-video object-cover group-hover:scale-105 transition-transform duration-200"
/>
<span className="absolute top-1.5 left-1.5 bg-black/70 text-white px-1.5 py-0.5 rounded text-xs font-medium pointer-events-none">
<span className="absolute top-1.5 left-1.5 bg-[#2B2620]/75 text-[#FBF8F2] px-1.5 py-0.5 rounded text-xs font-data pointer-events-none">
{formatTime(duration)}
</span>
@@ -82,18 +82,18 @@ const VideoCard = ({ id, title, duration, createdAt, onEdit, onDelete, className
>
<button
onClick={(e) => stopAndRun(e, () => { setMenuOpen(v => !v); setConfirmDelete(false); })}
className="bg-black/70 hover:bg-black/90 text-white rounded p-0.5 transition-colors"
className="bg-[#2B2620]/75 hover:bg-[#2B2620]/90 text-[#FBF8F2] rounded p-0.5 transition-colors"
>
<MoreVertical size={14} />
</button>
{menuOpen && (
<div className="absolute right-0 top-7 w-32 bg-white rounded-lg border border-gray-200 shadow-md z-50 py-1 overflow-hidden">
<div className="absolute right-0 top-7 w-32 bg-card rounded-lg border border-hairline shadow-md z-50 py-1 overflow-hidden">
{!confirmDelete && (<>
{onEdit && (
<button
onClick={(e) => stopAndRun(e, onEdit)}
className="flex items-center gap-2 w-full px-3 py-2 text-sm text-gray-700 hover:bg-gray-50"
className="flex items-center gap-2 w-full px-3 py-2 text-sm text-text-body hover:bg-hover"
>
<Pencil size={13} /> Edit
</button>
@@ -101,7 +101,7 @@ const VideoCard = ({ id, title, duration, createdAt, onEdit, onDelete, className
{onDelete && (
<button
onClick={(e) => stopAndRun(e, () => setConfirmDelete(true))}
className="flex items-center gap-2 w-full px-3 py-2 text-sm text-red-500 hover:bg-red-50"
className="flex items-center gap-2 w-full px-3 py-2 text-sm text-[#B5432E] hover:bg-hover"
>
<Trash2 size={13} /> Delete
</button>
@@ -109,17 +109,17 @@ const VideoCard = ({ id, title, duration, createdAt, onEdit, onDelete, className
</>)}
{confirmDelete && (
<div className="px-3 py-2">
<p className="text-xs text-gray-600 mb-2">Delete this {itemLabel}?</p>
<p className="text-xs text-text-secondary mb-2">Delete this {itemLabel}?</p>
<div className="flex gap-1.5">
<button
onClick={(e) => stopAndRun(e, onDelete!)}
className="flex-1 text-xs bg-red-500 text-white rounded px-2 py-1 hover:bg-red-600 transition-colors"
className="flex-1 text-xs bg-[#B5432E] text-[#FDFBF7] rounded px-2 py-1 hover:bg-[#94492A] transition-colors"
>
Delete
</button>
<button
onClick={(e) => stopAndRun(e, () => setConfirmDelete(false))}
className="flex-1 text-xs bg-gray-100 text-gray-600 rounded px-2 py-1 hover:bg-gray-200 transition-colors"
className="flex-1 text-xs bg-fields text-text-secondary rounded px-2 py-1 hover:bg-hover transition-colors"
>
Cancel
</button>
@@ -133,10 +133,10 @@ const VideoCard = ({ id, title, duration, createdAt, onEdit, onDelete, className
</div>
<div className="flex flex-col p-2">
<p className="font-medium text-gray-900 text-sm leading-snug line-clamp-2">
<p className="font-body text-text-strong text-sm leading-snug line-clamp-2">
{title === "" ? "(No Title)" : title}
</p>
<p className="text-gray-500 text-xs mt-0.5">{timeAgo}</p>
<p className="text-muted text-xs mt-0.5">{timeAgo}</p>
</div>
</div>
</Link>

View File

@@ -45,3 +45,26 @@ body {
font-weight: 400;
color: #2B2620;
}
/* Clip trim slider (react-range-slider-input). Its own stylesheet ships blue
defaults; the #id beats those class selectors regardless of import order. */
#range-slider {
height: 8px;
background: var(--color-fields);
border-radius: 8px;
}
#range-slider .range-slider__range {
background: #C87A52;
}
#range-slider .range-slider__thumb {
width: 18px;
height: 18px;
background: #A85A38;
box-shadow: 0 1px 2px rgba(90, 40, 20, 0.2);
}
#range-slider .range-slider__thumb:focus-visible {
box-shadow: 0 0 0 4px rgba(168, 90, 56, 0.35);
}

View File

@@ -79,7 +79,7 @@ const ClipEdit = () => {
const interval = setInterval(async () => await pollProgress(jobId, interval), 500);
};
const pollProgress = async (jobId: string, intervalId: number) => {
const pollProgress = async (jobId: string, intervalId: ReturnType<typeof setInterval>) => {
getJob(jobId)
.then((job) => {
setProgress(job.progress);
@@ -128,7 +128,7 @@ const ClipEdit = () => {
<video
ref={videoRef}
src={videoUrl}
className={"w-full rounded-lg shadow-sm bg-black"}
className={"w-full rounded-lg shadow-sm bg-text-primary"}
onLoadedMetadata={handleVideoMetadataLoaded}
/>
@@ -142,7 +142,7 @@ const ClipEdit = () => {
<Playbar
video={videoRef.current}
videoMetadata={metadata}
className={"w-full accent-primary"}
className={"w-full accent-[#A85A38]"}
/>
<PlaybackSlider
@@ -150,7 +150,7 @@ const ClipEdit = () => {
videoMetadata={metadata}
sliderValue={playbackValue}
setSliderValue={setPlaybackValue}
className={"w-full accent-primary"}
className={"w-full accent-[#A85A38]"}
/>
<ClipRangeSlider
@@ -158,11 +158,11 @@ const ClipEdit = () => {
videoMetadata={metadata}
setSliderValue={setPlaybackValue}
setMetadata={setOutputMetadata}
className={"w-full mt-2 bg-primary"}
className={"w-full mt-2"}
/>
{error && (
<div className={"text-red-600 text-center mt-2"}>
<div className={"text-[#B5432E] text-center mt-2"}>
{error}
</div>
)}

View File

@@ -3,9 +3,9 @@ import { useNavigate, useParams } from "react-router-dom";
import { ArrowLeft, Check } from "lucide-react";
import { getClipById, patchClip } from "../utils/api/clips.ts";
import Box from "../components/Box.tsx";
import BlueButton from "../components/buttons/BlueButton.tsx";
import PrimaryButton from "../components/buttons/PrimaryButton.tsx";
const inputClass = "border border-gray-300 bg-white rounded-md w-full p-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-colors";
const inputClass = "border border-hairline bg-fields rounded-md w-full p-2 text-sm focus:outline-none focus:ring-2 focus:ring-muted transition-colors";
const EditClip = () => {
const { id } = useParams();
@@ -50,20 +50,20 @@ const EditClip = () => {
<div className="px-8 py-10 max-w-2xl mx-auto">
<button
onClick={() => navigate(-1)}
className="flex items-center gap-1.5 text-sm text-gray-500 hover:text-gray-900 mb-6 transition-colors duration-150"
className="flex items-center gap-1.5 text-sm text-text-secondary hover:text-text-primary mb-6 transition-colors duration-150"
>
<ArrowLeft size={16} />
Back
</button>
<h1 className="text-2xl font-semibold text-gray-900 mb-6">Edit Clip</h1>
<h1 className="text-4xl font-heading text-text-primary mb-6">Edit Clip</h1>
{loading ? (
<div className="text-gray-400 text-sm">Loading...</div>
<div className="text-muted text-sm">Loading...</div>
) : (
<Box className="p-6 flex flex-col gap-4">
<div className="flex flex-col gap-1.5">
<label className="text-sm font-medium text-gray-700">Title</label>
<label className="text-sm font-medium text-text-secondary">Title</label>
<input
type="text"
value={title}
@@ -74,7 +74,7 @@ const EditClip = () => {
</div>
<div className="flex flex-col gap-1.5">
<label className="text-sm font-medium text-gray-700">Description</label>
<label className="text-sm font-medium text-text-secondary">Description</label>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
@@ -84,17 +84,17 @@ const EditClip = () => {
/>
</div>
{error && <p className="text-sm text-red-500">{error}</p>}
{error && <p className="text-sm text-[#B5432E]">{error}</p>}
<div className="flex items-center gap-3 pt-1">
<BlueButton onClick={handleSave} disabled={saving} className="w-28">
<PrimaryButton onClick={handleSave} disabled={saving} className="w-28">
{saving ? "Saving…" : saved
? <span className="flex items-center gap-1.5 justify-center"><Check size={14} />Saved</span>
: "Save"}
</BlueButton>
</PrimaryButton>
<button
onClick={() => navigate(-1)}
className="text-sm text-gray-500 hover:text-gray-900 transition-colors duration-150"
className="text-sm text-text-secondary hover:text-text-primary transition-colors duration-150"
>
Cancel
</button>

View File

@@ -3,9 +3,9 @@ import { useNavigate, useParams } from "react-router-dom";
import { ArrowLeft, Check } from "lucide-react";
import { getVodById, patchVod } from "../utils/api/vods.ts";
import Box from "../components/Box.tsx";
import BlueButton from "../components/buttons/BlueButton.tsx";
import PrimaryButton from "../components/buttons/PrimaryButton.tsx";
const inputClass = "border border-gray-300 bg-white rounded-md w-full p-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-colors";
const inputClass = "border border-hairline bg-fields rounded-md w-full p-2 text-sm focus:outline-none focus:ring-2 focus:ring-muted transition-colors";
const EditVod = () => {
const { id } = useParams();
@@ -50,20 +50,20 @@ const EditVod = () => {
<div className="px-8 py-10 max-w-2xl mx-auto">
<button
onClick={() => navigate(-1)}
className="flex items-center gap-1.5 text-sm text-gray-500 hover:text-gray-900 mb-6 transition-colors duration-150"
className="flex items-center gap-1.5 text-sm text-text-secondary hover:text-text-primary mb-6 transition-colors duration-150"
>
<ArrowLeft size={16} />
Back
</button>
<h1 className="text-2xl font-semibold text-gray-900 mb-6">Edit VoD</h1>
<h1 className="text-4xl font-heading text-text-primary mb-6">Edit VoD</h1>
{loading ? (
<div className="text-gray-400 text-sm">Loading...</div>
<div className="text-muted text-sm">Loading...</div>
) : (
<Box className="p-6 flex flex-col gap-4">
<div className="flex flex-col gap-1.5">
<label className="text-sm font-medium text-gray-700">Title</label>
<label className="text-sm font-medium text-text-secondary">Title</label>
<input
type="text"
value={title}
@@ -74,7 +74,7 @@ const EditVod = () => {
</div>
<div className="flex flex-col gap-1.5">
<label className="text-sm font-medium text-gray-700">Description</label>
<label className="text-sm font-medium text-text-secondary">Description</label>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
@@ -84,17 +84,17 @@ const EditVod = () => {
/>
</div>
{error && <p className="text-sm text-red-500">{error}</p>}
{error && <p className="text-sm text-[#B5432E]">{error}</p>}
<div className="flex items-center gap-3 pt-1">
<BlueButton onClick={handleSave} disabled={saving} className="w-28">
<PrimaryButton onClick={handleSave} disabled={saving} className="w-28">
{saving ? "Saving…" : saved
? <span className="flex items-center gap-1.5 justify-center"><Check size={14} />Saved</span>
: "Save"}
</BlueButton>
</PrimaryButton>
<button
onClick={() => navigate(-1)}
className="text-sm text-gray-500 hover:text-gray-900 transition-colors duration-150"
className="text-sm text-text-secondary hover:text-text-primary transition-colors duration-150"
>
Cancel
</button>

View File

@@ -59,7 +59,7 @@ const Home = () => {
className="bg-card border border-hairline rounded-xl p-5 flex flex-col gap-3 shadow-sm"
>
<div className="w-9 h-9 rounded-lg flex items-center justify-center shrink-0">
<Icon size={18} className="text-primary" />
<Icon size={18} className="text-[#A85A38]" />
</div>
<div>
<p className="font-body text-text-strong font-bold text-sm mb-1">{title}</p>

View File

@@ -202,16 +202,16 @@ const MediaPlayer = ({ noun, getMediaUrl, fetchDetails }: MediaPlayerProps) => {
if (notAuthenticated) {
return (
<div className="h-full flex flex-col items-center justify-center gap-4 text-center p-8">
<div className="w-14 h-14 rounded-full bg-gray-100 flex items-center justify-center">
<Lock size={24} className="text-gray-400" />
<div className="w-14 h-14 rounded-full bg-fields flex items-center justify-center">
<Lock size={24} className="text-muted" />
</div>
<div>
<p className="text-lg font-semibold text-gray-900">Not authenticated</p>
<p className="text-sm text-gray-500 mt-1">You don't have access to this {noun.toLowerCase()}.</p>
<p className="text-2xl font-heading text-text-primary">Not authenticated</p>
<p className="text-sm text-text-secondary mt-1">You don't have access to this {noun.toLowerCase()}.</p>
</div>
<button
onClick={() => navigate(-1)}
className="flex items-center gap-1.5 text-sm text-gray-500 hover:text-gray-900 transition-colors duration-150"
className="flex items-center gap-1.5 text-sm text-text-secondary hover:text-text-primary transition-colors duration-150"
>
<ArrowLeft size={16} />
Go back
@@ -224,7 +224,7 @@ const MediaPlayer = ({ noun, getMediaUrl, fetchDetails }: MediaPlayerProps) => {
<div className="h-full flex flex-col p-4 gap-3">
<button
onClick={() => navigate(-1)}
className="flex items-center gap-1.5 text-sm text-gray-500 hover:text-gray-900 flex-shrink-0 transition-colors duration-150"
className="flex items-center gap-1.5 text-sm text-text-secondary hover:text-text-primary flex-shrink-0 transition-colors duration-150"
>
<ArrowLeft size={16} />
Back
@@ -233,7 +233,7 @@ const MediaPlayer = ({ noun, getMediaUrl, fetchDetails }: MediaPlayerProps) => {
{/* Player */}
<div
ref={containerRef}
className="relative bg-black rounded-xl overflow-hidden flex-1 min-h-0 select-none"
className="relative bg-[#2B2620] rounded-xl overflow-hidden flex-1 min-h-0 select-none"
onMouseMove={showControls}
onMouseLeave={() => isPlaying && setControlsVisible(false)}
onClick={togglePlay}
@@ -265,7 +265,7 @@ const MediaPlayer = ({ noun, getMediaUrl, fetchDetails }: MediaPlayerProps) => {
{/* Buffering spinner */}
{isWaiting && mediaUrl && (
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
<div className="w-11 h-11 rounded-full border-[3px] border-white/20 border-t-white animate-spin" />
<div className="w-11 h-11 rounded-full border-[3px] border-[#FBF8F2]/20 border-t-[#FBF8F2] animate-spin" />
</div>
)}
@@ -282,20 +282,20 @@ const MediaPlayer = ({ noun, getMediaUrl, fetchDetails }: MediaPlayerProps) => {
{/* Scrub bar */}
<div
ref={progressRef}
className="relative h-1 rounded-full bg-white/25 cursor-pointer mb-3 group/prog hover:h-1.5 transition-[height] duration-150"
className="relative h-1 rounded-full bg-[#FBF8F2]/25 cursor-pointer mb-3 group/prog hover:h-1.5 transition-[height] duration-150"
onMouseDown={(e) => { setIsDragging(true); seekTo(e.clientX); }}
>
<div className="absolute inset-y-0 left-0 bg-white/35 rounded-full" style={{ width: `${bufferedPct}%` }} />
<div className="absolute inset-y-0 left-0 bg-primary rounded-full" style={{ width: `${progressPct}%` }} />
<div className="absolute inset-y-0 left-0 bg-[#FBF8F2]/35 rounded-full" style={{ width: `${bufferedPct}%` }} />
<div className="absolute inset-y-0 left-0 bg-[#C87A52] rounded-full" style={{ width: `${progressPct}%` }} />
<div
className="absolute top-1/2 -translate-y-1/2 w-3 h-3 bg-white rounded-full shadow opacity-0 group-hover/prog:opacity-100 transition-opacity"
className="absolute top-1/2 -translate-y-1/2 w-3 h-3 bg-[#FBF8F2] rounded-full shadow opacity-0 group-hover/prog:opacity-100 transition-opacity"
style={{ left: `calc(${progressPct}% - 6px)` }}
/>
</div>
{/* Buttons row */}
<div className="flex items-center gap-3 text-white">
<button onClick={togglePlay} className="hover:text-white/70 transition-colors">
<div className="flex items-center gap-3 text-[#FBF8F2]">
<button onClick={togglePlay} className="hover:text-[#FBF8F2]/70 transition-colors">
{isPlaying
? <Pause size={20} fill="currentColor" />
: <Play size={20} fill="currentColor" />}
@@ -303,7 +303,7 @@ const MediaPlayer = ({ noun, getMediaUrl, fetchDetails }: MediaPlayerProps) => {
{/* Volume — slider expands on hover */}
<div className="flex items-center gap-1.5 group/vol">
<button onClick={toggleMute} className="hover:text-white/70 transition-colors">
<button onClick={toggleMute} className="hover:text-[#FBF8F2]/70 transition-colors">
<VolumeIcon size={18} />
</button>
<div className="overflow-hidden w-0 group-hover/vol:w-16 transition-[width] duration-200">
@@ -312,18 +312,18 @@ const MediaPlayer = ({ noun, getMediaUrl, fetchDetails }: MediaPlayerProps) => {
min={0} max={1} step={0.02}
value={effectiveVolume}
onChange={handleVolumeChange}
className="w-16 accent-white cursor-pointer"
className="w-16 accent-[#FBF8F2] cursor-pointer"
/>
</div>
</div>
<span className="text-xs font-mono tabular-nums text-white/80">
<span className="text-xs font-data tabular-nums text-[#FBF8F2]/80">
{formatTime(currentTime)} / {formatTime(duration)}
</span>
<div className="flex-1" />
<button onClick={toggleFullscreen} className="hover:text-white/70 transition-colors">
<button onClick={toggleFullscreen} className="hover:text-[#FBF8F2]/70 transition-colors">
{isFullscreen ? <Minimize size={18} /> : <Maximize size={18} />}
</button>
</div>
@@ -331,17 +331,17 @@ const MediaPlayer = ({ noun, getMediaUrl, fetchDetails }: MediaPlayerProps) => {
</div>
</div>
{error && <div className="text-red-500 text-sm mt-3">{error}</div>}
{error && <div className="text-[#B5432E] text-sm mt-3">{error}</div>}
{!mediaLoaded && !error && (
<div className="text-gray-400 text-sm mt-3 flex items-center gap-2">
<div className="w-3 h-3 rounded-full border-2 border-gray-300 border-t-gray-500 animate-spin" />
<div className="text-muted text-sm mt-3 flex items-center gap-2">
<div className="w-3 h-3 rounded-full border-2 border-[#DED7CA] border-t-muted animate-spin" />
Loading video...
</div>
)}
<Box className="p-4 flex-shrink-0">
<p className="text-xl font-semibold text-gray-900">{item?.title || "(No Title)"}</p>
<p className="text-sm text-gray-500 mt-1">{timeAgo}</p>
<p className="text-2xl font-heading text-text-primary">{item?.title || "(No Title)"}</p>
<p className="text-sm text-text-secondary mt-1">{timeAgo}</p>
</Box>
</div>
);

View File

@@ -26,7 +26,7 @@ const MyClips = () => {
return (
<div className="p-6">
<h1 className="text-2xl font-semibold text-gray-900 mb-6">My Clips</h1>
<h1 className="text-4xl font-heading text-text-primary mb-6">My Clips</h1>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
{clips.map((clip) => (
@@ -43,13 +43,13 @@ const MyClips = () => {
</div>
{clips.length === 0 && !error && (
<div className="flex flex-col items-center justify-center py-16 text-gray-400">
<div className="flex flex-col items-center justify-center py-16 text-muted">
<p className="text-base">No clips yet.</p>
<p className="text-sm mt-1">Upload your first clip to get started.</p>
</div>
)}
{error && <p className="text-sm text-red-500 mt-4">{error.toString()}</p>}
{error && <p className="text-sm text-[#B5432E] mt-4">{error.toString()}</p>}
</div>
);
}

View File

@@ -26,7 +26,7 @@ const MyVods = () => {
return (
<div className="p-6">
<h1 className="text-2xl font-semibold text-gray-900 mb-6">My VoDs</h1>
<h1 className="text-4xl font-heading text-text-primary mb-6">My VoDs</h1>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
{vods.map((vod) => (
@@ -46,13 +46,13 @@ const MyVods = () => {
</div>
{vods.length === 0 && !error && (
<div className="flex flex-col items-center justify-center py-16 text-gray-400">
<div className="flex flex-col items-center justify-center py-16 text-muted">
<p className="text-base">No VoDs yet.</p>
<p className="text-sm mt-1">Your stream recordings will appear here.</p>
</div>
)}
{error && <p className="text-sm text-red-500 mt-4">{error}</p>}
{error && <p className="text-sm text-[#B5432E] mt-4">{error}</p>}
</div>
);
};

View File

@@ -23,7 +23,7 @@ const Profile = () => {
if (!user) {
return (
<div className="flex justify-center items-center h-full text-gray-400 text-base">
<div className="flex justify-center items-center h-full text-muted text-base">
Please log in to view your profile.
</div>
);
@@ -31,7 +31,7 @@ const Profile = () => {
return (
<div className="px-8 py-10 max-w-2xl mx-auto">
<h1 className="text-2xl font-semibold text-gray-900 mb-6">Profile</h1>
<h1 className="text-4xl font-heading text-text-primary mb-6">Profile</h1>
<Box className="p-6 mb-4 flex items-center gap-5">
<img
@@ -41,31 +41,31 @@ const Profile = () => {
className="w-20 h-20 rounded-full shadow-sm"
/>
<div>
<p className="text-lg font-semibold text-gray-900">{user.name}</p>
<p className="text-sm text-gray-500">@{user.username}</p>
<p className="text-sm text-gray-500">{user.email}</p>
<p className="text-xs text-gray-400 mt-1">
<p className="text-2xl font-heading text-text-primary">{user.name}</p>
<p className="text-sm text-text-secondary">@{user.username}</p>
<p className="text-sm text-text-secondary">{user.email}</p>
<p className="text-xs text-muted mt-1">
Member since {formatLocalDate(user.createdAt)}
</p>
</div>
</Box>
<Box className="p-6">
<h2 className="text-sm font-semibold text-gray-800 mb-1">Stream Key</h2>
<p className="text-sm text-gray-500 mb-4">
<h2 className="text-xl font-heading text-text-primary mb-1">Stream Key</h2>
<p className="text-sm text-text-secondary mb-4">
Use this key in OBS or any RTMP-compatible software to start streaming.
Keep it private — anyone with this key can stream to your account.
</p>
<div className="flex items-center gap-2">
<div className="flex-1 bg-gray-50 border border-gray-200 rounded-md px-4 py-2 font-mono text-sm text-gray-700 overflow-hidden">
<div className="flex-1 bg-fields border border-hairline rounded-md px-4 py-2 font-data text-sm text-text-strong overflow-hidden">
{keyVisible ? user.streamKey : "•".repeat(user.streamKey.length)}
</div>
<button
onClick={() => setKeyVisible(v => !v)}
title={keyVisible ? "Hide key" : "Show key"}
className="p-2 rounded-md bg-gray-100 hover:bg-gray-200 transition-colors duration-150 text-gray-600"
className="p-2 rounded-md bg-fields hover:bg-hover transition-colors duration-150 text-text-secondary"
>
{keyVisible ? <EyeOff size={16} /> : <Eye size={16} />}
</button>
@@ -73,9 +73,9 @@ const Profile = () => {
<button
onClick={handleCopy}
title="Copy key"
className="p-2 rounded-md bg-gray-100 hover:bg-gray-200 transition-colors duration-150 text-gray-600"
className="p-2 rounded-md bg-fields hover:bg-hover transition-colors duration-150 text-text-secondary"
>
{copied ? <Check size={16} className="text-green-600" /> : <Copy size={16} />}
{copied ? <Check size={16} className="text-[#6E7A50]" /> : <Copy size={16} />}
</button>
</div>
</Box>

View File

@@ -2,7 +2,7 @@ import { useEffect, useState } from "react";
import { Radio, Check, Bookmark, Scissors, Film, Activity, Loader2, X } from "lucide-react";
import clsx from "clsx";
import Box from "../components/Box.tsx";
import BlueButton from "../components/buttons/BlueButton.tsx";
import PrimaryButton from "../components/buttons/PrimaryButton.tsx";
import { getUser } from "../utils/api/users.ts";
import { getCurrentStream, getStreamHistory } from "../utils/api/stream.ts";
import { getMarkers, createMarker, deleteMarker } from "../utils/api/markers.ts";
@@ -11,8 +11,8 @@ import { getJob } from "../utils/api/jobs.ts";
import type { User, StreamStatus, StreamHistoryItem, Marker, JobResponse } from "../utils/types.ts";
import { formatTime, formatLocalDate, stringToDate } from "../utils/utils.ts";
const inputClass = "border border-gray-300 bg-white rounded-md w-full p-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-colors";
const labelClass = "text-xs font-medium text-gray-500 uppercase tracking-wide";
const inputClass = "border border-hairline bg-fields rounded-md w-full p-2 text-sm focus:outline-none focus:ring-2 focus:ring-muted transition-colors";
const labelClass = "font-data text-[11px] text-muted uppercase tracking-[0.12em]";
type TrackedJob = {
uuid: string;
@@ -24,10 +24,10 @@ type TrackedJob = {
const CardHeader = ({ icon: Icon, title, accent = "primary" }: { icon: React.ElementType; title: string; accent?: "primary" | "accent" }) => (
<div className="flex items-center gap-2.5">
<div className={clsx("w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0", accent === "primary" ? "bg-primary/10" : "bg-accent/10")}>
<Icon size={16} className={accent === "primary" ? "text-primary" : "text-accent"} />
<div className={clsx("w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0", accent === "primary" ? "bg-[#A85A38]/10" : "bg-[#6E7A50]/10")}>
<Icon size={16} className={accent === "primary" ? "text-[#A85A38]" : "text-[#6E7A50]"} />
</div>
<h2 className="text-sm font-semibold text-gray-800">{title}</h2>
<h2 className="text-xl font-heading text-text-primary">{title}</h2>
</div>
);
@@ -35,25 +35,25 @@ const JobStatus = ({ job }: { job: TrackedJob }) => {
if (job.state === 'FAILED') {
return (
<div className="flex items-start gap-2 text-sm">
<X size={15} className="text-red-500 mt-0.5 flex-shrink-0" />
<p className="text-red-500">{job.label} failed: {job.errorOutput ?? "Unknown error"}</p>
<X size={15} className="text-[#B5432E] mt-0.5 flex-shrink-0" />
<p className="text-[#B5432E]">{job.label} failed: {job.errorOutput ?? "Unknown error"}</p>
</div>
);
}
if (job.state === 'SUCCEEDED') {
return (
<div className="flex items-center gap-2 text-sm">
<Check size={15} className="text-green-600 flex-shrink-0" />
<p className="text-gray-700">{job.label} saved</p>
<Check size={15} className="text-[#6E7A50] flex-shrink-0" />
<p className="text-text-body">{job.label} saved</p>
</div>
);
}
return (
<div className="flex items-center gap-2">
<Loader2 size={15} className="text-primary animate-spin flex-shrink-0" />
<Loader2 size={15} className="text-[#A85A38] animate-spin flex-shrink-0" />
<div className="flex-1">
<p className="text-sm text-gray-600 mb-1">{job.label}…</p>
<progress value={job.progress} className="w-full h-1.5 rounded bg-gray-200" />
<p className="text-sm text-text-secondary mb-1">{job.label}…</p>
<progress value={job.progress} className="w-full h-1.5 rounded bg-fields" />
</div>
</div>
);
@@ -226,7 +226,7 @@ const StreamControl = () => {
if (!user) {
return (
<div className="flex justify-center items-center h-full text-gray-400 text-base">
<div className="flex justify-center items-center h-full text-muted text-base">
Please log in to control your stream.
</div>
);
@@ -241,35 +241,35 @@ const StreamControl = () => {
return (
<div className="px-8 py-10 max-w-5xl mx-auto flex flex-col gap-5">
<h1 className="text-2xl font-semibold text-gray-900">Stream Control</h1>
<h1 className="text-4xl font-heading text-text-primary">Stream Control</h1>
{/* Hero status bar */}
<Box className={clsx(
"p-6 flex items-center justify-between gap-6 border-l-4",
isStreaming ? "border-l-red-500" : "border-l-gray-200"
isStreaming ? "border-l-[#B5432E]" : "border-l-hairline"
)}>
<div className="flex items-center gap-3.5">
<span className="relative flex h-3.5 w-3.5">
{isStreaming && <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-500 opacity-75" />}
<span className={clsx("relative inline-flex rounded-full h-3.5 w-3.5", isStreaming ? "bg-red-500" : "bg-gray-300")} />
{isStreaming && <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-[#B5432E] opacity-75" />}
<span className={clsx("relative inline-flex rounded-full h-3.5 w-3.5", isStreaming ? "bg-[#B5432E]" : "bg-[#C4BAA9]")} />
</span>
<div>
<p className="font-semibold text-gray-900 text-lg leading-tight">{isStreaming ? "Live" : "Offline"}</p>
<p className="text-2xl font-heading text-text-primary leading-tight">{isStreaming ? "Live" : "Offline"}</p>
{isStreaming && streamDetails?.startDate ? (
<p className="text-sm text-gray-500">Started {formatLocalDate(streamDetails.startDate)}</p>
<p className="text-sm text-text-secondary">Started {formatLocalDate(streamDetails.startDate)}</p>
) : !isStreaming ? (
<p className="text-sm text-gray-500">Point OBS at your stream key (see Profile) to go live.</p>
<p className="text-sm text-text-secondary">Point OBS at your stream key (see Profile) to go live.</p>
) : null}
</div>
</div>
{isStreaming && elapsed ? (
<div className="text-right">
<p className="text-2xl font-mono font-semibold text-gray-900 tabular-nums">{elapsed}</p>
<p className="text-xs text-gray-400 uppercase tracking-wide">elapsed</p>
<p className="text-2xl font-data text-text-primary tabular-nums">{elapsed}</p>
<p className={labelClass}>elapsed</p>
</div>
) : (
<Radio size={28} className="text-gray-200" />
<Radio size={28} className="text-[#DED7CA]" />
)}
</Box>
@@ -278,7 +278,7 @@ const StreamControl = () => {
{/* Quick mark toolbar */}
<Box className="p-4 flex flex-col gap-3">
<div className="flex items-center gap-3">
<Bookmark size={18} className="text-accent flex-shrink-0" />
<Bookmark size={18} className="text-[#6E7A50] flex-shrink-0" />
<input
type="text"
value={markerMessage}
@@ -287,11 +287,11 @@ const StreamControl = () => {
placeholder="What just happened?"
className={inputClass}
/>
<BlueButton onClick={handleAddMarker} disabled={markerBusy || !markerMessage.trim()} className="whitespace-nowrap">
<PrimaryButton onClick={handleAddMarker} disabled={markerBusy || !markerMessage.trim()} className="whitespace-nowrap">
Add Marker
</BlueButton>
</PrimaryButton>
</div>
{markerError && <p className="text-sm text-red-500">{markerError}</p>}
{markerError && <p className="text-sm text-[#B5432E]">{markerError}</p>}
{markers.length > 0 && (
<div className="flex gap-2 overflow-x-auto pb-0.5 -mx-1 px-1 pt-1.5">
@@ -304,22 +304,22 @@ const StreamControl = () => {
onClick={() => handleChipClick(m.id)}
className={clsx(
"flex flex-col items-start gap-0.5 rounded-lg border px-3 py-1.5 text-left transition-colors duration-150",
isStart && "border-primary bg-primary/5 ring-1 ring-primary",
isEnd && "border-accent bg-accent/5 ring-1 ring-accent",
!isStart && !isEnd && "border-gray-200 hover:border-gray-300 hover:bg-gray-50"
isStart && "border-[#A85A38] bg-[#F6EFE6] ring-1 ring-[#A85A38]",
isEnd && "border-[#6E7A50] bg-[#F6EFE6] ring-1 ring-[#6E7A50]",
!isStart && !isEnd && "border-hairline hover:border-[#E2DACB] hover:bg-hover"
)}
>
<span className="text-xs font-medium text-gray-800 max-w-40 truncate">{m.message}</span>
<span className="text-[11px] text-gray-400">
<span className="text-xs font-medium text-text-strong max-w-40 truncate">{m.message}</span>
<span className="font-data text-[11px] text-muted">
{formatLocalDate(m.timestamp, { hour: "2-digit", minute: "2-digit", second: "2-digit" })}
{isStart && <span className="text-primary font-medium"> · start</span>}
{isEnd && <span className="text-accent font-medium"> · end</span>}
{isStart && <span className="text-[#A85A38] font-medium"> · start</span>}
{isEnd && <span className="text-[#6E7A50] font-medium"> · end</span>}
</span>
</button>
<button
onClick={(e) => { e.stopPropagation(); handleDeleteMarker(m.id); }}
title="Delete marker"
className="hidden group-hover:flex items-center justify-center absolute -top-1.5 -right-1.5 w-4 h-4 rounded-full bg-gray-400 hover:bg-red-500 text-white transition-colors duration-150"
className="hidden group-hover:flex items-center justify-center absolute -top-1.5 -right-1.5 w-4 h-4 rounded-full bg-muted hover:bg-[#B5432E] text-[#FDFBF7] transition-colors duration-150"
>
<X size={10} />
</button>
@@ -346,44 +346,44 @@ const StreamControl = () => {
</div>
<input type="text" placeholder="Title" value={clipTitle} onChange={(e) => setClipTitle(e.target.value)} className={inputClass} />
<textarea placeholder="Description" value={clipDescription} onChange={(e) => setClipDescription(e.target.value)} rows={2} className={inputClass + " resize-none"} />
{clipError && <p className="text-sm text-red-500">{clipError}</p>}
<BlueButton onClick={handleClip} className="self-start mt-auto">Clip</BlueButton>
{clipError && <p className="text-sm text-[#B5432E]">{clipError}</p>}
<PrimaryButton onClick={handleClip} className="self-start mt-auto">Clip</PrimaryButton>
</Box>
<Box className="p-6 flex flex-col gap-3">
<CardHeader icon={Film} title="Save between two markers" accent="accent" />
{markers.length < 2 ? (
<p className="text-sm text-gray-400">Add at least two markers above to save a section between them.</p>
<p className="text-sm text-muted">Add at least two markers above to save a section between them.</p>
) : (
<>
<div className="flex items-center gap-2 flex-wrap">
<span className={clsx(
"text-xs font-medium px-2.5 py-1 rounded-full border",
startMarker ? "border-primary text-primary bg-primary/5" : "border-dashed border-gray-300 text-gray-400"
startMarker ? "border-[#A85A38] text-[#A85A38] bg-[#F6EFE6]" : "border-dashed border-[#DED7CA] text-muted"
)}>
{startMarker ? startMarker.message : "Tap a marker to set start"}
</span>
<span className="text-gray-300">→</span>
<span className="text-[#C4BAA9]">→</span>
<span className={clsx(
"text-xs font-medium px-2.5 py-1 rounded-full border",
endMarker ? "border-accent text-accent bg-accent/5" : "border-dashed border-gray-300 text-gray-400"
endMarker ? "border-[#6E7A50] text-[#6E7A50] bg-[#F6EFE6]" : "border-dashed border-[#DED7CA] text-muted"
)}>
{endMarker ? endMarker.message : "Tap a marker to set end"}
</span>
</div>
<input type="text" placeholder="Title" value={markerSaveTitle} onChange={(e) => setMarkerSaveTitle(e.target.value)} className={inputClass} />
<textarea placeholder="Description" value={markerSaveDescription} onChange={(e) => setMarkerSaveDescription(e.target.value)} rows={2} className={inputClass + " resize-none"} />
{markerSaveError && <p className="text-sm text-red-500">{markerSaveError}</p>}
{markerSaveError && <p className="text-sm text-[#B5432E]">{markerSaveError}</p>}
</>
)}
<BlueButton onClick={handleSaveByMarkers} disabled={markers.length < 2} className="self-start mt-auto">Save</BlueButton>
<PrimaryButton onClick={handleSaveByMarkers} disabled={markers.length < 2} className="self-start mt-auto">Save</PrimaryButton>
</Box>
</div>
<Box className="p-5 flex flex-col gap-3">
<CardHeader icon={Activity} title="Recent actions" />
{jobs.length === 0 ? (
<p className="text-sm text-gray-400">Clip and save actions will show up here.</p>
<p className="text-sm text-muted">Clip and save actions will show up here.</p>
) : (
<div className="flex flex-col gap-3">
{jobs.map((job) => <JobStatus key={job.uuid} job={job} />)}