feat(beta): add beta testing features and privacy policy page

Implemented comprehensive beta testing system with tier badges and
reorganized environment configuration for better maintainability.

Beta Testing Features:
- Beta banner component with dismissible state (localStorage)
- Auto-assign SUPPORTER tier to new registrations (env controlled)
- TierBadge component with SUPPORTER/COMFORT tier display
- Badge shown in Navbar, ProfilePage, and PublicProfilePage
- Environment variables: VITE_BETA_MODE, BETA_AUTO_SUPPORTER

Environment Configuration Reorganization:
- Moved .env files from root to frontend/ and backend/ directories
- Created .env.{development,production}{,.example} structure
- Updated docker-compose.yml to use env_file for frontend
- All env vars properly namespaced and documented

Privacy Policy Implementation:
- New /privacy route with dedicated PrivacyPage component
- Comprehensive GDPR/RODO compliant privacy policy (privacy.html)
- Updated CookieConsent banner to link to /privacy
- Added Privacy Policy links to all footers (HomePage, PublicFooter)
- Removed privacy section from About Us page

HTML Content System:
- Replaced react-markdown dependency with simple HTML loader
- New HtmlContentPage component for rendering .html files
- Converted about-us.md and how-it-works.md to .html format
- Inline CSS support for full styling control
- Easier content editing without React knowledge

Backend Changes:
- Registration auto-assigns SUPPORTER tier when BETA_AUTO_SUPPORTER=true
- Added accountTier to auth middleware and user routes
- Updated public profile endpoint to include accountTier

Files:
- Added: frontend/.env.{development,production}{,.example}
- Added: backend/.env variables for BETA_AUTO_SUPPORTER
- Added: components/BetaBanner.jsx, TierBadge.jsx, HtmlContentPage.jsx
- Added: pages/PrivacyPage.jsx
- Added: public/content/{about-us,how-it-works,privacy}.html
- Modified: docker-compose.yml (env_file configuration)
- Modified: App.jsx (privacy route, beta banner)
- Modified: auth.js (auto SUPPORTER tier logic)
This commit is contained in:
Radosław Gierwiało
2025-12-06 11:50:28 +01:00
parent a786b1d92d
commit e2b10387c2
28 changed files with 841 additions and 251 deletions

View File

@@ -1,91 +1,5 @@
import { useState, useEffect } from 'react';
import ReactMarkdown from 'react-markdown';
import PublicLayout from '../components/layout/PublicLayout';
import HtmlContentPage from '../components/HtmlContentPage';
export default function AboutUsPage() {
const [content, setContent] = useState('');
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
useEffect(() => {
// Fetch markdown content
fetch('/content/about-us.md')
.then(response => {
if (!response.ok) {
throw new Error('Failed to load content');
}
return response.text();
})
.then(text => {
setContent(text);
setLoading(false);
})
.catch(err => {
console.error('Error loading about-us content:', err);
setError('Failed to load page content');
setLoading(false);
});
}, []);
if (loading) {
return (
<PublicLayout>
<div className="bg-gray-50 flex items-center justify-center py-12">
<div className="text-center">
<div className="w-12 h-12 border-4 border-primary-600 border-t-transparent rounded-full animate-spin mx-auto mb-4"></div>
<p className="text-gray-600">Loading...</p>
</div>
</div>
</PublicLayout>
);
}
if (error) {
return (
<PublicLayout>
<div className="bg-gray-50 flex items-center justify-center py-12">
<div className="text-center">
<p className="text-red-600">{error}</p>
</div>
</div>
</PublicLayout>
);
}
return (
<PublicLayout>
<div className="bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-4xl mx-auto">
<article className="bg-white rounded-lg shadow-sm p-8 md:p-12 prose prose-lg max-w-none">
<ReactMarkdown
components={{
// Customize link rendering to use React Router for internal links
a: ({ node, href, children, ...props }) => {
const isInternal = href && href.startsWith('/');
if (isInternal) {
return (
<a href={href} {...props}>
{children}
</a>
);
}
return (
<a href={href} target="_blank" rel="noopener noreferrer" {...props}>
{children}
</a>
);
},
// Ensure images are responsive
img: ({ node, ...props }) => (
<img className="rounded-lg shadow-md" {...props} alt={props.alt || ''} />
),
}}
>
{content}
</ReactMarkdown>
</article>
</div>
</div>
</PublicLayout>
);
return <HtmlContentPage contentFile="about-us.html" pageTitle="About Us" />;
}

View File

