egomaragustaf / consneakers

Consneakers
https://consneakers.egomaragustaf.com
0 stars 0 forks source link

Mapbox Embed #10

Open mhaidarhanif opened 1 year ago

mhaidarhanif commented 1 year ago
import { useState } from "react";
import { MapPin } from "lucide-react";
import type { MarkerDragEvent } from "react-map-gl";
import type { Address } from "@prisma/client";
import MapboxGL, {
  FullscreenControl,
  GeolocateControl,
  Marker,
  NavigationControl,
  Popup,
  ScaleControl,
} from "react-map-gl";

import { useRootLoaderData } from "~/hooks";
import { Debug } from "~/components";
import { defaultAddressCoordinate } from "~/schema";
import type { AddressCoordinate } from "~/schema";

interface Props {
  address?: Address;
  style?: {
    width: number | string;
    height: number | string;
  };
  zoom?: number;
  draggable?: boolean;

  coordinateValue?: AddressCoordinate;
  handleChangeCoordinate?: (arg0: AddressCoordinate) => void;
}

export function MapboxEmbed(props: Props) {
  const {
    address,
    style,
    zoom = 16,
    draggable,
    coordinateValue,
    handleChangeCoordinate,
  } = props;

  const { ENV } = useRootLoaderData();
  const [popupShown, setPopupShown] = useState(false);

  const mapboxAccessToken = ENV.MAPBOX_PUBLIC_TOKEN;
  const mapStyle = "mapbox://styles/mapbox/streets-v9";

  /**
   * Get initial coordinate from either:
   * 1. the form data changes (Map search API)
   * 2. or existing address data (Fujibox database)
   */
  const coordinate: AddressCoordinate = {
    longitude: coordinateValue?.longitude
      ? coordinateValue.longitude
      : address?.longitude
      ? Number(address?.longitude)
      : defaultAddressCoordinate.longitude,
    latitude: coordinateValue?.latitude
      ? coordinateValue.latitude
      : address?.latitude
      ? Number(address?.latitude)
      : defaultAddressCoordinate.latitude,
  };

  const initialViewState = { ...coordinate, zoom };

  const [markerPosition, setMarkerPosition] = useState(coordinate);

  const handleOnClick = (event: mapboxgl.MapboxEvent<MouseEvent>) => {
    event.originalEvent.stopPropagation();
    setPopupShown(true);
  };

  const handleOnDragStart = (event: MarkerDragEvent) => {
    console.info();
  };

  // Set the pointed coordinate in the Mapbox Embed UI
  const handleOnDrag = ({ lngLat }: MarkerDragEvent) => {
    const newCoordinate = { longitude: lngLat.lng, latitude: lngLat.lat };
    setMarkerPosition(newCoordinate);
  };

  // Send the pointed coordinate to the form
  const handleOnDragEnd = ({ lngLat }: MarkerDragEvent) => {
    const newCoordinate = { longitude: lngLat.lng, latitude: lngLat.lat };
    handleChangeCoordinate && handleChangeCoordinate(newCoordinate);
  };

  return (
    <div data-id="mapbox-embed">
      <MapboxGL
        mapboxAccessToken={mapboxAccessToken}
        initialViewState={initialViewState}
        mapStyle={mapStyle}
        style={style ? style : { width: 800, height: 420 }}
        renderWorldCopies={false}
      >
        <FullscreenControl />
        <GeolocateControl trackUserLocation showAccuracyCircle={false} />
        <NavigationControl visualizePitch />
        <ScaleControl />

        <Marker
          anchor="bottom"
          longitude={markerPosition.longitude}
          latitude={markerPosition.latitude}
          draggable={draggable}
          onClick={handleOnClick}
          onDragStart={handleOnDragStart}
          onDrag={handleOnDrag}
          onDragEnd={handleOnDragEnd}
        >
          <MapPin className="h-10 w-10 cursor-pointer text-red-500" />
        </Marker>
        {address && popupShown && (
          <Popup
            anchor="top"
            onClose={() => setPopupShown(false)}
            longitude={coordinate.longitude}
            latitude={coordinate.latitude}
            closeButton={true}
          >
            <h4>{address.name}</h4>
            <p>{address.addressComplete}</p>
          </Popup>
        )}
      </MapboxGL>

      <Debug title="markerPosition" className="max-w-xs">
        {markerPosition}
      </Debug>
    </div>
  );
}
mhaidarhanif commented 1 year ago

