mirror of
https://github.com/Kizuren/kizuren.dev.git
synced 2025-12-21 21:16:12 +01:00
Implement everything
This commit is contained in:
parent
d966dd9562
commit
30d747e046
26 changed files with 1069 additions and 6 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -22,3 +22,6 @@ logs
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
!.env.example
|
!.env.example
|
||||||
|
|
||||||
|
# Visual Studio Code
|
||||||
|
.vscode
|
||||||
42
app.vue
42
app.vue
|
|
@ -1,6 +1,40 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<UApp>
|
||||||
<NuxtRouteAnnouncer />
|
<NuxtLayout>
|
||||||
<NuxtWelcome />
|
<NuxtPage />
|
||||||
</div>
|
</NuxtLayout>
|
||||||
|
</UApp>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
const colorMode = useColorMode()
|
||||||
|
|
||||||
|
const favicon = computed(() => {
|
||||||
|
const timestamp = Date.now()
|
||||||
|
return colorMode.value === 'dark'
|
||||||
|
? `/favicon_black.ico?t=${timestamp}`
|
||||||
|
: `/favicon.ico?t=${timestamp}`
|
||||||
|
})
|
||||||
|
|
||||||
|
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>
|
||||||
88
assets/main.css
Normal file
88
assets/main.css
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
@import "tailwindcss";
|
||||||
|
@import "@nuxt/ui";
|
||||||
|
|
||||||
|
/* Global overrides for Nuxt UI components */
|
||||||
|
/*.nuxt-ui-button,
|
||||||
|
.nuxt-ui-link,
|
||||||
|
[class*="nuxt-ui-button"],
|
||||||
|
[class*="nuxt-ui-link"] {
|
||||||
|
cursor: default !important;
|
||||||
|
}*/
|
||||||
|
|
||||||
|
/* For UButton and ULink without nuxt-ui classes */
|
||||||
|
/*button, a {
|
||||||
|
cursor: default !important;
|
||||||
|
}*/
|
||||||
|
|
||||||
|
.prose {
|
||||||
|
max-width: 65ch;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose h1 {
|
||||||
|
font-size: 2.25rem;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-top: 2rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove link styling from headings in Markdown content */
|
||||||
|
.prose h2 a,
|
||||||
|
.prose h3 a,
|
||||||
|
.prose h4 a,
|
||||||
|
.prose h5 a,
|
||||||
|
.prose h6 a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose h2 a:hover,
|
||||||
|
.prose h3 a:hover,
|
||||||
|
.prose h4 a:hover,
|
||||||
|
.prose h5 a:hover,
|
||||||
|
.prose h6 a:hover {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide the # link icon that might appear on hover */
|
||||||
|
.prose h2 a::after,
|
||||||
|
.prose h3 a::after,
|
||||||
|
.prose h4 a::after,
|
||||||
|
.prose h5 a::after,
|
||||||
|
.prose h6 a::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose p {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose a {
|
||||||
|
color: #1d4ed8;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose a:hover {
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .prose {
|
||||||
|
color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .prose a {
|
||||||
|
color: #60a5fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .prose a:hover {
|
||||||
|
color: #93c5fd;
|
||||||
|
}
|
||||||
15
components/AppFooter.vue
Normal file
15
components/AppFooter.vue
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<template>
|
||||||
|
<footer class="bg-gray-800 text-white p-4">
|
||||||
|
<div class="flex justify-between items-center px-4 sm:px-6 md:px-8">
|
||||||
|
<div>© {{ new Date().getFullYear() }} <ULink to="/" class="text-white cursor-default">MarcUs7i.Net</ULink>. All rights reserved.</div>
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<ULink to="/discord" target="_blank">Discord</ULink>
|
||||||
|
<ULink v-if="config.siteLinks.github" :to="config.siteLinks.github" target="_blank">GitHub</ULink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
const { config } = useSiteConfig()
|
||||||
|
</script>
|
||||||
19
components/NavBar.vue
Normal file
19
components/NavBar.vue
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
<template>
|
||||||
|
<header class="bg-gray-800 text-white p-4">
|
||||||
|
<nav class="flex justify-between items-center">
|
||||||
|
<div class="logo">
|
||||||
|
<ULink as="button" to="/" class="text-xl text-white cursor-default">MarcUs7i.Net</ULink>
|
||||||
|
</div>
|
||||||
|
<ul class="flex gap-4">
|
||||||
|
<li><ULink to="/about">About</ULink></li>
|
||||||
|
<li><ULink to="/contact">Contact</ULink></li>
|
||||||
|
<li v-if="config.siteLinks.github"><ULink :to="config.siteLinks.github" target="_blank"><UIcon name="i-simple-icons-github" class="size-5" /></ULink></li>
|
||||||
|
<li><ULink to="/discord" target="_blank"><UIcon name="i-simple-icons-discord" class="size-5" /></ULink></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
const { config } = useSiteConfig()
|
||||||
|
</script>
|
||||||
77
composables/fetch-sitelinks.js
Normal file
77
composables/fetch-sitelinks.js
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
|
||||||
|
// Shared state to prevent multiple toasts
|
||||||
|
let toastShown = false
|
||||||
|
|
||||||
|
export function useSiteConfig() {
|
||||||
|
const config = reactive({
|
||||||
|
siteLinks: {},
|
||||||
|
buttons: []
|
||||||
|
})
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
if (!data['site-links'] && !data.buttons) {
|
||||||
|
throw new Error('Invalid configuration format: Missing required fields')
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
config,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
reload: loadConfig
|
||||||
|
}
|
||||||
|
}
|
||||||
17
content.config.ts
Normal file
17
content.config.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { defineCollection, defineContentConfig, z } from '@nuxt/content'
|
||||||
|
|
||||||
|
export default defineContentConfig({
|
||||||
|
collections: {
|
||||||
|
/**
|
||||||
|
* This is collection for content-wind theme
|
||||||
|
* Create `content.config.ts` in project root to overwrite this
|
||||||
|
*/
|
||||||
|
content: defineCollection({
|
||||||
|
type: 'page',
|
||||||
|
source: '**',
|
||||||
|
schema: z.object({
|
||||||
|
layout: z.string(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
})
|
||||||
17
content/about.md
Normal file
17
content/about.md
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
---
|
||||||
|
title: 'About'
|
||||||
|
description: 'about page of marcus7i.net'
|
||||||
|
---
|
||||||
|
|
||||||
|
# About MarcUs7i.Net
|
||||||
|
|
||||||
|
This site hosts multiple services and applications, mostly created, some improved by MarcUs7i
|
||||||
|
|
||||||
|
## Donations
|
||||||
|
|
||||||
|
You can support me by donating to my [Ko-fi](https://ko-fi.com/marcus7i).
|
||||||
|
|
||||||
|
## Contact
|
||||||
|
|
||||||
|
Feel free to [get in touch](/discord) on discord if you'd like to collaborate on a project.<br>
|
||||||
|
If discord is a no-go, you can also simply [contact](/contact) via the website.
|
||||||
9
layouts/default.vue
Normal file
9
layouts/default.vue
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<template>
|
||||||
|
<div class="min-h-screen flex flex-col">
|
||||||
|
<NavBar />
|
||||||
|
<main class="flex-grow container mx-auto px-4 py-8">
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
<AppFooter />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
compatibilityDate: '2024-11-01',
|
|
||||||
devtools: { enabled: true },
|
devtools: { enabled: true },
|
||||||
|
|
||||||
modules: [
|
modules: [
|
||||||
|
|
@ -12,5 +11,29 @@ export default defineNuxtConfig({
|
||||||
'@nuxt/scripts',
|
'@nuxt/scripts',
|
||||||
'@nuxt/test-utils',
|
'@nuxt/test-utils',
|
||||||
'@nuxt/ui'
|
'@nuxt/ui'
|
||||||
]
|
],
|
||||||
|
|
||||||
|
css: [
|
||||||
|
"~/assets/main.css"
|
||||||
|
],
|
||||||
|
|
||||||
|
colorMode: {
|
||||||
|
preference: 'system',
|
||||||
|
fallback: 'dark',
|
||||||
|
classSuffix: '',
|
||||||
|
},
|
||||||
|
|
||||||
|
app: {
|
||||||
|
head: {
|
||||||
|
title: 'MarcUs7i.Net',
|
||||||
|
meta: [
|
||||||
|
{ name: 'description', content: 'The official site for MarcUs7i.Net' }
|
||||||
|
],
|
||||||
|
link: [
|
||||||
|
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
compatibilityDate: '2025-03-22',
|
||||||
})
|
})
|
||||||
107
package-lock.json
generated
107
package-lock.json
generated
|
|
@ -15,12 +15,23 @@
|
||||||
"@nuxt/scripts": "^0.11.2",
|
"@nuxt/scripts": "^0.11.2",
|
||||||
"@nuxt/test-utils": "^3.17.2",
|
"@nuxt/test-utils": "^3.17.2",
|
||||||
"@nuxt/ui": "^3.0.1",
|
"@nuxt/ui": "^3.0.1",
|
||||||
|
"@tailwindcss/postcss": "^4.0.15",
|
||||||
|
"@tailwindcss/vite": "^4.0.15",
|
||||||
"@unhead/vue": "^2.0.0-rc.8",
|
"@unhead/vue": "^2.0.0-rc.8",
|
||||||
|
"animate.css": "^4.1.1",
|
||||||
"eslint": "^9.23.0",
|
"eslint": "^9.23.0",
|
||||||
"nuxt": "^3.16.1",
|
"nuxt": "^3.16.1",
|
||||||
|
"sass": "^1.86.0",
|
||||||
|
"tailwindcss": "^4.0.15",
|
||||||
"typescript": "^5.8.2",
|
"typescript": "^5.8.2",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
"vue-router": "^4.5.0"
|
"vue-router": "^4.5.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@iconify-json/material-symbols": "^1.2.17",
|
||||||
|
"@iconify-json/simple-icons": "^1.2.29",
|
||||||
|
"@nuxtjs/mdc": "^0.16.1",
|
||||||
|
"@tailwindcss/typography": "^0.5.16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@alloc/quick-lru": {
|
"node_modules/@alloc/quick-lru": {
|
||||||
|
|
@ -1432,6 +1443,26 @@
|
||||||
"url": "https://github.com/sponsors/nzakas"
|
"url": "https://github.com/sponsors/nzakas"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@iconify-json/material-symbols": {
|
||||||
|
"version": "1.2.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@iconify-json/material-symbols/-/material-symbols-1.2.17.tgz",
|
||||||
|
"integrity": "sha512-hKb+Ii5cqLXXefYMxUB2jIc8BNqxixQogud4KU/fn0F4puM1iCdCF2lFV+0U8wnJ6dZIx6E+w8Ree4bIT7To+A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@iconify/types": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@iconify-json/simple-icons": {
|
||||||
|
"version": "1.2.29",
|
||||||
|
"resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.29.tgz",
|
||||||
|
"integrity": "sha512-KYrxmxtRz6iOAulRiUsIBMUuXek+H+Evwf8UvYPIkbQ+KDoOqTegHx3q/w3GDDVC0qJYB+D3hXPMZcpm78qIuA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "CC0-1.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@iconify/types": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@iconify/collections": {
|
"node_modules/@iconify/collections": {
|
||||||
"version": "1.0.530",
|
"version": "1.0.530",
|
||||||
"resolved": "https://registry.npmjs.org/@iconify/collections/-/collections-1.0.530.tgz",
|
"resolved": "https://registry.npmjs.org/@iconify/collections/-/collections-1.0.530.tgz",
|
||||||
|
|
@ -3952,6 +3983,36 @@
|
||||||
"tailwindcss": "4.0.15"
|
"tailwindcss": "4.0.15"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tailwindcss/typography": {
|
||||||
|
"version": "0.5.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.16.tgz",
|
||||||
|
"integrity": "sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"lodash.castarray": "^4.4.0",
|
||||||
|
"lodash.isplainobject": "^4.0.6",
|
||||||
|
"lodash.merge": "^4.6.2",
|
||||||
|
"postcss-selector-parser": "6.0.10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": {
|
||||||
|
"version": "6.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
|
||||||
|
"integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cssesc": "^3.0.0",
|
||||||
|
"util-deprecate": "^1.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tailwindcss/vite": {
|
"node_modules/@tailwindcss/vite": {
|
||||||
"version": "4.0.15",
|
"version": "4.0.15",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.0.15.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.0.15.tgz",
|
||||||
|
|
@ -5038,6 +5099,12 @@
|
||||||
"integrity": "sha512-aITl4ODHNX9mqBqwZWr5oTYP74hemqVGV4KRLSQacjoZIdwNxbedHF656+c4zuGLtRtcowitoXdIfyrXgzniVg==",
|
"integrity": "sha512-aITl4ODHNX9mqBqwZWr5oTYP74hemqVGV4KRLSQacjoZIdwNxbedHF656+c4zuGLtRtcowitoXdIfyrXgzniVg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/animate.css": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/animate.css/-/animate.css-4.1.1.tgz",
|
||||||
|
"integrity": "sha512-+mRmCTv6SbCmtYJCN4faJMNFVNN5EuCTTprDTAo7YzIGji2KADmakjVA3+8mVDkZ2Bf09vayB35lSQIex2+QaQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/ansi-colors": {
|
"node_modules/ansi-colors": {
|
||||||
"version": "4.1.3",
|
"version": "4.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
|
||||||
|
|
@ -8812,6 +8879,12 @@
|
||||||
"integrity": "sha512-K6acvFaelNxx8wc2VjbIzXKDVB0Khs0QT35U6NkGfTdCmjLNcO2945m7RFNR9/RPVFm48hq7QPzK8uGH18HCGw==",
|
"integrity": "sha512-K6acvFaelNxx8wc2VjbIzXKDVB0Khs0QT35U6NkGfTdCmjLNcO2945m7RFNR9/RPVFm48hq7QPzK8uGH18HCGw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/immutable": {
|
||||||
|
"version": "5.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.0.3.tgz",
|
||||||
|
"integrity": "sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/import-fresh": {
|
"node_modules/import-fresh": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
||||||
|
|
@ -9824,6 +9897,13 @@
|
||||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash.castarray": {
|
||||||
|
"version": "4.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz",
|
||||||
|
"integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lodash.defaults": {
|
"node_modules/lodash.defaults": {
|
||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
|
||||||
|
|
@ -9836,6 +9916,13 @@
|
||||||
"integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==",
|
"integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash.isplainobject": {
|
||||||
|
"version": "4.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||||
|
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lodash.memoize": {
|
"node_modules/lodash.memoize": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
|
||||||
|
|
@ -13334,6 +13421,26 @@
|
||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/sass": {
|
||||||
|
"version": "1.86.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/sass/-/sass-1.86.0.tgz",
|
||||||
|
"integrity": "sha512-zV8vGUld/+mP4KbMLJMX7TyGCuUp7hnkOScgCMsWuHtns8CWBoz+vmEhoGMXsaJrbUP8gj+F1dLvVe79sK8UdA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"chokidar": "^4.0.0",
|
||||||
|
"immutable": "^5.0.2",
|
||||||
|
"source-map-js": ">=0.6.2 <2.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"sass": "sass.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@parcel/watcher": "^2.4.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/scslre": {
|
"node_modules/scslre": {
|
||||||
"version": "0.3.0",
|
"version": "0.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/scslre/-/scslre-0.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/scslre/-/scslre-0.3.0.tgz",
|
||||||
|
|
|
||||||
11
package.json
11
package.json
|
|
@ -18,11 +18,22 @@
|
||||||
"@nuxt/scripts": "^0.11.2",
|
"@nuxt/scripts": "^0.11.2",
|
||||||
"@nuxt/test-utils": "^3.17.2",
|
"@nuxt/test-utils": "^3.17.2",
|
||||||
"@nuxt/ui": "^3.0.1",
|
"@nuxt/ui": "^3.0.1",
|
||||||
|
"@tailwindcss/postcss": "^4.0.15",
|
||||||
|
"@tailwindcss/vite": "^4.0.15",
|
||||||
"@unhead/vue": "^2.0.0-rc.8",
|
"@unhead/vue": "^2.0.0-rc.8",
|
||||||
|
"animate.css": "^4.1.1",
|
||||||
"eslint": "^9.23.0",
|
"eslint": "^9.23.0",
|
||||||
"nuxt": "^3.16.1",
|
"nuxt": "^3.16.1",
|
||||||
|
"sass": "^1.86.0",
|
||||||
|
"tailwindcss": "^4.0.15",
|
||||||
"typescript": "^5.8.2",
|
"typescript": "^5.8.2",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
"vue-router": "^4.5.0"
|
"vue-router": "^4.5.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@iconify-json/material-symbols": "^1.2.17",
|
||||||
|
"@iconify-json/simple-icons": "^1.2.29",
|
||||||
|
"@nuxtjs/mdc": "^0.16.1",
|
||||||
|
"@tailwindcss/typography": "^0.5.16"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
const route = useRoute()
|
||||||
|
const { data: page } = await useAsyncData(route.path, () => {
|
||||||
|
return queryCollection('content').path(route.path).first()
|
||||||
|
})
|
||||||
|
|
||||||
|
useHead(() => ({
|
||||||
|
title: page.value?.title || 'About'
|
||||||
|
}))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<template v-if="page">
|
||||||
|
<div>
|
||||||
|
<ContentRenderer :value="page.body" class="prose dark:prose-invert" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="empty-page">
|
||||||
|
<h1>404 Page Not Found</h1>
|
||||||
|
<p>Oops! The content you're looking for doesn't exist.</p>
|
||||||
|
<ULink to="/">Go back home</ULink>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
144
pages/contact.vue
Normal file
144
pages/contact.vue
Normal file
|
|
@ -0,0 +1,144 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col items-center justify-center py-12 px-4">
|
||||||
|
<h1 class="text-3xl font-bold mb-12">Contact Me</h1>
|
||||||
|
|
||||||
|
<div class="w-full max-w-2xl">
|
||||||
|
<UForm
|
||||||
|
:schema="schema"
|
||||||
|
:state="state"
|
||||||
|
class="space-y-8 bg-gray-800 p-8 rounded-lg shadow-lg"
|
||||||
|
@submit="onSubmit"
|
||||||
|
>
|
||||||
|
<UFormField
|
||||||
|
label="Email Address"
|
||||||
|
name="email"
|
||||||
|
required
|
||||||
|
class="mb-8"
|
||||||
|
>
|
||||||
|
<UInput
|
||||||
|
v-model="state.email"
|
||||||
|
placeholder="your.email@example.com"
|
||||||
|
size="lg"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</UFormField>
|
||||||
|
|
||||||
|
<UFormField
|
||||||
|
label="Message Subject"
|
||||||
|
name="subject"
|
||||||
|
required
|
||||||
|
class="mb-8"
|
||||||
|
>
|
||||||
|
<UInput
|
||||||
|
v-model="state.subject"
|
||||||
|
placeholder="What is this about?"
|
||||||
|
size="xl"
|
||||||
|
class="w-full text-lg"
|
||||||
|
/>
|
||||||
|
</UFormField>
|
||||||
|
|
||||||
|
<UFormField
|
||||||
|
label="Your Message"
|
||||||
|
name="message"
|
||||||
|
required
|
||||||
|
class="mb-8"
|
||||||
|
>
|
||||||
|
<UTextarea
|
||||||
|
v-model="state.message"
|
||||||
|
placeholder="Your message here..."
|
||||||
|
:rows="8"
|
||||||
|
size="lg"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</UFormField>
|
||||||
|
|
||||||
|
<div class="flex justify-end mt-10">
|
||||||
|
<UButton
|
||||||
|
type="submit"
|
||||||
|
:loading="isSubmitting"
|
||||||
|
size="lg"
|
||||||
|
color="primary"
|
||||||
|
class="px-8 py-2"
|
||||||
|
>
|
||||||
|
Send Message
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</UForm>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
useHead({
|
||||||
|
title: 'Contact'
|
||||||
|
})
|
||||||
|
|
||||||
|
import { z } from 'zod'
|
||||||
|
import type { FormSubmitEvent } from '@nuxt/ui'
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
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' })
|
||||||
|
})
|
||||||
|
|
||||||
|
type Schema = z.infer<typeof schema>
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
email: '',
|
||||||
|
subject: '',
|
||||||
|
message: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const isSubmitting = ref(false)
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
async function onSubmit(event: FormSubmitEvent<Schema>) {
|
||||||
|
try {
|
||||||
|
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()
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
await $fetch('/api/send-contact', {
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
message: discordMessage
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
toast.add({
|
||||||
|
title: 'Message Sent!',
|
||||||
|
description: 'Your message has been sent successfully.',
|
||||||
|
icon: 'i-heroicons-check-circle',
|
||||||
|
color: 'success'
|
||||||
|
})
|
||||||
|
|
||||||
|
state.email = ''
|
||||||
|
state.subject = ''
|
||||||
|
state.message = ''
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
toast.add({
|
||||||
|
title: 'Error',
|
||||||
|
description: 'Something went wrong. Please try again.',
|
||||||
|
icon: 'i-heroicons-exclamation-circle',
|
||||||
|
color: 'error'
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
isSubmitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
62
pages/discord.vue
Normal file
62
pages/discord.vue
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col items-center justify-center min-h-screen px-4">
|
||||||
|
<!-- Title -->
|
||||||
|
<h1 class="text-2xl font-bold mb-8">Redirecting to Discord...</h1>
|
||||||
|
|
||||||
|
<!-- Progress bar -->
|
||||||
|
<div class="w-full max-w-md mb-12">
|
||||||
|
<UProgress v-model="progress" color="primary" size="lg" />
|
||||||
|
<p class="text-center mt-2 text-sm text-gray-500">
|
||||||
|
Redirecting in {{ Math.ceil(remainingTime) }} seconds...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UButton
|
||||||
|
size="xl"
|
||||||
|
:to="discordLink"
|
||||||
|
class="mt-4 text-lg px-8 py-4"
|
||||||
|
icon="i-simple-icons-discord"
|
||||||
|
>
|
||||||
|
Don't wait - Join Now
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
useHead({
|
||||||
|
title: 'Discord'
|
||||||
|
})
|
||||||
|
|
||||||
|
const { config } = useSiteConfig()
|
||||||
|
|
||||||
|
const discordLink = ref(config.siteLinks['discord-invite'] || 'https://discord.gg/e37aq2wc66')
|
||||||
|
|
||||||
|
// Progress state
|
||||||
|
const progress = ref(0)
|
||||||
|
const totalTime = 3 // seconds
|
||||||
|
const remainingTime = ref(totalTime)
|
||||||
|
const interval = 50 // ms
|
||||||
|
|
||||||
|
let timer
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
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)))
|
||||||
|
|
||||||
|
if (elapsed >= totalTime) {
|
||||||
|
clearInterval(timer)
|
||||||
|
window.location.href = discordLink.value
|
||||||
|
}
|
||||||
|
}, interval)
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (timer) clearInterval(timer)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
142
pages/games.vue
Normal file
142
pages/games.vue
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col items-center justify-center gap-8 pt-8">
|
||||||
|
<h1 class="font-bold text-5xl text-[--ui-primary] mt-10">
|
||||||
|
Games
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 max-w-6xl">
|
||||||
|
<UCard
|
||||||
|
v-for="game in games"
|
||||||
|
:key="game.title"
|
||||||
|
class="bg-gray-900 text-white shadow-lg"
|
||||||
|
:ui="{
|
||||||
|
root: 'bg-gray-900 text-white flex flex-col h-full shadow-lg rounded-lg ring-1 ring-gray-700',
|
||||||
|
header: 'p-4 pb-0',
|
||||||
|
body: 'flex-grow p-4 pt-2',
|
||||||
|
footer: 'p-4 pt-0'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<!-- Game Logo and Status -->
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<img
|
||||||
|
:src="getImageSrc(game)"
|
||||||
|
alt="Game Logo"
|
||||||
|
class="w-12 h-12 object-contain"
|
||||||
|
@error="handleImageError"
|
||||||
|
/>
|
||||||
|
<UBadge
|
||||||
|
:color="getBadgeColor(game.status)"
|
||||||
|
variant="soft"
|
||||||
|
size="sm"
|
||||||
|
class="font-semibold"
|
||||||
|
>
|
||||||
|
{{ game.status }}
|
||||||
|
</UBadge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Game Title -->
|
||||||
|
<h2 class="text-xl font-bold mt-4">{{ game.title }}</h2>
|
||||||
|
|
||||||
|
<!-- Game Description -->
|
||||||
|
<p class="text-gray-400 mt-2">{{ game.description }}</p>
|
||||||
|
|
||||||
|
<!-- Separator -->
|
||||||
|
<USeparator class="my-3" />
|
||||||
|
|
||||||
|
<!-- Game Links -->
|
||||||
|
<div class="flex gap-4 mt-2">
|
||||||
|
<a
|
||||||
|
v-for="link in game.url || []"
|
||||||
|
:key="link.url"
|
||||||
|
:href="link.url"
|
||||||
|
target="_blank"
|
||||||
|
class="text-blue-400 hover:text-blue-300 flex items-center gap-2 transition-colors"
|
||||||
|
>
|
||||||
|
<UIcon :name="link.logo" class="text-lg" />
|
||||||
|
<span class="text-sm">{{ link.host }}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import siteConfig from '~/public/site-config.json'
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
|
||||||
|
const games = siteConfig.games
|
||||||
|
const imageCache = ref(new Map())
|
||||||
|
|
||||||
|
function getBadgeColor(status: string) {
|
||||||
|
if (status === 'Released') return 'success'
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not checked yet, check it now
|
||||||
|
if (!imageCache.value.has(url)) {
|
||||||
|
checkImage(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()
|
||||||
|
img.onload = () => {
|
||||||
|
imageCache.value.set(url, true)
|
||||||
|
}
|
||||||
|
img.onerror = () => {
|
||||||
|
imageCache.value.set(url, false)
|
||||||
|
// Force a component update for this 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
img.src = url
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleImageError(event: Event) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-check all images when component is mounted
|
||||||
|
onMounted(() => {
|
||||||
|
games.forEach(game => {
|
||||||
|
if (game['logo-url']) {
|
||||||
|
checkImage(game['logo-url'])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
useHead({
|
||||||
|
title: 'Games'
|
||||||
|
})
|
||||||
|
</script>
|
||||||
101
pages/index.vue
101
pages/index.vue
|
|
@ -0,0 +1,101 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col items-center justify-center gap-8 pt-8">
|
||||||
|
<h1 class="font-bold text-5xl text-(--ui-primary) mt-10">
|
||||||
|
MarcUs7i.Net
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 mt-6">
|
||||||
|
<UButtonGroup>
|
||||||
|
<UButton
|
||||||
|
label="Discord"
|
||||||
|
icon="i-simple-icons-discord"
|
||||||
|
to="/discord"
|
||||||
|
target="_blank"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UButton
|
||||||
|
v-if="config.siteLinks.github"
|
||||||
|
label="GitHub"
|
||||||
|
color="neutral"
|
||||||
|
variant="outline"
|
||||||
|
icon="i-simple-icons-github"
|
||||||
|
:to="config.siteLinks.github"
|
||||||
|
target="_blank"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UButton
|
||||||
|
v-if="config.siteLinks['status-page']"
|
||||||
|
label="Status"
|
||||||
|
color="neutral"
|
||||||
|
icon="i-simple-icons-statuspage"
|
||||||
|
:to="config.siteLinks['status-page']"
|
||||||
|
target="_blank"
|
||||||
|
/>
|
||||||
|
</UButtonGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 2x2 Grid of Smaller Buttons -->
|
||||||
|
<div class="grid grid-cols-2 gap-4 pt-16 max-w-3xl">
|
||||||
|
<UButton
|
||||||
|
v-for="button in config.buttons"
|
||||||
|
:key="button.url"
|
||||||
|
class="big-button"
|
||||||
|
:to="button.url"
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<UIcon :name="button.icon" class="w-5 h-5 text-green-500" />
|
||||||
|
<span class="label">{{ button.title }}</span>
|
||||||
|
</div>
|
||||||
|
<span v-if="button.description" class="description">
|
||||||
|
{{ button.description }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
useHead({
|
||||||
|
title: 'Marcus7i.Net'
|
||||||
|
})
|
||||||
|
|
||||||
|
const { config } = useSiteConfig()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.big-button {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: flex-start;
|
||||||
|
padding: 1.5rem; /* p-6 */
|
||||||
|
min-height: 3.5rem; /* h-48 */
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
border-radius: 0.5rem; /* rounded-lg */
|
||||||
|
border: 1px solid rgb(55, 65, 81); /* border border-gray-700 */
|
||||||
|
background-color: rgb(31, 41, 55); /* bg-gray-800 */
|
||||||
|
color: white;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.big-button:hover {
|
||||||
|
border-color: rgb(34, 197, 94); /* border-green-500 */
|
||||||
|
background-color: rgb(17, 24, 39); /* bg-gray-900 */
|
||||||
|
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); /* shadow-lg */
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-size: 1.125rem; /* text-lg */
|
||||||
|
font-weight: 600; /* font-semibold */
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 0.875rem; /* text-sm */
|
||||||
|
color: rgb(156, 163, 175); /* text-gray-400 */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
24
pages/maintenance.vue
Normal file
24
pages/maintenance.vue
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col items-center justify-center gap-30 pt-20">
|
||||||
|
<h1 class="font-bold text-5xl text-(--ui-primary) mt-10">
|
||||||
|
This site is in maintenance!
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 mt-16">
|
||||||
|
<UButton
|
||||||
|
label="Return Home"
|
||||||
|
variant="outline"
|
||||||
|
color="neutral"
|
||||||
|
to="/"
|
||||||
|
size="xl"
|
||||||
|
class="px-8 text-lg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
useHead({
|
||||||
|
title: 'Maintenance'
|
||||||
|
})
|
||||||
|
</script>
|
||||||
1
public/cancel.svg
Normal file
1
public/cancel.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Line Icons by Vjacheslav Trushkin - https://github.com/cyberalien/line-md/blob/master/license.txt --><g fill="none" stroke="#ffffff" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path stroke-dasharray="64" stroke-dashoffset="64" d="M5.64 5.64c3.51 -3.51 9.21 -3.51 12.73 0c3.51 3.51 3.51 9.21 0 12.73c-3.51 3.51 -9.21 3.51 -12.73 0c-3.51 -3.51 -3.51 -9.21 -0 -12.73Z"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.6s" values="64;0"/></path><path stroke-dasharray="20" stroke-dashoffset="20" d="M6 6l12 12"><animate fill="freeze" attributeName="stroke-dashoffset" begin="0.6s" dur="0.2s" values="20;0"/></path></g></svg>
|
||||||
|
After Width: | Height: | Size: 763 B |
BIN
public/game-icons/shantimanti.png
Normal file
BIN
public/game-icons/shantimanti.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
BIN
public/game-icons/synthmaze.png
Normal file
BIN
public/game-icons/synthmaze.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 56 KiB |
91
public/site-config.json
Normal file
91
public/site-config.json
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
{
|
||||||
|
"site-links":
|
||||||
|
{
|
||||||
|
"discord-invite": "https://discord.gg/e37aq2wc66",
|
||||||
|
"github": "https://github.com/MarcUs7i",
|
||||||
|
"status-page": "https://status.marcus7i.net"
|
||||||
|
},
|
||||||
|
"buttons": [
|
||||||
|
{
|
||||||
|
"title": "Open-WebUI",
|
||||||
|
"url": "https://ollama.marcus7i.net",
|
||||||
|
"icon": "i-simple-icons-ollama",
|
||||||
|
"description": "Self-hosted WebUI for LLMs using Ollama"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "uLinkShortener",
|
||||||
|
"url": "https://u.marcus7i.net",
|
||||||
|
"icon": "line-md:link",
|
||||||
|
"description": "URL shortener and data collector"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Git",
|
||||||
|
"url": "https://git.marcus7i.net",
|
||||||
|
"icon": "i-simple-icons-git",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Games",
|
||||||
|
"url": "/games",
|
||||||
|
"icon": "line-md:play-filled",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Anywave",
|
||||||
|
"url": "/maintenance",
|
||||||
|
"icon": "i-simple-icons-stremio",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "SauceKudasai",
|
||||||
|
"url": "https://saucekudasai.marcus7i.net",
|
||||||
|
"icon": "i-simple-icons-sunrise",
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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/MarcUs7i/ShantiManti"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"host": "Itch",
|
||||||
|
"logo": "i-simple-icons-itchdotio",
|
||||||
|
"url": "https://marcus7i.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/MarcUs7i/SynthMaze"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"host": "Itch",
|
||||||
|
"logo": "i-simple-icons-itchdotio",
|
||||||
|
"url": "https://marcus7i.itch.io/synthmaze"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "TetrisRemastered",
|
||||||
|
"description": "Tetris in 2D & 3D with customizable music engine",
|
||||||
|
"status": "In development",
|
||||||
|
"logo-url": "/game-icons/tetrisremastered.png",
|
||||||
|
"leaderboard-api": "http://localhost:3001/leaderboard"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
35
server/api/send-contact.post.js
Normal file
35
server/api/send-contact.post.js
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
try {
|
||||||
|
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')
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(webhookUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(message)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text()
|
||||||
|
throw new Error(`Discord API error: ${response.status} - ${errorText}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true }
|
||||||
|
} catch (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'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
18
tailwind.config.js
Normal file
18
tailwind.config.js
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: [
|
||||||
|
"./components/**/*.{js,vue,ts}",
|
||||||
|
"./layouts/**/*.vue",
|
||||||
|
"./pages/**/*.vue",
|
||||||
|
"./plugins/**/*.{js,ts}",
|
||||||
|
"./app.vue",
|
||||||
|
"./error.vue",
|
||||||
|
"./content/**/*.md",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
require('@tailwindcss/typography'),
|
||||||
|
],
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue