-
- {/* Render genre rows dynamically */}
- {genres.map((genre) => (
-
-
-
+
+ {/* Chat Sidebar */}
+
+
+
+
+
+
+ AI
+
+
+ Insight AI
+
+
+
+
+
+
+
+ {/* navigation */}
+
+ {navItems.map((item) => (
+ setIsSidebarOpen(false)}
+ >
+
+ {item.name}
+
+ ))}
+
+
+
+
+ Recent Chats
+
+
+
+
+
+
+
+
+ {chats.length === 0 ? (
+
+ No conversations yet. Start a new one!
+
+ ) : (
+ chats.map(chat => (
+
handleSelectChat(chat.id)}
+ className={cn(
+ "flex items-center justify-between p-1 px-4 rounded-lg cursor-pointer group transition-all",
+ activeChat?.id === chat.id
+ ? "bg-financial-accent/20 border border-financial-accent/30"
+ : "hover:bg-muted/50 border border-transparent"
+ )}
+ >
+
+
+
+ {chat.title}
+ {chat.id === activeChat?.id && isGeneratingTitle && (
+
+ )}
+
+
+ {chat.messages.filter(m => m.sender === "user").length} messages • {format(new Date(chat.updatedAt), "MMM d")}
+
+
+
handleDeleteChat(chat.id, e)}
+ >
+
+
+
+ ))
+ )}
+
+
+ {/* Sidebar Footer */}
+
+
+
+ Settings
+
+
+
+
+
+
+
+ {/* Chat Main Area */}
+
+ {/* Mobile Header */}
+
+
+
+
+
+
+ {activeChat?.title || "New Chat"}
- ))}
+
+
+
+
+
+ {/* Chat Area */}
+
+ {!activeChat ? (
+
+
+
+
Welcome to Insight AI
+
+ Ask me anything, and I'll do my best to help you
+
+
+ Start a new conversation
+
+
+
+ ) : (
+ <>
+
+
+ {activeChat.messages.map((message) => (
+
+ ))}
+
+
+
+
+ {/* Input Area */}
+
+ >
+ )}
+
);
diff --git a/frontend/src/pages/Index.tsx b/frontend/src/pages/Index.tsx
index 2e86f115a51610d58fa12a89b12056bfcbb8ae52..4d506b3d736fd166076cebb251ec5a10d942389e 100644
--- a/frontend/src/pages/Index.tsx
+++ b/frontend/src/pages/Index.tsx
@@ -1,17 +1,8 @@
-import Navbar from '../components/Navbar';
-import Footer from '../components/Footer';
-import HomePage from './HomePage';
+
+import { Navigate } from "react-router-dom";
const Index = () => {
- return (
-
-
-
-
-
-
-
- );
+ return
;
};
export default Index;
diff --git a/frontend/src/pages/MainLayout.tsx b/frontend/src/pages/MainLayout.tsx
deleted file mode 100644
index 813e94589c5360594c45e0fba5a94b3ad6738181..0000000000000000000000000000000000000000
--- a/frontend/src/pages/MainLayout.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-
-import Navbar from '../components/Navbar';
-import Footer from '../components/Footer';
-import { Outlet } from 'react-router-dom';
-
-const MainLayout = () => {
- return (
-
-
-
-
-
-
-
- );
-};
-
-export default MainLayout;
diff --git a/frontend/src/pages/MovieDetailPage.tsx b/frontend/src/pages/MovieDetailPage.tsx
deleted file mode 100644
index c1ee8403b5e5c26a6d7e5dd302a7644a0991ae9b..0000000000000000000000000000000000000000
--- a/frontend/src/pages/MovieDetailPage.tsx
+++ /dev/null
@@ -1,226 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import { useParams, Link } from 'react-router-dom';
-import { Play, Plus, ThumbsUp, Share2 } from 'lucide-react';
-import { getMovieMetadata, getGenresItems, getMovieCard } from '../lib/api';
-import ContentRow from '../components/ContentRow';
-import { useToast } from '@/hooks/use-toast';
-
-const MovieDetailPage = () => {
- const { title } = useParams<{ title: string }>();
- const [movie, setMovie] = useState
(null);
- const [loading, setLoading] = useState(true);
- const [similarMovies, setSimilarMovies] = useState([]);
- const { toast } = useToast();
-
- useEffect(() => {
- const fetchMovieData = async () => {
- if (!title) return;
-
- try {
- setLoading(true);
- const data = await getMovieMetadata(title);
- setMovie(data);
- const movieData = data.data;
- console.log(movieData);
-
- // Fetch similar movies based on individual genres
- if (movieData.genres && movieData.genres.length > 0) {
- const currentMovieName = movieData.name;
- const moviesByGenre = await Promise.all(
- movieData.genres.map(async (genre: any) => {
- // Pass a single genre name for each call
- const genreResult = await getGenresItems([genre.name], 'movie', 10, 1);
- if (genreResult.movies && Array.isArray(genreResult.movies)) {
- return genreResult.movies.map((movieItem: any) => {
- const { title: similarTitle } = movieItem;
- // Skip current movie
- if (similarTitle === currentMovieName) return null;
- return {
- type: 'movie',
- title: similarTitle,
- };
- });
- }
- return [];
- })
- );
- // Flatten the array of arrays and remove null results
- const flattenedMovies = moviesByGenre.flat().filter(Boolean);
- // Remove duplicates based on the title
- const uniqueMovies = Array.from(
- new Map(flattenedMovies.map(movie => [movie.title, movie])).values()
- );
- setSimilarMovies(uniqueMovies);
- }
- } catch (error) {
- console.error(`Error fetching movie details for ${title}:`, error);
- toast({
- title: "Error loading movie details",
- description: "Please try again later",
- variant: "destructive"
- });
- } finally {
- setLoading(false);
- }
- };
-
- fetchMovieData();
- }, [title, toast]);
-
- if (loading) {
- return (
-
- );
- }
-
- if (!movie) {
- return (
-
-
Movie Not Found
-
We couldn't find the movie you're looking for.
-
- Back to Movies
-
-
- );
- }
-
- // Use movieData fields from the new structure
- const movieData = movie.data;
- const runtime = movieData.runtime
- ? `${Math.floor(movieData.runtime / 60)}h ${movieData.runtime % 60}m`
- : '';
- const releaseYear = movieData.year || '';
- const movieName = (movieData.translations?.nameTranslations?.find((t: any) => t.language === 'eng')?.name || movieData.name || '');
- const overview =
- movieData.overview ||
- (movieData.translations?.overviewTranslations?.find((t: any) => t.language === 'eng')?.overview || '');
-
- return (
-
- {/* Hero backdrop */}
-
-
-
{
- const target = e.target as HTMLImageElement;
- target.src = '/placeholder.svg';
- }}
- />
-
-
-
-
-
- {/* Movie details */}
-
-
- {/* Poster */}
-
-
{
- const target = e.target as HTMLImageElement;
- target.src = '/placeholder.svg';
- }}
- />
-
-
- {/* Details */}
-
-
{movieName}
-
-
- {releaseYear && {releaseYear} }
- {runtime && {runtime} }
- {movieData.contentRatings && movieData.contentRatings.length > 0 && (
-
- {movieData.contentRatings[0].name}+
-
- )}
-
-
-
- {movieData.genres &&
- movieData.genres.map((genre: any) => (
-
- {genre.name}
-
- ))}
-
-
-
{overview}
-
-
-
-
Play
-
-
-
- My List
-
-
-
-
-
-
-
-
-
-
-
- {/* Additional details */}
-
- {movieData.translations?.nameTranslations?.find((t: any) => t.isPrimary) && (
-
- "{movieData.translations.nameTranslations.find((t: any) => t.isPrimary).tagline}"
-
- )}
-
-
- {movieData.production_companies && movieData.production_companies.length > 0 && (
-
-
Production
-
- {movieData.production_companies.map((company: any) => company.name).join(', ')}
-
-
- )}
-
- {movieData.spoken_languages && movieData.spoken_languages.length > 0 && (
-
-
Languages
-
{movieData.spoken_languages.join(', ')}
-
- )}
-
-
-
-
-
- {/* Similar Movies */}
- {similarMovies.length > 0 && (
-
-
-
- )}
-
-
- );
-};
-
-export default MovieDetailPage;
diff --git a/frontend/src/pages/MoviePlayerPage.tsx b/frontend/src/pages/MoviePlayerPage.tsx
deleted file mode 100644
index 91e317d949966b8a556dc3f7623337575f341ba9..0000000000000000000000000000000000000000
--- a/frontend/src/pages/MoviePlayerPage.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-
-import React from 'react';
-import { useParams, useNavigate } from 'react-router-dom';
-import MoviePlayer from '../components/MoviePlayer';
-
-const MoviePlayerPage = () => {
- const { title } = useParams<{ title: string }>();
- const navigate = useNavigate();
-
- const handleBack = () => {
- navigate(`/movie/${encodeURIComponent(title || '')}`);
- };
-
- // Save playback progress to localStorage
- const savePlaybackProgress = (currentTime: number, duration: number) => {
- if (!title) return;
-
- try {
- const progressKey = `movie-progress-${title}`;
- const isCompleted = (currentTime / duration) > 0.9; // Mark as completed if 90% watched
-
- localStorage.setItem(progressKey, JSON.stringify({
- currentTime,
- duration,
- lastPlayed: new Date().toISOString(),
- completed: isCompleted
- }));
- } catch (error) {
- console.error('Error saving playback progress:', error);
- }
- };
-
- // Fetch stored playback progress from localStorage
- const getPlaybackProgress = () => {
- if (!title) return 0;
-
- try {
- const progressKey = `movie-progress-${title}`;
- const storedProgress = localStorage.getItem(progressKey);
-
- if (storedProgress) {
- const progress = JSON.parse(storedProgress);
- if (progress && progress.currentTime && !progress.completed) {
- return progress.currentTime;
- }
- }
- } catch (error) {
- console.error('Error reading playback progress:', error);
- }
-
- return 0;
- };
-
- return (
-
-
-
- );
-};
-
-export default MoviePlayerPage;
diff --git a/frontend/src/pages/MoviesPage.tsx b/frontend/src/pages/MoviesPage.tsx
deleted file mode 100644
index 3ecf1764baa02ec2b82c5aeeb050557948272139..0000000000000000000000000000000000000000
--- a/frontend/src/pages/MoviesPage.tsx
+++ /dev/null
@@ -1,92 +0,0 @@
-
-import React, { useEffect, useState } from 'react';
-import PageHeader from '../components/PageHeader';
-import ContentGrid from '../components/ContentGrid';
-import { getAllMovies, getMovieCard } from '../lib/api';
-import { useToast } from '@/hooks/use-toast';
-import { useSearchParams } from 'react-router-dom';
-
-const MoviesPage = () => {
- const [loading, setLoading] = useState(true);
- const [movies, setMovies] = useState([]);
- const [searchParams] = useSearchParams();
- const genreFilter = searchParams.get('genre');
- const { toast } = useToast();
-
- useEffect(() => {
- const fetchMovies = async () => {
- try {
- setLoading(true);
- const allMovies = await getAllMovies();
-
- // For each movie, get its card info for display
- const moviePromises = allMovies.slice(0, 30).map(async (movie: any) => {
- try {
- const movieInfo = await getMovieCard(movie.title);
- if (movieInfo) {
- return {
- type: 'movie',
- title: movie.title,
- image: movieInfo.image,
- description: movieInfo.overview,
- genre: movieInfo.genres?.map((g: any) => g.name) || [],
- year: movieInfo.year
- };
- }
- return null;
- } catch (error) {
- console.error(`Error fetching movie info for ${movie.title}:`, error);
- return null;
- }
- });
-
- let moviesData = await Promise.all(moviePromises);
- moviesData = moviesData.filter(movie => movie !== null);
-
- // Apply genre filter if present
- if (genreFilter) {
- moviesData = moviesData.filter(movie =>
- movie.genre.some((g: string) => g.toLowerCase() === genreFilter.toLowerCase())
- );
- }
-
- setMovies(moviesData);
- } catch (error) {
- console.error('Error fetching movies:', error);
- toast({
- title: "Error loading movies",
- description: "Please try again later",
- variant: "destructive"
- });
- } finally {
- setLoading(false);
- }
- };
-
- fetchMovies();
- }, [genreFilter, toast]);
-
- return (
-
-
-
- {loading ? (
-
- ) : (
-
- )}
-
- );
-};
-
-export default MoviesPage;
diff --git a/frontend/src/pages/MyListPage.tsx b/frontend/src/pages/MyListPage.tsx
deleted file mode 100644
index fd0fc53ff259da9f7f41709c0521b5911ef29aae..0000000000000000000000000000000000000000
--- a/frontend/src/pages/MyListPage.tsx
+++ /dev/null
@@ -1,168 +0,0 @@
-
-import React, { useState, useEffect } from 'react';
-import { useNavigate } from 'react-router-dom';
-import PageHeader from '../components/PageHeader';
-import ContentGrid, { ContentItem } from '../components/ContentGrid';
-import { getAllFromMyList, removeFromMyList } from '../lib/storage';
-import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
-import { Plus, TrashIcon } from 'lucide-react';
-import { useToast } from '@/hooks/use-toast';
-
-interface MyListItem {
- type: 'movie' | 'tvshow';
- title: string;
- addedAt: string;
-}
-
-const MyListPage = () => {
- const [myListItems, setMyListItems] = useState([]);
- const [showRemoveButtons, setShowRemoveButtons] = useState(false);
- const [activeTab, setActiveTab] = useState('all');
- const [isLoading, setIsLoading] = useState(true);
- const navigate = useNavigate();
- const { toast } = useToast();
-
- useEffect(() => {
- const fetchMyList = async () => {
- try {
- setIsLoading(true);
- const items = await getAllFromMyList();
- // Sort by most recently added
- items.sort((a, b) => new Date(b.addedAt).getTime() - new Date(a.addedAt).getTime());
- setMyListItems(items);
- } catch (error) {
- console.error("Error loading My List:", error);
- } finally {
- setIsLoading(false);
- }
- };
-
- fetchMyList();
- }, []);
-
- const handleRemoveItem = async (title: string, type: 'movie' | 'tvshow') => {
- try {
- await removeFromMyList(title, type);
- setMyListItems(prev => prev.filter(item => !(item.title === title && item.type === type)));
- toast({
- title: "Removed from My List",
- description: `"${title}" has been removed from your list`,
- });
- } catch (error) {
- console.error("Error removing item from My List:", error);
- toast({
- title: "Error",
- description: "Failed to remove item from your list",
- variant: "destructive"
- });
- }
- };
-
- const toggleRemoveButtons = () => {
- setShowRemoveButtons(!showRemoveButtons);
- };
-
- const getFilteredItems = (filter: string): ContentItem[] => {
- let filtered = myListItems;
- if (filter === 'movies') {
- filtered = myListItems.filter(item => item.type === 'movie');
- } else if (filter === 'tvshows') {
- filtered = myListItems.filter(item => item.type === 'tvshow');
- }
-
- // Convert to ContentItem format
- return filtered.map(item => ({
- type: item.type,
- title: item.title,
- image: undefined // ContentCard will fetch the image if not provided
- }));
- };
-
- const allItems = getFilteredItems('all');
- const movieItems = getFilteredItems('movies');
- const tvShowItems = getFilteredItems('tvshows');
-
- if (isLoading) {
- return (
-
-
-
- {[...Array(10)].map((_, i) => (
-
- ))}
-
-
- );
- }
-
- return (
-
-
-
-
-
- {myListItems.length > 0 && (
-
- {showRemoveButtons ? 'Done' : (
- <>
-
- Edit List
- >
- )}
-
- )}
-
-
navigate('/browse')}
- className="px-4 py-2 rounded-full bg-theme-primary hover:bg-theme-primary-hover text-white text-sm flex items-center gap-2"
- >
-
- Add Titles
-
-
-
-
- {myListItems.length === 0 ? (
-
-
🎬
-
Your list is empty
-
Start adding movies and shows to create your watchlist.
-
navigate('/browse')}
- className="px-6 py-2 rounded bg-theme-primary hover:bg-theme-primary-hover text-white text-sm font-medium"
- >
- Browse Content
-
-
- ) : (
-
-
- All ({allItems.length})
- Movies ({movieItems.length})
- TV Shows ({tvShowItems.length})
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- )}
-
- );
-};
-
-export default MyListPage;
diff --git a/frontend/src/pages/NotFound.tsx b/frontend/src/pages/NotFound.tsx
index c962034bfa945ea137e773bee642e285501fb572..cff73c27ef29cb206cedac93a0e5ece752f33f31 100644
--- a/frontend/src/pages/NotFound.tsx
+++ b/frontend/src/pages/NotFound.tsx
@@ -1,15 +1,36 @@
-import React from 'react';
-import { Link } from 'react-router-dom';
+import { useLocation } from "react-router-dom";
+import { useEffect } from "react";
+import { FileQuestion } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { Link } from "react-router-dom";
const NotFound = () => {
+ const location = useLocation();
+
+ useEffect(() => {
+ console.error(
+ "404 Error: User attempted to access non-existent route:",
+ location.pathname
+ );
+ }, [location.pathname]);
+
return (
-
-
404
-
This page could not be found.
-
- Back to Home
-
+
+
+
+
+
+
404
+
+ The page you are looking for could not be found.
+
+
+
+ Return to Home
+
+
+
);
};
diff --git a/frontend/src/pages/ProfilePage.tsx b/frontend/src/pages/ProfilePage.tsx
deleted file mode 100644
index 7d1d56acdb1b929c1e5d9aa9b4d259f514339a32..0000000000000000000000000000000000000000
--- a/frontend/src/pages/ProfilePage.tsx
+++ /dev/null
@@ -1,296 +0,0 @@
-
-import React, { useState, useEffect } from 'react';
-import PageHeader from '../components/PageHeader';
-import ContentGrid, { ContentItem } from '../components/ContentGrid';
-import { Button } from '@/components/ui/button';
-import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
-import { useToast } from '@/hooks/use-toast';
-import { Trash2, DownloadCloud, Upload } from 'lucide-react';
-import { getAllFromMyList } from '../lib/storage';
-
-interface WatchHistoryItem {
- type: 'movie' | 'tvshow';
- title: string;
- lastWatched: string;
- progress: number;
- completed: boolean;
-}
-
-const ProfilePage = () => {
- const [watchHistory, setWatchHistory] = useState
([]);
- const [myListItems, setMyListItems] = useState([]);
- const [activeTab, setActiveTab] = useState('history');
- const { toast } = useToast();
-
- // Load watch history from localStorage
- useEffect(() => {
- const loadWatchHistory = () => {
- try {
- const history: WatchHistoryItem[] = [];
-
- // Scan localStorage for movie progress
- for (let i = 0; i < localStorage.length; i++) {
- const key = localStorage.key(i);
- if (key?.startsWith('movie-progress-')) {
- const title = key.replace('movie-progress-', '');
- const data = JSON.parse(localStorage.getItem(key) || '{}');
-
- if (data && data.lastPlayed) {
- history.push({
- type: 'movie',
- title,
- lastWatched: data.lastPlayed,
- progress: Math.round((data.currentTime / data.duration) * 100) || 0,
- completed: data.completed || false
- });
- }
- }
-
- // Scan for TV show progress
- if (key?.startsWith('playback-')) {
- const showTitle = key.replace('playback-', '');
- const showData = JSON.parse(localStorage.getItem(key) || '{}');
-
- let lastEpisodeDate = '';
- let lastEpisodeProgress = 0;
- let anyEpisodeCompleted = false;
-
- // Find the most recently watched episode
- Object.entries(showData).forEach(([_, value]) => {
- const episodeData = value as {
- lastPlayed: string;
- currentTime: number;
- duration: number;
- completed: boolean;
- };
-
- if (!lastEpisodeDate || new Date(episodeData.lastPlayed) > new Date(lastEpisodeDate)) {
- lastEpisodeDate = episodeData.lastPlayed;
- lastEpisodeProgress = Math.round((episodeData.currentTime / episodeData.duration) * 100) || 0;
- if (episodeData.completed) anyEpisodeCompleted = true;
- }
- });
-
- if (lastEpisodeDate) {
- history.push({
- type: 'tvshow',
- title: showTitle,
- lastWatched: lastEpisodeDate,
- progress: lastEpisodeProgress,
- completed: anyEpisodeCompleted
- });
- }
- }
- }
-
- // Sort by most recently watched
- history.sort((a, b) =>
- new Date(b.lastWatched).getTime() - new Date(a.lastWatched).getTime()
- );
-
- setWatchHistory(history);
- } catch (error) {
- console.error('Error loading watch history:', error);
- }
- };
-
- loadWatchHistory();
- }, []);
-
- // Load My List items
- useEffect(() => {
- const loadMyList = async () => {
- try {
- const items = await getAllFromMyList();
- const contentItems: ContentItem[] = items.map(item => ({
- type: item.type,
- title: item.title,
- image: undefined // ContentCard component will fetch the image
- }));
- setMyListItems(contentItems);
- } catch (error) {
- console.error('Error loading my list:', error);
- }
- };
-
- loadMyList();
- }, []);
-
- const clearWatchHistory = () => {
- // Filter localStorage keys related to watch history
- const keysToRemove: string[] = [];
-
- for (let i = 0; i < localStorage.length; i++) {
- const key = localStorage.key(i);
- if (key && (key.startsWith('movie-progress-') || key.startsWith('playback-'))) {
- keysToRemove.push(key);
- }
- }
-
- // Remove the keys
- keysToRemove.forEach(key => localStorage.removeItem(key));
-
- // Update state
- setWatchHistory([]);
-
- toast({
- title: "Watch History Cleared",
- description: "Your watch history has been successfully cleared.",
- });
- };
-
- const exportUserData = () => {
- try {
- const userData = {
- watchHistory: {},
- myList: {}
- };
-
- // Export all localStorage data
- for (let i = 0; i < localStorage.length; i++) {
- const key = localStorage.key(i);
- if (!key) continue;
-
- if (key.startsWith('movie-progress-') || key.startsWith('playback-')) {
- userData.watchHistory[key] = JSON.parse(localStorage.getItem(key) || '{}');
- }
-
- if (key === 'myList') {
- userData.myList = JSON.parse(localStorage.getItem(key) || '[]');
- }
- }
-
- // Create downloadable JSON
- const dataStr = JSON.stringify(userData, null, 2);
- const blob = new Blob([dataStr], { type: 'application/json' });
- const url = URL.createObjectURL(blob);
-
- // Create temporary link and trigger download
- const a = document.createElement('a');
- a.href = url;
- a.download = `streamflix-user-data-${new Date().toISOString().slice(0, 10)}.json`;
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- URL.revokeObjectURL(url);
-
- toast({
- title: "Export Successful",
- description: "Your data has been exported successfully.",
- });
- } catch (error) {
- console.error('Error exporting user data:', error);
- toast({
- title: "Export Failed",
- description: "There was an error exporting your data.",
- variant: "destructive"
- });
- }
- };
-
- const renderWatchHistoryItems = (): ContentItem[] => {
- return watchHistory.map(item => ({
- type: item.type,
- title: item.title,
- image: undefined // ContentCard will fetch the image
- }));
- };
-
- return (
-
-
-
-
-
-
- Watch History
- My List
- Settings
-
-
-
-
-
Watch History
- {watchHistory.length > 0 && (
-
-
- Clear History
-
- )}
-
-
- {watchHistory.length === 0 ? (
-
-
You have no watch history yet.
-
Start watching movies and shows to build your history.
-
- ) : (
-
- )}
-
-
-
-
-
My List
-
-
- {myListItems.length === 0 ? (
-
-
You haven't added anything to your list yet.
-
Browse content and click the "+" icon to add titles to your list.
-
- ) : (
-
- )}
-
-
-
-
-
-
Data Management
-
-
-
-
Export Your Data
-
Download your watch history and list data as a JSON file.
-
-
- Export Data
-
-
-
-
-
Import Your Data
-
Restore previously exported data (coming soon)
-
-
- Import Data
-
-
-
-
-
-
-
Account Settings
-
Account management features coming soon.
-
-
-
-
-
-
- );
-};
-
-export default ProfilePage;
diff --git a/frontend/src/pages/SearchPage.tsx b/frontend/src/pages/SearchPage.tsx
deleted file mode 100644
index fad7599482b26065ced5499f995a68c58aaa76ca..0000000000000000000000000000000000000000
--- a/frontend/src/pages/SearchPage.tsx
+++ /dev/null
@@ -1,199 +0,0 @@
-
-import React, { useState, useEffect } from 'react';
-import { useSearchParams } from 'react-router-dom';
-import { useToast } from '@/hooks/use-toast';
-import { searchAPI } from '../lib/search-api';
-import PageHeader from '../components/PageHeader';
-import ContentGrid from '../components/ContentGrid';
-import ContentCard from '../components/ContentCard';
-import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
-import { Loader2 } from 'lucide-react';
-
-const SearchPage = () => {
- const [searchParams] = useSearchParams();
- const query = searchParams.get('q') || '';
- const [searchResults, setSearchResults] = useState<{
- movies: string[];
- shows: string[];
- episodes: {
- series: string;
- title: string;
- path: string;
- season: string;
- }[];
- }>({
- movies: [],
- shows: [],
- episodes: []
- });
- const [loading, setLoading] = useState(false);
- const [activeTab, setActiveTab] = useState('all');
- const { toast } = useToast();
-
- useEffect(() => {
- const fetchSearchResults = async () => {
- if (!query) return;
-
- try {
- setLoading(true);
- const results = await searchAPI.search(query);
- setSearchResults({
- movies: results.films || [],
- shows: results.series || [],
- episodes: results.episodes || []
- });
- } catch (error) {
- console.error('Search error:', error);
- toast({
- title: "Search Failed",
- description: "Unable to perform search. Please try again.",
- variant: "destructive"
- });
- } finally {
- setLoading(false);
- }
- };
-
- fetchSearchResults();
- }, [query, toast]);
-
- const getTotalResultsCount = () => {
- return searchResults.movies.length + searchResults.shows.length + searchResults.episodes.length;
- };
-
- const renderMovieResults = () => {
- if (searchResults.movies.length === 0) {
- return No movies found matching "{query}"
;
- }
-
- return (
-
- {searchResults.movies.map((title, index) => (
-
- ))}
-
- );
- };
-
- const renderShowResults = () => {
- if (searchResults.shows.length === 0) {
- return No TV shows found matching "{query}"
;
- }
-
- return (
-
- {searchResults.shows.map((title, index) => (
-
- ))}
-
- );
- };
-
- const renderEpisodeResults = () => {
- if (searchResults.episodes.length === 0) {
- return No episodes found matching "{query}"
;
- }
-
- return (
-
- {searchResults.episodes.map((episode, index) => (
-
-
{episode.title}
-
- {episode.series} • Season {episode.season}
-
-
- ))}
-
- );
- };
-
- return (
-
-
-
- {loading ? (
-
-
-
- ) : query ? (
-
-
-
- All Results ({getTotalResultsCount()})
-
-
- Movies ({searchResults.movies.length})
-
-
- TV Shows ({searchResults.shows.length})
-
-
- Episodes ({searchResults.episodes.length})
-
-
-
-
- {getTotalResultsCount() === 0 ? (
- No results found matching "{query}"
- ) : (
- <>
- {searchResults.movies.length > 0 && (
-
-
Movies
- {renderMovieResults()}
-
- )}
-
- {searchResults.shows.length > 0 && (
-
-
TV Shows
- {renderShowResults()}
-
- )}
-
- {searchResults.episodes.length > 0 && (
-
-
Episodes
- {renderEpisodeResults()}
-
- )}
- >
- )}
-
-
-
- {renderMovieResults()}
-
-
-
- {renderShowResults()}
-
-
-
- {renderEpisodeResults()}
-
-
- ) : (
-
-
Enter a search term to find movies, TV shows, and episodes.
-
- )}
-
- );
-};
-
-export default SearchPage;
diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..c3b04d0d2c5f057e3e2baeb483df621659768afb
--- /dev/null
+++ b/frontend/src/pages/SettingsPage.tsx
@@ -0,0 +1,272 @@
+import { useState, useRef } from "react";
+import { Settings, Moon, Sun, Globe, Shield, Database, Cloud } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
+import { Label } from "@/components/ui/label";
+import { Separator } from "@/components/ui/separator";
+import { Input } from "@/components/ui/input";
+import { toast } from "@/components/ui/sonner";
+import { storage, STORAGE_KEYS } from "@/lib/storage";
+
+const SettingsPage = () => {
+ const [theme, setTheme] = useState(() => {
+ return storage.get(STORAGE_KEYS.THEME) || (window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
+ });
+
+ const [apiEndpoint, setApiEndpoint] = useState(
+ storage.get(STORAGE_KEYS.API_ENDPOINT) || "http://localhost:8000"
+ );
+
+ const fileInputRef = useRef(null);
+
+ const handleThemeChange = (newTheme: string) => {
+ setTheme(newTheme);
+
+ const root = window.document.documentElement;
+
+ if (newTheme === "dark") {
+ root.classList.add("dark");
+ } else if (newTheme === "light") {
+ root.classList.remove("dark");
+ } else {
+ // System theme
+ if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
+ root.classList.add("dark");
+ } else {
+ root.classList.remove("dark");
+ }
+ }
+
+ storage.set(STORAGE_KEYS.THEME, newTheme);
+ toast.success("Theme updated successfully");
+ };
+
+ const handleSaveEndpoint = () => {
+ storage.set(STORAGE_KEYS.API_ENDPOINT, apiEndpoint);
+ toast.success("API endpoint saved successfully");
+ };
+
+ const handleClearChats = () => {
+ storage.set(STORAGE_KEYS.CHATS, []);
+ toast.success("Chat history cleared successfully");
+ };
+
+ const handleClearSources = () => {
+ storage.set(STORAGE_KEYS.SOURCES, []);
+ toast.success("Sources cleared successfully");
+ };
+
+ const handleResetSettings = () => {
+ storage.remove(STORAGE_KEYS.THEME);
+ storage.remove(STORAGE_KEYS.API_ENDPOINT);
+
+ setApiEndpoint("http://localhost:8000");
+ setTheme(window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
+
+ toast.success("Settings reset to defaults");
+ };
+
+ const handleExportStorage = () => {
+ const data = storage.export();
+ const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
+ const url = URL.createObjectURL(blob);
+
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = "insight-storage-export.json";
+ a.click();
+ URL.revokeObjectURL(url);
+
+ toast.success("Storage exported successfully");
+ };
+
+ const handleImportStorage = (event: React.ChangeEvent) => {
+ const file = event.target.files?.[0];
+ if (!file) return;
+ const reader = new FileReader();
+ reader.onload = (e) => {
+ try {
+ const result = e.target?.result as string;
+ const data = JSON.parse(result);
+ if (storage.import(data)) {
+ toast.success("Storage imported successfully. Please refresh the page.");
+ } else {
+ toast.error("Failed to import storage.");
+ }
+ } catch {
+ toast.error("Invalid file format.");
+ }
+ };
+ reader.readAsText(file);
+ event.target.value = "";
+ };
+
+ return (
+
+
+
+
+ System Settings
+
+
+
+
+
+
+
+ Appearance
+
+
+ Customize how Insight AI looks
+
+
+
+
+
+
+
+
+ Light
+
+
+
+
+
+
+ Dark
+
+
+
+
+
+
+ System
+
+
+
+
+
+
+
+
+
+
+ API Configuration
+
+
+ Configure connection to the financial rulings database
+
+
+
+
+
API Endpoint
+
+ setApiEndpoint(e.target.value)}
+ placeholder="http://localhost:8000"
+ className="glass-input"
+ />
+
+
+ Save
+
+
+
+
+
+
+
+
+
+
+ Privacy & Data
+
+
+ Manage your data and privacy settings
+
+
+
+
+
Data Management
+
+
+ Clear Chat History
+
+
+
+ Clear Source References
+
+
+
+ Reset All Settings
+
+
+
+
+
+
+
+
+
+
+ Storage Export / Import
+
+
+ Backup or restore your application data
+
+
+
+
+
+ Export Storage
+
+ fileInputRef.current?.click()}
+ className="tech-button"
+ >
+ Import Storage
+
+
+
+
+ Export will download your data as a JSON file. Import will overwrite your current data with the file contents.
+
+
+
+
+
+
+ );
+};
+
+export default SettingsPage;
diff --git a/frontend/src/pages/SourcesPage.tsx b/frontend/src/pages/SourcesPage.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..1f9b99109ecb1bf50eda738c3a09db93826ecbed
--- /dev/null
+++ b/frontend/src/pages/SourcesPage.tsx
@@ -0,0 +1,286 @@
+
+import { useState, useEffect } from "react";
+import { FileText, Search, Filter, Calendar, ArrowUpDown, ChevronDown, ChevronUp, ExternalLink } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
+import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
+import { Calendar as CalendarComponent } from "@/components/ui/calendar";
+import { cn } from "@/lib/utils";
+import { Separator } from "@/components/ui/separator";
+import { format } from "date-fns";
+import { DateRange } from "@/types";
+import { RetrievedSource } from "@/services/rulingService";
+import { storage, STORAGE_KEYS } from "@/lib/storage";
+
+const SourcesPage = () => {
+ const [searchTerm, setSearchTerm] = useState("");
+ const [selectedSource, setSelectedSource] = useState("");
+ const [selectedCategory, setSelectedCategory] = useState("");
+ const [date, setDate] = useState({
+ from: undefined,
+ to: undefined,
+ });
+ const [sortBy, setSortBy] = useState("date-desc");
+ const [sources, setSources] = useState([]);
+ const [sourceTypes, setSourceTypes] = useState([]);
+ const [categories, setCategories] = useState([]);
+
+ // Load sources from storage
+ useEffect(() => {
+ const storedSources = storage.get(STORAGE_KEYS.SOURCES) || [];
+ setSources(storedSources);
+
+ // Extract unique source types and categories
+ const types = Array.from(new Set(storedSources.map(s => s.metadata?.source).filter(Boolean)));
+ setSourceTypes(types as string[]);
+
+ const cats = Array.from(new Set(storedSources.map(s => {
+ // For this example, we'll use the first word of the content as a mock category
+ // In a real app, you'd use a proper category field from metadata
+ const firstWord = s.content_snippet.split(' ')[0];
+ return firstWord.length > 3 ? firstWord : "General";
+ })));
+ setCategories(cats);
+ }, []);
+
+ // Filter sources based on search term, source, category, and date
+ const filteredSources = sources.filter(source => {
+ const matchesSearch = !searchTerm ||
+ source.content_snippet.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ (source.metadata?.source || "").toLowerCase().includes(searchTerm.toLowerCase());
+
+ const matchesSource = !selectedSource || source.metadata?.source === selectedSource;
+
+ // Mock category matching based on first word of content
+ const sourceCategory = source.content_snippet.split(' ')[0].length > 3 ?
+ source.content_snippet.split(' ')[0] : "General";
+ const matchesCategory = !selectedCategory || sourceCategory === selectedCategory;
+
+ let matchesDate = true;
+ if (date.from && source.metadata?.ruling_date) {
+ matchesDate = matchesDate && new Date(source.metadata.ruling_date) >= date.from;
+ }
+ if (date.to && source.metadata?.ruling_date) {
+ matchesDate = matchesDate && new Date(source.metadata.ruling_date) <= date.to;
+ }
+
+ return matchesSearch && matchesSource && matchesCategory && matchesDate;
+ });
+
+ // Sort sources
+ const sortedSources = [...filteredSources].sort((a, b) => {
+ if (sortBy === "date-desc") {
+ return new Date(b.metadata?.ruling_date || "").getTime() -
+ new Date(a.metadata?.ruling_date || "").getTime();
+ } else if (sortBy === "date-asc") {
+ return new Date(a.metadata?.ruling_date || "").getTime() -
+ new Date(b.metadata?.ruling_date || "").getTime();
+ } else if (sortBy === "relevance-desc") {
+ return b.content_snippet.length - a.content_snippet.length;
+ }
+ return 0;
+ });
+
+ const resetFilters = () => {
+ setSearchTerm("");
+ setSelectedSource("");
+ setSelectedCategory("");
+ setDate({ from: undefined, to: undefined });
+ };
+
+ const [expandedSources, setExpandedSources] = useState>({});
+
+ const toggleSource = (index: number) => {
+ setExpandedSources(prev => ({
+ ...prev,
+ [index]: !prev[index]
+ }));
+ };
+
+ return (
+
+
+
+ Financial Sources
+
+
+
+
+
+
+
+
+ Date (Newest)
+ Date (Oldest)
+ Relevance
+
+
+
+
+
+
+
+
+
+ setSearchTerm(e.target.value)}
+ className="pl-10 glass-input"
+ />
+
+
+
+
+
+ {selectedSource || "All Sources"}
+
+
+
+ All Sources
+ {sourceTypes.map(type => (
+ {type}
+ ))}
+
+
+
+
+
+
+ {selectedCategory || "All Categories"}
+
+
+
+ All Categories
+ {categories.map(category => (
+ {category}
+ ))}
+
+
+
+
+
+
+
+
+ {date.from || date.to ? (
+ <>
+ {date.from ? format(date.from, "LLL dd, y") : "From"} - {" "}
+ {date.to ? format(date.to, "LLL dd, y") : "To"}
+ >
+ ) : (
+ "Date Range"
+ )}
+
+
+
+
+ {
+ if (value) setDate(value);
+ }}
+ className={cn("p-3 pointer-events-auto")}
+ />
+
+
+
+
+ Reset
+
+
+
+
+
+ {sortedSources.length > 0 ? (
+ sortedSources.map((source, index) => (
+
+
+
+
+
+ {source.metadata?.source || "Financial Ruling"}
+
+
+
+ {source.metadata?.source || "Unknown Source"}
+ {source.metadata?.ruling_date && (
+ <>
+ •
+
+ {new Date(source.metadata.ruling_date).toLocaleDateString()}
+ >
+ )}
+
+
+
toggleSource(index)}
+ className="p-0 h-8 w-8 hover:bg-accent/10"
+ >
+ {expandedSources[index] ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ {expandedSources[index]
+ ? source.content_snippet
+ : source.content_snippet.substring(0, 150) + "..."}
+
+ {!expandedSources[index] && (
+
+ )}
+
+ {expandedSources[index] && (
+
+
+
+ View source
+
+
+ )}
+
+ ))
+ ) : (
+
+
+
+
+
No sources found
+
+ {sources.length > 0
+ ? "No sources match your current search criteria. Try adjusting your filters."
+ : "Chat with Insight AI to get information with source citations."}
+
+
+ Reset Filters
+
+
+ )}
+
+
+ );
+};
+
+export default SourcesPage;
diff --git a/frontend/src/pages/TvShowDetailPage.tsx b/frontend/src/pages/TvShowDetailPage.tsx
deleted file mode 100644
index 44346ac940bd1a5b19fa51df3609e18a5beea172..0000000000000000000000000000000000000000
--- a/frontend/src/pages/TvShowDetailPage.tsx
+++ /dev/null
@@ -1,469 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import { useParams, Link } from 'react-router-dom';
-import { Play, Plus, ThumbsUp, Share2, ChevronDown } from 'lucide-react';
-import { getTvShowMetadata, getGenresItems } from '../lib/api';
-import ContentRow from '../components/ContentRow';
-import { useToast } from '@/hooks/use-toast';
-
-interface Episode {
- episode_number: number;
- name: string;
- overview: string;
- still_path: string;
- air_date: string;
- runtime: number;
- fileName?: string; // The actual file name with extension
-}
-
-interface Season {
- season_number: number;
- name: string;
- overview: string;
- poster_path: string;
- air_date: string;
- episodes: Episode[];
-}
-
-interface FileStructureItem {
- type: string;
- path: string;
- contents?: FileStructureItem[];
- size?: number;
-}
-
-const TvShowDetailPage = () => {
- const { title } = useParams<{ title: string }>();
- const [tvShow, setTvShow] = useState(null);
- const [seasons, setSeasons] = useState([]);
- const [selectedSeason, setSelectedSeason] = useState(1);
- const [episodes, setEpisodes] = useState([]);
- const [loading, setLoading] = useState(true);
- const [seasonsLoading, setSeasonsLoading] = useState(false);
- const [similarShows, setSimilarShows] = useState([]);
- const [expandedSeasons, setExpandedSeasons] = useState(false);
- const { toast } = useToast();
-
- // Helper function to extract episode info from file path
- const extractEpisodeInfoFromPath = (filePath: string): Episode | null => {
- // Get the actual file name (with extension) from the full file path
- const fileName = filePath.split('/').pop() || filePath;
- // For file names like "Nanbaka - S01E02 - The Inmates Are Stupid! The Guards Are Kind of Stupid, Too! SDTV.mp4"
- const episodeRegex = /S(\d+)E(\d+)\s*-\s*(.+?)(?=\.\w+$)/i;
- const match = fileName.match(episodeRegex);
-
- if (match) {
- const episodeNumber = parseInt(match[2], 10);
- const episodeName = match[3].trim();
-
- // Determine quality from the file name
- const isHD = fileName.toLowerCase().includes('720p') ||
- fileName.toLowerCase().includes('1080p') ||
- fileName.toLowerCase().includes('hdtv');
-
- return {
- episode_number: episodeNumber,
- name: episodeName,
- overview: '', // No overview available from file path
- still_path: '/placeholder.svg', // Use placeholder image
- air_date: '', // No air date available
- runtime: isHD ? 24 : 22, // Approximate runtime based on quality
- fileName: fileName // Store only the file name with extension
- };
- }
-
- return null;
- };
-
- // Helper function to extract season number and name from directory path
- const getSeasonInfoFromPath = (path: string): { number: number, name: string } => {
- const seasonRegex = /Season\s*(\d+)/i;
- const specialsRegex = /Specials/i;
-
- if (specialsRegex.test(path)) {
- return { number: 0, name: 'Specials' };
- }
-
- const match = path.match(seasonRegex);
- if (match) {
- return {
- number: parseInt(match[1], 10),
- name: `Season ${match[1]}`
- };
- }
-
- return { number: 1, name: 'Season 1' }; // Default if no match
- };
-
- // Process the file structure to extract seasons and episodes
- const processTvShowFileStructure = (fileStructure: any): Season[] => {
- if (!fileStructure || !fileStructure.contents) {
- return [];
- }
-
- const extractedSeasons: Season[] = [];
-
- // Find season directories
- const seasonDirectories = fileStructure.contents.filter(
- (item: FileStructureItem) => item.type === 'directory'
- );
-
- seasonDirectories.forEach((seasonDir: FileStructureItem) => {
- if (!seasonDir.contents) return;
-
- const seasonInfo = getSeasonInfoFromPath(seasonDir.path);
- const episodesArr: Episode[] = [];
-
- // Process files in this season directory
- seasonDir.contents.forEach((item: FileStructureItem) => {
- if (item.type === 'file') {
- const episode = extractEpisodeInfoFromPath(item.path);
- if (episode) {
- episodesArr.push(episode);
- }
- }
- });
-
- // Sort episodes by episode number
- episodesArr.sort((a, b) => a.episode_number - b.episode_number);
-
- if (episodesArr.length > 0) {
- extractedSeasons.push({
- season_number: seasonInfo.number,
- name: seasonInfo.name,
- overview: '', // No overview available
- poster_path: tvShow?.data?.image || '/placeholder.svg',
- air_date: tvShow?.data?.year || '',
- episodes: episodesArr
- });
- }
- });
-
- // Sort seasons by season number
- extractedSeasons.sort((a, b) => a.season_number - b.season_number);
- return extractedSeasons;
- };
-
- useEffect(() => {
- const fetchTvShowData = async () => {
- if (!title) return;
-
- try {
- setLoading(true);
- const data = await getTvShowMetadata(title);
- setTvShow(data);
-
- if (data && data.file_structure) {
- const processedSeasons = processTvShowFileStructure(data.file_structure);
- setSeasons(processedSeasons);
-
- // Select the first season by default (Specials = 0, Season 1 = 1)
- if (processedSeasons.length > 0) {
- setSelectedSeason(processedSeasons[0].season_number);
- }
- }
-
-
- // Fetch similar shows based on individual genres
- if (data.data && data.data.genres && data.data.genres.length > 0) {
- const currentShowName = data.data.name;
- const showsByGenre = await Promise.all(
- data.data.genres.map(async (genre: any) => {
- // Pass a single genre name for each call
- const genreResult = await getGenresItems([genre.name], 'series', 10, 1);
- console.log('Genre result:', genreResult);
- if (genreResult.series && Array.isArray(genreResult.series)) {
- return genreResult.series.map((showItem: any) => {
- const { title: similarTitle } = showItem;
- console.log('Similar show:', showItem);
- // Skip current show
- if (similarTitle === currentShowName) return null;
- return {
- type: 'tvshow',
- title: similarTitle,
- };
- });
- }
- return [];
- })
- );
-
- // Flatten the array of arrays and remove null results
- const flattenedShows = showsByGenre.flat().filter(Boolean);
- // Remove duplicates based on the title
- const uniqueShows = Array.from(
- new Map(flattenedShows.map(show => [show.title, show])).values()
- );
- setSimilarShows(uniqueShows);
- }
- } catch (error) {
- console.error(`Error fetching TV show details for ${title}:`, error);
- toast({
- title: "Error loading TV show details",
- description: "Please try again later",
- variant: "destructive"
- });
- } finally {
- setLoading(false);
- }
- };
-
- fetchTvShowData();
- }, [title, toast]);
-
- // Update episodes when selectedSeason or seasons change
- useEffect(() => {
- if (seasons.length > 0) {
- const season = seasons.find(s => s.season_number === selectedSeason);
- if (season) {
- setEpisodes(season.episodes);
- } else {
- setEpisodes([]);
- }
- }
- }, [selectedSeason, seasons]);
-
- const toggleExpandSeasons = () => {
- setExpandedSeasons(!expandedSeasons);
- };
-
- if (loading) {
- return (
-
- );
- }
-
- if (!tvShow) {
- return (
-
-
TV Show Not Found
-
We couldn't find the TV show you're looking for.
-
- Back to TV Shows
-
-
- );
- }
-
- const tvShowData = tvShow.data;
- const airYears = tvShowData.year;
- const language = tvShowData.originalLanguage;
- const showName = (tvShowData.translations?.nameTranslations?.find((t: any) => t.language === 'eng')?.name || tvShowData.name || '');
- const overview =
- tvShowData.translations?.overviewTranslations?.find((t: any) => t.language === 'eng')?.overview ||
- tvShowData.translations?.overviewTranslations?.[0]?.overview ||
- tvShowData.overview ||
- 'No overview available.';
-
- // Get the current season details
- const currentSeason = seasons.find(s => s.season_number === selectedSeason);
- const currentSeasonName = currentSeason?.name || `Season ${selectedSeason}`;
-
- return (
-
- {/* Hero backdrop */}
-
-
-
{
- const target = e.target as HTMLImageElement;
- target.src = '/placeholder.svg';
- }}
- />
-
-
-
-
-
- {/* TV Show details */}
-
-
- {/* Poster */}
-
-
{
- const target = e.target as HTMLImageElement;
- target.src = '/placeholder.svg';
- }}
- />
-
-
- {/* Details */}
-
-
{showName}
-
-
- {airYears && {airYears} }
- {tvShowData.vote_average && (
-
- ★ {tvShowData.vote_average.toFixed(1)}
-
- )}
- {seasons.length > 0 && (
- {seasons.length} Season{seasons.length !== 1 ? 's' : ''}
- )}
-
-
-
- {tvShowData.genres && tvShowData.genres.map((genre: any, index: number) => (
-
- {genre.name || genre}
-
- ))}
-
-
-
{overview}
-
-
-
-
Play
-
-
-
- My List
-
-
-
-
-
-
-
-
-
-
-
- {/* Additional details */}
-
- {language && (
-
-
Language
-
{language}
-
- )}
- {tvShowData.translations?.nameTranslations?.find((t: any) => t.isPrimary) && (
-
-
Tagline
-
- "{tvShowData.translations.nameTranslations.find((t: any) => t.isPrimary).tagline || ''}"
-
-
- )}
-
-
-
-
- {/* Episodes */}
-
-
-
-
Episodes
-
-
-
- {currentSeasonName}
-
-
-
- {expandedSeasons && (
-
- {seasons.map((season) => (
- {
- setSelectedSeason(season.season_number);
- setExpandedSeasons(false);
- }}
- >
- {season.name}
-
- ))}
-
- )}
-
-
-
-
-
- {seasonsLoading ? (
-
- ) : episodes.length === 0 ? (
-
- No episodes available for this season.
-
- ) : (
- episodes.map((episode) => (
-
-
-
-
{
- const target = e.target as HTMLImageElement;
- target.src = '/placeholder.svg';
- }}
- />
-
-
- {episode.runtime ? `${episode.runtime} min` : '--'}
-
-
-
-
-
-
- {episode.episode_number}. {episode.name}
-
-
- {episode.air_date ? new Date(episode.air_date).toLocaleDateString() : ''}
-
-
-
- {episode.overview || 'No description available.'}
-
-
-
-
- ))
- )}
-
-
-
- {/* Similar Shows */}
- {similarShows.length > 0 && (
-
-
-
- )}
-
-
- );
-};
-
-export default TvShowDetailPage;
diff --git a/frontend/src/pages/TvShowPlayerPage.tsx b/frontend/src/pages/TvShowPlayerPage.tsx
deleted file mode 100644
index 771872d8380c96da2a5a7f99d8baff34003681b1..0000000000000000000000000000000000000000
--- a/frontend/src/pages/TvShowPlayerPage.tsx
+++ /dev/null
@@ -1,423 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import { useParams, useNavigate, useSearchParams } from 'react-router-dom';
-import { getTvShowMetadata } from '../lib/api';
-import { useToast } from '@/hooks/use-toast';
-import TVShowPlayer from '../components/TVShowPlayer';
-import EpisodesPanel from '../components/EpisodesPanel';
-
-interface FileStructureItem {
- type: string;
- path: string;
- contents?: FileStructureItem[];
- size?: number;
-}
-
-interface Episode {
- episode_number: number;
- name: string;
- overview: string;
- still_path: string;
- air_date: string;
- runtime: number;
- fileName?: string; // The actual file name with extension
-}
-
-interface Season {
- season_number: number;
- name: string;
- episodes: Episode[];
-}
-
-interface PlaybackProgress {
- [key: string]: {
- currentTime: number;
- duration: number;
- lastPlayed: string;
- completed: boolean;
- };
-}
-
-const TvShowPlayerPage = () => {
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState(null);
- const [showInfo, setShowInfo] = useState(null);
- const [showName, setShowName] = useState('');
- const [seasons, setSeasons] = useState([]);
- const [selectedSeason, setSelectedSeason] = useState('');
- const [selectedEpisode, setSelectedEpisode] = useState('');
- const [activeEpisodeIndex, setActiveEpisodeIndex] = useState(0);
- const [activeSeasonIndex, setActiveSeasonIndex] = useState(0);
- const [showEpisodeSelector, setShowEpisodeSelector] = useState(false);
- const [playbackProgress, setPlaybackProgress] = useState({});
- const [needsReload, setNeedsReload] = useState(false); // Flag to trigger video reload
-
- const { title } = useParams<{ title: string }>();
- const [searchParams] = useSearchParams();
- const seasonParam = searchParams.get('season');
- const episodeParam = searchParams.get('episode');
-
- const navigate = useNavigate();
- const { toast } = useToast();
-
- // Helper function to extract episode info from file path
- const extractEpisodeInfoFromPath = (filePath: string): Episode | null => {
- const fileName = filePath.split('/').pop() || filePath;
- const episodeRegex = /S(\d+)E(\d+)\s*-\s*(.+?)(?=\.\w+$)/i;
- const match = fileName.match(episodeRegex);
-
- if (match) {
- const episodeNumber = parseInt(match[2], 10);
- const episodeName = match[3].trim();
- const isHD = fileName.toLowerCase().includes('720p') ||
- fileName.toLowerCase().includes('1080p') ||
- fileName.toLowerCase().includes('hdtv');
-
- return {
- episode_number: episodeNumber,
- name: episodeName,
- overview: '',
- still_path: '/placeholder.svg',
- air_date: '',
- runtime: isHD ? 24 : 22,
- fileName: fileName
- };
- }
-
- return null;
- };
-
- // Helper function to extract season info from directory path
- const getSeasonInfoFromPath = (path: string): { number: number, name: string } => {
- const seasonRegex = /Season\s*(\d+)/i;
- const specialsRegex = /Specials/i;
-
- if (specialsRegex.test(path)) {
- return { number: 0, name: 'Specials' };
- }
-
- const match = path.match(seasonRegex);
- if (match) {
- return {
- number: parseInt(match[1], 10),
- name: `Season ${match[1]}`
- };
- }
-
- return { number: 1, name: 'Season 1' };
- };
-
- // Process the file structure to extract seasons and episodes
- const processTvShowFileStructure = (fileStructure: any): Season[] => {
- if (!fileStructure || !fileStructure.contents) {
- return [];
- }
-
- const extractedSeasons: Season[] = [];
-
- // Find season directories
- const seasonDirectories = fileStructure.contents.filter(
- (item: FileStructureItem) => item.type === 'directory'
- );
-
- seasonDirectories.forEach((seasonDir: FileStructureItem) => {
- if (!seasonDir.contents) return;
-
- const seasonInfo = getSeasonInfoFromPath(seasonDir.path);
- const episodesArr: Episode[] = [];
-
- // Process files in this season directory
- seasonDir.contents.forEach((item: FileStructureItem) => {
- if (item.type === 'file') {
- const episode = extractEpisodeInfoFromPath(item.path);
- if (episode) {
- episodesArr.push(episode);
- }
- }
- });
-
- // Sort episodes by episode number
- episodesArr.sort((a, b) => a.episode_number - b.episode_number);
-
- if (episodesArr.length > 0) {
- extractedSeasons.push({
- season_number: seasonInfo.number,
- name: seasonInfo.name,
- episodes: episodesArr
- });
- }
- });
-
- // Sort seasons by season number
- extractedSeasons.sort((a, b) => a.season_number - b.season_number);
- return extractedSeasons;
- };
-
- // Select first available episode when none is specified
- const selectFirstAvailableEpisode = (seasons: Season[]) => {
- if (seasons.length === 0) return;
-
- // First try to find Season 1
- const regularSeason = seasons.find(s => s.season_number === 1);
- // If not available, use the first available season (could be Specials/Season 0)
- const firstSeason = regularSeason || seasons[0];
-
- if (firstSeason && firstSeason.episodes.length > 0) {
- setSelectedSeason(firstSeason.name);
- setSelectedEpisode(firstSeason.episodes[0].fileName || '');
- setActiveSeasonIndex(seasons.indexOf(firstSeason));
- setActiveEpisodeIndex(0);
- }
- };
-
- // Load playback progress from localStorage
- const loadPlaybackProgress = () => {
- try {
- const storedProgress = localStorage.getItem(`playback-${title}`);
- if (storedProgress) {
- setPlaybackProgress(JSON.parse(storedProgress));
- }
- } catch (error) {
- console.error("Failed to load playback progress:", error);
- }
- };
-
- // Save playback progress to localStorage
- const savePlaybackProgress = (episodeId: string, currentTime: number, duration: number, completed: boolean = false) => {
- try {
- const newProgress = {
- ...playbackProgress,
- [episodeId]: {
- currentTime,
- duration,
- lastPlayed: new Date().toISOString(),
- completed
- }
- };
-
- localStorage.setItem(`playback-${title}`, JSON.stringify(newProgress));
- setPlaybackProgress(newProgress);
- } catch (error) {
- console.error("Failed to save playback progress:", error);
- }
- };
-
- // Function to load the next episode and reset the video
- const loadNextEpisode = () => {
- if (!seasons.length) return;
-
- const currentSeason = seasons[activeSeasonIndex];
- if (!currentSeason) return;
-
- // If there's another episode in the current season
- if (activeEpisodeIndex < currentSeason.episodes.length - 1) {
- const nextEpisode = currentSeason.episodes[activeEpisodeIndex + 1];
- setSelectedEpisode(nextEpisode.fileName || '');
- setActiveEpisodeIndex(activeEpisodeIndex + 1);
- setNeedsReload(true); // Flag to reload the video
-
- // Update URL without page reload
- navigate(`/tv-show/${encodeURIComponent(title || '')}/watch?season=${encodeURIComponent(currentSeason.name)}&episode=${encodeURIComponent(nextEpisode.fileName || '')}`, { replace: true });
-
- toast({
- title: "Playing Next Episode",
- description: `${nextEpisode.name}`,
- });
- }
- // If there's another season available
- else if (activeSeasonIndex < seasons.length - 1) {
- const nextSeason = seasons[activeSeasonIndex + 1];
- if (nextSeason.episodes.length > 0) {
- const firstEpisode = nextSeason.episodes[0];
- setSelectedSeason(nextSeason.name);
- setSelectedEpisode(firstEpisode.fileName || '');
- setActiveSeasonIndex(activeSeasonIndex + 1);
- setActiveEpisodeIndex(0);
- setNeedsReload(true); // Flag to reload the video
-
- // Update URL without page reload
- navigate(`/tv-show/${encodeURIComponent(title || '')}/watch?season=${encodeURIComponent(nextSeason.name)}&episode=${encodeURIComponent(firstEpisode.fileName || '')}`, { replace: true });
-
- toast({
- title: "Starting Next Season",
- description: `${nextSeason.name}: ${firstEpisode.name}`,
- });
- }
- } else {
- toast({
- title: "End of Series",
- description: "You've watched all available episodes.",
- });
- }
- };
-
- // Watch for changes in seasons or episodes selection
- useEffect(() => {
- if (seasons.length && selectedSeason && selectedEpisode) {
- // Find active season and episode indexes
- const seasonIndex = seasons.findIndex(s => s.name === selectedSeason);
- if (seasonIndex >= 0) {
- setActiveSeasonIndex(seasonIndex);
-
- const episodeIndex = seasons[seasonIndex].episodes.findIndex(
- e => e.fileName === selectedEpisode
- );
- if (episodeIndex >= 0) {
- setActiveEpisodeIndex(episodeIndex);
- }
- }
- }
- }, [seasons, selectedSeason, selectedEpisode]);
-
- useEffect(() => {
- const fetchData = async () => {
- if (!title) return;
-
- try {
- setLoading(true);
- setError(null);
-
- // Load saved playback progress
- loadPlaybackProgress();
-
- // Get TV show metadata first
- const showData = await getTvShowMetadata(title);
- setShowInfo(showData);
- console.log('TV Show Metadata:', showData);
- setShowName(showInfo?.data?.translations?.nameTranslations?.find((t: any) => t.language === 'eng')?.name || showInfo?.name || '');
- // Process seasons and episodes from file structure
- if (showData && showData.file_structure) {
- const processedSeasons = processTvShowFileStructure(showData.file_structure);
- setSeasons(processedSeasons);
-
- // Set selected season and episode from URL params or select first available
- if (seasonParam && episodeParam) {
- setSelectedSeason(seasonParam);
- setSelectedEpisode(episodeParam);
- } else {
- selectFirstAvailableEpisode(processedSeasons);
- }
- }
- } catch (error) {
- console.error(`Error fetching metadata for ${title}:`, error);
- setError('Failed to load episode data');
- toast({
- title: "Error Loading Data",
- description: "Please try again later",
- variant: "destructive"
- });
- } finally {
- setLoading(false);
- }
- };
-
- fetchData();
- }, [title, seasonParam, episodeParam, toast]);
-
- const handleBack = () => {
- navigate(`/tv-show/${encodeURIComponent(title || '')}`);
- };
-
- const getEpisodeNumber = () => {
- if (!selectedEpisode) return "1";
-
- const episodeMatch = selectedEpisode.match(/E(\d+)/i);
- return episodeMatch ? episodeMatch[1] : "1";
- };
-
- const handleSelectEpisode = (seasonName: string, episode: Episode) => {
- // Only reload if we're changing episodes
- if (selectedEpisode !== episode.fileName) {
- setSelectedSeason(seasonName);
- setSelectedEpisode(episode.fileName || '');
- setNeedsReload(true);
-
- // Update URL
- navigate(`/tv-show/${encodeURIComponent(title || '')}/watch?season=${encodeURIComponent(seasonName)}&episode=${encodeURIComponent(episode.fileName || '')}`, { replace: true });
- }
-
- setShowEpisodeSelector(false);
- };
-
- const handleProgressUpdate = (currentTime: number, duration: number) => {
- if (!selectedEpisode || !title) return;
-
- const episodeId = `${selectedSeason}-${selectedEpisode}`;
- const isCompleted = (currentTime / duration) > 0.9; // Mark as completed if watched 90%
-
- savePlaybackProgress(episodeId, currentTime, duration, isCompleted);
- };
-
- const getStartTime = () => {
- if (!selectedSeason || !selectedEpisode) return 0;
-
- const episodeId = `${selectedSeason}-${selectedEpisode}`;
- const progress = playbackProgress[episodeId];
-
- if (progress && !progress.completed) {
- return progress.currentTime;
- }
- return 0;
- };
-
- const episodeTitle = showInfo
- ? `${showInfo.data?.name} ${selectedSeason}E${getEpisodeNumber()}`
- : `Episode`;
- // Reset needs reload flag when video has been updated
- useEffect(() => {
- if (needsReload) {
- setNeedsReload(false);
- }
- }, [selectedEpisode, selectedSeason]);
-
- if (loading) {
- return (
-
- );
- }
-
- // To force reload of video component when changing episodes
- const tvShowPlayerKey = `${selectedSeason}-${selectedEpisode}-${needsReload ? 'reload' : 'loaded'}`;
-
- return (
-
- {/* Episodes panel */}
- {showEpisodeSelector && (
-
setShowEpisodeSelector(false)}>
-
e.stopPropagation()}
- >
- setShowEpisodeSelector(false)}
- showTitle={showName || 'Episodes'}
- />
-
-
- )}
-
- {/* TV Show Player component with key to force reload */}
-
setShowEpisodeSelector(true)}
- />
-
- );
-};
-
-export default TvShowPlayerPage;
diff --git a/frontend/src/pages/TvShowsPage.tsx b/frontend/src/pages/TvShowsPage.tsx
deleted file mode 100644
index fcbf60f80a520412fd6f054e37bf11ed142a5df0..0000000000000000000000000000000000000000
--- a/frontend/src/pages/TvShowsPage.tsx
+++ /dev/null
@@ -1,93 +0,0 @@
-
-import React, { useEffect, useState } from 'react';
-import PageHeader from '../components/PageHeader';
-import ContentGrid from '../components/ContentGrid';
-import { getAllTvShows, getTvShowCard } from '../lib/api';
-import { useToast } from '@/hooks/use-toast';
-import { useSearchParams } from 'react-router-dom';
-
-const TvShowsPage = () => {
- const [loading, setLoading] = useState(true);
- const [tvShows, setTvShows] = useState([]);
- const [searchParams] = useSearchParams();
- const genreFilter = searchParams.get('genre');
- const { toast } = useToast();
-
- useEffect(() => {
- const fetchTvShows = async () => {
- try {
- setLoading(true);
- const allTvShows = await getAllTvShows();
-
- // For each tv show, get its card info for display
- const tvShowPromises = allTvShows.slice(0, 30).map(async (show: any) => {
- try {
- const showInfo = await getTvShowCard(show.title);
- if (showInfo) {
- return {
- type: 'tvshow',
- title: show.title,
- image: showInfo.image,
- description: showInfo.overview,
- genre: showInfo.genres?.map((g: any) => g.name) || [],
- year: showInfo.year,
- episodeCount: show.episodeCount
- };
- }
- return null;
- } catch (error) {
- console.error(`Error fetching tv show info for ${show.title}:`, error);
- return null;
- }
- });
-
- let tvShowsData = await Promise.all(tvShowPromises);
- tvShowsData = tvShowsData.filter(show => show !== null);
-
- // Apply genre filter if present
- if (genreFilter) {
- tvShowsData = tvShowsData.filter(show =>
- show.genre.some((g: string) => g.toLowerCase() === genreFilter.toLowerCase())
- );
- }
-
- setTvShows(tvShowsData);
- } catch (error) {
- console.error('Error fetching TV shows:', error);
- toast({
- title: "Error loading TV shows",
- description: "Please try again later",
- variant: "destructive"
- });
- } finally {
- setLoading(false);
- }
- };
-
- fetchTvShows();
- }, [genreFilter, toast]);
-
- return (
-
-
-
- {loading ? (
-
- ) : (
-
- )}
-
- );
-};
-
-export default TvShowsPage;
diff --git a/frontend/src/services/rulingService.ts b/frontend/src/services/rulingService.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4662633933e4998ac083ede59c460828558cb973
--- /dev/null
+++ b/frontend/src/services/rulingService.ts
@@ -0,0 +1,136 @@
+
+import { toast } from "@/components/ui/sonner";
+import { storage, STORAGE_KEYS } from "@/lib/storage";
+
+// Define types matching our API
+export interface SourceMetadata {
+ source?: string;
+ ruling_date?: string;
+ [key: string]: string | undefined;
+}
+
+export interface RetrievedSource {
+ content_snippet: string;
+ metadata?: SourceMetadata;
+}
+
+export interface QueryResponse {
+ answer: string;
+ retrieved_sources?: RetrievedSource[];
+}
+
+export interface TitleResponse {
+ title: string;
+}
+
+export interface Message {
+ role: "user" | "assistant" | "system";
+ content: string;
+}
+
+export interface QueryRequest {
+ query: string;
+ chat_history?: Message[];
+ filters?: Record;
+}
+
+// Add a delay function for better UX when showing loading states
+const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
+
+export const rulingService = {
+ async getApiUrl(): Promise {
+ // Get API URL from storage or use default
+ let API_URL = "http://localhost:8000";
+ const storedUrl = storage.get(STORAGE_KEYS.API_ENDPOINT);
+
+ if (storedUrl) {
+ API_URL = storedUrl;
+ } else {
+ // Set default if not found
+ storage.set(STORAGE_KEYS.API_ENDPOINT, API_URL);
+ }
+
+ return API_URL;
+ },
+
+ async queryRulings(request: QueryRequest): Promise {
+ try {
+ // Add a slight delay to make loading states more visible for demo purposes
+ await delay(1000);
+
+ const API_URL = await this.getApiUrl();
+
+ const response = await fetch(`${API_URL}/query`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(request),
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ const errorMessage = errorData.detail || `Error: ${response.status} ${response.statusText}`;
+ toast.error(errorMessage);
+ throw new Error(errorMessage);
+ }
+
+ const result = await response.json();
+
+ // Store sources in storage for later use on sources page
+ if (result.retrieved_sources && result.retrieved_sources.length > 0) {
+ // Get existing sources or initialize empty array
+ const existingSources = storage.get(STORAGE_KEYS.SOURCES) || [];
+
+ // Merge new sources, avoid duplicates based on content
+ const updatedSources = [...existingSources];
+
+ result.retrieved_sources.forEach((source: RetrievedSource) => {
+ const exists = existingSources.some(
+ existing => existing.content_snippet === source.content_snippet
+ );
+
+ if (!exists) {
+ updatedSources.push(source);
+ }
+ });
+
+ // Store updated sources
+ storage.set(STORAGE_KEYS.SOURCES, updatedSources);
+ }
+
+ return result;
+ } catch (error) {
+ console.error("Failed to query rulings:", error);
+ const errorMessage = error instanceof Error ? error.message : "Failed to query rulings";
+ toast.error(errorMessage);
+ throw error;
+ }
+ },
+
+ async generateTitle(query: string): Promise {
+ try {
+ const API_URL = await this.getApiUrl();
+
+ const response = await fetch(`${API_URL}/generate-title`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ query }),
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ const errorMessage = errorData.detail || `Error: ${response.status} ${response.statusText}`;
+ throw new Error(errorMessage);
+ }
+
+ return await response.json();
+ } catch (error) {
+ console.error("Failed to generate title:", error);
+ // Return a default title instead of throwing
+ return { title: "New Chat" };
+ }
+ }
+};
diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..26da8c27c6893c38e8184055c2aa9b05c6a8cb74
--- /dev/null
+++ b/frontend/src/types/index.ts
@@ -0,0 +1,20 @@
+
+// Filter types
+export interface DateRange {
+ from: Date | undefined;
+ to: Date | undefined;
+}
+
+export interface SearchFilters {
+ rulingDate?: DateRange;
+ source?: string;
+ [key: string]: any;
+}
+
+// Query history type
+export interface QueryHistoryItem {
+ id: string;
+ query: string;
+ timestamp: Date;
+ filters?: SearchFilters;
+}
diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts
index 15bde57be1a013b045fd226fdcb5204823edd50c..0d038668ff074d92d28f9672001171c9b3bb2689 100644
--- a/frontend/tailwind.config.ts
+++ b/frontend/tailwind.config.ts
@@ -1,135 +1,166 @@
-import type { Config } from 'tailwindcss';
+import type { Config } from "tailwindcss";
-const config = {
- darkMode: ["class"],
- content: [
- './pages/**/*.{ts,tsx}',
- './components/**/*.{ts,tsx}',
- './app/**/*.{ts,tsx}',
- './src/**/*.{ts,tsx}',
- ],
- prefix: "",
- theme: {
- container: {
- center: true,
- padding: "2rem",
- screens: {
- "2xl": "1400px",
- },
- },
- extend: {
- colors: {
- border: "hsl(var(--border))",
- input: "hsl(var(--input))",
- ring: "hsl(var(--ring))",
- background: "hsl(var(--background))",
- foreground: "hsl(var(--foreground))",
- primary: {
- DEFAULT: "hsl(var(--primary))",
- foreground: "hsl(var(--primary-foreground))",
- },
- secondary: {
- DEFAULT: "hsl(var(--secondary))",
- foreground: "hsl(var(--secondary-foreground))",
- },
- destructive: {
- DEFAULT: "hsl(var(--destructive))",
- foreground: "hsl(var(--destructive-foreground))",
- },
- muted: {
- DEFAULT: "hsl(var(--muted))",
- foreground: "hsl(var(--muted-foreground))",
- },
- accent: {
- DEFAULT: "hsl(var(--accent))",
- foreground: "hsl(var(--accent-foreground))",
- },
- popover: {
- DEFAULT: "hsl(var(--popover))",
- foreground: "hsl(var(--popover-foreground))",
- },
- card: {
- DEFAULT: "hsl(var(--card))",
- foreground: "hsl(var(--card-foreground))",
- },
- // Netflix-inspired palette
- "netflix-red": "#E50914",
- "netflix-black": "#141414",
- "netflix-dark-gray": "#181818",
- "netflix-gray": "#808080",
- "netflix-light-gray": "#b3b3b3",
-
- // Theme colors (can be customized)
- "theme-primary": "var(--theme-primary, #E50914)",
- "theme-primary-hover": "var(--theme-primary-hover, #B81D24)",
- "theme-primary-light": "var(--theme-primary-light, #F5222D)",
- "theme-secondary": "var(--theme-secondary, #6D6D6D)",
- "theme-background": "var(--theme-background, #141414)",
- "theme-background-dark": "var(--theme-background-dark, #0A0A0A)",
- "theme-background-light": "var(--theme-background-light, #181818)",
- "theme-surface": "var(--theme-surface, #222222)",
- "theme-text": "var(--theme-text, #FFFFFF)",
- "theme-text-secondary": "var(--theme-text-secondary, #B3B3B3)",
- "theme-border": "var(--theme-border, #303030)",
- "theme-divider": "var(--theme-divider, #2D2D2D)",
- "theme-error": "var(--theme-error, #FF574D)",
- "theme-warning": "var(--theme-warning, #FFB01F)",
- "theme-success": "var(--theme-success, #48BB78)",
- "theme-info": "var(--theme-info, #38B2AC)"
- },
- borderRadius: {
- lg: "var(--radius)",
- md: "calc(var(--radius) - 2px)",
- sm: "calc(var(--radius) - 4px)",
- },
- keyframes: {
- "accordion-down": {
- from: { height: "0" },
- to: { height: "var(--radix-accordion-content-height)" },
- },
- "accordion-up": {
- from: { height: "var(--radix-accordion-content-height)" },
- to: { height: "0" },
- },
- "fade-in": {
- "0%": { opacity: "0", transform: "translateY(10px)" },
- "100%": { opacity: "1", transform: "translateY(0)" }
- },
- "fade-out": {
- "0%": { opacity: "1", transform: "translateY(0)" },
- "100%": { opacity: "0", transform: "translateY(10px)" }
- },
- "scale-in": {
- "0%": { transform: "scale(0.95)", opacity: "0" },
- "100%": { transform: "scale(1)", opacity: "1" }
- },
- "scale-out": {
- from: { transform: "scale(1)", opacity: "1" },
- to: { transform: "scale(0.95)", opacity: "0" }
- },
- "slide-in": {
- "0%": { transform: "translateX(100%)" },
- "100%": { transform: "translateX(0)" }
- },
- "slide-out": {
- "0%": { transform: "translateX(0)" },
- "100%": { transform: "translateX(100%)" }
- },
- },
- animation: {
- "accordion-down": "accordion-down 0.2s ease-out",
- "accordion-up": "accordion-up 0.2s ease-out",
- "fade-in": "fade-in 0.3s ease-out forwards",
- "fade-out": "fade-out 0.3s ease-out forwards",
- "scale-in": "scale-in 0.2s ease-out",
- "scale-out": "scale-out 0.2s ease-out",
- "slide-in": "slide-in 0.3s ease-out",
- "slide-out": "slide-out 0.3s ease-out",
- },
- },
- },
- plugins: [require("tailwindcss-animate")],
+export default {
+ darkMode: ["class"],
+ content: [
+ "./pages/**/*.{ts,tsx}",
+ "./components/**/*.{ts,tsx}",
+ "./app/**/*.{ts,tsx}",
+ "./src/**/*.{ts,tsx}",
+ ],
+ prefix: "",
+ theme: {
+ container: {
+ center: true,
+ padding: '2rem',
+ screens: {
+ '2xl': '1400px'
+ }
+ },
+ extend: {
+ colors: {
+ border: 'hsl(var(--border))',
+ input: 'hsl(var(--input))',
+ ring: 'hsl(var(--ring))',
+ background: 'hsl(var(--background))',
+ foreground: 'hsl(var(--foreground))',
+ primary: {
+ DEFAULT: 'hsl(var(--primary))',
+ foreground: 'hsl(var(--primary-foreground))'
+ },
+ secondary: {
+ DEFAULT: 'hsl(var(--secondary))',
+ foreground: 'hsl(var(--secondary-foreground))'
+ },
+ destructive: {
+ DEFAULT: 'hsl(var(--destructive))',
+ foreground: 'hsl(var(--destructive-foreground))'
+ },
+ muted: {
+ DEFAULT: 'hsl(var(--muted))',
+ foreground: 'hsl(var(--muted-foreground))'
+ },
+ accent: {
+ DEFAULT: 'hsl(var(--accent))',
+ foreground: 'hsl(var(--accent-foreground))'
+ },
+ popover: {
+ DEFAULT: 'hsl(var(--popover))',
+ foreground: 'hsl(var(--popover-foreground))'
+ },
+ card: {
+ DEFAULT: 'hsl(var(--card))',
+ foreground: 'hsl(var(--card-foreground))'
+ },
+ sidebar: {
+ DEFAULT: 'hsl(var(--sidebar-background))',
+ foreground: 'hsl(var(--sidebar-foreground))',
+ primary: 'hsl(var(--sidebar-primary))',
+ 'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
+ accent: 'hsl(var(--sidebar-accent))',
+ 'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
+ border: 'hsl(var(--sidebar-border))',
+ ring: 'hsl(var(--sidebar-ring))'
+ },
+ financial: {
+ 'navy': '#1e2b47',
+ 'blue': '#3a5a80',
+ 'light-blue': '#7aa0c9',
+ 'gray': '#f5f7fa',
+ 'accent': '#7868e6',
+ 'light-accent': '#b8b5ff',
+ 'dark-gray': '#2c3142',
+ 'cool-gray': '#9ca3af'
+ }
+ },
+ borderRadius: {
+ lg: 'var(--radius)',
+ md: 'calc(var(--radius) - 2px)',
+ sm: 'calc(var(--radius) - 4px)'
+ },
+ fontFamily: {
+ sans: ['Inter', 'system-ui', 'sans-serif'],
+ heading: ['Space Grotesk', 'sans-serif']
+ },
+ keyframes: {
+ 'accordion-down': {
+ from: {
+ height: '0'
+ },
+ to: {
+ height: 'var(--radix-accordion-content-height)'
+ }
+ },
+ 'accordion-up': {
+ from: {
+ height: 'var(--radix-accordion-content-height)'
+ },
+ to: {
+ height: '0'
+ }
+ },
+ 'fade-in': {
+ '0%': {
+ opacity: '0',
+ transform: 'translateY(10px)'
+ },
+ '100%': {
+ opacity: '1',
+ transform: 'translateY(0)'
+ }
+ },
+ 'pulse-subtle': {
+ '0%, 100%': {
+ opacity: '1'
+ },
+ '50%': {
+ opacity: '0.8'
+ }
+ },
+ 'slide-in': {
+ '0%': {
+ transform: 'translateY(20px)',
+ opacity: '0'
+ },
+ '100%': {
+ transform: 'translateY(0)',
+ opacity: '1'
+ }
+ },
+ 'bounce-in': {
+ '0%': {
+ transform: 'scale(0.95)',
+ opacity: '0'
+ },
+ '70%': {
+ transform: 'scale(1.05)'
+ },
+ '100%': {
+ transform: 'scale(1)',
+ opacity: '1'
+ }
+ },
+ 'shimmer': {
+ '100%': {
+ transform: 'translateX(100%)'
+ }
+ }
+ },
+ animation: {
+ 'accordion-down': 'accordion-down 0.2s ease-out',
+ 'accordion-up': 'accordion-up 0.2s ease-out',
+ 'fade-in': 'fade-in 0.5s ease-out forwards',
+ 'pulse-subtle': 'pulse-subtle 2s ease-in-out infinite',
+ 'slide-in': 'slide-in 0.3s ease-out forwards',
+ 'bounce-in': 'bounce-in 0.4s ease-out',
+ 'shimmer': 'shimmer 2s infinite linear'
+ },
+ backdropFilter: {
+ 'none': 'none',
+ 'blur': 'blur(20px)'
+ }
+ }
+ },
+ plugins: [require("tailwindcss-animate")],
} satisfies Config;
-
-export default config;