Rewrite to TailwindCSS, enhance the DOM, uninstall Vuetify, improve styles

This commit is contained in:
Andrew Illarionov 2024-06-16 22:24:01 +03:00
parent 9339416c1b
commit e72a61b317
50 changed files with 1843 additions and 3459 deletions

View File

@ -1,5 +0,0 @@
> 1%
last 5 major versions
not dead
not ie <= 11
not op_mini all

View File

@ -1,33 +0,0 @@
module.exports = {
root: true,
env: {
browser: true,
node: true,
},
extends: [
'@nuxtjs/eslint-config-typescript',
'plugin:nuxt/recommended',
'plugin:prettier/recommended',
],
plugins: [],
rules: {
lintOnStart: 0,
indent: ['error', 2, { SwitchCase: 1 }],
quotes: [2, 'single', { avoidEscape: true }],
'no-console': 'off',
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
},
],
'vue/no-multiple-template-root': 'off',
'vue/multi-word-component-names': 'off',
},
settings: {
'import/ignore': ['vue-fontawesome'],
},
}

3
.gitignore vendored
View File

@ -9,6 +9,9 @@ dist
# Node dependencies # Node dependencies
node_modules node_modules
# Cache
.stylelintcache
# Logs # Logs
logs logs
*.log *.log

View File

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

View File

@ -1,6 +0,0 @@
{
"semi": false,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "all"
}

View File

@ -1 +1 @@
[{"D:\\Software\\Development\\Websites\\enderman.ch\\index\\assets\\styles\\transitions.scss":"1","D:\\Software\\Development\\Websites\\enderman.ch\\index\\app.vue":"2","D:\\Software\\Development\\Websites\\enderman.ch\\index\\components\\EMail.vue":"3","D:\\Software\\Development\\Websites\\enderman.ch\\index\\pages\\about.vue":"4","D:\\Software\\Development\\Websites\\enderman.ch\\index\\pages\\index.vue":"5","D:\\Software\\Development\\Websites\\enderman.ch\\index\\pages\\projects.vue":"6","D:\\Software\\Development\\Websites\\enderman.ch\\index\\pages\\social.vue":"7","D:\\Software\\Development\\Websites\\enderman.ch\\index\\layouts\\Card.vue":"8","D:\\Software\\Development\\Websites\\enderman.ch\\index\\components\\blocks\\Options.vue":"9","D:\\Software\\Development\\Websites\\enderman.ch\\index\\components\\blocks\\Flooter.vue":"10","D:\\Software\\Development\\Websites\\enderman.ch\\index\\components\\blocks\\SwipeControls.vue":"11","D:\\Software\\Development\\Websites\\enderman.ch\\index\\components\\blocks\\Logo.vue":"12","D:\\Software\\Development\\Websites\\enderman.ch\\index\\components\\blocks\\Route.vue":"13","D:\\Software\\Development\\Websites\\enderman.ch\\index\\components\\animations\\Portal.vue":"14","D:\\Software\\Development\\Websites\\enderman.ch\\index\\components\\ui\\Icon.vue":"15","D:\\Software\\Development\\Websites\\enderman.ch\\index\\assets\\styles\\vuetify.scss":"16","D:\\Software\\Development\\Websites\\enderman.ch\\index\\pages\\[...slug].vue":"17","D:\\Software\\Development\\Websites\\enderman.ch\\index\\pages\\blog\\index.vue":"18","D:\\Software\\Development\\Websites\\enderman.ch\\index\\pages\\blog\\[slug].vue":"19","D:\\Software\\Development\\Websites\\enderman.ch\\index\\components\\content\\Card.vue":"20","D:\\Software\\Development\\Websites\\enderman.ch\\index\\components\\content\\OgImage.vue":"21","D:\\Software\\Development\\Websites\\enderman.ch\\index\\components\\animations\\NotFound.vue":"22"},{"size":165,"mtime":1700688268778,"hashOfConfig":"23"},{"size":1550,"mtime":1700850776284,"hashOfConfig":"23"},{"size":804,"mtime":1700688952880,"hashOfConfig":"23"},{"size":2135,"mtime":1700689265787,"hashOfConfig":"23"},{"size":3117,"mtime":1700689271383,"hashOfConfig":"23"},{"size":2304,"mtime":1700689265769,"hashOfConfig":"23"},{"size":3232,"mtime":1700689265781,"hashOfConfig":"23"},{"size":1623,"mtime":1700748530633,"hashOfConfig":"23"},{"size":3855,"mtime":1708455917401,"hashOfConfig":"24"},{"size":1132,"mtime":1708254584095,"hashOfConfig":"24"},{"size":1269,"mtime":1708255068679,"hashOfConfig":"24"},{"size":1005,"mtime":1718446378238,"hashOfConfig":"24"},{"size":961,"mtime":1718445804903,"hashOfConfig":"24"},{"size":17150,"mtime":1708455647910,"hashOfConfig":"24"},{"size":935,"mtime":1706445231843,"hashOfConfig":"24"},{"size":120,"mtime":1706789613657,"hashOfConfig":"25"},{"size":2084,"mtime":1708424343371,"hashOfConfig":"26"},{"size":2560,"mtime":1708202402381,"hashOfConfig":"26"},{"size":983,"mtime":1708206997656,"hashOfConfig":"26"},{"size":1895,"mtime":1718444102273,"hashOfConfig":"24"},{"size":976,"mtime":1708431988520,"hashOfConfig":"26"},{"size":1673,"mtime":1718386852848,"hashOfConfig":"24"},"5tgxr3","lsooul","1lk8nat","1nljphs"] [{"D:\\Software\\Development\\Websites\\enderman.ch\\index\\components\\animations\\NotFound.vue":"1","D:\\Software\\Development\\Websites\\enderman.ch\\index\\components\\blocks\\Route.vue":"2","D:\\Software\\Development\\Websites\\enderman.ch\\index\\components\\blocks\\Logo.vue":"3","D:\\Software\\Development\\Websites\\enderman.ch\\index\\components\\blocks\\Settings.vue":"4","D:\\Software\\Development\\Websites\\enderman.ch\\index\\app.vue":"5","D:\\Software\\Development\\Websites\\enderman.ch\\index\\layouts\\Card.vue":"6"},{"size":1679,"mtime":1718453058852,"hashOfConfig":"7"},{"size":1065,"mtime":1718555359979,"hashOfConfig":"7"},{"size":985,"mtime":1718555386128,"hashOfConfig":"7"},{"size":3590,"mtime":1718563207348,"hashOfConfig":"7"},{"size":1874,"mtime":1718548398710,"hashOfConfig":"8"},{"size":1807,"mtime":1718548507733,"hashOfConfig":"8"},"2j789g","1lkkhgi"]

View File

@ -1,2 +0,0 @@
node_modules/
assets/styles/**/*.css

View File

@ -1,11 +0,0 @@
module.exports = {
defaultSeverity: 'warning',
extends: [
'stylelint-config-standard-scss',
'stylelint-config-recommended-vue',
],
plugins: [],
rules: {
'declaration-empty-line-before': null,
},
}

View File

@ -2,13 +2,4 @@ import config from './config'
export default defineAppConfig({ export default defineAppConfig({
...config, ...config,
nuxtIcon: {
size: '1em',
class: 'icon',
aliases: {},
iconifyApiOptions: {
url: 'https://api.iconify.design',
publicApiFallback: false,
},
},
}) })

81
app.vue
View File

