Compare commits

...

14 Commits

Author SHA1 Message Date
Jeff Emmett 6964364218 Add Open Graph and Twitter Card metadata
Generated with Claude Code

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-22 22:30:40 +01:00
Jeff Emmett 412f875c1f chore: add backlog task for SendGrid setup
Track remaining steps to finalize SendGrid integration:
- Create account and API key
- Verify sender domain
- Configure Cloudflare Pages env vars
- Test email delivery

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-06 15:39:10 +01:00
Jeff Emmett e3934509d4 feat: switch email provider to SendGrid and add Docker support
- Replace Resend with SendGrid for notification emails
- Add sendThankYouEmail function using SendGrid REST API
- Add SENDGRID_API_KEY to env vars documentation
- Fix URL handling in checkout/portal routes (use getBaseUrl helper)
- Simplify donation request body in homepage
- Add Dockerfile for standalone deployment
- Enable standalone output in next.config.mjs

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-06 15:35:51 +01:00
Jeff Emmett ddb673ede6 Add 🎺 emoji favicon 2025-11-30 22:18:35 -08:00
Jeff Emmett 242f9081b6 Add GitHub to Gitea mirror workflow
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-22 18:00:05 -08:00
Jeff Emmett ac0492e439 add wrangler 2025-10-13 22:03:53 -04:00
Jeff Emmett 6747986a1e new package-lock file 2025-10-13 21:23:08 -04:00
v0 4191aeabd6 fix: resolve deployment issues for Cloudflare Pages
Add Edge Runtime to 7 API routes, fix package versions, remove tw-animate-css

#VERCEL_SKIP

Co-authored-by: Jeff Emmett <46964190+Jeff-Emmett@users.noreply.github.com>
2025-10-14 00:48:10 +00:00
v0 8bfe8652e9 streamline packages: remove pnpm-lock.yaml, tw-animate-css, update Next.js
#VERCEL_SKIP

Co-authored-by: Jeff Emmett <46964190+Jeff-Emmett@users.noreply.github.com>
2025-10-14 00:42:20 +00:00
v0 d1230dc9c8 streamline packages: remove pnpm-lock.yaml and tw-animate-css, update Next.js
#VERCEL_SKIP

Co-authored-by: Jeff Emmett <46964190+Jeff-Emmett@users.noreply.github.com>
2025-10-13 03:05:24 +00:00
v0 f0951e529c fix: resolve deployment errors with Next.js and Stripe
Remove invalid Next.js config and lazy Stripe initialization.

#VERCEL_SKIP

Co-authored-by: Jeff Emmett <46964190+Jeff-Emmett@users.noreply.github.com>
2025-10-13 03:01:22 +00:00
v0 843cb603fd feat: configure app for Cloudflare Pages with Next.js runtime
Add @cloudflare/next-on-pages, update configs, and wrangler.toml

#VERCEL_SKIP

Co-authored-by: Jeff Emmett <46964190+Jeff-Emmett@users.noreply.github.com>
2025-10-13 02:55:15 +00:00
v0 d91ea91d46 fix: resolve CSS and hero image discrepancies
Replace incorrect hero image and fix CSS compilation issues.

#VERCEL_SKIP

Co-authored-by: Jeff Emmett <46964190+Jeff-Emmett@users.noreply.github.com>
2025-10-12 01:29:34 +00:00
devandoo 06910d7809
Merge pull request #1 from Jeff-Emmett/fix/css-compilation-issues
Fix CSS compilation issues
2025-09-17 10:16:30 +05:30
24 changed files with 16041 additions and 63 deletions

28
.github/workflows/mirror-to-gitea.yml vendored Normal file
View File

@ -0,0 +1,28 @@
name: Mirror to Gitea
on:
push:
branches:
- main
- master
workflow_dispatch:
jobs:
mirror:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Mirror to Gitea
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
GITEA_USERNAME: ${{ secrets.GITEA_USERNAME }}
run: |
REPO_NAME=$(basename $GITHUB_REPOSITORY)
git remote add gitea https://$GITEA_USERNAME:$GITEA_TOKEN@gitea.jeffemmett.com/jeffemmett/$REPO_NAME.git || true
git push gitea --all --force
git push gitea --tags --force

