Development Guide
This guide covers everything you need to develop the UA Capstone Tours project: setting up the backend with Docker, understanding how the Django API works, running the React frontend locally, and using the component tester for isolated UI development.
Prerequisites
- Docker Desktop — must be running before any
docker composecommands will work - macOS: standard install
- Windows: requires WSL2 enabled
- Git
- Node.js + npm — only required to run the Vite frontend dev server; the backend runs entirely in Docker
Environment Setup
Copy the example environment file to create your local config:
cp .env.example .env
.env is not committed to Git. Each developer keeps their own local copy with their own values.
| Variable | Purpose |
|---|---|
POSTGRES_DB |
Database name (default: ua_tour) |
POSTGRES_USER |
Database username (default: ua_tour) |
POSTGRES_PASSWORD |
Database password |
POSTGRES_PORT |
Host port the database is exposed on (default: 5432) |
DJANGO_SECRET_KEY |
Django cryptographic secret — use any long random string locally |
DJANGO_DEBUG |
Set to 1 for development, 0 for production |
DJANGO_ALLOWED_HOSTS |
Comma-separated list of allowed hostnames (e.g. localhost,127.0.0.1) |
DJANGO_PORT |
Host port the Django server is exposed on (default: 8000) |
POSTGRES_HOSTdoes not need to be set in.env. Docker Compose automatically sets it todb(the container service name) so Django can reach the database inside the Compose network.
Backend Setup (Docker)
All commands run from the repository root.
1. Build and start containers
docker compose up -d --build
This starts two services defined in compose.yaml:
db— PostGIS 3.4 on PostgreSQL 16, data persisted in a named Docker volume (pgdata)web— Django dev server;./backendis volume-mounted into the container so code changes reload automatically
The first build takes several minutes because it installs GDAL and other spatial libraries.
2. Run migrations
docker compose exec web python manage.py migrate
3. Create an admin user
docker compose exec web python manage.py createsuperuser
4. Verify
- API health check: http://localhost:8000/api/health/ →
{"status": "ok"} - Django admin: http://localhost:8000/admin
5. Stop the stack
docker compose down
Data in the pgdata volume persists between stops. To wipe the database entirely, run docker compose down -v.
Rebuild after dependency changes
Any time you add or remove packages from backend/requirements.txt, rebuild the image:
docker compose up -d --build
Common Backend Commands
Open a shell inside the Django container to avoid prefixing every command:
docker compose exec web bash
From inside the container (or with the docker compose exec web prefix from outside):
| Task | Command |
|---|---|
| Create migrations | python manage.py makemigrations |
| Apply migrations | python manage.py migrate |
| Django interactive shell | python manage.py shell |
| Run API tests | python manage.py test api |
| Check container status | docker compose ps (run from host) |
| View logs | docker compose logs web --tail=100 (run from host) |
Backend Architecture
The backend lives in the backend/ directory and is a standard Django project with two custom apps.
Directory Layout
backend/
├── config/ # Django project package (settings, root URL config, WSGI/ASGI)
├── api/ # REST API endpoints (views, serializers, URLs)
├── locations_db/ # Data models and Django admin configuration
├── manage.py
├── requirements.txt
└── Dockerfile
Runtime Architecture
- Clients call API endpoints under
/api/. - DRF generic views query
locations_dbmodels. - Serializers shape response payloads for list/detail/nearby use cases.
- Media files (landmark photos) are served from
/media/in development.
URL Routing
/api/→ API endpoints (backend/api/urls.py)/admin/→ Django admin/locations_db/→ temporary app route used for basic test response
Django Settings (config/settings.py)
Key configuration decisions:
- Database engine:
django.contrib.gis.db.backends.postgis— the GeoDjango-aware PostGIS backend, required for geographic field types and spatial queries - Media storage: In production, media files (landmark photos) are stored in a Google Cloud Storage bucket (
ua-capstone-media). In development (DEBUG=True), they are served locally frombackend/media/, which is Docker-mounted at./backend/media:/app/media - CORS: Currently set to
CORS_ALLOW_ALL_ORIGINS = Truefor development convenience - REST Framework: Default permission is
AllowAny— no authentication is required to read the API
The locations_db App — Data Models
This app owns the database schema.
Landmark model (locations_db/models.py):
| Field | Type | Notes |
|---|---|---|
name |
CharField(200) |
Display name |
short_description |
TextField |
Shown in map popups and list rows |
long_description |
TextField |
Shown on the full detail sheet |
location |
PointField(srid=4326, geography=True) |
WGS84 lat/lon, stored as PostGIS geography |
address |
CharField(255, blank) |
Optional street address |
is_published |
BooleanField(default=True) |
Controls visibility; unpublished landmarks are excluded from all API responses |
created_at / updated_at |
DateTimeField |
Auto-managed timestamps |
LandmarkPhoto model:
| Field | Type | Notes |
|---|---|---|
landmark |
ForeignKey(Landmark, CASCADE) |
Parent landmark |
image |
ImageField |
Stored under landmark_photos/%Y/%m/ |
caption |
CharField(255, blank) |
Optional display caption |
alt_text |
CharField(255, blank) |
Accessibility text |
sort_order |
PositiveIntegerField(default=0) |
Lower values appear first |
Admin: The Landmark admin uses a custom form that exposes separate latitude and longitude fields instead of a raw Point input. LandmarkPhotoInline lets you manage photos directly from the landmark edit page.
The api App — REST Endpoints
All endpoints live under the /api/ prefix.
| Method | Path | Description | Response shape |
|---|---|---|---|
GET |
/api/health/ |
Liveness check | {"status": "ok"} |
GET |
/api/landmarks/ |
All published landmarks — basic info for map pins | [{id, name, short_description, lat, lon}] |
GET |
/api/landmarks/full/ |
All published landmarks — includes first photo per landmark | [{id, name, short_description, long_description, lat, lon, address, photos}] |
GET |
/api/landmarks/<id>/ |
Single landmark — full detail with all photos | {id, name, short_description, long_description, lat, lon, address, photos[]} |
GET |
/api/landmarks/nearby/ |
Landmarks within a radius, ordered by distance | [{id, name, short_description, lat, lon, distance_m, cover_photo_url}] |
/api/landmarks/nearby/ query parameters:
| Parameter | Required | Default | Constraints |
|---|---|---|---|
lat |
Yes | — | −90 to 90 |
lon |
Yes | — | −180 to 180 |
radius_m |
No | 500 | 1 to 20000 |
Spatial query note: PostGIS Point objects are constructed as Point(lon, lat, srid=4326) — longitude is the x argument, latitude is y. This is the opposite of how coordinates are often written verbally, so be careful if you extend the nearby query logic.
Frontend Dev Server
The frontend is a React + Vite application in the frontend/ directory.
cd frontend
npm install # first time only
npm run dev
Vite starts at http://localhost:5173 with hot module replacement — edits to any .jsx or .css file reflect in the browser immediately without a full page reload.
Important: The Docker Compose
frontendservice (nginx at port 3000) is meant to preview a built static snapshot of the frontend, not for active development. Always use the Vite dev server at port 5173 when developing.
Build for production
npm run build
Output goes to frontend/dist/. The project deploys the built frontend to Firebase Hosting.
Frontend Architecture
Entry Point
src/main.jsx is the Vite entry point. It renders either the main application or the component tester:
// Application mode (default)
import App from './App.jsx'
// Component tester mode (swap in when doing isolated UI work)
import ComponentTester from './ComponentTester.jsx'
Routing
There is no URL-based router. App.jsx maintains an activeTab state string with four possible values: 'explore', 'list', 'tours', 'contribute'. It renders the matching page component and passes activeTab and onTabChange down as props.
Pages
| File | Status | Description |
|---|---|---|
pages/ExplorePage.jsx |
Active | Full-viewport map with all overlay UI |
pages/ListPage.jsx |
Active | Scrollable landmark list |
pages/ToursPage.jsx |
Placeholder | "Coming soon" |
pages/ContributePage.jsx |
Active | User contribution interface |
ExplorePage is the main interactive experience. It manages three overlay states — pinCard, exploreCard, and detailLandmark — and coordinates all map interaction through callbacks passed into MapComponent. Fetched landmark detail objects are cached in a useRef(new Map()) to avoid redundant network requests when the user taps the same pin multiple times.
Components
mapComponent.jsx
The Leaflet map. Key implementation details:
- The Leaflet map instance is created once in a
useEffectwith no dependency array and stored inmapRef. Re-renders do not re-initialize the map. callbacksRefis updated on every render (no dep array) so Leaflet's closed-over event handlers always call the latest prop callbacks without being in the dependency array — this is the standard pattern for integrating Leaflet (an imperative library) with React.- Geolocation uses
navigator.geolocation.watchPosition. Each position update runs a Haversine distance calculation against all landmark points. ASet(triggered) prevents the proximity callback from firing more than once per landmark per session. - Proximity radius is 61 m (≈ 200 ft), matching the
PROXIMITY_RADIUSconstant in the component. - Props:
onPinClick(point, viewportX, viewportY),onProximityEnter(point),onMapClick(),onPinMove(x, y),activePinId
landmarkDetailView.jsx
Full-screen bottom sheet that slides up over the map. Features:
- Entry animation uses an iOS-style
cubic-bezierspring curve - Touch drag-to-dismiss: tracking starts on
touchstart, sheet follows finger ontouchmove, closes if drag exceeds 100 px ontouchend - Photo carousel with previous/next buttons and dot indicators
topInsetprop positions the sheet below any fixed UI above it (passed from the parent page, which measures the navbar height)
exploreCard.jsx
Floating card that appears when the user enters proximity of a landmark. Positioned above the navbar using navbarHeight prop. Swipe-down-to-dismiss with a 50 px threshold; opacity fades as the card is dragged.
pinCard.jsx
Lightweight tooltip that appears above a tapped map pin. Positioned using viewportX / viewportY props (pixel coordinates from the map click event, passed up by MapComponent). Uses CSS transforms to center it horizontally and lift it above the pin with a triangular pointer.
navbar.jsx
Fixed bottom tab bar with four tabs (Explore, Tours, List, Contribute). Rendered as a crimson pill; the active tab has a white background.
listView.jsx
Renders a scrollable list of landmark rows. Each row shows a thumbnail, name, and short description with a chevron affordance.
Styling
src/index.css — global reset, UA brand design tokens, z-index scale:
--ua-crimson: #9E1B32; /* Primary brand color */
--ua-capstone-gray: #828A8F;
--ua-pachyderm: #5F6A72; /* Secondary text */
--ua-chimes-gray: #C1C6C9; /* Dividers */
--ua-crimson-glow: rgba(158, 27, 50, 0.35);
/* Z-index scale (Leaflet uses up to ~700) */
--z-explore-card: 800;
--z-pin-card: 810;
--z-navbar: 820;
--z-backdrop: 1050;
--z-card: 1051;
--z-chrome: 1100;
Fonts: Trade Gothic (headings), Minion Pro (body/description text).
src/styles/components.css — all component-level styles (~570 lines): navbar, detail sheet, explore card, pin card, map controls, list view, animations.
API Integration
The backend base URL is currently hardcoded in the components:
https://ua-capstone-backend-845958693022.us-central1.run.app/api
For local development against the Docker backend, you need to change this to http://localhost:8000/api in the relevant components (mapComponent.jsx, ExplorePage.jsx, ListPage.jsx). The app uses native fetch for all requests.
Component Tester
src/ComponentTester.jsx is an interactive development harness that lets you build and test UI components in isolation — no running backend required (except for the Map tester, which needs landmark data).
Activating the Component Tester
In src/main.jsx, replace the App import with ComponentTester:
// Component tester mode
import ComponentTester from './ComponentTester.jsx'
createRoot(document.getElementById('root')).render(
<StrictMode>
<ComponentTester />
</StrictMode>,
)
To switch back to the real application, restore the App import:
// Application mode
import App from './App.jsx'
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)
Available Testers
| Tab | File | Components exercised |
|---|---|---|
| Navbar | testers/NavbarTester.jsx |
Navbar |
| Landmark | testers/LandmarkDetailTester.jsx |
LandmarkDetailView, Navbar |
| Explore | testers/ExploreCardTester.jsx |
ExploreCard, LandmarkDetailView, Navbar |
| Pin Card | testers/PinCardTester.jsx |
PinCard, LandmarkDetailView, Navbar |
| List | testers/ListViewTester.jsx |
ListView, LandmarkDetailView, Navbar |
| Map | testers/MapComponentTester.jsx |
MapComponent (requires Django backend for landmark markers) |
How It Works
ComponentTester maintains a single activeTester state string. A TESTERS registry (an array of { id, label, icon, component } objects) maps each tab to its tester component. The tester renders inside a scrollable content area below a fixed top bar.
The top bar's pixel height is measured with useLayoutEffect and passed to each tester as topBarHeight. Testers forward this as topInset to components like LandmarkDetailView that need to avoid overlapping fixed UI above them.
Each individual tester:
- Defines one or more dummy landmark fixtures covering relevant visual states (multiple photos, single photo, no photos, description-only)
- Renders interactive pill buttons to select fixtures, toggle the navbar on/off, and trigger the component
- Mounts and unmounts the component under test via those controls, exercising the full entry and exit animation lifecycle without needing real data or a backend
Docs Workflow (Read the Docs + MkDocs)
Project documentation is hosted at capstone-tours.readthedocs.io and built automatically from the docs/ directory.
- Read the Docs config:
readthedocs.yml(repository root) - Site structure and navigation:
mkdocs.yml(repository root) - Source files:
docs/directory (Markdown)
To add a new documentation page:
- Create a Markdown file in
docs/ - Add it to the
navsection inmkdocs.yml - Push to the repository — Read the Docs will rebuild automatically