style: format with new style

This commit is contained in:
taskylizard 2024-01-25 16:32:45 +00:00
parent bc3bbaafeb
commit ef422dcda8
No known key found for this signature in database
GPG Key ID: 1820131ED1A24120
43 changed files with 686 additions and 583 deletions

View File

@ -20,7 +20,7 @@ jobs:
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
node-version: 18 node-version: 18
cache: "pnpm" cache: 'pnpm'
- run: pnpm install --no-frozen-lockfile - run: pnpm install --no-frozen-lockfile

View File

@ -33,7 +33,7 @@ jobs:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v4 uses: actions/setup-python@v4
with: with:
python-version: "3.10" python-version: '3.10'
- name: Install dependencies - name: Install dependencies
run: pnpm install run: pnpm install
- name: Preprocess files - name: Preprocess files

View File

@ -19,10 +19,10 @@ jobs:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v4 uses: actions/setup-python@v4
with: with:
python-version: "3.10" python-version: '3.10'
- name: Create local changes - name: Create local changes
run: python .github/single-page.py run: python .github/single-page.py
- uses: stefanzweifel/git-auto-commit-action@v4 - uses: stefanzweifel/git-auto-commit-action@v4
with: with:
commit_message: "♻️ update single page" commit_message: '♻️ update single page'

View File

@ -1,14 +0,0 @@
{
"semi": true,
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"singleQuote": false,
"quoteProps": "as-needed",
"jsxSingleQuote": false,
"trailingComma": "all",
"bracketSpacing": true,
"bracketSameLine": true,
"arrowParens": "always",
"proseWrap": "always"
}

6
.prettierrc.yaml Normal file
View File

@ -0,0 +1,6 @@
proseWrap: always
semi: false
singleQuote: true
printWidth: 80
trailingComma: none
htmlWhitespaceSensitivity: ignore

View File

@ -1,93 +1,108 @@
import { defineConfig } from "vitepress"; import { defineConfig } from 'vitepress'
import UnoCSS from "unocss/vite"; import UnoCSS from 'unocss/vite'
import consola from "consola"; import consola from 'consola'
import { commitRef, feedback, meta, search, sidebar, socialLinks } from "./constants"; import {
import { generateImages, generateMeta, generateFeed } from "./hooks"; commitRef,
import { toggleStarredPlugin } from "./markdown/toggleStarred"; feedback,
import { base64DecodePlugin } from "./markdown/base64"; meta,
import { movePlugin, emojiRender, defs } from "./markdown/emoji"; search,
sidebar,
socialLinks
} from './constants'
import { generateImages, generateMeta, generateFeed } from './hooks'
import { toggleStarredPlugin } from './markdown/toggleStarred'
import { base64DecodePlugin } from './markdown/base64'
import { movePlugin, emojiRender, defs } from './markdown/emoji'
const baseUrl = process.env.GITHUB_ACTIONS ? "/FMHYedit" : "/"; const baseUrl = process.env.GITHUB_ACTIONS ? '/FMHYedit' : '/'
export default defineConfig({ export default defineConfig({
title: "FMHY", title: 'FMHY',
description: meta.description, description: meta.description,
titleTemplate: ":title • freemediaheckyeah", titleTemplate: ':title • freemediaheckyeah',
lang: "en-US", lang: 'en-US',
lastUpdated: false, lastUpdated: false,
cleanUrls: true, cleanUrls: true,
appearance: "dark", appearance: 'dark',
base: baseUrl, base: baseUrl,
srcExclude: ["readme.md", "single-page"], srcExclude: ['readme.md', 'single-page'],
ignoreDeadLinks: true, ignoreDeadLinks: true,
sitemap: { sitemap: {
hostname: meta.hostname, hostname: meta.hostname
}, },
head: [ head: [
["meta", { name: "theme-color", content: "#7bc5e4" }], ['meta', { name: 'theme-color', content: '#7bc5e4' }],
["meta", { name: "og:type", content: "website" }], ['meta', { name: 'og:type', content: 'website' }],
["meta", { name: "og:locale", content: "en" }], ['meta', { name: 'og:locale', content: 'en' }],
["link", { rel: "icon", href: "/test.png" }], ['link', { rel: 'icon', href: '/test.png' }],
// PWA // PWA
["link", { rel: "icon", href: "/test.png", type: "image/svg+xml" }], ['link', { rel: 'icon', href: '/test.png', type: 'image/svg+xml' }],
["link", { rel: "alternate icon", href: "/test.png" }], ['link', { rel: 'alternate icon', href: '/test.png' }],
["link", { rel: "mask-icon", href: "/test.png", color: "#7bc5e4" }], ['link', { rel: 'mask-icon', href: '/test.png', color: '#7bc5e4' }],
// prettier-ignore // prettier-ignore
["meta", { name: "keywords", content: meta.keywords.join(" ") }], ["meta", { name: "keywords", content: meta.keywords.join(" ") }],
["link", { rel: "apple-touch-icon", href: "/test.png", sizes: "192x192" }], ['link', { rel: 'apple-touch-icon', href: '/test.png', sizes: '192x192' }]
], ],
transformHead: async (context) => generateMeta(context, meta.hostname), transformHead: async (context) => generateMeta(context, meta.hostname),
buildEnd: async (context) => { buildEnd: async (context) => {
generateImages(context) generateImages(context)
.then(() => generateFeed(context)) .then(() => generateFeed(context))
.finally(() => consola.success("Success!")); .finally(() => consola.success('Success!'))
}, },
vite: { vite: {
optimizeDeps: { exclude: ["workbox-window"] }, optimizeDeps: { exclude: ['workbox-window'] },
plugins: [ plugins: [
UnoCSS({ UnoCSS({
configFile: "../unocss.config.ts", configFile: '../unocss.config.ts'
}), }),
{ {
name: "custom:adjust-order", name: 'custom:adjust-order',
configResolved(c) { configResolved(c) {
movePlugin(c.plugins as any, "vitepress", "before", "unocss:transformers:pre"); movePlugin(
}, c.plugins as any,
}, 'vitepress',
'before',
'unocss:transformers:pre'
)
}
}
], ],
build: { build: {
// Shut the fuck up // Shut the fuck up
chunkSizeWarningLimit: Number.POSITIVE_INFINITY, chunkSizeWarningLimit: Number.POSITIVE_INFINITY
}, }
}, },
markdown: { markdown: {
emoji: { defs }, emoji: { defs },
config(md) { config(md) {
md.use(emojiRender); md.use(emojiRender)
md.use(toggleStarredPlugin); md.use(toggleStarredPlugin)
md.use(base64DecodePlugin); md.use(base64DecodePlugin)
}, }
}, },
themeConfig: { themeConfig: {
search, search,
footer: { footer: {
message: `${feedback} (rev: ${commitRef})`, message: `${feedback} (rev: ${commitRef})`
}, },
outline: "deep", outline: 'deep',
logo: "/fmhy.ico", logo: '/fmhy.ico',
nav: [ nav: [
{ text: "Beginners Guide", link: "/beginners-guide" }, { text: 'Beginners Guide', link: '/beginners-guide' },
{ text: "Glossary", link: "https://rentry.org/The-Piracy-Glossary" }, { text: 'Glossary', link: 'https://rentry.org/The-Piracy-Glossary' },
{ text: "Guides", link: "https://rentry.co/fmhy-guides" }, { text: 'Guides', link: 'https://rentry.co/fmhy-guides' },
{ text: "Backups", link: "https://www.reddit.com/r/FREEMEDIAHECKYEAH/wiki/backups" },
{ {
text: "About", text: 'Backups',
items: [ link: 'https://www.reddit.com/r/FREEMEDIAHECKYEAH/wiki/backups'
{ text: "Posts", link: "/posts" },
{ text: "Feedback", link: "/feedback" },
],
}, },
{
text: 'About',
items: [
{ text: 'Posts', link: '/posts' },
{ text: 'Feedback', link: '/feedback' }
]
}
], ],
sidebar, sidebar,
socialLinks, socialLinks
}, }
}); })

View File

