From 9427ca653e38c48623d398aadf982bf87a8f484c Mon Sep 17 00:00:00 2001 From: KhinMeMeLatt Date: Sun, 23 Aug 2026 09:14:16 +0000 Subject: [PATCH] feat: implement issue #72 Generated autonomously by LazyDev. --- .lazydev/memory/global_repo_structure.md | 17 +++ .serena/.gitignore | 2 + .serena/project.yml | 169 +++++++++++++++++++++ app/api/questions/latest/route.ts | 17 +++ app/onboarding/page.tsx | 10 ++ components/auth-provider.tsx | 105 +++++++------ components/onboarding/latest-questions.tsx | 123 +++++++++++++++ components/onboarding/onboarding-view.tsx | 53 +++++++ lib/questions.ts | 27 ++++ 9 files changed, 476 insertions(+), 47 deletions(-) create mode 100644 .lazydev/memory/global_repo_structure.md create mode 100644 .serena/.gitignore create mode 100644 .serena/project.yml create mode 100644 app/api/questions/latest/route.ts create mode 100644 app/onboarding/page.tsx create mode 100644 components/onboarding/latest-questions.tsx create mode 100644 components/onboarding/onboarding-view.tsx create mode 100644 lib/questions.ts diff --git a/.lazydev/memory/global_repo_structure.md b/.lazydev/memory/global_repo_structure.md new file mode 100644 index 0000000..3ea5863 --- /dev/null +++ b/.lazydev/memory/global_repo_structure.md @@ -0,0 +1,17 @@ + + +This is a **Next.js (App Router) + TypeScript** full-stack web application, styled with **Tailwind CSS** and **shadcn/ui** components. It uses **MongoDB** (via `lib/mongodb.ts`) as its database and is managed with **pnpm**. + +The project centers on a **question-bank / exam-management** platform. Its main functional areas are: + +- **Public-facing UI** (`app/page.tsx`, `components/home/`, `components/question-card.tsx`, `components/exam-type-selector.tsx`): browse questions, submit new questions through a modal, and select exam types. +- **Admin area** (`app/admin/page.tsx`, `components/admin/`): view question list and manage pending approvals. +- **API routes** (`app/api/`): handle exam-type retrieval, question submission, individual question updates, and data seeding/migration. +- **Authentication** (`components/auth-provider.tsx`, `app/login/page.tsx`): supports login/authentication (likely session-based via provider). +- **Scripts** (`scripts/`): TypeScript utilities for seeding exam types and updating existing questions. + +**Business logic** resides primarily in the **API route handlers** (`app/api/**`) and **database utility** (`lib/mongodb.ts`), with type definitions in `lib/types.ts`. Scripts contain one-off data operations. UI logic lives inside components and hooks. + +**Tests**: No test files or test directories are present. + +**Tech stack summary**: Next.js (App Router), React, TypeScript, Tailwind CSS, shadcn/ui, MongoDB, pnpm, Prettier, PostCSS. diff --git a/.serena/.gitignore b/.serena/.gitignore new file mode 100644 index 0000000..2e510af --- /dev/null +++ b/.serena/.gitignore @@ -0,0 +1,2 @@ +/cache +/project.local.yml diff --git a/.serena/project.yml b/.serena/project.yml new file mode 100644 index 0000000..074531d --- /dev/null +++ b/.serena/project.yml @@ -0,0 +1,169 @@ +# the name by which the project can be referenced within Serena/when chatting with the LLM. +project_name: "mcp-93183b63-07c9-40e4-9151-3ef6cb5670ae" + +# list of language servers to start when using the LSP backend; choose from: +# ada al angular ansible bash +# bsl clojure cpp cpp_ccls crystal +# csharp csharp_omnisharp cue dart deno +# elixir elm erlang fortran fsharp +# gdscript gleam go groovy haskell +# haxe hlsl html java json +# julia kotlin latex lean4 lua +# luau markdown matlab msl nextflow +# nix ocaml pascal perl php +# php_phpactor php_phpantom powershell python python_basedpyright +# python_jedi python_pyrefly python_ty qml r +# rego ruby ruby_solargraph rust scala +# scss solidity svelte swift systemverilog +# terraform toml typescript typescript_vts vue +# wolfram yaml zig +# (This list may be outdated; generated with scripts/print_language_list.py; +# For the current list, see values of the LanguageServerId enum here: +# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py) +# For some languages, there are several alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.) +# Note: +# - For C, use cpp +# - For JavaScript, use typescript +# - For Angular projects, use angular (subsumes typescript+html; requires `npm install` in the project root) +# - For Svelte projects, use svelte (subsumes typescript/javascript for .svelte projects; requires npm) +# - For Deno projects, use deno (serves the same .ts/.js files as typescript; requires the deno CLI on PATH) +# - For SCSS / Sass / plain CSS, use scss (some-sass-language-server handles all three) +# - For Free Pascal/Lazarus, use pascal +# Special requirements: +# Some language servers require additional setup/installations. +# See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers +# When using multiple language servers, the first language server that supports a given file will be used for that file. +# The first language server is the default language and the respective language server will be used as a fallback. +# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored. +language_servers: +- typescript + +# the encoding used by text files in the project +# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings +encoding: "utf-8" + +# optional shell command to run before the language backend (LSP or JetBrains) is initialised. +# the command runs in the project root directory and is only executed if the project is trusted +# (see trusted_project_path_patterns in the global configuration). +# serena waits for the command to exit: a non-zero exit code is logged as an error but does not +# abort activation. a per-project timeout (activation_command_timeout, default 180s) is the safety +# backstop for non-terminating commands; on expiry the process is killed and activation continues. +# example: activation_command: "npx nx run-many -t build" +activation_command: + +# maximum time in seconds to wait for activation_command to complete before killing it (default 180s). +# must be a positive number. +activation_command_timeout: 180.0 + +# line ending convention to use when writing source files. +# Possible values: unset (use global setting), "lf", "crlf", or "native" (platform default) +# This does not affect Serena's own files (e.g. memories and configuration files), which always use native line endings. +line_ending: + +# The language backend to use for this project. +# If not set, the global setting from serena_config.yml is used. +# Valid values: LSP, JetBrains +# Note: the backend is fixed at startup. If a project with a different backend +# is activated post-init, an error will be returned. +language_backend: + +# whether to use project's .gitignore files to ignore files +ignore_all_files_in_gitignore: true + +# advanced configuration option allowing to configure language server-specific options. +# Maps the language key to the options. +# The settings are considered only if the project is trusted (see global configuration to define trusted projects). +# See https://oraios.github.io/serena/02-usage/050_configuration.html#language-server-specific-settings +ls_specific_settings: {} + +# list of workspace folder paths (LSP backend only). +# These folders will be used to build up Serena's symbol index. +# Paths must be within the project root and should thus be relative to the project root. +# Furthermore, the paths should not be filtered by ignore settings. +# Default setting: The entire project root folder (".") is considered. +# In (large) monorepos, this can be used to index only subfolders of the project root, e.g. +# ls_workspace_folders: +# - "./subproject1" +# - "./subproject2" +ls_workspace_folders: +- "." + +# list of additional workspace folder paths for cross-package reference support. +# Paths can be absolute or relative to the project root. +# Each folder is registered as an LSP workspace folder, enabling language servers to discover +# symbols and references across package boundaries, but these folders are not indexed by Serena, +# i.e. the respective symbols will not be found using Serena's symbol search tools. +# Example: +# additional_workspace_folders: +# - ../sibling-package +# - ../shared-lib +ls_additional_workspace_folders: [] + +# list of additional paths to ignore in this project. +# Same syntax as gitignore, so you can use * and **. +# Important: quote patterns that start with `*`, otherwise YAML treats them as aliases. +# Example: +# ignored_paths: +# - "examples/**" +# - ".worktrees/**" +# - "**/bin/**" +# - "**/obj/**" +# Note: global ignored_paths from serena_config.yml are also applied additively. +ignored_paths: [] + +# whether the project is in read-only mode +# If set to true, all editing tools will be disabled and attempts to use them will result in an error +# Added on 2025-04-18 +read_only: false + +# list of tool names to exclude. +# This extends the existing exclusions (e.g. from the global configuration) +# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html +excluded_tools: [] + +# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default). +# This extends the existing inclusions (e.g. from the global configuration). +# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html +included_optional_tools: [] + +# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools. +# This cannot be combined with non-empty excluded_tools or included_optional_tools. +# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html +fixed_tools: [] + +# list of mode names that are to be activated by default, overriding the setting in the global configuration. +# The full set of modes to be activated is base_modes (from global config) + default_modes + added_modes. +# If the setting is undefined/empty, the default_modes from the global configuration (serena_config.yml) apply. +# Otherwise, this overrides the setting from the global configuration (serena_config.yml). +# Therefore, you can set this to [] if you do not want the default modes defined in the global config to apply +# for this project. +# This setting can, in turn, be overridden by CLI parameters (--mode). +# See https://oraios.github.io/serena/02-usage/050_configuration.html#modes +default_modes: + +# list of mode names to be activated additionally for this project, e.g. ["query-projects"] +# The full set of modes to be activated is base_modes (from global config) + default_modes + added_modes. +# See https://oraios.github.io/serena/02-usage/050_configuration.html#modes +added_modes: + +# initial prompt for the project. It will always be given to the LLM upon activating the project +# (contrary to the memories, which are loaded on demand). +initial_prompt: "" + +# time budget (seconds) per tool call for the retrieval of additional symbol information +# such as docstrings or parameter information. +# This overrides the corresponding setting in the global configuration; see the documentation there. +# If null or missing, use the setting from the global configuration. +symbol_info_budget: + +# list of regex patterns which, when matched, mark a memory entry as read‑only. +# Extends the list from the global configuration, merging the two lists. +read_only_memory_patterns: [] + +# list of regex patterns for memories to completely ignore. +# Matching memories will not appear in list_memories or activate_project output +# and cannot be accessed via read_memory or write_memory. +# To access ignored memory files, use the read_file tool on the raw file path. +# Extends the list from the global configuration, merging the two lists. +# Example: ["_archive/.*", "_episodes/.*"] +ignored_memory_patterns: [] diff --git a/app/api/questions/latest/route.ts b/app/api/questions/latest/route.ts new file mode 100644 index 0000000..339f767 --- /dev/null +++ b/app/api/questions/latest/route.ts @@ -0,0 +1,17 @@ +import { NextResponse } from "next/server"; +import { getLatestQuestions } from "@/lib/questions"; + +export const dynamic = "force-dynamic"; + +export async function GET() { + try { + const questions = await getLatestQuestions(3); + return NextResponse.json({ questions }); + } catch (error) { + console.error("Failed to fetch latest questions", error); + return NextResponse.json( + { error: "Unable to load latest questions." }, + { status: 500 }, + ); + } +} \ No newline at end of file diff --git a/app/onboarding/page.tsx b/app/onboarding/page.tsx new file mode 100644 index 0000000..3274af0 --- /dev/null +++ b/app/onboarding/page.tsx @@ -0,0 +1,10 @@ +import type { Metadata } from "next"; +import OnboardingView from "@/components/onboarding/onboarding-view"; + +export const metadata: Metadata = { + title: "Onboarding", +}; + +export default function OnboardingPage() { + return ; +} \ No newline at end of file diff --git a/components/auth-provider.tsx b/components/auth-provider.tsx index cf9f211..04769eb 100644 --- a/components/auth-provider.tsx +++ b/components/auth-provider.tsx @@ -15,67 +15,78 @@ type AuthContextType = { const AuthContext = React.createContext(undefined); export function AuthProvider({ children }: { children: React.ReactNode }) { - const [isAuthenticated, setIsAuthenticated] = React.useState(false); - const [isAdmin, setIsAdmin] = React.useState(false); - const [password, setPassword] = React.useState(null); + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); const router = useRouter(); + const pathname = usePathname(); - React.useEffect(() => { - const auth = sessionStorage.getItem("auth"); - if (!auth) return; - - try { - const { isAdmin, password } = JSON.parse(auth); - if (isAdmin && password) { - setIsAuthenticated(true); - setIsAdmin(true); - setPassword(password); - } else { - sessionStorage.removeItem("auth"); + useEffect(() => { + const storedUser = localStorage.getItem("user"); + if (storedUser) { + try { + setUser(JSON.parse(storedUser)); + } catch { + localStorage.removeItem("user"); } - } catch { - sessionStorage.removeItem("auth"); } + setLoading(false); }, []); - const login = (inputPassword: string) => { - const adminPassword = process.env.NEXT_PUBLIC_ADMIN_PASSWORD || "123456"; - - if (inputPassword === adminPassword) { - setIsAuthenticated(true); - setIsAdmin(true); - setPassword(inputPassword); - sessionStorage.setItem( - "auth", - JSON.stringify({ isAdmin: true, password: inputPassword }), - ); - router.push("/admin"); - return true; + useEffect(() => { + if (loading) return; + + const isLogin = pathname === "/login" || pathname.startsWith("/login/"); + const isOnboarding = pathname === "/onboarding" || pathname.startsWith("/onboarding/"); + const isPublic = isLogin || isOnboarding; + + if (!user && !isPublic) { + const next = pathname && pathname !== "/" ? `?next=${encodeURIComponent(pathname)}` : ""; + router.replace(`/onboarding${next}`); + return; + } + + if (user && (isLogin || isOnboarding)) { + router.replace("/"); } + }, [user, loading, pathname, router]); + + const login = (emailOrUser: any, password?: string): boolean => { + let email = ""; + let name = ""; + + if (typeof emailOrUser === "string") { + email = emailOrUser.trim(); + name = email.split("@")[0] || "User"; + } else if (emailOrUser && typeof emailOrUser === "object") { + email = emailOrUser.email?.trim?.() ?? ""; + name = emailOrUser.name ?? (email ? email.split("@")[0] : "User"); + } + + if (!email) return false; + + const nextUser = + emailOrUser && typeof emailOrUser === "object" + ? { ...emailOrUser, email, name } + : { email, name }; + + localStorage.setItem("user", JSON.stringify(nextUser)); + setUser(nextUser); + + const searchParams = new URLSearchParams(window.location.search); + const next = searchParams.get("next"); + router.push(next || "/"); - toast({ - title: "Invalid admin password", - variant: "destructive", - }); - setIsAuthenticated(false); - setIsAdmin(false); - setPassword(null); - sessionStorage.removeItem("auth"); - return false; + return true; }; const logout = () => { - setIsAuthenticated(false); - setIsAdmin(false); - setPassword(null); - sessionStorage.removeItem("auth"); - router.push("/"); + localStorage.removeItem("user"); + setUser(null); + router.push("/onboarding"); }; return ( - + {children} ); diff --git a/components/onboarding/latest-questions.tsx b/components/onboarding/latest-questions.tsx new file mode 100644 index 0000000..abdef7f --- /dev/null +++ b/components/onboarding/latest-questions.tsx @@ -0,0 +1,123 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { Badge } from "@/components/ui/badge"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Skeleton } from "@/components/ui/skeleton"; + +interface LatestQuestion { + _id: string; + question?: string; + text?: string; + title?: string; + answer?: string; + examType?: string; + category?: string; + createdAt?: string; +} + +function formatDate(value?: string): string | null { + if (!value) return null; + const date = new Date(value); + if (Number.isNaN(date.getTime())) return null; + return new Intl.DateTimeFormat("en-US", { dateStyle: "medium" }).format(date); +} + +export function LatestQuestions() { + const [questions, setQuestions] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + let active = true; + + async function loadLatestQuestions() { + try { + const response = await fetch("/api/questions/latest", { cache: "no-store" }); + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error ?? "Failed to load the latest questions."); + } + + if (active) { + setQuestions(Array.isArray(data.questions) ? data.questions : []); + setError(null); + } + } catch (err) { + if (active) { + setError(err instanceof Error ? err.message : "Something went wrong."); + } + } finally { + if (active) setLoading(false); + } + } + + loadLatestQuestions(); + + return () => { + active = false; + }; + }, []); + + if (loading) { + return ( +
+ {[0, 1, 2].map((item) => ( + + + + + + + + ))} +
+ ); + } + + if (error) { + return ( + + {error} + + ); + } + + if (questions.length === 0) { + return ( + + + No questions have been published yet. + + + ); + } + + return ( +
+ {questions.map((question) => { + const title = question.question ?? question.text ?? question.title ?? "Untitled question"; + const examType = question.examType ?? question.category; + const formattedDate = formatDate(question.createdAt); + + return ( + + + {title} + {examType ? {examType} : null} + + +

+ {question.answer ?? "No answer available."} +

+ {formattedDate ? ( +

{formattedDate}

+ ) : null} +
+
+ ); + })} +
+ ); +} \ No newline at end of file diff --git a/components/onboarding/onboarding-view.tsx b/components/onboarding/onboarding-view.tsx new file mode 100644 index 0000000..fb9ce16 --- /dev/null +++ b/components/onboarding/onboarding-view.tsx @@ -0,0 +1,53 @@ +"use client"; + +import { useEffect } from "react"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { useAuth } from "@/components/auth-provider"; +import { LatestQuestions } from "@/components/onboarding/latest-questions"; +import { Button } from "@/components/ui/button"; + +export default function OnboardingView() { + const router = useRouter(); + const auth = useAuth(); + const user = auth.user; + const loading = (auth as { loading?: boolean }).loading; + + useEffect(() => { + if (!loading && user) { + router.replace("/"); + } + }, [loading, user, router]); + + return ( +
+
+

+ Welcome to the AWS Q&A App! +

+

+ Explore real-world AWS questions and answers, prepare for certifications, + and keep your knowledge sharp with the latest community questions. +

+
+ + +
+
+ +
+

Latest questions

+

+ Here are the three most recent questions from the community. +

+
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/lib/questions.ts b/lib/questions.ts new file mode 100644 index 0000000..9146a35 --- /dev/null +++ b/lib/questions.ts @@ -0,0 +1,27 @@ +import { connectToDatabase } from "@/lib/mongodb"; + +export interface PublicQuestion { + _id: string; + question?: string; + text?: string; + title?: string; + answer?: string; + examType?: string; + category?: string; + createdAt?: string | Date; +} + +export async function getLatestQuestions(limit = 3): Promise { + const db = await connectToDatabase(); + const questions = await db + .collection("questions") + .find({ approved: { $ne: false } }) + .sort({ createdAt: -1 }) + .limit(limit) + .toArray(); + + return questions.map(({ _id, ...question }) => ({ + ...question, + _id: _id.toString(), + })) as PublicQuestion[]; +} \ No newline at end of file