major update in looks and feel

This commit is contained in:
becarta
2025-03-06 00:03:16 +01:00
parent 3b98eae137
commit d66123c029
40 changed files with 1123 additions and 422 deletions

View File

@@ -20,11 +20,11 @@ const languages = [
const currentLanguage = languages.find(lang => lang.code === currentLang) || languages[0];
---
<div class="relative inline-block text-left">
<div class="relative inline-block text-left language-dropdown">
<div>
<button
type="button"
class="inline-flex justify-center w-full rounded-md border border-gray-300 dark:border-gray-600 shadow-sm px-4 py-2 bg-white dark:bg-gray-800 text-sm font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 dark:focus:ring-offset-gray-800 focus:ring-indigo-500 dark:focus:ring-indigo-400 focus-visible:ring-4 transition-colors duration-200"
class="inline-flex justify-center w-full rounded-md border border-gray-300 dark:border-gray-600 shadow-sm px-4 py-2 bg-white dark:bg-gray-800 text-sm font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 dark:focus:ring-offset-gray-800 focus:ring-indigo-500 dark:focus:ring-indigo-400 focus-visible:ring-4 transition-colors duration-200 dropdown-button"
id="menu-button"
aria-expanded="false"
aria-haspopup="true"
@@ -42,7 +42,7 @@ const currentLanguage = languages.find(lang => lang.code === currentLang) || lan
</div>
<div
class="hidden absolute left-0 rounded-md shadow-lg bg-white dark:bg-gray-800 ring-1 ring-black ring-opacity-5 dark:ring-gray-600 focus:outline-none transform opacity-0 scale-95 transition-all duration-200 max-h-[300px] overflow-y-auto w-full"
class="absolute left-0 rounded-md shadow-lg bg-white dark:bg-gray-800 ring-1 ring-black ring-opacity-5 dark:ring-gray-600 focus:outline-none transform opacity-0 scale-95 transition-all duration-200 max-h-[300px] overflow-y-auto w-full language-menu"
role="menu"
aria-orientation="vertical"
aria-labelledby="menu-button"
@@ -66,26 +66,52 @@ const currentLanguage = languages.find(lang => lang.code === currentLang) || lan
))}
</div>
</div>
<select id="language-select" class="block w-full text-left px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-white focus:bg-gray-100 dark:focus:bg-gray-700 focus:text-gray-900 dark:focus:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-indigo-400 transition-colors duration-200 rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-200 language-select">
{languages.map(lang => (
<option value={lang.code} selected={lang.code === currentLang}>
{lang.name}
</option>
))}
</select>
</div>
<style>
#language-menu.open-downward {
.language-dropdown {
@apply md:inline-block md:relative;
}
.language-select {
display: none;
}
@media (max-width: 767px) {
.language-dropdown {
display: none;
}
.language-select {
display: block;
}
}
.language-menu.open-downward {
top: 100%;
margin-top: 0.5rem;
transform-origin: top;
}
#language-menu.open-upward {
.language-menu.open-upward {
bottom: 100%;
margin-bottom: 0.5rem;
transform-origin: bottom;
}
#language-menu:not(.hidden).open-downward {
.language-menu:not(.hidden).open-downward {
animation: slideDown 0.2s ease-out forwards;
}
#language-menu:not(.hidden).open-upward {
.language-menu:not(.hidden).open-upward {
animation: slideUp 0.2s ease-out forwards;
}
@@ -110,14 +136,6 @@ const currentLanguage = languages.find(lang => lang.code === currentLang) || lan
transform: scale(1) translateY(0);
}
}
@media (max-width: 767px) {
.absolute.right-0.w-56.rounded-md.shadow-lg.bg-white.dark\:bg-gray-800.ring-1.ring-black.ring-opacity-5.dark\:ring-gray-600.focus\:outline-none.transform.opacity-0.scale-95.transition-all.duration-200.max-h-\[300px\].overflow-y-auto {
width: auto;
min-width: 120px;
max-width: calc(100vw - 6rem);
}
}
</style>
<script define:vars={{ supportedLanguages }}>
@@ -127,8 +145,9 @@ const currentLanguage = languages.find(lang => lang.code === currentLang) || lan
const chevronIcon = document.querySelector('#chevron-icon');
const selectedLanguageText = document.querySelector('#selected-language');
const languageButtons = document.querySelectorAll('[data-lang-code]');
const languageSelect = document.querySelector('#language-select');
if (!button || !menu || !chevronIcon || !selectedLanguageText) {
if (!button || !menu || !chevronIcon || !selectedLanguageText || !languageSelect) {
return;
}
@@ -280,41 +299,83 @@ const currentLanguage = languages.find(lang => lang.code === currentLang) || lan
// Construct the full URL
const newFullUrl = `${window.location.origin}${newUrl}`;
// Reload the page to ensure all content is updated to the new language
window.location.href = newFullUrl;
// Force a complete page reload to ensure all content is updated to the new language
// This bypasses any client-side caching and ensures a fresh server render
window.location.href = newFullUrl + '?t=' + Date.now();
});
});
// Close when clicking outside
document.addEventListener('click', (e) => {
const target = e.target;
if (isOpen && !menu.contains(target) && !button.contains(target)) {
closeMenu();
}
});
// Handle language selection from select element
languageSelect.addEventListener('change', (event) => {
const langCode = event.target.value;
if (!langCode) return;
// Handle keyboard navigation
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && isOpen) {
closeMenu();
button.focus();
// Get current URL information
const currentUrl = new URL(window.location.href);
const currentPath = currentUrl.pathname.replace(/\/$/, '');
const currentHash = currentUrl.hash;
const pathSegments = currentPath.split('/').filter(Boolean);
// Check if we're on a language-specific path
const isLangPath = supportedLanguages.includes(pathSegments[0]);
// Get the previous language code
const previousLangCode = isLangPath ? pathSegments[0] : 'en';
// Extract the page path without language
let pagePath = '';
if (isLangPath && pathSegments.length > 1) {
// If we're on a language-specific path, get everything after the language code
pagePath = `/${pathSegments.slice(1).join('/')}`;
} else if (!isLangPath && pathSegments.length > 0) {
// If we're not on a language-specific path, use the current path
pagePath = `/${pathSegments.join('/')}`;
}
// Enhanced keyboard navigation with arrow keys
if (isOpen && (e.key === 'ArrowDown' || e.key === 'ArrowUp')) {
e.preventDefault();
const menuItems = Array.from(menu.querySelectorAll('button[role="menuitem"]'));
const currentIndex = menuItems.findIndex(item => item === document.activeElement);
let newIndex;
if (e.key === 'ArrowDown') {
newIndex = currentIndex < menuItems.length - 1 ? currentIndex + 1 : 0;
} else {
newIndex = currentIndex > 0 ? currentIndex - 1 : menuItems.length - 1;
}
menuItems[newIndex].focus();
// Handle special case for root path
const isRootPath = pathSegments.length === 0 || (isLangPath && pathSegments.length === 1);
// Construct the new URL
let newUrl = isRootPath ? `/${langCode}` : `/${langCode}${pagePath}`;
// Clean up any potential double slashes
newUrl = newUrl.replace(/\/+/g, '/');
// Append hash fragment if it exists
if (currentHash) {
newUrl += currentHash;
}
// Store the language preference in localStorage and cookies
if (window.languageUtils) {
window.languageUtils.storeLanguagePreference(langCode);
} else {
// Fallback if languageUtils is not available
localStorage.setItem('preferredLanguage', langCode);
// Also set a cookie for server-side detection
const expirationDate = new Date();
expirationDate.setFullYear(expirationDate.getFullYear() + 1);
document.cookie = `preferredLanguage=${langCode}; expires=${expirationDate.toUTCString()}; path=/; SameSite=Lax`;
}
// Dispatch the language changed event
const reloadEvent = new CustomEvent('languageChanged', {
detail: {
langCode,
previousLangCode,
path: newUrl,
willReload: true
}
});
document.dispatchEvent(reloadEvent);
// Construct the full URL
const newFullUrl = `${window.location.origin}${newUrl}`;
// Force a complete page reload to ensure all content is updated to the new language
// This bypasses any client-side caching and ensures a fresh server render
window.location.href = newFullUrl + '?t=' + Date.now();
});
}

