jennex

Our Wedding Site
git clone git://git.alexkarle.com.com/jennex
Log | Files | Refs | README | LICENSE

commit 200e5b0ce98880148edd5618b4e51e1319d5670e (patch)
parent e4d31c172398040a6c1690a94c3859481e7c2171
Author: Alex Karle <alex@alexkarle.com>
Date:   Sat, 13 May 2023 10:49:17 -0400

rsvp: Remove rsvp program

We're going old-school with the invites; paper snail mail!

Diffstat:
MMakefile | 5+----
MREADME.md | 7-------
Dsrc/.gitignore | 2--
Dsrc/README | 14--------------
Dsrc/db.scm | 83-------------------------------------------------------------------------------
Dsrc/dev.scm | 17-----------------
Dsrc/handler.scm | 194-------------------------------------------------------------------------------
Dsrc/main.scm | 22----------------------
Dsrc/prod.scm | 3---
Dsrc/setup.sql | 30------------------------------
10 files changed, 1 insertion(+), 376 deletions(-)

diff --git a/Makefile b/Makefile @@ -2,7 +2,7 @@ HTML != for f in *.tmpl; do echo $$(basename $$f tmpl)html; done SCHEME != find . -name '*.scm' .PHONY: build -build: build-html rsvp +build: build-html # Standalone target for Netlify, which doesn't have chicken-csc @@ -18,6 +18,3 @@ clean: (cat header.html; cat $<; cat footer.html) > $@ $(HTML): header.html footer.html Makefile - -rsvp: $(SCHEME) - (cd src; chicken-csc prod.scm -o ../$@) diff --git a/README.md b/README.md @@ -8,13 +8,6 @@ I'm putting this together for our wedding (:tada:) in 2023 after a quick survey of the common choices for hosted sites (The Knot, Minted, etc). -The goals are to have: - -1. A public wedding portfolio / information site for guests (mostly - done, just static HTML) -2. A lightweight RSVP system (likely the more interesting bit from a - code perspective) - ## License All code is MIT Licensed. The cursive font used in the header (Allure) diff --git a/src/.gitignore b/src/.gitignore @@ -1 +0,0 @@ -rsvp.sqlite3 -\ No newline at end of file diff --git a/src/README b/src/README @@ -1,14 +0,0 @@ -jennex/src -========== - -This directory contains all the code for the RSVP system, -which is (hopefully) small app to fetch, display, and -allow updates to RSVPs using simple server-side rendered -HTML and basic elements like HTML Forms. - -Totally for fun and the learnings, I've chosen to use -Spiffy, a CHICKEN Scheme Egg (library): - - http://wiki.call-cc.org/eggref/5/spiffy - -We'll see how far I get! diff --git a/src/db.scm b/src/db.scm @@ -1,83 +0,0 @@ -(import sqlite3 - sql-null - (chicken condition) - (chicken string) - (chicken format)) - -(define conn (open-database "rsvp.sqlite3")) - -(define-record guest id name party-id going meal-choice) -(set-record-printer! guest - (lambda (x o) - (fprintf o "(guest id: ~A name: ~A going: ~A meal: ~A)" - (guest-id x) - (guest-name x) - (guest-going x) - (guest-meal-choice x)))) - -(define-record party id notes guests) -(set-record-printer! party - (lambda (x o) - (fprintf o "(party id: ~A guests: ~A)" - (party-id x) - (map (lambda (x) (guest-name x)) (party-guests x))))) - -(define (get-guest-by-name name) - (apply make-guest - (first-row conn -"SELECT - id, - name, - party_id, - going, - meal_choice -FROM guests WHERE name = $name" name))) - -(define (get-guest-by-id id) - (apply make-guest - (first-row conn -"SELECT - id, - name, - party_id, - going, - meal_choice -FROM guests WHERE id = $name" id))) - -(define (get-guests-in-party pid) - (map-row make-guest conn -"SELECT - id, - name, - party_id, - going, - meal_choice -FROM guests WHERE party_id = $pid" pid)) - -(define (get-party-by-id pid) - (let ((res (first-row conn "SELECT id, notes FROM parties WHERE id = $pid" pid))) - (make-party (car res) (cadr res) '()))) - -(define (get-party-by-name name) - ;; Throws <sqlite3 exn> if any queries return empty fails - (let* ((guest (get-guest-by-name name)) - (party-id (guest-party-id guest)) - (guests (get-guests-in-party party-id)) - (party (get-party-by-id party-id))) - (party-guests-set! party guests) - party)) - -(define (update-guest g) - (update conn " - UPDATE guests SET - name = $1, - going = $2, - meal_choice = $3 - WHERE id = $4" - (guest-name g) - (guest-going g) - (guest-meal-choice g) - (guest-id g))) - -(define (update-party-notes pid notes) - (update conn "UPDATE parties SET notes = $1 WHERE id = $2" notes pid)) diff --git a/src/dev.scm b/src/dev.scm @@ -1,17 +0,0 @@ -;; dev.scm -- "dev mode" (runs main as thread for REPL development) -(import srfi-18 - spiffy - (chicken process-context)) - -(include "main.scm") - -(access-log "/tmp/rsvp-access.log") -(error-log "/tmp/rsvp-error.log") - -(print "Logging access to " (access-log)) -(print "Logging error to " (error-log)) - -(define thread - (begin - (thread-start! - (make-thread (lambda () (main '())))))) diff --git a/src/handler.scm b/src/handler.scm @@ -1,194 +0,0 @@ -;; handler.scm -- define the routes for Spiffy -(import - spiffy - uri-common - intarweb - sxml-serializer - sql-null - (chicken string) - (chicken condition)) - -(include "db.scm") - -(define email '(a (@ (href "mailto:rsvp@jennex.org")) "rsvp@jennex.org")) - -(define routes - ;; See uri-match for format: http://wiki.call-cc.org/eggref/5/uri-match#routes-format - ;; - ;; NOTE: wrap the route handlers in lambdas so that the REPL picks up - ;; changes on re-definition. - `(((/ "") (GET ,(lambda (c) (route-get-index c)))) - ((/ "rsvp") - (GET ,(lambda (c) (route-get-rsvp c))) - (POST ,(lambda (c) (route-post-rsvp c)))))) - -(define (send-sxml sxml) - (send-response status: 'ok body: (serialize-sxml sxml))) - -(define (template-page sxml) - `(html - (head (title "Alex and Jennie's Wedding") - (meta (@ (charset "utf-8"))) - (meta (@ (name "viewport") (content "width=device-width, initial-scale=1"))) - (link (@ (rel "icon") (type "image/png") (sizes "32x32") (href "/static/favicon-32x32.png"))) - (link (@ (rel "icon") (type "image/png") (sizes "16x16") (href "/static/favicon-16x16.png"))) - (link (@ (rel "stylesheet") (href "/style.css")))) - (body - (h1 (@ (class "index-title")) "Alex " (small "&") " Jennie") - (p (@ (class "subtitle")) "Tie the Knot") - (nav - (a (@ (href "https://jennex.org")) "Home") - (a (@ (href "https://jennex.org/story.html")) "Our Story") - (a (@ (href "/")) "RSVP") - (a (@ (href "https://jennex.org/event.html")) "Event") - (a (@ (href "https://jennex.org/travel.html")) "Travel") - (a (@ (href "https://jennex.org/registry.html")) "Registry")) - ,@sxml - (footer "Copyright 2022, Alex Karle (" (a (@ (href "https://jennex.org/license.html")) "License") ")")))) - - -(define (route-get-index c) - (send-sxml - (template-page - '((h2 "RSVP") - (p "Thanks for RSVP'ing! To start, please lookup the " - "name on your invitation.") - (form (@ (class "find-invite") (action "/rsvp")) - (input (@ (class "find-invite") (name "rsvp-name"))) - (button (@ (class "find-invite")) "Find Invitation")) - (p "Please let us know by DATE whether you can make it!"))))) - -(define (guest-to-form g) - (define (input-name key) - `(name ,(conc (number->string (guest-id g)) "__" key))) - (define (get-going-attrs val) - `(,(input-name "going") - (value ,val) - (type "radio") - (required "true") - ,@(if (equal? (guest-going g) val) '((checked)) '()))) - (define (get-meal-attrs val) - `((value ,val) - ,@(if (equal? (guest-meal-choice g) val) '((selected)) '()))) - ;; TODO: add notes section for allergies, etc - `((div (@ (class "guest")) - (fieldset - (legend ,(guest-name g)) - (label (strong "Name: ") - (input (@ ,(input-name "name") (required "true") (value ,(guest-name g))))) - (p (strong "Will You be Attending?")) - (label (input (@ ,@(get-going-attrs 1))) "Yes!") - (br) - (label (input (@ ,@(get-going-attrs 0))) "No :(") - (br) - (label (@ (class "meal-choice")) - (strong "Meal Choice:") - (select (@ ,(input-name "meal-choice")) - (option (@ ,@(get-meal-attrs "")) "-- Please Select if Attending --") - (option (@ ,@(get-meal-attrs "chicken")) "Chicken") - (option (@ ,@(get-meal-attrs "beef")) "Beef") - (option (@ ,@(get-meal-attrs "vegetarian")) "Vegetarian") - (option (@ ,@(get-meal-attrs "vegan")) "Vegan"))))))) - -(define (route-get-rsvp c) - ;; TODO: strip trailing spaces in names? (here and on save) - (call/cc - (lambda (c) - (with-exception-handler - (lambda (exn) - (print-error-message exn) - ;; TODO: only print stack trace if NOT a sqlite3 no-data exeption! - (print-call-chain) - (send-sxml - (template-page - `((h2 "RSVP") - (p "Sorry! We can't find anyone under that name. " - "Please double check the spelling and if it looks like a " - "mistake on our end email us at " ,email) - (p (a (@ (href "/")) "Back"))))) - (c #f)) - (lambda () - ;; TODO: consider a POST instead of GET to prevent people from sharing - ;; their edit links? - (let* ((q (uri-query (request-uri (current-request)))) - (name (alist-ref 'rsvp-name q))) - (send-sxml - (template-page - (let ((party (get-party-by-name name))) - `((h2 "RSVP") - (p "Great news! You're invited :) We can't wait to celebrate with you!") - (p "We've found the following guests under your name. For each, please " - "select whether you'll make it and your choice of meal.") - (form (@ (action "/rsvp") (method "POST")) - ,@(map guest-to-form (party-guests party)) - (h3 "Additional Information") - (label (@ (for "notes")) - "Anything else we should know? (Allergies, kids, ...)") - (textarea (@ (class "party-notes") - (name ,(conc (party-id party) "__notes")) - (rows 5)) - ,(let ((notes (party-notes party))) - (if (sql-null? notes) - "" - notes))) - (button (@ (class "party-update")) "Save")))))))))))) - -(define key-to-setter - ;; Just an a-list of input names -> guest-setter functions - ;; (allowing the request to specify any string is dangerous) - `(("going" . ,guest-going-set!) - ("meal-choice" . ,guest-meal-choice-set!) - ("name" . ,guest-name-set!))) - -(define (route-post-rsvp c) - (call/cc - (lambda (c) - (with-exception-handler - (lambda (exn) - (print-error-message exn) - (print-call-chain) - (send-sxml - (template-page - `((h2 "RSVP") - ;; TODO: log it better... - (p "There was an error saving your response please try again " - "and if it continues to fail, reach out to us at " ,email)))) - (c #f)) - (lambda () - ;; The data is in pairs of (ID__key . val), so the first thing to do - ;; is to walk through that data and build up a view of each guest - (let loop ((guests '()) - (fdata (read-urlencoded-request-data (current-request))) - (notes "")) - (if (null? fdata) ;; Done parsing all our form data - (begin - ;; This will raise if update-guest fails (so we get an error page) - (map update-guest (map cdr guests)) - (let* ((first-guest (cdadr guests)) - (first-name (guest-name first-guest)) - (edit-link (conc "/rsvp?rsvp-name=" (uri-encode-string first-name)))) - (update-party-notes (guest-party-id first-guest) notes) - (send-sxml - (template-page - `((h2 "RSVP") - (p "Success! Thanks for RSVP-ing.") - (p (a (@ (href ,edit-link))) "Edit your response") - (p (a (@ (href "https://jennex.org")) "Read more about the event"))))))) - (let* ((input (car fdata)) - (name (car input)) - (value (cdr input)) - (split (string-split (symbol->string name) "__")) - (id (string->number (car split))) - (cached-guest (alist-ref id guests)) - (key (cadr split)) - (setter! (alist-ref key key-to-setter equal?))) - ;; special case notes since it updates the party - (if (equal? key "notes") - (loop guests (cdr fdata) value) - (if cached-guest - (begin - (setter! cached-guest value) - (loop guests (cdr fdata) notes)) - (let ((guest (get-guest-by-id id))) - (setter! guest value) - (loop (cons (cons id guest) guests) (cdr fdata) notes)))))))))))) diff --git a/src/main.scm b/src/main.scm @@ -1,22 +0,0 @@ -#!/usr/local/bin/chicken-csi -ss -;; main.scm -- configure and start the Spiffy web server -(import - srfi-18 - spiffy - spiffy-uri-match - (chicken process-context) - (chicken format)) - -(include "handler.scm") ;; contains 'routes' - -(access-log (current-output-port)) -(error-log (current-error-port)) -(root-path "../") - -;; Spiffy recommends using vhost-map to assign dynamic routes -;; (even if we aren't using the vhost portion of it) -(vhost-map `((".*" . ,(uri-match/spiffy routes)))) - -(define (main args) - (print (format "Starting up! Listening on port ~A..." (server-port))) - (start-server)) diff --git a/src/prod.scm b/src/prod.scm @@ -1,3 +0,0 @@ -;; prod.scm -- runs main from main.scm (for compiled binary) -(include "main.scm") -(main '()) diff --git a/src/setup.sql b/src/setup.sql @@ -1,30 +0,0 @@ --- Start fresh -DROP TABLE parties; -DROP TABLE guests; - --- Parties just group a series of guests so they can RSVP for eachother -CREATE TABLE parties ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - notes TEXT -); - --- Guests store individual preferences for meals, etc -CREATE TABLE guests ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name VARCHAR(255) NOT NULL, - party_id INTEGER NOT NULL, - going INTEGER, - meal_choice VARCHAR(255), - - FOREIGN KEY(party_id) REFERENCES parties(id) -); - -INSERT INTO parties (id) VALUES (1), (2), (3); - -INSERT INTO guests (name, party_id) VALUES - ('Alex', 1), - ('Jennie', 1), - ('Matt', 2), - ('Sarah', 3), - ('Sarah''s plus 1', 3), - ('Sammy', 2);