@@ -284,6 +284,11 @@ const HomePage = () => {
Contact Us
</Link>
</li>
<li>
<Link to="/privacy" className="hover:text-primary-400 transition">
Privacy Policy
</Link>
</li>
</ul>
</div>

View File

@@ -1,91 +1,5 @@
import { useState, useEffect } from 'react';
import ReactMarkdown from 'react-markdown';
import PublicLayout from '../components/layout/PublicLayout';
import HtmlContentPage from '../components/HtmlContentPage';
export default function HowItWorksPage() {
const [content, setContent] = useState('');
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
useEffect(() => {
// Fetch markdown content
fetch('/content/how-it-works.md')
.then(response => {
if (!response.ok) {
throw new Error('Failed to load content');
}
return response.text();
})
.then(text => {
setContent(text);
setLoading(false);
})
.catch(err => {
console.error('Error loading how-it-works content:', err);
setError('Failed to load page content');
setLoading(false);
});
}, []);
if (loading) {
return (
<PublicLayout>
<div className="bg-gray-50 flex items-center justify-center py-12">
<div className="text-center">
<div className="w-12 h-12 border-4 border-primary-600 border-t-transparent rounded-full animate-spin mx-auto mb-4"></div>
<p className="text-gray-600">Loading...</p>
</div>
</div>
</PublicLayout>
);
}
if (error) {
return (
<PublicLayout>
<div className="bg-gray-50 flex items-center justify-center py-12">
<div className="text-center">
<p className="text-red-600">{error}</p>
</div>
</div>
</PublicLayout>
);
}
return (
<PublicLayout>
<div className="bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-4xl mx-auto">
<article className="bg-white rounded-lg shadow-sm p-8 md:p-12 prose prose-lg max-w-none">
<ReactMarkdown
components={{
// Customize link rendering to use React Router for internal links
a: ({ node, href, children, ...props }) => {
const isInternal = href && href.startsWith('/');
if (isInternal) {
return (
<a href={href} {...props}>
{children}
</a>
);
}
return (
<a href={href} target="_blank" rel="noopener noreferrer" {...props}>
{children}
</a>
);
},
// Ensure images are responsive
img: ({ node, ...props }) => (
<img className="rounded-lg shadow-md" {...props} alt={props.alt || ''} />
),
}}
>
{content}
</ReactMarkdown>
</article>
</div>
</div>
</PublicLayout>
);
return <HtmlContentPage contentFile="how-it-works.html" pageTitle="How It Works" />;
}

View File

@@ -0,0 +1,5 @@
import HtmlContentPage from '../components/HtmlContentPage';
export default function PrivacyPage() {
return <HtmlContentPage contentFile="privacy.html" pageTitle="Privacy Policy" />;
}

View File

@@ -2,6 +2,7 @@ import { useState } from 'react';
import { useAuth } from '../contexts/AuthContext';
import Layout from '../components/layout/Layout';
import Avatar from '../components/common/Avatar';
import TierBadge from '../components/common/TierBadge';
import { ProfileForm, PasswordChangeForm } from '../components/profile';
import { User, Lock } from 'lucide-react';
@@ -25,9 +26,12 @@ const ProfilePage = () => {
title={user?.username}
/>
<div>
<h1 className="text-2xl font-bold text-gray-900">
{user?.firstName || user?.username}
</h1>
<div className="flex items-center gap-3">
<h1 className="text-2xl font-bold text-gray-900">
{user?.firstName || user?.username}
</h1>
<TierBadge tier={user?.accountTier} size="md" showLabel={true} />
</div>
<p className="text-gray-600">@{user?.username}</p>
{!user?.emailVerified && (
<p className="text-sm text-yellow-600 mt-1">

View File

@@ -4,6 +4,7 @@ import { authAPI, ratingsAPI } from '../services/api';
import Layout from '../components/layout/Layout';
import { User, MapPin, Globe, Hash, Youtube, Instagram, Facebook, Award, Star, Calendar, Loader2, ThumbsUp } from 'lucide-react';
import Avatar from '../components/common/Avatar';
import TierBadge from '../components/common/TierBadge';
import NotFoundPage from './NotFoundPage';
const PublicProfilePage = () => {
@@ -80,11 +81,14 @@ const PublicProfilePage = () => {
title={profile.username}
/>
<div className="flex-1 min-w-0">
<h1 className="text-3xl font-bold text-gray-900 mb-1">
{profile.firstName && profile.lastName
? `${profile.firstName} ${profile.lastName}`
: profile.username}
</h1>
<div className="flex items-center gap-3 mb-1">
<h1 className="text-3xl font-bold text-gray-900">
{profile.firstName && profile.lastName
? `${profile.firstName} ${profile.lastName}`
: profile.username}
</h1>
<TierBadge tier={profile.accountTier} size="md" showLabel={true} />
</div>
<p className="text-lg text-gray-600 mb-4">@{profile.username}</p>
{/* Location */}