Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
398 changes: 278 additions & 120 deletions backend/college_transfer_ai/app.py

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion backend/college_transfer_ai/college_transfer_API.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ class CollegeTransferAPI:
def __init__(self):
self.base_url = "https://assist.org/api/"


def get_academic_years(self):
url = self.base_url + "AcademicYears"
response = requests.get(url)
Expand Down
568 changes: 565 additions & 3 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,13 @@
"preview": "vite preview"
},
"dependencies": {
"@react-oauth/google": "^0.12.1",
"dotenv": "^16.5.0",
"jwt-decode": "^4.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^6.30.0"
"react-router-dom": "^6.30.0",
"reactflow": "^11.11.4"
},
"devDependencies": {
"@eslint/js": "^9.22.0",
Expand Down
6 changes: 5 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,8 @@ pymongo
playwright
jinja2
pytest
PyMuPDF
PyMuPDF
openai
dotenv
react-router-dom
react
1 change: 0 additions & 1 deletion src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ body {

/* Container for centering content */
#root > div { /* Target the main div rendered by React */
max-width: 960px; /* Max width similar to assist.org content area */
margin: 20px auto; /* Center the container */
padding: 20px;
background-color: #fff; /* White background for content area */
Expand Down
166 changes: 153 additions & 13 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,161 @@
import React from 'react';
import { Routes, Route } from 'react-router-dom';
import React, { useState } from 'react';
import { Routes, Route, Link, useNavigate } from 'react-router-dom';
import { GoogleLogin, googleLogout } from '@react-oauth/google';
import { jwtDecode } from "jwt-decode";
import CollegeTransferForm from './components/CollegeTransferForm';
import AgreementViewerPage from './components/AgreementViewerPage'; // Import the new combined page
import AgreementViewerPage from './components/AgreementViewerPage';
import CourseMap from './components/CourseMap';
import './App.css';

// Define a key for localStorage
const USER_STORAGE_KEY = 'collegeTransferUser';

function App() {
return (
<Routes>
{/* Route for the main form */}
<Route path="/" element={<CollegeTransferForm />} />
// Initialize user state from localStorage on initial load
const [user, setUser] = useState(() => {
try {
const storedUser = localStorage.getItem(USER_STORAGE_KEY);
if (storedUser) {
const parsedUser = JSON.parse(storedUser);

// *** Check Token Expiration ***
if (parsedUser.idToken) {
const decoded = jwtDecode(parsedUser.idToken);
const isExpired = decoded.exp * 1000 < Date.now(); // Convert exp (seconds) to milliseconds

if (isExpired) {
console.log("Stored token expired, clearing storage.");
localStorage.removeItem(USER_STORAGE_KEY);
return null; // Treat as logged out
}
} else {
// Handle case where token might be missing in stored data
console.warn("Stored user data missing idToken, clearing storage.");
localStorage.removeItem(USER_STORAGE_KEY);
return null;
}
// *** End Check ***

console.log("Loaded valid user from localStorage");
return parsedUser;
}
} catch (error) {
console.error("Failed to load or validate user from localStorage:", error);
localStorage.removeItem(USER_STORAGE_KEY); // Clear corrupted/invalid data
}
return null; // Default to null
});

const navigate = useNavigate();

// Function to handle successful login
const handleLoginSuccess = (credentialResponse) => {
console.log("Google Login Success:", credentialResponse);
try {
const decoded = jwtDecode(credentialResponse.credential);
console.log("Decoded JWT:", decoded);
const newUser = {
idToken: credentialResponse.credential,
id: decoded.sub,
name: decoded.name,
email: decoded.email,
};
setUser(newUser); // Update React state

// Save user data to localStorage
try {
localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(newUser));
console.log("Saved user to localStorage");
} catch (storageError) {
console.error("Failed to save user to localStorage:", storageError);
}

{/* Route for the combined Agreement Viewer */}
<Route
path="/agreement/:sendingId/:receivingId/:yearId"
element={<AgreementViewerPage />}
/>
} catch (error) {
console.error("Error decoding JWT:", error);
setUser(null);
localStorage.removeItem(USER_STORAGE_KEY); // Clear storage on error
}
};

const handleLoginError = () => {
console.error("Google Login Failed");
setUser(null);
localStorage.removeItem(USER_STORAGE_KEY); // Clear storage on login error
};

// Function to handle logout
const handleLogout = () => {
googleLogout(); // Clear Google session
setUser(null); // Clear React state

// Remove user data from localStorage
try {
localStorage.removeItem(USER_STORAGE_KEY);
console.log("Removed user from localStorage");
} catch (storageError) {
console.error("Failed to remove user from localStorage:", storageError);
}

console.log("User logged out");
navigate('/');
};

// Optional: Effect to listen for storage changes in other tabs (advanced)
// useEffect(() => {
// const handleStorageChange = (event) => {
// if (event.key === USER_STORAGE_KEY) {
// if (!event.newValue) { // User logged out in another tab
// setUser(null);
// } else { // User logged in/updated in another tab
// try {
// setUser(JSON.parse(event.newValue));
// } catch {
// setUser(null);
// }
// }
// }
// };
// window.addEventListener('storage', handleStorageChange);
// return () => window.removeEventListener('storage', handleStorageChange);
// }, []);

return (
<>
{/* Navigation/Header remains the same */}
<nav style={{ padding: '10px 20px', backgroundColor: '#eee', borderBottom: '1px solid #ccc', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<Link to="/" style={{ marginRight: '15px' }}>Home</Link>
{user && <Link to="/course-map">Course Map</Link>}
</div>
<div>
{user ? (
<>
<span style={{ marginRight: '10px', fontSize: '0.9em' }}>Welcome, {user.name || user.email}!</span>
<button onClick={handleLogout}>Logout</button>
</>
) : (
<GoogleLogin
onSuccess={handleLoginSuccess}
onError={handleLoginError}
useOneTap
/>
)}
</div>
</nav>

</Routes>
{/* Routes remain the same */}
<Routes>
<Route path="/" element={<CollegeTransferForm />} />
<Route
path="/agreement/:sendingId/:receivingId/:yearId"
element={<AgreementViewerPage />}
/>
<Route
path="/course-map"
element={user ? <CourseMap user={user} /> : <p>Please log in to view the course map.</p>}
/>
</Routes>
</>
);
}

Expand Down
Loading
Loading