Skip to content

Commit 55b7c65

Browse files
feat: add a dashboard page to the front end to display aggregations (#18)
1 parent 19ea29b commit 55b7c65

File tree

9 files changed

+642
-27
lines changed

9 files changed

+642
-27
lines changed
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
/* Aggregations styles */
2+
.container {
3+
max-width: 1200px;
4+
margin: 0 auto;
5+
padding: 2rem;
6+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
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+
}
82+
83+
.rank {
84+
font-weight: 700;
85+
}
86+
87+
.directorName {
88+
font-weight: 600;
89+
color: #2c3e50;
90+
}
91+
92+
.commentsContainer {
93+
max-width: 300px;
94+
}
95+
96+
.comment {
97+
margin-bottom: 0.75rem;
98+
padding: 0.5rem;
99+
background: #f8f9fa;
100+
border-radius: 4px;
101+
border-left: 3px solid #3498db;
102+
}
103+
104+
.comment:last-child {
105+
margin-bottom: 0;
106+
}
107+
108+
.commentText {
109+
font-size: 0.85rem;
110+
color: #2c3e50;
111+
margin-bottom: 0.25rem;
112+
line-height: 1.4;
113+
}
114+
115+
.commentMeta {
116+
font-size: 0.75rem;
117+
color: #7f8c8d;
118+
font-style: italic;
119+
}
120+
121+
.error {
122+
color: #e74c3c;
123+
background: #ffeaa7;
124+
padding: 1rem;
125+
margin: 1rem 2rem;
126+
border-radius: 4px;
127+
font-weight: 500;
128+
}
129+
130+
/* Responsive design */
131+
@media (max-width: 768px) {
132+
.container {
133+
padding: 1rem;
134+
}
135+
136+
.title {
137+
font-size: 2rem;
138+
}
139+
140+
.sectionTitle {
141+
padding: 1rem;
142+
font-size: 1.25rem;
143+
}
144+
145+
.table {
146+
font-size: 0.8rem;
147+
}
148+
149+
.table th,
150+
.table td {
151+
padding: 0.5rem;
152+
}
153+
154+
.commentsContainer {
155+
max-width: 200px;
156+
}
157+
158+
.movieTitle {
159+
max-width: 150px;
160+
}
161+
}
162+
163+
@media (max-width: 480px) {
164+
.table th,
165+
.table td {
166+
padding: 0.25rem;
167+
}
168+
169+
.commentsContainer {
170+
max-width: 150px;
171+
}
172+
}

client/app/aggregations/page.tsx

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

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)