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 { type NextRequest, NextResponse } from "next/server"
import Stripe from "stripe" 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) { export async function POST(request: NextRequest) {
try { try {
const baseUrl = getBaseUrl(request)
const stripeSecretKey = process.env.STRIPE_SECRET_KEY const stripeSecretKey = process.env.STRIPE_SECRET_KEY
if (!stripeSecretKey) { if (!stripeSecretKey) {
@ -58,8 +74,8 @@ export async function POST(request: NextRequest) {
quantity: 1, quantity: 1,
}, },
], ],
success_url: `${request.nextUrl.origin}/success?session_id={CHECKOUT_SESSION_ID}`, success_url: `${baseUrl}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${request.nextUrl.origin}/cancel`, cancel_url: `${baseUrl}/cancel`,
}) })
console.log("[v0] Subscription checkout session created successfully:", session.id) console.log("[v0] Subscription checkout session created successfully:", session.id)
@ -87,8 +103,8 @@ export async function POST(request: NextRequest) {
quantity: 1, quantity: 1,
}, },
], ],
success_url: `${request.nextUrl.origin}/success?session_id={CHECKOUT_SESSION_ID}`, success_url: `${baseUrl}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${request.nextUrl.origin}/cancel`, cancel_url: `${baseUrl}/cancel`,
}) })
console.log("[v0] Monthly subscription checkout session created successfully:", session.id) console.log("[v0] Monthly subscription checkout session created successfully:", session.id)
@ -138,8 +154,8 @@ export async function POST(request: NextRequest) {
quantity: 1, quantity: 1,
}, },
], ],
success_url: `${request.nextUrl.origin}/success?session_id={CHECKOUT_SESSION_ID}`, success_url: `${baseUrl}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${request.nextUrl.origin}/cancel`, cancel_url: `${baseUrl}/cancel`,
}) })
console.log("[v0] Checkout session created successfully:", session.id) console.log("[v0] Checkout session created successfully:", session.id)

View File

@ -1,12 +1,33 @@
import { type NextRequest, NextResponse } from "next/server" import { type NextRequest, NextResponse } from "next/server"
import Stripe from "stripe" import Stripe from "stripe"
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { export const runtime = "edge"
apiVersion: "2024-06-20",
}) 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) { export async function POST(request: NextRequest) {
try { 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 { session_id } = await request.json()
const checkoutSession = await stripe.checkout.sessions.retrieve(session_id) 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({ const portalSession = await stripe.billingPortal.sessions.create({
customer: checkoutSession.customer as string, customer: checkoutSession.customer as string,
return_url: `${request.nextUrl.origin}`, return_url: baseUrl,
}) })
return NextResponse.json({ url: portalSession.url }) return NextResponse.json({ url: portalSession.url })

View File

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

View File

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

View File

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

View File

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

View File

@ -1,14 +1,101 @@
import { type NextRequest, NextResponse } from "next/server" import { type NextRequest, NextResponse } from "next/server"
import Stripe from "stripe" import Stripe from "stripe"
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { export const runtime = "edge"
apiVersion: "2024-06-20",
})
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) { export async function POST(request: NextRequest) {
try { 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 body = await request.text()
const signature = request.headers.get("stripe-signature")! const signature = request.headers.get("stripe-signature")!
@ -26,12 +113,45 @@ export async function POST(request: NextRequest) {
case "checkout.session.completed": case "checkout.session.completed":
const session = event.data.object as Stripe.Checkout.Session const session = event.data.object as Stripe.Checkout.Session
console.log(`💰 Payment successful for session: ${session.id}`) 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 break
case "payment_intent.succeeded":
const paymentIntent = event.data.object as Stripe.PaymentIntent case "customer.subscription.created":
console.log(`💰 Payment succeeded: ${paymentIntent.id}`) const newSubscription = event.data.object as Stripe.Subscription
console.log(`🎉 New subscription created: ${newSubscription.id}`)
break 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: default:
console.log(`Unhandled event type ${event.type}`) console.log(`Unhandled event type ${event.type}`)
} }

View File

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

View File

@ -21,6 +21,33 @@ export const metadata: Metadata = {
description: description:
"The official website of Jerry Higginson, the Alert Bay Trumpeter, entertaining cruise ship passengers since 1996", "The official website of Jerry Higginson, the Alert Bay Trumpeter, entertaining cruise ship passengers since 1996",
generator: "v0.app", 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({ export default function RootLayout({

View File

@ -50,14 +50,8 @@ export default function HomePage() {
try { try {
console.log("[v0] Starting donation process for product:", product.name) console.log("[v0] Starting donation process for product:", product.name)
let requestBody: any // Always use subscription mode for products fetched from Stripe
const requestBody = {
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, productId: product.id,
productName: product.name, productName: product.name,
price: product.price, price: product.price,
@ -65,7 +59,6 @@ export default function HomePage() {
description: product.description, description: product.description,
mode: "subscription", mode: "subscription",
} }
}
const response = await fetch("/api/create-checkout-session", { const response = await fetch("/api/create-checkout-session", {
method: "POST", method: "POST",

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: { variants: {
variant: { variant: {
default: default: 'bg-primary text-primary-foreground hover:bg-primary/90',
'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
destructive: 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: 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', '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: secondary:
'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80', 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: ghost:
'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50', 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
link: 'text-primary underline-offset-4 hover:underline', 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', 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', lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
icon: 'size-9', icon: 'size-9',
'icon-sm': 'size-8',
'icon-lg': 'size-10',
}, },
}, },
defaultVariants: { defaultVariants: {

View File

@ -20,7 +20,7 @@ function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
<div <div
data-slot="card-header" data-slot="card-header"
className={cn( 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, className,
)} )}
{...props} {...props}

View File

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

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, "private": true,
"scripts": { "scripts": {
"build": "next build", "build": "next build",
"deploy": "npm run pages:build && wrangler pages deploy",
"dev": "next dev", "dev": "next dev",
"lint": "next lint", "lint": "next lint",
"pages:build": "npx @cloudflare/next-on-pages",
"preview": "npm run pages:build && wrangler pages dev",
"start": "next start" "start": "next start"
}, },
"dependencies": { "dependencies": {
@ -44,17 +47,21 @@
"recharts": "latest", "recharts": "latest",
"stripe": "latest", "stripe": "latest",
"tailwind-merge": "^2.5.5", "tailwind-merge": "^2.5.5",
"tailwindcss-animate": "^1.0.7" "tailwindcss-animate": "^1.0.7",
"tw-animate-css": "latest"
}, },
"devDependencies": { "devDependencies": {
"@cloudflare/next-on-pages": "^1.13.5",
"@tailwindcss/postcss": "^4.1.9", "@tailwindcss/postcss": "^4.1.9",
"@types/node": "^22", "@types/node": "^22",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"eslint": "^9.17.0", "eslint": "^9.17.0",
"eslint-config-next": "15.1.3", "eslint-config-next": "15.2.4",
"postcss": "^8.5", "postcss": "^8.5",
"tailwindcss": "^4.1.9", "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 'tailwindcss';
@import 'tw-animate-css';
@custom-variant dark (&:is(.dark *)); @custom-variant dark (&:is(.dark *));
@ -74,8 +75,8 @@
} }
@theme inline { @theme inline {
--font-sans: var(--font-inter); --font-sans: var(--font-geist-sans);
--font-serif: var(--font-playfair); --font-mono: var(--font-geist-mono);
--color-background: var(--background); --color-background: var(--background);
--color-foreground: var(--foreground); --color-foreground: var(--foreground);
--color-card: var(--card); --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" }