Skip to content

Commit eca15a1

Browse files
SamSam
authored andcommitted
Added Search assist, inhouse trailer, watchlist, journal, genre charts
1 parent 5f69cdb commit eca15a1

12 files changed

Lines changed: 834 additions & 40 deletions

File tree

‎package-lock.json‎

Lines changed: 31 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@
1111
},
1212
"dependencies": {
1313
"axios": "^1.13.2",
14+
"chart.js": "^4.5.1",
1415
"lucide-react": "^0.562.0",
1516
"react": "^19.2.0",
17+
"react-chartjs-2": "^5.3.1",
1618
"react-dom": "^19.2.0"
1719
},
1820
"devDependencies": {

‎src/App.jsx‎

Lines changed: 32 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,17 @@ import { searchMovies, getTrendingMovies } from './services/tmdb';
33
import MovieCard from './components/MovieCard';
44
import MovieDetail from './components/MovieDetail';
55
import PersonDetail from './components/PersonDetail';
6-
import { Search, Film } from 'lucide-react';
6+
import Profile from './components/Profile';
7+
import SearchAutocomplete from './components/SearchAutocomplete';
8+
import { Search, Film, User } from 'lucide-react';
9+
10+
711

812
function App() {
9-
const [query, setQuery] = useState('');
1013
const [movies, setMovies] = useState([]);
1114
const [selectedMovieId, setSelectedMovieId] = useState(null);
1215
const [selectedPersonId, setSelectedPersonId] = useState(null);
16+
const [showProfile, setShowProfile] = useState(false);
1317
const [loading, setLoading] = useState(false);
1418

1519
useEffect(() => {
@@ -27,36 +31,26 @@ function App() {
2731
setLoading(false);
2832
};
2933

30-
const handleSearch = async (e) => {
31-
e.preventDefault();
32-
if (!query.trim()) return loadTrending();
33-
34-
setLoading(true);
35-
setSelectedMovieId(null);
36-
setSelectedPersonId(null);
37-
try {
38-
const results = await searchMovies(query);
39-
setMovies(results);
40-
} catch (e) {
41-
console.error(e);
42-
}
43-
setLoading(false);
34+
const handleMovieSelect = (movieId) => {
35+
setSelectedMovieId(movieId);
36+
setShowProfile(false);
4437
};
4538

4639
const goHome = () => {
4740
setSelectedMovieId(null);
4841
setSelectedPersonId(null);
49-
setQuery('');
42+
setShowProfile(false);
5043
loadTrending();
5144
};
5245

46+
5347
return (
5448
<div className="min-h-screen text-text font-body selection:bg-accent selection:text-black">
5549
<div className="scanlines"></div>
5650
<div className="grain"></div>
5751

5852
{/* Header */}
59-
{!selectedMovieId && !selectedPersonId && (
53+
{!selectedMovieId && !selectedPersonId && !showProfile && (
6054
<header className="sticky top-0 z-50 py-4 backdrop-blur-md border-b border-white/5 bg-black/50">
6155
<div className="container mx-auto px-4 flex flex-col md:flex-row items-center justify-between gap-4">
6256
<div
@@ -67,22 +61,30 @@ function App() {
6761
<span className='tracking-widest'>CINEHUB</span>
6862
</div>
6963

70-
<form onSubmit={handleSearch} className="relative w-full md:w-96 group">
71-
<input
72-
type="text"
73-
value={query}
74-
onChange={(e) => setQuery(e.target.value)}
75-
placeholder="SEARCH_DATABASE..."
76-
className="w-full bg-black/40 border border-white/10 rounded-none px-4 py-2 pl-10 font-mono text-sm text-cyan focus:outline-none focus:border-cyan focus:shadow-[0_0_15px_rgba(0,255,225,0.2)] transition-all"
77-
/>
78-
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-muted group-focus-within:text-cyan w-4 h-4" />
79-
</form>
64+
<div className="flex items-center gap-4 w-full md:w-auto">
65+
<SearchAutocomplete onSelectMovie={handleMovieSelect} />
66+
67+
<button
68+
onClick={() => setShowProfile(true)}
69+
className="px-4 py-2 border border-white/10 text-muted hover:border-cyan hover:text-cyan transition-all flex items-center gap-2 font-display text-sm tracking-wider"
70+
title="View Profile"
71+
>
72+
<User size={18} />
73+
<span className="hidden md:inline">PROFILE</span>
74+
</button>
75+
</div>
8076
</div>
8177
</header>
8278
)}
8379

80+
8481
<main className="container mx-auto px-4 py-8 relative z-10">
85-
{selectedPersonId ? (
82+
{showProfile ? (
83+
<Profile
84+
onBack={() => setShowProfile(false)}
85+
onSelectMovie={handleMovieSelect}
86+
/>
87+
) : selectedPersonId ? (
8688
<PersonDetail
8789
personId={selectedPersonId}
8890
onBack={() => setSelectedPersonId(null)}
@@ -102,7 +104,7 @@ function App() {
102104
<div className="flex items-center gap-2 mb-8">
103105
<div className="h-1 w-1 bg-accent rounded-full animate-ping"></div>
104106
<h2 className="font-display text-2xl text-muted tracking-wider">
105-
{query ? `RESULTS_FOR: "${query.toUpperCase()}"` : 'TRENDING_DATA_STREAM'}
107+
TRENDING_DATA_STREAM
106108
</h2>
107109
</div>
108110

‎src/components/GenrePieChart.jsx‎

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import React from 'react';
2+
import { Chart as ChartJS, ArcElement, Tooltip, Legend } from 'chart.js';
3+
import { Pie } from 'react-chartjs-2';
4+
5+
ChartJS.register(ArcElement, Tooltip, Legend);
6+
7+
const GenrePieChart = ({ genreStats }) => {
8+
if (!genreStats || Object.keys(genreStats).length === 0) {
9+
return (
10+
<div className="glass-panel p-8 text-center">
11+
<p className="text-muted font-mono">NO_GENRE_DATA_AVAILABLE</p>
12+
<p className="text-sm text-muted/60 mt-2">Watch some movies to see your genre preferences!</p>
13+
</div>
14+
);
15+
}
16+
17+
const genres = Object.keys(genreStats);
18+
const counts = Object.values(genreStats);
19+
20+
// Retro neon color palette
21+
const colors = [
22+
'rgba(255, 69, 0, 0.8)', // accent (orange-red)
23+
'rgba(255, 183, 0, 0.8)', // cyan (gold)
24+
'rgba(0, 255, 255, 0.8)', // cyan
25+
'rgba(255, 0, 255, 0.8)', // magenta
26+
'rgba(0, 255, 0, 0.8)', // green
27+
'rgba(255, 105, 180, 0.8)', // hot pink
28+
'rgba(138, 43, 226, 0.8)', // blue violet
29+
'rgba(255, 215, 0, 0.8)', // gold
30+
'rgba(0, 191, 255, 0.8)', // deep sky blue
31+
'rgba(255, 20, 147, 0.8)', // deep pink
32+
];
33+
34+
const borderColors = colors.map(color => color.replace('0.8', '1'));
35+
36+
const data = {
37+
labels: genres,
38+
datasets: [
39+
{
40+
label: 'Movies Watched',
41+
data: counts,
42+
backgroundColor: colors.slice(0, genres.length),
43+
borderColor: borderColors.slice(0, genres.length),
44+
borderWidth: 2,
45+
},
46+
],
47+
};
48+
49+
const options = {
50+
responsive: true,
51+
maintainAspectRatio: true,
52+
plugins: {
53+
legend: {
54+
position: 'right',
55+
labels: {
56+
color: '#eaeaea',
57+
font: {
58+
family: 'monospace',
59+
size: 12,
60+
},
61+
padding: 15,
62+
generateLabels: (chart) => {
63+
const data = chart.data;
64+
if (data.labels.length && data.datasets.length) {
65+
const total = data.datasets[0].data.reduce((a, b) => a + b, 0);
66+
return data.labels.map((label, i) => {
67+
const value = data.datasets[0].data[i];
68+
const percentage = ((value / total) * 100).toFixed(1);
69+
return {
70+
text: `${label}: ${value} (${percentage}%)`,
71+
fillStyle: data.datasets[0].backgroundColor[i],
72+
strokeStyle: data.datasets[0].borderColor[i],
73+
lineWidth: 2,
74+
hidden: false,
75+
index: i,
76+
};
77+
});
78+
}
79+
return [];
80+
},
81+
},
82+
},
83+
tooltip: {
84+
backgroundColor: 'rgba(0, 0, 0, 0.9)',
85+
titleColor: '#ff4500',
86+
bodyColor: '#eaeaea',
87+
borderColor: '#ff4500',
88+
borderWidth: 1,
89+
padding: 12,
90+
displayColors: true,
91+
callbacks: {
92+
label: function (context) {
93+
const label = context.label || '';
94+
const value = context.parsed || 0;
95+
const total = context.dataset.data.reduce((a, b) => a + b, 0);
96+
const percentage = ((value / total) * 100).toFixed(1);
97+
return `${label}: ${value} movies (${percentage}%)`;
98+
}
99+
}
100+
},
101+
},
102+
};
103+
104+
return (
105+
<div className="glass-panel p-6">
106+
<h3 className="font-display text-2xl text-accent mb-6 tracking-widest text-center">
107+
GENRE_DISTRIBUTION
108+
</h3>
109+
<div className="max-w-2xl mx-auto">
110+
<Pie data={data} options={options} />
111+
</div>
112+
</div>
113+
);
114+
};
115+
116+
export default GenrePieChart;

‎src/components/MovieCard.jsx‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,17 @@
11
import React from 'react';
22
import { getImageUrl } from '../services/tmdb';
3+
import { Bookmark } from 'lucide-react';
4+
import { useMovieContext } from '../context/MovieContext';
35

46
const MovieCard = ({ movie, onClick }) => {
7+
const { toggleWatchlist, isInWatchlist } = useMovieContext();
8+
const inWatchlist = isInWatchlist(movie.id);
9+
10+
const handleWatchlistClick = (e) => {
11+
e.stopPropagation();
12+
toggleWatchlist(movie);
13+
};
14+
515
return (
616
<div
717
onClick={() => onClick(movie)}
@@ -16,6 +26,18 @@ const MovieCard = ({ movie, onClick }) => {
1626
<div className="absolute inset-0 bg-gradient-to-t from-black/80 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-end p-4">
1727
<span className="text-accent font-display text-xl tracking-widest">VIEW DATA</span>
1828
</div>
29+
30+
{/* Watchlist Button */}
31+
<button
32+
onClick={handleWatchlistClick}
33+
className={`absolute top-2 right-2 p-2 backdrop-blur-md border transition-all duration-300 ${inWatchlist
34+
? 'bg-cyan/20 border-cyan text-cyan'
35+
: 'bg-black/40 border-white/20 text-white/60 hover:border-cyan hover:text-cyan'
36+
}`}
37+
title={inWatchlist ? 'Remove from watchlist' : 'Add to watchlist'}
38+
>
39+
<Bookmark size={18} className={inWatchlist ? 'fill-current' : ''} />
40+
</button>
1941
</div>
2042

2143
<div className="p-4">

0 commit comments

Comments
 (0)