diff --git a/backend/.env.development.example b/backend/.env.development.example index fc210b7..24ee6f0 100644 --- a/backend/.env.development.example +++ b/backend/.env.development.example @@ -67,3 +67,7 @@ TURNSTILE_SECRET_KEY=your-secret-key-here # Get your credentials from: https://dash.cloudflare.com/ -> Calls -> TURN CLOUDFLARE_TURN_TOKEN_ID=your-turn-token-id-here CLOUDFLARE_TURN_API_TOKEN=your-turn-api-token-here + +# Beta Testing +# Auto-assign SUPPORTER tier to new registrations during beta +BETA_AUTO_SUPPORTER=false diff --git a/backend/.env.production.example b/backend/.env.production.example index 8db49d8..b1b29e9 100644 --- a/backend/.env.production.example +++ b/backend/.env.production.example @@ -62,3 +62,12 @@ MATCHING_MIN_INTERVAL_SEC=120 # Cloudflare Turnstile (CAPTCHA) # Get your secret key from: https://dash.cloudflare.com/ TURNSTILE_SECRET_KEY=your-production-secret-key-here + +# Cloudflare TURN/STUN +# Get your credentials from: https://dash.cloudflare.com/ -> Calls -> TURN +CLOUDFLARE_TURN_TOKEN_ID=your-production-turn-token-id-here +CLOUDFLARE_TURN_API_TOKEN=your-production-turn-api-token-here + +# Beta Testing +# Auto-assign SUPPORTER tier to new registrations during beta +BETA_AUTO_SUPPORTER=false diff --git a/backend/src/controllers/auth.js b/backend/src/controllers/auth.js index 84e05b3..3e8ea2d 100644 --- a/backend/src/controllers/auth.js +++ b/backend/src/controllers/auth.js @@ -81,6 +81,10 @@ async function register(req, res, next) { ? `${firstName} ${lastName}` : username; + // Check if beta auto-supporter is enabled + const betaAutoSupporter = process.env.BETA_AUTO_SUPPORTER === 'true'; + const accountTier = betaAutoSupporter ? 'SUPPORTER' : 'BASIC'; + // Create user const user = await prisma.user.create({ data: { @@ -94,6 +98,7 @@ async function register(req, res, next) { verificationCode, verificationTokenExpiry, emailVerified: false, + accountTier, avatar: `https://ui-avatars.com/api/?name=${encodeURIComponent(displayName)}&background=6366f1&color=fff`, }, select: { @@ -104,6 +109,7 @@ async function register(req, res, next) { lastName: true, wsdcId: true, emailVerified: true, + accountTier: true, avatar: true, createdAt: true, }, diff --git a/backend/src/middleware/auth.js b/backend/src/middleware/auth.js index 3a1db2b..e7dc478 100644 --- a/backend/src/middleware/auth.js +++ b/backend/src/middleware/auth.js @@ -39,6 +39,7 @@ async function authenticate(req, res, next) { firstName: true, lastName: true, wsdcId: true, + accountTier: true, avatar: true, createdAt: true, updatedAt: true, diff --git a/backend/src/routes/users.js b/backend/src/routes/users.js index 109a300..390a925 100644 --- a/backend/src/routes/users.js +++ b/backend/src/routes/users.js @@ -94,6 +94,7 @@ router.get('/:username', async (req, res, next) => { firstName: true, lastName: true, wsdcId: true, + accountTier: true, youtubeUrl: true, instagramUrl: true, facebookUrl: true, diff --git a/docker-compose.yml b/docker-compose.yml index 9539543..d8a47ac 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -60,11 +60,11 @@ services: volumes: - ./frontend:/app - /app/node_modules + env_file: + - ./frontend/.env.development environment: - NODE_ENV=development - VITE_HOST=0.0.0.0 - - VITE_ALLOWED_HOSTS=${VITE_ALLOWED_HOSTS:-all} - - VITE_TURNSTILE_SITE_KEY=${VITE_TURNSTILE_SITE_KEY} stdin_open: true tty: true command: npm run dev diff --git a/frontend/.env.development b/frontend/.env.development new file mode 100644 index 0000000..bb8122f --- /dev/null +++ b/frontend/.env.development @@ -0,0 +1,24 @@ +# Frontend - Vite Allowed Hosts +# Comma-separated list of allowed hostnames +# Use 'all' to allow all hosts (NOT recommended for production) +VITE_ALLOWED_HOSTS=localhost,spotlight.cam,.spotlight.cam + +# Alternative: Allow all hosts (development only) +# VITE_ALLOWED_HOSTS=all + +# Cloudflare Turnstile (CAPTCHA) +# Get your keys from: https://dash.cloudflare.com/ +VITE_TURNSTILE_SITE_KEY=0x4AAAAAACE_ByYyzwZKe5sC + +# Google Analytics 4 +# Format: G-XXXXXXXXXX +# Get your measurement ID from: https://analytics.google.com/ +VITE_GA_MEASUREMENT_ID= + +# Beta Testing Features +# Set to 'true' to show beta banner and enable beta features +VITE_BETA_MODE=true + +# Auto-assign SUPPORTER tier to new registrations during beta +# Set to 'true' to automatically grant SUPPORTER tier to new users +VITE_BETA_AUTO_SUPPORTER=true diff --git a/.env.example b/frontend/.env.development.example similarity index 66% rename from .env.example rename to frontend/.env.development.example index 0e21018..357d3bd 100644 --- a/.env.example +++ b/frontend/.env.development.example @@ -14,3 +14,11 @@ VITE_TURNSTILE_SITE_KEY=your-site-key-here # Format: G-XXXXXXXXXX # Get your measurement ID from: https://analytics.google.com/ VITE_GA_MEASUREMENT_ID= + +# Beta Testing Features +# Set to 'true' to show beta banner and enable beta features +VITE_BETA_MODE=false + +# Auto-assign SUPPORTER tier to new registrations during beta +# Set to 'true' to automatically grant SUPPORTER tier to new users +VITE_BETA_AUTO_SUPPORTER=false diff --git a/frontend/.env.production.example b/frontend/.env.production.example new file mode 100644 index 0000000..e713fb9 --- /dev/null +++ b/frontend/.env.production.example @@ -0,0 +1,20 @@ +# Frontend - Vite Allowed Hosts +# Comma-separated list of allowed hostnames +VITE_ALLOWED_HOSTS=spotlight.cam,.spotlight.cam + +# Cloudflare Turnstile (CAPTCHA) +# Get your keys from: https://dash.cloudflare.com/ +VITE_TURNSTILE_SITE_KEY=your-production-site-key-here + +# Google Analytics 4 +# Format: G-XXXXXXXXXX +# Get your measurement ID from: https://analytics.google.com/ +VITE_GA_MEASUREMENT_ID= + +# Beta Testing Features +# Set to 'true' to show beta banner and enable beta features +VITE_BETA_MODE=false + +# Auto-assign SUPPORTER tier to new registrations during beta +# Set to 'true' to automatically grant SUPPORTER tier to new users +VITE_BETA_AUTO_SUPPORTER=false diff --git a/frontend/public/content/about-us.html b/frontend/public/content/about-us.html new file mode 100644 index 0000000..80d81c4 --- /dev/null +++ b/frontend/public/content/about-us.html @@ -0,0 +1,108 @@ + + +
+

