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
20 changes: 20 additions & 0 deletions app/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"input-otp": "^1.4.2",
"lucide-react": "^0.562.0",
"next-themes": "^0.4.6",
"prism-react-renderer": "^2.4.1",
"react": "^19.2.0",
"react-day-picker": "^9.13.0",
"react-dom": "^19.2.0",
Expand Down
11 changes: 8 additions & 3 deletions app/src/api/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,16 @@ export const getProblemsByTopic = async (topicId: string) => {

/**
* Fetches every problem in the system, ordered by index.
* When page/limit are provided, returns paginated response.
*
* @returns A promise resolving to an array of all problem objects.
* @returns A promise resolving to an array of all problem objects, or a paginated object.
*/
export const getAllProblems = async () => {
const response = await axios.get(`${API_BASE_URL}/api/content/problems`);
export const getAllProblems = async (page?: number, limit?: number) => {
const params = new URLSearchParams();
if (page) params.set('page', String(page));
if (limit) params.set('limit', String(limit));
const qs = params.toString();
const response = await axios.get(`${API_BASE_URL}/api/content/problems${qs ? `?${qs}` : ''}`);
return response.data;
};

Expand Down
131 changes: 118 additions & 13 deletions app/src/sections/Notes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,17 @@ import {
X,
FileText,
Clock,
BookOpen
BookOpen,
Eye,
Code2
} from 'lucide-react';
import { Input } from '@/components/ui/input';
import { toast } from 'sonner';
import { getUserProgress, updateNotes } from '@/api/userActions';
import { getAllProblems } from '@/api/content';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { Highlight, themes } from 'prism-react-renderer';
import {
AlertDialog,
AlertDialogContent,
Expand Down Expand Up @@ -42,6 +47,7 @@ export function Notes() {
const [isEditing, setIsEditing] = useState(false);
const [isCreating, setIsCreating] = useState(false);
const [editForm, setEditForm] = useState({ content: '' });
const [isPreview, setIsPreview] = useState(false);

interface Problem {
id: string;
Expand Down Expand Up @@ -106,6 +112,7 @@ export function Notes() {
const handleCreate = () => {
setIsCreating(true);
setIsEditing(true);
setIsPreview(false);
setSelectedNote(null);
setEditForm({ content: '' });
setNewNoteProblemId('');
Expand All @@ -116,6 +123,7 @@ export function Notes() {
setSelectedNote(note);
setIsEditing(true);
setIsCreating(false);
setIsPreview(false);
setEditForm({ content: note.content });
};

Expand Down Expand Up @@ -325,6 +333,16 @@ export function Notes() {
{isCreating ? 'Create Note' : 'Edit Note'}
</h3>
<div className="flex gap-2">
<button
onClick={() => setIsPreview(!isPreview)}
className={`px-3 py-1.5 rounded-lg flex items-center gap-1.5 text-sm transition-colors ${isPreview
? 'bg-[#a088ff]/20 text-[#a088ff]'
: 'text-white/60 hover:text-white hover:bg-white/5'
}`}
>
{isPreview ? <Code2 className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
{isPreview ? 'Edit' : 'Preview'}
</button>
<button
onClick={handleCancel}
className="px-3 py-1.5 rounded-lg text-white/60 hover:text-white hover:bg-white/5 transition-colors"
Expand Down Expand Up @@ -388,15 +406,68 @@ export function Notes() {
</div>
)}

<div>
<label className="text-sm text-white/60 mb-1 block">Notes</label>
<textarea
value={editForm.content}
onChange={(e) => setEditForm({ ...editForm, content: e.target.value })}
placeholder="Write your notes here... Key insights, approach, time complexity, etc."
rows={15}
className="w-full px-4 py-3 rounded-xl bg-white/5 border border-white/10 text-white placeholder:text-white/30 focus:outline-none focus:border-[#a088ff] resize-none font-mono text-sm"
/>
<div className="lg:grid lg:grid-cols-2 lg:gap-4">
{/* Editor */}
<div className={!isPreview ? '' : 'hidden lg:block'}>
<label className="text-sm text-white/60 mb-1 block">Notes</label>
<textarea
value={editForm.content}
onChange={(e) => setEditForm({ ...editForm, content: e.target.value })}
placeholder="Write your notes here... Supports **markdown** formatting"
rows={15}
className="w-full px-4 py-3 rounded-xl bg-white/5 border border-white/10 text-white placeholder:text-white/30 focus:outline-none focus:border-[#a088ff] resize-none font-mono text-sm"
/>
</div>

{/* Preview */}
<div className={isPreview ? '' : 'hidden lg:block'}>
<label className="text-sm text-white/60 mb-1 block">Preview</label>
<div className="w-full px-4 py-3 rounded-xl bg-white/5 border border-white/10 min-h-[39rem] overflow-y-auto">
{editForm.content ? (
<div className="prose prose-invert prose-sm max-w-none">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
code({ className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '');
const codeString = String(children).replace(/\n$/, '');
if (match) {
return (
<Highlight
theme={themes.nightOwl}
code={codeString}
language={match[1]}
>
{({ className, style, tokens, getLineProps, getTokenProps }) => (
<pre className={className} style={{ ...style, background: 'transparent', padding: '1rem', overflowX: 'auto' }}>
{tokens.map((line, i) => (
<div key={i} {...getLineProps({ line })}>
{line.map((token, key) => (
<span key={key} {...getTokenProps({ token })} />
))}
</div>
))}
</pre>
)}
</Highlight>
);
}
return (
<code className={className} {...props}>
{children}
</code>
);
}
}}
>
{editForm.content}
</ReactMarkdown>
</div>
) : (
<p className="text-white/30 text-sm italic">Nothing to preview</p>
)}
</div>
</div>
</div>
</div>
</div>
Expand Down Expand Up @@ -428,10 +499,44 @@ export function Notes() {
</div>
</div>

<div className="prose prose-invert max-w-none">
<div className="markdown-content text-white/80 whitespace-pre-wrap">
<div className="prose prose-invert prose-sm max-w-none">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
code({ className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '');
const codeString = String(children).replace(/\n$/, '');
if (match) {
return (
<Highlight
theme={themes.nightOwl}
code={codeString}
language={match[1]}
>
{({ className, style, tokens, getLineProps, getTokenProps }) => (
<pre className={className} style={{ ...style, background: 'transparent', padding: '1rem', overflowX: 'auto' }}>
{tokens.map((line, i) => (
<div key={i} {...getLineProps({ line })}>
{line.map((token, key) => (
<span key={key} {...getTokenProps({ token })} />
))}
</div>
))}
</pre>
)}
</Highlight>
);
}
return (
<code className={className} {...props}>
{children}
</code>
);
}
}}
>
{selectedNote.content}
</div>
</ReactMarkdown>
</div>
</div>
) : (
Expand Down
44 changes: 36 additions & 8 deletions backend/src/controllers/contentController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,21 +135,49 @@ export const getProblemsByTopic = async (req: Request, res: Response) => {
};

/**
* @desc Get all problems
* @route GET /api/content/problems
* @desc Get all problems (optionally paginated)
* @route GET /api/content/problems?page=1&limit=20
* @access Public
*
* Fetches every problem in the system, ordered by display index.
* When called without query params, returns a plain JSON array (backward compat).
* When called with `page` and/or `limit`, returns paginated response with metadata.
*
* @param req - Express request object.
* @param res - Express response. Returns a JSON array of all problem objects.
* @param req - Express request. Accepts optional `page` and `limit` query params.
* @param res - Express response. Returns an array or paginated object.
*/
export const getAllProblems = async (req: Request, res: Response) => {
try {
const problems = await prisma.problem.findMany({
orderBy: { order_index: 'asc' }
const pageParam = req.query.page as string;
const limitParam = req.query.limit as string;

// No pagination params → return all (backward compat for Notes, Dashboard, etc.)
if (!pageParam && !limitParam) {
const problems = await prisma.problem.findMany({
orderBy: { order_index: 'asc' }
});
return res.json(problems);
}

const page = Math.max(1, parseInt(pageParam) || 1);
const limit = Math.min(100, Math.max(1, parseInt(limitParam) || 20));
const skip = (page - 1) * limit;

const [problems, total] = await Promise.all([
prisma.problem.findMany({
orderBy: { order_index: 'asc' },
skip,
take: limit
}),
prisma.problem.count()
]);

res.json({
problems,
total,
page,
limit,
totalPages: Math.ceil(total / limit)
});
res.json(problems);
} catch (error) {
res.status(500).json({ message: 'Server Error' });
}
Expand Down
Loading