-
Notifications
You must be signed in to change notification settings - Fork 100
Fix/study timer completion feedback #998
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
9d164d0
61b4c6e
29ce73e
dcca98e
5e46ff7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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; | ||||||||
|
|
@@ -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" | ||||||||
|
|
@@ -60,5 +74,4 @@ export default function StudyTimer() { | |||||||
| </button> | ||||||||
| </div> | ||||||||
| </div> | ||||||||
| ); | ||||||||
| } | ||||||||
| ); | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing closing brace breaks component parsing. The component ends after 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
Suggested change
🧰 Tools🪛 Biome (2.4.16)[error] 77-77: expected (parse) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
|
@@ -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); | ||
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 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.sqlRepository: durdana3105/peer-learning Length of output: 1176 🏁 Script executed: sed -n '71,90p' src/pages/AnonymousDoubts.tsxRepository: durdana3105/peer-learning Length of output: 850 🏁 Script executed: rg -n "upvote.*doubts|doubts.*upvote" --type ts --type tsx -A 5 -B 5Repository: durdana3105/peer-learning Length of output: 97 🏁 Script executed: wc -l supabase/migrations/20260608000001_anonymous_doubts.sqlRepository: durdana3105/peer-learning Length of output: 128 🏁 Script executed: cat supabase/migrations/20260608000001_anonymous_doubts.sqlRepository: durdana3105/peer-learning Length of output: 973 🏁 Script executed: rg -n "upvote.*rpc|rpc.*upvote" --type ts --type tsx --type sqlRepository: durdana3105/peer-learning Length of output: 97 🏁 Script executed: rg -n "doubts.*update|update.*doubts" supabase/migrations/ --type sql -A 3 -B 1Repository: durdana3105/peer-learning Length of output: 51 🏁 Script executed: rg -n "update.*doubt|doubt.*update" supabase/migrations/ -A 2 -B 1Repository: durdana3105/peer-learning Length of output: 51 🏁 Script executed: rg -n "create.*function.*upvote|CREATE FUNCTION.*upvote" supabase/migrations/ -iRepository: durdana3105/peer-learning Length of output: 51 🏁 Script executed: rg -n "policy.*update.*doubt|policy.*doubt.*update" supabase/migrations/ -i -A 3Repository: 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 doubtRepository: durdana3105/peer-learning Length of output: 455 🏁 Script executed: rg -n "UPDATE|policy" supabase/migrations/20260608000001_anonymous_doubts.sqlRepository: durdana3105/peer-learning Length of output: 168 🏁 Script executed: cat -n src/pages/AnonymousDoubts.tsx | head -100Repository: 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 -iRepository: 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 ( Implement atomic upvote increments using SQL-level operations, following the pattern used by resource votes: create an RPC function with a trigger that performs 🤖 Prompt for AI Agents |
||
|
|
||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Doubt content input appears removed, which blocks posting. The form now shows a character counter but no visible control updates Also applies to: 122-122 🤖 Prompt for AI Agents |
||
| <input | ||
| className="border p-2 w-full rounded" | ||
| placeholder="Subject Tag (e.g. React, DSA)" | ||
|
|
@@ -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"} | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
View Profilenavigates to a path that is not routable today.navigate(\/profile/${peer.id}`)does not match the current router contract (/profile), andsrc/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