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_modules
# Cache
.stylelintcache
# Logs
logs
*.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">
import sky from '~/assets/images/portal/sky.png'
import particles from '~/assets/images/portal/particles.png'
import sky from 'assets/images/textures/sky.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 animated = defineModel<boolean>({
required: true,
})
const props = defineProps({
layout: {
@@ -62,7 +69,7 @@ class Canvas {
compatible(
obj: HTMLCanvasElement | HTMLElement | Element,
): obj is HTMLCanvasElement {
return obj.tagName === 'CANVAS'
return obj.tagName.toLowerCase() === 'canvas'
}
query(): HTMLCanvasElement {
@@ -133,7 +140,7 @@ class Portal {
constructor(
directory: string = '/',
create: boolean = true,
animate: boolean = true,
animate: boolean = false,
randomize: boolean = false,
fade: boolean = false,
speed: number = 1,
@@ -249,7 +256,7 @@ class Portal {
this.animate = animate
this.randomize = randomize
// Set default speed.
// Set the default speed.
this.speed = speed
this.initialSpeed = this.speed
this.dv = 20
@@ -316,7 +323,7 @@ class Portal {
notice() {
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: #D52DFF',
'color: #E58EFF',
@@ -327,7 +334,7 @@ class Portal {
)
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',
)
}
@@ -565,10 +572,6 @@ class Portal {
resize() {
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 (!this.animate) requestAnimationFrame(this.render.bind(this))
}
@@ -577,8 +580,8 @@ class Portal {
this.clickTime = Date.now()
}
continue() {
if (this.pauseTime === 0) return
play() {
if (this.pauseTime === 0) this.pauseTime = this.currentTime
// Add the time that has passed since the pause to the current time.
this.currentTime += Date.now() - this.pauseTime
@@ -611,47 +614,53 @@ class Portal {
}
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 portal = new Portal(
props.directory,
props.create,
props.animate,
animated.value,
props.randomize,
props.fade,
props.speed,
)
layout!.addEventListener('mousedown', ((e: UIEvent) => {
if (e.target === e.currentTarget && e.detail >= 2) {
const click: EventListener = (e: Event) => {
if (e.target === e.currentTarget) {
e.preventDefault()
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>
<template>
<canvas id="ecmaportal" class="parallax">
<canvas
id="ecmaportal"
class="fixed top-0 left-0 opacity-0 transition-portal -z-10"
>
<span>
Your browser does not support the &lt;canvas /&gt; element, which is
required for parallax animation.
</span>
</canvas>
</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,
default: 'Link',
},
width: {
type: Number,
default: 32,
},
height: {
type: Number,
default: 32,
},
})
</script>
<template>
<NuxtLink
class="link d-flex flex-row align-items-center gap-2 user-select-none no-decoration"
active-class="icon-active px-sm-3"
class="flex flex-row items-center gap-2 select-none text-inherit no-underline"
active-class="active"
:to="!props.external ? props.path : undefined"
:href="props.external ? props.path : undefined"
>
<img
class="icon-image"
draggable="false"
:src="props.icon"
:alt="props.alt"
:width="props.width"
:height="props.height"
/>
<span class="display-xs-none">
<span class="hidden lm:hidden lt:hidden sm:block">
<strong>
{{ props.name }}
</strong>
@@ -42,14 +51,8 @@ const props = defineProps({
</template>
<style scoped lang="scss">
.icon {
&-active {
.active {
@apply px-4;
transform: translate(2px, 2px) rotate3d(1, 1, 1, 5deg) scale(1.25);
}
&-image {
width: 1.5em;
height: 1.5em;
}
}
</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">
import { defineRouteRules } from '#imports'
const config = useAppConfig()
const age =
new Date(Number(new Date()) - Number(new Date('2003-09-18'))).getFullYear() -
1970
@@ -6,8 +9,8 @@ const age =
const meta = {
title: 'About Me',
description: `Nice to meet you!~ I'm Andrew, a ${age}-year-old developer from Kaluga, Russia.`,
image: 'https://enderman.ch/images/logo.png',
url: 'https://enderman.ch/about',
image: `${config.url}/images/logo.png`,
url: `${config.url}/about`,
}
useSeoMeta({
@@ -27,7 +30,7 @@ useSeoMeta({
useHead({
title: 'About',
htmlAttrs: {
lang: 'en',
lang: config.locale || 'en',
},
link: [
{
@@ -37,30 +40,31 @@ useHead({
},
],
})
defineRouteRules({
sitemap: {
changefreq: 'yearly',
priority: 0.6,
},
})
</script>
<template>
<section>
<h3 class="alex">About me</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 />
<section class="page">
<h3>About me</h3>
<Construction />
<p>
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
been interested in technology. I'm a middle-senior C/++ developer and a
junior full-stack engineer.
<sup>1</sup>
</p>
<p class="font-small">
<br />
<small>
<sup>1</sup>
All titles are based on knowledge and confidence, not on official work
experience.
</small>
</p>
<p><strong>Here's a little more about myself:</strong></p>
<ul>
@@ -69,9 +73,28 @@ useHead({
<li>My favorite field in mathematics is Algebraic Geometry.</li>
</ul>
<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
check them out!
</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>
</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">
import { defineRouteRules } from '#imports'
const config = useAppConfig()
const meta = {
title: 'Enderman',
description:
'A software engineer, a malware enthusiast and, most importantly, a weird tall creature.',
image: 'https://enderman.ch/images/logo.png',
url: 'https://enderman.ch/',
description: config.description,
image: `${config.url}/images/logo.png`,
url: config.url,
}
useSeoMeta({
@@ -34,30 +36,36 @@ useHead({
},
],
})
defineRouteRules({
sitemap: {
changefreq: 'yearly',
priority: 1,
},
})
</script>
<template>
<section>
<h3 class="alex">Welcome 👋</h3>
<section class="page">
<h3>Welcome 👋</h3>
<p>
I'm <strong>Enderman</strong> &ndash; a software engineer, a malware
enthusiast and most importantly, a weird tall creature. I have over 300K
subscribers on
<a class="link-hi" href="https://go.enderman.ch/youtube">YouTube</a> and
over 20K followers on
<a class="link-hi" href="https://go.enderman.ch/twitter">Twitter</a>.
Sometimes I wish there were 48 hours in a day.
subscribers on <a :href="`${config.shortener}/youtube`">YouTube</a> and
over 25K followers on
<a :href="`${config.shortener}/twitter`">Twitter</a>. Sometimes I wish
there were 48 hours in a day.
</p>
<div class="row g-3">
<div class="col-12 col-md-8">
<h5 class="alex">What I do</h5>
<div class="flex flex-col md:flex-row gap-4">
<div class="md:basis-0 md:flex-grow-[2]">
<h6>What I do</h6>
<ul class="list-style-type-do">
<li>
I develop cross-platform software. My language of choice is usually
<strong>C/C++</strong> for desktop applications and any common
backend stack with framework-loaded TypeScript for web. You can take
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>
I have the most unnecessary, yet fascinating knowledge about
@@ -70,14 +78,13 @@ useHead({
<li>
I research and analyze modern malware, educate computer users about
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>I make videos for you to enjoy!</li>
</ul>
</div>
<div class="col-12 col-md-4">
<h5 class="alex">What I enjoy</h5>
<div class="md:basis-0 md:flex-grow-[1]">
<h6>What I enjoy</h6>
<ul class="list-style-type-enjoy">
<li>Teaching</li>
<li>Mathematics</li>
@@ -86,7 +93,7 @@ useHead({
<li>Philosophy</li>
<li>Geopolitics</li>
<li>
<a class="link-hi" href="https://go.enderman.ch/chess">Chess</a>
<a :href="`${config.shortener}/chess`">Chess</a>
</li>
<li>Solitude</li>
</ul>
@@ -1,5 +1,7 @@
<script setup lang="ts">
// AI-Generated test data.
import { defineRouteRules } from '#imports'
const config = useAppConfig()
const projects = [
{
name: 'MalwareWatch',
@@ -25,7 +27,7 @@ const projects = [
{
name: 'Windows XP Keygen / UMSKT',
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",
description:
"A display of my largest projects. Some of them aren't published on YouTube.",
image: 'https://enderman.ch/images/logo.png',
url: 'https://enderman.ch/projects',
image: `${config.url}/images/logo.png`,
url: `${config.url}/projects`,
}
useSeoMeta({
@@ -54,7 +56,7 @@ useSeoMeta({
useHead({
title: 'Projects',
htmlAttrs: {
lang: 'en',
lang: config.locale || 'en',
},
link: [
{
@@ -64,23 +66,23 @@ useHead({
},
],
})
defineRouteRules({
sitemap: {
changefreq: 'monthly',
priority: 0.7,
},
})
</script>
<template>
<section>
<h3 class="alex">Projects</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 />
<section class="page">
<h3>Projects</h3>
<Construction />
<p><strong>My current projects are:</strong></p>
<ul>
<li v-for="(item, index) in projects" :key="index">
<a class="link-hi" :href="item.url">
<a :href="item.url">
{{ item.name }}
</a>
</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', () => {
const title: string = 'Enderman'
@@ -14,18 +17,42 @@ export const usePageStore = defineStore('page', () => {
{
name: 'About',
path: '/about',
icon: {
src: aboutIcon,
alt: 'Information',
},
},
{
name: 'Projects',
path: '/projects',
icon: {
src: projectIcon,
alt: 'Blocks',
},
},
{
name: 'Socials',
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()
useRouter()
@@ -43,5 +70,7 @@ export const usePageStore = defineStore('page', () => {
description,
keywords,
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