diff --git a/Readme.md b/Readme.md index a4d501c..3870a04 100644 --- a/Readme.md +++ b/Readme.md @@ -62,24 +62,61 @@ We're excited to be part of **Social Winter of Code 2026**! This is a great oppo ## 🏗️ Architecture -``` -┌─────────────────────────────────────────────────────────────┐ -│ Frontend (Client) │ -│ React 19 + TypeScript + Tailwind CSS + Vite │ -│ Deployed on Vercel │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ Backend (Node.js Server) │ -│ Express.js + Prisma + MongoDB + Socket.io │ -│ Deployed on Fly.io │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ AI Backend (Python FastAPI) │ -│ FastAPI + LangChain + Pinecone + Groq │ -│ Deployed on Fly.io │ -└─────────────────────────────────────────────────────────────┘ +```mermaid +flowchart TD + %% Define Styles + classDef frontend fill:#e1f5fe,stroke:#01579b,stroke-width:2px; + classDef backend fill:#fff3e0,stroke:#e65100,stroke-width:2px; + classDef aibackend fill:#f3e5f5,stroke:#4a148c,stroke-width:2px; + classDef db fill:#e8f5e9,stroke:#1b5e20,stroke-width:2px; + classDef external fill:#eee,stroke:#333,stroke-width:2px; + + subgraph Client ["🖥️ Frontend (Vercel)"] + direction TB + React["React 19 + Vite + Tailwind CSS"]:::frontend + end + + subgraph Server ["⚙️ Node.js Backend (Fly.io)"] + direction TB + API["Express.js + Socket.io"]:::backend + Prisma["Prisma ORM"]:::backend + end + + subgraph AI ["🤖 AI Python Backend (Fly.io)"] + direction TB + FastAPI["FastAPI Server + LangChain"]:::aibackend + PyPDF["PDF Processor"]:::aibackend + end + + subgraph Data ["💾 Databases & Storage"] + direction LR + Mongo[("MongoDB Atlas")]:::db + Redis[("Redis Cloud")]:::db + Pinecone[("Pinecone Vector DB")]:::db + Blob["Cloud Storage (R2/Blob)"]:::db + end + + subgraph Ext ["☁️ External APIs"] + direction LB + Groq["Groq LLM API"]:::external + SMTP["SMTP (Emails)"]:::external + end + + %% Core Connections + Client <-->|REST & WebSockets| API + Client -.->|Direct Uploads| Blob + + API --- Prisma + Prisma <-->|CRUD Operations| Mongo + API <-->|Session/Cache| Redis + API -->|Dispatch| SMTP + + API <-->|HTTP Queries| FastAPI + + FastAPI --- PyPDF + FastAPI <-->|Store/Search Vectors| Pinecone + PyPDF -.->|Process Docs| Blob + FastAPI <-->|LLM Context/Prompts| Groq ``` --- diff --git a/client/src/App.tsx b/client/src/App.tsx index 556900e..c975cba 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -223,11 +223,12 @@ const App: React.FC = () => { gutter={12} containerStyle={{ top: 40 }} toastOptions={{ + duration: 4000, style: { fontSize: "14px", maxWidth: "500px", padding: "12px 20px", - backgroundColor: "#1e1e1e", + backgroundColor: "#1e1e1e", color: "#fff", border: "2px solid #00cc33", borderRadius: "12px", diff --git a/client/src/components/auth/AuthForm.tsx b/client/src/components/auth/AuthForm.tsx index 9e91cfa..a580c8c 100644 --- a/client/src/components/auth/AuthForm.tsx +++ b/client/src/components/auth/AuthForm.tsx @@ -2,6 +2,7 @@ import React, { useState } from "react"; import { useNavigate } from "react-router-dom"; import { User, Lock, UserPlus, LogIn, Mail, Shield } from "lucide-react"; import { login, signup, sendOTP } from "../../utils/api"; +import toast from "react-hot-toast"; interface AuthFormProps { onAuthChange: () => void; @@ -18,27 +19,23 @@ const AuthForm: React.FC = ({ onAuthChange }) => { const [otpSent, setOtpSent] = useState(false); const [loading, setLoading] = useState(false); const [otpLoading, setOtpLoading] = useState(false); - const [error, setError] = useState(""); - const [success, setSuccess] = useState(""); const navigate = useNavigate(); const handleSendOTP = async () => { if (!email) { - setError("Please enter your email first"); + toast.error("Please enter your email first"); return; } setOtpLoading(true); - setError(""); - setSuccess(""); try { const response = await sendOTP(email, "signup"); - setSuccess(response.message); + toast.success(response.message); setOtpSent(true); setShowOTPField(true); } catch (err: any) { - setError(err.response?.data?.error || "Failed to send OTP"); + toast.error(err.userMessage || err.response?.data?.error || "Failed to send OTP"); } finally { setOtpLoading(false); } @@ -47,8 +44,6 @@ const AuthForm: React.FC = ({ onAuthChange }) => { const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setLoading(true); - setError(""); - setSuccess(""); try { const authResponse = isLogin @@ -59,7 +54,7 @@ const AuthForm: React.FC = ({ onAuthChange }) => { // Set success message for signup if (!isLogin) { - setSuccess("Account created successfully! Welcome to Edulume!"); + toast.success("Account created successfully! Welcome to Edulume!"); } // Notify parent component to re-check auth status @@ -74,7 +69,7 @@ const AuthForm: React.FC = ({ onAuthChange }) => { ); // Shorter delay for signup } catch (err: any) { console.error("❌ Authentication failed:", err); - setError(err.response?.data?.error || err.message || "An error occurred"); + toast.error(err.userMessage || err.response?.data?.error || err.message || "An error occurred"); } finally { setLoading(false); } @@ -226,18 +221,6 @@ const AuthForm: React.FC = ({ onAuthChange }) => { - {error && ( -
- {error} -
- )} - - {success && ( -
- {success} -
- )} - - - )} + )} + + )} + + {/* Reply Actions */} + {currentUser && ( +
+ +
+ )} + - - ); - })} - - )} - - {replyingTo === answer.id && ( -
0 - ? "mt-4 pt-4 border-t border-smoke-light/20" - : "" - }`} - > -
-
- - - Replying - {replyToUsername - ? ` to @${replyToUsername}` - : ""} - -
+ ); + })} +
+ )} -
- { - if ((e.ctrlKey || e.metaKey) && e.key === "Enter" && !submittingReply) { - e.preventDefault(); - submitReplyAction(); - } - }} - /> + {replyingTo === answer.id && ( +
0 + ? "mt-4 pt-4 border-t border-smoke-light/20" + : "" + }`} + > +
+
+ + + Replying + {replyToUsername + ? ` to @${replyToUsername}` + : ""} + +
- {/* Image Upload for Reply */} -
-
- - {/* */} -
- - {/* Form Actions */} -
-

