1
0
Fork 0

Use NuxtJS v3 and add start page

This commit is contained in:
Moritz Ruth 2021-12-12 21:59:27 +01:00
parent 4d5791fc2e
commit 2863beed61
No known key found for this signature in database
GPG key ID: AFD57E23E753841B
28 changed files with 5679 additions and 1798 deletions

View file

@ -1,13 +1,4 @@
{ {
"root": true, "root": true,
"extends": "awzzm-vue/v3", "extends": "awzzm-vue"
"rules": {
"vue/no-static-inline-styles": "off",
"unicorn/prevent-abbreviations": ["warn", {
"replacements": {
"i": false,
"props": false
}
}]
}
} }

93
.gitignore vendored
View file

@ -1,90 +1,7 @@
# Created by .ignore support plugin (hsz.mobi) .idea/
### Node template /node_modules/
# Logs
/logs
*.log *.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt .nuxt
nuxt.d.ts
# Nuxt generate .output
dist .env
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless
# IDE / Editor
.idea
# Service worker
sw.*
# Mac OSX
.DS_Store
# Vim swap files
*.swp

2
.nvmrc
View file

@ -1 +1 @@
16 16.11.0

View file

@ -1,3 +1,12 @@
# moritzruth.de # moritzruth.de
🔥 [**moritzruth.de**](https://moritzruth.de) 🔥 [**moritzruth.de**](https://moritzruth.de)
TODO:
- Start
- Blog
- Contact
- Photography
- Projects
- Apps
- Libraries

20
app.vue Normal file
View file

@ -0,0 +1,20 @@
<template>
<div class="min-h-100vh w-100vw text-light-900 overflow-x-hidden">
<NuxtPage/>
</div>
</template>
<style module>
html, body {
background: #070707;
overflow-x: hidden;
width: 100vw;
min-height: 100vh;
}
</style>
<script>
export default {
name: "App"
}
</script>

View file

@ -1,12 +1,19 @@
<template> <template>
<canvas ref="canvasElement" :style="{ width: size + 'px', height: size + 'px', filter: `blur(${blur}px)` }"/> <canvas ref="canvasElement" :style="{ filter: `blur(${blur}px)`, height: size + 'px', width: size + 'px' }"/>
</template> </template>
<script> <script>
import { canvasPath as createBlobAnimation } from "blobs/v2/animate/index.module.js" import { canvasPath as createBlobAnimation } from "blobs/v2/animate/index.module.js"
import { ref, watchEffect } from "vue" import { ref, watchEffect } from "vue"
import { useRafFn } from "@vueuse/core" import { useRafFn } from "@vueuse/core"
import { getComponentsOfHexColor } from "../utils/getComponentsOfHexColor.js"
function getComponentsOfHexColor(hexColorString) {
return [
Number.parseInt(hexColorString.slice(1, 3), 16),
Number.parseInt(hexColorString.slice(3, 5), 16),
Number.parseInt(hexColorString.slice(5, 7), 16)
]
}
export default { export default {
name: "BlurredBlobCanvas", name: "BlurredBlobCanvas",
@ -101,6 +108,8 @@
useRafFn(() => { useRafFn(() => {
const canvas = canvasElement.value const canvas = canvasElement.value
if (canvas === null) return if (canvas === null) return
canvas.width = props.size
canvas.height = props.size
const context = canvas.getContext("2d") const context = canvas.getContext("2d")

31
components/XSpacer.vue Normal file
View file

@ -0,0 +1,31 @@
<template>
<div :style="{ height, width }"/>
</template>
<style module>
</style>
<script>
export default {
name: "XSpacer",
props: {
v: {
default: 0,
type: null,
validate: value => ["number", "string"].includes(typeof value)
},
h: {
default: 0,
type: null,
validate: value => ["number", "string"].includes(typeof value)
}
},
setup(props) {
return {
height: (props.v * 0.25) + "rem",
width: (props.h * 0.25) + "rem"
}
}
}
</script>

View file

@ -1,27 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Moritz Ruth</title>
<link rel="preload" as="font" href="/node_modules/@fontsource/plus-jakarta-sans/files/plus-jakarta-sans-latin-400-normal.woff2" crossorigin="anonymous">
<link rel="preload" as="font" href="/node_modules/@fontsource/syne/files/syne-latin-800-normal.woff2" crossorigin="anonymous">
<link rel="stylesheet" href="/node_modules/@fontsource/plus-jakarta-sans/400.css">
<link rel="stylesheet" href="/node_modules/@fontsource/plus-jakarta-sans/800.css">
<link rel="stylesheet" href="/node_modules/@fontsource/syne/800.css">
<meta name="description" content="web development and graphic design.">
<meta name="keywords" content="web, dev, development, coding, moritz, ruth, development, design">
<link rel="shortcut icon" type="image/png" href="/icon.png">
</head>
<body class="bg-[#fefefe] overflow-x-hidden">
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
<script
async
defer
src="https://stats.moritzruth.de/umami.js"
data-website-id="e68ac4a6-c999-4f8e-be75-5a6252a55f17"
data-domains="moritzruth.de"
></script>
</body>
</html>

30
nuxt.config.ts Normal file
View file

@ -0,0 +1,30 @@
import { defineNuxtConfig } from "nuxt3"
import Icons from "unplugin-icons/vite"
// https://v3.nuxtjs.org/docs/directory-structure/nuxt.config
export default defineNuxtConfig({
ssr: true,
target: "static",
buildModules: [
"nuxt-windicss"
],
css: [
"@fontsource/plus-jakarta-sans/400.css",
"@fontsource/plus-jakarta-sans/700.css"
],
vite: {
plugins: [
Icons({
autoInstall: true
})
]
},
build: {
loaders: {
css: {
// @ts-expect-error
module: false
}
}
}
})

View file

@ -1,30 +1,27 @@
{ {
"name": "moritzruth.de", "name": "moritzruth.de",
"author": "Moritz Ruth <dev@moritzruth.de>", "private": true,
"scripts": { "scripts": {
"dev": "vite --host", "dev": "nuxi dev",
"build": "vite build", "build": "nuxi build",
"start": "vite preview", "start": "node .output/server/index.mjs",
"lint": "eslint . --fix" "fi": "pnpm i --shamefully-hoist"
},
"dependencies": {
"@fontsource/plus-jakarta-sans": "^4.4.5",
"@fontsource/syne": "^4.4.5",
"@vueuse/core": "^5.0.3",
"blobs": "^2.2.1-beta.1",
"vue": "^3.1.1",
"vue-i18n": "^9.1.6",
"vue-router": "4"
}, },
"devDependencies": { "devDependencies": {
"@intlify/vite-plugin-vue-i18n": "^2.2.1", "eslint": "^7.32.0",
"@vitejs/plugin-vue": "^1.2.3", "eslint-config-awzzm-vue": "^2.0.1",
"@vue/compiler-sfc": "^3.1.1", "nuxt-windicss": "^2.1.1",
"eslint": "^7.28.0", "nuxt3": "latest",
"eslint-config-awzzm-vue": "^1.6.0", "typescript": "^4.5.3",
"vite": "^2.3.7", "windicss": "^3.2.1"
"vite-plugin-pages": "^0.13.1", },
"vite-plugin-windicss": "^1.0.4", "dependencies": {
"windicss": "^3.1.3" "@fontsource/plus-jakarta-sans": "^4.5.0",
"@fontsource/syne": "^4.5.0",
"@vueuse/core": "^7.2.2",
"@windicss/plugin-interaction-variants": "^1.0.0",
"blobs": "^2.2.1-beta.1",
"unplugin-icons": "^0.12.23",
"vue": "^3.2.26"
} }
} }

145
pages/index.vue Normal file
View file

@ -0,0 +1,145 @@
<template>
<div class="h-100vh w-full max-w-[1200px] mx-auto flex justify-between -lg:flex-col p-5 sm:p-10">
<section class="relative pt-20">
<div class="absolute top-60 -left-20 lg:-left-10">
<BlurredBlobCanvas
:blur="30"
:size="300"
:randomness="80"
:minimum-duration="600"
:duration-variation="400"
:minimum-opacity="0.2"
:opacity-variation="0.5"
:colors="['#eb34cf', '#6577fc']"
/>
</div>
<div class="relative max-w-130 p-2 lg:pl-10">
<div class="font-extrabold text-3xl sm:text-4xl">
Moritz Ruth
</div>
<div class="text-lg sm:text-xl font-medium leading-8 pt-5">
<p>
Im&nbsp;a&nbsp;freelance
<router-link to="/projects" :class="$style.link">software&nbsp;developer</router-link>,
graphic&nbsp;design&nbsp;enthusiast&nbsp;and
<router-link to="/photography" :class="$style.link">hobby&nbsp;photographer</router-link>
from&nbsp;Europe.
</p>
<XSpacer v="5"/>
<p>
I&nbsp;primarily&nbsp;focus&nbsp;on
Web&nbsp;and&nbsp;Android&nbsp;development,
but&nbsp;I&nbsp;also&nbsp;do&nbsp;Backend&nbsp;sometimes.
</p>
</div>
<XSpacer v="10"/>
<router-link to="/contact" :class="$style.reachOut">Reach out</router-link>
</div>
</section>
<section class="relative lg:pr-20 pt-20 pb-10 mt-0">
<div class="absolute w-full pt-20 flex justify-center">
<BlurredBlobCanvas
:blur="30"
:size="300"
:randomness="100"
:minimum-duration="3000"
:duration-variation="1000"
:minimum-opacity="0.4"
:opacity-variation="0"
:colors="['#eb34cf', '#6577fc']"
/>
</div>
<div class="relative flex flex-col justify-center space-y-4">
<router-link
v-for="link in navigationLinks"
:key="link.to"
:to="link.to"
class="px-5 sm:px-6 py-4 bg-light-300 bg-opacity-5 rounded-lg backdrop-blur-lg flex
hover:bg-opacity-10 focus-visible:bg-opacity-10 transform hover:scale-104 transition duration-200 group"
>
<div class="flex items-center justify-center text-xl sm:text-2xl relative pr-3 sm:pr-5" :class="link.emojiClasses">
{{ link.emoji }}
</div>
<div class="flex-grow">
<div class="text-lg font-bold">{{ link.label }}</div>
<div class="opacity-60 -sm:text-sm">{{ link.text }}</div>
</div>
</router-link>
</div>
</section>
</div>
</template>
<style module>
.link {
background: linear-gradient(to bottom right, rgba(235, 52, 207, 0.5), rgba(101, 119, 252, 0.5));
background-size: 200% 200%;
animation: gradient 3s linear infinite;
}
@keyframes gradient {
0% {
background-position: 0 51%
}
50% {
background-position: 100% 50%
}
100% {
background-position: 0 51%
}
}
.reachOut {
@apply text-xl font-bold rounded-md bg-pink-900 px-5 py-2;
mix-blend-mode: color-dodge;
box-shadow: 0 2px 10px 0 rgba(112, 26, 117, 0.8);
}
</style>
<script>
import BlurredBlobCanvas from "../components/BlurredBlobCanvas.vue"
import XSpacer from "../components/XSpacer.vue"
const NAVIGATION_LINKS = [
{
emoji: "📝",
to: "/blog",
label: "Blog",
text: "My thoughts, mostly on dev things"
},
{
emoji: "✨",
to: "/projects",
label: "Projects",
text: "Apps and open-source projects"
},
{
emojiClasses: "top-[-0.25rem]",
emoji: "📷",
to: "/photography",
label: "Photography",
text: "Some photos Im proud of"
},
{
emoji: "💬",
to: "/contact",
label: "Contact me",
text: "Email, Matrix, Twitter"
}
]
export default {
name: "IndexPage",
components: {
XSpacer,
BlurredBlobCanvas
},
setup() {
return {
navigationLinks: NAVIGATION_LINKS
}
}
}
</script>

6252
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -1,68 +0,0 @@
<template>
<NavigationMenu/>
<div class="bg-white text-black max-w-1200px w-full mx-auto px-6 sm:px-10 _content">
<router-view/>
</div>
<div class="w-full h-20 space-x-10 flex items-center justify-center relative z-2">
<router-link
v-for="item in items"
:key="item.to"
class="uppercase tracking-wide text-sm"
:to="item.to"
>
{{ t(item.labelKey) }}
</router-link>
</div>
</template>
<style>
._content {
min-height: calc(100vh - 80px);
}
@screen sm {
._content {
min-height: calc(100vh - 160px);
}
}
</style>
<i18n lang="yaml">
en:
footer:
tac: Terms
legal: Legal Notice
de:
footer:
tac: AGB
legal: Impressum
</i18n>
<script>
import { useI18n } from "vue-i18n"
import NavigationMenu from "./components/NavigationMenu.vue"
const FOOTER_LINKS = [
{
labelKey: "footer.tac",
to: "/terms"
},
{
labelKey: "footer.legal",
to: "/impressum"
}
]
export default {
name: "App",
components: { NavigationMenu },
setup() {
const { t } = useI18n()
return {
t,
items: FOOTER_LINKS
}
}
}
</script>

View file

@ -1,24 +0,0 @@
<template>
<a
class="text-blue-700"
target="_blank"
rel="noopener noreferrer"
:href="href"
><slot/></a>
</template>
<style module>
</style>
<script>
export default {
name: "ExternalLink",
props: {
href: {
type: String,
required: true
}
}
}
</script>

View file

@ -1,149 +0,0 @@
<template>
<div
class="sm:hidden fixed z-101 bottom-5 right-5 rounded-full backdrop-filter bg-white w-18 h-18 flex justify-center items-center shadow-lg _blur-backdrop-or-hide"
role="button"
aria-label="Toggle navigation menu"
@click="active = !active"
>
<div class="flex flex-col justify-evenly items-center h-10">
<div class="w-10 h-2px bg-black transition duration-300 transform _easing" :style="`transform: ${active ? 'translateY(350%)' : ''} rotate(${active ? 45 : 0}deg)`"/>
<div class="w-10 h-2px bg-black transition duration-300 transform _easing" :style="`transform: ${active ? 'translateY(-350%)' : ''} rotate(${active ? -45 : 0}deg)`"/>
</div>
</div>
<nav
class="fixed sm:sticky top-0 z-100 w-full h-screen sm:h-20 backdrop-filter bg-white transition duration-400 _blur-backdrop-or-hide"
:class="[scrolled && 'shadow-lg', active ? 'opacity-100' : '-sm:opacity-0 -sm:pointer-events-none']"
>
<div class="flex items-center justify-between h-full max-w-1200px mx-auto flex-grow -sm:flex-col px-6 sm:px-10">
<div class="pointer-events-none fixed transition-all duration-500" :style="{ left: blobState.x + 'px', top: blobState.y + 'px', opacity: blobState.show ? 1 : 0 }">
<BlurredBlobCanvas
:colors="['#eb34cf', '#818cff']"
:opacity-variation="0"
:minimum-opacity="0.9"
:minimum-duration="1000"
:duration-variation="500"
:blur="10"
:size="130"
:randomness="20"
/>
</div>
<router-link class="uppercase font-special relative top-1 -sm:mt-20 transition duration-600 _home-link" to="/" @click="active = false">
Moritz Ruth
</router-link>
<div class="flex -sm:flex-col -sm:mb-30vh justify-end items-center sm:space-x-20 -sm:space-y-10 relative">
<router-link
v-for="item in items"
:key="item.to"
:ref="item.element"
class="lowercase text-2xl -sm:text-4xl"
:to="item.to"
@click="active = false"
>
{{ t(item.labelKey) }}
</router-link>
</div>
</div>
</nav>
</template>
<i18n lang="yaml">
en:
projects: Projects
contact: Contact
de:
projects: Projekte
contact: Kontakt
</i18n>
<style scoped>
._blur-backdrop-or-hide {
@apply bg-opacity-90;
backdrop-filter: blur(5px);
}
@supports (backdrop-filter: blur(5px)) {
._blur-backdrop-or-hide {
@apply bg-opacity-70;
}
}
._easing {
transition-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1);
}
/*noinspection CssUnusedSymbol*/
@screen sm {
._home-link.router-link-active {
opacity: 0;
pointer-events: none;
}
}
</style>
<script>
import { useWindowScroll, useWindowSize } from "@vueuse/core"
import { computed, ref, watch, reactive } from "vue"
import { useRoute } from "vue-router"
import { useI18n } from "vue-i18n"
import { useWindowScrollLock } from "../utils/useWindowScrollLock.js"
import BlurredBlobCanvas from "./BlurredBlobCanvas.vue"
const ITEMS = [
// {
// labelKey: "projects",
// to: "/projects"
// },
{
labelKey: "contact",
to: "/contact"
}
]
export default {
name: "NavigationMenu",
components: { BlurredBlobCanvas },
setup() {
const { y: windowScroll } = useWindowScroll()
const { width: windowWidth } = useWindowSize()
const route = useRoute()
const active = ref(false)
useWindowScrollLock(active)
const items = ITEMS.map(item => ({
...item,
element: ref(null)
}))
const activeItem = computed(() => items.find(item => item.to === route.path) ?? null)
const blobState = reactive({
x: windowWidth / 2,
y: 10,
show: false
})
watch([windowWidth, activeItem], () => {
if (activeItem.value === null) {
blobState.show = false
} else {
const { x, width, y } = activeItem.value.element.value.$el.getBoundingClientRect()
blobState.x = x + (width / 2) + 10
blobState.y = y - 30
blobState.show = true
}
}, { immediate: true })
const { t } = useI18n()
return {
scrolled: computed(() => windowScroll.value > 0),
blobState,
items,
active,
t
}
}
}
</script>

View file

@ -1,47 +0,0 @@
@layer components {
body {
font-size: 20px;
}
::selection {
@apply bg-blue-900 bg-opacity-80 text-white;
}
.prose {
h1 {
@apply font-bold text-3xl sm:text-5xl text-gray-900;
}
h2 {
@apply font-bold text-2xl sm:text-4xl text-gray-600 mt-8;
}
ol {
@apply list-decimal list-inside space-y-2;
::marker {
@apply text-gray-600;
}
}
ul {
@apply list-disc list-inside space-y-1;
}
address, p, ol, ul {
@apply not-italic text-lg max-w-240 mt-5;
}
}
.asterisk-list > li {
&:not(:last-child) {
margin-bottom: 5px;
}
&::before {
@apply text-blue-900;
content: "*";
margin-right: 5px;
}
}
}

View file

@ -1,30 +0,0 @@
import "virtual:windi.css"
import "./main.css"
import routes from "virtual:generated-pages"
import { createApp } from "vue"
import { createRouter, createWebHistory } from "vue-router"
import { createI18n } from "vue-i18n"
import App from "./App.vue"
const i18n = createI18n({
legacy: false,
fallbackLocale: "en",
locale: navigator.language
})
document.documentElement.lang = navigator.language.startsWith("de") ? "de" : "en"
const router = createRouter({
history: createWebHistory(),
routes,
scrollBehavior(to, from, savedPosition) {
if (savedPosition) return savedPosition
if (to.hash) return { el: to.hash }
return { top: 0 }
}
})
createApp(App)
.use(router)
.use(i18n)
.mount("#app")

View file

@ -1,41 +0,0 @@
<template>
<div class="prose pt-20 flex items-center justify-center flex-col h-50vh">
<h1 class="text-center">
{{ t("heading") }}
</h1>
<p class="mt-10">
<router-link to="/" class="text-blue-700">
{{ t("back") }}
</router-link>
</p>
</div>
</template>
<i18n lang="yaml">
en:
heading: This page does not exist.
back: Back to home
cat: Show me a picture of a cat
de:
heading: Diese Seite existiert nicht.
cat: Zeig mir ein Bild einer Katze
back: Zurück zur Startseite
</i18n>
<script>
import { useI18n } from "vue-i18n"
import ExternalLink from "../components/ExternalLink.vue"
export default {
name: "NotFoundPage",
components: { ExternalLink },
setup() {
const { t } = useI18n()
return {
t
}
}
}
</script>

View file

@ -1,51 +0,0 @@
<template>
<main class="text-2xl sm:text-3xl pt-26">
<h1 class="font-bold text-3xl sm:text-5xl text-gray-900 mb-10">{{ t("heading") }}</h1>
<p class="mt-10 mb-10">
{{ t("twitter") }}
<a
class="text-blue-900"
href="https://twitter.com/moritz_ruth"
target="_blank"
rel="noopener noreferrer"
>
@moritz_ruth
</a>
{{ t("or_email") }}
</p>
<p>
<a class="text-blue-900 text-4xl" href="mailto:hey@deltaa.xyz">
<span class="mr-2 relative bottom-2px"></span> hey@deltaa.xyz
</a>
</p>
<p class="mt-10 mb-10">
{{ t("happy_to_hear") }}
</p>
</main>
</template>
<i18n lang="yaml">
en:
heading: Contact
twitter: Hit me up on Twitter
or_email: "or send me an email (en/de):"
happy_to_hear: I'm looking forward to your message.
de:
heading: Kontakt
twitter: Schreib mir auf Twitter
or_email: "oder schicke mir eine Email:"
happy_to_hear: Ich freue mich auf deine Nachricht.
</i18n>
<script>
import { useI18n } from "vue-i18n"
export default {
name: "ContactPage",
setup() {
const { t } = useI18n()
return { t }
}
}
</script>

View file

@ -1,79 +0,0 @@
<template>
<main class="prose pt-20">
<h1>{{ t("heading") }}</h1>
<p v-if="!isGerman">
<i>As required by German law.</i>
</p>
<p>
{{ t("scope") }}
</p>
<h2>{{ t("according") }}</h2>
<address>
Moritz Ruth<br>
Zum Galgenberg 19<br>
66539 Neunkirchen
</address>
<p>
{{ t("email") }}:
<a class="text-blue-900" href="mailto:hey@deltaa.xyz">
hey@deltaa.xyz
</a>
</p>
<p>
{{ t("matrix") }}:
<ExternalLink href="https://moritzruth.de/matrix">
@moritz:moritzruth.de
</ExternalLink>
</p>
<h2 class="text-2xl text-blue-500 mt-20">{{ t("typefaces-used") }}</h2>
<ul>
<li>
<ExternalLink href="https://gitlab.com/bonjour-monde/fonderie/syne-typeface">
Syne
</ExternalLink>
</li>
<li>
<ExternalLink href="https://tokotype.github.io/plusjakarta-sans/">
+Jakarta Sans
</ExternalLink>
</li>
</ul>
</main>
</template>
<i18n lang="yaml">
en:
heading: Legal Notice
scope: The declarations on this page apply to this website (moritzruth.de) and all websites which reside under subdomains of moritzruth.de.
according: Information pursuant to § 5 TMG
email: Email address
typefaces-used: Typefaces used on this site
matrix: Matrix
de:
heading: Impressum
scope: Dieses Impressum gilt für diese Website (moritzruth.de) und alle Websites, welche unter Subdomains von moritzruth.de erreichbar sind.
according: Angaben gemäß § 5 TMG
email: Email-Adresse
typefaces-used: Typefaces auf dieser Seite
matrix: Matrix
</i18n>
<script>
import { useI18n } from "vue-i18n"
import { computed } from "vue"
import ExternalLink from "../components/ExternalLink.vue"
export default {
name: "LegalNoticePage",
components: { ExternalLink },
setup() {
const { locale, t } = useI18n()
return {
isGerman: computed(() => locale.value.startsWith("de")),
t
}
}
}
</script>

View file

@ -1,123 +0,0 @@
<template>
<main class="flex -md:flex-col justify-between items-start md:items-center min-h-80vh">
<div class="relative -md:pt-20vh">
<div class="_fade-2">
<div class="_pattern transform rotate-179.5 absolute w-full h-35 -left-4 md:-left-10 top-20vh -md:-mt-10 md:-top-15 opacity-3 md:opacity-5"/>
</div>
<div class="_slide">
<div class="text-4xl sm:text-5xl -md:-mt-2 mb-10 md:mb-20 font-special" style="--delay: 0">
Moritz Ruth
</div>
</div>
<div class="text-gray-800 text-2xl sm:text-3xl">
<ul class="asterisk-list">
<li class="_slide" style="--delay: 100">
{{ t("software_developer") }}
</li>
<li class="_slide" style="--delay: 200">
{{ t("designer") }}
</li>
<li class="_slide" style="--delay: 300">
{{ t("typography_enthusiast") }}
</li>
</ul>
</div>
<router-link class="mt-10 text-2xl sm:text-3xl block text-blue-900 _slide" style="--delay: 500" to="/contact">
<span class="mr-2"></span> {{ t("contact_me") }}
</router-link>
</div>
<div class="self-center -md:fixed -bottom-30">
<BlurredBlobCanvas
class="_fade-1"
:colors="['#6577fc', '#eb34cf', '#6577fc', '#eb34cf']"
:size="350"
:blur="30"
:minimum-duration="1000"
:duration-variation="1500"
:minimum-opacity="0.2"
:opacity-variation="0.4"
:points="10"
:randomness="80"
/>
</div>
</main>
</template>
<i18n lang="yaml">
en:
i_am: Im
software_developer: software developer
designer: web and print designer
typography_enthusiast: typography enthusiast
contact_me: Drop me a message!
de:
i_am: ""
software_developer: Software-Entwickler
designer: Web und Print-Designer
typography_enthusiast: Typographie-Enthusiast
contact_me: Schreib mir!
</i18n>
<style scoped>
._pattern {
background-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='36' height='72' viewBox='0 0 36 72'%3E%3Cg fill-rule='evenodd'%3E%3Cg \
fill='%23000000' fill-opacity='1'%3E%3Cpath d='M2 6h12L8 18 2 6zm18 36h12l-6 12-6-12z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
}
._fade-1 {
animation: fade 2s ease-in both;
}
._fade-2 {
animation: fade 2s 1s ease-out both;
}
._slide {
--delay: 0;
animation: slide-down 1s ease both;
animation-delay: calc(var(--delay) * 1ms);
}
@screen md {
._slide {
animation-delay: calc(var(--delay) * 1.4ms + 0.4s);
}
}
@keyframes fade {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slide-down {
from {
transform: translateY(-50%);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
</style>
<script>
import { useI18n } from "vue-i18n"
import BlurredBlobCanvas from "../components/BlurredBlobCanvas.vue"
export default {
name: "IndexPage",
components: { BlurredBlobCanvas },
setup() {
const { t } = useI18n()
return { t }
}
}
</script>

View file

@ -1,92 +0,0 @@
<template>
<div class="prose pt-20">
<h1>{{ t("heading") }}</h1>
<p>{{ t("message") }}</p>
<p>Stand: August 2021</p>
<section>
<h2>1. Generelles</h2>
<ol>
<li>Die nachfolgenden Bestimmungen gelten für alle zwischen Moritz Ruth und dem Auftraggeber geschlossenen Verträge.</li>
<li>Abweichende, widersprüchliche oder ergänzende Allgemeine Geschäftsbedingungen des Auftraggebers gelten nur nach ausdrücklicher, schriftlicher Zustimmung von Moritz Ruth.</li>
<li>Änderungen und Ergänzungen des Vertrags bedürfen der Schriftform, um wirksam zu sein.</li>
</ol>
</section>
<section>
<h2>2. Auftragsdurchführung</h2>
<ol>
<li>Moritz Ruth führt den Auftrag in eigener Verantwortung durch und hat Gestaltungsfreiheit im Rahmen dessen.</li>
<li>Handelt es sich um einen Werkvertrag, sind in der Konzeptions- und Entwurfsphase zwei Korrekturschleifen zulässig.</li>
<li>Die Mehrkosten durch eventuelle, von der Leistungsbeschreibung im Angebot abweichende Änderungswünsche hat der Auftraggeber zu tragen.</li>
<li>Wurde dem Auftraggeber die Fertigstellung einer Werkleistung in Textform mitgeteilt, hat er 14 Tage Zeit, die Abnahme zu verweigern. Tut er dies nicht, gilt die Leistung als abgenommen.</li>
<li>Moritz Ruth ist nicht verpflichtet, Quellcode und offene Dateien herauszugeben. Ist dies dennoch gewünscht, so muss es schriftlich vereinbart und gesondert vergütet werden.</li>
<li>Moritz Ruth bestimmt den Tätigkeitsort nach freiem Ermessen.</li>
<li>Moritz Ruth steht es frei, auch für andere Auftraggeber tätig zu werden.</li>
<li>Moritz Ruth unterliegt keinem Weisungs- oder Direktionsrecht des Auftraggebers.</li>
<li>Ein sozialversicherungspflichtiges Arbeitsverhältnis wird durch den Vertrag nicht begründet.</li>
<li>Moritz Ruth ist es gestattet, den Vertrag auch unter Zuhilfenahme von Erfüllungsgehilfen zu erfüllen.</li>
<li>Das Recht zum Rücktritt und Schadensersatz anstelle der ganzen Leistung besteht nur bei erheblichen Mängeln.</li>
</ol>
</section>
<section>
<h2>3. Pflichten des Auftraggebers</h2>
<ol>
<li>Der Auftraggeber ist verpflichtet, Moritz Ruth sämtliche zur Erfüllung des Vertrags erforderlichen Daten und Dateien zur Verfügung zu stellen.</li>
<li>Der Auftraggeber ist zum Ersatz von Schäden, die Moritz Ruth durch Schadprogramme in den vom Auftraggeber bereitgestellten Dateien und Daten entstehen, verpflichtet.</li>
<li>Das Anfertigen von Sicherungskopien liegt allein in der Verantwortung des Auftraggebers.</li>
<li>Ist zur Erfüllung des Auftrags die Mitwirkung des Auftraggebers erforderlich, muss dieser seiner Mitwirkungspflicht innerhalb von 14 Tagen nach Aufforderung durch Moritz Ruth nachkommen. Andernfalls kann Moritz Ruth den Vertrag kündigen. Sein Entschädigungsanspruch nach § 642 BGB bleibt hiervon unberührt.</li>
</ol>
</section>
<section>
<h2>4. Vergütung</h2>
<ol>
<li>Die Vergütung ist sofort zum Zeitpunkt der ordnungsgemäßen Rechnungsstellung fällig.</li>
<li>Grundsätzlich gilt eine Zahlungsfrist von 14 Tagen.</li>
</ol>
</section>
<section>
<h2>Nutzungsrechte</h2>
<ol>
<li>Moritz Ruth ist es erlaubt, die im Rahmen des Auftrags entstandenen Ergebnisse zur Selbstdarstellung zu verwenden (z. B. auf einer Website). Zudem darf er den Auftraggeber als Referenz nennen und in diesem Zuge auch dessen Logo verwenden.</li>
<li>Moritz Ruth überträgt nur die Nutzungsrechte, die zur Erfüllung des Vertrages erforderlich sind.</li>
</ol>
</section>
<section>
<h2>Haftung</h2>
<ol>
<li>Moritz Ruth haftet dem Auftraggeber gegenüber für die von ihm vorsätzlich oder grob fahrlässig verursachten Schäden sowie bei der Verletzung von Leben, Körper oder Gesundheit.</li>
<li>Moritz Ruth haftet nicht bei leichter Fahrlässigkeit, außer es handelt sich um die Verletzung wesentlicher Vertragspflichten nach den gesetzlichen Bestimmungen, wobei wesentliche Vertragspflichten solche sind, deren Erfüllung die ordnungsgemäße Durchführung des Vertrages überhaupt erst ermöglichen und auf deren Einhaltung der Auftraggeber regelmäßig vertrauen darf.</li>
</ol>
</section>
<section>
<h2>Schlussbestimmungen</h2>
<ol>
<li>Erfüllungsort und Gerichtsstand für alle Streitigkeiten aus und im Zusammenhang mit dieser Vereinbarung ist der Sitz von Moritz Ruth, sofern der Auftraggeber Kaufmann, juristische Person des öffentlichen Rechts oder öffentlich-rechtliches Sondervermögen ist.</li>
<li>Sollte eine Bestimmung dieser Vereinbarung ganz oder teilweise unwirksam sein oder werden oder sollte die Vereinbarung unvollständig sein, so wird die Vereinbarung im Übrigen Inhalt nicht berührt. Die Vertragspartner verpflichten sich, die unwirksame Bestimmung durch eine solche Bestimmung zu ersetzen, welche dem Sinn und Zweck der unwirksamen Bestimmung in rechtswirksamer Weise wirtschaftlich am nächsten kommt.</li>
<li>Es gilt deutsches Recht unter Ausschluss des UN-Kaufrechts.</li>
</ol>
</section>
</div>
</template>
<i18n lang="yaml">
en:
heading: Terms and Conditions
message: Only available in German at the moment.
de:
heading: Allgemeine Geschäftsbedingungen
message: ""
</i18n>
<script>
import { useI18n } from "vue-i18n"
export default {
name: "TermsPage",
setup() {
const { t } = useI18n()
return { t }
}
}
</script>

View file

@ -1,7 +0,0 @@
export function getComponentsOfHexColor(hexColorString) {
return [
Number.parseInt(hexColorString.slice(1, 3), 16),
Number.parseInt(hexColorString.slice(3, 5), 16),
Number.parseInt(hexColorString.slice(5, 7), 16)
]
}

View file

@ -1,22 +0,0 @@
import { watchEffect, onUnmounted, getCurrentInstance } from "vue"
const lockingInstances = new Set()
const update = () => {
document.body.style.overflowY = lockingInstances.size === 0 ? null : "hidden"
}
export function useWindowScrollLock(locked) {
const instance = getCurrentInstance()
watchEffect(() => {
if (locked.value) lockingInstances.add(instance)
else lockingInstances.delete(instance)
update()
})
onUnmounted(() => {
lockingInstances.delete(instance)
update()
})
}

9
tsconfig.json Normal file
View file

@ -0,0 +1,9 @@
{
// https://v3.nuxtjs.org/concepts/typescript
"extends": "./.nuxt/tsconfig.json",
"compilerOptions": {
"types": [
"nuxt-windicss"
]
}
}

View file

@ -1,17 +0,0 @@
import vuePlugin from "@vitejs/plugin-vue"
import windicssPlugin from "vite-plugin-windicss"
import pagesPlugin from "vite-plugin-pages"
import vueI18nPlugin from "@intlify/vite-plugin-vue-i18n"
/**
* https://vitejs.dev/config/
* @type {import('vite').UserConfig}
*/
export default {
plugins: [
vuePlugin(),
vueI18nPlugin(),
pagesPlugin(),
windicssPlugin()
]
}

View file

@ -1,17 +0,0 @@
import { defineConfig } from "vite-plugin-windicss"
export default defineConfig({
theme: {
fontFamily: {
sans: ["Plus Jakarta Sans", "sans-serif"],
special: ["Syne", "monospace"]
},
extend: {
colors: {
blue: {
900: "#0041ff"
}
}
}
}
})

25
windi.config.ts Normal file
View file

@ -0,0 +1,25 @@
import { defineConfig } from "windicss/helpers"
import colors from "windicss/colors"
import interactionVariantsPlugin from "@windicss/plugin-interaction-variants"
export default defineConfig({
darkMode: "media",
theme: {
colors: {
pink: colors.fuchsia,
red: colors.rose,
yellow: colors.amber,
green: colors.green,
blue: colors.blue,
dark: colors.dark,
light: colors.light
},
fontFamily: {
sans: ["Plus Jakarta Sans", "sans-serif"],
special: ["SyneVariable", "monospace"]
}
},
plugins: [
interactionVariantsPlugin
]
})