Add Stripe payment integration for donations
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
1
go.mod
1
go.mod
@@ -4,6 +4,7 @@ go 1.25.4
|
||||
|
||||
require (
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/stripe/stripe-go/v76 v76.25.0
|
||||
google.golang.org/api v0.258.0
|
||||
)
|
||||
|
||||
|
||||
13
go.sum
13
go.sum
@@ -4,6 +4,7 @@ cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIi
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
@@ -29,8 +30,12 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/stripe/stripe-go/v76 v76.25.0 h1:kmDoOTvdQSTQssQzWZQQkgbAR2Q8eXdMWbN/ylNalWA=
|
||||
github.com/stripe/stripe-go/v76 v76.25.0/go.mod h1:rw1MxjlAKKcZ+3FOXgTHgwiOa2ya6CPq6ykpJ0Q6Po4=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
|
||||
@@ -49,18 +54,24 @@ go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJr
|
||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/api v0.258.0 h1:IKo1j5FBlN74fe5isA2PVozN3Y5pwNKriEgAXPOkDAc=
|
||||
@@ -71,5 +82,7 @@ google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
||||
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
130
main.go
130
main.go
@@ -8,6 +8,7 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"html/template"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -15,6 +16,9 @@ import (
|
||||
"strings"
|
||||
|
||||
_ "github.com/lib/pq"
|
||||
"github.com/stripe/stripe-go/v76"
|
||||
"github.com/stripe/stripe-go/v76/checkout/session"
|
||||
"github.com/stripe/stripe-go/v76/webhook"
|
||||
"google.golang.org/api/idtoken"
|
||||
)
|
||||
|
||||
@@ -52,9 +56,13 @@ func init() {
|
||||
}
|
||||
|
||||
func main() {
|
||||
stripe.Key = os.Getenv("STRIPE_SECRET_KEY")
|
||||
|
||||
http.HandleFunc("/", handleStatic)
|
||||
http.HandleFunc("/auth/google/callback", handleGoogleCallback)
|
||||
http.HandleFunc("/api/rsvp/", handleRSVP)
|
||||
http.HandleFunc("/api/donate/", handleDonate)
|
||||
http.HandleFunc("/api/stripe/webhook", handleStripeWebhook)
|
||||
|
||||
log.Println("server starting on :8080")
|
||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||
@@ -174,23 +182,18 @@ func handleRSVP(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
var numPeople int
|
||||
var totalPeople int
|
||||
err := db.QueryRow("SELECT num_people FROM rsvps WHERE event_id = $1 AND google_username = $2", eventID, email).Scan(&numPeople)
|
||||
var donation float64
|
||||
err := db.QueryRow("SELECT num_people, donation FROM rsvps WHERE event_id = $1 AND google_username = $2", eventID, email).Scan(&numPeople, &donation)
|
||||
if err == sql.ErrNoRows {
|
||||
numPeople = 0
|
||||
donation = 0
|
||||
} else if err != nil {
|
||||
log.Println("[ERROR] failed to query rsvp:", err)
|
||||
http.Error(w, "database error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
err = db.QueryRow("SELECT COALESCE(SUM(num_people), 0) FROM rsvps WHERE event_id = $1", eventID).Scan(&totalPeople)
|
||||
if err != nil {
|
||||
log.Println("[ERROR] failed to query total:", err)
|
||||
http.Error(w, "database error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]int{"numPeople": numPeople, "totalPeople": totalPeople})
|
||||
json.NewEncoder(w).Encode(map[string]any{"numPeople": numPeople, "donation": donation})
|
||||
|
||||
case http.MethodPost:
|
||||
var req struct {
|
||||
@@ -209,12 +212,115 @@ func handleRSVP(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "database error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
var totalPeople int
|
||||
db.QueryRow("SELECT COALESCE(SUM(num_people), 0) FROM rsvps WHERE event_id = $1", eventID).Scan(&totalPeople)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]int{"numPeople": req.NumPeople, "totalPeople": totalPeople})
|
||||
json.NewEncoder(w).Encode(map[string]int{"numPeople": req.NumPeople})
|
||||
|
||||
default:
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
func handleDonate(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
eventID := strings.TrimPrefix(r.URL.Path, "/api/donate/")
|
||||
if eventID == "" {
|
||||
http.Error(w, "missing event id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
token := r.Header.Get("Authorization")
|
||||
email, ok := verifyToken(strings.TrimPrefix(token, "Bearer "))
|
||||
if !ok {
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Amount int64 `json:"amount"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid json", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Amount < 100 {
|
||||
http.Error(w, "minimum donation is $1", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
params := &stripe.CheckoutSessionParams{
|
||||
Mode: stripe.String(string(stripe.CheckoutSessionModePayment)),
|
||||
LineItems: []*stripe.CheckoutSessionLineItemParams{
|
||||
{
|
||||
PriceData: &stripe.CheckoutSessionLineItemPriceDataParams{
|
||||
Currency: stripe.String("usd"),
|
||||
ProductData: &stripe.CheckoutSessionLineItemPriceDataProductDataParams{
|
||||
Name: stripe.String("Donation - Applause for a Cause"),
|
||||
},
|
||||
UnitAmount: stripe.Int64(req.Amount),
|
||||
},
|
||||
Quantity: stripe.Int64(1),
|
||||
},
|
||||
},
|
||||
SuccessURL: stripe.String(os.Getenv("BASE_URL") + "/" + eventID + ".html?donated=1"),
|
||||
CancelURL: stripe.String(os.Getenv("BASE_URL") + "/" + eventID + ".html"),
|
||||
Metadata: map[string]string{
|
||||
"event_id": eventID,
|
||||
"email": email,
|
||||
},
|
||||
}
|
||||
|
||||
s, err := session.New(params)
|
||||
if err != nil {
|
||||
log.Println("[ERROR] failed to create checkout session:", err)
|
||||
http.Error(w, "failed to create checkout session", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{"url": s.URL})
|
||||
}
|
||||
|
||||
func handleStripeWebhook(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, "failed to read body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
event, err := webhook.ConstructEvent(body, r.Header.Get("Stripe-Signature"), os.Getenv("STRIPE_WEBHOOK_SECRET"))
|
||||
if err != nil {
|
||||
log.Println("[ERROR] failed to verify webhook:", err)
|
||||
http.Error(w, "invalid signature", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if event.Type == "checkout.session.completed" {
|
||||
var sess stripe.CheckoutSession
|
||||
if err := json.Unmarshal(event.Data.Raw, &sess); err != nil {
|
||||
log.Println("[ERROR] failed to parse session:", err)
|
||||
http.Error(w, "failed to parse session", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
eventID := sess.Metadata["event_id"]
|
||||
email := sess.Metadata["email"]
|
||||
amount := float64(sess.AmountTotal) / 100
|
||||
|
||||
_, err := db.Exec(`
|
||||
UPDATE rsvps SET donation = donation + $3
|
||||
WHERE event_id = $1 AND google_username = $2
|
||||
`, eventID, email, amount)
|
||||
if err != nil {
|
||||
log.Println("[ERROR] failed to update donation:", err)
|
||||
} else {
|
||||
log.Printf("recorded donation of $%.2f from %s for %s", amount, email, eventID)
|
||||
}
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
@@ -56,6 +56,37 @@
|
||||
gap: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.donation-options {
|
||||
margin-top: 1rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid var(--wa-color-neutral-80);
|
||||
}
|
||||
.donation-options label {
|
||||
display: block;
|
||||
margin: 0.5rem 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
.custom-amount {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-left: 1.5rem;
|
||||
}
|
||||
.thank-you {
|
||||
background: var(--wa-color-success-95);
|
||||
color: var(--wa-color-success-30);
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.wa-dark .thank-you {
|
||||
background: var(--wa-color-success-20);
|
||||
color: var(--wa-color-success-90);
|
||||
}
|
||||
.donation-info {
|
||||
margin-top: 0.5rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style="opacity: 0">
|
||||
@@ -67,6 +98,9 @@
|
||||
<span class="spacer"></span>
|
||||
<wa-button variant="neutral" size="small" id="logout-btn">Switch User</wa-button>
|
||||
</div>
|
||||
<div id="thank-you" class="thank-you" style="display: none;">
|
||||
Thank you for your generous donation!
|
||||
</div>
|
||||
<wa-card>
|
||||
<div class="event-header">
|
||||
<img src="/afac26-logo.png" alt="Applause for a Cause">
|
||||
@@ -79,16 +113,33 @@
|
||||
<div class="rsvp-controls">
|
||||
<span>Number of people:</span>
|
||||
<wa-input type="number" id="num-people" min="1" value="1" style="width: 80px;"></wa-input>
|
||||
<wa-button variant="brand" id="rsvp-btn">RSVP</wa-button>
|
||||
</div>
|
||||
<div class="donation-options">
|
||||
<strong>Would you like to make a donation?</strong>
|
||||
<label><input type="radio" name="donation" value="25" checked> $25 suggested donation per family</label>
|
||||
<label><input type="radio" name="donation" value="custom"> Other amount</label>
|
||||
<div class="custom-amount" id="custom-amount-row" style="display: none;">
|
||||
$<wa-input type="number" id="custom-amount" min="1" value="25" style="width: 100px;"></wa-input>
|
||||
</div>
|
||||
<label><input type="radio" name="donation" value="0"> Maybe later</label>
|
||||
</div>
|
||||
<wa-button variant="brand" id="rsvp-btn" style="margin-top: 1rem;">RSVP</wa-button>
|
||||
</div>
|
||||
<div id="rsvp-status" style="display: none;">
|
||||
<strong id="rsvp-message"></strong>
|
||||
<div id="donation-status" class="donation-info"></div>
|
||||
<div class="rsvp-controls">
|
||||
<span>Change to:</span>
|
||||
<wa-input type="number" id="num-people-update" min="0" value="1" style="width: 80px;"></wa-input>
|
||||
<wa-button variant="neutral" id="update-btn">Update</wa-button>
|
||||
</div>
|
||||
<div class="donation-options">
|
||||
<strong>Make an additional donation?</strong>
|
||||
<div style="display: flex; align-items: center; gap: 0.5rem; margin-top: 0.5rem;">
|
||||
$<wa-input type="number" id="additional-amount" min="1" value="25" style="width: 100px;"></wa-input>
|
||||
<wa-button variant="neutral" id="donate-btn">Donate</wa-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</wa-card>
|
||||
@@ -107,6 +158,18 @@
|
||||
|
||||
const eventId = 'afac26';
|
||||
|
||||
if (new URLSearchParams(location.search).get('donated') === '1') {
|
||||
document.getElementById('thank-you').style.display = 'block';
|
||||
history.replaceState({}, '', location.pathname);
|
||||
}
|
||||
|
||||
document.querySelectorAll('input[name="donation"]').forEach(radio => {
|
||||
radio.addEventListener('change', () => {
|
||||
document.getElementById('custom-amount-row').style.display =
|
||||
radio.value === 'custom' && radio.checked ? 'flex' : 'none';
|
||||
});
|
||||
});
|
||||
|
||||
async function loadRSVP() {
|
||||
const data = await api('GET', `/api/rsvp/${eventId}`);
|
||||
updateUI(data);
|
||||
@@ -119,16 +182,40 @@
|
||||
const word = data.numPeople === 1 ? 'person' : 'people';
|
||||
document.getElementById('rsvp-message').textContent = `You're RSVPed for ${data.numPeople} ${word}.`;
|
||||
document.getElementById('num-people-update').value = data.numPeople;
|
||||
if (data.donation > 0) {
|
||||
document.getElementById('donation-status').textContent = `You've donated $${data.donation.toFixed(2)}. Thank you!`;
|
||||
} else {
|
||||
document.getElementById('donation-status').textContent = '';
|
||||
}
|
||||
} else {
|
||||
document.getElementById('rsvp-prompt').style.display = 'block';
|
||||
document.getElementById('rsvp-status').style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function getSelectedDonation() {
|
||||
const selected = document.querySelector('input[name="donation"]:checked');
|
||||
if (!selected || selected.value === '0') return 0;
|
||||
if (selected.value === 'custom') {
|
||||
return parseInt(document.getElementById('custom-amount').value) || 0;
|
||||
}
|
||||
return parseInt(selected.value);
|
||||
}
|
||||
|
||||
async function startDonation(amount) {
|
||||
const data = await api('POST', `/api/donate/${eventId}`, { amount: amount * 100 });
|
||||
location.href = data.url;
|
||||
}
|
||||
|
||||
document.getElementById('rsvp-btn').addEventListener('click', async () => {
|
||||
const numPeople = parseInt(document.getElementById('num-people').value) || 1;
|
||||
const data = await api('POST', `/api/rsvp/${eventId}`, { numPeople });
|
||||
updateUI(data);
|
||||
|
||||
const donationAmount = getSelectedDonation();
|
||||
if (donationAmount > 0) {
|
||||
await startDonation(donationAmount);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('update-btn').addEventListener('click', async () => {
|
||||
@@ -137,6 +224,13 @@
|
||||
updateUI(data);
|
||||
});
|
||||
|
||||
document.getElementById('donate-btn').addEventListener('click', async () => {
|
||||
const amount = parseInt(document.getElementById('additional-amount').value) || 0;
|
||||
if (amount > 0) {
|
||||
await startDonation(amount);
|
||||
}
|
||||
});
|
||||
|
||||
await loadRSVP();
|
||||
|
||||
await customElements.whenDefined('wa-card');
|
||||
|
||||
Reference in New Issue
Block a user