- Tip: Press Ctrl + Enter to submit -

-
- -
-
-
- {/* Reply Image Preview */} - {replyImages.length > 0 && ( -
- {replyImages.map((imageUrl, index) => ( -
- {`Reply + {/* Form Actions */} +
+

+ Tip: Press Ctrl + Enter to submit +

+
+
- ))} +
- )} - + + {/* Reply Image Preview */} + {replyImages.length > 0 && ( +
+ {replyImages.map((imageUrl, index) => ( +
+ {`Reply + +
+ ))} +
+ )} + +
-
- )} -
- )} + )} + + )} ); })} diff --git a/client/src/components/resources/UploadForm.tsx b/client/src/components/resources/UploadForm.tsx index 5471e38..ec8a516 100644 --- a/client/src/components/resources/UploadForm.tsx +++ b/client/src/components/resources/UploadForm.tsx @@ -1,13 +1,12 @@ import React, { useState } from "react"; import { useNavigate } from "react-router-dom"; -import { Upload, FileText, BookOpen, AlertCircle } from "lucide-react"; +import { Upload, FileText, BookOpen } from "lucide-react"; import { - generatePDFUploadUrl, - generateEbookUploadUrl, storePDFMetadata, storeEbookMetadata, uploadToVercelBlob, } from "../../utils/api"; +import toast from "react-hot-toast"; const UploadForm: React.FC = () => { const [uploadType, setUploadType] = useState<"pdf" | "ebook">("pdf"); @@ -19,32 +18,27 @@ const UploadForm: React.FC = () => { const [department, setDepartment] = useState(""); const [yearOfStudy, setYearOfStudy] = useState(""); const [loading, setLoading] = useState(false); - const [error, setError] = useState(""); - const [success, setSuccess] = useState(""); const navigate = useNavigate(); const handleFileChange = (e: React.ChangeEvent) => { const selectedFile = e.target.files?.[0]; if (selectedFile) { if (selectedFile.type !== "application/pdf") { - setError("Please select a PDF file"); + toast.error("Please select a PDF file"); return; } setFile(selectedFile); - setError(""); } }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!file) { - setError("Please select a file"); + toast.error("Please select a file"); return; } setLoading(true); - setError(""); - setSuccess(""); try { // Step 1: Upload file directly to Vercel Blob @@ -69,7 +63,7 @@ const UploadForm: React.FC = () => { await storeEbookMetadata(metadata); } - setSuccess(`${uploadType.toUpperCase()} uploaded successfully!`); + toast.success(`${uploadType.toUpperCase()} uploaded successfully!`); // Reset form setFile(null); @@ -85,7 +79,7 @@ const UploadForm: React.FC = () => { navigate(uploadType === "pdf" ? "/pdfs" : "/ebooks"); }, 2000); } catch (err: any) { - setError(err.response?.data?.error || "Upload failed"); + toast.error(err.userMessage || err.response?.data?.error || "Upload failed"); } finally { setLoading(false); } @@ -117,11 +111,10 @@ const UploadForm: React.FC = () => {