From 9b95f5977b677d4c8d6676c995bc581056fe6084 Mon Sep 17 00:00:00 2001 From: Pulkit Krishna Date: Mon, 17 Nov 2025 13:58:56 +0000 Subject: [PATCH 1/4] add progress tracking --- .gitignore | 2 + docs/start-here.md | 15 +- package.json | 1 + scripts/generate-docs-manifest.mjs | 64 +++++ .../ReadingProgress/ReadingProgress.js | 269 ++++++++++++++++++ .../ReadingProgress/styles.module.css | 161 +++++++++++ .../StartHereProgress/StartHereProgress.jsx | 241 ++++++++++++++++ src/css/custom.css | 95 +++++++ src/theme/DocItem/index.js | 49 ++++ 9 files changed, 892 insertions(+), 5 deletions(-) create mode 100644 scripts/generate-docs-manifest.mjs create mode 100644 src/components/ReadingProgress/ReadingProgress.js create mode 100644 src/components/ReadingProgress/styles.module.css create mode 100644 src/components/StartHereProgress/StartHereProgress.jsx create mode 100644 src/theme/DocItem/index.js diff --git a/.gitignore b/.gitignore index b2d6de3..d0c0b74 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,5 @@ npm-debug.log* yarn-debug.log* yarn-error.log* + +/static/docs-manifest.json diff --git a/docs/start-here.md b/docs/start-here.md index 4ff4d9e..1f65724 100644 --- a/docs/start-here.md +++ b/docs/start-here.md @@ -1,10 +1,15 @@ +import StartHereProgress from '@site/src/components/StartHereProgress/StartHereProgress'; + # Start Here The Borr Project aims to empower learners to master college curricula through free resources. Choose a major and start today! -We offer the following fore curricula right now: +We offer the following four curricula right now: + + + +Happy learning! -- [Computer Science](../computer-science/) -- [Pre-College Math](../precollege-math/) -- [Data Science](../data-science/) -- [Math](../math/) +:::tip +The progress trackers above reflect your progress through each curriculum and are stored locally in your browser. This data is private and only accessible to you. To back up your progress more permanently, use the export button. +::: diff --git a/package.json b/package.json index 5fb8312..3896317 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "scripts": { "docusaurus": "docusaurus", "start": "docusaurus start", + "prebuild": "node ./scripts/generate-docs-manifest.mjs", "build": "docusaurus build", "swizzle": "docusaurus swizzle", "deploy": "docusaurus deploy", diff --git a/scripts/generate-docs-manifest.mjs b/scripts/generate-docs-manifest.mjs new file mode 100644 index 0000000..87c723a --- /dev/null +++ b/scripts/generate-docs-manifest.mjs @@ -0,0 +1,64 @@ +#!/usr/bin/env node +import fs from 'fs/promises'; +import path from 'path'; + +const DOCS_DIR = path.resolve(process.cwd(), 'docs'); +const OUT_DIR = path.resolve(process.cwd(), 'static'); +const OUT_FILE = path.join(OUT_DIR, 'docs-manifest.json'); + +function isDocFile(name) { + return name.endsWith('.md') || name.endsWith('.mdx'); +} + +async function walk(dir) { + const entries = await fs.readdir(dir, {withFileTypes: true}); + const files = []; + for (const ent of entries) { + const full = path.join(dir, ent.name); + if (ent.isDirectory()) { + files.push(...await walk(full)); + } else if (ent.isFile() && isDocFile(ent.name)) { + files.push(full); + } + } + return files; +} + +function toRoute(filePath) { + const rel = path.relative(DOCS_DIR, filePath).split(path.sep).join('/'); + const dirname = path.dirname(rel); + const basename = path.basename(rel); + const name = basename.replace(/\.(md|mdx)$/i, ''); + + if (name.toLowerCase() === 'index') { + if (dirname === '.' || dirname === '') return '/'; + return `/${dirname}/`; + } + + if (dirname === '.' || dirname === '') return `/${name}/`; + return `/${dirname}/${name}/`; +} + +async function main() { + try { + await fs.access(DOCS_DIR); + } catch (e) { + console.error('docs/ directory not found, skipping manifest generation.'); + process.exit(0); + } + + const files = await walk(DOCS_DIR); + const routes = files.map(toRoute); + const unique = Array.from(new Set(routes)).sort(); + + try { + await fs.mkdir(OUT_DIR, {recursive: true}); + await fs.writeFile(OUT_FILE, JSON.stringify(unique, null, 2), 'utf8'); + console.log(`Wrote docs manifest with ${unique.length} entries to ${path.relative(process.cwd(), OUT_FILE)}`); + } catch (e) { + console.error('Failed to write manifest:', e); + process.exit(1); + } +} + +main(); diff --git a/src/components/ReadingProgress/ReadingProgress.js b/src/components/ReadingProgress/ReadingProgress.js new file mode 100644 index 0000000..9ae87e2 --- /dev/null +++ b/src/components/ReadingProgress/ReadingProgress.js @@ -0,0 +1,269 @@ +import React, {useEffect, useState, useRef} from 'react'; +import styles from './styles.module.css'; + +const STORAGE_PREFIX = 'borr:read:'; + +function normalizePath(path) { + if (!path) return '/'; + try { + const url = new URL(path, typeof window !== 'undefined' ? window.location.origin : 'http://localhost'); + path = url.pathname; + } catch (e) { + } + if (path !== '/' && path.endsWith('/')) path = path.slice(0, -1); + return path || '/'; +} + +function getSidebarDocLinks() { + const anchors = Array.from(document.querySelectorAll('.menu__list a')); + const internal = anchors + .map((a) => a.getAttribute('href')) + .filter(Boolean) + .filter((h) => h.startsWith('/')) + .map((h) => normalizePath(h)); + return Array.from(new Set(internal)); +} + +const TRACK_SECTIONS = { + '/computer-science': 'Computer Science', + '/precollege-math': 'Pre-College Math', + '/data-science': 'Data Science', + '/math': 'Math', +}; + +export default function ReadingProgress() { + const [totalPages, setTotalPages] = useState(0); + const [readCount, setReadCount] = useState(0); + const [isRead, setIsRead] = useState(false); + const [pagesList, setPagesList] = useState(null); + const [currentSection, setCurrentSection] = useState(null); + const fileInputRef = useRef(null); + + useEffect(() => { + if (typeof window === 'undefined') return; + + const currentPath = normalizePath(window.location.pathname + window.location.search); + + const normalizedSections = Object.keys(TRACK_SECTIONS).map((p) => normalizePath(p)); + const found = normalizedSections.find((s) => currentPath === s || currentPath.startsWith(s + '/')) || null; + setCurrentSection(found); + + if (!found) { + setTotalPages(0); + setReadCount(0); + setIsRead(false); + setPagesList([]); + return; + } + async function loadManifest() { + try { + const res = await fetch('/docs-manifest.json', {cache: 'no-store'}); + if (res.ok) { + const arr = await res.json(); + const pages = arr.map((p) => normalizePath(p)); + const sectionPages = pages.filter((p) => p === found || p.startsWith(found + '/')); + setPagesList(sectionPages); + setTotalPages(sectionPages.length); + + const counts = sectionPages.reduce((acc, p) => acc + (localStorage.getItem(STORAGE_PREFIX + p) === '1' ? 1 : 0), 0); + setReadCount(counts); + + const currentKey = STORAGE_PREFIX + currentPath; + setIsRead(localStorage.getItem(currentKey) === '1'); + return; + } + } catch (e) { + } + + const links = getSidebarDocLinks(); + const sectionLinks = (links.length > 0 ? links : [currentPath]).filter((p) => p === found || p.startsWith(found + '/')); + + setPagesList(sectionLinks); + setTotalPages(sectionLinks.length); + + const counts = sectionLinks.reduce((acc, p) => { + const key = STORAGE_PREFIX + p; + if (localStorage.getItem(key) === '1') acc++; + return acc; + }, 0); + setReadCount(counts); + + const currentKey = STORAGE_PREFIX + currentPath; + setIsRead(localStorage.getItem(currentKey) === '1'); + } + + loadManifest(); + + function onStorage(ev) { + if (!ev.key) return; + if (!ev.key.startsWith(STORAGE_PREFIX)) return; + let newCounts = 0; + for (let i = 0; i < localStorage.length; i++) { + const k = localStorage.key(i); + if (!k || !k.startsWith(STORAGE_PREFIX)) continue; + const p = k.slice(STORAGE_PREFIX.length); + if (p === found || p.startsWith(found + '/')) newCounts++; + } + setReadCount(newCounts); + setIsRead(localStorage.getItem(STORAGE_PREFIX + currentPath) === '1'); + } + + window.addEventListener('storage', onStorage); + return () => window.removeEventListener('storage', onStorage); + }, []); + + if (typeof window === 'undefined') return null; + + if (!currentSection) return null; + + const percent = totalPages > 0 ? Math.round((readCount / totalPages) * 100) : 0; + function toggleRead() { + const currentPath = normalizePath(window.location.pathname + window.location.search); + const key = STORAGE_PREFIX + currentPath; + if (localStorage.getItem(key) === '1') { + localStorage.removeItem(key); + setIsRead(false); + setReadCount((c) => Math.max(0, c - 1)); + } else { + localStorage.setItem(key, '1'); + setIsRead(true); + setReadCount((c) => c + 1); + } + try { + window.dispatchEvent(new Event('storage')); + } catch (e) {} + } + + function exportProgress() { + if (!currentSection) return; + const entries = []; + for (let i = 0; i < localStorage.length; i++) { + const k = localStorage.key(i); + if (!k || !k.startsWith(STORAGE_PREFIX)) continue; + const p = k.slice(STORAGE_PREFIX.length); + if (p === currentSection || p.startsWith(currentSection + '/')) entries.push(p); + } + if (entries.length === 0) { + alert('No progress saved for this section.'); + return; + } + const data = {version: 1, section: currentSection, entries}; + const blob = new Blob([JSON.stringify(data, null, 2)], {type: 'application/json'}); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + const safeSection = currentSection.replace(/[^a-z0-9]/gi, '_').replace(/^_+|_+$/g, ''); + a.download = `borr-progress-${safeSection}.json`; + a.href = url; + document.body.appendChild(a); + a.click(); + a.remove(); + URL.revokeObjectURL(url); + } + + function openImportDialog() { + if (fileInputRef.current) fileInputRef.current.click(); + } + + async function handleImportFile(ev) { + const f = ev.target.files && ev.target.files[0]; + if (!f) return; + try { + const text = await f.text(); + const json = JSON.parse(text); + let entries = []; + if (Array.isArray(json)) entries = json; + else if (json && Array.isArray(json.entries)) entries = json.entries; + else if (json && typeof json === 'object') { + if (json.entries && typeof json.entries === 'object') entries = Object.keys(json.entries); + } + if (entries.length === 0) { + alert('No entries found in import file. Expected an array of doc paths or {entries: [...]}.'); + return; + } + + if (json.section && json.section !== currentSection) { + const ok = window.confirm(`Import file is for "${json.section}" but you are on "${currentSection}". Import anyway (will merge)?`); + if (!ok) return; + } + + let added = 0; + for (const p of entries) { + const norm = normalizePath(p); + if (norm === currentSection || norm.startsWith(currentSection + '/')) { + const key = STORAGE_PREFIX + norm; + if (localStorage.getItem(key) !== '1') { + localStorage.setItem(key, '1'); + added++; + } + } + } + const newCounts = pagesList ? pagesList.reduce((acc, p) => acc + (localStorage.getItem(STORAGE_PREFIX + p) === '1' ? 1 : 0), 0) : 0; + setReadCount(newCounts); + setIsRead(localStorage.getItem(STORAGE_PREFIX + normalizePath(window.location.pathname)) === '1'); + alert(`Imported ${added} entries for this section.`); + } catch (e) { + alert('Failed to import file: ' + (e && e.message ? e.message : String(e))); + } finally { + if (fileInputRef.current) fileInputRef.current.value = ''; + } + } + + function resetProgress() { + if (!currentSection) return; + const ok = window.confirm('Reset progress for this section? This will remove all saved "done" marks for this section.'); + if (!ok) return; + let removed = 0; + const toRemove = []; + for (let i = 0; i < localStorage.length; i++) { + const k = localStorage.key(i); + if (!k || !k.startsWith(STORAGE_PREFIX)) continue; + const p = k.slice(STORAGE_PREFIX.length); + if (p === currentSection || p.startsWith(currentSection + '/')) toRemove.push(k); + } + for (const k of toRemove) { + localStorage.removeItem(k); + removed++; + } + setReadCount(0); + setIsRead(false); + alert(`Removed ${removed} entries for this section.`); + } + + return ( +
+
+
+ {TRACK_SECTIONS[currentSection] ?? ''} + {readCount} + / + {totalPages} + +
+
+ +
+
+ +
+
+
+
{percent}%
+ +
+ +
+ + + +
+
+
+ ); +} diff --git a/src/components/ReadingProgress/styles.module.css b/src/components/ReadingProgress/styles.module.css new file mode 100644 index 0000000..7e06715 --- /dev/null +++ b/src/components/ReadingProgress/styles.module.css @@ -0,0 +1,161 @@ +.container { + border: 1px solid var(--ifm-color-emphasis-200, #e6eef6); + padding: 0.5rem 0.75rem; + border-radius: 6px; + background: var(--ifm-background-color, #fff); + display: flex; + flex-direction: column; + gap: 0.5rem; + width: 100%; + max-width: none; +} +.header { + display: flex; + justify-content: space-between; + align-items: center; +} +.summary { + font-size: 0.9rem; + color: var(--ifm-color-emphasis-700, #333); +} +.sep { + margin: 0 0.25rem; + color: var(--ifm-color-emphasis-300, #888); +} +.label { + margin-left: 0.5rem; + color: var(--ifm-color-emphasis-400, #666); + font-size: 0.8rem; +} +.tick { + border: 1px solid var(--ifm-color-emphasis-200, #ccc); + background: transparent; + color: var(--ifm-color-emphasis-700, #222); + padding: 0.25rem 0.5rem; + border-radius: 4px; + cursor: pointer; + font-size: 0.85rem; +} +.ticked { + background: var(--ifm-color-success, #25c29a); + color: white; + border-color: transparent; +} +.progressBar { + height: 8px; + background: var(--ifm-color-emphasis-100, #f1f6fb); + width: 100%; + border-radius: 6px; + overflow: hidden; +} +.filler { + height: 100%; + background: linear-gradient(90deg, var(--ifm-color-primary, #3b82f6), var(--ifm-color-success, #25c29a)); + width: 0%; + transition: width 300ms ease; +} +.percent { + font-size: 0.8rem; + text-align: right; + color: var(--ifm-color-emphasis-400, #666); +} + +.controls { + display: flex; + gap: 0.5rem; + justify-content: flex-end; + align-items: center; + margin-top: 0.25rem; +} +.fileInput { + display: none; +} + +.sectionName { + font-size: 0.9rem; + color: var(--ifm-color-emphasis-700, #222); + margin-right: 0.5rem; +} + +.controls .button { + padding: 0.3rem 0.6rem; + font-size: 0.85rem; + border-radius: 4px; +} + +.controls .button--secondary { + background: transparent; + color: var(--ifm-color-emphasis-700, #222); + border: 1px solid var(--ifm-color-emphasis-200, #d0d7df); +} +.controls .button--secondary:hover { + background: var(--ifm-color-emphasis-100, #f1f6fb); +} + +.controls .button--outline { + background: transparent; + color: var(--ifm-color-emphasis-700, #222); + border: 1px solid var(--ifm-color-emphasis-200, #d0d7df); +} +.controls .button--outline:hover { + background: var(--ifm-color-emphasis-100, #f1f6fb); +} + +.button.button--primary.tick { + background: var(--ifm-color-primary, #2e8555) !important; + color: white !important; + border-color: transparent !important; +} + +[data-theme='dark'] .controls .button--secondary, +[data-theme='dark'] .controls .button--outline, +[data-theme='dark'] .controls .button { + color: var(--ifm-color-emphasis-100, #eee); + border-color: rgba(255,255,255,0.06); +} +[data-theme='dark'] .controls .button--secondary:hover, +[data-theme='dark'] .controls .button--outline:hover, +[data-theme='dark'] .controls .button:hover { + background: rgba(255,255,255,0.03); +} + +.controls .button--outline:hover { + color: var(--ifm-color-emphasis-700, #222); +} + +.container .borr-tick { + background: var(--ifm-color-primary, #2e8555) !important; + color: #fff !important; + border-color: transparent !important; +} +.container .borr-export, +.container .borr-import { + background: transparent !important; + color: var(--ifm-color-emphasis-700, #222) !important; + border: 1px solid var(--ifm-color-emphasis-200, #d0d7df) !important; +} +.container .borr-export:hover, +.container .borr-import:hover { + background: var(--ifm-color-emphasis-100, #f1f6fb) !important; +} +.container .borr-reset { + background: transparent !important; + color: var(--ifm-color-emphasis-700, #222) !important; + border: 1px solid var(--ifm-color-emphasis-200, #d0d7df) !important; +} +.container .borr-reset:hover { + background: var(--ifm-color-emphasis-100, #f1f6fb) !important; + color: var(--ifm-color-emphasis-700, #222) !important; +} + +[data-theme='dark'] .container .borr-export, +[data-theme='dark'] .container .borr-import, +[data-theme='dark'] .container .borr-reset { + color: var(--ifm-color-emphasis-100, #eee) !important; + border-color: rgba(255,255,255,0.06) !important; +} +[data-theme='dark'] .container .borr-export:hover, +[data-theme='dark'] .container .borr-import:hover, +[data-theme='dark'] .container .borr-reset:hover { + background: rgba(255,255,255,0.03) !important; +} diff --git a/src/components/StartHereProgress/StartHereProgress.jsx b/src/components/StartHereProgress/StartHereProgress.jsx new file mode 100644 index 0000000..ef1f7ae --- /dev/null +++ b/src/components/StartHereProgress/StartHereProgress.jsx @@ -0,0 +1,241 @@ +import React, {useEffect, useState, useRef} from 'react'; +import BrowserOnly from '@docusaurus/BrowserOnly'; + +const STORAGE_PREFIX = 'borr:read:'; +const SECTIONS = [ + ['/computer-science', 'Computer Science'], + ['/precollege-math', 'Pre-College Math'], + ['/data-science', 'Data Science'], + ['/math', 'Math'], +]; + +function norm(p){ + if(!p) return '/'; + try{ p = new URL(p, typeof window !== 'undefined' ? window.location.origin : 'http://localhost').pathname }catch(e){} + if(p !== '/' && p.endsWith('/')) p = p.slice(0,-1); + return p || '/'; +} + +async function loadManifest(){ + try{ + const r = await fetch('/docs-manifest.json', {cache:'no-store'}); + if(r.ok){ + const arr = await r.json(); + return arr.map(norm); + } + }catch(e){} + try{ + const anchors = Array.from(document.querySelectorAll('.menu__list a')); + return Array.from(new Set(anchors.map(a=>a.getAttribute('href')).filter(Boolean).filter(h=>h.startsWith('/')).map(norm))); + }catch(e){} + return []; +} + +function Bar({label, done, total, onExport, onImport, onReset, onGo}){ + const pct = total > 0 ? Math.round((done/total)*100) : 0; + return ( +
+
+
+
${label} ${done} / ${total}`}} /> +
+
+
{pct}%
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+
+ ); +} + +function ProgressContainer(){ + function initialRowsFromStorage(){ + try{ + const out = SECTIONS.map(([_,label]) => ({label, done: 0, total: 0})); + for(let i=0;i({label,done:0,total:0})); + } + } + + const [rows, setRows] = useState(initialRowsFromStorage); + const fileInputRef = useRef(null); + const [pendingImportSection, setPendingImportSection] = useState(null); + async function handleImportFile(ev){ + const f = ev.target && ev.target.files && ev.target.files[0]; + if(!f) return; + try{ + const text = await f.text(); + const json = JSON.parse(text); + let entries = []; + if (Array.isArray(json)) entries = json; + else if (json && Array.isArray(json.entries)) entries = json.entries; + else if (json && json.entries && typeof json.entries === 'object') entries = Object.keys(json.entries); + + if(entries.length === 0){ + alert('No entries found in import file. Expected array or {entries:[...]}'); + return; + } + + if(!pendingImportSection){ + alert('No section selected for import.'); + return; + } + + let added = 0; + for(const p of entries){ + const normp = norm(p); + if(normp === pendingImportSection || normp.startsWith(pendingImportSection + '/')){ + const key = STORAGE_PREFIX + normp; + if(localStorage.getItem(key) !== '1'){ + localStorage.setItem(key, '1'); + added++; + } + } + } + alert(`Imported ${added} entries for this section.`); + + setRows((prev)=>{ + const copy = prev.slice(); + const idx = SECTIONS.findIndex(s=>norm(s[0])===pendingImportSection); + if(idx !== -1){ + const section = pendingImportSection; + const manifest = []; + + let done = 0; + for(let i=0;i{ + const copy = prev.slice(); + const idx = SECTIONS.findIndex(s => norm(s[0]) === section); + if(idx !== -1){ + copy[idx] = {...copy[idx], done: 0}; + } + return copy; + }); + } + + + useEffect(()=>{ + let mounted = true; + (async ()=>{ + const manifest = await loadManifest(); + const out = []; + for(const [path,label] of SECTIONS){ + const section = norm(path); + const pages = manifest.length ? manifest.filter(p => p === section || p.startsWith(section + '/')) : []; + let done = 0; + for(const p of pages){ if(localStorage.getItem(STORAGE_PREFIX + p) === '1') done++; } + out.push({label,done,total:pages.length}); + } + if(mounted) setRows(out); + })(); + return ()=>{ mounted = false }; + }, []); + + return ( +
+ +
+ {rows.map((r, i) => { + const path = norm(SECTIONS[i][0]); + return ( +
+ exportSection(path)} + onImport={() => triggerImportForSection(path)} + onReset={() => resetSection(path)} + onGo={() => { window.location.href = path + '/'; }} + /> +
+ ); + })} +
+
+ ); +} + +export default function StartHereProgress(){ + return ( + + {() => } + + ); +} diff --git a/src/css/custom.css b/src/css/custom.css index f92b183..632d02b 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -8,6 +8,101 @@ :root { --ifm-color-primary: #2e8555; --ifm-color-primary-dark: #29784c; + +.borr-tick { + background: var(--ifm-color-primary) !important; + color: #fff !important; + border-color: transparent !important; +} +.borr-export, +.borr-import, +.borr-reset { + background: transparent !important; + color: var(--ifm-color-emphasis-700) !important; + border: 1px solid var(--ifm-color-emphasis-200) !important; +} +.borr-export:hover, +.borr-import:hover, +.borr-reset:hover { + background: var(--ifm-color-emphasis-100) !important; +} + +[data-theme='dark'] .borr-export, +[data-theme='dark'] .borr-import, +[data-theme='dark'] .borr-reset, +[data-theme='dark'] .borr-tick { + color: var(--ifm-color-emphasis-100) !important; + border-color: rgba(255,255,255,0.06) !important; +} +[data-theme='dark'] .borr-export:hover, +[data-theme='dark'] .borr-import:hover, +[data-theme='dark'] .borr-reset:hover, +[data-theme='dark'] .borr-tick:hover { + background: rgba(255,255,255,0.03) !important; +} + +.borr-multi-progress { + display: flex; + flex-direction: column; + gap: 0.75rem; + margin-top: 0.5rem; +} + +.borr-export, +.borr-import, +.borr-reset, +.borr-go { + background: transparent !important; + color: var(--ifm-color-emphasis-700) !important; + border: 1px solid var(--ifm-color-emphasis-200) !important; +} +.borr-export:hover, +.borr-import:hover, +.borr-reset:hover, +.borr-go:hover { + background: var(--ifm-color-emphasis-100) !important; +} + +.borr-go { + background: var(--ifm-color-primary) !important; + color: #fff !important; + border-color: transparent !important; +} +.borr-go:hover { + background: var(--ifm-color-primary-dark) !important; +} + +.borr-reset { + background: transparent !important; + color: var(--ifm-color-emphasis-700) !important; + border: 1px solid var(--ifm-color-emphasis-200) !important; +} +.borr-reset:hover { + background: var(--ifm-color-emphasis-100) !important; + color: var(--ifm-color-emphasis-700) !important; +} + +[data-theme='dark'] .borr-export, +[data-theme='dark'] .borr-import, +[data-theme='dark'] .borr-reset, +[data-theme='dark'] .borr-go { + color: var(--ifm-color-emphasis-100) !important; + border-color: rgba(255,255,255,0.06) !important; + background: rgba(255,255,255,0.02) !important; +} +[data-theme='dark'] .borr-export:hover, +[data-theme='dark'] .borr-import:hover, +[data-theme='dark'] .borr-reset:hover { + background: rgba(255,255,255,0.03) !important; +} +[data-theme='dark'] .borr-go { + background: var(--ifm-color-primary) !important; + color: #fff !important; +} +[data-theme='dark'] .borr-go:hover { + background: var(--ifm-color-primary-dark) !important; +} + --ifm-color-primary-darker: #277148; --ifm-color-primary-darkest: #205d3b; --ifm-color-primary-light: #33925d; diff --git a/src/theme/DocItem/index.js b/src/theme/DocItem/index.js new file mode 100644 index 0000000..fed94cd --- /dev/null +++ b/src/theme/DocItem/index.js @@ -0,0 +1,49 @@ +import React, {useEffect, useState} from 'react'; +import * as OriginalModule from '@theme-original/DocItem'; +import ReadingProgress from '@site/src/components/ReadingProgress/ReadingProgress'; +import {createPortal} from 'react-dom'; + +export default function DocItem(props) { + const [portalContainer, setPortalContainer] = useState(null); + + useEffect(() => { + // Find the main article for the current doc page + const article = document.querySelector('article'); + if (!article) return; + + // Create a container and insert it before the doc footer (if any) + const container = document.createElement('div'); + container.className = 'borr-reading-progress-portal'; + container.style.width = '100%'; + container.style.marginTop = '1rem'; + + const footer = article.querySelector('.theme-doc-footer'); + if (footer) article.insertBefore(container, footer); + else article.appendChild(container); + + setPortalContainer(container); + + return () => { + try { + if (container && container.parentNode) container.parentNode.removeChild(container); + } catch (e) {} + }; + }, []); + + // Support both default and named module shapes that docusaurus might provide + const OriginalDocItem = OriginalModule && (OriginalModule.default ?? OriginalModule); + if (!OriginalDocItem) { + // Provide a helpful diagnostic if the theme original couldn't be resolved + // Render only the reading progress so the site remains usable. + // eslint-disable-next-line no-console + console.warn('[DocItem swizzle] @theme-original/DocItem resolved to', OriginalModule); + return portalContainer ? createPortal(, portalContainer) : ; + } + + return ( + <> + + {portalContainer ? createPortal(, portalContainer) : null} + + ); +} From 50969c5e7f19cf96f53594657e62ec28e447d220 Mon Sep 17 00:00:00 2001 From: Pulkit Krishna Date: Mon, 17 Nov 2025 14:06:32 +0000 Subject: [PATCH 2/4] add your progress page to nagivation bar --- docs/start-here.md | 2 +- docusaurus.config.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/start-here.md b/docs/start-here.md index 1f65724..379abae 100644 --- a/docs/start-here.md +++ b/docs/start-here.md @@ -1,6 +1,6 @@ import StartHereProgress from '@site/src/components/StartHereProgress/StartHereProgress'; -# Start Here +# Your Progress The Borr Project aims to empower learners to master college curricula through free resources. Choose a major and start today! diff --git a/docusaurus.config.js b/docusaurus.config.js index 24548e9..4483776 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -191,6 +191,7 @@ const config = { }, {to: '/getting-help', label: 'Getting Help'}, {to: '/blog', label: 'Blog'}, + {to: '/start-here', label: 'Your Progress', position: 'right'}, { href: 'https://github.com/BorrProject/', position: 'right', From 00127f2f3f9daa10c2d6c62ff23d798678d4e6d3 Mon Sep 17 00:00:00 2001 From: Pulkit Krishna Date: Tue, 18 Nov 2025 04:02:38 +0000 Subject: [PATCH 3/4] generate list of paths after build, not before --- package.json | 2 +- scripts/generate-docs-manifest.mjs | 39 ++++++++++-------------------- 2 files changed, 14 insertions(+), 27 deletions(-) diff --git a/package.json b/package.json index 3896317..ada3f35 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "scripts": { "docusaurus": "docusaurus", "start": "docusaurus start", - "prebuild": "node ./scripts/generate-docs-manifest.mjs", + "postbuild": "node ./scripts/generate-docs-manifest.mjs", "build": "docusaurus build", "swizzle": "docusaurus swizzle", "deploy": "docusaurus deploy", diff --git a/scripts/generate-docs-manifest.mjs b/scripts/generate-docs-manifest.mjs index 87c723a..e98fcdf 100644 --- a/scripts/generate-docs-manifest.mjs +++ b/scripts/generate-docs-manifest.mjs @@ -1,58 +1,45 @@ #!/usr/bin/env node + import fs from 'fs/promises'; import path from 'path'; -const DOCS_DIR = path.resolve(process.cwd(), 'docs'); -const OUT_DIR = path.resolve(process.cwd(), 'static'); -const OUT_FILE = path.join(OUT_DIR, 'docs-manifest.json'); - -function isDocFile(name) { - return name.endsWith('.md') || name.endsWith('.mdx'); -} +const BUILD_DIR = path.resolve(process.cwd(), 'build'); +const OUT_FILE = path.join(BUILD_DIR, 'docs-manifest.json'); async function walk(dir) { - const entries = await fs.readdir(dir, {withFileTypes: true}); + const entries = await fs.readdir(dir, { withFileTypes: true }); const files = []; + for (const ent of entries) { const full = path.join(dir, ent.name); if (ent.isDirectory()) { files.push(...await walk(full)); - } else if (ent.isFile() && isDocFile(ent.name)) { + } else if (ent.isFile() && ent.name === 'index.html') { files.push(full); } } + return files; } function toRoute(filePath) { - const rel = path.relative(DOCS_DIR, filePath).split(path.sep).join('/'); - const dirname = path.dirname(rel); - const basename = path.basename(rel); - const name = basename.replace(/\.(md|mdx)$/i, ''); - - if (name.toLowerCase() === 'index') { - if (dirname === '.' || dirname === '') return '/'; - return `/${dirname}/`; - } - - if (dirname === '.' || dirname === '') return `/${name}/`; - return `/${dirname}/${name}/`; + const rel = path.relative(BUILD_DIR, path.dirname(filePath)).split(path.sep).join('/'); + return rel === '' ? '/' : `/${rel}/`; } async function main() { try { - await fs.access(DOCS_DIR); + await fs.access(BUILD_DIR); } catch (e) { - console.error('docs/ directory not found, skipping manifest generation.'); + console.error('build/ directory not found, skipping manifest generation.'); process.exit(0); } - const files = await walk(DOCS_DIR); + const files = await walk(BUILD_DIR); const routes = files.map(toRoute); const unique = Array.from(new Set(routes)).sort(); try { - await fs.mkdir(OUT_DIR, {recursive: true}); await fs.writeFile(OUT_FILE, JSON.stringify(unique, null, 2), 'utf8'); console.log(`Wrote docs manifest with ${unique.length} entries to ${path.relative(process.cwd(), OUT_FILE)}`); } catch (e) { @@ -61,4 +48,4 @@ async function main() { } } -main(); +main(); \ No newline at end of file From 1f8396d401d643368ff5646dd09960f77dd8e458 Mon Sep 17 00:00:00 2001 From: Pulkit Krishna Date: Tue, 18 Nov 2025 04:59:31 +0000 Subject: [PATCH 4/4] make you progress page responsive on mobile --- .../StartHereProgress/StartHereProgress.jsx | 17 ++-- .../StartHereProgress/styles.module.css | 81 +++++++++++++++++++ 2 files changed, 90 insertions(+), 8 deletions(-) create mode 100644 src/components/StartHereProgress/styles.module.css diff --git a/src/components/StartHereProgress/StartHereProgress.jsx b/src/components/StartHereProgress/StartHereProgress.jsx index ef1f7ae..94d1f8c 100644 --- a/src/components/StartHereProgress/StartHereProgress.jsx +++ b/src/components/StartHereProgress/StartHereProgress.jsx @@ -1,5 +1,6 @@ import React, {useEffect, useState, useRef} from 'react'; import BrowserOnly from '@docusaurus/BrowserOnly'; +import styles from './styles.module.css'; const STORAGE_PREFIX = 'borr:read:'; const SECTIONS = [ @@ -34,13 +35,13 @@ async function loadManifest(){ function Bar({label, done, total, onExport, onImport, onReset, onGo}){ const pct = total > 0 ? Math.round((done/total)*100) : 0; return ( -
-
-
+
+
+
${label} ${done} / ${total}`}} />
-
-
{pct}%
+
+
{pct}%
@@ -55,8 +56,8 @@ function Bar({label, done, total, onExport, onImport, onReset, onGo}){
-
-
+
+
); @@ -212,7 +213,7 @@ function ProgressContainer(){ return (
-
+
{rows.map((r, i) => { const path = norm(SECTIONS[i][0]); return ( diff --git a/src/components/StartHereProgress/styles.module.css b/src/components/StartHereProgress/styles.module.css new file mode 100644 index 0000000..d279f45 --- /dev/null +++ b/src/components/StartHereProgress/styles.module.css @@ -0,0 +1,81 @@ +.borrMultiProgress { + display: flex; + flex-direction: column; + gap: 0.6rem; + padding: 0.5rem; +} + +.barContainer { + display: flex; + flex-direction: column; + gap: 0.4rem; + padding: 0.5rem; + border: 1px solid var(--ifm-color-emphasis-200, #e6eef6); + border-radius: 6px; + background: var(--ifm-background-color, #fff); +} + +.barHeader { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 0.5rem; +} + +.barLabel { + display: flex; + align-items: center; + flex: 1 1 auto; +} + +.barActions { + display: flex; + align-items: center; + gap: 0.5rem; + flex-wrap: wrap; + justify-content: flex-end; + flex: 1 1 auto; +} + +.barPercentage { + color: var(--ifm-color-emphasis-400, #666); + min-width: 3ch; +} + +.barProgressTrack { + height: 8px; + background: var(--ifm-color-emphasis-100, #f1f6fb); + border-radius: 6px; + overflow: hidden; +} + +.barProgressFill { + height: 100%; + transition: width 300ms ease; + background: linear-gradient( + 90deg, + var(--ifm-color-primary, #3b82f6), + var(--ifm-color-success, #25c29a) + ); +} + +/* Responsive tweaks */ +@media (max-width: 600px) { + .barHeader { + flex-direction: column; + align-items: stretch; + } + + .barActions { + justify-content: flex-end; + flex-wrap: wrap; + gap: 0.5rem; + width: 100%; + } + + .barActions button { + flex: 0 1 auto; + min-width: 100px; + } +} \ No newline at end of file