Ctrl K
ComunidadGuías…

src/components/auth/profile-completion-form.tsx

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"use client";

import React, { useState } from "react";
import { useForm, useWatch } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useAuth } from "@/contexts/AuthContext";
import {
  profileCompletionSchema,
  ProfileCompletionFormData,
} from "@/lib/validations/auth";
import { getApiErrorMessage } from "@/lib/api/error";
import { UserPlus } from "lucide-react";
import { ButtonSpinner } from "@/components/ui/button-spinner";
import { StatusMessage } from "@/components/ui/status-message";
import { useToast } from "@/components/ui/use-toast";
import { PictureField } from "@/components/user/picture-field";
import { usePictureEdit } from "@/hooks/usePictureEdit";
import { applyPictureEdit } from "@/lib/picture-edits";
import {
  StandaloneCard,
  StandaloneCardHeader,
} from "@/components/layout/standalone-card";

// Shared by both authentication methods: shown once, for a verified identity (email
// or Google) with no existing account. Email is read-only (it's already been
// verified - that's *why* this form exists), name is prefilled when Google supplied
// one but always editable, and there's no password field.
//
// The profile picture is optional and held until the account exists: there is none
// to upload it to before, so it goes in `completeProfile`'s `onCreated`.
export function ProfileCompletionForm() {
  const [error, setError] = useState<string | null>(null);
  const { onboarding, completeProfile } = useAuth();
  const { toast } = useToast();
  const router = useRouter();
  const [pictureEdit, setPictureEdit] = usePictureEdit();

  const {
    register,
    handleSubmit,
    control,
    formState: { errors, isSubmitting },
  } = useForm<ProfileCompletionFormData>({
    resolver: zodResolver(profileCompletionSchema),
    defaultValues: {
      name: onboarding?.name ?? "",
      username: "",
    },
  });

  // Whose initials the empty picture shows, following the name as it is typed.
  const name = useWatch({ control, name: "name" });

  if (!onboarding) {
    return null;
  }

  const onSubmit = async (data: ProfileCompletionFormData) => {
    // A picture that fails leaves the account made and signed in, so it is said
    // after the move on rather than in place of it.
    const pictureFailure: { error?: unknown } = {};
    const savePicture = async () => {
      if (!pictureEdit) return;
      try {
        await applyPictureEdit("avatar", pictureEdit);
      } catch (error) {
        console.error("Failed to save the profile picture:", error);
        pictureFailure.error = error;
      }
    };

    try {
      setError(null);
      await completeProfile(data.name, data.username, savePicture);
      if ("error" in pictureFailure) {
        toast({
          title: "Your account is ready, but your profile picture did not save",
          description: getApiErrorMessage(
            pictureFailure.error,
            "Add it again in Settings.",
          ),
          variant: "destructive",
        });
      }
      router.push("/home");
    } catch (err) {
      setError(getApiErrorMessage(err, "Could not complete your profile."));
    }
  };

  return (
    <StandaloneCard>
      <StandaloneCardHeader
        icon={UserPlus}
        title="Complete Your Profile"
        description="Just a couple more details and you're in."
      />
      <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
        <StatusMessage variant="error">{error}</StatusMessage>

        <PictureField
          picture="avatar"
          name={name || onboarding.email}
          edit={pictureEdit}
          onChange={setPictureEdit}
          disabled={isSubmitting}
        />

        <div className="space-y-2">
          <Label htmlFor="email">Email</Label>
          <Input
            id="email"
            type="email"
            value={onboarding.email}
            disabled
            readOnly
          />
        </div>

        <div className="space-y-2">
          <Label htmlFor="name">Full name</Label>
          <Input
            id="name"
            type="text"
            placeholder="Enter your full name"
            {...register("name")}
            className={errors.name ? "border-destructive" : ""}
          />
          {errors.name && (
            <p className="text-sm text-destructive">{errors.name.message}</p>
          )}
        </div>

        <div className="space-y-2">
          <Label htmlFor="username">Username</Label>
          <Input
            id="username"
            type="text"
            placeholder="Choose a username"
            {...register("username")}
            className={errors.username ? "border-destructive" : ""}
          />
          {errors.username && (
            <p className="text-sm text-destructive">
              {errors.username.message}
            </p>
          )}
        </div>

        <Button
          type="submit"
          className="w-full bg-coral text-primary-foreground hover:bg-coral/90"
          disabled={isSubmitting}
        >
          {isSubmitting ? (
            <div className="flex items-center space-x-2">
              <ButtonSpinner />
              <span>Creating your account...</span>
            </div>
          ) : (
            <div className="flex items-center space-x-2">
              <UserPlus size={16} />
              <span>Finish setting up</span>
            </div>
          )}
        </Button>
      </form>
    </StandaloneCard>
  );
}