Skip to content

Commit 1cca658

Browse files
committed
feat: switch to using Input OTP from shadcn-svelte for join code entry
1 parent c609847 commit 1cca658

7 files changed

Lines changed: 114 additions & 36 deletions

File tree

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
FROM oven/bun:1-debian AS base
44
WORKDIR /usr/src/app
55

6-
# For Coolify healthchecks
6+
# for Coolify healthchecks
77
RUN apt-get update && apt-get install -y curl \
88
&& rm -rf /var/lib/apt/lists/*
99

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import Root from "./input-otp.svelte";
2+
import Group from "./input-otp-group.svelte";
3+
import Slot from "./input-otp-slot.svelte";
4+
import Separator from "./input-otp-separator.svelte";
5+
6+
export {
7+
Root,
8+
Group,
9+
Slot,
10+
Separator,
11+
Root as InputOTP,
12+
Group as InputOTPGroup,
13+
Slot as InputOTPSlot,
14+
Separator as InputOTPSeparator
15+
};
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<script lang="ts">
2+
import type { HTMLAttributes } from "svelte/elements";
3+
import { cn, type WithElementRef } from "$lib/utils.js";
4+
5+
let {
6+
ref = $bindable(null),
7+
class: className,
8+
children,
9+
...restProps
10+
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
11+
</script>
12+
13+
<div bind:this={ref} data-slot="input-otp-group" class={cn("flex items-center", className)} {...restProps}>
14+
{@render children?.()}
15+
</div>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<script lang="ts">
2+
import type { HTMLAttributes } from "svelte/elements";
3+
import type { WithElementRef } from "$lib/utils.js";
4+
import DotIcon from "@lucide/svelte/icons/dot";
5+
6+
let { ref = $bindable(null), children, ...restProps }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
7+
</script>
8+
9+
<div bind:this={ref} data-slot="input-otp-separator" role="separator" {...restProps}>
10+
{#if children}
11+
{@render children?.()}
12+
{:else}
13+
<DotIcon />
14+
{/if}
15+
</div>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<script lang="ts">
2+
import { PinInput as InputOTPPrimitive } from "bits-ui";
3+
import { cn } from "$lib/utils.js";
4+
5+
let { ref = $bindable(null), cell, class: className, ...restProps }: InputOTPPrimitive.CellProps = $props();
6+
</script>
7+
8+
<InputOTPPrimitive.Cell
9+
{cell}
10+
bind:ref
11+
data-slot="input-otp-slot"
12+
class={cn(
13+
"border-input aria-invalid:border-destructive dark:bg-input/30 relative flex size-10 items-center justify-center border-y border-r text-sm outline-none transition-all first:rounded-l-md first:border-l last:rounded-r-md",
14+
cell.isActive &&
15+
"border-ring ring-ring/50 aria-invalid:border-destructive dark:aria-invalid:ring-destructive/40 aria-invalid:ring-destructive/20 ring-offset-background z-10 ring-[3px]",
16+
className
17+
)}
18+
{...restProps}
19+
>
20+
{cell.char}
21+
{#if cell.hasFakeCaret}
22+
<div class="pointer-events-none absolute inset-0 flex items-center justify-center">
23+
<div class="animate-caret-blink bg-foreground h-4 w-px duration-1000"></div>
24+
</div>
25+
{/if}
26+
</InputOTPPrimitive.Cell>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<script lang="ts">
2+
import { PinInput as InputOTPPrimitive } from "bits-ui";
3+
import { cn } from "$lib/utils.js";
4+
5+
let {
6+
ref = $bindable(null),
7+
class: className,
8+
value = $bindable(""),
9+
...restProps
10+
}: InputOTPPrimitive.RootProps = $props();
11+
</script>
12+
13+
<InputOTPPrimitive.Root
14+
bind:ref
15+
bind:value
16+
data-slot="input-otp"
17+
class={cn("has-disabled:opacity-50 flex items-center gap-2 [&_input]:disabled:cursor-not-allowed", className)}
18+
{...restProps}
19+
/>
Lines changed: 23 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,34 @@
11
<script lang="ts">
22
import { Button } from "$lib/components/ui/button";
33
import { Header } from "$lib/components/ui/header";
4+
import * as InputOTP from "$lib/components/ui/input-otp";
45
import MoveLeft from "@lucide/svelte/icons/move-left";
56
import { toast } from "svelte-sonner";
7+
import { REGEXP_ONLY_DIGITS_AND_CHARS } from "bits-ui";
68
79
import { goto } from "$app/navigation";
810
911
import { io, type Socket } from "socket.io-client";
1012
import type { RoomCreateClientToServerEvents, RoomCreateServerToClientEvents } from "$lib/mathex/schemas";
1113
const socket: Socket<RoomCreateServerToClientEvents, RoomCreateClientToServerEvents> = io("/rooms");
1214
13-
let code: string[] = $state(Array.from({ length: 8 }).map(() => ""));
15+
let code: string = $state("");
1416
let lastCode: string = "";
15-
async function checkLetter(i: number) {
16-
code[i] = code[i].trim();
17-
if (code[i].length > 1) {
18-
let rest = code[i].split("").slice(1).join("");
19-
code[i] = code[i][0].toUpperCase();
20-
if (code[i + 1] !== undefined) {
21-
code[i + 1] += rest;
22-
document.getElementById("code-input" + (i + 1))?.focus();
23-
checkLetter(i + 1);
24-
}
25-
} else {
26-
code[i] = code[i].toUpperCase();
27-
}
28-
29-
const codeStr = code.join("");
30-
if (codeStr.length === 8 && lastCode !== codeStr) {
31-
lastCode = codeStr;
17+
async function setCode(newCode: string) {
18+
code = newCode.toUpperCase();
19+
if (code.length === 8 && lastCode !== code) {
20+
lastCode = code;
3221
toast.promise(
3322
new Promise<void>(async (resolve, reject) => {
34-
if (await socket.emitWithAck("checkRoom", codeStr)) {
23+
if (await socket.emitWithAck("checkRoom", code)) {
3524
resolve();
3625
} else reject();
3726
}),
3827
{
3928
loading: "Loading...",
4029
success() {
4130
socket.disconnect();
42-
goto("/mathex/app/play/" + codeStr);
31+
goto("/mathex/app/play/" + code);
4332
return "Going to room...";
4433
},
4534
error: "That room does not exist! Try typing the room ID again."
@@ -52,21 +41,20 @@
5241
<div class="flex flex-col h-full w-full justify-center items-center">
5342
<Header size="h1">Join Room</Header>
5443
<div class="flex gap-1 mt-2">
55-
{#each code as letter, i}
56-
<!-- svelte-ignore a11y_autofocus -->
57-
<input
58-
type="text"
59-
id="code-input{i}"
60-
class="w-12 h-12 bg-gray-100 block rounded-sm text-slate-900 text-xl text-center"
61-
autofocus={i === 0}
62-
bind:value={code[i]}
63-
onkeydown={async (e) => {
64-
const prevEl = document.getElementById("code-input" + (i - 1));
65-
if (prevEl && letter === "" && e.key === "Backspace") prevEl.focus();
66-
}}
67-
oninput={() => checkLetter(i)}
68-
/>
69-
{/each}
44+
<InputOTP.Root
45+
maxlength={8}
46+
spellcheck="false"
47+
pattern={REGEXP_ONLY_DIGITS_AND_CHARS}
48+
bind:value={() => code, setCode}
49+
>
50+
{#snippet children({ cells })}
51+
{#each cells as cell (cell)}
52+
<InputOTP.Group>
53+
<InputOTP.Slot class="bg-gray-100 text-slate-900" {cell} />
54+
</InputOTP.Group>
55+
{/each}
56+
{/snippet}
57+
</InputOTP.Root>
7058
</div>
7159
<Button variant="link" class="text-white" href="/mathex/app"><MoveLeft class="mr-1" /> Back to home</Button>
7260
</div>

0 commit comments

Comments
 (0)