diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml new file mode 100644 index 0000000..f4032a8 --- /dev/null +++ b/.github/workflows/docker-image.yml @@ -0,0 +1,36 @@ +name: Docker Image CI + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build-and-push: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Log in to Docker Hub + if: github.event_name == 'push' + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build and push Docker image + uses: docker/build-push-action@v6 + with: + context: . + file: ./Dockerfile + target: builder + push: ${{ github.event_name == 'push' }} + tags: callofcode07/callofcode:latest + # change this if using this for production + build-args: | + API_BASE_URL=http://coc-api:3000 diff --git a/.gitignore b/.gitignore index 7e1e047..534fe13 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,10 @@ yarn-error.log* # local env files .env*.local .env + +# docker local env files (keep *.example tracked) +docker/.env.local.* +!docker/.env.local.*.example # vercel .vercel diff --git a/DOCKER.md b/DOCKER.md new file mode 100644 index 0000000..36e9eab --- /dev/null +++ b/DOCKER.md @@ -0,0 +1,207 @@ +# Docker — Local Development Guide + +This document explains how to spin up the full **Call of Code** local development environment using Docker Compose. + +The stack consists of two services running on a shared Docker network (`coc-local`): + +| Service | Image / Source | Port | +| ----------- | --------------------------------------- | ------ | +| `coc-api` | `callofcode07/coc-api:latest` (Docker Hub) | 3000 | +| `frontend` | Built locally from this repo (`Dockerfile`) | 3001 | + +--- + +## Prerequisites + +- [Docker Desktop](https://www.docker.com/products/docker-desktop/) ≥ 24 **or** Docker Engine + Docker Compose plugin ≥ 2.22 +- Git + +--- + +## Quick Start + +### 1. Configure environment variables + +The Compose setup reads env files from the `docker/` directory. Copy the example files and fill in your values: + +```bash +# COC API service +cp docker/.env.local.coc-api.example docker/.env.local.coc-api + +# Frontend service +cp docker/.env.local.frontend.example docker/.env.local.frontend +``` + +> **Never commit** the real `docker/.env.local.*` files — they are already listed in `.gitignore`. + +### 2. Start the services + +```bash +docker compose up --build +``` + +| Flag | Effect | +| ----------- | ------------------------------------------------- | +| `--build` | (Re)build the frontend image before starting | +| `--watch` | Enable hot-reload — see [Hot Reload](#hot-reload) | +| `-d` | Run in the background (detached mode) | + +The frontend will be available at **http://localhost:3001** once the `coc-api` health check passes. + +--- + +## Environment Variables + +### `docker/.env.local.coc-api` + +Consumed by the `coc-api` container. Credentials for the Supabase / Postgres backend. + +| Variable | Description | +| ------------------------- | --------------------------------------------------------- | +| `DATABASE_URL` | Postgres pooler connection string (used at runtime) | +| `DIRECT_URL` | Postgres direct connection string (used for migrations) | +| `SUPABASE_URL` | Your Supabase project URL | +| `SUPABASE_SERVICE_ROLE_KEY` | Supabase service-role JWT (keep this secret!) | +| `NODE_ENV` | Set to `development` for local use | + +Example: +```dotenv +DATABASE_URL=postgresql://postgres.:@aws-0-ap-south-1.pooler.supabase.com:5432/postgres +DIRECT_URL=postgresql://postgres.:@aws-0-ap-south-1.pooler.supabase.com:5432/postgres +SUPABASE_URL=https://.supabase.co +SUPABASE_SERVICE_ROLE_KEY= +NODE_ENV=development +``` + +--- + +### `docker/.env.local.frontend` + +Consumed by the `frontend` container at runtime. + +| Variable | Description | +| -------------- | --------------------------------------------------- | +| `API_BASE_URL` | URL the frontend uses to reach the API. Within the Docker network this is `http://coc-api:3000` | +| `GITHUB_TOKEN` | GitHub personal access token (optional, for contribution graphs) | + +Example: +```dotenv +API_BASE_URL=http://coc-api:3000 +GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx +``` + +--- + +## Hot Reload + +The Compose file uses Docker's `develop.watch` feature to sync source files into the running container **without a full rebuild**. + +Start with watch mode enabled: + +```bash +docker compose up --watch +``` + +Synced paths: + +| Local path | Container path | Action | +| -------------- | ----------------- | ------- | +| `./app` | `/app/app` | `sync` | +| `./components` | `/app/components` | `sync` | +| `./lib` | `/app/lib` | `sync` | +| `./public` | `/app/public` | `sync` | +| `package.json` / `package-lock.json` | — | `rebuild` (triggers a full image rebuild) | + +> **Note:** `sync` changes are reflected instantly. Dependency changes (`package.json`) trigger a full rebuild automatically. + +--- + +## Dockerfile Stages + +The multi-stage `Dockerfile` has three stages: + +| Stage | Base Image | Purpose | +| --------- | ----------------- | ------------------------------------------------ | +| `deps` | `node:20-alpine` | Install `node_modules` with `npm ci` | +| `builder` | `node:20-alpine` | Copy deps + source, run `npm run build`. **Used by Compose in dev** (keeps dev deps intact). | +| `runner` | `node:20-alpine` | Lean production image — only production artefacts | + +The Compose file targets the `builder` stage so that dev dependencies (like TypeScript types) remain available inside the container. + +--- + +## Useful Commands + +```bash +# Start all services (foreground) +docker compose up --build + +# Start with hot-reload +docker compose up --build --watch + +# Start in background +docker compose up -d --build + +# View logs for a specific service +docker compose logs -f frontend +docker compose logs -f coc-api + +# Stop all services +docker compose down + +# Stop and remove volumes +docker compose down -v + +# Rebuild only the frontend image +docker compose build frontend + +# Open a shell inside the frontend container +docker compose exec frontend sh + +# Check service health +docker compose ps +``` + +--- + +## Service Health Check + +The `coc-api` container exposes a health endpoint at `GET /health`. Docker polls it every **15 seconds** (3 retries, 5 s timeout, 15 s start period). The `frontend` service will not start until `coc-api` is reported **healthy**. + +```yaml +healthcheck: + test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"] + interval: 15s + timeout: 5s + retries: 3 + start_period: 15s +``` + +--- + +## Troubleshooting + +### Frontend can't reach the API + +- Confirm `docker/.env.local.frontend` has `API_BASE_URL=http://coc-api:3000`. +- Check that `coc-api` is healthy: `docker compose ps`. +- Inspect API logs: `docker compose logs coc-api`. + +### Port already in use + +Change the host-side port mapping in `docker-compose.yml`: +```yaml +ports: + - "3002:3001" # map host 3002 → container 3001 +``` + +### Hot-reload not working + +Ensure you started with `--watch`: `docker compose up --watch`. The feature requires Docker Compose ≥ 2.22. + +### Pulling a fresh copy of the API image + +```bash +docker compose pull coc-api +docker compose up --build +``` diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0bc7cf4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,52 @@ +# Stage 1 – deps: install node_modules with npm ci for reproducibility +FROM node:20-alpine AS deps +WORKDIR /app + +COPY package.json package-lock.json ./ +RUN npm ci + + +# Stage 2 – builder: compile the Next.js application +FROM node:20-alpine AS builder +WORKDIR /app + +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +# Build-time env vars (non-secret, baked into the bundle) +ARG API_BASE_URL +ENV API_BASE_URL=$API_BASE_URL + +RUN npm run build + +# Stage 3 – runner: lean production image +FROM node:20-alpine AS runner +WORKDIR /app + +ENV NODE_ENV=production + +# Create a non-root group and user to run the application securely +RUN addgroup --system --gid 1001 nodejs && \ + adduser --system --uid 1001 --ingroup nodejs nextjs + +# Next.js standalone output (enable in next.config if needed) +# COPY --from=builder /app/.next/standalone ./ +# COPY --from=builder /app/.next/static ./.next/static +# COPY --from=builder /app/public ./public + +# Standard (non-standalone) output +# node_modules sourced from deps stage; .next artefacts from builder +COPY --from=builder /app/public ./public +COPY --from=builder /app/.next ./.next +COPY --from=deps /app/node_modules ./node_modules +COPY --from=builder /app/package.json ./package.json + +# Transfer ownership of the working directory to the non-root user +RUN chown -R nextjs:nodejs /app + +# Drop privileges — never run production containers as root +USER nextjs + +EXPOSE 3001 + +CMD ["npm", "run", "start"] diff --git a/README.md b/README.md index d1f8bc7..1abd605 100644 --- a/README.md +++ b/README.md @@ -1,51 +1,102 @@ +# Call of Code — Public Website -# Call Of Code Public Website +The official website for the **Call of Code** programming club, built with **Next.js 15**, **React 18**, and **Tailwind CSS**. +--- -# Contributing Guide +## Tech Stack -Welcome to the official repository for the **Call of Code** programming club website. This project is built using **Next.js** and **React**. +| Layer | Technology | +| ---------- | --------------------------------------- | +| Framework | Next.js 15 (App Router) | +| Language | TypeScript | +| Styling | Tailwind CSS + Radix UI | +| Animations | Framer Motion | +| Runtime | Node.js 20 | +| API | `coc-api` — pre-built Docker Hub image | -## Getting Started +--- -To get a local copy up and running, follow these steps: +## Local Development + +### Prerequisites + +- Node.js ≥ 20 and npm +- Git + +### Setup 1. **Clone the repository:** ```bash - git clone https://github.com/yourusername/call-of-code.git - ``` -2. **Navigate to the project directory:** - ```bash + git clone https://github.com/callofcode07/call-of-code.git cd call-of-code ``` -3. **Install dependencies:** + +2. **Install dependencies:** ```bash npm install ``` -4. **Set API_BASE_URL env variable** -5. **Run the development server:** +3. **Configure environment variables:** + ```bash + cp .env.example .env + ``` + Open `.env` and fill in the required values: + + | Variable | Description | + | -------------- | -------------------------------------- | + | `API_BASE_URL` | Base URL for the COC API (e.g. `http://localhost:3000`) | + | `GITHUB_TOKEN` | GitHub personal access token (optional, for contribution data) | + +4. **Run the development server:** ```bash npm run dev ``` - + The app will be available at **http://localhost:3001**. + +--- + +## Docker — Local Development + +A fully containerised local environment (Next.js frontend + COC API) is available via Docker Compose. + +See **[DOCKER.md](./DOCKER.md)** for the complete guide, including: +- One-command startup with hot-reload (`--watch` mode) +- Environment variable configuration +- Health checks and service dependencies + +--- + +## Available Scripts + +| Command | Description | +| ----------------- | ---------------------------------------- | +| `npm run dev` | Start Next.js dev server on port 3001 | +| `npm run build` | Create a production build | +| `npm run start` | Serve the production build on port 3001 | +| `npm run lint` | Run ESLint | + +--- ## Contributing -We welcome contributions from the community. To ensure a smooth process, please follow these guidelines: +We welcome contributions from everyone! -1. **Fork the repository** and create your branch from `main`. -2. **Commit your changes** with clear and descriptive messages. -3. **Push to your branch** and create a pull request. -4. Ensure your code **adheres to the project's coding standards** and passes all tests. +1. **Fork** the repository and create a branch from `main`. +2. **Make your changes** with clear, descriptive commit messages. +3. **Push** your branch and open a **pull request**. +4. Ensure your code **passes linting** (`npm run lint`) and follows the project's coding standards. + +--- ## License -This project is licensed under the GNU GENERAL PUBLIC LICENSE v3. +This project is licensed under the [GNU General Public License v3.0](./LICENSE). + +--- ## Contact -For any questions or suggestions, please open an issue or contact the maintainers. -Can email on `callofcode07@gmail.com` +For questions or suggestions, open an issue or reach out at **callofcode07@gmail.com**. -Happy coding! +Happy coding! 🚀 diff --git a/app/api/home/route.ts b/app/api/home/route.ts new file mode 100644 index 0000000..d8c550b --- /dev/null +++ b/app/api/home/route.ts @@ -0,0 +1,97 @@ +// app/api/home/route.ts +import { NextResponse } from "next/server"; + +export const runtime = "edge"; + +export interface HomeAction { + key: string; + label: string; + url: string; + isVisible: boolean; +} + +export interface GalleryItem { + imageUrl: string; + caption: string; + altText: string; +} + +export interface HomeResponse { + success: boolean; + data: { + actions: HomeAction[]; + hero: { + imageUrl: string; + caption: string; + altText: string; + }; + gallery: GalleryItem[]; + }; +} + +export async function GET() { + const apiUrl = process.env.API_BASE_URL; + + if (!apiUrl) { + return NextResponse.json( + { success: false, message: "API base URL is not defined", data: { actions: [] } }, + { status: 500 } + ); + } + + try { + const res = await fetch(`${apiUrl}/api/v1/site-content`, { + cache: "no-store", + signal: AbortSignal.timeout(30000), // 30 second timeout + }); + + if (!res.ok) { + console.error(`Upstream API returned status ${res.status}`); + return NextResponse.json( + { success: false, message: `Upstream API error: ${res.status}`, data: { actions: [] } }, + { status: 502 } + ); + } + + const text = await res.text(); + let data; + try { + data = JSON.parse(text); + } catch (err: unknown) { + if (err instanceof Error) { + console.error("Invalid JSON from upstream API:", text, err.message); + } else { + console.error("Invalid JSON from upstream API:", text); + } + + return NextResponse.json( + { success: false, message: "Upstream API returned invalid JSON", data: { actions: [] } }, + { status: 500 } + ); + } + + if (!data || !data.success || !data.data || !Array.isArray(data.data.actions)) { + console.error("Unexpected API response structure", data); + return NextResponse.json( + { success: false, message: "Invalid response from upstream API", data: { actions: [] } }, + { status: 500 } + ); + } + + return NextResponse.json({ + success: true, + data: data.data, + }); + } catch (error: unknown) { + if (error instanceof Error) { + console.error("Error in /api/home route:", error.message); + } else { + console.error("Error in /api/home route:", error); + } + + return NextResponse.json( + { success: false, message: "Internal error", data: { actions: [] } }, + { status: 500 } + ); + } +} diff --git a/components/Home.tsx b/components/Home.tsx index b36c38e..8f9c972 100644 --- a/components/Home.tsx +++ b/components/Home.tsx @@ -8,6 +8,32 @@ import Ferris from "./ui/ferris-eyes"; import { Button } from 'pixel-retroui'; import localFont from "next/font/local"; +interface HomeAction { + key: string; + label: string; + url: string; + isVisible: boolean; +} + +interface GalleryItem { + imageUrl: string; + caption: string; + altText: string; +} + +interface HomeResponse { + success: boolean; + data: { + actions: HomeAction[]; + hero: { + imageUrl: string; + caption: string; + altText: string; + }; + gallery: GalleryItem[]; + }; +} + const pressStart2P = localFont({ src: "../app/fonts/PressStart2P-Regular.ttf", display: "swap", @@ -18,7 +44,24 @@ const pressStart2P = localFont({ export default function HeroSection() { const [color, setColor] = useState("#000000"); const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 }); - const [showButton ] = useState(false); + const [actions, setActions] = useState([]); + + useEffect(() => { + async function fetchHomeData() { + try { + const res = await fetch("/api/home"); + if (res.ok) { + const json: HomeResponse = await res.json(); + if (json.success && json.data && Array.isArray(json.data.actions)) { + setActions(json.data.actions); + } + } + } catch (err) { + console.error("Failed to fetch home page data:", err); + } + } + fetchHomeData(); + }, []); useEffect(() => { const updateColor = () => { @@ -68,7 +111,7 @@ export default function HeroSection() { shadow-[6px_6px_0px_rgba(0,0,0,1)] dark:shadow-[6px_6px_0px_rgba(255,255,255,1)] hover:shadow-[8px_8px_0px_rgba(0,0,0,1)] dark:hover:shadow-[8px_8px_0px_rgba(255,255,255,1)] active:translate-x-0.5 active:translate-y-0.5 - transition-all duration-200 mb-32 + transition-all duration-200 `.replace(/\s+/g, ' ').trim(); @@ -183,17 +226,24 @@ export default function HeroSection() { - - + {actions.filter(action => action.isVisible).length > 0 && ( +
+ {actions.filter(action => action.isVisible).map((action) => ( + + + + ))} +
+ )} +

