make the create menu items as a modal

This commit is contained in:
goro 2026-06-07 04:14:37 +03:00
parent ba6102d57a
commit 30c487942f
3 changed files with 190 additions and 49 deletions

View File

@ -0,0 +1,114 @@
import { useState } from "preact/compat";
import { MenuItem } from "./types";
const CATEGORIES = [
{ value: 'appetizer', label: 'Appetizer' },
{ value: 'main_course', label: 'Main Course' },
{ value: 'dessert', label: 'Dessert' },
{ value: 'beverages', label: 'Beverages' },
{ value: 'snack', label: 'Snack' },
];
const emptyItem = (): MenuItem => ({ name: '', price: 0, category: '', description: '' });
interface MenuItemsModalProps {
selected: Array<MenuItem>;
onSave: (items: Array<MenuItem>) => void;
onClose: () => void;
}
export default function MenuItemsModal({ selected, onSave, onClose }: MenuItemsModalProps) {
const [items, setItems] = useState<Array<MenuItem>>(selected.length > 0 ? selected : [emptyItem()]);
function onChange(idx: number, field: keyof MenuItem, value: string) {
setItems(prev => prev.map((item, i) =>
i === idx ? { ...item, [field]: field === 'price' ? parseInt(value) || 0 : value } : item
));
}
function onAdd() {
setItems(prev => [...prev, emptyItem()]);
}
function onDelete(idx: number) {
setItems(prev => prev.filter((_, i) => i !== idx));
}
function handleSave() {
const valid = items.filter(item => item.name.trim() !== '');
onSave(valid);
}
return (
<div className="amenities-overlay" onClick={onClose}>
<div className="amenities-modal" style={{ maxWidth: 680 }} onClick={e => e.stopPropagation()}>
<div className="amenities-modal-header">
<span>Restaurant Menu</span>
<button className="amenities-close-btn" onClick={onClose}></button>
</div>
<div className="amenities-modal-body">
{items.map((item, idx) => (
<div key={idx} className="menu-item-row">
<div className="menu-item-fields">
<div className="menu-item-field">
<span className="menu-item-label">Name <span style={{ color: '#ed4245' }}>*</span></span>
<input
className="bg-primary text-sm menu-item-input"
type="text"
value={item.name}
onInput={e => onChange(idx, 'name', (e.target as HTMLInputElement).value)}
placeholder="e.g. Nasi Goreng"
/>
</div>
<div className="menu-item-field" style={{ flex: '0 0 120px' }}>
<span className="menu-item-label">Price (IDR)</span>
<input
className="bg-primary text-sm menu-item-input"
type="number"
min={0}
value={item.price}
onInput={e => onChange(idx, 'price', (e.target as HTMLInputElement).value)}
/>
</div>
<div className="menu-item-field" style={{ flex: '0 0 140px' }}>
<span className="menu-item-label">Category</span>
<select
className="bg-primary text-sm menu-item-input"
value={item.category}
onChange={e => onChange(idx, 'category', (e.target as HTMLSelectElement).value)}
>
<option value=""></option>
{CATEGORIES.map(c => (
<option key={c.value} value={c.value}>{c.label}</option>
))}
</select>
</div>
<div className="menu-item-field" style={{ flex: '1 1 180px' }}>
<span className="menu-item-label">Description</span>
<input
className="bg-primary text-sm menu-item-input"
type="text"
value={item.description}
onInput={e => onChange(idx, 'description', (e.target as HTMLInputElement).value)}
placeholder="Optional"
/>
</div>
<button className="menu-item-delete-btn" onClick={() => onDelete(idx)}></button>
</div>
</div>
))}
<button type="button" className="amenities-trigger-btn" onClick={onAdd}>+ Add Item</button>
</div>
<div className="amenities-modal-footer">
<span className="text-sm" style={{ opacity: 0.6 }}>{items.filter(i => i.name.trim()).length} item(s)</span>
<div style={{ display: 'flex', gap: 8 }}>
<button className="amenities-cancel-btn" onClick={onClose}>Cancel</button>
<button className="amenities-save-btn" onClick={handleSave}>Save</button>
</div>
</div>
</div>
</div>
);
}

