added edit page and revised edit form

This commit is contained in:
Haeri Kim
2026-06-14 10:47:21 +09:00
parent aadc80b7a8
commit cdf3643622
9 changed files with 736 additions and 42 deletions

View File

@@ -0,0 +1,127 @@
<script>
/**
* Searchable combobox input.
* @type {{ id?: string, value: string, options: string[], placeholder?: string, required?: boolean, onchange?: (v: string) => void }}
*/
let { id, value = $bindable(), options, placeholder = '', required = false } = $props();
let query = $state(value);
let open = $state(false);
let focused = $state(-1);
let filtered = $derived(
query.trim() === ''
? options
: options.filter(o => o.toLowerCase().includes(query.toLowerCase()))
);
function select(opt) {
query = opt;
value = opt;
open = false;
focused = -1;
}
function onInput(e) {
query = e.currentTarget.value;
value = query;
open = true;
focused = -1;
}
function onKeydown(e) {
if (!open) { if (e.key === 'ArrowDown') { open = true; } return; }
if (e.key === 'ArrowDown') { e.preventDefault(); focused = Math.min(focused + 1, filtered.length - 1); }
else if (e.key === 'ArrowUp') { e.preventDefault(); focused = Math.max(focused - 1, 0); }
else if (e.key === 'Enter' && focused >= 0) { e.preventDefault(); select(filtered[focused]); }
else if (e.key === 'Escape') { open = false; focused = -1; }
}
function onBlur() {
setTimeout(() => { open = false; focused = -1; }, 150);
}
// Keep query in sync if value is changed externally
$effect(() => { query = value; });
</script>
<div class="combo">
<input
{id}
{required}
{placeholder}
class="combo-input"
type="text"
autocomplete="off"
value={query}
oninput={onInput}
onkeydown={onKeydown}
onfocus={() => open = true}
onblur={onBlur}
/>
{#if open && filtered.length > 0}
<ul class="dropdown" role="listbox">
{#each filtered as opt, i}
<li
class="option"
class:highlighted={i === focused}
role="option"
aria-selected={opt === value}
onmousedown={() => select(opt)}
>{opt}</li>
{/each}
</ul>
{/if}
</div>
<style>
.combo {
position: relative;
width: 100%;
}
.combo-input {
font-family: var(--sans);
font-size: 14px;
font-weight: 300;
color: var(--text-h);
background: var(--bg-subtle);
border: 1px solid var(--border);
border-radius: 8px;
padding: 8px 12px;
outline: none;
transition: border-color 0.15s;
width: 100%;
}
.combo-input:focus { border-color: var(--accent-border); }
.dropdown {
position: absolute;
top: calc(100% + 4px);
left: 0;
right: 0;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0,0,0,0.1);
list-style: none;
max-height: 200px;
overflow-y: auto;
z-index: 50;
padding: 4px;
}
.option {
font-size: 13px;
font-weight: 300;
color: var(--text);
padding: 7px 10px;
border-radius: 6px;
cursor: pointer;
transition: background 0.1s, color 0.1s;
}
.option:hover, .option.highlighted {
background: var(--accent-bg);
color: var(--accent);
}
</style>

View File

@@ -0,0 +1,25 @@
export const countryCodeMap = {
'Argentina': 'AR', 'Australia': 'AU', 'Austria': 'AT',
'Belgium': 'BE', 'Brazil': 'BR',
'Canada': 'CA', 'Chile': 'CL', 'China': 'CN', 'Croatia': 'HR',
'Czech Republic': 'CZ', 'Denmark': 'DK', 'Egypt': 'EG',
'Finland': 'FI', 'France': 'FR', 'Germany': 'DE', 'Greece': 'GR',
'Hungary': 'HU', 'India': 'IN', 'Indonesia': 'ID', 'Italy': 'IT',
'Japan': 'JP', 'Kenya': 'KE',
'Malaysia': 'MY', 'Mexico': 'MX', 'Morocco': 'MA',
'Netherlands': 'NL', 'New Zealand': 'NZ', 'Norway': 'NO',
'Peru': 'PE', 'Poland': 'PL', 'Portugal': 'PT',
'Singapore': 'SG', 'South Africa': 'ZA', 'South Korea': 'KR',
'Spain': 'ES', 'Sweden': 'SE', 'Switzerland': 'CH',
'Taiwan': 'TW', 'Thailand': 'TH', 'Turkey': 'TR',
'UK': 'GB', 'USA': 'US', 'Vietnam': 'VN',
};
export const countryNames = Object.keys(countryCodeMap).sort();
/** @param {string} country */
export function flagEmoji(country) {
const code = countryCodeMap[country];
if (!code) return '';
return [...code].map(c => String.fromCodePoint(0x1F1E6 - 65 + c.charCodeAt(0))).join('');
}

View File

@@ -116,3 +116,8 @@ export function addJournal(entry) {
export function removeJournal(id) {
journals.update((entries) => entries.filter((e) => e.id !== id));
}
/** @param {JournalEntry} updated */
export function updateJournal(updated) {
journals.update((entries) => entries.map((e) => e.id === updated.id ? updated : e));
}

View File

@@ -0,0 +1,88 @@
<script>
let { entry, onConfirm, onCancel } = $props();
</script>
<div class="overlay" role="dialog" aria-modal="true">
<div class="dialog">
<h2 class="title">Delete entry?</h2>
<p class="body">
<strong>{entry.location.city}, {entry.location.country}</strong>{entry.date.slice(0, 4)} will be permanently removed.
</p>
<div class="actions">
<button class="btn btn-cancel" onclick={onCancel}>Cancel</button>
<button class="btn btn-delete" onclick={onConfirm}>Delete</button>
</div>
</div>
</div>
<style>
.overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.4);
z-index: 200;
display: flex;
align-items: center;
justify-content: center;
}
.dialog {
background: var(--bg);
border: 1px solid var(--border);
border-radius: 14px;
padding: 28px 32px;
width: 360px;
box-shadow: 0 8px 32px rgba(0,0,0,0.15);
}
.title {
font-size: 17px;
font-weight: 400;
color: var(--text-h);
margin-bottom: 10px;
}
.body {
font-size: 14px;
font-weight: 300;
color: var(--text);
line-height: 1.6;
margin-bottom: 24px;
}
.actions {
display: flex;
justify-content: flex-end;
gap: 8px;
}
.btn {
font-family: var(--sans);
font-size: 13px;
font-weight: 300;
padding: 8px 18px;
border-radius: 8px;
border: 1px solid var(--border);
cursor: pointer;
transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.btn-cancel {
background: var(--bg);
color: var(--text);
}
.btn-cancel:hover {
background: var(--bg-subtle);
color: var(--text-h);
}
.btn-delete {
background: #dc2626;
color: #fff;
border-color: #dc2626;
}
.btn-delete:hover {
background: #b91c1c;
border-color: #b91c1c;
}
</style>

View File

@@ -0,0 +1,268 @@
<script>
import { get } from 'svelte/store';
import { journals, updateJournal } from '../stores/journalStore.js';
import { countryNames } from '../shared/countries.js';
import SearchInput from '../shared/SearchInput.svelte';
import PhotoEditor from './PhotoEditor.svelte';
/** @type {{ entry: import('../stores/journalStore.js').JournalEntry, onBack: () => void }} */
let { entry, onBack } = $props();
let city = $state(entry.location.city);
let country = $state(entry.location.country);
let date = $state(entry.date);
let days = $state(String(entry.days));
let tripType = $state(entry.tripType);
let photos = $state([...entry.photos]);
let memo = $state(entry.memo);
let songTitle = $state(entry.song.title);
let songArtist = $state(entry.song.artist);
// City suggestions from existing entries (deduplicated)
let cityOptions = $derived(
[...new Set(get(journals).map(e => e.location.city))].sort()
);
function save() {
updateJournal({
...entry,
date,
days: Number(days),
tripType,
memo,
photos,
location: { city, country },
song: { title: songTitle, artist: songArtist },
});
onBack();
}
</script>
<div class="edit-layout">
<header class="edit-topbar">
<div class="topbar-left">
<button class="topbar-btn" onclick={onBack}>
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<path d="M19 12H5M12 5l-7 7 7 7"/>
</svg>
Back
</button>
</div>
<span class="topbar-title">Edit</span>
<div class="topbar-right">
<button class="topbar-btn topbar-btn--save" onclick={save}>Save changes</button>
</div>
</header>
<div class="edit-scroll">
<form class="form" onsubmit={(e) => { e.preventDefault(); save(); }}>
<div class="row">
<div class="field">
<label class="label" for="edit-city">City <span class="req">*</span></label>
<SearchInput id="edit-city" bind:value={city} options={cityOptions} required />
</div>
<div class="field">
<label class="label" for="edit-country">Country <span class="req">*</span></label>
<SearchInput id="edit-country" bind:value={country} options={countryNames} required />
</div>
</div>
<div class="row">
<div class="field">
<label class="label" for="edit-date">Date <span class="req">*</span></label>
<input id="edit-date" class="input" type="date" bind:value={date} required />
</div>
<div class="field">
<label class="label" for="edit-days">Days <span class="req">*</span></label>
<input id="edit-days" class="input" type="number" min="1" bind:value={days} required />
</div>
</div>
<div class="field">
<label class="label">Trip type</label>
<div class="toggle-row">
<label class="toggle-opt" class:active={tripType === 'solo'}>
<input type="radio" name="tripType" value="solo" bind:group={tripType} /> Solo
</label>
<label class="toggle-opt" class:active={tripType === 'friends'}>
<input type="radio" name="tripType" value="friends" bind:group={tripType} /> With friends
</label>
</div>
</div>
<PhotoEditor {photos} onchange={(p) => (photos = p)} />
<div class="field">
<label class="label" for="edit-memo">How was it?</label>
<textarea id="edit-memo" class="input textarea" rows="4" bind:value={memo}></textarea>
</div>
<div class="row">
<div class="field">
<label class="label" for="edit-song-title">Song title</label>
<input id="edit-song-title" class="input" type="text" bind:value={songTitle} />
</div>
<div class="field">
<label class="label" for="edit-song-artist">Artist</label>
<input id="edit-song-artist" class="input" type="text" bind:value={songArtist} />
</div>
</div>
</form>
</div>
</div>
<style>
.edit-layout {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
background: var(--bg);
}
.edit-topbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
height: 52px;
flex-shrink: 0;
border-bottom: 1px solid var(--border);
background: var(--bg);
}
.topbar-left, .topbar-right {
display: flex;
align-items: center;
gap: 4px;
min-width: 120px;
}
.topbar-right { justify-content: flex-end; }
.topbar-title {
font-size: 14px;
font-weight: 400;
color: var(--text-h);
}
.topbar-btn {
display: inline-flex;
align-items: center;
gap: 6px;
font-family: var(--sans);
font-size: 13px;
font-weight: 300;
color: var(--text);
background: none;
border: 1px solid transparent;
border-radius: 8px;
padding: 6px 12px;
cursor: pointer;
transition: background 0.15s, color 0.15s, border-color 0.15s;
white-space: nowrap;
}
.topbar-btn:hover {
background: var(--bg-subtle);
border-color: var(--border);
color: var(--text-h);
}
.topbar-btn--save {
background: var(--accent);
color: #fff;
border-color: var(--accent);
}
.topbar-btn--save:hover {
background: var(--accent-dark);
border-color: var(--accent-dark);
color: #fff;
}
.edit-scroll {
flex: 1;
overflow-y: auto;
}
.form {
display: flex;
flex-direction: column;
gap: 18px;
max-width: 560px;
margin: 0 auto;
padding: 36px 48px 80px;
}
.row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 14px;
}
.field {
display: flex;
flex-direction: column;
gap: 6px;
}
.label {
font-size: 11px;
font-weight: 400;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text-sub);
}
.req {
color: var(--accent);
font-size: 11px;
}
.input {
font-family: var(--sans);
font-size: 14px;
font-weight: 300;
color: var(--text-h);
background: var(--bg-subtle);
border: 1px solid var(--border);
border-radius: 8px;
padding: 8px 12px;
outline: none;
transition: border-color 0.15s;
width: 100%;
}
.input:focus { border-color: var(--accent-border); }
.textarea {
resize: vertical;
line-height: 1.6;
}
.toggle-row {
display: flex;
gap: 8px;
}
.toggle-opt {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
font-weight: 300;
color: var(--text);
padding: 7px 14px;
border-radius: 8px;
border: 1px solid var(--border);
cursor: pointer;
transition: border-color 0.15s, background 0.15s, color 0.15s;
background: var(--bg-subtle);
}
.toggle-opt input { display: none; }
.toggle-opt.active {
border-color: var(--accent-border);
background: var(--accent-bg);
color: var(--accent);
}
</style>