<> CALL OF CODE </> diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..9388126 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,63 @@ + +services: + + coc-api: + image: callofcode07/coc-api:latest + pull_policy: missing # pull only if not already present locally + ports: + - "3000:3000" + env_file: + - docker/.env.local.coc-api + environment: + - NODE_ENV=development + healthcheck: + test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"] + interval: 15s + timeout: 5s + retries: 3 + start_period: 15s + restart: unless-stopped + + frontend: + build: + context: . + dockerfile: Dockerfile + target: builder # use the builder stage for dev (keeps dev deps) + args: + API_BASE_URL: ${API_BASE_URL:-http://coc-api:3000} + image: coc-frontend:dev + ports: + - "3001:3001" + env_file: + - docker/.env.local.frontend + environment: + - NODE_ENV=development + - API_BASE_URL=http://coc-api:3000 + depends_on: + coc-api: + condition: service_healthy + restart: unless-stopped + # Hot-reload via `docker compose up --watch` + develop: + watch: + - action: sync + path: ./app + target: /app/app + - action: sync + path: ./components + target: /app/components + - action: sync + path: ./lib + target: /app/lib + - action: sync + path: ./public + target: /app/public + - action: rebuild + path: package.json + - action: rebuild + path: package-lock.json + command: ["npm", "run", "dev"] + +networks: + default: + name: coc-local diff --git a/docker/.env.local.coc-api.example b/docker/.env.local.coc-api.example new file mode 100644 index 0000000..aa8b49d --- /dev/null +++ b/docker/.env.local.coc-api.example @@ -0,0 +1,15 @@ +# Copy this file to .env.local.coc-api and fill in the real values. + +# Supabase Postgres (connection pooler URL) +DATABASE_URL= + +# Direct connection — used for prisma migrate deploy on container start +DIRECT_URL= + +# Supabase project URL +SUPABASE_URL= + +# Supabase service-role key (keep this secret!) +SUPABASE_SERVICE_ROLE_KEY= + +NODE_ENV= diff --git a/docker/.env.local.frontend.example b/docker/.env.local.frontend.example new file mode 100644 index 0000000..754c0bf --- /dev/null +++ b/docker/.env.local.frontend.example @@ -0,0 +1,6 @@ + +# Points to the coc-api service on the shared Docker network +API_BASE_URL=http://coc-api:3000 + +# GitHub token for fetching contribution data (optional) +GITHUB_TOKEN=your_github_token_here diff --git a/package.json b/package.json index 4ed3182..d0835e7 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,9 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev", + "dev": "next dev -p 3001", "build": "next build", - "start": "next start", + "start": "next start -p 3001", "lint": "next lint", "pages:build": "npx @cloudflare/next-on-pages" },