add: only delete swipe, edit on tap

Edit price cards on tap, only delete swipe exists, fixed dialogs, add ripple
This commit is contained in:
Fiona Lena Urban 2025-05-12 14:56:05 +02:00
parent 1a5dd102e0
commit 38cd37cf74
9 changed files with 145 additions and 93 deletions

View file

@ -0,0 +1,71 @@
dialog {
top: 50%;
left: 50%;
width: 100vw;
translate: -50% -50%;
border: none;
border-radius: var(--radius-default);
background: var(--color-lightest);
font-size: 1rem;
color: var(--color-darkest);
position: relative;
opacity: 0;
scale: 0;
transition:
opacity var(--transition-default) ease-out,
scale var(--transition-default) ease-out,
overlay var(--transition-default) ease-out allow-discrete,
display var(--transition-default) ease-out allow-discrete;
&[open] {
opacity: 1;
scale: 1;
&::backdrop {
background-color: rgb(0 0 0 / 25%);
}
}
&::backdrop {
z-index: 2000;
background-color: rgb(0 0 0 / 0%);
transition:
display var(--transition-default) allow-discrete,
overlay var(--transition-default) allow-discrete,
background-color var(--transition-default);
}
& > .wrapper {
display: flex;
flex-direction: column;
gap: 1rem;
}
& header {
justify-content: space-between;
align-items: center;
padding: var(--padding-default) var(--padding-default) 0;
}
main {
padding: 0 var(--padding-default);
}
& footer {
justify-content: space-between;
padding: 0 var(--padding-default) var(--padding-default);
}
}
@starting-style {
dialog[open] {
opacity: 0;
scale: 0;
&::backdrop {
background-color: rgb(0 0 0 / 0%);
}
}
}

View file

