From 341ae4e1382026aeac735813197b60f8d8836538 Mon Sep 17 00:00:00 2001 From: S Prajwall Narayana <115879066+Developer1010x@users.noreply.github.com> Date: Wed, 12 Aug 2026 22:58:26 +0530 Subject: [PATCH 1/4] Rebuild the site: refactor, redesign, and stop the link rot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The catalog was a single 2926-line mockData.ts, five components were dead code, and the dynamic routes rendered on demand. This restructures all of it, redesigns the front end, and adds the tooling to keep the material from silently rotting. Structure - Split mockData.ts into one file per department plus contributors.ts; verified byte-faithful (identical link/title/name counts). - Delete the unused departmentCard -> yearSection -> semesterSection -> subjectSection -> noteItem subtree, the empty subjectCard, the all-commented links/resources.ts, and the duplicate postcss config (which declared a postcss-nested dependency that never existed). - Extract shared UI (Card, TileLink, PageHeader, Badge, EmptyState, icons) and shared lookups (lib/catalog.ts). - Route pages are async server components with awaited params, and every dynamic route prerenders via generateStaticParams: 535 static pages. - Pin next/eslint-config-next; drop the deprecated `next export` script. Design - New token system: colours are RGB triplets in globals.css exposed to Tailwind as semantic names, so light and dark both stay correct and every foreground/background pair clears WCAG AA. - Mobile-first header with a working menu, command palette on Cmd/Ctrl-K, breadcrumbs, empty states that say how to fix themselves, loading skeletons, a real 404, and per-route metadata and OG images. Features - Subject pages (175 of them) with their own URL, share card and metadata. - Set-your-semester, bookmarks, revision progress, and recently viewed — all localStorage, no account, nothing uploaded. - Exam-prep view: a semester's material regrouped by type. - Contribute wizard that writes the catalog entry and opens the right file on GitHub, with an issue-form fallback for non-coders. - What's New feed (dates derived from git history) plus RSS, sitemap, robots and JSON-LD. - Installable PWA with offline browsing of visited pages. Link rot - Audited every resource link: 55 Drive folders return a hard 404 and 3 GitHub repos are gone, because RVCE accounts are purged after students graduate. 30 links need an RVCE sign-in and are deliberately NOT flagged, since that is the healthy state for restricted material. - Dead links are marked in place with a replacement CTA rather than silently failing, and subject cards show honest counts. - check:data validates contributions, check:links re-audits, and a weekly workflow files the damage as an issue so this cannot go unnoticed again. --- .github/ISSUE_TEMPLATE/add-notes.yml | 96 + .github/ISSUE_TEMPLATE/broken-link.yml | 38 + .github/ISSUE_TEMPLATE/config.yml | 5 + .github/workflows/ci.yml | 35 + .github/workflows/link-check.yml | 70 + .gitignore | 4 + CONTRIBUTING.md | 138 +- README.md | 76 +- package-lock.json | 4 +- package.json | 13 +- postcss.config.js | 7 - public/manifest.webmanifest | 37 + public/sw.js | 78 + scripts/build-changelog.mjs | 84 + scripts/build-icons.mjs | 58 + scripts/check-links.mjs | 180 + scripts/validate-data.mjs | 141 + .../[semester]/[subject]/opengraph-image.tsx | 110 + .../[year]/[semester]/[subject]/page.tsx | 226 ++ .../[year]/[semester]/exam-prep/page.tsx | 189 ++ .../[department]/[year]/[semester]/page.tsx | 257 +- src/app/[department]/[year]/page.tsx | 164 +- src/app/[department]/page.tsx | 155 +- src/app/about/page.tsx | 232 +- src/app/contact/page.tsx | 157 +- src/app/contribute/page.tsx | 165 + src/app/contributors/page.tsx | 39 +- src/app/feed.xml/route.ts | 48 + src/app/globals.css | 268 +- src/app/icon.svg | 5 + src/app/layout.tsx | 181 +- src/app/loading.tsx | 27 + src/app/not-found.tsx | 50 + src/app/offline/page.tsx | 35 + src/app/opengraph-image.tsx | 65 + src/app/page.tsx | 169 +- src/app/privacy/page.tsx | 101 +- src/app/robots.ts | 9 + src/app/saved/page.tsx | 137 + src/app/search/page.tsx | 187 +- src/app/sitemap.ts | 72 + src/app/whats-new/page.tsx | 91 + src/components/CommandPalette.tsx | 239 ++ src/components/ContributeCTA.tsx | 55 + src/components/ContributeWizard.tsx | 272 ++ src/components/ContributorCard.tsx | 68 + src/components/CopyLinks.tsx | 46 + src/components/InstallApp.tsx | 83 + src/components/MySemester.tsx | 214 ++ src/components/NoteLink.tsx | 155 + src/components/Notice.tsx | 54 + src/components/Progress.tsx | 83 + src/components/RecentlyViewed.tsx | 51 + src/components/SearchClient.tsx | 167 + src/components/SemesterSwitcher.tsx | 51 + src/components/ShareButton.tsx | 87 + src/components/SiteFooter.tsx | 109 + src/components/SiteHeader.tsx | 141 + src/components/StructuredData.tsx | 72 + src/components/SubjectCard.tsx | 87 + src/components/ThemeProvider.tsx | 43 +- src/components/ThemeToggle.tsx | 42 +- src/components/contributorCard.tsx | 40 - src/components/departmentCard.tsx | 27 - src/components/noteItem.tsx | 31 - src/components/semesterSection.tsx | 15 - src/components/subjectCard.tsx | 0 src/components/subjectSection.tsx | 18 - src/components/ui/Badge.tsx | 57 + src/components/ui/Card.tsx | 36 + src/components/ui/EmptyState.tsx | 56 + src/components/ui/PageHeader.tsx | 72 + src/components/ui/TileLink.tsx | 57 + src/components/ui/icons.tsx | 77 + src/components/yearSection.tsx | 16 - src/data/contributors.ts | 213 ++ src/data/departments/1st-year.ts | 48 + src/data/departments/aerospace.ts | 264 ++ src/data/departments/all-engineering.ts | 31 + src/data/departments/biotech.ts | 215 ++ src/data/departments/chemical.ts | 192 ++ src/data/departments/civil.ts | 193 ++ src/data/departments/cse-ise-aiml.ts | 326 ++ src/data/departments/ece.ts | 192 ++ src/data/departments/eee.ts | 204 ++ src/data/departments/ei.ts | 193 ++ src/data/departments/iem.ts | 193 ++ src/data/departments/index.ts | 36 + src/data/departments/maths.ts | 55 + src/data/departments/mech.ts | 204 ++ src/data/departments/megaaccess.ts | 261 ++ src/data/departments/miscellaneous.ts | 85 + src/data/departments/pyqp.ts | 43 + src/data/generated/added.json | 215 ++ src/data/generated/link-status.json | 61 + src/data/mockData.ts | 2926 ----------------- src/data/{dataTypes.ts => types.ts} | 24 +- src/lib/catalog.ts | 244 ++ src/lib/changelog.ts | 61 + src/lib/linkHealth.ts | 38 + src/lib/noteTypes.ts | 65 + src/lib/prefs.ts | 151 + src/lib/recent.ts | 45 + src/lib/searchIndex.ts | 110 + src/lib/site.ts | 98 + src/lib/theme.ts | 69 +- src/links/resources.ts | 113 - tailwind.config.ts | 58 +- 108 files changed, 9662 insertions(+), 4388 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/add-notes.yml create mode 100644 .github/ISSUE_TEMPLATE/broken-link.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/link-check.yml delete mode 100644 postcss.config.js create mode 100644 public/manifest.webmanifest create mode 100644 public/sw.js create mode 100644 scripts/build-changelog.mjs create mode 100644 scripts/build-icons.mjs create mode 100644 scripts/check-links.mjs create mode 100644 scripts/validate-data.mjs create mode 100644 src/app/[department]/[year]/[semester]/[subject]/opengraph-image.tsx create mode 100644 src/app/[department]/[year]/[semester]/[subject]/page.tsx create mode 100644 src/app/[department]/[year]/[semester]/exam-prep/page.tsx create mode 100644 src/app/contribute/page.tsx create mode 100644 src/app/feed.xml/route.ts create mode 100644 src/app/icon.svg create mode 100644 src/app/loading.tsx create mode 100644 src/app/not-found.tsx create mode 100644 src/app/offline/page.tsx create mode 100644 src/app/opengraph-image.tsx create mode 100644 src/app/robots.ts create mode 100644 src/app/saved/page.tsx create mode 100644 src/app/sitemap.ts create mode 100644 src/app/whats-new/page.tsx create mode 100644 src/components/CommandPalette.tsx create mode 100644 src/components/ContributeCTA.tsx create mode 100644 src/components/ContributeWizard.tsx create mode 100644 src/components/ContributorCard.tsx create mode 100644 src/components/CopyLinks.tsx create mode 100644 src/components/InstallApp.tsx create mode 100644 src/components/MySemester.tsx create mode 100644 src/components/NoteLink.tsx create mode 100644 src/components/Notice.tsx create mode 100644 src/components/Progress.tsx create mode 100644 src/components/RecentlyViewed.tsx create mode 100644 src/components/SearchClient.tsx create mode 100644 src/components/SemesterSwitcher.tsx create mode 100644 src/components/ShareButton.tsx create mode 100644 src/components/SiteFooter.tsx create mode 100644 src/components/SiteHeader.tsx create mode 100644 src/components/StructuredData.tsx create mode 100644 src/components/SubjectCard.tsx delete mode 100644 src/components/contributorCard.tsx delete mode 100644 src/components/departmentCard.tsx delete mode 100644 src/components/noteItem.tsx delete mode 100644 src/components/semesterSection.tsx delete mode 100644 src/components/subjectCard.tsx delete mode 100644 src/components/subjectSection.tsx create mode 100644 src/components/ui/Badge.tsx create mode 100644 src/components/ui/Card.tsx create mode 100644 src/components/ui/EmptyState.tsx create mode 100644 src/components/ui/PageHeader.tsx create mode 100644 src/components/ui/TileLink.tsx create mode 100644 src/components/ui/icons.tsx delete mode 100644 src/components/yearSection.tsx create mode 100644 src/data/contributors.ts create mode 100644 src/data/departments/1st-year.ts create mode 100644 src/data/departments/aerospace.ts create mode 100644 src/data/departments/all-engineering.ts create mode 100644 src/data/departments/biotech.ts create mode 100644 src/data/departments/chemical.ts create mode 100644 src/data/departments/civil.ts create mode 100644 src/data/departments/cse-ise-aiml.ts create mode 100644 src/data/departments/ece.ts create mode 100644 src/data/departments/eee.ts create mode 100644 src/data/departments/ei.ts create mode 100644 src/data/departments/iem.ts create mode 100644 src/data/departments/index.ts create mode 100644 src/data/departments/maths.ts create mode 100644 src/data/departments/mech.ts create mode 100644 src/data/departments/megaaccess.ts create mode 100644 src/data/departments/miscellaneous.ts create mode 100644 src/data/departments/pyqp.ts create mode 100644 src/data/generated/added.json create mode 100644 src/data/generated/link-status.json delete mode 100644 src/data/mockData.ts rename src/data/{dataTypes.ts => types.ts} (60%) create mode 100644 src/lib/catalog.ts create mode 100644 src/lib/changelog.ts create mode 100644 src/lib/linkHealth.ts create mode 100644 src/lib/noteTypes.ts create mode 100644 src/lib/prefs.ts create mode 100644 src/lib/recent.ts create mode 100644 src/lib/searchIndex.ts create mode 100644 src/lib/site.ts delete mode 100644 src/links/resources.ts diff --git a/.github/ISSUE_TEMPLATE/add-notes.yml b/.github/ISSUE_TEMPLATE/add-notes.yml new file mode 100644 index 0000000..946afcb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/add-notes.yml @@ -0,0 +1,96 @@ +name: Add notes +description: Share notes, a lab manual or a question paper. No coding needed. +title: "Add notes: " +labels: ["content"] +body: + - type: markdown + attributes: + value: | + Thanks for adding to Knotes Central. Fill this in and a maintainer will + put it on the site — usually within a few days. + + **Before you start:** upload your files to a Google Drive folder and set + sharing to *Anyone at RVCE with the link*, or use a public GitHub repo. + + - type: input + id: department + attributes: + label: Department + description: e.g. CSE / ISE / AIML, ECE, Mechanical + placeholder: Computer Science, Information Science, AIML Engineering + validations: + required: true + + - type: input + id: year + attributes: + label: Year + placeholder: "2" + validations: + required: true + + - type: input + id: semester + attributes: + label: Semester + placeholder: "3" + validations: + required: true + + - type: input + id: subject + attributes: + label: Subject name + placeholder: Data Structures and its Applications + validations: + required: true + + - type: input + id: subject_code + attributes: + label: Subject code + placeholder: CSE201 + + - type: dropdown + id: type + attributes: + label: What kind of material is it? + options: + - Theory notes + - Lab material + - Question papers + validations: + required: true + + - type: input + id: title + attributes: + label: Title to show on the site + placeholder: DSA Unit 1–5 notes (Prof. Sharma) + validations: + required: true + + - type: input + id: link + attributes: + label: Link + description: Google Drive folder, Google Doc or GitHub repo. + placeholder: https://drive.google.com/drive/folders/... + validations: + required: true + + - type: input + id: credit + attributes: + label: Name for the contributors page + description: Leave blank to stay anonymous. + + - type: checkboxes + id: confirm + attributes: + label: Confirm + options: + - label: This is my own work, or I have permission to share it. + required: true + - label: The link is accessible to RVCE students. + required: true diff --git a/.github/ISSUE_TEMPLATE/broken-link.yml b/.github/ISSUE_TEMPLATE/broken-link.yml new file mode 100644 index 0000000..13a8282 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/broken-link.yml @@ -0,0 +1,38 @@ +name: Broken or wrong link +description: A note does not open, or points at the wrong material. +title: "Broken link: " +labels: ["broken-link"] +body: + - type: input + id: page + attributes: + label: Page + description: The page on the site where you found it. + placeholder: /cse-ise-aiml/2/3 + validations: + required: true + + - type: input + id: note + attributes: + label: Which note? + placeholder: DMS Study Materials + validations: + required: true + + - type: dropdown + id: problem + attributes: + label: What happens? + options: + - Link is dead / 404 + - Asks for access I cannot get + - Opens the wrong material + - Something else + validations: + required: true + + - type: input + id: replacement + attributes: + label: Working link, if you have one diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..35321ac --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: true +contact_links: + - name: Ask the RVCE community + url: https://www.reddit.com/r/KnotesCentral/ + about: General questions about notes, subjects or the site. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d39b6de --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,35 @@ +name: CI + +on: + pull_request: + push: + branches: [main] + +jobs: + verify: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # What's New reads dates from history + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - run: npm ci + + # Fast, contributor-friendly check first: a bad catalog entry fails here + # with a message that says how to fix it. + - name: Validate catalog data + run: npm run check:data + + - name: Typecheck + run: npm run typecheck + + - name: Lint + run: npm run lint + + - name: Build + run: npm run build diff --git a/.github/workflows/link-check.yml b/.github/workflows/link-check.yml new file mode 100644 index 0000000..56995df --- /dev/null +++ b/.github/workflows/link-check.yml @@ -0,0 +1,70 @@ +name: Link check + +on: + schedule: + - cron: "0 3 * * 1" # Mondays, 03:00 UTC + workflow_dispatch: + +# Drive folders die when the RVCE account that owned them is purged, so the +# catalog rots silently. This finds the damage weekly and files it as an issue. +jobs: + check: + runs-on: ubuntu-latest + permissions: + contents: read + issues: write + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Check every resource link + run: node scripts/check-links.mjs > link-report.txt 2>/dev/null || true + + - name: Summarise + run: cat link-report.txt >> "$GITHUB_STEP_SUMMARY" + + - name: File or update the tracking issue + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const report = fs.readFileSync('link-report.txt', 'utf8'); + const title = 'Broken links — automated weekly check'; + const body = [ + 'Automated check of every link in `src/data/departments/`.', + '', + '`needs-auth` is normal for RVCE-restricted Drive folders and is not a problem.', + '`probably-gone` means Drive returned 404 — the folder was almost certainly deleted.', + '', + '```', + report.trim().slice(0, 55000), + '```', + ].join('\n'); + + const { data: issues } = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + labels: 'broken-link', + }); + + const existing = issues.find((issue) => issue.title === title); + + if (existing) { + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: existing.number, + body, + }); + } else { + await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title, + body, + labels: ['broken-link'], + }); + } diff --git a/.gitignore b/.gitignore index 5ef6a52..d4251fc 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,7 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# generated at build time (added.json is committed so a fresh clone typechecks) +/public/icon-*.png +/public/apple-icon.png diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a182bae..f443eea 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,48 +1,94 @@ # Contributing to Knotes Central -Thank you for considering contributing to Knotes Central! Your contributions help us create a comprehensive resource for students. Please follow the guidelines below to ensure a smooth contribution process. - -## How to Contribute Notes - -### 1. Submit Your Contribution - -- **Use RVCE Mail ID:** Contributions will only be accepted from users with an RVCE mail ID. -- **Upload ID Card:** Please upload a clear image of your RVCE ID card through the Google form provided on the [Credits page](credits.html). - -### 2. Fill Out the Google Form - -1. Go to the [Credits page](https://developer1010x.github.io/KnotesCentral/main/credits.html). -2. Navigate to the "Report Issues and Send Content" section. -3. Fill out the Google form with the following details: - - Your full name - - Your RVCE mail ID - - Link to your Google Drive folder containing the notes/materials you wish to contribute - - Any additional comments or information - -### 3. Content Review - -- **Content Verification:** Once submitted, your content will be reviewed by our team for accuracy and relevance. -- **Approval:** If approved, your content will be added to the appropriate section of Knotes Central. - -## Important Guidelines - -- **Original Content:** Ensure that the content you submit is your own work or that you have the necessary permissions to share it. -- **File Format:** Preferred formats are PDF, DOCX, and PPTX for documents and ZIP for multiple files. -- **Quality:** Make sure the notes are clear, well-organized, and free from any inappropriate content. - -## Liability - -By submitting content, you agree that: - -- The content will be checked and verified by our team. -- If any issues arise, you will be liable for the content you submitted. - -## Contributing to the GitHub Repository - -We welcome contributions to the Knotes Central GitHub repository as well. Here are some general guidelines for contributing code, documentation, or other materials: - -### 1. Fork the Repository - -1. Fork the repository to your own GitHub account. -2. Clone the forked repository to your local machine: - git clone https://github.com/your-username/KnotesCentral.git +Everything on the site was uploaded by a student a year or two ahead of whoever +is reading it. Adding to it should take a minute — here are the two ways. + +## 1. The short form (no code) + +**[→ Add notes](https://github.com/Developer1010x/KnotesCentral-Source-Code/issues/new?template=add-notes.yml)** + +1. Upload your files to a Google Drive folder and share it with RVCE (or push + them to a public GitHub repo). +2. Open the form, pick the department / year / semester, paste the link. +3. Submit. A maintainer adds the entry, and your name goes on the + [contributors page](https://knotescentral.github.io/contributors). + +You need a free GitHub account and nothing else. No Git, no cloning. + +Broken link instead? +**[Report it](https://github.com/Developer1010x/KnotesCentral-Source-Code/issues/new?template=broken-link.yml)** — +that counts as contributing too. + +## 2. Edit the department file directly + +The whole catalog is plain data: one file per department under +`src/data/departments/`. Open the file for your branch on GitHub, hit the pencil +icon, and add your entry. GitHub forks the repo and opens a pull request for +you — nothing can break. + +Find the right `semester`, then add to its `subjects` array: + +```ts +{ + name: "Data Structures and its Applications", + subject_code: "CSE201", + notes: [ + { + title: "DSA Unit 1-5 notes", + type: "theory", // "theory" | "lab" | "question-paper" + link: "https://drive.google.com/drive/folders/...", + }, + ], +} +``` + +Keep the shape exactly as above. The site reads these files directly, so a typo +fails the build on your pull request rather than breaking a page. + +New department? Add `src/data/departments/.ts` following any existing +file, then register it in `src/data/departments/index.ts`. + +Adding yourself to the contributors page: append an entry to +`src/data/contributors.ts`. + +## Guidelines + +- **Original content.** Submit your own work, or material you have permission to + share. +- **Access.** Links must be openable by RVCE students — share Drive folders with + *Anyone at RVCE with the link*. +- **Formats.** PDF, DOCX, PPTX for documents; ZIP for bundles; a public repo for + code. +- **Quality.** Clear, organised, correctly labelled with the subject and, where + it matters, the professor or year. + +By submitting content you confirm it is yours to share, and that you are +responsible for what you submit. + +## Working on the site itself + +```bash +npm install +npm run dev # http://localhost:3000 +npm run check:data # validates the catalog — run this before opening a PR +npm run typecheck +npm run lint +npm run build # prerenders every department, year, semester and subject +``` + +`check:data` is the one that matters for content PRs. It catches unbalanced +braces, invalid `type` values, links missing `https://`, empty titles, and +departments missing from `index.ts` — and says how to fix each one. CI runs it +on every pull request, so a bad entry fails the check instead of shipping a +broken card. + +- `src/data/` — the catalog (departments, contributors) and its types. +- `src/lib/` — lookups, search, note-type metadata, theme, site links. +- `src/components/ui/` — the shared design-system pieces. +- `src/app/` — routes; the dynamic ones prerender via `generateStaticParams`. +- `scripts/` — build-time helpers: catalog validation, PWA icons, and the + What's New dates (read from git history, so no manual changelog). + +Colours are CSS custom properties in `src/app/globals.css`, exposed to Tailwind +as semantic names (`bg-surface`, `text-muted`, `border-line`, `text-brand`). +Prefer those over raw palette classes so both themes stay correct. diff --git a/README.md b/README.md index e8d13b7..39c2938 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,69 @@ -# Knotes-Central -https://knotes-central-v3.github.io/KnotesCentralV2/ +# Knotes Central -A humble attempt to create a collaborative platform for students to share notes and resources. +Notes, lab manuals and previous-year question papers for RVCE — every +department, year and semester in one place. -## Website Developers: +**Contribute:** [add notes in about a minute](https://github.com/Developer1010x/KnotesCentral-Source-Code/issues/new?template=add-notes.yml) +· [report a broken link](https://github.com/Developer1010x/KnotesCentral-Source-Code/issues/new?template=broken-link.yml) +· [CONTRIBUTING.md](CONTRIBUTING.md) -- [Developer1010x](https://developer1010x.github.io/PORTFOLIO/) V1 & Idea S Prajwall N +## What it does -- [KTS-o7](https://kts-o7.github.io) Refactoring and Updates Krishna Tejaswi +- **Browse** by department → year → semester → subject, with note counts and + coverage badges at every level, so you can see what exists before you click. +- **Search** subjects by name, code or note title — from the search page, or + with ⌘K / Ctrl+K anywhere on the site. +- **Filter** by material type: theory notes, lab material, question papers. +- **Know where a link goes** before opening it — Drive (RVCE login) vs GitHub. +- **Set your semester once** and land on your own subjects every visit. +- **Save notes and tick off subjects** — bookmarks and revision progress, stored + on your device, no account. +- **Exam prep view** — a whole semester's material in one list, grouped by type. +- **Installable** on Android/iOS, with offline browsing of pages you have seen. +- **What's new** feed (plus [RSS](/feed.xml)), dated automatically from git + history. +- Light and dark themes, mobile-first layout, and every page prerendered as + static HTML. -## Overview +Most material lives in RVCE Workspace Drive, so you will usually need to be +signed in with your college account. -Knotes-Central is a collaborative platform designed to help students share notes, resources, and past papers. The platform aims to make it easier for students to access the study materials they need and contribute their own resources to help their peers. +## How the catalog works -## Features +The catalog is plain TypeScript data, not a database — one file per department: -- **User-Friendly Interface:** An intuitive design to easily navigate through different courses and resources. -- **Collaborative Sharing:** Users can upload and share notes, question papers, and other study materials. -- **Course Categorization:** Materials are organized based on courses, making it easy to find relevant resources. -- **Search Functionality:** Quickly find specific notes or documents using the search feature. -- **Open Drive Storage:** All materials are stored in an open drive for easy access. +``` +src/data/departments/.ts one Department each, registered in index.ts +src/data/contributors.ts the contributors page +src/data/types.ts the shape everything must match +``` -## Getting Started -- Use RVCE EMail to access +Adding notes means adding an object to a `subjects` array. Anyone can do it +through the GitHub editor; see [CONTRIBUTING.md](CONTRIBUTING.md). + +## Running it + +```bash +npm install +npm run dev # http://localhost:3000 +npm run check:data # validate the catalog +npm run typecheck +npm run lint +npm run build +``` + +CI runs all four on every pull request. + +Next.js App Router + Tailwind. Colours are CSS custom properties in +`src/app/globals.css` surfaced as semantic Tailwind names (`bg-surface`, +`text-muted`, `text-brand`), so both themes stay consistent. + +## Credits + +- [Developer1010x](https://developer1010x.github.io/PORTFOLIO/) — V1 and the idea (S Prajwall N) +- [KTS-o7](https://kts-o7.github.io) — refactoring and updates (Krishna Tejaswi) +- Everyone on the [contributors page](https://knotescentral.github.io/contributors) + +Previous versions: +[V1](https://knotes-central-v3.github.io/KnotesCentralV1/) · +[V2](https://knotes-central-v3.github.io/KnotesCentralV2/) diff --git a/package-lock.json b/package-lock.json index 323ca61..e242de8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "knotes-central-v3", "version": "0.1.0", "dependencies": { - "next": "15.1.6", + "next": "^15.1.6", "react": "^19.0.0", "react-dom": "^19.0.0" }, @@ -19,7 +19,7 @@ "@types/react-dom": "^19", "autoprefixer": "^10.4.20", "eslint": "^9", - "eslint-config-next": "15.1.6", + "eslint-config-next": "^15.1.6", "postcss": "^8.5.1", "tailwindcss": "^3.4.17", "typescript": "^5" diff --git a/package.json b/package.json index 377b7e2..8aa48d7 100644 --- a/package.json +++ b/package.json @@ -3,14 +3,20 @@ "version": "0.1.0", "private": true, "scripts": { + "predev": "node scripts/build-changelog.mjs", "dev": "next dev", + "prebuild": "node scripts/build-changelog.mjs && node scripts/build-icons.mjs && node scripts/validate-data.mjs", "build": "next build", "start": "next start", "lint": "next lint", - "export": "next export" + "typecheck": "tsc --noEmit", + "icons": "node scripts/build-icons.mjs", + "check:data": "node scripts/validate-data.mjs", + "check:links": "node scripts/check-links.mjs", + "check:links:write": "node scripts/check-links.mjs --write" }, "dependencies": { - "next": "latest", + "next": "^15.1.6", "react": "^19.0.0", "react-dom": "^19.0.0" }, @@ -21,10 +27,9 @@ "@types/react-dom": "^19", "autoprefixer": "^10.4.20", "eslint": "^9", - "eslint-config-next": "latest", + "eslint-config-next": "^15.1.6", "postcss": "^8.5.1", "tailwindcss": "^3.4.17", "typescript": "^5" } } - diff --git a/postcss.config.js b/postcss.config.js deleted file mode 100644 index 4230d3e..0000000 --- a/postcss.config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - "postcss-nested": {}, - }, -}; diff --git a/public/manifest.webmanifest b/public/manifest.webmanifest new file mode 100644 index 0000000..b6cb0ed --- /dev/null +++ b/public/manifest.webmanifest @@ -0,0 +1,37 @@ +{ + "name": "Knotes Central — RVCE notes", + "short_name": "Knotes", + "description": "Notes, lab manuals and question papers for RVCE, in one place.", + "start_url": "/", + "scope": "/", + "display": "standalone", + "orientation": "portrait", + "background_color": "#f8fafc", + "theme_color": "#4f46e5", + "categories": ["education", "books"], + "icons": [ + { + "src": "/icon-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/icon-maskable.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ], + "shortcuts": [ + { "name": "Search subjects", "url": "/search" }, + { "name": "Saved", "url": "/saved" }, + { "name": "Contribute", "url": "/contribute" } + ] +} diff --git a/public/sw.js b/public/sw.js new file mode 100644 index 0000000..55317d6 --- /dev/null +++ b/public/sw.js @@ -0,0 +1,78 @@ +/* + * Offline support for the catalog. Pages are network-first (so notes stay + * current on college wifi) with a cached fallback; static assets are + * cache-first. Nothing here caches Drive or GitHub — only this origin. + */ +const VERSION = "knotes-v1"; +const SHELL = `${VERSION}-shell`; +const PAGES = `${VERSION}-pages`; +const OFFLINE_URL = "/offline"; + +const PRECACHE = ["/", "/search", "/saved", OFFLINE_URL, "/manifest.webmanifest"]; + +self.addEventListener("install", (event) => { + event.waitUntil( + caches + .open(SHELL) + .then((cache) => cache.addAll(PRECACHE)) + .then(() => self.skipWaiting()) + .catch(() => self.skipWaiting()) + ); +}); + +self.addEventListener("activate", (event) => { + event.waitUntil( + caches + .keys() + .then((keys) => + Promise.all( + keys + .filter((key) => !key.startsWith(VERSION)) + .map((key) => caches.delete(key)) + ) + ) + .then(() => self.clients.claim()) + ); +}); + +self.addEventListener("fetch", (event) => { + const { request } = event; + if (request.method !== "GET") return; + + const url = new URL(request.url); + if (url.origin !== self.location.origin) return; + + // Immutable build output — cache first. + if (url.pathname.startsWith("/_next/static/")) { + event.respondWith( + caches.match(request).then( + (hit) => + hit || + fetch(request).then((response) => { + const copy = response.clone(); + caches.open(SHELL).then((cache) => cache.put(request, copy)); + return response; + }) + ) + ); + return; + } + + // Pages — network first, fall back to whatever we have. + if (request.mode === "navigate") { + event.respondWith( + fetch(request) + .then((response) => { + const copy = response.clone(); + caches.open(PAGES).then((cache) => cache.put(request, copy)); + return response; + }) + .catch(() => + caches + .match(request) + .then((hit) => hit || caches.match(OFFLINE_URL)) + .then((hit) => hit || Response.error()) + ) + ); + } +}); diff --git a/scripts/build-changelog.mjs b/scripts/build-changelog.mjs new file mode 100644 index 0000000..aa3160a --- /dev/null +++ b/scripts/build-changelog.mjs @@ -0,0 +1,84 @@ +#!/usr/bin/env node +/** + * Derives "when was this note added" from git history, so the What's New feed + * needs no manual bookkeeping — the commit that first introduced a link is its + * publication date. + * + * Writes src/data/generated/added.json: { [note link]: ISO date }. + * Degrades to an empty map when git history is unavailable (shallow CI clones), + * which simply means nothing is badged as new. + */ +import { execFileSync } from "node:child_process"; +import { mkdirSync, writeFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const OUT = resolve(root, "src/data/generated/added.json"); + +const git = (...args) => + execFileSync("git", args, { cwd: root, encoding: "utf8", maxBuffer: 64 << 20 }); + +const isDataFile = (path) => + path.startsWith("src/data/") && path.endsWith(".ts"); + +const LINK = /link:\s*"([^"]+)"/g; + +function linksIn(text) { + return new Set(Array.from(text.matchAll(LINK), (match) => match[1])); +} + +function build() { + const log = git( + "log", + "--reverse", + "--format=%H\t%cI", + "--", + "src/data" + ).trim(); + + if (!log) return {}; + + const added = {}; + const seen = new Set(); + + for (const line of log.split("\n")) { + const [sha, date] = line.split("\t"); + + const files = git("ls-tree", "-r", "--name-only", sha) + .split("\n") + .filter(isDataFile); + + for (const file of files) { + let contents = ""; + try { + contents = git("show", `${sha}:${file}`); + } catch { + continue; + } + + for (const link of linksIn(contents)) { + if (seen.has(link)) continue; + seen.add(link); + added[link] = date; + } + } + } + + return added; +} + +let added = {}; +try { + added = build(); + console.log( + `build-changelog: dated ${Object.keys(added).length} links from git history` + ); +} catch (error) { + console.warn( + `build-changelog: no git history available (${error.message.split("\n")[0]}) — What's New will be empty` + ); +} + +mkdirSync(dirname(OUT), { recursive: true }); +writeFileSync(OUT, `${JSON.stringify(added, null, 2)}\n`); diff --git a/scripts/build-icons.mjs b/scripts/build-icons.mjs new file mode 100644 index 0000000..4231b88 --- /dev/null +++ b/scripts/build-icons.mjs @@ -0,0 +1,58 @@ +#!/usr/bin/env node +/** + * Renders the PWA icons from one source description, so the manifest can point + * at stable file names (Next's generated icon routes are content-hashed). + */ +import { ImageResponse } from "next/og.js"; +import { mkdirSync, writeFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); + +const BRAND = "#4f46e5"; + +function icon(size, maskable) { + const pad = maskable ? size * 0.14 : 0; + const radius = maskable ? size / 2 : size * 0.22; + + return { + type: "div", + props: { + style: { + width: size, + height: size, + display: "flex", + alignItems: "center", + justifyContent: "center", + background: BRAND, + borderRadius: maskable ? 0 : radius, + color: "#fff", + fontSize: size * (maskable ? 0.42 : 0.5), + fontWeight: 700, + letterSpacing: "-0.05em", + padding: pad, + }, + children: "K", + }, + }; +} + +const TARGETS = [ + ["public/icon-192.png", 192, false], + ["public/icon-512.png", 512, false], + ["public/icon-maskable.png", 512, true], + ["public/apple-icon.png", 180, false], +]; + +for (const [file, size, maskable] of TARGETS) { + const response = new ImageResponse(icon(size, maskable), { + width: size, + height: size, + }); + const buffer = Buffer.from(await response.arrayBuffer()); + const out = resolve(root, file); + mkdirSync(dirname(out), { recursive: true }); + writeFileSync(out, buffer); + console.log(`build-icons: ${file} (${buffer.length} bytes)`); +} diff --git a/scripts/check-links.mjs b/scripts/check-links.mjs new file mode 100644 index 0000000..23b1533 --- /dev/null +++ b/scripts/check-links.mjs @@ -0,0 +1,180 @@ +#!/usr/bin/env node +/** + * Pings every resource link and classifies what came back. + * + * npm run check:links # human-readable report + * npm run check:links -- --json # machine-readable, for CI + * + * IMPORTANT CAVEAT about Google Drive: an anonymous request cannot tell + * "deleted" apart from "shared only with RVCE accounts" — both look like a + * sign-in wall or a 404. Drive results are therefore reported as `needs-auth` + * or `probably-gone`, never as a confident "dead". GitHub links are checkable + * properly and are reported as ok/dead. + */ +import { readFileSync, readdirSync, writeFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const DIR = resolve(root, "src/data/departments"); +const JSON_OUT = process.argv.includes("--json"); +const WRITE = process.argv.includes("--write"); +const STATUS_OUT = resolve(root, "src/data/generated/link-status.json"); +const CONCURRENCY = 8; +const TIMEOUT_MS = 15000; + +/** Pull every note link out of the data files, with where it came from. */ +function collect() { + const links = []; + + for (const file of readdirSync(DIR).filter( + (name) => name.endsWith(".ts") && name !== "index.ts" + )) { + const text = readFileSync(resolve(DIR, file), "utf8"); + const entries = text.matchAll( + /title:\s*"((?:[^"\\]|\\.)*)",\s*\n\s*type:\s*"([^"]*)",\s*\n\s*link:\s*"([^"]*)"/g + ); + + for (const [, title, type, link] of entries) { + links.push({ file, title, type, link }); + } + } + + return links; +} + +function classifyDrive(response, body) { + // Drive serves a sign-in interstitial for anything not public. + if (response.status === 404) return "probably-gone"; + if (response.status === 403) return "needs-auth"; + if (response.url.includes("accounts.google.com")) return "needs-auth"; + if (/Sign in|Request access|You need access/i.test(body)) return "needs-auth"; + if (/(no longer exists|item has been (deleted|removed)|Page not found)/i.test(body)) { + return "probably-gone"; + } + if (response.ok) return "ok"; + return "unknown"; +} + +async function check(entry) { + if (!entry.link.trim()) return { ...entry, status: "missing" }; + + const isDrive = /(drive|docs)\.google\.com/i.test(entry.link); + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), TIMEOUT_MS); + + try { + const response = await fetch(entry.link, { + redirect: "follow", + signal: controller.signal, + headers: { + // Drive returns very different bodies to non-browser agents. + "user-agent": + "Mozilla/5.0 (compatible; KnotesCentralLinkCheck/1.0; +https://github.com/Developer1010x/KnotesCentral-Source-Code)", + }, + }); + + const body = isDrive ? (await response.text()).slice(0, 20000) : ""; + const status = isDrive + ? classifyDrive(response, body) + : response.status === 404 + ? "dead" + : response.ok + ? "ok" + : "unknown"; + + return { ...entry, status, code: response.status }; + } catch (error) { + return { + ...entry, + status: "unreachable", + code: 0, + error: error.name === "AbortError" ? "timeout" : error.message, + }; + } finally { + clearTimeout(timer); + } +} + +async function run() { + const entries = collect(); + const results = []; + let index = 0; + + async function worker() { + while (index < entries.length) { + const entry = entries[index++]; + results.push(await check(entry)); + if (!JSON_OUT && results.length % 20 === 0) { + process.stderr.write(` checked ${results.length}/${entries.length}\n`); + } + } + } + + await Promise.all( + Array.from({ length: Math.min(CONCURRENCY, entries.length) }, worker) + ); + + const byStatus = results.reduce((counts, result) => { + counts[result.status] = (counts[result.status] ?? 0) + 1; + return counts; + }, {}); + + if (WRITE) { + // Only record verdicts the site should act on. "needs-auth" is the normal, + // healthy state for RVCE-restricted material and must never be flagged. + const gone = {}; + for (const result of results) { + if (["probably-gone", "dead"].includes(result.status) && result.link) { + gone[result.link] = result.status; + } + } + writeFileSync( + STATUS_OUT, + `${JSON.stringify( + { checkedAt: new Date().toISOString(), gone }, + null, + 2 + )}\n` + ); + console.log( + `check-links: recorded ${Object.keys(gone).length} broken links in src/data/generated/link-status.json` + ); + } + + if (JSON_OUT) { + writeFileSync( + resolve(root, "link-report.json"), + `${JSON.stringify({ checkedAt: new Date().toISOString(), byStatus, results }, null, 2)}\n` + ); + console.log(JSON.stringify(byStatus, null, 2)); + return; + } + + console.log("\n=== link check ==="); + for (const [status, count] of Object.entries(byStatus).sort( + (a, b) => b[1] - a[1] + )) { + console.log(`${String(count).padStart(4)} ${status}`); + } + + const problems = results.filter((result) => + ["dead", "probably-gone", "unreachable", "missing"].includes(result.status) + ); + + if (problems.length) { + console.log("\nNeeds attention:"); + for (const problem of problems) { + console.log( + ` [${problem.status}] ${problem.file}: ${problem.title}\n ${problem.link || "(no link)"}` + ); + } + } + + console.log( + "\nNote: Drive links shared only with RVCE accounts look identical to deleted ones\n" + + "from an anonymous check. Verify `needs-auth` entries while signed in.\n" + ); +} + +run(); diff --git a/scripts/validate-data.mjs b/scripts/validate-data.mjs new file mode 100644 index 0000000..9add4e2 --- /dev/null +++ b/scripts/validate-data.mjs @@ -0,0 +1,141 @@ +#!/usr/bin/env node +/** + * Catches the mistakes a first-time contributor actually makes, with a message + * that says how to fix it. Runs in CI on every pull request, so a bad entry + * fails the check instead of shipping a broken card. + * + * npm run check:data + */ +import { readdirSync, readFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const DIR = resolve(root, "src/data/departments"); + +const VALID_TYPES = new Set(["theory", "lab", "question-paper"]); + +const problems = []; +const warnings = []; + +const fail = (file, message) => problems.push({ file, message }); +const warn = (file, message) => warnings.push({ file, message }); + +/** Reads the exported object literals without executing TypeScript. */ +function fields(text, key) { + return Array.from( + text.matchAll(new RegExp(`${key}:\\s*"((?:[^"\\\\]|\\\\.)*)"`, "g")), + (match) => match[1] + ); +} + +const files = readdirSync(DIR).filter( + (file) => file.endsWith(".ts") && file !== "index.ts" +); + +const index = readFileSync(resolve(DIR, "index.ts"), "utf8"); +const seenLinks = new Map(); +const seenSlugs = new Set(); + +for (const file of files) { + const path = `src/data/departments/${file}`; + const text = readFileSync(resolve(DIR, file), "utf8"); + + // Registered in the barrel file? + const moduleName = file.replace(/\.ts$/, ""); + if (!index.includes(`"./${moduleName}"`)) { + fail( + path, + `not imported in index.ts — add it there or the department will not appear on the site` + ); + } + + // Balanced braces catch the classic "pasted one brace too few". + const open = (text.match(/{/g) ?? []).length; + const close = (text.match(/}/g) ?? []).length; + if (open !== close) { + fail(path, `unbalanced braces (${open} "{" vs ${close} "}")`); + } + + const links = fields(text, "link"); + const types = fields(text, "type"); + const titles = fields(text, "title"); + + const deptLink = links.find((link) => link.startsWith("/")); + if (!deptLink) { + fail(path, `no department link — needs a top-level link like "/cse-ise-aiml"`); + } else { + const slug = deptLink.replace(/^\//, ""); + if (seenSlugs.has(slug.toLowerCase())) { + fail(path, `duplicate department link "${deptLink}"`); + } + seenSlugs.add(slug.toLowerCase()); + } + + for (const type of types) { + if (!VALID_TYPES.has(type)) { + fail( + path, + `type "${type}" is not valid — use ${[...VALID_TYPES] + .map((value) => `"${value}"`) + .join(", ")}` + ); + } + } + + for (const title of titles) { + if (!title.trim()) fail(path, `a note has an empty title`); + } + + for (const link of links) { + if (link.startsWith("/")) continue; // department route, not a resource + + if (!link.trim()) { + warn(path, `a note has no link yet — it shows as "not uploaded yet" on the site`); + continue; + } + + if (!/^https?:\/\//i.test(link)) { + fail(path, `link "${link}" must start with http:// or https://`); + continue; + } + + if (/^https?:\/\/(drive|docs)\.google\.com\/?$/i.test(link)) { + fail(path, `link "${link}" points at Google Drive's home page, not a file`); + } + + const previous = seenLinks.get(link); + if (previous && previous !== path) { + warn(path, `link also used in ${previous} — is that intentional?`); + } + seenLinks.set(link, path); + } + + // Notes without a matching title count usually mean a malformed entry. + if (titles.length !== types.length) { + fail( + path, + `${titles.length} titles but ${types.length} types — every note needs both` + ); + } +} + +for (const { file, message } of warnings) { + console.warn(`warning ${file}: ${message}`); +} + +if (problems.length) { + console.error(""); + for (const { file, message } of problems) { + console.error(`error ${file}: ${message}`); + } + console.error( + `\n${problems.length} problem${problems.length === 1 ? "" : "s"} found. ` + + `See CONTRIBUTING.md for the expected shape.` + ); + process.exit(1); +} + +console.log( + `check:data — ${files.length} departments, ${seenLinks.size} links, no problems.` +); diff --git a/src/app/[department]/[year]/[semester]/[subject]/opengraph-image.tsx b/src/app/[department]/[year]/[semester]/[subject]/opengraph-image.tsx new file mode 100644 index 0000000..dccec2d --- /dev/null +++ b/src/app/[department]/[year]/[semester]/[subject]/opengraph-image.tsx @@ -0,0 +1,110 @@ +import { ImageResponse } from "next/og"; +import { departments } from "@/data/departments"; +import { + countNotes, + departmentSlug, + subjectSlug, + findDepartment, + findSemester, + findSubject, + findYear, +} from "@/lib/catalog"; +import { SITE } from "@/lib/site"; + +export const alt = "Subject on Knotes Central"; +export const size = { width: 1200, height: 630 }; +export const contentType = "image/png"; + +export function generateStaticParams() { + return departments.flatMap((department) => + department.years.flatMap((year) => + year.semesters.flatMap((semester) => + semester.subjects.map((subject) => ({ + department: departmentSlug(department), + year: String(year.year), + semester: String(semester.number), + subject: subjectSlug(subject, semester), + })) + ) + ) + ); +} + +/** + * Per-subject share card: what a WhatsApp link preview shows when a junior + * forwards "DSA notes" into a batch group. + */ +export default async function Image({ + params, +}: { + params: Promise<{ + department: string; + year: string; + semester: string; + subject: string; + }>; +}) { + const { department: deptSlug, year: y, semester: s, subject: sub } = await params; + const department = findDepartment(deptSlug); + const year = findYear(department, y); + const semester = findSemester(year, s); + const subject = findSubject(semester, sub); + + const counts = subject ? countNotes(subject.notes) : null; + + return new ImageResponse( + ( +
+
+
+ K +
+
{SITE.name}
+
+ +
+
+ {(department?.name ?? "RVCE").toUpperCase()} +
+
+ {subject?.name ?? "Subject"} +
+
+ {subject?.subject_code ?
{subject.subject_code}
: null} +
Year {year?.year ?? "?"}
+
Semester {semester?.number ?? "?"}
+
+
+ +
+ {counts?.total ?? 0} resources · notes, labs and papers +
+
+ ), + size + ); +} diff --git a/src/app/[department]/[year]/[semester]/[subject]/page.tsx b/src/app/[department]/[year]/[semester]/[subject]/page.tsx new file mode 100644 index 0000000..a5c4952 --- /dev/null +++ b/src/app/[department]/[year]/[semester]/[subject]/page.tsx @@ -0,0 +1,226 @@ +import type { Metadata } from "next"; +import Link from "next/link"; +import { notFound } from "next/navigation"; +import { departments } from "@/data/departments"; +import { PageHeader } from "@/components/ui/PageHeader"; +import { CoverageBadges } from "@/components/ui/Badge"; +import { NoteLink } from "@/components/NoteLink"; +import { ShareButton } from "@/components/ShareButton"; +import { DoneToggle } from "@/components/Progress"; +import { TrackVisit } from "@/components/RecentlyViewed"; +import { AddNotesButton } from "@/components/ContributeCTA"; +import { Icon } from "@/components/ui/icons"; +import { + countNotes, + departmentSlug, + findDepartment, + findSemester, + findSubject, + findYear, + subjectSlug, +} from "@/lib/catalog"; +import { isNew } from "@/lib/changelog"; +import { isGone } from "@/lib/linkHealth"; +import { reportIssueUrl } from "@/lib/site"; +import { JsonLd, breadcrumbLd, courseLd } from "@/components/StructuredData"; + +type Params = Promise<{ + department: string; + year: string; + semester: string; + subject: string; +}>; + +export function generateStaticParams() { + return departments.flatMap((department) => + department.years.flatMap((year) => + year.semesters.flatMap((semester) => + semester.subjects.map((subject) => ({ + department: departmentSlug(department), + year: String(year.year), + semester: String(semester.number), + subject: subjectSlug(subject, semester), + })) + ) + ) + ); +} + +async function resolve(params: Params) { + const { + department: deptSlug, + year: yearParam, + semester: semesterParam, + subject: subjectParam, + } = await params; + + const department = findDepartment(deptSlug); + const year = findYear(department, yearParam); + const semester = findSemester(year, semesterParam); + const subject = findSubject(semester, subjectParam); + + return { department, year, semester, subject }; +} + +export async function generateMetadata({ + params, +}: { + params: Params; +}): Promise { + const { department, year, semester, subject } = await resolve(params); + if (!department || !year || !semester || !subject) { + return { title: "Subject not found" }; + } + + const title = `${subject.name}${ + subject.subject_code ? ` (${subject.subject_code})` : "" + }`; + const description = `${subject.notes.length} resources for ${subject.name} — ${department.name}, year ${year.year}, semester ${semester.number} at RVCE.`; + + return { + title, + description, + openGraph: { title, description }, + }; +} + +export default async function SubjectPage({ params }: { params: Params }) { + const { department, year, semester, subject } = await resolve(params); + if (!department || !year || !semester || !subject) notFound(); + + const semesterPath = `${department.link}/${year.year}/${semester.number}`; + const path = `${semesterPath}/${subjectSlug(subject, semester)}`; + const counts = countNotes(subject.notes); + const siblings = semester.subjects.filter((other) => other !== subject); + + const trail = [ + { label: "Departments", href: "/" }, + { label: department.name, href: department.link }, + { label: `Year ${year.year}`, href: `${department.link}/${year.year}` }, + { label: `Semester ${semester.number}`, href: semesterPath }, + { label: subject.name }, + ]; + + return ( +
+ + + + + +
+ {subject.subject_code && ( + + {subject.subject_code} + + )} + + + {counts.total} {counts.total === 1 ? "resource" : "resources"} + +
+ +
+ + + +
+
+ +
+

+ Resources +

+ + {subject.notes.length ? ( +
+ {subject.notes.map((note) => ( + + ))} +
+ ) : ( +
+ Nothing uploaded for this subject yet. +
+ )} + +

+ A link not opening?{" "} + + Report it + {" "} + and it gets fixed. +

+
+ + {siblings.length > 0 && ( +
+

+ Also in semester {semester.number} +

+
+ {siblings.map((other) => ( + + {other.name} + + ))} +
+ + + Exam prep for this semester + +
+ )} +
+ ); +} diff --git a/src/app/[department]/[year]/[semester]/exam-prep/page.tsx b/src/app/[department]/[year]/[semester]/exam-prep/page.tsx new file mode 100644 index 0000000..2f5f8b8 --- /dev/null +++ b/src/app/[department]/[year]/[semester]/exam-prep/page.tsx @@ -0,0 +1,189 @@ +import type { Metadata } from "next"; +import Link from "next/link"; +import { notFound } from "next/navigation"; +import { departments } from "@/data/departments"; +import type { Note, NoteType } from "@/data/types"; +import { PageHeader } from "@/components/ui/PageHeader"; +import { NoteLink } from "@/components/NoteLink"; +import { ShareButton } from "@/components/ShareButton"; +import { SemesterSwitcher } from "@/components/SemesterSwitcher"; +import { CopyLinks } from "@/components/CopyLinks"; +import { SemesterProgress } from "@/components/Progress"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { Icon } from "@/components/ui/icons"; +import { + countNotes, + departmentSlug, + findDepartment, + findSemester, + findYear, + semesterNotes, + subjectSlug, +} from "@/lib/catalog"; +import { isNew } from "@/lib/changelog"; +import { isGone } from "@/lib/linkHealth"; +import { NOTE_TYPE_ORDER, noteTypeMeta } from "@/lib/noteTypes"; + +type Params = Promise<{ department: string; year: string; semester: string }>; + +export function generateStaticParams() { + return departments.flatMap((department) => + department.years.flatMap((year) => + year.semesters.map((semester) => ({ + department: departmentSlug(department), + year: String(year.year), + semester: String(semester.number), + })) + ) + ); +} + +async function resolve(params: Params) { + const { + department: deptSlug, + year: yearParam, + semester: semesterParam, + } = await params; + const department = findDepartment(deptSlug); + const year = findYear(department, yearParam); + const semester = findSemester(year, semesterParam); + return { department, year, semester }; +} + +export async function generateMetadata({ + params, +}: { + params: Params; +}): Promise { + const { department, year, semester } = await resolve(params); + if (!department || !year || !semester) return { title: "Not found" }; + + return { + title: `Exam prep · ${department.name} Semester ${semester.number}`, + description: `Every note, lab manual and question paper for semester ${semester.number} of ${department.name}, grouped for revision.`, + }; +} + +export default async function ExamPrepPage({ params }: { params: Params }) { + const { department, year, semester } = await resolve(params); + if (!department || !year || !semester) notFound(); + + const semesterPath = `${department.link}/${year.year}/${semester.number}`; + const counts = countNotes(semesterNotes(semester)); + + // Everything in the semester, regrouped by what it is rather than by subject. + const byType = new Map>(); + for (const subject of semester.subjects) { + const path = `${semesterPath}/${subjectSlug(subject, semester)}`; + for (const note of subject.notes) { + const bucket = byType.get(note.type) ?? []; + bucket.push({ note, subject: subject.name, path }); + byType.set(note.type, bucket); + } + } + + const subjectPaths = semester.subjects.map( + (subject) => `${semesterPath}/${subjectSlug(subject, semester)}` + ); + + const groups = NOTE_TYPE_ORDER.filter((type) => byType.get(type)?.length); + + return ( +
+ +
+ + ({ + title: note.title, + link: note.link, + }))} + /> + + + Browse by subject + +
+ +
+ ({ + year: y.year, + semesters: y.semesters.map((s) => s.number), + }))} + current={{ year: year.year, semester: semester.number }} + /> +
+
+ + + + {counts.total === 0 ? ( + + ) : ( + groups.map((type) => { + const meta = noteTypeMeta(type); + const items = byType.get(type) ?? []; + + return ( +
+
+ +

+ {meta.label} +

+ {items.length} +
+ +
+ {items.map(({ note, subject, path }) => ( +
+ + + {subject} + +
+ ))} +
+
+ ); + }) + )} +
+ ); +} diff --git a/src/app/[department]/[year]/[semester]/page.tsx b/src/app/[department]/[year]/[semester]/page.tsx index 81e3f25..f6f91aa 100644 --- a/src/app/[department]/[year]/[semester]/page.tsx +++ b/src/app/[department]/[year]/[semester]/page.tsx @@ -1,117 +1,172 @@ -'use client'; -import { departments } from "../../../../data/mockData"; +import type { Metadata } from "next"; import { notFound } from "next/navigation"; -import { use } from "react"; +import { departments } from "@/data/departments"; +import { CardGrid } from "@/components/ui/Card"; +import { CoverageBadges } from "@/components/ui/Badge"; +import { PageHeader } from "@/components/ui/PageHeader"; +import { SubjectCard } from "@/components/SubjectCard"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { TrackVisit } from "@/components/RecentlyViewed"; +import { SemesterProgress } from "@/components/Progress"; +import { ShareButton } from "@/components/ShareButton"; +import { SemesterSwitcher } from "@/components/SemesterSwitcher"; +import Link from "next/link"; +import { Icon } from "@/components/ui/icons"; +import { ContributeBanner } from "@/components/ContributeCTA"; +import { + countNotes, + departmentSlug, + findDepartment, + findSemester, + findYear, + semesterNotes, + subjectSlug, +} from "@/lib/catalog"; -export default function SemesterPage({ +export function generateStaticParams() { + return departments.flatMap((department) => + department.years.flatMap((year) => + year.semesters.map((semester) => ({ + department: departmentSlug(department), + year: String(year.year), + semester: String(semester.number), + })) + ) + ); +} + +export async function generateMetadata({ + params, +}: { + params: Promise<{ department: string; year: string; semester: string }>; +}): Promise { + const { + department: slug, + year: yearParam, + semester: semesterParam, + } = await params; + const department = findDepartment(slug); + const year = findYear(department, yearParam); + const semester = findSemester(year, semesterParam); + if (!department || !year || !semester) return { title: "Not found" }; + + const subjects = semester.subjects.map((s) => s.name).join(", "); + + return { + title: `${department.name} · Semester ${semester.number}`, + description: `Notes, lab material and question papers for semester ${semester.number}: ${subjects}.`, + }; +} + +export default async function SemesterPage({ params, }: { params: Promise<{ department: string; year: string; semester: string }>; }) { - const resolvedParams = use(params); - - const department = departments.find( - (dept) => dept.link === `/${resolvedParams.department}` - ); - const year = department?.years.find( - (y) => y.year === parseInt(resolvedParams.year) - ); - const semester = year?.semesters.find( - (s) => s.number === parseInt(resolvedParams.semester) - ); + const { + department: slug, + year: yearParam, + semester: semesterParam, + } = await params; - if (!department || !year || !semester) { - notFound(); - } + const department = findDepartment(slug); + const year = findYear(department, yearParam); + const semester = findSemester(year, semesterParam); + + if (!department || !year || !semester) notFound(); + + const counts = countNotes(semesterNotes(semester)); + const semesterPath = `${department.link}/${year.year}/${semester.number}`; + const subjectPaths = semester.subjects.map( + (subject) => `${semesterPath}/${subjectSlug(subject, semester)}` + ); return (
- {/* Header with Breadcrumb */} -
-
- - {department.name} - - → - - Year {year.year} - - → - Semester {semester.number} + + + +
+ + + {semester.subjects.length} subjects · {counts.total} resources +
-

- Semester {semester.number} -

-
- {/* Subjects Grid */} -
- {semester.subjects.map((subject) => ( -
+ - {/* Subject Header */} -
-
-
- - - -
-
-

- {subject.name} -

- - {subject.subject_code} - -
-
-
+ + Exam prep view + + +
+ +
+ ({ + year: y.year, + semesters: y.semesters.map((s) => s.number), + }))} + current={{ year: year.year, semester: semester.number }} + /> +
+ + + + + {semester.subjects.length ? ( + + {semester.subjects.map((subject) => ( + + ))} + + ) : ( + + )} - {/* Notes Grid */} - -
- ))} -
+
); -} \ No newline at end of file +} diff --git a/src/app/[department]/[year]/page.tsx b/src/app/[department]/[year]/page.tsx index 55d1f63..b2c7bcd 100644 --- a/src/app/[department]/[year]/page.tsx +++ b/src/app/[department]/[year]/page.tsx @@ -1,96 +1,94 @@ -import { departments } from "../../../data/mockData"; +import type { Metadata } from "next"; import { notFound } from "next/navigation"; -import { use } from "react"; +import { departments } from "@/data/departments"; +import { CardGrid } from "@/components/ui/Card"; +import { CoverageBadges } from "@/components/ui/Badge"; +import { PageHeader } from "@/components/ui/PageHeader"; +import { TileLink } from "@/components/ui/TileLink"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { + countNotes, + departmentSlug, + findDepartment, + findYear, + semesterNotes, + yearNotes, +} from "@/lib/catalog"; -export default function YearPage({ +export function generateStaticParams() { + return departments.flatMap((department) => + department.years.map((year) => ({ + department: departmentSlug(department), + year: String(year.year), + })) + ); +} + +export async function generateMetadata({ + params, +}: { + params: Promise<{ department: string; year: string }>; +}): Promise { + const { department: slug, year: yearParam } = await params; + const department = findDepartment(slug); + const year = findYear(department, yearParam); + if (!department || !year) return { title: "Not found" }; + + return { + title: `${department.name} · Year ${year.year}`, + description: `Semesters and subjects for year ${year.year} of ${department.name} at RVCE.`, + }; +} + +export default async function YearPage({ params, }: { params: Promise<{ department: string; year: string }>; }) { - const resolvedParams = use(params); - - const department = departments.find( - (dept) => dept.link === `/${resolvedParams.department}` - ); - const year = department?.years.find( - (y) => y.year === parseInt(resolvedParams.year) - ); + const { department: slug, year: yearParam } = await params; + const department = findDepartment(slug); + const year = findYear(department, yearParam); - if (!department || !year) { - notFound(); - } + if (!department || !year) notFound(); return (
- {/* Header */} -
-
- - {department.name} - - → - Year {year.year} -
-

Year {year.year}

-
- - {/* Semesters Grid */} - + {year.semesters.length ? ( + + {year.semesters.map((semester) => ( + + + + ))} + + ) : ( + + )}
); -} \ No newline at end of file +} diff --git a/src/app/[department]/page.tsx b/src/app/[department]/page.tsx index e8ea853..a1fc864 100644 --- a/src/app/[department]/page.tsx +++ b/src/app/[department]/page.tsx @@ -1,91 +1,94 @@ -import { departments } from "../../data/mockData"; +import type { Metadata } from "next"; import { notFound } from "next/navigation"; -import { use } from "react"; +import { departments } from "@/data/departments"; +import { CardGrid } from "@/components/ui/Card"; +import { CoverageBadges } from "@/components/ui/Badge"; +import { PageHeader } from "@/components/ui/PageHeader"; +import { TileLink } from "@/components/ui/TileLink"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { + countNotes, + departmentNotes, + departmentSlug, + findDepartment, + subjectCount, + yearNotes, +} from "@/lib/catalog"; -export default function DepartmentPage({ +export function generateStaticParams() { + return departments.map((department) => ({ + department: departmentSlug(department), + })); +} + +export async function generateMetadata({ + params, +}: { + params: Promise<{ department: string }>; +}): Promise { + const { department: slug } = await params; + const department = findDepartment(slug); + if (!department) return { title: "Department not found" }; + + return { + title: department.name, + description: department.description, + openGraph: { title: department.name, description: department.description }, + }; +} + +export default async function DepartmentPage({ params, }: { params: Promise<{ department: string }>; }) { - // Unwrap params using React.use() - const resolvedParams = use(params); - const departmentSlug = resolvedParams.department; + const { department: slug } = await params; + const department = findDepartment(slug); - // Find the department from the URL parameter - const department = departments.find( - (dept) => dept.link === `/${departmentSlug}` - ); + if (!department) notFound(); - // If department not found, show 404 - if (!department) { - notFound(); - } + const counts = countNotes(departmentNotes(department)); return (
- {/* Department Header */} -
-

{department.name}

-

{department.description}

-
- - {/* Years Grid */} - + {department.years.length ? ( + + {department.years.map((year) => ( + + + + ))} + + ) : ( + + )}
); } diff --git a/src/app/about/page.tsx b/src/app/about/page.tsx index ecfaa0c..ad61403 100644 --- a/src/app/about/page.tsx +++ b/src/app/about/page.tsx @@ -1,162 +1,138 @@ -import React from "react"; +import type { Metadata } from "next"; import Link from "next/link"; +import { PageHeader } from "@/components/ui/PageHeader"; +import { Icon } from "@/components/ui/icons"; +import { SITE } from "@/lib/site"; +import { AddNotesButton } from "@/components/ContributeCTA"; + +export const metadata: Metadata = { + title: "About", + description: + "What Knotes Central is, how the notes are stored, and how RVCE students can keep it going.", +}; + +const STEPS = [ + { + title: "Upload and share", + body: "Put your files in a Google Drive folder and share it with RVCE, or push them to a public GitHub repo.", + }, + { + title: "Open the form", + body: "Pick the department, year and semester, paste the link, and submit. It is a GitHub issue form — no Git knowledge needed.", + }, + { + title: "It goes live", + body: "A maintainer adds the entry to the department file, and your name goes on the contributors page.", + }, +]; + export default function About() { return ( -
-

About Knotes Central

+
+ -

- Knotes Central is a comprehensive collection of academic resources for - RVCE. Our mission is to provide students with easy access to a wide - range of study materials, including notes, question papers, and lab - manuals, to help them excel in their academic pursuits. Our main Motto ,{" "} - "For all For Always!" -

- -
-

Our Mission

-

- Knotes Central aims to offer a comprehensive method of accessing - notes, ensuring our community members no longer need to search - extensively for study materials. All documents and notes are - exclusively from RVCE, making this a centralized resource for our - students. +

+

+ Knotes Central is a comprehensive collection of academic resources for + RVCE — notes, question papers and lab manuals, gathered in one place + so nobody has to hunt for them again. Our motto:{" "} + "{SITE.motto}"

-
-
-

Features

-
    -
  • Wide range of courses covered
  • -
  • Easy access to notes and question papers
  • -
  • Accessible via RVCE Email ID
  • -
  • No running around for notes
  • -
-
+

How to access the material

+

+ All notes are stored in RVCE Workspace Drive, so most links require + you to be signed in with your college account. Some are made public + during beta. +

-
-

How to Access

-

- All notes are stored in RVCE Workspace Drive, requiring login to view - the content.But sometimes made public for beta versions. +

Declaration

+

+ This website is intended for the RVCE community. Its use is free and + fair, strictly for educational purposes.

-
-
-

Declaration

-

- This website is intended exclusively for RVCE students and is not - accessible to individuals outside this community. We declare that its - use is free and fair, strictly for educational purposes. +

Keeping it alive

+

+ The site is open source, and the catalog is just a set of files in + the repository — one per department. Anyone can add to it: either + through a short form, or by editing the department file directly on + GitHub. As each batch graduates, the next one keeps it current. That + is the only reason anything here exists.

-
+
-
-

- Continuous Updates and Contributions +
+

+ How to contribute

-

- This website connects to shared drives where all the notes, previous - years' question papers, and other resources are stored. As we - near graduation, we want to ensure that this resource remains valuable - and up-to-date for future batches. -

-
-
-

Batch Contributions

-

- After we graduate, we encourage our batch mates, as well as - juniors, to continue updating the drives with new notes and - resources. This collaborative effort will benefit everyone. -

-
-
-

List of Contributors

-

- We maintain a list of contributors who have editor access to the - drive. If you need to update the content, please contact the - nearest senior or junior from this list to gain editor access. The - list will be regularly updated to reflect active contributors. -

-
-
-

How to Contribute

-
    -
  • - Contact Us: To become a contributor, reach out - to a current contributor from the list. They will guide you on - gaining editor access and the guidelines for updating the - content. -
  • -
  • - Add New Content: When adding new content, - please follow the existing structure to maintain consistency. - Label your notes and resources clearly with course names and - relevant details. -
  • -
-
+
    + {STEPS.map((step, i) => ( +
  1. + + {i + 1} + +

    + {step.title} +

    +

    {step.body}

    +
  2. + ))} +
+ +
+ + + See both ways to contribute + +
-
-

Additional Notes

-

- Feel free to clone the drive (whose link will be in the sheet) and - create your own if the website is not updated. Additionally, you can - clone the website too. This project is still in its early stages and - has a long way to go. We are looking for your contributions to make - this project better. -

-
-
-

Previous Versions

-

- As we continue to improve the Knotes Central platform, we have had - several iterations. Below are links to the previous versions for your - reference: -

-
    +
    +

    Previous versions

    +
    • - Version 1:{" "} - - Knotes Central V1.0 - + Knotes Central V1 +
    • - Version 2:{" "} - Knotes Central V2 - +
    -
    -

    Contact Us

    -

    - If you have any questions, suggestions, or feedback, please feel free - to{" "} - - contact us - - . We are always looking for ways to improve and would love to hear - from you. -

    -
    +

    + Questions, suggestions or corrections?{" "} + + Get in touch + + . +

); } diff --git a/src/app/contact/page.tsx b/src/app/contact/page.tsx index cdccaa2..be1dab7 100644 --- a/src/app/contact/page.tsx +++ b/src/app/contact/page.tsx @@ -1,72 +1,105 @@ +import type { Metadata } from "next"; +import { PageHeader } from "@/components/ui/PageHeader"; +import { Icon, type IconName } from "@/components/ui/icons"; +import { + CONTACT_EMAIL, + CONTACT_FORM_URL, + REDDIT, + addNotesIssueUrl, + reportIssueUrl, +} from "@/lib/site"; + +export const metadata: Metadata = { + title: "Contact", + description: + "Questions, corrections or notes to contribute — how to reach the Knotes Central maintainers.", +}; + +const CHANNELS: Array<{ + icon: IconName; + title: string; + body: string; + href: string; + cta: string; +}> = [ + { + icon: "sparkles", + title: "Add notes", + body: "Paste a Drive or GitHub link into a short form. A maintainer puts it on the site.", + href: addNotesIssueUrl(), + cta: "Open the form", + }, + { + icon: "inbox", + title: "Report a broken link", + body: "A note that will not open, or points at the wrong material.", + href: reportIssueUrl(), + cta: "Report it", + }, + { + icon: "check", + title: "Anything else", + body: "Requests, corrections and general feedback through our form.", + href: CONTACT_FORM_URL, + cta: "Open the form", + }, + { + icon: "mail", + title: "Email", + body: CONTACT_EMAIL, + href: `mailto:${CONTACT_EMAIL}`, + cta: "Send an email", + }, + { + icon: "users", + title: "Reddit", + body: "Ask the wider RVCE community on r/rvce, or follow r/KnotesCentral for updates.", + href: REDDIT.knotes, + cta: "Open r/KnotesCentral", + }, +]; + export default function Contact() { return ( -
-

Contact Us

-
-
-

- Have questions, suggestions, or want to contribute? We'd love - to hear from you. Click the button below to fill out our contact - form. -

- +
+ + + - - -
-

You can also reach us at:

- - knotescentral@gmail.com - -

Follow us on Reddit:

- - RVCE Reddit - - - Knotes Reddit - -
- - - - - - - - - - - - - - - - - - - + ))}
+ +

+ Most notes live on RVCE Workspace Drive, so you may need to be signed in + with your college account to open them. +

); } diff --git a/src/app/contribute/page.tsx b/src/app/contribute/page.tsx new file mode 100644 index 0000000..d8b9251 --- /dev/null +++ b/src/app/contribute/page.tsx @@ -0,0 +1,165 @@ +import type { Metadata } from "next"; +import { departments } from "@/data/departments"; +import { PageHeader } from "@/components/ui/PageHeader"; +import { AddNotesButton } from "@/components/ContributeCTA"; +import { Icon } from "@/components/ui/icons"; +import { ContributeWizard } from "@/components/ContributeWizard"; +import { departmentOptions, departmentSlug } from "@/lib/catalog"; +import { githubEditUrl, REPO_URL, reportIssueUrl } from "@/lib/site"; + +export const metadata: Metadata = { + title: "Contribute notes", + description: + "Two ways to add notes to Knotes Central: a short GitHub form, or editing the department file directly.", +}; + +const SNIPPET = `{ + name: "Data Structures and its Applications", + subject_code: "CSE201", + notes: [ + { + title: "DSA Unit 1-5 notes", + type: "theory", // "theory" | "lab" | "question-paper" + link: "https://drive.google.com/drive/folders/...", + }, + ], +}`; + +export default function ContributePage() { + return ( +
+ + +
+

+ Build your entry +

+

+ Fill this in and it writes the catalog entry for you — then send it + either way below. +

+
+ +
+
+ +
+
+ + Easiest + +

+ Fill a short form on GitHub +

+

+ Pick the department, year and semester, paste your Google Drive or + GitHub link, and submit. A maintainer adds it to the site. You need + a free GitHub account and nothing else — no Git, no code. +

+
    + {[ + "Upload your files to Drive and share them with RVCE.", + "Open the form and paste the link.", + "Add your name if you want the credit.", + ].map((step, i) => ( +
  1. + + {i + 1} + + {step} +
  2. + ))} +
+
+ +
+
+ +
+ + Direct + +

+ Edit the department file yourself +

+

+ Each department is a single file. Click edit, add your entry in the + right semester, and GitHub opens a pull request for you — it forks + the repo automatically, so you cannot break anything. +

+
+            {SNIPPET}
+          
+

+ Keep the shape exactly as above — the site reads these files + directly, and a typo shows up as a build failure on your pull + request rather than a broken page. +

+
+
+ +
+

+ Edit a department directly +

+

+ Opens GitHub's editor on that department's file. +

+
+ {departments.map((department) => ( + + + {department.name} + + + ))} +
+
+ +
+
+

+ Found a dead link instead? +

+

+ Report it and it gets fixed — that is a contribution too. +

+
+ +
+
+ ); +} diff --git a/src/app/contributors/page.tsx b/src/app/contributors/page.tsx index 3bfad61..d1f2a25 100644 --- a/src/app/contributors/page.tsx +++ b/src/app/contributors/page.tsx @@ -1,17 +1,36 @@ -import { contributors } from "../../data/mockData"; -import ContributorCard from "../../components/contributorCard"; +import type { Metadata } from "next"; +import { contributors } from "@/data/contributors"; +import ContributorCard from "@/components/ContributorCard"; +import { PageHeader } from "@/components/ui/PageHeader"; +import { CardGrid } from "@/components/ui/Card"; +import { AddNotesButton } from "@/components/ContributeCTA"; + +export const metadata: Metadata = { + title: "Contributors", + description: + "The RVCE students who collected, uploaded and maintain the notes on Knotes Central.", +}; export default function ContributorsPage() { return ( -
-

- Contributors -

-
- {contributors.map((contributor, index) => ( - +
+ + + + + + {contributors.map((contributor) => ( + ))} -
+
); } diff --git a/src/app/feed.xml/route.ts b/src/app/feed.xml/route.ts new file mode 100644 index 0000000..03a372f --- /dev/null +++ b/src/app/feed.xml/route.ts @@ -0,0 +1,48 @@ +import { recentNotes } from "@/lib/changelog"; +import { SITE } from "@/lib/site"; + +export const dynamic = "force-static"; + +const escape = (value: string) => + value + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); + +/** RSS of newly added material, for anyone who wants updates without checking. */ +export function GET() { + const items = recentNotes(50) + .map((item) => { + const link = `${SITE.url}${item.path}`; + const where = `${item.location.department.name} · Year ${item.location.year.year} · Semester ${item.location.semester.number}`; + + return ` + ${escape(item.note.title)} — ${escape(item.location.subject.name)} + ${escape(link)} + ${escape(item.note.link)} + ${new Date(item.date).toUTCString()} + ${escape(where)} + `; + }) + .join("\n"); + + const xml = ` + + + ${escape(SITE.name)} — what's new + ${SITE.url} + ${escape(SITE.description)} + en +${items} + + +`; + + return new Response(xml, { + headers: { + "content-type": "application/rss+xml; charset=utf-8", + "cache-control": "public, max-age=3600", + }, + }); +} diff --git a/src/app/globals.css b/src/app/globals.css index eda258f..7fcf9da 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -2,201 +2,159 @@ @tailwind components; @tailwind utilities; +/* + * Colour tokens are RGB triplets so Tailwind's `/opacity` modifiers work + * (see tailwind.config.ts). Light is the default; `.dark` on flips them. + * Every foreground/background pair below clears WCAG AA at body size. + */ :root { - /* Base colors */ - --background: #ffffff; - --foreground: #171717; - - /* Neon Colors */ - --neon-blue: #00ffff; /* Cyan */ - --neon-purple: #ff00ff; /* Magenta */ - --neon-pink: #ff69b4; /* Hot Pink */ - --neon-green: #39ff14; /* Neon Green */ - - /* Glow effects */ - --neon-blue-glow: 0 0 10px rgba(0, 255, 255, 0.5); - --neon-purple-glow: 0 0 10px rgba(255, 0, 255, 0.5); - --neon-text-glow: 0 0 5px rgba(255, 255, 255, 0.5); - - /* Card and UI */ - --card-bg: rgba(255, 255, 255, 0.05); - --card-border: rgba(255, 255, 255, 0.1); - --card-hover: rgba(255, 255, 255, 0.1); - - color-scheme: light dark; -} + --page: 248 250 252; /* slate-50 */ + --surface: 255 255 255; + --raised: 241 245 249; /* slate-100 */ + --line: 226 232 240; /* slate-200 */ + --fg: 15 23 42; /* slate-900 */ + --muted: 71 85 105; /* slate-600 — 7.2:1 on --surface */ -/* Dark mode overrides */ -.dark { - --background: #191919; - --foreground: #bbbbbb; - --card-bg: rgba(0, 0, 0, 0.02); - --card-border: rgba(0, 0, 0, 0.1); - --card-hover: rgba(0, 0, 0, 0.05); -} + --brand: 79 70 229; /* indigo-600 */ + --brand-soft: 238 242 255; /* indigo-50 */ + --brand-contrast: 255 255 255; -/* Base styles */ -body { - color: var(--foreground); - background: var(--background); - min-height: 100vh; - background-image: radial-gradient( - circle at 15% 50%, - rgba(0, 255, 255, 0.08) 0%, - transparent 25% - ), - radial-gradient( - circle at 85% 50%, - rgba(255, 0, 255, 0.08) 0%, - transparent 25% - ); - background-attachment: fixed; -} + --accent: 13 148 136; /* teal-600 */ -/* Smooth transitions */ -* { - transition: all 0.2s ease-in-out; -} + --theory: 37 99 235; /* blue-600 */ + --lab: 5 150 105; /* emerald-600 */ + --paper: 217 119 6; /* amber-600 */ -/* Mobile-first card styles */ -.card { - @apply rounded-xl p-4 md:p-6 backdrop-blur-sm; - background: var(--card-bg); - border: 1px solid var(--card-border); + color-scheme: light; } -.card:hover { - background: var(--card-hover); - box-shadow: var(--neon-purple-glow); -} +.dark { + --page: 2 6 23; /* slate-950 */ + --surface: 15 23 42; /* slate-900 */ + --raised: 30 41 59; /* slate-800 */ + --line: 51 65 85; /* slate-700 */ + --fg: 241 245 249; /* slate-100 */ + --muted: 148 163 184; /* slate-400 — 7.4:1 on --surface */ -/* Neon text effects */ -.neon-text { - text-shadow: var(--neon-text-glow); -} + --brand: 129 140 248; /* indigo-400 */ + --brand-soft: 30 27 75; /* indigo-950 */ + --brand-contrast: 15 23 42; -.neon-text-blue { - color: var(--neon-blue); - text-shadow: var(--neon-blue-glow); -} + --accent: 45 212 191; /* teal-400 */ -.neon-text-purple { - color: var(--neon-purple); - text-shadow: var(--neon-purple-glow); -} + --theory: 96 165 250; /* blue-400 */ + --lab: 52 211 153; /* emerald-400 */ + --paper: 251 191 36; /* amber-400 */ -/* Mobile-friendly container */ -.container { - @apply px-4 mx-auto w-full; - max-width: min(100%, 1280px); + color-scheme: dark; } -/* Mobile navigation adjustments */ -@media (max-width: 768px) { - .nav-items { - @apply flex-wrap justify-center gap-2; +@layer base { + html { + -webkit-text-size-adjust: 100%; + scroll-behavior: smooth; } - .nav-link { - @apply text-sm py-1 px-2; + body { + @apply bg-page text-fg antialiased; } -} -/* Gradient borders */ -.gradient-border { - position: relative; - border-radius: 0.75rem; - background: var(--card-bg); -} + /* One visible focus treatment for every interactive element. */ + :where(a, button, input, select, textarea, [tabindex]):focus-visible { + @apply outline-none ring-2 ring-brand ring-offset-2 ring-offset-page rounded-md; + } -.gradient-border::before { - content: ""; - position: absolute; - top: -1px; - left: -1px; - right: -1px; - bottom: -1px; - border-radius: 0.75rem; - padding: 1px; - background: linear-gradient(45deg, var(--neon-blue), var(--neon-purple)); - -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); - mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); - -webkit-mask-composite: xor; - mask-composite: exclude; + ::selection { + @apply bg-brand/20; + } } -/* Animation keyframes */ -@keyframes glow { - 0%, - 100% { - text-shadow: var(--neon-text-glow); +@layer components { + /* Page gutter. Wide enough to breathe, capped so lines stay readable. */ + .container { + @apply w-full mx-auto px-4 sm:px-6 lg:px-8; + max-width: 80rem; } - 50% { - text-shadow: var(--neon-purple-glow); + + .card { + @apply bg-surface border border-line rounded-card shadow-card; } -} -.animate-glow { - animation: glow 2s ease-in-out infinite; -} + .card-interactive { + @apply card transition duration-200 hover:border-brand/40 hover:shadow-lift focus-within:border-brand/40 motion-safe:hover:-translate-y-0.5; + } -/* Responsive typography */ -h1 { - @apply text-2xl md:text-3xl lg:text-4xl font-bold; -} + .prose-page { + @apply max-w-prose text-[0.975rem] leading-7 text-muted; + } -h2 { - @apply text-xl md:text-2xl lg:text-3xl font-bold; -} + .prose-page h2 { + @apply text-xl font-semibold text-fg mt-10 mb-3; + } -h3 { - @apply text-lg md:text-xl font-semibold; -} + .prose-page h3 { + @apply text-base font-semibold text-fg mt-6 mb-2; + } -/* Custom scrollbar */ -::-webkit-scrollbar { - width: 8px; -} + .prose-page p + p { + @apply mt-4; + } -::-webkit-scrollbar-track { - background: var(--background); -} + .prose-page a { + @apply text-brand font-medium underline underline-offset-4 decoration-brand/30 hover:decoration-brand; + } -::-webkit-scrollbar-thumb { - background: var(--card-border); - border-radius: 4px; -} + .prose-page ul { + @apply mt-4 space-y-2 list-disc pl-5 marker:text-brand/60; + } -::-webkit-scrollbar-thumb:hover { - background: var(--card-hover); -} + .skeleton { + @apply relative overflow-hidden bg-raised rounded-md; + } -/* Update the nav-link styles */ -.nav-link { - @apply px-3 py-2 rounded-md text-sm font-medium transition-colors flex items-center; + .skeleton::after { + content: ""; + @apply absolute inset-0 -translate-x-full; + background: linear-gradient( + 90deg, + transparent, + rgb(var(--fg) / 0.06), + transparent + ); + animation: shimmer 1.6s infinite; + } } -/* Add mobile menu transition */ -@media (max-width: 768px) { - nav { - transition: all 0.3s ease-in-out; +@media (prefers-reduced-motion: reduce) { + html { + scroll-behavior: auto; } - .nav-link { - @apply w-full justify-between; + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; } } -/* Smooth theme transitions */ -html { - transition: background-color 0.3s ease; +/* Scrollbar tuned to the surface tokens rather than fixed greys. */ +::-webkit-scrollbar { + width: 10px; + height: 10px; } -html.dark { - color-scheme: dark; +::-webkit-scrollbar-track { + background: rgb(var(--page)); } -/* Prevent transition flash */ -.no-transitions * { - transition: none !important; +::-webkit-scrollbar-thumb { + background: rgb(var(--line)); + border-radius: 6px; + border: 2px solid rgb(var(--page)); +} + +::-webkit-scrollbar-thumb:hover { + background: rgb(var(--muted) / 0.5); } diff --git a/src/app/icon.svg b/src/app/icon.svg new file mode 100644 index 0000000..0243782 --- /dev/null +++ b/src/app/icon.svg @@ -0,0 +1,5 @@ + + + + diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 3d3f444..0a699e3 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,36 +1,52 @@ +import type { Metadata, Viewport } from "next"; import { Inter } from "next/font/google"; -import "./globals.css"; // Make sure this import is present +import "./globals.css"; +import { SiteHeader } from "@/components/SiteHeader"; +import { Notice } from "@/components/Notice"; +import { SiteFooter } from "@/components/SiteFooter"; +import { InstallApp } from "@/components/InstallApp"; import { ThemeProvider } from "@/components/ThemeProvider"; -import { ThemeToggle } from "@/components/ThemeToggle"; -import Link from "next/link"; +import { THEME_INIT_SCRIPT } from "@/lib/theme"; +import { SITE } from "@/lib/site"; +import { JsonLd, siteLd } from "@/components/StructuredData"; -const inter = Inter({ subsets: ["latin"] }); +const inter = Inter({ subsets: ["latin"], display: "swap" }); -// This script runs before React hydration to prevent flash of wrong theme -// It causes a hydration warning in development, which is expected and can be ignored -const themeInitScript = ` - (function() { - const theme = localStorage.getItem('theme') ?? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'); - document.documentElement.classList[theme === 'dark' ? 'add' : 'remove']('dark'); - })() -`; +export const metadata: Metadata = { + metadataBase: new URL(SITE.url), + title: { + default: `${SITE.name} — RVCE notes, labs and question papers`, + template: `%s · ${SITE.name}`, + }, + description: SITE.description, + keywords: [ + "RVCE", + "RV College of Engineering", + "notes", + "question papers", + "lab manuals", + "VTU", + "study material", + ], + openGraph: { + type: "website", + siteName: SITE.name, + title: `${SITE.name} — RVCE notes, labs and question papers`, + description: SITE.description, + }, + twitter: { card: "summary_large_image" }, + manifest: "/manifest.webmanifest", + appleWebApp: { capable: true, title: SITE.name, statusBarStyle: "default" }, + icons: { apple: "/apple-icon.png" }, + alternates: { types: { "application/rss+xml": "/feed.xml" } }, +}; -// Add this with other icon components at the top -const SearchIcon = () => ( - - - -); +export const viewport: Viewport = { + themeColor: [ + { media: "(prefers-color-scheme: light)", color: "#f8fafc" }, + { media: "(prefers-color-scheme: dark)", color: "#020617" }, + ], +}; export default function RootLayout({ children, @@ -38,105 +54,22 @@ export default function RootLayout({ children: React.ReactNode; }) { return ( - + - -