Compare commits
10 commits
73f09ad76e
...
2c42f24d0f
Author | SHA1 | Date | |
---|---|---|---|
2c42f24d0f | |||
9d691974ce | |||
cffb719a05 | |||
fa0435efdf | |||
ca3868299c | |||
a86b89dc98 | |||
bb77bb6a5b | |||
48efe0f75b | |||
8668b96eff | |||
4e953392fc |
45 changed files with 446 additions and 605 deletions
|
@ -8,8 +8,6 @@
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
useSeoMeta({
|
useSeoMeta({
|
||||||
title: 'Home',
|
|
||||||
description: 'Headless CMS, Components & APIs by Fiona Urban. Storyblok, FirstSpirit, Nuxt.',
|
|
||||||
author: 'webfussel',
|
author: 'webfussel',
|
||||||
robots: 'index, follow',
|
robots: 'index, follow',
|
||||||
themeColor: '#2a2723',
|
themeColor: '#2a2723',
|
||||||
|
|
|
@ -123,6 +123,10 @@ a.mail {
|
||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
|
list-style-position: inside;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.row {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
.Spoiler {
|
.Spoiler {
|
||||||
background: var(--color-black);
|
background: var(--color-black);
|
||||||
padding: 1rem 2rem;
|
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
|
|
||||||
& .icon {
|
& .icon {
|
||||||
|
@ -17,13 +16,20 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
border-radius: 20px;
|
||||||
|
transition: 150ms ease-in-out;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(0, 0, 0, .3);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& > div {
|
& > div {
|
||||||
margin-top: 1rem;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
padding: 1rem 2rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="Burger" :class="{ open }">
|
<div class="Burger" :class="{ open }">
|
||||||
<nav class="z-4" ref="navElement">
|
<nav class="z-4" ref="navElement">
|
||||||
<ul>
|
<ul class="row">
|
||||||
<li v-for="({label, to, aria, icon}) in navigation" :key="label">
|
<li v-for="({label, to, aria, icon}) in navigation" :key="label">
|
||||||
<NuxtLink :to="to" :aria-label="aria" active-class="active" class="inline-flex-row big-gap" @click="close">
|
<NuxtLink :to="to" :aria-label="aria" active-class="active" class="inline-flex-row big-gap" @click="close">
|
||||||
<Icon :name="icon" mode="svg" />
|
<Icon :name="icon" mode="svg" />
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<article class="z-2 card flex-col margin-top font-big text-center">
|
<article class="z-2 card flex-col margin-top font-big text-center">
|
||||||
<p>Derzeit habe ich <span class="highlight">keine freien Plätze</span>.</p>
|
<p>Derzeit habe ich <Highlight>keine freien Plätze</Highlight>.</p>
|
||||||
<p class="margin-top-small">Das ändert sich ab <span class="highlight">01. Januar 2026</span>.</p>
|
<p class="margin-top-small">Das ändert sich ab <Highlight>01. Januar 2026</Highlight>.</p>
|
||||||
</article>
|
</article>
|
||||||
</template>
|
</template>
|
3
app/components/Highlight.vue
Normal file
3
app/components/Highlight.vue
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<template>
|
||||||
|
<span class="highlight"><slot /></span>
|
||||||
|
</template>
|
3
app/components/Icon/LinkExternal.vue
Normal file
3
app/components/Icon/LinkExternal.vue
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<template>
|
||||||
|
<Icon name="ph:arrow-square-out-duotone" mode="svg" />
|
||||||
|
</template>
|
3
app/components/Icon/LinkInternal.vue
Normal file
3
app/components/Icon/LinkInternal.vue
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<template>
|
||||||
|
<Icon name="ph:link-simple-duotone" mode="svg" />
|
||||||
|
</template>
|
|
@ -7,7 +7,7 @@
|
||||||
<main>
|
<main>
|
||||||
<small class="customer">{{ company }}</small>
|
<small class="customer">{{ company }}</small>
|
||||||
<h3 class="title">{{ title }}</h3>
|
<h3 class="title">{{ title }}</h3>
|
||||||
<ul>
|
<ul class="row">
|
||||||
<li v-for="skill in tech">
|
<li v-for="skill in tech">
|
||||||
<Technology v-bind="skill" link="" />
|
<Technology v-bind="skill" link="" />
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -1,40 +0,0 @@
|
||||||
<template>
|
|
||||||
<component :is="computedComponent.is" v-bind="computedComponent.props" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { RichText } from './Types'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
element : RichText
|
|
||||||
}
|
|
||||||
|
|
||||||
const { element } = defineProps<Props>()
|
|
||||||
|
|
||||||
const computedComponent = computed(() => {
|
|
||||||
let is : ReturnType<typeof resolveComponent> = {}
|
|
||||||
|
|
||||||
switch (element.type) {
|
|
||||||
case 'plain':
|
|
||||||
is = resolveComponent('RichTextPlain')
|
|
||||||
break
|
|
||||||
case 'p':
|
|
||||||
is = resolveComponent('RichTextParagraph')
|
|
||||||
break
|
|
||||||
case 'span':
|
|
||||||
is = resolveComponent('RichTextString')
|
|
||||||
break
|
|
||||||
case 'a':
|
|
||||||
is = resolveComponent('RichTextLink')
|
|
||||||
break
|
|
||||||
case 'br':
|
|
||||||
is = resolveComponent('RichTextNewLine')
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
return { is, props: { ...element } }
|
|
||||||
})
|
|
||||||
|
|
||||||
</script>
|
|
|
@ -1,18 +0,0 @@
|
||||||
<template>
|
|
||||||
<a v-if="isExternal" :href="href" :target="target" class="text inline-flex-row">
|
|
||||||
{{ content }}
|
|
||||||
<Icon name="ph:arrow-square-out-duotone" mode="svg" />
|
|
||||||
</a>
|
|
||||||
<NuxtLink v-else :to="href" class="text inline-flex-row">
|
|
||||||
{{ content }}
|
|
||||||
<Icon name="ph:link-simple-duotone" mode="svg" />
|
|
||||||
</NuxtLink>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { RichTextLink } from './Types'
|
|
||||||
|
|
||||||
const { href } = defineProps<RichTextLink>()
|
|
||||||
|
|
||||||
const isExternal = href.startsWith('http')
|
|
||||||
</script>
|
|
|
@ -1,3 +0,0 @@
|
||||||
<template>
|
|
||||||
<br>
|
|
||||||
</template>
|
|
|
@ -1,11 +0,0 @@
|
||||||
<template>
|
|
||||||
<p :class="css">
|
|
||||||
<RichTextGeneral v-for="element in children" :element="element" />
|
|
||||||
</p>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { RichTextParagraph } from './Types'
|
|
||||||
|
|
||||||
defineProps<RichTextParagraph>()
|
|
||||||
</script>
|
|
|
@ -1,9 +0,0 @@
|
||||||
<template>
|
|
||||||
{{ content }}
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { RichTextPlain } from './Types'
|
|
||||||
|
|
||||||
defineProps<RichTextPlain>()
|
|
||||||
</script>
|
|
|
@ -1,20 +0,0 @@
|
||||||
<template>
|
|
||||||
<template v-if="Array.isArray(elements)">
|
|
||||||
<General v-for="element in elements" :element="element" />
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<General :element="elements" />
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { RichText } from './Types'
|
|
||||||
import General from './General.vue'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
elements : RichText | RichText[]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
defineProps<Props>()
|
|
||||||
</script>
|
|
|
@ -1,11 +0,0 @@
|
||||||
<template>
|
|
||||||
<span :class="css">
|
|
||||||
{{ content }}
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { RichTextSpan } from './Types'
|
|
||||||
|
|
||||||
defineProps<RichTextSpan>()
|
|
||||||
</script>
|
|
|
@ -1,30 +0,0 @@
|
||||||
type RichTextBasis = {
|
|
||||||
type: string
|
|
||||||
content: string
|
|
||||||
css ?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type RichTextPlain = RichTextBasis & {
|
|
||||||
type: 'plain'
|
|
||||||
}
|
|
||||||
|
|
||||||
export type RichTextParagraph = Omit<RichTextBasis, 'content'> & {
|
|
||||||
type: 'p'
|
|
||||||
children ?: RichText[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export type RichTextSpan = RichTextBasis & {
|
|
||||||
type: 'span'
|
|
||||||
}
|
|
||||||
|
|
||||||
export type RichTextLink = RichTextBasis & {
|
|
||||||
type: 'a'
|
|
||||||
href: string
|
|
||||||
target ?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type RichTextNewLine = {
|
|
||||||
type: 'br'
|
|
||||||
}
|
|
||||||
|
|
||||||
export type RichText = RichTextPlain | RichTextParagraph | RichTextSpan | RichTextLink | RichTextNewLine
|
|
|
@ -1,11 +1,22 @@
|
||||||
<template>
|
<template>
|
||||||
<section id="services" class="Services content">
|
<section id="services" class="Services content">
|
||||||
<h1>Projekt buchen</h1>
|
<h1>Projekt buchen</h1>
|
||||||
<h2>Du hast also beschlossen, dass du <span class="highlight">meine Hilfe</span> brauchst. Cool!</h2>
|
<h2>Paketpreise für
|
||||||
|
<Highlight>feste Ergebnisse</Highlight>
|
||||||
|
.
|
||||||
|
</h2>
|
||||||
<p class="margin-top">Manchmal brauchen wir alle einfach nur eine Kleinigkeit und wollen uns nicht lange binden. Das ist natürlich
|
<p class="margin-top">Manchmal brauchen wir alle einfach nur eine Kleinigkeit und wollen uns nicht lange binden. Das ist natürlich
|
||||||
völlig in Ordnung und genau deshalb biete ich dir die Möglichkeit mich gezielt für <span class="highlight">kleinere Projekte</span> zu buchen.</p>
|
völlig in Ordnung und genau deshalb biete ich dir die Möglichkeit mich gezielt für
|
||||||
<p class="margin-top-small">Hinter diesen Angeboten gibt es <span class="highlight">keinerlei Abos oder versteckte Kosten</span>.
|
<Highlight>kleinere Projekte</Highlight>
|
||||||
Aus Transparenzgründen sei aber gesagt, dass sich <span class="highlight">alle Preise zzgl. 19 % Umsatzsteuer</span> verstehen.</p>
|
zu buchen.
|
||||||
|
</p>
|
||||||
|
<p class="margin-top-small">Hinter diesen Angeboten gibt es
|
||||||
|
<Highlight>keinerlei Abos oder versteckte Kosten</Highlight>
|
||||||
|
.
|
||||||
|
Aus Transparenzgründen sei aber gesagt, dass sich
|
||||||
|
<Highlight>alle Preise zzgl. 19 % Umsatzsteuer</Highlight>
|
||||||
|
verstehen.
|
||||||
|
</p>
|
||||||
|
|
||||||
<FreeInfo/>
|
<FreeInfo/>
|
||||||
|
|
||||||
|
@ -43,8 +54,8 @@
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-col gap-sm margin-top">
|
<div v-if="faq" class="flex-col gap-sm margin-top">
|
||||||
<Spoiler v-for="entry in faq" v-bind="entry" class="z-2" />
|
<ContentRenderer :value="faq" :style="{ display: 'contents' }"/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
@ -71,7 +82,8 @@ const intl = new Intl.NumberFormat(
|
||||||
currency: 'EUR',
|
currency: 'EUR',
|
||||||
minimumFractionDigits: 0,
|
minimumFractionDigits: 0,
|
||||||
maximumFractionDigits: 0,
|
maximumFractionDigits: 0,
|
||||||
})
|
},
|
||||||
|
)
|
||||||
|
|
||||||
const oneOff: Service[] = [
|
const oneOff: Service[] = [
|
||||||
{
|
{
|
||||||
|
@ -128,36 +140,21 @@ const oneOff : Service[] = [
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const faq = [
|
const { data: faq } = await useAsyncData('faq_booking', () => queryCollection('faq').path('/snippets/faq/booking').first())
|
||||||
{
|
const texts = generatePlainText<['title']>(faq.value?.body.value)
|
||||||
header: 'Warum machst du keine Stundensätze?',
|
|
||||||
content: [
|
|
||||||
'Ich finde Stundensätze haben für beide Seiten nur Nachteile:',
|
|
||||||
'Wenn ich schnell und gut arbeite, dann bekomme ich weniger Geld. Hab ich mal einen Knoten im Gehirn und brauche sehr lange, muss der Kunde mehr zahlen.',
|
|
||||||
'Klar kann man sagen, dass sich das irgendwann ausgleichen könnte - aber so weit will ich es garnicht erst kommen lassen.'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Welche Themen bietest du für deine Schulungen an?',
|
|
||||||
content: [
|
|
||||||
'Sprachen: JavaScript, TypeScript, HTML, CSS',
|
|
||||||
'Frameworks: Vue, Nuxt',
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Wo finden die Schulungen statt?',
|
|
||||||
content: [
|
|
||||||
'Die Schulungen finden online statt. Normalerweise nutze ich dafür Google Meet, aber wenn du oder deine Firma eine andere Plattform wünschen und bereitstellen bin ich natürlich flexibel.',
|
|
||||||
'Wenn sich deine Firma in der Nähe meines Wohnortes befindet - und damit meine ich "In einer Stunde mit der Straßenbahn zu erreichen", dann kann alles natürlich auch vor Ort stattfinden.',
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Ich hab ein cooles Projekt! Aber kein Geld...',
|
|
||||||
content: [
|
|
||||||
'Tja.',
|
|
||||||
'Ne, awas. Meld dich einfach trotzdem über meine E-Mail-Adresse und vielleicht finden wir eine Lösung.'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
|
if (faq) {
|
||||||
|
useSchemaOrg({
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@type': 'FAQPage',
|
||||||
|
'mainEntity': texts.map(entity => ({
|
||||||
|
'@type': 'Question',
|
||||||
|
'name': entity.meta.title,
|
||||||
|
'acceptedAnswer': {
|
||||||
|
'@type': 'Answer',
|
||||||
|
'text': entity.text,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<section id="contact" class="Contact content full gap-default">
|
<section id="contact" class="Contact content full gap-default">
|
||||||
<h1>Kontakt <span class="highlight">&</span> Social Media</h1>
|
<h1>Kontakt <Highlight>&</Highlight> Social Media</h1>
|
||||||
<h2>Kannst ruhig in meine <span class="highlight">DMs sliden</span>. Oder in's Postfach.</h2>
|
<h2>Kannst ruhig in meine <Highlight>DMs sliden</Highlight>. Oder in's Postfach.</h2>
|
||||||
|
|
||||||
<article class="z-2 card flex-col gap-sm margin-top">
|
<article class="z-2 card flex-col gap-sm margin-top">
|
||||||
<h3>Du willst einfach nur 'ne Mail schreiben?</h3>
|
<h3>Du willst einfach nur 'ne Mail schreiben?</h3>
|
||||||
|
@ -12,10 +12,10 @@
|
||||||
|
|
||||||
<article class="z-2 card flex-col gap-sm margin-top">
|
<article class="z-2 card flex-col gap-sm margin-top">
|
||||||
<h3>Ich auf Social Media</h3>
|
<h3>Ich auf Social Media</h3>
|
||||||
<p>Falls du irgendwo einen anderen Social Media Account von mir findest, der nicht hier aufgelistet ist, aber aktiv postet, dann ist dieser höchstwahrscheinlich <span class="highlight">Fake</span>.
|
<p>Falls du irgendwo einen anderen Social Media Account von mir findest, der nicht hier aufgelistet ist, aber aktiv postet, dann ist dieser höchstwahrscheinlich <Highlight>Fake</Highlight>.
|
||||||
<br />Meld' dich gerne bei mir, wenn du so einen findest.
|
<br />Meld' dich gerne bei mir, wenn du so einen findest.
|
||||||
</p>
|
</p>
|
||||||
<ul class="social-media">
|
<ul class="row social-media">
|
||||||
<li v-for="({icon, name, ...rest}) in socials" :key="rest.href">
|
<li v-for="({icon, name, ...rest}) in socials" :key="rest.href">
|
||||||
<a v-bind="rest" target="_blank">
|
<a v-bind="rest" target="_blank">
|
||||||
<Icon :name="icon" :alt="rest['aria-label']" size="1.5em" mode="svg" />
|
<Icon :name="icon" :alt="rest['aria-label']" size="1.5em" mode="svg" />
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<section id="customers" class="Customers content">
|
<section id="customers" class="Customers content">
|
||||||
<h1>Kunden <span class="highlight">&</span> Projekte.</h1>
|
<h1>Kunden <Highlight>&</Highlight> Projekte.</h1>
|
||||||
<h2>Meine bisherigen Geschäftpartner</h2>
|
<h2>Meine bisherigen Geschäftpartner</h2>
|
||||||
<div class="customer-list margin-top gap-default">
|
<div class="customer-list margin-top gap-default">
|
||||||
<a v-for="customer in customers" :href="customer.link" target="_blank" rel="noopener noreferrer">
|
<a v-for="customer in customers" :href="customer.link" target="_blank" rel="noopener noreferrer">
|
||||||
|
|
|
@ -1,11 +1,21 @@
|
||||||
<template>
|
<template>
|
||||||
<section id="services" class="Services content">
|
<section id="services" class="Services content">
|
||||||
<h1>Prepaid Flatrates</h1>
|
<h1>Prepaid Flatrates</h1>
|
||||||
<h2>Genieße fusselige <span class="highlight">Qualität</span> ohne groß herumzurechnen.</h2>
|
<h2>Genieße fusselige
|
||||||
|
<Highlight>Qualität</Highlight>
|
||||||
|
ohne groß herumzurechnen.
|
||||||
|
</h2>
|
||||||
|
|
||||||
<p class="margin-top">Bei dir fällt ständig was an oder du hast ein langlaufendes Projekt, bei dem du immer wieder mal Unterstützung brauchst? Kein Ding.
|
<p class="margin-top">Bei dir fällt ständig was an oder du hast ein langlaufendes Projekt, bei dem du immer wieder mal Unterstützung brauchst?
|
||||||
Hier gibt's die <span class="highlight">Entwickler-Flat</span> für planbare Kosten und On-Demand-Entwicklung.</p>
|
Kein Ding.
|
||||||
<p class="margin-top-small">Aus Transparenzgründen sei gesagt, dass sich <span class="highlight">alle Preise zzgl. 19 % Umsatzsteuer</span> verstehen.</p>
|
Hier gibt's die
|
||||||
|
<Highlight>Entwickler-Flat</Highlight>
|
||||||
|
für planbare Kosten und On-Demand-Entwicklung.
|
||||||
|
</p>
|
||||||
|
<p class="margin-top-small">Aus Transparenzgründen sei gesagt, dass sich
|
||||||
|
<Highlight>alle Preise zzgl. 19 % Umsatzsteuer</Highlight>
|
||||||
|
verstehen.
|
||||||
|
</p>
|
||||||
|
|
||||||
<FreeInfo/>
|
<FreeInfo/>
|
||||||
|
|
||||||
|
@ -27,7 +37,8 @@
|
||||||
<span class="value">{{ service.hours }} Stunden pro Woche zugesichert</span>
|
<span class="value">{{ service.hours }} Stunden pro Woche zugesichert</span>
|
||||||
</li>
|
</li>
|
||||||
<li v-for="(check, index) in service.checks">
|
<li v-for="(check, index) in service.checks">
|
||||||
<Icon class="icon" :class="{ 'yes' : check, 'no' : !check }" :name="check ? 'ph:check-circle-duotone' : 'ph:x-circle-duotone'" mode="svg" />
|
<Icon class="icon" :class="{ 'yes' : check, 'no' : !check }" :name="check ? 'ph:check-circle-duotone' : 'ph:x-circle-duotone'"
|
||||||
|
mode="svg"/>
|
||||||
<span class="value">{{ points[index] }}</span>
|
<span class="value">{{ points[index] }}</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -43,8 +54,8 @@
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-col gap-sm margin-top">
|
<div v-if="faq" class="flex-col gap-sm margin-top">
|
||||||
<Spoiler v-for="entry in faq" v-bind="entry" class="z-2" />
|
<ContentRenderer :value="faq" :style="{ display: 'contents' }"/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
@ -135,51 +146,24 @@ const flatrate : Service[] =
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
],
|
],
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const faq = [
|
const { data: faq } = await useAsyncData('faq_flatrate', () => queryCollection('faq').path('/snippets/faq/flatrate').first())
|
||||||
{
|
const texts = generatePlainText<['title']>(faq.value?.body.value)
|
||||||
header: 'Was ist eine Entwickler-Flatrate?',
|
|
||||||
content: [
|
if (faq) {
|
||||||
'Die Entwickler-Flatrate ist ein Angebot, bei dem du für eine bestimmte Zeit eine bestimmte Menge an Leistungen erhältst.',
|
useSchemaOrg({
|
||||||
'Sie wird auch oft als sogenannter "Retainer" bezeichnet und ist eine günstige Art immer wieder anfallende Aufgaben auszulagern.'
|
'@context': 'https://schema.org',
|
||||||
]
|
'@type': 'FAQPage',
|
||||||
|
'mainEntity': texts.map(entity => ({
|
||||||
|
'@type': 'Question',
|
||||||
|
'name': entity.meta.title,
|
||||||
|
'acceptedAnswer': {
|
||||||
|
'@type': 'Answer',
|
||||||
|
'text': entity.text,
|
||||||
},
|
},
|
||||||
{
|
})),
|
||||||
header: 'Wie läuft die Zusammenarbeit ab?',
|
})
|
||||||
content: [
|
|
||||||
'Nach einer ersten Analyse deines Projekts legen wir gemeinsam den Umfang und die monatlichen Aufgaben fest. Du kannst mich je nach Paket auf den vereinbarten Wegen jederzeit kontaktieren.',
|
|
||||||
'Die Bearbeitung erfolgt innerhalb der vereinbarten Fristen.',
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Wie wird abgerechnet?',
|
|
||||||
content: [
|
|
||||||
'Du zahlst monatlich einen festen Betrag im Prepaid-Format unabhängig davon wie viele Aufgaben tatsächlich anfallen.',
|
|
||||||
'Dadurch kannst du dein Budget besser einplanen und Aufgaben verteilen.'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Kann ich den Retainer jederzeit kündigen?',
|
|
||||||
content: [
|
|
||||||
'Ja klar. Du musst dabei nur die Mindestvertragslaufzeit beachten - die bezahlte Leistung steht dir weiterhin zu.',
|
|
||||||
'Falls wir merken, dass wir für eine Zusammenarbeit mehr als ungeeignet sind, bekommst du dein Geld anteilig der verbleibenden Zeit zurück und verlierst den Anspruch auf Leistungen ab diesem Zeitpunkt.'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Was passiert, wenn ich dich mal weniger brauche?',
|
|
||||||
content: [
|
|
||||||
'Der monatliche feste Betrag bleibt bestehen. Ähnlich wie bei einer Versicherung bezahlst du hier für den Fall, dass du mich brauchst und erhältst dann entsprechend die vereinbarten Leistungen.',
|
|
||||||
'Wenn du merkst, dass du mich nicht mehr brauchst, dann kannst du den Vertrag jederzeit kündigen und die Leistungen auslaufen lassen.'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Für wen lohnt sich sowas überhaupt?',
|
|
||||||
content: [
|
|
||||||
'Vor allem Unternehmen, bei denen immer wieder Aufgaben anfallen für die sich aber kein komplettes Projekt oder gar eine feste Stelle in der Firma lohnt, profitieren von dieser Art von Leistung.',
|
|
||||||
'Sie ist ideal für eine langfristige und zuverlässige Zusammenarbeit.'
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
<template>
|
<template>
|
||||||
<footer class="Footer flex-col gap-default">
|
<footer class="Footer flex-col gap-default">
|
||||||
<ul class="sitemap gap-default">
|
<ul class="row sitemap gap-default">
|
||||||
<li v-for="{ label, ...rest} in nav" :key="label">
|
<li v-for="{ label, ...rest} in nav" :key="label">
|
||||||
<NuxtLink v-bind="rest">{{label}}</NuxtLink>
|
<NuxtLink v-bind="rest">{{label}}</NuxtLink>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul class="sitemap gap-default">
|
<ul class="row sitemap gap-default">
|
||||||
<li v-for="({icon, ...rest}) in socials" :key="rest.href">
|
<li v-for="({icon, ...rest}) in socials" :key="rest.href">
|
||||||
<a v-bind="rest" target="_blank">
|
<a v-bind="rest" target="_blank">
|
||||||
<Icon :name="icon" :alt="rest['aria-label']" size="1.5em" mode="svg" />
|
<Icon :name="icon" :alt="rest['aria-label']" size="1.5em" mode="svg" />
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul class="sitemap gap-default">
|
<ul class="row sitemap gap-default">
|
||||||
<li class="tip-container">
|
<li class="tip-container">
|
||||||
<Icon name="wf:cookie-slash" size="1.5rem" mode="svg" />
|
<Icon name="wf:cookie-slash" size="1.5rem" mode="svg" />
|
||||||
<span class="tip">Ohne Cookies</span>
|
<span class="tip">Ohne Cookies</span>
|
||||||
|
@ -22,7 +22,7 @@
|
||||||
<span class="tip">Ohne Tracker</span>
|
<span class="tip">Ohne Tracker</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p class="inline-flex-row"><Icon name="ph:copyright-duotone" mode="svg"/> 2024 by <a href="https://webfussel.de">webfussel</a></p>
|
<p class="inline-flex-row"><Icon name="ph:copyright-duotone" mode="svg"/> 2024 by <NuxtLink to="/">webfussel</NuxtLink></p>
|
||||||
</footer>
|
</footer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
<Icon name="ph:waves" mode="svg" size="2em" />
|
<Icon name="ph:waves" mode="svg" size="2em" />
|
||||||
</button>
|
</button>
|
||||||
<nav>
|
<nav>
|
||||||
<ul class="main-nav">
|
<ul class="row main-nav">
|
||||||
<li v-for="({label, to, aria, icon}) in navigation" :key="label">
|
<li v-for="({label, to, aria, icon}) in navigation" :key="label">
|
||||||
<NuxtLink :to="to" :aria-label="aria" active-class="active" class="inline-flex-row big-gap">
|
<NuxtLink :to="to" :aria-label="aria" active-class="active" class="inline-flex-row big-gap">
|
||||||
<Icon :name="icon" mode="svg" />
|
<Icon :name="icon" mode="svg" />
|
||||||
|
|
|
@ -3,10 +3,10 @@
|
||||||
<div class="intro-text flex-col gap-default">
|
<div class="intro-text flex-col gap-default">
|
||||||
<h1 class="flex-col">
|
<h1 class="flex-col">
|
||||||
<span class="greeting inline-flex-row">Moin. <Icon name="ph:hand-peace-duotone" mode="svg"/></span>
|
<span class="greeting inline-flex-row">Moin. <Icon name="ph:hand-peace-duotone" mode="svg"/></span>
|
||||||
<span class="my-name-wrapper">Ich bin <span class="highlight">Fiona</span>.</span>
|
<span class="my-name-wrapper">Ich bin <Highlight>Fiona</Highlight>.</span>
|
||||||
</h1>
|
</h1>
|
||||||
<h2>
|
<h2>
|
||||||
Component <span class="highlight">&</span> API Entwicklerin
|
Component <Highlight>&</Highlight> API Entwicklerin
|
||||||
</h2>
|
</h2>
|
||||||
<p class="fulltext">
|
<p class="fulltext">
|
||||||
Ich unterstütze Unternehmen dabei, ihre Daten von verschiedenen Endpunkten sauber aufzubereiten
|
Ich unterstütze Unternehmen dabei, ihre Daten von verschiedenen Endpunkten sauber aufzubereiten
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<section id="skills" class="Skills content">
|
<section id="skills" class="Skills content">
|
||||||
<h2>Meine Expertise.</h2>
|
<h2>Meine Expertise.</h2>
|
||||||
<h3>Dies sind meine <span class="highlight">Spezialgebiete</span> - aber ich bin flexibel!</h3>
|
<h3>Dies sind meine <Highlight>Spezialgebiete</Highlight> - aber ich bin flexibel!</h3>
|
||||||
<div class="skill-list margin-top gap-default">
|
<div class="skill-list margin-top gap-default">
|
||||||
<Card v-for="skill in skills" :title="skill.title" titleTag="h3">
|
<Card v-for="skill in skills" :title="skill.title" titleTag="h3">
|
||||||
<p v-for="(t, i) in skill.text" :class="[i === skill.text.length - 1 && 'bold']">{{t}}</p>
|
<p v-for="(t, i) in skill.text" :class="[i === skill.text.length - 1 && 'bold']">{{t}}</p>
|
||||||
|
@ -9,7 +9,7 @@
|
||||||
</div>
|
</div>
|
||||||
<Card title="Technologien" titleTag="h3" class="margin-top tech-list">
|
<Card title="Technologien" titleTag="h3" class="margin-top tech-list">
|
||||||
<p>Neben den klassischen Webentwicklungsstandards JavaScript, HTML und CSS biete ich außerdem folgende Technologien.</p>
|
<p>Neben den klassischen Webentwicklungsstandards JavaScript, HTML und CSS biete ich außerdem folgende Technologien.</p>
|
||||||
<ul class="gap-default margin-top-small">
|
<ul class="row gap-default margin-top-small">
|
||||||
<li v-for="tech in technologies">
|
<li v-for="tech in technologies">
|
||||||
<Technology v-bind="tech" size="l"/>
|
<Technology v-bind="tech" size="l"/>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
<template>
|
<template>
|
||||||
<section id="skills_easy" class="Skills content">
|
<section id="skills_easy" class="Skills content">
|
||||||
<h2>Jetzt mal ganz konkret.</h2>
|
<h2>Jetzt mal ganz konkret.</h2>
|
||||||
<h3>In diesem Abschnitt ganz <span class="highlight">ohne Technik-Blabla</span>.</h3>
|
<h3>In diesem Abschnitt ganz <Highlight>ohne Technik-Blabla</Highlight>.</h3>
|
||||||
<div class="skill-container flex-col margin-top gap-default">
|
<div class="skill-container flex-col margin-top gap-default">
|
||||||
<ContentRenderer v-for="skill in skills" :key="skill.id" :value="skill" :style="{ display: 'contents' }" />
|
<ContentRenderer v-for="skill in skills" :key="skill.id" :value="skill" :style="{ display: 'contents' }" />
|
||||||
</div>
|
</div>
|
||||||
<div class="bottom flex-col margin-top gap-default">
|
<div class="bottom flex-col margin-top gap-default">
|
||||||
<h3>Verwirkliche jetzt dein Webprojekt.</h3>
|
<h3>Verwirkliche jetzt dein Webprojekt.</h3>
|
||||||
<Button href="/booking" class="cta">
|
<Button href="/booking/" class="cta">
|
||||||
<Icon name="ph:chat-circle-text-duotone" size="1.5em" mode="svg" />
|
<Icon name="ph:chat-circle-text-duotone" size="1.5em" mode="svg" />
|
||||||
Lass mal reden
|
Lass mal reden
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -16,246 +16,5 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { RichText } from '@/components/RichText/Types'
|
|
||||||
|
|
||||||
type Skill = {
|
|
||||||
img: string
|
|
||||||
title: string
|
|
||||||
text: RichText[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const getExplanationImage = (img : string) => getImage('/img/explanations/', img)
|
|
||||||
|
|
||||||
// const skills : Skill[] = [
|
|
||||||
// {
|
|
||||||
// img: 'components',
|
|
||||||
// title: 'Das, was du sehen kannst',
|
|
||||||
// text: [
|
|
||||||
// {
|
|
||||||
// type: 'p',
|
|
||||||
// children: [
|
|
||||||
// {
|
|
||||||
// type: 'plain',
|
|
||||||
// content: 'Die meisten Anwendungen haben eine grafische Benutzeroberfläche - die so genannte GUI. Wenn etwas graphisch ist, ' +
|
|
||||||
// 'dann bedeutet das auch natürlich, dass es sinnvoll dargestellt werden muss. Dafür verwenden wir kleine Bausteine, die wir Komponenten nennen.',
|
|
||||||
// }
|
|
||||||
// ]
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// type: 'p',
|
|
||||||
// children: [
|
|
||||||
// {
|
|
||||||
// type: 'plain',
|
|
||||||
// content: 'Grundsätzlich lassen sich komplette Anwendungen und auch einfache Webseiten ziemlich cool über Komponenten aufbauen, sodass wir wiederkehrende Elemente ' +
|
|
||||||
// 'wie Buttons, Textabschnitte, Links oder Teaser immer wieder verwenden können - selbst wenn sich diese in ihren Details wie Farben oder Icons unterscheiden.',
|
|
||||||
// }
|
|
||||||
// ]
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// type: 'p',
|
|
||||||
// children: [
|
|
||||||
// {
|
|
||||||
// type: 'plain',
|
|
||||||
// content: 'Diese Komponenten so zu entwickeln, dass sie wirklich flexibel sind und auch perfekt mit dem Design übereinstimmen ist gar nicht so einfach, denn oft sollten sie ' +
|
|
||||||
// 'auch mehr können als im Design kurzfristig ersichtlich ist.',
|
|
||||||
// }
|
|
||||||
// ]
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// type: 'p',
|
|
||||||
// css: 'place-bottom bold',
|
|
||||||
// children: [
|
|
||||||
// {
|
|
||||||
// type: 'plain',
|
|
||||||
// content: 'Diese Voraussicht, für den Fall der Fälle vorzusorgen:',
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// type: 'br',
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// type: 'plain',
|
|
||||||
// content: 'Die gibt\'s bei mir dazu.',
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// type: 'br',
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// type: 'span',
|
|
||||||
// css: 'highlight',
|
|
||||||
// content: 'Fussel-Ehrenwort.'
|
|
||||||
// }
|
|
||||||
// ]
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
// }, {
|
|
||||||
// img: 'cms',
|
|
||||||
// title: 'Da, wo du eintragen kannst',
|
|
||||||
// text: [
|
|
||||||
// {
|
|
||||||
// type: 'p',
|
|
||||||
// children: [
|
|
||||||
// {
|
|
||||||
// type: 'plain',
|
|
||||||
// content: 'In vielen Fällen ist es natürlich praktisch, wenn du einfach Inhalte einer Seite auf das ändern kannst, was gerade aktuell ist, ohne auf andere angewiesen zu sein.'
|
|
||||||
// + 'Damit das reibungslos möglich ist, entwickle ich dir gerne eine Anwendung oder Homepage, deren Inhalte du komplett selbst auf dem neuesten Stand halten kannst - und zwar '
|
|
||||||
// + 'mit einem sogenannten CMS - ein ',
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// type: 'span',
|
|
||||||
// css: 'highlight',
|
|
||||||
// content: 'C'
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// type: 'plain',
|
|
||||||
// content: 'ontent '
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// type: 'span',
|
|
||||||
// css: 'highlight',
|
|
||||||
// content: 'M'
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// type: 'plain',
|
|
||||||
// content: 'anagement '
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// type: 'span',
|
|
||||||
// css: 'highlight',
|
|
||||||
// content: 'S'
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// type: 'plain',
|
|
||||||
// content: 'ystem.'
|
|
||||||
// },
|
|
||||||
// ]
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// type: 'p',
|
|
||||||
// children: [
|
|
||||||
// {
|
|
||||||
// type: 'plain',
|
|
||||||
// content: 'Für CMS setze ich in erster Linie auf die cloudbasierte Lösung ',
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// type: 'a',
|
|
||||||
// target: '_blank',
|
|
||||||
// css: 'text',
|
|
||||||
// href: 'https://www.storyblok.com',
|
|
||||||
// content: 'Storyblok'
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// type: 'plain',
|
|
||||||
// content: '. Dies stellt für die Meisten eine kostenlose bis kostengünstige Lösung dar ohne viel technisches Wissen mitbringen zu müssen.',
|
|
||||||
// }
|
|
||||||
// ]
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// type: 'p',
|
|
||||||
// children: [
|
|
||||||
// {
|
|
||||||
// type: 'plain',
|
|
||||||
// content: 'Falls du aber nicht möchtest, dass deine Daten auf irgendeinem Fremden server liegen - was ich durchaus verstehen kann! - dann gibt es auch die Möglichkeit mit ',
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// type: 'a',
|
|
||||||
// target: '_blank',
|
|
||||||
// css: 'text',
|
|
||||||
// href: 'https://www.strapi.io',
|
|
||||||
// content: 'Strapi'
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// type: 'plain',
|
|
||||||
// content: ' selbst das eigene CMS zu hosten. Das musst du dann aber allerdings selbst erledigen oder ich erledige das für dich für einen Aufpreis.',
|
|
||||||
// }
|
|
||||||
// ]
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// type: 'p',
|
|
||||||
// css: 'place-bottom bold',
|
|
||||||
// children: [
|
|
||||||
// {
|
|
||||||
// type: 'plain',
|
|
||||||
// content: 'Nie wieder jemand anderen Fragen zu müssen, um deine Website auf dem neuesten Stand zu halten.',
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// type: 'br',
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// type: 'span',
|
|
||||||
// css: 'highlight',
|
|
||||||
// content: 'Mit Fussel-Garantie.'
|
|
||||||
// }
|
|
||||||
// ]
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
// }, {
|
|
||||||
// img: 'result',
|
|
||||||
// title: 'Was dabei am Ende rauskommt',
|
|
||||||
// text: [
|
|
||||||
// {
|
|
||||||
// type: 'p',
|
|
||||||
// children: [
|
|
||||||
// {
|
|
||||||
// type: 'plain',
|
|
||||||
// content: 'Grundsätzlich lässt sich das ganz einfach zusammenfassen: ',
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// type: 'span',
|
|
||||||
// css: 'highlight',
|
|
||||||
// content: 'Dein persönlicher Webauftritt.',
|
|
||||||
// }
|
|
||||||
// ]
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// type: 'p',
|
|
||||||
// children: [
|
|
||||||
// {
|
|
||||||
// type: 'plain',
|
|
||||||
// content: 'Ob du nun etwas kleineres brauchst, um ein paar Hobbies zu zeigen. Oder vielleicht etwas größeres, weil du dir ein eigenes Business aufbauen willst. Eventuell ein eigener Blog, eine komplette Applikation oder '
|
|
||||||
// + 'die Möglichkeit für andere Leute deine Daten bereit zu stellen. All das, das kann ich dir mit meinen Fähigkeiten und meiner Erfahrung bieten.'
|
|
||||||
// }
|
|
||||||
// ]
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// type: 'p',
|
|
||||||
// children: [
|
|
||||||
// {
|
|
||||||
// type: 'plain',
|
|
||||||
// content: 'Erkunde einfach meine ',
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// type: 'a',
|
|
||||||
// href: '/references',
|
|
||||||
// content: 'Referenzen'
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// type: 'plain',
|
|
||||||
// content: ' und dir wird auffallen, dass diese äußerst durchmischt und bunt sind. So individuell wie die Projekte meiner bisherigen Kunden wird auch dein Projekt behandelt. Und auch, wenn '
|
|
||||||
// + 'du glaubst, dass die Referenzen nicht zu deinem Projekt passen, werden wir deine ganz individuelle Lösung gemeinsam erarbeiten.'
|
|
||||||
// }
|
|
||||||
// ]
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// type: 'p',
|
|
||||||
// css: 'place-bottom bold',
|
|
||||||
// children: [
|
|
||||||
// {
|
|
||||||
// type: 'plain',
|
|
||||||
// content: 'Denn jedes Projekt ist etwas eigenes und besonderes.',
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// type: 'br',
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// type: 'span',
|
|
||||||
// css: 'highlight',
|
|
||||||
// content: 'Auch deins.'
|
|
||||||
// }
|
|
||||||
// ]
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
// }
|
|
||||||
// ]
|
|
||||||
|
|
||||||
const skills = await queryCollection('skills').all()
|
const skills = await queryCollection('skills').all()
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
<template>
|
<template>
|
||||||
<details class="Spoiler" :open="open" @click="toggle">
|
<details class="Spoiler" :open="open">
|
||||||
<summary><Icon class="icon" :name="icon" mode="svg" />{{ header }}</summary>
|
<summary @click="toggle"><Icon class="icon" :name="icon" mode="svg" />{{ title }}</summary>
|
||||||
<div>
|
<div>
|
||||||
<p v-for="text in content">{{ text }}</p>
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
type Props = {
|
type Props = {
|
||||||
header: string
|
title: string
|
||||||
content: string[]
|
answer ?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
defineProps<Props>()
|
const { title, answer = '' } = defineProps<Props>()
|
||||||
|
|
||||||
const open = ref(false)
|
const open = ref(false)
|
||||||
const toggle = (event : MouseEvent) => {
|
const toggle = (event : MouseEvent) => {
|
||||||
|
|
|
@ -3,3 +3,10 @@
|
||||||
<SectionBooking />
|
<SectionBooking />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
useSeoMeta({
|
||||||
|
title: 'Projektbuchung',
|
||||||
|
description: 'Buche jetzt dein Projekt auf webfussel. Du brauchst eine Schulung in JavaScript, Typescript, HTML, CSS, Vue oder Nuxt? Kein Problem.',
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
|
@ -3,3 +3,10 @@
|
||||||
<SectionContact />
|
<SectionContact />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
useSeoMeta({
|
||||||
|
title: 'Kontakt',
|
||||||
|
description: 'Nimm Kontakt zu webfussel auf. Egal ob über E-Mail oder Social Media - ich freue mich auf deine Nachricht.',
|
||||||
|
})
|
||||||
|
</script>
|
|
@ -3,3 +3,10 @@
|
||||||
<SectionFlatrate />
|
<SectionFlatrate />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
useSeoMeta({
|
||||||
|
title: 'Flatrate',
|
||||||
|
description: 'Buche webfussel ganz einfach für eine zugesicherte Anzahl Stunden pro Woche. Wenn immer mal wieder was anfällt - vertrau auf Fusselqualität.',
|
||||||
|
})
|
||||||
|
</script>
|
|
@ -14,4 +14,9 @@ useHead({
|
||||||
{ rel: 'icon', href: '/favicon.ico', type: 'image/x-icon' },
|
{ rel: 'icon', href: '/favicon.ico', type: 'image/x-icon' },
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
useSeoMeta({
|
||||||
|
title: 'Home',
|
||||||
|
description: 'Webprojekte und Retainer mit Fusselqualität. Du brauchst eine Website mit CMS? Bock auf Flatrate? webfussel by Fiona Urban',
|
||||||
|
})
|
||||||
</script>
|
</script>
|
|
@ -3,3 +3,10 @@
|
||||||
<SectionCustomers />
|
<SectionCustomers />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
useSeoMeta({
|
||||||
|
title: 'Referenzen',
|
||||||
|
description: 'Schau dir dir Projekte von webfussel an. Über persönliche Webseiten, über Schulungen bis hin zu API Projekten.',
|
||||||
|
})
|
||||||
|
</script>
|
27
app/utils/markdown.ts
Normal file
27
app/utils/markdown.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import type { MinimalElement, MinimalNode } from '@nuxt/content'
|
||||||
|
|
||||||
|
type TypedRecord<T extends readonly string[]> = Record<T[number], string>
|
||||||
|
|
||||||
|
type PlainText<T extends string[]> = {
|
||||||
|
meta: TypedRecord<T>
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const extractText = (element ?: MinimalNode) : string => {
|
||||||
|
if (!element) return ''
|
||||||
|
if (typeof element === 'string') return element
|
||||||
|
const [,, ...nodes] = element
|
||||||
|
return nodes?.map((el : MinimalNode) => typeof el === 'string' ? el : extractText(el)).join(' ') ?? ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export const generatePlainText = <T extends string[] = []>(body ?: MinimalNode[]) : PlainText<T>[] => {
|
||||||
|
if (!body) return []
|
||||||
|
return body.map<PlainText<T>>(part => {
|
||||||
|
const [, meta] = part as MinimalElement
|
||||||
|
return {
|
||||||
|
meta : meta as TypedRecord<T>,
|
||||||
|
text: extractText(part).replace(/\n/g, ' ')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -18,12 +18,12 @@ export const socials = [
|
||||||
name: '@webfussel.de',
|
name: '@webfussel.de',
|
||||||
'aria-label': 'Externer Link: Bluesky Profil'
|
'aria-label': 'Externer Link: Bluesky Profil'
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
href: 'https://twitch.tv/webfussel',
|
// href: 'https://twitch.tv/webfussel',
|
||||||
icon: 'ph:twitch-logo-duotone',
|
// icon: 'ph:twitch-logo-duotone',
|
||||||
name: 'webfussel',
|
// name: 'webfussel',
|
||||||
'aria-label': 'Externer Link: Twitch Kanal'
|
// 'aria-label': 'Externer Link: Twitch Kanal'
|
||||||
},
|
// },
|
||||||
{
|
{
|
||||||
href: 'https://ko-fi.com/webfussel',
|
href: 'https://ko-fi.com/webfussel',
|
||||||
icon: 'wf:kofi',
|
icon: 'wf:kofi',
|
||||||
|
|
|
@ -10,13 +10,13 @@ export default defineContentConfig({
|
||||||
skills: defineCollection({
|
skills: defineCollection({
|
||||||
type: 'page',
|
type: 'page',
|
||||||
source: 'snippets/skills/*.md',
|
source: 'snippets/skills/*.md',
|
||||||
schema: z.object({
|
|
||||||
title: z.string(),
|
|
||||||
img: z.object({
|
|
||||||
path: z.string(),
|
|
||||||
name: z.string(),
|
|
||||||
position: z.string(),
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
faq: defineCollection({
|
||||||
|
type: 'page',
|
||||||
|
source: 'snippets/faq/*.md',
|
||||||
|
schema: z.object({
|
||||||
|
rawbody: z.string(),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
67
content/snippets/faq/booking.md
Normal file
67
content/snippets/faq/booking.md
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
::spoiler
|
||||||
|
---
|
||||||
|
title: "Warum machst du keine Stundensätze?"
|
||||||
|
---
|
||||||
|
Ich finde, Stundensätze haben für beide Seiten nur Nachteile:
|
||||||
|
|
||||||
|
Wenn ich schnell und gut arbeite, bekomme ich weniger Geld. Hab ich mal einen Knoten im Gehirn und brauche sehr lange, muss der Kunde mehr zahlen.
|
||||||
|
|
||||||
|
Klar kann man sagen, dass sich das irgendwann ausgleichen könnte – aber so weit will ich es gar nicht erst kommen lassen.
|
||||||
|
::
|
||||||
|
|
||||||
|
::spoiler
|
||||||
|
---
|
||||||
|
title: "Was bekomme ich denn für die 999 €?"
|
||||||
|
---
|
||||||
|
Grundsätzlich kommt das natürlich immer darauf an, was du genau willst. Da spielen einige Faktoren eine Rolle.
|
||||||
|
|
||||||
|
**Ein Beispiel für eine 999 € Seite wäre**
|
||||||
|
|
||||||
|
---
|
||||||
|
- Kein CMS
|
||||||
|
- Eine Section auf der Landingpage
|
||||||
|
- Impressum, Datenschutzerklärung
|
||||||
|
- Standarddesign
|
||||||
|
---
|
||||||
|
|
||||||
|
Je nachdem was du willst, kann sich das aber auch anders zusammensetzen.
|
||||||
|
Zu Beachten ist, dass es sich hier um reines HTML und CSS handelt. Keine Fancy Daten aus einem Backend, keine 3D-Animationen.
|
||||||
|
Große und gute Websites werden normalerweise über einen längeren Zeitraum entwickelt und kosten entsprechend auch mehr.
|
||||||
|
|
||||||
|
Bei diesem Startangebot handelt es sich also einfach nur um eine kleine Online-Visitenkarte.
|
||||||
|
::
|
||||||
|
|
||||||
|
::spoiler
|
||||||
|
---
|
||||||
|
title: "Welche Themen bietest du für deine Schulungen an?"
|
||||||
|
---
|
||||||
|
<div>
|
||||||
|
<Highlight>Sprachen</Highlight>
|
||||||
|
<br />JavaScript, TypeScript, HTML, CSS
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Highlight>Frameworks</Highlight>
|
||||||
|
<br />Vue, Nuxt
|
||||||
|
</div>
|
||||||
|
::
|
||||||
|
|
||||||
|
::spoiler
|
||||||
|
---
|
||||||
|
title: "Wo finden die Schulungen statt?"
|
||||||
|
---
|
||||||
|
Die Schulungen finden <Highlight>online</Highlight> statt. Normalerweise nutze ich dafür Google Meet, aber wenn du oder deine Firma eine andere Plattform wünschen und bereitstellen bin ich natürlich flexibel.
|
||||||
|
|
||||||
|
Wenn sich deine Firma in der Nähe meines Wohnortes befindet – und damit meine ich "In einer Stunde mit der Straßenbahn zu erreichen", dann kann alles natürlich auch vor Ort stattfinden.
|
||||||
|
::
|
||||||
|
|
||||||
|
::spoiler
|
||||||
|
---
|
||||||
|
title: "Ich hab ein cooles Projekt! Aber kein Geld …"
|
||||||
|
---
|
||||||
|
Tja.
|
||||||
|
|
||||||
|
Ne, awas. Meld dich einfach trotzdem über meine E-Mail-Adresse und vielleicht finden wir eine Lösung.
|
||||||
|
|
||||||
|
Du findest weitere Kontaktmöglichkeiten auf meiner [Kontakt-Seite <IconLinkInternal />](/contact/){class="text inline-flex-row"}.
|
||||||
|
::
|
53
content/snippets/faq/flatrate.md
Normal file
53
content/snippets/faq/flatrate.md
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
::spoiler
|
||||||
|
---
|
||||||
|
title: "Was ist eine Entwickler-Flatrate?"
|
||||||
|
---
|
||||||
|
Die Entwickler-Flatrate ist ein Angebot, bei dem du für eine bestimmte Zeit eine bestimmte Menge an Leistungen erhältst.
|
||||||
|
|
||||||
|
Sie wird auch oft als sogenannter "Retainer" bezeichnet und ist eine günstige Art, immer wieder anfallende Aufgaben auszulagern.
|
||||||
|
::
|
||||||
|
|
||||||
|
::spoiler
|
||||||
|
---
|
||||||
|
title: "Wie läuft die Zusammenarbeit ab?"
|
||||||
|
---
|
||||||
|
Nach einer ersten Analyse deines Projekts legen wir gemeinsam den Umfang und die monatlichen Aufgaben fest. Du kannst mich je nach Paket auf den vereinbarten Wegen jederzeit kontaktieren.
|
||||||
|
|
||||||
|
Die Bearbeitung erfolgt innerhalb der vereinbarten Fristen.
|
||||||
|
::
|
||||||
|
|
||||||
|
::spoiler
|
||||||
|
---
|
||||||
|
title: "Wie wird abgerechnet?"
|
||||||
|
---
|
||||||
|
Du zahlst monatlich einen festen Betrag im Prepaid-Format unabhängig davon, wie viele Aufgaben tatsächlich anfallen.
|
||||||
|
|
||||||
|
Dadurch kannst du dein Budget besser einplanen und Aufgaben verteilen.
|
||||||
|
::
|
||||||
|
|
||||||
|
::spoiler
|
||||||
|
---
|
||||||
|
title: "Kann ich den Retainer jederzeit kündigen?"
|
||||||
|
---
|
||||||
|
Ja klar. Du musst dabei nur die Mindestvertragslaufzeit beachten – die bezahlte Leistung steht dir weiterhin zu.
|
||||||
|
|
||||||
|
Falls wir merken, dass wir für eine Zusammenarbeit mehr als ungeeignet sind, bekommst du dein Geld anteilig der verbleibenden Zeit zurück und verlierst den Anspruch auf Leistungen ab diesem Zeitpunkt.
|
||||||
|
::
|
||||||
|
|
||||||
|
::spoiler
|
||||||
|
---
|
||||||
|
title: "Was passiert, wenn ich dich mal weniger brauche?"
|
||||||
|
---
|
||||||
|
Der monatliche feste Betrag bleibt bestehen. Ähnlich wie bei einer Versicherung bezahlst du hier für den Fall, dass du mich brauchst, und erhältst dann entsprechend die vereinbarten Leistungen.
|
||||||
|
|
||||||
|
Wenn du merkst, dass du mich nicht mehr brauchst, dann kannst du den Vertrag jederzeit kündigen und die Leistungen auslaufen lassen.
|
||||||
|
::
|
||||||
|
|
||||||
|
::spoiler
|
||||||
|
---
|
||||||
|
title: "Für wen lohnt sich sowas überhaupt?"
|
||||||
|
---
|
||||||
|
Vor allem Unternehmen, bei denen immer wieder Aufgaben anfallen, für die sich aber kein komplettes Projekt oder gar eine feste Stelle in der Firma lohnt, profitieren von dieser Art von Leistung.
|
||||||
|
|
||||||
|
Sie ist ideal für eine langfristige und zuverlässige Zusammenarbeit.
|
||||||
|
::
|
|
@ -15,5 +15,5 @@ Diese Komponenten so zu entwickeln, dass sie wirklich flexibel sind und auch per
|
||||||
|
|
||||||
**Diese Voraussicht, für den Fall der Fälle vorzusorgen:**
|
**Diese Voraussicht, für den Fall der Fälle vorzusorgen:**
|
||||||
**Die gibt's bei mir dazu.**
|
**Die gibt's bei mir dazu.**
|
||||||
<span class="highlight">Fussel-Ehrenwort.</span>
|
<Highlight>Fussel-Ehrenwort.</Highlight>
|
||||||
::
|
::
|
|
@ -7,12 +7,12 @@ image:
|
||||||
position: "right"
|
position: "right"
|
||||||
---
|
---
|
||||||
|
|
||||||
In vielen Fällen ist es natürlich praktisch, wenn du einfach Inhalte einer Seite auf das ändern kannst, was gerade aktuell ist, ohne auf andere angewiesen zu sein. Damit das reibungslos möglich ist, entwickle ich dir gerne eine Anwendung oder Homepage, deren Inhalte du komplett selbst auf dem neuesten Stand halten kannst – und zwar mit einem sogenannten <span class="highlight">CMS</span> – ein <span class="highlight">C</span>ontent <span class="highlight">M</span>anagement <span class="highlight">S</span>ystem.
|
In vielen Fällen ist es natürlich praktisch, wenn du einfach Inhalte einer Seite auf das ändern kannst, was gerade aktuell ist, ohne auf andere angewiesen zu sein. Damit das reibungslos möglich ist, entwickle ich dir gerne eine Anwendung oder Homepage, deren Inhalte du komplett selbst auf dem neuesten Stand halten kannst – und zwar mit einem sogenannten <Highlight>CMS</Highlight> – ein <Highlight>C</Highlight>ontent <Highlight>M</Highlight>anagement <Highlight>S</Highlight>ystem.
|
||||||
|
|
||||||
Für CMS setze ich in erster Linie auf die cloudbasierte Lösung [Storyblok <Icon name="ph:arrow-square-out-duotone" mode="svg" />](https://www.storyblok.com){class="inline-flex-row text"}. Dies stellt für die Meisten eine kostenlose bis kostengünstige Lösung dar ohne viel technisches Wissen mitbringen zu müssen.
|
Für CMS setze ich in erster Linie auf die cloudbasierte Lösung [Storyblok <IconLinkExternal />](https://www.storyblok.com){class="inline-flex-row text"}. Dies stellt für die Meisten eine kostenlose bis kostengünstige Lösung dar, ohne viel technisches Wissen mitbringen zu müssen.
|
||||||
|
|
||||||
Falls du aber nicht möchtest, dass deine Daten auf irgendeinem Fremden Server liegen – was ich durchaus verstehen kann! – dann gibt es auch die Möglichkeit mit [Strapi <Icon name="ph:arrow-square-out-duotone" mode="svg" />](https://strapi.io){class="inline-flex-row text"} selbst das eigene CMS zu hosten. Das musst du dann aber allerdings selbst erledigen oder ich erledige das für dich für einen Aufpreis.
|
Falls du aber nicht möchtest, dass deine Daten auf irgendeinem Fremden Server liegen – was ich durchaus verstehen kann! – dann gibt es auch die Möglichkeit mit [Strapi <IconLinkExternal />](https://strapi.io){class="inline-flex-row text"} selbst das eigene CMS zu hosten. Das musst du dann aber allerdings selbst erledigen oder ich erledige das für dich für einen Aufpreis.
|
||||||
|
|
||||||
**Nie wieder jemand anderen fragen zu müssen, um deine Website auf dem neuesten Stand zu halten.**
|
**Nie wieder jemand anderen fragen zu müssen, um deine Website auf dem neuesten Stand zu halten.**
|
||||||
<span class="highlight">Mit Fussel-Garantie.</span>
|
<Highlight>Mit Fussel-Garantie.</Highlight>
|
||||||
::
|
::
|
|
@ -7,12 +7,12 @@ image:
|
||||||
position: "left"
|
position: "left"
|
||||||
---
|
---
|
||||||
|
|
||||||
Grundsätzlich lässt sich das ganz einfach zusammenfassen: <span class="highlight">Dein persönlicher Webauftritt.</span>
|
Grundsätzlich lässt sich das ganz einfach zusammenfassen: <Highlight>Dein persönlicher Webauftritt.</Highlight>
|
||||||
|
|
||||||
Ob du nun etwas kleineres brauchst, um ein paar Hobbies zu zeigen. Oder vielleicht etwas größeres, weil du dir ein eigenes Business aufbauen willst. Eventuell ein eigener Blog, eine komplette Applikation oder die Möglichkeit für andere Leute deine Daten bereit zu stellen. All das, das kann ich dir mit meinen Fähigkeiten und meiner Erfahrung bieten.
|
Ob du nun etwas Kleineres brauchst, um ein paar Hobbys zu zeigen. Oder vielleicht etwas Größeres, weil du dir ein eigenes Business aufbauen willst. Eventuell ein eigener Blog, eine komplette Applikation oder die Möglichkeit für andere Leute deine Daten bereitzustellen. All das, das kann ich dir mit meinen Fähigkeiten und meiner Erfahrung bieten.
|
||||||
|
|
||||||
Erkunde einfach meine [Referenzen <Icon name="ph:link-simple-duotone" mode="svg" />](references/){class="inline-flex-row text"} und dir wird auffallen, dass diese äußerst durchmischt und bunt sind. So individuell wie die Projekte meiner bisherigen Kunden wird auch dein Projekt behandelt. Und auch, wenn du glaubst, dass die Referenzen nicht zu deinem Projekt passen, werden wir deine ganz individuelle Lösung gemeinsam erarbeiten.
|
Erkunde einfach meine [Referenzen <IconLinkInternal >](references/){class="inline-flex-row text"} und dir wird auffallen, dass diese äußerst durchmischt und bunt sind. So individuell wie die Projekte meiner bisherigen Kunden wird auch dein Projekt behandelt. Und auch, wenn du glaubst, dass die Referenzen nicht zu deinem Projekt passen, werden wir deine ganz individuelle Lösung gemeinsam erarbeiten.
|
||||||
|
|
||||||
**Denn jedes Projekt ist etwas eigenes und besonderes.**
|
**Denn jedes Projekt ist etwas Eigenes und Besonderes.**
|
||||||
<span class="highlight">Auch deins.</span>
|
<Highlight>Auch deins.</Highlight>
|
||||||
::
|
::
|
47
docker-compose.yaml
Normal file
47
docker-compose.yaml
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
wf4:
|
||||||
|
image: oven/bun:latest
|
||||||
|
container_name: wf4
|
||||||
|
working_dir: /app
|
||||||
|
ports:
|
||||||
|
- "1337:3000"
|
||||||
|
volumes:
|
||||||
|
- wf4_data:/app
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
command: >
|
||||||
|
sh -c "
|
||||||
|
# Install git and curl if not already installed
|
||||||
|
if ! command -v git &> /dev/null || ! command -v curl &> /dev/null; then
|
||||||
|
echo 'Installing required packages...'
|
||||||
|
apt-get update && apt-get install -y git curl
|
||||||
|
fi &&
|
||||||
|
|
||||||
|
# Clone repository if not already cloned
|
||||||
|
if [ ! -d /app/.git ]; then
|
||||||
|
echo 'Cloning repository...'
|
||||||
|
git clone https://git.webfussel.de/webfussel/wf4 /tmp/wf4 &&
|
||||||
|
cp -r /tmp/wf4/. /app/ &&
|
||||||
|
rm -rf /tmp/wf4
|
||||||
|
fi &&
|
||||||
|
|
||||||
|
# Install dependencies and start application
|
||||||
|
echo 'Installing dependencies...' &&
|
||||||
|
bun install &&
|
||||||
|
echo 'Building application...' &&
|
||||||
|
bun run build &&
|
||||||
|
echo 'Starting application...' &&
|
||||||
|
bun .output/server/index.mjs
|
||||||
|
"
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:1337"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 40s
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
wf4_data:
|
1
package-lock.json
generated
1
package-lock.json
generated
|
@ -3723,7 +3723,6 @@
|
||||||
},
|
},
|
||||||
"node_modules/@parcel/watcher-wasm/node_modules/napi-wasm": {
|
"node_modules/@parcel/watcher-wasm/node_modules/napi-wasm": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"dev": true,
|
|
||||||
"inBundle": true,
|
"inBundle": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
|
BIN
public/img/network/orell@1x.webp
Normal file
BIN
public/img/network/orell@1x.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.1 KiB |
BIN
public/img/network/orell@2x.webp
Normal file
BIN
public/img/network/orell@2x.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.9 KiB |
BIN
public/img/network/orell@3x.webp
Normal file
BIN
public/img/network/orell@3x.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.4 KiB |
Loading…
Add table
Add a link
Reference in a new issue