diff --git a/app/src/components/ui/badge.tsx b/app/src/components/ui/badge.tsx
index fd3a406..933164a 100644
--- a/app/src/components/ui/badge.tsx
+++ b/app/src/components/ui/badge.tsx
@@ -43,4 +43,5 @@ function Badge({
)
}
+// eslint-disable-next-line react-refresh/only-export-components
export { Badge, badgeVariants }
diff --git a/app/src/components/ui/button-group.tsx b/app/src/components/ui/button-group.tsx
index 8600af0..9c3ca29 100644
--- a/app/src/components/ui/button-group.tsx
+++ b/app/src/components/ui/button-group.tsx
@@ -79,5 +79,6 @@ export {
ButtonGroup,
ButtonGroupSeparator,
ButtonGroupText,
+ // eslint-disable-next-line react-refresh/only-export-components
buttonGroupVariants,
}
diff --git a/app/src/components/ui/button.tsx b/app/src/components/ui/button.tsx
index 37a7d4b..aa07f35 100644
--- a/app/src/components/ui/button.tsx
+++ b/app/src/components/ui/button.tsx
@@ -59,4 +59,5 @@ function Button({
)
}
+// eslint-disable-next-line react-refresh/only-export-components
export { Button, buttonVariants }
diff --git a/app/src/components/ui/form.tsx b/app/src/components/ui/form.tsx
index 2b529e6..c5a6dab 100644
--- a/app/src/components/ui/form.tsx
+++ b/app/src/components/ui/form.tsx
@@ -156,6 +156,7 @@ function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
}
export {
+ // eslint-disable-next-line react-refresh/only-export-components
useFormField,
Form,
FormItem,
diff --git a/app/src/components/ui/navigation-menu.tsx b/app/src/components/ui/navigation-menu.tsx
index 1199945..be1cded 100644
--- a/app/src/components/ui/navigation-menu.tsx
+++ b/app/src/components/ui/navigation-menu.tsx
@@ -164,5 +164,6 @@ export {
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
+ // eslint-disable-next-line react-refresh/only-export-components
navigationMenuTriggerStyle,
}
diff --git a/app/src/components/ui/sidebar.tsx b/app/src/components/ui/sidebar.tsx
index 30638ac..1fef2ba 100644
--- a/app/src/components/ui/sidebar.tsx
+++ b/app/src/components/ui/sidebar.tsx
@@ -607,9 +607,7 @@ function SidebarMenuSkeleton({
showIcon?: boolean
}) {
// Random width between 50 to 90%.
- const width = React.useMemo(() => {
- return `${Math.floor(Math.random() * 40) + 50}%`
- }, [])
+ const [width] = React.useState(() => `${Math.floor(Math.random() * 40) + 50}%`)
return (
;
diff --git a/app/src/sections/CommunityForum.tsx b/app/src/sections/CommunityForum.tsx
index beff79d..02aaa01 100644
--- a/app/src/sections/CommunityForum.tsx
+++ b/app/src/sections/CommunityForum.tsx
@@ -1,4 +1,6 @@
import { useState, useEffect, useCallback } from 'react';
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
import { motion, AnimatePresence } from 'framer-motion';
import {
ArrowLeft,
@@ -24,6 +26,7 @@ import {
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { useAuth } from '@/contexts/AuthContext';
+import { createPostSchema, TITLE_MAX, CONTENT_MAX } from '@/lib/validators';
import {
getPosts,
getPost,
@@ -95,11 +98,27 @@ export function CommunityForum({ onBack, onAuthClick }: CommunityForumProps) {
// Create post state
const [showCreateForm, setShowCreateForm] = useState(false);
- const [newTitle, setNewTitle] = useState('');
- const [newContent, setNewContent] = useState('');
- const [newCategory, setNewCategory] = useState('general');
const [createSubmitting, setCreateSubmitting] = useState(false);
+ const {
+ register,
+ handleSubmit,
+ watch,
+ reset,
+ formState: { errors, isValid },
+ } = useForm({
+ resolver: zodResolver(createPostSchema),
+ defaultValues: {
+ title: '',
+ content: '',
+ category: 'general',
+ },
+ mode: 'onChange',
+ });
+
+ const watchTitle = watch('title');
+ const watchContent = watch('content');
+
// Fetch posts
const fetchPosts = useCallback(async () => {
setLoading(true);
@@ -130,23 +149,20 @@ export function CommunityForum({ onBack, onAuthClick }: CommunityForumProps) {
}
};
- const handleCreatePost = async () => {
+ const handleCreatePost = async (data: { title: string; content: string; category: string }) => {
if (!user) {
onAuthClick('login');
return;
}
- if (!newTitle.trim() || !newContent.trim()) return;
setCreateSubmitting(true);
try {
await createPost({
- title: newTitle.trim(),
- content: newContent.trim(),
- category: newCategory
+ title: data.title.trim(),
+ content: data.content.trim(),
+ category: data.category,
});
- setNewTitle('');
- setNewContent('');
- setNewCategory('general');
+ reset();
setShowCreateForm(false);
fetchPosts();
} catch (err) {
@@ -488,34 +504,64 @@ export function CommunityForum({ onBack, onAuthClick }: CommunityForumProps) {
exit={{ opacity: 0, height: 0 }}
className="overflow-hidden mb-6"
>
-
+
)}
diff --git a/app/src/sections/ProblemWorkspace.tsx b/app/src/sections/ProblemWorkspace.tsx
index d539772..e1d0b8f 100644
--- a/app/src/sections/ProblemWorkspace.tsx
+++ b/app/src/sections/ProblemWorkspace.tsx
@@ -26,11 +26,13 @@ const SUPPORTED_LANGUAGES = [
* @param onBack - Callback invoked when the user navigates back to the problem list.
*/
export function ProblemWorkspace({ problemId, onBack }: ProblemWorkspaceProps) {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
const [problem, setProblem] = useState
(null);
const [loading, setLoading] = useState(true);
const [code, setCode] = useState('// Write your code here');
const [language, setLanguage] = useState('javascript');
const [theme, setTheme] = useState<'vs-dark' | 'light'>('vs-dark');
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
const [executionResult, setExecutionResult] = useState(null);
const [isExecuting, setIsExecuting] = useState(false);
const consoleRef = useRef(null);
@@ -110,7 +112,7 @@ export function ProblemWorkspace({ problemId, onBack }: ProblemWorkspaceProps) {
const result = await executeCode(problemId, code, language);
setExecutionResult(result);
return result;
- } catch (error: any) {
+ } catch (error: any) { // eslint-disable-line @typescript-eslint/no-explicit-any
const errRes = { error: `Execution failed: ${error.message || 'Server error'}` };
setExecutionResult(errRes);
toast.error('Failed to execute code');
@@ -131,7 +133,7 @@ export function ProblemWorkspace({ problemId, onBack }: ProblemWorkspaceProps) {
toast.success('All test cases passed! (Submission saved)');
try {
await updateProblemStatus(problemId, 'SOLVED');
- } catch (err) {
+ } catch {
// silently fail if not logged in or other issues
}
}
@@ -308,6 +310,7 @@ export function ProblemWorkspace({ problemId, onBack }: ProblemWorkspaceProps) {
{executionResult.allPassed ? 'All Test Cases Passed!' : 'Some Test Cases Failed'}
+ {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
{executionResult.results.map((res: any, idx: number) => (
diff --git a/app/src/sections/Problems.tsx b/app/src/sections/Problems.tsx
index f13c879..941c2a3 100644
--- a/app/src/sections/Problems.tsx
+++ b/app/src/sections/Problems.tsx
@@ -65,6 +65,7 @@ export function Problems() {
const bookmarked = new Set
();
const notesData: Record = {};
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
userProgressData.forEach((p: any) => {
if (p.status === 'SOLVED') completed.add(p.problem_id);
if (p.is_bookmarked) bookmarked.add(p.problem_id);
@@ -86,6 +87,7 @@ export function Problems() {
// Get all unique tags
const allTags = useMemo(() => {
const tags = new Set();
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
allProblems.forEach((p: any) => {
if (p.tags) p.tags.forEach((t: string) => tags.add(t));
});
@@ -94,7 +96,7 @@ export function Problems() {
// Filter problems
const filteredProblems = useMemo(() => {
- return allProblems.filter((problem: any) => {
+ return allProblems.filter((problem: any) => { // eslint-disable-line @typescript-eslint/no-explicit-any
const tags = problem.tags || [];
const matchesSearch = problem.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
tags.some((tag: string) => tag.toLowerCase().includes(searchQuery.toLowerCase()));
@@ -107,8 +109,11 @@ export function Problems() {
// Stats
const stats = {
total: allProblems.length,
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
easy: allProblems.filter((p: any) => p.difficulty === 'Easy').length,
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
medium: allProblems.filter((p: any) => p.difficulty === 'Medium').length,
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
hard: allProblems.filter((p: any) => p.difficulty === 'Hard').length,
};
@@ -129,7 +134,7 @@ export function Problems() {
if (!wasCompleted) toast.success(`Problem marked as complete! +${SOLVE_XP} XP`);
// Refresh profile so nav XP updates immediately
refreshProfile();
- } catch (e) {
+ } catch {
setCompletedProblems(prev => {
const newSet = new Set(prev);
if (wasCompleted) newSet.add(problemMongoId);
@@ -156,7 +161,7 @@ export function Problems() {
await apiToggleBookmark(problemMongoId);
if (wasBookmarked) toast.info('Bookmark removed');
else toast.success('Problem bookmarked');
- } catch (e) {
+ } catch {
setBookmarkedProblems(prev => {
const newSet = new Set(prev);
if (wasBookmarked) newSet.add(problemMongoId);
@@ -313,10 +318,12 @@ export function Problems() {
transition={{ duration: 0.6, delay: 0.3 }}
className="grid gap-3"
>
+ {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
{filteredProblems.map((problem: any, index: number) => {
- const problemMongoId = problem.id || problem.id;
+ const problemMongoId = problem._id || problem.id;
const isCompleted = completedProblems.has(problemMongoId);
const isBookmarked = bookmarkedProblems.has(problemMongoId);
+ {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
const topic = topics.find((t: any) => t.id === problem.topic_id);
return (
@@ -460,7 +467,7 @@ export function Problems() {
setNotesMap(prev => ({ ...prev, [notesModal.problemId]: noteContent }));
toast.success(noteContent.trim() ? 'Note saved!' : 'Note cleared');
setNotesModal(null);
- } catch (e) {
+ } catch {
toast.error('Failed to save note. Please log in.');
} finally {
setSavingNote(false);
diff --git a/app/src/sections/Roadmaps.tsx b/app/src/sections/Roadmaps.tsx
index a973067..ed1c4d8 100644
--- a/app/src/sections/Roadmaps.tsx
+++ b/app/src/sections/Roadmaps.tsx
@@ -33,7 +33,9 @@ export function Roadmaps({ onPathClick }: RoadmapsProps) {
const [hoveredCategory, setHoveredCategory] = useState(null);
const { problemCount, videoCount, roadmapCount, userCount } = useStats();
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
const [categories, setCategories] = useState([]);
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
const [topicsMap, setTopicsMap] = useState>({});
const [pathSolvedCounts, setPathSolvedCounts] = useState>({});
const [loading, setLoading] = useState(true);
@@ -44,19 +46,23 @@ export function Roadmaps({ onPathClick }: RoadmapsProps) {
const paths = await getLearningPaths();
setCategories(paths);
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
const topicsData: Record = {};
// Also collect all problem _ids per path for progress matching
const pathProblemIds: Record = {};
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
await Promise.all(paths.map(async (path: any) => {
const pathTopics = await getTopicsByPath(path.id);
topicsData[path.id] = pathTopics;
// Fetch problems for each topic to get their _ids
const problemIds: string[] = [];
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
await Promise.all(pathTopics.map(async (topic: any) => {
try {
const problems = await getProblemsByTopic(topic.id);
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
problems.forEach((p: any) => problemIds.push(p.id));
} catch { /* ignore */ }
}));
@@ -70,7 +76,9 @@ export function Roadmaps({ onPathClick }: RoadmapsProps) {
const progressData = await getUserProgress();
const solvedSet = new Set(
progressData
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
.filter((p: any) => p.status === 'SOLVED')
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
.map((p: any) => p.problem_id)
);
@@ -226,6 +234,7 @@ export function Roadmaps({ onPathClick }: RoadmapsProps) {
{/* Topics Preview */}
+ {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
{topics.slice(0, 3).map((topic: any) => (
(null);
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
const [problems, setProblems] = useState
([]);
const [loading, setLoading] = useState(true);
@@ -61,6 +63,7 @@ export function TopicDetail({ topicId, onBack }: TopicDetailProps) {
const completed = new Set();
const bookmarked = new Set();
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
progressData.forEach((p: any) => {
if (p.status === 'SOLVED') completed.add(p.problem_id);
if (p.is_bookmarked) bookmarked.add(p.problem_id);
@@ -71,7 +74,7 @@ export function TopicDetail({ topicId, onBack }: TopicDetailProps) {
setCompletedProblems(completed);
setBookmarkedProblems(bookmarked);
setNotesMap(notesData);
- } catch (err) {
+ } catch {
// User might not be logged in, ignore
}
@@ -130,7 +133,7 @@ export function TopicDetail({ topicId, onBack }: TopicDetailProps) {
if (!wasCompleted) toast.success(`Problem marked as complete! +${SOLVE_XP} XP`);
// Refresh profile so nav XP updates immediately
refreshProfile();
- } catch (e) {
+ } catch {
// Revert
setCompletedProblems(prev => {
const newSet = new Set(prev);
@@ -158,7 +161,7 @@ export function TopicDetail({ topicId, onBack }: TopicDetailProps) {
await apiToggleBookmark(problemMongoId);
if (wasBookmarked) toast.info('Bookmark removed');
else toast.success('Problem bookmarked');
- } catch (e) {
+ } catch {
setBookmarkedProblems(prev => {
const newSet = new Set(prev);
if (wasBookmarked) newSet.add(problemMongoId);
@@ -182,7 +185,7 @@ export function TopicDetail({ topicId, onBack }: TopicDetailProps) {
setNotesMap(prev => ({ ...prev, [notesModal.problemId]: noteContent }));
toast.success(noteContent.trim() ? 'Note saved!' : 'Note cleared');
setNotesModal(null);
- } catch (e) {
+ } catch {
toast.error('Failed to save note. Please log in.');
} finally {
setSavingNote(false);
@@ -295,6 +298,7 @@ export function TopicDetail({ topicId, onBack }: TopicDetailProps) {
transition={{ duration: 0.6, delay: 0.2 }}
className="space-y-3"
>
+ {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
{filteredProblems.map((problem: any, index: number) => {
const problemMongoId = problem.id;
const isCompleted = completedProblems.has(problemMongoId);
diff --git a/app/src/sections/UserHero.tsx b/app/src/sections/UserHero.tsx
index b94f830..fc45add 100644
--- a/app/src/sections/UserHero.tsx
+++ b/app/src/sections/UserHero.tsx
@@ -6,14 +6,19 @@ import { getDashboardStats, getUserProgress } from '@/api/userActions';
import { getAllProblems, getAllTopics } from '@/api/content';
interface UserHeroProps {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
user: any;
onTopicClick: (topicId: string) => void;
}
export function UserHero({ user, onTopicClick }: UserHeroProps) {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
const [dashboardStats, setDashboardStats] = useState(null);
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
const [problems, setProblems] = useState([]);
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
const [topics, setTopics] = useState([]);
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
const [userProgress, setUserProgress] = useState([]);
useEffect(() => {
@@ -31,7 +36,7 @@ export function UserHero({ user, onTopicClick }: UserHeroProps) {
try {
const progress = await getUserProgress();
setUserProgress(progress);
- } catch (e) {
+ } catch {
// Not logged in or error
}
} catch (e) {
@@ -43,7 +48,9 @@ export function UserHero({ user, onTopicClick }: UserHeroProps) {
// Compute solved stats
const solvedIds = useMemo(() => {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
const solved = userProgress.filter((p: any) => p.status === 'SOLVED');
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
return new Set(solved.map((p: any) => p.problem_id));
}, [userProgress]);
@@ -64,7 +71,7 @@ export function UserHero({ user, onTopicClick }: UserHeroProps) {
// Weekly activity from backend
const weeklyActivity = useMemo(() => {
if (dashboardStats?.weeklyActivity) {
- return dashboardStats.weeklyActivity.map((d: any) => {
+ return dashboardStats.weeklyActivity.map((d: any) => { // eslint-disable-line @typescript-eslint/no-explicit-any
const date = new Date(d.date + 'T00:00:00');
return {
day: date.toLocaleDateString('en-US', { weekday: 'short' }),
@@ -83,23 +90,31 @@ export function UserHero({ user, onTopicClick }: UserHeroProps) {
];
}, [dashboardStats]);
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
const maxActivity = Math.max(...weeklyActivity.map((d: any) => d.count), 1);
// Continue Learning - find the topic with most recent activity
const continueTopicData = useMemo(() => {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
const solvedProgress = userProgress.filter((p: any) => p.status === 'SOLVED');
// Build per-topic stats
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
const topicStats = topics.map((topic: any) => {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
const topicProblems = problems.filter((p: any) => p.topic_id === topic.id);
const totalInTopic = topicProblems.length;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
const solvedInTopic = topicProblems.filter((p: any) => solvedIds.has(p.id)).length;
const progress = totalInTopic > 0 ? Math.round((solvedInTopic / totalInTopic) * 100) : 0;
// Most recent solve for this topic
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
const topicProblemIds = new Set(topicProblems.map((p: any) => p.id));
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
const topicSolves = solvedProgress.filter((p: any) => topicProblemIds.has(p.problem_id));
const lastSolveDate = topicSolves.length > 0
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
? Math.max(...topicSolves.map((p: any) => new Date(p.updatedAt).getTime()))
: 0;
@@ -114,20 +129,21 @@ export function UserHero({ user, onTopicClick }: UserHeroProps) {
// Topic with the most recent activity (that isn't 100% complete)
const inProgress = topicStats
- .filter((t: any) => t.lastSolveDate > 0 && t.progress < 100)
- .sort((a: any, b: any) => b.lastSolveDate - a.lastSolveDate);
+ .filter((t: any) => t.lastSolveDate > 0 && t.progress < 100) // eslint-disable-line @typescript-eslint/no-explicit-any
+ .sort((a: any, b: any) => b.lastSolveDate - a.lastSolveDate); // eslint-disable-line @typescript-eslint/no-explicit-any
if (inProgress.length > 0) {
return inProgress[0];
}
// Fallback: first topic with any problems
- const withProblems = topicStats.filter((t: any) => t.totalInTopic > 0);
+ const withProblems = topicStats.filter((t: any) => t.totalInTopic > 0); // eslint-disable-line @typescript-eslint/no-explicit-any
return withProblems.length > 0 ? withProblems[0] : null;
}, [topics, problems, userProgress, solvedIds]);
// Next goals: dynamically based on progress
const nextGoals = useMemo(() => {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
const goals: any[] = [];
// Level up goal
@@ -381,7 +397,7 @@ export function UserHero({ user, onTopicClick }: UserHeroProps) {
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 1, delay: 0.8 }}
- points={`40,100 ${weeklyActivity.map((d: any, i: number) => {
+ points={`40,100 ${weeklyActivity.map((d: any, i: number) => { // eslint-disable-line @typescript-eslint/no-explicit-any
const x = 40 + i * 46;
const y = maxActivity > 0 ? 100 - (d.count / maxActivity) * 82 : 100;
return `${x},${y}`;
@@ -394,7 +410,7 @@ export function UserHero({ user, onTopicClick }: UserHeroProps) {
initial={{ pathLength: 0, opacity: 0 }}
animate={{ pathLength: 1, opacity: 1 }}
transition={{ duration: 1.5, delay: 0.6 }}
- points={weeklyActivity.map((d: any, i: number) => {
+ points={weeklyActivity.map((d: any, i: number) => { // eslint-disable-line @typescript-eslint/no-explicit-any
const x = 40 + i * 46;
const y = maxActivity > 0 ? 100 - (d.count / maxActivity) * 82 : 100;
return `${x},${y}`;
@@ -408,6 +424,7 @@ export function UserHero({ user, onTopicClick }: UserHeroProps) {
/>
{/* Dots + labels */}
+ {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
{weeklyActivity.map((d: any, i: number) => {
const x = 40 + i * 46;
const y = maxActivity > 0 ? 100 - (d.count / maxActivity) * 82 : 100;
@@ -439,6 +456,7 @@ export function UserHero({ user, onTopicClick }: UserHeroProps) {
})}
{/* Day labels */}
+ {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
{weeklyActivity.map((d: any, i: number) => {
const x = 40 + i * 46;
const isToday = i === 6;
@@ -482,6 +500,7 @@ export function UserHero({ user, onTopicClick }: UserHeroProps) {
{/* Dynamic goals */}
+ {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
{nextGoals.map((goal: any, index: number) => (
{goal.title}
diff --git a/backend/src/controllers/forumController.ts b/backend/src/controllers/forumController.ts
index 3cc2ac4..aea6e2c 100644
--- a/backend/src/controllers/forumController.ts
+++ b/backend/src/controllers/forumController.ts
@@ -147,16 +147,56 @@ export const createPost = async (req: Request, res: Response) => {
try {
const { title, content, category, tags } = req.body;
- if (!title || !content) {
- return res.status(400).json({ message: 'Title and content are required' });
+ // Validate title
+ if (typeof title !== 'string') {
+ return res.status(400).json({ message: 'Title is required' });
+ }
+ const trimmedTitle = title.trim();
+ if (!trimmedTitle) {
+ return res.status(400).json({ message: 'Title is required' });
+ }
+ if (trimmedTitle.length < 10) {
+ return res.status(400).json({ message: 'Title must be at least 10 characters' });
+ }
+ if (trimmedTitle.length > 150) {
+ return res.status(400).json({ message: 'Title must be less than 150 characters' });
+ }
+
+ // Validate content
+ if (typeof content !== 'string') {
+ return res.status(400).json({ message: 'Content is required' });
+ }
+ const trimmedContent = content.trim();
+ if (!trimmedContent) {
+ return res.status(400).json({ message: 'Content is required' });
+ }
+ if (trimmedContent.length < 20) {
+ return res.status(400).json({ message: 'Content must be at least 20 characters' });
+ }
+ if (trimmedContent.length > 10000) {
+ return res.status(400).json({ message: 'Content must be less than 10,000 characters' });
+ }
+
+ // Validate tags
+ if (tags !== undefined && tags !== null && !Array.isArray(tags)) {
+ return res.status(400).json({ message: 'Tags must be an array' });
+ }
+ const postTags = Array.isArray(tags) ? tags : [];
+ if (postTags.length > 5) {
+ return res.status(400).json({ message: 'Maximum 5 tags allowed' });
+ }
+ for (const tag of postTags) {
+ if (typeof tag !== 'string' || tag.trim().length > 30) {
+ return res.status(400).json({ message: 'Each tag must be 30 characters or less' });
+ }
}
const post = await prisma.forumPost.create({
data: {
- title,
- content,
+ title: trimmedTitle,
+ content: trimmedContent,
category: category || 'general',
- tags: tags || [],
+ tags: postTags.map((t: string) => t.trim()),
authorId: req.user.id
},
include: { author: { select: { id: true, name: true, avatar: true } } }