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 { 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)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user