diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index d648ade..3dd46ba 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -7,6 +7,7 @@ import Home from './pages/Home';
import {useEffect} from "react";
import MyClips from './pages/MyClips';
import VideoPlayer from "./pages/VideoPlayer.tsx";
+import EditClip from "./pages/EditClip.tsx";
import Profile from "./pages/Profile.tsx";
import { GoogleOAuthProvider } from '@react-oauth/google';
@@ -26,6 +27,7 @@ function App() {
} />
} />
} />
+ } />
} />
diff --git a/frontend/src/components/video/VideoCard.tsx b/frontend/src/components/video/VideoCard.tsx
index a635ad4..c4131c5 100644
--- a/frontend/src/components/video/VideoCard.tsx
+++ b/frontend/src/components/video/VideoCard.tsx
@@ -3,13 +3,14 @@ import { formatTime, stringToDate, dateToTimeAgo } from "../../utils/utils.ts";
import { Link } from "react-router-dom";
import { useEffect, useRef, useState } from "react";
import { isThumbnailAvailable } from "../../utils/endpoints.ts";
-import { MoreVertical, Trash2 } from "lucide-react";
+import { MoreVertical, Pencil, Trash2 } from "lucide-react";
type VideoCardProps = {
id: number,
title: string,
duration: number,
createdAt: string,
+ onEdit?: () => void,
onDelete?: () => void,
className?: string,
}
@@ -17,7 +18,7 @@ type VideoCardProps = {
const fallbackThumbnail = "../../../public/default_thumbnail.png";
const API_URL = import.meta.env.VITE_API_URL;
-const VideoCard = ({ id, title, duration, createdAt, onDelete, className }: VideoCardProps) => {
+const VideoCard = ({ id, title, duration, createdAt, onEdit, onDelete, className }: VideoCardProps) => {
const [timeAgo, setTimeAgo] = useState(dateToTimeAgo(stringToDate(createdAt)));
const [thumbnailAvailable, setThumbnailAvailable] = useState(true);
const [menuOpen, setMenuOpen] = useState(false);
@@ -66,7 +67,7 @@ const VideoCard = ({ id, title, duration, createdAt, onDelete, className }: Vide
{formatTime(duration)}
- {onDelete && (
+ {(onEdit || onDelete) && (
- {!confirmDelete && (
-
- )}
+ {!confirmDelete && (<>
+ {onEdit && (
+
+ )}
+ {onDelete && (
+
+ )}
+ >)}
{confirmDelete && (
Delete this clip?
diff --git a/frontend/src/pages/EditClip.tsx b/frontend/src/pages/EditClip.tsx
new file mode 100644
index 0000000..58d86b1
--- /dev/null
+++ b/frontend/src/pages/EditClip.tsx
@@ -0,0 +1,108 @@
+import { useEffect, useState } from "react";
+import { useNavigate, useParams } from "react-router-dom";
+import { ArrowLeft, Check } from "lucide-react";
+import { getClipById, patchClip } from "../utils/endpoints.ts";
+import Box from "../components/Box.tsx";
+import BlueButton from "../components/buttons/BlueButton.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 EditClip = () => {
+ const { id } = useParams();
+ const navigate = useNavigate();
+
+ const [title, setTitle] = useState("");
+ const [description, setDescription] = useState("");
+ const [loading, setLoading] = useState(true);
+ const [saving, setSaving] = useState(false);
+ const [saved, setSaved] = useState(false);
+ const [error, setError] = useState
(null);
+
+ useEffect(() => {
+ if (!id) return;
+ getClipById(id)
+ .then((clip) => {
+ if (clip) {
+ setTitle(clip.title ?? "");
+ setDescription(clip.description ?? "");
+ }
+ })
+ .catch(() => setError("Failed to load clip."))
+ .finally(() => setLoading(false));
+ }, [id]);
+
+ const handleSave = async () => {
+ if (!id) return;
+ setSaving(true);
+ setError(null);
+ try {
+ await patchClip(Number(id), { title, description });
+ setSaved(true);
+ setTimeout(() => setSaved(false), 2000);
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Failed to save.");
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ return (
+
+
+
+
Edit Clip
+
+ {loading ? (
+
Loading...
+ ) : (
+
+
+
+ setTitle(e.target.value)}
+ placeholder="Enter title"
+ className={inputClass}
+ />
+
+
+
+
+
+
+ {error && {error}
}
+
+
+
+ {saving ? "Saving…" : saved
+ ? Saved
+ : "Save"}
+
+
+
+
+ )}
+
+ );
+};
+
+export default EditClip;
diff --git a/frontend/src/pages/MyClips.tsx b/frontend/src/pages/MyClips.tsx
index 60e3a18..bf4f502 100644
--- a/frontend/src/pages/MyClips.tsx
+++ b/frontend/src/pages/MyClips.tsx
@@ -1,11 +1,13 @@
import VideoCard from "../components/video/VideoCard";
import { useEffect, useState } from "react";
+import { useNavigate } from "react-router-dom";
import { getClips, deleteClip } from "../utils/endpoints";
import type { Clip } from "../utils/types";
const MyClips = () => {
const [clips, setClips] = useState([]);
const [error, setError] = useState(null);
+ const navigate = useNavigate();
useEffect(() => {
getClips()
@@ -34,6 +36,7 @@ const MyClips = () => {
title={clip.title}
duration={clip.duration}
createdAt={clip.createdAt}
+ onEdit={() => navigate(`/clips/${clip.id}/edit`)}
onDelete={() => handleDelete(clip.id)}
/>
))}
diff --git a/frontend/src/utils/endpoints.ts b/frontend/src/utils/endpoints.ts
index e944516..159dd03 100644
--- a/frontend/src/utils/endpoints.ts
+++ b/frontend/src/utils/endpoints.ts
@@ -253,6 +253,26 @@ const getVideoBlob = async(id: string): Promise => {
}
}
+const patchClip = async (id: number, data: { title?: string; description?: string }): Promise => {
+ const response = await fetch(API_URL + `/api/v1/clips/${id}`, {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json' },
+ credentials: 'include',
+ body: JSON.stringify(data),
+ });
+
+ if (!response.ok) {
+ throw new Error(`Failed to update clip: ${response.status}`);
+ }
+
+ const result: APIResponse = await response.json();
+ if (result.status === 'error') {
+ throw new Error(`Failed to update clip: ${result.message}`);
+ }
+
+ return result.data;
+};
+
const deleteClip = async (id: number): Promise => {
const response = await fetch(API_URL + `/api/v1/clips/${id}`, {
method: 'DELETE',
@@ -289,6 +309,7 @@ export {
getClips,
getClipById,
getVideoBlob,
+ patchClip,
deleteClip,
isThumbnailAvailable
};
\ No newline at end of file
diff --git a/src/main/java/com/ddf/vodsystem/configuration/CorsConfig.java b/src/main/java/com/ddf/vodsystem/configuration/CorsConfig.java
index 3812aed..c9d8c4b 100644
--- a/src/main/java/com/ddf/vodsystem/configuration/CorsConfig.java
+++ b/src/main/java/com/ddf/vodsystem/configuration/CorsConfig.java
@@ -19,7 +19,7 @@ public class CorsConfig {
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/v1/**")
.allowedOrigins(frontendUrl)
- .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
+ .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")
.allowedHeaders("*")
.allowCredentials(true);
}