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
3 changes: 3 additions & 0 deletions src/components/PeerCard.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useNavigate } from "react-router-dom";
import { motion } from "framer-motion";
import {
Star,
Expand All @@ -23,6 +24,7 @@ const PeerCard = ({
onConnect,
index = 0,
}: PeerCardProps) => {
const navigate = useNavigate();
return (
<motion.div
initial={{ opacity: 0, y: 30 }}
Expand Down Expand Up @@ -178,6 +180,7 @@ const PeerCard = ({
<Button
size="sm"
variant="outline"
onClick={() => navigate(`/profile/${peer.id}`)}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

View Profile navigates to a path that is not routable today.

navigate(\/profile/${peer.id}`) does not match the current router contract (/profile), and src/pages/Profile.tsx` currently reads the authenticated session user, not a URL id. This makes the button behavior incorrect at runtime.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/PeerCard.tsx` at line 183, The onClick handler in the PeerCard
component uses navigate(`/profile/${peer.id}`) which attempts to navigate to a
route that does not exist in the current router configuration. The router only
recognizes the /profile path (without a dynamic ID segment), and Profile.tsx
currently renders the authenticated session user's profile rather than accepting
a user ID from the URL. Change the navigate call from the dynamic route pattern
to simply navigate to /profile, or if peer-specific profile viewing is needed,
coordinate with the router and Profile.tsx page to support the dynamic route
pattern by updating both the route definition and the Profile component to
accept and use the URL parameter.

className="flex-1 rounded-xl border-white/10 bg-white/5 hover:bg-white/10"
>
View Profile
Expand Down
36 changes: 33 additions & 3 deletions src/components/studyroom/LiveCodeRunner.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useState } from "react";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { Play, Code, Share, Loader2 } from "lucide-react";
import { Play, Code, Share, Loader2, Clipboard, Trash2 } from "lucide-react";
import { toast } from "sonner";
import { env } from "@/env";

Expand Down Expand Up @@ -80,7 +80,18 @@ export function LiveCodeRunner({ onShare }: LiveCodeRunnerProps) {
onShare(code, language.id, output);
setIsOpen(false);
};

const handleCopyOutput = () => {
if (!output) { toast.error("No output to copy"); return; }
navigator.clipboard.writeText(output).then(
() => toast.success("Output copied!"),
() => toast.error("Failed to copy output")
);
};

const handleClearCode = () => {
setCode("");
setOutput("");
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
Expand Down Expand Up @@ -113,6 +124,14 @@ export function LiveCodeRunner({ onShare }: LiveCodeRunnerProps) {
</select>

<div className="flex gap-2">
<button
type="button"
onClick={handleClearCode}
disabled={isRunning}
className="flex items-center gap-2 bg-gray-700 text-white px-4 py-2 rounded-lg font-medium hover:bg-gray-600 transition disabled:opacity-50"
>
<Trash2 size={16} /> Clear
</button>
<button
onClick={runCode}
disabled={isRunning}
Expand Down Expand Up @@ -142,7 +161,18 @@ export function LiveCodeRunner({ onShare }: LiveCodeRunnerProps) {
/>
</div>
<div className="flex flex-col gap-2 h-full">
<label className="text-sm font-medium text-gray-400">Output</label>
<div className="flex items-center justify-between">
+ <label className="text-sm font-medium text-gray-400">Output</label>
{output && (
<button
onClick={handleCopyOutput}
disabled={isRunning}
className="flex items-center gap-1 text-xs text-gray-400 hover:text-gray-200 disabled:opacity-50"
>
<Clipboard size={13} /> Copy
</button>
)}
</div>
<div className="w-full h-full bg-black text-green-400 font-mono text-sm p-4 rounded-xl border border-gray-800 overflow-y-auto whitespace-pre-wrap">
{output || "Output will appear here..."}
</div>
Expand Down
63 changes: 38 additions & 25 deletions src/components/studyroom/StudyTimer.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { useState, useEffect } from "react";
import { toast } from "sonner";

export default function StudyTimer() {
const [seconds, setSeconds] = useState(1500);
const [isRunning, setIsRunning] = useState(false);
const [hasCompleted, setHasCompleted] = useState(false);

useEffect(() => {
let timer: NodeJS.Timeout;
Expand All @@ -11,47 +13,59 @@ export default function StudyTimer() {
timer = setInterval(() => {
setSeconds((prev) => prev - 1);
}, 1000);
} else if (isRunning && seconds === 0) {
setIsRunning(false);
setHasCompleted(true);
toast.success("⏰ Study session complete! Great work.", { duration: 5000 });
}

return () => clearInterval(timer);
}, [isRunning, seconds]);

const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
const TOTAL = 1500;
const progress = ((TOTAL - seconds) / TOTAL) * 100;
const isWarning = seconds <= 60 && seconds > 0;



const handleReset = () => {
setSeconds(1500);
setIsRunning(false);
setHasCompleted(false);
};
const handleToggle = () => {
if (seconds === 0) return;
setIsRunning((prev) => !prev);
};

return (
<div className="bg-slate-900 border border-slate-700 rounded-xl p-4">
<h2 className="font-semibold mb-3">
⏳ Collaborative Study Timer
</h2>

<p className="text-3xl font-bold text-center mb-4">
{minutes}:
{remainingSeconds
.toString()
.padStart(2, "0")}
<div className={`bg-slate-900 border rounded-xl p-4 transition-colors ${isWarning ? "border-red-500" : "border-slate-700"}`}>
<h2 className="font-semibold mb-3">⏳ Collaborative Study Timer</h2>

{/* Progress bar */}
<div className="w-full bg-slate-700 rounded-full h-2 mb-3">
<div
className={`h-2 rounded-full transition-all ${hasCompleted ? "bg-green-500" : isWarning ? "bg-red-500" : "bg-blue-500"}`}
style={{ width: `${progress}%` }}
/>
</div>

<p className={`text-3xl font-bold text-center mb-4 ${isWarning ? "text-red-400" : ""}`}>
{minutes}:{remainingSeconds.toString().padStart(2, "0")}
</p>

<div className="flex gap-2">
<button
onClick={() => setIsRunning(true)}
className="flex-1 bg-green-600 hover:bg-green-700 px-3 py-2 rounded-lg text-sm font-medium"
>
Start
</button>

<button
onClick={() => setIsRunning(false)}
className="flex-1 bg-yellow-600 hover:bg-yellow-700 px-3 py-2 rounded-lg text-sm font-medium"
onClick={handleToggle}
disabled={seconds === 0}
className={`flex-1 px-3 py-2 rounded-lg text-sm font-medium disabled:opacity-50 ${
isRunning ? "bg-yellow-600 hover:bg-yellow-700" : "bg-green-600 hover:bg-green-700"
}`}
>
Pause
{isRunning ? "Pause" : "Start"}
</button>

<button
onClick={handleReset}
className="flex-1 bg-red-600 hover:bg-red-700 px-3 py-2 rounded-lg text-sm font-medium"
Expand All @@ -60,5 +74,4 @@ export default function StudyTimer() {
</button>
</div>
</div>
);
}
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Missing closing brace breaks component parsing.

The component ends after ); without a final } for StudyTimer, which causes a compile-time parse failure.

Proposed fix
   return (
     <div className={`bg-slate-900 border rounded-xl p-4 transition-colors ${isWarning ? "border-red-500" : "border-slate-700"}`}>
       ...
     </div>
   );
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
);
);
}
🧰 Tools
🪛 Biome (2.4.16)

[error] 77-77: expected } but instead the file ends

(parse)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/studyroom/StudyTimer.tsx` at line 77, The StudyTimer component
is missing its closing brace at the end of the file. After the final `);`
statement at line 77, add a closing brace `}` to properly close the StudyTimer
function definition. This will resolve the parse error that prevents the
component from compiling.

Source: Linters/SAST tools

47 changes: 32 additions & 15 deletions src/pages/AnonymousDoubts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { supabase } from "@/integrations/supabase/client";
import { useAuth } from "@/contexts/useAuth";
import { toast } from "sonner";
import { Loader2 } from "lucide-react";

const MAX_CHARS = 500;
type Doubt = {
id: string;
content: string;
Expand Down Expand Up @@ -45,6 +45,7 @@ export default function AnonymousDoubts() {
const trimmedText = text.trim();
const trimmedSubject = subject.trim();
if (!trimmedText || !trimmedSubject) return;
if (text.length > MAX_CHARS) { toast.error(`Doubt exceeds ${MAX_CHARS} character limit.`); return; }
if (!user) { toast.error("Please log in to post a doubt."); return; }

setSubmitting(true);
Expand All @@ -67,25 +68,41 @@ export default function AnonymousDoubts() {
setSubmitting(false);
};

const upvote = (id: string) => {
setDoubts(
doubts.map((d) => (d.id === id ? { ...d, upvotes: d.upvotes + 1 } : d))
);
};

const upvote = async (id: string) => {
if (!user) { toast.error("Please log in to upvote."); return; }

// Optimistic UI update
setDoubts((prev) => prev.map((d) => (d.id === id ? { ...d, upvotes: d.upvotes + 1 } : d)));

const { error } = await (supabase as any)
.from("doubts")
.update({ upvotes: doubts.find((d) => d.id === id)!.upvotes + 1 })
.eq("id", id);
Comment on lines +75 to +80

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Look for Supabase SQL functions/migrations that implement atomic upvote increment.
fd -HI -t f --extension sql
rg -n -i "create\\s+function|rpc|upvote|increment|doubts"

# Look for existing client-side RPC usage patterns that could be reused.
rg -n --type ts --type tsx "\\.rpc\\("

# Locate all doubts upvote writes for consistency review.
rg -n --type ts --type tsx "from\\(\"doubts\"\\).*update\\(|upvotes\\s*:"

Repository: durdana3105/peer-learning

Length of output: 31858


🏁 Script executed:

cat -n supabase/migrations/20260608000001_anonymous_doubts.sql

Repository: durdana3105/peer-learning

Length of output: 1176


🏁 Script executed:

sed -n '71,90p' src/pages/AnonymousDoubts.tsx

Repository: durdana3105/peer-learning

Length of output: 850


🏁 Script executed:

rg -n "upvote.*doubts|doubts.*upvote" --type ts --type tsx -A 5 -B 5

Repository: durdana3105/peer-learning

Length of output: 97


🏁 Script executed:

wc -l supabase/migrations/20260608000001_anonymous_doubts.sql

Repository: durdana3105/peer-learning

Length of output: 128


🏁 Script executed:

cat supabase/migrations/20260608000001_anonymous_doubts.sql

Repository: durdana3105/peer-learning

Length of output: 973


🏁 Script executed:

rg -n "upvote.*rpc|rpc.*upvote" --type ts --type tsx --type sql

Repository: durdana3105/peer-learning

Length of output: 97


🏁 Script executed:

rg -n "doubts.*update|update.*doubts" supabase/migrations/ --type sql -A 3 -B 1

Repository: durdana3105/peer-learning

Length of output: 51


🏁 Script executed:

rg -n "update.*doubt|doubt.*update" supabase/migrations/ -A 2 -B 1

Repository: durdana3105/peer-learning

Length of output: 51


🏁 Script executed:

rg -n "create.*function.*upvote|CREATE FUNCTION.*upvote" supabase/migrations/ -i

Repository: durdana3105/peer-learning

Length of output: 51


🏁 Script executed:

rg -n "policy.*update.*doubt|policy.*doubt.*update" supabase/migrations/ -i -A 3

Repository: durdana3105/peer-learning

Length of output: 51


🏁 Script executed:

# Check if there are other doubt-related migrations after the initial one
fd -t f --extension sql supabase/migrations/ | grep -i doubt

Repository: durdana3105/peer-learning

Length of output: 455


🏁 Script executed:

rg -n "UPDATE|policy" supabase/migrations/20260608000001_anonymous_doubts.sql

Repository: durdana3105/peer-learning

Length of output: 168


🏁 Script executed:

cat -n src/pages/AnonymousDoubts.tsx | head -100

Repository: durdana3105/peer-learning

Length of output: 3853


🏁 Script executed:

# Check if there's a bypass or special RLS that allows updates
rg -n "on doubts for update" supabase/migrations/ --type sql -i

Repository: durdana3105/peer-learning

Length of output: 51


🏁 Script executed:

# Compare with resource voting which was mentioned as using atomic SQL
cat -n supabase/migrations/20260601000000_resource_votes_and_saves.sql | grep -A 10 -B 5 "upvotes"

Repository: durdana3105/peer-learning

Length of output: 1906


Use an atomic database operation to increment upvotes, not client-derived absolute values.

The current upvote implementation reads the upvotes value from client state (doubts.find(...).upvotes + 1) and writes it back to the database. This read-modify-write pattern is non-atomic and vulnerable to concurrent vote loss: when multiple users upvote simultaneously, later writes overwrite earlier increments with stale values. Additionally, the non-null assertion is brittle if the item disappears from local state.

Implement atomic upvote increments using SQL-level operations, following the pattern used by resource votes: create an RPC function with a trigger that performs update doubts set upvotes = upvotes + 1 where id = ? as a single atomic database operation, or use Supabase's rpc() interface to delegate the increment to the database.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/AnonymousDoubts.tsx` around lines 75 - 80, Replace the non-atomic
read-modify-write pattern in the upvote handler with an atomic database
operation. Instead of reading the current upvotes value from client state with
doubts.find((d) => d.id === id)!.upvotes + 1 and sending that absolute value to
the database, create a Supabase RPC function (or use the rpc() interface) that
performs the increment atomically at the database level using SQL like update
doubts set upvotes = upvotes + 1 where id = id. This ensures concurrent upvotes
do not overwrite each other and eliminates the brittle non-null assertion.
Follow the same pattern already implemented for resource votes in your codebase.


if (error) {
// Rollback on failure
setDoubts((prev) => prev.map((d) => (d.id === id ? { ...d, upvotes: d.upvotes - 1 } : d)));
toast.error("Failed to register upvote.");
}
};
return (
<div className="p-6 max-w-3xl mx-auto">
<h1 className="text-3xl font-bold mb-6">Anonymous Doubt Posting</h1>

{/* Post a Doubt */}
<div className="border rounded-xl p-4 mb-8 space-y-3">
<textarea
className="border p-2 w-full rounded resize-none"
placeholder="Ask your doubt..."
rows={3}
value={text}
onChange={(e) => setText(e.target.value)}
/>


<div className="relative">

<span className={`absolute bottom-2 right-2 text-xs select-none ${
text.length > MAX_CHARS ? "text-red-500 font-semibold"
: text.length >= MAX_CHARS * 0.9 ? "text-amber-500"
: "text-slate-400"}`}
>
{text.length}/{MAX_CHARS}
</span>
</div>
Comment on lines +96 to +105

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Doubt content input appears removed, which blocks posting.

The form now shows a character counter but no visible control updates text; with disabled={... || !text.trim() ...} the “Post Doubt” action becomes effectively unreachable.

Also applies to: 122-122

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/AnonymousDoubts.tsx` around lines 96 - 105, The character counter
display is present but the actual textarea or input field that captures user
input is missing from this div element with className="relative". Add a textarea
control inside this div (before the character counter span) that accepts user
input, has an onChange handler to update the `text` state variable, includes
appropriate placeholder text and styling to be visible and usable, and respects
the MAX_CHARS limit to prevent exceeding the character boundary. This will allow
users to actually enter doubt content and make the "Post Doubt" button
reachable. The same textarea control needs to be added at the other affected
location as well.

<input
className="border p-2 w-full rounded"
placeholder="Subject Tag (e.g. React, DSA)"
Expand All @@ -102,7 +119,7 @@ export default function AnonymousDoubts() {
</label>
<button
onClick={addDoubt}
disabled={submitting || !text.trim() || !subject.trim()}
disabled={submitting || !text.trim() || !subject.trim() || text.length > MAX_CHARS}
className="bg-blue-500 text-white px-4 py-2 rounded disabled:opacity-50"
>
{submitting ? "Posting..." : "Post Doubt"}
Expand Down