Skip to content
Open
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
42 changes: 19 additions & 23 deletions src/App.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,30 @@
import React from "react";
import { BrowserRouter as Router, Route, Routes, Navigate} from 'react-router-dom';
import PageTemplate from './PageTemplate';
import './modalStyles.css'
import './modalStyles.css';
import './App.css';
import { latestSemester } from './LatestSemester';
import { SettingsProvider } from './SettingsContext';


function App() {


// loop over the data and create a list of links
return (
<Router>
<Routes>


<Route path="/catalog/:semester/:department/:org/:number" element={<PageTemplate target={"catalog-page"}/>} />
<Route path="/catalog/:semester/:department" element={<PageTemplate target={"catalog-page"}/>} />
<Route path="/catalog/:semester/:department" element={<PageTemplate target={"catalog-page"}/>} />
<Route path="/catalog/:semester" element={<PageTemplate target={"catalog"}/>}/>

<Route path="/catalog/semesters" element={<PageTemplate target={"catalog-semester-list"}/>} />

<Route path="/search" element={<PageTemplate target={"search"}/>}/>
<Route path="/search/:query" element={<PageTemplate target={"search"}/>}/>
<Route path="/" element={<Navigate to={`/catalog/${latestSemester}`} />} />
</Routes>
</Router>
)

// loop over the data and create a list of links
return (
<SettingsProvider>
<Router>
<Routes>
<Route path="/catalog/:semester/:department/:org/:number" element={<PageTemplate target={"catalog-page"}/>} />
<Route path="/catalog/:semester/:department" element={<PageTemplate target={"catalog-page"}/>} />
<Route path="/catalog/:semester/:department" element={<PageTemplate target={"catalog-page"}/>} />
<Route path="/catalog/:semester" element={<PageTemplate target={"catalog"}/>}/>
<Route path="/catalog/semesters" element={<PageTemplate target={"catalog-semester-list"}/>} />
<Route path="/search" element={<PageTemplate target={"search"}/>}/>
<Route path="/search/:query" element={<PageTemplate target={"search"}/>}/>
<Route path="/" element={<Navigate to={`/catalog/${latestSemester}`} />} />
</Routes>
</Router>
</SettingsProvider>
);
}

export default App;
36 changes: 32 additions & 4 deletions src/CatalogPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useParams, useNavigate} from 'react-router-dom';
import './Catalog.css'
import './Catalog.css'
import { requirementMapping } from './RequirementMap';
import { useSettings } from './SettingsContext';

function CatalogPage() {
const { semester, department, org, number} = useParams();
Expand All @@ -12,13 +13,16 @@ function CatalogPage() {

const [tableExpansions, setTableExpansions] = useState({});
const [allTablesExpanded, setAllTablesExpanded] = useState(false); // state for overall table expansion
const [activeCourseKey, setActiveCourseKey] = useState(null);

const [allSemesters, setAllSemesters] = useState([]);
const [selectedSemester, setSelectedSemester] = useState(semester);

const [noDataFound, setNoDataFound] = useState(false);

const [showBackToTop, setShowBackToTop] = useState(false);
const { settings } = useSettings();
const autoFollowCamera = settings.autoFollowCamera;


const scrollToTop = () => {
Expand Down Expand Up @@ -69,6 +73,9 @@ function CatalogPage() {
}
if (org && number) {
newTableExpansions[`${org}${number}`] = true;
setActiveCourseKey(`${org}${number}`);
} else {
setActiveCourseKey(null);
}
setTableExpansions(newTableExpansions);
setAllTablesExpanded(false);
Expand Down Expand Up @@ -114,21 +121,28 @@ function CatalogPage() {
if(org && number){
scrollKey = org + number;
}
if (scrollKey) {
if (scrollKey && autoFollowCamera) {
setTimeout(() => {
// Use JavaScript to scroll to the specified table
const tableElement = document.getElementById(scrollKey);
if (tableElement) {
tableElement.scrollIntoView({ behavior: 'smooth' });
tableElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, 750);
}
}, [number, org]);
}, [number, org, autoFollowCamera]);


const toggleTableExpansion = (tableKey) => {
setTableExpansions((prevTableExpansions) => {
return { ...prevTableExpansions, [tableKey]: !prevTableExpansions[tableKey] };
const shouldExpand = !prevTableExpansions[tableKey];
setActiveCourseKey((previousActive) => {
if (shouldExpand) {
return tableKey;
}
return previousActive === tableKey ? null : previousActive;
});
return { ...prevTableExpansions, [tableKey]: shouldExpand };
});
};

Expand All @@ -141,10 +155,24 @@ function CatalogPage() {
return state;
}, {});
setTableExpansions(newExpansionState);
setActiveCourseKey(null);
return newState;
});
};

