Skip to content

Commit

Permalink
feat(checkout): build shipping info flow (#57)
Browse files Browse the repository at this point in the history
* feat(checkout): build shipping info flow

* build current locations view

* Add progress bar

* remvoe console log

* rearrange imports

* add missing trackPageView

* linter

* add gap

* add newline at eof
  • Loading branch information
mapra99 committed Dec 29, 2022
1 parent 7ac2e72 commit eda5ff9
Show file tree
Hide file tree
Showing 26 changed files with 929 additions and 34 deletions.
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ SESSION_SECRET="super-duper-s3cret"
AUDIOPHILE_API_BASE_URL=http://0.0.0.0:3001
AUDIOPHILE_API_KEY=audiophile
AUDIOPHILE_API_VERSION=v1
MAPBOX_GL_TOKEN=token123
2 changes: 2 additions & 0 deletions app/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,5 @@ export { default as MobileNavigation } from './mobile-navigation'
export { default as PurchaseCartSummary } from './purchase-cart-summary'
export { default as PurchaseCartSummaryItem } from './purchase-cart-summary-item'
export { default as PurchaseCartSummaryFee } from './purchase-cart-summary-fee'
export { default as LocationInfo } from './location-info'
export { default as ProgressBar } from './progress-bar'
1 change: 1 addition & 0 deletions app/components/location-info/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './location-info'
23 changes: 23 additions & 0 deletions app/components/location-info/location-info.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Text } from '~/components'

import type { LocationInfoProps } from "./types"

const LocationInfo = ({ location }: LocationInfoProps) => {
const { street_address, postal_code, city, country, extra_info } = location

return (
<div className="flex flex-col sm:flex-row sm:gap-4 sm:items-center">
<Text variant="heading-6">
{ street_address }
</Text>
<Text variant="body">
{ postal_code } { city }, { country }
</Text>
<Text variant="body" className="opacity-50">
{ extra_info }
</Text>
</div>
)
}

export default LocationInfo
5 changes: 5 additions & 0 deletions app/components/location-info/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { Location } from '~/models/location'

export interface LocationInfoProps {
location: Location
}
1 change: 1 addition & 0 deletions app/components/progress-bar/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './progress-bar'
11 changes: 11 additions & 0 deletions app/components/progress-bar/progress-bar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { ProgressBarProps } from "./types"

const ProgressBar = ({ progress }: ProgressBarProps) => {
return (
<div className="w-full">
<div style={{width: progress}} className="h-1 bg-orange transition-all" />
</div>
)
}

export default ProgressBar
3 changes: 3 additions & 0 deletions app/components/progress-bar/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface ProgressBarProps {
progress: string
}
17 changes: 4 additions & 13 deletions app/components/radio-input/radio-input.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,19 @@
import { useState } from 'react'
import type { ChangeEvent } from 'react'
import type { RadioInputProps } from './types'

const RadioInput = ({ id, label, className, onChange, checked, ...props }: RadioInputProps) => {
const [isChecked, setIsChecked] = useState<boolean | undefined>(checked)

let inputStyles = 'absolute left-4 top-5 appearance-none bg-white m-0 w-5 h-5 rounded-full border-2 border-gray ' // base, unchecked state
inputStyles += 'flex items-center justify-center checked:before:block checked:before:w-2.5 checked:before:h-2.5 checked:before:bg-orange checked:before:rounded-full' // checked state

const labelStyles = `py-4.5 px-13 text-sm border-2 border-gray rounded-lg font-bold hover:border-orange transition-all ${isChecked ? 'border-orange' : ''}`

const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
setIsChecked(event.target.checked)
if (onChange) onChange(event)
}
const labelStyles = `w-full py-4.5 px-13 text-sm border-2 border-gray rounded-lg font-bold hover:border-orange hover:cursor-pointer transition-all`

