Compare commits

...
37 Commits
Author SHA1 Message Date
endermanch 97c0b7c5ed Upgrade to Nuxt 4 folder structure 2025-01-27 00:04:14 +03:00
endermanch ad70bdc5b6 Add nuxt-image and remove old image directory 2024-10-14 13:17:44 +03:00
endermanch a7c03d2327 Optimize images (PNG → AVIF) 2024-10-14 12:37:16 +03:00
endermanch 673948bce9 Add loading indicator 2024-06-20 00:33:16 +03:00
endermanch 7a3cdcc1da Wage a war against nuxt-simple-robots. BAD MODULE 2024-06-19 19:21:55 +03:00
endermanch 44469bd9f2 Fix robots & sitemap SEO disaster 2024-06-19 19:05:58 +03:00
endermanch 00060ede92 Bump version to 0.3.8 2024-06-19 18:08:22 +03:00
endermanch 2204709c0e Fix light theme, fix animation bugs, add programmatic scrolling, improve custom card blog component 2024-06-19 18:06:17 +03:00
endermanch 3e39bda014 Pass component into hyperscript 2024-06-19 02:07:17 +03:00
endermanch 05db0b693e Fix defineOgImageComponent, remove swipe actions in reader mode, change onClick to addEventListener 2024-06-19 01:19:57 +03:00
endermanch 4543680a92 Bump version, refine element hydration, fix animation, fix title in blog post 2024-06-19 00:45:05 +03:00
endermanch 858142f09a Unstage cache 2024-06-18 22:00:54 +03:00
endermanch 830c34855c Clean up styles, fix and improve the blog post card layout 2024-06-18 21:48:50 +03:00
endermanch a7f2d527dc % → dvh 2024-06-16 23:18:58 +03:00
endermanch e72a61b317 Rewrite to TailwindCSS, enhance the DOM, uninstall Vuetify, improve styles 2024-06-16 22:24:01 +03:00
endermanch 9339416c1b Upgrade to Nuxt 3.12, simplify logic 2024-06-15 13:14:34 +03:00
endermanch 753bdb5c0a Upload blog post 2024-03-02 16:00:21 +03:00
endermanch bfeaca5f09 Upload blog post thumbnail 2024-03-02 15:53:47 +03:00
endermanch 10e3a7fc40 Update blog post 2024-03-02 15:50:17 +03:00
endermanch 1ed1d47b07 Update blog 2024-03-02 15:48:39 +03:00
endermanch d1e249b51d Fix the 'Page not found' bug 2024-02-20 22:47:01 +03:00
endermanch 7ce8674fea Add public images for blog 2024-02-20 22:20:04 +03:00
endermanch 36dd770ce4 Version 0.2.1 2024-02-20 22:14:58 +03:00
endermanch eadc0b119d Raw blog finished 2024-02-20 21:15:29 +03:00
endermanch 44c60f7069 Blog slowly looming over 2024-02-19 22:49:55 +03:00
endermanch b58407d358 Added new icons, improved the blog 2024-02-19 01:18:50 +03:00
endermanch 9386c1d158 Remove v-if prop anti-pattern, clean up, add reader mode 2024-02-18 14:36:50 +03:00
endermanch 3853e8fa20 Created blog page, many improvements 2024-02-18 11:30:50 +03:00
endermanch cfbaf56159 Finished integrating Shadow DOM icons, first steps towards dynamic theme 2024-01-28 21:22:05 +03:00
endermanch 014e64d174 Move over to iconify-icon, wrap around 2024-01-27 23:29:31 +03:00
endermanch 1f7c8f0ed4 Move to appConfig, Vuetify 3 2024-01-27 19:17:30 +03:00
endermanch 4ac1fd9574 Added options, preparing for Quasar rewrite 2024-01-23 23:53:21 +03:00
endermanch dd084afb61 Fixed minor CSS bugs 2023-11-27 14:49:57 +03:00
endermanch 3c69f73dc6 0.1.3 2023-11-25 20:10:23 +03:00
endermanch 92ad0a50f9 0.1.2 2023-11-25 20:10:21 +03:00
endermanch 4939d24f62 Dockerized the application, fixed fonts, improved SEO, upgraded to Nuxt 3.8.2 2023-11-25 19:52:25 +03:00
endermanch 2bd1e4a512 Took my time to tweak linter configurations 2023-11-23 18:14:23 +03:00
162 changed files with 3504 additions and 37942 deletions
-5
View File
@@ -1,5 +0,0 @@
> 1%
last 5 major versions
not dead
not ie <= 11
not op_mini all
+10
View File
@@ -0,0 +1,10 @@
# .dockerignore
/.git
/.nuxt
/.output
/node_modules
.gitignore
README.md
.stylelintcache
-23
View File
@@ -1,23 +0,0 @@
module.exports = {
root: true,
env: {
browser: true,
node: true,
},
extends: [
'@nuxtjs/eslint-config-typescript',
'plugin:nuxt/recommended',
'prettier',
],
plugins: [],
rules: {
indent: ['error', 2],
quotes: [2, 'single', { avoidEscape: true }],
'no-console': 'off',
'vue/no-multiple-template-root': 'off',
'vue/multi-word-component-names': 'off',
},
settings: {
'import/ignore': ['vue-fontawesome'],
},
}
+3
View File
@@ -9,6 +9,9 @@ dist
# Node dependencies # Node dependencies
node_modules node_modules
# Cache
.stylelintcache
# Logs # Logs
logs logs
*.log *.log
-96
View File
@@ -1,96 +0,0 @@
###
# Place your Prettier ignore content here
###
# .gitignore content is duplicated here due to https://github.com/prettier/prettier/issues/8506
# Created by .ignore support plugin (hsz.mobi)
### Node template
# Logs
/logs
*.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 generate
dist
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless
# IDE / Editor
.idea
# Service worker
sw.*
# macOS
.DS_Store
# Vim swap files
*.swp
-6
View File
@@ -1,6 +0,0 @@
{
"semi": false,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "all"
}
-1
View File
@@ -1 +0,0 @@
[{"D:\\Software\\Development\\Websites\\enderman.ch\\index\\assets\\styles\\list-types.scss":"1","D:\\Software\\Development\\Websites\\enderman.ch\\index\\assets\\styles\\transitions.scss":"2","D:\\Software\\Development\\Websites\\enderman.ch\\index\\components\\Logo.vue":"3","D:\\Software\\Development\\Websites\\enderman.ch\\index\\components\\LinkButton.vue":"4","D:\\Software\\Development\\Websites\\enderman.ch\\index\\components\\TransitionY.vue":"5","D:\\Software\\Development\\Websites\\enderman.ch\\index\\app.vue":"6","D:\\Software\\Development\\Websites\\enderman.ch\\index\\components\\EMail.vue":"7","D:\\Software\\Development\\Websites\\enderman.ch\\index\\components\\Navigation.vue":"8","D:\\Software\\Development\\Websites\\enderman.ch\\index\\components\\Separator.vue":"9","D:\\Software\\Development\\Websites\\enderman.ch\\index\\components\\Flooter.vue":"10","D:\\Software\\Development\\Websites\\enderman.ch\\index\\components\\Portal.vue":"11","D:\\Software\\Development\\Websites\\enderman.ch\\index\\pages\\about.vue":"12","D:\\Software\\Development\\Websites\\enderman.ch\\index\\pages\\index.vue":"13","D:\\Software\\Development\\Websites\\enderman.ch\\index\\pages\\projects.vue":"14","D:\\Software\\Development\\Websites\\enderman.ch\\index\\pages\\social.vue":"15"},{"size":389,"mtime":1700689887460,"hashOfConfig":"16"},{"size":165,"mtime":1700688268778,"hashOfConfig":"16"},{"size":1113,"mtime":1700251498711,"hashOfConfig":"16"},{"size":986,"mtime":1700324714794,"hashOfConfig":"16"},{"size":1513,"mtime":1700430002840,"hashOfConfig":"16"},{"size":2315,"mtime":1700689012136,"hashOfConfig":"16"},{"size":804,"mtime":1700688952880,"hashOfConfig":"16"},{"size":1183,"mtime":1700689030040,"hashOfConfig":"16"},{"size":300,"mtime":1700689033070,"hashOfConfig":"16"},{"size":1853,"mtime":1700508715727,"hashOfConfig":"16"},{"size":16880,"mtime":1700502035614,"hashOfConfig":"16"},{"size":2135,"mtime":1700689265787,"hashOfConfig":"16"},{"size":3117,"mtime":1700689271383,"hashOfConfig":"16"},{"size":2304,"mtime":1700689265769,"hashOfConfig":"16"},{"size":3232,"mtime":1700689265781,"hashOfConfig":"16"},"5tgxr3"]
-2
View File
@@ -1,2 +0,0 @@
node_modules/
assets/styles/**/*.css
-11
View File
@@ -1,11 +0,0 @@
module.exports = {
defaultSeverity: 'warning',
extends: [
'stylelint-config-standard-scss',
'stylelint-config-recommended-vue',
],
plugins: [],
rules: {
'declaration-empty-line-before': null,
},
}
-55
View File
@@ -1,55 +0,0 @@
<script setup lang="ts">
import { useQuasar } from 'quasar'
const quasar = useQuasar()
useHead({
titleTemplate: (chunk) => {
return !chunk || chunk === 'Enderman' ? 'Enderman' : `${chunk} – Enderman`
},
})
</script>
<template>
<NuxtLoadingIndicator color="purple" />
<div
id="ender-layout"
class="d-flex flex-column align-items-center pt-sm-5 h-100 animate__animated-sm animate__delay-1-5s animate__fadeInDown"
>
<NuxtLayout name="card">
<template #header> </template>
<template #footer>
<ClientOnly>
<template #fallback> </template>
<Transition
appear
enter-active-class="animate__animated animate__delay-1s animate__fadeIn"
leave-active-class="animate__animated animate__fadeOut"
mode="out-in"
>
<Flooter
v-if="
quasar.screen.width && quasar.screen.height && quasar.screen.xs
"
opaque
/>
</Transition>
</ClientOnly>
</template>
</NuxtLayout>
</div>
<Portal layout="#ender-layout" animate randomize fade />
<Transition
appear
enter-active-class="animate__animated animate__delay-1s animate__fadeInUpBig animate__slow"
leave-active-class="animate__animated animate__fadeOutDown"
mode="out-in"
>
<Flooter
v-if="quasar.screen.width && quasar.screen.height && !quasar.screen.xs"
class="floaty mb-2 px-2"
/>
</Transition>
</template>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

