diff --git a/web/src/FAQ/FAQ.css b/web/src/FAQ/FAQ.css index ee1b763..c7f1b95 100644 --- a/web/src/FAQ/FAQ.css +++ b/web/src/FAQ/FAQ.css @@ -1,28 +1,262 @@ -@import url(..//reset.css); -@import "tailwindcss"; +@import url(../reset.css); +@import url(../colors.css); +@import url(../glass.css); * { overscroll-behavior: none; } +/* Like the About page and unlike the single-screen homepage, this page scrolls. + The noise canvas is position: fixed, so it stays put behind the panels. */ body { font-family: "Azeret Mono"; margin: 0; - overflow: hidden; + overflow-x: hidden; + overflow-y: auto; } -#notication-text { +/* Sit above the fixed noise canvas (z-index: 0). */ +.faq { + position: relative; + z-index: 1; + max-width: 1100px; + margin: 0 auto; + padding: clamp(2rem, 6vh, 4rem) 5vw clamp(3rem, 8vh, 5rem); + display: flex; + flex-direction: column; + gap: clamp(1.25rem, 3vh, 2rem); +} + +/* --- Hero: the HELP label above the wordmark, matching the About page ------ */ + +.faq__head { + text-align: center; + display: flex; + flex-direction: column; + align-items: center; + gap: clamp(0.5rem, 2vh, 1.25rem); + margin-bottom: clamp(0.5rem, 2vh, 1.5rem); +} + +.faq__eyebrow { font-family: "Monomaniac One"; - font-size: 96px; + font-size: clamp(24px, 4vh, 40px); text-transform: uppercase; + letter-spacing: 0.35em; + /* letter-spacing pads the right edge; pull it back to stay optically centered */ + text-indent: 0.35em; + color: var(--color-white); +} + +/* Same size as the About page's wordmark so the two read as sibling pages. */ +#logo-faq { + width: min(92vw, 620px); +} + +.faq__tagline { + max-width: 46ch; + line-height: 1.6; + color: var(--color-white); + opacity: 0.85; } -.bottom-page { - width: 100vw; - height: 94vh; +/* --- Panels --------------------------------------------------------------- */ + +.faq__panel { + padding: clamp(1.5rem, 3.5vh, 2.25rem) clamp(1.25rem, 3vw, 2rem); + display: flex; + flex-direction: column; + gap: 1rem; +} + +.faq__h2 { + font-family: "Monomaniac One"; + font-size: clamp(22px, 3.2vh, 30px); + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--color-white); + padding-bottom: 0.6rem; + border-bottom: 2px solid var(--color-process-cyan); + /* Let the accent rule stop at the text rather than span the panel. */ + align-self: flex-start; +} + +/* --- Accordion ------------------------------------------------------------ */ + +.faq__items { + display: flex; + flex-direction: column; +} + +/* Hairline separators between rows, but not above the first one. */ +.faq__item + .faq__item { + border-top: 1px solid rgba(255, 255, 255, 0.12); +} + +.faq__q { + width: 100%; + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 1rem; + padding: 1rem 0.5rem; + background: none; + border: none; + border-radius: 10px; + cursor: pointer; + text-align: left; + font-family: "Azeret Mono"; + font-size: 1rem; + line-height: 1.5; + color: var(--color-white); + transition: background 0.15s ease; +} + +.faq__q:hover { + background: rgba(255, 255, 255, 0.06); +} + +/* Nothing else in the app styles focus, and this page is meant to be usable + from the keyboard, so make the ring unmistakable. */ +.faq__q:focus-visible { + outline: 2px solid var(--color-process-cyan); + outline-offset: 2px; +} + +.faq__q-arrow { + flex: none; + color: var(--color-process-cyan); + /* Nudge onto the first line of a question that wraps to two. */ + line-height: 1.5; +} + +.faq__a { + display: flex; + flex-direction: column; + gap: 0.75rem; + padding: 0 0.5rem 1.25rem; + line-height: 1.65; + opacity: 0.92; +} + +/* reset.css strips list styling globally, so put it back inside answers. */ +.faq__a ul { + list-style: disc outside; + padding-left: 1.5rem; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.faq__a code { + font-family: "Azeret Mono"; + font-size: 0.875em; + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.14); + border-radius: 6px; + padding: 0.1em 0.4em; + /* Long inline code (the special-character list) must not widen the page. + When it does wrap, give each fragment its own border and rounding rather + than leaving the chip sliced open at the line break. */ + overflow-wrap: break-word; + -webkit-box-decoration-break: clone; + box-decoration-break: clone; +} + +.faq__a strong { + font-weight: 700; + color: var(--color-white); +} + +.faq__a em { + font-style: italic; +} + +/* Caveats and gotchas: a cyan edge marks "read this bit" without shouting. */ +.faq__a .faq__warn { + border-left: 3px solid var(--color-process-cyan); + padding-left: 0.85rem; +} + +/* --- Expand all ----------------------------------------------------------- */ + +.faq__controls { + display: flex; + justify-content: flex-end; +} + +.faq__toggle-all { + background: none; + border: none; + padding: 0.25rem; + cursor: pointer; + font-family: "Azeret Mono"; + font-size: 0.875rem; + /* This control sits on the bare noise field rather than on a glass panel, + where cyan-on-cyan gets thin; the dark halo keeps it readable. */ + color: var(--color-white); + text-decoration: underline; + text-shadow: 0 1px 4px rgba(0, 0, 0, 0.8); +} + +.faq__toggle-all:hover { + color: var(--color-aero-blue); +} + +.faq__toggle-all:focus-visible { + outline: 2px solid var(--color-process-cyan); + outline-offset: 2px; +} + +/* --- Closing note and footer ---------------------------------------------- */ + +.faq__note { + max-width: 70ch; + margin: 0 auto; + text-align: center; + line-height: 1.65; + font-size: 0.9375rem; + color: var(--color-white); + opacity: 0.8; +} + +.faq__footer { display: flex; flex-direction: column; - flex-wrap: wrap; align-items: center; + gap: 1.5rem; + padding-top: clamp(1rem, 3vh, 2rem); +} + +.faq__cta { + text-decoration: none; +} + +.faq__cta .blueButton { + padding: 0.9rem 2.75rem; +} + +.faq__links { + display: flex; + gap: 1rem; + flex-wrap: wrap; justify-content: center; -} \ No newline at end of file +} + +/* --- Narrow screens ------------------------------------------------------- */ + +@media (max-width: 800px) { + .faq__eyebrow { + letter-spacing: 0.2em; + text-indent: 0.2em; + } + + .faq__q { + font-size: 0.9375rem; + padding: 0.875rem 0.25rem; + } + + .faq__a { + padding: 0 0.25rem 1rem; + } +} diff --git a/web/src/FAQ/FAQ.html b/web/src/FAQ/FAQ.html index d19af5a..6c83dac 100644 --- a/web/src/FAQ/FAQ.html +++ b/web/src/FAQ/FAQ.html @@ -3,14 +3,14 @@ - + - UDCS Autograder + Help / FAQ · UDCS Autograder
diff --git a/web/src/FAQ/FAQ.tsx b/web/src/FAQ/FAQ.tsx index 9321711..0acba82 100644 --- a/web/src/FAQ/FAQ.tsx +++ b/web/src/FAQ/FAQ.tsx @@ -1,14 +1,579 @@ -import { StrictMode } from 'react' +import { StrictMode, useEffect, useState } from 'react' import { createRoot } from 'react-dom/client' import Navbar from "../components/navbar/Navbar" +import Logo from "../components/logo/Logo" +import NoiseBackground from "../components/background/NoiseBackground" +import BlueButton from "../components/buttons/BlueButton" +import FaqItem from './FaqItem' import './FAQ.css' +/* Answers are JSX rather than strings because several need inline code, lists, + or more than one paragraph. Sections render in order as glass panels. */ +const SECTIONS = [ + { + title: "Getting started", + items: [ + { + q: "I got an email inviting me to Autograder. What is this?", + a: <> +

+ Autograder is the platform your instructor uses for programming + assignments. You write your solution in the browser, submit it, and it + is run against the instructor's test cases and graded automatically — + usually within seconds. +

+

+ The email means an instructor has added you to one of their courses. + Follow the link in it to create your account, and your classes will + already be waiting for you. +

+ , + }, + { + q: "How do I get an account?", + a: <> +

+ You cannot sign yourself up. An instructor or an administrator invites + you by email address, and that email contains a link that creates your + account. +

+

+ Invitation links are valid for 7 days. If yours has + expired, ask your instructor to have a new one sent. +

+ , + }, + { + q: "My invitation link says \"Invalid invite\".", + a: <> +

That means the link is no longer usable. The usual reasons:

+ +

Ask your instructor to check with an administrator and re-issue it.

+ , + }, + { + q: "What are the password rules?", + a: <> +

+ Between 8 and 64 characters, with at least one uppercase letter, one + lowercase letter, one digit, and one special character. +

+

+ The accepted special characters are exactly these:{" "} + ! @ # ~ $ % ^ & * ( ) _ + {"{"} {"}"} " : ; ' ? / > . < , +

+

+ Watch out: - = [ ]{" "} + \ | and ` do not{" "} + count as special characters. A password like Passw0rd- will + be rejected even though it looks like it should qualify. +

+ , + }, + { + q: "I forgot my password.", + a: <> +

+ Use Forgot Password? on the sign-in page. If you are + already signed in, there is a Reset Password button in + Account Settings. +

+

+ You will get an email with a reset link. It is valid for 7 days and can + only be used once. For your privacy the confirmation message looks the + same whether or not an account exists for that address, so it is not a + way to check whether someone is registered. +

+ , + }, + { + q: "Why was I signed out?", + a:

+ Sessions are short-lived, so a tab left open for a while will ask you to + sign in again. Your work is not lost — code you have typed is saved + automatically as you go. +

, + }, + ], + }, + { + title: "Using Autograder as a student", + items: [ + { + q: "How do I join a class?", + a: <> +

+ You don't, and there is no join code to enter. Your + instructor adds you to the class by email address, and it appears on + your dashboard automatically. +

+

+ If a class is missing, the usual cause is an address mismatch: your + enrolment only connects if you registered with the exact address your + instructor typed. Check with them which one they used. +

+ , + }, + { + q: "Where are my assignments?", + a: <> +

+ Your dashboard groups your classes into Enrolled (term + hasn't started), Active, and{" "} + Expired, based on the dates your instructor set. Open a + class to see its assignments, and click one to open its questions. +

+

+ You only see assignments the instructor has published. Anything still in + draft is invisible to you. +

+ , + }, + { + q: "Do I have to save my work?", + a: <> +

+ No — there is no Save button. Your code is saved automatically a few + seconds after you stop typing, and periodically while you type. + Submitting saves it too. +

+

+ There is no "saved" indicator, so give it a moment before closing the + tab on something you just finished typing. +

+ , + }, + { + q: "Can I run my code before submitting it?", + a:

+ Not currently. Submitting is the only way to run your code, and there is + no scratch pad for trying your own inputs. Since there is no limit on + submissions, submitting is how you test — but you will get more + out of it by running your code locally first. +

, + }, + { + q: "Can I choose which language to use?", + a:

+ No. Each question is set to one language by your instructor, shown above + the editor as Language: python. Autograder supports Python, + C, Java, and Racket, but which one a given question uses is not your + choice. +

, + }, + { + q: "How many times can I submit?", + a: <> +

As many times as you like. There is no attempt limit and no cooldown.

+

+ But each submission replaces your previous result, + including a worse one. Autograder does not keep your best attempt, so + don't submit an experiment you haven't checked. +

+ , + }, + { + q: "What does \"Reset Starter Code\" do?", + a:

+ It replaces everything in the editor with the starter code your + instructor provided — immediately, with no confirmation prompt. Your work + is not recoverable afterwards, so copy it somewhere first if you might + want it back. +

, + }, + { + q: "Can I see my earlier submissions?", + a:

+ Not at the moment. You see the score and feedback from your most recent + submission. Your instructor can see the full history of your attempts, so + ask them if you need to know what an earlier one scored. +

, + }, + { + q: "What happens if I submit after the deadline?", + a: <> +

+ Late submissions are accepted and graded normally. Nothing is blocked, + and Autograder does not deduct any marks by itself — but your instructor + sees the submission flagged as late, and how they handle that is up to + them. +

+

+ Assignments are due at 11:59:59 pm US Central time on + the date shown in your class sidebar. +

+ , + }, + ], + }, + { + title: "Understanding your results", + items: [ + { + q: "How is my code graded?", + a:

+ Each test case runs your program once, feeding that test case's input to + it on standard input, then compares what your program prints to the + expected output. Each test case is worth points, and your score for the + question is the total of the ones that pass. +

, + }, + { + q: "What counts as matching output?", + a: <> +

Before comparing, Autograder tidies both sides a little. It ignores:

+ +

Everything else has to match exactly. In particular these all matter:

+ + , + }, + { + q: "My output looks right but the test still failed.", + a: <> +

Three causes account for almost all of these:

+ + , + }, + { + q: "What does a hidden test case show me?", + a: <> +

+ Only the verdict and the points, like{" "} + Test "edge cases" (hidden): FAILED (0/10 points). The + input, the expected output, and your output are all withheld. +

+

+ One consequence worth knowing: if every test case on a question is + hidden, error messages and compile errors are withheld too, so a failing + submission gives you nothing to go on. If you are stuck with no + explanation at all, ask your instructor to make one test case visible. +

+ , + }, + { + q: "Why does it say Timeout?", + a:

+ Your program ran longer than the time limit for that test case. Each test + case has its own limit, set by your instructor, so a slow or looping + program only loses the cases it actually hits — the rest still run and + still count. +

, + }, + { + q: "Are there gotchas for particular languages?", + a: <> + + , + }, + { + q: "My submission says \"error\". What did I do?", + a:

+ Nothing — that status means the grader itself could not run your + submission, most often because the question has no test cases yet. It is + not a verdict on your code. Let your instructor know. +

, + }, + ], + }, + { + title: "For instructors and TAs", + items: [ + { + q: "How do I create a course?", + a:

+ Use + Create New Classroom on your dashboard — available + to instructors and administrators. You set a name, a course code (up to 16 + characters), start and end dates, and a description. A blank assignment is + created alongside it so you have somewhere to start. +

, + }, + { + q: "How do I add students?", + a: <> +

+ Manage Classroom → Students →{" "} + + Add Student, then type an email address and save. + Students who don't have an account yet are invited automatically and + join the class the moment they register, so you can build a full roster + before term starts. +

+

+ Two limitations to plan around: it is one address at a time — there is + no CSV import or bulk paste — and a pending enrolment only connects if + the student registers with the exact address you entered. +

+ , + }, + { + q: "How do I add a co-instructor?", + a:

+ Not through the interface, currently. The classroom roster only offers the + student and assistant roles. Granting someone the instructor role has to + be done by an administrator through the API. +

, + }, + { + q: "What can a TA (assistant) do?", + a:

+ Less than intended, at the moment. Assistants are granted most instructor + permissions by the server, but the Manage Classroom screens are currently + restricted to instructors and administrators — so in practice an assistant + cannot manage a classroom today. Note also that assistants appear in the + gradebook alongside students. +

, + }, + { + q: "How do I publish an assignment?", + a:

+ Each assignment has a visibility setting: Draft or{" "} + Visible. Draft assignments are invisible to students and + cannot be opened even with a direct link, so you can build one in the open + without anyone seeing it. Switch it to Visible to release it. +

, + }, + { + q: "How do points work?", + a:

+ Points are set per test case, and a question is worth the + total of its test cases. There is no separate question-level points field. + A question with no test cases is worth nothing and will report an error + when a student submits, so add at least one before publishing. +

, + }, + { + q: "Do I have to type the expected output by hand?", + a: <> +

+ No. Write a reference solution in the Solution tab, + then use Generate Expected Output to run it against a + test case's input and fill in the expected output for you. It asks + before overwriting anything you have already entered. +

+

+ Run Tests on Solution checks your solution against every + test case at once — a good sanity check before publishing. Neither + button touches student grades, though both do save the question. +

+ , + }, + { + q: "Can I export grades to a spreadsheet?", + a:

+ Not currently — there is no CSV or spreadsheet export. Grades are viewable + in Manage Classroom → Grades, which you can pivot by + assignment or by student. +

, + }, + { + q: "Can I change a grade by hand?", + a:

+ Yes. On a student's row, tick Manual Grade, enter a score, + and press Update Grade. The automatically computed score + is kept underneath, so unticking the box restores it — an override is never + destructive. +

, + }, + { + q: "Can I re-run grading for a student?", + a:

+ Yes, with Resubmit Code on their row. It re-grades their + current code and updates the score, but deliberately does not add an entry + to their submission history or flag them as late, so your re-runs never + look like their attempts. +

, + }, + { + q: "Can I duplicate an assignment or copy one between classes?", + a:

+ Not yet. Test cases can be copied within a question, but there is no + duplicate for assignments or questions, no copying between classrooms, and + no assignment templates. +

, + }, + { + q: "Can I write shell-script test cases?", + a:

+ No. Autograder currently supports one kind of test case: supply input, + compare the program's output to what you expect. Multi-line input is fine, + and each test case has its own points, time limit, and hidden setting. +

, + }, + { + q: "Does Autograder email students about anything else?", + a:

+ No. It sends exactly two kinds of email: account invitations and password + resets. There are no deadline reminders, no "your submission has been + graded" notices, and no grade notifications — so tell your class where to + look rather than expecting Autograder to nudge them. +

, + }, + { + q: "What can't I delete?", + a: <> +

+ An assignment must always have at least one question, and a question at + least one test case, so the delete option is unavailable when only one + is left. The same applies to the last assignment in a classroom. +

+

+ Deleting an assignment permanently destroys the code, grades, and + submission history your students accumulated under it. There is no undo. +

+ , + }, + ], + }, +]; + +const ALL_KEYS = SECTIONS.flatMap((section, s) => section.items.map((_, i) => `${s}-${i}`)); + +function Faq() { + const [loggedIn, setLoggedIn] = useState(false); + const [openKeys, setOpenKeys] = useState>(new Set()); + + useEffect(() => { + let alive = true; + (async () => { + try { + const response = await fetch('/api/auth/valid_login'); + if (response.ok) { + const json = await response.json(); + if (alive) setLoggedIn(json['message'] === 'true'); + } + } catch (err) { + console.error("Fetch error: ", err); + } + })(); + return () => { alive = false; }; + }, []); + + const toggle = (key: string) => { + setOpenKeys(prev => { + const next = new Set(prev); + if (next.has(key)) next.delete(key); else next.add(key); + return next; + }); + }; + + // Expanding everything is what keeps the browser's own find-in-page useful. + const allOpen = openKeys.size === ALL_KEYS.length; + const toggleAll = () => setOpenKeys(allOpen ? new Set() : new Set(ALL_KEYS)); + + return ( + <> + + +
+
+

Help

+
+ +
+

+ Answers to the questions we get asked most. If yours isn't here, ask + your instructor. +

+
+ +
+ +
+ + {SECTIONS.map((section, s) => ( +
+

{section.title}

+
+ {section.items.map((item, i) => { + const key = `${s}-${i}`; + return ( + toggle(key)} + > + {item.a} + + ); + })} +
+
+ ))} + +

+ Still stuck? Ask your instructor — they can see your submissions and + test results in full detail, including the parts hidden from you. + Instructors: contact your department administrator. +

+ + +
+ + ); +} + createRoot(document.getElementById('root')!).render( - -
-

Page under development

- Test page -
+
, -) \ No newline at end of file +) diff --git a/web/src/FAQ/FaqItem.tsx b/web/src/FAQ/FaqItem.tsx new file mode 100644 index 0000000..a9827c9 --- /dev/null +++ b/web/src/FAQ/FaqItem.tsx @@ -0,0 +1,36 @@ +import { useId } from "react"; + +/* One question/answer row. Open state is owned by the page rather than this + component so the "Expand all" control can drive every row at once. + + A real + {open &&
{children}
} + + ); +} + +export default FaqItem; diff --git a/web/src/Index.tsx b/web/src/Index.tsx index a4fed86..1b046ac 100644 --- a/web/src/Index.tsx +++ b/web/src/Index.tsx @@ -1,18 +1,64 @@ -import { StrictMode } from 'react' +import { StrictMode, useEffect, useState } from 'react' import { createRoot } from 'react-dom/client' import Navbar from "./components/navbar/Navbar" import Logo from "./components/logo/Logo" +import NoiseBackground from "./components/background/NoiseBackground" +import BlueButton from "./components/buttons/BlueButton" import './index.css' +function Home() { + const [loggedIn, setLoggedIn] = useState(false); + + useEffect(() => { + let alive = true; + (async () => { + try { + const response = await fetch('/api/auth/valid_login'); + if (response.ok) { + const json = await response.json(); + if (alive) setLoggedIn(json['message'] === 'true'); + } + } catch (err) { + console.error("Fetch error: ", err); + } + })(); + return () => { alive = false; }; + }, []); + + return ( + <> + + +
+
+

Welcome to

+
+ +
+
+ +
+

+ Autograder is an automated code-grading platform for the classroom. + Instructors build assignments with custom test cases; students submit + solutions in the language of their choice and get instant, consistent + feedback and grades. +

+ + {loggedIn ? "Go to Dashboard" : "Login"} + + +
+
+ + ); +} + createRoot(document.getElementById('root')!).render( - -
-

Welcome to

-
- -
- Test page -
+
, ) diff --git a/web/src/about/About.tsx b/web/src/about/About.tsx index c2f3aff..42c4171 100644 --- a/web/src/about/About.tsx +++ b/web/src/about/About.tsx @@ -1,14 +1,203 @@ -import { StrictMode } from 'react' +import { StrictMode, useEffect, useState } from 'react' import { createRoot } from 'react-dom/client' import Navbar from "../components/navbar/Navbar" +import Logo from "../components/logo/Logo" +import NoiseBackground from "../components/background/NoiseBackground" +import BlueButton from "../components/buttons/BlueButton" import './about.css' +const LANGUAGES = [ + { name: "Python", detail: "Run on Python 3." }, + { name: "C", detail: "Compiled with gcc -O2, linked against libm." }, + { name: "Java", detail: "Compiled and run on OpenJDK 17." }, + { name: "Racket", detail: "Run on Racket — with a #lang line added if you forget one." }, +]; + +const STEPS = [ + { + title: "Submit", + body: "Your code is saved against the question and the grader is queued. " + + "Nothing is scheduled behind a shared job queue you have to wait your turn in.", + }, + { + title: "Isolate", + body: "Each submission runs in its own disposable container as an unprivileged " + + "user, in a scratch workspace that is destroyed afterwards. Student code runs " + + "with a minimal, allow-listed environment, so it can never read the platform's " + + "own credentials.", + }, + { + title: "Run", + body: "Every test case gets its own time limit, set by the instructor, plus a hard " + + "cap on how much output it can produce. Programs that hang are cut off and " + + "reported as timeouts instead of taking the platform down with them.", + }, + { + title: "Report", + body: "Output is normalized for line endings and trailing whitespace before it is " + + "compared, so a stray newline never costs anyone a point. Each test case is " + + "scored, and the results roll up into a grade.", + }, +]; + +function About() { + const [loggedIn, setLoggedIn] = useState(false); + + useEffect(() => { + let alive = true; + (async () => { + try { + const response = await fetch('/api/auth/valid_login'); + if (response.ok) { + const json = await response.json(); + if (alive) setLoggedIn(json['message'] === 'true'); + } + } catch (err) { + console.error("Fetch error: ", err); + } + })(); + return () => { alive = false; }; + }, []); + + return ( + <> + + +
+
+

About

+
+ +
+

+ An automated code-grading platform, built at the University of Dallas + by the students who use it. +

+
+ +
+

Why we built it

+

+ Automated grading is only worth having if you can trust it. UD's computer + science courses have leaned on outside grading platforms before, and when + one of those platforms changes its pricing, changes its rules, or simply + goes away, the coursework built on top of it goes with it. +

+

+ Autograder is the alternative: a grading platform the department runs + itself, on its own terms, with no vendor standing between an instructor + and their students' work. If something needs to change, the people who + need it changed are the people who wrote it. +

+
+ +
+
+

For instructors

+

+ Set up a classroom with a course code, description, and term dates, + then build the roster — students can be enrolled before they + have even made an account. Assignments stay in draft until you make + them visible, so nothing goes live before you are ready, and deadlines + are stored as real instants in the course's own timezone rather than + whatever the server happens to be set to. +

+

+ Every question carries its own starter code and reference solution, and + its points are set per test case. Run your solution against the test + cases and Autograder fills + in the expected output for you. Grades roll up per student and per + assignment, late submissions are flagged automatically, and any grade + can be overridden by hand when a submission deserves it. +

+
+ +
+

For students

+

+ Write your solution in the browser — a real code editor, with + syntax highlighting for every supported language — and submit. + Grading starts immediately and the verdict appears as soon as it lands: + passed, partial, or failed, with per-test-case feedback that tells you + what actually went wrong. +

+

+ Hidden test cases stay hidden. You see the verdict and the points, not + the instructor's inputs. Every attempt you make is kept, so your + instructor can see how a solution got to where it is. +

+
+
+ +
+

How grading works

+
    + {STEPS.map((step, i) => ( +
  1. + +
    +

    {step.title}

    +

    {step.body}

    +
    +
  2. + ))} +
+
+ +
+

Four languages

+

+ One platform for the whole sequence, from a first intro course to the + harder ones later on. +

+
    + {LANGUAGES.map((lang) => ( +
  • + {lang.name} + {lang.detail} +
  • + ))} +
+

+ Racket support meant more than picking a compiler: the browser editor had + no Racket mode, so we wrote one. +

+
+ +
+

Built by UD students

+

+ Autograder has been built by University of Dallas computer science + students since September 2024 — the same students who submit to it. + Sitting on both sides of the grader tends to sharpen your opinions about + how it should behave. +

+

+ It ships as a single self-contained binary: a Go backend on PostgreSQL, + with the React front end embedded directly in the executable, so deploying + it is copying one file. Accounts are invitation-only and sessions are + tracked server-side, so a classroom stays a classroom. +

+
+ + +
+ + ); +} + createRoot(document.getElementById('root')!).render( - -
-

Page under development

- Test page -
+
, -) \ No newline at end of file +) diff --git a/web/src/about/about.css b/web/src/about/about.css index ee1b763..9bd9102 100644 --- a/web/src/about/about.css +++ b/web/src/about/about.css @@ -1,28 +1,203 @@ -@import url(..//reset.css); -@import "tailwindcss"; +@import url(../reset.css); +@import url(../colors.css); +@import url(../glass.css); * { overscroll-behavior: none; } +/* Unlike the single-screen homepage, this page scrolls. The noise canvas is + position: fixed, so it stays put behind the panels while it does. */ body { font-family: "Azeret Mono"; margin: 0; - overflow: hidden; + overflow-x: hidden; + overflow-y: auto; } -#notication-text { +/* Sit above the fixed noise canvas (z-index: 0). */ +.about { + position: relative; + z-index: 1; + max-width: 1100px; + margin: 0 auto; + padding: clamp(2rem, 6vh, 4rem) 5vw clamp(3rem, 8vh, 5rem); + display: flex; + flex-direction: column; + gap: clamp(1.25rem, 3vh, 2rem); +} + +/* --- Hero: the ABOUT label above the wordmark ------------------------------ */ + +.about__head { + text-align: center; + display: flex; + flex-direction: column; + align-items: center; + gap: clamp(0.5rem, 2vh, 1.25rem); + margin-bottom: clamp(0.5rem, 2vh, 1.5rem); +} + +.about__eyebrow { font-family: "Monomaniac One"; - font-size: 96px; + font-size: clamp(24px, 4vh, 40px); text-transform: uppercase; + letter-spacing: 0.35em; + /* letter-spacing pads the right edge; pull it back to stay optically centered */ + text-indent: 0.35em; + color: var(--color-white); +} + +/* Narrower than the homepage's 920px so the wordmark reads as a section + header rather than a landing splash. Glow comes from .wordmark. */ +#logo-about { + width: min(92vw, 620px); +} + +.about__tagline { + max-width: 46ch; + line-height: 1.6; + color: var(--color-white); + opacity: 0.85; } -.bottom-page { - width: 100vw; - height: 94vh; +/* --- Panels --------------------------------------------------------------- */ + +.about__panel { + padding: clamp(1.5rem, 3.5vh, 2.25rem) clamp(1.5rem, 4vw, 2.5rem); + display: flex; + flex-direction: column; + gap: 1rem; +} + +.about__panel p { + line-height: 1.65; +} + +.about__h2 { + font-family: "Monomaniac One"; + font-size: clamp(22px, 3.2vh, 30px); + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--color-white); + padding-bottom: 0.6rem; + border-bottom: 2px solid var(--color-process-cyan); + /* Let the accent rule stop at the text rather than span the panel. */ + align-self: flex-start; +} + +/* Two panels side by side; collapses at the breakpoint below. Left to stretch + so the pair reads as matched cards rather than two ragged-bottom columns. */ +.about__row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: clamp(1.25rem, 3vh, 2rem); +} + +.about__note { + font-size: 0.875rem; + opacity: 0.75; +} + +/* --- "How grading works" numbered flow ------------------------------------ */ + +.about__steps { + list-style: none; + display: flex; + flex-direction: column; + gap: 1.25rem; +} + +.about__step { + display: grid; + grid-template-columns: auto 1fr; + gap: 1.25rem; + align-items: start; +} + +.about__step-num { + font-family: "Monomaniac One"; + font-size: clamp(28px, 4vh, 40px); + line-height: 1; + color: var(--color-process-cyan); +} + +.about__step-title { + font-family: "Monomaniac One"; + font-size: 1.15rem; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--color-white); + margin-bottom: 0.35rem; +} + +/* --- Language tiles ------------------------------------------------------- */ + +.about__langs { + list-style: none; + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 1rem; +} + +.about__lang { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 1rem; + border: 1px solid rgba(255, 255, 255, 0.16); + border-radius: 14px; + background: rgba(255, 255, 255, 0.06); +} + +.about__lang-name { + font-family: "Monomaniac One"; + font-size: 1.5rem; + text-transform: uppercase; + color: var(--color-aero-blue); +} + +.about__lang-detail { + font-size: 0.8125rem; + line-height: 1.5; + opacity: 0.85; +} + +/* --- Footer CTA ----------------------------------------------------------- */ + +.about__footer { display: flex; flex-direction: column; - flex-wrap: wrap; align-items: center; + gap: 1.5rem; + padding-top: clamp(1rem, 3vh, 2rem); +} + +.about__cta { + text-decoration: none; +} + +.about__cta .blueButton { + padding: 0.9rem 2.75rem; +} + +.about__links { + display: flex; + gap: 1rem; + flex-wrap: wrap; justify-content: center; -} \ No newline at end of file +} + +/* --- Narrow screens ------------------------------------------------------- */ + +@media (max-width: 800px) { + .about__row, + .about__langs { + grid-template-columns: 1fr; + } + + .about__eyebrow { + letter-spacing: 0.2em; + text-indent: 0.2em; + } +} diff --git a/web/src/about/about.html b/web/src/about/about.html index 775a23b..4acfd28 100644 --- a/web/src/about/about.html +++ b/web/src/about/about.html @@ -10,7 +10,7 @@ - UDCS Autograder + About Us · UDCS Autograder
diff --git a/web/src/components/background/NoiseBackground.css b/web/src/components/background/NoiseBackground.css new file mode 100644 index 0000000..e4b43a6 --- /dev/null +++ b/web/src/components/background/NoiseBackground.css @@ -0,0 +1,16 @@ +.noise-bg { + position: fixed; + inset: 0; + width: 100vw; + height: 100vh; + z-index: 0; + display: block; + pointer-events: none; +} + +/* Fallback when WebGL is unavailable: the signature brand gradient. */ +.noise-bg--fallback { + background: linear-gradient(135deg, + var(--color-process-cyan) 0%, + var(--color-raisin-black) 100%); +} diff --git a/web/src/components/background/NoiseBackground.tsx b/web/src/components/background/NoiseBackground.tsx new file mode 100644 index 0000000..26c27b1 --- /dev/null +++ b/web/src/components/background/NoiseBackground.tsx @@ -0,0 +1,276 @@ +import { useEffect, useRef, useState } from "react"; +import "./NoiseBackground.css"; + +const VERT = ` +attribute vec2 aPos; +void main() { gl_Position = vec4(aPos, 0.0, 1.0); } +`; + +const FRAG = ` +#ifdef GL_FRAGMENT_PRECISION_HIGH +precision highp float; +#else +precision mediump float; +#endif + +uniform vec2 uResolution; +uniform float uTime; +uniform vec2 uMouse; // eased cursor, 0..1 +uniform float uReducedMotion; // 0.0 or 1.0 + +// --- Ashima simplex noise (public domain) --- +vec3 mod289(vec3 x){ return x - floor(x * (1.0 / 289.0)) * 289.0; } +vec2 mod289(vec2 x){ return x - floor(x * (1.0 / 289.0)) * 289.0; } +vec3 permute(vec3 x){ return mod289(((x * 34.0) + 1.0) * x); } + +float snoise(vec2 v){ + const vec4 C = vec4(0.211324865405187, 0.366025403784439, + -0.577350269189626, 0.024390243902439); + vec2 i = floor(v + dot(v, C.yy)); + vec2 x0 = v - i + dot(i, C.xx); + vec2 i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0); + vec4 x12 = x0.xyxy + C.xxzz; + x12.xy -= i1; + i = mod289(i); + vec3 p = permute(permute(i.y + vec3(0.0, i1.y, 1.0)) + + i.x + vec3(0.0, i1.x, 1.0)); + vec3 m = max(0.5 - vec3(dot(x0, x0), dot(x12.xy, x12.xy), + dot(x12.zw, x12.zw)), 0.0); + m = m * m; m = m * m; + vec3 x = 2.0 * fract(p * C.www) - 1.0; + vec3 h = abs(x) - 0.5; + vec3 ox = floor(x + 0.5); + vec3 a0 = x - ox; + m *= 1.79284291400159 - 0.85373472095314 * (a0 * a0 + h * h); + vec3 g; + g.x = a0.x * x0.x + h.x * x0.y; + g.yz = a0.yz * x12.xz + h.yz * x12.yw; + return 130.0 * dot(m, g); +} + +float fbm(vec2 p){ + float sum = 0.0, amp = 0.5, freq = 1.0; + for (int i = 0; i < 5; i++){ + sum += amp * snoise(p * freq); + freq *= 2.0; + amp *= 0.5; + } + return sum; +} + +void main(){ + vec2 uv = gl_FragCoord.xy / uResolution.xy; + float aspect = uResolution.x / uResolution.y; + vec2 p = vec2(uv.x * aspect, uv.y); + vec2 m = vec2(uMouse.x * aspect, uMouse.y); + + float t = uTime * (uReducedMotion > 0.5 ? 0.0 : 0.018); + + // (1) gentle parallax drift toward the cursor + p += (uMouse - 0.5) * 0.2; + + // time-animated domain warp + vec2 q = vec2(fbm(p + vec2(0.0, t)), + fbm(p + vec2(5.2, 1.3) - t * 0.5)); + + // (2) subtle localized lens around the cursor + float d = distance(p, m); + float pull = exp(-d * d * 3.0); + vec2 warp = q * (0.5 + 0.5 * pull); + warp += (p - m) * 0.1 * pull; + + float n = fbm(p * 1.4 + warp + vec2(t * 0.25, 0.0)); + n = n * 0.5 + 0.5; + + // brand palette: raisin-black -> NCS blue -> process cyan -> aero blue + vec3 c0 = vec3(0.1176, 0.1294, 0.1686); // #1E212B + vec3 c1 = vec3(0.0784, 0.5059, 0.7294); // #1481BA + vec3 c2 = vec3(0.0471, 0.6667, 0.8627); // #0CAADC + vec3 c3 = vec3(0.0667, 0.7098, 0.8941); // #11B5E4 + + vec3 col = mix(c0, c1, smoothstep(0.00, 0.40, n)); + col = mix(col, c2, smoothstep(0.35, 0.70, n)); + col = mix(col, c3, smoothstep(0.70, 1.00, n)); + + // (3) small spotlight lift toward the brightest blue near the cursor + col = mix(col, c3, pull * 0.2); + + // vignette + overall darken so foreground text stays legible + float vig = smoothstep(1.2, 0.2, distance(uv, vec2(0.5))); + col *= mix(0.7, 1.0, vig); + col *= 0.85; + + gl_FragColor = vec4(col, 1.0); +} +`; + +function compile(gl: WebGLRenderingContext, type: number, src: string) { + const shader = gl.createShader(type)!; + gl.shaderSource(shader, src); + gl.compileShader(shader); + if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { + const log = gl.getShaderInfoLog(shader); + gl.deleteShader(shader); + throw new Error(log || "shader compile failed"); + } + return shader; +} + +/** + * Full-viewport animated Perlin/simplex-noise background rendered with a WebGL + * fragment shader. Flows slowly over time and eases toward the mouse cursor. + * Decorative only (pointer-events: none, aria-hidden). Falls back to a CSS + * gradient if WebGL is unavailable or fails to initialise. + */ +export default function NoiseBackground({ className = "" }: { className?: string }) { + const canvasRef = useRef(null); + const [failed, setFailed] = useState(false); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + + let disposed = false; + let raf = 0; + const target = { x: 0.5, y: 0.5 }; + const eased = { x: 0.5, y: 0.5 }; + const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches; + + const gl = canvas.getContext("webgl", { + alpha: false, + antialias: false, + powerPreference: "high-performance", + preserveDrawingBuffer: false, + }) as WebGLRenderingContext | null; + + if (!gl) { + setFailed(true); + return; + } + + let program: WebGLProgram | null = null; + let buffer: WebGLBuffer | null = null; + let uRes: WebGLUniformLocation | null = null; + let uTime: WebGLUniformLocation | null = null; + let uMouse: WebGLUniformLocation | null = null; + let uReduce: WebGLUniformLocation | null = null; + + const init = () => { + const vs = compile(gl, gl.VERTEX_SHADER, VERT); + const fs = compile(gl, gl.FRAGMENT_SHADER, FRAG); + program = gl.createProgram()!; + gl.attachShader(program, vs); + gl.attachShader(program, fs); + gl.linkProgram(program); + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { + const log = gl.getProgramInfoLog(program); + throw new Error(log || "program link failed"); + } + gl.deleteShader(vs); + gl.deleteShader(fs); + + buffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, buffer); + gl.bufferData( + gl.ARRAY_BUFFER, + new Float32Array([-1, -1, 3, -1, -1, 3]), + gl.STATIC_DRAW + ); + const loc = gl.getAttribLocation(program, "aPos"); + gl.enableVertexAttribArray(loc); + gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0); + + gl.useProgram(program); + uRes = gl.getUniformLocation(program, "uResolution"); + uTime = gl.getUniformLocation(program, "uTime"); + uMouse = gl.getUniformLocation(program, "uMouse"); + uReduce = gl.getUniformLocation(program, "uReducedMotion"); + gl.uniform1f(uReduce, reduce ? 1 : 0); + }; + + const resize = () => { + const dpr = Math.min(window.devicePixelRatio || 1, 2); + const w = Math.floor(canvas.clientWidth * dpr); + const h = Math.floor(canvas.clientHeight * dpr); + if (w > 0 && h > 0 && (canvas.width !== w || canvas.height !== h)) { + canvas.width = w; + canvas.height = h; + gl.viewport(0, 0, w, h); + } + }; + + const onMove = (e: MouseEvent) => { + target.x = e.clientX / window.innerWidth; + target.y = 1 - e.clientY / window.innerHeight; // GL is y-up + }; + + const startTime = performance.now(); + const draw = () => { + if (disposed || !program) return; + eased.x += (target.x - eased.x) * 0.06; + eased.y += (target.y - eased.y) * 0.06; + gl.uniform2f(uRes, canvas.width, canvas.height); + gl.uniform1f(uTime, reduce ? 0 : (performance.now() - startTime) / 1000); + gl.uniform2f(uMouse, eased.x, eased.y); + gl.drawArrays(gl.TRIANGLES, 0, 3); + }; + const frame = () => { + if (disposed) return; + resize(); + draw(); + raf = requestAnimationFrame(frame); + }; + + // Size the backing buffer (and paint a correctly-sized frame) as soon as + // the canvas is laid out, independent of the rAF loop — which is throttled + // in backgrounded tabs and may not have run yet. + const ro = new ResizeObserver(() => { + resize(); + draw(); + }); + + const onLost = (e: Event) => { + e.preventDefault(); + cancelAnimationFrame(raf); + }; + const onRestored = () => { + try { + init(); + frame(); + } catch (err) { + console.error("NoiseBackground restore failed:", err); + setFailed(true); + } + }; + canvas.addEventListener("webglcontextlost", onLost as EventListener, false); + canvas.addEventListener("webglcontextrestored", onRestored, false); + + try { + init(); + resize(); + ro.observe(canvas); + window.addEventListener("mousemove", onMove, { passive: true }); + frame(); + } catch (err) { + console.error("NoiseBackground init failed:", err); + setFailed(true); + } + + return () => { + disposed = true; + cancelAnimationFrame(raf); + ro.disconnect(); + window.removeEventListener("mousemove", onMove); + canvas.removeEventListener("webglcontextlost", onLost as EventListener); + canvas.removeEventListener("webglcontextrestored", onRestored); + if (program) gl.deleteProgram(program); + if (buffer) gl.deleteBuffer(buffer); + gl.getExtension("WEBGL_lose_context")?.loseContext(); + }; + }, []); + + if (failed) { + return