diff --git a/astro.config.ts b/astro.config.ts index be007be..5df9336 100644 --- a/astro.config.ts +++ b/astro.config.ts @@ -25,7 +25,7 @@ export default defineConfig({ output: 'static', i18n: { - locales: ["en", "de", "nl"], + locales: ["en", "de", "nl", "fr"], defaultLocale: "en", }, @@ -67,7 +67,7 @@ export default defineConfig({ removeAttributeQuotes: false, }, }, - Image: false, + Image: true, JavaScript: true, SVG: false, Logger: 1, diff --git a/src/components/CustomStyles.astro b/src/components/CustomStyles.astro index 4de74d6..bbd2522 100644 --- a/src/components/CustomStyles.astro +++ b/src/components/CustomStyles.astro @@ -20,6 +20,12 @@ import '@fontsource-variable/inter'; --- \ No newline at end of file diff --git a/src/components/ui/ImageModal.astro b/src/components/ui/ImageModal.astro new file mode 100644 index 0000000..5919485 --- /dev/null +++ b/src/components/ui/ImageModal.astro @@ -0,0 +1,169 @@ +--- +// ImageModal.astro - A reusable modal component for displaying enlarged images +--- + +
+ + + + + +
+ + \ No newline at end of file diff --git a/src/components/ui/ModernTimeline.astro b/src/components/ui/ModernTimeline.astro new file mode 100644 index 0000000..5064d80 --- /dev/null +++ b/src/components/ui/ModernTimeline.astro @@ -0,0 +1,164 @@ +--- +import { Icon } from 'astro-icon/components'; +import { twMerge } from 'tailwind-merge'; +import type { Item } from '~/types'; + +export interface Props { + items?: Array; + defaultIcon?: string; + classes?: Record; + compact?: boolean; +} + +const { items = [], classes = {}, defaultIcon, compact = false } = Astro.props as Props; + +const { + container: containerClass = '', + panel: panelClass = '', + title: titleClass = '', + description: descriptionClass = '', + icon: defaultIconClass = 'text-primary dark:text-slate-200 border-primary dark:border-blue-700', + 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', +} = classes; +--- + +{ + items && items.length && ( +
+ {/* Main timeline line */} +
+ +
+ {items.map((item, index) => { + const { title, description, icon, classes: itemClasses = {} } = item; + const isEven = index % 2 === 0; + + // Use the year property if available, otherwise try to extract from date + let year = item.year; + + // If year is not provided, try to extract from date in the description + if (!year && description) { + // Look for a date pattern like MM-YYYY + const dateMatch = description.match(/\d{2}-(\d{4})/); + if (dateMatch) { + year = dateMatch[1]; + } + } + + return ( +
+ {/* Year marker (if available) */} + {year && ( +
+ {year} +
+ )} + + {/* Timeline dot */} +
+ + {/* Content card */} +
+
+
+ {(icon || defaultIcon) && ( + + )} + {title &&

} +

+ {description && ( +
+
+
+
+
+
+ )} +
+ + {/* Connector line to timeline (visible only on desktop) */} + +
+
+ ); + })} +
+
+ ) +} + + \ No newline at end of file diff --git a/src/components/ui/StaggeredTimeline.astro b/src/components/ui/StaggeredTimeline.astro new file mode 100644 index 0000000..389b55d --- /dev/null +++ b/src/components/ui/StaggeredTimeline.astro @@ -0,0 +1,128 @@ +--- +import { Icon } from 'astro-icon/components'; +import { twMerge } from 'tailwind-merge'; +import type { Item } from '~/types'; + +export interface Props { + items?: Array; + defaultIcon?: string; + classes?: Record; +} + +const { items = [], classes = {}, defaultIcon } = Astro.props as Props; + +const { + container: containerClass = '', + panel: panelClass = '', + title: titleClass = '', + description: descriptionClass = '', + icon: defaultIconClass = 'text-primary dark:text-slate-200 border-primary dark:border-blue-700', + arrow: arrowClass = 'text-primary dark:text-slate-200', +} = classes; +--- + +{ + items && items.length && ( +
+ {/* Mobile timeline line */} +
+ +
+ {items.map((item, index) => { + const { title, description, icon, classes: itemClasses = {} } = item; + const isEven = index % 2 === 0; + const isFirst = index === 0; + + // Calculate vertical offset based on position with consistent spacing + const offsetValue = index * 8; + + return ( +
+ {/* Arrow connecting to previous item (except for first item) */} + {!isFirst && ( + + )} + + {/* Timeline item */} +
+
+ {(icon || defaultIcon) && ( + + )} +
+
+ {title &&

} + {description && ( +

+
+
+ )} +
+
+
+ ); + })} +
+ + {/* Responsive styles for small screens */} + +
+ ) +} \ No newline at end of file diff --git a/src/components/widgets/CompactCertifications.astro b/src/components/widgets/CompactCertifications.astro new file mode 100644 index 0000000..cb63ab4 --- /dev/null +++ b/src/components/widgets/CompactCertifications.astro @@ -0,0 +1,105 @@ +--- +import Headline from '~/components/ui/Headline.astro'; +import WidgetWrapper from '~/components/ui/WidgetWrapper.astro'; +import Button from '~/components/ui/Button.astro'; +import ImageModal from '~/components/ui/ImageModal.astro'; +import type { Testimonials as Props } from '~/types'; +import DefaultImage from '~/assets/images/default.png'; + +// Function to get the correct image path for a testimonial +const getImagePath = (image: unknown) => { + if (typeof image === 'object' && image !== null && 'src' in image && typeof (image as { src: unknown }).src === 'string') { + // If the image has a src property, use it + return String((image as { src: string }).src); + } + // Otherwise, return the default image path + return DefaultImage.src; +}; + +// Function to get the alt text for an image +const getImageAlt = (image: unknown, fallback: string = "Certification badge") => { + if (typeof image === 'object' && image !== null && 'alt' in image && typeof (image as { alt: unknown }).alt === 'string') { + return String((image as { alt: string }).alt); + } + return fallback; +}; + +const { + title = '', + subtitle = '', + tagline = '', + testimonials = [], + callToAction, + + id, + isDark = false, + classes = {}, + bg = await Astro.slots.render('bg'), +} = Astro.props; +--- + + + + +
+ { + testimonials && + testimonials.map(({ linkUrl, name, issueDate, description, image }) => ( + + )) + } +
+ + {/* Include the image modal component */} + + + { + callToAction && ( +
+
+ ) + } +
+ + \ No newline at end of file diff --git a/src/components/widgets/CompactSkills.astro b/src/components/widgets/CompactSkills.astro new file mode 100644 index 0000000..ed0efa7 --- /dev/null +++ b/src/components/widgets/CompactSkills.astro @@ -0,0 +1,57 @@ +--- +import Headline from '~/components/ui/Headline.astro'; +import WidgetWrapper from '~/components/ui/WidgetWrapper.astro'; +import { Icon } from 'astro-icon/components'; +import type { Features as Props } from '~/types'; + +const { + title = await Astro.slots.render('title'), + subtitle = await Astro.slots.render('subtitle'), + tagline = await Astro.slots.render('tagline'), + items = [], + defaultIcon = 'tabler:point-filled', + + id, + isDark = false, + classes = {}, + bg = await Astro.slots.render('bg'), +} = Astro.props; +--- + + + + +
+ {items.map(({ title, description, icon }) => ( +
+
+ +

{title}

+
+
+

{description}

+
+
+ ))} +
+
+ + \ No newline at end of file diff --git a/src/components/widgets/CompactSteps.astro b/src/components/widgets/CompactSteps.astro new file mode 100644 index 0000000..cf6a450 --- /dev/null +++ b/src/components/widgets/CompactSteps.astro @@ -0,0 +1,34 @@ +--- +import WidgetWrapper from '~/components/ui/WidgetWrapper.astro'; +import CompactTimeline from '~/components/ui/CompactTimeline.astro'; +import Headline from '~/components/ui/Headline.astro'; +import type { Steps as Props } from '~/types'; + +const { + title = await Astro.slots.render('title'), + subtitle = await Astro.slots.render('subtitle'), + tagline = await Astro.slots.render('tagline'), + items = [], + + id, + isDark = false, + classes = {}, + bg = await Astro.slots.render('bg'), +} = Astro.props; +--- + + +
+ + } /> +
+
\ No newline at end of file diff --git a/src/components/widgets/Footer.astro b/src/components/widgets/Footer.astro index 12a18e5..3ec652c 100644 --- a/src/components/widgets/Footer.astro +++ b/src/components/widgets/Footer.astro @@ -16,14 +16,14 @@ interface Links { } export interface Props { - links: Array; + links?: Array; secondaryLinks: Array; socialLinks: Array; footNote?: string; theme?: string; } -const { socialLinks = [], secondaryLinks = [], links = [], footNote = '', theme = 'light' } = Astro.props; +const { socialLinks = [], theme = 'light' } = Astro.props; ---
@@ -52,8 +52,8 @@ const { socialLinks = [], secondaryLinks = [], links = [], footNote = '', theme { socialLinks?.length && (
    - {socialLinks.map(({ ariaLabel, href, icon }, index) => ( -
  • + {socialLinks.map(({ ariaLabel, href, icon }) => ( +
+ +