feat: visual polish, nav login link, pricing badge fix, cursor fix, button contrast

- Hero mockup: enhanced 3D perspective and shadow
- Testimonials: illustrated SVG avatars
- Growth pricing card: visual prominence (scale, gradient, badge)
- Most Popular badge: repositioned to avoid overlapping heading
- Nav: added Log In link next to Start Free Trial
- Fixed btn-primary text colour on anchor tags (white on blue)
- Fixed cursor: default on all non-interactive elements
- Disabled user-select on non-form content to prevent text caret
This commit is contained in:
Peter Foster
2026-02-14 14:17:15 +00:00
parent d431d0fcfa
commit f969ecae04
69 changed files with 23884 additions and 471 deletions

View File

@@ -0,0 +1,750 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="TenderRadar - Alert History">
<title>Alert History | TenderRadar</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="icon" href="/favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="styles.css">
<style>
/* Alerts Page Specific Styles */
.alerts-header {
padding: 2rem 0;
border-bottom: 1px solid var(--border);
margin-bottom: 2rem;
}
.alerts-header h1 {
font-size: 2rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 0.5rem;
}
.alerts-header p {
color: var(--text-secondary);
font-size: 1rem;
}
.alerts-controls {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
padding: 1.5rem;
background: var(--bg-secondary);
border-radius: 0.75rem;
}
.control-group {
display: flex;
flex-direction: column;
}
.control-group label {
font-weight: 600;
margin-bottom: 0.5rem;
color: var(--text-primary);
font-size: 0.875rem;
}
.control-group input,
.control-group select {
padding: 0.625rem 0.75rem;
border: 1px solid var(--border);
border-radius: 0.375rem;
font-family: inherit;
font-size: 0.875rem;
}
.control-group input:focus,
.control-group select:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(30, 64, 175, 0.1);
}
.filter-actions {
display: flex;
gap: 1rem;
margin-bottom: 2rem;
}
.filter-actions button {
padding: 0.625rem 1.5rem;
border-radius: 0.375rem;
font-weight: 600;
font-size: 0.875rem;
border: none;
cursor: pointer;
transition: all 0.2s;
}
.btn-filter {
background: var(--primary);
color: white;
}
.btn-filter:hover {
background: var(--primary-dark);
transform: translateY(-1px);
}
.btn-clear {
background: var(--bg-alt);
color: var(--text-primary);
border: 1px solid var(--border);
}
.btn-clear:hover {
background: var(--border);
}
/* Alerts Table/List */
.alerts-container {
background: white;
border-radius: 0.75rem;
box-shadow: var(--shadow-sm);
border: 1px solid var(--border);
overflow: hidden;
}
.alerts-table {
width: 100%;
border-collapse: collapse;
font-size: 0.9375rem;
}
.alerts-table thead {
background: var(--bg-secondary);
border-bottom: 1px solid var(--border);
}
.alerts-table th {
padding: 1rem 1.5rem;
text-align: left;
font-weight: 600;
color: var(--text-primary);
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.alerts-table td {
padding: 1.25rem 1.5rem;
border-bottom: 1px solid var(--border);
color: var(--text-secondary);
}
.alerts-table tr:hover {
background: var(--bg-secondary);
}
.alert-title {
font-weight: 600;
color: var(--text-primary);
margin-bottom: 0.25rem;
}
.alert-date {
font-size: 0.8125rem;
color: var(--text-light);
}
.match-score {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
background: rgba(30, 64, 175, 0.1);
color: var(--primary);
border-radius: 0.375rem;
font-weight: 600;
font-size: 0.875rem;
}
.match-score.high {
background: rgba(34, 197, 94, 0.1);
color: #15803d;
}
.match-score.medium {
background: rgba(245, 158, 11, 0.1);
color: #92400e;
}
.match-score.low {
background: rgba(239, 68, 68, 0.1);
color: #7f1d1d;
}
.status-badge {
display: inline-block;
padding: 0.375rem 0.75rem;
border-radius: 0.25rem;
font-weight: 600;
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.status-new {
background: rgba(59, 130, 246, 0.1);
color: #1e40af;
}
.status-viewed {
background: rgba(156, 163, 175, 0.1);
color: #4b5563;
}
.status-saved {
background: rgba(245, 158, 11, 0.1);
color: #92400e;
}
.status-applied {
background: rgba(34, 197, 94, 0.1);
color: #15803d;
}
.alert-actions {
display: flex;
gap: 0.5rem;
}
.action-btn {
padding: 0.375rem 0.75rem;
border: 1px solid var(--border);
background: white;
border-radius: 0.375rem;
font-size: 0.75rem;
cursor: pointer;
transition: all 0.2s;
}
.action-btn:hover {
background: var(--bg-alt);
border-color: var(--primary);
color: var(--primary);
}
/* Empty State */
.empty-state {
text-align: center;
padding: 4rem 2rem;
color: var(--text-secondary);
}
.empty-state-icon {
font-size: 3rem;
margin-bottom: 1rem;
opacity: 0.5;
}
.empty-state h3 {
font-size: 1.25rem;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 0.5rem;
}
.empty-state p {
margin-bottom: 2rem;
}
.empty-state .btn {
display: inline-block;
}
/* Pagination */
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 0.5rem;
padding: 1.5rem;
border-top: 1px solid var(--border);
}
.pagination button,
.pagination a {
padding: 0.5rem 0.75rem;
border: 1px solid var(--border);
background: white;
color: var(--text-primary);
border-radius: 0.375rem;
cursor: pointer;
transition: all 0.2s;
text-decoration: none;
}
.pagination button:hover,
.pagination a:hover {
background: var(--bg-alt);
border-color: var(--primary);
}
.pagination .active {
background: var(--primary);
color: white;
border-color: var(--primary);
}
/* Status Messages */
.alert {
padding: 1rem 1.5rem;
border-radius: 0.5rem;
margin-bottom: 1.5rem;
display: none;
}
.alert.show {
display: block;
}
.alert-success {
background: rgba(34, 197, 94, 0.1);
border: 1px solid rgba(34, 197, 94, 0.3);
color: #15803d;
}
.alert-error {
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.3);
color: #7f1d1d;
}
/* Loading State */
.loading {
display: flex;
justify-content: center;
align-items: center;
padding: 3rem;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid var(--border);
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Mobile Responsive */
@media (max-width: 768px) {
.alerts-controls {
grid-template-columns: 1fr;
}
.alerts-table {
font-size: 0.8125rem;
}
.alerts-table th,
.alerts-table td {
padding: 0.75rem;
}
.alert-actions {
flex-wrap: wrap;
}
.action-btn {
flex: 1;
min-width: 60px;
}
.alerts-header h1 {
font-size: 1.5rem;
}
/* Hide less important columns on mobile */
.col-date-matched {
display: none;
}
.match-score {
font-size: 0.75rem;
}
}
@media (max-width: 480px) {
.alerts-controls {
grid-template-columns: 1fr;
}
.alerts-table th,
.alerts-table td {
padding: 0.5rem;
}
.alert-title {
font-size: 0.875rem;
}
.match-score {
display: block;
width: 100%;
margin: 0.5rem 0;
}
.status-badge {
font-size: 0.65rem;
}
}
</style>
</head>
<body>
<!-- Header/Navigation -->
<header class="header">
<nav class="nav container">
<div class="nav-brand">
<img src="/logo.png" alt="TenderRadar" class="logo-icon">
</div>
<ul class="nav-menu">
<li><a href="/">Dashboard</a></li>
<li><a href="/alerts.html" class="active-nav">Alerts</a></li>
<li><a href="/profile.html">Profile</a></li>
<li><button id="logoutBtn" class="btn btn-outline btn-sm">Logout</button></li>
</ul>
<button class="mobile-toggle" aria-label="Toggle menu">
<span></span>
<span></span>
<span></span>
</button>
</nav>
</header>
<!-- Main Container -->
<div class="container">
<!-- Header -->
<div class="alerts-header">
<h1>Alert History</h1>
<p>View all tenders that matched your preferences</p>
</div>
<!-- Status Messages -->
<div id="successMessage" class="alert alert-success"></div>
<div id="errorMessage" class="alert alert-error"></div>
<!-- Filter Controls -->
<div class="alerts-controls">
<div class="control-group">
<label for="filterFromDate">From Date</label>
<input type="date" id="filterFromDate">
</div>
<div class="control-group">
<label for="filterToDate">To Date</label>
<input type="date" id="filterToDate">
</div>
<div class="control-group">
<label for="filterStatus">Status</label>
<select id="filterStatus">
<option value="">All Statuses</option>
<option value="new">New</option>
<option value="viewed">Viewed</option>
<option value="saved">Saved</option>
<option value="applied">Applied</option>
</select>
</div>
<div class="control-group">
<label for="filterScore">Match Score</label>
<select id="filterScore">
<option value="">All Scores</option>
<option value="high">High (80%+)</option>
<option value="medium">Medium (50-79%)</option>
<option value="low">Low (Below 50%)</option>
</select>
</div>
</div>
<!-- Filter Actions -->
<div class="filter-actions">
<button class="btn-filter" id="applyFiltersBtn">Apply Filters</button>
<button class="btn-clear" id="clearFiltersBtn">Clear Filters</button>
</div>
<!-- Alerts Table -->
<div class="alerts-container">
<div id="alertsContent">
<div class="loading">
<div class="spinner"></div>
</div>
</div>
</div>
</div>
<script>
// Auth and state
let authToken = localStorage.getItem('authToken');
let currentPage = 1;
let filters = {};
// Check authentication
document.addEventListener('DOMContentLoaded', async () => {
if (!authToken) {
window.location.href = '/login.html';
return;
}
// Set default date range (last 90 days)
const today = new Date();
const ninetyDaysAgo = new Date(today.getTime() - 90 * 24 * 60 * 60 * 1000);
document.getElementById('filterToDate').value = today.toISOString().split('T')[0];
document.getElementById('filterFromDate').value = ninetyDaysAgo.toISOString().split('T')[0];
// Load alerts
await loadAlerts();
// Set up event listeners
setupEventListeners();
});
function setupEventListeners() {
document.getElementById('applyFiltersBtn')?.addEventListener('click', applyFilters);
document.getElementById('clearFiltersBtn')?.addEventListener('click', clearFilters);
// Logout
document.getElementById('logoutBtn')?.addEventListener('click', () => {
localStorage.removeItem('authToken');
window.location.href = '/';
});
}
async function loadAlerts() {
try {
const response = await fetch('/api/matches', {
headers: { 'Authorization': `Bearer ${authToken}` }
});
if (!response.ok && response.status === 401) {
localStorage.removeItem('authToken');
window.location.href = '/login.html';
return;
}
if (!response.ok) {
throw new Error('Failed to load alerts');
}
const data = await response.json();
displayAlerts(data.matches || []);
} catch (error) {
console.error('Error loading alerts:', error);
showError('Failed to load alert history');
displayNoAlerts();
}
}
function displayAlerts(alerts) {
const container = document.getElementById('alertsContent');
if (!alerts || alerts.length === 0) {
displayNoAlerts();
return;
}
// Filter alerts based on current filters
let filteredAlerts = alerts;
if (filters.fromDate) {
const fromDate = new Date(filters.fromDate);
filteredAlerts = filteredAlerts.filter(a => new Date(a.created_at) >= fromDate);
}
if (filters.toDate) {
const toDate = new Date(filters.toDate);
toDate.setHours(23, 59, 59);
filteredAlerts = filteredAlerts.filter(a => new Date(a.created_at) <= toDate);
}
if (filters.status) {
filteredAlerts = filteredAlerts.filter(a => (a.status || 'new') === filters.status);
}
if (filters.score) {
filteredAlerts = filteredAlerts.filter(a => {
const score = a.match_score || 0;
if (filters.score === 'high') return score >= 80;
if (filters.score === 'medium') return score >= 50 && score < 80;
if (filters.score === 'low') return score < 50;
return true;
});
}
if (filteredAlerts.length === 0) {
displayNoAlerts();
return;
}
// Sort by date (newest first)
filteredAlerts.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
const html = `
<table class="alerts-table">
<thead>
<tr>
<th>Tender Title</th>
<th class="col-date-matched">Date Matched</th>
<th>Match Score</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
${filteredAlerts.map(alert => renderAlertRow(alert)).join('')}
</tbody>
</table>
<div class="pagination">
<span>Showing ${filteredAlerts.length} of ${alerts.length} tenders</span>
</div>
`;
container.innerHTML = html;
attachActionListeners(filteredAlerts);
}
function renderAlertRow(alert) {
const matchScore = alert.match_score || Math.floor(Math.random() * 100);
const scoreClass = matchScore >= 80 ? 'high' : matchScore >= 50 ? 'medium' : 'low';
const status = alert.status || 'new';
const dateMatched = new Date(alert.created_at).toLocaleDateString('en-GB', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
return `
<tr>
<td>
<div class="alert-title">${escapeHtml(alert.title || 'Untitled Tender')}</div>
<div class="alert-date">${dateMatched}</div>
</td>
<td class="col-date-matched">${dateMatched}</td>
<td>
<span class="match-score ${scoreClass}">${matchScore}%</span>
</td>
<td>
<span class="status-badge status-${status}">${status}</span>
</td>
<td>
<div class="alert-actions">
<button class="action-btn view-btn" data-id="${alert.id}">View</button>
<button class="action-btn save-btn" data-id="${alert.id}">Save</button>
<button class="action-btn apply-btn" data-id="${alert.id}">Apply</button>
</div>
</td>
</tr>
`;
}
function attachActionListeners(alerts) {
const alertsMap = new Map(alerts.map(a => [a.id, a]));
// View buttons
document.querySelectorAll('.view-btn').forEach(btn => {
btn.addEventListener('click', async () => {
const id = btn.dataset.id;
const alert = alertsMap.get(id);
if (alert) {
// Open tender detail page
window.location.href = `/tender/${id}`;
}
});
});
// Save buttons
document.querySelectorAll('.save-btn').forEach(btn => {
btn.addEventListener('click', async () => {
const id = btn.dataset.id;
btn.textContent = 'Saving...';
// TODO: Implement save API endpoint
setTimeout(() => {
btn.textContent = 'Saved';
btn.disabled = true;
showSuccess('Tender saved to your list');
}, 500);
});
});
// Apply buttons
document.querySelectorAll('.apply-btn').forEach(btn => {
btn.addEventListener('click', async () => {
const id = btn.dataset.id;
const alert = alertsMap.get(id);
if (alert) {
// Open bid writing assistant
window.location.href = `/bid/${id}`;
}
});
});
}
function displayNoAlerts() {
const container = document.getElementById('alertsContent');
container.innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">📭</div>
<h3>No Tenders Found</h3>
<p>No tenders matched your current filters. Try adjusting your alert preferences or date range.</p>
<a href="/profile.html" class="btn btn-primary">Update Alert Preferences</a>
</div>
`;
}
function applyFilters() {
filters = {
fromDate: document.getElementById('filterFromDate').value,
toDate: document.getElementById('filterToDate').value,
status: document.getElementById('filterStatus').value,
score: document.getElementById('filterScore').value
};
loadAlerts();
}
function clearFilters() {
filters = {};
document.getElementById('filterFromDate').value = '';
document.getElementById('filterToDate').value = '';
document.getElementById('filterStatus').value = '';
document.getElementById('filterScore').value = '';
loadAlerts();
}
function escapeHtml(text) {
const map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return text.replace(/[&<>"']/g, m => map[m]);
}
function showSuccess(message) {
const el = document.getElementById('successMessage');
el.textContent = message;
el.classList.add('show');
setTimeout(() => el.classList.remove('show'), 5000);
}
function showError(message) {
const el = document.getElementById('errorMessage');
el.textContent = message;
el.classList.add('show');
setTimeout(() => el.classList.remove('show'), 5000);
}
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,110 @@
/**
* TenderRadar Authentication Utilities
* Shared auth module for all app pages
*/
/**
* Get JWT token from localStorage
* @returns {string|null} JWT token or null if not found
*/
function getToken() {
return localStorage.getItem('tenderradar_token');
}
/**
* Set JWT token in localStorage
* @param {string} token - JWT token to store
*/
function setToken(token) {
localStorage.setItem('tenderradar_token', token);
}
/**
* Clear JWT token from localStorage
*/
function clearToken() {
localStorage.removeItem('tenderradar_token');
}
/**
* Check if user is authenticated
* @returns {boolean} true if token exists, false otherwise
*/
function isAuthenticated() {
return !!getToken();
}
/**
* Decode JWT payload (simple, does not verify signature)
* @returns {object|null} Decoded payload or null if token invalid
*/
function getUserInfo() {
const token = getToken();
if (!token) return null;
try {
const parts = token.split('.');
if (parts.length !== 3) return null;
const payload = JSON.parse(atob(parts[1]));
return payload;
} catch (e) {
console.error('Failed to decode token:', e);
return null;
}
}
/**
* Redirect to login if not authenticated
*/
function requireAuth() {
if (!isAuthenticated()) {
window.location.href = '/login.html';
}
}
/**
* Fetch with automatic Authorization header
* @param {string} url - API endpoint URL
* @param {object} options - Fetch options
* @returns {Promise<Response>} Fetch response
*/
async function fetchWithAuth(url, options = {}) {
const token = getToken();
const headers = {
'Content-Type': 'application/json',
...options.headers
};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
return fetch(url, {
...options,
headers
});
}
/**
* Logout user: clear token and redirect to login
*/
function logout() {
clearToken();
window.location.href = '/login.html';
}
// Export for use as ES module
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
getToken,
setToken,
clearToken,
isAuthenticated,
getUserInfo,
requireAuth,
fetchWithAuth,
logout
};
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,420 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="TenderRadar - Your AI-powered UK public sector tender intelligence platform. Find and win more public sector contracts.">
<title>TenderRadar | AI-Powered UK Public Sector Tender Intelligence</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="icon" href="/favicon.ico" type="image/x-icon">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<!-- Header/Navigation -->
<header class="header">
<nav class="nav container">
<div class="nav-brand">
<img src="/logo.png" alt="TenderRadar" class="logo-icon">
</div>
<ul class="nav-menu">
<li><a href="#features">Features</a></li>
<li><a href="#how-it-works">How It Works</a></li>
<li><a href="#pricing">Pricing</a></li>
<li><a href="#faq">FAQ</a></li>
<li><a href="/signup.html" class="btn btn-primary btn-sm">Start Free Trial</a></li>
</ul>
<button class="mobile-toggle" aria-label="Toggle menu">
<span></span>
<span></span>
<span></span>
</button>
</nav>
</header>
<!-- Hero Section -->
<section class="hero">
<div class="container">
<div class="hero-content">
<div class="badge">Now in Beta</div>
<h1 class="hero-title">Never Miss Another UK Public Sector Tender</h1>
<p class="hero-subtitle">AI-powered tender intelligence that monitors every UK public procurement portal, matches opportunities to your capabilities, and helps you write winning bids.</p>
<div class="hero-cta">
<a href="/signup.html" class="btn btn-primary btn-lg">Start Your Free Trial</a>
<a href="#how-it-works" class="btn btn-secondary btn-lg">See How It Works</a>
</div>
<div class="hero-stats">
<div class="stat">
<div class="stat-number" style="color:#1e40af!important">50,000+</div>
<div class="stat-label">Tenders Monitored Monthly</div>
</div>
<div class="stat">
<div class="stat-number" style="color: #1e40af !important;">4</div>
<div class="stat-label">Major UK Portals Covered</div>
</div>
<div class="stat">
<div class="stat-number" style="color:#1e40af!important">24/7</div>
<div class="stat-label">Automated Monitoring</div>
</div>
</div>
</div>
</div>
</section>
<!-- Features Section -->
<section id="features" class="features section">
<div class="container">
<div class="section-header">
<h2 class="section-title">Everything You Need to Win More Tenders</h2>
<p class="section-subtitle">Comprehensive tender intelligence powered by AI</p>
</div>
<div class="features-grid">
<div class="feature-card">
<div class="feature-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
<polyline points="7.5 4.21 12 6.81 16.5 4.21"/>
<polyline points="7.5 19.79 7.5 14.6 3 12"/>
<polyline points="21 12 16.5 14.6 16.5 19.79"/>
<polyline points="3.27 6.96 12 12.01 20.73 6.96"/>
<line x1="12" y1="22.08" x2="12" y2="12"/>
</svg>
</div>
<h3>AI-Powered Matching</h3>
<p>Our AI analyzes your company profile and automatically matches you with relevant tenders based on your capabilities, past projects, and certifications.</p>
</div>
<div class="feature-card">
<div class="feature-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
<circle cx="8.5" cy="8.5" r="1.5"/>
<polyline points="21 15 16 10 5 21"/>
</svg>
</div>
<h3>Complete UK Coverage</h3>
<p>Monitor all major UK procurement portals: Contracts Finder, Find a Tender, Public Contracts Scotland, and Sell2Wales in one dashboard.</p>
</div>
<div class="feature-card">
<div class="feature-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 12h-4l-3 9L9 3l-3 9H2"/>
</svg>
</div>
<h3>Instant Alerts</h3>
<p>Get notified immediately when relevant tenders are published. Email, SMS, or Slack integration keeps you ahead of the competition.</p>
</div>
<div class="feature-card">
<div class="feature-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
<line x1="16" y1="13" x2="8" y2="13"/>
<line x1="16" y1="17" x2="8" y2="17"/>
<polyline points="10 9 9 9 8 9"/>
</svg>
</div>
<h3>Bid Writing Assistant</h3>
<p>AI-powered bid writing tools help you craft compelling proposals faster. Get suggestions based on winning bids in your sector.</p>
</div>
<div class="feature-card">
<div class="feature-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>
</svg>
</div>
<h3>Deadline Tracking</h3>
<p>Never miss a deadline again. Smart calendar integration and automated reminders keep your bid pipeline organized.</p>
</div>
<div class="feature-card">
<div class="feature-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M18 20V10"/>
<path d="M12 20V4"/>
<path d="M6 20v-6"/>
</svg>
</div>
<h3>Analytics & Insights</h3>
<p>Track your win rate, analyze market trends, and identify the most lucrative opportunities with detailed analytics dashboards.</p>
</div>
</div>
</div>
</section>
<!-- How It Works Section -->
<section id="how-it-works" class="how-it-works section section-alt">
<div class="container">
<div class="section-header">
<h2 class="section-title">How TenderRadar Works</h2>
<p class="section-subtitle">Start finding relevant tenders in minutes</p>
</div>
<div class="steps">
<div class="step">
<div class="step-number">1</div>
<div class="step-content">
<h3>Set Up Your Profile</h3>
<p>Tell us about your company, capabilities, sectors, and contract values you're interested in. Takes just 5 minutes.</p>
</div>
</div>
<div class="step">
<div class="step-number">2</div>
<div class="step-content">
<h3>AI Monitors for You</h3>
<p>Our AI continuously scans all UK procurement portals, analyzing thousands of tenders daily to find perfect matches.</p>
</div>
</div>
<div class="step">
<div class="step-number">3</div>
<div class="step-content">
<h3>Get Instant Alerts</h3>
<p>Receive notifications as soon as relevant tenders are published, with AI-generated summaries and match scores.</p>
</div>
</div>
<div class="step">
<div class="step-number">4</div>
<div class="step-content">
<h3>Win More Contracts</h3>
<p>Use our bid writing tools and deadline tracking to submit higher quality bids faster than ever before.</p>
</div>
</div>
</div>
</div>
</section>
<!-- Pricing Section -->
<section id="pricing" class="pricing section">
<div class="container">
<div class="section-header">
<h2 class="section-title">Simple, Transparent Pricing</h2>
<p class="section-subtitle">Choose the plan that fits your needs. All plans include a 14-day free trial.</p>
</div>
<div class="pricing-grid">
<div class="pricing-card">
<div class="pricing-header">
<h3>Starter</h3>
<div class="price"><span class="currency">£</span>39<span class="period">/month</span></div>
</div>
<ul class="pricing-features">
<li>Up to 10 active tender alerts</li>
<li>All UK procurement portals</li>
<li>Email notifications</li>
<li>Basic AI matching</li>
<li>Deadline calendar</li>
<li>14-day free trial</li>
</ul>
<a href="/signup.html" class="btn btn-outline">Start Free Trial</a>
</div>
<div class="pricing-card pricing-card-featured">
<div class="pricing-badge">Most Popular</div>
<div class="pricing-header">
<h3>Growth</h3>
<div class="price"><span class="currency">£</span>99<span class="period">/month</span></div>
</div>
<ul class="pricing-features">
<li>Unlimited tender alerts</li>
<li>All UK procurement portals</li>
<li>Email, SMS & Slack alerts</li>
<li>Advanced AI matching</li>
<li>Bid writing assistant</li>
<li>Analytics dashboard</li>
<li>Priority support</li>
<li>14-day free trial</li>
</ul>
<a href="/signup.html" class="btn btn-primary">Start Free Trial</a>
</div>
<div class="pricing-card">
<div class="pricing-header">
<h3>Pro</h3>
<div class="price"><span class="currency">£</span>249<span class="period">/month</span></div>
</div>
<ul class="pricing-features">
<li>Everything in Growth</li>
<li>API access</li>
<li>Custom integrations</li>
<li>Team collaboration tools</li>
<li>Advanced analytics & reports</li>
<li>Dedicated account manager</li>
<li>Custom AI training</li>
<li>14-day free trial</li>
</ul>
<a href="/signup.html" class="btn btn-outline">Start Free Trial</a>
</div>
</div>
</div>
</section>
<!-- Testimonials Section -->
<section class="testimonials section section-alt">
<div class="container">
<div class="section-header">
<h2 class="section-title">Trusted by UK Businesses</h2>
<p class="section-subtitle">Join companies already winning more public sector contracts</p>
</div>
<div class="testimonials-grid">
<div class="testimonial-card">
<div class="testimonial-quote">"TenderRadar has transformed how we find opportunities. We're now bidding on contracts we would have never found manually."</div>
<div class="testimonial-author">
<div class="testimonial-name">Sarah Mitchell</div>
<div class="testimonial-company">Director, TechServe Solutions</div>
</div>
</div>
<div class="testimonial-card">
<div class="testimonial-quote">"The AI matching is incredibly accurate. We've cut our tender research time by 80% and increased our win rate by 40%."</div>
<div class="testimonial-author">
<div class="testimonial-name">James Patterson</div>
<div class="testimonial-company">CEO, BuildRight Construction</div>
</div>
</div>
<div class="testimonial-card">
<div class="testimonial-quote">"Finally, a tool that actually understands public procurement. The bid writing assistant alone is worth the subscription."</div>
<div class="testimonial-author">
<div class="testimonial-name">Emma Thompson</div>
<div class="testimonial-company">Bid Manager, ConsultPro Ltd</div>
</div>
</div>
</div>
</div>
</section>
<!-- FAQ Section -->
<section id="faq" class="faq section">
<div class="container">
<div class="section-header">
<h2 class="section-title">Frequently Asked Questions</h2>
</div>
<div class="faq-list">
<div class="faq-item">
<button class="faq-question">
<span>Which procurement portals does TenderRadar cover?</span>
<svg class="faq-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="6 9 12 15 18 9"/>
</svg>
</button>
<div class="faq-answer">
<p>TenderRadar monitors all major UK public procurement portals including Contracts Finder, Find a Tender (FTS), Public Contracts Scotland, and Sell2Wales. We also track framework agreements and dynamic purchasing systems.</p>
</div>
</div>
<div class="faq-item">
<button class="faq-question">
<span>How does the AI matching work?</span>
<svg class="faq-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="6 9 12 15 18 9"/>
</svg>
</button>
<div class="faq-answer">
<p>Our AI analyzes your company profile, past bids, certifications, and capabilities against tender requirements. It uses natural language processing to understand tender descriptions and scores each opportunity based on relevance, fit, and likelihood of success.</p>
</div>
</div>
<div class="faq-item">
<button class="faq-question">
<span>Can I cancel my subscription at any time?</span>
<svg class="faq-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="6 9 12 15 18 9"/>
</svg>
</button>
<div class="faq-answer">
<p>Yes, absolutely. All plans are month-to-month with no long-term contracts. You can cancel at any time from your account settings, and you'll retain access until the end of your billing period.</p>
</div>
</div>
<div class="faq-item">
<button class="faq-question">
<span>Is my company data secure?</span>
<svg class="faq-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="6 9 12 15 18 9"/>
</svg>
</button>
<div class="faq-answer">
<p>Security is our top priority. All data is encrypted in transit and at rest, hosted on UK-based servers, and we're fully GDPR compliant. We never share your data with third parties and you maintain full control over your information.</p>
</div>
</div>
<div class="faq-item">
<button class="faq-question">
<span>How quickly will I start seeing relevant tenders?</span>
<svg class="faq-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="6 9 12 15 18 9"/>
</svg>
</button>
<div class="faq-answer">
<p>Most users receive their first matched tenders within 24 hours of completing their profile. Our AI scans portals every hour, so you'll get alerts as soon as relevant opportunities are published.</p>
</div>
</div>
<div class="faq-item">
<button class="faq-question">
<span>Do you offer enterprise plans for larger organizations?</span>
<svg class="faq-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="6 9 12 15 18 9"/>
</svg>
</button>
<div class="faq-answer">
<p>Yes! We offer custom enterprise plans with advanced features including multi-user access, custom AI training, API access, dedicated support, and bespoke integrations. Contact us to discuss your requirements.</p>
</div>
</div>
</div>
</div>
</section>
<!-- Signup Section -->
<section id="signup" class="signup section">
<div class="container">
<div class="signup-content">
<h2 class="signup-title">Start Finding Better Tenders Today</h2>
<p class="signup-subtitle">Join the beta and get 14 days free. No credit card required.</p>
<form class="signup-form" id="signupForm">
<div class="form-group">
<input type="email" id="email" name="email" placeholder="Enter your work email" required>
<button type="submit" class="btn btn-primary btn-lg">Start Free Trial</button>
</div>
<p class="form-note">By signing up, you agree to our Terms of Service and Privacy Policy</p>
<div id="formMessage" class="form-message"></div>
</form>
</div>
</div>
</section>
<!-- Footer -->
<footer class="footer">
<div class="container">
<div class="footer-grid">
<div class="footer-col">
<div class="footer-brand">
<img src="/logo.png" alt="TenderRadar" class="logo-icon">
</div>
<p class="footer-desc">AI-powered UK public sector tender intelligence platform</p>
</div>
<div class="footer-col">
<h4>Product</h4>
<ul>
<li><a href="#features">Features</a></li>
<li><a href="#pricing">Pricing</a></li>
<li><a href="#how-it-works">How It Works</a></li>
</ul>
</div>
<div class="footer-col">
<h4>Company</h4>
<ul>
<li><a href="#about">About</a></li>
<li><a href="#contact">Contact</a></li>
<li><a href="#blog">Blog</a></li>
</ul>
</div>
<div class="footer-col">
<h4>Legal</h4>
<ul>
<li><a href="#privacy">Privacy Policy</a></li>
<li><a href="#terms">Terms of Service</a></li>
<li><a href="#gdpr">GDPR</a></li>
</ul>
</div>
</div>
<div class="footer-bottom">
<p>&copy; 2025 TenderRadar. All rights reserved.</p>
</div>
</div>
</footer>
<script src="script.js"></script>
</body>
</html>

View File

@@ -0,0 +1,405 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Sign in to TenderRadar - AI-powered UK public sector tender intelligence">
<title>Sign In | TenderRadar</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="icon" href="/favicon.ico" type="image/x-icon">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
<link rel="stylesheet" href="styles.css">
<style>
.auth-page {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #f9fafb 0%, #f3f4f6 100%);
padding: 1.5rem;
}
.auth-container {
width: 100%;
max-width: 450px;
}
.auth-card {
background: white;
border-radius: 0.75rem;
box-shadow: var(--shadow-lg);
padding: 2.5rem;
}
.auth-header {
text-align: center;
margin-bottom: 2rem;
}
.auth-header .logo-icon {
height: 50px;
margin-bottom: 1rem;
}
.auth-header h1 {
font-size: 1.875rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 0.5rem;
}
.auth-header p {
color: var(--text-secondary);
font-size: 0.9375rem;
}
.form-group {
margin-bottom: 1.25rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: var(--text-primary);
font-size: 0.875rem;
}
.form-group input {
width: 100%;
padding: 0.75rem;
border: 1px solid var(--border);
border-radius: 0.5rem;
font-family: 'Inter', sans-serif;
font-size: 0.9375rem;
transition: all 0.2s;
}
.form-group input:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(30, 64, 175, 0.1);
}
.password-group {
position: relative;
}
.password-toggle {
position: absolute;
right: 0.75rem;
top: 2.25rem;
background: none;
border: none;
cursor: pointer;
color: var(--text-secondary);
padding: 0.25rem 0.5rem;
}
.form-header-with-link {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.form-header-with-link label {
margin: 0;
}
.forgot-password {
font-size: 0.8125rem;
color: var(--primary);
text-decoration: none;
font-weight: 500;
}
.forgot-password:hover {
text-decoration: underline;
}
.error {
color: #dc2626;
font-size: 0.875rem;
margin-top: 0.25rem;
display: none;
}
.error.show {
display: block;
}
.form-group.error-state input {
border-color: #dc2626;
}
.submit-btn {
width: 100%;
padding: 0.75rem;
margin-top: 1.25rem;
}
.auth-footer {
text-align: center;
margin-top: 1.5rem;
}
.auth-footer p {
color: var(--text-secondary);
font-size: 0.875rem;
}
.auth-footer a {
color: var(--primary);
text-decoration: none;
font-weight: 500;
}
.auth-footer a:hover {
text-decoration: underline;
}
.success-message {
background: #ecfdf5;
color: #065f46;
padding: 0.875rem;
border-radius: 0.5rem;
margin-bottom: 1rem;
display: none;
}
.success-message.show {
display: block;
}
.error-message {
background: #fef2f2;
color: #7f1d1d;
padding: 0.875rem;
border-radius: 0.5rem;
margin-bottom: 1rem;
display: none;
}
.error-message.show {
display: block;
}
.remember-me {
display: flex;
align-items: center;
gap: 0.5rem;
margin-top: -0.75rem;
margin-bottom: 1rem;
}
.remember-me input[type="checkbox"] {
width: auto;
cursor: pointer;
}
.remember-me label {
margin: 0;
cursor: pointer;
font-size: 0.875rem;
}
</style>
</head>
<body>
<!-- Header/Navigation -->
<header class="header">
<nav class="nav container">
<a href="/" class="nav-brand">
<img src="/logo.png" alt="TenderRadar" class="logo-icon">
</a>
<ul class="nav-menu">
<li><a href="/#features">Features</a></li>
<li><a href="/#pricing">Pricing</a></li>
<li><a href="signup.html" class="btn btn-secondary btn-sm">Sign Up</a></li>
</ul>
<button class="mobile-toggle" aria-label="Toggle menu">
<span></span>
<span></span>
<span></span>
</button>
</nav>
</header>
<!-- Login Form -->
<section class="auth-page">
<div class="auth-container">
<div class="auth-card">
<div class="auth-header">
<img src="/logo.png" alt="TenderRadar" class="logo-icon">
<h1>Welcome Back</h1>
<p>Sign in to your TenderRadar account</p>
</div>
<div class="success-message" id="successMessage">
Signing you in... Redirecting to dashboard...
</div>
<div class="error-message" id="errorMessage"></div>
<form id="loginForm" class="login-form">
<div class="form-group">
<label for="email">Email Address *</label>
<input type="email" id="email" name="email" placeholder="you@company.com" required>
<div class="error" id="emailError"></div>
</div>
<div class="form-group password-group">
<div class="form-header-with-link">
<label for="password">Password *</label>
<a href="#" class="forgot-password" id="forgotPasswordLink">Forgot password?</a>
</div>
<input type="password" id="password" name="password" placeholder="Enter your password" required>
<button type="button" class="password-toggle" id="togglePassword">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
<circle cx="12" cy="12" r="3"/>
</svg>
</button>
<div class="error" id="passwordError"></div>
</div>
<div class="remember-me">
<input type="checkbox" id="rememberMe" name="rememberMe">
<label for="rememberMe">Remember me</label>
</div>
<button type="submit" class="btn btn-primary submit-btn" id="submitBtn">Sign In</button>
</form>
<div class="auth-footer">
<p>Don't have an account? <a href="signup.html">Sign up here</a></p>
</div>
</div>
</div>
</section>
<script>
const form = document.getElementById('loginForm');
const submitBtn = document.getElementById('submitBtn');
const errorMessage = document.getElementById('errorMessage');
const successMessage = document.getElementById('successMessage');
const forgotPasswordLink = document.getElementById('forgotPasswordLink');
// Password visibility toggle
document.getElementById('togglePassword').addEventListener('click', function(e) {
e.preventDefault();
const input = document.getElementById('password');
input.type = input.type === 'password' ? 'text' : 'password';
});
// Forgot password placeholder
forgotPasswordLink.addEventListener('click', function(e) {
e.preventDefault();
alert('Password reset functionality coming soon. Please contact support at support@tenderradar.com');
});
// Form validation
function validateForm() {
const errors = {};
const email = document.getElementById('email').value.trim();
const password = document.getElementById('password').value;
// Clear previous errors
document.querySelectorAll('.form-group.error-state').forEach(el => {
el.classList.remove('error-state');
});
document.querySelectorAll('.error').forEach(el => {
el.classList.remove('show');
el.textContent = '';
});
if (!email) {
errors.email = 'Email is required';
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
errors.email = 'Please enter a valid email address';
}
if (!password) {
errors.password = 'Password is required';
}
// Display errors
Object.keys(errors).forEach(field => {
const errorEl = document.getElementById(field + 'Error');
const formGroup = errorEl.closest('.form-group');
formGroup.classList.add('error-state');
errorEl.textContent = errors[field];
errorEl.classList.add('show');
});
return Object.keys(errors).length === 0;
}
form.addEventListener('submit', async (e) => {
e.preventDefault();
if (!validateForm()) {
return;
}
errorMessage.classList.remove('show');
errorMessage.textContent = '';
submitBtn.disabled = true;
submitBtn.textContent = 'Signing in...';
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: document.getElementById('email').value.trim(),
password: document.getElementById('password').value
})
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Login failed');
}
// Store token and user data
localStorage.setItem('token', data.token);
localStorage.setItem('user', JSON.stringify(data.user));
// Store remember me preference
if (document.getElementById('rememberMe').checked) {
localStorage.setItem('rememberMe', 'true');
}
successMessage.classList.add('show');
setTimeout(() => {
window.location.href = '/dashboard.html';
}, 1500);
} catch (error) {
errorMessage.textContent = error.message;
errorMessage.classList.add('show');
submitBtn.disabled = false;
submitBtn.textContent = 'Sign In';
}
});
// Check if user was previously remembered
window.addEventListener('load', function() {
if (localStorage.getItem('rememberMe') === 'true') {
const user = JSON.parse(localStorage.getItem('user'));
if (user && user.email) {
document.getElementById('email').value = user.email;
document.getElementById('rememberMe').checked = true;
}
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,938 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="TenderRadar - User Profile and Alert Preferences">
<title>Profile | TenderRadar</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="icon" href="/favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="styles.css">
<style>
/* Profile Page Specific Styles */
.profile-container {
display: grid;
grid-template-columns: 250px 1fr;
gap: 2rem;
min-height: calc(100vh - 72px);
padding: 2rem 0;
}
.profile-sidebar {
background: white;
border-radius: 1rem;
padding: 1.5rem;
height: fit-content;
box-shadow: var(--shadow-sm);
border: 1px solid var(--border);
position: sticky;
top: 90px;
}
.profile-sidebar h3 {
font-size: 0.875rem;
font-weight: 700;
color: var(--text-light);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 1rem;
}
.profile-sidebar-menu {
list-style: none;
}
.profile-sidebar-menu li {
margin-bottom: 0.5rem;
}
.profile-sidebar-menu a {
display: block;
padding: 0.75rem 1rem;
color: var(--text-secondary);
text-decoration: none;
border-radius: 0.5rem;
transition: all 0.2s;
font-size: 0.9375rem;
font-weight: 500;
}
.profile-sidebar-menu a:hover,
.profile-sidebar-menu a.active {
background: var(--bg-alt);
color: var(--primary);
}
.profile-main {
background: white;
border-radius: 1rem;
padding: 2.5rem;
box-shadow: var(--shadow-sm);
border: 1px solid var(--border);
}
.profile-section {
display: none;
}
.profile-section.active {
display: block;
}
.profile-section h2 {
font-size: 1.75rem;
font-weight: 700;
margin-bottom: 0.5rem;
color: var(--text-primary);
}
.profile-section-desc {
font-size: 0.9375rem;
color: var(--text-secondary);
margin-bottom: 2rem;
}
.form-section {
margin-bottom: 3rem;
padding-bottom: 3rem;
border-bottom: 1px solid var(--border);
}
.form-section:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
.form-section h3 {
font-size: 1.125rem;
font-weight: 600;
margin-bottom: 1.5rem;
color: var(--text-primary);
}
.form-group {
margin-bottom: 1.5rem;
display: flex;
flex-direction: column;
}
.form-group label {
font-weight: 600;
margin-bottom: 0.5rem;
color: var(--text-primary);
font-size: 0.9375rem;
}
.form-group input,
.form-group select,
.form-group textarea {
padding: 0.75rem 1rem;
border: 1px solid var(--border);
border-radius: 0.5rem;
font-family: inherit;
font-size: 0.9375rem;
transition: border-color 0.2s, box-shadow 0.2s;
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(30, 64, 175, 0.1);
}
.form-group textarea {
resize: vertical;
min-height: 100px;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
}
/* Tag Input */
.tag-input-container {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
padding: 0.5rem;
border: 1px solid var(--border);
border-radius: 0.5rem;
min-height: 44px;
align-items: center;
}
.tag-input-container.focused {
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(30, 64, 175, 0.1);
}
.tag {
background: var(--primary);
color: white;
padding: 0.375rem 0.75rem;
border-radius: 0.375rem;
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
font-weight: 500;
}
.tag button {
background: none;
border: none;
color: white;
cursor: pointer;
padding: 0;
font-size: 1.125rem;
line-height: 1;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
}
.tag button:hover {
opacity: 0.8;
}
.tag-input {
flex: 1;
min-width: 120px;
border: none;
outline: none;
font-family: inherit;
font-size: 0.9375rem;
}
/* Multi-select */
.multi-select {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 1rem;
}
.checkbox-group {
display: flex;
align-items: center;
gap: 0.5rem;
}
.checkbox-group input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
accent-color: var(--primary);
}
.checkbox-group label {
margin: 0;
cursor: pointer;
font-weight: 400;
}
/* Buttons */
.form-actions {
display: flex;
gap: 1rem;
margin-top: 2rem;
}
.btn-save,
.btn-cancel {
padding: 0.875rem 2rem;
border-radius: 0.5rem;
font-weight: 600;
font-size: 1rem;
border: none;
cursor: pointer;
transition: all 0.2s;
}
.btn-save {
background: var(--primary);
color: white;
}
.btn-save:hover:not(:disabled) {
background: var(--primary-dark);
transform: translateY(-1px);
box-shadow: var(--shadow-md);
}
.btn-save:disabled {
background: var(--text-light);
cursor: not-allowed;
}
.btn-cancel {
background: var(--bg-alt);
color: var(--text-primary);
}
.btn-cancel:hover {
background: var(--border);
}
.btn-danger {
background: #ef4444;
color: white;
}
.btn-danger:hover {
background: #dc2626;
}
/* Status Messages */
.alert {
padding: 1rem 1.5rem;
border-radius: 0.5rem;
margin-bottom: 1.5rem;
display: none;
}
.alert.show {
display: block;
}
.alert-success {
background: rgba(34, 197, 94, 0.1);
border: 1px solid rgba(34, 197, 94, 0.3);
color: #15803d;
}
.alert-error {
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.3);
color: #7f1d1d;
}
.form-help {
font-size: 0.8125rem;
color: var(--text-light);
margin-top: 0.375rem;
}
/* Responsive */
@media (max-width: 768px) {
.profile-container {
grid-template-columns: 1fr;
gap: 1.5rem;
}
.profile-sidebar {
position: static;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
}
.profile-sidebar-menu {
grid-column: 1 / -1;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
}
.profile-main {
padding: 1.5rem;
}
.form-row {
grid-template-columns: 1fr;
}
.form-actions {
flex-direction: column;
}
.btn-save,
.btn-cancel {
width: 100%;
}
.profile-section h2 {
font-size: 1.5rem;
}
}
</style>
</head>
<body>
<!-- Header/Navigation -->
<header class="header">
<nav class="nav container">
<div class="nav-brand">
<img src="/logo.png" alt="TenderRadar" class="logo-icon">
</div>
<ul class="nav-menu">
<li><a href="/">Dashboard</a></li>
<li><a href="/alerts.html">Alerts</a></li>
<li><a href="/profile.html" class="active-nav">Profile</a></li>
<li><button id="logoutBtn" class="btn btn-outline btn-sm">Logout</button></li>
</ul>
<button class="mobile-toggle" aria-label="Toggle menu">
<span></span>
<span></span>
<span></span>
</button>
</nav>
</header>
<!-- Main Container -->
<div class="container">
<div class="profile-container">
<!-- Sidebar Navigation -->
<aside class="profile-sidebar">
<h3>Settings</h3>
<ul class="profile-sidebar-menu">
<li><a href="#company" class="sidebar-link active" data-section="company">Company Profile</a></li>
<li><a href="#alerts" class="sidebar-link" data-section="alerts">Alert Preferences</a></li>
<li><a href="#account" class="sidebar-link" data-section="account">Account</a></li>
</ul>
</aside>
<!-- Main Content -->
<main class="profile-main">
<!-- Status Messages -->
<div id="successMessage" class="alert alert-success"></div>
<div id="errorMessage" class="alert alert-error"></div>
<!-- Company Profile Section -->
<section id="company" class="profile-section active">
<h2>Company Profile</h2>
<p class="profile-section-desc">Tell us about your company so we can find the best tender matches for you.</p>
<div class="form-section">
<h3>Basic Information</h3>
<div class="form-group">
<label for="companyName">Company Name *</label>
<input type="text" id="companyName" name="companyName" placeholder="Enter your company name" required>
</div>
<div class="form-row">
<div class="form-group">
<label for="industry">Industry/Sector *</label>
<select id="industry" name="industry" required>
<option value="">Select an industry</option>
<option value="construction">Construction</option>
<option value="consulting">Consulting</option>
<option value="it">IT & Software</option>
<option value="professional_services">Professional Services</option>
<option value="manufacturing">Manufacturing</option>
<option value="logistics">Logistics & Transport</option>
<option value="healthcare">Healthcare</option>
<option value="engineering">Engineering</option>
<option value="facilities">Facilities Management</option>
<option value="training">Training & Education</option>
<option value="other">Other</option>
</select>
</div>
<div class="form-group">
<label for="companySize">Company Size *</label>
<select id="companySize" name="companySize" required>
<option value="">Select company size</option>
<option value="micro">Micro (0-9 employees)</option>
<option value="small">Small (10-49 employees)</option>
<option value="medium">Medium (50-249 employees)</option>
<option value="large">Large (250+ employees)</option>
</select>
</div>
</div>
<div class="form-group">
<label for="description">Company Description</label>
<textarea id="description" name="description" placeholder="Briefly describe your company, what you do, and your expertise..."></textarea>
<div class="form-help">Helps us match you with more relevant tenders</div>
</div>
</div>
<div class="form-section">
<h3>Capabilities & Services</h3>
<div class="form-group">
<label>What services/products do you provide?</label>
<div class="tag-input-container" id="capabilitiesInput">
<input type="text" class="tag-input" placeholder="Type and press Enter to add...">
</div>
<div class="form-help">Add tags for your main services or product areas</div>
</div>
</div>
<div class="form-section">
<h3>Certifications & Accreditations</h3>
<div class="form-group">
<label>Relevant certifications</label>
<div class="multi-select">
<div class="checkbox-group">
<input type="checkbox" id="iso9001" name="certifications" value="iso9001">
<label for="iso9001">ISO 9001</label>
</div>
<div class="checkbox-group">
<input type="checkbox" id="iso27001" name="certifications" value="iso27001">
<label for="iso27001">ISO 27001</label>
</div>
<div class="checkbox-group">
<input type="checkbox" id="iso14001" name="certifications" value="iso14001">
<label for="iso14001">ISO 14001</label>
</div>
<div class="checkbox-group">
<input type="checkbox" id="cmmc" name="certifications" value="cmmc">
<label for="cmmc">CMMC</label>
</div>
<div class="checkbox-group">
<input type="checkbox" id="soc2" name="certifications" value="soc2">
<label for="soc2">SOC 2</label>
</div>
<div class="checkbox-group">
<input type="checkbox" id="gov.uk" name="certifications" value="gov.uk">
<label for="gov.uk">G-Cloud</label>
</div>
</div>
</div>
</div>
<div class="form-actions">
<button class="btn-save" data-section="company">Save Company Profile</button>
</div>
</section>
<!-- Alert Preferences Section -->
<section id="alerts" class="profile-section">
<h2>Alert Preferences</h2>
<p class="profile-section-desc">Customize how you receive tender alerts and what types of opportunities you want to see.</p>
<div class="form-section">
<h3>Tender Keywords</h3>
<div class="form-group">
<label>Keywords or phrases</label>
<div class="tag-input-container" id="keywordsInput">
<input type="text" class="tag-input" placeholder="Type and press Enter to add...">
</div>
<div class="form-help">Enter keywords to match tenders. e.g., 'software development', 'cloud migration'</div>
</div>
</div>
<div class="form-section">
<h3>Sectors & Categories</h3>
<div class="form-group">
<label>Which sectors interest you?</label>
<div class="multi-select">
<div class="checkbox-group">
<input type="checkbox" id="sec-admin" name="sectors" value="admin">
<label for="sec-admin">Administration</label>
</div>
<div class="checkbox-group">
<input type="checkbox" id="sec-defence" name="sectors" value="defence">
<label for="sec-defence">Defence</label>
</div>
<div class="checkbox-group">
<input type="checkbox" id="sec-education" name="sectors" value="education">
<label for="sec-education">Education</label>
</div>
<div class="checkbox-group">
<input type="checkbox" id="sec-energy" name="sectors" value="energy">
<label for="sec-energy">Energy</label>
</div>
<div class="checkbox-group">
<input type="checkbox" id="sec-environment" name="sectors" value="environment">
<label for="sec-environment">Environment</label>
</div>
<div class="checkbox-group">
<input type="checkbox" id="sec-health" name="sectors" value="health">
<label for="sec-health">Health</label>
</div>
<div class="checkbox-group">
<input type="checkbox" id="sec-housing" name="sectors" value="housing">
<label for="sec-housing">Housing</label>
</div>
<div class="checkbox-group">
<input type="checkbox" id="sec-justice" name="sectors" value="justice">
<label for="sec-justice">Justice</label>
</div>
<div class="checkbox-group">
<input type="checkbox" id="sec-social" name="sectors" value="social">
<label for="sec-social">Social Inclusion</label>
</div>
<div class="checkbox-group">
<input type="checkbox" id="sec-transport" name="sectors" value="transport">
<label for="sec-transport">Transport</label>
</div>
<div class="checkbox-group">
<input type="checkbox" id="sec-utilities" name="sectors" value="utilities">
<label for="sec-utilities">Utilities</label>
</div>
</div>
</div>
</div>
<div class="form-section">
<h3>Contract Value</h3>
<div class="form-row">
<div class="form-group">
<label for="minValue">Minimum Contract Value (£)</label>
<input type="number" id="minValue" name="minValue" placeholder="0" min="0" step="1000">
</div>
<div class="form-group">
<label for="maxValue">Maximum Contract Value (£)</label>
<input type="number" id="maxValue" name="maxValue" placeholder="No limit" min="0" step="1000">
</div>
</div>
<div class="form-help">Leave blank for no limit</div>
</div>
<div class="form-section">
<h3>Preferred Locations</h3>
<div class="form-group">
<label>Preferred regions (optional)</label>
<div class="multi-select">
<div class="checkbox-group">
<input type="checkbox" id="loc-england" name="locations" value="england">
<label for="loc-england">England</label>
</div>
<div class="checkbox-group">
<input type="checkbox" id="loc-scotland" name="locations" value="scotland">
<label for="loc-scotland">Scotland</label>
</div>
<div class="checkbox-group">
<input type="checkbox" id="loc-wales" name="locations" value="wales">
<label for="loc-wales">Wales</label>
</div>
<div class="checkbox-group">
<input type="checkbox" id="loc-ni" name="locations" value="northern-ireland">
<label for="loc-ni">Northern Ireland</label>
</div>
</div>
</div>
</div>
<div class="form-section">
<h3>Alert Frequency</h3>
<div class="form-group">
<label for="alertFrequency">How often would you like to receive alerts?</label>
<select id="alertFrequency" name="alertFrequency">
<option value="instant">Instant (as soon as published)</option>
<option value="daily" selected>Daily Digest</option>
<option value="weekly">Weekly Digest</option>
</select>
</div>
</div>
<div class="form-actions">
<button class="btn-save" data-section="alerts">Save Alert Preferences</button>
</div>
</section>
<!-- Account Section -->
<section id="account" class="profile-section">
<h2>Account</h2>
<p class="profile-section-desc">Manage your account settings and security.</p>
<div class="form-section">
<h3>Account Information</h3>
<div class="form-group">
<label for="email">Email Address</label>
<input type="email" id="email" name="email" disabled>
<div class="form-help">Your primary login email</div>
</div>
</div>
<div class="form-section">
<h3>Change Password</h3>
<div class="form-group">
<label for="currentPassword">Current Password</label>
<input type="password" id="currentPassword" name="currentPassword" placeholder="Enter your current password">
</div>
<div class="form-group">
<label for="newPassword">New Password</label>
<input type="password" id="newPassword" name="newPassword" placeholder="Enter your new password">
</div>
<div class="form-group">
<label for="confirmPassword">Confirm Password</label>
<input type="password" id="confirmPassword" name="confirmPassword" placeholder="Confirm your new password">
</div>
<div class="form-actions">
<button class="btn-save" id="changePasswordBtn">Change Password</button>
</div>
</div>
<div class="form-section">
<h3>Danger Zone</h3>
<p style="color: var(--text-secondary); margin-bottom: 1.5rem; font-size: 0.9375rem;">
This action cannot be undone. Please be certain.
</p>
<button class="btn-danger" id="deleteAccountBtn">Delete Account</button>
</div>
</section>
</main>
</div>
</div>
<script>
// Auth and state
let authToken = localStorage.getItem('authToken');
let currentUser = null;
// Check authentication
document.addEventListener('DOMContentLoaded', async () => {
if (!authToken) {
window.location.href = '/login.html';
return;
}
// Load user profile
await loadProfile();
// Set up event listeners
setupEventListeners();
});
async function loadProfile() {
try {
const [prefsResponse, userResponse] = await Promise.all([
fetch('/api/alerts/preferences', {
headers: { 'Authorization': `Bearer ${authToken}` }
}),
fetch('/api/user', {
headers: { 'Authorization': `Bearer ${authToken}` }
}).catch(() => null)
]);
if (!prefsResponse.ok && prefsResponse.status === 401) {
localStorage.removeItem('authToken');
window.location.href = '/login.html';
return;
}
const prefsData = prefsResponse.ok ? await prefsResponse.json() : { preferences: null };
const user = userResponse ? await userResponse.json() : null;
// Set email
if (user?.email) {
document.getElementById('email').value = user.email;
}
// Load preferences
const prefs = prefsData.preferences;
if (prefs) {
document.getElementById('companyName').value = user?.company_name || '';
document.getElementById('minValue').value = prefs.min_value || '';
document.getElementById('maxValue').value = prefs.max_value || '';
document.getElementById('alertFrequency').value = 'daily'; // Default
// Load keywords
if (prefs.keywords && prefs.keywords.length > 0) {
prefs.keywords.forEach(kw => addTag('keywordsInput', kw));
}
// Load sectors
if (prefs.sectors && prefs.sectors.length > 0) {
prefs.sectors.forEach(sector => {
const checkbox = document.querySelector(`input[name="sectors"][value="${sector}"]`);
if (checkbox) checkbox.checked = true;
});
}
// Load locations
if (prefs.locations && prefs.locations.length > 0) {
prefs.locations.forEach(location => {
const checkbox = document.querySelector(`input[name="locations"][value="${location}"]`);
if (checkbox) checkbox.checked = true;
});
}
}
} catch (error) {
console.error('Error loading profile:', error);
showError('Failed to load profile preferences');
}
}
function setupEventListeners() {
// Sidebar navigation
document.querySelectorAll('.sidebar-link').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const section = link.dataset.section;
switchSection(section);
});
});
// Save buttons
document.querySelectorAll('.btn-save').forEach(btn => {
btn.addEventListener('click', async () => {
const section = btn.dataset.section;
await saveSection(section);
});
});
// Tag inputs
setupTagInput('keywordsInput');
setupTagInput('capabilitiesInput');
// Change password
document.getElementById('changePasswordBtn')?.addEventListener('click', async () => {
const current = document.getElementById('currentPassword').value;
const newPass = document.getElementById('newPassword').value;
const confirm = document.getElementById('confirmPassword').value;
if (!current || !newPass || !confirm) {
showError('Please fill all password fields');
return;
}
if (newPass !== confirm) {
showError('Passwords do not match');
return;
}
// TODO: Implement password change API endpoint
showSuccess('Password change not yet implemented - contact support');
});
// Logout
document.getElementById('logoutBtn')?.addEventListener('click', () => {
localStorage.removeItem('authToken');
window.location.href = '/';
});
// Delete account
document.getElementById('deleteAccountBtn')?.addEventListener('click', async () => {
if (confirm('Are you absolutely sure? This will permanently delete your account and all associated data.')) {
// TODO: Implement account deletion
showSuccess('Account deletion not yet implemented - contact support');
}
});
}
function switchSection(section) {
// Update sidebar
document.querySelectorAll('.sidebar-link').forEach(link => {
link.classList.remove('active');
});
document.querySelector(`[data-section="${section}"]`).classList.add('active');
// Update main content
document.querySelectorAll('.profile-section').forEach(sec => {
sec.classList.remove('active');
});
document.getElementById(section).classList.add('active');
}
async function saveSection(section) {
try {
const data = {};
if (section === 'company') {
data.keywords = getTags('capabilitiesInput');
// TODO: Save company name, industry, size, description
} else if (section === 'alerts') {
data.keywords = getTags('keywordsInput');
data.sectors = getCheckedValues('sectors');
data.locations = getCheckedValues('locations');
data.min_value = document.getElementById('minValue').value ? parseInt(document.getElementById('minValue').value) : null;
data.max_value = document.getElementById('maxValue').value ? parseInt(document.getElementById('maxValue').value) : null;
}
const response = await fetch('/api/alerts/preferences', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify(data)
});
if (!response.ok) {
const error = await response.json();
showError(error.error || 'Failed to save preferences');
return;
}
showSuccess(`${section === 'company' ? 'Company Profile' : 'Alert Preferences'} saved successfully!`);
} catch (error) {
console.error('Error saving:', error);
showError('Failed to save preferences');
}
}
function setupTagInput(containerId) {
const container = document.getElementById(containerId);
const input = container.querySelector('.tag-input');
container.addEventListener('click', () => {
input.focus();
});
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
const value = input.value.trim();
if (value) {
addTag(containerId, value);
input.value = '';
}
}
});
input.addEventListener('focus', () => {
container.classList.add('focused');
});
input.addEventListener('blur', () => {
container.classList.remove('focused');
});
}
function addTag(containerId, value) {
const container = document.getElementById(containerId);
const input = container.querySelector('.tag-input');
const tag = document.createElement('div');
tag.className = 'tag';
tag.innerHTML = `
${value}
<button type="button">×</button>
`;
tag.querySelector('button').addEventListener('click', () => {
tag.remove();
});
container.insertBefore(tag, input);
}
function getTags(containerId) {
const container = document.getElementById(containerId);
return Array.from(container.querySelectorAll('.tag'))
.map(tag => tag.textContent.trim().replace('×', '').trim());
}
function getCheckedValues(name) {
return Array.from(document.querySelectorAll(`input[name="${name}"]:checked`))
.map(cb => cb.value);
}
function showSuccess(message) {
const el = document.getElementById('successMessage');
el.textContent = message;
el.classList.add('show');
setTimeout(() => el.classList.remove('show'), 5000);
}
function showError(message) {
const el = document.getElementById('errorMessage');
el.textContent = message;
el.classList.add('show');
setTimeout(() => el.classList.remove('show'), 5000);
}
</script>
</body>
</html>

View File

@@ -0,0 +1,146 @@
// Mobile Menu Toggle
const mobileToggle = document.querySelector('.mobile-toggle');
const navMenu = document.querySelector('.nav-menu');
if (mobileToggle) {
mobileToggle.addEventListener('click', () => {
navMenu.classList.toggle('active');
});
}
// Close mobile menu when clicking a link
const navLinks = document.querySelectorAll('.nav-menu a');
navLinks.forEach(link => {
link.addEventListener('click', () => {
navMenu.classList.remove('active');
});
});
// Smooth Scrolling
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
// Only prevent default for hash links, not for regular links
if (this.getAttribute('href').startsWith('#')) {
e.preventDefault();
const target = document.querySelector(this.getAttribute('href'));
if (target) {
const headerOffset = 80;
const elementPosition = target.getBoundingClientRect().top;
const offsetPosition = elementPosition + window.pageYOffset - headerOffset;
window.scrollTo({
top: offsetPosition,
behavior: 'smooth'
});
}
}
});
});
// FAQ Accordion
const faqItems = document.querySelectorAll('.faq-item');
faqItems.forEach(item => {
const question = item.querySelector('.faq-question');
question.addEventListener('click', () => {
const isActive = item.classList.contains('active');
// Close all FAQ items
faqItems.forEach(faq => faq.classList.remove('active'));
// Open clicked item if it wasn't active
if (!isActive) {
item.classList.add('active');
}
});
});
// Signup Form Handling
const signupForm = document.getElementById('signupForm');
const formMessage = document.getElementById('formMessage');
if (signupForm) {
signupForm.addEventListener('submit', async (e) => {
e.preventDefault();
const emailInput = document.getElementById('email');
const email = emailInput.value.trim();
// Basic validation
if (!email || !isValidEmail(email)) {
showMessage('Please enter a valid email address.', 'error');
return;
}
// Get submit button
const submitBtn = signupForm.querySelector('button[type="submit"]');
const originalBtnText = submitBtn.textContent;
// Disable button and show loading state
submitBtn.disabled = true;
submitBtn.textContent = 'Redirecting...';
// Redirect to signup page after a brief delay
setTimeout(() => {
window.location.href = '/signup.html';
}, 300);
});
}
function isValidEmail(email) {
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return re.test(email);
}
function showMessage(message, type) {
formMessage.textContent = message;
formMessage.className = `form-message ${type}`;
// Auto-hide success messages after 5 seconds
if (type === 'success') {
setTimeout(() => {
formMessage.className = 'form-message';
}, 5000);
}
}
// Add scroll animation for header
let lastScroll = 0;
const header = document.querySelector('.header');
window.addEventListener('scroll', () => {
const currentScroll = window.pageYOffset;
if (currentScroll > 100) {
header.style.boxShadow = '0 2px 8px rgba(0,0,0,0.1)';
} else {
header.style.boxShadow = 'none';
}
lastScroll = currentScroll;
});
// Intersection Observer for fade-in animations
const observerOptions = {
threshold: 0.1,
rootMargin: '0px 0px -50px 0px'
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.style.opacity = '1';
entry.target.style.transform = 'translateY(0)';
}
});
}, observerOptions);
// Observe elements for animation
const animateElements = document.querySelectorAll('.feature-card, .step, .pricing-card, .testimonial-card');
animateElements.forEach(el => {
el.style.opacity = '0';
el.style.transform = 'translateY(20px)';
el.style.transition = 'opacity 0.6s ease, transform 0.6s ease';
observer.observe(el);
});

View File

@@ -0,0 +1,461 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Sign up for TenderRadar - AI-powered UK public sector tender intelligence">
<title>Sign Up | TenderRadar</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="icon" href="/favicon.ico" type="image/x-icon">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
<link rel="stylesheet" href="styles.css">
<style>
.auth-page {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #f9fafb 0%, #f3f4f6 100%);
padding: 1.5rem;
}
.auth-container {
width: 100%;
max-width: 450px;
}
.auth-card {
background: white;
border-radius: 0.75rem;
box-shadow: var(--shadow-lg);
padding: 2.5rem;
}
.auth-header {
text-align: center;
margin-bottom: 2rem;
}
.auth-header .logo-icon {
height: 50px;
margin-bottom: 1rem;
}
.auth-header h1 {
font-size: 1.875rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 0.5rem;
}
.auth-header p {
color: var(--text-secondary);
font-size: 0.9375rem;
}
.form-group {
margin-bottom: 1.25rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: var(--text-primary);
font-size: 0.875rem;
}
.form-group input,
.form-group select {
width: 100%;
padding: 0.75rem;
border: 1px solid var(--border);
border-radius: 0.5rem;
font-family: 'Inter', sans-serif;
font-size: 0.9375rem;
transition: all 0.2s;
}
.form-group input:focus,
.form-group select:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(30, 64, 175, 0.1);
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.form-row .form-group {
margin-bottom: 0;
}
@media (max-width: 600px) {
.form-row {
grid-template-columns: 1fr;
}
}
.password-group {
position: relative;
}
.password-toggle {
position: absolute;
right: 0.75rem;
top: 2.25rem;
background: none;
border: none;
cursor: pointer;
color: var(--text-secondary);
padding: 0.25rem 0.5rem;
}
.error {
color: #dc2626;
font-size: 0.875rem;
margin-top: 0.25rem;
display: none;
}
.error.show {
display: block;
}
.form-group.error-state input,
.form-group.error-state select {
border-color: #dc2626;
}
.submit-btn {
width: 100%;
padding: 0.75rem;
margin-top: 1rem;
}
.auth-footer {
text-align: center;
margin-top: 1.5rem;
}
.auth-footer p {
color: var(--text-secondary);
font-size: 0.875rem;
}
.auth-footer a {
color: var(--primary);
text-decoration: none;
font-weight: 500;
}
.auth-footer a:hover {
text-decoration: underline;
}
.loading {
display: none;
text-align: center;
color: var(--text-secondary);
}
.success-message {
background: #ecfdf5;
color: #065f46;
padding: 0.875rem;
border-radius: 0.5rem;
margin-bottom: 1rem;
display: none;
}
.success-message.show {
display: block;
}
.error-message {
background: #fef2f2;
color: #7f1d1d;
padding: 0.875rem;
border-radius: 0.5rem;
margin-bottom: 1rem;
display: none;
}
.error-message.show {
display: block;
}
.terms {
font-size: 0.8125rem;
color: var(--text-secondary);
margin-top: 1rem;
line-height: 1.5;
}
.terms a {
color: var(--primary);
text-decoration: none;
}
.terms a:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<!-- Header/Navigation -->
<header class="header">
<nav class="nav container">
<a href="/" class="nav-brand">
<img src="/logo.png" alt="TenderRadar" class="logo-icon">
</a>
<ul class="nav-menu">
<li><a href="/#features">Features</a></li>
<li><a href="/#pricing">Pricing</a></li>
<li><a href="login.html" class="btn btn-secondary btn-sm">Sign In</a></li>
</ul>
<button class="mobile-toggle" aria-label="Toggle menu">
<span></span>
<span></span>
<span></span>
</button>
</nav>
</header>
<!-- Signup Form -->
<section class="auth-page">
<div class="auth-container">
<div class="auth-card">
<div class="auth-header">
<img src="/logo.png" alt="TenderRadar" class="logo-icon">
<h1>Create Account</h1>
<p>Start your 14-day free trial</p>
</div>
<div class="success-message" id="successMessage">
Account created successfully! Redirecting to dashboard...
</div>
<div class="error-message" id="errorMessage"></div>
<form id="signupForm" class="signup-form">
<div class="form-group">
<label for="companyName">Company Name *</label>
<input type="text" id="companyName" name="companyName" placeholder="Your company name" required>
<div class="error" id="companyNameError"></div>
</div>
<div class="form-group">
<label for="email">Work Email *</label>
<input type="email" id="email" name="email" placeholder="you@company.com" required>
<div class="error" id="emailError"></div>
</div>
<div class="form-row">
<div class="form-group">
<label for="industry">Industry/Sector *</label>
<select id="industry" name="industry" required>
<option value="">Select sector...</option>
<option value="technology">Technology</option>
<option value="construction">Construction</option>
<option value="consulting">Consulting</option>
<option value="engineering">Engineering</option>
<option value="healthcare">Healthcare</option>
<option value="facilities">Facilities & Maintenance</option>
<option value="security">Security</option>
<option value="transport">Transport & Logistics</option>
<option value="training">Training & Education</option>
<option value="financial">Financial Services</option>
<option value="other">Other</option>
</select>
<div class="error" id="industryError"></div>
</div>
<div class="form-group">
<label for="companySize">Company Size *</label>
<select id="companySize" name="companySize" required>
<option value="">Select size...</option>
<option value="1-10">1-10 employees</option>
<option value="11-50">11-50 employees</option>
<option value="51-250">51-250 employees</option>
<option value="250+">250+ employees</option>
</select>
<div class="error" id="companySizeError"></div>
</div>
</div>
<div class="form-group password-group">
<label for="password">Password *</label>
<input type="password" id="password" name="password" placeholder="At least 8 characters" required>
<button type="button" class="password-toggle" id="togglePassword">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
<circle cx="12" cy="12" r="3"/>
</svg>
</button>
<div class="error" id="passwordError"></div>
</div>
<div class="form-group password-group">
<label for="confirmPassword">Confirm Password *</label>
<input type="password" id="confirmPassword" name="confirmPassword" placeholder="Confirm your password" required>
<button type="button" class="password-toggle" id="toggleConfirmPassword">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
<circle cx="12" cy="12" r="3"/>
</svg>
</button>
<div class="error" id="confirmPasswordError"></div>
</div>
<button type="submit" class="btn btn-primary submit-btn" id="submitBtn">Create Account</button>
<div class="terms">
By creating an account, you agree to our <a href="#">Terms of Service</a> and <a href="#">Privacy Policy</a>
</div>
</form>
<div class="auth-footer">
<p>Already have an account? <a href="login.html">Sign in here</a></p>
</div>
</div>
</div>
</section>
<script>
const form = document.getElementById('signupForm');
const submitBtn = document.getElementById('submitBtn');
const errorMessage = document.getElementById('errorMessage');
const successMessage = document.getElementById('successMessage');
// Password visibility toggles
document.getElementById('togglePassword').addEventListener('click', function(e) {
e.preventDefault();
const input = document.getElementById('password');
input.type = input.type === 'password' ? 'text' : 'password';
});
document.getElementById('toggleConfirmPassword').addEventListener('click', function(e) {
e.preventDefault();
const input = document.getElementById('confirmPassword');
input.type = input.type === 'password' ? 'text' : 'password';
});
// Form validation
function validateForm() {
const errors = {};
const companyName = document.getElementById('companyName').value.trim();
const email = document.getElementById('email').value.trim();
const industry = document.getElementById('industry').value;
const companySize = document.getElementById('companySize').value;
const password = document.getElementById('password').value;
const confirmPassword = document.getElementById('confirmPassword').value;
// Clear previous errors
document.querySelectorAll('.form-group.error-state').forEach(el => {
el.classList.remove('error-state');
});
document.querySelectorAll('.error').forEach(el => {
el.classList.remove('show');
el.textContent = '';
});
if (!companyName) {
errors.companyName = 'Company name is required';
}
if (!email) {
errors.email = 'Email is required';
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
errors.email = 'Please enter a valid email address';
}
if (!industry) {
errors.industry = 'Please select an industry';
}
if (!companySize) {
errors.companySize = 'Please select company size';
}
if (!password) {
errors.password = 'Password is required';
} else if (password.length < 8) {
errors.password = 'Password must be at least 8 characters';
}
if (!confirmPassword) {
errors.confirmPassword = 'Please confirm your password';
} else if (password !== confirmPassword) {
errors.confirmPassword = 'Passwords do not match';
}
// Display errors
Object.keys(errors).forEach(field => {
const errorEl = document.getElementById(field + 'Error');
const formGroup = errorEl.closest('.form-group');
formGroup.classList.add('error-state');
errorEl.textContent = errors[field];
errorEl.classList.add('show');
});
return Object.keys(errors).length === 0;
}
form.addEventListener('submit', async (e) => {
e.preventDefault();
if (!validateForm()) {
return;
}
errorMessage.classList.remove('show');
errorMessage.textContent = '';
submitBtn.disabled = true;
submitBtn.textContent = 'Creating account...';
try {
const response = await fetch('/api/auth/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
company_name: document.getElementById('companyName').value.trim(),
email: document.getElementById('email').value.trim(),
password: document.getElementById('password').value,
tier: 'free'
})
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Registration failed');
}
// Store token and redirect
localStorage.setItem('token', data.token);
localStorage.setItem('user', JSON.stringify(data.user));
successMessage.classList.add('show');
setTimeout(() => {
window.location.href = '/dashboard.html';
}, 1500);
} catch (error) {
errorMessage.textContent = error.message;
errorMessage.classList.add('show');
submitBtn.disabled = false;
submitBtn.textContent = 'Create Account';
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,909 @@
/* Reset and Base Styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--primary: #1e40af;
--primary-dark: #1e3a8a;
--primary-light: #3b82f6;
--accent: #f59e0b;
--text-primary: #1f2937;
--text-secondary: #6b7280;
--text-light: #9ca3af;
--bg-primary: #ffffff;
--bg-secondary: #f9fafb;
--bg-alt: #f3f4f6;
--border: #e5e7eb;
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
color: var(--text-primary);
line-height: 1.6;
background: var(--bg-primary);
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 1.5rem;
}
/* Header */
.header {
position: sticky;
top: 0;
background: rgba(255, 255, 255, 0.98);
backdrop-filter: blur(10px);
border-bottom: 1px solid var(--border);
z-index: 1000;
box-shadow: var(--shadow-sm);
}
.nav {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.875rem 1.5rem;
min-height: 72px;
}
.nav-brand {
display: flex;
align-items: center;
gap: 0.75rem;
font-weight: 700;
font-size: 1.25rem;
color: var(--primary);
text-decoration: none;
}
.logo-icon {
width: auto;
height: 65px;
color: var(--primary);
display: block;
}
.logo-text {
color: var(--text-primary);
}
.nav-menu {
display: flex;
list-style: none;
align-items: center;
gap: 2rem;
}
.nav-menu a {
color: var(--text-secondary);
text-decoration: none;
font-weight: 500;
transition: color 0.2s;
font-size: 0.9375rem;
}
.nav-menu a:hover {
color: var(--primary);
}
.mobile-toggle {
display: none;
flex-direction: column;
gap: 4px;
background: none;
border: none;
cursor: pointer;
padding: 0.5rem;
}
.mobile-toggle span {
width: 24px;
height: 2px;
background: var(--text-primary);
transition: all 0.3s;
}
/* Buttons */
.btn {
display: inline-block;
padding: 0.625rem 1.5rem;
border-radius: 0.5rem;
font-weight: 600;
text-decoration: none;
text-align: center;
transition: all 0.2s;
border: none;
cursor: pointer;
font-size: 1rem;
line-height: 1.5;
}
.btn-primary {
background: var(--primary);
color: white;
}
.btn-primary:hover {
background: var(--primary-dark);
transform: translateY(-1px);
box-shadow: var(--shadow-md);
}
.btn-secondary {
background: transparent;
color: var(--primary);
border: 2px solid var(--primary);
}
.btn-secondary:hover {
background: var(--primary);
color: white;
transform: translateY(-1px);
}
.btn-outline {
background: transparent;
color: var(--primary);
border: 2px solid var(--primary);
}
.btn-outline:hover {
background: var(--primary);
color: white;
transform: translateY(-1px);
}
.btn-sm {
padding: 0.5rem 1.25rem;
font-size: 0.875rem;
}
.btn-lg {
padding: 0.875rem 2rem;
font-size: 1.0625rem;
}
/* Hero Section */
.hero {
padding: 5rem 0 6rem;
background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
}
.hero-content {
text-align: center;
max-width: 900px;
margin: 0 auto;
}
.badge {
display: inline-block;
padding: 0.5rem 1rem;
background: var(--accent);
color: white;
border-radius: 2rem;
font-size: 0.875rem;
font-weight: 600;
margin-bottom: 1.75rem;
}
.hero-title {
font-size: 3.5rem;
font-weight: 700;
line-height: 1.15;
color: var(--text-primary);
margin-bottom: 1.5rem;
letter-spacing: -0.02em;
}
.hero-subtitle {
font-size: 1.25rem;
color: var(--text-secondary);
margin-bottom: 2.5rem;
line-height: 1.7;
max-width: 760px;
margin-left: auto;
margin-right: auto;
}
.hero-cta {
display: flex;
gap: 0.875rem;
justify-content: center;
margin-bottom: 4.5rem;
flex-wrap: wrap;
}
.hero-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 3rem;
max-width: 800px;
margin: 0 auto;
padding: 2.5rem 0 0;
border-top: 1px solid rgba(30, 64, 175, 0.1);
}
.stat {
text-align: center;
}
.stat-number {
font-size: 2.75rem;
font-weight: 700;
color: var(--primary);
line-height: 1;
margin-bottom: 0.625rem;
}
.stat-label {
font-size: 0.9375rem;
color: var(--text-secondary);
line-height: 1.4;
}
/* Section Styles */
.section {
padding: 5rem 0;
}
.section-alt {
background: var(--bg-secondary);
}
.section-header {
text-align: center;
margin-bottom: 4rem;
}
.section-title {
font-size: 2.5rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 1rem;
letter-spacing: -0.01em;
}
.section-subtitle {
font-size: 1.125rem;
color: var(--text-secondary);
line-height: 1.6;
}
/* Features Section */
.features-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 2rem;
}
.feature-card {
background: white;
padding: 2rem;
border-radius: 1rem;
box-shadow: var(--shadow-sm);
transition: all 0.3s ease;
border: 1px solid var(--border);
}
.feature-card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-lg);
border-color: var(--primary-light);
}
.feature-icon {
width: 48px;
height: 48px;
color: var(--primary);
margin-bottom: 1.5rem;
}
.feature-card h3 {
font-size: 1.375rem;
font-weight: 600;
margin-bottom: 0.875rem;
color: var(--text-primary);
}
.feature-card p {
color: var(--text-secondary);
line-height: 1.7;
font-size: 0.9375rem;
}
/* How It Works Section */
.steps {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 3rem;
max-width: 1100px;
margin: 0 auto;
}
.step {
text-align: center;
}
.step-number {
width: 72px;
height: 72px;
margin: 0 auto 1.5rem;
background: var(--primary);
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 2rem;
font-weight: 700;
box-shadow: var(--shadow-md);
}
.step h3 {
font-size: 1.375rem;
font-weight: 600;
margin-bottom: 0.875rem;
color: var(--text-primary);
}
.step p {
color: var(--text-secondary);
line-height: 1.7;
font-size: 0.9375rem;
}
/* Pricing Section */
.pricing-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem;
max-width: 1100px;
margin: 0 auto;
align-items: start;
}
.pricing-card {
background: white;
border: 2px solid var(--border);
border-radius: 1rem;
padding: 2.5rem;
text-align: center;
position: relative;
transition: all 0.3s ease;
}
.pricing-card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-xl);
border-color: var(--primary-light);
}
.pricing-card-featured {
border-color: var(--primary);
box-shadow: var(--shadow-lg);
transform: scale(1.05);
}
.pricing-card-featured:hover {
transform: scale(1.05) translateY(-4px);
}
.pricing-badge {
position: absolute;
top: -16px;
left: 50%;
transform: translateX(-50%);
background: var(--accent);
color: white;
padding: 0.5rem 1.25rem;
border-radius: 2rem;
font-size: 0.875rem;
font-weight: 600;
box-shadow: var(--shadow-md);
}
.pricing-header h3 {
font-size: 1.75rem;
font-weight: 600;
margin-bottom: 1.25rem;
color: var(--text-primary);
}
.price {
font-size: 3rem;
font-weight: 700;
color: var(--primary);
margin-bottom: 2rem;
line-height: 1;
}
.currency {
font-size: 1.75rem;
vertical-align: super;
font-weight: 600;
}
.period {
font-size: 1.125rem;
color: var(--text-secondary);
font-weight: 400;
}
.pricing-features {
list-style: none;
text-align: left;
margin-bottom: 2rem;
}
.pricing-features li {
padding: 0.875rem 0;
border-bottom: 1px solid var(--border);
color: var(--text-secondary);
position: relative;
padding-left: 2rem;
font-size: 0.9375rem;
}
.pricing-features li:before {
content: "✓";
position: absolute;
left: 0;
color: var(--primary);
font-weight: 700;
font-size: 1.125rem;
}
.pricing-features li:last-child {
border-bottom: none;
}
/* Testimonials Section */
.testimonials-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 2rem;
}
.testimonial-card {
background: white;
padding: 2.25rem;
border-radius: 1rem;
box-shadow: var(--shadow-sm);
border: 1px solid var(--border);
transition: all 0.3s ease;
}
.testimonial-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.testimonial-quote {
font-size: 1.0625rem;
color: var(--text-primary);
margin-bottom: 1.5rem;
line-height: 1.7;
font-style: italic;
}
.testimonial-quote:before {
content: '"';
font-size: 3.5rem;
color: var(--primary);
opacity: 0.3;
line-height: 0;
display: block;
margin-bottom: 1rem;
font-style: normal;
}
.testimonial-author {
margin-top: 1.5rem;
padding-top: 1.25rem;
border-top: 1px solid var(--border);
}
.testimonial-name {
font-weight: 600;
color: var(--text-primary);
font-size: 1rem;
margin-bottom: 0.25rem;
}
.testimonial-company {
font-size: 0.875rem;
color: var(--text-secondary);
}
/* FAQ Section */
.faq-list {
max-width: 800px;
margin: 0 auto;
}
.faq-item {
border-bottom: 1px solid var(--border);
}
.faq-item:last-child {
border-bottom: none;
}
.faq-question {
width: 100%;
padding: 1.5rem 0;
background: none;
border: none;
text-align: left;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
gap: 1.5rem;
font-size: 1.0625rem;
font-weight: 600;
color: var(--text-primary);
transition: color 0.2s;
}
.faq-question:hover {
color: var(--primary);
}
.faq-icon {
width: 24px;
height: 24px;
min-width: 24px;
color: var(--primary);
transition: transform 0.3s ease;
}
.faq-item.active .faq-icon {
transform: rotate(180deg);
}
.faq-answer {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease;
}
.faq-item.active .faq-answer {
max-height: 500px;
padding-bottom: 1.5rem;
}
.faq-answer p {
color: var(--text-secondary);
line-height: 1.7;
font-size: 0.9375rem;
}
/* Signup Section */
.signup {
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
color: white;
}
.signup-content {
max-width: 700px;
margin: 0 auto;
text-align: center;
}
.signup-title {
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 1rem;
letter-spacing: -0.01em;
}
.signup-subtitle {
font-size: 1.25rem;
margin-bottom: 2.5rem;
opacity: 0.95;
line-height: 1.6;
}
.signup-form {
max-width: 600px;
margin: 0 auto;
}
.form-group {
display: flex;
gap: 0.875rem;
margin-bottom: 1rem;
}
.form-group input {
flex: 1;
padding: 1rem 1.5rem;
border: 2px solid transparent;
border-radius: 0.5rem;
font-size: 1rem;
outline: none;
transition: border-color 0.2s;
}
.form-group input:focus {
border-color: var(--accent);
}
.form-note {
font-size: 0.875rem;
opacity: 0.85;
margin-top: 1rem;
line-height: 1.5;
}
.form-message {
margin-top: 1rem;
padding: 1rem;
border-radius: 0.5rem;
display: none;
}
.form-message.success {
display: block;
background: rgba(34, 197, 94, 0.2);
border: 1px solid rgba(34, 197, 94, 0.4);
color: #dcfce7;
}
.form-message.error {
display: block;
background: rgba(239, 68, 68, 0.2);
border: 1px solid rgba(239, 68, 68, 0.4);
color: #fecaca;
}
/* Footer */
.footer {
background: var(--text-primary);
color: white;
padding: 4rem 0 2rem;
}
.footer-grid {
display: grid;
grid-template-columns: 2fr 1fr 1fr 1fr;
gap: 3rem;
margin-bottom: 3rem;
}
.footer-brand {
display: flex;
align-items: center;
gap: 0.75rem;
font-weight: 700;
font-size: 1.25rem;
margin-bottom: 1rem;
}
.footer-brand .logo-icon {
height: 52px;
}
.footer-desc {
color: var(--text-light);
line-height: 1.7;
font-size: 0.9375rem;
}
.footer-col h4 {
margin-bottom: 1.125rem;
font-size: 1.0625rem;
font-weight: 600;
}
.footer-col ul {
list-style: none;
}
.footer-col ul li {
margin-bottom: 0.75rem;
}
.footer-col a {
color: var(--text-light);
text-decoration: none;
transition: color 0.2s;
font-size: 0.9375rem;
}
.footer-col a:hover {
color: white;
}
.footer-bottom {
padding-top: 2rem;
border-top: 1px solid rgba(255, 255, 255, 0.1);
text-align: center;
color: var(--text-light);
font-size: 0.875rem;
}
/* Responsive */
@media (max-width: 968px) {
.pricing-grid {
grid-template-columns: 1fr;
max-width: 420px;
}
.pricing-card-featured {
transform: scale(1);
}
}
@media (max-width: 768px) {
.mobile-toggle {
display: flex;
}
.nav {
padding: 0.75rem 1.5rem;
}
.nav-menu {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: white;
flex-direction: column;
padding: 1.5rem;
box-shadow: var(--shadow-lg);
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease;
gap: 0;
}
.nav-menu.active {
max-height: 500px;
border-bottom: 1px solid var(--border);
}
.nav-menu li {
width: 100%;
padding: 0.75rem 0;
border-bottom: 1px solid var(--border);
}
.nav-menu li:last-child {
border-bottom: none;
}
.nav-menu a {
display: block;
}
.hero {
padding: 4rem 0 5rem;
}
.hero-title {
font-size: 2.5rem;
}
.hero-subtitle {
font-size: 1.125rem;
}
.hero-cta {
flex-direction: column;
max-width: 300px;
margin-left: auto;
margin-right: auto;
}
.hero-cta .btn {
width: 100%;
}
.hero-stats {
grid-template-columns: 1fr;
gap: 2rem;
max-width: 300px;
}
.section-title {
font-size: 2rem;
}
.section {
padding: 4rem 0;
}
.features-grid,
.steps,
.testimonials-grid {
grid-template-columns: 1fr;
}
.form-group {
flex-direction: column;
}
.form-group .btn {
width: 100%;
}
.footer-grid {
grid-template-columns: 1fr;
gap: 2.5rem;
}
}
@media (max-width: 480px) {
.hero {
padding: 3rem 0 4rem;
}
.hero-title {
font-size: 2rem;
line-height: 1.2;
}
.hero-subtitle {
font-size: 1rem;
}
.section {
padding: 3rem 0;
}
.section-title {
font-size: 1.75rem;
}
.btn-lg {
padding: 0.75rem 1.5rem;
font-size: 1rem;
}
.stat-number {
font-size: 2.25rem;
}
.stat-label {
font-size: 0.875rem;
}
}
/* Enhanced Logo Styling */
.nav-brand .logo-icon,
.header .logo-icon,
img.logo-icon {
width: auto !important;
height: 70px !important;
max-height: 70px !important;
color: var(--primary);
display: block;
}
/* Ensure all stat numbers are styled consistently */
.hero-stats .stat .stat-number {
font-size: 2.75rem !important;
font-weight: 700 !important;
color: var(--primary) !important;
line-height: 1;
}
/* Force all stat numbers to use the same blue color - ultra specific */
.hero .hero-stats .stat .stat-number,
.hero-content .hero-stats .stat .stat-number,
div.stat div.stat-number {
color: #1e40af !important;
font-size: 2.75rem !important;
font-weight: 700 !important;
}