Ran biome format

This commit is contained in:
Kizuren 2026-01-05 15:51:33 +01:00
parent edf0bf3817
commit 4d92cea640
18 changed files with 361 additions and 369 deletions

View file

@ -7,53 +7,51 @@ export default defineAppConfig({
compoundVariants: [
{
color: 'pixelgreen',
variant: [
'outline',
'subtle'
],
class: 'focus-visible:ring-2 focus-visible:ring-inset ring-pixelgreen focus-visible:ring-pixelgreen'
}
variant: ['outline', 'subtle'],
class:
'focus-visible:ring-2 focus-visible:ring-inset ring-pixelgreen focus-visible:ring-pixelgreen',
},
],
variants: {
variant: {
outline: 'text-highlighted bg-black ring ring-inset ring-accented',
soft: 'text-highlighted bg-elevated/50 hover:bg-elevated focus:bg-elevated disabled:bg-elevated/50',
subtle: 'text-highlighted bg-elevated ring ring-inset ring-accented',
ghost: 'text-highlighted bg-transparent hover:bg-elevated focus:bg-elevated disabled:bg-transparent dark:disabled:bg-transparent',
none: 'text-highlighted bg-transparent'
ghost:
'text-highlighted bg-transparent hover:bg-elevated focus:bg-elevated disabled:bg-transparent dark:disabled:bg-transparent',
none: 'text-highlighted bg-transparent',
},
},
defaultVariants: {
size: 'md',
color: 'pixelgreen',
variant: 'outline'
}
variant: 'outline',
},
},
textarea: {
compoundVariants: [
{
color: 'pixelgreen',
variant: [
'outline',
'subtle'
],
class: 'focus-visible:ring-2 focus-visible:ring-inset ring-pixelgreen focus-visible:ring-pixelgreen'
}
variant: ['outline', 'subtle'],
class:
'focus-visible:ring-2 focus-visible:ring-inset ring-pixelgreen focus-visible:ring-pixelgreen',
},
],
variants: {
variant: {
outline: 'text-highlighted bg-black ring ring-inset ring-accented',
soft: 'text-highlighted bg-elevated/50 hover:bg-elevated focus:bg-elevated disabled:bg-elevated/50',
subtle: 'text-highlighted bg-elevated ring ring-inset ring-accented',
ghost: 'text-highlighted bg-transparent hover:bg-elevated focus:bg-elevated disabled:bg-transparent dark:disabled:bg-transparent',
none: 'text-highlighted bg-transparent'
ghost:
'text-highlighted bg-transparent hover:bg-elevated focus:bg-elevated disabled:bg-transparent dark:disabled:bg-transparent',
none: 'text-highlighted bg-transparent',
},
},
defaultVariants: {
size: 'md',
color: 'pixelgreen',
variant: 'outline'
}
variant: 'outline',
},
},
toast: {
slots: {
@ -64,13 +62,13 @@ export default defineAppConfig({
pixelgreen: {
root: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-pixelgreen',
icon: 'text-pixelgreen',
progress: 'bg-pixelgreen'
}
progress: 'bg-pixelgreen',
},
},
},
defaultVariants: {
color: 'pixelgreen'
}
}
}
})
color: 'pixelgreen',
},
},
},
});

39
app.vue
View file

@ -7,14 +7,14 @@
</template>
<script setup>
const colorMode = useColorMode()
const colorMode = useColorMode();
const favicon = computed(() => {
const timestamp = Date.now()
return colorMode.value === 'dark'
? `/favicon_black.ico?t=${timestamp}`
: `/favicon.ico?t=${timestamp}`
})
const timestamp = Date.now();
return colorMode.value === 'dark'
? `/favicon_black.ico?t=${timestamp}`
: `/favicon.ico?t=${timestamp}`;
});
useHead({
link: [
@ -24,17 +24,20 @@ useHead({
href: favicon.value,
},
],
})
});
watch(() => colorMode.value, () => {
useHead({
link: [
{
rel: 'icon',
type: 'image/x-icon',
href: favicon.value,
},
],
})
})
watch(
() => colorMode.value,
() => {
useHead({
link: [
{
rel: 'icon',
type: 'image/x-icon',
href: favicon.value,
},
],
});
}
);
</script>