About Us

+ +

+ Hi, I'm Radek – a software engineer, a West Coast Swing dancer and the person behind spotlight.cam. +

+ +

+ Spotlight.cam is a project built by someone who actually stands in the same registration lines, dances in the same heats and scrolls through the same event pages as you :P +

+ +

+ If we ever meet at an event, I'll probably be somewhere near the dance floor, probably pressing "record" on someone's spotlight. +

+ +

+ To be fair, the original idea for this service was actually dropped on me by a friend who was tired of hunting for someone to film her dances – I just did what any backend developer / DevOps / Linux admin would do: said "okay, that shouldn't be that hard… right?", opened my editor, set up a few servers and scripts… and suddenly we had a new project on our hands 😅 I also had a not-so-secret teammate: AI. Without it, this would probably still be stuck in my "one day" folder for about a year 😄 +

+ +

The Vision

+ +

+ Our goal is to make it easier for dancers to capture and share their competition moments. No more running around looking for someone with a free hand to record your heat. No more missing your own spotlight because you were recording someone else's. +

+ +

+ We're building a platform that connects dancers who want to help each other out, making the whole process smoother, fairer, and more organized. +

+ +

Technology

+ +

+ Under the hood, spotlight.cam uses modern web technologies to deliver a fast, secure, and reliable experience: +

+ + + + +
diff --git a/frontend/public/content/about-us.md b/frontend/public/content/about-us.md deleted file mode 100644 index 01c4287..0000000 --- a/frontend/public/content/about-us.md +++ /dev/null @@ -1,33 +0,0 @@ -Hi, I'm Radek – a software engineer, a West Coast Swing dancer and the person behind **spotlight.cam**. - - -Spotlight.cam is a project built by someone who actually stands in the same registration lines, dances in the same heats and scrolls through the same event pages as you :P - - -If we ever meet at an event, I'll probably be somewhere near the dance floor, probably pressing "record" on someone's spotlight. - -To be fair, the original idea for this service was actually dropped on me by a friend who was tired of hunting for someone to film her dances – I just did what any backend developer / DevOps / Linux admin would do: said "okay, that shouldn't be that hard… right?", opened my editor, set up a few servers and scripts… and suddenly we had a new project on our hands 😅 I also had a not-so-secret teammate: AI. Without it, this would probably still be stuck in my "one day" folder for about a year 😄 - ---- - -## Privacy & Cookies - -We use cookies and similar technologies to provide you with a better experience. Here's what you should know: - -### What cookies do we use? - -- **Essential cookies**: Required for authentication and core functionality (login sessions, security) -- **Analytics**: To understand how people use the platform and improve it -- **Preferences**: To remember your settings and preferences - -### Your data - -We respect your privacy and comply with GDPR/RODO regulations. We: -- Only collect data necessary for the service to function -- Never sell your personal information to third parties -- Store your data securely -- Allow you to delete your account and data at any time - -### Contact - -If you have any questions about privacy or cookies, feel free to reach out through our [contact page](/contact). \ No newline at end of file diff --git a/frontend/public/content/how-it-works.html b/frontend/public/content/how-it-works.html new file mode 100644 index 0000000..f1fada7 --- /dev/null +++ b/frontend/public/content/how-it-works.html @@ -0,0 +1,120 @@ + + +
+

How It Works

+ +

Lorem Ipsum

+ +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. +

+ +

Ut Enim Ad Minim

+ +

+ Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat: +

+ +
    +
  1. Duis aute irure - Dolor in reprehenderit in voluptate velit esse cillum dolore
  2. +
  3. Eu fugiat nulla - Pariatur excepteur sint occaecat cupidatat non proident
  4. +
  5. Sunt in culpa - Qui officia deserunt mollit anim id est laborum
  6. +
+ +

Sed Ut Perspiciatis

+ +

+ Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. +

+ +

Nemo Enim Ipsam

+ +

+ Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. +

+ +

Neque Porro Quisquam

+ +

+ Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. +

+ +
+ + +
diff --git a/frontend/public/content/how-it-works.md b/frontend/public/content/how-it-works.md deleted file mode 100644 index 294717f..0000000 --- a/frontend/public/content/how-it-works.md +++ /dev/null @@ -1,29 +0,0 @@ -# How It Works - -## Lorem Ipsum - -Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. - -## Ut Enim Ad Minim - -Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat: - -1. **Duis aute irure** - Dolor in reprehenderit in voluptate velit esse cillum dolore -2. **Eu fugiat nulla** - Pariatur excepteur sint occaecat cupidatat non proident -3. **Sunt in culpa** - Qui officia deserunt mollit anim id est laborum - -## Sed Ut Perspiciatis - -Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. - -## Nemo Enim Ipsam - -Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. - -### Neque Porro Quisquam - -Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. - ---- - -*For questions or feedback, [contact us](/contact).* diff --git a/frontend/public/content/privacy.html b/frontend/public/content/privacy.html new file mode 100644 index 0000000..0688744 --- /dev/null +++ b/frontend/public/content/privacy.html @@ -0,0 +1,240 @@ + + +
+

Privacy Policy & Cookie Policy

+ +

Last updated: December 2025

+ +

+ At spotlight.cam, we respect your privacy and are committed to protecting your personal data. + This policy explains how we collect, use, and safeguard your information in compliance with GDPR/RODO regulations. +

+ +

Information We Collect

+ +

Account Information

+ + +

Usage Data

+ + +

Communication Data

+ + +

Cookies We Use

+ +

+ We use cookies and similar technologies to provide you with a better experience. Here's what cookies we use: +

+ +

Essential Cookies (Always Active)

+ + +

These cookies are necessary for the platform to function and cannot be disabled.

+ +

Analytics Cookies (Optional)

+ + +

These cookies are only activated after you accept them via the cookie consent banner.

+ +

How We Use Your Data

+ +

We use your personal data for the following purposes:

+ + + +

Data Sharing & Third Parties

+ +

We respect your privacy. Here's what we do and don't do with your data:

+ +

We DO:

+ + +

We DON'T:

+ + +

Data Security

+ +

We implement industry-standard security measures to protect your data:

+ + + +

Your Rights (GDPR/RODO)

+ +

Under GDPR/RODO, you have the following rights:

+ + + +

+ To exercise any of these rights, please contact us. +

+ +

Data Retention

+ + + +

Children's Privacy

+ +

+ Our service is not intended for users under the age of 16. We do not knowingly collect personal data from children. + If you believe a child has provided us with personal data, please contact us immediately. +

+ +

Changes to This Policy

+ +

+ We may update this Privacy Policy from time to time. We will notify users of significant changes via email or + prominent notice on the platform. The "Last updated" date at the top of this page shows when the policy was last revised. +

+ +

Contact Us

+ +

+ If you have any questions, concerns, or requests regarding this Privacy Policy or your personal data, + please contact us through our contact page. +

+ +
+ +

+ spotlight.cam - Dance Event Video Exchange Platform
+ Built with privacy and security in mind. 🔒 +

+
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 7895e3a..f51a1df 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -26,10 +26,12 @@ import ContactMessagesPage from './pages/admin/ContactMessagesPage'; import ContactPage from './pages/ContactPage'; import AboutUsPage from './pages/AboutUsPage'; import HowItWorksPage from './pages/HowItWorksPage'; +import PrivacyPage from './pages/PrivacyPage'; import NotFoundPage from './pages/NotFoundPage'; import VerificationBanner from './components/common/VerificationBanner'; import InstallPWA from './components/pwa/InstallPWA'; import CookieConsent from './components/common/CookieConsent'; +import BetaBanner from './components/BetaBanner'; // Protected Route Component with Verification Banner const ProtectedRoute = ({ children }) => { @@ -98,6 +100,9 @@ function App() { + {/* Beta Testing Banner */} + + {/* PWA Install Prompt */} @@ -263,6 +268,9 @@ function App() { {/* How It Works Page - Public route */} } /> + {/* Privacy Policy Page - Public route */} + } /> + {/* Public Profile - /u/username format (no auth required) */} } /> diff --git a/frontend/src/components/BetaBanner.jsx b/frontend/src/components/BetaBanner.jsx new file mode 100644 index 0000000..b796743 --- /dev/null +++ b/frontend/src/components/BetaBanner.jsx @@ -0,0 +1,63 @@ +import { useState, useEffect } from 'react'; +import { X } from 'lucide-react'; + +const BetaBanner = () => { + const [isVisible, setIsVisible] = useState(false); + const [isClosing, setIsClosing] = useState(false); + const betaMode = import.meta.env.VITE_BETA_MODE === 'true'; + + useEffect(() => { + if (!betaMode) { + setIsVisible(false); + return; + } + + // Check if banner was dismissed + const dismissed = localStorage.getItem('betaBannerDismissed'); + if (!dismissed) { + setIsVisible(true); + } + }, [betaMode]); + + const handleDismiss = () => { + setIsClosing(true); + setTimeout(() => { + setIsVisible(false); + localStorage.setItem('betaBannerDismissed', 'true'); + }, 300); // Match animation duration + }; + + if (!isVisible || !betaMode) { + return null; + } + + return ( +
+
+
+ + Beta + +

+ Welcome to our beta testing! + Help us improve by reporting any issues or suggestions. + All beta testers get SUPPORTER status! 🎉 +