View File

@@ -1,19 +1,16 @@
<script>
/** @type {{ entry: import('../stores/journalStore.js').JournalEntry, onBack: () => void }} */
let { entry, onBack } = $props();
import { removeJournal } from '../stores/journalStore.js';
import { flagEmoji } from '../shared/countries.js';
import DeleteConfirm from './DeleteConfirm.svelte';
const countryCodeMap = {
'Japan': 'JP', 'France': 'FR', 'Spain': 'ES', 'USA': 'US',
'Thailand': 'TH', 'Germany': 'DE', 'Italy': 'IT', 'UK': 'GB',
'Australia': 'AU', 'Canada': 'CA', 'China': 'CN', 'India': 'IN',
'Brazil': 'BR', 'Mexico': 'MX', 'Portugal': 'PT', 'Netherlands': 'NL',
'Greece': 'GR', 'Turkey': 'TR', 'Vietnam': 'VN', 'Indonesia': 'ID',
'South Korea': 'KR', 'Singapore': 'SG', 'Taiwan': 'TW', 'New Zealand': 'NZ',
};
function flagEmoji(country) {
const code = countryCodeMap[country];
if (!code) return '';
return [...code].map(c => String.fromCodePoint(0x1F1E6 - 65 + c.charCodeAt(0))).join('');
/** @type {{ entry: import('../stores/journalStore.js').JournalEntry, onBack: () => void, onEdit: () => void }} */
let { entry, onBack, onEdit } = $props();
let showDeleteConfirm = $state(false);
function handleDelete() {
removeJournal(entry.id);
onBack();
}
function formatDate(iso) {
@@ -25,6 +22,10 @@
let lightboxSrc = $state(null);
</script>
{#if showDeleteConfirm}
<DeleteConfirm {entry} onConfirm={handleDelete} onCancel={() => showDeleteConfirm = false} />
{/if}
<!-- Lightbox -->
{#if lightboxSrc}
<div class="lightbox" onclick={() => lightboxSrc = null} role="button" tabindex="0"
@@ -55,14 +56,14 @@
</div>
<div class="topbar-right">
<button class="topbar-btn" title="Edit entry">
<button class="topbar-btn" title="Edit entry" onclick={onEdit}>
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
</svg>
Edit
</button>
<button class="topbar-btn topbar-btn--danger" title="Delete entry">
<button class="topbar-btn topbar-btn--danger" title="Delete entry" onclick={() => showDeleteConfirm = true}>
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 6h18M8 6V4h8v2M19 6l-1 14H6L5 6"/>
</svg>

View File

@@ -0,0 +1,189 @@
<script>
/** @type {{ photos: string[], onchange: (photos: string[]) => void }} */
let { photos, onchange } = $props();
let fileInput;
function remove(index) {
const next = photos.filter((_, i) => i !== index);
onchange(next);
}
async function addFiles(e) {
const files = Array.from(e.currentTarget.files ?? []);
if (!files.length) return;
const dataUrls = await Promise.all(files.map(fileToDataUrl));
onchange([...photos, ...dataUrls]);
// reset so the same file can be picked again
e.currentTarget.value = '';
}
/** @param {File} file */
function fileToDataUrl(file) {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onload = (e) => resolve(/** @type {string} */ (e.target.result));
reader.readAsDataURL(file);
});
}
</script>
<div class="photo-editor">
<div class="label-row">
<span class="label">Photos</span>
<button type="button" class="add-btn" onclick={() => fileInput.click()}>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<path d="M12 5v14M5 12h14"/>
</svg>
Add photos
</button>
<input bind:this={fileInput} type="file" accept="image/*" multiple onchange={addFiles} hidden />
</div>
{#if photos.length === 0}
<button type="button" class="empty-zone" onclick={() => fileInput.click()}>
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2">
<rect x="3" y="3" width="18" height="18" rx="3"/>
<circle cx="8.5" cy="8.5" r="1.5"/>
<path d="M21 15l-5-5L5 21"/>
</svg>
<span>Click to add photos</span>
</button>
{:else}
<div class="grid">
{#each photos as src, i (src + i)}
<div class="cell">
<img {src} alt="photo {i + 1}" />
<button type="button" class="remove-btn" onclick={() => remove(i)} aria-label="Remove photo">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round">
<path d="M18 6L6 18M6 6l12 12"/>
</svg>
</button>
</div>
{/each}
<button type="button" class="add-cell" onclick={() => fileInput.click()}>
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round">
<path d="M12 5v14M5 12h14"/>
</svg>
</button>
</div>
{/if}
</div>
<style>
.photo-editor {
display: flex;
flex-direction: column;
gap: 10px;
}
.label-row {
display: flex;
align-items: center;
gap: 10px;
}
.label {
font-size: 11px;
font-weight: 400;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text-sub);
flex: 1;
}
.add-btn {
display: inline-flex;
align-items: center;
gap: 5px;
font-family: var(--sans);
font-size: 12px;
font-weight: 300;
color: var(--accent);
background: var(--accent-bg);
border: 1px solid var(--accent-border);
border-radius: 6px;
padding: 4px 10px;
cursor: pointer;
transition: background 0.15s;
}
.add-btn:hover { background: rgba(124,58,237,0.12); }
.empty-zone {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
height: 120px;
border: 1.5px dashed var(--border-bright);
border-radius: 10px;
color: var(--text-sub);
font-family: var(--sans);
font-size: 13px;
font-weight: 300;
cursor: pointer;
background: var(--bg-subtle);
transition: border-color 0.15s, color 0.15s;
width: 100%;
}
.empty-zone:hover { border-color: var(--accent-border); color: var(--accent); }
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 6px;
}
.cell {
position: relative;
aspect-ratio: 1;
border-radius: 8px;
overflow: hidden;
background: var(--bg-subtle);
}
.cell img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.remove-btn {
position: absolute;
top: 5px;
right: 5px;
width: 22px;
height: 22px;
border-radius: 50%;
border: none;
background: rgba(0,0,0,0.55);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
opacity: 0;
transition: opacity 0.15s, background 0.15s;
}
.cell:hover .remove-btn { opacity: 1; }
.remove-btn:hover { background: rgba(220,38,38,0.85); }
.add-cell {
aspect-ratio: 1;
border-radius: 8px;
border: 1.5px dashed var(--border-bright);
background: var(--bg-subtle);
color: var(--text-sub);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: border-color 0.15s, color 0.15s;
}
.add-cell:hover { border-color: var(--accent-border); color: var(--accent); }
</style>

View File

@@ -1,28 +1,9 @@
<script>
import { flagEmoji } from '../shared/countries.js';
/** @type {{ entry: import('../stores/journalStore.js').JournalEntry, onClick: () => void }} */
let { entry, onClick } = $props();
/** Convert country name to flag emoji via ISO 3166-1 alpha-2 */
const countryCodeMap = {
'Japan': 'JP', 'France': 'FR', 'Spain': 'ES', 'USA': 'US',
'Thailand': 'TH', 'Germany': 'DE', 'Italy': 'IT', 'UK': 'GB',
'Australia': 'AU', 'Canada': 'CA', 'China': 'CN', 'India': 'IN',
'Brazil': 'BR', 'Mexico': 'MX', 'Portugal': 'PT', 'Netherlands': 'NL',
'Greece': 'GR', 'Turkey': 'TR', 'Vietnam': 'VN', 'Indonesia': 'ID',
'Malaysia': 'MY', 'Singapore': 'SG', 'South Korea': 'KR', 'Taiwan': 'TW',
'New Zealand': 'NZ', 'Argentina': 'AR', 'Chile': 'CL', 'Peru': 'PE',
'Morocco': 'MA', 'Egypt': 'EG', 'Kenya': 'KE', 'South Africa': 'ZA',
'Sweden': 'SE', 'Norway': 'NO', 'Denmark': 'DK', 'Finland': 'FI',
'Switzerland': 'CH', 'Austria': 'AT', 'Belgium': 'BE', 'Poland': 'PL',
'Czech Republic': 'CZ', 'Hungary': 'HU', 'Croatia': 'HR',
};
function flagEmoji(country) {
const code = countryCodeMap[country];
if (!code) return '';
return [...code].map(c => String.fromCodePoint(0x1F1E6 - 65 + c.charCodeAt(0))).join('');
}
function formatDate(/** @type {string} */ iso) {
return new Date(iso).toLocaleDateString('en-US', {
month: 'short', day: 'numeric', year: 'numeric',

View File

@@ -5,10 +5,12 @@
import TimelineCard from './TimelineCard.svelte';
import JournalDetail from './JournalDetail.svelte';
import JournalSummary from './JournalSummary.svelte';
import EditForm from './EditForm.svelte';
/** @type {import('../stores/journalStore.js').JournalEntry|null} */
let { onDetailChange = () => {} } = $props();
let selected = $state(null);
let selectedId = $state(/** @type {string|null} */(null));
let view = $state(/** @type {'list'|'detail'|'edit'} */('list'));
let selected = $derived(selectedId ? (entries.find(e => e.id === selectedId) ?? null) : null);
let entries = $state(get(journals));
$effect(() => {
@@ -37,9 +39,17 @@
<div class="journal-page">
{#if selected}
{#if view === 'edit' && selected}
<div class="detail-scroll">
<JournalDetail entry={selected} onBack={() => { selected = null; onDetailChange(false); }} />
<EditForm entry={selected} onBack={() => { view = 'detail'; }} />
</div>
{:else if view === 'detail' && selected}
<div class="detail-scroll">
<JournalDetail
entry={selected}
onBack={() => { selectedId = null; view = 'list'; onDetailChange(false); }}
onEdit={() => { view = 'edit'; }}
/>
</div>
{:else}
<div class="right-panel">
@@ -69,7 +79,7 @@
<span class="year-label">{getYear(entry.date)}</span>
</li>
{/if}
<TimelineCard {entry} onClick={() => { selected = entry; onDetailChange(true); }} />
<TimelineCard {entry} onClick={() => { selectedId = entry.id; view = 'detail'; onDetailChange(true); }} />
{/each}
</ol>
{/if}