|
|
@@ -0,0 +1,101 @@
|
|
|
+"use client";
|
|
|
+
|
|
|
+import { useState } from "react";
|
|
|
+import { useMutation } from "@tanstack/react-query";
|
|
|
+import { useRouter } from "next/navigation";
|
|
|
+import { DISPLAY_NAME_MAX_LENGTH } from "@/lib/constants";
|
|
|
+import { getSupabaseBrowserClient } from "@/lib/supabase/client";
|
|
|
+import { AvatarColorPicker } from "./avatar-color-picker";
|
|
|
+
|
|
|
+function validateDisplayName(name: string): string | null {
|
|
|
+ const trimmed = name.trim();
|
|
|
+ if (trimmed.length === 0) return "Display name is required";
|
|
|
+ if (trimmed.length > DISPLAY_NAME_MAX_LENGTH)
|
|
|
+ return `Display name must be ${DISPLAY_NAME_MAX_LENGTH} characters or less`;
|
|
|
+ if (/<|>/.test(trimmed)) return "Display name cannot contain < or >";
|
|
|
+ return null;
|
|
|
+}
|
|
|
+
|
|
|
+export function DisplayNameForm() {
|
|
|
+ const router = useRouter();
|
|
|
+ const [displayName, setDisplayName] = useState("");
|
|
|
+ const [avatarColor, setAvatarColor] = useState<string | null>(null);
|
|
|
+ const [validationError, setValidationError] = useState<string | null>(null);
|
|
|
+
|
|
|
+ const signUpMutation = useMutation({
|
|
|
+ mutationFn: async ({ name, color }: { name: string; color: string | null }) => {
|
|
|
+ const supabase = getSupabaseBrowserClient();
|
|
|
+
|
|
|
+ const { data: authData, error: authError } = await supabase.auth.signInAnonymously();
|
|
|
+ if (authError) throw authError;
|
|
|
+ if (!authData.user) throw new Error("Sign-in failed: no user returned");
|
|
|
+
|
|
|
+ const { error: insertError } = await supabase.from("users").insert({
|
|
|
+ id: authData.user.id,
|
|
|
+ display_name: name,
|
|
|
+ avatar_color: color,
|
|
|
+ });
|
|
|
+ if (insertError) throw insertError;
|
|
|
+
|
|
|
+ return { userId: authData.user.id };
|
|
|
+ },
|
|
|
+ onSuccess: () => {
|
|
|
+ router.push("/recovery");
|
|
|
+ },
|
|
|
+ });
|
|
|
+
|
|
|
+ function handleSubmit(e: React.FormEvent) {
|
|
|
+ e.preventDefault();
|
|
|
+ const trimmed = displayName.trim();
|
|
|
+ const error = validateDisplayName(trimmed);
|
|
|
+ if (error) {
|
|
|
+ setValidationError(error);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ setValidationError(null);
|
|
|
+ signUpMutation.mutate({ name: trimmed, color: avatarColor });
|
|
|
+ }
|
|
|
+
|
|
|
+ return (
|
|
|
+ <form onSubmit={handleSubmit} className="space-y-6">
|
|
|
+ <div>
|
|
|
+ <label htmlFor="display-name" className="block text-sm font-medium mb-1">
|
|
|
+ Display name
|
|
|
+ </label>
|
|
|
+ <input
|
|
|
+ id="display-name"
|
|
|
+ type="text"
|
|
|
+ value={displayName}
|
|
|
+ onChange={(e) => {
|
|
|
+ setDisplayName(e.target.value);
|
|
|
+ setValidationError(null);
|
|
|
+ }}
|
|
|
+ maxLength={DISPLAY_NAME_MAX_LENGTH}
|
|
|
+ placeholder="What should we call you?"
|
|
|
+ className="w-full rounded-lg border border-foreground/20 bg-background px-3 py-2 text-foreground placeholder:text-foreground/40 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
|
+ autoFocus
|
|
|
+ autoComplete="off"
|
|
|
+ />
|
|
|
+ {validationError && <p className="mt-1 text-sm text-red-500">{validationError}</p>}
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <AvatarColorPicker value={avatarColor} onChange={setAvatarColor} />
|
|
|
+
|
|
|
+ {signUpMutation.error && (
|
|
|
+ <p className="text-sm text-red-500" role="alert">
|
|
|
+ {signUpMutation.error instanceof Error
|
|
|
+ ? signUpMutation.error.message
|
|
|
+ : "Something went wrong. Please try again."}
|
|
|
+ </p>
|
|
|
+ )}
|
|
|
+
|
|
|
+ <button
|
|
|
+ type="submit"
|
|
|
+ disabled={signUpMutation.isPending}
|
|
|
+ className="w-full rounded-lg bg-blue-600 px-4 py-2 font-medium text-white transition-colors hover:bg-blue-700 disabled:opacity-50"
|
|
|
+ >
|
|
|
+ {signUpMutation.isPending ? "Setting up..." : "Get started"}
|
|
|
+ </button>
|
|
|
+ </form>
|
|
|
+ );
|
|
|
+}
|