+
+ +
+
+ ); +}; + +export default BetaBanner; diff --git a/frontend/src/components/HtmlContentPage.jsx b/frontend/src/components/HtmlContentPage.jsx new file mode 100644 index 0000000..bfd0716 --- /dev/null +++ b/frontend/src/components/HtmlContentPage.jsx @@ -0,0 +1,70 @@ +import { useState, useEffect } from 'react'; +import PublicLayout from './layout/PublicLayout'; +import { Loader2 } from 'lucide-react'; + +/** + * Generic component for rendering HTML content from public/content/*.html files + * Supports inline CSS and full HTML structure + */ +export default function HtmlContentPage({ contentFile, pageTitle = 'Page' }) { + const [content, setContent] = useState(''); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + + useEffect(() => { + // Fetch HTML content + fetch(`/content/${contentFile}`) + .then(response => { + if (!response.ok) { + throw new Error('Failed to load content'); + } + return response.text(); + }) + .then(html => { + setContent(html); + setLoading(false); + }) + .catch(err => { + console.error(`Error loading ${contentFile}:`, err); + setError('Failed to load page content'); + setLoading(false); + }); + }, [contentFile]); + + if (loading) { + return ( + +
+ +
+
+ ); + } + + if (error) { + return ( + +
+
+

{error}

+

Please try again later.

+
+
+
+ ); + } + + return ( + +
+
+ {/* Render raw HTML with inline styles */} +
+
+
+ + ); +} diff --git a/frontend/src/components/common/CookieConsent.jsx b/frontend/src/components/common/CookieConsent.jsx index a82f3e7..5d226e1 100644 --- a/frontend/src/components/common/CookieConsent.jsx +++ b/frontend/src/components/common/CookieConsent.jsx @@ -57,12 +57,12 @@ const CookieConsent = () => { We use cookies and similar technologies to enhance your experience, analyze site traffic, and for authentication purposes. By clicking "Accept", you consent to our use of cookies.{' '} - Learn more + Privacy Policy

diff --git a/frontend/src/components/common/TierBadge.jsx b/frontend/src/components/common/TierBadge.jsx new file mode 100644 index 0000000..c89755e --- /dev/null +++ b/frontend/src/components/common/TierBadge.jsx @@ -0,0 +1,71 @@ +import { Crown, Sparkles, User } from 'lucide-react'; + +const TierBadge = ({ tier, size = 'sm', showLabel = false }) => { + if (!tier || tier === 'BASIC') { + return null; // Don't show badge for BASIC tier + } + + const configs = { + SUPPORTER: { + icon: Sparkles, + label: 'Supporter', + bgColor: 'bg-blue-100', + textColor: 'text-blue-700', + borderColor: 'border-blue-300', + iconColor: 'text-blue-600', + }, + COMFORT: { + icon: Crown, + label: 'Comfort', + bgColor: 'bg-purple-100', + textColor: 'text-purple-700', + borderColor: 'border-purple-300', + iconColor: 'text-purple-600', + }, + }; + + const config = configs[tier]; + if (!config) return null; + + const sizeClasses = { + xs: { + container: 'px-1.5 py-0.5 text-xs', + icon: 'w-3 h-3', + }, + sm: { + container: 'px-2 py-1 text-xs', + icon: 'w-3.5 h-3.5', + }, + md: { + container: 'px-2.5 py-1.5 text-sm', + icon: 'w-4 h-4', + }, + }; + + const sizes = sizeClasses[size] || sizeClasses.sm; + const Icon = config.icon; + + if (!showLabel) { + // Icon only - for compact display + return ( + + + + ); + } + + // Full badge with label + return ( + + + {config.label} + + ); +}; + +export default TierBadge; diff --git a/frontend/src/components/layout/Navbar.jsx b/frontend/src/components/layout/Navbar.jsx index 07dc40f..d3e52b3 100644 --- a/frontend/src/components/layout/Navbar.jsx +++ b/frontend/src/components/layout/Navbar.jsx @@ -2,6 +2,7 @@ import { Link, useNavigate } from 'react-router-dom'; import { useAuth } from '../../contexts/AuthContext'; import { Video, LogOut, User, History, Users, Menu, X, LayoutDashboard, Calendar, Shield, Mail, Activity, ChevronDown } from 'lucide-react'; import Avatar from '../common/Avatar'; +import TierBadge from '../common/TierBadge'; import { useState, useEffect, useRef } from 'react'; import { matchesAPI } from '../../services/api'; import { connectSocket, disconnectSocket, getSocket } from '../../services/socket'; @@ -178,7 +179,10 @@ const Navbar = ({ pageTitle = null }) => { className="flex items-center space-x-3 px-3 py-2 rounded-md hover:bg-gray-100" > - {user.username} +
+ {user.username} + +