vines
This commit is contained in:
+88
-10
@@ -10,6 +10,9 @@ const copied = ref(false)
|
||||
const items = ref([])
|
||||
const newName = ref('')
|
||||
|
||||
const editingId = ref(null)
|
||||
const editName = ref('')
|
||||
|
||||
const sublists = ref([])
|
||||
const newSublistName = ref('')
|
||||
const newSublistColor = ref('#16a34a')
|
||||
@@ -180,24 +183,62 @@ async function removeItem(item) {
|
||||
items.value = items.value.filter((i) => i.id !== item.id)
|
||||
}
|
||||
|
||||
function startEdit(item) {
|
||||
editingId.value = item.id
|
||||
editName.value = item.name
|
||||
}
|
||||
|
||||
function cancelEdit() {
|
||||
editingId.value = null
|
||||
}
|
||||
|
||||
function saveEdit(item) {
|
||||
if (editingId.value !== item.id) return
|
||||
editingId.value = null
|
||||
const name = editName.value.trim()
|
||||
if (!name || name === item.name) return
|
||||
fetch(`/api/lists/${listId.value}/items/${item.id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name }),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((updated) => Object.assign(item, updated))
|
||||
}
|
||||
|
||||
const SWIPE_THRESHOLD = 70
|
||||
const SWIPE_MAX = 140
|
||||
const LONG_PRESS_MS = 500
|
||||
const LONG_PRESS_TOLERANCE = 10
|
||||
let drag = null
|
||||
let longPressTimer = null
|
||||
|
||||
function onSwipeStart(e) {
|
||||
drag = { startX: e.clientX, dx: 0, el: e.currentTarget }
|
||||
function onSwipeStart(e, item) {
|
||||
drag = { startX: e.clientX, startY: e.clientY, dx: 0, el: e.currentTarget }
|
||||
drag.el.setPointerCapture(e.pointerId)
|
||||
drag.el.style.transition = 'none'
|
||||
|
||||
clearTimeout(longPressTimer)
|
||||
longPressTimer = setTimeout(() => {
|
||||
if (!drag) return
|
||||
startEdit(item)
|
||||
drag.el.style.transform = ''
|
||||
drag = null
|
||||
}, LONG_PRESS_MS)
|
||||
}
|
||||
|
||||
function onSwipeMove(e) {
|
||||
if (!drag) return
|
||||
if (Math.abs(e.clientX - drag.startX) > LONG_PRESS_TOLERANCE || Math.abs(e.clientY - drag.startY) > LONG_PRESS_TOLERANCE) {
|
||||
clearTimeout(longPressTimer)
|
||||
}
|
||||
const dx = e.clientX - drag.startX
|
||||
drag.dx = Math.max(-SWIPE_MAX, Math.min(SWIPE_MAX, dx))
|
||||
drag.el.style.transform = `translateX(${drag.dx}px)`
|
||||
}
|
||||
|
||||
function onSwipeEnd(e, item) {
|
||||
clearTimeout(longPressTimer)
|
||||
if (!drag) return
|
||||
const { dx, el } = drag
|
||||
el.style.transition = 'transform 0.2s ease'
|
||||
@@ -371,6 +412,19 @@ function onHandleEnd() {
|
||||
item.position = position
|
||||
item.sublist_id = targetSublistId
|
||||
|
||||
// Reposition within the underlying array too — groups only filter it, they
|
||||
// don't sort by position, so without this the item stays wherever it
|
||||
// physically sat in the array until the next full refetch reorders it.
|
||||
const globalIdx = items.value.findIndex((i) => i.id === item.id)
|
||||
items.value.splice(globalIdx, 1)
|
||||
const anchor = next ?? prev
|
||||
if (anchor) {
|
||||
const anchorIdx = items.value.findIndex((i) => i.id === anchor.id)
|
||||
items.value.splice(next ? anchorIdx : anchorIdx + 1, 0, item)
|
||||
} else {
|
||||
items.value.push(item)
|
||||
}
|
||||
|
||||
fetch(`/api/lists/${listId.value}/items/${item.id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -412,7 +466,7 @@ onMounted(init)
|
||||
<main v-else>
|
||||
<h1>skeps</h1>
|
||||
|
||||
<form @submit.prevent="addItem">
|
||||
<form class="add-item-bar" @submit.prevent="addItem">
|
||||
<input v-model="newName" type="text" placeholder="Add an item" />
|
||||
<span class="dot" :style="{ background: newItemSublistColor }"></span>
|
||||
<select class="item-sublist" v-model="newItemSublistId">
|
||||
@@ -444,7 +498,7 @@ onMounted(init)
|
||||
<div
|
||||
class="swipe-content"
|
||||
:class="{ done: item.done }"
|
||||
@pointerdown="onSwipeStart"
|
||||
@pointerdown="onSwipeStart($event, item)"
|
||||
@pointermove="onSwipeMove"
|
||||
@pointerup="onSwipeEnd($event, item)"
|
||||
@pointercancel="onSwipeEnd($event, item)"
|
||||
@@ -457,10 +511,21 @@ onMounted(init)
|
||||
@pointercancel.stop="onHandleEnd"
|
||||
>⠿</span
|
||||
>
|
||||
<label>
|
||||
<label v-if="editingId !== item.id">
|
||||
<input type="checkbox" :checked="item.done" @change="toggleDone(item)" />
|
||||
{{ item.name }} <span class="qty">x{{ item.quantity }}</span>
|
||||
{{ item.name }}
|
||||
</label>
|
||||
<input
|
||||
v-else
|
||||
class="edit-name"
|
||||
v-model="editName"
|
||||
type="text"
|
||||
autofocus
|
||||
@pointerdown.stop
|
||||
@keydown.enter="saveEdit(item)"
|
||||
@keydown.esc="cancelEdit"
|
||||
@blur="saveEdit(item)"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -635,9 +700,21 @@ body {
|
||||
main {
|
||||
max-width: 480px;
|
||||
margin: 2rem auto;
|
||||
padding: env(safe-area-inset-top) 1rem env(safe-area-inset-bottom);
|
||||
padding: calc(env(safe-area-inset-top) + 4.25rem) 1rem env(safe-area-inset-bottom);
|
||||
font-family: sans-serif;
|
||||
}
|
||||
.add-item-bar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 20;
|
||||
max-width: 480px;
|
||||
margin: 0 auto;
|
||||
padding: calc(env(safe-area-inset-top) + 0.75rem) 1rem 0.75rem;
|
||||
background: var(--bg);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.theme-picker {
|
||||
display: block;
|
||||
font-size: 16px;
|
||||
@@ -754,6 +831,10 @@ ul {
|
||||
text-decoration: line-through;
|
||||
color: var(--muted);
|
||||
}
|
||||
.edit-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.handle {
|
||||
touch-action: none;
|
||||
cursor: grab;
|
||||
@@ -791,9 +872,6 @@ ul {
|
||||
.add-sublist input[type='color']::-moz-color-swatch {
|
||||
border: none;
|
||||
}
|
||||
.qty {
|
||||
color: var(--muted);
|
||||
}
|
||||
.settings {
|
||||
margin-top: 2rem;
|
||||
color: var(--muted);
|
||||
|
||||
Reference in New Issue
Block a user