feat(frontend): restyle and design frontend
This commit is contained in:
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user