updated 3 tier structure
This commit is contained in:
+15
-9
@@ -2,29 +2,35 @@
|
|||||||
|
|
||||||
## Project overview
|
## Project overview
|
||||||
|
|
||||||
This is a standalone-component Angular 21 app for a community radio station website. It renders a hero page, about page, broadcast schedule, donation tiers, upcoming events, and a contact form — all styled with a warm mountain/nature color palette (blue primary + orange accent on cream).
|
A three-tier web app for a community radio station: Angular 21 frontend, FastAPI backend, PostgreSQL database. Renders a hero page, about page, broadcast schedule, donation tiers, upcoming events, and a contact form — styled with a warm mountain/nature color palette (blue primary + orange accent on cream).
|
||||||
|
|
||||||
## Code style
|
## Code style
|
||||||
|
|
||||||
- **Components:** Standalone only (no NgModules). Every component is a single file with `@Component` decorator, `imports` array, and the four standard files (`.ts`, `.html`, `.scss`, component).
|
- **Components:** Standalone only (no NgModules). Every component is a single file with `@Component` decorator, `imports` array, and four standard files (`.ts`, `.html`, `.scss`, component).
|
||||||
- **Data patterns:** Page content lives as `readonly` arrays with interfaces defined in the same file. Never put content in services or APIs — this is a static site.
|
- **Data patterns:** Page content (programs, events, tiers) is stored in PostgreSQL and served via the FastAPI backend. The frontend uses HTTP services ([src/app/services/](src/app/services/)) to fetch data at runtime. To add or modify content, interact with the API or run `seed.py` — never hardcode content in components.
|
||||||
- **SCSS:** All design tokens go in [src/styles/_variables.scss](src/styles/_variables.scss). Mixins in [_mixins.scss](src/styles/_mixins.scss). CSS custom properties exposed in [_themes.scss](src/styles/_themes.scss). Components import via `@use 'variables'` (no relative path needed).
|
- **SCSS:** All design tokens go in [src/styles/_variables.scss](src/styles/_variables.scss). Mixins in [_mixins.scss](src/styles/_mixins.scss). CSS custom properties exposed in [_themes.scss](src/styles/_themes.scss). Components import via `@use 'variables'` (no relative path needed).
|
||||||
- **Routes:** Defined in [src/app/app.routes.ts](src/app/app.routes.ts) using `loadComponent()` lazy loading. Add new pages here after creating the component.
|
- **Routes:** Defined in [src/app/app.routes.ts](src/app/app.routes.ts) using `loadComponent()` lazy loading. Add new pages here after creating the component.
|
||||||
- **TypeScript:** Strict mode enabled. Use `readonly` for data arrays. No unnecessary interfaces — keep data shapes close to their components.
|
- **TypeScript:** Strict mode enabled. Use `readonly` for constants (config, URLs). Keep data shapes close to their components where used only once.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
See [README.md](../README.md) for a full architecture map. Key files:
|
See [README.md](README.md) for a full architecture map. Key files:
|
||||||
|
|
||||||
- `src/app/app.routes.ts` — all routing
|
- `src/app/app.routes.ts` — all frontend routing
|
||||||
|
- `src/environments/environment.ts` — dev API URL (`http://localhost:8000`)
|
||||||
|
- `src/environments/environment.prod.ts` — prod API URL (`''`, proxied through Nginx)
|
||||||
- `src/styles/_variables.scss` — color palette, spacing, breakpoints (change these to re-skin)
|
- `src/styles/_variables.scss` — color palette, spacing, breakpoints (change these to re-skin)
|
||||||
- `src/styles/_mixins.scss` — card-style, button-style, responsive, gradient-text, flowery-border, fade-in
|
- `src/styles/_mixins.scss` — card-style, button-style, responsive, gradient-text, flowery-border, fade-in
|
||||||
- `src/styles/_themes.scss` — SCSS → CSS custom property mapping
|
- `src/styles/_themes.scss` — SCSS → CSS custom property mapping
|
||||||
|
- `docker-compose.yml` — 3-service stack (db, api, frontend)
|
||||||
|
- `backend/app/main.py` — FastAPI entry point (CORS, router registration)
|
||||||
|
- `backend/app/models.py` — SQLAlchemy ORM (Program, Event, DonationTier)
|
||||||
|
|
||||||
## Common tasks
|
## Common tasks
|
||||||
|
|
||||||
- **Re-skin:** Edit colors in `_variables.scss` only.
|
- **Re-skin:** Edit colors in `_variables.scss` only.
|
||||||
- **Add a page:** Create component folder → add route in `app.routes.ts` → add nav link (if needed).
|
- **Add a page:** Create component folder → add route in `app.routes.ts` → add nav link (if needed).
|
||||||
- **Update content:** Edit the `readonly` array in the relevant component file (`schedule.component.ts`, `events.component.ts`, `donate.component.ts`).
|
- **Update content (dev):** Run the API, then use Swagger UI at [http://localhost:8000/docs](http://localhost:8000/docs) to create/update content.
|
||||||
- **Run the app:** `ng serve` → [localhost:4200](http://localhost:4200).
|
- **Update content (seed):** Edit [backend/seed.py](backend/seed.py) and run `docker compose exec api python seed.py`.
|
||||||
- **Build:** `ng build` → `dist/`.
|
- **Run the app:** `docker compose up --build` → [localhost:4200](http://localhost:4200).
|
||||||
|
- **Build:** `ng build` → `dist/`, then `docker compose up --build -d` for full stack.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
FROM alpine:3.23.0
|
FROM ubuntu:latest
|
||||||
|
|
||||||
RUN apk update && apk add git npm python3 bash podman
|
RUN apt update -y && apt install -y git npm python3 bash podman podman-compose fuse-overlayfs
|
||||||
|
|
||||||
# Install Angular.js via NPM
|
# Install Angular.js via NPM
|
||||||
RUN npm install -g @angular/cli@21.2.14
|
RUN npm install -g @angular/cli@21.2.14
|
||||||
|
|
||||||
ENV SHELL /bin/bash
|
# ENV SHELL /bin/bash
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# KMountain Flower Radio Station — Environment Variables
|
||||||
|
# Copy this file to .env and fill in the values. Do NOT commit .env.
|
||||||
|
|
||||||
|
# PostgreSQL connection string (backend)
|
||||||
|
# Format: postgresql+asyncpg://user:password@host:port/database
|
||||||
|
KMTN_DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5432/kmountain
|
||||||
|
|
||||||
|
# CORS allowlist for the backend (JSON array of allowed origins)
|
||||||
|
KMTN_CORS_ORIGINS=["http://localhost:4200"]
|
||||||
@@ -42,3 +42,6 @@ __screenshots__/
|
|||||||
# System files
|
# System files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
|
# Environment files
|
||||||
|
.env
|
||||||
|
|||||||
@@ -1,132 +1,250 @@
|
|||||||
# RadioStation — KMountain Flower
|
# KMountain Flower Radio Station
|
||||||
|
|
||||||
A static Angular boilerplate for a community radio station website. The app showcases a broadcast schedule, upcoming events, donation tiers, an about page, and a contact form — all styled with a warm mountain/nature aesthetic.
|
A three-tier web application for a community radio station: Angular 21 frontend, FastAPI backend, PostgreSQL database.
|
||||||
|
|
||||||
## Architecture
|
Renders a hero page, about page, broadcast schedule, donation tiers, upcoming events, and a contact form — styled with a warm mountain/nature aesthetic.
|
||||||
|
|
||||||
```
|
## Quick Start
|
||||||
src/
|
|
||||||
├── app/ # Angular application root
|
|
||||||
│ ├── app.ts # Root <app-root> component (shell layout)
|
|
||||||
│ ├── app.config.ts # Application config: providers + router
|
|
||||||
│ ├── app.html # Shell template: navbar + <router-outlet> + footer
|
|
||||||
│ ├── app.routes.ts # Route definitions (lazy-loaded components)
|
|
||||||
│ ├── app.scss # Root component styles
|
|
||||||
│ ├── hero/ # Landing page (home route)
|
|
||||||
│ ├── about/ # About the station page
|
|
||||||
│ ├── schedule/ # Program schedule (data-driven table)
|
|
||||||
│ ├── donate/ # Donation tiers (data-driven cards)
|
|
||||||
│ ├── events/ # Upcoming events (data-driven cards)
|
|
||||||
│ ├── contact/ # Contact form (two-way binding via FormsModule)
|
|
||||||
│ ├── navbar/ # Top navigation bar (responsive hamburger menu)
|
|
||||||
│ └── footer/ # Site footer
|
|
||||||
├── styles/ # Global SCSS design tokens
|
|
||||||
│ ├── _variables.scss # Color palette, spacing, breakpoints, z-index scale
|
|
||||||
│ ├── _mixins.scss # Reusable SCSS mixins (cards, buttons, responsiveness)
|
|
||||||
│ ├── _themes.scss # Maps SCSS vars → CSS custom properties on :root
|
|
||||||
│ └── index.scss # Global entry: reset + utility classes + theme import
|
|
||||||
├── assets/
|
|
||||||
│ └── svg/ # Decorative SVGs (mountain dividers, floral accents)
|
|
||||||
├── main.ts # App bootstrap entry point
|
|
||||||
└── index.html # HTML shell (<app-root> mount point)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Key architectural decisions
|
### Option A — Docker Compose (recommended)
|
||||||
|
|
||||||
- **Standalone components** — Every component is standalone (no NgModules). This is Angular 21's default and keeps imports explicit.
|
|
||||||
- **Lazy-loaded routes** — Each page is code-splitted via `loadComponent()` in [app.routes.ts](src/app/app.routes.ts). The router imports each component on first navigation.
|
|
||||||
- **Data-driven pages** — `ScheduleComponent`, `DonateComponent`, and `EventsComponent` all hold their content as readonly arrays (`readonly programs: Program[]`, etc.). To update content, edit these arrays in the component TypeScript files.
|
|
||||||
- **Design tokens in SCSS** — All colors, spacing, breakpoints, and typography live in [src/styles/_variables.scss](src/styles/_variables.scss). Change these once and every component inherits the new values via [_themes.scss](src/styles/_themes.scss) (which exposes them as CSS custom properties) and the mixins in [_mixins.scss](src/styles/_mixins.scss).
|
|
||||||
- **SCSS preprocessor paths** — [angular.json](angular.json) adds `src/styles/` to the include path, so any component can `@use 'variables'` without a relative path.
|
|
||||||
|
|
||||||
## Pages
|
|
||||||
|
|
||||||
| Route | Component | Description |
|
|
||||||
| -------- | --------------- | -------------------------------------------- |
|
|
||||||
| `/` | HeroComponent | Hero landing section |
|
|
||||||
| `/about` | AboutComponent | Station mission & info |
|
|
||||||
| `/schedule` | ScheduleComponent | Program schedule, rendered from `programs[]` |
|
|
||||||
| `/donate` | DonateComponent | Donation tiers, rendered from `tiers[]` |
|
|
||||||
| `/events` | EventsComponent | Upcoming events, rendered from `upcomingEvents[]` |
|
|
||||||
| `/contact` | ContactComponent | Contact form with submission handler |
|
|
||||||
|
|
||||||
## Customization guide
|
|
||||||
|
|
||||||
### Re-skinning the color palette
|
|
||||||
1. Edit the color variables in [src/styles/_variables.scss](src/styles/_variables.scss). The primary palette (blue) and accent palette (orange) control the entire site.
|
|
||||||
2. That's it — [_themes.scss](src/styles/_themes.scss) automatically maps them to CSS custom properties used by every component.
|
|
||||||
|
|
||||||
### Changing typography
|
|
||||||
Edit `$font-primary`, `$font-heading`, and `$font-mono` in [src/styles/_variables.scss](src/styles/_variables.scss).
|
|
||||||
|
|
||||||
### Adding a new page
|
|
||||||
1. Create a new folder under `src/app/` with a `*.component.ts`, `*.component.html`, and `*.component.scss`.
|
|
||||||
2. Export a `@Component` decorated class.
|
|
||||||
3. Add a route in [app.routes.ts](src/app/app.routes.ts) using `loadComponent()` to lazy-load it.
|
|
||||||
4. (Optional) Add a `RouterLink` in [navbar.component.html](src/app/navbar/navbar.component.html).
|
|
||||||
|
|
||||||
### Updating schedule / events / donation content
|
|
||||||
Edit the `readonly` arrays directly in their respective component files:
|
|
||||||
- [schedule.component.ts](src/app/schedule/schedule.component.ts) — `programs`
|
|
||||||
- [events.component.ts](src/app/events/events.component.ts) — `upcomingEvents`
|
|
||||||
- [donate.component.ts](src/app/donate/donate.component.ts) — `tiers`
|
|
||||||
|
|
||||||
Each interface (`Program`, `Event`, `Tier`) is defined in the component file. Modify the interfaces and data together.
|
|
||||||
|
|
||||||
## Getting started
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
- Node.js 20+ (or use the dev container)
|
|
||||||
- npm 11+
|
|
||||||
|
|
||||||
### Quick start
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install
|
# Start all services
|
||||||
ng serve
|
docker compose up --build
|
||||||
|
|
||||||
|
# Seed the database (first run only)
|
||||||
|
docker compose exec api python seed.py
|
||||||
```
|
```
|
||||||
|
|
||||||
Open [http://localhost:4200](http://localhost:4200).
|
Open [http://localhost:4200](http://localhost:4200).
|
||||||
|
|
||||||
### Development server
|
### Option B — Manual (frontend + backend separately)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ng serve # Start dev server (hot reload enabled)
|
# Database
|
||||||
ng serve --port 8080 # Custom port
|
createdb kmountain
|
||||||
|
export KMTN_DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5432/kmountain
|
||||||
|
|
||||||
|
# Backend
|
||||||
|
cd backend && pip install -r requirements.txt
|
||||||
|
uvicorn app.main:app --reload
|
||||||
|
|
||||||
|
# Frontend (new terminal)
|
||||||
|
npm install && ng serve
|
||||||
```
|
```
|
||||||
|
|
||||||
### Building
|
Open [http://localhost:4200](http://localhost:4200) and ensure the backend is running on [http://localhost:8000](http://localhost:8000).
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
| Requirement | Version | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| Docker + Docker Compose | 24+ | For the Docker Compose path |
|
||||||
|
| Node.js | 20+ | For manual frontend development |
|
||||||
|
| npm | 11+ | |
|
||||||
|
| Python | 3.12+ | For manual backend development |
|
||||||
|
| PostgreSQL | 16+ | For manual backend development (Docker handles it) |
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────┐
|
||||||
|
│ Browser │
|
||||||
|
│ :4200 / :80│
|
||||||
|
└──────┬───────┘
|
||||||
|
│
|
||||||
|
┌────────────┴────────────┐
|
||||||
|
│ │
|
||||||
|
┌────▼─────┐ ┌──────▼─────┐
|
||||||
|
│ Nginx │ /api/* │ FastAPI │
|
||||||
|
│ (static) │─ proxy ──▶ │ :8000 │
|
||||||
|
└────┬─────┘ └──────┬─────┘
|
||||||
|
│ │
|
||||||
|
┌────▼─────┐ ┌──────▼─────┐
|
||||||
|
│ Angular 21│ │ PostgreSQL │
|
||||||
|
│ SPA │ │ :5432 │
|
||||||
|
└──────────┘ └────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Three tiers:**
|
||||||
|
|
||||||
|
1. **Frontend** — Angular 21 SPA built via multi-stage Dockerfile and served by Nginx. In production, Nginx serves the static files and proxies `/api/*` requests to the backend. In development, both run on separate ports and the browser talks directly to both.
|
||||||
|
2. **Backend** — Python FastAPI with async SQLAlchemy. Provides CRUD REST endpoints for programs (broadcast schedule), events (upcoming events), and donation tiers. Auto-generates Swagger/OpenAPI docs at `/docs`.
|
||||||
|
3. **Database** — PostgreSQL 16. Schema contains three tables: `programs`, `events`, `donation_tiers`. Initial data is loaded via [seed.py](backend/seed.py).
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├── docker-compose.yml # 3-service stack (db, api, frontend)
|
||||||
|
├── Dockerfile # Multi-stage: Angular build → Nginx serve
|
||||||
|
├── nginx.conf # Static files + /api/* proxy
|
||||||
|
├── .env.example # Environment variable template
|
||||||
|
├── .devcontainer/ # VS Code dev container
|
||||||
|
│
|
||||||
|
├── src/ # Angular 21 frontend
|
||||||
|
│ ├── app/ # Standalone components
|
||||||
|
│ │ ├── app.routes.ts # Lazy-loaded route map
|
||||||
|
│ │ ├── hero/ about/ schedule/ donate/ events/ contact/ navbar/ footer/
|
||||||
|
│ │ ├── services/ # HTTP services (program, event, tier)
|
||||||
|
│ │ └── interfaces/ # TypeScript types (Program, Event, Tier)
|
||||||
|
│ ├── environments/ # dev/prod config (apiBaseUrl)
|
||||||
|
│ └── styles/ # SCSS design tokens
|
||||||
|
│ ├── _variables.scss # Colors, spacing, breakpoints
|
||||||
|
│ ├── _mixins.scss # Card, button, responsive mixins
|
||||||
|
│ └── _themes.scss # SCSS → CSS custom properties
|
||||||
|
│
|
||||||
|
└── backend/ # FastAPI backend
|
||||||
|
├── Dockerfile # Python gunicorn/uvicorn
|
||||||
|
├── requirements.txt
|
||||||
|
├── seed.py # Initial data for all models
|
||||||
|
└── app/
|
||||||
|
├── main.py # FastAPI app, CORS, router registration
|
||||||
|
├── config.py # ENV-based settings (KMTN_* prefix)
|
||||||
|
├── database.py # async engine + session factory
|
||||||
|
├── models.py # SQLAlchemy ORM (Program, Event, DonationTier)
|
||||||
|
├── schemas.py # Pydantic schemas (Create/Update/Response)
|
||||||
|
└── api/
|
||||||
|
├── programs.py # CRUD /api/programs
|
||||||
|
├── events.py # CRUD /api/events
|
||||||
|
└── tiers.py # CRUD /api/tiers
|
||||||
|
```
|
||||||
|
|
||||||
|
## Local Development
|
||||||
|
|
||||||
|
### Docker Compose
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ng build # Production build → dist/
|
docker compose up --build # Start all services
|
||||||
ng build --configuration development # With source maps
|
docker compose exec api python seed.py # Seed database (first run)
|
||||||
|
docker compose logs -f api # Stream backend logs
|
||||||
|
docker compose down # Stop and remove containers
|
||||||
```
|
```
|
||||||
|
|
||||||
### Code style
|
The `pgdata` volume persists the database across container restarts.
|
||||||
|
|
||||||
This project uses [Prettier](https://prettier.io/) (configured in [.prettierrc](.prettierrc)). Run:
|
### Manual Development
|
||||||
|
|
||||||
|
Run all three layers in parallel:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx prettier --write .
|
# 1. Database (install PostgreSQL 16 locally)
|
||||||
|
createdb kmountain
|
||||||
|
|
||||||
|
# 2. Backend (new terminal)
|
||||||
|
cd backend
|
||||||
|
pip install -r requirements.txt
|
||||||
|
export KMTN_DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5432/kmountain
|
||||||
|
export KMTN_CORS_ORIGINS='["http://localhost:4200"]'
|
||||||
|
uvicorn app.main:app --reload
|
||||||
|
|
||||||
|
# 3. Frontend (another terminal)
|
||||||
|
npm install
|
||||||
|
ng serve
|
||||||
```
|
```
|
||||||
|
|
||||||
### Testing
|
## Configuration
|
||||||
|
|
||||||
|
| Variable | Default | Where | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `KMTN_DATABASE_URL` | `postgresql+asyncpg://postgres:postgres@localhost:5432/kmountain` | [backend/app/config.py](backend/app/config.py) | Async PostgreSQL connection string |
|
||||||
|
| `KMTN_CORS_ORIGINS` | `["http://localhost:4200"]` | [backend/app/config.py](backend/app/config.py) | CORS allowlist (JSON array) |
|
||||||
|
| `apiBaseUrl` | `http://localhost:8000` (dev) / `''` (prod) | [src/environments/](src/environments/) | Where the Angular app finds the API |
|
||||||
|
|
||||||
|
Environment variables are prefixed with `KMTN_` in the backend. For production, set `KMTN_DATABASE_URL` to your managed PostgreSQL connection and update `KMTN_CORS_ORIGINS` to your actual domain.
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ng test # Run unit tests (Vitest)
|
# Frontend (produces dist/radio-station/browser/)
|
||||||
|
ng build --configuration production
|
||||||
|
|
||||||
|
# Backend Docker image
|
||||||
|
docker build -t kmountain-api -f backend/Dockerfile backend/
|
||||||
|
|
||||||
|
# Full production stack
|
||||||
|
docker compose up --build -d
|
||||||
```
|
```
|
||||||
|
|
||||||
## Dev container
|
## Testing
|
||||||
|
|
||||||
This project ships with a [.devcontainer/](.devcontainer/) for VS Code. Open the repo in VS Code → "Reopen in Container" to get a pre-configured environment with Angular CLI, Node.js, Python 3, and Git.
|
```bash
|
||||||
|
ng test # Angular + Vitest
|
||||||
|
```
|
||||||
|
|
||||||
## Project tech stack
|
The backend API is self-documented at [http://localhost:8000/docs](http://localhost:8000/docs) (Swagger UI). Run `docker compose exec api python seed.py` and visit that URL to interactively test endpoints.
|
||||||
|
|
||||||
- **Framework:** Angular 21 (standalone components, signals-ready)
|
## API Reference
|
||||||
- **Language:** TypeScript 5.9
|
|
||||||
- **Styling:** SCSS with design tokens (variables → mixins → CSS custom properties)
|
The backend exposes CRUD endpoints for three models:
|
||||||
- **Routing:** Angular Router with lazy loading
|
|
||||||
- **Form handling:** FormsModule (two-way binding)
|
| Method | Endpoint | Description |
|
||||||
- **Linting/formatting:** Prettier
|
|---|---|---|
|
||||||
- **Testing:** Vitest (via Angular CLI)
|
| `GET` | `/api/programs` | List all programs (`?day=1..7` to filter by day of week) |
|
||||||
- **Package manager:** npm 11.11
|
| `GET` | `/api/programs/:id` | Single program |
|
||||||
|
| `POST` | `/api/programs` | Create program |
|
||||||
|
| `PUT` | `/api/programs/:id` | Update program |
|
||||||
|
| `DELETE` | `/api/programs/:id` | Delete program |
|
||||||
|
| `GET` | `/api/events` | List events (`?active=true` to filter active events) |
|
||||||
|
| `GET` | `/api/events/:id` | Single event |
|
||||||
|
| `POST` | `/api/events` | Create event |
|
||||||
|
| `PUT` | `/api/events/:id` | Update event |
|
||||||
|
| `DELETE` | `/api/events/:id` | Delete event |
|
||||||
|
| `GET` | `/api/tiers` | List donation tiers (ordered by `display_order`) |
|
||||||
|
| `GET` | `/api/tiers/:id` | Single tier |
|
||||||
|
| `POST` | `/api/tiers` | Create tier |
|
||||||
|
| `PUT` | `/api/tiers/:id` | Update tier |
|
||||||
|
| `DELETE` | `/api/tiers/:id` | Delete tier |
|
||||||
|
|
||||||
|
Full interactive API docs at [http://localhost:8000/docs](http://localhost:8000/docs).
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
### Production (Docker Compose on a VPS)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Clone repo on server
|
||||||
|
git clone <repo-url> kmountain
|
||||||
|
cd kmountain
|
||||||
|
|
||||||
|
# 2. Edit docker-compose.yml
|
||||||
|
# - Change POSTGRES_PASSWORD to a strong value
|
||||||
|
# - Update KMTN_CORS_ORIGINS to your domain
|
||||||
|
# - Optionally set KMTN_DATABASE_URL to a managed PostgreSQL
|
||||||
|
|
||||||
|
# 3. Build and start
|
||||||
|
docker compose up --build -d
|
||||||
|
|
||||||
|
# 4. Seed the database
|
||||||
|
docker compose exec api python seed.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production hardening checklist
|
||||||
|
|
||||||
|
- [ ] Change the default `POSTGRES_PASSWORD` in [docker-compose.yml](docker-compose.yml)
|
||||||
|
- [ ] Update `KMTN_CORS_ORIGINS` to your actual domain (not `localhost`)
|
||||||
|
- [ ] Set `KMTN_DATABASE_URL` to a managed PostgreSQL connection (RDS, Cloud SQL, etc.)
|
||||||
|
- [ ] Add a reverse proxy (Caddy, Traefik) in front of Nginx for HTTPS
|
||||||
|
- [ ] Set up log rotation for Docker containers
|
||||||
|
- [ ] Configure Docker image registry and push/pull workflow
|
||||||
|
|
||||||
|
## Development Tools
|
||||||
|
|
||||||
|
- **Dev container** — Open the repo in VS Code → "Reopen in Container" for a pre-configured environment with Angular CLI, Node.js, Python 3, and Git ([.devcontainer/](.devcontainer/)).
|
||||||
|
- **VS Code workspace** — [workspace file](web_app.code-workspace) in the repo root configures both Angular and Python extensions.
|
||||||
|
- **Prettier** — Run `npx prettier --write .` to format the codebase.
|
||||||
|
- **Prettier config** — [**.prettierrc**](.prettierrc) (100 char width, single quotes, Angular HTML parser).
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
| Layer | Technology |
|
||||||
|
|---|---|
|
||||||
|
| Frontend | Angular 21 (standalone components, signals-ready) |
|
||||||
|
| Backend | FastAPI 0.115, Python 3.12 |
|
||||||
|
| Database | PostgreSQL 16 (async via SQLAlchemy 2.0) |
|
||||||
|
| Styling | SCSS with design tokens (variables → mixins → CSS custom properties) |
|
||||||
|
| Routing | Angular Router (lazy loaded) + Nginx SPA fallback |
|
||||||
|
| HTTP | RxJS observables (frontend → FastAPI) |
|
||||||
|
| Containers | Docker Compose (3 services: db, api, frontend) |
|
||||||
|
| CI/CD | _(not yet configured)_ |
|
||||||
|
| Testing | Vitest (via Angular CLI) |
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# Dev-sidecar compose: app container + PostgreSQL sidecar.
|
||||||
|
# Used by .devcontainer/devcontainer.json via dockerComposeFile.
|
||||||
|
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: .devcontainer/Dockerfile
|
||||||
|
restart: "no"
|
||||||
|
ports:
|
||||||
|
- "8000:8000" # FastAPI
|
||||||
|
- "4200:4200" # Angular dev server
|
||||||
|
environment:
|
||||||
|
KMTN_DATABASE_URL: postgresql+asyncpg://postgres:postgres@db:5432/kmountain
|
||||||
|
KMTN_CORS_ORIGINS: '["http://localhost:4200"]'
|
||||||
|
volumes:
|
||||||
|
- ..:/workspaces/web_app:cached
|
||||||
|
- node_modules:/workspaces/web_app/node_modules
|
||||||
|
- backend_venv:/app/.venv
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
command: >
|
||||||
|
sh -c "
|
||||||
|
pip install -r backend/requirements.txt &&
|
||||||
|
cd backend &&
|
||||||
|
uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload &
|
||||||
|
cd /workspaces/web_app &&
|
||||||
|
npm install &&
|
||||||
|
npx ng serve --host 0.0.0.0 --port 4200
|
||||||
|
"
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: docker.io/library/postgres:16-alpine
|
||||||
|
restart: "no"
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: kmountain
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: postgres
|
||||||
|
volumes:
|
||||||
|
- pgdata:/var/lib/postgresql/data
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
pgdata:
|
||||||
|
node_modules:
|
||||||
|
backend_venv:
|
||||||
@@ -2,6 +2,8 @@ services:
|
|||||||
db:
|
db:
|
||||||
image: docker.io/library/postgres:16-alpine
|
image: docker.io/library/postgres:16-alpine
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
# NOTE: Change POSTGRES_PASSWORD in production. Consider using Docker secrets or
|
||||||
|
# a managed PostgreSQL service instead of local credentials.
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_DB: kmountain
|
POSTGRES_DB: kmountain
|
||||||
POSTGRES_USER: postgres
|
POSTGRES_USER: postgres
|
||||||
@@ -14,6 +16,8 @@ services:
|
|||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|
||||||
|
# NOTE: Update KMTN_CORS_ORIGINS in production to your actual domain(s).
|
||||||
|
# Set KMTN_DATABASE_URL to your managed PostgreSQL connection if not using the local db service.
|
||||||
api:
|
api:
|
||||||
build:
|
build:
|
||||||
context: ./backend
|
context: ./backend
|
||||||
|
|||||||
@@ -1,243 +0,0 @@
|
|||||||
# Backend Infrastructure for KMountain Flower Radio Station
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
The app currently has a polished frontend with zero backend — all dynamic content (schedule, events, donations) lives as hardcoded `readonly` arrays in components. The contact form just shows an alert. We need to add persistent data, a backend API, and Docker-based deployment while keeping things simple for a volunteer-run community radio station.
|
|
||||||
|
|
||||||
## VS Code Workspace Setup
|
|
||||||
|
|
||||||
Use a **multi-root workspace** so both projects get their own IDE context, linting, and debugging configs:
|
|
||||||
|
|
||||||
```
|
|
||||||
web_app/ ← repo root (existing)
|
|
||||||
├── .vscode/
|
|
||||||
│ ├── settings.json ← workspace settings (shared)
|
|
||||||
│ ├── tasks.json ← common tasks (start backend, start frontend)
|
|
||||||
│ └── launch.json ← debug configs for both projects
|
|
||||||
├── .vscode/workspace.code-workspace ← multi-root workspace file (open this in VS Code)
|
|
||||||
├── src/ ← existing Angular frontend (unchanged structure)
|
|
||||||
├── backend/ ← new FastAPI backend
|
|
||||||
│ ├── .vscode/
|
|
||||||
│ │ ├── settings.json ← Python/Flake8/Pylance settings
|
|
||||||
│ │ └── launch.json ← FastAPI debug config
|
|
||||||
├── docker-compose.yml
|
|
||||||
├── Dockerfile ← Angular build + nginx serve (at repo root)
|
|
||||||
└── nginx.conf ← nginx config (at repo root)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Steps:**
|
|
||||||
1. Create `web_app/.vscode/` directory with shared workspace settings
|
|
||||||
2. Create `web_app/.vscode/workspace.code-workspace` listing both roots:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"folders": [
|
|
||||||
{ "path": "." },
|
|
||||||
{ "path": "backend" }
|
|
||||||
],
|
|
||||||
"settings": {}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
3. VS Code: File → Open Workspace from File → select `workspace.code-workspace`
|
|
||||||
4. Backend folder gets Python language server; frontend folder gets Angular TS language server — no conflict
|
|
||||||
|
|
||||||
## Decisions (confirmed)
|
|
||||||
|
|
||||||
- **Admin panel**: Skip for now. Initial data via a seed script.
|
|
||||||
- **Contact form**: Leave as-is (alert only, unimplemented for now).
|
|
||||||
- **Rollout**: Phase by phase.
|
|
||||||
- **Backend**: FastAPI (Python 3 already in devcontainer, auto-docs, async).
|
|
||||||
- **Database**: PostgreSQL in Docker (production-ready, any managed host compatible).
|
|
||||||
- **Frontend serve**: nginx in Docker (build Angular → serve static).
|
|
||||||
|
|
||||||
## Target Architecture
|
|
||||||
|
|
||||||
```
|
|
||||||
docker-compose.yml
|
|
||||||
├── db (postgres:16-alpine) -- data tier, persistent volume
|
|
||||||
├── api (FastAPI + uvicorn) -- middleware tier, CORS + all endpoints
|
|
||||||
└── frontend (nginx) -- frontend tier, Angular build + API proxy in prod
|
|
||||||
```
|
|
||||||
|
|
||||||
## Phase 1: Backend + Schedule Page + Docker Skeleton
|
|
||||||
|
|
||||||
### 1.0 VS Code workspace setup
|
|
||||||
|
|
||||||
1. Create `.vscode/` directory with `workspace.code-workspace`, `settings.json`, `tasks.json`, `launch.json`
|
|
||||||
2. Create `backend/.vscode/settings.json` (Python language settings)
|
|
||||||
3. Open `workspace.code-workspace` in VS Code — both roots visible, each with its own language server
|
|
||||||
4. `tasks.json` provides unified commands: "Start Backend" (uvicorn --reload), "Start Frontend" (ng serve), "Docker Up"
|
|
||||||
|
|
||||||
### 1.1 Backend skeleton
|
|
||||||
|
|
||||||
Create `backend/` directory:
|
|
||||||
|
|
||||||
```
|
|
||||||
backend/
|
|
||||||
├── requirements.txt -- fastapi, uvicorn, psycopg2-binary, alembic, python-multipart
|
|
||||||
├── Dockerfile -- python:3.12-slim, copy requirements, copy app, CMD uvicorn
|
|
||||||
├── seed.py -- seed 10 weekday programs
|
|
||||||
└── app/
|
|
||||||
├── __init__.py
|
|
||||||
├── main.py -- FastAPI app, lifespan (DB init), CORS middleware
|
|
||||||
├── config.py -- DATABASE_URL, CORS_ORIGINS from env vars
|
|
||||||
├── database.py -- async SessionLocal engine
|
|
||||||
├── models.py -- Program SQLAlchemy model
|
|
||||||
├── schemas.py -- Program Pydantic schemas (read + create + update)
|
|
||||||
└── api/
|
|
||||||
├── __init__.py
|
|
||||||
└── programs.py -- GET /api/programs (with ?day filter), POST, PUT, DELETE
|
|
||||||
```
|
|
||||||
|
|
||||||
**Program model**: `id, day_of_week (int), day_label (str), time (str), title (str), host (str), genre (str)`
|
|
||||||
|
|
||||||
**Seed data**: The 10 existing weekday programs from [schedule.component.ts:19-29](src/app/schedule/schedule.component.ts).
|
|
||||||
|
|
||||||
### 1.2 Angular service layer
|
|
||||||
|
|
||||||
Modify:
|
|
||||||
- [src/app/app.config.ts](src/app/app.config.ts) -- add `provideHttpClient()` to providers
|
|
||||||
- [src/environments/environment.ts](src/environments/environment.ts) -- **new**: `apiBaseUrl` for dev/prod
|
|
||||||
|
|
||||||
Create:
|
|
||||||
- [src/app/interfaces/program.ts](src/app/interfaces/program.ts) -- `Program` interface mirroring backend
|
|
||||||
- [src/app/services/program.service.ts](src/app/services/program.service.ts) -- `getPrograms()`, `getProgram()`, `createProgram()`, etc.
|
|
||||||
|
|
||||||
### 1.3 Convert schedule component
|
|
||||||
|
|
||||||
Modify [src/app/schedule/schedule.component.ts](src/app/schedule/schedule.component.ts):
|
|
||||||
- Remove `readonly programs: Program[]` array
|
|
||||||
- `inject(ProgramService)` in class body
|
|
||||||
- Add `programs: Program[] = []` and `loading = true`
|
|
||||||
- Load data in `ngOnInit()` via service
|
|
||||||
- Template: add loading/empty states, keep `*ngFor` structure
|
|
||||||
|
|
||||||
### 1.4 Docker skeleton
|
|
||||||
|
|
||||||
Create:
|
|
||||||
- [docker-compose.yml](docker-compose.yml) -- db + api services, healthcheck on db, depends_on ordering, named `pgdata` volume
|
|
||||||
- [backend/Dockerfile](backend/Dockerfile) -- already covered in 1.1
|
|
||||||
|
|
||||||
### Phase 1 verification
|
|
||||||
|
|
||||||
| Check | How |
|
|
||||||
|-------|-----|
|
|
||||||
| Backend runs locally | `cd backend && uvicorn app.main:app --reload` |
|
|
||||||
| OpenAPI docs at `/docs` | Browse `http://localhost:8000/docs` |
|
|
||||||
| Seed data present | `curl http://localhost:8000/api/programs` returns 10 items |
|
|
||||||
| Schedule page fetches from DB | DevTools Network tab shows GET to `/api/programs`, page renders |
|
|
||||||
| Docker stack | `docker compose up` -- db + api running, schedule renders on frontend |
|
|
||||||
|
|
||||||
## Phase 2: Donate + Events Pages
|
|
||||||
|
|
||||||
### 2.1 Backend additions
|
|
||||||
|
|
||||||
Modify `backend/app/`:
|
|
||||||
- [models.py](backend/app/models.py) -- add `Event` and `DonationTier` models
|
|
||||||
- Event: `id, date (DATE), title, description (TEXT), location, icon, rsvp_url (nullable), active (bool)`
|
|
||||||
- DonationTier: `id, name (UNIQUE), amount (NUMERIC), icon, benefits (TEXT[]), display_order (int)`
|
|
||||||
- [schemas.py](backend/app/schemas.py) -- add Pydantic schemas for both
|
|
||||||
- [api/events.py](backend/app/api/events.py) -- GET/POST/PUT/DELETE with `?active` filter
|
|
||||||
- [api/tiers.py](backend/app/api/tiers.py) -- GET/POST/PUT/DELETE ordered by `display_order`
|
|
||||||
- [seed.py](backend/seed.py) -- add events and tiers data (from [events.component.ts](src/app/events/events.component.ts) and [donate.component.ts](src/app/donate/donate.component.ts))
|
|
||||||
|
|
||||||
### 2.2 Angular additions
|
|
||||||
|
|
||||||
Create:
|
|
||||||
- [src/app/interfaces/event.ts](src/app/interfaces/event.ts)
|
|
||||||
- [src/app/interfaces/tier.ts](src/app/interfaces/tier.ts)
|
|
||||||
- [src/app/services/event.service.ts](src/app/services/event.service.ts) -- `getEvents()`, `createEvent()`, etc.
|
|
||||||
- [src/app/services/tier.service.ts](src/app/services/tier.service.ts) -- `getTiers()` ordered
|
|
||||||
|
|
||||||
Modify:
|
|
||||||
- [src/app/events/events.component.ts](src/app/events/events.component.ts) -- remove readonly array, inject EventService, load in ngOnInit
|
|
||||||
- [src/app/donate/donate.component.ts](src/app/donate/donate.component.ts) -- remove readonly array, inject TierService, load in ngOnInit
|
|
||||||
- Keep template structure identical, just swap the data source
|
|
||||||
|
|
||||||
### Phase 2 verification
|
|
||||||
|
|
||||||
| Check | How |
|
|
||||||
|-------|-----|
|
|
||||||
| Events API | `curl http://localhost:8000/api/events?active=true` returns 4 events |
|
|
||||||
| Tiers API | `curl http://localhost:8000/api/tiers` returns 4 tiers ordered |
|
|
||||||
| Events page renders from DB | Navigate to /events, verify from DB not hardcoded |
|
|
||||||
| Donate page renders from DB | Navigate to /donate, verify from DB not hardcoded |
|
|
||||||
|
|
||||||
## Phase 3: Frontend Docker + Full Stack
|
|
||||||
|
|
||||||
### 3.1 Dockerfile + nginx config
|
|
||||||
|
|
||||||
Create at repo root alongside `src/` and `backend/`:
|
|
||||||
|
|
||||||
- `Dockerfile` -- multi-stage: node:20-alpine build Angular → nginx:alpine serve the dist output
|
|
||||||
- `nginx.conf` -- serve Angular static files; in production mode, proxy `/api/*` to api service (avoids CORS in prod)
|
|
||||||
|
|
||||||
### 3.2 Update docker-compose.yml
|
|
||||||
|
|
||||||
Add frontend service:
|
|
||||||
```yaml
|
|
||||||
frontend:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
ports:
|
|
||||||
- "4200:80"
|
|
||||||
depends_on:
|
|
||||||
- api
|
|
||||||
```
|
|
||||||
|
|
||||||
### Phase 3 verification
|
|
||||||
|
|
||||||
| Check | How |
|
|
||||||
|-------|-----|
|
|
||||||
| Full stack up | `docker compose up --build` -- 3 containers, all healthy |
|
|
||||||
| Frontend accessible | Browse `http://localhost:4200` -- all pages load |
|
|
||||||
| All data pages dynamic | schedule, donate, events all render from DB |
|
|
||||||
| Data persists | `docker compose down && docker compose up` -- data still present |
|
|
||||||
|
|
||||||
## Files to Create (new)
|
|
||||||
|
|
||||||
| File | Purpose |
|
|
||||||
|------|---------|
|
|
||||||
| `.vscode/workspace.code-workspace` | Multi-root workspace (frontend + backend) |
|
|
||||||
| `.vscode/settings.json` | Shared workspace settings |
|
|
||||||
| `.vscode/tasks.json` | Common tasks (start backend, start frontend, docker up) |
|
|
||||||
| `.vscode/launch.json` | Debug configs for both projects |
|
|
||||||
| `backend/.vscode/settings.json` | Python-specific VS Code settings |
|
|
||||||
| `backend/.vscode/launch.json` | FastAPI debug config |
|
|
||||||
| `backend/requirements.txt` | Python dependencies |
|
|
||||||
| `backend/Dockerfile` | API container image |
|
|
||||||
| `backend/seed.py` | Initial station data |
|
|
||||||
| `backend/app/main.py` | FastAPI app entry point |
|
|
||||||
| `backend/app/config.py` | Env-based config |
|
|
||||||
| `backend/app/database.py` | async session engine |
|
|
||||||
| `backend/app/models.py` | SQLAlchemy ORM models |
|
|
||||||
| `backend/app/schemas.py` | Pydantic schemas |
|
|
||||||
| `backend/app/api/programs.py` | Programs CRUD endpoints |
|
|
||||||
| `backend/app/api/events.py` | Events CRUD endpoints |
|
|
||||||
| `backend/app/api/tiers.py` | Tiers CRUD endpoints |
|
|
||||||
| `src/app/interfaces/program.ts` | Program TS type |
|
|
||||||
| `src/app/interfaces/event.ts` | Event TS type |
|
|
||||||
| `src/app/interfaces/tier.ts` | Tier TS type |
|
|
||||||
| `src/app/services/program.service.ts` | Program HTTP service |
|
|
||||||
| `src/app/services/event.service.ts` | Event HTTP service |
|
|
||||||
| `src/app/services/tier.service.ts` | Tier HTTP service |
|
|
||||||
| `src/environments/environment.ts` | Dev/prod API URL |
|
|
||||||
| `docker-compose.yml` | 3-service compose |
|
|
||||||
| `Dockerfile` | Angular build + nginx serve (at repo root) |
|
|
||||||
| `nginx.conf` | nginx config with API proxy (at repo root) |
|
|
||||||
|
|
||||||
## Files to Modify
|
|
||||||
|
|
||||||
| File | Change |
|
|
||||||
|------|--------|
|
|
||||||
| `src/app/app.config.ts` | Add `provideHttpClient()` |
|
|
||||||
| `src/app/schedule/schedule.component.ts` | Remove readonly array, inject service |
|
|
||||||
| `src/app/events/events.component.ts` | Same pattern |
|
|
||||||
| `src/app/donate/donate.component.ts` | Same pattern |
|
|
||||||
| `docker-compose.yml` | Created in Phase 1, extended in Phase 3 |
|
|
||||||
|
|
||||||
## Not In Scope (for now)
|
|
||||||
|
|
||||||
- Admin panel (mentioned as Phase 5 in analysis — will be a future addition)
|
|
||||||
- Contact form implementation (left as alert per user decision)
|
|
||||||
- Streaming status endpoint (can add later via same pattern)
|
|
||||||
Reference in New Issue
Block a user