Compare commits

..

8 Commits

Author SHA1 Message Date
48e9cbcb02 chore(deps): update dependency python to 3.14
Some checks failed
test-build / build (pull_request) Successful in 2m22s
test-build / guarddog (pull_request) Failing after 2m36s
2026-02-16 05:41:09 +00:00
a74cc775d0 feat: final refactor of sections
All checks were successful
test-build / guarddog (push) Successful in 35s
test-build / build (push) Successful in 1m1s
renovate / renovate (push) Successful in 2m15s
2026-02-15 23:38:55 -06:00
5271be52a2 feat: rename button components to include button in name for consistency 2026-02-15 22:05:36 -06:00
8a649b7647 feat: imporvement pass over sections 2026-02-15 15:42:27 -06:00
c4be4653be fix: run theme on page swap 2026-02-15 00:05:16 -06:00
47a637353c feat: move improved components out of ui folder 2026-02-14 23:10:43 -06:00
a09a4ee240 feat: imporve theme toggle button 2026-02-14 23:08:12 -06:00
342ae8900a feat: refactor buttons, except for theme 2026-02-14 22:09:49 -06:00
45 changed files with 692 additions and 831 deletions

View File

@@ -60,7 +60,7 @@ jobs:
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v6 uses: actions/setup-python@v6
with: with:
python-version: '3.12' python-version: '3.14'
- name: Install GuardDog - name: Install GuardDog
run: | run: |

View File

@@ -1,6 +1,6 @@
--- ---
import BrandLogo from '@components/ui/logos/BrandLogo.astro'; import BrandLogo from '@components/ui/logos/BrandLogo.astro';
import ThemeToggle from '@components/ui/buttons/ThemeToggle.astro'; import ThemeToggleButton from '@components/buttons/ThemeToggleButton.astro';
import { NavigationLinks } from '@/config'; import { NavigationLinks } from '@/config';
const pathname = new URL(Astro.request.url).pathname; const pathname = new URL(Astro.request.url).pathname;
@@ -67,7 +67,7 @@ const currentPath = pathname.slice(1);
<div <div
id="navbar-collapse-with-animation" id="navbar-collapse-with-animation"
class="hs-collapse hidden grow basis-full overflow-hidden transition-all duration-300 md:block" class="hs-collapse hidden grow basis-full overflow-hidden transition-all duration-300 md:block md:overflow-visible"
> >
<div <div
class="mt-5 flex flex-col gap-x-0 gap-y-4 md:mt-0 md:flex-row md:items-center md:justify-end md:gap-x-4 md:gap-y-0 md:ps-7 lg:gap-x-7" class="mt-5 flex flex-col gap-x-0 gap-y-4 md:mt-0 md:flex-row md:items-center md:justify-end md:gap-x-4 md:gap-y-0 md:ps-7 lg:gap-x-7"
@@ -90,7 +90,7 @@ const currentPath = pathname.slice(1);
}) })
} }
<span class="md:inline-block"> <span class="md:inline-block">
<ThemeToggle /> <ThemeToggleButton />
</span> </span>
</div> </div>
</div> </div>

View File