66
CLOUDFLARE_DEPLOYMENT.md Normal file
View File

@ -0,0 +1,66 @@
# Deploying to Cloudflare Pages
This Next.js app is configured to run on Cloudflare Pages with full server-side functionality.
## Prerequisites
1. Install Wrangler CLI globally:
\`\`\`bash
npm install -g wrangler
\`\`\`
2. Login to Cloudflare:
\`\`\`bash
wrangler login
\`\`\`
## Local Development
Run the app locally with Cloudflare Workers simulation:
\`\`\`bash
npm run preview
\`\`\`
## Build for Cloudflare
Build the app for Cloudflare Pages:
\`\`\`bash
npm run pages:build
\`\`\`
This creates a `.vercel/output/static` directory with your built app.
## Deploy to Cloudflare Pages
### Option 1: Command Line Deployment
\`\`\`bash
npm run deploy
\`\`\`
### Option 2: Cloudflare Dashboard
1. Go to [Cloudflare Pages](https://dash.cloudflare.com/pages)
2. Create a new project
3. Connect your Git repository
4. Set build settings:
- **Build command**: `npm run pages:build`
- **Build output directory**: `.vercel/output/static`
- **Root directory**: `/`
## Environment Variables
Add these environment variables in Cloudflare Pages dashboard:
- `STRIPE_SECRET_KEY`
- `STRIPE_PUBLISHABLE_KEY`
- `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY`
- `STRIPE_WEBHOOK_SECRET`
- `SENDGRID_API_KEY` - For sending notification emails (thank you emails after purchases)
## Important Notes
- API routes run as Cloudflare Workers
- Stripe webhooks will need to be updated to your Cloudflare Pages URL
- All server-side features (API routes, dynamic rendering) are fully supported
- The app uses Edge Runtime for optimal performance on Cloudflare's network

43
Dockerfile Normal file
View File

@ -0,0 +1,43 @@
FROM node:20-alpine AS base
# Install dependencies only when needed
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
ENV NODE_ENV=production
RUN npm run build
# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]

View File

@ -1,8 +1,24 @@
import { type NextRequest, NextResponse } from "next/server"
import Stripe from "stripe"
export const runtime = "edge"
function getBaseUrl(request: NextRequest): string {
// Use environment variable if set, otherwise construct from headers
if (process.env.NEXT_PUBLIC_BASE_URL) {
return process.env.NEXT_PUBLIC_BASE_URL
}
const host = request.headers.get("host") || request.headers.get("x-forwarded-host")
const protocol = request.headers.get("x-forwarded-proto") || "https"
if (host) {
return `${protocol}://${host}`
}
return "https://alertbaytrumpeter.com"
}
export async function POST(request: NextRequest) {
try {
const baseUrl = getBaseUrl(request)
const stripeSecretKey = process.env.STRIPE_SECRET_KEY
if (!stripeSecretKey) {
@ -58,8 +74,8 @@ export async function POST(request: NextRequest) {
quantity: 1,
},
],
success_url: `${request.nextUrl.origin}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${request.nextUrl.origin}/cancel`,
success_url: `${baseUrl}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${baseUrl}/cancel`,
})
console.log("[v0] Subscription checkout session created successfully:", session.id)
@ -87,8 +103,8 @@ export async function POST(request: NextRequest) {
quantity: 1,
},
],
success_url: `${request.nextUrl.origin}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${request.nextUrl.origin}/cancel`,
success_url: `${baseUrl}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${baseUrl}/cancel`,
})
console.log("[v0] Monthly subscription checkout session created successfully:", session.id)
@ -138,8 +154,8 @@ export async function POST(request: NextRequest) {
quantity: 1,
},
],
success_url: `${request.nextUrl.origin}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${request.nextUrl.origin}/cancel`,
success_url: `${baseUrl}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${baseUrl}/cancel`,
})
console.log("[v0] Checkout session created successfully:", session.id)

View File

