feat(edit-modal): implement mobile edit modal for medication with responsive design
This commit is contained in:
+126
-3
@@ -192,6 +192,7 @@ function AppContent() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [editingId, setEditingId] = useState<number | null>(null);
|
||||
const [showEditModal, setShowEditModal] = useState(false);
|
||||
const [form, setForm] = useState<FormState>(defaultForm());
|
||||
const [range, setRange] = useState<{ start: string; end: string }>({
|
||||
start: toInputValue(todayIso()),
|
||||
@@ -686,10 +687,15 @@ function AppContent() {
|
||||
startTime: toTimeValue(s.start)
|
||||
})),
|
||||
});
|
||||
// Show modal on mobile
|
||||
if (window.innerWidth <= 768) {
|
||||
setShowEditModal(true);
|
||||
}
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
setEditingId(null);
|
||||
setShowEditModal(false);
|
||||
setPendingImage(null);
|
||||
setPendingImagePreview(null);
|
||||
setForm(defaultForm());
|
||||
@@ -1314,7 +1320,7 @@ function AppContent() {
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article className="card form">
|
||||
<article className="card form desktop-only">
|
||||
<div className="card-head">
|
||||
<h2>{editingId ? t('form.editEntry') : t('form.newEntry')}</h2>
|
||||
<span className="pill">{t('form.badge')}</span>
|
||||
@@ -2148,8 +2154,8 @@ function AppContent() {
|
||||
|
||||
{/* Image Lightbox */}
|
||||
{showImageLightbox && selectedMed.imageUrl && (
|
||||
<div className="lightbox-overlay" onClick={() => setShowImageLightbox(false)}>
|
||||
<button className="lightbox-close" onClick={() => setShowImageLightbox(false)}>×</button>
|
||||
<div className="lightbox-overlay" onClick={(e) => { e.stopPropagation(); setShowImageLightbox(false); }}>
|
||||
<button className="lightbox-close" onClick={(e) => { e.stopPropagation(); setShowImageLightbox(false); }}>×</button>
|
||||
<img
|
||||
src={`/api/images/${selectedMed.imageUrl}`}
|
||||
alt={selectedMed.name}
|
||||
@@ -2282,6 +2288,123 @@ function AppContent() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mobile Edit Modal */}
|
||||
{showEditModal && (
|
||||
<div className="modal-overlay" onClick={() => setShowEditModal(false)}>
|
||||
<div className="modal-content edit-modal" onClick={(e) => e.stopPropagation()}>
|
||||
<button className="modal-close" onClick={() => { setShowEditModal(false); resetForm(); }}>×</button>
|
||||
<div className="edit-modal-header">
|
||||
<h2>{editingId ? t('form.editEntry') : t('form.newEntry')}</h2>
|
||||
</div>
|
||||
<form className="form-grid mobile-edit-form" onSubmit={(e) => { saveMedication(e); setShowEditModal(false); }}>
|
||||
<label>
|
||||
{t('form.commercialName')}
|
||||
<input value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} placeholder={t('form.placeholders.commercial')} required />
|
||||
</label>
|
||||
<label>
|
||||
{t('form.genericName')}
|
||||
<input value={form.genericName} onChange={(e) => setForm({ ...form, genericName: e.target.value })} placeholder={t('form.placeholders.generic')} />
|
||||
</label>
|
||||
<label>
|
||||
{t('form.takenBy')}
|
||||
<input value={form.takenBy} onChange={(e) => setForm({ ...form, takenBy: e.target.value })} placeholder={t('form.placeholders.takenBy')} />
|
||||
</label>
|
||||
<label>
|
||||
{t('form.packs')}
|
||||
<input type="number" min="0" value={form.packCount} onChange={(e) => handleValueChange("packCount", e.target.value)} />
|
||||
</label>
|
||||
<label>
|
||||
{t('form.blistersPerPack')}
|
||||
<input type="number" min="0" value={form.stripsPerPack} onChange={(e) => handleValueChange("stripsPerPack", e.target.value)} />
|
||||
</label>
|
||||
<label>
|
||||
{t('form.pillsPerBlister')}
|
||||
<input type="number" min="1" value={form.tabsPerStrip} onChange={(e) => handleValueChange("tabsPerStrip", e.target.value)} />
|
||||
</label>
|
||||
<label>
|
||||
{t('form.loosePills')}
|
||||
<input type="number" min="0" value={form.looseTablets} onChange={(e) => handleValueChange("looseTablets", e.target.value)} />
|
||||
</label>
|
||||
<label>
|
||||
{t('form.pillWeight')}
|
||||
<input type="number" min="0" step="0.1" value={form.pillWeightMg} onChange={(e) => setForm({ ...form, pillWeightMg: e.target.value })} placeholder={t('form.placeholders.pillWeight')} />
|
||||
</label>
|
||||
<label>
|
||||
{t('form.expiry')}
|
||||
<input type="date" value={form.expiryDate} onChange={(e) => setForm({ ...form, expiryDate: e.target.value })} />
|
||||
</label>
|
||||
<label className="full">
|
||||
{t('form.notes')}
|
||||
<textarea value={form.notes} onChange={(e) => setForm({ ...form, notes: e.target.value })} placeholder={t('form.placeholders.notes')} rows={2} />
|
||||
</label>
|
||||
<div className="full">
|
||||
<p className="sub"><strong>{t('form.total')}:</strong> {deriveTotal(form)} {t('common.pills')}</p>
|
||||
</div>
|
||||
|
||||
{editingId && (() => {
|
||||
const currentMed = meds.find(m => m.id === editingId);
|
||||
if (currentMed?.imageUrl) {
|
||||
return (
|
||||
<div className="full image-field">
|
||||
<span className="field-label">{t('form.medicationImage')}</span>
|
||||
<div className="image-preview">
|
||||
<img src={currentMed.imageUrl} alt={currentMed.name} />
|
||||
<button type="button" className="ghost danger" onClick={() => deleteMedImage(editingId)}>{t('form.removeImage')}</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<label className="full">
|
||||
{t('form.medicationImage')}
|
||||
<input type="file" accept="image/*" onChange={(e) => e.target.files?.[0] && uploadMedImage(editingId, e.target.files[0])} />
|
||||
</label>
|
||||
);
|
||||
})()}
|
||||
|
||||
<fieldset className="full blister-section">
|
||||
<legend>
|
||||
{t('form.blisters.title')}
|
||||
<label className="toggle-switch small" title={t('form.blisters.remindTooltip')}>
|
||||
<input type="checkbox" checked={form.intakeRemindersEnabled} onChange={(e) => setForm({ ...form, intakeRemindersEnabled: e.target.checked })} />
|
||||
<span className="toggle-slider"></span>
|
||||
</label>
|
||||
<span className="legend-hint">{t('form.blisters.remind')}</span>
|
||||
</legend>
|
||||
{form.blisters.map((b, idx) => (
|
||||
<div key={idx} className="blister-row">
|
||||
<label className="compact">
|
||||
<span>{t('form.blisters.usage')}</span>
|
||||
<input type="number" min="0.5" step="0.5" value={b.usage} onChange={(e) => setBlisterValue(idx, "usage", e.target.value)} />
|
||||
</label>
|
||||
<label className="compact">
|
||||
<span>{t('form.blisters.everyDays')}</span>
|
||||
<input type="number" min="1" value={b.every} onChange={(e) => setBlisterValue(idx, "every", e.target.value)} />
|
||||
</label>
|
||||
<label className="compact">
|
||||
<span>{t('form.blisters.startDate')}</span>
|
||||
<div className="datetime-inputs">
|
||||
<input type="date" value={b.startDate} onChange={(e) => setBlisterValue(idx, "startDate", e.target.value)} />
|
||||
<input type="time" value={b.startTime} onChange={(e) => setBlisterValue(idx, "startTime", e.target.value)} />
|
||||
</div>
|
||||
</label>
|
||||
{form.blisters.length > 1 && <button type="button" className="ghost danger remove-blister-btn" onClick={() => removeBlister(idx)} title={t('common.delete')}>🗑</button>}
|
||||
</div>
|
||||
))}
|
||||
<button type="button" className="ghost add-blister" onClick={addBlister}>+ {t('form.blisters.addIntake')}</button>
|
||||
</fieldset>
|
||||
|
||||
<div className="full align-end gap">
|
||||
<button type="button" className="ghost" onClick={() => { setShowEditModal(false); resetForm(); }}>
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
<button type="submit" disabled={saving}>{saving ? t('common.saving') : t('common.save')}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
+129
-2
@@ -1921,10 +1921,11 @@ textarea {
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
padding: 1rem;
|
||||
padding: 2rem 1rem;
|
||||
overflow-y: auto;
|
||||
animation: fadeIn 0.2s ease;
|
||||
}
|
||||
|
||||
@@ -2661,6 +2662,8 @@ h3 .reminder-icon.info-tooltip {
|
||||
.share-dialog-modal {
|
||||
max-width: 480px;
|
||||
padding: 1.5rem;
|
||||
margin-top: 0;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.share-dialog-header {
|
||||
@@ -2867,3 +2870,127 @@ h3 .reminder-icon.info-tooltip {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Desktop only - hide on mobile */
|
||||
@media (max-width: 768px) {
|
||||
.desktop-only {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile Edit Modal */
|
||||
.edit-modal {
|
||||
max-width: 95vw;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.edit-modal-header {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.edit-modal-header h2 {
|
||||
font-size: 1.25rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.mobile-edit-form {
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.mobile-edit-form label {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.mobile-edit-form input,
|
||||
.mobile-edit-form textarea,
|
||||
.mobile-edit-form select {
|
||||
font-size: 16px; /* Prevents zoom on iOS */
|
||||
}
|
||||
|
||||
/* Mobile Blister/Intake Schedule Section */
|
||||
.mobile-edit-form .blister-section {
|
||||
padding: 1rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.mobile-edit-form .blister-section legend {
|
||||
font-size: 0.95rem;
|
||||
padding: 0 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: nowrap;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mobile-edit-form .blister-section legend .toggle-switch {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.mobile-edit-form .blister-section legend .legend-hint {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.mobile-edit-form .blister-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr auto;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 0.5rem;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.mobile-edit-form .blister-row label.compact {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.mobile-edit-form .blister-row label.compact span {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.mobile-edit-form .blister-row label.compact:nth-child(3) {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.mobile-edit-form .blister-row .datetime-inputs {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.mobile-edit-form .blister-row .datetime-inputs input[type="date"] {
|
||||
flex: 2;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.mobile-edit-form .blister-row .datetime-inputs input[type="time"] {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Remove blister button */
|
||||
.remove-blister-btn {
|
||||
padding: 0.25rem 0.5rem !important;
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
border-radius: 6px !important;
|
||||
min-width: auto;
|
||||
align-self: center;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.remove-blister-btn:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.mobile-edit-form .add-blister {
|
||||
margin-top: 0.5rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user