Merge pull request #4 from Amudieshwar-AG/feature/notes-pyqs
feat(notes-pyqs): implement dynamic notes scanning, subject dropdown,…
This commit is contained in:
@@ -16,6 +16,8 @@ Welcome to the team! This guide explains how to get set up, which Git branch you
|
|||||||
Find your assigned module below, and check out your branch:
|
Find your assigned module below, and check out your branch:
|
||||||
```bash
|
```bash
|
||||||
|
|
||||||
|
# Checkout your module's branch
|
||||||
|
git checkout feature/<your-assigned-branch-name>
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Install frontend dependencies**:
|
3. **Install frontend dependencies**:
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
<url />
|
<url />
|
||||||
</scm>
|
</scm>
|
||||||
<properties>
|
<properties>
|
||||||
<java.version>21</java.version>
|
<java.version>17</java.version>
|
||||||
</properties>
|
</properties>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import java.util.Arrays;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import com.rit.portal.controller.NotePyqController;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class DataInitializer implements CommandLineRunner {
|
public class DataInitializer implements CommandLineRunner {
|
||||||
|
|
||||||
@@ -26,100 +28,19 @@ public class DataInitializer implements CommandLineRunner {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private BusRouteRepository busRouteRepository;
|
private BusRouteRepository busRouteRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private NotePyqController notePyqController;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run(String... args) throws Exception {
|
public void run(String... args) throws Exception {
|
||||||
if (noteRepository.count() == 0) {
|
// Clear all previous mock data from the database
|
||||||
noteRepository.saveAll(Arrays.asList(
|
noteRepository.deleteAll();
|
||||||
NotePyq.builder()
|
// Run initial scan to populate database with whatever is currently in /uploads
|
||||||
.title("Engineering Mathematics – Unit 1-5")
|
try {
|
||||||
.subject("Mathematics")
|
notePyqController.syncUploads();
|
||||||
.department("Computer Science & Engineering")
|
System.out.println("🌱 Initialized Note/PYQ database from local uploads folder!");
|
||||||
.semester(1)
|
} catch (Exception e) {
|
||||||
.fileType("notes")
|
System.err.println("⚠️ Failed to run initial uploads sync: " + e.getMessage());
|
||||||
.downloadUrl("#")
|
|
||||||
.fileSize("4.2 MB")
|
|
||||||
.downloadsCount(348)
|
|
||||||
.uploadedAt(LocalDateTime.now())
|
|
||||||
.build(),
|
|
||||||
NotePyq.builder()
|
|
||||||
.title("Physics PYQ 2020-2024")
|
|
||||||
.subject("Physics")
|
|
||||||
.department("Computer Science & Engineering")
|
|
||||||
.semester(1)
|
|
||||||
.fileType("pyq")
|
|
||||||
.downloadUrl("#")
|
|
||||||
.fileSize("8.1 MB")
|
|
||||||
.downloadsCount(512)
|
|
||||||
.uploadedAt(LocalDateTime.now())
|
|
||||||
.build(),
|
|
||||||
NotePyq.builder()
|
|
||||||
.title("C Programming Complete Notes")
|
|
||||||
.subject("Programming")
|
|
||||||
.department("Computer Science & Engineering")
|
|
||||||
.semester(1)
|
|
||||||
.fileType("notes")
|
|
||||||
.downloadUrl("#")
|
|
||||||
.fileSize("6.3 MB")
|
|
||||||
.downloadsCount(734)
|
|
||||||
.uploadedAt(LocalDateTime.now())
|
|
||||||
.build(),
|
|
||||||
NotePyq.builder()
|
|
||||||
.title("Data Structures PYQ 2019-2024")
|
|
||||||
.subject("Data Structures")
|
|
||||||
.department("Computer Science & Engineering")
|
|
||||||
.semester(3)
|
|
||||||
.fileType("pyq")
|
|
||||||
.downloadUrl("#")
|
|
||||||
.fileSize("10.2 MB")
|
|
||||||
.downloadsCount(621)
|
|
||||||
.uploadedAt(LocalDateTime.now())
|
|
||||||
.build(),
|
|
||||||
NotePyq.builder()
|
|
||||||
.title("Circuit Theory Full Notes")
|
|
||||||
.subject("Circuit Theory")
|
|
||||||
.department("Electronics & Communication")
|
|
||||||
.semester(2)
|
|
||||||
.fileType("notes")
|
|
||||||
.downloadUrl("#")
|
|
||||||
.fileSize("5.7 MB")
|
|
||||||
.downloadsCount(289)
|
|
||||||
.uploadedAt(LocalDateTime.now())
|
|
||||||
.build(),
|
|
||||||
NotePyq.builder()
|
|
||||||
.title("Anna University Syllabus 2021")
|
|
||||||
.subject("Syllabus")
|
|
||||||
.department("All Departments")
|
|
||||||
.semester(1)
|
|
||||||
.fileType("syllabus")
|
|
||||||
.downloadUrl("#")
|
|
||||||
.fileSize("2.1 MB")
|
|
||||||
.downloadsCount(890)
|
|
||||||
.uploadedAt(LocalDateTime.now())
|
|
||||||
.build(),
|
|
||||||
NotePyq.builder()
|
|
||||||
.title("Thermodynamics Notes")
|
|
||||||
.subject("Thermodynamics")
|
|
||||||
.department("Mechanical Engineering")
|
|
||||||
.semester(3)
|
|
||||||
.fileType("notes")
|
|
||||||
.downloadUrl("#")
|
|
||||||
.fileSize("3.9 MB")
|
|
||||||
.downloadsCount(201)
|
|
||||||
.uploadedAt(LocalDateTime.now())
|
|
||||||
.build(),
|
|
||||||
NotePyq.builder()
|
|
||||||
.title("Digital Electronics PYQ")
|
|
||||||
.subject("Digital Electronics")
|
|
||||||
.department("Electronics & Communication")
|
|
||||||
.semester(4)
|
|
||||||
.fileType("pyq")
|
|
||||||
.downloadUrl("#")
|
|
||||||
.fileSize("7.3 MB")
|
|
||||||
.downloadsCount(445)
|
|
||||||
.uploadedAt(LocalDateTime.now())
|
|
||||||
.build()
|
|
||||||
));
|
|
||||||
System.out.println("🌱 Database successfully seeded with Notes & PYQs test data!");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Seed Bus Routes
|
// Seed Bus Routes
|
||||||
|
|||||||
@@ -9,10 +9,16 @@ public class WebConfig implements WebMvcConfigurer {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addCorsMappings(CorsRegistry registry) {
|
public void addCorsMappings(CorsRegistry registry) {
|
||||||
registry.addMapping("/api/**")
|
registry.addMapping("/**")
|
||||||
.allowedOriginPatterns("*")
|
.allowedOriginPatterns("*")
|
||||||
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
|
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
|
||||||
.allowedHeaders("*")
|
.allowedHeaders("*")
|
||||||
.allowCredentials(true);
|
.allowCredentials(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addResourceHandlers(org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry registry) {
|
||||||
|
registry.addResourceHandler("/uploads/**")
|
||||||
|
.addResourceLocations("file:uploads/");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ import org.springframework.http.ResponseEntity;
|
|||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/notes")
|
@RequestMapping("/api/notes")
|
||||||
public class NotePyqController {
|
public class NotePyqController {
|
||||||
@@ -14,21 +19,117 @@ public class NotePyqController {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private NotePyqRepository noteRepository;
|
private NotePyqRepository noteRepository;
|
||||||
|
|
||||||
|
// Sync uploads directory with DB
|
||||||
|
public synchronized void syncUploads() {
|
||||||
|
File uploadsDir = new File("uploads");
|
||||||
|
if (!uploadsDir.exists()) {
|
||||||
|
uploadsDir.mkdirs();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<NotePyq> currentNotes = noteRepository.findAll();
|
||||||
|
List<String> activeUrls = Collections.synchronizedList(new ArrayList<>());
|
||||||
|
|
||||||
|
File[] semDirs = uploadsDir.listFiles();
|
||||||
|
if (semDirs != null) {
|
||||||
|
for (File semDir : semDirs) {
|
||||||
|
if (semDir.isDirectory() && semDir.getName().startsWith("sem")) {
|
||||||
|
String semStr = semDir.getName().substring(3);
|
||||||
|
try {
|
||||||
|
Integer semester = Integer.parseInt(semStr);
|
||||||
|
File[] subDirs = semDir.listFiles();
|
||||||
|
if (subDirs != null) {
|
||||||
|
for (File subDir : subDirs) {
|
||||||
|
if (subDir.isDirectory()) {
|
||||||
|
String subject = subDir.getName();
|
||||||
|
File[] typeDirs = subDir.listFiles();
|
||||||
|
if (typeDirs != null) {
|
||||||
|
for (File typeDir : typeDirs) {
|
||||||
|
if (typeDir.isDirectory()) {
|
||||||
|
String fileType = typeDir.getName();
|
||||||
|
File[] files = typeDir.listFiles();
|
||||||
|
if (files != null) {
|
||||||
|
for (File file : files) {
|
||||||
|
if (file.isFile() && !file.getName().equals(".gitkeep")) {
|
||||||
|
String fileName = file.getName();
|
||||||
|
String downloadUrl = "http://localhost:8080/uploads/" +
|
||||||
|
semDir.getName() + "/" +
|
||||||
|
subDir.getName() + "/" +
|
||||||
|
typeDir.getName() + "/" +
|
||||||
|
fileName;
|
||||||
|
activeUrls.add(downloadUrl);
|
||||||
|
|
||||||
|
boolean exists = currentNotes.stream().anyMatch(n -> n.getDownloadUrl().equals(downloadUrl));
|
||||||
|
if (!exists) {
|
||||||
|
String baseName = fileName.contains(".") ? fileName.substring(0, fileName.lastIndexOf('.')) : fileName;
|
||||||
|
String title = baseName.replace("_", " ").replace("-", " ");
|
||||||
|
|
||||||
|
String department = "All Departments";
|
||||||
|
|
||||||
|
long bytes = file.length();
|
||||||
|
String fileSize = formatFileSize(bytes);
|
||||||
|
|
||||||
|
NotePyq newNote = NotePyq.builder()
|
||||||
|
.title(title)
|
||||||
|
.subject(subject)
|
||||||
|
.department(department)
|
||||||
|
.semester(semester)
|
||||||
|
.fileType(fileType)
|
||||||
|
.downloadUrl(downloadUrl)
|
||||||
|
.fileSize(fileSize)
|
||||||
|
.downloadsCount(0)
|
||||||
|
.uploadedAt(LocalDateTime.now())
|
||||||
|
.build();
|
||||||
|
noteRepository.save(newNote);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete notes from DB that no longer exist on disk
|
||||||
|
for (NotePyq note : currentNotes) {
|
||||||
|
if (!activeUrls.contains(note.getDownloadUrl())) {
|
||||||
|
noteRepository.delete(note);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatFileSize(long bytes) {
|
||||||
|
if (bytes < 1024) return bytes + " B";
|
||||||
|
int exp = (int) (Math.log(bytes) / Math.log(1024));
|
||||||
|
char pre = "KMGTPE".charAt(exp - 1);
|
||||||
|
return String.format("%.1f %sB", bytes / Math.pow(1024, exp), pre);
|
||||||
|
}
|
||||||
|
|
||||||
// Get all notes
|
// Get all notes
|
||||||
@GetMapping
|
@GetMapping
|
||||||
public List<NotePyq> getAllNotes() {
|
public List<NotePyq> getAllNotes() {
|
||||||
|
syncUploads();
|
||||||
return noteRepository.findAll();
|
return noteRepository.findAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get notes by semester
|
// Get notes by semester
|
||||||
@GetMapping("/semester/{semester}")
|
@GetMapping("/semester/{semester}")
|
||||||
public List<NotePyq> getNotesBySemester(@PathVariable Integer semester) {
|
public List<NotePyq> getNotesBySemester(@PathVariable Integer semester) {
|
||||||
|
syncUploads();
|
||||||
return noteRepository.findBySemester(semester);
|
return noteRepository.findBySemester(semester);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get notes by department
|
// Get notes by department
|
||||||
@GetMapping("/department")
|
@GetMapping("/department")
|
||||||
public List<NotePyq> getNotesByDepartment(@RequestParam String dept) {
|
public List<NotePyq> getNotesByDepartment(@RequestParam String dept) {
|
||||||
|
syncUploads();
|
||||||
return noteRepository.findByDepartment(dept);
|
return noteRepository.findByDepartment(dept);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
BIN
backend/uploads/sem1/Matrices and Calculus/syllabus/syllabus.pdf
Normal file
BIN
backend/uploads/sem1/Matrices and Calculus/syllabus/syllabus.pdf
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
14
src/App.tsx
14
src/App.tsx
@@ -1,4 +1,5 @@
|
|||||||
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
import { useEffect } from 'react';
|
||||||
|
import { BrowserRouter, Routes, Route, useLocation } from 'react-router-dom';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import MainLayout from '@/layouts/MainLayout';
|
import MainLayout from '@/layouts/MainLayout';
|
||||||
import Home from '@/pages/Home/Home';
|
import Home from '@/pages/Home/Home';
|
||||||
@@ -18,10 +19,21 @@ const queryClient = new QueryClient({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function ScrollToTop() {
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
}, [pathname]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
|
<ScrollToTop />
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route element={<MainLayout />}>
|
<Route element={<MainLayout />}>
|
||||||
<Route path="/" element={<Home />} />
|
<Route path="/" element={<Home />} />
|
||||||
|
|||||||
@@ -1,11 +1,37 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { Search, Download, Filter, BookOpen, FileText, ScrollText, BookMarked, ChevronRight } from 'lucide-react';
|
import { Search, Download, Filter, BookOpen, FileText, ScrollText, BookMarked, ChevronRight } from 'lucide-react';
|
||||||
import SectionTitle from '@/components/SectionTitle/SectionTitle';
|
import SectionTitle from '@/components/SectionTitle/SectionTitle';
|
||||||
import { StaggerContainer, StaggerItem } from '@/components/AnimatedContainer/AnimatedContainer';
|
import { StaggerContainer, StaggerItem } from '@/components/AnimatedContainer/AnimatedContainer';
|
||||||
import { TOOLKIT_ITEMS, DEPARTMENTS } from '@/constants';
|
import { TOOLKIT_ITEMS, DEPARTMENTS } from '@/constants';
|
||||||
|
|
||||||
const SEMESTERS = ['All', '1st Sem', '2nd Sem', '3rd Sem', '4th Sem', '5th Sem', '6th Sem', '7th Sem', '8th Sem'];
|
const SEMESTERS = ['All', '1st Sem', '2nd Sem'];
|
||||||
|
|
||||||
|
const SUBJECTS_BY_SEM: Record<number, string[]> = {
|
||||||
|
1: [
|
||||||
|
"Communicative English",
|
||||||
|
"Matrices and Calculus",
|
||||||
|
"Physics for Information Science",
|
||||||
|
"Problem Solving and C Programming",
|
||||||
|
"Basic Electrical and Electronics Engineering",
|
||||||
|
"Heritage of Tamils",
|
||||||
|
"Physics Laboratory",
|
||||||
|
"Problem Solving and C Programming Laboratory",
|
||||||
|
"Engineering Practices Laboratory"
|
||||||
|
],
|
||||||
|
2: [
|
||||||
|
"Professional English",
|
||||||
|
"Engineering Chemistry",
|
||||||
|
"Statistics and Numerical Methods",
|
||||||
|
"Python for Data Science",
|
||||||
|
"Tamils and Technology",
|
||||||
|
"Engineering Graphics",
|
||||||
|
"Data Structures Design",
|
||||||
|
"Chemistry Laboratory",
|
||||||
|
"Python for Data Science Laboratory",
|
||||||
|
"Communication Laboratory"
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
const TYPE_COLORS: Record<string, { bg: string; text: string; icon: React.ComponentType<{ className?: string }> }> = {
|
const TYPE_COLORS: Record<string, { bg: string; text: string; icon: React.ComponentType<{ className?: string }> }> = {
|
||||||
notes: { bg: '#EFF6FF', text: '#3B82F6', icon: BookOpen },
|
notes: { bg: '#EFF6FF', text: '#3B82F6', icon: BookOpen },
|
||||||
@@ -14,30 +40,74 @@ const TYPE_COLORS: Record<string, { bg: string; text: string; icon: React.Compon
|
|||||||
assignment: { bg: '#FDF2F8', text: '#EC4899', icon: BookMarked },
|
assignment: { bg: '#FDF2F8', text: '#EC4899', icon: BookMarked },
|
||||||
};
|
};
|
||||||
|
|
||||||
const NOTES_DATA = [
|
interface NoteItem {
|
||||||
{ id: '1', title: 'Engineering Mathematics – Unit 1-5', subject: 'Mathematics', department: 'Computer Science & Engineering', semester: 1, type: 'notes', downloads: 348, fileSize: '4.2 MB', uploadedAt: '2025-06-01' },
|
id: string;
|
||||||
{ id: '2', title: 'Physics PYQ 2020-2024', subject: 'Physics', department: 'Computer Science & Engineering', semester: 1, type: 'pyq', downloads: 512, fileSize: '8.1 MB', uploadedAt: '2025-06-10' },
|
title: string;
|
||||||
{ id: '3', title: 'C Programming Complete Notes', subject: 'Programming', department: 'Computer Science & Engineering', semester: 1, type: 'notes', downloads: 734, fileSize: '6.3 MB', uploadedAt: '2025-06-15' },
|
subject: string;
|
||||||
{ id: '4', title: 'Data Structures PYQ 2019-2024', subject: 'Data Structures', department: 'Computer Science & Engineering', semester: 3, type: 'pyq', downloads: 621, fileSize: '10.2 MB', uploadedAt: '2025-07-01' },
|
department: string;
|
||||||
{ id: '5', title: 'Circuit Theory Full Notes', subject: 'Circuit Theory', department: 'Electronics & Communication', semester: 2, type: 'notes', downloads: 289, fileSize: '5.7 MB', uploadedAt: '2025-06-20' },
|
semester: number;
|
||||||
{ id: '6', title: 'Anna University Syllabus 2021', subject: 'Syllabus', department: 'All Departments', semester: 1, type: 'syllabus', downloads: 890, fileSize: '2.1 MB', uploadedAt: '2025-05-01' },
|
type: string;
|
||||||
{ id: '7', title: 'Thermodynamics Notes', subject: 'Thermodynamics', department: 'Mechanical Engineering', semester: 3, type: 'notes', downloads: 201, fileSize: '3.9 MB', uploadedAt: '2025-06-25' },
|
downloads: number;
|
||||||
{ id: '8', title: 'Digital Electronics PYQ', subject: 'Digital Electronics', department: 'Electronics & Communication', semester: 4, type: 'pyq', downloads: 445, fileSize: '7.3 MB', uploadedAt: '2025-07-05' },
|
fileSize: string;
|
||||||
];
|
uploadedAt: string;
|
||||||
|
downloadUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default function Notes() {
|
export default function Notes() {
|
||||||
|
const [notes, setNotes] = useState<NoteItem[]>([]);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [selectedSem, setSelectedSem] = useState('All');
|
const [selectedSem, setSelectedSem] = useState('All');
|
||||||
const [selectedDept, setSelectedDept] = useState('All Departments');
|
const [selectedDept, setSelectedDept] = useState('All Departments');
|
||||||
const [selectedType, setSelectedType] = useState('all');
|
const [selectedType, setSelectedType] = useState('all');
|
||||||
|
const [selectedSubject, setSelectedSubject] = useState('All Subjects');
|
||||||
|
|
||||||
const filtered = NOTES_DATA.filter((note) => {
|
useEffect(() => {
|
||||||
|
fetch('http://localhost:8080/api/notes')
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data) => {
|
||||||
|
const mapped = data.map((item: any) => ({
|
||||||
|
id: item.id.toString(),
|
||||||
|
title: item.title,
|
||||||
|
subject: item.subject,
|
||||||
|
department: item.department,
|
||||||
|
semester: item.semester,
|
||||||
|
type: item.fileType,
|
||||||
|
downloads: item.downloadsCount,
|
||||||
|
fileSize: item.fileSize,
|
||||||
|
uploadedAt: item.uploadedAt ? item.uploadedAt.split('T')[0] : '',
|
||||||
|
downloadUrl: item.downloadUrl,
|
||||||
|
}));
|
||||||
|
setNotes(mapped);
|
||||||
|
})
|
||||||
|
.catch((err) => console.error('Error fetching notes:', err));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDownload = async (note: NoteItem) => {
|
||||||
|
// Open in new tab
|
||||||
|
window.open(note.downloadUrl, '_blank');
|
||||||
|
|
||||||
|
// Call backend to increment download count
|
||||||
|
try {
|
||||||
|
await fetch(`http://localhost:8080/api/notes/${note.id}/download`, {
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
// Increment locally to update UI immediately
|
||||||
|
setNotes((prevNotes) =>
|
||||||
|
prevNotes.map((n) => (n.id === note.id ? { ...n, downloads: n.downloads + 1 } : n))
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error incrementing download count:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const filtered = notes.filter((note) => {
|
||||||
const matchSearch = note.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
const matchSearch = note.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
note.subject.toLowerCase().includes(searchQuery.toLowerCase());
|
note.subject.toLowerCase().includes(searchQuery.toLowerCase());
|
||||||
const matchSem = selectedSem === 'All' || note.semester === parseInt(selectedSem);
|
const matchSem = selectedSem === 'All' || note.semester === parseInt(selectedSem);
|
||||||
const matchDept = selectedDept === 'All Departments' || note.department === selectedDept || note.department === 'All Departments';
|
const matchDept = selectedDept === 'All Departments' || note.department === selectedDept || note.department === 'All Departments';
|
||||||
const matchType = selectedType === 'all' || note.type === selectedType;
|
const matchType = selectedType === 'all' || note.type === selectedType;
|
||||||
return matchSearch && matchSem && matchDept && matchType;
|
const matchSubject = selectedSubject === 'All Subjects' || note.subject === selectedSubject;
|
||||||
|
return matchSearch && matchSem && matchDept && matchType && matchSubject;
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -90,7 +160,7 @@ export default function Notes() {
|
|||||||
{/* Type Filter */}
|
{/* Type Filter */}
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
<Filter className="w-4 h-4 text-[#94A3B8]" />
|
<Filter className="w-4 h-4 text-[#94A3B8]" />
|
||||||
{['all', 'notes', 'pyq', 'syllabus'].map((type) => (
|
{['all', 'notes', 'pyq', 'syllabus', 'assignment'].map((type) => (
|
||||||
<button
|
<button
|
||||||
key={type}
|
key={type}
|
||||||
onClick={() => setSelectedType(type)}
|
onClick={() => setSelectedType(type)}
|
||||||
@@ -109,10 +179,13 @@ export default function Notes() {
|
|||||||
|
|
||||||
{/* Semester Filter */}
|
{/* Semester Filter */}
|
||||||
<div className="flex gap-2 flex-wrap">
|
<div className="flex gap-2 flex-wrap">
|
||||||
{SEMESTERS.slice(0, 5).map((sem) => (
|
{SEMESTERS.map((sem) => (
|
||||||
<button
|
<button
|
||||||
key={sem}
|
key={sem}
|
||||||
onClick={() => setSelectedSem(sem)}
|
onClick={() => {
|
||||||
|
setSelectedSem(sem);
|
||||||
|
setSelectedSubject('All Subjects');
|
||||||
|
}}
|
||||||
className="px-3 py-1.5 rounded-lg text-xs font-medium transition-all"
|
className="px-3 py-1.5 rounded-lg text-xs font-medium transition-all"
|
||||||
style={{
|
style={{
|
||||||
fontFamily: 'Poppins, sans-serif',
|
fontFamily: 'Poppins, sans-serif',
|
||||||
@@ -125,11 +198,58 @@ export default function Notes() {
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Subject Dropdown */}
|
||||||
|
{selectedSem !== 'All' && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xs font-semibold text-[#64748B]" style={{ fontFamily: 'Poppins, sans-serif' }}>Subject:</span>
|
||||||
|
<select
|
||||||
|
value={selectedSubject}
|
||||||
|
onChange={(e) => setSelectedSubject(e.target.value)}
|
||||||
|
className="px-3 py-1.5 rounded-lg text-xs font-medium bg-[#F8FAFC] border border-[#E5E7EB] text-[#475569] focus:outline-none focus:border-[#F97316] transition-all cursor-pointer"
|
||||||
|
style={{ fontFamily: 'Poppins, sans-serif' }}
|
||||||
|
>
|
||||||
|
<option value="All Subjects">All Subjects</option>
|
||||||
|
{SUBJECTS_BY_SEM[parseInt(selectedSem)].map((sub) => (
|
||||||
|
<option key={sub} value={sub}>
|
||||||
|
{sub}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Stats Summary Banner */}
|
||||||
|
<div className="grid grid-cols-2 gap-4 bg-white border border-[#E5E7EB] rounded-2xl p-5 mb-8" style={{ boxShadow: '0 2px 15px -3px rgba(0,0,0,0.07)' }}>
|
||||||
|
{/* Stat 1: Notes */}
|
||||||
|
<div className="flex items-center justify-center gap-4">
|
||||||
|
<div className="w-12 h-12 rounded-xl flex items-center justify-center bg-[#FFF7ED] shrink-0">
|
||||||
|
<FileText className="w-6 h-6 text-[#F97316]" />
|
||||||
|
</div>
|
||||||
|
<div className="text-left">
|
||||||
|
<div className="text-xl md:text-2xl font-bold text-[#1E293B]">{notes.length}+</div>
|
||||||
|
<div className="text-xs text-[#64748B] font-medium" style={{ fontFamily: 'Poppins, sans-serif' }}>Notes & Materials</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stat 2: Subjects */}
|
||||||
|
<div className="flex items-center justify-center gap-4 border-l border-[#E5E7EB]">
|
||||||
|
<div className="w-12 h-12 rounded-xl flex items-center justify-center bg-[#EFF6FF] shrink-0">
|
||||||
|
<BookOpen className="w-6 h-6 text-[#3B82F6]" />
|
||||||
|
</div>
|
||||||
|
<div className="text-left pl-4">
|
||||||
|
<div className="text-xl md:text-2xl font-bold text-[#1E293B]">
|
||||||
|
{Array.from(new Set(notes.map(n => n.subject))).length}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-[#64748B] font-medium" style={{ fontFamily: 'Poppins, sans-serif' }}>Subjects</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Results */}
|
{/* Results */}
|
||||||
<StaggerContainer className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 mb-12">
|
<StaggerContainer key={filtered.length} className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 mb-12">
|
||||||
{filtered.map((note) => {
|
{filtered.map((note) => {
|
||||||
const typeConfig = TYPE_COLORS[note.type];
|
const typeConfig = TYPE_COLORS[note.type];
|
||||||
const TypeIcon = typeConfig.icon;
|
const TypeIcon = typeConfig.icon;
|
||||||
@@ -137,35 +257,64 @@ export default function Notes() {
|
|||||||
<StaggerItem key={note.id}>
|
<StaggerItem key={note.id}>
|
||||||
<motion.div
|
<motion.div
|
||||||
whileHover={{ y: -4 }}
|
whileHover={{ y: -4 }}
|
||||||
className="bg-white rounded-2xl border border-[#E5E7EB] p-5 flex flex-col gap-3"
|
className="bg-white rounded-2xl border border-[#E5E7EB] p-5 flex flex-col justify-between gap-4"
|
||||||
style={{ boxShadow: '0 2px 15px -3px rgba(0,0,0,0.07)' }}
|
style={{
|
||||||
|
borderLeft: `4px solid ${typeConfig.text}`,
|
||||||
|
boxShadow: '0 2px 15px -3px rgba(0,0,0,0.07)'
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between gap-2">
|
<div className="flex gap-4">
|
||||||
<div className="w-10 h-10 rounded-xl flex items-center justify-center shrink-0" style={{ backgroundColor: typeConfig.bg }}>
|
{/* Visual PDF Preview Icon */}
|
||||||
<TypeIcon className="w-5 h-5 text-orange-500" />
|
<div className="w-14 h-20 border border-slate-200 rounded-xl p-2 flex flex-col justify-between bg-[#F8FAFC] shrink-0 shadow-sm">
|
||||||
|
<div className="bg-[#EF4444] text-[8px] font-extrabold text-white px-1 py-0.5 rounded w-fit uppercase tracking-wider" style={{ fontFamily: 'Poppins, sans-serif' }}>
|
||||||
|
PDF
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<div className="w-full h-1 bg-slate-200 rounded" />
|
||||||
|
<div className="w-5/6 h-1 bg-slate-200 rounded" />
|
||||||
|
<div className="w-2/3 h-1 bg-slate-200 rounded" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Text Details */}
|
||||||
|
<div className="flex flex-col justify-between flex-1 min-w-0">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-1.5">
|
||||||
|
<div className="w-6 h-6 rounded-lg flex items-center justify-center shrink-0" style={{ backgroundColor: typeConfig.bg }}>
|
||||||
|
<TypeIcon className="w-3.5 h-3.5" style={{ color: typeConfig.text }} />
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className="px-2 py-0.5 rounded-full text-[9px] font-bold uppercase tracking-wider"
|
||||||
|
style={{ backgroundColor: typeConfig.bg, color: typeConfig.text, fontFamily: 'Poppins, sans-serif' }}
|
||||||
|
>
|
||||||
|
{note.type}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-sm font-bold text-[#1E293B] line-clamp-2 leading-tight mb-1" style={{ fontFamily: 'Poppins, sans-serif' }}>
|
||||||
|
{note.title}
|
||||||
|
</h3>
|
||||||
|
<p className="text-[11px] text-[#64748B] font-medium truncate" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||||
|
{note.subject} • Sem {note.semester}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span
|
|
||||||
className="px-2 py-0.5 rounded-full text-[10px] font-bold uppercase"
|
|
||||||
style={{ backgroundColor: typeConfig.bg, color: typeConfig.text, fontFamily: 'Poppins, sans-serif' }}
|
|
||||||
>
|
|
||||||
{note.type}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<h3 className="text-sm font-semibold text-[#1E293B] leading-snug mb-1" style={{ fontFamily: 'Poppins, sans-serif' }}>
|
{/* Metadata Line */}
|
||||||
{note.title}
|
<div className="flex items-center justify-between border-t border-[#F1F5F9] pt-3 text-[11px] text-[#64748B]" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||||
</h3>
|
<div className="flex items-center gap-1.5">
|
||||||
<p className="text-xs text-[#94A3B8]" style={{ fontFamily: 'Inter, sans-serif' }}>
|
<span className="font-semibold text-slate-800">{note.fileSize}</span>
|
||||||
{note.subject} · Sem {note.semester}
|
<span className="px-1 py-0.2 bg-[#F1F5F9] rounded text-[9px] font-bold text-slate-600">PDF</span>
|
||||||
</p>
|
</div>
|
||||||
</div>
|
<span>{note.uploadedAt ? `Updated ${note.uploadedAt}` : 'Recently'}</span>
|
||||||
<div className="flex items-center justify-between text-xs text-[#94A3B8] pt-2 border-t border-[#E5E7EB]">
|
|
||||||
<span style={{ fontFamily: 'Inter, sans-serif' }}>{note.downloads} downloads · {note.fileSize}</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Download Button */}
|
||||||
<motion.button
|
<motion.button
|
||||||
whileHover={{ scale: 1.02 }}
|
whileHover={{ scale: 1.02 }}
|
||||||
whileTap={{ scale: 0.97 }}
|
whileTap={{ scale: 0.97 }}
|
||||||
className="w-full flex items-center justify-center gap-2 py-2.5 rounded-xl text-sm font-semibold text-white"
|
onClick={() => handleDownload(note)}
|
||||||
|
className="w-full flex items-center justify-center gap-2 py-2.5 rounded-xl text-sm font-semibold text-white cursor-pointer"
|
||||||
style={{
|
style={{
|
||||||
fontFamily: 'Poppins, sans-serif',
|
fontFamily: 'Poppins, sans-serif',
|
||||||
background: 'linear-gradient(135deg, #F97316, #FB923C)',
|
background: 'linear-gradient(135deg, #F97316, #FB923C)',
|
||||||
|
|||||||
Reference in New Issue
Block a user