-22
View File
@@ -1,22 +0,0 @@
@charset "UTF-8";
@counter-style do {
system: cyclic;
symbols: "💻" "📌" "👻" "📷";
suffix: " ";
}
@counter-style enjoy {
system: cyclic;
symbols: "💻" "📌" "👻" "📷" "♟" "⛄";
suffix: " ";
}
.list-style-type-none {
list-style-type: none;
}
.list-style-type-do {
list-style-type: do;
}
.list-style-type-enjoy {
list-style-type: enjoy;
}
/*# sourceMappingURL=list-types.css.map */
-1
View File
@@ -1 +0,0 @@
{"version":3,"sourceRoot":"","sources":["list-types.scss"],"names":[],"mappings":";AAAA;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAIA;EACE;;AAGF;EACE;;AAGF;EACE","file":"list-types.css"}
-25
View File
@@ -1,25 +0,0 @@
@counter-style do {
system: fixed;
symbols: "\1F4BB" "\1F4CC" "\1F47B" "\1F4F7";
suffix: " ";
}
@counter-style enjoy {
system: fixed;
symbols: "\2728" "\1F4A1" "\1F4BE" "\1F3A5" "\1F4D5" "\1F30D" "\2615" "\26C4";
suffix: " ";
}
.list-style-type {
&-none {
list-style-type: none;
}
&-do {
list-style-type: do;
}
&-enjoy {
list-style-type: enjoy;
}
}
-15791
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-15
View File
File diff suppressed because one or more lines are too long
-269
View File
@@ -1,269 +0,0 @@
// Fonts.
@import '@fontsource/Lato/index.css';
@import '@fontsource/Alexandria/700.css';
$grid-breakpoints: (
xs: 0,
sm: 601px,
md: 996px,
lg: 1281px,
xl: 1921px,
xxl: 2561px
);
// Bootstrap & animations.
@import '../../node_modules/bootstrap/scss/bootstrap';
@import '../../node_modules/animate.css/animate';
@import 'transitions';
@import 'list-types';
// CSS Variables.
:root {
--animate-duration: 1s;
--animate-delay: 0.75s;
--animate-repeat: 1;
}
// The HTML page.
html {
height: 100%;
}
// The body element.
body {
display: flex;
flex-direction: column;
background-color: rgb(6 25 28);
background-image: url('~/assets/images/sky.png');
background-attachment: fixed;
background-blend-mode: multiply;
background-size: cover;
background-repeat: no-repeat;
image-rendering: pixelated;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: white;
font-family: Lato, sans-serif;
font-size: 18px;
height: 100%;
}
// Entry point (Vue mount).
#ender-app {
flex: 1;
image-rendering: auto;
}
// Helper classes that don't exist in Bootstrap.
.no-decoration {
text-decoration: none;
color: inherit;
}
.text-align-center {
text-align: center;
}
.font-small {
font-size: 14px;
}
.grid-cols-3 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.display-xs-none {
display: none;
}
.alex {
font-family: Alexandria, sans-serif;
}
.smooth-glide {
transition: height 0.4s linear;
}
// Query-overridable classes.
.background {
background-color: rgb(0 0 0 / 50%);
}
.dimensions {
width: 100%;
min-height: 100%;
}
.link {
color: inherit;
text-decoration: none;
transition: 0.3s ease;
&:hover {
color: cornflowerblue;
}
&-hi {
@extend .link;
color: cornflowerblue;
&:hover {
color: royalblue;
}
&-force-dark {
@extend .link;
color: cornflowerblue;
&:hover {
color: royalblue;
}
}
}
}
.radial-gradient {
background: radial-gradient(
circle at left,
rgb(255 255 255 / 80%),
rgb(255 255 255 / 75%),
rgb(255 255 255 / 0%)
);
}
.scrollbar {
min-height: 128px;
scrollbar-width: thin;
scrollbar-color: rgb(255 255 255 / 15%) rgb(0 0 0 / 20%);
&::-webkit-scrollbar {
width: 8px;
}
&::-webkit-scrollbar-track {
border-radius: 8px;
background-color: rgb(0 0 0 / 20%);
}
&::-webkit-scrollbar-thumb {
border-radius: 8px;
background-color: rgb(255 255 255 / 15%);
}
}
@media (prefers-color-scheme: light) {
body {
color: black;
}
.scrollbar {
scrollbar-color: rgb(0 0 0 / 20%) rgb(255 255 255 / 20%);
&::-webkit-scrollbar-track {
background-color: rgb(255 255 255 / 20%);
}
&::-webkit-scrollbar-thumb {
border-radius: 8px;
background-color: rgb(0 0 0 / 20%);
}
}
.background {
background-color: rgb(255 255 255 / 80%);
}
.link {
&:hover {
color: royalblue;
}
&-hi {
color: royalblue;
&:hover {
color: darkblue;
}
}
}
.radial-gradient {
background: radial-gradient(
circle at left,
rgb(0 0 0 / 95%),
rgb(0 0 0 / 80%),
rgb(0 0 0 / 0%)
);
}
}
.floaty {
position: fixed;
bottom: 0;
left: 0;
right: 0;
width: fit-content;
height: fit-content;
margin: auto;
}
// Dynamic classes.
@include media-breakpoint-up(sm) {
#ender-app {
height: 100%;
}
.scrollbar {
scrollbar-gutter: stable;
}
.dimensions {
width: 80%;
min-height: fit-content;
max-height: 90%;
}
.fade-mask-sm {
mask-image: linear-gradient(to bottom, rgb(0 0 0 / 0%), rgb(0 0 0 / 100%) 5%, rgb(0 0 0 / 100%) 95%, rgb(0 0 0 / 0%));
}
.rounded-1 {
border-radius: 1em;
}
.display-xs-none {
display: block;
}
.animate__animated-sm {
animation-duration: var(--animate-duration);
animation-fill-mode: both;
}
.animate__delay-1-5s {
animation-delay: calc(var(--animate-delay) * 1.5);
}
}
@include media-breakpoint-up(md) {
}
@include media-breakpoint-up(lg) {
.dimensions {
width: 60%;
}
}
@include media-breakpoint-up(xl) {
.dimensions {
width: 40%;
}
}
@include media-breakpoint-up(xxl) {
.dimensions {
width: 30%;
}
}
-9
View File
@@ -1,9 +0,0 @@
.page-enter-active, .page-leave-active {
transition: all 0.4s;
}
.page-enter-from, .page-leave-to {
opacity: 0;
filter: blur(1rem);
}
/*# sourceMappingURL=transitions.css.map */
-1
View File
@@ -1 +0,0 @@
{"version":3,"sourceRoot":"","sources":["transitions.scss"],"names":[],"mappings":"AACE;EAEE;;AAGF;EAEE;EACA","file":"transitions.css"}
-12
View File
@@ -1,12 +0,0 @@
.page {
&-enter-active,
&-leave-active {
transition: all 0.15s;
}
&-enter-from,
&-leave-to {
opacity: 0;
filter: blur(1em);
}
}
-65
View File
@@ -1,65 +0,0 @@
<script setup lang="ts">
import { useRuntimeConfig } from '#imports'
const props = defineProps({
opaque: Boolean,
})
const config = useRuntimeConfig()
const currentYear = new Date().getFullYear()
const mailTemplate = `I've just found a bug on https://enderman.ch and would like to report it.%0D%0A%0D%0AWebsite version: ${
config.public.version
}%0D%0ABuild date: ${
config.public.buildDate
}%0D%0ATimestamp: ${new Date().toISOString()}%0D%0A%0D%0A%0D%0ASteps to reproduce:%0D%0A{ Explain in detail what happened here, or attach a video/screenshot }%0D%0A%0D%0AAdditional information:%0D%0A{ Helpful information, such as developer console output / Leave empty if none }%0D%0A%0D%0A%0D%0A// Keep in mind that it's just a template, you can change it as you wish! :)`
</script>
<template>
<footer class="user-select-none text-align-center line-height-1-5">
<span
class="font-small"
:class="{ transpaque: !props.opaque, 'text-shadow': !props.opaque }"
>
<EMail
class="link-hi-force-dark"
address="[email protected]"
cc="[email protected]"
subject="Bug report: enderman.ch"
:body="mailTemplate"
>
<strong>Report a bug</strong>
</EMail>
<br />
© 2018-{{ currentYear }},
<a class="link-hi-force-dark" href="https://enderman.ch">Enderman</a>. All
rights reserved.
<sub>
β{{ config.public.version ? config.public.version : '?.?.?' }} ({{
config.public.buildDate ? config.public.buildDate : '1970-01-01'
}})
</sub>
</span>
</footer>
</template>
<style scoped lang="scss">
.transpaque {
color: white;
opacity: 0.25;
transition: ease 0.3s;
}
.transpaque:hover {
opacity: 1;
}
.line-height-1-5 {
line-height: 14px * 1.5;
}
.text-shadow {
text-shadow: black 0 2px 5px;
}
</style>
-64
View File
@@ -1,64 +0,0 @@
<script setup lang="ts">
const props = defineProps({
src: {
type: String,
required: true,
},
alt: {
type: String,
default: 'Logo',
},
width: {
type: Number,
default: 100,
},
height: {
type: Number,
default: 100,
},
title: {
type: String,
default: 'Title',
},
description: {
type: String,
default: 'Description',
},
})
</script>
<template>
<NuxtLink class="no-decoration user-select-none" to="/">
<div
class="logo d-flex flex-row align-items-center justify-content-sm-center"
>
<img
class="logo-image"
draggable="false"
:width="props.width"
:height="props.height"
:src="props.src"
:alt="props.alt"
/>
<div>
<h1 class="alex mb-0">{{ props.title }}</h1>
<Separator />
<p class="mb-0">{{ props.description }}</p>
</div>
</div>
</NuxtLink>
</template>
<style scoped lang="scss">
.logo {
> img.logo-image {
transition: ease 0.3s;
}
&:hover {
> img.logo-image {
transform: scale(105%);
}
}
}
</style>
-40
View File
@@ -1,40 +0,0 @@
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { usePageStore } from '~/stores/pages'
import logo from '~/assets/images/logo.gif'
import aboutIcon from '~/assets/images/icons/info.png'
import projectIcon from '~/assets/images/icons/defrag.png'
import socialIcon from '~/assets/images/icons/user.png'
const pageStore = usePageStore()
const { pages } = storeToRefs(pageStore)
const links = toRaw(pages.value).filter((page) => page.path !== '/')
const icons = [aboutIcon, projectIcon, socialIcon]
</script>
<template>
<nav
class="d-flex flex-row flex-wrap flex-sm-column flex-lg-row justify-content-around justify-content-sm-start justify-content-lg-between gap-2 gap-lg-3"
>
<Logo
:src="logo"
alt="Endermanch"
title="Enderman"
description="official website"
/>
<ul
class="navbar-nav flex-row align-items-center align-items-sm-start align-items-lg-center justify-content-center gap-3 m-2 m-sm-0 mx-lg-2 my-sm-2 my-lg-0"
>
<li v-for="(page, index) in links" :key="index" class="nav-item">
<LinkButton
:path="page.path"
:name="page.name"
:icon="icons[index]"
alt="Info"
/>
</li>
</ul>
</nav>
</template>
-19
View File
@@ -1,19 +0,0 @@
<script setup lang="ts">
const props = defineProps({
color: {
type: String,
default: 'white',
},
height: {
type: Number,
default: 1,
},
})
</script>
<template>
<hr
class="radial-gradient m-0 border-0 w-100"
:style="{ height: props.height + 'px' }"
/>
</template>
-73
View File
@@ -1,73 +0,0 @@
<script setup>
console.log('Transition started')
function enter(el, done) {
const width = getComputedStyle(el).width
console.log('@enter')
el.style.width = width
el.style.position = 'absolute'
el.style.visibility = 'hidden'
el.style.height = 'auto'
const height = getComputedStyle(el).height
el.style.width = ''
el.style.position = ''
el.style.visibility = ''
el.style.height = '0'
// Force repaint to make sure the
// animation is triggered correctly.
getComputedStyle(el).height
// Trigger the animation.
// We use `requestAnimationFrame` because we need
// to make sure the browser has finished
// painting after setting the `height`
// to `0` in the line above.
requestAnimationFrame(() => {
el.style.height = height
})
done()
}
function afterEnter(element) {
console.log('@after-enter')
element.style.height = 'auto'
}
function leave(element) {
console.log('@leave')
const height = getComputedStyle(element).height
element.style.height = height
// Force repaint to make sure the
// animation is triggered correctly.
getComputedStyle(element).height
requestAnimationFrame(() => {
element.style.height = '0'
})
}
</script>
<template>
<Transition
name="expand"
@enter="enter"
@after-enter="afterEnter"
@leave="leave"
>
<slot />
</Transition>
</template>
<style scoped lang="scss">
* {
will-change: height;
transform: translateZ(0);
backface-visibility: hidden;
perspective: 1000px;
}
</style>
+13
View File
@@ -0,0 +1,13 @@
services:
index:
container_name: frontend-index
image: endermanch/index:latest
environment:
- NODE_ENV=production
- PORT=3000
- HOST=0.0.0.0
build:
context: ./src
ports:
- "127.0.0.1:3670:3000"
restart: unless-stopped
-60
View File
@@ -1,60 +0,0 @@
<script setup lang="ts">
import type { UseSwipeDirection } from '@vueuse/core'
import { useSwipe } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import { usePageStore } from '~/stores/pages'
const router = useRouter()
const route = useRoute()
const card = ref<HTMLElement | null>(null)
const pageStore = usePageStore()
const { pages } = storeToRefs(pageStore)
const swipe = useSwipe(card, {
passive: true,
onSwipe: (touch: TouchEvent) => {
console.log(card.value?.offsetWidth, swipe.lengthX.value)
},
onSwipeEnd: (touch: TouchEvent, direction: UseSwipeDirection) => {
const currentPage = pages.value.find(
(page) => page.path === route.fullPath,
)!
const currentIndex = pages.value.indexOf(currentPage)
if (
card.value?.offsetWidth &&
Math.abs(swipe.lengthX.value) / card.value?.offsetWidth >= 0.5
) {
if (swipe.lengthX.value > 0) {
router.push(
toRaw(pages.value)[
currentIndex - 1 < 0
? pages.value.length - (currentIndex + 1)
: currentIndex - 1
].path,
)
} else {
router.push(
toRaw(pages.value)[(currentIndex + 1) % pages.value.length].path,
)
}
}
},
})
</script>
<template>
<main
ref="card"
class="dimensions background rounded-1 overflow-auto d-flex flex-column gap-3 gap-sm-2 px-4 pt-4 pb-3"
>
<Navigation />
<slot name="header" />
<NuxtPage
class="scrollbar fade-mask-sm flex-grow-1 overflow-x-hidden overflow-y-auto py-sm-4 pe-sm-3"
/>
<slot name="footer" />
</main>
</template>
-68
View File
@@ -1,68 +0,0 @@
import packageJSON from './package.json'
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
app: {
head: {
htmlAttrs: {
lang: 'en',
},
link: [
{
rel: 'icon',
type: 'image/x-icon',
href: '/favicon.ico',
},
],
meta: [
{
charset: 'utf-8',
},
{
name: 'viewport',
content: 'width=device-width, initial-scale=1',
},
{
'http-equiv': 'X-UA-Compatible',
content: 'ie=edge',
},
],
},
pageTransition: { name: 'page', mode: 'out-in' },
rootId: 'ender-app',
},
components: {
dirs: [
{
path: '~/components',
pathPrefix: false,
extensions: ['.vue'],
},
],
},
css: [
'~/assets/styles/main.min.css',
'@fortawesome/fontawesome-svg-core/styles.css',
],
plugins: [],
modules: [
'nuxt-font-loader',
'@nuxt/content',
'@nuxtjs/eslint-module',
'@pinia/nuxt',
'nuxt-quasar-ui',
'@nuxtjs/stylelint-module',
],
quasar: {
lang: 'en-US',
},
typescript: {
typeCheck: true,
},
runtimeConfig: {
public: {
version: JSON.stringify(packageJSON.version).slice(1, -1),
buildDate: new Date().toISOString().split('T')[0],
},
},
})
-20810
View File
File diff suppressed because it is too large Load Diff
-64
View File
@@ -1,64 +0,0 @@
{
"name": "index",
"private": true,
"type": "module",
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare",
"lint:js": "eslint --ext \".js,.ts,.vue\" --ignore-path .gitignore .",
"lint:prettier": "prettier --check .",
"lint": "npm run lint:js && npm run lint:style && npm run lint:prettier",
"lintfix": "prettier --write --list-different . && npm run lint:js -- --fix"
},
"devDependencies": {
"@nuxt/content": "^2.9.0",
"@nuxt/devtools": "latest",
"@nuxt/types": "^2.17.2",
"@nuxtjs/eslint-config-typescript": "^12.1.0",
"@nuxtjs/eslint-module": "^4.1.0",
"@nuxtjs/stylelint-module": "^5.1.0",
"@pinia/nuxt": "^0.5.1",
"@typescript-eslint/parser": "^6.10.0",
"csso": "^5.0.5",
"eslint": "^8.53.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-nuxt": "^4.0.0",
"eslint-plugin-prettier": "^5.0.1",
"nuxt": "^3.8.1",
"nuxt-font-loader": "^2.3.4",
"nuxt-quasar-ui": "^2.0.6",
"prettier": "^3.0.3",
"sass": "^1.69.5",
"stylelint": "^15.11.0",
"stylelint-config-recommended-vue": "^1.5.0",
"stylelint-config-standard-scss": "^11.1.0",
"typescript": "^5.2.2",
"vite-plugin-require": "^0.0.3",
"vue": "^3.3.8",
"vue-router": "^4.2.5",
"vue-tsc": "^1.8.22"
},
"dependencies": {
"@fontsource/alexandria": "^5.0.8",
"@fontsource/jetbrains-mono": "^5.0.17",
"@fontsource/lato": "^5.0.17",
"@fortawesome/fontawesome": "^1.1.8",
"@fortawesome/fontawesome-free": "^6.4.2",
"@fortawesome/fontawesome-svg-core": "^6.4.2",
"@fortawesome/free-brands-svg-icons": "^6.4.2",
"@fortawesome/free-regular-svg-icons": "^6.4.2",
"@fortawesome/free-solid-svg-icons": "^6.4.2",
"@fortawesome/vue-fontawesome": "^3.0.5",
"@quasar/extras": "^1.16.8",
"animate.css": "^4.1.1",
"bootstrap": "^5.3.2",
"caniuse-lite": "^1.0.30001564",
"humps": "^2.0.1",
"pinia": "^2.1.7",
"quasar": "^2.14.0"
},
"version": "0.1.1"
}
-139
View File
@@ -1,139 +0,0 @@
<script setup lang="ts">
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
const socials = [
{
name: 'YouTube',
url: 'https://go.enderman.ch/youtube',
icon: 'youtube',
},
{
name: 'Andrew',
url: 'https://go.enderman.ch/andrew',
icon: 'youtube',
},
{
name: 'Twitch',
url: 'https://go.enderman.ch/twitch',
icon: 'twitch',
},
{
name: 'TikTok',
url: 'https://go.enderman.ch/tiktok',
icon: 'tiktok',
},
{
name: 'Twitter',
url: 'https://go.enderman.ch/twitter',
icon: 'twitter',
},
{
name: 'Telegram',
url: 'https://go.enderman.ch/telegram',
icon: 'telegram',
},
{
name: 'Discord',
url: 'https://go.enderman.ch/discord',
icon: 'discord',
},
{
name: 'GitHub',
url: 'https://go.enderman.ch/github',
icon: 'github',
},
{
name: 'Steam',
url: 'https://go.enderman.ch/steam',
icon: 'steam',
},
]
const meta = {
title: 'Enderman Online',
description:
'Discover me on your favorite social media and find ways to contact me here.',
image: 'https://enderman.ch/images/logo.png',
url: 'https://enderman.ch/projects',
}
useSeoMeta({
title: meta.title,
description: meta.description,
ogTitle: meta.title,
ogDescription: meta.description,
ogImage: meta.image,
ogUrl: meta.url,
ogType: 'website',
twitterTitle: meta.title,
twitterDescription: meta.description,
twitterImage: meta.image,
twitterCard: 'summary',
})
useHead({
title: 'Socials',
htmlAttrs: {
lang: 'en',
},
link: [
{
rel: 'icon',
type: 'image/x-icon',
href: '/favicon.ico',
},
],
})
</script>
<template>
<section>
<h3 class="alex">Online presence</h3>
<hr />
<p>
<strong>🚧 This page is currently under construction.</strong> Expect a
lot more content to be added as time passes.
<em>Please report all bugs you encounter via the link in the footer</em>,
I will make sure to sand them down.
</p>
<hr />
<div class="row">
<div class="col-12 col-md-3">
<h5 class="alex">Social media</h5>
<ul class="list-style-type-none p-0">
<li v-for="(page, index) in socials" :key="index">
<div class="d-flex flex-row align-items-center gap-1">
<ClientOnly>
<FontAwesomeIcon
:icon="`fa-brands fa-${page.icon}`"
fixed-width
/>
</ClientOnly>
<a
class="link-hi"
target="_blank"
rel="noopener"
:href="page.url"
>
{{ page.name }}
</a>
</div>
</li>
</ul>
</div>
<div class="col-12 col-md-9">
<h5 class="alex">Contact me</h5>
<p>
Personal:
<EMail
class="link-hi"
address="[email protected]"
subject="Hey Enderman!"
/><br />
Manager: <EMail class="link-hi" address="[email protected]" /><br />
Abuse: <EMail class="link-hi" address="[email protected]" />
</p>
</div>
</div>
</section>
</template>
-17
View File
@@ -1,17 +0,0 @@
import { library, config } from '@fortawesome/fontawesome-svg-core'
import { far } from '@fortawesome/free-regular-svg-icons'
import { fab } from '@fortawesome/free-brands-svg-icons'
import { fas } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
// Add icon packs.
library.add(far, fab, fas)
// Let Nuxt manage CSS.
config.autoAddCss = false
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.component('FontAwesomeIcon', FontAwesomeIcon)
})
-10
View File
@@ -1,10 +0,0 @@
import { Screen } from 'quasar'
export default defineNuxtPlugin(() => {
Screen.setSizes({
sm: 601,
md: 996,
lg: 1281,
xl: 1921,
})
})
-31
View File
@@ -1,31 +0,0 @@
import type { Router } from 'vue-router'
interface CustomRouter extends Router {
running?: boolean
nextRoute?: string | null
}
export default defineNuxtPlugin((nuxtApp) => {
const customRouter: CustomRouter = useRouter()
nuxtApp.hook('page:start', () => {
customRouter.running = false
customRouter.beforeEach((to, _from, next) => {
if (customRouter.running) {
next(true)
} else {
customRouter.nextRoute = to.fullPath
next(false)
}
})
})
nuxtApp.hook('page:transition:finish', () => {
customRouter.running = true
if (customRouter.nextRoute) {
customRouter
.push(customRouter.nextRoute)
.then(() => (customRouter.nextRoute = null))
}
})
})
-3
View File
@@ -1,3 +0,0 @@
User-agent: *
Disallow: /images
Sitemap: https://enderman.ch/sitemap.xml
-27
View File
@@ -1,27 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http:www.w3.org/1999/xhtml">
<url>
<loc>https://enderman.ch/</loc>
<lastmod>2023-11-19</lastmod>
<changefreq>never</changefreq>
<priority>1</priority>
</url>
<url>
<loc>https://enderman.ch/about</loc>
<lastmod>2023-11-19</lastmod>
<changefreq>yearly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://enderman.ch/projects</loc>
<lastmod>2023-11-19</lastmod>
<changefreq>monthly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://enderman.ch/socials</loc>
<lastmod>2023-11-19</lastmod>
<changefreq>yearly</changefreq>
<priority>0.75</priority>
</url>
</urlset>
+31
View File
@@ -0,0 +1,31 @@
FROM oven/bun:alpine AS base
WORKDIR /usr/src/app
# install dependencies into temp directory
# this will cache them and speed up future builds
FROM base AS install
RUN mkdir -p /temp
COPY package.json bun.lockb /temp/
RUN cd /temp && bun install --production
# copy node_modules from temp directory
# then copy all (non-ignored) project files into the image
FROM base AS prerelease
COPY --from=install /temp/node_modules node_modules
COPY . .
ENV NODE_ENV=production
RUN bun test
RUN bun --bun run build
# copy production dependencies and source code into final image
FROM base AS release
COPY --from=install /temp/node_modules node_modules
COPY --from=prerelease /usr/src/app/.output .
COPY --from=prerelease /usr/src/app/package.json .
ENV HOST=0.0.0.0
USER bun
ENTRYPOINT [ "bun", "run", "server/index.mjs" ]
+5
View File
@@ -0,0 +1,5 @@
import config from '../config'
export default defineAppConfig({
...config,
})
+62
View File
@@ -0,0 +1,62 @@
<script setup lang="ts">
import 'iconify-icon'
const config = useAppConfig()
const route = useRoute()
const { reader, animate } = storeToRefs(usePageStore())
useHead({
titleTemplate: (chunk?) => {
if (route.fullPath === '/') return config.title.full
if (route.fullPath.split('/').length === 2)
switch (route.fullPath.split('/')[1]) {
case 'blog':
return 'The Ender Chest'
default:
return `${chunk} – ${config.title.short}`
}
return chunk!
},
})
</script>
<template>
<div
id="ender-layout"
class="flex flex-col lm:justify-center lt:justify-center items-center h-full"
>
<ClientOnly>
<LazySwipeControls v-if="!reader" class="block sm:hidden" />
</ClientOnly>
<NuxtLoadingIndicator />
<NuxtLayout name="card">
<template #footer>
<footer
v-if="!reader"
class="lm:static sm:fixed sm:bottom-0 sm:left-0 sm:right-0 mx-auto w-auto lm:mb-0 sm:mb-2 pass-through text-center"
>
<small
class="lm:text-inherit sm:accent-text sm:drop-shadow-lg sm:transition-all lm:opacity-100 sm:opacity-25 sm:hover:opacity-100"
>
© 2018-{{ new Date().getFullYear() }},
<a class="sm:link-dark" :href="config.url">Enderman</a>. All rights
reserved.
<wbr />
<sub class="whitespace-nowrap">
β{{ config.build.version ? config.build.version : '?.?.?' }} ({{
config.build.date ? config.build.date : '1970-01-01'
}})
</sub>
</small>
</footer>
</template>
</NuxtLayout>
</div>
<ClientOnly>
<Portal v-model="animate" layout="#ender-layout" randomize fade />
</ClientOnly>
</template>
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

