From 6107c7f19290a8d5389975f56df540e1488b8e31 Mon Sep 17 00:00:00 2001 From: filippofilip95 Date: Mon, 22 Dec 2025 21:25:05 +0100 Subject: [PATCH] feat: implement 80/20 features and content format support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Features implemented: - One-click email approve/reject with magic links - Mobile-first review page with fixed bottom action bar - Slack interactive buttons with signature verification - Dashboard search/filter and review templates - Toast notification system (Radix-based) - Content format support (PLAIN_TEXT, MARKDOWN, HTML) - ContentRenderer component with DOMPurify sanitization Security improvements (50 issues fixed across 3 code reviews): - HTML sanitization with DOMPurify - Slack webhook HMAC-SHA256 signature verification - Race conditions fixed with Prisma transactions - XSS prevention in markdown link rendering - Input validation and sanitization throughout Accessibility improvements: - ARIA labels on all interactive elements - Role attributes for status indicators and dialogs - Full i18n support with next-intl 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .idea/modules.xml | 8 + .idea/sendvelo.iml | 8 + .idea/vcs.xml | 6 + .idea/workspace.xml | 110 +- README.md | 151 - _bmad-output/ACTION_PLAN_CONSOLIDATED.md | 377 ++ _bmad-output/DEEP_USER_RESEARCH_PROMPT.md | 527 +++ _bmad-output/IMPLEMENTATION_PLAN_80_20.md | 1060 ++++++ .../IMPLEMENTATION_PLAN_V2.1_CONTENT_TYPES.md | 316 ++ _bmad-output/IMPLEMENTATION_PLAN_V2.md | 3186 +++++++++++++++++ .../MARKET_ANALYSIS_CHATGPT_APPSTORE.md | 1121 ++++++ _bmad-output/USER_RESEARCH_REPORT.md | 359 ++ .../brainstorming-session-2025-12-20.md | 15 + .../marketing/00-MARKETING-EXECUTION-GUIDE.md | 318 ++ .../marketing/01-LINKEDIN-CONTENT-CALENDAR.md | 537 +++ .../marketing/02-REDDIT-SEEDING-PLAYBOOK.md | 384 ++ .../marketing/03-PRODUCT-HUNT-LAUNCH-KIT.md | 576 +++ .../marketing/04-SEO-BLOG-CONTENT-OUTLINES.md | 742 ++++ .../05-INFLUENCER-OUTREACH-SEQUENCES.md | 358 ++ .../marketing/06-COLD-EMAIL-SEQUENCES.md | 562 +++ .../marketing/07-CASE-STUDY-TEMPLATE.md | 458 +++ app/custom-page/page.tsx | 22 - app/favicon.ico | Bin 25931 -> 0 bytes app/globals.css | 26 - app/hooks/index.ts | 19 - app/hooks/types.ts | 104 - app/hooks/use-call-tool.ts | 36 - app/hooks/use-display-mode.ts | 23 - app/hooks/use-is-chatgpt-app.ts | 19 - app/hooks/use-max-height.ts | 21 - app/hooks/use-open-external.ts | 43 - app/hooks/use-openai-global.ts | 53 - app/hooks/use-request-display-mode.ts | 30 - app/hooks/use-send-message.ts | 27 - app/hooks/use-widget-props.ts | 29 - app/hooks/use-widget-state.ts | 72 - app/layout.tsx | 162 - app/mcp/route.ts | 102 - app/page.tsx | 129 - approval-tool/app/(app)/dashboard/page.tsx | 348 -- .../app/[locale]/(app)/dashboard/page.tsx | 564 +++ .../app/{ => [locale]}/(app)/layout.tsx | 0 .../app/{ => [locale]}/(auth)/signin/page.tsx | 0 .../app/{ => [locale]}/(auth)/signup/page.tsx | 0 .../approve/[reviewId]/[reviewerId]/page.tsx | 161 + approval-tool/app/[locale]/layout.tsx | 31 + approval-tool/app/{ => [locale]}/page.tsx | 0 .../app/[locale]/review/[slug]/page.tsx | 459 +++ .../app/api/slack/interactions/route.ts | 264 ++ .../app/api/webhooks/stripe/route.ts | 16 +- approval-tool/app/globals.css | 70 +- approval-tool/app/layout.tsx | 14 +- approval-tool/app/mcp/route.ts | 888 +++-- approval-tool/app/review/[slug]/page.tsx | 215 -- .../docs/CHATGPT_APP_STORE_STRATEGY.md | 768 ++++ approval-tool/docs/IMPLEMENTATION_PLAN.md | 1382 +++++++ approval-tool/messages/en.json | 15 +- approval-tool/package.json | 7 +- approval-tool/pnpm-lock.yaml | 642 +++- approval-tool/prisma/schema.prisma | 401 ++- .../src/components/content-renderer.tsx | 271 ++ approval-tool/src/components/ui/toast.tsx | 143 + approval-tool/src/env.ts | 14 + approval-tool/src/lib/auth-client.ts | 7 +- approval-tool/src/lib/auth.ts | 38 +- approval-tool/src/lib/email.ts | 335 +- approval-tool/src/lib/slack.ts | 306 ++ approval-tool/src/lib/templates.ts | 180 + approval-tool/src/server/api/root.ts | 2 + .../src/server/api/routers/organization.ts | 367 ++ .../src/server/api/routers/review.ts | 606 +++- approval-tool/src/server/api/routers/user.ts | 6 +- baseUrl.ts | 7 - docs/CRITICAL_REVIEW.md | 416 --- docs/IMPLEMENTATION_PLAN.md | 1896 ---------- docs/PRODUCT_STRATEGY.md | 311 -- docs/RECOMMENDED_ACTION_PLAN.md | 362 -- docs/TECHNICAL_ARCHITECTURE.md | 1771 --------- docs/TECH_STACK.md | 207 -- docs/index.md | 10 - middleware.ts | 26 - next.config.ts | 8 - package-lock.json | 1667 --------- package.json | 27 - pnpm-lock.yaml | 1819 ---------- postcss.config.mjs | 5 - public/file.svg | 1 - public/globe.svg | 1 - public/next.svg | 1 - public/vercel.svg | 1 - public/window.svg | 1 - tsconfig.json | 27 - 92 files changed, 18511 insertions(+), 10669 deletions(-) create mode 100644 .idea/modules.xml create mode 100644 .idea/sendvelo.iml create mode 100644 .idea/vcs.xml delete mode 100644 README.md create mode 100644 _bmad-output/ACTION_PLAN_CONSOLIDATED.md create mode 100644 _bmad-output/DEEP_USER_RESEARCH_PROMPT.md create mode 100644 _bmad-output/IMPLEMENTATION_PLAN_80_20.md create mode 100644 _bmad-output/IMPLEMENTATION_PLAN_V2.1_CONTENT_TYPES.md create mode 100644 _bmad-output/IMPLEMENTATION_PLAN_V2.md create mode 100644 _bmad-output/MARKET_ANALYSIS_CHATGPT_APPSTORE.md create mode 100644 _bmad-output/USER_RESEARCH_REPORT.md create mode 100644 _bmad-output/analysis/brainstorming-session-2025-12-20.md create mode 100644 _bmad-output/marketing/00-MARKETING-EXECUTION-GUIDE.md create mode 100644 _bmad-output/marketing/01-LINKEDIN-CONTENT-CALENDAR.md create mode 100644 _bmad-output/marketing/02-REDDIT-SEEDING-PLAYBOOK.md create mode 100644 _bmad-output/marketing/03-PRODUCT-HUNT-LAUNCH-KIT.md create mode 100644 _bmad-output/marketing/04-SEO-BLOG-CONTENT-OUTLINES.md create mode 100644 _bmad-output/marketing/05-INFLUENCER-OUTREACH-SEQUENCES.md create mode 100644 _bmad-output/marketing/06-COLD-EMAIL-SEQUENCES.md create mode 100644 _bmad-output/marketing/07-CASE-STUDY-TEMPLATE.md delete mode 100644 app/custom-page/page.tsx delete mode 100644 app/favicon.ico delete mode 100644 app/globals.css delete mode 100644 app/hooks/index.ts delete mode 100644 app/hooks/types.ts delete mode 100644 app/hooks/use-call-tool.ts delete mode 100644 app/hooks/use-display-mode.ts delete mode 100644 app/hooks/use-is-chatgpt-app.ts delete mode 100644 app/hooks/use-max-height.ts delete mode 100644 app/hooks/use-open-external.ts delete mode 100644 app/hooks/use-openai-global.ts delete mode 100644 app/hooks/use-request-display-mode.ts delete mode 100644 app/hooks/use-send-message.ts delete mode 100644 app/hooks/use-widget-props.ts delete mode 100644 app/hooks/use-widget-state.ts delete mode 100644 app/layout.tsx delete mode 100644 app/mcp/route.ts delete mode 100644 app/page.tsx delete mode 100644 approval-tool/app/(app)/dashboard/page.tsx create mode 100644 approval-tool/app/[locale]/(app)/dashboard/page.tsx rename approval-tool/app/{ => [locale]}/(app)/layout.tsx (100%) rename approval-tool/app/{ => [locale]}/(auth)/signin/page.tsx (100%) rename approval-tool/app/{ => [locale]}/(auth)/signup/page.tsx (100%) create mode 100644 approval-tool/app/[locale]/approve/[reviewId]/[reviewerId]/page.tsx create mode 100644 approval-tool/app/[locale]/layout.tsx rename approval-tool/app/{ => [locale]}/page.tsx (100%) create mode 100644 approval-tool/app/[locale]/review/[slug]/page.tsx create mode 100644 approval-tool/app/api/slack/interactions/route.ts delete mode 100644 approval-tool/app/review/[slug]/page.tsx create mode 100644 approval-tool/docs/CHATGPT_APP_STORE_STRATEGY.md create mode 100644 approval-tool/docs/IMPLEMENTATION_PLAN.md create mode 100644 approval-tool/src/components/content-renderer.tsx create mode 100644 approval-tool/src/components/ui/toast.tsx create mode 100644 approval-tool/src/lib/slack.ts create mode 100644 approval-tool/src/lib/templates.ts create mode 100644 approval-tool/src/server/api/routers/organization.ts delete mode 100644 baseUrl.ts delete mode 100644 docs/CRITICAL_REVIEW.md delete mode 100644 docs/IMPLEMENTATION_PLAN.md delete mode 100644 docs/PRODUCT_STRATEGY.md delete mode 100644 docs/RECOMMENDED_ACTION_PLAN.md delete mode 100644 docs/TECHNICAL_ARCHITECTURE.md delete mode 100644 docs/TECH_STACK.md delete mode 100644 docs/index.md delete mode 100644 middleware.ts delete mode 100644 next.config.ts delete mode 100644 package-lock.json delete mode 100644 package.json delete mode 100644 pnpm-lock.yaml delete mode 100644 postcss.config.mjs delete mode 100644 public/file.svg delete mode 100644 public/globe.svg delete mode 100644 public/next.svg delete mode 100644 public/vercel.svg delete mode 100644 public/window.svg delete mode 100644 tsconfig.json diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..392d93e --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/sendvelo.iml b/.idea/sendvelo.iml new file mode 100644 index 0000000..c956989 --- /dev/null +++ b/.idea/sendvelo.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 1ac928b..32eeb5d 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -1,4 +1,112 @@ - {} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - ); -} diff --git a/app/mcp/route.ts b/app/mcp/route.ts deleted file mode 100644 index aae6eba..0000000 --- a/app/mcp/route.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { baseURL } from "@/baseUrl"; -import { createMcpHandler } from "mcp-handler"; -import { z } from "zod"; - -const getAppsSdkCompatibleHtml = async (baseUrl: string, path: string) => { - const result = await fetch(`${baseUrl}${path}`); - return await result.text(); -}; - -type ContentWidget = { - id: string; - title: string; - templateUri: string; - invoking: string; - invoked: string; - html: string; - description: string; - widgetDomain: string; -}; - -function widgetMeta(widget: ContentWidget) { - return { - "openai/outputTemplate": widget.templateUri, - "openai/toolInvocation/invoking": widget.invoking, - "openai/toolInvocation/invoked": widget.invoked, - "openai/widgetAccessible": false, - "openai/resultCanProduceWidget": true, - } as const; -} - -const handler = createMcpHandler(async (server) => { - const html = await getAppsSdkCompatibleHtml(baseURL, "/"); - - const contentWidget: ContentWidget = { - id: "show_content", - title: "Show Content", - templateUri: "ui://widget/content-template.html", - invoking: "Loading content...", - invoked: "Content loaded", - html: html, - description: "Displays the homepage content", - widgetDomain: "https://nextjs.org/docs", - }; - server.registerResource( - "content-widget", - contentWidget.templateUri, - { - title: contentWidget.title, - description: contentWidget.description, - mimeType: "text/html+skybridge", - _meta: { - "openai/widgetDescription": contentWidget.description, - "openai/widgetPrefersBorder": true, - }, - }, - async (uri) => ({ - contents: [ - { - uri: uri.href, - mimeType: "text/html+skybridge", - text: `${contentWidget.html}`, - _meta: { - "openai/widgetDescription": contentWidget.description, - "openai/widgetPrefersBorder": true, - "openai/widgetDomain": contentWidget.widgetDomain, - }, - }, - ], - }) - ); - - server.registerTool( - contentWidget.id, - { - title: contentWidget.title, - description: - "Fetch and display the homepage content with the name of the user", - inputSchema: { - name: z.string().describe("The name of the user to display on the homepage"), - }, - _meta: widgetMeta(contentWidget), - }, - async ({ name }) => { - return { - content: [ - { - type: "text", - text: name, - }, - ], - structuredContent: { - name: name, - timestamp: new Date().toISOString(), - }, - _meta: widgetMeta(contentWidget), - }; - } - ); -}); - -export const GET = handler; -export const POST = handler; diff --git a/app/page.tsx b/app/page.tsx deleted file mode 100644 index c0d11bb..0000000 --- a/app/page.tsx +++ /dev/null @@ -1,129 +0,0 @@ -"use client"; - -import Image from "next/image"; -import Link from "next/link"; -import { - useWidgetProps, - useMaxHeight, - useDisplayMode, - useRequestDisplayMode, - useIsChatGptApp, -} from "./hooks"; - -export default function Home() { - const toolOutput = useWidgetProps<{ - name?: string; - result?: { structuredContent?: { name?: string } }; - }>(); - const maxHeight = useMaxHeight() ?? undefined; - const displayMode = useDisplayMode(); - const requestDisplayMode = useRequestDisplayMode(); - const isChatGptApp = useIsChatGptApp(); - - const name = toolOutput?.result?.structuredContent?.name || toolOutput?.name; - - return ( -
- {displayMode !== "fullscreen" && ( - - )} -
- {!isChatGptApp && ( -
-
- -
-

- This app relies on data from a ChatGPT session. -

-

- No{" "} - - window.openai - {" "} - property detected -

-
-
-
- )} - Next.js logo -
    -
  1. - Welcome to the ChatGPT Apps SDK Next.js Starter -
  2. -
  3. - Name returned from tool call: {name ?? "..."} -
  4. -
  5. MCP server path: /mcp
  6. -
- -
- - Visit another page - - - Deploy on Vercel - -
-
-
- ); -} diff --git a/approval-tool/app/(app)/dashboard/page.tsx b/approval-tool/app/(app)/dashboard/page.tsx deleted file mode 100644 index 53da823..0000000 --- a/approval-tool/app/(app)/dashboard/page.tsx +++ /dev/null @@ -1,348 +0,0 @@ -"use client"; - -import { api, type RouterOutputs } from "@/trpc/react"; -import { STRIPE_PRICES } from "@/lib/stripe"; -import { useState } from "react"; -import { useTranslations } from "next-intl"; - -type Review = RouterOutputs["review"]["getMyReviews"]["reviews"][number]; - -/** - * Dashboard Page - * - * Shows user's reviews, subscription status, and allows creating new reviews. - */ - -export default function DashboardPage() { - const t = useTranslations("dashboard"); - const tCommon = useTranslations("common"); - const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); - - const { data: profile, isLoading: profileLoading } = - api.user.getProfile.useQuery(); - - const { data: reviewsData, isLoading: reviewsLoading, refetch } = - api.review.getMyReviews.useQuery(); - - const reviews = reviewsData?.reviews || []; - - const { data: stats } = api.review.getStats.useQuery(); - - const createReviewMutation = api.review.create.useMutation({ - onSuccess: () => { - refetch(); - setIsCreateModalOpen(false); - // TODO: Add toast notification - toast.success(t("reviews.createSuccess")) - }, - onError: (error) => { - console.error("Failed to create review:", error); - alert(error.message || "Failed to create review"); - }, - }); - - const deleteReviewMutation = api.review.delete.useMutation({ - onSuccess: () => { - refetch(); - // TODO: Add toast notification - toast.success(t("reviews.deleteSuccess")) - }, - onError: (error) => { - console.error("Failed to delete review:", error); - alert(error.message || "Failed to delete review"); - }, - }); - - const createCheckoutMutation = api.user.createCheckout.useMutation({ - onSuccess: (data) => { - if (data.url) { - window.location.href = data.url; - } - }, - onError: (error) => { - console.error("Failed to create checkout session:", error); - alert(error.message || "Failed to start checkout"); - }, - }); - - const createPortalMutation = api.user.createPortalSession.useMutation({ - onSuccess: (data) => { - if (data.url) { - window.location.href = data.url; - } - }, - onError: (error) => { - console.error("Failed to create portal session:", error); - alert(error.message || "Failed to open billing portal"); - }, - }); - - const [newReview, setNewReview] = useState({ - title: "", - content: "", - reviewerEmail: "", - }); - - const handleCreateReview = async (e: React.FormEvent) => { - e.preventDefault(); - try { - await createReviewMutation.mutateAsync(newReview); - setNewReview({ title: "", content: "", reviewerEmail: "" }); - } catch (error) { - // Error is already handled by onError callback - // Keep form data so user can retry - } - }; - - if (profileLoading || reviewsLoading) { - return ( -
-
-

{tCommon("loading")}

-
-
- ); - } - - const tier = profile?.subscriptionTier ?? "free"; - const remaining = - tier === "free" ? 5 - (profile?.reviewsThisMonth ?? 0) : "unlimited"; - - return ( -
- {/* Stats Cards */} -
-
-

- {t("stats.totalReviews")} -

-

- {stats?.total ?? 0} -

-
-
-

- {t("stats.pending")} -

-

- {stats?.pending ?? 0} -

-
-
-

- {t("stats.subscription")} -

-
-

- {tier} -

- {tier === "free" ? ( - - ) : ( - - )} -
-

- {remaining === "unlimited" - ? t("subscription.unlimited") - : t("subscription.reviewsRemaining", { count: remaining })} -

-
-
- - {/* Header */} -
-

- {t("reviews.title")} -

- -
- - {/* Reviews List */} - {reviews.length > 0 ? ( -
- - - - - - - - - - - - {reviews.map((review: Review) => ( - - - - - - - - ))} - -
- {t("reviews.table.title")} - - {t("reviews.table.reviewer")} - - {t("reviews.table.status")} - - {t("reviews.table.created")} - - {t("reviews.table.actions")} -
- - {review.title} - - - {review.reviewerEmail} - - - {review.status} - - - {new Date(review.createdAt).toLocaleDateString()} - - -
-
- ) : ( -
-

{t("reviews.noReviews")}

-
- )} - - {/* Create Review Modal */} - {isCreateModalOpen && ( -
-
-

- {t("createModal.title")} -

- -
-
- - - setNewReview({ ...newReview, title: e.target.value }) - } - required - className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" - /> -
- -
- - - setNewReview({ ...newReview, reviewerEmail: e.target.value }) - } - required - className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" - /> -
- -
- -