feat(frontend): restyle and design frontend

This commit is contained in:
2026-06-20 12:52:51 +01:00
parent 1062f72661
commit 91c3259331
21 changed files with 185 additions and 266 deletions

View File

@@ -95,10 +95,10 @@ const ClipEdit = () => {
}, [id]);
return (
<div className={"grid grid-cols-[7fr_3fr]"}>
<div className={"grid grid-cols-[7fr_3fr] gap-4 p-4"}>
<video
ref={videoRef}
className={"w-full rounded-lg shadow-lg border border-gray-300 bg-black m-auto"}>
className={"w-full rounded-lg shadow-sm bg-black"}>
<source src={videoUrl} type="video/mp4" />
<source src={videoUrl} type="video/webm" />
<source src={videoUrl} type="video/ogg" />

View File

@@ -38,20 +38,16 @@ const ClipUpload = () => {
return;
}
uploadFile(file)
.then(uuid => {
if (isVideoFileSupported(file)) {
navigate(`/create/${uuid}`)
} else {
convertFile(uuid);
const interval = setInterval(async() => await pollProgress(uuid, interval), 500);
}
})
.catch((e: Error) => setError(`Failed to upload file: ${e.message}`));
});
const pollProgress = async (id: string, intervalId: number) => {
@@ -71,27 +67,27 @@ const ClipUpload = () => {
}
return (
<Box className={"flex flex-col justify-between gap-3 p-5"}>
<input
type="file"
onChange={(e) => {
const selected = e.target.files?.[0] ?? null;
setFile(selected);
}}
className={"h-100 cursor-pointer rounded-lg border border-dashed border-gray-400 bg-white p-4 text-center hover:bg-gray-50 transition"}
/>
<div className="flex items-start justify-center p-8 min-h-full">
<Box className="flex flex-col gap-4 p-6 w-full max-w-md">
<h1 className="text-lg font-semibold text-gray-900">Upload Clip</h1>
<BlueButton
onClick={press}>
Upload
</BlueButton>
<input
type="file"
onChange={(e) => {
const selected = e.target.files?.[0] ?? null;
setFile(selected);
setError(null);
}}
className="h-36 cursor-pointer rounded-lg border-2 border-dashed border-gray-300 bg-gray-50 p-4 text-center text-sm text-gray-500 hover:bg-gray-100 hover:border-primary transition-colors duration-150"
/>
<label className={"text-center text-red-500"}>{error}</label>
<progress
value={progress}
className={"bg-gray-300 rounded-lg h-1"}>
</progress>
</Box>
<BlueButton onClick={press}>Upload</BlueButton>
{error && <p className="text-sm text-red-500 text-center">{error}</p>}
<progress value={progress} className="w-full h-1 rounded bg-gray-200" />
</Box>
</div>
)
};

View File

@@ -1,42 +1,39 @@
const Home = () => {
return (
<div className={"max-h-screen flex flex-col justify-center items-center px-6 py-12 text-gray-900"}>
{/* Logo */}
<div className="flex flex-col items-center justify-center px-6 py-12 min-h-full">
<img
src="/logo.png"
alt="VoD System Logo"
className={"h-44 mb-8 object-contain"}
className="h-36 mb-8 object-contain"
/>
{/* Description Container */}
<div className={"max-w-xl text-center"}>
<h2 className={"text-3xl font-semibold mb-6 text-gray-700"}>
<div className="max-w-xl text-center">
<h2 className="text-3xl font-semibold mb-4 text-gray-900">
What is the VoD System?
</h2>
<p className={"text-lg leading-relaxed text-gray-600 mb-6"}>
<p className="text-base leading-relaxed text-gray-600 mb-6">
The VoD System is a powerful clip management platform designed to streamline how you
handle your video content. Whether you're a content creator, streamer, or educator,
VoD System lets you:
</p>
<ul className={"list-disc list-inside text-gray-600 mb-8 space-y-2"}>
<ul className="list-disc list-inside text-gray-600 mb-8 space-y-2 text-left">
<li>Upload clips effortlessly and securely.</li>
<li>Edit and trim videos with intuitive controls.</li>
<li>Compress files to specific file sizes.</li>
<li>Organize your clips for quick access and sharing.</li>
</ul>
<p className={"text-lg leading-relaxed text-gray-600"}>
<p className="text-base leading-relaxed text-gray-600 mb-8">
Designed with simplicity and efficiency in mind, VoD System adapts to your workflow,
making video clip management faster and more enjoyable than ever.
</p>
</div>
{/* File Support Note */}
<div className={"bg-gray-100 border border-gray-300 rounded-md p-4 text-sm text-gray-500 max-w-md mx-auto"}>
<strong>Note:</strong> Currently, only <code>.mp4</code> files are supported for upload and processing.
Support for additional video formats will be added in future updates.
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 text-sm text-blue-700 max-w-md mx-auto">
<strong>Note:</strong> Currently, only{" "}
<code className="font-mono bg-blue-100 px-1 rounded">.mp4</code> files are supported
for upload and processing. Support for additional video formats will be added in future updates.
</div>
</div>
);

View File

@@ -14,18 +14,29 @@ const MyClips = () => {
}, []);
return (
<div className={"flex flex-row"}>
{clips.map((clip) => (
<VideoCard
id={clip.id}
title={clip.title}
duration={clip.duration}
createdAt={clip.createdAt}
className={"w-40 m-5"}
/>
))}
<div className="p-6">
<h1 className="text-2xl font-semibold text-gray-900 mb-6">My Clips</h1>
{error}
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
{clips.map((clip) => (
<VideoCard
key={clip.id}
id={clip.id}
title={clip.title}
duration={clip.duration}
createdAt={clip.createdAt}
/>
))}
</div>
{clips.length === 0 && !error && (
<div className="flex flex-col items-center justify-center py-16 text-gray-400">
<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>}
</div>
);
}