@ -1,92 +1,101 @@
import type { DefaultTheme } from "vitepress"; import type { DefaultTheme } from 'vitepress'
export const meta = { export const meta = {
name: "FreeMediaHeckYeah", name: 'FreeMediaHeckYeah',
description: "The largest collection of free stuff on the internet!", description: 'The largest collection of free stuff on the internet!',
hostname: "https://fmhy.net", hostname: 'https://fmhy.net',
keywords: ["stream", "movies", "gaming", "reading", "anime"], keywords: ['stream', 'movies', 'gaming', 'reading', 'anime']
}; }
export const commitRef = process.env.CF_PAGES export const commitRef = process.env.CF_PAGES
? `<a href="https://github.com/fmhy/FMHYEdit/commit/${ ? `<a href="https://github.com/fmhy/FMHYEdit/commit/${
process.env.CF_PAGES_COMMIT_SHA process.env.CF_PAGES_COMMIT_SHA
}">${process.env.CF_PAGES_COMMIT_SHA.slice(0, 8)}</a>` }">${process.env.CF_PAGES_COMMIT_SHA.slice(0, 8)}</a>`
: "dev"; : 'dev'
export const feedback = `<a href="/feedback" class="feedback-footer">Made with ❤️</a>`; export const feedback = `<a href="/feedback" class="feedback-footer">Made with ❤️</a>`
export const search: DefaultTheme.Config["search"] = { export const search: DefaultTheme.Config['search'] = {
options: { options: {
miniSearch: { miniSearch: {
searchOptions: { searchOptions: {
combineWith: "AND", combineWith: 'AND',
fuzzy: false, fuzzy: false,
// @ts-ignore // @ts-ignore
boostDocument: (_, term, storedFields: Record<string, string | string[]>) => { boostDocument: (
_,
term,
storedFields: Record<string, string | string[]>
) => {
const titles = (storedFields?.titles as string[]) const titles = (storedFields?.titles as string[])
.filter((t) => Boolean(t)) .filter((t) => Boolean(t))
.map((t) => t.toLowerCase()); .map((t) => t.toLowerCase())
// Uprate if term appears in titles. Add bonus for higher levels (i.e. lower index) // Uprate if term appears in titles. Add bonus for higher levels (i.e. lower index)
const titleIndex = const titleIndex =
titles.map((t, i) => (t?.includes(term) ? i : -1)).find((i) => i >= 0) ?? -1; titles
if (titleIndex >= 0) return 10000 - titleIndex; .map((t, i) => (t?.includes(term) ? i : -1))
.find((i) => i >= 0) ?? -1
if (titleIndex >= 0) return 10000 - titleIndex
return 1; return 1
}, }
}, }
}, },
detailedView: true, detailedView: true
}, },
provider: "local", provider: 'local'
}; }
export const socialLinks: DefaultTheme.SocialLink[] = [ export const socialLinks: DefaultTheme.SocialLink[] = [
{ icon: "github", link: "https://github.com/fmhy/FMHYEdit" }, { icon: 'github', link: 'https://github.com/fmhy/FMHYEdit' },
{ icon: "discord", link: "https://discord.gg/Stz6y6NgNg" }, { icon: 'discord', link: 'https://discord.gg/Stz6y6NgNg' },
{ {
icon: "reddit", icon: 'reddit',
link: "https://reddit.com/r/FREEMEDIAHECKYEAH", link: 'https://reddit.com/r/FREEMEDIAHECKYEAH'
}, }
]; ]
export const sidebar: DefaultTheme.Sidebar = [ export const sidebar: DefaultTheme.Sidebar = [
{ text: "📛 Adblocking / Privacy", link: "/adblockvpnguide" }, { text: '📛 Adblocking / Privacy', link: '/adblockvpnguide' },
{ text: "🤖 Artificial Intelligence", link: "/ai" }, { text: '🤖 Artificial Intelligence', link: '/ai' },
{ text: "📺 Movies / TV / Anime", link: "/videopiracyguide" }, { text: '📺 Movies / TV / Anime', link: '/videopiracyguide' },
{ text: "🎵 Music / Podcasts / Radio", link: "/audiopiracyguide" }, { text: '🎵 Music / Podcasts / Radio', link: '/audiopiracyguide' },
{ text: "🎮 Gaming / Emulation", link: "/gamingpiracyguide" }, { text: '🎮 Gaming / Emulation', link: '/gamingpiracyguide' },
{ text: "📗 Books / Comics / Manga", link: "/readingpiracyguide" }, { text: '📗 Books / Comics / Manga', link: '/readingpiracyguide' },
{ text: "💾 Downloading", link: "/downloadpiracyguide" }, { text: '💾 Downloading', link: '/downloadpiracyguide' },
{ text: "🌀 Torrenting", link: "/torrentpiracyguide" }, { text: '🌀 Torrenting', link: '/torrentpiracyguide' },
{ text: "🧠 Educational", link: "/edupiracyguide" }, { text: '🧠 Educational', link: '/edupiracyguide' },
{ text: "📱 Android / iOS", link: "/android-iosguide" }, { text: '📱 Android / iOS', link: '/android-iosguide' },
{ text: "🐧 Linux / MacOS", link: "/linuxguide" }, { text: '🐧 Linux / MacOS', link: '/linuxguide' },
{ text: "🌍 Non-English", link: "/non-english" }, { text: '🌍 Non-English', link: '/non-english' },
{ text: "📂 Miscellaneous", link: "/miscguide" }, { text: '📂 Miscellaneous', link: '/miscguide' },
{ {
text: "🔧 Tools", text: '🔧 Tools',
collapsed: false, collapsed: false,
items: [ items: [
{ text: "💻 System Tools", link: "/system-tools" }, { text: '💻 System Tools', link: '/system-tools' },
{ text: "🗃️ File Tools", link: "/file-tools" }, { text: '🗃️ File Tools', link: '/file-tools' },
{ text: "🔗 Internet Tools", link: "/internet-tools" }, { text: '🔗 Internet Tools', link: '/internet-tools' },
{ text: "💬 Social Media Tools", link: "/social-media-tools" }, { text: '💬 Social Media Tools', link: '/social-media-tools' },
{ text: "📝 Text Tools", link: "/text-tools" }, { text: '📝 Text Tools', link: '/text-tools' },
{ text: "👾 Gaming Tools", link: "/gamingpiracyguide#gaming-tools" }, { text: '👾 Gaming Tools', link: '/gamingpiracyguide#gaming-tools' },
{ text: "📷 Image Tools", link: "/img-tools" }, { text: '📷 Image Tools', link: '/img-tools' },
{ text: "📼 Video Tools", link: "/video-tools" }, { text: '📼 Video Tools', link: '/video-tools' },
{ text: "🔊 Audio Tools", link: "/audiopiracyguide#audio-tools" }, { text: '🔊 Audio Tools', link: '/audiopiracyguide#audio-tools' },
{ text: "🍎 Educational Tools", link: "/edupiracyguide#educational-tools" }, {
{ text: "👨‍💻 Developer Tools", link: "/devtools" }, text: '🍎 Educational Tools',
], link: '/edupiracyguide#educational-tools'
},
{ text: '👨‍💻 Developer Tools', link: '/devtools' }
]
}, },
{ {
text: " More", text: ' More',
collapsed: true, collapsed: true,
items: [ items: [
{ text: "🔞 NSFW", link: "/nsfwpiracy" }, { text: '🔞 NSFW', link: '/nsfwpiracy' },
{ text: "⚠️ Unsafe Sites", link: "/unsafesites" }, { text: '⚠️ Unsafe Sites', link: '/unsafesites' },
{ text: "📦 Storage", link: "/storage" }, { text: '📦 Storage', link: '/storage' }
], ]
}, }
]; ]

View File

@ -1,15 +1,20 @@
<script setup lang="ts"> <script setup lang="ts">
defineProps<{ title: string; description?: string }>(); defineProps<{ title: string; description?: string }>()
</script> </script>
<template> <template>
<div <div
tw="w-full h-full bg-black flex flex-col" tw="w-full h-full bg-black flex flex-col"
style="background-image: url(https://fmhy.pages.dev/og.png)"> style="background-image: url(https://fmhy.pages.dev/og.png)"
<div tw="p-10 w-full min-h-0 grow flex flex-col items-center justify-between"> >
<div
tw="p-10 w-full min-h-0 grow flex flex-col items-center justify-between"
>
<div tw="w-full flex justify-between items-center text-5xl font-medium"> <div tw="w-full flex justify-between items-center text-5xl font-medium">
<div tw="flex items-center"> <div tw="flex items-center">
<div tw="text-zinc-100 ml-2 mt-1 font-semibold">freemediaheckyeah</div> <div tw="text-zinc-100 ml-2 mt-1 font-semibold">
freemediaheckyeah
</div>
</div> </div>
</div> </div>
<div tw="w-full pr-56 flex flex-col items-start justify-end"> <div tw="w-full pr-56 flex flex-col items-start justify-end">

View File

@ -2,7 +2,7 @@
* Barrel generated using @taskylizard/tasker. * Barrel generated using @taskylizard/tasker.
*/ */
export * from "./meta"; export * from './meta'
export * from "./opengraph"; export * from './opengraph'
export * from "./rss"; export * from './rss'
export * from "./satoriConfig"; export * from './satoriConfig'

View File

@ -1,89 +1,100 @@
import type { HeadConfig, TransformContext } from "vitepress"; import type { HeadConfig, TransformContext } from 'vitepress'
export function generateMeta(context: TransformContext, hostname: string) { export function generateMeta(context: TransformContext, hostname: string) {
const head: HeadConfig[] = []; const head: HeadConfig[] = []
const { pageData } = context; const { pageData } = context
const url = `${hostname}/${pageData.relativePath.replace(/((^|\/)index)?\.md$/, "$2")}`; const url = `${hostname}/${pageData.relativePath.replace(/((^|\/)index)?\.md$/, '$2')}`
head.push( head.push(
["link", { rel: "canonical", href: url }], ['link', { rel: 'canonical', href: url }],
["meta", { property: "og:url", content: url }], ['meta', { property: 'og:url', content: url }],
["meta", { name: "twitter:url", content: url }], ['meta', { name: 'twitter:url', content: url }],
["meta", { name: "twitter:card", content: "summary_large_image" }], ['meta', { name: 'twitter:card', content: 'summary_large_image' }],
["meta", { property: "og:title", content: pageData.frontmatter.title }], ['meta', { property: 'og:title', content: pageData.frontmatter.title }],
["meta", { name: "twitter:title", content: pageData.frontmatter.title }], ['meta', { name: 'twitter:title', content: pageData.frontmatter.title }]
); )
if (pageData.frontmatter.description) { if (pageData.frontmatter.description) {
head.push( head.push(
[ [
"meta", 'meta',
{ {
property: "og:description", property: 'og:description',
content: pageData.frontmatter.description, content: pageData.frontmatter.description
}, }
], ],
[ [
"meta", 'meta',
{ {
name: "twitter:description", name: 'twitter:description',
content: pageData.frontmatter.description, content: pageData.frontmatter.description
}, }
], ]
); )
} }
if (pageData.frontmatter.image) { if (pageData.frontmatter.image) {
head.push([ head.push([
"meta", 'meta',
{ {
property: "og:image", property: 'og:image',
content: `${hostname}/${pageData.frontmatter.image.replace(/^\//, "")}`, content: `${hostname}/${pageData.frontmatter.image.replace(/^\//, '')}`
}, }
]); ])
head.push([ head.push([
"meta", 'meta',
{ {
name: "twitter:image", name: 'twitter:image',
content: `${hostname}/${pageData.frontmatter.image.replace(/^\//, "")}`, content: `${hostname}/${pageData.frontmatter.image.replace(/^\//, '')}`
}, }
]); ])
} else { } else {
const url = pageData.filePath.replace("index.md", "").replace(".md", ""); const url = pageData.filePath.replace('index.md', '').replace('.md', '')
const imageUrl = `${url}/__og_image__/og.png`.replaceAll("//", "/").replace(/^\//, ""); const imageUrl = `${url}/__og_image__/og.png`
.replaceAll('//', '/')
.replace(/^\//, '')
head.push( head.push(
["meta", { property: "og:image", content: `${hostname}/${imageUrl}` }], ['meta', { property: 'og:image', content: `${hostname}/${imageUrl}` }],
["meta", { property: "og:image:width", content: "1200" }], ['meta', { property: 'og:image:width', content: '1200' }],
["meta", { property: "og:image:height", content: "628" }], ['meta', { property: 'og:image:height', content: '628' }],
["meta", { property: "og:image:type", content: "image/png" }], ['meta', { property: 'og:image:type', content: 'image/png' }],
["meta", { property: "og:image:alt", content: pageData.frontmatter.title }], [
["meta", { name: "twitter:image", content: `${hostname}/${imageUrl}` }], 'meta',
["meta", { name: "twitter:image:width", content: "1200" }], { property: 'og:image:alt', content: pageData.frontmatter.title }
["meta", { name: "twitter:image:height", content: "628" }], ],
["meta", { name: "twitter:image:alt", content: pageData.frontmatter.title }], ['meta', { name: 'twitter:image', content: `${hostname}/${imageUrl}` }],
); ['meta', { name: 'twitter:image:width', content: '1200' }],
['meta', { name: 'twitter:image:height', content: '628' }],
[
'meta',
{ name: 'twitter:image:alt', content: pageData.frontmatter.title }
]
)
} }
if (pageData.frontmatter.tag) { if (pageData.frontmatter.tag) {
head.push(["meta", { property: "article:tag", content: pageData.frontmatter.tag }]); head.push([
'meta',
{ property: 'article:tag', content: pageData.frontmatter.tag }
])
} }
if (pageData.frontmatter.date) { if (pageData.frontmatter.date) {
head.push([ head.push([
"meta", 'meta',
{ {
property: "article:published_time", property: 'article:published_time',
content: pageData.frontmatter.date, content: pageData.frontmatter.date
}, }
]); ])
} }
if (pageData.lastUpdated && pageData.frontmatter.lastUpdated !== false) { if (pageData.lastUpdated && pageData.frontmatter.lastUpdated !== false) {
head.push([ head.push([
"meta", 'meta',
{ {
property: "article:modified_time", property: 'article:modified_time',
content: new Date(pageData.lastUpdated).toISOString(), content: new Date(pageData.lastUpdated).toISOString()
}, }
]); ])
} }
return head; return head
} }