+39
View File
@@ -0,0 +1,39 @@
@counter-style do {
system: fixed;
symbols: "\1F4BB" "\1F4CC" "\1F47B" "\1F4F7";
suffix: " ";
}
@counter-style enjoy {
system: fixed;
symbols: "\2728" "\1F4A1" "\1F4BE" "\1F3A5" "\1F4D5" "\1F30D" "\2615" "\26C4";
suffix: " ";
}
@counter-style faq {
system: cyclic;
symbols: "\2753" "\1F4A1";
suffix: " ";
}
.list-style-type {
&-none {
list-style-type: none !important;
}
&-do {
list-style-type: do !important;
}
&-enjoy {
list-style-type: enjoy !important;
}
&-faq {
list-style-type: faq !important;
> li:nth-child(2n):not(:last-child) {
@apply mb-2;
}
}
}
+598
View File
@@ -0,0 +1,598 @@
@use 'animate.css';
// Modules.
@use 'transitions';
@use 'lists';
@tailwind base;
@tailwind components;
@tailwind utilities;
@font-face {
font-family: Enchant;
src:
url("~/assets/fonts/enchant/enchant.woff") format('woff'),
url("~/assets/fonts/enchant/enchant.woff2") format('woff2')
}
// CSS Variables.
:root {
--animate-duration: 1s;
--animate-delay: 0.75s;
--animate-repeat: 1;
}
@-moz-document url-prefix() {
html {
min-height: 128px;
scrollbar-width: thin;
scrollbar-color: rgb(255 255 255 / 15%) rgb(0 0 0 / 20%);
&.light {
scrollbar-color: rgb(0 0 0 / 15%) rgb(255 255 255 / 20%);
}
}
}
// The HTML page.
html {
height: 100dvh;
font-size: 14px;
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
border-radius: 8px;
background-color: rgb(0 0 0 / 20%);
}
::-webkit-scrollbar-thumb {
border-radius: 8px;
background-color: rgb(255 255 255 / 15%);
}
::-webkit-scrollbar-corner {
border-radius: 3px;
background-color: rgb(255 255 255 / 15%);
}
&.light {
::-webkit-scrollbar-track {
background-color: rgb(255 255 255 / 15%);
}
::-webkit-scrollbar-thumb {
background-color: rgb(0 0 0 / 30%);
}
::-webkit-scrollbar-corner {
background-color: rgb(0 0 0 / 30%);
}
}
}
// The body element.
body {
display: flex;
flex-direction: column;
position: sticky;
background-color: rgb(6 25 28);
background-image: url('~/assets/images/textures/sky.png');
background-attachment: fixed;
background-blend-mode: multiply;
background-size: cover;
background-repeat: no-repeat;
image-rendering: pixelated;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: white;
height: 100%;
}
@layer base {
h1, h2, h3, h4, h5, h6,
.text-h1, .text-h2, .text-h3, .text-h4, .text-h5, .text-h6 {
@apply font-serif;
font-weight: bold;
}
h1, .text-h1 {
font-size: 2.5rem;
}
h2, .text-h2 {
font-size: 2rem;
}
h3, .text-h3 {
font-size: 1.75rem;
}
h4, .text-h4 {
font-size: 1.5rem;
}
h5, .text-h5 {
font-size: 1.25rem;
}
h6, .text-h6 {
font-size: 1rem;
}
small {
font-size: 0.75rem;
}
a {
@apply no-underline transition-all;
color: cornflowerblue;
&:hover {
color: royalblue;
}
}
center {
text-align: center;
margin: auto;
}
:not(nav) {
> ul {
@apply ps-6;
list-style-type: disc;
}
> ol {
@apply ps-6;
list-style-type: decimal;
}
}
:where(section, article, aside).page {
:is(div, p):not(:last-child) {
@apply mb-4;
}
:is(h1, h2, h3, h4, h5, h6) {
a {
@apply text-inherit no-underline transition-all;
&:hover {
color: indianred;
}
}
&:not(:last-child) {
@apply mb-2;
}
}
ul {
list-style-type: disc;
}
ol {
list-style-type: decimal;
}
:where(ul, ol) {
&:not(:last-child) {
@apply mb-4;
}
li:not(:last-child) {
@apply mb-1;
}
}
code:not(pre *) {
@apply px-1 py-0.5 break-words cursor-pointer rounded-lg transition-ease;
background-color: rgb(83 35 162 / 10%);
&:hover {
background-color: rgb(138 71 245 / 20%);
}
}
img {
@apply max-w-full max-h-[400px];
}
blockquote {
@apply px-4 py-3 mb-4 border-l-4 rounded-r-xl transition-ease;
border-left-color: rgb(153 153 255 / 60%);
background-color: rgb(153 153 255 / 10%);
&:hover {
border-left-color: rgb(153 153 255 / 100%);
background-color: rgb(153 153 255 / 15%);
}
}
pre {
@apply relative font-small overflow-auto min-h-14 max-h-[80rem] rounded-xl p-2 mb-4;
background-color: rgb(83 35 162 / 5%);
&:hover > .absolute {
@apply opacity-100;
}
> code {
@apply scroll-p-2;
counter-reset: step;
counter-increment: step 0;
.line::before {
@apply inline-block w-4 mr-6 text-right;
content: counter(step);
counter-increment: step;
color: rgb(238 246 250 / 40%);
}
> span span:last-child {
@apply me-4;
}
}
.overlay-action {
background-color: rgb(153 153 255 / 10%);
border-color: rgb(153 153 255 / 60%);
}
}
}
}
// Entry point (Vue mount).
#ender-app {
flex: 1 0 auto;
image-rendering: auto;
}
// Helper classes that don't exist in Bootstrap.
.overlay {
grid-area: 1 / 1;
}
.pass-through {
pointer-events: none;
> * {
pointer-events: auto;
}
}
ruby {
ruby-position: under;
ruby {
ruby-position: over;
}
}
rt {
ruby-align: space-around;
}
.font-small {
font-size: 14px;
}
// Query-overridable classes.
.dimensions {
width: 100%;
min-height: 100%;
}
@layer utilities {
.accent-background {
background-color: rgb(0 0 0 / 50%);
}
.accent-overlay-background {
background-color: rgb(0 0 0 / 90%);
}
.accent-text {
color: white;
}
.accent-text-shadow {
text-shadow: black 2px 3px 4px;
}
.drop-shadow,
.accent-drop-shadow {
filter: drop-shadow(1px 2px 3px black);
}
.link-dark {
@apply text-inherit no-underline transition-all;
color: cornflowerblue;
&:hover {
color: royalblue;
}
}
.accent-fade {
background: linear-gradient(
to top,
black,
rgb(0 0 0 / 50%) 75%,
transparent
);
}
.fade-mask {
mask-image: linear-gradient(to bottom, transparent, black 3%, black 97%, transparent);
}
}
.dialog {
display: grid;
min-width: 240px;
max-width: 640px;
background-color: rgb(25 20 40 / 95%);
box-shadow: rgb(81 75 89 / 50%) 7px 7px 2em;
border-radius: 16px !important;
> div.background {
background-image: url('~/assets/images/textures/aero.png');
background-attachment: scroll;
background-repeat: no-repeat;
background-size: cover;
background-blend-mode: lighten;
width: 100%;
height: 100%;
opacity: 0.2;
box-shadow: azure 0 0 20rem;
filter: blur(5px);
}
> div.content > img.icon-badge {
position: fixed;
margin-top: -20px;
margin-left: -20px;
transform: rotate(-25deg);
width: 60px;
height: 60px;
}
}
.accent-gradient {
background: radial-gradient(
circle at left,
rgb(255 255 255 / 80%),
rgb(255 255 255 / 75%),
rgb(255 255 255 / 0%)
);
}
.post {
scroll-snap-type: y mandatory;
scroll-snap-stop: normal;
&::after {
content: '';
scroll-snap-align: start;
}
&-box {
border-color: rgb(153 153 255 / 60%);
background-color: rgb(45 7 110 / 10%);
&:hover {
border-color: rgb(153 153 255 / 100%);
background-color: rgb(45 7 110 / 30%);
}
}
&-pocket {
background-color: rgb(153 153 255 / 10%);
border-left-color: rgb(153 153 255 / 60%);
border-top-color: rgb(153 153 255 / 60%);
}
&-tag {
background-color: rgb(153 153 255 / 10%);
border-color: rgb(153 153 255 / 60%);
}
}
:where(html.light) {
body {
color: black;
}
.accent-background {
background-color: rgb(255 255 255 / 90%);
}
.accent-overlay-background {
background-color: rgb(255 255 255 / 90%);
}
.accent-drop-shadow {
filter: drop-shadow(1px 2px 3px white);
}
a {
@apply no-underline transition-all;
color: royalblue;
&:hover {
color: darkblue;
}
}
.accent-text {
color: black;
}
.accent-gradient {
background: radial-gradient(
circle at left,
rgb(0 0 0 / 95%),
rgb(0 0 0 / 80%),
transparent
);
}
.accent-text-shadow {
text-shadow: white 1px 2px 3px;
}
.accent-fade {
background: linear-gradient(
to top,
white,
rgb(255 255 255 / 50%) 75%,
transparent
);
}
.post {
&-box {
border-color: rgb(123 123 255 / 1000%);
background-color: rgb(45 7 110 / 20%);
&:hover {
border-color: rgb(123 123 255 / 80%);
background-color: rgb(45 7 110 / 10%);
}
}
&-pocket {
background-color: rgb(123 123 255 / 10%);
border-left-color: rgb(123 123 255 / 80%);
border-top-color: rgb(123 123 255 / 80%);
}
&-tag {
background-color: rgb(123 123 255 / 10%);
border-color: rgb(123 123 255 / 80%);
}
}
:is(section, article, aside).page {
code:not(pre *) {
background-color: rgb(138 71 245 / 20%);
&:hover {
background-color: rgb(83 35 162 / 20%);
}
}
blockquote {
border-left-color: rgb(153 153 255 / 60%);
background-color: rgb(153 153 255 / 10%);
&:hover {
border-left-color: rgb(153 153 255 / 100%);
background-color: rgb(153 153 255 / 15%);
}
}
pre {
background-color: rgb(83 35 162 / 10%);
code {
.line::before {
color: rgb(83 85 89 / 40%);
}
}
.overlay-action {
background-color: rgb(153 153 255 / 30%);
border-color: rgb(153 153 255 / 80%);
}
}
}
}
// Dynamic classes.
@screen lm {
.dimensions {
width: 100% !important;
min-height: 100% !important;
}
}
@screen sm {
html {
font-size: 16px;
}
#ender-app {
height: 100dvh;
scrollbar-gutter: stable;
}
.dimensions {
width: 90%;
min-height: fit-content;
max-height: 90%;
}
.animate__animated-sm {
animation-duration: var(--animate-duration);
animation-fill-mode: both;
}
.animate__delay-1-5s {
animation-delay: calc(var(--animate-delay) * 1.5);
}
}
@screen md {
html {
font-size: 18px;
}
}
@screen lg {
html {
font-size: 20px;
}
.dimensions {
width: 75%;
}
}
@screen xl {
.dimensions {
width: 60%;
}
}
@screen 2xl {
.dimensions {
width: 50%;
}
}
+44
View File
@@ -0,0 +1,44 @@
.page {
&-enter-active,
&-leave-active {
transition: all 0.15s;
}
&-enter-from,
&-leave-to {
opacity: 0;
filter: blur(1em);
}
}
.switch {
&-enter-active,
&-leave-active {
transition: all 0.15s ease-in-out;
}
&-enter-from,
&-leave-to {
opacity: 0;
}
}
.transition {
&-ease {
transition: all 0.3s ease;
}
&-portal {
transition: all 0.6942s ease-in;
}
}
.button {
&-twitter:hover {
transform: translateZ(0) scale(1.1) rotate(-10deg);
}
&-back:hover {
transform: translateZ(0) scale(0.9);
}
}
@@ -0,0 +1,58 @@
<script setup lang="ts">
import type { PropType } from 'vue'
const props = defineProps({
title: {
type: String,
required: true,
},
description: {
type: String,
required: true,
},
logo: {
type: String,
required: true,
},
sectionLogo: {
type: null as unknown as PropType<string | null>,
default: null,
required: false,
},
thumbnail: {
type: String,
required: true,
},
alt: {
type: String,
required: true,
},
})
const sentence = computed(() => {
return props.description.split('.').at(0) + '...'
})
</script>
<template>
<div class="w-full h-full flex flex-col justify-end text-white relative">
<img
:src="thumbnail"
:alt="alt"
class="w-full h-full object-cover absolute top-0 left-0"
/>
<div class="absolute top-0 left-0 p-5">
<img :src="logo" alt="Logo" class="w-[100] h-[100]" />
</div>
<div v-if="sectionLogo" class="absolute top-0 right-0 p-5">
<img :src="sectionLogo" alt="Logo" class="w-[100] h-[100]" />
</div>
<div class="mb-5 px-5" style="text-shadow: black 1px 1px 10px">
<h1 class="m-0 text-[60px]">{{ title }}</h1>
<strong class="min-h-[90px] m-0 text-[24px]" style="font-family: Lato">
{{ sentence }}
</strong>
</div>
</div>
</template>
+7
View File
@@ -0,0 +1,7 @@
<script setup lang="ts"></script>
<template>
<button type="button" data-theme-toggle aria-label="Change to light theme">
Change to light theme
</button>
</template>
@@ -0,0 +1,71 @@
<script setup lang="ts">
const config = useAppConfig()
const meta = {
title: 'Page not found',
description:
'The page you are looking for does not exist. It might have been removed, had its name changed, or is temporarily unavailable.',
image: `${config.url}/images/logo.png`,
url: config.url,
}
let slug = useRoute().params.slug
if (Array.isArray(slug)) slug = slug.join('/')
const error = {
http_code: 200,
title: config.title,
url: config.url + '/' + slug,
locale: config.locale || 'en',
data: {
path: '/error.vue',
content: {
title: 'Page not found!',
description:
"The page you are looking for does not exist. However, no 404 error has occurred, as you're browsing through a web application loaded into memory and HTTP error codes make no sense here.",
},
},
slug: useRoute().params.slug,
build: config.build,
hint: 'Use the navigation bar to get back to the main page.',
}
const errorString = JSON.stringify(error, null, 2)
const buffer = ref('')
const startTime = Date.now()
const dt = 20
let i = 0
function type() {
if (Date.now() > startTime + dt * i) buffer.value += errorString[i++]
if (i < errorString.length) requestAnimationFrame(type)
}
onMounted(() => setTimeout(type, 1500))
</script>
<template>
<section>
<h3>Page not found!</h3>
<pre class="whitespace-pre-wrap break-keep font-small">{{
buffer
}}<span class="blinker">&#9610;</span></pre>
</section>
</template>
<style scoped lang="scss">
.blinker {
animation: blinker 1s steps(2, start) infinite;
}
@keyframes blinker {
from {
visibility: visible;
}
to {
visibility: hidden;
}
}
</style>
@@ -1,8 +1,15 @@
<script setup lang="ts"> <script setup lang="ts">
import sky from '~/assets/images/portal/sky.png' import sky from 'assets/images/textures/sky.png'
import particles from '~/assets/images/portal/particles.png' import particles from 'assets/images/textures/particles.png'
const { $local } = useNuxtApp()
const config = useAppConfig()
const fqdn = config.url.split('//').at(1)
const resources: string[] = [sky, particles] const resources: string[] = [sky, particles]
const animated = defineModel<boolean>({
required: true,
})
const props = defineProps({ const props = defineProps({
layout: { layout: {
@@ -62,7 +69,7 @@ class Canvas {
compatible( compatible(
obj: HTMLCanvasElement | HTMLElement | Element, obj: HTMLCanvasElement | HTMLElement | Element,
): obj is HTMLCanvasElement { ): obj is HTMLCanvasElement {
return obj.tagName === 'CANVAS' return obj.tagName.toLowerCase() === 'canvas'
} }
query(): HTMLCanvasElement { query(): HTMLCanvasElement {
@@ -133,7 +140,7 @@ class Portal {
constructor( constructor(
directory: string = '/', directory: string = '/',
create: boolean = true, create: boolean = true,
animate: boolean = true, animate: boolean = false,
randomize: boolean = false, randomize: boolean = false,
fade: boolean = false, fade: boolean = false,
speed: number = 1, speed: number = 1,
@@ -249,7 +256,7 @@ class Portal {
this.animate = animate this.animate = animate
this.randomize = randomize this.randomize = randomize
// Set default speed. // Set the default speed.
this.speed = speed this.speed = speed
this.initialSpeed = this.speed this.initialSpeed = this.speed
this.dv = 20 this.dv = 20
@@ -316,7 +323,7 @@ class Portal {
notice() { notice() {
console.log( console.log(
`%c \u2587%c\u2587%c\u2587 %c\u2587%c\u2587%c\u2587 %c // ECMAPortal v${this.version} by Endermanch & WiPet\n\n\thttps://enderman.ch\n\thttps://go.enderman.ch/wipet`, `%c \u2587%c\u2587%c\u2587 %c\u2587%c\u2587%c\u2587 %c // ECMAPortal v${this.version} by Endermanch & WiPet\n\n\t${config.url}\n\t${config.shortener}/wipet`,
'color: #E58EFF', 'color: #E58EFF',
'color: #D52DFF', 'color: #D52DFF',
'color: #E58EFF', 'color: #E58EFF',
@@ -327,7 +334,7 @@ class Portal {
) )
console.log( console.log(
'If any errors occur below, please send a screenshot of them my way!\n\t%ccontact@enderman.ch', `If any errors occur below, please send a screenshot of them my way!\n\t%ccontact@${fqdn}`,
'color: #87CEFA', 'color: #87CEFA',
) )
} }
@@ -565,10 +572,6 @@ class Portal {
resize() { resize() {
if (!this.canvas.full()) this.canvas.fill() if (!this.canvas.full()) this.canvas.fill()
// Pause the animation if the viewport becomes too small.
if (window.innerWidth <= 600) this.pause()
else this.continue()
// If we have animation disabled, we still have to re-render the scene once on resize. // If we have animation disabled, we still have to re-render the scene once on resize.
if (!this.animate) requestAnimationFrame(this.render.bind(this)) if (!this.animate) requestAnimationFrame(this.render.bind(this))
} }
@@ -577,8 +580,8 @@ class Portal {
this.clickTime = Date.now() this.clickTime = Date.now()
} }
continue() { play() {
if (this.pauseTime === 0) return if (this.pauseTime === 0) this.pauseTime = this.currentTime
// Add the time that has passed since the pause to the current time. // Add the time that has passed since the pause to the current time.
this.currentTime += Date.now() - this.pauseTime this.currentTime += Date.now() - this.pauseTime
@@ -611,47 +614,53 @@ class Portal {
} }
onMounted(() => { onMounted(() => {
// If the localStorage value is null, trigger the setup.
// If the viewport is too small, disable by default.
const ecmaportal: string | null | undefined = $local.getItem('ecmaportal')
if (ecmaportal === null) {
const notMobile = window.innerWidth > 600
$local.setItem('ecmaportal', notMobile)
animated.value = notMobile
} else animated.value = ecmaportal === 'true'
const layout = document.querySelector(props.layout) const layout = document.querySelector(props.layout)
const portal = new Portal( const portal = new Portal(
props.directory, props.directory,
props.create, props.create,
props.animate, animated.value,
props.randomize, props.randomize,
props.fade, props.fade,
props.speed, props.speed,
) )
layout!.addEventListener('mousedown', ((e: UIEvent) => { const click: EventListener = (e: Event) => {
if (e.target === e.currentTarget && e.detail >= 2) { if (e.target === e.currentTarget) {
e.preventDefault() e.preventDefault()
portal.click() portal.click()
} }
}) as EventListener) }
for (const event of ['dblclick', 'touchstart'])
layout?.addEventListener(event, click)
// Save the new value to local storage.
watch(animated, (newAnimate) => {
$local.setItem('ecmaportal', newAnimate)
newAnimate ? portal.play() : portal.pause()
})
}) })
</script> </script>
<template> <template>
<canvas id="ecmaportal" class="parallax"> <canvas
id="ecmaportal"
class="fixed top-0 left-0 opacity-0 transition-portal -z-10"
>
<span> <span>
Your browser does not support the &lt;canvas /&gt; element, which is Your browser does not support the &lt;canvas /&gt; element, which is
required for parallax animation. required for parallax animation.
</span> </span>
</canvas> </canvas>
</template> </template>
<style scoped>
.parallax {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
transition: all 0.6942s ease-in;
z-index: -1;
}
</style>
@@ -0,0 +1,14 @@
<script setup lang="ts"></script>
<template>
<div>
<hr class="accent-text accent-gradient border-0 h-px my-4" />
<p>
<strong>🚧 This page is currently under construction.</strong> Expect a
lot more content to be added as time passes.
<em>Please report all bugs you encounter via the link in the footer</em>,
I will make sure to sand them down.
</p>
<hr class="accent-text accent-gradient border-0 h-px my-4" />
</div>
</template>
+55
View File
@@ -0,0 +1,55 @@
<script setup lang="ts">
const props = defineProps({
src: {
type: String,
required: true,
},
alt: {
type: String,
default: 'Logo',
},
width: {
type: Number,
default: 100,
},
height: {
type: Number,
default: 100,
},
title: {
type: String,
default: 'Title',
},
description: {
type: String,
default: 'Description',
},
})
</script>
<template>
<NuxtLink
class="flex flex-row items-center text-inherit hover:text-inherit select-none sm:m-auto lm:m-0 lt:m-0 lg:m-0"
to="/"
>
<img
class="transition-all"
draggable="false"
:width="props.width"
:height="props.height"
:src="props.src"
:alt="props.alt"
/>
<div>
<h2>{{ props.title }}</h2>
<hr class="accent-text accent-gradient border-0 h-px" />
<p>{{ props.description }}</p>
</div>
</NuxtLink>
</template>
<style scoped lang="scss">
a:hover > img {
transform: scale(105%);
}
</style>
+36
View File
@@ -0,0 +1,36 @@
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { usePageStore } from '~/stores/pages'
import logo from 'assets/images/logo.avif'
const pageStore = usePageStore()
const { pages } = storeToRefs(pageStore)
const links = toRaw(pages.value).filter((page) => page.path !== '/')
</script>
<template>
<nav
class="flex flex-row flex-wrap lm:flex-row lt:flex-row sm:flex-col lg:flex-row justify-around sm:justify-start lm:justify-between lt:justify-between lg:justify-between gap-2 lg:gap-4"
>
<Logo
:src="logo"
alt="Endermanch"
title="Enderman"
description="official website"
/>
<ul
class="flex flex-row flex-wrap items-center sm:items-start lm:items-center lt:items-center lg:items-center justify-center gap-4 m-2 sm:m-0 lm:mx-2 lt:mx-2 lg:mx-2 sm:my-2 lg:my-0"
>
<li v-for="(page, index) in links" :key="index" class="nav-item">
<Route
:path="page.path"
:name="page.name"
:icon="page.icon!.src"
:alt="page.icon!.alt"
/>
</li>
</ul>
</nav>
</template>
@@ -17,23 +17,32 @@ const props = defineProps({
type: String, type: String,
default: 'Link', default: 'Link',
}, },
width: {
type: Number,
default: 32,
},
height: {
type: Number,
default: 32,
},
}) })
</script> </script>
<template> <template>
<NuxtLink <NuxtLink
class="link d-flex flex-row align-items-center gap-2 user-select-none no-decoration" class="flex flex-row items-center gap-2 select-none text-inherit no-underline"
active-class="icon-active px-sm-3" active-class="active"
:to="!props.external ? props.path : undefined" :to="!props.external ? props.path : undefined"
:href="props.external ? props.path : undefined" :href="props.external ? props.path : undefined"
> >
<img <img
class="icon-image"
draggable="false" draggable="false"
:src="props.icon" :src="props.icon"
:alt="props.alt" :alt="props.alt"
:width="props.width"
:height="props.height"
/> />
<span class="display-xs-none"> <span class="hidden lm:hidden lt:hidden sm:block">
<strong> <strong>
{{ props.name }} {{ props.name }}
</strong> </strong>
@@ -42,14 +51,8 @@ const props = defineProps({
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.icon { .active {
&-active { @apply px-4;
transform: translate(2px, 2px) rotate3d(1, 1, 1, 5deg) scale(1.25); transform: translate(2px, 2px) rotate3d(1, 1, 1, 5deg) scale(1.25);
} }
&-image {
width: 1.5em;
height: 1.5em;
}
}
</style> </style>
+106
View File
@@ -0,0 +1,106 @@
<script setup lang="ts">
import cogIcon from 'assets/images/icons/cog.avif'
import pearlIcon from 'assets/images/icons/pearl.avif'
const config = useAppConfig()
const { animate } = storeToRefs(usePageStore())
const fqdn = config.url.split('//').at(1)
const mailTemplate = `I've just found a bug on ${config.url} and would like to report it.%0D%0A%0D%0AWebsite version: ${
config.build.version
}%0D%0ABuild date: ${
config.build.date
}%0D%0ATimestamp: ${new Date().toISOString()}%0D%0A%0D%0A%0D%0ASteps to reproduce:%0D%0A{ Explain in detail what happened here, or attach a video/screenshot }%0D%0A%0D%0AAdditional information:%0D%0A{ Helpful information, such as developer console output / Leave empty if none }%0D%0A%0D%0A%0D%0A// Keep in mind that it's just a template, you can change it as you wish! :)`
const active = ref(false)
</script>
<template>
<div
class="absolute top-0 right-0 flex flex-col cursor-pointer select-none p-2"
@click="active = true"
>
<img
draggable="false"
:src="cogIcon"
alt="Options"
width="16"
height="16"
/>
</div>
<Transition
enter-active-class="animate__animated animate__fadeIn"
leave-active-class="animate__animated animate__fadeOut"
>
<div
v-if="active"
class="fixed top-0 left-0 flex flex-row justify-center items-center w-full h-full z-20"
>
<div
class="relative flex flex-col gap-4 accent-overlay-background rounded-xl max-w-[800px] mx-4 p-4"
>
<img
draggable="false"
:src="pearlIcon"
alt="Pearl"
class="absolute -top-8 -left-6 badge select-none w-16 lg:w-auto"
/>
<h3 class="text-center">Site settings</h3>
<div class="overlay accent-overlay-background" />
<div class="overlay">
<p class="mb-3">
The site settings are experimental. Suggest what you want to see
here next!
<strong>Please report any bugs that may occur.</strong>
</p>
<div class="flex flex-col md:flex-row gap-4">
<div class="md:basis-0 flex-grow">
<strong>Theme</strong>
<p>Available in vanilla and chocolate flavors.</p>
<select
v-model="$colorMode.preference"
class="accent accent-overlay-background"
>
<option value="system">System</option>
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="sepia">Sepia</option>
</select>
</div>
<div class="md:basis-0 flex-grow">
<strong>Animation</strong>
<p>
Some computers may have issues rendering that gorgeous
background animation. Disabling it substantially decreases power
consumption, but also makes the website less cool.
</p>
<input v-model="animate" type="checkbox" color="primary" />
</div>
</div>
<span class="mt-3">
<EMail
:address="`contact@${fqdn}`"
:cc="`admin@${fqdn}`"
:subject="`Bug report: ${fqdn}`"
:body="mailTemplate"
>
<strong>Report a bug</strong>
</EMail>
</span>
</div>
<button @click="active = false">Close</button>
</div>
</div>
</Transition>
</template>
<style scoped lang="scss">
.badge {
transform: rotate(-15deg);
}
</style>
@@ -0,0 +1,37 @@
<script setup lang="ts">
const { pages } = storeToRefs(usePageStore())
const route = useRoute()
const state = computed(() => {
const i = pages.value.indexOf(
pages.value.find(
(page) => page.path === '/' + route.fullPath.split('/')[1],
)!,
)
return {
current: route.fullPath,
index: i,
next: toRaw(pages.value)[(i + 1) % pages.value.length].path,
prev: toRaw(pages.value)[i - 1 < 0 ? pages.value.length - (i + 1) : i - 1]
.path,
}
})
</script>
<template>
<div
class="flex fixed flex-row justify-between items-center w-full h-full pass-through"
>
<NuxtLink
class="opacity-25 hover:opacity-100 text-inherit no-underline transition-all"
:to="state.prev"
>
<iconify-icon icon="iconoir:nav-arrow-left" inline />
</NuxtLink>
<NuxtLink class="control-icon text-inherit no-underline" :to="state.next">
<iconify-icon icon="iconoir:nav-arrow-right" inline />
</NuxtLink>
</div>
</template>
+91
View File
@@ -0,0 +1,91 @@
<script setup lang="ts">
import type { PropType } from 'vue'
import warningIcon from 'assets/images/icons/accent/warning.avif'
import errorIcon from 'assets/images/icons/accent/error.avif'
import infoIcon from 'assets/images/icons/accent/info.avif'
import helpIcon from 'assets/images/icons/accent/help.avif'
import checkIcon from 'assets/images/icons/accent/checkmark.avif'
import ideaIcon from 'assets/images/icons/accent/idea.avif'
type IconOptions = 'warning' | 'error' | 'info' | 'help' | 'checkmark' | 'idea'
const props = defineProps({
type: {
type: String as PropType<IconOptions>,
default: 'warning',
},
title: {
type: String,
default: 'Important',
},
})
const cards = {
warning: {
color: 'border-warning',
icon: {
src: warningIcon,
alt: 'Warning triangle',
},
},
error: {
color: 'border-error',
icon: {
src: errorIcon,
alt: 'Error circle',
},
},
info: {
color: 'border-info',
icon: {
src: infoIcon,
alt: 'Information circle',
},
},
help: {
color: 'border-help',
icon: {
src: helpIcon,
alt: 'Help circle',
},
},
checkmark: {
color: 'border-checkmark',
icon: {
src: checkIcon,
alt: 'Checkmark',
},
},
idea: {
color: 'border-idea',
icon: {
src: ideaIcon,
alt: 'Lightbulb',
},
},
}
</script>
<template>
<div class="tilt flex flex-row items-center gap-2 mb-0 sm:mb-2 md:mb-4">
<img
draggable="false"
:src="cards[props.type].icon.src"
:alt="cards[props.type].icon.alt"
class="w-12 h-12"
/>
<h3 class="whitespace-nowrap mb-0">{{ props.title }}</h3>
</div>
<div
:class="`box p-4 mb-4 ${cards[props.type].color} border-b border-r rounded-xl`"
>
<slot />
</div>
</template>
<style lang="scss">
.tilt {
transform: rotate(-2deg);
}
</style>
@@ -0,0 +1,35 @@
<script setup lang="ts">
const emit = defineEmits(['copy'])
const clicked = ref<boolean>(false)
function p() {
emit('copy')
if (!clicked.value) setTimeout(() => (clicked.value = false), 2000)
clicked.value = true
}
</script>
<template>
<div
class="absolute flex flex-col justify-start top-0 left-0 w-full h-full pointer-events-none opacity-0 hover:opacity-100 transition-ease"
>
<div class="flex flex-row justify-end items-center p-2">
<span
class="overlay-action flex-grow-0 pointer-events-auto cursor-pointer p-2 border rounded-xl"
@click="p"
>
<Transition name="switch" mode="out-in">
<iconify-icon v-if="!clicked" icon="mdi:content-copy" inline />
<iconify-icon
v-else
icon="mdi:check-bold"
class="text-green-500"
inline
/>
</Transition>
</span>
</div>
</div>
</template>
+70
View File
@@ -0,0 +1,70 @@
<script setup lang="ts">
import type { UseSwipeDirection } from '@vueuse/core'
import { useSwipe } from '@vueuse/core'
const router = useRouter()
const route = useRoute()
const card = ref<HTMLElement | null>(null)
const { pages, reader } = storeToRefs(usePageStore())
const swipe = useSwipe(card, {
passive: true,
onSwipeEnd: (_touch: TouchEvent, _direction: UseSwipeDirection) => {
if (reader.value) return
const currentPage = pages.value.find(
(page) => page.path === route.fullPath,
)!
const currentIndex = pages.value.indexOf(currentPage)
if (
card.value?.offsetWidth &&
Math.abs(swipe.lengthX.value) > 1.5 * Math.abs(swipe.lengthY.value) &&
Math.abs(swipe.lengthX.value) / card.value?.offsetWidth >= 0.1
) {
router.push(
toRaw(pages.value)[
swipe.lengthX.value > 0
? (currentIndex + 1) % pages.value.length
: currentIndex - 1 < 0
? pages.value.length - (currentIndex + 1)
: currentIndex - 1
].path,
)
}
},
})
const animationComplete = ref(false)
onMounted(() => {
card.value?.addEventListener(
'animationend',
() => (animationComplete.value = true),
)
})
</script>
<template>
<main
ref="card"
class="dimensions accent-background transition-ease overflow-auto flex flex-col gap-4 lm:gap-0 sm:gap-2 py-2 px-6 sm:p-4"
:class="{
'animate__animated-sm animate__delay-1-5s animate__fadeInDown':
!reader && !animationComplete,
'animate__animated-sm animate__fadeIn': reader && !animationComplete,
'lm:rounded-none sm:rounded-xl sm:mt-8 lm:mt-0 lt:mt-0': !reader,
'!max-h-full h-full': reader,
}"
>
<Settings />
<Navigation />
<slot name="header" />
<NuxtPage
class="sm:fade-mask flex-grow overflow-y-auto h-full sm:py-4 sm:pe-4"
/>
<slot name="footer" />
</main>
</template>
+5
View File
@@ -0,0 +1,5 @@
export default defineNuxtRouteMiddleware((to, from) => {
const { reader } = storeToRefs(usePageStore())
reader.value = /\/blog\/(.*)/.test(to.path)
})
+42
View File
@@ -0,0 +1,42 @@
<script setup lang="ts">
const config = useAppConfig()
const meta = {
title: 'Page not found',
description:
'The page you are looking for does not exist. It might have been removed, had its name changed, or is temporarily unavailable.',
image: `${config.url}/images/logo.png`,
url: config.url,
}
useSeoMeta({
title: meta.title,
description: meta.description,
ogTitle: meta.title,
ogDescription: meta.description,
ogImage: meta.image,
ogUrl: meta.url,
ogType: 'website',
twitterTitle: meta.title,
twitterDescription: meta.description,
twitterImage: meta.image,
twitterCard: 'summary',
})
useHead({
title: 'Page not found',
htmlAttrs: {
lang: config.locale || 'en',
},
link: [
{
rel: 'icon',
type: 'image/x-icon',
href: '/favicon.ico',
},
],
})
</script>
<template>
<NotFound />
</template>
+39 -16
View File
@@ -1,4 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { defineRouteRules } from '#imports'
const config = useAppConfig()
const age = const age =
new Date(Number(new Date()) - Number(new Date('2003-09-18'))).getFullYear() - new Date(Number(new Date()) - Number(new Date('2003-09-18'))).getFullYear() -
1970 1970
@@ -6,8 +9,8 @@ const age =
const meta = { const meta = {
title: 'About Me', title: 'About Me',
description: `Nice to meet you!~ I'm Andrew, a ${age}-year-old developer from Kaluga, Russia.`, description: `Nice to meet you!~ I'm Andrew, a ${age}-year-old developer from Kaluga, Russia.`,
image: 'https://enderman.ch/images/logo.png', image: `${config.url}/images/logo.png`,
url: 'https://enderman.ch/about', url: `${config.url}/about`,
} }
useSeoMeta({ useSeoMeta({
@@ -27,7 +30,7 @@ useSeoMeta({
useHead({ useHead({
title: 'About', title: 'About',
htmlAttrs: { htmlAttrs: {
lang: 'en', lang: config.locale || 'en',
}, },
link: [ link: [
{ {
@@ -37,30 +40,31 @@ useHead({
}, },
], ],
}) })
defineRouteRules({
sitemap: {
changefreq: 'yearly',
priority: 0.6,
},
})
</script> </script>
<template> <template>
<section> <section class="page">
<h3 class="alex">About me</h3> <h3>About me</h3>
<hr /> <Construction />
<p>
<strong>🚧 This page is currently under construction.</strong> Expect a
lot more content to be added as time passes.
<em>Please report all bugs you encounter via the link in the footer</em>,
I will make sure to sand them down.
</p>
<hr />
<p> <p>
Nice to meet you! I'm Andrew, a {{ age }}-year-old guy from Kaluga, Nice to meet you! I'm Andrew, a {{ age }}-year-old guy from Kaluga,
Russia. I have been developing software since I was 10, and I have always Russia. I have been developing software since I was 10, and I have always
been interested in technology. I'm a middle-senior C/++ developer and a been interested in technology. I'm a middle-senior C/++ developer and a
junior full-stack engineer. junior full-stack engineer.
<sup>1</sup> <sup>1</sup>
</p> <br />
<p class="font-small"> <small>
<sup>1</sup> <sup>1</sup>
All titles are based on knowledge and confidence, not on official work All titles are based on knowledge and confidence, not on official work
experience. experience.
</small>
</p> </p>
<p><strong>Here's a little more about myself:</strong></p> <p><strong>Here's a little more about myself:</strong></p>
<ul> <ul>
@@ -69,9 +73,28 @@ useHead({
<li>My favorite field in mathematics is Algebraic Geometry.</li> <li>My favorite field in mathematics is Algebraic Geometry.</li>
</ul> </ul>
<p> <p>
I work on all sorts of projects, most of the times simultaneously, which I work on all sorts of projects, most of the time simultaneously, which
doesn't help with adhering to deadlines at all. However, you can always doesn't help with adhering to deadlines at all. However, you can always
check them out! check them out!
</p> </p>
<h5>FAQ</h5>
<ul class="list-style-type-faq">
<li><strong>Why are you called Endermanch?</strong></li>
<li>
There's no particular reason to it. When I was rebranding my YouTube
channel from Ender's Show to Enderman, the short URL was taken. I went
thinking for quite some time, and then concluded that adding «ch» at the
end sounded pretty good. I was 14 at the time of making that decision.
</li>
<li>
<strong>
When will you make a new video? What happened to your schedule?
</strong>
</li>
<li>
One day. Haven't been particularly motivated lately, but life's busy. 🙁
I'm alive and well, though.
</li>
</ul>
</section> </section>
</template> </template>
+230
View File
@@ -0,0 +1,230 @@
<script setup lang="ts">
import { formatDate } from 'date-fns'
import chestAnimation from 'assets/images/chest.avif'
import { render } from 'vue'
import { CodeControls } from '#components'
const config = useAppConfig()
const router = useRouter()
const route = useRoute()
let slug = route.params.slug
if (Array.isArray(slug)) slug = slug.join('/')
const { data, status } = await useAsyncData('home', () =>
queryContent(`/blog/${slug}`).findOne(),
)
const meta = {
title: data.value ? data.value.title : 'Page not found',
description: data.value
? data.value.description
: 'The page you are looking for does not exist. It might have been removed, had its name changed, or is temporarily unavailable.',
url: `${config.url}/blog`,
}
// When defineOgImage is used, useSeoMeta must exclude ogImage and twitterImage properties.
useSeoMeta({
title: meta.title,
description: meta.description,
ogTitle: meta.title,
ogDescription: meta.description,
// ogImage: EXCLUDED
ogUrl: meta.url,
ogType: 'article',
twitterTitle: meta.title,
twitterDescription: meta.description,
// twitterImage: EXCLUDED
twitterCard: 'summary_large_image',
})
const clipboard = (el: HTMLElement) => {
if (el.textContent)
navigator.clipboard.writeText(el.textContent).catch(() => {
console.error(
'Failed to copy element data to the clipboard! Element data:',
el,
)
})
}
const content = ref<Element | null>(null)
const thumbnail: string =
data.value!.thumbnail ??
`/images/blog/thumbnails/${data.value!._path!.split('/').at(-1)}.png`
// Generate the article's Open Graph image.
defineOgImageComponent(
'ArticleThumbnail',
{
title: data.value!.title,
description: data.value!.description,
logo: '/images/logo.png',
sectionLogo: '/images/chest.png',
thumbnail,
alt: data.value!.title,
},
{
fonts: ['Alexandria:700', 'Lato:700'],
},
)
// Hydrate the rendered items.
onMounted(() => {
if (route.hash)
content.value
?.querySelector(route.hash.toLowerCase())
?.scrollIntoView({ behavior: 'smooth' })
content.value
?.querySelectorAll('code:not(pre *)')
.forEach((code: Element) => {
if (code instanceof HTMLElement)
code.addEventListener('click', () => clipboard(code))
})
content.value?.querySelectorAll('pre').forEach((pre: HTMLPreElement) => {
const overlay = h(CodeControls, { onCopy: () => clipboard(pre) })
render(overlay, pre)
})
content.value?.querySelectorAll('a').forEach((a: HTMLAnchorElement) => {
if (
a.hash &&
a.host === window.location.host &&
a.pathname.toLowerCase() === route.path.toLowerCase()
)
a.addEventListener('click', (e: Event) => {
e.preventDefault()
content.value
?.querySelector(a.hash.toLowerCase())
?.scrollIntoView({ behavior: 'smooth' })
history.replaceState(history.state, '', a.hash)
})
})
})
useHead({
title: data.value!.title,
htmlAttrs: {
lang: config.locale || 'en',
},
link: [
{
rel: 'icon',
type: 'image/x-icon',
href: '/favicon.ico',
},
],
})
</script>
<template>
<div
v-if="!data"
class="flex flex-col items-center justify-center gap-4 w-full select-none text-center"
>
<img draggable="false" :src="chestAnimation" alt="The Ender Chest" />
<div>
<h1 class="font-enchant">Loading document...</h1>
<span class="opacity-0 hover:opacity-100 transition-ease">
<em>Loading document...</em>
</span>
</div>
</div>
<article
v-else-if="data"
class="flex-grow post snap-normal fade-mask-sm flex flex-col gap-4 overflow-y-auto sm:py-4 sm:pe-4"
>
<div class="grid-thumbnail grid max-w-[768px] snap-end">
<img
draggable="false"
:src="thumbnail!"
alt="Thumbnail"
class="grid-thumbnail-image object-cover object-center rounded-xl aspect-[16/9] select-none"
/>
<NuxtLink to="/blog" class="grid-thumbnail-start button-back p-2">
<iconify-icon
icon="icon-park-solid:back"
width="2em"
height="2em"
class="drop-shadow text-pink-200"
/>
</NuxtLink>
<NuxtLink
class="grid-thumbnail-end button-twitter p-2"
:href="`https://twitter.com/share?url=${config.url}/blog/${slug}&text=${data.title}&hashtags=${data.tags.slice(0, 3).join(',').replace(/ /g, '')}`"
target="_blank"
>
<iconify-icon
icon="logos:twitter"
width="2em"
height="2em"
class="drop-shadow"
/>
</NuxtLink>
<div
class="grid-thumbnail-title accent-fade w-full rounded-b-lg p-2 mt-16"
>
<h3 class="mb-1">{{ data.title }}</h3>
<div class="flex flex-row flex-wrap gap-x-2 gap-y-0">
<small class="flex flex-row items-center gap-1 whitespace-nowrap">
<iconify-icon icon="mdi:calendar" inline />
<strong>
{{ formatDate(data.created, 'HH:mm • LLLL do, y') }}
</strong>
</small>
<small class="flex flex-row items-center gap-1 whitespace-nowrap">
<iconify-icon icon="mdi:clock-outline" inline />
<strong>
{{ data!.readingTime.text.split(' ')[0] + ' minutes to read' }}
</strong>
</small>
</div>
</div>
</div>
<section ref="content" class="page snap-start">
<ContentRenderer :value="data" />
</section>
</article>
<NotFound v-else />
</template>
<style scoped lang="scss">
.grid-thumbnail {
&-image {
grid-column: 1;
grid-row: 1;
place-self: start center;
max-width: clamp(1px, 100%, 768px);
min-height: 100%;
}
&-title {
grid-column: 1;
grid-row: 1;
place-self: end center;
}
&-start {
grid-column: 1;
grid-row: 1;
place-self: start start;
}
&-end {
grid-column: 1;
grid-row: 1;
place-self: start end;
}
}
</style>
+152
View File
@@ -0,0 +1,152 @@
<script setup lang="ts">
import { formatDate } from 'date-fns'
import { defineRouteRules } from '#imports'
const config = useAppConfig()
const meta = {
title: 'The Ender Chest',
description:
'A blog about software development, scientific research and Windows quirks.',
image: `${config.url}/images/chest.png`,
url: `${config.url}/blog`,
}
useSeoMeta({
title: meta.title,
description: meta.description,
ogTitle: meta.title,
ogDescription: meta.description,
ogImage: meta.image,
ogUrl: meta.url,
ogType: 'website',
twitterTitle: meta.title,
twitterDescription: meta.description,
twitterImage: meta.image,
twitterCard: 'summary',
})
useHead({
title: 'The Ender Chest',
htmlAttrs: {
lang: config.locale || 'en',
},
link: [
{
rel: 'icon',
type: 'image/x-icon',
href: '/favicon.ico',
},
],
})
defineRouteRules({
sitemap: {
changefreq: 'monthly',
priority: 0.7,
},
})
</script>
<template>
<section>
<h3 class="mb-2">The Ender Chest</h3>
<p class="mb-4">
You're browsing the Ender Chest — a blog about software development,
scientific research and Windows quirks.
</p>
<h4 class="mb-2">Recent posts</h4>
<ContentList
:query="{
path: '/blog',
where: [{ draft: false }],
limit: 3,
sort: [{ created: -1 }],
}"
>
<template #default="{ list }">
<div class="grid gap-4">
<NuxtLink
v-for="post in list"
:key="post._path"
:to="post._path"
class="text-inherit hover:text-inherit no-underline"
>
<article
class="post-box relative flex flex-col md:flex-row gap-4 p-2 border-2 rounded-xl transition-ease"
>
<img
draggable="false"
:src="
post.thumbnail ??
`/images/blog/thumbnails/${post._path!.split('/').at(-1)}.png`
"
:alt="post.title"
class="md:flex-grow-[2] md:basis-0 xl:flex-grow-1 object-cover object-center aspect-[16/9] w-fit md:w-0 xl:w-fit h-full max-h-[300px] rounded-xl"
/>
<div class="md:flex-grow-[3] md:basis-0 xl:flex-grow-1 w-full">
<h3 class="post-title md:line-clamp-2 md:overflow-ellipsis">
{{ post.title }}
</h3>
<p
class="post-description mb-0 md:line-clamp-4 md:overflow-ellipsis"
>
{{ post.description }}
</p>
<div class="flex flex-row flex-wrap gap-2 py-2">
<small
v-for="(tag, index) in post.tags.slice(0, 3)"
:key="index"
class="post-tag px-2 py-1 rounded-xl border whitespace-nowrap"
>
{{ tag }}
</small>
<small
v-if="post.tags.length > 3"
class="post-tag px-2 py-1 rounded-xl border whitespace-nowrap"
>
{{ post.tags.length - 3 }} more
</small>
</div>
<div class="post-details py-2">
<div class="flex flex-row items-center gap-2">
<iconify-icon icon="mdi:calendar" />
<small class="whitespace-nowrap">
{{ formatDate(post.created, 'HH:mm • LLLL do, y') }}
</small>
</div>
<div
v-if="post.updated"
class="flex flex-row items-center gap-2"
>
<iconify-icon icon="mdi:pencil" />
<small class="whitespace-nowrap">
{{ formatDate(post.updated, 'HH:mm • LLLL do, y') }}
</small>
</div>
</div>
</div>
<div
class="post-pocket border-l border-t absolute bottom-0 right-0 uppercase rounded-tl-xl flex flex-row items-center gap-2 p-2"
>
<iconify-icon icon="mdi:clock-outline" />
<small class="whitespace-nowrap font-mono font-small">
{{ post.readingTime.text }}
</small>
</div>
</article>
</NuxtLink>
</div>
</template>
<template #not-found>
<div class="flex flex-col justify-center items-center gap-4">
<span>No posts found 🙁</span>
<NuxtLink to="/">Back to index</NuxtLink>
</div>
</template>
</ContentList>
</section>
</template>
+27 -20
View File
@@ -1,10 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { defineRouteRules } from '#imports'
const config = useAppConfig()
const meta = { const meta = {
title: 'Enderman', title: 'Enderman',
description: description: config.description,
'A software engineer, a malware enthusiast and, most importantly, a weird tall creature.', image: `${config.url}/images/logo.png`,
image: 'https://enderman.ch/images/logo.png', url: config.url,
url: 'https://enderman.ch/',
} }
useSeoMeta({ useSeoMeta({
@@ -34,30 +36,36 @@ useHead({
}, },
], ],
}) })
defineRouteRules({
sitemap: {
changefreq: 'yearly',
priority: 1,
},
})
</script> </script>
<template> <template>
<section> <section class="page">
<h3 class="alex">Welcome 👋</h3> <h3>Welcome 👋</h3>
<p> <p>
I'm <strong>Enderman</strong> &ndash; a software engineer, a malware I'm <strong>Enderman</strong> &ndash; a software engineer, a malware
enthusiast and most importantly, a weird tall creature. I have over 300K enthusiast and most importantly, a weird tall creature. I have over 300K
subscribers on subscribers on <a :href="`${config.shortener}/youtube`">YouTube</a> and
<a class="link-hi" href="https://go.enderman.ch/youtube">YouTube</a> and over 25K followers on
over 20K followers on <a :href="`${config.shortener}/twitter`">Twitter</a>. Sometimes I wish
<a class="link-hi" href="https://go.enderman.ch/twitter">Twitter</a>. there were 48 hours in a day.
Sometimes I wish there were 48 hours in a day.
</p> </p>
<div class="row g-3"> <div class="flex flex-col md:flex-row gap-4">
<div class="col-12 col-md-8"> <div class="md:basis-0 md:flex-grow-[2]">
<h5 class="alex">What I do</h5> <h6>What I do</h6>
<ul class="list-style-type-do"> <ul class="list-style-type-do">
<li> <li>
I develop cross-platform software. My language of choice is usually I develop cross-platform software. My language of choice is usually
<strong>C/C++</strong> for desktop applications and any common <strong>C/C++</strong> for desktop applications and any common
backend stack with framework-loaded TypeScript for web. You can take backend stack with framework-loaded TypeScript for web. You can take
a look at my code on a look at my code on
<a class="link-hi" href="https://go.enderman.ch/github">GitHub</a>. <a :href="`${config.shortener}/github`">GitHub</a>.
</li> </li>
<li> <li>
I have the most unnecessary, yet fascinating knowledge about I have the most unnecessary, yet fascinating knowledge about
@@ -70,14 +78,13 @@ useHead({
<li> <li>
I research and analyze modern malware, educate computer users about I research and analyze modern malware, educate computer users about
it and preserve history. The repository can be found it and preserve history. The repository can be found
<a class="link-hi" href="https://go.enderman.ch/repository">here</a <a :href="`${config.shortener}/repository`">here</a>.
>.
</li> </li>
<li>I make videos for you to enjoy!</li> <li>I make videos for you to enjoy!</li>
</ul> </ul>
</div> </div>
<div class="col-12 col-md-4"> <div class="md:basis-0 md:flex-grow-[1]">
<h5 class="alex">What I enjoy</h5> <h6>What I enjoy</h6>
<ul class="list-style-type-enjoy"> <ul class="list-style-type-enjoy">
<li>Teaching</li> <li>Teaching</li>
<li>Mathematics</li> <li>Mathematics</li>
@@ -86,7 +93,7 @@ useHead({
<li>Philosophy</li> <li>Philosophy</li>
<li>Geopolitics</li> <li>Geopolitics</li>
<li> <li>
<a class="link-hi" href="https://go.enderman.ch/chess">Chess</a> <a :href="`${config.shortener}/chess`">Chess</a>
</li> </li>
<li>Solitude</li> <li>Solitude</li>
</ul> </ul>
@@ -1,5 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
// AI-Generated test data. import { defineRouteRules } from '#imports'
const config = useAppConfig()
const projects = [ const projects = [
{ {
name: 'MalwareWatch', name: 'MalwareWatch',
@@ -25,7 +27,7 @@ const projects = [
{ {
name: 'Windows XP Keygen / UMSKT', name: 'Windows XP Keygen / UMSKT',
description: 'A key generator for Windows XP.', description: 'A key generator for Windows XP.',
url: 'https://go.enderman.ch/xpkeygen', url: `${config.shortener}/xpkeygen`,
}, },
] ]
@@ -33,8 +35,8 @@ const meta = {
title: "Enderman's Projects", title: "Enderman's Projects",
description: description:
"A display of my largest projects. Some of them aren't published on YouTube.", "A display of my largest projects. Some of them aren't published on YouTube.",
image: 'https://enderman.ch/images/logo.png', image: `${config.url}/images/logo.png`,
url: 'https://enderman.ch/projects', url: `${config.url}/projects`,
} }
useSeoMeta({ useSeoMeta({
@@ -54,7 +56,7 @@ useSeoMeta({
useHead({ useHead({
title: 'Projects', title: 'Projects',
htmlAttrs: { htmlAttrs: {
lang: 'en', lang: config.locale || 'en',
}, },
link: [ link: [
{ {
@@ -64,23 +66,23 @@ useHead({
}, },
], ],
}) })
defineRouteRules({
sitemap: {
changefreq: 'monthly',
priority: 0.7,
},
})
</script> </script>
<template> <template>
<section> <section class="page">
<h3 class="alex">Projects</h3> <h3>Projects</h3>
<hr /> <Construction />
<p>
<strong>🚧 This page is currently under construction.</strong> Expect a
lot more content to be added as time passes.
<em>Please report all bugs you encounter via the link in the footer</em>,
I will make sure to sand them down.
</p>
<hr />
<p><strong>My current projects are:</strong></p> <p><strong>My current projects are:</strong></p>
<ul> <ul>
<li v-for="(item, index) in projects" :key="index"> <li v-for="(item, index) in projects" :key="index">
<a class="link-hi" :href="item.url"> <a :href="item.url">
{{ item.name }} {{ item.name }}
</a> </a>
</li> </li>
+159
View File
@@ -0,0 +1,159 @@
<script setup lang="ts">
import { defineRouteRules } from '#imports'
const config = useAppConfig()
const fqdn = config.url.split('//')[1]
const socials = [
{
name: 'YouTube',
url: `${config.shortener}/youtube`,
icon: {
name: 'logos:youtube-icon',
color: 'white',
},
},
{
name: 'Andrew',
url: `${config.shortener}/andrew`,
icon: {
name: 'logos:youtube-icon',
color: 'white',
},
},
{
name: 'Twitch',
url: `${config.shortener}/twitch`,
icon: {
name: 'fa:twitch',
color: '#B68CFF',
},
},
{
name: 'TikTok',
url: `${config.shortener}/tiktok`,
icon: {
name: 'logos:tiktok-icon',
color: 'white',
},
},
{
name: 'Twitter',
url: `${config.shortener}/twitter`,
icon: {
name: 'logos:twitter',
color: 'white',
},
},
{
name: 'Telegram',
url: `${config.shortener}/telegram`,
icon: {
name: 'logos:telegram',
color: 'white',
},
},
{
name: 'Discord',
url: `${config.shortener}/discord`,
icon: {
name: 'logos:discord-icon',
color: 'white',
},
},
{
name: 'GitHub',
url: `${config.shortener}/github`,
icon: {
name: 'fa:github',
color: 'white',
},
},
{
name: 'Steam',
url: `${config.shortener}/steam`,
icon: {
name: 'fa:steam',
color: 'white',
},
},
]
const meta = {
title: 'Enderman Online',
description:
'Discover me on your favorite social media and find ways to contact me here.',
image: `${config.url}/images/logo.png`,
url: `${config.url}/projects`,
}
useSeoMeta({
title: meta.title,
description: meta.description,
ogTitle: meta.title,
ogDescription: meta.description,
ogImage: meta.image,
ogUrl: meta.url,
ogType: 'website',
twitterTitle: meta.title,
twitterDescription: meta.description,
twitterImage: meta.image,
twitterCard: 'summary',
})
useHead({
title: 'Socials',
htmlAttrs: {
lang: config.locale || 'en',
},
link: [
{
rel: 'icon',
type: 'image/x-icon',
href: '/favicon.ico',
},
],
})
defineRouteRules({
sitemap: {
changefreq: 'yearly',
priority: 0.7,
},
})
</script>
<template>
<section class="page">
<h3>Online presence</h3>
<Construction />
<div class="flex flex-col md:flex-row gap-4">
<div class="md:basis-0 md:flex-grow-[1]">
<h5>Social media</h5>
<ul class="list-style-type-none p-0">
<li v-for="(page, index) in socials" :key="index">
<NuxtLink target="_blank" rel="noopener" :href="page.url">
<iconify-icon
:icon="page.icon.name"
:style="{ color: page.icon.color }"
width="1em"
height="1em"
inline
/>
<span class="mx-2">{{ page.name }}</span>
</NuxtLink>
</li>
</ul>
</div>
<div class="md:basis-0 md:flex-grow-[3]">
<h5>Contact me</h5>
<p>
Personal:
<EMail :address="`contact@${fqdn}`" subject="Hey Enderman!" /><br />
Manager: <EMail :address="`manager@${fqdn}`" /><br />
Abuse: <EMail :address="`abuse@${fqdn}`" />
</p>
</div>
</div>
</section>
</template>
+21
View File
@@ -0,0 +1,21 @@
export default defineNuxtPlugin(() => {
return {
provide: {
local: {
getItem(item: string) {
if (import.meta.client) {
return localStorage.getItem(item)
} else {
return undefined
}
},
setItem(item: string, value: any) {
if (import.meta.client) {
return localStorage.setItem(item, value)
}
},
},
},
}
})
+31 -2
View File
@@ -1,4 +1,7 @@
import { defineStore } from 'pinia' import aboutIcon from 'assets/images/icons/accent/info.avif'
import projectIcon from 'assets/images/icons/defrag.avif'
import socialIcon from 'assets/images/icons/user.avif'
import blogIcon from 'assets/images/icons/book.avif'
export const usePageStore = defineStore('page', () => { export const usePageStore = defineStore('page', () => {
const title: string = 'Enderman' const title: string = 'Enderman'
@@ -14,18 +17,42 @@ export const usePageStore = defineStore('page', () => {
{ {
name: 'About', name: 'About',
path: '/about', path: '/about',
icon: {
src: aboutIcon,
alt: 'Information',
},
}, },
{ {
name: 'Projects', name: 'Projects',
path: '/projects', path: '/projects',
icon: {
src: projectIcon,
alt: 'Blocks',
},
}, },
{ {
name: 'Socials', name: 'Socials',
path: '/social', path: '/social',
icon: {
src: socialIcon,
alt: 'Users',
},
},
{
name: 'Blog',
path: '/blog',
icon: {
src: blogIcon,
alt: 'Book',
},
}, },
]) ])
function autoFetchPages() { const reader = ref<boolean>(false)
const animate = ref<boolean>(false)
const dark = ref<boolean>(false)
function _autoFetchPages() {
while (pages.value.length) pages.value.pop() while (pages.value.length) pages.value.pop()
useRouter() useRouter()
@@ -43,5 +70,7 @@ export const usePageStore = defineStore('page', () => {
description, description,
keywords, keywords,
pages, pages,
reader,
animate,
} }
}) })
BIN
View File
Binary file not shown.
+38
View File
@@ -0,0 +1,38 @@
import packageJSON from './package.json'
interface TitleConfig {
full: string
short: string
}
interface BuildConfig {
date?: string
version: string
}
type config = {
url: string
shortener: string
name: string
title: TitleConfig
description: string
locale: string
build: BuildConfig
}
export default {
url: 'https://enderman.ch',
shortener: 'https://go.enderman.ch',
name: packageJSON.name || 'app',
title: {
full: "Enderman's Website",
short: 'Enderman',
},
description:
'A software engineer, a malware enthusiast and, most importantly, a weird tall creature. I have over 300K subscribers on YouTube and over 20K followers on Twitter.',
locale: 'en',
build: {
date: new Date().toISOString().split('T')[0],
version: packageJSON.version || '0.0.0',
},
} satisfies config as config

Some files were not shown because too many files have changed in this diff Show More