diff --git a/e2e/login.spec.ts b/e2e/login.spec.ts new file mode 100644 index 0000000..3be2f7c --- /dev/null +++ b/e2e/login.spec.ts @@ -0,0 +1,27 @@ +import { expect, test } from "@playwright/test"; + +/** + * Smoke test: the app loads, serves the login page, and the main form + * controls are present and reachable. We deliberately avoid hitting the + * real backend — this is a lightweight sanity check that catches broken + * routing / bundle / asset problems before they reach production. + */ +test("login page renders the sign-in form", async ({ page }) => { + await page.goto("/login"); + await expect( + page.getByRole("heading", { name: /sign in|login|welcome/i }), + ).toBeVisible({ timeout: 10_000 }); + + const email = page.getByLabel(/email/i); + const password = page.getByLabel(/password/i); + await expect(email).toBeVisible(); + await expect(password).toBeVisible(); + + await expect(page.getByRole("button", { name: /sign in|log in/i })).toBeVisible(); +}); + +test("root redirects to login for anonymous users", async ({ page }) => { + const response = await page.goto("/"); + expect(response?.ok()).toBeTruthy(); + await page.waitForURL(/\/login/i, { timeout: 10_000 }); +}); diff --git a/package-lock.json b/package-lock.json index f5e02ef..497f4fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,6 +42,8 @@ "cmdk": "^1.1.1", "date-fns": "^3.6.0", "embla-carousel-react": "^8.6.0", + "i18next": "^26.0.6", + "i18next-browser-languagedetector": "^8.2.1", "input-otp": "^1.4.2", "lucide-react": "^0.462.0", "next-themes": "^0.3.0", @@ -49,6 +51,7 @@ "react-day-picker": "^8.10.1", "react-dom": "^18.3.1", "react-hook-form": "^7.61.1", + "react-i18next": "^17.0.4", "react-resizable-panels": "^2.1.9", "react-router-dom": "^6.30.1", "recharts": "^2.15.4", @@ -60,6 +63,7 @@ }, "devDependencies": { "@eslint/js": "^9.32.0", + "@playwright/test": "^1.59.1", "@tailwindcss/typography": "^0.5.16", "@testing-library/jest-dom": "^6.6.0", "@testing-library/react": "^16.0.0", @@ -905,6 +909,22 @@ "node": ">= 8" } }, + "node_modules/@playwright/test": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz", + "integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@radix-ui/number": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", @@ -5480,6 +5500,15 @@ "node": ">=12" } }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, "node_modules/http-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", @@ -5509,6 +5538,46 @@ "node": ">= 6" } }, + "node_modules/i18next": { + "version": "26.0.6", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.0.6.tgz", + "integrity": "sha512-A4U6eCXodIbrhf8EarRurB9/4ebyaurH4+fu4gig9bqxmpSt+fCAFm/GpRQDcN1Xzu/LdFCx4nYHsnM1edIIbg==", + "funding": [ + { + "type": "individual", + "url": "https://www.locize.com/i18next" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + }, + { + "type": "individual", + "url": "https://www.locize.com" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2" + }, + "peerDependencies": { + "typescript": "^5 || ^6" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/i18next-browser-languagedetector": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.1.tgz", + "integrity": "sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -6215,6 +6284,53 @@ "node": ">= 6" } }, + "node_modules/playwright": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", + "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", + "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/postcss": { "version": "8.5.9", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz", @@ -6547,6 +6663,33 @@ "react": "^16.8.0 || ^17 || ^18 || ^19" } }, + "node_modules/react-i18next": { + "version": "17.0.4", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.4.tgz", + "integrity": "sha512-hQipmK4EF0y6RO6tt6WuqnmWpWYEXmQUUzecmMBuNsIgYd3smXcG4GtYPWhvgxn0pqMOItKlEO8H24HCs5hc3g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "html-parse-stringify": "^3.0.1", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "i18next": ">= 26.0.1", + "react": ">= 16.8.0", + "typescript": "^5 || ^6" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -7369,7 +7512,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -8164,6 +8307,15 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/w3c-xmlserializer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", diff --git a/package.json b/package.json index 04b2365..230daf3 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,9 @@ "lint": "eslint .", "preview": "vite preview", "test": "vitest run", - "test:watch": "vitest" + "test:watch": "vitest", + "test:e2e": "playwright test", + "test:e2e:install": "playwright install --with-deps chromium" }, "dependencies": { "@hookform/resolvers": "^3.10.0", @@ -47,6 +49,8 @@ "cmdk": "^1.1.1", "date-fns": "^3.6.0", "embla-carousel-react": "^8.6.0", + "i18next": "^26.0.6", + "i18next-browser-languagedetector": "^8.2.1", "input-otp": "^1.4.2", "lucide-react": "^0.462.0", "next-themes": "^0.3.0", @@ -54,6 +58,7 @@ "react-day-picker": "^8.10.1", "react-dom": "^18.3.1", "react-hook-form": "^7.61.1", + "react-i18next": "^17.0.4", "react-resizable-panels": "^2.1.9", "react-router-dom": "^6.30.1", "recharts": "^2.15.4", @@ -65,9 +70,10 @@ }, "devDependencies": { "@eslint/js": "^9.32.0", + "@playwright/test": "^1.59.1", + "@tailwindcss/typography": "^0.5.16", "@testing-library/jest-dom": "^6.6.0", "@testing-library/react": "^16.0.0", - "@tailwindcss/typography": "^0.5.16", "@types/node": "^22.16.5", "@types/react": "^18.3.23", "@types/react-dom": "^18.3.7", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..66b3b3a --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,42 @@ +import { defineConfig, devices } from "@playwright/test"; + +/** + * Playwright configuration for EnCoach frontend smoke tests. + * + * By default we run against a Vite preview server on localhost:4173. In CI + * this is started by the `playwright` step in .github/workflows/ci.yml after + * `npm run build`. + * + * Override the base URL with `PLAYWRIGHT_BASE_URL=https://... npx playwright + * test` to run against a deployed environment. + */ +const baseURL = process.env.PLAYWRIGHT_BASE_URL || "http://localhost:4173"; + +export default defineConfig({ + testDir: "./e2e", + timeout: 30_000, + expect: { timeout: 5_000 }, + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + reporter: process.env.CI ? [["github"], ["list"]] : [["list"]], + use: { + baseURL, + trace: "on-first-retry", + screenshot: "only-on-failure", + }, + webServer: process.env.PLAYWRIGHT_SKIP_WEBSERVER + ? undefined + : { + command: "npm run preview -- --host 127.0.0.1 --port 4173", + url: baseURL, + reuseExistingServer: !process.env.CI, + timeout: 60_000, + }, + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + ], +}); diff --git a/public/logo.svg b/public/logo.svg new file mode 100644 index 0000000..d832f1d --- /dev/null +++ b/public/logo.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/App.tsx b/src/App.tsx index 70e3c3d..148daa8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,3 +1,5 @@ +import { lazy, Suspense } from "react"; +import { ThemeProvider } from "next-themes"; import { Toaster } from "@/components/ui/toaster"; import { Toaster as Sonner } from "@/components/ui/sonner"; import { TooltipProvider } from "@/components/ui/tooltip"; @@ -8,146 +10,166 @@ import ProtectedRoute from "@/components/ProtectedRoute"; import StudentLayout from "@/components/StudentLayout"; import TeacherLayout from "@/components/TeacherLayout"; import AdminLmsLayout from "@/components/AdminLmsLayout"; -import Login from "@/pages/Login"; -import Register from "@/pages/Register"; -import EmailVerification from "@/pages/EmailVerification"; -import OnboardingWizard from "@/pages/OnboardingWizard"; -import ForgotPassword from "@/pages/ForgotPassword"; -import ResetPassword from "@/pages/ResetPassword"; -import ScoreVerification from "@/pages/ScoreVerification"; -// Original platform pages -import AdminDashboard from "@/pages/AdminDashboard"; -import UsersPage from "@/pages/UsersPage"; -import EntitiesPage from "@/pages/EntitiesPage"; -import AssignmentsPage from "@/pages/AssignmentsPage"; -import ExamsListPage from "@/pages/ExamsListPage"; -import ExamStructuresPage from "@/pages/ExamStructuresPage"; -import RubricsPage from "@/pages/RubricsPage"; -import GenerationPage from "@/pages/GenerationPage"; -import ApprovalWorkflowsPage from "@/pages/ApprovalWorkflowsPage"; -import ClassroomsPage from "@/pages/ClassroomsPage"; -import StudentPerformancePage from "@/pages/StudentPerformancePage"; -import StatsCorporatePage from "@/pages/StatsCorporatePage"; -import RecordPage from "@/pages/RecordPage"; -import VocabularyPage from "@/pages/VocabularyPage"; -import GrammarPage from "@/pages/GrammarPage"; -import PaymentRecordPage from "@/pages/PaymentRecordPage"; -import TicketsPage from "@/pages/TicketsPage"; -import SettingsPage from "@/pages/SettingsPage"; -import ProfilePage from "@/pages/ProfilePage"; -import ExamPage from "@/pages/ExamPage"; -// Student pages -import StudentDashboard from "@/pages/student/StudentDashboard"; -import StudentCourses from "@/pages/student/StudentCourses"; -import StudentCourseDetail from "@/pages/student/StudentCourseDetail"; -import StudentAssignments from "@/pages/student/StudentAssignments"; -import StudentGrades from "@/pages/student/StudentGrades"; -import StudentAttendance from "@/pages/student/StudentAttendance"; -import StudentTimetable from "@/pages/student/StudentTimetable"; -import StudentProfile from "@/pages/student/StudentProfile"; -// Adaptive learning pages -import SubjectSelection from "@/pages/student/SubjectSelection"; -import DiagnosticTest from "@/pages/student/DiagnosticTest"; -import ProficiencyProfile from "@/pages/student/ProficiencyProfile"; -import LearningPlanPage from "@/pages/student/LearningPlan"; -import TopicLearning from "@/pages/student/TopicLearning"; -// Teacher pages -import TeacherDashboard from "@/pages/teacher/TeacherDashboard"; -import TeacherCourses from "@/pages/teacher/TeacherCourses"; -import CourseBuilder from "@/pages/teacher/CourseBuilder"; -import TeacherAssignments from "@/pages/teacher/TeacherAssignments"; -import TeacherAssignmentDetail from "@/pages/teacher/TeacherAssignmentDetail"; -import TeacherAttendance from "@/pages/teacher/TeacherAttendance"; -import TeacherStudents from "@/pages/teacher/TeacherStudents"; -import TeacherTimetable from "@/pages/teacher/TeacherTimetable"; -import TeacherProfile from "@/pages/teacher/TeacherProfile"; -import TeacherLibrary from "@/pages/teacher/TeacherLibrary"; -import AdaptiveSettings from "@/pages/teacher/AdaptiveSettings"; -// Admin LMS pages -import AdminLmsDashboard from "@/pages/admin/AdminLmsDashboard"; -import AdminCourses from "@/pages/admin/AdminCourses"; -import AdminStudents from "@/pages/admin/AdminStudents"; -import AdminTeachers from "@/pages/admin/AdminTeachers"; -import AdminBatches from "@/pages/admin/AdminBatches"; -import AdminBatchDetail from "@/pages/admin/AdminBatchDetail"; -import AdminTimetable from "@/pages/admin/AdminTimetable"; -import AdminReports from "@/pages/admin/AdminReports"; -import AdminSettings from "@/pages/admin/AdminSettings"; -import AdminProfileLms from "@/pages/admin/AdminProfile"; -import TaxonomyManager from "@/pages/admin/TaxonomyManager"; -import ResourceManager from "@/pages/admin/ResourceManager"; -import AcademicYearManager from "@/pages/admin/AcademicYearManager"; -import DepartmentManager from "@/pages/admin/DepartmentManager"; -import AdmissionList from "@/pages/admin/AdmissionList"; -import AdmissionDetail from "@/pages/admin/AdmissionDetail"; -import AdmissionRegisterPage from "@/pages/admin/AdmissionRegisterPage"; -import InstitutionalExamSessions from "@/pages/admin/InstitutionalExamSessions"; -import MarksheetManager from "@/pages/admin/MarksheetManager"; -import AdminStudentLeave from "@/pages/admin/AdminStudentLeave"; -import AdminFees from "@/pages/admin/AdminFees"; -import AdminLessons from "@/pages/admin/AdminLessons"; -import AdminGradebook from "@/pages/admin/AdminGradebook"; -import AdminStudentProgress from "@/pages/admin/AdminStudentProgress"; -import AdminLibrary from "@/pages/admin/AdminLibrary"; -import AdminActivities from "@/pages/admin/AdminActivities"; -import AdminFacilities from "@/pages/admin/AdminFacilities"; -import AdmissionApplication from "@/pages/AdmissionApplication"; -import SubjectRegistrationPage from "@/pages/student/SubjectRegistrationPage"; -// Courseware pages -import CourseChapters from "@/pages/teacher/CourseChapters"; -import ChapterDetail from "@/pages/teacher/ChapterDetail"; -import AiWorkbench from "@/pages/teacher/AiWorkbench"; -import TeacherDiscussionBoard from "@/pages/teacher/TeacherDiscussionBoard"; -import TeacherAnnouncements from "@/pages/teacher/TeacherAnnouncements"; -import StudentChapterView from "@/pages/student/StudentChapterView"; -import StudentDiscussionBoard from "@/pages/student/StudentDiscussionBoard"; -import StudentAnnouncements from "@/pages/student/StudentAnnouncements"; -import StudentMessages from "@/pages/student/StudentMessages"; -import StudentJourney from "@/pages/student/StudentJourney"; -import AiEnglishCourse from "@/pages/student/AiEnglishCourse"; -import AiIeltsCourse from "@/pages/student/AiIeltsCourse"; -import ExamSession from "@/pages/student/ExamSession"; -import ExamStatus from "@/pages/student/ExamStatus"; -import ExamResults from "@/pages/student/ExamResults"; -import GapAnalysis from "@/pages/student/GapAnalysis"; -import CourseDelivery from "@/pages/student/CourseDelivery"; -import GradingQueue from "@/pages/admin/GradingQueue"; -import CourseConfig from "@/pages/admin/CourseConfig"; -import ModuleBuilder from "@/pages/admin/ModuleBuilder"; -import CourseProgress from "@/pages/teacher/CourseProgress"; -import PlacementBriefing from "@/pages/student/PlacementBriefing"; -import PlacementTest from "@/pages/student/PlacementTest"; -import PlacementResults from "@/pages/student/PlacementResults"; -import PlacementAccess from "@/pages/student/PlacementAccess"; -import FaqManager from "@/pages/admin/FaqManager"; -import NotificationRules from "@/pages/admin/NotificationRules"; -import ApprovalWorkflowConfig from "@/pages/admin/ApprovalWorkflowConfig"; -import RolesPermissions from "@/pages/admin/RolesPermissions"; -import AuthorityMatrix from "@/pages/admin/AuthorityMatrix"; -import UserRoles from "@/pages/admin/UserRoles"; -import BulkStudentUpload from "@/pages/admin/BulkStudentUpload"; -import CredentialDashboard from "@/pages/admin/CredentialDashboard"; -import AiEnglishQuality from "@/pages/admin/AiEnglishQuality"; -import AiEnglishTaxonomy from "@/pages/admin/AiEnglishTaxonomy"; -import AiIeltsValidation from "@/pages/admin/AiIeltsValidation"; -import AdaptiveDashboard from "@/pages/admin/AdaptiveDashboard"; -import AdaptiveStudentDetail from "@/pages/admin/AdaptiveStudentDetail"; -import LevelMappingConfig from "@/pages/admin/LevelMappingConfig"; -import WhiteLabelBranding from "@/pages/admin/WhiteLabelBranding"; -import ScoreApprovalQueue from "@/pages/admin/ScoreApprovalQueue"; -import ExamTemplateSelection from "@/pages/admin/ExamTemplateSelection"; -import IeltsExamCreate from "@/pages/admin/IeltsExamCreate"; -import IeltsSkillConfig from "@/pages/admin/IeltsSkillConfig"; -import IeltsContentPool from "@/pages/admin/IeltsContentPool"; -import IeltsExamValidation from "@/pages/admin/IeltsExamValidation"; -import CustomExamCreate from "@/pages/admin/CustomExamCreate"; -import OfficialExamAccess from "@/pages/OfficialExamAccess"; -import FaqPage from "@/pages/FaqPage"; -import NotFound from "@/pages/NotFound"; import { queryClient } from "@/lib/query-client"; import { Button } from "@/components/ui/button"; import { ErrorBoundary } from "@/components/ErrorBoundary"; +// ----------------------------------------------------------------------------- +// Lazy-loaded route pages +// ----------------------------------------------------------------------------- +// Keeping the initial bundle small matters on slow networks / modest hardware +// (many teachers/students open the app from school Wi-Fi). Each page below is +// split into its own chunk and only fetched when the user hits the matching +// route. The shell (layouts, providers, router) is still eagerly imported so +// the first paint stays snappy. + +// Auth +const Login = lazy(() => import("@/pages/Login")); +const Register = lazy(() => import("@/pages/Register")); +const EmailVerification = lazy(() => import("@/pages/EmailVerification")); +const OnboardingWizard = lazy(() => import("@/pages/OnboardingWizard")); +const ForgotPassword = lazy(() => import("@/pages/ForgotPassword")); +const ResetPassword = lazy(() => import("@/pages/ResetPassword")); +const ScoreVerification = lazy(() => import("@/pages/ScoreVerification")); + +// Original platform pages +const AdminDashboard = lazy(() => import("@/pages/AdminDashboard")); +const UsersPage = lazy(() => import("@/pages/UsersPage")); +const EntitiesPage = lazy(() => import("@/pages/EntitiesPage")); +const AssignmentsPage = lazy(() => import("@/pages/AssignmentsPage")); +const ExamsListPage = lazy(() => import("@/pages/ExamsListPage")); +const ExamStructuresPage = lazy(() => import("@/pages/ExamStructuresPage")); +const RubricsPage = lazy(() => import("@/pages/RubricsPage")); +const GenerationPage = lazy(() => import("@/pages/GenerationPage")); +const ApprovalWorkflowsPage = lazy(() => import("@/pages/ApprovalWorkflowsPage")); +const ClassroomsPage = lazy(() => import("@/pages/ClassroomsPage")); +const StudentPerformancePage = lazy(() => import("@/pages/StudentPerformancePage")); +const StatsCorporatePage = lazy(() => import("@/pages/StatsCorporatePage")); +const RecordPage = lazy(() => import("@/pages/RecordPage")); +const VocabularyPage = lazy(() => import("@/pages/VocabularyPage")); +const GrammarPage = lazy(() => import("@/pages/GrammarPage")); +const PaymentRecordPage = lazy(() => import("@/pages/PaymentRecordPage")); +const TicketsPage = lazy(() => import("@/pages/TicketsPage")); +const SettingsPage = lazy(() => import("@/pages/SettingsPage")); + +// Student pages +const StudentDashboard = lazy(() => import("@/pages/student/StudentDashboard")); +const StudentCourses = lazy(() => import("@/pages/student/StudentCourses")); +const StudentCourseDetail = lazy(() => import("@/pages/student/StudentCourseDetail")); +const StudentAssignments = lazy(() => import("@/pages/student/StudentAssignments")); +const StudentGrades = lazy(() => import("@/pages/student/StudentGrades")); +const StudentAttendance = lazy(() => import("@/pages/student/StudentAttendance")); +const StudentTimetable = lazy(() => import("@/pages/student/StudentTimetable")); +const StudentProfile = lazy(() => import("@/pages/student/StudentProfile")); + +// Adaptive learning pages +const SubjectSelection = lazy(() => import("@/pages/student/SubjectSelection")); +const DiagnosticTest = lazy(() => import("@/pages/student/DiagnosticTest")); +const ProficiencyProfile = lazy(() => import("@/pages/student/ProficiencyProfile")); +const LearningPlanPage = lazy(() => import("@/pages/student/LearningPlan")); +const TopicLearning = lazy(() => import("@/pages/student/TopicLearning")); + +// Teacher pages +const TeacherDashboard = lazy(() => import("@/pages/teacher/TeacherDashboard")); +const TeacherCourses = lazy(() => import("@/pages/teacher/TeacherCourses")); +const CourseBuilder = lazy(() => import("@/pages/teacher/CourseBuilder")); +const TeacherAssignments = lazy(() => import("@/pages/teacher/TeacherAssignments")); +const TeacherAssignmentDetail = lazy(() => import("@/pages/teacher/TeacherAssignmentDetail")); +const TeacherAttendance = lazy(() => import("@/pages/teacher/TeacherAttendance")); +const TeacherStudents = lazy(() => import("@/pages/teacher/TeacherStudents")); +const TeacherTimetable = lazy(() => import("@/pages/teacher/TeacherTimetable")); +const TeacherProfile = lazy(() => import("@/pages/teacher/TeacherProfile")); +const TeacherLibrary = lazy(() => import("@/pages/teacher/TeacherLibrary")); +const AdaptiveSettings = lazy(() => import("@/pages/teacher/AdaptiveSettings")); + +// Admin LMS pages +const AdminLmsDashboard = lazy(() => import("@/pages/admin/AdminLmsDashboard")); +const AdminCourses = lazy(() => import("@/pages/admin/AdminCourses")); +const AdminStudents = lazy(() => import("@/pages/admin/AdminStudents")); +const AdminTeachers = lazy(() => import("@/pages/admin/AdminTeachers")); +const AdminBatches = lazy(() => import("@/pages/admin/AdminBatches")); +const AdminBatchDetail = lazy(() => import("@/pages/admin/AdminBatchDetail")); +const AdminTimetable = lazy(() => import("@/pages/admin/AdminTimetable")); +const AdminReports = lazy(() => import("@/pages/admin/AdminReports")); +const AdminSettings = lazy(() => import("@/pages/admin/AdminSettings")); +const AdminProfileLms = lazy(() => import("@/pages/admin/AdminProfile")); +const TaxonomyManager = lazy(() => import("@/pages/admin/TaxonomyManager")); +const ResourceManager = lazy(() => import("@/pages/admin/ResourceManager")); +const AcademicYearManager = lazy(() => import("@/pages/admin/AcademicYearManager")); +const DepartmentManager = lazy(() => import("@/pages/admin/DepartmentManager")); +const AdmissionList = lazy(() => import("@/pages/admin/AdmissionList")); +const AdmissionDetail = lazy(() => import("@/pages/admin/AdmissionDetail")); +const AdmissionRegisterPage = lazy(() => import("@/pages/admin/AdmissionRegisterPage")); +const InstitutionalExamSessions = lazy(() => import("@/pages/admin/InstitutionalExamSessions")); +const MarksheetManager = lazy(() => import("@/pages/admin/MarksheetManager")); +const AdminStudentLeave = lazy(() => import("@/pages/admin/AdminStudentLeave")); +const AdminFees = lazy(() => import("@/pages/admin/AdminFees")); +const AdminLessons = lazy(() => import("@/pages/admin/AdminLessons")); +const AdminGradebook = lazy(() => import("@/pages/admin/AdminGradebook")); +const AdminStudentProgress = lazy(() => import("@/pages/admin/AdminStudentProgress")); +const AdminLibrary = lazy(() => import("@/pages/admin/AdminLibrary")); +const AdminActivities = lazy(() => import("@/pages/admin/AdminActivities")); +const AdminFacilities = lazy(() => import("@/pages/admin/AdminFacilities")); +const AdmissionApplication = lazy(() => import("@/pages/AdmissionApplication")); +const SubjectRegistrationPage = lazy(() => import("@/pages/student/SubjectRegistrationPage")); + +// Courseware pages +const CourseChapters = lazy(() => import("@/pages/teacher/CourseChapters")); +const ChapterDetail = lazy(() => import("@/pages/teacher/ChapterDetail")); +const AiWorkbench = lazy(() => import("@/pages/teacher/AiWorkbench")); +const TeacherDiscussionBoard = lazy(() => import("@/pages/teacher/TeacherDiscussionBoard")); +const TeacherAnnouncements = lazy(() => import("@/pages/teacher/TeacherAnnouncements")); +const StudentChapterView = lazy(() => import("@/pages/student/StudentChapterView")); +const StudentDiscussionBoard = lazy(() => import("@/pages/student/StudentDiscussionBoard")); +const StudentAnnouncements = lazy(() => import("@/pages/student/StudentAnnouncements")); +const StudentMessages = lazy(() => import("@/pages/student/StudentMessages")); +const StudentJourney = lazy(() => import("@/pages/student/StudentJourney")); +const AiEnglishCourse = lazy(() => import("@/pages/student/AiEnglishCourse")); +const AiIeltsCourse = lazy(() => import("@/pages/student/AiIeltsCourse")); +const ExamSession = lazy(() => import("@/pages/student/ExamSession")); +const ExamStatus = lazy(() => import("@/pages/student/ExamStatus")); +const ExamResults = lazy(() => import("@/pages/student/ExamResults")); +const GapAnalysis = lazy(() => import("@/pages/student/GapAnalysis")); +const CourseDelivery = lazy(() => import("@/pages/student/CourseDelivery")); +const GradingQueue = lazy(() => import("@/pages/admin/GradingQueue")); +const CourseConfig = lazy(() => import("@/pages/admin/CourseConfig")); +const ModuleBuilder = lazy(() => import("@/pages/admin/ModuleBuilder")); +const CourseProgress = lazy(() => import("@/pages/teacher/CourseProgress")); +const PlacementBriefing = lazy(() => import("@/pages/student/PlacementBriefing")); +const PlacementTest = lazy(() => import("@/pages/student/PlacementTest")); +const PlacementResults = lazy(() => import("@/pages/student/PlacementResults")); +const PlacementAccess = lazy(() => import("@/pages/student/PlacementAccess")); +const FaqManager = lazy(() => import("@/pages/admin/FaqManager")); +const NotificationRules = lazy(() => import("@/pages/admin/NotificationRules")); +const ApprovalWorkflowConfig = lazy(() => import("@/pages/admin/ApprovalWorkflowConfig")); +const RolesPermissions = lazy(() => import("@/pages/admin/RolesPermissions")); +const AuthorityMatrix = lazy(() => import("@/pages/admin/AuthorityMatrix")); +const UserRoles = lazy(() => import("@/pages/admin/UserRoles")); +const BulkStudentUpload = lazy(() => import("@/pages/admin/BulkStudentUpload")); +const CredentialDashboard = lazy(() => import("@/pages/admin/CredentialDashboard")); +const AiEnglishQuality = lazy(() => import("@/pages/admin/AiEnglishQuality")); +const AiEnglishTaxonomy = lazy(() => import("@/pages/admin/AiEnglishTaxonomy")); +const AiIeltsValidation = lazy(() => import("@/pages/admin/AiIeltsValidation")); +const AdaptiveDashboard = lazy(() => import("@/pages/admin/AdaptiveDashboard")); +const AdaptiveStudentDetail = lazy(() => import("@/pages/admin/AdaptiveStudentDetail")); +const LevelMappingConfig = lazy(() => import("@/pages/admin/LevelMappingConfig")); +const WhiteLabelBranding = lazy(() => import("@/pages/admin/WhiteLabelBranding")); +const ScoreApprovalQueue = lazy(() => import("@/pages/admin/ScoreApprovalQueue")); +const ExamTemplateSelection = lazy(() => import("@/pages/admin/ExamTemplateSelection")); +const IeltsExamCreate = lazy(() => import("@/pages/admin/IeltsExamCreate")); +const IeltsSkillConfig = lazy(() => import("@/pages/admin/IeltsSkillConfig")); +const IeltsContentPool = lazy(() => import("@/pages/admin/IeltsContentPool")); +const IeltsExamValidation = lazy(() => import("@/pages/admin/IeltsExamValidation")); +const CustomExamCreate = lazy(() => import("@/pages/admin/CustomExamCreate")); +const ExamReviewQueue = lazy(() => import("@/pages/admin/ExamReviewQueue")); +const ExamReviewDetail = lazy(() => import("@/pages/admin/ExamReviewDetail")); +const AIPromptEditor = lazy(() => import("@/pages/admin/AIPromptEditor")); +const AIFeedbackTriage = lazy(() => import("@/pages/admin/AIFeedbackTriage")); +const PrivacyCenter = lazy(() => import("@/pages/PrivacyCenter")); +const OfficialExamAccess = lazy(() => import("@/pages/OfficialExamAccess")); +const FaqPage = lazy(() => import("@/pages/FaqPage")); +const NotFound = lazy(() => import("@/pages/NotFound")); + function StudentSubscriptionPlaceholder() { const navigate = useNavigate(); return ( @@ -158,14 +180,30 @@ function StudentSubscriptionPlaceholder() { ); } +function RouteFallback() { + return ( +
+
+
+ ); +} + const App = () => ( + + }> {/* Auth routes */} } /> @@ -200,6 +238,7 @@ const App = () => ( } /> } /> } /> + } /> } /> } /> } /> @@ -241,6 +280,7 @@ const App = () => ( } /> } /> } /> + } /> } /> } /> @@ -263,6 +303,7 @@ const App = () => ( } /> } /> } /> + } /> {/* Original academic pages */} } /> } /> @@ -285,13 +326,16 @@ const App = () => ( } /> } /> } /> - } /> } /> } /> } /> } /> } /> } /> + } /> + } /> + } /> + } /> } /> } /> {/* Institutional LMS pages */} @@ -340,10 +384,12 @@ const App = () => ( } /> } /> + + ); diff --git a/src/components/AIFeedbackButtons.tsx b/src/components/AIFeedbackButtons.tsx new file mode 100644 index 0000000..b96e851 --- /dev/null +++ b/src/components/AIFeedbackButtons.tsx @@ -0,0 +1,164 @@ +import { useEffect, useState } from "react"; +import { ThumbsDown, ThumbsUp } from "lucide-react"; +import { toast } from "sonner"; + +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Textarea } from "@/components/ui/textarea"; +import { + useAIFeedbackSummary, + useSubmitAIFeedback, +} from "@/hooks/queries/useAIFeedback"; +import { cn } from "@/lib/utils"; +import type { + AIFeedbackRating, + AIFeedbackSubjectType, +} from "@/types/ai-feedback"; + +export interface AIFeedbackButtonsProps { + subjectType: AIFeedbackSubjectType; + subjectId: number; + promptKey?: string; + promptVersion?: number; + aiLogId?: number; + entityId?: number; + courseId?: number; + size?: "sm" | "md"; + className?: string; +} + +/** Thumbs up/down widget for any AI-generated artefact. + * + * Always renders the same UI regardless of whether the user has rated the + * artefact before — a previous rating will show as highlighted. Clicking the + * same button again is a no-op; clicking the opposite button updates the + * server-side row in place. A thumbs-down always prompts the user for a short + * comment (required by the backend). + */ +export function AIFeedbackButtons({ + subjectType, + subjectId, + promptKey, + promptVersion, + aiLogId, + entityId, + courseId, + size = "sm", + className, +}: AIFeedbackButtonsProps) { + const summary = useAIFeedbackSummary(subjectType, subjectId); + const submit = useSubmitAIFeedback(); + + const [downOpen, setDownOpen] = useState(false); + const [comment, setComment] = useState(""); + + useEffect(() => { + if (downOpen) { + setComment(summary.data?.my_comment ?? ""); + } + }, [downOpen, summary.data?.my_comment]); + + const myRating: AIFeedbackRating | null = summary.data?.my_rating ?? null; + + const submitUp = async () => { + if (myRating === "up") return; + try { + await submit.mutateAsync({ + subject_type: subjectType, + subject_id: subjectId, + rating: "up", + prompt_key: promptKey, + prompt_version: promptVersion, + ai_log_id: aiLogId, + entity_id: entityId, + course_id: courseId, + }); + } catch (err) { + toast.error(err instanceof Error ? err.message : "Failed to submit"); + } + }; + + const submitDown = async () => { + if (!comment.trim()) { + toast.error("Please tell us what was wrong."); + return; + } + try { + await submit.mutateAsync({ + subject_type: subjectType, + subject_id: subjectId, + rating: "down", + comment: comment.trim(), + prompt_key: promptKey, + prompt_version: promptVersion, + ai_log_id: aiLogId, + entity_id: entityId, + course_id: courseId, + }); + setDownOpen(false); + } catch (err) { + toast.error(err instanceof Error ? err.message : "Failed to submit"); + } + }; + + const btnSize = size === "sm" ? "sm" : "default"; + const iconCls = size === "sm" ? "h-3.5 w-3.5" : "h-4 w-4"; + + return ( +
+ + + + + + + What went wrong? + +