View File

@ -1,71 +1,71 @@
import { mkdir, readFile, writeFile } from "node:fs/promises"; import { mkdir, readFile, writeFile } from 'node:fs/promises'
import { dirname, resolve } from "node:path"; import { dirname, resolve } from 'node:path'
import { fileURLToPath } from "node:url"; import { fileURLToPath } from 'node:url'
import { createContentLoader } from "vitepress"; import { createContentLoader } from 'vitepress'
import type { ContentData, SiteConfig } from "vitepress"; import type { ContentData, SiteConfig } from 'vitepress'
import { type SatoriOptions, satoriVue } from "x-satori/vue"; import { type SatoriOptions, satoriVue } from 'x-satori/vue'
import { renderAsync } from "@resvg/resvg-js"; import { renderAsync } from '@resvg/resvg-js'
import consola from "consola"; import consola from 'consola'
const __dirname = dirname(fileURLToPath(import.meta.url)); const __dirname = dirname(fileURLToPath(import.meta.url))
const __fonts = resolve(__dirname, "../fonts"); const __fonts = resolve(__dirname, '../fonts')
export async function generateImages(config: SiteConfig): Promise<void> { export async function generateImages(config: SiteConfig): Promise<void> {
const pages = await createContentLoader("**/*.md", { excerpt: true }).load(); const pages = await createContentLoader('**/*.md', { excerpt: true }).load()
const template = await readFile(resolve(__dirname, "./Template.vue"), "utf-8"); const template = await readFile(resolve(__dirname, './Template.vue'), 'utf-8')
const fonts: SatoriOptions["fonts"] = [ const fonts: SatoriOptions['fonts'] = [
{ {
name: "Inter", name: 'Inter',
data: await readFile(resolve(__fonts, "Inter-Regular.otf")), data: await readFile(resolve(__fonts, 'Inter-Regular.otf')),
weight: 400, weight: 400,
style: "normal", style: 'normal'
}, },
{ {
name: "Inter", name: 'Inter',
data: await readFile(resolve(__fonts, "Inter-Medium.otf")), data: await readFile(resolve(__fonts, 'Inter-Medium.otf')),
weight: 500, weight: 500,
style: "normal", style: 'normal'
}, },
{ {
name: "Inter", name: 'Inter',
data: await readFile(resolve(__fonts, "Inter-SemiBold.otf")), data: await readFile(resolve(__fonts, 'Inter-SemiBold.otf')),
weight: 600, weight: 600,
style: "normal", style: 'normal'
}, },
{ {
name: "Inter", name: 'Inter',
data: await readFile(resolve(__fonts, "Inter-Bold.otf")), data: await readFile(resolve(__fonts, 'Inter-Bold.otf')),
weight: 700, weight: 700,
style: "normal", style: 'normal'
}, }
]; ]
for (const page of pages) { for (const page of pages) {
await generateImage({ await generateImage({
page, page,
template, template,
outDir: config.outDir, outDir: config.outDir,
fonts, fonts
}); })
} }
return consola.info("Generated opengraph images."); return consola.info('Generated opengraph images.')
} }
interface GenerateImagesOptions { interface GenerateImagesOptions {
page: ContentData; page: ContentData
template: string; template: string
outDir: string; outDir: string
fonts: SatoriOptions["fonts"]; fonts: SatoriOptions['fonts']
} }
async function generateImage({ async function generateImage({
page, page,
template, template,
outDir, outDir,
fonts, fonts
}: GenerateImagesOptions): Promise<void> { }: GenerateImagesOptions): Promise<void> {
const { frontmatter, url } = page; const { frontmatter, url } = page
const options: SatoriOptions = { const options: SatoriOptions = {
width: 1200, width: 1200,
@ -73,24 +73,24 @@ async function generateImage({
fonts, fonts,
props: { props: {
title: title:
frontmatter.layout === "home" frontmatter.layout === 'home'
? frontmatter.hero.name ?? frontmatter.title ? frontmatter.hero.name ?? frontmatter.title
: frontmatter.title, : frontmatter.title,
description: description:
frontmatter.layout === "home" frontmatter.layout === 'home'
? frontmatter.hero.tagline ?? frontmatter.description ? frontmatter.hero.tagline ?? frontmatter.description
: frontmatter.description, : frontmatter.description
}, }
}; }
const svg = await satoriVue(options, template); const svg = await satoriVue(options, template)
const render = await renderAsync(svg); const render = await renderAsync(svg)
const outputFolder = resolve(outDir, url.slice(1), "__og_image__"); const outputFolder = resolve(outDir, url.slice(1), '__og_image__')
const outputFile = resolve(outputFolder, "og.png"); const outputFile = resolve(outputFolder, 'og.png')
await mkdir(outputFolder, { recursive: true }); await mkdir(outputFolder, { recursive: true })
await writeFile(outputFile, render.asPng()); await writeFile(outputFile, render.asPng())
} }

View File

@ -1,9 +1,13 @@
import path from "node:path"; import path from 'node:path'
import { writeFileSync } from "node:fs"; import { writeFileSync } from 'node:fs'
import { Feed } from "feed"; import { Feed } from 'feed'
import { createContentLoader, type ContentData, type SiteConfig } from "vitepress"; import {
import consola from "consola"; createContentLoader,
import { meta } from "../constants"; type ContentData,
type SiteConfig
} from 'vitepress'
import consola from 'consola'
import { meta } from '../constants'
export async function generateFeed(config: SiteConfig): Promise<void> { export async function generateFeed(config: SiteConfig): Promise<void> {
const feed: Feed = new Feed({ const feed: Feed = new Feed({
@ -11,32 +15,35 @@ export async function generateFeed(config: SiteConfig): Promise<void> {
link: meta.hostname, link: meta.hostname,
title: `FMHY blog`, title: `FMHY blog`,
description: meta.description, description: meta.description,
language: "en-US", language: 'en-US',
image: "https://github.com/fmhy.png", image: 'https://github.com/fmhy.png',
favicon: `${meta.hostname}/favicon.ico`, favicon: `${meta.hostname}/favicon.ico`,
copyright: `Copyright (c) 2023-present FMHY`, copyright: `Copyright (c) 2023-present FMHY`
}); })
const posts: ContentData[] = await createContentLoader("posts/*.md", { const posts: ContentData[] = await createContentLoader('posts/*.md', {
excerpt: true, excerpt: true,
render: true, render: true,
transform: (rawData) => { transform: (rawData) => {
return rawData.sort((a, b) => { return rawData.sort((a, b) => {
return Number(new Date(b.frontmatter.date)) - Number(new Date(a.frontmatter.date)); return (
}); Number(new Date(b.frontmatter.date)) -
}, Number(new Date(a.frontmatter.date))
}).load(); )
})
}
}).load()
for (const { url, frontmatter, html } of posts) { for (const { url, frontmatter, html } of posts) {
feed.addItem({ feed.addItem({
title: frontmatter.title as string, title: frontmatter.title as string,
id: `${meta.hostname}${url.replace(/\/\d+\./, "/")}`, id: `${meta.hostname}${url.replace(/\/\d+\./, '/')}`,
link: `${meta.hostname}${url.replace(/\/\d+\./, "/")}`, link: `${meta.hostname}${url.replace(/\/\d+\./, '/')}`,
date: frontmatter.date, date: frontmatter.date,
content: html!, content: html!
}); })
} }
writeFileSync(path.join(config.outDir, "feed.rss"), feed.rss2()); writeFileSync(path.join(config.outDir, 'feed.rss'), feed.rss2())
return consola.info("Generated rss feed."); return consola.info('Generated rss feed.')
} }

View File

@ -1,47 +1,47 @@
import { readFile } from "node:fs/promises"; import { readFile } from 'node:fs/promises'
import { dirname, resolve } from "node:path"; import { dirname, resolve } from 'node:path'
import { fileURLToPath } from "node:url"; import { fileURLToPath } from 'node:url'
import type { SatoriOptions } from "x-satori/vue"; import type { SatoriOptions } from 'x-satori/vue'
import { defineSatoriConfig } from "x-satori/vue"; import { defineSatoriConfig } from 'x-satori/vue'
const __dirname = dirname(fileURLToPath(import.meta.url)); const __dirname = dirname(fileURLToPath(import.meta.url))
const __fonts = resolve(__dirname, "../fonts"); const __fonts = resolve(__dirname, '../fonts')
const fonts: SatoriOptions["fonts"] = [ const fonts: SatoriOptions['fonts'] = [
{ {
name: "Inter", name: 'Inter',
data: await readFile(resolve(__fonts, "Inter-Regular.otf")), data: await readFile(resolve(__fonts, 'Inter-Regular.otf')),
weight: 400, weight: 400,
style: "normal", style: 'normal'
}, },
{ {
name: "Inter", name: 'Inter',
data: await readFile(resolve(__fonts, "Inter-Medium.otf")), data: await readFile(resolve(__fonts, 'Inter-Medium.otf')),
weight: 500, weight: 500,
style: "normal", style: 'normal'
}, },
{ {
name: "Inter", name: 'Inter',
data: await readFile(resolve(__fonts, "Inter-SemiBold.otf")), data: await readFile(resolve(__fonts, 'Inter-SemiBold.otf')),
weight: 600, weight: 600,
style: "normal", style: 'normal'
}, },
{ {
name: "Inter", name: 'Inter',
data: await readFile(resolve(__fonts, "Inter-Bold.otf")), data: await readFile(resolve(__fonts, 'Inter-Bold.otf')),
weight: 700, weight: 700,
style: "normal", style: 'normal'
}, }
]; ]
export default defineSatoriConfig({ export default defineSatoriConfig({
width: 1200, width: 1200,
height: 628, height: 628,
fonts, fonts,
props: { props: {
title: "Title", title: 'Title',
description: description:
"Lorem ipsum dolor sit amet, qui minim labore adipisicing minim sint cillum sint consectetur cupidatat.", 'Lorem ipsum dolor sit amet, qui minim labore adipisicing minim sint cillum sint consectetur cupidatat.',
dir: "/j", dir: '/j'
}, }
}); })

View File

@ -1,25 +1,29 @@
import { type MarkdownRenderer } from "vitepress"; import { type MarkdownRenderer } from 'vitepress'
// FIXME: tasky: possibly write less horror jank? // FIXME: tasky: possibly write less horror jank?
export function base64DecodePlugin(md: MarkdownRenderer) { export function base64DecodePlugin(md: MarkdownRenderer) {
const decode = (str: string): string => Buffer.from(str, "base64").toString("binary"); const decode = (str: string): string =>
Buffer.from(str, 'base64').toString('binary')
// Save the original rule for backticks // Save the original rule for backticks
const defaultRender = const defaultRender =
md.renderer.rules.code_inline || md.renderer.rules.code_inline ||
function (tokens, idx, options, env, self) { function (tokens, idx, options, env, self) {
return self.renderToken(tokens, idx, options); return self.renderToken(tokens, idx, options)
}; }
md.renderer.rules.code_inline = function (tokens, idx, options, env, self) { md.renderer.rules.code_inline = function (tokens, idx, options, env, self) {
// @ts-expect-error shut the fuck up already I HATE THIS // @ts-expect-error shut the fuck up already I HATE THIS
if (!env.frontmatter.title || (env.frontmatter.title && !env.frontmatter.title === "base64")) { if (
return defaultRender(tokens, idx, options, env, self); !env.frontmatter.title ||
(env.frontmatter.title && !env.frontmatter.title === 'base64')
) {
return defaultRender(tokens, idx, options, env, self)
} }
const token = tokens[idx]; const token = tokens[idx]
const content = token.content; const content = token.content
return `<button class='base64' onclick="(function(btn){ const codeEl = btn.querySelector('code'); navigator.clipboard.writeText('${decode( return `<button class='base64' onclick="(function(btn){ const codeEl = btn.querySelector('code'); navigator.clipboard.writeText('${decode(
content, content
)}').then(() => { const originalText = codeEl.textContent; codeEl.textContent = 'Copied'; setTimeout(() => codeEl.textContent = originalText, 3000); }).catch(console.error); })(this)"><code>${content}</code></button>`; )}').then(() => { const originalText = codeEl.textContent; codeEl.textContent = 'Copied'; setTimeout(() => codeEl.textContent = originalText, 3000); }).catch(console.error); })(this)"><code>${content}</code></button>`
}; }
} }

