diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index db00921..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,18 +0,0 @@ -# Repository guidance - -Read `CONTRIBUTING.md` before making changes. Contributions target `dev`; -production promotions follow `docs/branching-and-releases.md`. - -For UI work, follow `design-system/MASTER.md` and reuse the shared brand and -action styles. Keep real student activity distinct from interactive samples. -Verify keyboard navigation and narrow layouts alongside the relevant checks. - - - -## This is NOT the Next.js you know - -This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices. - -This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean. - - diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6f6c5b8..a895501 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 git commit -m "Describe the change" @@ -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; diff --git a/README.md b/README.md index 1ea5608..8e1dee3 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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) - -
-Student learning workspace - ![Guest learning workspace before the first session](docs/images/learning-workspace.png) -
- -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 diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 2e63418..ed624bc 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -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) diff --git a/design-system/MASTER.md b/design-system/MASTER.md deleted file mode 100644 index eb08ec4..0000000 --- a/design-system/MASTER.md +++ /dev/null @@ -1,44 +0,0 @@ -# ProCoderJii interface - -ProCoderJii is a student learning brand, spanning notes, placement practice, games, videos, -community and portfolios. The interface should feel like a useful study desk, with clear -choices and readable content. It must not imply hiring guarantees or invent social proof. - -## Direction - -UI/UX Pro Max's Swiss Modernism guidance informs the grid, hierarchy, restrained decoration -and keyboard accessibility. Use an editorial opening on the public site and compact, -functional layouts in the student workspace. The existing gold brand remains an accent. - -| Token | Value | Use | -| ------------- | --------- | ------------------------------------------- | -| Paper | `#f8f7f2` | Page background | -| Surface | `#ffffff` | Forms, reading and practice | -| Ink | `#242b25` | Headings and primary text | -| Secondary ink | `#60685d` | Supporting text | -| Forest | `#345443` | Links, primary actions, selected navigation | -| Gold | `#edc76e` | Small highlights and warm-up activities | -| Rule | `#dedfd5` | Quiet dividers and input boundaries | - -DM Sans is self-hosted under the SIL Open Font License. Georgia provides occasional editorial -display headings; the workspace keeps sans-serif headings. Body text is at least 16px for -reading and form inputs. Small labels supplement, never replace, visible field labels. - -## Patterns - -- One clear primary action per section. Prefer content rows and dividers over repeated cards. -- Desktop public navigation is shared across public pages. Mobile navigation has visible - controls, an expanded state, Escape dismissal and focus restoration. -- Learning navigation stays consistent between practice, account and portfolio editing. -- Progress displays real saved values, with useful empty states; homepage samples are labelled - as samples and do not impersonate saved activity. -- Notes keep branch/semester URLs and readable document layouts. Games keep their original - controls and feedback. Portfolio drafts remain private until explicit publication. -- Avoid animated backgrounds, gradient text, glass effects, floating decorations and invented - testimonials, rankings, company affiliations or activity counts. - -## Verification - -Check 375px, 768px, 1024px and 1440px layouts; keyboard focus and mobile menu; practice feedback; -notes navigation; portfolio preview; reading contrast; reduced motion; and existing browser/SEO -checks. Stage changes through `dev`; production uses the reviewed promotion workflow. diff --git a/docs/images/homepage.png b/docs/images/homepage.png deleted file mode 100644 index 3b67191..0000000 Binary files a/docs/images/homepage.png and /dev/null differ diff --git a/docs/images/learning-workspace.png b/docs/images/learning-workspace.png index 7ec5774..a450fc4 100644 Binary files a/docs/images/learning-workspace.png and b/docs/images/learning-workspace.png differ diff --git a/docs/images/responsive-home-mobile.png b/docs/images/responsive-home-mobile.png new file mode 100644 index 0000000..c3df5c5 Binary files /dev/null and b/docs/images/responsive-home-mobile.png differ diff --git a/docs/images/responsive-menu-mobile.png b/docs/images/responsive-menu-mobile.png new file mode 100644 index 0000000..5ff25f6 Binary files /dev/null and b/docs/images/responsive-menu-mobile.png differ diff --git a/e2e/checks.mjs b/e2e/checks.mjs index 4be8357..79581f0 100644 --- a/e2e/checks.mjs +++ b/e2e/checks.mjs @@ -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"]]) { @@ -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(); @@ -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) => { @@ -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)"); @@ -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) => { @@ -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())); @@ -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); diff --git a/package-lock.json b/package-lock.json index 964d33d..4ab4f51 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,11 @@ "name": "procoderji-notes", "version": "0.1.0", "dependencies": { + "@lottiefiles/dotlottie-react": "^0.21.1", "@radix-ui/react-slot": "^1.5.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "framer-motion": "^14.10.1", "lucide-react": "^0.577.0", "next": "16.4.5", "next-mdx-remote": "6.0.0", @@ -1138,6 +1140,32 @@ "dev": true, "license": "MIT" }, + "node_modules/@lottiefiles/dotlottie-react": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@lottiefiles/dotlottie-react/-/dotlottie-react-0.21.1.tgz", + "integrity": "sha512-y5ec2Ipko1TvUfoUmS9zn+g0Ud+oYtUEJS5Xj27edv1kO8ZAowoA2v6DUWNniZ/+A9m5wjEswqPzShOK6+pdAw==", + "license": "MIT", + "dependencies": { + "@lottiefiles/dotlottie-web": "0.82.1" + }, + "engines": { + "node": ">=18.17.0", + "npm": ">=9.5.0" + }, + "peerDependencies": { + "react": "^17 || ^18 || ^19" + } + }, + "node_modules/@lottiefiles/dotlottie-web": { + "version": "0.82.1", + "resolved": "https://registry.npmjs.org/@lottiefiles/dotlottie-web/-/dotlottie-web-0.82.1.tgz", + "integrity": "sha512-hLPdAL0dUeHKvQvXqw1p3FtvzZUHRtBsTMt3FLLgphFThsUPNUj2+U+GyttJ5H5qSt5JreLzoaD+IaKO8H/VnA==", + "license": "MIT", + "engines": { + "node": ">=18.17.0", + "npm": ">=9.5.0" + } + }, "node_modules/@mdx-js/mdx": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.1.tgz", @@ -4297,6 +4325,33 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/framer-motion": { + "version": "14.10.1", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-14.10.1.tgz", + "integrity": "sha512-hT8WLczAcreOHG4EHymMtATztwUc3al1/88/stal3tpxYkMefGNzIPHHgFFHW3iLnrNY36BrKKJDuT2V7o8Tfw==", + "license": "MIT", + "dependencies": { + "motion-dom": "14.10.1", + "motion-utils": "14.7.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0", + "remix": "^3.1.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "remix": { + "optional": true + } + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -6506,6 +6561,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/motion-dom": { + "version": "14.10.1", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-14.10.1.tgz", + "integrity": "sha512-NzVzM7y3JJl3rYlVN4yM6kbziab/tHNBEV6gqOoAQIg8npHlIMhW2oPnzr0HkP+rR8xMoiX9+zSZ4BLaVuJadw==", + "license": "MIT", + "dependencies": { + "motion-utils": "14.7.0" + } + }, + "node_modules/motion-utils": { + "version": "14.7.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-14.7.0.tgz", + "integrity": "sha512-woO0/Nb7ys75f94fjvQi/6leiHGvZUYkeHUPEZ3vxsV+9OpEVhkxp01x8MlRwX0zt31EqgXsKDAoBoJLC148Tw==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", diff --git a/package.json b/package.json index eaa0468..27a1826 100644 --- a/package.json +++ b/package.json @@ -16,9 +16,11 @@ "launch-check": "node e2e/launch.mjs" }, "dependencies": { + "@lottiefiles/dotlottie-react": "^0.21.1", "@radix-ui/react-slot": "^1.5.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "framer-motion": "^14.10.1", "lucide-react": "^0.577.0", "next": "16.4.5", "next-mdx-remote": "6.0.0", diff --git a/public/fonts/OFL.txt b/public/fonts/OFL.txt deleted file mode 100644 index a0eedfb..0000000 --- a/public/fonts/OFL.txt +++ /dev/null @@ -1,93 +0,0 @@ -Copyright 2014 The DM Sans Project Authors (https://github.com/googlefonts/dm-fonts) - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -https://scripts.sil.org/OFL - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/public/fonts/README.md b/public/fonts/README.md deleted file mode 100644 index 4add13f..0000000 --- a/public/fonts/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# DM Sans - -`dm-sans-variable.ttf` is the variable DM Sans font distributed by Google Fonts. -It is self-hosted with Next.js so reading the application does not request fonts -from a third-party server. - -- Source: https://github.com/google/fonts/tree/main/ofl/dmsans -- License: [SIL Open Font License 1.1](OFL.txt) - -Georgia, where used for editorial headings, is a system-font fallback. diff --git a/public/fonts/dm-sans-variable.ttf b/public/fonts/dm-sans-variable.ttf deleted file mode 100644 index c672f98..0000000 Binary files a/public/fonts/dm-sans-variable.ttf and /dev/null differ diff --git a/src/app/(student)/practice/page.tsx b/src/app/(student)/practice/page.tsx index 6116225..e9db4db 100644 --- a/src/app/(student)/practice/page.tsx +++ b/src/app/(student)/practice/page.tsx @@ -14,39 +14,44 @@ export const metadata = pageMetadata({ export default function PracticePage() { return ( <> -

