feat: feedback v2

This commit is contained in:
taskylizard 2024-08-25 11:00:20 +00:00
parent d17887ca75
commit 9faa251c86
No known key found for this signature in database
GPG key ID: 1820131ED1A24120
26 changed files with 1042 additions and 419 deletions

View file

@ -1,8 +1,8 @@
{
"name": "FMHYedit",
"postStartCommand": "pnpm install",
"postAttachCommand": "pnpm docs:dev",
"appPort": 5173,
"features": {},
"image": "mcr.microsoft.com/devcontainers/universal:2",
"features": {}
"name": "FMHYedit",
"postAttachCommand": "pnpm docs:dev",
"postStartCommand": "pnpm install"
}

View file

@ -1,3 +1,9 @@
# https://github.com/vitejs/vite/issues/17291
[tools]
node = "21"
[tasks]
d = "nrr docs:dev --host"
b = "nrr docs:build"
s = "nrr docs:preview --host"
bb = "nrr docs:build && nrr docs:preview --host"

View file

@ -4,3 +4,6 @@ singleQuote: true
printWidth: 80
trailingComma: none
htmlWhitespaceSensitivity: ignore
plugins:
- prettier-plugin-tailwindcss
- prettier-plugin-pkgsort

View file

@ -5,12 +5,33 @@ import {
} from '../../docs/.vitepress/types/Feedback'
export default defineEventHandler(async (event) => {
const { message, page, type } = await readValidatedBody(
const { message, page, type, heading } = await readValidatedBody(
event,
FeedbackSchema.parseAsync
)
const env = useRuntimeConfig(event)
const fields = [
{
name: 'Page',
value: page,
inline: true
},
{
name: 'Message',
value: message,
inline: false
}
]
if (heading) {
fields.push({
name: 'Section',
value: heading,
inline: true
})
}
// FIXME: somehow this is not working, but it worked before
// const path = 'feedback'
//
@ -21,9 +42,6 @@ export default defineEventHandler(async (event) => {
// })
// }
let description = `${message}\n\n`
if (page) description += `**Page:** \`${page}\``
await fetcher()
.post(env.WEBHOOK_URL, {
username: 'Feedback',
@ -33,7 +51,7 @@ export default defineEventHandler(async (event) => {
{
color: 3447003,
title: getFeedbackOption(type).label,
description
fields
}
]
})

3
api/tsconfig.json Normal file
View file

@ -0,0 +1,3 @@
{
"extends": "../.nitro/types/tsconfig.json"
}

View file

@ -1,8 +1,12 @@
{
"$schema": "https://biomejs.dev/schemas/1.8.3/schema.json",
"extends": ["@taskylizard/biome-config"],
"extends": ["@taskylizard/biome-config", "./docs/.vitepress/.imports.json"],
"files": {
"ignore": ["docs/.vitepress/**/*.vue", "docs/.vitepress/vue-shim.d.ts"]
"ignore": [
"docs/.vitepress/**/*.vue",
"docs/.vitepress/vue-shim.d.ts",
"docs/.vitepress/imports.d.ts"
]
},
"formatter": {
"enabled": false
@ -19,12 +23,7 @@
"linter": {
"rules": {
"style": {
"useFilenamingConvention": {
"level": "info",
"options": {
"filenameCases": ["camelCase", "PascalCase"]
}
},
"useFilenamingConvention": "off",
"noDefaultExport": "off"
}
}

View file

@ -0,0 +1,73 @@
{
"javascript": {
"globals": [
"Component",
"ComponentPublicInstance",
"ComputedRef",
"EffectScope",
"ExtractDefaultPropTypes",
"ExtractPropTypes",
"ExtractPublicPropTypes",
"InjectionKey",
"PropType",
"Ref",
"VNode",
"WritableComputedRef",
"computed",
"createApp",
"customRef",
"defineAsyncComponent",
"defineComponent",
"effectScope",
"getCurrentInstance",
"getCurrentScope",
"h",
"inject",
"isProxy",
"isReactive",
"isReadonly",
"isRef",
"markRaw",
"nextTick",
"onActivated",
"onBeforeMount",
"onBeforeUnmount",
"onBeforeUpdate",
"onDeactivated",
"onErrorCaptured",
"onMounted",
"onRenderTracked",
"onRenderTriggered",
"onScopeDispose",
"onServerPrefetch",
"onUnmounted",
"onUpdated",
"provide",
"reactive",
"readonly",
"ref",
"resolveComponent",
"shallowReactive",
"shallowReadonly",
"shallowRef",
"toRaw",
"toRef",
"toRefs",
"toValue",
"triggerRef",
"unref",
"useAttrs",
"useCssModule",
"useCssVars",
"useData",
"useRoute",
"useRouter",
"useSlots",
"watch",
"watchEffect",
"watchPostEffect",
"watchSyncEffect",
"withBase"
]
}
}

View file

@ -12,7 +12,9 @@ import {
import { generateFeed, generateImages, generateMeta } from './hooks'
import { defs, emojiRender, movePlugin } from './markdown/emoji'
import { toggleStarredPlugin } from './markdown/toggleStarred'
import { headersPlugin } from './markdown/headers'
import { transforms } from './transformer'
import AutoImport from 'unplugin-auto-import/vite'
// @unocss-include
@ -56,6 +58,15 @@ export default defineConfig({
UnoCSS({
configFile: '../unocss.config.ts'
}),
AutoImport({
dts: './.vitepress/imports.d.ts',
imports: ['vue', 'vitepress'],
vueTemplate: true,
biomelintrc: {
enabled: true,
filepath: './docs/.vitepress/.imports.json'
}
}),
transforms(),
{
name: 'custom:adjust-order',
@ -85,6 +96,7 @@ export default defineConfig({
config(md) {
md.use(emojiRender)
md.use(toggleStarredPlugin)
md.use(headersPlugin)
}
},
themeConfig: {

View file

@ -71,14 +71,14 @@ async function generateImage({
const _page = getPage(url)
const title =
frontmatter.layout === 'home'
? frontmatter.hero.name ?? frontmatter.title
? (frontmatter.hero.name ?? frontmatter.title)
: frontmatter.title
? frontmatter.title
: _page?.title
const description =
frontmatter.layout === 'home'
? frontmatter.hero.tagline ?? frontmatter.description
? (frontmatter.hero.tagline ?? frontmatter.description)
: frontmatter.description
? frontmatter.description
: _page?.description

133
docs/.vitepress/imports.d.ts vendored Normal file
View file

@ -0,0 +1,133 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
export {}
declare global {
const EffectScope: typeof import('vue')['EffectScope']
const computed: typeof import('vue')['computed']
const createApp: typeof import('vue')['createApp']
const customRef: typeof import('vue')['customRef']
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
const defineComponent: typeof import('vue')['defineComponent']
const effectScope: typeof import('vue')['effectScope']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope']
const h: typeof import('vue')['h']
const inject: typeof import('vue')['inject']
const isProxy: typeof import('vue')['isProxy']
const isReactive: typeof import('vue')['isReactive']
const isReadonly: typeof import('vue')['isReadonly']
const isRef: typeof import('vue')['isRef']
const markRaw: typeof import('vue')['markRaw']
const nextTick: typeof import('vue')['nextTick']
const onActivated: typeof import('vue')['onActivated']
const onBeforeMount: typeof import('vue')['onBeforeMount']
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
const onDeactivated: typeof import('vue')['onDeactivated']
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
const onMounted: typeof import('vue')['onMounted']
const onRenderTracked: typeof import('vue')['onRenderTracked']
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
const onScopeDispose: typeof import('vue')['onScopeDispose']
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: typeof import('vue')['onUpdated']
const provide: typeof import('vue')['provide']
const reactive: typeof import('vue')['reactive']
const readonly: typeof import('vue')['readonly']
const ref: typeof import('vue')['ref']
const resolveComponent: typeof import('vue')['resolveComponent']
const shallowReactive: typeof import('vue')['shallowReactive']
const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef']
const toRaw: typeof import('vue')['toRaw']
const toRef: typeof import('vue')['toRef']
const toRefs: typeof import('vue')['toRefs']
const toValue: typeof import('vue')['toValue']
const triggerRef: typeof import('vue')['triggerRef']
const unref: typeof import('vue')['unref']
const useAttrs: typeof import('vue')['useAttrs']
const useCssModule: typeof import('vue')['useCssModule']
const useCssVars: typeof import('vue')['useCssVars']
const useData: typeof import('vitepress')['useData']
const useRoute: typeof import('vitepress')['useRoute']
const useRouter: typeof import('vitepress')['useRouter']
const useSlots: typeof import('vue')['useSlots']
const watch: typeof import('vue')['watch']
const watchEffect: typeof import('vue')['watchEffect']
const watchPostEffect: typeof import('vue')['watchPostEffect']
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
const withBase: typeof import('vitepress')['withBase']
}
// for type re-export
declare global {
// @ts-ignore
export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
import('vue')
}
// for vue template auto import
import { UnwrapRef } from 'vue'
declare module 'vue' {
interface GlobalComponents {}
interface ComponentCustomProperties {
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
readonly computed: UnwrapRef<typeof import('vue')['computed']>
readonly createApp: UnwrapRef<typeof import('vue')['createApp']>
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']>
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
readonly h: UnwrapRef<typeof import('vue')['h']>
readonly inject: UnwrapRef<typeof import('vue')['inject']>
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
readonly onBeforeUnmount: UnwrapRef<typeof import('vue')['onBeforeUnmount']>
readonly onBeforeUpdate: UnwrapRef<typeof import('vue')['onBeforeUpdate']>
readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']>
readonly onErrorCaptured: UnwrapRef<typeof import('vue')['onErrorCaptured']>
readonly onMounted: UnwrapRef<typeof import('vue')['onMounted']>
readonly onRenderTracked: UnwrapRef<typeof import('vue')['onRenderTracked']>
readonly onRenderTriggered: UnwrapRef<typeof import('vue')['onRenderTriggered']>
readonly onScopeDispose: UnwrapRef<typeof import('vue')['onScopeDispose']>
readonly onServerPrefetch: UnwrapRef<typeof import('vue')['onServerPrefetch']>
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
readonly provide: UnwrapRef<typeof import('vue')['provide']>
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
readonly readonly: UnwrapRef<typeof import('vue')['readonly']>
readonly ref: UnwrapRef<typeof import('vue')['ref']>
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
readonly toRaw: UnwrapRef<typeof import('vue')['toRaw']>
readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
readonly toValue: UnwrapRef<typeof import('vue')['toValue']>
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
readonly unref: UnwrapRef<typeof import('vue')['unref']>
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']>
readonly useData: UnwrapRef<typeof import('vitepress')['useData']>
readonly useRoute: UnwrapRef<typeof import('vitepress')['useRoute']>
readonly useRouter: UnwrapRef<typeof import('vitepress')['useRouter']>
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
readonly watch: UnwrapRef<typeof import('vue')['watch']>
readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']>
readonly watchSyncEffect: UnwrapRef<typeof import('vue')['watchSyncEffect']>
readonly withBase: UnwrapRef<typeof import('vitepress')['withBase']>
}
}

View file

@ -0,0 +1,16 @@
import type { MarkdownRenderer } from 'vitepress'
import { headers } from '../transformer/constants'
const titles = Object.keys(headers).map((key) => headers[key].title)
export const headersPlugin = (md: MarkdownRenderer) => {
// Add the Feedback component after the heading and close the container
md.renderer.rules.heading_close = (tokens, idx, options, env, self) => {
const result = self.renderToken(tokens, idx, options)
const heading = tokens[idx - 1]
const level = tokens[idx].tag.slice(1)
if (!titles.includes(env.frontmatter.title) || level !== '2') return result
return `<Feedback heading="${heading.content}" />${result}`
}
}

View file

@ -1,7 +1,5 @@
<script setup lang="ts">
import DefaultTheme from 'vitepress/theme'
import { useData } from 'vitepress'
import { nextTick, provide } from 'vue'
import Sidebar from './components/SidebarCard.vue'
import Announcement from './components/Announcement.vue'

View file

@ -1,5 +1,4 @@
<script setup lang="ts">
import { useData } from 'vitepress'
import Authors from './components/Authors.vue'
const props = defineProps<{

View file

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

View file

@ -40,8 +40,8 @@ const authors = computed(() =>
<template>
<div class="flex flex-wrap gap-4 pt-2">
<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" />
<div v-for="(c, index) of authors" class="flex items-center gap-2">
<img :src="`${c.github}.png`" class="h-8 w-8 rounded-full" />
<a :href="c.github">{{ c.name }}</a>
<span v-if="index < authors.length - 1"></span>
</div>

View file

@ -5,7 +5,7 @@ defineProps<{
</script>
<template>
<div class="flex items-center mb-[8px] g-[12px]">
<div class="g-[12px] mb-[8px] flex items-center">
<span class="flex items-center">
<div class="text-2xl" :class="icon" />
<div class="ml-2 text-sm text-[var(--vp-c-text-2)]">

View file

@ -1,53 +1,100 @@
<script setup lang="ts">
import { ref, reactive } from 'vue'
import {
TransitionRoot,
TransitionChild,
Dialog,
DialogPanel,
DialogTitle,
DialogDescription,
Listbox,
ListboxLabel,
ListboxButton,
ListboxOptions,
ListboxOption
} from '@headlessui/vue'
import { useRouter } from 'vitepress'
import { computed, ref, reactive } from 'vue'
import {
feedbackOptions,
type FeedbackType,
getFeedbackOption,
feedbackOptions
getFeedbackOption
} from '../../types/Feedback'
import { useRouter } from 'vitepress'
const props = defineProps<{
heading?: string
}>()
const prompts = [
'Make it count!',
'Leave some feedback for us!',
`We're all ears 🐰`,
'Tell us what is missing in FMHY',
'Your thoughts matter to us 💡',
'Feedback is a gift 🎁',
'What do you think?',
'We appreciate your support 🙏',
'Help us make FMHY better 🤝',
'We need your help 👋',
'Your feedback is valuable 💯',
'So... what do you think?',
"I guess you don't need to say anything 😉",
'Spill the beans 💣',
"We're always looking for ways to improve!.",
'Your feedback is valuable and helps us make FMHY better.',
'aliens are watching you 👽',
'tasky was here 👀',
'The internet is full of crap 😱'
]
function getPrompt() {
return prompts[Math.floor(Math.random() * prompts.length)]
}
const messages = {
bug: [
"We're sorry to hear that!",
'Please try to be as specific as possible and provide us with the steps to reproduce the bug.'
],
suggestion: [
"We're glad you want to share your ideas!",
'Nix the fluff and just tell us what you think!',
"We'll be happy to read your thoughts and incorporate them into our wiki.",
"Hello! We're glad you want to share your ideas!"
],
appreciation: [
'We appreciate your support!',
"We're always looking for ways to improve!.",
'Your feedback is valuable and helps us make FMHY better.'
],
other: [
"We're always looking for ways to improve!",
'Your feedback is valuable and helps us make FMHY better.'
]
}
function getMessage(type: FeedbackType['type']) {
return messages[type][Math.floor(Math.random() * messages[type].length)]
}
const loading = ref<boolean>(false)
const error = ref<unknown>(null)
const success = ref<boolean>(false)
const isDisabled = computed(() => {
return (
!feedback.message.length ||
feedback.message.length < 5 ||
feedback.message.length > 1000
)
})
const router = useRouter()
const feedback = reactive<FeedbackType>({ message: '' })
// prettier-ignore
const feedback = reactive<
Pick<FeedbackType, 'message' | 'page'> & Partial<Pick<FeedbackType, 'type'>>
>({
page: router.route.path,
message: ''
})
const options = [
{
label: '💡 Suggestion',
value: 'suggestion'
},
{
label: '❤️ Appreciation',
value: 'appreciate'
},
{ label: '🐞 Bug', value: 'bug' },
{ label: '📂 Other', value: 'other' }
]
const selectedOption = ref(options[0])
const selectedOption = ref(feedbackOptions[0])
async function handleSubmit() {
async function handleSubmit(type?: FeedbackType['type']) {
if (type) feedback.type = type
loading.value = true
const body: FeedbackType = {
message: feedback.message,
type: selectedOption.value.value,
page: router.route.path
page: feedback.page,
...(props.heading && { heading: props.heading })
}
try {
@ -60,7 +107,6 @@ async function handleSubmit() {
})
const data = await response.json()
if (data.error) {
error.value = data.error
return
@ -68,262 +114,214 @@ async function handleSubmit() {
if (data.status === 'ok') {
success.value = true
}
} catch (error) {
error.value = error
} catch (err) {
error.value = err
} finally {
loading.value = false
}
}
const isOpen = ref(false)
const isCardShown = ref<boolean>(false)
const helpfulText = props.heading
? 'What do you think about this section?'
: 'What do you think about this page?'
const helpfulDescription = props.heading
? 'Let us know how helpful this section is.'
: 'Let us know how helpful this page is.'
function closeModal() {
isOpen.value = false
}
function openModal() {
isOpen.value = true
}
const prompt = computed(() => getPrompt())
const message = computed(() => getMessage(feedback.type!))
const toggleCard = () => (isCardShown.value = !isCardShown.value)
</script>
<template>
<button
type="button"
class="inline-flex items-center justify-center whitespace-nowrap text-sm text-primary font-medium border border-primary bg-bg-alt h-8 rounded-md px-2 py-2"
@click="openModal"
>
<span class="i-carbon-send-alt" />
</button>
<template v-if="props.heading">
<button @click="toggleCard()"
class="bg-$vp-c-default-soft hover:bg-$vp-c-default-soft/40 text-primary border-$vp-c-default-soft hover:border-primary ml-3 inline-flex h-7 items-center justify-center whitespace-nowrap rounded-md border-2 border-solid px-1.5 py-1.5 text-sm font-medium transition-all duration-300 sm:h-6">
<span :class="isCardShown === false
? `i-lucide:heart-handshake`
: `i-lucide:circle-x`
" />
</button>
</template>
<template v-else>
<button
class="bg-$vp-c-default-soft hover:bg-$vp-c-default-soft/40 text-primary px2 py1 border-$vp-c-default-soft hover:border-primary mt-2 select-none rounded border-2 border-solid font-bold transition-all duration-300"
@click="toggleCard()">
<span :class="isCardShown === false
? `i-lucide:heart-handshake mr-2`
: `i-lucide:circle-x mr-2`
" />
<span>Send Feedback</span>
</button>
</template>
<Teleport to="body">
<TransitionRoot appear :show="isOpen" as="template">
<Dialog as="div" class="relative z-10" @close="closeModal">
<TransitionChild
as="template"
enter="duration-300 ease-out"
enter-from="opacity-0"
enter-to="opacity-100"
leave="duration-200 ease-in"
leave-from="opacity-100"
leave-to="opacity-0"
>
<div class="fixed inset-0 bg-black/25" />
</TransitionChild>
<div class="fixed inset-0 overflow-y-auto">
<div
class="flex min-h-full items-center justify-center p-4 text-center"
>
<TransitionChild
as="template"
enter="duration-300 ease-out"
enter-from="opacity-0 scale-95"
enter-to="opacity-100 scale-100"
leave="duration-200 ease-in"
leave-from="opacity-100 scale-100"
leave-to="opacity-0 scale-95"
>
<DialogPanel
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"
>
Feedback
</DialogTitle>
<div class="mt-4 top-16 w-72" v-if="!success">
<Listbox v-model="selectedOption">
<div class="relative mt-1">
<ListboxButton
class="relative w-full cursor-default rounded-lg bg-bg-alt text-text py-2 pl-3 pr-10 text-left shadow-md focus:outline-none focus-visible:border-indigo-500 focus-visible:ring-2 focus-visible:ring-white/75 focus-visible:ring-offset-2 focus-visible:ring-offset-orange-300 sm:text-sm"
>
<span class="block truncate">
{{ selectedOption.label }}
</span>
<span
class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"
>
<div
class="i-heroicons-solid:chevron-up-down h-5 w-5 text-gray-400"
aria-hidden="true"
/>
</span>
</ListboxButton>
<transition
leave-active-class="transition duration-100 ease-in"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<ListboxOptions
class="absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-bg-alt py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-none sm:text-sm"
>
<ListboxOption
v-slot="{ active, selected }"
v-for="option in options"
:key="option.value"
:value="option"
as="template"
>
<li
:class="[
active ? 'text-primary' : 'text-gray-500',
'relative cursor-default select-none py-2 pl-10 pr-4'
]"
>
<span
:class="[
selected ? 'font-medium' : 'font-normal',
'block truncate'
]"
>
{{ option.label }}
</span>
<span
v-if="selected"
class="absolute inset-y-0 left-0 flex items-center pl-3 text-primary"
>
<div
class="i-heroicons-solid:check h-5 w-5"
aria-hidden="true"
/>
</span>
</li>
</ListboxOption>
</ListboxOptions>
</transition>
</div>
</Listbox>
<div class="mt-2">
<div>
<label class="field-label">Message</label>
<textarea
v-model="feedback.message"
class="mt-2 h-32"
placeholder="What a lovely wiki!"
rows="5"
/>
</div>
</div>
<p class="text-sm text-gray-400 mb-2">
If you want a reply to your feedback, feel free to mention a
contact in the message or join our
<a
class="text-primary font-semibold text-underline"
href="https://discord.gg/Stz6y6NgNg"
>
Discord.
</a>
</p>
<details
v-if="selectedOption.value === 'suggestion'"
class="text-sm text-gray-400"
>
<summary class="mb-2">Submission Guidelines</summary>
<strong>🕹 Emulators</strong>
<p>
They're already on the
<a
class="text-primary font-bold text-underline"
href="https://emulation.gametechwiki.com/index.php/Main_Page"
>
Game Tech Wiki.
</a>
</p>
<strong>🔻 Leeches</strong>
<p>
They're already on the
<a
class="text-primary font-bold text-underline"
href="https://filehostlist.miraheze.org/wiki/Free_Premium_Leeches"
>
File Hosting Wiki.
</a>
</p>
<strong>🐧 Distros</strong>
<p>
They're already on
<a
class="text-primary font-bold text-underline"
href="https://distrowatch.com/"
>
DistroWatch.
</a>
</p>
<strong>🎲 Mining / Betting Sites</strong>
<p>
Don't post anything related to betting, mining, BINs, CCs,
etc.
</p>
<strong>🎮 Multiplayer Game Hacks</strong>
<p>
Don't post any hacks/exploits that give unfair advantages
in multiplayer games.
</p>
</details>
<div class="mt-4">
<button
type="button"
class="inline-flex justify-center rounded-md border border-transparent bg-blue-500 px-4 py-2 text-sm font-medium text-blue-100 hover:bg-blue-600 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:bg-blue-400"
:disabled="
feedback.message.length < 5 ||
feedback.message.length > 1000
"
@click="handleSubmit()"
>
Submit
</button>
<button
type="button"
class="ml-2 inline-flex justify-center rounded-md border border-transparent bg-red-500 px-4 py-2 text-sm font-medium text-red-100 hover:bg-red-600 focus:outline-none focus-visible:ring-2 focus-visible:ring-red-500 focus-visible:ring-offset-2"
@click="closeModal()"
>
Close
</button>
</div>
</div>
<div v-else-if="error">
<div class="text-sm font-medium leading-6 text-text">
Error!
</div>
<details>{{ error }}</details>
</div>
<div v-else>
<TransitionRoot
enter="transition-opacity duration-75"
enter-from="opacity-0"
enter-to="opacity-100"
>
Thanks!
</TransitionRoot>
</div>
</DialogPanel>
</TransitionChild>
<Transition name="fade" mode="out-in">
<div v-if="isCardShown"
class="border-$vp-c-divider bg-$vp-c-bg-alt b-rd-4 m-[2rem 0] step mt-4 border-2 border-solid p-6">
<Transition name="fade" mode="out-in">
<div v-if="!feedback.type" class="step">
<div>
<div>
<p class="desc">{{ prompt }}</p>
<p class="heading">
{{ helpfulText }}
</p>
</div>
</div>
<div class="flex flex-wrap gap-2">
<button v-for="item in feedbackOptions" :key="item.value" class="btn" @click="handleSubmit(item.value)">
<span>{{ item.label }}</span>
</button>
</div>
</div>
</Dialog>
</TransitionRoot>
</Teleport>
<div v-else-if="feedback.type && !success" class="step">
<div>
<p class="desc">
{{ helpfulDescription }}
</p>
<div>
<span>{{ getFeedbackOption(feedback.type)?.label }}</span>
<button style="margin-left: 0.5rem" class="btn" @click="feedback.type = undefined">
<span class="i-lucide:arrow-left-from-line">close</span>
</button>
</div>
</div>
<p class="heading">
{{ message }}
</p>
<div v-if="feedback.type === 'suggestion'" class="mb-2 text-sm">
<details>
<summary>
<span class="i-lucide:shield-alert bg-cerise-400 mb-1 ml-1" />
Do not submit any of the following:
</summary>
<strong>🕹 Emulators</strong>
<p class="desc">
They're already on the
<a class="text-primary text-underline font-bold"
href="https://emulation.gametechwiki.com/index.php/Main_Page">
Game Tech Wiki.
</a>
</p>
<strong>🔻 Leeches</strong>
<p class="desc">
They're already on the
<a class="text-primary text-underline font-bold"
href="https://filehostlist.miraheze.org/wiki/Free_Premium_Leeches">
File Hosting Wiki.
</a>
</p>
<strong>🐧 Distros</strong>
<p class="desc">
They're already on
<a class="text-primary text-underline font-bold" href="https://distrowatch.com/">
DistroWatch.
</a>
</p>
<strong>🎲 Mining / Betting Sites</strong>
<p class="desc">
Don't post anything related to betting, mining, BINs, CCs, etc.
</p>
<strong>🎮 Multiplayer Game Hacks</strong>
<p class="desc">
Don't post any hacks/exploits that give unfair advantages in
multiplayer games.
</p>
</details>
</div>
<textarea v-model="feedback.message" autofocus class="input" placeholder="What a lovely wiki!" />
<p class="desc mb-2">
If you want a reply to your feedback, feel free to mention a contact
in the message or join our
<a class="text-primary text-underline font-semibold" href="https://discord.gg/Stz6y6NgNg">
Discord.
</a>
</p>
<button type="submit" class="btn btn-primary" :disabled="isDisabled" @click="handleSubmit()">
Send Feedback 📩
</button>
</div>
<div v-else class="step">
<p class="heading">Thanks for your feedback!</p>
</div>
</Transition>
</div>
</Transition>
</template>
<style scoped>
textarea,
input {
font-family: var(--vp-font-family-base);
background: var(--vp-c-bg-soft);
font-size: 14px;
border-radius: 4px;
padding: 16px;
width: 100%;
<style scoped lang="css">
.step>*+* {
margin-top: 1rem;
}
&::placeholder {
color: var(--vp-c-text-2) !important;
opacity: 1;
}
.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;
}
.input {
background-color: var(--vp-c-bg-alt);
color: var(--vp-c-text-2);
width: 100%;
height: 100px;
border: 1px solid var(--vp-c-divider);
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>

View file

@ -1,6 +1,5 @@
<script setup lang="ts">
import Field from './CardField.vue'
import Feedback from './Feedback.vue'
import InputField from './InputField.vue'
import ToggleStarred from './ToggleStarred.vue'
</script>
@ -16,11 +15,6 @@ import ToggleStarred from './ToggleStarred.vue'
<div class="card-header">
<div class="card-title">Options</div>
</div>
<InputField id="feedback" label="Feedback">
<template #display>
<Feedback />
</template>
</InputField>
<InputField id="toggle-starred" label="Toggle Starred">
<template #display>
<ToggleStarred />

View file

@ -8,14 +8,14 @@ defineProps<{
</script>
<template>
<div class="rounded-lg border bg-bg-elv text-text shadow-sm w-full max-w-md">
<div class="bg-bg-elv text-text w-full max-w-md rounded-lg border shadow-sm">
<div class="flex flex-col space-y-1.5 p-6 pb-4">
<h4
class="whitespace-nowrap text-2xl font-semibold leading-none tracking-tight"
>
{{ title }}
</h4>
<p class="text-sm text-text-2 font-medium leading-none tracking-tight">
<p class="text-text-2 text-sm font-medium leading-none tracking-tight">
{{ description }}
</p>
</div>
@ -23,12 +23,12 @@ defineProps<{
<img
:src="desktop"
:alt="title"
class="rounded-md object-cover aspect-[3/2]"
class="aspect-[3/2] rounded-md object-cover"
width="1080"
height="1920"
/>
</div>
<div class="items-center p-6 flex gap-2">
<div class="flex items-center gap-2 p-6">
<a :href="mobile" target="_blank">Mobile</a>
<span class="text-text-2"></span>
<a :href="desktop" target="_blank">Desktop</a>

View file

@ -1,7 +1,9 @@
import nprogress, { type NProgress } from 'nprogress'
import type { EnhanceAppContext } from 'vitepress'
export function loadProgress(router: EnhanceAppContext['router']): NProgress {
export function loadProgress(
router: EnhanceAppContext['router']
): NProgress | undefined {
if (typeof window === 'undefined') return
setTimeout(() => {

View file

@ -5,12 +5,14 @@ import Post from './PostLayout.vue'
import { loadProgress } from './composables/nprogress'
import './style.css'
import 'uno.css'
import Feedback from './components/Feedback.vue'
export default {
extends: DefaultTheme,
Layout,
enhanceApp({ router, app }) {
app.component('Post', Post)
app.component('Feedback', Feedback)
loadProgress(router)
}
} satisfies Theme

View file

@ -125,12 +125,14 @@ export function getHeader(id: string) {
const description = '<p class="text-black dark:text-text-2">'
const feedback = '<Feedback />'
const data = headers[id]
let header = '---\n'
header += `title: "${data.title}"\n`
header += `description: ${data.description}\n`
header += '---\n'
header += `${title}${data.title}</h1>\n`
header += `${description}${data.description}</p></div>\n\n`
header += `${description}${data.description}</p></div>\n\n${feedback}\n\n`
return header
}

View file

@ -2,27 +2,34 @@ import z from 'zod'
export const FeedbackSchema = z.object({
message: z.string().min(5).max(1000),
type: z.enum(['bug', 'suggestion', 'appreciate', 'other']),
page: z.string().optional()
type: z.enum(['bug', 'suggestion', 'appreciation', 'other']),
page: z.string().min(3).max(20),
// For heading based feedback
heading: z.string().min(3).max(20).optional()
})
export const feedbackOptions = [
{ label: '🐞 Bug', value: 'bug' },
export interface Option {
label: string
value: FeedbackType['type']
}
export const feedbackOptions: Option[] = [
{
label: '💡 Suggestion',
label: '💡 I have a suggestion',
value: 'suggestion'
},
{ label: '📂 Other', value: 'other' },
{ label: '🐛 I want to report a website bug', value: 'bug' },
{
label: '❤️ Appreciation',
value: 'appreciate'
}
label: '👍 I appreciate the work',
value: 'appreciation'
},
{ label: '📂 Something else', value: 'other' }
]
export function getFeedbackOption(value: string): {
label: string
value: string
} {
export function getFeedbackOption(
value: FeedbackType['type']
): Option | undefined {
return feedbackOptions.find((option) => option.value === value)
}

View file

@ -1,27 +1,26 @@
{
"name": "fmhy",
"type": "module",
"packageManager": "pnpm@9.7.1",
"type": "module",
"scripts": {
"docs:dev": "vitepress dev docs/",
"docs:build": "vitepress build docs/",
"docs:preview": "vitepress preview docs/",
"api:prepare": "nitropack prepare",
"api:dev": "nitropack dev",
"api:build": "nitropack build",
"api:dev": "nitropack dev",
"api:prepare": "nitropack prepare",
"api:preview": "node .output/server/index.mjs",
"og:dev": "x-satori -t ./docs/.vitepress/hooks/Template.vue -c ./.vitepress/hooks/satoriConfig.ts --dev",
"docs:build": "vitepress build docs/",
"docs:dev": "vitepress dev docs/",
"docs:preview": "vitepress preview docs/",
"format": "prettier -w --cache --check .",
"postinstall": "nitropack prepare",
"lint": "biome lint .",
"lint:fix": "biome lint . --write",
"lint:fix:unsafe": "biome lint . --write --unsafe",
"postinstall": "nitropack prepare"
"og:dev": "x-satori -t ./docs/.vitepress/hooks/Template.vue -c ./.vitepress/hooks/satoriConfig.ts --dev"
},
"dependencies": {
"@fmhy/colors": "^0.0.11",
"@headlessui/vue": "^1.7.22",
"@resvg/resvg-js": "^2.6.2",
"vitepress": "npm:@taskylizard/vitepress@1.1.1",
"consola": "^3.2.3",
"feed": "^4.2.2",
"itty-fetcher": "^0.9.4",
@ -30,6 +29,7 @@
"nprogress": "^0.2.0",
"pathe": "^1.1.2",
"unocss": "^0.58.9",
"vitepress": "npm:@taskylizard/vitepress@1.1.1",
"vue": "^3.4.38",
"x-satori": "^0.1.5",
"zod": "^3.23.8"
@ -38,13 +38,17 @@
"@biomejs/biome": "^1.8.3",
"@iconify-json/carbon": "^1.1.37",
"@iconify-json/heroicons-solid": "^1.1.12",
"@iconify-json/lucide": "^1.1.207",
"@iconify-json/mdi": "^1.1.68",
"@iconify-json/twemoji": "^1.1.16",
"@taskylizard/biome-config": "^1.0.1",
"@types/node": "^20.15.0",
"@types/nprogress": "^0.2.3",
"prettier": "^3.3.3",
"wrangler": "^3.72.0"
"prettier-plugin-pkgsort": "^0.2.1",
"prettier-plugin-tailwindcss": "^0.6.6",
"unplugin-auto-import": "^0.18.2",
"wrangler": "^3.68.0"
},
"pnpm": {
"peerDependencyRules": {

491
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,26 @@
{
"extends": "./.nitro/types/tsconfig.json"
"compilerOptions": {
"verbatimModuleSyntax": true,
"baseUrl": ".",
"module": "esnext",
"target": "esnext",
"lib": ["DOM", "ESNext"],
"strict": true,
"jsx": "preserve",
"esModuleInterop": true,
"skipLibCheck": true,
"moduleResolution": "Bundler",
"resolveJsonModule": true,
"noUnusedLocals": true,
"strictNullChecks": true,
"forceConsistentCasingInFileNames": true,
"types": ["vitepress"]
},
"exclude": ["node_modules"],
"include": [
"**/.vitepress/**/*.ts",
"**/.vitepress/**/*.mts",
"**/.vitepress/**/*.vue",
"./docs/.vitepress/imports.d.ts"
]
}