View File

@ -1,42 +1,42 @@
import { icons as twemoji } from "@iconify-json/twemoji"; import { icons as twemoji } from '@iconify-json/twemoji'
import type { MarkdownRenderer } from "vitepress"; import type { MarkdownRenderer } from 'vitepress'
export const defs = { export const defs = {
...Object.fromEntries( ...Object.fromEntries(
Object.entries(twemoji.icons).map(([key]) => { Object.entries(twemoji.icons).map(([key]) => {
return [key, ""]; return [key, '']
}), })
), )
}; }
export function emojiRender(md: MarkdownRenderer) { export function emojiRender(md: MarkdownRenderer) {
md.renderer.rules.emoji = (tokens, idx) => { md.renderer.rules.emoji = (tokens, idx) => {
if (tokens[idx].markup.startsWith("star")) { if (tokens[idx].markup.startsWith('star')) {
return `<span class="i-twemoji-${tokens[idx].markup} starred"></span>`; return `<span class="i-twemoji-${tokens[idx].markup} starred"></span>`
} }
return `<span class="i-twemoji-${tokens[idx].markup}"></span>`; return `<span class="i-twemoji-${tokens[idx].markup}"></span>`
}; }
} }
export function movePlugin( export function movePlugin(
plugins: { name: string }[], plugins: { name: string }[],
pluginAName: string, pluginAName: string,
order: "before" | "after", order: 'before' | 'after',
pluginBName: string, pluginBName: string
) { ) {
const pluginBIndex = plugins.findIndex((p) => p.name === pluginBName); const pluginBIndex = plugins.findIndex((p) => p.name === pluginBName)
if (pluginBIndex === -1) return; if (pluginBIndex === -1) return
const pluginAIndex = plugins.findIndex((p) => p.name === pluginAName); const pluginAIndex = plugins.findIndex((p) => p.name === pluginAName)
if (pluginAIndex === -1) return; if (pluginAIndex === -1) return
if (order === "before" && pluginAIndex > pluginBIndex) { if (order === 'before' && pluginAIndex > pluginBIndex) {
const pluginA = plugins.splice(pluginAIndex, 1)[0]; const pluginA = plugins.splice(pluginAIndex, 1)[0]
plugins.splice(pluginBIndex, 0, pluginA); plugins.splice(pluginBIndex, 0, pluginA)
} }
if (order === "after" && pluginAIndex < pluginBIndex) { if (order === 'after' && pluginAIndex < pluginBIndex) {
const pluginA = plugins.splice(pluginAIndex, 1)[0]; const pluginA = plugins.splice(pluginAIndex, 1)[0]
plugins.splice(pluginBIndex, 0, pluginA); plugins.splice(pluginBIndex, 0, pluginA)
} }
} }

