File size: 8,009 Bytes
d988ae4 3bbb98d d988ae4 56181a0 d988ae4 56181a0 d988ae4 3bbb98d d988ae4 3bbb98d 56181a0 d988ae4 56181a0 d988ae4 56181a0 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 |
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import PasswordModal from './PasswordModal';
import { useToast } from './ui/Toast';
import { apiUrl } from '@/lib/constants';
import { withAdminTokenHeader } from '@/lib/adminAuth';
export default function CreateClipboardCard() {
const [isLoading, setIsLoading] = useState(false);
const [showPasswordModal, setShowPasswordModal] = useState(false);
const [customCode, setCustomCode] = useState('');
const [codeError, setCodeError] = useState('');
const router = useRouter();
const { addToast } = useToast();
const handleCreateClipboard = async (withPassword = false) => {
if (withPassword) {
setShowPasswordModal(true);
return;
}
await createClipboard();
};
const createClipboard = async (passwordToUse?: string) => {
setIsLoading(true);
try {
setCodeError('');
const roomCode = customCode.trim().toUpperCase();
if (roomCode) {
if (roomCode.length < 4 || roomCode.length > 6) {
setCodeError('Code must be between 4 and 6 characters.');
return;
}
if (!/^[A-Z0-9]+$/.test(roomCode)) {
setCodeError('Only letters and numbers are allowed.');
return;
}
}
const payload: Record<string, string> = {};
if (passwordToUse) {
payload.password = passwordToUse;
}
if (roomCode) {
payload.roomCode = roomCode;
}
const response = await fetch(`${apiUrl}/clipboard/create`, {
method: 'POST',
headers: withAdminTokenHeader({
'Content-Type': 'application/json',
}),
body: Object.keys(payload).length ? JSON.stringify(payload) : undefined,
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || 'Failed to create clipboard');
}
const data = await response.json();
if (data.roomCode) {
router.push(`/${data.roomCode}`);
} else {
throw new Error('Room code not received from server');
}
} catch (err: any) {
console.error('Error creating clipboard:', err);
addToast(err.message || 'An unexpected error occurred', 'error', 'Error');
} finally {
setIsLoading(false);
setShowPasswordModal(false);
}
};
const handlePasswordSubmit = (password: string) => {
createClipboard(password);
};
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-glow-sm transition-all duration-300 ease-out relative overflow-hidden group">
{/* Background elements */}
<div className="absolute -top-10 -left-10 w-32 h-32 bg-primary/10 rounded-full animate-pulse-slow group-hover:scale-110 transition-transform duration-500"></div>
<div className="absolute -bottom-16 -right-16 w-40 h-40 bg-primary/5 rounded-full opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
<div className="relative z-10 flex flex-col h-full gap-10 md:gap-0">
<div className="flex items-center mb-auto">
<div className="w-10 h-10 bg-primary/10 rounded-full flex items-center justify-center mr-3">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
</div>
<h2 className="text-2xl font-semibold text-text-primary">New Clipboard</h2>
</div>
<p className="text-text-secondary my-auto pl-1">
Start a new shared clipboard. Choose your own 4-6 character code or leave it blank to get a random one.
</p>
<div className="mt-2">
<label className="block text-sm font-medium text-text-secondary mb-2 pl-1">Custom code (optional)</label>
<input
type="text"
maxLength={6}
value={customCode}
onChange={(e) => {
const value = e.target.value.toUpperCase().replace(/[^A-Z0-9]/g, '');
setCustomCode(value);
if (codeError) setCodeError('');
}}
placeholder="e.g. CLIP4"
className="w-full px-4 py-3 rounded-lg bg-surface/50 border-2 border-surface-hover focus:border-primary focus:outline-none focus:ring-0 transition-colors duration-300 ease-in-out text-text-primary placeholder-text-secondary/50 font-mono"
aria-invalid={!!codeError}
/>
{codeError && (
<p className="mt-2 text-sm text-error flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
{codeError}
</p>
)}
</div>
<div className="flex mt-auto w-full">
<button
onClick={() => handleCreateClipboard(false)}
disabled={isLoading}
className="flex-grow py-3 px-4 bg-primary hover:bg-primary/90 text-white font-medium rounded-l-lg flex items-center justify-center transition-all duration-200 shadow-md hover:shadow-lg focus:outline-none focus:ring-2 focus:ring-primary/30 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isLoading ? (
<span className="flex items-center justify-center">
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Creating...
</span>
) : (
<>
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Create Clipboard
</>
)}
</button>
<button
onClick={() => handleCreateClipboard(true)}
disabled={isLoading}
className="w-12 py-3 px-0 bg-primary hover:bg-primary/90 text-white font-medium rounded-r-lg flex items-center justify-center transition-all duration-200 shadow-md hover:shadow-lg focus:outline-none focus:ring-2 focus:ring-primary/30 disabled:opacity-50 disabled:cursor-not-allowed border-l border-primary-dark"
aria-label="Create password-protected clipboard"
title="Create with password"
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
</button>
</div>
</div>
{/* Error is now displayed as a toast notification */}
</div>
{/* Password Modal */}
<PasswordModal
isOpen={showPasswordModal}
onClose={() => setShowPasswordModal(false)}
onSubmit={handlePasswordSubmit}
isLoading={isLoading}
/>
</>
);
}
|