Merge pull request 'fix: normalized and centralized sizes and padding & landingpage structure' (#5) from landingpage into main

Reviewed-on: #5
This commit is contained in:
Fiona Lena Urban 2025-05-22 10:01:29 +02:00
commit fb10e5b746
25 changed files with 472 additions and 389 deletions

23
app/app.vue Executable file → Normal file
View file

@ -1,11 +1,18 @@
<template> <template>
<div class="page-wrapper"> <PpNavigation />
<PpHeader /> <NuxtLayout>
<div class="page">
<NuxtPage /> <NuxtPage />
</div> </NuxtLayout>
<PpFooter />
</div>
</template> </template>
<script setup lang="ts">
</script> <style>
.page-enter-active,
.page-leave-active {
transition: all 2000ms;
}
.page-enter-from,
.page-leave-to {
opacity: 0;
filter: blur(.5rem);
}
</style>

View file

@ -1,5 +1,5 @@
.Button { .Button {
--padding: .2rem; --padding: var(--padding-xs);
--background: var(--color-main-dark); --background: var(--color-main-dark);
--color: var(--color-lightest); --color: var(--color-lightest);
--background-hover: var(--color-main-dark); --background-hover: var(--color-main-dark);
@ -8,7 +8,7 @@
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
gap: 1rem; gap: var(--padding-default);
cursor: pointer; cursor: pointer;
transition: var(--transition-default); transition: var(--transition-default);
outline: none; outline: none;
@ -19,13 +19,13 @@
&.transparent { &.transparent {
--background: transparent; --background: transparent;
box-shadow: none; box-shadow: none;
padding: .5em 1.5em; padding: var(--padding-s) var(--padding-l);
border-radius: var(--radius-default); border-radius: var(--radius-default);
} }
&.raised { &.raised {
box-shadow: var(--box-shadow-z2); box-shadow: var(--box-shadow-z2);
padding: .5em 1.5em; padding: var(--padding-s) var(--padding-l);
border-radius: var(--radius-default); border-radius: var(--radius-default);
&.danger { &.danger {
@ -37,7 +37,7 @@
&.text { &.text {
--background: transparent; --background: transparent;
--color: var(--color-darkest); --color: var(--color-darkest);
padding: .5em 1.5em; padding: var(--padding-s) var(--padding-l);
border-radius: var(--radius-default); border-radius: var(--radius-default);
&:hover { &:hover {
@ -62,13 +62,13 @@
justify-content: center; justify-content: center;
align-items: center; align-items: center;
border-radius: 100%; border-radius: 100%;
padding: .5rem; padding: var(--padding-s);
} }
&.cta { &.cta {
background: var(--background); background: var(--background);
color: var(--color); color: var(--color);
padding: .5rem 1.5rem; padding: var(--padding-s) var(--padding-l);
border-radius: var(--radius-default); border-radius: var(--radius-default);
box-shadow: var(--box-shadow-z2); box-shadow: var(--box-shadow-z2);
@ -98,7 +98,7 @@
--color: var(--color-main-darkest); --color: var(--color-main-darkest);
border-radius: 100%; border-radius: 100%;
padding: var(--padding); padding: var(--padding);
font-size: 1.1em; font-size: var(--font-size-l);
&:hover { &:hover {
scale: 1.2; scale: 1.2;
@ -106,17 +106,17 @@
} }
&.mini-button { &.mini-button {
padding: .5rem 1.5rem; padding: var(--padding-s) var(--padding-l);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: .5rem; gap: var(--padding-xs);
& > .icon { & > .icon {
font-size: 1.5rem; font-size: var(--font-size-xl);
} }
& > span { & > span {
font-size: .8rem; font-size: var(--font-size-s);
} }
} }
} }

View file

@ -9,8 +9,8 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: .5rem; gap: var(--padding-xs);
padding: .5rem; padding: var(--padding-s);
flex-grow: 1; flex-grow: 1;
background: var(--background); background: var(--background);
color: var(--color); color: var(--color);

View file

@ -7,7 +7,7 @@ dialog {
border: none; border: none;
border-radius: var(--radius-default); border-radius: var(--radius-default);
background: var(--color-lightest); background: var(--color-lightest);
font-size: 1rem; font-size: var(--font-size-default);
color: var(--color-darkest); color: var(--color-darkest);
position: relative; position: relative;
@ -40,7 +40,7 @@ dialog {
& > .wrapper { & > .wrapper {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1rem; gap: var(--padding-default);
} }
& header { & header {

View file

@ -12,27 +12,27 @@
& .bottom { & .bottom {
display: flex; display: flex;
gap: 1rem; gap: var(--padding-default);
justify-content: space-between; justify-content: space-between;
color: var(--color-light); color: var(--color-light);
} }
& .socials { & .socials {
font-size: 1.5rem; font-size: var(--font-size-xl);
justify-content: center; justify-content: center;
margin-bottom: 2rem; margin-bottom: var(--padding-xl);
} }
& .data-links { & .data-links {
justify-content: flex-end; justify-content: flex-end;
font-size: .8rem; font-size: var(--font-size-s);
} }
& ul { & ul {
list-style: none; list-style: none;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 1rem; gap: var(--padding-default);
& a { & a {
color: var(--color-lightest); color: var(--color-lightest);

View file

@ -1,15 +1,15 @@
.Search { .Search {
border-radius: 9999px; border-radius: 9999px;
background: var(--color-lightest); background: var(--color-lightest);
padding: 0 1rem; padding: 0 var(--padding-default);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 1rem; gap: var(--padding-default);
& > input { & > input {
all: unset; all: unset;
flex-grow: 1; flex-grow: 1;
padding: .5rem 0; padding: var(--padding-s) 0;
} }
} }

View file

@ -1,9 +1,9 @@
.TextField { .TextField {
--border-color: var(--color-light); --border-color: var(--color-light);
--label-color: var(--color-middle); --label-color: var(--color-middle);
--label-position-top: 0.4em; --label-position-top: var(--font-size-default);
--label-font-size: 1rem; --label-position-left: 2.5rem;
--icon-color: var(--color-main-dark); --label-font-size: var(--font-size-default);
--message-color: var(--color-middle); --message-color: var(--color-middle);
position: relative; position: relative;
@ -15,15 +15,11 @@
&:focus-within, &:focus-within,
&:has(input:not(:placeholder-shown)) { &:has(input:not(:placeholder-shown)) {
--icon-color: var(--color-main-dark);
--label-color: var(--color-main-dark); --label-color: var(--color-main-dark);
--label-position-top: -11px;
--label-font-size: 0.8rem;
} }
&.error { &.error {
--label-color: var(--color-error); --label-color: var(--color-error) !important;
--icon-color: var(--color-error);
--border-color: var(--color-error); --border-color: var(--color-error);
--message-color: var(--color-error); --message-color: var(--color-error);
} }
@ -31,37 +27,46 @@
& .wrapper { & .wrapper {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 0.1rem 0.5rem; padding: var(--padding-xxs) var(--padding-xs);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: var(--radius-default); border-radius: var(--radius-default);
} }
& label { & label {
position: absolute; position: absolute;
background: var(--color-lightest); display: flex;
padding: 0.2rem; align-items: center;
left: 1.8rem;
color: var(--label-color); color: var(--label-color);
top: var(--label-position-top); gap: 2px;
font-size: var(--label-font-size); top: -6px;
left: calc(var(--padding-xs) - 2px);
font-size: var(--font-size-s);
transition: var(--transition-default); transition: var(--transition-default);
& > * {
background: var(--color-lightest);
padding: 0 2px;
}
} }
& .icon { & .icon {
color: var(--icon-color); position: relative;
color: var(--label-color);
font-size: var(--font-size-default);
top: -1px;
} }
& input { & input {
all: unset; all: unset;
padding: var(--padding-small); padding: var(--padding-xxs) var(--padding-s) var(--padding-xxs) 0;
font-size: 1rem; font-size: var(--font-size-s);
width: 100%; width: 100%;
flex: 25% 1 0; flex: 25% 1 0;
color: var(--color-darkest); color: var(--color-darkest);
} }
& span { & > span {
color: var(--message-color); color: var(--message-color);
font-size: 0.6rem; font-size: var(--font-size-xs);
} }
} }

View file

@ -1,14 +1,7 @@
:root { :root {
--padding-default: 1rem;
--padding-small: 0.5rem;
--padding-large: 2rem;
--radius-default: 3px;
--radius-border: 15px;
--transition-default: 150ms;
--color-success: #328104; --color-success: #328104;
--color-error: #a20606; --color-error: #a20606;
--color-blue-light: #0ddce7; --color-blue-light: #d7e1f1;
--color-blue: #05b0ff; --color-blue: #05b0ff;
--color-blue-dark: #0266f2; --color-blue-dark: #0266f2;
--color-blue-darkest: #013174; --color-blue-darkest: #013174;
@ -26,6 +19,7 @@
--color-green-darkest-most: #157c2a; --color-green-darkest-most: #157c2a;
--color-main: var(--color-blue); --color-main: var(--color-blue);
--color-main-lightest: var(--color-blue-light);
--color-main-light: var(--color-blue-light); --color-main-light: var(--color-blue-light);
--color-main-dark: var(--color-blue-dark); --color-main-dark: var(--color-blue-dark);
--color-main-darkest: var(--color-blue-darkest); --color-main-darkest: var(--color-blue-darkest);
@ -35,6 +29,9 @@
--color-accent-dark: var(--color-green-dark); --color-accent-dark: var(--color-green-dark);
--color-accent-darkest: var(--color-green-darkest); --color-accent-darkest: var(--color-green-darkest);
--color-text: var(--color-darkest);
--color-text-invert: var(--color-lightest);
--color-gradient-main: linear-gradient(to bottom right, var(--color-main), var(--color-main-light)); --color-gradient-main: linear-gradient(to bottom right, var(--color-main), var(--color-main-light));
--color-gradient-main-dark: linear-gradient(to bottom right, var(--color-main-darkest), var(--color-main-dark)); --color-gradient-main-dark: linear-gradient(to bottom right, var(--color-main-darkest), var(--color-main-dark));
--color-gradient-accent: linear-gradient(to bottom right, var(--color-accent), var(--color-accent-light)); --color-gradient-accent: linear-gradient(to bottom right, var(--color-accent), var(--color-accent-light));
@ -45,6 +42,29 @@
--box-shadow-upper: 0 -3px 6px rgba(0, 0, 0, 0.16), 0 -3px 6px rgba(0, 0, 0, 0.23); --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-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); --box-shadow-inset: inset 0 3px 6px rgba(0, 0, 0, 0.16), inset 0 3px 6px rgba(0, 0, 0, 0.23);
/* Font Sizes & Scaling Factor*/
--scaling-factor: 1.25;
--font-size-xs: calc(var(--font-size-s) / var(--scaling-factor));
--font-size-s: calc(var(--font-size-default) / var(--scaling-factor));
--font-size-default: 1rem;
--font-size-l: calc(var(--font-size-default) * var(--scaling-factor));
--font-size-xl: calc(var(--font-size-l) * var(--scaling-factor));
--font-size-xxl: calc(var(--font-size-xl) * var(--scaling-factor));
/* Paddings depend on Font-Size */
--padding-xxs: calc(var(--padding-xs) / var(--scaling-factor));
--padding-xs: calc(var(--padding-s) / var(--scaling-factor));
--padding-default: var(--font-size-default);
--padding-s: calc(var(--padding-default) / var(--scaling-factor));
--padding-l: calc(var(--padding-default) * var(--scaling-factor));
--padding-xl: calc(var(--padding-l) * var(--scaling-factor));
--padding-xxl: calc(var(--padding-xl) * var(--scaling-factor));
--radius-default: calc(var(--font-size-default) / 3);
--radius-border: var(--font-size-default);
--transition-default: 150ms;
} }
* { * {
@ -59,43 +79,29 @@ body {
overflow-x: hidden; overflow-x: hidden;
font-family: sans-serif; font-family: sans-serif;
background: var(--color-main-darkest); background: var(--color-main-darkest);
color: var(--color-text);
font-size: var(--font-size-default);
} }
.dot { h1, h2, h3 {
--size: 10px; margin: var(--padding-xl) 0 var(--padding-default);
width: var(--size); text-wrap: balance;
height: var(--size); hyphens: auto;
top: 5px; font-size: var(--font-size-xl);
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 { h2 {
0% { margin: var(--padding-l) 0 var(--padding-default);
transform: scale(0.95); font-size: var(--font-size-l);
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% { h3 {
transform: scale(0.95); margin: var(--padding-default) 0 var(--padding-default);
box-shadow: 0 0 0 0 rgba(255, 255, 255, 0); font-size: var(--font-size-default);
} }
a:has(button) {
text-decoration: none;
} }
.card { .card {
@ -115,7 +121,7 @@ body {
} }
.text-white { .text-white {
color: var(--color-lightest); color: var(--color-text-invert);
} }
.bg-main { .bg-main {
@ -130,12 +136,16 @@ body {
background-color: var(--color-main-dark); background-color: var(--color-main-dark);
} }
.bg-main-darkest {
background-color: var(--color-main-darkest);
}
.bg-main-dark-hover:hover { .bg-main-dark-hover:hover {
background-color: var(--color-main-dark); background-color: var(--color-main-dark);
} }
.gap-default { .gap-default {
gap: 1rem; gap: var(--padding-default);
} }
.bg-blue { .bg-blue {
@ -147,13 +157,13 @@ body {
} }
.padding { .padding {
gap: 1rem; gap: var(--padding-default);
padding: var(--padding-default); padding: var(--padding-default);
} }
.padding-small { .padding-small {
gap: 1rem; gap: var(--padding-default);
padding: var(--padding-small); padding: var(--padding-s);
} }
.roboto-condensed { .roboto-condensed {
@ -170,5 +180,5 @@ body {
.grow { .grow {
flex-grow: 1; flex-grow: 1;
height: 10px; height: var(--padding-s);
} }

View file

@ -2,20 +2,37 @@
position: sticky; position: sticky;
top: 0; top: 0;
z-index: 100; z-index: 100;
background: var(--color-main-darkest);
display: flex; display: flex;
align-items: center; align-items: center;
gap: 1rem; gap: var(--padding-default);
padding: var(--padding-default); padding: var(--padding-default);
&:not(.lp) {
background: var(--color-main-darkest);
}
&.lp {
position: absolute;
backdrop-filter: blur(10px);
mask: linear-gradient(to top, transparent, black 20%);
width: 100%;
top: 0;
}
& .logo { & .logo {
height: 40px; height: 40px;
} }
& .burger-button {
all: unset;
color: var(--color-lightest);
}
& header { & header {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
width: 100%;
font-weight: bold; font-weight: bold;
& a { & a {
@ -26,7 +43,7 @@
& .header-text { & .header-text {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 1rem; gap: var(--padding-default);
& > div { & > div {
display: flex; display: flex;
@ -34,52 +51,17 @@
} }
& .big { & .big {
font-size: 1.5em; font-size: var(--font-size-xl);
} }
& .small { & .small {
font-size: .8em; font-size: var(--font-size-s);
font-weight: 100; font-weight: 100;
} }
} }
& input[type="checkbox"] {
display: none;
}
& input[type="checkbox"]:checked + nav {
translate: 0;
}
& nav,
& ul {
gap: 1em;
}
& nav {
position: fixed;
padding: var(--padding-default);
translate: 100% 0;
width: 100vw;
right: 0;
top: 0;
height: 100dvh;
transition: 150ms ease-in-out;
background: var(--color-lightest);
font-size: 2em;
align-items: end;
z-index: 100;
}
& ul {
width: 100%;
align-items: center;
& li {
list-style: none;
}
}
} }
&:not(.lp) {
&:after, &:before { &:after, &:before {
content: ''; content: '';
display: block; display: block;
@ -100,3 +82,4 @@
mask: radial-gradient(var(--radius-border) at 100% 100%,#0000 98%,#000); mask: radial-gradient(var(--radius-border) at 100% 100%,#0000 98%,#000);
} }
} }
}

View file

@ -0,0 +1,30 @@
.home-hero {
background-image: linear-gradient(rgba(0,0,0,0.4), rgba(0,0,0,0.4)), url("/img/hero-image.webp"); /* single color gradient for dark layer over image */
background-repeat: no-repeat;
background-position: center center;
background-size: cover;
color: var(--color-text-invert);
text-align: center;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
button {
margin: var(--padding-xxl) auto 0;
}
.text {
padding: var(--padding-default);
}
h1 {
font-size: var(--font-size-xxl);
}
}
.home-text {
padding: var(--padding-xxl) var(--padding-default);
text-align: center;
}

View file

@ -0,0 +1,69 @@
.Navigation {
position: fixed;
display: flex;
justify-content: flex-end;
align-items: center;
z-index: 5000;
width: 100vw;
height: 100dvh;
right: 0;
top: 0;
transition: 150ms ease-in-out;
pointer-events: none;
&.open {
pointer-events: all;
background: rgba(0, 0, 0, .5);
& nav {
translate: -1rem 0;
}
}
nav {
background: var(--color-lightest);
align-items: end;
box-shadow: var(--box-shadow-z2);
padding: var(--padding-default);
height: calc(100% - var(--padding-xxl));
width: 70%;
translate: 100% 0;
transition: 150ms ease-in-out;
border-radius: var(--radius-default);
}
& button {
justify-self: flex-end;
font-size: var(--font-size-xl);
}
& ul {
width: 100%;
align-items: flex-start;
font-size: var(--font-size-l);
gap: var(--padding-default);
& li {
list-style: none;
width: 100%;
& a {
text-decoration: none;
display: flex;
align-items: center;
gap: var(--padding-default);
color: var(--color-middle);
border-radius: var(--radius-default);
padding: var(--padding-xs) var(--padding-s);
transition: var(--transition-default);
background: transparent;
cursor: pointer;
&.active {
background: var(--color-main-light);
color: var(--color-main-dark);
}
}
}
}
}

View file

@ -49,10 +49,18 @@
overflow: hidden; overflow: hidden;
background: var(--color-lightest); background: var(--color-lightest);
height: 100%; height: 100%;
h1:first-of-type,
h2:first-of-type,
h3:first-of-type,
p:first-of-type,
figure:first-of-type {
margin-top: 0;
}
} }
.content-text { .content-text {
padding: var(--padding-large) var(--padding-default) var(--padding-default); padding: var(--padding-xl) var(--padding-default) 0;
color: var(--color-darkest); color: var(--color-darkest);
text-align: center; text-align: center;
} }
@ -67,6 +75,6 @@
} }
.Legal { .Legal {
padding: var(--padding-large) var(--padding-default); padding: var(--padding-l) var(--padding-default);
color: var(--color-darkest); color: var(--color-darkest);
} }

View file

@ -15,7 +15,7 @@
& > * { & > * {
flex-grow: 1; flex-grow: 1;
color: var(--color-lightest); color: var(--color-lightest);
font-size: 2rem; font-size: var(--font-size-xl);
display: flex; display: flex;
align-items: center; align-items: center;
@ -26,13 +26,13 @@
& .bg-edit { & .bg-edit {
background: var(--color-main-dark); background: var(--color-main-dark);
padding: 2rem; padding: var(--padding-xl);
text-align: left; text-align: left;
} }
& .bg-delete { & .bg-delete {
background: var(--color-error); background: var(--color-error);
padding: 2rem; padding: var(--padding-xl);
text-align: right; text-align: right;
justify-content: flex-end; justify-content: flex-end;
} }
@ -42,7 +42,7 @@
position: relative; position: relative;
background: var(--color-lightest); background: var(--color-lightest);
z-index: 2; z-index: 2;
gap: 1rem; gap: var(--padding-default);
padding: var(--padding-default); padding: var(--padding-default);
&.animated { &.animated {
@ -56,14 +56,14 @@
color: var(--color-darkest); color: var(--color-darkest);
& .icon { & .icon {
font-size: 1rem; font-size: var(--font-size-default);
cursor: pointer; cursor: pointer;
} }
} }
& .name-price { & .name-price {
display: flex; display: flex;
gap: .5rem; gap: var(--padding-xs);
& > span:nth-child(1) { & > span:nth-child(1) {
font-weight: bold; font-weight: bold;
@ -75,7 +75,7 @@
& > span:nth-child(2)::before { & > span:nth-child(2)::before {
content: '•'; content: '•';
margin-right: .5rem; margin-right: var(--padding-xs);
color: var(--color-middle); color: var(--color-middle);
} }
} }
@ -84,19 +84,19 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
width: 100%; width: 100%;
gap: 1rem; gap: var(--padding-default);
justify-content: space-between; justify-content: space-between;
& > .info { & > .info {
flex-grow: 0; flex-grow: 0;
align-items: center; align-items: center;
gap: .25rem; gap: var(--padding-xxs);
font-weight: bold; font-weight: bold;
& > .price { & > .price {
display: flex; display: flex;
align-items: center; align-items: center;
gap: .5rem; gap: var(--padding-xs);
& > .icon { & > .icon {
color: var(--color-main-dark); color: var(--color-main-dark);
@ -104,7 +104,7 @@
} }
& > .pro { & > .pro {
font-size: .6rem; font-size: var(--font-size-xs);
color: var(--color-middle); color: var(--color-middle);
font-weight: lighter; font-weight: lighter;
} }

View file

@ -8,7 +8,6 @@
box-shadow: var(--box-shadow-upper); box-shadow: var(--box-shadow-upper);
& > .Button { & > .Button {
--padding: 1rem; font-size: var(--font-size-default);
font-size: 1rem;
} }
} }

View file

@ -1,9 +1,11 @@
<template> <template>
<div class="TextField"> <div class="TextField">
<div class="wrapper"> <div class="wrapper">
<input v-model="text" :type="type" :id="id" :placeholder="placeholder" @blur="emit('blur')" @input="emit('input')" :inputmode="mode" />
<label :for="id">
<Icon v-if="icon" class="icon" :name="icon" mode="svg" /> <Icon v-if="icon" class="icon" :name="icon" mode="svg" />
<input v-model="text" :type="type" :id="id" placeholder=" " @blur="emit('blur')" @input="emit('input')" :inputmode="mode" /> <span>{{ label }}</span>
<label :for="id">{{ label }}</label> </label>
</div> </div>
<span v-if="message">{{ message }}</span> <span v-if="message">{{ message }}</span>
</div> </div>
@ -11,12 +13,13 @@
<script setup lang="ts"> <script setup lang="ts">
type Props = { type Props = {
type?: 'text' | 'number'; type?: 'text' | 'number'
message?: string; message?: string
icon?: string; icon?: string
label: string; label: string
id: string; placeholder: string
mode?: 'text' | 'email' | 'search' | 'tel' | 'url' | 'none' | 'numeric' | 'decimal'; id: string
mode?: 'text' | 'email' | 'search' | 'tel' | 'url' | 'none' | 'numeric' | 'decimal'
}; };
const { type = "text", mode = "text" } = defineProps<Props>(); const { type = "text", mode = "text" } = defineProps<Props>();

View file

@ -1,6 +1,5 @@
<template> <template>
<div class="Header"> <div class="Header" :class="[type]">
<div>
<header class="roboto-condensed"> <header class="roboto-condensed">
<NuxtLink class="header-text" to="/"> <NuxtLink class="header-text" to="/">
<img class="logo" src="/img/propapier.svg" alt="ProPapier logo" /> <img class="logo" src="/img/propapier.svg" alt="ProPapier logo" />
@ -9,25 +8,21 @@
<span class="small">Vergleichen. Schnell. Unkompliziert.</span> <span class="small">Vergleichen. Schnell. Unkompliziert.</span>
</div> </div>
</NuxtLink> </NuxtLink>
<label for="burger_nav_toggle" v-if="available"> <button class="burger-button" @click="open()">
<Icon name="solar:hamburger-menu-broken" size="2em" /> <Icon name="uil:bars" size="2em" />
</label> </button>
<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>
<ul class="flex-col">
<li>Home</li>
<li>Übersicht</li>
</ul>
</nav>
</header> </header>
<div id="subheader" /> <div id="subheader" />
</div> </div>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const available = false type Props = {
type ?: 'lp'
}
defineProps<Props>()
const nav = useNavigation()
const open = () => { nav.showNavigation() }
</script> </script>

View file

@ -0,0 +1,41 @@
<template>
<section class="Navigation" :class="{ open }">
<nav>
<PpButton class="round text" @click="close()">
<Icon name="uil:times" mode="svg" />
</PpButton>
<ul class="flex-col">
<li v-for="page in pages">
<NuxtLink :to="page.route" @click="close()" active-class="active">
<Icon class="icon" :name="`uil:${page.icon}`" mode="svg" />
<span>{{ page.label }}</span>
</NuxtLink>
</li>
</ul>
</nav>
</section>
</template>
<script setup lang="ts">
const nav = useNavigation()
const close = () => nav.hideNavigation()
const open = computed(() => nav.isNavigationVisible.value)
const pages = [
{
label: 'Home',
icon: 'home',
route: '/'
},
{
label: 'Schnellrechner',
icon: 'calculator',
route: '/rechner'
},
// {
// label: 'Über uns',
// icon: 'users-alt',
// route: '/about-us'
// }
]
</script>

View file

@ -16,7 +16,8 @@
v-model="currentCard.name" v-model="currentCard.name"
id="card_name" id="card_name"
label="Name" label="Name"
icon="uil:user" icon="uil:pricetag-alt"
:placeholder="randomName"
:class="{ error: !validFields.name }" :class="{ error: !validFields.name }"
:message="!validFields.name ? 'Feld darf nicht leer sein.' : ''" :message="!validFields.name ? 'Feld darf nicht leer sein.' : ''"
@input="validFields.name = true" @input="validFields.name = true"
@ -25,6 +26,7 @@
v-model="currentCard.price" v-model="currentCard.price"
id="card_price" id="card_price"
label="Preis" label="Preis"
placeholder="2,49"
icon="uil:euro" icon="uil:euro"
mode="decimal" mode="decimal"
:class="{ error: !validFields.price }" :class="{ error: !validFields.price }"
@ -37,6 +39,7 @@
v-model="currentCard.roles" v-model="currentCard.roles"
id="card_roles" id="card_roles"
label="Rollen" label="Rollen"
placeholder="8"
icon="uil:toilet-paper" icon="uil:toilet-paper"
mode="decimal" mode="decimal"
:class="{ error: !validFields.roles }" :class="{ error: !validFields.roles }"
@ -47,6 +50,7 @@
v-model="currentCard.sheets" v-model="currentCard.sheets"
id="card_sheets" id="card_sheets"
label="Blatt" label="Blatt"
placeholder="150"
icon="uil:file-landscape" icon="uil:file-landscape"
mode="decimal" mode="decimal"
:class="{ error: !validFields.sheets }" :class="{ error: !validFields.sheets }"
@ -57,6 +61,7 @@
v-model="currentCard.layers" v-model="currentCard.layers"
id="card_layers" id="card_layers"
label="Lagen" label="Lagen"
placeholder="3"
icon="uil:layer-group" icon="uil:layer-group"
mode="decimal" mode="decimal"
:class="{ error: !validFields.layers }" :class="{ error: !validFields.layers }"
@ -94,6 +99,25 @@ const emit = defineEmits(['update'])
const dialog = useTemplateRef<HTMLDialogElement>('dialog') const dialog = useTemplateRef<HTMLDialogElement>('dialog')
const wrapper = useTemplateRef<HTMLElement>('wrapper') const wrapper = useTemplateRef<HTMLElement>('wrapper')
const market = [
'Lotl',
'Olda',
'Bäwä',
'Brutto',
]
const product = [
'Weichelig',
'Sau Rauh',
'Bissl Sanft',
'Ganz ok',
'Flauschi'
]
const generateRandomName = () => `${market[Math.floor(Math.random() * market.length)]} ${product[Math.floor(Math.random() * product.length)]}`
const randomName = useState('randomName', () => generateRandomName())
const checkPrice = () => { const checkPrice = () => {
if (!currentCard) return false if (!currentCard) return false
if (currentCard.price.length === 0) return false if (currentCard.price.length === 0) return false
@ -138,6 +162,7 @@ onMounted(() => {
validFields.roles = true validFields.roles = true
validFields.sheets = true validFields.sheets = true
validFields.layers = true validFields.layers = true
randomName.value = generateRandomName()
}) })
onClickOutside(wrapper, () => dialog.value?.close()) onClickOutside(wrapper, () => dialog.value?.close())

View file

@ -0,0 +1,24 @@
import { ref } from 'vue'
const isNavigationVisible = ref(false)
export const useNavigation = () => {
const toggleNavigation = () => {
isNavigationVisible.value = !isNavigationVisible.value
}
const showNavigation = () => {
isNavigationVisible.value = true
}
const hideNavigation = () => {
isNavigationVisible.value = false
}
return {
isNavigationVisible,
toggleNavigation,
showNavigation,
hideNavigation
}
}

11
app/layouts/default.vue Executable file
View file

@ -0,0 +1,11 @@
<template>
<div class="page-wrapper">
<PpHeader />
<div class="page">
<NuxtPage />
</div>
<PpFooter />
</div>
</template>
<script setup lang="ts">
</script>

View file

@ -0,0 +1,11 @@
<template>
<div class="page-wrapper">
<PpHeader type="lp" />
<div class="page">
<NuxtPage />
</div>
<PpFooter />
</div>
</template>
<script setup lang="ts">
</script>

159
app/pages/index.vue Executable file → Normal file
View file

@ -1,154 +1,25 @@
<template> <template>
<div class="nuxt-page-wrapper flex-col"> <section class="Home flex-col content full">
<ClientOnly> <div class="home-hero">
<PpDeleteDialog <div class="text">
ref="deleteModal" <h1>
:current-card-index="currentCardIndex" Du zahlst zuviel fürs Papier?
@delete="removeCard(currentCardIndex)" </h1>
/> <NuxtLink to="/rechner">
<PpPriceCardDialog <PpButton class="cta">Preise vergleichen</PpButton>
ref="modal" </NuxtLink>
:current-card="currentCard"
:current-card-index="currentCardIndex"
@update="updateCard()"
/>
<section class="content flex-col">
<div class="content-text">
<h1>Mit ProPapier Preise vergleichen und sparen</h1>
</div> </div>
<aside class="filter-bar">
<button v-for="(button, index) in filterButtons" @click="() => sort(index)" :class="{ 'active': button.active }">
{{ button.label }}
</button>
</aside>
<div class="flex-col" role="list" v-if="cards.length">
<PpPriceCard
ref="priceCard"
v-for="(card, index) in cards"
:key="card.uuid"
:deletable="cards.length > 1"
:card="card"
@update="openModal(false, index)"
@remove="openDeleteModal()"
/>
</div> </div>
<p class="info-text grow" v-else> <div class="home-text padding ">
Du hast noch keinerlei Einträge angelegt. <p>
<br />Aber das ist gar nicht schlimm! Mit <strong>ProPapier</strong> vergleichst du schnell & unkompliziert Preise für Klo-, Küchen- und Haushaltspapier und sparst so bares Geld.
<br />Tippe einfach unten auf "+ Hinzufügen" und leg los.
</p> </p>
</section>
<PpToolbar>
<PpButton class="mini-button text-white transparent" @click="openModal(true, -1)">
<Icon class="icon" name="uil:plus" mode="svg" />
<span>Hinzufügen</span>
</PpButton>
</PpToolbar>
</ClientOnly>
</div> </div>
</section>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { Card } from '../../shared/Card' definePageMeta({
import type { Button } from '../../shared/ButtonGroup' layout: 'landingpage'
import { PpPriceCardDialog, PpDeleteDialog, PpPriceCard } from '#components'
const cards = useLocalStorage<Card[]>('cards', [])
const currentSort = useLocalStorage<number>('sort', 0)
const currentCard = ref<Card>()
const currentCardIndex = ref<number>(-1)
const modal = useTemplateRef<typeof PpPriceCardDialog>('modal')
const deleteModal = useTemplateRef<typeof PpDeleteDialog>('deleteModal')
const priceCards = useTemplateRef<(typeof PpPriceCard)[]>('priceCard')
const createCard = (uuid : string) : Card => ({
uuid,
name: '',
price: '',
roles: '',
sheets: '',
layers: '',
}) })
const addCard = (card : Card) => {
cards.value.unshift({ ...card })
sort()
}
const removeCard = (index : number) => {
cards.value.splice(index, 1)
sort()
}
const updateCard = () => {
if (currentCardIndex.value === -1) {
addCard(currentCard.value!)
return
}
const newCard = { ...currentCard.value! }
cards.value.splice(currentCardIndex.value, 1, newCard)
sort()
}
const openModal = (createNew : boolean, index : number) => {
if (createNew) {
currentCardIndex.value = -1
currentCard.value = createCard(randomUUID())
modal.value?.$el.showModal()
return
}
currentCardIndex.value = index
currentCard.value = { ...cards.value[index]! }
modal.value?.$el.showModal()
return
}
const openDeleteModal = () => {
deleteModal.value?.$el.showModal()
}
const filterButtons = ref<Button[]>([
{
label: 'Rollen',
icon: 'uil:toilet-paper',
active: currentSort.value === 0,
},
{
label: 'Blatt',
icon: 'uil:file-landscape',
active: currentSort.value === 1,
},
{
label: 'Lagen',
icon: 'uil:layer-group',
active: currentSort.value === 2,
},
])
const sortBy = (key : 'ppr' | 'pps' | 'ppl') => {
cards.value.sort((a, b) => {
const aCard = priceCards.value?.find(card => card.uuid === a.uuid) || null
const bCard = priceCards.value?.find(card => card.uuid === b.uuid) || null
if (!aCard || !bCard) return 0
return aCard[key] - bCard[key]
})
}
const sort = async (index : number = currentSort.value) => {
currentSort.value = index
filterButtons.value.forEach(button => { button.active = false })
filterButtons.value[index]!.active = true
await nextTick()
switch (index) {
case 0: return sortBy('ppr')
case 1: return sortBy('pps')
case 2: return sortBy('ppl')
}
}
</script> </script>

41
app/pages/other.vue → app/pages/rechner.vue Normal file → Executable file
View file

@ -1,5 +1,5 @@
<template> <template>
<div> <div class="nuxt-page-wrapper flex-col">
<ClientOnly> <ClientOnly>
<PpDeleteDialog <PpDeleteDialog
ref="deleteModal" ref="deleteModal"
@ -12,20 +12,16 @@
:current-card-index="currentCardIndex" :current-card-index="currentCardIndex"
@update="updateCard()" @update="updateCard()"
/> />
<div class="search-bar">
<PpFormSearch
v-model="search"
label="Suche nach Klopapier!"
id="search_field"
/>
</div>
<section class="content flex-col"> <section class="content flex-col">
<div class="content-text">
<h1>Mit ProPapier Preise vergleichen und sparen</h1>
</div>
<aside class="filter-bar"> <aside class="filter-bar">
<button v-for="(button, index) in filterButtons" @click="() => sort(index)" :class="{ 'active': button.active }"> <button v-for="(button, index) in filterButtons" @click="() => sort(index)" :class="{ 'active': button.active }">
{{ button.label }} {{ button.label }}
</button> </button>
</aside> </aside>
<div class="flex-col" role="list"> <div class="flex-col" role="list" v-if="cards.length">
<PpPriceCard <PpPriceCard
ref="priceCard" ref="priceCard"
v-for="(card, index) in cards" v-for="(card, index) in cards"
@ -36,17 +32,13 @@
@remove="openDeleteModal()" @remove="openDeleteModal()"
/> />
</div> </div>
<p class="info-text grow" v-else>
Du hast noch keinerlei Einträge angelegt.
<br />Aber das ist gar nicht schlimm!
<br />Tippe einfach unten auf "+ Hinzufügen" und leg los.
</p>
</section> </section>
<PpToolbar> <PpToolbar>
<PpButton class="mini-button text-white transparent" @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 transparent" @click="openModal(true, -1)"> <PpButton class="mini-button text-white transparent" @click="openModal(true, -1)">
<Icon class="icon" name="uil:plus" mode="svg" /> <Icon class="icon" name="uil:plus" mode="svg" />
<span>Hinzufügen</span> <span>Hinzufügen</span>
@ -63,15 +55,12 @@ import { PpPriceCardDialog, PpDeleteDialog, PpPriceCard } from '#components'
const cards = useLocalStorage<Card[]>('cards', []) const cards = useLocalStorage<Card[]>('cards', [])
const currentSort = useLocalStorage<number>('sort', 0) const currentSort = useLocalStorage<number>('sort', 0)
const isDirty = ref(false)
const currentCard = ref<Card>() const currentCard = ref<Card>()
const currentCardIndex = ref<number>(-1) const currentCardIndex = ref<number>(-1)
const modal = useTemplateRef<typeof PpPriceCardDialog>('modal') const modal = useTemplateRef<typeof PpPriceCardDialog>('modal')
const deleteModal = useTemplateRef<typeof PpDeleteDialog>('deleteModal') const deleteModal = useTemplateRef<typeof PpDeleteDialog>('deleteModal')
const priceCards = useTemplateRef<(typeof PpPriceCard)[]>('priceCard') const priceCards = useTemplateRef<(typeof PpPriceCard)[]>('priceCard')
const search = ref('')
const createCard = (uuid : string) : Card => ({ const createCard = (uuid : string) : Card => ({
uuid, uuid,
name: '', name: '',
@ -84,12 +73,12 @@ const createCard = (uuid : string) : Card => ({
const addCard = (card : Card) => { const addCard = (card : Card) => {
cards.value.unshift({ ...card }) cards.value.unshift({ ...card })
isDirty.value = true sort()
} }
const removeCard = (index : number) => { const removeCard = (index : number) => {
cards.value.splice(index, 1) cards.value.splice(index, 1)
isDirty.value = true sort()
} }
const updateCard = () => { const updateCard = () => {
@ -100,7 +89,7 @@ const updateCard = () => {
const newCard = { ...currentCard.value! } const newCard = { ...currentCard.value! }
cards.value.splice(currentCardIndex.value, 1, newCard) cards.value.splice(currentCardIndex.value, 1, newCard)
isDirty.value = true sort()
} }
const openModal = (createNew : boolean, index : number) => { const openModal = (createNew : boolean, index : number) => {
@ -149,12 +138,12 @@ const sortBy = (key : 'ppr' | 'pps' | 'ppl') => {
}) })
} }
const sort = (index : number) => { const sort = async (index : number = currentSort.value) => {
currentSort.value = index currentSort.value = index
filterButtons.value.forEach(button => { button.active = false }) filterButtons.value.forEach(button => { button.active = false })
filterButtons.value[index]!.active = true filterButtons.value[index]!.active = true
isDirty.value = false await nextTick()
switch (index) { switch (index) {
case 0: return sortBy('ppr') case 0: return sortBy('ppr')

View file

@ -41,6 +41,7 @@ export default defineNuxtConfig({
css : [ css : [
'./app/assets/styles/general.css', './app/assets/styles/general.css',
'./app/assets/styles/header.css', './app/assets/styles/header.css',
'./app/assets/styles/navigation.css',
'./app/assets/styles/footer.css', './app/assets/styles/footer.css',
'./app/assets/styles/button.css', './app/assets/styles/button.css',
'./app/assets/styles/buttonGroup.css', './app/assets/styles/buttonGroup.css',
@ -50,6 +51,7 @@ export default defineNuxtConfig({
'./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', './app/assets/styles/dialog.css',
'./app/assets/styles/landingpage.css',
], ],
site: { site: {

BIN
public/img/hero-image.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB