Initial build: Great Bear microbrewery site

- TanStack Start (React 19, Tailwind 4) frontend recreated from Stitch design
- PocketBase backend with schema migration + idempotent content seed
- Single Docker image running both services (non-root, healthchecked)
- deploy.sh to build & push to registry.tanshu.com
- Ansible playbook (roles: app, caddy) deploying to www.greatbear.in
  with Caddy TLS entries for the site and the PocketBase admin
This commit is contained in:
2026-09-03 08:51:47 +05:30
commit 7b070fde81
66 changed files with 8375 additions and 0 deletions
+122
View File
@@ -0,0 +1,122 @@
#!/usr/bin/env bash
# One-off helper used during development to create the Great Bear collections
# on a running PocketBase instance via the admin API.
# Usage: PB_URL=http://127.0.0.1:8099 PB_EMAIL=... PB_PASSWORD=... ./scripts/create-collections.sh
set -euo pipefail
PB_URL="${PB_URL:-http://127.0.0.1:8099}"
PB_EMAIL="${PB_EMAIL:?PB_EMAIL required}"
PB_PASSWORD="${PB_PASSWORD:?PB_PASSWORD required}"
TOKEN=$(curl -sS -X POST "$PB_URL/api/collections/_superusers/auth-with-password" \
-H "Content-Type: application/json" \
-d "{\"identity\":\"$PB_EMAIL\",\"password\":\"$PB_PASSWORD\"}" \
| python3 -c "import sys,json;print(json.load(sys.stdin)['token'])")
echo "authenticated: ${TOKEN:0:12}..."
WORKDIR=$(mktemp -d)
trap 'rm -rf "$WORKDIR"' EXIT
cd "$WORKDIR"
content_rules='"listRule":"","viewRule":"","createRule":null,"updateRule":null,"deleteRule":null'
# Reservations: visitors may create (forms) but only admins may read/update.
reservation_rules='"listRule":null,"viewRule":null,"createRule":"","updateRule":null,"deleteRule":null'
mk() { NAME=$1; FIELDS=$2; IDX=$3; RULES=${4:-$content_rules}; cat > "$NAME.json" <<EOF
{"name":"$NAME","type":"base",$RULES,"fields":$FIELDS,"indexes":$IDX}
EOF
}
mk brews '[
{"name":"uid","type":"text","required":true},
{"name":"name","type":"text","required":true},
{"name":"style","type":"text","required":true},
{"name":"short_name","type":"text"},
{"name":"abv","type":"number"},
{"name":"ibu","type":"number"},
{"name":"description","type":"text"},
{"name":"tasting_notes","type":"text"},
{"name":"pairing","type":"text"},
{"name":"image","type":"text"},
{"name":"featured","type":"bool"},
{"name":"flagship","type":"bool"},
{"name":"on_tap","type":"bool"},
{"name":"seasonal","type":"bool"},
{"name":"seasonal_icon","type":"text"},
{"name":"flight","type":"bool"},
{"name":"sort","type":"number"}]' '["CREATE UNIQUE INDEX idx_brews_uid ON brews (uid)"]'
mk stats '[
{"name":"uid","type":"text","required":true},
{"name":"value","type":"text","required":true},
{"name":"label","type":"text","required":true},
{"name":"sort","type":"number"}]' '["CREATE UNIQUE INDEX idx_stats_uid ON stats (uid)"]'
mk experiences '[
{"name":"uid","type":"text","required":true},
{"name":"title","type":"text","required":true},
{"name":"description","type":"text"},
{"name":"icon","type":"text"},
{"name":"image","type":"text"},
{"name":"sort","type":"number"}]' '["CREATE UNIQUE INDEX idx_experiences_uid ON experiences (uid)"]'
mk testimonials '[
{"name":"uid","type":"text","required":true},
{"name":"name","type":"text","required":true},
{"name":"role","type":"text"},
{"name":"quote","type":"text","required":true},
{"name":"rating","type":"number"},
{"name":"sort","type":"number"}]' '["CREATE UNIQUE INDEX idx_testimonials_uid ON testimonials (uid)"]'
mk menu_categories '[
{"name":"uid","type":"text","required":true},
{"name":"slug","type":"text","required":true},
{"name":"name","type":"text","required":true},
{"name":"sort","type":"number"}]' '["CREATE UNIQUE INDEX idx_menu_categories_uid ON menu_categories (uid)","CREATE UNIQUE INDEX idx_menu_categories_slug ON menu_categories (slug)"]'
mk menu_items '[
{"name":"uid","type":"text","required":true},
{"name":"category","type":"text","required":true},
{"name":"name","type":"text","required":true},
{"name":"description","type":"text"},
{"name":"price","type":"number"},
{"name":"pairing","type":"text"},
{"name":"tag","type":"text"},
{"name":"image","type":"text"},
{"name":"sort","type":"number"}]' '["CREATE UNIQUE INDEX idx_menu_items_uid ON menu_items (uid)"]'
mk pairings '[
{"name":"uid","type":"text","required":true},
{"name":"title","type":"text","required":true},
{"name":"description","type":"text"},
{"name":"sort","type":"number"}]' '["CREATE UNIQUE INDEX idx_pairings_uid ON pairings (uid)"]'
mk settings '[
{"name":"uid","type":"text","required":true},
{"name":"brand","type":"text","required":true},
{"name":"footer_about","type":"text"},
{"name":"address_line","type":"text"},
{"name":"city_line","type":"text"},
{"name":"hours_line","type":"text"},
{"name":"hours_multiline","type":"text"},
{"name":"phone_primary","type":"text"},
{"name":"phone_secondary","type":"text"},
{"name":"instagram_url","type":"text"},
{"name":"instagram_handle","type":"text"},
{"name":"maps_query","type":"text"}]' '["CREATE UNIQUE INDEX idx_settings_uid ON settings (uid)"]'
mk reservations '[
{"name":"name","type":"text","required":true},
{"name":"phone","type":"text","required":true},
{"name":"source","type":"text","required":true},
{"name":"date","type":"text"},
{"name":"time","type":"text"},
{"name":"guests","type":"text"},
{"name":"requests","type":"text"},
{"name":"status","type":"select","maxSelect":1,"values":["new","confirmed","cancelled"]}]' '[]' "$reservation_rules"
for NAME in brews stats experiences testimonials menu_categories menu_items pairings settings reservations; do
CODE=$(curl -sS -o "resp_$NAME.json" -w "%{http_code}" -X POST "$PB_URL/api/collections" \
-H "Authorization: $TOKEN" -H "Content-Type: application/json" -d @"$NAME.json")
echo "$NAME -> HTTP $CODE"
if [ "$CODE" != "200" ]; then cat "resp_$NAME.json"; echo; fi
done