1
0
Fork 0

Switch to pnpm, add home page and navigation menu

This commit is contained in:
Moritz Ruth 2021-06-19 18:02:46 +02:00
parent 2ae875cb45
commit e86f299580
21 changed files with 3209 additions and 2173 deletions

View file

@ -1,4 +1,7 @@
{ {
"root": true, "root": true,
"extends": "awzzm-vue/v3" "extends": "awzzm-vue/v3",
"rules": {
"vue/no-static-inline-styles": "off"
}
} }

2
.nvmrc
View file

@ -1 +1 @@
15 16

View file

@ -1,4 +1,3 @@
# moritzruth.de # moritzruth.de
> My personal website
:fire: [**moritzruth.de**](https://moritzruth.de) 🔥 [**moritzruth.de**](https://moritzruth.de)

View file

@ -4,12 +4,14 @@
<meta charset="utf-8"/> <meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Moritz Ruth</title> <title>Moritz Ruth</title>
<link rel="stylesheet" href="https://rsms.me/inter/inter.css"> <link rel="stylesheet" href="/node_modules/@fontsource/plus-jakarta-sans/400.css">
<meta name="description" content="I do web and print things, sometimes photography."> <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"> <meta name="keywords" content="web, dev, development, coding, moritz, ruth, development, design">
<link rel="shortcut icon" type="image/png" href="/icon.png"> <link rel="shortcut icon" type="image/png" href="/icon.png">
</head> </head>
<body> <body class="bg-[#fefefe]">
<div id="app"></div> <div id="app"></div>
<script type="module" src="/src/main.js"></script> <script type="module" src="/src/main.js"></script>
<script <script

View file

@ -2,22 +2,28 @@
"name": "moritzruth.de", "name": "moritzruth.de",
"author": "Moritz Ruth <dev@moritzruth.de>", "author": "Moritz Ruth <dev@moritzruth.de>",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite --host",
"build": "vite build", "build": "vite build",
"serve": "vite preview", "serve": "vite preview",
"lint": "eslint . --fix" "lint": "eslint . --fix"
}, },
"dependencies": { "dependencies": {
"@zhuowenli/vue-feather-icons": "^5.0.2", "@fontsource/plus-jakarta-sans": "^4.4.5",
"vue": "^3.0.11" "@fontsource/syne": "^4.4.5",
"@vueuse/core": "^5.0.3",
"blobs": "^2.2.1-beta.1",
"kute.js": "^2.1.2",
"vue": "^3.1.1",
"vue-router": "4"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^1.2.2", "@vitejs/plugin-vue": "^1.2.3",
"@vue/compiler-sfc": "^3.0.11", "@vue/compiler-sfc": "^3.1.1",
"eslint": "^7.25.0", "eslint": "^7.28.0",
"eslint-config-awzzm-vue": "^1.6.0", "eslint-config-awzzm-vue": "^1.6.0",
"vite": "^2.2.3", "vite": "^2.3.7",
"vite-plugin-windicss": "^0.15.10", "vite-plugin-pages": "^0.13.1",
"windicss": "^2.5.14" "vite-plugin-windicss": "^1.0.4",
"windicss": "^3.1.3"
} }
} }

2705
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,2 +1 @@
https://moritz-ruth.de/* https://moritzruth.de/:splat 301! https://moritz-ruth.de/* https://moritzruth.de/:splat 301!
/impressum /impressum.txt 200

View file

@ -1,3 +1,2 @@
User-agent: * User-agent: *
Disallow: /impressum Disallow: /impressum
Disallow: /impressum.txt

View file

