diff --git a/README.md b/README.md index 1a202df..21a305f 100644 --- a/README.md +++ b/README.md @@ -48,3 +48,26 @@ This repository contains the source code for the TechTorque 2025 customer and em npm run setup-hooks ``` This configures automatic linting on commit and build checking on push. See [GIT_HOOKS.md](GIT_HOOKS.md) for details. + + ### šŸ”Œ Environment + + - The frontend expects an API base to be available at runtime. You can configure this via the + `NEXT_PUBLIC_API_BASE_URL` environment variable. When not set, the runtime defaults to + `http://localhost:8080` (useful in development). + + - The AI chat widget reaches the AI chat proxy at `/api/v1/ai/chat` (or `{{NEXT_PUBLIC_API_BASE_URL}}/api/v1/ai/chat` when the public API base is set). This ensures the frontend talks to the configured API gateway or the local Next.js proxy. + +### šŸŽØ Theme & Styling (Centralized) + +- A single global stylesheet lives at `src/app/globals.css` and contains all theme variables for light & dark mode, plus utility classes like: + - `.theme-button-primary`, `.theme-button-secondary` — semantic buttons + - `.theme-bg-primary`, `.theme-text-primary`, `.theme-border`, etc — consistent building blocks + - `.automotive-accent`, `.accent-badge`, `.text-gradient-accent`, `.progress-accent` — accent utilities + +- Theme switching is implemented using `src/app/contexts/ThemeContext.tsx` plus a small pre-hydration script in `src/app/layout.tsx` (so the app applies the saved system preference or previously saved theme before React mounts to avoid flashes). + +- Quick checks added: + - `npm run check:theme` — verifies `globals.css` and `ThemeContext` contain expected hooks and variables. + - `npm run check:colors` — scans `src/` for hardcoded hex/rgb color usage (ignores `globals.css`). + +If you're adding new UI colors, add variables to `src/app/globals.css` and use the semantic utility classes (or create new ones) — this keeps light/dark behavior centralized and consistent across the app. diff --git a/package-lock.json b/package-lock.json index f14c5d8..2fa2a8b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "axios": "^1.12.2", "js-cookie": "^3.0.5", "lucide-react": "^0.553.0", - "next": "^16.0.1", + "next": "^16.0.3", "react": "19.1.0", "react-dom": "19.1.0", "sockjs-client": "^1.6.1" @@ -765,9 +765,9 @@ } }, "node_modules/@next/env": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.1.tgz", - "integrity": "sha512-LFvlK0TG2L3fEOX77OC35KowL8D7DlFF45C0OvKMC4hy8c/md1RC4UMNDlUGJqfCoCS2VWrZ4dSE6OjaX5+8mw==", + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.3.tgz", + "integrity": "sha512-IqgtY5Vwsm14mm/nmQaRMmywCU+yyMIYfk3/MHZ2ZTJvwVbBn3usZnjMi1GacrMVzVcAxJShTCpZlPs26EdEjQ==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { @@ -781,9 +781,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.0.1.tgz", - "integrity": "sha512-R0YxRp6/4W7yG1nKbfu41bp3d96a0EalonQXiMe+1H9GTHfKxGNCGFNWUho18avRBPsO8T3RmdWuzmfurlQPbg==", + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.0.3.tgz", + "integrity": "sha512-MOnbd92+OByu0p6QBAzq1ahVWzF6nyfiH07dQDez4/Nku7G249NjxDVyEfVhz8WkLiOEU+KFVnqtgcsfP2nLXg==", "cpu": [ "arm64" ], @@ -797,9 +797,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.0.1.tgz", - "integrity": "sha512-kETZBocRux3xITiZtOtVoVvXyQLB7VBxN7L6EPqgI5paZiUlnsgYv4q8diTNYeHmF9EiehydOBo20lTttCbHAg==", + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.0.3.tgz", + "integrity": "sha512-i70C4O1VmbTivYdRlk+5lj9xRc2BlK3oUikt3yJeHT1unL4LsNtN7UiOhVanFdc7vDAgZn1tV/9mQwMkWOJvHg==", "cpu": [ "x64" ], @@ -813,9 +813,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.0.1.tgz", - "integrity": "sha512-hWg3BtsxQuSKhfe0LunJoqxjO4NEpBmKkE+P2Sroos7yB//OOX3jD5ISP2wv8QdUwtRehMdwYz6VB50mY6hqAg==", + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.0.3.tgz", + "integrity": "sha512-O88gCZ95sScwD00mn/AtalyCoykhhlokxH/wi1huFK+rmiP5LAYVs/i2ruk7xST6SuXN4NI5y4Xf5vepb2jf6A==", "cpu": [ "arm64" ], @@ -829,9 +829,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.0.1.tgz", - "integrity": "sha512-UPnOvYg+fjAhP3b1iQStcYPWeBFRLrugEyK/lDKGk7kLNua8t5/DvDbAEFotfV1YfcOY6bru76qN9qnjLoyHCQ==", + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.0.3.tgz", + "integrity": "sha512-CEErFt78S/zYXzFIiv18iQCbRbLgBluS8z1TNDQoyPi8/Jr5qhR3e8XHAIxVxPBjDbEMITprqELVc5KTfFj0gg==", "cpu": [ "arm64" ], @@ -845,9 +845,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.0.1.tgz", - "integrity": "sha512-Et81SdWkcRqAJziIgFtsFyJizHoWne4fzJkvjd6V4wEkWTB4MX6J0uByUb0peiJQ4WeAt6GGmMszE5KrXK6WKg==", + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.0.3.tgz", + "integrity": "sha512-Tc3i+nwt6mQ+Dwzcri/WNDj56iWdycGVh5YwwklleClzPzz7UpfaMw1ci7bLl6GRYMXhWDBfe707EXNjKtiswQ==", "cpu": [ "x64" ], @@ -861,9 +861,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.0.1.tgz", - "integrity": "sha512-qBbgYEBRrC1egcG03FZaVfVxrJm8wBl7vr8UFKplnxNRprctdP26xEv9nJ07Ggq4y1adwa0nz2mz83CELY7N6Q==", + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.0.3.tgz", + "integrity": "sha512-zTh03Z/5PBBPdTurgEtr6nY0vI9KR9Ifp/jZCcHlODzwVOEKcKRBtQIGrkc7izFgOMuXDEJBmirwpGqdM/ZixA==", "cpu": [ "x64" ], @@ -877,9 +877,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.0.1.tgz", - "integrity": "sha512-cPuBjYP6I699/RdbHJonb3BiRNEDm5CKEBuJ6SD8k3oLam2fDRMKAvmrli4QMDgT2ixyRJ0+DTkiODbIQhRkeQ==", + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.0.3.tgz", + "integrity": "sha512-Jc1EHxtZovcJcg5zU43X3tuqzl/sS+CmLgjRP28ZT4vk869Ncm2NoF8qSTaL99gh6uOzgM99Shct06pSO6kA6g==", "cpu": [ "arm64" ], @@ -893,9 +893,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.0.1.tgz", - "integrity": "sha512-XeEUJsE4JYtfrXe/LaJn3z1pD19fK0Q6Er8Qoufi+HqvdO4LEPyCxLUt4rxA+4RfYo6S9gMlmzCMU2F+AatFqQ==", + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.0.3.tgz", + "integrity": "sha512-N7EJ6zbxgIYpI/sWNzpVKRMbfEGgsWuOIvzkML7wxAAZhPk1Msxuo/JDu1PKjWGrAoOLaZcIX5s+/pF5LIbBBg==", "cpu": [ "x64" ], @@ -4739,12 +4739,12 @@ "license": "MIT" }, "node_modules/next": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/next/-/next-16.0.1.tgz", - "integrity": "sha512-e9RLSssZwd35p7/vOa+hoDFggUZIUbZhIUSLZuETCwrCVvxOs87NamoUzT+vbcNAL8Ld9GobBnWOA6SbV/arOw==", + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/next/-/next-16.0.3.tgz", + "integrity": "sha512-Ka0/iNBblPFcIubTA1Jjh6gvwqfjrGq1Y2MTI5lbjeLIAfmC+p5bQmojpRZqgHHVu5cG4+qdIiwXiBSm/8lZ3w==", "license": "MIT", "dependencies": { - "@next/env": "16.0.1", + "@next/env": "16.0.3", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", @@ -4757,14 +4757,14 @@ "node": ">=20.9.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "16.0.1", - "@next/swc-darwin-x64": "16.0.1", - "@next/swc-linux-arm64-gnu": "16.0.1", - "@next/swc-linux-arm64-musl": "16.0.1", - "@next/swc-linux-x64-gnu": "16.0.1", - "@next/swc-linux-x64-musl": "16.0.1", - "@next/swc-win32-arm64-msvc": "16.0.1", - "@next/swc-win32-x64-msvc": "16.0.1", + "@next/swc-darwin-arm64": "16.0.3", + "@next/swc-darwin-x64": "16.0.3", + "@next/swc-linux-arm64-gnu": "16.0.3", + "@next/swc-linux-arm64-musl": "16.0.3", + "@next/swc-linux-x64-gnu": "16.0.3", + "@next/swc-linux-x64-musl": "16.0.3", + "@next/swc-win32-arm64-msvc": "16.0.3", + "@next/swc-win32-x64-msvc": "16.0.3", "sharp": "^0.34.4" }, "peerDependencies": { diff --git a/package.json b/package.json index 7a7262c..814355e 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,8 @@ "build": "next build --turbopack", "start": "next start", "lint": "eslint", + "check:colors": "node ./scripts/check-hardcoded-colors.js", + "check:theme": "node ./scripts/check-theme-setup.js", "setup-hooks": "chmod +x .githooks/pre-commit .githooks/pre-push && git config core.hooksPath .githooks && echo 'āœ… Git hooks configured successfully!'" }, "dependencies": { @@ -14,7 +16,7 @@ "axios": "^1.12.2", "js-cookie": "^3.0.5", "lucide-react": "^0.553.0", - "next": "^16.0.1", + "next": "^16.0.3", "react": "19.1.0", "react-dom": "19.1.0", "sockjs-client": "^1.6.1" diff --git a/scripts/check-hardcoded-colors.js b/scripts/check-hardcoded-colors.js new file mode 100644 index 0000000..0f512b5 --- /dev/null +++ b/scripts/check-hardcoded-colors.js @@ -0,0 +1,35 @@ +#!/usr/bin/env node +import fs from 'fs' +import path from 'path' + +const root = path.resolve(__dirname, '..', 'src') + +const hexRe = /#[0-9a-fA-F]{3,6}/g +const rgbRe = /rgba?\(/gi + +function walk(dir) { + const entries = fs.readdirSync(dir, { withFileTypes: true }) + for (const e of entries) { + const full = path.join(dir, e.name) + if (e.isDirectory()) { + walk(full) + } else if (e.isFile()) { + if (full.endsWith('globals.css')) continue + if (!full.endsWith('.css') && !full.endsWith('.tsx') && !full.endsWith('.ts') && !full.endsWith('.jsx') && !full.endsWith('.js') && !full.endsWith('.svg')) continue + const content = fs.readFileSync(full, 'utf8') + const hex = content.match(hexRe) + const rgb = content.match(rgbRe) + if ((hex && hex.length) || (rgb && rgb.length)) { + console.log(`Found in ${path.relative(process.cwd(), full)} -> hex:${hex ? hex.join(',') : '0'} rgb:${rgb ? rgb.length : 0}`) + } + } + } +} + +try { + walk(root) + console.log('\nSearch complete — open the above files and replace hardcoded colors with theme variables in src/app/globals.css.') +} catch (err) { + console.error(err) + process.exit(1) +} diff --git a/scripts/check-theme-setup.js b/scripts/check-theme-setup.js new file mode 100644 index 0000000..7adccae --- /dev/null +++ b/scripts/check-theme-setup.js @@ -0,0 +1,27 @@ +#!/usr/bin/env node +import fs from 'fs' +import path from 'path' + +const globals = fs.readFileSync(path.resolve(__dirname, '../src/app/globals.css'), 'utf8') +const themeContext = fs.readFileSync(path.resolve(__dirname, '../src/app/contexts/ThemeContext.tsx'), 'utf8') + +let ok = true + +function assert(cond, msg) { + if (!cond) { + console.error('FAIL:', msg) + ok = false + } else { + console.log('OK:', msg) + } +} + +assert(globals.includes(':root'), 'globals.css has :root declarations') +assert(globals.includes('html.dark'), 'globals.css has html.dark overrides') +assert(globals.includes('--accent-primary'), 'globals.css defines --accent-primary var') + +assert(themeContext.includes("localStorage.getItem('theme')"), 'ThemeContext reads localStorage') +assert(themeContext.includes("document.documentElement.classList"), 'ThemeContext manipulates document.documentElement classList') + +if (!ok) process.exit(1) +console.log('\nTheme setup basic checks passed — globals.css + ThemeContext look correctly configured.') diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 99eaafe..ad05582 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -94,7 +94,7 @@ export default function AdminDashboard() {
@@ -108,13 +108,13 @@ export default function AdminDashboard() {
{error && ( -
-

{error}

+
+

{error}

)} {success && ( -
+

{success}

diff --git a/src/app/auth/forgot-password/page.tsx b/src/app/auth/forgot-password/page.tsx index 139d021..e474f8d 100644 --- a/src/app/auth/forgot-password/page.tsx +++ b/src/app/auth/forgot-password/page.tsx @@ -6,8 +6,8 @@ import Link from 'next/link' import authService from '../../../services/authService' import ThemeToggle from '../../components/ThemeToggle' -const Icon = ({ d, size = 10 }: { d: string; size?: number }) => ( ); -const BoltIcon = ({size = 10}) => ; +// (removed unused Icon helper) + // --- Forgot Password Page Component --- export default function ForgotPasswordPage() { @@ -46,7 +46,7 @@ export default function ForgotPasswordPage() {
-

TechTorque Auto

+
TT

TechTorque

Back to Sign In
@@ -74,7 +74,7 @@ export default function ForgotPasswordPage() {
{error && (
-

{error}

+

{error}

)}
diff --git a/src/app/auth/login/page.tsx b/src/app/auth/login/page.tsx index ffc6ba1..316b010 100644 --- a/src/app/auth/login/page.tsx +++ b/src/app/auth/login/page.tsx @@ -8,13 +8,8 @@ import authService from '../../../services/authService'; import type { LoginRequest } from '../../../types/api'; import ThemeToggle from '../../components/ThemeToggle'; -// Icon Components -const Icon = ({ d, size = 10 }: { d: string; size?: number }) => ( - - - -); -const BoltIcon = ({size = 10}) => ; +// (removed unused Icon helper) + const LockClosedIcon = () => ; @@ -74,11 +69,11 @@ export default function LoginPage() {
-
- +
+ TT

- TechTorque Auto + TechTorque

@@ -104,15 +99,7 @@ export default function LoginPage() {

Sign in to access your dashboard.

-
-

Demo Users (Real Backend):

-
-

SuperAdmin: superadmin / superadmin123

-

Admin: admin / admin123

-

Employee: employee / emp123

-

Customer: customer / cust123

-
-
+
@@ -160,7 +147,7 @@ export default function LoginPage() { {error && (
-

{error}

+

{error}

{unverifiedEmail && ( ( ); -const BoltIcon = ({size = 10}) => ; +// (removed unused Icon helper) + // --- OTP Page Component --- export default function OtpVerifyPage() { @@ -39,7 +39,7 @@ export default function OtpVerifyPage() {
-

TechTorque Auto

+
TT

TechTorque

diff --git a/src/app/auth/register/page.tsx b/src/app/auth/register/page.tsx index 1dafed5..f86ce2e 100644 --- a/src/app/auth/register/page.tsx +++ b/src/app/auth/register/page.tsx @@ -6,21 +6,8 @@ import { useState } from "react"; import authService from "../../../services/authService"; import ThemeToggle from "../../components/ThemeToggle"; -// Icon Components -const Icon = ({ d, size = 10 }: { d: string; size?: number }) => ( - - - -); -const BoltIcon = ({ size = 10 }) => ( - -); +// (removed unused Icon helper) + const UserPlusIcon = () => (
-
- +
+ TT

- TechTorque Auto + TechTorque

@@ -178,7 +165,7 @@ export default function RegisterPage() { Account Created!

- Welcome to TechTorque Auto, {formData.fullName}! + Welcome to TechTorque, {formData.fullName}!

diff --git a/src/app/auth/resend-verification/page.tsx b/src/app/auth/resend-verification/page.tsx index 683ced8..5a0a1f9 100644 --- a/src/app/auth/resend-verification/page.tsx +++ b/src/app/auth/resend-verification/page.tsx @@ -6,13 +6,8 @@ import { useSearchParams } from 'next/navigation'; import authService from '../../../services/authService'; import ThemeToggle from '../../components/ThemeToggle'; -// Icon Components -const Icon = ({ d, size = 10 }: { d: string; size?: number }) => ( - - - -); -const BoltIcon = ({size = 10}) => ; +// (removed unused Icon helper) + const MailIcon = () => ; function ResendVerificationContent() { @@ -112,7 +107,7 @@ function ResendVerificationContent() { {error && (

-

{error}

+

{error}

)} @@ -138,11 +133,11 @@ export default function ResendVerificationPage() {
-
- +
+ TT

- TechTorque Auto + TechTorque

diff --git a/src/app/auth/reset-password/ResetPasswordForm.tsx b/src/app/auth/reset-password/ResetPasswordForm.tsx index e8e7a1f..ed32f49 100644 --- a/src/app/auth/reset-password/ResetPasswordForm.tsx +++ b/src/app/auth/reset-password/ResetPasswordForm.tsx @@ -143,7 +143,7 @@ export default function ResetPasswordForm() { {error && (
-

{error}

+

{error}

)} diff --git a/src/app/auth/reset-password/page.tsx b/src/app/auth/reset-password/page.tsx index 30d5461..d64d123 100644 --- a/src/app/auth/reset-password/page.tsx +++ b/src/app/auth/reset-password/page.tsx @@ -3,19 +3,8 @@ import Link from "next/link"; import ThemeToggle from "../../components/ThemeToggle"; import ResetPasswordForm from "./ResetPasswordForm"; -const Icon = ({ d, size = 10 }: { d: string; size?: number }) => ( - - - -); -const BoltIcon = ({ size = 10 }) => ( - -); +// (removed unused Icon helper) + export default function ResetPasswordPage() { return ( @@ -24,11 +13,11 @@ export default function ResetPasswordPage() {
-
- +
+ TT

- TechTorque Auto + TechTorque

diff --git a/src/app/auth/verify-email/page.tsx b/src/app/auth/verify-email/page.tsx index c5cc1b0..cb65ec8 100644 --- a/src/app/auth/verify-email/page.tsx +++ b/src/app/auth/verify-email/page.tsx @@ -3,21 +3,8 @@ import Link from "next/link"; import ThemeToggle from "../../components/ThemeToggle"; import VerifyEmailForm from "./VerifyEmailForm"; -// Icon Components -const Icon = ({ d, size = 10 }: { d: string; size?: number }) => ( - - - -); -const BoltIcon = ({ size = 10 }) => ( - -); +// (removed unused Icon helper) + export default function VerifyEmailPage() { return ( @@ -27,11 +14,11 @@ export default function VerifyEmailPage() {
-
- +
+ TT

- TechTorque Auto + TechTorque

diff --git a/src/app/components/AddVehicleForm.tsx b/src/app/components/AddVehicleForm.tsx index c5a9c0d..fdf371e 100644 --- a/src/app/components/AddVehicleForm.tsx +++ b/src/app/components/AddVehicleForm.tsx @@ -60,7 +60,7 @@ export default function AddVehicleForm({ onSuccess, onCancel }: AddVehicleFormPr

Add New Vehicle

{error && ( -
+
{error}
)} diff --git a/src/app/components/EditVehicleForm.tsx b/src/app/components/EditVehicleForm.tsx index 48f10a1..8178320 100644 --- a/src/app/components/EditVehicleForm.tsx +++ b/src/app/components/EditVehicleForm.tsx @@ -59,7 +59,7 @@ export default function EditVehicleForm({ {error && ( -
+
{error}
)} @@ -72,7 +72,7 @@ export default function EditVehicleForm({ name="color" value={formData.color} onChange={handleChange} - className="w-full px-4 py-3 border rounded-xl focus:ring-2 focus:ring-blue-500 transition-all theme-bg-primary theme-text-primary automotive-border" + className="theme-input w-full" placeholder="e.g., Blue" />
@@ -85,7 +85,7 @@ export default function EditVehicleForm({ value={formData.mileage} onChange={handleChange} min="0" - className="w-full px-4 py-3 border rounded-xl focus:ring-2 focus:ring-blue-500 transition-all theme-bg-primary theme-text-primary automotive-border" + className="theme-input w-full" placeholder="e.g., 50000" />
@@ -97,10 +97,8 @@ export default function EditVehicleForm({ name="licensePlate" value={formData.licensePlate} onChange={handleChange} - className="w-full px-4 py-3 border rounded-xl focus:ring-2 focus:ring-blue-500 transition-all theme-bg-primary theme-text-primary automotive-border opacity-60 cursor-not-allowed" + className="theme-input w-full" placeholder="e.g., ABC-1234" - disabled - readOnly />
@@ -109,7 +107,7 @@ export default function EditVehicleForm({ diff --git a/src/app/components/NotificationBell.tsx b/src/app/components/NotificationBell.tsx index 39a3407..18a9f7d 100644 --- a/src/app/components/NotificationBell.tsx +++ b/src/app/components/NotificationBell.tsx @@ -4,9 +4,9 @@ import { useEffect, useRef, useState } from 'react' import { useNotifications } from '@/app/contexts/NotificationContext' export default function NotificationBell() { - const { notifications, unreadCount, loading, markAsRead, markAllAsRead, fetchNotifications, isConnected } = useNotifications() + const { notifications, unreadCount, loading, error, markAsRead, markAllAsRead, fetchNotifications, isConnected } = useNotifications() const [open, setOpen] = useState(false) - const [error, setError] = useState(null) + const [localError, setLocalError] = useState(null) const panelRef = useRef(null) useEffect(() => { @@ -29,6 +29,7 @@ export default function NotificationBell() { const nextState = !open setOpen(nextState) if (nextState) { + setLocalError(null) await fetchNotifications() } } @@ -36,20 +37,20 @@ export default function NotificationBell() { const handleMarkAsRead = async (notificationId: string) => { try { await markAsRead(notificationId) - setError(null) + setLocalError(null) } catch (err: unknown) { const message = (err as { response?: { data?: { message?: string } } }).response?.data?.message || 'Failed to mark notification as read' - setError(message) + setLocalError(message) } } const handleMarkAllAsRead = async () => { try { await markAllAsRead() - setError(null) + setLocalError(null) } catch (err: unknown) { const message = (err as { response?: { data?: { message?: string } } }).response?.data?.message || 'Failed to mark notifications as read' - setError(message) + setLocalError(message) } } @@ -58,11 +59,11 @@ export default function NotificationBell() { {open && ( -
+
Notifications {notifications.length > 0 && ( @@ -101,8 +104,8 @@ export default function NotificationBell() { {loading ? (
Loading notifications...
- ) : error ? ( -
{error}
+ ) : (localError || error) ? ( +
{localError || error}
) : notifications.length === 0 ? (
No notifications yet.
) : ( @@ -110,7 +113,7 @@ export default function NotificationBell() { {notifications.map((notification) => (
  • @@ -126,7 +129,7 @@ export default function NotificationBell() { @@ -138,7 +141,7 @@ export default function NotificationBell() { )} View all notifications diff --git a/src/app/components/PaymentGateway.tsx b/src/app/components/PaymentGateway.tsx index 40649ca..0343e1d 100644 --- a/src/app/components/PaymentGateway.tsx +++ b/src/app/components/PaymentGateway.tsx @@ -123,14 +123,14 @@ export default function PaymentGateway({
    {error && ( -
    -

    {error}

    +
    +

    {error}

    )} {success && ( -
    -

    Payment completed successfully!

    +
    +

    Payment completed successfully!

    )} @@ -144,8 +144,8 @@ export default function PaymentGateway({ {loading ? 'Processing...' : success ? 'Payment Completed' : 'Pay with PayHere'} -
    -

    +

    +

    Secure payment powered by PayHere. Your payment information is encrypted and secure.

    diff --git a/src/app/components/RoleSwitcher.tsx b/src/app/components/RoleSwitcher.tsx index 8d8f319..f91e08e 100644 --- a/src/app/components/RoleSwitcher.tsx +++ b/src/app/components/RoleSwitcher.tsx @@ -13,29 +13,29 @@ const roleConfig = { SUPER_ADMIN: { label: 'Super Admin', icon: 'šŸ‘‘', - color: 'text-purple-600 dark:text-purple-400', - bgColor: 'bg-purple-100 dark:bg-purple-900/30', + color: 'theme-text-primary', + bgColor: 'theme-bg-secondary', dashboardPath: '/dashboard', }, ADMIN: { label: 'Admin', icon: 'āš™ļø', - color: 'text-blue-600 dark:text-blue-400', - bgColor: 'bg-blue-100 dark:bg-blue-900/30', + color: 'theme-text-info', + bgColor: 'theme-alert-info', dashboardPath: '/dashboard', }, EMPLOYEE: { label: 'Employee', icon: 'šŸ‘·', - color: 'text-green-600 dark:text-green-400', - bgColor: 'bg-green-100 dark:bg-green-900/30', + color: 'theme-text-success', + bgColor: 'theme-alert-success', dashboardPath: '/dashboard', }, CUSTOMER: { label: 'Customer', icon: 'šŸ‘¤', - color: 'text-gray-600 dark:text-gray-400', - bgColor: 'bg-gray-100 dark:bg-gray-800', + color: 'theme-text-secondary', + bgColor: 'theme-bg-tertiary', dashboardPath: '/dashboard', }, }; @@ -76,8 +76,8 @@ export default function RoleSwitcher({ roles, currentRole, onRoleChange }: RoleS const currentConfig = roleConfig[currentRole as keyof typeof roleConfig] || { label: currentRole, icon: 'šŸ”¹', - color: 'text-gray-600', - bgColor: 'bg-gray-100', + color: 'theme-text-muted', + bgColor: 'theme-bg-secondary', dashboardPath: '/dashboard', }; @@ -112,17 +112,17 @@ export default function RoleSwitcher({ roles, currentRole, onRoleChange }: RoleS {/* Dropdown Menu */} {isOpen && ( -
    +
    -
    +
    Switch View
    {sortedRoles.map((role) => { const config = roleConfig[role as keyof typeof roleConfig] || { label: role, icon: 'šŸ”¹', - color: 'text-gray-600', - bgColor: 'bg-gray-100', + color: 'theme-text-muted', + bgColor: 'theme-bg-secondary', }; const isActive = role === currentRole; @@ -133,7 +133,7 @@ export default function RoleSwitcher({ roles, currentRole, onRoleChange }: RoleS className={`w-full flex items-center gap-3 px-3 py-2 rounded-md text-sm transition-colors ${ isActive ? `${config.bgColor} ${config.color} font-medium` - : 'theme-text-secondary hover:bg-gray-100 dark:hover:bg-gray-800' + : 'theme-text-secondary hover:theme-bg-hover' }`} > {config.icon} @@ -151,7 +151,7 @@ export default function RoleSwitcher({ roles, currentRole, onRoleChange }: RoleS ); })}
    -
    +

    šŸ’” Your current view affects the dashboard and available menu items

    diff --git a/src/app/components/VehicleCard.tsx b/src/app/components/VehicleCard.tsx index fcdac0f..fed2ba1 100644 --- a/src/app/components/VehicleCard.tsx +++ b/src/app/components/VehicleCard.tsx @@ -10,7 +10,7 @@ interface VehicleCardProps { export default function VehicleCard({ vehicle, onDelete, onEdit }: VehicleCardProps) { return ( -
    +

    @@ -19,7 +19,7 @@ export default function VehicleCard({ vehicle, onDelete, onEdit }: VehicleCardPr

    License: {vehicle.licensePlate}

    {vehicle.color && ( - + {vehicle.color} )} @@ -51,7 +51,7 @@ export default function VehicleCard({ vehicle, onDelete, onEdit }: VehicleCardPr onDelete(vehicle.vehicleId); } }} - className="px-4 py-2.5 border automotive-border text-red-600 dark:text-red-400 rounded-xl hover:bg-red-50 dark:hover:bg-red-900/30 transition-colors font-medium" + className="px-4 py-2.5 border automotive-border theme-text-danger hover:theme-bg-hover transition-colors font-medium rounded-xl" > Delete diff --git a/src/app/components/chatbot/AIChatWidget.tsx b/src/app/components/chatbot/AIChatWidget.tsx index 5ef7678..c68ede5 100644 --- a/src/app/components/chatbot/AIChatWidget.tsx +++ b/src/app/components/chatbot/AIChatWidget.tsx @@ -1,205 +1,234 @@ -import React, { useState, useRef, useEffect, useCallback } from 'react'; -import { Sparkles, Bolt } from 'lucide-react'; -import Cookies from 'js-cookie'; -import { config } from '@/config/runtime'; - -// --- Theme Simulation & Constants --- -const theme = { - 'theme-text-primary': 'text-gray-900 dark:text-gray-100', - 'theme-text-muted': 'text-gray-500 dark:text-gray-400', - 'theme-bg-primary': 'bg-gray-50 dark:bg-gray-800', - 'theme-button-primary': 'bg-indigo-600 hover:bg-indigo-700 text-white', - 'theme-button-secondary': 'bg-gray-200 dark:bg-gray-700', - 'theme-border': 'border-gray-300 dark:border-gray-700', - 'theme-input': 'bg-white dark:bg-gray-600 border theme-border p-2 rounded-lg focus:ring-indigo-500 focus:border-indigo-500', - 'automotive-card': 'bg-white dark:bg-gray-800' -}; - -// --- TypeScript Interface Definitions --- -interface Message { - text: string; - sender: 'user' | 'ai' | 'system'; -} - -interface ChatResponse { - reply: string; - session_id: string; - tool_executed?: string | null; -} - -// Use the runtime config to get the API Gateway URL -const API_ENDPOINT = `${config.NEXT_PUBLIC_API_BASE_URL}/api/v1/ai/chat`; - -const AIChatWidget: React.FC = () => { - // State Management - const [userToken, setUserToken] = useState(null); - - const [conversationHistory, setConversationHistory] = useState([ - { text: "šŸ‘‹ Hello! I'm TechTorque Assistant, your friendly car service companion! šŸš—\n\nI can help you with:\nāœ… Booking appointments\nāœ… Checking service status\nāœ… Vehicle information\nāœ… Pricing & estimates\n\nWhat can I do for you today? 😊", sender: 'ai' } - ]); - const [inputMessage, setInputMessage] = useState(''); - const [sessionId, setSessionId] = useState(null); - const [isLoading, setIsLoading] = useState(false); - - const messagesEndRef = useRef(null); - - // Check for cookie on mount - useEffect(() => { - const token = Cookies.get('tt_access_token'); - setUserToken(token || null); - }, []); - - // Auto-scroll to latest message - useEffect(() => { - messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); - }, [conversationHistory]); - - // Core Logic - const sendMessage = useCallback(async (message: string) => { - const currentToken = Cookies.get('tt_access_token'); - - if (!message.trim() || isLoading || !currentToken) return; - - const userMessage: Message = { text: message, sender: 'user' }; - setConversationHistory(prev => [...prev, userMessage]); - setInputMessage(''); - setIsLoading(true); - - try { - const payload = { - query: message, - session_id: sessionId, - token: currentToken, - }; - - const response = await fetch(API_ENDPOINT, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${currentToken}`, - }, - body: JSON.stringify(payload), - }); - - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.detail || `HTTP Error ${response.status}`); - } - - const data: ChatResponse = await response.json(); - - let replyText = data.reply; - if (data.tool_executed) { - replyText = `āš™ļø (Tool used: ${data.tool_executed}) ${replyText}`; - } - - const aiResponse: Message = { text: replyText, sender: 'ai' }; - setConversationHistory(prev => [...prev, aiResponse]); - setSessionId(data.session_id); - - } catch (error: unknown) { - console.error("Chat Error:", error); - const errorMessage: Message = { - text: (error instanceof Error && error.message.includes('401')) - ? "šŸ”’ Your session has expired. Please log in again to continue chatting!" - : "āš ļø Oops! I'm having trouble connecting to my services right now. Please try again in a moment! šŸ”„", - sender: 'system' - }; - setConversationHistory(prev => [...prev, errorMessage]); - } finally { - setIsLoading(false); - } - }, [isLoading, sessionId]); - - // Handler for form submission - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - sendMessage(inputMessage); - }; - - return ( -
    - - {/* Header */} -
    -
    - -
    -

    - TechTorque AI Assistant -

    -
    - - {/* Message Display Area */} -
    - {conversationHistory.map((msg, index) => ( -
    -
    - {msg.sender === 'ai' && } - {msg.text} -
    -
    - ))} - - {/* Typing Indicator */} - {isLoading && ( -
    -
    - - - Thinking - - . - . - . - - -
    -
    - )} -
    -
    - - {/* Input Form */} -
    - setInputMessage(e.target.value)} - placeholder={!userToken ? "Please sign in to chat..." : "Ask about appointments, status, or services..."} - className="flex-1 px-4 py-3 text-base border border-gray-300 dark:border-gray-600 rounded-xl focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:bg-gray-800 dark:text-white" - disabled={isLoading || !userToken} - /> - -
    - - {/* Login Warning Message */} - {!userToken && ( -
    -

    Please log in to start using the AI assistant.

    -
    - )} -
    - ); -}; - -export default AIChatWidget; \ No newline at end of file +"use client"; + +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import Cookies from 'js-cookie'; +import apiClient from '@/lib/apiClient'; +import { Bolt, Sparkles } from 'lucide-react'; + +// Local helper types used only in this component +type MessageSender = 'ai' | 'user' | 'system'; +type Message = { text: string; sender: MessageSender }; + +interface ChatResponse { + reply: string; + session_id?: string; + tool_executed?: string; +} + +const AIChatWidget: React.FC = () => { + // State Management + + + const [conversationHistory, setConversationHistory] = useState([ + { + text: + "šŸ‘‹ Hello! I'm TechTorque Assistant, your friendly car service companion! šŸš—\n\nI can help you with:\nāœ… Booking appointments\nāœ… Checking service status\nāœ… Vehicle information\nāœ… Pricing & estimates\n\nWhat can I do for you today? 😊", + sender: 'ai', + }, + ]); + + const [inputMessage, setInputMessage] = useState(''); + const [sessionId, setSessionId] = useState(null); + const [isLoading, setIsLoading] = useState(false); + + const messagesEndRef = useRef(null); + + + + // Auto-scroll to latest message + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, [conversationHistory]); + + // Core Logic + const sendMessage = useCallback( + async (message: string) => { + const currentToken = Cookies.get('tt_access_token'); + + if (!message.trim() || isLoading || !currentToken) return; + + const userMessage: Message = { text: message, sender: 'user' }; + setConversationHistory((prev) => [...prev, userMessage]); + setInputMessage(''); + setIsLoading(true); + + try { + const payload = { + query: message, + session_id: sessionId, + token: currentToken, + }; + + // Let the shared `apiClient` add auth headers via request interceptor + const { data } = await apiClient.post('/ai/chat', payload); + + let replyText = data.reply; + if (data.tool_executed) { + replyText = `āš™ļø (Tool used: ${data.tool_executed}) ${replyText}`; + } + + const aiResponse: Message = { text: replyText, sender: 'ai' }; + setConversationHistory((prev) => [...prev, aiResponse]); + // session_id may be undefined from the server — convert to null so it matches our state type + setSessionId(data.session_id ?? null); + } catch (error: unknown) { + // Log a small, useful summary to the console — keep heavy objects off the top-level + const getErrorStatus = (e: unknown): number | undefined => { + if (typeof e !== 'object' || e === null) return undefined; + const obj = e as Record; + if (typeof obj.status === 'number') return obj.status; + if (obj.response && typeof obj.response === 'object') { + const resp = obj.response as Record; + if (typeof resp.status === 'number') return resp.status; + } + return undefined; + }; + + const getServerMessage = (e: unknown): string | undefined => { + if (typeof e !== 'object' || e === null) return undefined; + const obj = e as Record; + + // Prefer top-level message if present + if (typeof obj.message === 'string') return obj.message; + + // Try nested response -> data -> detail/message/error/msg + const resp = obj.response as Record | undefined; + const data = resp?.data as Record | undefined; + const detail = data?.detail ?? data?.message ?? data?.error ?? data?.msg; + if (typeof detail === 'string') return detail; + + // If detail is an object (validation payload), stringify a small piece + if (typeof detail === 'object' && detail !== null) { + const keys = Object.keys(detail as Record); + if (keys.length) { + const candidate = (detail as Record)[keys[0]]; + if (typeof candidate === 'string') return candidate; + try { + return JSON.stringify(candidate); + } catch { + return String(candidate); + } + } + } + + return undefined; + }; + + const status = getErrorStatus(error); + const serverMessage = getServerMessage(error) ?? undefined; + // Sanitize the message before logging to avoid huge multi-line validation + // payloads from backend (e.g. Pydantic validation objects). + const sanitizeForLog = (v: unknown, maxLen = 300): string => { + if (typeof v === 'string') return v.split(/\r?\n/)[0].slice(0, maxLen); + try { + const s = JSON.stringify(v); + return s.split(/\r?\n/)[0].slice(0, maxLen); + } catch { + return String(v).split(/\r?\n/)[0].slice(0, maxLen); + } + }; + + console.error( + 'Chat Error: status=%s message=%s', + status ?? 'unknown', + sanitizeForLog(serverMessage ?? error) + ); + + // Build a user-facing message based on status and any server-provided message + let displayText = "āš ļø Oops! I'm having trouble connecting to my services right now. Please try again in a moment! šŸ”„"; + if (status === 401) { + displayText = "šŸ”’ Your session has expired. Please log in again to continue chatting!"; + } else if (status === 422 || status === 400) { + // Validation errors are common — show a simple hint and any server-provided message + displayText = serverMessage + ? `āš ļø Validation failed: ${String(serverMessage)}` + : "āš ļø The data you sent wasn't valid. Please check and try again."; + } else if (typeof serverMessage === 'string' && serverMessage.length) { + displayText = `āš ļø ${serverMessage}`; + } + + const errorMessage: Message = { text: displayText, sender: 'system' }; + setConversationHistory((prev) => [...prev, errorMessage]); + } finally { + setIsLoading(false); + } + }, + [isLoading, sessionId] + ); + + // Handler for form submission + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + sendMessage(inputMessage); + }; + + return ( +
    + {/* Header */} +
    +
    + +
    +

    TechTorque AI Assistant

    +
    + + {/* Message Display Area */} +
    + {conversationHistory.map((msg, index) => ( +
    +
    + {msg.sender === 'ai' && } + {msg.text} +
    +
    + ))} + + {/* Typing Indicator */} + {isLoading && ( +
    +
    + + + Thinking + + . + . + . + + +
    +
    + )} + +
    +
    + + {/* Input area */} +
    +
    +