chore(prod): sync with main #4

Merged
dylan merged 67 commits from master into prod 2026-06-25 23:28:52 +01:00
2 changed files with 38 additions and 5 deletions
Showing only changes of commit 07305f0513 - Show all commits

View File

@@ -1,8 +1,8 @@
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { ArrowLeft, Maximize, Minimize, Pause, Play, Volume1, Volume2, VolumeX } from "lucide-react"; import { ArrowLeft, Lock, Maximize, Minimize, Pause, Play, Volume1, Volume2, VolumeX } from "lucide-react";
import type { Clip } from "../utils/types"; import type { Clip } from "../utils/types";
import { getClipById, getVideoBlob } from "../utils/endpoints.ts"; import { AuthError, getClipById, getVideoBlob } from "../utils/endpoints.ts";
import Box from "../components/Box.tsx"; import Box from "../components/Box.tsx";
import { dateToTimeAgo, formatTime, stringToDate } from "../utils/utils.ts"; import { dateToTimeAgo, formatTime, stringToDate } from "../utils/utils.ts";
@@ -18,6 +18,7 @@ const VideoPlayer = () => {
// Data // Data
const [videoUrl, setVideoUrl] = useState<string | undefined>(undefined); const [videoUrl, setVideoUrl] = useState<string | undefined>(undefined);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [notAuthenticated, setNotAuthenticated] = useState(false);
const [clip, setClip] = useState<Clip | null>(null); const [clip, setClip] = useState<Clip | null>(null);
const [timeAgo, setTimeAgo] = useState(""); const [timeAgo, setTimeAgo] = useState("");
@@ -41,11 +42,16 @@ const VideoPlayer = () => {
useEffect(() => { useEffect(() => {
if (!id) { setError("Clip ID is required."); return; } if (!id) { setError("Clip ID is required."); return; }
getVideoBlob(id).then((blob) => setVideoUrl(URL.createObjectURL(blob))); getVideoBlob(id)
.then((blob) => setVideoUrl(URL.createObjectURL(blob)))
.catch((e) => { if (e instanceof AuthError) setNotAuthenticated(true); });
getClipById(id) getClipById(id)
.then(setClip) .then(setClip)
.catch(() => setError("Failed to load clip details.")); .catch((e) => {
if (e instanceof AuthError) setNotAuthenticated(true);
else setError("Failed to load clip details.");
});
}, [id]); }, [id]);
useEffect(() => { useEffect(() => {
@@ -177,6 +183,27 @@ const VideoPlayer = () => {
const effectiveVolume = isMuted ? 0 : volume; const effectiveVolume = isMuted ? 0 : volume;
const VolumeIcon = effectiveVolume === 0 ? VolumeX : effectiveVolume < 0.5 ? Volume1 : Volume2; const VolumeIcon = effectiveVolume === 0 ? VolumeX : effectiveVolume < 0.5 ? Volume1 : Volume2;
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>
<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 clip.</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"
>
<ArrowLeft size={16} />
Go back
</button>
</div>
);
}
return ( return (
<div className="h-full flex flex-col p-4 gap-3"> <div className="h-full flex flex-col p-4 gap-3">
<button <button

View File

@@ -2,6 +2,10 @@ import type {VideoMetadata, APIResponse, User, Clip, ProgressResult } from "./ty
const API_URL = import.meta.env.VITE_API_URL; const API_URL = import.meta.env.VITE_API_URL;
export class AuthError extends Error {
constructor() { super("Not authenticated"); this.name = "AuthError"; }
}
/** /**
* Login function * Login function
* @param GoogleToken - The Google token received from the frontend. * @param GoogleToken - The Google token received from the frontend.
@@ -227,6 +231,7 @@ const getClipById = async (id: string): Promise<Clip | null> => {
const response = await fetch(API_URL + `/api/v1/clips/${id}`, {credentials: "include",}); const response = await fetch(API_URL + `/api/v1/clips/${id}`, {credentials: "include",});
if (!response.ok) { if (!response.ok) {
if (response.status === 401 || response.status === 403) throw new AuthError();
throw new Error(`Failed to fetch clip: ${response.status}`); throw new Error(`Failed to fetch clip: ${response.status}`);
} }
@@ -241,8 +246,9 @@ const getClipById = async (id: string): Promise<Clip | null> => {
const getVideoBlob = async(id: string): Promise<Blob> => { const getVideoBlob = async(id: string): Promise<Blob> => {
const response = await fetch(API_URL + `/api/v1/download/clip/${id}`, {credentials: "include",}); const response = await fetch(API_URL + `/api/v1/download/clip/${id}`, {credentials: "include",});
if (!response.ok) { if (!response.ok) {
if (response.status === 401 || response.status === 403) throw new AuthError();
throw new Error(`Failed to fetch video: ${id}: ${response.status}`) throw new Error(`Failed to fetch video: ${id}: ${response.status}`)
} }