@ -1,59 +1,23 @@
<template> <template>
<div class="bg-black w-screen h-screen text-white overflow-hidden flex justify-center items-center"> <NavigationMenu/>
<div class="px-8 mx-auto max-w-4xl w-full h-96 font-extrabold relative"> <div class="overflow-x-hidden">
<div class="app__circle w-52 h-52 rounded-full bg-gradient-to-bl from-yellow-300 to-yellow-800 mr-15"/> <div class="bg-white text-black max-w-1200px mx-auto px-6 sm:px-10">
<div class="app__box w-96 h-44 bg-gradient-to-bl from-yellow-300 to-yellow-800 rotate-12 transform"/> <router-view/>
<div class="text-6xl sm:text-8xl font-black relative -left-1">
Moritz Ruth
</div>
<div class="flex mt-8 justify-items-stretch">
<div class="w-1 bg-gradient-to-b from-yellow-300 to-yellow-500"/>
<div class="text-2xl sm:text-4xl ml-5 sm:ml-8 space-y-2 pb-1">
<p>I do web and print things.</p>
</div>
</div>
</div>
<div class="fixed flex bottom-15 sm:bottom-8 space-x-5">
<a class="app__social-link" href="https://twitter.com/moritz_ruth" aria-label="Twitter"><TwitterIcon size="1x"/></a>
<a class="app__social-link" href="https://github.com/moritzruth" aria-label="GitHub"><GithubIcon size="1x"/></a>
<a class="app__social-link" href="mailto:hey@m0.is" aria-label="Email"><MailIcon size="1x"/></a>
</div>
<div class="fixed bottom-0 right-0 p-2 sm:p-8 sm:text-gray-400 font-bold">
<a href="/impressum" target="_blank">Impressum</a>
</div> </div>
</div> </div>
</template> </template>
<style scoped> <style>
body { body {
background: rgba(255, 255, 255, 0.2); font-size: 20px;
}
.app__circle {
position: absolute;
top: -10em;
right: -15vw;
}
.app__box {
position: absolute;
top: 25em;
left: -25vw;
}
.app__social-link {
@apply text-black text-3xl p-4 bg-white bg-opacity-50 from-yellow-300 to-yellow-500 -sm:rounded-full flex items-center justify-center
transition duration-200 sm:bg-opacity-0 sm:text-white sm:hover:bg-opacity-30;
backdrop-filter: blur(5px);
} }
</style> </style>
<script> <script>
import { TwitterIcon, MailIcon, GithubIcon } from "@zhuowenli/vue-feather-icons" import NavigationMenu from "./components/NavigationMenu.vue"
export default { export default {
name: "App", name: "App",
components: { TwitterIcon, GithubIcon, MailIcon } components: { NavigationMenu }
} }
</script> </script>

View file

@ -0,0 +1,132 @@
<template>
<svg :style="{ width: size, height: size, filter: `blur(${blur}px)` }">
<path
v-for="(blob, index) in blobs"
:key="index"
:ref="blob"
style="transition: 2000ms; transition-property: opacity, transform; transform-origin: center"
d=""
/>
</svg>
</template>
<script>
import * as blobs2 from "blobs/v2"
import KUTE from "kute.js"
import { ref } from "vue"
import { getListOfIndexes } from "../getListOfIndexes.js"
const BLOB_OPTIONS = {
extraPoints: 6,
randomness: 30
}
// let lastMouseEvent = null
// let currentMouseEvent = null
// let mouseSpeed = 0
//
// window.addEventListener("mousemove", event => {
// currentMouseEvent = event
// })
//
// setInterval(() => {
// if (currentMouseEvent !== null && lastMouseEvent !== null && currentMouseEvent !== lastMouseEvent) {
// const deltaX = currentMouseEvent.screenX - lastMouseEvent.screenX
// const deltaY = currentMouseEvent.screenY - lastMouseEvent.screenY
// mouseSpeed = Math.sqrt((deltaX * deltaX) + (deltaY * deltaY))
// } else {
// mouseSpeed = 0
// }
//
// lastMouseEvent = currentMouseEvent
// console.log(mouseSpeed)
// }, 100)
export default {
name: "BlurredBlobCanvas",
props: {
colors: {
type: Array,
required: true
},
size: {
type: Number,
required: true
},
blur: {
type: Number,
default: 30
},
minimumDuration: {
type: Number,
required: true
},
durationVariation: {
type: Number,
default: 0
},
minimumOpacity: {
type: Number,
required: true
},
opacityVariation: {
type: Number,
default: 0
}
},
data() {
return {
// eslint-disable-next-line unicorn/no-new-array
blobs: getListOfIndexes(this.colors.length).map(() => ref(null))
}
},
mounted() {
const animate = index => {
const blob = this.blobs[index]
const duration = (Math.random() * this.durationVariation) + this.minimumDuration
if (blob.value) {
KUTE.to(
blob.value,
{
path: blobs2.svgPath(this.getRandomBlobOptions())
},
{
duration,
morphPrecision: 5,
easing: "easingSinusoidalInOut",
onComplete() {
animate(index)
}
}
).start()
this.setOpacity(blob.value)
}
}
requestAnimationFrame(() => {
this.blobs.forEach((blob, index) => {
blob.value.setAttribute("d", blobs2.svgPath(this.getRandomBlobOptions()))
blob.value.setAttribute("fill", this.colors[index])
this.setOpacity(blob.value)
animate(index)
})
})
},
methods: {
getRandomBlobOptions() {
return {
...BLOB_OPTIONS,
seed: Math.random(),
size: this.size
}
},
setOpacity(element) {
element.style.opacity = (Math.random() * this.opacityVariation) + this.minimumOpacity
}
}
}
</script>

