From 67301d928cb4bd4c58f3af6ea453c8141c8da05e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carl-Gerhard=20Lindesva=CC=88rd?= Date: Thu, 22 Jan 2026 10:11:44 +0100 Subject: [PATCH] feat: add product hunt badge widget --- apps/start/src/routeTree.gen.ts | 21 ++++ ...onId.$projectId.settings._tabs.widgets.tsx | 98 +++++++++++++++++++ apps/start/src/routes/widget/badge.tsx | 76 ++++++++++++++ packages/trpc/src/routers/widget.ts | 44 +++++++++ 4 files changed, 239 insertions(+) create mode 100644 apps/start/src/routes/widget/badge.tsx diff --git a/apps/start/src/routeTree.gen.ts b/apps/start/src/routeTree.gen.ts index 5d514401..e27951ad 100644 --- a/apps/start/src/routeTree.gen.ts +++ b/apps/start/src/routeTree.gen.ts @@ -19,6 +19,7 @@ import { Route as IndexRouteImport } from './routes/index' import { Route as WidgetTestRouteImport } from './routes/widget/test' import { Route as WidgetRealtimeRouteImport } from './routes/widget/realtime' import { Route as WidgetCounterRouteImport } from './routes/widget/counter' +import { Route as WidgetBadgeRouteImport } from './routes/widget/badge' import { Route as ApiHealthcheckRouteImport } from './routes/api/healthcheck' import { Route as ApiConfigRouteImport } from './routes/api/config' import { Route as PublicOnboardingRouteImport } from './routes/_public.onboarding' @@ -138,6 +139,11 @@ const WidgetCounterRoute = WidgetCounterRouteImport.update({ path: '/widget/counter', getParentRoute: () => rootRouteImport, } as any) +const WidgetBadgeRoute = WidgetBadgeRouteImport.update({ + id: '/widget/badge', + path: '/widget/badge', + getParentRoute: () => rootRouteImport, +} as any) const ApiHealthcheckRoute = ApiHealthcheckRouteImport.update({ id: '/api/healthcheck', path: '/api/healthcheck', @@ -531,6 +537,7 @@ export interface FileRoutesByFullPath { '/onboarding': typeof PublicOnboardingRoute '/api/config': typeof ApiConfigRoute '/api/healthcheck': typeof ApiHealthcheckRoute + '/widget/badge': typeof WidgetBadgeRoute '/widget/counter': typeof WidgetCounterRoute '/widget/realtime': typeof WidgetRealtimeRoute '/widget/test': typeof WidgetTestRoute @@ -596,6 +603,7 @@ export interface FileRoutesByTo { '/onboarding': typeof PublicOnboardingRoute '/api/config': typeof ApiConfigRoute '/api/healthcheck': typeof ApiHealthcheckRoute + '/widget/badge': typeof WidgetBadgeRoute '/widget/counter': typeof WidgetCounterRoute '/widget/realtime': typeof WidgetRealtimeRoute '/widget/test': typeof WidgetTestRoute @@ -659,6 +667,7 @@ export interface FileRoutesById { '/_public/onboarding': typeof PublicOnboardingRoute '/api/config': typeof ApiConfigRoute '/api/healthcheck': typeof ApiHealthcheckRoute + '/widget/badge': typeof WidgetBadgeRoute '/widget/counter': typeof WidgetCounterRoute '/widget/realtime': typeof WidgetRealtimeRoute '/widget/test': typeof WidgetTestRoute @@ -734,6 +743,7 @@ export interface FileRouteTypes { | '/onboarding' | '/api/config' | '/api/healthcheck' + | '/widget/badge' | '/widget/counter' | '/widget/realtime' | '/widget/test' @@ -799,6 +809,7 @@ export interface FileRouteTypes { | '/onboarding' | '/api/config' | '/api/healthcheck' + | '/widget/badge' | '/widget/counter' | '/widget/realtime' | '/widget/test' @@ -861,6 +872,7 @@ export interface FileRouteTypes { | '/_public/onboarding' | '/api/config' | '/api/healthcheck' + | '/widget/badge' | '/widget/counter' | '/widget/realtime' | '/widget/test' @@ -935,6 +947,7 @@ export interface RootRouteChildren { StepsRoute: typeof StepsRouteWithChildren ApiConfigRoute: typeof ApiConfigRoute ApiHealthcheckRoute: typeof ApiHealthcheckRoute + WidgetBadgeRoute: typeof WidgetBadgeRoute WidgetCounterRoute: typeof WidgetCounterRoute WidgetRealtimeRoute: typeof WidgetRealtimeRoute WidgetTestRoute: typeof WidgetTestRoute @@ -1001,6 +1014,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof WidgetCounterRouteImport parentRoute: typeof rootRouteImport } + '/widget/badge': { + id: '/widget/badge' + path: '/widget/badge' + fullPath: '/widget/badge' + preLoaderRoute: typeof WidgetBadgeRouteImport + parentRoute: typeof rootRouteImport + } '/api/healthcheck': { id: '/api/healthcheck' path: '/api/healthcheck' @@ -1874,6 +1894,7 @@ const rootRouteChildren: RootRouteChildren = { StepsRoute: StepsRouteWithChildren, ApiConfigRoute: ApiConfigRoute, ApiHealthcheckRoute: ApiHealthcheckRoute, + WidgetBadgeRoute: WidgetBadgeRoute, WidgetCounterRoute: WidgetCounterRoute, WidgetRealtimeRoute: WidgetRealtimeRoute, WidgetTestRoute: WidgetTestRoute, diff --git a/apps/start/src/routes/_app.$organizationId.$projectId.settings._tabs.widgets.tsx b/apps/start/src/routes/_app.$organizationId.$projectId.settings._tabs.widgets.tsx index f62ef880..c9ab5ce0 100644 --- a/apps/start/src/routes/_app.$organizationId.$projectId.settings._tabs.widgets.tsx +++ b/apps/start/src/routes/_app.$organizationId.$projectId.settings._tabs.widgets.tsx @@ -107,6 +107,11 @@ function Component() { isToggling={toggleMutation.isPending} onToggle={(enabled) => handleToggle('counter', enabled)} /> + + ); } @@ -369,3 +374,96 @@ function CounterWidgetSection({ ); } + +interface BadgeWidgetSectionProps { + widget: { + id: string; + public: boolean; + } | null; + dashboardUrl: string; +} + +function BadgeWidgetSection({ widget, dashboardUrl }: BadgeWidgetSectionProps) { + const isEnabled = widget?.public ?? false; + const badgeUrl = + isEnabled && widget?.id + ? `${dashboardUrl}/widget/badge?shareId=${widget.id}` + : null; + const badgeEmbedCode = badgeUrl + ? ` + +` + : null; + + if (!isEnabled || !badgeUrl) { + return null; + } + + return ( + + +
+ Analytics Badge +

+ A Product Hunt-style badge showing your 30-day unique visitor count. + Perfect for showcasing your analytics powered by OpenPanel. +

+
+
+ +
+

Widget URL

+ +

+ Direct link to the analytics badge widget. +

+
+ +
+

Embed Code

+ +

+ Copy this code and paste it into your website HTML where you want + the badge to appear. +

+
+ +
+

Preview

+
+ +