Skip to content
Merged
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
1 change: 1 addition & 0 deletions app/src/components/ui/badge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,5 @@ function Badge({
)
}

// eslint-disable-next-line react-refresh/only-export-components
export { Badge, badgeVariants }
1 change: 1 addition & 0 deletions app/src/components/ui/button-group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,5 +79,6 @@ export {
ButtonGroup,
ButtonGroupSeparator,
ButtonGroupText,
// eslint-disable-next-line react-refresh/only-export-components
buttonGroupVariants,
}
1 change: 1 addition & 0 deletions app/src/components/ui/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,5 @@ function Button({
)
}

// eslint-disable-next-line react-refresh/only-export-components
export { Button, buttonVariants }
1 change: 1 addition & 0 deletions app/src/components/ui/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
}

export {
// eslint-disable-next-line react-refresh/only-export-components
useFormField,
Form,
FormItem,
Expand Down
1 change: 1 addition & 0 deletions app/src/components/ui/navigation-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -164,5 +164,6 @@ export {
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
// eslint-disable-next-line react-refresh/only-export-components
navigationMenuTriggerStyle,
}
5 changes: 2 additions & 3 deletions app/src/components/ui/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div
Expand Down Expand Up @@ -722,5 +720,6 @@ export {
SidebarRail,
SidebarSeparator,
SidebarTrigger,
// eslint-disable-next-line react-refresh/only-export-components
useSidebar,
}
1 change: 1 addition & 0 deletions app/src/components/ui/toggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,5 @@ function Toggle({
)
}

// eslint-disable-next-line react-refresh/only-export-components
export { Toggle, toggleVariants }
22 changes: 22 additions & 0 deletions app/src/lib/validators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { z } from 'zod';

export const TITLE_MAX = 150;
export const CONTENT_MAX = 10000;

export const createPostSchema = z.object({
title: z
.string()
.trim()
.min(1, 'Title is required')
.min(10, 'Title must be at least 10 characters')
.max(TITLE_MAX, `Title must be less than ${TITLE_MAX} characters`),
content: z
.string()
.trim()
.min(1, 'Content is required')
.min(20, 'Content must be at least 20 characters')
.max(CONTENT_MAX, `Content must be less than ${CONTENT_MAX.toLocaleString()} characters`),
category: z.string().default('general'),
});

export type CreatePostFormData = z.infer<typeof createPostSchema>;
106 changes: 76 additions & 30 deletions app/src/sections/CommunityForum.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -488,34 +504,64 @@ export function CommunityForum({ onBack, onAuthClick }: CommunityForumProps) {
exit={{ opacity: 0, height: 0 }}
className="overflow-hidden mb-6"
>
<div className="glass rounded-2xl p-6 border border-[#a088ff]/20 bg-[#0a0a0a]/60 backdrop-blur-md">
<form
onSubmit={handleSubmit(handleCreatePost)}
className="glass rounded-2xl p-6 border border-[#a088ff]/20 bg-[#0a0a0a]/60 backdrop-blur-md"
>
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-white">Create New Post</h3>
<button onClick={() => setShowCreateForm(false)} className="text-white/40 hover:text-white">
<button
type="button"
onClick={() => { setShowCreateForm(false); reset(); }}
className="text-white/40 hover:text-white"
>
<X className="w-5 h-5" />
</button>
</div>

<input
value={newTitle}
onChange={e => setNewTitle(e.target.value)}
placeholder="Post title..."
className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white placeholder-white/30 focus:outline-none focus:border-[#a088ff]/50 mb-3"
/>
<div className="mb-3">
<input
{...register('title')}
placeholder="Post title..."
maxLength={TITLE_MAX}
className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white placeholder-white/30 focus:outline-none focus:border-[#a088ff]/50"
/>
<div className="flex justify-between mt-1.5 px-1">
{errors.title ? (
<span className="text-red-400 text-xs">{errors.title.message}</span>
) : (
<span />
)}
<span className={`text-xs ${watchTitle.length >= TITLE_MAX ? 'text-red-400' : 'text-white/30'}`}>
{watchTitle.length} / {TITLE_MAX}
</span>
</div>
</div>

<textarea
value={newContent}
onChange={e => setNewContent(e.target.value)}
placeholder="What's on your mind? Share your thoughts, questions, or solutions..."
className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white text-sm placeholder-white/30 focus:outline-none focus:border-[#a088ff]/50 resize-none min-h-[120px] mb-3"
/>
<div className="mb-3">
<textarea
{...register('content')}
placeholder="What's on your mind? Share your thoughts, questions, or solutions..."
maxLength={CONTENT_MAX}
className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white text-sm placeholder-white/30 focus:outline-none focus:border-[#a088ff]/50 resize-none min-h-[120px]"
/>
<div className="flex justify-between mt-1.5 px-1">
{errors.content ? (
<span className="text-red-400 text-xs">{errors.content.message}</span>
) : (
<span />
)}
<span className={`text-xs ${watchContent.length >= CONTENT_MAX ? 'text-red-400' : 'text-white/30'}`}>
{watchContent.length.toLocaleString()} / {CONTENT_MAX.toLocaleString()}
</span>
</div>
</div>

<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-white/40 text-xs">Category:</span>
<select
value={newCategory}
onChange={e => setNewCategory(e.target.value)}
{...register('category')}
className="bg-white/5 border border-white/10 rounded-lg px-3 py-1.5 text-white text-sm focus:outline-none focus:border-[#a088ff]/50 appearance-none cursor-pointer"
>
{categories.filter(c => c.id !== 'all').map(c => (
Expand All @@ -525,8 +571,8 @@ export function CommunityForum({ onBack, onAuthClick }: CommunityForumProps) {
</div>

<Button
onClick={handleCreatePost}
disabled={!newTitle.trim() || !newContent.trim() || createSubmitting}
type="submit"
disabled={!isValid || createSubmitting}
className="bg-[#a088ff] hover:bg-[#8f76fa] text-white text-sm"
size="sm"
>
Expand All @@ -538,7 +584,7 @@ export function CommunityForum({ onBack, onAuthClick }: CommunityForumProps) {
Post
</Button>
</div>
</div>
</form>
</motion.div>
)}
</AnimatePresence>
Expand Down
7 changes: 5 additions & 2 deletions app/src/sections/ProblemWorkspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>(null);
const [loading, setLoading] = useState(true);
const [code, setCode] = useState<string>('// Write your code here');
const [language, setLanguage] = useState<string>('javascript');
const [theme, setTheme] = useState<'vs-dark' | 'light'>('vs-dark');
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const [executionResult, setExecutionResult] = useState<any>(null);
const [isExecuting, setIsExecuting] = useState(false);
const consoleRef = useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -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');
Expand All @@ -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
}
}
Expand Down Expand Up @@ -308,6 +310,7 @@ export function ProblemWorkspace({ problemId, onBack }: ProblemWorkspaceProps) {
<div className={`text-lg font-bold ${executionResult.allPassed ? 'text-green-400' : 'text-red-400'}`}>
{executionResult.allPassed ? 'All Test Cases Passed!' : 'Some Test Cases Failed'}
</div>
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
{executionResult.results.map((res: any, idx: number) => (
<div key={idx} className="bg-white/5 p-3 rounded-lg border border-white/10">
<div className="flex items-center justify-between mb-2">
Expand Down
17 changes: 12 additions & 5 deletions app/src/sections/Problems.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export function Problems() {
const bookmarked = new Set<string>();
const notesData: Record<string, string> = {};

// 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);
Expand All @@ -86,6 +87,7 @@ export function Problems() {
// Get all unique tags
const allTags = useMemo(() => {
const tags = new Set<string>();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
allProblems.forEach((p: any) => {
if (p.tags) p.tags.forEach((t: string) => tags.add(t));
});
Expand All @@ -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()));
Expand All @@ -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,
};

Expand All @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -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);
Expand Down
Loading
Loading