all other page restyling
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
) :
|
||||
|
||||
@@ -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;
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
14
frontend/src/components/buttons/PrimaryButton.tsx
Normal file
14
frontend/src/components/buttons/PrimaryButton.tsx
Normal 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;
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />)}
|
||||
|
||||
Reference in New Issue
Block a user