FIX: mpa, nuxt4 future, improvements in intro and services
Added mpa support, nuxt4 future compatibility, improvements in intro and services
This commit is contained in:
parent
078d4bfd82
commit
9642496e5a
35 changed files with 324 additions and 172 deletions
39
app/app.vue
Normal file
39
app/app.vue
Normal file
|
@ -0,0 +1,39 @@
|
|||
<template>
|
||||
<section>
|
||||
<NuxtLayout>
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
useSeoMeta({
|
||||
title: 'webfussel | mehr Fussel im Web by Fiona Urban',
|
||||
description: 'Headless CMS, Components & APIs by Fiona Urban. Storyblok, FirstSpirit, Nuxt.',
|
||||
author: 'webfussel',
|
||||
robots: 'index, follow',
|
||||
themeColor: '#2a2723',
|
||||
ogTitle: 'webfussel | mehr Fussel im Web',
|
||||
ogDescription: 'Headless CMS, Components & APIs by Fiona Urban. Storyblok, FirstSpirit, Nuxt.',
|
||||
ogImage: '/img/og.webp',
|
||||
ogImageAlt: 'Das webfussel Logo auf einem dunklen Hintergrund',
|
||||
ogUrl: 'https://webfussel.de',
|
||||
twitterTitle: 'webfussel | mehr Fussel im Web',
|
||||
twitterDescription: 'Headless CMS, Components & APIs by Fiona Urban. Storyblok, FirstSpirit, Nuxt.',
|
||||
twitterImage: '/img/og.webp',
|
||||
twitterImageAlt: 'Das webfussel Logo auf einem dunklen Hintergrund',
|
||||
twitterUrl: 'https://webfussel.de',
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.page-enter-active,
|
||||
.page-leave-active {
|
||||
transition: all 0.4s;
|
||||
}
|
||||
.page-enter-from,
|
||||
.page-leave-to {
|
||||
opacity: 0;
|
||||
filter: blur(.5rem);
|
||||
}
|
||||
</style>
|
75
app/assets/css/button.css
Normal file
75
app/assets/css/button.css
Normal file
|
@ -0,0 +1,75 @@
|
|||
.Button {
|
||||
all: unset;
|
||||
transition: 250ms;
|
||||
cursor: pointer;
|
||||
padding: 1rem 1.5rem;
|
||||
outline: 3px solid transparent;
|
||||
box-shadow: 0 0 0 0 var(--color-orange);
|
||||
border-radius: 99999px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
|
||||
&.default {
|
||||
background: var(--color-orange);
|
||||
color: var(--color-black);
|
||||
}
|
||||
|
||||
&.white {
|
||||
background: var(--color-white);
|
||||
color: var(--color-black);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
outline-color: var(--color-black);
|
||||
box-shadow: 0 0 0 6px var(--color-orange);
|
||||
}
|
||||
|
||||
&.cta {
|
||||
font-size: clamp(1rem, 2vw, 1.5rem);
|
||||
}
|
||||
}
|
||||
|
||||
.DualButton {
|
||||
--size: 2.2rem;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
|
||||
& .divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--color-black);
|
||||
color: var(--color-white);
|
||||
border-radius: 9999px;
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
padding: var(--size);
|
||||
font-size: 1.2rem;
|
||||
z-index: 1;
|
||||
margin-left: calc(var(--size) * -1 - 25px);
|
||||
border: 2px solid var(--color-black);
|
||||
}
|
||||
|
||||
.Button {
|
||||
border: 2px solid currentColor;
|
||||
}
|
||||
|
||||
& .Button:hover {
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
background-color: var(--color-black);
|
||||
color: var(--color-white);
|
||||
border-color: var(--color-orange);
|
||||
}
|
||||
|
||||
& .Button:first-child {
|
||||
padding-right: calc(var(--size) * 2);
|
||||
}
|
||||
|
||||
& .Button:last-child {
|
||||
padding-left: calc(var(--size) * 2);
|
||||
margin-left: calc(var(--size) * -1 - 25px);
|
||||
}
|
||||
}
|
112
app/assets/css/customers.css
Normal file
112
app/assets/css/customers.css
Normal file
|
@ -0,0 +1,112 @@
|
|||
.Customers {
|
||||
& .customer-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
& img {
|
||||
height: 50px;
|
||||
|
||||
&.white {
|
||||
filter: brightness(0) invert(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& .projects-list {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
|
||||
& > article {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 0;
|
||||
flex-basis: clamp(350px, calc(33% - 3rem), 400px);
|
||||
height: 350px;
|
||||
display: grid;
|
||||
overflow: hidden;
|
||||
|
||||
& .bg {
|
||||
padding: 0;
|
||||
height: 350px;
|
||||
width: 100%;
|
||||
|
||||
& img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
& > * {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
& > div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba(0, 0, 0, 0.88);
|
||||
backdrop-filter: blur(1px);
|
||||
transition: var(--transition-time);
|
||||
height: 100%;
|
||||
|
||||
text-shadow: 0 0 5px rgba(0, 0, 0, .7);
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
|
||||
& > main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
|
||||
max-height: 6rem;
|
||||
overflow: hidden;
|
||||
transition: var(--transition-time);
|
||||
|
||||
& .customer {
|
||||
font-size: 1rem;
|
||||
color: var(--color-white-transparent);
|
||||
}
|
||||
|
||||
& .title {
|
||||
margin-top: -1rem;
|
||||
}
|
||||
|
||||
& ul {
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
& a {
|
||||
color: var(--color-orange);
|
||||
|
||||
&:hover {
|
||||
color: var(--color-white);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
backdrop-filter: blur(5px);
|
||||
|
||||
& > main {
|
||||
max-height: 25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 780px) {
|
||||
.Customers {
|
||||
& .customer-list {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
24
app/assets/css/fonts.css
Normal file
24
app/assets/css/fonts.css
Normal file
|
@ -0,0 +1,24 @@
|
|||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-stretch: 100%;
|
||||
font-display: swap;
|
||||
src: url('/fonts/opensans.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Roboto Condensed';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url('/fonts/roboto_con_reg.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Roboto Condensed';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('/fonts/roboto_con_bold.woff2') format('woff2');
|
||||
}
|
27
app/assets/css/footer.css
Normal file
27
app/assets/css/footer.css
Normal file
|
@ -0,0 +1,27 @@
|
|||
.Footer {
|
||||
align-items: center;
|
||||
padding: 1rem 15vw;
|
||||
|
||||
& .notes {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
gap: 3rem .5rem;
|
||||
}
|
||||
|
||||
& a:hover {
|
||||
color: var(--color-orange);
|
||||
}
|
||||
|
||||
& p {
|
||||
color: var(--color-white-transparent);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
& .sitemap {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
252
app/assets/css/global.css
Normal file
252
app/assets/css/global.css
Normal file
|
@ -0,0 +1,252 @@
|
|||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: var(--color-orange);
|
||||
color: var(--color-black);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
border-radius: 20px;
|
||||
background: #b2bec3;
|
||||
transition: var(--transition-time);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
border-radius: 20px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-corner {
|
||||
border-radius: 100%;
|
||||
background: #b2bec3;
|
||||
}
|
||||
|
||||
:root {
|
||||
--spacing-standard: 3rem;
|
||||
--transition-time: 250ms;
|
||||
--radius-standard: 4px;
|
||||
|
||||
--color-white: #ecf0f1;
|
||||
--color-white-transparent: rgba(236, 240, 241, 0.7);
|
||||
|
||||
--color-black: #2a2723;
|
||||
--color-black-transparent: #2a2723aa;
|
||||
|
||||
--color-orange: #ff9100;
|
||||
--color-orange-light: #ffc36f;
|
||||
--color-orange-black: #332b22;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
min-height: 100vh;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
scrollbar-gutter: auto;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
&.layer {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
color: var(--color-white);
|
||||
background: var(--color-black);
|
||||
}
|
||||
|
||||
.h1, .h2, .h3, .h4, .h5, .h6,
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
text-align: left;
|
||||
font-family: 'Roboto Condensed', sans-serif;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 4rem;
|
||||
}
|
||||
|
||||
.h2,
|
||||
h2,
|
||||
h3 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
transition: var(--transition-time);
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
||||
.nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
a.mail {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: .5rem;
|
||||
color: var(--color-orange);
|
||||
font-weight: bold;
|
||||
|
||||
& img {
|
||||
filter: invert(50%) sepia(84%) saturate(868%) hue-rotate(1deg) brightness(103%) contrast(100%);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--color-orange-light);
|
||||
|
||||
& img {
|
||||
filter: invert(72%) sepia(59%) saturate(390%) hue-rotate(343deg) brightness(102%) contrast(103%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
span.highlight {
|
||||
color: var(--color-orange);
|
||||
}
|
||||
|
||||
span.chip {
|
||||
background: var(--color-orange);
|
||||
border-radius: 999px;
|
||||
font-size: 1rem;
|
||||
color: var(--color-black);
|
||||
height: max-content;
|
||||
padding: .5em 1em;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2rem;
|
||||
background: var(--color-black);
|
||||
border-radius: 20px;
|
||||
min-width: 300px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.color-icon {
|
||||
filter: invert(50%) sepia(84%) saturate(868%) hue-rotate(1deg) brightness(103%) contrast(100%);
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
z-index: 100;
|
||||
padding: 150px 15vw;
|
||||
}
|
||||
|
||||
.full {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.margin-top {
|
||||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
.margin-top-big {
|
||||
margin-top: 6rem;
|
||||
}
|
||||
|
||||
.flex-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.default-gap {
|
||||
gap: 3rem;
|
||||
}
|
||||
|
||||
.z-0 {
|
||||
box-shadow: 0 0 0 rgba(0, 0, 0, 0), 0 0 0 rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
.z-1 {
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
|
||||
}
|
||||
|
||||
.z-2 {
|
||||
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23);
|
||||
}
|
||||
|
||||
.z-3 {
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23);
|
||||
}
|
||||
|
||||
.z-4 {
|
||||
box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
|
||||
}
|
||||
|
||||
.z-5 {
|
||||
box-shadow: 0 19px 38px rgba(0, 0, 0, 0.30), 0 15px 12px rgba(0, 0, 0, 0.22);
|
||||
}
|
||||
|
||||
.tip-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tip-container .tip {
|
||||
scale: 0;
|
||||
position: absolute;
|
||||
top: -3rem;
|
||||
width: max-content;
|
||||
border: 1px solid var(--color-white);
|
||||
border-radius: 999px;
|
||||
background-color: var(--color-black);
|
||||
padding: .5em 1.5rem;
|
||||
transition: 150ms;
|
||||
}
|
||||
|
||||
.tip-container:hover .tip {
|
||||
scale: 1;
|
||||
}
|
||||
|
||||
.animate-up-down {
|
||||
animation: up-down 1.5s ease-in-out alternate-reverse infinite;
|
||||
}
|
||||
|
||||
@keyframes up-down {
|
||||
0% {
|
||||
translate: 0 -0.1rem;
|
||||
}
|
||||
|
||||
100% {
|
||||
translate: 0 0.4em;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 780px) {
|
||||
h1, h2, h3, h4, h5, h6, p {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 450px) {
|
||||
.content {
|
||||
padding: 150px 10vw;
|
||||
}
|
||||
}
|
189
app/assets/css/header.css
Normal file
189
app/assets/css/header.css
Normal file
|
@ -0,0 +1,189 @@
|
|||
.stickyWatch {
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.Header {
|
||||
padding: 15px calc(15vw - 30px);
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
top: 0;
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
|
||||
& .logo {
|
||||
fill-rule: evenodd;
|
||||
clip-rule: evenodd;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
stroke-miterlimit: 1.5;
|
||||
|
||||
& .fussel {
|
||||
stroke: white;
|
||||
fill: var(--color-black);
|
||||
stroke-width: 20px;
|
||||
}
|
||||
|
||||
& .glasses {
|
||||
fill: none;
|
||||
stroke: white;
|
||||
stroke-width: 62px;
|
||||
}
|
||||
}
|
||||
|
||||
& strong {
|
||||
font-family: 'Roboto Condensed', sans-serif;
|
||||
font-size: 1.5rem;
|
||||
flex: 1.5;
|
||||
cursor: default;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
|
||||
& svg {
|
||||
--size: 40px;
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
}
|
||||
}
|
||||
|
||||
& nav {
|
||||
position: relative;
|
||||
font-weight: normal;
|
||||
font-size: 1.2rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
& .active {
|
||||
color: var(--color-orange);
|
||||
}
|
||||
}
|
||||
|
||||
& > .wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 15px 22px;
|
||||
transition: 750ms;
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 0;
|
||||
|
||||
& > label {
|
||||
display: none;
|
||||
width: 30px;
|
||||
height: 25px;
|
||||
position: relative;
|
||||
transform: rotate(0deg);
|
||||
transition: var(--transition-time) ease-in-out;
|
||||
cursor: pointer;
|
||||
z-index: 20000;
|
||||
|
||||
& > span {
|
||||
display: block;
|
||||
position: absolute;
|
||||
height: 5px;
|
||||
width: 100%;
|
||||
background: var(--color-white);
|
||||
border-radius: 9px;
|
||||
opacity: 1;
|
||||
left: 0;
|
||||
transform: rotate(0deg);
|
||||
transition: .25s ease-in-out;
|
||||
|
||||
&:nth-child(1) {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
&:nth-child(2), &:nth-child(3) {
|
||||
top: 9px;
|
||||
}
|
||||
|
||||
&:nth-child(4) {
|
||||
top: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& > input[type="checkbox"]:checked + label span:nth-child(1) {
|
||||
top: 18px;
|
||||
width: 0;
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
& > input[type="checkbox"]:checked + label span:nth-child(2) {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
& > input[type="checkbox"]:checked + label span:nth-child(3) {
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
& > input[type="checkbox"]:checked + label span:nth-child(4) {
|
||||
top: 18px;
|
||||
width: 0;
|
||||
left: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
&.sticks > .wrapper {
|
||||
background: var(--color-black-transparent);
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
& input[type="checkbox"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& ul {
|
||||
gap: var(--spacing-standard);
|
||||
transform: scale(1);
|
||||
|
||||
& a {
|
||||
display: block;
|
||||
text-align: center;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.15);
|
||||
color: var(--color-orange);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@media screen and (width < 1180px) {
|
||||
.Header {
|
||||
& > .wrapper.wrapper > label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
& input[type="checkbox"]:checked ~ nav {
|
||||
transform: translateX(-15vw);
|
||||
}
|
||||
|
||||
& nav {
|
||||
background: var(--color-black);
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
top: -15px;
|
||||
transition: var(--transition-time);
|
||||
transform: translateX(100%);
|
||||
color: var(--color-white);
|
||||
flex-direction: column;
|
||||
|
||||
& ul {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
gap: 8vh;
|
||||
|
||||
& li {
|
||||
font-size: clamp(1rem, 10vw, 3rem);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
82
app/assets/css/intro.css
Normal file
82
app/assets/css/intro.css
Normal file
|
@ -0,0 +1,82 @@
|
|||
.Intro {
|
||||
background-image: radial-gradient(circle at -50vw -50vh, rgba(255,145,0,0.2) 0%, rgba(0,0,0,0) 63%, rgba(0,0,0,0) 100%);
|
||||
background-repeat: no-repeat;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
||||
& .intro-img {
|
||||
width: 750px;
|
||||
display: flex;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
|
||||
& img {
|
||||
transition: 250ms;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.intro-text {
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
|
||||
& h1 {
|
||||
position: relative;
|
||||
|
||||
& .dot {
|
||||
position: relative;
|
||||
right: 1rem;
|
||||
}
|
||||
|
||||
& small {
|
||||
position: absolute;
|
||||
font-size: 1.5rem;
|
||||
rotate: -30deg;
|
||||
translate: -30px -20px;
|
||||
|
||||
&:before {
|
||||
content: '(';
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: ')';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& .fulltext {
|
||||
color: var(--color-white);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 430px) {
|
||||
.Intro {
|
||||
& .intro-img{
|
||||
width: 430px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (width < 900px) {
|
||||
.Intro {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
& .intro-text, & .intro-img {
|
||||
grid-column-start: 1;
|
||||
}
|
||||
|
||||
& .intro-text,
|
||||
& h1 {
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
& .intro-img img {
|
||||
filter: brightness(.5);
|
||||
}
|
||||
}
|
||||
}
|
41
app/assets/css/person.css
Normal file
41
app/assets/css/person.css
Normal file
|
@ -0,0 +1,41 @@
|
|||
.Person {
|
||||
flex-basis: clamp(350px, calc(33% - 3rem), 500px);
|
||||
flex-grow: 1;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: stretch;
|
||||
gap: 1rem;
|
||||
|
||||
& img {
|
||||
outline: 4px solid var(--color-orange);
|
||||
border-radius: 50%;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
& span {
|
||||
font-family: 'Roboto Condensed', sans-serif;
|
||||
font-weight: bold;
|
||||
|
||||
&:not(:last-child):after {
|
||||
content: " | "
|
||||
}
|
||||
}
|
||||
|
||||
& p {
|
||||
text-align: center;
|
||||
white-space: pre-wrap;
|
||||
|
||||
&:first-of-type {
|
||||
margin-top: -1rem;
|
||||
}
|
||||
}
|
||||
|
||||
& .flavour {}
|
||||
|
||||
& .button {
|
||||
margin-top: auto;
|
||||
}
|
||||
}
|
||||
|
72
app/assets/css/services.css
Normal file
72
app/assets/css/services.css
Normal file
|
@ -0,0 +1,72 @@
|
|||
.Services {
|
||||
background-image: radial-gradient(circle at 100vw 100vh, rgba(255,145,0,0.2) 0%, rgba(0,0,0,0) 63%, rgba(0,0,0,0) 100%);
|
||||
background-color: var(--color-orange-black);
|
||||
background-repeat: no-repeat;
|
||||
|
||||
.service-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
grid-template-rows: repeat(4, auto);
|
||||
|
||||
& article {
|
||||
grid-row: span 4;
|
||||
display: grid;
|
||||
grid-template-rows: subgrid;
|
||||
position: relative;
|
||||
|
||||
& .chip {
|
||||
position: absolute;
|
||||
right: -1rem;
|
||||
top: calc(-1rem - 3px);
|
||||
}
|
||||
|
||||
& header {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
& ul {
|
||||
gap: 1rem;
|
||||
|
||||
& li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
& .extra {
|
||||
margin-top: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.network-list {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
padding-top: 4px;
|
||||
|
||||
& .scroll-container {
|
||||
display: flex;
|
||||
padding-bottom: 3rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (width < 1423px) {
|
||||
.Services {
|
||||
& .service-list article:last-child {
|
||||
grid-column: 1/-1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (width < 600px) {
|
||||
.Services {
|
||||
& .network-list {
|
||||
--height: 380px;
|
||||
& article {
|
||||
flex-basis: 70vw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
20
app/assets/css/skills.css
Normal file
20
app/assets/css/skills.css
Normal file
|
@ -0,0 +1,20 @@
|
|||
.Skills {
|
||||
background: var(--color-orange-black);
|
||||
background-image: radial-gradient(circle at 90vw 0, rgba(255,145,0,0.2) 0%, rgba(0,0,0,0) 63%, rgba(0,0,0,0) 100%);
|
||||
background-repeat: no-repeat;
|
||||
|
||||
& .skill-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
& .tech-list ul {
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
& .bottom {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
16
app/assets/css/technology.css
Normal file
16
app/assets/css/technology.css
Normal file
|
@ -0,0 +1,16 @@
|
|||
.Technology {
|
||||
position: relative;
|
||||
align-items: center;
|
||||
|
||||
&.s img {
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
&.m img {
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
&.l img {
|
||||
height: 50px;
|
||||
}
|
||||
}
|
29
app/components/Button.vue
Normal file
29
app/components/Button.vue
Normal file
|
@ -0,0 +1,29 @@
|
|||
<template>
|
||||
<component :is="type" v-bind="actualProps()" class="Button" :class="[design]">
|
||||
<slot />
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
type Props = {
|
||||
type ?: 'a' | 'button'
|
||||
href ?: string
|
||||
design ?: string
|
||||
}
|
||||
|
||||
const {
|
||||
type = 'a',
|
||||
href = '#',
|
||||
design = 'default',
|
||||
} = defineProps<Props>()
|
||||
|
||||
const actualProps = () => {
|
||||
if (type === 'a') {
|
||||
return {
|
||||
href: href,
|
||||
target: href!.startsWith('https://') ? '_blank' : undefined,
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
204
app/components/Customers.vue
Normal file
204
app/components/Customers.vue
Normal file
|
@ -0,0 +1,204 @@
|
|||
<template>
|
||||
<section id="customers" class="Customers content">
|
||||
<h2>Kunden <span class="highlight">&</span> Projekte.</h2>
|
||||
<h3>Meine bisherigen Geschäftpartner</h3>
|
||||
<div class="customer-list margin-top default-gap">
|
||||
<a v-for="customer in customers" :href="customer.link" target="_blank" rel="noopener noreferrer">
|
||||
<img
|
||||
loading="lazy"
|
||||
height="50"
|
||||
:width="customer.logo.width"
|
||||
:alt="customer.name"
|
||||
:src="`/img/customers/${customer.logo.src}.svg`"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<h3 class="margin-top-big">Projektauswahl</h3>
|
||||
<div class="projects-list margin-top">
|
||||
<article v-for="pr in projects">
|
||||
<div class="bg">
|
||||
<img height="350" width="400" loading="lazy" :alt="pr.title" :src="pr.image" aria-hidden="true"/>
|
||||
</div>
|
||||
<div>
|
||||
<main>
|
||||
<small class="customer">{{ pr.customer }}</small>
|
||||
<h3 class="title">{{ pr.title }}</h3>
|
||||
<ul>
|
||||
<li v-for="skill in pr.technologies">
|
||||
<Technology v-bind="skill" link="" />
|
||||
</li>
|
||||
</ul>
|
||||
<p v-for="d in pr.desc">{{ d }}</p>
|
||||
<a v-if="pr.link" :href="pr.link" target="_blank">Zur {{pr.type ?? 'Seite'}}</a>
|
||||
</main>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { android, css, dart, flutter, html, js, njs, nuxt, pcss, scss, ts, tw, vue } from '../utils/skills'
|
||||
|
||||
const customers = [
|
||||
{
|
||||
name: 'Bounce Commerce',
|
||||
link: 'https://bounce-commerce.de',
|
||||
logo: {
|
||||
src: 'bounce',
|
||||
width: 150,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'GMX',
|
||||
link: 'https://gmx.net',
|
||||
logo: {
|
||||
src: 'gmx',
|
||||
width: 148,
|
||||
white: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'WEB.DE',
|
||||
link: 'https://web.de',
|
||||
logo: {
|
||||
src: 'webde',
|
||||
width: 50,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '1&1',
|
||||
link: 'https://1und1.de',
|
||||
logo: {
|
||||
src: '1u1',
|
||||
width: 50,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Körrie',
|
||||
link: 'https://körrie.de',
|
||||
logo: {
|
||||
src: 'koerrie',
|
||||
width: 50,
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Pembe',
|
||||
link: 'https://pembe.io',
|
||||
logo: {
|
||||
src: 'pembe',
|
||||
width: 48,
|
||||
white: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'SAE Institute Germany',
|
||||
link: 'https://www.sae.edu/deu/en/sae-home/',
|
||||
logo: {
|
||||
src: 'sae',
|
||||
width: 77,
|
||||
white: true,
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
const projects = [
|
||||
{
|
||||
title: 'Kauft Körrie! App',
|
||||
customer: 'KVK Berlin',
|
||||
image: '/img/projects/koerrie_app.webp',
|
||||
desc: [
|
||||
'Entwicklung einer Android Info-App für die Gewürzmischungen "Körrie" und passendem Zubehör.',
|
||||
'Zusätzlich die Übertragung des Körrie-O-Mat von der Landingpage in die App mit Ergebnisverlauf.',
|
||||
],
|
||||
technologies: [flutter, dart, android],
|
||||
link: 'https://play.google.com/store/apps/details?id=com.koerrieomat&hl=de',
|
||||
type: 'App',
|
||||
},
|
||||
{
|
||||
title: 'Unterricht',
|
||||
customer: 'SAE Institute Germany',
|
||||
image: '/img/projects/education.webp',
|
||||
desc: [
|
||||
'Vorbereitung und Durchführung von Unterricht in JavaScript und TypeScript.',
|
||||
],
|
||||
technologies: [js, ts]
|
||||
},
|
||||
{
|
||||
title: 'Headless CMS & Cache',
|
||||
customer: 'DEKRA',
|
||||
image: '/img/projects/dekra.webp',
|
||||
desc: [
|
||||
'Anbindung an ein Headless CMS und Entwicklung der dazugehörigen Komponentenbibliothek unter Einsatz von Tailwind, sowie serverseitiges Caching.',
|
||||
],
|
||||
technologies: [ts, nuxt, tw, njs]
|
||||
},
|
||||
{
|
||||
title: 'Bounce Script',
|
||||
customer: 'Bounce Commerce',
|
||||
link: 'https://bounce-commerce.de',
|
||||
image: '/img/projects/bounce.webp',
|
||||
desc: [
|
||||
'Script zum Einbinden in Web Shops für Bounce Management.',
|
||||
'Pures JavaScript, so klein gehalten wie möglich zur einfach Integration.',
|
||||
],
|
||||
technologies: [js]
|
||||
},
|
||||
{
|
||||
title: 'WEB.DE / GMX',
|
||||
customer: '1&1 Mail & Media',
|
||||
link: 'https://web.de',
|
||||
image: '/img/projects/webde.webp',
|
||||
desc: [
|
||||
'Neubau der Seiten web.de und GMX mit einem komponentenbasierten Ansatz unter Verwendung von VueJS.',
|
||||
'Optimiert für moderne Browser, während Internet Explorer in einer Extraversion angefertigt wurde.',
|
||||
],
|
||||
technologies: [js, vue, scss]
|
||||
},
|
||||
{
|
||||
title: 'Körrie! Landingpage',
|
||||
customer: 'KVK Berlin',
|
||||
link: 'https://körrie.de',
|
||||
image: '/img/projects/krrie.webp',
|
||||
desc: [
|
||||
'Neubau der Landingpage für "Kauft Körrie!". Die Prämisse war: Kein Schnickschnack.',
|
||||
'Deshalb aufgebaut mit simplem Js, HTML und CSS',
|
||||
],
|
||||
technologies: [html, css, js],
|
||||
},
|
||||
{
|
||||
title: 'UI Tools',
|
||||
customer: 'webfussel',
|
||||
link: 'https://uitools.webfussel.de',
|
||||
image: '/img/projects/uitools.webp',
|
||||
desc: [
|
||||
'Eine kleine Sammlung an Tools für die Erstellung von UIs.',
|
||||
'Farbpalette, Kontraste und CSS Variablen.',
|
||||
'Ist in aktiver Entwicklung.',
|
||||
],
|
||||
technologies: [nuxt, ts, pcss]
|
||||
},
|
||||
// {
|
||||
// customer: 'webfussel',
|
||||
// title: 'Shnaik - Teh Gaem',
|
||||
// link: 'https://shnaik.webfussel.de',
|
||||
// image: '/img/projects/shnaik.webp',
|
||||
// desc: [
|
||||
// 'Nachbau des bekannten Spiels "Snake" für die damaligen Nokia Handys.',
|
||||
// 'Meine erste Erfahrung mit Gaming Libraries und wurde eher als Experiment und Zeitvertreib angefertigt.',
|
||||
// ],
|
||||
// technologies: [ts, css]
|
||||
// },
|
||||
// {
|
||||
// title: 'PixelPalette',
|
||||
// customer: 'webfussel',
|
||||
// link: 'https://pixelpalette.webfussel.de',
|
||||
// image: '/img/projects/pp.webp',
|
||||
// desc: [
|
||||
// 'Ich hatte einige Tage eine Idee, wie man Grafiken mit 4 Farben - angelehnt den Gameboy - komprimieren und im Speicher unterbringen kann.',
|
||||
// 'Prototypisch zum Spaß erstellt.',
|
||||
// ],
|
||||
// technologies: [js, html, css]
|
||||
// },
|
||||
]
|
||||
</script>
|
84
app/components/Footer.vue
Normal file
84
app/components/Footer.vue
Normal file
|
@ -0,0 +1,84 @@
|
|||
<template>
|
||||
<footer class="Footer flex-col default-gap">
|
||||
<ul class="sitemap default-gap">
|
||||
<li v-for="{ label, ...rest} in nav" :key="label">
|
||||
<NuxtLink v-bind="rest">{{label}}</NuxtLink>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="sitemap default-gap">
|
||||
<li v-for="({icon, ...rest}) in socials" :key="rest.href">
|
||||
<a v-bind="rest" target="_blank">
|
||||
<Icon :name="icon" :alt="rest['aria-label']" size="1.5em" mode="svg" />
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="sitemap default-gap">
|
||||
<li class="tip-container">
|
||||
<Icon name="material-symbols:cookie-off-outline" size="1.5rem" mode="svg" />
|
||||
<span class="tip">Ohne Cookies</span>
|
||||
</li>
|
||||
<li class="tip-container">
|
||||
<Icon name="material-symbols:fingerprint-off" size="1.5rem" mode="svg" />
|
||||
<span class="tip">Ohne Tracker</span>
|
||||
</li>
|
||||
</ul>
|
||||
<p>© 2024 by <a href="https://webfussel.de">webfussel</a></p>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
|
||||
const nav = [
|
||||
{
|
||||
to: `/#intro`,
|
||||
label: 'Über mich',
|
||||
'aria-label': 'Link dieser Seite: Über mich'
|
||||
}, {
|
||||
to: `/#skills`,
|
||||
label: 'Meine Expertise',
|
||||
'aria-label': 'Link dieser Seite: Meine Expertise'
|
||||
}, {
|
||||
to: `/#customers`,
|
||||
label: 'Kunden',
|
||||
'aria-label': 'Link dieser Seite: Kunden'
|
||||
}, {
|
||||
to: `/#services`,
|
||||
label: 'Services',
|
||||
'aria-label': 'Link dieser Seite: Services'
|
||||
}, {
|
||||
to: `/#network`,
|
||||
label: 'Mein Netzwerk',
|
||||
'aria-label': 'Link dieser Seite: Mein Netzwerk'
|
||||
}, {
|
||||
to: '/imp',
|
||||
label: 'Impressum',
|
||||
'aria-label': 'Link dieser Seite: Impressum'
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
const socials = [
|
||||
{
|
||||
href: 'https://www.linkedin.com/in/webfussel/',
|
||||
icon: 'ri:linkedin-box-line',
|
||||
'aria-label': 'Externer Link: LinkedIn Profil'
|
||||
},
|
||||
{
|
||||
href: 'https://mastodontech.de/@webfussel',
|
||||
icon: 'ri:mastodon-line',
|
||||
rel: 'me',
|
||||
'aria-label': 'Externer Link: Mastodon Profil'
|
||||
},
|
||||
{
|
||||
href: 'https://bsky.app/profile/webfussel.de',
|
||||
icon: 'ri:bluesky-line',
|
||||
'aria-label': 'Externer Link: Bluesky Profil'
|
||||
},
|
||||
{
|
||||
href: 'https://ko-fi.com/webfussel',
|
||||
icon: 'wf:kofi',
|
||||
'aria-label': 'Externer Link: KoFi Profil'
|
||||
},
|
||||
]
|
||||
</script>
|
90
app/components/Header.vue
Normal file
90
app/components/Header.vue
Normal file
|
@ -0,0 +1,90 @@
|
|||
<template>
|
||||
<div ref="stickyWatch" />
|
||||
<header ref="header" class="Header">
|
||||
<div ref="headerWrapper" class="wrapper z-0">
|
||||
<strong>
|
||||
<svg aria-label="Logo" class="logo" height="40" viewBox="0 0 2500 2500" width="40">
|
||||
<g id="Logo">
|
||||
<g transform="matrix(2.00744,0,-5.91646e-31,2.00744,-1223.85,-1050.52)">
|
||||
<path class="fussel"
|
||||
d="M1232.34,1444.88L1356.88,1532.06C1356.88,1532.06 1405.5,1504.81 1444.06,1395.07C1464.03,1338.21 1476.18,1339.49 1506.32,1320.35C1579.8,1273.69 1638.29,1212.62 1630.86,1195.81C1560.6,1178.12 1512.77,1137.84 1506.32,1102.15L1618.41,946.736C1618.41,946.736 1514.23,877.412 1406.69,896.922C1407.7,845.817 1413.57,804.009 1481.42,759.931C1417.36,736.758 1260.23,740.351 1182.53,834.653C1115.13,783.067 1068.98,763.931 1008.18,759.931L1045.54,872.014C999.993,865.527 914.886,866.941 858.733,902.888C912.917,941.197 943.173,985.627 958.362,1033.91C883.905,1079.32 844.648,1134.09 808.918,1195.81C875.598,1205.68 938.224,1226.42 970.816,1282.99C1016.82,1362.83 1028.77,1456.11 1107.81,1532.06L1232.34,1444.88"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-422.589,697.589)">
|
||||
<path class="glasses" d="M1747.59,277.411C1695.36,294.131 1645.34,294.246 1597.59,277.411"/>
|
||||
</g>
|
||||
<path class="glasses"
|
||||
d="M1175,975C1189.02,1037.51 1161.76,1216.53 1125,1300C1027.14,1307.22 909.088,1298.04 825,1275C798.072,1183.9 789.715,1050.66 825,950C935.158,934.697 1076.23,935.423 1175,975Z"/>
|
||||
<g transform="matrix(-1,0,0,1,2500,2.20268e-13)">
|
||||
<path class="glasses"
|
||||
d="M1175,975C1189.02,1037.51 1161.76,1216.53 1125,1300C1027.14,1307.22 909.088,1298.04 825,1275C798.072,1183.9 789.715,1050.66 825,950C935.158,934.697 1076.23,935.423 1175,975Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
webfussel
|
||||
</strong>
|
||||
<input id="navToggle" v-model="isBurgerOpen" type="checkbox">
|
||||
<label :aria-label="burgerLabel" for="navToggle">
|
||||
<span/><span/><span/><span/>
|
||||
</label>
|
||||
<nav>
|
||||
<ul class="main-nav">
|
||||
<li v-for="({label, to, aria}) in nav" :key="label" @click="isBurgerOpen = false">
|
||||
<NuxtLink :to="to" :aria-label="aria" active-class="active">{{ label }}</NuxtLink>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
let observer: IntersectionObserver
|
||||
const header = ref<HTMLElement | null>(null)
|
||||
const headerWrapper = ref<HTMLElement | null>(null)
|
||||
const stickyWatch = ref<HTMLElement | null>(null)
|
||||
|
||||
const isBurgerOpen = ref<boolean>(false)
|
||||
const burgerOpenLabel = 'Burgermenü öffnen'
|
||||
const burgerCloseLabel = 'Burgermenü schließen'
|
||||
const burgerLabel = computed(() => isBurgerOpen.value ? burgerCloseLabel : burgerOpenLabel)
|
||||
|
||||
const nav = [
|
||||
{
|
||||
to: `/`,
|
||||
label: 'home',
|
||||
'aria-label': 'Link dieser Seite: Startseite'
|
||||
},
|
||||
{
|
||||
to: `/services`,
|
||||
label: 'leistungen',
|
||||
aria: 'Link dieser Seite: Leistungen'
|
||||
},
|
||||
{
|
||||
to: `/references`,
|
||||
label: 'referenzen',
|
||||
aria: 'Link dieser Seite: Referenzen'
|
||||
},
|
||||
{
|
||||
to: `/contact`,
|
||||
label: 'kontakt',
|
||||
aria: 'Link dieser Seite: Kontakt'
|
||||
},
|
||||
]
|
||||
|
||||
onMounted(() => {
|
||||
observer = new IntersectionObserver(([entry]) => {
|
||||
if (!entry) return
|
||||
const { isIntersecting } = entry
|
||||
header.value?.classList.toggle('sticks', !isIntersecting)
|
||||
headerWrapper.value?.classList.toggle('z-4', !isIntersecting)
|
||||
headerWrapper.value?.classList.toggle('z-0', isIntersecting)
|
||||
}, {
|
||||
rootMargin: '3% 0px 0px 0px'
|
||||
})
|
||||
observer.observe(stickyWatch.value!)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
observer.disconnect()
|
||||
})
|
||||
</script>
|
37
app/components/Intro.vue
Normal file
37
app/components/Intro.vue
Normal file
|
@ -0,0 +1,37 @@
|
|||
<template>
|
||||
<section id="intro" class="Intro content full default-gap">
|
||||
<div class="intro-text flex-col default-gap">
|
||||
<h1 class="flex-col">
|
||||
<span class="greeting">Moin.</span>
|
||||
<span class="my-name-wrapper">Ich bin <span class="nowrap"><span class="highlight">Fiona </span><small>Urban</small><span class="dot">.</span></span></span>
|
||||
</h1>
|
||||
<h2>
|
||||
Component <span class="highlight">&</span> API Entwicklerin
|
||||
</h2>
|
||||
<p class="fulltext">
|
||||
Ich unterstütze Unternehmen dabei, ihre Daten von verschiedenen Endpunkten sauber aufzubereiten
|
||||
und anschließend in einer Webapplication schön zu verpacken.
|
||||
</p>
|
||||
<p class="fulltext">
|
||||
Mit über 20 Jahren Erfahrung in der Webentwicklung habe ich
|
||||
inzwischen so ziemlich jeden Stuff miterlebt.
|
||||
</p>
|
||||
<p class="fulltext">
|
||||
Egal, ob Komponenten, Schnittstellen oder Anbindung an Headless CMS.
|
||||
Ich biete dir genau das, was du brauchst, um eine individuelle WebApp in Fahrt zu bringen, deren Inhalte einfach zu verändern sind.
|
||||
</p>
|
||||
<Button class="cta" href="#skills">
|
||||
<Icon name="ph:lightbulb-duotone" size="1.5em" mode="svg" />
|
||||
Fussel erklärt's dir
|
||||
</Button>
|
||||
</div>
|
||||
<div class="intro-img">
|
||||
<picture>
|
||||
<source width="750" height="866" media="(min-width: 431px)" srcset="/img/profile_big.webp" />
|
||||
<img width="430" height="866" src="/img/profile_small.webp" alt="Bild von Fiona Urban" />
|
||||
</picture>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
</script>
|
34
app/components/Person.vue
Normal file
34
app/components/Person.vue
Normal file
|
@ -0,0 +1,34 @@
|
|||
<template>
|
||||
<article class="Person flex-col">
|
||||
<img
|
||||
loading="lazy"
|
||||
width="150"
|
||||
height="150"
|
||||
:srcset="[userImage('1x', true), userImage('2x', true), userImage('3x', true)].join(', ')"
|
||||
:src="userImage('1x', false)"
|
||||
:alt="`Bild von ${name}`"
|
||||
/>
|
||||
<h3>{{name}}</h3>
|
||||
<p>
|
||||
<span v-for="tag in tags">{{tag}}</span>
|
||||
</p>
|
||||
<p class="flavour">{{flavour}}</p>
|
||||
<Button :href="link" class="button" target="_blank" rel="noreferrer noopener" :aria-label="`Externer Link zur Homepage von ${name}`">
|
||||
Zur Homepage
|
||||
</Button>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
type Props = {
|
||||
img: string
|
||||
name: string
|
||||
tags: string[]
|
||||
flavour: string
|
||||
link: string
|
||||
}
|
||||
|
||||
const { img } = defineProps<Props>()
|
||||
|
||||
const userImage = getImage('/img/network/', img)
|
||||
</script>
|
157
app/components/Services.vue
Normal file
157
app/components/Services.vue
Normal file
|
@ -0,0 +1,157 @@
|
|||
<template>
|
||||
<section id="services" class="Services content">
|
||||
<h2>Services.</h2>
|
||||
<h3>Du hast also beschlossen, dass du <span class="highlight">meine Hilfe</span> brauchst. Cool!</h3>
|
||||
<p class="margin-top">Hinter meinen Angeboten gibt es <span class="highlight">keinerlei Abos oder versteckte Kosten</span>.
|
||||
Aus Transparenzgründen sei aber gesagt, dass sich alle Preise zzgl. 19 % Umsatzsteuer verstehen.</p>
|
||||
<div class="service-list margin-top default-gap">
|
||||
<article v-for="service in services" class="z-2 card flex-col default-gap">
|
||||
<h3 class="flex-col default-gap">
|
||||
<span>{{service.title}}</span>
|
||||
<span class="highlight">{{service.price}}</span>
|
||||
</h3>
|
||||
<span class="chip">{{service.availability}}</span>
|
||||
<p>{{service.smallClaim}}</p>
|
||||
<Button :href="service.link" class="cta" aria-label="Zur externen Seite von zur Terminbuchung">
|
||||
{{ service.button }}
|
||||
<Icon :name="`ph:${service.icon}-duotone`" size="1.5em" mode="svg"></Icon>
|
||||
</Button>
|
||||
<ul class="flex-col">
|
||||
<li v-for="point in service.list">
|
||||
<Icon class="color-icon" name="ph:caret-circle-double-right-duotone" aria-hidden="true" alt="checkmark icon" size="1.5em" mode="svg" />
|
||||
<span>{{point}}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</article>
|
||||
</div>
|
||||
<article class="z-2 card flex-col default-gap margin-top">
|
||||
<h3>Keinen Bock auf Telen? Understandable.</h3>
|
||||
<p>Dann schreib mir einfach gerne direkt eine E-Mail an
|
||||
<ClientOnly><a class="mail" href="mailto:anfragen@webfussel.de">anfragen@webfussel.de<Icon name="ri:mail-line" aria-hidden="true" alt="mail icon" mode="svg" /></a></ClientOnly>
|
||||
</p>
|
||||
<h3>Keine Kohle? Kommt vor.</h3>
|
||||
<p>Meld dich trotzdem. Eventuell ist dein Projekt ja cool genug, dass ich dir da auch entsprechend entgegenkommen kann. :)</p>
|
||||
</article>
|
||||
<h3 id="network" class="margin-top-big">Mein Netzwerk</h3>
|
||||
<p class="margin-top">Doch auch wenn ich mal voll ausgelastet bin - keine Sorge!
|
||||
Mein <span class="highlight">Netzwerk an Profis</span> kann dir sicher auch weiterhelfen.
|
||||
</p>
|
||||
<ClientOnly>
|
||||
<div class="network-list margin-top">
|
||||
<div class="scroll-container default-gap">
|
||||
<Person ref="persons" v-for="person in network" v-bind="person" />
|
||||
</div>
|
||||
</div>
|
||||
</ClientOnly>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Person from '~/components/Person.vue'
|
||||
|
||||
const slots : number = 0
|
||||
const slotsLabel = `${slots} ${slots === 1 ? `Slot` : `Slots`} frei`
|
||||
|
||||
const freeFromDate = new Date(2025, 6, 1)
|
||||
const intl = new Intl.DateTimeFormat('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric'})
|
||||
const readableDate = intl.format(freeFromDate)
|
||||
|
||||
const services = [
|
||||
{
|
||||
title: 'Quick Check',
|
||||
price: '149 € / Einmalig',
|
||||
availability: 'Frei',
|
||||
smallClaim: 'Du hast eine Homepage und willst mal drüber schauen lassen?',
|
||||
icon: 'magnifying-glass',
|
||||
button: 'Jetzt untersuchen',
|
||||
link: 'https://tidycal.com/webfussel/quick-check',
|
||||
list: [
|
||||
'Untersuchung des Quellcodes',
|
||||
'Untersuchung der Performance',
|
||||
'Tipps zu CSS und Best Practices',
|
||||
'Behebung unkompliziert nachbuchen',
|
||||
'Für selbst gebaute Seiten',
|
||||
],
|
||||
}, {
|
||||
title: 'Projektbuchung',
|
||||
price: 'ab 999 € je nach Umfang',
|
||||
availability: slotsLabel,
|
||||
smallClaim: 'Umsetzung deiner Vision. Von einzelnen Tickets bis hin zu kompletten Anwendungen.',
|
||||
icon: 'trend-up',
|
||||
button: 'Jetzt durchstarten',
|
||||
link: 'https://tidycal.com/webfussel/project-booking',
|
||||
list: [
|
||||
'Anforderungsanalyse',
|
||||
'Kontinuierliche Projekt-Updates',
|
||||
'Fixe Kosten und Feature-Sets',
|
||||
'Nur 50 % Projektpreis als Anzahlung',
|
||||
],
|
||||
}, {
|
||||
title: 'Stundenbuchung',
|
||||
availability: `Frei ab ${readableDate}`,
|
||||
price: '105 € / Stunde',
|
||||
smallClaim: 'Du brauchst einfach Unterstützung im Team, bis sich der Trubel legt?',
|
||||
icon: 'timer',
|
||||
button: 'Jetzt buchen',
|
||||
link: 'https://tidycal.com/webfussel/hourly-booking',
|
||||
list: [
|
||||
'Flexible Aufgabenverteilung',
|
||||
'Arbeiten nach agilen Prinzipien',
|
||||
'Monatliche Abrechnung',
|
||||
'Kündigungsfrist von einer Woche',
|
||||
'Flexible Buchung ab 80 Stunden',
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
const shuffle = <T>(unshuffled : T[]) => unshuffled
|
||||
.map(value => ({ value, sort: Math.random() }))
|
||||
.sort((a, b) => a.sort - b.sort)
|
||||
.map(({ value }) => value)
|
||||
|
||||
const network = shuffle([
|
||||
{
|
||||
name: 'Robert Janus',
|
||||
img: 'robert',
|
||||
tags: ['Digitalberatung', 'Webentwicklung', 'eCommerce'],
|
||||
flavour: 'Website, SEO und Conversions. Auf einen Klick.',
|
||||
link: 'https://robertjanus.de/webertoire',
|
||||
},
|
||||
// {
|
||||
// name: 'Matthias Lehmann',
|
||||
// img: 'matthias',
|
||||
// tags: ['Onlineportale für Patienten', 'Kunden', 'Mitarbeiter'],
|
||||
// flavour: 'Software die macht, was DU willst!',
|
||||
// link: 'https://mind-deploy.de',
|
||||
// },
|
||||
{
|
||||
name: 'Maximilian Schluer',
|
||||
img: 'max',
|
||||
tags: ['iOS Development', 'Software-QA'],
|
||||
flavour: 'Kann dein iOS-Team unterstützen oder dein Software-Qualitätsproblem lösen – egal welches.',
|
||||
link: 'https://max-schluer.de',
|
||||
},
|
||||
// {
|
||||
// name: 'Maria Salcedo',
|
||||
// img: 'maria',
|
||||
// tags: ['Backend', 'DevOps', 'Architektur'],
|
||||
// flavour: 'Effizient und kommunikativ. "You build it, you run it."',
|
||||
// link: 'https://masagu.dev',
|
||||
// },
|
||||
{
|
||||
name: 'Judith Böhlert',
|
||||
img: 'judith',
|
||||
tags: ['Full-stack', 'Frontend'],
|
||||
flavour: 'MVPs und Prototypen - schnell, schick und ohne Drama.',
|
||||
link: 'https://judithboehlert.com',
|
||||
},
|
||||
{
|
||||
name: 'Kevin Damiani',
|
||||
img: 'kevin',
|
||||
tags: ['Webentwicklung', 'Frontend'],
|
||||
flavour: 'Erfahrener Frontend-Entwickler mit Fokus auf Performance, Barrierefreiheit und moderne Technologien.',
|
||||
link: 'https://kevin-damiani.de',
|
||||
},
|
||||
])
|
||||
</script>
|
64
app/components/Skills.vue
Normal file
64
app/components/Skills.vue
Normal file
|
@ -0,0 +1,64 @@
|
|||
<template>
|
||||
<section id="skills" class="Skills content">
|
||||
<h2>Meine Expertise.</h2>
|
||||
<h3>Dies sind meine <span class="highlight">Spezialgebiete</span> - aber ich bin flexibel!</h3>
|
||||
<div class="skill-list margin-top default-gap">
|
||||
<article class="z-2 card flex-col default-gap" v-for="skill in skills">
|
||||
<h3>{{skill.title}}</h3>
|
||||
<main>
|
||||
<p v-for="(t, i) in skill.text" :class="[i === skills.length - 1 && 'margin-top bold']">{{t}}</p>
|
||||
</main>
|
||||
</article>
|
||||
</div>
|
||||
<article class="tech-list z-2 card flex-col default-gap margin-top">
|
||||
<h3>Technologien</h3>
|
||||
<p>Neben den klassischen Webentwicklungsstandards JavaScript, HTML und CSS biete ich außerdem folgende Technologien.</p>
|
||||
<ul class="default-gap">
|
||||
<li v-for="tech in technologies">
|
||||
<Technology v-bind="tech" size="l"/>
|
||||
</li>
|
||||
</ul>
|
||||
</article>
|
||||
<div class="bottom flex-col margin-top default-gap">
|
||||
<h3>Manche von euch haben hier sicher kein Wort verstanden.</h3>
|
||||
<Button href="#services" class="cta">
|
||||
<span class="animate-up-down">
|
||||
<Icon name="ph:caret-double-down-duotone" size="1.5em" mode="svg" />
|
||||
</span>
|
||||
Für normale Menschen
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import { gl, njs, nuxt, pcss, react, rust, ts, vitest, vue, webstorm } from '../utils/skills'
|
||||
|
||||
const technologies = [nuxt, ts, pcss, vue, react, njs, vitest, gl, webstorm, rust]
|
||||
|
||||
const skills = [
|
||||
{
|
||||
title: 'Komponenten',
|
||||
text: [
|
||||
'Komponenten sind die Teile in deiner Applikation, die alles ansehnlich machen.',
|
||||
'Mit sauber implementierten, responsiven Bausteinen kannst du deine Seite gut Strukturieren, Daten sauber darstellen und den User einspannen.',
|
||||
'Vom kleinen Button bis hin zur Umfangreichen Tabelle bau ich dir (fast) alles.'
|
||||
],
|
||||
}, {
|
||||
title: 'APIs',
|
||||
text: [
|
||||
'Du hast Daten in einer Datenbank liegen, aber keine Ahnung, wie du da gescheit rankommen sollst?',
|
||||
'Liegen deine Daten eventuell sogar verstreut an mehreren Orten, über Datenbanken, Dateien und anderen Storagemöglichkeiten verteilt?',
|
||||
'Kein Ding. Ich bau dir eine Schnittstelle, die alles easy zusammenträgt.'
|
||||
],
|
||||
}, {
|
||||
title: 'Headless CMS',
|
||||
text: [
|
||||
'Wenn man ein Headless CMS anbinden will, dann verknüpft das Komponenten und APIs.',
|
||||
'Für eine saubere und dynamische Einbindung reicht die Library, die euch vom Hersteller zur Verfügung gestellt wird, oft nicht aus.',
|
||||
'Übersichtliche Projektstruktur und saubere Auflösung der Daten - mit Fusselgarantie.'
|
||||
],
|
||||
}
|
||||
]
|
||||
</script>
|
35
app/components/Technology.vue
Normal file
35
app/components/Technology.vue
Normal file
|
@ -0,0 +1,35 @@
|
|||
<template>
|
||||
<div class="Technology flex-col tip-container" :class="[size]">
|
||||
<a v-if="link" :href="link" target="_blank" rel="noopener noreferrer">
|
||||
<img loading="lazy" :src="img" :alt="altText()" :height="getPixelForSize()" :width="getWidth()" />
|
||||
</a>
|
||||
<img v-else loading="lazy" :height="getPixelForSize()" :width="getWidth()" :src="img" :alt="altText()"/>
|
||||
<span class="tip">{{name}}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
type Props = {
|
||||
img: string
|
||||
name: string
|
||||
link?: string
|
||||
width?: number
|
||||
size?: 's' | 'm' | 'l'
|
||||
}
|
||||
|
||||
const {
|
||||
name,
|
||||
size = 'm',
|
||||
width = 50,
|
||||
} = defineProps<Props>()
|
||||
|
||||
const sizes = {
|
||||
s: 15,
|
||||
m: 30,
|
||||
l: 50,
|
||||
}
|
||||
|
||||
const altText = () => `Icon für ${name}`
|
||||
const getPixelForSize = () => sizes[size]
|
||||
const getWidth = () => width / 50 * getPixelForSize()
|
||||
</script>
|
5
app/layouts/default.vue
Normal file
5
app/layouts/default.vue
Normal file
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<Header />
|
||||
<slot />
|
||||
<Footer />
|
||||
</template>
|
8
app/pages/contact.vue
Normal file
8
app/pages/contact.vue
Normal file
|
@ -0,0 +1,8 @@
|
|||
<template>
|
||||
<div>
|
||||
<Intro />
|
||||
<Skills />
|
||||
<Customers />
|
||||
<Services />
|
||||
</div>
|
||||
</template>
|
78
app/pages/imp.vue
Normal file
78
app/pages/imp.vue
Normal file
|
@ -0,0 +1,78 @@
|
|||
<template>
|
||||
<div>
|
||||
<section class="Imp flex-col default-gap content full">
|
||||
<div>
|
||||
<p>
|
||||
Fiona Lena Urban<br/>
|
||||
Fiona Urban aka webfussel<br/>
|
||||
Teichäckerweg 39<br/>
|
||||
76297 Stutensee
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3>Kontakt</h3>
|
||||
<ClientOnly>
|
||||
<p>
|
||||
Telefon: 017631640961<br/>
|
||||
E-Mail: fiona@webfussel.de
|
||||
</p>
|
||||
</ClientOnly>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3>Umsatzsteuer-ID</h3>
|
||||
<p>
|
||||
Umsatzsteuer-Identifikationsnummer gemäß § 27 a Umsatzsteuergesetz:<br/>
|
||||
DE348500161
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3>Angaben zur Berufs­haftpflicht­versicherung</h3>
|
||||
<p>
|
||||
<strong>Name und Sitz des Versicherers:</strong><br/>
|
||||
Hiscox SA<br/>
|
||||
Arnulfstr. 31<br/>
|
||||
80636 München
|
||||
</p>
|
||||
<p>
|
||||
<strong>Geltungsraum der Versicherung:</strong><br/>
|
||||
Bundesrepublik Deutschland
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3>Redaktionell verantwortlich</h3>
|
||||
<p>Fiona Lena Urban</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3>Verbraucher­streit­beilegung / Universal­schlichtungs­stelle</h3>
|
||||
<p>
|
||||
Wir sind nicht bereit oder verpflichtet, an Streitbeilegungsverfahren vor einer
|
||||
Verbraucherschlichtungsstelle teilzunehmen.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex-col default-gap">
|
||||
<h3>Quellenangaben</h3>
|
||||
<div>
|
||||
<h4>Bilder</h4>
|
||||
<p>
|
||||
Copyright © 2024 JetBrains s.r.o. WebStorm and the WebStorm logo are registered trademarks of JetBrains s.r.o.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4>Impressumstext</h4>
|
||||
<p>Quelle: <a href="https://www.e-recht24.de">e-recht24.de</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
useSeoMeta({
|
||||
robots: 'noindex, nofollow',
|
||||
})
|
||||
</script>
|
6
app/pages/index.vue
Normal file
6
app/pages/index.vue
Normal file
|
@ -0,0 +1,6 @@
|
|||
<template>
|
||||
<div>
|
||||
<Intro />
|
||||
<Skills />
|
||||
</div>
|
||||
</template>
|
5
app/pages/references.vue
Normal file
5
app/pages/references.vue
Normal file
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<div>
|
||||
<Customers />
|
||||
</div>
|
||||
</template>
|
5
app/pages/services.vue
Normal file
5
app/pages/services.vue
Normal file
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<div>
|
||||
<Services />
|
||||
</div>
|
||||
</template>
|
3
app/utils/image.ts
Normal file
3
app/utils/image.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export const getImage =
|
||||
(path: string, img: string) => (size: "1x" | "2x" | "3x", set: boolean) =>
|
||||
`${path}${img}@${size}.webp${set ? ` ${size}` : ""}`;
|
44
app/utils/skills.ts
Normal file
44
app/utils/skills.ts
Normal file
|
@ -0,0 +1,44 @@
|
|||
const jsImg = '/img/skills/javascript.svg'
|
||||
const tsImg = '/img/skills/typescript.svg'
|
||||
const vueImg = '/img/skills/vue.svg'
|
||||
const reactImg = '/img/skills/react.svg'
|
||||
const postCssImg = '/img/skills/postcss.svg'
|
||||
const scssImg = '/img/skills/sass.svg'
|
||||
const cssImg = '/img/skills/css.svg'
|
||||
const htmlImg = '/img/skills/html.svg'
|
||||
const glImg = '/img/skills/gitlab.svg'
|
||||
const vitestImg = '/img/skills/vitest.svg'
|
||||
const njsImg = '/img/skills/nodejs.svg'
|
||||
const webstormImg = '/img/skills/webstorm.svg'
|
||||
const nuxtImg = '/img/skills/nuxt.svg'
|
||||
const twImg = '/img/skills/tw.svg'
|
||||
const rustImg = '/img/skills/rust.svg'
|
||||
const flutterImg = '/img/skills/flutter.svg'
|
||||
const dartImg = '/img/skills/dart.svg'
|
||||
const androidImg = '/img/skills/android.svg'
|
||||
|
||||
export type ISkill = {
|
||||
name: string
|
||||
img: string
|
||||
link ?: string
|
||||
width ?: number
|
||||
}
|
||||
|
||||
export const android: ISkill = {name: 'Android', img: androidImg, link: 'https://www.android.com', width: 88}
|
||||
export const css: ISkill = {name: 'CSS', img: cssImg }
|
||||
export const dart: ISkill = {name: 'Dart', img: dartImg, link: 'https://dart.dev'}
|
||||
export const flutter: ISkill = {name: 'Flutter', img: flutterImg, link: 'https://flutter.dev', width: 40}
|
||||
export const gl: ISkill = {name: 'GitLab', img: glImg, link: 'https://gitlab.com', width: 55}
|
||||
export const html: ISkill = {name: 'HTML', img: htmlImg, width: 44}
|
||||
export const js: ISkill = {name: 'JavaScript', img: jsImg}
|
||||
export const njs: ISkill = {name: 'Nodejs', img: njsImg, link: 'https://nodejs.org/en', width: 46}
|
||||
export const nuxt: ISkill = {name: 'Nuxt', img: nuxtImg, link: 'https://nuxt.com', width: 75}
|
||||
export const pcss: ISkill = {name: 'PostCSS', img: postCssImg, link: 'https://postcss.org'}
|
||||
export const react: ISkill = {name: 'React', img: reactImg, link: 'https://reactjs.org', width: 56}
|
||||
export const rust: ISkill = {name: 'Rust', img: rustImg, link: 'https://www.rust-lang.org'}
|
||||
export const scss: ISkill = {name: 'SCSS', img: scssImg, width: 67}
|
||||
export const tw: ISkill = {name: 'Tailwind', img: twImg, width: 84}
|
||||
export const ts: ISkill = {name: 'TypeScript', img: tsImg, link: 'https://www.typescriptlang.org'}
|
||||
export const vitest: ISkill = {name: 'Vitest', img: vitestImg, link: 'https://vitest.dev', width: 55}
|
||||
export const vue: ISkill = {name: 'Vue', img: vueImg, link: 'https://vuejs.org', width: 58}
|
||||
export const webstorm: ISkill = {name: 'JetBrains IDEs', img: webstormImg, link: 'https://www.jetbrains.com/webstorm'}
|
Loading…
Add table
Add a link
Reference in a new issue