diff --git a/apps/admin/src/app/discounts/page.tsx b/apps/admin/src/app/discounts/page.tsx index 3c2b5c3781..27d6cfebeb 100644 --- a/apps/admin/src/app/discounts/page.tsx +++ b/apps/admin/src/app/discounts/page.tsx @@ -70,6 +70,7 @@ export default async function GlobalDiscountsPage() { model: string | null; discountPercent: number; reason: string | null; + expiresAt: string | null; }): Promise<{ success: boolean; error?: string }> { "use server"; @@ -79,6 +80,7 @@ export default async function GlobalDiscountsPage() { model: data.model, discountPercent: data.discountPercent, reason: data.reason, + expiresAt: data.expiresAt, }); if (!result) { @@ -143,6 +145,7 @@ export default async function GlobalDiscountsPage() { Model Discount Reason + Expires Created @@ -151,7 +154,7 @@ export default async function GlobalDiscountsPage() { {discounts.length === 0 ? (
@@ -189,6 +192,21 @@ export default async function GlobalDiscountsPage() { {discount.reason ?? "—"} + + {discount.expiresAt ? ( + + {formatDate(discount.expiresAt)} + + ) : ( + "Never" + )} + {formatDate(discount.createdAt)} diff --git a/apps/admin/src/app/organizations/[orgId]/discounts/page.tsx b/apps/admin/src/app/organizations/[orgId]/discounts/page.tsx index 54b619c6a8..efc8914f4e 100644 --- a/apps/admin/src/app/organizations/[orgId]/discounts/page.tsx +++ b/apps/admin/src/app/organizations/[orgId]/discounts/page.tsx @@ -84,6 +84,7 @@ export default async function OrganizationDiscountsPage({ model: string | null; discountPercent: number; reason: string | null; + expiresAt: string | null; }): Promise<{ success: boolean; error?: string }> { "use server"; @@ -93,6 +94,7 @@ export default async function OrganizationDiscountsPage({ model: data.model, discountPercent: data.discountPercent, reason: data.reason, + expiresAt: data.expiresAt, }); if (!result) { @@ -164,6 +166,7 @@ export default async function OrganizationDiscountsPage({ Model Discount Reason + Expires Created @@ -172,7 +175,7 @@ export default async function OrganizationDiscountsPage({ {discounts.length === 0 ? (
@@ -209,6 +212,21 @@ export default async function OrganizationDiscountsPage({ {discount.reason ?? "—"} + + {discount.expiresAt ? ( + + {formatDate(discount.expiresAt)} + + ) : ( + "Never" + )} + {formatDate(discount.createdAt)} diff --git a/apps/admin/src/components/discount-form.tsx b/apps/admin/src/components/discount-form.tsx index 3124133261..3acd295b29 100644 --- a/apps/admin/src/components/discount-form.tsx +++ b/apps/admin/src/components/discount-form.tsx @@ -36,6 +36,7 @@ interface DiscountFormProps { model: string | null; discountPercent: number; reason: string | null; + expiresAt: string | null; }) => Promise<{ success: boolean; error?: string }>; } @@ -53,6 +54,7 @@ export function DiscountForm({ const [model, setModel] = useState("__all__"); const [discountPercent, setDiscountPercent] = useState(""); const [reason, setReason] = useState(""); + const [expiresAt, setExpiresAt] = useState(""); // Filter mappings by selected provider const filteredMappings = useMemo(() => { @@ -131,6 +133,7 @@ export function DiscountForm({ model: model === "__all__" ? null : model, discountPercent: percent, reason: reason || null, + expiresAt: expiresAt ? new Date(expiresAt).toISOString() : null, }); setLoading(false); @@ -141,6 +144,7 @@ export function DiscountForm({ setModel("__all__"); setDiscountPercent(""); setReason(""); + setExpiresAt(""); router.refresh(); } else { setError(result.error ?? "Failed to create discount"); @@ -264,6 +268,19 @@ export function DiscountForm({ />
+
+ + setExpiresAt(e.target.value)} + /> +

+ Leave empty for a discount that never expires +

+
+ {error && (
{error} diff --git a/apps/admin/src/lib/api/v1.d.ts b/apps/admin/src/lib/api/v1.d.ts index 963fbf67d4..11e940fd5b 100644 --- a/apps/admin/src/lib/api/v1.d.ts +++ b/apps/admin/src/lib/api/v1.d.ts @@ -247,6 +247,53 @@ export interface paths { patch?: never; trace?: never; }; + "/public/discounts/model/{modelId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: { + parameters: { + query?: never; + header?: never; + path: { + modelId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Active global discounts for the specified model */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + discounts: { + id: string; + provider: string | null; + model: string | null; + discountPercent: string; + reason: string | null; + expiresAt: string | null; + createdAt: string; + }[]; + }; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/user/me": { parameters: { query?: never; @@ -3884,6 +3931,66 @@ export interface paths { patch?: never; trace?: never; }; + "/orgs/{id}/discounts": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Active discounts for the organization (org-specific and global) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + orgDiscounts: { + id: string; + organizationId: string | null; + provider: string | null; + model: string | null; + discountPercent: string; + reason: string | null; + expiresAt: string | null; + createdAt: string; + updatedAt: string; + }[]; + globalDiscounts: { + id: string; + organizationId: string | null; + provider: string | null; + model: string | null; + discountPercent: string; + reason: string | null; + expiresAt: string | null; + createdAt: string; + updatedAt: string; + }[]; + }; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/team/{organizationId}/members": { parameters: { query?: never; diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index ba9609b357..afd0b4d444 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -21,6 +21,7 @@ import { tracingMiddleware } from "./middleware/tracing.js"; import { beacon } from "./routes/beacon.js"; import { routes } from "./routes/index.js"; import { internalModels } from "./routes/internal-models.js"; +import { publicDiscounts } from "./routes/public-discounts.js"; import { referral } from "./routes/referral.js"; import { stripeRoutes } from "./stripe.js"; @@ -220,6 +221,8 @@ app.route("/", referral); app.route("/internal", internalModels); +app.route("/public/discounts", publicDiscounts); + app.doc("/json", config); app.get("/docs", swaggerUI({ url: "./json" })); diff --git a/apps/api/src/routes/organization.ts b/apps/api/src/routes/organization.ts index ceccd25c37..ae90853eed 100644 --- a/apps/api/src/routes/organization.ts +++ b/apps/api/src/routes/organization.ts @@ -5,7 +5,7 @@ import { z } from "zod"; import { userHasOrganizationAccess } from "@/utils/authorization.js"; import { logAuditEvent } from "@llmgateway/audit"; -import { db, eq, tables } from "@llmgateway/db"; +import { and, db, desc, eq, gte, isNull, or, tables } from "@llmgateway/db"; import type { ServerTypes } from "@/vars.js"; @@ -739,4 +739,82 @@ organization.openapi(getReferralStats, async (c) => { }); }); +const discountSchema = z.object({ + id: z.string(), + organizationId: z.string().nullable(), + provider: z.string().nullable(), + model: z.string().nullable(), + discountPercent: z.string(), + reason: z.string().nullable(), + expiresAt: z.date().nullable(), + createdAt: z.date(), + updatedAt: z.date(), +}); + +const getOrgDiscounts = createRoute({ + method: "get", + path: "/{id}/discounts", + request: { + params: z.object({ + id: z.string(), + }), + }, + responses: { + 200: { + content: { + "application/json": { + schema: z.object({ + orgDiscounts: z.array(discountSchema).openapi({}), + globalDiscounts: z.array(discountSchema).openapi({}), + }), + }, + }, + description: + "Active discounts for the organization (org-specific and global)", + }, + }, +}); + +organization.openapi(getOrgDiscounts, async (c) => { + const user = c.get("user"); + if (!user) { + throw new HTTPException(401, { + message: "Unauthorized", + }); + } + + const { id } = c.req.param(); + + const hasAccess = await userHasOrganizationAccess(user.id, id); + if (!hasAccess) { + throw new HTTPException(403, { + message: "You do not have access to this organization", + }); + } + + const now = new Date(); + const notExpired = or( + isNull(tables.discount.expiresAt), + gte(tables.discount.expiresAt, now), + ); + + const [orgDiscounts, globalDiscounts] = await Promise.all([ + db + .select() + .from(tables.discount) + .where(and(eq(tables.discount.organizationId, id), notExpired)) + .orderBy(desc(tables.discount.createdAt)), + db + .select() + .from(tables.discount) + .where(and(isNull(tables.discount.organizationId), notExpired)) + .orderBy(desc(tables.discount.createdAt)), + ]); + + return c.json({ + orgDiscounts, + globalDiscounts, + }); +}); + export default organization; diff --git a/apps/api/src/routes/public-discounts.ts b/apps/api/src/routes/public-discounts.ts new file mode 100644 index 0000000000..650ff95260 --- /dev/null +++ b/apps/api/src/routes/public-discounts.ts @@ -0,0 +1,66 @@ +import { createRoute, OpenAPIHono } from "@hono/zod-openapi"; +import { z } from "zod"; + +import { and, db, desc, eq, gte, isNull, or, tables } from "@llmgateway/db"; + +import type { ServerTypes } from "@/vars.js"; + +export const publicDiscounts = new OpenAPIHono(); + +const discountSchema = z.object({ + id: z.string(), + provider: z.string().nullable(), + model: z.string().nullable(), + discountPercent: z.string(), + reason: z.string().nullable(), + expiresAt: z.date().nullable(), + createdAt: z.date(), +}); + +const getModelDiscounts = createRoute({ + method: "get", + path: "/model/{modelId}", + request: { + params: z.object({ + modelId: z.string(), + }), + }, + responses: { + 200: { + content: { + "application/json": { + schema: z.object({ + discounts: z.array(discountSchema).openapi({}), + }), + }, + }, + description: "Active global discounts for the specified model", + }, + }, +}); + +publicDiscounts.openapi(getModelDiscounts, async (c) => { + const { modelId } = c.req.param(); + + const now = new Date(); + const notExpired = or( + isNull(tables.discount.expiresAt), + gte(tables.discount.expiresAt, now), + ); + + const discounts = await db + .select() + .from(tables.discount) + .where( + and( + isNull(tables.discount.organizationId), + or(isNull(tables.discount.model), eq(tables.discount.model, modelId)), + notExpired, + ), + ) + .orderBy(desc(tables.discount.createdAt)); + + return c.json({ + discounts, + }); +}); diff --git a/apps/code/src/lib/api/v1.d.ts b/apps/code/src/lib/api/v1.d.ts index 963fbf67d4..11e940fd5b 100644 --- a/apps/code/src/lib/api/v1.d.ts +++ b/apps/code/src/lib/api/v1.d.ts @@ -247,6 +247,53 @@ export interface paths { patch?: never; trace?: never; }; + "/public/discounts/model/{modelId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: { + parameters: { + query?: never; + header?: never; + path: { + modelId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Active global discounts for the specified model */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + discounts: { + id: string; + provider: string | null; + model: string | null; + discountPercent: string; + reason: string | null; + expiresAt: string | null; + createdAt: string; + }[]; + }; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/user/me": { parameters: { query?: never; @@ -3884,6 +3931,66 @@ export interface paths { patch?: never; trace?: never; }; + "/orgs/{id}/discounts": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Active discounts for the organization (org-specific and global) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + orgDiscounts: { + id: string; + organizationId: string | null; + provider: string | null; + model: string | null; + discountPercent: string; + reason: string | null; + expiresAt: string | null; + createdAt: string; + updatedAt: string; + }[]; + globalDiscounts: { + id: string; + organizationId: string | null; + provider: string | null; + model: string | null; + discountPercent: string; + reason: string | null; + expiresAt: string | null; + createdAt: string; + updatedAt: string; + }[]; + }; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/team/{organizationId}/members": { parameters: { query?: never; diff --git a/apps/playground/src/lib/api/v1.d.ts b/apps/playground/src/lib/api/v1.d.ts index 963fbf67d4..11e940fd5b 100644 --- a/apps/playground/src/lib/api/v1.d.ts +++ b/apps/playground/src/lib/api/v1.d.ts @@ -247,6 +247,53 @@ export interface paths { patch?: never; trace?: never; }; + "/public/discounts/model/{modelId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: { + parameters: { + query?: never; + header?: never; + path: { + modelId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Active global discounts for the specified model */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + discounts: { + id: string; + provider: string | null; + model: string | null; + discountPercent: string; + reason: string | null; + expiresAt: string | null; + createdAt: string; + }[]; + }; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/user/me": { parameters: { query?: never; @@ -3884,6 +3931,66 @@ export interface paths { patch?: never; trace?: never; }; + "/orgs/{id}/discounts": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Active discounts for the organization (org-specific and global) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + orgDiscounts: { + id: string; + organizationId: string | null; + provider: string | null; + model: string | null; + discountPercent: string; + reason: string | null; + expiresAt: string | null; + createdAt: string; + updatedAt: string; + }[]; + globalDiscounts: { + id: string; + organizationId: string | null; + provider: string | null; + model: string | null; + discountPercent: string; + reason: string | null; + expiresAt: string | null; + createdAt: string; + updatedAt: string; + }[]; + }; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/team/{organizationId}/members": { parameters: { query?: never; diff --git a/apps/ui/src/app/dashboard/[orgId]/org/discounts/discounts-client.tsx b/apps/ui/src/app/dashboard/[orgId]/org/discounts/discounts-client.tsx new file mode 100644 index 0000000000..8fe68ae409 --- /dev/null +++ b/apps/ui/src/app/dashboard/[orgId]/org/discounts/discounts-client.tsx @@ -0,0 +1,217 @@ +"use client"; + +import { Percent } from "lucide-react"; + +import { Countdown } from "@/components/countdown"; +import { useIsMobile } from "@/hooks/use-mobile"; +import { Badge } from "@/lib/components/badge"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/lib/components/card"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/lib/components/table"; + +interface Discount { + id: string; + organizationId: string | null; + provider: string | null; + model: string | null; + discountPercent: string; + reason: string | null; + expiresAt: string | null; + createdAt: string; + updatedAt: string; +} + +interface DiscountsClientProps { + data: { + orgDiscounts: Discount[]; + globalDiscounts: Discount[]; + }; +} + +function formatPercent(decimal: string): string { + return `${(parseFloat(decimal) * 100).toFixed(1)}%`; +} + +function DiscountRow({ + discount, + scope, +}: { + discount: Discount; + scope: "org" | "global"; +}) { + return ( + + + + {scope === "org" ? "Org" : "Global"} + + + + {discount.provider ? ( + {discount.provider} + ) : ( + All + )} + + + {discount.model ? ( + {discount.model} + ) : ( + All + )} + + + + {formatPercent(discount.discountPercent)} off + + + + {discount.expiresAt ? ( + + ) : ( + Never + )} + + + {discount.reason ?? "—"} + + + ); +} + +function DiscountCard({ + discount, + scope, +}: { + discount: Discount; + scope: "org" | "global"; +}) { + return ( + + +
+
+ + {scope === "org" ? "Org" : "Global"} + + + {formatPercent(discount.discountPercent)} off + +
+
+ Provider: + {discount.provider ? ( + {discount.provider} + ) : ( + All + )} +
+
+ Model: + {discount.model ? ( + {discount.model} + ) : ( + All + )} +
+
+ Expires: + {discount.expiresAt ? ( + + ) : ( + Never + )} +
+ {discount.reason && ( +
+ {discount.reason} +
+ )} +
+
+
+ ); +} + +export function DiscountsClient({ data }: DiscountsClientProps) { + const isMobile = useIsMobile(); + const allDiscounts = [ + ...data.orgDiscounts.map((d) => ({ ...d, scope: "org" as const })), + ...data.globalDiscounts.map((d) => ({ ...d, scope: "global" as const })), + ]; + + return ( +
+ + + Your Discounts + + Active discounts applied to your organization's API usage + + + + {allDiscounts.length === 0 ? ( +
+ +
+

+ No active discounts +

+

+ When discounts are applied to your organization, they'll + appear here. +

+
+
+ ) : isMobile ? ( +
+ {allDiscounts.map((discount) => ( + + ))} +
+ ) : ( +
+ + + + Scope + Provider + Model + Discount + Expires + Reason + + + + {allDiscounts.map((discount) => ( + + ))} + +
+
+ )} +
+
+
+ ); +} diff --git a/apps/ui/src/app/dashboard/[orgId]/org/discounts/page.tsx b/apps/ui/src/app/dashboard/[orgId]/org/discounts/page.tsx new file mode 100644 index 0000000000..ce5ce54d99 --- /dev/null +++ b/apps/ui/src/app/dashboard/[orgId]/org/discounts/page.tsx @@ -0,0 +1,63 @@ +import { fetchServerData } from "@/lib/server-api"; + +import { DiscountsClient } from "./discounts-client"; + +interface Discount { + id: string; + organizationId: string | null; + provider: string | null; + model: string | null; + discountPercent: string; + reason: string | null; + expiresAt: string | null; + createdAt: string; + updatedAt: string; +} + +interface DiscountsData { + orgDiscounts: Discount[]; + globalDiscounts: Discount[]; +} + +async function fetchDiscounts(orgId: string): Promise { + const data = await fetchServerData( + "GET", + "/orgs/{id}/discounts" as any, + { + params: { + path: { id: orgId }, + }, + }, + ); + + return data ?? { orgDiscounts: [], globalDiscounts: [] }; +} + +export default async function OrgDiscountsPage({ + params, +}: { + params: Promise<{ orgId: string }>; +}) { + const { orgId } = await params; + + if (!orgId) { + return ( +
+
+
+

+ Your Discounts +

+
+
+ No organization selected +
+
+
+ ); + } + + const data = await fetchDiscounts(orgId); + + return ; +} diff --git a/apps/ui/src/app/globals.css b/apps/ui/src/app/globals.css index 5b60b3143f..36e5aa89a4 100644 --- a/apps/ui/src/app/globals.css +++ b/apps/ui/src/app/globals.css @@ -32,6 +32,7 @@ --chart-3: oklch(0.398 0.07 227.392); --chart-4: oklch(0.828 0.189 84.429); --chart-5: oklch(0.769 0.188 70.08); + --surface-elevated: oklch(0.98 0.001 286); --radius: 0.625rem; --sidebar: oklch(0.985 0 0); --sidebar-foreground: oklch(0.141 0.005 285.823); @@ -68,6 +69,7 @@ --chart-3: oklch(0.769 0.188 70.08); --chart-4: oklch(0.627 0.265 303.9); --chart-5: oklch(0.645 0.246 16.439); + --surface-elevated: oklch(0.18 0.005 286); --sidebar: oklch(0.21 0.006 285.885); --sidebar-foreground: oklch(0.985 0 0); --sidebar-primary: oklch(0.488 0.243 264.376); @@ -115,6 +117,8 @@ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); --color-sidebar-border: var(--sidebar-border); --color-sidebar-ring: var(--sidebar-ring); + --color-surface-elevated: var(--surface-elevated); + --font-family-display: var(--font-display); --animate-marquee: marquee var(--duration) infinite linear; --animate-marquee-vertical: marquee-vertical var(--duration) linear infinite; @keyframes marquee { @@ -205,3 +209,24 @@ @apply bg-background text-foreground; } } + +@utility font-display { + font-family: var(--font-display), var(--font-sans), system-ui, sans-serif; +} + +@utility bg-noise { + &::before { + content: ""; + position: absolute; + inset: 0; + opacity: 0.03; + background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); + background-repeat: repeat; + background-size: 256px 256px; + pointer-events: none; + } +} + +@utility bg-surface-elevated { + background-color: var(--surface-elevated); +} diff --git a/apps/ui/src/app/layout.tsx b/apps/ui/src/app/layout.tsx index 78c3ae9a75..b1b65ac012 100644 --- a/apps/ui/src/app/layout.tsx +++ b/apps/ui/src/app/layout.tsx @@ -1,4 +1,4 @@ -import { Inter, Geist_Mono } from "next/font/google"; +import { Inter, Geist_Mono, Plus_Jakarta_Sans } from "next/font/google"; import { Providers } from "@/components/providers"; import { getConfig } from "@/lib/config-server"; @@ -20,6 +20,13 @@ const geistMono = Geist_Mono({ display: "swap", }); +const plusJakarta = Plus_Jakarta_Sans({ + variable: "--font-display", + subsets: ["latin"], + weight: ["500", "600", "700", "800"], + display: "swap", +}); + export const dynamic = "force-dynamic"; export const metadata: Metadata = { @@ -134,7 +141,7 @@ export default function RootLayout({ children }: { children: ReactNode }) { /> {children} diff --git a/apps/ui/src/app/models/[name]/page.tsx b/apps/ui/src/app/models/[name]/page.tsx index a961b938a0..ab7de37214 100644 --- a/apps/ui/src/app/models/[name]/page.tsx +++ b/apps/ui/src/app/models/[name]/page.tsx @@ -15,6 +15,7 @@ import { notFound } from "next/navigation"; import Footer from "@/components/landing/footer"; import { Navbar } from "@/components/landing/navbar"; import { CopyModelName } from "@/components/models/copy-model-name"; +import { GlobalDiscountBanner } from "@/components/models/global-discount-banner"; import { ModelProviderCard } from "@/components/models/model-provider-card"; import { ModelStatusBadgeAuto } from "@/components/models/model-status-badge-auto"; import { ProviderTabs } from "@/components/models/provider-tabs"; @@ -397,6 +398,13 @@ export default async function ModelPage({ params }: PageProps) {
+
+ +
+

Select Provider diff --git a/apps/ui/src/components/countdown.tsx b/apps/ui/src/components/countdown.tsx new file mode 100644 index 0000000000..e917de4a35 --- /dev/null +++ b/apps/ui/src/components/countdown.tsx @@ -0,0 +1,72 @@ +"use client"; + +import { useEffect, useState } from "react"; + +interface CountdownProps { + expiresAt: string; +} + +function getTimeRemaining(expiresAt: string) { + const now = Date.now(); + const end = new Date(expiresAt).getTime(); + const expired = { + expired: true, + days: 0, + hours: 0, + minutes: 0, + seconds: 0, + diff: 0, + }; + + if (Number.isNaN(end) || end <= now) { + return expired; + } + + const diff = end - now; + const days = Math.floor(diff / (1000 * 60 * 60 * 24)); + const hours = Math.floor((diff / (1000 * 60 * 60)) % 24); + const minutes = Math.floor((diff / (1000 * 60)) % 60); + const seconds = Math.floor((diff / 1000) % 60); + + return { expired: false, days, hours, minutes, seconds, diff }; +} + +export function Countdown({ expiresAt }: CountdownProps) { + const [time, setTime] = useState(() => getTimeRemaining(expiresAt)); + + useEffect(() => { + const current = getTimeRemaining(expiresAt); + setTime(current); + + if (current.expired) { + return; + } + + const interval = setInterval( + () => { + setTime(getTimeRemaining(expiresAt)); + }, + current.diff < 60 * 60 * 1000 ? 1000 : 60000, + ); + + return () => clearInterval(interval); + }, [expiresAt]); + + if (time.expired) { + return Expired; + } + + if (time.days > 0) { + return ( + + {time.days}d {time.hours}h {time.minutes}m remaining + + ); + } + + return ( + + {time.hours}h {time.minutes}m {time.seconds}s remaining + + ); +} diff --git a/apps/ui/src/components/dashboard/animated-nav-icons.tsx b/apps/ui/src/components/dashboard/animated-nav-icons.tsx index 0fb1b39a46..eaebcbb014 100644 --- a/apps/ui/src/components/dashboard/animated-nav-icons.tsx +++ b/apps/ui/src/components/dashboard/animated-nav-icons.tsx @@ -268,6 +268,42 @@ export function AnimatedBotMessageSquare({ isHovered }: AnimatedIconProps) { ); } +// Percent — bounces on hover +export function AnimatedPercent({ isHovered }: AnimatedIconProps) { + return ( + + + + + + ); +} + // ExternalLink — arrow shoots out diagonally export function AnimatedExternalLink({ isHovered }: AnimatedIconProps) { return ( diff --git a/apps/ui/src/components/dashboard/dashboard-sidebar.tsx b/apps/ui/src/components/dashboard/dashboard-sidebar.tsx index 7683a9395c..f567946317 100644 --- a/apps/ui/src/components/dashboard/dashboard-sidebar.tsx +++ b/apps/ui/src/components/dashboard/dashboard-sidebar.tsx @@ -35,6 +35,7 @@ import { AnimatedLayoutDashboard, AnimatedMessageSquare, AnimatedSettings, + AnimatedPercent, AnimatedShield, AnimatedShieldAlert, } from "@/components/dashboard/animated-nav-icons"; @@ -403,6 +404,14 @@ function OrganizationSection({ isMobile={isMobile} toggleSidebar={toggleSidebar} /> + setSettingsHovered(true)} onMouseLeave={() => setSettingsHovered(false)} diff --git a/apps/ui/src/components/enterprise/features.tsx b/apps/ui/src/components/enterprise/features.tsx index 1fb3c1cfad..84ab4aab98 100644 --- a/apps/ui/src/components/enterprise/features.tsx +++ b/apps/ui/src/components/enterprise/features.tsx @@ -7,6 +7,7 @@ import { MessageSquare, Rocket, Shield, + ShieldCheck, Users, Wallet, } from "lucide-react"; @@ -74,6 +75,12 @@ const features = [ description: "Full-featured admin panel to manage organizations, monitor providers, track model performance, and control pricing.", }, + { + icon: ShieldCheck, + title: "SOC 2 Type II (In Progress)", + description: + "We are actively pursuing SOC 2 Type II certification to meet the highest standards of security, availability, and confidentiality for your data.", + }, ]; export function FeaturesEnterprise() { diff --git a/apps/ui/src/components/enterprise/hero.tsx b/apps/ui/src/components/enterprise/hero.tsx index 49a0fe9c96..6c6ca41d59 100644 --- a/apps/ui/src/components/enterprise/hero.tsx +++ b/apps/ui/src/components/enterprise/hero.tsx @@ -74,14 +74,14 @@ export function HeroEnterprise() {
- + ("python"); const { resolvedTheme } = useTheme(); const [mounted, setMounted] = useState(false); + const [copied, setCopied] = useState(false); useEffect(() => { setMounted(true); @@ -210,6 +218,8 @@ export function CodeExample() { const copyToClipboard = async (text: string, language: string) => { try { await navigator.clipboard.writeText(text); + setCopied(true); + setTimeout(() => setCopied(false), 2000); toast({ title: "Copied to clipboard", description: `${language} code snippet has been copied to your clipboard.`, @@ -229,113 +239,162 @@ export function CodeExample() { const currentExample = codeExamples[activeTab]; return ( -
+
-
-

- Integrate in Under 2 Minutes -

- -

- Already using OpenAI's SDK? Change one line—your base URL—and you're - done. Works with any language or framework. -

- - {/* Language Tabs */} -
-
+
+ {/* Left column: heading, description, bullets, tabs */} + +
+

+ Integration +

+

+ Drop-in compatible. +
+ Zero learning curve. +

+
+ +

+ Already using OpenAI's SDK? Change one line—your base URL—and + you're done. Works with any language or framework. +

+ +
    + {bullets.map((bullet) => ( +
  • +
    + {bullet} +
  • + ))} +
+ + {/* Vertical language tabs (desktop) */} +
{Object.entries(codeExamples).map(([key, example]) => ( ))}
-
- - {/* Code Block */} -
-
- - {currentExample.label} Example - - -
- -
- - {({ - className, - style, - tokens, - getLineProps, - getTokenProps, - }: { - className: string; - style: CSSProperties; - tokens: any[]; - getLineProps: (props: any) => any; - getTokenProps: (props: any) => any; - }) => ( -
+
+					{/* Right column: code block (sticky on desktop) */}
+					
+ {/* Faint glow behind code block */} +
+ + {/* Horizontal tabs (mobile) */} +
+
+ {Object.entries(codeExamples).map(([key, example]) => ( +
- )} -
+ {example.label} + + ))} +
-
-

- Every request is tracked with cost, latency, and token usage—giving - you visibility you don't get from providers directly. -

+
+ {/* macOS-style header */} +
+
+
+
+
+
+
+ + {currentExample.label} + +
+ +
+ +
+ + {({ + className, + style, + tokens, + getLineProps, + getTokenProps, + }) => ( +
+											{tokens.map((line, i) => {
+												const lineProps = getLineProps({ line });
+												return (
+													
+ {line.map((token, key) => { + const tokenProps = getTokenProps({ token }); + return ; + })} +
+ ); + })} +
+ )} +
+
+
+
diff --git a/apps/ui/src/components/landing/comparison.tsx b/apps/ui/src/components/landing/comparison.tsx index e9f897d1ce..1e086bf557 100644 --- a/apps/ui/src/components/landing/comparison.tsx +++ b/apps/ui/src/components/landing/comparison.tsx @@ -20,7 +20,7 @@ const comparisonData = [ { title: "Bring Your Own Keys", description: "Use your own provider API keys", - llmgateway: "1% tracking fee — pay providers directly", + llmgateway: "Free — pay providers directly", openrouter: "5% fee still applies with own API key", }, { @@ -126,7 +126,7 @@ export function Comparison() {
- Bring Your Own Keys with just 1% tracking fee + Bring Your Own Keys at no extra cost
diff --git a/apps/ui/src/components/landing/cta.tsx b/apps/ui/src/components/landing/cta.tsx index d79ac26e2a..e58b6ca4c3 100644 --- a/apps/ui/src/components/landing/cta.tsx +++ b/apps/ui/src/components/landing/cta.tsx @@ -1,39 +1,60 @@ "use client"; import { AuthLink } from "@/components/shared/auth-link"; import { Button } from "@/lib/components/button"; +import { ShimmerButton } from "@/lib/components/shimmer-button"; import { useAppConfig } from "@/lib/config"; +import { AnimatedGroup } from "./animated-group"; + export default function CallToAction() { const config = useAppConfig(); return ( -
-
+
+ {/* Gradient separator at top */} +
+ + {/* Atmospheric background */} +
+ + {/* Soft radial glow */} +
+ +
-

- Ready to Simplify Your LLM Integration? -

-

- Join teams processing 27B+ tokens through LLM Gateway. Start free, - no credit card required. -

+ +

+ Start routing requests +
+ in 30 seconds +

+

+ Join thousands of developers processing 100B+ tokens through LLM + Gateway. Free tier included, no credit card required. +

+
-
- + + + + Create Free Account + + -
+
diff --git a/apps/ui/src/components/landing/faq.tsx b/apps/ui/src/components/landing/faq.tsx index 7f761e6c85..b56837c527 100644 --- a/apps/ui/src/components/landing/faq.tsx +++ b/apps/ui/src/components/landing/faq.tsx @@ -18,7 +18,7 @@ const faqData = [ { question: "What models do you support?", answer: - "We support 180+ models across 60+ providers—including GPT-4o, Claude, Gemini, Llama, Mistral, and more. We add new releases within 48 hours of launch.", + "We support 210+ models across 25+ providers—including GPT-4o, Claude, Gemini, Llama, Mistral, and more. We add new releases within 48 hours of launch.", }, { question: "What is your uptime guarantee?", @@ -47,10 +47,7 @@ const faqSchema = { export function Faq() { return ( -
+