Skip to content
Closed
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
8 changes: 8 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,11 @@ BLOB_READ_WRITE_TOKEN=your_blob_token_here
# Desktop App
VITE_DEV_SERVER_PORT=4927
DESKTOP_AUTH_SECRET=your_desktop_auth_secret_here_min_32_chars

# PostHog Analytics (Client-side tracking)
NEXT_PUBLIC_POSTHOG_KEY=your_posthog_project_api_key_here
NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com

# PostHog Analytics (Server-side API for admin dashboard)
POSTHOG_API_KEY=your_posthog_personal_api_key_here
POSTHOG_PROJECT_ID=your_posthog_project_id_here
11 changes: 9 additions & 2 deletions .github/workflows/deploy-preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ jobs:
CLERK_WEBHOOK_SECRET: ${{ secrets.CLERK_WEBHOOK_SECRET }}
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${{ secrets.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY }}
DESKTOP_AUTH_SECRET: ${{ secrets.DESKTOP_AUTH_SECRET }}
POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_KEY }}
POSTHOG_PROJECT_ID: ${{ secrets.POSTHOG_PROJECT_ID }}
run: |
vercel pull --yes --environment=preview --token=$VERCEL_TOKEN
vercel build --token=$VERCEL_TOKEN
Expand All @@ -137,7 +139,9 @@ jobs:
--env NEXT_PUBLIC_WEB_URL=$NEXT_PUBLIC_WEB_URL \
--env NEXT_PUBLIC_ADMIN_URL=$NEXT_PUBLIC_ADMIN_URL \
--env NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=$NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY \
--env DESKTOP_AUTH_SECRET=$DESKTOP_AUTH_SECRET)
--env DESKTOP_AUTH_SECRET=$DESKTOP_AUTH_SECRET \
--env POSTHOG_API_KEY=$POSTHOG_API_KEY \
--env POSTHOG_PROJECT_ID=$POSTHOG_PROJECT_ID)
vercel alias $VERCEL_URL ${{ env.API_ALIAS }} --scope=$VERCEL_ORG_ID --token=$VERCEL_TOKEN
echo "vercel_url=$VERCEL_URL" >> $GITHUB_OUTPUT

Expand Down Expand Up @@ -208,14 +212,16 @@ jobs:
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${{ secrets.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY }}
NEXT_PUBLIC_COOKIE_DOMAIN: ${{ secrets.NEXT_PUBLIC_COOKIE_DOMAIN }}
DESKTOP_AUTH_SECRET: ${{ secrets.DESKTOP_AUTH_SECRET }}
NEXT_PUBLIC_POSTHOG_KEY: ${{ secrets.NEXT_PUBLIC_POSTHOG_KEY }}
run: |
vercel pull --yes --environment=preview --token=$VERCEL_TOKEN
vercel build --token=$VERCEL_TOKEN
VERCEL_URL=$(vercel deploy --prebuilt --token=$VERCEL_TOKEN \
--env CLERK_SECRET_KEY=$CLERK_SECRET_KEY \
--env DATABASE_URL=$DATABASE_URL \
--env DATABASE_URL_UNPOOLED=$DATABASE_URL_UNPOOLED \
--env DESKTOP_AUTH_SECRET=$DESKTOP_AUTH_SECRET)
--env DESKTOP_AUTH_SECRET=$DESKTOP_AUTH_SECRET \
--env NEXT_PUBLIC_POSTHOG_KEY=$NEXT_PUBLIC_POSTHOG_KEY)
vercel alias $VERCEL_URL ${{ env.WEB_ALIAS }} --scope=$VERCEL_ORG_ID --token=$VERCEL_TOKEN
echo "vercel_url=$VERCEL_URL" >> $GITHUB_OUTPUT