View file

@ -95,4 +95,4 @@
}
}
}
}
}

View file

@ -11,5 +11,5 @@
</template>
<script setup>
const { config } = useSiteConfig()
const { config } = useSiteConfig();
</script>

View file

@ -15,7 +15,7 @@
</template>
<script setup>
const { config } = useSiteConfig()
const { config } = useSiteConfig();
</script>
<style scoped>

View file

@ -1,77 +1,77 @@
import { ref, reactive, onMounted } from 'vue'
import { ref, reactive, onMounted } from 'vue';
// Shared state to prevent multiple toasts
let toastShown = false
let toastShown = false;
export function useSiteConfig() {
const config = reactive({
siteLinks: {},
buttons: []
})
const config = reactive({
siteLinks: {},
buttons: [],
});
const isLoading = ref(true)
const error = ref(null)
const toast = useToast()
const isLoading = ref(true);
const error = ref(null);
const toast = useToast();
async function loadConfig() {
try {
toastShown = false
isLoading.value = true
const response = await fetch('/site-config.json')
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`)
}
const data = await response.json()
async function loadConfig() {
try {
toastShown = false;
isLoading.value = true;
if (!data['site-links'] && !data.buttons) {
throw new Error('Invalid configuration format: Missing required fields')
}
const response = await fetch('/site-config.json');
config.siteLinks = data['site-links'] || {}
config.buttons = data.buttons || []
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
error.value = null
} catch (err) {
console.error('Failed to load site configuration:', err)
error.value = err
const data = await response.json();
let errorMessage = 'Failed to load site configuration. Please try again later.'
if (err.message.includes('HTTP error')) {
const status = err.message.match(/\d+/) ? err.message.match(/\d+/)[0] : 'unknown'
errorMessage = `Server returned ${status} error. Please check if the configuration file exists.`
} else if (err.name === 'SyntaxError') {
errorMessage = 'Invalid JSON format in configuration file.'
} else if (err.message.includes('Invalid configuration format')) {
errorMessage = err.message
}
if (!data['site-links'] && !data.buttons) {
throw new Error('Invalid configuration format: Missing required fields');
}
if (!toastShown) {
toast.add({
title: 'Configuration Error',
description: errorMessage,
icon: 'i-lucide-alert-triangle',
color: 'error',
timeout: 5000
})
toastShown = true
}
} finally {
isLoading.value = false
}
config.siteLinks = data['site-links'] || {};
config.buttons = data.buttons || [];
error.value = null;
} catch (err) {
console.error('Failed to load site configuration:', err);
error.value = err;
let errorMessage = 'Failed to load site configuration. Please try again later.';
if (err.message.includes('HTTP error')) {
const status = err.message.match(/\d+/) ? err.message.match(/\d+/)[0] : 'unknown';
errorMessage = `Server returned ${status} error. Please check if the configuration file exists.`;
} else if (err.name === 'SyntaxError') {
errorMessage = 'Invalid JSON format in configuration file.';
} else if (err.message.includes('Invalid configuration format')) {
errorMessage = err.message;
}
if (!toastShown) {
toast.add({
title: 'Configuration Error',
description: errorMessage,
icon: 'i-lucide-alert-triangle',
color: 'error',
timeout: 5000,
});
toastShown = true;
}
} finally {
isLoading.value = false;
}
}
onMounted(() => {
loadConfig()
})
onMounted(() => {
loadConfig();
});
return {
config,
isLoading,
error,
reload: loadConfig
}
}
return {
config,
isLoading,
error,
reload: loadConfig,
};
}

View file

@ -1,4 +1,4 @@
import { defineCollection, defineContentConfig, z } from '@nuxt/content'
import { defineCollection, defineContentConfig, z } from '@nuxt/content';
export default defineContentConfig({
collections: {
@ -14,4 +14,4 @@ export default defineContentConfig({
}),
}),
},
})
});

View file

@ -1,14 +1,16 @@
module.exports = {
apps: [{
name: 'marcus7i',
script: '.output/server/index.mjs',
instances: 'max',
exec_mode: 'cluster',
env: {
NODE_ENV: 'production',
PORT: 3000
},
watch: false,
max_memory_restart: '1G'
}]
}
apps: [
{
name: 'marcus7i',
script: '.output/server/index.mjs',
instances: 'max',
exec_mode: 'cluster',
env: {
NODE_ENV: 'production',
PORT: 3000,
},
watch: false,
max_memory_restart: '1G',
},
],
};

View file

@ -1,9 +1,9 @@
<script setup lang="ts">
import type { NuxtError } from '#app'
import type { NuxtError } from '#app';
const props = defineProps({
error: Object as () => NuxtError
})
error: Object as () => NuxtError,
});
</script>
<template>

View file

@ -9,44 +9,30 @@ export default defineNuxtConfig({
'@nuxt/image',
'@nuxt/scripts',
'@nuxt/test-utils',
'@nuxt/ui'
'@nuxt/ui',
],
css: [
"~/assets/main.css"
],
css: ['~/assets/main.css'],
colorMode: {
preference: 'system',
fallback: 'dark',
classSuffix: '',
},
app: {
head: {
title: 'Kizuren',
meta: [
{ name: 'description', content: 'The official site for Kizuren.dev' }
],
link: [
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
]
}
meta: [{ name: 'description', content: 'The official site for Kizuren.dev' }],
link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }],
},
},
ui: {
theme: {
colors: [
'primary',
'pixelgreen',
'secondary',
'info',
'success',
'warning',
'error'
]
}
colors: ['primary', 'pixelgreen', 'secondary', 'info', 'success', 'warning', 'error'],
},
},
compatibilityDate: '2025-05-29',
})
});

View file

@ -1,12 +1,12 @@
<script lang="ts" setup>
const route = useRoute()
const route = useRoute();
const { data: page } = await useAsyncData(route.path, () => {
return queryCollection('content').path(route.path).first()
})
return queryCollection('content').path(route.path).first();
});
useHead(() => ({
title: page.value?.title || 'About'
}))
title: page.value?.title || 'About',
}));
</script>
<template>

View file

@ -68,52 +68,57 @@
</template>
<script setup lang="ts">
import { z } from 'zod'
import { z } from 'zod';
useHead({
title: 'Contact'
})
title: 'Contact',
});
const schema = z.object({
email: z.string().email({ message: 'Please enter a valid email address' }).min(1, { message: 'Email is required' }),
email: z
.string()
.email({ message: 'Please enter a valid email address' })
.min(1, { message: 'Email is required' }),
subject: z.string().min(3, { message: 'Subject must be at least 3 characters' }),
message: z.string().min(10, { message: 'Message must be at least 10 characters' })
})
message: z.string().min(10, { message: 'Message must be at least 10 characters' }),
});
const state = reactive({
email: '',
subject: '',
message: ''
})
message: '',
});
const isSubmitting = ref(false)
const toast = useToast()
const isSubmitting = ref(false);
const toast = useToast();
async function onSubmit() {
try {
isSubmitting.value = true
isSubmitting.value = true;
const discordMessage = {
embeds: [{
title: `Contact Form: ${state.subject}`,
description: state.message,
color: 3447003, // Blue
fields: [
{
name: 'Email',
value: state.email
}
],
timestamp: new Date().toISOString()
}]
}
embeds: [
{
title: `Contact Form: ${state.subject}`,
description: state.message,
color: 3447003, // Blue
fields: [
{
name: 'Email',
value: state.email,
},
],
timestamp: new Date().toISOString(),
},
],
};
await $fetch('/api/send-contact', {
method: 'POST',
body: {
message: discordMessage
}
})
body: {
message: discordMessage,
},
});
toast.add({
title: 'Message Sent!',
@ -122,22 +127,21 @@ async function onSubmit() {
color: 'pixelgreen',
ui: {
root: '',
}
})
state.email = ''
state.subject = ''
state.message = ''
},
});
state.email = '';
state.subject = '';
state.message = '';
} catch {
toast.add({
title: 'Error',
description: 'Something went wrong. Please try again.',
icon: 'i-heroicons-exclamation-circle',
color: 'error'
})
color: 'error',
});
} finally {
isSubmitting.value = false
isSubmitting.value = false;
}
}
</script>

View file

@ -24,39 +24,39 @@
<script setup>
useHead({
title: 'Discord'
})
title: 'Discord',
});
const { config } = useSiteConfig()
const { config } = useSiteConfig();
const discordLink = computed(() => config.siteLinks?.['discord-invite'] || '/maintenance')
const discordLink = computed(() => config.siteLinks?.['discord-invite'] || '/maintenance');
// Progress state
const progress = ref(0)
const totalTime = 3 // seconds
const remainingTime = ref(totalTime)
const interval = 50 // ms
const progress = ref(0);
const totalTime = 3; // seconds
const remainingTime = ref(totalTime);
const interval = 50; // ms
let timer
let timer;
onMounted(() => {
const startTime = Date.now()
timer = setInterval(() => {
const elapsed = (Date.now() - startTime) / 1000
remainingTime.value = Math.max(0, totalTime - elapsed)
const startTime = Date.now();
timer = setInterval(() => {
const elapsed = (Date.now() - startTime) / 1000;
remainingTime.value = Math.max(0, totalTime - elapsed);
const rawProgress = (elapsed / totalTime) * 100;
progress.value = Math.min(100, Math.max(0, Math.round(rawProgress)));
const rawProgress = (elapsed / totalTime) * 100
progress.value = Math.min(100, Math.max(0, Math.round(rawProgress)))
if (elapsed >= totalTime) {
clearInterval(timer)
window.location.href = discordLink.value
clearInterval(timer);
window.location.href = discordLink.value;
}
}, interval)
})
}, interval);
});
onBeforeUnmount(() => {
if (timer) clearInterval(timer)
})
if (timer) clearInterval(timer);
});
</script>

View file

@ -60,68 +60,68 @@
</template>
<script lang="ts" setup>
import siteConfig from '~/public/site-config.json'
import { ref, onMounted } from 'vue'
import siteConfig from '~/public/site-config.json';
import { ref, onMounted } from 'vue';
const games = siteConfig.games
const imageCache = ref(new Map())
const games = siteConfig.games;
const imageCache = ref(new Map());
function getBadgeColor(status: string) {
if (status === 'Released') return 'pixelgreen'
if (status === 'Abandoned') return 'error'
return 'warning'
if (status === 'Released') return 'pixelgreen';
if (status === 'Abandoned') return 'error';
return 'warning';
}
// Check if image exists and cache result
function getImageSrc(game: { [x: string]: any }) {
// During SSR, just return the URL or fallback
if (typeof window === 'undefined') {
return game['logo-url'] || '/cancel.svg'
return game['logo-url'] || '/cancel.svg';
}
const url = game['logo-url']
const url = game['logo-url'];
// If no URL or already checked and failed, use cancel.svg
if (!url || imageCache.value.get(url) === false) {
return '/cancel.svg'
return '/cancel.svg';
}
// If not checked yet, check it now
if (!imageCache.value.has(url)) {
checkImage(url)
checkImage(url);
}
return url
return url;
}
// Function to check if image exists
function checkImage(url: string | null) {
// Skip this function if not in browser or url is null
if (typeof window === 'undefined' || url === null) return
const img = new window.Image()
if (typeof window === 'undefined' || url === null) return;
const img = new window.Image();
img.onload = () => {
imageCache.value.set(url, true)
}
imageCache.value.set(url, true);
};
img.onerror = () => {
imageCache.value.set(url, false)
imageCache.value.set(url, false);
// Force a component update for this URL
const affectedGames = games.filter(g => g['logo-url'] === url)
const affectedGames = games.filter(g => g['logo-url'] === url);
if (affectedGames.length) {
// This will trigger a re-render
imageCache.value = new Map(imageCache.value)
imageCache.value = new Map(imageCache.value);
}
}
img.src = url
};
img.src = url;
}
function handleImageError(event: Event) {
const img = event.target as HTMLImageElement
img.src = '/cancel.svg'
const img = event.target as HTMLImageElement;
img.src = '/cancel.svg';
// Also cache this failure for future renders
if (img.dataset.originalSrc) {
imageCache.value.set(img.dataset.originalSrc, false)
imageCache.value.set(img.dataset.originalSrc, false);
}
}
@ -129,12 +129,12 @@ function handleImageError(event: Event) {
onMounted(() => {
games.forEach(game => {
if (game['logo-url']) {
checkImage(game['logo-url'])
checkImage(game['logo-url']);
}
})
})
});
});
useHead({
title: 'Games'
})
title: 'Games',
});
</script>

View file

@ -64,10 +64,10 @@
<script setup>
useHead({
title: 'Kizuren'
})
title: 'Kizuren',
});
const { config } = useSiteConfig()
const { config } = useSiteConfig();
</script>
<style>

View file

@ -19,8 +19,8 @@
<script setup>
useHead({
title: 'Maintenance'
})
title: 'Maintenance',
});
</script>
<style scoped>

View file

@ -1,90 +1,89 @@
{
"site-links":
"site-links": {
"discord-invite": "https://discord.gg/e37aq2wc66",
"github": "https://github.com/Kizuren",
"status-page": "https://status.kizuren.dev"
},
"buttons": [
{
"discord-invite": "https://discord.gg/e37aq2wc66",
"github": "https://github.com/Kizuren",
"status-page": "https://status.kizuren.dev"
"title": "Otakuanime",
"url": "https://otakuani.me",
"icon": "i-simple-icons-stremio",
"description": "An anime site to watch for free"
},
"buttons": [
{
"title": "Otakuanime",
"url": "https://otakuani.me",
"icon": "i-simple-icons-stremio",
"description": "An anime site to watch for free"
},
{
"title": "µLinkShortener",
"url": "https://u.kizuren.dev",
"icon": "line-md:link",
"description": "URL shortener and data collector"
},
{
"title": "Git",
"url": "https://git.kizuren.dev",
"icon": "i-simple-icons-git",
"description": "Self-hosted Git instance"
},
{
"title": "Games",
"url": "/games",
"icon": "line-md:play-filled",
"description": ""
},
{
"title": "SauceKudasai",
"url": "https://saucekudasai.kizuren.dev",
"icon": "i-simple-icons-sunrise",
"description": "Find a specific anime from a screenshot"
},
{
"title": "Open-WebUI",
"url": "https://ollama.kizuren.dev",
"icon": "i-simple-icons-ollama",
"description": "Self-hosted WebUI for LLMs using Ollama"
}
],
"games": [
{
"title": "Shanti Manti",
"description": "Shanti Manti has to fight against his classmates to survive.",
"status": "Released",
"logo-url": "/game-icons/shantimanti.png",
"url": [
{
"host": "GitHub",
"logo": "i-simple-icons-github",
"url": "https://github.com/Kizuren/ShantiManti"
},
{
"host": "Itch",
"logo": "i-simple-icons-itchdotio",
"url": "https://kizuren.itch.io/shanti-manti"
}
]
},
{
"title": "SynthMaze",
"description": "You have to solve the Mazes to escape from the Enemy's headquarter.",
"status": "Abandoned",
"logo-url": "/game-icons/synthmaze.png",
"url": [
{
"host": "GitHub",
"logo": "i-simple-icons-github",
"url": "https://github.com/Kizuren/SynthMaze"
},
{
"host": "Itch",
"logo": "i-simple-icons-itchdotio",
"url": "https://kizuren.itch.io/synthmaze"
}
]
},
{
"title": "TetrisRemastered",
"description": "Tetris in 2D & 3D with customizable music engine",
"status": "In development",
"logo-url": "/game-icons/tetrisremastered.png"
}
]
}
{
"title": "µLinkShortener",
"url": "https://u.kizuren.dev",
"icon": "line-md:link",
"description": "URL shortener and data collector"
},
{
"title": "Git",
"url": "https://git.kizuren.dev",
"icon": "i-simple-icons-git",
"description": "Self-hosted Git instance"
},
{
"title": "Games",
"url": "/games",
"icon": "line-md:play-filled",
"description": ""
},
{
"title": "SauceKudasai",
"url": "https://saucekudasai.kizuren.dev",
"icon": "i-simple-icons-sunrise",
"description": "Find a specific anime from a screenshot"
},
{
"title": "Open-WebUI",
"url": "https://ollama.kizuren.dev",
"icon": "i-simple-icons-ollama",
"description": "Self-hosted WebUI for LLMs using Ollama"
}
],
"games": [
{
"title": "Shanti Manti",
"description": "Shanti Manti has to fight against his classmates to survive.",
"status": "Released",
"logo-url": "/game-icons/shantimanti.png",
"url": [
{
"host": "GitHub",
"logo": "i-simple-icons-github",
"url": "https://github.com/Kizuren/ShantiManti"
},
{
"host": "Itch",
"logo": "i-simple-icons-itchdotio",
"url": "https://kizuren.itch.io/shanti-manti"
}
]
},
{
"title": "SynthMaze",
"description": "You have to solve the Mazes to escape from the Enemy's headquarter.",
"status": "Abandoned",
"logo-url": "/game-icons/synthmaze.png",
"url": [
{
"host": "GitHub",
"logo": "i-simple-icons-github",
"url": "https://github.com/Kizuren/SynthMaze"
},
{
"host": "Itch",
"logo": "i-simple-icons-itchdotio",
"url": "https://kizuren.itch.io/synthmaze"
}
]
},
{
"title": "TetrisRemastered",
"description": "Tetris in 2D & 3D with customizable music engine",
"status": "In development",
"logo-url": "/game-icons/tetrisremastered.png"
}
]
}

View file

@ -1,35 +1,35 @@
export default defineEventHandler(async (event) => {
export default defineEventHandler(async event => {
try {
const body = await readBody(event)
const { message } = body
const webhookUrl = process.env.DISCORD_WEBHOOK
const body = await readBody(event);
const { message } = body;
const webhookUrl = process.env.DISCORD_WEBHOOK;
if (!webhookUrl) {
throw new Error('Discord webhook URL is not configured')
throw new Error('Discord webhook URL is not configured');
}
const response = await fetch(webhookUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
'Content-Type': 'application/json',
},
body: JSON.stringify(message)
})
body: JSON.stringify(message),
});
if (!response.ok) {
const errorText = await response.text()
throw new Error(`Discord API error: ${response.status} - ${errorText}`)
const errorText = await response.text();
throw new Error(`Discord API error: ${response.status} - ${errorText}`);
}
return { success: true }
return { success: true };
} catch (error) {
console.error('Failed to send message to Discord:', error)
console.error('Failed to send message to Discord:', error);
return createError({
statusCode: 500,
statusMessage: 'Failed to send message',
message: error.message || 'An unknown error occurred'
})
message: error.message || 'An unknown error occurred',
});
}
})
});