File size: 7,823 Bytes
d988ae4 3bbb98d d988ae4 013a898 d988ae4 013a898 d988ae4 013a898 d988ae4 3bbb98d d988ae4 013a898 d988ae4 013a898 d988ae4 013a898 d988ae4 013a898 d988ae4 013a898 d988ae4 013a898 d988ae4 013a898 d988ae4 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 |
'use client';
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import OtpInput from 'react-otp-input';
import axios from 'axios';
import { useSavedClipboards } from '../lib/hooks/useSavedClipboards';
import SavedClipboardsButton from './SavedClipboardsButton';
import { LogIn, AlertCircle, Loader } from 'lucide-react';
import { apiUrl } from '@/lib/constants';
import { getAdminToken } from '@/lib/adminAuth';
export default function JoinClipboardCard() {
const [roomCode, setRoomCode] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [clipboardExists, setClipboardExists] = useState(true);
const [isComplete, setIsComplete] = useState(false);
const [lastVisitedClipboard, setLastVisitedClipboard] = useState('');
const router = useRouter();
const { addClipboard, savedClipboards } = useSavedClipboards();
// Get the last visited clipboard when component mounts and pre-fill the input
useEffect(() => {
if (savedClipboards.length > 0) {
// Sort by lastVisited date and get the most recent one
const sorted = [...savedClipboards].sort((a, b) =>
new Date(b.lastVisited).getTime() - new Date(a.lastVisited).getTime()
);
const lastCode = sorted[0].roomCode;
setLastVisitedClipboard(lastCode);
// Pre-fill the input with the last visited clipboard code
setRoomCode(lastCode);
// Check if it's valid
const isValid = lastCode.length === 4 && isValidRoomCode(lastCode);
setIsComplete(isValid);
}
}, [savedClipboards]);
// Validate room code format (6 alphanumeric characters, uppercase)
const isValidRoomCode = (code: string): boolean => {
const roomCodeRegex = /^[A-Z0-9]{4}$/;
return roomCodeRegex.test(code);
};
const handleJoinClipboard = async (e?: React.FormEvent) => {
// If called from a form submit, prevent default
if (e) e.preventDefault();
// Reset states
setClipboardExists(true);
// Validate room code is not empty
if (!roomCode.trim()) {
return;
}
// Validate room code format
if (!isValidRoomCode(roomCode)) {
return;
}
// Set loading state and check if clipboard exists
setIsLoading(true);
try {
// Check if clipboard exists
const token = getAdminToken();
const response = await axios.get(`${apiUrl}/clipboard/${roomCode}/exists`, {
headers: token ? { 'x-admin-token': token } : undefined,
params: token ? { token } : undefined,
});
if (response.data.exists) {
// Save to recently visited clipboards
addClipboard(roomCode);
// Clipboard exists, navigate to it
router.push(`/${roomCode}`);
} else {
// Clipboard doesn't exist
setClipboardExists(false);
setIsLoading(false);
}
} catch (error) {
// Error checking clipboard existence
setClipboardExists(false);
setIsLoading(false);
}
};
return (
<div className="flex-1 p-6 md:p-8 rounded-xl border border-surface-hover bg-surface/50 backdrop-blur-sm shadow-lg hover:shadow-secondary/20 hover:border-secondary/50 transition-all duration-300 ease-out relative overflow-hidden group">
{/* Background elements */}
<div className="absolute -top-10 -right-10 w-32 h-32 bg-secondary/10 rounded-full animate-pulse-slow group-hover:scale-110 transition-transform duration-500"></div>
<div className="absolute -bottom-16 -left-16 w-40 h-40 bg-secondary/5 rounded-full opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
<div className="relative z-10">
<div className="flex items-center mb-4">
<div className="w-10 h-10 bg-secondary/10 rounded-full flex items-center justify-center mr-3">
<LogIn className="h-5 w-5 text-secondary" />
</div>
<h2 className="text-2xl font-semibold text-text-primary">Join Clipboard</h2>
</div>
<p className="text-text-secondary mb-6 pl-1">
Enter a 4-character code to join an existing clipboard.
</p>
<form onSubmit={handleJoinClipboard} className="space-y-4">
<div>
<div className="mb-2">
<OtpInput
inputStyle={{
width: '50px',
height: '50px',
}}
onPaste={(e) => {
e.preventDefault()
const pastedValue = e.clipboardData?.getData('text') || '';
const filteredValue = pastedValue.replace(/[^a-zA-Z0-9]/g, '').toUpperCase();
if (filteredValue.length === 4) {
setRoomCode(filteredValue);
}
}}
value={roomCode}
placeholder={lastVisitedClipboard}
onChange={(value: string) => {
// Convert to uppercase automatically
const uppercaseValue = value.toUpperCase();
setRoomCode(uppercaseValue);
setClipboardExists(true);
const isValid = uppercaseValue.length === 4 && isValidRoomCode(uppercaseValue);
setIsComplete(isValid)
}}
numInputs={4}
renderInput={(props, index) => (
<input
{...props}
className={`w-full h-full text-center text-xl sm:text-2xl font-mono font-bold
border-2 rounded-lg shadow-none outline-none
transition-colors duration-200 ease-in-out
${roomCode.length === 4 && isComplete
? 'border-green-500 text-green-500 bg-green-500/5'
: props.value
? 'border-secondary text-text-primary bg-surface/80'
: 'border-surface-hover text-text-primary bg-surface/50 hover:border-primary/30'
}`}
key={index}
/>
)}
inputType="text"
shouldAutoFocus
containerStyle="flex justify-center gap-2 sm:gap-3"
/>
</div>
{!clipboardExists && (
<div className="mt-3 text-error text-sm flex items-center justify-center">
<AlertCircle className="h-4 w-4 mr-1 flex-shrink-0" />
<span>Clipboard does not exist.</span>
</div>
)}
</div>
<div className="flex gap-2 mt-4">
<button
type="submit"
disabled={isLoading || !isComplete}
className="flex-1 py-3 px-4 bg-secondary hover:bg-secondary/90 text-white font-medium rounded-lg flex items-center justify-center transition-all duration-200 shadow-md hover:shadow-lg focus:outline-none focus:ring-2 focus:ring-secondary/30 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isLoading ? (
<>
<Loader className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" />
Joining...
</>
) : (
<>
<LogIn className="h-5 w-5 mr-2" />
Join Clipboard
</>
)}
</button>
<SavedClipboardsButton
onSelectClipboard={(code) => {
setRoomCode(code);
setClipboardExists(true);
setIsComplete(true);
// Don't automatically join - just fill the input
// This allows users to see the code before joining
}}
/>
</div>
</form>
</div>
</div>
);
}
|