Compare commits
14 Commits
fix/css-co
...
main
| Author | SHA1 | Date |
|---|---|---|
|
|
6964364218 | |
|
|
412f875c1f | |
|
|
e3934509d4 | |
|
|
ddb673ede6 | |
|
|
242f9081b6 | |
|
|
ac0492e439 | |
|
|
6747986a1e | |
|
|
4191aeabd6 | |
|
|
8bfe8652e9 | |
|
|
d1230dc9c8 | |
|
|
f0951e529c | |
|
|
843cb603fd | |
|
|
d91ea91d46 | |
|
|
06910d7809 |
|
|
@ -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
|
||||||
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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"]
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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 })
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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",
|
|
||||||
|
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>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!
|
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}`)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
@import "tw-animate-css";
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--font-inter: var(--font-inter);
|
--font-inter: var(--font-inter);
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
11
app/page.tsx
11
app/page.tsx
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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 -->
|
||||||
|
|
@ -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: {
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
/** @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,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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
2921
pnpm-lock.yaml
2921
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 186 KiB |
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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" }
|
||||||
Loading…
Reference in New Issue