fix test
This commit is contained in:
@@ -346,11 +346,22 @@ export function TestSession() {
|
|||||||
[answers],
|
[answers],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const [showSubmitConfirm, setShowSubmitConfirm] = useState(false)
|
||||||
|
|
||||||
const handleSubmit = useCallback(() => {
|
const handleSubmit = useCallback(() => {
|
||||||
submitExam(totalSeconds > 0 ? totalSeconds - timeLeft : timeUsed)
|
submitExam(totalSeconds > 0 ? totalSeconds - timeLeft : timeUsed)
|
||||||
navigate({ to: '/toeic/result' })
|
navigate({ to: '/toeic/result' })
|
||||||
}, [submitExam, navigate, totalSeconds, timeLeft, timeUsed])
|
}, [submitExam, navigate, totalSeconds, timeLeft, timeUsed])
|
||||||
|
|
||||||
|
// Manual click → confirm if any unanswered; auto (timer expire) skips confirm.
|
||||||
|
const requestSubmit = useCallback(() => {
|
||||||
|
if (totalQuestions > 0 && answeredCount < totalQuestions) {
|
||||||
|
setShowSubmitConfirm(true)
|
||||||
|
} else {
|
||||||
|
handleSubmit()
|
||||||
|
}
|
||||||
|
}, [totalQuestions, answeredCount, handleSubmit])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (parts.length === 0) return
|
if (parts.length === 0) return
|
||||||
const id = setInterval(() => {
|
const id = setInterval(() => {
|
||||||
@@ -398,7 +409,7 @@ export function TestSession() {
|
|||||||
timeUsed={timeUsed}
|
timeUsed={timeUsed}
|
||||||
totalQuestions={totalQuestions}
|
totalQuestions={totalQuestions}
|
||||||
answeredCount={answeredCount}
|
answeredCount={answeredCount}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={requestSubmit}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex flex-1 overflow-hidden">
|
<div className="flex flex-1 overflow-hidden">
|
||||||
@@ -497,6 +508,49 @@ export function TestSession() {
|
|||||||
onPrev={() => setCurrentPart(currentPartIndex - 1)}
|
onPrev={() => setCurrentPart(currentPartIndex - 1)}
|
||||||
onNext={() => setCurrentPart(currentPartIndex + 1)}
|
onNext={() => setCurrentPart(currentPartIndex + 1)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{showSubmitConfirm && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-50 flex items-center justify-center px-4"
|
||||||
|
style={{ background: 'rgba(15, 17, 20, 0.5)' }}
|
||||||
|
onClick={() => setShowSubmitConfirm(false)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="w-full max-w-md rounded-2xl p-6 shadow-2xl"
|
||||||
|
style={{ background: 'var(--at-surface)', border: '1px solid var(--at-line)' }}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="at-serif"
|
||||||
|
style={{ fontSize: 22, fontWeight: 500, letterSpacing: '-0.02em', color: 'var(--at-ink)', marginBottom: 8 }}
|
||||||
|
>
|
||||||
|
Nộp bài <i style={{ fontStyle: 'italic', color: 'var(--at-brand)' }}>ngay?</i>
|
||||||
|
</div>
|
||||||
|
<p style={{ fontSize: 14, color: 'var(--at-mute)', lineHeight: 1.55, marginBottom: 20 }}>
|
||||||
|
Bạn còn{' '}
|
||||||
|
<b style={{ color: 'var(--at-bad)' }}>{totalQuestions - answeredCount}</b>/{totalQuestions} câu
|
||||||
|
chưa trả lời. Các câu chưa trả lời sẽ tính là{' '}
|
||||||
|
<b>bỏ qua</b> và không có điểm.
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-2 justify-end">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowSubmitConfirm(false)}
|
||||||
|
className="px-4 py-2 rounded-lg text-sm font-semibold transition-colors hover:bg-[var(--at-line-2)]"
|
||||||
|
style={{ background: 'var(--at-surface)', border: '1px solid var(--at-line)', color: 'var(--at-ink-2)' }}
|
||||||
|
>
|
||||||
|
Tiếp tục làm
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => { setShowSubmitConfirm(false); handleSubmit() }}
|
||||||
|
className="px-4 py-2 rounded-lg text-sm font-semibold text-white transition-[filter] hover:brightness-110"
|
||||||
|
style={{ background: '#e53935' }}
|
||||||
|
>
|
||||||
|
Nộp bài
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
102
supabase/migrations/007_fix_gamification_trigger.sql
Normal file
102
supabase/migrations/007_fix_gamification_trigger.sql
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
-- Migration 007: Fix gamification trigger that blocks auth signup
|
||||||
|
--
|
||||||
|
-- Problem: trigger `on_auth_user_created_gamification` fires after every
|
||||||
|
-- auth.users INSERT and writes to user_gamification + xu_transactions. If those
|
||||||
|
-- tables are missing or schema-drifted, the trigger raises and the entire
|
||||||
|
-- signup transaction rolls back → POST /auth/v1/signup returns 500.
|
||||||
|
--
|
||||||
|
-- Fix:
|
||||||
|
-- 1. Re-create gamification tables idempotently so the trigger has a target.
|
||||||
|
-- 2. Wrap inserts in BEGIN/EXCEPTION block so any future schema break logs a
|
||||||
|
-- warning instead of breaking auth signup.
|
||||||
|
-- 3. Replace the trigger function in place (DROP not needed because of CREATE
|
||||||
|
-- OR REPLACE).
|
||||||
|
--
|
||||||
|
-- Run in Supabase Dashboard → SQL Editor.
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 1. Ensure tables exist (idempotent — same shape as 002_gamification.sql)
|
||||||
|
-- ============================================================
|
||||||
|
CREATE TABLE IF NOT EXISTS user_gamification (
|
||||||
|
user_id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||||
|
xp INT NOT NULL DEFAULT 0,
|
||||||
|
level TEXT NOT NULL DEFAULT 'beginner'
|
||||||
|
CHECK (level IN ('beginner', 'bronze', 'silver', 'gold', 'master')),
|
||||||
|
streak INT NOT NULL DEFAULT 0,
|
||||||
|
longest_streak INT NOT NULL DEFAULT 0,
|
||||||
|
last_active DATE,
|
||||||
|
xu INT NOT NULL DEFAULT 50,
|
||||||
|
freeze_count INT NOT NULL DEFAULT 0,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS xu_transactions (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||||
|
type TEXT NOT NULL
|
||||||
|
CHECK (type IN (
|
||||||
|
'earn_welcome', 'earn_daily', 'earn_streak', 'earn_ads',
|
||||||
|
'spend_freeze', 'spend_writing', 'spend_test'
|
||||||
|
)),
|
||||||
|
amount INT NOT NULL,
|
||||||
|
balance INT NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_xu_transactions_user_date
|
||||||
|
ON xu_transactions(user_id, created_at DESC);
|
||||||
|
|
||||||
|
-- Re-apply RLS (idempotent — OR REPLACE not supported on policies, so drop-then-create)
|
||||||
|
ALTER TABLE user_gamification ENABLE ROW LEVEL SECURITY;
|
||||||
|
DROP POLICY IF EXISTS "Users can read own gamification" ON user_gamification;
|
||||||
|
DROP POLICY IF EXISTS "Users can insert own gamification" ON user_gamification;
|
||||||
|
DROP POLICY IF EXISTS "Users can update own gamification" ON user_gamification;
|
||||||
|
CREATE POLICY "Users can read own gamification"
|
||||||
|
ON user_gamification FOR SELECT USING (auth.uid() = user_id);
|
||||||
|
CREATE POLICY "Users can insert own gamification"
|
||||||
|
ON user_gamification FOR INSERT WITH CHECK (auth.uid() = user_id);
|
||||||
|
CREATE POLICY "Users can update own gamification"
|
||||||
|
ON user_gamification FOR UPDATE USING (auth.uid() = user_id);
|
||||||
|
|
||||||
|
ALTER TABLE xu_transactions ENABLE ROW LEVEL SECURITY;
|
||||||
|
DROP POLICY IF EXISTS "Users can read own xu transactions" ON xu_transactions;
|
||||||
|
DROP POLICY IF EXISTS "Users can insert own xu transactions" ON xu_transactions;
|
||||||
|
CREATE POLICY "Users can read own xu transactions"
|
||||||
|
ON xu_transactions FOR SELECT USING (auth.uid() = user_id);
|
||||||
|
CREATE POLICY "Users can insert own xu transactions"
|
||||||
|
ON xu_transactions FOR INSERT WITH CHECK (auth.uid() = user_id);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 2. Replace trigger function with one that NEVER blocks signup
|
||||||
|
-- ============================================================
|
||||||
|
-- Why the EXCEPTION wrapper:
|
||||||
|
-- A trigger error inside auth.users INSERT rolls back the whole transaction,
|
||||||
|
-- meaning a broken gamification side-effect kills the user's ability to sign
|
||||||
|
-- up. We swallow gamification failures and log a warning so signup always
|
||||||
|
-- succeeds; missing rows can be backfilled later.
|
||||||
|
CREATE OR REPLACE FUNCTION handle_new_user_gamification()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO user_gamification (user_id, xu)
|
||||||
|
VALUES (NEW.id, 50)
|
||||||
|
ON CONFLICT (user_id) DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO xu_transactions (user_id, type, amount, balance, description)
|
||||||
|
VALUES (NEW.id, 'earn_welcome', 50, 50, 'Welcome bonus');
|
||||||
|
EXCEPTION WHEN OTHERS THEN
|
||||||
|
RAISE WARNING 'gamification side-effect failed for user %: %', NEW.id, SQLERRM;
|
||||||
|
END;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 3. Re-attach the trigger (idempotent)
|
||||||
|
-- ============================================================
|
||||||
|
DROP TRIGGER IF EXISTS on_auth_user_created_gamification ON auth.users;
|
||||||
|
CREATE TRIGGER on_auth_user_created_gamification
|
||||||
|
AFTER INSERT ON auth.users
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION handle_new_user_gamification();
|
||||||
Reference in New Issue
Block a user