From 46224fca79fb177f60bcd8130f26b5e80c742e8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Gierwia=C5=82o?= Date: Thu, 13 Nov 2025 16:11:04 +0100 Subject: [PATCH] feat: add auto-lookup and duplicate check for WSDC registration Enhanced WSDC registration flow with auto-lookup and account validation: Backend changes: - Add accountExists flag to WSDC lookup endpoint - Check database for existing users with WSDC ID - Fix Prisma binary target for Alpine Linux Docker containers Frontend changes: - Auto-lookup WSDC data after entering 4+ digits (500ms debounce) - Show live preview dropdown with dancer information - Display warning if account with WSDC ID already exists - Block registration and suggest login for existing accounts - Improve UX with real-time validation feedback - Add CheckCircle, XCircle, AlertCircle icons for visual feedback This prevents duplicate WSDC ID registrations and provides immediate feedback to users, improving the registration experience. Tested with: - ID 26111 (Vince Yap) - new account allowed - ID 26997 (Radoslaw) - existing account blocked --- backend/prisma/schema.prisma | 3 +- backend/src/controllers/wsdc.js | 9 +- frontend/src/pages/RegisterPage.jsx | 174 ++++++++++++++++++++-------- 3 files changed, 137 insertions(+), 49 deletions(-) diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index f6db193..01a3299 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -2,7 +2,8 @@ // Database: PostgreSQL 15 generator client { - provider = "prisma-client-js" + provider = "prisma-client-js" + binaryTargets = ["native", "linux-musl-openssl-3.0.x"] } datasource db { diff --git a/backend/src/controllers/wsdc.js b/backend/src/controllers/wsdc.js index ba882f6..375ce3c 100644 --- a/backend/src/controllers/wsdc.js +++ b/backend/src/controllers/wsdc.js @@ -3,6 +3,7 @@ * Provides proxy endpoint for World Swing Dance Council (WSDC) dancer lookup */ +const { prisma } = require('../utils/db'); const WSDC_API_BASE = 'https://points.worldsdc.com/lookup2020/find'; /** @@ -61,9 +62,15 @@ exports.lookupDancer = async (req, res) => { dominateRole: data.dominate_data?.short_dominate_role || null }; + // Check if user with this WSDC ID already exists in our database + const existingUser = await prisma.user.findUnique({ + where: { wsdcId: String(data.dancer_wsdcid) } + }); + return res.status(200).json({ success: true, - dancer: dancerData + dancer: dancerData, + accountExists: !!existingUser }); } catch (error) { diff --git a/frontend/src/pages/RegisterPage.jsx b/frontend/src/pages/RegisterPage.jsx index 6837280..4691020 100644 --- a/frontend/src/pages/RegisterPage.jsx +++ b/frontend/src/pages/RegisterPage.jsx @@ -1,8 +1,8 @@ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { useNavigate, Link } from 'react-router-dom'; import { useAuth } from '../contexts/AuthContext'; import { wsdcAPI } from '../services/api'; -import { Video, Mail, Lock, User, Hash, ArrowRight, ArrowLeft, Loader2 } from 'lucide-react'; +import { Video, Mail, Lock, User, Hash, ArrowRight, ArrowLeft, Loader2, CheckCircle, XCircle, AlertCircle } from 'lucide-react'; import PasswordStrengthIndicator from '../components/common/PasswordStrengthIndicator'; const RegisterPage = () => { @@ -13,6 +13,8 @@ const RegisterPage = () => { const [hasWsdcId, setHasWsdcId] = useState(null); const [wsdcId, setWsdcId] = useState(''); const [wsdcData, setWsdcData] = useState(null); + const [wsdcPreview, setWsdcPreview] = useState(null); // Preview data after 4+ digits + const [accountExists, setAccountExists] = useState(false); // Check if account exists const [wsdcLoading, setWsdcLoading] = useState(false); const [wsdcError, setWsdcError] = useState(''); @@ -31,35 +33,68 @@ const RegisterPage = () => { const { register } = useAuth(); const navigate = useNavigate(); - // Handle WSDC ID lookup - const handleWsdcLookup = async () => { - if (!wsdcId || wsdcId.trim() === '') { - setWsdcError('Please enter your WSDC ID'); + // Auto-lookup WSDC ID after 4+ digits are entered + useEffect(() => { + const lookupWsdcId = async () => { + if (wsdcId.length >= 4 && /^\d+$/.test(wsdcId)) { + setWsdcLoading(true); + setWsdcError(''); + setWsdcPreview(null); + setAccountExists(false); + + try { + const response = await wsdcAPI.lookupDancer(wsdcId); + + if (response.success && response.dancer) { + setWsdcPreview(response.dancer); + setAccountExists(response.accountExists || false); + + if (!response.accountExists) { + setWsdcError(''); + } + } + } catch (err) { + setWsdcPreview(null); + setAccountExists(false); + if (err.status === 404) { + setWsdcError('WSDC ID not found in the registry'); + } else { + setWsdcError(''); + } + } finally { + setWsdcLoading(false); + } + } else { + setWsdcPreview(null); + setAccountExists(false); + setWsdcError(''); + } + }; + + const timeoutId = setTimeout(lookupWsdcId, 500); // Debounce 500ms + return () => clearTimeout(timeoutId); + }, [wsdcId]); + + // Handle WSDC ID confirmation and continue to registration + const handleWsdcContinue = () => { + if (!wsdcPreview) { + setWsdcError('Please enter a valid WSDC ID'); return; } - setWsdcLoading(true); - setWsdcError(''); - - try { - const response = await wsdcAPI.lookupDancer(wsdcId); - - if (response.success && response.dancer) { - setWsdcData(response.dancer); - setFormData(prev => ({ - ...prev, - firstName: response.dancer.firstName, - lastName: response.dancer.lastName, - })); - setStep(2); - } else { - setWsdcError('WSDC ID not found. Please check and try again.'); - } - } catch (err) { - setWsdcError(err.data?.message || 'Failed to lookup WSDC ID. Please try again.'); - } finally { - setWsdcLoading(false); + if (accountExists) { + setWsdcError('An account with this WSDC ID already exists'); + return; } + + // Set data and move to registration form + setWsdcData(wsdcPreview); + setFormData(prev => ({ + ...prev, + firstName: wsdcPreview.firstName, + lastName: wsdcPreview.lastName, + })); + setStep(2); }; // Handle "No WSDC ID" option @@ -158,39 +193,84 @@ const RegisterPage = () => { setWsdcId(e.target.value)} + onChange={(e) => setWsdcId(e.target.value.replace(/\D/g, ''))} className="pl-10 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500" placeholder="26997" - disabled={wsdcLoading} + maxLength={10} /> + {wsdcLoading && ( +
+ +
+ )} - {wsdcError && ( -

{wsdcError}

+ + {/* Preview dropdown - shows after 4 digits */} + {wsdcId.length >= 4 && wsdcPreview && !wsdcLoading && ( +
+
+
+
+ + Dancer found +
+
+

WSDC ID: {wsdcPreview.wsdcId}

+

Name: {wsdcPreview.firstName} {wsdcPreview.lastName}

+
+
+
+ + {accountExists && ( +
+ +
+

Account already exists

+

+ An account with this WSDC ID is already registered. + Sign in instead +

+
+
+ )} + + {!accountExists && ( +
+ +

+ Looks good! Click Continue to proceed. +

+
+ )} +
+ )} + + {wsdcError && !wsdcPreview && ( +
+ +

{wsdcError}

+
)}