Skip to content

Commit 0ba0ea5

Browse files
committed
feat: Added year and semester fields in frontend (TODO Backend), fixed course grid
1 parent ad04e19 commit 0ba0ea5

File tree

10 files changed

+135
-58
lines changed

10 files changed

+135
-58
lines changed

backend/.sqlx/query-da74627a942531dc4ca3a41cfdce3a93757a77309dacba2901851269b4375789.json renamed to backend/.sqlx/query-a52a088f49659fd7129d0cef2fd2b7f981f5641a15933072432cbd1d5069b84f.json

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

backend/src/api/handlers/auth.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,7 @@ pub async fn google_auth_callback(
102102

103103
let token_data =
104104
decode::<GoogleClaims>(&payload.token, decoding_key, &validation).map_err(|e| {
105-
tracing::error!("Token validation error: {}", e);
106-
crate::api::errors::AuthError::InvalidToken("Token validation failed".to_string())
105+
crate::api::errors::AuthError::InvalidToken(format!("Token validation failed: {}", e))
107106
})?;
108107

109108
let claims = token_data.claims;

backend/src/db/handlers/notes.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ pub async fn search_notes_by_query(
155155
LEFT JOIN
156156
votes user_vote ON n.id = user_vote.note_id AND user_vote.user_id = $2
157157
WHERE course_name ILIKE $1 OR course_code ILIKE $1
158+
ORDER BY COALESCE(upvote_counts.count, 0) DESC, n.created_at DESC
158159
"#,
159160
search_term,
160161
current_user_id.as_ref()

frontend/package-lock.json

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

frontend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,6 @@
3333
"tailwindcss": "^3.4.4",
3434
"typescript": "~5.8.3",
3535
"typescript-eslint": "^8.39.1",
36-
"vite": "^7.1.2"
36+
"vite": "^7.1.10"
3737
}
3838
}