+

Prepare with purpose

-

Campus placement practice

-

+

+ Campus placement practice +

+

Work through short topic sessions. Understand why an answer works, review mistakes, and use your own progress to decide what comes next.

-
+
- - -
-
+
{PRACTICE_TOPICS.map((topic, index) => ( -
- +
+ 0{index + 1}

{topic.title}

-

+

{topic.description}

-

+

{QUESTION_BANK.filter((q) => q.topicId === topic.id).length}{" "} questions · Easy & medium · Explanations included

@@ -60,9 +65,9 @@ export default function PracticePage() {
))}
-
+

Learn from every attempt

-

+

These starter sets build familiarity with common reasoning and aptitude skills. They are independent learning exercises, and the small question bank is not a calibrated employer assessment. Use diff --git a/src/app/about/page.tsx b/src/app/about/page.tsx index 67f61c9..2027a3c 100644 --- a/src/app/about/page.tsx +++ b/src/app/about/page.tsx @@ -1,111 +1,73 @@ import Link from "next/link"; -import { - EmailLink, - H2, - LegalLayout, - Operator, - P, - UL, - legalMetadata, -} from "@/components/legal/legal-layout"; +import { EmailLink, H2, LegalLayout, Operator, P, UL, legalMetadata } from "@/components/legal/legal-layout"; export const metadata = legalMetadata( - "About", - "Learn about ProCoderJii, an open-source student platform for placement practice, explanations, free games and saved progress, and how to contribute.", - "/about", + "About", + "Learn about ProCoderJii, an open-source student platform for placement practice, explanations, free games and saved progress, and how to contribute.", + "/about" ); export default function AboutPage() { - return ( - -

- ProCoderJii is a student learning brand built around concept videos, - course resources, placement practice and a community of people learning - together. Our YouTube channel, Telegram and WhatsApp communities connect - students beyond this website. The open-source platform is operated by{" "} - and grows through reviewed community contributions. -

+ return ( + +

+ ProCoderJii is an open-source learning and campus-placement practice project for students. + It is operated by and grows through reviewed community contributions. +

-

Practise and understand your answers

-
    -
  • - - Placement practice - {" "} - covers quantitative aptitude, logical reasoning, verbal ability and - programming foundations. Each session includes correct answers and - explanations. -
  • -
  • - - Free practice games - {" "} - include Pattern Sprint for number patterns and Digit Recall for - short-term memory. -
  • -
  • - The{" "} - - learner workspace - {" "} - shows question-practice history, topic progress and a suggested next - step. -
  • -
+

Practise and understand your answers

+
    +
  • + Placement practice covers + quantitative aptitude, logical reasoning, verbal ability and programming foundations. + Each session includes correct answers and explanations. +
  • +
  • + Free practice games include + Pattern Sprint for number patterns and Digit Recall for short-term memory. +
  • +
  • + The learner workspace shows + question-practice history, topic progress and a suggested next step. +
  • +
-

Free practice and your progress

-

- Practice is free and works without an account. Question-practice history - is saved in this browser; clearing site data removes it. Optional Google - sign-in can synchronize that history when account services are - available. Game scores are shown at the end of a round and are not yet - saved. -

+

Free practice and your progress

+

+ Practice is free and works without an account. Question-practice history is saved in this + browser; clearing site data removes it. Optional Google sign-in can synchronize that + history when account services are available. Game scores are shown at the end of a round + and are not yet saved. +

-

Learning content in development

-

- Course, branch and semester navigation is in place. Reviewed course - notes, guides and job listings are awaiting publication. The current - learning exercises are independent practice for building skills; they do - not predict or guarantee a placement result. -

+

Learning content in development

+

+ Course, branch and semester navigation is in place. Reviewed course notes, guides and job + listings are awaiting publication. The current learning exercises are independent practice + for building skills; they do not predict or guarantee a placement result. +

-

How to contribute

-

- Students and developers can help with original questions, explanations, - accessibility, documentation and code. Visit the{" "} - - GitHub repository and contribution guide - {" "} - to find a focused task and the checks it needs. -

-
    -
  • - Learning material is reviewed for correctness and clarity before - publication. -
  • -
  • - Where an official syllabus or question paper exists, we link to the - university's own page instead of copying it. -
  • -
  • - We do not republish other people's notes, papers or PDFs without - permission. -
  • -
+

How to contribute

+

+ Students and developers can help with original questions, explanations, accessibility, + documentation and code. Visit the{" "} + + GitHub repository and contribution guide + {" "} + to find a focused task and the checks it needs. +

+
    +
  • Learning material is reviewed for correctness and clarity before publication.
  • +
  • Where an official syllabus or question paper exists, we link to the university's own page instead of copying it.
  • +
  • We do not republish other people's notes, papers or PDFs without permission.
  • +
-

Corrections and requests

-

- Found a mistake, or want a subject covered? Email or open - a reproducible issue in the repository. See the{" "} - - Disclaimer - {" "} - for the limits of the learning material. -

-
- ); +

Corrections and requests

+

+ Found a mistake, or want a subject covered? Email or open a reproducible issue + in the repository. See the Disclaimer{" "} + for the limits of the learning material. +

+ + ); } diff --git a/src/app/dashboard/dashboard-layout-client.tsx b/src/app/dashboard/dashboard-layout-client.tsx index a99a604..c0be6d6 100644 --- a/src/app/dashboard/dashboard-layout-client.tsx +++ b/src/app/dashboard/dashboard-layout-client.tsx @@ -1,76 +1,144 @@ "use client"; -import { useEffect, useRef } from "react"; +import React, { useEffect, useRef, useState } from "react"; import { Menu, X } from "lucide-react"; -import { usePathname } from "next/navigation"; import { AppSidebar } from "@/components/app-sidebar"; -import { Brand } from "@/components/brand"; +import { usePathname } from "next/navigation"; +import { motion, AnimatePresence } from "framer-motion"; export function DashboardClientLayout({ - children, + children, }: { - children: React.ReactNode; + children: React.ReactNode; }) { - const pathname = usePathname(); - const drawer = useRef(null); - const toggle = useRef(null); + const pathname = usePathname(); + // Remember which route the drawer was opened on: navigating elsewhere closes it + // without needing an effect. + const [openOnPath, setOpenOnPath] = useState(null); + const isSidebarOpen = openOnPath === pathname; + const drawer = useRef(null); + const toggle = useRef(null); + const closeButton = useRef(null); + const closeSidebar = () => setOpenOnPath(null); + + // Keep keyboard focus and scrolling inside the mobile drawer. + useEffect(() => { + if (!isSidebarOpen) return; + const trigger = toggle.current; + const previousOverflow = document.body.style.overflow; + document.body.style.overflow = "hidden"; + const frame = requestAnimationFrame(() => + closeButton.current?.focus({ preventScroll: true }), + ); + const onKey = (e: KeyboardEvent) => { + if (e.key === "Escape") setOpenOnPath(null); + if (e.key === "Tab") { + const items = [ + ...(drawer.current?.querySelectorAll( + 'a[href],button:not(:disabled),[tabindex="0"]', + ) ?? []), + ].filter((item) => item.getClientRects().length); + const first = items[0]; + const last = items.at(-1); + if (e.shiftKey && document.activeElement === first) { + e.preventDefault(); + last?.focus(); + } else if (!e.shiftKey && document.activeElement === last) { + e.preventDefault(); + first?.focus(); + } + } + }; + const desktop = window.matchMedia("(min-width: 768px)"); + const onResize = () => { + if (desktop.matches) setOpenOnPath(null); + }; + desktop.addEventListener("change", onResize); + document.addEventListener("keydown", onKey); + return () => { + cancelAnimationFrame(frame); + document.body.style.overflow = previousOverflow; + document.removeEventListener("keydown", onKey); + desktop.removeEventListener("change", onResize); + if (!desktop.matches) trigger?.focus({ preventScroll: true }); + }; + }, [isSidebarOpen]); + + return ( +
+ {/* Mobile Header */} +
+
ProCoderJii
+ +
+ + {/* Mobile Sidebar Overlay */} + + {isSidebarOpen && ( + <> + - useEffect(() => { - drawer.current?.close(); - }, [pathname]); - useEffect(() => { - const desktop = window.matchMedia("(min-width: 768px)"); - const closeOnDesktop = () => { - if (desktop.matches) drawer.current?.close(); - }; - desktop.addEventListener("change", closeOnDesktop); - return () => desktop.removeEventListener("change", closeOnDesktop); - }, []); + {/* Desktop Sidebar */} +
+ +
- return ( -
-
- - -
- toggle.current?.focus()} - onClick={(event) => { - if (event.target === event.currentTarget) drawer.current?.close(); - }} - className="notes-drawer fixed inset-y-0 left-0 m-0 h-dvh max-h-none w-80 max-w-[90vw] border-r border-border bg-background text-foreground backdrop:bg-black/40" - > - - - -
- -
-
-
- {children} + {/* Main Content */} +
+
+
{children}
+
+
-
-
- ); + ); } diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx index 1a12c67..ab33597 100644 --- a/src/app/dashboard/layout.tsx +++ b/src/app/dashboard/layout.tsx @@ -1,15 +1,20 @@ import { DashboardClientLayout } from "./dashboard-layout-client"; export default function DashboardLayout({ - children, + children, }: { - children: React.ReactNode; + children: React.ReactNode; }) { - return ( - <> -