Skip to content

Commit

Permalink
♻️ Revamp the Shipping UI (#88)
Browse files Browse the repository at this point in the history
* ✨ Add the entrance for editing shipping rates

* 🚚 Move reusable UI & logic to Modal

* 💄 Add view mode

* 🚚 Move ShippingRatesModal to ShippingRatesInfo Modal

* ✨ Add method to update shipping rates

* ♻️ Refactor shipping related UI to <ShippingRates>

* ♻️ Refactor shipping UI for edition collection

* ♻️ Refactor Shipping UI for listing new book

* ♻️ Refactor shipping UI for editing or listing edition

* ♻️ Refactor shipping UI for listing new collection

* 🚨 Fix lint errors

* 🐛 Correct the button action in view modal

* 💄 Move the <shippingRates/> before the delete button

* ♻️ Replace mode props to readOnly

* 🩹 Fix typo

* ♻️ Use boolean prop to check if is new listing page

* ♻️ Refactor buttonConfig using nested conditions

* 💄 Display checkbox section if is not editMode

* 🚸 Handle edit shipping options in the same page
  • Loading branch information
AuroraHuang22 authored Feb 15, 2024
1 parent 0bdb4c1 commit 32d2eb1
Show file tree
Hide file tree
Showing 9 changed files with 526 additions and 280 deletions.
188 changes: 188 additions & 0 deletions components/ShippingRates/InfoModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
<template>
<UModal :model-value="modelValue">
<UCard :ui="{ header: { base: 'font-bold font-mono' } }">
<template #header>
<div class="flex justify-between items-center">
{{ modalTitle }}
<UButton
color="gray"
variant="ghost"
icon="i-heroicons-x-mark-20-solid"
@click="closeModal"
/>
</div>
</template>

<component
:is="hasMultipleShippingRates ? 'ul' : 'div'"
v-if="shippingRates.length"
class="space-y-4"
>
<component
:is="hasMultipleShippingRates ? 'li' : 'div'"
v-for="(s, index) in shippingRates"
:key="s.index"
>
<UCard
:ui="{
body: { base: 'space-y-4' },
footer: { base: 'flex justify-end items-center' },
}"
>
<UFormGroup
label="Name of this shipping option"
:ui="{ container: 'space-y-2' }"
>
<UInput
:value="s.nameEn"
placeholder="Shipping option name"
:disabled="isViewMode"
@input="(e) => handleInputShippingRates(e, 'nameEn', index)"
/>
<UInput
placeholder="運送選項名稱"
:value="s.nameZh"
:disabled="isViewMode"
@input="(e) => handleInputShippingRates(e, 'nameZh', index)"
/>
</UFormGroup>

<UFormGroup label="Price(USD) of this shipping option">
<UInput
:value="s.price"
type="number"
step="0.01"
:min="0"
:disabled="isViewMode"
@input="(e) => handleInputShippingRates(e, 'price', index)"
/>
</UFormGroup>

<template v-if="hasMultipleShippingRates && isEditMode" #footer>
<UButton
label="Delete"
variant="outline"
color="red"
@click="deleteShippingRate(index)"
/>
</template>
</UCard>
</component>
<div class="flex justify-center">
<UButton
v-if="isEditMode"
label="Add Options"
variant="outline"
icon="i-heroicons-plus-20-solid"
@click="addMoreShippingRate"
/>
</div>
</component>
<div v-else class="flex justify-center items-center w-full gap-[8px] py-[36px]">
<span>
No items
</span>
<UButton
v-if="isEditMode"
label="Add Options"
variant="outline"
icon="i-heroicons-plus-20-solid"
@click="addMoreShippingRate"
/>
</div>

<template #footer>
<div class="flex justify-end items-center">
<UButton v-if="isEditMode" label="Save" @click="handleOnSave" />
<div v-else class="flex justify-end items-center">
<UButton
label="Set Shipping Options"
variant="outline"
@click="updateShippingRates"
>
<template #trailing>
<UIcon name="i-heroicons-arrow-right-20-solid" />
</template>
</UButton>
</div>
</div>
</template>
</UCard>
</UModal>
</template>

