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/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"apexcharts": "^5.3.4",
"caniuse-lite": "^1.0.30001690",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.13",
"html-to-image": "^1.11.11",
"html2canvas": "^1.4.1",
"jsqr": "^1.4.0",
"lucide-react": "^0.541.0",
"qr-scanner": "^1.4.2",
"qrcode.react": "^3.1.0",
"react": "^18.3.1",
@@ -3905,171 +3907,6 @@
"integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==",
"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": {
"version": "7.4.5",
"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",
"integrity": "sha512-7iJFm9lL3C40HQcqzEfezK2l+dW2CpoVY3b77KQGqc8GXWa6LhhmX5Ckv7alQfUXBuZbjpICZ+Dvq1czlGx7gA==",
"license": "MIT",
"peer": true,
"peerDependencies": {
"@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",
"integrity": "sha512-/69XMRCDoam2HgC4ldHIaDgeQf1ViHIsa0Ld4uWgiXtZ+E24DWHe/9Ib6kbNiZ7WRIdlVokUDR1Fg0kjIpkfbw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@svgdotjs/svg.js": "^3.2.4"
},
@@ -4661,7 +4496,6 @@
"resolved": "https://registry.npmjs.org/@svgdotjs/svg.js/-/svg.js-3.2.4.tgz",
"integrity": "sha512-BjJ/7vWNowlX3Z8O4ywT58DqbNRyYlkk6Yz/D13aB7hGmfQTvGX4Tkgtm/ApYlu9M7lCQi15xUEidqMUmdMYwg==",
"license": "MIT",
"peer": true,
"funding": {
"type": "github",
"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",
"integrity": "sha512-4heRW4B1QrJeENfi7326lUPYBCevj78FJs8kfeDxn5st0IYPIRXoTtOSYvTzFWgaWWXd3YCDE6ao4fmv91RthA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 14.18"
},
@@ -4686,7 +4519,6 @@
"resolved": "https://registry.npmjs.org/@svgdotjs/svg.select.js/-/svg.select.js-4.0.3.tgz",
"integrity": "sha512-qkMgso1sd2hXKd1FZ1weO7ANq12sNmQJeGDjs46QwDVsxSRcHmvWKL2NDF7Yimpwf3sl5esOLkPqtV2bQ3v/Jg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 14.18"
},
@@ -4924,26 +4756,6 @@
"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": {
"version": "5.17.0",
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.17.0.tgz",
@@ -5976,8 +5788,7 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz",
"integrity": "sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/@zxing/browser": {
"version": "0.0.7",
@@ -6315,7 +6126,6 @@
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-5.3.4.tgz",
"integrity": "sha512-N0gNh8uLu/BN8N+BCphNK+gZAoSoUtDDn1jFGB+3+EMcv8s6vajuP3W0g4dMLTRp6chFkjMmQK3uD8pz4ISmLA==",
"license": "SEE LICENSE IN LICENSE",
"peer": true,
"dependencies": {
"@svgdotjs/svg.draggable.js": "^3.0.4",
"@svgdotjs/svg.filter.js": "^3.0.8",
@@ -15199,6 +15009,15 @@
"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": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
@@ -20825,20 +20644,6 @@
"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": {
"version": "1.1.0",
"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/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"apexcharts": "^5.3.4",
"caniuse-lite": "^1.0.30001690",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.13",
"html-to-image": "^1.11.11",
"html2canvas": "^1.4.1",
"jsqr": "^1.4.0",
"lucide-react": "^0.541.0",
"qr-scanner": "^1.4.2",
"qrcode.react": "^3.1.0",
"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=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");
: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,
body {
scrollbar-width: none; /* Firefox */
@@ -54,6 +62,8 @@ body {
color: white;
}
/* removed two-column layout; reverted to single column */
.App-link {
color: #61dafb;
}

View File

@@ -4,6 +4,7 @@ import { useLocation } from "react-router-dom";
import { useNavigationHelpers } from "../helpers/navigationHelpers";
import Switch from "react-switch";
// Restore original gradient background for header container when shopName exists
const HeaderBarbackground = styled.div`
${({ shopName }) =>
shopName &&
@@ -19,7 +20,7 @@ const HeaderBar = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 15px;
padding: 12px 14px;
color: black;
background-color: #ffffff;
z-index: 200;
@@ -33,30 +34,15 @@ const HeaderBar = styled.div`
const Title = styled.h2`
margin: 0;
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 500;
font-weight: 600;
font-style: normal;
font-size:${(props) => (props.HeaderSize)};
color: rgba(88, 55, 50, 1);
text-transform: uppercase;
color: rgba(45, 45, 45, 1);
`;
const ProfileName = styled.h2`
position: absolute;
font-family: "Plus Jakarta Sans", sans-serif;
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;
`;
// SubTitle removed per redesign (no subtitle below cafe name)
// Deprecated the animated ProfileName in favor of a cleaner layout
const nn = keyframes`
0% {
@@ -103,22 +89,17 @@ const ss = keyframes`
}
`;
const ProfileImage = styled.img`
position: relative;
width: 60px;
height: 60px;
border-radius: 50%;
object-fit: contain;
cursor: pointer;
z-index: 199;
animation: ${(props) => {
if (props.animate === "grow") return g;
if (props.animate === "shrink") return s;
return "none";
}}
0.5s forwards;
const CafeAvatar = styled.img`
width: clamp(32px, 5vw, 56px);
height: clamp(32px, 5vw, 56px);
border-radius: 8px;
object-fit: cover;
background: #f2f2f2;
margin-left: 8px; /* extra left padding so its not too tight */
`;
// User initial avatar removed; only cafe image is shown on the left
const g = keyframes`
0% {
top: 0px;
@@ -149,62 +130,43 @@ const s = keyframes`
}
`;
/* Replace bubble-like animation with subtle fade/slide */
const grow = keyframes`
0% {
right: 12px;
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;
}
0% { opacity: 0; transform: translateY(-6px) scale(0.98); }
100% { opacity: 1; transform: translateY(0) scale(1); }
`;
const shrink = keyframes`
0% {
right: 12px;
width: 300px;
height: auto;
border-radius: 20px;
}
100% {
right: 28px;
width: 60px;
height: 60px;
border-radius: 50%;
}
0% { opacity: 1; transform: translateY(0) scale(1); }
100% { opacity: 0; transform: translateY(-6px) scale(0.98); }
`;
const Rectangle = styled.div`
overflow-y: hidden;
overflow-y: auto;
position: absolute;
top: 39px;
right: 12px;
width: 200px;
max-height: 87vh; /* or another appropriate value */
top: calc(100% + 8px);
right: 0;
width: 240px;
max-height: 75vh;
background-color: white;
z-index: 198;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
animation: ${(props) => (props.animate === "grow" ? grow : shrink)} 0.5s
forwards;
padding: 10px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
border: 1px solid #f0f0f0;
border-radius: 12px;
animation: ${(props) => (props.animate === "grow" ? grow : shrink)} 0.2s forwards;
padding: 10px 12px;
box-sizing: border-box;
overflow-x: hidden;
font-size: 14px;
color: #393939;
backdrop-filter: blur(2px);
`;
const ChildContainer = styled.div`
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-end;
flex-wrap: wrap;
padding-top: 70px;
align-items: stretch;
flex-wrap: nowrap;
`;
const ChildWrapper = styled.div`
@@ -213,25 +175,88 @@ const ChildWrapper = styled.div`
width: 100%;
`;
const Child = styled.div`
width: 100%;
height: 36px;
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 500;
font-style: normal;
width: 100%;
min-height: 36px;
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 500;
font-style: normal;
padding: ${(props) => (props.hasChildren ? '8px 0 4px' : '8px 4px')};
${(props) =>
props.hasChildren
? `
margin-top: 14px;
border-top: 0.5px solid #a5a5a5;
margin-top: 10px;
border-top: 0.5px solid #e9e9e9;
height: auto;
`
: `
display: flex;
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 = ({
HeaderText,
@@ -261,10 +286,10 @@ const Header = ({
const [guestSideOf, setGuestSideOf] = useState(null);
const location = useLocation();
const handleImageClick = () => {
const toggleMenu = () => {
if (showRectangle) {
setAnimate("shrink");
setTimeout(() => setShowRectangle(false), 500);
setTimeout(() => setShowRectangle(false), 200);
} else {
setAnimate("grow");
setShowRectangle(true);
@@ -274,15 +299,14 @@ const Header = ({
const handleClickOutside = (event) => {
if (rectangleRef.current && !rectangleRef.current.contains(event.target)) {
setAnimate("shrink");
setTimeout(() => setShowRectangle(false), 500);
rectangleRef.current.style.overflow = "hidden";
setTimeout(() => setShowRectangle(false), 200);
}
};
const handleScroll = () => {
if (showRectangle) {
setAnimate("shrink");
setTimeout(() => setShowRectangle(false), 500);
setTimeout(() => setShowRectangle(false), 200);
}
};
@@ -321,38 +345,48 @@ const Header = ({
// Otherwise, use the possessive function
return `${cafeName}'s menu`;
};
return (
const formatCafeName = (name) => {
if (!name) return name;
return name
.toLowerCase()
.replace(/\b\w/g, (c) => c.toUpperCase());
};
return (
<HeaderBarbackground shopName={shopName}>
<HeaderBar HeaderMargin={HeaderMargin} shopName={shopName}>
<Title HeaderSize={HeaderSize}>
{shopName == null
? HeaderText == null
? "kedaimaster"
: HeaderText
: shopName}
</Title>
<div style={{ visibility: showProfile ? "visible" : "hidden" }}>
<ProfileImage
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"}
alt="Profile"
onClick={user.username !== undefined ? handleImageClick : null}
animate={showRectangle && animate}
/>
<ProfileName animate={showRectangle && animate}>
{showProfile && user.username !== undefined ? user.username : "guest"}
</ProfileName>
{showRectangle && (
<Rectangle ref={rectangleRef} animate={animate}>
<ChildContainer>
<HeaderBar HeaderMargin={HeaderMargin} shopName={shopName}>
<LeftGroup>
<CafeAvatar
src={shopImage && !shopImage.includes('undefined') ? shopImage : "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcS-DjX_bGBax4NL14ULvkAdU4FP3FKoWXWu5w&s"}
alt="Cafe"
/>
</LeftGroup>
<CenterGroup>
<Title HeaderSize={HeaderSize}>
{formatCafeName(
shopName == null
? HeaderText == null
? "kedaimaster"
: HeaderText
: shopName
)}
</Title>
</CenterGroup>
<RightGroup style={{ visibility: showProfile ? "visible" : "hidden", position: 'relative' }}>
<HamburgerButton onClick={toggleMenu} aria-label="Open menu">
<HamburgerIcon />
</HamburgerButton>
{showRectangle && (
<Rectangle ref={rectangleRef} animate={animate}>
<ChildContainer>
{guestSideOfClerk && guestSideOfClerk.clerkUsername && (
<Child hasChildren>
this is the guest side of {guestSideOfClerk.clerkUsername}
</Child>
)}
{user.username !== undefined && (
<Child onClick={() => setModal("edit_account")}>
Kelola akun
</Child>
<CategoryLabel>Kelola akun</CategoryLabel>
)}
{user.roleId == 0 && (
<Child onClick={()=>setModal('create_coupon', {})}>Buat Voucher</Child>)}
@@ -364,9 +398,9 @@ const Header = ({
user.roleId === 1 && (
<>
<Child hasChildren>
<Child>
{shopName}
</Child>
<CategoryLabel>
{formatCafeName(shopName)}
</CategoryLabel>
<Child>
Mode pengembangan &nbsp;
<Switch
@@ -381,7 +415,7 @@ const Header = ({
</Child>
<Child hasChildren>
<Child>Konfigurasi</Child>
<CategoryLabel>Konfigurasi</CategoryLabel>
<Child onClick={() => setModal("welcome_config")}>
Desain kafe
</Child>
@@ -393,7 +427,7 @@ const Header = ({
</Child>
</Child>
<Child hasChildren>
<Child>Kasir</Child>
<CategoryLabel>Kasir</CategoryLabel>
<Child onClick={() => setModal("create_clerk")}>
+ Tambah
</Child>
@@ -420,7 +454,7 @@ const Header = ({
user.cafeId == shopId &&
user.roleId === 2 && (
<Child hasChildren>
<Child>{shopName}</Child>
<CategoryLabel>{formatCafeName(shopName)}</CategoryLabel>
<Child>
Mode pengembangan&nbsp;
@@ -435,7 +469,7 @@ const Header = ({
</Child>
<Child hasChildren>
<Child>Konfigurasi</Child>
<CategoryLabel>Konfigurasi</CategoryLabel>
<Child onClick={() => setModal("welcome_config")}>
Desain kafe
</Child>
@@ -478,11 +512,12 @@ const Header = ({
{user.username !== undefined && (
<Child hasChildren ><Child onClick={isLogout}>Logout</Child></Child>
)}
</ChildContainer>
</Rectangle>
)}
</div>
</HeaderBar></HeaderBarbackground>
</ChildContainer>
</Rectangle>
)}
</RightGroup>
</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) => {
setItemPrice(event.target.value);
};
@@ -93,23 +98,17 @@ const Item = ({
<div className={`${!last && !forInvoice ? styles.notLast : ""}`}>
<div className={`${styles.item} ${forInvoice ? styles.itemInvoice : ""} `}>
{!forInvoice && (
// <div className={styles.imageContainer}>
<img
src={
previewUrl
}
src={previewUrl}
onError={({ currentTarget }) => {
currentTarget.onerror = null; // prevents looping
currentTarget.onerror = null;
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";
}}
alt={itemName}
style={{
filter: !isAvailable ? "grayscale(100%)" : "none",
}}
style={{ filter: !isAvailable ? "grayscale(100%)" : "none" }}
className={styles.imageContainer}
/>
// </div>
)}
<div className={styles.itemDetails}>
{forInvoice &&
@@ -141,168 +140,63 @@ const Item = ({
onChange={handleNameChange}
disabled={!blank && !isBeingEdit}
/> */}
<h3 style={{
textTransform: 'capitalize',
margin: `${forInvoice ? '13px 0px 10px 10px' : '5px 0px 10px 10px'}`,
fontSize: '16px',
display: '-webkit-box',
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
WebkitLineClamp: 2,
textOverflow: 'ellipsis',
width: `${forInvoice? '160px' : 'unset'}`
}}>
{itemName}
</h3>
<div style={{ marginRight: forInvoice ? 10 : 0 }}>
<h3 className={styles.title} style={{ width: forInvoice ? 160 : 'auto' }}>{itemName}</h3>
{!forInvoice && (
<div className={styles.priceRow}>
{promoPrice && promoPrice != 0 && promoPrice != '' ? (
<>
<div className={styles.promoBadge} style={{ background: !isAvailable ? 'gray' : undefined }}>
Promo {(((initialPrice - promoPrice) / initialPrice) * 100).toFixed(0)}%
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span className={styles.priceNow}>Rp {formatCurrency(promoPrice)}</span>
<span className={styles.priceOld}>Rp {formatCurrency(initialPrice)}</span>
</div>
</>
) : (
<span className={styles.priceNow}>Rp {formatCurrency(initialPrice)}</span>
)}
</div>
)}
</div>
{forInvoice && (
<>
<p className={styles.multiplySymbol}>x</p>
<p className={styles.qtyInvoice}>{itemQty}</p>
</>
)}
{!forInvoice && (
// <input
// 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 ? (
!isBeingEdit && itemQty > 0 ? (
<div className={styles.itemQty}>
<svg
className={styles.plusNegative}
onClick={handleNegativeClick}
clipRule="evenodd"
fillRule="evenodd"
strokeLinejoin="round"
strokeMiterlimit="2"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<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 className={styles.qtyGroup}>
<button className={styles.qtyBtn} onClick={handleNegativeClick} aria-label="Kurangi">-</button>
{!blank && !isBeingEdit ? (
<span className={styles.qtyVal}>{itemQty}</span>
) : (
<input className={styles.itemQtyInput} value={itemQty} onChange={handleQtyChange} disabled={!blank && !isBeingEdit} />
)}
<button className={styles.qtyBtn} onClick={handlePlusClick} aria-label="Tambah">+</button>
</div>
</div>
) : !blank && !isBeingEdit ? (
<div className={styles.itemQty}>
<button
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
>
<button className={styles.addButton} onClick={handlePlusClick} disabled={!isAvailable}>
Pesan
</button>
</div>
) : (
<div className={styles.itemQty}>
<button
className={styles.addButton}
style={{
backgroundColor: "white",
width: "150px",
color: '#a8c7a9'
}}
onClick={isBeingEdit ? handleUpdate : handleCreate}
>
{isBeingEdit ? "Simpan" : "Buat"}
<button className={styles.addButton} style={{ backgroundColor: '#ffffff', color: 'var(--brand-sage, #6B8F71)', borderColor: 'var(--brand-sage, #6B8F71)', width: 150 }} onClick={isBeingEdit ? handleUpdate : handleCreate}>
{isBeingEdit ? 'Simpan' : 'Buat'}
</button>
</div>
))}
)
)}
{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>
{forCart && (
@@ -316,18 +210,11 @@ const Item = ({
</button>
)} */}
</div>
{itemDescription && itemDescription != 'undefined' && itemDescription?.length &&
{itemDescription && itemDescription != 'undefined' && itemDescription?.length ? (
<div>
<p style={{
maxHeight: '34px',
display: '-webkit-box',
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
WebkitLineClamp: 2,
textOverflow: 'ellipsis', color: '#5f5f5f', fontSize: '14px', padding: '5px', margin: 0
}}>{itemDescription}</p>
<p className={styles.desc} style={{ padding: '4px 6px', margin: 0 }}>{itemDescription}</p>
</div>
}
) : null}
</div>
);
};

View File

@@ -6,19 +6,20 @@
.item {
display: flex;
align-items: stretch;
align-items: center;
justify-content: space-between;
padding-left: 5px;
margin-top: 5px;
margin-bottom: 5px;
color: rgba(88, 55, 50, 1);
font-size: 32px;
box-sizing: border-box; /* Include padding and border in the element's total width */
width: 100%; /* Ensure the item does not exceed the parent's width */
overflow: hidden; /* Prevent internal overflow */
padding-top: 10px;
margin-bottom: 5px;
width: 100%;
gap: 10px;
padding: 8px 10px;
margin: 6px 0;
border: 1px solid #e3ece6;
border-radius: 12px;
background: var(--brand-sage-50, #F0F6F2);
box-shadow: 0 1px 3px rgba(0,0,0,0.03);
box-sizing: border-box;
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) {
/* border-top: 2px solid #00000017; */
@@ -50,9 +51,9 @@
.imageContainer {
position: relative;
width: 26vw;
height: 26vw;
border-radius: 12px;
width: clamp(68px, 18vw, 96px);
height: clamp(68px, 18vw, 96px);
border-radius: 10px;
object-fit: cover;
}
@@ -84,11 +85,15 @@
.itemDetails {
display: flex;
flex-direction: column;
justify-content: space-between;
margin-left: 10px;
margin-right: 10px;
flex-grow: 1;
justify-content: center;
align-items: stretch;
gap: 6px;
margin-left: 6px;
margin-right: 6px;
flex: 1;
min-width: 0;
}
.infoRow { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
.itemInvoiceDetails {
display: flex;
@@ -127,17 +132,7 @@
font-weight: 500;
}
.itemPrice {
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;
}
.itemPrice { display: none; }
.itemPriceInvoice {
font-family: "Plus Jakarta Sans", sans-serif;
@@ -153,11 +148,9 @@
.itemQty {
display: flex;
align-items: center;
font-size: 0.9rem;
margin-left: 5px;
color: #a8c7a9;
fill: #a8c7a9;
height: 40px;
justify-content: flex-end;
gap: 8px;
min-height: 32px;
}
.itemQtyValue {
@@ -183,19 +176,21 @@
}
.addButton {
background-color: #ffffff;
border: 2px solid #a8c7a9;
/* border: none; */
display: inline-block;
font-size: 14px;
font-weight: 600;
cursor: pointer;
width: 87px;
height: 32px;
margin-left: 5px;
margin-top: 5px;
border-radius: 20px;
background-color: var(--brand-sage, #6B8F71);
border: 1px solid var(--brand-sage, #6B8F71);
color: #ffffff;
display: inline-block;
font-size: 13px;
font-weight: 600;
cursor: pointer;
min-width: 84px;
height: 34px;
padding: 0 14px;
border-radius: 10px; /* square rounded corner */
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 {
filter: grayscale(100%);
}
@@ -204,9 +199,8 @@
color: gray;
}
.plusNegative {
width: 35px;
height: 35px;
margin: 2.5px 0 -0.5px 0px;
width: 30px;
height: 30px;
}
.plusNegative2 {
@@ -224,6 +218,91 @@
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 {
flex-direction: row;
justify-content: space-between;

View File

@@ -17,6 +17,7 @@ import {
import ItemType from "./ItemType.js";
import { createItemType, updateItemDeletionStatus } from "../helpers/itemHelper.js";
import ItemConfig from "./ItemConfig.js"
import { ArrowUp, ArrowDown, Pencil } from 'lucide-react';
const ItemLister = ({
index,
@@ -618,87 +619,31 @@ const ItemLister = ({
disabled={!isEdit}
/>
{isEditMode && !isEdit && (
<>
<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 == 0 ? null : () => moveItemTypeUp(itemTypeId)} // Move onClick here for the whole div
<div className={styles.titleActions}>
<button
className={styles.iconBtn}
onClick={() => index === 0 ? null : moveItemTypeUp(itemTypeId)}
disabled={index === 0}
aria-label="Naikkan kategori"
>
<svg
viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg"
fill="#000000"
style={{ width: '100%', height: '100%' }} // Ensure SVG fits the div
>
<g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
<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
<ArrowUp size={18} />
</button>
<button
className={styles.iconBtn}
onClick={() => index === indexTotal - 1 ? null : moveItemTypeDown(itemTypeId)}
disabled={index === indexTotal - 1}
aria-label="Turunkan kategori"
>
<svg
viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg"
fill="#000000"
style={{ width: '100%', height: '100%' }} // Ensure SVG fits the div
>
<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
<ArrowDown size={18} />
</button>
<button
className={styles.iconBtn}
onClick={toggleEditTypeItem}
aria-label="Edit kategori"
>
<svg
fill="#000000"
viewBox="0 0 32 32"
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>
</>
<Pencil size={18} />
</button>
</div>
)}
</div>
}

View File

@@ -85,6 +85,33 @@
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 {
background-color: transparent;
font-family: "Plus Jakarta Sans", sans-serif;
@@ -224,4 +251,4 @@
font-size: 6vw;
color: black;
text-align: left;
}
}

View File

@@ -1,5 +1,6 @@
import React, { useRef, useEffect, useState } from "react";
import styles from "./ItemType.module.css";
import { Coffee, CupSoda, CakeSlice, Utensils, Grid2X2, Plus } from 'lucide-react';
export default function ItemType({
onClick,
@@ -57,6 +58,15 @@ export default function ItemType({
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 (
<div
className={
@@ -72,27 +82,25 @@ export default function ItemType({
>
<div
onClick={
rectangular ? (blank ? null : () => onClick(imageUrl)) : onClick
rectangular ? (blank ? null : () => onClick(iconImageUrl)) : onClick
}
className={styles["item-type-rect"]}
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' ?
<img
src={previewUrl}
alt={namee}
className={styles["item-type-image"]}
/>
:<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
{iconImageUrl === 'uploads/assets/All.png' ? (
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="100%" height="100%" viewBox="0 0 800.000000 800.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.16, written by Peter Selinger 2001-2019
</metadata>
<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
-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
@@ -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"/>
</g>
</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 && (
<div className={styles["item-type-image-container"]}>
<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
ref={inputRef}
className={`${styles["item-type-name"]} ${styles.noborder}`}
value={namee}
value={formatName(namee)}
onChange={handleNameChange}
disabled={true}
style={{
top: selected ? "-5px" : "initial",
borderBottom: selected ? "1px solid #000" : "none",
top: 'initial',
borderBottom: 'none',
color: selected ? '#2d2d2d' : '#333',
textTransform: 'capitalize'
}}
/>
)}
</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 {
width: calc(25vw - 20px);
height: calc(30vw - 20px);
margin: 1px 10px 0px;
width: auto;
height: auto;
margin: 0 4px; /* tighter spacing between tiles */
overflow: visible;
text-align: center;
align-items: center;
@@ -34,24 +34,33 @@
}
.item-type-rect {
position: relative;
height: 13vw;
width: 13vw;
height: clamp(48px, 9vw, 80px);
width: clamp(48px, 9vw, 80px);
object-fit: cover;
border-radius: 15px;
border-radius: 12px;
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 {
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 500;
font-style: normal;
font-size: 14px;
font-size: 12px;
color: #333;
width: calc(25vw - 30px);
width: auto;
text-align: center;
background-color: transparent;
position: relative; /* Needed for positioning the button */
position: relative;
margin-top: 6px; /* keep label spacing constant; avoid jumping */
}
.item-type-image {

View File

@@ -1,11 +1,23 @@
/* New clean, intuitive category bar */
.item-type-lister {
width: 100vw;
width: 100%;
overflow-x: auto;
white-space: nowrap;
padding: 3px 0px;
margin-bottom: -5px;
padding: 2px 0px; /* tighter top/bottom padding */
}
.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 {
display: inline-flex;
-ms-overflow-style: none; /* IE and Edge */
@@ -13,11 +25,44 @@
overflow-y: hidden;
}
.item-type {
display: inline-block;
margin-right: 20px;
/* Space between items */
.category-chip {
flex: 0 0 auto;
display: inline-flex;
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 {
width: 100vw;
height: 100vh;
@@ -55,13 +100,4 @@
bottom: 0;
align-self: center; /* Center the button horizontally */
}
.item-type-name {
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;
}
/* Legacy styles kept for ItemType grid if needed elsewhere */

View File

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

View File

@@ -55,6 +55,7 @@ function CafePage({
const [searchParams] = useSearchParams();
const token = searchParams.get("token");
const { shopIdentifier, tableCode } = useParams();
// Send params to parent immediately (original behavior)
sendParam({ shopIdentifier, tableCode });
const {
@@ -319,7 +320,7 @@ function CafePage({
/>
))}
{!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) &&
<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>
@@ -338,7 +339,7 @@ function CafePage({
</div>
}
{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', marginLeft: '5px', width: '20px' }}>
<svg viewBox="0 0 512 512">