Poki01 commited on
Commit
56181a0
·
1 Parent(s): 521f621

Allow custom clipboard codes

Browse files
apps/backend/src/clipboard/clipboard.controller.ts CHANGED
@@ -6,11 +6,38 @@ export class ClipboardController {
6
  constructor(private readonly clipboardService: ClipboardService) { }
7
 
8
  @Post('create')
9
- async createClipboard(@Body() body: { password?: string }) {
 
 
 
 
 
 
 
 
 
10
  try {
11
- const roomCode = await this.clipboardService.createClipboard(body.password);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  return { roomCode };
13
  } catch (error) {
 
 
 
 
14
  throw new HttpException('Failed to create clipboard', HttpStatus.INTERNAL_SERVER_ERROR);
15
  }
16
  }
 
6
  constructor(private readonly clipboardService: ClipboardService) { }
7
 
8
  @Post('create')
9
+ async createClipboard(@Body() body: { password?: string; roomCode?: string }) {
10
+ const requestedRoomCode = body.roomCode?.trim().toUpperCase();
11
+
12
+ if (requestedRoomCode && !/^[A-Z0-9]{4,6}$/.test(requestedRoomCode)) {
13
+ throw new HttpException(
14
+ 'Invalid room code. Must be 4-6 alphanumeric characters.',
15
+ HttpStatus.BAD_REQUEST,
16
+ );
17
+ }
18
+
19
  try {
20
+ const roomCode = requestedRoomCode
21
+ ? requestedRoomCode
22
+ : await this.clipboardService.createClipboard(body.password);
23
+
24
+ if (requestedRoomCode) {
25
+ const created = await this.clipboardService.createClipboardWithCode(
26
+ requestedRoomCode,
27
+ body.password,
28
+ );
29
+
30
+ if (!created) {
31
+ throw new HttpException('Room code already exists', HttpStatus.CONFLICT);
32
+ }
33
+ }
34
+
35
  return { roomCode };
36
  } catch (error) {
37
+ if (error instanceof HttpException) {
38
+ throw error;
39
+ }
40
+
41
  throw new HttpException('Failed to create clipboard', HttpStatus.INTERNAL_SERVER_ERROR);
42
  }
43
  }
apps/frontend/src/components/CreateClipboardCard.tsx CHANGED
@@ -9,6 +9,8 @@ import { apiUrl } from '@/lib/constants';
9
  export default function CreateClipboardCard() {
10
  const [isLoading, setIsLoading] = useState(false);
11
  const [showPasswordModal, setShowPasswordModal] = useState(false);
 
 
12
  const router = useRouter();
13
  const { addToast } = useToast();
14
 
@@ -24,12 +26,35 @@ export default function CreateClipboardCard() {
24
  const createClipboard = async (passwordToUse?: string) => {
25
  setIsLoading(true);
26
  try {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  const response = await fetch(`${apiUrl}/clipboard/create`, {
28
  method: 'POST',
29
  headers: {
30
  'Content-Type': 'application/json',
31
  },
32
- body: passwordToUse ? JSON.stringify({ password: passwordToUse }) : undefined,
33
  });
34
 
35
  if (!response.ok) {
@@ -75,9 +100,34 @@ export default function CreateClipboardCard() {
75
  </div>
76
 
77
  <p className="text-text-secondary my-auto pl-1">
78
- Start a new shared clipboard. A unique 4-character code will be generated for you.
79
  </p>
80
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  <div className="flex mt-auto w-full">
82
  <button
83
  onClick={() => handleCreateClipboard(false)}
 
9
  export default function CreateClipboardCard() {
10
  const [isLoading, setIsLoading] = useState(false);
11
  const [showPasswordModal, setShowPasswordModal] = useState(false);
12
+ const [customCode, setCustomCode] = useState('');
13
+ const [codeError, setCodeError] = useState('');
14
  const router = useRouter();
15
  const { addToast } = useToast();
16
 
 
26
  const createClipboard = async (passwordToUse?: string) => {
27
  setIsLoading(true);
28
  try {
29
+ setCodeError('');
30
+ const roomCode = customCode.trim().toUpperCase();
31
+
32
+ if (roomCode) {
33
+ if (roomCode.length < 4 || roomCode.length > 6) {
34
+ setCodeError('Code must be between 4 and 6 characters.');
35
+ return;
36
+ }
37
+
38
+ if (!/^[A-Z0-9]+$/.test(roomCode)) {
39
+ setCodeError('Only letters and numbers are allowed.');
40
+ return;
41
+ }
42
+ }
43
+
44
+ const payload: Record<string, string> = {};
45
+ if (passwordToUse) {
46
+ payload.password = passwordToUse;
47
+ }
48
+ if (roomCode) {
49
+ payload.roomCode = roomCode;
50
+ }
51
+
52
  const response = await fetch(`${apiUrl}/clipboard/create`, {
53
  method: 'POST',
54
  headers: {
55
  'Content-Type': 'application/json',
56
  },
57
+ body: Object.keys(payload).length ? JSON.stringify(payload) : undefined,
58
  });
59
 
60
  if (!response.ok) {
 
100
  </div>
101
 
102
  <p className="text-text-secondary my-auto pl-1">
103
+ Start a new shared clipboard. Choose your own 4-6 character code or leave it blank to get a random one.
104
  </p>
105
 
106
+ <div className="mt-2">
107
+ <label className="block text-sm font-medium text-text-secondary mb-2 pl-1">Custom code (optional)</label>
108
+ <input
109
+ type="text"
110
+ maxLength={6}
111
+ value={customCode}
112
+ onChange={(e) => {
113
+ const value = e.target.value.toUpperCase().replace(/[^A-Z0-9]/g, '');
114
+ setCustomCode(value);
115
+ if (codeError) setCodeError('');
116
+ }}
117
+ placeholder="e.g. CLIP4"
118
+ 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"
119
+ aria-invalid={!!codeError}
120
+ />
121
+ {codeError && (
122
+ <p className="mt-2 text-sm text-error flex items-center">
123
+ <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
124
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
125
+ </svg>
126
+ {codeError}
127
+ </p>
128
+ )}
129
+ </div>
130
+
131
  <div className="flex mt-auto w-full">
132
  <button
133
  onClick={() => handleCreateClipboard(false)}