<script setup lang="ts">
import { v4 as uuidv4 } from 'uuid'
definePageMeta({ layout: 'page' })
const props = defineProps({
modelValue: {
type: Boolean
},
readOnly: {
type: Boolean,
default: false
},
shippingInfo: {
type: Array,
default: () => []
}
})
const emit = defineEmits(['update:modelValue', 'on-update-shipping-rates'])
const isEditMode = computed(() => !isReadOnlyMode.value)
const isViewMode = computed(() => isReadOnlyMode.value)
const modalTitle = computed(() =>
isEditMode.value ? 'Editing Shipping Options' : 'Shipping Options Info'
)
const shippingRates = ref<any[]>(
props.shippingInfo.length
? props.shippingInfo.map((option: any) => ({
price: option.priceInDecimal / 100,
nameEn: option.name?.en,
nameZh: option.name?.zh
}))
: []
)
const hasMultipleShippingRates = computed(() => shippingRates.value.length > 1)
const isReadOnlyMode = ref(props.readOnly)
function closeModal () {
emit('update:modelValue', false)
}
function updateShippingRates () {
isReadOnlyMode.value = false
}
function handleInputShippingRates (e: InputEvent, key: string, index: number) {
shippingRates.value[index][key] = (e.target as HTMLInputElement)?.value
}
function addMoreShippingRate () {
if (!shippingRates.value.length) {
shippingRates.value.push({
index: uuidv4(),
price: 10,
nameEn: 'Standard Shipping',
nameZh: '標準寄送'
})
return
}
shippingRates.value.push({
index: uuidv4(),
price: 20,
nameEn: 'International Shipping',
nameZh: '國際寄送'
})
}
function deleteShippingRate (index: number) {
shippingRates.value.splice(index, 1)
}
function handleOnSave () {
emit('on-update-shipping-rates', shippingRates.value.map((option:any) => ({ name: { en: option.nameEn, zh: option.nameZh }, priceInDecimal: Math.round(Number(option.price) * 100) })))
closeModal()
}
</script>
160 changes: 160 additions & 0 deletions components/ShippingRates/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
<template>
<UCard :ui="{ body: { padding: '' } }">
<template #header>
<div class="flex flex-row justify-between items-center">
<div class="flex justify-start items-center gap-[8px]">
<h3 class="font-bold font-mono">
Shipping Options
</h3>
<slot name="header" />
</div>
<UButton
v-if="!shouldHideViewButtonOnViewMode"
:icon="buttonConfig.icon"
:label="buttonConfig.text"
:loading="isLoading"
@click="buttonConfig.action"
/>
</div>
</template>

<UTable
v-if="isEditMode"
:columns="[
{ key: 'index' },
{ key: 'name', label: 'Name' },
{ key: 'price', label: 'Price (USD)' },
]"
:rows="shippingRatesTableRows"
>
<template #name-data="{ row }">
<div class="flex flex-col gap-[8px] items-start">
<span class="text-center">en: {{ row.name.en }}</span>
<span class="text-center">zh: {{ row.name.zh }}</span>
</div>
</template>
<template #price-data="{ row }">
<span class="text-center">{{ row.price }}</span>
</template>
</UTable>

<div v-else class="px-[24px] py-[12px]">
<UFormGroup
label="Physical Goods"
:ui="{ label: { base: 'font-mono font-bold' } }"
>
<div class="flex flex-col gap-[16px]">
<UCheckbox
v-model="hasShipping"
name="hasShipping"
label="Includes physical good that requires shipping"
:disabled="!shippingInfo.length"
/>
<UAlert
v-if="!shippingInfo.length"
icon="i-heroicons-face-frown-solid"
color="yellow"
variant="solid"
title="Please set the shipping options first to enable this feature."
/>
</div>
</UFormGroup>
</div>
<ShippingRatesInfoModal
v-if="isShippingModalOpened"
v-model="isShippingModalOpened"
:read-only="isModalReadOnly"
:shipping-info="shippingInfo"
@on-update-shipping-rates="
(value) => emit('on-update-shipping-rates', value)"
/>
</UCard>
</template>

<script setup lang="ts">
const props = defineProps({
isLoading: {
type: Boolean,
default: false
},
readOnly: {
type: Boolean,
default: false
},
shippingInfo: {
type: Array,
default: () => []
},
modelValue: {
type: Boolean
},
isNewListingPage: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['update:modelValue', 'on-update-shipping-rates'])
const isShippingModalOpened = ref<Boolean>(false)
const hasShipping = ref(props.modelValue)
const isModalReadOnly = ref(props.readOnly)
watch(() => props.modelValue, (newValue) => {
hasShipping.value = newValue
})
watch(hasShipping, (hasShipping) => {
emit('update:modelValue', hasShipping)
})
const isEditMode = computed(() => !(props.readOnly))
const isViewMode = computed(() => (props.readOnly))
const shouldHideViewButtonOnViewMode = computed(() => Boolean(isViewMode.value && props.isNewListingPage))
const buttonConfig = computed(() => {
if (isEditMode.value) {
if (props.shippingInfo.length) {
return {
icon: 'i-heroicons-pencil-square',
text: 'Edit',
action: handleOpenShippingModal
}
} else {
return {
icon: 'i-heroicons-plus-20-solid',
text: 'Add',
action: handleOpenShippingModal
}
}
} else if (isViewMode.value) {
if (props.shippingInfo.length) {
return {
icon: 'i-heroicons-eye-20-solid',
text: 'View Current Shipping Options',
action: handleOpenShippingModal
}
} else {
return {
icon: 'i-heroicons-plus-20-solid',
text: 'Add Shipping Options',
action: handleOpenEditModal
}
}
}
})
const shippingRatesTableRows = computed(() => {
if (!props.shippingInfo.length) {
return []
}
return props.shippingInfo.map((r: any, index: number) => ({
index: index + 1,
name: r.name,
price: r.priceInDecimal / 100
}))
})
function handleOpenShippingModal () {
isShippingModalOpened.value = true
}
function handleOpenEditModal () {
isModalReadOnly.value = false
isShippingModalOpened.value = true
}
</script>
Loading

0 comments on commit 32d2eb1

Please sign in to comment.