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_modules
# Cache
.stylelintcache
# Logs
logs
*.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({
...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">
import 'iconify-icon'
const config = useAppConfig()
const theme = useVThemeSSR()
const vdisp = ref(useVDisplay())
const { reader } = storeToRefs(usePageStore())
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 Enderchest'
return 'The Ender Chest'
default:
return `${chunk} ${config.title.short}`
}
@ -22,47 +23,39 @@ useHead({
</script>
<template>
<VThemeProvider
:theme="theme.dark.value ? config.theme.dark : config.theme.light"
<div
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>
<template #fallback> </template>
<LazyPortal layout="#ender-layout" animate randomize fade />
<LazySwipeControls class="block sm:hidden" />
</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>

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 {
list-style-type: faq;
> li:nth-child(2n):not(:last-child) {
@apply mb-2;
}
}
}

View File

@ -3,6 +3,14 @@
// Modules.
@use 'transitions';
@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.
:root {
@ -11,10 +19,47 @@
--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: 100%;
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.
@ -69,8 +114,12 @@ h6 {
font-size: 1rem;
}
small {
font-size: 0.75rem;
}
a {
@apply text-inherit no-underline transition-all;
@apply no-underline transition-all;
color: cornflowerblue;
@ -79,34 +128,41 @@ a {
}
}
ul {
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 {
center {
text-align: center;
margin: auto;
}
.nobr {
white-space: nowrap;
:not(nav) > ul {
list-style-type: disc;
padding-left: 1.5em;
}
.clickable {
cursor: pointer;
:is(section, article, aside).page {
: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 {
grid-area: 1 / 1;
}
@ -119,57 +175,59 @@ ul {
}
}
.no-decoration {
text-decoration: none;
color: inherit;
ruby {
ruby-position: under;
ruby {
ruby-position: over;
}
}
.text-align-center {
text-align: center;
rt {
ruby-align: space-around;
}
.font-small {
font-size: 14px;
}
.grid-cols-3 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.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;
.transition-ease {
@extend %transition;
}
// Query-overridable classes.
.background {
background-color: rgb(0 0 0 / 50%);
}
.dimensions {
width: 100%;
min-height: 100%;
}
.accent {
color: white;
.accent-background {
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 {
@ -212,16 +270,8 @@ ul {
transition: 0.3s ease;
}
.link {
&-hi-force-dark {
@apply text-inherit no-underline transition-all;
color: cornflowerblue;
&:hover {
color: royalblue;
}
}
.parallax {
transition: all 0.6942s ease-in;
}
.accent-gradient {
@ -233,53 +283,26 @@ ul {
);
}
.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%);
}
&::-webkit-scrollbar-button {
color: white;
}
.accent-text-shadow {
text-shadow: black 2px 3px 4px;
}
@media (prefers-color-scheme: light) {
:where(html.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 {
.accent-background {
background-color: rgb(255 255 255 / 80%);
}
.accent-overlay-background {
background-color: rgb(255 255 255 / 90%);
}
a {
@apply no-underline transition-all;
color: royalblue;
&:hover {
@ -287,7 +310,7 @@ ul {
}
}
.accent {
.accent-text {
color: black;
}
@ -299,20 +322,14 @@ ul {
rgb(0 0 0 / 0%)
);
}
}
.floaty {
position: fixed;
bottom: 0;
left: 0;
right: 0;
width: fit-content;
height: fit-content;
margin: auto;
.accent-text-shadow {
text-shadow: white 1px 2px 3px;
}
}
.post {
scroll-snap-type: both mandatory;
scroll-snap-type: y mandatory;
scroll-snap-stop: normal;
&::after {
@ -337,19 +354,10 @@ ul {
}
&-pocket {
position: absolute;
bottom: 0;
right: 0;
text-transform: uppercase;
background-color: rgb(153 153 255 / 10%);
border-left: 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 {
@ -377,45 +385,16 @@ ul {
}
&-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-position: center;
background-size: cover;
background-attachment: fixed;
border-radius: 1em;
scroll-snap-align: end;
text-shadow: black 1px 1px 7px;
iconify-icon {
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 {
@ -550,6 +529,13 @@ ul {
}
// Dynamic classes.
@screen lm {
.dimensions {
width: 100% !important;
min-height: 100% !important;
}
}
@screen sm {
html {
font-size: 16px;
@ -557,9 +543,7 @@ ul {
#ender-app {
height: 100%;
}
.scrollbar {
scrollbar-gutter: stable;
}
@ -569,18 +553,6 @@ ul {
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 {
animation-duration: var(--animate-duration);
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>
<section>
<h3 class="alex">Page not found!</h3>
<pre
class="whitespace-pre-wrap break-keep">{{ buffer }}<span class="blinker">&#9610;</span></pre>
<h3>Page not found!</h3>
<pre class="whitespace-pre-wrap break-keep font-small">{{
buffer
}}<span class="blinker">&#9610;</span></pre>
</section>
</template>

View File

@ -2,12 +2,14 @@
import sky from '~/assets/images/textures/sky.png'
import particles from '~/assets/images/textures/particles.png'
const { $local } = useNuxtApp()
const config = useAppConfig()
const fqdn = config.url.split('//')[1]
const fqdn = config.url.split('//').at(1)
const resources: string[] = [sky, particles]
const pages = storeToRefs(usePageStore())
const animated = defineModel<boolean>({
required: true,
})
const props = defineProps({
layout: {
@ -138,7 +140,7 @@ class Portal {
constructor(
directory: string = '/',
create: boolean = true,
animate: boolean = true,
animate: boolean = false,
randomize: boolean = false,
fade: boolean = false,
speed: number = 1,
@ -563,9 +565,6 @@ class Portal {
// Run the 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.
if (this.animate) requestAnimationFrame(this.render.bind(this))
}
@ -573,10 +572,6 @@ class Portal {
resize() {
if (!this.canvas.full()) this.canvas.fill()
// Pause the animation if the viewport becomes too small.
if (window.innerWidth <= 600) this.pause()
else this.continue()
// If we have animation disabled, we still have to re-render the scene once on resize.
if (!this.animate) requestAnimationFrame(this.render.bind(this))
}
@ -585,8 +580,8 @@ class Portal {
this.clickTime = Date.now()
}
continue() {
if (this.pauseTime === 0) return
play() {
if (this.pauseTime === 0) this.pauseTime = this.currentTime
// Add the time that has passed since the pause to the current time.
this.currentTime += Date.now() - this.pauseTime
@ -619,47 +614,50 @@ class Portal {
}
onMounted(() => {
// If the localStorage value is null, trigger the setup.
// If the viewport is too small, disable by default.
const ecmaportal: string | null | undefined = $local.getItem('ecmaportal')
if (ecmaportal === null) {
const notMobile = window.innerWidth > 600
$local.setItem('ecmaportal', notMobile)
animated.value = notMobile
} else animated.value = ecmaportal === 'true'
const layout = document.querySelector(props.layout)
const portal = new Portal(
props.directory,
props.create,
props.animate && window.innerWidth > 600,
animated.value,
props.randomize,
props.fade,
props.speed,
)
layout!.addEventListener('mousedown', ((e: UIEvent) => {
if (e.target === e.currentTarget && e.detail >= 2) {
const click: EventListener = (e: Event) => {
if (e.target === e.currentTarget) {
e.preventDefault()
portal.click()
}
}) as EventListener)
}
for (const event of ['dblclick', 'touchstart'])
layout!.addEventListener(event, click)
// Save the new value to local storage.
watch(animated, (newAnimate) => {
$local.setItem('ecmaportal', newAnimate)
newAnimate ? portal.play() : portal.pause()
})
})
</script>
<template>
<canvas id="ecmaportal" class="parallax">
<canvas id="ecmaportal" class="fixed top-0 left-0 opacity-0 parallax -z-10">
<span>
Your browser does not support the &lt;canvas /&gt; element, which is
required for parallax animation.
</span>
</canvas>
</template>
<style scoped>
.parallax {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
transition: all 0.6942s ease-in;
z-index: -1;
}
</style>

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>
<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="/"
>
<img
class="transition-all"
draggable="false"
:width="props.width"
:height="props.height"
@ -41,22 +42,14 @@ const props = defineProps({
/>
<div>
<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>
</div>
</NuxtLink>
</template>
<style scoped lang="scss">
a {
> img {
transition: ease 0.3s;
}
&:hover {
> img {
transform: scale(105%);
}
}
a:hover > img {
transform: scale(105%);
}
</style>

View File

@ -12,7 +12,7 @@ const links = toRaw(pages.value).filter((page) => page.path !== '/')
<template>
<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
:src="logo"
@ -21,7 +21,7 @@ const links = toRaw(pages.value).filter((page) => page.path !== '/')
description="official website"
/>
<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">
<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,
default: 'Link',
},
width: {
type: Number,
default: 32,
},
height: {
type: Number,
default: 32,
},
})
</script>
<template>
<NuxtLink
class="flex flex-row items-center gap-2 select-none no-decoration"
active-class="icon-active sm:px-3"
class="flex flex-row items-center gap-2 select-none text-inherit no-underline"
active-class="active"
:to="!props.external ? props.path : undefined"
:href="props.external ? props.path : undefined"
>
<img
class="icon-image"
draggable="false"
:src="props.icon"
:alt="props.alt"
:width="props.width"
:height="props.height"
/>
<span class="display-sm">
<span class="hidden lm:hidden lt:hidden sm:block">
<strong>
{{ props.name }}
</strong>
@ -42,14 +51,8 @@ const props = defineProps({
</template>
<style scoped lang="scss">
.icon {
&-active {
transform: translate(2px, 2px) rotate3d(1, 1, 1, 5deg) scale(1.25);
}
&-image {
width: 32px;
height: 32px;
}
.active {
@apply px-4;
transform: translate(2px, 2px) rotate3d(1, 1, 1, 5deg) scale(1.25);
}
</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">
import { storeToRefs } from 'pinia'
import { usePageStore } from '~/stores/pages'
const pageStore = usePageStore()
const { pages } = storeToRefs(pageStore)
const { pages } = storeToRefs(usePageStore())
const route = useRoute()
const state = computed(() => {
@ -24,39 +20,18 @@ const state = computed(() => {
</script>
<template>
<div class="control-layout pass-through">
<NuxtLink class="control-icon no-decoration" :to="state.prev">
<Icon name="iconoir:nav-arrow-left" />
<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 no-decoration" :to="state.next">
<Icon name="iconoir:nav-arrow-right" />
<NuxtLink class="control-icon text-inherit no-underline" :to="state.next">
<iconify-icon icon="iconoir:nav-arrow-right" inline />
</NuxtLink>
</div>
</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`"
class="icon-image"
/>
<h3 class="mb-0 nobr">{{ props.title }}</h3>
<h3 class="whitespace-nowrap mb-0">{{ props.title }}</h3>
</div>
<div class="box p-4 rounded-xl mb-4">
<slot />

View File

@ -30,7 +30,7 @@ const props = defineProps({
})
const sentence = computed(() => {
return props.description.split('.')[0] + '...'
return props.description.split('.').at(0) + '...'
})
</script>
@ -50,9 +50,9 @@ const sentence = computed(() => {
<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>
<strong class="min-h-[90px] m-0 text-[24px]" style="font-family: Lato">
{{ sentence }}
</strong>
</div>
</div>
</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'
interface TitleConfig {
@ -11,15 +10,6 @@ interface BuildConfig {
version: string
}
interface ThemeConfig {
file: string
cookie: string
default: string
light: string
dark: string
themes: Record<string, ThemeDefinition>
}
type config = {
url: string
shortener: string
@ -28,7 +18,6 @@ type config = {
description: string
locale: string
build: BuildConfig
theme: ThemeConfig
}
export default {
@ -46,43 +35,4 @@ export default {
date: new Date().toISOString().split('T')[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

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">
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()
@ -36,23 +34,34 @@ const swipe = useSwipe(card, {
}
},
})
const animationComplete = ref(false)
onMounted(() => {
if (card.value)
card.value.addEventListener('animationend', () => {
animationComplete.value = true
})
})
</script>
<template>
<main
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="{
'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,
}"
>
<Options />
<Settings v-if="animationComplete" />
<Navigation />
<slot name="header" />
<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>
</template>

View File

@ -1,35 +1,35 @@
import config from './config'
import config from "./config";
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
app: {
head: {
htmlAttrs: {
lang: config.locale || 'en',
lang: config.locale || "en",
},
link: [
{
rel: 'icon',
type: 'image/x-icon',
href: '/favicon.ico',
rel: "icon",
type: "image/x-icon",
href: "/favicon.ico",
},
],
meta: [
{
charset: 'utf-8',
charset: "utf-8",
},
{
name: 'viewport',
content: 'width=device-width, initial-scale=1',
name: "viewport",
content: "width=device-width, initial-scale=1",
},
{
'http-equiv': 'X-UA-Compatible',
content: 'ie=edge',
"http-equiv": "X-UA-Compatible",
content: "ie=edge",
},
],
},
pageTransition: { name: 'page', mode: 'out-in' },
rootId: 'ender-app',
pageTransition: { name: "page", mode: "out-in" },
rootId: "ender-app",
},
devtools: {
enabled: true,
@ -43,56 +43,64 @@ export default defineNuxtConfig({
components: {
dirs: [
{
path: '~/components',
path: "~/components",
pathPrefix: false,
extensions: ['.vue'],
extensions: [".vue"],
},
],
},
css: ['~/assets/styles/main.scss'],
css: ["~/assets/styles/main.scss"],
plugins: [],
modules: [
['@nuxtjs/eslint-module', { failOnError: false, lintOnStart: false }],
['@nuxtjs/stylelint-module', { failOnError: true, lintOnStart: false }],
'@pinia/nuxt',
'@nuxt/content',
'vuetify-nuxt-module',
'@nuxtjs/seo',
'@nuxtjs/google-fonts',
'@nuxtjs/tailwindcss',
"@pinia/nuxt",
"@nuxt/content",
"@nuxtjs/seo",
"@nuxtjs/google-fonts",
"@nuxtjs/tailwindcss",
"@nuxtjs/color-mode",
"@nuxt/eslint",
["@nuxtjs/stylelint-module", { failOnError: true, lintOnStart: false }],
],
colorMode: {
preference: "system",
fallback: "dark",
classPrefix: "",
classSuffix: "",
componentName: "NuxtTheme",
storageKey: "ecmatheme",
},
content: {
markdown: {
remarkPlugins: ['remark-reading-time'],
remarkPlugins: ["remark-reading-time"],
},
highlight: {
theme: 'github-dark',
theme: "github-dark",
langs: [
'shell',
'batch',
'vb',
'ini',
'asm',
'c',
'cpp',
'java',
'python',
'csv',
'xml',
'json',
'yaml',
'html',
'css',
'sass',
'php',
'js',
'ts',
'vue',
'md',
'mdc',
'pascal',
'lisp',
'sql',
"shell",
"batch",
"vb",
"ini",
"asm",
"c",
"cpp",
"java",
"python",
"csv",
"xml",
"json",
"yaml",
"html",
"css",
"sass",
"php",
"js",
"ts",
"vue",
"md",
"mdc",
"pascal",
"lisp",
"sql",
],
},
},
@ -101,53 +109,7 @@ export default defineNuxtConfig({
crawlLinks: true,
autoSubfolderIndex: true,
failOnError: true,
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;',
},
},
routes: ["/robots.txt", "/sitemap.xml"],
},
},
googleFonts: {
@ -198,22 +160,22 @@ export default defineNuxtConfig({
// },
// },
sitemap: {
sources: ['/api/__sitemap__/content'],
sources: ["/api/__sitemap__/content"],
cacheMaxAgeSeconds: 360,
exclude: [],
credits: false,
xslColumns: [
{ label: 'URL', width: '50%' },
{ label: 'Last Modified', select: 'sitemap:lastmod', width: '25%' },
{ label: 'Priority', select: 'sitemap:priority', width: '12.5%' },
{ label: "URL", width: "50%" },
{ label: "Last Modified", select: "sitemap:lastmod", width: "25%" },
{ label: "Priority", select: "sitemap:priority", width: "12.5%" },
{
label: 'Change Frequency',
select: 'sitemap:changefreq',
width: '12.5%',
label: "Change Frequency",
select: "sitemap:changefreq",
width: "12.5%",
},
],
defaults: {
changefreq: 'yearly',
changefreq: "yearly",
priority: 0.7,
lastmod: config.build.date,
},
@ -232,7 +194,7 @@ export default defineNuxtConfig({
},
vue: {
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",
"version": "0.2.1",
"version": "0.2.5",
"private": true,
"type": "module",
"scripts": {
@ -17,41 +17,43 @@
"devDependencies": {
"@nuxt/content": "^2.12.1",
"@nuxt/devtools": "^1.3.3",
"@nuxt/eslint": "^0.3.13",
"@nuxt/types": "^2.17.3",
"@nuxtjs/eslint-config-typescript": "^12.1.0",
"@nuxtjs/eslint-module": "^4.1.0",
"@nuxtjs/color-mode": "^3.4.1",
"@nuxtjs/google-fonts": "^3.2.0",
"@nuxtjs/seo": "^2.0.0-rc.10",
"@nuxtjs/stylelint-module": "^5.2.0",
"@nuxtjs/tailwindcss": "^6.12.0",
"@pinia/nuxt": "^0.5.1",
"@tailwindcss/typography": "^0.5.13",
"@typescript-eslint/eslint-plugin": "^7.13.0",
"@typescript-eslint/parser": "^7.13.0",
"animate.css": "latest",
"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",
"pinia": "^2.1.7",
"prettier": "^3.3.2",
"sass": "^1.77.5",
"stylelint": "^16.6.1",
"stylelint-config-recommended-scss": "^14.0.0",
"stylelint-config-recommended-vue": "^1.5.0",
"stylelint-config-standard-scss": "^13.1.0",
"stylelint-config-tailwindcss": "^0.0.7",
"stylelint-scss": "^6.3.1",
"typescript": "^5.4.5",
"vue": "^3.4.28",
"vue-router": "^4.3.3",
"vue-tsc": "^1.8.22",
"vuetify": "^3.6.9",
"vuetify-nuxt-module": "^0.14.1"
"vue-tsc": "^1.8.22"
},
"dependencies": {
"@date-io/date-fns": "^3.0.0",
"date-fns": "^3.6.0",
"iconify-icon": "^2.1.0",
"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>
<template>
<section>
<h4>About me</h4>
<hr class="accent accent-gradient border-0 h-px" />
<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" />
<section class="page">
<h3>About me</h3>
<Construction />
<p>
Nice to meet you! I'm Andrew, a {{ age }}-year-old guy from Kaluga,
Russia. I have been developing software since I was 10, and I have always
been interested in technology. I'm a middle-senior C/++ developer and a
junior full-stack engineer.
<sup>1</sup>
</p>
<p class="font-small">
<sup>1</sup>
All titles are based on knowledge and confidence, not on official work
experience.
<br />
<small>
<sup>1</sup>
All titles are based on knowledge and confidence, not on official work
experience.
</small>
</p>
<p><strong>Here's a little more about myself:</strong></p>
<ul>
@ -70,11 +64,11 @@ useHead({
<li>My favorite field in mathematics is Algebraic Geometry.</li>
</ul>
<p>
I work on all sorts of projects, most of the times simultaneously, which
I work on all sorts of projects, most of the time simultaneously, which
doesn't help with adhering to deadlines at all. However, you can always
check them out!
</p>
<h5 class="alex">FAQ</h5>
<h5>FAQ</h5>
<ul class="list-style-type-faq">
<li><strong>Why are you called Endermanch?</strong></li>
<li>
@ -84,10 +78,9 @@ useHead({
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
>
<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. 🙁

View File

@ -1,15 +1,16 @@
<script setup lang="ts">
import { formatDate } from 'date-fns'
import { useAsyncData } from '#app'
import chestAnimation from '~/assets/images/chest.webp'
const config = useAppConfig()
const route = useRoute()
let thumbnail: string | null = null
let slug = useRoute().params.slug
let slug = route.params.slug
if (Array.isArray(slug)) slug = slug.join('/')
const { data } = await useAsyncData('home', () =>
const { data, status } = await useAsyncData('home', () =>
queryContent(`/blog/${slug}`).findOne(),
)
@ -58,55 +59,55 @@ if (data.value) {
)
// Hydrate the rendered items.
onMounted(() => {
document.querySelectorAll('pre').forEach((pre) => {
const icon = document.createElement('iconify-icon')
icon.setAttribute('icon', 'mdi:content-copy')
icon.setAttribute('inline', 'true')
icon.classList.add('button')
pre.appendChild(icon)
})
document
.querySelectorAll('code:not(pre *), pre > iconify-icon.button')
.forEach((code) => {
if (code instanceof HTMLElement) {
code.onclick = () => {
const area = document.createElement('textarea')
area.textContent =
code.nodeName && code.nodeName.toLowerCase() === 'code'
? code.textContent
: code.parentElement!.textContent
// 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.select()
// Copy the text inside the textarea.
navigator.clipboard.writeText(area.value)
// TODO: Alert the user text has been successfully copied.
// Remove the textarea element.
area.remove()
}
}
})
})
onUnmounted(() => {
document.querySelectorAll('code').forEach((code) => {
code.onclick = null
})
document.querySelectorAll('pre').forEach((pre) => {
pre.querySelector('.clipboard')?.remove()
})
})
// onMounted(() => {
// document.querySelectorAll('pre').forEach((pre) => {
// const icon = document.createElement('iconify-icon')
//
// icon.setAttribute('icon', 'mdi:content-copy')
// icon.setAttribute('inline', 'true')
//
// icon.classList.add('button')
//
// pre.appendChild(icon)
// })
//
// document
// .querySelectorAll('code:not(pre *), pre > iconify-icon.button')
// .forEach((code) => {
// if (code instanceof HTMLElement) {
// code.onclick = () => {
// const area = document.createElement('textarea')
//
// area.textContent =
// code.nodeName && code.nodeName.toLowerCase() === 'code'
// ? code.textContent
// : code.parentElement!.textContent
//
// // 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.select()
//
// // Copy the text inside the textarea.
// navigator.clipboard.writeText(area.value)
//
// // TODO: Alert the user text has been successfully copied.
//
// // Remove the textarea element.
// area.remove()
// }
// }
// })
// })
//
// onUnmounted(() => {
// document.querySelectorAll('code').forEach((code) => {
// code.onclick = null
// })
//
// document.querySelectorAll('pre').forEach((pre) => {
// pre.querySelector('.clipboard')?.remove()
// })
// })
}
useHead({
@ -125,44 +126,68 @@ useHead({
</script>
<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
v-if="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"
v-else-if="status === 'success' && data"
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
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 + ')' }"
>
<div class="p-2">
<h3 class="alex">{{ data.title }}</h3>
<div class="flex flex-row gap-x-0 gap-y-2 flex-wrap">
<div class="flex flex-row items-center gap-1">
<h3>{{ data.title }}</h3>
<div class="flex flex-row flex-wrap gap-x-2 gap-y-0">
<div class="flex flex-row items-center gap-2">
<iconify-icon icon="mdi:calendar" />
<strong class="nobr font-small">
{{ formatDate(data.created, 'LLLL do, y &ndash; HH:mm') }}
</strong>
<small class="whitespace-nowrap">
<strong>
{{ formatDate(data.created, 'LLLL do, y &ndash; HH:mm') }}
</strong>
</small>
</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" />
<strong class="nobr font-small">
{{ data!.readingTime.text.split(' ')[0] + ' minutes to read' }}
</strong>
<small class="whitespace-nowrap">
<strong>
{{ data!.readingTime.text.split(' ')[0] + ' minutes to read' }}
</strong>
</small>
</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
:href="`https://twitter.com/share?url=${config.url}/blog/${slug}&text=${data.title}&hashtags=${data.tags.slice(0, 3).join(',').replace(/ /g, '')}`"
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>
</NuxtLink>
<NuxtLink to="/blog" class="link post-preamble-control px-2 py-1">
<iconify-icon icon="icon-park-solid:back"></iconify-icon>
<iconify-icon icon="logos:twitter" width="2em" height="2em" />
</NuxtLink>
</div>
<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" />
</div>
</article>

View File

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

View File

@ -46,18 +46,17 @@ defineRouteRules({
</script>
<template>
<section>
<section class="page">
<h3>Welcome 👋</h3>
<p class="mb-4">
<p>
I'm <strong>Enderman</strong> &ndash; a software engineer, a malware
enthusiast and most importantly, a weird tall creature. I have over 300K
subscribers on <a :href="`${config.shortener}/youtube`">YouTube</a> and
over 25K followers on
<a class="text-inherit no-underline" :href="`${config.shortener}/twitter`"
>Twitter</a
>. Sometimes I wish there were 48 hours in a day.
<a :href="`${config.shortener}/twitter`">Twitter</a>. Sometimes I wish
there were 48 hours in a day.
</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]">
<h6>What I do</h6>
<ul class="list-style-type-do">
@ -66,7 +65,7 @@ defineRouteRules({
<strong>C/C++</strong> for desktop applications and any common
backend stack with framework-loaded TypeScript for web. You can take
a look at my code on
<a class="link-hi" :href="`${config.shortener}/github`">GitHub</a>.
<a :href="`${config.shortener}/github`">GitHub</a>.
</li>
<li>
I have the most unnecessary, yet fascinating knowledge about
@ -79,8 +78,7 @@ defineRouteRules({
<li>
I research and analyze modern malware, educate computer users about
it and preserve history. The repository can be found
<a class="link-hi" :href="`${config.shortener}/repository`">here</a
>.
<a :href="`${config.shortener}/repository`">here</a>.
</li>
<li>I make videos for you to enjoy!</li>
</ul>
@ -95,7 +93,7 @@ defineRouteRules({
<li>Philosophy</li>
<li>Geopolitics</li>
<li>
<a class="link-hi" :href="`${config.shortener}/chess`">Chess</a>
<a :href="`${config.shortener}/chess`">Chess</a>
</li>
<li>Solitude</li>
</ul>

View File

@ -67,20 +67,13 @@ useHead({
</script>
<template>
<section>
<h3 class="alex">Projects</h3>
<hr class="accent accent-gradient border-0 h-px" />
<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" />
<section class="page">
<h3>Projects</h3>
<Construction />
<p><strong>My current projects are:</strong></p>
<ul>
<li v-for="(item, index) in projects" :key="index">
<a class="link-hi" :href="item.url">
<a :href="item.url">
{{ item.name }}
</a>
</li>

View File

@ -115,39 +115,34 @@ useHead({
</script>
<template>
<section>
<h3 class="alex">Online presence</h3>
<hr class="accent accent-gradient border-0 h-px" />
<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" />
<div class="row">
<div class="col-12 col-md-3">
<h5 class="alex">Social media</h5>
<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">
<a class="link-hi" target="_blank" rel="noopener" :href="page.url">
<Icon :name="page.icon.name" :color="page.icon.color" inline />
<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>
</a>
</NuxtLink>
</li>
</ul>
</div>
<div class="col-12 col-md-9">
<h5 class="alex">Contact me</h5>
<div class="md:basis-0 md:flex-grow-[3]">
<h5>Contact me</h5>
<p>
Personal:
<EMail
class="link-hi"
:address="`contact@${fqdn}`"
subject="Hey Enderman!"
/><br />
Manager: <EMail class="link-hi" :address="`manager@${fqdn}`" /><br />
Abuse: <EMail class="link-hi" :address="`abuse@${fqdn}`" />
<EMail :address="`contact@${fqdn}`" subject="Hey Enderman!" /><br />
Manager: <EMail :address="`manager@${fqdn}`" /><br />
Abuse: <EMail :address="`abuse@${fqdn}`" />
</p>
</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 projectIcon from '~/assets/images/icons/defrag.png'
import socialIcon from '~/assets/images/icons/user.png'
@ -50,8 +48,9 @@ export const usePageStore = defineStore('page', () => {
},
])
const reader = ref(false)
const animate = ref(true)
const reader = ref<boolean>(false)
const animate = ref<boolean>(false)
const dark = ref<boolean>(false)
function _autoFetchPages() {
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',
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: {
sans: [
@ -38,6 +46,7 @@ export default <Partial<Config>>{
'monospace',
],
alex: ['Alexandria', 'serif'],
enchant: ['Enchant', 'serif'],
lato: ['Lato', 'sans-serif'],
},
},