Example on route:

import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";

import { getFirstAddress } from "~/models";
import { Debug, MapboxEmbed } from "~/components";

export async function loader() {
  return json({
    address: await getFirstAddress(),
  });
}

export default function ExamplesMapboxRoute() {
  const { address } = useLoaderData<typeof loader>();

  if (!address) {
    return <p>Address is not available</p>;
  }

  return (
    <div className="space-y-8 p-4">
      <h1>Example Mapbox</h1>

      <section className="flex max-w-full gap-4">
        <div>
          <h2>With Address</h2>
          <MapboxEmbed
            address={address as any}
            style={{ width: 500, height: 300 }}
            draggable
          />
        </div>
        <div>
          <Debug title="address" className="whitespace-pre-wrap">
            {address}
          </Debug>
        </div>
      </section>

      <section>
        <h2>Without Address</h2>
        <MapboxEmbed draggable zoom={10} />
      </section>
    </div>
  );
}
mhaidarhanif commented 1 year ago

Usage on new address route:

import { redirect } from "@remix-run/node";
import { useActionData } from "@remix-run/react";
import {
  ValidatedForm as RemixValidatedForm,
  validationError,
} from "remix-validated-form";
import { withZod } from "@remix-validated-form/with-zod";
import type { DataFunctionArgs } from "@remix-run/node";

import {
  Debug,
  FormInput,
  FormSubmitButton,
  FormTextArea,
  Link,
  MapboxEmbed,
} from "~/components";
import { useRootLoaderData } from "~/hooks";
import { createNewAddressForUser, getFirstAddressByUser } from "~/models";
import { useState } from "react";
import { getUserRedirect } from "~/helpers";
import {
  placeholderAddress,
  defaultAddressCoordinate,
  upsertAddressSchema,
} from "~/schema";
import type { AddressCoordinate } from "~/schema";
import { getRedirectTo, useRedirectTo } from "~/utils";

export const validator = withZod(upsertAddressSchema);

export const action = async ({ request }: DataFunctionArgs) => {
  const redirectTo = getRedirectTo(request);
  const userSession = await getUserRedirect(request);
  const userId = userSession.id;

  const result = await validator.validate(await request.formData());
  if (result.error) return validationError(result.error);

  const existingAdress = await getFirstAddressByUser({ userId });
  const hasExistingAddress = Boolean(existingAdress?.id);

  await createNewAddressForUser({
    userId,
    address: result.data as any,
    isPrimary: Boolean(redirectTo || !hasExistingAddress),
    // The new address will be set auomaticlaly as primary if:
    // A. There is a redirect to searchParams
    // B. There is no existing address yet
  });

  return redirect(redirectTo || "/user/addresses");
};

