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" />

- ACM Dev Logo UTD Grades + ACM Dev Logo + 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" ? : } +
- ACM Dev Logo + ACM Dev Logo
- UTD GRADES - by ACM + UTD GRADES + by ACM Dev
- 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