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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 0 additions & 18 deletions AGENTS.md

This file was deleted.

6 changes: 4 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@ any that could not run; maintainers and CI complete the remaining validation.

## Submit the pull request

Keep the existing yellow/black visual identity while core student features are being
completed. Improve responsiveness, usability and consistency within that design.
A broad website redesign is deferred until the core student journeys are working.

```bash
git add <files-you-changed>
git commit -m "Describe the change"
Expand All @@ -124,8 +128,6 @@ Open a pull request from your fork branch to `procoderji-com/procoderji:dev`. Fi
template, link a related issue when one exists, explain the resulting behavior, and list the
checks you ran.
Include screenshots for visible UI changes and sources or permissions for learning content.
For interface changes, follow the [design system](design-system/MASTER.md): use the shared
brand, typography, buttons and colour tokens, and check both keyboard and mobile navigation.

`dev` is the integration/staging branch; `main` is production. Feature and dependency changes
go through `dev`. Maintainers promote the canonical `dev` branch to `main` after staging checks;
Expand Down
15 changes: 2 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@

# ProCoderJii Learning Hub

### For the semester. For what’s next.
### Practise for campus placements. Understand your answers. Build a study habit.

A student learning brand bringing together course resources, placement practice, games,
portfolios, concept videos and community. This repository builds the open-source web platform.
A student learning workspace with original practice questions, explanations, and saved progress.

[Website](https://procoderji.com) · [Documentation](docs/README.md) · [Roadmap](https://github.com/procoderji-com/procoderji/issues/22) · [Good first issues](https://github.com/procoderji-com/procoderji/issues?q=is%3Aissue%20is%3Aopen%20label%3A%22good%20first%20issue%22) · [Report a bug](https://github.com/procoderji-com/procoderji/issues/new?template=bug_report.yml) · [Propose a question](https://github.com/procoderji-com/procoderji/issues/new?template=practice_question.yml)

Expand All @@ -22,18 +21,8 @@ portfolios, concept videos and community. This repository builds the open-source
> [!NOTE]
> This is an early student-product foundation. Four practice topics contain 24 original multiple-choice questions. Practice and browser-saved progress work without login. An optional ASP.NET Core backend provides Google sign-in, account progress, and private portfolio drafts. The portfolio editor, two themes, Markdown import, and public-page support are implemented; public publishing is off by default pending deployment configuration. Billing and checkout are not enabled. Course notes, guides, and job listings are awaiting published content.

![ProCoderJii homepage with a placement practice sample](docs/images/homepage.png)

<details>
<summary>Student learning workspace</summary>

![Guest learning workspace before the first session](docs/images/learning-workspace.png)

</details>

Interface contributions follow the [design system](design-system/MASTER.md), with shared
navigation, self-hosted typography, keyboard access and responsive layouts.

See the [product roadmap](docs/product-roadmap.md) for student validation, content growth, and future monetization work.

## Contents
Expand Down
8 changes: 0 additions & 8 deletions THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,6 @@ is distributed. Preserve applicable package licenses and component notices when
frontend archive or image, and inspect that actual artifact against the upstream terms. The
backend-only container does not bundle the Next.js image-processing dependencies.

## DM Sans

Copyright 2014 The DM Sans Project Authors (https://github.com/googlefonts/dm-fonts).

The self-hosted font in `public/fonts/dm-sans-variable.ttf` is distributed under the
[SIL Open Font License 1.1](public/fonts/OFL.txt). Its full notice is included alongside
the font. See [the source and usage notes](public/fonts/README.md).

## UI/UX Pro Max

Source: [nextlevelbuilder/ui-ux-pro-max-skill](https://github.com/nextlevelbuilder/ui-ux-pro-max-skill)
Expand Down
44 changes: 0 additions & 44 deletions design-system/MASTER.md

This file was deleted.

Binary file removed docs/images/homepage.png
Binary file not shown.
Binary file modified docs/images/learning-workspace.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/responsive-home-mobile.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/responsive-menu-mobile.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
82 changes: 48 additions & 34 deletions e2e/checks.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,18 @@ await interaction("home: mobile menu opens, lists links, closes", "mobile", asyn
await page.locator("#mobile-menu").waitFor({ state: "hidden" });
});

await interaction("home: original mobile menu links remain reachable on short screens", "mobile", async (page) => {
await page.setViewportSize({ width: 320, height: 568 });
await page.goto(BASE + "/", { waitUntil: "load" });
await page.getByRole("button", { name: "Open menu", exact: true }).click();
const notes = page.locator("#mobile-menu").getByRole("link", { name: "Browse Notes", exact: true });
await notes.scrollIntoViewIfNeeded();
const bounds = await notes.boundingBox();
expectTrue(bounds.y + bounds.height <= 568, "last menu action stays within the viewport");
await page.getByRole("button", { name: "Close menu", exact: true }).click();
await page.locator("#mobile-menu").waitFor({ state: "hidden" });
});

await interaction("home: desktop nav has working links", "desktop", async (page) => {
await page.goto(BASE + "/", { waitUntil: "networkidle" });
for (const [t, href] of [["Games", "/games"], ["Guides", "/guides"], ["Jobs", "/jobs"], ["Browse Notes", "/dashboard"]]) {
Expand All @@ -183,23 +195,6 @@ await interaction("home: desktop nav has working links", "desktop", async (page)
expectTrue((await page.locator("a[href*='procoderji.xyz']").count()) === 0, "no .xyz links");
});

for (const vp of ["desktop", "mobile"]) {
await interaction("home: sample explains answers without saving a session", vp, async (page) => {
await page.goto(BASE + "/", { waitUntil: "networkidle" });
const sample = page.locator(".sample-question");
const check = sample.getByRole("button", { name: "Check answer" });
expectTrue(await check.isDisabled(), "an answer is required");
await sample.getByRole("radio").first().check();
await check.click();
expectTrue((await sample.locator(".sample-answer").textContent()).length > 30, "show a useful explanation");
await sample.getByRole("radio").nth(1).check();
expectTrue(await sample.locator(".sample-answer").count() === 0, "changing an answer clears outdated feedback");
await page.goto(BASE + "/learn", { waitUntil: "networkidle" });
const sessions = page.locator("section").filter({ has: page.getByRole("heading", { name: "Completed sessions", exact: true }) });
expectTrue(await sessions.getByText("0", { exact: true }).isVisible(), "a sample is not recorded as a practice session");
});
}

await interaction("games: pattern round reaches a result", "desktop", async (page) => {
await page.goto(BASE + "/games/pattern-sprint", { waitUntil: "networkidle" });
await page.getByRole("button", { name: "Start game" }).click();
Expand Down Expand Up @@ -307,9 +302,12 @@ await interaction("dashboard: mobile drawer opens, Escape closes", "mobile", asy
const dlg = page.getByRole("dialog");
await dlg.waitFor({ state: "visible" });
expectTrue(await dlg.getByRole("link", { name: /Browse Notes/ }).isVisible(), "drawer has Browse Notes");
await page.getByRole("button", { name: "Close navigation menu", exact: true }).focus();
await page.keyboard.press("Shift+Tab");
expectTrue(await dlg.evaluate(el => el.contains(document.activeElement)), "keyboard focus stays inside the drawer");
await page.keyboard.press("Escape");
await dlg.waitFor({ state: "hidden" });
expectTrue(await page.getByRole("button", { name: /open navigation menu/i }).evaluate(el => el === document.activeElement), "Escape restores focus to the drawer toggle");
expectTrue(await page.getByRole("button", { name: /open navigation menu/i }).evaluate(el => el === document.activeElement), "drawer closes back to its toggle");
});

await interaction("dashboard: sidebar expands to semester and navigates (desktop)", "desktop", async (page) => {
Expand Down Expand Up @@ -378,22 +376,40 @@ await interaction("legal pages: completed details are indexable", "desktop", asy
expectTrue((await page.locator('link[rel="canonical"]').getAttribute("href")) === "https://procoderji.com/terms", "canonical URL");
});

await interaction("home: headline and sample stay separate at every breakpoint", "desktop", async (page) => {
for (const [width, height] of [[375, 900], [768, 900], [1024, 768], [1280, 800], [1366, 768], [1440, 900], [1920, 1080]]) {
await interaction("hero headline is never covered by the floating logo (several desktop widths)", "desktop", async (page) => {
// The floating logo tile has pointer-events:none, so elementFromPoint cannot see it: compare geometry instead.
for (const [width, height] of [[768, 900], [1024, 768], [1280, 800], [1366, 768], [1440, 900], [1920, 1080]]) {
await page.setViewportSize({ width, height });
// Visible layout and fonts are the readiness signal. Responsive link prefetches
// need not become network-idle before we can measure the rendered interface.
await page.goto(BASE + "/", { waitUntil: "domcontentloaded" });
await page.getByRole("heading", { level: 1 }).waitFor({ state: "visible" });
await page.locator(".sample-question").waitFor({ state: "visible" });
await page.evaluate(() => document.fonts.ready);
const heading = await page.getByRole("heading", { level: 1 }).boundingBox();
const sample = await page.locator(".sample-question").boundingBox();
const overlaps = heading.x < sample.x + sample.width && heading.x + heading.width > sample.x && heading.y < sample.y + sample.height && heading.y + heading.height > sample.y;
expectTrue(!overlaps, "headline and question overlap at " + width + "px");
await page.goto(BASE + "/", { waitUntil: "networkidle" });
await page.waitForTimeout(1200); // let the entrance animations finish
const overlaps = await page.evaluate(() => {
const h1 = document.querySelector("h1");
const range = document.createRange();
range.selectNodeContents(h1);
const textLines = [...range.getClientRects()].filter((r) => r.width > 0 && r.height > 0);
// Outermost pointer-events:none wrapper around every visible image = decorative overlays such as the logo tile.
const overlays = new Set();
for (const img of document.querySelectorAll("img")) {
if (!img.getClientRects().length) continue;
let el = img;
while (el.parentElement && getComputedStyle(el.parentElement).pointerEvents === "none") el = el.parentElement;
if (el !== img || getComputedStyle(img).pointerEvents === "none") overlays.add(el);
}
const hits = [];
for (const el of overlays) {
const o = el.getBoundingClientRect();
if (o.width === 0 || o.height === 0 || getComputedStyle(el).display === "none") continue;
for (const t of textLines) {
if (o.left < t.right && o.right > t.left && o.top < t.bottom && o.bottom > t.top) hits.push(`${el.tagName}.${String(el.className).slice(0, 50)}`);
}
}
return [...new Set(hits)];
});
expectTrue(overlaps.length === 0, `at ${width}px the headline text is overlapped by: ${overlaps.join(", ")}`);
}
});


// Needs the live channel feed, so it only runs when YOUTUBE_CHANNEL_ID is set (otherwise the section is hidden by design).
const videosEnabled = /^UC[A-Za-z0-9_-]{22}$/.test(process.env.YOUTUBE_CHANNEL_ID ?? "");
if (!videosEnabled) console.log("SKIP latest videos responsive checks (set YOUTUBE_CHANNEL_ID to run them)");
Expand Down Expand Up @@ -427,7 +443,7 @@ if (videosEnabled) {

await interaction("privacy page discloses every third-party host the site contacts", "desktop", async (page) => {
// Every external host that these pages request must be named on the Privacy page.
const DISCLOSED_AS = { "i.ytimg.com": "i.ytimg.com" };
const DISCLOSED_AS = { "lottie.host": "lottie.host", "cdn.jsdelivr.net": "jsdelivr", "i.ytimg.com": "i.ytimg.com" };
const base = new URL(BASE);
const hosts = new Set();
page.on("request", (r) => {
Expand All @@ -441,8 +457,7 @@ await interaction("privacy page discloses every third-party host the site contac
await page.waitForTimeout(120);
}
}
const retiredServices = [...hosts].filter(h => /(^|\.)(lottie\.host|jsdelivr\.net|googleapis\.com|gstatic\.com)$/.test(h));
expectTrue(retiredServices.length === 0, "interface fonts and artwork must stay self-hosted: " + retiredServices.join(", "));
expectTrue(hosts.size > 0, "expected the home page to contact at least the animation hosts");
await page.goto(BASE + "/privacy", { waitUntil: "networkidle" });
const text = (await page.locator("main").innerText()).toLowerCase();
const missing = [...hosts].filter((h) => !text.includes((DISCLOSED_AS[h] ?? h).toLowerCase()));
Expand All @@ -460,7 +475,6 @@ if (FULLSTACK) {
await browser.close();

console.log("\n========== FINDINGS ==========");
fs.writeFileSync(path.join(OUT, "findings.json"), JSON.stringify(findings, null, 2) + "\n");
if (!findings.length) console.log("none");
const grouped = {};
for (const f of findings) (grouped[f.kind] ??= []).push(f);
Expand Down
Loading