# Nexora Esports Championship

Flask-based esports tournament website with a full admin panel.
Light, professional theme — everything on the public site is controlled from `/admin`.

---

## Quick Start (Windows)

1. Double-click **`setup.bat`** — creates the virtual environment, installs packages, builds the database.
2. Double-click **`start.bat`** — starts the server.
3. Open <http://127.0.0.1:5000>

| | |
|---|---|
| Website | http://127.0.0.1:5000 |
| Admin panel | http://127.0.0.1:5000/admin |
| Default login | `admin` / `admin123` |

> Change the admin login in `.env` (`ADMIN_USERNAME` / `ADMIN_PASSWORD`) before going live.

### Manual start (any OS)

```bash
pip install -r requirements.txt
cp .env.example .env
python app.py
```

---

## Admin Panel

| Page | What it controls |
|---|---|
| **Dashboard** | Registration / payment / sponsor / media counts, entries per game, recent signups |
| **Game Cards** | Add, edit, hide, or delete game cards — including a **registration open/close date** per game |
| **Covers & Gallery** | `Cover` → home page hero slider · `Gallery` → Event Moments slider + gallery page |
| **Leadership Quotes** | Sports CEO / Secretary / Director — name, designation, photo, quote, display order. Editable inline. |
| **Sponsors** | Company name, tier, commitment text, website, transparent PNG logo |
| **Registrations** | Search by name/email/phone/team, filter by game, status and date range, **export to Excel**, verify payments |
| **Messages** | Contact-form submissions with reply and delete |
| **Settings** | Countdown date, early-bird deadline, venue, payment account, About-page text, contact info, social links, open/close registration |

### Image tips
- **Cover / hero:** wide landscape, roughly 1920×1080.
- **Sponsor logos:** transparent PNG — they sit inside a white box, so transparency looks clean.
- **Leadership photos:** square images crop best into the circular avatar.
- Max upload size is 8 MB (`png`, `jpg`, `jpeg`, `webp`, `gif`).

---

## Email Flow

| When | Who gets it | Subject |
|---|---|---|
| Player submits the form | Player | *Registration Received — Pending Approval* |
| Player submits the form | Admin | *New Registration — Approval Needed* |
| Admin switches status to **Paid** | Player | *Payment Received — Slot Confirmed* (game, entry type, amount, venue) |
| Someone uses Get In Touch | Admin | *New Contact Message* |

The approval email only fires the first time a registration flips to Paid, so re-saving never spams the player.

---

## Registration Windows

Each game card has **Registration Opens** and **Registration Closes** dates:

- Blank open date → open immediately. Blank close date → no deadline.
- Cards show a live tag: **Open** (pulsing green), **Opens Soon** (amber), **Closed** (red).
- Closed games are greyed out, can't be clicked, and are removed from the form's dropdown — the server rejects them too, so nobody can register through a stale link.
- The global **Settings → Registration Open** switch overrides everything and closes the whole site.

---

## Excel Export

**Registrations → Export to Excel** downloads an `.xlsx` that respects whatever filters are active. Paid rows are shaded green, unpaid rows red, the header row is frozen and auto-filter is switched on.

If `openpyxl` isn't installed the export falls back to CSV automatically.

---

## Notes

- The home page shows a single countdown (Settings → Event Date) — the duplicate early-bird counter has been removed.
- Sponsor logos scroll continuously; the Memories strip advances one card at a time with a pause between moves. Both pause on hover.
- Gallery and Memories images open in a full-screen lightbox (arrow keys and Esc work).
- The countdown reads **Settings → Event Date**. Once it passes, the timer is replaced by an "event has started" message.
- The sponsors and leadership sections only appear on the site once you add at least one entry.
- Deleting a game no longer hides its past registrations — they stay listed as *Deleted game*.
- `nexora.db` (SQLite) holds all data. Back it up before making big changes.
- Emails are optional. Without SMTP settings, registrations still save and the console logs that the email was skipped.

## Project Structure

```
app.py                  Flask routes, database schema, migrations
nexora.db               SQLite database
static/css/style.css    Full stylesheet (light theme)
static/js/main.js       Nav, sliders, countdown, form logic
static/uploads/         Uploaded images
templates/              Public pages + admin panel + email templates
```

---

## Update — Responsive + QR Entry Control