frontend/src/App.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,13 @@ const AppContent: React.FC = () => {
6161
<AuthProvider onSignIn={loadNotes}>
6262
<Header onNoteUploaded={handleNoteUploaded} />
6363

64-
<main className="flex-1 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
65-
<div className="mb-8">
64+
<main className="flex-1 w-full px-6 sm:px-8 lg:px-12 xl:px-16 py-8">
65+
<div className="mb-8 max-w-3xl mx-auto">
6666
<SearchBar searchQuery={searchQuery} onSearchChange={handleSearchChange} />
6767
</div>
6868

6969
{error && (
70-
<div className="mb-6 p-4 bg-red-900 border border-red-700 rounded-lg text-red-300">
70+
<div className="mb-6 p-4 bg-red-900 border border-red-700 rounded-lg text-red-300 max-w-3xl mx-auto">
7171
{error}
7272
</div>
7373
)}
@@ -91,4 +91,4 @@ const App: React.FC = () => {
9191
return <AppContent />;
9292
};
9393

94-
export default App;
94+
export default App;

frontend/src/components/CourseCard.tsx

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
// components/CourseCard.tsx
2-
import React, { useState } from 'react';
3-
import { Download, ThumbsUp, ThumbsDown, Edit, FileText } from 'lucide-react';
4-
import { useSignInFlow } from '../hooks/useSignInFlow';
2+
import React, {useState} from 'react';
3+
import {Download, Edit, FileText, ThumbsUp} from 'lucide-react';
4+
import {useSignInFlow} from '../hooks/useSignInFlow';
55
import SignInModal from './SignInModal';
6-
import type { CourseCardProps, VoteType } from '../types';
7-
import { notesApi } from '../api/notesApi';
8-
import { useAuth } from '../contexts/AuthContext';
6+
import type {CourseCardProps, VoteType} from '../types';
7+
import {notesApi} from '../api/notesApi';
8+
import {useAuth} from '../contexts/AuthContext';
99

1010
const CourseCard: React.FC<CourseCardProps> = ({ note }) => {
1111
const { isAuthenticated, user } = useAuth();
@@ -27,7 +27,7 @@ const CourseCard: React.FC<CourseCardProps> = ({ note }) => {
2727
convertVoteToString(note.user_vote)
2828
);
2929
const [upvoteCount, setUpvoteCount] = useState(note.upvotes || 0);
30-
const [downvoteCount, setDownvoteCount] = useState(note.downvotes || 0);
30+
// const [downvoteCount, setDownvoteCount] = useState(note.downvotes || 0);
3131
const [downloadCount, setDownloadCount] = useState(note.downloads || 0);
3232
const [isVoting, setIsVoting] = useState(false);
3333
const [imageError, setImageError] = useState(false);
@@ -61,37 +61,36 @@ const CourseCard: React.FC<CourseCardProps> = ({ note }) => {
6161
return { visibleTags, remainingCount };
6262
};
6363

64-
const handleVote = async (voteType: 'upvote' | 'downvote') => {
64+
const handleVote = async (voteType: 'upvote') => {
6565
if (isVoting) return;
6666

6767
requireAuth(voteType, async () => {
6868
setIsVoting(true);
6969
const previousVote = userVote;
70-
const previousUpvoteCount = upvoteCount;
71-
const previousDownvoteCount = downvoteCount;
70+
// const previousDownvoteCount = downvoteCount;
7271

7372
try {
74-
let actualVoteType: VoteType = userVote === voteType ? 'remove' : voteType;
73+
const actualVoteType: VoteType = userVote === voteType ? 'remove' : voteType;
7574

7675
if (actualVoteType === 'remove') {
7776
setUserVote(null);
7877
if (previousVote === 'upvote') setUpvoteCount(prev => Math.max(0, prev - 1));
79-
else if (previousVote === 'downvote') setDownvoteCount(prev => Math.max(0, prev - 1));
78+
// else if (previousVote === 'downvote') setDownvoteCount(prev => Math.max(0, prev - 1));
8079
} else if (actualVoteType === 'upvote') {
8180
setUserVote('upvote');
8281
setUpvoteCount(prev => prev + 1);
83-
if (previousVote === 'downvote') setDownvoteCount(prev => Math.max(0, prev - 1));
82+
// if (previousVote === 'downvote') setDownvoteCount(prev => Math.max(0, prev - 1));
8483
} else if (actualVoteType === 'downvote') {
8584
setUserVote('downvote');
86-
setDownvoteCount(prev => prev + 1);
85+
// setDownvoteCount(prev => prev + 1);
8786
if (previousVote === 'upvote') setUpvoteCount(prev => Math.max(0, prev - 1));
8887
}
8988

9089
await notesApi.voteOnNote(note.id, actualVoteType);
9190
} catch (error) {
9291
setUserVote(previousVote);
93-
setUpvoteCount(previousUpvoteCount);
94-
setDownvoteCount(previousDownvoteCount);
92+
setUpvoteCount(upvoteCount);
93+
// setDownvoteCount(previousDownvoteCount);
9594
console.error('Vote failed:', error);
9695
alert('Failed to vote. Please try again.');
9796
} finally {
@@ -182,7 +181,7 @@ const CourseCard: React.FC<CourseCardProps> = ({ note }) => {
182181
<span className="text-gray-600 text-xs italic">No tags provided</span>
183182
)}
184183
</div>
185-
<div className="mb-4 h-5">
184+
<div className="mb-3 h-5">
186185
{note.professor_names && note.professor_names.length > 0 ? (
187186
<p className="text-sm text-text-muted flex items-center">
188187
<span className="text-gray-600 mr-1">Prof:</span>
@@ -196,6 +195,11 @@ const CourseCard: React.FC<CourseCardProps> = ({ note }) => {
196195
</p>
197196
)}
198197
</div>
198+
<div className="mb-4">
199+
<p className="text-xs text-gray-500">
200+
By: <span className="text-text-muted">{note.uploader_user.full_name}</span>
201+
</p>
202+
</div>
199203
<div className="flex items-center justify-between pt-3 border-t border-border">
200204
<div className="flex items-center space-x-4">
201205
<button
@@ -245,4 +249,4 @@ const CourseCard: React.FC<CourseCardProps> = ({ note }) => {
245249
);
246250
};
247251

248-
export default CourseCard;
252+
export default CourseCard;

frontend/src/components/CourseGrid.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,12 @@ const CourseGrid: React.FC<CourseGridProps> = ({ notes }) => {
1414

1515
return (
1616
<div className="w-full max-w-full">
17-
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 w-full">
17+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-6 w-full">
1818
{notes.map((note) => (
1919
<CourseCard key={note.id} note={note} />
2020
))}
2121
</div>
2222
</div>
2323
);
2424
};
25-
2625
export default CourseGrid;

frontend/src/components/UploadModal.tsx

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ interface UploadModalProps {
1313
interface FormData {
1414
courseName: string;
1515
courseCode: string;
16+
year: string;
17+
semester: 'Autumn' | 'Spring';
1618
description: string;
1719
professorNames: string;
1820
tags: string;
@@ -23,6 +25,8 @@ const UploadModal: React.FC<UploadModalProps> = ({ isOpen, onClose, onSuccess })
2325
const [formData, setFormData] = useState<FormData>({
2426
courseName: '',
2527
courseCode: '',
28+
year: new Date().getFullYear().toString(),
29+
semester: 'Autumn',
2630
description: '',
2731
professorNames: '',
2832
tags: '',
@@ -92,6 +96,8 @@ const UploadModal: React.FC<UploadModalProps> = ({ isOpen, onClose, onSuccess })
9296
const uploadFormData = new FormData();
9397
uploadFormData.append('course_name', formData.courseName.trim());
9498
uploadFormData.append('course_code', formData.courseCode.trim());
99+
uploadFormData.append('year', formData.year);
100+
uploadFormData.append('semester', formData.semester);
95101
if (formData.description.trim()) uploadFormData.append('description', formData.description.trim());
96102
if (formData.professorNames.trim()) uploadFormData.append('professor_names', formData.professorNames.trim());
97103
if (formData.tags.trim()) uploadFormData.append('tags', formData.tags.trim());
@@ -100,7 +106,16 @@ const UploadModal: React.FC<UploadModalProps> = ({ isOpen, onClose, onSuccess })
100106
const newNote = await notesApi.uploadNote(uploadFormData);
101107
onSuccess(newNote);
102108
onClose();
103-
setFormData({ courseName: '', courseCode: '', description: '', professorNames: '', tags: '', file: null });
109+
setFormData({
110+
courseName: '',
111+
courseCode: '',
112+
year: new Date().getFullYear().toString(),
113+
semester: 'Autumn',
114+
description: '',
115+
professorNames: '',
116+
tags: '',
117+
file: null
118+
});
104119
} catch (err) {
105120
setError(err instanceof Error ? err.message : 'Upload failed. Please try again.');
106121
} finally {
@@ -109,7 +124,16 @@ const UploadModal: React.FC<UploadModalProps> = ({ isOpen, onClose, onSuccess })
109124
};
110125

111126
const resetAndClose = () => {
112-
setFormData({ courseName: '', courseCode: '', description: '', professorNames: '', tags: '', file: null });
127+
setFormData({
128+
courseName: '',
129+
courseCode: '',
130+
year: new Date().getFullYear().toString(),
131+
semester: 'Autumn',
132+
description: '',
133+
professorNames: '',
134+
tags: '',
135+
file: null
136+
});
113137
setError(null);
114138
onClose();
115139
};
@@ -158,6 +182,50 @@ const UploadModal: React.FC<UploadModalProps> = ({ isOpen, onClose, onSuccess })
158182
/>
159183
</div>
160184

185+
<div className="grid grid-cols-2 gap-4">
186+
<div>
187+
<label className="block text-sm font-medium text-text-muted mb-2">Year *</label>
188+
<select
189+
value={formData.year}
190+
onChange={(e) => handleInputChange('year', e.target.value)}
191+
className="w-full px-3 py-2 border border-border bg-surface rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent"
192+
required
193+
>
194+
{Array.from({ length: 50 }, (_, i) => new Date().getFullYear() - i).map(year => (
195+
<option key={year} value={year}>{year}</option>
196+
))}
197+
</select>
198+
</div>
199+
200+
<div>
201+
<label className="block text-sm font-medium text-text-muted mb-2">Semester *</label>
202+
<div className="flex space-x-4 mt-2">
203+
<label className="flex items-center space-x-2 cursor-pointer">
204+
<input
205+
type="radio"
206+
name="semester"
207+
value="Autumn"
208+
checked={formData.semester === 'Autumn'}
209+
onChange={(e) => handleInputChange('semester', e.target.value)}
210+
className="w-4 h-4 text-primary focus:ring-2 focus:ring-primary"
211+
/>
212+
<span className="text-text-base">Autumn</span>
213+
</label>
214+
<label className="flex items-center space-x-2 cursor-pointer">
215+
<input
216+
type="radio"
217+
name="semester"
218+
value="Spring"
219+
checked={formData.semester === 'Spring'}
220+
onChange={(e) => handleInputChange('semester', e.target.value)}
221+
className="w-4 h-4 text-primary focus:ring-2 focus:ring-primary"
222+
/>
223+
<span className="text-text-base">Spring</span>
224+
</label>
225+
</div>
226+
</div>
227+
</div>
228+
161229
<div>
162230
<label className="block text-sm font-medium text-text-muted mb-2">Description</label>
163231
<textarea
@@ -249,4 +317,4 @@ const UploadModal: React.FC<UploadModalProps> = ({ isOpen, onClose, onSuccess })
249317
);
250318
};
251319

252-
export default UploadModal;
320+
export default UploadModal;

0 commit comments

Comments
 (0)