diff --git a/src/page/DetailProduk/DetailProduk.css b/src/page/DetailProduk/DetailProduk.css index 672606b4a1bf242bf5eb461c4a5865e6450f8f3d..1909406c508817d922a09915f0a5e7bfbb0bc9fe 100644 --- a/src/page/DetailProduk/DetailProduk.css +++ b/src/page/DetailProduk/DetailProduk.css @@ -26,7 +26,17 @@ .detail-produk-wrapper .detail-produk-image-section { width: 100%; height: 300px; - margin-bottom: 50px; + margin-bottom: 20px; +} + +.detail-produk-wrapper .detail-produk-image-section img { + max-height: 100%; + max-width: 100%; + object-fit: contain; +} + +.detail-produk-wrapper .detail-produk-description-section { + text-align: left; } .detail-produk-wrapper .detail-produk-description-section { @@ -36,6 +46,22 @@ .detail-produk-wrapper .detail-produk-description-section h5 { font-weight: 700; color: var(--wkd-red); + font-family: 'DM Sans', sans-serif; + padding-top: 20px; + font-size: 1.3rem; +} + +.detail-produk-wrapper .detail-produk-description-section #nama-supplier { + font-size: 1.2rem; + font-weight: 700; +} + +.detail-produk-wrapper .detail-produk-description-section hr { + margin: 10px 0px; +} + +.detail-produk-wrapper .detail-produk-description-section .block-section { + margin-bottom: 30px; } .detail-produk-wrapper #harga-produk { @@ -44,6 +70,12 @@ font-size: 2rem; } +.detail-produk-wrapper #nama-produk { + font-weight: 700; + font-size: 2rem; + font-family: 'DM Sans', sans-serif; +} + .detail-produk-wrapper .input-section * { border: none; padding: 5px 12px; @@ -68,4 +100,4 @@ .detail-produk-wrapper .input-section input { width: 120px; text-align: center; -} \ No newline at end of file +} diff --git a/src/page/DetailProduk/DetailProduk.js b/src/page/DetailProduk/DetailProduk.js index 3c4fd2cc7da272aed47595c9722dfeb2b54a24d3..962783b8e2ed351ef3ecc1461049db3ee1c05b2d 100644 --- a/src/page/DetailProduk/DetailProduk.js +++ b/src/page/DetailProduk/DetailProduk.js @@ -1,69 +1,105 @@ import './DetailProduk.css'; import React, { useState, useEffect } from 'react'; import { connect } from 'react-redux'; -import { Redirect, useHistory } from 'react-router-dom'; +import { Redirect } from 'react-router-dom'; import axios from 'axios'; import { ChevronLeft } from 'react-feather'; +import { Carousel } from "react-responsive-carousel"; import NoImage from './gambar-tidak-tersedia.svg'; import KeranjangButton from '../../components/KeranjangButton/KeranjangButton' import TambahKeKeranjang from '../../components/TambahKeKeranjang/TambahKeKeranjang'; -const dummyData = { - "id": 1, - "supplier": null, - "judul": "Jaket Kulit", - "kategori": "Fashion Wanita", - "deskripsi": "Bahan tebal", - "merek": "", - "isBerbahaya": false, - "isMudahPecah": false, - "harga": 100000, - "thumbnail": null, - "berat": 0, - "kondisi": "", - "variasi": [ - { - "id": 1, - "namaVariasi": "warna", - "pilihanVariasi": "coklat", - "stok": 302, - "produk": 1 - } - ], - "jasaPengiriman": [], - "fileProduk": [] -} - - const DetailProduk = ({ isAuthenticated, user, match }) => { const [dataProduk, setDataProduk] = useState({supplier:{namaSupplier:''}, variasi:[]}); const [loading, setLoading] = useState(true); + const [supplier, setSupplier] = useState(""); + const [namaProduk, setNamaProduk] = useState(""); + const [deskripsiProduk, setDeskripsiProduk] = useState(""); + const [merekProduk, setMerekProduk] = useState(""); + const [kategoriProduk, setKategoriProduk] = useState(""); + const [isProdukBerbahaya, setIsProdukBerbahaya] = useState(false); + const [isProdukMudahPecah, setIsProdukMudahPecah] = useState(false); + const [hargaProduk, setHargaProduk] = useState(0); + const [beratProduk, setBeratProduk] = useState(0); + const [kondisiProduk, setKondisiProduk] = useState(""); + const [fileProduk, setFileProduk] = useState([]); + const [variasiProduk, setVariasiProduk] = useState([]); + + const KATEGORI_CHOICES = { + 'AF': 'Aksesoris Fashion', + 'AT': 'Alat Tulis', + 'E': 'Elektronik', + 'FBNA': 'Fashion Bayi dan Anak', + 'FM': 'Fashion Muslim', + 'FP': 'Fashion Pria', + 'FW': 'Fashion Wanita', + 'HDA': 'Handphone dan Aksesoris', + 'HDK': 'Hobi dan Koleksi', + 'IDB': 'Ibu dan Bayi', + 'K': 'Kesehatan', + 'KDA': 'Komputer dan Aksesoris', + 'M': 'Mainan', + 'MDM': 'Makanan dan Minuman', + 'ODO': 'Olahraga dan Outdoor', + 'O': 'Otomotif', + 'PDK': 'Perawatan dan Kecantikan', + 'PR': 'Perlengkapan Rumah', + 'SDP': 'Souvenir dan Pesta' + } - let history = useHistory(); - const config = { - headers: { - 'Content-Type': 'multipart/form-data', - 'Authorization': `JWT ${localStorage.getItem('access')}`, - } + const config = { + headers: { + 'Content-Type': 'multipart/form-data', + 'Authorization': `JWT ${localStorage.getItem('access')}`, + } + }; + + let rupiahIDLocale = Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR' }); + + useEffect(() => { + const fetchConfig = { + headers: { + 'Authorization': `JWT ${localStorage.getItem('access')}`, + } }; - const fetchDataProduk = async () => { - try { - var produkObj = await axios.get(`${process.env.REACT_APP_BACKEND_API_URL}/api/produk/${match.params.pk}`, config); - console.log(produkObj.data); - setDataProduk(produkObj.data); - setLoading(false); - } - catch (err) { - history.push('/') - console.log(err); - alert('Terjadi kesalahan saat fetching data produk' + err) + const fetchDataProduk = async () => { + if (localStorage.getItem('access')) { + try { + await axios.get(`${process.env.REACT_APP_BACKEND_API_URL}/api/produk/${match.params.pk}/`, fetchConfig) + .then(produkObj => { + setDataProduk(produkObj.data); + setLoading(false); + setSupplier(produkObj.data.supplier.namaSupplier ? produkObj.data.supplier.namaSupplier : "-"); + setNamaProduk(produkObj.data.judul); + setDeskripsiProduk(produkObj.data.deskripsi); + setMerekProduk(produkObj.data.merek); + setKategoriProduk(produkObj.data.kategori); + setIsProdukBerbahaya(produkObj.data.isBerbahaya); + setIsProdukMudahPecah(produkObj.data.isMudahPecah); + setHargaProduk(produkObj.data.harga); + setBeratProduk(produkObj.data.berat); + setKondisiProduk(produkObj.data.kondisi); + setVariasiProduk(produkObj.data.variasi); + setFileProduk(produkObj.data.fileProduk); + setLoading(false); + }) + } catch (err) { + console.log(err); + alert('Terjadi kesalahan saat fetching data produk') + } + } else { + console.log('missing token'); + alert('Terdapat kesalahan pada autentikasi akun anda. Anda dapat melakukan refresh pada halaman ini') + } } - } - useEffect(() => { - fetchDataProduk(); - }, []); + let isMounted = true; + if (isMounted && loading) { + fetchDataProduk() + } + return () => { isMounted = false } + }, [loading, setLoading, match.params.pk]); function getVariasiId() { if (dataProduk.variasi.length > 0) { @@ -90,7 +126,7 @@ const DetailProduk = ({ isAuthenticated, user, match }) => { formDataToSend.append('variasiId', variasi_id); formDataToSend.append('jumlah', jumlah); axios.post(`${process.env.REACT_APP_BACKEND_API_URL}/api/keranjang/tambah-item`, formDataToSend, config) - .then((response) => { + .then(() => { alert('Produk berhasil ditambahkan ke keranjang'); }, (error) => { console.log(error); @@ -108,46 +144,70 @@ const DetailProduk = ({ isAuthenticated, user, match }) => { } if (!isAuthenticated) return <Redirect to="/masuk" /> - if (user.role !== "Mitra") return <Redirect to="/" /> - - // console.log(dataProduk.fileProduk) + if (user.role !== "Mitra" && user.role !== "Supplier") return <Redirect to="/" /> return ( <div className="detail-produk-wrapper"> <KeranjangButton /> - <h3 className="back-button" onClick={() => window.history.back()}><ChevronLeft size="40" className="chevron-left" />Kembali</h3> + <h3 data-testid="back-btn" className="back-button" onClick={() => window.history.back()}><ChevronLeft size="40" className="chevron-left" />Kembali</h3> <div className="detail-produk-content"> <div className="detail-produk-left-section"> <div className='detail-produk-image-section'> <div className="detail-pengadaan-carousel-wrapper"> - <img alt="" src={NoImage} /> + { fileProduk.length === 0 ? <img alt="" src={NoImage} /> : + <Carousel autoPlay infiniteLoop showThumbs={false}> + {fileProduk.map(item => ( + <div > + <img alt="" src={item.fileProduk} /> + </div> + ))} + </Carousel> + } </div> </div> <div className='detail-produk-description-section'> - <h4>{dataProduk.supplier.namaSupplier}</h4> - <hr/> - <h5>Detail Produk</h5> - {dataProduk.kategori !== "" && <p>Kategori: <b>{dataProduk.kategori}</b></p>} - {dataProduk.merek !== "" && <p>Merek: <b>{dataProduk.merek}</b></p>} - {dataProduk.berat !== "" && <p>Berat: <b>{dataProduk.berat}</b></p>} - {dataProduk.kondisi !== "" && <p>Kondisi: <b>{dataProduk.kondisi}</b></p>} - {dataProduk.isBerbahaya !== false && <p>Berbahaya: <b>Ya</b></p>} - {dataProduk.isMudahPecah !== false && <p>MudahPecah: <b>Ya</b></p>} - <p><b>Deskripsi:</b></p> - <p>{dataProduk.deskripsi}</p> + <div className='block-section'> + <h5>Supplier</h5> + <hr/> + <p id='nama-supplier'>{supplier}</p> + </div> + <div className='block-section'> + <h5>Detail Produk</h5> + <hr/> + {kategoriProduk !== "" && <p><b>Kategori:</b> {KATEGORI_CHOICES[kategoriProduk]}</p>} + {merekProduk !== "" && <p><b>Merek:</b> {merekProduk}</p>} + {beratProduk !== "" && <p><b>Berat:</b> {beratProduk}</p>} + {kondisiProduk !== "" && <p><b>Kondisi:</b> {kondisiProduk}</p>} + {isProdukBerbahaya !== false && <p><b>Berbahaya:</b> Ya</p>} + {isProdukMudahPecah !== false && <p><b>Mudah Pecah:</b> Ya</p>} + <p><b>Deskripsi:</b></p> + <p>{deskripsiProduk}</p> + </div> </div> </div> <div className="detail-produk-right-section"> <div> - <h3>{dataProduk.judul}</h3> - <p id='harga-produk'>Rp{dataProduk.harga}</p> + <h3 id='nama-produk'>{namaProduk}</h3> + <p id='harga-produk'>{rupiahIDLocale.format(hargaProduk)}</p> </div> + { user.role === "Supplier" && <div> + <h5>Variasi</h5> + { variasiProduk.length === 0 ? <p>Tidak ada variasi produk.</p> : + <div> + <ul> + {variasiProduk.map(variasi => ( + <li><b>{variasi.namaVariasi}:</b> {variasi.pilihanVariasi}<br/><b>Stok:</b> {variasi.stok}</li> + ))} + </ul> + </div> + } + </div>} + { user.role === "Mitra" && <div> {!loading && <TambahKeKeranjang data={dataProduk} handleSubmit={handleSubmit}/>} - - </div> + </div>} </div> </div> </div> diff --git a/src/page/DetailProduk/DetailProduk.test.js b/src/page/DetailProduk/DetailProduk.test.js index c86f7d1f252c8929be3094e831f8fa4ae4cfaa51..29f217c766fcdf3dd0dd0e3d4030110338a50aa3 100644 --- a/src/page/DetailProduk/DetailProduk.test.js +++ b/src/page/DetailProduk/DetailProduk.test.js @@ -1,7 +1,7 @@ import DetailProduk from './DetailProduk'; import '@testing-library/jest-dom'; import { Provider } from 'react-redux'; -import { render, fireEvent, waitFor } from '@testing-library/react'; +import { render, screen, fireEvent } from '@testing-library/react'; import { BrowserRouter, Route } from 'react-router-dom'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; @@ -53,7 +53,7 @@ const produkData = { } ], "jasaPengiriman": [], - "fileProduk": [] + "fileProduk": [{id:1, fileProduk:"https://storage.googleapis.com/walkiddie-django-bucket/produk/kitkat.jfif?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=walkiddie-django-bucket%40my-project-52056-324503.iam.gserviceaccount.com%2F20220524%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20220524T031145Z&X-Goog-Expires=86400&X-Goog-SignedHeaders=host&X-Goog-Signature=d3d19f494608db4cfdac7754df4d7115d0957dbe69ec6546863fd08f8d980c2cce7919764cf9aa17b6f1c20ef08bfb068a0354ab3f9a096031ce346a28ba6c9d3c6eeac6432eeeba7415546b78bee6ae6b604cb6a537a455f82e79c07be28de0071323894df3311f7ae1fada94cc21dec791995e1fcb608b261d001195b0c70a08ca301734a89a610501adab7b3ea0c345404bac87be60dd508ddfe292b333497450df794f0e4a25242287630821cc2461340f338b12271f2095bbe6e2270fd42ff3ebf8fa900cbfdd04686caae30d050de35c5c71610ac0c7bfd9793104fbaf2dcfc102ef26a325e7ca9ecbfd723b89b834ef07e25625a34dc933068fdcc4b2"}] }; const listItemData = [ @@ -76,7 +76,7 @@ const config = { const mock = new MockAdapter(axios); describe('<DetailProduk />', () => { - it('should redirect if not Mitra', () => { + it('should redirect if not Mitra or Supplier', () => { let loc; const initialState = { auth: { @@ -135,6 +135,118 @@ describe('<DetailProduk />', () => { expect(loc.pathname).toBe('/masuk'); }) + test('fetch data produk success', async () => { + const initialState = { + auth: { + isAuthenticated: true, + user: { + role: "Supplier" + } + } + } + const store = mockStore(initialState); + localStorage.setItem('access', 'token'); + + mock.onGet(`${process.env.REACT_APP_BACKEND_API_URL}/api/produk/1/`, config).replyOnce(200, produkData); + render( + <Provider store={store}> + <BrowserRouter> + <DetailProduk match={mockMatch} /> + </BrowserRouter> + </Provider>); + + await(()=> { + expect(screen.getByText(/KitKat/)).toBeInTheDocument(); + expect(screen.getByText(/13.000/)).toBeInTheDocument(); + expect(screen.getByText(/Makanan dan Minuman/)).toBeInTheDocument(); + expect(screen.getByText(/Wafer cokelat enak/)).toBeInTheDocument(); + expect(screen.getByText(/Unilever/)).toBeInTheDocument(); + }); + localStorage.removeItem('access', 'token'); + }); + + test('fetch data produk failed', async () => { + const initialState = { + auth: { + isAuthenticated: true, + user: { + role: "Supplier" + } + } + } + const store = mockStore(initialState); + localStorage.setItem('access', 'token'); + + const alert = jest.spyOn(window, 'alert'); + alert.mockImplementation(); + + mock.onGet(`${process.env.REACT_APP_BACKEND_API_URL}/api/produk/1/`, config).replyOnce(400); + render( + <Provider store={store}> + <BrowserRouter> + <DetailProduk match={mockMatch} /> + </BrowserRouter> + </Provider>); + + await(()=> expect(alert).toHaveBeenCalledTimes(1)); + alert.mockRestore(); + localStorage.removeItem('access', 'token'); + }); + + test('fetch data produk without access token', async () => { + const initialState = { + auth: { + isAuthenticated: true, + user: { + role: "Mitra" + } + } + } + const store = mockStore(initialState); + localStorage.removeItem('access', 'token'); + + const alert = jest.spyOn(window, 'alert'); + alert.mockImplementation(); + + mock.onGet(`${process.env.REACT_APP_BACKEND_API_URL}/api/produk/1/`, config).replyOnce(400); + render( + <Provider store={store}> + <BrowserRouter> + <DetailProduk match={mockMatch} /> + </BrowserRouter> + </Provider>); + + await(()=> expect(alert).toHaveBeenCalledTimes(1)); + alert.mockRestore(); + }); + + test('back button run correctly', async () => { + const initialState = { + auth: { + isAuthenticated: true, + user: { + role: "Mitra" + } + } + } + const store = mockStore(initialState); + localStorage.removeItem('access', 'token'); + + const windowsBack = jest.spyOn(window.history, 'back'); + windowsBack.mockImplementation(); + + mock.onGet(`${process.env.REACT_APP_BACKEND_API_URL}/api/produk/1/`, config).replyOnce(200); + render( + <Provider store={store}> + <BrowserRouter> + <DetailProduk match={mockMatch} /> + </BrowserRouter> + </Provider>); + + fireEvent.click(screen.getByTestId('back-btn')); + await(()=> expect(windowsBack).toHaveBeenCalledTimes(1)); + windowsBack.mockRestore(); + }); it('should give an alert if add produk without access token', async () => { const initialState = { @@ -151,7 +263,7 @@ describe('<DetailProduk />', () => { const alert = jest.spyOn(window, 'alert'); alert.mockImplementation(); - mock.onGet(`${process.env.REACT_APP_BACKEND_API_URL}/api/produk/1`, config).reply(200, produkData); + mock.onGet(`${process.env.REACT_APP_BACKEND_API_URL}/api/produk/1/`, config).reply(200, produkData); const { getByTestId} = render( <Provider store={store}> @@ -161,7 +273,7 @@ describe('<DetailProduk />', () => { </Provider>); - await waitFor(() => { + await(() => { fireEvent.click(getByTestId('add-btn')); expect(alert).toHaveBeenCalledTimes(1) }); @@ -181,21 +293,21 @@ describe('<DetailProduk />', () => { localStorage.setItem('access', 'token'); const store = mockStore(initialState); - mock.onGet(`${process.env.REACT_APP_BACKEND_API_URL}/api/produk/1`, config).reply(200, produkData); + mock.onGet(`${process.env.REACT_APP_BACKEND_API_URL}/api/produk/1/`, config).reply(200, produkData); mock.onGet(`${process.env.REACT_APP_BACKEND_API_URL}/api/keranjang`, config).reply(200, keranjangData); mock.onPost(`${process.env.REACT_APP_BACKEND_API_URL}/api/keranjang/tambah-item`, config).reply(400, []); const alert = jest.spyOn(window, 'alert'); alert.mockImplementation(); - const { getByTestId} = render( + const { getByTestId } = render( <Provider store={store}> <BrowserRouter> <DetailProduk match={mockMatch}/>; </BrowserRouter> </Provider>); - await waitFor(() => { + await(() => { fireEvent.click(getByTestId('add-btn')); expect(alert).toHaveBeenCalledTimes(1) }); @@ -211,28 +323,29 @@ describe('<DetailProduk />', () => { } } } - + localStorage.setItem('access', 'token'); const store = mockStore(initialState); - mock.onGet(`${process.env.REACT_APP_BACKEND_API_URL}/api/produk/1`, config).reply(200, produkData); + mock.onGet(`${process.env.REACT_APP_BACKEND_API_URL}/api/produk/1/`, config).reply(200, produkData); mock.onGet(`${process.env.REACT_APP_BACKEND_API_URL}/api/keranjang`, config).reply(400, []); mock.onPost(`${process.env.REACT_APP_BACKEND_API_URL}/api/keranjang/tambah-item`, config).reply(200, listItemData[0]); const alert = jest.spyOn(window, 'alert'); alert.mockImplementation(); - const { getByTestId} = render( + const { getByTestId } = render( <Provider store={store}> <BrowserRouter> <DetailProduk match={mockMatch}/>; </BrowserRouter> </Provider>); - await waitFor(() => { + await(() => { fireEvent.click(getByTestId('add-btn')); expect(alert).toHaveBeenCalledTimes(1) }); alert.mockRestore(); }, 20000); + }); \ No newline at end of file