@ -1,12 +1,33 @@
import { type NextRequest, NextResponse } from "next/server"
import Stripe from "stripe"
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2024-06-20",
})
export const runtime = "edge"
function getBaseUrl(request: NextRequest): string {
if (process.env.NEXT_PUBLIC_BASE_URL) {
return process.env.NEXT_PUBLIC_BASE_URL
}
const host = request.headers.get("host") || request.headers.get("x-forwarded-host")
const protocol = request.headers.get("x-forwarded-proto") || "https"
if (host) {
return `${protocol}://${host}`
}
return "https://alertbaytrumpeter.com"
}
export async function POST(request: NextRequest) {
try {
const baseUrl = getBaseUrl(request)
const stripeSecretKey = process.env.STRIPE_SECRET_KEY?.trim()
if (!stripeSecretKey) {
return NextResponse.json({ error: "Stripe configuration missing" }, { status: 500 })
}
const stripe = new Stripe(stripeSecretKey, {
apiVersion: "2024-06-20",
})
const { session_id } = await request.json()
const checkoutSession = await stripe.checkout.sessions.retrieve(session_id)
@ -17,7 +38,7 @@ export async function POST(request: NextRequest) {
const portalSession = await stripe.billingPortal.sessions.create({
customer: checkoutSession.customer as string,
return_url: `${request.nextUrl.origin}`,
return_url: baseUrl,
})
return NextResponse.json({ url: portalSession.url })

View File

@ -1,6 +1,8 @@
import { NextResponse } from "next/server"
import Stripe from "stripe"
export const runtime = "edge"
export async function POST(request: Request) {
try {
const { priceId } = await request.json()

View File

@ -1,6 +1,8 @@
import { NextResponse } from "next/server"
import Stripe from "stripe"
export const runtime = "edge"
export async function GET() {
try {
const stripeSecretKey = process.env.STRIPE_SECRET_KEY?.trim()

View File

@ -1,5 +1,7 @@
import { type NextRequest, NextResponse } from "next/server"
export const runtime = "edge"
export async function POST(request: NextRequest) {
try {
const { name, email, subject, message } = await request.json()

View File

@ -1,6 +1,8 @@
import { type NextRequest, NextResponse } from "next/server"
import Stripe from "stripe"
export const runtime = "edge"
export async function GET(request: NextRequest) {
try {
const stripeSecretKey = process.env.STRIPE_SECRET_KEY?.trim()

View File

@ -1,14 +1,101 @@
import { type NextRequest, NextResponse } from "next/server"
import Stripe from "stripe"
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2024-06-20",
})
export const runtime = "edge"
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!
async function sendThankYouEmail(
customerEmail: string,
customerName: string | null,
amount: number,
currency: string,
productName: string
) {
const sendgridApiKey = process.env.SENDGRID_API_KEY?.trim()
if (!sendgridApiKey) {
console.log("⚠️ SENDGRID_API_KEY not configured, skipping email")
return
}
const formattedAmount = new Intl.NumberFormat("en-CA", {
style: "currency",
currency: currency.toUpperCase(),
}).format(amount / 100)
const name = customerName || "Valued Supporter"
try {
const response = await fetch("https://api.sendgrid.com/v3/mail/send", {
method: "POST",
headers: {
"Authorization": `Bearer ${sendgridApiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
personalizations: [
{
to: [{ email: customerEmail, name: name }],
},
],
from: {
email: "noreply@alertbaytrumpeter.com",
name: "Alert Bay Trumpeter",
},
subject: "Thank You for Supporting the Alert Bay Trumpeter!",
content: [
{
type: "text/html",
value: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h1 style="color: #1e40af;">Thank You, ${name}!</h1>
<p>Your generous support means the world to Jerry Higginson, the Alert Bay Trumpeter.</p>
<div style="background-color: #f0f9ff; padding: 20px; border-radius: 8px; margin: 20px 0;">
<h2 style="color: #1e40af; margin-top: 0;">Subscription Details</h2>
<p><strong>Amount:</strong> ${formattedAmount}/month</p>
<p><strong>Plan:</strong> ${productName}</p>
</div>
<p>Your monthly contribution helps Jerry continue spreading smiles at sea by serenading cruise ship passengers with his trumpet.</p>
<p>With over 1,000 nautical serenades since 1996, your support keeps the music playing!</p>
<p style="margin-top: 30px;">
With gratitude,<br>
<strong>Jerry Higginson</strong><br>
The Alert Bay Trumpeter
</p>
<hr style="border: none; border-top: 1px solid #e5e7eb; margin: 30px 0;">
<p style="color: #6b7280; font-size: 12px;">
If you have any questions, contact us at alertbaytrumpeter@icloud.com
</p>
</div>
`,
},
],
}),
})
if (response.ok || response.status === 202) {
console.log(`✅ Thank you email sent to ${customerEmail}`)
} else {
const error = await response.text()
console.error(`❌ Failed to send email: ${error}`)
}
} catch (error) {
console.error("❌ Error sending email:", error)
}
}
export async function POST(request: NextRequest) {
try {
const stripeSecretKey = process.env.STRIPE_SECRET_KEY?.trim()
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET?.trim()
if (!stripeSecretKey || !webhookSecret) {
return NextResponse.json({ error: "Stripe configuration missing" }, { status: 500 })
}
const stripe = new Stripe(stripeSecretKey, {
apiVersion: "2024-06-20",
})
const body = await request.text()
const signature = request.headers.get("stripe-signature")!
@ -26,12 +113,45 @@ export async function POST(request: NextRequest) {
case "checkout.session.completed":
const session = event.data.object as Stripe.Checkout.Session
console.log(`💰 Payment successful for session: ${session.id}`)
// Handle successful payment here
// Send thank you email for subscriptions
if (session.mode === "subscription" && session.customer_details?.email) {
// Get subscription details
const subscriptionId = session.subscription as string
if (subscriptionId) {
const subscription = await stripe.subscriptions.retrieve(subscriptionId)
const item = subscription.items.data[0]
const amount = item.price.unit_amount || 0
const currency = item.price.currency
const productId = item.price.product as string
const product = await stripe.products.retrieve(productId)
await sendThankYouEmail(
session.customer_details.email,
session.customer_details.name,
amount,
currency,
product.name
)
}
}
break
case "payment_intent.succeeded":
const paymentIntent = event.data.object as Stripe.PaymentIntent
console.log(`💰 Payment succeeded: ${paymentIntent.id}`)
case "customer.subscription.created":
const newSubscription = event.data.object as Stripe.Subscription
console.log(`🎉 New subscription created: ${newSubscription.id}`)
break
case "invoice.paid":
const invoice = event.data.object as Stripe.Invoice
console.log(`💵 Invoice paid: ${invoice.id}`)
break
case "customer.subscription.deleted":
const cancelledSubscription = event.data.object as Stripe.Subscription
console.log(`😢 Subscription cancelled: ${cancelledSubscription.id}`)
break
default:
console.log(`Unhandled event type ${event.type}`)
}

View File

@ -1,4 +1,5 @@
@import "tailwindcss";
@import "tw-animate-css";
:root {
--font-inter: var(--font-inter);

View File

@ -21,6 +21,33 @@ export const metadata: Metadata = {
description:
"The official website of Jerry Higginson, the Alert Bay Trumpeter, entertaining cruise ship passengers since 1996",
generator: "v0.app",
icons: {
icon: "data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🎺</text></svg>",
},
openGraph: {
title: "Alert Bay Trumpeter - Jerry Higginson",
description:
"The official website of Jerry Higginson, the Alert Bay Trumpeter, entertaining cruise ship passengers since 1996",
url: "https://alertbaytrumpeter.com",
siteName: "Alert Bay Trumpeter",
images: [
{
url: "/og-image.jpg",
width: 1200,
height: 630,
alt: "Jerry Higginson - Alert Bay Trumpeter",
},
],
locale: "en_US",
type: "website",
},
twitter: {
card: "summary_large_image",
title: "Alert Bay Trumpeter - Jerry Higginson",
description:
"The official website of Jerry Higginson, the Alert Bay Trumpeter, entertaining cruise ship passengers since 1996",
images: ["/og-image.jpg"],
},
}
export default function RootLayout({

View File

@ -50,21 +50,14 @@ export default function HomePage() {
try {
console.log("[v0] Starting donation process for product:", product.name)
let requestBody: any
if (product.lookup_key) {
// Use lookup_key for one-time donations (legacy sponsor tiers)
requestBody = { lookup_key: product.lookup_key }
} else {
// Use product info for subscription checkouts
requestBody = {
productId: product.id,
productName: product.name,
price: product.price,
currency: product.currency,
description: product.description,
mode: "subscription",
}
// Always use subscription mode for products fetched from Stripe
const requestBody = {
productId: product.id,
productName: product.name,
price: product.price,
currency: product.currency,
description: product.description,
mode: "subscription",
}
const response = await fetch("/api/create-checkout-session", {

15
backlog/config.yml Normal file
View File

@ -0,0 +1,15 @@
project_name: "Alert Bay Trumpeter"
default_status: "To Do"
statuses: ["To Do", "In Progress", "Done"]
labels: []
milestones: []
date_format: yyyy-mm-dd
max_column_width: 20
default_editor: "vim"
auto_open_browser: true
default_port: 6420
remote_operations: true
auto_commit: false
bypass_git_hooks: false
check_active_branches: true
active_branch_days: 30

View File

@ -0,0 +1,24 @@
---
id: task-1
title: Finalize SendGrid API connection for alertbaytrumpeter.com
status: To Do
assignee: []
created_date: '2025-12-06 14:37'
labels: []
dependencies: []
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Complete the SendGrid integration for the Alert Bay Trumpeter website notification emails
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [ ] #1 Create SendGrid account and generate API key
- [ ] #2 Verify sender domain (alertbaytrumpeter.com) in SendGrid
- [ ] #3 Add SENDGRID_API_KEY to Cloudflare Pages environment variables
- [ ] #4 Test email delivery with a real subscription
<!-- AC:END -->

View File

@ -9,14 +9,13 @@ const buttonVariants = cva(
{
variants: {
variant: {
default:
'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive:
'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
outline:
'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
secondary:
'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost:
'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
link: 'text-primary underline-offset-4 hover:underline',
@ -26,6 +25,8 @@ const buttonVariants = cva(
sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
icon: 'size-9',
'icon-sm': 'size-8',
'icon-lg': 'size-10',
},
},
defaultVariants: {

View File

@ -20,7 +20,7 @@ function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
<div
data-slot="card-header"
className={cn(
'@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6',
'@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6',
className,
)}
{...props}

View File

@ -1,5 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
eslint: {
ignoreDuringBuilds: true,
},
@ -11,4 +12,4 @@ const nextConfig = {
},
}
export default nextConfig
export default nextConfig

12716
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -4,8 +4,11 @@
"private": true,
"scripts": {
"build": "next build",
"deploy": "npm run pages:build && wrangler pages deploy",
"dev": "next dev",
"lint": "next lint",
"pages:build": "npx @cloudflare/next-on-pages",
"preview": "npm run pages:build && wrangler pages dev",
"start": "next start"
},
"dependencies": {
@ -44,17 +47,21 @@
"recharts": "latest",
"stripe": "latest",
"tailwind-merge": "^2.5.5",
"tailwindcss-animate": "^1.0.7"
"tailwindcss-animate": "^1.0.7",
"tw-animate-css": "latest"
},
"devDependencies": {
"@cloudflare/next-on-pages": "^1.13.5",
"@tailwindcss/postcss": "^4.1.9",
"@types/node": "^22",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9.17.0",
"eslint-config-next": "15.1.3",
"eslint-config-next": "15.2.4",
"postcss": "^8.5",
"tailwindcss": "^4.1.9",
"typescript": "^5"
"typescript": "^5",
"vercel": "^37.14.0",
"wrangler": "^3.94.0"
}
}

File diff suppressed because it is too large Load Diff

BIN
public/og-image.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

View File

@ -1,4 +1,5 @@
@import 'tailwindcss';
@import 'tw-animate-css';
@custom-variant dark (&:is(.dark *));
@ -74,8 +75,8 @@
}
@theme inline {
--font-sans: var(--font-inter);
--font-serif: var(--font-playfair);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);

7
wrangler.toml Normal file
View File

@ -0,0 +1,7 @@
name = "alert-bay-trumpeter"
compatibility_date = "2024-12-01"
compatibility_flags = ["nodejs_compat"]
pages_build_output_dir = ".vercel/output/static"
[env.production]
vars = { NODE_ENV = "production" }