View File

@@ -1,7 +1,5 @@
---
import { UI } from 'astrowind:config';
// TODO: This code is temporary
---
<script is:inline define:vars={{ defaultTheme: UI.theme || 'system' }}>

View File

@@ -42,7 +42,7 @@ import { UI } from 'astrowind:config';
const onLoad = function () {
let lastKnownScrollPosition = window.scrollY;
let ticking = true;
let ticking = false;
attachEvent('#header nav', 'click', function () {
document.querySelector('[data-aw-toggle-menu]')?.classList.remove('expanded');
@@ -239,7 +239,6 @@ import { UI } from 'astrowind:config';
// Store language preference in localStorage
function storeLanguagePreference(langCode) {
localStorage.setItem('preferredLanguage', langCode);
console.log('Language preference stored:', langCode);
}
// Function to update URLs with the current language
@@ -301,11 +300,16 @@ import { UI } from 'astrowind:config';
// Store the selected language in localStorage
if (event.detail && event.detail.langCode) {
storeLanguagePreference(event.detail.langCode);
console.log('Language changed:', event.detail);
// Always update all internal links with the new language
// regardless of whether we're doing a full page reload
updateUrlsWithLanguage();
// If this event includes a path but doesn't indicate it will reload,
// force a full page reload to ensure all content is properly translated
if (event.detail.path && !event.detail.willReload) {
window.location.replace(event.detail.path);
}
}
});
@@ -313,10 +317,7 @@ import { UI } from 'astrowind:config';
updateUrlsWithLanguage();
// Process links after client-side navigation
document.addEventListener('astro:page-load', () => {
// Short delay to ensure DOM is fully updated
setTimeout(updateUrlsWithLanguage, 0);
});
document.addEventListener('astro:page-load', updateUrlsWithLanguage);
// Also update links when the DOM content is loaded
document.addEventListener('DOMContentLoaded', updateUrlsWithLanguage);
@@ -472,19 +473,15 @@ import { UI } from 'astrowind:config';
// Form validation and submission
contactForm.addEventListener('submit', async function(e) {
e.preventDefault();
console.log('Form submitted');
// Reset previous error messages
resetFormErrors();
// Client-side validation
if (!validateForm(contactForm)) {
console.log('Form validation failed');
return;
}
console.log('Form validation passed');
// Show loading state
const submitButton = contactForm.querySelector('button[type="submit"]');
const originalButtonText = submitButton.innerHTML;
@@ -494,37 +491,24 @@ import { UI } from 'astrowind:config';
try {
const formData = new FormData(contactForm);
// Log form data
console.log('Form data:');
for (const [key, value] of formData.entries()) {
console.log(`${key}: ${value}`);
}
// Add timestamp to help prevent duplicate submissions
formData.append('timestamp', Date.now().toString());
// Submit the form to Netlify
console.log('Submitting form to Netlify');
const response = await fetch('/', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams(formData).toString()
});
console.log('Response status:', response.status);
if (response.ok) {
console.log('Form submission successful');
// Show success message
document.getElementById('form-success').classList.remove('hidden');
contactForm.reset();
} else {
console.log('Form submission failed');
document.getElementById('form-error').classList.remove('hidden');
}
} catch (error) {
console.error('Error submitting form:', error);
console.error('Error details:', error.message);
} catch {
document.getElementById('form-error').classList.remove('hidden');
} finally {
// Restore button state

View File

@@ -0,0 +1,85 @@
---
interface Props {
items: {
title: string;
description: string;
}[];
}
const { items } = Astro.props;
---
<div class="accordion">
{items.map((item, index) => (
<div class="accordion-item">
<button class="accordion-header" data-accordion-target={`#accordion-body-${index}`}>
{item.title}
<svg class="w-4 h-4 ml-2 shrink-0" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
</button>
<div id={`accordion-body-${index}`} class="accordion-body" aria-labelledby={`accordion-header-${index}`}>
<div class="p-5 border border-b-0 dark:border-gray-700 dark:bg-gray-900">
<p class="text-gray-500 dark:text-gray-400">{item.description}</p>
</div>
</div>
</div>
))}
</div>
<script>
const accordionHeaders = document.querySelectorAll('.accordion-header');
accordionHeaders.forEach(header => {
header.addEventListener('click', () => {
const target = header.getAttribute('data-accordion-target');
const body = document.querySelector(target);
if (body) {
const expanded = (body as HTMLElement).classList.contains('expanded');
// Close all accordion items
document.querySelectorAll('.accordion-body.expanded').forEach(item => {
if (item && 'style' in item) {
(item as any).style.display = 'none';
item.classList.remove('expanded');
}
});
// Open the clicked accordion item if it was closed
if (!expanded) {
body.classList.add('expanded');
if (body && 'style' in body) {
(body as HTMLElement).style.display = 'block';
}
}
}
});
});
</script>
<style>
.accordion-header {
display: flex;
align-items: center;
width: 100%;
padding: 1rem;
font-size: 1.125rem;
line-height: 1.75rem;
font-weight: 500;
text-align: left;
border-bottom: 1px solid #e5e7eb;
cursor: pointer;
transition: background-color 0.15s ease;
}
.accordion-header:hover {
background-color: #f9fafb;
}
.accordion-body {
display: none;
}
.accordion-body.expanded {
display: block;
}
</style>

View File

@@ -1,11 +1,302 @@
---
import { Icon } from 'astro-icon/components';
export interface Props {
isDark?: boolean;
showIcons?: boolean;
disableParallax?: boolean;
}
const { isDark = false } = Astro.props;
---
const { isDark = false, showIcons = true, disableParallax = false } = Astro.props;
<div class:list={['absolute inset-0', { 'bg-dark dark:bg-transparent': isDark }]}>
// Define color palettes for light and dark modes with higher contrast
const lightModeColors = [
'text-blue-600/40',
'text-indigo-600/40',
'text-purple-600/40',
'text-cyan-600/40',
'text-teal-600/40',
'text-emerald-600/40',
'text-sky-600/40',
'text-violet-600/40',
];
const darkModeColors = [
'dark:text-blue-400/45',
'dark:text-indigo-400/45',
'dark:text-purple-400/45',
'dark:text-cyan-400/45',
'dark:text-teal-400/45',
'dark:text-emerald-400/45',
'dark:text-sky-400/45',
'dark:text-violet-400/45',
];
// Define interfaces for our icon objects
interface BaseIconObject {
icon: string;
x: string;
y: string;
size: string;
opacity: string;
rotate: string;
}
interface IconWithColors extends BaseIconObject {
lightColor: string;
darkColor: string;
}
// Function to get a random color from the palette
const getRandomColor = (palette: string[]): string => {
const randomIndex = Math.floor(Math.random() * palette.length);
return palette[randomIndex];
};
// List of icons to be used in the background
// Expanded to include a wider variety of symbols for more visual diversity
const iconNames: string[] = [
// Core IT/Tech icons
'tabler:settings-automation',
'tabler:brand-office',
'tabler:api',
'tabler:server',
'tabler:message-chatbot',
'tabler:share',
'tabler:code',
'tabler:cloud',
'tabler:device-laptop',
'tabler:chart-line',
'tabler:database',
'tabler:brand-github',
'tabler:device-desktop',
'tabler:brand-azure',
// Additional tech-related icons
'tabler:cpu',
'tabler:device-mobile',
'tabler:wifi',
'tabler:network',
'tabler:shield',
'tabler:lock',
'tabler:key',
'tabler:rocket',
// Tangentially related icons for visual diversity
'tabler:bulb',
'tabler:compass',
'tabler:binary',
'tabler:infinity',
'tabler:brain',
];
// Function to get a random value within a range
const getRandomInRange = (min: number, max: number): number => {
return Math.random() * (max - min) + min;
};
// Function to get a random rotation with increased range
const getRandomRotation = (): string => {
return `${getRandomInRange(-30, 30)}deg`;
};
// Function to get a random size (restored to original dimensions)
const getRandomSize = (): string => {
return `${getRandomInRange(140, 180)}px`;
};
// Function to get a random opacity
const getRandomOpacity = (): string => {
return getRandomInRange(0.32, 0.38).toFixed(2);
};
// Create a spacious layout with well-separated icons
const createSpacedIcons = (): BaseIconObject[] => {
const icons: BaseIconObject[] = [];
const rows = 6; // Reduced from 8 to 6 for fewer potential positions
const cols = 6; // Reduced from 8 to 6 for fewer potential positions
// Define larger margins to keep icons away from edges (in percentage)
const marginX = 10; // 10% margin from left and right edges (increased from 5%)
const marginY = 10; // 10% margin from top and bottom edges (increased from 5%)
// Minimum distance between icons (in percentage points)
const minDistance = 20; // Ensure at least 20% distance between any two icons
// Create a base grid of positions
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
// Randomly skip more positions (75% chance) for fewer icons overall
if (Math.random() < 0.75) {
continue;
}
// Base position in the grid with margins applied
const baseX = marginX + ((col / cols) * (100 - 2 * marginX));
const baseY = marginY + ((row / rows) * (100 - 2 * marginY));
// Add limited randomness to the position (±5%) to maintain spacing
const randomOffsetX = getRandomInRange(-5, 5);
const randomOffsetY = getRandomInRange(-5, 5);
// Ensure positions stay within margins
const x = Math.max(marginX, Math.min(100 - marginX, baseX + randomOffsetX));
const y = Math.max(marginY, Math.min(100 - marginY, baseY + randomOffsetY));
// Check if this position is too close to any existing icon
let tooClose = false;
for (const existingIcon of icons) {
// Extract numeric values from percentage strings
const existingX = parseFloat(existingIcon.x);
const existingY = parseFloat(existingIcon.y);
// Calculate distance between points
const distance = Math.sqrt(
Math.pow(x - existingX, 2) +
Math.pow(y - existingY, 2)
);
// If too close to an existing icon, skip this position
if (distance < minDistance) {
tooClose = true;
break;
}
}
if (tooClose) {
continue;
}
// Randomly select an icon from the expanded set
const iconIndex = Math.floor(Math.random() * iconNames.length);
icons.push({
icon: iconNames[iconIndex],
x: `${x}%`,
y: `${y}%`,
size: getRandomSize(),
opacity: getRandomOpacity(),
rotate: getRandomRotation(),
});
}
}
return icons;
};
const icons: BaseIconObject[] = createSpacedIcons();
// Assign random colors to each icon
const iconsWithColors: IconWithColors[] = icons.map(icon => ({
icon: icon.icon,
x: icon.x,
y: icon.y,
size: icon.size,
opacity: icon.opacity,
rotate: icon.rotate,
lightColor: getRandomColor(lightModeColors),
darkColor: getRandomColor(darkModeColors),
}));
---
<div class:list={['absolute inset-0', { 'backdrop-blur-sm bg-white/5 dark:bg-gray-900/10': isDark }]}>
<slot />
{showIcons && (
/* Decorative background icons with parallax effect */
<div id="parallax-background" class="absolute inset-0 overflow-hidden pointer-events-none z-[-5]">
{iconsWithColors.map(({ icon, x, y, size, opacity, rotate, lightColor, darkColor }, index) => (
<div
class={`absolute ${lightColor} ${darkColor} parallax-icon`}
style={`left: ${x}; top: ${y}; opacity: ${opacity}; transform: rotate(${rotate}); will-change: transform; transition: transform 0.1s ease-out;`}
data-depth={`${0.5 + (index % 3) * 0.2}`}
data-initial-x={x}
data-initial-y={y}
>
<Icon name={icon} style={`width: ${size}; height: ${size};`} />
</div>
))}
</div>
)}
</div>
{showIcons && (
<script define:vars={{ disableParallax }}>
// Parallax scrolling effect for background icons
document.addEventListener('DOMContentLoaded', () => {
// Get all parallax icons
const parallaxIcons = document.querySelectorAll('.parallax-icon');
// Skip parallax on mobile devices for better performance or if parallax is disabled
const isMobile = window.matchMedia('(max-width: 768px)').matches;
if (isMobile || disableParallax) return;
// Variables to track scroll position
let lastScrollY = window.scrollY;
let ticking = false;
// Function to update icon positions based on scroll
const updateParallax = () => {
parallaxIcons.forEach((icon) => {
const depth = parseFloat(icon.getAttribute('data-depth') || '0.5');
// Calculate parallax offset based on scroll position and depth
// Lower depth value means the icon moves slower (appears further away)
const yOffset = (lastScrollY * depth * 0.15);
// Get the original rotation
const transformValue = icon.style.transform;
const rotateMatch = transformValue.match(/rotate\([^)]+\)/);
const rotateValue = rotateMatch ? rotateMatch[0] : 'rotate(0deg)';
// Apply transform with the original rotation plus the parallax offset
icon.style.transform = `${rotateValue} translate3d(0, ${yOffset}px, 0)`;
});
ticking = false;
};
// Throttle scroll events for better performance
const onScroll = () => {
lastScrollY = window.scrollY;
if (!ticking) {
window.requestAnimationFrame(() => {
updateParallax();
ticking = false;
});
ticking = true;
}
};
// Add scroll event listener
window.addEventListener('scroll', onScroll, { passive: true });
// Update on resize (debounced)
let resizeTimer;
window.addEventListener('resize', () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
// Check if device is now mobile and disable parallax if needed
const isMobileNow = window.matchMedia('(max-width: 768px)').matches;
if (isMobileNow) {
// Reset positions on mobile
parallaxIcons.forEach((icon) => {
const transformValue = icon.style.transform;
const rotateMatch = transformValue.match(/rotate\([^)]+\)/);
const rotateValue = rotateMatch ? rotateMatch[0] : 'rotate(0deg)';
icon.style.transform = rotateValue;
});
} else {
// Update parallax on desktop
updateParallax();
}
}, 200);
}, { passive: true });
// Initial update
updateParallax();
});
</script>
)}