View file

@ -0,0 +1,55 @@
<template>
<div ref="wrapper" class="select-none">
<slot :setTrigger="setTrigger"/>
</div>
</template>
<script>
import { ref, watchEffect } from "vue"
import KUTE from "kute.js"
export default {
name: "ClickToBounce",
setup() {
const trigger = ref(null)
const wrapper = ref(null)
let current = null
watchEffect(onInvalidate => {
if (trigger.value !== null) {
const listener = () => {
current?.stop()
current = KUTE.to(wrapper.value, { scale: 0.9 }, {
duration: 100,
easing: "easingElasticInOut",
onComplete() {
current = KUTE.to(wrapper.value, { scale: 1 }, {
duration: 100,
easing: "easingElasticInOut",
onComplete() {
current = null
}
}).start()
}
}).start()
}
trigger.value.addEventListener("click", listener, { passive: true })
onInvalidate(() => {
trigger.value.removeEventListener("click", listener)
})
}
})
return {
setTrigger(element) {
trigger.value = element
},
wrapper
}
}
}
</script>

View file

@ -0,0 +1,104 @@
<template>
<div class="sm:hidden fixed z-101 bottom-3 right-3 rounded-full backdrop-filter backdrop-blur-20 bg-white bg-opacity-20 w-20 h-20 flex justify-center items-center shadow-lg">
<div role="button" class="flex flex-col justify-evenly items-center h-10" @click="active = !active">
<div class="w-10 h-2px bg-black transition duration-200 transform" :style="`transform: ${active ? 'translateY(350%)' : ''} rotate(${active ? 45 : 0}deg)`"/>
<div class="w-10 h-2px bg-black transition duration-200 transform" :style="`transform: ${active ? 'translateY(-350%)' : ''} rotate(${active ? -45 : 0}deg)`"/>
</div>
</div>
<nav
class="fixed sm:sticky top-0 z-100 w-screen h-screen sm:h-20 backdrop-filter backdrop-blur-40 bg-white bg-opacity-40 transition duration-200"
: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="fixed transition-all duration-500" :style="{ left: blobState.x + 'px', top: blobState.y + 'px', opacity: blobState.show ? 1 : 0 }">
<BlurredBlobCanvas
:colors="['#eb34cf', '#79faff']"
:opacity-variation="0"
:minimum-opacity="0.9"
:minimum-duration="1000"
:duration-variation="500"
:blur="10"
:size="100"
/>
</div>
<router-link class="uppercase font-special relative top-1 -sm:mt-20" 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"
>
{{ item.label }}
</router-link>
</div>
</div>
</nav>
</template>
<script>
import { useWindowScroll, useWindowSize } from "@vueuse/core"
import { computed, ref, watch, reactive } from "vue"
import { useRoute } from "vue-router"
import { useWindowScrollLock } from "../utils/useWindowScrollLock.js"
import BlurredBlobCanvas from "./BlurredBlobCanvas.vue"
const ITEMS = [
{
label: "Projects",
to: "/projects"
},
{
label: "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: 20,
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) - 20
blobState.y = y - 20
blobState.show = true
}
}, { immediate: true })
return {
scrolled: computed(() => windowScroll.value > 0),
blobState,
items,
active
}
}
}
</script>

2
src/getListOfIndexes.js Normal file
View file

@ -0,0 +1,2 @@
// eslint-disable-next-line unicorn/no-new-array
export const getListOfIndexes = size => [...new Array(size).keys()]

