From 605282064fad05b8addfd80349f0d64f49fc3210 Mon Sep 17 00:00:00 2001 From: knoxiboy Date: Fri, 24 Jul 2026 13:53:59 +0530 Subject: [PATCH 1/6] fix(seo): remove unstable lastModified dates from static sitemap entries (#946) --- src/__tests__/app/sitemap.test.ts | 47 +++++++++++++++++++++++++++++++ src/app/sitemap.ts | 19 +++++++------ 2 files changed, 57 insertions(+), 9 deletions(-) create mode 100644 src/__tests__/app/sitemap.test.ts diff --git a/src/__tests__/app/sitemap.test.ts b/src/__tests__/app/sitemap.test.ts new file mode 100644 index 00000000..58dd34c4 --- /dev/null +++ b/src/__tests__/app/sitemap.test.ts @@ -0,0 +1,47 @@ +import sitemap from "@/app/sitemap"; + +const siteUrlEnvironmentKeys = [ + "NEXT_PUBLIC_SITE_URL", + "NEXT_PUBLIC_APP_URL", + "VERCEL_PROJECT_PRODUCTION_URL", + "VERCEL_URL", +] as const; + +const originalEnvironment = Object.fromEntries( + siteUrlEnvironmentKeys.map((key) => [key, process.env[key]]), +); + +describe("sitemap", () => { + afterEach(() => { + for (const key of siteUrlEnvironmentKeys) { + const originalValue = originalEnvironment[key]; + if (originalValue === undefined) { + delete process.env[key]; + } else { + process.env[key] = originalValue; + } + } + }); + + it("preserves static route metadata without generated modification dates", () => { + for (const key of siteUrlEnvironmentKeys) { + delete process.env[key]; + } + + const entries = sitemap(); + expect(entries.every((entry) => !("lastModified" in entry))).toBe(true); + expect(entries[0].url).toBe("https://doubt-desk-seven.vercel.app"); + expect(entries[0].changeFrequency).toBe("weekly"); + expect(entries[0].priority).toBe(1); + }); + + it("uses the configured site URL without introducing duplicate slashes", () => { + process.env.NEXT_PUBLIC_SITE_URL = "https://docs.example.com/"; + + const entries = sitemap(); + + expect(entries[0].url).toBe("https://docs.example.com"); + expect(entries[1].url).toBe("https://docs.example.com/about"); + expect(entries.every((entry) => !("lastModified" in entry))).toBe(true); + }); +}); diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts index 62716e02..e2d8ad3d 100644 --- a/src/app/sitemap.ts +++ b/src/app/sitemap.ts @@ -16,14 +16,15 @@ export default function sitemap(): MetadataRoute.Sitemap { const baseUrl = getSiteUrl(); return [ - { url: baseUrl, lastModified: new Date(), changeFrequency: "weekly", priority: 1 }, - { url: `${baseUrl}/about`, lastModified: new Date(), changeFrequency: "monthly", priority: 0.8 }, - { url: `${baseUrl}/faq`, lastModified: new Date(), changeFrequency: "monthly", priority: 0.7 }, - { url: `${baseUrl}/contributors`, lastModified: new Date(), changeFrequency: "weekly", priority: 0.6 }, - { url: `${baseUrl}/contact`, lastModified: new Date(), changeFrequency: "monthly", priority: 0.5 }, - { url: `${baseUrl}/discussions`, lastModified: new Date(), changeFrequency: "daily", priority: 0.8 }, - { url: `${baseUrl}/public-rooms`, lastModified: new Date(), changeFrequency: "daily", priority: 0.9 }, - { url: `${baseUrl}/terms-of-service`, lastModified: new Date(), changeFrequency: "yearly", priority: 0.3 }, - { url: `${baseUrl}/privacy-policy`, lastModified: new Date(), changeFrequency: "yearly", priority: 0.3 }, + { url: baseUrl, changeFrequency: "weekly", priority: 1 }, + { url: `${baseUrl}/about`, changeFrequency: "monthly", priority: 0.8 }, + { url: `${baseUrl}/faq`, changeFrequency: "monthly", priority: 0.7 }, + { url: `${baseUrl}/contributors`, changeFrequency: "weekly", priority: 0.6 }, + { url: `${baseUrl}/contact`, changeFrequency: "monthly", priority: 0.5 }, + { url: `${baseUrl}/discussions`, changeFrequency: "daily", priority: 0.8 }, + { url: `${baseUrl}/roadmaps`, changeFrequency: "weekly", priority: 0.7 }, + { url: `${baseUrl}/public-rooms`, changeFrequency: "daily", priority: 0.9 }, + { url: `${baseUrl}/terms-of-service`, changeFrequency: "yearly", priority: 0.3 }, + { url: `${baseUrl}/privacy-policy`, changeFrequency: "yearly", priority: 0.3 }, ]; } From 1c3afeb974bd972daf50ba9ec0c4ad1007e41f13 Mon Sep 17 00:00:00 2001 From: knoxiboy Date: Fri, 24 Jul 2026 13:59:36 +0530 Subject: [PATCH 2/6] feat(ui): add client-side search filter for bookmarks (#656) --- src/app/(routes)/bookmarks/page.tsx | 38 ++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/src/app/(routes)/bookmarks/page.tsx b/src/app/(routes)/bookmarks/page.tsx index a0c48d4a..846d040e 100644 --- a/src/app/(routes)/bookmarks/page.tsx +++ b/src/app/(routes)/bookmarks/page.tsx @@ -1,7 +1,7 @@ "use client"; import { useEffect, useState } from "react"; -import { Bookmark, Loader2, ArrowLeft, RefreshCw } from "lucide-react"; +import { Bookmark, Loader2, ArrowLeft, RefreshCw, Search, X } from "lucide-react"; import DoubtCard from "@/components/classroom/DoubtCard"; import { useRouter } from "next/navigation"; import { useAppUser } from "@/app/provider"; @@ -21,6 +21,7 @@ export default function BookmarksPage() { const [bookmarks, setBookmarks] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [searchQuery, setSearchQuery] = useState(""); const router = useRouter(); const { appUser } = useAppUser(); const { isLoaded, isSignedIn } = useAuth(); @@ -54,6 +55,15 @@ export default function BookmarksPage() { fetchBookmarks(); }, [isLoaded, isSignedIn]); + const filteredBookmarks = bookmarks.filter((doubt) => { + if (!searchQuery.trim()) return true; + const q = searchQuery.toLowerCase(); + return ( + doubt.subject?.toLowerCase().includes(q) || + doubt.content?.toLowerCase().includes(q) + ); + }); + return ( <> @@ -86,6 +96,28 @@ export default function BookmarksPage() { + {!loading && !error && bookmarks.length > 0 && ( +
+ + setSearchQuery(e.target.value)} + placeholder="Search bookmarks by subject or content..." + className="w-full pl-11 pr-10 py-3 rounded-xl border border-slate-200 dark:border-zinc-800 bg-slate-50 dark:bg-zinc-900 text-slate-900 dark:text-white placeholder:text-slate-400 dark:placeholder:text-zinc-500 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-purple-500/40 transition-all" + /> + {searchQuery && ( + + )} +
+ )} + {loading ? (
@@ -112,9 +144,9 @@ export default function BookmarksPage() { {UI_TEXT.RETRY_BUTTON}
- ) : bookmarks.length > 0 ? ( + ) : filteredBookmarks.length > 0 ? (
- {bookmarks.map((doubt) => ( + {filteredBookmarks.map((doubt) => ( Date: Fri, 24 Jul 2026 14:01:42 +0530 Subject: [PATCH 3/6] feat(ui): enhance navbar hover effects and accessibility (#752) --- src/components/layout/Header.tsx | 39 ++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index 25676cff..e4299f23 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useCallback } from "react"; import Link from "next/link"; import { useRouter, usePathname } from "next/navigation"; import { SignedIn, SignedOut, UserButton } from "@clerk/nextjs"; @@ -31,6 +31,31 @@ export default function Header() { return () => window.removeEventListener("popstate", handlePopState); }, [pathname]); + // Lock body scroll while the mobile menu is open + useEffect(() => { + if (isOpen) { + const previousOverflow = document.body.style.overflow; + document.body.style.overflow = "hidden"; + return () => { + document.body.style.overflow = previousOverflow; + }; + } + }, [isOpen]); + + // Close mobile menu on Escape + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape") setIsOpen(false); + }; + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, []); + + // Close mobile menu automatically on route change + useEffect(() => { + setIsOpen(false); + }, [pathname]); + // Scroll Progress Bar useEffect(() => { const updateScrollProgress = () => { @@ -55,8 +80,14 @@ export default function Header() { { href: "/public-rooms", label: "Public Rooms" }, { href: "/faq", label: "FAQ" }, { href: "/contact", label: "Contact" }, + { href: "/about", label: "About" }, ]; + const isActive = useCallback( + (href: string) => !href.startsWith("#") && pathname === href, + [pathname] + ); + const handleScrollNavigation = (targetId: string) => { setIsOpen(false); @@ -77,7 +108,7 @@ export default function Header() { {showBackButton && ( - - - - - - - - - - -
- - {/* Right Live Panel Graphic Column */} -
-
- -
-
- - - - {/* Features Section */} -
-
-
-
- Features -
-

- Everything your classroom needs to solve doubts, stay aligned, - and move faster. -

-

- Built for modern study teams, DoubtDesk blends AI-powered doubt - solving, shared resources, and smart classroom flows into a - single polished platform. -

-
- -
- {features.map((feature, i) => { - const Icon = feature.icon; - return ( -
-
- -
-

- {feature.title} -

-

- {feature.description} -

-
- ); - })} -
-
-
- - {/* How It Works, Testimonials, etc. remain unchanged */} - {/* How It Works Section */} -
-
-
-
- Process -
-

- How it works -

-

- Simple flow from doubt → solution → understanding -

-
-
-
- {howItWorks.map((step, index) => ( -
-
- {index + 1} -
-

- {step.title} -

-

- {step.description} -

-
- ))} -
-
-
- - {/* Testimonials Section */} -
-
-
-
- Testimonials -
-

- What students say -

-

- Real feedback from learners and educators -

-
-
- {testimonials.map((t, index) => ( -
-

- “{t.quote}” -

-
-
- {t.name} -
-
- {t.role} -
-
-
- ))} -
-
-
- + + + + + + + + ); } + diff --git a/src/components/ScrollToTopButton.tsx b/src/components/ScrollToTopButton.tsx new file mode 100644 index 00000000..1d1acbec --- /dev/null +++ b/src/components/ScrollToTopButton.tsx @@ -0,0 +1,124 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { usePathname } from "next/navigation"; +import { ArrowUp } from "lucide-react"; +import { cn } from "@/lib/utils/utils"; +import { getScrollBehavior } from "@/lib/utils/scroll-to-section"; + +type ScrollToTopButtonProps = { + showProgress?: boolean; + /** Scroll progress % (0–100) to show button when showProgress is true */ + progressThreshold?: number; + /** Pixel scroll offset to show button when showProgress is false */ + scrollThreshold?: number; +}; + +export default function ScrollToTopButton({ + showProgress = false, + progressThreshold = 5, + scrollThreshold = 300, +}: ScrollToTopButtonProps) { + const pathname = usePathname(); + const showProgressOnHome = showProgress || pathname === "/"; + const [isVisible, setIsVisible] = useState(false); + const [scrollProgress, setScrollProgress] = useState(0); + + useEffect(() => { + const handleScroll = () => { + const scrollTop = window.scrollY; + const docHeight = + document.documentElement.scrollHeight - window.innerHeight; + const progress = + docHeight > 0 ? (scrollTop / docHeight) * 100 : 0; + setScrollProgress(progress); + + if (showProgressOnHome) { + setIsVisible(progress > progressThreshold); + } else { + setIsVisible(scrollTop > scrollThreshold); + } + }; + + handleScroll(); + window.addEventListener("scroll", handleScroll, { passive: true }); + + return () => window.removeEventListener("scroll", handleScroll); + }, [showProgressOnHome, progressThreshold, scrollThreshold]); + + if (!isVisible) return null; + + const circumference = 2 * Math.PI * 24; + const scrollToTop = () => + window.scrollTo({ top: 0, behavior: getScrollBehavior() }); + + const focusRingClass = + "focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:ring-offset-background"; + + if (showProgressOnHome) { + return ( + + ); + } + + return ( + + ); +} diff --git a/src/components/landing/FeaturesSection.tsx b/src/components/landing/FeaturesSection.tsx new file mode 100644 index 00000000..82b21aba --- /dev/null +++ b/src/components/landing/FeaturesSection.tsx @@ -0,0 +1,60 @@ +import { features } from "./landingContent"; +import { Staatliches } from "next/font/google"; + +const staatliches = Staatliches({ weight: "400", subsets: ["latin"] }); + +export default function FeaturesSection() { + return ( +
+
+
+
+ Features +
+

+ Everything your classroom needs to solve doubts, stay aligned, + and move faster. +

+

+ Built for modern study teams, DoubtDesk blends AI-powered doubt + solving, shared resources, and smart classroom flows into a + single polished platform. +

+
+ +
+ {features.map((feature, i) => { + const Icon = feature.icon; + return ( +
+
+ +
+

+ {feature.title} +

+

+ {feature.description} +

+
+ ); + })} +
+
+
+ ); +} + diff --git a/src/components/landing/HeroSection.tsx b/src/components/landing/HeroSection.tsx new file mode 100644 index 00000000..78e834bd --- /dev/null +++ b/src/components/landing/HeroSection.tsx @@ -0,0 +1,105 @@ +import { ArrowRight, Globe } from "lucide-react"; +import Link from "next/link"; +import { SignedIn, SignedOut } from "@clerk/nextjs"; +import { Staatliches } from "next/font/google"; +import ShapeGrid from "@/components/marketing/ShapeGrid"; +import LiveCampusThreadPanel from "@/components/classroom/LiveCampusThreadPanel"; + +const staatliches = Staatliches({ weight: "400", subsets: ["latin"] }); + + +export default function HeroSection() { + return ( +
+
+ +
+
+
+
+
+
+

+ Empower
+ Your Learning
+ with{" "} + + Collaborative AI. + +

+ +
+
+ Collaborative classrooms +
+

+ Built for collaborative classrooms, instant doubt solving, and + smarter learning. +

+
+ +
+ + + + + + + + + + + + + + + +
+
+ +
+
+ +
+
+
+
+
+ ); +} + diff --git a/src/components/landing/HowItWorksSection.tsx b/src/components/landing/HowItWorksSection.tsx new file mode 100644 index 00000000..c86a8cd7 --- /dev/null +++ b/src/components/landing/HowItWorksSection.tsx @@ -0,0 +1,47 @@ +import { howItWorks } from "./landingContent"; + +export default function HowItWorksSection() { + return ( +
+
+
+
+ Process +
+

+ How it works +

+

+ Simple flow from doubt → solution → understanding +

+
+ +
+
+ + {howItWorks.map((step, index) => ( +
+
+ {index + 1} +
+

+ {step.title} +

+

+ {step.description} +

+
+ ))} +
+
+
+ ); +} + diff --git a/src/components/landing/SignOutDialog.tsx b/src/components/landing/SignOutDialog.tsx new file mode 100644 index 00000000..4783896e --- /dev/null +++ b/src/components/landing/SignOutDialog.tsx @@ -0,0 +1,48 @@ +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; + +type SignOutDialogProps = { + open: boolean; + onOpenChange: (open: boolean) => void; + onSignOut: () => void | Promise; +}; + +export default function SignOutDialog({ + open, + onOpenChange, + onSignOut, +}: SignOutDialogProps) { + return ( + + + + Are you sure you want to sign out? + + You will need to log in again to access your classroom insights and + doubt-solving history. + + + + + Cancel + + + Sign Out + + + + + ); +} + diff --git a/src/components/landing/TestimonialsSection.tsx b/src/components/landing/TestimonialsSection.tsx new file mode 100644 index 00000000..a0de75ce --- /dev/null +++ b/src/components/landing/TestimonialsSection.tsx @@ -0,0 +1,47 @@ +import { testimonials } from "./landingContent"; + +export default function TestimonialsSection() { + return ( +
+
+
+
+ Testimonials +
+

+ What students say +

+

+ Real feedback from learners and educators +

+
+ +
+ {testimonials.map((t, index) => ( +
+

+ “{t.quote}” +

+
+
+ {t.name} +
+
+ {t.role} +
+
+
+ ))} +
+
+
+ ); +} + diff --git a/src/components/landing/landingContent.ts b/src/components/landing/landingContent.ts new file mode 100644 index 00000000..e593fac1 --- /dev/null +++ b/src/components/landing/landingContent.ts @@ -0,0 +1,157 @@ +import { + Activity, + Clipboard, + LayoutGrid, + Map, + MessageCircle, + Users, +} from "lucide-react"; + +export type LandingFeature = { + slug: string; + title: string; + description: string; + // Stored as a React component type (lucide icons) + icon: React.ComponentType<{ className?: string }>; +}; + + +export type LandingHowItWorksStep = { + title: string; + description: string; +}; + +export type LandingTestimonial = { + id: string; + name: string; + role: string; + institution: string; + avatarUrl: string; + quote: string; + rating: number; +}; + +export const features: LandingFeature[] = [ + { + slug: "collaborative-discussions", + title: "Real-time collaborative discussions", + description: + "Share questions, answers, and classroom updates instantly across study groups.", + icon: MessageCircle, + }, + { + slug: "classroom-management", + title: "Smart classroom management", + description: + "Organize learning spaces, schedules, and teacher workflows with ease.", + icon: LayoutGrid, + }, + { + slug: "notes-resource-sharing", + title: "Notes and resource sharing", + description: + "Keep study materials, highlights, and shared guides organized in one hub.", + icon: Clipboard, + }, + { + slug: "learning-roadmaps", + title: "Learning roadmaps and guidance", + description: + "Follow curated study paths that keep learners focused on milestones.", + icon: Map, + }, + { + slug: "ai-powered-doubt-solving", + title: "AI-powered doubt solving", + description: + "Get instant, context-aware answers to questions with smart AI support.", + icon: Activity, + }, + { + slug: "study-collaboration", + title: "Organized study collaboration", + description: + "Coordinate projects, peer review, and group work with clear tools and structure.", + icon: Users, + }, +]; + +export const howItWorks: LandingHowItWorksStep[] = [ + { + title: "Join or create a classroom", + description: "Teachers set up rooms, students join using invite codes.", + }, + { + title: "Ask doubts instantly", + description: "Post questions using text or image and get AI + peer help.", + }, + { + title: "Get clear answers & insights", + description: + "AI explanations, teacher guidance, and analytics all in one place.", + }, +]; + +export const testimonials: LandingTestimonial[] = [ + { + id: "1", + name: "Aarav Sharma", + role: "B.Tech Student", + institution: "DTU", + avatarUrl: "/avatars/arav.png", + quote: + "DoubtDesk helped me resolve doubts 3x faster during exams. The AI explanations are incredibly clear.", + rating: 5, + }, + { + id: "2", + name: "Neha Verma", + role: "CS Student", + institution: "Punjab University", + avatarUrl: "/avatars/neha.png", + quote: + "Everything is organized in one place. No more scrolling through endless chat groups.", + rating: 5, + }, + { + id: "3", + name: "Rohit Mehta", + role: "Teaching Assistant", + institution: "NIT Jalandhar", + avatarUrl: "/avatars/rohit.png", + quote: + "Analytics help me quickly identify where students struggle the most.", + rating: 5, + }, + { + id: "4", + name: "Priya Singh", + role: "MBA Student", + institution: "IIM Indore", + avatarUrl: "/avatars/priya.png", + quote: + "My exam preparation became much more efficient after switching to DoubtDesk.", + rating: 5, + }, + { + id: "5", + name: "Karan Gupta", + role: "Engineering Student", + institution: "PEC Chandigarh", + avatarUrl: "/avatars/karan.png", + quote: + "Instant answers saved hours of searching through notes and forums.", + rating: 5, + }, + { + id: "6", + name: "Ananya Kapoor", + role: "Professor", + institution: "Chitkara University", + avatarUrl: "/avatars/ananya.png", + quote: + "Managing classroom discussions has become significantly easier.", + rating: 5, + }, +]; + diff --git a/src/lib/scroll-to-section.ts b/src/lib/scroll-to-section.ts new file mode 100644 index 00000000..4385d2d6 --- /dev/null +++ b/src/lib/scroll-to-section.ts @@ -0,0 +1,25 @@ +export function getScrollBehavior(): ScrollBehavior { + const prefersReducedMotion = window.matchMedia( + "(prefers-reduced-motion: reduce)" + ).matches; + return prefersReducedMotion ? "auto" : "smooth"; +} + +export function scrollToSection( + targetId: string, + options?: { updateHash?: boolean } +) { + const target = document.getElementById(targetId); + if (!target) return; + + const headerHeight = + document.querySelector("header")?.getBoundingClientRect().height ?? 80; + const top = + target.getBoundingClientRect().top + window.scrollY - headerHeight; + + window.scrollTo({ top, behavior: getScrollBehavior() }); + + if (options?.updateHash !== false) { + history.pushState(null, "", `#${targetId}`); + } +}