This commit is contained in:
karyamanswasta
2025-08-26 13:07:13 +07:00
parent 67cf759b31
commit b28c6ed0fe
14 changed files with 656 additions and 755 deletions

58
note/detailUI.md Normal file
View File

@@ -0,0 +1,58 @@
**Overlay/Status**
- Loader Overlay: layar penuh dengan spinner `ThreeDots` dan pesan `screenMessag
e`; muncul saat `loading` true.
- Grayscale State: seluruh konten `div.Cafe` menjadi abu-abu saat `isExceededDea
dline` true (kelas `grayscale`).
- Modal Pesan: “Kafe sedang tidak tersedia” dapat muncul via `setModal("message"
, ...)` ketika `isExceededDeadline` dari socket; modalnya dikelola global (di lu
ar file ini).
**Header**
- Bar Atas “Menu”: judul “Menu”, info toko (nama, gambar), profil user, opsi log
out, akses pegawai, table code.
- Edit Mode Toggle: kontrol untuk mengaktifkan/menonaktifkan mode edit kategori/
item (prop `isEditMode` + `setIsEditMode`).
**Branding/Watermark**
- Dev Watermark (atas): `div.Watermark` kecil hanya tampil jika `API_BASE_URL` b
ukan domain resmi prod/dev (indikator environment).
- Footer Watermark: komponen `<Watermark />` di bagian paling bawah halaman.
**Music**
- Music Player: widget pemutar/antrian lagu dengan dukungan Spotify; menampilkan
status login Spotify (`isSpotifyNeedLogin`) dan antrian (`queue`), interaksi vi
a `socket`.
**Kategori (ItemTypeLister)**
- Daftar Kategori: list tipe menu (nama + gambar tipe, visibilitas).
- Filter Kategori: memilih 1 kategori untuk menyaring tampilan item (`filterId`)
.
- Edit Kategori: saat `isEditMode` aktif, bisa memilih tipe yang sedang diedit (
`beingEditedType`) dan mengubah urutan tipe (via kontrol di ItemLister).
**Daftar Item per Kategori (ItemLister x N)**
- Section per Kategori: render berulang untuk setiap tipe yang lolos filter. Men
ampilkan:
- Judul/nama kategori dan gambar (bila ada).
- Daftar item dalam kategori (nama, harga, promo, deskripsi, gambar, visibilit
as).
- Aksi Item:
- Tambah item: tombol/form untuk membuat item (owner/akses yang berwenang).
- Ubah item: edit nama, harga, promo, deskripsi, gambar.
- Reorder Kategori: panah/aksi “naik/turun” pada section untuk memindahkan posis
i kategori (memanggil `moveItemType*`).
- Raw Mode: jika tidak edit dan filter spesifik aktif, `raw` true untuk gaya tam
pilan ringkas.
**Sticky Bar (Keranjang & Transaksi)**
- Tombol Cart (kiri, utama): muncul jika bukan edit mode dan (user login atau ke
ranjang > 0).
- Menampilkan jumlah item (dengan “+” jika ada transaksi terakhir), total harg
a “Rp{totalPrice}” atau teks “Open bill” (jika `lastTransaction.payment_type ==
'paylater'`).
- Posisi: sticky di bawah (offset bottom ~40px), lebar responsif.
- Tombol Transactions (kanan, kecil): hanya muncul jika user login; navigasi ke
riwayat transaksi.
material list, material

219
package-lock.json generated
View File

