DEV Community

buildr
buildr

Posted on

This Article describe how u can Add Item in your data base from client

React TypeScript Property Form Validation

export interface PropertyForm {
  propertyTitle: string;
  description: string;
  amenities: string;
  monthlyRent: string;
  location: string;
  unitsAvailable: string;
  applicationDeadline: string;
}

export interface PropertyFormErrors {
  propertyTitle?: string;
  description?: string;
  amenities?: string;
  monthlyRent?: string;
  location?: string;
  unitsAvailable?: string;
  applicationDeadline?: string;
}

export const validatePropertyField = (
  name: keyof PropertyForm,
  value: string
): string => {
  switch (name) {
    case "propertyTitle":
      if (!value.trim()) {
        return "Property title is required";
      }

      if (value.trim().length < 3) {
        return "Property title must be at least 3 characters";
      }

      return "";

    case "description":
      if (!value.trim()) {
        return "Description is required";
      }

      if (value.trim().length > 2000) {
        return "Description cannot exceed 2000 characters";
      }

      return "";

    case "amenities":
      if (!value.trim()) {
        return "Amenities are required";
      }

      return "";

    case "monthlyRent":
      if (!value.trim()) {
        return "Monthly rent is required";
      }

      if (Number(value) <= 0) {
        return "Monthly rent must be greater than 0";
      }

      return "";

    case "location":
      if (!value.trim()) {
        return "Location is required";
      }

      return "";

    case "unitsAvailable":
      if (!value.trim()) {
        return "Units available is required";
      }

      if (!Number.isInteger(Number(value))) {
        return "Units available must be a whole number";
      }

      if (Number(value) < 1) {
        return "At least 1 unit must be available";
      }

      return "";

    case "applicationDeadline":
      if (!value) {
        return "Application deadline is required";
      }

      return "";

    default:
      return "";
  }
};

export const validatePropertyForm = (
  formData: PropertyForm
): PropertyFormErrors => {
  const errors: PropertyFormErrors = {};

  Object.entries(formData).forEach(([name, value]) => {
    const error = validatePropertyField(
      name as keyof PropertyForm,
      value
    );

    if (error) {
      errors[name as keyof PropertyFormErrors] = error;
    }
  });

  return errors;
};

Enter fullscreen mode Exit fullscreen mode

Add Product Form **

import { useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { toast } from "react-toastify";

import { useAddPropertyMutation } from "../services/propertyApi";

import {
  PropertyForm,
  PropertyFormErrors,
  validatePropertyField,
  validatePropertyForm,
} from "../validations/propertyValidation";

const AddProperty = () => {
  const navigate = useNavigate();

  const { agencyId } = useParams<{ agencyId: string }>();

  const [addProperty, { isLoading: isAdding }] =
    useAddPropertyMutation();

  const [formData, setFormData] = useState<PropertyForm>({
    propertyTitle: "",
    description: "",
    amenities: "",
    monthlyRent: "",
    location: "",
    unitsAvailable: "",
    applicationDeadline: "",
  });

  const [errors, setErrors] = useState<PropertyFormErrors>({});

  // ----------------------------------------
  // Handle input change
  // ----------------------------------------

  const handleChange = (
    e: React.ChangeEvent<
      HTMLInputElement | HTMLTextAreaElement
    >
  ) => {
    const { name, value } = e.target;

    setFormData((prev) => ({
      ...prev,
      [name]: value,
    }));

    // Clear field error while typing
    if (errors[name as keyof PropertyFormErrors]) {
      setErrors((prev) => ({
        ...prev,
        [name]: "",
      }));
    }
  };

  // ----------------------------------------
  // Blur validation
  // ----------------------------------------

  const handleBlur = (
    e: React.FocusEvent<
      HTMLInputElement | HTMLTextAreaElement
    >
  ) => {
    const { name, value } = e.target;

    const error = validatePropertyField(
      name as keyof PropertyForm,
      value
    );

    setErrors((prev) => ({
      ...prev,
      [name]: error,
    }));
  };

  // ----------------------------------------
  // Submit
  // ----------------------------------------

  const handleSubmit = async (
    e: React.FormEvent<HTMLFormElement>
  ) => {
    e.preventDefault();

    // Validate complete form
    const validationErrors = validatePropertyForm(formData);

    setErrors(validationErrors);

    if (Object.keys(validationErrors).length > 0) {
      toast.error("Please fix the validation errors");
      return;
    }

    // Validate Agency ID
    const parsedAgencyId = Number(agencyId);

    if (!agencyId || Number.isNaN(parsedAgencyId)) {
      toast.error("Invalid agency ID");
      return;
    }

    try {
      const result = await addProperty({
        agencyId: parsedAgencyId,
        propertyTitle: formData.propertyTitle.trim(),
        description: formData.description.trim(),
        amenities: formData.amenities.trim(),
        monthlyRent: Number(formData.monthlyRent),
        location: formData.location.trim(),
        unitsAvailable: Number(formData.unitsAvailable),
        applicationDeadline: formData.applicationDeadline,
      }).unwrap();

      toast.success(result.response);

      // Navigate only after successful API call
      navigate(-1);
    } catch (error) {
      console.error("Add property error:", error);

      toast.error("Failed to add property");
    }
  };

  // ----------------------------------------
  // Back
  // ----------------------------------------

  const handleBack = () => {
    navigate(-1);
  };

  return (
    <div className="min-h-screen bg-gray-50 px-4 py-6 sm:px-6 lg:px-8">
      <div className="mx-auto max-w-4xl">

        {/* Header */}
        <div className="mb-6 flex items-center gap-3">

          <button
            type="button"
            onClick={handleBack}
            disabled={isAdding}
            className="flex h-10 w-10 items-center justify-center rounded-full border bg-white text-gray-700 shadow-sm transition hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-50"
          >
            <span className="text-xl"></span>
          </button>

          <div>
            <h1 className="text-2xl font-bold text-gray-900 sm:text-3xl">
              Add Property
            </h1>

            <p className="mt-1 text-sm text-gray-500">
              Add a new property for Agency #{agencyId}
            </p>
          </div>

        </div>

        {/* Form */}
        <div className="rounded-xl bg-white p-5 shadow-sm sm:p-8">

          <form onSubmit={handleSubmit} noValidate>

            <div className="grid grid-cols-1 gap-5 md:grid-cols-2">

              {/* Property Title */}
              <div className="md:col-span-2">
                <label
                  htmlFor="propertyTitle"
                  className="mb-2 block text-sm font-medium text-gray-700"
                >
                  Property Title
                  <span className="text-red-500"> *</span>
                </label>

                <input
                  id="propertyTitle"
                  name="propertyTitle"
                  type="text"
                  value={formData.propertyTitle}
                  onChange={handleChange}
                  onBlur={handleBlur}
                  placeholder="Enter property title"
                  className={`w-full rounded-lg border px-4 py-3 outline-none ${
                    errors.propertyTitle
                      ? "border-red-500"
                      : "border-gray-300 focus:border-blue-500"
                  }`}
                />

                {errors.propertyTitle && (
                  <p className="mt-1 text-sm text-red-500">
                    {errors.propertyTitle}
                  </p>
                )}
              </div>

              {/* Description */}
              <div className="md:col-span-2">

                <div className="mb-2 flex justify-between">
                  <label
                    htmlFor="description"
                    className="text-sm font-medium text-gray-700"
                  >
                    Description
                    <span className="text-red-500"> *</span>
                  </label>

                  <span className="text-xs text-gray-400">
                    {formData.description.length}/2000
                  </span>
                </div>

                <textarea
                  id="description"
                  name="description"
                  rows={5}
                  maxLength={2000}
                  value={formData.description}
                  onChange={handleChange}
                  onBlur={handleBlur}
                  placeholder="Describe the property..."
                  className={`w-full resize-none rounded-lg border px-4 py-3 outline-none ${
                    errors.description
                      ? "border-red-500"
                      : "border-gray-300 focus:border-blue-500"
                  }`}
                />

                {errors.description && (
                  <p className="mt-1 text-sm text-red-500">
                    {errors.description}
                  </p>
                )}
              </div>

              {/* Amenities */}
              <div className="md:col-span-2">

                <label
                  htmlFor="amenities"
                  className="mb-2 block text-sm font-medium text-gray-700"
                >
                  Amenities
                  <span className="text-red-500"> *</span>
                </label>

                <input
                  id="amenities"
                  name="amenities"
                  type="text"
                  value={formData.amenities}
                  onChange={handleChange}
                  onBlur={handleBlur}
                  placeholder="Parking, WiFi, Gym, Swimming Pool"
                  className={`w-full rounded-lg border px-4 py-3 outline-none ${
                    errors.amenities
                      ? "border-red-500"
                      : "border-gray-300 focus:border-blue-500"
                  }`}
                />

                {errors.amenities && (
                  <p className="mt-1 text-sm text-red-500">
                    {errors.amenities}
                  </p>
                )}
              </div>

              {/* Monthly Rent */}
              <div>

                <label
                  htmlFor="monthlyRent"
                  className="mb-2 block text-sm font-medium text-gray-700"
                >
                  Monthly Rent
                  <span className="text-red-500"> *</span>
                </label>

                <input
                  id="monthlyRent"
                  name="monthlyRent"
                  type="number"
                  min="1"
                  value={formData.monthlyRent}
                  onChange={handleChange}
                  onBlur={handleBlur}
                  placeholder="25000"
                  className={`w-full rounded-lg border px-4 py-3 outline-none ${
                    errors.monthlyRent
                      ? "border-red-500"
                      : "border-gray-300 focus:border-blue-500"
                  }`}
                />

                {errors.monthlyRent && (
                  <p className="mt-1 text-sm text-red-500">
                    {errors.monthlyRent}
                  </p>
                )}
              </div>

              {/* Units Available */}
              <div>

                <label
                  htmlFor="unitsAvailable"
                  className="mb-2 block text-sm font-medium text-gray-700"
                >
                  Units Available
                  <span className="text-red-500"> *</span>
                </label>

                <input
                  id="unitsAvailable"
                  name="unitsAvailable"
                  type="number"
                  min="1"
                  step="1"
                  value={formData.unitsAvailable}
                  onChange={handleChange}
                  onBlur={handleBlur}
                  placeholder="5"
                  className={`w-full rounded-lg border px-4 py-3 outline-none ${
                    errors.unitsAvailable
                      ? "border-red-500"
                      : "border-gray-300 focus:border-blue-500"
                  }`}
                />

                {errors.unitsAvailable && (
                  <p className="mt-1 text-sm text-red-500">
                    {errors.unitsAvailable}
                  </p>
                )}
              </div>

              {/* Location */}
              <div className="md:col-span-2">

                <label
                  htmlFor="location"
                  className="mb-2 block text-sm font-medium text-gray-700"
                >
                  Location
                  <span className="text-red-500"> *</span>
                </label>

                <input
                  id="location"
                  name="location"
                  type="text"
                  value={formData.location}
                  onChange={handleChange}
                  onBlur={handleBlur}
                  placeholder="Enter property location"
                  className={`w-full rounded-lg border px-4 py-3 outline-none ${
                    errors.location
                      ? "border-red-500"
                      : "border-gray-300 focus:border-blue-500"
                  }`}
                />

                {errors.location && (
                  <p className="mt-1 text-sm text-red-500">
                    {errors.location}
                  </p>
                )}
              </div>

              {/* Application Deadline */}
              <div>

                <label
                  htmlFor="applicationDeadline"
                  className="mb-2 block text-sm font-medium text-gray-700"
                >
                  Application Deadline
                  <span className="text-red-500"> *</span>
                </label>

                <input
                  id="applicationDeadline"
                  name="applicationDeadline"
                  type="date"
                  min={new Date().toISOString().split("T")[0]}
                  value={formData.applicationDeadline}
                  onChange={handleChange}
                  onBlur={handleBlur}
                  className={`w-full rounded-lg border px-4 py-3 outline-none ${
                    errors.applicationDeadline
                      ? "border-red-500"
                      : "border-gray-300 focus:border-blue-500"
                  }`}
                />

                {errors.applicationDeadline && (
                  <p className="mt-1 text-sm text-red-500">
                    {errors.applicationDeadline}
                  </p>
                )}
              </div>

            </div>

            {/* Buttons */}
            <div className="mt-8 flex flex-col-reverse gap-3 border-t pt-6 sm:flex-row sm:justify-end">

              <button
                type="button"
                onClick={handleBack}
                disabled={isAdding}
                className="rounded-lg border border-gray-300 px-6 py-3 font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50"
              >
                Cancel
              </button>

              <button
                type="submit"
                disabled={isAdding}
                className="rounded-lg bg-blue-600 px-6 py-3 font-medium text-white hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-60"
              >
                {isAdding
                  ? "Adding Property..."
                  : "Add Property"}
              </button>

            </div>

          </form>
        </div>
      </div>
    </div>
  );
};

export default AddProperty;
Enter fullscreen mode Exit fullscreen mode

Top comments (0)