View file

@ -1,7 +1,14 @@
import "virtual:windi.css" import "virtual:windi.css"
import routes from "virtual:generated-pages"
import { createApp } from "vue" import { createApp } from "vue"
import { createRouter, createWebHistory } from "vue-router"
import App from "./App.vue" import App from "./App.vue"
createApp(App).mount("#app") const router = createRouter({
history: createWebHistory(),
routes
})
if (window.location.path !== "/") history.replaceState({}, "", "/") createApp(App)
.use(router)
.mount("#app")

11
src/pages/contact.vue Normal file
View file

@ -0,0 +1,11 @@
<template>
<div>
contact
</div>
</template>
<script>
export default {
name: "ContactPage"
}
</script>

113
src/pages/index.vue Normal file
View file

@ -0,0 +1,113 @@
<template>
<main class="flex -md:flex-col justify-between items-start md:items-center min-h-80vh">
<div class="font-bold relative -md:pt-20vh -md:pb-20 transform skew-x-7 -skew-y-7 rotate-7">
<div class="_fade-2">
<div class="_pattern transform rotate-179 absolute w-full h-40 -left-4 md:-left-10 top-20vh -md:-mt-10 md:-top-10 opacity-3 md:opacity-5"/>
</div>
<div class="_slide">
<ClickToBounce v-slot="{ setTrigger }" class="text-6xl md:text-7xl -md:-mt-2 mb-30 font-special" style="--delay: 0">
Hey <span :ref="setTrigger" class="_clap">👋</span>
</ClickToBounce>
</div>
<div class="font-special text-3xl sm:text-5xl mb-5 _slide" style="--delay: 200">
Im<br>
<span class="text-1.1em text-transparent bg-clip-text bg-gradient-to-b from-blue-500 to-blue-900">Moritz Ruth</span>
</div>
<div class="text-gray-800 text-2xl sm:text-3xl">
<ul class="_list">
<li class="_slide" style="--delay: 400">
software developer
</li>
<li class="_slide" style="--delay: 600">
graphic designer
</li>
<li class="_slide" style="--delay: 800">
typography enthusiast
</li>
</ul>
</div>
</div>
<div class="self-center">
<BlurredBlobCanvas
class="_fade-1"
:colors="['#6577fc', '#eb34cf', '#6577fc', '#eb34cf']"
:size="300"
:blur="30"
:minimum-duration="2000"
:duration-variation="500"
:minimum-opacity="0.2"
:opacity-variation="0.4"
/>
</div>
</main>
</template>
<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");
}
._list > li::before {
@apply text-blue-900;
content: "*";
}
/* created using emojicursor.app */
._clap {
cursor: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='50' height='90' viewport='0 0 100 100' style='fill:black;font-size:50px;'><text y='50%'>\
</text></svg>") 16 0,auto;
}
._fade-1 {
animation: fade 2s ease-in both;
}
._fade-2 {
animation: fade 2s 2s 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 + 1.5s);
}
}
@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 BlurredBlobCanvas from "../components/BlurredBlobCanvas.vue"
import ClickToBounce from "../components/ClickToBounce.vue"
export default {
name: "IndexPage",
components: { ClickToBounce, BlurredBlobCanvas }
}
</script>

11
src/pages/projects.vue Normal file
View file

@ -0,0 +1,11 @@
<template>
<div>
projects
</div>
</template>
<script>
export default {
name: "ProjectsPage"
}
</script>

View file

@ -0,0 +1,22 @@
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()
})
}

View file

@ -1,5 +1,6 @@
import vuePlugin from "@vitejs/plugin-vue" import vuePlugin from "@vitejs/plugin-vue"
import windicssPlugin from "vite-plugin-windicss" import windicssPlugin from "vite-plugin-windicss"
import pagesPlugin from "vite-plugin-pages"
/** /**
* https://vitejs.dev/config/ * https://vitejs.dev/config/
@ -8,6 +9,7 @@ import windicssPlugin from "vite-plugin-windicss"
export default { export default {
plugins: [ plugins: [
vuePlugin(), vuePlugin(),
pagesPlugin(),
windicssPlugin() windicssPlugin()
] ]
} }

View file

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

2108
yarn.lock

File diff suppressed because it is too large Load diff