From 4924d1ec5e91d708a27bd93da3963494f3707d7d Mon Sep 17 00:00:00 2001 From: John Yeo Date: Thu, 16 Apr 2026 17:43:19 +0100 Subject: [PATCH 01/10] Revert "Store invoice amount_paid to fix refund status when credits applied" This reverts commit 4256a3d86045f82461bc4542cab5e0e27608f137. --- .../src/internal/invoices/InvoiceService.ts | 7 - .../invoices/actions/updateFromStripe.ts | 4 - .../invoices/utils/initInvoiceFromStripe.ts | 6 - .../cusModels/invoiceModels/invoiceModels.ts | 1 - .../cusModels/invoiceModels/invoiceTable.ts | 1 - .../components/sheets/InvoiceDetailSheet.tsx | 157 ++++++++++-------- .../components/sheets/RefundInvoiceDialog.tsx | 12 +- .../CustomerInvoiceStatus.tsx | 13 +- .../CustomerInvoicesColumns.tsx | 4 +- .../CustomerInvoicesTable.tsx | 2 +- .../customers2/customer/CustomerSheets.tsx | 10 +- 11 files changed, 107 insertions(+), 110 deletions(-) diff --git a/server/src/internal/invoices/InvoiceService.ts b/server/src/internal/invoices/InvoiceService.ts index 6b3aea1f7..abdd98829 100644 --- a/server/src/internal/invoices/InvoiceService.ts +++ b/server/src/internal/invoices/InvoiceService.ts @@ -216,11 +216,6 @@ export class InvoiceService { currency: stripeInvoice.currency, }); - const atmnAmountPaid = stripeToAtmnAmount({ - amount: stripeInvoice.amount_paid, - currency: stripeInvoice.currency, - }); - const invoice: Invoice = { id: generateId("inv"), internal_customer_id: internalCustomerId, @@ -234,7 +229,6 @@ export class InvoiceService { // Stripe stuff total: atmnTotal, - amount_paid: atmnAmountPaid, refunded_amount: 0, currency: stripeInvoice.currency, discounts: getInvoiceDiscounts({ @@ -329,7 +323,6 @@ export class InvoiceService { hosted_invoice_url: invoice.hosted_invoice_url, discounts: invoice.discounts, total: invoice.total, - amount_paid: invoice.amount_paid, product_ids: invoice.product_ids?.length ? sql`CASE WHEN ${invoices.product_ids} IS NULL OR cardinality(${invoices.product_ids}) = 0 diff --git a/server/src/internal/invoices/actions/updateFromStripe.ts b/server/src/internal/invoices/actions/updateFromStripe.ts index 1b9e0a938..1046955e2 100644 --- a/server/src/internal/invoices/actions/updateFromStripe.ts +++ b/server/src/internal/invoices/actions/updateFromStripe.ts @@ -29,10 +29,6 @@ export const updateInvoiceFromStripe = async ({ amount: stripeInvoice.total, currency: stripeInvoice.currency, }), - amount_paid: stripeToAtmnAmount({ - amount: stripeInvoice.amount_paid, - currency: stripeInvoice.currency, - }), }, }); diff --git a/server/src/internal/invoices/utils/initInvoiceFromStripe.ts b/server/src/internal/invoices/utils/initInvoiceFromStripe.ts index 460120c84..094ec9f37 100644 --- a/server/src/internal/invoices/utils/initInvoiceFromStripe.ts +++ b/server/src/internal/invoices/utils/initInvoiceFromStripe.ts @@ -50,11 +50,6 @@ export const initInvoiceFromStripe = async ({ currency: stripeInvoice.currency, }); - const atmnAmountPaid = stripeToAtmnAmount({ - amount: stripeInvoice.amount_paid, - currency: stripeInvoice.currency, - }); - return { id: generateId("inv"), internal_customer_id: internalCustomerId, @@ -66,7 +61,6 @@ export const initInvoiceFromStripe = async ({ status: stripeInvoice.status as string | undefined, internal_entity_id: internalEntityId || null, total: atmnTotal, - amount_paid: atmnAmountPaid, currency: stripeInvoice.currency, discounts: getInvoiceDiscounts({ expandedInvoice: stripeInvoice }), items: autumnInvoiceItems, diff --git a/shared/models/cusModels/invoiceModels/invoiceModels.ts b/shared/models/cusModels/invoiceModels/invoiceModels.ts index 056c46d61..6c9446468 100644 --- a/shared/models/cusModels/invoiceModels/invoiceModels.ts +++ b/shared/models/cusModels/invoiceModels/invoiceModels.ts @@ -39,7 +39,6 @@ export const InvoiceSchema = z.object({ // Total amount of the invoice total: z.number(), - amount_paid: z.number().nullish(), refunded_amount: z.number().default(0), currency: z.string(), receipt_url: z.string().nullish(), diff --git a/shared/models/cusModels/invoiceModels/invoiceTable.ts b/shared/models/cusModels/invoiceModels/invoiceTable.ts index 157344cc5..a37f321b9 100644 --- a/shared/models/cusModels/invoiceModels/invoiceTable.ts +++ b/shared/models/cusModels/invoiceModels/invoiceTable.ts @@ -28,7 +28,6 @@ export const invoices = pgTable( status: text("status").notNull().default("draft"), hosted_invoice_url: text("hosted_invoice_url"), total: numeric({ mode: "number" }).notNull().default(0), - amount_paid: numeric({ mode: "number" }), refunded_amount: numeric({ mode: "number" }).notNull().default(0), currency: text("currency").notNull().default("usd"), discounts: jsonb("discounts").$type().array().default([]), diff --git a/vite/src/views/customers2/components/sheets/InvoiceDetailSheet.tsx b/vite/src/views/customers2/components/sheets/InvoiceDetailSheet.tsx index 6ff7bf717..f1b584056 100644 --- a/vite/src/views/customers2/components/sheets/InvoiceDetailSheet.tsx +++ b/vite/src/views/customers2/components/sheets/InvoiceDetailSheet.tsx @@ -10,6 +10,8 @@ import { ArrowSquareOutIcon, CalendarBlankIcon, CreditCardIcon, + EyeIcon, + EyeSlashIcon, HashIcon, } from "@phosphor-icons/react"; import { format } from "date-fns"; @@ -23,7 +25,6 @@ import { SheetHeader, SheetSection } from "@/components/v2/sheets/InlineSheet"; import { useFeaturesQuery } from "@/hooks/queries/useFeaturesQuery"; import { useOrgStripeQuery } from "@/hooks/queries/useOrgStripeQuery"; import { useProductsQuery } from "@/hooks/queries/useProductsQuery"; -import { useSheetStore } from "@/hooks/stores/useSheetStore"; import { cn } from "@/lib/utils"; import { useEnv } from "@/utils/envUtils"; import { getStripeInvoiceLink } from "@/utils/linkUtils"; @@ -31,6 +32,11 @@ import { useCusQuery } from "@/views/customers/customer/hooks/useCusQuery"; import { CustomerInvoiceStatus } from "../table/customer-invoices/CustomerInvoiceStatus"; import { RefundInvoiceDialog } from "./RefundInvoiceDialog"; +interface InvoiceDetailSheetProps { + invoice: Invoice; + lineItems: InvoiceLineItem[]; +} + type LineItemGroup = { groupKey: string; label: string; @@ -58,19 +64,29 @@ const resolveFeatureName = ({ return feature?.name ?? featureId; }; -export function InvoiceDetailSheet() { - const sheetData = useSheetStore((s) => s.data); - const invoice = sheetData?.invoice as Invoice | undefined; - const lineItems = (sheetData?.lineItems as InvoiceLineItem[]) ?? []; - +export function InvoiceDetailSheet({ + invoice, + lineItems, +}: InvoiceDetailSheetProps) { const { stripeAccount } = useOrgStripeQuery(); const { features } = useFeaturesQuery(); const { products } = useProductsQuery(); const env = useEnv(); + const [showDescriptions, setShowDescriptions] = useState(false); const [refundDialogOpen, setRefundDialogOpen] = useState(false); const { customer } = useCusQuery(); + const isStripeCustomer = customer?.processor?.type === ProcessorType.Stripe; + const isFullyRefunded = + invoice.refunded_amount > 0 && + invoice.refunded_amount >= Math.abs(invoice.total); + const canRefund = + isStripeCustomer && + invoice.status === InvoiceStatus.Paid && + !isFullyRefunded; - + // Group line items: first by product_id, then within each product by + // stripe_subscription_item_id (for tiered) or individually. + // Sort: base price first, then alphabetically by label within each product. const productGroups = useMemo(() => { // Step 1: bucket line items by product_id const byProduct = new Map(); @@ -134,17 +150,6 @@ export function InvoiceDetailSheet() { return result; }, [lineItems, features, products]); - if (!invoice) return null; - - const isStripeCustomer = customer?.processor?.type === ProcessorType.Stripe; - const refundableAmount = Math.abs(invoice.amount_paid ?? invoice.total); - const isFullyRefunded = - invoice.refunded_amount > 0 && invoice.refunded_amount >= refundableAmount; - const canRefund = - isStripeCustomer && - invoice.status === InvoiceStatus.Paid && - !isFullyRefunded; - const formatAmount = (amount: number, currency: string) => { const absAmount = Math.abs(amount); return new Intl.NumberFormat("en-US", { @@ -195,7 +200,6 @@ export function InvoiceDetailSheet() { Invoice @@ -209,10 +213,24 @@ export function InvoiceDetailSheet() { key={productGroup.productId ?? "unknown"} withSeparator={true} > -
+
{productGroup.productName} +
@@ -223,6 +241,7 @@ export function InvoiceDetailSheet() { formatAmount={formatAmount} formatPeriod={formatPeriod} currency={invoice.currency} + showDescriptions={showDescriptions} /> ))}
@@ -230,21 +249,11 @@ export function InvoiceDetailSheet() { ))} -
-
- Total - - {formatSignedAmount(invoice.total, invoice.currency)} - -
- {invoice.amount_paid != null && invoice.amount_paid !== invoice.total && ( -
- Amount Paid - - {formatSignedAmount(invoice.amount_paid, invoice.currency)} - -
- )} +
+ Total + + {formatSignedAmount(invoice.total, invoice.currency)} +
@@ -275,7 +284,10 @@ export function InvoiceDetailSheet() { label="Created" value={ } @@ -319,11 +331,13 @@ function LineItemGroupRow({ formatAmount, formatPeriod, currency, + showDescriptions, }: { group: LineItemGroup; formatAmount: (amount: number, currency: string) => string; formatPeriod: (startMs: number | null, endMs: number | null) => string | null; currency: string; + showDescriptions: boolean; }) { const isSingleItem = group.items.length === 1; const firstItem = group.items[0]; @@ -359,19 +373,20 @@ function LineItemGroupRow({ {/* Group header with label and total */}
-
- {group.label} - {totalQuantity > 0 && ( - - Qty: {totalQuantity} - - )} -
- {firstItem.description && ( + {showDescriptions ? ( {firstItem.description} + ) : ( +
+ {group.label} + {totalQuantity > 0 && ( + + Qty: {totalQuantity} + + )} +
)} {period && {period}}
@@ -380,6 +395,7 @@ function LineItemGroupRow({
+ {/* Tier breakdown - each row has own hover */}
{group.items.map((item) => ( ))}
@@ -404,25 +421,26 @@ function LineItemGroupRow({
-
- {group.label} - {(!group.isBasePrice && firstItem.total_quantity) || - (group.isBasePrice && - firstItem.stripe_quantity && - firstItem.stripe_quantity > 1) ? ( - - Qty:{" "} - {group.isBasePrice - ? firstItem.stripe_quantity - : firstItem.total_quantity} - - ) : null} -
- {firstItem.description && ( + {showDescriptions ? ( {firstItem.description} + ) : ( +
+ {group.label} + {(!group.isBasePrice && firstItem.total_quantity) || + (group.isBasePrice && + firstItem.stripe_quantity && + firstItem.stripe_quantity > 1) ? ( + + Qty:{" "} + {group.isBasePrice + ? firstItem.stripe_quantity + : firstItem.total_quantity} + + ) : null} +
)} {period && {period}}
@@ -472,18 +490,23 @@ function TierRow({ hoverTexts, formatAmount, currency, + showDescriptions, }: { item: InvoiceLineItem; hoverTexts: { key: string; value: string }[]; formatAmount: (amount: number, currency: string) => string; currency: string; + showDescriptions: boolean; }) { const isRefund = item.direction === "refund"; + const quantityLabel = item.total_quantity ? `${item.total_quantity}` : ""; return (
- {item.description} + + {showDescriptions ? item.description : `${quantityLabel} units`} + { if (mode === "partial") { const parsed = Number.parseFloat(amount); @@ -79,8 +77,8 @@ export function RefundInvoiceDialog({ toast.error("Please enter a valid refund amount"); return; } - if (parsed > refundableAmount) { - toast.error("Refund amount cannot exceed the amount paid"); + if (parsed > invoice.total) { + toast.error("Refund amount cannot exceed invoice total"); return; } } @@ -88,7 +86,7 @@ export function RefundInvoiceDialog({ }; const formattedTotal = formatAmount({ - amount: refundableAmount, + amount: invoice.total, currency: invoice.currency, minFractionDigits: 2, amountFormatOptions: { @@ -102,7 +100,7 @@ export function RefundInvoiceDialog({ Refund Invoice - Amount paid: {formattedTotal} {invoice.currency.toUpperCase()} + Invoice total: {formattedTotal} {invoice.currency.toUpperCase()} @@ -131,7 +129,7 @@ export function RefundInvoiceDialog({ type="number" min="0.01" step="0.01" - max={refundableAmount} + max={invoice.total} placeholder="0.00" value={amount} onChange={(e) => setAmount(e.target.value)} diff --git a/vite/src/views/customers2/components/table/customer-invoices/CustomerInvoiceStatus.tsx b/vite/src/views/customers2/components/table/customer-invoices/CustomerInvoiceStatus.tsx index 5c416703d..aa7e68034 100644 --- a/vite/src/views/customers2/components/table/customer-invoices/CustomerInvoiceStatus.tsx +++ b/vite/src/views/customers2/components/table/customer-invoices/CustomerInvoiceStatus.tsx @@ -24,17 +24,14 @@ const statusConfig = { }; const getRefundStatus = ({ - amountPaid, total, refundedAmount, }: { - amountPaid: number; total: number; refundedAmount: number; }): { color: string; label: string } | null => { if (refundedAmount <= 0) return null; - const refundableAmount = Math.abs(amountPaid ?? total); - if (refundedAmount >= refundableAmount) { + if (refundedAmount >= Math.abs(total)) { return { color: "bg-amber-500 dark:bg-amber-600", label: "Fully Refunded", @@ -48,12 +45,10 @@ const getRefundStatus = ({ export function CustomerInvoiceStatus({ status, - amountPaid, total, refundedAmount, }: { status: InvoiceStatus | null | undefined; - amountPaid?: number | null; total?: number; refundedAmount?: number; }) { @@ -64,11 +59,7 @@ export function CustomerInvoiceStatus({ status === InvoiceStatus.Paid && total !== undefined && refundedAmount !== undefined - ? getRefundStatus({ - amountPaid: amountPaid ?? total, - total, - refundedAmount, - }) + ? getRefundStatus({ total, refundedAmount }) : null; const config = refundStatus ?? statusConfig[status]; diff --git a/vite/src/views/customers2/components/table/customer-invoices/CustomerInvoicesColumns.tsx b/vite/src/views/customers2/components/table/customer-invoices/CustomerInvoicesColumns.tsx index 6e683ca4a..560896f01 100644 --- a/vite/src/views/customers2/components/table/customer-invoices/CustomerInvoicesColumns.tsx +++ b/vite/src/views/customers2/components/table/customer-invoices/CustomerInvoicesColumns.tsx @@ -32,11 +32,10 @@ export const CustomerInvoicesColumns = [ accessorKey: "total", cell: ({ row }: { row: Row }) => { const invoice = row.original; - const displayTotal = invoice.amount_paid ?? invoice.total; const discountAmount = getTotalDiscountAmount(invoice); return (
- {displayTotal.toFixed(2)} {invoice.currency.toUpperCase()} + {invoice.total.toFixed(2)} {invoice.currency.toUpperCase()} {discountAmount > 0 && ( (-{discountAmount.toFixed(2)}) )} @@ -52,7 +51,6 @@ export const CustomerInvoicesColumns = [ return ( diff --git a/vite/src/views/customers2/components/table/customer-invoices/CustomerInvoicesTable.tsx b/vite/src/views/customers2/components/table/customer-invoices/CustomerInvoicesTable.tsx index 55e4721b6..c4254c14e 100644 --- a/vite/src/views/customers2/components/table/customer-invoices/CustomerInvoicesTable.tsx +++ b/vite/src/views/customers2/components/table/customer-invoices/CustomerInvoicesTable.tsx @@ -36,7 +36,7 @@ export function CustomerInvoicesTable() { ); const { lineItemsByInvoiceId } = useInvoiceLineItemsQuery({ - customerId: customer?.id || customer?.internal_id, + customerId: customer?.internal_id || customer?.id, invoiceIds, enabled: invoiceIds.length > 0, }); diff --git a/vite/src/views/customers2/customer/CustomerSheets.tsx b/vite/src/views/customers2/customer/CustomerSheets.tsx index 38c84d8f0..dfaabb0bb 100644 --- a/vite/src/views/customers2/customer/CustomerSheets.tsx +++ b/vite/src/views/customers2/customer/CustomerSheets.tsx @@ -1,3 +1,4 @@ +import type { Invoice, InvoiceLineItem } from "@autumn/shared"; import { AnimatePresence, motion } from "motion/react"; import { SheetContainer } from "@/components/v2/sheets/InlineSheet"; import { SheetCloseButton } from "@/components/v2/sheets/SheetCloseButton"; @@ -31,6 +32,7 @@ import { SHEET_ANIMATION } from "./customerAnimations"; export function CustomerSheets() { const isMobile = useIsMobile(); const sheetType = useSheetStore((s) => s.type); + const sheetData = useSheetStore((s) => s.data); const closeSheet = useSheetStore((s) => s.closeSheet); const closeBalanceSheet = useCustomerBalanceSheetStore((s) => s.closeSheet); useSheetEscapeHandler(); @@ -62,8 +64,12 @@ export function CustomerSheets() { return ; case "balance-create": return ; - case "invoice-detail": - return ; + case "invoice-detail": { + const invoice = sheetData?.invoice as Invoice | undefined; + const lineItems = (sheetData?.lineItems as InvoiceLineItem[]) ?? []; + if (!invoice) return null; + return ; + } case "sync-stripe": return ; case "billing-auto-topup-add": From 3753edaa20ebac15d21e4c44e970346af1434c2a Mon Sep 17 00:00:00 2001 From: John Yeo Date: Thu, 16 Apr 2026 17:43:23 +0100 Subject: [PATCH 02/10] Revert "Fix invoice detail sheet styling to match subscription detail sheet" This reverts commit 96001829ebe06419dd88a74d4b609f4588022c4f. --- .../components/sheets/InvoiceDetailSheet.tsx | 157 +++++++++--------- 1 file changed, 75 insertions(+), 82 deletions(-) diff --git a/vite/src/views/customers2/components/sheets/InvoiceDetailSheet.tsx b/vite/src/views/customers2/components/sheets/InvoiceDetailSheet.tsx index f1b584056..4fa67236e 100644 --- a/vite/src/views/customers2/components/sheets/InvoiceDetailSheet.tsx +++ b/vite/src/views/customers2/components/sheets/InvoiceDetailSheet.tsx @@ -8,11 +8,8 @@ import { import { ArrowCounterClockwiseIcon, ArrowSquareOutIcon, - CalendarBlankIcon, - CreditCardIcon, EyeIcon, EyeSlashIcon, - HashIcon, } from "@phosphor-icons/react"; import { format } from "date-fns"; import { useMemo, useState } from "react"; @@ -20,7 +17,6 @@ import { AdminHover } from "@/components/general/AdminHover"; import { Badge } from "@/components/v2/badges/Badge"; import { Button } from "@/components/v2/buttons/Button"; import { MiniCopyButton } from "@/components/v2/buttons/CopyButton"; -import { InfoRow } from "@/components/v2/InfoRow"; import { SheetHeader, SheetSection } from "@/components/v2/sheets/InlineSheet"; import { useFeaturesQuery } from "@/hooks/queries/useFeaturesQuery"; import { useOrgStripeQuery } from "@/hooks/queries/useOrgStripeQuery"; @@ -193,7 +189,7 @@ export function InvoiceDetailSheet({ }; return ( -
+
@@ -208,94 +204,91 @@ export function InvoiceDetailSheet({ description={`${formatDate(invoice.created_at)} • ${formatSignedAmount(invoice.total, invoice.currency)}`} /> - {productGroups.map((productGroup) => ( - -
- - {productGroup.productName} - - -
+
+ {/* Product groups with line items */} + {productGroups.map((productGroup) => ( + + {/* Product header with description toggle */} +
+ + {productGroup.productName} + + +
-
- {productGroup.lineItemGroups.map((group) => ( - - ))} + {/* Line item groups within this product */} +
+ {productGroup.lineItemGroups.map((group) => ( + + ))} +
+ + ))} + + {/* Invoice Total */} + +
+ Total + + {formatSignedAmount(invoice.total, invoice.currency)} +
- ))} - -
- Total - - {formatSignedAmount(invoice.total, invoice.currency)} - -
-
- - -
- } - label="Invoice ID" - value={ + {/* Invoice Details - Compact */} + +
+
+ Invoice ID - } - /> - } - label="Stripe ID" - value={ +
+
+ Stripe ID - } - /> - } - label="Created" - value={ - - } - /> -
- +
+
+ Created + + {format(new Date(invoice.created_at), "MMM d, yyyy HH:mm")} + +
+
+
+
-
+ {/* Footer */} +
- -
-
-
-
- ); -} diff --git a/apps/website/app/not-found.tsx b/apps/website/app/not-found.tsx new file mode 100644 index 000000000..0f2f7ee20 --- /dev/null +++ b/apps/website/app/not-found.tsx @@ -0,0 +1,114 @@ +"use client"; +import { motion } from "motion/react"; +import Image from "next/image"; +import Link from "next/link"; +import { IconArrowRightSmall } from "@/app/constant"; +import Navbar from "@/components/navbar"; +import type { PageStyle } from "@/lib/types"; + +export default function NotFound() { + return ( +
+
+
+
+ + + +
+
+ + 404 Background City Base + + + + 404 Background City Glitch + +
+ +
+ Autumn Logo + +

+ Page not found +

+ +

+ The page you are looking for doesn't exist or has been moved. +

+ + + + +
+
+
+
+ ); +} diff --git a/apps/website/app/page.tsx b/apps/website/app/page.tsx new file mode 100644 index 000000000..f64ad1fde --- /dev/null +++ b/apps/website/app/page.tsx @@ -0,0 +1,44 @@ +import ElasticRecoil from "@/components/elastic-footer"; +import HomeSections from "@/components/home-sections"; +import Navbar from "@/components/navbar"; +import Preloader from "@/components/preloader"; +import type { PageStyle } from "@/lib/types"; + +export default function Home() { + return ( +
+ + +
+
+
+ +
+ +
+
+
+
+
+
+
+ + +
+
+
+
+
+
+
+
+
+ +
+ ); +} diff --git a/apps/website/app/privacy/page.js b/apps/website/app/privacy/page.js deleted file mode 100644 index 164ce0f21..000000000 --- a/apps/website/app/privacy/page.js +++ /dev/null @@ -1,191 +0,0 @@ -import AnimatedFooterImage from "@/components/animated-footer-image"; -import Footer from "@/components/footer"; -import Navbar from "@/components/navbar"; - -const privacyTerms = [ - { - title: "1. Acceptance of Terms", - content: - 'By accessing or using the Autumn platform, APIs, SDKs, or any related services (collectively, the "Services") provided by Rebase, Inc., a Delaware corporation ("Autumn," "we," "us," or "our"), you agree to be bound by these Terms of Service ("Terms"). If you do not agree to these Terms, do not use our Services.', - isUppercase: false, - }, - { - title: "2. Description of Services", - content: - "Autumn provides billing infrastructure, including usage-based billing, credits management, pricing configuration, and entitlements management for software applications. Our Services are designed for developers and businesses building products that require flexible billing and pricing capabilities.", - isUppercase: false, - }, - { - title: "3. Account Registration", - content: - "To use certain features of our Services, you must create an account. You agree to provide accurate, current, and complete information during registration and to update such information as necessary. You are responsible for maintaining the confidentiality of your account credentials and for all activities that occur under your account.", - isUppercase: false, - }, - { - title: "4. Acceptable Use", - content: - "You agree not to use the Services for any unlawful purpose or in violation of any applicable laws; (b) interfere with or disrupt the integrity or performance of the Services; (c) attempt to gain unauthorized access to the Services or related systems; (d) use the Services to transmit malicious code or harmful content; or (e) resell or redistribute the Services without our prior written consent.", - isUppercase: false, - }, - { - title: "5. Payment Terms", - content: - "Fees for the Services are set forth on our pricing page or in a separate agreement. You agree to pay all applicable fees in accordance with the payment terms. All fees are non-refundable except as expressly stated otherwise. We reserve the right to modify our pricing with 30 days' notice.", - isUppercase: false, - }, - { - title: "6. Data and Privacy", - content: - 'Your use of the Services is subject to our Privacy Policy. You retain ownership of any data you submit to the Services ("Customer Data"). You grant us a limited license to process Customer Data solely to provide the Services. We implement reasonable security measures to protect Customer Data, but you acknowledge that no system is completely secure.', - isUppercase: false, - }, - { - title: "7. Intellectual Property", - content: - "The Services, including all software, APIs, documentation, and related materials, are owned by Autumn and protected by intellectual property laws. We grant you a limited, non-exclusive, non-transferable license to use the Services in accordance with these Terms. You may not copy, modify, or create derivative works of the Services except as expressly permitted.", - isUppercase: false, - }, - { - title: "8. Confidentiality", - content: - "Each party agrees to maintain the confidentiality of any non-public information disclosed by the other party and to use such information only for the purposes of these Terms. This obligation does not apply to information that is publicly available, independently developed, or rightfully received from a third party.", - isUppercase: false, - }, - { - title: "9. Warranties and Disclaimers", - content: - 'THE SERVICES ARE PROVIDED "AS IS" AND "AS AVAILABLE" WITHOUT WARRANTIES OF ANY KIND, WHETHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. WE DO NOT WARRANT THAT THE SERVICES WILL BE UNINTERRUPTED OR ERROR-FREE.', - isUppercase: true, - }, - { - title: "10. Limitation of Liability", - content: - "TO THE MAXIMUM EXTENT PERMITTED BY LAW, AUTUMN SHALL NOT BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES, OR ANY LOSS OF PROFITS OR REVENUES. OUR TOTAL LIABILITY FOR ANY CLAIMS ARISING FROM THESE TERMS SHALL NOT EXCEED THE AMOUNTS PAID BY YOU TO AUTUMN IN THE TWELVE MONTHS PRECEDING THE CLAIM.", - isUppercase: true, - }, - { - title: "11. Indemnification", - content: - "You agree to indemnify, defend, and hold harmless Autumn and its officers, directors, employees, and agents from any claims, liabilities, damages, losses, or expenses arising from your use of the Services, your violation of these Terms, or your infringement of any third-party rights.", - isUppercase: false, - }, - { - title: "12. Term and Termination", - content: - "These Terms remain in effect until terminated. Either party may terminate for convenience with 30 days' written notice. We may suspend or terminate your access immediately if you breach these Terms. Upon termination, your right to use the Services ceases, and you must pay any outstanding fees.", - isUppercase: false, - }, - { - title: "13. Modifications", - content: - "We may modify these Terms at any time by posting the revised Terms on our website. Material changes will be communicated with at least 30 days' notice. Your continued use of the Services after such changes constitutes acceptance of the modified Terms.", - isUppercase: false, - }, - { - title: "14. Governing Law and Disputes", - content: - "These Terms are governed by the laws of the State of Delaware, without regard to conflict of law principles. Any disputes arising from these Terms shall be resolved through binding arbitration in accordance with the rules of the American Arbitration Association, except that either party may seek injunctive relief in any court of competent jurisdiction.", - isUppercase: false, - }, - { - title: "15. General Provisions", - content: - "These Terms constitute the entire agreement between you and Autumn regarding the Services. If any provision is found unenforceable, the remaining provisions will continue in effect. Our failure to enforce any right or provision does not constitute a waiver. You may not assign these Terms without our prior written consent.", - isUppercase: false, - }, - { - title: "16. Contact Information", - content: ( - <> - For questions about these Terms, please contact us at - security@useautumn.com or at: -
- Rebase, Inc. (d/b/a Autumn) -
- Email: security@useautumn.com -
- Website: https://useautumn.com - - ), - isUppercase: false, - }, -]; - -export default function PrivacyPolicy() { - return ( -
-
-
-
-
- -
-
- -
-
-
-
-
-
-
- -
-
-
-
-

- Terms of Service -

-
-
-
- -
-
-
-

- Autumn (Rebase, Inc.) -
- Effective Date: February 1, 2025 -

- - {privacyTerms.map((term, index) => ( -
-

- {term.title} -

-

- {term.content} -

-
- ))} -
-
-
-
- -
-
- -
-
-
-
-
-
-
-
- -
- -
- ); -} diff --git a/apps/website/app/privacy/page.tsx b/apps/website/app/privacy/page.tsx new file mode 100644 index 000000000..0b6797725 --- /dev/null +++ b/apps/website/app/privacy/page.tsx @@ -0,0 +1,201 @@ +import type { ReactNode } from "react"; +import AnimatedFooterImage from "@/components/animated-footer-image"; +import Footer from "@/components/footer"; +import Navbar from "@/components/navbar"; +import type { PageStyle } from "@/lib/types"; +import { cn } from "@/lib/utils"; + +const privacyTerms: Array<{ + title: string; + content: ReactNode; + isUppercase: boolean; +}> = [ + { + title: "1. Acceptance of Terms", + content: + 'By accessing or using the Autumn platform, APIs, SDKs, or any related services (collectively, the "Services") provided by Rebase, Inc., a Delaware corporation ("Autumn," "we," "us," or "our"), you agree to be bound by these Terms of Service ("Terms"). If you do not agree to these Terms, do not use our Services.', + isUppercase: false, + }, + { + title: "2. Description of Services", + content: + "Autumn provides billing infrastructure, including usage-based billing, credits management, pricing configuration, and entitlements management for software applications. Our Services are designed for developers and businesses building products that require flexible billing and pricing capabilities.", + isUppercase: false, + }, + { + title: "3. Account Registration", + content: + "To use certain features of our Services, you must create an account. You agree to provide accurate, current, and complete information during registration and to update such information as necessary. You are responsible for maintaining the confidentiality of your account credentials and for all activities that occur under your account.", + isUppercase: false, + }, + { + title: "4. Acceptable Use", + content: + "You agree not to use the Services for any unlawful purpose or in violation of any applicable laws; (b) interfere with or disrupt the integrity or performance of the Services; (c) attempt to gain unauthorized access to the Services or related systems; (d) use the Services to transmit malicious code or harmful content; or (e) resell or redistribute the Services without our prior written consent.", + isUppercase: false, + }, + { + title: "5. Payment Terms", + content: + "Fees for the Services are set forth on our pricing page or in a separate agreement. You agree to pay all applicable fees in accordance with the payment terms. All fees are non-refundable except as expressly stated otherwise. We reserve the right to modify our pricing with 30 days' notice.", + isUppercase: false, + }, + { + title: "6. Data and Privacy", + content: + 'Your use of the Services is subject to our Privacy Policy. You retain ownership of any data you submit to the Services ("Customer Data"). You grant us a limited license to process Customer Data solely to provide the Services. We implement reasonable security measures to protect Customer Data, but you acknowledge that no system is completely secure.', + isUppercase: false, + }, + { + title: "7. Intellectual Property", + content: + "The Services, including all software, APIs, documentation, and related materials, are owned by Autumn and protected by intellectual property laws. We grant you a limited, non-exclusive, non-transferable license to use the Services in accordance with these Terms. You may not copy, modify, or create derivative works of the Services except as expressly permitted.", + isUppercase: false, + }, + { + title: "8. Confidentiality", + content: + "Each party agrees to maintain the confidentiality of any non-public information disclosed by the other party and to use such information only for the purposes of these Terms. This obligation does not apply to information that is publicly available, independently developed, or rightfully received from a third party.", + isUppercase: false, + }, + { + title: "9. Warranties and Disclaimers", + content: + 'THE SERVICES ARE PROVIDED "AS IS" AND "AS AVAILABLE" WITHOUT WARRANTIES OF ANY KIND, WHETHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. WE DO NOT WARRANT THAT THE SERVICES WILL BE UNINTERRUPTED OR ERROR-FREE.', + isUppercase: true, + }, + { + title: "10. Limitation of Liability", + content: + "TO THE MAXIMUM EXTENT PERMITTED BY LAW, AUTUMN SHALL NOT BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES, OR ANY LOSS OF PROFITS OR REVENUES. OUR TOTAL LIABILITY FOR ANY CLAIMS ARISING FROM THESE TERMS SHALL NOT EXCEED THE AMOUNTS PAID BY YOU TO AUTUMN IN THE TWELVE MONTHS PRECEDING THE CLAIM.", + isUppercase: true, + }, + { + title: "11. Indemnification", + content: + "You agree to indemnify, defend, and hold harmless Autumn and its officers, directors, employees, and agents from any claims, liabilities, damages, losses, or expenses arising from your use of the Services, your violation of these Terms, or your infringement of any third-party rights.", + isUppercase: false, + }, + { + title: "12. Term and Termination", + content: + "These Terms remain in effect until terminated. Either party may terminate for convenience with 30 days' written notice. We may suspend or terminate your access immediately if you breach these Terms. Upon termination, your right to use the Services ceases, and you must pay any outstanding fees.", + isUppercase: false, + }, + { + title: "13. Modifications", + content: + "We may modify these Terms at any time by posting the revised Terms on our website. Material changes will be communicated with at least 30 days' notice. Your continued use of the Services after such changes constitutes acceptance of the modified Terms.", + isUppercase: false, + }, + { + title: "14. Governing Law and Disputes", + content: + "These Terms are governed by the laws of the State of Delaware, without regard to conflict of law principles. Any disputes arising from these Terms shall be resolved through binding arbitration in accordance with the rules of the American Arbitration Association, except that either party may seek injunctive relief in any court of competent jurisdiction.", + isUppercase: false, + }, + { + title: "15. General Provisions", + content: + "These Terms constitute the entire agreement between you and Autumn regarding the Services. If any provision is found unenforceable, the remaining provisions will continue in effect. Our failure to enforce any right or provision does not constitute a waiver. You may not assign these Terms without our prior written consent.", + isUppercase: false, + }, + { + title: "16. Contact Information", + content: ( + <> + For questions about these Terms, please contact us at + security@useautumn.com or at: +
+ Rebase, Inc. (d/b/a Autumn) +
+ Email: security@useautumn.com +
+ Website: https://useautumn.com + + ), + isUppercase: false, + }, +]; + +export default function PrivacyPolicy() { + return ( +
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+

+ Terms of Service +

+
+
+
+ +
+
+
+

+ Autumn (Rebase, Inc.) +
+ Effective Date: February 1, 2025 +

+ + {privacyTerms.map((term, index) => ( +
+

+ {term.title} +

+

+ {term.content} +

+
+ ))} +
+
+
+
+ +
+
+ +
+
+
+
+
+
+
+
+ +
+ +
+ ); +} diff --git a/apps/website/components/animated-footer-image.jsx b/apps/website/components/animated-footer-image.jsx deleted file mode 100644 index ce7484254..000000000 --- a/apps/website/components/animated-footer-image.jsx +++ /dev/null @@ -1,25 +0,0 @@ -"use client"; -import Image from "next/image"; -import { forwardRef } from "react"; - -const AnimatedFooterImage = forwardRef(function AnimatedFooterImage(_, ref) { - return ( -
-
- footer background -
-
- ); -}); - -export default AnimatedFooterImage; diff --git a/apps/website/components/animated-footer-image.tsx b/apps/website/components/animated-footer-image.tsx new file mode 100644 index 000000000..c45b2414d --- /dev/null +++ b/apps/website/components/animated-footer-image.tsx @@ -0,0 +1,29 @@ +"use client"; +import Image from "next/image"; +import { forwardRef } from "react"; + +const AnimatedFooterImage = forwardRef( + function AnimatedFooterImage(_props, ref) { + return ( +
+
+ footer background +
+
+ ); + }, +); + +AnimatedFooterImage.displayName = "AnimatedFooterImage"; + +export default AnimatedFooterImage; diff --git a/apps/website/components/autumn-config.jsx b/apps/website/components/autumn-config.jsx deleted file mode 100644 index 9c5776af1..000000000 --- a/apps/website/components/autumn-config.jsx +++ /dev/null @@ -1,247 +0,0 @@ -"use client"; - -import Image from "next/image"; -import { useEffect, useLayoutEffect, useRef, useState } from "react"; -import { Light as SyntaxHighlighter } from "react-syntax-highlighter"; -import js from "react-syntax-highlighter/dist/esm/languages/hljs/javascript"; - -// useLayoutEffect runs synchronously before browser paint on the client, -// eliminating the container-query font-size race that causes CLS on reload. -const useIsomorphicLayoutEffect = - typeof window !== "undefined" ? useLayoutEffect : useEffect; - -SyntaxHighlighter.registerLanguage("javascript", js); - -const autumnTheme = { - hljs: { - display: "block", - background: "transparent", - color: "#BFBFBF", - padding: "0", - margin: "0", - }, - "hljs-comment": { color: "#6B6B6B" }, - "hljs-keyword": { color: "#9564ff" }, - "hljs-built_in": { color: "#BFBFBF" }, - "hljs-string": { color: "#2B8C3F" }, - "hljs-number": { color: "#9564ff" }, - "hljs-literal": { color: "#2B8C3F" }, - "hljs-attr": { color: "#FF1F12" }, - "hljs-property": { color: "#FF1F12" }, - "hljs-variable": { color: "#0161B5" }, - "hljs-title": { color: "#BFBFBF" }, - "hljs-params": { color: "#0161B5" }, - "hljs-punctuation": { color: "#BFBFBF" }, -}; - -const TOTAL_LINES = 21; -const LINE_HEIGHT = 24; - -const codeContent = `// Your entire billing integration -const { allowed, remaining } = await -autumn.check({ - featureId: "ai_tokens" -}); - -if (allowed) { - await autumn.track({ - featureId: "ai_tokens", - value: 1024 - }); -}`; - -const TYPING_SPEED = 10; - -export default function AutumnConfig({ - lines = TOTAL_LINES, - initialDelay = 0, - awaitEvent = null, -}) { - const fullCode = (() => { - const lineCount = codeContent.split("\n").length; - return codeContent + "\n".repeat(Math.max(0, lines - lineCount)); - })(); - const containerRef = useRef(null); - const [fontSize, setFontSize] = useState(16); - const [displayed, setDisplayed] = useState(""); - const [cursorVisible, setCursorVisible] = useState(true); - const [awaitDone, setAwaitDone] = useState(!awaitEvent); - const [started, setStarted] = useState(initialDelay === 0 && !awaitEvent); - const done = displayed.length >= fullCode.length; - - // Wait for the signal event before starting the delay countdown - useEffect(() => { - if (!awaitEvent) return; - const handler = () => setAwaitDone(true); - window.addEventListener(awaitEvent, handler, { once: true }); - return () => window.removeEventListener(awaitEvent, handler); - }, [awaitEvent]); - - // Start typing after awaitDone, respecting initialDelay - useEffect(() => { - if (!awaitDone) return; - const t = setTimeout(() => setStarted(true), initialDelay); - return () => clearTimeout(t); - }, [awaitDone, initialDelay]); - - useIsomorphicLayoutEffect(() => { - const el = containerRef.current; - if (!el) return; - const measure = () => - setFontSize(Math.min(Math.max(el.offsetWidth * 0.0385, 10), 20)); - measure(); - const ro = new ResizeObserver(measure); - ro.observe(el); - return () => ro.disconnect(); - }, []); - - const displayedPadded = - displayed + "\n".repeat(Math.max(0, lines - displayed.split("\n").length)); - useEffect(() => { - if (!started || done) return; - const timer = setTimeout(() => { - setDisplayed(fullCode.slice(0, displayed.length + 1)); - }, TYPING_SPEED); - return () => clearTimeout(timer); - }, [displayed, done, started]); - - // Blinking cursor after done - useEffect(() => { - if (!done) return; - const interval = setInterval(() => setCursorVisible((v) => !v), 530); - return () => clearInterval(interval); - }, [done]); - - return ( -
- {/* Title bar */} - {/*
-
- Box -
- - autumn.config.ts - -
- Box -
- - cross - -
*/} -
- {/* Left Vent: flex-1 makes it stretch, min-w-0 allows it to shrink below its content if needed */} -
-
-
-
-
- - {/* Filename: shrink-0 ensures the text never gets squashed */} - - billing.ts - - - {/* Right Vent: Matches the left one */} -
-
-
-
-
- - {/* Close Button Container */} -
- Box -
-
- - {/* Code area — fluidly driven by container query relative font sizes so height never distorts */} -
- {/* Dynamic-height inner box: 20 lines × 1.25em */} -
- - {displayedPadded} - -
- -
- -
- -
- -
-
-
- allowed: - true - remaining: - 8976 -
- 92ms -
-
-
-
- ); -} diff --git a/apps/website/components/autumn-config.tsx b/apps/website/components/autumn-config.tsx new file mode 100644 index 000000000..1c812830e --- /dev/null +++ b/apps/website/components/autumn-config.tsx @@ -0,0 +1,261 @@ +"use client"; + +import Image from "next/image"; +import { useEffect, useLayoutEffect, useRef, useState } from "react"; +import { Light as RawSyntaxHighlighter } from "react-syntax-highlighter"; +import js from "react-syntax-highlighter/dist/esm/languages/hljs/javascript"; + +const SyntaxHighlighter = RawSyntaxHighlighter as unknown as ((props: { + children: string; + customStyle?: Record; + language: string; + lineNumberStyle?: Record; + showLineNumbers?: boolean; + style?: Record>; +}) => JSX.Element) & { + registerLanguage: (name: string, language: unknown) => void; +}; + +// useLayoutEffect runs synchronously before browser paint on the client, +// eliminating the container-query font-size race that causes CLS on reload. +const useIsomorphicLayoutEffect = + typeof window !== "undefined" ? useLayoutEffect : useEffect; + +SyntaxHighlighter.registerLanguage("javascript", js); + +const autumnTheme = { + hljs: { + display: "block", + background: "transparent", + color: "#BFBFBF", + padding: "0", + margin: "0", + }, + "hljs-comment": { color: "#6B6B6B" }, + "hljs-keyword": { color: "#9564ff" }, + "hljs-built_in": { color: "#BFBFBF" }, + "hljs-string": { color: "#2B8C3F" }, + "hljs-number": { color: "#9564ff" }, + "hljs-literal": { color: "#2B8C3F" }, + "hljs-attr": { color: "#FF1F12" }, + "hljs-property": { color: "#FF1F12" }, + "hljs-variable": { color: "#0161B5" }, + "hljs-title": { color: "#BFBFBF" }, + "hljs-params": { color: "#0161B5" }, + "hljs-punctuation": { color: "#BFBFBF" }, +}; + +const TOTAL_LINES = 16; +const LINE_HEIGHT = 24; + +const codeContent = `// Your entire billing integration +const { allowed } = await check({ + featureId: "ai_tokens" +}); + +if (allowed) { + await track({ + featureId: "ai_tokens", + value: 1024 + }); +}`; + +const TYPING_SPEED = 10; + +export default function AutumnConfig({ + lines = TOTAL_LINES, + initialDelay = 0, + awaitEvent = null, +}: { + awaitEvent?: string | null; + initialDelay?: number; + lines?: number; +}) { + const fullCode = (() => { + const lineCount = codeContent.split("\n").length; + return codeContent + "\n".repeat(Math.max(0, lines - lineCount)); + })(); + const containerRef = useRef(null); + const [fontSize, setFontSize] = useState(16); + const [displayed, setDisplayed] = useState(""); + const [cursorVisible, setCursorVisible] = useState(true); + const [awaitDone, setAwaitDone] = useState(!awaitEvent); + const [started, setStarted] = useState(initialDelay === 0 && !awaitEvent); + const done = displayed.length >= fullCode.length; + + // Wait for the signal event before starting the delay countdown + useEffect(() => { + if (!awaitEvent) return; + const handler = () => setAwaitDone(true); + window.addEventListener(awaitEvent, handler, { once: true }); + return () => window.removeEventListener(awaitEvent, handler); + }, [awaitEvent]); + + // Start typing after awaitDone, respecting initialDelay + useEffect(() => { + if (!awaitDone) return; + const t = setTimeout(() => setStarted(true), initialDelay); + return () => clearTimeout(t); + }, [awaitDone, initialDelay]); + + useIsomorphicLayoutEffect(() => { + const el = containerRef.current; + if (!el) return; + const measure = () => + setFontSize(Math.min(Math.max(el.offsetWidth * 0.0385, 10), 20)); + measure(); + const ro = new ResizeObserver(measure); + ro.observe(el); + return () => ro.disconnect(); + }, []); + + const displayedPadded = + displayed + "\n".repeat(Math.max(0, lines - displayed.split("\n").length)); + useEffect(() => { + if (!started || done) return; + const timer = setTimeout(() => { + setDisplayed(fullCode.slice(0, displayed.length + 1)); + }, TYPING_SPEED); + return () => clearTimeout(timer); + }, [displayed, done, started, fullCode]); + + // Blinking cursor after done + useEffect(() => { + if (!done) return; + const interval = setInterval(() => setCursorVisible((v) => !v), 530); + return () => clearInterval(interval); + }, [done]); + + return ( +
+ {/* Title bar */} + {/*
+
+ Box +
+ + autumn.config.ts + +
+ Box +
+ + cross + +
*/} +
+ {/* Left Vent: flex-1 makes it stretch, min-w-0 allows it to shrink below its content if needed */} +
+
+
+
+
+ + {/* Filename: shrink-0 ensures the text never gets squashed */} + + billing.ts + + + {/* Right Vent: Matches the left one */} +
+
+
+
+
+ + {/* Close Button Container */} +
+ Box +
+
+ + {/* Code area — fluidly driven by container query relative font sizes so height never distorts */} +
+ {/* Dynamic-height inner box: 20 lines × 1.25em */} +
+ + {displayedPadded} + +
+ +
+ +
+ +
+ +
+
+
+ allowed: + true + remaining: + 8976 +
+ 92ms +
+
+
+
+ ); +} diff --git a/apps/website/components/blogComponents.jsx b/apps/website/components/blogComponents.jsx deleted file mode 100644 index 19ceab8f5..000000000 --- a/apps/website/components/blogComponents.jsx +++ /dev/null @@ -1,89 +0,0 @@ -import Link from "next/link"; - -function BlogHeading({ as: Tag, children, ...props }) { - return ( - - {children} - - ); -} - -export const mdxComponents = { - h1: (props) => , - h2: (props) => , - h3: (props) => , - h4: (props) => , - a: ({ href, children, ...props }) => { - const isExternal = href?.startsWith("http"); - if (isExternal) { - return ( - - {children} - - ); - } - return ( - - {children} - - ); - }, - pre: ({ children, ...props }) => ( -
-      {children}
-    
- ), - code: ({ children, ...props }) => { - const isInline = typeof children === "string"; - if (isInline && !props.className) { - return ( - - {children} - - ); - } - return {children}; - }, - blockquote: ({ children, ...props }) => ( -
- {children} -
- ), - hr: (props) =>
, - table: ({ children, ...props }) => ( -
- - {children} -
-
- ), - th: ({ children, ...props }) => ( - - {children} - - ), - td: ({ children, ...props }) => ( - - {children} - - ), -}; diff --git a/apps/website/components/blogComponents.tsx b/apps/website/components/blogComponents.tsx new file mode 100644 index 000000000..fc5f1ed6c --- /dev/null +++ b/apps/website/components/blogComponents.tsx @@ -0,0 +1,110 @@ +import Link from "next/link"; +import type { ComponentPropsWithoutRef, ElementType, ReactNode } from "react"; + +function BlogHeading({ + as: Tag, + children, + ...props +}: { + as: ElementType; + children?: ReactNode; +} & ComponentPropsWithoutRef<"h1">) { + return ( + + {children} + + ); +} + +export const mdxComponents = { + h1: (props: ComponentPropsWithoutRef<"h1">) => ( + + ), + h2: (props: ComponentPropsWithoutRef<"h2">) => ( + + ), + h3: (props: ComponentPropsWithoutRef<"h3">) => ( + + ), + h4: (props: ComponentPropsWithoutRef<"h4">) => ( + + ), + a: ({ href, children, ...props }: ComponentPropsWithoutRef<"a">) => { + const isExternal = href?.startsWith("http"); + if (isExternal) { + return ( + + {children} + + ); + } + return ( + + {children} + + ); + }, + pre: ({ children, ...props }: ComponentPropsWithoutRef<"pre">) => ( +
+			{children}
+		
+ ), + code: ({ children, ...props }: ComponentPropsWithoutRef<"code">) => { + const isInline = typeof children === "string"; + if (isInline && !props.className) { + return ( + + {children} + + ); + } + return {children}; + }, + blockquote: ({ + children, + ...props + }: ComponentPropsWithoutRef<"blockquote">) => ( +
+ {children} +
+ ), + hr: (props: ComponentPropsWithoutRef<"hr">) => ( +
+ ), + table: ({ children, ...props }: ComponentPropsWithoutRef<"table">) => ( +
+ + {children} +
+
+ ), + th: ({ children, ...props }: ComponentPropsWithoutRef<"th">) => ( + + {children} + + ), + td: ({ children, ...props }: ComponentPropsWithoutRef<"td">) => ( + + {children} + + ), +}; diff --git a/apps/website/components/dashboard-icon-pixel.jsx b/apps/website/components/dashboard-icon-pixel.jsx deleted file mode 100644 index 3758710dc..000000000 --- a/apps/website/components/dashboard-icon-pixel.jsx +++ /dev/null @@ -1,65 +0,0 @@ -"use client"; -import { forwardRef, useImperativeHandle, useRef, useEffect } from "react"; -import gsap from "gsap"; - -export const DashboardIconPixel = forwardRef(function DashboardIconPixel( - { Icon }, - ref, -) { - const iconRef = useRef(null); - const tlRef = useRef(null); - - useImperativeHandle(ref, () => ({ - restart: () => tlRef.current?.play(), - reverse: () => tlRef.current?.reverse(), - })); - - useEffect(() => { - const pixelEls = iconRef.current?.querySelectorAll(".icon-pixel-path"); - if (!pixelEls?.length) return; - - // Sort pixels by visual position: bottom-left → top-right diagonal - const pixels = Array.from(pixelEls).sort((a, b) => { - const aBox = a.getBBox(); - const bBox = b.getBBox(); - return ( - aBox.x + - aBox.width / 2 - - (aBox.y + aBox.height / 2) - - (bBox.x + bBox.width / 2 - (bBox.y + bBox.height / 2)) - ); - }); - - gsap.set(pixels, { - opacity: 0.15, - scale: 0.8, - transformOrigin: "left bottom", - fill: "currentColor", - }); - - tlRef.current = gsap.timeline({ paused: true }); - - tlRef.current - .to(pixels, { - opacity: 1, - scale: 1.15, - fill: "#FFFFFF", - duration: 0.2, - stagger: 0.01, - ease: "power2.out", - }) - .to(pixels, { - scale: 1, - duration: 0.01, - ease: "back.out(3)", - }); - - return () => tlRef.current?.kill(); - }, []); - - return ( - - - - ); -}); diff --git a/apps/website/components/dashboard-icon-pixel.tsx b/apps/website/components/dashboard-icon-pixel.tsx new file mode 100644 index 000000000..561b1e362 --- /dev/null +++ b/apps/website/components/dashboard-icon-pixel.tsx @@ -0,0 +1,70 @@ +"use client"; +import gsap from "gsap"; +import { forwardRef, useEffect, useImperativeHandle, useRef } from "react"; +import type { PixelHoverHandle, PixelIconComponent } from "@/lib/types"; + +export const DashboardIconPixel = forwardRef< + PixelHoverHandle, + { Icon: PixelIconComponent } +>(function DashboardIconPixel({ Icon }, ref) { + const iconRef = useRef(null); + const tlRef = useRef(null); + + useImperativeHandle(ref, () => ({ + restart: () => tlRef.current?.play(), + reverse: () => tlRef.current?.reverse(), + })); + + useEffect(() => { + const pixelEls = + iconRef.current?.querySelectorAll(".icon-pixel-path"); + if (!pixelEls?.length) return; + + // Sort pixels by visual position: bottom-left → top-right diagonal + const pixels = Array.from(pixelEls).sort((a, b) => { + const aBox = a.getBBox(); + const bBox = b.getBBox(); + return ( + aBox.x + + aBox.width / 2 - + (aBox.y + aBox.height / 2) - + (bBox.x + bBox.width / 2 - (bBox.y + bBox.height / 2)) + ); + }); + + gsap.set(pixels, { + opacity: 0.15, + scale: 0.8, + transformOrigin: "left bottom", + fill: "currentColor", + }); + + tlRef.current = gsap.timeline({ paused: true }); + + tlRef.current + .to(pixels, { + opacity: 1, + scale: 1.15, + fill: "#FFFFFF", + duration: 0.2, + stagger: 0.01, + ease: "power2.out", + }) + .to(pixels, { + scale: 1, + duration: 0.01, + ease: "back.out(3)", + }); + + return () => { + tlRef.current?.kill(); + }; + }, []); + + return ( + + + + ); +}); +DashboardIconPixel.displayName = "DashboardIconPixel"; diff --git a/apps/website/components/elastic-footer.jsx b/apps/website/components/elastic-footer.jsx deleted file mode 100644 index 20f2d120b..000000000 --- a/apps/website/components/elastic-footer.jsx +++ /dev/null @@ -1,143 +0,0 @@ -"use client"; -import { motion, useMotionValue, useSpring, useTransform } from "framer-motion"; -import { useEffect, useState } from "react"; -import AnimatedFooterImage from "./animated-footer-image"; - -export default function ElasticRecoil({ children }) { - const liftAmount = useMotionValue(0); - - const [showFooter, setShowFooter] = useState(false); - const [isMobile, setIsMobile] = useState(false); - - useEffect(() => { - const check = () => setIsMobile(window.innerWidth < 768); - check(); - window.addEventListener("resize", check); - return () => window.removeEventListener("resize", check); - }, []); - - const desktopSpring = { stiffness: 200, damping: 15, mass: 0.5 }; - const mobileSpring = { stiffness: 800, damping: 60, mass: 0.2 }; - const animatedLift = useSpring( - liftAmount, - isMobile ? mobileSpring : desktopSpring, - ); - const cappedMax = isMobile ? -420 : -580; - const y = useTransform(animatedLift, [0, 400], [0, cappedMax]); - - useEffect(() => { - return animatedLift.on("change", (v) => { - setShowFooter(v > 1); - }); - }, [animatedLift]); - - useEffect(() => { - let timeout; - let recoilFired = false; - let isTouching = false; // Track if finger is on screen - - const triggerRebound = () => { - liftAmount.set(0); - if (!recoilFired) { - recoilFired = true; - window.dispatchEvent(new CustomEvent("elastic-recoil")); - } - }; - - const handleWheel = (e) => { - const isAtBottom = - window.innerHeight + window.pageYOffset >= - document.documentElement.scrollHeight - 5; - - if (isAtBottom && e.deltaY > 0) { - const normalizedDelta = - e.deltaMode === 1 - ? e.deltaY * 20 - : e.deltaMode === 2 - ? e.deltaY * 300 - : e.deltaY; - liftAmount.set(Math.min(liftAmount.get() + normalizedDelta * 0.5, 400)); - recoilFired = false; - - clearTimeout(timeout); - timeout = setTimeout(() => { - if (!isTouching) { - triggerRebound(); - } - }, 1500); - } else if (e.deltaY < 0) { - if (!isTouching) { - liftAmount.set(0); - recoilFired = false; - } - } - }; - - let lastTouchY = 0; - let atBottomOnStart = false; - const handleTouchStart = (e) => { - isTouching = true; - lastTouchY = e.touches[0].clientY; - atBottomOnStart = - window.innerHeight + window.pageYOffset >= - document.documentElement.scrollHeight - 5; - recoilFired = false; - clearTimeout(timeout); - }; - - const handleTouchMove = (e) => { - const currentY = e.touches[0].clientY; - const delta = lastTouchY - currentY; - lastTouchY = currentY; - - if (!atBottomOnStart) { - atBottomOnStart = - window.innerHeight + window.pageYOffset >= - document.documentElement.scrollHeight - 5; - return; - } - - if (delta > 0) { - liftAmount.set(Math.min(liftAmount.get() + delta * 4, 400)); - recoilFired = false; - } else if (liftAmount.get() > 0) { - liftAmount.set(Math.max(0, liftAmount.get() + delta * 4)); - } - }; - - const handleTouchEnd = () => { - isTouching = false; - atBottomOnStart = false; - if (liftAmount.get() > 0) { - clearTimeout(timeout); - timeout = setTimeout(() => { - triggerRebound(); - }, 1500); - } - }; - - window.addEventListener("wheel", handleWheel, { passive: true }); - window.addEventListener("touchstart", handleTouchStart, { passive: true }); - window.addEventListener("touchmove", handleTouchMove, { passive: true }); - window.addEventListener("touchend", handleTouchEnd, { passive: true }); - window.addEventListener("touchcancel", handleTouchEnd, { passive: true }); - - return () => { - clearTimeout(timeout); - window.removeEventListener("wheel", handleWheel); - window.removeEventListener("touchstart", handleTouchStart); - window.removeEventListener("touchmove", handleTouchMove); - window.removeEventListener("touchend", handleTouchEnd); - window.removeEventListener("touchcancel", handleTouchEnd); - }; - }, [liftAmount]); - - return ( -
- {showFooter && } - - {children} - -
- ); -} diff --git a/apps/website/components/elastic-footer.tsx b/apps/website/components/elastic-footer.tsx new file mode 100644 index 000000000..a745acb57 --- /dev/null +++ b/apps/website/components/elastic-footer.tsx @@ -0,0 +1,144 @@ +"use client"; +import { motion, useMotionValue, useSpring, useTransform } from "framer-motion"; +import { useEffect, useState } from "react"; +import type { LayoutProps } from "@/lib/types"; +import AnimatedFooterImage from "./animated-footer-image"; + +export default function ElasticRecoil({ children }: LayoutProps) { + const liftAmount = useMotionValue(0); + + const [showFooter, setShowFooter] = useState(false); + const [isMobile, setIsMobile] = useState(false); + + useEffect(() => { + const check = () => setIsMobile(window.innerWidth < 768); + check(); + window.addEventListener("resize", check); + return () => window.removeEventListener("resize", check); + }, []); + + const desktopSpring = { stiffness: 200, damping: 15, mass: 0.5 }; + const mobileSpring = { stiffness: 800, damping: 60, mass: 0.2 }; + const animatedLift = useSpring( + liftAmount, + isMobile ? mobileSpring : desktopSpring, + ); + const cappedMax = isMobile ? -420 : -580; + const y = useTransform(animatedLift, [0, 400], [0, cappedMax]); + + useEffect(() => { + return animatedLift.on("change", (v) => { + setShowFooter(v > 1); + }); + }, [animatedLift]); + + useEffect(() => { + let timeout: ReturnType | null = null; + let recoilFired = false; + let isTouching = false; + + const triggerRebound = () => { + liftAmount.set(0); + if (!recoilFired) { + recoilFired = true; + window.dispatchEvent(new CustomEvent("elastic-recoil")); + } + }; + + const handleWheel = (e: WheelEvent) => { + const isAtBottom = + window.innerHeight + window.pageYOffset >= + document.documentElement.scrollHeight - 5; + + if (isAtBottom && e.deltaY > 0) { + const normalizedDelta = + e.deltaMode === 1 + ? e.deltaY * 20 + : e.deltaMode === 2 + ? e.deltaY * 300 + : e.deltaY; + liftAmount.set(Math.min(liftAmount.get() + normalizedDelta * 0.5, 400)); + recoilFired = false; + + if (timeout) clearTimeout(timeout); + timeout = setTimeout(() => { + if (!isTouching) { + triggerRebound(); + } + }, 1500); + } else if (e.deltaY < 0) { + if (!isTouching) { + liftAmount.set(0); + recoilFired = false; + } + } + }; + + let lastTouchY = 0; + let atBottomOnStart = false; + const handleTouchStart = (e: TouchEvent) => { + isTouching = true; + lastTouchY = e.touches[0].clientY; + atBottomOnStart = + window.innerHeight + window.pageYOffset >= + document.documentElement.scrollHeight - 5; + recoilFired = false; + if (timeout) clearTimeout(timeout); + }; + + const handleTouchMove = (e: TouchEvent) => { + const currentY = e.touches[0].clientY; + const delta = lastTouchY - currentY; + lastTouchY = currentY; + + if (!atBottomOnStart) { + atBottomOnStart = + window.innerHeight + window.pageYOffset >= + document.documentElement.scrollHeight - 5; + return; + } + + if (delta > 0) { + liftAmount.set(Math.min(liftAmount.get() + delta * 4, 400)); + recoilFired = false; + } else if (liftAmount.get() > 0) { + liftAmount.set(Math.max(0, liftAmount.get() + delta * 4)); + } + }; + + const handleTouchEnd = () => { + isTouching = false; + atBottomOnStart = false; + if (liftAmount.get() > 0) { + if (timeout) clearTimeout(timeout); + timeout = setTimeout(() => { + triggerRebound(); + }, 1500); + } + }; + + window.addEventListener("wheel", handleWheel, { passive: true }); + window.addEventListener("touchstart", handleTouchStart, { passive: true }); + window.addEventListener("touchmove", handleTouchMove, { passive: true }); + window.addEventListener("touchend", handleTouchEnd, { passive: true }); + window.addEventListener("touchcancel", handleTouchEnd, { passive: true }); + + return () => { + if (timeout) clearTimeout(timeout); + window.removeEventListener("wheel", handleWheel); + window.removeEventListener("touchstart", handleTouchStart); + window.removeEventListener("touchmove", handleTouchMove); + window.removeEventListener("touchend", handleTouchEnd); + window.removeEventListener("touchcancel", handleTouchEnd); + }; + }, [liftAmount]); + + return ( +
+ {showFooter && } + + {children} + +
+ ); +} diff --git a/apps/website/components/faq.jsx b/apps/website/components/faq.jsx deleted file mode 100644 index 2b95ca5d6..000000000 --- a/apps/website/components/faq.jsx +++ /dev/null @@ -1,147 +0,0 @@ -"use client"; -import { faqData, AnimatedPlusMinus } from "@/app/constant"; -import Image from "next/image"; -import { useState } from "react"; -import { motion, AnimatePresence } from "framer-motion"; - -export default function FAQ() { - const [openId, setOpenId] = useState(3); - - const springConfig = { - type: "spring", - stiffness: 280, - damping: 32, - mass: 1, - restDelta: 0.01, - }; - - const toggleAccordion = (id) => { - setOpenId(openId === id ? null : id); - }; - - return ( -
-
-
-

- - Frequently Asked - -
- Questions -

-
- -
- -
-
-
- -
- {faqData.map((faq) => { - const isOpen = openId === faq.id; - - return ( -
toggleAccordion(faq.id)} - className={`group relative border-b last:border-b-0 border-[#292929] cursor-pointer w-full flex flex-col justify-center transition-colors duration-300 ${isOpen ? "" : "hover:bg-[#080808]"}`} - > -
-
- - faq background -
-
- -
-
-

- {faq.question} -

-
- -
-
- -
-
- - {isOpen && ( - - {faq.answer.split("\n\n").map((paragraph, idx) => ( -

{paragraph}

- ))} -
- )} -
-
-
-
-
- ); - })} - - -
-
-
- ); -} diff --git a/apps/website/components/faq.tsx b/apps/website/components/faq.tsx new file mode 100644 index 000000000..c3a9d6c1a --- /dev/null +++ b/apps/website/components/faq.tsx @@ -0,0 +1,154 @@ +"use client"; +import { AnimatePresence, motion } from "framer-motion"; +import { useState } from "react"; +import { AnimatedPlusMinus, faqData } from "@/app/constant"; +import { cn } from "@/lib/utils"; + +export default function FAQ() { + const [openId, setOpenId] = useState(3); + + const springConfig = { + type: "spring" as const, + stiffness: 280, + damping: 32, + mass: 1, + restDelta: 0.01, + }; + + const toggleAccordion = (id: number) => { + setOpenId(openId === id ? null : id); + }; + + return ( +
+
+
+

+ + Frequently Asked + +
+ Questions +

+
+ +
+ +
+
+
+ +
+ {faqData.map((faq) => { + const isOpen = openId === faq.id; + + return ( +
toggleAccordion(faq.id)} + className={cn( + "group relative flex w-full cursor-pointer flex-col justify-center border-b border-[#292929] transition-colors duration-300 last:border-b-0", + !isOpen && "hover:bg-[#080808]", + )} + > +
+
+ + faq background +
+
+ +
+
+

+ {faq.question} +

+
+ +
+
+ +
+
+ + {isOpen && ( + + {faq.answer.split("\n\n").map((paragraph, idx) => ( +

{paragraph}

+ ))} +
+ )} +
+
+
+
+
+ ); + })} + + +
+
+
+ ); +} diff --git a/apps/website/components/feature-icon-animation.jsx b/apps/website/components/feature-icon-animation.jsx deleted file mode 100644 index bfcb978cb..000000000 --- a/apps/website/components/feature-icon-animation.jsx +++ /dev/null @@ -1,68 +0,0 @@ -"use client"; -import { forwardRef, useImperativeHandle, useRef, useEffect } from "react"; -import gsap from "gsap"; - -export const FeatureIconAnimation = forwardRef(({ Icon }, ref) => { - const iconRef = useRef(null); - const tlRef = useRef(null); - - useImperativeHandle(ref, () => ({ - play: () => tlRef.current?.play(), - reverse: () => tlRef.current?.reverse(), - })); - - useEffect(() => { - const pixelEls = iconRef.current?.querySelectorAll("path"); - if (!pixelEls?.length) return; - - // Sort pixels by distance from center — center pixels animate first, outer ones last - const centers = Array.from(pixelEls).map((el) => { - const b = el.getBBox(); - return { el, cx: b.x + b.width / 2, cy: b.y + b.height / 2 }; - }); - const midX = centers.reduce((s, c) => s + c.cx, 0) / centers.length; - const midY = centers.reduce((s, c) => s + c.cy, 0) / centers.length; - const pixels = centers - .sort((a, b) => { - const da = (a.cx - midX) ** 2 + (a.cy - midY) ** 2; - const db = (b.cx - midX) ** 2 + (b.cy - midY) ** 2; - return da - db; - }) - .map((c) => c.el); - - gsap.set(pixels, { - opacity: 0.15, - scale: 0.8, - transformOrigin: "center center", - fill: "currentColor", - }); - - tlRef.current = gsap.timeline({ paused: true }); - - tlRef.current - .to(pixels, { - opacity: 1, - scale: 1.15, - fill: "#9564ff", - duration: 0.2, - stagger: 0.04, - ease: "power2.out", - }) - .to(pixels, { - scale: 1, - duration: 0.15, - ease: "back.out(3)", - }); - - return () => tlRef.current?.kill(); - }, []); - - return ( -
-
- -
-
- ); -}); -FeatureIconAnimation.displayName = "FeatureIconAnimation"; diff --git a/apps/website/components/feature-icon-animation.tsx b/apps/website/components/feature-icon-animation.tsx new file mode 100644 index 000000000..74eddf187 --- /dev/null +++ b/apps/website/components/feature-icon-animation.tsx @@ -0,0 +1,75 @@ +"use client"; +import gsap from "gsap"; +import { forwardRef, useEffect, useImperativeHandle, useRef } from "react"; +import type { PixelAnimationHandle, PixelIconComponent } from "@/lib/types"; + +export const FeatureIconAnimation = forwardRef< + PixelAnimationHandle, + { Icon: PixelIconComponent } +>(({ Icon }, ref) => { + const iconRef = useRef(null); + const tlRef = useRef(null); + + useImperativeHandle(ref, () => ({ + play: () => tlRef.current?.play(), + reverse: () => tlRef.current?.reverse(), + })); + + useEffect(() => { + const pixelEls = + iconRef.current?.querySelectorAll("path"); + if (!pixelEls?.length) return; + + // Sort pixels by distance from center — center pixels animate first, outer ones last + const centers = Array.from(pixelEls).map((el) => { + const b = el.getBBox(); + return { el, cx: b.x + b.width / 2, cy: b.y + b.height / 2 }; + }); + const midX = centers.reduce((s, c) => s + c.cx, 0) / centers.length; + const midY = centers.reduce((s, c) => s + c.cy, 0) / centers.length; + const pixels = centers + .sort((a, b) => { + const da = (a.cx - midX) ** 2 + (a.cy - midY) ** 2; + const db = (b.cx - midX) ** 2 + (b.cy - midY) ** 2; + return da - db; + }) + .map((c) => c.el); + + gsap.set(pixels, { + opacity: 0.15, + scale: 0.8, + transformOrigin: "center center", + fill: "currentColor", + }); + + tlRef.current = gsap.timeline({ paused: true }); + + tlRef.current + .to(pixels, { + opacity: 1, + scale: 1.15, + fill: "#9564ff", + duration: 0.2, + stagger: 0.04, + ease: "power2.out", + }) + .to(pixels, { + scale: 1, + duration: 0.15, + ease: "back.out(3)", + }); + + return () => { + tlRef.current?.kill(); + }; + }, []); + + return ( +
+
+ +
+
+ ); +}); +FeatureIconAnimation.displayName = "FeatureIconAnimation"; diff --git a/apps/website/components/features.jsx b/apps/website/components/features.jsx deleted file mode 100755 index 4da1f75e0..000000000 --- a/apps/website/components/features.jsx +++ /dev/null @@ -1,73 +0,0 @@ -"use client"; -import { featuresData } from "@/app/constant"; -import { useRef } from "react"; -import { FeatureIconAnimation } from "./feature-icon-animation"; - -function FeatureCard({ feature }) { - const isDesktop = - typeof window !== "undefined" && - window.matchMedia("(hover: hover)").matches; - const iconRef = useRef(null); - return ( -
isDesktop && iconRef.current?.play()} - onMouseLeave={() => isDesktop && iconRef.current?.reverse()} - className="group relative flex px-4 md:px-4 flex-col justify-between p-6 bg-[#0F0F0F] min-h-[200px] md:min-h-[280px] border-r border-b border-[#292929] overflow-hidden cursor-pointer" - > -
-
- {/* Hover Gradient Overlay */} -
- -
- -
- -
-
-

- {feature.title} -

-

- {feature.description} -

-
-
-
- ); -} - -export default function Features() { - return ( -
-
-

-
Everything you need
-
for AI and usage billing.
-

-
- Your entire billing infrastructure, {" "} - - fully managed. - -
-
- -
-
- {featuresData.map((feature, i) => ( - - ))} - {/*
*/} -
-
- ); -} diff --git a/apps/website/components/features.tsx b/apps/website/components/features.tsx new file mode 100755 index 000000000..12bc08463 --- /dev/null +++ b/apps/website/components/features.tsx @@ -0,0 +1,72 @@ +"use client"; +import { useRef } from "react"; +import { featuresData } from "@/app/constant"; +import type { PixelAnimationHandle } from "@/lib/types"; +import { FeatureIconAnimation } from "./feature-icon-animation"; + +function FeatureCard({ feature }: { feature: (typeof featuresData)[number] }) { + const isDesktop = + typeof window !== "undefined" && + window.matchMedia("(hover: hover)").matches; + const iconRef = useRef(null); + return ( +
isDesktop && iconRef.current?.play()} + onMouseLeave={() => isDesktop && iconRef.current?.reverse()} + className="group relative flex px-4 md:px-4 flex-col justify-between p-6 bg-[#0F0F0F] min-h-[200px] md:min-h-[280px] border-r border-b border-[#292929] overflow-hidden cursor-pointer" + > +
+
+ {/* Hover Gradient Overlay */} +
+ +
+ +
+ +
+
+

+ {feature.title} +

+

+ {feature.description} +

+
+
+
+ ); +} + +export default function Features() { + return ( +
+
+

+
Everything you need
+
for AI and usage billing.
+

+
+ Your entire billing infrastructure,{" "} + fully managed. +
+
+ +
+
+ {featuresData.map((feature, i) => ( + + ))} + {/*
*/} +
+
+ ); +} diff --git a/apps/website/components/footer.jsx b/apps/website/components/footer.jsx deleted file mode 100644 index ef07d6bd8..000000000 --- a/apps/website/components/footer.jsx +++ /dev/null @@ -1,181 +0,0 @@ -import Image from "next/image"; -import Link from "next/link"; - -const footerColumns = [ - { - title: "PRODUCT", - links: [ - { label: "FEATURES", href: "#" }, - { label: "INTEGRATIONS", href: "#" }, - { label: "PRICING", href: "#" }, - { label: "CHANGELOG", href: "#" }, - { label: "ROADMAP", href: "#" }, - ], - }, - { - title: "COMPANY", - links: [ - { label: "OUR TEAM", href: "#" }, - { label: "OUR VALUES", href: "/privacy" }, - { label: "BLOG", href: "/blog" }, - ], - }, - { - title: "RESOURCES", - links: [ - { label: "DOWNLOADS", href: "https://useautumn.com/" }, - { label: "DOCUMENTATION", href: "https://docs.useautumn.com/welcome" }, - { label: "CONTACT", href: "https://cal.com/ayrod/a?user=ayrod" }, - ], - }, -]; - -export default function Footer() { - return ( - <> -
-
-
-
- -
- {/* Left Side: Logo and Description */} -
- - Autumn - -

- Autumn is built on top of Stripe Billing (for now), so their - fees (0.7%, and 2.9% + 30c) still apply. -

-
- - {/* Right Side: Columns */} -
- {footerColumns.map((col, index) => ( -
-

-
- {col.title} -

-
    - {col.links.map((link, i) => ( -
  • - - - - - - {link.label} - - - - -
  • - ))} -
-
- ))} -
-
-
- -
-
- {/* Social Left Box */} -
-
- SOCIAL -
-
- - - - LINKEDIN - - - - -
- - - - TWITTER - - - - -
-
- -
- Copyright © 2026 Autumn All rights reserved -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- - ); -} diff --git a/apps/website/components/footer.tsx b/apps/website/components/footer.tsx new file mode 100644 index 000000000..17942d4da --- /dev/null +++ b/apps/website/components/footer.tsx @@ -0,0 +1,180 @@ +import Image from "next/image"; +import Link from "next/link"; + +const footerColumns: any[] = [ + // { + // title: "PRODUCT", + // links: [ + // { label: "FEATURES", href: "#" }, + // { label: "INTEGRATIONS", href: "#" }, + // { label: "PRICING", href: "#" }, + // { label: "CHANGELOG", href: "#" }, + // { label: "ROADMAP", href: "#" }, + // ], + // }, + // { + // title: "COMPANY", + // links: [ + // { label: "OUR TEAM", href: "#" }, + // { label: "OUR VALUES", href: "/privacy" }, + // { label: "BLOG", href: "/blog" }, + // ], + // }, + // { + // title: "RESOURCES", + // links: [ + // { label: "DOWNLOADS", href: "https://useautumn.com/" }, + // { label: "DOCUMENTATION", href: "https://docs.useautumn.com/welcome" }, + // { label: "CONTACT", href: "https://cal.com/ayrod/a?user=ayrod" }, + // ], + // }, +]; + +export default function Footer() { + return ( + <> +
+
+
+
+ +
+ {/* Left Side: Logo and Description */} +
+ + Autumn + +

+ Autumn is drop-in billing for AI companies. +

+
+ + {/* Right Side: Columns */} +
+ {footerColumns.map((col, index) => ( +
+

+
+ {col.title} +

+
    + {col.links.map((link, i) => ( +
  • + + + + + + {link.label} + + + + +
  • + ))} +
+
+ ))} +
+
+
+ +
+
+ {/* Social Left Box */} +
+
+ SOCIAL +
+
+ + + + LINKEDIN + + + + +
+ + + + TWITTER + + + + +
+
+ +
+ Copyright © 2026 Autumn All rights reserved +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + ); +} diff --git a/apps/website/components/hero.tsx b/apps/website/components/hero.tsx new file mode 100755 index 000000000..e1a7f4455 --- /dev/null +++ b/apps/website/components/hero.tsx @@ -0,0 +1,307 @@ +"use client"; + +import { useGSAP } from "@gsap/react"; +import gsap from "gsap"; +import { motion } from "motion/react"; +import Image from "next/image"; +import Link from "next/link"; +import { useEffect, useRef, useState } from "react"; +import { CTALines, IconCTADocs, IconCTAStart } from "@/app/constant"; +import AutumnConfig from "./autumn-config"; + +// import dynamic from "next/dynamic"; + +// const AutumnConfig = dynamic(() => import("./autumn-config"), { ssr: false }); + +const BADGE_TEXT = "// 100% open source"; + +const getLoggedInHintCookie = () => { + if (typeof window === "undefined") return null; + return ( + document.cookie + .split("; ") + .find((row) => row.startsWith("logged_in_hint=")) + ?.split("=")[1] === "1" + ); +}; + +export default function Hero() { + const containerRef = useRef(null); + const heroTlRef = useRef(null); + const [displayedText, setDisplayedText] = useState(""); + const [isLoggedIn, setIsLoggedIn] = useState(false); + + // Read the hint cookie after mount to avoid SSR/CSR hydration mismatch. + useEffect(() => { + setIsLoggedIn(getLoggedInHintCookie() === true); + }, []); + + // Badge typewriter + useEffect(() => { + const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*0123456789"; + let intervalId: ReturnType | null = null; + let timeoutId: ReturnType | null = null; + + const startTypewriter = () => { + timeoutId = setTimeout(() => { + let iteration = 0; + intervalId = setInterval(() => { + setDisplayedText( + BADGE_TEXT.split("") + .map((char, index) => { + if (char === " ") return " "; + if (index < Math.floor(iteration)) return char; + if (index === Math.floor(iteration)) + return chars[Math.floor(Math.random() * chars.length)]; + return ""; + }) + .join(""), + ); + + iteration += 0.4; + + if (iteration >= BADGE_TEXT.length) { + if (intervalId) clearInterval(intervalId); + setDisplayedText(BADGE_TEXT); + } + }, 30); + }, 150); + }; + + window.addEventListener("preloader:complete", startTypewriter, { + once: true, + }); + return () => { + window.removeEventListener("preloader:complete", startTypewriter); + if (timeoutId) clearTimeout(timeoutId); + if (intervalId) clearInterval(intervalId); + }; + }, []); + + useEffect(() => { + const handler = () => heroTlRef.current?.play(); + window.addEventListener("preloader:complete", handler, { once: true }); + return () => window.removeEventListener("preloader:complete", handler); + }, []); + + useGSAP( + () => { + gsap.set(".hero-root", { opacity: 0 }); + + gsap.set(".hero-bg", { + opacity: 0, + filter: "blur(6px) brightness(1)", + scale: 0.97, + transformOrigin: "center top", + }); + + gsap.set(".hero-reveal", { + opacity: 0, + y: 25, + filter: "blur(12px)", + scale: 0.96, + transformOrigin: "center bottom", + }); + + gsap.set(".hero-cta", { opacity: 0, scale: 0.95 }); + + const tl = gsap.timeline({ + paused: true, + defaults: { overwrite: "auto" }, + }); + heroTlRef.current = tl; + + tl.to(".hero-root", { opacity: 1, duration: 0.3, ease: "none" }) + + .to(".hero-bg", { + opacity: 1, + filter: "blur(0px) brightness(1)", + scale: 1, + duration: 0.4, + ease: "power2.out", + }) + + .to(".hero-bg", { + filter: "blur(0px) brightness(1.6)", + duration: 0.125, + ease: "power2.in", + }) + + .to(".hero-bg", { + filter: "blur(0px) brightness(1)", + duration: 0.125, + ease: "power2.out", + }) + + .to( + ".hero-reveal", + { + opacity: 1, + y: 0, + filter: "blur(0px)", + scale: 1, + duration: 1.1, + stagger: 0.1, + ease: "power3.out", + }, + "-=0.2", + ) + + .to( + ".hero-cta", + { + opacity: 1, + scale: 1, + duration: 0.3, + stagger: 0.06, + ease: "back.out(1.5)", + }, + "-=0.1", + ); + }, + { scope: containerRef }, + ); + + return ( +
+
+
+
+

+ + + {displayedText} + +

+
+

+ + The drop-in billing layer for + {" "} + AI startups +

+

+ Stop rebuilding usage limits, credit ledgers and payment logic.{" "} + + Autumn is your customer database + {" "} + that scales from your first user to your largest contract. +

+
+
+
+
+
+
+ +
+
+
+
+
+ {/* Primary CTA */} +
+ + + {/* Adjusted px-3 for mobile, md:px-4 for desktop */} +
+ + + {isLoggedIn ? "Dashboard" : "Start for free"} + + + + +
+
+ +
+ + {/* Secondary CTA */} +
+ + +
+ + + Book a call + + + + +
+
+ +
+ +
+
+
+
+ +
+
+
+
+
+
+
+
+
+ {/* MOBILE VIEW*/} +
+
+
+
+
+
+ ); +} diff --git a/apps/website/components/home-sections.tsx b/apps/website/components/home-sections.tsx new file mode 100755 index 000000000..ccd6b7b4e --- /dev/null +++ b/apps/website/components/home-sections.tsx @@ -0,0 +1,46 @@ +"use client"; + +import dynamic from "next/dynamic"; +import Hero from "./hero"; +import SectionDivider from "./section-divider"; + +const ProductionScale = dynamic(() => import("@/components/production-scale"), { + ssr: false, +}); +const Problem = dynamic(() => import("@/components/problem"), { ssr: false }); +const Solution = dynamic(() => import("@/components/solution"), { ssr: false }); +const Features = dynamic(() => import("@/components/features"), { ssr: false }); +const PricingModels = dynamic(() => import("@/components/pricing-models"), { + ssr: false, +}); +const Testimonials = dynamic(() => import("@/components/testimonials"), { + ssr: false, +}); +const Pricing = dynamic(() => import("@/components/pricing"), { ssr: false }); +const FAQ = dynamic(() => import("@/components/faq"), { ssr: false }); +const Footer = dynamic(() => import("@/components/footer"), { ssr: false }); + +export default function HomeSections() { + return ( + <> + + + + + + + + + + + + + + + + + +