@@ -1,5 +1,5 @@
--- ---
import PrimaryCTA from '@components/ui/buttons/PrimaryCTA.astro'; import GoLinkPrimaryButton from '@components/buttons/GoLinkPrimaryButton.astro';
import Image from '@components/ui/images/Image.astro'; import Image from '@components/ui/images/Image.astro';
interface Props { interface Props {
@@ -39,6 +39,6 @@ const { title, subTitle, btnExists, btnTitle, btnURL, img, imgAlt } = Astro.prop
> >
{subTitle} {subTitle}
</p> </p>
{btnExists ? <PrimaryCTA title={btnTitle} url={btnURL} /> : null} {btnExists ? <GoLinkPrimaryButton title={btnTitle} url={btnURL} /> : null}
</div> </div>
</section> </section>

View File

@@ -1,5 +1,5 @@
--- ---
import PrimaryCTA from '@components/ui/buttons/PrimaryCTA.astro'; import GoLinkPrimaryButton from '@components/buttons/GoLinkPrimaryButton.astro';
import Image from '@components/ui/images/Image.astro'; import Image from '@components/ui/images/Image.astro';
interface Props { interface Props {
@@ -43,7 +43,7 @@ const {
> >
{subTitle} {subTitle}
</p> </p>
{btnExists ? <PrimaryCTA title={btnTitle} url={btnURL} /> : null} {btnExists ? <GoLinkPrimaryButton title={btnTitle} url={btnURL} /> : null}
</div> </div>
{ {

View File

@@ -4,7 +4,7 @@ import Icon from '@components/ui/icons/icon.astro';
<button <button
type="button" type="button"
class="focus-visible:ring-secondary group inline-flex items-center rounded-lg p-2.5 text-neutral-600 ring-neutral-500 transition duration-300 outline-none hover:bg-neutral-100 focus:outline-none focus-visible:ring-1 focus-visible:outline-none dark:text-neutral-400 dark:ring-neutral-200 dark:hover:bg-neutral-700" class="button-base button-bg-blue group inline-flex items-center rounded-lg p-2.5"
data-bookmark-button="bookmark-button" data-bookmark-button="bookmark-button"
> >
<Icon name="bookmark" /> <Icon name="bookmark" />

View File

@@ -0,0 +1,31 @@
---
import { Icon } from 'astro-icon/components';
interface Props {
title?: string;
url?: string;
}
const { title, url } = Astro.props;
---
<a
class="button-base button-bg-gitea group inline-flex rounded-full gap-x-2"
href={url}
target="_blank"
rel="noopener noreferrer"
>
<div class="button-text-title flex relative items-center text-center">
<Icon
name="pajamas:gitea"
class="h-4 w-4 md:h-6 md:w-6"
/>
<span class="ml-2">
Continue to Gitea
</span>
<Icon
name="mdi:keyboard-arrow-right"
class="button-hover-arrow"
/>
</div>
</a>

View File

@@ -0,0 +1,28 @@
---
import Icon from '@components/ui/icons/icon.astro';
interface Props {
noArrow?: boolean;
}
const { noArrow } = Astro.props;
---
<button
class="button-base button-bg-blue group inline-flex rounded-lg gap-x-2"
id="back-button"
data-astro-prefetch
>
<div class="button-text-title flex relative items-center text-center">
{noArrow ? null : <Icon name="arrowLeft" />}
<span class="ml-2">
Go Back
</span>
</div>
</button>
<script>
document.getElementById('back-button')?.addEventListener('click', () => {
window.history.back();
});
</script>

View File

@@ -0,0 +1,25 @@
---
import { Icon } from 'astro-icon/components';
interface Props {
url?: string;
}
const { url } = Astro.props;
---
<a
class="button-base button-bg-teal group inline-flex rounded-lg gap-x-2"
href={url}
data-astro-prefetch
>
<div class="button-text-title flex relative items-center text-center">
<Icon
name="mdi:home-variant-outline"
class="card-hover-icon-scale h-3 w-3 md:h-5 md:w-5"
/>
<span class="ml-2">
Return Home
</span>
</div>
</a>

View File

@@ -0,0 +1,29 @@
---
import { Icon } from 'astro-icon/components';
interface Props {
title?: string;
url?: string;
noArrow?: boolean;
}
const { title, url, noArrow } = Astro.props;
---
<a
class="button-base button-bg-teal group inline-flex rounded-lg gap-x-2"
href={url}
data-astro-prefetch
>
<div class="button-text-title flex relative items-center text-center">
<span class="mr-2">
{title}
</span>
{noArrow ? null : (
<Icon
name="mdi:keyboard-arrow-right"
class="button-hover-arrow"
/>
)}
</div>
</a>

View File

@@ -0,0 +1,20 @@
---
interface Props {
title?: string;
url?: string;
}
const { title, url } = Astro.props;
---
<a
class="button-base button-bg-neutral group inline-flex rounded-lg gap-x-2"
href={url}
data-astro-prefetch
>
<div class="button-text-title flex relative items-center text-center">
<span>
{title}
</span>
</div>
</a>

View File

@@ -0,0 +1,51 @@
---
import Icon from '@components/ui/icons/icon.astro';
type SocialPlatform = {
name: string;
url: string;
svg: string;
};
interface Props {
pageTitle: string;
}
const { pageTitle } = Astro.props;
const socialPlatforms: SocialPlatform[] = [
{
name: 'Facebook',
url: `https://www.facebook.com/sharer/sharer.php?u=${Astro.url}`,
svg: 'facebook',
},
{
name: 'X',
url: `https://x.com/intent/tweet?url=${Astro.url}&text=${pageTitle}`,
svg: 'x',
},
{
name: 'LinkedIn',
url: `https://www.linkedin.com/sharing/share-offsite/?url=${Astro.url}`,
svg: 'linkedIn',
},
];
---
<div class="inline-flex items-center gap-x-2">
{
socialPlatforms.map((platform) => (
<a
class="button-base-hidden group inline-flex rounded-lg gap-x-2"
href={platform.url}
target="_blank"
rel="noopener noreferrer"
title={`Share on ${platform.name}`}
>
<div class="button-text-title-hidden flex relative items-center text-center">
<Icon name={platform.svg} class="h-5 w-5" />
</div>
</a>
))
}
</div>

View File

@@ -5,14 +5,14 @@
<button <button
id="theme-toggle" id="theme-toggle"
data-theme-toggle data-theme-toggle
class="group dark:hover:bg-steel/30 relative touch-manipulation overflow-hidden rounded-full p-1.5 transition-all duration-300 hover:bg-yellow-300/20 focus:outline-hidden sm:p-2" class="group dark:hover:bg-steel/30 hover:bg-yellow-300/20 transition-all duration-300 relative rounded-full p-1.5 sm:p-2 touch-manipulation"
aria-label="Toggle dark mode" aria-label="Toggle dark mode"
> >
<div class="relative z-10 flex h-5 w-5 items-center justify-center"> <div class="relative flex h-5 w-5 items-center justify-center">
<!-- Sun icon --> <!-- Sun icon -->
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="icon-light absolute h-5 w-5 scale-100 rotate-0 text-neutral-600 transition-all duration-500 dark:scale-0 dark:-rotate-90 dark:text-neutral-400" class="icon-light absolute h-5 w-5 text-neutral-600 dark:text-neutral-400 scale-100 dark:scale-0 rotate-0 dark:-rotate-90 transition-all duration-500"
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
@@ -29,7 +29,7 @@
<!-- Moon icon --> <!-- Moon icon -->
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="icon-dark absolute h-5 w-5 scale-0 rotate-90 text-neutral-600 transition-all duration-500 dark:scale-100 dark:rotate-0 dark:text-neutral-400" class="icon-dark absolute h-5 w-5 text-neutral-600 dark:text-neutral-400 scale-0 dark:scale-100 rotate-90 dark:rotate-0 transition-all duration-500"
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
@@ -43,25 +43,23 @@
</button> </button>
<script is:inline> <script is:inline>
// Use a function to persist theme when using SPA transitions const applyTheme = () => {
// https://docs.astro.build/en/guides/view-transitions/#script-re-execution const isDark =
function applyTheme() { localStorage.theme === 'dark' ||
localStorage.theme === 'dark' (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches);
? document.documentElement.classList.add('dark') document.documentElement.classList.toggle('dark', isDark);
: document.documentElement.classList.remove('dark'); };
}
document.addEventListener('astro:after-swap', applyTheme);
applyTheme(); applyTheme();
document.addEventListener('astro:after-swap', applyTheme);
</script> </script>
<script> <script>
// Use a function to handle theme toggle to ensure it can be called from anywhere
function setupThemeToggle() { function setupThemeToggle() {
const themeToggles = document.querySelectorAll('[data-theme-toggle]'); const themeToggles = document.querySelectorAll('[data-theme-toggle]');
// Create theme switch overlay element if it doesn't exist // Create theme switch overlay element
if (!document.querySelector('.theme-switch-overlay')) { if (!document.querySelector('.theme-switch-overlay')) {
const overlay = document.createElement('div'); const overlay = document.createElement('div');
overlay.className = 'theme-switch-overlay fixed inset-0 pointer-events-none z-50'; overlay.className = 'theme-switch-overlay fixed inset-0 pointer-events-none z-50';
@@ -70,9 +68,7 @@
document.body.appendChild(overlay); document.body.appendChild(overlay);
} }
// Toggle theme when any theme toggle button is clicked
themeToggles.forEach((toggle) => { themeToggles.forEach((toggle) => {
// Add event listeners for both click and touch events
['click', 'touchend'].forEach((eventType) => { ['click', 'touchend'].forEach((eventType) => {
toggle.addEventListener( toggle.addEventListener(
eventType, eventType,
@@ -92,14 +88,10 @@
y = e.clientY - rect.top; y = e.clientY - rect.top;
} }
// Set the position variables for the radial gradient
document.documentElement.style.setProperty('--x', `${x}px`); document.documentElement.style.setProperty('--x', `${x}px`);
document.documentElement.style.setProperty('--y', `${y}px`); document.documentElement.style.setProperty('--y', `${y}px`);
// Get the overlay element
const overlay = document.querySelector('.theme-switch-overlay'); const overlay = document.querySelector('.theme-switch-overlay');
// Determine the new theme
const isDark = document.documentElement.classList.contains('dark'); const isDark = document.documentElement.classList.contains('dark');
const newTheme = isDark ? 'light' : 'dark'; const newTheme = isDark ? 'light' : 'dark';
@@ -110,7 +102,6 @@
overlay.style.opacity = '1'; overlay.style.opacity = '1';
} }
// Add transition class
document.documentElement.classList.add('theme-switching'); document.documentElement.classList.add('theme-switching');
// Force a reflow to ensure all elements update // Force a reflow to ensure all elements update
@@ -124,10 +115,7 @@
document.documentElement.classList.add('dark'); document.documentElement.classList.add('dark');
} }
// Store the preference
localStorage.setItem('theme', newTheme); localStorage.setItem('theme', newTheme);
// Dispatch a custom event for other components to react to
document.dispatchEvent( document.dispatchEvent(
new CustomEvent('themeChanged', { new CustomEvent('themeChanged', {
detail: { isDark: newTheme === 'dark' }, detail: { isDark: newTheme === 'dark' },
@@ -137,13 +125,10 @@
// Force another reflow to ensure all elements update // Force another reflow to ensure all elements update
document.body.offsetHeight; document.body.offsetHeight;
// Hide overlay after theme has changed
setTimeout(() => { setTimeout(() => {
if (overlay) { if (overlay) {
overlay.style.opacity = '0'; overlay.style.opacity = '0';
} }
// Remove transition class after animation completes
document.documentElement.classList.remove('theme-switching'); document.documentElement.classList.remove('theme-switching');
}, 300); }, 300);
}, 50); }, 50);
@@ -151,25 +136,6 @@
{ passive: false } { passive: false }
); );
}); });
// Add touch feedback
toggle.addEventListener(
'touchstart',
() => {
toggle.classList.add('active-touch');
},
{ passive: true }
);
toggle.addEventListener(
'touchend',
() => {
setTimeout(() => {
toggle.classList.remove('active-touch');
}, 150);
},
{ passive: true }
);
}); });
} }
@@ -201,61 +167,32 @@
</script> </script>
<style> <style>
/* Smooth transition for the entire page when theme changes */
:global(body) {
transition:
background-color 0.5s ease,
color 0.5s ease;
}
/* Theme transition overlay */
:global(.theme-switch-overlay) {
position: fixed;
inset: 0;
z-index: 9999;
pointer-events: none;
transition: opacity 0.3s ease-out;
}
/* Ensure theme transitions apply to all elements */
:global(.theme-switching *) {
transition-duration: 0.5s !important;
transition-property: background-color, border-color, color, fill, stroke !important;
}
/* Subtle hover animation */ /* Subtle hover animation */
#theme-toggle { #theme-toggle {
transform: translateY(0); transform: translateY(0);
box-shadow: 0 0 0 rgba(0, 0, 0, 0); box-shadow: 0 0 0 rgba(0, 0, 0, 0);
-webkit-tap-highlight-color: transparent; /* Remove default mobile tap highlight */ -webkit-tap-highlight-color: transparent;
min-height: 32px; /* Ensure minimum touch target size */ min-height: 32px;
min-width: 32px; /* Ensure minimum touch target size */ min-width: 32px;
} }
/* Only apply hover effects on non-touch devices */
@media (hover: hover) { @media (hover: hover) {
#theme-toggle:hover { #theme-toggle:hover {
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
} }
#theme-toggle:hover .icon-light:not(.dark .icon-light) { :global(:root:not(.dark)) #theme-toggle:hover .icon-light {
filter: drop-shadow-sm(0 0 2px rgba(251, 191, 36, 0.6)); filter: drop-shadow(0 0 2px rgba(251, 191, 36, 0.6));
transform: scale(1.1) rotate(15deg); transform: scale(1.1) rotate(15deg);
} }
#theme-toggle:hover .icon-dark:not(:not(.dark) .icon-dark) { :global(:root.dark) #theme-toggle:hover .icon-dark {
filter: drop-shadow-sm(0 0 2px rgba(129, 140, 248, 0.6)); filter: drop-shadow(0 0 2px rgba(129, 140, 248, 0.6));
transform: scale(1.1) rotate(-15deg); transform: scale(1.1) rotate(-15deg);
} }
} }
/* Touch feedback */
#theme-toggle.active-touch {
transform: scale(0.95);
transition: transform 0.15s ease-in-out;
}
/* Optimize animations for mobile */ /* Optimize animations for mobile */
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
.icon-light, .icon-light,

View File

@@ -55,7 +55,7 @@ const visitClass = visitSource ? 'card-hover-text-gitea' : 'card-hover-text-titl
)} )}
<div class="ml-6 flex"> <div class="ml-6 flex">
<div class="relative inline-block"> <div class="relative inline-block">
<div class={`card-text-title ${visitClass} flex relative mx-auto min-h-11 items-center font-semibold text-md sm:mx-0 sm:mt-4`}> <div class={`card-text-title ${visitClass} flex relative items-center mx-auto min-h-11 font-semibold text-md sm:mx-0 sm:mt-4`}>
{visitSource && <Icon name="pajamas:gitea" />} {visitSource && <Icon name="pajamas:gitea" />}
<span class="relative inline-block overflow-hidden ml-2"> <span class="relative inline-block overflow-hidden ml-2">
{visitText} {visitText}

View File