View File

@ -1,18 +1,18 @@
import type { MarkdownRenderer } from "vitepress"; import type { MarkdownRenderer } from 'vitepress'
const excluded = ["Beginners Guide"]; const excluded = ['Beginners Guide']
export function toggleStarredPlugin(md: MarkdownRenderer) { export function toggleStarredPlugin(md: MarkdownRenderer) {
md.renderer.rules.list_item_open = (tokens, index, options, env, self) => { md.renderer.rules.list_item_open = (tokens, index, options, env, self) => {
const contentToken = tokens[index + 2]; const contentToken = tokens[index + 2]
if ( if (
!excluded.includes(env.frontmatter.title) && !excluded.includes(env.frontmatter.title) &&
contentToken && contentToken &&
contentToken.content.startsWith(":star:") contentToken.content.startsWith(':star:')
) { ) {
return `<li class="starred">`; return `<li class="starred">`
} else { } else {
return self.renderToken(tokens, index, options); return self.renderToken(tokens, index, options)
} }
}; }
} }

View File

@ -1,6 +1,6 @@
import { corsEventHandler } from "nitro-cors"; import { corsEventHandler } from 'nitro-cors'
export default corsEventHandler((_event) => {}, { export default corsEventHandler((_event) => {}, {
origin: "*", origin: '*',
methods: "*", methods: '*'
}); })

View File

@ -1,28 +1,32 @@
import { fetcher } from "itty-fetcher"; import { fetcher } from 'itty-fetcher'
import { FeedbackSchema, getFeedbackOption } from "../types/Feedback"; import { FeedbackSchema, getFeedbackOption } from '../types/Feedback'
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const { message, page, type } = await readValidatedBody(event, FeedbackSchema.parseAsync); const { message, page, type } = await readValidatedBody(
const env = useRuntimeConfig(event); event,
FeedbackSchema.parseAsync
)
const env = useRuntimeConfig(event)
let description = `${message}\n\n`; let description = `${message}\n\n`
if (page) description += `**Page:** \`${page}\``; if (page) description += `**Page:** \`${page}\``
await fetcher() await fetcher()
.post(env.WEBHOOK_URL, { .post(env.WEBHOOK_URL, {
username: "Feedback", username: 'Feedback',
avatar_url: "https://i.kym-cdn.com/entries/icons/facebook/000/043/403/cover3.jpg", avatar_url:
'https://i.kym-cdn.com/entries/icons/facebook/000/043/403/cover3.jpg',
embeds: [ embeds: [
{ {
color: 3447003, color: 3447003,
title: getFeedbackOption(type).label, title: getFeedbackOption(type).label,
description, description
}, }
], ]
}) })
.catch((error) => { .catch((error) => {
throw new Error(error); throw new Error(error)
}); })
return { status: "ok" }; return { status: 'ok' }
}); })

View File

@ -1,3 +1,3 @@
export default eventHandler(() => { export default eventHandler(() => {
return { nitro: "works" }; return { nitro: 'works' }
}); })

View File

@ -1,47 +1,47 @@
<script setup lang="ts"> <script setup lang="ts">
import DefaultTheme from "vitepress/theme"; import DefaultTheme from 'vitepress/theme'
import { useData } from "vitepress"; import { useData } from 'vitepress'
import { nextTick, provide } from "vue"; import { nextTick, provide } from 'vue'
import Sidebar from "./components/SidebarCard.vue"; import Sidebar from './components/SidebarCard.vue'
import Announcement from "./components/Announcement.vue"; import Announcement from './components/Announcement.vue'
const { isDark } = useData(); const { isDark } = useData()
const enableTransitions = () => const enableTransitions = () =>
"startViewTransition" in document && 'startViewTransition' in document &&
window.matchMedia("(prefers-reduced-motion: no-preference)").matches; window.matchMedia('(prefers-reduced-motion: no-preference)').matches
provide("toggle-appearance", async ({ clientX: x, clientY: y }: MouseEvent) => { provide('toggle-appearance', async ({ clientX: x, clientY: y }: MouseEvent) => {
if (!enableTransitions()) { if (!enableTransitions()) {
isDark.value = !isDark.value; isDark.value = !isDark.value
return; return
} }
const clipPath = [ const clipPath = [
`circle(0px at ${x}px ${y}px)`, `circle(0px at ${x}px ${y}px)`,
`circle(${Math.hypot( `circle(${Math.hypot(
Math.max(x, innerWidth - x), Math.max(x, innerWidth - x),
Math.max(y, innerHeight - y), Math.max(y, innerHeight - y)
)}px at ${x}px ${y}px)`, )}px at ${x}px ${y}px)`
]; ]
// @ts-expect-error // @ts-expect-error
await document.startViewTransition(async () => { await document.startViewTransition(async () => {
isDark.value = !isDark.value; isDark.value = !isDark.value
await nextTick(); await nextTick()
}).ready; }).ready
document.documentElement.animate( document.documentElement.animate(
{ clipPath: isDark.value ? clipPath.reverse() : clipPath }, { clipPath: isDark.value ? clipPath.reverse() : clipPath },
{ {
duration: 300, duration: 300,
easing: "ease-in", easing: 'ease-in',
pseudoElement: `::view-transition-${isDark.value ? "old" : "new"}(root)`, pseudoElement: `::view-transition-${isDark.value ? 'old' : 'new'}(root)`
}, }
); )
}); })
const { Layout } = DefaultTheme; const { Layout } = DefaultTheme
</script> </script>
<template> <template>

View File

