@xk2800/nextjs-template 0.7.1-beta.7 → 0.7.1-beta.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/dashboard/admin/broadcastActions.tsx +148 -0
- package/components/dashboard/admin/confirmSendDialog.tsx +44 -0
- package/components/dashboard/admin/marketingEmailForm.tsx +115 -0
- package/components/dashboard/admin/marketingSection.tsx +67 -0
- package/components/dashboard/admin/subscribersCard.tsx +62 -0
- package/components/dashboard/marketingUpdatesCard.tsx +62 -0
- package/components/dashboard/marketingUpdatesSection.tsx +10 -0
- package/components/ui/alert-dialog.tsx +1 -1
- package/components/ui/badge.tsx +1 -1
- package/components/ui/card.tsx +1 -1
- package/components/ui/dialog.tsx +1 -1
- package/components/ui/dropdown-menu.tsx +2 -2
- package/components/ui/select.tsx +1 -1
- package/components/ui/switch.tsx +2 -2
- package/dist/api/feedback.mjs +2 -1
- package/dist/api/feedback.mjs.map +1 -1
- package/dist/api/marketing.d.mts +36 -0
- package/dist/api/marketing.d.ts +36 -0
- package/dist/api/marketing.js +1357 -0
- package/dist/api/marketing.js.map +1 -0
- package/dist/api/marketing.mjs +170 -0
- package/dist/api/marketing.mjs.map +1 -0
- package/dist/auth/helpers.d.mts +3 -3
- package/dist/auth/helpers.d.ts +3 -3
- package/dist/auth/helpers.mjs +5 -60
- package/dist/auth/helpers.mjs.map +1 -1
- package/dist/auth/index.mjs +2 -1
- package/dist/auth-client/index.d.mts +16 -16
- package/dist/auth-client/index.d.ts +16 -16
- package/dist/chunk-6Z3WUSDJ.mjs +62 -0
- package/dist/chunk-6Z3WUSDJ.mjs.map +1 -0
- package/dist/{chunk-6B45N7ZB.mjs → chunk-7QVIOQ47.mjs} +4 -13
- package/dist/chunk-7QVIOQ47.mjs.map +1 -0
- package/dist/chunk-C6RGN6QA.mjs +60 -0
- package/dist/chunk-C6RGN6QA.mjs.map +1 -0
- package/dist/chunk-OVKVD2HH.mjs +25 -0
- package/dist/chunk-OVKVD2HH.mjs.map +1 -0
- package/dist/marketing/index.d.mts +35 -0
- package/dist/marketing/index.d.ts +35 -0
- package/dist/marketing/index.js +125 -0
- package/dist/marketing/index.js.map +1 -0
- package/dist/marketing/index.mjs +6 -0
- package/dist/marketing/index.mjs.map +1 -0
- package/package.json +22 -2
- package/styles/theme.css +17 -3
- package/dist/chunk-6B45N7ZB.mjs.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../lib/feedback-route.ts"],"names":[],"mappings":"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","file":"feedback.mjs","sourcesContent":["import \"server-only\"\n// POST handler behind components/dashboard/feedbackDialog.tsx. Shipped as\n// `@xk2800/nextjs-template/api/feedback` so apps mount it with a one-line\n// app/api/feedback/route.ts: export { POST } from \"@xk2800/nextjs-template/api/feedback\"\nimport { NextRequest, NextResponse } from \"next/server\"\nimport { headers } from \"next/headers\"\nimport { z } from \"zod\"\nimport { auth } from \"../server/auth\"\nimport { config } from \"../config/env\"\nimport { getResend, EMAIL_FROM } from \"./resend\"\nimport { getSystemSettings } from \"./settings-queries\"\nimport { lookupErrorDetail, unexpectedErrorMessage } from \"./errors\"\n\nconst feedbackSchema = z.object({\n type: z.enum([\"bug\", \"feedback\"]),\n // Keep in sync with FEEDBACK_MAX_LENGTH in components/dashboard/feedbackDialog.tsx.\n message: z.string().trim().min(1).max(2000),\n page: z.string().max(300).optional(),\n // The error the user reported from (FeedbackDialog's errorDetails).\n error: z.string().max(2000).optional(),\n})\n\n// const COOLDOWN_MS = 30_000\nconst COOLDOWN_MS = 0\n// ponytail: in-memory, per instance; move to a DB/Redis counter if one user\n// spamming across instances ever matters.\nconst lastSentAt = new Map<string, number>()\n\nexport async function POST(request: NextRequest) {\n const discordUrl = config.FEEDBACK_DISCORD_WEBHOOK_URL\n const emailTo = config.FEEDBACK_EMAIL\n if (!discordUrl && !emailTo) {\n return NextResponse.json({ error: \"Feedback is not configured\" }, { status: 503 })\n }\n\n const session = await auth.api.getSession({ headers: await headers() })\n if (!session?.user) {\n return NextResponse.json({ error: \"Unauthorized\" }, { status: 401 })\n }\n\n const parsed = feedbackSchema.safeParse(await request.json().catch(() => null))\n if (!parsed.success) {\n return NextResponse.json({ error: \"Message must be 1-2000 characters\" }, { status: 400 })\n }\n\n const now = Date.now()\n if (now - (lastSentAt.get(session.user.id) ?? 0) < COOLDOWN_MS) {\n return NextResponse.json(\n { error: \"Please wait a moment before sending more feedback\" },\n { status: 429 }\n )\n }\n lastSentAt.set(session.user.id, now)\n\n const { type, message, page } = parsed.data\n // Admin-controlled (System Settings → Feedback); off drops it entirely.\n const errorDetails =\n parsed.data.error && (await getSystemSettings()).feedbackAttachErrors\n ? parsed.data.error\n : undefined\n // Technical detail stored under the error's ref (lib/errors.ts). Goes\n // server → Discord/email only; the browser never sees it.\n const technicalDetail = errorDetails ? await lookupErrorDetail(errorDetails) : null\n const { user } = session\n const title = type === \"bug\" ? \"🐞 Bug report\" : \"💬 Feedback\"\n const userAgent = request.headers.get(\"user-agent\") || \"unknown\"\n\n const sends: Promise<unknown>[] = []\n\n if (discordUrl) {\n sends.push(\n fetch(discordUrl, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n username: config.APP_NAME,\n // User text must never ping @everyone/@here/roles in the channel.\n allowed_mentions: { parse: [] },\n embeds: [{\n title,\n description: message,\n color: type === \"bug\" ? 0xef4444 : 0x3b82f6,\n fields: [\n { name: \"User\", value: `${user.name} (${user.email})`.slice(0, 1024) },\n { name: \"Page\", value: page || \"unknown\", inline: true },\n ...(errorDetails ? [{ name: \"Error\", value: errorDetails.slice(0, 1024) }] : []),\n ...(technicalDetail\n ? [{ name: \"Technical details\", value: \"```\\n\" + technicalDetail.slice(0, 1000) + \"\\n```\" }]\n : []),\n { name: \"User agent\", value: userAgent.slice(0, 1024) },\n ],\n timestamp: new Date(now).toISOString(),\n }],\n }),\n }).then((res) => {\n if (!res.ok) throw new Error(`Discord webhook responded ${res.status}`)\n })\n )\n }\n\n if (emailTo) {\n sends.push(\n getResend()\n .then((resend) => resend.emails.send({\n from: EMAIL_FROM,\n to: emailTo.split(\",\").map((s) => s.trim()),\n replyTo: user.email,\n subject: `[${config.APP_NAME}] ${title} from ${user.name}`,\n text: `${message}\\n\\n---\\nUser: ${user.name} <${user.email}>\\nPage: ${page || \"unknown\"}${errorDetails ? `\\nError: ${errorDetails}` : \"\"}${technicalDetail ? `\\n\\nTechnical details:\\n${technicalDetail}` : \"\"}\\nUser agent: ${userAgent}`,\n }))\n .then(({ error }) => {\n if (error) throw new Error(error.message)\n })\n )\n }\n\n const results = await Promise.allSettled(sends)\n results.forEach((r) => {\n if (r.status === \"rejected\") console.error(\"Feedback delivery failed:\", r.reason)\n })\n\n if (!results.some((r) => r.status === \"fulfilled\")) {\n lastSentAt.delete(user.id)\n return NextResponse.json({ error: \"Failed to send feedback\" }, { status: 502 })\n }\n return NextResponse.json({ success: true })\n}\n\n/**\n * Development-only endpoint behind FeedbackTestForm, for testing the\n * report-from-error flow. \"!error\" answers with a safe, user-facing error;\n * \"!crash\" simulates an unexpected failure, so the user gets a generic message\n * with an error ref and the technical detail only reaches the server log.\n * 404 outside development.\n * Mount with app/api/feedback/test/route.ts:\n * export { testErrorPOST as POST } from \"@xk2800/nextjs-template/api/feedback\"\n */\nexport async function testErrorPOST(request: NextRequest) {\n if (process.env.NODE_ENV !== \"development\") {\n return NextResponse.json({ error: \"Not found\" }, { status: 404 })\n }\n const session = await auth.api.getSession({ headers: await headers() })\n if (!session?.user) {\n return NextResponse.json({ error: \"Unauthorized\" }, { status: 401 })\n }\n const body = await request.json().catch(() => null)\n if (typeof body?.text === \"string\" && body.text.trim() === \"!error\") {\n return NextResponse.json({ error: \"Test error from the feedback test form\" }, { status: 400 })\n }\n if (typeof body?.text === \"string\" && body.text.trim() === \"!crash\") {\n const crash = new Error(\"Test crash: DB connection to postgres://admin:hunter2@db.internal failed\")\n return NextResponse.json({ error: unexpectedErrorMessage(crash) }, { status: 500 })\n }\n return NextResponse.json({ success: true })\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../lib/feedback-route.ts"],"names":[],"mappings":"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","file":"feedback.mjs","sourcesContent":["import \"server-only\"\n// POST handler behind components/dashboard/feedbackDialog.tsx. Shipped as\n// `@xk2800/nextjs-template/api/feedback` so apps mount it with a one-line\n// app/api/feedback/route.ts: export { POST } from \"@xk2800/nextjs-template/api/feedback\"\nimport { NextRequest, NextResponse } from \"next/server\"\nimport { headers } from \"next/headers\"\nimport { z } from \"zod\"\nimport { auth } from \"../server/auth\"\nimport { config } from \"../config/env\"\nimport { getResend, EMAIL_FROM } from \"./resend\"\nimport { getSystemSettings } from \"./settings-queries\"\nimport { lookupErrorDetail, unexpectedErrorMessage } from \"./errors\"\n\nconst feedbackSchema = z.object({\n type: z.enum([\"bug\", \"feedback\"]),\n // Keep in sync with FEEDBACK_MAX_LENGTH in components/dashboard/feedbackDialog.tsx.\n message: z.string().trim().min(1).max(2000),\n page: z.string().max(300).optional(),\n // The error the user reported from (FeedbackDialog's errorDetails).\n error: z.string().max(2000).optional(),\n})\n\n// const COOLDOWN_MS = 30_000\nconst COOLDOWN_MS = 0\n// ponytail: in-memory, per instance; move to a DB/Redis counter if one user\n// spamming across instances ever matters.\nconst lastSentAt = new Map<string, number>()\n\nexport async function POST(request: NextRequest) {\n const discordUrl = config.FEEDBACK_DISCORD_WEBHOOK_URL\n const emailTo = config.FEEDBACK_EMAIL\n if (!discordUrl && !emailTo) {\n return NextResponse.json({ error: \"Feedback is not configured\" }, { status: 503 })\n }\n\n const session = await auth.api.getSession({ headers: await headers() })\n if (!session?.user) {\n return NextResponse.json({ error: \"Unauthorized\" }, { status: 401 })\n }\n\n const parsed = feedbackSchema.safeParse(await request.json().catch(() => null))\n if (!parsed.success) {\n return NextResponse.json({ error: \"Message must be 1-2000 characters\" }, { status: 400 })\n }\n\n const now = Date.now()\n if (now - (lastSentAt.get(session.user.id) ?? 0) < COOLDOWN_MS) {\n return NextResponse.json(\n { error: \"Please wait a moment before sending more feedback\" },\n { status: 429 }\n )\n }\n lastSentAt.set(session.user.id, now)\n\n const { type, message, page } = parsed.data\n // Admin-controlled (System Settings → Feedback); off drops it entirely.\n const errorDetails =\n parsed.data.error && (await getSystemSettings()).feedbackAttachErrors\n ? parsed.data.error\n : undefined\n // Technical detail stored under the error's ref (lib/errors.ts). Goes\n // server → Discord/email only; the browser never sees it.\n const technicalDetail = errorDetails ? await lookupErrorDetail(errorDetails) : null\n const { user } = session\n const title = type === \"bug\" ? \"🐞 Bug report\" : \"💬 Feedback\"\n const userAgent = request.headers.get(\"user-agent\") || \"unknown\"\n\n const sends: Promise<unknown>[] = []\n\n if (discordUrl) {\n sends.push(\n fetch(discordUrl, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n username: config.APP_NAME,\n // User text must never ping @everyone/@here/roles in the channel.\n allowed_mentions: { parse: [] },\n embeds: [{\n title,\n description: message,\n color: type === \"bug\" ? 0xef4444 : 0x3b82f6,\n fields: [\n { name: \"User\", value: `${user.name} (${user.email})`.slice(0, 1024) },\n { name: \"Page\", value: page || \"unknown\", inline: true },\n ...(errorDetails ? [{ name: \"Error\", value: errorDetails.slice(0, 1024) }] : []),\n ...(technicalDetail\n ? [{ name: \"Technical details\", value: \"```\\n\" + technicalDetail.slice(0, 1000) + \"\\n```\" }]\n : []),\n { name: \"User agent\", value: userAgent.slice(0, 1024) },\n ],\n timestamp: new Date(now).toISOString(),\n }],\n }),\n }).then((res) => {\n if (!res.ok) throw new Error(`Discord webhook responded ${res.status}`)\n })\n )\n }\n\n if (emailTo) {\n sends.push(\n getResend()\n .then((resend) => resend.emails.send({\n from: EMAIL_FROM,\n to: emailTo.split(\",\").map((s) => s.trim()),\n replyTo: user.email,\n subject: `[${config.APP_NAME}] ${title} from ${user.name}`,\n text: `${message}\\n\\n---\\nUser: ${user.name} <${user.email}>\\nPage: ${page || \"unknown\"}${errorDetails ? `\\nError: ${errorDetails}` : \"\"}${technicalDetail ? `\\n\\nTechnical details:\\n${technicalDetail}` : \"\"}\\nUser agent: ${userAgent}`,\n }))\n .then(({ error }) => {\n if (error) throw new Error(error.message)\n })\n )\n }\n\n const results = await Promise.allSettled(sends)\n results.forEach((r) => {\n if (r.status === \"rejected\") console.error(\"Feedback delivery failed:\", r.reason)\n })\n\n if (!results.some((r) => r.status === \"fulfilled\")) {\n lastSentAt.delete(user.id)\n return NextResponse.json({ error: \"Failed to send feedback\" }, { status: 502 })\n }\n return NextResponse.json({ success: true })\n}\n\n/**\n * Development-only endpoint behind FeedbackTestForm, for testing the\n * report-from-error flow. \"!error\" answers with a safe, user-facing error;\n * \"!crash\" simulates an unexpected failure, so the user gets a generic message\n * with an error ref and the technical detail only reaches the server log.\n * 404 outside development.\n * Mount with app/api/feedback/test/route.ts:\n * export { testErrorPOST as POST } from \"@xk2800/nextjs-template/api/feedback\"\n */\nexport async function testErrorPOST(request: NextRequest) {\n if (process.env.NODE_ENV !== \"development\") {\n return NextResponse.json({ error: \"Not found\" }, { status: 404 })\n }\n const session = await auth.api.getSession({ headers: await headers() })\n if (!session?.user) {\n return NextResponse.json({ error: \"Unauthorized\" }, { status: 401 })\n }\n const body = await request.json().catch(() => null)\n if (typeof body?.text === \"string\" && body.text.trim() === \"!error\") {\n return NextResponse.json({ error: \"Test error from the feedback test form\" }, { status: 400 })\n }\n if (typeof body?.text === \"string\" && body.text.trim() === \"!crash\") {\n const crash = new Error(\"Test crash: DB connection to postgres://admin:hunter2@db.internal failed\")\n return NextResponse.json({ error: unexpectedErrorMessage(crash) }, { status: 500 })\n }\n return NextResponse.json({ success: true })\n}\n"]}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
|
|
3
|
+
type IdParams = {
|
|
4
|
+
params: Promise<{
|
|
5
|
+
id: string;
|
|
6
|
+
}>;
|
|
7
|
+
};
|
|
8
|
+
declare function createBroadcastPOST(request: NextRequest): Promise<NextResponse<{
|
|
9
|
+
error: string;
|
|
10
|
+
}> | NextResponse<{
|
|
11
|
+
success: boolean;
|
|
12
|
+
id: string;
|
|
13
|
+
sent: boolean;
|
|
14
|
+
}>>;
|
|
15
|
+
declare function getBroadcastGET(_request: NextRequest, { params }: IdParams): Promise<NextResponse<{
|
|
16
|
+
error: string;
|
|
17
|
+
}> | NextResponse<{
|
|
18
|
+
subject: string | null;
|
|
19
|
+
from: string | null;
|
|
20
|
+
previewText: string | null;
|
|
21
|
+
html: string | null;
|
|
22
|
+
text: string | null;
|
|
23
|
+
status: "draft" | "sent" | "queued";
|
|
24
|
+
}>>;
|
|
25
|
+
declare function sendBroadcastPOST(_request: NextRequest, { params }: IdParams): Promise<NextResponse<{
|
|
26
|
+
error: string;
|
|
27
|
+
}> | NextResponse<{
|
|
28
|
+
success: boolean;
|
|
29
|
+
}>>;
|
|
30
|
+
declare function updateSubscriptionPUT(request: NextRequest): Promise<NextResponse<{
|
|
31
|
+
error: string;
|
|
32
|
+
}> | NextResponse<{
|
|
33
|
+
subscribed: boolean;
|
|
34
|
+
}>>;
|
|
35
|
+
|
|
36
|
+
export { createBroadcastPOST, getBroadcastGET, sendBroadcastPOST, updateSubscriptionPUT };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
|
|
3
|
+
type IdParams = {
|
|
4
|
+
params: Promise<{
|
|
5
|
+
id: string;
|
|
6
|
+
}>;
|
|
7
|
+
};
|
|
8
|
+
declare function createBroadcastPOST(request: NextRequest): Promise<NextResponse<{
|
|
9
|
+
error: string;
|
|
10
|
+
}> | NextResponse<{
|
|
11
|
+
success: boolean;
|
|
12
|
+
id: string;
|
|
13
|
+
sent: boolean;
|
|
14
|
+
}>>;
|
|
15
|
+
declare function getBroadcastGET(_request: NextRequest, { params }: IdParams): Promise<NextResponse<{
|
|
16
|
+
error: string;
|
|
17
|
+
}> | NextResponse<{
|
|
18
|
+
subject: string | null;
|
|
19
|
+
from: string | null;
|
|
20
|
+
previewText: string | null;
|
|
21
|
+
html: string | null;
|
|
22
|
+
text: string | null;
|
|
23
|
+
status: "draft" | "sent" | "queued";
|
|
24
|
+
}>>;
|
|
25
|
+
declare function sendBroadcastPOST(_request: NextRequest, { params }: IdParams): Promise<NextResponse<{
|
|
26
|
+
error: string;
|
|
27
|
+
}> | NextResponse<{
|
|
28
|
+
success: boolean;
|
|
29
|
+
}>>;
|
|
30
|
+
declare function updateSubscriptionPUT(request: NextRequest): Promise<NextResponse<{
|
|
31
|
+
error: string;
|
|
32
|
+
}> | NextResponse<{
|
|
33
|
+
subscribed: boolean;
|
|
34
|
+
}>>;
|
|
35
|
+
|
|
36
|
+
export { createBroadcastPOST, getBroadcastGET, sendBroadcastPOST, updateSubscriptionPUT };
|