feat: initial project setup with frontend mockup
- Docker Compose setup with nginx reverse proxy and frontend service - React + Vite + Tailwind CSS configuration - Complete mockup of all application views: - Authentication (login/register) - Events list and selection - Event chat with matchmaking - 1:1 private chat with WebRTC P2P video transfer mockup - Partner rating system - Collaboration history - Mock data for users, events, messages, matches, and ratings - All UI text and messages in English - Project documentation (CONTEXT.md, TODO.md, README.md, QUICKSTART.md)
This commit is contained in:
38
.gitignore
vendored
Normal file
38
.gitignore
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
frontend/node_modules/
|
||||||
|
backend/node_modules/
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Build output
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
frontend/dist/
|
||||||
|
backend/dist/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
92
QUICKSTART.md
Normal file
92
QUICKSTART.md
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
# Quick Start - spotlight.cam 🚀
|
||||||
|
|
||||||
|
## Uruchomienie (1 minuta!)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Uruchom Docker Compose
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# 2. Otwórz przeglądarkę
|
||||||
|
http://localhost:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
## Demo Flow (2 minuty)
|
||||||
|
|
||||||
|
### 1. Zaloguj się
|
||||||
|
- URL: http://localhost:8080/login
|
||||||
|
- Wpisz **dowolny** email i hasło (np. `test@test.com` / `test123`)
|
||||||
|
- Mock auth - natychmiast zaloguje
|
||||||
|
|
||||||
|
### 2. Wybierz event
|
||||||
|
- Kliknij na "Warsaw Dance Festival 2025"
|
||||||
|
- Przycisk "Dołącz do czatu"
|
||||||
|
|
||||||
|
### 3. Czat eventowy - Matchmaking
|
||||||
|
- Zobacz mockowane wiadomości
|
||||||
|
- Po prawej lista użytkowników
|
||||||
|
- Kliknij **➕** przy "sarah_swing"
|
||||||
|
- Zostaniesz przekierowany do czatu 1:1
|
||||||
|
|
||||||
|
### 4. 🔥 Główna funkcjonalność - Wysyłanie filmu WebRTC
|
||||||
|
- Kliknij **"Wyślij film (WebRTC)"**
|
||||||
|
- Wybierz dowolny plik wideo z dysku
|
||||||
|
- Kliknij **"Wyślij film (P2P)"**
|
||||||
|
- Zobacz:
|
||||||
|
- ✅ Status WebRTC: disconnected → connecting → connected
|
||||||
|
- ✅ Progress bar: 0% → 100%
|
||||||
|
- ✅ Info o szyfrrowaniu E2E (DTLS/SRTP)
|
||||||
|
- ✅ Wiadomość o przesłanym pliku w czacie
|
||||||
|
|
||||||
|
### 5. Fallback - Wysyłanie linku
|
||||||
|
- Kliknij **"Link"**
|
||||||
|
- Wklej URL (np. https://drive.google.com/file/d/abc123)
|
||||||
|
- Kliknij "Wyślij link"
|
||||||
|
|
||||||
|
### 6. Oceń partnera
|
||||||
|
- Kliknij **"Zakończ i oceń"**
|
||||||
|
- Wybierz 5 gwiazdek ⭐⭐⭐⭐⭐
|
||||||
|
- Dodaj komentarz: "Świetna współpraca!"
|
||||||
|
- Zaznacz "Chcę współpracować ponownie"
|
||||||
|
- Kliknij "Zapisz ocenę"
|
||||||
|
|
||||||
|
### 7. Historia
|
||||||
|
- URL: http://localhost:8080/history
|
||||||
|
- Zobacz wszystkie matche
|
||||||
|
- Zobacz otrzymane oceny
|
||||||
|
- Zobacz statystyki
|
||||||
|
|
||||||
|
## Co to jest?
|
||||||
|
|
||||||
|
**spotlight.cam** to mockup aplikacji PWA dla społeczności tanecznej. Główna funkcjonalność to **peer-to-peer przesyłanie filmów przez WebRTC**.
|
||||||
|
|
||||||
|
### ✅ Zrobione (Mockup)
|
||||||
|
- Autoryzacja (mock)
|
||||||
|
- Wybór eventów
|
||||||
|
- Czat eventowy (matchmaking)
|
||||||
|
- Czat 1:1
|
||||||
|
- **🔥 Mockup WebRTC P2P transfer** (symulacja transferu plików)
|
||||||
|
- System ocen
|
||||||
|
- Historia współprac
|
||||||
|
|
||||||
|
### 🔜 Do zrobienia
|
||||||
|
- Backend (Node.js + Express + PostgreSQL)
|
||||||
|
- WebSocket (Socket.IO) - real-time
|
||||||
|
- **Prawdziwy WebRTC P2P** (RTCDataChannel, chunking, progress monitoring)
|
||||||
|
- JWT autoryzacja
|
||||||
|
- Deployment
|
||||||
|
|
||||||
|
## Zatrzymanie
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pomoc
|
||||||
|
|
||||||
|
- Pełna dokumentacja: `README.md`
|
||||||
|
- Architektura: `docs/CONTEXT.md`
|
||||||
|
- Roadmap: `docs/TODO.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Mockup jest w pełni funkcjonalny!** WebRTC transfer jest symulowany, prawdziwa implementacja będzie w kolejnym etapie.
|
||||||
253
README.md
Normal file
253
README.md
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
# spotlight.cam 🎥
|
||||||
|
|
||||||
|
Aplikacja webowa (PWA) dla społeczności tanecznej umożliwiająca matchmaking, czatowanie i wymianę nagrań wideo bezpośrednio przez WebRTC (peer-to-peer).
|
||||||
|
|
||||||
|
## 🚀 Funkcjonalności (Mockup)
|
||||||
|
|
||||||
|
### ✅ Zaimplementowane (Frontend Mockup)
|
||||||
|
- **Autoryzacja** - logowanie i rejestracja (mock)
|
||||||
|
- **Wybór eventu** - przeglądanie i dołączanie do eventów tanecznych
|
||||||
|
- **Czat eventowy** - publiczny czat dla uczestników eventu z listą aktywnych użytkowników
|
||||||
|
- **Matchmaking** - łączenie się w pary bezpośrednio z czatu
|
||||||
|
- **Czat 1:1** - prywatny czat dla sparowanych użytkowników
|
||||||
|
- **📹 Transfer wideo (mockup WebRTC)** - symulacja przesyłania filmów P2P
|
||||||
|
- Wybór pliku z galerii urządzenia
|
||||||
|
- Symulacja połączenia WebRTC (connecting → connected)
|
||||||
|
- Progress bar transferu pliku
|
||||||
|
- Status połączenia (disconnected/connecting/connected/failed)
|
||||||
|
- Fallback: wysyłanie linków do filmów (Google Drive, Dropbox)
|
||||||
|
- **System ocen** - ocenianie partnera po współpracy (1-5 gwiazdek, komentarz)
|
||||||
|
- **Historia współprac** - lista poprzednich matchów i otrzymanych ocen
|
||||||
|
|
||||||
|
### 🔜 Do implementacji w kolejnych etapach
|
||||||
|
- **Backend API** (Node.js + Express + PostgreSQL)
|
||||||
|
- **WebSocket** (Socket.IO) - real-time chat i signaling
|
||||||
|
- **WebRTC P2P** - prawdziwy transfer plików przez RTCDataChannel
|
||||||
|
- **Autentykacja JWT** - prawdziwa autoryzacja
|
||||||
|
- **Baza danych** - PostgreSQL z pełnym schematem
|
||||||
|
|
||||||
|
## 🛠️ Stack Technologiczny
|
||||||
|
|
||||||
|
### Frontend (Current)
|
||||||
|
- **React 18** - framework UI
|
||||||
|
- **Vite** - build tool i dev server
|
||||||
|
- **Tailwind CSS** - stylowanie
|
||||||
|
- **React Router** - routing
|
||||||
|
- **Lucide React** - ikony
|
||||||
|
- **Context API** - zarządzanie stanem (auth)
|
||||||
|
|
||||||
|
### Infrastructure
|
||||||
|
- **Docker Compose** - orkiestracja kontenerów
|
||||||
|
- **Nginx** - reverse proxy i serving statycznych plików
|
||||||
|
- **Node.js 20 Alpine** - kontener dla frontendu
|
||||||
|
|
||||||
|
## 📁 Struktura projektu
|
||||||
|
|
||||||
|
```
|
||||||
|
spotlightcam/
|
||||||
|
├── docker-compose.yml # Konfiguracja Docker Compose
|
||||||
|
├── nginx/ # Konfiguracja Nginx
|
||||||
|
│ ├── nginx.conf # Główna konfiguracja
|
||||||
|
│ └── conf.d/
|
||||||
|
│ └── default.conf # Proxy do frontendu (i backendu w przyszłości)
|
||||||
|
├── frontend/ # React PWA
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── components/ # Komponenty React
|
||||||
|
│ │ │ ├── common/
|
||||||
|
│ │ │ ├── chat/
|
||||||
|
│ │ │ ├── video/
|
||||||
|
│ │ │ └── layout/ # Navbar, Layout
|
||||||
|
│ │ ├── pages/ # Strony aplikacji
|
||||||
|
│ │ │ ├── LoginPage.jsx
|
||||||
|
│ │ │ ├── RegisterPage.jsx
|
||||||
|
│ │ │ ├── EventsPage.jsx
|
||||||
|
│ │ │ ├── EventChatPage.jsx
|
||||||
|
│ │ │ ├── MatchChatPage.jsx # 🔥 Główna funkcjonalność - mockup WebRTC
|
||||||
|
│ │ │ ├── RatePartnerPage.jsx
|
||||||
|
│ │ │ └── HistoryPage.jsx
|
||||||
|
│ │ ├── contexts/ # Context API (AuthContext)
|
||||||
|
│ │ ├── hooks/ # Custom hooks
|
||||||
|
│ │ ├── utils/ # Utility functions
|
||||||
|
│ │ ├── services/ # API services (przyszłość)
|
||||||
|
│ │ └── mocks/ # Mock data
|
||||||
|
│ │ ├── events.js
|
||||||
|
│ │ ├── users.js
|
||||||
|
│ │ ├── messages.js
|
||||||
|
│ │ └── matches.js
|
||||||
|
│ ├── Dockerfile
|
||||||
|
│ └── package.json
|
||||||
|
└── docs/ # Dokumentacja
|
||||||
|
├── CONTEXT.md # Opis projektu
|
||||||
|
└── TODO.md # Lista zadań
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Uruchomienie projektu
|
||||||
|
|
||||||
|
### Wymagania
|
||||||
|
- Docker i Docker Compose
|
||||||
|
- (Opcjonalnie) Node.js 20+ dla developmentu bez Dockera
|
||||||
|
|
||||||
|
### Uruchomienie z Docker Compose
|
||||||
|
|
||||||
|
1. Sklonuj repozytorium:
|
||||||
|
```bash
|
||||||
|
git clone <repo-url>
|
||||||
|
cd spotlightcam
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Uruchom Docker Compose:
|
||||||
|
```bash
|
||||||
|
docker-compose up --build
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Otwórz przeglądarkę:
|
||||||
|
```
|
||||||
|
http://localhost:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
Frontend Vite dev server działa na porcie 5173 (wewnątrz kontenera), ale jest dostępny przez Nginx na porcie 8080.
|
||||||
|
|
||||||
|
### Zatrzymanie
|
||||||
|
```bash
|
||||||
|
docker-compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rebuild po zmianach
|
||||||
|
```bash
|
||||||
|
docker-compose up --build
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🧪 Testowanie aplikacji
|
||||||
|
|
||||||
|
### Flow testowy:
|
||||||
|
|
||||||
|
1. **Logowanie** (http://localhost:8080/login)
|
||||||
|
- Wpisz dowolny email i hasło (mock auth)
|
||||||
|
- Zostaniesz zalogowany jako użytkownik "john_dancer"
|
||||||
|
|
||||||
|
2. **Wybór eventu** (http://localhost:8080/events)
|
||||||
|
- Wybierz jeden z eventów (np. "Warsaw Dance Festival 2025")
|
||||||
|
- Kliknij "Dołącz do czatu"
|
||||||
|
|
||||||
|
3. **Czat eventowy**
|
||||||
|
- Zobacz mockowane wiadomości w czacie publicznym
|
||||||
|
- Po prawej stronie lista aktywnych użytkowników
|
||||||
|
- Kliknij ikonę "+" przy użytkowniku aby się z nim połączyć
|
||||||
|
- Po 1 sekundzie zostaniesz przekierowany do prywatnego czatu 1:1
|
||||||
|
|
||||||
|
4. **Czat 1:1 - Główna funkcjonalność!** 🔥
|
||||||
|
- Zobacz profil partnera na górze
|
||||||
|
- Status WebRTC połączenia (disconnected/connecting/connected)
|
||||||
|
- **Wysyłanie filmu przez WebRTC (mockup):**
|
||||||
|
- Kliknij "Wyślij film (WebRTC)"
|
||||||
|
- Wybierz plik wideo z dysku
|
||||||
|
- Kliknij "Wyślij film (P2P)"
|
||||||
|
- Zobacz symulację:
|
||||||
|
- Status WebRTC: connecting → connected
|
||||||
|
- Progress bar transferu (0% → 100%)
|
||||||
|
- Informacja o przesłanym pliku w czacie
|
||||||
|
- **Fallback - wysyłanie linku:**
|
||||||
|
- Kliknij "Link"
|
||||||
|
- Wklej URL do filmu (np. Google Drive)
|
||||||
|
- Link pojawi się w czacie
|
||||||
|
|
||||||
|
5. **Ocena partnera**
|
||||||
|
- Kliknij "Zakończ i oceń"
|
||||||
|
- Wybierz ocenę (1-5 gwiazdek)
|
||||||
|
- Dodaj komentarz (opcjonalnie)
|
||||||
|
- Zaznacz czy chcesz współpracować ponownie
|
||||||
|
- Kliknij "Zapisz ocenę"
|
||||||
|
|
||||||
|
6. **Historia współprac** (http://localhost:8080/history)
|
||||||
|
- Zobacz listę twoich poprzednich matchów
|
||||||
|
- Zobacz otrzymane oceny
|
||||||
|
- Zobacz statystyki
|
||||||
|
|
||||||
|
## 🎨 Główne widoki
|
||||||
|
|
||||||
|
### LoginPage & RegisterPage
|
||||||
|
- Formularz logowania/rejestracji
|
||||||
|
- Mock autoryzacja (dowolny email/hasło)
|
||||||
|
- Info box o demo mode
|
||||||
|
|
||||||
|
### EventsPage
|
||||||
|
- Karty eventów z worldsdc.com
|
||||||
|
- Informacje: lokalizacja, daty, liczba uczestników
|
||||||
|
- Przycisk "Dołącz do czatu"
|
||||||
|
|
||||||
|
### EventChatPage
|
||||||
|
- Czat publiczny dla eventu
|
||||||
|
- Lista aktywnych użytkowników (sidebar)
|
||||||
|
- Wysyłanie wiadomości
|
||||||
|
- Matchmaking przez ikonę "+"
|
||||||
|
|
||||||
|
### MatchChatPage ⭐ (Główna funkcjonalność)
|
||||||
|
- Profil partnera
|
||||||
|
- Status WebRTC połączenia (disconnected/connecting/connected/failed)
|
||||||
|
- Czat 1:1
|
||||||
|
- **Mockup transferu wideo WebRTC:**
|
||||||
|
- Wybór pliku z galerii
|
||||||
|
- Symulacja nawiązywania połączenia WebRTC
|
||||||
|
- Progress bar (chunking simulation)
|
||||||
|
- Info o szyfrrowaniu E2E (DTLS/SRTP)
|
||||||
|
- Fallback: wysyłanie linków do filmów
|
||||||
|
- Przycisk "Zakończ i oceń"
|
||||||
|
|
||||||
|
### RatePartnerPage
|
||||||
|
- Ocena partnera (1-5 gwiazdek)
|
||||||
|
- Pole komentarza
|
||||||
|
- Checkbox "Chcę współpracować ponownie"
|
||||||
|
|
||||||
|
### HistoryPage
|
||||||
|
- Lista matchów
|
||||||
|
- Otrzymane oceny
|
||||||
|
- Statystyki użytkownika
|
||||||
|
|
||||||
|
## 📝 Mock Data
|
||||||
|
|
||||||
|
Aplikacja używa mock data dla wszystkich funkcjonalności:
|
||||||
|
- **Users**: 5 użytkowników testowych
|
||||||
|
- **Events**: 4 eventy (Warsaw, Berlin, Prague, Krakow)
|
||||||
|
- **Messages**: Przykładowe wiadomości w czatach
|
||||||
|
- **Matches**: 3 przykładowe matche
|
||||||
|
- **Ratings**: 3 przykładowe oceny
|
||||||
|
|
||||||
|
Mock data znajduje się w `frontend/src/mocks/`.
|
||||||
|
|
||||||
|
## 🔐 Bezpieczeństwo (Mockup)
|
||||||
|
|
||||||
|
W obecnej wersji (mockup):
|
||||||
|
- ✅ Autoryzacja jest symulowana (localStorage)
|
||||||
|
- ✅ WebRTC status jest symulowany
|
||||||
|
- ⏳ Backend API będzie dodany w kolejnym etapie
|
||||||
|
- ⏳ Prawdziwe WebRTC P2P będzie zaimplementowane później
|
||||||
|
- ⏳ JWT autoryzacja będzie dodana później
|
||||||
|
|
||||||
|
## 🎯 Kolejne kroki
|
||||||
|
|
||||||
|
Zobacz `docs/TODO.md` dla pełnej listy zadań. Najważniejsze:
|
||||||
|
|
||||||
|
1. **Backend setup** - Node.js + Express + PostgreSQL
|
||||||
|
2. **WebSocket** - Socket.IO dla real-time communication
|
||||||
|
3. **WebRTC Signaling** - Serwer sygnalizacyjny (SDP/ICE exchange)
|
||||||
|
4. **WebRTC P2P Transfer** - Prawdziwy transfer plików przez RTCDataChannel
|
||||||
|
5. **Autentykacja** - JWT + bcrypt
|
||||||
|
6. **Testy** - Unit, integration, E2E
|
||||||
|
7. **Deployment** - Production setup
|
||||||
|
|
||||||
|
## 📖 Dokumentacja
|
||||||
|
|
||||||
|
- `docs/CONTEXT.md` - Szczegółowy opis projektu i architektury
|
||||||
|
- `docs/TODO.md` - Lista zadań do zrobienia
|
||||||
|
|
||||||
|
## 🤝 Contributing
|
||||||
|
|
||||||
|
Projekt jest w fazie MVP. Backend i prawdziwy WebRTC będą dodane w kolejnych etapach.
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
TBD
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Uwaga:** To jest wersja mockup frontendu. WebRTC transfer jest symulowany. Pełna funkcjonalność (backend + prawdziwy WebRTC) będzie dostępna w kolejnych wersjach.
|
||||||
29
docker-compose.yml
Normal file
29
docker-compose.yml
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
services:
|
||||||
|
nginx:
|
||||||
|
image: nginx:alpine
|
||||||
|
container_name: spotlightcam-nginx
|
||||||
|
ports:
|
||||||
|
- "8080:80"
|
||||||
|
volumes:
|
||||||
|
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||||
|
- ./nginx/conf.d:/etc/nginx/conf.d:ro
|
||||||
|
depends_on:
|
||||||
|
- frontend
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
context: ./frontend
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: spotlightcam-frontend
|
||||||
|
expose:
|
||||||
|
- "5173"
|
||||||
|
volumes:
|
||||||
|
- ./frontend:/app
|
||||||
|
- /app/node_modules
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=development
|
||||||
|
- VITE_HOST=0.0.0.0
|
||||||
|
stdin_open: true
|
||||||
|
tty: true
|
||||||
|
command: npm run dev
|
||||||
205
docs/CONTEXT.md
Normal file
205
docs/CONTEXT.md
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
|
||||||
|
# spotlight.cam
|
||||||
|
|
||||||
|
Aplikacja umożliwiająca użytkownikom łączenie się w pary, czatowanie, **przesyłanie filmów bezpośrednio peer-to-peer przez WebRTC** i ocenianie współpracy – bez przechowywania danych na serwerze.
|
||||||
|
|
||||||
|
Stworzyć aplikację umożliwiającą peer-to-peer przesyłanie nagrań wideo między użytkownikami, nawet gdy znajdują się na różnych platformach (Android/iOS), z szyfrowaniem end-to-end, minimalnym udziałem backendu i opcjonalnym czatem tekstowym.
|
||||||
|
|
||||||
|
✅ Projekt zakłada pełne wsparcie dla Android/iOS (przez przeglądarkę), bez hostowania plików, z backendem i frontendem działającym w ramach Docker Compose.
|
||||||
|
|
||||||
|
🧱 ETAPY WDROŻENIA
|
||||||
|
1. 🔍 Analiza technologii i wymagań
|
||||||
|
|
||||||
|
Zidentyfikuj ograniczenia platform: Android, iOS (szczególnie iOS WebView vs Safari).
|
||||||
|
|
||||||
|
Sprawdź kompatybilność WebRTC na poziomie przeglądarki (getUserMedia, RTCPeerConnection, RTCDataChannel).
|
||||||
|
|
||||||
|
Określ typy danych do przesyłania: pliki wideo (MP4), wiadomości tekstowe, metadane.
|
||||||
|
|
||||||
|
Zdecyduj, czy to aplikacja webowa (PWA) czy natywna (Flutter/React Native).
|
||||||
|
|
||||||
|
Oszacuj maksymalny rozmiar plików i czas transferu.
|
||||||
|
|
||||||
|
2. 🧠 Projekt architektury systemu
|
||||||
|
🔗 Połączenie WebRTC
|
||||||
|
|
||||||
|
Użyj RTCPeerConnection + RTCDataChannel do przesyłania wideo.
|
||||||
|
|
||||||
|
Zaimplementuj STUN i opcjonalnie TURN (np. Google STUN, własny TURN przy złych NAT-ach).
|
||||||
|
|
||||||
|
☁️ Serwer sygnalizacyjny (Signaling)
|
||||||
|
|
||||||
|
Stwórz prosty backend (np. Node.js + Express + WebSocket) do wymiany SDP/ICE.
|
||||||
|
|
||||||
|
Alternatywnie: pozwól użytkownikom przekazywać dane ręcznie (przez QR/link/komunikator).
|
||||||
|
|
||||||
|
📤 Przesyłanie danych
|
||||||
|
|
||||||
|
Skorzystaj z RTCDataChannel do przesyłania plików binarnych (Blob/FileReader).
|
||||||
|
|
||||||
|
Podziel pliki na fragmenty (chunking), monitoruj postęp.
|
||||||
|
|
||||||
|
3. 🔐 Bezpieczeństwo i prywatność
|
||||||
|
🔒 Szyfrowanie
|
||||||
|
|
||||||
|
Potwierdź, że WebRTC już szyfruje (DTLS/SRTP).
|
||||||
|
|
||||||
|
Zaszyfruj czat tekstowy: np. z użyciem libsodium, Signal Protocol lub WebCrypto.
|
||||||
|
|
||||||
|
Nie przechowuj żadnych plików na serwerze — tylko wymiana sygnalizacyjna.
|
||||||
|
|
||||||
|
🔄 Wymiana danych sygnalizacyjnych
|
||||||
|
|
||||||
|
Dodaj 2 tryby:
|
||||||
|
|
||||||
|
QR code + ręczne skanowanie
|
||||||
|
|
||||||
|
Link generowany przez jedną stronę i wklejany przez drugą
|
||||||
|
|
||||||
|
Kodowanie
|
||||||
|
|
||||||
|
Zserializuj offer (SDP) do JSON → Base64 → QR/link.
|
||||||
|
|
||||||
|
Po stronie odbiorcy: parsuj i odpowiadaj answer przez podobny kanał.
|
||||||
|
|
||||||
|
6. 🧪 UX i potwierdzenia
|
||||||
|
|
||||||
|
Pokaż komunikaty typu:
|
||||||
|
|
||||||
|
„Czekam na połączenie…”
|
||||||
|
|
||||||
|
„Połączenie nawiązane z użytkownikiem”
|
||||||
|
|
||||||
|
„Trwa przesyłanie: 87%”
|
||||||
|
|
||||||
|
Po zakończeniu przesyłania: „Plik otrzymany”, przycisk „Zapisz”.
|
||||||
|
|
||||||
|
## 🧠 Główne założenia
|
||||||
|
|
||||||
|
- 🔒 Prywatność: **przesyłanie filmów bezpośrednio peer-to-peer przez WebRTC** – żadnych plików wideo na serwerze. Opcjonalnie możliwość wymiany linków (np. Google Drive).
|
||||||
|
- 💬 Czat + matchmaking: użytkownicy rejestrują się, wybierają event, trafiają na czat eventowy, łączą się w pary.
|
||||||
|
- 🤝 1:1: po potwierdzeniu – para znika z czatu publicznego i przechodzi do prywatnego.
|
||||||
|
- 📦 Docker Compose: całość uruchamiana w kontenerach.
|
||||||
|
- 📱 Frontend: PWA – działa mobilnie i na desktopie.
|
||||||
|
|
||||||
|
## 🧱 Architektura systemu
|
||||||
|
|
||||||
|
Web client (PWA) ↔ Backend (Node.js + WebSocket + REST API) ↔ PostgreSQL
|
||||||
|
|
||||||
|
## 🐳 Docker Compose – komponenty
|
||||||
|
|
||||||
|
Zestaw trzech kontenerów:
|
||||||
|
- `frontend`: PWA (React/Vite/Tailwind)
|
||||||
|
- `backend`: Node.js/Express/Socket.IO
|
||||||
|
- `db`: PostgreSQL 15
|
||||||
|
|
||||||
|
## 🗃️ Modele bazy danych
|
||||||
|
|
||||||
|
- `users`: identyfikacja, event, dostępność
|
||||||
|
- `events`: lista eventów z worldsdc.com
|
||||||
|
- `chat_rooms`: publiczne i prywatne pokoje
|
||||||
|
- `messages`: wiadomości tekstowe/linki
|
||||||
|
- `matches`: relacja między dwoma użytkownikami
|
||||||
|
- `ratings`: oceny po wymianie nagraniami
|
||||||
|
|
||||||
|
## 🔁 Flow użytkownika
|
||||||
|
|
||||||
|
1. Rejestracja użytkownika
|
||||||
|
2. Wybór eventu (lista z worldsdc.com) na którym użytkownik jest.
|
||||||
|
3. Czat ogólny dla eventu – matchmaking.
|
||||||
|
4. Potwierdzenie: przejście do prywatnego czatu 1:1.
|
||||||
|
5. **Wybór pliku wideo z galerii urządzenia** (nagranie odbywa się poza aplikacją).
|
||||||
|
6. **Przesłanie filmu bezpośrednio przez WebRTC** (peer-to-peer, bez serwera). Opcjonalnie: wymiana linków (np. Google Drive).
|
||||||
|
7. Po wymianie nagraniami – ocena partnera (1–5, komentarz, chęć ponownej współpracy).
|
||||||
|
|
||||||
|
## 💬 Frontend (PWA)
|
||||||
|
|
||||||
|
Zbudowany np. w:
|
||||||
|
- React + Vite + Tailwind
|
||||||
|
- lub SvelteKit / SolidStart
|
||||||
|
|
||||||
|
Widoki:
|
||||||
|
- Logowanie / Rejestracja
|
||||||
|
- Wybór eventu
|
||||||
|
- Czat eventowy
|
||||||
|
- Profil partnera
|
||||||
|
- Czat 1:1
|
||||||
|
- Ocena partnera
|
||||||
|
- Historia współprac
|
||||||
|
|
||||||
|
## 🔐 Bezpieczeństwo
|
||||||
|
|
||||||
|
- Hasła haszowane (bcrypt).
|
||||||
|
- **WebRTC zapewnia natywne szyfrowanie** (DTLS/SRTP) dla transferu P2P.
|
||||||
|
- **Opcjonalne dodatkowe szyfrowanie czatu** tekstowego (WebCrypto/libsodium).
|
||||||
|
- Brak przechowywania nagrań na serwerze – tylko sygnalizacja WebRTC.
|
||||||
|
- Możliwość zgłaszania użytkownika.
|
||||||
|
|
||||||
|
## 📹 WebRTC P2P Transfer Filmów (główna funkcjonalność)
|
||||||
|
|
||||||
|
### Komponenty:
|
||||||
|
- **RTCPeerConnection**: nawiązanie połączenia P2P między użytkownikami
|
||||||
|
- **RTCDataChannel**: kanał do transferu danych binarnych (pliki wideo)
|
||||||
|
- **Chunking**: podział dużych plików wideo na fragmenty (np. 16KB chunks)
|
||||||
|
- **Progress monitoring**: śledzenie postępu wysyłania/odbierania (pasek postępu)
|
||||||
|
- **STUN/TURN**: serwery do przejścia przez NAT/firewall
|
||||||
|
|
||||||
|
### Flow transferu:
|
||||||
|
1. Użytkownik wybiera plik wideo z galerii urządzenia (`<input type="file">`)
|
||||||
|
2. Nawiązanie połączenia WebRTC między parą użytkowników (przez serwer sygnalizacyjny WebSocket)
|
||||||
|
3. Otwarcie RTCDataChannel
|
||||||
|
4. Wysłanie metadanych: nazwa pliku, rozmiar, typ MIME
|
||||||
|
5. Podział pliku na chunki i przesłanie przez DataChannel
|
||||||
|
6. Odbieranie chunków i składanie w całość (Blob)
|
||||||
|
7. Po zakończeniu: zapis pliku w pamięci urządzenia odbiorcy
|
||||||
|
|
||||||
|
### Fallback:
|
||||||
|
- Jeśli WebRTC nie zadziała (problemy z NAT, firewall), użytkownik może wymienić się linkami do filmów (Google Drive, Dropbox, itp.)
|
||||||
|
|
||||||
|
## ➕ Opcjonalne rozszerzenia
|
||||||
|
|
||||||
|
- System oznaczeń zaufania (badge).
|
||||||
|
- Blokowanie użytkowników.
|
||||||
|
- Publiczne profile z opiniami.
|
||||||
|
- Powiadomienia push.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Zasady rozwoju projektu (Development Guidelines)
|
||||||
|
|
||||||
|
### Język w kodzie
|
||||||
|
- **Wszystkie stringi, komunikaty, UI text, komentarze w kodzie**: **angielski**
|
||||||
|
- **Nazwy zmiennych, funkcji, klas**: **angielski**
|
||||||
|
- **Dokumentacja techniczna w kodzie (JSDoc, docstrings)**: **angielski**
|
||||||
|
|
||||||
|
### Komunikacja
|
||||||
|
- **Komunikacja z zespołem/developerem**: **polski**
|
||||||
|
- **Dokumentacja projektowa (CONTEXT.md, README.md)**: **polski** (może być mieszana z angielskim dla części technicznych)
|
||||||
|
|
||||||
|
### Git commits
|
||||||
|
- **Commit messages**: standardowy format bez wzmianek o AI/automatycznym generowaniu kodu
|
||||||
|
- Przykład dobrego commita: `feat: add WebRTC P2P video transfer mockup`
|
||||||
|
- Przykład złego commita: ~~`feat: add video transfer (generated by AI)`~~
|
||||||
|
- Commituj zmiany logicznie (feature by feature, bugfix by bugfix)
|
||||||
|
|
||||||
|
### Przykład
|
||||||
|
```jsx
|
||||||
|
// ✅ Poprawnie - kod po angielsku
|
||||||
|
const sendMessage = (message) => {
|
||||||
|
if (!message.trim()) {
|
||||||
|
alert('Please enter a message');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Send message via WebSocket
|
||||||
|
socket.emit('message', message);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ❌ Niepoprawnie - kod po polsku
|
||||||
|
const wyslijWiadomosc = (wiadomosc) => {
|
||||||
|
if (!wiadomosc.trim()) {
|
||||||
|
alert('Proszę wpisać wiadomość');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
socket.emit('wiadomosc', wiadomosc);
|
||||||
|
};
|
||||||
|
```
|
||||||
352
docs/TODO.md
Normal file
352
docs/TODO.md
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
# TODO - spotlight.cam
|
||||||
|
|
||||||
|
Lista zadań do realizacji projektu spotlight.cam - aplikacji do matchmaking i wymiany nagrań wideo na eventach tanecznych.
|
||||||
|
|
||||||
|
## 📋 Status realizacji
|
||||||
|
- ⏳ Do zrobienia
|
||||||
|
- 🔄 W trakcie
|
||||||
|
- ✅ Zrobione
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐳 1. Setup projektu i infrastruktura
|
||||||
|
|
||||||
|
### Docker Compose
|
||||||
|
- [ ] ⏳ Utworzenie `docker-compose.yml` z trzema serwisami (frontend, backend, db)
|
||||||
|
- [ ] ⏳ Konfiguracja kontenera PostgreSQL 15
|
||||||
|
- [ ] ⏳ Konfiguracja kontenera backend (Node.js)
|
||||||
|
- [ ] ⏳ Konfiguracja kontenera frontend (React/Vite)
|
||||||
|
- [ ] ⏳ Konfiguracja volumes dla persystencji danych
|
||||||
|
- [ ] ⏳ Konfiguracja sieci między kontenerami
|
||||||
|
|
||||||
|
### Struktura projektu
|
||||||
|
- [ ] ⏳ Inicjalizacja projektu backend (Node.js + Express)
|
||||||
|
- [ ] ⏳ Inicjalizacja projektu frontend (React + Vite + Tailwind)
|
||||||
|
- [ ] ⏳ Konfiguracja ESLint i Prettier
|
||||||
|
- [ ] ⏳ Konfiguracja TypeScript (opcjonalnie)
|
||||||
|
- [ ] ⏳ Utworzenie `.env` i `.env.example`
|
||||||
|
- [ ] ⏳ Utworzenie `.gitignore`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🗄️ 2. Baza danych PostgreSQL
|
||||||
|
|
||||||
|
### Schema i migracje
|
||||||
|
- [ ] ⏳ Setup narzędzia do migracji (np. node-pg-migrate, Knex, Prisma)
|
||||||
|
- [ ] ⏳ Utworzenie tabeli `users`
|
||||||
|
- id, username, email, password_hash, created_at, updated_at
|
||||||
|
- [ ] ⏳ Utworzenie tabeli `events`
|
||||||
|
- id, name, location, start_date, end_date, worldsdc_id
|
||||||
|
- [ ] ⏳ Utworzenie tabeli `chat_rooms`
|
||||||
|
- id, event_id, type (event/private), created_at
|
||||||
|
- [ ] ⏳ Utworzenie tabeli `messages`
|
||||||
|
- id, room_id, user_id, content, type (text/link), created_at
|
||||||
|
- [ ] ⏳ Utworzenie tabeli `matches`
|
||||||
|
- id, user1_id, user2_id, event_id, status, room_id, created_at
|
||||||
|
- [ ] ⏳ Utworzenie tabeli `ratings`
|
||||||
|
- id, match_id, rater_id, rated_id, score (1-5), comment, would_collaborate_again
|
||||||
|
- [ ] ⏳ Utworzenie indeksów dla optymalizacji zapytań
|
||||||
|
- [ ] ⏳ Seed danych testowych (events z worldsdc.com)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 3. Backend - REST API
|
||||||
|
|
||||||
|
### Autentykacja i autoryzacja
|
||||||
|
- [ ] ⏳ Endpoint: `POST /api/auth/register` - rejestracja użytkownika
|
||||||
|
- [ ] ⏳ Endpoint: `POST /api/auth/login` - logowanie użytkownika
|
||||||
|
- [ ] ⏳ Endpoint: `POST /api/auth/logout` - wylogowanie
|
||||||
|
- [ ] ⏳ Implementacja bcrypt do hashowania haseł
|
||||||
|
- [ ] ⏳ Implementacja JWT/session dla autoryzacji
|
||||||
|
- [ ] ⏳ Middleware do weryfikacji tokena
|
||||||
|
|
||||||
|
### Users API
|
||||||
|
- [ ] ⏳ Endpoint: `GET /api/users/me` - pobranie profilu użytkownika
|
||||||
|
- [ ] ⏳ Endpoint: `PATCH /api/users/me` - aktualizacja profilu
|
||||||
|
- [ ] ⏳ Endpoint: `GET /api/users/:id` - pobranie profilu innego użytkownika
|
||||||
|
|
||||||
|
### Events API
|
||||||
|
- [ ] ⏳ Endpoint: `GET /api/events` - lista wszystkich eventów
|
||||||
|
- [ ] ⏳ Endpoint: `GET /api/events/:id` - szczegóły eventu
|
||||||
|
- [ ] ⏳ Endpoint: `POST /api/events/:id/join` - dołączenie do eventu
|
||||||
|
- [ ] ⏳ Integracja z worldsdc.com API (jeśli dostępne)
|
||||||
|
|
||||||
|
### Matches API
|
||||||
|
- [ ] ⏳ Endpoint: `GET /api/matches` - historia matchów użytkownika
|
||||||
|
- [ ] ⏳ Endpoint: `POST /api/matches` - utworzenie matcha
|
||||||
|
- [ ] ⏳ Endpoint: `PATCH /api/matches/:id` - aktualizacja statusu matcha
|
||||||
|
|
||||||
|
### Ratings API
|
||||||
|
- [ ] ⏳ Endpoint: `POST /api/ratings` - dodanie oceny partnera
|
||||||
|
- [ ] ⏳ Endpoint: `GET /api/ratings/user/:id` - oceny danego użytkownika
|
||||||
|
- [ ] ⏳ Endpoint: `GET /api/ratings/stats/:id` - statystyki użytkownika
|
||||||
|
|
||||||
|
### Reports API
|
||||||
|
- [ ] ⏳ Endpoint: `POST /api/reports` - zgłoszenie użytkownika
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💬 4. Backend - WebSocket (Socket.IO)
|
||||||
|
|
||||||
|
### Czat eventowy (publiczny)
|
||||||
|
- [ ] ⏳ Setup Socket.IO na backendzie
|
||||||
|
- [ ] ⏳ Obsługa połączenia/rozłączenia użytkownika
|
||||||
|
- [ ] ⏳ Dołączenie użytkownika do pokoju eventowego
|
||||||
|
- [ ] ⏳ Wysyłanie wiadomości tekstowych do pokoju
|
||||||
|
- [ ] ⏳ Broadcast wiadomości do wszystkich w pokoju
|
||||||
|
- [ ] ⏳ Lista aktywnych użytkowników w pokoju
|
||||||
|
|
||||||
|
### Czat 1:1 (prywatny)
|
||||||
|
- [ ] ⏳ Utworzenie prywatnego pokoju dla pary
|
||||||
|
- [ ] ⏳ Wysyłanie wiadomości prywatnych
|
||||||
|
- [ ] ⏳ Powiadomienia o nowych wiadomościach
|
||||||
|
|
||||||
|
### WebRTC Signaling
|
||||||
|
- [ ] ⏳ Obsługa wymiany SDP offer/answer
|
||||||
|
- [ ] ⏳ Obsługa wymiany ICE candidates
|
||||||
|
- [ ] ⏳ Sygnalizacja statusu połączenia WebRTC
|
||||||
|
- [ ] ⏳ Error handling dla failed connections
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎥 5. WebRTC - Peer-to-Peer Transfer Filmów (GŁÓWNA FUNKCJONALNOŚĆ)
|
||||||
|
|
||||||
|
### Setup WebRTC
|
||||||
|
- [ ] ⏳ Konfiguracja STUN servers (Google STUN)
|
||||||
|
- [ ] ⏳ Konfiguracja TURN server (opcjonalnie, dla trudnych NAT)
|
||||||
|
- [ ] ⏳ Utworzenie RTCPeerConnection
|
||||||
|
- [ ] ⏳ Utworzenie RTCDataChannel dla transferu plików
|
||||||
|
|
||||||
|
### Nawiązanie połączenia
|
||||||
|
- [ ] ⏳ Inicjacja połączenia przez wysyłającego (createOffer)
|
||||||
|
- [ ] ⏳ Odpowiedź odbierającego (createAnswer)
|
||||||
|
- [ ] ⏳ Wymiana ICE candidates
|
||||||
|
- [ ] ⏳ Monitoring statusu połączenia (connecting, connected, failed)
|
||||||
|
|
||||||
|
### Transfer plików
|
||||||
|
- [ ] ⏳ Wybór pliku z galerii (`<input type="file" accept="video/*">`)
|
||||||
|
- [ ] ⏳ Walidacja pliku (typ, rozmiar max)
|
||||||
|
- [ ] ⏳ Wysłanie metadanych (nazwa, rozmiar, MIME type)
|
||||||
|
- [ ] ⏳ Implementacja chunkingu (podział na fragmenty 16KB)
|
||||||
|
- [ ] ⏳ Wysyłanie chunków przez DataChannel
|
||||||
|
- [ ] ⏳ Odbieranie chunków i składanie w Blob
|
||||||
|
- [ ] ⏳ Progress monitoring (pasek postępu % dla wysyłającego i odbierającego)
|
||||||
|
- [ ] ⏳ Obsługa błędów transferu
|
||||||
|
- [ ] ⏳ Możliwość anulowania transferu
|
||||||
|
- [ ] ⏳ Zapis pliku do pamięci urządzenia (download)
|
||||||
|
|
||||||
|
### Fallback - wymiana linków
|
||||||
|
- [ ] ⏳ UI do wklejenia linku do filmu (Google Drive, Dropbox, itp.)
|
||||||
|
- [ ] ⏳ Walidacja URL
|
||||||
|
- [ ] ⏳ Wysłanie linku przez czat
|
||||||
|
|
||||||
|
### Optymalizacje
|
||||||
|
- [ ] ⏳ Dostosowanie rozmiaru chunka do bandwidth
|
||||||
|
- [ ] ⏳ Buffer management (kontrola przepełnienia bufora)
|
||||||
|
- [ ] ⏳ Reconnection logic przy utracie połączenia
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 6. Frontend - PWA (React + Vite + Tailwind)
|
||||||
|
|
||||||
|
### Setup PWA
|
||||||
|
- [ ] ⏳ Konfiguracja Vite PWA plugin
|
||||||
|
- [ ] ⏳ Utworzenie `manifest.json`
|
||||||
|
- [ ] ⏳ Utworzenie service worker
|
||||||
|
- [ ] ⏳ Ikony aplikacji (różne rozmiary)
|
||||||
|
- [ ] ⏳ Splash screen
|
||||||
|
|
||||||
|
### Routing
|
||||||
|
- [ ] ⏳ Setup React Router
|
||||||
|
- [ ] ⏳ Ochrona tras (require authentication)
|
||||||
|
|
||||||
|
### Widoki/Komponenty
|
||||||
|
- [ ] ⏳ **Logowanie** (`/login`)
|
||||||
|
- Formularz email + hasło
|
||||||
|
- Link do rejestracji
|
||||||
|
- [ ] ⏳ **Rejestracja** (`/register`)
|
||||||
|
- Formularz username, email, hasło
|
||||||
|
- Walidacja formularza
|
||||||
|
- [ ] ⏳ **Wybór eventu** (`/events`)
|
||||||
|
- Lista eventów z worldsdc.com
|
||||||
|
- Wyszukiwanie/filtrowanie
|
||||||
|
- Przycisk "Dołącz do eventu"
|
||||||
|
- [ ] ⏳ **Czat eventowy** (`/events/:id/chat`)
|
||||||
|
- Lista wiadomości (scroll do najnowszej)
|
||||||
|
- Pole wysyłania wiadomości
|
||||||
|
- Lista aktywnych użytkowników
|
||||||
|
- Przycisk "Połącz z użytkownikiem X"
|
||||||
|
- [ ] ⏳ **Czat 1:1** (`/matches/:id/chat`)
|
||||||
|
- Profil partnera (zdjęcie, nick, statystyki)
|
||||||
|
- Historia czatu
|
||||||
|
- Pole wysyłania wiadomości
|
||||||
|
- **Przycisk "Wyślij film"** (główna funkcja)
|
||||||
|
- Wyświetlanie statusu połączenia WebRTC
|
||||||
|
- Progress bar transferu pliku
|
||||||
|
- Podgląd przesłanego filmu
|
||||||
|
- Opcja wysłania linku (fallback)
|
||||||
|
- [ ] ⏳ **Ocena partnera** (`/matches/:id/rate`)
|
||||||
|
- Formularz oceny (1-5 gwiazdek)
|
||||||
|
- Pole komentarza
|
||||||
|
- Checkbox "Chcę współpracować ponownie"
|
||||||
|
- [ ] ⏳ **Historia współprac** (`/history`)
|
||||||
|
- Lista poprzednich matchów
|
||||||
|
- Filtrowanie po evencie
|
||||||
|
- Oceny otrzymane/wystawione
|
||||||
|
- [ ] ⏳ **Profil użytkownika** (`/profile`)
|
||||||
|
- Edycja danych
|
||||||
|
- Statystyki (liczba matchów, średnia ocena)
|
||||||
|
- Historia eventów
|
||||||
|
|
||||||
|
### Komponenty reużywalne
|
||||||
|
- [ ] ⏳ `<Navbar>` - nawigacja
|
||||||
|
- [ ] ⏳ `<MessageBubble>` - wiadomość w czacie
|
||||||
|
- [ ] ⏳ `<UserCard>` - karta użytkownika
|
||||||
|
- [ ] ⏳ `<EventCard>` - karta eventu
|
||||||
|
- [ ] ⏳ `<RatingStars>` - gwiazdki oceny
|
||||||
|
- [ ] ⏳ `<VideoUploader>` - komponent do wyboru i wysyłania filmu
|
||||||
|
- [ ] ⏳ `<ProgressBar>` - pasek postępu transferu
|
||||||
|
- [ ] ⏳ `<WebRTCStatus>` - status połączenia WebRTC
|
||||||
|
|
||||||
|
### Stylowanie (Tailwind)
|
||||||
|
- [ ] ⏳ Konfiguracja motywu kolorystycznego
|
||||||
|
- [ ] ⏳ Responsive design (mobile-first)
|
||||||
|
- [ ] ⏳ Dark mode (opcjonalnie)
|
||||||
|
|
||||||
|
### State Management
|
||||||
|
- [ ] ⏳ Setup state management (Context API / Zustand / Redux)
|
||||||
|
- [ ] ⏳ Auth state (current user, token)
|
||||||
|
- [ ] ⏳ Chat state (messages, active users)
|
||||||
|
- [ ] ⏳ WebRTC state (connection status, transfer progress)
|
||||||
|
|
||||||
|
### Integracja Socket.IO (client)
|
||||||
|
- [ ] ⏳ Setup socket.io-client
|
||||||
|
- [ ] ⏳ Połączenie z backendem
|
||||||
|
- [ ] ⏳ Obsługa eventów czatu
|
||||||
|
- [ ] ⏳ Obsługa eventów WebRTC signaling
|
||||||
|
- [ ] ⏳ Reconnection logic
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 7. Bezpieczeństwo
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
- [ ] ⏳ Rate limiting (np. express-rate-limit)
|
||||||
|
- [ ] ⏳ Helmet.js dla security headers
|
||||||
|
- [ ] ⏳ CORS configuration
|
||||||
|
- [ ] ⏳ Input sanitization (XSS prevention)
|
||||||
|
- [ ] ⏳ SQL injection prevention (prepared statements)
|
||||||
|
- [ ] ⏳ Walidacja wszystkich inputów (express-validator)
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
- [ ] ⏳ XSS prevention (sanitize user input)
|
||||||
|
- [ ] ⏳ CSRF protection
|
||||||
|
- [ ] ⏳ Secure token storage (httpOnly cookies lub secure localStorage)
|
||||||
|
|
||||||
|
### WebRTC
|
||||||
|
- [ ] ⏳ Potwierdzenie, że WebRTC używa DTLS/SRTP (natywne szyfrowanie)
|
||||||
|
- [ ] ⏳ Opcjonalne: dodatkowe szyfrowanie czatu (WebCrypto API)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 8. Testowanie
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
- [ ] ⏳ Setup Jest + Supertest
|
||||||
|
- [ ] ⏳ Testy jednostkowe endpoints API
|
||||||
|
- [ ] ⏳ Testy integracyjne z bazą danych
|
||||||
|
- [ ] ⏳ Testy WebSocket (Socket.IO)
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
- [ ] ⏳ Setup Vitest + React Testing Library
|
||||||
|
- [ ] ⏳ Testy jednostkowe komponentów
|
||||||
|
- [ ] ⏳ Testy integracyjne widoków
|
||||||
|
- [ ] ⏳ E2E testy (Playwright / Cypress)
|
||||||
|
|
||||||
|
### WebRTC
|
||||||
|
- [ ] ⏳ Testy manualne transferu plików (różne rozmiary)
|
||||||
|
- [ ] ⏳ Testy na różnych urządzeniach (Android/iOS)
|
||||||
|
- [ ] ⏳ Testy w różnych warunkach sieciowych
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 9. Dokumentacja
|
||||||
|
|
||||||
|
- [ ] ⏳ README.md - instrukcja uruchomienia projektu
|
||||||
|
- [ ] ⏳ API documentation (Swagger/OpenAPI)
|
||||||
|
- [ ] ⏳ Architektura systemu (diagram)
|
||||||
|
- [ ] ⏳ WebRTC flow diagram
|
||||||
|
- [ ] ⏳ User flow diagrams
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 10. Deployment i DevOps
|
||||||
|
|
||||||
|
- [ ] ⏳ Konfiguracja CI/CD (GitHub Actions)
|
||||||
|
- [ ] ⏳ Automated tests w CI
|
||||||
|
- [ ] ⏳ Docker image optimization (multi-stage builds)
|
||||||
|
- [ ] ⏳ Konfiguracja production environment variables
|
||||||
|
- [ ] ⏳ Setup HTTPS (Let's Encrypt)
|
||||||
|
- [ ] ⏳ Deployment backendu (VPS / Cloud)
|
||||||
|
- [ ] ⏳ Deployment frontendu (Vercel / Netlify / własny serwer)
|
||||||
|
- [ ] ⏳ Setup PostgreSQL backup strategy
|
||||||
|
- [ ] ⏳ Monitoring (logs, errors, performance)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ➕ 11. Opcjonalne rozszerzenia (po MVP)
|
||||||
|
|
||||||
|
- [ ] ⏳ System oznaczeń zaufania (badges dla zweryfikowanych użytkowników)
|
||||||
|
- [ ] ⏳ Blokowanie użytkowników
|
||||||
|
- [ ] ⏳ Publiczne profile z opiniami
|
||||||
|
- [ ] ⏳ Powiadomienia push (Web Push API)
|
||||||
|
- [ ] ⏳ Kompresja wideo przed wysłaniem (opcjonalnie)
|
||||||
|
- [ ] ⏳ Podgląd thumbnail filmu przed wysłaniem
|
||||||
|
- [ ] ⏳ Multi-file transfer (wysyłanie wielu filmów naraz)
|
||||||
|
- [ ] ⏳ Integracja z worldsdc.com API (automatyczne pobieranie eventów)
|
||||||
|
- [ ] ⏳ Statystyki użycia aplikacji (analytics)
|
||||||
|
- [ ] ⏳ Admin panel
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Priorytet realizacji (sugerowany)
|
||||||
|
|
||||||
|
### Faza 1: MVP Foundation (2-3 tygodnie)
|
||||||
|
1. Setup projektu (Docker, struktura, baza danych)
|
||||||
|
2. Backend: podstawowe API (auth, users, events)
|
||||||
|
3. Frontend: podstawowe widoki (login, register, events list)
|
||||||
|
4. WebSocket: czat eventowy
|
||||||
|
|
||||||
|
### Faza 2: Core Features (3-4 tygodnie)
|
||||||
|
1. Matching system (połączenie w pary)
|
||||||
|
2. Czat 1:1
|
||||||
|
3. WebRTC signaling server
|
||||||
|
4. WebRTC P2P connection (podstawy)
|
||||||
|
|
||||||
|
### Faza 3: Główna funkcjonalność (4-5 tygodni)
|
||||||
|
1. **WebRTC file transfer** (wybór pliku, chunking, transfer)
|
||||||
|
2. **Progress monitoring**
|
||||||
|
3. Error handling i reconnection
|
||||||
|
4. Fallback (wymiana linków)
|
||||||
|
|
||||||
|
### Faza 4: Finalizacja MVP (2-3 tygodnie)
|
||||||
|
1. System ocen
|
||||||
|
2. Historia współprac
|
||||||
|
3. Bezpieczeństwo (hardening)
|
||||||
|
4. Testy
|
||||||
|
5. Deployment
|
||||||
|
|
||||||
|
### Faza 5: Opcjonalne rozszerzenia (ongoing)
|
||||||
|
- Features z sekcji 11
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Łączny czas realizacji MVP: ~11-15 tygodni**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Notatki
|
||||||
|
- Regularnie aktualizuj status zadań (⏳ → 🔄 → ✅)
|
||||||
|
- Dodawaj nowe zadania w miarę postępu prac
|
||||||
|
- Priorytetuj zadania krytyczne dla MVP
|
||||||
|
- WebRTC file transfer to najważniejsza i najtrudniejsza część projektu
|
||||||
24
frontend/.gitignore
vendored
Normal file
24
frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
18
frontend/Dockerfile
Normal file
18
frontend/Dockerfile
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
FROM node:20-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
# Copy project files
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Expose Vite dev server port
|
||||||
|
EXPOSE 5173
|
||||||
|
|
||||||
|
# Start dev server
|
||||||
|
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
|
||||||
16
frontend/README.md
Normal file
16
frontend/README.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# React + Vite
|
||||||
|
|
||||||
|
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||||
|
|
||||||
|
Currently, two official plugins are available:
|
||||||
|
|
||||||
|
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
|
||||||
|
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||||
|
|
||||||
|
## React Compiler
|
||||||
|
|
||||||
|
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
||||||
|
|
||||||
|
## Expanding the ESLint configuration
|
||||||
|
|
||||||
|
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
|
||||||
29
frontend/eslint.config.js
Normal file
29
frontend/eslint.config.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import js from '@eslint/js'
|
||||||
|
import globals from 'globals'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
globalIgnores(['dist']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{js,jsx}'],
|
||||||
|
extends: [
|
||||||
|
js.configs.recommended,
|
||||||
|
reactHooks.configs.flat.recommended,
|
||||||
|
reactRefresh.configs.vite,
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
ecmaFeatures: { jsx: true },
|
||||||
|
sourceType: 'module',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
13
frontend/index.html
Normal file
13
frontend/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>frontend</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
4225
frontend/package-lock.json
generated
Normal file
4225
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
32
frontend/package.json
Normal file
32
frontend/package.json
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "frontend",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"lucide-react": "^0.553.0",
|
||||||
|
"react": "^19.2.0",
|
||||||
|
"react-dom": "^19.2.0",
|
||||||
|
"react-router-dom": "^7.9.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.39.1",
|
||||||
|
"@types/react": "^19.2.2",
|
||||||
|
"@types/react-dom": "^19.2.2",
|
||||||
|
"@vitejs/plugin-react": "^5.1.0",
|
||||||
|
"autoprefixer": "^10.4.22",
|
||||||
|
"eslint": "^9.39.1",
|
||||||
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.24",
|
||||||
|
"globals": "^16.5.0",
|
||||||
|
"postcss": "^8.5.6",
|
||||||
|
"tailwindcss": "^3.4.18",
|
||||||
|
"vite": "^7.2.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
frontend/postcss.config.js
Normal file
6
frontend/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
1
frontend/public/vite.svg
Normal file
1
frontend/public/vite.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
42
frontend/src/App.css
Normal file
42
frontend/src/App.css
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
#root {
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
height: 6em;
|
||||||
|
padding: 1.5em;
|
||||||
|
will-change: filter;
|
||||||
|
transition: filter 300ms;
|
||||||
|
}
|
||||||
|
.logo:hover {
|
||||||
|
filter: drop-shadow(0 0 2em #646cffaa);
|
||||||
|
}
|
||||||
|
.logo.react:hover {
|
||||||
|
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes logo-spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
a:nth-of-type(2) .logo {
|
||||||
|
animation: logo-spin infinite 20s linear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
padding: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.read-the-docs {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
123
frontend/src/App.jsx
Normal file
123
frontend/src/App.jsx
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
||||||
|
import { AuthProvider, useAuth } from './contexts/AuthContext';
|
||||||
|
import LoginPage from './pages/LoginPage';
|
||||||
|
import RegisterPage from './pages/RegisterPage';
|
||||||
|
import EventsPage from './pages/EventsPage';
|
||||||
|
import EventChatPage from './pages/EventChatPage';
|
||||||
|
import MatchChatPage from './pages/MatchChatPage';
|
||||||
|
import RatePartnerPage from './pages/RatePartnerPage';
|
||||||
|
import HistoryPage from './pages/HistoryPage';
|
||||||
|
|
||||||
|
// Protected Route Component
|
||||||
|
const ProtectedRoute = ({ children }) => {
|
||||||
|
const { isAuthenticated, loading } = useAuth();
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center">
|
||||||
|
<div className="text-lg text-gray-600">Loading...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
return <Navigate to="/login" replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return children;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Public Route Component (redirect to events if already logged in)
|
||||||
|
const PublicRoute = ({ children }) => {
|
||||||
|
const { isAuthenticated, loading } = useAuth();
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center">
|
||||||
|
<div className="text-lg text-gray-600">Loading...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAuthenticated) {
|
||||||
|
return <Navigate to="/events" replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return children;
|
||||||
|
};
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<BrowserRouter>
|
||||||
|
<AuthProvider>
|
||||||
|
<Routes>
|
||||||
|
{/* Public Routes */}
|
||||||
|
<Route
|
||||||
|
path="/login"
|
||||||
|
element={
|
||||||
|
<PublicRoute>
|
||||||
|
<LoginPage />
|
||||||
|
</PublicRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/register"
|
||||||
|
element={
|
||||||
|
<PublicRoute>
|
||||||
|
<RegisterPage />
|
||||||
|
</PublicRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Protected Routes */}
|
||||||
|
<Route
|
||||||
|
path="/events"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<EventsPage />
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/events/:eventId/chat"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<EventChatPage />
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/matches/:matchId/chat"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<MatchChatPage />
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/matches/:matchId/rate"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<RatePartnerPage />
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/history"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<HistoryPage />
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Default redirect */}
|
||||||
|
<Route path="/" element={<Navigate to="/events" replace />} />
|
||||||
|
<Route path="*" element={<Navigate to="/events" replace />} />
|
||||||
|
</Routes>
|
||||||
|
</AuthProvider>
|
||||||
|
</BrowserRouter>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
1
frontend/src/assets/react.svg
Normal file
1
frontend/src/assets/react.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 4.0 KiB |
14
frontend/src/components/layout/Layout.jsx
Normal file
14
frontend/src/components/layout/Layout.jsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import Navbar from './Navbar';
|
||||||
|
|
||||||
|
const Layout = ({ children }) => {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50">
|
||||||
|
<Navbar />
|
||||||
|
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Layout;
|
||||||
59
frontend/src/components/layout/Navbar.jsx
Normal file
59
frontend/src/components/layout/Navbar.jsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
|
import { useAuth } from '../../contexts/AuthContext';
|
||||||
|
import { Video, LogOut, User, History } from 'lucide-react';
|
||||||
|
|
||||||
|
const Navbar = () => {
|
||||||
|
const { user, logout } = useAuth();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
logout();
|
||||||
|
navigate('/login');
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!user) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className="bg-white shadow-md">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="flex justify-between h-16">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Link to="/events" className="flex items-center space-x-2">
|
||||||
|
<Video className="w-8 h-8 text-primary-600" />
|
||||||
|
<span className="text-xl font-bold text-gray-900">spotlight.cam</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<Link
|
||||||
|
to="/history"
|
||||||
|
className="flex items-center space-x-1 px-3 py-2 rounded-md text-sm font-medium text-gray-700 hover:text-primary-600 hover:bg-gray-100"
|
||||||
|
>
|
||||||
|
<History className="w-4 h-4" />
|
||||||
|
<span>History</span>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<img
|
||||||
|
src={user.avatar}
|
||||||
|
alt={user.username}
|
||||||
|
className="w-8 h-8 rounded-full"
|
||||||
|
/>
|
||||||
|
<span className="text-sm font-medium text-gray-700">{user.username}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleLogout}
|
||||||
|
className="flex items-center space-x-1 px-3 py-2 rounded-md text-sm font-medium text-gray-700 hover:text-red-600 hover:bg-red-50"
|
||||||
|
>
|
||||||
|
<LogOut className="w-4 h-4" />
|
||||||
|
<span>Logout</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Navbar;
|
||||||
70
frontend/src/contexts/AuthContext.jsx
Normal file
70
frontend/src/contexts/AuthContext.jsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { createContext, useContext, useState, useEffect } from 'react';
|
||||||
|
import { mockCurrentUser } from '../mocks/users';
|
||||||
|
|
||||||
|
const AuthContext = createContext(null);
|
||||||
|
|
||||||
|
export const useAuth = () => {
|
||||||
|
const context = useContext(AuthContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useAuth must be used within AuthProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AuthProvider = ({ children }) => {
|
||||||
|
const [user, setUser] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Check if user is logged in (from localStorage)
|
||||||
|
const storedUser = localStorage.getItem('user');
|
||||||
|
if (storedUser) {
|
||||||
|
setUser(JSON.parse(storedUser));
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const login = async (email, password) => {
|
||||||
|
// Mock login - w przyszłości będzie API call
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
const userData = mockCurrentUser;
|
||||||
|
setUser(userData);
|
||||||
|
localStorage.setItem('user', JSON.stringify(userData));
|
||||||
|
resolve(userData);
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const register = async (username, email, password) => {
|
||||||
|
// Mock register - w przyszłości będzie API call
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
const userData = {
|
||||||
|
...mockCurrentUser,
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
};
|
||||||
|
setUser(userData);
|
||||||
|
localStorage.setItem('user', JSON.stringify(userData));
|
||||||
|
resolve(userData);
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const logout = () => {
|
||||||
|
setUser(null);
|
||||||
|
localStorage.removeItem('user');
|
||||||
|
};
|
||||||
|
|
||||||
|
const value = {
|
||||||
|
user,
|
||||||
|
loading,
|
||||||
|
login,
|
||||||
|
register,
|
||||||
|
logout,
|
||||||
|
isAuthenticated: !!user,
|
||||||
|
};
|
||||||
|
|
||||||
|
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||||
|
};
|
||||||
9
frontend/src/index.css
Normal file
9
frontend/src/index.css
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
body {
|
||||||
|
@apply bg-gray-50 text-gray-900;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
frontend/src/main.jsx
Normal file
10
frontend/src/main.jsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { StrictMode } from 'react'
|
||||||
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import './index.css'
|
||||||
|
import App from './App.jsx'
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>,
|
||||||
|
)
|
||||||
42
frontend/src/mocks/events.js
Normal file
42
frontend/src/mocks/events.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
export const mockEvents = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'Warsaw Dance Festival 2025',
|
||||||
|
location: 'Warsaw, Poland',
|
||||||
|
start_date: '2025-03-15',
|
||||||
|
end_date: '2025-03-17',
|
||||||
|
worldsdc_id: 'wdf-2025',
|
||||||
|
participants_count: 156,
|
||||||
|
description: 'The biggest West Coast Swing event in Central Europe',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'Berlin Swing Out',
|
||||||
|
location: 'Berlin, Germany',
|
||||||
|
start_date: '2025-04-20',
|
||||||
|
end_date: '2025-04-22',
|
||||||
|
worldsdc_id: 'bso-2025',
|
||||||
|
participants_count: 203,
|
||||||
|
description: 'Three days of amazing dancing and workshops',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: 'Prague Dance Weekend',
|
||||||
|
location: 'Prague, Czech Republic',
|
||||||
|
start_date: '2025-05-10',
|
||||||
|
end_date: '2025-05-12',
|
||||||
|
worldsdc_id: 'pdw-2025',
|
||||||
|
participants_count: 89,
|
||||||
|
description: 'Intimate event with world-class instructors',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
name: 'Krakow Swing Festival',
|
||||||
|
location: 'Krakow, Poland',
|
||||||
|
start_date: '2025-06-07',
|
||||||
|
end_date: '2025-06-09',
|
||||||
|
worldsdc_id: 'ksf-2025',
|
||||||
|
participants_count: 124,
|
||||||
|
description: 'Dancing in the heart of historical Krakow',
|
||||||
|
},
|
||||||
|
];
|
||||||
79
frontend/src/mocks/matches.js
Normal file
79
frontend/src/mocks/matches.js
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
export const mockMatches = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
user1_id: 1,
|
||||||
|
user2_id: 2,
|
||||||
|
user2_name: 'sarah_swing',
|
||||||
|
user2_avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Sarah',
|
||||||
|
event_id: 1,
|
||||||
|
event_name: 'Warsaw Dance Festival 2025',
|
||||||
|
status: 'active',
|
||||||
|
room_id: 10,
|
||||||
|
created_at: '2025-03-14T11:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
user1_id: 1,
|
||||||
|
user2_id: 3,
|
||||||
|
user2_name: 'mike_moves',
|
||||||
|
user2_avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Mike',
|
||||||
|
event_id: 1,
|
||||||
|
event_name: 'Warsaw Dance Festival 2025',
|
||||||
|
status: 'completed',
|
||||||
|
room_id: 11,
|
||||||
|
created_at: '2025-03-13T14:30:00Z',
|
||||||
|
completed_at: '2025-03-13T16:45:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
user1_id: 1,
|
||||||
|
user2_id: 4,
|
||||||
|
user2_name: 'emma_elegant',
|
||||||
|
user2_avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Emma',
|
||||||
|
event_id: 2,
|
||||||
|
event_name: 'Berlin Swing Out',
|
||||||
|
status: 'completed',
|
||||||
|
room_id: 12,
|
||||||
|
created_at: '2025-02-20T10:00:00Z',
|
||||||
|
completed_at: '2025-02-20T15:30:00Z',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const mockRatings = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
match_id: 2,
|
||||||
|
rater_id: 1,
|
||||||
|
rated_id: 3,
|
||||||
|
rated_name: 'mike_moves',
|
||||||
|
rated_avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Mike',
|
||||||
|
score: 5,
|
||||||
|
comment: 'Great energy and smooth moves! Would love to dance again.',
|
||||||
|
would_collaborate_again: true,
|
||||||
|
created_at: '2025-03-13T17:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
match_id: 3,
|
||||||
|
rater_id: 1,
|
||||||
|
rated_id: 4,
|
||||||
|
rated_name: 'emma_elegant',
|
||||||
|
rated_avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Emma',
|
||||||
|
score: 5,
|
||||||
|
comment: 'Amazing dancer with excellent technique. Highly recommend!',
|
||||||
|
would_collaborate_again: true,
|
||||||
|
created_at: '2025-02-20T16:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
match_id: 2,
|
||||||
|
rater_id: 3,
|
||||||
|
rated_id: 1,
|
||||||
|
rated_name: 'john_dancer',
|
||||||
|
rated_avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=John',
|
||||||
|
score: 4,
|
||||||
|
comment: 'Good collaboration, very professional!',
|
||||||
|
would_collaborate_again: true,
|
||||||
|
created_at: '2025-03-13T17:15:00Z',
|
||||||
|
},
|
||||||
|
];
|
||||||
81
frontend/src/mocks/messages.js
Normal file
81
frontend/src/mocks/messages.js
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
export const mockEventMessages = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
room_id: 1,
|
||||||
|
user_id: 2,
|
||||||
|
username: 'sarah_swing',
|
||||||
|
avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Sarah',
|
||||||
|
content: 'Hey everyone! Looking forward to dancing this weekend!',
|
||||||
|
type: 'text',
|
||||||
|
created_at: '2025-03-14T10:30:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
room_id: 1,
|
||||||
|
user_id: 3,
|
||||||
|
username: 'mike_moves',
|
||||||
|
avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Mike',
|
||||||
|
content: 'Anyone interested in filming some socials together?',
|
||||||
|
type: 'text',
|
||||||
|
created_at: '2025-03-14T10:32:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
room_id: 1,
|
||||||
|
user_id: 4,
|
||||||
|
username: 'emma_elegant',
|
||||||
|
avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Emma',
|
||||||
|
content: 'I\'m in! Would love to collaborate',
|
||||||
|
type: 'text',
|
||||||
|
created_at: '2025-03-14T10:35:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
room_id: 1,
|
||||||
|
user_id: 5,
|
||||||
|
username: 'alex_awesome',
|
||||||
|
avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Alex',
|
||||||
|
content: 'This is my first event. Super excited!',
|
||||||
|
type: 'text',
|
||||||
|
created_at: '2025-03-14T10:40:00Z',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const mockPrivateMessages = [
|
||||||
|
{
|
||||||
|
id: 101,
|
||||||
|
room_id: 10,
|
||||||
|
user_id: 2,
|
||||||
|
username: 'sarah_swing',
|
||||||
|
content: 'Hi! Great to match with you!',
|
||||||
|
type: 'text',
|
||||||
|
created_at: '2025-03-14T11:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 102,
|
||||||
|
room_id: 10,
|
||||||
|
user_id: 1,
|
||||||
|
username: 'john_dancer',
|
||||||
|
content: 'Same here! When do you want to record?',
|
||||||
|
type: 'text',
|
||||||
|
created_at: '2025-03-14T11:02:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 103,
|
||||||
|
room_id: 10,
|
||||||
|
user_id: 2,
|
||||||
|
username: 'sarah_swing',
|
||||||
|
content: 'How about after the next workshop?',
|
||||||
|
type: 'text',
|
||||||
|
created_at: '2025-03-14T11:03:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 104,
|
||||||
|
room_id: 10,
|
||||||
|
user_id: 1,
|
||||||
|
username: 'john_dancer',
|
||||||
|
content: 'Perfect! See you then 👍',
|
||||||
|
type: 'text',
|
||||||
|
created_at: '2025-03-14T11:05:00Z',
|
||||||
|
},
|
||||||
|
];
|
||||||
58
frontend/src/mocks/users.js
Normal file
58
frontend/src/mocks/users.js
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
export const mockUsers = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
username: 'john_dancer',
|
||||||
|
email: 'john@example.com',
|
||||||
|
avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=John',
|
||||||
|
rating: 4.8,
|
||||||
|
matches_count: 23,
|
||||||
|
created_at: '2024-01-15',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
username: 'sarah_swing',
|
||||||
|
email: 'sarah@example.com',
|
||||||
|
avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Sarah',
|
||||||
|
rating: 4.9,
|
||||||
|
matches_count: 31,
|
||||||
|
created_at: '2024-02-20',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
username: 'mike_moves',
|
||||||
|
email: 'mike@example.com',
|
||||||
|
avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Mike',
|
||||||
|
rating: 4.6,
|
||||||
|
matches_count: 18,
|
||||||
|
created_at: '2024-03-10',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
username: 'emma_elegant',
|
||||||
|
email: 'emma@example.com',
|
||||||
|
avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Emma',
|
||||||
|
rating: 4.95,
|
||||||
|
matches_count: 42,
|
||||||
|
created_at: '2023-11-05',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
username: 'alex_awesome',
|
||||||
|
email: 'alex@example.com',
|
||||||
|
avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Alex',
|
||||||
|
rating: 4.7,
|
||||||
|
matches_count: 27,
|
||||||
|
created_at: '2024-04-12',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Current logged-in user
|
||||||
|
export const mockCurrentUser = {
|
||||||
|
id: 1,
|
||||||
|
username: 'john_dancer',
|
||||||
|
email: 'john@example.com',
|
||||||
|
avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=John',
|
||||||
|
rating: 4.8,
|
||||||
|
matches_count: 23,
|
||||||
|
created_at: '2024-01-15',
|
||||||
|
};
|
||||||
183
frontend/src/pages/EventChatPage.jsx
Normal file
183
frontend/src/pages/EventChatPage.jsx
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
import { useState, useRef, useEffect } from 'react';
|
||||||
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
|
import Layout from '../components/layout/Layout';
|
||||||
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
|
import { mockEvents } from '../mocks/events';
|
||||||
|
import { mockEventMessages } from '../mocks/messages';
|
||||||
|
import { mockUsers } from '../mocks/users';
|
||||||
|
import { Send, UserPlus } from 'lucide-react';
|
||||||
|
|
||||||
|
const EventChatPage = () => {
|
||||||
|
const { eventId } = useParams();
|
||||||
|
const { user } = useAuth();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [messages, setMessages] = useState(mockEventMessages);
|
||||||
|
const [newMessage, setNewMessage] = useState('');
|
||||||
|
const [activeUsers, setActiveUsers] = useState(mockUsers.slice(1, 5));
|
||||||
|
const messagesEndRef = useRef(null);
|
||||||
|
|
||||||
|
const event = mockEvents.find(e => e.id === parseInt(eventId));
|
||||||
|
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
scrollToBottom();
|
||||||
|
}, [messages]);
|
||||||
|
|
||||||
|
const handleSendMessage = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!newMessage.trim()) return;
|
||||||
|
|
||||||
|
const message = {
|
||||||
|
id: messages.length + 1,
|
||||||
|
room_id: parseInt(eventId),
|
||||||
|
user_id: user.id,
|
||||||
|
username: user.username,
|
||||||
|
avatar: user.avatar,
|
||||||
|
content: newMessage,
|
||||||
|
type: 'text',
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
setMessages([...messages, message]);
|
||||||
|
setNewMessage('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMatchWith = (userId) => {
|
||||||
|
// Mockup - in the future will be WebSocket request
|
||||||
|
alert(`Match request sent to user!`);
|
||||||
|
// Simulate acceptance after 1 second
|
||||||
|
setTimeout(() => {
|
||||||
|
navigate(`/matches/1/chat`);
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!event) {
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<div className="text-center">Event not found</div>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<div className="max-w-6xl mx-auto">
|
||||||
|
<div className="bg-white rounded-lg shadow-md overflow-hidden">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="bg-primary-600 text-white p-4">
|
||||||
|
<h2 className="text-2xl font-bold">{event.name}</h2>
|
||||||
|
<p className="text-primary-100 text-sm">{event.location}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex h-[calc(100vh-280px)]">
|
||||||
|
{/* Active Users Sidebar */}
|
||||||
|
<div className="w-64 border-r bg-gray-50 p-4 overflow-y-auto">
|
||||||
|
<h3 className="font-semibold text-gray-900 mb-4">
|
||||||
|
Active users ({activeUsers.length})
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{activeUsers.map((activeUser) => (
|
||||||
|
<div
|
||||||
|
key={activeUser.id}
|
||||||
|
className="flex items-center justify-between p-2 hover:bg-gray-100 rounded-lg"
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<img
|
||||||
|
src={activeUser.avatar}
|
||||||
|
alt={activeUser.username}
|
||||||
|
className="w-8 h-8 rounded-full"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-900">
|
||||||
|
{activeUser.username}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
⭐ {activeUser.rating}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => handleMatchWith(activeUser.id)}
|
||||||
|
className="p-1 text-primary-600 hover:bg-primary-50 rounded"
|
||||||
|
title="Connect"
|
||||||
|
>
|
||||||
|
<UserPlus className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Chat Area */}
|
||||||
|
<div className="flex-1 flex flex-col">
|
||||||
|
{/* Messages */}
|
||||||
|
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
||||||
|
{messages.map((message) => {
|
||||||
|
const isOwnMessage = message.user_id === user.id;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={message.id}
|
||||||
|
className={`flex ${isOwnMessage ? 'justify-end' : 'justify-start'}`}
|
||||||
|
>
|
||||||
|
<div className={`flex items-start space-x-2 max-w-md ${isOwnMessage ? 'flex-row-reverse space-x-reverse' : ''}`}>
|
||||||
|
<img
|
||||||
|
src={message.avatar}
|
||||||
|
alt={message.username}
|
||||||
|
className="w-8 h-8 rounded-full"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<div className="flex items-baseline space-x-2 mb-1">
|
||||||
|
<span className="text-sm font-medium text-gray-900">
|
||||||
|
{message.username}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-gray-500">
|
||||||
|
{new Date(message.created_at).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`rounded-lg px-4 py-2 ${
|
||||||
|
isOwnMessage
|
||||||
|
? 'bg-primary-600 text-white'
|
||||||
|
: 'bg-gray-100 text-gray-900'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{message.content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<div ref={messagesEndRef} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Message Input */}
|
||||||
|
<div className="border-t p-4">
|
||||||
|
<form onSubmit={handleSendMessage} className="flex space-x-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={newMessage}
|
||||||
|
onChange={(e) => setNewMessage(e.target.value)}
|
||||||
|
placeholder="Write a message..."
|
||||||
|
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-primary-500 focus:border-primary-500"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors"
|
||||||
|
>
|
||||||
|
<Send className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EventChatPage;
|
||||||
60
frontend/src/pages/EventsPage.jsx
Normal file
60
frontend/src/pages/EventsPage.jsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import Layout from '../components/layout/Layout';
|
||||||
|
import { mockEvents } from '../mocks/events';
|
||||||
|
import { Calendar, MapPin, Users } from 'lucide-react';
|
||||||
|
|
||||||
|
const EventsPage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleJoinEvent = (eventId) => {
|
||||||
|
navigate(`/events/${eventId}/chat`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<div className="max-w-4xl mx-auto">
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900 mb-2">Choose an event</h1>
|
||||||
|
<p className="text-gray-600 mb-8">Join an event and start connecting with other dancers</p>
|
||||||
|
|
||||||
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
|
{mockEvents.map((event) => (
|
||||||
|
<div
|
||||||
|
key={event.id}
|
||||||
|
className="bg-white rounded-lg shadow-md hover:shadow-lg transition-shadow p-6 border border-gray-200"
|
||||||
|
>
|
||||||
|
<h3 className="text-xl font-bold text-gray-900 mb-3">{event.name}</h3>
|
||||||
|
|
||||||
|
<div className="space-y-2 mb-4">
|
||||||
|
<div className="flex items-center text-gray-600">
|
||||||
|
<MapPin className="w-4 h-4 mr-2" />
|
||||||
|
<span className="text-sm">{event.location}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center text-gray-600">
|
||||||
|
<Calendar className="w-4 h-4 mr-2" />
|
||||||
|
<span className="text-sm">
|
||||||
|
{new Date(event.start_date).toLocaleDateString('en-US')} - {new Date(event.end_date).toLocaleDateString('en-US')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center text-gray-600">
|
||||||
|
<Users className="w-4 h-4 mr-2" />
|
||||||
|
<span className="text-sm">{event.participants_count} participants</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-gray-600 text-sm mb-4">{event.description}</p>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => handleJoinEvent(event.id)}
|
||||||
|
className="w-full px-4 py-2 bg-primary-600 text-white rounded-md hover:bg-primary-700 transition-colors"
|
||||||
|
>
|
||||||
|
Join chat
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EventsPage;
|
||||||
129
frontend/src/pages/HistoryPage.jsx
Normal file
129
frontend/src/pages/HistoryPage.jsx
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
import Layout from '../components/layout/Layout';
|
||||||
|
import { mockMatches, mockRatings } from '../mocks/matches';
|
||||||
|
import { Calendar, MapPin, Star, MessageCircle } from 'lucide-react';
|
||||||
|
|
||||||
|
const HistoryPage = () => {
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<div className="max-w-4xl mx-auto">
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900 mb-2">Collaboration history</h1>
|
||||||
|
<p className="text-gray-600 mb-8">Your previous matches and ratings</p>
|
||||||
|
|
||||||
|
{/* Matches Section */}
|
||||||
|
<div className="mb-12">
|
||||||
|
<h2 className="text-xl font-bold text-gray-900 mb-4">Your matches</h2>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{mockMatches.map((match) => (
|
||||||
|
<div
|
||||||
|
key={match.id}
|
||||||
|
className="bg-white rounded-lg shadow-md p-6 border border-gray-200"
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex items-start space-x-4">
|
||||||
|
<img
|
||||||
|
src={match.user2_avatar}
|
||||||
|
alt={match.user2_name}
|
||||||
|
className="w-16 h-16 rounded-full"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-bold text-gray-900">{match.user2_name}</h3>
|
||||||
|
<div className="flex items-center space-x-4 mt-2 text-sm text-gray-600">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<MapPin className="w-4 h-4 mr-1" />
|
||||||
|
{match.event_name}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Calendar className="w-4 h-4 mr-1" />
|
||||||
|
{new Date(match.created_at).toLocaleDateString('pl-PL')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span
|
||||||
|
className={`px-3 py-1 rounded-full text-sm font-medium ${
|
||||||
|
match.status === 'completed'
|
||||||
|
? 'bg-green-100 text-green-800'
|
||||||
|
: 'bg-blue-100 text-blue-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{match.status === 'completed' ? 'Completed' : 'Active'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Ratings Section */}
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-bold text-gray-900 mb-4">Received ratings</h2>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{mockRatings.filter(r => r.rated_id === 1).map((rating) => (
|
||||||
|
<div
|
||||||
|
key={rating.id}
|
||||||
|
className="bg-white rounded-lg shadow-md p-6 border border-gray-200"
|
||||||
|
>
|
||||||
|
<div className="flex items-start space-x-4 mb-4">
|
||||||
|
<img
|
||||||
|
src={rating.rated_avatar}
|
||||||
|
alt={rating.rated_name}
|
||||||
|
className="w-12 h-12 rounded-full"
|
||||||
|
/>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<h3 className="font-bold text-gray-900">{rating.rated_name}</h3>
|
||||||
|
<div className="flex items-center space-x-1">
|
||||||
|
{[...Array(5)].map((_, i) => (
|
||||||
|
<Star
|
||||||
|
key={i}
|
||||||
|
className={`w-4 h-4 ${
|
||||||
|
i < rating.score
|
||||||
|
? 'fill-yellow-400 text-yellow-400'
|
||||||
|
: 'text-gray-300'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-600 mb-2">{rating.comment}</p>
|
||||||
|
<div className="flex items-center space-x-4 text-xs text-gray-500">
|
||||||
|
<span>{new Date(rating.created_at).toLocaleDateString('en-US')}</span>
|
||||||
|
{rating.would_collaborate_again && (
|
||||||
|
<span className="text-green-600 font-medium">
|
||||||
|
✓ Would collaborate again
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats Card */}
|
||||||
|
<div className="mt-8 bg-gradient-to-br from-primary-500 to-primary-700 rounded-lg shadow-md p-6 text-white">
|
||||||
|
<h3 className="text-xl font-bold mb-4">Your statistics</h3>
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-3xl font-bold">{mockMatches.length}</div>
|
||||||
|
<div className="text-sm text-primary-100">Collaborations</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-3xl font-bold">4.8</div>
|
||||||
|
<div className="text-sm text-primary-100">Average rating</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-3xl font-bold">100%</div>
|
||||||
|
<div className="text-sm text-primary-100">Would collaborate again</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HistoryPage;
|
||||||
102
frontend/src/pages/LoginPage.jsx
Normal file
102
frontend/src/pages/LoginPage.jsx
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { useNavigate, Link } from 'react-router-dom';
|
||||||
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
|
import { Video, Mail, Lock } from 'lucide-react';
|
||||||
|
|
||||||
|
const LoginPage = () => {
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const { login } = useAuth();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
await login(email, password);
|
||||||
|
navigate('/events');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Login failed:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center px-4">
|
||||||
|
<div className="max-w-md w-full bg-white rounded-lg shadow-xl p-8">
|
||||||
|
<div className="flex flex-col items-center mb-8">
|
||||||
|
<Video className="w-16 h-16 text-primary-600 mb-4" />
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900">spotlight.cam</h1>
|
||||||
|
<p className="text-gray-600 mt-2">Sign in to your account</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Email
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
|
<Mail className="h-5 w-5 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
className="pl-10 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500"
|
||||||
|
placeholder="your@email.com"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
|
<Lock className="h-5 w-5 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
className="pl-10 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500"
|
||||||
|
placeholder="••••••••"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{loading ? 'Signing in...' : 'Sign in'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="mt-6 text-center">
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
Don't have an account?{' '}
|
||||||
|
<Link to="/register" className="font-medium text-primary-600 hover:text-primary-500">
|
||||||
|
Sign up
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 p-4 bg-yellow-50 border border-yellow-200 rounded-md">
|
||||||
|
<p className="text-xs text-yellow-800">
|
||||||
|
<strong>Demo:</strong> Enter any email and password to sign in (mock auth)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoginPage;
|
||||||
405
frontend/src/pages/MatchChatPage.jsx
Normal file
405
frontend/src/pages/MatchChatPage.jsx
Normal file
@@ -0,0 +1,405 @@
|
|||||||
|
import { useState, useRef, useEffect } from 'react';
|
||||||
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
|
import Layout from '../components/layout/Layout';
|
||||||
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
|
import { mockPrivateMessages } from '../mocks/messages';
|
||||||
|
import { mockUsers } from '../mocks/users';
|
||||||
|
import { Send, Video, Upload, X, Check, Link as LinkIcon } from 'lucide-react';
|
||||||
|
|
||||||
|
const MatchChatPage = () => {
|
||||||
|
const { matchId } = useParams();
|
||||||
|
const { user } = useAuth();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [messages, setMessages] = useState(mockPrivateMessages);
|
||||||
|
const [newMessage, setNewMessage] = useState('');
|
||||||
|
const [selectedFile, setSelectedFile] = useState(null);
|
||||||
|
const [isTransferring, setIsTransferring] = useState(false);
|
||||||
|
const [transferProgress, setTransferProgress] = useState(0);
|
||||||
|
const [webrtcStatus, setWebrtcStatus] = useState('disconnected'); // disconnected, connecting, connected, failed
|
||||||
|
const [showLinkInput, setShowLinkInput] = useState(false);
|
||||||
|
const [videoLink, setVideoLink] = useState('');
|
||||||
|
const messagesEndRef = useRef(null);
|
||||||
|
const fileInputRef = useRef(null);
|
||||||
|
|
||||||
|
// Partner user (mockup)
|
||||||
|
const partner = mockUsers[1]; // sarah_swing
|
||||||
|
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
scrollToBottom();
|
||||||
|
}, [messages]);
|
||||||
|
|
||||||
|
const handleSendMessage = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!newMessage.trim()) return;
|
||||||
|
|
||||||
|
const message = {
|
||||||
|
id: messages.length + 1,
|
||||||
|
room_id: 10,
|
||||||
|
user_id: user.id,
|
||||||
|
username: user.username,
|
||||||
|
content: newMessage,
|
||||||
|
type: 'text',
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
setMessages([...messages, message]);
|
||||||
|
setNewMessage('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileSelect = (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (file && file.type.startsWith('video/')) {
|
||||||
|
setSelectedFile(file);
|
||||||
|
} else {
|
||||||
|
alert('Proszę wybrać plik wideo');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const simulateWebRTCConnection = () => {
|
||||||
|
setWebrtcStatus('connecting');
|
||||||
|
setTimeout(() => {
|
||||||
|
setWebrtcStatus('connected');
|
||||||
|
}, 1500);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStartTransfer = () => {
|
||||||
|
if (!selectedFile) return;
|
||||||
|
|
||||||
|
// Simulate WebRTC connection
|
||||||
|
simulateWebRTCConnection();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsTransferring(true);
|
||||||
|
setTransferProgress(0);
|
||||||
|
|
||||||
|
// Simulate transfer progress
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setTransferProgress((prev) => {
|
||||||
|
if (prev >= 100) {
|
||||||
|
clearInterval(interval);
|
||||||
|
setIsTransferring(false);
|
||||||
|
setSelectedFile(null);
|
||||||
|
setWebrtcStatus('disconnected');
|
||||||
|
|
||||||
|
// Add message about completed transfer
|
||||||
|
const message = {
|
||||||
|
id: messages.length + 1,
|
||||||
|
room_id: 10,
|
||||||
|
user_id: user.id,
|
||||||
|
username: user.username,
|
||||||
|
content: `📹 Video sent: ${selectedFile.name} (${(selectedFile.size / 1024 / 1024).toFixed(2)} MB)`,
|
||||||
|
type: 'video',
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
setMessages((prev) => [...prev, message]);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return prev + 5;
|
||||||
|
});
|
||||||
|
}, 200);
|
||||||
|
}, 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelTransfer = () => {
|
||||||
|
setIsTransferring(false);
|
||||||
|
setTransferProgress(0);
|
||||||
|
setSelectedFile(null);
|
||||||
|
setWebrtcStatus('disconnected');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSendLink = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!videoLink.trim()) return;
|
||||||
|
|
||||||
|
const message = {
|
||||||
|
id: messages.length + 1,
|
||||||
|
room_id: 10,
|
||||||
|
user_id: user.id,
|
||||||
|
username: user.username,
|
||||||
|
content: `🔗 Video link: ${videoLink}`,
|
||||||
|
type: 'link',
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
setMessages([...messages, message]);
|
||||||
|
setVideoLink('');
|
||||||
|
setShowLinkInput(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEndMatch = () => {
|
||||||
|
navigate(`/matches/${matchId}/rate`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getWebRTCStatusColor = () => {
|
||||||
|
switch (webrtcStatus) {
|
||||||
|
case 'connected':
|
||||||
|
return 'text-green-600';
|
||||||
|
case 'connecting':
|
||||||
|
return 'text-yellow-600';
|
||||||
|
case 'failed':
|
||||||
|
return 'text-red-600';
|
||||||
|
default:
|
||||||
|
return 'text-gray-400';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getWebRTCStatusText = () => {
|
||||||
|
switch (webrtcStatus) {
|
||||||
|
case 'connected':
|
||||||
|
return 'Connected (P2P)';
|
||||||
|
case 'connecting':
|
||||||
|
return 'Connecting...';
|
||||||
|
case 'failed':
|
||||||
|
return 'Connection failed';
|
||||||
|
default:
|
||||||
|
return 'Disconnected';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<div className="max-w-4xl mx-auto">
|
||||||
|
<div className="bg-white rounded-lg shadow-md overflow-hidden">
|
||||||
|
{/* Header with Partner Info */}
|
||||||
|
<div className="bg-primary-600 text-white p-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<img
|
||||||
|
src={partner.avatar}
|
||||||
|
alt={partner.username}
|
||||||
|
className="w-12 h-12 rounded-full border-2 border-white"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-bold">{partner.username}</h2>
|
||||||
|
<p className="text-sm text-primary-100">⭐ {partner.rating} • {partner.matches_count} collaborations</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleEndMatch}
|
||||||
|
className="px-4 py-2 bg-white text-primary-600 rounded-md hover:bg-primary-50 transition-colors"
|
||||||
|
>
|
||||||
|
End & rate
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* WebRTC Status Bar */}
|
||||||
|
<div className="bg-gray-50 border-b px-4 py-2 flex items-center justify-between">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className={`w-2 h-2 rounded-full ${webrtcStatus === 'connected' ? 'bg-green-500' : 'bg-gray-300'}`} />
|
||||||
|
<span className={`text-sm font-medium ${getWebRTCStatusColor()}`}>
|
||||||
|
{getWebRTCStatusText()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-gray-500">
|
||||||
|
{webrtcStatus === 'connected' ? '🔒 E2E Encrypted (DTLS/SRTP)' : 'WebRTC ready to connect'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col h-[calc(100vh-320px)]">
|
||||||
|
{/* Messages */}
|
||||||
|
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
||||||
|
{messages.map((message) => {
|
||||||
|
const isOwnMessage = message.user_id === user.id;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={message.id}
|
||||||
|
className={`flex ${isOwnMessage ? 'justify-end' : 'justify-start'}`}
|
||||||
|
>
|
||||||
|
<div className={`flex items-start space-x-2 max-w-md ${isOwnMessage ? 'flex-row-reverse space-x-reverse' : ''}`}>
|
||||||
|
<img
|
||||||
|
src={isOwnMessage ? user.avatar : partner.avatar}
|
||||||
|
alt={message.username}
|
||||||
|
className="w-8 h-8 rounded-full"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<div className="flex items-baseline space-x-2 mb-1">
|
||||||
|
<span className="text-sm font-medium text-gray-900">
|
||||||
|
{message.username}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-gray-500">
|
||||||
|
{new Date(message.created_at).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`rounded-lg px-4 py-2 ${
|
||||||
|
isOwnMessage
|
||||||
|
? 'bg-primary-600 text-white'
|
||||||
|
: 'bg-gray-100 text-gray-900'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{message.content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<div ref={messagesEndRef} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Video Transfer Section */}
|
||||||
|
{(selectedFile || isTransferring) && (
|
||||||
|
<div className="border-t bg-blue-50 p-4">
|
||||||
|
<div className="bg-white rounded-lg p-4 shadow-sm">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<Video className="w-6 h-6 text-primary-600" />
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-gray-900">{selectedFile?.name}</p>
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
{selectedFile && `${(selectedFile.size / 1024 / 1024).toFixed(2)} MB`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{!isTransferring && (
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedFile(null)}
|
||||||
|
className="text-gray-400 hover:text-gray-600"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isTransferring ? (
|
||||||
|
<>
|
||||||
|
<div className="mb-2">
|
||||||
|
<div className="flex justify-between text-sm text-gray-600 mb-1">
|
||||||
|
<span>Transferring via WebRTC...</span>
|
||||||
|
<span>{transferProgress}%</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||||
|
<div
|
||||||
|
className="bg-primary-600 h-2 rounded-full transition-all duration-200"
|
||||||
|
style={{ width: `${transferProgress}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleCancelTransfer}
|
||||||
|
className="w-full px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 transition-colors"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={handleStartTransfer}
|
||||||
|
className="w-full px-4 py-2 bg-primary-600 text-white rounded-md hover:bg-primary-700 transition-colors flex items-center justify-center space-x-2"
|
||||||
|
>
|
||||||
|
<Upload className="w-4 h-4" />
|
||||||
|
<span>Send video (P2P)</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Link Input Section */}
|
||||||
|
{showLinkInput && (
|
||||||
|
<div className="border-t bg-yellow-50 p-4">
|
||||||
|
<div className="bg-white rounded-lg p-4 shadow-sm">
|
||||||
|
<form onSubmit={handleSendLink} className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Video link (Google Drive, Dropbox, etc.)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
value={videoLink}
|
||||||
|
onChange={(e) => setVideoLink(e.target.value)}
|
||||||
|
placeholder="https://drive.google.com/..."
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-primary-500 focus:border-primary-500"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="flex-1 px-4 py-2 bg-primary-600 text-white rounded-md hover:bg-primary-700 transition-colors"
|
||||||
|
>
|
||||||
|
Send link
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setShowLinkInput(false);
|
||||||
|
setVideoLink('');
|
||||||
|
}}
|
||||||
|
className="px-4 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 transition-colors"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Message Input & Actions */}
|
||||||
|
<div className="border-t p-4 bg-gray-50">
|
||||||
|
<div className="flex space-x-2 mb-3">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref={fileInputRef}
|
||||||
|
onChange={handleFileSelect}
|
||||||
|
accept="video/*"
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
disabled={isTransferring || selectedFile}
|
||||||
|
className="flex-1 px-4 py-2 bg-primary-600 text-white rounded-md hover:bg-primary-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center space-x-2"
|
||||||
|
>
|
||||||
|
<Video className="w-4 h-4" />
|
||||||
|
<span>Send video (WebRTC)</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowLinkInput(!showLinkInput)}
|
||||||
|
disabled={isTransferring}
|
||||||
|
className="px-4 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center space-x-2"
|
||||||
|
>
|
||||||
|
<LinkIcon className="w-4 h-4" />
|
||||||
|
<span>Link</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSendMessage} className="flex space-x-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={newMessage}
|
||||||
|
onChange={(e) => setNewMessage(e.target.value)}
|
||||||
|
placeholder="Write a message..."
|
||||||
|
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-primary-500 focus:border-primary-500"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors"
|
||||||
|
>
|
||||||
|
<Send className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Info Box */}
|
||||||
|
<div className="mt-4 p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
||||||
|
<p className="text-sm text-blue-800">
|
||||||
|
<strong>🚀 WebRTC P2P Functionality Mockup:</strong> In the full version, videos will be transferred directly
|
||||||
|
between users via RTCDataChannel, with chunking and progress monitoring.
|
||||||
|
The server is only used for SDP/ICE exchange (signaling).
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MatchChatPage;
|
||||||
135
frontend/src/pages/RatePartnerPage.jsx
Normal file
135
frontend/src/pages/RatePartnerPage.jsx
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
|
import Layout from '../components/layout/Layout';
|
||||||
|
import { mockUsers } from '../mocks/users';
|
||||||
|
import { Star } from 'lucide-react';
|
||||||
|
|
||||||
|
const RatePartnerPage = () => {
|
||||||
|
const { matchId } = useParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [rating, setRating] = useState(0);
|
||||||
|
const [hoveredRating, setHoveredRating] = useState(0);
|
||||||
|
const [comment, setComment] = useState('');
|
||||||
|
const [wouldCollaborateAgain, setWouldCollaborateAgain] = useState(true);
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
|
||||||
|
// Partner user (mockup)
|
||||||
|
const partner = mockUsers[1]; // sarah_swing
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (rating === 0) {
|
||||||
|
alert('Please select a rating (1-5 stars)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSubmitting(true);
|
||||||
|
|
||||||
|
// Mockup - in the future will be API call
|
||||||
|
setTimeout(() => {
|
||||||
|
alert('Rating saved!');
|
||||||
|
navigate('/history');
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<div className="max-w-2xl mx-auto">
|
||||||
|
<div className="bg-white rounded-lg shadow-md p-8">
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900 mb-6 text-center">
|
||||||
|
Rate the collaboration
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
{/* Partner Info */}
|
||||||
|
<div className="flex items-center justify-center space-x-4 mb-8 p-6 bg-gray-50 rounded-lg">
|
||||||
|
<img
|
||||||
|
src={partner.avatar}
|
||||||
|
alt={partner.username}
|
||||||
|
className="w-16 h-16 rounded-full"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-bold text-gray-900">{partner.username}</h3>
|
||||||
|
<p className="text-gray-600">⭐ {partner.rating} • {partner.matches_count} collaborations</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
|
{/* Rating Stars */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-3 text-center">
|
||||||
|
How would you rate the collaboration?
|
||||||
|
</label>
|
||||||
|
<div className="flex justify-center space-x-2">
|
||||||
|
{[1, 2, 3, 4, 5].map((star) => (
|
||||||
|
<button
|
||||||
|
key={star}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setRating(star)}
|
||||||
|
onMouseEnter={() => setHoveredRating(star)}
|
||||||
|
onMouseLeave={() => setHoveredRating(0)}
|
||||||
|
className="focus:outline-none transition-transform hover:scale-110"
|
||||||
|
>
|
||||||
|
<Star
|
||||||
|
className={`w-12 h-12 ${
|
||||||
|
star <= (hoveredRating || rating)
|
||||||
|
? 'fill-yellow-400 text-yellow-400'
|
||||||
|
: 'text-gray-300'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<p className="text-center text-sm text-gray-500 mt-2">
|
||||||
|
{rating === 0 && 'Click to rate'}
|
||||||
|
{rating === 1 && 'Poor'}
|
||||||
|
{rating === 2 && 'Fair'}
|
||||||
|
{rating === 3 && 'Good'}
|
||||||
|
{rating === 4 && 'Very good'}
|
||||||
|
{rating === 5 && 'Excellent!'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Comment */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Comment (optional)
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={comment}
|
||||||
|
onChange={(e) => setComment(e.target.value)}
|
||||||
|
rows={4}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-primary-500 focus:border-primary-500"
|
||||||
|
placeholder="Share your thoughts about the collaboration..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Would Collaborate Again */}
|
||||||
|
<div className="flex items-center space-x-3 p-4 bg-gray-50 rounded-lg">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="collaborate"
|
||||||
|
checked={wouldCollaborateAgain}
|
||||||
|
onChange={(e) => setWouldCollaborateAgain(e.target.checked)}
|
||||||
|
className="w-5 h-5 text-primary-600 border-gray-300 rounded focus:ring-primary-500"
|
||||||
|
/>
|
||||||
|
<label htmlFor="collaborate" className="text-sm font-medium text-gray-700 cursor-pointer">
|
||||||
|
I would like to collaborate again
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Submit Button */}
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={submitting || rating === 0}
|
||||||
|
className="w-full px-6 py-3 bg-primary-600 text-white rounded-md hover:bg-primary-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed font-medium"
|
||||||
|
>
|
||||||
|
{submitting ? 'Saving...' : 'Save rating'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RatePartnerPage;
|
||||||
140
frontend/src/pages/RegisterPage.jsx
Normal file
140
frontend/src/pages/RegisterPage.jsx
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { useNavigate, Link } from 'react-router-dom';
|
||||||
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
|
import { Video, Mail, Lock, User } from 'lucide-react';
|
||||||
|
|
||||||
|
const RegisterPage = () => {
|
||||||
|
const [username, setUsername] = useState('');
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const { register } = useAuth();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (password !== confirmPassword) {
|
||||||
|
alert('Passwords do not match');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
await register(username, email, password);
|
||||||
|
navigate('/events');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Registration failed:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center px-4">
|
||||||
|
<div className="max-w-md w-full bg-white rounded-lg shadow-xl p-8">
|
||||||
|
<div className="flex flex-col items-center mb-8">
|
||||||
|
<Video className="w-16 h-16 text-primary-600 mb-4" />
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900">spotlight.cam</h1>
|
||||||
|
<p className="text-gray-600 mt-2">Create a new account</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Username
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
|
<User className="h-5 w-5 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
className="pl-10 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500"
|
||||||
|
placeholder="your_username"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Email
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
|
<Mail className="h-5 w-5 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
className="pl-10 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500"
|
||||||
|
placeholder="your@email.com"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
|
<Lock className="h-5 w-5 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
className="pl-10 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500"
|
||||||
|
placeholder="••••••••"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Confirm password
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
|
<Lock className="h-5 w-5 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
|
className="pl-10 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500"
|
||||||
|
placeholder="••••••••"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{loading ? 'Creating account...' : 'Sign up'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="mt-6 text-center">
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
Already have an account?{' '}
|
||||||
|
<Link to="/login" className="font-medium text-primary-600 hover:text-primary-500">
|
||||||
|
Sign in
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RegisterPage;
|
||||||
26
frontend/tailwind.config.js
Normal file
26
frontend/tailwind.config.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: [
|
||||||
|
"./index.html",
|
||||||
|
"./src/**/*.{js,ts,jsx,tsx}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
primary: {
|
||||||
|
50: '#f0f9ff',
|
||||||
|
100: '#e0f2fe',
|
||||||
|
200: '#bae6fd',
|
||||||
|
300: '#7dd3fc',
|
||||||
|
400: '#38bdf8',
|
||||||
|
500: '#0ea5e9',
|
||||||
|
600: '#0284c7',
|
||||||
|
700: '#0369a1',
|
||||||
|
800: '#075985',
|
||||||
|
900: '#0c4a6e',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
17
frontend/vite.config.js
Normal file
17
frontend/vite.config.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
host: '0.0.0.0',
|
||||||
|
port: 5173,
|
||||||
|
watch: {
|
||||||
|
usePolling: true,
|
||||||
|
},
|
||||||
|
hmr: {
|
||||||
|
clientPort: 8080,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
59
nginx/conf.d/default.conf
Normal file
59
nginx/conf.d/default.conf
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
upstream frontend {
|
||||||
|
server frontend:5173;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Backend będzie dodany później
|
||||||
|
# upstream backend {
|
||||||
|
# server backend:3000;
|
||||||
|
# }
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
client_max_body_size 500M; # Dla dużych plików wideo
|
||||||
|
|
||||||
|
# Frontend - Vite Dev Server
|
||||||
|
location / {
|
||||||
|
proxy_pass http://frontend;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
|
||||||
|
# WebSocket support (dla Vite HMR)
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
|
||||||
|
# Standard proxy headers
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
# Timeouts
|
||||||
|
proxy_connect_timeout 60s;
|
||||||
|
proxy_send_timeout 60s;
|
||||||
|
proxy_read_timeout 60s;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Backend API (do dodania później)
|
||||||
|
# location /api {
|
||||||
|
# proxy_pass http://backend;
|
||||||
|
# proxy_http_version 1.1;
|
||||||
|
#
|
||||||
|
# proxy_set_header Host $host;
|
||||||
|
# proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
# proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
# }
|
||||||
|
|
||||||
|
# WebSocket dla Socket.IO (do dodania później)
|
||||||
|
# location /socket.io {
|
||||||
|
# proxy_pass http://backend;
|
||||||
|
# proxy_http_version 1.1;
|
||||||
|
#
|
||||||
|
# proxy_set_header Upgrade $http_upgrade;
|
||||||
|
# proxy_set_header Connection "upgrade";
|
||||||
|
# proxy_set_header Host $host;
|
||||||
|
# proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
# }
|
||||||
|
}
|
||||||
36
nginx/nginx.conf
Normal file
36
nginx/nginx.conf
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
user nginx;
|
||||||
|
worker_processes auto;
|
||||||
|
error_log /var/log/nginx/error.log warn;
|
||||||
|
pid /var/run/nginx.pid;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||||
|
'$status $body_bytes_sent "$http_referer" '
|
||||||
|
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||||
|
|
||||||
|
access_log /var/log/nginx/access.log main;
|
||||||
|
|
||||||
|
sendfile on;
|
||||||
|
tcp_nopush on;
|
||||||
|
tcp_nodelay on;
|
||||||
|
keepalive_timeout 65;
|
||||||
|
types_hash_max_size 2048;
|
||||||
|
|
||||||
|
gzip on;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_proxied any;
|
||||||
|
gzip_comp_level 6;
|
||||||
|
gzip_types text/plain text/css text/xml text/javascript
|
||||||
|
application/json application/javascript application/xml+rss
|
||||||
|
application/rss+xml font/truetype font/opentype
|
||||||
|
application/vnd.ms-fontobject image/svg+xml;
|
||||||
|
|
||||||
|
include /etc/nginx/conf.d/*.conf;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user