make the create menu items as a modal
This commit is contained in:
parent
ba6102d57a
commit
30c487942f
114
src/pages/AddLocation/MenuItemsModal.tsx
Normal file
114
src/pages/AddLocation/MenuItemsModal.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -4,7 +4,7 @@ import { LocationInfo } from "../../domains/LocationInfo";
|
||||
import { DropdownInput } from "../../components";
|
||||
import { IndonesiaRegionsInfo, LocationType } from "../../types/common";
|
||||
import { Regency, emptyRegency } from "../../domains";
|
||||
import { Form, MenuItem } from './types';
|
||||
import { Form } from './types';
|
||||
import { enumKeys } from "../../utils";
|
||||
import './style.css';
|
||||
import { createLocationService } from "../../services/locations";
|
||||
@ -13,6 +13,7 @@ import { UserRootState } from "../../store/type";
|
||||
import DefaultLoadingAnimation from "../../components/LoadingAnimation/Default";
|
||||
import FallbackImage from "../../../src/components/Img/FallbackImage";
|
||||
import AmenitiesModal from "./AmenitiesModal";
|
||||
import MenuItemsModal from "./MenuItemsModal";
|
||||
|
||||
function AddLocation() {
|
||||
const [recentLocations, setRecentLocations] = useState<Array<LocationInfo>>()
|
||||
@ -30,6 +31,7 @@ function AddLocation() {
|
||||
restaurant_menu: [],
|
||||
})
|
||||
const [showAmenitiesModal, setShowAmenitiesModal] = useState(false)
|
||||
const [showMenuModal, setShowMenuModal] = useState(false)
|
||||
const [submitLoading, setSubmitLoading ] = useState<boolean>(false)
|
||||
|
||||
const user = useSelector((state: UserRootState) => state.auth)
|
||||
@ -70,23 +72,6 @@ function AddLocation() {
|
||||
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) {
|
||||
e.preventDefault();
|
||||
|
||||
@ -222,38 +207,18 @@ function AddLocation() {
|
||||
</select>
|
||||
{form.location_type === LocationType.Culinary && (
|
||||
<div>
|
||||
<span className={'block mt-2 text-sm mb-2'}>Restaurant Menu</span>
|
||||
{form.restaurant_menu.map((item, idx) => (
|
||||
<div key={idx} style={{ backgroundColor: '#23272a', padding: '8px 10px', marginBottom: 8, borderRadius: 4 }}>
|
||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'flex-end' }}>
|
||||
<div style={{ flex: '1 1 140px' }}>
|
||||
<span className={'block text-xs mb-1'}>Name <span className={'text-error'}>*</span></span>
|
||||
<input className={'bg-primary text-sm input-text'} type={'text'} value={item.name} onInput={(e) => onChangeMenuItem(idx, 'name', (e.target as HTMLInputElement).value)} />
|
||||
</div>
|
||||
<div style={{ flex: '1 1 100px' }}>
|
||||
<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>
|
||||
<button type="button" className="amenities-trigger-btn" onClick={() => setShowMenuModal(true)}>
|
||||
{form.restaurant_menu.length > 0
|
||||
? `Menu (${form.restaurant_menu.length} item${form.restaurant_menu.length > 1 ? 's' : ''})`
|
||||
: '+ Configure Menu'}
|
||||
</button>
|
||||
{form.restaurant_menu.length > 0 && (
|
||||
<div className="amenities-tags">
|
||||
{form.restaurant_menu.map(item => (
|
||||
<span key={item.name} className="amenity-tag">{item.name}{item.category ? ` · ${item.category}` : ''}</span>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
<button type="button" className="amenities-trigger-btn" onClick={onAddMenuItem}>+ Add Menu Item</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{(form.location_type === LocationType.Accommodation || form.location_type === LocationType.Culinary || form.location_type === LocationType.Mall) && (
|
||||
@ -340,6 +305,13 @@ function AddLocation() {
|
||||
onClose={() => setShowAmenitiesModal(false)}
|
||||
/>
|
||||
)}
|
||||
{showMenuModal && (
|
||||
<MenuItemsModal
|
||||
selected={form.restaurant_menu}
|
||||
onSave={(restaurant_menu) => { setForm({ ...form, restaurant_menu }); setShowMenuModal(false); }}
|
||||
onClose={() => setShowMenuModal(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -218,4 +218,59 @@
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
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;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user