export default function AdminNewAddressRoute() {
  const { ENV } = useRootLoaderData();
  const actionData = useActionData<typeof action>();

  const [coordinate, setCoordinate] = useState<AddressCoordinate>({
    longitude: defaultAddressCoordinate.longitude,
    latitude: defaultAddressCoordinate.latitude,
  });

  const handleChangeCoordinate = (newCoordinate: AddressCoordinate) => {
    setCoordinate(newCoordinate);
  };

  const defaultValues =
    ENV && ENV.NODE_ENV === "development" ? placeholderAddress : {};

  const redirectTo = useRedirectTo();
  const createAddressLinkText = redirectTo
    ? `Simpan dan Jadikan Alamat Utama`
    : "Simpan Alamat";
  const createAddressLinkLoadingText = redirectTo
    ? `Menyimpan dan Menjadikan Alamat Utama...`
    : "Menyimpan Alamat...";

  return (
    <div className="space-y-4 rounded border bg-white p-4">
      <header>
        <h2>Tambah Alamat Tujuan Pengiriman</h2>
      </header>

      <main className="w-full max-w-xl basis-1/2">
        <RemixValidatedForm
          validator={validator}
          method="post"
          defaultValues={defaultValues}
          className="space-y-8"
        >
          <section className="space-y-2">
            <h3>Detail alamat</h3>
            <div className="space-y-1">
              <FormInput
                name="name"
                label="Nama Label Alamat"
                placeholder="Rumah Utama"
              />
              <p className="text-xs">
                Hindari penamaan ambigu seperti "Rumah Saya", "Toko Saya", dan
                sejenisnya.
              </p>

              <FormInput
                name="street"
                label="Jalan"
                placeholder={placeholderAddress.street}
              />
              <FormTextArea
                name="streetDetails"
                label="Detail Jalan"
                placeholder={placeholderAddress.streetDetails}
                rows={2}
              />
              <div className="flex flex-wrap gap-2 md:flex-nowrap">
                <FormInput
                  name="subDistrict"
                  label="Kelurahan"
                  placeholder={placeholderAddress.subDistrict}
                  className="grow md:basis-1/2"
                />
                <FormInput
                  name="district"
                  label="Kecamatan"
                  placeholder={placeholderAddress.district}
                  className="grow md:basis-1/2"
                />
              </div>
              <div className="flex flex-wrap gap-2 md:flex-nowrap">
                <FormInput
                  name="city"
                  label="Kota/Kabupaten"
                  placeholder={placeholderAddress.city}
                  className="grow md:basis-1/2"
                />
                <FormInput
                  name="province"
                  label="Provinsi"
                  placeholder={placeholderAddress.province}
                  className="grow md:basis-1/2"
                />
              </div>
              <div className="flex flex-wrap gap-2 md:flex-nowrap">
                <FormInput
                  name="postalCode"
                  label="Kode Pos"
                  placeholder={placeholderAddress.postalCode}
                  className="grow md:basis-1/2"
                />
                <FormInput
                  name="countryCode"
                  label="Negara"
                  placeholder={placeholderAddress.countryCode}
                  className="grow md:basis-1/2"
                  disabled
                />
              </div>
            </div>
          </section>

          <section className="space-y-2">
            <h3>Detail penerima</h3>
            <div className="space-y-1">
              <FormInput
                name="recipientName"
                label="Nama Penerima"
                placeholder="Nona Wijaya"
              />
              <FormInput
                name="recipientPhone"
                label="Nomor HP"
                placeholder="0812 3456 7890"
              />
              <p className="text-xs">
                Nomor HP ini akan digunakan agar kurir dapat menghubungi
                penerima/seseorang di lokasi tersebut. Boleh sama atau berbeda
                dengan nomor HP di profil Anda.
              </p>
            </div>

            <FormInput
              name="notesForCourier"
              label="Catatan untuk kurir (opsional)"
            />
          </section>

          <section className="space-y-2">
            <h3>Titik lokasi (pinpoint)</h3>

            {/* <div>
              <label htmlFor="search-location">Cari lokasi</label>
              <Input
                id="search-location"
                placeholder="Ketik nama kecamatan atau kelurahan..."
              />
            </div> */}

            <div className="space-y-1">
              <MapboxEmbed
                style={{ width: "100%", height: 300 }}
                zoom={12}
                draggable
                coordinateValue={coordinate}
                handleChangeCoordinate={handleChangeCoordinate}
              />
              <Debug title="coordinate" className="max-w-xs">
                {coordinate}
              </Debug>
            </div>

            <div>
              <input
                hidden
                type="number"
                name="latitude"
                value={coordinate.latitude}
                readOnly
              />
              <input
                hidden
                type="number"
                name="longitude"
                value={coordinate.longitude}
                readOnly
              />
            </div>
          </section>

          <section className="space-y-1">
            <p className="text-sm">
              Dengan menyimpan alamat, kamu menyetujui{" "}
              <Link to="/terms" className="font-bold text-brand-500">
                Syarat & Ketentuan
              </Link>
            </p>
            <FormSubmitButton
              loadingChildren={createAddressLinkLoadingText}
              className="w-full"
            >
              {createAddressLinkText}
            </FormSubmitButton>
          </section>
        </RemixValidatedForm>
      </main>

      <Debug title="actionData">{actionData}</Debug>
    </div>
  );
}