View File

@@ -22,7 +22,7 @@ const Profile = () => {
if (!user) {
return (
<div className="flex justify-center items-center h-full text-gray-500 text-lg">
<div className="flex justify-center items-center h-full text-gray-400 text-base">
Please log in to view your profile.
</div>
);
@@ -30,17 +30,17 @@ const Profile = () => {
return (
<div className="px-8 py-10 max-w-2xl mx-auto">
<h1 className="text-2xl font-semibold text-gray-700 mb-6">Profile</h1>
<h1 className="text-2xl font-semibold text-gray-900 mb-6">Profile</h1>
<Box className="p-6 mb-6 flex items-center gap-6">
<Box className="p-6 mb-4 flex items-center gap-5">
<img
src={user.profilePictureUrl}
referrerPolicy="no-referrer"
alt="Profile picture"
className="w-20 h-20 rounded-full shadow"
className="w-20 h-20 rounded-full shadow-sm"
/>
<div>
<p className="text-xl font-semibold text-gray-800">{user.name}</p>
<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">
@@ -50,31 +50,31 @@ const Profile = () => {
</Box>
<Box className="p-6">
<h2 className="text-lg font-semibold text-gray-700 mb-1">Stream Key</h2>
<h2 className="text-sm font-semibold text-gray-800 mb-1">Stream Key</h2>
<p className="text-sm text-gray-500 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-100 border border-gray-300 rounded-md px-4 py-2 font-mono text-sm text-gray-700 overflow-hidden">
<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">
{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-300 hover:bg-gray-400 transition-colors text-gray-700"
className="p-2 rounded-md bg-gray-100 hover:bg-gray-200 transition-colors duration-150 text-gray-600"
>
{keyVisible ? <EyeOff size={18} /> : <Eye size={18} />}
{keyVisible ? <EyeOff size={16} /> : <Eye size={16} />}
</button>
<button
onClick={handleCopy}
title="Copy key"
className="p-2 rounded-md bg-gray-300 hover:bg-gray-400 transition-colors text-gray-700"
className="p-2 rounded-md bg-gray-100 hover:bg-gray-200 transition-colors duration-150 text-gray-600"
>
{copied ? <Check size={18} className="text-green-600" /> : <Copy size={18} />}
{copied ? <Check size={16} className="text-green-600" /> : <Copy size={16} />}
</button>
</div>
</Box>

View File

@@ -1,12 +1,14 @@
import {useEffect, useState} from "react";
import {useParams} from "react-router-dom";
import {useNavigate, useParams} from "react-router-dom";
import { ArrowLeft } from "lucide-react";
import type {Clip} from "../utils/types";
import {getClipById, getVideoBlob } from "../utils/endpoints.ts";
import {getClipById, getVideoBlob} from "../utils/endpoints.ts";
import Box from "../components/Box.tsx"
import {dateToTimeAgo, stringToDate} from "../utils/utils.ts";
const VideoPlayer = () => {
const { id } = useParams();
const navigate = useNavigate();
const [videoUrl, setVideoUrl] = useState<string | undefined>(undefined);
const [error, setError] = useState<string | null>(null);
const [clip, setClip] = useState<Clip | null>(null);
@@ -24,7 +26,6 @@ const VideoPlayer = () => {
setVideoUrl(url);
})
getClipById(id)
.then((fetchedClip) => {setClip(fetchedClip)})
.catch((err) => {
@@ -34,7 +35,6 @@ const VideoPlayer = () => {
}, [id]);
// Update timeAgo live every second
useEffect(() => {
if (!clip || !clip.createdAt) return;
@@ -43,16 +43,24 @@ const VideoPlayer = () => {
setTimeAgo(dateToTimeAgo(date));
};
update(); // initial update
update();
const interval = setInterval(update, 1000);
return () => clearInterval(interval); // cleanup
return () => clearInterval(interval);
}, [clip]);
return (
<div className={"w-9/10 m-auto"}>
<div className="p-6 max-w-5xl mx-auto">
<button
onClick={() => navigate(-1)}
className="flex items-center gap-1.5 text-sm text-gray-500 hover:text-gray-900 mb-4 transition-colors duration-150"
>
<ArrowLeft size={16} />
Back
</button>
<video
className={"w-full h-full rounded-lg m-auto"}
className="w-full rounded-xl shadow-md bg-black"
controls
autoPlay
src={videoUrl}
@@ -63,16 +71,15 @@ const VideoPlayer = () => {
Your browser does not support the video tag.
</video>
{error && <div className="text-red-500 mt-2">{error}</div>}
{!videoUrl && !error && <div className="text-gray-500 mt-2">Loading video...</div>}
{error && <div className="text-red-500 text-sm mt-3">{error}</div>}
{!videoUrl && !error && <div className="text-gray-400 text-sm mt-3">Loading video...</div>}
<Box className={"p-2 m-2 flex flex-col"}>
<p className={"text-2xl font-bold text-gray-600"}>{clip?.title ? clip?.title : "(No Title)"}</p>
<p>{timeAgo}</p>
<Box className="p-4 mt-4">
<p className="text-xl font-semibold text-gray-900">{clip?.title || "(No Title)"}</p>
<p className="text-sm text-gray-500 mt-1">{timeAgo}</p>
</Box>
</div>
);
};
export default VideoPlayer;
export default VideoPlayer;