View File

@ -4,7 +4,7 @@ import { LocationInfo } from "../../domains/LocationInfo";
import { DropdownInput } from "../../components"; import { DropdownInput } from "../../components";
import { IndonesiaRegionsInfo, LocationType } from "../../types/common"; import { IndonesiaRegionsInfo, LocationType } from "../../types/common";
import { Regency, emptyRegency } from "../../domains"; import { Regency, emptyRegency } from "../../domains";
import { Form, MenuItem } from './types'; import { Form } from './types';
import { enumKeys } from "../../utils"; import { enumKeys } from "../../utils";
import './style.css'; import './style.css';
import { createLocationService } from "../../services/locations"; import { createLocationService } from "../../services/locations";
@ -13,6 +13,7 @@ import { UserRootState } from "../../store/type";
import DefaultLoadingAnimation from "../../components/LoadingAnimation/Default"; import DefaultLoadingAnimation from "../../components/LoadingAnimation/Default";
import FallbackImage from "../../../src/components/Img/FallbackImage"; import FallbackImage from "../../../src/components/Img/FallbackImage";
import AmenitiesModal from "./AmenitiesModal"; import AmenitiesModal from "./AmenitiesModal";
import MenuItemsModal from "./MenuItemsModal";
function AddLocation() { function AddLocation() {
const [recentLocations, setRecentLocations] = useState<Array<LocationInfo>>() const [recentLocations, setRecentLocations] = useState<Array<LocationInfo>>()
@ -30,6 +31,7 @@ function AddLocation() {
restaurant_menu: [], restaurant_menu: [],
}) })
const [showAmenitiesModal, setShowAmenitiesModal] = useState(false) const [showAmenitiesModal, setShowAmenitiesModal] = useState(false)
const [showMenuModal, setShowMenuModal] = useState(false)
const [submitLoading, setSubmitLoading ] = useState<boolean>(false) const [submitLoading, setSubmitLoading ] = useState<boolean>(false)
const user = useSelector((state: UserRootState) => state.auth) const user = useSelector((state: UserRootState) => state.auth)
@ -70,23 +72,6 @@ function AddLocation() {
setForm({ ...form, location_type: value, amenities: [], restaurant_menu: [] }) setForm({ ...form, location_type: value, amenities: [], restaurant_menu: [] })
} }
function onAddMenuItem(e: TargetedEvent) {
e.preventDefault()
setForm({ ...form, restaurant_menu: [...form.restaurant_menu, { name: '', price: 0, category: '', description: '' }] })
}
function onChangeMenuItem(idx: number, field: keyof MenuItem, value: string) {
const updated = form.restaurant_menu.map((item, i) =>
i === idx ? { ...item, [field]: field === 'price' ? parseInt(value) || 0 : value } : item
)
setForm({ ...form, restaurant_menu: updated })
}
function onDeleteMenuItem(e: TargetedEvent, idx: number) {
e.preventDefault()
setForm({ ...form, restaurant_menu: form.restaurant_menu.filter((_, i) => i !== idx) })
}
async function onSubmitForm(e: TargetedEvent) { async function onSubmitForm(e: TargetedEvent) {
e.preventDefault(); e.preventDefault();
@ -222,38 +207,18 @@ function AddLocation() {
</select> </select>
{form.location_type === LocationType.Culinary && ( {form.location_type === LocationType.Culinary && (
<div> <div>
<span className={'block mt-2 text-sm mb-2'}>Restaurant Menu</span> <button type="button" className="amenities-trigger-btn" onClick={() => setShowMenuModal(true)}>
{form.restaurant_menu.map((item, idx) => ( {form.restaurant_menu.length > 0
<div key={idx} style={{ backgroundColor: '#23272a', padding: '8px 10px', marginBottom: 8, borderRadius: 4 }}> ? `Menu (${form.restaurant_menu.length} item${form.restaurant_menu.length > 1 ? 's' : ''})`
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'flex-end' }}> : '+ Configure Menu'}
<div style={{ flex: '1 1 140px' }}> </button>
<span className={'block text-xs mb-1'}>Name <span className={'text-error'}>*</span></span> {form.restaurant_menu.length > 0 && (
<input className={'bg-primary text-sm input-text'} type={'text'} value={item.name} onInput={(e) => onChangeMenuItem(idx, 'name', (e.target as HTMLInputElement).value)} /> <div className="amenities-tags">
</div> {form.restaurant_menu.map(item => (
<div style={{ flex: '1 1 100px' }}> <span key={item.name} className="amenity-tag">{item.name}{item.category ? ` · ${item.category}` : ''}</span>
<span className={'block text-xs mb-1'}>Price (IDR) <span className={'text-error'}>*</span></span>
<input className={'bg-primary text-sm input-text'} type={'number'} min={0} value={item.price} onInput={(e) => onChangeMenuItem(idx, 'price', (e.target as HTMLInputElement).value)} />
</div>
<div style={{ flex: '1 1 120px' }}>
<span className={'block text-xs mb-1'}>Category</span>
<select className={'bg-primary p-1 text-sm'} value={item.category} onChange={(e) => onChangeMenuItem(idx, 'category', (e.target as HTMLSelectElement).value)}>
<option value={''}></option>
<option value={'dessert'}>Dessert</option>
<option value={'main_course'}>Main Course</option>
<option value={'beverages'}>Beverages</option>
<option value={'appetizer'}>Appetizer</option>
<option value={'snack'}>Snack</option>
</select>
</div>
<div style={{ flex: '2 1 180px' }}>
<span className={'block text-xs mb-1'}>Description</span>
<input className={'bg-primary text-sm input-text'} type={'text'} value={item.description} onInput={(e) => onChangeMenuItem(idx, 'description', (e.target as HTMLInputElement).value)} />
</div>
<a onClick={(e) => onDeleteMenuItem(e, idx)} style={{ cursor: 'pointer', color: '#ed4245', fontWeight: 'bold', paddingBottom: 4 }}></a>
</div>
</div>
))} ))}
<button type="button" className="amenities-trigger-btn" onClick={onAddMenuItem}>+ Add Menu Item</button> </div>
)}
</div> </div>
)} )}
{(form.location_type === LocationType.Accommodation || form.location_type === LocationType.Culinary || form.location_type === LocationType.Mall) && ( {(form.location_type === LocationType.Accommodation || form.location_type === LocationType.Culinary || form.location_type === LocationType.Mall) && (
@ -340,6 +305,13 @@ function AddLocation() {
onClose={() => setShowAmenitiesModal(false)} onClose={() => setShowAmenitiesModal(false)}
/> />
)} )}
{showMenuModal && (
<MenuItemsModal
selected={form.restaurant_menu}
onSave={(restaurant_menu) => { setForm({ ...form, restaurant_menu }); setShowMenuModal(false); }}
onClose={() => setShowMenuModal(false)}
/>
)}
</> </>
) )
} }

View File

@ -219,3 +219,58 @@
font-size: 12px; font-size: 12px;
opacity: 0.8; opacity: 0.8;
} }
/* Menu Items Modal */
.menu-item-row {
border-bottom: 1px solid #202225;
padding-bottom: 12px;
margin-bottom: 12px;
}
.menu-item-row:last-of-type {
border-bottom: none;
margin-bottom: 0;
}
.menu-item-fields {
display: flex;
gap: 8px;
align-items: flex-end;
flex-wrap: wrap;
}
.menu-item-field {
display: flex;
flex-direction: column;
gap: 4px;
flex: 1 1 140px;
}
.menu-item-label {
font-size: 11px;
opacity: 0.6;
text-transform: uppercase;
letter-spacing: 0.8px;
}
.menu-item-input {
padding: 5px 8px;
border-radius: 5px;
width: 100%;
}
.menu-item-delete-btn {
background: none;
border: none;
color: #ed4245;
cursor: pointer;
font-size: 15px;
padding: 0 4px;
align-self: flex-end;
margin-bottom: 2px;
opacity: 0.7;
}
.menu-item-delete-btn:hover {
opacity: 1;
}