Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97c0b7c5ed | ||
|
|
ad70bdc5b6 | ||
|
|
a7c03d2327 | ||
|
|
673948bce9 | ||
|
|
7a3cdcc1da | ||
|
|
44469bd9f2 | ||
|
|
00060ede92 | ||
|
|
2204709c0e | ||
|
|
3e39bda014 | ||
|
|
05db0b693e | ||
|
|
4543680a92 | ||
|
|
858142f09a | ||
|
|
830c34855c | ||
|
|
a7f2d527dc | ||
|
|
e72a61b317 | ||
|
|
9339416c1b | ||
|
|
753bdb5c0a | ||
|
|
bfeaca5f09 | ||
|
|
10e3a7fc40 | ||
|
|
1ed1d47b07 | ||
|
|
d1e249b51d | ||
|
|
7ce8674fea | ||
|
|
36dd770ce4 | ||
|
|
eadc0b119d | ||
|
|
44c60f7069 | ||
|
|
b58407d358 | ||
|
|
9386c1d158 | ||
|
|
3853e8fa20 | ||
|
|
cfbaf56159 | ||
|
|
014e64d174 | ||
|
|
1f7c8f0ed4 | ||
|
|
4ac1fd9574 | ||
|
|
dd084afb61 | ||
|
|
3c69f73dc6 | ||
|
|
92ad0a50f9 | ||
|
|
4939d24f62 | ||
|
|
2bd1e4a512 |
@@ -1,5 +0,0 @@
|
|||||||
> 1%
|
|
||||||
last 5 major versions
|
|
||||||
not dead
|
|
||||||
not ie <= 11
|
|
||||||
not op_mini all
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# .dockerignore
|
||||||
|
/.git
|
||||||
|
/.nuxt
|
||||||
|
/.output
|
||||||
|
/node_modules
|
||||||
|
|
||||||
|
.gitignore
|
||||||
|
README.md
|
||||||
|
|
||||||
|
.stylelintcache
|
||||||
@@ -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'],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -9,6 +9,9 @@ dist
|
|||||||
# Node dependencies
|
# Node dependencies
|
||||||
node_modules
|
node_modules
|
||||||
|
|
||||||
|
# Cache
|
||||||
|
.stylelintcache
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
logs
|
logs
|
||||||
*.log
|
*.log
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"semi": false,
|
|
||||||
"singleQuote": true,
|
|
||||||
"tabWidth": 2,
|
|
||||||
"trailingComma": "all"
|
|
||||||
}
|
|
||||||
@@ -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"]
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
node_modules/
|
|
||||||
assets/styles/**/*.css
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
defaultSeverity: 'warning',
|
|
||||||
extends: [
|
|
||||||
'stylelint-config-standard-scss',
|
|
||||||
'stylelint-config-recommended-vue',
|
|
||||||
],
|
|
||||||
plugins: [],
|
|
||||||
rules: {
|
|
||||||
'declaration-empty-line-before': null,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 18 KiB |
@@ -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 +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"}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
{"version":3,"sourceRoot":"","sources":["transitions.scss"],"names":[],"mappings":"AACE;EAEE;;AAGF;EAEE;EACA","file":"transitions.css"}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
.page {
|
|
||||||
&-enter-active,
|
|
||||||
&-leave-active {
|
|
||||||
transition: all 0.15s;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-enter-from,
|
|
||||||
&-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
filter: blur(1em);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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
|
||||||
@@ -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>
|
|
||||||
@@ -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],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
@@ -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)
|
|
||||||
})
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import { Screen } from 'quasar'
|
|
||||||
|
|
||||||
export default defineNuxtPlugin(() => {
|
|
||||||
Screen.setSizes({
|
|
||||||
sm: 601,
|
|
||||||
md: 996,
|
|
||||||
lg: 1281,
|
|
||||||
xl: 1921,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -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))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
User-agent: *
|
|
||||||
Disallow: /images
|
|
||||||
Sitemap: https://enderman.ch/sitemap.xml
|
|
||||||
@@ -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>
|
|
||||||
@@ -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" ]
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import config from '../config'
|
||||||
|
|
||||||
|
export default defineAppConfig({
|
||||||
|
...config,
|
||||||
|
})
|
||||||
@@ -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>
|
||||||
|
After Width: | Height: | Size: 200 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 105 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
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 |
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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%;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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">▊</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 <canvas /> element, which is
|
Your browser does not support the <canvas /> 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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export default defineNuxtRouteMiddleware((to, from) => {
|
||||||
|
const { reader } = storeToRefs(usePageStore())
|
||||||
|
|
||||||
|
reader.value = /\/blog\/(.*)/.test(to.path)
|
||||||
|
})
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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> – a software engineer, a malware
|
I'm <strong>Enderman</strong> – 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>
|
||||||
@@ -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>
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -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,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -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
|
||||||