Skip to content
Merged
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
5 changes: 2 additions & 3 deletions apps/code/src/app/dashboard/(main)/billing/page.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import { fetchServerData } from "@/lib/server-api";
import { fetchServerData, getDevPlanStatus } from "@/lib/server-api";

import BillingClient from "./BillingClient";

import type { DevPlanStatus } from "@/app/dashboard/useDevPlanStatus";
import type { paths } from "@/lib/api/v1";

type PaymentMethod =
paths["/dev-plans/payment-method"]["get"]["responses"]["200"]["content"]["application/json"];

export default async function BillingPage() {
const [devPlanStatus, paymentMethod] = await Promise.all([
fetchServerData<DevPlanStatus>("GET", "/dev-plans/status"),
getDevPlanStatus(),
fetchServerData<PaymentMethod>("GET", "/dev-plans/payment-method"),
]);

Expand Down
9 changes: 3 additions & 6 deletions apps/code/src/app/dashboard/(main)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
import { redirect } from "next/navigation";

import DashboardShell from "@/app/dashboard/DashboardShell";
import { fetchServerData } from "@/lib/server-api";

import type { DevPlanStatus } from "@/app/dashboard/useDevPlanStatus";
import type { UserMe } from "@/hooks/useUser";
import { getDevPlanStatus, getUserMe } from "@/lib/server-api";

export default async function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
const [userData, devPlanStatus] = await Promise.all([
fetchServerData<UserMe>("GET", "/user/me"),
fetchServerData<DevPlanStatus>("GET", "/dev-plans/status"),
getUserMe(),
getDevPlanStatus(),
]);

if (!userData?.user) {
Expand Down
6 changes: 2 additions & 4 deletions apps/code/src/app/dashboard/(main)/profile/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { redirect } from "next/navigation";

import { fetchServerData } from "@/lib/server-api";
import { fetchServerData, getUserMe } from "@/lib/server-api";

import { ProfilePageClient } from "./ProfilePageClient";

Expand All @@ -9,8 +9,6 @@ import type { Metadata } from "next";

type ProfileResponse =
paths["/user/profile"]["get"]["responses"][200]["content"]["application/json"];
type UserMeResponse =
paths["/user/me"]["get"]["responses"][200]["content"]["application/json"];

export const metadata: Metadata = {
title: "Your Profile · DevPass",
Expand All @@ -20,7 +18,7 @@ export const metadata: Metadata = {
export default async function ProfilePage() {
const [profileData, userData] = await Promise.all([
fetchServerData<ProfileResponse>("GET", "/user/profile"),
fetchServerData<UserMeResponse>("GET", "/user/me"),
getUserMe(),
]);

if (!userData?.user) {
Expand Down
6 changes: 5 additions & 1 deletion apps/code/src/app/dashboard/DashboardShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,6 @@ export default function DashboardShell({
const { posthogKey, googleAdsPurchaseConversion, googleAdsSignupConversion } =
config;
const api = useApi();
const { stripe, isLoading: stripeLoading } = useStripe();
const queryClient = useQueryClient();

const { user } = useUser({
Expand All @@ -191,6 +190,11 @@ export default function DashboardShell({
const subscribeMutation = api.useMutation("post", "/dev-plans/subscribe");
const finalizeMutation = api.useMutation("post", "/dev-plans/finalize");
const setupSessionId = searchParams.get("setup_session_id");
// Stripe.js is only needed to finalize a checkout setup session, so skip
// loading it on every other dashboard view.
const { stripe, isLoading: stripeLoading } = useStripe(
Boolean(setupSessionId),
);
const signupMethod = searchParams.get("signup_method");
const signupTracked = useRef(false);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ import {
Terminal,
Zap,
} from "lucide-react";
import dynamic from "next/dynamic";
import Link from "next/link";
import { notFound, useRouter, useSearchParams } from "next/navigation";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { toast } from "sonner";

import { AgentModelUsageChart } from "@/app/dashboard/components/AgentModelUsageChart";
import {
type AgentDefinition,
AGENTS,
Expand All @@ -31,6 +31,16 @@ import { useApi, useFetchClient } from "@/lib/fetch-client";

import { buildAgentLogsCsv } from "@llmgateway/shared";

// The model usage chart pulls in recharts and renders below the fold, so
// keep it out of the agent detail page's initial bundle.
const AgentModelUsageChart = dynamic(
() =>
import("@/app/dashboard/components/AgentModelUsageChart").then(
(mod) => mod.AgentModelUsageChart,
),
{ ssr: false, loading: () => <div className="h-[280px]" /> },
);

type ModelSortColumn =
| "id"
| "provider"
Expand Down
10 changes: 9 additions & 1 deletion apps/code/src/app/dashboard/components/UsageOverview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

import { format, formatDistanceToNowStrict } from "date-fns";
import { Activity, Coins, Cpu, Gem, TrendingUp } from "lucide-react";
import dynamic from "next/dynamic";
import { usePostHog } from "posthog-js/react";
import { useEffect } from "react";

import { useAppConfig } from "@/lib/config";
import { useApi } from "@/lib/fetch-client";

import { AgentModelUsageChart } from "./AgentModelUsageChart";
import AllowanceExhaustedCard from "./AllowanceExhaustedCard";
import PayAsYouGoCard from "./PayAsYouGoCard";
import ResetPassCard from "./ResetPassCard";
Expand All @@ -17,6 +17,14 @@ import { UsageBar } from "./UsageBar";
import type { paths } from "@/lib/api/v1";
import type { DevPlanCycle } from "@llmgateway/shared";

// The model usage chart pulls in recharts and renders below the fold, so
// keep it out of the usage page's initial bundle.
const AgentModelUsageChart = dynamic(
() =>
import("./AgentModelUsageChart").then((mod) => mod.AgentModelUsageChart),
{ ssr: false, loading: () => <div className="h-[280px]" /> },
);

type ActivityResponse =
paths["/activity"]["get"]["responses"][200]["content"]["application/json"];
type ActivityItem = ActivityResponse["activity"][number];
Expand Down
20 changes: 12 additions & 8 deletions apps/code/src/components/ui/flickering-grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ export const FlickeringGrid: React.FC<FlickeringGridProps> = ({
}) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const [isInView, setIsInView] = useState(false);
// Ref instead of state: visibility only gates the animation loop, and
// keeping it out of the effect's dependencies stops every viewport
// crossing from tearing down and rebuilding the observers and buffers.
const isInViewRef = useRef(false);
const [canvasSize, setCanvasSize] = useState({ width: 0, height: 0 });

const memoizedColor = useMemo(() => {
Expand Down Expand Up @@ -149,7 +152,7 @@ export const FlickeringGrid: React.FC<FlickeringGridProps> = ({

let lastTime = 0;
const animate = (time: number) => {
if (!isInView || !gridParams) {
if (!isInViewRef.current || !gridParams) {
return;
}

Expand All @@ -176,15 +179,16 @@ export const FlickeringGrid: React.FC<FlickeringGridProps> = ({

intersectionObserver = new IntersectionObserver(
([entry]) => {
setIsInView(entry.isIntersecting);
const wasInView = isInViewRef.current;
isInViewRef.current = entry.isIntersecting;
if (entry.isIntersecting && !wasInView) {
lastTime = performance.now();
animationFrameId = requestAnimationFrame(animate);
}
},
{ threshold: 0 },
);
intersectionObserver.observe(canvas);

if (isInView) {
animationFrameId = requestAnimationFrame(animate);
}
}

return () => {
Expand All @@ -198,7 +202,7 @@ export const FlickeringGrid: React.FC<FlickeringGridProps> = ({
intersectionObserver.disconnect();
}
};
}, [setupCanvas, updateSquares, drawGrid, width, height, isInView]);
}, [setupCanvas, updateSquares, drawGrid, width, height]);

return (
<div
Expand Down
9 changes: 6 additions & 3 deletions apps/code/src/lib/public-profile.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { cache } from "react";

import { getConfig } from "@/lib/config-server";

import type { ProfileData } from "@/components/profile/ProfileView";

/**
* Fetch a public DevPass profile by username from the API. Returns null when
* the profile does not exist or is private. Used by the public profile page
* and its dynamic OG image.
* and its dynamic OG image. cache() dedupes the generateMetadata + page
* calls within a single request.
*/
export async function fetchPublicProfile(
export const fetchPublicProfile = cache(async function fetchPublicProfile(
username: string,
): Promise<ProfileData | null> {
const config = getConfig();
Expand All @@ -24,4 +27,4 @@ export async function fetchPublicProfile(
} catch {
return null;
}
}
});
14 changes: 14 additions & 0 deletions apps/code/src/lib/server-api.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { cookies } from "next/headers";
import createFetchClient from "openapi-fetch";
import { cache } from "react";

import { getConfig } from "./config-server";

import type { paths } from "./api/v1";
import type { DevPlanStatus } from "@/app/dashboard/useDevPlanStatus";
import type { UserMe } from "@/hooks/useUser";

// Typed client for public, unauthenticated endpoints. Unlike
// createServerApiClient below it never reads the request cookies, so pages
Expand Down Expand Up @@ -54,6 +57,17 @@ type PatchPaths = {
[P in keyof paths]: paths[P] extends { patch: any } ? P : never;
}[keyof paths];

// The dashboard layout and several pages request these endpoints
// independently in the same render pass; cache() collapses the duplicate
// round-trips into one per request.
export const getUserMe = cache(() =>
fetchServerData<UserMe>("GET", "/user/me"),
);

export const getDevPlanStatus = cache(() =>
fetchServerData<DevPlanStatus>("GET", "/dev-plans/status"),
);

export async function fetchServerData<T>(
method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH",
path: keyof paths,
Expand Down
9 changes: 7 additions & 2 deletions apps/code/src/lib/stripe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,18 @@ function getStripePromise(publishableKey: string) {
return stripePromise;
}

export function useStripe() {
// Pass enabled: false to skip loading Stripe.js entirely until the caller
// actually needs it — the script is ~200KB and phones home on load.
export function useStripe(enabled = true) {
const { stripePublishableKey } = useAppConfig();
const [stripe, setStripe] = useState<Stripe | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);

useEffect(() => {
if (!enabled) {
return;
}
getStripePromise(stripePublishableKey ?? FALLBACK_TEST_PUBLISHABLE_KEY)
.then((stripeInstance) => {
setStripe(stripeInstance);
Expand All @@ -35,7 +40,7 @@ export function useStripe() {
setError(err);
setIsLoading(false);
});
}, [stripePublishableKey]);
}, [enabled, stripePublishableKey]);

return { stripe, isLoading, error };
}
5 changes: 5 additions & 0 deletions apps/docs/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ const nextConfig: NextConfig = {
productionBrowserSourceMaps: true,
reactCompiler: true,
transpilePackages: ["shiki"],
// lib/source.ts imports the full lucide-react icon barrel; transform it
// to direct imports so the whole icon set stays out of the bundles.
experimental: {
optimizePackageImports: ["lucide-react"],
},

rewrites() {
return [
Expand Down
Loading
Loading