Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/assets/css/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

@source "../../../content/**/*";
@source "../../../node_modules/.c12";
@source "../../../modules/";

@theme static {
--font-sans: 'Public Sans', sans-serif;
Expand Down
5 changes: 0 additions & 5 deletions app/layouts/admin.vue

This file was deleted.

2 changes: 1 addition & 1 deletion app/pages/docs/[...slug].vue
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ function refreshHeading(opened: boolean) {
<UPageBody>
<ContentRenderer v-if="page.body" :value="page" />
<div>
<Feedback :page="page" />
<FeedbackWidget :page="page" />
<USeparator class="mt-6 mb-10">
<div class="flex items-center gap-2 text-sm text-muted">
<UButton
Expand Down
110 changes: 110 additions & 0 deletions modules/feedback/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { addComponentsDir, addImportsDir, addRouteMiddleware, addServerHandler, addServerImportsDir, createResolver, defineNuxtModule, extendPages } from 'nuxt/kit'
import type { FeedbackModuleOptions } from './types'

export default defineNuxtModule<FeedbackModuleOptions>({
meta: {
name: 'feedback',
configKey: 'feedback'
},
defaults: {
adminPath: '/_feedback/admin'
},
async setup(options, nuxt) {
const { resolve } = createResolver(import.meta.url)
const adminPath = options.adminPath!.replace(/\/$/, '')

const adminPassword = process.env.NUXT_FEEDBACK_ADMIN_PASSWORD || ''

nuxt.options.runtimeConfig.feedback = {
adminPassword
}

nuxt.options.runtimeConfig.public.feedback = {
adminPath,
hasPasswordAuth: !!adminPassword
}

nuxt.hook('hub:db:schema:extend', async ({ dialect, paths }) => {
if (dialect === 'sqlite') {
paths.push(resolve('./runtime/server/db/schema.sqlite.ts'))
}
})

addComponentsDir({
path: resolve('./runtime/components'),
prefix: 'Feedback'
})

addImportsDir(resolve('./runtime/composables'))

addServerImportsDir(resolve('./runtime/server/utils'))

addServerHandler({
route: '/api/_feedback',
method: 'get',
handler: resolve('./runtime/server/api/feedback/index.get')
})

addServerHandler({
route: '/api/_feedback',
method: 'post',
handler: resolve('./runtime/server/api/feedback/index.post')
})

addServerHandler({
route: '/api/_feedback/:id',
method: 'delete',
handler: resolve('./runtime/server/api/feedback/[id].delete')
})

addServerHandler({
route: '/api/auth/github',
method: 'get',
handler: resolve('./runtime/server/api/auth/github.get')
})

addServerHandler({
route: '/api/auth/password',
method: 'post',
handler: resolve('./runtime/server/api/auth/password.post')
})

addRouteMiddleware({
name: 'feedback-auth',
path: resolve('./runtime/middleware/auth')
})

addRouteMiddleware({
name: 'feedback-guest',
path: resolve('./runtime/middleware/guest')
})

extendPages((pages) => {
pages.push({
name: 'feedback-admin',
path: adminPath,
file: resolve('./runtime/pages/admin/index.vue')
})

pages.push({
name: 'feedback-admin-login',
path: `${adminPath}/login`,
file: resolve('./runtime/pages/admin/login.vue')
})
})

nuxt.options.routeRules![adminPath] = { ssr: false }
nuxt.options.routeRules![`${adminPath}/**`] = { ssr: false }

nuxt.hook('prepare:types', ({ references }) => {
references.push({
path: resolve('./types.ts')
})
})
}
})

export type { FeedbackModuleOptions } from './types'
export { FEEDBACK_OPTIONS, FEEDBACK_RATINGS, feedbackSchema, feedbackFormSchema } from './types'
export type { FeedbackRating, FeedbackOption, FeedbackInput, FeedbackItem, FeedbackSubmission, PageAnalytic } from './types'
export type { Feedback } from './runtime/shared/types/feedback'
Original file line number Diff line number Diff line change
@@ -1,16 +1,7 @@
<script setup lang="ts">
type PageAnalytic = {
path: string
total: number
positive: number
negative: number
averageScore: number
positivePercentage: number
feedback: any[]
lastFeedback: any
createdAt: Date
updatedAt: Date
}
import { Motion, AnimatePresence } from 'motion-v'
import type { PageAnalytic } from '../../types'
import { FEEDBACK_OPTIONS } from '../../types'

interface Props {
pageAnalytics: PageAnalytic[]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
import { getPaginationRowModel } from '@tanstack/vue-table'
import type { TableColumn, DropdownMenuItem } from '@nuxt/ui'
import { h, resolveComponent } from 'vue'
import type { PageAnalytic } from '../../types'

const UButton = resolveComponent('UButton')
const { user, clear } = useUserSession()
const { public: { feedback } } = useRuntimeConfig()

async function logout() {
await clear()
navigateTo('/admin/login')
navigateTo(`${feedback.adminPath}/login`)
}

const items = computed<DropdownMenuItem[][]>(() => [
Expand Down Expand Up @@ -43,7 +45,7 @@ const items = computed<DropdownMenuItem[][]>(() => [
]
])

const { data: rawFeedback, refresh: refreshFeedback } = await useFetch('/api/feedback')
const { data: rawFeedback, refresh: refreshFeedback } = await useFetch('/api/_feedback')
const { deleteFeedback } = useFeedbackDelete()
const { exportFeedbackData, exportPageAnalytics } = useFeedbackExport()

Expand Down Expand Up @@ -563,9 +565,9 @@ watch(currentPage, () => {
<div class="space-y-3 sm:space-y-4">
<div ref="feedbackContainer" class="space-y-2 sm:space-y-3 max-h-[300px] sm:max-h-[400px] overflow-y-auto">
<FeedbackItem
v-for="(feedback, index) in paginatedFeedback"
v-for="(_feedback, index) in paginatedFeedback"
:key="index"
:feedback="feedback"
:feedback="_feedback"
show-delete
@delete="handleDeleteFeedback"
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<script setup lang="ts">
import type { FeedbackItem } from '../../types'

interface Props {
feedback: FeedbackItem
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { AnimatePresence, MotionConfig, motion } from 'motion-v'
import { FEEDBACK_OPTIONS, feedbackFormSchema } from '../../types'

const props = defineProps<{
page: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
// === Ratings & Score Management ===
import type { Ref } from 'vue'
import type { FeedbackRating, FeedbackItem, FeedbackSubmission, PageAnalytic } from '../../types'
import { FEEDBACK_OPTIONS } from '../../types'

export function useFeedbackRatings() {
const ratingConfig = computed(() => {
return FEEDBACK_OPTIONS.reduce((acc, option) => {
Expand Down Expand Up @@ -43,17 +46,16 @@ export function useFeedbackRatings() {
}
}

// === Data Analysis & Processing ===
export function useFeedbackData(rawFeedback: Ref<FeedbackItem[] | null>) {
export function useFeedbackData(rawFeedback: Ref<Record<string, unknown>[] | null>) {
const { calculateStats } = useFeedbackRatings()
const { filterFeedbackByDateRange } = useDateRange()

const allFeedbackData = computed(() =>
const allFeedbackData = computed<FeedbackItem[]>(() =>
rawFeedback.value?.map(item => ({
...item,
createdAt: new Date(item.createdAt),
updatedAt: new Date(item.updatedAt)
})) || []
createdAt: new Date(item.createdAt as string),
updatedAt: new Date(item.updatedAt as string)
}) as FeedbackItem) || []
)

const feedbackData = computed(() =>
Expand Down Expand Up @@ -98,7 +100,6 @@ export function useFeedbackData(rawFeedback: Ref<FeedbackItem[] | null>) {
}
}

// === Modal Management ===
export function useFeedbackModal() {
const selectedPage = ref<PageAnalytic | null>(null)
const showFeedbackModal = ref(false)
Expand Down Expand Up @@ -146,13 +147,12 @@ export function useFeedbackModal() {
}
}

// === Delete Management ===
export function useFeedbackDelete() {
const toast = useToast()

async function deleteFeedback(id: number): Promise<boolean> {
try {
await $fetch(`/api/feedback/${id}`, {
await $fetch(`/api/_feedback/${id}`, {
method: 'DELETE'
})

Expand Down Expand Up @@ -182,7 +182,6 @@ export function useFeedbackDelete() {
}
}

// === Form Submission ===
interface UseFeedbackFormOptions {
page: {
title: string
Expand Down Expand Up @@ -235,7 +234,7 @@ export function useFeedbackForm(options: UseFeedbackFormOptions) {
}

try {
await $fetch('/api/feedback', {
await $fetch('/api/_feedback', {
method: 'POST',
body: submission
})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import type { FeedbackItem, PageAnalytic } from '../../types'
import { FEEDBACK_OPTIONS } from '../../types'

export function useFeedbackExport() {
const toast = useToast()

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
export default defineNuxtRouteMiddleware(() => {
const { loggedIn } = useUserSession()
const { public: { feedback } } = useRuntimeConfig()

if (!loggedIn.value) {
return navigateTo('/admin/login')
return navigateTo(`${feedback.adminPath}/login`)
}
})
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
export default defineNuxtRouteMiddleware(() => {
const { loggedIn } = useUserSession()
const { public: { feedback } } = useRuntimeConfig()

if (loggedIn.value) {
return navigateTo('/admin')
return navigateTo(feedback.adminPath)
}
})
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script setup lang="ts">
definePageMeta({
layout: 'admin',
middleware: 'auth'
layout: false,
middleware: 'feedback-auth'
})

useSeoMeta({
Expand All @@ -13,5 +13,5 @@ useSeoMeta({
</script>

<template>
<AdminDashboard />
<FeedbackDashboard />
</template>
Loading
Loading