-
-
Notifications
You must be signed in to change notification settings - Fork 4
fix: navigation issues, AI explain validation, and user data sync #416
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
Changes from all commits
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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,143 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'use server'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { and, asc, eq } from 'drizzle-orm'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { db } from '@/db'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { aiLearnedTerms } from '@/db/schema/ai'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { getCurrentUser } from '@/lib/auth'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { ExplanationResponse } from '@/lib/ai/prompts'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function normalizeTerm(term: string): string { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return term.toLowerCase().trim(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function saveLearnedTerm( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| term: string, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| explanation: ExplanationResponse | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): Promise<{ success: boolean; error?: string }> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const user = await getCurrentUser(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!user) return { success: false, error: 'Unauthorized' }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const normalized = normalizeTerm(term); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!normalized) return { success: false, error: 'Invalid term' }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await db | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .insert(aiLearnedTerms) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .values({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| userId: user.id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| term: normalized, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| explanationUk: explanation.uk, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| explanationEn: explanation.en, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| explanationPl: explanation.pl, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sortOrder: 0, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .onConflictDoUpdate({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| target: [aiLearnedTerms.userId, aiLearnedTerms.term], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| set: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| explanationUk: explanation.uk, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| explanationEn: explanation.en, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| explanationPl: explanation.pl, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { success: true }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error('[ai] Failed to save learned term:', error); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { success: false, error: 'Failed to save term' }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function getLearnedTerms(): Promise< | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| | { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| success: true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| terms: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| term: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| explanationUk: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| explanationEn: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| explanationPl: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isHidden: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sortOrder: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }[]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| | { success: false; error: string } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const user = await getCurrentUser(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!user) return { success: false, error: 'Unauthorized' }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const rows = await db | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .select({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| term: aiLearnedTerms.term, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| explanationUk: aiLearnedTerms.explanationUk, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| explanationEn: aiLearnedTerms.explanationEn, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| explanationPl: aiLearnedTerms.explanationPl, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isHidden: aiLearnedTerms.isHidden, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sortOrder: aiLearnedTerms.sortOrder, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .from(aiLearnedTerms) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .where(eq(aiLearnedTerms.userId, user.id)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .orderBy(asc(aiLearnedTerms.sortOrder), asc(aiLearnedTerms.createdAt)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { success: true, terms: rows }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error('[ai] Failed to fetch learned terms:', error); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { success: false, error: 'Failed to fetch terms' }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function setTermHidden( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| term: string, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isHidden: boolean | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): Promise<{ success: boolean; error?: string }> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const user = await getCurrentUser(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!user) return { success: false, error: 'Unauthorized' }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const normalized = normalizeTerm(term); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!normalized) return { success: false, error: 'Invalid term' }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await db | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .update(aiLearnedTerms) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .set({ isHidden }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .where( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| and( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| eq(aiLearnedTerms.userId, user.id), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| eq(aiLearnedTerms.term, normalized) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { success: true }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error('[ai] Failed to update term visibility:', error); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { success: false, error: 'Failed to update term' }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function updateTermsOrder( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| orderedTerms: string[] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): Promise<{ success: boolean; error?: string }> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const user = await getCurrentUser(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!user) return { success: false, error: 'Unauthorized' }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await Promise.all( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| orderedTerms.map((term, index) => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| db | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .update(aiLearnedTerms) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .set({ sortOrder: index }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .where( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| and( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| eq(aiLearnedTerms.userId, user.id), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| eq(aiLearnedTerms.term, normalizeTerm(term)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+124
to
+136
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: # First, let's locate and examine the file
git ls-files frontend/actions/ | head -20Repository: DevLoversTeam/devlovers.net Length of output: 180 🏁 Script executed: # Check the actual content around lines 123-135
cat -n frontend/actions/ai.ts | sed -n '110,145p'Repository: DevLoversTeam/devlovers.net Length of output: 1200 🏁 Script executed: # Find database imports and setup in the file
rg "import.*db" frontend/actions/ai.tsRepository: DevLoversTeam/devlovers.net Length of output: 146 🏁 Script executed: # Look for database library setup
fd "*.ts" | xargs rg -l "db\.(transaction|update)" | head -5Repository: DevLoversTeam/devlovers.net Length of output: 467 🏁 Script executed: # Check package.json for database dependencies
cat package.json | grep -A 20 "dependencies"Repository: DevLoversTeam/devlovers.net Length of output: 115 🏁 Script executed: # Look for transaction usage patterns in the codebase
rg "db\.transaction\|\.transaction\(" frontend/ --type ts -A 2Repository: DevLoversTeam/devlovers.net Length of output: 53 🏁 Script executed: # Check the db setup/exports
cat frontend/db/index.ts 2>/dev/null || cat -n frontend/db.ts 2>/dev/null || find . -path "*/db/index.ts" -o -path "*/db.ts" | head -1 | xargs catRepository: DevLoversTeam/devlovers.net Length of output: 2161 🏁 Script executed: # Look at the db schema and setup
find . -name "index.ts" -path "*/db/*" | head -1 | xargs cat -nRepository: DevLoversTeam/devlovers.net Length of output: 2707 🏁 Script executed: # Search for any transaction usage in the codebase
rg "transaction\(" . --type ts -B 2 -A 2Repository: DevLoversTeam/devlovers.net Length of output: 53 🏁 Script executed: # Check Drizzle ORM documentation and verify neon-http transaction support
# First, let's see if there are any existing transaction patterns
rg "db\." frontend/actions/ --type ts | grep -i "transaction\|batch" | head -20Repository: DevLoversTeam/devlovers.net Length of output: 53 🏁 Script executed: # Let's check if there's a package-lock.json or similar to see exact versions
find . -name "package-lock.json" -o -name "pnpm-lock.yaml" -o -name "yarn.lock" | head -1Repository: DevLoversTeam/devlovers.net Length of output: 99 🏁 Script executed: # Check how other files handle multiple updates
rg "Promise\.all.*update\|batch.*update" frontend/actions/ --type ts -B 3 -A 3Repository: DevLoversTeam/devlovers.net Length of output: 53 🏁 Script executed: # Check the drizzle-orm version in package-lock.json
cat ./frontend/package-lock.json | grep -A 2 '"drizzle-orm"' | head -20Repository: DevLoversTeam/devlovers.net Length of output: 169 🏁 Script executed: # Also check for `@neondatabase/serverless` version
cat ./frontend/package-lock.json | grep -A 2 '@neondatabase/serverless' | head -20Repository: DevLoversTeam/devlovers.net Length of output: 902 🏁 Script executed: # Let's also check if there's any documentation or comments about transaction support
rg "transaction" frontend/db/ --type tsRepository: DevLoversTeam/devlovers.net Length of output: 697 🏁 Script executed: # Check if there are other update operations in the codebase to see patterns used
rg "\.update\(" frontend/actions/ --type ts -B 1 -A 5 | head -50Repository: DevLoversTeam/devlovers.net Length of output: 1671 🏁 Script executed: # Verify if drizzle-orm/neon-http supports transactions by checking documentation patterns
# Let's see if there are any workarounds or batch operations documented
rg "batch\|transaction" frontend/db/index.tsRepository: DevLoversTeam/devlovers.net Length of output: 53 🏁 Script executed: # Check if there's any conditional logic for transaction handling
cat frontend/db/index.ts | grep -A 50 "export"Repository: DevLoversTeam/devlovers.net Length of output: 85 🌐 Web query:
💡 Result: No, Drizzle ORM's neon-http driver does not support interactive transactions (like db.transaction) in v0.45.x. It explicitly throws "No transactions support in neon-http driver" as seen in recent issues from September 2025 with Drizzle v0.44.5. Use db.batch for non-interactive multi-statement batches instead, added in v0.29.4. For full transaction support (interactive with rollbacks/savepoints), switch to neon-serverless (WebSocket) driver. Official docs note neon-http is for "single, non-interactive transactions" and recommend neon-serverless for "session or interactive transaction support". No changelog entries in v0.45.0/0.45.1 mention adding transaction support to neon-http. Citations:
🏁 Script executed: # Check if there are any batch operations documented or used elsewhere
rg "\.batch\(" frontend/actions/ --type tsRepository: DevLoversTeam/devlovers.net Length of output: 53 🏁 Script executed: # Let's check the actual error handling and transaction attempts in ai.ts more carefully
cat -n frontend/actions/ai.ts | sed -n '115,145p'Repository: DevLoversTeam/devlovers.net Length of output: 1008 🌐 Web query:
💡 Result: Drizzle ORM's db.batch method executes multiple SQL statements in a single batch call, supported for specific drivers: LibSQL (Turso), Neon, and D1 (Cloudflare). It reduces network round trips for better performance. For LibSQL, it's an implicit transaction: all succeed or all rollback. For D1, statements execute sequentially in auto-commit; failure aborts the sequence. Not supported for standard PostgreSQL/node-postgres due to protocol limitations (extended-query doesn't allow multiple statements). Usage example: const batchResponse = await db.batch([ db.insert(usersTable).values({ id: 1, name: 'John' }).returning({ id: usersTable.id }), db.update(usersTable).set({ name: 'Dan' }).where(eq(usersTable.id, 1)), db.query.usersTable.findMany({}), db.select.from(usersTable).where(eq(usersTable.id, 1)), db.select({ id: usersTable.id, invitedBy: usersTable.invitedBy }).from(usersTable), ]); Supported builders: db.all, db.get, db.values, db.run, db.execute, db.query. .findMany, db.query.Citations:
Use db.batch() instead of Promise.all for atomic ordering updates. The current 💡 Suggested fix try {
- await Promise.all(
- orderedTerms.map((term, index) =>
- db
- .update(aiLearnedTerms)
- .set({ sortOrder: index })
- .where(
- and(
- eq(aiLearnedTerms.userId, user.id),
- eq(aiLearnedTerms.term, normalizeTerm(term))
- )
- )
- )
- );
+ await db.batch(
+ orderedTerms.map((term, index) =>
+ db
+ .update(aiLearnedTerms)
+ .set({ sortOrder: index })
+ .where(
+ and(
+ eq(aiLearnedTerms.userId, user.id),
+ eq(aiLearnedTerms.term, normalizeTerm(term))
+ )
+ )
+ )
+ );
return { success: true };📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { success: true }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error('[ai] Failed to update term order:', error); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { success: false, error: 'Failed to update order' }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,35 +16,52 @@ import { | |
| useState, | ||
| } from 'react'; | ||
|
|
||
| import AIWordHelper from '@/components/q&a/AIWordHelper'; | ||
| import { getCachedTerms } from '@/lib/ai/explainCache'; | ||
| import { | ||
| getHiddenTerms, | ||
| hideTermFromDashboard, | ||
| unhideTermFromDashboard, | ||
| } from '@/lib/ai/hiddenTerms'; | ||
| import { saveTermOrder, sortTermsByOrder } from '@/lib/ai/termOrder'; | ||
| getLearnedTerms, | ||
| setTermHidden, | ||
| updateTermsOrder, | ||
| } from '@/actions/ai'; | ||
| import AIWordHelper from '@/components/q&a/AIWordHelper'; | ||
| import { setCachedExplanation } from '@/lib/ai/explainCache'; | ||
|
|
||
| export function ExplainedTermsCard() { | ||
| const t = useTranslations('dashboard.explainedTerms'); | ||
| const [terms, setTerms] = useState<string[]>([]); | ||
| const [hiddenTerms, setHiddenTerms] = useState<string[]>([]); | ||
| const [loadError, setLoadError] = useState(false); | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| const [isInitialLoad, setIsInitialLoad] = useState(true); | ||
|
|
||
| useEffect(() => { | ||
| const cached = getCachedTerms(); | ||
| const hidden = getHiddenTerms(); | ||
| startTransition(() => { | ||
| setTerms( | ||
| sortTermsByOrder( | ||
| cached.filter(term => !hidden.has(term.toLowerCase().trim())) | ||
| ) | ||
| ); | ||
| setHiddenTerms( | ||
| sortTermsByOrder( | ||
| cached.filter(term => hidden.has(term.toLowerCase().trim())) | ||
| ) | ||
| ); | ||
| }); | ||
| getLearnedTerms() | ||
| .then(result => { | ||
| if (!result.success) { | ||
| setLoadError(true); | ||
| return; | ||
| } | ||
| const visible: string[] = []; | ||
| const hidden: string[] = []; | ||
| for (const row of result.terms) { | ||
| setCachedExplanation(row.term, { | ||
| uk: row.explanationUk, | ||
| en: row.explanationEn, | ||
| pl: row.explanationPl, | ||
| }); | ||
| if (row.isHidden) { | ||
| hidden.push(row.term); | ||
| } else { | ||
| visible.push(row.term); | ||
| } | ||
| } | ||
| startTransition(() => { | ||
| setTerms(visible); | ||
| setHiddenTerms(hidden); | ||
| setIsInitialLoad(false); | ||
| }); | ||
| }) | ||
| .catch(() => { | ||
| setLoadError(true); | ||
| setIsInitialLoad(false); | ||
| }); | ||
| }, []); | ||
| const [showMore, setShowMore] = useState(false); | ||
| const [selectedTerm, setSelectedTerm] = useState<string | null>(null); | ||
|
|
@@ -59,18 +76,28 @@ export function ExplainedTermsCard() { | |
| } | null>(null); | ||
|
|
||
| const handleRemoveTerm = (term: string) => { | ||
| hideTermFromDashboard(term); | ||
| setTerms(prevTerms => prevTerms.filter(t => t !== term)); | ||
| setHiddenTerms(prevHidden => [...prevHidden, term]); | ||
| const prevTerms = terms; | ||
| const prevHidden = hiddenTerms; | ||
| setTerms(prevTerms.filter(t => t !== term)); | ||
| setHiddenTerms([...prevHidden, term]); | ||
| setTermHidden(term, true).catch(() => { | ||
| setTerms(prevTerms); | ||
| setHiddenTerms(prevHidden); | ||
| }); | ||
|
Comment on lines
78
to
+86
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. Refetch authoritative state on mutation failure instead of restoring captured arrays. Each Also applies to: 86-98, 115-123, 216-223 🤖 Prompt for AI Agents |
||
| }; | ||
|
|
||
| const handleRestoreTerm = (term: string) => { | ||
| unhideTermFromDashboard(term); | ||
| setHiddenTerms(prevHidden => prevHidden.filter(t => t !== term)); | ||
| setTerms(prevTerms => { | ||
| const updated = [...prevTerms, term]; | ||
| saveTermOrder(updated); | ||
| return updated; | ||
| const prevTerms = terms; | ||
| const prevHidden = hiddenTerms; | ||
| const updatedTerms = [...prevTerms, term]; | ||
| setTerms(updatedTerms); | ||
| setHiddenTerms(prevHidden.filter(t => t !== term)); | ||
| Promise.all([ | ||
| setTermHidden(term, false), | ||
| updateTermsOrder(updatedTerms), | ||
| ]).catch(() => { | ||
| setTerms(prevTerms); | ||
| setHiddenTerms(prevHidden); | ||
| }); | ||
| }; | ||
|
|
||
|
|
@@ -88,16 +115,15 @@ export function ExplainedTermsCard() { | |
| return; | ||
| } | ||
|
|
||
| setTerms(prevTerms => { | ||
| const newTerms = [...prevTerms]; | ||
| const [dragged] = newTerms.splice(draggedIndex, 1); | ||
| newTerms.splice(targetIndex, 0, dragged); | ||
|
|
||
| saveTermOrder(newTerms); | ||
| return newTerms; | ||
| }); | ||
|
|
||
| const prevTerms = [...terms]; | ||
| const newTerms = [...terms]; | ||
| const [dragged] = newTerms.splice(draggedIndex, 1); | ||
| newTerms.splice(targetIndex, 0, dragged); | ||
| setTerms(newTerms); | ||
| setDraggedIndex(null); | ||
| updateTermsOrder(newTerms).catch(() => { | ||
| setTerms(prevTerms); | ||
| }); | ||
| }; | ||
|
|
||
| const touchDragIndex = useRef<number | null>(null); | ||
|
|
@@ -191,12 +217,13 @@ export function ExplainedTermsCard() { | |
| const toIndex = dragTargetIndex.current; | ||
|
|
||
| if (fromIndex !== null && toIndex !== null && fromIndex !== toIndex) { | ||
| setTerms(prevTerms => { | ||
| const newTerms = [...prevTerms]; | ||
| const [dragged] = newTerms.splice(fromIndex, 1); | ||
| newTerms.splice(toIndex, 0, dragged); | ||
| saveTermOrder(newTerms); | ||
| return newTerms; | ||
| const prevTerms = [...termsRef.current]; | ||
| const newTerms = [...termsRef.current]; | ||
| const [dragged] = newTerms.splice(fromIndex, 1); | ||
| newTerms.splice(toIndex, 0, dragged); | ||
| setTerms(newTerms); | ||
| updateTermsOrder(newTerms).catch(() => { | ||
| setTerms(prevTerms); | ||
| }); | ||
| } | ||
|
|
||
|
|
@@ -255,7 +282,13 @@ export function ExplainedTermsCard() { | |
| </div> | ||
| </div> | ||
|
|
||
| {hasTerms ? ( | ||
| {loadError ? ( | ||
| <div className="py-6 text-center"> | ||
| <p className="text-sm text-red-500 dark:text-red-400"> | ||
| {t('loadError')} | ||
| </p> | ||
| </div> | ||
| ) : hasTerms ? ( | ||
| <> | ||
| <p className="mb-4 text-sm text-gray-500 dark:text-gray-400"> | ||
| {t('termCount', { count: terms.length })} | ||
|
|
@@ -318,7 +351,7 @@ export function ExplainedTermsCard() { | |
| })} | ||
| </div> | ||
| </> | ||
| ) : ( | ||
| ) : !isInitialLoad ? ( | ||
| <div className="py-6 text-center"> | ||
| <p className="text-sm font-medium text-gray-600 dark:text-gray-300"> | ||
| {t('empty')} | ||
|
|
@@ -327,7 +360,7 @@ export function ExplainedTermsCard() { | |
| {t('emptyHint')} | ||
| </p> | ||
| </div> | ||
| )} | ||
| ) : null} | ||
|
|
||
| {/* Explained Terms Section */} | ||
| <div className="mt-6"> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| 'use client'; | ||
|
|
||
| import { useRouter } from 'next/navigation'; | ||
|
|
||
| export default function LegalBackButton({ label }: { label: string }) { | ||
| const router = useRouter(); | ||
|
|
||
| const handleBack = () => { | ||
| if (window.history.length > 1) { | ||
| router.back(); | ||
| } else { | ||
| router.push('/'); | ||
| } | ||
| }; | ||
|
|
||
| return ( | ||
| <button | ||
| type="button" | ||
| onClick={handleBack} | ||
| className="inline-flex text-sm text-slate-600 transition-colors hover:text-blue-600 dark:text-slate-300 dark:hover:text-white" | ||
| > | ||
| ← {label} | ||
| </button> | ||
| ); | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.