Compare commits
No commits in common. "main" and "fix/css-compilation-issues" have entirely different histories.
main
...
fix/css-co
|
|
@ -1,28 +0,0 @@
|
||||||
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
|
|
||||||
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
# 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
43
Dockerfile
|
|
@ -1,43 +0,0 @@
|
||||||
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"]
|
|
||||||
|
|
@ -1,24 +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"
|
|
||||||
|
|
||||||
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) {
|
||||||
|
|
@ -74,8 +58,8 @@ export async function POST(request: NextRequest) {
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
success_url: `${baseUrl}/success?session_id={CHECKOUT_SESSION_ID}`,
|
success_url: `${request.nextUrl.origin}/success?session_id={CHECKOUT_SESSION_ID}`,
|
||||||
cancel_url: `${baseUrl}/cancel`,
|
cancel_url: `${request.nextUrl.origin}/cancel`,
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log("[v0] Subscription checkout session created successfully:", session.id)
|
console.log("[v0] Subscription checkout session created successfully:", session.id)
|
||||||
|
|
@ -103,8 +87,8 @@ export async function POST(request: NextRequest) {
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
success_url: `${baseUrl}/success?session_id={CHECKOUT_SESSION_ID}`,
|
success_url: `${request.nextUrl.origin}/success?session_id={CHECKOUT_SESSION_ID}`,
|
||||||
cancel_url: `${baseUrl}/cancel`,
|
cancel_url: `${request.nextUrl.origin}/cancel`,
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log("[v0] Monthly subscription checkout session created successfully:", session.id)
|
console.log("[v0] Monthly subscription checkout session created successfully:", session.id)
|
||||||
|
|
@ -154,8 +138,8 @@ export async function POST(request: NextRequest) {
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
success_url: `${baseUrl}/success?session_id={CHECKOUT_SESSION_ID}`,
|
success_url: `${request.nextUrl.origin}/success?session_id={CHECKOUT_SESSION_ID}`,
|
||||||
cancel_url: `${baseUrl}/cancel`,
|
cancel_url: `${request.nextUrl.origin}/cancel`,
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log("[v0] Checkout session created successfully:", session.id)
|
console.log("[v0] Checkout session created successfully:", session.id)
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,12 @@
|
||||||
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"
|
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
|
||||||
|
|
||||||
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",
|
apiVersion: "2024-06-20",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
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)
|
||||||
|
|
@ -38,7 +17,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: baseUrl,
|
return_url: `${request.nextUrl.origin}`,
|
||||||
})
|
})
|
||||||
|
|
||||||
return NextResponse.json({ url: portalSession.url })
|
return NextResponse.json({ url: portalSession.url })
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
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()
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
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()
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
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()
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
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()
|
||||||
|
|
|
||||||
|
|
@ -1,101 +1,14 @@
|
||||||
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"
|
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
|
||||||
|
|
||||||
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",
|
apiVersion: "2024-06-20",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
const body = await request.text()
|
const body = await request.text()
|
||||||
const signature = request.headers.get("stripe-signature")!
|
const signature = request.headers.get("stripe-signature")!
|
||||||
|
|
||||||
|
|
@ -113,45 +26,12 @@ 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":
|
||||||
case "customer.subscription.created":
|
const paymentIntent = event.data.object as Stripe.PaymentIntent
|
||||||
const newSubscription = event.data.object as Stripe.Subscription
|
console.log(`💰 Payment succeeded: ${paymentIntent.id}`)
|
||||||
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}`)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
@import "tw-animate-css";
|
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--font-inter: var(--font-inter);
|
--font-inter: var(--font-inter);
|
||||||
|
|
|
||||||
|
|
@ -21,33 +21,6 @@ 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({
|
||||||
|
|
|
||||||
11
app/page.tsx
11
app/page.tsx
|
|
@ -50,8 +50,14 @@ 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)
|
||||||
|
|
||||||
// Always use subscription mode for products fetched from Stripe
|
let requestBody: any
|
||||||
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,
|
||||||
|
|
@ -59,6 +65,7 @@ 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",
|
||||||
|
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
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
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
---
|
|
||||||
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 -->
|
|
||||||
|
|
@ -9,13 +9,14 @@ const buttonVariants = cva(
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
default:
|
||||||
|
'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
|
||||||
destructive:
|
destructive:
|
||||||
'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
|
'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',
|
||||||
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 hover:bg-secondary/80',
|
'bg-secondary text-secondary-foreground shadow-xs 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',
|
||||||
|
|
@ -25,8 +26,6 @@ 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: {
|
||||||
|
|
|
||||||
|
|
@ -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-2 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-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
output: 'standalone',
|
|
||||||
eslint: {
|
eslint: {
|
||||||
ignoreDuringBuilds: true,
|
ignoreDuringBuilds: true,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
13
package.json
13
package.json
|
|
@ -4,11 +4,8 @@
|
||||||
"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": {
|
||||||
|
|
@ -47,21 +44,17 @@
|
||||||
"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.2.4",
|
"eslint-config-next": "15.1.3",
|
||||||
"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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
2921
pnpm-lock.yaml
2921
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 186 KiB |
|
|
@ -1,5 +1,4 @@
|
||||||
@import 'tailwindcss';
|
@import 'tailwindcss';
|
||||||
@import 'tw-animate-css';
|
|
||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
|
@ -75,8 +74,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
--font-sans: var(--font-geist-sans);
|
--font-sans: var(--font-inter);
|
||||||
--font-mono: var(--font-geist-mono);
|
--font-serif: var(--font-playfair);
|
||||||
--color-background: var(--background);
|
--color-background: var(--background);
|
||||||
--color-foreground: var(--foreground);
|
--color-foreground: var(--foreground);
|
||||||
--color-card: var(--card);
|
--color-card: var(--card);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
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" }
|
|
||||||
Loading…
Reference in New Issue