return (
<div className="flex relative">
<div className={`flex relative ${className || ''}`}>
<input
{...props}
id={id}
type="radio"
className={`${inputStyles} ${className || ''}`}
onChange={handleChange}
className={inputStyles}
onChange={onChange}
checked={checked}
/>
<label htmlFor={id} className={labelStyles}>
Expand Down
5 changes: 3 additions & 2 deletions app/components/radio-input/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { InputHTMLAttributes } from 'react'
import type { ReactNode, InputHTMLAttributes } from 'react'

export interface RadioInputProps extends InputHTMLAttributes<HTMLInputElement> {
label: string
label: ReactNode
className?: string
}
1 change: 1 addition & 0 deletions app/hooks/use-map/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './use-map'
12 changes: 12 additions & 0 deletions app/hooks/use-map/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { LngLatLike } from 'mapbox-gl'

export interface MarkerArgs {
position: LngLatLike
}

export interface UseMapArgs {
containerId: string
mapboxToken: string
center?: LngLatLike
marker?: MarkerArgs
}
46 changes: 46 additions & 0 deletions app/hooks/use-map/use-map.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { useEffect, useState } from 'react';
import mapboxgl from 'mapbox-gl'

import type { Map } from 'mapbox-gl'
import type { UseMapArgs } from './types'

const MAP_ZOOM_LEVEL = 17
const MAP_STYLES_URL = 'mapbox://styles/mapbox/streets-v12'

const useMap = ({ containerId, center, marker, mapboxToken }: UseMapArgs) => {
const [map, setMap] = useState<Map | undefined>()

useEffect(() => {
if (!containerId || !center || !mapboxToken) return;
if (map) return

mapboxgl.accessToken = mapboxToken;
const createdMap = new mapboxgl.Map({
container: containerId,
style: MAP_STYLES_URL,
center,
zoom: MAP_ZOOM_LEVEL
});

setMap(createdMap)
}, [map, containerId, center, mapboxToken])

useEffect(() => {
if (!map) return;
if (!marker) return;

const { position } = marker

new mapboxgl.Marker()
.setLngLat(position)
.addTo(map)

setMap(map)
}, [map, marker])

return {
map
}
}

export default useMap
2 changes: 2 additions & 0 deletions app/models/location/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './location.server'
export * from './schema'
33 changes: 33 additions & 0 deletions app/models/location/location.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import z from 'zod'
import * as AudiophileClient from '~/utils/audiophile-client'
import { LocationSchema } from './schema'

import type { LocationPayload } from './schema'

export const createLocation = async (authToken: string, locationPayload: LocationPayload) => {
const response = await AudiophileClient.sendRequest('post', 'locations', {
authToken,
body: locationPayload
})

const location = LocationSchema.parse(response)
return location
}

export const getLocation = async(authToken: string, locationUuid: string) => {
const response = await AudiophileClient.sendRequest('get', `locations/${locationUuid}`, { authToken })

const location = LocationSchema.parse(response)
return location
}

export const deleteLocation = async(authToken: string, locationUuid: string) => {
await AudiophileClient.sendRequest('delete', `locations/${locationUuid}`, { authToken })
}

export const getAllLocations = async(authToken: string) => {
const response = await AudiophileClient.sendRequest('get', 'locations', { authToken })
const locations = z.array(LocationSchema).parse(response)

return locations
}
22 changes: 22 additions & 0 deletions app/models/location/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import z from 'zod'

export interface LocationPayload {
street_address: string
city: string
country: string
postal_code: string
extra_info?: string
}

export const LocationSchema = z.object({
uuid: z.string(),
extra_info: z.string().nullable(),
street_address: z.string(),
city: z.string(),
country: z.string(),
postal_code: z.string(),
longitude: z.string(),
latitude: z.string()
})

export type Location = z.infer<typeof LocationSchema>
7 changes: 7 additions & 0 deletions app/models/purchase-cart/purchase-cart.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,10 @@ export const createOrUpdateCart = async(sessionId: string, cartItem: PurchaseCar
export const removeCart = async (sessionId: string, cartUuid: string) => {
await AudiophileClient.sendRequest('delete', `purchase_carts/${cartUuid}`, { sessionToken: sessionId })
}

export const updateCartLocation = async(sessionId: string, cartUuid: string, locationUuid: string) => {
await AudiophileClient.sendRequest('patch', `purchase_carts/${cartUuid}`, {
sessionToken: sessionId,
body: { user_location_uuid: locationUuid }
})
}
26 changes: 19 additions & 7 deletions app/routes/checkout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Outlet, useLoaderData } from '@remix-run/react'
import { json, redirect } from '@remix-run/node'
import invariant from 'tiny-invariant'
import { Text, PurchaseCartSummary } from '~/components'
import { Text, PurchaseCartSummary, ProgressBar } from '~/components'
import { getLastStartedCart } from '~/models/purchase-cart'
import { getSessionId } from '~/utils/session-storage'
import trackPageView from '~/utils/track-page-view'
Expand All @@ -20,17 +20,25 @@ export const loader = async ({ request }: LoaderArgs) => {
if(!activeCart) return redirect('/')

// checkout flow navigation
let progress = "0";

const url = new URL(request.url)
if (url.pathname === '/checkout') {
const accessToken = await getAccessToken(request)
if(!accessToken) return redirect('/checkout/billing-details')
const accessToken = await getAccessToken(request)
if(!accessToken) {
progress = "33%"
if (url.pathname === '/checkout') return redirect('/checkout/billing-details')
} else if (!activeCart.user_location_uuid) {
progress = "66%"
if (url.pathname === '/checkout') return redirect('/checkout/shipping-info')
} else {
progress = "100%"
}

return json({ activeCart })
return json({ activeCart, progress })
}

export default () => {
const { activeCart } = useLoaderData()
const { activeCart, progress } = useLoaderData<typeof loader>()

return (
<div className="bg-gray">
Expand All @@ -44,7 +52,11 @@ export default () => {

<div className="px-6 pb-24 sm:px-10 sm:pb-28">
<div className="max-w-6xl mx-auto flex flex-col gap-8 lg:flex-row">
<div className="rounded-lg bg-white px-6 py-8 sm:p-8 lg:flex-1 lg:pt-14 lg:px-12 lg:pb-12">
<div className="rounded-lg bg-white px-6 py-8 sm:p-8 lg:flex-1 lg:pt-14 lg:px-12 lg:pb-12 relative">
<div className="absolute top-0 left-0 w-full">
<ProgressBar progress={progress} />
</div>

<Text variant="heading-3" className="!text-3xl sm:!text-4xl mb-8 sm:mb-10" as="h2">
Checkout
</Text>
Expand Down
14 changes: 14 additions & 0 deletions app/routes/checkout/shipping-info.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Outlet } from '@remix-run/react'
import { Text } from '~/components'

export default () => {
return (
<div>
<Text variant="subtitle" as="h3" className="mb-4">
Shipping Info
</Text>

<Outlet />
</div>
)
}
Loading

0 comments on commit eda5ff9

Please sign in to comment.