diff options
| author | trisusilo48 <tri.susilo@altama.co.id> | 2024-07-10 15:58:51 +0700 |
|---|---|---|
| committer | trisusilo48 <tri.susilo@altama.co.id> | 2024-07-10 15:58:51 +0700 |
| commit | 2e3c726bc8217f3960cfecec44b81303b03de72b (patch) | |
| tree | 1b85ced7f61f3e4c3f1f27b577b37aa161615065 /src-migrate/modules/cart | |
| parent | 2b3bd9c0a454dbad69ce29cee877bfb1fca5dfa6 (diff) | |
| parent | a99bf6480eea556e53b85e6db45f3b8c2361e693 (diff) | |
Merge branch 'release' into development
# Conflicts:
# src/pages/shop/product/variant/[slug].jsx
Diffstat (limited to 'src-migrate/modules/cart')
| -rw-r--r-- | src-migrate/modules/cart/components/CartSummaryMobile.tsx | 111 | ||||
| -rw-r--r-- | src-migrate/modules/cart/components/Item.tsx | 156 | ||||
| -rw-r--r-- | src-migrate/modules/cart/components/ItemAction.tsx | 111 | ||||
| -rw-r--r-- | src-migrate/modules/cart/components/ItemPromo.tsx | 44 | ||||
| -rw-r--r-- | src-migrate/modules/cart/components/ItemSelect.tsx | 54 | ||||
| -rw-r--r-- | src-migrate/modules/cart/components/Summary.tsx | 75 | ||||
| -rw-r--r-- | src-migrate/modules/cart/stores/useCartStore.ts | 64 | ||||
| -rw-r--r-- | src-migrate/modules/cart/styles/item-action.module.css | 32 | ||||
| -rw-r--r-- | src-migrate/modules/cart/styles/item-promo.module.css | 31 | ||||
| -rw-r--r-- | src-migrate/modules/cart/styles/item.module.css | 60 | ||||
| -rw-r--r-- | src-migrate/modules/cart/styles/summary.module.css | 21 |
11 files changed, 759 insertions, 0 deletions
diff --git a/src-migrate/modules/cart/components/CartSummaryMobile.tsx b/src-migrate/modules/cart/components/CartSummaryMobile.tsx new file mode 100644 index 00000000..d9f72e0e --- /dev/null +++ b/src-migrate/modules/cart/components/CartSummaryMobile.tsx @@ -0,0 +1,111 @@ +import style from '../styles/summary.module.css'; + +import React, { useState } from 'react'; +import formatCurrency from '~/libs/formatCurrency'; +import clsxm from '~/libs/clsxm'; +import { Button, Skeleton } from '@chakra-ui/react'; +import _ from 'lodash'; +import { ChevronDownIcon } from '@heroicons/react/24/outline'; +import BottomPopup from '@/core/components/elements/Popup/BottomPopup'; +import useDevice from '@/core/hooks/useDevice'; + +type Props = { + total?: number; + discount?: number; + subtotal?: number; + tax?: number; + shipping?: number; + grandTotal?: number; + isLoaded: boolean; +}; + +const CartSummaryMobile = ({ + total, + discount, + subtotal, + tax, + shipping, + grandTotal, + isLoaded = false, +}: Props) => { + const [showPopup, setShowPopup] = useState(false); + return ( + <> + <BottomPopup + className=' !h-[35%]' + title='Ringkasan Pensanan' + active={showPopup} + close={() => setShowPopup(false)} + > + <div className='mt-4'> + <div className='flex flex-col gap-y-3'> + <Skeleton isLoaded={isLoaded} className={style.line}> + <span className={style.label}>Total Belanja</span> + <span className={style.value}> + Rp {formatCurrency(subtotal || 0)} + </span> + </Skeleton> + + <Skeleton isLoaded={isLoaded} className={style.line}> + <span className={style.label}>Total Diskon</span> + <span className={clsxm(style.value, style.discount)}> + - Rp {formatCurrency(discount || 0)} + </span> + </Skeleton> + + <div className={style.divider} /> + + <Skeleton isLoaded={isLoaded} className={style.line}> + <span className={style.label}>Subtotal</span> + <span className={style.value}> + Rp {formatCurrency(total || 0)} + </span> + </Skeleton> + + <Skeleton isLoaded={isLoaded} className={style.line}> + <span className={style.label}>Tax 11%</span> + <span className={style.value}>Rp {formatCurrency(tax || 0)}</span> + </Skeleton> + + <Skeleton isLoaded={isLoaded} className={style.line}> + <span className={style.label}>Biaya Kirim</span> + <span className={style.value}> + Rp {formatCurrency(shipping || 0)} + </span> + </Skeleton> + + <div className={style.divider} /> + <Skeleton isLoaded={isLoaded} className={style.line}> + <span className={clsxm(style.label, style.grandTotal)}> + Grand Total + </span> + <span className={style.value}> + Rp {formatCurrency(grandTotal || 0)} + </span> + </Skeleton> + </div> + </div> + </BottomPopup> + <div className='flex flex-col gap-y-3'> + <Skeleton isLoaded={isLoaded} className={style.line}> + <span className={clsxm(style.label, style.grandTotal)}> + Grand Total + </span> + <button + onClick={() => setShowPopup(true)} + className='bg-gray-300 w-6 h-6 items-center justify-center cursor-pointer hover:bg-red-400 md:hidden ' + > + <ChevronDownIcon className='h-6 w-6 text-white' /> + </button> + </Skeleton> + <Skeleton isLoaded={isLoaded} className={style.line}> + <span className={style.value}> + Rp {formatCurrency(grandTotal || 0)} + </span> + </Skeleton> + </div> + </> + ); +}; + +export default CartSummaryMobile; diff --git a/src-migrate/modules/cart/components/Item.tsx b/src-migrate/modules/cart/components/Item.tsx new file mode 100644 index 00000000..6ded6373 --- /dev/null +++ b/src-migrate/modules/cart/components/Item.tsx @@ -0,0 +1,156 @@ +import style from '../styles/item.module.css' + +import { Skeleton, SkeletonProps, Tooltip } from '@chakra-ui/react' +import { InfoIcon } from 'lucide-react' +import Image from 'next/image' +import Link from 'next/link' + +import { PROMO_CATEGORY } from '~/constants/promotion' +import formatCurrency from '~/libs/formatCurrency' +import { createSlug } from '~/libs/slug' +import { CartItem as CartItemProps } from '~/types/cart' + +import CartItemAction from './ItemAction' +import CartItemPromo from './ItemPromo' +import CartItemSelect from './ItemSelect' + +type Props = { + item: CartItemProps + editable?: boolean +} + +const CartItem = ({ item, editable = true }: Props) => { + return ( + <div className={style.wrapper}> + {item.cart_type === 'promotion' && ( + <div className={style.header}> + {item.promotion_type?.value && ( + <Tooltip label={PROMO_CATEGORY[item.promotion_type?.value].description} placement="top" bgColor='red.600' p={2} rounded={6}> + <div className={style.badgeType}> + Paket {PROMO_CATEGORY[item.promotion_type?.value].alias} + <InfoIcon size={14} /> + </div> + </Tooltip> + )} + <div className='w-2' /> + <div> + Selamat! Pembelian anda lebih hemat {' '} + <span className={style.savingAmt}> + Rp{formatCurrency((item.package_price || 0) * item.quantity - item.subtotal)} + </span> + </div> + </div> + )} + + <div className={style.mainProdWrapper}> + {editable && <CartItemSelect item={item} />} + <div className='w-4' /> + + <CartItem.Image item={item} /> + + <div className={style.details}> + <CartItem.Name item={item} /> + + <div className='mt-2 flex justify-between w-full'> + <div className='flex flex-col gap-y-1'> + {item.cart_type === 'promotion' && ( + <div className={style.discPriceSection}> + <span className={style.priceBefore}> + Rp {formatCurrency((item.package_price || 0))} + </span> + <span className={style.price}> + Rp {formatCurrency(item.price.price)} + </span> + </div> + )} + + {item.cart_type === 'product' && ( + <div className={style.discPriceSection}> + {item.price.discount_percentage > 0 && ( + <span className={style.priceBefore}> + Rp {formatCurrency((item.price.price || 0))} + </span> + )} + + <div className={style.price}> + {item.price.price_discount > 0 && `Rp ${formatCurrency(item.price.price_discount)}`} + {item.price.price_discount === 0 && '-'} + </div> + </div> + )} + + <div>{item.cart_type === 'product' && item.code}</div> + <div>{Math.round(item.weight * 10) / 10} Kg</div> + </div> + + {editable && <CartItemAction item={item} />} + {!editable && <div className={style.quantity}>{item.quantity}</div>} + </div> + </div> + + </div> + + <div className="flex flex-col"> + {item.products?.map((product) => <CartItemPromo key={product.id} product={product} />)} + {item.free_products?.map((product) => <CartItemPromo key={product.id} product={product} />)} + </div> + </div> + ) +} + +CartItem.Image = function CartItemImage({ item }: { item: CartItemProps }) { + const image = item?.image || item?.parent?.image + + return ( + <> + {item.cart_type === 'promotion' && ( + <div className={style.image}> + {image && <Image src={image} alt={item.name} width={128} height={128} />} + {!image && <div className={style.noImage}>No Image</div>} + </div> + )} + + {item.cart_type === 'product' && ( + <Link + href={createSlug('/shop/product/', item.parent.name, item.parent.id.toString())} + className={style.image} + > + {image && <Image src={image} alt={item.name} width={128} height={128} />} + {!image && <div className={style.noImage}>No Image</div>} + </Link> + )} + </> + ) +} + +CartItem.Name = function CartItemName({ item }: { item: CartItemProps }) { + return ( + <> + {item.cart_type === 'promotion' && ( + <div className={style.name}>{item.name}</div> + )} + + {item.cart_type === 'product' && ( + <Link + href={createSlug('/shop/product/', item.parent.name, item.parent.id.toString())} + className={style.name} + > + {item.name} + </Link> + )} + </> + ) +} + +CartItem.Skeleton = function CartItemSkeleton(props: SkeletonProps & { count: number }) { + return Array.from({ length: props.count }).map((_, index) => ( + <Skeleton key={index} + height='100px' + width='100%' + rounded='md' + {...props} + /> + )) +} + +export default CartItem
\ No newline at end of file diff --git a/src-migrate/modules/cart/components/ItemAction.tsx b/src-migrate/modules/cart/components/ItemAction.tsx new file mode 100644 index 00000000..e73d507b --- /dev/null +++ b/src-migrate/modules/cart/components/ItemAction.tsx @@ -0,0 +1,111 @@ +import style from '../styles/item-action.module.css' + +import React, { useEffect, useState } from 'react' + +import { Spinner, Tooltip } from '@chakra-ui/react' +import { MinusIcon, PlusIcon, Trash2Icon } from 'lucide-react' + +import { CartItem } from '~/types/cart' +import { getAuth } from '~/libs/auth' +import { deleteUserCart, upsertUserCart } from '~/services/cart' + +import { useDebounce } from 'usehooks-ts' +import { useCartStore } from '../stores/useCartStore' + + +type Props = { + item: CartItem +} + +const CartItemAction = ({ item }: Props) => { + const auth = getAuth() + + const [isLoadDelete, setIsLoadDelete] = useState<boolean>(false) + const [isLoadQuantity, setIsLoadQuantity] = useState<boolean>(false) + + const [quantity, setQuantity] = useState<number>(item.quantity) + + const { loadCart } = useCartStore() + + const limitQty = item.limit_qty?.transaction || 0 + + const handleDelete = async () => { + if (typeof auth !== 'object') return + + setIsLoadDelete(true) + await deleteUserCart(auth.id, [item.cart_id]) + await loadCart(auth.id) + setIsLoadDelete(false) + } + + const decreaseQty = () => { setQuantity((quantity) => quantity -= 1) } + const increaseQty = () => { setQuantity((quantity) => quantity += 1) } + const debounceQty = useDebounce(quantity, 1000) + useEffect(() => { + if (isNaN(debounceQty)) setQuantity(1) + if (limitQty > 0 && debounceQty > limitQty) setQuantity(limitQty) + }, [debounceQty, limitQty]) + + useEffect(() => { + const updateCart = async () => { + if (typeof auth !== 'object' || isNaN(debounceQty)) return + + setIsLoadQuantity(true) + await upsertUserCart({ + userId: auth.id, + type: item.cart_type, + id: item.id, + qty: debounceQty, + selected: item.selected, + }) + await loadCart(auth.id) + setIsLoadQuantity(false) + } + updateCart() + //eslint-disable-next-line react-hooks/exhaustive-deps + }, [debounceQty]) + + return ( + <div className={style.actionSection}> + <button className={style.deleteButton} onClick={handleDelete} disabled={isLoadDelete}> + {isLoadDelete && <Spinner size='xs' />} + {!isLoadDelete && <Trash2Icon size={16} />} + </button> + + <div className={style.quantitySection}> + {isLoadQuantity && ( + <div className={style.quantityLoading}> + <Spinner size='sm' /> + </div> + )} + + <button + className={style.quantityControl} + onClick={decreaseQty} + disabled={quantity <= 1} + > + <MinusIcon size={16} /> + </button> + + <input + type='number' + className={style.quantity.toString()} + onChange={(e) => setQuantity(parseInt(e.target.value))} + value={quantity} + /> + + <Tooltip label={limitQty > 0 ? `Max. ${limitQty}` : ''}> + <button + className={style.quantityControl} + onClick={increaseQty} + disabled={limitQty > 0 && quantity >= limitQty} + > + <PlusIcon size={16} /> + </button> + </Tooltip> + </div> + </div> + ) +} + +export default CartItemAction
\ No newline at end of file diff --git a/src-migrate/modules/cart/components/ItemPromo.tsx b/src-migrate/modules/cart/components/ItemPromo.tsx new file mode 100644 index 00000000..878e17ac --- /dev/null +++ b/src-migrate/modules/cart/components/ItemPromo.tsx @@ -0,0 +1,44 @@ +import style from '../styles/item-promo.module.css' + +import Image from 'next/image' +import Link from 'next/link' +import { createSlug } from '~/libs/slug' + +import { CartProduct } from '~/types/cart' + +type Props = { + product: CartProduct +} + +const CartItemPromo = ({ product }: Props) => { + return ( + <div key={product.id} className={style.wrapper}> + <Link href={createSlug('/shop/product/', product.parent.name, product.parent.id.toString())} className={style.imageWrapper}> + {product?.image && <Image src={product.image} alt={product.name} width={128} height={128} className={style.image} />} + </Link> + + <div className={style.details}> + <Link href={createSlug('/shop/product/', product.parent.name, product.parent.id.toString())} className={style.name}> + {product.display_name} + </Link> + + <div className='flex w-full'> + <div className="flex flex-col"> + <div className={style.code}>{product.code}</div> + <div> + <span className={style.weightLabel}>Berat Barang: </span> + <span>{product.package_weight} Kg</span> + </div> + </div> + + <div className={style.quantity}> + {product.qty} + </div> + </div> + </div> + + </div> + ) +} + +export default CartItemPromo
\ No newline at end of file diff --git a/src-migrate/modules/cart/components/ItemSelect.tsx b/src-migrate/modules/cart/components/ItemSelect.tsx new file mode 100644 index 00000000..b904a1de --- /dev/null +++ b/src-migrate/modules/cart/components/ItemSelect.tsx @@ -0,0 +1,54 @@ +import { Checkbox, Spinner } from '@chakra-ui/react' +import React, { useState } from 'react' + +import { getAuth } from '~/libs/auth' +import { CartItem } from '~/types/cart' +import { upsertUserCart } from '~/services/cart' + +import { useCartStore } from '../stores/useCartStore' + +type Props = { + item: CartItem +} + +const CartItemSelect = ({ item }: Props) => { + const auth = getAuth() + const { loadCart } = useCartStore() + + const [isLoad, setIsLoad] = useState<boolean>(false) + + const handleChange = async (e: React.ChangeEvent<HTMLInputElement>) => { + if (typeof auth !== 'object') return + + setIsLoad(true) + await upsertUserCart({ + userId: auth.id, + type: item.cart_type, + id: item.id, + qty: item.quantity, + selected: e.target.checked + }) + await loadCart(auth.id) + setIsLoad(false) + } + + return ( + <div className='w-6 my-auto'> + {isLoad && ( + <Spinner className='my-auto' size='sm' /> + )} + + {!isLoad && ( + <Checkbox + borderColor='gray.600' + colorScheme='red' + size='lg' + isChecked={item.selected} + onChange={handleChange} + /> + )} + </div> + ) +} + +export default CartItemSelect
\ No newline at end of file diff --git a/src-migrate/modules/cart/components/Summary.tsx b/src-migrate/modules/cart/components/Summary.tsx new file mode 100644 index 00000000..2e55c8df --- /dev/null +++ b/src-migrate/modules/cart/components/Summary.tsx @@ -0,0 +1,75 @@ +import style from '../styles/summary.module.css' + +import React from 'react' +import formatCurrency from '~/libs/formatCurrency' +import clsxm from '~/libs/clsxm' +import { Skeleton } from '@chakra-ui/react' +import _ from 'lodash' + +type Props = { + total?: number + discount?: number + subtotal?: number + tax?: number + shipping?: number + grandTotal?: number + isLoaded: boolean +} + +const CartSummary = ({ + total, + discount, + subtotal, + tax, + shipping, + grandTotal, + isLoaded = false, +}: Props) => { + return ( + <> + <div className='text-h-sm font-medium'>Ringkasan Pesanan</div> + + <div className="h-6" /> + + <div className='flex flex-col gap-y-3'> + <Skeleton isLoaded={isLoaded} className={style.line}> + <span className={style.label}>Total Belanja</span> + <span className={style.value}>Rp {formatCurrency(subtotal || 0)}</span> + </Skeleton> + + <Skeleton isLoaded={isLoaded} className={style.line}> + <span className={style.label}>Total Diskon</span> + <span className={clsxm(style.value, style.discount)}>- Rp {formatCurrency(discount || 0)}</span> + </Skeleton> + + <div className={style.divider} /> + + <Skeleton isLoaded={isLoaded} className={style.line}> + <span className={style.label}>Subtotal</span> + <span className={style.value}>Rp {formatCurrency(total || 0)}</span> + </Skeleton> + + <Skeleton isLoaded={isLoaded} className={style.line}> + <span className={style.label}>Tax 11%</span> + <span className={style.value}>Rp {formatCurrency(tax || 0)}</span> + </Skeleton> + + <Skeleton isLoaded={isLoaded} className={style.line}> + <span className={style.label}>Biaya Kirim</span> + <span className={style.value}>Rp {formatCurrency(shipping || 0)}</span> + </Skeleton> + + <div className={style.divider} /> + + <Skeleton isLoaded={isLoaded} className={style.line}> + <span className={clsxm(style.label, style.grandTotal)}> + Grand Total + </span> + <span className={style.value}>Rp {formatCurrency(grandTotal || 0)}</span> + </Skeleton> + </div> + </> + ) +} + +export default CartSummary
\ No newline at end of file diff --git a/src-migrate/modules/cart/stores/useCartStore.ts b/src-migrate/modules/cart/stores/useCartStore.ts new file mode 100644 index 00000000..3d9a0aed --- /dev/null +++ b/src-migrate/modules/cart/stores/useCartStore.ts @@ -0,0 +1,64 @@ +import { create } from 'zustand'; +import { CartProps } from '~/types/cart'; +import { getUserCart } from '~/services/cart'; + +type State = { + cart: CartProps | null; + isLoadCart: boolean; + summary: { + subtotal: number; + discount: number; + total: number; + tax: number; + grandTotal: number; + }; +}; + +type Action = { + loadCart: (userId: number) => Promise<void>; +}; + +export const useCartStore = create<State & Action>((set, get) => ({ + cart: null, + isLoadCart: false, + summary: { + subtotal: 0, + discount: 0, + total: 0, + tax: 0, + grandTotal: 0, + }, + loadCart: async (userId) => { + if (get().isLoadCart === true) return; + + set({ isLoadCart: true }); + const cart: CartProps = (await getUserCart(userId)) as CartProps; + set({ cart }); + set({ isLoadCart: false }); + + const summary = computeSummary(cart); + set({ summary }); + }, +})); + +const computeSummary = (cart: CartProps) => { + let subtotal = 0; + let discount = 0; + for (const item of cart.products) { + if (!item.selected) continue; + + let price = 0; + if (item.cart_type === 'promotion') + price = (item?.package_price || 0) * item.quantity; + else if (item.cart_type === 'product') + price = item.price.price * item.quantity; + + subtotal += price; + discount += price - item.price.price_discount * item.quantity; + } + let total = subtotal - discount; + let tax = Math.round(total * 0.11); + let grandTotal = total + tax; + + return { subtotal, discount, total, tax, grandTotal }; +}; diff --git a/src-migrate/modules/cart/styles/item-action.module.css b/src-migrate/modules/cart/styles/item-action.module.css new file mode 100644 index 00000000..e4db7fa5 --- /dev/null +++ b/src-migrate/modules/cart/styles/item-action.module.css @@ -0,0 +1,32 @@ +.actionSection { + @apply flex ml-auto h-fit my-auto; +} + +.deleteButton { + @apply bg-red-100 disabled:bg-gray-100 + text-red-700 disabled:text-gray-500 + hover:bg-red-200 + disabled:cursor-not-allowed + transition-all + p-2.5 rounded; +} + +.quantitySection { + @apply relative flex border border-gray-300 rounded ml-4 items-center text-red-700; +} + +.quantityLoading { + @apply absolute flex items-center justify-center text-white rounded w-full h-full bg-gray-900/50 z-10; +} + +.quantityControl { + @apply h-full w-8 flex items-center justify-center hover:bg-gray-100 + disabled:text-gray-500 + disabled:bg-transparent + disabled:cursor-not-allowed + transition; +} + +.quantity { + @apply text-gray-900 font-medium max-w-[28px] outline-none text-center; +} diff --git a/src-migrate/modules/cart/styles/item-promo.module.css b/src-migrate/modules/cart/styles/item-promo.module.css new file mode 100644 index 00000000..15bf8146 --- /dev/null +++ b/src-migrate/modules/cart/styles/item-promo.module.css @@ -0,0 +1,31 @@ +.wrapper { + @apply md:ml-16 ml-8 mt-4 flex; +} + +.imageWrapper { + @apply md:h-24 md:w-24 md:min-w-[96px] + h-20 w-20 min-w-[80px] + border border-gray-300 rounded + p-2.5; +} + +.image { + @apply w-full h-full object-contain; +} + +.details { + @apply ml-4 w-full flex flex-col gap-y-1; +} + +.name { + @apply font-medium; +} + +.code, +.weightLabel { + @apply text-gray-600; +} + +.quantity { + @apply w-12 min-w-[42px] py-2.5 bg-gray-100 border border-gray-300 h-fit my-auto rounded-md ml-auto font-medium text-center; +} diff --git a/src-migrate/modules/cart/styles/item.module.css b/src-migrate/modules/cart/styles/item.module.css new file mode 100644 index 00000000..dfbbf5e8 --- /dev/null +++ b/src-migrate/modules/cart/styles/item.module.css @@ -0,0 +1,60 @@ +.wrapper { + @apply border-b border-gray-300 pb-8; +} + +.header { + @apply mb-4 flex items-center text-caption-1 leading-6; +} + +.badgeType { + @apply min-w-fit p-2 flex items-center gap-x-1.5 rounded-md border border-danger-500 text-danger-500; +} + +.mainProdWrapper { + @apply flex; +} + +.image { + @apply md:h-32 md:w-32 md:min-w-[128px] + w-24 h-24 min-w-[96px] rounded flex p-2 border border-gray-300; +} + +.noImage { + @apply m-auto font-semibold text-gray-400; +} + +.details { + @apply ml-4 flex flex-col gap-y-1 w-full; +} + +.name { + @apply font-medium; +} + +.spacing2 { + @apply h-2; +} + +.discPriceSection { + @apply flex flex-col md:flex-row gap-x-2.5; +} + +.priceBefore { + @apply line-through text-gray-500; +} + +.price { + @apply text-red-600 font-medium; +} + +.savingAmt { + @apply text-success-600; +} + +.weightLabel { + @apply text-gray-500; +} + +.quantity { + @apply py-2.5 bg-red-100 border border-red-300 text-red-800 h-fit my-auto rounded-md ml-auto font-medium w-12 text-center; +} diff --git a/src-migrate/modules/cart/styles/summary.module.css b/src-migrate/modules/cart/styles/summary.module.css new file mode 100644 index 00000000..48ccec28 --- /dev/null +++ b/src-migrate/modules/cart/styles/summary.module.css @@ -0,0 +1,21 @@ +.line { + @apply flex justify-between; +} + +.label, +.value { + @apply text-gray-700; +} + +.value, +.grandTotal { + @apply font-medium; +} + +.discount { + @apply text-red-700; +} + +.divider { + @apply my-0.5 h-0.5 bg-gray-200; +} |