useEffect(() => {
if (!autoFollowCamera || !activeCourseKey) {
return;
}
if (!tableExpansions[activeCourseKey]) {
return;
}
const element = document.getElementById(activeCourseKey);
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, [autoFollowCamera, activeCourseKey, tableExpansions]);


const generateMeetingTable = (meetings) => {
if (!meetings || meetings.length === 0) {
Expand Down
71 changes: 64 additions & 7 deletions src/PageTemplate.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,30 @@
import Modal from 'react-modal';
import React, { useState} from "react";
import React, { useEffect, useState } from "react";
import Catalog from './Catalog';
import SearchComponent from './SearchComponent';
import CatalogPage from './CatalogPage';
import './modalStyles.css'
import {latestSemester} from './LatestSemester';
import AllSemesters from './AllSemesters';
import { useSettings } from './SettingsContext';



function PageTemplate(props){

const [isModalOpen, setIsModalOpen] = useState(false);

Modal.setAppElement('#root');
const [isModalOpen, setIsModalOpen] = useState(false);
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
const { settings, updateSetting } = useSettings();

useEffect(() => {
if (typeof document === 'undefined') {
return;
}
const appRoot = document.getElementById('root');
if (appRoot) {
Modal.setAppElement(appRoot);
}
}, []);

const openModal = () => {
setIsModalOpen(true);
Expand All @@ -23,6 +34,14 @@ function PageTemplate(props){
setIsModalOpen(false);
};

const openSettingsModal = () => {
setIsSettingsOpen(true);
};

const closeSettingsModal = () => {
setIsSettingsOpen(false);
};



const renderTarget = props.target;
Expand All @@ -49,8 +68,46 @@ function PageTemplate(props){
<div className="App">
<header className="App-header">

{ renderTarget !== "catalog-page" && <button onClick={openModal} className="fixed-button" style={{textAlign: "center"}}><span className='info-character'>i</span></button>}

<button
onClick={openSettingsModal}
className="fixed-button settings-button"
style={{textAlign: "center"}}
aria-label="Open settings"
>
<span aria-hidden="true">⚙️</span>
</button>
{ renderTarget !== "catalog-page" && <button onClick={openModal} className="fixed-button info-button" style={{textAlign: "center"}} aria-label="Open info modal"><span className='info-character'>i</span></button>}

<Modal
isOpen={isSettingsOpen}
onRequestClose={closeSettingsModal}
contentLabel="Settings"
className="modal settings-modal"
>
<div className='scroll-div settings-content'>
<h2 className="modal-content">Settings</h2>
<div className="settings-row">
<div className="settings-copy">
<p className="settings-title">Auto-follow camera</p>
<p className="settings-description">
Automatically centers the visualization on the active layer so you can follow the main action without manual panning.
</p>
</div>
<label className="settings-toggle">
<input
type="checkbox"
checked={settings.autoFollowCamera}
onChange={(event) => updateSetting('autoFollowCamera', event.target.checked)}
/>
<span>{settings.autoFollowCamera ? 'On' : 'Off'}</span>
</label>
</div>
</div>
<div>
<button onClick={closeSettingsModal} className="close-button" aria-label="Close settings">X</button>
</div>
</Modal>

<div className="modal-background">
<Modal
isOpen={isModalOpen}
Expand Down Expand Up @@ -92,7 +149,7 @@ function PageTemplate(props){
We hope you find this tool useful 😊.
</div>
<div>
<button onClick={closeModal} className="close-button">X</button>
<button onClick={closeModal} className="close-button" aria-label="Close info">X</button>
</div>
</Modal>
</div>
Expand Down
72 changes: 72 additions & 0 deletions src/SettingsContext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';

const defaultSettings = {
autoFollowCamera: true,
};

const SettingsContext = createContext();

const STORAGE_KEY = 'course-explorer-settings';

function readStoredSettings() {
if (typeof window === 'undefined') {
return null;
}
try {
const raw = window.localStorage.getItem(STORAGE_KEY);
if (!raw) {
return null;
}
const parsed = JSON.parse(raw);
if (typeof parsed !== 'object' || parsed === null) {
return null;
}
return parsed;
} catch (error) {
console.error('Failed to read stored settings', error);
return null;
}
}

export function SettingsProvider({ children }) {
const [settings, setSettings] = useState(() => {
const stored = readStoredSettings();
if (stored) {
return { ...defaultSettings, ...stored };
}
return defaultSettings;
});

useEffect(() => {
if (typeof window === 'undefined') {
return;
}
try {
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
} catch (error) {
console.error('Failed to persist settings', error);
}
}, [settings]);

const updateSetting = useCallback((key, value) => {
setSettings((prevSettings) => ({ ...prevSettings, [key]: value }));
}, []);

const value = useMemo(
() => ({
settings,
updateSetting,
}),
[settings, updateSetting],
);

return <SettingsContext.Provider value={value}>{children}</SettingsContext.Provider>;
}

export function useSettings() {
const context = useContext(SettingsContext);
if (!context) {
throw new Error('useSettings must be used within a SettingsProvider');
}
return context;
}
56 changes: 56 additions & 0 deletions src/modalStyles.css
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@
margin: "0 auto";
}

.settings-button {
right: 4.5em;
}

.info-button {
right: 1em;
}

.fixed-button:hover {
background: #c9732c;
}
Expand Down Expand Up @@ -85,6 +93,54 @@
padding-right: 10%;
}

.settings-modal {
max-width: 40rem;
width: 90%;
height: auto;
max-height: 80vh;
}

.settings-content {
display: flex;
flex-direction: column;
gap: 1.5em;
}

.settings-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 1.5em;
}

.settings-copy {
flex: 1;
}

.settings-title {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 0.35em;
}

.settings-description {
font-size: 0.95rem;
line-height: 1.4;
opacity: 0.85;
}

.settings-toggle {
display: flex;
align-items: center;
gap: 0.5em;
font-size: 1rem;
}

.settings-toggle input[type="checkbox"] {
width: 1.2em;
height: 1.2em;
}

/* modalStyles.css */
.custom-modal-content {
width: 60%; /* Set the width of the modal content */
Expand Down