perf(frontend): implement thumbnail caching #19
@@ -1,8 +1,7 @@
|
||||
import clsx from "clsx";
|
||||
import { formatTime, stringToDate, dateToTimeAgo } from "../../utils/utils.ts";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { isThumbnailAvailable } from "../../utils/api/client.ts";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { MoreVertical, Pencil, Trash2 } from "lucide-react";
|
||||
import {config} from "../../config.ts";
|
||||
|
||||
@@ -19,12 +18,12 @@ type VideoCardProps = {
|
||||
itemLabel?: string,
|
||||
}
|
||||
|
||||
const fallbackThumbnail = "../../../public/default_thumbnail.png";
|
||||
const fallbackThumbnail = "public/default_thumbnail.png";
|
||||
const API_URL = config.apiUrl;
|
||||
|
||||
const VideoCard = ({ id, title, duration, createdAt, onEdit, onDelete, className, mediaApiPath = '/api/v1/clips', playerPath = '/video', itemLabel = 'clip' }: VideoCardProps) => {
|
||||
const [timeAgo, setTimeAgo] = useState(dateToTimeAgo(stringToDate(createdAt)));
|
||||
const [thumbnailAvailable, setThumbnailAvailable] = useState(true);
|
||||
const [imgFailed, setImgFailed] = useState(false);
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const [confirmDelete, setConfirmDelete] = useState(false);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
@@ -37,12 +36,6 @@ const VideoCard = ({ id, title, duration, createdAt, onEdit, onDelete, className
|
||||
return () => clearInterval(interval);
|
||||
}, [createdAt]);
|
||||
|
||||
useEffect(() => {
|
||||
isThumbnailAvailable(thumbnailUrl)
|
||||
.then(setThumbnailAvailable)
|
||||
.catch(() => setThumbnailAvailable(false));
|
||||
}, [thumbnailUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!menuOpen) return;
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
@@ -66,7 +59,8 @@ const VideoCard = ({ id, title, duration, createdAt, onEdit, onDelete, className
|
||||
<div className={clsx("flex flex-col group cursor-pointer", className)}>
|
||||
<div className="relative overflow-hidden rounded-lg">
|
||||
<img
|
||||
src={thumbnailAvailable ? thumbnailUrl : fallbackThumbnail}
|
||||
src={imgFailed ? fallbackThumbnail : thumbnailUrl}
|
||||
onError={() => setImgFailed(true)}
|
||||
alt="Video Thumbnail"
|
||||
className="w-full aspect-video object-cover group-hover:scale-105 transition-transform duration-200"
|
||||
/>
|
||||
|
||||
@@ -6,14 +6,12 @@ import com.ddf.vodsystem.controllers.dto.ClipUpdateRequest;
|
||||
import com.ddf.vodsystem.entities.Clip;
|
||||
import com.ddf.vodsystem.services.ClipService;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.MediaTypeFactory;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.*;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/clips")
|
||||
@@ -96,7 +94,7 @@ public class ClipController {
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/thumbnail")
|
||||
public ResponseEntity<Resource> downloadThumbnail(@PathVariable Long id) {
|
||||
public ResponseEntity<Resource> downloadThumbnail(@PathVariable Long id) throws IOException {
|
||||
Resource resource = clipService.downloadThumbnail(id);
|
||||
|
||||
if (resource == null || !resource.exists()) {
|
||||
@@ -104,6 +102,8 @@ public class ClipController {
|
||||
}
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.cacheControl(CacheControl.maxAge(2, TimeUnit.DAYS).cachePublic())
|
||||
.lastModified(resource.lastModified())
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, String.format(FILENAME_HEADER, resource.getFilename()))
|
||||
.contentType(MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM))
|
||||
.body(resource);
|
||||
|
||||
@@ -6,14 +6,12 @@ import com.ddf.vodsystem.dto.APIResponse;
|
||||
import com.ddf.vodsystem.entities.Vod;
|
||||
import com.ddf.vodsystem.services.VodService;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.MediaTypeFactory;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.*;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/vods")
|
||||
@@ -73,7 +71,7 @@ public class VodController {
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/thumbnail")
|
||||
public ResponseEntity<Resource> downloadThumbnail(@PathVariable Long id) {
|
||||
public ResponseEntity<Resource> downloadThumbnail(@PathVariable Long id) throws IOException {
|
||||
Resource resource = vodService.downloadThumbnail(id);
|
||||
|
||||
if (resource == null || !resource.exists()) {
|
||||
@@ -81,6 +79,8 @@ public class VodController {
|
||||
}
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.cacheControl(CacheControl.maxAge(2, TimeUnit.DAYS).cachePublic())
|
||||
.lastModified(resource.lastModified())
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, String.format(FILENAME_HEADER, resource.getFilename()))
|
||||
.contentType(MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM))
|
||||
.body(resource);
|
||||
|
||||
Reference in New Issue
Block a user