### Admin panel
- On phones and tablets (≤1024px) the sidebar is now a **left slide-in drawer**.
  A fixed top bar holds the hamburger; tap it to slide the menu in from the left.
  Closing works via the X button, the dark backdrop, the Escape key, or tapping any link.
- On desktop (≥1025px) nothing changed — the sidebar stays fixed on the left.
- Every admin page (dashboard, games, media, leaders, sponsors, prize pool,
  registrations, QR scanner, messages, settings) was made responsive:
  stat cards reflow, filter bars stack, tables scroll horizontally instead of
  breaking the layout, and registration cards adapt down to a single column.

### QR scanner — approve / reject flow
Scanning no longer marks a ticket automatically. The new flow is:

1. Scan a QR (or paste a token manually).
2. A **full ticket overlay** opens with player details, game, team, payment
   status and a link to the payment proof.
3. The admin chooses:
   - **Approve Entry** → `entry_status = approved`, entry allowed.
   - **Reject Entry** → `entry_status = rejected`, entry denied.
   - **X (cross)** → close without deciding.

New routes:
- `POST /admin/qr-scanner/verify` — lookup only, never writes.
- `POST /admin/qr-scanner/decision` — saves `approved` / `rejected`.

New `registrations` columns (added automatically on first run):
`entry_status`, `entry_marked_at`, `entry_note`.

The decision shows as a badge on the registrations list and as an
**Entry Decision** column in the Excel and PDF exports.

### Sound and permissions
- The scanner page has an explicit permission strip with separate **Allow**
  buttons for the camera and for scan sound.
- Sound uses the Web Audio API: a beep on scan, a rising two-tone on approve,
  a low two-tone on reject, and an error tone plus vibration on an invalid QR.
- Browsers block audio until a real tap, which is why the Allow button exists.
  Pressing "Start Camera" also unlocks the sound.

> **Camera on mobile requires HTTPS.** Phone browsers block `getUserMedia` on
> plain HTTP. Serve the site over HTTPS (or use `localhost` / ngrok) for the
> camera to work. The manual token box always works as a fallback.

### Public site
- Game cards render **3 per row** on laptops, 2 on tablets and phones, and 1 on
  very small screens — the same structure everywhere instead of breaking on mobile.
- The homepage shows the **6 newest** game cards with a **View All** button on the
  right of the section heading; older cards remain available on the registration page.
- Cover / hero images now fit their frame properly (`cover` + centred focus,
  shifted slightly upward on portrait screens so faces are not cropped).
- Messages and Directors sections keep the desktop layout on mobile, with proper
  text wrapping instead of overflow.
- About, Gallery and the registration form are fully responsive. Form inputs use
  16px text so iOS does not zoom the page on focus, and tap targets are at least 42px.
- A global overflow guard prevents any page from scrolling sideways.

---

## Update 2 — Ticket security, Excel colouring, banner fixes

### QR codes are admin-only now
The QR on a ticket PDF used to encode a full URL to the scanner page. Anyone
who scanned a ticket with a phone camera got a tappable link. The QR now
encodes an opaque token only (`NXT:<token>`), which means nothing outside the
admin scanner. All three scanner endpoints sit behind the admin login, so a
scan from a normal phone camera does nothing at all.

Old tickets keep working — `normalize_qr_token()` still understands the
previous URL format.

### Verify by ticket number
The scanner's manual box now accepts any of these, so the camera is optional:
- the ticket number printed on the PDF (`NX-0007`)
- the ticket serial (`NX-TICKET-0007`)
- a scanned QR token

`find_registration_by_code()` tries token → serial → numeric reference.

### Excel export
- Selecting **Paid** or **Unpaid** from the dropdown now recolours the **entire
  row** live inside Excel — green for Paid, red for Unpaid.
- The conditional-format fills use `end_color`, which is the slot Excel actually
  reads for differential formats. Without it the rules were written to the file
  but never rendered.
- The column-width list was one short after the Entry Decision column was
  added, so the last column had no width. Fixed.

### Layout
- Game cards stay **two per row on phones**, matching the laptop layout instead
  of collapsing to a single column.
- **View All** sits on the left on mobile and links to the registration page.
- Registration page cards are ordered **newest first**, same as the homepage.
- The prize pool banner image was being cut in half: the rotator was absolutely
  positioned inside a grid column that had no height. It now has its own aspect
  ratio and shows the full picture on every screen size.
- Banner headings, prize amount, body text and the prize items were given
  consistent clamped font sizes so nothing looks oversized on mobile.