@@ -14,12 +14,14 @@
"@testing-library/jest-dom": "^5.17.0", "@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0", "@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0", "@testing-library/user-event": "^13.5.0",
"apexcharts": "^5.3.4",
"caniuse-lite": "^1.0.30001690", "caniuse-lite": "^1.0.30001690",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"html-to-image": "^1.11.11", "html-to-image": "^1.11.11",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"jsqr": "^1.4.0", "jsqr": "^1.4.0",
"lucide-react": "^0.541.0",
"qr-scanner": "^1.4.2", "qr-scanner": "^1.4.2",
"qrcode.react": "^3.1.0", "qrcode.react": "^3.1.0",
"react": "^18.3.1", "react": "^18.3.1",
@@ -3905,171 +3907,6 @@
"integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@mui/core-downloads-tracker": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.1.tgz",
"integrity": "sha512-+mIK1Z0BhOaQ0vCgOkT1mSrIpEHLo338h4/duuL4TBLXPvUMit732mnwJY3W40Avy30HdeSfwUAAGRkKmwRaEQ==",
"license": "MIT",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
}
},
"node_modules/@mui/material": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.1.tgz",
"integrity": "sha512-Xf6Shbo03YmcBedZMwSpEFOwpYDtU7tC+rhAHTrA9FHk0FpsDqiQ9jUa1j/9s3HLs7KWb5mDcGnlwdh9Q9KAag==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.28.2",
"@mui/core-downloads-tracker": "^7.3.1",
"@mui/system": "^7.3.1",
"@mui/types": "^7.4.5",
"@mui/utils": "^7.3.1",
"@popperjs/core": "^2.11.8",
"@types/react-transition-group": "^4.4.12",
"clsx": "^2.1.1",
"csstype": "^3.1.3",
"prop-types": "^15.8.1",
"react-is": "^19.1.1",
"react-transition-group": "^4.4.5"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
"@mui/material-pigment-css": "^7.3.1",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
},
"@mui/material-pigment-css": {
"optional": true
},
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/private-theming": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.3.1.tgz",
"integrity": "sha512-WU3YLkKXii/x8ZEKnrLKsPwplCVE11yZxUvlaaZSIzCcI3x2OdFC8eMlNy74hVeUsYQvzzX1Es/k4ARPlFvpPQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.28.2",
"@mui/utils": "^7.3.1",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/styled-engine": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.3.1.tgz",
"integrity": "sha512-Nqo6OHjvJpXJ1+9TekTE//+8RybgPQUKwns2Lh0sq+8rJOUSUKS3KALv4InSOdHhIM9Mdi8/L7LTF1/Ky6D6TQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.28.2",
"@emotion/cache": "^11.14.0",
"@emotion/serialize": "^1.3.3",
"@emotion/sheet": "^1.4.0",
"csstype": "^3.1.3",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.4.1",
"@emotion/styled": "^11.3.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
}
}
},
"node_modules/@mui/system": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.1.tgz",
"integrity": "sha512-mIidecvcNVpNJMdPDmCeoSL5zshKBbYPcphjuh6ZMjhybhqhZ4mX6k9zmIWh6XOXcqRQMg5KrcjnO0QstrNj3w==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.28.2",
"@mui/private-theming": "^7.3.1",
"@mui/styled-engine": "^7.3.1",
"@mui/types": "^7.4.5",
"@mui/utils": "^7.3.1",
"clsx": "^2.1.1",
"csstype": "^3.1.3",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
},
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/types": { "node_modules/@mui/types": {
"version": "7.4.5", "version": "7.4.5",
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.5.tgz", "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.5.tgz",
@@ -4638,7 +4475,6 @@
"resolved": "https://registry.npmjs.org/@svgdotjs/svg.draggable.js/-/svg.draggable.js-3.0.6.tgz", "resolved": "https://registry.npmjs.org/@svgdotjs/svg.draggable.js/-/svg.draggable.js-3.0.6.tgz",
"integrity": "sha512-7iJFm9lL3C40HQcqzEfezK2l+dW2CpoVY3b77KQGqc8GXWa6LhhmX5Ckv7alQfUXBuZbjpICZ+Dvq1czlGx7gA==", "integrity": "sha512-7iJFm9lL3C40HQcqzEfezK2l+dW2CpoVY3b77KQGqc8GXWa6LhhmX5Ckv7alQfUXBuZbjpICZ+Dvq1czlGx7gA==",
"license": "MIT", "license": "MIT",
"peer": true,
"peerDependencies": { "peerDependencies": {
"@svgdotjs/svg.js": "^3.2.4" "@svgdotjs/svg.js": "^3.2.4"
} }
@@ -4648,7 +4484,6 @@
"resolved": "https://registry.npmjs.org/@svgdotjs/svg.filter.js/-/svg.filter.js-3.0.9.tgz", "resolved": "https://registry.npmjs.org/@svgdotjs/svg.filter.js/-/svg.filter.js-3.0.9.tgz",
"integrity": "sha512-/69XMRCDoam2HgC4ldHIaDgeQf1ViHIsa0Ld4uWgiXtZ+E24DWHe/9Ib6kbNiZ7WRIdlVokUDR1Fg0kjIpkfbw==", "integrity": "sha512-/69XMRCDoam2HgC4ldHIaDgeQf1ViHIsa0Ld4uWgiXtZ+E24DWHe/9Ib6kbNiZ7WRIdlVokUDR1Fg0kjIpkfbw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@svgdotjs/svg.js": "^3.2.4" "@svgdotjs/svg.js": "^3.2.4"
}, },
@@ -4661,7 +4496,6 @@
"resolved": "https://registry.npmjs.org/@svgdotjs/svg.js/-/svg.js-3.2.4.tgz", "resolved": "https://registry.npmjs.org/@svgdotjs/svg.js/-/svg.js-3.2.4.tgz",
"integrity": "sha512-BjJ/7vWNowlX3Z8O4ywT58DqbNRyYlkk6Yz/D13aB7hGmfQTvGX4Tkgtm/ApYlu9M7lCQi15xUEidqMUmdMYwg==", "integrity": "sha512-BjJ/7vWNowlX3Z8O4ywT58DqbNRyYlkk6Yz/D13aB7hGmfQTvGX4Tkgtm/ApYlu9M7lCQi15xUEidqMUmdMYwg==",
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/Fuzzyma" "url": "https://github.com/sponsors/Fuzzyma"
@@ -4672,7 +4506,6 @@
"resolved": "https://registry.npmjs.org/@svgdotjs/svg.resize.js/-/svg.resize.js-2.0.5.tgz", "resolved": "https://registry.npmjs.org/@svgdotjs/svg.resize.js/-/svg.resize.js-2.0.5.tgz",
"integrity": "sha512-4heRW4B1QrJeENfi7326lUPYBCevj78FJs8kfeDxn5st0IYPIRXoTtOSYvTzFWgaWWXd3YCDE6ao4fmv91RthA==", "integrity": "sha512-4heRW4B1QrJeENfi7326lUPYBCevj78FJs8kfeDxn5st0IYPIRXoTtOSYvTzFWgaWWXd3YCDE6ao4fmv91RthA==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">= 14.18" "node": ">= 14.18"
}, },
@@ -4686,7 +4519,6 @@
"resolved": "https://registry.npmjs.org/@svgdotjs/svg.select.js/-/svg.select.js-4.0.3.tgz", "resolved": "https://registry.npmjs.org/@svgdotjs/svg.select.js/-/svg.select.js-4.0.3.tgz",
"integrity": "sha512-qkMgso1sd2hXKd1FZ1weO7ANq12sNmQJeGDjs46QwDVsxSRcHmvWKL2NDF7Yimpwf3sl5esOLkPqtV2bQ3v/Jg==", "integrity": "sha512-qkMgso1sd2hXKd1FZ1weO7ANq12sNmQJeGDjs46QwDVsxSRcHmvWKL2NDF7Yimpwf3sl5esOLkPqtV2bQ3v/Jg==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">= 14.18" "node": ">= 14.18"
}, },
@@ -4924,26 +4756,6 @@
"tslib": "^2.8.0" "tslib": "^2.8.0"
} }
}, },
"node_modules/@testing-library/dom": {
"version": "10.4.1",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.10.4",
"@babel/runtime": "^7.12.5",
"@types/aria-query": "^5.0.1",
"aria-query": "5.3.0",
"dom-accessibility-api": "^0.5.9",
"lz-string": "^1.5.0",
"picocolors": "1.1.1",
"pretty-format": "^27.0.2"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@testing-library/jest-dom": { "node_modules/@testing-library/jest-dom": {
"version": "5.17.0", "version": "5.17.0",
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.17.0.tgz", "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.17.0.tgz",
@@ -5976,8 +5788,7 @@
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz", "resolved": "https://registry.npmjs.org/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz",
"integrity": "sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==", "integrity": "sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==",
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/@zxing/browser": { "node_modules/@zxing/browser": {
"version": "0.0.7", "version": "0.0.7",
@@ -6315,7 +6126,6 @@
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-5.3.4.tgz", "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-5.3.4.tgz",
"integrity": "sha512-N0gNh8uLu/BN8N+BCphNK+gZAoSoUtDDn1jFGB+3+EMcv8s6vajuP3W0g4dMLTRp6chFkjMmQK3uD8pz4ISmLA==", "integrity": "sha512-N0gNh8uLu/BN8N+BCphNK+gZAoSoUtDDn1jFGB+3+EMcv8s6vajuP3W0g4dMLTRp6chFkjMmQK3uD8pz4ISmLA==",
"license": "SEE LICENSE IN LICENSE", "license": "SEE LICENSE IN LICENSE",
"peer": true,
"dependencies": { "dependencies": {
"@svgdotjs/svg.draggable.js": "^3.0.4", "@svgdotjs/svg.draggable.js": "^3.0.4",
"@svgdotjs/svg.filter.js": "^3.0.8", "@svgdotjs/svg.filter.js": "^3.0.8",
@@ -15199,6 +15009,15 @@
"yallist": "^3.0.2" "yallist": "^3.0.2"
} }
}, },
"node_modules/lucide-react": {
"version": "0.541.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.541.0.tgz",
"integrity": "sha512-s0Vircsu5WaGv2KoJZ5+SoxiAJ3UXV5KqEM3eIFDHaHkcLIFdIWgXtZ412+Gh02UsdS7Was+jvEpBvPCWQISlg==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/lz-string": { "node_modules/lz-string": {
"version": "1.5.0", "version": "1.5.0",
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
@@ -20825,20 +20644,6 @@
"is-typedarray": "^1.0.0" "is-typedarray": "^1.0.0"
} }
}, },
"node_modules/typescript": {
"version": "5.9.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/unbox-primitive": { "node_modules/unbox-primitive": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",

View File

@@ -10,12 +10,14 @@
"@testing-library/jest-dom": "^5.17.0", "@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0", "@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0", "@testing-library/user-event": "^13.5.0",
"apexcharts": "^5.3.4",
"caniuse-lite": "^1.0.30001690", "caniuse-lite": "^1.0.30001690",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"html-to-image": "^1.11.11", "html-to-image": "^1.11.11",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"jsqr": "^1.4.0", "jsqr": "^1.4.0",
"lucide-react": "^0.541.0",
"qr-scanner": "^1.4.2", "qr-scanner": "^1.4.2",
"qrcode.react": "^3.1.0", "qrcode.react": "^3.1.0",
"react": "^18.3.1", "react": "^18.3.1",

View File

@@ -1,6 +1,14 @@
@import url("https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap"); @import url("https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Aboreto&family=Rubik+Doodle+Shadow&display=swap"); @import url("https://fonts.googleapis.com/css2?family=Aboreto&family=Rubik+Doodle+Shadow&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@200;300;400;500;600;700;800&ital,wght@0,200..800;1,200..800&display=swap"); @import url("https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@200;300;400;500;600;700;800&ital,wght@0,200..800;1,200..800&display=swap");
:root {
--brand-primary: #73a585; /* general brand (e.g., music player) */
--brand-sage: #6B8F71; /* sage green for active category */
--brand-sage-50: #F0F6F2; /* very light hover bg */
--brand-sage-100: #E9F3ED; /* light hover bg */
--brand-sage-hover: #7FAE7D; /* hover for filled buttons */
--brand-sage-muted: #CFD8D3; /* disabled button */
}
html, html,
body { body {
scrollbar-width: none; /* Firefox */ scrollbar-width: none; /* Firefox */
@@ -54,6 +62,8 @@ body {
color: white; color: white;
} }
/* removed two-column layout; reverted to single column */
.App-link { .App-link {
color: #61dafb; color: #61dafb;
} }

View File

@@ -4,6 +4,7 @@ import { useLocation } from "react-router-dom";
import { useNavigationHelpers } from "../helpers/navigationHelpers"; import { useNavigationHelpers } from "../helpers/navigationHelpers";
import Switch from "react-switch"; import Switch from "react-switch";
// Restore original gradient background for header container when shopName exists
const HeaderBarbackground = styled.div` const HeaderBarbackground = styled.div`
${({ shopName }) => ${({ shopName }) =>
shopName && shopName &&
@@ -19,7 +20,7 @@ const HeaderBar = styled.div`
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 20px 15px; padding: 12px 14px;
color: black; color: black;
background-color: #ffffff; background-color: #ffffff;
z-index: 200; z-index: 200;
@@ -33,30 +34,15 @@ const HeaderBar = styled.div`
const Title = styled.h2` const Title = styled.h2`
margin: 0; margin: 0;
font-family: "Plus Jakarta Sans", sans-serif; font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 500; font-weight: 600;
font-style: normal; font-style: normal;
font-size:${(props) => (props.HeaderSize)}; font-size:${(props) => (props.HeaderSize)};
color: rgba(88, 55, 50, 1); color: rgba(45, 45, 45, 1);
text-transform: uppercase;
`; `;
const ProfileName = styled.h2` // SubTitle removed per redesign (no subtitle below cafe name)
position: absolute;
font-family: "Plus Jakarta Sans", sans-serif; // Deprecated the animated ProfileName in favor of a cleaner layout
font-weight: 500;
font-style: normal;
font-size: 30px;
z-index: 199;
overflow: hidden;
white-space: nowrap;
animation: ${(props) => {
if (props.animate === "grow") return gg;
if (props.animate === "shrink") return ss;
return nn;
}}
0.5s forwards;
text-align: left;
`;
const nn = keyframes` const nn = keyframes`
0% { 0% {
@@ -103,22 +89,17 @@ const ss = keyframes`
} }
`; `;
const ProfileImage = styled.img` const CafeAvatar = styled.img`
position: relative; width: clamp(32px, 5vw, 56px);
width: 60px; height: clamp(32px, 5vw, 56px);
height: 60px; border-radius: 8px;
border-radius: 50%; object-fit: cover;
object-fit: contain; background: #f2f2f2;
cursor: pointer; margin-left: 8px; /* extra left padding so its not too tight */
z-index: 199;
animation: ${(props) => {
if (props.animate === "grow") return g;
if (props.animate === "shrink") return s;
return "none";
}}
0.5s forwards;
`; `;
// User initial avatar removed; only cafe image is shown on the left
const g = keyframes` const g = keyframes`
0% { 0% {
top: 0px; top: 0px;
@@ -149,62 +130,43 @@ const s = keyframes`
} }
`; `;
/* Replace bubble-like animation with subtle fade/slide */
const grow = keyframes` const grow = keyframes`
0% { 0% { opacity: 0; transform: translateY(-6px) scale(0.98); }
right: 12px; 100% { opacity: 1; transform: translateY(0) scale(1); }
width: 60px;
height: 60px;
border-top-left-radius: 50%;
border-bottom-left-radius: 50%;
}
100% {
right: 28px;
width: 300px;
border-top-left-radius: 15px;
border-bottom-left-radius: 15px;
}
`; `;
const shrink = keyframes` const shrink = keyframes`
0% { 0% { opacity: 1; transform: translateY(0) scale(1); }
right: 12px; 100% { opacity: 0; transform: translateY(-6px) scale(0.98); }
width: 300px;
height: auto;
border-radius: 20px;
}
100% {
right: 28px;
width: 60px;
height: 60px;
border-radius: 50%;
}
`; `;
const Rectangle = styled.div` const Rectangle = styled.div`
overflow-y: hidden; overflow-y: auto;
position: absolute; position: absolute;
top: 39px; top: calc(100% + 8px);
right: 12px; right: 0;
width: 200px; width: 240px;
max-height: 87vh; /* or another appropriate value */ max-height: 75vh;
background-color: white; background-color: white;
z-index: 198; z-index: 198;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
animation: ${(props) => (props.animate === "grow" ? grow : shrink)} 0.5s border: 1px solid #f0f0f0;
forwards; border-radius: 12px;
padding: 10px; animation: ${(props) => (props.animate === "grow" ? grow : shrink)} 0.2s forwards;
padding: 10px 12px;
box-sizing: border-box; box-sizing: border-box;
overflow-x: hidden; overflow-x: hidden;
font-size: 14px; font-size: 14px;
color: #393939; color: #393939;
backdrop-filter: blur(2px);
`; `;
const ChildContainer = styled.div` const ChildContainer = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: flex-start; justify-content: flex-start;
align-items: flex-end; align-items: stretch;
flex-wrap: wrap; flex-wrap: nowrap;
padding-top: 70px;
`; `;
const ChildWrapper = styled.div` const ChildWrapper = styled.div`
@@ -213,25 +175,88 @@ const ChildWrapper = styled.div`
width: 100%; width: 100%;
`; `;
const Child = styled.div` const Child = styled.div`
width: 100%; width: 100%;
height: 36px; min-height: 36px;
font-family: "Plus Jakarta Sans", sans-serif; font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 500; font-weight: 500;
font-style: normal; font-style: normal;
padding: ${(props) => (props.hasChildren ? '8px 0 4px' : '8px 4px')};
${(props) => ${(props) =>
props.hasChildren props.hasChildren
? ` ? `
margin-top: 14px; margin-top: 10px;
border-top: 0.5px solid #a5a5a5; border-top: 0.5px solid #e9e9e9;
height: auto; height: auto;
` `
: ` : `
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between;
cursor: pointer;
`} `}
`; `;
const LeftGroup = styled.div`
display: flex;
align-items: center;
gap: 10px;
`;
const CenterGroup = styled.div`
flex: 1;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none; /* so clicks pass through center when needed */
`;
const CafeInfo = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
`;
const RightGroup = styled.div`
display: flex;
align-items: center;
gap: 10px;
`;
const CategoryLabel = styled.div`
font-family: "Plus Jakarta Sans", sans-serif;
font-size: 12px;
font-weight: 700;
color: #6B8F71;
text-transform: uppercase;
letter-spacing: 0.04em;
padding: 6px 2px;
pointer-events: none;
`;
const HamburgerButton = styled.button`
width: clamp(32px, 4.5vw, 52px);
height: clamp(32px, 4.5vw, 52px);
border-radius: 8px;
border: 1px solid #e5e5e5;
background: #ffffff;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
& > svg {
width: 60%;
height: 60%;
}
`;
const HamburgerIcon = () => (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="3" y="6" width="18" height="2" rx="1" fill="#2d2d2d"/>
<rect x="3" y="11" width="18" height="2" rx="1" fill="#2d2d2d"/>
<rect x="3" y="16" width="18" height="2" rx="1" fill="#2d2d2d"/>
</svg>
);
const Header = ({ const Header = ({
HeaderText, HeaderText,
@@ -261,10 +286,10 @@ const Header = ({
const [guestSideOf, setGuestSideOf] = useState(null); const [guestSideOf, setGuestSideOf] = useState(null);
const location = useLocation(); const location = useLocation();
const handleImageClick = () => { const toggleMenu = () => {
if (showRectangle) { if (showRectangle) {
setAnimate("shrink"); setAnimate("shrink");
setTimeout(() => setShowRectangle(false), 500); setTimeout(() => setShowRectangle(false), 200);
} else { } else {
setAnimate("grow"); setAnimate("grow");
setShowRectangle(true); setShowRectangle(true);
@@ -274,15 +299,14 @@ const Header = ({
const handleClickOutside = (event) => { const handleClickOutside = (event) => {
if (rectangleRef.current && !rectangleRef.current.contains(event.target)) { if (rectangleRef.current && !rectangleRef.current.contains(event.target)) {
setAnimate("shrink"); setAnimate("shrink");
setTimeout(() => setShowRectangle(false), 500); setTimeout(() => setShowRectangle(false), 200);
rectangleRef.current.style.overflow = "hidden";
} }
}; };
const handleScroll = () => { const handleScroll = () => {
if (showRectangle) { if (showRectangle) {
setAnimate("shrink"); setAnimate("shrink");
setTimeout(() => setShowRectangle(false), 500); setTimeout(() => setShowRectangle(false), 200);
} }
}; };
@@ -321,38 +345,48 @@ const Header = ({
// Otherwise, use the possessive function // Otherwise, use the possessive function
return `${cafeName}'s menu`; return `${cafeName}'s menu`;
}; };
const formatCafeName = (name) => {
if (!name) return name;
return name
.toLowerCase()
.replace(/\b\w/g, (c) => c.toUpperCase());
};
return ( return (
<HeaderBarbackground shopName={shopName}> <HeaderBarbackground shopName={shopName}>
<HeaderBar HeaderMargin={HeaderMargin} shopName={shopName}> <HeaderBar HeaderMargin={HeaderMargin} shopName={shopName}>
<Title HeaderSize={HeaderSize}> <LeftGroup>
{shopName == null <CafeAvatar
? HeaderText == null src={shopImage && !shopImage.includes('undefined') ? shopImage : "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcS-DjX_bGBax4NL14ULvkAdU4FP3FKoWXWu5w&s"}
? "kedaimaster" alt="Cafe"
: HeaderText />
: shopName} </LeftGroup>
</Title> <CenterGroup>
<div style={{ visibility: showProfile ? "visible" : "hidden" }}> <Title HeaderSize={HeaderSize}>
<ProfileImage {formatCafeName(
src={shopImage && !shopImage.includes('undefined') ? shopImage || 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcS-DjX_bGBax4NL14ULvkAdU4FP3FKoWXWu5w&s' : "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcS-DjX_bGBax4NL14ULvkAdU4FP3FKoWXWu5w&s"} shopName == null
alt="Profile" ? HeaderText == null
onClick={user.username !== undefined ? handleImageClick : null} ? "kedaimaster"
animate={showRectangle && animate} : HeaderText
/> : shopName
<ProfileName animate={showRectangle && animate}> )}
{showProfile && user.username !== undefined ? user.username : "guest"} </Title>
</ProfileName> </CenterGroup>
{showRectangle && (
<Rectangle ref={rectangleRef} animate={animate}> <RightGroup style={{ visibility: showProfile ? "visible" : "hidden", position: 'relative' }}>
<ChildContainer> <HamburgerButton onClick={toggleMenu} aria-label="Open menu">
<HamburgerIcon />
</HamburgerButton>
{showRectangle && (
<Rectangle ref={rectangleRef} animate={animate}>
<ChildContainer>
{guestSideOfClerk && guestSideOfClerk.clerkUsername && ( {guestSideOfClerk && guestSideOfClerk.clerkUsername && (
<Child hasChildren> <Child hasChildren>
this is the guest side of {guestSideOfClerk.clerkUsername} this is the guest side of {guestSideOfClerk.clerkUsername}
</Child> </Child>
)} )}
{user.username !== undefined && ( {user.username !== undefined && (
<Child onClick={() => setModal("edit_account")}> <CategoryLabel>Kelola akun</CategoryLabel>
Kelola akun
</Child>
)} )}
{user.roleId == 0 && ( {user.roleId == 0 && (
<Child onClick={()=>setModal('create_coupon', {})}>Buat Voucher</Child>)} <Child onClick={()=>setModal('create_coupon', {})}>Buat Voucher</Child>)}
@@ -364,9 +398,9 @@ const Header = ({
user.roleId === 1 && ( user.roleId === 1 && (
<> <>
<Child hasChildren> <Child hasChildren>
<Child> <CategoryLabel>
{shopName} {formatCafeName(shopName)}
</Child> </CategoryLabel>
<Child> <Child>
Mode pengembangan &nbsp; Mode pengembangan &nbsp;
<Switch <Switch
@@ -381,7 +415,7 @@ const Header = ({
</Child> </Child>
<Child hasChildren> <Child hasChildren>
<Child>Konfigurasi</Child> <CategoryLabel>Konfigurasi</CategoryLabel>
<Child onClick={() => setModal("welcome_config")}> <Child onClick={() => setModal("welcome_config")}>
Desain kafe Desain kafe
</Child> </Child>
@@ -393,7 +427,7 @@ const Header = ({
</Child> </Child>
</Child> </Child>
<Child hasChildren> <Child hasChildren>
<Child>Kasir</Child> <CategoryLabel>Kasir</CategoryLabel>
<Child onClick={() => setModal("create_clerk")}> <Child onClick={() => setModal("create_clerk")}>
+ Tambah + Tambah
</Child> </Child>
@@ -420,7 +454,7 @@ const Header = ({
user.cafeId == shopId && user.cafeId == shopId &&
user.roleId === 2 && ( user.roleId === 2 && (
<Child hasChildren> <Child hasChildren>
<Child>{shopName}</Child> <CategoryLabel>{formatCafeName(shopName)}</CategoryLabel>
<Child> <Child>
Mode pengembangan&nbsp; Mode pengembangan&nbsp;
@@ -435,7 +469,7 @@ const Header = ({
</Child> </Child>
<Child hasChildren> <Child hasChildren>
<Child>Konfigurasi</Child> <CategoryLabel>Konfigurasi</CategoryLabel>
<Child onClick={() => setModal("welcome_config")}> <Child onClick={() => setModal("welcome_config")}>
Desain kafe Desain kafe
</Child> </Child>
@@ -478,11 +512,12 @@ const Header = ({
{user.username !== undefined && ( {user.username !== undefined && (
<Child hasChildren ><Child onClick={isLogout}>Logout</Child></Child> <Child hasChildren ><Child onClick={isLogout}>Logout</Child></Child>
)} )}
</ChildContainer> </ChildContainer>
</Rectangle> </Rectangle>
)} )}
</div> </RightGroup>
</HeaderBar></HeaderBarbackground> </HeaderBar>
</HeaderBarbackground>
); );
}; };

View File

@@ -74,6 +74,11 @@ const Item = ({
} }
}; };
const formatCurrency = (value) => {
const num = Number(value) || 0;
return num.toLocaleString('id-ID');
};
const handlePriceChange = (event) => { const handlePriceChange = (event) => {
setItemPrice(event.target.value); setItemPrice(event.target.value);
}; };
@@ -93,23 +98,17 @@ const Item = ({
<div className={`${!last && !forInvoice ? styles.notLast : ""}`}> <div className={`${!last && !forInvoice ? styles.notLast : ""}`}>
<div className={`${styles.item} ${forInvoice ? styles.itemInvoice : ""} `}> <div className={`${styles.item} ${forInvoice ? styles.itemInvoice : ""} `}>
{!forInvoice && ( {!forInvoice && (
// <div className={styles.imageContainer}>
<img <img
src={ src={previewUrl}
previewUrl
}
onError={({ currentTarget }) => { onError={({ currentTarget }) => {
currentTarget.onerror = null; // prevents looping currentTarget.onerror = null;
currentTarget.src = currentTarget.src =
"https://png.pngtree.com/png-vector/20221125/ourmid/pngtree-no-image-available-icon-flatvector-illustration-pic-design-profile-vector-png-image_40966566.jpg"; "https://png.pngtree.com/png-vector/20221125/ourmid/pngtree-no-image-available-icon-flatvector-illustration-pic-design-profile-vector-png-image_40966566.jpg";
}} }}
alt={itemName} alt={itemName}
style={{ style={{ filter: !isAvailable ? "grayscale(100%)" : "none" }}
filter: !isAvailable ? "grayscale(100%)" : "none",
}}
className={styles.imageContainer} className={styles.imageContainer}
/> />
// </div>
)} )}
<div className={styles.itemDetails}> <div className={styles.itemDetails}>
{forInvoice && {forInvoice &&
@@ -141,168 +140,63 @@ const Item = ({
onChange={handleNameChange} onChange={handleNameChange}
disabled={!blank && !isBeingEdit} disabled={!blank && !isBeingEdit}
/> */} /> */}
<h3 style={{ <div style={{ marginRight: forInvoice ? 10 : 0 }}>
textTransform: 'capitalize', <h3 className={styles.title} style={{ width: forInvoice ? 160 : 'auto' }}>{itemName}</h3>
margin: `${forInvoice ? '13px 0px 10px 10px' : '5px 0px 10px 10px'}`, {!forInvoice && (
fontSize: '16px', <div className={styles.priceRow}>
display: '-webkit-box', {promoPrice && promoPrice != 0 && promoPrice != '' ? (
WebkitBoxOrient: 'vertical', <>
overflow: 'hidden', <div className={styles.promoBadge} style={{ background: !isAvailable ? 'gray' : undefined }}>
WebkitLineClamp: 2, Promo {(((initialPrice - promoPrice) / initialPrice) * 100).toFixed(0)}%
textOverflow: 'ellipsis', </div>
width: `${forInvoice? '160px' : 'unset'}` <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
}}> <span className={styles.priceNow}>Rp {formatCurrency(promoPrice)}</span>
{itemName} <span className={styles.priceOld}>Rp {formatCurrency(initialPrice)}</span>
</h3> </div>
</>
) : (
<span className={styles.priceNow}>Rp {formatCurrency(initialPrice)}</span>
)}
</div>
)}
</div>
{forInvoice && ( {forInvoice && (
<> <>
<p className={styles.multiplySymbol}>x</p> <p className={styles.multiplySymbol}>x</p>
<p className={styles.qtyInvoice}>{itemQty}</p> <p className={styles.qtyInvoice}>{itemQty}</p>
</> </>
)} )}
{!forInvoice && ( {!forInvoice && (
// <input !isBeingEdit && itemQty > 0 ? (
// className={`${styles.itemPrice} ${
// isBeingEdit || blank ? styles.blank : styles.notblank
// } ${!isAvailable ? styles.disabled : ""}`}
// value={itemPrice}
// placeholder="Harga"
// onChange={handlePriceChange}
// disabled={!blank && !isBeingEdit}
// />
<div style={{
display: 'flex',
flexWrap: 'wrap',
alignItems: 'center',
color: 'rgb(28, 29, 29)',
fontSize: '0.875rem',
fontWeight: 600,
lineHeight: '1rem',
marginLeft: 10
}}>
{promoPrice && promoPrice != 0 && promoPrice != '' ?
<>
<div style={{
position: 'relative',
marginTop: '0.125rem',
display: 'flex',
width: '87px',
alignItems: 'center',
whiteSpace: 'nowrap',
borderRadius: '9999px',
backgroundColor: !isAvailable ? 'gray' : 'unset',
backgroundImage: isAvailable && 'linear-gradient(to right, #e52535, #fe6d78)',
padding: '0.25rem 0rem',
color: 'rgb(255, 255, 255)',
fontSize: '0.75rem',
fontWeight: 600,
lineHeight: '1rem',
justifyContent: 'center'
}}>
Promo {(((initialPrice - promoPrice) / initialPrice) * 100).toFixed(0)}%
</div>
<div style={{ display: 'flex' }}>
<span style={{
marginLeft: '1rem',
marginRight: '0.5rem',
marginTop: '0.125rem'
}}>{promoPrice}</span>
<span style={{
marginTop: '0.125rem',
color: 'rgb(114, 114, 114)',
textDecoration: 'line-through'
}}>{initialPrice}</span>
</div>
</>
:
<>
<div style={{ display: 'flex' }}>
<span style={{
marginRight: '0.5rem',
marginTop: '0.125rem'
}}>{initialPrice}</span>
</div>
</>
}
</div>
)}
{!forInvoice &&
(!isBeingEdit && itemQty != 0 ? (
<div className={styles.itemQty}> <div className={styles.itemQty}>
<svg <div className={styles.qtyGroup}>
className={styles.plusNegative} <button className={styles.qtyBtn} onClick={handleNegativeClick} aria-label="Kurangi">-</button>
onClick={handleNegativeClick} {!blank && !isBeingEdit ? (
clipRule="evenodd" <span className={styles.qtyVal}>{itemQty}</span>
fillRule="evenodd" ) : (
strokeLinejoin="round" <input className={styles.itemQtyInput} value={itemQty} onChange={handleQtyChange} disabled={!blank && !isBeingEdit} />
strokeMiterlimit="2" )}
viewBox="0 0 24 24" <button className={styles.qtyBtn} onClick={handlePlusClick} aria-label="Tambah">+</button>
xmlns="http://www.w3.org/2000/svg" </div>
>
<path
d="m12.002 2.005c5.518 0 9.998 4.48 9.998 9.997 0 5.518-4.48 9.998-9.998 9.998-5.517 0-9.997-4.48-9.997-9.998 0-5.517 4.48-9.997 9.997-9.997zm0 1.5c-4.69 0-8.497 3.807-8.497 8.497s3.807 8.498 8.497 8.498 8.498-3.808 8.498-8.498-3.808-8.497-8.498-8.497zm4.253 7.75h-8.5c-.414 0-.75.336-.75.75s.336.75.75.75h8.5c.414 0 .75-.336.75-.75s-.336-.75-.75-.75z"
fillRule="nonzero"
/>
</svg>
{!blank && !isBeingEdit ? (
<p className={styles.itemQtyValue}>{itemQty}</p>
) : (
<input
className={styles.itemQtyInput}
value={itemQty}
onChange={handleQtyChange}
disabled={!blank && !isBeingEdit}
/>
)}
<svg
className={styles.plusNegative}
onClick={handlePlusClick}
clipRule="evenodd"
fillRule="evenodd"
strokeLinejoin="round"
strokeMiterlimit="2"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m12.002 2c5.518 0 9.998 4.48 9.998 9.998 0 5.517-4.48 9.997-9.998 9.997-5.517 0-9.997-4.48-9.997-9.997 0-5.518 4.48-9.998 9.997-9.998zm0 1.5c-4.69 0-8.497 3.808-8.497 8.498s3.807 8.497 8.497 8.497 8.498-3.807 8.498-8.497-3.808-8.498-8.498-8.498zm-.747 7.75h-3.5c-.414 0-.75.336-.75.75s.336.75.75.75h3.5v3.5c0 .414.336.75.75.75s.75-.336.75-.75v-3.5h3.5c.414 0 .75-.336.75-.75s-.336-.75-.75-.75h-3.5v-3.5c0-.414-.336-.75-.75-.75s-.75.336-.75.75z"
fillRule="nonzero"
/>
</svg>
</div> </div>
) : !blank && !isBeingEdit ? ( ) : !blank && !isBeingEdit ? (
<div className={styles.itemQty}> <div className={styles.itemQty}>
<button <button className={styles.addButton} onClick={handlePlusClick} disabled={!isAvailable}>
className={styles.addButton}
style={{ backgroundColor: !isAvailable ? "" : "inherit", border: `2px solid ${isAvailable ? 'inherit' : 'gray'}`, color: `${isAvailable ? '#a8c7a9' : 'gray'}` }}
onClick={handlePlusClick}
disabled={!isAvailable} // Optionally disable the button if not available
>
Pesan Pesan
</button> </button>
</div> </div>
) : ( ) : (
<div className={styles.itemQty}> <div className={styles.itemQty}>
<button <button className={styles.addButton} style={{ backgroundColor: '#ffffff', color: 'var(--brand-sage, #6B8F71)', borderColor: 'var(--brand-sage, #6B8F71)', width: 150 }} onClick={isBeingEdit ? handleUpdate : handleCreate}>
className={styles.addButton} {isBeingEdit ? 'Simpan' : 'Buat'}
style={{
backgroundColor: "white",
width: "150px",
color: '#a8c7a9'
}}
onClick={isBeingEdit ? handleUpdate : handleCreate}
>
{isBeingEdit ? "Simpan" : "Buat"}
</button> </button>
</div> </div>
))} )
)}
{forInvoice && ( {forInvoice && (
<p className={styles.itemPriceInvoice}>Rp {itemQty * (promoPrice > 0? promoPrice : itemPrice)}</p> <p className={styles.itemPriceInvoice}>Rp {formatCurrency(itemQty * (promoPrice > 0 ? promoPrice : itemPrice))}</p>
)} )}
</div> </div>
{forCart && ( {forCart && (
@@ -316,18 +210,11 @@ const Item = ({
</button> </button>
)} */} )} */}
</div> </div>
{itemDescription && itemDescription != 'undefined' && itemDescription?.length && {itemDescription && itemDescription != 'undefined' && itemDescription?.length ? (
<div> <div>
<p style={{ <p className={styles.desc} style={{ padding: '4px 6px', margin: 0 }}>{itemDescription}</p>
maxHeight: '34px',
display: '-webkit-box',
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
WebkitLineClamp: 2,
textOverflow: 'ellipsis', color: '#5f5f5f', fontSize: '14px', padding: '5px', margin: 0
}}>{itemDescription}</p>
</div> </div>
} ) : null}
</div> </div>
); );
}; };