@ -1,20 +1,20 @@
<script setup lang="ts"> <script setup lang="ts">
import { useData } from "vitepress"; import { useData } from 'vitepress'
import Authors from "./components/Authors.vue"; import Authors from './components/Authors.vue'
const props = defineProps<{ const props = defineProps<{
authors: string[]; authors: string[]
}>(); }>()
const formatDate = (raw: string): string => { const formatDate = (raw: string): string => {
const date = new Date(raw); const date = new Date(raw)
return date.toLocaleDateString("en-US", { return date.toLocaleDateString('en-US', {
month: "short", month: 'short',
day: "numeric", day: 'numeric'
}); })
}; }
const { frontmatter } = useData(); const { frontmatter } = useData()
</script> </script>
<template> <template>
@ -22,6 +22,8 @@ const { frontmatter } = useData();
{{ frontmatter.title }} {{ frontmatter.title }}
</h3> </h3>
<span>{{ frontmatter.description }} {{ formatDate(frontmatter.date) }}</span> <span>
{{ frontmatter.description }} {{ formatDate(frontmatter.date) }}
</span>
<Authors :authors="props.authors" /> <Authors :authors="props.authors" />
</template> </template>

View File

@ -1,14 +1,14 @@
<!-- eslint-disable vue/require-v-for-key --> <!-- eslint-disable vue/require-v-for-key -->
<script setup lang="ts"> <script setup lang="ts">
import { data as posts } from "./posts.data"; import { data as posts } from './posts.data'
const formatDate = (raw: string): string => { const formatDate = (raw: string): string => {
const date = new Date(raw); const date = new Date(raw)
return date.toLocaleDateString("en-US", { return date.toLocaleDateString('en-US', {
month: "short", month: 'short',
day: "numeric", day: 'numeric'
}); })
}; }
</script> </script>
<template> <template>
@ -28,11 +28,14 @@ const formatDate = (raw: string): string => {
<ul> <ul>
<li v-for="post of posts[year]" :key="post.url"> <li v-for="post of posts[year]" :key="post.url">
<article> <article>
<a :href="post.url" class="border-none">{{ post.title }}</a> - <a :href="post.url" class="border-none">{{ post.title }}</a>
-
<dl class="m-0 inline"> <dl class="m-0 inline">
<dt class="sr-only">Published on</dt> <dt class="sr-only">Published on</dt>
<dd class="m-0 inline"> <dd class="m-0 inline">
<time :datetime="post.date" class="font-bold">{{ formatDate(post.date) }}</time> <time :datetime="post.date" class="font-bold">
{{ formatDate(post.date) }}
</time>
</dd> </dd>
</dl> </dl>
</article> </article>

View File

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { useData } from "vitepress"; import { useData } from 'vitepress'
const { frontmatter } = useData(); const { frontmatter } = useData()
</script> </script>
<template> <template>
@ -9,7 +9,8 @@ const { frontmatter } = useData();
v-if="frontmatter.hero.prelink" v-if="frontmatter.hero.prelink"
:href="frontmatter.hero.prelink.link" :href="frontmatter.hero.prelink.link"
target="_blank" target="_blank"
class="inline-flex items-center rounded-lg bg-[var(--vp-c-default-soft)] px-4 py-1 text-sm font-semibold mb-3"> class="inline-flex items-center rounded-lg bg-[var(--vp-c-default-soft)] px-4 py-1 text-sm font-semibold mb-3"
>
{{ frontmatter.hero.prelink.title }} {{ frontmatter.hero.prelink.title }}
</a> </a>
</template> </template>

View File

@ -1,39 +1,41 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from "vue"; import { computed } from 'vue'
const props = defineProps<{ const props = defineProps<{
authors: string[]; authors: string[]
}>(); }>()
interface Author { interface Author {
name: string; name: string
github: string; github: string
} }
const data = [ const data = [
{ {
name: "nbats", name: 'nbats',
github: "https://github.com/nbats", github: 'https://github.com/nbats'
}, },
{ {
name: "Kai", name: 'Kai',
github: "https://github.com/Kai-FMHY", github: 'https://github.com/Kai-FMHY'
}, },
{ {
name: "taskylizard", name: 'taskylizard',
github: "https://github.com/taskylizard", github: 'https://github.com/taskylizard'
}, },
{ {
name: "zinklog", name: 'zinklog',
github: "https://github.com/zinklog2", github: 'https://github.com/zinklog2'
}, },
{ {
name: "Q", name: 'Q',
github: "https://github.com/qiracy", github: 'https://github.com/qiracy'
}, }
] satisfies Author[]; ] satisfies Author[]
const authors = computed(() => data.filter((author) => props.authors.includes(author.name))); const authors = computed(() =>
data.filter((author) => props.authors.includes(author.name))
)
</script> </script>
<template> <template>
@ -41,7 +43,7 @@ const authors = computed(() => data.filter((author) => props.authors.includes(au
<div v-for="(c, index) of authors" class="flex gap-2 items-center"> <div v-for="(c, index) of authors" class="flex gap-2 items-center">
<img :src="`${c.github}.png`" class="w-8 h-8 rounded-full" /> <img :src="`${c.github}.png`" class="w-8 h-8 rounded-full" />
<a :href="c.github">{{ c.name }}</a> <a :href="c.github">{{ c.name }}</a>
<span v-if="index < authors.length - 1"> </span> <span v-if="index < authors.length - 1"></span>
</div> </div>
</div> </div>
</template> </template>

View File

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
defineProps<{ defineProps<{
icon: string; icon: string
}>(); }>()
</script> </script>
<template> <template>

View File

@ -1,48 +1,52 @@
<script setup lang="ts"> <script setup lang="ts">
import { reactive, ref } from "vue"; import { reactive, ref } from 'vue'
import { useRouter } from "vitepress"; import { useRouter } from 'vitepress'
import { type FeedbackType, getFeedbackOption, feedbackOptions } from "../../types/Feedback"; import {
type FeedbackType,
getFeedbackOption,
feedbackOptions
} from '../../types/Feedback'
const loading = ref<boolean>(false); const loading = ref<boolean>(false)
const error = ref<unknown>(null); const error = ref<unknown>(null)
const success = ref<boolean>(false); const success = ref<boolean>(false)
const router = useRouter(); const router = useRouter()
const feedback = reactive<FeedbackType>({ message: "" }); const feedback = reactive<FeedbackType>({ message: '' })
async function handleSubmit(type?: FeedbackType["type"]) { async function handleSubmit(type?: FeedbackType['type']) {
if (type) feedback.type = type; if (type) feedback.type = type
loading.value = true; loading.value = true
const body: FeedbackType = { const body: FeedbackType = {
message: feedback.message, message: feedback.message,
type: feedback.type, type: feedback.type,
page: router.route.path, page: router.route.path
}; }
try { try {
const response = await fetch("https://feedback.tasky.workers.dev", { const response = await fetch('https://feedback.tasky.workers.dev', {
method: "POST", method: 'POST',
headers: { headers: {
"Content-Type": "application/json", 'Content-Type': 'application/json'
}, },
body: JSON.stringify(body), body: JSON.stringify(body)
}); })
const data = await response.json(); const data = await response.json()
if (data.error) { if (data.error) {
error.value = data.error; error.value = data.error
return; return
} }
if (data.status === "ok") { if (data.status === 'ok') {
success.value = true; success.value = true
} }
} catch (error) { } catch (error) {
error.value = error; error.value = error
} finally { } finally {
loading.value = false; loading.value = false
} }
} }
</script> </script>
@ -61,7 +65,8 @@ async function handleSubmit(type?: FeedbackType["type"]) {
v-for="item in feedbackOptions" v-for="item in feedbackOptions"
:key="item.value" :key="item.value"
class="btn" class="btn"
@click="handleSubmit(item.value as FeedbackType['type'])"> @click="handleSubmit(item.value as FeedbackType['type'])"
>
<span>{{ item.label }}</span> <span>{{ item.label }}</span>
</button> </button>
</div> </div>
@ -71,7 +76,11 @@ async function handleSubmit(type?: FeedbackType["type"]) {
<p class="desc">Page: {{ router.route.path }}</p> <p class="desc">Page: {{ router.route.path }}</p>
<div> <div>
<span>{{ getFeedbackOption(feedback.type)?.label }}</span> <span>{{ getFeedbackOption(feedback.type)?.label }}</span>
<button style="margin-left: 0.5rem" class="btn" @click="feedback.type = undefined"> <button
style="margin-left: 0.5rem"
class="btn"
@click="feedback.type = undefined"
>
<span class="i-carbon-close-large">close</span> <span class="i-carbon-close-large">close</span>
</button> </button>
</div> </div>
@ -80,19 +89,27 @@ async function handleSubmit(type?: FeedbackType["type"]) {
v-model="feedback.message" v-model="feedback.message"
autofocus autofocus
class="input" class="input"
placeholder="What a lovely wiki!" /> placeholder="What a lovely wiki!"
/>
<p class="desc mb-2"> <p class="desc mb-2">
If you'd prefer to be contacted through another platform, feel free to mention it in the If you'd prefer to be contacted through another platform, feel free to
message or join our mention it in the message or join our
<a class="text-primary font-semibold text-underline" href="https://discord.gg/Stz6y6NgNg" <a
>Discord</a class="text-primary font-semibold text-underline"
>. href="https://discord.gg/Stz6y6NgNg"
>
Discord
</a>
.
</p> </p>
<button <button
type="submit" type="submit"
class="btn btn-primary" class="btn btn-primary"
:disabled="feedback.message.length < 5 || feedback.message.length > 1000" :disabled="
@click="handleSubmit()"> feedback.message.length < 5 || feedback.message.length > 1000
"
@click="handleSubmit()"
>
Submit Submit
</button> </button>
</div> </div>

View File

@ -1,34 +1,34 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue"; import { ref } from 'vue'
import { import {
TransitionRoot, TransitionRoot,
TransitionChild, TransitionChild,
Dialog, Dialog,
DialogPanel, DialogPanel,
DialogTitle, DialogTitle,
DialogDescription, DialogDescription
} from "@headlessui/vue"; } from '@headlessui/vue'
const isOpen = ref(true); const isOpen = ref(true)
const feedbackOptions = [ const feedbackOptions = [
{ {
label: "💡 Suggestion", label: '💡 Suggestion',
value: "suggestion", value: 'suggestion'
}, },
{ {
label: "❤️ Appreciation", label: '❤️ Appreciation',
value: "appreciate", value: 'appreciate'
}, },
{ label: "🐞 Bug", value: "bug" }, { label: '🐞 Bug', value: 'bug' },
{ label: "📂 Other", value: "other" }, { label: '📂 Other', value: 'other' }
]; ]
function closeModal() { function closeModal() {
isOpen.value = false; isOpen.value = false
} }
function openModal() { function openModal() {
isOpen.value = true; isOpen.value = true
} }
</script> </script>
@ -36,7 +36,8 @@ function openModal() {
<button <button
type="button" type="button"
class="p-[4px 8px] text-xl i-carbon:user-favorite-alt-filled" class="p-[4px 8px] text-xl i-carbon:user-favorite-alt-filled"
@click="openModal" /> @click="openModal"
/>
<TransitionRoot appear :show="isOpen" as="template"> <TransitionRoot appear :show="isOpen" as="template">
<Dialog as="div" class="relative z-10" @close="closeModal"> <Dialog as="div" class="relative z-10" @close="closeModal">
@ -47,12 +48,15 @@ function openModal() {
enter-to="opacity-100" enter-to="opacity-100"
leave="duration-200 ease-in" leave="duration-200 ease-in"
leave-from="opacity-100" leave-from="opacity-100"
leave-to="opacity-0"> leave-to="opacity-0"
>
<div class="fixed inset-0 bg-black/25" /> <div class="fixed inset-0 bg-black/25" />
</TransitionChild> </TransitionChild>
<div class="fixed inset-0 overflow-y-auto"> <div class="fixed inset-0 overflow-y-auto">
<div class="flex min-h-full items-center justify-center p-4 text-center"> <div
class="flex min-h-full items-center justify-center p-4 text-center"
>
<TransitionChild <TransitionChild
as="template" as="template"
enter="duration-300 ease-out" enter="duration-300 ease-out"
@ -60,10 +64,15 @@ function openModal() {
enter-to="opacity-100 scale-100" enter-to="opacity-100 scale-100"
leave="duration-200 ease-in" leave="duration-200 ease-in"
leave-from="opacity-100 scale-100" leave-from="opacity-100 scale-100"
leave-to="opacity-0 scale-95"> leave-to="opacity-0 scale-95"
>
<DialogPanel <DialogPanel
class="w-full max-w-md transform overflow-hidden rounded-2xl bg-bg p-6 text-left align-middle shadow-xl transition-all"> class="w-full max-w-md transform overflow-hidden rounded-2xl bg-bg p-6 text-left align-middle shadow-xl transition-all"
<DialogTitle as="h3" class="text-lg font-medium leading-6 text-text"> >
<DialogTitle
as="h3"
class="text-lg font-medium leading-6 text-text"
>
Feedback Feedback
</DialogTitle> </DialogTitle>
@ -72,7 +81,8 @@ function openModal() {
<button <button
v-for="item in feedbackOptions" v-for="item in feedbackOptions"
:key="item.value" :key="item.value"
class="inline-flex justify-center rounded-md border border-transparent bg-bg-alt px-4 py-2 text-sm font-medium text-text hover:border-primary focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2"> class="inline-flex justify-center rounded-md border border-transparent bg-bg-alt px-4 py-2 text-sm font-medium text-text hover:border-primary focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2"
>
<span>{{ item.label }}</span> <span>{{ item.label }}</span>
</button> </button>
</div> </div>
@ -90,7 +100,8 @@ function openModal() {
<button <button
type="button" type="button"
class="inline-flex justify-center rounded-md border border-transparent bg-blue-100 px-4 py-2 text-sm font-medium text-blue-900 hover:bg-blue-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2" class="inline-flex justify-center rounded-md border border-transparent bg-blue-100 px-4 py-2 text-sm font-medium text-blue-900 hover:bg-blue-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
@click="closeModal"> @click="closeModal"
>
Close Close
</button> </button>
</div> </div>

View File

@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
defineProps<{ defineProps<{
label: string; label: string
id: string; id: string
}>(); }>()
</script> </script>
<template> <template>

View File

@ -1,12 +1,15 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue"; import { ref } from 'vue'
import Feedback from "./Feedback.vue"; import Feedback from './Feedback.vue'
const showModal = ref(false); const showModal = ref(false)
</script> </script>
<template> <template>
<button class="p-[4px 8px] text-xl i-carbon:user-favorite-alt-filled" @click="showModal = true" /> <button
class="p-[4px 8px] text-xl i-carbon:user-favorite-alt-filled"
@click="showModal = true"
/>
<Teleport to="body"> <Teleport to="body">
<Transition name="modal"> <Transition name="modal">
@ -14,7 +17,9 @@ const showModal = ref(false);
<div class="modal-container"> <div class="modal-container">
<Feedback /> <Feedback />
<div class="model-footer"> <div class="model-footer">
<button class="modal-button" @click="showModal = false">Close</button> <button class="modal-button" @click="showModal = false">
Close
</button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import Field from "./CardField.vue"; import Field from './CardField.vue'
import Modal from "./Modal.vue"; import Modal from './Modal.vue'
import InputField from "./InputField.vue"; import InputField from './InputField.vue'
import ToggleStarred from "./ToggleStarred.vue"; import ToggleStarred from './ToggleStarred.vue'
</script> </script>
<template> <template>
@ -10,9 +10,9 @@ import ToggleStarred from "./ToggleStarred.vue";
<div class="card-header"> <div class="card-header">
<div class="card-title">Emoji Legend</div> <div class="card-title">Emoji Legend</div>
</div> </div>
<Field icon="i-twemoji-star"> Recommendations </Field> <Field icon="i-twemoji-star">Recommendations</Field>
<Field icon="i-twemoji-globe-with-meridians"> Indexes </Field> <Field icon="i-twemoji-globe-with-meridians">Indexes</Field>
<Field icon="i-twemoji-repeat-button"> Storage Links </Field> <Field icon="i-twemoji-repeat-button">Storage Links</Field>
<div class="card-header"> <div class="card-header">
<div class="card-title">Options</div> <div class="card-title">Options</div>
</div> </div>

View File

@ -1,8 +1,8 @@
<script setup> <script setup>
import { ref } from "vue"; import { ref } from 'vue'
import { Switch } from "@headlessui/vue"; import { Switch } from '@headlessui/vue'
const enabled = ref(false); const enabled = ref(false)
</script> </script>
<template> <template>

View File

@ -1,7 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import Switch from "./Switch.vue"; import Switch from './Switch.vue'
const toggleStarred = () => document.documentElement.classList.toggle("starred-only"); const toggleStarred = () =>
document.documentElement.classList.toggle('starred-only')
</script> </script>
<template> <template>

View File

@ -1,23 +1,23 @@
import nprogress, { type NProgress } from "nprogress"; import nprogress, { type NProgress } from 'nprogress'
import type { EnhanceAppContext } from "vitepress"; import type { EnhanceAppContext } from 'vitepress'
export function loadProgress(router: EnhanceAppContext["router"]): NProgress { export function loadProgress(router: EnhanceAppContext['router']): NProgress {
if (typeof window === "undefined") return; if (typeof window === 'undefined') return
setTimeout(() => { setTimeout(() => {
nprogress.configure({ showSpinner: false }); nprogress.configure({ showSpinner: false })
const cacheBeforeRouteChange = router.onBeforeRouteChange; const cacheBeforeRouteChange = router.onBeforeRouteChange
const cacheAfterRouteChange = router.onAfterRouteChanged; const cacheAfterRouteChange = router.onAfterRouteChanged
router.onBeforeRouteChange = (to) => { router.onBeforeRouteChange = (to) => {
nprogress.start(); nprogress.start()
cacheBeforeRouteChange?.(to); cacheBeforeRouteChange?.(to)
}; }
router.onAfterRouteChanged = (to) => { router.onAfterRouteChanged = (to) => {
nprogress.done(); nprogress.done()
cacheAfterRouteChange?.(to); cacheAfterRouteChange?.(to)
}; }
}); })
return nprogress; return nprogress
} }

View File

@ -1,16 +1,16 @@
import { type Theme } from "vitepress"; import { type Theme } from 'vitepress'
import DefaultTheme from "vitepress/theme"; import DefaultTheme from 'vitepress/theme'
import Layout from "./Layout.vue"; import Layout from './Layout.vue'
import Post from "./PostLayout.vue"; import Post from './PostLayout.vue'
import { loadProgress } from "./composables/nprogress"; import { loadProgress } from './composables/nprogress'
import "./style.css"; import './style.css'
import "uno.css"; import 'uno.css'
export default { export default {
extends: DefaultTheme, extends: DefaultTheme,
Layout, Layout,
enhanceApp({ router, app }) { enhanceApp({ router, app }) {
app.component("Post", Post); app.component('Post', Post)
loadProgress(router); loadProgress(router)
}, }
} satisfies Theme; } satisfies Theme

View File

@ -1,30 +1,30 @@
import { createContentLoader, type ContentData } from "vitepress"; import { createContentLoader, type ContentData } from 'vitepress'
import { groupBy } from "../utils"; import { groupBy } from '../utils'
interface Post { interface Post {
title: string; title: string
url: string; url: string
date: string; date: string
} }
type Dictionary = ReturnType<typeof transformRawPosts>; type Dictionary = ReturnType<typeof transformRawPosts>
declare const data: Dictionary; declare const data: Dictionary
export { data }; export { data }
function transformRawPosts(rawPosts: ContentData[]): Record<string, Post[]> { function transformRawPosts(rawPosts: ContentData[]): Record<string, Post[]> {
const posts: Post[] = rawPosts const posts: Post[] = rawPosts
.map(({ url, frontmatter }) => ({ .map(({ url, frontmatter }) => ({
title: frontmatter.title, title: frontmatter.title,
url, url,
date: (frontmatter.date as Date).toISOString().slice(0, 10), date: (frontmatter.date as Date).toISOString().slice(0, 10)
})) }))
.sort((a, b) => b.date.localeCompare(a.date)); .sort((a, b) => b.date.localeCompare(a.date))
return groupBy(posts, (post) => post.date.slice(0, 4)); return groupBy(posts, (post) => post.date.slice(0, 4))
} }
export default createContentLoader("posts/*.md", { export default createContentLoader('posts/*.md', {
includeSrc: true, includeSrc: true,
transform: (raw) => transformRawPosts(raw), transform: (raw) => transformRawPosts(raw)
}); })

View File

@ -124,9 +124,17 @@
*/ */
:root { :root {
--vp-home-hero-name-color: transparent; --vp-home-hero-name-color: transparent;
--vp-home-hero-name-background: -webkit-linear-gradient(120deg, #c4b5fd 30%, #7bc5e4); --vp-home-hero-name-background: -webkit-linear-gradient(
120deg,
#c4b5fd 30%,
#7bc5e4
);
--vp-home-hero-image-background-image: linear-gradient(-45deg, #c4b5fd 50%, #47caff 50%); --vp-home-hero-image-background-image: linear-gradient(
-45deg,
#c4b5fd 50%,
#47caff 50%
);
--vp-home-hero-image-filter: blur(44px); --vp-home-hero-image-filter: blur(44px);
} }

View File

@ -1,26 +1,29 @@
import z from "zod"; import z from 'zod'
export const FeedbackSchema = z.object({ export const FeedbackSchema = z.object({
message: z.string().min(5).max(1000), message: z.string().min(5).max(1000),
type: z.enum(["bug", "suggestion", "appreciate", "other"]), type: z.enum(['bug', 'suggestion', 'appreciate', 'other']),
page: z.string().optional(), page: z.string().optional()
}); })
export const feedbackOptions = [ export const feedbackOptions = [
{ label: "🐞 Bug", value: "bug" }, { label: '🐞 Bug', value: 'bug' },
{ {
label: "💡 Suggestion", label: '💡 Suggestion',
value: "suggestion", value: 'suggestion'
}, },
{ label: "📂 Other", value: "other" }, { label: '📂 Other', value: 'other' },
{ {
label: "❤️ Appreciation", label: '❤️ Appreciation',
value: "appreciate", value: 'appreciate'
}, }
]; ]
export function getFeedbackOption(value: string): { label: string; value: string } { export function getFeedbackOption(value: string): {
return feedbackOptions.find((option) => option.value === value); label: string
value: string
} {
return feedbackOptions.find((option) => option.value === value)
} }
export type FeedbackType = z.infer<typeof FeedbackSchema>; export type FeedbackType = z.infer<typeof FeedbackSchema>

View File

@ -1,9 +1,12 @@
export function groupBy<T, K extends keyof any>(arr: T[], key: (i: T) => K): Record<K, T[]> { export function groupBy<T, K extends keyof any>(
arr: T[],
key: (i: T) => K
): Record<K, T[]> {
return arr.reduce( return arr.reduce(
(groups, item) => { (groups, item) => {
(groups[key(item)] ||= []).push(item); ;(groups[key(item)] ||= []).push(item)
return groups; return groups
}, },
{} as Record<K, T[]>, {} as Record<K, T[]>
); )
} }

View File

@ -1,5 +1,5 @@
/* eslint-disable ts/consistent-type-imports */ /* eslint-disable ts/consistent-type-imports */
declare module "*.vue" { declare module '*.vue' {
const component: import("vue").Component; const component: import('vue').Component
export default component; export default component
} }

View File

@ -1,7 +1,7 @@
// @ts-check // @ts-check
import tasky from "@taskylizard/eslint-config"; import tasky from '@taskylizard/eslint-config'
export default tasky({ export default tasky({
vue: true, vue: true,
browser: true, browser: true
}); })

View File

@ -1,12 +1,12 @@
//https://nitro.unjs.io/config //https://nitro.unjs.io/config
export default defineNitroConfig({ export default defineNitroConfig({
runtimeConfig: { runtimeConfig: {
WEBHOOK_URL: process.env.WEBHOOK_URL, WEBHOOK_URL: process.env.WEBHOOK_URL
}, },
srcDir: ".vitepress", srcDir: '.vitepress',
routeRules: { routeRules: {
"/": { '/': {
cors: false, cors: false
}, }
}, }
}); })

View File

@ -1,7 +1,7 @@
{ {
"extends": "./.nitro/types/tsconfig.json", "extends": "./.nitro/types/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"verbatimModuleSyntax": true, "verbatimModuleSyntax": true
}, },
"include": ["./.vitepress/"], "include": ["./.vitepress/"]
} }

View File

@ -1,16 +1,16 @@
import { defineConfig, presetUno, presetAttributify, presetIcons } from "unocss"; import { defineConfig, presetUno, presetAttributify, presetIcons } from 'unocss'
export default defineConfig({ export default defineConfig({
theme: { theme: {
colors: { colors: {
primary: "var(--vp-c-brand-1)", primary: 'var(--vp-c-brand-1)',
bg: "var(--vp-c-bg)", bg: 'var(--vp-c-bg)',
"bg-alt": "var(--vp-c-bg-alt)", 'bg-alt': 'var(--vp-c-bg-alt)',
"bg-elv": "var(--vp-c-bg-elv)", 'bg-elv': 'var(--vp-c-bg-elv)',
text: "var(--vp-c-text-1)", text: 'var(--vp-c-text-1)',
"text-2": "var(--vp-c-text-2)", 'text-2': 'var(--vp-c-text-2)',
div: "var(--vp-c-divider)", div: 'var(--vp-c-divider)'
}, }
}, },
presets: [ presets: [
presetUno(), presetUno(),
@ -18,9 +18,9 @@ export default defineConfig({
presetIcons({ presetIcons({
scale: 1.2, scale: 1.2,
extraProperties: { extraProperties: {
display: "inline-block", display: 'inline-block',
"vertical-align": "middle", 'vertical-align': 'middle'
}, }
}), })
], ]
}); })