diff options
| author | it-fixcomart <it@fixcomart.co.id> | 2024-11-28 10:47:43 +0700 |
|---|---|---|
| committer | it-fixcomart <it@fixcomart.co.id> | 2024-11-28 10:47:43 +0700 |
| commit | 5bc7a6807847610b190ea9d5046021d2db15afc5 (patch) | |
| tree | e895b02c65bf97e3c6c970bb8d777922120f4570 | |
| parent | 7ed3fd96322d08bd91434b8ec4dcbc542a610998 (diff) | |
| parent | 952421c810b53ec4d25ad5ef605bae1bd1d5d616 (diff) | |
Merge branch 'new-release' into Feature/switch-account
85 files changed, 4786 insertions, 1724 deletions
diff --git a/package.json b/package.json index a846749c..fd8dee81 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@chakra-ui/react": "^2.8.1", + "@choc-ui/chakra-autocomplete": "^5.6.2", "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", "@heroicons/react": "^2.0.13", @@ -47,6 +48,7 @@ "react-query": "^3.39.3", "react-select": "^5.8.0", "react-web-share": "^2.0.2", + "redis": "^4.7.0", "snakecase-keys": "^5.5.0", "swiper": "^8.4.4", "tw-merge": "^0.0.1-alpha.3", diff --git a/public/images/CHECKOUT-PESANAN.svg b/public/images/CHECKOUT-PESANAN.svg new file mode 100644 index 00000000..6291b4cd --- /dev/null +++ b/public/images/CHECKOUT-PESANAN.svg @@ -0,0 +1,958 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 24.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 500 500" style="enable-background:new 0 0 500 500;" xml:space="preserve"> +<style type="text/css"> + .st0{fill:#EBEBEB;} + .st1{fill:#FAFAFA;} + .st2{fill:#E0E0E0;} + .st3{fill:#F5F5F5;} + .st4{fill:#FFFFFF;} + .st5{display:none;} + .st6{display:inline;} + .st7{display:inline;fill:#EBEBEB;} + .st8{fill:#263238;} + .st9{fill:#EA0000;} + .st10{font-family:'Gilroy-Bold';} + .st11{font-size:12.4079px;} + .st12{font-family:'Gilroy-Regular';} + .st13{font-size:9.0789px;} + .st14{fill:#FFBE9D;} + .st15{fill:#EB996E;} + .st16{fill:#455A64;} + .st17{fill:none;stroke:#FFFFFF;stroke-width:2.000000e-02;stroke-miterlimit:10;} + .st18{opacity:0.3;} + .st19{fill:#E8505B;} + .st20{fill:#CC0000;} +</style> +<g id="Background_Complete"> + <g> + <g> + <g> + <path class="st0" d="M450.1,142.2L360.5,162c-3.7,0.8-7.3-1.5-8.1-5.2l-18.7-84.3c-0.8-3.7,1.5-7.3,5.2-8.1l89.6-19.8 + c3.7-0.8,7.3,1.5,8.1,5.2l18.7,84.3C456,137.8,453.7,141.4,450.1,142.2z"/> + </g> + </g> + <g> + <g> + <g> + <g> + <g> + <g> + <path class="st1" d="M414.9,114.2c0,0-0.2,0.1-0.7,0.2c-0.5,0.1-1.2,0.3-2,0.5c-1.8,0.4-4.3,1-7.4,1.7 + c-6.3,1.4-15,3.4-24.7,5.6l-0.2,0l-0.1-0.2c-0.5-1.3-1-2.6-1.5-4c-4.3-11.6-8.2-22-10.7-28.8l0.2,0.1 + c-2.8,0.6-5,1.1-6.6,1.4c-0.7,0.1-1.3,0.3-1.8,0.4c-0.4,0.1-0.6,0.1-0.6,0.1c0,0,0.2-0.1,0.6-0.2c0.4-0.1,1-0.3,1.7-0.4 + c1.6-0.4,3.8-0.9,6.6-1.5l0.1,0l0,0.1c2.6,6.8,6.5,17.1,10.9,28.7c0.5,1.4,1,2.7,1.5,4l-0.3-0.1c9.8-2.2,18.5-4.1,24.8-5.5 + c3.1-0.7,5.6-1.2,7.4-1.6c0.8-0.2,1.5-0.3,2-0.4C414.7,114.2,414.9,114.2,414.9,114.2z"/> + </g> + </g> + </g> + <g> + <g> + <g> + <path class="st1" d="M377.5,115c0,0,0.1,0,0.2-0.1c0.1,0,0.3-0.1,0.5-0.1c0.5-0.1,1.1-0.3,2-0.5c1.8-0.4,4.3-1,7.4-1.7 + c6.4-1.5,15.3-3.5,25.7-5.8l-0.2,0.2c-0.2-4.5-0.4-9.3-0.7-14.4c-0.1-2.6-0.2-5.1-0.3-7.5l0.3,0.2c-12,2.7-22.6,5-30.3,6.7 + c-3.8,0.8-6.8,1.5-9,1.9c-1,0.2-1.8,0.4-2.4,0.5c-0.3,0-0.5,0.1-0.6,0.1c-0.1,0-0.2,0-0.2,0c0,0,0.1,0,0.2-0.1 + c0.2,0,0.4-0.1,0.6-0.2c0.6-0.1,1.4-0.3,2.4-0.6c2.1-0.5,5.2-1.2,8.9-2.1c7.6-1.7,18.2-4.1,30.2-6.9l0.3-0.1l0,0.3 + c0.1,2.4,0.2,5,0.3,7.5c0.2,5,0.4,9.9,0.6,14.4l0,0.2l-0.2,0c-10.4,2.3-19.3,4.3-25.7,5.7c-3.2,0.7-5.7,1.2-7.5,1.6 + c-0.8,0.2-1.5,0.3-2,0.4c-0.2,0-0.4,0.1-0.5,0.1C377.6,115,377.5,115,377.5,115z"/> + </g> + </g> + </g> + <g> + <g> + <g> + <path class="st1" d="M391,129.5c0,0-0.1-0.3-0.4-0.8c-0.3-0.5-0.8-1.1-1.7-1.5c-0.9-0.4-2.1-0.4-3.2,0.3 + c-1.1,0.7-1.8,2.1-1.4,3.5c0.3,1.4,1.6,2.4,2.8,2.6c1.3,0.2,2.4-0.4,3-1.1c0.6-0.7,0.8-1.6,0.9-2.1 + C391,129.8,390.9,129.5,391,129.5c0,0,0.2,0.3,0.2,0.9c0,0.6-0.1,1.5-0.8,2.3c-0.6,0.8-1.9,1.5-3.3,1.3 + c-1.4-0.1-2.9-1.3-3.2-2.9c-0.4-1.6,0.5-3.3,1.7-4c1.2-0.8,2.7-0.7,3.6-0.2c1,0.5,1.5,1.2,1.7,1.7 + C391,129.2,391,129.5,391,129.5z"/> + </g> + </g> + </g> + <g> + <g> + <g> + <path class="st1" d="M413.3,124.5c0,0-0.1-0.3-0.4-0.8c-0.3-0.5-0.8-1.1-1.7-1.5c-0.9-0.4-2.1-0.4-3.2,0.3 + c-1.1,0.7-1.8,2.1-1.4,3.5c0.3,1.4,1.6,2.4,2.8,2.6c1.3,0.2,2.4-0.4,3-1.1c0.6-0.7,0.8-1.6,0.9-2.1 + C413.4,124.8,413.3,124.5,413.3,124.5c0,0,0.2,0.3,0.2,0.9c0,0.6-0.1,1.5-0.8,2.3c-0.6,0.8-1.9,1.5-3.3,1.3 + c-1.4-0.1-2.9-1.3-3.2-2.9c-0.4-1.6,0.5-3.3,1.7-4c1.2-0.8,2.7-0.7,3.6-0.2c1,0.5,1.5,1.2,1.7,1.7 + C413.4,124.1,413.4,124.5,413.3,124.5z"/> + </g> + </g> + </g> + <g> + <g> + <path class="st1" d="M419.6,79.8c1.2,5.5-2.2,10.9-7.7,12.2c-5.5,1.2-10.9-2.2-12.2-7.7c-1.2-5.5,2.2-10.9,7.7-12.2 + C412.9,70.8,418.3,74.3,419.6,79.8z"/> + </g> + </g> + </g> + <g> + <g> + <g> + <path class="st2" d="M414.2,77.1c0.1,0-0.3,0.8-0.9,1.9c-0.6,1.1-1.5,2.7-2.5,4.4c-0.4,0.6-0.7,1.2-1.1,1.8l-0.1,0.2 + l-0.2-0.1c-2.6-1.4-4.4-2.5-4.3-2.6c0.1-0.1,2,0.8,4.6,2.2l-0.3,0.1c0.3-0.6,0.7-1.2,1.1-1.8c1-1.7,2-3.2,2.7-4.3 + C413.7,77.7,414.1,77,414.2,77.1z"/> + </g> + </g> + </g> + </g> + </g> + </g> + <g> + <g> + <g> + <path class="st2" d="M72.3,194.4c-1.8-6.5-10.3-5.9-10.3-5.9l0.2,4.5c4.9-0.8,8,4.9,8,4.9L72.3,194.4z"/> + </g> + </g> + <g> + <g> + <path class="st2" d="M197,118.9c9.8,3,16-8.4,16-8.4l-6-3.4c-2.9,7-12.8,6.2-12.8,6.2L197,118.9z"/> + </g> + </g> + <g> + <g> + <path class="st2" d="M278.7,179.9c-1.9-5.4-9.1-4.4-9.1-4.4l0.4,3.8c4.1-1,7,3.7,7,3.7L278.7,179.9z"/> + </g> + </g> + <g> + <g> + <path class="st2" d="M165.8,52.7c-1.9-5.4-9.1-4.4-9.1-4.4l0.4,3.8c4.1-1,7,3.7,7,3.7L165.8,52.7z"/> + </g> + </g> + <g> + <g> + <path class="st0" d="M80.4,122.2c-1.9-5.4-9.1-4.4-9.1-4.4l0.4,3.8c4.1-1,7,3.7,7,3.7L80.4,122.2z"/> + </g> + </g> + <g> + <g> + <path class="st0" d="M62.5,71.4c-5.7,0.9-6,8.1-6,8.1l3.9,0.2c-0.2-4.2,4.9-6.2,4.9-6.2L62.5,71.4z"/> + </g> + </g> + <g> + <g> + <path class="st0" d="M21.6,240.4c9.9,3,16-8.4,16-8.4l-6-3.4c-2.9,7-12.8,6.2-12.8,6.2L21.6,240.4z"/> + </g> + </g> + <g> + <g> + <path class="st2" d="M302.2,119.6c0.7-4.6-4.8-6.5-4.8-6.5l-1.1,2.9c3.3,0.8,3.6,5.2,3.6,5.2L302.2,119.6z"/> + </g> + </g> + <g> + <g> + <path class="st0" d="M269.5,69.5c-3.8,2.5-1.7,7.9-1.7,7.9l2.9-1.1c-1.6-3,1.5-6.2,1.5-6.2L269.5,69.5z"/> + </g> + </g> + </g> + <g> + <g> + <g> + <g> + <polygon class="st3" points="436.1,171.6 422.8,210.7 462.1,218.5 464.4,177.2 "/> + </g> + </g> + </g> + <g> + <g> + <g> + <g> + <path class="st4" d="M460,182.4c0,0-0.1,0.4-0.4,1.1c-0.4,0.7-1,1.6-2,2.6c-1,1-2.4,2-4.2,2.6c-1.8,0.6-3.9,0.7-6,0.3 + c-2.1-0.4-4-1.2-5.5-2.5c-1.5-1.2-2.3-2.7-2.8-4c-0.5-1.3-0.6-2.5-0.7-3.2c0-0.8,0-1.2,0.1-1.2c0.1,0,0,1.7,1.1,4.2 + c0.5,1.2,1.4,2.6,2.8,3.7c1.4,1.1,3.2,1.9,5.2,2.3c2,0.4,4,0.3,5.7-0.2c1.7-0.5,3-1.4,4-2.3 + C459.2,183.9,459.9,182.3,460,182.4z"/> + </g> + </g> + </g> + </g> + </g> +</g> +<g id="Background_Simple" class="st5"> + <g class="st6"> + <path class="st0" d="M462.6,168.7c-10.1-32-28.2-62.7-55.9-80.6c-25.8-16.7-58.9-21-88-11.3c-29,9.7-53.4,33.1-64.6,62.2 + c-6,15.6-8.4,32.4-13.4,48.4c-5,16-13.4,31.9-27.6,40.2c-15.9,9.3-35.8,7.3-53.6,2.5c-17.7-4.8-35.1-12.1-53.5-12.5 + c-38-0.8-67.2,33.9-75.5,72c-9.2,42.5-4.1,86.6,51.5,117.4c0.9,0.5,1.2,0.7,1.1,0.7c20.2,12.8,32,22.3,54.3,28.4 + c52.7,12.9,110.8,2.8,152.9,0c54.8-3.7,73.2-18.6,107.5-43.8c32.9-24.2,56.8-60.6,67.8-100.6C476.7,251.5,475.1,208.2,462.6,168.7 + z"/> + <path class="st0" d="M83.2,407.6c-0.4-0.2-0.7-0.5-1.1-0.7C78.8,405.6,83,407.6,83.2,407.6z"/> + </g> + <path class="st7" d="M175.1,60.3c-10.8-8.5-25.8-11-39.1-7.5c-11.6,3-22.4,10.8-26.5,22.1c-3.3,9-2,19.3,2.2,27.9 + c5.9,12.2,17.5,21.6,30.7,24.9c13.2,3.2,27.8,0.3,38.7-7.8l-1.8,0.7c6.6-4.5,7.8-13.9,9.9-23.7C191.9,83.5,185.9,68.8,175.1,60.3z" + /> +</g> +<g id="Desk"> + <g> + <g> + <path class="st8" d="M474.1,454.2c0,0.1-102.6,0.3-229.1,0.3c-126.5,0-229.1-0.1-229.1-0.3s102.5-0.3,229.1-0.3 + C371.5,453.9,474.1,454,474.1,454.2z"/> + </g> + </g> +</g> +<g id="Screen"> + <g> + <g> + <g> + <rect x="32.8" y="145.8" class="st1" width="243.9" height="190.9"/> + </g> + <g> + <g> + <path class="st8" d="M276.6,336.7c0-1.8-0.1-75.7-0.2-190.9l0.2,0.2c-69.5,0-153.4,0-243.8,0h0l0.3-0.3c0,68.8,0,133.9,0,190.9 + l-0.2-0.2C177.5,336.6,274.6,336.7,276.6,336.7c-2,0-99.1,0.1-243.9,0.2l-0.2,0l0-0.2c0-57,0-122.1,0-190.9l0-0.3h0.3h0 + c90.4,0,174.3,0,243.8,0l0.2,0l0,0.2C276.7,261,276.6,334.9,276.6,336.7z"/> + </g> + </g> + </g> + <g> + <g> + <path class="st9" d="M190.9,310.7h-72.5c-8.3,0-15-6.7-15-15l0,0c0-8.3,6.7-15,15-15h72.5c8.3,0,15,6.7,15,15l0,0 + C206,304,199.2,310.7,190.9,310.7z"/> + </g> + <g> + <path class="st1" d="M136.4,298.7c-0.8,0-1.4-0.2-2-0.5c-0.5-0.3-0.9-0.8-1.1-1.4l1.5-0.9c0.3,0.7,0.8,1.1,1.6,1.1 + c0.7,0,1-0.2,1-0.6c0-0.2-0.1-0.4-0.3-0.5c-0.2-0.1-0.6-0.3-1.3-0.4c-0.3-0.1-0.6-0.2-0.8-0.3c-0.2-0.1-0.5-0.3-0.7-0.4 + c-0.2-0.2-0.4-0.4-0.5-0.7s-0.2-0.6-0.2-0.9c0-0.7,0.3-1.3,0.8-1.7c0.5-0.4,1.1-0.6,1.8-0.6c0.6,0,1.2,0.1,1.7,0.4 + c0.5,0.3,0.9,0.7,1.1,1.3l-1.5,0.9c-0.1-0.3-0.3-0.5-0.5-0.7c-0.2-0.1-0.5-0.2-0.8-0.2c-0.3,0-0.5,0.1-0.6,0.2 + c-0.1,0.1-0.2,0.3-0.2,0.4c0,0.2,0.1,0.4,0.3,0.5c0.2,0.1,0.6,0.3,1.1,0.5c0.3,0.1,0.5,0.2,0.7,0.2c0.2,0.1,0.4,0.2,0.6,0.3 + c0.3,0.1,0.5,0.3,0.6,0.4s0.3,0.3,0.4,0.6c0.1,0.2,0.2,0.5,0.2,0.8c0,0.7-0.3,1.3-0.8,1.7C137.9,298.5,137.2,298.7,136.4,298.7z + "/> + <path class="st1" d="M142,296.8h3.1v1.7h-4.8v-7.7h4.8v1.7h-3v1.3h2.8v1.7H142V296.8z"/> + <path class="st1" d="M148.1,296.8h2.8v1.7h-4.5v-7.7h1.8V296.8z"/> + <path class="st1" d="M153.6,296.8h3.1v1.7h-4.8v-7.7h4.8v1.7h-3v1.3h2.8v1.7h-2.8V296.8z"/> + <path class="st1" d="M160.6,298.7c-0.8,0-1.4-0.2-2-0.5c-0.5-0.3-0.9-0.8-1.1-1.4l1.5-0.9c0.3,0.7,0.8,1.1,1.6,1.1 + c0.7,0,1-0.2,1-0.6c0-0.2-0.1-0.4-0.3-0.5c-0.2-0.1-0.6-0.3-1.3-0.4c-0.3-0.1-0.6-0.2-0.8-0.3c-0.2-0.1-0.5-0.3-0.7-0.4 + c-0.2-0.2-0.4-0.4-0.5-0.7s-0.2-0.6-0.2-0.9c0-0.7,0.3-1.3,0.8-1.7c0.5-0.4,1.1-0.6,1.8-0.6c0.6,0,1.2,0.1,1.7,0.4 + c0.5,0.3,0.9,0.7,1.1,1.3l-1.5,0.9c-0.1-0.3-0.3-0.5-0.5-0.7c-0.2-0.1-0.5-0.2-0.8-0.2c-0.3,0-0.5,0.1-0.6,0.2 + c-0.1,0.1-0.2,0.3-0.2,0.4c0,0.2,0.1,0.4,0.3,0.5c0.2,0.1,0.6,0.3,1.1,0.5c0.3,0.1,0.5,0.2,0.7,0.2c0.2,0.1,0.4,0.2,0.6,0.3 + c0.3,0.1,0.5,0.3,0.6,0.4s0.3,0.3,0.4,0.6c0.1,0.2,0.2,0.5,0.2,0.8c0,0.7-0.3,1.3-0.8,1.7C162.1,298.5,161.4,298.7,160.6,298.7z + "/> + <path class="st1" d="M169.4,298.5l-0.4-1.2h-3l-0.4,1.2h-1.9l2.6-7.7h2.2l2.6,7.7H169.4z M166.6,295.7h1.9l-1-2.8L166.6,295.7z" + /> + <path class="st1" d="M172.1,290.8h1.8v7.7h-1.8V290.8z"/> + </g> + </g> + <g> + <g> + <path class="st9" d="M156.7,219c-10.7,0-19.4-8.7-19.4-19.4c0-10.7,8.7-19.4,19.4-19.4s19.4,8.7,19.4,19.4 + C176.1,210.3,167.4,219,156.7,219z M156.7,182.2c-9.6,0-17.4,7.8-17.4,17.4c0,9.6,7.8,17.4,17.4,17.4c9.6,0,17.4-7.8,17.4-17.4 + C174.1,190,166.3,182.2,156.7,182.2z"/> + </g> + <g> + <path class="st9" d="M155.3,205.6c-0.2,0-0.5-0.1-0.7-0.2c-1.7-1.5-4.6-4.1-5-4.6c-0.3-0.5-0.2-1.1,0.3-1.4 + c0.5-0.3,1.1-0.2,1.4,0.3c0.3,0.3,2.1,2,4,3.7l9.8-9.3c0.4-0.4,1-0.4,1.4,0c0.4,0.4,0.4,1,0,1.4l-10.5,9.9 + C155.8,205.5,155.5,205.6,155.3,205.6z"/> + </g> + </g> + <g> + <g> + <path class="st8" d="M276.6,164.5c0,0.1-54.5,0.3-121.6,0.3c-67.2,0-121.6-0.1-121.6-0.3s54.4-0.3,121.6-0.3 + C222.2,164.2,276.6,164.3,276.6,164.5z"/> + </g> + </g> + <g> + <g> + <circle class="st8" cx="41.4" cy="154.9" r="1.4"/> + </g> + <g> + <circle class="st8" cx="46.7" cy="154.9" r="1.4"/> + </g> + <g> + <path class="st8" d="M53.3,154.9c0,0.8-0.6,1.4-1.4,1.4c-0.8,0-1.4-0.6-1.4-1.4c0-0.8,0.6-1.4,1.4-1.4 + C52.6,153.5,53.3,154.1,53.3,154.9z"/> + </g> + </g> + <text transform="matrix(1 0 0 1 61.8303 245.5229)" class="st8 st10 st11">Pembelianmu sudah kami terima</text> + <text transform="matrix(1 0 0 1 69.6563 258.5025)" class="st8 st12 st13">Mohon cek pesananmu di detail pesanan</text> + </g> +</g> +<g id="Character"> + <g> + <g> + <path class="st14" d="M436.9,274.1c0,0,12.1-7.4,14.9-5.2c3.9,3.1,5.3,8.4,5.3,8.4s6.4-24.4,12.5-23.5c3.5,0.5,1.8,3.3,0.2,6.7 + c-1.5,3.2-4,14.9-4,14.9s11.6-13.8,15-13.5c2.6,0.2,3,2.5-0.8,6.2c-3.8,3.7-10.1,15.4-10.1,15.4s8.1-7.6,11.5-7.6 + c2.7,0,4.4,1.8,0.9,4.8c-3.5,3-8.7,5.3-11.5,10.7c-3.4,6.7-10.8,23.4-10.8,23.4l-25.7-9.1c0,0-4.2-17.1-4.4-21.2 + c0,0,0.4-8.8,0.3-10.3c0-1.6,4.1-2,5.3,1c1.3,3,0.7,6.1,1.2,8.1c0.5,2,1.4,3.8,2.9,4.7c1.5,0.9,5.2,1,6.1-0.1 + c0.9-1.1,2.1-4.3,2.3-6.3c0.2-2-0.1-5.1-0.1-5.1S437.4,280.9,436.9,274.1z"/> + </g> + <g> + <g> + <g> + <path class="st14" d="M147.9,353.7L130,347c0,0-18.2-10.7-21.6-6.9c0,0-3.6-1.7-5.2,2.1c-0.9,2.2,2.1,5.5,2.1,5.5s-1,1,0.6,4.2 + c1.5,3.3,18.8,13.6,18.8,13.6s22.5,12.2,31.5,18.1l15.7-21.3l-2.2-0.8c0,0-1.3-10.6-5.6-13.5c-4.7-3.1-10.4-11.4-11.7-14.7 + c-1.3-3.3-7.3-1.8-6.3,5c1,6.8,6.3,8.5,5.8,13.1C151.4,356,147.9,353.7,147.9,353.7z"/> + </g> + <g> + <g> + <path class="st15" d="M120.5,356.2c-0.1,0.1-3.6-1.7-7.8-4.1c-4.2-2.4-7.6-4.4-7.5-4.5c0.1-0.1,3.6,1.7,7.8,4.1 + C117.2,354,120.5,356.1,120.5,356.2z"/> + </g> + </g> + <g> + <g> + <path class="st15" d="M129.2,351.2c0,0.1-1.3-0.4-3.2-1.3c-2-0.9-4.6-2.1-7.5-3.6c-2.9-1.5-5.5-3-7.2-4.1 + c-0.9-0.6-1.6-1.1-2.1-1.4c-0.5-0.4-0.7-0.6-0.7-0.6c0-0.1,1.1,0.7,3,1.7c1.8,1.1,4.4,2.5,7.3,4c2.9,1.5,5.5,2.7,7.5,3.7 + C128.1,350.5,129.2,351.1,129.2,351.2z"/> + </g> + </g> + <g> + <g> + <path class="st15" d="M133,361c0,0-0.6-0.3-1.6-0.9c-1.1-0.6-2.5-1.5-4.2-2.5l0,0l0-0.1c-0.8-1.7-1.8-3.7-3-5.8 + c-0.6-1-1.3-2.1-2.3-3c-0.9-0.9-2.4-1-3.5-0.3c-0.2,0.1-0.5,0.4-0.6,0.5c0,0,0,0.1,0,0.1l0.1,0.2c0,0.1,0.1,0.3,0.2,0.4 + c0.2,0.6,0.5,1.2,0.7,1.8c0.5,1.2,0.9,2.3,1.4,3.3c0.9,2.1,1.7,4.1,2.4,5.8l0-0.1c1.5,1.7,2.7,3.1,3.6,4.2 + c0.4,0.5,0.7,0.9,1,1.2c0.2,0.3,0.3,0.4,0.3,0.4c0,0-0.2-0.1-0.4-0.4c-0.3-0.3-0.6-0.7-1-1.1c-0.9-1-2.2-2.4-3.7-4.1l0,0l0,0 + c-0.7-1.7-1.6-3.6-2.5-5.7c-0.4-1.1-0.9-2.2-1.4-3.3c-0.2-0.6-0.5-1.2-0.7-1.8c-0.1-0.2-0.1-0.3-0.2-0.5l-0.1-0.2 + c0-0.1,0-0.2-0.1-0.4c0.1-0.3,0.2-0.4,0.4-0.5c0.1-0.1,0.3-0.2,0.4-0.3c0.6-0.4,1.4-0.6,2.2-0.6c0.8,0,1.4,0.4,2,0.9 + c1,1,1.7,2.1,2.4,3.1c1.3,2.1,2.2,4.2,3,5.9l-0.1-0.1c1.7,1.1,3.1,2,4.1,2.7C132.5,360.6,133,361,133,361z"/> + </g> + </g> + </g> + <g> + <g> + <path class="st15" d="M150.9,361.2c0,0.1-0.9-0.2-2.2-0.5c-1.3-0.4-3.2-0.9-5.3-0.9c-2.1,0-3.9,0.5-5.1,1.2 + c-1.2,0.7-1.7,1.4-1.8,1.4c0,0,0.4-0.8,1.6-1.7c1.2-0.8,3.1-1.5,5.3-1.5c2.2,0.1,4.1,0.6,5.4,1.1 + C150.1,360.8,150.9,361.1,150.9,361.2z"/> + </g> + </g> + <g> + <g> + <path class="st15" d="M149.5,356.5c0.1,0.1-0.3,0.9-0.8,1.9c-0.4,1-0.7,1.8-0.9,1.8c-0.1,0-0.1-1,0.4-2 + C148.8,357.1,149.4,356.4,149.5,356.5z"/> + </g> + </g> + </g> + <g> + <g> + <path class="st8" d="M252.6,271.5c1.2-8.4,11-20.5,19.7-20.5c-5-7.2-5.1-17.4-0.2-24.6c3.3-5,8.8-8.7,11-14.3 + c2.6-6.3,0.6-13.4,0.9-20.2c0.4-9.9,5.5-19,10.5-27.6c4.6-7.9,10.2-16.5,19.3-18.5c5.7-1.2,11.7,0.6,17.1,2.9 + c25.7,11,45.8,34.1,52.6,60.8c1.6,6.4,2.7,13.3,7.2,18.2c4.7,5.2,13,8.1,14.4,14.9c1,4.9-2.1,9.8-1.7,14.8 + c0.4,4.2,8.2,16.3,10,20.1c3.1,6.5,2,13.6-1.2,20.1c-3.2,6.5-11.2,12.9-18.3,14.7l-79.8-26.6c-7.5,4.3-15,20.8-23.6,21.8 + c-8.6,1-19.1,4.8-27.4-4.3C254.4,293.8,251.3,279.9,252.6,271.5z"/> + </g> + <g> + <path class="st8" d="M384.9,192.1c-0.9,6.7,0.9,17.7,4.6,23.4c2.4,3.8,6.5,6.3,10.6,8.5c4,2.2,8.3,4.3,11.3,7.7 + c3.1,3.4,4.7,8.5,2.7,12.5c7.2,2.7,10.4,11.8,7.8,18.9c-2.6,7.1-9.9,11.8-17.4,13.3l-32.9-9.9l-7.2-42l19-32.4"/> + </g> + <g> + <g> + <path class="st16" d="M388.9,234.9c0,0,0.2,0.1,0.6,0.3c0.4,0.2,1.1,0.5,1.8,0.9c1.5,0.9,3.6,2.4,5.8,4.8 + c1.1,1.2,2.1,2.6,3.1,4.3c0.9,1.7,1.8,3.5,2.4,5.6c1.3,4.1,1.6,9.1,0.1,14l-0.1-0.3c1.9,1.1,3.7,2.4,5.4,4.1 + c4.2,4.2,6.6,9.6,7.2,14.7c0.6,5.1-0.7,9.7-2.4,13.2c-1.7,3.5-3.8,5.8-5.4,7.3c-0.8,0.7-1.4,1.2-1.9,1.6 + c-0.5,0.3-0.7,0.5-0.7,0.5c0,0,0.2-0.2,0.7-0.6c0.4-0.3,1.1-0.9,1.8-1.6c1.5-1.5,3.6-3.8,5.2-7.3c1.6-3.4,2.8-8,2.2-13 + c-0.6-5-3-10.3-7.1-14.3c-1.6-1.6-3.4-3-5.2-4l-0.2-0.1l0.1-0.2c1.4-4.8,1.2-9.7,0-13.7c-0.6-2.1-1.4-3.9-2.3-5.5 + c-0.9-1.6-1.9-3.1-3-4.2c-2.1-2.4-4.1-4-5.6-4.9c-0.7-0.5-1.4-0.8-1.7-1C389.1,235.1,388.9,234.9,388.9,234.9z"/> + </g> + </g> + </g> + <g> + <g> + <path class="st9" d="M291.6,278.4c0,0-14.7,4.5-19.9,12c-3.6,5.3-41,86.2-41,86.2l-56.7-17l-20.3,26.8l63.1,35 + c11.7,6.5,25.9,6.4,37.5-0.2l0,0c6.5-3.7,11.7-9.1,15.1-15.7l16-30.7L291.6,278.4z"/> + </g> + <g> + <g> + <path class="st9" d="M276.5,454.5l8.9-38.1l-6.7-27.7c-4.3-14.3-8-37.5-3.3-56.2l15.7-53.8l17.2-7.9l39.4-6.8 + c1.5,0.5,41.2,11.7,41.2,11.7c5.5,1.3,7.7,2.2,7.7,2.2l4.6,29.5l-13.7,48.6l-6,48.4l13.1,50.3H276.5z"/> + </g> + <g> + <g> + <path class="st17" d="M273.6,335.9"/> + </g> + </g> + <g> + <g> + <path class="st17" d="M366,339.4"/> + </g> + </g> + </g> + <g> + <path class="st14" d="M299.3,274.9l11.4-5.1l51.5,0.7l12.2,7.1c0,0-42.6,47-53.3,48.6C294.3,330,299.3,274.9,299.3,274.9z"/> + </g> + <g> + <path class="st9" d="M395.5,277.4c0,0,7.7,1.7,14.3,15.9c6.6,14.1,17.3,53.4,17.3,53.4l4.6-41.6l37.3,4.1l-7.2,99.5 + c0,0-13.2,19.1-35.3,17.4c-22.1-1.8-50.1-71.5-50.1-71.5L395.5,277.4z"/> + </g> + <g> + <g> + <path class="st8" d="M421.4,397.6c0,0,0-0.1,0-0.2c0-0.2,0-0.4,0-0.7c0-0.7,0.1-1.6,0.2-2.7c0.1-2.4,0.4-5.8,0.8-9.9 + c0.8-8.4,2.2-19.9,3.9-32.7c1.7-12.7,3.2-24.3,4.1-32.7c0.4-4.2,0.8-7.6,1-9.9c0.1-1.1,0.2-2,0.3-2.7c0-0.3,0.1-0.5,0.1-0.7 + c0-0.2,0-0.2,0-0.2c0,0,0,0.1,0,0.2c0,0.2,0,0.4,0,0.7c0,0.7-0.1,1.6-0.2,2.7c-0.1,2.4-0.4,5.8-0.8,9.9 + c-0.8,8.4-2.2,19.9-3.9,32.7c-1.7,12.7-3.2,24.3-4.1,32.6c-0.4,4.2-0.8,7.6-1,9.9c-0.1,1.1-0.2,2-0.3,2.7 + c0,0.3-0.1,0.5-0.1,0.7C421.4,397.5,421.4,397.6,421.4,397.6z"/> + </g> + </g> + <g> + <g> + <path class="st8" d="M231.4,398c0,0-0.2-0.3-0.4-0.7c-0.2-0.5-0.5-1.2-0.9-2.1c-0.7-1.8-1.3-4.5-1.6-7.5 + c-0.3-3-0.1-5.7,0.2-7.6c0.1-1,0.3-1.7,0.4-2.3c0.1-0.5,0.2-0.8,0.3-0.8c0.1,0-0.1,1.2-0.3,3.1c-0.2,1.9-0.3,4.6,0,7.5 + c0.3,2.9,0.9,5.5,1.5,7.4C231,396.9,231.4,398,231.4,398z"/> + </g> + </g> + <g> + <g> + <path class="st8" d="M237.6,399c0,0-0.3-0.2-0.7-0.7c-0.4-0.5-0.9-1.2-1.6-2.2c-1.2-1.9-2.7-4.7-3.9-8 + c-1.1-3.3-1.7-6.4-1.9-8.7c-0.1-1.1-0.2-2.1-0.2-2.7c0-0.6,0-1,0.1-1c0.1,0,0.2,1.4,0.5,3.6c0.3,2.2,0.9,5.3,2.1,8.6 + c1.1,3.3,2.6,6,3.7,8C236.9,397.8,237.7,398.9,237.6,399z"/> + </g> + </g> + <g> + <g> + <path class="st8" d="M281.7,310c0,0,0,0.3-0.2,0.8c-0.2,0.6-0.4,1.3-0.6,2.3c-0.6,2-1.4,4.9-2.4,8.4 + c-1.9,7.1-4.6,16.9-5.9,28.1c-0.6,5.6-0.8,10.9-0.2,15.8c0.5,4.8,1.6,9.1,2.6,12.6c1.1,3.5,2.1,6.3,2.7,8.3 + c0.3,0.9,0.6,1.7,0.8,2.2c0.2,0.5,0.3,0.8,0.2,0.8c0,0-0.1-0.3-0.3-0.8c-0.2-0.5-0.5-1.3-0.9-2.2c-0.7-1.9-1.8-4.7-2.9-8.2 + c-1.1-3.5-2.2-7.8-2.8-12.7c-0.6-4.9-0.5-10.3,0.2-15.9c1.3-11.2,4.1-21.1,6.1-28.1c1-3.5,1.9-6.4,2.5-8.3 + c0.3-0.9,0.5-1.7,0.7-2.3C281.6,310.3,281.7,310,281.7,310z"/> + </g> + </g> + <g> + <g> + <path class="st8" d="M386,375.6c0,0-0.1-0.1-0.1-0.2c-0.1-0.2-0.2-0.4-0.3-0.7c-0.3-0.7-0.7-1.6-1.3-2.9 + c-0.6-1.2-1.3-2.8-2-4.6c-0.7-1.8-1.5-3.9-2.4-6.1c-0.9-2.3-1.7-4.8-2.7-7.5c-0.4-1.4-0.9-2.8-1.3-4.3 + c-0.2-0.7-0.5-1.5-0.7-2.2c-0.2-0.8-0.4-1.5-0.6-2.3c-1.7-6.2-3.1-13.2-3.5-20.7c-0.5-7.5,0.3-14.7,2.2-20.9 + c1.8-6.3,4.9-11.4,8.3-15c3.3-3.6,6.8-5.7,9.3-6.8c1.3-0.5,2.3-0.9,3-1.1c0.3-0.1,0.6-0.2,0.8-0.2c0.2,0,0.3-0.1,0.3-0.1 + c0,0-0.1,0-0.3,0.1c-0.2,0.1-0.5,0.2-0.8,0.3c-0.7,0.2-1.7,0.6-2.9,1.2c-2.4,1.2-5.9,3.3-9.1,6.9c-3.3,3.6-6.3,8.7-8.1,14.8 + c-1.9,6.2-2.5,13.3-2.1,20.7c0.4,7.4,1.8,14.4,3.5,20.6c0.2,0.8,0.4,1.5,0.6,2.3c0.2,0.8,0.4,1.5,0.7,2.2 + c0.4,1.5,0.9,2.9,1.3,4.3c1,2.7,1.8,5.2,2.6,7.5c0.9,2.3,1.7,4.3,2.4,6.1c0.7,1.8,1.4,3.3,1.9,4.6c0.5,1.2,0.9,2.2,1.2,2.9 + c0.1,0.3,0.2,0.6,0.3,0.8C385.9,375.5,386,375.6,386,375.6z"/> + </g> + </g> + <g class="st18"> + <g> + <path d="M371.8,330.5c-4.9,23-4.9,46.8,3.5,68.8c0.5,1.3,2.4,5.8,3.8,5.9c1.7,0.1,2.6-2.1,2.9-3.8c0.6-3.5,1-9.3,1.6-12.8 + c0.8-4.4,1.8-14.6,1.8-14.6s-4-9.2-5.8-14.2c-3.3-9.2-6.1-17.9-7.6-28.6"/> + </g> + </g> + <g> + <g> + <path class="st8" d="M367.8,419.8c0,0-0.1,0.2-0.4,0.6c-0.3,0.4-0.7,1-1.2,1.7c-0.3,0.4-0.6,0.8-0.9,1.2 + c-0.3,0.4-0.7,0.9-1.1,1.3c-0.8,0.9-1.7,2.1-2.9,3.2c-0.6,0.6-1.2,1.1-1.8,1.8c-0.6,0.6-1.4,1.2-2.1,1.8 + c-1.4,1.3-3.1,2.4-4.8,3.6c-3.5,2.3-7.6,4.6-12.2,6.4c-4.6,1.7-9.1,2.9-13.3,3.5c-2.1,0.2-4.1,0.5-6,0.6 + c-0.9,0-1.8,0.1-2.7,0.1c-0.9,0-1.7-0.1-2.5-0.1c-1.6,0-3-0.2-4.3-0.4c-0.6-0.1-1.2-0.1-1.8-0.2c-0.5-0.1-1-0.2-1.5-0.3 + c-0.8-0.2-1.5-0.3-2-0.4c-0.5-0.1-0.7-0.2-0.7-0.2c0,0,0.3,0,0.7,0.1c0.5,0.1,1.2,0.2,2,0.3c0.4,0.1,0.9,0.2,1.5,0.2 + c0.5,0.1,1.1,0.1,1.8,0.2c1.3,0.1,2.7,0.3,4.3,0.3c0.8,0,1.6,0,2.5,0c0.9,0,1.8-0.1,2.7-0.1c1.9,0,3.8-0.3,5.9-0.6 + c4.1-0.7,8.6-1.8,13.2-3.5c4.5-1.8,8.6-4,12.1-6.3c1.7-1.2,3.4-2.3,4.8-3.6c0.7-0.6,1.4-1.1,2.1-1.7c0.6-0.6,1.2-1.2,1.8-1.7 + c1.2-1.1,2.1-2.2,3-3.1c0.4-0.5,0.8-0.9,1.2-1.3c0.3-0.4,0.6-0.8,0.9-1.2c0.5-0.7,0.9-1.2,1.3-1.6 + C367.6,420,367.8,419.8,367.8,419.8z"/> + </g> + </g> + <g> + <g> + <path class="st8" d="M359.7,404.6c0,0,0,0.4-0.1,1.1c-0.1,0.7-0.3,1.7-0.7,2.9c-0.7,2.4-2,5.7-4.1,9c-2.1,3.3-4.6,5.8-6.5,7.4 + c-1,0.8-1.8,1.4-2.4,1.8c-0.6,0.4-0.9,0.6-0.9,0.6c-0.1-0.1,1.2-1,3.1-2.7c1.9-1.7,4.2-4.2,6.3-7.5c2.1-3.2,3.4-6.4,4.2-8.8 + C359.3,406.1,359.6,404.6,359.7,404.6z"/> + </g> + </g> + </g> + <g> + <g> + <g> + <path class="st9" d="M341.5,139.4c14,0.1,27.6,8,34.7,20c7.1,12,7.3,27.8,0.6,40c-2.3,4.2-5.6,8.1-10.1,9.6 + c1.5-23.9-8-48.3-25.3-64.9"/> + </g> + <g> + <path class="st9" d="M357.8,143.1c-2.1-4.3,0.1-9.7,3.8-12.7c3.7-3,8.5-4.3,13.2-5.4c4.6-1.1,9.5-2.3,13.2-5.2 + c-0.5,7.5-2.4,15.2-7.3,20.9c-4.8,5.7-13.2,8.9-20.2,6"/> + </g> + <g> + <path class="st9" d="M362.6,148c-3.2,1.3-7,0.5-9.8-1.4c-2.8-2-4.9-4.9-6.6-7.9c-2.1-3.7-3.7-7.7-4.2-11.9s0-8.6,2.1-12.3 + c5.5,5.8,11.5,10.7,14.6,18.1c1.8,4.3,1.6,11.3-0.4,15.5"/> + </g> + <g> + <g> + <path class="st8" d="M378.1,172.2c0,0,0.1,0.5,0.3,1.4c0.1,0.5,0.2,1,0.2,1.6c0.1,0.6,0.1,1.4,0.2,2.2 + c0.1,1.6,0.1,3.6-0.1,5.7c-0.2,2.2-0.5,4.5-1.1,6.9c-0.6,2.4-1.3,4.7-2.1,6.7c-0.8,2-1.7,3.8-2.5,5.2 + c-0.4,0.7-0.8,1.3-1.1,1.9c-0.3,0.6-0.7,1-0.9,1.4c-0.5,0.7-0.8,1.1-0.9,1.1c0,0,0.2-0.5,0.7-1.2c0.2-0.4,0.5-0.9,0.8-1.4 + c0.3-0.6,0.7-1.2,1.1-1.9c0.8-1.4,1.6-3.2,2.4-5.2c0.8-2,1.5-4.2,2.1-6.6c0.6-2.4,0.9-4.7,1.1-6.9c0.2-2.1,0.3-4.1,0.2-5.7 + c0-0.8-0.1-1.5-0.1-2.2c0-0.6-0.1-1.2-0.1-1.6C378.1,172.8,378.1,172.2,378.1,172.2z"/> + </g> + </g> + <g> + <g> + <path class="st8" d="M375,145.4c0,0-0.4,0.3-1.1,0.9c-0.8,0.4-2,1.1-3.6,1.4c-3.2,0.7-7.9,0.6-12.8-0.7l-0.1,0l0-0.1 + c-0.1-0.3-0.1-0.6-0.2-0.9c-0.5-2.6-0.6-5.2-0.3-7.4c0.3-2.3,1.1-4.3,2.1-5.7c1-1.5,2.2-2.3,3-2.8c0.9-0.5,1.3-0.7,1.3-0.7 + c0,0-0.4,0.3-1.2,0.8c-0.7,0.6-1.9,1.4-2.8,2.9c-0.9,1.4-1.7,3.3-1.9,5.6c-0.3,2.2-0.2,4.7,0.4,7.3c0.1,0.3,0.1,0.6,0.2,0.9 + l-0.2-0.2c4.8,1.3,9.4,1.5,12.5,0.9c1.6-0.3,2.8-0.8,3.6-1.2C374.6,145.7,375,145.4,375,145.4z"/> + </g> + </g> + <g> + <g> + <path class="st8" d="M379.1,132.5c0,0.1-1.5,0.3-3.8,1c-1.1,0.3-2.5,0.8-3.9,1.5c-1.4,0.7-3,1.5-4.5,2.5c-1.5,1-2.9,2.1-4,3.1 + c-1.2,1.1-2.1,2.1-2.9,3c-1.5,1.8-2.3,3.1-2.4,3.1c0,0,0.1-0.3,0.5-0.9c0.3-0.6,0.9-1.4,1.6-2.4c0.7-1,1.7-2,2.9-3.1 + c1.2-1.1,2.5-2.2,4.1-3.2c1.6-1,3.1-1.8,4.6-2.5c1.5-0.6,2.8-1.1,4-1.4c1.2-0.3,2.1-0.5,2.8-0.6 + C378.7,132.5,379.1,132.4,379.1,132.5z"/> + </g> + </g> + <g> + <g> + <path class="st8" d="M362.4,147.9c0,0,0,0.1,0,0.3c0,0.2-0.1,0.5-0.3,0.8c-0.3,0.7-0.9,1.7-2,2.5c-1.1,0.8-2.6,1.5-4.5,1.5 + c-1.8-0.1-3.8-1.2-4.9-2.9c-1.2-1.7-1.5-3.9-0.9-5.7c0.6-1.8,1.8-3,3-3.7c1.2-0.7,2.3-0.9,3-0.9c0.4,0,0.7,0,0.9,0 + c0.2,0,0.3,0.1,0.3,0.1c0,0.1-0.4,0-1.1,0.1c-0.7,0.1-1.8,0.4-2.9,1.1c-1.1,0.7-2.2,1.9-2.7,3.5c-0.5,1.6-0.2,3.6,0.9,5.2 + c1.1,1.6,2.9,2.6,4.5,2.7c1.7,0.1,3.2-0.5,4.2-1.3c1-0.8,1.7-1.6,2-2.3C362.3,148.3,362.4,147.9,362.4,147.9z"/> + </g> + </g> + <g> + <g> + <path class="st8" d="M350.3,148.1c0,0-0.3-0.5,0.1-1.3c0.2-0.4,0.6-0.7,1.2-0.9c0.5-0.2,1.2-0.2,1.8-0.2 + c2.5,0.2,4.3,1.2,4.2,1.3c-0.1,0.1-1.9-0.6-4.3-0.7c-1.2-0.1-2.2,0.2-2.6,0.8C350.3,147.6,350.4,148.1,350.3,148.1z"/> + </g> + </g> + <g> + <g> + <path class="st8" d="M344.1,114.4c0.1,0-1.1,1.5-1.5,4.3c-0.2,1.4-0.3,3.1-0.2,5c0,1.9,0.1,3.9,0.5,6.1 + c0.9,4.2,2.7,7.8,4.3,10.1c0.8,1.2,1.5,2.1,2,2.7c0.5,0.6,0.8,0.9,0.8,0.9c-0.1,0.1-1.4-1.1-3.1-3.4 + c-1.7-2.3-3.5-5.9-4.5-10.2c-0.4-2.2-0.5-4.3-0.5-6.2c0-1.9,0.1-3.6,0.4-5c0.3-1.4,0.7-2.5,1.1-3.2 + C343.8,114.7,344.1,114.4,344.1,114.4z"/> + </g> + </g> + <g> + <g> + <path class="st8" d="M349.3,126.3c0.1,0,0.6,0.6,1.4,1.7c0.8,1.1,1.8,2.7,2.7,4.6c0.9,1.9,1.4,3.7,1.8,5 + c0.3,1.3,0.4,2.2,0.3,2.2c-0.2,0-0.8-3.3-2.6-7C351.2,129.1,349.2,126.4,349.3,126.3z"/> + </g> + </g> + <g> + <g> + <path class="st8" d="M347.6,132.5c0.1-0.1,1.8,1.6,4,3.6c2.2,2,4,3.6,3.9,3.8c-0.1,0.1-2-1.4-4.2-3.4 + C349.1,134.5,347.5,132.6,347.6,132.5z"/> + </g> + </g> + <g> + <g> + <path class="st1" d="M373.8,156c0,0,0,0.1-0.1,0.1c-0.1,0.1-0.3,0.2-0.4,0.4c-0.4,0.3-1,0.8-1.7,1.3c-1.5,1-3.7,2.2-6.3,3.1 + c-2.6,0.9-5.1,1.2-6.9,1.3c-0.9,0-1.6,0-2.1,0c-0.2,0-0.4,0-0.6,0c-0.1,0-0.2,0-0.2,0c0,0,0.1,0,0.2,0c0.2,0,0.4,0,0.6,0 + c0.5,0,1.2,0,2.1-0.1c1.8-0.1,4.2-0.5,6.8-1.3c2.6-0.9,4.8-2,6.3-3c0.7-0.5,1.3-0.9,1.7-1.2c0.2-0.1,0.3-0.2,0.5-0.3 + C373.7,156,373.8,155.9,373.8,156z"/> + </g> + </g> + <g> + <g> + <path class="st1" d="M379.7,168.1c0,0,0,0.1-0.1,0.1c-0.1,0.1-0.3,0.2-0.4,0.4c-0.4,0.3-1,0.8-1.7,1.3c-1.5,1-3.7,2.2-6.3,3.2 + c-2.6,0.9-5.1,1.4-6.8,1.6c-0.9,0.1-1.6,0.1-2.1,0.1c-0.2,0-0.4,0-0.6,0c-0.1,0-0.2,0-0.2,0c0,0,0.1,0,0.2,0 + c0.2,0,0.4,0,0.6,0c0.5,0,1.2-0.1,2.1-0.2c1.8-0.2,4.2-0.7,6.8-1.6c2.6-0.9,4.7-2.1,6.2-3.1c0.8-0.5,1.3-0.9,1.7-1.2 + c0.2-0.1,0.3-0.2,0.5-0.3C379.6,168.1,379.7,168,379.7,168.1z"/> + </g> + </g> + <g> + <g> + <path class="st1" d="M378.1,185.7c0,0-0.1,0.1-0.4,0.3c-0.3,0.2-0.7,0.4-1.2,0.7c-1.1,0.6-2.6,1.3-4.4,1.7 + c-1.8,0.5-3.5,0.6-4.7,0.6c-0.6,0-1.1,0-1.4,0c-0.3,0-0.5-0.1-0.5-0.1c0,0,0.2,0,0.5,0c0.3,0,0.8,0,1.4,0 + c1.2,0,2.9-0.2,4.6-0.7c1.8-0.5,3.3-1.1,4.4-1.7c0.5-0.3,1-0.5,1.3-0.7C377.9,185.8,378.1,185.7,378.1,185.7z"/> + </g> + </g> + <g> + <g> + <path class="st1" d="M381.5,183c0,0.1-0.6,0.2-1.4,0.4c-0.8,0.1-1.4,0.2-1.4,0.2s0.6-0.2,1.4-0.4 + C380.8,183,381.4,182.9,381.5,183z"/> + </g> + </g> + <g> + <g> + <path class="st1" d="M371.5,203.8c0,0.1-1,0.2-2.3,0.1c-1.3-0.1-2.3-0.4-2.2-0.4c0-0.1,1,0.2,2.3,0.2 + C370.5,203.8,371.5,203.7,371.5,203.8z"/> + </g> + </g> + <g> + <g> + <path class="st1" d="M352.6,151.5c0,0.1-0.6,0.4-1.3,0.9c-0.8,0.4-1.4,0.7-1.4,0.7s0.6-0.4,1.3-0.9 + C351.9,151.7,352.5,151.4,352.6,151.5z"/> + </g> + </g> + <g> + <g> + <path class="st1" d="M347.6,141c0,0.1-1.4,0.5-2.9,1.6c-1.5,1.1-2.4,2.3-2.5,2.3c0,0,0.2-0.3,0.6-0.8c0.4-0.5,1-1.1,1.8-1.6 + c0.8-0.5,1.5-0.9,2.1-1.1C347.2,141,347.6,140.9,347.6,141z"/> + </g> + </g> + <g> + <g> + <path class="st1" d="M356.6,152.6c-0.1,0,0.3-1.4,0.1-3c-0.2-1.7-0.9-2.9-0.8-2.9c0,0,0.2,0.3,0.4,0.8 + c0.2,0.5,0.5,1.3,0.6,2.1c0.1,0.9,0.1,1.6,0,2.2C356.7,152.3,356.6,152.6,356.6,152.6z"/> + </g> + </g> + <g> + <g> + <path class="st1" d="M387.5,120c0,0-0.4,0.5-1.1,1.2c-0.7,0.7-1.9,1.7-3.5,2.7c-1.6,1-3.5,2-5.8,3c-2.2,1-4.7,2-7.2,3.2 + c-2.5,1.2-4.8,2.6-6.6,4.2c-1.8,1.6-3.1,3.4-3.9,5.1c-0.8,1.7-1.1,3.1-1.2,4.1c-0.1,1,0,1.6,0,1.6c0,0,0-0.1,0-0.4 + c0-0.3,0-0.7,0-1.2c0.1-1,0.4-2.5,1.1-4.2c0.8-1.7,2.1-3.5,3.9-5.1c1.8-1.6,4.1-3.1,6.7-4.3c2.5-1.2,5-2.2,7.3-3.2 + c2.2-1,4.2-2,5.7-2.9c1.6-1,2.7-1.9,3.5-2.6c0.4-0.3,0.7-0.6,0.9-0.8C387.4,120.1,387.5,120,387.5,120z"/> + </g> + </g> + <g> + <g> + <path class="st1" d="M384.8,131.9c0,0,0,0.1-0.1,0.2c-0.1,0.2-0.3,0.4-0.4,0.6c-0.4,0.5-0.9,1.3-1.7,2.2 + c-1.5,1.8-3.7,4.2-6.5,6.4c-2.8,2.2-5.7,3.8-7.8,4.7c-1.1,0.5-1.9,0.8-2.6,1.1c-0.3,0.1-0.5,0.2-0.7,0.2 + c-0.2,0.1-0.3,0.1-0.3,0.1c0,0,0.1,0,0.2-0.1c0.2-0.1,0.4-0.2,0.7-0.3c0.6-0.2,1.5-0.6,2.5-1.1c2.1-1,4.9-2.6,7.7-4.8 + c2.8-2.2,5-4.5,6.5-6.3c0.7-0.9,1.3-1.6,1.7-2.2c0.2-0.2,0.3-0.4,0.4-0.6C384.8,131.9,384.8,131.8,384.8,131.9z"/> + </g> + </g> + <g> + <g> + <path class="st1" d="M343.1,117.8c0,0,1.2,0.5,3,1.6c1.8,1.1,4.1,3,6.1,5.5c2,2.6,3.3,5.2,4,7.2c0.7,2,0.9,3.3,0.9,3.3 + c0,0,0-0.1-0.1-0.2c-0.1-0.2-0.1-0.4-0.2-0.7c-0.1-0.6-0.4-1.4-0.8-2.4c-0.7-2-2-4.6-4-7.2c-2-2.5-4.3-4.4-6-5.5 + c-0.9-0.6-1.6-1-2.1-1.3c-0.2-0.1-0.4-0.2-0.6-0.3C343.2,117.8,343.1,117.8,343.1,117.8z"/> + </g> + </g> + </g> + <g> + <g> + <g> + <g> + <path class="st8" d="M292.9,155.4c0.4,5.3-3.1,10-6.6,14c-3.5,4-7.5,8.1-8.2,13.4c-1.4,9.3,7.8,18,5.9,27.2 + c-1,4.9-5.1,8.9-5.8,13.9c-0.7,4.8,1.9,9.6,5.1,13.2c3.2,3.7,6.9,3.1,9.8,7.1c3,4.2,7.8,8.5,12.3,10.9 + c10.2,5.6,22.5-2.5,27.4-4"/> + </g> + </g> + <g> + <g> + <path class="st8" d="M370.3,177.1c4.7-2.1,5.1-18.9-6.7-20.9c-4.7-0.8-8.7-3.1-12.7-5.8c-3.5-2.4-4.6-8.4-7.6-11.4 + c-7.3-7.4-18.8-10.5-28.9-7.8c-10.1,2.7-18.4,11.2-20.9,21.4c-1.7,6.8-0.5,14.9,4.9,19.5c6.3,5.3,15.5,4.1,23.6,2.5 + c8-1.6,17.1-3.3,23.8,1.5c4,2.8,6.4,7.5,8.8,11.7c2.5,4.3,5.6,8.6,10.2,10.3c0,0,3.6,8.4,0.7,14.2s3.4-4.1,3.4-4.1 + S380.4,187.6,370.3,177.1"/> + </g> + </g> + <g> + <g> + <g> + <g> + <g> + <g> + <g> + <g> + <g> + <g> + <g> + <g> + <g> + <g> + <g> + <path class="st14" d="M310.4,295.7c-0.1-11,0-42,0-41.9c0,0-17.5-2.9-21.1-25.8c-1.8-11.4-1.2-30-0.2-45.3 + c0.9-13.7,10-30.5,23.7-29.1l42.7,10.9c4.2,0.4,7.3,4,7.2,8.2l0,0l-2.6,106L310.4,295.7z"/> + </g> + </g> + </g> + </g> + </g> + </g> + </g> + </g> + <g> + <g> + <g> + <g> + <g> + <g> + <g> + <g> + <g> + <path class="st8" d="M296.4,195.7c-0.1,1.5,1.2,2.9,2.8,3c1.6,0.1,3-1.1,3-2.6c0.1-1.5-1.2-2.9-2.8-3 + C297.8,193,296.4,194.2,296.4,195.7z"/> + </g> + </g> + </g> + </g> + </g> + </g> + </g> + </g> + <g> + <g> + <g> + <g> + <g> + <g> + <g> + <g> + <g> + <path class="st8" d="M293.1,192.4c0.4,0.4,2.6-1.3,5.7-1.2c3.2,0,5.5,1.6,5.8,1.2c0.2-0.2-0.2-0.9-1.2-1.6 + c-1-0.7-2.7-1.4-4.6-1.4c-2,0-3.6,0.7-4.6,1.5C293.3,191.5,292.9,192.2,293.1,192.4z"/> + </g> + </g> + </g> + </g> + </g> + </g> + </g> + </g> + </g> + </g> + <g> + <g> + <g> + <g> + <g> + <g> + <g> + <g> + <g> + <g> + <path class="st8" d="M324.1,197.4c0.4,0.4,2.6-1.3,5.7-1.2c3.2,0,5.5,1.6,5.8,1.2c0.2-0.2-0.2-0.9-1.2-1.6 + c-1-0.7-2.7-1.4-4.6-1.4c-2,0-3.6,0.7-4.6,1.5C324.3,196.5,324,197.2,324.1,197.4z"/> + </g> + </g> + </g> + </g> + </g> + </g> + </g> + </g> + </g> + </g> + <g> + <g> + <g> + <g> + <g> + <g> + <g> + <g> + <g> + <path class="st8" d="M312.6,216.4c0-0.2-1.9-0.5-5.1-0.9c-0.8-0.1-1.6-0.2-1.7-0.8c-0.2-0.6,0.2-1.5,0.5-2.4 + c0.8-1.9,1.6-4,2.4-6.1c3.3-8.7,5.7-15.9,5.4-16c-0.4-0.1-3.3,6.8-6.7,15.6c-0.8,2.2-1.6,4.2-2.3,6.2 + c-0.3,0.9-0.8,2-0.4,3.2c0.2,0.6,0.8,1,1.3,1.2c0.5,0.2,1,0.2,1.4,0.2C310.6,216.6,312.6,216.6,312.6,216.4z" + /> + </g> + </g> + </g> + </g> + </g> + </g> + </g> + </g> + </g> + <g> + <g> + <g> + <g> + <g> + <g> + <g> + <g> + <path class="st15" d="M310.4,253.8c0,0,15.4,1,30.6-8.1c-0.3,0.1-8.4,15.9-30.3,13.5L310.4,253.8z"/> + </g> + </g> + </g> + </g> + </g> + </g> + </g> + </g> + <g> + <path class="st15" d="M313.5,225.5c-0.4-1.9,1.9-2.6,3.6-3.6c1.7-1,2.6-2.4,4.2-1.2c0.9,0.7,1.5,1.7,1.9,2.7 + c0.3,0.7,0.4,1.5,0.3,2.2c-0.2,0.9-0.7,1.7-1.4,2.3c-0.9,0.9-2.1,1.6-3.4,1.8c-1.3,0.2-2.7-0.1-3.7-0.9 + c-1-0.8-1.6-2.2-1.4-3.5"/> + </g> + <g> + <g> + <g> + <g> + <g> + <g> + <g> + <g> + <g> + <path class="st8" d="M321.9,218.6c-0.5,0-0.5,3.4-3.5,5.8c-2.9,2.4-6.6,2-6.6,2.5c0,0.2,0.8,0.7,2.4,0.7 + c1.5,0.1,3.7-0.4,5.5-1.9c1.8-1.5,2.6-3.5,2.7-4.9C322.5,219.4,322.2,218.6,321.9,218.6z"/> + </g> + </g> + </g> + </g> + </g> + </g> + </g> + </g> + </g> + </g> + </g> + </g> + <g> + <g> + <g> + <g> + <g> + <g> + <g> + <g> + <path class="st8" d="M323.7,190.4c0.3,0.9,3.5,0.5,7.2,0.9c3.7,0.4,6.7,1.5,7.2,0.7c0.2-0.4-0.3-1.2-1.5-2 + c-1.2-0.8-3.1-1.6-5.3-1.9c-2.2-0.2-4.3,0.1-5.6,0.6C324.2,189.3,323.5,189.9,323.7,190.4z"/> + </g> + </g> + </g> + </g> + </g> + </g> + </g> + </g> + <g> + <g> + <g> + <g> + <g> + <g> + <g> + <g> + <path class="st8" d="M293.6,181.5c0.6,0.7,2.7,0,5.3-0.1c2.6-0.1,4.8,0.5,5.3-0.3c0.2-0.4-0.1-1.1-1.1-1.8 + c-1-0.7-2.5-1.2-4.3-1.2c-1.8,0.1-3.4,0.7-4.3,1.4C293.6,180.4,293.3,181.1,293.6,181.5z"/> + </g> + </g> + </g> + </g> + </g> + </g> + </g> + </g> + </g> + </g> + </g> + </g> + <g> + <g> + <path class="st8" d="M365.1,241c-6.3,1.3-6.9-1.9-11.5-6.3c-4.7-4.4-7.1-11.1-6.3-17.4c0.7-5.6,3.7-11.3,1.8-16.6 + c-2.6-7-12.4-9.2-15-16.2c-1.5-4.1-0.2-8.8-1.9-12.8c-1.6-3.8-5.6-5.9-9.4-7.3c-3.8-1.4-8-2.4-11.2-4.9 + c-3.2-2.5-5.2-7.2-3.2-10.8c17.1,2,33.9,6.1,50,12.1c3.7,1.4,7.7,3.1,9.9,6.4c2.3,3.5,2.2,8,2,12.3 + c-1,20.5-1.9,41.1-2.9,61.6"/> + </g> + </g> + </g> + <g> + <g> + <g> + <path class="st16" d="M379.7,233.3c0,0,0.1-0.1,0.3-0.3c0.1-0.2,0.4-0.5,0.6-0.9c0.5-0.8,1.1-2.2,1.5-4 + c0.4-1.8,0.4-4.2-0.4-6.8c-0.7-2.6-2.5-5.3-5.3-7.2c-2.7-1.9-6.2-3.2-8.5-6.3c-1.1-1.5-1.6-3.4-1.2-5.4c0.3-2,1.2-3.9,2-5.9 + c1.7-4.1,1.8-9.2-0.3-13.7c-1-2.2-2.6-4.3-4.6-5.8c-2-1.5-4.5-2.3-7-2.9c-2.5-0.6-4.9-0.9-7.2-1.6c-2.3-0.7-4.5-1.7-6.1-3.3 + c-1.6-1.6-2.3-3.8-2.6-6c-0.3-2.2-0.5-4.5-1.1-6.6c-1.1-4.3-3.3-8.1-6.2-10.8c-2.9-2.7-6.4-4.3-9.7-4.8 + c-3.3-0.5-6.5,0.1-8.8,1.3c-2.4,1.2-4,2.9-5.1,4.4c-1.1,1.5-1.6,2.9-1.8,3.9c-0.2,1-0.2,1.5-0.2,1.5c0.1,0.1,0.2-2.1,2.3-5.1 + c1.1-1.4,2.7-3,5-4.2c2.3-1.1,5.3-1.7,8.5-1.1c3.2,0.5,6.5,2.1,9.2,4.7c2.7,2.6,4.8,6.3,5.9,10.5c0.6,2.1,0.7,4.3,1,6.6 + c0.3,2.3,1.1,4.8,2.9,6.5c1.8,1.8,4.1,2.9,6.5,3.6c2.4,0.7,4.9,1.1,7.3,1.6c2.4,0.5,4.8,1.3,6.6,2.7c1.9,1.4,3.4,3.3,4.3,5.4 + c1.9,4.2,2,9,0.3,12.9c-0.8,2-1.7,4-2,6.1c-0.4,2.1,0.2,4.4,1.4,6c2.5,3.3,6.1,4.6,8.8,6.4c2.7,1.8,4.4,4.3,5.2,6.7 + c0.8,2.4,0.8,4.7,0.5,6.5C381.1,231.7,379.6,233.3,379.7,233.3z"/> + </g> + </g> + </g> + </g> + <g> + <g> + <g> + <path class="st8" d="M349.9,210.6c-5.2,11.9-6.1,25.6-2.7,38.1c1,3.7,2.5,7.6,1.6,11.4c-1,4.1-4.4,7.1-6.9,10.4 + c-4.3,5.8-6,13.5-4.5,20.6c1.5,7.1,6.1,13.4,12.4,17c0.7-3.8,4.2-6.3,7.6-8c3.5-1.6,7.3-2.8,9.9-5.6c2.6-2.8,3.4-7.8,0.3-10.1 + c8.8-2.3,13.8-12.8,11.9-21.7c-1.9-8.9-9.1-15.9-17.3-19.8"/> + </g> + <g> + <path class="st8" d="M359.3,299.4c-1.6,0.9-3.1,1.6-3.3,3.4c-0.2,2.1,1.8,5.8,2.5,7.8c1.4,3.9,0.3,8.5-2.6,11.4 + c-3,2.9-7.6,3.8-11.5,2.3c2.8,0.2,5.2-2.5,5.5-5.3c0.3-2.8-1.1-5.5-2.8-7.8c-1.7-2.3-3.7-4.3-5.1-6.7 + c-2.9-5-7.2-18.5-3.9-26.1"/> + </g> + </g> + <g> + <g> + <path class="st16" d="M367.3,284.9c0,0,0.2-0.2,0.5-0.5c0.4-0.3,0.9-0.7,1.6-1.3c1.3-1.1,3.2-2.8,5.1-5.3 + c1.9-2.5,3.9-5.7,4.9-9.7c1-4,1.1-8.7-0.6-13.1c-0.9-2.2-2.1-4.2-3.6-5.8c-1.5-1.7-3.1-3.2-4.6-4.6c-1.5-1.4-2.9-2.8-4.1-4.3 + c-1.2-1.4-2.1-3-2.7-4.4c-1.3-2.9-1.3-5.6-0.9-7.3c0.2-0.9,0.5-1.5,0.7-1.9c0.2-0.4,0.4-0.6,0.4-0.6c0.1,0-0.5,0.8-0.9,2.5 + c-0.4,1.7-0.3,4.3,1,7.2c0.6,1.4,1.5,2.9,2.7,4.3c1.2,1.4,2.6,2.8,4.1,4.2c1.5,1.4,3.1,2.9,4.6,4.6c1.5,1.7,2.8,3.7,3.7,6 + c1.8,4.6,1.7,9.4,0.6,13.4c-1.1,4.1-3.1,7.4-5,9.8c-2,2.5-3.9,4.2-5.3,5.2c-0.7,0.5-1.2,0.9-1.6,1.2 + C367.6,284.7,367.3,284.9,367.3,284.9z"/> + </g> + </g> + </g> + </g> + </g> +</g> +<g id="Plant"> + <g> + <g> + <g> + <path class="st19" d="M79.9,404.3c2.5,3.2,7,1.8,8.9-0.4c2-2.2,2.7-5.1,3.3-8c1.4-6.5,2.9-13.3,1.1-19.7 + c-0.5-1.9-1.4-3.8-2.8-5.2c-1.5-1.4-3.6-2.1-5.5-1.5c-2.2,0.7-3.5,3.1-4.4,5.2c-2.6,6.4-3.8,13.4-3.4,20.3 + C77.4,398.4,78,401.7,79.9,404.3"/> + </g> + </g> + <g> + <g> + <path class="st20" d="M79.9,404.3c2.5,3.2,7,1.8,8.9-0.4c2-2.2,2.7-5.1,3.3-8c1.4-6.5,2.9-13.3,1.1-19.7 + c-0.5-1.9-1.4-3.8-2.8-5.2c-1.5-1.4-3.6-2.1-5.5-1.5c-2.2,0.7-3.5,3.1-4.4,5.2c-2.6,6.4-3.8,13.4-3.4,20.3 + C77.4,398.4,78,401.7,79.9,404.3"/> + </g> + </g> + <g> + <g> + <path class="st9" d="M89.7,420.5c3.7-2.2,8.1-2.7,12.4-2.3c2.3,0.2,4.7,0.8,6.3,2.4c1.6,1.6,2.1,4.6,0.5,6.1 + c-1,0.9-2.5,1.2-3.8,1.2c-3.3,0.2-7-0.2-9.5,2c-1.4,1.2-2.3,3.2-4.1,3.8c-1.7,0.7-3.8-0.3-4.8-1.8c-1-1.5-1.2-3.5-0.9-5.3 + C85.8,426.7,86,422.7,89.7,420.5z"/> + </g> + </g> + <g> + <g> + <path class="st9" d="M73.9,409.4c2.3-3.1,2.5-7.5,1.2-11.2c-1.4-3.7-4.1-6.7-7.2-9.1c-3.6-2.8-7.8-5-12.2-6.2 + c-1.7-0.5-3.6-0.9-5.3-0.6c-1.8,0.3-3.5,1.3-4.3,3c-1.1,2.4,0.3,5.2,1.7,7.5c2.5,4,5.4,7.8,8.5,11.3c2.5,2.8,5.3,5.6,9,6.7 + c3.6,1.1,7.3,0.7,9-1.8"/> + </g> + </g> + <g> + <g> + <g> + <path class="st8" d="M82.9,454.3c0,0,0.1-0.5,0.1-1.5c0-1.1,0.1-2.5,0.1-4.1c0.1-1.7,0.2-3.8,0.5-6.1c0.3-2.3,0.9-4.7,1.7-7.3 + c0.8-2.5,1.9-4.8,3.2-6.7c1.3-1.9,2.9-3.3,4.4-4.1c1.5-0.9,2.8-1.3,3.8-1.4c1-0.2,1.5-0.3,1.5-0.3c0,0-0.5,0-1.5,0.1 + c-1,0.1-2.4,0.5-4,1.3c-1.6,0.8-3.2,2.2-4.6,4.2c-1.4,1.9-2.5,4.3-3.4,6.8c-0.8,2.6-1.4,5.1-1.7,7.4c-0.3,2.3-0.4,4.4-0.4,6.2 + c0,1.8,0,3.2,0.1,4.1C82.8,453.7,82.9,454.3,82.9,454.3z"/> + </g> + </g> + </g> + <g> + <g> + <g> + <path class="st8" d="M83,451.1c0,0,0-0.2,0-0.7c0-0.5-0.1-1.2-0.1-2c-0.1-1.7-0.3-4.2-0.4-7.3c-0.2-6.2-0.2-14.8,0.2-24.3 + c0.4-9.5,1.3-18,2.2-24.2c0.2-1.5,0.4-2.9,0.6-4.1c0.2-1.2,0.4-2.3,0.5-3.1c0.1-0.8,0.3-1.5,0.3-2c0.1-0.5,0.1-0.7,0.1-0.7 + c0,0-0.1,0.2-0.2,0.7c-0.1,0.5-0.3,1.2-0.5,2c-0.2,0.8-0.4,1.9-0.6,3.1c-0.2,1.2-0.5,2.6-0.7,4.1c-1,6.1-1.9,14.7-2.3,24.2 + c-0.4,9.5-0.4,18.1,0,24.3c0.2,3.1,0.4,5.6,0.6,7.3c0.1,0.8,0.2,1.5,0.2,2C83,450.9,83,451.1,83,451.1z"/> + </g> + </g> + </g> + <g> + <g> + <g> + <path class="st8" d="M82,434.6c0,0,0-0.7-0.1-2.1c-0.2-1.3-0.5-3.3-1-5.6c-0.5-2.3-1.3-5.1-2.4-8.1c-1-3-2.4-6.3-4.1-9.5 + c-3.3-6.6-7.5-11.9-11.1-15.1c-0.8-0.9-1.8-1.5-2.5-2.1c-0.4-0.3-0.7-0.6-1.1-0.8c-0.3-0.2-0.7-0.4-1-0.6 + c-1.2-0.7-1.8-1.1-1.8-1c-0.1,0.1,2.6,1.5,6.1,4.8c3.5,3.3,7.6,8.5,10.9,15.1c1.7,3.2,3,6.5,4.1,9.4c1.1,3,1.9,5.7,2.5,8 + c0.6,2.3,0.9,4.2,1.2,5.5C81.8,433.8,82,434.6,82,434.6z"/> + </g> + </g> + </g> + </g> +</g> +<g id="Confetti"> + <g> + <g> + <path class="st19" d="M234.6,205.8c-1.8-6.5-10.3-5.9-10.3-5.9l0.2,4.5c4.9-0.8,8,4.9,8,4.9L234.6,205.8z"/> + </g> + </g> + <g> + <g> + <path class="st19" d="M151,129.7c-4.6,0.5-5.1,6.2-5.1,6.2l3.1,0.4c0-3.4,4.2-4.8,4.2-4.8L151,129.7z"/> + </g> + </g> + <g> + <g> + <path class="st19" d="M193.7,68.8c2.8,7.1,12.3,5.5,12.3,5.5l-0.8-5.1c-5.4,1.5-9.5-4.6-9.5-4.6L193.7,68.8z"/> + </g> + </g> + <g> + <g> + <path class="st19" d="M87.1,189.1c-4.6,0.5-5.1,6.2-5.1,6.2l3.1,0.4c0-3.4,4.2-4.8,4.2-4.8L87.1,189.1z"/> + </g> + </g> + <g> + <g> + <path class="st19" d="M106,91.5c0.7-4.6-4.8-6.5-4.8-6.5l-1.1,2.9c3.3,0.8,3.6,5.2,3.6,5.2L106,91.5z"/> + </g> + </g> + <g> + <g> + <path class="st19" d="M27.6,161.5c-0.8,7.2,8.7,8.9,8.7,8.9l1.7-4.7c-5.7-0.5-6.5-7.2-6.5-7.2L27.6,161.5z"/> + </g> + </g> + <g> + <g> + <path class="st19" d="M252.8,127c4.6,0.3,6.1-5.3,6.1-5.3l-3-0.9c-0.5,3.3-4.9,4-4.9,4L252.8,127z"/> + </g> + </g> +</g> +</svg> diff --git a/public/images/writing.png b/public/images/writing.png Binary files differnew file mode 100644 index 00000000..17fedd74 --- /dev/null +++ b/public/images/writing.png diff --git a/src-migrate/modules/cart/components/Item.tsx b/src-migrate/modules/cart/components/Item.tsx index 6ffbb524..ab2e7ce1 100644 --- a/src-migrate/modules/cart/components/Item.tsx +++ b/src-migrate/modules/cart/components/Item.tsx @@ -36,26 +36,29 @@ const CartItem = ({ item, editable = true, selfPicking}: Props) => { )} <div className='w-2' /> <div> - Selamat! Pembelian anda lebih hemat {' '} + Selamat! Pembelian anda lebih hemat{' '} <span className={style.savingAmt}> - Rp{formatCurrency((item.package_price || 0) * item.quantity - item.subtotal)} + Rp + {formatCurrency( + (item.package_price || 0) * item.quantity - item.subtotal + )} </span> </div> </div> )} <div className={style.mainProdWrapper}> - {editable && ( - <CartItemSelect item={item} /> - )} + {editable && <CartItemSelect item={item} />} <div className='w-4' /> <CartItem.Image item={item} /> <div className={style.details}> - {(item.is_in_bu) && (item.on_hand_qty >= item.quantity) && ( + {item?.available_quantity > 0 && ( <div className='text-[10px] text-red-500 italic'> - *Barang ini bisa di pickup maksimal pukul 16.00 + {item.quantity <= item?.available_quantity + ? '*Barang ini bisa di pickup maksimal pukul 16.00' + : `*${item?.available_quantity} Barang ini bisa di pickup maksimal pukul 16.00`} </div> )} <CartItem.Name item={item} /> diff --git a/src-migrate/modules/page-content/index.tsx b/src-migrate/modules/page-content/index.tsx index edecb855..54ee0a04 100644 --- a/src-migrate/modules/page-content/index.tsx +++ b/src-migrate/modules/page-content/index.tsx @@ -1,4 +1,4 @@ -import { useMemo } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { useQuery } from 'react-query'; import { PageContentProps } from '~/types/pageContent'; import { getPageContent } from '~/services/pageContent'; @@ -8,18 +8,31 @@ type Props = { }; const PageContent = ({ path }: Props) => { - const { data, isLoading } = useQuery<PageContentProps>( - `page-content:${path}`, - async () => await getPageContent({ path }) - ); + const [localData, setData] = useState<PageContentProps>(); + const [shouldFetch, setShouldFetch] = useState(false); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + const fetchData = async () => { + setIsLoading(true); + const res = await fetch(`/api/page-content?path=${path}`); + const { data } = await res.json(); + if (data) { + setData(data); + } + setIsLoading(false); + }; + + fetchData(); + }, []); const parsedContent = useMemo<string>(() => { - if (!data) return ''; - return data.content.replaceAll( + if (!localData) return ''; + return localData.content.replaceAll( 'src="/web/image', `src="${process.env.NEXT_PUBLIC_ODOO_API_HOST}/web/image` ); - }, [data]); + }, [localData]); if (isLoading) return <PageContentSkeleton />; return <div dangerouslySetInnerHTML={{ __html: parsedContent || '' }}></div>; diff --git a/src-migrate/modules/product-card/components/ProductCard.tsx b/src-migrate/modules/product-card/components/ProductCard.tsx index 0febfadb..a439cdc8 100644 --- a/src-migrate/modules/product-card/components/ProductCard.tsx +++ b/src-migrate/modules/product-card/components/ProductCard.tsx @@ -1,95 +1,108 @@ -import style from '../styles/product-card.module.css' +import style from '../styles/product-card.module.css'; import ImageNext from 'next/image'; -import clsx from 'clsx' -import Link from 'next/link' -import React, { useEffect, useMemo, useState } from 'react' -import Image from '~/components/ui/image' -import useUtmSource from '~/hooks/useUtmSource' -import clsxm from '~/libs/clsxm' -import formatCurrency from '~/libs/formatCurrency' -import { formatToShortText } from '~/libs/formatNumber' -import { createSlug } from '~/libs/slug' -import { IProduct } from '~/types/product' - +import clsx from 'clsx'; +import Link from 'next/link'; +import React, { useEffect, useMemo, useState } from 'react'; +import Image from '~/components/ui/image'; +import useUtmSource from '~/hooks/useUtmSource'; +import clsxm from '~/libs/clsxm'; +import formatCurrency from '~/libs/formatCurrency'; +import { formatToShortText } from '~/libs/formatNumber'; +import { createSlug } from '~/libs/slug'; +import { IProduct } from '~/types/product'; +import useDevice from '@/core/hooks/useDevice'; type Props = { - product: IProduct - layout?: 'vertical' | 'horizontal' -} + product: IProduct; + layout?: 'vertical' | 'horizontal'; +}; const ProductCard = ({ product, layout = 'vertical' }: Props) => { - const utmSource = useUtmSource() - + const utmSource = useUtmSource(); + const { isDesktop, isMobile } = useDevice(); const URL = { - product: createSlug('/shop/product/', product.name, product.id.toString()) + `?utm_source=${utmSource}`, - manufacture: createSlug('/shop/brands/', product.manufacture.name, product.manufacture.id.toString()), - } + product: + createSlug('/shop/product/', product.name, product.id.toString()) + + `?utm_source=${utmSource}`, + manufacture: createSlug( + '/shop/brands/', + product.manufacture.name, + product.manufacture.id.toString() + ), + }; const image = useMemo(() => { - if (product.image) return product.image + '?ratio=square' - return '/images/noimage.jpeg' - }, [product.image]) + if (!isDesktop && product.image_mobile) { + return product.image_mobile + '?ratio=square'; + } else { + if (product.image) return product.image + '?ratio=square'; + return '/images/noimage.jpeg'; + } + }, [product.image, product.image_mobile]); return ( - <div className={clsxm(style['wrapper'], { - [style['wrapper-v']]: layout === 'vertical', - [style['wrapper-h']]: layout === 'horizontal', - })} + <div + className={clsxm(style['wrapper'], { + [style['wrapper-v']]: layout === 'vertical', + [style['wrapper-h']]: layout === 'horizontal', + })} > - <div className={clsxm('relative', { - [style['image-v']]: layout === 'vertical', - [style['image-h']]: layout === 'horizontal', - })}> + <div + className={clsxm('relative', { + [style['image-v']]: layout === 'vertical', + [style['image-h']]: layout === 'horizontal', + })} + > <Link href={URL.product}> - - <div className="relative"> - <Image - src={image} - alt={product.name} - width={128} - height={128} - className='object-contain object-center h-full w-full' - /> - <div className="absolute top-0 right-0 flex mt-2"> - <div className="gambarB "> - {product.isSni && ( - <ImageNext - src="/images/sni-logo.png" - alt="SNI Logo" - className="w-3 h-4 object-contain object-top sm:h-4" - width={50} - height={50} - /> - )} - </div> - <div className="gambarC "> - {product.isTkdn && ( - <ImageNext - src="/images/TKDN.png" - alt="TKDN" - className="w-5 h-4 object-contain object-top ml-1 mr-1 sm:h-6" - width={50} - height={50} - /> - )} + <div className='relative'> + <Image + src={image} + alt={product.name} + width={128} + height={128} + className='object-contain object-center h-full w-full' + /> + <div className='absolute top-0 right-0 flex mt-2'> + <div className='gambarB '> + {product.isSni && ( + <ImageNext + src='/images/sni-logo.png' + alt='SNI Logo' + className='w-3 h-4 object-contain object-top sm:h-4' + width={50} + height={50} + /> + )} + </div> + <div className='gambarC '> + {product.isTkdn && ( + <ImageNext + src='/images/TKDN.png' + alt='TKDN' + className='w-5 h-4 object-contain object-top ml-1 mr-1 sm:h-6' + width={50} + height={50} + /> + )} + </div> </div> </div> - </div> {product.variant_total > 1 && ( - <div className={style['variant-badge']}>{product.variant_total} Varian</div> + <div className={style['variant-badge']}> + {product.variant_total} Varian + </div> )} </Link> </div> - <div className={clsxm({ - [style['content-v']]: layout === 'vertical', - [style['content-h']]: layout === 'horizontal', - })}> - <Link - href={URL.manufacture} - className={style['brand']} - > + <div + className={clsxm({ + [style['content-v']]: layout === 'vertical', + [style['content-h']]: layout === 'horizontal', + })} + > + <Link href={URL.manufacture} className={style['brand']}> {product.manufacture.name} </Link> @@ -113,17 +126,15 @@ const ProductCard = ({ product, layout = 'vertical' }: Props) => { <div className='h-1.5' /> <div className={style['price-inc']}> - Inc PPN: - Rp {formatCurrency(Math.round(product.lowest_price.price * 1.11))} + Inc PPN: Rp{' '} + {formatCurrency(Math.round(product.lowest_price.price * 1.11))} </div> <div className='h-1' /> <div className='flex items-center gap-x-2.5'> {product.stock_total > 0 && ( - <div className={style['ready-stock']}> - Ready Stock - </div> + <div className={style['ready-stock']}>Ready Stock</div> )} {product.qty_sold > 0 && ( <div className={style['sold']}> @@ -131,14 +142,11 @@ const ProductCard = ({ product, layout = 'vertical' }: Props) => { </div> )} </div> - </div> </div> - ) -} - -const classPrefix = ({ layout }: Props) => { + ); +}; -} +const classPrefix = ({ layout }: Props) => {}; -export default ProductCard
\ No newline at end of file +export default ProductCard; diff --git a/src-migrate/modules/product-detail/components/AddToCart.tsx b/src-migrate/modules/product-detail/components/AddToCart.tsx index a5284637..280e4a7a 100644 --- a/src-migrate/modules/product-detail/components/AddToCart.tsx +++ b/src-migrate/modules/product-detail/components/AddToCart.tsx @@ -1,51 +1,55 @@ -import BottomPopup from '@/core/components/elements/Popup/BottomPopup' +import BottomPopup from '@/core/components/elements/Popup/BottomPopup'; import style from '../styles/price-action.module.css'; -import { Button, Link, useToast } from '@chakra-ui/react' -import product from 'next-seo/lib/jsonld/product' -import { useRouter } from 'next/router' -import { useEffect, useState } from 'react' -import Image from '~/components/ui/image' -import { getAuth } from '~/libs/auth' -import { upsertUserCart } from '~/services/cart' +import { Button, Link, useToast } from '@chakra-ui/react'; +import product from 'next-seo/lib/jsonld/product'; +import { useRouter } from 'next/router'; +import { useEffect, useState } from 'react'; +import Image from '~/components/ui/image'; +import { getAuth } from '~/libs/auth'; +import { upsertUserCart } from '~/services/cart'; import LazyLoad from 'react-lazy-load'; import ProductSimilar from '../../../../src/lib/product/components/ProductSimilar'; import { IProductDetail } from '~/types/product'; import ImageNext from 'next/image'; -import { useProductCartContext } from '@/contexts/ProductCartContext' -import { createSlug } from '~/libs/slug' -import formatCurrency from '~/libs/formatCurrency' +import { useProductCartContext } from '@/contexts/ProductCartContext'; +import { createSlug } from '~/libs/slug'; +import formatCurrency from '~/libs/formatCurrency'; import { useProductDetail } from '../stores/useProductDetail'; type Props = { - variantId: number | null, + variantId: number | null; quantity?: number; source?: 'buy' | 'add_to_cart'; - products : IProductDetail -} + products: IProductDetail; +}; -type Status = 'idle' | 'loading' | 'success' +type Status = 'idle' | 'loading' | 'success'; const AddToCart = ({ variantId, quantity = 1, source = 'add_to_cart', - products + products, }: Props) => { - const auth = getAuth() - const router = useRouter() + let auth = getAuth(); + const router = useRouter(); const toast = useToast({ position: 'top', - isClosable: true - }) + isClosable: true, + }); - const { - askAdminUrl, - } = useProductDetail(); + const { askAdminUrl } = useProductDetail(); const [product, setProducts] = useState(products); - const [status, setStatus] = useState<Status>('idle') - const { productCart, setRefreshCart, setProductCart, refreshCart, isLoading, setIsloading } = - useProductCartContext() + const [status, setStatus] = useState<Status>('idle'); + const { + productCart, + setRefreshCart, + setProductCart, + refreshCart, + isLoading, + setIsloading, + } = useProductCartContext(); const productSimilarQuery = [ product?.name, @@ -55,32 +59,48 @@ const AddToCart = ({ const [addCartAlert, setAddCartAlert] = useState(false); const handleButton = async () => { - if (typeof auth !== 'object') { - const currentUrl = encodeURIComponent(router.asPath) - router.push(`/login?next=${currentUrl}`) - return; + let isLoggedIn = typeof auth === 'object'; + + if (!isLoggedIn) { + const currentUrl = encodeURIComponent(router.asPath); + await router.push(`/login?next=${currentUrl}`); + + // Tunggu login berhasil, misalnya dengan memantau perubahan status auth. + const authCheckInterval = setInterval(() => { + const newAuth = getAuth(); + if (typeof newAuth === 'object') { + isLoggedIn = true; + auth = newAuth; // Update nilai auth setelah login + clearInterval(authCheckInterval); + } + }, 500); // Periksa status login setiap 500ms + + await new Promise((resolve) => { + const checkLogin = setInterval(() => { + if (isLoggedIn) { + clearInterval(checkLogin); + resolve(null); + } + }, 500); + }); } - - if ( - !variantId || - isNaN(quantity) || - typeof auth !== 'object' - ) return; - if (status === 'success') return - setStatus('loading') + + if (!variantId || isNaN(quantity) || typeof auth !== 'object') return; + if (status === 'success') return; + setStatus('loading'); await upsertUserCart({ userId: auth.id, - type: 'product', - id: variantId, - qty: quantity, - selected: true, - source: source, - qtyAppend: true - }) - setStatus('idle') + type: 'product', + id: variantId, + qty: quantity, + selected: true, + source: source, + qtyAppend: true, + }); + setStatus('idle'); setRefreshCart(true); setAddCartAlert(true); - + toast({ title: 'Tambah ke keranjang', description: 'Berhasil menambahkan barang ke keranjang belanja', @@ -88,120 +108,130 @@ const AddToCart = ({ duration: 3000, isClosable: true, position: 'top', - }) - + }); + if (source === 'buy') { - router.push('/shop/checkout?source=buy') + router.push('/shop/checkout?source=buy'); } - } + }; useEffect(() => { - if (status === 'success') setTimeout(() => { setStatus('idle') }, 3000) - }, [status]) + if (status === 'success') + setTimeout(() => { + setStatus('idle'); + }, 3000); + }, [status]); const btnConfig = { - 'add_to_cart': { + add_to_cart: { colorScheme: 'yellow', - text: 'Keranjang' + text: 'Keranjang', }, - 'buy': { + buy: { colorScheme: 'red', - text: 'Beli' - } - } + text: 'Beli', + }, + }; return ( <div className='w-full'> - <Button onClick={handleButton} colorScheme={btnConfig[source].colorScheme} className='w-full'> + <Button + onClick={handleButton} + colorScheme={btnConfig[source].colorScheme} + className='w-full' + > {btnConfig[source].text} </Button> <BottomPopup - className='!container' - title='Berhasil Ditambahkan' - active={addCartAlert} - close={() => { - setAddCartAlert(false); - }} - > - <div className='flex mt-4'> - <div className='w-[10%]'> - <ImageNext - src={product.image} - alt={product.name} - className='h-32 object-contain object-center w-full border border-gray_r-4' - width={80} - height={80} - /> - </div> - <div className='ml-3 flex flex-1 items-start font-medium justify-center flex-col gap-y-1'> - {!!product.manufacture.name ? ( - <Link - href={createSlug('/shop/brands/', product.manufacture.name, product.manufacture.id.toString())} - className=' hover:underline' - color={"red"} - > - {product.manufacture.name} - </Link> - ) : '-'} - <p className='text-ellipsis overflow-hidden'> - {product.name} - </p> - <p> - {product.code} - </p> - {!!product.lowest_price && product.lowest_price.price > 0 && ( + className='!container' + title='Berhasil Ditambahkan' + active={addCartAlert} + close={() => { + setAddCartAlert(false); + }} + > + <div className='flex mt-4'> + <div className='w-[10%]'> + <ImageNext + src={product.image} + alt={product.name} + className='h-32 object-contain object-center w-full border border-gray_r-4' + width={80} + height={80} + /> + </div> + <div className='ml-3 flex flex-1 items-start font-medium justify-center flex-col gap-y-1'> + {!!product.manufacture.name ? ( + <Link + href={createSlug( + '/shop/brands/', + product.manufacture.name, + product.manufacture.id.toString() + )} + className=' hover:underline' + color={'red'} + > + {product.manufacture.name} + </Link> + ) : ( + '-' + )} + <p className='text-ellipsis overflow-hidden'>{product.name}</p> + <p>{product.code}</p> + {!!product.lowest_price && product.lowest_price.price > 0 && ( + <> + <div className='flex items-end gap-x-2'> + {product.lowest_price.discount_percentage > 0 && ( <> - <div className='flex items-end gap-x-2'> - {product.lowest_price.discount_percentage > 0 && ( - <> - <div className='badge-solid-red'> - {Math.floor(product.lowest_price.discount_percentage)}% - </div> - <div className='text-gray_r-11 line-through text-[11px] sm:text-caption-2'> - Rp {formatCurrency(product.lowest_price.price || 0)} - </div> - </> - )} - <div className='text-danger-500 font-semibold'> - Rp {formatCurrency(product.lowest_price.price_discount || 0)} - </div> + <div className='badge-solid-red'> + {Math.floor(product.lowest_price.discount_percentage)}% + </div> + <div className='text-gray_r-11 line-through text-[11px] sm:text-caption-2'> + Rp {formatCurrency(product.lowest_price.price || 0)} </div> </> )} + <div className='text-danger-500 font-semibold'> + Rp{' '} + {formatCurrency(product.lowest_price.price_discount || 0)} + </div> + </div> + </> + )} - {!!product.lowest_price && product.lowest_price.price === 0 && ( - <span> - Hubungi kami untuk dapatkan harga terbaik,{' '} - <Link - href={askAdminUrl} - target='_blank' - className='font-medium underline' - color={'red'} - > - klik disini - </Link> - </span> - )} - </div> - <div className='ml-3 flex items-center font-normal'> - <Link - href='/shop/cart' - className='flex-1 py-2 text-gray_r-12 btn-yellow' - > - Lihat Keranjang - </Link> - </div> + {!!product.lowest_price && product.lowest_price.price === 0 && ( + <span> + Hubungi kami untuk dapatkan harga terbaik,{' '} + <Link + href={askAdminUrl} + target='_blank' + className='font-medium underline' + color={'red'} + > + klik disini + </Link> + </span> + )} + </div> + <div className='ml-3 flex items-center font-normal'> + <Link + href='/shop/cart' + className='flex-1 py-2 text-gray_r-12 btn-yellow' + > + Lihat Keranjang + </Link> </div> - <div className='mt-8 mb-4'> - <div className='text-h-sm font-semibold mb-6'> - Kamu Mungkin Juga Suka - </div> - <LazyLoad> - <ProductSimilar query={productSimilarQuery} /> - </LazyLoad> + </div> + <div className='mt-8 mb-4'> + <div className='text-h-sm font-semibold mb-6'> + Kamu Mungkin Juga Suka </div> - </BottomPopup> + <LazyLoad> + <ProductSimilar query={productSimilarQuery} /> + </LazyLoad> + </div> + </BottomPopup> </div> - ) -} + ); +}; -export default AddToCart
\ No newline at end of file +export default AddToCart; diff --git a/src-migrate/modules/product-detail/components/AddToQuotation.tsx b/src-migrate/modules/product-detail/components/AddToQuotation.tsx new file mode 100644 index 00000000..f9b6c2b3 --- /dev/null +++ b/src-migrate/modules/product-detail/components/AddToQuotation.tsx @@ -0,0 +1,227 @@ +import BottomPopup from '@/core/components/elements/Popup/BottomPopup'; +import style from '../styles/price-action.module.css'; +import { Button, Link, useToast } from '@chakra-ui/react'; +import { ArrowDownTrayIcon } from '@heroicons/react/24/outline'; +import product from 'next-seo/lib/jsonld/product'; +import { useRouter } from 'next/router'; +import { useEffect, useState } from 'react'; +import Image from '~/components/ui/image'; +import { getAuth } from '~/libs/auth'; +import { upsertUserCart } from '~/services/cart'; +import LazyLoad from 'react-lazy-load'; +import ProductSimilar from '../../../../src/lib/product/components/ProductSimilar'; +import { IProductDetail } from '~/types/product'; +import ImageNext from 'next/image'; +import { useProductCartContext } from '@/contexts/ProductCartContext'; +import { createSlug } from '~/libs/slug'; +import formatCurrency from '~/libs/formatCurrency'; +import { useProductDetail } from '../stores/useProductDetail'; + +type Props = { + variantId: number | null; + quantity?: number; + source?: 'buy' | 'add_to_cart'; + products: IProductDetail; +}; + +type Status = 'idle' | 'loading' | 'success'; + +const AddToQuotation = ({ + variantId, + quantity = 1, + source = 'add_to_cart', + products, +}: Props) => { + const auth = getAuth(); + const router = useRouter(); + const toast = useToast({ + position: 'top', + isClosable: true, + }); + + const { askAdminUrl } = useProductDetail(); + + const [product, setProducts] = useState(products); + const [status, setStatus] = useState<Status>('idle'); + const { + productCart, + setRefreshCart, + setProductCart, + refreshCart, + isLoading, + setIsloading, + } = useProductCartContext(); + + const productSimilarQuery = [ + product?.name, + `fq=-product_id_i:${product.id}`, + `fq=-manufacture_id_i:${product.manufacture?.id || 0}`, + ].join('&'); + const [addCartAlert, setAddCartAlert] = useState(false); + + const handleButton = async () => { + if (typeof auth !== 'object') { + const currentUrl = encodeURIComponent(router.asPath); + router.push(`/login?next=${currentUrl}`); + return; + } + + if (!variantId || isNaN(quantity) || typeof auth !== 'object') return; + if (status === 'success') return; + setStatus('loading'); + await upsertUserCart({ + userId: auth.id, + type: 'product', + id: variantId, + qty: quantity, + selected: true, + source: source, + qtyAppend: true, + }); + setStatus('idle'); + setRefreshCart(true); + setAddCartAlert(true); + + toast({ + title: 'Tambah ke keranjang', + description: 'Berhasil menambahkan barang ke keranjang belanja', + status: 'success', + duration: 3000, + isClosable: true, + position: 'top', + }); + + if (source === 'buy') { + router.push('/shop/quotation?source=buy'); + } + }; + useEffect(() => { + if (status === 'success') + setTimeout(() => { + setStatus('idle'); + }, 3000); + }, [status]); + + const btnConfig = { + add_to_cart: { + colorScheme: 'yellow', + + text: 'Keranjang', + }, + buy: { + colorScheme: 'red', + text: 'Beli', + }, + }; + + return ( + <div className='w-full'> + <Button + onClick={handleButton} + color={'red'} + colorScheme='white' + className='w-full border-2 p-2 gap-1 hover:bg-slate-100 flex items-center' + > + <ImageNext + src='/images/writing.png' + alt='penawaran instan' + className='' + width={25} + height={25} + /> + Penawaran Harga Instan + </Button> + <BottomPopup + className='!container' + title='Berhasil Ditambahkan' + active={addCartAlert} + close={() => { + setAddCartAlert(false); + }} + > + <div className='flex mt-4'> + <div className='w-[10%]'> + <ImageNext + src={product.image} + alt={product.name} + className='h-32 object-contain object-center w-full border border-gray_r-4' + width={80} + height={80} + /> + </div> + <div className='ml-3 flex flex-1 items-start font-medium justify-center flex-col gap-y-1'> + {!!product.manufacture.name ? ( + <Link + href={createSlug( + '/shop/brands/', + product.manufacture.name, + product.manufacture.id.toString() + )} + className=' hover:underline' + color={'red'} + > + {product.manufacture.name} + </Link> + ) : ( + '-' + )} + <p className='text-ellipsis overflow-hidden'>{product.name}</p> + <p>{product.code}</p> + {!!product.lowest_price && product.lowest_price.price > 0 && ( + <> + <div className='flex items-end gap-x-2'> + {product.lowest_price.discount_percentage > 0 && ( + <> + <div className='badge-solid-red'> + {Math.floor(product.lowest_price.discount_percentage)}% + </div> + <div className='text-gray_r-11 line-through text-[11px] sm:text-caption-2'> + Rp {formatCurrency(product.lowest_price.price || 0)} + </div> + </> + )} + <div className='text-danger-500 font-semibold'> + Rp{' '} + {formatCurrency(product.lowest_price.price_discount || 0)} + </div> + </div> + </> + )} + + {!!product.lowest_price && product.lowest_price.price === 0 && ( + <span> + Hubungi kami untuk dapatkan harga terbaik,{' '} + <Link + href={askAdminUrl} + target='_blank' + className='font-medium underline' + color={'red'} + > + klik disini + </Link> + </span> + )} + </div> + <div className='ml-3 flex items-center font-normal'> + <Link + href='/shop/cart' + className='flex-1 py-2 text-gray_r-12 btn-yellow' + > + Lihat Keranjang + </Link> + </div> + </div> + <div className='mt-8 mb-4'> + <div className='text-h-sm font-semibold mb-6'> + Kamu Mungkin Juga Suka + </div> + <LazyLoad> + <ProductSimilar query={productSimilarQuery} /> + </LazyLoad> + </div> + </BottomPopup> + </div> + ); +}; + +export default AddToQuotation; diff --git a/src-migrate/modules/product-detail/components/Image.tsx b/src-migrate/modules/product-detail/components/Image.tsx index 30ca0d34..96ae2027 100644 --- a/src-migrate/modules/product-detail/components/Image.tsx +++ b/src-migrate/modules/product-detail/components/Image.tsx @@ -1,22 +1,22 @@ import style from '../styles/image.module.css'; import ImageNext from 'next/image'; -import React, { useEffect, useMemo, useState } from 'react' -import { InfoIcon } from 'lucide-react' -import { Tooltip } from '@chakra-ui/react' +import React, { useEffect, useMemo, useState } from 'react'; +import { InfoIcon } from 'lucide-react'; +import { Tooltip } from '@chakra-ui/react'; -import { IProductDetail } from '~/types/product' -import ImageUI from '~/components/ui/image' +import { IProductDetail } from '~/types/product'; +import ImageUI from '~/components/ui/image'; import moment from 'moment'; - +import useDevice from '@/core/hooks/useDevice'; type Props = { - product: IProductDetail -} + product: IProductDetail; +}; const Image = ({ product }: Props) => { - const flashSale = product.flash_sale + const flashSale = product.flash_sale; const [count, setCount] = useState(flashSale?.remaining_time || 0); - + const { isDesktop, isMobile } = useDevice(); useEffect(() => { let interval: NodeJS.Timeout; @@ -34,59 +34,60 @@ const Image = ({ product }: Props) => { }; }, [flashSale?.remaining_time]); - const duration = moment.duration(count, 'seconds') - + const duration = moment.duration(count, 'seconds'); const image = useMemo(() => { - if (product.image) return product.image + '?ratio=square' - return '/images/noimage.jpeg' - }, [product.image]) + if (!isDesktop && product.image_mobile) { + return product.image_mobile + '?ratio=square'; + } else { + if (product.image) return product.image + '?ratio=square'; + return '/images/noimage.jpeg'; + } + }, [product.image, product.image_mobile]); return ( <div className={style['wrapper']}> {/* <div className="relative"> */} - <ImageUI - src={image} - alt={product.name} - width={256} - height={256} - className={style['image']} - loading='eager' - priority - /> - <div className="absolute top-4 right-10 flex "> - <div className="gambarB "> - {product.isSni && ( - <ImageNext - src="/images/sni-logo.png" - alt="SNI Logo" - className="w-12 h-8 object-contain object-top sm:h-6" - width={50} - height={50} - /> - )} - </div> - <div className="gambarC "> - {product.isTkdn && ( - <ImageNext - src="/images/TKDN.png" - alt="TKDN" - className="w-16 h-8 object-contain object-top ml-1 mr-1 sm:h-6" - width={50} - height={50} - /> - )} - </div> - </div> - {/* </div> */} - - + <ImageUI + src={image} + alt={product.name} + width={256} + height={256} + className={style['image']} + loading='eager' + priority + /> + <div className='absolute top-4 right-10 flex '> + <div className='gambarB '> + {product.isSni && ( + <ImageNext + src='/images/sni-logo.png' + alt='SNI Logo' + className='w-12 h-8 object-contain object-top sm:h-6' + width={50} + height={50} + /> + )} + </div> + <div className='gambarC '> + {product.isTkdn && ( + <ImageNext + src='/images/TKDN.png' + alt='TKDN' + className='w-16 h-8 object-contain object-top ml-1 mr-1 sm:h-6' + width={50} + height={50} + /> + )} + </div> + </div> + {/* </div> */} <div className={style['absolute-info']}> <Tooltip placement='bottom-end' label='Gambar atau foto berperan sebagai ilustrasi produk. Kadang tidak sesuai dengan kondisi terbaru dengan berbagai perubahan dan perbaikan. Hubungi admin kami untuk informasi yang lebih baik perihal gambar.' > - <div className="text-gray-600"> + <div className='text-gray-600'> <InfoIcon size={20} /> </div> </Tooltip> @@ -94,7 +95,7 @@ const Image = ({ product }: Props) => { {flashSale.remaining_time > 0 && ( <div className='absolute bottom-0 w-full h-14'> - <div className="relative w-full h-full"> + <div className='relative w-full h-full'> <ImageUI src='/images/BG-FLASH-SALE.jpg' alt='Flash Sale Indoteknik' @@ -105,7 +106,9 @@ const Image = ({ product }: Props) => { <div className={style['flashsale']}> <div className='flex items-center gap-x-3'> - <div className={style['disc-badge']}>{Math.floor(product.lowest_price.discount_percentage)}%</div> + <div className={style['disc-badge']}> + {Math.floor(product.lowest_price.discount_percentage)}% + </div> <div className={style['flashsale-text']}> <ImageUI src='/images/ICON_FLASH_SALE_WEBSITE_INDOTEKNIK.svg' @@ -122,12 +125,11 @@ const Image = ({ product }: Props) => { <span>{duration.seconds().toString().padStart(2, '0')}</span> </div> </div> - </div> </div> )} </div> - ) -} + ); +}; -export default Image
\ No newline at end of file +export default Image; diff --git a/src-migrate/modules/product-detail/components/Information.tsx b/src-migrate/modules/product-detail/components/Information.tsx index 75ae3c41..5e1ea186 100644 --- a/src-migrate/modules/product-detail/components/Information.tsx +++ b/src-migrate/modules/product-detail/components/Information.tsx @@ -1,56 +1,232 @@ -import style from '../styles/information.module.css' +import { + AutoComplete, + AutoCompleteInput, + AutoCompleteItem, + AutoCompleteList, +} from '@choc-ui/chakra-autocomplete'; +import style from '../styles/information.module.css'; -import React from 'react' -import dynamic from 'next/dynamic' -import Link from 'next/link' -import { useQuery } from 'react-query' +import dynamic from 'next/dynamic'; +import Link from 'next/link'; +import { useEffect, useRef, useState } from 'react'; -import { IProductDetail } from '~/types/product' -import { IProductVariantSLA } from '~/types/productVariant' -import { createSlug } from '~/libs/slug' -import { getVariantSLA } from '~/services/productVariant' -import { formatToShortText } from '~/libs/formatNumber' +import currencyFormat from '@/core/utils/currencyFormat'; +import { InputGroup, InputRightElement } from '@chakra-ui/react'; +import { ChevronDownIcon } from '@heroicons/react/24/outline'; +import Image from 'next/image'; +import { formatToShortText } from '~/libs/formatNumber'; +import { createSlug } from '~/libs/slug'; +import { getVariantSLA } from '~/services/productVariant'; +import { IProductDetail } from '~/types/product'; +import { useProductDetail } from '../stores/useProductDetail'; -const Skeleton = dynamic(() => import('@chakra-ui/react').then((mod) => mod.Skeleton)) +const Skeleton = dynamic(() => + import('@chakra-ui/react').then((mod) => mod.Skeleton) +); type Props = { - product: IProductDetail -} + product: IProductDetail; +}; const Information = ({ product }: Props) => { - const querySLA = useQuery<IProductVariantSLA>({ - queryKey: ['variant-sla', product.variants[0]?.id], - queryFn: () => getVariantSLA(product.variants[0].id), - enabled: product.variant_total === 1 - }) + const { selectedVariant, setSelectedVariant, setSla, setActive, sla } = + useProductDetail(); - const sla = querySLA?.data + const [inputValue, setInputValue] = useState<string | null>( + selectedVariant?.code + ' - ' + selectedVariant?.attributes[0] + ); + const [disableFilter, setDisableFilter] = useState<boolean>(false); + const inputRef = useRef<HTMLInputElement>(null); + + const [variantOptions, setVariantOptions] = useState<any[]>( + product?.variants + ); + // let variantOptions = product?.variants; + + // const querySLA = useQuery<IProductVariantSLA>({ + // queryKey: ['variant-sla', selectedVariant?.id], + // queryFn: () => getVariantSLA(selectedVariant?.id), + // enabled: !!selectedVariant?.id, + // }); + // const sla = querySLA?.data; + + const getsla = async () => { + const querySLA = await getVariantSLA(selectedVariant?.id); + setSla(querySLA); + }; + + useEffect(() => { + if (selectedVariant) { + getsla(); + setInputValue( + selectedVariant?.code + + (selectedVariant?.attributes[0] + ? ' - ' + selectedVariant?.attributes[0] + : '') + ); + } + }, [selectedVariant]); + + const handleOnChange = (vals: any) => { + setDisableFilter(true); + let code = vals.replace(/\s-\s.*$/, '').trim(); + let variant = variantOptions.find((item) => item.code === code); + setSelectedVariant(variant); + setInputValue( + variant?.code + + (variant?.attributes[0] ? ' - ' + variant?.attributes[0] : '') + ); + if (variant) { + const filteredOptions = product?.variants.filter( + (item) => item !== variant + ); + const newOptions = [variant, ...filteredOptions]; + setVariantOptions(newOptions); + } + }; + + const handleOnKeyUp = (e: any) => { + setDisableFilter(false); + setInputValue(e.target.value); + }; return ( <div className={style['wrapper']}> + <div className='realtive mb-5'> + <label className='form-label mb-2 text-lg text-red-600'> + Pilih Variant * :{' '} + <span className='text-gray_r-9 text-sm'> + {product?.variants?.length} Variants + </span>{' '} + </label> + <AutoComplete + disableFilter={disableFilter} + openOnFocus + className='form-input' + onChange={(vals) => handleOnChange(vals)} + > + <InputGroup> + <AutoCompleteInput + ref={inputRef} + value={inputValue as string} + onChange={(e) => handleOnKeyUp(e)} + onFocus={() => setDisableFilter(true)} + /> + <InputRightElement className='mr-4'> + <ChevronDownIcon + className='h-6 w-6 text-gray-500' + onClick={() => inputRef?.current?.focus()} + /> + </InputRightElement> + </InputGroup> + + <AutoCompleteList> + {variantOptions.map((option, cid) => ( + <AutoCompleteItem + key={`option-${cid}`} + value={ + option.code + + (option?.attributes[0] ? ' - ' + option?.attributes[0] : '') + } + _selected={ + option.id === selectedVariant?.id + ? { + bg: 'gray.300', + } + : undefined + } + textTransform='capitalize' + > + <div + key={cid} + className='flex gap-x-2 w-full justify-between px-3 items-center p-2' + > + <div className='text-small'> + {option.code + + (option?.attributes[0] + ? ' - ' + option?.attributes[0] + : '')} + </div> + <div + className={ + option?.price?.discount_percentage + ? 'flex gap-x-4 items-center justify-between' + : '' + } + > + {option?.price?.discount_percentage > 0 && ( + <> + <div className='badge-solid-red text-xs'> + {Math.floor(option?.price?.discount_percentage)}% + </div> + <div className='min-w-16 sm:min-w-24 text-gray_r-11 line-through text-[11px] sm:text-caption-2'> + {currencyFormat(option?.price?.price)} + </div> + </> + )} + <div className='min-w-20 sm:min-w-28 text-danger-500 font-semibold'> + {currencyFormat(option?.price?.price_discount)} + </div> + </div> + </div> + </AutoCompleteItem> + ))} + </AutoCompleteList> + </AutoComplete> + </div> + <div className={style['row']}> - <div className={style['label']}>SKU Number</div> - <div className={style['value']}>SKU-{product.id}</div> + <div className={style['label']}>Item Code</div> + <div className={style['value']}>{selectedVariant?.code}</div> </div> <div className={style['row']}> <div className={style['label']}>Manufacture</div> <div className={style['value']}> {!!product.manufacture.name ? ( <Link - href={createSlug('/shop/brands/', product.manufacture.name, product.manufacture.id.toString())} - className='text-danger-500 hover:underline' + href={createSlug( + '/shop/brands/', + product.manufacture.name, + product.manufacture.id.toString() + )} > - {product.manufacture.name} + {product?.manufacture.logo ? ( + <Image + height={50} + width={100} + src={product.manufacture.logo} + alt={product.manufacture.name} + className='h-8 object-fit' + /> + ) : ( + <p className='font-bold text-red-500'> + {product.manufacture.name} + </p> + )} </Link> - ) : '-'} + ) : ( + '-' + )} + </div> + </div> + <div className={style['row']}> + <div className={style['label']}>Berat Barang</div> + <div className={style['value']}> + {selectedVariant?.weight > 0 ? `${selectedVariant?.weight} Kg` : '-'} </div> </div> <div className={style['row']}> <div className={style['label']}>Terjual</div> - <div className={style['value']}>{product.qty_sold > 0 ? formatToShortText(product.qty_sold) : '-'}</div> + <div className={style['value']}> + {product.qty_sold > 0 ? formatToShortText(product.qty_sold) : '-'} + </div> + </div> + <div className={style['row']}> + <div className={style['label']}>Persiapan Barang</div> + <div className={style['value']}>{sla?.sla_date}</div> </div> </div> - ) -} + ); +}; -export default Information
\ No newline at end of file +export default Information; diff --git a/src-migrate/modules/product-detail/components/PriceAction.tsx b/src-migrate/modules/product-detail/components/PriceAction.tsx index 9021264e..0b27b1b3 100644 --- a/src-migrate/modules/product-detail/components/PriceAction.tsx +++ b/src-migrate/modules/product-detail/components/PriceAction.tsx @@ -1,12 +1,17 @@ import style from '../styles/price-action.module.css'; -import React, { useEffect } from 'react'; +import Image from 'next/image'; +import Link from 'next/link'; +import { useEffect, useState } from 'react'; import formatCurrency from '~/libs/formatCurrency'; import { IProductDetail } from '~/types/product'; import { useProductDetail } from '../stores/useProductDetail'; import AddToCart from './AddToCart'; -import Link from 'next/link'; +import AddToQuotation from './AddToQuotation'; import { getAuth } from '~/libs/auth'; +import useDevice from '@/core/hooks/useDevice'; +import odooApi from '~/libs/odooApi'; +import { Button, Skeleton } from '@chakra-ui/react'; type Props = { product: IProductDetail; @@ -22,27 +27,58 @@ const PriceAction = ({ product }: Props) => { askAdminUrl, isApproval, setIsApproval, + selectedVariant, + sla, } = useProductDetail(); - + const [qtyPickUp, setQtyPickUp] = useState(0); + const { isDesktop, isMobile } = useDevice(); useEffect(() => { - setActive(product.variants[0]) - if(product.variants.length > 2 && product.variants[0].price.price === 0){ - const variants = product.variants + setActive(selectedVariant); + if (product.variants.length > 2 && product.variants[0].price.price === 0) { + const variants = product.variants; for (let i = 0; i < variants.length; i++) { - if(variants[i].price.price > 0){ - setActive(variants[i]) + if (variants[i].price.price > 0) { + setActive(variants[i]); break; } } } - - }, [product, setActive]); + }, [product, setActive, selectedVariant]); + useEffect(() => { + const fetchData = async () => { + const qty_available = await odooApi( + 'GET', + `/api/v1/product_variant/${selectedVariant.id}/qty_available` + ); + + setQtyPickUp(qty_available?.qty); + }; + fetchData(); + }, [selectedVariant]); + useEffect(() => { + setQuantityInput('1'); + }, [selectedVariant]); + + let voucherPastiHemat = 0; + + if ( + product?.voucher_pasti_hemat + ? product?.voucher_pasti_hemat.length + : voucherPastiHemat > 0 + ) { + const stringVoucher = product?.voucher_pasti_hemat[0]; + const validJsonString = stringVoucher.replace(/'/g, '"'); + voucherPastiHemat = JSON.parse(validJsonString); + } return ( <div - className='block md:sticky top-[150px] bg-white py-0 md:py-6 z-10' + className={`block md:sticky md:top-[150px] md:py-6 fixed bottom-0 left-0 right-0 bg-white p-2 z-10 ${ + isMobile && + 'pb-6 pt-6 rounded-lg shadow-[rgba(0,0,4,0.1)_0px_-4px_4px_0px] ' + }`} id='price-section' > {!!activePrice && activePrice.price > 0 && ( @@ -84,18 +120,69 @@ const PriceAction = ({ product }: Props) => { )} <div className='h-4' /> + <div className='flex gap-x-5 items-center'> + <div className='relative flex items-center'> + <button + type='button' + className='absolute left-0 px-2 py-1 h-full text-gray-500' + onClick={() => + setQuantityInput(String(Math.max(1, Number(quantityInput) - 1))) + } + > + - + </button> + <input + type='number' + id='quantity' + min={1} + value={quantityInput} + onChange={(e) => setQuantityInput(e.target.value)} + className={style['quantity-input']} + /> + <button + type='button' + className='absolute right-0 px-2 py-1 h-full text-gray-500' + onClick={() => setQuantityInput(String(Number(quantityInput) + 1))} + > + + + </button> + </div> + + <div> + <Skeleton + isLoaded={sla} + h='21px' + // w={16} + className={sla?.qty < 10 ? 'text-red-600 font-medium' : ''} + > + Stock : {sla?.qty}{' '} + </Skeleton> + {/* <span className={sla?.qty < 10 ? 'text-red-600 font-medium' : ''}> + {' '} + </span> */} + </div> + <div> + {selectedVariant?.is_in_bu && ( + <Link href='/panduan-pick-up-service' className='group'> + <Image + src='/images/PICKUP-NOW.png' + className='group-hover:scale-105 transition-transform duration-200' + alt='pickup now' + width={100} + height={12} + /> + </Link> + )} + </div> + </div> + {qtyPickUp > 0 && ( + <div className='text-[12px] mt-1 text-red-500 italic'> + * {qtyPickUp} barang bisa di pickup + </div> + )} + <div className='h-4' /> - <div className={style['action-wrapper']}> - <label htmlFor='quantity' className='hidden'> - Quantity - </label> - <input - type='number' - id='quantity' - value={quantityInput} - onChange={(e) => setQuantityInput(e.target.value)} - className={style['quantity-input']} - /> + <div className={`${style['action-wrapper']}`}> <AddToCart products={product} variantId={activeVariantId} @@ -110,6 +197,14 @@ const PriceAction = ({ product }: Props) => { /> )} </div> + <div className='mt-4'> + <AddToQuotation + source='buy' + products={product} + variantId={activeVariantId} + quantity={Number(quantityInput)} + /> + </div> </div> ); }; diff --git a/src-migrate/modules/product-detail/components/ProductDetail.tsx b/src-migrate/modules/product-detail/components/ProductDetail.tsx index e4555913..b036cc2d 100644 --- a/src-migrate/modules/product-detail/components/ProductDetail.tsx +++ b/src-migrate/modules/product-detail/components/ProductDetail.tsx @@ -1,46 +1,52 @@ -import style from '../styles/product-detail.module.css' - -import Link from 'next/link' -import { useRouter } from 'next/router' -import { useEffect } from 'react' - -import { Button } from '@chakra-ui/react' -import { MessageCircleIcon, Share2Icon } from 'lucide-react' -import { LazyLoadComponent } from 'react-lazy-load-image-component' -import { RWebShare } from 'react-web-share' - -import useDevice from '@/core/hooks/useDevice' -import { whatsappUrl } from '~/libs/whatsappUrl' -import ProductPromoSection from '~/modules/product-promo/components/Section' -import { IProductDetail } from '~/types/product' -import { useProductDetail } from '../stores/useProductDetail' -import AddToWishlist from './AddToWishlist' -import Breadcrumb from './Breadcrumb' -import ProductImage from './Image' -import Information from './Information' -import PriceAction from './PriceAction' -import SimilarBottom from './SimilarBottom' -import SimilarSide from './SimilarSide' -import VariantList from './VariantList' -import { getAuth } from '~/libs/auth' - -import { gtagProductDetail } from '@/core/utils/googleTag' +import style from '../styles/product-detail.module.css'; + +import Link from 'next/link'; +import { useRouter } from 'next/router'; +import { useEffect } from 'react'; + +import { Button } from '@chakra-ui/react'; +import { MessageCircleIcon, Share2Icon } from 'lucide-react'; +import { LazyLoadComponent } from 'react-lazy-load-image-component'; +import { RWebShare } from 'react-web-share'; + +import useDevice from '@/core/hooks/useDevice'; +import { getAuth } from '~/libs/auth'; +import { whatsappUrl } from '~/libs/whatsappUrl'; +import ProductPromoSection from '~/modules/product-promo/components/Section'; +import { IProductDetail } from '~/types/product'; +import { useProductDetail } from '../stores/useProductDetail'; +import AddToWishlist from './AddToWishlist'; +import Breadcrumb from './Breadcrumb'; +import ProductImage from './Image'; +import Information from './Information'; +import PriceAction from './PriceAction'; +import SimilarBottom from './SimilarBottom'; +import SimilarSide from './SimilarSide'; + +import { gtagProductDetail } from '@/core/utils/googleTag'; type Props = { - product: IProductDetail -} + product: IProductDetail; +}; -const SELF_HOST = process.env.NEXT_PUBLIC_SELF_HOST +const SELF_HOST = process.env.NEXT_PUBLIC_SELF_HOST; const ProductDetail = ({ product }: Props) => { - const { isDesktop, isMobile } = useDevice() - const router = useRouter() - const auth = getAuth() - const { setAskAdminUrl, askAdminUrl, activeVariantId, setIsApproval, isApproval } = useProductDetail() + const { isDesktop, isMobile } = useDevice(); + const router = useRouter(); + const auth = getAuth(); + const { + setAskAdminUrl, + askAdminUrl, + activeVariantId, + setIsApproval, + isApproval, + setSelectedVariant, + } = useProductDetail(); useEffect(() => { gtagProductDetail(product); - },[product]) + }, [product]); useEffect(() => { const createdAskUrl = whatsappUrl({ @@ -48,76 +54,43 @@ const ProductDetail = ({ product }: Props) => { payload: { manufacture: product.manufacture.name, productName: product.name, - url: process.env.NEXT_PUBLIC_SELF_HOST + router.asPath + url: process.env.NEXT_PUBLIC_SELF_HOST + router.asPath, }, - fallbackUrl: router.asPath - }) + fallbackUrl: router.asPath, + }); - setAskAdminUrl(createdAskUrl) - }, [router.asPath, product.manufacture.name, product.name, setAskAdminUrl]) + setAskAdminUrl(createdAskUrl); + }, [router.asPath, product.manufacture.name, product.name, setAskAdminUrl]); useEffect(() => { if (typeof auth === 'object') { setIsApproval(auth?.feature?.soApproval); } + setSelectedVariant(product?.variants[0]) }, []); return ( <> <div className='md:flex md:flex-wrap'> - <div className="w-full mb-4 md:mb-0 px-4 md:px-0"> + <div className='w-full mb-4 md:mb-0 px-4 md:px-0'> <Breadcrumb id={product.id} name={product.name} /> </div> <div className='md:w-9/12 md:flex md:flex-col md:pr-4 md:pt-6'> <div className='md:flex md:flex-wrap'> - <div className="md:w-4/12"> + <div className='md:w-4/12'> <ProductImage product={product} /> </div> <div className='md:w-8/12 px-4 md:pl-6'> <div className='h-6 md:h-0' /> - <h1 className={style['title']}> - {product.name} - </h1> + <h1 className={style['title']}>{product.name}</h1> - <div className='h-6 md:h-8' /> + <div className='h-3 md:h-0' /> <Information product={product} /> <div className='h-6' /> - - <div className="flex gap-x-5"> - <Button - as={Link} - href={askAdminUrl} - variant='link' - target='_blank' - colorScheme='gray' - leftIcon={<MessageCircleIcon size={18} />} - > - Ask Admin - </Button> - - <AddToWishlist productId={product.id} /> - - <RWebShare - data={{ - text: 'Check out this product', - title: `${product.name} - Indoteknik.com`, - url: SELF_HOST + router.asPath - }} - > - <Button - variant='link' - colorScheme='gray' - leftIcon={<Share2Icon size={18} />} - > - Share - </Button> - </RWebShare> - </div> - </div> </div> @@ -131,38 +104,72 @@ const ProductDetail = ({ product }: Props) => { <div className='h-4 md:h-10' /> {!!activeVariantId && !isApproval && <ProductPromoSection product={product} productId={activeVariantId} />} - <div className={style['section-card']}> + {/* <div className={style['section-card']}> <h2 className={style['heading']}> Variant ({product.variant_total}) </h2> <div className='h-4' /> <VariantList variants={product.variants} /> - </div> + </div> */} <div className='h-0 md:h-6' /> <div className={style['section-card']}> - <h2 className={style['heading']}> - Informasi Produk - </h2> + <h2 className={style['heading']}>Informasi Produk</h2> <div className='h-4' /> <div className={style['description']} - dangerouslySetInnerHTML={{ __html: !product.description || product.description == '<p><br></p>' ? 'Belum ada deskripsi' : product.description }} + dangerouslySetInnerHTML={{ + __html: + !product.description || product.description == '<p><br></p>' + ? 'Belum ada deskripsi' + : product.description, + }} /> </div> </div> </div> {isDesktop && ( - <div className="md:w-3/12"> + <div className='md:w-3/12'> <PriceAction product={product} /> + <div className='flex gap-x-5 items-center justify-center'> + <Button + as={Link} + href={askAdminUrl} + variant='link' + target='_blank' + colorScheme='gray' + leftIcon={<MessageCircleIcon size={18} />} + > + Ask Admin + </Button> + + <span>|</span> + + <AddToWishlist productId={product.id} /> + + <span>|</span> + + <RWebShare + data={{ + text: 'Check out this product', + title: `${product.name} - Indoteknik.com`, + url: SELF_HOST + router.asPath, + }} + > + <Button + variant='link' + colorScheme='gray' + leftIcon={<Share2Icon size={18} />} + > + Share + </Button> + </RWebShare> + </div> <div className='h-6' /> - - <div className={style['heading']}> - Produk Serupa - </div> + <div className={style['heading']}>Produk Serupa</div> <div className='h-4' /> @@ -171,9 +178,7 @@ const ProductDetail = ({ product }: Props) => { )} <div className='md:w-full pt-4 md:py-10 px-4 md:px-0'> - <div className={style['heading']}> - Kamu Mungkin Juga Suka - </div> + <div className={style['heading']}>Kamu Mungkin Juga Suka</div> <div className='h-6' /> @@ -185,7 +190,7 @@ const ProductDetail = ({ product }: Props) => { <div className='h-6 md:h-0' /> </div> </> - ) -} + ); +}; -export default ProductDetail
\ No newline at end of file +export default ProductDetail; diff --git a/src-migrate/modules/product-detail/stores/useProductDetail.ts b/src-migrate/modules/product-detail/stores/useProductDetail.ts index eb409930..dee6b342 100644 --- a/src-migrate/modules/product-detail/stores/useProductDetail.ts +++ b/src-migrate/modules/product-detail/stores/useProductDetail.ts @@ -7,6 +7,8 @@ type State = { quantityInput: string; askAdminUrl: string; isApproval : boolean; + selectedVariant : any; + sla : any; }; type Action = { @@ -14,6 +16,8 @@ type Action = { setQuantityInput: (value: string) => void; setAskAdminUrl: (url: string) => void; setIsApproval : (value : boolean) => void; + setSelectedVariant : (value : any) => void; + setSla : (value : any) => void; }; export const useProductDetail = create<State & Action>((set, get) => ({ @@ -22,6 +26,8 @@ export const useProductDetail = create<State & Action>((set, get) => ({ quantityInput: '1', askAdminUrl: '', isApproval : false, + selectedVariant: null, + sla : null, setActive: (variant) => { set({ activeVariantId: variant?.id, activePrice: variant?.price }); }, @@ -33,5 +39,11 @@ export const useProductDetail = create<State & Action>((set, get) => ({ }, setIsApproval : (value : boolean) => { set({ isApproval : value }) + }, + setSelectedVariant : (value : any) => { + set({ selectedVariant : value }) + }, + setSla : (value : any ) => { + set({ sla : value }) } })); diff --git a/src-migrate/modules/product-detail/styles/information.module.css b/src-migrate/modules/product-detail/styles/information.module.css index c9b29020..5aa64fe5 100644 --- a/src-migrate/modules/product-detail/styles/information.module.css +++ b/src-migrate/modules/product-detail/styles/information.module.css @@ -3,11 +3,11 @@ } .row { - @apply flex p-3 rounded; + @apply flex p-4 rounded-sm bg-gray-100; } .row:nth-child(odd) { - @apply bg-gray-100; + @apply bg-white; } .label { diff --git a/src-migrate/modules/product-detail/styles/price-action.module.css b/src-migrate/modules/product-detail/styles/price-action.module.css index 651de958..cea50bff 100644 --- a/src-migrate/modules/product-detail/styles/price-action.module.css +++ b/src-migrate/modules/product-detail/styles/price-action.module.css @@ -8,7 +8,10 @@ @apply flex gap-x-2.5; } .quantity-input { - @apply px-2 rounded text-center border border-gray-300 w-14 h-10 focus:outline-none; + @apply w-24 h-10 text-center border border-gray-300 rounded focus:outline-none; + /* Padding di kiri dan kanan untuk memberi ruang bagi tombol */ + padding-left: 2rem; + padding-right: 2rem; } .contact-us { diff --git a/src-migrate/modules/promo/components/FlashSaleNonDisplay.tsx b/src-migrate/modules/promo/components/FlashSaleNonDisplay.tsx new file mode 100644 index 00000000..5685b83a --- /dev/null +++ b/src-migrate/modules/promo/components/FlashSaleNonDisplay.tsx @@ -0,0 +1,17 @@ +import dynamic from 'next/dynamic'; +import React from 'react'; +import { FlashSaleSkeleton } from '@/lib/flashSale/skeleton/FlashSaleSkeleton'; +const FlashSaleNonDisplay = dynamic( + () => import('@/lib/flashSale/components/FlashSaleNonDisplay'), + { + loading: () => <FlashSaleSkeleton />, + } +); +const FlashSalePromo = () => { + return ( + <> + <FlashSaleNonDisplay /> + </> + ); +}; +export default FlashSalePromo; diff --git a/src-migrate/modules/promo/components/PromoList.tsx b/src-migrate/modules/promo/components/PromoList.tsx index d59d1867..9f808718 100644 --- a/src-migrate/modules/promo/components/PromoList.tsx +++ b/src-migrate/modules/promo/components/PromoList.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; -import { Button, Skeleton } from '@chakra-ui/react' -import clsxm from "~/libs/clsxm" +import { Button, Skeleton } from '@chakra-ui/react'; +import clsxm from '~/libs/clsxm'; import ProductPromoCard from '../../product-promo/components/Card'; import { fetchPromoItemsSolr } from '../../../../src/api/promoApi'; import { Swiper, SwiperSlide } from 'swiper/react'; @@ -8,7 +8,7 @@ import SwiperCore, { Navigation, Pagination } from 'swiper'; import useDevice from '@/core/hooks/useDevice'; import LogoSpinner from '../../../../src/core/components/elements/Spinner/LogoSpinner'; import usePromoStore from './promoStore'; -import Link from "next/link" +import Link from 'next/link'; import { IPromotion } from '~/types/promotion'; interface PromoListProps { selectedPromo: string; // Tipe selectedPromo ditetapkan sebagai string @@ -32,11 +32,11 @@ const PromoList: React.FC<PromoListProps> = ({ selectedPromo }) => { const swiperBanner = { modules: [Navigation], - className: 'h-[400px] w-full', + className: 'h-full w-full', slidesPerView: isMobile ? 1.1 : 3.25, spaceBetween: 10, - navigation:isMobile? true : false, - allowTouchMove:isMobile? false : true, + navigation: isMobile ? true : false, + allowTouchMove: isMobile ? false : true, }; useEffect(() => { @@ -56,7 +56,7 @@ const PromoList: React.FC<PromoListProps> = ({ selectedPromo }) => { const fetchPromotions = async () => { setIsLoading(true); try { - const items = await fetchPromoItemsSolr(`type_value_s:${slug}`, 0, 10); + const items = await fetchPromoItemsSolr(`type_value_s:${slug}`, 0, 10); setPromoItems(items); const promoDataPromises = items?.map(async (item) => { @@ -69,9 +69,11 @@ const PromoList: React.FC<PromoListProps> = ({ selectedPromo }) => { }); const promoDataArray = await Promise.all(promoDataPromises); - const mergedPromoData = promoDataArray?.reduce((accumulator, currentValue) => accumulator.concat(currentValue), []); + const mergedPromoData = promoDataArray?.reduce( + (accumulator, currentValue) => accumulator.concat(currentValue), + [] + ); setPromoData(mergedPromoData); - } catch (error) { console.error('Error fetching promo items:', error); } finally { @@ -92,44 +94,49 @@ const PromoList: React.FC<PromoListProps> = ({ selectedPromo }) => { <div className='flex justify-between items-center'> <h1 className='text-h-sm md:text-h-lg font-semibold py-4'>{title}</h1> <div> - <Link href={`/shop/promo/${slug}`} className='!text-red-500 font-semibold'> + <Link + href={`/shop/promo/${slug}`} + className='!text-red-500 font-semibold' + > Lihat Semua </Link> </div> </div> {isLoading ? ( - <div className="loading-spinner flex justify-center"> + <div className='loading-spinner flex justify-center'> <LogoSpinner width={48} height={48} /> </div> ) : ( <Skeleton - isLoaded={!isLoading} - className={clsxm( - "flex gap-x-4 overflow-x-auto px-4 md:px-0", { - "min-h-[340px]": promoData[0] && promoData?.length > 0 - })} - > - {isDesktop && ( - <Swiper {...swiperBanner}> - {promoData?.map((promotion: IPromotion) => ( - <SwiperSlide key={promotion.id}> - <div className="min-w-36 max-w-[400px] mb-[20px] sm:w-full md:w-full lg:w-full xl:w-full"> - <ProductPromoCard product={promoItems} promotion={promotion} /> - </div> - </SwiperSlide> - ))} - </Swiper> - )} - {isMobile && (promoData?.map((promotion: IPromotion) => ( - <div key={promotion.id} className="min-w-[400px] max-w-[400px]"> - <ProductPromoCard product={promoItems} promotion={promotion} /> - </div> - )))} - - </Skeleton> + isLoaded={!isLoading} + className={clsxm('flex gap-x-4 overflow-x-auto px-4 md:px-0', { + 'min-h-[340px]': promoData[0] && promoData?.length > 0, + })} + > + {isDesktop && ( + <Swiper {...swiperBanner}> + {promoData?.map((promotion: IPromotion) => ( + <SwiperSlide key={promotion.id}> + <div className='min-w-36 max-w-[400px] mb-[20px] sm:w-full md:w-full lg:w-full xl:w-full'> + <ProductPromoCard + product={promoItems} + promotion={promotion} + /> + </div> + </SwiperSlide> + ))} + </Swiper> + )} + {isMobile && + promoData?.map((promotion: IPromotion) => ( + <div key={promotion.id} className='min-w-[400px] max-w-[400px]'> + <ProductPromoCard product={promoItems} promotion={promotion} /> + </div> + ))} + </Skeleton> )} </div> ); }; -export default PromoList;
\ No newline at end of file +export default PromoList; diff --git a/src-migrate/modules/register/components/TermCondition.tsx b/src-migrate/modules/register/components/TermCondition.tsx index d54fe921..44275917 100644 --- a/src-migrate/modules/register/components/TermCondition.tsx +++ b/src-migrate/modules/register/components/TermCondition.tsx @@ -1,34 +1,48 @@ -import { Checkbox } from '@chakra-ui/react' -import React from 'react' -import { Modal } from '~/components/ui/modal' -import { useRegisterStore } from "../stores/useRegisterStore"; -import PageContent from '~/modules/page-content' +import { Checkbox } from '@chakra-ui/react'; +import React from 'react'; +import { Modal } from '~/components/ui/modal'; +import { useRegisterStore } from '../stores/useRegisterStore'; + +import dynamic from 'next/dynamic'; +const PageContent = dynamic( + () => import('@/lib/content/components/PageContent') +); const TermCondition = () => { - const { isOpenTNC, closeTNC, isCheckedTNC, toggleCheckTNC, openTNC } = useRegisterStore() + const { isOpenTNC, closeTNC, isCheckedTNC, toggleCheckTNC, openTNC } = + useRegisterStore(); return ( <> - <div className="mt-4 flex items-center gap-x-2"> - <Checkbox id='tnc' name='tnc' colorScheme='red' isChecked={isCheckedTNC} onChange={toggleCheckTNC} /> + <div className='mt-4 flex items-center gap-x-2'> + <Checkbox + id='tnc' + name='tnc' + colorScheme='red' + isChecked={isCheckedTNC} + onChange={toggleCheckTNC} + /> <div> - <label htmlFor="tnc" className="cursor-pointer">Dengan ini saya menyetujui</label> - {' '} + <label htmlFor='tnc' className='cursor-pointer'> + Dengan ini saya menyetujui + </label>{' '} <span - className="font-medium text-danger-500 cursor-pointer" + className='font-medium text-danger-500 cursor-pointer' onClick={openTNC} > syarat dan ketentuan </span> - <label htmlFor="tnc" className="ml-2 cursor-pointer">yang berlaku</label> + <label htmlFor='tnc' className='ml-2 cursor-pointer'> + yang berlaku + </label> </div> </div> - <Modal active={isOpenTNC} close={closeTNC} > - <PageContent path='/register#tnd' /> + <Modal active={isOpenTNC} close={closeTNC}> + <PageContent path='/registerTnd' /> </Modal> </> - ) -} + ); +}; -export default TermCondition
\ No newline at end of file +export default TermCondition; diff --git a/src-migrate/pages/shop/cart/index.tsx b/src-migrate/pages/shop/cart/index.tsx index c5386c91..24baa933 100644 --- a/src-migrate/pages/shop/cart/index.tsx +++ b/src-migrate/pages/shop/cart/index.tsx @@ -84,19 +84,19 @@ const CartPage = () => { const hasSelectedPromo = useMemo(() => { if (!cart) return false; - return cart.products.some( + return cart?.products?.some( (item) => item.cart_type === 'promotion' && item.selected ); }, [cart]); const hasSelected = useMemo(() => { if (!cart) return false; - return cart.products.some((item) => item.selected); + return cart?.products?.some((item) => item.selected); }, [cart]); const hasSelectNoPrice = useMemo(() => { if (!cart) return false; - return cart.products.some( + return cart?.products?.some( (item) => item.selected && item.price.price_discount === 0 ); }, [cart]); @@ -230,7 +230,7 @@ const CartPage = () => { </div> <div className={style['items']}> - {cart?.products.map((item) => ( + {cart?.products?.map((item) => ( <CartItemModule key={item.id} item={item} /> ))} diff --git a/src-migrate/pages/shop/promo/index.tsx b/src-migrate/pages/shop/promo/index.tsx index febe31a4..689c2537 100644 --- a/src-migrate/pages/shop/promo/index.tsx +++ b/src-migrate/pages/shop/promo/index.tsx @@ -1,13 +1,14 @@ -import dynamic from 'next/dynamic' -import React, { useState } from 'react' -import { LazyLoadComponent } from 'react-lazy-load-image-component' -import Hero from '~/modules/promo/components/Hero' -import PromotionProgram from '~/modules/promo/components/PromotinProgram' -import Voucher from '~/modules/promo/components/Voucher' -import FlashSale from '../../../modules/promo/components/FlashSale' -const PromoList = dynamic(() => import('../../../modules/promo/components/PromoList')); - - +import dynamic from 'next/dynamic'; +import React, { useState } from 'react'; +import { LazyLoadComponent } from 'react-lazy-load-image-component'; +import Hero from '~/modules/promo/components/Hero'; +import PromotionProgram from '~/modules/promo/components/PromotinProgram'; +import Voucher from '~/modules/promo/components/Voucher'; +import FlashSale from '../../../modules/promo/components/FlashSale'; +import FlashSaleNonDisplay from '../../../modules/promo/components/FlashSaleNonDisplay'; +const PromoList = dynamic( + () => import('../../../modules/promo/components/PromoList') +); const PromoPage = () => { const [selectedPromo, setSelectedPromo] = useState('Bundling'); @@ -17,22 +18,26 @@ const PromoPage = () => { <Hero /> </LazyLoadComponent> <LazyLoadComponent> - <PromotionProgram - selectedPromo={selectedPromo} - onSelectPromo={setSelectedPromo} - /> + <PromotionProgram + selectedPromo={selectedPromo} + onSelectPromo={setSelectedPromo} + /> <PromoList selectedPromo={selectedPromo} /> </LazyLoadComponent> - + <LazyLoadComponent> <FlashSale /> </LazyLoadComponent> <h1 className='h-1'></h1> <LazyLoadComponent> + <FlashSaleNonDisplay /> + </LazyLoadComponent> + <h1 className='h-1'></h1> + <LazyLoadComponent> <Voucher /> </LazyLoadComponent> </> - ) -} + ); +}; -export default PromoPage
\ No newline at end of file +export default PromoPage; diff --git a/src-migrate/types/cart.ts b/src-migrate/types/cart.ts index a3115103..05fdcadb 100644 --- a/src-migrate/types/cart.ts +++ b/src-migrate/types/cart.ts @@ -34,6 +34,7 @@ export type CartItem = { stock: number; is_in_bu: boolean; on_hand_qty: number; + available_quantity: number; weight: number; attributes: string[]; parent: { diff --git a/src-migrate/types/product.ts b/src-migrate/types/product.ts index 31ea0ce1..85ea702a 100644 --- a/src-migrate/types/product.ts +++ b/src-migrate/types/product.ts @@ -3,6 +3,7 @@ import { IProductVariantDetail } from './productVariant'; export interface IProduct { id: number; image: string; + image_mobile: string; code: string; display_name: string; name: string; @@ -31,7 +32,9 @@ export interface IProduct { manufacture: { id: number; name: string; + logo: string; }; + voucher_pasti_hemat : any; } export interface IProductDetail extends IProduct { diff --git a/src-migrate/types/productVariant.ts b/src-migrate/types/productVariant.ts index 861b216a..5144e7c1 100644 --- a/src-migrate/types/productVariant.ts +++ b/src-migrate/types/productVariant.ts @@ -4,6 +4,7 @@ export interface IProductVariantDetail { code: string; name: string; weight: number; + is_in_bu: boolean; is_flashsale: { remaining_time: number; is_flashsale: boolean; diff --git a/src/components/ui/HeroBanner.jsx b/src/components/ui/HeroBanner.jsx index 64838b85..2eea5915 100644 --- a/src/components/ui/HeroBanner.jsx +++ b/src/components/ui/HeroBanner.jsx @@ -6,7 +6,7 @@ import 'swiper/css/pagination'; import { Swiper, SwiperSlide } from 'swiper/react'; import Image from 'next/image'; -import { useMemo } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { useQuery } from 'react-query'; import { bannerApi } from '@/api/bannerApi'; @@ -27,7 +27,20 @@ const swiperBanner = { }; const HeroBanner = () => { - const heroBanner = useQuery('heroBanner', bannerApi({ type: 'index-a-1' })); + // const heroBanner = useQuery('heroBanner', bannerApi({ type: 'index-a-1' })); + const [data, setData] = useState(null); + useEffect(() => { + const fetchData = async () => { + const res = await fetch(`/api/hero-banner?type=index-a-1`); + const { data } = await res.json(); + if (data) { + setData(data); + } + }; + + fetchData(); + }, []); + const heroBanner = data; const swiperBannerMobile = { ...swiperBanner, @@ -44,9 +57,9 @@ const HeroBanner = () => { }; const BannerComponent = useMemo(() => { - if (!heroBanner.data) return null; + if (!heroBanner) return null; - return heroBanner.data.map((banner, index) => ( + return heroBanner.map((banner, index) => ( <SwiperSlide key={index}> <Link href={banner.url} className='w-full h-auto'> <Image @@ -56,22 +69,22 @@ const HeroBanner = () => { width={1152} height={768} className='w-full h-auto' - priority={index === 0} - loading={index === 0 ? 'eager' : 'lazy'} - placeholder="blur" - blurDataURL="/images/indoteknik-placeholder.png" - sizes="(max-width: 768px) 100vw, 50vw" + priority={index === 0} + loading={index === 0 ? 'eager' : 'lazy'} + placeholder='blur' + blurDataURL='/images/indoteknik-placeholder.png' + sizes='(max-width: 768px) 100vw, 50vw' /> </Link> </SwiperSlide> )); - }, [heroBanner.data]); + }, [heroBanner]); return ( <> <MobileView> <SmoothRender - isLoaded={heroBanner.data?.length > 0} + isLoaded={heroBanner?.length > 0} height='68vw' duration='750ms' delay='100ms' @@ -81,7 +94,7 @@ const HeroBanner = () => { </MobileView> <DesktopView> - {heroBanner.data?.length > 0 && ( + {heroBanner?.length > 0 && ( <Swiper {...swiperBannerDesktop}>{BannerComponent}</Swiper> )} </DesktopView> diff --git a/src/components/ui/HeroBannerSecondary.jsx b/src/components/ui/HeroBannerSecondary.jsx index a7b32a4a..6074c9a6 100644 --- a/src/components/ui/HeroBannerSecondary.jsx +++ b/src/components/ui/HeroBannerSecondary.jsx @@ -1,39 +1,58 @@ -import Link from '@/core/components/elements/Link/Link' -import { getRandomInt } from '@/utils/getRandomInt' -import Image from 'next/image' -import { useMemo } from 'react' -import { useQuery } from 'react-query' -import { HeroBannerSkeleton } from '../skeleton/BannerSkeleton' -import { bannerApi } from '@/api/bannerApi' +import Link from '@/core/components/elements/Link/Link'; +import { getRandomInt } from '@/utils/getRandomInt'; +import Image from 'next/image'; +import { useMemo, useEffect, useState } from 'react'; +import { useQuery } from 'react-query'; +import { HeroBannerSkeleton } from '../skeleton/BannerSkeleton'; +import { bannerApi } from '@/api/bannerApi'; const HeroBannerSecondary = () => { - const heroBannerSecondary = useQuery('heroBannerSecondary', bannerApi({ type: 'index-a-2' })) + const [heroBannerSecondary, setHeroBannerSecondary] = useState([]); + const [isLoading, setIsLoading] = useState(false); + // const heroBannerSecondary = useQuery( + // 'heroBannerSecondary', + // bannerApi({ type: 'index-a-2' }) + // ); - const randomIndex = useMemo(() => { - if (!heroBannerSecondary.data) return null - const length = heroBannerSecondary.data?.length - return getRandomInt(length) - }, [heroBannerSecondary.data]) + useEffect(() => { + const fetchData = async () => { + setIsLoading(true); + const res = await fetch(`/api/hero-banner?type=index-a-2`); + const { data } = await res.json(); + if (data) { + setHeroBannerSecondary(data); + } + setIsLoading(false); + }; + + fetchData(); + }, []); - if (heroBannerSecondary.isLoading) return <HeroBannerSkeleton /> + const randomIndex = useMemo(() => { + if (!heroBannerSecondary) return null; + const length = heroBannerSecondary?.length; + return getRandomInt(length); + }, [heroBannerSecondary]); + if (isLoading) return <HeroBannerSkeleton />; return ( - heroBannerSecondary.data && randomIndex !== null && ( - <Link href={heroBannerSecondary.data[randomIndex].url} className="h-full"> + heroBannerSecondary && + randomIndex !== null && ( + <Link href={heroBannerSecondary[randomIndex]?.url} className='h-full'> <Image - src={heroBannerSecondary.data[randomIndex].image} + src={heroBannerSecondary[randomIndex]?.image} width={512} height={1024} - alt={heroBannerSecondary.data[randomIndex].name} - className="object-cover object-center h-full" - loading="lazy" - placeholder="blur" - blurDataURL="/images/indoteknik-placeholder.png" - sizes="(max-width: 768px) 100vw, 50vw" + alt={heroBannerSecondary[randomIndex]?.name} + className='object-cover object-center h-full' + loading='lazy' + placeholder='blur' + blurDataURL='/images/indoteknik-placeholder.png' + sizes='(max-width: 768px) 100vw, 50vw' /> </Link> ) ); -} +}; -export default HeroBannerSecondary +export default HeroBannerSecondary; diff --git a/src/core/components/elements/Footer/BasicFooter.jsx b/src/core/components/elements/Footer/BasicFooter.jsx index 4688b15b..05dc4d8c 100644 --- a/src/core/components/elements/Footer/BasicFooter.jsx +++ b/src/core/components/elements/Footer/BasicFooter.jsx @@ -264,7 +264,7 @@ const InformationCenter = () => ( <li className='text-gray_r-12/80 flex items-center'> <PhoneArrowUpRightIcon className='w-[18px] mr-2' /> <a href='tel:02129338828' target='_blank' rel='noreferrer'> - (021) 2933-8828 + (021) 29338828 </a> </li> <li className='text-gray_r-12/80 flex items-center'> diff --git a/src/core/components/elements/Footer/SimpleFooter.jsx b/src/core/components/elements/Footer/SimpleFooter.jsx index 371b1652..1f5e13e7 100644 --- a/src/core/components/elements/Footer/SimpleFooter.jsx +++ b/src/core/components/elements/Footer/SimpleFooter.jsx @@ -13,7 +13,7 @@ const SimpleFooter = () => ( <ul className='flex flex-col gap-y-2'> <li className='text-gray_r-12/80 flex items-center'> <PhoneArrowUpRightIcon className='w-[18px] mr-2' /> - <a href='tel:02129338828'>(021) 2933-8828 / 29</a> + <a href='tel:02129338828'>(021) 29338828</a> </li> <li className='text-gray_r-12/80 flex items-center'> <EnvelopeIcon className='w-[18px] mr-2' /> diff --git a/src/core/components/elements/Navbar/NavbarDesktop.jsx b/src/core/components/elements/Navbar/NavbarDesktop.jsx index eebfbcd5..fa3df5bf 100644 --- a/src/core/components/elements/Navbar/NavbarDesktop.jsx +++ b/src/core/components/elements/Navbar/NavbarDesktop.jsx @@ -5,34 +5,28 @@ import { createSlug } from '@/core/utils/slug'; import whatsappUrl from '@/core/utils/whatsappUrl'; import IndoteknikLogo from '@/images/logo.png'; import Cardheader from '@/lib/cart/components/Cartheader'; -import Quotationheader from '../../../../../src/lib/quotation/components/Quotationheader.jsx'; import Category from '@/lib/category/components/Category'; -import { useProductCartContext } from '@/contexts/ProductCartContext'; +import useTransactions from '@/lib/transaction/hooks/useTransactions'; +import { + Menu, + MenuButton, + MenuItem, + MenuList, + useDisclosure +} from '@chakra-ui/react'; import { ChevronDownIcon, - DocumentCheckIcon, - HeartIcon, - ArrowUpRightIcon, + HeartIcon } from '@heroicons/react/24/outline'; import dynamic from 'next/dynamic'; -import Image from 'next/image'; +import { default as Image, default as NextImage } from 'next/image'; import { useRouter } from 'next/router'; import { useCallback, useEffect, useState } from 'react'; +import { useCartStore } from '~/modules/cart/stores/useCartStore'; +import Quotationheader from '../../../../../src/lib/quotation/components/Quotationheader.jsx'; import DesktopView from '../../views/DesktopView'; import Link from '../Link/Link'; import NavbarUserDropdown from './NavbarUserDropdown'; -import NextImage from 'next/image'; -import { - Button, - Menu, - MenuButton, - MenuItem, - MenuList, - useDisclosure, -} from '@chakra-ui/react'; -import style from './style/NavbarDesktop.module.css'; -import useTransactions from '@/lib/transaction/hooks/useTransactions'; -import { useCartStore } from '~/modules/cart/stores/useCartStore'; const Search = dynamic(() => import('./Search'), { ssr: false }); const TopBanner = dynamic(() => import('./TopBanner'), { ssr: false }); @@ -397,7 +391,7 @@ const SocialMedias = () => ( > <NextImage src='/images/socials/youtube.webp' - alt='Youtube - Indoteknik.com' + // alt='Youtube - Indoteknik.com' width={24} height={24} /> @@ -409,7 +403,7 @@ const SocialMedias = () => ( > <NextImage src='/images/socials/tiktok.png' - alt='TikTok - Indoteknik.com' + // alt='TikTok - Indoteknik.com' width={24} height={24} /> @@ -429,7 +423,7 @@ const SocialMedias = () => ( > <NextImage src='/images/socials/Facebook.png' - alt='Facebook - Indoteknik.com' + // alt='Facebook - Indoteknik.com' width={24} height={24} /> @@ -441,7 +435,7 @@ const SocialMedias = () => ( > <NextImage src='/images/socials/Instagram.png' - alt='Instagram - Indoteknik.com' + // alt='Instagram - Indoteknik.com' width={24} height={24} /> @@ -453,7 +447,7 @@ const SocialMedias = () => ( > <NextImage src='/images/socials/Linkedin.png' - alt='Linkedin - Indoteknik.com' + // alt='Linkedin - Indoteknik.com' width={24} height={24} /> @@ -465,7 +459,7 @@ const SocialMedias = () => ( > <NextImage src='/images/socials/g_maps.png' - alt='Maps - Indoteknik.com' + // alt='Maps - Indoteknik.com' width={24} height={24} /> diff --git a/src/core/components/elements/Navbar/TopBanner.jsx b/src/core/components/elements/Navbar/TopBanner.jsx index f438ae67..709495ce 100644 --- a/src/core/components/elements/Navbar/TopBanner.jsx +++ b/src/core/components/elements/Navbar/TopBanner.jsx @@ -1,22 +1,37 @@ import Image from 'next/image'; -import { useQuery } from 'react-query';import useDevice from '@/core/hooks/useDevice' +import { useQuery } from 'react-query'; +import useDevice from '@/core/hooks/useDevice'; import odooApi from '@/core/api/odooApi'; import SmoothRender from '~/components/ui/smooth-render'; import Link from '../Link/Link'; import { background } from '@chakra-ui/react'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; const TopBanner = ({ onLoad = () => {} }) => { - const { isDesktop, isMobile } = useDevice() - const topBanner = useQuery({ - queryKey: 'topBanner', - queryFn: async () => await odooApi('GET', '/api/v1/banner?type=top-banner'), - refetchOnWindowFocus: false, - }); + const [topBanner, setTopBanner] = useState([]); + const { isDesktop, isMobile } = useDevice(); + + useEffect(() => { + const fetchData = async () => { + const res = await fetch(`/api/hero-banner?type=top-banner`); + const { data } = await res.json(); + if (data) { + setTopBanner(data); + } + }; + + fetchData(); + }, []); + + // const topBanner = useQuery({ + // queryKey: 'topBanner', + // queryFn: async () => await odooApi('GET', '/api/v1/banner?type=top-banner'), + // refetchOnWindowFocus: false, + // }); // const backgroundColor = topBanner.data?.[0]?.backgroundColor || 'transparent'; - const hasData = topBanner.data?.length > 0; - const data = topBanner.data?.[0] || null; + const hasData = topBanner?.length > 0; + const data = topBanner?.[0] || null; useEffect(() => { if (hasData) { @@ -31,17 +46,15 @@ const TopBanner = ({ onLoad = () => {} }) => { duration='700ms' delay='300ms' className='h-auto' - > + > <Link - href={data?.url} - className="block bg-cover bg-center h-3 md:h-6 lg:h-[36px]" - style={{ - backgroundImage: `url('${data?.image}')`, - }} - > - </Link> - - </SmoothRender> + href={data?.url} + className='block bg-cover bg-center h-3 md:h-6 lg:h-[36px]' + style={{ + backgroundImage: `url('${data?.image}')`, + }} + ></Link> + </SmoothRender> ); }; diff --git a/src/core/components/layouts/BasicLayout.jsx b/src/core/components/layouts/BasicLayout.jsx index c4674344..1b62bf05 100644 --- a/src/core/components/layouts/BasicLayout.jsx +++ b/src/core/components/layouts/BasicLayout.jsx @@ -8,6 +8,7 @@ import odooApi from '@/core/api/odooApi'; import whatsappUrl from '@/core/utils/whatsappUrl'; import Navbar from '../elements/Navbar/Navbar'; import styles from './BasicLayout.module.css'; // Import modul CSS +import useDevice from '@/core/hooks/useDevice'; const AnimationLayout = dynamic(() => import('./AnimationLayout'), { ssr: false, @@ -23,6 +24,9 @@ const BasicLayout = ({ children }) => { const [highlight, setHighlight] = useState(false); const [buttonPosition, setButtonPosition] = useState(null); const [wobble, setWobble] = useState(false); + const [isProductPage, setIsProductPage] = useState(false); + + const { isDesktop, isMobile } = useDevice(); const router = useRouter(); const buttonRef = useRef(null); @@ -43,13 +47,16 @@ const BasicLayout = ({ children }) => { setUrlPath(router.asPath); } + if (router.pathname.includes('/shop/product/')) { + setIsProductPage(true); + } }, [product, router]); useEffect(() => { const handleMouseOut = (event) => { const rect = buttonRef.current.getBoundingClientRect(); if (event.clientY <= 0) { - setButtonPosition(rect) + setButtonPosition(rect); setHighlight(true); } else { setHighlight(false); @@ -92,13 +99,15 @@ const BasicLayout = ({ children }) => { return ( <> - {highlight && buttonPosition && ( + {highlight && buttonPosition && ( <div className={styles['overlay-highlight']} style={{ - '--button-x': `${buttonPosition.x + buttonPosition.width / 2}px`, + '--button-x': `${buttonPosition.x + buttonPosition.width / 2}px`, '--button-y': `${buttonPosition.y + buttonPosition.height / 2}px`, - '--button-radius': `${Math.max(buttonPosition.width, buttonPosition.height) / 2}px` + '--button-radius': `${ + Math.max(buttonPosition.width, buttonPosition.height) / 2 + }px`, }} onAnimationEnd={() => setHighlight(false)} /> @@ -106,11 +115,25 @@ const BasicLayout = ({ children }) => { <Navbar /> <AnimationLayout> {children} - <div className='fixed bottom-4 right-4 sm:bottom-14 sm:right-10 z-50'> - <div className='flex flex-row items-center'> - <a href={whatsappUrl(templateWA, payloadWA, urlPath)} className='flex flex-row items-center' rel='noopener noreferrer' target='_blank'> - <span className={`text-green-300 text-lg font-bold mr-4 ${wobble ? 'animate-wobble' : ''}`} onAnimationEnd={() => setWobble(false)}> - Whatsapp + <div + className={`fixed ${ + isMobile && isProductPage ? 'bottom-40' : 'bottom-16' + } right-4 sm:bottom-14 sm:right-10 z-50`} + > + <div className='flex flex-row items-center'> + <a + href={whatsappUrl(templateWA, payloadWA, urlPath)} + className='flex flex-row items-center' + rel='noopener noreferrer' + target='_blank' + > + <span + className={`text-green-300 text-lg font-bold mr-4 ${ + wobble ? 'animate-wobble' : '' + }`} + onAnimationEnd={() => setWobble(false)} + > + {isDesktop && 'Whatsapp'} </span> </a> <a diff --git a/src/core/utils/whatsappUrl.js b/src/core/utils/whatsappUrl.js index 7a129aa6..c840e105 100644 --- a/src/core/utils/whatsappUrl.js +++ b/src/core/utils/whatsappUrl.js @@ -2,28 +2,31 @@ import { getAuth } from "./auth" const whatsappUrl = (template = 'default', payload, urlPath = null) => { let user = getAuth() - if(!user){ - if(urlPath) return `/login?next=${urlPath}` - if(!urlPath) return '/login' - } + // if(!user){ + // if(urlPath) return `/login?next=${urlPath}` + // if(!urlPath) return '/login' + // } let parentName = user.parentName || '-' let url = 'https://wa.me/6281717181922' let text = 'Hallo Indoteknik.com,' + if(user){ + text += `Saya ${user.name}, Saya dari ${parentName}` + } switch (template) { case 'product': - text += ` Saya ${user.name} , Saya dari ${parentName} Saya mencari barang dibawah ini\n\n: Brand = ${payload?.manufacture}\n\n Item Name = ${payload?.name}\n\nLink : ${payload?.url}` + text += ` Saya mencari barang dibawah ini\n\n: Brand = ${payload?.manufacture}\n\n Item Name = ${payload?.name}\n\nLink : ${payload?.url}` break case 'productWeight': - text += ` Saya ${user.name} , Saya dari ${parentName} Saya mencari barang dibawah ini\n\n: Brand = ${payload?.manufacture}\n\n Item Name = ${payload?.name}\n\nLink : ${payload?.url}` + text += ` Saya mencari barang dibawah ini\n\n: Brand = ${payload?.manufacture}\n\n Item Name = ${payload?.name}\n\nLink : ${payload?.url}` break case 'productSearch': - text += `Saya lagi cari-cari produk ${payload?.name}, bisa bantu saya cari produknya?` + text += ` Saya lagi cari-cari produk ${payload?.name}, bisa bantu saya cari produknya?` break case null: - text += `Saya ${user.name}, Saya dari ${parentName} Bisa tolong bantu kebutuhan saya?` + text += ` Bisa tolong bantu kebutuhan saya?` break; default: - text += `Saya ${user.name}, Saya dari ${parentName} Bisa tolong bantu kebutuhan saya?` + text += ` Bisa tolong bantu kebutuhan saya?` break } if (text) url += `?text=${encodeURI(text)}` diff --git a/src/lib/address/api/cityApi.js b/src/lib/address/api/cityApi.js index 7873435b..0b0201e6 100644 --- a/src/lib/address/api/cityApi.js +++ b/src/lib/address/api/cityApi.js @@ -1,7 +1,7 @@ import odooApi from '@/core/api/odooApi' -const cityApi = async () => { - const dataCities = await odooApi('GET', '/api/v1/city') +const cityApi = async ({stateId}) => { + const dataCities = await odooApi('GET', '/api/v1/city?state_id='+stateId) return dataCities } diff --git a/src/lib/address/api/stateApi.js b/src/lib/address/api/stateApi.js new file mode 100644 index 00000000..cea49e7e --- /dev/null +++ b/src/lib/address/api/stateApi.js @@ -0,0 +1,8 @@ +import odooApi from '@/core/api/odooApi' + +const stateApi = async () => { + const dataState = await odooApi('GET', '/api/v1/state') + return dataState +} + +export default stateApi
\ No newline at end of file diff --git a/src/lib/address/components/CreateAddress.jsx b/src/lib/address/components/CreateAddress.jsx index e315affe..9d70e8fc 100644 --- a/src/lib/address/components/CreateAddress.jsx +++ b/src/lib/address/components/CreateAddress.jsx @@ -12,6 +12,7 @@ import { toast } from 'react-hot-toast'; import { yupResolver } from '@hookform/resolvers/yup'; import Menu from '@/lib/auth/components/Menu'; import useAddresses from '../hooks/useAddresses'; +import stateApi from '../api/stateApi'; const CreateAddress = () => { const auth = useAuth(); @@ -28,23 +29,40 @@ const CreateAddress = () => { defaultValues, }); const { addresses = [] } = useAddresses(); // Ensure addresses is an array + const [states, setState] = useState([]); const [cities, setCities] = useState([]); const [districts, setDistricts] = useState([]); const [subDistricts, setSubDistricts] = useState([]); const [filteredTypes, setFilteredTypes] = useState(types); // State to manage filtered types useEffect(() => { - const loadCities = async () => { - let dataCities = await cityApi(); - dataCities = dataCities.map((city) => ({ - value: city.id, - label: city.name, + const loadState = async () => { + let dataState = await stateApi(); + dataState = dataState.map((state) => ({ + value: state.id, + label: state.name, })); - setCities(dataCities); + setState(dataState); }; - loadCities(); + loadState(); }, []); + const watchState = watch('state'); + useEffect(() => { + setValue('city', ''); + if (watchState) { + const loadCities = async () => { + let dataCities = await cityApi({stateId: watchState}); + dataCities = dataCities.map((city) => ({ + value: city.id, + label: city.name, + })); + setCities(dataCities); + }; + loadCities(); + } + }, [watchState, setValue]); + useEffect(() => { if (addresses) { let hasContactAddress = false; @@ -100,6 +118,7 @@ const CreateAddress = () => { const onSubmitHandler = async (values) => { const data = { ...values, + state_id: values.state, city_id: values.city, district_id: values.district, sub_district_id: values.subDistrict, @@ -205,12 +224,26 @@ const CreateAddress = () => { </div> <div> + <label className='form-label mb-2'>Provinsi</label> + <Controller + name='state' + control={control} + render={(props) => ( + <HookFormSelect {...props} options={states} /> + )} + /> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.state?.message} + </div> + </div> + + <div> <label className='form-label mb-2'>Kota</label> <Controller name='city' control={control} render={(props) => ( - <HookFormSelect {...props} options={cities} /> + <HookFormSelect {...props} options={cities} disabled={!watchState}/> )} /> <div className='text-caption-2 text-danger-500 mt-1'> @@ -270,6 +303,7 @@ const validationSchema = Yup.object().shape({ mobile: Yup.string().required('Harus di-isi'), street: Yup.string().required('Harus di-isi'), zip: Yup.string().required('Harus di-isi'), + state: Yup.string().required('Harus di-pilih'), city: Yup.string().required('Harus di-pilih'), district: Yup.string().required('Harus di-pilih'), }); @@ -280,6 +314,7 @@ const defaultValues = { email: '', mobile: '', street: '', + state: '', city: '', district: '', subDistrict: '', diff --git a/src/lib/address/components/EditAddress.jsx b/src/lib/address/components/EditAddress.jsx index 182c8a31..23cf72a9 100644 --- a/src/lib/address/components/EditAddress.jsx +++ b/src/lib/address/components/EditAddress.jsx @@ -13,6 +13,7 @@ import { toast } from 'react-hot-toast'; import Menu from '@/lib/auth/components/Menu'; import useAuth from '@/core/hooks/useAuth'; import odooApi from '@/core/api/odooApi'; +import stateApi from '../api/stateApi'; const EditAddress = ({ id, defaultValues }) => { const auth = useAuth(); @@ -29,9 +30,11 @@ const EditAddress = ({ id, defaultValues }) => { resolver: yupResolver(validationSchema), defaultValues, }); + + const [states, setStates] = useState([]); const [cities, setCities] = useState([]); const [districts, setDistricts] = useState([]); - const [subDistricts, setSubDistricts] = useState([]); + const [subDistricts, setSubDistricts] = useState([]); useEffect(() => { const loadProfile = async () => { @@ -48,16 +51,38 @@ const EditAddress = ({ id, defaultValues }) => { }, [auth?.parentId]); useEffect(() => { - const loadCities = async () => { - let dataCities = await cityApi(); - dataCities = dataCities.map((city) => ({ - value: city.id, - label: city.name, + const loadStates = async () => { + let dataStates = await stateApi(); + dataStates = dataStates.map((state) => ({ + value: state.id, + label: state.name, })); - setCities(dataCities); + setStates(dataStates); }; - loadCities(); - }, []); + loadStates(); + },[]) + + const watchState = watch('state'); + useEffect(() => { + setValue('city', ''); + if(watchState) { + const loadCities = async () => { + let dataCities = await cityApi({ stateId: watchState }); + dataCities = dataCities.map((city) => ({ + value: city.id, + label: city.name, + })); + setCities(dataCities); + let oldCity = getValues('oldCity'); + if (oldCity) { + setValue('city', oldCity); + setValue('oldCity', ''); + } + }; + loadCities(); + } + + }, [watchState, setValue, getValues]); const watchCity = watch('city'); useEffect(() => { @@ -107,6 +132,7 @@ const EditAddress = ({ id, defaultValues }) => { const data = { ...values, phone: values.mobile, + state_id: values.state, city_id: values.city, district_id: values.district, sub_district_id: values.subDistrict, @@ -242,12 +268,26 @@ const EditAddress = ({ id, defaultValues }) => { </div> <div> + <label className='form-label mb-2'>Provinsi</label> + <Controller + name='state' + control={control} + render={(props) => ( + <HookFormSelect {...props} options={states} /> + )} + /> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.state?.message} + </div> + </div> + + <div> <label className='form-label mb-2'>Kota</label> <Controller name='city' control={control} render={(props) => ( - <HookFormSelect {...props} options={cities} /> + <HookFormSelect {...props} options={cities} disabled={!watchState} /> )} /> <div className='text-caption-2 text-danger-500 mt-1'> @@ -308,6 +348,7 @@ const validationSchema = Yup.object().shape({ mobile: Yup.string().required('Harus di-isi'), street: Yup.string().required('Harus di-isi'), zip: Yup.string().required('Harus di-isi'), + state : Yup.string().required('Harus di-pilih'), city: Yup.string().required('Harus di-pilih'), district: Yup.string().required('Harus di-pilih'), }); diff --git a/src/lib/cart/components/Cartheader.jsx b/src/lib/cart/components/Cartheader.jsx index ddb77c1f..1c30bb13 100644 --- a/src/lib/cart/components/Cartheader.jsx +++ b/src/lib/cart/components/Cartheader.jsx @@ -1,105 +1,115 @@ -import { useCallback, useEffect, useMemo, useState } from 'react' -import { getCartApi } from '../api/CartApi' -import currencyFormat from '@/core/utils/currencyFormat' -import { createSlug } from '@/core/utils/slug' -import useAuth from '@/core/hooks/useAuth' -import { useRouter } from 'next/router' -import odooApi from '@/core/api/odooApi' -import { useProductCartContext } from '@/contexts/ProductCartContext' -import Image from '@/core/components/elements/Image/Image' -import whatsappUrl from '@/core/utils/whatsappUrl' -import { AnimatePresence, motion } from 'framer-motion' -import style from '../../../../src-migrate/modules/cart/styles/item-promo.module.css' -const { ShoppingCartIcon, PhotoIcon } = require('@heroicons/react/24/outline') -const { default: Link } = require('next/link') +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { getCartApi } from '../api/CartApi'; +import currencyFormat from '@/core/utils/currencyFormat'; +import { createSlug } from '@/core/utils/slug'; +import useAuth from '@/core/hooks/useAuth'; +import { useRouter } from 'next/router'; +import odooApi from '@/core/api/odooApi'; +import { useProductCartContext } from '@/contexts/ProductCartContext'; +import Image from '@/core/components/elements/Image/Image'; +import whatsappUrl from '@/core/utils/whatsappUrl'; +import { AnimatePresence, motion } from 'framer-motion'; +import style from '../../../../src-migrate/modules/cart/styles/item-promo.module.css'; +const { ShoppingCartIcon, PhotoIcon } = require('@heroicons/react/24/outline'); +const { default: Link } = require('next/link'); const Cardheader = (cartCount) => { - - const router = useRouter() - const [subTotal, setSubTotal] = useState(null) - const [buttonLoading, SetButtonTerapkan] = useState(false) - const itemLoading = [1, 2, 3] - const auth = useAuth() - const [countCart, setCountCart] = useState(null) - const { productCart, setRefreshCart, setProductCart, refreshCart, isLoading, setIsloading } = - useProductCartContext() + const router = useRouter(); + const [subTotal, setSubTotal] = useState(null); + const [buttonLoading, SetButtonTerapkan] = useState(false); + const itemLoading = [1, 2, 3]; + const auth = useAuth(); + const [countCart, setCountCart] = useState(null); + const { + productCart, + setRefreshCart, + setProductCart, + refreshCart, + isLoading, + setIsloading, + } = useProductCartContext(); - const [isHovered, setIsHovered] = useState(false) - const [isTop, setIsTop] = useState(true) + const [isHovered, setIsHovered] = useState(false); + const [isTop, setIsTop] = useState(true); const products = useMemo(() => { - return productCart?.products || [] - }, [productCart]) + return productCart?.products || []; + }, [productCart]); const handleMouseEnter = () => { - setIsHovered(true) - getCart() - } + setIsHovered(true); + getCart(); + }; const handleMouseLeave = () => { - setIsHovered(false) - } + setIsHovered(false); + }; const getCart = () => { if (!productCart && auth) { - refreshCartf() + refreshCartf(); } - } + }; const refreshCartf = useCallback(async () => { - setIsloading(true) - let cart = await getCartApi() - setProductCart(cart) - setCountCart(cart?.productTotal) - setIsloading(false) - }, [setProductCart, setIsloading]) + setIsloading(true); + let cart = await getCartApi(); + setProductCart(cart); + setCountCart(cart?.products?.length); + setIsloading(false); + }, [setProductCart, setIsloading]); useEffect(() => { - if (!products) return + if (!products) return; - let calculateTotalPriceBeforeTax = 0 - let calculateTotalTaxAmount = 0 - let calculateTotalDiscountAmount = 0 + let calculateTotalPriceBeforeTax = 0; + let calculateTotalTaxAmount = 0; + let calculateTotalDiscountAmount = 0; for (const product of products) { - if (product.quantity == '') continue + if (product.quantity == '') continue; - let priceBeforeTax = product.price.price / 1.11 - calculateTotalPriceBeforeTax += priceBeforeTax * product.quantity - calculateTotalTaxAmount += (product.price.price - priceBeforeTax) * product.quantity + let priceBeforeTax = product.price.price / 1.11; + calculateTotalPriceBeforeTax += priceBeforeTax * product.quantity; + calculateTotalTaxAmount += + (product.price.price - priceBeforeTax) * product.quantity; calculateTotalDiscountAmount += - (product.price.price - product.price.priceDiscount) * product.quantity + (product.price.price - product.price.priceDiscount) * product.quantity; } let subTotal = - calculateTotalPriceBeforeTax - calculateTotalDiscountAmount + calculateTotalTaxAmount - setSubTotal(subTotal) - }, [products]) + calculateTotalPriceBeforeTax - + calculateTotalDiscountAmount + + calculateTotalTaxAmount; + setSubTotal(subTotal); + }, [products]); useEffect(() => { if (refreshCart) { - refreshCartf() + refreshCartf(); } - setRefreshCart(false) - }, [refreshCart, refreshCartf, setRefreshCart]) + setRefreshCart(false); + }, [refreshCart, refreshCartf, setRefreshCart]); useEffect(() => { - setCountCart(cartCount.cartCount) - setRefreshCart(false) - }, [cartCount]) + setCountCart(cartCount.cartCount); + setRefreshCart(false); + }, [cartCount]); useEffect(() => { const handleScroll = () => { - setIsTop(window.scrollY === 0) - } - window.addEventListener('scroll', handleScroll) + setIsTop(window.scrollY === 0); + }; + window.addEventListener('scroll', handleScroll); return () => { - window.removeEventListener('scroll', handleScroll) - } - }, []) + window.removeEventListener('scroll', handleScroll); + }; + }, []); const handleCheckout = async () => { - SetButtonTerapkan(true) - let checkoutAll = await odooApi('POST', `/api/v1/user/${auth.id}/cart/select-all`) - router.push('/shop/checkout') - } - + SetButtonTerapkan(true); + let checkoutAll = await odooApi( + 'POST', + `/api/v1/user/${auth.id}/cart/select-all` + ); + router.push('/shop/checkout'); + }; return ( <div className='relative group'> @@ -152,8 +162,13 @@ const Cardheader = (cartCount) => { className='w-full max-w-md p-2 bg-white border border-gray-200 rounded-lg shadow overflow-hidden' > <div className='p-2 flex justify-between items-center'> - <h5 className='text-base font-semibold leading-none'>Keranjang Belanja</h5> - <Link href='/shop/cart' class='text-sm font-medium text-red-600 underline'> + <h5 className='text-base font-semibold leading-none'> + Keranjang Belanja + </h5> + <Link + href='/shop/cart' + class='text-sm font-medium text-red-600 underline' + > Lihat Semua </Link> </div> @@ -163,7 +178,10 @@ const Cardheader = (cartCount) => { <div className='justify-center p-4'> <p className='text-gray-500 text-center '> Silahkan{' '} - <Link href='/login' className='text-red-600 underline leading-6'> + <Link + href='/login' + className='text-red-600 underline leading-6' + > Login </Link>{' '} Untuk Melihat Daftar Keranjang Belanja Anda @@ -172,7 +190,11 @@ const Cardheader = (cartCount) => { )} {isLoading && itemLoading.map((item) => ( - <div key={item} role='status' className='max-w-sm animate-pulse'> + <div + key={item} + role='status' + className='max-w-sm animate-pulse' + > <div className='flex items-center space-x-4 mb- 2'> <div className='flex-shrink-0'> <PhotoIcon className='h-16 w-16 text-gray-500' /> @@ -194,14 +216,17 @@ const Cardheader = (cartCount) => { )} {auth && products.length > 0 && !isLoading && ( <> - <ul role='list' className='divide-y divide-gray-200 dark:divide-gray-700'> + <ul + role='list' + className='divide-y divide-gray-200 dark:divide-gray-700' + > {products && products?.map((product, index) => ( <> <li className='py-1 sm:py-2'> <div className='flex items-center space-x-4'> <div className='bagian gambar flex-shrink-0'> - {product.cartType === 'promotion' && ( + {product.cartType === 'promotion' && ( <Image src={product.imageProgram[0]} alt={product.name} @@ -227,10 +252,10 @@ const Cardheader = (cartCount) => { </div> <div className='bagian tulisan dan harga flex-1 min-w-0'> {product.cartType === 'promotion' && ( - <p className='text-caption-2 font-medium text-gray-900 truncate dark:text-white'> - {product.name} - </p> - )} + <p className='text-caption-2 font-medium text-gray-900 truncate dark:text-white'> + {product.name} + </p> + )} {product.cartType === 'product' && ( <Link href={createSlug( @@ -252,27 +277,32 @@ const Cardheader = (cartCount) => { {product?.price?.discountPercentage}% </div> <div className='text-gray_r-11 line-through text-caption-2'> - {currencyFormat(product?.price?.price)} + {currencyFormat( + product?.price?.price + )} </div> </div> )} - + <div className='flex justify-between items-center'> <div className='font-semibold text-sm text-red-600'> {product?.price?.priceDiscount > 0 ? ( - currencyFormat(product?.price?.priceDiscount) + currencyFormat( + product?.price?.priceDiscount + ) ) : ( <span className='text-gray_r-12/90 font-normal text-caption-1'> <a href={whatsappUrl('product', { name: product.name, - manufacture: product.manufacture?.name, + manufacture: + product.manufacture?.name, url: createSlug( '/shop/product/', product.name, product.id, true - ) + ), })} className='text-danger-500 underline' rel='noopener noreferrer' @@ -286,56 +316,112 @@ const Cardheader = (cartCount) => { </div> </div> </div> - <div className="flex flex-col w-3/4"> - {product.products?.map((product) => - <div key={product.id} className='md:ml-8 ml-4 mt-2 flex'> - <Link href={createSlug('/shop/product/', product.parent.name, product.parent.id.toString())} className='md:h-12 md:w-12 md:min-w-[48px] h-10 w-10 min-w-[40px] border border-gray-300 rounded '> - {product?.image && <Image src={product.image} alt={product.name} width={40} height={40} className='w-full h-full object-fill' />} - </Link> - - <div className="ml-4 w-full flex flex-col gap-y-1"> - <Link href={createSlug('/shop/product/', product.parent.name, product.parent.id.toString())} className="text-caption-2 font-medium text-gray-900 truncate dark:text-white"> - {product.displayName} - </Link> - - <div className='flex w-full'> - <div className="flex flex-col"> - {/* <div className="text-gray-500 text-caption-1">{product.code}</div> */} - <div> - <span className="text-gray-500 text-caption-1">Berat Barang: </span> - <span className="text-gray-500 text-caption-1">{product.packageWeight} Kg</span> - </div> - </div> + <div className='flex flex-col w-3/4'> + {product.products?.map((product) => ( + <div + key={product.id} + className='md:ml-8 ml-4 mt-2 flex' + > + <Link + href={createSlug( + '/shop/product/', + product.parent.name, + product.parent.id.toString() + )} + className='md:h-12 md:w-12 md:min-w-[48px] h-10 w-10 min-w-[40px] border border-gray-300 rounded ' + > + {product?.image && ( + <Image + src={product.image} + alt={product.name} + width={40} + height={40} + className='w-full h-full object-fill' + /> + )} + </Link> + + <div className='ml-4 w-full flex flex-col gap-y-1'> + <Link + href={createSlug( + '/shop/product/', + product.parent.name, + product.parent.id.toString() + )} + className='text-caption-2 font-medium text-gray-900 truncate dark:text-white' + > + {product.displayName} + </Link> + + <div className='flex w-full'> + <div className='flex flex-col'> + {/* <div className="text-gray-500 text-caption-1">{product.code}</div> */} + <div> + <span className='text-gray-500 text-caption-1'> + Berat Barang:{' '} + </span> + <span className='text-gray-500 text-caption-1'> + {product.packageWeight} Kg + </span> </div> </div> - </div> + </div> + </div> + ))} + {product.freeProducts?.map((product) => ( + <div + key={product.id} + className='md:ml-8 ml-4 mt-2 flex' + > + <Link + href={createSlug( + '/shop/product/', + product.parent.name, + product.parent.id.toString() )} - {product.freeProducts?.map((product) => - <div key={product.id} className='md:ml-8 ml-4 mt-2 flex'> - <Link href={createSlug('/shop/product/', product.parent.name, product.parent.id.toString())} className='md:h-12 md:w-12 md:min-w-[48px] h-10 w-10 min-w-[40px] border border-gray-300 rounded '> - {product?.image && <Image src={product.image} alt={product.name} width={40} height={40} className='w-full h-full object-fill' />} + className='md:h-12 md:w-12 md:min-w-[48px] h-10 w-10 min-w-[40px] border border-gray-300 rounded ' + > + {product?.image && ( + <Image + src={product.image} + alt={product.name} + width={40} + height={40} + className='w-full h-full object-fill' + /> + )} + </Link> + + <div className='ml-4 w-full flex flex-col gap-y-1'> + <Link + href={createSlug( + '/shop/product/', + product.parent.name, + product.parent.id.toString() + )} + className='text-caption-2 font-medium text-gray-900 truncate dark:text-white' + > + {product.displayName} </Link> - - <div className="ml-4 w-full flex flex-col gap-y-1"> - <Link href={createSlug('/shop/product/', product.parent.name, product.parent.id.toString())} className="text-caption-2 font-medium text-gray-900 truncate dark:text-white"> - {product.displayName} - </Link> - - <div className='flex w-full'> - <div className="flex flex-col"> - {/* <div className="text-gray-500 text-caption-1">{product.code}</div> */} - <div> - <span className="text-gray-500 text-caption-1">Berat Barang: </span> - <span className="text-gray-500 text-caption-1">{product.packageWeight} Kg</span> - </div> - </div> + + <div className='flex w-full'> + <div className='flex flex-col'> + {/* <div className="text-gray-500 text-caption-1">{product.code}</div> */} + <div> + <span className='text-gray-500 text-caption-1'> + Berat Barang:{' '} + </span> + <span className='text-gray-500 text-caption-1'> + {product.packageWeight} Kg + </span> </div> </div> - </div> - )} + </div> </div> + ))} + </div> </li> </> ))} @@ -347,8 +433,12 @@ const Cardheader = (cartCount) => { {auth && products.length > 0 && !isLoading && ( <> <div className='mt-3'> - <span className='text-gray-400 text-caption-2'>Subtotal Sebelum PPN : </span> - <span className='font-semibold text-red-600'>{currencyFormat(subTotal)}</span> + <span className='text-gray-400 text-caption-2'> + Subtotal Sebelum PPN :{' '} + </span> + <span className='font-semibold text-red-600'> + {currencyFormat(subTotal)} + </span> </div> <div className='mt-5 mb-2'> <button @@ -357,7 +447,9 @@ const Cardheader = (cartCount) => { onClick={handleCheckout} disabled={buttonLoading} > - {buttonLoading ? 'Loading...' : 'Lanjutkan Ke Pembayaran'} + {buttonLoading + ? 'Loading...' + : 'Lanjutkan Ke Pembayaran'} </button> </div> </> @@ -368,7 +460,7 @@ const Cardheader = (cartCount) => { )} </AnimatePresence> </div> - ) -} + ); +}; -export default Cardheader +export default Cardheader; diff --git a/src/lib/checkout/components/Checkout.jsx b/src/lib/checkout/components/Checkout.jsx index 4c7e852f..6fb5cdb4 100644 --- a/src/lib/checkout/components/Checkout.jsx +++ b/src/lib/checkout/components/Checkout.jsx @@ -37,6 +37,18 @@ const SELF_PICKUP_ID = 32; const { checkoutApi } = require('../api/checkoutApi'); const { getProductsCheckout } = require('../api/checkoutApi'); +function convertToInternational(number) { + if (typeof number !== 'string') { + throw new Error("Input harus berupa string"); + } + + if (number.startsWith('08')) { + return '+62' + number.slice(2); + } + + return number; +} + const Checkout = () => { const router = useRouter(); const query = router.query.source ?? null; @@ -413,7 +425,12 @@ const Checkout = () => { Math.round(parseInt(finalShippingAmt * 1.1) / 1000) * 1000; const finalGT = GT < 0 ? 0 : GT; setGrandTotal(finalGT); - }, [biayaKirim, cartCheckout?.grandTotal, activeVoucher, activeVoucherShipping]); + }, [ + biayaKirim, + cartCheckout?.grandTotal, + activeVoucher, + activeVoucherShipping, + ]); const checkout = async () => { const file = poFile.current.files[0]; @@ -442,6 +459,7 @@ const Checkout = () => { const productOrder = products.map((product) => ({ product_id: product.id, quantity: product.quantity, + available_quantity: product?.availableQuantity, })); let data = { // partner_shipping_id: auth.partnerId, @@ -483,6 +501,13 @@ const Checkout = () => { transaction_id: isCheckouted.id, }); + gtag('set', 'user_data', { + email: auth.email, + phone_number: convertToInternational(auth.mobile) ?? convertToInternational(auth.phone), + }); + + gtag('config', 'AW-954540379', { ' allow_enhanced_conversions':true } ) ; + for (const product of products) deleteItemCart({ productId: product.id }); if (grandTotal > 0) { const payment = await axios.post( @@ -500,7 +525,7 @@ const Checkout = () => { } } - /* const midtrans = async () => { + /* const midtrans = async () => { for (const product of products) deleteItemCart({ productId: product.id }); if (grandTotal > 0) { const payment = await axios.post( @@ -1192,7 +1217,11 @@ const Checkout = () => { <div className='text-gray_r-11'> Biaya Kirim <p className='text-xs mt-1'>{etdFix}</p> </div> - <div>{currencyFormat(Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000)}</div> + <div> + {currencyFormat( + Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000 + )} + </div> </div> {activeVoucherShipping && voucherShippingAmt && ( <div className='flex gap-x-2 justify-between'> @@ -1493,7 +1522,11 @@ const Checkout = () => { Biaya Kirim <p className='text-xs mt-1'>{etdFix}</p> </div> - <div>{currencyFormat(Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000) }</div> + <div> + {currencyFormat( + Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000 + )} + </div> </div> {activeVoucherShipping && voucherShippingAmt && ( <div className='flex gap-x-2 justify-between'> @@ -1664,7 +1697,7 @@ const SectionAddress = ({ address, label, url }) => ( ); const SectionValidation = ({ address }) => - address?.rajaongkirCityId == 0 && ( + address?.stateId == 0 && ( <BottomPopup active={true} title='Update Alamat'> <div className='leading-7 text-gray_r-12/80'> Mohon untuk memperbarui alamat Anda dengan mengklik tombol di bawah ini.{' '} diff --git a/src/lib/checkout/components/FinishCheckout.jsx b/src/lib/checkout/components/FinishCheckout.jsx index 92245e31..4a67b252 100644 --- a/src/lib/checkout/components/FinishCheckout.jsx +++ b/src/lib/checkout/components/FinishCheckout.jsx @@ -1,27 +1,86 @@ -import Link from '@/core/components/elements/Link/Link' +import Link from 'next/link'; +import Image from '~/components/ui/image'; +import whatsappUrl from '@/core/utils/whatsappUrl'; +import { useEffect, useState } from 'react'; +import odooApi from '@/core/api/odooApi'; +import useDevice from '@/core/hooks/useDevice'; +import useAuth from '@/core/hooks/useAuth'; +import axios from 'axios'; +import { toast } from 'react-hot-toast'; const FinishCheckout = ({ query }) => { + const [data, setData] = useState(); + const [transactionData, setTransactionData] = useState(); + const { isDesktop, isMobile } = useDevice(); + const auth = useAuth(); + + const so_order = query?.order_id?.replaceAll('-', '/'); + useEffect(() => { + const fetchData = async () => { + const fetchedData = await odooApi( + 'GET', + `/api/v1/sale_order_number?sale_number=${so_order}` + ); + setData(fetchedData[0]); + }; + fetchData(); + }, [query]); + + // Kirim email ketika komponen ini dimount atau sesuai kondisi + const sendEmail = async () => { + try { + const send = await axios.post( + `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/finish-checkout?orderName=${query?.order_id}`, + {} + ); + if (send.status === 200) { + toast.success('Berhasil mengirim rincian pesanan'); + } else { + toast.error('Gagal mengirimkan rincian pesanan'); + } + } catch (error) { + console.error(error); + toast.error('Gagal mengirimkan rincian pesanan'); + } + }; + return ( - <div className='mx-auto container p-4 md:p-0 mt-0 md:mt-10'> - <div className='rounded-xl bg-warning-100 text-center border border-warning-300 w-full md:w-1/2 mx-auto'> - <div className='px-4 py-6 text-warning-900'> - <p className='font-semibold mb-2'>Terima Kasih atas Pembelian Anda</p> - <p className='text-warning-800 mb-4 leading-6'> - Rincian belanja sudah kami kirimkan ke email anda. Mohon dicek kembali. jika tidak - menerima email, anda dapat menghubungi kami disini. - </p> - <p className='mb-2 font-medium'>{query?.order_id?.replaceAll('-', '/')}</p> - <p className='text-caption-2 text-warning-800'>No. Transaksi</p> - </div> + <div className='flex flex-col items-center'> + <Image + src='/images/CHECKOUT-PESANAN.svg' + alt='Checkout Pesanan' + width={isMobile ? 300 : 450} + height={isMobile ? 300 : 450} + /> + <div className='text-title-sm md:text-title-lg text-center font-semibold'> + Terima Kasih atas Pembelian Kamu + </div> + <div className='flex flex-col justify-center items-center text-body-2 md:text-body-1 text-center mt-3 px-24 md:px-36 py-4 border-2 gap-y-2 rounded'> + <p className='font-bold'>No. Transaksi</p> + <p className='mb-2 font-medium text-red-500 text-xl'> + {query?.order_id?.replaceAll('-', '/')} + </p> <Link - href='/my/quotations' - className='bg-warning-400 text-warning-900 rounded-b-xl py-4 block' + href={`/my/quotations/${data?.id}`} + className='btn-solid-red rounded-md text-base' > - Lihat detail pembelian Anda disini + Cek Detail Transaksi </Link> </div> + <div className='mt-2 text-center leading-6 text-base p-4 md:p-0 md:max-w-[700px]'> + Rincian pembelian sudah kami kirimkan ke email kamu. Mohon dicek + kembali. jika tidak menerima email, kamu dapat menghubungi kami{' '} + <a className='text-red-500' href={whatsappUrl()}> + di sini + </a>{' '} + atau{' '} + <span onClick={sendEmail} className='text-red-500 cursor-pointer'> + kirim rincian pesanan ulang + </span> + . + </div> </div> - ) -} + ); +}; -export default FinishCheckout +export default FinishCheckout; diff --git a/src/lib/flashSale/components/FlashSale.jsx b/src/lib/flashSale/components/FlashSale.jsx index 5be6d4e3..6d90cad7 100644 --- a/src/lib/flashSale/components/FlashSale.jsx +++ b/src/lib/flashSale/components/FlashSale.jsx @@ -2,10 +2,8 @@ import Image from 'next/image'; import { useEffect, useState } from 'react'; import CountDown from '@/core/components/elements/CountDown/CountDown'; -import productSearchApi from '@/lib/product/api/productSearchApi'; import ProductSlider from '@/lib/product/components/ProductSlider'; -import flashSaleApi from '../api/flashSaleApi'; import { FlashSaleSkeleton } from '../skeleton/FlashSaleSkeleton'; const FlashSale = () => { @@ -14,10 +12,14 @@ const FlashSale = () => { useEffect(() => { const loadFlashSales = async () => { - const dataFlashSales = await flashSaleApi(); - setFlashSales(dataFlashSales); + const res = await fetch('/api/flashsale-header'); + const { data } = await res.json(); + if (data) { + setFlashSales(data); + } setIsLoading(false); }; + loadFlashSales(); }, []); @@ -26,54 +28,64 @@ const FlashSale = () => { } return ( - flashSales?.length > 0 && ( - <div className='px-4 sm:px-0 grid grid-cols-1 gap-y-8'> - {flashSales.map((flashSale, index) => ( - <div key={index}> - <div className='flex gap-x-3 mb-4 justify-between sm:justify-start'> - <div className='font-medium sm:text-h-lg mt-1.5'> - {flashSale.name} + <div className='sm:mt-4'> + {flashSales?.length > 0 && ( + <div className='px-4 sm:px-0 grid grid-cols-1 gap-y-8 sm:mt-4'> + {flashSales.map((flashSale, index) => ( + <div key={index}> + <div className='flex gap-x-3 mb-4 justify-between sm:justify-start'> + <div className='font-medium sm:text-h-lg mt-1.5'> + {flashSale.name} + </div> + <CountDown initialTime={flashSale.duration} /> </div> - <CountDown initialTime={flashSale.duration} /> - </div> - <div className='relative'> - <Image - src={flashSale.banner} - alt={flashSale.name} - width={1080} - height={192} - className='w-full rounded mb-4 hidden sm:block' - /> - <Image - src={flashSale.bannerMobile} - alt={flashSale.name} - width={256} - height={48} - className='w-full rounded mb-4 block sm:hidden' - /> - <FlashSaleProduct flashSaleId={flashSale.pricelistId} /> + <div className='relative'> + <Image + src={flashSale.banner} + alt={flashSale.name} + width={1080} + height={192} + className='w-full rounded mb-4 hidden sm:block' + /> + <Image + src={flashSale.bannerMobile} + alt={flashSale.name} + width={256} + height={48} + className='w-full rounded mb-4 block sm:hidden' + /> + <FlashSaleProduct + flashSaleId={flashSale.pricelistId} + duration={flashSale.duration} + /> + </div> </div> - </div> - ))} - </div> - ) + ))} + </div> + )} + </div> ); }; -const FlashSaleProduct = ({ flashSaleId }) => { +const FlashSaleProduct = ({ flashSaleId, duration }) => { const [products, setProducts] = useState(null); - useEffect(() => { + const data_search = new URLSearchParams({ + query: `fq=flashsale_id_i:${flashSaleId}&fq=flashsale_price_f:[1 TO *]&limit=500&orderBy=flashsale-price-asc&source=similar`, + operation: 'AND', + duration: `${duration}`, + }); const loadProducts = async () => { - const dataProducts = await productSearchApi({ - query: `fq=flashsale_id_i:${flashSaleId}&fq=flashsale_price_f:[1 TO *]&limit=500&orderBy=flashsale-price-asc`, - operation: 'AND', - }); - setProducts(dataProducts.response); + const res = await fetch( + `/api/search-flashsale?${data_search.toString()}` + ); + const { data } = await res.json(); + setProducts(data.response); }; + loadProducts(); - }, [flashSaleId]); + }, []); return <ProductSlider products={products} />; }; diff --git a/src/lib/flashSale/components/FlashSaleNonDisplay.jsx b/src/lib/flashSale/components/FlashSaleNonDisplay.jsx new file mode 100644 index 00000000..4b420fac --- /dev/null +++ b/src/lib/flashSale/components/FlashSaleNonDisplay.jsx @@ -0,0 +1,68 @@ +import Image from 'next/image'; +import { useEffect, useState } from 'react'; +import CountDown from '@/core/components/elements/CountDown/CountDown'; +import productSearchApi from '@/lib/product/api/productSearchApi'; +import ProductSlider from '@/lib/product/components/ProductSlider'; +import flashSaleApi from '../api/flashSaleApi'; +import { FlashSaleSkeleton } from '../skeleton/FlashSaleSkeleton'; +import Link from 'next/link'; +import { useRouter } from 'next/router'; +const FlashSaleNonDisplay = () => { + const [flashSales, setFlashSales] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const router = useRouter(); + useEffect(() => { + const loadFlashSales = async () => { + const dataFlashSales = await flashSaleApi(); + setFlashSales(dataFlashSales); + setIsLoading(false); + }; + loadFlashSales(); + }, []); + const handleSubmit = () => { + router.push(`/shop/search?penawaran=${flashSales[0]?.pricelistId}`); + }; + if (isLoading) { + return <FlashSaleSkeleton />; + } + + return ( + flashSales?.length > 0 && ( + <div className='px-4 sm:px-0 grid grid-cols-1 gap-y-8'> + {flashSales.map((flashSale, index) => ( + <div key={index}> + <div className='flex items-center mb-4 justify-between '> + <div className='font-medium sm:text-h-lg mt-1.5'> + Penawaran Terbatas + </div> + <div + onClick={handleSubmit} + className='!text-red-500 font-semibold cursor-pointer' + > + Lihat Semua + </div> + </div> + <div className='relative'> + <FlashSaleProduct flashSaleId={flashSale.pricelistId} /> + </div> + </div> + ))} + </div> + ) + ); +}; +const FlashSaleProduct = ({ flashSaleId }) => { + const [products, setProducts] = useState(null); + useEffect(() => { + const loadProducts = async () => { + const dataProducts = await productSearchApi({ + query: `fq=-flashsale_id_i:${flashSaleId}&fq=flashsale_price_f:[1 TO *]&limit=25&orderBy=flashsale-discount-desc&source=similar`, + operation: 'AND', + }); + setProducts(dataProducts.response); + }; + loadProducts(); + }, [flashSaleId]); + return <ProductSlider products={products} />; +}; +export default FlashSaleNonDisplay; diff --git a/src/lib/home/api/categoryManagementApi.js b/src/lib/home/api/categoryManagementApi.js index 2ff4fdfc..4101f87a 100644 --- a/src/lib/home/api/categoryManagementApi.js +++ b/src/lib/home/api/categoryManagementApi.js @@ -42,3 +42,11 @@ const map = async (promotions) => { return productMapped; }); }; + +export const fetchCategoryManagementVersion = async () => { + const response = await fetch( + '/solr/admin/cores?action=STATUS&core=category_management' + ); + const data = await response.json(); + return data.status.category_management.index.version; +}; diff --git a/src/lib/home/components/BannerSection.jsx b/src/lib/home/components/BannerSection.jsx index f83c36fc..303b5c4b 100644 --- a/src/lib/home/components/BannerSection.jsx +++ b/src/lib/home/components/BannerSection.jsx @@ -1,18 +1,48 @@ import Link from '@/core/components/elements/Link/Link'; import Image from 'next/image'; +import { useEffect, useState } from 'react'; +import { bannerApi } from '../../../api/bannerApi'; const { useQuery } = require('react-query'); const { default: bannerSectionApi } = require('../api/bannerSectionApi'); const BannerSection = () => { - const fetchBannerSection = async () => await bannerSectionApi(); - const bannerSection = useQuery('bannerSection', fetchBannerSection); + const [data, setData] = useState(null); + const [shouldFetch, setShouldFetch] = useState(false); + useEffect(() => { + const fetchCategoryData = async () => { + const res = await fetch('/api/banner-section'); + const { data } = await res.json(); + if (data) { + setData(data); + } + }; + + fetchCategoryData(); + }, []); + + // const fetchBannerSection = async () => await bannerSectionApi(); + const getBannerSection = useQuery( + 'bannerSection', + bannerApi({ type: 'home-banner' }), + { + enabled: shouldFetch, + onSuccess: (data) => { + if (data) { + localStorage.setItem('Homepage_bannerSection', JSON.stringify(data)); + setData(data); + } + }, + } + ); + + const bannerSection = data; return ( - bannerSection.data && - bannerSection.data?.length > 0 && ( + bannerSection && + bannerSection?.length > 0 && ( <div className='grid grid-cols-1 sm:grid-cols-2 gap-4'> - {bannerSection.data?.map((banner) => ( + {bannerSection?.map((banner) => ( <Link key={banner.id} href={banner.url}> <Image width={1024} diff --git a/src/lib/home/components/CategoryDynamic.jsx b/src/lib/home/components/CategoryDynamic.jsx index 49a9a93f..cc4f42b7 100644 --- a/src/lib/home/components/CategoryDynamic.jsx +++ b/src/lib/home/components/CategoryDynamic.jsx @@ -1,9 +1,9 @@ -import React, { useEffect, useState, useCallback } from 'react'; +import React, { useEffect, useState } from 'react'; import { fetchCategoryManagementSolr } from '../api/categoryManagementApi'; +import { Skeleton } from '@chakra-ui/react'; import NextImage from 'next/image'; import Link from 'next/link'; import { createSlug } from '@/core/utils/slug'; -import { Skeleton } from '@chakra-ui/react'; import { Swiper, SwiperSlide } from 'swiper/react'; import 'swiper/css'; import 'swiper/css/navigation'; @@ -12,45 +12,21 @@ import { Pagination } from 'swiper'; const CategoryDynamic = () => { const [categoryManagement, setCategoryManagement] = useState([]); - const [isLoading, setIsLoading] = useState(false); - const loadBrand = useCallback(async () => { - setIsLoading(true); - const items = await fetchCategoryManagementSolr(); - - setIsLoading(false); - setCategoryManagement(items); - }, []); + const [isLoading, setIsLoading] = useState(true); useEffect(() => { - loadBrand(); - }, [loadBrand]); - - // const [categoryData, setCategoryData] = useState({}); - // const [subCategoryData, setSubCategoryData] = useState({}); - - // useEffect(() => { - // const fetchCategoryData = async () => { - // if (categoryManagement && categoryManagement.data) { - // const updatedCategoryData = {}; - // const updatedSubCategoryData = {}; + const fetchCategoryData = async () => { + setIsLoading(true); + const res = await fetch('/api/category-management'); + const { data } = await res.json(); + if (data) { + setCategoryManagement(data); + } + setIsLoading(false); + }; - // for (const category of categoryManagement.data) { - // const countLevel1 = await odooApi('GET', `/api/v1/category/numFound?parent_id=${category.categoryIdI}`); - - // updatedCategoryData[category.categoryIdI] = countLevel1?.numFound; - - // for (const subCategory of countLevel1?.children) { - // updatedSubCategoryData[subCategory.id] = subCategory?.numFound; - // } - // } - - // setCategoryData(updatedCategoryData); - // setSubCategoryData(updatedSubCategoryData); - // } - // }; - - // fetchCategoryData(); - // }, [categoryManagement.isLoading]); + fetchCategoryData(); + }, []); const swiperBanner = { modules: [Pagination], @@ -66,115 +42,99 @@ const CategoryDynamic = () => { return ( <div> {categoryManagement && - categoryManagement?.map((category) => { - // const countLevel1 = categoryData[category.categoryIdI] || 0; - return ( - <Skeleton key={category.id} isLoaded={!isLoading}> - <div key={category.id}> - <div className='bagian-judul flex flex-row justify-start items-center gap-3 mb-4 mt-4'> - <h1 className='font-semibold text-[14px] sm:text-h-lg mr-2'> - {category.name} - </h1> - {/* <Skeleton isLoaded={countLevel1 != 0}> - <p className={`text-gray_r-10 text-sm`}>{countLevel1} Produk tersedia</p> - </Skeleton> */} - <Link - href={createSlug( - '/shop/category/', - category?.name, - category?.category_id - )} - className='!text-red-500 font-semibold' - > - Lihat Semua - </Link> - </div> - - {/* Swiper for SubCategories */} - <Swiper {...swiperBanner}> - {category.categories.map((subCategory) => { - // const countLevel2 = subCategoryData[subCategory.idLevel2] || 0; + categoryManagement.map((category) => ( + <Skeleton key={category.id} isLoaded={!isLoading}> + <div key={category.id}> + <div className='bagian-judul flex flex-row justify-start items-center gap-3 mb-4 mt-4'> + <h1 className='font-semibold text-[14px] sm:text-h-lg mr-2'> + {category.name} + </h1> + <Link + href={createSlug( + '/shop/category/', + category?.name, + category?.category_id + )} + className='!text-red-500 font-semibold' + > + Lihat Semua + </Link> + </div> - return ( - <SwiperSlide key={subCategory.id}> - <div className='border rounded justify-start items-start '> - <div className='p-3'> - <div className='flex flex-row border rounded mb-2 justify-start items-center'> - <NextImage - src={ - subCategory.image - ? subCategory.image - : '/images/noimage.jpeg' - } - alt={subCategory.name} - width={90} - height={30} - className='object-fit p-4' - /> - <div className='bagian-judul flex flex-col justify-center items-start gap-2 ml-2'> - <h2 className='font-semibold text-lg mr-2'> - {subCategory?.name} - </h2> - {/* <Skeleton isLoaded={countLevel2 != 0}> - <p className={`text-gray_r-10 text-sm`}> - {countLevel2} Produk tersedia - </p> - </Skeleton> */} + <Swiper {...swiperBanner}> + {category?.categories?.map((subCategory) => ( + <SwiperSlide key={subCategory.id}> + <div className='border rounded justify-start items-start '> + <div className='p-3'> + <div className='flex flex-row border rounded mb-2 justify-start items-center'> + <NextImage + src={ + subCategory.image + ? subCategory.image + : '/images/noimage.jpeg' + } + alt={subCategory.name} + width={90} + height={30} + className='object-fit p-4' + /> + <div className='bagian-judul flex flex-col justify-center items-start gap-2 ml-2'> + <h2 className='font-semibold text-lg mr-2'> + {subCategory?.name} + </h2> + <Link + href={createSlug( + '/shop/category/', + subCategory?.name, + subCategory?.id_level_2 + )} + className='!text-red-500 font-semibold' + > + Lihat Semua + </Link> + </div> + </div> + <div className='grid grid-cols-2 gap-2 overflow-y-auto max-h-[240px] min-h-[240px] content-start'> + {subCategory.child_frontend_id_i.map( + (childCategory) => ( + <div key={childCategory.id} className=''> <Link href={createSlug( '/shop/category/', - subCategory?.name, - subCategory?.id_level_2 + childCategory?.name, + childCategory?.id_level_3 )} - className='!text-red-500 font-semibold' + className='flex flex-row gap-2 border rounded group hover:border-red-500' > - Lihat Semua + <NextImage + src={ + childCategory.image + ? childCategory.image + : '/images/noimage.jpeg' + } + alt={childCategory.name} + className='p-2 ml-1' + width={40} + height={40} + /> + <div className='bagian-judul flex flex-col justify-center items-center gap-2 break-words line-clamp-2 group-hover:text-red-500'> + <h3 className='font-semibold line-clamp-2 group-hover:text-red-500 text-sm mr-2'> + {childCategory.name} + </h3> + </div> </Link> </div> - </div> - <div className='grid grid-cols-2 gap-2 overflow-y-auto max-h-[240px] min-h-[240px] content-start'> - {subCategory.child_frontend_id_i.map( - (childCategory) => ( - <div key={childCategory.id} className=''> - <Link - href={createSlug( - '/shop/category/', - childCategory?.name, - childCategory?.id_level_3 - )} - className='flex flex-row gap-2 border rounded group hover:border-red-500' - > - <NextImage - src={ - childCategory.image - ? childCategory.image - : '/images/noimage.jpeg' - } - alt={childCategory.name} - className='p-2 ml-1' - width={40} - height={40} - /> - <div className='bagian-judul flex flex-col justify-center items-center gap-2 break-words line-clamp-2 group-hover:text-red-500'> - <h3 className='font-semibold line-clamp-2 group-hover:text-red-500 text-sm mr-2'> - {childCategory.name} - </h3> - </div> - </Link> - </div> - ) - )} - </div> - </div> + ) + )} </div> - </SwiperSlide> - ); - })} - </Swiper> - </div> - </Skeleton> - ); - })} + </div> + </div> + </SwiperSlide> + ))} + </Swiper> + </div> + </Skeleton> + ))} </div> ); }; diff --git a/src/lib/home/components/CategoryDynamicMobile.jsx b/src/lib/home/components/CategoryDynamicMobile.jsx index 4a8f13cf..67ae6f5f 100644 --- a/src/lib/home/components/CategoryDynamicMobile.jsx +++ b/src/lib/home/components/CategoryDynamicMobile.jsx @@ -4,52 +4,46 @@ import Link from 'next/link'; import { createSlug } from '@/core/utils/slug'; import { Swiper, SwiperSlide } from 'swiper/react'; import 'swiper/css'; -import { fetchCategoryManagementSolr } from '../api/categoryManagementApi'; +import { + fetchCategoryManagementSolr, + fetchCategoryManagementVersion, +} from '../api/categoryManagementApi'; const CategoryDynamicMobile = () => { const [selectedCategory, setSelectedCategory] = useState({}); const [categoryManagement, setCategoryManagement] = useState([]); const [isLoading, setIsLoading] = useState(false); - const loadBrand = useCallback(async () => { - setIsLoading(true); - const items = await fetchCategoryManagementSolr(); + useEffect(() => { + const fetchCategoryData = async () => { + setIsLoading(true); + const res = await fetch('/api/category-management'); + const { data } = await res.json(); + if (data) { + setCategoryManagement(data); + } + setIsLoading(false); + }; - setIsLoading(false); - setCategoryManagement(items); + fetchCategoryData(); }, []); useEffect(() => { - loadBrand(); - }, [loadBrand]); - - useEffect(() => { - const loadPromo = async () => { - try { - if (categoryManagement?.length > 0) { - const initialSelections = categoryManagement.reduce( - (acc, category) => { - if (category.categories.length > 0) { - acc[category.id] = category.categories[0].id_level_2; - } - return acc; - }, - {} - ); - setSelectedCategory(initialSelections); + if (categoryManagement?.length > 0) { + const initialSelections = categoryManagement.reduce((acc, category) => { + if (category.categories.length > 0) { + acc[category.id] = category.categories[0].id_level_2; } - } catch (loadError) { - // console.error("Error loading promo items:", loadError); - } - }; - - loadPromo(); + return acc; + }, {}); + setSelectedCategory(initialSelections); + } }, [categoryManagement]); - const handleCategoryLevel2Click = (categoryIdI, idLevel2) => { + const handleCategoryLevel2Click = (categoryId, idLevel2) => { setSelectedCategory((prev) => ({ ...prev, - [categoryIdI]: idLevel2, + [categoryId]: idLevel2, })); }; diff --git a/src/lib/home/components/PreferredBrand.jsx b/src/lib/home/components/PreferredBrand.jsx index eefced60..b7a30503 100644 --- a/src/lib/home/components/PreferredBrand.jsx +++ b/src/lib/home/components/PreferredBrand.jsx @@ -1,49 +1,50 @@ -import { Swiper, SwiperSlide } from 'swiper/react' -import { Navigation, Pagination, Autoplay } from 'swiper'; -import { useCallback, useEffect, useState } from 'react' -import usePreferredBrand from '../hooks/usePreferredBrand' -import PreferredBrandSkeleton from './Skeleton/PreferredBrandSkeleton' -import BrandCard from '@/lib/brand/components/BrandCard' -import useDevice from '@/core/hooks/useDevice' -import Link from '@/core/components/elements/Link/Link' -import axios from 'axios' +import { Swiper, SwiperSlide } from 'swiper/react'; +import { Navigation, Pagination, Autoplay } from 'swiper'; +import { useCallback, useEffect, useState } from 'react'; +import usePreferredBrand from '../hooks/usePreferredBrand'; +import PreferredBrandSkeleton from './Skeleton/PreferredBrandSkeleton'; +import BrandCard from '@/lib/brand/components/BrandCard'; +import useDevice from '@/core/hooks/useDevice'; +import Link from '@/core/components/elements/Link/Link'; +import axios from 'axios'; const PreferredBrand = () => { - let query = '' - let params = 'prioritas' - const [isLoading, setIsLoading] = useState(true) - const [startWith, setStartWith] = useState(null) - const [manufactures, setManufactures] = useState([]) + let query = ''; + let params = 'prioritas'; + const [isLoading, setIsLoading] = useState(true); + const [startWith, setStartWith] = useState(null); + const [manufactures, setManufactures] = useState([]); const loadBrand = useCallback(async () => { - setIsLoading(true) - const name = startWith ? `${startWith}*` : '' - const result = await axios(`${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/brands?rows=20`) - - setIsLoading(false) - setManufactures((manufactures) => [...result.data]) - }, [startWith]) + setIsLoading(true); + const name = startWith ? `${startWith}*` : ''; + const result = await axios( + `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/preferredBrand?rows=20` + ); + setIsLoading(false); + setManufactures((manufactures) => [...result.data]); + }, [startWith]); const toggleStartWith = (alphabet) => { - setManufactures([]) + setManufactures([]); if (alphabet == startWith) { - setStartWith(null) - return + setStartWith(null); + return; } - setStartWith(alphabet) - } + setStartWith(alphabet); + }; useEffect(() => { - loadBrand() - }, []) + loadBrand(); + }, []); // const { preferredBrands } = usePreferredBrand(query) - const { isMobile, isDesktop } = useDevice() + const { isMobile, isDesktop } = useDevice(); const swiperBanner = { - modules:[Navigation, Pagination, Autoplay], + modules: [Navigation, Pagination, Autoplay], autoplay: { delay: 4000, - disableOnInteraction: false + disableOnInteraction: false, }, loop: true, className: 'h-[70px] md:h-[100px] w-full', @@ -53,13 +54,17 @@ const PreferredBrand = () => { dynamicBullets: true, dynamicMainBullets: isMobile ? 6 : 8, clickable: true, - } - } - const preferredBrandsData = manufactures ? manufactures.slice(0, 20) : [] + }, + }; + const preferredBrandsData = manufactures ? manufactures.slice(0, 20) : []; return ( <div className='px-4 sm:px-0'> <div className='flex justify-between items-center mb-4'> - <h1 className='font-semibold text-[14px] sm:text-h-lg'><Link href='/shop/brands' className='!text-black font-semibold'>Brand Pilihan</Link></h1> + <h1 className='font-semibold text-[14px] sm:text-h-lg'> + <Link href='/shop/brands' className='!text-black font-semibold'> + Brand Pilihan + </Link> + </h1> {isDesktop && ( <Link href='/shop/brands' className='!text-red-500 font-semibold'> Lihat Semua @@ -79,7 +84,7 @@ const PreferredBrand = () => { )} </div> </div> - ) -} + ); +}; -export default PreferredBrand
\ No newline at end of file +export default PreferredBrand; diff --git a/src/lib/home/components/PromotionProgram.jsx b/src/lib/home/components/PromotionProgram.jsx index ae06bd4d..562fa138 100644 --- a/src/lib/home/components/PromotionProgram.jsx +++ b/src/lib/home/components/PromotionProgram.jsx @@ -4,15 +4,56 @@ import { bannerApi } from '@/api/bannerApi'; import useDevice from '@/core/hooks/useDevice'; import { Swiper, SwiperSlide } from 'swiper/react'; import BannerPromoSkeleton from '../components/Skeleton/BannerPromoSkeleton'; +import { useEffect, useState } from 'react'; const { useQuery } = require('react-query'); const BannerSection = () => { - const promotionProgram = useQuery( - 'promotionProgram', - bannerApi({ type: 'banner-promotion' }) - ); const { isMobile, isDesktop } = useDevice(); + const [data, setData] = useState(null); + const [shouldFetch, setShouldFetch] = useState(false); + useEffect(() => { + const fetchData = async () => { + const res = await fetch(`/api/hero-banner?type=banner-promotion`); + const { data } = await res.json(); + if (data) { + setData(data); + } + }; + + fetchData(); + }, []); + + // useEffect(() => { + // const localData = localStorage.getItem('Homepage_promotionProgram'); + // if (localData) { + // setData(JSON.parse(localData)); + // } else { + // setShouldFetch(true); + // } + // }, []); + + // const getPromotionProgram = useQuery( + // 'promotionProgram', + // bannerApi({ type: 'banner-promotion' }), + // { + // enabled: shouldFetch, + // onSuccess: (data) => { + // if (data) { + // localStorage.setItem( + // 'Homepage_promotionProgram', + // JSON.stringify(data) + // ); + // setData(data); + // } + // }, + // } + // ); + + const promotionProgram = data; - if (promotionProgram.isLoading) { + // if (getPromotionProgram?.isLoading && !data) { + // return <BannerPromoSkeleton />; + // } + if (!data) { return <BannerPromoSkeleton />; } @@ -39,28 +80,26 @@ const BannerSection = () => { </Link> )} </div> - {isDesktop && - promotionProgram.data && - promotionProgram.data?.length > 0 && ( - <div className='grid grid-cols-3 sm:grid-cols-3 gap-4 rounded-md'> - {promotionProgram.data?.map((banner) => ( - <Link key={banner.id} href={banner.url}> - <Image - width={439} - height={150} - quality={85} - src={banner.image} - alt={banner.name} - className='h-auto w-full rounded hover:scale-105 transition duration-500 ease-in-out' - /> - </Link> - ))} - </div> - )} + {isDesktop && promotionProgram && promotionProgram?.length > 0 && ( + <div className='grid grid-cols-3 sm:grid-cols-3 gap-4 rounded-md'> + {promotionProgram?.map((banner) => ( + <Link key={banner.id} href={banner.url}> + <Image + width={439} + height={150} + quality={85} + src={banner.image} + alt={banner.name} + className='h-auto w-full rounded hover:scale-105 transition duration-500 ease-in-out' + /> + </Link> + ))} + </div> + )} {isMobile && ( <Swiper slidesPerView={1.1} spaceBetween={8} freeMode> - {promotionProgram.data?.map((banner) => ( + {promotionProgram?.map((banner) => ( <SwiperSlide key={banner.id}> <Link key={banner.id} href={banner.url}> <Image diff --git a/src/lib/home/components/ServiceList.jsx b/src/lib/home/components/ServiceList.jsx index 5b16915d..b3cc8fe5 100644 --- a/src/lib/home/components/ServiceList.jsx +++ b/src/lib/home/components/ServiceList.jsx @@ -32,7 +32,7 @@ const ServiceList = () => { </div> <div className='w-full'> <Link - href='/tentang-kami' + href='/garansi-resmi' className='border border-gray-200 p-2 flex items-center gap-x-2 rounded-lg' > <div className=''> @@ -57,7 +57,7 @@ const ServiceList = () => { </div> <div className='w-full '> <Link - href='/tentang-kami' + href='/pembayaran-tempo' className='border border-gray-200 p-2 flex items-center gap-x-2 rounded-lg' > <div className=''> diff --git a/src/lib/product/components/Product/ProductDesktopVariant.jsx b/src/lib/product/components/Product/ProductDesktopVariant.jsx index 09b30a44..5dfd452b 100644 --- a/src/lib/product/components/Product/ProductDesktopVariant.jsx +++ b/src/lib/product/components/Product/ProductDesktopVariant.jsx @@ -1,12 +1,12 @@ - -import { Box, Skeleton, Tooltip } from '@chakra-ui/react'; +import { Box, Button, Skeleton, Tooltip } from '@chakra-ui/react'; import { HeartIcon } from '@heroicons/react/24/outline'; -import { Info } from 'lucide-react'; +import { Info, MessageCircleIcon, Share2Icon } from 'lucide-react'; import { useRouter } from 'next/router'; import { useCallback, useEffect, useRef, useState } from 'react'; import { toast } from 'react-hot-toast'; +import AddToWishlist from '../../../../../src-migrate/modules/product-detail/components/AddToWishlist'; +import { RWebShare } from 'react-web-share'; import LazyLoad from 'react-lazy-load'; - import { useProductCartContext } from '@/contexts/ProductCartContext'; import odooApi from '@/core/api/odooApi'; import Image from '@/core/components/elements/Image/Image'; @@ -18,10 +18,16 @@ import { updateItemCart } from '@/core/utils/cart'; import currencyFormat from '@/core/utils/currencyFormat'; import { createSlug } from '@/core/utils/slug'; import whatsappUrl from '@/core/utils/whatsappUrl'; +import { getAuth } from '~/libs/auth'; +import ImageNext from 'next/image'; import productSimilarApi from '../../api/productSimilarApi'; import ProductCard from '../ProductCard'; import ProductSimilar from '../ProductSimilar'; +import ProductPromoSection from '~/modules/product-promo/components/Section'; +import SimilarBottom from '~/modules/product-detail/components/SimilarBottom'; + +const SELF_HOST = process.env.NEXT_PUBLIC_SELF_HOST; const ProductDesktopVariant = ({ product, @@ -30,9 +36,10 @@ const ProductDesktopVariant = ({ isVariant, }) => { const router = useRouter(); - const auth = useAuth(); + let auth = useAuth(); const { slug } = router.query; - + const { srsltid } = router.query; + const [askAdminUrl, setAskAdminUrl, isApproval] = useState(); const [lowestPrice, setLowestPrice] = useState(null); const [addCartAlert, setAddCartAlert] = useState(false); @@ -40,11 +47,20 @@ const ProductDesktopVariant = ({ const { setRefreshCart } = useProductCartContext(); + const [quantityInput, setQuantityInput] = useState(1); + + const createdAskUrl = whatsappUrl({ + template: 'product', + payload: { + manufacture: product.manufacture.name, + productName: product.name, + url: process.env.NEXT_PUBLIC_SELF_HOST + router.asPath, + }, + fallbackUrl: router.asPath, + }); + const getLowestPrice = useCallback(() => { const lowest = product.price; - /* const lowest = prices.reduce((lowest, price) => { - return price.priceDiscount < lowest.priceDiscount ? price : lowest - }, prices[0])*/ return lowest; }, [product]); @@ -74,10 +90,10 @@ const ProductDesktopVariant = ({ const handleAddToCart = (variant) => { if (!auth) { - router.push(`/login?next=/shop/product/${slug}`); + router.push(`/login?next=/shop/product/${slug}?srsltid=${srsltid}`); return; } - const quantity = variantQuantityRefs.current[product.id].value; + const quantity = quantityInput; if (!validQuantity(quantity)) return; updateItemCart({ productId: product.id, @@ -91,8 +107,34 @@ const ProductDesktopVariant = ({ setAddCartAlert(true); }; - const handleBuy = (variant) => { - const quantity = variantQuantityRefs.current[product.id].value; + const handleBuy = async (variant) => { + const quantity = variantQuantityRefs?.current[product.id]?.value; + let isLoggedIn = typeof auth === 'object'; + + if (!isLoggedIn) { + const currentUrl = encodeURIComponent(router.asPath); + await router.push(`/login?next=${currentUrl}`); + + // Tunggu login berhasil, misalnya dengan memantau perubahan status auth. + const authCheckInterval = setInterval(() => { + const newAuth = getAuth(); + if (typeof newAuth === 'object') { + isLoggedIn = true; + auth = newAuth; // Update nilai auth setelah login + clearInterval(authCheckInterval); + } + }, 500); // Periksa status login setiap 500ms + + await new Promise((resolve) => { + const checkLogin = setInterval(() => { + if (isLoggedIn) { + clearInterval(checkLogin); + resolve(null); + } + }, 500); + }); + } + if (!validQuantity(quantity)) return; updateItemCart({ @@ -105,6 +147,45 @@ const ProductDesktopVariant = ({ router.push(`/shop/checkout?source=buy`); }; + const handleButton = async (variant) => { + const quantity = quantityInput; + let isLoggedIn = typeof auth === 'object'; + + if (!isLoggedIn) { + const currentUrl = encodeURIComponent(router.asPath); + await router.push(`/login?next=${currentUrl}`); + + // Tunggu login berhasil, misalnya dengan memantau perubahan status auth. + const authCheckInterval = setInterval(() => { + const newAuth = getAuth(); + if (typeof newAuth === 'object') { + isLoggedIn = true; + auth = newAuth; // Update nilai auth setelah login + clearInterval(authCheckInterval); + } + }, 500); // Periksa status login setiap 500ms + + await new Promise((resolve) => { + const checkLogin = setInterval(() => { + if (isLoggedIn) { + clearInterval(checkLogin); + resolve(null); + } + }, 500); + }); + } + if (!validQuantity(quantity)) return; + + updateItemCart({ + productId: variant, + quantity, + programLineId: null, + selected: true, + source: 'buy', + }); + router.push('/shop/quotation?source=buy'); + }; + const variantSectionRef = useRef(null); const goToVariantSection = () => { if (variantSectionRef.current) { @@ -160,87 +241,45 @@ const ProductDesktopVariant = ({ <Image src={product.image + '?variant=True'} alt={product.name} - className='h-[430px] object-contain object-center w-full border border-gray_r-4' + className='w-full h-[350px]' /> </div> - <div className='w-7/12 px-4'> + <div className='w-7/12 px-6'> <h1 className='text-title-md leading-10 font-medium'> {product?.name} </h1> <div className='mt-10'> - <div className='flex p-3'> - <div className='w-4/12 text-gray_r-12/70'>Nomor SKU</div> - <div className='w-8/12'>SKU-{product.id}</div> - </div> <div className='flex p-3 bg-gray_r-4'> - <div className='w-4/12 text-gray_r-12/70'>Part Number</div> - <div className='w-8/12'>{product.code || '-'}</div> + <div className='w-4/12 text-gray_r-12/70'>Item Code</div> + <div className='w-8/12'>{product.code}</div> </div> - <div className='flex p-3'> + <div className='flex p-3 items-center '> <div className='w-4/12 text-gray_r-12/70'>Manufacture</div> <div className='w-8/12'> - {product.manufacture?.name ? ( - <Link - href={createSlug( - '/shop/brands/', - product.manufacture?.name, - product.manufacture?.id - )} - > - {product.manufacture?.name} - </Link> - ) : ( - <div>-</div> - )} - </div> - </div> - - <div className='flex p-3 items-center bg-gray_r-4'> - <div className='w-4/12 text-gray_r-12/70'> - Persiapan Barang - </div> - <div className='w-8/12'> - {!product?.sla && <Skeleton width='20%' height='16px' />} - {product?.sla && ( - <Tooltip - placement='top' - label={`Masa Persiapan Barang ${product?.sla?.slaDate}`} - > - <Box className='w-fit flex items-center gap-x-2'> - {product?.sla?.slaDate} - <Info size={16} /> - </Box> - </Tooltip> - )} + <Link + href={createSlug( + '/shop/brands/', + product.manufacture.name, + product.manufacture.id.toString() + )} + > + {product?.manufacture.logo ? ( + <Image + width={100} + src={product.manufacture.logo} + alt={product.manufacture.name} + /> + ) : ( + <p className='font-bold text-red-500'> + {product.manufacture.name} + </p> + )} + </Link> </div> </div> - <div className='flex p-3'> - <div className='w-4/12 text-gray_r-12/70'>Stock</div> - <div className='w-8/12'> - {!product?.sla && <Skeleton width='10%' height='16px' />} - {product?.sla?.qty > 0 && <span>{product?.sla?.qty}</span>} - {product?.sla?.qty == 0 && ( - <a - href={whatsappUrl('product', { - name: product.name, - manufacture: product?.manufacture?.name, - url: createSlug( - '/shop/product/', - product.name, - product.id, - true - ), - })} - className='text-danger-500 font-medium' - > - Tanya Admin - </a> - )} - </div> - </div> - <div className='flex p-3 bg-gray_r-4'> + <div className='flex p-3 bg-gray_r-4 '> <div className='w-4/12 text-gray_r-12/70'>Berat Barang</div> <div className='w-8/12'> {product?.weight > 0 && <span>{product?.weight} KG</span>} @@ -262,24 +301,55 @@ const ProductDesktopVariant = ({ )} </div> </div> + <div className='flex p-3 items-center '> + <div className='w-4/12 text-gray_r-12/70'>Terjual</div> + <div className='w-8/12'>-</div> + </div> + + <div className='flex p-3 items-center bg-gray_r-4 '> + <div className='w-4/12 text-gray_r-12/70'> + Persiapan Barang + </div> + <div className='w-8/12'> + {!product?.sla && <Skeleton width='20%' height='16px' />} + {product?.sla && ( + <Tooltip + placement='top' + label={`Masa Persiapan Barang ${product?.sla?.slaDate}`} + > + <Box className='w-fit flex items-center gap-x-2'> + {product?.sla?.slaDate} + <Info size={16} /> + </Box> + </Tooltip> + )} + </div> + </div> </div> </div> - <div className='p-4 md:p-6 md:bg-gray-50 rounded-xl'> - <h2 className='text-h-md md:text-h-lg font-medium'>Informasi Produk</h2> - <div className='h-4' /> - <div - className='leading-relaxed text-gray-700' - dangerouslySetInnerHTML={{ - __html: - !product.parent.description || product.parent.description == '<p><br></p>' - ? 'Belum ada deskripsi' - : product.parent.description, - }} - /> + <div className='p-4 md:p-6 w-full'> + <ProductPromoSection product={product} productId={product.id} /> + + <div className='p-4 md:p-6 md:bg-gray-50 rounded-xl'> + <h2 className='text-h-md md:text-h-lg font-medium'> + Informasi Produk + </h2> + <div className='h-4' /> + <div + className='leading-relaxed text-gray-700' + dangerouslySetInnerHTML={{ + __html: + !product.parent.description || + product.parent.description == '<p><br></p>' + ? 'Belum ada deskripsi' + : product.parent.description, + }} + /> + </div> </div> </div> - <div className='w-[25%]'> + <div className='w-[33%]'> {product?.isFlashsale > 0 && product?.price?.discountPercentage > 0 ? ( <> @@ -337,46 +407,143 @@ const ProductDesktopVariant = ({ )} </h3> )} - <div className='flex gap-x-3 mt-4'> - <input - type='number' - className='form-input w-16 py-2 text-center bg-gray_r-1' - ref={setVariantQuantityRef(product.id)} - defaultValue={1} - /> - <button - type='button' + <div className='flex justify-between items-center py-5 px-3'> + <div className='relative flex items-center'> + <button + type='button' + className='absolute left-0 px-2 py-1 h-full text-gray-500' + onClick={() => + setQuantityInput( + String(Math.max(1, Number(quantityInput) - 1)) + ) + } + > + - + </button> + <input + type='number' + id='quantity' + min={1} + value={quantityInput} + onChange={(e) => setQuantityInput(e.target.value)} + className=' w-24 h-10 text-center border border-gray-300 rounded focus:outline-none' + /> + <button + type='button' + className='absolute right-0 px-2 py-1 h-full text-gray-500' + onClick={() => + setQuantityInput(String(Number(quantityInput) + 1)) + } + > + + + </button> + </div> + <div> + <Skeleton + isLoaded={!isLoadingSLA} + h='21px' + // w={16} + className={ + product?.sla?.qty < 10 ? 'text-red-600 font-medium' : '' + } + > + Stock : {product?.sla?.qty}{' '} + </Skeleton> + </div> + <div> + {product?.sla?.qty > 0 && ( + <Link href='/panduan-pick-up-service' className='group'> + <Image + src='/images/PICKUP-NOW.png' + className='group-hover:scale-105 transition-transform duration-200 w-28' + alt='pickup now' + /> + </Link> + )} + </div> + </div> + <div className='flex gap-x-3'> + <Button onClick={() => handleAddToCart(product.id)} - className='flex-1 py-2 btn-yellow' + className='w-full' + colorScheme='yellow' > Keranjang - </button> - <button - type='button' + </Button> + <Button onClick={() => handleBuy(product.id)} - className='flex-1 py-2 btn-solid-red' + className='w-full' + colorScheme='red' > Beli - </button> + </Button> </div> - <div className='flex mt-4'> - <button - className='flex items-center gap-x-1' - onClick={toggleWishlist} - > - {wishlist.data?.productTotal > 0 ? ( - <HeartIcon className='w-6 fill-danger-500 text-danger-500' /> - ) : ( - <HeartIcon className='w-6' /> - )} - Wishlist - </button> + <Button + onClick={() => handleButton(product.id)} + color={'red'} + colorScheme='white' + className='w-full border-2 p-2 gap-1 mt-2 hover:bg-slate-100 flex items-center' + > + <ImageNext + src='/images/writing.png' + alt='penawaran instan' + className='' + width={25} + height={25} + /> + Penawaran Harga Instan + </Button> + <div className='flex py-5'> + <div className='flex gap-x-5 items-center justify-center'> + <Button + as={Link} + href={createdAskUrl} + variant='link' + target='_blank' + colorScheme='gray' + leftIcon={<MessageCircleIcon size={18} />} + > + Ask Admin + </Button> + + <span>|</span> + + <button + className='flex items-center gap-x-1' + onClick={toggleWishlist} + > + {wishlist.data?.productTotal > 0 ? ( + <HeartIcon className='w-6 fill-danger-500 text-danger-500' /> + ) : ( + <HeartIcon className='w-6' /> + )} + Wishlist + </button> + + <span>|</span> + + <RWebShare + data={{ + text: 'Check out this product', + title: `${product.name} - Indoteknik.com`, + url: SELF_HOST + router.asPath, + }} + > + <Button + variant='link' + colorScheme='gray' + leftIcon={<Share2Icon size={18} />} + > + Share + </Button> + </RWebShare> + </div> </div> <div className='border border-gray_r-6 overflow-auto mt-4'> <div className='font-medium text-center p-4 bg-gray_r-1 border-b border-gray_r-6 sticky top-0 z-10'> Produk Serupa </div> - <div className='h-full divide-y divide-gray_r-6 max-h-96'> + <div className='h-full divide-y divide-gray_r-6 max-h-[500px]'> {productSimilarInBrand && productSimilarInBrand?.map((product) => ( <div className='py-2' key={product.id}> @@ -393,8 +560,11 @@ const ProductDesktopVariant = ({ Kamu Mungkin Juga Suka </div> <LazyLoad> - <ProductSimilar query={productSimilarQuery} /> + <SimilarBottom product={product} /> </LazyLoad> + {/* <LazyLoad> + <ProductSimilar query={productSimilarQuery} /> + </LazyLoad> */} </div> <BottomPopup @@ -429,8 +599,11 @@ const ProductDesktopVariant = ({ Kamu Mungkin Juga Suka </div> <LazyLoad> - <ProductSimilar query={productSimilarQuery} /> + <SimilarBottom product={product} /> </LazyLoad> + {/* <LazyLoad> + <ProductSimilar query={productSimilarQuery} /> + </LazyLoad> */} </div> </BottomPopup> </div> diff --git a/src/lib/product/components/Product/ProductMobileVariant.jsx b/src/lib/product/components/Product/ProductMobileVariant.jsx index af9e52bb..de5c3f10 100644 --- a/src/lib/product/components/Product/ProductMobileVariant.jsx +++ b/src/lib/product/components/Product/ProductMobileVariant.jsx @@ -1,10 +1,10 @@ -import { Skeleton } from '@chakra-ui/react'; +import { Button, Skeleton } from '@chakra-ui/react'; import { HeartIcon } from '@heroicons/react/24/outline'; import { useRouter } from 'next/router'; import { useEffect, useState } from 'react'; import { toast } from 'react-hot-toast'; import LazyLoad from 'react-lazy-load'; - +import ImageNext from 'next/image'; import odooApi from '@/core/api/odooApi'; import Divider from '@/core/components/elements/Divider/Divider'; import Image from '@/core/components/elements/Image/Image'; @@ -16,12 +16,15 @@ import currencyFormat from '@/core/utils/currencyFormat'; import { gtagAddToCart } from '@/core/utils/googleTag'; import { createSlug } from '@/core/utils/slug'; import whatsappUrl from '@/core/utils/whatsappUrl'; - +import { getAuth } from '~/libs/auth'; +import SimilarBottom from '~/modules/product-detail/components/SimilarBottom'; import ProductSimilar from '../ProductSimilar'; const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => { const router = useRouter(); - + const { slug } = router.query; + const { srsltid } = router.query; + let auth = getAuth(); const [quantity, setQuantity] = useState('1'); const [selectedVariant, setSelectedVariant] = useState(product.id); const [informationTab, setInformationTab] = useState( @@ -73,11 +76,16 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => { return isValid; }; - const handleClickCart = () => { + const handleClickCart = async () => { + if (!auth) { + router.push(`/login?next=/shop/product/${slug}?srsltid=${srsltid}`); + return; + } + if (!validAction()) return; gtagAddToCart(activeVariant, quantity); updateItemCart({ - productId: variant, + productId: product.id, quantity, programLineId: null, selected: true, @@ -86,7 +94,33 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => { setAddCartAlert(true); }; - const handleClickBuy = () => { + const handleClickBuy = async () => { + let isLoggedIn = typeof auth === 'object'; + + if (!isLoggedIn) { + const currentUrl = encodeURIComponent(router.asPath); + await router.push(`/login?next=${currentUrl}`); + + // Tunggu login berhasil, misalnya dengan memantau perubahan status auth. + const authCheckInterval = setInterval(() => { + const newAuth = getAuth(); + if (typeof newAuth === 'object') { + isLoggedIn = true; + auth = newAuth; // Update nilai auth setelah login + clearInterval(authCheckInterval); + } + }, 500); // Periksa status login setiap 500ms + + await new Promise((resolve) => { + const checkLogin = setInterval(() => { + if (isLoggedIn) { + clearInterval(checkLogin); + resolve(null); + } + }, 500); + }); + } + if (!validAction()) return; updateItemCart({ @@ -99,6 +133,20 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => { router.push(`/shop/checkout?source=buy`); }; + const handleButton = (variant) => { + const quantity = quantityInput; + if (!validQuantity(quantity)) return; + + updateItemCart({ + productId: variant, + quantity, + programLineId: null, + selected: true, + source: 'buy', + }); + router.push('/shop/quotation?source=buy'); + }; + const productSimilarQuery = [ product?.name, `fq=-product_id_i:${product.id}`, @@ -120,42 +168,14 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => { return ( <MobileView> - <Image - src={product.image + '?variant=True'} - alt={product.name} - className='h-72 object-contain object-center w-full border-b border-gray_r-4' - /> - - <div className='p-4'> - <div className='flex items-end mb-2'> - {product.manufacture?.name ? ( - <Link - href={createSlug( - '/shop/brands/', - product.manufacture?.name, - product.manufacture?.id - )} - > - {product.manufacture?.name} - </Link> - ) : ( - <div>-</div> - )} - <button type='button' className='ml-auto' onClick={toggleWishlist}> - {wishlist.data?.productTotal > 0 ? ( - <HeartIcon className='w-6 fill-danger-500 text-danger-500' /> - ) : ( - <HeartIcon className='w-6' /> - )} - </button> - </div> - <h1 className='leading-6 font-medium mb-3'>{activeVariant?.name}</h1> - + <div + className={`px-4 block md:sticky md:top-[150px] md:py-6 fixed bottom-0 left-0 right-0 bg-white p-2 z-10 pb-6 pt-6 rounded-lg shadow-[rgba(0,0,4,0.1)_0px_-4px_4px_0px] `} + > {activeVariant.isFlashSale && activeVariant?.price?.discountPercentage > 0 ? ( <> <div className='flex gap-x-1 items-center'> - <div className='badge-solid-red'> + <div className='bg-danger-500 px-2 py-1.5 rounded text-white text-caption-2'> {activeVariant?.price?.discountPercentage}% </div> <div className='text-gray_r-11 line-through text-caption-1'> @@ -173,7 +193,7 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => { </div> </> ) : ( - <h3 className='text-danger-500 font-semibold mt-1'> + <div className='text-danger-500 font-semibold mt-1 text-3xl'> {activeVariant?.price?.price > 0 ? ( <> {currencyFormat(activeVariant?.price?.price)} @@ -203,39 +223,84 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => { </a> </span> )} - </h3> + </div> )} + <div className=''> + <div className='mt-4 mb-2'>Jumlah</div> + <div className='flex gap-x-3'> + <div className='w-2/12'> + <input + name='quantity' + type='number' + className='form-input' + value={quantity} + onChange={(e) => setQuantity(e.target.value)} + /> + </div> + <button + type='button' + className='btn-yellow flex-1' + onClick={handleClickCart} + > + Keranjang + </button> + <button + type='button' + className='btn-solid-red flex-1' + onClick={handleClickBuy} + > + Beli + </button> + </div> + <Button + onClick={() => handleButton(product.id)} + color={'red'} + colorScheme='white' + className='w-full border-2 p-2 gap-1 mt-2 hover:bg-slate-100 flex items-center' + > + <ImageNext + src='/images/writing.png' + alt='penawaran instan' + className='' + width={25} + height={25} + /> + Penawaran Harga Instan + </Button> + </div> </div> - - <Divider /> + <Image + src={product.image + '?variant=True'} + alt={product.name} + className='h-72 object-contain object-center w-full border-b border-gray_r-4' + /> <div className='p-4'> - <div className='mt-4 mb-2'>Jumlah</div> - <div className='flex gap-x-3'> - <div className='w-2/12'> - <input - name='quantity' - type='number' - className='form-input' - value={quantity} - onChange={(e) => setQuantity(e.target.value)} - /> - </div> - <button - type='button' - className='btn-yellow flex-1' - onClick={handleClickCart} - > - Keranjang - </button> - <button - type='button' - className='btn-solid-red flex-1' - onClick={handleClickBuy} - > - Beli + <div className='flex items-end mb-2'> + {product.manufacture?.name ? ( + <Link + href={createSlug( + '/shop/brands/', + product.manufacture?.name, + product.manufacture?.id + )} + > + {product.manufacture?.name} + </Link> + ) : ( + <div>-</div> + )} + <button type='button' className='ml-auto' onClick={toggleWishlist}> + {wishlist.data?.productTotal > 0 ? ( + <HeartIcon className='w-6 fill-danger-500 text-danger-500' /> + ) : ( + <HeartIcon className='w-6' /> + )} </button> </div> + <h1 className='font-medium text-h-lg leading-8 md:text-title-md md:leading-10 mb-3'> + {activeVariant?.name} + </h1> </div> <Divider /> @@ -375,8 +440,11 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => { <div className='p-4'> <h2 className='font-semibold mb-4'>Kamu Mungkin Juga Suka</h2> <LazyLoad> - <ProductSimilar query={productSimilarQuery} /> + <SimilarBottom product={product} /> </LazyLoad> + {/* <LazyLoad> + <ProductSimilar query={productSimilarQuery} /> + </LazyLoad> */} </div> <BottomPopup @@ -409,8 +477,11 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => { Kamu Mungkin Juga Suka </div> <LazyLoad> - <ProductSimilar query={productSimilarQuery} /> + <SimilarBottom product={product} /> </LazyLoad> + {/* <LazyLoad> + <ProductSimilar query={productSimilarQuery} /> + </LazyLoad> */} </div> </BottomPopup> </MobileView> diff --git a/src/lib/product/components/ProductCard.jsx b/src/lib/product/components/ProductCard.jsx index d3b50302..3e6a6913 100644 --- a/src/lib/product/components/ProductCard.jsx +++ b/src/lib/product/components/ProductCard.jsx @@ -10,12 +10,13 @@ import { sellingProductFormat } from '@/core/utils/formatValue'; import { createSlug } from '@/core/utils/slug'; import whatsappUrl from '@/core/utils/whatsappUrl'; import useUtmSource from '~/hooks/useUtmSource'; +import useDevice from '@/core/hooks/useDevice'; const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { const router = useRouter(); const utmSource = useUtmSource(); const [discount, setDiscount] = useState(0); - + const { isDesktop, isMobile } = useDevice(); let voucherPastiHemat = 0; voucherPastiHemat = product?.newVoucherPastiHemat[0]; @@ -26,9 +27,13 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { }); const image = useMemo(() => { - if (product.image) return product.image + '?ratio=square'; - return '/images/noimage.jpeg'; - }, [product.image]); + if (!isDesktop && product.image_mobile) { + return product.image_mobile + '?ratio=square'; + } else { + if (product.image) return product.image + '?ratio=square'; + return '/images/noimage.jpeg'; + } + }, [product.image, product.image_mobile]); const URL = { product: @@ -143,7 +148,7 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { <div className='p-2 sm:p-3 pb-3 text-caption-2 sm:text-body-2 leading-5'> <div className='flex justify-between '> {product?.manufacture?.name ? ( - <Link href={URL.manufacture} className='mb-1 mt-1'> + <Link href={URL.manufacture} className='mb-1 mt-1 truncate'> {product.manufacture.name} </Link> ) : ( diff --git a/src/lib/product/components/ProductFilter.jsx b/src/lib/product/components/ProductFilter.jsx index d52fcb90..947550b7 100644 --- a/src/lib/product/components/ProductFilter.jsx +++ b/src/lib/product/components/ProductFilter.jsx @@ -1,88 +1,96 @@ -import BottomPopup from '@/core/components/elements/Popup/BottomPopup' -import { useRouter } from 'next/router' -import { useState } from 'react' -import _ from 'lodash' -import { toQuery } from 'lodash-contrib' -import { Checkbox } from '@chakra-ui/react' +import BottomPopup from '@/core/components/elements/Popup/BottomPopup'; +import { useRouter } from 'next/router'; +import { useState } from 'react'; +import _ from 'lodash'; +import { toQuery } from 'lodash-contrib'; +import { Checkbox } from '@chakra-ui/react'; const orderOptions = [ { value: 'price-asc', label: 'Harga Terendah' }, { value: 'price-desc', label: 'Harga Tertinggi' }, { value: 'popular', label: 'Populer' }, - { value: 'stock', label: 'Ready Stock' } -] + { value: 'stock', label: 'Ready Stock' }, +]; -const ProductFilter = ({ active, close, brands, categories, prefixUrl, defaultBrand = null }) => { - const router = useRouter() - const { query } = router - const [order, setOrder] = useState(query?.orderBy || 'popular') - const [brand, setBrand] = useState(query?.brand) - const [category, setCategory] = useState(query?.category) - const [priceFrom, setPriceFrom] = useState(query?.priceFrom) - const [priceTo, setPriceTo] = useState(query?.priceTo) +const ProductFilter = ({ + active, + close, + brands, + categories, + prefixUrl, + defaultBrand = null, +}) => { + const router = useRouter(); + const { query } = router; + const [order, setOrder] = useState(query?.orderBy || 'popular'); + const [brand, setBrand] = useState(query?.brand); + const [category, setCategory] = useState(query?.category); + const [priceFrom, setPriceFrom] = useState(query?.priceFrom); + const [priceTo, setPriceTo] = useState(query?.priceTo); - const [stock, setStock] = useState(query?.stock) + const [stock, setStock] = useState(query?.stock); - const [activeRange, setActiveRange] = useState(null) + const [activeRange, setActiveRange] = useState(null); const priceRange = [ { priceFrom: 100000, - priceTo: 200000 + priceTo: 200000, }, { priceFrom: 200000, - priceTo: 300000 + priceTo: 300000, }, { priceFrom: 300000, - priceTo: 400000 + priceTo: 400000, }, { priceFrom: 400000, - priceTo: 500000 - } - ] + priceTo: 500000, + }, + ]; const handlePriceFromChange = async (priceFromr, priceTor, index) => { - await setPriceFrom(priceFromr) - await setPriceTo(priceTor) - setActiveRange(index) - } + await setPriceFrom(priceFromr); + await setPriceTo(priceTor); + setActiveRange(index); + }; const handleReadyStockChange = (event) => { - const value = event.target.value - const isChecked = event.target.checked + const value = event.target.value; + const isChecked = event.target.checked; if (isChecked) { - setStock(value) + setStock(value); } else { - setStock(null) + setStock(null); } - } + }; const handleSubmit = () => { let params = { + penawaran: router.query.penawaran, q: router.query.q, orderBy: order, brand, category, priceFrom, priceTo, - stock: stock - } - params = _.pickBy(params, _.identity) - params = toQuery(params) - router.push(`${prefixUrl}?${params}`) - } + stock: stock, + }; + params = _.pickBy(params, _.identity); + params = toQuery(params); + router.push(`${prefixUrl}?${params}`); + }; const formatCurrency = (value) => { if (value >= 1000) { - const thousands = Math.floor(value / 1000) // Menghitung ribuan - return `Rp${thousands}k` + const thousands = Math.floor(value / 1000); // Menghitung ribuan + return `Rp${thousands}k`; } else { - return `Rp${value}` + return `Rp${value}`; } - } + }; return ( <BottomPopup active={active} close={close} title='Filter Produk'> @@ -101,7 +109,10 @@ const ProductFilter = ({ active, close, brands, categories, prefixUrl, defaultBr <option value=''>Pilih Brand...</option> {brands.map((brand, index) => ( <option value={brand.brand} key={index}> - {brand.brand} <span className='text-sm text-gray-200'>({brand.qty})</span> + {brand.brand}{' '} + <span className='text-sm text-gray-200'> + ({brand.qty}) + </span> </option> ))} </> @@ -125,7 +136,10 @@ const ProductFilter = ({ active, close, brands, categories, prefixUrl, defaultBr <option value=''>Pilih Kategori...</option> {categories.map((category, index) => ( <option value={category.name} key={index}> - {category.name} <span className='text-sm text-gray-200'>({category.qty})</span> + {category.name}{' '} + <span className='text-sm text-gray-200'> + ({category.qty}) + </span> </option> ))} </> @@ -141,7 +155,9 @@ const ProductFilter = ({ active, close, brands, categories, prefixUrl, defaultBr <button key={orderOption.value} className={`btn-light px-3 font-normal flex-shrink-0 ${ - order == orderOption.value ? 'bg-warning-500' : 'bg-transparent' + order == orderOption.value + ? 'bg-warning-500' + : 'bg-transparent' }`} onClick={() => setOrder(orderOption.value)} > @@ -173,13 +189,16 @@ const ProductFilter = ({ active, close, brands, categories, prefixUrl, defaultBr {priceRange.map((price, i) => ( <button key={i} - onClick={() => handlePriceFromChange(price.priceFrom, price.priceTo, i)} + onClick={() => + handlePriceFromChange(price.priceFrom, price.priceTo, i) + } className={`w-full border ${ i === activeRange ? 'border-red-600' : 'border-gray-400' } py-2 p-3 rounded-full text-sm whitespace-nowrap`} > - {formatCurrency(price.priceFrom)} - {formatCurrency(price.priceTo)} + {formatCurrency(price.priceFrom)} -{' '} + {formatCurrency(price.priceTo)} </button> ))} </div> @@ -197,12 +216,16 @@ const ProductFilter = ({ active, close, brands, categories, prefixUrl, defaultBr </Checkbox> </div> </div> */} - <button type='button' className='btn-solid-red w-full mt-2' onClick={handleSubmit}> + <button + type='button' + className='btn-solid-red w-full mt-2' + onClick={handleSubmit} + > Terapkan Filter </button> </div> </BottomPopup> - ) -} + ); +}; -export default ProductFilter +export default ProductFilter; diff --git a/src/lib/product/components/ProductFilterDesktop.jsx b/src/lib/product/components/ProductFilterDesktop.jsx index 73fecab5..d2ecb4d9 100644 --- a/src/lib/product/components/ProductFilterDesktop.jsx +++ b/src/lib/product/components/ProductFilterDesktop.jsx @@ -1,7 +1,7 @@ -import { useRouter } from 'next/router' -import { useEffect, useState } from 'react' -import _ from 'lodash' -import { toQuery } from 'lodash-contrib' +import { useRouter } from 'next/router'; +import { useEffect, useState } from 'react'; +import _ from 'lodash'; +import { toQuery } from 'lodash-contrib'; import { Accordion, AccordionButton, @@ -15,110 +15,119 @@ import { InputGroup, InputLeftAddon, Stack, - VStack -} from '@chakra-ui/react' -import Image from '@/core/components/elements/Image/Image' -import { formatCurrency } from '@/core/utils/formatValue' + VStack, +} from '@chakra-ui/react'; +import Image from '@/core/components/elements/Image/Image'; +import { formatCurrency } from '@/core/utils/formatValue'; -const ProductFilterDesktop = ({ brands, categories, prefixUrl, defaultBrand = null }) => { - - - const router = useRouter() - const { query } = router - const [order, setOrder] = useState(query?.orderBy) - const [brandValues, setBrand] = useState(query?.brand?.split(',') || []) - const [categoryValues, setCategory] = useState(query?.category?.split(',') || []) - const [priceFrom, setPriceFrom] = useState(query?.priceFrom) - const [priceTo, setPriceTo] = useState(query?.priceTo) - const [stock, setStock] = useState(query?.stock) - const [activeRange, setActiveRange] = useState(null) - const [activeIndeces, setActiveIndeces] = useState([]) +const ProductFilterDesktop = ({ + brands, + categories, + prefixUrl, + defaultBrand = null, +}) => { + const router = useRouter(); + const { query } = router; + const [order, setOrder] = useState(query?.orderBy); + const [brandValues, setBrand] = useState(query?.brand?.split(',') || []); + const [categoryValues, setCategory] = useState( + query?.category?.split(',') || [] + ); + const [priceFrom, setPriceFrom] = useState(query?.priceFrom); + const [priceTo, setPriceTo] = useState(query?.priceTo); + const [stock, setStock] = useState(query?.stock); + const [activeRange, setActiveRange] = useState(null); + const [activeIndeces, setActiveIndeces] = useState([]); const priceRange = [ { priceFrom: 100000, - priceTo: 200000 + priceTo: 200000, }, { priceFrom: 200000, - priceTo: 300000 + priceTo: 300000, }, { priceFrom: 300000, - priceTo: 400000 + priceTo: 400000, }, { priceFrom: 400000, - priceTo: 500000 - } - ] + priceTo: 500000, + }, + ]; const indexRange = priceRange.findIndex((range) => { - return range.priceFrom === parseInt(priceFrom) && range.priceTo == parseInt(priceTo) - }) + return ( + range.priceFrom === parseInt(priceFrom) && + range.priceTo == parseInt(priceTo) + ); + }); const handleCategoriesChange = (event) => { - const value = event.target.value - const isChecked = event.target.checked + const value = event.target.value; + const isChecked = event.target.checked; if (isChecked) { - setCategory([...categoryValues, value]) + setCategory([...categoryValues, value]); } else { - setCategory(categoryValues.filter((val) => val !== value)) + setCategory(categoryValues.filter((val) => val !== value)); } - } + }; const handleBrandsChange = (event) => { - const value = event.target.value - const isChecked = event.target.checked + const value = event.target.value; + const isChecked = event.target.checked; if (isChecked) { - setBrand([...brandValues, value]) + setBrand([...brandValues, value]); } else { - setBrand(brandValues.filter((val) => val !== value)) + setBrand(brandValues.filter((val) => val !== value)); } - } + }; const handleReadyStockChange = (event) => { - const value = event.target.value - const isChecked = event.target.checked + const value = event.target.value; + const isChecked = event.target.checked; if (isChecked) { - setStock(value) + setStock(value); } else { - setStock(null) + setStock(null); } - } + }; const handlePriceFromChange = async (priceFromr, priceTor, index) => { - await setPriceFrom(priceFromr) - await setPriceTo(priceTor) - setActiveRange(index) - } + await setPriceFrom(priceFromr); + await setPriceTo(priceTor); + setActiveRange(index); + }; const handleSubmit = () => { let params = { + penawaran: router.query.penawaran, q: router.query.q, orderBy: order, brand: brandValues.join(','), category: categoryValues.join(','), priceFrom, priceTo, - stock: stock - } - params = _.pickBy(params, _.identity) - params = toQuery(params) + stock: stock, + }; + params = _.pickBy(params, _.identity); + params = toQuery(params); - const slug = Array.isArray(router.query.slug) ? router.query.slug[0] : router.query.slug; + const slug = Array.isArray(router.query.slug) + ? router.query.slug[0] + : router.query.slug; if (slug) { - if(prefixUrl.includes('category') || prefixUrl.includes('lob')){ - router.push(`${prefixUrl}?${params}`) - }else{ - router.push(`${prefixUrl}/${slug}?${params}`) + if (prefixUrl.includes('category') || prefixUrl.includes('lob')) { + router.push(`${prefixUrl}?${params}`); + } else { + router.push(`${prefixUrl}/${slug}?${params}`); } } else { - router.push(`${prefixUrl}?${params}`) + router.push(`${prefixUrl}?${params}`); } - } - - + }; /*const handleIndexAccordion = async () => { if (brandValues) { @@ -136,9 +145,8 @@ const ProductFilterDesktop = ({ brands, categories, prefixUrl, defaultBrand = nu }*/ useEffect(() => { - setActiveRange(indexRange) - }, []) - + setActiveRange(indexRange); + }, []); return ( <> @@ -165,13 +173,17 @@ const ProductFilterDesktop = ({ brands, categories, prefixUrl, defaultBrand = nu > <div className='flex items-center gap-2'> <span>{brand.brand} </span> - <span className='text-sm text-gray-600'>({brand.qty})</span> + <span className='text-sm text-gray-600'> + ({brand.qty}) + </span> </div> </Checkbox> </div> )) ) : ( - <div className='flex items-center gap-2'>Brands tidak tersedia</div> + <div className='flex items-center gap-2'> + Brands tidak tersedia + </div> )} </Stack> </AccordionPanel> @@ -199,13 +211,17 @@ const ProductFilterDesktop = ({ brands, categories, prefixUrl, defaultBrand = nu > <div className='flex items-center gap-2'> <span>{category.name} </span> - <span className='text-sm text-gray-600'>({category.qty})</span> + <span className='text-sm text-gray-600'> + ({category.qty}) + </span> </div> </Checkbox> </div> )) ) : ( - <div className='flex items-center gap-2'>Kategori tidak tersedia</div> + <div className='flex items-center gap-2'> + Kategori tidak tersedia + </div> )} </Stack> </AccordionPanel> @@ -243,13 +259,16 @@ const ProductFilterDesktop = ({ brands, categories, prefixUrl, defaultBrand = nu {priceRange.map((price, i) => ( <button key={i} - onClick={() => handlePriceFromChange(price.priceFrom, price.priceTo, i)} + onClick={() => + handlePriceFromChange(price.priceFrom, price.priceTo, i) + } className={`w-full border ${ i === activeRange ? 'border-red-600' : 'border-gray-400' } py-2 p-3 rounded-full text-sm whitespace-nowrap`} > - {formatCurrency(price.priceFrom)} - {formatCurrency(price.priceTo)} + {formatCurrency(price.priceFrom)} -{' '} + {formatCurrency(price.priceTo)} </button> ))} </div> @@ -282,7 +301,7 @@ const ProductFilterDesktop = ({ brands, categories, prefixUrl, defaultBrand = nu Terapkan </Button> </> - ) -} + ); +}; -export default ProductFilterDesktop +export default ProductFilterDesktop; diff --git a/src/lib/product/components/ProductSearch.jsx b/src/lib/product/components/ProductSearch.jsx index 26114acf..f7b044aa 100644 --- a/src/lib/product/components/ProductSearch.jsx +++ b/src/lib/product/components/ProductSearch.jsx @@ -79,6 +79,24 @@ const ProductSearch = ({ } }, [categoryId]); + useEffect(() => { + const checkIfPenawaran = async () => { + if (router.asPath.includes('penawaran')) { + query = { + ...query, + fq: [ + `-flashsale_id_i:${router.query.penawaran}`, + `flashsale_price_f:[1 TO *]`, + ], + orderBy: 'flashsale-discount-desc', + }; + setFinalQuery(query); + setOrderBy('flashsale-discount-desc'); + } + }; + checkIfPenawaran(); + }, [router.query]); + const collectIds = (category) => { const ids = []; function recurse(cat) { @@ -337,6 +355,7 @@ const ProductSearch = ({ const handleDeleteFilter = async (source, value) => { let params = { + penawaran: router.query.penawaran, q: router.query.q, orderBy: orderBy, brand: brandValues.join(','), @@ -364,6 +383,7 @@ const ProductSearch = ({ break; case 'delete': params = { + penawaran: router.query.penawaran, q: router.query.q, orderBy: orderBy, }; diff --git a/src/lib/quotation/components/Quotation.jsx b/src/lib/quotation/components/Quotation.jsx index cf0ad41f..5a2f63a5 100644 --- a/src/lib/quotation/components/Quotation.jsx +++ b/src/lib/quotation/components/Quotation.jsx @@ -39,9 +39,12 @@ const { getProductsCheckout } = require('@/lib/checkout/api/checkoutApi'); const Quotation = () => { const router = useRouter(); const auth = useAuth(); + const query = router.query.source ?? null; const { data: cartCheckout } = useQuery('cartCheckout', () => - getProductsCheckout() + getProductsCheckout({ + source: query, + }) ); const { setRefreshCart } = useProductCartContext(); diff --git a/src/lib/review/components/CustomerReviews.jsx b/src/lib/review/components/CustomerReviews.jsx index a6e697f0..6ca0fa7b 100644 --- a/src/lib/review/components/CustomerReviews.jsx +++ b/src/lib/review/components/CustomerReviews.jsx @@ -3,16 +3,37 @@ import MobileView from '@/core/components/views/MobileView'; import Image from 'next/image'; import { Swiper, SwiperSlide } from 'swiper/react'; import { Autoplay } from 'swiper'; +import { useEffect, useState } from 'react'; const { useQuery } = require('react-query'); const { getCustomerReviews } = require('../api/customerReviewsApi'); const CustomerReviews = () => { - const { data: customerReviews } = useQuery( + const [data, setData] = useState(null); + + useEffect(() => { + const localData = localStorage.getItem('Homepage_customerReviews'); + if (localData) { + setData(JSON.parse(localData)); + } + },[]) + + + const { data: fetchCustomerReviews } = useQuery( 'customerReviews', - getCustomerReviews + getCustomerReviews,{ + enabled: !data, + onSuccess: (data) => { + if (data) { + localStorage.setItem('Homepage_customerReviews', JSON.stringify(data)); + setData(data); + } + } + } ); + const customerReviews = data + return ( <div className='px-4 sm:px-0'> <h1 className='font-semibold text-[14px] sm:text-h-lg mb-4'> diff --git a/src/lib/shipment/components/Shipments.jsx b/src/lib/shipment/components/Shipments.jsx index 115bbd3a..20dbb013 100644 --- a/src/lib/shipment/components/Shipments.jsx +++ b/src/lib/shipment/components/Shipments.jsx @@ -1,62 +1,83 @@ -import DesktopView from '@/core/components/views/DesktopView' -import MobileView from '@/core/components/views/MobileView' -import Menu from '@/lib/auth/components/Menu' -import { EllipsisVerticalIcon, MagnifyingGlassIcon } from '@heroicons/react/24/outline' -import ImageNext from 'next/image' -import { useRouter } from 'next/router' -import { useQuery } from 'react-query' -import _, { forEach } from 'lodash-contrib' -import Spinner from '@/core/components/elements/Spinner/Spinner' -import Manifest from '@/lib/treckingAwb/component/Manifest' -import { useState } from 'react' -import Pagination from '@/core/components/elements/Pagination/Pagination' -import Link from 'next/link' -import TransactionStatusBadge from '@/lib/transaction/components/TransactionStatusBadge' +import DesktopView from '@/core/components/views/DesktopView'; +import MobileView from '@/core/components/views/MobileView'; +import Menu from '@/lib/auth/components/Menu'; +import { + EllipsisVerticalIcon, + MagnifyingGlassIcon, +} from '@heroicons/react/24/outline'; +import ImageNext from 'next/image'; +import { useEffect } from 'react'; +import { useRouter } from 'next/router'; +import { useQuery } from 'react-query'; +import _, { forEach } from 'lodash-contrib'; +import Spinner from '@/core/components/elements/Spinner/Spinner'; +import Manifest from '@/lib/treckingAwb/component/Manifest'; +import { useState } from 'react'; +import Pagination from '@/core/components/elements/Pagination/Pagination'; +import Link from 'next/link'; +import TransactionStatusBadge from '@/lib/transaction/components/TransactionStatusBadge'; -const { listShipments } = require('../api/listShipment') +const { listShipments } = require('../api/listShipment'); const Shipments = () => { - const router = useRouter() - const { q = '', page = 1 } = router.query - const [paramStatus, setParamStatus] = useState(null) - - const limit = 15 + const router = useRouter(); + const { q = '', page = 1, status = null } = router.query; + const [paramStatus, setParamStatus] = useState(status); + const limit = 15; const query = { q: q, status: paramStatus, offset: (page - 1) * limit, - limit - } - const [inputQuery, setInputQuery] = useState(q) - const queryString = _.toQuery(query) + limit, + }; + const [inputQuery, setInputQuery] = useState(q); + const queryString = _.toQuery(query); const { data: shipments } = useQuery('shipments' + queryString, () => listShipments({ query: queryString }) - ) - const [idAWB, setIdAWB] = useState(null) + ); + const [idAWB, setIdAWB] = useState(null); - const pageCount = Math.ceil(shipments?.pickingTotal / limit) - let pageQuery = _.omit(query, ['limit', 'offset', 'context']) - pageQuery = _.pickBy(pageQuery, _.identity) - pageQuery = _.toQuery(pageQuery) + const pageCount = Math.ceil(shipments?.pickingTotal / limit); + let pageQuery = _.omit(query, ['limit', 'offset', 'context']); + pageQuery = _.pickBy(pageQuery, _.identity); + pageQuery = _.toQuery(pageQuery); const closePopup = () => { - setIdAWB(null) - } + setIdAWB(null); + }; const handleSubmit = async (e) => { - e.preventDefault() - router.push(`${router.pathname}?q=${inputQuery}`) - } + e.preventDefault(); + router.push(`${router.pathname}?q=${inputQuery}`); + }; const filterStatus = async (status) => { if (status === paramStatus) { - setParamStatus(null) + setParamStatus(null); } else { - setParamStatus(status) + setParamStatus(status); } - } + }; + + useEffect(() => { + const resetQuery = () => { + const newQuery = { + status: paramStatus || undefined, + q: '', + page: 1, + }; + router.push({ + pathname: router.pathname, + query: newQuery, + }); + }; + + if (paramStatus !== status) { + resetQuery(); + } + }, [paramStatus]); return ( <> <MobileView> @@ -84,7 +105,10 @@ const Shipments = () => { </form> {shipments?.pickings.map((shipment) => ( - <div className='p-4 shadow border border-gray_r-3 rounded-md' key={shipment.id}> + <div + className='p-4 shadow border border-gray_r-3 rounded-md' + key={shipment.id} + > <div className='flex justify-between items-center mb-3'> <div className='text-caption-2 text-gray_r-11'> <p> @@ -93,7 +117,9 @@ const Shipments = () => { {shipment.carrierName || '-'} </span> </p> - <p className='mt-2'>No. Resi : {shipment.trackingNumber || '-'}</p> + <p className='mt-2'> + No. Resi : {shipment.trackingNumber || '-'} + </p> </div> <div className='flex justify-between'> {shipment?.status === 'completed' && ( @@ -116,11 +142,17 @@ const Shipments = () => { <hr /> <div className='flex justify-between mt-2 items-center mb-5'> <div> - <span className='text-caption-2 text-gray_r-11'>No. Transaksi</span> + <span className='text-caption-2 text-gray_r-11'> + No. Transaksi + </span> <Link href={`/my/transactions/${shipment.saleOrder.id}`}> - <h2 className='text-danger-500 mt-1 mb-2'>{shipment.saleOrder.name}</h2> + <h2 className='text-danger-500 mt-1 mb-2'> + {shipment.saleOrder.name} + </h2> </Link> - <span className='text-caption-2 text-gray_r-11'>{shipment.date}</span> + <span className='text-caption-2 text-gray_r-11'> + {shipment.date} + </span> </div> <div> <button @@ -136,7 +168,11 @@ const Shipments = () => { onClick={() => setIdAWB(shipment.id)} className='flex items-center mt-1 gap-x-1 min-w-full' > - <ImageNext src={`/images/BOX_DELIVERY_GREEN.svg`} width={20} height={20} /> + <ImageNext + src={`/images/BOX_DELIVERY_GREEN.svg`} + width={20} + height={20} + /> <p className='text-sm text-green-700 truncate'> {shipment.lastManifest.description} </p> @@ -148,7 +184,7 @@ const Shipments = () => { <Pagination pageCount={pageCount} currentPage={parseInt(page)} - url={router.pathname + pageQuery} + url={`${router.pathname}${pageQuery ? '?' + pageQuery : ''}`} className='mt-2 mb-2' /> </div> @@ -176,7 +212,8 @@ const Shipments = () => { <path d='M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z' /> </svg> <div> - Lacak pengiriman untuk setiap transaksi anda semakin mudah di Indoteknik.com + Lacak pengiriman untuk setiap transaksi anda semakin mudah di + Indoteknik.com </div> </div> <div className='flex justify-between gap-x-5'> @@ -190,7 +227,9 @@ const Shipments = () => { </div> <div className='p-4 bg-white border border-gray_r-6 rounded'> <div className='flex mb-6 items-center justify-between'> - <h1 className='text-title-sm font-semibold'>Detail Pengiriman</h1> + <h1 className='text-title-sm font-semibold'> + Detail Pengiriman + </h1> <form className='flex gap-x-2' onSubmit={handleSubmit}> <input type='text' @@ -199,7 +238,10 @@ const Shipments = () => { value={inputQuery} onChange={(e) => setInputQuery(e.target.value)} /> - <button className='btn-light bg-transparent px-3' type='submit'> + <button + className='btn-light bg-transparent px-3' + type='submit' + > <MagnifyingGlassIcon className='w-6' /> </button> </form> @@ -254,7 +296,7 @@ const Shipments = () => { <Pagination pageCount={pageCount} currentPage={parseInt(page)} - url={router.pathname + pageQuery} + url={`${router.pathname}${pageQuery ? '?' + pageQuery : ''}`} className='mt-2 mb-2' /> </div> @@ -263,16 +305,16 @@ const Shipments = () => { <Manifest idAWB={idAWB} closePopup={closePopup} /> </DesktopView> </> - ) -} + ); +}; const CardStatus = ({ device, paramStatus, shipments, filterStatus }) => { - const status = [`pending`, `shipment`, `completed`] + const status = [`pending`, `shipment`, `completed`]; return ( <> {status.map((value) => { - const statusData = getStatusLabel(device, value, shipments) + const statusData = getStatusLabel(device, value, shipments); if (device === 'desktop') { return ( <div @@ -282,13 +324,15 @@ const CardStatus = ({ device, paramStatus, shipments, filterStatus }) => { }`} onClick={() => filterStatus(value)} > - <h2 className='mb-2 text-lg font-bold tracking-tight'>{statusData.label}</h2> + <h2 className='mb-2 text-lg font-bold tracking-tight'> + {statusData.label} + </h2> {statusData.image} <h1 className='text-xl font-bold'> {statusData.shipCount} <span className='text-sm'>Pesanan</span> </h1> </div> - ) + ); } else { return ( <div @@ -305,15 +349,15 @@ const CardStatus = ({ device, paramStatus, shipments, filterStatus }) => { <span className='truncate'>{statusData.shipCount}</span> {'>'} </h1> </div> - ) + ); } })} </> - ) -} + ); +}; const getStatusLabel = (device, status, shipments) => { - let images = null + let images = null; switch (status) { case 'pending': if (device === 'desktop') { @@ -328,40 +372,48 @@ const getStatusLabel = (device, status, shipments) => { /> </div> </div> - ) + ); } else { images = ( <div> <ImageNext src='/images/BOX(1).svg' width={15} height={20} /> </div> - ) + ); } return { label: 'Pending', shipCount: shipments?.summary?.pendingCount, - image: images - } + image: images, + }; case 'shipment': if (device === 'desktop') { images = ( <div className='bg-yellow-100 border border-yellow-200 rounded-sm p-1 w-20 mb-2'> <div> - <ImageNext src='/images/BOX_DELIVER_(1).svg' width={30} height={20} /> + <ImageNext + src='/images/BOX_DELIVER_(1).svg' + width={30} + height={20} + /> </div> </div> - ) + ); } else { images = ( <div> - <ImageNext src='/images/BOX_DELIVER_(1).svg' width={18} height={20} /> + <ImageNext + src='/images/BOX_DELIVER_(1).svg' + width={18} + height={20} + /> </div> - ) + ); } return { label: 'Pengiriman', shipCount: shipments?.summary?.shipmentCount, - image: images - } + image: images, + }; case 'completed': if (device === 'desktop') { images = ( @@ -375,22 +427,22 @@ const getStatusLabel = (device, status, shipments) => { /> </div> </div> - ) + ); } else { images = ( <div> <ImageNext src='/images/open-box(1).svg' width={16} height={20} /> </div> - ) + ); } return { label: 'Pesanan Tiba', shipCount: shipments?.summary?.completedCount, - image: images - } + image: images, + }; default: - return 'Status Tidak Dikenal' + return 'Status Tidak Dikenal'; } -} +}; -export default Shipments +export default Shipments; diff --git a/src/lib/transaction/api/transactionsApi.js b/src/lib/transaction/api/transactionsApi.js index f4e36e6f..5ea2b5b0 100644 --- a/src/lib/transaction/api/transactionsApi.js +++ b/src/lib/transaction/api/transactionsApi.js @@ -3,6 +3,9 @@ import { getAuth } from '@/core/utils/auth' const transactionsApi = async ({ query }) => { const auth = getAuth() + if (!auth) { + return null + } const dataTransactions = await odooApi( 'GET', `/api/v1/partner/${auth.partnerId}/sale_order?${query}` diff --git a/src/lib/transaction/components/Transaction.jsx b/src/lib/transaction/components/Transaction.jsx index 4d401037..d001c7f4 100644 --- a/src/lib/transaction/components/Transaction.jsx +++ b/src/lib/transaction/components/Transaction.jsx @@ -778,6 +778,10 @@ const Transaction = ({ id }) => { ? `| ${product?.attributes.join(', ')}` : ''} </div> + <div className='text-[10px] text-red-500 italic mt-2'> + {product.availableQuantity} barang ini bisa di + pickup maksimal pukul 16.00 + </div> </div> </td> {/* <td> @@ -879,7 +883,7 @@ const Transaction = ({ id }) => { </div> </div> </div> - )} + )} {transaction?.data?.productsRejectLine.length > 0 && ( <div className='text-h-sm font-semibold mt-10 mb-4'> diff --git a/src/lib/treckingAwb/component/Manifest.jsx b/src/lib/treckingAwb/component/Manifest.jsx index fbc95702..02d0bc7a 100644 --- a/src/lib/treckingAwb/component/Manifest.jsx +++ b/src/lib/treckingAwb/component/Manifest.jsx @@ -1,16 +1,16 @@ -import odooApi from '@/core/api/odooApi' -import BottomPopup from '@/core/components/elements/Popup/BottomPopup' -import LogoSpinner from '@/core/components/elements/Spinner/LogoSpinner' -import { getAuth } from '@/core/utils/auth' -import { useEffect, useState } from 'react' -import { toast } from 'react-hot-toast' -import ImageNext from 'next/image' -import { list } from 'postcss' +import odooApi from '@/core/api/odooApi'; +import BottomPopup from '@/core/components/elements/Popup/BottomPopup'; +import LogoSpinner from '@/core/components/elements/Spinner/LogoSpinner'; +import { getAuth } from '@/core/utils/auth'; +import { useEffect, useState } from 'react'; +import { toast } from 'react-hot-toast'; +import ImageNext from 'next/image'; +import { list } from 'postcss'; const Manifest = ({ idAWB, closePopup }) => { - const [manifests, setManifests] = useState(null) - const [isLoading, setIsLoading] = useState(false) - + const [manifests, setManifests] = useState(null); + const [isLoading, setIsLoading] = useState(false); + console.log('manifests', manifests); const formatCustomDate = (date) => { const months = [ 'Jan', @@ -24,61 +24,60 @@ const Manifest = ({ idAWB, closePopup }) => { 'Sep', 'Oct', 'Nov', - 'Dec' - ] + 'Dec', + ]; - const parts = date.split(' ') // Pisahkan tanggal dan waktu - const [datePart, timePart] = parts - const [yyyy, mm, dd] = datePart.split('-') - const [hh, min] = timePart.split(':') + const parts = date.split(' '); // Pisahkan tanggal dan waktu + const [datePart, timePart] = parts; + const [yyyy, mm, dd] = datePart.split('-'); + const [hh, min] = timePart.split(':'); - const monthAbbreviation = months[parseInt(mm, 10) - 1] + const monthAbbreviation = months[parseInt(mm, 10) - 1]; - return `${dd} ${monthAbbreviation} ${hh}:${min}` - } + return `${dd} ${monthAbbreviation} ${hh}:${min}`; + }; const getManifest = async () => { - setIsLoading(true) - const auth = getAuth() - let list - if(auth){ + setIsLoading(true); + const auth = getAuth(); + let list; + if (auth) { list = await odooApi( 'GET', `/api/v1/partner/${auth.partnerId}/stock-picking/${idAWB}/tracking` - ) - }else{ - list = await odooApi( - 'GET', - `/api/v1/stock-picking/${idAWB}/tracking` - ) + ); + } else { + list = await odooApi('GET', `/api/v1/stock-picking/${idAWB}/tracking`); } - setManifests(list) - setIsLoading(false) - } + setManifests(list); + setIsLoading(false); + }; useEffect(() => { if (idAWB) { - getManifest() + getManifest(); } else { - setManifests(null) + setManifests(null); } - }, [idAWB]) + }, [idAWB]); - const [copied, setCopied] = useState(false) + const [copied, setCopied] = useState(false); const handleCopyClick = () => { - const textToCopy = manifests?.waybillNumber - navigator.clipboard.writeText(textToCopy) - setCopied(true) - toast.success('No Resi Berhasil di Copy') - setTimeout(() => setCopied(false), 2000) // Reset copied state after 2 seconds - } + const textToCopy = manifests?.waybillNumber; + navigator.clipboard.writeText(textToCopy); + setCopied(true); + toast.success('No Resi Berhasil di Copy'); + setTimeout(() => setCopied(false), 2000); // Reset copied state after 2 seconds + }; return ( <> {isLoading && ( <BottomPopup active={true} close=''> - <div className='leading-7 text-gray_r-12/80 flex justify-center'>Mohon Tunggu</div> + <div className='leading-7 text-gray_r-12/80 flex justify-center'> + Mohon Tunggu + </div> <div className='container flex justify-center my-4'> <LogoSpinner width={48} height={48} /> </div> @@ -111,11 +110,14 @@ const Manifest = ({ idAWB, closePopup }) => { </div> <div className=''> <h1 className='text-body-1'> - Estimasi tiba pada <span className='text-gray_r-11 text-sm'>({manifests?.eta})</span> + Estimasi tiba pada{' '} + <span className='text-gray_r-11 text-sm'>({manifests?.eta})</span> </h1> <h1 className='text-sm mt-2 mb-3'> Dikirim Menggunakan{' '} - <span className='text-red-500 font-semibold'>{manifests?.deliveryOrder.carrier}</span> + <span className='text-red-500 font-semibold'> + {manifests?.deliveryOrder.carrier} + </span> </h1> {manifests?.waybillNumber && ( <div className='flex justify-between items-center'> @@ -154,10 +156,16 @@ const Manifest = ({ idAWB, closePopup }) => { {manifests.delivered == true && index == 0 ? ( <div class={`absolute w-6 h-6 rounded-full mt-1.5 -left-3 border ${ - index == 0 ? 'bg-green-100 border-green-100' : 'bg-gray_r-7 border-white' + index == 0 + ? 'bg-green-100 border-green-100' + : 'bg-gray_r-7 border-white' }`} > - <ImageNext src='/images/open-box(1).svg' width={30} height={20} /> + <ImageNext + src='/images/open-box(1).svg' + width={30} + height={20} + /> </div> ) : ( <div @@ -167,7 +175,9 @@ const Manifest = ({ idAWB, closePopup }) => { {manifests.delivered != true && ( <div class={`absolute w-3 h-3 rounded-full mt-1.5 -left-1.5 border ${ - index == 0 ? 'bg-green-600 border-green-600' : 'bg-gray_r-7 border-white' + index == 0 + ? 'bg-green-600 border-green-600' + : 'bg-gray_r-7 border-white' } `} /> )} @@ -176,9 +186,15 @@ const Manifest = ({ idAWB, closePopup }) => { {formatCustomDate(manifest.datetime)} </time> {manifests.delivered == true && index == 0 && ( - <p class={`leading-6 font-semibold text-sm text-green-600 `}>Sudah Sampai</p> + <p + class={`leading-6 font-semibold text-sm text-green-600 `} + > + Sudah Sampai + </p> )} - <p class={`leading-6 text-[12px] text-gray_r-11`}>{manifest.description}</p> + <p class={`leading-6 text-[12px] text-gray_r-11`}> + {manifest.description} + </p> </li> </> ))} @@ -187,7 +203,7 @@ const Manifest = ({ idAWB, closePopup }) => { </BottomPopup> )} </> - ) -} + ); +}; -export default Manifest +export default Manifest; diff --git a/src/lib/variant/components/VariantCard.jsx b/src/lib/variant/components/VariantCard.jsx index 68cdf54f..08b7a97e 100644 --- a/src/lib/variant/components/VariantCard.jsx +++ b/src/lib/variant/components/VariantCard.jsx @@ -103,30 +103,42 @@ const VariantCard = ({ product, openOnClick = true, buyMore = false }) => { </div> </div> </div> - </div> <div className='w-8/12 flex flex-col'> - <p className='product-card__title wrap-line-ellipsis-2'>{product.parent.name}</p> + <p className='product-card__title wrap-line-ellipsis-2'> + {product.parent.name} + </p> <p className='text-caption-2 text-gray_r-11 mt-1'> {product.code || '-'} - {product.attributes.length > 0 ? ` ・ ${product.attributes.join(', ')}` : ''} + {product.attributes.length > 0 + ? ` ・ ${product.attributes.join(', ')}` + : ''} </p> <p className='text-caption-2 text-gray_r-11 mt-1'> Berat Item : {product?.weight} Kg x {product?.quantity} Barang </p> + <p className='text-[10px] text-red-500 italic mt-2'> + {product.availableQuantity} barang ini bisa di pickup maksimal pukul + 16.00 + </p> <div className='flex flex-wrap gap-x-1 items-center mt-auto'> {product.hasFlashsale && ( <> <p className='text-caption-2 text-gray_r-11 line-through'> {currencyFormat(product.price.price)} </p> - <span className='badge-red'>{product.price.discountPercentage}%</span> + <span className='badge-red'> + {product.price.discountPercentage}% + </span> </> )} </div> <p className='text-caption-2 text-gray_r-11 mt-1'> {product.price.priceDiscount > 0 - ? currencyFormat(product.price.priceDiscount) + ' × ' + product.quantity + ' Barang' + ? currencyFormat(product.price.priceDiscount) + + ' × ' + + product.quantity + + ' Barang' : ''} </p> <p className='text-caption-2 text-gray_r-12 font-bold mt-2'> diff --git a/src/pages/_app.jsx b/src/pages/_app.jsx index bcb41dd6..f52aa5f7 100644 --- a/src/pages/_app.jsx +++ b/src/pages/_app.jsx @@ -85,7 +85,7 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }) { return ( <SessionProvider session={session}> <ScrollToTop /> - + <AnimatePresence> {animateLoader && ( <motion.div diff --git a/src/pages/_document.jsx b/src/pages/_document.jsx index 6af6294f..4b67c3f9 100644 --- a/src/pages/_document.jsx +++ b/src/pages/_document.jsx @@ -115,6 +115,19 @@ export default function MyDocument() { }} /> + <Script + async + id='gtag-config' + strategy='afterInteractive' + dangerouslySetInnerHTML={{ + __html: ` + gtag('config', 'AW-954540379/fCU8CI3Y8OoZENvClMcD', { + 'phone_conversion_number': '(021) 29338828' + }); + `, + }} + /> + {/* <Script id='tawk-script-tag' strategy='afterInteractive' diff --git a/src/pages/api/banner-section.js b/src/pages/api/banner-section.js new file mode 100644 index 00000000..7d7040c0 --- /dev/null +++ b/src/pages/api/banner-section.js @@ -0,0 +1,44 @@ +import odooApi from '@/core/api/odooApi'; +import { createClient } from 'redis'; + +const client = createClient(); + +client.on('error', (err) => console.error('Redis Client Error', err)); + +const connectRedis = async () => { + if (!client.isOpen) { + await client.connect(); + } +}; + +export default async function handler(req, res) { + try { + await connectRedis(); + const cacheKey = 'hero-banner'; + // await client.del(cacheKey); + let cachedData = await client.get(cacheKey); + + if (cachedData) { + const data = JSON.parse(cachedData); + return res.status(200).json({ data }); + } else { + const dataBannerSections = await odooApi( + 'GET', + '/api/v1/banner?type=home-banner' + ); + + // Simpan hasil fetch ke Redis dengan masa kadaluarsa 3 hari (259200 detik) + await client.set( + cacheKey, + JSON.stringify(dataBannerSections), + 'EX', + 259200 + ); + + return res.status(200).json({ data: dataBannerSections }); + } + } catch (error) { + console.error('Error interacting with Redis or fetching data:', error); + return res.status(500).json({ error: 'Internal Server Error' }); + } +} diff --git a/src/pages/api/category-management.js b/src/pages/api/category-management.js new file mode 100644 index 00000000..f05d8644 --- /dev/null +++ b/src/pages/api/category-management.js @@ -0,0 +1,85 @@ +import { createClient } from 'redis'; +// import { fetchCategoryManagementSolr } from '../../lib/home/api/categoryManagementApi'; +const client = createClient(); +client.on('error', (err) => console.error('Redis Client Error', err)); + +const connectRedis = async () => { + if (!client.isOpen) { + await client.connect(); + } +}; + +export default async function handler(req, res) { + try { + await connectRedis(); + // await client.del('homepage_categoryDynamic'); + + let cachedData; + if (req.method === 'GET') { + cachedData = await client.get('homepage_categoryDynamic'); + + if (!cachedData) { + const items = await fetchCategoryManagementSolr(); + await client.set( + 'homepage_categoryDynamic', + JSON.stringify(items), + 'EX', + 259200 // Expiry 3 hari + ); + cachedData = await client.get('homepage_categoryDynamic'); + } + const data = cachedData ? JSON.parse(cachedData) : null; + res.status(200).json({ data }); + } else { + res.setHeader('Allow', ['GET']); + res.status(405).end(`Method ${req.method} Not Allowed`); + } + } catch (error) { + console.error('Error interacting with Redis:', error); + res.status(500).json({ error: 'Error interacting with Redis' }); + } +} + +const fetchCategoryManagementSolr = async () => { + let sort = 'sort=sequence_i asc'; + try { + const response = await fetch( + `http://34.101.189.218:8983/solr/category_management/query?q=*:*&q.op=OR&indent=true&${sort}&&rows=20` + ); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const data = await response.json(); + const promotions = await map(data.response.docs); + return promotions; + } catch (error) { + console.error('Error fetching promotion data:', error); + return []; + } +}; +const map = async (promotions) => { + return promotions.map((promotion) => { + let parsedCategories = promotion.categories.map((category) => { + // Parse string JSON utama + let parsedCategory = JSON.parse(category); + + // Parse setiap elemen di child_frontend_id_i jika ada + if (parsedCategory.child_frontend_id_i) { + parsedCategory.child_frontend_id_i = + parsedCategory.child_frontend_id_i.map((child) => JSON.parse(child)); + } + + return parsedCategory; + }); + let productMapped = { + id: promotion.id, + name: promotion.name_s, + image: promotion.image_s, + sequence: promotion.sequence_i, + numFound: promotion.numFound_i, + categories: parsedCategories, + category_id: promotion.category_id_i, + }; + return productMapped; + }); +}; diff --git a/src/pages/api/flashsale-header.js b/src/pages/api/flashsale-header.js new file mode 100644 index 00000000..31f8efdd --- /dev/null +++ b/src/pages/api/flashsale-header.js @@ -0,0 +1,40 @@ +import odooApi from '@/core/api/odooApi'; +import { createClient } from 'redis'; + +const client = createClient(); + +client.on('error', (err) => console.error('Redis Client Error', err)); + +const connectRedis = async () => { + if (!client.isOpen) { + await client.connect(); + } +}; + +export default async function handler(req, res) { + try { + await connectRedis(); + const cacheKey = `flashsale_header`; + // await client.del(cacheKey); + let cachedData = await client.get(cacheKey); + + if (cachedData) { + const data = JSON.parse(cachedData); + return res.status(200).json({ data }); + } else { + const flashSale = await odooApi('GET', `/api/v1/flashsale/header`); + + await client.set( + cacheKey, + JSON.stringify(flashSale), + 'EX', + flashSale.duration + ); + cachedData = await client.get(cacheKey); + return res.status(200).json({ data: cachedData }); + } + } catch (error) { + console.error('Error interacting with Redis or fetching data:', error); + return res.status(500).json({ error: 'Internal Server Error' }); + } +} diff --git a/src/pages/api/hero-banner.js b/src/pages/api/hero-banner.js new file mode 100644 index 00000000..7a348cfa --- /dev/null +++ b/src/pages/api/hero-banner.js @@ -0,0 +1,45 @@ +import odooApi from '@/core/api/odooApi'; +import { createClient } from 'redis'; + +const client = createClient(); + +client.on('error', (err) => console.error('Redis Client Error', err)); + +const connectRedis = async () => { + if (!client.isOpen) { + await client.connect(); + } +}; + +export default async function handler(req, res) { + const { type } = req.query; + try { + await connectRedis(); + const cacheKey = `homepage_bannerSection_${type}`; + // await client.del(cacheKey); + let cachedData = await client.get(cacheKey); + + if (cachedData) { + const data = JSON.parse(cachedData); + return res.status(200).json({ data }); + } else { + const dataBannerSections = await odooApi( + 'GET', + `/api/v1/banner?type=${type}` + ); + + // Simpan hasil fetch ke Redis dengan masa kadaluarsa 3 hari (259200 detik) + await client.set( + cacheKey, + JSON.stringify(dataBannerSections), + 'EX', + 259200 + ); + cachedData = await client.get(cacheKey); + return res.status(200).json({ data: cachedData }); + } + } catch (error) { + console.error('Error interacting with Redis or fetching data:', error); + return res.status(500).json({ error: 'Internal Server Error' }); + } +} diff --git a/src/pages/api/page-content.js b/src/pages/api/page-content.js new file mode 100644 index 00000000..a6514505 --- /dev/null +++ b/src/pages/api/page-content.js @@ -0,0 +1,43 @@ +import { createClient } from 'redis'; +import { getPageContent } from '~/services/pageContent'; +// import { fetchCategoryManagementSolr } from '../../lib/home/api/categoryManagementApi'; +const client = createClient(); +client.on('error', (err) => console.error('Redis Client Error', err)); + +const connectRedis = async () => { + if (!client.isOpen) { + await client.connect(); + } +}; + +export default async function handler(req, res) { + const { path } = req.query; + try { + await connectRedis(); + // await client.del('onbording-popup'); + + let cachedData; + if (req.method === 'GET') { + cachedData = await client.get(`page-content:${path}`); + + if (!cachedData) { + const items = await getPageContent({ path }); + await client.set( + `page-content:${path}`, + JSON.stringify(items), + 'EX', + 604800 // Expiry 1 minggu + ); + cachedData = await client.get(`page-content:${path}`); + } + const data = cachedData ? JSON.parse(cachedData) : null; + res.status(200).json({ data }); + } else { + res.setHeader('Allow', ['GET']); + res.status(405).end(`Method ${req.method} Not Allowed`); + } + } catch (error) { + console.error('Error interacting with Redis:', error); + res.status(500).json({ error: 'Error interacting with Redis' }); + } +} diff --git a/src/pages/api/search-flashsale.js b/src/pages/api/search-flashsale.js new file mode 100644 index 00000000..d9e56c83 --- /dev/null +++ b/src/pages/api/search-flashsale.js @@ -0,0 +1,45 @@ +import odooApi from '@/core/api/odooApi'; +import { createClient } from 'redis'; +import _ from 'lodash-contrib'; +import axios from 'axios'; + +const client = createClient(); + +client.on('error', (err) => console.error('Redis Client Error', err)); + +const connectRedis = async () => { + if (!client.isOpen) { + await client.connect(); + } +}; + +export default async function handler(req, res) { + const { query, operation, duration } = req.query; + try { + await connectRedis(); + const cacheKey = `flashsale_product`; + // await client.del(cacheKey); + let cachedData = await client.get(cacheKey); + + if (cachedData) { + const data = JSON.parse(cachedData); + return res.status(200).json({ data }); + } else { + const dataProductSearch = await axios( + `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/search?${query}&operation=${operation}]` + ); + + await client.set( + cacheKey, + JSON.stringify(dataProductSearch.data), + 'EX', + duration + ); + cachedData = await client.get(cacheKey); + return res.status(200).json({ data: cachedData }); + } + } catch (error) { + console.error('Error interacting with Redis or fetching data:', error); + return res.status(500).json({ error: 'Internal Server Error' }); + } +} diff --git a/src/pages/api/shop/brands.js b/src/pages/api/shop/brands.js index 9c2824b3..d56e4b13 100644 --- a/src/pages/api/shop/brands.js +++ b/src/pages/api/shop/brands.js @@ -1,8 +1,20 @@ import axios from 'axios'; +import { createClient } from 'redis'; const SOLR_HOST = process.env.SOLR_HOST; +const client = createClient(); + +client.on('error', (err) => console.error('Redis Client Error', err)); + +const connectRedis = async () => { + if (!client.isOpen) { + await client.connect(); + } +}; export default async function handler(req, res) { + await connectRedis(); + try { let params = '*:*'; let sort = @@ -11,12 +23,12 @@ export default async function handler(req, res) { if (req.query.params) { rows = 100; - switch (req?.query?.params) { + switch (req.query.params) { case 'level_s': params = 'level_s:prioritas'; break; case 'search': - params = `name_s:"${req?.query?.q.toLowerCase()}"`; + params = `name_s:"${req.query.q.toLowerCase()}"`; sort = ''; rows = 1; break; @@ -24,11 +36,11 @@ export default async function handler(req, res) { params = `name_s:${req.query.params}`.toLowerCase(); } } - if(req.query.rows) rows = req.query.rows; - + if (req.query.rows) rows = req.query.rows; + const url = `${SOLR_HOST}/solr/brands/select?q=${params}&q.op=OR&indent=true&rows=${rows}&${sort}`; - let brands = await axios(url); - let dataBrands = responseMap(brands.data.response.docs); + const brands = await axios(url); + const dataBrands = responseMap(brands.data.response.docs); res.status(200).json(dataBrands); } catch (error) { @@ -39,13 +51,11 @@ export default async function handler(req, res) { const responseMap = (brands) => { return brands.map((brand) => { - let brandMapping = { + return { id: brand.id, name: brand.display_name_s, logo: brand.image_s || '', - sequance: brand.sequence_i || '', + sequence: brand.sequence_i || '', }; - - return brandMapping; }); }; diff --git a/src/pages/api/shop/preferredBrand.js b/src/pages/api/shop/preferredBrand.js new file mode 100644 index 00000000..4cb35c84 --- /dev/null +++ b/src/pages/api/shop/preferredBrand.js @@ -0,0 +1,61 @@ +import axios from 'axios'; +import { createClient } from 'redis'; + +const SOLR_HOST = process.env.SOLR_HOST; +const client = createClient(); + +client.on('error', (err) => console.error('Redis Client Error', err)); + +const connectRedis = async () => { + if (!client.isOpen) { + await client.connect(); + } +}; + +export default async function handler(req, res) { + await connectRedis(); + + try { + let params = '*:*'; + let sort = + 'sort=if(exists(sequence_i),0,1) asc,sequence_i asc, if(exists(image_s),0,1) asc '; + let rows = 20; + + if (req.query.params) { + rows = 20; + switch (req.query.params) { + case 'level_s': + params = 'level_s:prioritas'; + break; + case 'search': + params = `name_s:"${req.query.q.toLowerCase()}"`; + sort = ''; + rows = 1; + break; + default: + params = `name_s:${req.query.params}`.toLowerCase(); + } + } + if (req.query.rows) rows = req.query.rows; + + const url = `${SOLR_HOST}/solr/brands/select?q=${params}&q.op=OR&indent=true&rows=${rows}&${sort}`; + const brands = await axios(url); + const dataBrands = responseMap(brands.data.response.docs); + + res.status(200).json(dataBrands); + } catch (error) { + console.error('Error fetching data from Solr:', error); + res.status(500).json({ error: 'Internal Server Error' }); + } +} + +const responseMap = (brands) => { + return brands.map((brand) => { + return { + id: brand.id, + name: brand.display_name_s, + logo: brand.image_s || '', + sequence: brand.sequence_i || '', + }; + }); +}; diff --git a/src/pages/api/shop/product-detail.js b/src/pages/api/shop/product-detail.js index 247f2a04..faa96028 100644 --- a/src/pages/api/shop/product-detail.js +++ b/src/pages/api/shop/product-detail.js @@ -8,7 +8,7 @@ export default async function handler(req, res) { ) let productVariants = await axios( process.env.SOLR_HOST + - `/solr/variants/select?q=template_id_i:${req.query.id}&q.op=OR&indent=true&rows=100&fq=-publish_b:false` + `/solr/variants/select?q=template_id_i:${req.query.id}&q.op=OR&indent=true&rows=100&fq=-publish_b:false AND price_tier1_v2_f:[1 TO *]` ) let auth = req.query.auth === 'false' ? JSON.parse(req.query.auth) : req.query.auth let result = productMappingSolr(productTemplate.data.response.docs, auth || false) diff --git a/src/pages/api/shop/search.js b/src/pages/api/shop/search.js index 6269d3ed..63ec7ca0 100644 --- a/src/pages/api/shop/search.js +++ b/src/pages/api/shop/search.js @@ -20,9 +20,11 @@ export default async function handler(req, res) { } = req.query; let { stock = '' } = req.query; - let paramOrderBy = ''; switch (orderBy) { + case 'flashsale-discount-desc': + paramOrderBy += 'flashsale_discount_f DESC'; + break; case 'price-asc': paramOrderBy += 'price_tier1_v2_f ASC'; break; @@ -68,16 +70,40 @@ export default async function handler(req, res) { let checkQ = q.trim().split(/[\s\+\-\!\(\)\{\}\[\]\^"~\*\?:\\\/]+/); let newQ = escapeSolrQuery(q); - const formattedQuery = `(${newQ.split(' ').map(term => `${term}*`).join(' ') })`; - const mm = checkQ.length > 2 ? checkQ.length > 5 ? '55%' : '85%' : `${checkQ.length}`; + const formattedQuery = `(${newQ + .split(' ') + .map((term) => (term.length < 2 ? term : `${term}*`)) // Tambahkan '*' hanya jika panjang kata >= 2 + .join(' ')})`; + + const mm = + checkQ.length > 2 + ? checkQ.length > 5 + ? '55%' + : '85%' + : `${checkQ.length}`; const filterQueries = [ '-publish_b:false', 'product_rating_f:[8 TO *]', - 'price_tier1_v2_f:[1 TO *]' + 'price_tier1_v2_f:[1 TO *]', ]; - - const fq_ = filterQueries.join('AND '); + + if (fq && source != 'similar' && typeof fq != 'string') { + // filterQueries.push(fq); + fq.push(...filterQueries); + } + const fq_ = filterQueries.join(' AND '); + + let keywords = newQ; + if (source === 'similar' || checkQ.length < 3) { + if (checkQ.length < 2 || checkQ[1].length < 2) { + keywords = newQ; + } else { + keywords = newQ + '*'; + } + } else { + keywords = formattedQuery; + } let offset = (page - 1) * limit; let parameter = [ @@ -87,9 +113,9 @@ export default async function handler(req, res) { 'indent=true', `facet.query=${escapeSolrQuery(q)}`, `q.op=OR`, - `q=${source == 'similar' || checkQ.length < 3 ? checkQ.length < 2 ? newQ : newQ + '*' : formattedQuery }`, + `q=${keywords}`, `defType=edismax`, - 'qf=name_s description_clean_t category_name manufacture_name_s variants_code_t variants_name_t category_id_ids default_code_s', + 'qf=name_s description_clean_t category_name manufacture_name_s variants_code_t variants_name_t category_id_ids default_code_s manufacture_id_i category_id_i ', `start=${parseInt(offset)}`, `rows=${limit}`, `sort=${paramOrderBy}`, @@ -135,12 +161,13 @@ export default async function handler(req, res) { if (typeof fq === 'string') parameter.push(`fq=${encodeURIComponent(fq)}`); // Multi fq in url params if (Array.isArray(fq)) - parameter = parameter.concat(fq.map((val) => `fq=${encodeURIComponent(val)}`)); - + parameter = parameter.concat( + fq.map((val) => `fq=${encodeURIComponent(val)}`) + ); let result = await axios( process.env.SOLR_HOST + '/solr/product/select?' + parameter.join('&') ); - + try { result.data.response.products = productMappingSolr( result.data.response.docs, diff --git a/src/pages/api/shop/url-category_brand.js b/src/pages/api/shop/url-category_brand.js new file mode 100644 index 00000000..160aa166 --- /dev/null +++ b/src/pages/api/shop/url-category_brand.js @@ -0,0 +1,20 @@ +import axios from 'axios'; + +export default async function handler(req, res) { + const { url = '', page = 1, limit = 30 } = req.query; + + let offset = (page - 1) * limit; + + const params = [`q.op=AND`, `q=${url ? `"${url}"` : '*'}`, `indent=true`, `rows=${limit}`, `start=${offset}`]; + + try { + let result = await axios( + process.env.SOLR_HOST + + `/solr/url_category_brand/select?` + + params.join('&') + ); + res.status(200).json(result.data); + } catch (error) { + res.status(500).json({ error: 'Internal Server Error' }); + } +} diff --git a/src/pages/api/shop/variant-detail.js b/src/pages/api/shop/variant-detail.js index 08ce75b8..af3525b3 100644 --- a/src/pages/api/shop/variant-detail.js +++ b/src/pages/api/shop/variant-detail.js @@ -1,21 +1,28 @@ -import { productMappingSolr, variantsMappingSolr } from '@/utils/solrMapping' -import axios from 'axios' +import { productMappingSolr, variantsMappingSolr } from '@/utils/solrMapping'; +import axios from 'axios'; export default async function handler(req, res) { try { let productVariants = await axios( process.env.SOLR_HOST + `/solr/variants/select?q=id:${req.query.id}&q.op=OR&indent=true` - ) - let auth = req.query.auth === 'false' ? JSON.parse(req.query.auth) : req.query.auth + ); + let template_id = productVariants.data.response.docs[0].template_id_i; + let auth = + req.query.auth === 'false' ? JSON.parse(req.query.auth) : req.query.auth; let productTemplate = await axios( - process.env.SOLR_HOST + `/solr/product/select?q=id:${req.query.id}&q.op=OR&indent=true` - ) - let result = variantsMappingSolr(productTemplate.data.response.docs, productVariants.data.response.docs, auth || false) - - res.status(200).json(result) + process.env.SOLR_HOST + + `/solr/product/select?q=id:${template_id}&q.op=OR&indent=true` + ); + let result = variantsMappingSolr( + productTemplate.data.response.docs, + productVariants.data.response.docs, + auth || false + ); + + res.status(200).json(result); } catch (error) { - console.error('Error fetching data from Solr:', error) - res.status(500).json({ error: 'Internal Server Error' }) + console.error('Error fetching data from Solr:', error); + res.status(500).json({ error: 'Internal Server Error' }); } -}
\ No newline at end of file +} diff --git a/src/pages/garansi-resmi.jsx b/src/pages/garansi-resmi.jsx new file mode 100644 index 00000000..7384a89d --- /dev/null +++ b/src/pages/garansi-resmi.jsx @@ -0,0 +1,13 @@ +import Seo from '@/core/components/Seo' +import BasicLayout from '@/core/components/layouts/BasicLayout' +import IframeContent from '@/lib/iframe/components/IframeContent' + +export default function GaransiResmi() { + return ( + <BasicLayout> + <Seo title='Garansi Resmi - Indoteknik.com' /> + + <IframeContent url={`${process.env.NEXT_PUBLIC_ODOO_HOST}/content?url=garansi-resmi`} /> + </BasicLayout> + ) +} diff --git a/src/pages/index.jsx b/src/pages/index.jsx index 6077c192..2ec1231a 100644 --- a/src/pages/index.jsx +++ b/src/pages/index.jsx @@ -1,21 +1,19 @@ -import dynamic from 'next/dynamic'; -import { useEffect, useRef, useState } from 'react'; import { HeroBannerSkeleton } from '@/components/skeleton/BannerSkeleton'; import { PopularProductSkeleton } from '@/components/skeleton/PopularProductSkeleton'; +import odooApi from '@/core/api/odooApi'; import Seo from '@/core/components/Seo'; import DelayRender from '@/core/components/elements/DelayRender/DelayRender'; import DesktopView from '@/core/components/views/DesktopView'; import MobileView from '@/core/components/views/MobileView'; import { FlashSaleSkeleton } from '@/lib/flashSale/skeleton/FlashSaleSkeleton'; -import PreferredBrandSkeleton from '@/lib/home/components/Skeleton/PreferredBrandSkeleton'; import BannerPromoSkeleton from '@/lib/home/components/Skeleton/BannerPromoSkeleton'; -import PromotinProgram from '@/lib/promotinProgram/components/HomePage'; +import PreferredBrandSkeleton from '@/lib/home/components/Skeleton/PreferredBrandSkeleton'; +import dynamic from 'next/dynamic'; +import { useEffect, useRef, useState } from 'react'; +import { getAuth } from '~/libs/auth'; import PagePopupIformation from '~/modules/popup-information'; // need change to dynamic and ssr : false import CategoryPilihan from '../lib/home/components/CategoryPilihan'; -import odooApi from '@/core/api/odooApi'; -import { getAuth } from '~/libs/auth'; // import { getAuth } from '~/libs/auth'; -import useProductDetail from '~/modules/product-detail/stores/useProductDetail'; const BasicLayout = dynamic(() => import('@/core/components/layouts/BasicLayout'),{ssr: false} @@ -47,36 +45,38 @@ const FlashSale = dynamic( } ); -const ProgramPromotion = dynamic(() => - import('@/lib/home/components/PromotionProgram'), -{ - loading: () => <BannerPromoSkeleton />, -} +const ProgramPromotion = dynamic( + () => import('@/lib/home/components/PromotionProgram'), + { + loading: () => <BannerPromoSkeleton />, + } ); const BannerSection = dynamic(() => - import('@/lib/home/components/BannerSection'), {ssr: false} -); -const CategoryHomeId = dynamic(() => - import('@/lib/home/components/CategoryHomeId'), {ssr: false} + import('@/lib/home/components/BannerSection') +); +const CategoryHomeId = dynamic( + () => import('@/lib/home/components/CategoryHomeId'), + { ssr: false } ); const CategoryDynamic = dynamic(() => - import('@/lib/home/components/CategoryDynamic'), {ssr: false} + import('@/lib/home/components/CategoryDynamic') ); const CategoryDynamicMobile = dynamic(() => -import('@/lib/home/components/CategoryDynamicMobile'), {ssr: false} + import('@/lib/home/components/CategoryDynamicMobile') ); -const CustomerReviews = dynamic(() => - import('@/lib/review/components/CustomerReviews'), {ssr: false} +const CustomerReviews = dynamic( + () => import('@/lib/review/components/CustomerReviews'), + { ssr: false } ); // need to ssr:false -const ServiceList = dynamic(() => import('@/lib/home/components/ServiceList'), {ssr: false}); // need to ssr: false +const ServiceList = dynamic(() => import('@/lib/home/components/ServiceList'), { + ssr: false, +}); // need to ssr: false - - -export default function Home({categoryId}) { +export default function Home({ categoryId }) { const bannerRef = useRef(null); const wrapperRef = useRef(null); @@ -87,123 +87,110 @@ export default function Home({categoryId}) { bannerRef.current?.querySelector(':first-child')?.clientHeight + 'px'; }; - useEffect(() => { - const loadCategories = async () => { - const getCategories = await odooApi('GET', '/api/v1/category/child?partner_id='+{categoryId}) - if(getCategories){ - setDataCategories(getCategories) - } - } - loadCategories() - }, []) - - const [dataCategories, setDataCategories] = useState([]) return ( <> - <BasicLayout> - <Seo - title='Indoteknik.com: B2B Industrial Supply & Solution' - description='Temukan pilihan produk B2B Industri & Alat Teknik untuk Perusahaan, UMKM & Pemerintah dengan lengkap, mudah dan transparan.' - additionalMetaTags={[ - { - name: 'keywords', - content: 'indoteknik, indoteknik.com, toko teknik, toko perkakas, jual genset, jual fogging, jual krisbow, harga krisbow, harga alat safety, harga pompa air', - }, - ]} /> - - <PagePopupIformation /> - - <DesktopView> - <div className='container mx-auto'> - <div - className='flex min-h-[400px] h-[460px]' - ref={wrapperRef} - onLoad={handleOnLoad} - > - <div className='w-2/12'> - <HeroBannerSecondary /> - </div> - <div className='w-7/12 px-1' ref={bannerRef}> - <HeroBanner /> - </div> - <div className='w-3/12'> - <DelayRender renderAfter={200}> - <PopularProduct /> - </DelayRender> + <BasicLayout> + <Seo + title='Indoteknik.com: B2B Industrial Supply & Solution' + description='Temukan pilihan produk B2B Industri & Alat Teknik untuk Perusahaan, UMKM & Pemerintah dengan lengkap, mudah dan transparan.' + additionalMetaTags={[ + { + name: 'keywords', + content: + 'indoteknik, indoteknik.com, toko teknik, toko perkakas, jual genset, jual fogging, jual krisbow, harga krisbow, harga alat safety, harga pompa air', + }, + ]} + /> + + <PagePopupIformation /> + + <DesktopView> + <div className='container mx-auto'> + <div + className='flex min-h-[400px] h-[460px]' + ref={wrapperRef} + onLoad={handleOnLoad} + > + <div className='w-2/12'> + <HeroBannerSecondary /> + </div> + <div className='w-7/12 px-1' ref={bannerRef}> + <HeroBanner /> + </div> + <div className='w-3/12'> + <DelayRender renderAfter={200}> + <PopularProduct /> + </DelayRender> + </div> </div> - </div> - <div className='my-16 flex flex-col gap-y-8'> - <ServiceList /> - <div id='flashsale'> - <PreferredBrand /> + <div className='my-16 flex flex-col gap-y-8'> + <ServiceList /> + <div id='flashsale'> + <PreferredBrand /> + </div> + {!auth?.feature?.soApproval && ( + <> + <DelayRender renderAfter={200}> + <ProgramPromotion /> + </DelayRender> + <DelayRender renderAfter={200}> + <FlashSale /> + </DelayRender> + </> + )} + {/* <PromotinProgram /> */} + <CategoryPilihan /> + <CategoryDynamic /> + <CategoryHomeId /> + <BannerSection /> + <CustomerReviews /> </div> + </div> + </DesktopView> + <MobileView> + <DelayRender renderAfter={200}> + <HeroBanner /> + </DelayRender> + <div className='flex flex-col gap-y-4 my-6'> + <DelayRender renderAfter={400}> + <ServiceList /> + </DelayRender> + <DelayRender renderAfter={400}> + <div id='flashsale'> + <PreferredBrand /> + </div> + </DelayRender> {!auth?.feature?.soApproval && ( <> - <DelayRender renderAfter={200}> - <ProgramPromotion /> - </DelayRender> - <DelayRender renderAfter={200}> - <FlashSale /> - </DelayRender> + <DelayRender renderAfter={400}> + <ProgramPromotion /> + </DelayRender> + <DelayRender renderAfter={600}> + <FlashSale /> + </DelayRender> </> )} - {/* <PromotinProgram /> */} - {dataCategories &&( - <CategoryPilihan categories={dataCategories} /> - )} - <CategoryDynamic /> - <CategoryHomeId /> - <BannerSection /> - <CustomerReviews /> + <DelayRender renderAfter={600}> + {/* <PromotinProgram /> */} + </DelayRender> + <DelayRender renderAfter={600}> + <CategoryPilihan /> + <CategoryDynamicMobile /> + </DelayRender> + <DelayRender renderAfter={800}> + <PopularProduct /> + </DelayRender> + <DelayRender renderAfter={1000}> + <CategoryHomeId /> + <BannerSection /> + </DelayRender> + <DelayRender renderAfter={1200}> + <CustomerReviews /> + </DelayRender> </div> - </div> - </DesktopView> - <MobileView> - <DelayRender renderAfter={200}> - <HeroBanner /> - </DelayRender> - <div className='flex flex-col gap-y-4 my-6'> - <DelayRender renderAfter={400}> - <ServiceList /> - </DelayRender> - <DelayRender renderAfter={400}> - <div id='flashsale'> - <PreferredBrand /> - </div> - </DelayRender> - {!auth?.feature?.soApproval && ( - <> - <DelayRender renderAfter={400}> - <ProgramPromotion /> - </DelayRender> - <DelayRender renderAfter={600}> - <FlashSale /> - </DelayRender> - </> - )} - <DelayRender renderAfter={600}> - {/* <PromotinProgram /> */} - </DelayRender> - <DelayRender renderAfter={600}> - {dataCategories &&( - <CategoryPilihan categories={dataCategories} /> - )} - <CategoryDynamicMobile /> - </DelayRender> - <DelayRender renderAfter={800}> - <PopularProduct /> - </DelayRender> - <DelayRender renderAfter={1000}> - <CategoryHomeId /> - <BannerSection /> - </DelayRender> - <DelayRender renderAfter={1200}> - <CustomerReviews /> - </DelayRender> - </div> - </MobileView> - </BasicLayout> - </> + </MobileView> + </BasicLayout> + </> ); -}
\ No newline at end of file +} diff --git a/src/pages/my/address/[id]/edit.jsx b/src/pages/my/address/[id]/edit.jsx index c552659b..19d7af41 100644 --- a/src/pages/my/address/[id]/edit.jsx +++ b/src/pages/my/address/[id]/edit.jsx @@ -37,12 +37,15 @@ export async function getServerSideProps(context) { mobile: address.mobile, street: address.street, zip: address.zip, - city: address.city?.id || '', + state: address.stateId?.id || '', + oldCity: address.city?.id || '', + city: '', oldDistrict: address.district?.id || '', district: '', oldSubDistrict: address.subDistrict?.id || '', subDistrict: '', business_name: '', }; + // console.log('ini default',defaultValues); return { props: { id, defaultValues } }; } diff --git a/src/pages/pembayaran-tempo.jsx b/src/pages/pembayaran-tempo.jsx new file mode 100644 index 00000000..363e3099 --- /dev/null +++ b/src/pages/pembayaran-tempo.jsx @@ -0,0 +1,13 @@ +import Seo from '@/core/components/Seo' +import BasicLayout from '@/core/components/layouts/BasicLayout' +import IframeContent from '@/lib/iframe/components/IframeContent' + +export default function PembnayaranTempo() { + return ( + <BasicLayout> + <Seo title='Pambayaran Tempo - Indoteknik.com' /> + + <IframeContent url={`${process.env.NEXT_PUBLIC_ODOO_HOST}/content?url=pembayaran-tempo`} /> + </BasicLayout> + ) +} diff --git a/src/pages/shop/checkout/[status].jsx b/src/pages/shop/checkout/[status].jsx index 2c3bebcf..0d5cffe8 100644 --- a/src/pages/shop/checkout/[status].jsx +++ b/src/pages/shop/checkout/[status].jsx @@ -1,22 +1,22 @@ -import BasicLayout from '@/core/components/layouts/BasicLayout' -import IsAuth from '@/lib/auth/components/IsAuth' -import FinishCheckoutComponent from '@/lib/checkout/components/FinishCheckout' -import { useRouter } from 'next/router' -import axios from 'axios' -import Seo from '@/core/components/Seo' +import BasicLayout from '@/core/components/layouts/BasicLayout'; +import IsAuth from '@/lib/auth/components/IsAuth'; +import FinishCheckoutComponent from '@/lib/checkout/components/FinishCheckout'; +import { useRouter } from 'next/router'; +import axios from 'axios'; +import Seo from '@/core/components/Seo'; export async function getServerSideProps(context) { - const { order_id } = context.query + const { order_id } = context.query; await axios.post( `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/finish-checkout?orderName=${order_id}`, {}, { headers: context.req.headers } - ) - return { props: {} } + ); + return { props: {} }; } export default function Finish() { - const router = useRouter() + const router = useRouter(); return ( <> @@ -28,5 +28,5 @@ export default function Finish() { </BasicLayout> </IsAuth> </> - ) + ); } diff --git a/src/pages/shop/find/[slug].jsx b/src/pages/shop/find/[slug].jsx new file mode 100644 index 00000000..268b1e56 --- /dev/null +++ b/src/pages/shop/find/[slug].jsx @@ -0,0 +1,69 @@ +import axios from 'axios'; +import { useRouter } from 'next/router'; +import { useEffect, useState } from 'react'; +import Seo from '@/core/components/Seo'; +import dynamic from 'next/dynamic'; +import { get } from 'lodash-contrib'; +import { getIdFromSlug, getNameFromSlug } from '@/core/utils/slug'; +import { capitalizeEachWord } from '../../../utils/capializeFIrstWord'; + +const BasicLayout = dynamic(() => + import('@/core/components/layouts/BasicLayout') +); +const ProductSearch = dynamic(() => + import('@/lib/product/components/ProductSearch') +); + +const BASE_URL = 'https://indoteknik.com'; +export default function FindPage() { + const route = useRouter(); + const qSlug = route?.query?.slug || null; + const url = BASE_URL + route.asPath.split('?')[0]; + const [result, setResult] = useState(null); + const [query, setQuery] = useState(null); + + const __slug = qSlug ? getNameFromSlug(route?.query?.slug) +' '+ getIdFromSlug(route?.query?.slug) : ''; + const slug = capitalizeEachWord(__slug); + + const getUrls = async (url) => { + try { + let response = await axios( + `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/url-category_brand?url=${url}` + ); + let result = response?.data?.response?.docs[0] || null; + setResult(result); + } catch (error) { + console.error('Error fetching data:', error); + } + }; + + useEffect(() => { + getUrls(url); + }, []); + + useEffect(() => { + if (result) { + let fq = `category_parent_ids:${result.category_id_i} AND manufacture_id_i:${result.brand_id_i}`; + setQuery({ + fq: fq, + }); + } + }, [result]); + + return ( + <BasicLayout> + <Seo + title={`Beli ${slug} Original & Harga Terjangkau - indoteknik.com`} + description={`Beli ${slug} Kirim Jakarta Surabaya Semarang Makassar Manado Denpasar Balikpapan Medan Palembang Lampung Bali Bandung Makassar Manado.`} + additionalMetaTags={[ + { + property: 'keywords', + content: `Beli ${slug}, harga ${slug}, ${slug} murah, toko ${slug}, ${slug} jakarta, ${slug} surabaya`, + + }, + ]} + /> + {query && <ProductSearch query={query} prefixUrl={`${route.asPath}`} />} + </BasicLayout> + ); +} diff --git a/src/pages/shop/product/variant/[slug].jsx b/src/pages/shop/product/variant/[slug].jsx index 42f38774..2c0dd64b 100644 --- a/src/pages/shop/product/variant/[slug].jsx +++ b/src/pages/shop/product/variant/[slug].jsx @@ -32,16 +32,9 @@ export async function getServerSideProps(context) { tier ); let product = response.data; - // let product = await variantApi({ id: getIdFromSlug(slug), headers: { Token: authToken } }) if (product?.length == 1) { product = product[0]; - /* const regexHtmlTags = /(<([^>]+)>)/gi - const regexHtmlTagsExceptP = /<\/?(?!p\b)[^>]*>/g - product.description = product.description - .replace(regexHtmlTagsExceptP, ' ') - .replace(regexHtmlTags, ' ') - .trim()*/ } else { product = null; } diff --git a/src/pages/sitemap/categories-brand.xml.js b/src/pages/sitemap/categories-brand.xml.js new file mode 100644 index 00000000..b23363e9 --- /dev/null +++ b/src/pages/sitemap/categories-brand.xml.js @@ -0,0 +1,35 @@ +import productSearchApi from '@/lib/product/api/productSearchApi' +import { create } from 'xmlbuilder' +import _ from 'lodash-contrib' +import axios from 'axios' + +export async function getServerSideProps({ res }) { + const baseUrl = process.env.SELF_HOST + '/sitemap/categories-brand' + const limit = 500 + const categories = await axios( + `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/url-category_brand?limit=${limit}` + ) + const pageCount = Math.ceil(categories.data.response.numFound / limit) + const pages = Array.from({ length: pageCount }, (_, i) => i + 1) + const sitemapIndex = create('sitemapindex', { encoding: 'UTF-8' }).att( + 'xmlns', + 'http://www.sitemaps.org/schemas/sitemap/0.9' + ) + + const date = new Date() + pages.forEach((page) => { + const sitemap = sitemapIndex.ele('sitemap') + sitemap.ele('loc', `${baseUrl}/${page}.xml`) + sitemap.ele('lastmod', date.toISOString().slice(0, 10)) + }) + + res.setHeader('Content-Type', 'text/xml') + res.write(sitemapIndex.end()) + res.end() + + return { props: {} } +} + +export default function SitemapProducts() { + return null +} diff --git a/src/pages/sitemap/categories-brand/[page].js b/src/pages/sitemap/categories-brand/[page].js new file mode 100644 index 00000000..6b55e426 --- /dev/null +++ b/src/pages/sitemap/categories-brand/[page].js @@ -0,0 +1,43 @@ + +import productSearchApi from '@/lib/product/api/productSearchApi' +import { create } from 'xmlbuilder' +import _ from 'lodash-contrib' +import { createSlug } from '@/core/utils/slug' +import axios from 'axios' + +export async function getServerSideProps({ query, res }) { + const baseUrl = process.env.SELF_HOST + '/shop/product/' + const { page } = query + const limit = 500 + const categories = await axios( + `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/url-category_brand?limit=${limit}&page=${page.replace( + '.xml', + '' + )}` + ) + + const sitemap = create('urlset', { encoding: 'utf-8' }).att( + 'xmlns', + 'http://www.sitemaps.org/schemas/sitemap/0.9' + ) + + const date = new Date() + categories.data.response.docs.forEach((product) => { + const url = sitemap.ele('url') + const loc = product.url_s; + url.ele('loc', loc) + url.ele('lastmod', date.toISOString().slice(0, 10)) + url.ele('changefreq', 'daily') + url.ele('priority', '0.8') + }) + + res.setHeader('Content-Type', 'text/xml') + res.write(sitemap.end()) + res.end() + + return { props: {} } +} + +export default function SitemapProducts() { + return null +} diff --git a/src/utils/capializeFIrstWord.js b/src/utils/capializeFIrstWord.js new file mode 100644 index 00000000..b62d0c06 --- /dev/null +++ b/src/utils/capializeFIrstWord.js @@ -0,0 +1,9 @@ +export const capitalizeEachWord = (str) => { + return str + .split(' ') // Pisahkan string menjadi array kata-kata + .map((word) => // Ubah huruf pertama setiap kata menjadi besar + word.charAt(0).toUpperCase() + word.slice(1).toLowerCase() + ) + .join(' '); // Gabungkan kembali menjadi string + }; +
\ No newline at end of file diff --git a/src/utils/solrMapping.js b/src/utils/solrMapping.js index f73e966a..ecd62be2 100644 --- a/src/utils/solrMapping.js +++ b/src/utils/solrMapping.js @@ -43,6 +43,7 @@ export const productMappingSolr = (products, pricelist) => { let productMapped = { id: product.product_id_i || '', image: product.image_s || '', + imageMobile: product.image_mobile_s || '', code: product.default_code_s || '', description: product.description_t || '', displayName: product.display_name_s || '', @@ -74,6 +75,7 @@ export const productMappingSolr = (products, pricelist) => { name: product.manufacture_name_s || '', imagePromotion1: product.image_promotion_1_s || '', imagePromotion2: product.image_promotion_2_s || '', + logo: product.x_logo_manufacture_s || '', }; } @@ -127,12 +129,14 @@ export const variantsMappingSolr = (parent, products, pricelist) => { manufacture: {}, parent: {}, qtySold: product?.qty_sold_f || 0, + is_in_bu: product?.is_in_bu_b || false, }; if (product.manufacture_id_i && product.manufacture_name_s) { productMapped.manufacture = { id: product.manufacture_id_i || '', name: product.manufacture_name_s || '', + logo: parent[0]?.x_logo_manufacture_s, }; } productMapped.parent = { |
