Skip to content

Commit a5bdec2

Browse files
feat: adds aggregation page to the front end
1 parent 259aaae commit a5bdec2

8 files changed

Lines changed: 640 additions & 28 deletions

File tree

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/* Aggregations styles */
2+
.container {
3+
max-width: 1200px;
4+
margin: 0 auto;
5+
padding: 2rem;
6+
font-family: var(--font-system);
7+
}
8+
9+
.title {
10+
font-size: 2.5rem;
11+
font-weight: 700;
12+
color: #1a1a1a;
13+
margin-bottom: 0.5rem;
14+
text-align: center;
15+
}
16+
17+
.subtitle {
18+
font-size: 1.1rem;
19+
color: #666;
20+
text-align: center;
21+
margin-bottom: 3rem;
22+
}
23+
24+
.section {
25+
margin-bottom: 3rem;
26+
background: #fff;
27+
border-radius: 8px;
28+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
29+
overflow: hidden;
30+
}
31+
32+
.sectionTitle {
33+
font-size: 1.5rem;
34+
font-weight: 600;
35+
color: #2c3e50;
36+
margin: 0;
37+
padding: 1.5rem 2rem;
38+
background: #f8f9fa;
39+
border-bottom: 1px solid #e9ecef;
40+
}
41+
42+
.tableContainer {
43+
overflow-x: auto;
44+
padding: 0;
45+
}
46+
47+
.table {
48+
width: 100%;
49+
border-collapse: collapse;
50+
font-size: 0.9rem;
51+
}
52+
53+
.table th {
54+
background: #34495e;
55+
color: white;
56+
font-weight: 600;
57+
padding: 1rem;
58+
text-align: left;
59+
white-space: nowrap;
60+
}
61+
62+
.table td {
63+
padding: 1rem;
64+
border-bottom: 1px solid #e9ecef;
65+
vertical-align: top;
66+
}
67+
68+
.table tr:hover {
69+
background: #f8f9fa;
70+
}
71+
72+
.movieTitle {
73+
font-weight: 600;
74+
color: #2c3e50;
75+
max-width: 200px;
76+
word-wrap: break-word;
77+
}
78+
79+
.year {
80+
font-weight: 600;
81+
color: #3498db;
82+
}
83+
84+
.rank {
85+
font-weight: 700;
86+
color: #e74c3c;
87+
}
88+
89+
.directorName {
90+
font-weight: 600;
91+
color: #2c3e50;
92+
}
93+
94+
.commentsContainer {
95+
max-width: 300px;
96+
}
97+
98+
.comment {
99+
margin-bottom: 0.75rem;
100+
padding: 0.5rem;
101+
background: #f8f9fa;
102+
border-radius: 4px;
103+
border-left: 3px solid #3498db;
104+
}
105+
106+
.comment:last-child {
107+
margin-bottom: 0;
108+
}
109+
110+
.commentText {
111+
font-size: 0.85rem;
112+
color: #2c3e50;
113+
margin-bottom: 0.25rem;
114+
line-height: 1.4;
115+
}
116+
117+
.commentMeta {
118+
font-size: 0.75rem;
119+
color: #7f8c8d;
120+
font-style: italic;
121+
}
122+
123+
.error {
124+
color: #e74c3c;
125+
background: #ffeaa7;
126+
padding: 1rem;
127+
margin: 1rem 2rem;
128+
border-radius: 4px;
129+
font-weight: 500;
130+
}
131+
132+
/* Responsive design */
133+
@media (max-width: 768px) {
134+
.container {
135+
padding: 1rem;
136+
}
137+
138+
.title {
139+
font-size: 2rem;
140+
}
141+
142+
.sectionTitle {
143+
padding: 1rem;
144+
font-size: 1.25rem;
145+
}
146+
147+
.table {
148+
font-size: 0.8rem;
149+
}
150+
151+
.table th,
152+
.table td {
153+
padding: 0.5rem;
154+
}
155+
156+
.commentsContainer {
157+
max-width: 200px;
158+
}
159+
160+
.movieTitle {
161+
max-width: 150px;
162+
}
163+
}
164+
165+
@media (max-width: 480px) {
166+
.table th,
167+
.table td {
168+
padding: 0.25rem;
169+
}
170+
171+
.commentsContainer {
172+
max-width: 150px;
173+
}
174+
}