View File

@@ -16,7 +16,7 @@ const {
panel: panelClass = '',
title: titleClass = '',
description: descriptionClass = '',
icon: defaultIconClass = 'text-primary dark:text-slate-200 border-primary dark:border-blue-700',
icon: defaultIconClass = 'text-primary dark:text-blue-300 border-primary dark:border-blue-500 dark:shadow-blue-500/40 dark:shadow-sm',
} = classes;
---
@@ -26,7 +26,7 @@ const {
{items.map(({ title, description, icon, classes: itemClasses = {} }) => (
<div
class={twMerge(
'flex intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade border border-gray-200 dark:border-gray-700 rounded-lg p-4 hover:shadow-md transition-all duration-300 ease-in-out hover:bg-gray-50 dark:hover:bg-gray-800',
'flex intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade border border-gray-200 dark:border-gray-700 rounded-lg p-4 hover:shadow-md transition-all duration-300 ease-in-out backdrop-blur-sm bg-white/15 dark:bg-transparent hover:bg-white/30 hover:backdrop-blur-md dark:hover:backdrop-blur-md hover:translate-y-[-2px]',
panelClass,
itemClasses?.panel
)}

View File

@@ -15,7 +15,7 @@ const { inputs, textarea, disclaimer, button = 'Contact us', description = '' }
}
</style>
<form id="contact-form" name="contact" netlify method="POST" class="needs-validation" data-netlify="true" data-netlify-honeypot="bot-field" novalidate>
<form id="contact-form" name="contact" method="POST" class="needs-validation" data-netlify="true" data-netlify-honeypot="bot-field" novalidate>
<!-- Form status messages -->
<div id="form-success" class="hidden mb-6 p-4 bg-green-100 border border-green-200 text-green-700 rounded-lg">

View File

@@ -0,0 +1,305 @@
---
import { Icon } from 'astro-icon/components';
export interface Props {
isDark?: boolean;
}
const { isDark = false } = Astro.props;
// Define color palettes for light and dark modes with higher contrast
const lightModeColors = [
'text-blue-600/40',
'text-indigo-600/40',
'text-purple-600/40',
'text-cyan-600/40',
'text-teal-600/40',
'text-emerald-600/40',
'text-sky-600/40',
'text-violet-600/40',
];
const darkModeColors = [
'dark:text-blue-400/45',
'dark:text-indigo-400/45',
'dark:text-purple-400/45',
'dark:text-cyan-400/45',
'dark:text-teal-400/45',
'dark:text-emerald-400/45',
'dark:text-sky-400/45',
'dark:text-violet-400/45',
];
// Define interfaces for our icon objects
interface BaseIconObject {
icon: string;
x: string;
y: string;
size: string;
opacity: string;
rotate: string;
}
interface IconWithColors extends BaseIconObject {
lightColor: string;
darkColor: string;
}
// Function to get a random color from the palette
const getRandomColor = (palette: string[]): string => {
const randomIndex = Math.floor(Math.random() * palette.length);
return palette[randomIndex];
};
// List of icons to be used in the background
// Expanded to include a wider variety of symbols for more visual diversity
const iconNames: string[] = [
// Core IT/Tech icons
'tabler:settings-automation',
'tabler:brand-office',
'tabler:api',
'tabler:server',
'tabler:message-chatbot',
'tabler:share',
'tabler:code',
'tabler:cloud',
'tabler:device-laptop',
'tabler:chart-line',
'tabler:database',
'tabler:brand-github',
'tabler:device-desktop',
'tabler:brand-azure',
// Additional tech-related icons
'tabler:cpu',
'tabler:device-mobile',
'tabler:wifi',
'tabler:network',
'tabler:shield',
'tabler:lock',
'tabler:key',
'tabler:rocket',
'tabler:satellite-off',
// Tangentially related icons for visual diversity
'tabler:bulb',
'tabler:puzzle',
'tabler:compass',
'tabler:chart-dots',
'tabler:math',
'tabler:atom',
'tabler:binary',
'tabler:circuit-resistor',
'tabler:infinity',
'tabler:planet',
'tabler:brain',
'tabler:cube',
];
// Function to get a random value within a range
const getRandomInRange = (min: number, max: number): number => {
return Math.random() * (max - min) + min;
};
// Function to get a random rotation with increased range
const getRandomRotation = (): string => {
return `${getRandomInRange(-30, 30)}deg`;
};
// Function to get a random size
const getRandomSize = (): string => {
return `${getRandomInRange(140, 180)}px`;
};
// Function to get a random opacity
const getRandomOpacity = (): string => {
return getRandomInRange(0.32, 0.38).toFixed(2);
};
// Create a spacious layout with well-separated icons
const createSpacedIcons = (): BaseIconObject[] => {
const icons: BaseIconObject[] = [];
const rows = 10; // Increased from 6 to 10 for more coverage across the entire page
const cols = 6;
// Define larger margins to keep icons away from edges (in percentage)
const marginX = 10;
const marginY = 5; // Reduced from 10 to 5 to allow icons to span more of the page height
// Minimum distance between icons (in percentage points)
const minDistance = 15; // Reduced from 20 to 15 to allow more icons
// Create a base grid of positions
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
// Randomly skip positions (65% chance) for fewer icons overall
if (Math.random() < 0.65) {
continue;
}
// Base position in the grid with margins applied
const baseX = marginX + ((col / cols) * (100 - 2 * marginX));
const baseY = marginY + ((row / rows) * (100 - 2 * marginY));
// Add limited randomness to the position (±5%) to maintain spacing
const randomOffsetX = getRandomInRange(-5, 5);
const randomOffsetY = getRandomInRange(-5, 5);
// Ensure positions stay within margins
const x = Math.max(marginX, Math.min(100 - marginX, baseX + randomOffsetX));
const y = Math.max(marginY, Math.min(100 - marginY, baseY + randomOffsetY));
// Check if this position is too close to any existing icon
let tooClose = false;
for (const existingIcon of icons) {
// Extract numeric values from percentage strings
const existingX = parseFloat(existingIcon.x);
const existingY = parseFloat(existingIcon.y);
// Calculate distance between points
const distance = Math.sqrt(
Math.pow(x - existingX, 2) +
Math.pow(y - existingY, 2)
);
// If too close to an existing icon, skip this position
if (distance < minDistance) {
tooClose = true;
break;
}
}
if (tooClose) {
continue;
}
// Randomly select an icon from the expanded set
const iconIndex = Math.floor(Math.random() * iconNames.length);
icons.push({
icon: iconNames[iconIndex],
x: `${x}%`,
y: `${y}%`,
size: getRandomSize(),
opacity: getRandomOpacity(),
rotate: getRandomRotation(),
});
}
}
return icons;
};
const icons: BaseIconObject[] = createSpacedIcons();
// Assign random colors to each icon
const iconsWithColors: IconWithColors[] = icons.map(icon => ({
icon: icon.icon,
x: icon.x,
y: icon.y,
size: icon.size,
opacity: icon.opacity,
rotate: icon.rotate,
lightColor: getRandomColor(lightModeColors),
darkColor: getRandomColor(darkModeColors),
}));
---
<div class="fixed inset-0 overflow-hidden pointer-events-none z-[-5]" aria-hidden="true">
<div class:list={['absolute inset-0', { 'backdrop-blur-sm bg-white/5 dark:bg-gray-900/10': isDark }]}></div>
{/* Decorative background icons with parallax effect */}
<div id="parallax-background" class="absolute inset-0 overflow-hidden">
{iconsWithColors.map(({ icon, x, y, size, opacity, rotate, lightColor, darkColor }, index) => (
<div
class={`absolute ${lightColor} ${darkColor} parallax-icon`}
style={`left: ${x}; top: ${y}; opacity: ${opacity}; transform: rotate(${rotate}); will-change: transform; transition: transform 0.1s ease-out;`}
data-depth={`${0.5 + (index % 3) * 0.2}`}
data-initial-x={x}
data-initial-y={y}
>
<Icon name={icon} style={`width: ${size}; height: ${size};`} />
</div>
))}
</div>
</div>
<script>
// Parallax scrolling effect for background icons
document.addEventListener('DOMContentLoaded', () => {
// Get all parallax icons
const parallaxIcons = document.querySelectorAll<HTMLElement>('.parallax-icon');
// Skip parallax on mobile devices for better performance
const isMobile = window.matchMedia('(max-width: 768px)').matches;
if (isMobile) return;
// Variables to track scroll position
let lastScrollY = window.scrollY;
let ticking = false;
// Function to update icon positions based on scroll
const updateParallax = () => {
parallaxIcons.forEach((icon) => {
const depth = parseFloat(icon.getAttribute('data-depth') || '0.5');
// Calculate parallax offset based on scroll position and depth
// Lower depth value means the icon moves slower (appears further away)
const yOffset = (lastScrollY * depth * 0.15);
// Get the original rotation
const transformValue = icon.style.transform;
const rotateMatch = transformValue.match(/rotate\([^)]+\)/);
const rotateValue = rotateMatch ? rotateMatch[0] : 'rotate(0deg)';
// Apply transform with the original rotation plus the parallax offset
icon.style.transform = `${rotateValue} translate3d(0, ${yOffset}px, 0)`;
});
ticking = false;
};
// Throttle scroll events for better performance
const onScroll = () => {
lastScrollY = window.scrollY;
if (!ticking) {
window.requestAnimationFrame(() => {
updateParallax();
ticking = false;
});
ticking = true;
}
};
// Add scroll event listener
window.addEventListener('scroll', onScroll, { passive: true });
// Update on resize (debounced)
let resizeTimer: number;
window.addEventListener('resize', () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
// Check if device is now mobile and disable parallax if needed
const isMobileNow = window.matchMedia('(max-width: 768px)').matches;
if (isMobileNow) {
// Reset positions on mobile
parallaxIcons.forEach((icon) => {
const transformValue = icon.style.transform;
const rotateMatch = transformValue.match(/rotate\([^)]+\)/);
const rotateValue = rotateMatch ? rotateMatch[0] : 'rotate(0deg)';
icon.style.transform = rotateValue;
});
} else {
// Update parallax on desktop
updateParallax();
}
}, 200) as unknown as number;
}, { passive: true });
// Initial update
updateParallax();
});
</script>

