feat: initial .NET 9 static site for SilverDOCK deployment
This commit is contained in:
2453
wwwroot/css/style.css
Normal file
2453
wwwroot/css/style.css
Normal file
File diff suppressed because it is too large
Load Diff
1322
wwwroot/index.html
Normal file
1322
wwwroot/index.html
Normal file
File diff suppressed because it is too large
Load Diff
616
wwwroot/js/main.js
Normal file
616
wwwroot/js/main.js
Normal file
@@ -0,0 +1,616 @@
|
||||
/**
|
||||
* Privacy First - Main JavaScript
|
||||
* Handles animations, interactions, and visual effects
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// ==========================================================================
|
||||
// Configuration
|
||||
// ==========================================================================
|
||||
|
||||
const CONFIG = {
|
||||
particles: {
|
||||
count: 80,
|
||||
color: '#00d4aa',
|
||||
colorSecondary: '#7c5cff',
|
||||
maxDistance: 120,
|
||||
speed: 0.3,
|
||||
mouseRadius: 150
|
||||
},
|
||||
scroll: {
|
||||
revealOffset: 100,
|
||||
headerOffset: 50
|
||||
}
|
||||
};
|
||||
|
||||
// ==========================================================================
|
||||
// Particle System
|
||||
// ==========================================================================
|
||||
|
||||
class ParticleSystem {
|
||||
constructor(canvasId) {
|
||||
this.canvas = document.getElementById(canvasId);
|
||||
if (!this.canvas) return;
|
||||
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
this.particles = [];
|
||||
this.mouse = { x: null, y: null };
|
||||
this.animationId = null;
|
||||
|
||||
this.init();
|
||||
this.addEventListeners();
|
||||
this.animate();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.resize();
|
||||
this.createParticles();
|
||||
}
|
||||
|
||||
resize() {
|
||||
this.width = this.canvas.width = this.canvas.offsetWidth;
|
||||
this.height = this.canvas.height = this.canvas.offsetHeight;
|
||||
}
|
||||
|
||||
createParticles() {
|
||||
this.particles = [];
|
||||
const count = Math.min(CONFIG.particles.count, Math.floor((this.width * this.height) / 15000));
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
this.particles.push({
|
||||
x: Math.random() * this.width,
|
||||
y: Math.random() * this.height,
|
||||
vx: (Math.random() - 0.5) * CONFIG.particles.speed,
|
||||
vy: (Math.random() - 0.5) * CONFIG.particles.speed,
|
||||
radius: Math.random() * 2 + 1,
|
||||
color: Math.random() > 0.5 ? CONFIG.particles.color : CONFIG.particles.colorSecondary,
|
||||
alpha: Math.random() * 0.5 + 0.3
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
addEventListeners() {
|
||||
window.addEventListener('resize', () => {
|
||||
this.resize();
|
||||
this.createParticles();
|
||||
});
|
||||
|
||||
this.canvas.addEventListener('mousemove', (e) => {
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
this.mouse.x = e.clientX - rect.left;
|
||||
this.mouse.y = e.clientY - rect.top;
|
||||
});
|
||||
|
||||
this.canvas.addEventListener('mouseleave', () => {
|
||||
this.mouse.x = null;
|
||||
this.mouse.y = null;
|
||||
});
|
||||
}
|
||||
|
||||
drawParticle(particle) {
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(particle.x, particle.y, particle.radius, 0, Math.PI * 2);
|
||||
this.ctx.fillStyle = particle.color;
|
||||
this.ctx.globalAlpha = particle.alpha;
|
||||
this.ctx.fill();
|
||||
this.ctx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
drawConnections() {
|
||||
for (let i = 0; i < this.particles.length; i++) {
|
||||
for (let j = i + 1; j < this.particles.length; j++) {
|
||||
const dx = this.particles[i].x - this.particles[j].x;
|
||||
const dy = this.particles[i].y - this.particles[j].y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance < CONFIG.particles.maxDistance) {
|
||||
const alpha = (1 - distance / CONFIG.particles.maxDistance) * 0.3;
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(this.particles[i].x, this.particles[i].y);
|
||||
this.ctx.lineTo(this.particles[j].x, this.particles[j].y);
|
||||
this.ctx.strokeStyle = CONFIG.particles.color;
|
||||
this.ctx.globalAlpha = alpha;
|
||||
this.ctx.lineWidth = 0.5;
|
||||
this.ctx.stroke();
|
||||
this.ctx.globalAlpha = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateParticle(particle) {
|
||||
// Mouse interaction
|
||||
if (this.mouse.x !== null && this.mouse.y !== null) {
|
||||
const dx = particle.x - this.mouse.x;
|
||||
const dy = particle.y - this.mouse.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance < CONFIG.particles.mouseRadius) {
|
||||
const force = (CONFIG.particles.mouseRadius - distance) / CONFIG.particles.mouseRadius;
|
||||
particle.vx += (dx / distance) * force * 0.02;
|
||||
particle.vy += (dy / distance) * force * 0.02;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply velocity
|
||||
particle.x += particle.vx;
|
||||
particle.y += particle.vy;
|
||||
|
||||
// Apply friction
|
||||
particle.vx *= 0.99;
|
||||
particle.vy *= 0.99;
|
||||
|
||||
// Boundary check
|
||||
if (particle.x < 0 || particle.x > this.width) {
|
||||
particle.vx *= -1;
|
||||
particle.x = Math.max(0, Math.min(this.width, particle.x));
|
||||
}
|
||||
if (particle.y < 0 || particle.y > this.height) {
|
||||
particle.vy *= -1;
|
||||
particle.y = Math.max(0, Math.min(this.height, particle.y));
|
||||
}
|
||||
}
|
||||
|
||||
animate() {
|
||||
this.ctx.clearRect(0, 0, this.width, this.height);
|
||||
|
||||
// Update and draw particles
|
||||
this.particles.forEach(particle => {
|
||||
this.updateParticle(particle);
|
||||
this.drawParticle(particle);
|
||||
});
|
||||
|
||||
// Draw connections
|
||||
this.drawConnections();
|
||||
|
||||
this.animationId = requestAnimationFrame(() => this.animate());
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.animationId) {
|
||||
cancelAnimationFrame(this.animationId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Scroll Reveal
|
||||
// ==========================================================================
|
||||
|
||||
class ScrollReveal {
|
||||
constructor() {
|
||||
this.elements = document.querySelectorAll('.reveal');
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// Initial check
|
||||
this.checkElements();
|
||||
|
||||
// Scroll listener with throttle
|
||||
let ticking = false;
|
||||
window.addEventListener('scroll', () => {
|
||||
if (!ticking) {
|
||||
window.requestAnimationFrame(() => {
|
||||
this.checkElements();
|
||||
ticking = false;
|
||||
});
|
||||
ticking = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
checkElements() {
|
||||
this.elements.forEach(element => {
|
||||
if (this.isInViewport(element)) {
|
||||
element.classList.add('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
isInViewport(element) {
|
||||
const rect = element.getBoundingClientRect();
|
||||
return (
|
||||
rect.top <= (window.innerHeight || document.documentElement.clientHeight) - CONFIG.scroll.revealOffset
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Header Controller
|
||||
// ==========================================================================
|
||||
|
||||
class HeaderController {
|
||||
constructor() {
|
||||
this.header = document.getElementById('header');
|
||||
this.lastScroll = 0;
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
window.addEventListener('scroll', () => this.handleScroll());
|
||||
this.handleScroll(); // Initial check
|
||||
}
|
||||
|
||||
handleScroll() {
|
||||
const currentScroll = window.pageYOffset;
|
||||
|
||||
if (currentScroll > CONFIG.scroll.headerOffset) {
|
||||
this.header.classList.add('scrolled');
|
||||
} else {
|
||||
this.header.classList.remove('scrolled');
|
||||
}
|
||||
|
||||
this.lastScroll = currentScroll;
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Mobile Navigation
|
||||
// ==========================================================================
|
||||
|
||||
class MobileNav {
|
||||
constructor() {
|
||||
this.toggle = document.getElementById('nav-toggle');
|
||||
this.close = document.getElementById('nav-close');
|
||||
this.menu = document.getElementById('nav-menu');
|
||||
this.links = document.querySelectorAll('.nav__link');
|
||||
|
||||
if (this.toggle && this.menu) {
|
||||
this.init();
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
this.toggle.addEventListener('click', () => this.openMenu());
|
||||
this.close.addEventListener('click', () => this.closeMenu());
|
||||
|
||||
// Close on link click
|
||||
this.links.forEach(link => {
|
||||
link.addEventListener('click', () => this.closeMenu());
|
||||
});
|
||||
|
||||
// Close on outside click
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!this.menu.contains(e.target) && !this.toggle.contains(e.target)) {
|
||||
this.closeMenu();
|
||||
}
|
||||
});
|
||||
|
||||
// Close on escape
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
this.closeMenu();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
openMenu() {
|
||||
this.menu.classList.add('active');
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
closeMenu() {
|
||||
this.menu.classList.remove('active');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Pricing Toggle
|
||||
// ==========================================================================
|
||||
|
||||
class PricingToggle {
|
||||
constructor() {
|
||||
this.toggle = document.getElementById('pricing-toggle');
|
||||
this.labels = document.querySelectorAll('.pricing__toggle-label');
|
||||
this.amounts = document.querySelectorAll('.pricing-card__amount[data-monthly]');
|
||||
this.isAnnual = false;
|
||||
|
||||
if (this.toggle) {
|
||||
this.init();
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
this.toggle.addEventListener('click', () => this.handleToggle());
|
||||
this.updateLabels();
|
||||
}
|
||||
|
||||
handleToggle() {
|
||||
this.isAnnual = !this.isAnnual;
|
||||
this.toggle.classList.toggle('annual', this.isAnnual);
|
||||
this.updatePrices();
|
||||
this.updateLabels();
|
||||
}
|
||||
|
||||
updatePrices() {
|
||||
this.amounts.forEach(amount => {
|
||||
const monthly = amount.dataset.monthly;
|
||||
const annual = amount.dataset.annual;
|
||||
amount.textContent = this.isAnnual ? annual : monthly;
|
||||
});
|
||||
}
|
||||
|
||||
updateLabels() {
|
||||
this.labels.forEach(label => {
|
||||
const period = label.dataset.period;
|
||||
if (period === 'annual') {
|
||||
label.classList.toggle('active', this.isAnnual);
|
||||
} else {
|
||||
label.classList.toggle('active', !this.isAnnual);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Smooth Scroll
|
||||
// ==========================================================================
|
||||
|
||||
class SmoothScroll {
|
||||
constructor() {
|
||||
this.links = document.querySelectorAll('a[href^="#"]');
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.links.forEach(link => {
|
||||
link.addEventListener('click', (e) => this.handleClick(e, link));
|
||||
});
|
||||
}
|
||||
|
||||
handleClick(e, link) {
|
||||
const href = link.getAttribute('href');
|
||||
if (href === '#') return;
|
||||
|
||||
const target = document.querySelector(href);
|
||||
if (target) {
|
||||
e.preventDefault();
|
||||
const headerOffset = 80;
|
||||
const elementPosition = target.getBoundingClientRect().top;
|
||||
const offsetPosition = elementPosition + window.pageYOffset - headerOffset;
|
||||
|
||||
window.scrollTo({
|
||||
top: offsetPosition,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Typing Effect for Hero
|
||||
// ==========================================================================
|
||||
|
||||
class TypeWriter {
|
||||
constructor(element, words, wait = 2000) {
|
||||
this.element = element;
|
||||
this.words = words;
|
||||
this.txt = '';
|
||||
this.wordIndex = 0;
|
||||
this.wait = parseInt(wait, 10);
|
||||
this.isDeleting = false;
|
||||
this.type();
|
||||
}
|
||||
|
||||
type() {
|
||||
const current = this.wordIndex % this.words.length;
|
||||
const fullTxt = this.words[current];
|
||||
|
||||
if (this.isDeleting) {
|
||||
this.txt = fullTxt.substring(0, this.txt.length - 1);
|
||||
} else {
|
||||
this.txt = fullTxt.substring(0, this.txt.length + 1);
|
||||
}
|
||||
|
||||
this.element.innerHTML = `<span class="txt">${this.txt}</span>`;
|
||||
|
||||
let typeSpeed = 100;
|
||||
|
||||
if (this.isDeleting) {
|
||||
typeSpeed /= 2;
|
||||
}
|
||||
|
||||
if (!this.isDeleting && this.txt === fullTxt) {
|
||||
typeSpeed = this.wait;
|
||||
this.isDeleting = true;
|
||||
} else if (this.isDeleting && this.txt === '') {
|
||||
this.isDeleting = false;
|
||||
this.wordIndex++;
|
||||
typeSpeed = 500;
|
||||
}
|
||||
|
||||
setTimeout(() => this.type(), typeSpeed);
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Form Handlers
|
||||
// ==========================================================================
|
||||
|
||||
class FormHandler {
|
||||
constructor() {
|
||||
this.notifyForm = document.querySelector('.notify-form');
|
||||
if (this.notifyForm) {
|
||||
this.init();
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
this.notifyForm.addEventListener('submit', (e) => this.handleSubmit(e));
|
||||
}
|
||||
|
||||
handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
const input = this.notifyForm.querySelector('input[type="email"]');
|
||||
const button = this.notifyForm.querySelector('button');
|
||||
|
||||
if (input.value && this.validateEmail(input.value)) {
|
||||
button.textContent = 'Subscribed!';
|
||||
button.disabled = true;
|
||||
input.disabled = true;
|
||||
|
||||
setTimeout(() => {
|
||||
button.textContent = 'Notify Me';
|
||||
button.disabled = false;
|
||||
input.disabled = false;
|
||||
input.value = '';
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
validateEmail(email) {
|
||||
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return re.test(email);
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Mockup Animations
|
||||
// ==========================================================================
|
||||
|
||||
class MockupAnimations {
|
||||
constructor() {
|
||||
this.mockupServices = document.querySelectorAll('.mockup__service');
|
||||
this.passportServices = document.querySelectorAll('.passport-card__service');
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// Cycle through mockup services
|
||||
if (this.mockupServices.length > 0) {
|
||||
this.cycleServices(this.mockupServices, 2500);
|
||||
}
|
||||
|
||||
// Cycle through passport services
|
||||
if (this.passportServices.length > 0) {
|
||||
this.cycleServices(this.passportServices, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
cycleServices(services, interval) {
|
||||
let currentIndex = 0;
|
||||
|
||||
setInterval(() => {
|
||||
services.forEach(s => s.classList.remove('active'));
|
||||
currentIndex = (currentIndex + 1) % services.length;
|
||||
services[currentIndex].classList.add('active');
|
||||
}, interval);
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Intersection Observer for Stats Animation
|
||||
// ==========================================================================
|
||||
|
||||
class StatsAnimation {
|
||||
constructor() {
|
||||
this.stats = document.querySelectorAll('.stat-card__value, .cp-stat__value');
|
||||
this.animated = new Set();
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting && !this.animated.has(entry.target)) {
|
||||
this.animateValue(entry.target);
|
||||
this.animated.add(entry.target);
|
||||
}
|
||||
});
|
||||
}, { threshold: 0.5 });
|
||||
|
||||
this.stats.forEach(stat => observer.observe(stat));
|
||||
}
|
||||
|
||||
animateValue(element) {
|
||||
const text = element.textContent;
|
||||
const match = text.match(/^([\d,.]+)/);
|
||||
|
||||
if (!match) return;
|
||||
|
||||
const finalValue = parseFloat(match[1].replace(/,/g, ''));
|
||||
const suffix = text.replace(match[1], '');
|
||||
const duration = 1500;
|
||||
const startTime = performance.now();
|
||||
|
||||
const animate = (currentTime) => {
|
||||
const elapsed = currentTime - startTime;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
|
||||
// Easing function
|
||||
const easeOutQuart = 1 - Math.pow(1 - progress, 4);
|
||||
const currentValue = finalValue * easeOutQuart;
|
||||
|
||||
// Format number
|
||||
let displayValue;
|
||||
if (finalValue >= 1000) {
|
||||
displayValue = Math.floor(currentValue).toLocaleString();
|
||||
} else if (finalValue >= 100) {
|
||||
displayValue = Math.floor(currentValue);
|
||||
} else if (finalValue % 1 !== 0) {
|
||||
displayValue = currentValue.toFixed(1);
|
||||
} else {
|
||||
displayValue = Math.floor(currentValue);
|
||||
}
|
||||
|
||||
element.textContent = displayValue + suffix;
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(animate);
|
||||
} else {
|
||||
element.textContent = text; // Restore original
|
||||
}
|
||||
};
|
||||
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Initialize Everything
|
||||
// ==========================================================================
|
||||
|
||||
function init() {
|
||||
// Initialize particle system
|
||||
new ParticleSystem('particle-canvas');
|
||||
|
||||
// Initialize scroll reveal
|
||||
new ScrollReveal();
|
||||
|
||||
// Initialize header controller
|
||||
new HeaderController();
|
||||
|
||||
// Initialize mobile navigation
|
||||
new MobileNav();
|
||||
|
||||
// Initialize pricing toggle
|
||||
new PricingToggle();
|
||||
|
||||
// Initialize smooth scroll
|
||||
new SmoothScroll();
|
||||
|
||||
// Initialize form handlers
|
||||
new FormHandler();
|
||||
|
||||
// Initialize mockup animations
|
||||
new MockupAnimations();
|
||||
|
||||
// Initialize stats animation
|
||||
new StatsAnimation();
|
||||
|
||||
// Log init complete
|
||||
console.log('Privacy First website initialized');
|
||||
}
|
||||
|
||||
// Run on DOM ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user