Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,5 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts

/src/generated/prisma
34 changes: 30 additions & 4 deletions app/[locale]/(auth)/login/LoginForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import {
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import Link from "next/link";
import { useRouter, useParams } from "next/navigation";
import { useNotification } from "@/src/contexts";

type LoginProps = {
headingText: string;
Expand Down Expand Up @@ -58,6 +60,10 @@ export default function LoginForm({
validationMessages,
}: LoginProps) {
const [showPassword, setShowPassword] = useState(false);
const router = useRouter();
const params = useParams();
const locale = params.locale as string;
const { notify } = useNotification();

const loginSchema = useMemo(
() =>
Expand All @@ -77,8 +83,27 @@ export default function LoginForm({
defaultValues: { email: "", password: "", remember: false },
});

const onSubmit = (values: LoginSchema) => {
console.log(values);
const onSubmit = async (values: LoginSchema) => {
try {
const response = await fetch("/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(values),
});
const data = await response.json();
if (response.ok) {
localStorage.setItem("user_id", data.user.id);
localStorage.setItem("user_email", data.user.email);
localStorage.setItem("user_name", data.user.fullName || "User");
Comment on lines +95 to +97
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Storing user details like ID, email, and name in localStorage is a security risk. This data is accessible to any JavaScript running on the page, making it vulnerable to Cross-Site Scripting (XSS) attacks. Since you are already using a secure, httpOnly session cookie, the frontend should rely on that session to fetch user data from a protected API endpoint when needed, rather than storing it in localStorage upon login.

Suggested change
localStorage.setItem("user_id", data.user.id);
localStorage.setItem("user_email", data.user.email);
localStorage.setItem("user_name", data.user.fullName || "User");
// User data should be fetched from a protected endpoint on the dashboard, not stored here.

Comment on lines +95 to +97
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Storing sensitive user information like user_id and user_email in localStorage is a security risk, as it's accessible to any script running on the page (XSS vulnerability). Since the backend is already setting a secure, httpOnly session cookie, this information should not be stored on the client side in localStorage. User-specific data should be fetched from a protected API endpoint when needed. The user's name is less sensitive, but for consistency and better security, it's also recommended to fetch it from an endpoint rather than storing it in localStorage.

Suggested change
localStorage.setItem("user_id", data.user.id);
localStorage.setItem("user_email", data.user.email);
localStorage.setItem("user_name", data.user.fullName || "User");
localStorage.setItem("user_name", data.user.fullName || "User");

notify("Login successful!", "success");
router.push(`/${locale}/dashboard`);
Comment on lines +95 to +99
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

Storing sensitive user information like ID, email, and name in localStorage is a security vulnerability. localStorage is accessible via client-side scripts, making this data vulnerable to Cross-Site Scripting (XSS) attacks. An attacker who finds an XSS vulnerability on your site could steal this user information.

Instead of storing this data in localStorage, you should rely on the secure, httpOnly session cookie that is already being set. When user information is needed on the client, it should be fetched from a dedicated, session-protected API endpoint.

Suggested change
localStorage.setItem("user_id", data.user.id);
localStorage.setItem("user_email", data.user.email);
localStorage.setItem("user_name", data.user.fullName || "User");
notify("Login successful!", "success");
router.push(`/${locale}/dashboard`);
// User data should not be stored in localStorage for security reasons.
// The user's session is managed by a secure httpOnly cookie.
// Data can be fetched from the server when needed.
notify("Login successful!", "success");
router.push(`/${locale}/dashboard`);

} else {
notify(data.error || "Login failed", "error");
}
} catch (error) {
console.error("Login Error:", error);
notify("An error occurred during login", "error");
}
};

return (
Expand Down Expand Up @@ -138,6 +163,7 @@ export default function LoginForm({
className="rounded-[10px] w-full py-6"
/>
<button
type="button"
className="absolute right-3 rtl:left-3 rtl:right-auto top-1/2 -translate-y-1/2 cursor-pointer"
onClick={() => setShowPassword(!showPassword)}
>
Expand Down Expand Up @@ -175,7 +201,7 @@ export default function LoginForm({
)}
/>
<Link
href="/forgot-password"
href={`/${locale}/forgot-password`}
className="text-sm md:text-base text-primary hover:underline"
>
{forgotPasswordLink}
Expand All @@ -193,7 +219,7 @@ export default function LoginForm({

<div className="flex flex-row items-center justify-center gap-2 text-base mt-8">
<p>{registerText}</p>
<Link href="/register">
<Link href={`/${locale}/register`}>
<span className="text-primary">{registerLink}</span>
</Link>
</div>
Expand Down
28 changes: 25 additions & 3 deletions app/[locale]/(auth)/register/RegisterForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import Link from "next/link";
import { Eye, EyeOff } from "lucide-react";
import { useRouter, useParams } from "next/navigation";
import { useNotification } from "@/src/contexts";
import {
Select,
SelectContent,
Expand Down Expand Up @@ -77,6 +79,10 @@ export default function SignupForm({
}: SignupProps) {
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const router = useRouter();
const params = useParams();
const locale = params.locale as string;
const { notify } = useNotification();

const signupSchema = useMemo(
() =>
Expand Down Expand Up @@ -115,8 +121,24 @@ export default function SignupForm({
},
});

function onSubmit(values: SignupSchema) {
console.log("Form submitted:", values);
async function onSubmit(values: SignupSchema) {
try {
const response = await fetch("/api/auth/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(values),
});
const data = await response.json();
if (response.ok) {
notify("Registration successful! Please login.", "success");
router.push(`/${locale}/login`);
} else {
notify(data.error || "Registration failed", "error");
}
} catch (error) {
console.error("Registration Error:", error);
notify("An error occurred during registration", "error");
}
}

return (
Expand Down Expand Up @@ -293,7 +315,7 @@ export default function SignupForm({
<div className="flex flex-row items-center justify-center gap-2 text-base mt-5">
<p>
{signinText}
<Link href="/login">
<Link href={`/${locale}/login`}>
<span className="text-primary"> {signinLink}</span>
</Link>
</p>
Expand Down
9 changes: 9 additions & 0 deletions app/[locale]/(dashboard-segment)/chat/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { LiveChat } from "@/src/components/Chat/LiveChat";

export default function ChatPage() {
return (
<div className="p-6">
<LiveChat />
</div>
);
}
79 changes: 49 additions & 30 deletions app/[locale]/(dashboard-segment)/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,52 @@ import { useState, useEffect } from "react";
import { FaUpload, FaPlus, FaLanguage } from "react-icons/fa";
import { BsGraphUp } from "react-icons/bs";
import Link from "next/link";
import { useParams } from "next/navigation";

interface Translation {
id: string;
inputText: string;
sourceLanguage: string;
targetLanguage: string;
createdAt: string;
}

export default function Dashboard() {
const t = useTranslations("Dashboard");
const params = useParams();
const locale = params.locale as string;
const [userName, setUserName] = useState("User");
const [stats, setStats] = useState({
totalTranslations: 128,
wordsThisWeek: 4200,
documentsCount: 23,
mostUsedLanguage: "English → French",
totalTranslations: 0,
wordsThisWeek: 0,
documentsCount: 0,
mostUsedLanguage: "N/A",
});
const [recentTranslations, setRecentTranslations] = useState<Translation[]>([]);

useEffect(() => {
const storedName = localStorage.getItem("user_name");
if (storedName) setUserName(storedName);

useEffect(() => {}, []);
const fetchStats = async () => {
try {
const response = await fetch(`/api/user/stats`);
const data = await response.json();
if (response.ok) {
setStats({
totalTranslations: data.totalTranslations,
wordsThisWeek: data.wordsThisWeek,
documentsCount: data.documentsCount,
mostUsedLanguage: data.mostUsedLanguage,
});
setRecentTranslations(data.recentTranslations);
}
} catch (error) {
console.error("Failed to fetch dashboard stats", error);
}
};
fetchStats();
}, []);

return (
<div className="flex flex-col gap-8 p-6 md:p-10 w-full">
Expand All @@ -29,10 +64,10 @@ export default function Dashboard() {
className="flex flex-col md:flex-row justify-between items-start md:items-center"
>
<h1 className="text-2xl md:text-3xl font-semibold text-gray-900">
{t("Welcome", { name: "Olamide" })}
{t("Welcome", { name: userName })}
</h1>
<div className="flex gap-3 mt-4 md:mt-0">
<Link href="/translation-history">
<Link href={`/${locale}/dashboard`}>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The "Start Translation" button currently links back to the dashboard page itself (/${locale}/dashboard). This seems to be a bug. It should likely link to the main translation page, which appears to be /translation-history based on other parts of the application and its previous value.

Suggested change
<Link href={`/${locale}/dashboard`}>
<Link href={`/${locale}/translation-history`}>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The "Start Translation" button currently links to the dashboard page itself, which is where the user already is. This should likely navigate the user to the page where they can perform a new translation.

Suggested change
<Link href={`/${locale}/dashboard`}>
<Link href={`/${locale}/translation-history`}>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The 'Start Translation' button currently links to the dashboard page itself, which is incorrect. It should navigate the user to the main translation page. Based on the file structure, the correct destination appears to be /translation-history.

Suggested change
<Link href={`/${locale}/dashboard`}>
<Link href={`/${locale}/translation-history`}>

<Button className="flex items-center gap-2">
<FaPlus /> {t("StartTranslation")}
</Button>
Expand Down Expand Up @@ -114,35 +149,19 @@ export default function Dashboard() {
<span>Date</span>
</div>

{[
{
id: 1,
text: "Hello world",
lang: "English → French",
date: "2025-10-10",
},
{
id: 2,
text: "Good morning",
lang: "English → Spanish",
date: "2025-10-09",
},
{
id: 3,
text: "Translate this text",
lang: "English → German",
date: "2025-10-08",
},
].map((item) => (
{recentTranslations.map((item) => (
<div
key={item.id}
className="p-4 flex justify-between text-sm border-b last:border-none hover:bg-gray-50 transition"
>
<span className="truncate w-[40%]">{item.text}</span>
<span>{item.lang}</span>
<span className="text-gray-500">{item.date}</span>
<span className="truncate w-[40%]">{item.inputText}</span>
<span>{item.sourceLanguage} → {item.targetLanguage}</span>
<span className="text-gray-500">{new Date(item.createdAt).toLocaleDateString()}</span>
</div>
))}
{recentTranslations.length === 0 && (
<div className="p-8 text-center text-gray-400">No recent translations found.</div>
)}
</div>
</motion.div>
</div>
Expand Down
9 changes: 9 additions & 0 deletions app/[locale]/(dashboard-segment)/image-translation/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ImageTranslation } from "@/src/components/Translation/ImageTranslation";

export default function ImageTranslationPage() {
return (
<div className="p-6">
<ImageTranslation />
</div>
);
}
19 changes: 12 additions & 7 deletions app/[locale]/(dashboard-segment)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { getMessages, unstable_setRequestLocale } from "next-intl/server";
import { Locale, locales } from "@/i18n.config";
import SidebarPage from "@/src/components/Sidebar";
import TopNavPage from "@/src/components/TopNav";
import { ModalProvider, NotificationProvider } from "@/src/contexts";

export function generateStaticParams() {
return locales.map((locale) => ({ locale }));
Expand All @@ -22,13 +23,17 @@ export default async function LocaleLayout({

return (
<NextIntlClientProvider locale={locale} messages={messages}>
<div className="min-h-screen flex bg-gray-50">
<SidebarPage />
<div className="flex-1 flex flex-col">
<TopNavPage />
<main className="flex-1 p-4 overflow-y-auto">{children}</main>
</div>
</div>
<NotificationProvider>
<ModalProvider>
<div className="min-h-screen flex bg-gray-50">
<SidebarPage />
<div className="flex-1 flex flex-col">
<TopNavPage />
<main className="flex-1 p-4 overflow-y-auto">{children}</main>
</div>
</div>
</ModalProvider>
</NotificationProvider>
</NextIntlClientProvider>
);
}
11 changes: 11 additions & 0 deletions app/[locale]/(dashboard-segment)/learning/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Flashcards } from "@/src/components/Learning/Flashcards";
import { DailyLearning } from "@/src/components/Learning/DailyLearning";

export default function LearningPage() {
return (
<div className="p-6 space-y-12">
<Flashcards />
<DailyLearning />
</div>
);
}
47 changes: 29 additions & 18 deletions app/[locale]/(dashboard-segment)/translation-history/page.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,41 @@
"use client";

import { useTranslations } from "next-intl";
import { useState } from "react";
import { useState, useEffect } from "react";
import { motion } from "framer-motion";
import { Button } from "@/components/ui/button";
import { Card, CardHeader, CardContent, CardTitle } from "@/components/ui/card";
import { FaClock, FaChevronLeft, FaChevronRight } from "react-icons/fa";
import { Translation } from "@/src/components";
import { ModalProvider, NotificationProvider } from "@/src/contexts";

interface TranslationItem {
id: string;
inputText: string;
sourceLanguage: string;
targetLanguage: string;
createdAt: string;
}

export default function TranslationHistory() {
const t = useTranslations("TranslationHistory");
const [showHistory, setShowHistory] = useState(true);
const [historyItems, setHistoryItems] = useState<TranslationItem[]>([]);

const historyItems = [
{
id: 1,
input: "Hello world",
output: "Bonjour le monde",
date: "2025-10-12",
},
{ id: 2, input: "Good morning", output: "Buenos días", date: "2025-10-11" },
{
id: 3,
input: "How are you?",
output: "Wie geht's dir?",
date: "2025-10-10",
},
];
useEffect(() => {
const fetchHistory = async () => {
try {
const response = await fetch("/api/user/stats");
const data = await response.json();
if (response.ok) {
setHistoryItems(data.recentTranslations);
}
} catch (error) {
console.error("Failed to fetch history", error);
}
};
fetchHistory();
}, []);

return (
<NotificationProvider>
Expand Down Expand Up @@ -80,10 +88,13 @@ export default function TranslationHistory() {
key={item.id}
className="p-3 border-b hover:bg-gray-50 rounded-lg mb-2 cursor-pointer"
>
<p className="text-sm text-gray-700 truncate">{item.input}</p>
<p className="text-xs text-gray-500">{item.date}</p>
<p className="text-sm text-gray-700 truncate">{item.inputText}</p>
<p className="text-xs text-gray-500">{new Date(item.createdAt).toLocaleDateString()}</p>
</div>
))}
{historyItems.length === 0 && (
<p className="text-sm text-gray-400 text-center py-4">No history yet.</p>
)}
</motion.aside>

{/* Toggle Button (if collapsed) */}
Expand Down
Loading