Refactor routing in App component to enhance navigation and improve error handling by integrating dynamic routes and updating the NotFound route.
This commit is contained in:
56
src/components/CTA.astro
Normal file
56
src/components/CTA.astro
Normal file
@@ -0,0 +1,56 @@
|
||||
---
|
||||
import { t } from '../utils/i18n';
|
||||
---
|
||||
|
||||
<section class="py-20 bg-gradient-to-br from-primary via-primary to-secondary text-primary-foreground relative overflow-hidden">
|
||||
<!-- Background decoration -->
|
||||
<div class="absolute inset-0 opacity-10">
|
||||
<div class="absolute top-10 left-10 w-32 h-32 rounded-full bg-white blur-2xl"></div>
|
||||
<div class="absolute bottom-10 right-10 w-48 h-48 rounded-full bg-white blur-3xl"></div>
|
||||
<div class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-64 h-64 rounded-full bg-white blur-3xl"></div>
|
||||
</div>
|
||||
|
||||
<div class="container-custom relative z-10">
|
||||
<div class="text-center max-w-4xl mx-auto animate-on-scroll">
|
||||
<!-- CTA heading -->
|
||||
<h2 class="text-3xl sm:text-4xl lg:text-5xl font-display font-bold mb-6">
|
||||
{t('cta.title')}
|
||||
</h2>
|
||||
|
||||
<!-- CTA subtitle -->
|
||||
<p class="text-lg sm:text-xl mb-8 opacity-90 leading-relaxed">
|
||||
{t('cta.subtitle')}
|
||||
</p>
|
||||
|
||||
<!-- CTA button -->
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center items-center">
|
||||
<a
|
||||
href="/contact"
|
||||
class="bg-background text-foreground hover:bg-background/90 px-8 py-4 text-lg font-semibold rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 hover:scale-105 inline-flex items-center group"
|
||||
>
|
||||
{t('cta.button')}
|
||||
<svg class="h-5 w-5 ml-2 group-hover:translate-x-1 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/>
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="tel:+391234567890"
|
||||
class="border-2 border-primary-foreground text-primary-foreground hover:bg-primary-foreground hover:text-primary px-8 py-4 text-lg font-semibold rounded-xl transition-all duration-300 hover:scale-105 inline-flex items-center"
|
||||
>
|
||||
Call Now
|
||||
<svg class="h-5 w-5 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Contact info -->
|
||||
<div class="mt-8 opacity-80">
|
||||
<p class="text-sm">
|
||||
📧 info@tiber365.it | 📞 +39 123 456 7890
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
222
src/components/ContactForm.astro
Normal file
222
src/components/ContactForm.astro
Normal file
@@ -0,0 +1,222 @@
|
||||
---
|
||||
import { t } from '../utils/i18n';
|
||||
---
|
||||
|
||||
<section class="py-20 bg-background">
|
||||
<div class="container-custom">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<!-- Section header -->
|
||||
<div class="text-center mb-12 animate-on-scroll">
|
||||
<h2 class="text-3xl sm:text-4xl lg:text-5xl font-display font-bold text-foreground mb-4">
|
||||
{t('contact.title')}
|
||||
</h2>
|
||||
<p class="text-lg sm:text-xl text-muted-foreground">
|
||||
{t('contact.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-12">
|
||||
<!-- Contact form -->
|
||||
<div class="animate-on-scroll">
|
||||
<form id="contact-form" class="space-y-6">
|
||||
<!-- Name field -->
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-foreground mb-2">
|
||||
{t('contact.form.name')} *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
required
|
||||
class="w-full px-4 py-3 border border-border rounded-lg bg-background text-foreground placeholder-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all"
|
||||
placeholder="John Doe"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Email field -->
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium text-foreground mb-2">
|
||||
{t('contact.form.email')} *
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
required
|
||||
class="w-full px-4 py-3 border border-border rounded-lg bg-background text-foreground placeholder-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all"
|
||||
placeholder="john@company.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Company field -->
|
||||
<div>
|
||||
<label for="company" class="block text-sm font-medium text-foreground mb-2">
|
||||
{t('contact.form.company')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="company"
|
||||
name="company"
|
||||
class="w-full px-4 py-3 border border-border rounded-lg bg-background text-foreground placeholder-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all"
|
||||
placeholder="Your Company"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Service field -->
|
||||
<div>
|
||||
<label for="service" class="block text-sm font-medium text-foreground mb-2">
|
||||
{t('contact.form.service')}
|
||||
</label>
|
||||
<select
|
||||
id="service"
|
||||
name="service"
|
||||
class="w-full px-4 py-3 border border-border rounded-lg bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all"
|
||||
>
|
||||
<option value="">Select a service</option>
|
||||
<option value="microsoft365">Microsoft 365 Support</option>
|
||||
<option value="management">Full M365 Management</option>
|
||||
<option value="networking">Networking & Infrastructure</option>
|
||||
<option value="hosting">Web Hosting & Management</option>
|
||||
<option value="custom">Custom IT Projects</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Message field -->
|
||||
<div>
|
||||
<label for="message" class="block text-sm font-medium text-foreground mb-2">
|
||||
{t('contact.form.message')} *
|
||||
</label>
|
||||
<textarea
|
||||
id="message"
|
||||
name="message"
|
||||
rows="4"
|
||||
required
|
||||
class="w-full px-4 py-3 border border-border rounded-lg bg-background text-foreground placeholder-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all resize-y"
|
||||
placeholder="Tell us about your IT needs..."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Submit button -->
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full btn-primary px-6 py-3 text-lg font-semibold rounded-lg transition-all duration-300 hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
id="submit-btn"
|
||||
>
|
||||
<span id="submit-text">{t('contact.form.send')}</span>
|
||||
<svg id="submit-spinner" class="hidden inline h-5 w-5 ml-2 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- Success/Error messages -->
|
||||
<div id="form-message" class="hidden mt-4 p-4 rounded-lg"></div>
|
||||
</div>
|
||||
|
||||
<!-- Contact info -->
|
||||
<div class="animate-on-scroll" style="animation-delay: 0.2s">
|
||||
<div class="card p-8">
|
||||
<h3 class="text-xl font-display font-semibold text-foreground mb-6">
|
||||
Get in Touch
|
||||
</h3>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- Email -->
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0 w-10 h-10 bg-primary/10 rounded-lg flex items-center justify-center mr-4">
|
||||
<svg class="h-5 w-5 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm text-muted-foreground">Email</div>
|
||||
<a href="mailto:info@tiber365.it" class="text-foreground hover:text-primary transition-colors">
|
||||
{t('contact.info.email')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Phone -->
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0 w-10 h-10 bg-primary/10 rounded-lg flex items-center justify-center mr-4">
|
||||
<svg class="h-5 w-5 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm text-muted-foreground">Phone</div>
|
||||
<a href="tel:+391234567890" class="text-foreground hover:text-primary transition-colors">
|
||||
{t('contact.info.phone')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Location -->
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0 w-10 h-10 bg-primary/10 rounded-lg flex items-center justify-center mr-4">
|
||||
<svg class="h-5 w-5 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm text-muted-foreground">Location</div>
|
||||
<div class="text-foreground">
|
||||
{t('contact.info.address')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
// Contact form handling
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const form = document.getElementById('contact-form') as HTMLFormElement;
|
||||
const submitBtn = document.getElementById('submit-btn') as HTMLButtonElement;
|
||||
const submitText = document.getElementById('submit-text') as HTMLSpanElement;
|
||||
const submitSpinner = document.getElementById('submit-spinner') as HTMLElement;
|
||||
const formMessage = document.getElementById('form-message') as HTMLDivElement;
|
||||
|
||||
if (form) {
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
// Show loading state
|
||||
submitBtn.disabled = true;
|
||||
submitText.textContent = 'Sending...';
|
||||
submitSpinner.classList.remove('hidden');
|
||||
|
||||
// Simulate form submission (replace with actual form handling)
|
||||
try {
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
// Show success message
|
||||
formMessage.className = 'mt-4 p-4 rounded-lg bg-green-50 border border-green-200 text-green-800';
|
||||
formMessage.textContent = 'Message sent successfully! We\'ll get back to you soon.';
|
||||
formMessage.classList.remove('hidden');
|
||||
|
||||
// Reset form
|
||||
form.reset();
|
||||
|
||||
} catch (error) {
|
||||
// Show error message
|
||||
formMessage.className = 'mt-4 p-4 rounded-lg bg-red-50 border border-red-200 text-red-800';
|
||||
formMessage.textContent = 'Failed to send message. Please try again.';
|
||||
formMessage.classList.remove('hidden');
|
||||
} finally {
|
||||
// Reset button state
|
||||
submitBtn.disabled = false;
|
||||
submitText.textContent = 'Send Message';
|
||||
submitSpinner.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
153
src/components/Footer.astro
Normal file
153
src/components/Footer.astro
Normal file
@@ -0,0 +1,153 @@
|
||||
---
|
||||
import { NAVIGATION } from '../site.config';
|
||||
import { t } from '../utils/i18n';
|
||||
---
|
||||
|
||||
<footer class="bg-secondary-900 text-secondary-100 pt-16 pb-8">
|
||||
<div class="container-custom">
|
||||
<!-- Main footer content -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8 mb-8">
|
||||
<!-- Company info -->
|
||||
<div class="lg:col-span-2">
|
||||
<!-- Logo -->
|
||||
<div class="flex items-center space-x-2 mb-4">
|
||||
<div class="h-8 w-8 flex items-center justify-center">
|
||||
<img
|
||||
src="/images/TIBER365.png"
|
||||
alt="Tiber365 Logo"
|
||||
class="h-6 w-6 object-contain"
|
||||
/>
|
||||
</div>
|
||||
<span class="font-display font-bold text-xl text-white">Tiber365</span>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<p class="text-secondary-300 mb-6 max-w-md leading-relaxed">
|
||||
{t('footer.description')}
|
||||
</p>
|
||||
|
||||
<!-- Contact info -->
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center text-secondary-300">
|
||||
<svg class="h-5 w-5 mr-3 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
{t('contact.info.email')}
|
||||
</div>
|
||||
<div class="flex items-center text-secondary-300">
|
||||
<svg class="h-5 w-5 mr-3 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"/>
|
||||
</svg>
|
||||
{t('contact.info.phone')}
|
||||
</div>
|
||||
<div class="flex items-center text-secondary-300">
|
||||
<svg class="h-5 w-5 mr-3 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||
</svg>
|
||||
{t('contact.info.address')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Links -->
|
||||
<div>
|
||||
<h3 class="font-semibold text-white mb-4">Quick Links</h3>
|
||||
<ul class="space-y-2">
|
||||
{NAVIGATION.filter(item => item.type === 'internal').map((item) => (
|
||||
<li>
|
||||
<a
|
||||
href={item.href}
|
||||
class="text-secondary-300 hover:text-primary transition-colors"
|
||||
>
|
||||
{t(item.label)}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
<li>
|
||||
<a
|
||||
href="/contact"
|
||||
class="text-secondary-300 hover:text-primary transition-colors"
|
||||
>
|
||||
{t('footer.links.contact')}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- External Links -->
|
||||
<div>
|
||||
<h3 class="font-semibold text-white mb-4">Resources</h3>
|
||||
<ul class="space-y-2">
|
||||
{NAVIGATION.filter(item => item.type === 'external').map((item) => (
|
||||
<li>
|
||||
<a
|
||||
href={item.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-secondary-300 hover:text-primary transition-colors inline-flex items-center"
|
||||
>
|
||||
{t(item.label)}
|
||||
<svg class="h-3 w-3 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/>
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
<li>
|
||||
<a
|
||||
href="/privacy"
|
||||
class="text-secondary-300 hover:text-primary transition-colors"
|
||||
>
|
||||
{t('footer.links.privacy')}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="/terms"
|
||||
class="text-secondary-300 hover:text-primary transition-colors"
|
||||
>
|
||||
{t('footer.links.terms')}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer bottom -->
|
||||
<div class="border-t border-secondary-800 pt-8">
|
||||
<div class="flex flex-col md:flex-row justify-between items-center">
|
||||
<!-- Copyright -->
|
||||
<p class="text-secondary-400 text-sm">
|
||||
{t('footer.copyright')}
|
||||
</p>
|
||||
|
||||
<!-- Social links placeholder -->
|
||||
<div class="flex items-center space-x-4 mt-4 md:mt-0">
|
||||
<a
|
||||
href="https://blog.tiber365.it"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-secondary-400 hover:text-primary transition-colors"
|
||||
aria-label="Blog"
|
||||
>
|
||||
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-5 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
href="https://support.tiber365.it"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-secondary-400 hover:text-primary transition-colors"
|
||||
aria-label="Support Portal"
|
||||
>
|
||||
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
119
src/components/Header.astro
Normal file
119
src/components/Header.astro
Normal file
@@ -0,0 +1,119 @@
|
||||
---
|
||||
import { NAVIGATION } from '../site.config';
|
||||
import ThemeToggle from './ThemeToggle.astro';
|
||||
import LanguageSwitcher from './LanguageSwitcher.astro';
|
||||
import { t } from '../utils/i18n';
|
||||
---
|
||||
|
||||
<header class="sticky top-0 z-50 w-full border-b border-border/40 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<nav class="container-custom">
|
||||
<div class="flex h-16 items-center justify-between">
|
||||
<!-- Logo -->
|
||||
<div class="flex items-center">
|
||||
<a href="/" class="flex items-center space-x-2">
|
||||
<div class="h-8 w-8 flex items-center justify-center">
|
||||
<img
|
||||
src="/images/TIBER365.png"
|
||||
alt="Tiber365 Logo"
|
||||
class="h-6 w-6 object-contain"
|
||||
/>
|
||||
</div>
|
||||
<span class="font-display font-bold text-xl text-foreground">Tiber365</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Desktop Navigation -->
|
||||
<div class="hidden md:flex items-center space-x-6">
|
||||
{NAVIGATION.map((item) => (
|
||||
<a
|
||||
href={item.href}
|
||||
target={item.type === 'external' ? '_blank' : undefined}
|
||||
rel={item.type === 'external' ? 'noopener noreferrer' : undefined}
|
||||
class="text-sm font-medium text-muted-foreground hover:text-foreground transition-colors relative group"
|
||||
>
|
||||
{t(item.label)}
|
||||
{item.type === 'external' && (
|
||||
<svg class="inline h-3 w-3 ml-1 opacity-70" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/>
|
||||
</svg>
|
||||
)}
|
||||
<span class="absolute inset-x-0 -bottom-1 h-0.5 bg-primary scale-x-0 group-hover:scale-x-100 transition-transform origin-left"></span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<!-- Theme Toggle & Language Switcher -->
|
||||
<div class="flex items-center space-x-4">
|
||||
<LanguageSwitcher />
|
||||
<ThemeToggle />
|
||||
|
||||
<!-- Mobile Menu Button -->
|
||||
<button
|
||||
id="mobile-menu-button"
|
||||
class="md:hidden inline-flex items-center justify-center p-2 rounded-md text-muted-foreground hover:text-foreground hover:bg-accent focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
aria-expanded="false"
|
||||
aria-label="Toggle mobile menu"
|
||||
>
|
||||
<svg id="mobile-menu-icon" class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>
|
||||
</svg>
|
||||
<svg id="mobile-close-icon" class="h-6 w-6 hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Navigation -->
|
||||
<div id="mobile-menu" class="md:hidden hidden border-t border-border">
|
||||
<div class="px-2 pt-2 pb-3 space-y-1">
|
||||
{NAVIGATION.map((item) => (
|
||||
<a
|
||||
href={item.href}
|
||||
target={item.type === 'external' ? '_blank' : undefined}
|
||||
rel={item.type === 'external' ? 'noopener noreferrer' : undefined}
|
||||
class="block px-3 py-2 text-base font-medium text-muted-foreground hover:text-foreground hover:bg-accent rounded-md transition-colors"
|
||||
>
|
||||
{t(item.label)}
|
||||
{item.type === 'external' && (
|
||||
<svg class="inline h-4 w-4 ml-1 opacity-70" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/>
|
||||
</svg>
|
||||
)}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<script>
|
||||
// Mobile menu functionality
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const mobileMenuButton = document.getElementById('mobile-menu-button');
|
||||
const mobileMenu = document.getElementById('mobile-menu');
|
||||
const menuIcon = document.getElementById('mobile-menu-icon');
|
||||
const closeIcon = document.getElementById('mobile-close-icon');
|
||||
|
||||
if (mobileMenuButton && mobileMenu && menuIcon && closeIcon) {
|
||||
mobileMenuButton.addEventListener('click', () => {
|
||||
const isExpanded = mobileMenuButton.getAttribute('aria-expanded') === 'true';
|
||||
|
||||
mobileMenuButton.setAttribute('aria-expanded', (!isExpanded).toString());
|
||||
mobileMenu.classList.toggle('hidden');
|
||||
menuIcon.classList.toggle('hidden');
|
||||
closeIcon.classList.toggle('hidden');
|
||||
});
|
||||
|
||||
// Close mobile menu when clicking outside
|
||||
document.addEventListener('click', (event) => {
|
||||
if (!mobileMenuButton.contains(event.target as Node) && !mobileMenu.contains(event.target as Node)) {
|
||||
mobileMenuButton.setAttribute('aria-expanded', 'false');
|
||||
mobileMenu.classList.add('hidden');
|
||||
menuIcon.classList.remove('hidden');
|
||||
closeIcon.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
102
src/components/Hero.astro
Normal file
102
src/components/Hero.astro
Normal file
@@ -0,0 +1,102 @@
|
||||
---
|
||||
import { t, getCurrentLocale } from '../utils/i18n';
|
||||
|
||||
const currentLocale = getCurrentLocale();
|
||||
console.log('Hero component locale:', currentLocale);
|
||||
---
|
||||
|
||||
<section class="relative min-h-screen flex items-center justify-center overflow-hidden bg-gradient-to-br from-background via-background to-muted">
|
||||
<!-- Background decoration -->
|
||||
<div class="absolute inset-0 overflow-hidden">
|
||||
<div class="absolute -top-40 -right-32 w-80 h-80 rounded-full bg-primary/5 blur-3xl"></div>
|
||||
<div class="absolute -bottom-40 -left-32 w-96 h-96 rounded-full bg-secondary/5 blur-3xl"></div>
|
||||
<div class="absolute top-20 left-1/4 w-32 h-32 rounded-full bg-primary/10 blur-2xl animate-bounce-subtle"></div>
|
||||
</div>
|
||||
|
||||
<!-- Grid overlay -->
|
||||
<div class="absolute inset-0 bg-grid-pattern opacity-5"></div>
|
||||
|
||||
<div class="container-custom relative z-10">
|
||||
<div class="text-center max-w-4xl mx-auto">
|
||||
<!-- Main headline -->
|
||||
<h1 class="text-4xl sm:text-5xl lg:text-6xl xl:text-7xl font-display font-bold text-foreground mb-6 animate-on-scroll">
|
||||
<span class="bg-gradient-to-r from-primary via-primary to-secondary bg-clip-text text-transparent">
|
||||
{t('hero.title', currentLocale)}
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<!-- Subtitle -->
|
||||
<p class="text-lg sm:text-xl lg:text-2xl text-muted-foreground mb-8 max-w-3xl mx-auto leading-relaxed animate-on-scroll" style="animation-delay: 0.2s">
|
||||
{t('hero.subtitle', currentLocale)}
|
||||
</p>
|
||||
|
||||
<!-- CTA Buttons -->
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center items-center mb-12 animate-on-scroll" style="animation-delay: 0.4s">
|
||||
<a
|
||||
href="/contact"
|
||||
class="btn-primary px-8 py-4 text-lg font-semibold rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 hover:scale-105 group"
|
||||
>
|
||||
{t('hero.cta.primary', currentLocale)}
|
||||
<svg class="inline h-5 w-5 ml-2 group-hover:translate-x-1 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
href="/services"
|
||||
class="btn-outline px-8 py-4 text-lg font-semibold rounded-xl transition-all duration-300 hover:scale-105"
|
||||
>
|
||||
{t('hero.cta.secondary', currentLocale)}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Trust indicators -->
|
||||
<div class="flex flex-col items-center animate-on-scroll" style="animation-delay: 0.6s">
|
||||
<p class="text-sm text-muted-foreground mb-4">{t('hero.trusted', currentLocale)}</p>
|
||||
|
||||
<!-- Service icons -->
|
||||
<div class="flex items-center justify-center space-x-8 opacity-60 mb-12">
|
||||
<!-- Microsoft 365 -->
|
||||
<div class="text-2xl sm:text-3xl" title="Microsoft 365">
|
||||
🏢
|
||||
</div>
|
||||
<!-- Networking -->
|
||||
<div class="text-2xl sm:text-3xl" title="Networking">
|
||||
🌐
|
||||
</div>
|
||||
<!-- Web Hosting -->
|
||||
<div class="text-2xl sm:text-3xl" title="Web Hosting">
|
||||
🚀
|
||||
</div>
|
||||
<!-- Automation -->
|
||||
<div class="text-2xl sm:text-3xl" title="Automation">
|
||||
⚙️
|
||||
</div>
|
||||
<!-- Custom Solutions -->
|
||||
<div class="text-2xl sm:text-3xl" title="Custom Solutions">
|
||||
🛠️
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scroll indicator -->
|
||||
<div class="animate-bounce text-muted-foreground hover:text-foreground transition-colors">
|
||||
<button
|
||||
onclick="document.getElementById('services').scrollIntoView({behavior: 'smooth'})"
|
||||
class="p-2"
|
||||
aria-label="Scroll to services"
|
||||
>
|
||||
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.bg-grid-pattern {
|
||||
background-image: radial-gradient(circle, rgba(var(--color-foreground) / 0.1) 1px, transparent 1px);
|
||||
background-size: 30px 30px;
|
||||
}
|
||||
</style>
|
141
src/components/LanguageSwitcher.astro
Normal file
141
src/components/LanguageSwitcher.astro
Normal file
@@ -0,0 +1,141 @@
|
||||
---
|
||||
import { LANGUAGES } from '../site.config';
|
||||
import { t, getCurrentLocale } from '../utils/i18n';
|
||||
|
||||
// Get the current locale on the server side
|
||||
const currentLocale = getCurrentLocale();
|
||||
console.log('Server-side current locale:', currentLocale);
|
||||
---
|
||||
|
||||
<div class="relative inline-block text-left">
|
||||
<button
|
||||
id="language-toggle"
|
||||
class="inline-flex items-center justify-center p-2 rounded-md text-muted-foreground hover:text-foreground hover:bg-accent focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
aria-label={t('nav.language')}
|
||||
title={t('nav.language')}
|
||||
>
|
||||
<!-- Globe icon -->
|
||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"/>
|
||||
</svg>
|
||||
<!-- Current language indicator -->
|
||||
<span id="current-lang" class="ml-1 text-sm font-medium" data-current-lang={currentLocale}>
|
||||
{currentLocale.toUpperCase()}
|
||||
</span>
|
||||
<!-- Chevron down -->
|
||||
<svg id="language-chevron" class="h-4 w-4 ml-1 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Dropdown menu -->
|
||||
<div
|
||||
id="language-dropdown"
|
||||
class="hidden absolute right-0 mt-2 w-40 origin-top-right rounded-md bg-background border border-border shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none z-50"
|
||||
role="menu"
|
||||
aria-orientation="vertical"
|
||||
aria-labelledby="language-toggle"
|
||||
>
|
||||
<div class="py-1" role="none">
|
||||
{Object.entries(LANGUAGES).map(([code, name]) => (
|
||||
<button
|
||||
type="button"
|
||||
class:list={[
|
||||
"language-option w-full text-left block px-4 py-2 text-sm text-foreground hover:bg-accent hover:text-accent-foreground transition-colors",
|
||||
{ "bg-accent text-accent-foreground": code === currentLocale }
|
||||
]}
|
||||
role="menuitem"
|
||||
data-lang={code}
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<span class="text-base mr-2">
|
||||
{code === 'en' ? '🇬🇧' : code === 'nl' ? '🇳🇱' : '🇮🇹'}
|
||||
</span>
|
||||
{name}
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
import { setCurrentLocale, getCurrentLocale } from '../utils/i18n';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const languageToggle = document.getElementById('language-toggle');
|
||||
const languageDropdown = document.getElementById('language-dropdown');
|
||||
const languageChevron = document.getElementById('language-chevron');
|
||||
const currentLangSpan = document.getElementById('current-lang');
|
||||
const languageOptions = document.querySelectorAll('.language-option');
|
||||
|
||||
if (!languageToggle || !languageDropdown || !languageChevron || !currentLangSpan) return;
|
||||
|
||||
// Update current language display
|
||||
const currentLocale = getCurrentLocale();
|
||||
console.log('Client-side current locale:', currentLocale);
|
||||
currentLangSpan.textContent = currentLocale.toUpperCase();
|
||||
currentLangSpan.setAttribute('data-current-lang', currentLocale);
|
||||
|
||||
// Update active state in dropdown
|
||||
languageOptions.forEach((option) => {
|
||||
const optionLang = option.getAttribute('data-lang');
|
||||
if (optionLang === currentLocale) {
|
||||
option.classList.add('bg-accent', 'text-accent-foreground');
|
||||
} else {
|
||||
option.classList.remove('bg-accent', 'text-accent-foreground');
|
||||
}
|
||||
});
|
||||
|
||||
// Toggle dropdown
|
||||
languageToggle.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
const isExpanded = languageToggle.getAttribute('aria-expanded') === 'true';
|
||||
|
||||
languageToggle.setAttribute('aria-expanded', (!isExpanded).toString());
|
||||
languageDropdown.classList.toggle('hidden');
|
||||
languageChevron.classList.toggle('rotate-180');
|
||||
});
|
||||
|
||||
// Handle language selection
|
||||
languageOptions.forEach((option) => {
|
||||
option.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const selectedLang = option.getAttribute('data-lang');
|
||||
const currentLang = currentLangSpan.getAttribute('data-current-lang');
|
||||
|
||||
if (selectedLang && selectedLang !== currentLang) {
|
||||
console.log('Switching language from', currentLang, 'to', selectedLang);
|
||||
setCurrentLocale(selectedLang as 'en' | 'nl' | 'it');
|
||||
}
|
||||
|
||||
// Close dropdown
|
||||
languageToggle.setAttribute('aria-expanded', 'false');
|
||||
languageDropdown.classList.add('hidden');
|
||||
languageChevron.classList.remove('rotate-180');
|
||||
});
|
||||
});
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
document.addEventListener('click', (event) => {
|
||||
if (!languageToggle.contains(event.target as Node) && !languageDropdown.contains(event.target as Node)) {
|
||||
languageToggle.setAttribute('aria-expanded', 'false');
|
||||
languageDropdown.classList.add('hidden');
|
||||
languageChevron.classList.remove('rotate-180');
|
||||
}
|
||||
});
|
||||
|
||||
// Close dropdown when pressing Escape
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Escape') {
|
||||
languageToggle.setAttribute('aria-expanded', 'false');
|
||||
languageDropdown.classList.add('hidden');
|
||||
languageChevron.classList.remove('rotate-180');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
81
src/components/Services.astro
Normal file
81
src/components/Services.astro
Normal file
@@ -0,0 +1,81 @@
|
||||
---
|
||||
import { SERVICES } from '../site.config';
|
||||
import { t } from '../utils/i18n';
|
||||
---
|
||||
|
||||
<section id="services" class="py-20 bg-muted/30">
|
||||
<div class="container-custom">
|
||||
<!-- Section header -->
|
||||
<div class="text-center mb-16 animate-on-scroll">
|
||||
<h2 class="text-3xl sm:text-4xl lg:text-5xl font-display font-bold text-foreground mb-4">
|
||||
{t('services.title')}
|
||||
</h2>
|
||||
<p class="text-lg sm:text-xl text-muted-foreground max-w-3xl mx-auto">
|
||||
{t('services.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Services grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 mb-12">
|
||||
{SERVICES.map((service, index) => (
|
||||
<div
|
||||
class="card p-6 hover:shadow-xl transition-all duration-300 hover:scale-105 animate-on-scroll group"
|
||||
style={`animation-delay: ${index * 0.1}s`}
|
||||
>
|
||||
<!-- Service icon -->
|
||||
<div class="text-4xl mb-4 group-hover:scale-110 transition-transform duration-300">
|
||||
{service.icon}
|
||||
</div>
|
||||
|
||||
<!-- Service title -->
|
||||
<h3 class="text-xl font-display font-semibold text-foreground mb-3">
|
||||
{t(service.titleKey)}
|
||||
</h3>
|
||||
|
||||
<!-- Service description -->
|
||||
<p class="text-muted-foreground mb-4 leading-relaxed">
|
||||
{t(service.descriptionKey)}
|
||||
</p>
|
||||
|
||||
<!-- Service features -->
|
||||
<ul class="space-y-2">
|
||||
{service.features.map((feature) => (
|
||||
<li class="flex items-start text-sm text-muted-foreground">
|
||||
<svg class="h-4 w-4 mt-0.5 mr-2 text-primary flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
{t(feature)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<!-- Learn more link -->
|
||||
<div class="mt-6">
|
||||
<a
|
||||
href="/services"
|
||||
class="inline-flex items-center text-primary hover:text-primary/80 font-medium text-sm group-hover:translate-x-1 transition-all duration-200"
|
||||
>
|
||||
Learn more
|
||||
<svg class="h-4 w-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<!-- CTA section -->
|
||||
<div class="text-center animate-on-scroll">
|
||||
<a
|
||||
href="/services"
|
||||
class="btn-primary px-8 py-4 text-lg font-semibold rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 hover:scale-105 inline-flex items-center group"
|
||||
>
|
||||
{t('services.viewAll')}
|
||||
<svg class="h-5 w-5 ml-2 group-hover:translate-x-1 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
79
src/components/Testimonials.astro
Normal file
79
src/components/Testimonials.astro
Normal file
@@ -0,0 +1,79 @@
|
||||
---
|
||||
import { TESTIMONIALS } from '../site.config';
|
||||
import { t } from '../utils/i18n';
|
||||
---
|
||||
|
||||
<section class="py-20 bg-background">
|
||||
<div class="container-custom">
|
||||
<!-- Section header -->
|
||||
<div class="text-center mb-16 animate-on-scroll">
|
||||
<h2 class="text-3xl sm:text-4xl lg:text-5xl font-display font-bold text-foreground mb-4">
|
||||
{t('testimonials.title')}
|
||||
</h2>
|
||||
<p class="text-lg sm:text-xl text-muted-foreground max-w-3xl mx-auto">
|
||||
{t('testimonials.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Testimonials grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{TESTIMONIALS.map((testimonial, index) => (
|
||||
<div
|
||||
class="card p-6 hover:shadow-xl transition-all duration-300 hover:scale-105 animate-on-scroll"
|
||||
style={`animation-delay: ${index * 0.1}s`}
|
||||
>
|
||||
<!-- Star rating -->
|
||||
<div class="flex items-center mb-4">
|
||||
{Array.from({ length: testimonial.rating }, (_, i) => (
|
||||
<svg class="h-5 w-5 text-yellow-400 fill-current" viewBox="0 0 20 20">
|
||||
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/>
|
||||
</svg>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<!-- Testimonial content -->
|
||||
<blockquote class="text-muted-foreground mb-6 leading-relaxed italic">
|
||||
"{t(testimonial.contentKey)}"
|
||||
</blockquote>
|
||||
|
||||
<!-- Customer info -->
|
||||
<div class="flex items-center">
|
||||
<!-- Avatar placeholder -->
|
||||
<div class="w-12 h-12 rounded-full bg-primary/10 flex items-center justify-center mr-4">
|
||||
<svg class="w-6 h-6 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="font-semibold text-foreground">
|
||||
{t(testimonial.nameKey)}
|
||||
</div>
|
||||
<div class="text-sm text-muted-foreground">
|
||||
{t(testimonial.companyKey)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<!-- Additional social proof -->
|
||||
<div class="mt-16 text-center animate-on-scroll">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-8 max-w-3xl mx-auto">
|
||||
<div class="text-center">
|
||||
<div class="text-3xl font-bold text-primary mb-2">5+</div>
|
||||
<div class="text-sm text-muted-foreground">{t('about.experience')}</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-3xl font-bold text-primary mb-2">100+</div>
|
||||
<div class="text-sm text-muted-foreground">{t('about.clients')}</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-3xl font-bold text-primary mb-2">200+</div>
|
||||
<div class="text-sm text-muted-foreground">{t('about.projects')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
79
src/components/ThemeToggle.astro
Normal file
79
src/components/ThemeToggle.astro
Normal file
@@ -0,0 +1,79 @@
|
||||
---
|
||||
import { t } from '../utils/i18n';
|
||||
---
|
||||
|
||||
<button
|
||||
id="theme-toggle"
|
||||
class="inline-flex items-center justify-center p-2 rounded-md text-muted-foreground hover:text-foreground hover:bg-accent focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
aria-label={t('nav.theme.toggle')}
|
||||
title={t('nav.theme.toggle')}
|
||||
>
|
||||
<!-- Sun icon (light mode) -->
|
||||
<svg
|
||||
id="theme-toggle-light-icon"
|
||||
class="h-5 w-5"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<!-- Moon icon (dark mode) -->
|
||||
<svg
|
||||
id="theme-toggle-dark-icon"
|
||||
class="h-5 w-5 hidden"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<script>
|
||||
import { toggleTheme, getThemePreference } from '../utils/theme';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const themeToggle = document.getElementById('theme-toggle');
|
||||
const lightIcon = document.getElementById('theme-toggle-light-icon');
|
||||
const darkIcon = document.getElementById('theme-toggle-dark-icon');
|
||||
|
||||
if (!themeToggle || !lightIcon || !darkIcon) return;
|
||||
|
||||
// Update icons based on current theme
|
||||
function updateIcons() {
|
||||
const currentTheme = getThemePreference();
|
||||
if (currentTheme === 'dark') {
|
||||
lightIcon.classList.add('hidden');
|
||||
darkIcon.classList.remove('hidden');
|
||||
} else {
|
||||
lightIcon.classList.remove('hidden');
|
||||
darkIcon.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize icons
|
||||
updateIcons();
|
||||
|
||||
// Handle toggle click
|
||||
themeToggle.addEventListener('click', () => {
|
||||
toggleTheme();
|
||||
updateIcons();
|
||||
});
|
||||
|
||||
// Listen for theme changes (from other sources)
|
||||
const observer = new MutationObserver(() => {
|
||||
updateIcons();
|
||||
});
|
||||
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['data-theme']
|
||||
});
|
||||
});
|
||||
</script>
|
Reference in New Issue
Block a user