Expand Down Expand Up @@ -270,6 +276,7 @@ jobs:
CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }}
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${{ secrets.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY }}
NEXT_PUBLIC_COOKIE_DOMAIN: ${{ secrets.NEXT_PUBLIC_COOKIE_DOMAIN }}
NEXT_PUBLIC_POSTHOG_KEY: ${{ secrets.NEXT_PUBLIC_POSTHOG_KEY }}
run: |
vercel pull --yes --environment=preview --token=$VERCEL_TOKEN
vercel build --token=$VERCEL_TOKEN
Expand Down
11 changes: 9 additions & 2 deletions .github/workflows/deploy-production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ jobs:
BLOB_READ_WRITE_TOKEN: ${{ secrets.BLOB_READ_WRITE_TOKEN }}
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${{ secrets.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY }}
DESKTOP_AUTH_SECRET: ${{ secrets.DESKTOP_AUTH_SECRET }}
POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_KEY }}
POSTHOG_PROJECT_ID: ${{ secrets.POSTHOG_PROJECT_ID }}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
run: |
vercel pull --yes --environment=production --token=$VERCEL_TOKEN
vercel build --prod --token=$VERCEL_TOKEN
Expand All @@ -89,7 +91,9 @@ jobs:
--env NEXT_PUBLIC_WEB_URL=$NEXT_PUBLIC_WEB_URL \
--env NEXT_PUBLIC_ADMIN_URL=$NEXT_PUBLIC_ADMIN_URL \
--env NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=$NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY \
--env DESKTOP_AUTH_SECRET=$DESKTOP_AUTH_SECRET
--env DESKTOP_AUTH_SECRET=$DESKTOP_AUTH_SECRET \
--env POSTHOG_API_KEY=$POSTHOG_API_KEY \
--env POSTHOG_PROJECT_ID=$POSTHOG_PROJECT_ID

deploy-web:
name: Deploy Web to Vercel
Expand Down Expand Up @@ -132,14 +136,16 @@ jobs:
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${{ secrets.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY }}
NEXT_PUBLIC_COOKIE_DOMAIN: ${{ secrets.NEXT_PUBLIC_COOKIE_DOMAIN }}
DESKTOP_AUTH_SECRET: ${{ secrets.DESKTOP_AUTH_SECRET }}
NEXT_PUBLIC_POSTHOG_KEY: ${{ secrets.NEXT_PUBLIC_POSTHOG_KEY }}
run: |
vercel pull --yes --environment=production --token=$VERCEL_TOKEN
vercel build --prod --token=$VERCEL_TOKEN
vercel deploy --prod --prebuilt --token=$VERCEL_TOKEN \
--env CLERK_SECRET_KEY=$CLERK_SECRET_KEY \
--env DATABASE_URL=$DATABASE_URL \
--env DATABASE_URL_UNPOOLED=$DATABASE_URL_UNPOOLED \
--env DESKTOP_AUTH_SECRET=$DESKTOP_AUTH_SECRET
--env DESKTOP_AUTH_SECRET=$DESKTOP_AUTH_SECRET \
--env NEXT_PUBLIC_POSTHOG_KEY=$NEXT_PUBLIC_POSTHOG_KEY

deploy-marketing:
name: Deploy Marketing to Vercel
Expand Down Expand Up @@ -178,6 +184,7 @@ jobs:
CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }}
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${{ secrets.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY }}
NEXT_PUBLIC_COOKIE_DOMAIN: ${{ secrets.NEXT_PUBLIC_COOKIE_DOMAIN }}
NEXT_PUBLIC_POSTHOG_KEY: ${{ secrets.NEXT_PUBLIC_POSTHOG_KEY }}
run: |
vercel pull --yes --environment=production --token=$VERCEL_TOKEN
vercel build --prod --token=$VERCEL_TOKEN
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
"use client";

import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@superset/ui/card";
import { cn } from "@superset/ui/utils";
import { LuArrowRight, LuLoaderCircle } from "react-icons/lu";

interface FunnelStep {
name: string;
count: number;
conversionRate?: number;
}

interface ActivationFunnelProps {
steps: FunnelStep[];
isLoading?: boolean;
error?: string;
}