client/app/aggregations/page.tsx

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import React from 'react';
2+
import { fetchMoviesWithComments, fetchMoviesByYear, fetchDirectorStats } from '../lib/api';
3+
import styles from './aggregations.module.css';
4+
5+
// Type definitions for better type safety
6+
interface MovieWithComments {
7+
_id: string;
8+
title: string;
9+
year: number;
10+
genres: string[];
11+
imdbRating: number;
12+
totalComments: number;
13+
recentComments: Array<{
14+
userName: string;
15+
userEmail: string;
16+
text: string;
17+
date: string;
18+
}>;
19+
}
20+
21+
interface YearlyStats {
22+
year: number;
23+
movieCount: number;
24+
averageRating: number;
25+
highestRating: number;
26+
lowestRating: number;
27+
totalVotes: number;
28+
}
29+
30+
interface DirectorStats {
31+
director: string;
32+
movieCount: number;
33+
averageRating: number;
34+
}
35+
36+
export default async function AggregationsPage() {
37+
38+
// Fetch all aggregation data with error handling
39+
const [commentsResult, yearResult, directorsResult] = await Promise.allSettled([
40+
fetchMoviesWithComments(5),
41+
fetchMoviesByYear(),
42+
fetchDirectorStats(15)
43+
]);
44+
45+
// Process results with fallbacks
46+
const commentsData = commentsResult.status === 'fulfilled' ? commentsResult.value : { success: false, error: 'Failed to fetch comments data' };
47+
const yearData = yearResult.status === 'fulfilled' ? yearResult.value : { success: false, error: 'Failed to fetch year data' };
48+
const directorsData = directorsResult.status === 'fulfilled' ? directorsResult.value : { success: false, error: 'Failed to fetch directors data' };
49+
50+
console.log('Aggregations SSR: Data fetch completed', {
51+
comments: commentsData.success,
52+
year: yearData.success,
53+
directors: directorsData.success
54+
});
55+
56+
return (
57+
<div className={styles.container}>
58+
<h1 className={styles.title}>Movie Analytics Aggregations</h1>
59+
<p className={styles.subtitle}>
60+
Explore movie data through various aggregations and insights
61+
</p>
62+
63+
{/* Movies with Recent Comments Section */}
64+
<section className={styles.section}>
65+
<h2 className={styles.sectionTitle}>Movies with Recent Comments</h2>
66+
{commentsData.success && commentsData.data ? (
67+
<div className={styles.tableContainer}>
68+
<table className={styles.table}>
69+
<thead>
70+
<tr>
71+
<th>Movie Title</th>
72+
<th>Year</th>
73+
<th>Rating</th>
74+
<th>Total Comments</th>
75+
<th>Recent Comments</th>
76+
</tr>
77+
</thead>
78+
<tbody>
79+
{(commentsData.data as MovieWithComments[]).map((movie) => (
80+
<tr key={movie._id}>
81+
<td className={styles.movieTitle}>{movie.title}</td>
82+
<td>{movie.year}</td>
83+
<td>{movie.imdbRating ? movie.imdbRating.toFixed(1) : 'N/A'}</td>
84+
<td>{movie.totalComments}</td>
85+
<td>
86+
<div className={styles.commentsContainer}>
87+
{movie.recentComments?.slice(0, 2).map((comment, index) => (
88+
<div key={index} className={styles.comment}>
89+
<div className={styles.commentText}>
90+
&ldquo;{(comment.text || 'No text').slice(0, 80)}{comment.text?.length > 80 ? '...' : ''}&rdquo;
91+
</div>
92+
<div className={styles.commentMeta}>
93+
by {comment.userName} on {new Date(comment.date).toLocaleDateString()}
94+
</div>
95+
</div>
96+
)) || <div>No recent comments</div>}
97+
</div>
98+
</td>
99+
</tr>
100+
))}
101+
</tbody>
102+
</table>
103+
</div>
104+
) : (
105+
<div className={styles.error}>
106+
Failed to load movies with comments: {commentsData.error || 'Unknown error'}
107+
</div>
108+
)}
109+
</section>
110+
111+
{/* Movies by Year Section */}
112+
<section className={styles.section}>
113+
<h2 className={styles.sectionTitle}>Movies by Year Statistics</h2>
114+
{yearData.success && yearData.data ? (
115+
<div className={styles.tableContainer}>
116+
<table className={styles.table}>
117+
<thead>
118+
<tr>
119+
<th>Year</th>
120+
<th>Movie Count</th>
121+
<th>Average Rating</th>
122+
<th>Highest Rating</th>
123+
<th>Lowest Rating</th>
124+
<th>Total Votes</th>
125+
</tr>
126+
</thead>
127+
<tbody>
128+
{(yearData.data as YearlyStats[]).slice(0, 20).map((yearStats) => (
129+
<tr key={yearStats.year}>
130+
<td className={styles.year}>{yearStats.year}</td>
131+
<td>{yearStats.movieCount}</td>
132+
<td>{yearStats.averageRating ? yearStats.averageRating.toFixed(2) : 'N/A'}</td>
133+
<td>{yearStats.highestRating ? yearStats.highestRating.toFixed(1) : 'N/A'}</td>
134+
<td>{yearStats.lowestRating ? yearStats.lowestRating.toFixed(1) : 'N/A'}</td>
135+
<td>{yearStats.totalVotes?.toLocaleString() || 'N/A'}</td>
136+
</tr>
137+
))}
138+
</tbody>
139+
</table>
140+
</div>
141+
) : (
142+
<div className={styles.error}>
143+
Failed to load yearly statistics: {yearData.error || 'Unknown error'}
144+
</div>
145+
)}
146+
</section>
147+
148+
{/* Directors with Most Movies Section */}
149+
<section className={styles.section}>
150+
<h2 className={styles.sectionTitle}>Directors with Most Movies</h2>
151+
{directorsData.success && directorsData.data ? (
152+
<div className={styles.tableContainer}>
153+
<table className={styles.table}>
154+
<thead>
155+
<tr>
156+
<th>Rank</th>
157+
<th>Director</th>
158+
<th>Movie Count</th>
159+
<th>Average Rating</th>
160+
</tr>
161+
</thead>
162+
<tbody>
163+
{(directorsData.data as DirectorStats[]).map((director, index) => (
164+
<tr key={director.director}>
165+
<td className={styles.rank}>#{index + 1}</td>
166+
<td className={styles.directorName}>{director.director}</td>
167+
<td>{director.movieCount}</td>
168+
<td>{director.averageRating ? director.averageRating.toFixed(2) : 'N/A'}</td>
169+
</tr>
170+
))}
171+
</tbody>
172+
</table>
173+
</div>
174+
) : (
175+
<div className={styles.error}>
176+
Failed to load director statistics: {directorsData.error || 'Unknown error'}
177+
</div>
178+
)}
179+
</section>
180+
</div>
181+
);
182+
}

client/app/layout.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ export default function RootLayout({
3838
>
3939
Movies
4040
</Link>
41+
<Link
42+
href={ROUTES.aggregations}
43+
className={styles.navLink}
44+
>
45+
Aggregations
46+
</Link>
4147
</div>
4248
</div>
4349
</nav>

0 commit comments

Comments
 (0)