View File

@@ -6,19 +6,20 @@
.item { .item {
display: flex; display: flex;
align-items: stretch; align-items: center;
justify-content: space-between; justify-content: space-between;
padding-left: 5px; width: 100%;
margin-top: 5px; gap: 10px;
margin-bottom: 5px; padding: 8px 10px;
color: rgba(88, 55, 50, 1); margin: 6px 0;
font-size: 32px; border: 1px solid #e3ece6;
box-sizing: border-box; /* Include padding and border in the element's total width */ border-radius: 12px;
width: 100%; /* Ensure the item does not exceed the parent's width */ background: var(--brand-sage-50, #F0F6F2);
overflow: hidden; /* Prevent internal overflow */ box-shadow: 0 1px 3px rgba(0,0,0,0.03);
padding-top: 10px; box-sizing: border-box;
margin-bottom: 5px; transition: box-shadow 0.2s ease, border-color 0.2s ease, background-color 0.2s ease;
} }
.item:hover { box-shadow: 0 4px 10px rgba(0,0,0,0.08); border-color: #d9e6de; }
.item:not(.itemInvoice) { .item:not(.itemInvoice) {
/* border-top: 2px solid #00000017; */ /* border-top: 2px solid #00000017; */
@@ -50,9 +51,9 @@
.imageContainer { .imageContainer {
position: relative; position: relative;
width: 26vw; width: clamp(68px, 18vw, 96px);
height: 26vw; height: clamp(68px, 18vw, 96px);
border-radius: 12px; border-radius: 10px;
object-fit: cover; object-fit: cover;
} }
@@ -84,11 +85,15 @@
.itemDetails { .itemDetails {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: center;
margin-left: 10px; align-items: stretch;
margin-right: 10px; gap: 6px;
flex-grow: 1; margin-left: 6px;
margin-right: 6px;
flex: 1;
min-width: 0;
} }
.infoRow { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
.itemInvoiceDetails { .itemInvoiceDetails {
display: flex; display: flex;
@@ -127,17 +132,7 @@
font-weight: 500; font-weight: 500;
} }
.itemPrice { .itemPrice { display: none; }
font-family: "Plus Jakarta Sans", sans-serif;
font-style: normal;
font-weight: 600;
width: calc(100% - 15px); /* Adjust the width to prevent overflow */
font-size: 3.3vw;
/* margin-bottom: 35px; */
margin-left: 5px;
color: #3a3a3a;
background-color: transparent;
}
.itemPriceInvoice { .itemPriceInvoice {
font-family: "Plus Jakarta Sans", sans-serif; font-family: "Plus Jakarta Sans", sans-serif;
@@ -153,11 +148,9 @@
.itemQty { .itemQty {
display: flex; display: flex;
align-items: center; align-items: center;
font-size: 0.9rem; justify-content: flex-end;
margin-left: 5px; gap: 8px;
color: #a8c7a9; min-height: 32px;
fill: #a8c7a9;
height: 40px;
} }
.itemQtyValue { .itemQtyValue {
@@ -183,19 +176,21 @@
} }
.addButton { .addButton {
background-color: #ffffff; background-color: var(--brand-sage, #6B8F71);
border: 2px solid #a8c7a9; border: 1px solid var(--brand-sage, #6B8F71);
/* border: none; */ color: #ffffff;
display: inline-block; display: inline-block;
font-size: 14px; font-size: 13px;
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
width: 87px; min-width: 84px;
height: 32px; height: 34px;
margin-left: 5px; padding: 0 14px;
margin-top: 5px; border-radius: 10px; /* square rounded corner */
border-radius: 20px; box-shadow: 0 1px 2px rgba(0,0,0,0.08);
} }
.addButton:hover { background-color: var(--brand-sage-hover, #7FAE7D); border-color: var(--brand-sage-hover, #7FAE7D); }
.addButton:disabled { background-color: var(--brand-sage-muted, #CFD8D3); border-color: var(--brand-sage-muted, #CFD8D3); cursor: default; }
.grayscale { .grayscale {
filter: grayscale(100%); filter: grayscale(100%);
} }
@@ -204,9 +199,8 @@
color: gray; color: gray;
} }
.plusNegative { .plusNegative {
width: 35px; width: 30px;
height: 35px; height: 30px;
margin: 2.5px 0 -0.5px 0px;
} }
.plusNegative2 { .plusNegative2 {
@@ -224,6 +218,91 @@
margin-right: 10px; margin-right: 10px;
} }
/* New elements for clean cafe item card */
.title {
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 600;
font-size: 16px;
color: #2d2d2d;
margin: 0 0 2px 0;
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
-webkit-line-clamp: 2;
text-transform: capitalize;
text-align: left;
flex: 1; min-width: 0;
}
/* Responsive type scale for title and price */
@media (min-width: 600px) {
.title { font-size: 17px; }
.priceNow { font-size: 15px; }
}
@media (min-width: 992px) {
.title { font-size: 18px; }
.priceNow { font-size: 16px; }
}
.desc {
color: #5f5f5f;
font-size: 12px;
line-height: 1.25;
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
-webkit-line-clamp: 2;
}
.priceRow { display: inline-flex; align-items: center; gap: 8px; }
.promoBadge {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 2px 8px;
height: 20px;
border-radius: 999px;
background: linear-gradient(to right, #e52535, #fe6d78);
color: #fff;
font-size: 11px;
font-weight: 700;
}
.priceNow {
color: #1c1d1d;
font-weight: 700;
font-size: 14px;
white-space: nowrap;
}
.priceOld {
color: #727272;
font-size: 12px;
text-decoration: line-through;
white-space: nowrap;
}
.qtyGroup {
display: inline-flex;
align-items: center;
border: 1px solid #e6e6e6;
border-radius: 10px; /* square rounded corners */
height: 32px;
overflow: hidden;
}
.qtyBtn {
width: 34px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
background: #fff;
color: var(--brand-sage, #6B8F71);
border: none;
}
.qtyBtn:hover { background: var(--brand-sage-50, #F0F6F2); }
.qtyVal {
min-width: 28px;
text-align: center;
font-weight: 700;
color: #2d2d2d;
}
.itemInvoice .itemDetails { .itemInvoice .itemDetails {
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;

View File

@@ -17,6 +17,7 @@ import {
import ItemType from "./ItemType.js"; import ItemType from "./ItemType.js";
import { createItemType, updateItemDeletionStatus } from "../helpers/itemHelper.js"; import { createItemType, updateItemDeletionStatus } from "../helpers/itemHelper.js";
import ItemConfig from "./ItemConfig.js" import ItemConfig from "./ItemConfig.js"
import { ArrowUp, ArrowDown, Pencil } from 'lucide-react';
const ItemLister = ({ const ItemLister = ({
index, index,
@@ -618,87 +619,31 @@ const ItemLister = ({
disabled={!isEdit} disabled={!isEdit}
/> />
{isEditMode && !isEdit && ( {isEditMode && !isEdit && (
<> <div className={styles.titleActions}>
<div <button
style={{ className={styles.iconBtn}
width: '32px', onClick={() => index === 0 ? null : moveItemTypeUp(itemTypeId)}
height: '32px', // Add a height to the div disabled={index === 0}
display: 'flex', // Use flexbox aria-label="Naikkan kategori"
justifyContent: 'center', // Center horizontally
alignItems: 'center', // Center vertically
cursor: 'pointer'
}}
onClick={index == 0 ? null : () => moveItemTypeUp(itemTypeId)} // Move onClick here for the whole div
> >
<svg <ArrowUp size={18} />
viewBox="0 0 16 16" </button>
xmlns="http://www.w3.org/2000/svg" <button
fill="#000000" className={styles.iconBtn}
style={{ width: '100%', height: '100%' }} // Ensure SVG fits the div onClick={() => index === indexTotal - 1 ? null : moveItemTypeDown(itemTypeId)}
> disabled={index === indexTotal - 1}
<g id="SVGRepo_bgCarrier" strokeWidth="0"></g> aria-label="Turunkan kategori"
<g id="SVGRepo_tracerCarrier" strokeLinecap="round" strokeLinejoin="round"></g>
<g id="SVGRepo_iconCarrier">
<path d="m 1 11 c 0 -0.265625 0.105469 -0.519531 0.292969 -0.707031 l 6 -6 c 0.390625 -0.390625 1.023437 -0.390625 1.414062 0 l 6 6 c 0.1875 0.1875 0.292969 0.441406 0.292969 0.707031 s -0.105469 0.519531 -0.292969 0.707031 c -0.390625 0.390625 -1.023437 0.390625 -1.414062 0 l -5.292969 -5.292969 l -5.292969 5.292969 c -0.390625 0.390625 -1.023437 0.390625 -1.414062 0 c -0.1875 -0.1875 -0.292969 -0.441406 -0.292969 -0.707031 z m 0 0" fill={index === 0 ? "gray" : "#2e3436"}></path>
</g>
</svg>
</div>
<div
style={{
width: '32px',
height: '32px', // Add a height to the div
display: 'flex', // Use flexbox
justifyContent: 'center', // Center horizontally
alignItems: 'center', // Center vertically
cursor: 'pointer'
}}
onClick={index == indexTotal - 1 ? null : () => moveItemTypeDown(itemTypeId)} // Move onClick here for the whole div
> >
<svg <ArrowDown size={18} />
viewBox="0 0 16 16" </button>
xmlns="http://www.w3.org/2000/svg" <button
fill="#000000" className={styles.iconBtn}
style={{ width: '100%', height: '100%' }} // Ensure SVG fits the div onClick={toggleEditTypeItem}
> aria-label="Edit kategori"
<g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
<g id="SVGRepo_tracerCarrier" strokeLinecap="round" strokeLinejoin="round"></g>
<g id="SVGRepo_iconCarrier">
<path d="m 1 5 c 0 -0.265625 0.105469 -0.519531 0.292969 -0.707031 c 0.390625 -0.390625 1.023437 -0.390625 1.414062 0 l 5.292969 5.292969 l 5.292969 -5.292969 c 0.390625 -0.390625 1.023437 -0.390625 1.414062 0 c 0.1875 0.1875 0.292969 0.441406 0.292969 0.707031 s -0.105469 0.519531 -0.292969 0.707031 l -6 6 c -0.390625 0.390625 -1.023437 0.390625 -1.414062 0 l -6 -6 c -0.1875 -0.1875 -0.292969 -0.441406 -0.292969 -0.707031 z m 0 0" fill={index === indexTotal - 1 ? "gray" : "#2e3436"}></path>
</g>
</svg>
</div>
<div
style={{
width: '32px',
height: '32px', // Add a height to the div
display: 'flex', // Use flexbox
justifyContent: 'center', // Center horizontally
alignItems: 'center', // Center vertically
cursor: 'pointer'
}}
onClick={toggleEditTypeItem} // Move onClick here for the whole div
> >
<svg <Pencil size={18} />
fill="#000000" </button>
viewBox="0 0 32 32" </div>
style={{ fillRule: 'evenodd', clipRule: 'evenodd', strokeLinejoin: 'round', strokeMiterlimit: 2 }}
version="1.1"
xmlSpace="preserve"
xmlns="http://www.w3.org/2000/svg"
xmlnsSerif="http://www.serif.com/"
xmlnsXlink="http://www.w3.org/1999/xlink"
>
<g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
<g id="SVGRepo_tracerCarrier" strokeLinecap="round" strokeLinejoin="round"></g>
<g id="SVGRepo_iconCarrier">
<path d="M12.965,5.462c0,-0 -2.584,0.004 -4.979,0.008c-3.034,0.006 -5.49,2.467 -5.49,5.5l0,13.03c0,1.459 0.579,2.858 1.611,3.889c1.031,1.032 2.43,1.611 3.889,1.611l13.003,0c3.038,-0 5.5,-2.462 5.5,-5.5c0,-2.405 0,-5.004 0,-5.004c0,-0.828 -0.672,-1.5 -1.5,-1.5c-0.827,-0 -1.5,0.672 -1.5,1.5l0,5.004c0,1.381 -1.119,2.5 -2.5,2.5l-13.003,0c-0.663,-0 -1.299,-0.263 -1.768,-0.732c-0.469,-0.469 -0.732,-1.105 -0.732,-1.768l0,-13.03c0,-1.379 1.117,-2.497 2.496,-2.5c2.394,-0.004 4.979,-0.008 4.979,-0.008c0.828,-0.002 1.498,-0.675 1.497,-1.503c-0.001,-0.828 -0.675,-1.499 -1.503,-1.497Z"></path>
<path d="M20.046,6.411l-6.845,6.846c-0.137,0.137 -0.232,0.311 -0.271,0.501l-1.081,5.152c-0.069,0.329 0.032,0.671 0.268,0.909c0.237,0.239 0.577,0.343 0.907,0.277l5.194,-1.038c0.193,-0.039 0.371,-0.134 0.511,-0.274l6.845,-6.845l-5.528,-5.528Zm1.415,-1.414l5.527,5.528l1.112,-1.111c1.526,-1.527 1.526,-4.001 -0,-5.527c-0.001,-0 -0.001,-0.001 -0.001,-0.001c-1.527,-1.526 -4.001,-1.526 -5.527,-0l-1.111,1.111Z"></path>
</g>
</svg>
</div>
</>
)} )}
</div> </div>
} }

View File

@@ -85,6 +85,33 @@
justify-content: space-between; justify-content: space-between;
} }
.titleActions {
display: inline-flex;
align-items: center;
gap: 6px;
}
.iconBtn {
width: 32px;
height: 32px;
display: inline-flex;
align-items: center;
justify-content: center;
border: 1px solid #e6e6e6;
background: #ffffff;
color: #2d2d2d;
border-radius: 8px; /* square rounded */
cursor: pointer;
}
.iconBtn:disabled {
opacity: 0.5;
cursor: default;
}
.iconBtn:hover:not(:disabled) {
background: var(--brand-sage-50, #F0F6F2);
border-color: var(--brand-sage, #6B8F71);
}
.title { .title {
background-color: transparent; background-color: transparent;
font-family: "Plus Jakarta Sans", sans-serif; font-family: "Plus Jakarta Sans", sans-serif;

View File

@@ -1,5 +1,6 @@
import React, { useRef, useEffect, useState } from "react"; import React, { useRef, useEffect, useState } from "react";
import styles from "./ItemType.module.css"; import styles from "./ItemType.module.css";
import { Coffee, CupSoda, CakeSlice, Utensils, Grid2X2, Plus } from 'lucide-react';
export default function ItemType({ export default function ItemType({
onClick, onClick,
@@ -57,6 +58,15 @@ export default function ItemType({
onCreate(namee, selectedImage); onCreate(namee, selectedImage);
}; };
const formatName = (val) => {
if (!val || typeof val !== 'string') return val;
return val
.toLowerCase()
.replace(/\b\w/g, (c) => c.toUpperCase());
};
const iconImageUrl = imageUrl === 'uploads/assets/All.png' ? 'icon:all' : imageUrl;
return ( return (
<div <div
className={ className={
@@ -72,27 +82,25 @@ export default function ItemType({
> >
<div <div
onClick={ onClick={
rectangular ? (blank ? null : () => onClick(imageUrl)) : onClick rectangular ? (blank ? null : () => onClick(iconImageUrl)) : onClick
} }
className={styles["item-type-rect"]} className={styles["item-type-rect"]}
style={{ style={{
top: selected ? "-10px" : "initial", // Remove lift-up effect; only color changes when selected
backgroundColor: selected ? 'var(--brand-sage, #6B8F71)' : '#ffffff',
border: selected ? '1px solid var(--brand-sage, #6B8F71)' : '1px solid #e6e6e6',
color: selected ? '#ffffff' : '#4a6b5a'
}} }}
> >
{imageUrl != 'uploads/assets/All.png' ? {iconImageUrl === 'uploads/assets/All.png' ? (
<img <svg version="1.0" xmlns="http://www.w3.org/2000/svg"
src={previewUrl}
alt={namee}
className={styles["item-type-image"]}
/>
:<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="100%" height="100%" viewBox="0 0 800.000000 800.000000" width="100%" height="100%" viewBox="0 0 800.000000 800.000000"
preserveAspectRatio="xMidYMid meet"> preserveAspectRatio="xMidYMid meet">
<metadata> <metadata>
Created by potrace 1.16, written by Peter Selinger 2001-2019 Created by potrace 1.16, written by Peter Selinger 2001-2019
</metadata> </metadata>
<g transform="translate(0.000000,800.000000) scale(0.100000,-0.100000)" <g transform="translate(0.000000,800.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none"> fill="currentColor" stroke="none">
<path d="M3708 7165 c-3 -4 -44 -10 -90 -15 -266 -28 -530 -91 -753 -180 -11 <path d="M3708 7165 c-3 -4 -44 -10 -90 -15 -266 -28 -530 -91 -753 -180 -11
-4 -42 -16 -70 -26 -27 -9 -129 -57 -225 -106 -186 -94 -188 -95 -262 -145 -4 -42 -16 -70 -26 -27 -9 -129 -57 -225 -106 -186 -94 -188 -95 -262 -145
-26 -18 -52 -33 -58 -33 -5 0 -24 -13 -42 -28 -18 -16 -53 -43 -78 -61 -124 -26 -18 -52 -33 -58 -33 -5 0 -24 -13 -42 -28 -18 -16 -53 -43 -78 -61 -124
@@ -138,7 +146,17 @@ c261 0 329 -3 352 -14z m1237 -2 c52 -35 54 -49 54 -379 0 -348 -2 -360 -69
58 40 59 387 60 178 0 328 -4 342 -10z"/> 58 40 59 387 60 178 0 328 -4 342 -10z"/>
</g> </g>
</svg> </svg>
} ) : (iconImageUrl && typeof iconImageUrl === 'string' && iconImageUrl.startsWith('icon:')) ? (
<div style={{width:'100%',height:'100%',display:'flex',alignItems:'center',justifyContent:'center'}}>
<LucideCategoryIcon name={namee} iconKey={(iconImageUrl || '').split(':')[1]} />
</div>
) : (
<img
src={previewUrl}
alt={namee}
className={styles["item-type-image"]}
/>
)}
{blank && rectangular && ( {blank && rectangular && (
<div className={styles["item-type-image-container"]}> <div className={styles["item-type-image-container"]}>
<input <input
@@ -155,15 +173,49 @@ c261 0 329 -3 352 -14z m1237 -2 c52 -35 54 -49 54 -379 0 -348 -2 -360 -69
<input <input
ref={inputRef} ref={inputRef}
className={`${styles["item-type-name"]} ${styles.noborder}`} className={`${styles["item-type-name"]} ${styles.noborder}`}
value={namee} value={formatName(namee)}
onChange={handleNameChange} onChange={handleNameChange}
disabled={true} disabled={true}
style={{ style={{
top: selected ? "-5px" : "initial", top: 'initial',
borderBottom: selected ? "1px solid #000" : "none", borderBottom: 'none',
color: selected ? '#2d2d2d' : '#333',
textTransform: 'capitalize'
}} }}
/> />
)} )}
</div> </div>
); );
} }
function LucideCategoryIcon({ name, iconKey }) {
const key = pickIconKey(name, iconKey);
const size = '56%';
switch (key) {
case 'coffee':
return <Coffee color={'currentColor'} size={size} strokeWidth={2} />;
case 'drink':
return <CupSoda color={'currentColor'} size={size} strokeWidth={2} />;
case 'dessert':
return <CakeSlice color={'currentColor'} size={size} strokeWidth={2} />;
case 'food':
return <Utensils color={'currentColor'} size={size} strokeWidth={2} />;
case 'all':
return <Grid2X2 color={'currentColor'} size={size} strokeWidth={2} />;
case 'plus':
return <Plus color={'currentColor'} size={size} strokeWidth={2} />;
default:
return <Utensils color={'currentColor'} size={size} strokeWidth={2} />;
}
}
function pickIconKey(name, iconKey) {
const n = (name || '').toLowerCase();
if (iconKey === 'plus') return 'plus';
if (iconKey === 'all') return 'all';
if (/(kopi|coffee|espresso|latte|americano|kapal|brew)/.test(n)) return 'coffee';
if (/(teh|tea|drink|minum|soda|juice|jus|milk|susu|lemon)/.test(n)) return 'drink';
if (/(dessert|cake|kue|manis|ice|es krim|ice-cream)/.test(n)) return 'dessert';
if (/(food|makan|snack|cemilan|nasi|mie|noodle|soup|sup|ayam|daging|ikan|roti|sandwich|burger|pizza)/.test(n)) return 'food';
return 'food';
}

View File

@@ -1,7 +1,7 @@
.item-type { .item-type {
width: calc(25vw - 20px); width: auto;
height: calc(30vw - 20px); height: auto;
margin: 1px 10px 0px; margin: 0 4px; /* tighter spacing between tiles */
overflow: visible; overflow: visible;
text-align: center; text-align: center;
align-items: center; align-items: center;
@@ -34,24 +34,33 @@
} }
.item-type-rect { .item-type-rect {
position: relative; position: relative;
height: 13vw; height: clamp(48px, 9vw, 80px);
width: 13vw; width: clamp(48px, 9vw, 80px);
object-fit: cover; object-fit: cover;
border-radius: 15px; border-radius: 12px;
background-color: #fff; background-color: #fff;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
display: flex;
align-items: center;
justify-content: center;
}
.item-type-rect:hover {
background-color: var(--brand-sage-100, #E9F3ED);
border-color: var(--brand-sage, #6B8F71);
} }
.item-type-name { .item-type-name {
font-family: "Plus Jakarta Sans", sans-serif; font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 500; font-weight: 500;
font-style: normal; font-style: normal;
font-size: 14px; font-size: 12px;
color: #333; color: #333;
width: calc(25vw - 30px); width: auto;
text-align: center; text-align: center;
background-color: transparent; background-color: transparent;
position: relative; /* Needed for positioning the button */ position: relative;
margin-top: 6px; /* keep label spacing constant; avoid jumping */
} }
.item-type-image { .item-type-image {

View File

@@ -1,11 +1,23 @@
/* New clean, intuitive category bar */
.item-type-lister { .item-type-lister {
width: 100vw; width: 100%;
overflow-x: auto; overflow-x: auto;
white-space: nowrap; white-space: nowrap;
padding: 3px 0px; padding: 2px 0px; /* tighter top/bottom padding */
margin-bottom: -5px;
} }
.category-bar {
display: flex;
align-items: center;
gap: 8px;
overflow-x: auto;
padding: 8px 12px 4px;
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
.category-bar::-webkit-scrollbar { display: none; }
/* Legacy horizontal tile list container (used for tile UI) */
.item-type-list { .item-type-list {
display: inline-flex; display: inline-flex;
-ms-overflow-style: none; /* IE and Edge */ -ms-overflow-style: none; /* IE and Edge */
@@ -13,11 +25,44 @@
overflow-y: hidden; overflow-y: hidden;
} }
.item-type { .category-chip {
display: inline-block; flex: 0 0 auto;
margin-right: 20px; display: inline-flex;
/* Space between items */ align-items: center;
gap: 8px;
height: 36px;
padding: 0 14px;
border-radius: 999px;
border: 1px solid #e6e6e6;
background: #ffffff;
color: #2d2d2d;
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 500;
font-size: 14px;
cursor: pointer;
user-select: none;
} }
.category-chip:hover { border-color: #d0d0d0; }
.category-chip.selected {
background: #73a585;
color: #ffffff;
border-color: #73a585;
}
.category-chip .chip-icon {
width: 18px;
height: 18px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.add-item-chip {
background: #f4f7f5;
border-color: #dfe7e2;
color: #4a6b5a;
}
.add-item-chip:hover { background: #eaf1ed; }
.rect-creator { .rect-creator {
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
@@ -55,13 +100,4 @@
bottom: 0; bottom: 0;
align-self: center; /* Center the button horizontally */ align-self: center; /* Center the button horizontally */
} }
.item-type-name { /* Legacy styles kept for ItemType grid if needed elsewhere */
font-family: "Plus Jakarta Sans", sans-serif;
font-style: normal;
height: 20vw;
font-size: 1.5rem;
font-weight: 500;
color: black;
text-transform: capitalize;
z-index: 301;
}

View File

@@ -2,7 +2,7 @@ import React, { useState, useRef, useEffect } from "react";
import smoothScroll from "smooth-scroll-into-view-if-needed"; import smoothScroll from "smooth-scroll-into-view-if-needed";
import "./ItemTypeLister.css"; import "./ItemTypeLister.css";
import ItemType from "./ItemType"; import ItemType from "./ItemType";
import { createItem, createItemType } from "../helpers/itemHelper.js"; import { createItem } from "../helpers/itemHelper.js";
import { getImageUrl } from "../helpers/itemHelper"; import { getImageUrl } from "../helpers/itemHelper";
import ItemLister from "./ItemLister"; import ItemLister from "./ItemLister";
@@ -22,24 +22,6 @@ const ItemTypeLister = ({
const newItemDivRef = useRef(null); const newItemDivRef = useRef(null);
const [items, setItems] = useState([]); const [items, setItems] = useState([]);
const [itemTypeName, setItemTypeName] = useState("");
const handleCreateItem = (name, price, selectedImage, previewUrl, description, promoPrice) => {
console.log(previewUrl);
const newItem = {
itemId: items.length + 1,
name,
price,
selectedImage,
image: previewUrl,
availability: true,
description,
promoPrice
};
// Update the items state with the new item
setItems((prevItems) => [...prevItems, newItem]);
};
// Effect to handle changes to isAddingNewItem // Effect to handle changes to isAddingNewItem
useEffect(() => { useEffect(() => {
if (isAddingNewItem && newItemDivRef.current) { if (isAddingNewItem && newItemDivRef.current) {
@@ -67,90 +49,63 @@ const ItemTypeLister = ({
document.body.style.overflow = !isAddingNewItem ? "hidden" : "auto"; document.body.style.overflow = !isAddingNewItem ? "hidden" : "auto";
}; };
async function handleCreate(name, selectedImage) { // Removed legacy image upload logic used by the old tile view
createItemType(shopId, name, selectedImage);
}
const [selectedImage, setSelectedImage] = useState(null); const canManage = user && (user.user_id == shopOwnerId || user.cafeId == shopId);
const [previewUrl, setPreviewUrl] = useState("");
const [imageUrl, setImaguUrl] = useState("");
useEffect(() => { const formatName = (name) => {
// if (selectedImage) { if (!name) return name;
// const reader = new FileReader(); return name
// reader.onloadend = () => { .toLowerCase()
// setPreviewUrl(reader.result); .replace(/\b\w/g, (c) => c.toUpperCase());
// };
// reader.readAsDataURL(selectedImage);
// } else {
// setPreviewUrl(getImageUrl(imageUrl));
setPreviewUrl(selectedImage);
// }
}, [selectedImage, imageUrl]);
const handleImageChange = (e) => {
setSelectedImage(e);
}; };
return ( return (
<div <div className="item-type-lister" style={{ overflowX: isAddingNewItem ? 'hidden' : 'auto' }}>
className="item-type-lister" <div ref={newItemDivRef} className="item-type-list" style={{ display: 'inline-flex' }}>
style={{ overflowX: isAddingNewItem ? "hidden" : "" }} {isEditMode && !isAddingNewItem && canManage && (
> <ItemType
<div onClick={toggleAddNewItem}
ref={newItemDivRef} name={"buat baru"}
className="item-type-list" imageUrl={"icon:plus"}
style={{ display: isAddingNewItem ? "inline-flex" : "inline-flex" }} />
> )}
{isEditMode &&
!isAddingNewItem && {canManage && isAddingNewItem && (
user && ( <ItemLister
user.user_id == shopOwnerId || user.cafeId == shopId) && ( shopId={shopId}
<ItemType shopOwnerId={shopOwnerId}
onClick={toggleAddNewItem} user={user}
name={"buat baru"} typeName={""}
imageUrl={getImageUrl("uploads/assets/addnew.png")} setShopItems={setShopItems}
/> itemList={items}
)} isEditMode={true}
{user &&( handleCreateItem={(itemTypeId, name, price, selectedImage, description, promoPrice) => createItem(shopId, name, price, selectedImage, itemTypeId, description, promoPrice)}
user.user_id == shopOwnerId || user.cafeId == shopId) && beingEditedType={beingEditedType}
isAddingNewItem && ( setBeingEditedType={setBeingEditedType}
<> alwaysEdit={true}
<ItemLister handleUnEdit={toggleAddNewItem}
shopId={shopId} />
shopOwnerId={shopOwnerId} )}
user={user}
typeName={""}
setShopItems={setShopItems}
itemList={items}
isEditMode={true}
handleCreateItem={(itemTypeId, name, price, selectedImage, description, promoPrice) => createItem(shopId, name, price, selectedImage, itemTypeId, description, promoPrice)}
beingEditedType={beingEditedType}
setBeingEditedType={setBeingEditedType}
alwaysEdit={true}
handleUnEdit={toggleAddNewItem}
/>
</>
)}
{itemTypes && itemTypes.length > 0 && ( {itemTypes && itemTypes.length > 0 && (
<ItemType <ItemType
name={"semua"} name={"semua"}
onClick={() => onFilterChange(0)} onClick={() => onFilterChange(0)}
imageUrl={"uploads/assets/All.png"} imageUrl={"icon:all"}
selected={filterId === 0}
/> />
)} )}
{itemTypes &&
itemTypes.map( {itemTypes && itemTypes.map((itemType) => (
(itemType) => <ItemType
( key={itemType.itemTypeId}
itemType.itemList.length > 0 || (user && (user.user_id == shopOwnerId || user.cafeId == shopId))) && ( name={itemType.name}
<ItemType imageUrl={"icon:category"}
key={itemType.itemTypeId} onClick={() => onFilterChange(itemType.itemTypeId)}
name={itemType.name} selected={filterId === itemType.itemTypeId}
imageUrl={getImageUrl(itemType.image)} />
onClick={() => onFilterChange(itemType.itemTypeId)} ))}
selected={filterId === itemType.itemTypeId}
/>
)
)}
</div> </div>
</div> </div>
); );

View File

@@ -55,6 +55,7 @@ function CafePage({
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const token = searchParams.get("token"); const token = searchParams.get("token");
const { shopIdentifier, tableCode } = useParams(); const { shopIdentifier, tableCode } = useParams();
// Send params to parent immediately (original behavior)
sendParam({ shopIdentifier, tableCode }); sendParam({ shopIdentifier, tableCode });
const { const {
@@ -319,7 +320,7 @@ function CafePage({
/> />
))} ))}
{!isEditMode && (user.username || cartItemsLength > 0) && {!isEditMode && (user.username || cartItemsLength > 0) &&
<div style={{ marginTop: '10px', height: '40px', position: 'sticky', bottom: '40px', display: 'flex', justifyContent: 'center', alignItems: 'center', textAlign: 'center' }}> <div className="StickyCartBar" style={{ marginTop: '10px', height: '40px', position: 'sticky', bottom: '40px', display: 'flex', justifyContent: 'center', alignItems: 'center', textAlign: 'center' }}>
{(lastTransaction != null || cartItemsLength > 0) && {(lastTransaction != null || cartItemsLength > 0) &&
<div onClick={goToCart} style={{ backgroundColor: '#73a585', width: user.username ? '55vw' : '70vw', height: '40px', borderRadius: '30px', display: 'flex', justifyContent: 'space-between', padding: '0 20px' }}> <div onClick={goToCart} style={{ backgroundColor: '#73a585', width: user.username ? '55vw' : '70vw', height: '40px', borderRadius: '30px', display: 'flex', justifyContent: 'space-between', padding: '0 20px' }}>
<div style={{ display: 'flex', flexWrap: 'wrap', alignContent: 'center' }}>{lastTransaction != null && '+'}{cartItemsLength} item</div> <div style={{ display: 'flex', flexWrap: 'wrap', alignContent: 'center' }}>{lastTransaction != null && '+'}{cartItemsLength} item</div>
@@ -338,7 +339,7 @@ function CafePage({
</div> </div>
} }
{user.username && {user.username &&
<div onClick={goToTransactions} style={{ backgroundColor: '#73a585', width: '15vw', height: '40px', borderRadius: '30px', display: 'flex', justifyContent: 'center', marginLeft: lastTransaction != null || cartItemsLength > 0 ? '6px' : '0px' }}> <div onClick={goToTransactions} style={{ backgroundColor: '#73a585', width: '15vw', height: '40px', borderRadius: '30px', display: 'flex', justifyContent: 'center', marginLeft: (lastTransaction != null || cartItemsLength > 0) ? '6px' : '0px' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: '38px', marginRight: '5px' }}> <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: '38px', marginRight: '5px' }}>
<div style={{ display: 'flex', alignItems: 'center', marginLeft: '5px', width: '20px' }}> <div style={{ display: 'flex', alignItems: 'center', marginLeft: '5px', width: '20px' }}>
<svg viewBox="0 0 512 512"> <svg viewBox="0 0 512 512">