🎉 New website!
This commit is contained in:
135
.vitepress/config.mts
Normal file
135
.vitepress/config.mts
Normal file
@@ -0,0 +1,135 @@
|
||||
import { defineConfig } from "vitepress";
|
||||
import { withPwa } from "@vite-pwa/vitepress";
|
||||
import UnoCSS from "unocss/vite";
|
||||
import { presetUno, presetAttributify, presetIcons } from "unocss";
|
||||
import { commitRef, meta } from "./constants";
|
||||
import { pwa } from "./pwa";
|
||||
import { fileURLToPath } from "url";
|
||||
import { generateImages, generateMeta } from "./hooks";
|
||||
import { toggleStarredPlugin } from "./markdown/toggleStarred";
|
||||
import { base64DecodePlugin } from "./markdown/base64";
|
||||
|
||||
export default withPwa(
|
||||
defineConfig({
|
||||
title: "FMHY",
|
||||
description: meta.description,
|
||||
titleTemplate: ":title • freemediaheckyeah",
|
||||
lang: "en-US",
|
||||
lastUpdated: true,
|
||||
cleanUrls: true,
|
||||
appearance: "dark",
|
||||
srcExclude: ["readme.md", "single-page", "toolsguide.md"],
|
||||
ignoreDeadLinks: true,
|
||||
metaChunk: true,
|
||||
sitemap: {
|
||||
hostname: meta.hostname,
|
||||
},
|
||||
head: [
|
||||
["meta", { name: "theme-color", content: "#7bc5e4" }],
|
||||
["meta", { name: "og:type", content: "website" }],
|
||||
["meta", { name: "og:locale", content: "en" }],
|
||||
["link", { rel: "icon", href: "/test.png" }],
|
||||
// PWA
|
||||
["link", { rel: "icon", href: "/test.png", type: "image/svg+xml" }],
|
||||
["link", { rel: "alternate icon", href: "/test.png" }],
|
||||
["link", { rel: "mask-icon", href: "/test.png", color: "#7bc5e4" }],
|
||||
// prettier-ignore
|
||||
["meta", { name: "keywords", content: meta.keywords.join(" ") }],
|
||||
["link", { rel: "apple-touch-icon", href: "/test.png", sizes: "192x192" }],
|
||||
],
|
||||
transformHead: async (context) => generateMeta(context, meta.hostname),
|
||||
buildEnd: async (context) => {
|
||||
generateImages(context);
|
||||
},
|
||||
vite: {
|
||||
plugins: [
|
||||
UnoCSS({
|
||||
presets: [
|
||||
presetUno(),
|
||||
presetAttributify(),
|
||||
presetIcons({
|
||||
scale: 1.2,
|
||||
extraProperties: {
|
||||
display: "inline-block",
|
||||
"vertical-align": "middle",
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
build: {
|
||||
// Shut the fuck up
|
||||
chunkSizeWarningLimit: Infinity,
|
||||
},
|
||||
resolve: {
|
||||
alias: [
|
||||
{
|
||||
find: /^.*VPSwitchAppearance\.vue$/,
|
||||
replacement: fileURLToPath(
|
||||
new URL("./theme/components/ThemeSwitch.vue", import.meta.url),
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
markdown: {
|
||||
config(md) {
|
||||
md.use(toggleStarredPlugin);
|
||||
md.use(base64DecodePlugin);
|
||||
},
|
||||
},
|
||||
themeConfig: {
|
||||
search: {
|
||||
options: {
|
||||
detailedView: true,
|
||||
},
|
||||
provider: "local",
|
||||
},
|
||||
footer: {
|
||||
message: `Made with ❤️ (rev: ${commitRef})`,
|
||||
},
|
||||
outline: "deep",
|
||||
logo: "/fmhy.ico",
|
||||
nav: [
|
||||
{ text: "Beginners Guide", link: "/beginners-guide" },
|
||||
{ text: "Glossary", link: "https://rentry.org/The-Piracy-Glossary" },
|
||||
{ text: "Guides", link: "https://rentry.co/fmhy-guides" },
|
||||
{ text: "Backups", link: "https://www.reddit.com/r/FREEMEDIAHECKYEAH/wiki/backups" },
|
||||
{ text: "Updates", link: "https://www.reddit.com/r/FREEMEDIAHECKYEAH/wiki/updates/" },
|
||||
],
|
||||
sidebar: [
|
||||
{ text: "📛 Adblocking / Privacy", link: "/adblockvpnguide" },
|
||||
{ text: "🤖 Artificial Intelligence", link: "/ai" },
|
||||
{ text: "📺 Movies / TV / Anime", link: "/videopiracyguide" },
|
||||
{ text: "🎵 Music / Podcasts / Radio", link: "/audiopiracyguide" },
|
||||
{ text: "🎮 Gaming / Emulation", link: "/gamingpiracyguide" },
|
||||
{ text: "📗 Books / Comics / Manga", link: "/readingpiracyguide" },
|
||||
{ text: "💾 Downloading", link: "/downloadpiracyguide" },
|
||||
{ text: "🌀 Torrenting", link: "/torrentpiracyguide" },
|
||||
{ text: "🧠 Educational", link: "/edupiracyguide" },
|
||||
{ text: "🔧 Tools", link: "/toolsguide" },
|
||||
{ text: "📷 Image Tools", link: "/img-tools" },
|
||||
{ text: "👨💻 Developer Tools", link: "/devtools" },
|
||||
{ text: "📱 Android / iOS", link: "/android-iosguide" },
|
||||
{ text: "🐧 Linux / MacOS", link: "/linuxguide" },
|
||||
{ text: "🌍 Non-English", link: "/non-english" },
|
||||
{ text: "📂 Miscellaneous", link: "/miscguide" },
|
||||
{ text: "🔞 NSFW", link: "/nsfwpiracy" },
|
||||
{ text: "⚠️ Unsafe Sites", link: "/unsafesites" },
|
||||
{ text: "🔑 Base64", link: "/base64" },
|
||||
{ text: "📦 Storage", link: "/storage" },
|
||||
],
|
||||
socialLinks: [
|
||||
{ icon: "github", link: "https://github.com/fmhy/FMHYEdit" },
|
||||
{ icon: "discord", link: "https://discord.gg/Stz6y6NgNg" },
|
||||
{
|
||||
icon: {
|
||||
svg: '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path d="M12 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12a12 12 0 0 0 12-12A12 12 0 0 0 12 0zm5.01 4.744c.688 0 1.25.561 1.25 1.249a1.25 1.25 0 0 1-2.498.056l-2.597-.547l-.8 3.747c1.824.07 3.48.632 4.674 1.488c.308-.309.73-.491 1.207-.491c.968 0 1.754.786 1.754 1.754c0 .716-.435 1.333-1.01 1.614a3.111 3.111 0 0 1 .042.52c0 2.694-3.13 4.87-7.004 4.87c-3.874 0-7.004-2.176-7.004-4.87c0-.183.015-.366.043-.534A1.748 1.748 0 0 1 4.028 12c0-.968.786-1.754 1.754-1.754c.463 0 .898.196 1.207.49c1.207-.883 2.878-1.43 4.744-1.487l.885-4.182a.342.342 0 0 1 .14-.197a.35.35 0 0 1 .238-.042l2.906.617a1.214 1.214 0 0 1 1.108-.701zM9.25 12C8.561 12 8 12.562 8 13.25c0 .687.561 1.248 1.25 1.248c.687 0 1.248-.561 1.248-1.249c0-.688-.561-1.249-1.249-1.249zm5.5 0c-.687 0-1.248.561-1.248 1.25c0 .687.561 1.248 1.249 1.248c.688 0 1.249-.561 1.249-1.249c0-.687-.562-1.249-1.25-1.249zm-5.466 3.99a.327.327 0 0 0-.231.094a.33.33 0 0 0 0 .463c.842.842 2.484.913 2.961.913c.477 0 2.105-.056 2.961-.913a.361.361 0 0 0 .029-.463a.33.33 0 0 0-.464 0c-.547.533-1.684.73-2.512.73c-.828 0-1.979-.196-2.512-.73a.326.326 0 0 0-.232-.095z"/></svg>',
|
||||
},
|
||||
link: "https://reddit.com/r/FREEMEDIAHECKYEAH",
|
||||
},
|
||||
],
|
||||
...pwa,
|
||||
},
|
||||
}),
|
||||
);
|
16
.vitepress/constants.ts
Normal file
16
.vitepress/constants.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export const meta = {
|
||||
name: "FreeMediaHeckYeah",
|
||||
description:
|
||||
"The Largest Collection Of Free Stuff On The Internet!The Largest Collection Of Free Stuff On The Internet!",
|
||||
hostname: process.env.COMMIT_REF ? "https://fmhy.netlify.app" : "https://fmhy.pages.dev",
|
||||
keywords: ["stream", "movies", "gaming", "reading", "anime"],
|
||||
};
|
||||
|
||||
// Netlify to Cloudflare otherwise dev
|
||||
export const commitRef = process.env.COMMIT_REF
|
||||
? `<a href="https://github.com/fmhy/FMHYEdit/commit/${process.env.COMMIT_REF
|
||||
}">${process.env.COMMIT_REF.slice(0, 8)}</a>`
|
||||
: process.env.CF_PAGES_COMMIT_SHA
|
||||
? `<a href="https://github.com/fmhy/FMHYEdit/commit/${process.env.CF_PAGES_COMMIT_SHA
|
||||
}">${process.env.CF_PAGES_COMMIT_SHA.slice(0, 8)}</a>`
|
||||
: "dev";
|
BIN
.vitepress/fonts/Inter-Bold.otf
Normal file
BIN
.vitepress/fonts/Inter-Bold.otf
Normal file
Binary file not shown.
BIN
.vitepress/fonts/Inter-Medium.otf
Normal file
BIN
.vitepress/fonts/Inter-Medium.otf
Normal file
Binary file not shown.
BIN
.vitepress/fonts/Inter-Regular.otf
Normal file
BIN
.vitepress/fonts/Inter-Regular.otf
Normal file
Binary file not shown.
BIN
.vitepress/fonts/Inter-SemiBold.otf
Normal file
BIN
.vitepress/fonts/Inter-SemiBold.otf
Normal file
Binary file not shown.
22
.vitepress/hooks/Template.vue
Normal file
22
.vitepress/hooks/Template.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{ title: string; description?: string }>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
tw="w-full h-full bg-black flex flex-col"
|
||||
style="background-image: url(https://files.catbox.moe/1f84dy.png)">
|
||||
<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="flex items-center">
|
||||
<div tw="text-zinc-100 ml-2 mt-1 font-semibold">freemediaheckyeah</div>
|
||||
</div>
|
||||
</div>
|
||||
<div tw="w-full pr-56 flex flex-col items-start justify-end">
|
||||
<div style="color: #f3f4f6" tw="text-6xl font-bold" v-html="title" />
|
||||
<div style="color: #c0caf5" tw="mt-2 text-4xl" v-html="description" />
|
||||
</div>
|
||||
</div>
|
||||
<div tw="shrink-0 h-2 w-full flex" style="background-color: #c4b5fd" />
|
||||
</div>
|
||||
</template>
|
6
.vitepress/hooks/index.ts
Normal file
6
.vitepress/hooks/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Barrel generated using @taskylizard/tasker.
|
||||
*/
|
||||
|
||||
export * from "./meta";
|
||||
export * from "./opengraph";
|
84
.vitepress/hooks/meta.ts
Normal file
84
.vitepress/hooks/meta.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import type { HeadConfig, TransformContext } from "vitepress";
|
||||
|
||||
export function generateMeta(context: TransformContext, hostname: string) {
|
||||
const head: HeadConfig[] = [];
|
||||
const { pageData } = context;
|
||||
|
||||
const url = `${hostname}/${pageData.relativePath.replace(/((^|\/)index)?\.md$/, "$2")}`;
|
||||
|
||||
head.push(["link", { rel: "canonical", href: url }]);
|
||||
head.push(["meta", { property: "og:url", content: url }]);
|
||||
head.push(["meta", { name: "twitter:url", content: url }]);
|
||||
head.push(["meta", { name: "twitter:card", content: "summary_large_image" }]);
|
||||
|
||||
head.push(["meta", { property: "og:title", content: pageData.frontmatter.title }]);
|
||||
head.push(["meta", { name: "twitter:title", content: pageData.frontmatter.title }]);
|
||||
|
||||
head.push([
|
||||
"meta",
|
||||
{
|
||||
property: "og:description",
|
||||
content: pageData.frontmatter.description,
|
||||
},
|
||||
]);
|
||||
head.push([
|
||||
"meta",
|
||||
{
|
||||
name: "twitter:description",
|
||||
content: pageData.frontmatter.description,
|
||||
},
|
||||
]);
|
||||
|
||||
if (pageData.frontmatter.image) {
|
||||
head.push([
|
||||
"meta",
|
||||
{
|
||||
property: "og:image",
|
||||
content: `${hostname}/${pageData.frontmatter.image.replace(/^\//, "")}`,
|
||||
},
|
||||
]);
|
||||
head.push([
|
||||
"meta",
|
||||
{
|
||||
name: "twitter:image",
|
||||
content: `${hostname}/${pageData.frontmatter.image.replace(/^\//, "")}`,
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
const url = pageData.filePath.replace("index.md", "").replace(".md", "");
|
||||
const imageUrl = `${url}/__og_image__/og.png`.replace(/\/\//g, "/").replace(/^\//, "");
|
||||
|
||||
head.push(["meta", { property: "og:image", content: `${hostname}/${imageUrl}` }]);
|
||||
head.push(["meta", { property: "og:image:width", content: "1200" }]);
|
||||
head.push(["meta", { property: "og:image:height", content: "628" }]);
|
||||
head.push(["meta", { property: "og:image:type", content: "image/png" }]);
|
||||
head.push(["meta", { property: "og:image:alt", content: pageData.frontmatter.title }]);
|
||||
head.push(["meta", { name: "twitter:image", content: `${hostname}/${imageUrl}` }]);
|
||||
head.push(["meta", { name: "twitter:image:width", content: "1200" }]);
|
||||
head.push(["meta", { name: "twitter:image:height", content: "628" }]);
|
||||
head.push(["meta", { name: "twitter:image:alt", content: pageData.frontmatter.title }]);
|
||||
}
|
||||
if (pageData.frontmatter.tag) {
|
||||
head.push(["meta", { property: "article:tag", content: pageData.frontmatter.tag }]);
|
||||
}
|
||||
if (pageData.frontmatter.date) {
|
||||
head.push([
|
||||
"meta",
|
||||
{
|
||||
property: "article:published_time",
|
||||
content: pageData.frontmatter.date,
|
||||
},
|
||||
]);
|
||||
}
|
||||
if (pageData.lastUpdated && pageData.frontmatter.lastUpdated !== false) {
|
||||
head.push([
|
||||
"meta",
|
||||
{
|
||||
property: "article:modified_time",
|
||||
content: new Date(pageData.lastUpdated).toISOString(),
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
return head;
|
||||
}
|
89
.vitepress/hooks/opengraph.ts
Normal file
89
.vitepress/hooks/opengraph.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { createContentLoader } from "vitepress";
|
||||
import type { ContentData, SiteConfig } from "vitepress";
|
||||
import { type SatoriOptions, satoriVue } from "x-satori/vue";
|
||||
import { renderAsync } from "@resvg/resvg-js";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const __fonts = resolve(__dirname, "../fonts");
|
||||
|
||||
export async function generateImages(config: SiteConfig) {
|
||||
const pages = await createContentLoader("**/*.md", { excerpt: true }).load();
|
||||
const template = await readFile(resolve(__dirname, "./Template.vue"), "utf-8");
|
||||
|
||||
const fonts: SatoriOptions["fonts"] = [
|
||||
{
|
||||
name: "Inter",
|
||||
data: await readFile(resolve(__fonts, "Inter-Regular.otf")),
|
||||
weight: 400,
|
||||
style: "normal",
|
||||
},
|
||||
{
|
||||
name: "Inter",
|
||||
data: await readFile(resolve(__fonts, "Inter-Medium.otf")),
|
||||
weight: 500,
|
||||
style: "normal",
|
||||
},
|
||||
{
|
||||
name: "Inter",
|
||||
data: await readFile(resolve(__fonts, "Inter-SemiBold.otf")),
|
||||
weight: 600,
|
||||
style: "normal",
|
||||
},
|
||||
{
|
||||
name: "Inter",
|
||||
data: await readFile(resolve(__fonts, "Inter-Bold.otf")),
|
||||
weight: 700,
|
||||
style: "normal",
|
||||
},
|
||||
];
|
||||
|
||||
for (const page of pages) {
|
||||
await generateImage({
|
||||
page,
|
||||
template,
|
||||
outDir: config.outDir,
|
||||
fonts,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
interface GenerateImagesOptions {
|
||||
page: ContentData;
|
||||
template: string;
|
||||
outDir: string;
|
||||
fonts: SatoriOptions["fonts"];
|
||||
}
|
||||
|
||||
async function generateImage({ page, template, outDir, fonts }: GenerateImagesOptions) {
|
||||
const { frontmatter, url } = page;
|
||||
|
||||
const options: SatoriOptions = {
|
||||
width: 1200,
|
||||
height: 628,
|
||||
fonts,
|
||||
props: {
|
||||
title:
|
||||
frontmatter.layout === "home"
|
||||
? frontmatter.hero.name ?? frontmatter.title
|
||||
: frontmatter.title,
|
||||
description:
|
||||
frontmatter.layout === "home"
|
||||
? frontmatter.hero.tagline ?? frontmatter.description
|
||||
: frontmatter.description,
|
||||
},
|
||||
};
|
||||
|
||||
const svg = await satoriVue(options, template);
|
||||
|
||||
const render = await renderAsync(svg);
|
||||
|
||||
const outputFolder = resolve(outDir, url.substring(1), "__og_image__");
|
||||
const outputFile = resolve(outputFolder, "og.png");
|
||||
|
||||
await mkdir(outputFolder, { recursive: true });
|
||||
|
||||
return await writeFile(outputFile, render.asPng());
|
||||
}
|
45
.vitepress/hooks/satoriConfig.ts
Normal file
45
.vitepress/hooks/satoriConfig.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { SatoriOptions, defineSatoriConfig } from "x-satori/vue";
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const __fonts = resolve(__dirname, "../fonts");
|
||||
|
||||
const fonts: SatoriOptions["fonts"] = [
|
||||
{
|
||||
name: "Inter",
|
||||
data: await readFile(resolve(__fonts, "Inter-Regular.otf")),
|
||||
weight: 400,
|
||||
style: "normal",
|
||||
},
|
||||
{
|
||||
name: "Inter",
|
||||
data: await readFile(resolve(__fonts, "Inter-Medium.otf")),
|
||||
weight: 500,
|
||||
style: "normal",
|
||||
},
|
||||
{
|
||||
name: "Inter",
|
||||
data: await readFile(resolve(__fonts, "Inter-SemiBold.otf")),
|
||||
weight: 600,
|
||||
style: "normal",
|
||||
},
|
||||
{
|
||||
name: "Inter",
|
||||
data: await readFile(resolve(__fonts, "Inter-Bold.otf")),
|
||||
weight: 700,
|
||||
style: "normal",
|
||||
},
|
||||
];
|
||||
|
||||
export default defineSatoriConfig({
|
||||
width: 1200,
|
||||
height: 628,
|
||||
fonts,
|
||||
props: {
|
||||
title: "Title",
|
||||
description:
|
||||
"Lorem ipsum dolor sit amet, qui minim labore adipisicing minim sint cillum sint consectetur cupidatat.",
|
||||
dir: "/j",
|
||||
},
|
||||
});
|
46
.vitepress/loaders/guides.data.ts
Normal file
46
.vitepress/loaders/guides.data.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { defineLoader } from "vitepress";
|
||||
import { writeFile, readFile } from "fs/promises";
|
||||
|
||||
interface Data {
|
||||
title?: string;
|
||||
content?: string;
|
||||
url?: string;
|
||||
}
|
||||
declare const data: Data;
|
||||
export { data };
|
||||
|
||||
const page = "https://rentry.co/fmhy-guides/raw";
|
||||
const regex = /\* \[([^\]]+)\]\(([^)]+)\)/g;
|
||||
const rentryRe = /(?<=rentry\.(co|org)).*/;
|
||||
const guides = new Set<Data>();
|
||||
|
||||
const f = async (url: string) => {
|
||||
const contents = await (await fetch(url))
|
||||
.text()
|
||||
.catch((error: Error) => console.error(`Failed at ${url}`, error));
|
||||
return contents;
|
||||
};
|
||||
|
||||
export default defineLoader({
|
||||
async load(): Promise<Data> {
|
||||
const contents = await f(page);
|
||||
let match: any[] | null;
|
||||
while ((match = regex.exec(contents)) !== null) {
|
||||
const title = match[1];
|
||||
const url = match[2];
|
||||
// Fetch rentry guides
|
||||
if (url.match(rentryRe)) {
|
||||
const content = await f(url + "/raw");
|
||||
guides.add({ title, content });
|
||||
} else {
|
||||
// Everything else can be here
|
||||
guides.add({ title, url });
|
||||
}
|
||||
}
|
||||
const obj = Object.fromEntries(
|
||||
[...guides.entries()].map((entry, index) => [index.toString(), entry]),
|
||||
);
|
||||
await writeFile("./guides.json", JSON.stringify(obj, null, 4), "utf-8");
|
||||
return JSON.parse(await readFile("./guides.json", { encoding: "utf-8" })) as Data;
|
||||
},
|
||||
});
|
25
.vitepress/markdown/base64.ts
Normal file
25
.vitepress/markdown/base64.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { type MarkdownRenderer } from "vitepress";
|
||||
|
||||
// FIXME: tasky: possibly write less horror jank?
|
||||
export function base64DecodePlugin(md: MarkdownRenderer) {
|
||||
const decode = (str: string): string => Buffer.from(str, "base64").toString("binary");
|
||||
// Save the original rule for backticks
|
||||
const defaultRender =
|
||||
md.renderer.rules.code_inline ||
|
||||
function (tokens, idx, options, env, self) {
|
||||
return self.renderToken(tokens, idx, options);
|
||||
};
|
||||
|
||||
md.renderer.rules.code_inline = function (tokens, idx, options, env, self) {
|
||||
// @ts-expect-error shut the fuck up already I HATE THIS
|
||||
if (!env.frontmatter.title || (env.frontmatter.title && !env.frontmatter.title === "base64")) {
|
||||
return defaultRender(tokens, idx, options, env, self);
|
||||
}
|
||||
const token = tokens[idx];
|
||||
const content = token.content;
|
||||
|
||||
return `<button class='base64' onclick="(function(btn){ const codeEl = btn.querySelector('code'); navigator.clipboard.writeText('${decode(
|
||||
content,
|
||||
)}').then(() => { const originalText = codeEl.textContent; codeEl.textContent = 'Copied'; setTimeout(() => codeEl.textContent = originalText, 3000); }).catch(console.error); })(this)"><code>${content}</code></button>`;
|
||||
};
|
||||
}
|
17
.vitepress/markdown/toggleStarred.ts
Normal file
17
.vitepress/markdown/toggleStarred.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { MarkdownRenderer } from "vitepress";
|
||||
|
||||
const excluded = ["Beginners Guide"];
|
||||
|
||||
export function toggleStarredPlugin(md: MarkdownRenderer) {
|
||||
md.renderer.rules.list_item_open = (tokens, index, options, env, self) => {
|
||||
const contentToken = tokens[index + 2];
|
||||
if (
|
||||
!excluded.includes(env.frontmatter.title) &&
|
||||
contentToken &&
|
||||
contentToken.content.startsWith("⭐")
|
||||
) {
|
||||
return `<li class="starred">`;
|
||||
}
|
||||
return self.renderToken(tokens, index, options);
|
||||
};
|
||||
}
|
6
.vitepress/middleware/cors.ts
Normal file
6
.vitepress/middleware/cors.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { corsEventHandler } from "nitro-cors";
|
||||
|
||||
export default corsEventHandler((_event) => {}, {
|
||||
origin: "*",
|
||||
methods: "*",
|
||||
});
|
102
.vitepress/pwa.ts
Normal file
102
.vitepress/pwa.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import type { PwaOptions } from "@vite-pwa/vitepress";
|
||||
import { meta } from "./constants";
|
||||
import { resolve } from "pathe";
|
||||
import fg from "fast-glob";
|
||||
|
||||
export const pwa = {
|
||||
outDir: ".vitepress/dist",
|
||||
registerType: "autoUpdate",
|
||||
includeManifestIcons: false,
|
||||
includeAssets: fg.sync("**/*.{png,webp,svg,gif,ico,txt}", {
|
||||
cwd: resolve(__dirname, "../public"),
|
||||
}),
|
||||
|
||||
manifest: {
|
||||
id: "/",
|
||||
name: meta.name,
|
||||
short_name: meta.name,
|
||||
description: meta.description,
|
||||
theme_color: "#ffffff",
|
||||
start_url: "/",
|
||||
lang: "en-US",
|
||||
dir: "ltr",
|
||||
orientation: "natural",
|
||||
display: "standalone",
|
||||
display_override: ["window-controls-overlay"],
|
||||
categories: meta.keywords,
|
||||
// TODO: replace with actual icons
|
||||
icons: [
|
||||
{
|
||||
src: "test.png",
|
||||
sizes: "64x64",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
src: "test.png",
|
||||
sizes: "192x192",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
src: "test.png",
|
||||
sizes: "512x512",
|
||||
type: "image/png",
|
||||
purpose: "any",
|
||||
},
|
||||
{
|
||||
src: "maskable-icon.png",
|
||||
sizes: "512x512",
|
||||
type: "image/png",
|
||||
purpose: "maskable",
|
||||
},
|
||||
],
|
||||
handle_links: "preferred",
|
||||
launch_handler: {
|
||||
client_mode: ["navigate-existing", "auto"],
|
||||
},
|
||||
edge_side_panel: {
|
||||
preferred_width: 480,
|
||||
},
|
||||
},
|
||||
experimental: {
|
||||
includeAllowlist: true,
|
||||
},
|
||||
workbox: {
|
||||
globPatterns: ["**/*.{css,js,html,svg,png,ico,txt,woff2,json}"],
|
||||
globIgnores: ["**/404.html"],
|
||||
navigateFallback: null,
|
||||
runtimeCaching: [
|
||||
{
|
||||
urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i,
|
||||
handler: "CacheFirst",
|
||||
options: {
|
||||
cacheName: "google-fonts-cache",
|
||||
expiration: {
|
||||
maxEntries: 10,
|
||||
maxAgeSeconds: 60 * 60 * 24 * 365, // <== 365 days
|
||||
},
|
||||
cacheableResponse: {
|
||||
statuses: [0, 200],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
urlPattern: /^https:\/\/fonts\.gstatic\.com\/.*/i,
|
||||
handler: "CacheFirst",
|
||||
options: {
|
||||
cacheName: "gstatic-fonts-cache",
|
||||
expiration: {
|
||||
maxEntries: 10,
|
||||
maxAgeSeconds: 60 * 60 * 24 * 365, // <== 365 days
|
||||
},
|
||||
cacheableResponse: {
|
||||
statuses: [0, 200],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
devOptions: {
|
||||
enabled: true,
|
||||
suppressWarnings: false,
|
||||
},
|
||||
} satisfies PwaOptions;
|
49
.vitepress/routes/index.post.ts
Normal file
49
.vitepress/routes/index.post.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { fetcher } from "itty-fetcher";
|
||||
import { FeedbackSchema } from "../types/Feedback";
|
||||
|
||||
const feedbackOptions = [
|
||||
{ label: "🐞 Bug", value: "bug" },
|
||||
{
|
||||
label: "♻️ Suggestion",
|
||||
value: "suggestion",
|
||||
},
|
||||
{ label: "📂 Other", value: "other" },
|
||||
{
|
||||
label: "❤️ Appreciation",
|
||||
value: "appreciate",
|
||||
},
|
||||
];
|
||||
|
||||
function getFeedbackOption(value: string) {
|
||||
return feedbackOptions.find((option) => option.value === value);
|
||||
}
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const { message, page, contact, type } = await readValidatedBody(event, FeedbackSchema.parse);
|
||||
const env = useRuntimeConfig(event);
|
||||
|
||||
if (!["bug", "suggestion", "other", "appreciate"].includes(type!) || !message)
|
||||
throw new Error("Invalid input.");
|
||||
|
||||
let description = `${message}\n\n`;
|
||||
if (contact) description += `**Contact:** ${contact}`;
|
||||
if (page) description += `**Page:** \`${page}\``;
|
||||
|
||||
await fetcher()
|
||||
.post(env.WEBHOOK_URL, {
|
||||
username: "Feedback",
|
||||
avatar_url: "https://i.kym-cdn.com/entries/icons/facebook/000/043/403/cover3.jpg",
|
||||
embeds: [
|
||||
{
|
||||
color: 3447003,
|
||||
title: getFeedbackOption(type).label,
|
||||
description: description,
|
||||
},
|
||||
],
|
||||
})
|
||||
.catch((error) => {
|
||||
throw new Error(error);
|
||||
});
|
||||
|
||||
return { status: "ok" };
|
||||
});
|
3
.vitepress/routes/test.ts
Normal file
3
.vitepress/routes/test.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default eventHandler(() => {
|
||||
return { nitro: "works" };
|
||||
});
|
19
.vitepress/theme/Layout.vue
Normal file
19
.vitepress/theme/Layout.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import DefaultTheme from "vitepress/theme";
|
||||
import Sidebar from "./components/SidebarCard.vue";
|
||||
import Announcement from "./components/Announcement.vue";
|
||||
|
||||
const { Layout } = DefaultTheme;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Layout>
|
||||
<template #sidebar-nav-after>
|
||||
<Sidebar />
|
||||
</template>
|
||||
<template #home-hero-prelink>
|
||||
<Announcement />
|
||||
</template>
|
||||
<Content />
|
||||
</Layout>
|
||||
</template>
|
15
.vitepress/theme/components/Announcement.vue
Normal file
15
.vitepress/theme/components/Announcement.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { useData } from "vitepress";
|
||||
|
||||
const { frontmatter } = useData();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a
|
||||
v-if="frontmatter.hero.prelink"
|
||||
:href="frontmatter.hero.prelink.link"
|
||||
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">
|
||||
{{ frontmatter.hero.prelink.title }}
|
||||
</a>
|
||||
</template>
|
16
.vitepress/theme/components/CardField.vue
Normal file
16
.vitepress/theme/components/CardField.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
icon: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center mb-[8px] g-[12px]">
|
||||
<span class="flex items-center">
|
||||
<div class="text-2xl" :class="icon" />
|
||||
<div class="ml-2 text-sm text-[var(--vp-c-text-2)]">
|
||||
<slot />
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
199
.vitepress/theme/components/Feedback.vue
Normal file
199
.vitepress/theme/components/Feedback.vue
Normal file
@@ -0,0 +1,199 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from "vue";
|
||||
import { useRoute } from "vitepress";
|
||||
import type { FeedbackType } from "../../types/Feedback";
|
||||
|
||||
const loading = ref<boolean>(false);
|
||||
const error = ref<unknown>(null);
|
||||
const success = ref<boolean>(false);
|
||||
const { path } = useRoute();
|
||||
|
||||
const feedback = reactive<FeedbackType>({ message: "", contact: "" });
|
||||
|
||||
const feedbackOptions = [
|
||||
{ label: "🐞 Bug", value: "bug" },
|
||||
{
|
||||
label: "♻️ Suggestion",
|
||||
value: "suggestion",
|
||||
},
|
||||
{ label: "📂 Other", value: "other" },
|
||||
{
|
||||
label: "❤️ Appreciation",
|
||||
value: "appreciate",
|
||||
},
|
||||
];
|
||||
|
||||
function getFeedbackOption(value: string) {
|
||||
return feedbackOptions.find((option) => option.value === value);
|
||||
}
|
||||
|
||||
async function handleSubmit(type?: FeedbackType["type"]) {
|
||||
if (type) feedback.type = type;
|
||||
loading.value = true;
|
||||
|
||||
const body: FeedbackType = {
|
||||
message: feedback.message,
|
||||
type: feedback.type,
|
||||
contact: feedback.contact,
|
||||
page: path,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch("https://feedback.tasky.workers.dev", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.error) {
|
||||
error.value = data.error;
|
||||
return;
|
||||
}
|
||||
if (data.status === "success") {
|
||||
success.value = true;
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = err;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="wrapper">
|
||||
<Transition name="fade" mode="out-in">
|
||||
<div v-if="!feedback.type" class="step">
|
||||
<div>
|
||||
<div>
|
||||
<p class="heading">Feedback</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="button-container">
|
||||
<button v-for="item in feedbackOptions" :key="item.value" class="btn"
|
||||
@click="handleSubmit(item.value as FeedbackType['type'])">
|
||||
<span>{{ item.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="feedback.type && !success" class="step">
|
||||
<div>
|
||||
<p class="desc">The wiki is... • {{ path }}</p>
|
||||
<div>
|
||||
<span>{{ getFeedbackOption(feedback.type)?.label }}</span>
|
||||
<button style="margin-left: 0.5rem" class="btn" @click="feedback.type = undefined">
|
||||
<span class="i-carbon-close-large">close</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<textarea v-model="feedback.message" autofocus class="input" />
|
||||
<p class="desc">Contacts, so we can get back to you. (Optional)</p>
|
||||
<textarea v-model="feedback.contact" class="contact-input" />
|
||||
<button type="submit" class="btn btn-primary" :disabled="feedback.message.length > 10" @click="handleSubmit()">
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
<div v-else class="step">
|
||||
<p class="heading">Thanks for your feedback!</p>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.step>*+* {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background-color: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
transition:
|
||||
border-color 0.25s,
|
||||
background-color 0.25s;
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
padding: 0.375rem 0.75rem;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
color: #fff;
|
||||
background-color: var(--vp-c-brand);
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: var(--vp-c-brand-darker);
|
||||
border-color: var(--vp-c-brand-darker);
|
||||
}
|
||||
|
||||
.heading {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
display: grid;
|
||||
grid-gap: 0.5rem;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
margin: 2rem 0;
|
||||
padding: 1.5rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg-alt);
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
padding: 0.375rem 0.75rem;
|
||||
}
|
||||
|
||||
.contact-input {
|
||||
height: 50px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
padding: 0.375rem 0.75rem;
|
||||
}
|
||||
|
||||
.desc {
|
||||
display: block;
|
||||
line-height: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.25s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
47
.vitepress/theme/components/InputField.vue
Normal file
47
.vitepress/theme/components/InputField.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
label: string;
|
||||
id: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="input-field">
|
||||
<div class="input-label" v-if="label">
|
||||
<label :for="id" class="pane-label">
|
||||
{{ label }}
|
||||
</label>
|
||||
<div class="display-value">
|
||||
<slot name="display" />
|
||||
</div>
|
||||
</div>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.pane-label {
|
||||
line-height: 20px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--vt-c-text-1);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.input-field:not(:last-child) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.display-value {
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.input-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
gap: 12px;
|
||||
}
|
||||
</style>
|
78
.vitepress/theme/components/Modal.vue
Normal file
78
.vitepress/theme/components/Modal.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import Feedback from "./Feedback.vue";
|
||||
|
||||
const showModal = ref(false);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button class="p-[4px 8px] text-xl i-carbon:user-favorite-alt-filled" @click="showModal = true" />
|
||||
|
||||
<Teleport to="body">
|
||||
<Transition name="modal">
|
||||
<div v-show="showModal" class="modal-mask">
|
||||
<div class="modal-container">
|
||||
<Feedback />
|
||||
<div class="model-footer">
|
||||
<button class="modal-button" @click="showModal = false">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.modal-mask {
|
||||
position: fixed;
|
||||
z-index: 200;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
width: 300px;
|
||||
margin: auto;
|
||||
padding: 20px 30px;
|
||||
background-color: var(--vp-c-bg);
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.model-footer {
|
||||
margin-top: 8px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.modal-button {
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
border-color: var(--vp-button-alt-border);
|
||||
color: var(--vp-button-alt-text);
|
||||
background-color: var(--vp-button-alt-bg);
|
||||
}
|
||||
|
||||
.modal-button:hover {
|
||||
border-color: var(--vp-button-alt-hover-border);
|
||||
color: var(--vp-button-alt-hover-text);
|
||||
background-color: var(--vp-button-alt-hover-bg);
|
||||
}
|
||||
|
||||
.modal-enter-from,
|
||||
.modal-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.modal-enter-from .modal-container,
|
||||
.modal-leave-to .modal-container {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
</style>
|
60
.vitepress/theme/components/SidebarCard.vue
Normal file
60
.vitepress/theme/components/SidebarCard.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<script setup lang="ts">
|
||||
import Field from "./CardField.vue";
|
||||
import Modal from "./Modal.vue";
|
||||
import InputField from "./InputField.vue";
|
||||
import ToggleStarred from "./ToggleStarred.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-title">Emoji Legend</div>
|
||||
</div>
|
||||
<Field icon="i-twemoji-star">Recommendations</Field>
|
||||
<Field icon="i-twemoji-globe-with-meridians">Indexes</Field>
|
||||
<Field icon="i-twemoji-repeat-button">Storage Links</Field>
|
||||
<div class="card-header">
|
||||
<div class="card-title">Options</div>
|
||||
</div>
|
||||
<InputField id="feedback" label="Feedback">
|
||||
<template #display>
|
||||
<Modal />
|
||||
</template>
|
||||
</InputField>
|
||||
<InputField id="toggle-starred" label="Toggle Starred">
|
||||
<template #display>
|
||||
<ToggleStarred />
|
||||
</template>
|
||||
</InputField>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-text-1);
|
||||
line-height: 32px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--vp-c-bg);
|
||||
padding: 12px 24px 24px;
|
||||
border-radius: 12px;
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
border: 1px solid transparent;
|
||||
transition: border-color 0.4s ease-in-out;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
border-color: var(--vp-c-brand-1);
|
||||
}
|
||||
</style>
|
52
.vitepress/theme/components/Switch.vue
Normal file
52
.vitepress/theme/components/Switch.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import { Switch } from "@headlessui/vue";
|
||||
|
||||
const enabled = ref(false);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Switch v-model="enabled" class="switch" :class="{ enabled }">
|
||||
<span class="thumb" />
|
||||
</Switch>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.switch {
|
||||
display: inline-flex;
|
||||
position: relative;
|
||||
width: 40px;
|
||||
height: 22px;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid var(--vp-input-border-color);
|
||||
background-color: var(--vp-input-switch-bg-color);
|
||||
transition:
|
||||
border-color 0.25s,
|
||||
background-color 0.4s ease;
|
||||
border-radius: 11px;
|
||||
}
|
||||
|
||||
.switch.enabled {
|
||||
background-color: var(--vp-c-brand);
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.switch:hover {
|
||||
border-color: var(--vp-input-hover-border-color);
|
||||
}
|
||||
|
||||
.thumb {
|
||||
display: inline-block;
|
||||
background-color: #fff;
|
||||
transition: transform 0.25s;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
box-shadow: var(--vp-shadow-1);
|
||||
}
|
||||
|
||||
.switch.enabled .thumb {
|
||||
transform: translateX(18px);
|
||||
}
|
||||
</style>
|
53
.vitepress/theme/components/ThemeSwitch.vue
Normal file
53
.vitepress/theme/components/ThemeSwitch.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<script setup lang="ts">
|
||||
import { inject } from "vue";
|
||||
import { useData } from "vitepress";
|
||||
|
||||
import VPIconMoon from "vitepress/dist/client/theme-default/components/icons/VPIconMoon.vue";
|
||||
import VPIconSun from "vitepress/dist/client/theme-default/components/icons/VPIconSun.vue";
|
||||
|
||||
const { isDark } = useData();
|
||||
|
||||
const toggleAppearance = inject("toggle-appearance", () => {
|
||||
isDark.value = !isDark.value;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
title="Toggle dark mode"
|
||||
class="VPSwitchAppearance"
|
||||
:aria-checked="isDark"
|
||||
@click="toggleAppearance">
|
||||
<ClientOnly>
|
||||
<Transition name="fade" mode="out-in">
|
||||
<VPIconSun v-if="!isDark" class="sun" />
|
||||
<VPIconMoon v-else class="moon" />
|
||||
</Transition>
|
||||
</ClientOnly>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPSwitchAppearance {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
color: var(--vp-c-text-2);
|
||||
transition: color 0.5s;
|
||||
|
||||
&:hover {
|
||||
color: var(--vp-c-text-1);
|
||||
transition: color 0.25s;
|
||||
}
|
||||
|
||||
& > :deep(svg) {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
fill: currentColor;
|
||||
}
|
||||
}
|
||||
</style>
|
15
.vitepress/theme/components/ToggleStarred.vue
Normal file
15
.vitepress/theme/components/ToggleStarred.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import Switch from "./Switch.vue";
|
||||
|
||||
const toggleStarred = () => document.documentElement.classList.toggle("starred-only");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Switch @click="toggleStarred()" />
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.starred-only li:not(.starred) {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
13
.vitepress/theme/index.ts
Normal file
13
.vitepress/theme/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { type Theme, inBrowser } from "vitepress";
|
||||
import DefaultTheme from "vitepress/theme";
|
||||
import Layout from "./Layout.vue";
|
||||
import "./style.css";
|
||||
import "uno.css";
|
||||
|
||||
if (inBrowser) import("./pwa");
|
||||
|
||||
export default {
|
||||
extends: DefaultTheme,
|
||||
Layout: Layout,
|
||||
enhanceApp({ app, router, siteData }) {},
|
||||
} satisfies Theme;
|
3
.vitepress/theme/pwa.ts
Normal file
3
.vitepress/theme/pwa.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { registerSW } from "virtual:pwa-register";
|
||||
|
||||
registerSW({ immediate: true });
|
145
.vitepress/theme/style.css
Normal file
145
.vitepress/theme/style.css
Normal file
@@ -0,0 +1,145 @@
|
||||
:root {
|
||||
/* Colors: Brand */
|
||||
--vp-c-brand-1: #7bc5e4;
|
||||
--vp-c-brand-2: #c4e2f2;
|
||||
--vp-c-brand-3: #4882a7;
|
||||
--vp-c-brand-soft: #a4d5ec;
|
||||
|
||||
/* Colors: Button */
|
||||
--vp-button-brand-bg: var(--vp-c-brand-1);
|
||||
--vp-button-brand-border: var(--vp-c-brand-soft);
|
||||
--vp-button-brand-text: var(--vp-c-white);
|
||||
--vp-button-brand-hover-border: var(--vp-c-brand-soft);
|
||||
--vp-button-brand-hover-text: var(--vp-c-white);
|
||||
--vp-button-brand-hover-bg: var(--vp-c-brand-soft);
|
||||
--vp-button-brand-active-border: var(--vp-c-brand-soft);
|
||||
--vp-button-brand-active-text: var(--vp-c-white);
|
||||
--vp-button-brand-active-bg: var(--vp-button-brand-bg);
|
||||
--vp-button-alt-bg: #484848;
|
||||
--vp-button-alt-text: #f0eeee;
|
||||
--vp-button-alt-hover-bg: #484848;
|
||||
--vp-button-alt-hover-text: #f0eeee;
|
||||
|
||||
--vp-c-bg-elv: rgba(255, 255, 255, 0.7);
|
||||
--vp-c-bg-mark: rgb(232, 232, 232);
|
||||
|
||||
/* Colors: Custom Block */
|
||||
--vp-custom-block-info-bg: rgba(171, 210, 244, 0.05);
|
||||
--vp-custom-block-info-border: #60c4fa;
|
||||
--vp-custom-block-info-text: rgb(39, 115, 145);
|
||||
--vp-custom-block-info-text-deep: rgb(18, 121, 162);
|
||||
--vp-custom-block-tip-bg: rgba(137, 202, 176, 0.05);
|
||||
--vp-custom-block-tip-border: rgba(34, 197, 94, 1);
|
||||
--vp-custom-block-tip-text: rgb(10, 128, 90);
|
||||
--vp-custom-block-tip-text-deep: rgb(11, 133, 94);
|
||||
--vp-custom-block-warning-bg: rgba(250, 204, 21, 0.05);
|
||||
--vp-custom-block-warning-border: rgba(245, 158, 11, 1);
|
||||
--vp-custom-block-warning-text: rgb(166, 114, 35);
|
||||
--vp-custom-block-warning-text-deep: rgb(199, 109, 6);
|
||||
--vp-custom-block-danger-bg: rgba(220, 38, 38, 0.05);
|
||||
--vp-custom-block-danger-border: rgba(248, 113, 113, 1);
|
||||
--vp-custom-block-danger-text: rgb(196, 46, 46);
|
||||
--vp-custom-block-danger-text-deep: rgba(220, 38, 38, 1);
|
||||
}
|
||||
|
||||
.dark {
|
||||
/* Colors: Background */
|
||||
--vp-c-bg: rgb(26, 26, 26);
|
||||
--vp-c-bg-alt: rgb(23, 23, 23);
|
||||
--vp-c-bg-elv: rgba(23, 23, 23, 0.8);
|
||||
|
||||
/* Colors: Custom Block */
|
||||
--vp-custom-block-info-bg: rgba(84, 110, 155, 0.1);
|
||||
--vp-custom-block-info-border: #3686b1;
|
||||
--vp-custom-block-info-text: #52b0e3;
|
||||
--vp-custom-block-info-text-deep: #00b7ff;
|
||||
--vp-custom-block-tip-bg: rgba(51, 130, 118, 0.1);
|
||||
--vp-custom-block-tip-border: rgba(4, 120, 87, 1);
|
||||
--vp-custom-block-tip-text: rgb(25, 190, 129);
|
||||
--vp-custom-block-tip-text-deep: rgba(52, 211, 153, 1);
|
||||
--vp-custom-block-warning-bg: rgba(253, 224, 71, 0.1);
|
||||
--vp-custom-block-warning-border: rgba(202, 138, 4, 1);
|
||||
--vp-custom-block-warning-text: rgba(234, 179, 8, 1);
|
||||
--vp-custom-block-warning-text-deep: rgba(250, 204, 21, 1);
|
||||
--vp-custom-block-danger-bg: rgba(239, 68, 68, 0.1);
|
||||
--vp-custom-block-danger-border: rgba(127, 29, 29, 1);
|
||||
--vp-custom-block-danger-text: rgba(248, 113, 113, 1);
|
||||
--vp-custom-block-danger-text-deep: rgba(248, 113, 113, 1);
|
||||
}
|
||||
|
||||
.vp-doc a {
|
||||
color: var(--vp-c-brand-1);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 4px;
|
||||
text-decoration-style: solid;
|
||||
text-decoration-color: transparent;
|
||||
-webkit-text-decoration-color: transparent;
|
||||
transition: text-decoration-color 0.25s;
|
||||
}
|
||||
|
||||
.vp-doc a:hover {
|
||||
color: var(--vp-c-brand-1);
|
||||
text-decoration-color: var(--vp-c-brand-1);
|
||||
-webkit-text-decoration-color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.vp-doc .custom-block a {
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 4px;
|
||||
text-decoration-style: solid;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background-color: var(--vp-button-brand-bg);
|
||||
}
|
||||
|
||||
.VPFooter a {
|
||||
text-decoration-line: underline;
|
||||
text-decoration-style: dashed;
|
||||
text-underline-offset: 5px;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
.VPFooter a:hover {
|
||||
color: var(--vp-c-text-1);
|
||||
text-decoration-line: underline;
|
||||
text-decoration-style: dashed;
|
||||
text-underline-offset: 5px;
|
||||
}
|
||||
|
||||
/* Custom scrollbar */
|
||||
.VPSidebar::-webkit-scrollbar {
|
||||
block-size: 4px;
|
||||
border-end-end-radius: 14px;
|
||||
border-start-end-radius: 14px;
|
||||
inline-size: 4px;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component: Home
|
||||
*/
|
||||
:root {
|
||||
--vp-home-hero-name-color: transparent;
|
||||
--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-filter: blur(44px);
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
:root {
|
||||
--vp-home-hero-image-filter: blur(56px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
:root {
|
||||
--vp-home-hero-image-filter: blur(68px);
|
||||
}
|
||||
}
|
||||
|
||||
.base64 {
|
||||
min-width: 100%;
|
||||
width: 0px;
|
||||
white-space: pre-wrap;
|
||||
}
|
10
.vitepress/types/Feedback.ts
Normal file
10
.vitepress/types/Feedback.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import z from "zod";
|
||||
|
||||
export const FeedbackSchema = z.object({
|
||||
message: z.string().min(5).max(1000),
|
||||
type: z.enum(["bug", "suggestion", "appreciate", "other"]),
|
||||
contact: z.string().min(5).max(20).optional(),
|
||||
page: z.string().min(3).max(10),
|
||||
});
|
||||
|
||||
export type FeedbackType = z.infer<typeof FeedbackSchema>;
|
4
.vitepress/vue-shim.d.ts
vendored
Normal file
4
.vitepress/vue-shim.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
declare module "*.vue" {
|
||||
const component: import("vue").Component;
|
||||
export default component;
|
||||
}
|
Reference in New Issue
Block a user