add: iteration 1 finished

Finished simple calculator iteration
This commit is contained in:
Fiona Lena Urban 2025-04-07 18:52:48 +02:00
parent e4ff2ba229
commit 85e6035a9a
12 changed files with 293 additions and 34 deletions

View file

@ -4,6 +4,7 @@
--color: var(--color-white);
--background-hover: var(--color-main-dark);
position: relative;
display: flex;
justify-content: center;
align-items: center;

View file

@ -0,0 +1,32 @@
.ButtonGroup {
display: flex;
& button {
--color: var(--color-white-transparent);
--background: var(--color-main);
all: unset;
display: flex;
align-items: center;
justify-content: center;
gap: .5rem;
padding: .5rem;
flex-grow: 1;
background: var(--background);
color: var(--color);
cursor: pointer;
transition: var(--transition-default);
&.active {
--color: var(--color-white);
--background: var(--color-main-dark);
}
&:first-child {
border-radius: var(--radius-default) 0 0 var(--radius-default);
}
&:last-child {
border-radius: 0 var(--radius-default) var(--radius-default) 0;
}
}
}

View file

@ -0,0 +1,40 @@
.Footer {
background: var(--color-main-dark);
padding: 1rem;
& h4 {
color: var(--color-white);
text-align: center;
margin-bottom: 1rem;
}
& .bottom {
display: flex;
gap: 1rem;
justify-content: space-between;
color: var(--color-white-transparent);
}
& .socials {
font-size: 1.5rem;
justify-content: center;
margin-bottom: 2rem;
}
& .data-links {
justify-content: flex-end;
font-size: .8rem;
}
& ul {
list-style: none;
display: flex;
align-items: center;
gap: 1rem;
& a {
color: var(--color-white);
text-decoration: none;
}
}
}

View file

