diff --git a/.replit b/.replit
index 990e492..c25f461 100644
--- a/.replit
+++ b/.replit
@@ -38,3 +38,6 @@ author = "agent"
task = "shell.exec"
args = "npm run dev"
waitForPort = 5000
+
+[agent]
+integrations = ["javascript_database==1.0.0", "javascript_log_in_with_replit==1.0.0"]
diff --git a/client/src/App.tsx b/client/src/App.tsx
index 57750f6..47f094e 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -5,6 +5,8 @@ import { Toaster } from "@/components/ui/toaster";
import { TooltipProvider } from "@/components/ui/tooltip";
import Home from "@/pages/home";
import VideoPage from "@/pages/video";
+import { PlaylistsPage } from "@/pages/playlists";
+import { FavoritesPage } from "@/pages/favorites";
import NotFound from "@/pages/not-found";
function Router() {
@@ -12,6 +14,8 @@ function Router() {
+
+
);
diff --git a/client/src/components/navigation.tsx b/client/src/components/navigation.tsx
new file mode 100644
index 0000000..911707e
--- /dev/null
+++ b/client/src/components/navigation.tsx
@@ -0,0 +1,81 @@
+import { Link, useLocation } from "wouter";
+import { Button } from "@/components/ui/button";
+import { Home, List, Heart, User } from "lucide-react";
+
+export function Navigation() {
+ const [location] = useLocation();
+
+ const navItems = [
+ { path: "/", label: "Home", icon: Home },
+ { path: "/playlists", label: "Playlists", icon: List },
+ { path: "/favorites", label: "Favorites", icon: Heart },
+ ];
+
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/components/thumbnail-generator.tsx b/client/src/components/thumbnail-generator.tsx
index 0a16d5c..b554cec 100644
--- a/client/src/components/thumbnail-generator.tsx
+++ b/client/src/components/thumbnail-generator.tsx
@@ -1,4 +1,4 @@
-import { useState } from "react";
+import React, { useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@@ -88,7 +88,7 @@ export default function ThumbnailGenerator({ video, onThumbnailGenerated }: Thum
}
};
- useState(() => {
+ React.useEffect(() => {
loadExistingThumbnails();
}, [video.id]);
diff --git a/client/src/components/video-actions.tsx b/client/src/components/video-actions.tsx
new file mode 100644
index 0000000..60fae23
--- /dev/null
+++ b/client/src/components/video-actions.tsx
@@ -0,0 +1,133 @@
+import { useState } from "react";
+import { Button } from "@/components/ui/button";
+import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
+import { Card, CardContent } from "@/components/ui/card";
+import { Heart, Plus, List, Check } from "lucide-react";
+import { useFavorites } from "@/hooks/use-favorites";
+import { usePlaylists } from "@/hooks/use-playlists";
+import type { Video, Playlist } from "@shared/schema";
+
+interface VideoActionsProps {
+ video: Video;
+ showLabel?: boolean;
+}
+
+export function VideoActions({ video, showLabel = false }: VideoActionsProps) {
+ const [isPlaylistDialogOpen, setIsPlaylistDialogOpen] = useState(false);
+ const { useIsFavorited, addToFavorites, removeFromFavorites } = useFavorites();
+ const { useUserPlaylists, addVideoToPlaylist } = usePlaylists();
+
+ const { data: favoriteStatus } = useIsFavorited(video.id);
+ const isFavorited = favoriteStatus?.isFavorited || false;
+
+ const { data: playlists } = useUserPlaylists();
+ const playlistsArray = Array.isArray(playlists) ? playlists : [];
+
+ const handleFavoriteToggle = () => {
+ if (isFavorited) {
+ removeFromFavorites(video.id);
+ } else {
+ addToFavorites(video.id);
+ }
+ };
+
+ const handleAddToPlaylist = (playlistId: string) => {
+ addVideoToPlaylist({ playlistId, videoId: video.id });
+ setIsPlaylistDialogOpen(false);
+ };
+
+ return (
+
+ {/* Favorite Button */}
+
+
+ {/* Add to Playlist Button */}
+
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/components/video-card.tsx b/client/src/components/video-card.tsx
index fb22be8..b29d30b 100644
--- a/client/src/components/video-card.tsx
+++ b/client/src/components/video-card.tsx
@@ -1,6 +1,7 @@
import { Play, Share2 } from "lucide-react";
import { type Video } from "@shared/schema";
import { Button } from "@/components/ui/button";
+import { VideoActions } from "@/components/video-actions";
interface VideoCardProps {
video: Video;
@@ -121,17 +122,20 @@ export default function VideoCard({ video, onClick, onShare }: VideoCardProps) {
{formatDate(video.createdAt)}
- {onShare && (
-
- )}
+
+
+ {onShare && (
+
+ )}
+
diff --git a/client/src/components/video-modal.tsx b/client/src/components/video-modal.tsx
index af63423..28fa35c 100644
--- a/client/src/components/video-modal.tsx
+++ b/client/src/components/video-modal.tsx
@@ -1,9 +1,11 @@
-import { useEffect, useRef } from "react";
-import { X } from "lucide-react";
+import { useEffect, useRef, useState } from "react";
+import { X, Settings } from "lucide-react";
import { type Video } from "@shared/schema";
import { Button } from "@/components/ui/button";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { apiRequest } from "@/lib/queryClient";
import Hls from "hls.js";
+import ThumbnailGenerator from "./thumbnail-generator";
interface VideoModalProps {
video: Video | null;
@@ -47,6 +49,7 @@ function formatDate(date: Date | string): string {
export default function VideoModal({ video, isOpen, onClose }: VideoModalProps) {
const videoRef = useRef(null);
const hlsRef = useRef(null);
+ const [activeTab, setActiveTab] = useState("video");
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
diff --git a/client/src/hooks/use-favorites.ts b/client/src/hooks/use-favorites.ts
new file mode 100644
index 0000000..78cc098
--- /dev/null
+++ b/client/src/hooks/use-favorites.ts
@@ -0,0 +1,64 @@
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import { apiRequest } from "@/lib/queryClient";
+import { useToast } from "@/hooks/use-toast";
+
+const CURRENT_USER_ID = "test-user-123"; // For demo purposes
+
+export function useFavorites() {
+ const queryClient = useQueryClient();
+ const { toast } = useToast();
+
+ // Check if video is favorited
+ const useIsFavorited = (videoId: string) => {
+ return useQuery({
+ queryKey: ["/api/favorites", CURRENT_USER_ID, videoId],
+ queryFn: async () => {
+ const response = await fetch(`/api/favorites/${CURRENT_USER_ID}/${videoId}`);
+ if (!response.ok) throw new Error('Failed to check favorite status');
+ return response.json();
+ },
+ enabled: !!videoId,
+ });
+ };
+
+ // Add to favorites
+ const addToFavoritesMutation = useMutation({
+ mutationFn: (videoId: string) =>
+ apiRequest("/api/favorites", "POST", {
+ userId: CURRENT_USER_ID,
+ videoId,
+ }),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ["/api/favorites"] });
+ toast({ title: "Added to favorites!" });
+ },
+ onError: (error: any) => {
+ if (error.message?.includes("already in favorites")) {
+ toast({ title: "Already in favorites", variant: "destructive" });
+ } else {
+ toast({ title: "Failed to add to favorites", variant: "destructive" });
+ }
+ },
+ });
+
+ // Remove from favorites
+ const removeFromFavoritesMutation = useMutation({
+ mutationFn: (videoId: string) =>
+ apiRequest(`/api/favorites/${CURRENT_USER_ID}/${videoId}`, "DELETE"),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ["/api/favorites"] });
+ toast({ title: "Removed from favorites" });
+ },
+ onError: () => {
+ toast({ title: "Failed to remove from favorites", variant: "destructive" });
+ },
+ });
+
+ return {
+ useIsFavorited,
+ addToFavorites: addToFavoritesMutation.mutate,
+ removeFromFavorites: removeFromFavoritesMutation.mutate,
+ isAddingToFavorites: addToFavoritesMutation.isPending,
+ isRemovingFromFavorites: removeFromFavoritesMutation.isPending,
+ };
+}
\ No newline at end of file
diff --git a/client/src/hooks/use-playlists.ts b/client/src/hooks/use-playlists.ts
new file mode 100644
index 0000000..8df076d
--- /dev/null
+++ b/client/src/hooks/use-playlists.ts
@@ -0,0 +1,77 @@
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import { apiRequest } from "@/lib/queryClient";
+import { useToast } from "@/hooks/use-toast";
+import type { Playlist, InsertPlaylist } from "@shared/schema";
+
+const CURRENT_USER_ID = "test-user-123"; // For demo purposes
+
+export function usePlaylists() {
+ const queryClient = useQueryClient();
+ const { toast } = useToast();
+
+ // Get user playlists
+ const useUserPlaylists = () => {
+ return useQuery({
+ queryKey: ["/api/playlists", CURRENT_USER_ID],
+ });
+ };
+
+ // Get single playlist
+ const usePlaylist = (playlistId: string) => {
+ return useQuery({
+ queryKey: ["/api/playlists", playlistId],
+ enabled: !!playlistId,
+ });
+ };
+
+ // Get playlist videos
+ const usePlaylistVideos = (playlistId: string) => {
+ return useQuery({
+ queryKey: ["/api/playlists", playlistId, "videos"],
+ enabled: !!playlistId,
+ });
+ };
+
+ // Add video to playlist
+ const addVideoToPlaylistMutation = useMutation({
+ mutationFn: ({ playlistId, videoId }: { playlistId: string; videoId: string }) =>
+ apiRequest(`/api/playlists/${playlistId}/videos`, "POST", {
+ videoId,
+ position: 0,
+ }),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ["/api/playlists"] });
+ toast({ title: "Added to playlist!" });
+ },
+ onError: (error: any) => {
+ if (error.message?.includes("already in playlist")) {
+ toast({ title: "Video is already in this playlist", variant: "destructive" });
+ } else {
+ toast({ title: "Failed to add to playlist", variant: "destructive" });
+ }
+ },
+ });
+
+ // Remove video from playlist
+ const removeVideoFromPlaylistMutation = useMutation({
+ mutationFn: ({ playlistId, videoId }: { playlistId: string; videoId: string }) =>
+ apiRequest(`/api/playlists/${playlistId}/videos/${videoId}`, "DELETE"),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ["/api/playlists"] });
+ toast({ title: "Removed from playlist" });
+ },
+ onError: () => {
+ toast({ title: "Failed to remove from playlist", variant: "destructive" });
+ },
+ });
+
+ return {
+ useUserPlaylists,
+ usePlaylist,
+ usePlaylistVideos,
+ addVideoToPlaylist: addVideoToPlaylistMutation.mutate,
+ removeVideoFromPlaylist: removeVideoFromPlaylistMutation.mutate,
+ isAddingToPlaylist: addVideoToPlaylistMutation.isPending,
+ isRemovingFromPlaylist: removeVideoFromPlaylistMutation.isPending,
+ };
+}
\ No newline at end of file
diff --git a/client/src/pages/favorites.tsx b/client/src/pages/favorites.tsx
new file mode 100644
index 0000000..8cfaeaf
--- /dev/null
+++ b/client/src/pages/favorites.tsx
@@ -0,0 +1,147 @@
+import { useQuery } from "@tanstack/react-query";
+import { apiRequest } from "@/lib/queryClient";
+import { Card, CardContent } from "@/components/ui/card";
+import { Heart, Clock, Play } from "lucide-react";
+import VideoModal from "@/components/video-modal";
+import { useState } from "react";
+import { Navigation } from "@/components/navigation";
+import type { UserFavorite, Video } from "@shared/schema";
+
+const CURRENT_USER_ID = "test-user-123"; // For demo purposes
+
+export function FavoritesPage() {
+ const [selectedVideo, setSelectedVideo] = useState