diff --git a/client/public/ACMDev-logo-white.svg b/client/public/ACMDev-logo-white.svg
new file mode 100644
index 0000000..8980866
--- /dev/null
+++ b/client/public/ACMDev-logo-white.svg
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/client/public/fonts/Gilroy-Bold.ttf b/client/public/fonts/Gilroy-Bold.ttf
new file mode 100644
index 0000000..1aea716
Binary files /dev/null and b/client/public/fonts/Gilroy-Bold.ttf differ
diff --git a/client/public/fonts/Gilroy-Light.ttf b/client/public/fonts/Gilroy-Light.ttf
new file mode 100644
index 0000000..b08db4e
Binary files /dev/null and b/client/public/fonts/Gilroy-Light.ttf differ
diff --git a/client/public/fonts/Gilroy-Regular.ttf b/client/public/fonts/Gilroy-Regular.ttf
new file mode 100644
index 0000000..ad17f71
Binary files /dev/null and b/client/public/fonts/Gilroy-Regular.ttf differ
diff --git a/client/public/fonts/Gilroy-SemiBold.ttf b/client/public/fonts/Gilroy-SemiBold.ttf
new file mode 100644
index 0000000..cb3cbb6
Binary files /dev/null and b/client/public/fonts/Gilroy-SemiBold.ttf differ
diff --git a/client/src/components/Core.tsx b/client/src/components/Core.tsx
index 91b7fee..f95e9ee 100644
--- a/client/src/components/Core.tsx
+++ b/client/src/components/Core.tsx
@@ -1,8 +1,7 @@
-import { HeartTwoTone } from "@ant-design/icons";
-import { Popover } from "antd";
import type { ReactNode } from "react";
-import { useEffect } from "react";
+import { useEffect, useState } from "react";
import styled from "styled-components";
+import SageAd from "./SageAd";
const Container = styled.div`
min-height: 100%;
@@ -38,32 +37,7 @@ const Footer = styled.div`
}
`;
-const SageLink = styled.a`
- background: linear-gradient(90deg, rgba(7,67,37,1) 0%, rgba(22,50,36,1) 100%);
- display: inline-flex;
- align-items: center;
- gap: 0.25rem;
- padding: 0.6rem 1.2rem;
- margin-bottom: 0.3rem;
- border-radius: 100rem;
- color: #5AED86;
- text-shadow: 0 0 4px rgb(0 0 0 / 0.6);
- box-shadow: 0 2px 6px rgb(0 0 0 / 0.2);
- transition: transform cubic-bezier(0.4, 0, 0.2, 1) 150ms, box-shadow cubic-bezier(0.4, 0, 0.2, 1) 150ms;
- &:hover {
- color: #5AED86;
- box-shadow: 0 2px 8px rgb(0 0 0 / 0.2);
- transform: scale(1.01);
- }
-`;
-
-const SageText = styled.p`
- line-height: 1.2rem;
- margin-bottom: 0;
- font-size: 0.9rem;
-`;
-
-const BuiltWithLove = styled.p`
+/*const BuiltWithLove = styled.p`
display: flex;
align-items: center;
justify-content: center;
@@ -71,20 +45,11 @@ const BuiltWithLove = styled.p`
font-size: 1rem;
margin: 0.3rem 0;
font-weight: 550;
-`;
-
-const SageLogo = styled.img`
- height: 1.2rem;
- margin-right: 0.4rem;
- filter: drop-shadow(0 0 4px rgb(0 0 0 / 0.6));
-`;
-
-const SageTextMark = styled.img`
- height: 1.2rem;
-`
+`;*/
const CreditsText = styled.p`
- font-size: 0.9rem;
+ font-family: 'Gilroy-Bold', sans-serif;
+ color: var(--muted-text);
margin: 0.2rem 0;
@media (max-width: 768px) {
@@ -92,11 +57,43 @@ const CreditsText = styled.p`
}
`;
+const GitHubLink = styled.a`
+ color: var(--link-color) !important;
+ text-decoration: none;
+
+ &:hover {
+ text-decoration: underline;
+ }
+`;
+
interface CoreProps {
children: ReactNode;
+ showSageAd?: boolean;
}
-function Core({ children }: CoreProps) {
+function Core({ children, showSageAd = false }: CoreProps) {
+ const [, setTheme] = useState<"light" | "dark" | null>(null);
+
+ // Initialize theme on start
+ useEffect(() => {
+ const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
+ const theme = prefersDark ? "dark" : "light";
+ setTheme(theme);
+ document.documentElement.setAttribute("data-theme", theme);
+ }, []);
+
+ useEffect(() => {
+ const handleThemeChange = (e: StorageEvent) => {
+ if ((e.newValue === "light" || e.newValue === "dark")) {
+ setTheme(e.newValue);
+ document.documentElement.setAttribute("data-theme", e.newValue);
+ }
+ };
+
+ window.addEventListener("storage", handleThemeChange);
+ return () => window.removeEventListener("storage", handleThemeChange);
+ }, []);
+
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
const target = event.target as HTMLElement | null;
@@ -148,40 +145,21 @@ function Core({ children }: CoreProps) {
window.removeEventListener("keydown", handleKeyDown);
};
}, []);
- const donors = (
-
-
- Thank you to the following people for donating and making this possible (in order of most
- monetary support): Anthony-Tien Huynh, Adam Butcher, Paul Denino, Thomas Sowders, Xavier
- Brown, Enza Denino, David Garvin, Alastair Feille, Andrew Vaccaro and other anonymous
- donors.
-
-
- );
return (
{children}
diff --git a/client/src/components/Header.tsx b/client/src/components/Header.tsx
index b942a10..9637041 100644
--- a/client/src/components/Header.tsx
+++ b/client/src/components/Header.tsx
@@ -2,13 +2,15 @@ import { HomeOutlined } from "@ant-design/icons";
import { Button, Row } from "antd";
import Image from "next/image";
import Router from "next/router";
-import React from "react";
+import React, { useEffect, useState } from "react";
import styled from "styled-components";
const Menu = styled(Row)`
padding: 30px;
display: flex;
align-items: center;
+ justify-content: space-between;
+ width: 100%;
`;
const Back = styled(Button) <{ $dummy?: boolean }>`
@@ -16,11 +18,12 @@ const Back = styled(Button) <{ $dummy?: boolean }>`
outline: none;
border: none;
cursor: pointer;
- outline: none;
- border: none;
box-shadow: none;
+ color: var(--text-color);
+ svg {
+ color: inherit;
+ }
visibility: ${(props) => (props.$dummy ? "hidden" : "visible")};
-
&:hover,
&:focus,
&:active {
@@ -35,15 +38,83 @@ const Back = styled(Button) <{ $dummy?: boolean }>`
}
`;
+const ThemeToggle = styled.button`
+ position: absolute;
+ top: 20px;
+ right: 20px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 36px;
+ height: 36px;
+ border-radius: 9999px;
+ border: 1px solid var(--toggle-border, #e4e4e7);
+ background: var(--toggle-bg);
+ color: var(--text-color);
+ cursor: pointer;
+ transition: all 0.2s ease;
+
+ &:hover {
+ background: var(--toggle-hover-bg);
+ color: var(--toggle-hover-color, #333333);
+ }
+
+ svg {
+ width: 16px;
+ height: 16px;
+ }
+
+ @media (prefers-color-scheme: light) {
+ border-color: rgba(255, 255, 255, 0.1);
+ background: rgba(255, 255, 255, 0.05);
+
+ &:hover {
+ background: rgba(255, 255, 255, 0.1);
+ color: #727272;
+ }
+ }
+`;
+
+const SunIcon = () => (
+
+
+
+);
+
+const MoonIcon = () => (
+
+
+
+);
+
+
+
const HeaderText = styled.a`
- margin-right: auto;
- margin-left: auto;
+ position: absolute;
+ left: 50%;
+ transform: translateX(-50%);
display: block;
& h2 {
- font-family: var(--font-family);
- text-transform: uppercase;
- color: rgb(78, 78, 78);
+ color: var(--header-color);
font-weight: 300;
letter-spacing: 2px;
font-size: 24px;
@@ -54,16 +125,42 @@ const HeaderText = styled.a`
}
`;
+
const HeaderBold = styled.span`
+ font-family: 'Gilroy-Bold', sans-serif;
font-weight: 700;
`;
+const HeaderLight = styled.span`
+ font-family: 'Gilroy-Light', sans-serif;
+`;
+
const Logo = {
height: "36px",
width: "36px",
};
export default function Header() {
+ const [theme, setTheme] = useState<"light" | "dark">(() => {
+ if (typeof window === "undefined") return "light";
+ const saved = localStorage.getItem("theme");
+ if (saved === "light" || saved === "dark") return saved;
+ return "light";
+ });
+
+ const toggleTheme = () => {
+ setTheme((prevTheme) => (prevTheme === "dark" ? "light" : "dark"));
+ };
+
+ useEffect(() => {
+ try {
+ document.documentElement.setAttribute("data-theme", theme);
+ localStorage.setItem("theme", theme);
+ } catch (e) {
+ // ignore during SSR
+ }
+ }, [theme]);
+
function goHome() {
(async function () {
await Router.push("/");
@@ -75,17 +172,19 @@ export default function Header() {
} shape="circle" size="large" />
- UTD Grades
+
+ UTD GRADES
- }
- shape="circle"
- size="large"
- />
+
+ {theme === "dark" ? : }
+
);
}
diff --git a/client/src/components/SageAd.tsx b/client/src/components/SageAd.tsx
new file mode 100644
index 0000000..1c7d64a
--- /dev/null
+++ b/client/src/components/SageAd.tsx
@@ -0,0 +1,46 @@
+import styled from "styled-components";
+
+const SageLogo = styled.img`
+ height: 1.2rem;
+ margin-right: 0.4rem;
+ filter: drop-shadow(0 0 4px rgb(0 0 0 / 0.6));
+`;
+
+const SageTextMark = styled.img`
+ height: 1.2rem;
+`;
+const SageLink = styled.a`
+ background: linear-gradient(90deg, rgba(7,67,37,1) 0%, rgba(22,50,36,1) 100%);
+ display: inline-flex;
+ align-items: center;
+ gap: 0.25rem;
+ padding: 0.6rem 1.2rem;
+ margin-bottom: 0.3rem;
+ border-radius: 100rem;
+ color: #5AED86;
+ text-shadow: 0 0 4px rgb(0 0 0 / 0.6);
+ box-shadow: 0 2px 6px rgb(0 0 0 / 0.2);
+ transition: transform cubic-bezier(0.4, 0, 0.2, 1) 150ms, box-shadow cubic-bezier(0.4, 0, 0.2, 1) 150ms;
+ &:hover {
+ color: #5AED86;
+ box-shadow: 0 2px 8px rgb(0 0 0 / 0.2);
+ transform: scale(1.01);
+ }
+`;
+
+const SageText = styled.p`
+ line-height: 1.2rem;
+ margin-bottom: 0;
+ font-size: 0.9rem;
+`;
+
+export default function SageAd() {
+return(
+
+
+
+ Get AI-powered UTD advising with
+
+
+
+ )}
diff --git a/client/src/components/Search.tsx b/client/src/components/Search.tsx
index 32a3cc1..5eb1e3b 100644
--- a/client/src/components/Search.tsx
+++ b/client/src/components/Search.tsx
@@ -14,15 +14,63 @@ const Hint = styled(AntPopover)`
margin-left: auto;
margin-right: auto;
display: block;
- font-family: var(--font-family);
+ font-family: 'Gilroy-Regular', sans-serif;
color: #95989a;
`;
const Popover = styled.div`
- font-family: var(--font-family);
+ font-family: 'Gilroy-Regular', sans-serif;
width: 375px;
`;
+const DarkModeSearch = styled(Input.Search)`
+
+ .ant-input-group {
+ border-radius: 20px;
+ overflow: hidden;
+ border: 1px solid rgb(198, 198, 198);
+ }
+
+ .ant-input-group-addon {
+ padding: 0;
+ background: transparent;
+ }
+ .ant-input {
+ background-color: transparent;
+ color: var(--text-color);
+ font-family: 'Gilroy', sans-serif;
+ height: 44px;
+ line-height: 44px;
+ padding-top: 0;
+ padding-bottom: 0;
+ }
+
+ .ant-input::placeholder {
+ color: var(--search-placeholder);
+ }
+
+ .ant-input-search-button {
+ background-color: transparent;
+ height: 44px;
+ line-height: 44px;
+ padding-top: 0;
+ padding-bottom: 0;
+ }
+
+ .ant-input-search-button .anticon {
+ color: #95989a;
+ }
+
+ .ant-input-search-button .anticon svg {
+ fill: currentColor;
+ }
+
+ .ant-input-search-button:hover {
+ opacity: 0.95;
+ }
+`;
+
+
interface SearchProps {
onSubmit: (query: SearchQuery) => void;
initialSearchValue?: string;
@@ -78,8 +126,7 @@ export default function Search({ onSubmit, initialSearchValue: initialSearch = "
onChange={(value: unknown) => onChange(value as string)}
value={searchValue}
>
- onSubmit({ search })}
name="search"
size="large"
diff --git a/client/src/components/SearchResults.tsx b/client/src/components/SearchResults.tsx
index 002055d..d8031e9 100644
--- a/client/src/components/SearchResults.tsx
+++ b/client/src/components/SearchResults.tsx
@@ -11,7 +11,7 @@ import { normalizeName } from "../utils/index";
import { useDb } from "../utils/useDb";
import Search from "./Search";
import SearchResultsContent from "./SearchResultsContent";
-import { SectionList } from "./SectionList";
+import {SectionList} from "./SectionList";
const Container = styled.div`
display: block;
@@ -22,6 +22,7 @@ const ResultsContainer = styled(Col)`
padding-bottom: 20px;
margin-top: 35px;
border-radius: 5px;
+ color: var(--text-color);
& .ant-list-pagination {
padding-left: 10px;
@@ -32,15 +33,23 @@ const ResultsContainer = styled(Col)`
font-family: var(--font-family);
}
+ & .ant-list-item-meta-title, .ant-card {
+ color: var(--text-color) !important;
+ background: transparent !important;
+ }
+
+ & .ant-list-item-meta-description {
+ color: var(--muted-text) !important;
+ }
+
@media (max-width: 992px) {
& {
box-shadow: none;
}
}
-
@media (min-width: 992px) {
& {
- box-shadow: 0 15px 50px rgba(233, 233, 233, 0.7);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
}
`;
@@ -52,7 +61,7 @@ interface ResultsProps {
}
const Results = React.memo(function Results({ search, sectionId, router }: ResultsProps) {
- // Track current page for SectionList pagination
+ // Track current page for SectionList pagination
const [currentPage, setCurrentPage] = useState(1);
const scrollRef = useRef(null);
const hasAutoSelected = useRef(false);
@@ -107,6 +116,7 @@ const Results = React.memo(function Results({ search, sectionId, router }: Resul
}
}, [sectionId, sections]);
+
// get the section data
const {
data: section,
@@ -271,6 +281,60 @@ const Results = React.memo(function Results({ search, sectionId, router }: Resul
void debouncedNavigate(id);
}, [sectionId, debouncedNavigate]);
+ // Arrow key navigation between sections
+ useEffect(() => {
+ const handleKeyDown = (event: KeyboardEvent) => {
+ // Don't handle arrow keys if user is typing in an input field or textarea
+ const target = event.target as HTMLElement;
+ if (
+ target.tagName === 'INPUT' ||
+ target.tagName === 'TEXTAREA' ||
+ target.isContentEditable
+ ) {
+ return;
+ }
+
+ // Only handle arrow keys when a section is selected
+ if (!sections || sections.length === 0 || !sectionId) {
+ return;
+ }
+
+ // Find the current section index
+ const currentIndex = sections.findIndex((s) => s.id === sectionId);
+
+ if (currentIndex === -1) {
+ return;
+ }
+
+ let newIndex = -1;
+
+ if (event.key === "ArrowLeft") {
+ // Navigate to previous section
+ newIndex = currentIndex > 0 ? currentIndex - 1 : currentIndex;
+ event.preventDefault();
+ } else if (event.key === "ArrowRight") {
+ // Navigate to next section
+ newIndex = currentIndex < sections.length - 1 ? currentIndex + 1 : currentIndex;
+ event.preventDefault();
+ }
+
+ // Navigate to the new section if index changed
+ if (newIndex !== -1 && newIndex !== currentIndex) {
+ const target = sections[newIndex];
+ if (target && typeof target.id === "number") {
+ handleClick(target.id);
+ }
+ }
+ };
+
+ // Add event listener
+ window.addEventListener("keydown", handleKeyDown);
+
+ // Cleanup
+ return () => {
+ window.removeEventListener("keydown", handleKeyDown);
+ };
+ }, [sections, sectionId, handleClick]);
// Arrow key navigation between sections
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
@@ -376,6 +440,7 @@ const Results = React.memo(function Results({ search, sectionId, router }: Resul
error={sectionsError}
page={currentPage}
setPage={setCurrentPage}
+
/>
diff --git a/client/src/components/SearchResultsContent.tsx b/client/src/components/SearchResultsContent.tsx
index 0b0a658..e0a8bc5 100644
--- a/client/src/components/SearchResultsContent.tsx
+++ b/client/src/components/SearchResultsContent.tsx
@@ -13,8 +13,8 @@ const EmptyContainer = styled.div`
`;
const Empty = styled.h2`
- font-family: var(--font-family);
- color: #a4a4a4;
+ font-family: 'Gilroy-Regular';
+ color: var(--muted-text);
font-weight: 300;
font-size: 26px;
`;
diff --git a/client/src/components/SectionContent.tsx b/client/src/components/SectionContent.tsx
index 2bfc5c4..b630e5c 100644
--- a/client/src/components/SectionContent.tsx
+++ b/client/src/components/SectionContent.tsx
@@ -43,7 +43,6 @@ const Container = styled.div`
const GraphContainer = styled.div`
width: 100%;
- flex: 1;
min-height: 250px;
max-height: 400px;
@@ -59,7 +58,7 @@ const GraphContainer = styled.div`
@media (min-width: 992px) {
& {
- box-shadow: 0 15px 15px rgba(233, 233, 233, 0.7);
+ box-shadow: var(--section-card-shadow);
border-radius: 5px;
padding: 20px;
}
@@ -79,7 +78,7 @@ const ProfessorDetailsContainer = styled.div`
@media (min-width: 992px) {
& {
- box-shadow: 0 15px 15px rgba(233, 233, 233, 0.7);
+ box-shadow: var(--section-card-shadow);
border-radius: 5px;
padding: 20px;
}
@@ -87,18 +86,18 @@ const ProfessorDetailsContainer = styled.div`
`;
const Header = styled.h3`
- font-family: var(--font-family);
- font-weight: 700;
+ font-family: 'Gilroy-Bold', sans-serif;
font-size: 48px;
margin-bottom: 0px !important;
margin-top: 0px !important;
+ color: var(--text-color);
`;
const SubHeader = styled.h5`
- font-family: var(--font-family);
+ font-family: 'Gilroy-SemiBold', sans-serif;
font-weight: 600;
font-size: 22px;
- color: rgb(117, 117, 117);
+ color: var(--muted-text);
margin-top: 0.25rem !important;
margin-bottom: 0rem !important;
word-wrap: break-word;
@@ -112,16 +111,16 @@ const SubHeader = styled.h5`
`;
const Stat = styled.h5`
- font-family: var(--font-family);
+ font-family: 'Gilroy-SemiBold', sans-serif;
font-weight: 600;
font-size: 18px;
- color: rgb(117, 117, 117);
+ color: var(--muted-text);
margin-top: 0px !important;
margin-bottom: 0px !important;
`;
const RMPScore = styled.span`
- color: #333333;
+ color: var(--text-color);
line-height: 1;
@media (max-width: 992px) {
@@ -159,7 +158,7 @@ const RMPSubHeader = styled(SubHeader)`
const RMPStat = styled.h5`
font-family: var(--font-family);
font-weight: 800;
- color: #333333;
+ color: var(--text-color);
margin-top: 0px !important;
margin-bottom: 0px !important;
@media (max-width: 1200px) {
@@ -177,9 +176,9 @@ const RMPStat = styled.h5`
`;
const RMPDescpription = styled.p`
- font-family: var(--font-family);
+ font-family: 'Gilroy-Regular', sans-serif;
font-weight: 500;
- color: rgb(117, 117, 117);
+ color: var(--muted-text);
margin-top: 0px !important;
margin-bottom: 0px !important;
@media (max-width: 768px) {
@@ -202,29 +201,42 @@ const RMPDescpription = styled.p`
`;
const RMPTag = styled.p`
- font-family: var(--font-family);
+ font-family: 'Gilroy-Regular', sans-serif;
font-weight: 500;
- color: rgb(84, 84, 84);
- border-radius: 1px;
- background-color: #f5f5f5;
+ color: var(--text-color);
+ border-radius: 4px;
+ background-color: var(--card-bg);
padding: 0.4rem 0.4rem;
transition: all 0.2s ease-in-out;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
&:hover {
- background-color: #e8e8e8;
+ background-color: var(--card-hover-bg);
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
`;
+const TagsHeader = styled.h4`
+ font-family: 'Gilroy-Bold', sans-serif;
+ font-weight: 700;
+ font-size: 1.15rem;
+ color: var(--text-color);
+ text-decoration: none;
+ margin-top: 1.5rem;
+ margin-bottom: 0.5rem;
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+`;
+
const RMPHeader = styled.a`
- font-family: var(--font-family);
+ font-family: 'Gilroy-Bold', sans-serif;
font-weight: 700;
font-size: 1.15rem;
- color: #333333 !important;
+ color: var(--text-color) !important;
text-decoration: none !important;
- border-bottom: ${(props) => (props.href && props.href !== "#" ? "1px solid #333333" : "none")};
+ border-bottom: ${(props) => (props.href && props.href !== "#" ? "1px solid var(--rmp-link-underline)" : "none")};
margin-bottom: 0.5rem;
transition: color 0.2s ease;
display: inline-flex;
@@ -232,7 +244,7 @@ const RMPHeader = styled.a`
gap: 0.5rem;
&:hover {
- color: #666666 !important;
+ color: var(--muted-text) !important;
}
@media (max-width: 768px) {
@@ -243,8 +255,9 @@ const RMPHeader = styled.a`
`;
const Section = styled.span`
- color: rgb(198, 198, 198);
+ font-family: 'Gilroy-Regular', sans-serif;
font-weight: 400;
+ color: #c7c7c7ff;
`;
// const OtherSectionsHeader = styled.p`
@@ -342,7 +355,8 @@ const SectionContent = React.memo(function SectionContent({
datasets: [{ backgroundColor: getColors(keys), data: values }],
};
- const options: ChartOptions<"bar"> = {
+// READ: I had to hardcode the colors to a color in between light and dark mode here because using CSS variables in ChartJS options was not working properly. If someone has a better solution, please help.
+const options: ChartOptions<"bar"> = {
responsive: true,
maintainAspectRatio: false,
plugins: {
@@ -361,6 +375,26 @@ const SectionContent = React.memo(function SectionContent({
},
},
},
+ scales: {
+ x: {
+ grid: {
+ color: "#87878747",
+ borderColor: "#87878747",
+ },
+ ticks: {
+ color: "#868686",
+ },
+ },
+ y: {
+ grid: {
+ color: "#87878747",
+ borderColor: "#87878747",
+ },
+ ticks: {
+ color: "#868686",
+ },
+ },
+ },
};
// FIXME (median)
@@ -389,7 +423,7 @@ const SectionContent = React.memo(function SectionContent({
fontFamily: "var(--font-family)",
fontWeight: "550",
fontSize: "20px",
- color: "gray",
+ color: "var(--muted-text)",
}}
>
{courseRating ? "/5" : ""}
@@ -407,14 +441,14 @@ const SectionContent = React.memo(function SectionContent({
- Total Students {section.totalStudents}
+ Total Students {section.totalStudents}
-
+
@@ -446,9 +480,9 @@ const SectionContent = React.memo(function SectionContent({
borderRadius: "0.5rem",
fontSize: "0.75rem",
lineHeight: "1rem",
- color: "#000000",
+ color: "var(--text-color)",
whiteSpace: "nowrap",
- backgroundColor: "#ffffff",
+ backgroundColor: "var(--card-bg)",
boxShadow:
"0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)",
}}
@@ -514,7 +548,7 @@ const SectionContent = React.memo(function SectionContent({
{instructor?.tags && (
<>
- Tags
+ Tags
{instructor.tags.split(",").map((tag) => (
{tag}
diff --git a/client/src/components/SectionList.tsx b/client/src/components/SectionList.tsx
index 757bc4f..25f2c43 100644
--- a/client/src/components/SectionList.tsx
+++ b/client/src/components/SectionList.tsx
@@ -8,16 +8,12 @@ import styled, { css } from "styled-components";
const Item = styled(List.Item)<{ selected: boolean }>`
padding: 25px;
- border-right: 1px solid #e8e8e8;
- border-bottom: 1px solid #e8e8e8;
+ border-right: 1px solid var(--border-color);
+ border-bottom: 1px solid var(--border-color);
cursor: pointer;
transition: all 300ms ease-out;
font-family: var(--font-family);
- &:hover {
- background-color: #fcfcfc;
- }
-
&:first-child {
border-top-left-radius: 5px;
}
@@ -25,6 +21,13 @@ const Item = styled(List.Item)<{ selected: boolean }>`
& .ant-list-item-meta-title a {
font-weight: 600;
font-family: var(--font-family);
+ color: inherit;
+ text-decoration: none;
+ }
+
+ & .ant-list-item-meta-title a:hover {
+ color: var(--link-color);
+ text-decoration: none;
}
& .ant-list-item-meta {
@@ -35,9 +38,9 @@ const Item = styled(List.Item)<{ selected: boolean }>`
`;
const selectedStyles = css`
- border-right: 6px solid #333 !important;
+ border-right: 6px solid var(--select-tag) !important;
box-shadow: inset -5px 0px 10px rgba(0, 0, 0, 0.05);
- background-color: #fcfcfc;
+ background-color: var(--card-bg);
`;
const Hint = styled(AntPopover)`
@@ -46,7 +49,7 @@ const Hint = styled(AntPopover)`
margin-right: auto;
display: block;
font-family: var(--font-family);
- color: #95989a;
+ color: var(--description-color);
`;
const Popover = styled.div`
@@ -62,7 +65,7 @@ const Error = styled.p`
font-family: var(--font-family);
font-size: 22px;
text-align: center;
- color: #a4a4a4;
+ color: var(--muted-text);
font-weight: 300;
`;
@@ -72,7 +75,7 @@ const StyledIcon = styled(FrownTwoTone)`
margin-bottom: 15px;
margin-left: auto;
margin-right: auto;
- display: block !important;
+ display: block;
`;
const LoadingItem = styled(List.Item)`
@@ -85,8 +88,10 @@ const LoadingItem = styled(List.Item)`
}
`;
+ /*For the person icon*/
const IconWrapper = styled.div`
margin-right: 8;
+ color: var(--description-color);
`;
const PaginationContainer = styled.div`
@@ -102,9 +107,9 @@ const PaginationButton = styled.button<{ active?: boolean; disabled?: boolean }>
min-width: 28px;
height: 28px;
padding: 0 8px;
- border: 1px solid ${props => props.active ? 'rgb(198, 198, 198 )' : '#d9d9d9'};
- background: ${props => props.active ? 'rgb(198, 198, 198 )' : props.disabled ? '#f5f5f5' : '#fff'};
- color: ${props => props.active ? '#333' : props.disabled ? '#bfbfbf' : 'rgba(0, 0, 0, 0.85)'};
+ border: 1px solid ${props => props.active ? 'var(--pagination-border-active)' : 'var(--pagination-border)'};
+ background: ${props => props.active ? 'var(--pagination-bg-active)' : props.disabled ? 'var(--pagination-bg-disabled)' : 'var(--pagination-bg)'};
+ color: ${props => props.active ? 'var(--pagination-text-active)' : props.disabled ? 'var(--pagination-text-disabled)' : 'var(--pagination-text)'};
border-radius: 2px;
cursor: ${props => props.disabled ? 'not-allowed' : 'pointer'};
font-family: var(--font-family);
@@ -115,9 +120,9 @@ const PaginationButton = styled.button<{ active?: boolean; disabled?: boolean }>
&:hover {
${props => !props.disabled && !props.active && css`
- border-color: rgb(198, 198, 198 );
- color: #333;
- background: #fafafa;
+ border-color: var(--pagination-hover-border);
+ color: var(--pagination-hover-text);
+ background: var(--pagination-hover-bg);
`}
}
@@ -126,6 +131,10 @@ const PaginationButton = styled.button<{ active?: boolean; disabled?: boolean }>
}
`;
+
+const EnrollmentText = styled.span<{ $color?: string }>`
+ color: ${(p) => p.$color || "var(--description-color)"};
+`;
// FIXME (median)
// const AverageWrapper = styled.div<{ average: number }>`
// color: ${(p) => getLetterGradeColor(getLetterGrade(p.average))};
@@ -213,7 +222,7 @@ export function SectionList({ loading, id, data, onClick, error, page, setPage }
const currentPageData = data.slice(startIndex, endIndex);
return (
- <>
+ <>
itemLayout="vertical"
size="large"
@@ -225,7 +234,7 @@ export function SectionList({ loading, id, data, onClick, error, page, setPage }
actions={[
}
- child={item.totalStudents.toString()}
+ child={{item.totalStudents.toString()} }
key="students-total"
/>,
// FIXME (median)
diff --git a/client/src/pages/_app.tsx b/client/src/pages/_app.tsx
index cbafa6f..51beddd 100644
--- a/client/src/pages/_app.tsx
+++ b/client/src/pages/_app.tsx
@@ -26,6 +26,18 @@ function MyApp({ Component, pageProps }: AppProps): JSX.Element {
return (
<>
+
+
diff --git a/client/src/pages/index.tsx b/client/src/pages/index.tsx
index 4769134..17295b6 100644
--- a/client/src/pages/index.tsx
+++ b/client/src/pages/index.tsx
@@ -1,7 +1,7 @@
import { Col } from "antd";
import Image from "next/image";
import Router from "next/router";
-import React from "react";
+import React, { useEffect, useState } from "react";
import styled from "styled-components";
import FadeIn from "../components/animations/FadeIn";
import Core from "../components/Core";
@@ -22,10 +22,10 @@ const Main = styled.div`
`;
const Header = styled.h1`
- font-family: var(--font-family);
+ font-family: Gilroy, sans-serif;
text-transform: uppercase;
text-align: center;
- color: rgb(78, 78, 78);
+ color: var(--header-color);
font-weight: 300;
letter-spacing: 3px;
font-size: 48px;
@@ -49,23 +49,32 @@ const Header = styled.h1`
`;
const Description = styled.p`
- font-family: var(--font-family);
+ font-family: 'Gilroy-Regular', sans-serif;
text-align: center;
- color: #95989a;
+ color: var(--description-color);
font-weight: 400;
font-size: 18px;
margin-bottom: 30px;
+
+ strong {
+ font-family: 'Gilroy-Bold', sans-serif;
+ }
`;
const HeaderBold = styled.span`
+ font-family: 'Gilroy-Bold', sans-serif;
font-weight: 700;
`;
+const HeaderLight = styled.span`
+ font-family: 'Gilroy-Light', sans-serif;
+ font-weight: 300;
+`;
const ByACM = styled.span`
font-size: 16px;
font-weight: 400;
letter-spacing: 1px;
- color: #95989a;
+ color: rgb(159, 159, 159);
margin-left: 12px;
@media (max-width: 768px) {
@@ -85,9 +94,102 @@ const Logo = {
flexShrink: 0,
};
+const ThemeToggle = styled.button`
+ position: absolute;
+ top: 20px;
+ right: 20px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 36px;
+ height: 36px;
+ border-radius: 9999px;
+ border: 1px solid var(--toggle-border, #e4e4e7);
+ background: var(--toggle-bg);
+ color: var(--text-color);
+ cursor: pointer;
+ transition: all 0.2s ease;
+
+ &:hover {
+ background: var(--toggle-hover-bg);
+ color: var(--toggle-hover-color, #333333);
+ }
+
+ svg {
+ width: 16px;
+ height: 16px;
+ }
+
+ @media (prefers-color-scheme: light) {
+ border-color: rgba(255, 255, 255, 0.1);
+ background: rgba(255, 255, 255, 0.05);
+
+ &:hover {
+ background: rgba(255, 255, 255, 0.1);
+ color: #727272;
+ }
+ }
+`;
+
+const SunIcon = () => (
+
+
+
+);
+
+const MoonIcon = () => (
+
+
+
+);
+
export default function Home() {
+ const [theme, setTheme] = useState<"light" | "dark">(() => {
+ try {
+ if (typeof window === "undefined") return "dark";
+ const saved = localStorage.getItem("theme");
+ if (saved === "light" || saved === "dark") return saved;
+ const docTheme = document.documentElement.getAttribute("data-theme");
+ if (docTheme === "light" || docTheme === "dark") return docTheme;
+ } catch (e) {
+ /* ignore */
+ }
+ return "dark";
+ });
+
+ useEffect(() => {
+ try {
+ document.documentElement.setAttribute("data-theme", theme);
+ localStorage.setItem("theme", theme);
+ } catch (e) {
+ /* ignore */
+ }
+ }, [theme]);
+
+ const toggleTheme = () => {
+ setTheme((prevTheme) => (prevTheme === "dark" ? "light" : "dark"));
+ };
+
function handleSubmit({ search }: SearchQuery) {
(async function () {
await Router.push({
@@ -98,22 +200,31 @@ export default function Home() {
}
return (
-
+
+
+ {theme === "dark" ? : }
+
- See how students did in any given class. And it's free, forever.
+ See how students did in any given class. And it's free, forever.
diff --git a/client/src/pages/results.tsx b/client/src/pages/results.tsx
index 004bfc0..bca3adf 100644
--- a/client/src/pages/results.tsx
+++ b/client/src/pages/results.tsx
@@ -23,7 +23,7 @@ export default function ResultsPage() {
// https://github.com/zeit/next.js/issues/8259
if (router.asPath !== router.route) {
return (
-
+
diff --git a/client/src/pages/styles.css b/client/src/pages/styles.css
index 752d7bc..ed88165 100644
--- a/client/src/pages/styles.css
+++ b/client/src/pages/styles.css
@@ -1,28 +1,132 @@
-@import url("https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300;0,400;0,600;0,700;0,800;1,300;1,400;1,600;1,700;1,800&display=swap");
-
-:root {
- --font-family: "Open Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu,
- Cantarell, "Helvetica Neue", sans-serif;
- --box-shadow-base: 0 1px 2px rgba(0, 0, 0, 0.07), 0 2px 4px rgba(0, 0, 0, 0.07),
- 0 4px 8px rgba(0, 0, 0, 0.07), 0 8px 16px rgba(0, 0, 0, 0.07), 0 16px 32px rgba(0, 0, 0, 0.07),
+@font-face {
+ font-family: 'Gilroy-Light';
+ src: url('/fonts/Gilroy-Light.ttf') format('truetype');
+ font-style: normal;
+ font-display: swap;
+}
+
+@font-face {
+ font-family: 'Gilroy-Regular';
+ src: url('/fonts/Gilroy-Regular.ttf') format('truetype');
+ font-style: normal;
+ font-display: swap;
+}
+
+@font-face {
+ font-family: 'Gilroy-SemiBold';
+ src: url('/fonts/Gilroy-SemiBold.ttf') format('truetype');
+ font-style: normal;
+ font-display: swap;
+}
+
+@font-face {
+ font-family: 'Gilroy-Bold';
+ src: url('/fonts/Gilroy-Bold.ttf') format('truetype');
+ font-style: normal;
+ font-display: swap;
+}
+
+:root {--font-family: var(--font-family);
+/* Defines global and dark mode theme variables (light mode is further down) */
+ /* Core colors */
+ --bg-color: #1B1B1B;
+ --text-color: #DFDFDF;
+ --muted-text: #ffffff86;
+ --link-color: #7f99e5;
+ --description-color: #95989a;
+ --header-color: #cfcfcf;
+
+ /* UI surfaces */
+ --card-bg: #333333;
+ --border-color: #ffffff14;
+ --select-tag: #cdcdcd;
+ --result-container-bg: #ffffff14;
+ --search-placeholder: #959595;
+ --section-card-shadow: 0 5px 5px rgba(21, 21, 21, 0.319);
+
+ /* Pagination */
+ --pagination-border: #d9d9d9;
+ --pagination-border-active: rgb(198, 198, 198);
+ --pagination-bg: #4d4d4d;
+ --pagination-bg-disabled: #282828;
+ --pagination-bg-active: #e5e5e5;
+
+ --pagination-text: #e0e0e0;
+ --pagination-text-disabled: #bfbfbf;
+ --pagination-text-active: #1a1a1a;
+
+ --pagination-hover-bg: #c3c3c3;
+ --pagination-hover-text: #333;
+ --pagination-hover-border: rgb(198, 198, 198);
+
+ /* Shadows */
+ --box-shadow-base:
+ 0 1px 2px rgba(0, 0, 0, 0.07),
+ 0 2px 4px rgba(0, 0, 0, 0.07),
+ 0 4px 8px rgba(0, 0, 0, 0.07),
+ 0 8px 16px rgba(0, 0, 0, 0.07),
+ 0 16px 32px rgba(0, 0, 0, 0.07),
0 32px 64px rgba(0, 0, 0, 0.07);
- --box-shadow-inactive: 0 1px 1px rgba(151, 151, 151, 0.11), 0 2px 2px rgba(151, 151, 151, 0.11),
- 0 4px 4px rgba(151, 151, 151, 0.11), 0 6px 8px rgba(151, 151, 151, 0.11),
+
+ --box-shadow-inactive:
+ 0 1px 1px rgba(151, 151, 151, 0.11),
+ 0 2px 2px rgba(151, 151, 151, 0.11),
+ 0 4px 4px rgba(151, 151, 151, 0.11),
+ 0 6px 8px rgba(151, 151, 151, 0.11),
0 8px 16px rgba(151, 151, 151, 0.11);
- --box-shadow-active: 0 2px 1px rgba(151, 151, 151, 0.09), 0 4px 2px rgba(151, 151, 151, 0.09),
- 0 8px 4px rgba(151, 151, 151, 0.09), 0 16px 8px rgba(151, 151, 151, 0.09),
+
+ --box-shadow-active:
+ 0 2px 1px rgba(151, 151, 151, 0.09),
+ 0 4px 2px rgba(151, 151, 151, 0.09),
+ 0 8px 4px rgba(151, 151, 151, 0.09),
+ 0 16px 8px rgba(151, 151, 151, 0.09),
0 32px 16px rgba(151, 151, 151, 0.09);
- --ant-wave-shadow-color: rgb(198, 198, 198);
- --antd-wave-shadow-color: rgb(198, 198, 198);
+
+ /* helper theme variables for components */
+ --card-hover-bg: #494949;
+ --tag-hover-bg: #1c1c1c;
+ --rmp-link-underline: #333333;
+}
+
+[data-theme="light"] {
+ --bg-color: #ffffff;
+ --text-color: #1a1a1a;
+ --muted-text: #555555;
+ --link-color: #1e40af;
+ --description-color: #95989a;
+ --header-color: #4e4e4e;
+
+
+ --card-bg: #ffffff;
+ --select-tag: #696969;
+ --border-color: rgba(0, 0, 0, 0.12);
+ --card-hover-bg: #e6e6e9;
+ --tag-hover-bg: #e6e6e9;
+ --rmp-link-underline: rgba(0,0,0,0.08);
+ --result-container-bg: #fdfdfdfd;
+ --search-placeholder: #c6c6c6;
+ --section-card-shadow: 0 15px 15px rgba(233, 233, 233, 0.7);
+
+ --pagination-border: rgba(0, 0, 0, 0.15);
+ --pagination-border-active: #e1e1e1;
+ --pagination-bg: #ffffff;
+ --pagination-bg-disabled: #f0f0f0;
+ --pagination-bg-active: #1a1a1a;
+
+ --pagination-text: #1a1a1a;
+ --pagination-text-disabled: #999999;
+ --pagination-text-active: #ffffff;
+
+ --pagination-hover-bg: #f5f5f5;
+ --pagination-hover-text: #1a1a1a;
+ --pagination-hover-border: #1a1a1a;
}
html,
body {
height: 100%;
-}
-
-body {
- font-family: sans-serif;
+ background-color: var(--bg-color);
+ color: var(--text-color);
margin: 0;
padding: 0;
}
@@ -39,6 +143,7 @@ body {
.ant-input {
border-radius: 20px 0 0 20px !important;
+ border: none !important;
}
.ant-input:hover,
@@ -52,88 +157,14 @@ body {
.ant-input-search-button {
border-radius: 0 20px 20px 0 !important;
- border-color: rgb(198, 198, 198 ) !important;
- border-width: 1px !important;
- color: #333 !important;
-}
-
-.ant-input-search-button:hover,
-.ant-input-search-button:focus {
- border-color: rgb(116, 116, 116) !important;
- color: #333 !important;
-}
-
-.ant-input-affix-wrapper:hover,
-.ant-input-affix-wrapper:focus-within,
-.ant-input-affix-wrapper-focused {
- border-color: rgb(198, 198, 198) !important;
- box-shadow: none !important;
-}
-
-.ant-btn {
- --ant-wave-shadow-color: rgb(198, 198, 198);
- --antd-wave-shadow-color: rgb(198, 198, 198);
-}
-
-.ant-btn:focus,
-.ant-btn:active,
-.ant-btn:focus-visible {
- outline: none !important;
- border-color: rgb(198, 198, 198) !important;
-}
-
-.ant-wave,
-.ant-wave-holder,
-.ant-click-animating-node,
-.ant-click-animating {
- --ant-wave-shadow-color: rgb(198, 198, 198) !important;
- --antd-wave-shadow-color: rgb(198, 198, 198) !important;
-}
-
-.ant-btn-primary,
-.ant-btn-primary:hover,
-.ant-btn-primary:focus {
- background-color: rgb(198, 198, 198 ) !important;
- border-color: rgb(198, 198, 198 ) !important;
- color: #333 !important;
-}
-
-.ant-menu-item-selected {
- background-color: rgb(198, 198, 198 ) !important;
-}
-
-.ant-menu-item-active {
- background-color: rgb(198, 198, 198 ) !important;
-}
-
-.ant-card-bordered {
- border: 1px solid rgb(198, 198, 198 ) !important;
-}
-
-.ant-select-item-option-selected:not(.ant-select-item-option-disabled) {
- background-color: rgb(198, 198, 198 ) !important;
-}
-
-.ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled) {
- background-color: rgb(198, 198, 198 ) !important;
- border-color: rgb(198, 198, 198 ) !important;
- color: #333 !important;
-}
-
-.ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):hover {
- background-color: rgb(217, 217, 217) !important;
- border-color: rgb(217, 217, 217) !important;
- color: #333 !important;
-}
-
-.ant-radio-button-wrapper:hover {
- color: #333 !important;
+ border: none !important;
}
-a {
- color: #333;
+.ant-list-item, .ant-list-item-meta, .ant-list-item-meta-title, .ant-list-item-meta-description {
+ color: #ffffff86;
}
-a:hover {
- color: #555;
+/* Ant Design default text color overrides for dark mode */
+.ant-typography, .ant-typography a, .ant-typography p {
+ color: #DFDFDF;
}
\ No newline at end of file