@@ -4,7 +4,7 @@ import { readItems } from '@directus/sdk';
import type { Application } from '@lib/directusTypes'; import type { Application } from '@lib/directusTypes';
import directus from '@lib/directus'; import directus from '@lib/directus';
import HighlightsCard from '@components/ui/cards/HighlightsCard.astro'; import HighlightsCard from '@components/cards/HighlightsCard.astro';
const applications = ((await directus.request( const applications = ((await directus.request(
readItems('site_applications' as any, { readItems('site_applications' as any, {

View File

@@ -5,7 +5,7 @@ import type { Education, Certificate} from '@lib/directusTypes';
import directus from '@lib/directus'; import directus from '@lib/directus';
import { getDirectusImageURL } from '@lib/directusFunctions'; import { getDirectusImageURL } from '@lib/directusFunctions';
import EducationCard from '@components/ui/cards/EducationCard.astro'; import EducationCard from '@components/cards/EducationCard.astro';
const educations = ((await directus.request( const educations = ((await directus.request(
readItems('site_education' as any, { readItems('site_education' as any, {
@@ -27,7 +27,7 @@ const certificates = ((await directus.request(
Education Education
</h3> </h3>
<div class="mx-8"> <div class="mx-8">
<h4 class="smooth-reveal card-text-header-minor pt-5 "> <h4 class="smooth-reveal card-text-header-minor pt-5">
College College
</h4> </h4>
<div class="grid md:grid-cols-2 sm:grid-cols-1 gap-4 py-3"> <div class="grid md:grid-cols-2 sm:grid-cols-1 gap-4 py-3">

View File

@@ -0,0 +1,159 @@
---
import { Icon } from 'astro-icon/components';
import { readItems } from '@directus/sdk';
import type { Experience } from '@lib/directusTypes';
import directus from '@lib/directus';
const experiences = ((await directus.request(
readItems('site_experience'as any, {
fields: ['*'],
sort: ['-endDate'],
})
)) as unknown) as Experience[];
---
<section class:list={['flex flex-col gap-8', Astro.props.className]}>
<h3 class="smooth-reveal card-text-header flex relative items-center w-full gap-3 pb-10">
Experience
</h3>
<ul class="ml-8 w-full flex flex-col">
{experiences.map((experience: Experience) => {
const startYear = new Date(experience.startDate).getFullYear();
const endYear = experience.endDate != null ? new Date(experience.endDate).getFullYear() : 'Present';
return (
<li class="relative">
<div class="smooth-reveal group relative grid sm:grid-cols-18 sm:gap-8 md:gap-6 pb-16">
<header class="relative sm:col-span-3 text-header font-semibold text-lg mt-1">
<time datetime={experience.startDate} data-title={experience.startDate}>
{startYear}
</time>
{' '}-{' '}
<time datetime={experience.endDate} data-title={experience.endDate}>
{endYear}
</time>
</header>
<div class="relative flex flex-col sm:col-span-12 pb-6">
<div class="absolute bg-stone-400 -translate-x-[1.71rem] rounded-full h-2 w-2 mt-4"/>
<h3>
<div
class="inline-flex items-center text-2xl leading-tight font-semibold"
aria-label="{position} - {company}"
>
<span class="text-header">
{experience.position} <span>@</span>
{experience.url ? (
<a
class="hover:text-main"
href={experience.url}
title={`Ver ${experience.name}`}
target="_blank"
>
{experience.name}
</a>
) : (
<span>{experience.name}</span>
)}
</span>
</div>
</h3>
{(experience.location || experience.location_type) && (
<div class="text-secondary text-sm">
{experience.location} {experience.location && experience.location_type && '-'} {experience.location_type}
</div>
)}
<div class="text-md mt-4 flex flex-col gap-4" x-data="{ expanded: false }">
{experience.summary && (
<div class="flex flex-col gap-1">
<h4 class="text-header font-semibold">
Summary:
</h4>
<ul class="flex flex-col text-primary list-disc gap-2 [&>li]:ml-4">
<li class="marker:text-main">
{experience.summary}
</li>
</ul>
</div>
)}
{(experience.responsibilities || experience.achievements) && (
<div class="relative flex flex-col gap-4 max-sm:h-auto! md:after:absolute md:after:bottom-0 md:after:h-12 md:after:w-full md:after:bg-gradient-to-t md:after:from-neutral-200 dark:md:after:from-stone-700 md:after:content-[''] " :class="expanded ? 'after:hidden' : ''" x-show="expanded" x-collapse.min.50px>
{experience.responsibilities && (
<div class="flex flex-col gap-1">
<h4 class="text-header font-semibold">
Responsibilities:
</h4>
<ul class="flex flex-col text-primary list-disc gap-2 [&>li]:ml-4">
{experience.responsibilities.map(responsibility => (
<li class="marker:text-main">
{responsibility}
</li>
))}
</ul>
</div>
)}
{experience.achievements && (
<div class="flex flex-col gap-1">
<h4 class="text-header font-semibold">
Achievements:
</h4>
<ul class="flex flex-col text-primary list-disc gap-2 [&>li]:ml-4">
{experience.achievements.map(achievement => (
<li class="marker:text-main">
{achievement}
</li>
))}
</ul>
</div>
)}
</div>
<button @click="expanded = ! expanded" class="group/more flex items-center justify-center text-primary hover:text-primary-hover text-xs underline transition-all gap-1.5 w-fit cursor-pointer">
<span x-text="expanded ? 'Show less' : 'Show more'">
Show more
</span>
<svg
class="group-hover/more:translate-y-0.5 ease-out duration-300 h-4 w-4"
:class="{ 'rotate-180': expanded }"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="6 9 12 15 18 9" />
</svg>
</button>
<ul
class="flex print:hidden flex-wrap gap-2"
aria-label="Technologies used"
>
{experience.skills && experience.skills.map(skill => {
const iconName = skill.toLowerCase();
const skillName = skill.split(':')[1].replace(/^language-/, '').replace(/-/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase());
return (
<li class="flex items-center bg-steel/20 dark:bg-bermuda/20 text-neutral-800 dark:text-neutral-200 text-xs rounded-md border border-solid border-steel/20 dark:border-bermuda/20 gap-1 px-2 py-0.5">
<Icon name={`${iconName}`} class="h-4 w-4" /> <span>{skillName}</span>
</li>
)
})}
</ul>
)}
</div>
</div>
</div>
</li>
);
})}
</ul>
</section>
<!-- Alpine Plugins -->
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/collapse@3.x.x/dist/cdn.min.js"></script>
<!-- Alpine Core -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>

View File

@@ -2,15 +2,13 @@
import { readSingleton } from '@directus/sdk'; import { readSingleton } from '@directus/sdk';
import directus from '@lib/directus'; import directus from '@lib/directus';
import FeaturesCard from '@components/ui/cards/FeaturesCard.astro'; import FeaturesCard from '@components/cards/FeaturesCard.astro';
const global = await directus.request(readSingleton('site_global')); const global = await directus.request(readSingleton('site_global'));
--- ---
<section class="mx-auto mb-20 max-w-340 px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"> <section class="mx-auto mb-20 max-w-340 px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full">
<div <div class="flex flex-col items-center justify-center gap-y-2 sm:flex-row sm:gap-x-12 sm:gap-y-0 lg:gap-x-24">
class="flex flex-col items-center justify-center gap-y-2 sm:flex-row sm:gap-x-12 sm:gap-y-0 lg:gap-x-24"
>
<div class="max-w-5xl sm:px-6 lg:px-8"> <div class="max-w-5xl sm:px-6 lg:px-8">
<div class="flex flex-wrap gap-6 sm:grid-cols-2 sm:gap-6 lg:grid-cols-3 justify-center"> <div class="flex flex-wrap gap-6 sm:grid-cols-2 sm:gap-6 lg:grid-cols-3 justify-center">
<FeaturesCard <FeaturesCard

View File

@@ -1,22 +1,20 @@
--- ---
import GiteaBtn from '@components/ui/buttons/GiteaBtn.astro'; import GiteaButton from '@components/buttons/GiteaButton.astro';
const { title, subTitle, url } = Astro.props;
const btnTitle = 'Continue to Gitea';
interface Props { interface Props {
title: string; title: string;
subTitle?: string; subTitle?: string;
url?: string; url?: string;
} }
const { title, subTitle, url } = Astro.props;
--- ---
<section class="lg:px- relative mx-auto mb-20 max-w-340 px-4 pt-30 pb-30 sm:px-6"> <section class="lg:px- relative mx-auto mb-20 max-w-340 px-4 pt-30 pb-30 sm:px-6">
<div <!-- Animated shapes -->
class="smooth-reveal absolute top-[55%] left-0 scale-90 md:top-[20%] xl:top-[25%] xl:left-[10%]" <div class="smooth-reveal absolute top-[55%] left-0 scale-90 md:top-[20%] xl:top-[25%] xl:left-[10%]">
>
<svg <svg
class="animate-hover animate-hover-1" class="gitea-animate-hover gitea-animate-hover-1"
width="64" width="64"
height="64" height="64"
fill="none" fill="none"
@@ -46,7 +44,7 @@ interface Props {
</div> </div>
<div class="smooth-reveal absolute top-0 left-[85%] scale-75"> <div class="smooth-reveal absolute top-0 left-[85%] scale-75">
<svg <svg
class="animate-hover animate-hover-2" class="gitea-animate-hover gitea-animate-hover-2"
width="64" width="64"
height="64" height="64"
fill="none" fill="none"
@@ -80,11 +78,9 @@ interface Props {
d="M10.5 19H9M15 19h-1.5"></path> d="M10.5 19H9M15 19h-1.5"></path>
</svg> </svg>
</div> </div>
<div <div class="smooth-reveal absolute bottom-[5%] left-[60%] scale-[.6] xl:bottom-[15%] xl:left-[35%]">
class="smooth-reveal absolute bottom-[5%] left-[60%] scale-[.6] xl:bottom-[15%] xl:left-[35%]"
>
<svg <svg
class="animate-hover animate-hover-3" class="gitea-animate-hover gitea-animate-hover-3"
width="64" width="64"
height="64" height="64"
fill="none" fill="none"
@@ -106,59 +102,54 @@ interface Props {
></path> ></path>
</svg> </svg>
</div> </div>
<!-- Hero Section Heading --> <!-- Heading -->
<div class="smooth-reveal-2 mx-auto mt-5 max-w-xl text-center"> <div class="smooth-reveal-2 mx-auto mt-5 max-w-xl text-center">
<h2 <h1 class="card-text-header block">
class="block text-4xl leading-tight font-bold tracking-tight text-balance text-neutral-800 md:text-5xl lg:text-5xl dark:text-neutral-200"
>
{title} {title}
</h2> </h1>
</div> </div>
<!-- Hero Section Sub-heading --> <!-- Sub-heading -->
<div class="smooth-reveal-2 mx-auto mt-5 max-w-3xl text-center"> <div class="smooth-reveal-2 mx-auto mt-5 max-w-3xl text-center">
{ {subTitle && (
subTitle && ( <p class="card-text-header-description">
<p class="text-lg text-pretty text-neutral-600 dark:text-neutral-400">{subTitle}</p> {subTitle}
) </p>
} )}
</div> </div>
<!-- Github Button --> <!-- Gitea Button -->
{ {url && (
url && ( <div class="smooth-reveal-2 flex justify-center mt-8 gap-3">
<div class="smooth-reveal-2 mt-8 flex justify-center gap-3"> <GiteaButton url={url}/>
<GiteaBtn url={url} title={btnTitle} /> </div>
</div> )}
)
}
</section> </section>
<style> <style>
@keyframes animate-hover { @keyframes gitea-animate-hover {
from { from {
transform: translateY(15px); transform: translateY(15px);
} }
to { to {
transform: translateY(-15px); transform: translateY(-15px);
} }
} }
.animate-hover { .gitea-animate-hover {
animation: animate-hover ease-in-out; animation: gitea-animate-hover ease-in-out;
animation-iteration-count: infinite; animation-iteration-count: infinite;
animation-direction: alternate; animation-direction: alternate;
} }
.animate-hover-1 { .gitea-animate-hover-1 {
animation-duration: 5s; animation-duration: 5s;
} }
.animate-hover-2 { .gitea-animate-hover-2 {
animation-duration: 5.5s; animation-duration: 5.5s;
} }
.animate-hover-3 { .gitea-animate-hover-3 {
animation-duration: 6s; animation-duration: 6s;
} }
</style> </style>

View File

@@ -0,0 +1,31 @@
---
import GoLinkPrimaryButton from '@components/buttons/GoLinkPrimaryButton.astro';
interface Props {
title: string;
subTitle: string;
btnExists?: boolean;
btnTitle?: string;
btnURL?: string;
}
const { title, subTitle, btnExists, btnTitle, btnURL } = Astro.props;
---
<section class="mx-auto mt-10 px-4 sm:px-6 lg:px-8 lg:pt-10 2xl:max-w-full">
<div class="flex-wrap md:flex md:items-center md:justify-between">
<div class="w-full md:w-auto">
<h1 class="smooth-reveal card-text-header block lg:text-6xl">
{title}
</h1>
<p class="smooth-reveal card-text-header-description mt-4">
{subTitle}
</p>
{btnExists ? (
<div class="smooth-reveal mt-4 md:mt-8">
<GoLinkPrimaryButton title={btnTitle} url={btnURL}/>
</div>
) : null}
</div>
</div>
</section>

View File

@@ -0,0 +1,56 @@
---
import GoLinkPrimaryButton from '@components/buttons/GoLinkPrimaryButton.astro';
import GoLinkSecondaryButton from '@components/buttons/GoLinkSecondaryButton.astro';
import Image from '@components/ui/images/Image.astro';
interface Props {
title: string;
subTitle?: string;
primaryBtn?: string;
primaryBtnURL?: string;
secondaryBtn?: string;
secondaryBtnURL?: string;
src?: any;
alt?: string;
rounded?: boolean;
}
const { title, subTitle, primaryBtn, primaryBtnURL, secondaryBtn, secondaryBtnURL, src, alt } = Astro.props;
const roundedClasses = Astro.props.rounded ? "rounded-xl" : null;
---
<section class="mx-auto grid max-w-340 gap-4 px-4 py-14 sm:px-6 md:grid-cols-2 md:items-center md:gap-8 lg:px-8 2xl:max-w-full">
<div>
<h1 class="smooth-reveal card-text-header block lg:text-7xl">
<Fragment set:html={title} />
</h1>
{subTitle && (
<p class="smooth-reveal card-text-header-description lg:w-4/5 mt-6">
{subTitle}
</p>
)}
<div class="smooth-reveal grid sm:inline-flex mt-7 w-full gap-3">
{primaryBtn && <GoLinkPrimaryButton title={primaryBtn} url={primaryBtnURL} />}
{secondaryBtn && <GoLinkSecondaryButton title={secondaryBtn} url={secondaryBtnURL} />}
</div>
</div>
<div class="smooth-reveal-fade md:block w-full hidden">
<div class="flex justify-center w-full top-12 md:ml-4 overflow-hidden">
{src && alt && (
<Image
src={src}
alt={alt}
class={`h-full w-105 scale-100 object-cover object-center ${roundedClasses}`}
draggable="false"
loading="eager"
format="webp"
quality="low"
widths={[840]}
disableBlur={true}
/>
)}
</div>
</div>
</section>

View File

@@ -1,8 +1,9 @@
--- ---
import { readItems } from '@directus/sdk'; import { readItems } from '@directus/sdk';
import directus from '@lib/directus';
import type { Post } from '@lib/directusTypes'; import type { Post } from '@lib/directusTypes';
import directus from '@lib/directus';
import BlogCard from '@components/blog/BlogCard.astro'; import BlogCard from '@components/blog/BlogCard.astro';
const posts = await directus.request( const posts = await directus.request(
@@ -20,11 +21,14 @@ const recentPosts = posts
<section class="mx-auto mb-20 max-w-340 px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"> <section class="mx-auto mb-20 max-w-340 px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full">
<div class="mx-auto mb-10 max-w-2xl text-center lg:mb-14"> <div class="mx-auto mb-10 max-w-2xl text-center lg:mb-14">
<h1 <h1 class="smooth-reveal card-text-header block">
class="smooth-reveal block text-4xl font-bold text-neutral-800 md:text-5xl md:leading-tight lg:text-5xl dark:text-neutral-200"
>
Latest Posts Latest Posts
</h1> </h1>
<div class="smooth-reveal mx-auto mt-5 max-w-3xl text-center">
<span class="card-text-header-description">
Checkout my most recent thoughts here
</span>
</div>
</div> </div>
<div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-3"> <div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{recentPosts.map((b) => <BlogCard post={b} />)} {recentPosts.map((b) => <BlogCard post={b} />)}

View File

@@ -4,7 +4,7 @@ import { readItems } from '@directus/sdk';
import type { Project } from '@lib/directusTypes'; import type { Project } from '@lib/directusTypes';
import directus from '@lib/directus'; import directus from '@lib/directus';
import HighlightsCard from '@components/ui/cards/HighlightsCard.astro'; import HighlightsCard from '@components/cards/HighlightsCard.astro';
const projects = ((await directus.request( const projects = ((await directus.request(
readItems('site_projects' as any, { readItems('site_projects' as any, {

View File

@@ -6,86 +6,63 @@ import type { Skill } from '@lib/directusTypes';
import directus from '@lib/directus'; import directus from '@lib/directus';
const skills = await directus.request( const skills = ((await directus.request(
readItems('site_skills', { readItems('site_skills' as any, {
fields: ['*'], fields: ['*'],
sort: ['-date_created'], sort: ['-date_created'],
}) })
); )) as unknown) as Skill[];
const baseClasses = 'mx-2 min-w-[220px] sm:mx-4 sm:min-w-[280px]';
const borderClasses =
'border border-neutral-100 hover:border-neutral-200 dark:border-stone-500/20 dark:hover:border-neutral-800';
const bgColorClasses = 'bg-neutral-100/80 dark:bg-neutral-800/60 dark:hover:bg-neutral-800/90';
const hoverClasses = 'hover:-translate-y-2 hover:scale-105 ';
const shadowClasses = 'shadow-xs hover:shadow-lg';
--- ---
<section class:list={['flex flex-col gap-4', Astro.props.className]}> <section class:list={['flex flex-col gap-4', Astro.props.className]}>
<h3 <h3 class="smooth-reveal card-text-header flex relative items-center w-full gap-3 pb-5">
class="relative flex w-full items-center gap-3 pb-4 text-5xl text-neutral-800 dark:text-neutral-200"
>
Skills Skills
</h3> </h3>
<div class=""> <div class="">
<div class="tech-stack-slider relative overflow-hidden py-4 sm:py-8"> <div class="tech-stack-slider relative overflow-hidden py-4 sm:py-8">
<!-- Main slider container --> <!-- Main slider container -->
<div class="slider-track animate-slide flex"> <div class="slider-track animate-slide flex">
{ {[...skills, ...skills, ...skills].map((skill: Skill) => {
[...skills, ...skills, ...skills].map((skill: Skill) => { return (
return ( <div class="skill-card card-base transform hover:-translate-y-2 hover:scale-105 transition-all duration-300 mx-2 min-w-55 sm:mx-4 sm:min-w-70">
<div <div class="p-4 sm:p-6">
class={`skill-card transform rounded-xl transition-all duration-300 ${baseClasses} ${borderClasses} ${bgColorClasses} ${hoverClasses} ${shadowClasses}`} <div class="flex items-center justify-between mb-4 sm:mb-6">
> <div class="flex items-center gap-2 sm:gap-4">
<div class="p-4 sm:p-6"> <div class="flex items-center justify-center rounded-lg text-primary">
<div class="mb-4 flex items-center justify-between sm:mb-6"> <Icon name={skill.icon} class="h-8 w-8 sm:h-12 sm:w-12" />
<div class="flex items-center gap-2 sm:gap-4">
<div class="flex transform items-center justify-center rounded-lg text-neutral-800 transition-transform group-hover:rotate-12 dark:text-neutral-200">
<Icon name={skill.icon} class="h-8 w-8 sm:h-12 sm:w-12" />
</div>
<h3 class="text-base font-semibold text-neutral-900 sm:text-xl dark:text-neutral-100">
{skill.title}
</h3>
</div>
<span class="rounded-full bg-neutral-200 px-2 py-0.5 font-mono text-xs text-neutral-700 sm:px-2.5 sm:py-1 sm:text-sm dark:bg-neutral-800 dark:text-neutral-300">
{skill.level}%
</span>
</div>
<div class="relative h-1.5 w-full overflow-hidden rounded-full bg-stone-500/20 sm:h-2 dark:bg-stone-500/20">
<div
class="progress-bar-animate from-steel via-bermuda to-steel absolute top-0 left-0 h-full rounded-full bg-linear-to-r transition-all duration-1000"
style={`width: ${skill.level}%`}
/>
</div>
<div class="mt-1 flex justify-between font-mono text-[10px] text-neutral-600 sm:mt-2 sm:text-xs dark:text-neutral-400">
<span>Beginner</span>
<span>Advanced</span>
</div>
</div> </div>
<h3 class="text-neutral-900 dark:text-neutral-100 text-base font-semibold sm:text-xl">
{skill.title}
</h3>
</div> </div>
); <span class=" bg-neutral-200 dark:bg-neutral-800 text-neutral-700 dark:text-neutral-300 font-mono text-xs sm:text-sm rounded-full px-2 sm:px-2.5 py-0.5 sm:py-1">
}) {skill.level}%
} </span>
</div>
<div class="relative bg-stone-500/20 dark:bg-stone-500/20 rounded-full h-1.5 sm:h-2 w-full overflow-hidden">
<div
class="progress-bar-animate bg-linear-to-r from-steel via-bermuda to-steel absolute top-0 left-0 h-full rounded-full transition-all duration-1000"
style={`width: ${skill.level}%`}
/>
</div>
<div class="flex justify-between text-secondary font-mono text-[10px] mt-1 sm:mt-2 sm:text-xs">
<span>Beginner</span>
<span>Advanced</span>
</div>
</div>
</div>
);
})}
</div> </div>
<!-- Gradient overlays --> <!-- Gradient overlays -->
<div <div class="bg-linear-to-r from-neutral-200 to-transparent dark:from-stone-700 absolute top-0 bottom-0 left-0 z-10 w-12 sm:w-24"/>
class="absolute top-0 bottom-0 left-0 z-10 w-12 bg-linear-to-r from-neutral-200 to-transparent sm:w-24 dark:from-stone-700" <div class="bg-linear-to-l from-neutral-200 to-transparent dark:from-stone-700 absolute top-0 bottom-0 right-0 z-10 w-12 sm:w-24"/>
>
</div>
<div
class="absolute top-0 right-0 bottom-0 z-10 w-12 bg-linear-to-l from-neutral-200 to-transparent sm:w-24 dark:from-stone-700"
>
</div>
</div> </div>
</div> </div>
</section> </section>
<script> <script>
document.addEventListener('astro:page-load', () => { document.addEventListener('astro:page-load', () => {
// Create infinite scrolling effect
function setupInfiniteScroll() { function setupInfiniteScroll() {
const cards = document.querySelectorAll('.skill-card'); const cards = document.querySelectorAll('.skill-card');
if (!cards.length) return; if (!cards.length) return;
@@ -93,7 +70,6 @@ const shadowClasses = 'shadow-xs hover:shadow-lg';
setupInfiniteScroll(); setupInfiniteScroll();
// Add hover effects to cards - only on non-touch devices
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0; const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
const cards = document.querySelectorAll('.skill-card'); const cards = document.querySelectorAll('.skill-card');
@@ -144,7 +120,7 @@ const shadowClasses = 'shadow-xs hover:shadow-lg';
</script> </script>
<style> <style>
/* Tech Stack Slider */ /* Specific css to enable sliding effect */
.slider-track { .slider-track {
width: fit-content; width: fit-content;
animation: scroll 40s linear infinite; animation: scroll 40s linear infinite;
@@ -155,7 +131,7 @@ const shadowClasses = 'shadow-xs hover:shadow-lg';
transform: translateX(0); transform: translateX(0);
} }
100% { 100% {
transform: translateX(calc(-220px * 6 - 16px * 6)); /* Card width + margin for mobile */ transform: translateX(calc(-220px * 6 - 16px * 6));
} }
} }
@@ -169,7 +145,7 @@ const shadowClasses = 'shadow-xs hover:shadow-lg';
transform: translateX(0); transform: translateX(0);
} }
100% { 100% {
transform: translateX(calc(-280px * 6 - 32px * 6)); /* Card width + margin for desktop */ transform: translateX(calc(-280px * 6 - 32px * 6));
} }
} }
} }

View File

@@ -1,5 +1,5 @@
--- ---
import WeatherCard from '@components/ui/cards/WeatherCard.astro'; import WeatherCard from '@components/cards/WeatherCard.astro';
import { getFiveDayForecast } from '@support/weather'; import { getFiveDayForecast } from '@support/weather';
const { latitude = "44.95", longitude = "-93.09", cityName = "St. Paul, Minnesota", timezone = "America/Chicago" } = Astro.props; const { latitude = "44.95", longitude = "-93.09", cityName = "St. Paul, Minnesota", timezone = "America/Chicago" } = Astro.props;
@@ -8,7 +8,7 @@ const { forecastDays, error } = await getFiveDayForecast(latitude, longitude, ti
<section class="mx-auto mb-20 max-w-340 px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"> <section class="mx-auto mb-20 max-w-340 px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full">
<div class="mx-auto mb-10 max-w-2xl text-center lg:mb-14"> <div class="mx-auto mb-10 max-w-2xl text-center lg:mb-14">
<h1 class="card-text-header smooth-reveal block font-bold md:leading-tight"> <h1 class="smooth-reveal card-text-header block">
Weather in my Area Weather in my Area
</h1> </h1>
<div class="smooth-reveal mx-auto mt-5 max-w-3xl text-center"> <div class="smooth-reveal mx-auto mt-5 max-w-3xl text-center">
@@ -17,7 +17,6 @@ const { forecastDays, error } = await getFiveDayForecast(latitude, longitude, ti
</span> </span>
</div> </div>
</div> </div>
{error ? ( {error ? (
<div class="card-base p-10 text-accent text-center"> <div class="card-base p-10 text-accent text-center">
{error} {error}

View File

@@ -1,32 +0,0 @@
---
import { Icon } from 'astro-icon/components';
const { title, url } = Astro.props;
interface Props {
title?: string;
url?: string;
}
const baseClasses =
'group group-hover inline-flex items-center justify-center gap-x-3 rounded-full px-4 py-3 text-center text-sm font-medium text-neutral-200';
const borderClasses = 'border border-transparent';
const bgColorClasses =
'bg-gitea-primary hover:bg-gitea-secondary dark:bg-gitea-secondary dark:hover:bg-gitea-primary';
const shadowClasses = 'shadow-sm';
const fontSizeClasses = '2xl:text-base';
---
<a
class={`${baseClasses} ${borderClasses} ${bgColorClasses} ${shadowClasses} ${fontSizeClasses} `}
href={url}
target="_blank"
rel="noopener noreferrer"
>
<Icon name="pajamas:gitea" class="h-4 w-4 md:h-6 md:w-6" />
{title}
<Icon
name="mdi:keyboard-arrow-right"
class="h-3 w-3 translate-y-0.25 transition duration-300 group-hover:translate-x-1 md:h-5 md:w-5"
/>
</a>

View File

@@ -1,35 +0,0 @@
---
import Icon from '@components/ui/icons/icon.astro';
const { title, noArrow } = Astro.props;
interface Props {
title?: string;
url?: string;
noArrow?: boolean;
addHome?: boolean;
}
const baseClasses =
'group inline-flex items-center justify-center gap-x-2 rounded-lg px-4 py-3 text-sm font-bold text-neutral-50 ring-neutral-500 transition duration-300 focus-visible:ring outline-none';
const borderClasses = 'border border-transparent';
const bgColorClasses = 'bg-steel hover:bg-sky-800 active:bg-orange-500 dark:focus:outline-none';
const disableClasses = 'disabled:pointer-events-none disabled:opacity-50';
const fontSizeClasses = '2xl:text-base';
const ringClasses = 'dark:ring-neutral-200';
---
<button
class={`${baseClasses} ${borderClasses} ${bgColorClasses} ${disableClasses} ${fontSizeClasses} ${ringClasses}`}
id="back-button"
data-astro-prefetch
>
{noArrow ? null : <Icon name="arrowLeft" />}
{title}
</button>
<script>
document.getElementById('back-button')?.addEventListener('click', () => {
window.history.back();
});
</script>

View File

@@ -1,45 +0,0 @@
---
import { Icon } from 'astro-icon/components';
const { title, url, noArrow, addHome, addClass } = Astro.props;
interface Props {
title?: string;
url?: string;
noArrow?: boolean;
addHome?: boolean;
addClass?: string;
}
const baseClasses =
'group inline-flex items-center justify-center gap-x-2 rounded-lg px-4 py-3 text-sm font-bold text-neutral-100 transition duration-300 ';
const borderClasses = 'border border-transparent';
const bgColorClasses = 'bg-bermuda hover:bg-turquoise dark:bg-turquoise dark:hover:bg-bermuda';
const disableClasses = 'disabled:pointer-events-none disabled:opacity-50';
const fontSizeClasses = '2xl:text-base';
const ringClasses = 'dark:ring-neutral-200';
---
<a
class={`${baseClasses} ${borderClasses} ${bgColorClasses} ${disableClasses} ${fontSizeClasses} ${ringClasses} ${addClass}`}
href={url}
data-astro-prefetch
>
{
addHome ? (
<Icon
name="mdi:home-variant-outline"
class="h-3 w-3 translate-y-0.25 transition duration-300 group-hover:translate-x-1 md:h-5 md:w-5"
/>
) : null
}
{title}
{
noArrow ? null : (
<Icon
name="mdi:keyboard-arrow-right"
class="h-3 w-3 translate-y-0.25 transition duration-300 group-hover:translate-x-1 md:h-5 md:w-5"
/>
)
}
</a>

View File

@@ -1,26 +0,0 @@
---
const { title, url } = Astro.props;
interface Props {
title?: string;
url?: string;
}
const baseClasses =
'inline-flex items-center justify-center gap-x-2 rounded-lg px-4 py-3 text-center text-sm font-medium text-neutral-600 shadow-sm outline-none ring-neutral-500 focus-visible:ring transition duration-300';
const borderClasses = 'border border-neutral-200';
const bgColorClasses = 'bg-neutral-300';
const hoverClasses = 'hover:bg-neutral-400/50 hover:text-neutral-600 active:text-neutral-700';
const disableClasses = 'disabled:pointer-events-none disabled:opacity-50';
const fontSizeClasses = '2xl:text-base';
const ringClasses = 'ring-neutral-500';
const darkClasses =
'dark:border-neutral-700 dark:bg-neutral-700 dark:text-neutral-300 dark:ring-neutral-200 dark:hover:bg-neutral-600 dark:focus:outline-none';
---
<a
class={`${baseClasses} ${borderClasses} ${bgColorClasses} ${hoverClasses} ${disableClasses} ${fontSizeClasses} ${ringClasses} ${darkClasses}`}
href={url}
>
{title}
</a>

View File

@@ -1,150 +0,0 @@
---
import Icon from '@components/ui/icons/icon.astro';
const { pageTitle, title = 'Share' } = Astro.props;
interface Props {
pageTitle: string;
title?: string;
}
type SocialPlatform = {
name: string;
url: string;
svg: string;
};
const socialPlatforms: SocialPlatform[] = [
{
name: 'Facebook',
url: `https://www.facebook.com/share.php?u=${Astro.url}&title=${pageTitle}`,
svg: 'facebook',
},
{
name: 'X',
url: `https://twitter.com/home/?status=${pageTitle}${Astro.url}`,
svg: 'x',
},
{
name: 'LinkedIn',
url: `https://www.linkedin.com/shareArticle?mini=true&url=${Astro.url}&title=${pageTitle}`,
svg: 'linkedIn',
},
];
---
<div class="hs-dropdown relative inline-flex [--auto-close:inside] [--placement:top-left]">
<button
id="hs-dropup"
type="button"
class="hs-dropdown-toggle inline-flex items-center gap-x-2 rounded-lg px-4 py-3 text-sm font-medium text-neutral-600 ring-neutral-500 transition duration-300 outline-none hover:bg-neutral-100 hover:text-neutral-700 focus-visible:ring dark:text-neutral-400 dark:ring-neutral-200 dark:hover:bg-neutral-700 dark:hover:text-neutral-300 dark:focus:outline-none"
>
<Icon name="share" />
{title}
</button>
<div
class="hs-dropdown-menu duration hs-dropdown-open:opacity-100 z-10 hidden w-72 divide-y divide-neutral-200 rounded-lg bg-neutral-50 p-2 opacity-0 shadow-md transition-[opacity,margin] dark:divide-neutral-700 dark:border dark:border-neutral-700 dark:bg-neutral-800"
aria-labelledby="hs-dropup"
>
<div class="py-2 first:pt-0 last:pb-0">
{
socialPlatforms.map((platform) => (
<a
class="flex items-center gap-x-3.5 rounded-lg px-3 py-2 text-sm text-neutral-700 hover:bg-neutral-200 focus:bg-neutral-100 focus:outline-none dark:text-neutral-300 dark:hover:bg-neutral-700 dark:hover:text-neutral-300 dark:focus:bg-neutral-700"
href={platform.url}
>
<Icon name={platform.svg} />
Share on {platform.name}
</a>
))
}
</div>
<div class="py-2 first:pt-0 last:pb-0">
<button
type="button"
class="js-clipboard hover:text-dark focus-visible:ring-secondary group inline-flex w-full items-center gap-x-3.5 rounded-lg px-3 py-2 text-sm text-neutral-700 hover:bg-neutral-200 focus:bg-neutral-100 focus:outline-none focus-visible:ring-1 focus-visible:outline-none dark:text-neutral-300 dark:hover:bg-neutral-700 dark:hover:text-neutral-300 dark:focus:bg-neutral-700"
data-clipboard-success-text="Copied"
>
<svg
class="js-clipboard-default h-4 w-4 transition group-hover:rotate-6"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect width="8" height="4" x="8" y="2" rx="1" ry="1"></rect>
<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path>
</svg>
<svg
class="js-clipboard-success hidden h-4 w-4 text-neutral-700 dark:text-neutral-300"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
<span class="js-clipboard-success-text">Copy link</span>
</button>
</div>
</div>
</div>
<!--Import the necessary Dropdown and Clipboard plugins-->
<!--https://preline.co/plugins/html/dropdown.html-->
<!--<script is:inline src="/scripts/vendor/preline/dropdown/index.js"></script>-->
<!-- https://clipboardjs.com/ -->
<!--<script is:inline src="/scripts/vendor/clipboard.min.js"></script>-->
<script is:inline>
(function () {
window.addEventListener('load', () => {
const $clipboards = document.querySelectorAll('.js-clipboard');
$clipboards.forEach((el) => {
const clipboard = new ClipboardJS(el, {
text: () => {
return window.location.href;
},
});
clipboard.on('success', () => {
const $default = el.querySelector('.js-clipboard-default');
const $success = el.querySelector('.js-clipboard-success');
const $successText = el.querySelector('.js-clipboard-success-text');
const successText = el.dataset.clipboardSuccessText || '';
let oldSuccessText;
if ($successText) {
oldSuccessText = $successText.textContent;
$successText.textContent = successText;
}
if ($default && $success) {
$default.style.display = 'none';
$success.style.display = 'block';
}
setTimeout(() => {
if ($successText && oldSuccessText) {
$successText.textContent = oldSuccessText;
}
if ($default && $success) {
$success.style.display = '';
$default.style.display = '';
}
}, 800);
});
});
});
})();
</script>

View File

@@ -1,152 +0,0 @@
---
import { Icon } from 'astro-icon/components';
import { readItems } from '@directus/sdk';
import type { Experience } from '@lib/directusTypes';
import directus from '@lib/directus';
const experiences = await directus.request(
readItems('site_experience', {
fields: ['*'],
sort: ['-endDate'],
})
);
---
<section
class:list={['flex flex-col gap-8', Astro.props.className]}
>
<h3 class="relative flex w-full items-center gap-3 pb-10 text-5xl text-neutral-800 dark:text-neutral-200">Experience</h3>
<ul class="ml-8 w-full flex flex-col">
{
experiences.map(
(experience: Experience) => {
const startYear = new Date(experience.startDate).getFullYear();
const endYear = experience.endDate != null ? new Date(experience.endDate).getFullYear() : 'Present';
return (
<li class="relative">
<div class="group smooth-reveal relative grid pb-1 transition-all sm:grid-cols-18 sm:gap-8 md:gap-6 lg:hover:!opacity-100">
<header class="relative mt-1 text-lg font-semibold sm:col-span-3 text-neutral-800 dark:text-neutral-200">
<time datetime={experience.startDate} data-title={experience.startDate}>
{startYear}
</time>{' '}
-{' '}
<time datetime={experience.endDate} data-title={experience.endDate}>
{endYear}
</time>
</header>
<div class="relative flex flex-col pb-6 before:absolute before:mt-8 before:-ml-6 before:h-full before:w-px before:bg-stone-400 sm:col-span-12">
<div class="absolute mt-4 h-2 w-2 -translate-x-[1.71rem] rounded-full bg-stone-400" />
<h3>
<div
class="inline-flex items-center text-2xl leading-tight font-semibold"
aria-label="{position} - {company}"
>
<span class="text-neutral-800 dark:text-neutral-200">
{experience.position} <span>@</span>
{experience.url ? (
<a
class="hover:text-steel dark:hover:text-bermuda"
href={experience.url}
title={`Ver ${experience.name}`}
target="_blank"
>
{experience.name}
</a>
) : (
<span>{experience.name}</span>
)}
</span>
</div>
</h3>
{(experience.location || experience.location_type) && (
<div class="text-sm text-neutral-600 dark:text-neutral-400">
{experience.location} {experience.location && experience.location_type && '-'} {experience.location_type}
</div>
)}
<div class="text-md mt-4 flex flex-col gap-4" x-data="{ expanded: false }">
{experience.summary && (
<div class="flex flex-col gap-1">
<h4 class="font-semibold text-neutral-800 dark:text-neutral-200">Summary:</h4>
<ul class="flex list-disc flex-col gap-2 text-neutral-700 dark:text-neutral-400 [&>li]:ml-4">
{Array.isArray(experience.summary) ? (
experience.summary.map((item) => ({ item }))
) : (
<li class="marker:text-steel dark:marker:text-bermuda">{experience.summary}</li>
)}
</ul>
</div>
)}
{(experience.responsibilities || experience.achievements) && (
<div class="relative flex flex-col gap-4 max-sm:!h-auto md:after:absolute md:after:bottom-0 md:after:h-12 md:after:w-full md:after:bg-gradient-to-t md:after:from-neutral-200 dark:md:after:from-stone-700 md:after:content-[''] " :class="expanded ? 'after:hidden' : ''" x-show="expanded" x-collapse.min.50px>
{experience.responsibilities && (
<div class="flex flex-col gap-1">
<h4 class="font-semibold text-neutral-800 dark:text-neutral-200">Responsibilities:</h4>
<ul class="text-neutral-700 dark:text-neutral-400 [&>li]:ml-4 flex list-disc flex-col gap-2">
{experience.responsibilities.map(responsibility => (
<li class="marker:text-steel dark:marker:text-bermuda">{responsibility}</li>
))}
</ul>
</div>
)}
{experience.achievements && (
<div class="flex flex-col gap-1">
<h4 class="font-semibold text-neutral-800 dark:text-neutral-200">Achievements:</h4>
<ul class="text-neutral-700 dark:text-neutral-400 [&>li]:ml-4 flex list-disc flex-col gap-2">
{experience.achievements.map(achievement => (
<li class="marker:text-steel dark:marker:text-bermuda">{achievement}</li>
))}
</ul>
</div>
)}
</div>
<button @click="expanded = ! expanded" class="group/more w-fit cursor-pointer items-center justify-center gap-1.5 text-xs underline text-neutral-700 dark:text-neutral-300 hover:text-neutral-900 dark:hover:text-neutral-400 transition-all flex">
<span x-text="expanded ? 'Show less' : 'Show more'">Show more</span>
<svg
class="h-4 w-4 duration-200 ease-out group-hover/more:translate-y-0.5"
:class="{ 'rotate-180': expanded }"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="6 9 12 15 18 9" />
</svg>
</button>
<ul class="flex print:hidden flex-wrap gap-2" aria-label="Technologies used">
{experience.skills && experience.skills.map(skill => {
const iconName = skill.toLowerCase();
return (
<li class="bg-steel/20 border-steel/20 text-neutral-800 dark:bg-bermuda/20 dark:border-bermuda/20 dark:text-neutral-200 flex gap-1 items-center border-solid border rounded-md px-2 py-0.5 text-xs">
<Icon name={`mdi:${iconName}`} /> <span>{skill}</span>
</li>
)
})}
</ul>
)}
</div>
</div>
</div>
</li>
);
}
)
}
</ul>
</section>
<!-- Alpine Plugins -->
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/collapse@3.x.x/dist/cdn.min.js"></script>
<!-- Alpine Core -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>

View File

@@ -1,35 +0,0 @@
---
import PrimaryCTA from '@components/ui/buttons/PrimaryCTA.astro';
interface Props {
title: string;
subTitle: string;
btnExists?: boolean;
btnTitle?: string;
btnURL?: string;
}
const { title, subTitle, btnExists, btnTitle, btnURL } = Astro.props;
---
<section class="mx-auto mt-10 px-4 sm:px-6 lg:px-8 lg:pt-10 2xl:max-w-full">
<div class="flex-wrap md:flex md:items-center md:justify-between">
<div class="w-full md:w-auto">
<h1
class="smooth-reveal block text-4xl font-bold tracking-tight text-balance text-neutral-800 md:text-5xl lg:text-6xl dark:text-neutral-200"
>
{title}
</h1>
<p class="smooth-reveal mt-4 text-lg text-pretty text-neutral-600 dark:text-neutral-400">
{subTitle}
</p>
{
btnExists ? (
<div class="smooth-reveal mt-4 md:mt-8">
<PrimaryCTA title={btnTitle} url={btnURL} />
</div>
) : null
}
</div>
</div>
</section>

View File

@@ -1,66 +0,0 @@
---
import PrimaryCTA from '@components/ui/buttons/PrimaryCTA.astro';
import SecondaryCTA from '@components/ui/buttons/SecondaryCTA.astro';
import Image from '@components/ui/images/Image.astro';
const { title, subTitle, primaryBtn, primaryBtnURL, secondaryBtn, secondaryBtnURL, src, alt } =
Astro.props;
interface Props {
title: string;
subTitle?: string;
primaryBtn?: string;
primaryBtnURL?: string;
secondaryBtn?: string;
secondaryBtnURL?: string;
src?: any;
alt?: string;
rounded?: boolean;
}
const roundedClasses = Astro.props.rounded ? "rounded-xl" : null;
---
<section
class="mx-auto grid max-w-340 gap-4 px-4 py-14 sm:px-6 md:grid-cols-2 md:items-center md:gap-8 lg:px-8 2xl:max-w-full"
>
<div>
<h1
class="smooth-reveal block text-3xl font-bold tracking-tight text-balance text-neutral-800 sm:text-4xl lg:text-7xl lg:leading-tight dark:text-neutral-200"
>
<Fragment set:html={title} />
</h1>
{
subTitle && (
<p class="smooth-reveal mt-6 text-lg leading-relaxed text-pretty text-neutral-700 lg:w-4/5 dark:text-neutral-300">
{subTitle}
</p>
)
}
<div class="smooth-reveal mt-7 grid w-full gap-3 sm:inline-flex">
{primaryBtn && <PrimaryCTA title={primaryBtn} url={primaryBtnURL} />}
{secondaryBtn && <SecondaryCTA title={secondaryBtn} url={secondaryBtnURL} />}
</div>
</div>
<div class="smooth-reveal-fade hidden w-full md:block">
<div class="top-12 flex w-full justify-center overflow-hidden md:ml-4">
{
src && alt && (
<Image
src={src}
alt={alt}
class={`h-full w-[420px] scale-100 object-cover object-center ${roundedClasses}`}
draggable="false"
loading="eager"
format="webp"
quality="low"
widths={[840]}
disableBlur={true}
/>
)
}
</div>
</div>
</section>

View File

@@ -3,8 +3,8 @@ import { readSingleton } from '@directus/sdk';
import directus from '@lib/directus'; import directus from '@lib/directus';
import BaseLayout from '@layouts/BaseLayout.astro'; import BaseLayout from '@layouts/BaseLayout.astro';
import PrimaryCTA from '@components/ui/buttons/PrimaryCTA.astro'; import GoBackButton from '@/components/buttons/GoBackButton.astro';
import GoBack from '@/components/ui/buttons/GoBack.astro'; import GoHomeButton from '@/components/buttons/GoHomeButton.astro';
const global = await directus.request(readSingleton('site_global')); const global = await directus.request(readSingleton('site_global'));
--- ---
@@ -61,8 +61,8 @@ const global = await directus.request(readSingleton('site_global'));
<div <div
class="smooth-reveal mt-10 flex flex-col items-center justify-center gap-4 sm:flex-row" class="smooth-reveal mt-10 flex flex-col items-center justify-center gap-4 sm:flex-row"
> >
<GoBack title="Go Back" /> <GoBackButton/>
<PrimaryCTA title="Return Home" url={global.site_url} noArrow addHome /> <GoHomeButton url={global.site_url} />
</div> </div>
</div> </div>
</div> </div>

View File

@@ -3,11 +3,11 @@ import { readSingleton } from '@directus/sdk';
import directus from '@lib/directus'; import directus from '@lib/directus';
import BaseLayout from '@layouts/BaseLayout.astro'; import BaseLayout from '@layouts/BaseLayout.astro';
import HeroSection from '@components/ui/sections/HeroSection.astro'; import HeroSection from '@components/sections/HeroSection.astro';
import Experience from '@components/ui/sections/Experience.astro'; import ExperienceSection from '@components/sections/ExperienceSection.astro';
import EducationSection from '@components/ui/sections/EducationSection.astro'; import EducationSection from '@components/sections/EducationSection.astro';
import ProjectSection from '@components/ui/sections/ProjectSection.astro'; import ProjectSection from '@components/sections/ProjectSection.astro';
import SkillsSlider from '@components/ui/sections/SkillsSlider.astro'; import SkillsSliderSection from '@components/sections/SkillsSliderSection.astro';
import portraitImg from '@images/portrait.avif'; import portraitImg from '@images/portrait.avif';
@@ -44,10 +44,10 @@ const global = await directus.request(readSingleton('site_global'));
<section class="mx-auto max-w-7xl px-4 py-10 sm:px-6 lg:px-8 lg:py-14"> <section class="mx-auto max-w-7xl px-4 py-10 sm:px-6 lg:px-8 lg:py-14">
<div class="flex flex-col gap-y-24 md:gap-y-32"> <div class="flex flex-col gap-y-24 md:gap-y-32">
<Experience className="smooth-reveal" /> <ExperienceSection className="smooth-reveal" />
<EducationSection className="smooth-reveal" /> <EducationSection className="smooth-reveal" />
<ProjectSection className="smooth-reveal" /> <ProjectSection className="smooth-reveal" />
<SkillsSlider className="smooth-reveal" /> <SkillsSliderSection className="smooth-reveal" />
</div> </div>
</section> </section>

View File

@@ -3,8 +3,8 @@ import { readSingleton } from '@directus/sdk';
import directus from '@lib/directus'; import directus from '@lib/directus';
import BaseLayout from '@layouts/BaseLayout.astro'; import BaseLayout from '@layouts/BaseLayout.astro';
import HeroSection from '@components/ui/sections/HeroSection.astro'; import HeroSection from '@components/sections/HeroSection.astro';
import ApplicationSection from '@components/ui/sections/ApplicationSection.astro'; import ApplicationSection from '@components/sections/ApplicationSection.astro';
import applicationImg from '@images/cedar_tree.png'; import applicationImg from '@images/cedar_tree.png';

View File

@@ -10,6 +10,7 @@ import { createHighlighter } from 'shiki';
import { getDirectusImageURL } from '@lib/directusFunctions'; import { getDirectusImageURL } from '@lib/directusFunctions';
import BaseLayout from '@layouts/BaseLayout.astro'; import BaseLayout from '@layouts/BaseLayout.astro';
import Image from '@components/ui/images/Image.astro'; import Image from '@components/ui/images/Image.astro';
import SocialShareButton from '@components/buttons/SocialShareButton.astro';
import { formatDateTime } from '@support/time'; import { formatDateTime } from '@support/time';
export async function getStaticPaths() { export async function getStaticPaths() {
@@ -162,6 +163,9 @@ const content = marked.parse(post.content);
)) ))
} }
</div> </div>
<SocialShareButton
pageTitle={post.title}
/>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -7,7 +7,7 @@ import directus from '@lib/directus';
import BaseLayout from '@layouts/BaseLayout.astro'; import BaseLayout from '@layouts/BaseLayout.astro';
import BlogSelectedArticles from '@components/blog/BlogSelectedArticles.astro'; import BlogSelectedArticles from '@components/blog/BlogSelectedArticles.astro';
import BlogRecentArticles from '@components/blog/BlogRecentArticles.astro'; import BlogRecentArticles from '@components/blog/BlogRecentArticles.astro';
import HeroSection from '@components/ui/sections/HeroSection.astro'; import HeroSection from '@components/sections/HeroSection.astro';
import blogImg from '@images/autumn_tree.png'; import blogImg from '@images/autumn_tree.png';
const global = await directus.request(readSingleton('site_global')); const global = await directus.request(readSingleton('site_global'));

View File

@@ -6,7 +6,7 @@ import directus from '@lib/directus';
import type { Post } from '@lib/directusTypes'; import type { Post } from '@lib/directusTypes';
import BaseLayout from '@layouts/BaseLayout.astro'; import BaseLayout from '@layouts/BaseLayout.astro';
import BlogCard from '@components/blog/BlogCard.astro'; import BlogCard from '@components/blog/BlogCard.astro';
import HeaderSection from '@components/ui/sections/HeaderSection.astro'; import HeaderSection from '@components/sections/HeaderSection.astro';
export async function getStaticPaths() { export async function getStaticPaths() {
const categories = await getCollection('categories'); const categories = await getCollection('categories');

View File

@@ -7,7 +7,7 @@ import type { Post } from '@lib/directusTypes';
import directus from '@lib/directus'; import directus from '@lib/directus';
import BaseLayout from '@layouts/BaseLayout.astro'; import BaseLayout from '@layouts/BaseLayout.astro';
import BlogCategoryCard from '@components/blog/BlogCategoryCard.astro'; import BlogCategoryCard from '@components/blog/BlogCategoryCard.astro';
import HeroSection from '@components/ui/sections/HeroSection.astro'; import HeroSection from '@components/sections/HeroSection.astro';
import { timeago } from '@support/time'; import { timeago } from '@support/time';
import categoryImg from '@images/autumn_bench.png'; import categoryImg from '@images/autumn_bench.png';

View File

@@ -3,11 +3,12 @@ import { readSingleton } from '@directus/sdk';
import directus from '@lib/directus'; import directus from '@lib/directus';
import BaseLayout from '@layouts/BaseLayout.astro'; import BaseLayout from '@layouts/BaseLayout.astro';
import HeroSection from '@components/ui/sections/HeroSection.astro'; import HeroSection from '@components/sections/HeroSection.astro';
import FeaturesSection from '@components/ui/sections/FeaturesSection.astro'; import FeaturesSection from '@components/sections/FeaturesSection.astro';
import WeatherSection from '@components/ui/sections/WeatherSection.astro'; import WeatherSection from '@components/sections/WeatherSection.astro';
import LatestPosts from '@components/ui/sections/LatestPosts.astro'; import LatestPostsSection from '@components/sections/LatestPostsSection.astro';
import HeroSectionAlt from '@components/ui/sections/HeroSectionAlt.astro'; import GiteaSection from '@components/sections/GiteaSection.astro';
import homeImg from '@images/autumn_mountain.png'; import homeImg from '@images/autumn_mountain.png';
const global = await directus.request(readSingleton('site_global')); const global = await directus.request(readSingleton('site_global'));
@@ -53,9 +54,9 @@ const weather = await directus.request(readSingleton('site_weather'));
> >
</WeatherSection> </WeatherSection>
<LatestPosts /> <LatestPostsSection />
<HeroSectionAlt <GiteaSection
title="Follow me on Gitea" title="Follow me on Gitea"
subTitle="I love open source and have my code availabile on my Gitea server." subTitle="I love open source and have my code availabile on my Gitea server."
url="https://gitea.alexlebens.dev" url="https://gitea.alexlebens.dev"

View File

@@ -1,5 +1,7 @@
@import 'tailwindcss'; @import 'tailwindcss';
@import 'preline/variants.css'; @import 'preline/variants.css';
@import './utilities.css';
@plugin '@tailwindcss/typography'; @plugin '@tailwindcss/typography';
@plugin '@tailwindcss/forms'; @plugin '@tailwindcss/forms';
@@ -10,11 +12,17 @@
/* Custom colors */ /* Custom colors */
@theme { @theme {
--color-midnight: #0c354d; --color-midnight: #0c354d;
--color-turquoise: #0da797; --color-ocean: #134e70;
--color-cobalt: #6c9cb0;
--color-steel: #4682b4; --color-steel: #4682b4;
--color-turquoise: #0da797;
--color-bermuda: #7fbab4; --color-bermuda: #7fbab4;
--color-desert: #f9deb2; --color-desert: #f9deb2;
--color-bronze: #9e7f5e; --color-bronze: #9e7f5e;
--color-gitea-primary: #609926; --color-gitea-primary: #609926;
--color-gitea-secondary: #4c7a33; --color-gitea-secondary: #4c7a33;
@@ -23,6 +31,7 @@
--color-header: light-dark(var(--color-neutral-800), var(--color-neutral-200)); --color-header: light-dark(var(--color-neutral-800), var(--color-neutral-200));
--color-primary: light-dark(var(--color-neutral-600), var(--color-neutral-200)); --color-primary: light-dark(var(--color-neutral-600), var(--color-neutral-200));
--color-primary-hover: light-dark(var(--color-neutral-800), var(--color-neutral-400));
--color-secondary: light-dark(var(--color-neutral-500), var(--color-neutral-400)); --color-secondary: light-dark(var(--color-neutral-500), var(--color-neutral-400));
} }
@@ -98,63 +107,6 @@
} }
} }
@utility card-base {
@apply rounded-xl transition-all duration-300
border border-neutral-100 dark:border-stone-500/20
bg-neutral-100/80 hover:bg-neutral-100 dark:bg-neutral-800/60 dark:hover:bg-neutral-800/90
shadow-xs hover:shadow-md dark:shadow-md dark:hover:shadow-lg
}
@utility card-hover-icon-color {
@apply transition-all duration-300
text-primary
group-hover:text-main
}
@utility card-hover-icon-scale {
@apply transition-all duration-300
drop-shadow-sm
group-hover:scale-110
}
@utility card-text-header {
@apply text-header
md:text-5xl
text-4xl
}
@utility card-text-header-minor {
@apply text-header
md:text-3xl
text-2xl
font-semibold
}
@utility card-text-header-description {
@apply text-primary
text-lg
text-pretty
}
@utility card-text-title {
@apply text-primary
font-bold
}
@utility card-text-description {
@apply text-secondary
}
@utility card-hover-text-title {
@apply transition-all duration-300
group-hover:text-main
}
@utility card-hover-text-gitea {
@apply transition-all duration-300
group-hover:text-gitea-primary
}
/* Content reveal animations */ /* Content reveal animations */
.smooth-reveal, .smooth-reveal,
.smooth-reveal-2, .smooth-reveal-2,

110
src/styles/utilities.css Normal file
View File

@@ -0,0 +1,110 @@
/* Button classes */
@utility button-base {
@apply transition-all duration-300
border border-transparent
shadow-sm hover:shadow-md dark:shadow-md dark:hover:shadow-lg
px-4 py-3
}
@utility button-base-hidden {
@apply transition-all duration-300
border border-transparent
hover:bg-neutral-100 dark:hover:bg-neutral-700
p-2
}
@utility button-hover-arrow {
@apply translate-y-px transition duration-300
group-hover:translate-x-1
h-3 w-3 md:h-5 md:w-5
}
@utility button-text-title {
@apply text-neutral-200 2xl:text-base
text-sm font-bold
}
@utility button-text-title-hidden {
@apply transition-all duration-300
text-neutral-600 group-hover:text-neutral-700 dark:text-neutral-400 dark:group-hover:text-neutral-300 2xl:text-base
text-sm font-medium
}
@utility button-bg-blue {
@apply transition-all duration-300
bg-cobalt hover:bg-steel dark:bg-steel dark:hover:bg-cobalt
}
@utility button-bg-teal {
@apply transition-all duration-300
bg-bermuda hover:bg-turquoise dark:bg-turquoise dark:hover:bg-bermuda
}
@utility button-bg-neutral {
@apply transition-all duration-300
border border-neutral-100 dark:border-stone-500/20
bg-neutral-100/80 hover:bg-neutral-100 dark:bg-neutral-800/60 dark:hover:bg-neutral-800/90
}
@utility button-bg-gitea {
@apply transition-all duration-300
bg-gitea-primary hover:bg-gitea-secondary dark:bg-gitea-secondary dark:hover:bg-gitea-primary
}
/* Card classes */
@utility card-base {
@apply rounded-xl
border border-neutral-100 dark:border-stone-500/20
bg-neutral-100/80 hover:bg-neutral-100 dark:bg-neutral-800/60 dark:hover:bg-neutral-800/90
shadow-xs hover:shadow-md dark:shadow-md dark:hover:shadow-lg
}
@utility card-hover-icon-color {
@apply transition-all duration-300
text-primary
group-hover:text-main
}
@utility card-hover-icon-scale {
@apply transition-all duration-300
drop-shadow-sm
group-hover:scale-110
}
@utility card-text-header {
@apply text-header
text-4xl md:text-5xl
font-bold leading-tight tracking-tight text-balance
}
@utility card-text-header-minor {
@apply text-header
md:text-3xl
text-2xl
font-semibold
}
@utility card-text-header-description {
@apply text-primary
text-lg
text-pretty leading-relaxed
}
@utility card-text-title {
@apply text-primary
font-bold
}
@utility card-hover-text-title {
@apply transition-all duration-300
group-hover:text-main
}
@utility card-hover-text-gitea {
@apply transition-all duration-300
group-hover:text-gitea-primary
}
@utility card-text-description {
@apply text-secondary
}