From 686e39a8d9ad42be0ea4d352742488df499bd46e Mon Sep 17 00:00:00 2001 From: SamruddhiPatil0078 Date: Fri, 29 May 2026 16:03:24 +0530 Subject: [PATCH] feat: add estimated reading time feature --- app/components/ReadingTime.tsx | 99 ++++++++++++++++++++++++++++++++ app/sem1/c/[chapter]/page.tsx | 15 +++-- app/sem1/em1/[chapter]/page.tsx | 15 +++-- app/sem1/ep/[chapter]/page.tsx | 31 +++++----- app/sem2/dsc/[chapter]/page.tsx | 15 +++-- app/sem2/em2/[chapter]/page.tsx | 15 +++-- app/sem2/oops/[chapter]/page.tsx | 15 +++-- app/sem3/coa/[chapter]/page.tsx | 13 +++-- app/sem4/dbms/[chapter]/page.tsx | 13 +++-- app/sem4/dops/[chapter]/page.tsx | 6 +- app/sem4/os/[chapter]/page.tsx | 6 +- app/sem5/cd/[chapter]/page.tsx | 6 +- app/sem5/cle/[chapter]/page.tsx | 24 ++++---- app/sem6/ml/[chapter]/page.tsx | 6 +- 14 files changed, 219 insertions(+), 60 deletions(-) create mode 100644 app/components/ReadingTime.tsx diff --git a/app/components/ReadingTime.tsx b/app/components/ReadingTime.tsx new file mode 100644 index 0000000..1e9f27f --- /dev/null +++ b/app/components/ReadingTime.tsx @@ -0,0 +1,99 @@ +'use client'; + +import React, { useEffect, useState } from 'react'; + +interface ReadingTimeProps { + /** + * Optional key to force recalculation when routes or chapters change. + */ + chapterKey?: string; +} + +export default function ReadingTime({ chapterKey }: ReadingTimeProps) { + const [readingTime, setReadingTime] = useState(null); + + useEffect(() => { + const calculateReadingTime = () => { + // Look for the specific content area first, then fallbacks + const contentElement = + document.getElementById('reading-content') || + document.querySelector('.course-content') || + document.querySelector('main'); + + if (contentElement) { + // Retrieve pure text content + const text = contentElement.textContent || ''; + // Extract words using whitespace regex matcher + const words = text.trim().split(/\s+/).filter((w) => w.length > 0).length; + // Average reading speed: 200 words per minute, rounded up + const time = Math.ceil(words / 200); + setReadingTime(time || 1); // Minimum 1 min read + } else { + setReadingTime(null); + } + }; + + // Delay slightly to ensure React has fully rendered and painted the child components to the DOM + const timer = setTimeout(calculateReadingTime, 150); + + return () => clearTimeout(timer); + }, [chapterKey]); + + if (readingTime === null) { + return ( +
+ + + + + Calculating reading time... +
+ ); + } + + return ( + <> + +
+ {/* Sleek SVG Clock with animated rotating hands on hover */} + + + + + + {readingTime} min read + +
+ + ); +} diff --git a/app/sem1/c/[chapter]/page.tsx b/app/sem1/c/[chapter]/page.tsx index c80013e..d7224d5 100644 --- a/app/sem1/c/[chapter]/page.tsx +++ b/app/sem1/c/[chapter]/page.tsx @@ -10,6 +10,7 @@ import { moduleQuizzes } from "@/lib/quizData"; import ChapterQuizInline from "../components/ChapterQuizInline"; import { ArrowBigLeft, ArrowBigRight } from "lucide-react"; import { Righteous } from "next/font/google"; +import ReadingTime from "@/app/components/ReadingTime"; const righteous = Righteous({ subsets: ['latin'], @@ -29,11 +30,12 @@ const chapters = [ ]; type ChapterProps = { - params: { chapter: string }; + params: Promise<{ chapter: string }>; }; -export default function ChapterPage({ params }: ChapterProps) { - const currentIndex = chapters.findIndex((c) => c.id === params.chapter); +export default async function ChapterPage({ params }: ChapterProps) { + const { chapter: chapterId } = await params; + const currentIndex = chapters.findIndex((c) => c.id === chapterId); const chapter = chapters[currentIndex]; if (!chapter) { @@ -53,7 +55,7 @@ export default function ChapterPage({ params }: ChapterProps) { ch6: "c-file-memory-preprocessors", }; - const chapterQuiz = moduleQuizzes.find((quiz) => quiz.slug === chapterQuizSlugMap[params.chapter]); + const chapterQuiz = moduleQuizzes.find((quiz) => quiz.slug === chapterQuizSlugMap[chapterId]); return (
@@ -63,6 +65,7 @@ export default function ChapterPage({ params }: ChapterProps) { Programming in C

{chapter.title}

+ {/* Navigation Buttons */}
@@ -92,7 +95,9 @@ export default function ChapterPage({ params }: ChapterProps) {

- +
+ +
{chapterQuiz ? (
diff --git a/app/sem1/em1/[chapter]/page.tsx b/app/sem1/em1/[chapter]/page.tsx index 507c77e..6fb3130 100644 --- a/app/sem1/em1/[chapter]/page.tsx +++ b/app/sem1/em1/[chapter]/page.tsx @@ -8,6 +8,7 @@ import { ArrowBigLeft, ArrowBigRight } from "lucide-react"; import { Righteous } from "next/font/google"; import { moduleQuizzes } from "@/lib/quizData"; import ChapterQuizInline from "../components/ChapterQuizInline"; +import ReadingTime from "@/app/components/ReadingTime"; const righteous = Righteous({ subsets: ["latin"], @@ -25,11 +26,12 @@ const chapters = [ ]; type ChapterProps = { - params: { chapter: string }; + params: Promise<{ chapter: string }>; }; -export default function ChapterPage({ params }: ChapterProps) { - const currentIndex = chapters.findIndex((c) => c.id === params.chapter); +export default async function ChapterPage({ params }: ChapterProps) { + const { chapter: chapterId } = await params; + const currentIndex = chapters.findIndex((c) => c.id === chapterId); const chapter = chapters[currentIndex]; if (!chapter) { @@ -46,7 +48,7 @@ export default function ChapterPage({ params }: ChapterProps) { ch3: "em1-ordinary-differential-equations", ch4: "em1-laplace-transforms", }; - const chapterQuiz = moduleQuizzes.find((quiz) => quiz.slug === chapterQuizSlugMap[params.chapter]); + const chapterQuiz = moduleQuizzes.find((quiz) => quiz.slug === chapterQuizSlugMap[chapterId]); return (
@@ -59,6 +61,7 @@ export default function ChapterPage({ params }: ChapterProps) {

{chapter.title}

+ {/* Top Navigation */}
@@ -88,7 +91,9 @@ export default function ChapterPage({ params }: ChapterProps) {
{/* Chapter Body */} - +
+ +
{chapterQuiz ? (
diff --git a/app/sem1/ep/[chapter]/page.tsx b/app/sem1/ep/[chapter]/page.tsx index 2cebf2a..8a9e7c8 100644 --- a/app/sem1/ep/[chapter]/page.tsx +++ b/app/sem1/ep/[chapter]/page.tsx @@ -9,6 +9,7 @@ import ChapterQuizInline from "../components/ChapterQuizInline"; import { ArrowBigLeft, ArrowBigRight } from "lucide-react"; import { Righteous } from "next/font/google"; import { moduleQuizzes } from "@/lib/quizData"; +import ReadingTime from "@/app/components/ReadingTime"; const righteous = Righteous({ subsets: ['latin'], @@ -27,11 +28,12 @@ const chapters = [ ]; type ChapterProps = { - params: { chapter: string }; + params: Promise<{ chapter: string }>; }; -export default function ChapterPage({ params }: ChapterProps) { - const currentIndex = chapters.findIndex((c) => c.id === params.chapter); +export default async function ChapterPage({ params }: ChapterProps) { + const { chapter: chapterId } = await params; + const currentIndex = chapters.findIndex((c) => c.id === chapterId); const chapter = chapters[currentIndex]; if (!chapter) { @@ -42,15 +44,15 @@ export default function ChapterPage({ params }: ChapterProps) { const prevChapter = currentIndex > 0 ? chapters[currentIndex - 1] : null; const nextChapter = currentIndex < chapters.length - 1 ? chapters[currentIndex + 1] : null; - const chapterQuizSlugMap: Record = { - ch1: "ep-vector-fields", - ch2: "ep-electrostatics-magnetostatics", - ch3: "ep-electrodynamics-maxwell", - ch4: "ep-superconductivity", - ch5: "ep-laser-fibre-optics", - }; - - const chapterQuiz = moduleQuizzes.find((quiz) => quiz.slug === chapterQuizSlugMap[params.chapter]); + const chapterQuizSlugMap: Record = { + ch1: "ep-vector-fields", + ch2: "ep-electrostatics-magnetostatics", + ch3: "ep-electrodynamics-maxwell", + ch4: "ep-superconductivity", + ch5: "ep-laser-fibre-optics", + }; + + const chapterQuiz = moduleQuizzes.find((quiz) => quiz.slug === chapterQuizSlugMap[chapterId]); return (
@@ -60,6 +62,7 @@ export default function ChapterPage({ params }: ChapterProps) { Engineering Physics

{chapter.title}

+ {/* Navigation Buttons */}
@@ -89,7 +92,9 @@ export default function ChapterPage({ params }: ChapterProps) {

- +
+ +
{chapterQuiz ? (
diff --git a/app/sem2/dsc/[chapter]/page.tsx b/app/sem2/dsc/[chapter]/page.tsx index 174d0da..33ac345 100644 --- a/app/sem2/dsc/[chapter]/page.tsx +++ b/app/sem2/dsc/[chapter]/page.tsx @@ -9,6 +9,7 @@ import { Ch3Content } from "../content/chapter3"; import { ArrowBigLeft, ArrowBigRight } from "lucide-react"; import { moduleQuizzes } from "@/lib/quizData"; import ChapterQuizInline from "../components/ChapterQuizInline"; +import ReadingTime from "@/app/components/ReadingTime"; const righteous = Righteous({ subsets: ["latin"], @@ -24,11 +25,12 @@ const chapters = [ ]; type ChapterProps = { - params: { chapter: string }; + params: Promise<{ chapter: string }>; }; -export default function ChapterPage({ params }: ChapterProps) { - const currentIndex = chapters.findIndex((c) => c.id === params.chapter); +export default async function ChapterPage({ params }: ChapterProps) { + const { chapter: chapterId } = await params; + const currentIndex = chapters.findIndex((c) => c.id === chapterId); const chapter = chapters[currentIndex]; if (!chapter) { @@ -43,7 +45,7 @@ export default function ChapterPage({ params }: ChapterProps) { const chapterQuizSlugMap: Record = { ch1: "dsc-arrays", }; - const chapterQuiz = moduleQuizzes.find((quiz) => quiz.slug === chapterQuizSlugMap[params.chapter]); + const chapterQuiz = moduleQuizzes.find((quiz) => quiz.slug === chapterQuizSlugMap[chapterId]); return (
@@ -56,6 +58,7 @@ export default function ChapterPage({ params }: ChapterProps) {

{chapter.title}

+ {/* Navigation */}
@@ -85,7 +88,9 @@ export default function ChapterPage({ params }: ChapterProps) {

- +
+ +
{chapterQuiz ? (
diff --git a/app/sem2/em2/[chapter]/page.tsx b/app/sem2/em2/[chapter]/page.tsx index 498dc9b..070fd3c 100644 --- a/app/sem2/em2/[chapter]/page.tsx +++ b/app/sem2/em2/[chapter]/page.tsx @@ -8,6 +8,7 @@ import { ArrowBigLeft, ArrowBigRight } from "lucide-react"; import { Righteous } from "next/font/google"; import { moduleQuizzes } from "@/lib/quizData"; import ChapterQuizInline from "../components/ChapterQuizInline"; +import ReadingTime from "@/app/components/ReadingTime"; const righteous = Righteous({ subsets: ["latin"], @@ -24,11 +25,12 @@ const chapters = [ ]; type ChapterProps = { - params: { chapter: string }; + params: Promise<{ chapter: string }>; }; -export default function ChapterPage({ params }: ChapterProps) { - const currentIndex = chapters.findIndex((c) => c.id === params.chapter); +export default async function ChapterPage({ params }: ChapterProps) { + const { chapter: chapterId } = await params; + const currentIndex = chapters.findIndex((c) => c.id === chapterId); const chapter = chapters[currentIndex]; if (!chapter) { @@ -46,7 +48,7 @@ export default function ChapterPage({ params }: ChapterProps) { ch3: "em2-complex-variables", ch4: "em2-integral-calculus", }; - const chapterQuiz = moduleQuizzes.find((quiz) => quiz.slug === chapterQuizSlugMap[params.chapter]); + const chapterQuiz = moduleQuizzes.find((quiz) => quiz.slug === chapterQuizSlugMap[chapterId]); return (
@@ -58,6 +60,7 @@ export default function ChapterPage({ params }: ChapterProps) {

{chapter.title}

+ {/* Top Navigation */}
@@ -88,7 +91,9 @@ export default function ChapterPage({ params }: ChapterProps) {
- +
+ +
{chapterQuiz ? (
diff --git a/app/sem2/oops/[chapter]/page.tsx b/app/sem2/oops/[chapter]/page.tsx index 1b33ad9..a1d19e9 100644 --- a/app/sem2/oops/[chapter]/page.tsx +++ b/app/sem2/oops/[chapter]/page.tsx @@ -14,6 +14,7 @@ import { Ch8Content } from "../content/chapter8"; import { ArrowBigLeft, ArrowBigRight } from "lucide-react"; import { moduleQuizzes } from "@/lib/quizData"; import ChapterQuizInline from "../components/ChapterQuizInline"; +import ReadingTime from "@/app/components/ReadingTime"; const righteous = Righteous({ subsets: ["latin"], @@ -34,11 +35,12 @@ const chapters = [ ]; type ChapterProps = { - params: { chapter: string }; + params: Promise<{ chapter: string }>; }; -export default function ChapterPage({ params }: ChapterProps) { - const currentIndex = chapters.findIndex((c) => c.id === params.chapter); +export default async function ChapterPage({ params }: ChapterProps) { + const { chapter: chapterId } = await params; + const currentIndex = chapters.findIndex((c) => c.id === chapterId); const chapter = chapters[currentIndex]; if (!chapter) { @@ -60,7 +62,7 @@ export default function ChapterPage({ params }: ChapterProps) { ch7: "oops-generics", ch8: "oops-java-lib-swing", }; - const chapterQuiz = moduleQuizzes.find((quiz) => quiz.slug === chapterQuizSlugMap[params.chapter]); + const chapterQuiz = moduleQuizzes.find((quiz) => quiz.slug === chapterQuizSlugMap[chapterId]); return (
@@ -73,6 +75,7 @@ export default function ChapterPage({ params }: ChapterProps) {

{chapter.title}

+ {/* Navigation */}
@@ -102,7 +105,9 @@ export default function ChapterPage({ params }: ChapterProps) {

- +
+ +
{chapterQuiz ? (
diff --git a/app/sem3/coa/[chapter]/page.tsx b/app/sem3/coa/[chapter]/page.tsx index 1ac3bbd..b03800e 100644 --- a/app/sem3/coa/[chapter]/page.tsx +++ b/app/sem3/coa/[chapter]/page.tsx @@ -10,6 +10,7 @@ import { Ch7Content } from "../content/chapter7"; import { Ch8Content } from "../content/chapter8"; import { ArrowBigLeft, ArrowBigRight } from "lucide-react"; import { Righteous } from "next/font/google"; +import ReadingTime from "@/app/components/ReadingTime"; const righteous = Righteous({ subsets: ['latin'], @@ -31,11 +32,12 @@ const chapters = [ ]; type ChapterProps = { - params: { chapter: string }; + params: Promise<{ chapter: string }>; }; -export default function ChapterPage({ params }: ChapterProps) { - const currentIndex = chapters.findIndex((c) => c.id === params.chapter); +export default async function ChapterPage({ params }: ChapterProps) { + const { chapter: chapterId } = await params; + const currentIndex = chapters.findIndex((c) => c.id === chapterId); const chapter = chapters[currentIndex]; if (!chapter) { @@ -54,6 +56,7 @@ export default function ChapterPage({ params }: ChapterProps) { Computer Organization and Architecture

{chapter.title}

+ {/* Navigation Buttons */}
@@ -83,7 +86,9 @@ export default function ChapterPage({ params }: ChapterProps) {

- +
+ +
{/* Navigation Buttons */} diff --git a/app/sem4/dbms/[chapter]/page.tsx b/app/sem4/dbms/[chapter]/page.tsx index eccd7b8..212c8ae 100644 --- a/app/sem4/dbms/[chapter]/page.tsx +++ b/app/sem4/dbms/[chapter]/page.tsx @@ -11,6 +11,7 @@ import { Ch1Content } from "../content/chapter1"; import { ArrowBigLeft, ArrowBigRight } from "lucide-react"; import { Righteous } from "next/font/google"; +import ReadingTime from "@/app/components/ReadingTime"; const righteous = Righteous({ subsets: ["latin"], @@ -31,11 +32,12 @@ const chapters = [ ]; type ChapterProps = { - params: { chapter: string }; + params: Promise<{ chapter: string }>; }; -export default function ChapterPage({ params }: ChapterProps) { - const currentIndex = chapters.findIndex((c) => c.id === params.chapter); +export default async function ChapterPage({ params }: ChapterProps) { + const { chapter: chapterId } = await params; + const currentIndex = chapters.findIndex((c) => c.id === chapterId); const chapter = chapters[currentIndex]; if (!chapter) { @@ -56,6 +58,7 @@ export default function ChapterPage({ params }: ChapterProps) {

{chapter.title}

+
{prevChapter ? ( @@ -83,7 +86,9 @@ export default function ChapterPage({ params }: ChapterProps) {
- +
+ +
diff --git a/app/sem4/dops/[chapter]/page.tsx b/app/sem4/dops/[chapter]/page.tsx index 9f7a476..4ef758f 100644 --- a/app/sem4/dops/[chapter]/page.tsx +++ b/app/sem4/dops/[chapter]/page.tsx @@ -12,6 +12,7 @@ import { Ch7Content } from "../content/chapter7"; import { Ch8Content } from "../content/chapter8"; import { ArrowBigLeft, ArrowBigRight } from "lucide-react"; +import ReadingTime from "@/app/components/ReadingTime"; const righteous = Righteous({ subsets: ["latin"], @@ -59,6 +60,7 @@ export default function ChapterPage({ params }: ChapterProps) {

{chapter.title}

+ {/* Navigation */}
@@ -88,7 +90,9 @@ export default function ChapterPage({ params }: ChapterProps) {

- +
+ +
{/* Bottom Navigation */} diff --git a/app/sem4/os/[chapter]/page.tsx b/app/sem4/os/[chapter]/page.tsx index b6e1f9c..9daeb15 100644 --- a/app/sem4/os/[chapter]/page.tsx +++ b/app/sem4/os/[chapter]/page.tsx @@ -12,6 +12,7 @@ import ChapterQuizInline from "../components/ChapterQuizInline"; import { ArrowBigLeft, ArrowBigRight } from "lucide-react"; import { Righteous } from "next/font/google"; import { moduleQuizzes } from "@/lib/quizData"; +import ReadingTime from "@/app/components/ReadingTime"; const righteous = Righteous({ subsets: ["latin"], @@ -131,6 +132,7 @@ export default function ChapterPage({ params }: ChapterProps) { > {chapter.title}

+ {/* Top Navigation */}
@@ -163,7 +165,9 @@ export default function ChapterPage({ params }: ChapterProps) {
- +
+ +
{chapterQuiz ? (
diff --git a/app/sem5/cd/[chapter]/page.tsx b/app/sem5/cd/[chapter]/page.tsx index 967e05a..bce887a 100644 --- a/app/sem5/cd/[chapter]/page.tsx +++ b/app/sem5/cd/[chapter]/page.tsx @@ -24,6 +24,7 @@ import { LalrSolvedProblemContent } from "../content/ch11-lalr-solved-problem"; import { Ch12Content } from "../content/chapter12"; import { ArrowBigLeft, ArrowBigRight } from "lucide-react"; import { chapters, SubTopic } from "../constants"; +import ReadingTime from "@/app/components/ReadingTime"; const righteous = Righteous({ subsets: ["latin"], @@ -173,6 +174,7 @@ export default async function ChapterPage({ params }: ChapterProps) { ? `${parentChapter.title} / ${chapterData.title}` : chapterData.title}

+ {/* Navigation */}
@@ -202,7 +204,9 @@ export default async function ChapterPage({ params }: ChapterProps) {

- {ChapterComponent ? :

Content loading...

} +
+ {ChapterComponent ? :

Content loading...

} +
{/* Bottom Navigation */} diff --git a/app/sem5/cle/[chapter]/page.tsx b/app/sem5/cle/[chapter]/page.tsx index efac1a9..1ba131c 100644 --- a/app/sem5/cle/[chapter]/page.tsx +++ b/app/sem5/cle/[chapter]/page.tsx @@ -4,6 +4,7 @@ import { Righteous } from "next/font/google"; import { Ch0Content } from "../content/chapter0"; // ← only ch0 for now import { ArrowBigLeft, ArrowBigRight } from "lucide-react"; import { chapters, Chapter, SubTopic } from "../constants"; // ← cle constants +import ReadingTime from "@/app/components/ReadingTime"; function findChapterOrSubtopic(chapterId: string) { const chapter = chapters.find((c) => c.id === chapterId); @@ -117,6 +118,7 @@ export default async function ChapterPage({ params }: ChapterProps) { ? `${parentChapter.title} / ${chapterData.title}` : chapterData.title}

+
{prevChapter ? ( @@ -146,16 +148,18 @@ export default async function ChapterPage({ params }: ChapterProps) {
{/* Show content if available, else show coming soon message */} - {ChapterComponent ? ( - - ) : ( -
-

Coming Soon

-

- This chapter is under development. Check back soon! -

-
- )} +
+ {ChapterComponent ? ( + + ) : ( +
+

Coming Soon

+

+ This chapter is under development. Check back soon! +

+
+ )} +
diff --git a/app/sem6/ml/[chapter]/page.tsx b/app/sem6/ml/[chapter]/page.tsx index 9faee9e..0716066 100644 --- a/app/sem6/ml/[chapter]/page.tsx +++ b/app/sem6/ml/[chapter]/page.tsx @@ -45,6 +45,7 @@ import { ArrowBigLeft, ArrowBigRight } from "lucide-react"; import { chapters, Chapter, SubTopic } from "../constants"; import { moduleQuizzes } from "@/lib/quizData"; import ChapterQuizInline from "../components/ChapterQuizInline"; +import ReadingTime from "@/app/components/ReadingTime"; function findChapterOrSubtopic(chapterId: string) { const chapter = chapters.find((c) => c.id === chapterId); @@ -198,6 +199,7 @@ export default async function ChapterPage({ params }: ChapterProps) {

{isSubTopic && parentChapter ? `${parentChapter.title} / ${chapterData.title}` : chapterData.title}

+ {/* Navigation */}
@@ -227,7 +229,9 @@ export default async function ChapterPage({ params }: ChapterProps) {

- +
+ +
{chapterQuiz ? (