Files
Peter Foster f969ecae04 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
2026-02-14 14:17:15 +00:00

939 lines
37 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>