View File

@@ -20,16 +20,16 @@ const {
(title || subtitle || tagline) && (
<div class={twMerge('mb-8 md:mx-auto md:mb-12 text-center', containerClass)}>
{tagline && (
<p class="text-base text-secondary dark:text-blue-200 font-bold tracking-wide uppercase" set:html={tagline} />
<p class="text-base text-secondary dark:text-blue-200 font-bold tracking-wide uppercase content-backdrop p-2 inline-block rounded-md" set:html={tagline} />
)}
{title && (
<h2
class={twMerge('font-bold leading-tighter tracking-tighter font-heading text-heading text-3xl', titleClass)}
class={twMerge('font-bold leading-tighter tracking-tighter font-heading text-heading text-3xl content-backdrop p-2 block rounded-md', titleClass)}
set:html={title}
/>
)}
{subtitle && <p class={twMerge('mt-4 text-muted', subtitleClass)} set:html={subtitle} />}
{subtitle && <p class={twMerge('mt-2 text-muted content-backdrop p-2 rounded-md', subtitleClass)} set:html={subtitle} />}
</div>
)
}

View File

@@ -11,7 +11,7 @@ const {
panel: panelClass = '',
title: titleClass = '',
description: descriptionClass = '',
icon: defaultIconClass = 'text-primary',
icon: defaultIconClass = 'text-primary dark:text-blue-300 dark:shadow-blue-500/40 dark:shadow-sm',
action: actionClass = '',
} = classes;
---
@@ -34,7 +34,7 @@ const {
>
{items.map(({ title, description, icon, callToAction, classes: itemClasses = {} }) => (
<div class="intersect-once motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade">
<div class={twMerge('flex flex-row max-w-md', panelClass, itemClasses?.panel)}>
<div class={twMerge('flex flex-row max-w-md backdrop-blur-sm bg-white/15 dark:bg-transparent p-3 rounded-md border border-gray-200 dark:border-gray-700 hover:bg-white/30 hover:backdrop-blur-md dark:hover:backdrop-blur-md transition-all duration-300 ease-in-out', panelClass, itemClasses?.panel)}>
<div class="flex justify-center">
{(icon || defaultIcon) && (
<Icon

View File

@@ -17,7 +17,7 @@ const {
panel: panelClass = '',
title: titleClass = '',
description: descriptionClass = '',
icon: defaultIconClass = 'text-primary dark:text-slate-200 border-primary dark:border-blue-700',
icon: defaultIconClass = 'text-primary dark:text-blue-300 border-primary dark:border-blue-500 dark:shadow-blue-500/40 dark:shadow-sm',
timeline: timelineClass = 'bg-primary/30 dark:bg-blue-700/30',
timelineDot: timelineDotClass = 'bg-primary dark:bg-blue-700',
year: yearClass = 'text-primary dark:text-blue-300',
@@ -28,7 +28,7 @@ const {
items && items.length && (
<div class={twMerge("relative mx-auto max-w-5xl", containerClass)}>
{/* Main timeline line */}
<div class="absolute left-4 md:left-1/2 top-0 h-full w-1 transform -translate-x-1/2 z-0 transition-all duration-300 ease-in-out" class:list={[timelineClass]}></div>
<div class="absolute left-4 md:left-1/2 top-0 h-full w-1 transform -translate-x-1/2 z-0 transition-all duration-300 ease-in-out shadow-sm bg-primary/70 dark:bg-blue-700/30" class:list={[timelineClass]}></div>
<div class="relative">
{items.map((item, index) => {
@@ -69,7 +69,7 @@ const {
>
<div
class={twMerge(
`flex flex-col border border-gray-200 dark:border-gray-700 rounded-lg ${compact ? 'p-3' : 'p-4'} shadow-sm hover:shadow-md transition-all duration-300 ease-in-out bg-white dark:bg-gray-900 hover:translate-y-[-3px] group card-container`,
`flex flex-col border border-gray-200 dark:border-gray-700 rounded-lg ${compact ? 'p-3' : 'p-4'} shadow-sm hover:shadow-md transition-all duration-300 ease-in-out backdrop-blur-sm bg-white/15 dark:bg-transparent hover:bg-white/30 hover:backdrop-blur-md dark:hover:backdrop-blur-md hover:translate-y-[-3px] group card-container`,
panelClass,
itemClasses?.panel
)}
@@ -95,12 +95,11 @@ const {
</div>
)}
</div>
{/* Connector line to timeline (visible only on desktop) */}
<div class={twMerge(
'absolute top-5 hidden md:block h-0.5 w-6 z-0',
isEven ? 'right-0 bg-gradient-to-r' : 'left-0 bg-gradient-to-l',
'from-transparent to-primary/70 dark:to-blue-700/70'
'from-transparent to-primary/50 dark:to-blue-700'
)}></div>
</div>
</div>

View File

@@ -16,7 +16,7 @@ const {
panel: panelClass = '',
title: titleClass = '',
description: descriptionClass = '',
icon: defaultIconClass = 'text-primary dark:text-slate-200 border-primary dark:border-blue-700',
icon: defaultIconClass = 'text-primary dark:text-blue-300 border-primary dark:border-blue-500 dark:shadow-blue-500/40 dark:shadow-sm',
arrow: arrowClass = 'text-primary dark:text-slate-200',
} = classes;
---
@@ -68,7 +68,7 @@ const {
{/* Timeline item */}
<div
class={twMerge(
'flex intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade border border-gray-200 dark:border-gray-700 rounded-lg p-5 hover:shadow-md transition-all duration-300 ease-in-out hover:bg-gray-50 dark:hover:bg-gray-800 ml-8 md:ml-0 shadow-sm relative',
'flex intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade border border-gray-200 dark:border-gray-700 rounded-lg p-5 hover:shadow-md transition-all duration-300 ease-in-out backdrop-blur-sm bg-white/15 dark:bg-transparent hover:bg-white/30 hover:backdrop-blur-md dark:hover:backdrop-blur-md ml-8 md:ml-0 shadow-sm relative hover:translate-y-[-2px]',
panelClass,
itemClasses?.panel
)}

View File

@@ -16,7 +16,7 @@ const {
panel: panelClass = '',
title: titleClass = '',
description: descriptionClass = '',
icon: defaultIconClass = 'text-primary dark:text-slate-200 border-primary dark:border-blue-700',
icon: defaultIconClass = 'text-primary dark:text-blue-300 border-primary dark:border-blue-500 dark:shadow-blue-500/40 dark:shadow-sm',
} = classes;
---
@@ -42,7 +42,7 @@ const {
)}
</div>
</div>
{index !== items.length - 1 && <div class="w-px h-full bg-black/10 dark:bg-slate-400/50" />}
{index !== items.length - 1 && <div class="w-px h-full bg-primary/30 dark:bg-slate-400/50" />}
</div>
<div class={`pt-1 ${index !== items.length - 1 ? 'pb-8' : ''}`}>
{title && <p class={twMerge('text-xl font-bold', titleClass, itemClasses?.title)} set:html={title} />}

View File

@@ -7,19 +7,22 @@ import Background from './Background.astro';
export interface Props extends Widget {
containerClass?: string;
['as']?: HTMLTag;
disableBackground?: boolean;
}
const { id, isDark = false, containerClass = '', bg, as = 'section' } = Astro.props;
const { id, isDark = false, containerClass = '', bg, as = 'section', disableBackground = true } = Astro.props;
const WrapperTag = as;
---
<WrapperTag class="relative not-prose scroll-mt-[72px]" {...id ? { id } : {}}>
<div class="absolute inset-0 pointer-events-none -z-[1]" aria-hidden="true">
<slot name="bg">
{bg ? <Fragment set:html={bg} /> : <Background isDark={isDark} />}
</slot>
</div>
{!disableBackground && (
<div class="absolute inset-0 pointer-events-none -z-[1]" aria-hidden="true">
<slot name="bg">
{bg ? <Fragment set:html={bg} /> : <Background isDark={isDark} />}
</slot>
</div>
)}
<div
class:list={[
twMerge(

View File

@@ -27,7 +27,7 @@ const {
<WidgetWrapper id={id} isDark={isDark} containerClass={`max-w-6xl mx-auto ${classes?.container ?? ''}`} bg={bg}>
<div
class="max-w-3xl mx-auto text-center p-6 rounded-md shadow-xl dark:shadow-none dark:border dark:border-slate-600"
class="max-w-3xl mx-auto text-center p-6 rounded-md shadow-md backdrop-blur-sm bg-white/15 dark:bg-transparent border border-gray-200 dark:border-slate-600 hover:bg-white/25 dark:hover:backdrop-blur-md transition-all duration-300 ease-in-out hover:shadow-lg"
>
<Headline
title={title}

View File

@@ -48,7 +48,7 @@ const {
{
testimonials &&
testimonials.map(({ linkUrl, name, issueDate, description, image }) => (
<div class="flex flex-col p-3 rounded-md shadow-md dark:shadow-none dark:border dark:border-slate-600 hover:shadow-lg transition-all duration-300 ease-in-out hover:bg-gray-50 dark:hover:bg-gray-800">
<div class="flex flex-col p-3 rounded-md shadow-md dark:shadow-none border border-gray-200 dark:border-slate-600 backdrop-blur-sm bg-white/15 dark:bg-transparent hover:shadow-lg transition-all duration-300 ease-in-out hover:bg-white/30 hover:backdrop-blur-md dark:hover:backdrop-blur-md hover:translate-y-[-2px]">
<div class="flex items-center mb-3">
<div
class="h-12 w-12 mr-3 flex-shrink-0 bg-gray-100 dark:bg-gray-800 rounded-md flex items-center justify-center overflow-hidden cursor-pointer"

View File

@@ -31,9 +31,9 @@ const {
<div class="mt-6 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{items.map(({ title, description, icon }) => (
<div class="group bg-white dark:bg-slate-800 p-3 rounded-lg shadow-sm hover:shadow-md transition-all duration-300 ease-in-out hover:bg-gray-50 dark:hover:bg-gray-800 border border-gray-200 dark:border-gray-700">
<div class="group backdrop-blur-sm bg-white/15 dark:bg-transparent p-3 rounded-lg shadow-sm hover:shadow-md transition-all duration-300 ease-in-out hover:bg-white/30 hover:backdrop-blur-md dark:hover:backdrop-blur-md border border-gray-200 dark:border-gray-700 hover:translate-y-[-2px]">
<div class="flex items-center mb-2">
<Icon name={icon || defaultIcon} class="w-5 h-5 mr-2 text-primary transition-transform duration-300 group-hover:scale-110" />
<Icon name={icon || defaultIcon} class="w-5 h-5 mr-2 text-primary dark:text-blue-300 transition-transform duration-300 group-hover:scale-110 dark:shadow-blue-500/40 dark:shadow-sm" />
<h3 class="text-base font-semibold">{title}</h3>
</div>
<div class="text-muted overflow-hidden">

View File

@@ -26,7 +26,7 @@ const {
{
inputs && (
<div class="flex flex-col max-w-xl mx-auto rounded-lg backdrop-blur border border-gray-200 dark:border-gray-700 bg-white dark:bg-slate-900 shadow p-4 sm:p-6 lg:p-8 w-full">
<div class="flex flex-col max-w-xl mx-auto rounded-lg backdrop-blur-sm bg-white/15 dark:bg-transparent border border-gray-200 dark:border-gray-700 shadow-md hover:shadow-lg hover:bg-white/25 dark:hover:backdrop-blur-md transition-all duration-300 ease-in-out p-4 sm:p-6 lg:p-8 w-full">
<FormContainer
inputs={inputs}
textarea={textarea}

View File

@@ -44,7 +44,7 @@ const {
<div class="mx-auto max-w-7xl p-4 md:px-8">
<div class={`md:flex ${isReversed ? 'md:flex-row-reverse' : ''} md:gap-16`}>
<div class="md:basis-1/2 self-center">
{content && <div class="mb-12 text-lg dark:text-slate-400" set:html={content} />}
{content && <div class="mb-12 text-lg dark:text-slate-400 content-backdrop p-3 rounded-md" set:html={content} />}
{
callToAction && (
@@ -60,7 +60,7 @@ const {
defaultIcon="tabler:check"
classes={{
container: `gap-y-4 md:gap-y-8`,
panel: 'max-w-none',
panel: 'max-w-none content-backdrop p-3 rounded-md',
title: 'text-lg font-medium leading-6 dark:text-white ml-2 rtl:ml-0 rtl:mr-2',
description: 'text-muted dark:text-slate-400 ml-2 rtl:ml-0 rtl:mr-2',
icon: 'flex h-7 w-7 items-center justify-center rounded-full bg-green-600 dark:bg-green-700 text-gray-50 p-1',

View File

@@ -1,6 +1,6 @@
---
import Headline from '~/components/ui/Headline.astro';
import ItemGrid from '~/components/ui/ItemGrid.astro';
import Accordion from '~/components/ui/Accordion.astro';
import WidgetWrapper from '~/components/ui/WidgetWrapper.astro';
import type { Faqs as Props } from '~/types';
@@ -20,14 +20,5 @@ const {
<WidgetWrapper id={id} isDark={isDark} containerClass={`max-w-7xl mx-auto ${classes?.container ?? ''}`} bg={bg}>
<Headline title={title} subtitle={subtitle} tagline={tagline} />
<ItemGrid
items={items}
columns={columns}
defaultIcon="tabler:chevron-right"
classes={{
container: `${columns === 1 ? 'max-w-4xl' : ''} gap-y-8 md:gap-y-12`,
panel: 'max-w-none',
icon: 'flex-shrink-0 mt-1 w-6 h-6 text-primary',
}}
/>
<Accordion items={items} />
</WidgetWrapper>

View File

@@ -63,8 +63,9 @@ if (!currentLang) {
}
}
// Get translated header data
// Get translated header data - ensure we're using the current language
const headerData = getHeaderData(currentLang);
console.log(`Header initialized with language: ${currentLang}`);
const {
id = 'header',

View File

@@ -1,8 +1,13 @@
---
import Image from '~/components/common/Image.astro';
import Button from '~/components/ui/Button.astro';
import Background from '~/components/ui/Background.astro';
import type { Hero as Props } from '~/types';
import type { Hero } from '~/types';
export interface Props extends Hero {
disableBackground?: boolean;
}
const {
title = await Astro.slots.render('title'),
@@ -14,16 +19,20 @@ const {
image = await Astro.slots.render('image'),
id,
isDark = false,
bg = await Astro.slots.render('bg'),
disableBackground = true,
} = Astro.props;
---
<section class="relative md:-mt-[76px] not-prose bg-hero" {...id ? { id } : {}}>
<div class="absolute inset-0 pointer-events-none" aria-hidden="true">
<slot name="bg">
{bg ? <Fragment set:html={bg} /> : null}
</slot>
</div>
<section class="relative md:-mt-[76px] not-prose" {...id ? { id } : {}}>
{!disableBackground && (
<div class="absolute inset-0 pointer-events-none" aria-hidden="true">
<slot name="bg">
{bg ? <Fragment set:html={bg} /> : <Background isDark={isDark} disableParallax={true} />}
</slot>
</div>
)}
<div class="relative max-w-7xl mx-auto px-4 sm:px-6">
<div class="pt-0 md:pt-[76px] pointer-events-none"></div>
<div class="py-12 md:py-20">
@@ -39,7 +48,7 @@ const {
{
title && (
<h1
class="text-5xl md:text-6xl font-bold leading-tighter tracking-tighter mb-4 font-heading dark:text-gray-200 intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade"
class="text-5xl md:text-6xl font-bold leading-tighter tracking-tighter mb-4 font-heading dark:text-gray-200 intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade content-backdrop p-2 inline-block rounded-md"
set:html={title}
/>
)
@@ -48,14 +57,14 @@ const {
{
subtitle && (
<p
class="text-xl text-muted mb-6 dark:text-slate-300 intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade"
class="text-xl text-muted mb-6 dark:text-slate-300 intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade content-backdrop p-2 rounded-md"
set:html={subtitle}
/>
)
}
{
actions && (
<div class="max-w-xs sm:max-w-md m-auto flex flex-nowrap flex-col sm:flex-row sm:justify-center gap-4 intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade">
<div class="max-w-xs sm:max-w-md m-auto flex flex-nowrap flex-col sm:flex-row sm:justify-center gap-4 intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade content-backdrop p-2 rounded-md">
{Array.isArray(actions) ? (
actions.map((action) => (
<div class="flex w-full sm:w-auto">
@@ -76,7 +85,7 @@ const {
>
{
image && (
<div class="relative m-auto max-w-5xl">
<div class="relative m-auto max-w-5xl z-10">
{typeof image === 'string' ? (
<Fragment set:html={image} />
) : (

View File

@@ -1,6 +1,7 @@
---
import Image from '~/components/common/Image.astro';
import Button from '~/components/ui/Button.astro';
import Background from '~/components/ui/Background.astro';
import type { Hero as Props } from '~/types';
@@ -14,6 +15,7 @@ const {
image = await Astro.slots.render('image'),
id,
isDark = false,
bg = await Astro.slots.render('bg'),
} = Astro.props;
---
@@ -21,7 +23,7 @@ const {
<section class="relative md:-mt-[76px] not-prose" {...id ? { id } : {}}>
<div class="absolute inset-0 pointer-events-none" aria-hidden="true">
<slot name="bg">
{bg ? <Fragment set:html={bg} /> : null}
{bg ? <Fragment set:html={bg} /> : <Background isDark={isDark} showIcons={false} disableParallax={true} />}
</slot>
</div>
<div class="relative max-w-7xl mx-auto px-4 sm:px-6">
@@ -39,7 +41,7 @@ const {
{
title && (
<h1
class="text-5xl md:text-6xl font-bold leading-tighter tracking-tighter mb-4 font-heading dark:text-gray-200 intersect-once motion-safe:md:intersect:animate-fade motion-safe:md:opacity-0 intersect-quarter"
class="text-5xl md:text-6xl font-bold leading-tighter tracking-tighter mb-4 font-heading dark:text-gray-200 intersect-once motion-safe:md:intersect:animate-fade motion-safe:md:opacity-0 intersect-quarter content-backdrop p-2 inline-block rounded-md"
set:html={title}
/>
)
@@ -48,7 +50,7 @@ const {
{
subtitle && (
<p
class="text-xl text-muted mb-6 dark:text-slate-300 intersect-once motion-safe:md:intersect:animate-fade motion-safe:md:opacity-0 intersect-quarter"
class="text-xl text-muted mb-6 dark:text-slate-300 intersect-once motion-safe:md:intersect:animate-fade motion-safe:md:opacity-0 intersect-quarter content-backdrop p-2 rounded-md"
set:html={subtitle}
/>
)
@@ -56,7 +58,7 @@ const {
{
actions && (
<div class="max-w-xs sm:max-w-md m-auto flex flex-nowrap flex-col sm:flex-row sm:justify-center gap-4 lg:justify-start lg:m-0 lg:max-w-7xl intersect-once motion-safe:md:intersect:animate-fade motion-safe:md:opacity-0 intersect-quarter">
<div class="max-w-xs sm:max-w-md m-auto flex flex-nowrap flex-col sm:flex-row sm:justify-center gap-4 lg:justify-start lg:m-0 lg:max-w-7xl intersect-once motion-safe:md:intersect:animate-fade motion-safe:md:opacity-0 intersect-quarter content-backdrop p-2 rounded-md">
{Array.isArray(actions) ? (
actions.map((action) => (
<div class="flex w-full sm:w-auto">
@@ -75,7 +77,7 @@ const {
<div class="basis-1/2">
{
image && (
<div class="relative m-auto max-w-5xl intersect-once intercept-no-queue motion-safe:md:intersect:animate-fade motion-safe:md:opacity-0 intersect-quarter">
<div class="relative m-auto max-w-5xl z-10 intersect-once intercept-no-queue motion-safe:md:intersect:animate-fade motion-safe:md:opacity-0 intersect-quarter">
{typeof image === 'string' ? (
<Fragment set:html={image} />
) : (

View File

@@ -26,7 +26,7 @@ const {
{
testimonials &&
testimonials.map(({ title, linkUrl, name, issueDate, description, image }) => (
<a href={linkUrl} target="_blank" rel="noopener noreferrer" class="flex flex-col justify-start items-center p-2 md:p-4 rounded-md shadow-xl dark:shadow-none dark:border dark:border-slate-600 w-[425px] mx-auto sm:mx-0">
<a href={linkUrl} target="_blank" rel="noopener noreferrer" class="flex flex-col justify-start items-center p-2 md:p-4 rounded-md shadow-md backdrop-blur-sm bg-white/15 dark:bg-transparent border border-gray-200 dark:border-slate-600 hover:bg-white/30 hover:backdrop-blur-md dark:hover:backdrop-blur-md transition-all duration-300 ease-in-out hover:shadow-lg hover:translate-y-[-2px] w-[425px] mx-auto sm:mx-0">
{title && <h2 class="text-lg font-medium leading-6 pb-4 text-center">{title}</h2>}
{image && (
<div class="h-[160px] w-[160px] border-slate-200 dark:border-slate-600 mx-auto">
@@ -36,11 +36,8 @@ const {
height={160}
widths={[400, 768]}
layout="fixed"
// If image is a string, use it as the src; otherwise assume it's an object with a src property.
src={typeof image === 'string' ? image : image.src}
// Use a default alt text if image is a string; otherwise use the provided alt.
alt={typeof image === 'string' ? 'Image' : image.alt}
// Spread the rest of the properties if image is an object.
src={typeof image === 'string' ? image : (image as { src: string }).src}
alt={typeof image === 'string' ? 'Image' : (image as { alt?: string }).alt || 'Testimonial'}
{...(typeof image === 'string' ? {} : image)}
/>
</div>

View File

@@ -73,8 +73,8 @@ const timelineItems = items.map(item => {
title: 'text-xl font-bold',
description: 'text-muted mt-2',
icon: 'text-primary dark:text-slate-200 border-primary dark:border-blue-700',
timeline: 'bg-primary/30 dark:bg-blue-700/30',
timelineDot: 'bg-primary dark:bg-blue-700',
timeline: '',
timelineDot: 'bg-primary shadow-md dark:bg-blue-700',
year: 'text-primary dark:text-blue-300',
}}
/>