@ -4,16 +4,21 @@
--transition-default: 150ms;
--color-white: white;
--color-white-transparent: rgba(255, 255, 255, 0.67);
--color-red: #cc0001;
--color-blue-light: #61a7fd;
--color-blue: #2e86de;
--color-blue-dark: #1b4b7f;
--color-grey: #c7c7c7;
--color-orange: #DE9C2F;
--color-main: var(--color-blue);
--color-main-light: var(--color-blue-light);
--color-main-dark: var(--color-blue-dark);
--color-accent: var(--color-orange);
--box-shadow-upper: 0 -3px 6px rgba(0,0,0,0.16), 0 -3px 6px rgba(0,0,0,0.23);
--box-shadow-z2: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
--box-shadow-inset: inset 0 3px 6px rgba(0,0,0,0.16), inset 0 3px 6px rgba(0,0,0,0.23);
@ -32,6 +37,43 @@ body {
font-family: sans-serif;
}
.dot {
--size: 10px;
width: var(--size);
height: var(--size);
top: 5px;
right: 25%;
border-radius: 50%;
background-color: var(--color-accent);
display: inline-block;
margin-right: 0.5rem;
position: absolute;
box-shadow: var(--box-shadow-z2);
scale: 0;
transition: var(--transition-default);
&.visible {
scale: 1;
animation: pulse 1s infinite;
}
}
@keyframes pulse {
0% {
transform: scale(0.95);
box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.7);
}
70% {
transform: scale(1);
box-shadow: 0 0 0 10px rgba(255, 255, 255, 0);
}
100% {
transform: scale(0.95);
box-shadow: 0 0 0 0 rgba(255, 255, 255, 0);
}
}
.card {
overflow: hidden;
border-radius: var(--radius-default);
@ -44,6 +86,10 @@ body {
min-height: 100dvh;
}
.filter-bar {
margin-bottom: 1rem;
}
.pc-wrapper {
gap: 1rem;
margin-bottom: 1rem;

View file

@ -0,0 +1,25 @@
<template>
<div class="ButtonGroup">
<button
v-for="(button, index) in buttons"
@click="click(index)"
:class="{ 'active': button.active}"
>
<Icon :name="button.icon" mode="svg" />
<span>{{ button.label }}</span>
</button>
</div>
</template>
<script setup lang="ts">
import type { Button } from '../../../shared/ButtonGroup'
type Props = {
buttons: Button[]
}
defineProps<Props>()
const emit = defineEmits(['click'])
const click = (index : number) => emit('click', index)
</script>

View file

@ -1,7 +1,46 @@
<template>
<div>THIS IS A FOOTER</div>
<footer class="Footer">
<h4>Socials</h4>
<ul class="socials">
<li v-for="social in socials">
<a :href="social.href" target="_blank">
<Icon :name="social.icon" mode="svg" />
</a>
</li>
</ul>
<div class="bottom">
<small>&copy; 2025 by webfussel</small>
<ul class="data-links">
<li v-for="dataLink in dataLinks">
<NuxtLink :to="dataLink.to">
{{ dataLink.label }}
</NuxtLink>
</li>
</ul>
</div>
</footer>
</template>
<script setup lang="ts">
const socials = [
{
icon: 'simple-icons:kofi',
href: 'https://ko-fi.com/webfussel',
},
{
icon: 'simple-icons:forgejo',
href: 'https://git.webfussel.de/webfussel/propapier',
},
]
const dataLinks = [
{
label: 'Impressum',
to: '/imprint',
},
{
label: 'Datenschutz',
to: '/privacy',
}
]
</script>

View file

@ -1,11 +1,11 @@
<template>
<header class="Header">
<strong><span>Pro</span>Papier</strong>
<label for="burger_nav_toggle">
<label for="burger_nav_toggle" v-if="available">
<Icon name="solar:hamburger-menu-broken" size="2em" />
</label>
<input type="checkbox" id="burger_nav_toggle" />
<nav class="flex-col">
<input type="checkbox" id="burger_nav_toggle" v-if="available" />
<nav class="flex-col" v-if="available">
<label for="burger_nav_toggle">
<Icon name="solar:close-circle-broken" />
</label>
@ -18,5 +18,5 @@
</template>
<script setup lang="ts">
const available = false
</script>

View file

@ -53,7 +53,7 @@
<Icon name="uil:trash" mode="svg" />
Entfernen
</PpButton>
<PpButton class="cta white">
<PpButton v-if="false" class="cta white">
<Icon name="uil:qrcode-scan" mode="svg" />
Scan
</PpButton>
@ -72,46 +72,44 @@ type Props = {
const { card } = defineProps<Props>()
const emit = defineEmits(['remove', 'update'])
onMounted(() => {
calculate()
if (card.price) folded.value = true
})
const root = ref<HTMLElement>()
const folded = ref<boolean>(false)
const deleting = ref<boolean>(false)
const ppr = ref(0)
const pps = ref(0)
const ppl = ref(0)
const ppr = ref(card.ppr)
const pps = ref(card.pps)
const ppl = ref(card.ppl)
const intl = Intl.NumberFormat('de-DE', {
style: 'currency',
currency: 'EUR',
})
const update = () => {
emit('update', card)
}
const calculate = () => {
if (!card.price || !card.roles) return
ppr.value = card.price / card.roles
if (!card.sheets) return
if (!card.sheets) {
update()
return
}
pps.value = (ppr.value / card.sheets) * 10
if(!card.layers) return
if(!card.layers) {
update()
return
}
ppl.value = (pps.value / card.layers) * 10
update()
}
const deleteCard = async () => {
root.value?.addEventListener('transitionend', () => {
emit('remove')
})
const update = () => emit('update', { ...card, ppr: ppr.value, pps: pps.value, ppl: ppl.value })
const deleteCard = async () => {
root.value?.addEventListener('transitionend', () => emit('remove'))
deleting.value = true
}
onMounted(() => folded.value = !!card.price)
</script>

View file

@ -1,17 +1,32 @@
<template>
<section class="content flex-col">
<div class="pc-wrapper flex-col">
<aside class="filter-bar">
<PpButtonGroup
:buttons="filterButtons"
@click="sort"
/>
</aside>
<div class="pc-wrapper flex-col" role="list">
<PpPriceCard
v-for="(card, index) in cards"
:key="card.uuid"
:deletable="cards.length > 1"
:card="card"
@update="updateCard(card, index)"
@update="newCard => updateCard(newCard, index)"
@remove="removeCard(card)"
/>
</div>
</section>
<PpToolbar>
<PpButton class="mini-button text-white" @click="sort(currentSort)">
<Icon class="icon" name="uil:refresh" mode="svg" />
<span>Neu sortieren</span>
<span
class="dot"
:class="{ visible : isDirty}"
aria-hidden="true"
/>
</PpButton>
<PpButton class="mini-button text-white" @click="addCard">
<Icon class="icon" name="uil:plus" mode="svg" />
<span>Hinzufügen</span>
@ -21,42 +36,95 @@
<script setup lang="ts">
import type { Card } from '../../shared/Card'
import type { Button } from '../../shared/ButtonGroup'
const createCard = (uuid : string) => ({
const currentSort = ref(0)
const isDirty = ref(false)
const createCard = (uuid : string) : Card => ({
uuid,
name: '',
price: 0,
roles: 0,
sheets: 0,
layers: 0,
ppr: 0,
pps: 0,
ppl: 0,
})
const cards = useState('cards', () => [
createCard(crypto.randomUUID()),
])
onMounted(() => {
const fromStorage = JSON.parse(localStorage.getItem('cards') ?? '[]')
cards.value = fromStorage.length !== 0 ? fromStorage : cards.value
})
const addCard = () => {
cards.value.push(createCard(crypto.randomUUID()))
cards.value.unshift(createCard(crypto.randomUUID()))
isDirty.value = true
}
const removeCard = (card : Card) => {
cards.value = cards.value.filter(element => element.uuid !== card.uuid)
isDirty.value = true
updateLocalStorage()
}
const updateCard = (card : Card, index : number) => {
cards.value[index] = card
isDirty.value = true
updateLocalStorage()
}
const updateLocalStorage = () => {
localStorage.setItem('cards', JSON.stringify(cards.value))
localStorage.setItem('sort', JSON.stringify(currentSort.value))
}
const sort = () => {}
const filterButtons = ref<Button[]>([
{
label: 'Rollen',
icon: 'uil:toilet-paper',
},
{
label: 'Blatt',
icon: 'uil:file-landscape',
},
{
label: 'Lagen',
icon: 'uil:layer-group',
},
])
const sortBy = (key : 'ppr' | 'pps' | 'ppl') => {
cards.value.sort((a : Card, b : Card) => a[key] - b[key])
}
const sort = (index : number) => {
currentSort.value = index
filterButtons.value.forEach(button => { button.active = false })
filterButtons.value[index]!.active = true
switch (index) {
case 0:
sortBy('ppr')
break
case 1:
sortBy('pps')
break
case 2:
sortBy('ppl')
break
}
updateLocalStorage()
isDirty.value = false
}
onMounted(() => {
const cardsFromStorage = JSON.parse(localStorage.getItem('cards') ?? '[]')
cards.value = cardsFromStorage.length !== 0 ? cardsFromStorage : cards.value
const sortFromStorage = +JSON.parse(localStorage.getItem('sort') ?? '0')
sort(sortFromStorage)
filterButtons.value[sortFromStorage]!.active = true
})
</script>

View file

@ -13,7 +13,9 @@ export default defineNuxtConfig({
css : [
'./app/assets/styles/general.css',
'./app/assets/styles/header.css',
'./app/assets/styles/footer.css',
'./app/assets/styles/button.css',
'./app/assets/styles/buttonGroup.css',
'./app/assets/styles/priceCard.css',
'./app/assets/styles/formInput.css',
'./app/assets/styles/toolbar.css',

5
shared/ButtonGroup.ts Normal file
View file

@ -0,0 +1,5 @@
export type Button = {
label : string
icon : string
active ?: boolean
}

View file

@ -5,4 +5,7 @@ export type Card = {
roles: number
sheets: number
layers: number
ppr: number
pps: number
ppl: number
}