@ -171,41 +171,4 @@ body {
.grow { .grow {
flex-grow: 1; flex-grow: 1;
height: 10px; height: 10px;
}
dialog {
top: 50%;
left: 50%;
width: 100vw;
transform: translate(-50%, -50%);
border: none;
border-radius: var(--radius-default);
background: var(--color-lightest);
font-size: 1rem;
color: var(--color-darkest);
& > .wrapper {
display: flex;
flex-direction: column;
gap: 1rem;
}
& header {
justify-content: space-between;
align-items: center;
padding: var(--padding-default) var(--padding-default) 0;
}
main {
padding: 0 var(--padding-default);
}
& footer {
justify-content: space-between;
padding: 0 var(--padding-default) var(--padding-default);
}
&::backdrop {
background: rgba(0, 0, 0, 0.5);
}
} }

View file

@ -4,7 +4,7 @@
background: var(--color-main-darkest); background: var(--color-main-darkest);
position: sticky; position: sticky;
bottom: 0; bottom: 0;
z-index: 100; z-index: 1000;
box-shadow: var(--box-shadow-upper); box-shadow: var(--box-shadow-upper);
& > .Button { & > .Button {

View file

@ -1,9 +1,9 @@
<template> <template>
<dialog <dialog
ref="dialog" ref="dialog"
closedby="any" closedby="none"
> >
<form method="dialog" class="wrapper"> <form method="dialog" class="wrapper" ref="wrapper">
<header class="flex-row"> <header class="flex-row">
Wirklich löschen? Wirklich löschen?
<PpButton class="round text"> <PpButton class="round text">
@ -35,5 +35,11 @@ type Props = {
defineProps<Props>() defineProps<Props>()
defineEmits(['delete']) defineEmits(['delete'])
const dialog = useTemplateRef<HTMLDialogElement>('dialog')
const wrapper = useTemplateRef<HTMLElement>('wrapper')
onMounted(() => {
onClickOutside(wrapper, () => dialog.value?.close())
})
</script> </script>

View file

@ -1,5 +1,5 @@
<template> <template>
<article class="PriceCard roboto-condensed"> <article class="PriceCard roboto-condensed" v-ripple="{ color: 'rgba(0, 0, 0, 0.1)' }">
<div class="bottom"> <div class="bottom">
<div class="bg-edit"> <div class="bg-edit">
<Icon class="icon" name="uil:pen" mode="svg" /> <Icon class="icon" name="uil:pen" mode="svg" />
@ -13,6 +13,7 @@
class="top flex-col" class="top flex-col"
:class="{ 'animated' : !isSwiping }" :class="{ 'animated' : !isSwiping }"
:style="{ left }" :style="{ left }"
@click="update"
> >
<header> <header>
<div class="name-price"> <div class="name-price">
@ -20,10 +21,10 @@
<span>{{ intl.format(+replaceComma(card.price))}}</span> <span>{{ intl.format(+replaceComma(card.price))}}</span>
</div> </div>
<div v-if="$device.isDesktop" class="flex-row gap-default"> <div v-if="$device.isDesktop" class="flex-row gap-default">
<PpButton class="icon-button" @click="update()"> <PpButton class="icon-button" @click="update">
<Icon class="icon" name="uil:pen" mode="svg" /> <Icon class="icon" name="uil:pen" mode="svg" />
</PpButton> </PpButton>
<PpButton class="icon-button" @click="deleteCard()"> <PpButton class="icon-button" @click="deleteCard">
<Icon class="icon" name="uil:trash-alt" mode="svg" /> <Icon class="icon" name="uil:trash-alt" mode="svg" />
</PpButton> </PpButton>
</div> </div>
@ -78,14 +79,13 @@ const intl = Intl.NumberFormat('de-DE', {
const { lengthX, direction, isSwiping } = useSwipe(top, { const { lengthX, direction, isSwiping } = useSwipe(top, {
passive: true, passive: true,
threshold: 30, threshold: 20,
onSwipe() { onSwipe() {
if (['down', 'up'].includes(direction.value)) return if (['down', 'up'].includes(direction.value)) return
left.value = `${-clamp(lengthX.value, -100, 100)}px` left.value = `${-clamp(lengthX.value, 0, 100)}px`
}, },
onSwipeEnd() { onSwipeEnd() {
if (['down', 'up'].includes(direction.value)) return if (['down', 'up'].includes(direction.value)) return
if (lengthX.value < -50) update()
if (lengthX.value > 50) deleteCard() if (lengthX.value > 50) deleteCard()
vibrate(100) vibrate(100)
left.value = '0' left.value = '0'
@ -98,7 +98,6 @@ const pps = computed(() => (ppr.value / +card.sheets) * 100)
const ppl = computed(() => (pps.value / +card.layers) * 10) const ppl = computed(() => (pps.value / +card.layers) * 10)
const update = () => emit('update') const update = () => emit('update')
const deleteCard = () => emit('remove') const deleteCard = () => emit('remove')
defineExpose({ defineExpose({

View file

@ -1,6 +1,6 @@
<template> <template>
<dialog ref="dialog" closedby="any"> <dialog ref="dialog" closedby="none">
<div class="wrapper"> <div class="wrapper" ref="wrapper">
<form method="dialog"> <form method="dialog">
<header class="flex-row"> <header class="flex-row">
{{ currentCardIndex > -1 ? 'Bearbeiten' : 'Neues hinzufügen' }} {{ currentCardIndex > -1 ? 'Bearbeiten' : 'Neues hinzufügen' }}
@ -81,38 +81,32 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { Card } from '../../../shared/Card'; import type { Card } from '../../../shared/Card'
type Props = { type Props = {
currentCardIndex: number; currentCardIndex: number
currentCard?: Card; currentCard?: Card
}; }
const { currentCardIndex, currentCard } = defineProps<Props>(); const { currentCardIndex, currentCard } = defineProps<Props>()
const emit = defineEmits(['update']); const emit = defineEmits(['update'])
const dialog = useTemplateRef<HTMLDialogElement>("dialog"); const dialog = useTemplateRef<HTMLDialogElement>('dialog')
const wrapper = useTemplateRef<HTMLElement>('wrapper')
const checkPrice = () => { const checkPrice = () => {
if (!currentCard) { if (!currentCard) return false
return false; if (currentCard.price.length === 0) return false
}
if (currentCard.price.length === 0) { const price = +replaceComma(currentCard.price)
return false; return !isNaN(price)
} }
const price = +replaceComma(currentCard.price);
return !isNaN(price);
};
const checkIfInteger = (toBeNumber: string) => { const checkIfInteger = (toBeNumber: string) => {
if (toBeNumber.length === 0) { if (toBeNumber.length === 0) return false
return false; if (toBeNumber.includes(',') || toBeNumber.includes(',')) return false
} return !isNaN(+toBeNumber)
if (toBeNumber.includes(",") || toBeNumber.includes(".")) { }
return false;
}
return !isNaN(+toBeNumber);
};
const validFields = reactive({ const validFields = reactive({
name: true, name: true,
@ -120,32 +114,32 @@ const validFields = reactive({
roles: true, roles: true,
sheets: true, sheets: true,
layers: true, layers: true,
}); })
const validate = () => { const validate = () => {
if (!currentCard) { if (!currentCard) return
return;
}
validFields.name = currentCard.name.length > 0; validFields.name = currentCard.name.length > 0
validFields.price = checkPrice(); validFields.price = checkPrice()
validFields.roles = checkIfInteger(currentCard.roles); validFields.roles = checkIfInteger(currentCard.roles)
validFields.sheets = checkIfInteger(currentCard.sheets); validFields.sheets = checkIfInteger(currentCard.sheets)
validFields.layers = checkIfInteger(currentCard.layers); validFields.layers = checkIfInteger(currentCard.layers)
if (Object.values(validFields).every((value) => value)) { if (Object.values(validFields).every((value) => value)) {
emit('update'); emit('update')
dialog.value?.close(); dialog.value?.close()
} }
}; }
onMounted(() => { onMounted(() => {
dialog.value?.addEventListener('close', () => { dialog.value?.addEventListener('close', () => {
validFields.name = true; validFields.name = true
validFields.price = true; validFields.price = true
validFields.roles = true; validFields.roles = true
validFields.sheets = true; validFields.sheets = true
validFields.layers = true; validFields.layers = true
}); })
});
onClickOutside(wrapper, () => dialog.value?.close())
})
</script> </script>

View file

@ -36,7 +36,14 @@ export default defineNuxtConfig({
'/privacy': { prerender: true }, '/privacy': { prerender: true },
}, },
modules: ['@nuxt/icon', '@vueuse/nuxt', '@nuxtjs/device', '@nuxt/fonts', 'nuxt-seo-utils'], modules: [
'@nuxt/icon',
'@vueuse/nuxt',
'@nuxtjs/device',
'@nuxt/fonts',
'nuxt-seo-utils',
'nuxt-ripple',
],
css : [ css : [
'./app/assets/styles/general.css', './app/assets/styles/general.css',
@ -49,6 +56,7 @@ export default defineNuxtConfig({
'./app/assets/styles/form/search.css', './app/assets/styles/form/search.css',
'./app/assets/styles/toolbar.css', './app/assets/styles/toolbar.css',
'./app/assets/styles/page.css', './app/assets/styles/page.css',
'./app/assets/styles/dialog.css',
], ],
site: { site: {

10
package-lock.json generated
View file

@ -14,6 +14,7 @@
"@nuxtjs/device": "^3.2.4", "@nuxtjs/device": "^3.2.4",
"@vueuse/nuxt": "^13.1.0", "@vueuse/nuxt": "^13.1.0",
"nuxt": "^3.16.2", "nuxt": "^3.16.2",
"nuxt-ripple": "^0.0.8",
"nuxt-seo-utils": "^7.0.11", "nuxt-seo-utils": "^7.0.11",
"vue": "latest", "vue": "latest",
"vue-router": "latest" "vue-router": "latest"
@ -9161,6 +9162,15 @@
} }
} }
}, },
"node_modules/nuxt-ripple": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/nuxt-ripple/-/nuxt-ripple-0.0.8.tgz",
"integrity": "sha512-ellqn8+OXIS4SffFhIqZHSdTNQglAwGJ7B8R2ISR4+BauP360E7cfK6vr1FGqd40HyoeSLV03Z142bTey25hCg==",
"license": "MIT",
"dependencies": {
"@nuxt/kit": "^3.14.1592"
}
},
"node_modules/nuxt-seo-utils": { "node_modules/nuxt-seo-utils": {
"version": "7.0.11", "version": "7.0.11",
"resolved": "https://registry.npmjs.org/nuxt-seo-utils/-/nuxt-seo-utils-7.0.11.tgz", "resolved": "https://registry.npmjs.org/nuxt-seo-utils/-/nuxt-seo-utils-7.0.11.tgz",

View file

@ -19,6 +19,7 @@
"@nuxtjs/device": "^3.2.4", "@nuxtjs/device": "^3.2.4",
"@vueuse/nuxt": "^13.1.0", "@vueuse/nuxt": "^13.1.0",
"nuxt": "^3.16.2", "nuxt": "^3.16.2",
"nuxt-ripple": "^0.0.8",
"nuxt-seo-utils": "^7.0.11", "nuxt-seo-utils": "^7.0.11",
"vue": "latest", "vue": "latest",
"vue-router": "latest" "vue-router": "latest"