export function ActivationFunnel({
steps,
isLoading,
error,
}: ActivationFunnelProps) {
if (isLoading) {
return (
<Card>
<CardHeader>
<CardTitle>Activation Funnel</CardTitle>
<CardDescription>
Signup → Download → First Task → Completed
</CardDescription>
</CardHeader>
<CardContent className="flex items-center justify-center py-12">
<LuLoaderCircle className="text-muted-foreground h-8 w-8 animate-spin" />
</CardContent>
</Card>
);
}

if (error) {
return (
<Card>
<CardHeader>
<CardTitle>Activation Funnel</CardTitle>
</CardHeader>
<CardContent className="text-muted-foreground py-12 text-center">
<p>Failed to load funnel data</p>
<p className="text-sm">{error}</p>
</CardContent>
</Card>
);
}

const maxCount = Math.max(...steps.map((s) => s.count), 1);

return (
<Card>
<CardHeader>
<CardTitle>Activation Funnel</CardTitle>
<CardDescription>
Track users through signup to first completed task
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-end justify-between gap-2">
{steps.map((step, index) => (
<div key={step.name} className="flex flex-1 items-center gap-2">
<div className="flex flex-1 flex-col items-center">
<div
className="bg-primary/20 mb-2 w-full rounded-t-md transition-all"
style={{
height: `${Math.max((step.count / maxCount) * 120, 20)}px`,
}}
>
<div
className="bg-primary h-full w-full rounded-t-md"
style={{
opacity: 1 - index * 0.15,
}}
/>
</div>
<div className="text-center">
<div className="text-2xl font-bold">
{step.count.toLocaleString()}
</div>
<div className="text-muted-foreground text-xs">
{step.name}
</div>
{step.conversionRate !== undefined && index > 0 && (
<div
className={cn(
"mt-1 text-xs font-medium",
step.conversionRate >= 50
? "text-green-600"
: step.conversionRate >= 25
? "text-yellow-600"
: "text-red-600",
)}
>
{step.conversionRate.toFixed(0)}%
</div>
)}
</div>
</div>
{index < steps.length - 1 && (
<LuArrowRight className="text-muted-foreground h-4 w-4 shrink-0" />
)}
</div>
))}
</div>
</CardContent>
</Card>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ActivationFunnel } from "./ActivationFunnel";
77 changes: 77 additions & 0 deletions apps/admin/src/app/(dashboard)/components/StatsCard/StatsCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"use client";

import { Card, CardContent, CardHeader, CardTitle } from "@superset/ui/card";
import { cn } from "@superset/ui/utils";
import { LuArrowDown, LuArrowUp, LuMinus } from "react-icons/lu";

interface StatsCardProps {
title: string;
value: number | string;
description?: string;
trend?: {
value: number;
label: string;
};
isLoading?: boolean;
}

export function StatsCard({
title,
value,
description,
trend,
isLoading,
}: StatsCardProps) {
const trendDirection =
trend && trend.value > 0
? "up"
: trend && trend.value < 0
? "down"
: "neutral";

return (
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">{title}</CardTitle>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="space-y-2">
<div className="bg-muted h-8 w-24 animate-pulse rounded" />
<div className="bg-muted h-4 w-32 animate-pulse rounded" />
</div>
) : (
<>
<div className="text-3xl font-bold">{value}</div>
{(trend || description) && (
<div className="text-muted-foreground mt-1 flex items-center gap-1 text-xs">
{trend && (
<span
className={cn(
"flex items-center gap-0.5 font-medium",
trendDirection === "up" && "text-green-600",
trendDirection === "down" && "text-red-600",
)}
>
{trendDirection === "up" && (
<LuArrowUp className="h-3 w-3" />
)}
{trendDirection === "down" && (
<LuArrowDown className="h-3 w-3" />
)}
{trendDirection === "neutral" && (
<LuMinus className="h-3 w-3" />
)}
{Math.abs(trend.value)}%
</span>
)}
{trend && <span>{trend.label}</span>}
{!trend && description && <span>{description}</span>}
</div>
)}
</>
)}
</CardContent>
</Card>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { StatsCard } from "./StatsCard";
Loading
Loading