@ -1,17 +1,18 @@
<script setup lang="ts"> <script setup lang="ts">
import 'iconify-icon'
const config = useAppConfig() const config = useAppConfig()
const theme = useVThemeSSR()
const vdisp = ref(useVDisplay())
const { reader } = storeToRefs(usePageStore())
const route = useRoute() const route = useRoute()
const { reader, animate } = storeToRefs(usePageStore())
useHead({ useHead({
titleTemplate: (chunk?) => { titleTemplate: (chunk?) => {
if (route.fullPath === '/') return config.title.full if (route.fullPath === '/') return config.title.full
if (route.fullPath.split('/').length === 2) if (route.fullPath.split('/').length === 2)
switch (route.fullPath.split('/')[1]) { switch (route.fullPath.split('/')[1]) {
case 'blog': case 'blog':
return 'The Enderchest' return 'The Ender Chest'
default: default:
return `${chunk} ${config.title.short}` return `${chunk} ${config.title.short}`
} }
@ -22,47 +23,39 @@ useHead({
</script> </script>
<template> <template>
<VThemeProvider <div
:theme="theme.dark.value ? config.theme.dark : config.theme.light" id="ender-layout"
class="flex flex-col lm:justify-center lt:justify-center items-center h-full"
> >
<div
id="ender-layout"
class="p-animated flex flex-col items-center h-full"
:class="{ 'sm:pt-8': !reader }"
>
<AClientOnly appear mode="out-in">
<SwipeControls v-if="vdisp.xs" />
</AClientOnly>
<NuxtLayout
name="card"
class="animate__animated-sm animate__delay-1-5s animate__fadeInDown"
>
<template #footer>
<AClientOnly
appear
mode="in-out"
enter="animate__animated animate__delay-1s animate__fadeIn"
leave="animate__animated animate__fadeOut"
>
<Flooter v-if="vdisp.xs" opaque />
</AClientOnly>
</template>
</NuxtLayout>
</div>
<AClientOnly
appear
mode="out-in"
enter="animate__animated animate__delay-1s animate__fadeInUpBig animate__slow"
leave="animate__animated animate__fadeOutDown"
>
<Flooter v-if="vdisp.smAndUp && !reader" class="floaty mb-4 px-2" />
</AClientOnly>
<ClientOnly> <ClientOnly>
<template #fallback> </template> <LazySwipeControls class="block sm:hidden" />
<LazyPortal layout="#ender-layout" animate randomize fade />
</ClientOnly> </ClientOnly>
</VThemeProvider>
<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>
<LazyPortal v-model="animate" layout="#ender-layout" randomize fade />
</ClientOnly>
</template> </template>

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
assets/images/chest.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 536 KiB

View File

@ -1 +0,0 @@
@layer base, vuetify, overrides

View File

@ -31,5 +31,9 @@
&-faq { &-faq {
list-style-type: faq; list-style-type: faq;
> li:nth-child(2n):not(:last-child) {
@apply mb-2;
}
} }
} }

View File

@ -3,6 +3,14 @@
// Modules. // Modules.
@use 'transitions'; @use 'transitions';
@use 'lists'; @use 'lists';
@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. // CSS Variables.
:root { :root {
@ -11,10 +19,47 @@
--animate-repeat: 1; --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. // The HTML page.
html { html {
height: 100%; height: 100%;
font-size: 14px; font-size: 14px;
::-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%);
}
&.light {
::-webkit-scrollbar-track {
background-color: rgb(255 255 255 / 15%);
}
::-webkit-scrollbar-thumb {
border-radius: 8px;
background-color: rgb(0 0 0 / 30%);
}
}
} }
// The body element. // The body element.
@ -69,8 +114,12 @@ h6 {
font-size: 1rem; font-size: 1rem;
} }
small {
font-size: 0.75rem;
}
a { a {
@apply text-inherit no-underline transition-all; @apply no-underline transition-all;
color: cornflowerblue; color: cornflowerblue;
@ -79,34 +128,41 @@ a {
} }
} }
ul { center {
padding-left: 1.5em;
}
// Entry point (Vue mount).
#ender-app {
flex: 1 1 0;
image-rendering: auto;
}
// Helper classes that don't exist in Bootstrap.
.inline {
display: inline;
}
.center {
text-align: center; text-align: center;
margin: auto; margin: auto;
} }
.nobr { :not(nav) > ul {
white-space: nowrap; list-style-type: disc;
padding-left: 1.5em;
} }
.clickable { :is(section, article, aside).page {
cursor: pointer; :is(div, p):not(:last-child) {
@apply mb-4;
}
:is(h1, h2, h3, h4, h5, h6):not(:last-child) {
@apply mb-2;
}
:is(ul, ol):not(:last-child) {
@apply mb-4;
}
} }
iconify-icon {
display: inline-block;
}
// Entry point (Vue mount).
#ender-app {
flex: 1 0 auto;
image-rendering: auto;
}
// Helper classes that don't exist in Bootstrap.
.overlay { .overlay {
grid-area: 1 / 1; grid-area: 1 / 1;
} }
@ -119,57 +175,59 @@ ul {
} }
} }
.no-decoration { ruby {
text-decoration: none; ruby-position: under;
color: inherit;
ruby {
ruby-position: over;
}
} }
.text-align-center { rt {
text-align: center; ruby-align: space-around;
} }
.font-small { .font-small {
font-size: 14px; font-size: 14px;
} }
.grid-cols-3 { .transition-ease {
grid-template-columns: repeat(2, minmax(0, 1fr)); @extend %transition;
}
.display-sm {
display: none;
}
.alex {
font-family: Alexandria, sans-serif;
font-weight: 700;
}
.pre-wrap {
white-space: pre-wrap;
word-break: keep-all;
}
.h-animated {
transition: max-height 0.3s ease;
}
.p-animated {
transition: padding 0.3s ease;
} }
// Query-overridable classes. // Query-overridable classes.
.background {
background-color: rgb(0 0 0 / 50%);
}
.dimensions { .dimensions {
width: 100%; width: 100%;
min-height: 100%; min-height: 100%;
} }
.accent { .accent-background {
color: white; background-color: rgb(0 0 0 / 50%);
}
.accent-overlay-background {
background-color: rgb(0 0 0 / 90%);
}
@responsive {
.accent-text {
color: white;
}
.link-dark {
@apply text-inherit no-underline transition-all;
color: cornflowerblue;
&:hover {
color: royalblue;
}
}
.fade-mask {
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%));
}
} }
.dialog { .dialog {
@ -212,16 +270,8 @@ ul {
transition: 0.3s ease; transition: 0.3s ease;
} }
.link { .parallax {
&-hi-force-dark { transition: all 0.6942s ease-in;
@apply text-inherit no-underline transition-all;
color: cornflowerblue;
&:hover {
color: royalblue;
}
}
} }
.accent-gradient { .accent-gradient {
@ -233,53 +283,26 @@ ul {
); );
} }
.scrollbar { .accent-text-shadow {
min-height: 128px; text-shadow: black 2px 3px 4px;
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%);
}
&::-webkit-scrollbar-button {
color: white;
}
} }
@media (prefers-color-scheme: light) { :where(html.light) {
body { body {
color: black; color: black;
} }
.scrollbar { .accent-background {
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%); background-color: rgb(255 255 255 / 80%);
} }
.accent-overlay-background {
background-color: rgb(255 255 255 / 90%);
}
a { a {
@apply no-underline transition-all;
color: royalblue; color: royalblue;
&:hover { &:hover {
@ -287,7 +310,7 @@ ul {
} }
} }
.accent { .accent-text {
color: black; color: black;
} }
@ -299,20 +322,14 @@ ul {
rgb(0 0 0 / 0%) rgb(0 0 0 / 0%)
); );
} }
}
.floaty { .accent-text-shadow {
position: fixed; text-shadow: white 1px 2px 3px;
bottom: 0; }
left: 0;
right: 0;
width: fit-content;
height: fit-content;
margin: auto;
} }
.post { .post {
scroll-snap-type: both mandatory; scroll-snap-type: y mandatory;
scroll-snap-stop: normal; scroll-snap-stop: normal;
&::after { &::after {
@ -337,19 +354,10 @@ ul {
} }
&-pocket { &-pocket {
position: absolute;
bottom: 0;
right: 0;
text-transform: uppercase;
background-color: rgb(153 153 255 / 10%); background-color: rgb(153 153 255 / 10%);
border-left: 1px solid rgb(153 153 255 / 60%); border-left: 1px solid rgb(153 153 255 / 60%);
border-top: 1px solid rgb(153 153 255 / 60%); border-top: 1px solid rgb(153 153 255 / 60%);
border-top-left-radius: var(--bs-border-radius-xl);
border-bottom-right-radius: var(--bs-border-radius-xl);
} }
&-thumb { &-thumb {
@ -377,45 +385,16 @@ ul {
} }
&-preamble { &-preamble {
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: flex-start;
position: relative;
width: 100%;
min-height: 400px;
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: center; background-position: center;
background-size: cover; background-size: cover;
background-attachment: fixed; background-attachment: fixed;
border-radius: 1em;
scroll-snap-align: end; scroll-snap-align: end;
text-shadow: black 1px 1px 7px;
iconify-icon { iconify-icon {
filter: drop-shadow(1px 2px 3px rgb(0 0 0 / 100%)); filter: drop-shadow(1px 2px 3px rgb(0 0 0 / 100%));
} }
&-control {
position: absolute;
top: 0;
left: 0;
}
&-share {
position: absolute;
top: 0;
right: 0;
}
} }
&-content { &-content {
@ -550,6 +529,13 @@ ul {
} }
// Dynamic classes. // Dynamic classes.
@screen lm {
.dimensions {
width: 100% !important;
min-height: 100% !important;
}
}
@screen sm { @screen sm {
html { html {
font-size: 16px; font-size: 16px;
@ -557,9 +543,7 @@ ul {
#ender-app { #ender-app {
height: 100%; height: 100%;
}
.scrollbar {
scrollbar-gutter: stable; scrollbar-gutter: stable;
} }
@ -569,18 +553,6 @@ ul {
max-height: 90%; 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-sm {
border-radius: 1em;
}
.display-sm {
display: block;
}
.animate__animated-sm { .animate__animated-sm {
animation-duration: var(--animate-duration); animation-duration: var(--animate-duration);
animation-fill-mode: both; animation-fill-mode: both;

View File

@ -1,6 +0,0 @@
@forward 'vuetify/settings' with (
$utilities: false,
$color-pack: false,
$reset: false,
$layers: true,
);

View File

@ -47,9 +47,10 @@ onMounted(() => setTimeout(type, 1500))
<template> <template>
<section> <section>
<h3 class="alex">Page not found!</h3> <h3>Page not found!</h3>
<pre <pre class="whitespace-pre-wrap break-keep font-small">{{
class="whitespace-pre-wrap break-keep">{{ buffer }}<span class="blinker">&#9610;</span></pre> buffer
}}<span class="blinker">&#9610;</span></pre>
</section> </section>
</template> </template>

View File

@ -2,12 +2,14 @@
import sky from '~/assets/images/textures/sky.png' import sky from '~/assets/images/textures/sky.png'
import particles from '~/assets/images/textures/particles.png' import particles from '~/assets/images/textures/particles.png'
const { $local } = useNuxtApp()
const config = useAppConfig() const config = useAppConfig()
const fqdn = config.url.split('//')[1] const fqdn = config.url.split('//').at(1)
const resources: string[] = [sky, particles] const resources: string[] = [sky, particles]
const animated = defineModel<boolean>({
const pages = storeToRefs(usePageStore()) required: true,
})
const props = defineProps({ const props = defineProps({
layout: { layout: {
@ -138,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,
@ -563,9 +565,6 @@ class Portal {
// Run the scene. // Run the scene.
this.scene() this.scene()
// TODO: Make it into a composable? The toggle will not work. Hardcoded.
if (!pages.animate.value) this.pause()
// Request the next animation frame if animation is enabled. // Request the next animation frame if animation is enabled.
if (this.animate) requestAnimationFrame(this.render.bind(this)) if (this.animate) requestAnimationFrame(this.render.bind(this))
} }
@ -573,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))
} }
@ -585,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
@ -619,47 +614,50 @@ 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 && window.innerWidth > 600, 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 parallax -z-10">
<span> <span>
Your browser does not support the &lt;canvas /&gt; element, which is Your browser does not support the &lt;canvas /&gt; element, which is
required for parallax animation. required for parallax animation.
</span> </span>
</canvas> </canvas>
</template> </template>
<style scoped>
.parallax {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
transition: all 0.6942s ease-in;
z-index: -1;
}
</style>

View File

@ -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>

View File

@ -1,59 +0,0 @@
<script setup lang="ts">
const props = defineProps({
opaque: Boolean,
})
const config = useAppConfig()
const currentYear = new Date().getFullYear()
</script>
<template>
<footer
class="user-select-none pass-through text-align-center line-height-1-5"
>
<span
class="font-small"
:class="{
'footer-transparent': !props.opaque,
'text-shadow': !props.opaque,
}"
>
© 2018-{{ currentYear }},
<a
:class="{
'link-hi-force-dark': !props.opaque,
'link-hi': props.opaque,
}"
:href="config.url"
>Enderman</a
>. All rights reserved.
<wbr />
<sub class="nobr">
β{{ config.build.version ? config.build.version : '?.?.?' }} ({{
config.build.date ? config.build.date : '1970-01-01'
}})
</sub>
</span>
</footer>
</template>
<style scoped lang="scss">
.footer-transparent {
color: white;
opacity: 0.25;
transition: ease 0.3s;
&:hover {
opacity: 1;
}
}
.line-height-1-5 {
line-height: 14px * 1.5;
}
.text-shadow {
text-shadow: black 0 2px 5px;
}
</style>

View File

@ -29,10 +29,11 @@ const props = defineProps({
<template> <template>
<NuxtLink <NuxtLink
class="flex flex-row items-center text-inherit hover:text-inherit select-none sm:m-auto lg:m-0" 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="/" to="/"
> >
<img <img
class="transition-all"
draggable="false" draggable="false"
:width="props.width" :width="props.width"
:height="props.height" :height="props.height"
@ -41,22 +42,14 @@ const props = defineProps({
/> />
<div> <div>
<h2>{{ props.title }}</h2> <h2>{{ props.title }}</h2>
<hr class="accent accent-gradient border-0 h-px" /> <hr class="accent-text accent-gradient border-0 h-px" />
<p>{{ props.description }}</p> <p>{{ props.description }}</p>
</div> </div>
</NuxtLink> </NuxtLink>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
a { a:hover > img {
> img { transform: scale(105%);
transition: ease 0.3s;
}
&:hover {
> img {
transform: scale(105%);
}
}
} }
</style> </style>

View File

@ -12,7 +12,7 @@ const links = toRaw(pages.value).filter((page) => page.path !== '/')
<template> <template>
<nav <nav
class="flex flex-row flex-wrap sm:flex-col lg:flex-row justify-around sm:justify-start lg:justify-between gap-2 lg:gap-3" 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 <Logo
:src="logo" :src="logo"
@ -21,7 +21,7 @@ const links = toRaw(pages.value).filter((page) => page.path !== '/')
description="official website" description="official website"
/> />
<ul <ul
class="flex flex-row flex-wrap items-center sm:items-start lg:items-center justify-center gap-3 m-2 sm:m-0 lg:mx-2 sm:my-2 lg:my-0" 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"> <li v-for="(page, index) in links" :key="index" class="nav-item">
<Route <Route

View File

@ -1,132 +0,0 @@
<script setup lang="ts">
import cogIcon from '~/assets/images/icons/cog.png'
import pearlIcon from '~/assets/images/icons/pearl.gif'
const theme = useVThemeSSR()
const config = useAppConfig()
const fqdn = config.url.split('//')[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 pages = storeToRefs(usePageStore())
</script>
<template>
<VDialog width="auto">
<template #activator="{ props }">
<div v-bind="props" class="options clickable">
<img
draggable="false"
:src="cogIcon"
alt="Options"
class="logo user-select-none"
/>
</div>
</template>
<template #default="{ isActive }">
<VCard>
<img
draggable="false"
:src="pearlIcon"
alt="Pearl"
class="icon-badge user-select-none"
/>
<template #title>
<h3 class="alex text-align-center">Site options</h3>
</template>
<div class="overlay background"></div>
<template #text>
<p class="center mb-3">
This tab is experimental and isn't ready for production.
<em>I was too lazy to exclude it from the build.</em><br />
<strong>Features provided here may or may not work.</strong>
</p>
<div>
<VRow>
<VCol>
<strong>Theme</strong>
<p>Available in all flavors, vanilla and chocolate.</p>
<VBtn variant="flat" color="secondary" @click="theme.toggle()">
Toggle theme
</VBtn>
</VCol>
<VCol>
<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>
<VBtn
variant="flat"
color="primary"
@click="pages.animate.value = !pages.animate.value"
>
Stop animation
</VBtn>
</VCol>
</VRow>
</div>
<span class="mt-3">
Since you're so curious and thus here, why not report a bug from the
production environment behind this tab?<br />
It will greatly help me improve the website!
<EMail
class="link-hi"
:address="`contact@${fqdn}`"
:cc="`admin@${fqdn}`"
:subject="`Bug report: ${fqdn}`"
:body="mailTemplate"
>
<strong>Report a bug</strong>
</EMail>
</span>
</template>
<template #actions>
<VSpacer />
<VBtn
variant="outlined"
prepend-icon="close"
@click="isActive.value = false"
>
Close
</VBtn>
</template>
</VCard>
</template>
</VDialog>
</template>
<style scoped lang="scss">
.options {
display: flex;
position: fixed;
top: 0;
right: 0;
padding: 10px;
}
.icon-badge {
position: fixed;
top: -48px;
left: -32px;
transform: rotate(-15deg);
}
.logo {
width: 16px;
height: 16px;
}
</style>

View File

@ -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="flex flex-row items-center gap-2 select-none no-decoration" class="flex flex-row items-center gap-2 select-none text-inherit no-underline"
active-class="icon-active sm:px-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-sm"> <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: 32px;
height: 32px;
}
} }
</style> </style>

View File

@ -0,0 +1,106 @@
<script setup lang="ts">
import cogIcon from '~/assets/images/icons/cog.png'
import pearlIcon from '~/assets/images/icons/pearl.gif'
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>

View File

@ -1,9 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { storeToRefs } from 'pinia' const { pages } = storeToRefs(usePageStore())
import { usePageStore } from '~/stores/pages'
const pageStore = usePageStore()
const { pages } = storeToRefs(pageStore)
const route = useRoute() const route = useRoute()
const state = computed(() => { const state = computed(() => {
@ -24,39 +20,18 @@ const state = computed(() => {
</script> </script>
<template> <template>
<div class="control-layout pass-through"> <div
<NuxtLink class="control-icon no-decoration" :to="state.prev"> class="flex fixed flex-row justify-between items-center w-full h-full pass-through"
<Icon name="iconoir:nav-arrow-left" /> >
<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>
<NuxtLink class="control-icon no-decoration" :to="state.next"> <NuxtLink class="control-icon text-inherit no-underline" :to="state.next">
<Icon name="iconoir:nav-arrow-right" /> <iconify-icon icon="iconoir:nav-arrow-right" inline />
</NuxtLink> </NuxtLink>
</div> </div>
</template> </template>
<style scoped lang="scss">
.control {
&-layout {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
position: fixed;
width: 100%;
height: 100%;
}
&-icon {
opacity: 0.25;
transition: 0.15s ease;
&:hover {
opacity: 1;
}
}
}
</style>

View File

@ -68,7 +68,7 @@ switch (props.icon) {
:alt="`Download`" :alt="`Download`"
class="icon-image" class="icon-image"
/> />
<h3 class="mb-0 nobr">{{ props.title }}</h3> <h3 class="whitespace-nowrap mb-0">{{ props.title }}</h3>
</div> </div>
<div class="box p-4 rounded-xl mb-4"> <div class="box p-4 rounded-xl mb-4">
<slot /> <slot />

View File

@ -30,7 +30,7 @@ const props = defineProps({
}) })
const sentence = computed(() => { const sentence = computed(() => {
return props.description.split('.')[0] + '...' return props.description.split('.').at(0) + '...'
}) })
</script> </script>
@ -50,9 +50,9 @@ const sentence = computed(() => {
<div class="mb-5 px-5" style="text-shadow: black 1px 1px 10px"> <div class="mb-5 px-5" style="text-shadow: black 1px 1px 10px">
<h1 class="m-0 text-[60px]">{{ title }}</h1> <h1 class="m-0 text-[60px]">{{ title }}</h1>
<strong class="min-h-[90px] m-0 text-[24px]" style="font-family: Lato">{{ <strong class="min-h-[90px] m-0 text-[24px]" style="font-family: Lato">
sentence {{ sentence }}
}}</strong> </strong>
</div> </div>
</div> </div>
</template> </template>

View File

@ -1,20 +0,0 @@
import { useDark, useToggle } from '@vueuse/core'
export default () => {
const { $vuetify } = useNuxtApp()
const config = useAppConfig()
const dark = useDark({
valueLight: config.theme.light,
valueDark: config.theme.dark,
onChanged: (dark: boolean) => {
$vuetify.theme.global.name.value = dark
? config.theme.dark
: config.theme.light
},
})
const toggle = useToggle(dark)
return { dark, toggle }
}

View File

@ -1,4 +1,3 @@
import type { ThemeDefinition } from 'vuetify'
import packageJSON from './package.json' import packageJSON from './package.json'
interface TitleConfig { interface TitleConfig {
@ -11,15 +10,6 @@ interface BuildConfig {
version: string version: string
} }
interface ThemeConfig {
file: string
cookie: string
default: string
light: string
dark: string
themes: Record<string, ThemeDefinition>
}
type config = { type config = {
url: string url: string
shortener: string shortener: string
@ -28,7 +18,6 @@ type config = {
description: string description: string
locale: string locale: string
build: BuildConfig build: BuildConfig
theme: ThemeConfig
} }
export default { export default {
@ -46,43 +35,4 @@ export default {
date: new Date().toISOString().split('T')[0], date: new Date().toISOString().split('T')[0],
version: packageJSON.version || '0.0.0', version: packageJSON.version || '0.0.0',
}, },
theme: {
file: './assets/styles/vuetify.scss',
cookie: 'color-scheme',
default: 'chocolate',
light: 'vanilla',
dark: 'chocolate',
themes: {
vanilla: {
dark: false,
colors: {
background: '#FFFFFF',
surface: '#FFFFFF',
primary: '#6200EE',
'primary-darken-1': '#3700B3',
secondary: '#03DAC6',
'secondary-darken-1': '#018786',
error: '#B00020',
info: '#2196F3',
success: '#4CAF50',
warning: '#FB8C00',
},
},
chocolate: {
dark: true,
colors: {
background: '#000',
surface: '#000',
primary: '#795548',
'primary-darken-1': '#5D4037',
secondary: '#FF9800',
'secondary-darken-1': '#F57C00',
error: '#B00020',
info: '#2196F3',
success: '#4CAF50',
warning: '#FB8C00',
},
},
},
},
} satisfies config as config } satisfies config as config

19
eslint.config.mjs Normal file
View File

@ -0,0 +1,19 @@
// @ts-check
import withNuxt from './.nuxt/eslint.config.mjs'
export default withNuxt({
rules: {
'no-unused-vars': 'warn',
'vue/multi-word-component-names': 'off',
'vue/max-attributes-per-line': [
'warn',
{
singleline: 3,
multiline: 1,
},
],
'space-in-parens': 'off',
'computed-property-spacing': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
})

View File

@ -1,65 +0,0 @@
import 'iconify-icon'
import type { IconAliases, IconSet } from 'vuetify'
const aliases = <IconAliases>{
/* custom ones */
email: 'mdi:email',
/* vuetify aliases */
collapse: 'mdi:chevron-up',
complete: 'mdi:check',
cancel: 'mdi:close-circle',
close: 'mdi:close',
delete: 'mdi:close-circle',
// delete (e.g. v-chip close)
clear: 'mdi:close-circle',
success: 'mdi:check-circle',
info: 'mdi:information',
warning: 'mdi:alert-circle',
error: 'mdi:close-circle',
prev: 'mdi:chevron-left',
next: 'mdi:chevron-right',
checkboxOn: 'mdi:checkbox-marked',
checkboxOff: 'mdi:checkbox-blank-outline',
checkboxIndeterminate: 'mdi:minus-box',
delimiter: 'mdi:circle',
// for carousel
sortAsc: 'mdi:arrow-up',
sortDesc: 'mdi:arrow-down',
expand: 'mdi:chevron-down',
menu: 'mdi:menu',
subgroup: 'mdi:menu-down',
dropdown: 'mdi:menu-down',
radioOn: 'mdi:radiobox-marked',
radioOff: 'mdi:radiobox-blank',
edit: 'mdi:pencil',
ratingEmpty: 'mdi:star-outline',
ratingFull: 'mdi:star',
ratingHalf: 'mdi:star-half-full',
loading: 'mdi:cached',
first: 'mdi:page-first',
last: 'mdi:page-last',
unfold: 'mdi:unfold-more-horizontal',
file: 'mdi:paperclip',
plus: 'mdi:plus',
minus: 'mdi:minus',
calendar: 'mdi:calendar',
$checkboxOn: 'mdi:checkbox-marked',
$checkboxOff: 'mdi:checkbox-blank-outline',
}
const iconify = <IconSet>{
component: (props: any) => {
const { icon, tag, ...rest } = props
const strIcon = icon as string
return h(tag, rest, [
h('iconify-icon', {
key: strIcon,
icon: aliases[strIcon] ?? icon,
...rest,
}),
])
},
}
export { aliases, iconify }

View File

@ -1,8 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { UseSwipeDirection } from '@vueuse/core' import type { UseSwipeDirection } from '@vueuse/core'
import { useSwipe } from '@vueuse/core' import { useSwipe } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import { usePageStore } from '~/stores/pages'
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
@ -36,23 +34,34 @@ const swipe = useSwipe(card, {
} }
}, },
}) })
const animationComplete = ref(false)
onMounted(() => {
if (card.value)
card.value.addEventListener('animationend', () => {
animationComplete.value = true
})
})
</script> </script>
<template> <template>
<main <main
ref="card" ref="card"
class="dimensions background h-animated overflow-auto flex flex-col gap-3 sm:gap-2 px-4 pt-4 pb-3" 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="{ :class="{
'sm:rounded-xl': !reader, 'animate__animated-sm animate__delay-1-5s animate__fadeInDown':
!animationComplete,
'lm:rounded-none sm:rounded-xl sm:mt-8 lm:mt-0 lt:mt-0': !reader,
'!max-h-full h-full': reader, '!max-h-full h-full': reader,
}" }"
> >
<Options /> <Settings v-if="animationComplete" />
<Navigation /> <Navigation />
<slot name="header" /> <slot name="header" />
<NuxtPage <NuxtPage
class="scrollbar fade-mask-sm flex-grow overflow-x-hidden overflow-y-auto min-h-full sm:py-4 sm:pe-3" class="sm:fade-mask flex-grow overflow-y-auto h-full sm:py-4 sm:pe-4"
/> />
<slot name="footer" /> <slot v-if="animationComplete" name="footer" />
</main> </main>
</template> </template>

View File

@ -1,35 +1,35 @@
import config from './config' import config from "./config";
// https://nuxt.com/docs/api/configuration/nuxt-config // https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({ export default defineNuxtConfig({
app: { app: {
head: { head: {
htmlAttrs: { htmlAttrs: {
lang: config.locale || 'en', lang: config.locale || "en",
}, },
link: [ link: [
{ {
rel: 'icon', rel: "icon",
type: 'image/x-icon', type: "image/x-icon",
href: '/favicon.ico', href: "/favicon.ico",
}, },
], ],
meta: [ meta: [
{ {
charset: 'utf-8', charset: "utf-8",
}, },
{ {
name: 'viewport', name: "viewport",
content: 'width=device-width, initial-scale=1', content: "width=device-width, initial-scale=1",
}, },
{ {
'http-equiv': 'X-UA-Compatible', "http-equiv": "X-UA-Compatible",
content: 'ie=edge', content: "ie=edge",
}, },
], ],
}, },
pageTransition: { name: 'page', mode: 'out-in' }, pageTransition: { name: "page", mode: "out-in" },
rootId: 'ender-app', rootId: "ender-app",
}, },
devtools: { devtools: {
enabled: true, enabled: true,
@ -43,56 +43,64 @@ export default defineNuxtConfig({
components: { components: {
dirs: [ dirs: [
{ {
path: '~/components', path: "~/components",
pathPrefix: false, pathPrefix: false,
extensions: ['.vue'], extensions: [".vue"],
}, },
], ],
}, },
css: ['~/assets/styles/main.scss'], css: ["~/assets/styles/main.scss"],
plugins: [], plugins: [],
modules: [ modules: [
['@nuxtjs/eslint-module', { failOnError: false, lintOnStart: false }], "@pinia/nuxt",
['@nuxtjs/stylelint-module', { failOnError: true, lintOnStart: false }], "@nuxt/content",
'@pinia/nuxt', "@nuxtjs/seo",
'@nuxt/content', "@nuxtjs/google-fonts",
'vuetify-nuxt-module', "@nuxtjs/tailwindcss",
'@nuxtjs/seo', "@nuxtjs/color-mode",
'@nuxtjs/google-fonts', "@nuxt/eslint",
'@nuxtjs/tailwindcss', ["@nuxtjs/stylelint-module", { failOnError: true, lintOnStart: false }],
], ],
colorMode: {
preference: "system",
fallback: "dark",
classPrefix: "",
classSuffix: "",
componentName: "NuxtTheme",
storageKey: "ecmatheme",
},
content: { content: {
markdown: { markdown: {
remarkPlugins: ['remark-reading-time'], remarkPlugins: ["remark-reading-time"],
}, },
highlight: { highlight: {
theme: 'github-dark', theme: "github-dark",
langs: [ langs: [
'shell', "shell",
'batch', "batch",
'vb', "vb",
'ini', "ini",
'asm', "asm",
'c', "c",
'cpp', "cpp",
'java', "java",
'python', "python",
'csv', "csv",
'xml', "xml",
'json', "json",
'yaml', "yaml",
'html', "html",
'css', "css",
'sass', "sass",
'php', "php",
'js', "js",
'ts', "ts",
'vue', "vue",
'md', "md",
'mdc', "mdc",
'pascal', "pascal",
'lisp', "lisp",
'sql', "sql",
], ],
}, },
}, },
@ -101,53 +109,7 @@ export default defineNuxtConfig({
crawlLinks: true, crawlLinks: true,
autoSubfolderIndex: true, autoSubfolderIndex: true,
failOnError: true, failOnError: true,
routes: ['/robots.txt', '/sitemap.xml'], routes: ["/robots.txt", "/sitemap.xml"],
},
},
vuetify: {
moduleOptions: {
importComposables: true,
prefixComposables: true,
styles: {
configFile: config.theme.file,
},
includeTransformAssetsUrls: true,
ssrClientHints: {
reloadOnFirstRequest: false,
viewportSize: true,
prefersColorScheme: true,
prefersColorSchemeOptions: {
cookieName: config.theme.cookie,
lightThemeName: config.theme.light,
darkThemeName: config.theme.dark,
useBrowserThemeOnly: true,
},
prefersReducedMotion: false,
},
},
vuetifyOptions: {
ssr: {
clientWidth: 0,
clientHeight: 0,
},
components: false,
labComponents: true,
directives: false,
date: {
adapter: 'vuetify',
},
theme: {
defaultTheme: config.theme.default,
themes: config.theme.themes,
},
icons: {
defaultSet: 'custom',
},
defaults: {
VBtn: {
style: 'text-transform: none; letter-spacing: normal;',
},
},
}, },
}, },
googleFonts: { googleFonts: {
@ -198,22 +160,22 @@ export default defineNuxtConfig({
// }, // },
// }, // },
sitemap: { sitemap: {
sources: ['/api/__sitemap__/content'], sources: ["/api/__sitemap__/content"],
cacheMaxAgeSeconds: 360, cacheMaxAgeSeconds: 360,
exclude: [], exclude: [],
credits: false, credits: false,
xslColumns: [ xslColumns: [
{ label: 'URL', width: '50%' }, { label: "URL", width: "50%" },
{ label: 'Last Modified', select: 'sitemap:lastmod', width: '25%' }, { label: "Last Modified", select: "sitemap:lastmod", width: "25%" },
{ label: 'Priority', select: 'sitemap:priority', width: '12.5%' }, { label: "Priority", select: "sitemap:priority", width: "12.5%" },
{ {
label: 'Change Frequency', label: "Change Frequency",
select: 'sitemap:changefreq', select: "sitemap:changefreq",
width: '12.5%', width: "12.5%",
}, },
], ],
defaults: { defaults: {
changefreq: 'yearly', changefreq: "yearly",
priority: 0.7, priority: 0.7,
lastmod: config.build.date, lastmod: config.build.date,
}, },
@ -232,7 +194,7 @@ export default defineNuxtConfig({
}, },
vue: { vue: {
compilerOptions: { compilerOptions: {
isCustomElement: (tag) => tag === 'iconify-icon', isCustomElement: (tag) => tag === "iconify-icon",
}, },
}, },
}) });

3452
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "enderapp", "name": "enderapp",
"version": "0.2.1", "version": "0.2.5",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
@ -17,41 +17,43 @@
"devDependencies": { "devDependencies": {
"@nuxt/content": "^2.12.1", "@nuxt/content": "^2.12.1",
"@nuxt/devtools": "^1.3.3", "@nuxt/devtools": "^1.3.3",
"@nuxt/eslint": "^0.3.13",
"@nuxt/types": "^2.17.3", "@nuxt/types": "^2.17.3",
"@nuxtjs/eslint-config-typescript": "^12.1.0", "@nuxtjs/color-mode": "^3.4.1",
"@nuxtjs/eslint-module": "^4.1.0",
"@nuxtjs/google-fonts": "^3.2.0", "@nuxtjs/google-fonts": "^3.2.0",
"@nuxtjs/seo": "^2.0.0-rc.10", "@nuxtjs/seo": "^2.0.0-rc.10",
"@nuxtjs/stylelint-module": "^5.2.0", "@nuxtjs/stylelint-module": "^5.2.0",
"@nuxtjs/tailwindcss": "^6.12.0", "@nuxtjs/tailwindcss": "^6.12.0",
"@pinia/nuxt": "^0.5.1", "@pinia/nuxt": "^0.5.1",
"@tailwindcss/typography": "^0.5.13", "@tailwindcss/typography": "^0.5.13",
"@typescript-eslint/eslint-plugin": "^7.13.0",
"@typescript-eslint/parser": "^7.13.0",
"animate.css": "latest", "animate.css": "latest",
"caniuse-lite": "^1.0.30001634", "caniuse-lite": "^1.0.30001634",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-nuxt": "^4.0.0",
"eslint-plugin-prettier": "^5.1.3",
"iconify-icon": "^1.0.8",
"nuxt": "^3.12.1", "nuxt": "^3.12.1",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"prettier": "^3.3.2", "prettier": "^3.3.2",
"sass": "^1.77.5", "sass": "^1.77.5",
"stylelint": "^16.6.1", "stylelint": "^16.6.1",
"stylelint-config-recommended-scss": "^14.0.0",
"stylelint-config-recommended-vue": "^1.5.0", "stylelint-config-recommended-vue": "^1.5.0",
"stylelint-config-standard-scss": "^13.1.0", "stylelint-config-standard-scss": "^13.1.0",
"stylelint-config-tailwindcss": "^0.0.7",
"stylelint-scss": "^6.3.1",
"typescript": "^5.4.5", "typescript": "^5.4.5",
"vue": "^3.4.28", "vue": "^3.4.28",
"vue-router": "^4.3.3", "vue-router": "^4.3.3",
"vue-tsc": "^1.8.22", "vue-tsc": "^1.8.22"
"vuetify": "^3.6.9",
"vuetify-nuxt-module": "^0.14.1"
}, },
"dependencies": { "dependencies": {
"@date-io/date-fns": "^3.0.0", "@date-io/date-fns": "^3.0.0",
"date-fns": "^3.6.0", "date-fns": "^3.6.0",
"iconify-icon": "^2.1.0",
"remark-reading-time": "^2.0.1" "remark-reading-time": "^2.0.1"
} },
"browserslist": [
">0.3%",
"not dead",
"defaults",
"fully supports es6-module",
"maintained node versions"
]
} }

View File

@ -41,27 +41,21 @@ useHead({
</script> </script>
<template> <template>
<section> <section class="page">
<h4>About me</h4> <h3>About me</h3>
<hr class="accent accent-gradient border-0 h-px" /> <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 class="accent accent-gradient border-0 h-px" />
<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>
@ -70,11 +64,11 @@ 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 class="alex">FAQ</h5> <h5>FAQ</h5>
<ul class="list-style-type-faq"> <ul class="list-style-type-faq">
<li><strong>Why are you called Endermanch?</strong></li> <li><strong>Why are you called Endermanch?</strong></li>
<li> <li>
@ -84,10 +78,9 @@ useHead({
end sounded pretty good. I was 14 at the time of making that decision. end sounded pretty good. I was 14 at the time of making that decision.
</li> </li>
<li> <li>
<strong <strong>
>When will you make a new video? What happened to your When will you make a new video? What happened to your schedule?
schedule?</strong </strong>
>
</li> </li>
<li> <li>
One day. Haven't been particularly motivated lately, but life's busy. 🙁 One day. Haven't been particularly motivated lately, but life's busy. 🙁

View File

@ -1,15 +1,16 @@
<script setup lang="ts"> <script setup lang="ts">
import { formatDate } from 'date-fns' import { formatDate } from 'date-fns'
import { useAsyncData } from '#app' import chestAnimation from '~/assets/images/chest.webp'
const config = useAppConfig() const config = useAppConfig()
const route = useRoute()
let thumbnail: string | null = null let thumbnail: string | null = null
let slug = useRoute().params.slug let slug = route.params.slug
if (Array.isArray(slug)) slug = slug.join('/') if (Array.isArray(slug)) slug = slug.join('/')
const { data } = await useAsyncData('home', () => const { data, status } = await useAsyncData('home', () =>
queryContent(`/blog/${slug}`).findOne(), queryContent(`/blog/${slug}`).findOne(),
) )
@ -58,55 +59,55 @@ if (data.value) {
) )
// Hydrate the rendered items. // Hydrate the rendered items.
onMounted(() => { // onMounted(() => {
document.querySelectorAll('pre').forEach((pre) => { // document.querySelectorAll('pre').forEach((pre) => {
const icon = document.createElement('iconify-icon') // const icon = document.createElement('iconify-icon')
//
icon.setAttribute('icon', 'mdi:content-copy') // icon.setAttribute('icon', 'mdi:content-copy')
icon.setAttribute('inline', 'true') // icon.setAttribute('inline', 'true')
//
icon.classList.add('button') // icon.classList.add('button')
//
pre.appendChild(icon) // pre.appendChild(icon)
}) // })
//
document // document
.querySelectorAll('code:not(pre *), pre > iconify-icon.button') // .querySelectorAll('code:not(pre *), pre > iconify-icon.button')
.forEach((code) => { // .forEach((code) => {
if (code instanceof HTMLElement) { // if (code instanceof HTMLElement) {
code.onclick = () => { // code.onclick = () => {
const area = document.createElement('textarea') // const area = document.createElement('textarea')
//
area.textContent = // area.textContent =
code.nodeName && code.nodeName.toLowerCase() === 'code' // code.nodeName && code.nodeName.toLowerCase() === 'code'
? code.textContent // ? code.textContent
: code.parentElement!.textContent // : code.parentElement!.textContent
//
// It's necessary to create the textarea element every time you copy to get access to the select() method. // // It's necessary to create the textarea element every time you copy to get access to the select() method.
area.setSelectionRange(0, 99999) // An iOS gotcha. // area.setSelectionRange(0, 99999) // An iOS gotcha.
area.select() // area.select()
//
// Copy the text inside the textarea. // // Copy the text inside the textarea.
navigator.clipboard.writeText(area.value) // navigator.clipboard.writeText(area.value)
//
// TODO: Alert the user text has been successfully copied. // // TODO: Alert the user text has been successfully copied.
//
// Remove the textarea element. // // Remove the textarea element.
area.remove() // area.remove()
} // }
} // }
}) // })
}) // })
//
onUnmounted(() => { // onUnmounted(() => {
document.querySelectorAll('code').forEach((code) => { // document.querySelectorAll('code').forEach((code) => {
code.onclick = null // code.onclick = null
}) // })
//
document.querySelectorAll('pre').forEach((pre) => { // document.querySelectorAll('pre').forEach((pre) => {
pre.querySelector('.clipboard')?.remove() // pre.querySelector('.clipboard')?.remove()
}) // })
}) // })
} }
useHead({ useHead({
@ -125,44 +126,68 @@ useHead({
</script> </script>
<template> <template>
<div
v-if="status === 'pending'"
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 <article
v-if="data" v-else-if="status === 'success' && data"
class="post scrollbar fade-mask-sm flex flex-col gap-3 flex-grow-1 overflow-x-hidden overflow-y-auto sm:py-4 sm:pe-3" class="flex-grow post fade-mask-sm flex flex-col gap-4 overflow-x-hidden overflow-y-auto sm:py-4 sm:pe-4"
> >
<div <div
class="post-preamble" class="relative flex flex-col justify-end items-start w-full min-h-[400px] rounded-xl accent-text-shadow post-preamble"
:style="{ backgroundImage: 'url(' + thumbnail + ')' }" :style="{ backgroundImage: 'url(' + thumbnail + ')' }"
> >
<div class="p-2"> <div class="p-2">
<h3 class="alex">{{ data.title }}</h3> <h3>{{ data.title }}</h3>
<div class="flex flex-row gap-x-0 gap-y-2 flex-wrap"> <div class="flex flex-row flex-wrap gap-x-2 gap-y-0">
<div class="flex flex-row items-center gap-1"> <div class="flex flex-row items-center gap-2">
<iconify-icon icon="mdi:calendar" /> <iconify-icon icon="mdi:calendar" />
<strong class="nobr font-small"> <small class="whitespace-nowrap">
{{ formatDate(data.created, 'LLLL do, y &ndash; HH:mm') }} <strong>
</strong> {{ formatDate(data.created, 'LLLL do, y &ndash; HH:mm') }}
</strong>
</small>
</div> </div>
<div class="flex flex-row items-center gap-1"> <div class="flex flex-row items-center gap-2">
<iconify-icon icon="mdi:clock-outline" /> <iconify-icon icon="mdi:clock-outline" />
<strong class="nobr font-small"> <small class="whitespace-nowrap">
{{ data!.readingTime.text.split(' ')[0] + ' minutes to read' }} <strong>
</strong> {{ data!.readingTime.text.split(' ')[0] + ' minutes to read' }}
</strong>
</small>
</div> </div>
</div> </div>
</div> </div>
<NuxtLink
to="/blog"
class="absolute top-0 left-0 p-2 text-inherit no-underline"
>
<iconify-icon
icon="icon-park-solid:back"
width="2em"
height="2em"
style="color: lavender"
/>
</NuxtLink>
<NuxtLink <NuxtLink
:href="`https://twitter.com/share?url=${config.url}/blog/${slug}&text=${data.title}&hashtags=${data.tags.slice(0, 3).join(',').replace(/ /g, '')}`" :href="`https://twitter.com/share?url=${config.url}/blog/${slug}&text=${data.title}&hashtags=${data.tags.slice(0, 3).join(',').replace(/ /g, '')}`"
target="_blank" target="_blank"
class="link post-preamble-share px-2 py-1" class="absolute top-0 right-0 p-2"
> >
<iconify-icon icon="mdi:twitter"></iconify-icon> <iconify-icon icon="logos:twitter" width="2em" height="2em" />
</NuxtLink>
<NuxtLink to="/blog" class="link post-preamble-control px-2 py-1">
<iconify-icon icon="icon-park-solid:back"></iconify-icon>
</NuxtLink> </NuxtLink>
</div> </div>
<div class="post-content"> <div class="post-content">
<hr class="accent accent-gradient border-0 h-px" /> <hr class="accent-text accent-gradient border-0 h-px" />
<ContentRenderer :value="data" /> <ContentRenderer :value="data" />
</div> </div>
</article> </article>

View File

@ -3,7 +3,7 @@ import { formatDate } from 'date-fns'
const config = useAppConfig() const config = useAppConfig()
const meta = { const meta = {
title: 'The Enderchest', title: 'The Ender Chest',
description: description:
'A blog about software development, scientific research and Windows quirks.', 'A blog about software development, scientific research and Windows quirks.',
image: `${config.url}/images/chest.png`, image: `${config.url}/images/chest.png`,
@ -25,7 +25,7 @@ useSeoMeta({
}) })
useHead({ useHead({
title: 'The Enderchest', title: 'The Ender Chest',
htmlAttrs: { htmlAttrs: {
lang: config.locale || 'en', lang: config.locale || 'en',
}, },
@ -41,12 +41,12 @@ useHead({
<template> <template>
<section> <section>
<h3 class="alex">The Enderchest</h3> <h3 class="mb-2">The Ender Chest</h3>
<p> <p class="mb-4">
You're browsing the enderchest a blog about software development, You're browsing the Ender Chest a blog about software development,
scientific research and Windows quirks. scientific research and Windows quirks.
</p> </p>
<h4 class="alex">Recent posts</h4> <h4 class="mb-2">Recent posts</h4>
<ContentList <ContentList
:query="{ :query="{
path: '/blog', path: '/blog',
@ -56,15 +56,15 @@ useHead({
}" }"
> >
<template #default="{ list }"> <template #default="{ list }">
<div class="grid gap-3"> <div class="grid gap-4">
<NuxtLink <NuxtLink
v-for="post in list" v-for="post in list"
:key="post._path" :key="post._path"
:to="post._path" :to="post._path"
class="no-decoration" class="text-inherit hover:text-inherit no-underline"
> >
<article <article
class="post-box flex flex-col md:flex-row gap-3 rounded-xl h-full" class="post-box flex flex-col md:flex-row gap-4 rounded-xl h-full"
> >
<div class="post-thumb"> <div class="post-thumb">
<img <img
@ -78,46 +78,48 @@ useHead({
/> />
</div> </div>
<div class="w-full"> <div class="w-full">
<h3 class="post-title alex">{{ post.title }}</h3> <h3 class="post-title">{{ post.title }}</h3>
<p class="post-description mb-0">{{ post.description }}</p> <p class="post-description mb-0">{{ post.description }}</p>
<div class="post-tags flex flex-row flex-wrap gap-2 py-2"> <div class="post-tags flex flex-row flex-wrap gap-2 py-2">
<span <span
v-for="(tag, index) in post.tags.slice(0, 3)" v-for="(tag, index) in post.tags.slice(0, 3)"
:key="index" :key="index"
class="nobr" class="whitespace-nowrap"
> >
{{ tag }} {{ tag }}
</span> </span>
<span v-if="post.tags.length > 3" class="nobr"> <span v-if="post.tags.length > 3" class="whitespace-nowrap">
{{ post.tags.length - 3 }} more {{ post.tags.length - 3 }} more
</span> </span>
</div> </div>
<hr class="accent accent-gradient border-0 h-px" /> <hr class="accent-text accent-gradient border-0 h-px" />
<div class="post-details py-2"> <div class="post-details py-2">
<div class="flex flex-row items-center gap-1"> <div class="flex flex-row items-center gap-2">
<iconify-icon icon="mdi:calendar" /> <iconify-icon icon="mdi:calendar" />
<small class="nobr"> <small class="whitespace-nowrap">
{{ formatDate(post.created, 'LLLL do, y &ndash; HH:mm') }} {{ formatDate(post.created, 'LLLL do, y &ndash; HH:mm') }}
</small> </small>
</div> </div>
<div <div
v-if="post.updated" v-if="post.updated"
class="flex flex-row items-center gap-1" class="flex flex-row items-center gap-2"
> >
<iconify-icon icon="mdi:pencil" /> <iconify-icon icon="mdi:pencil" />
<small class="nobr"> <small class="whitespace-nowrap">
{{ formatDate(post.updated, 'LLLL do, y &ndash; HH:mm') }} {{ formatDate(post.updated, 'LLLL do, y &ndash; HH:mm') }}
</small> </small>
</div> </div>
</div> </div>
</div> </div>
<div class="post-pocket flex flex-row items-center gap-1 p-2"> <div
class="post-pocket absolute bottom-0 right-0 uppercase rounded-tl-xl flex flex-row items-center gap-2 p-2"
>
<iconify-icon icon="mdi:clock-outline" /> <iconify-icon icon="mdi:clock-outline" />
<small class="nobr font-monospace font-small"> <small class="whitespace-nowrap font-mono font-small">
{{ post.readingTime.text }} {{ post.readingTime.text }}
</small> </small>
</div> </div>
@ -127,13 +129,11 @@ useHead({
</template> </template>
<template #not-found> <template #not-found>
<div class="flex flex-col justify-center items-center gap-3"> <div class="flex flex-col justify-center items-center gap-4">
<span>No posts found 🙁</span> <span>No posts found 🙁</span>
<NuxtLink to="/" class="link-hi">Back to index</NuxtLink> <NuxtLink to="/">Back to index</NuxtLink>
</div> </div>
</template> </template>
</ContentList> </ContentList>
</section> </section>
</template> </template>
<style scoped lang="scss"></style>

View File

@ -46,18 +46,17 @@ defineRouteRules({
</script> </script>
<template> <template>
<section> <section class="page">
<h3>Welcome 👋</h3> <h3>Welcome 👋</h3>
<p class="mb-4"> <p>
I'm <strong>Enderman</strong> &ndash; a software engineer, a malware I'm <strong>Enderman</strong> &ndash; a software engineer, a malware
enthusiast and most importantly, a weird tall creature. I have over 300K enthusiast and most importantly, a weird tall creature. I have over 300K
subscribers on <a :href="`${config.shortener}/youtube`">YouTube</a> and subscribers on <a :href="`${config.shortener}/youtube`">YouTube</a> and
over 25K followers on over 25K followers on
<a class="text-inherit no-underline" :href="`${config.shortener}/twitter`" <a :href="`${config.shortener}/twitter`">Twitter</a>. Sometimes I wish
>Twitter</a there were 48 hours in a day.
>. Sometimes I wish there were 48 hours in a day.
</p> </p>
<div class="flex flex-col md:flex-row gap-3 mb-4"> <div class="flex flex-col md:flex-row gap-4">
<div class="md:basis-0 md:flex-grow-[2]"> <div class="md:basis-0 md:flex-grow-[2]">
<h6>What I do</h6> <h6>What I do</h6>
<ul class="list-style-type-do"> <ul class="list-style-type-do">
@ -66,7 +65,7 @@ defineRouteRules({
<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="`${config.shortener}/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
@ -79,8 +78,7 @@ defineRouteRules({
<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="`${config.shortener}/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>
@ -95,7 +93,7 @@ defineRouteRules({
<li>Philosophy</li> <li>Philosophy</li>
<li>Geopolitics</li> <li>Geopolitics</li>
<li> <li>
<a class="link-hi" :href="`${config.shortener}/chess`">Chess</a> <a :href="`${config.shortener}/chess`">Chess</a>
</li> </li>
<li>Solitude</li> <li>Solitude</li>
</ul> </ul>

View File

@ -67,20 +67,13 @@ useHead({
</script> </script>
<template> <template>
<section> <section class="page">
<h3 class="alex">Projects</h3> <h3>Projects</h3>
<hr class="accent accent-gradient border-0 h-px" /> <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 class="accent accent-gradient border-0 h-px" />
<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>

View File

@ -115,39 +115,34 @@ useHead({
</script> </script>
<template> <template>
<section> <section class="page">
<h3 class="alex">Online presence</h3> <h3>Online presence</h3>
<hr class="accent accent-gradient border-0 h-px" /> <Construction />
<p> <div class="flex flex-col md:flex-row gap-4">
<strong>🚧 This page is currently under construction.</strong> Expect a <div class="md:basis-0 md:flex-grow-[1]">
lot more content to be added as time passes. <h5>Social media</h5>
<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 accent-gradient border-0 h-px" />
<div class="row">
<div class="col-12 col-md-3">
<h5 class="alex">Social media</h5>
<ul class="list-style-type-none p-0"> <ul class="list-style-type-none p-0">
<li v-for="(page, index) in socials" :key="index"> <li v-for="(page, index) in socials" :key="index">
<a class="link-hi" target="_blank" rel="noopener" :href="page.url"> <NuxtLink target="_blank" rel="noopener" :href="page.url">
<Icon :name="page.icon.name" :color="page.icon.color" inline /> <iconify-icon
:icon="page.icon.name"
:style="{ color: page.icon.color }"
width="1em"
height="1em"
inline
/>
<span class="mx-2">{{ page.name }}</span> <span class="mx-2">{{ page.name }}</span>
</a> </NuxtLink>
</li> </li>
</ul> </ul>
</div> </div>
<div class="col-12 col-md-9"> <div class="md:basis-0 md:flex-grow-[3]">
<h5 class="alex">Contact me</h5> <h5>Contact me</h5>
<p> <p>
Personal: Personal:
<EMail <EMail :address="`contact@${fqdn}`" subject="Hey Enderman!" /><br />
class="link-hi" Manager: <EMail :address="`manager@${fqdn}`" /><br />
:address="`contact@${fqdn}`" Abuse: <EMail :address="`abuse@${fqdn}`" />
subject="Hey Enderman!"
/><br />
Manager: <EMail class="link-hi" :address="`manager@${fqdn}`" /><br />
Abuse: <EMail class="link-hi" :address="`abuse@${fqdn}`" />
</p> </p>
</div> </div>
</div> </div>

21
plugins/local.ts Normal file
View File

@ -0,0 +1,21 @@
export default defineNuxtPlugin(() => {
return {
provide: {
local: {
getItem(item: string) {
if (import.meta.client) {
return localStorage.getItem(item)
} else {
return undefined
}
},
setItem(item: string, value: any) {
if (import.meta.client) {
return localStorage.setItem(item, value)
}
},
},
},
}
})

View File

@ -1,11 +0,0 @@
import { aliases, iconify } from '~/iconify'
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.hook('vuetify:before-create', ({ vuetifyOptions }) => {
vuetifyOptions.icons = {
defaultSet: 'iconify',
aliases,
sets: { iconify },
}
})
})

13
prettier.config.mjs Normal file
View File

@ -0,0 +1,13 @@
/**
* @see https://prettier.io/docs/en/configuration.html
* @type {import("prettier").Config}
*/
export default {
semi: false,
quoteProps: 'as-needed',
singleQuote: true,
useTabs: false,
tabWidth: 2,
trailingComma: 'all',
bracketSpacing: true,
}

View File

@ -1,5 +1,3 @@
import { defineStore } from 'pinia'
import aboutIcon from '~/assets/images/icons/accent/info.png' import aboutIcon from '~/assets/images/icons/accent/info.png'
import projectIcon from '~/assets/images/icons/defrag.png' import projectIcon from '~/assets/images/icons/defrag.png'
import socialIcon from '~/assets/images/icons/user.png' import socialIcon from '~/assets/images/icons/user.png'
@ -50,8 +48,9 @@ export const usePageStore = defineStore('page', () => {
}, },
]) ])
const reader = ref(false) const reader = ref<boolean>(false)
const animate = ref(true) const animate = ref<boolean>(false)
const dark = ref<boolean>(false)
function _autoFetchPages() { function _autoFetchPages() {
while (pages.value.length) pages.value.pop() while (pages.value.length) pages.value.pop()

24
stylelint.config.mjs Normal file
View File

@ -0,0 +1,24 @@
/** @type {import("stylelint").Config} */
export default {
defaultSeverity: 'warning',
formatter: 'compact',
cache: false,
fix: true,
extends: [
'stylelint-config-standard-scss',
'stylelint-config-recommended-scss',
'stylelint-config-tailwindcss/scss',
'stylelint-config-recommended-vue/scss',
],
plugins: ['stylelint-scss'],
rules: {
'at-rule-no-unknown': null,
'scss/at-rule-no-unknown': [
true,
{
ignoreAtRules: ['tailwind', 'responsive', 'screen'],
},
],
'declaration-empty-line-before': null,
},
}

View File

@ -11,6 +11,14 @@ export default <Partial<Config>>{
'2xl': '2560px', '2xl': '2560px',
desktop: '960px', desktop: '960px',
// Landscape Mobile (LM)
lm: {
raw: '(max-height: 600px)',
},
// Landscape Tablet (LT)
lt: {
raw: '(max-height: 996px) and (min-width: 601px) and (max-width: 1280px)',
},
}, },
fontFamily: { fontFamily: {
sans: [ sans: [
@ -38,6 +46,7 @@ export default <Partial<Config>>{
'monospace', 'monospace',
], ],
alex: ['Alexandria', 'serif'], alex: ['Alexandria', 'serif'],
enchant: ['Enchant', 'serif'],
lato: ['Lato', 'sans-serif'], lato: ['Lato', 'sans-serif'],
}, },
}, },