Compare commits
19 Commits
2.7.0
...
renovate/s
| Author | SHA1 | Date | |
|---|---|---|---|
|
539ef40813
|
|||
| a74cc775d0 | |||
| 5271be52a2 | |||
| 8a649b7647 | |||
| c4be4653be | |||
| 47a637353c | |||
| a09a4ee240 | |||
| 342ae8900a | |||
| 2cdef1a553 | |||
| a8d6446674 | |||
| fcd3057f40 | |||
| d464f0fe43 | |||
| 0f403fa274 | |||
| 0fc359a973 | |||
| 104fe35ee8 | |||
| a57f43e082 | |||
| efad6c30d1 | |||
| c2d26228ba | |||
|
94fe56022d
|
@@ -48,6 +48,13 @@ jobs:
|
||||
username: ${{ gitea.actor }}
|
||||
password: ${{ secrets.REPOSITORY_TOKEN }}
|
||||
|
||||
- name: Login to Docker
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ vars.DH_REGISTRY }}
|
||||
username: ${{ secrets.DH_USERNAME }}
|
||||
password: ${{ secrets.DH_TOKEN }}
|
||||
|
||||
- name: Create Kubeconfig
|
||||
run: |
|
||||
mkdir $HOME/.kube
|
||||
|
||||
@@ -48,6 +48,13 @@ jobs:
|
||||
username: ${{ vars.REGISTRY_USER }}
|
||||
password: ${{ secrets.REGISTRY_SECRET }}
|
||||
|
||||
- name: Login to Docker
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ vars.DH_REGISTRY }}
|
||||
username: ${{ secrets.DH_USERNAME }}
|
||||
password: ${{ secrets.DH_TOKEN }}
|
||||
|
||||
- name: Create Kubeconfig
|
||||
run: |
|
||||
mkdir $HOME/.kube
|
||||
|
||||
12
Dockerfile
12
Dockerfile
@@ -1,15 +1,13 @@
|
||||
ARG REGISTRY=docker.io
|
||||
FROM ${REGISTRY}/node:24.13.0-alpine AS base
|
||||
FROM docker.io/node:24.13.1-alpine AS builder
|
||||
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
|
||||
FROM base AS prod-deps
|
||||
FROM builder AS prod-deps
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile
|
||||
|
||||
FROM prod-deps AS build-deps
|
||||
@@ -18,9 +16,9 @@ RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
|
||||
FROM build-deps AS build
|
||||
COPY . .
|
||||
RUN pnpm run build
|
||||
RUN pnpm prune --prod
|
||||
|
||||
FROM base AS runtime
|
||||
FROM dhi.io/node:24.13.1 AS runtime
|
||||
WORKDIR /app
|
||||
COPY --from=prod-deps /app/node_modules /app/node_modules
|
||||
COPY --from=build /app/dist /app/dist
|
||||
|
||||
@@ -29,7 +27,7 @@ ENV SITE_URL=https://www.alexlebens.dev
|
||||
ENV DIRECTUS_URL=https://directus.alexlebens.net
|
||||
ENV PORT=4321
|
||||
|
||||
LABEL version="2.7.0"
|
||||
LABEL version="2.10.1"
|
||||
LABEL description="Astro based personal website"
|
||||
|
||||
EXPOSE $PORT
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "site-profile",
|
||||
"type": "module",
|
||||
"version": "2.7.0",
|
||||
"version": "2.10.1",
|
||||
"homepage": "https://www.alexlebens.dev",
|
||||
"bugs": {
|
||||
"url": "https://gitea.alexlebens.dev/alexlebens/site-profile/issues",
|
||||
|
||||
18
pnpm-lock.yaml
generated
18
pnpm-lock.yaml
generated
@@ -46,7 +46,7 @@ importers:
|
||||
version: 0.2.1(@types/node@25.2.3)(jiti@2.6.1)(rollup@2.79.2)(typescript@5.9.3)(yaml@2.8.2)
|
||||
'@swup/astro':
|
||||
specifier: ^1.7.0
|
||||
version: 1.7.0(@types/babel__core@7.20.5)
|
||||
version: 1.8.0(@types/babel__core@7.20.5)
|
||||
'@tailwindcss/postcss':
|
||||
specifier: ^4.1.18
|
||||
version: 4.1.18
|
||||
@@ -1747,8 +1747,8 @@ packages:
|
||||
peerDependencies:
|
||||
swup: ^4.0.0
|
||||
|
||||
'@swup/astro@1.7.0':
|
||||
resolution: {integrity: sha512-OxZskzeYkEZ4+McbwJKyD018sHNlrKQLdtHzlmNjivrzQY/dB7ncnWqDs3d4IkA7Y+ydNjpwBqpR/asjDhTiag==}
|
||||
'@swup/astro@1.8.0':
|
||||
resolution: {integrity: sha512-FI8/jNXgyUoWpnOgdB3MOUobRtRzedKBE8+qbBH5c8CE4q9Ni/GHPGrXK/0xG+g1ZiIQuTF3OXRvTLNVouiBig==}
|
||||
|
||||
'@swup/body-class-plugin@3.3.0':
|
||||
resolution: {integrity: sha512-4h/6mAgDd0+ml8Gc2kX8tikIR0HZZLF+WTnfm0JTVrGDw74bgEBenbsaWFfIS+gn4RWKZdWBUOfaZ+Im5J7Gvw==}
|
||||
@@ -2395,6 +2395,9 @@ packages:
|
||||
caniuse-lite@1.0.30001769:
|
||||
resolution: {integrity: sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==}
|
||||
|
||||
caniuse-lite@1.0.30001770:
|
||||
resolution: {integrity: sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw==}
|
||||
|
||||
ccount@2.0.1:
|
||||
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
|
||||
|
||||
@@ -5140,6 +5143,7 @@ packages:
|
||||
tar@7.5.7:
|
||||
resolution: {integrity: sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==}
|
||||
engines: {node: '>=18'}
|
||||
deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||
|
||||
terser@5.44.1:
|
||||
resolution: {integrity: sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==}
|
||||
@@ -7377,7 +7381,7 @@ snapshots:
|
||||
focus-options-polyfill: 1.6.0
|
||||
swup: 4.8.2
|
||||
|
||||
'@swup/astro@1.7.0(@types/babel__core@7.20.5)':
|
||||
'@swup/astro@1.8.0(@types/babel__core@7.20.5)':
|
||||
dependencies:
|
||||
'@swup/a11y-plugin': 5.0.0(swup@4.8.2)
|
||||
'@swup/body-class-plugin': 3.3.0(swup@4.8.2)
|
||||
@@ -8217,7 +8221,7 @@ snapshots:
|
||||
autoprefixer@10.4.24(postcss@8.5.6):
|
||||
dependencies:
|
||||
browserslist: 4.28.1
|
||||
caniuse-lite: 1.0.30001769
|
||||
caniuse-lite: 1.0.30001770
|
||||
fraction.js: 5.3.4
|
||||
picocolors: 1.1.1
|
||||
postcss: 8.5.6
|
||||
@@ -8351,12 +8355,14 @@ snapshots:
|
||||
caniuse-api@3.0.0:
|
||||
dependencies:
|
||||
browserslist: 4.28.1
|
||||
caniuse-lite: 1.0.30001769
|
||||
caniuse-lite: 1.0.30001770
|
||||
lodash.memoize: 4.1.2
|
||||
lodash.uniq: 4.5.0
|
||||
|
||||
caniuse-lite@1.0.30001769: {}
|
||||
|
||||
caniuse-lite@1.0.30001770: {}
|
||||
|
||||
ccount@2.0.1: {}
|
||||
|
||||
chalk@1.1.3:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
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';
|
||||
|
||||
const pathname = new URL(Astro.request.url).pathname;
|
||||
@@ -67,7 +67,7 @@ const currentPath = pathname.slice(1);
|
||||
|
||||
<div
|
||||
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
|
||||
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">
|
||||
<ThemeToggle />
|
||||
<ThemeToggleButton />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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';
|
||||
|
||||
interface Props {
|
||||
@@ -39,6 +39,6 @@ const { title, subTitle, btnExists, btnTitle, btnURL, img, imgAlt } = Astro.prop
|
||||
>
|
||||
{subTitle}
|
||||
</p>
|
||||
{btnExists ? <PrimaryCTA title={btnTitle} url={btnURL} /> : null}
|
||||
{btnExists ? <GoLinkPrimaryButton title={btnTitle} url={btnURL} /> : null}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -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';
|
||||
|
||||
interface Props {
|
||||
@@ -43,7 +43,7 @@ const {
|
||||
>
|
||||
{subTitle}
|
||||
</p>
|
||||
{btnExists ? <PrimaryCTA title={btnTitle} url={btnURL} /> : null}
|
||||
{btnExists ? <GoLinkPrimaryButton title={btnTitle} url={btnURL} /> : null}
|
||||
</div>
|
||||
|
||||
{
|
||||
|
||||
@@ -4,7 +4,7 @@ import Icon from '@components/ui/icons/icon.astro';
|
||||
|
||||
<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"
|
||||
>
|
||||
<Icon name="bookmark" />
|
||||
31
src/components/buttons/GiteaButton.astro
Normal file
31
src/components/buttons/GiteaButton.astro
Normal 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>
|
||||
28
src/components/buttons/GoBackButton.astro
Normal file
28
src/components/buttons/GoBackButton.astro
Normal 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>
|
||||
25
src/components/buttons/GoHomeButton.astro
Normal file
25
src/components/buttons/GoHomeButton.astro
Normal 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>
|
||||
29
src/components/buttons/GoLinkPrimaryButton.astro
Normal file
29
src/components/buttons/GoLinkPrimaryButton.astro
Normal 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>
|
||||
20
src/components/buttons/GoLinkSecondaryButton.astro
Normal file
20
src/components/buttons/GoLinkSecondaryButton.astro
Normal 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>
|
||||
51
src/components/buttons/SocialShareButton.astro
Normal file
51
src/components/buttons/SocialShareButton.astro
Normal 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>
|
||||
@@ -5,14 +5,14 @@
|
||||
<button
|
||||
id="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"
|
||||
>
|
||||
<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 -->
|
||||
<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"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
@@ -29,7 +29,7 @@
|
||||
<!-- Moon icon -->
|
||||
<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"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
@@ -43,25 +43,23 @@
|
||||
</button>
|
||||
|
||||
<script is:inline>
|
||||
// Use a function to persist theme when using SPA transitions
|
||||
// https://docs.astro.build/en/guides/view-transitions/#script-re-execution
|
||||
function applyTheme() {
|
||||
localStorage.theme === 'dark'
|
||||
? document.documentElement.classList.add('dark')
|
||||
: document.documentElement.classList.remove('dark');
|
||||
}
|
||||
|
||||
document.addEventListener('astro:after-swap', applyTheme);
|
||||
const applyTheme = () => {
|
||||
const isDark =
|
||||
localStorage.theme === 'dark' ||
|
||||
(!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
document.documentElement.classList.toggle('dark', isDark);
|
||||
};
|
||||
|
||||
applyTheme();
|
||||
|
||||
document.addEventListener('astro:after-swap', applyTheme);
|
||||
</script>
|
||||
|
||||
<script>
|
||||
// Use a function to handle theme toggle to ensure it can be called from anywhere
|
||||
function setupThemeToggle() {
|
||||
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')) {
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'theme-switch-overlay fixed inset-0 pointer-events-none z-50';
|
||||
@@ -70,9 +68,7 @@
|
||||
document.body.appendChild(overlay);
|
||||
}
|
||||
|
||||
// Toggle theme when any theme toggle button is clicked
|
||||
themeToggles.forEach((toggle) => {
|
||||
// Add event listeners for both click and touch events
|
||||
['click', 'touchend'].forEach((eventType) => {
|
||||
toggle.addEventListener(
|
||||
eventType,
|
||||
@@ -92,14 +88,10 @@
|
||||
y = e.clientY - rect.top;
|
||||
}
|
||||
|
||||
// Set the position variables for the radial gradient
|
||||
document.documentElement.style.setProperty('--x', `${x}px`);
|
||||
document.documentElement.style.setProperty('--y', `${y}px`);
|
||||
|
||||
// Get the overlay element
|
||||
const overlay = document.querySelector('.theme-switch-overlay');
|
||||
|
||||
// Determine the new theme
|
||||
const isDark = document.documentElement.classList.contains('dark');
|
||||
const newTheme = isDark ? 'light' : 'dark';
|
||||
|
||||
@@ -110,7 +102,6 @@
|
||||
overlay.style.opacity = '1';
|
||||
}
|
||||
|
||||
// Add transition class
|
||||
document.documentElement.classList.add('theme-switching');
|
||||
|
||||
// Force a reflow to ensure all elements update
|
||||
@@ -124,10 +115,7 @@
|
||||
document.documentElement.classList.add('dark');
|
||||
}
|
||||
|
||||
// Store the preference
|
||||
localStorage.setItem('theme', newTheme);
|
||||
|
||||
// Dispatch a custom event for other components to react to
|
||||
document.dispatchEvent(
|
||||
new CustomEvent('themeChanged', {
|
||||
detail: { isDark: newTheme === 'dark' },
|
||||
@@ -137,13 +125,10 @@
|
||||
// Force another reflow to ensure all elements update
|
||||
document.body.offsetHeight;
|
||||
|
||||
// Hide overlay after theme has changed
|
||||
setTimeout(() => {
|
||||
if (overlay) {
|
||||
overlay.style.opacity = '0';
|
||||
}
|
||||
|
||||
// Remove transition class after animation completes
|
||||
document.documentElement.classList.remove('theme-switching');
|
||||
}, 300);
|
||||
}, 50);
|
||||
@@ -151,25 +136,6 @@
|
||||
{ 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>
|
||||
|
||||
<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 */
|
||||
#theme-toggle {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 0 0 rgba(0, 0, 0, 0);
|
||||
-webkit-tap-highlight-color: transparent; /* Remove default mobile tap highlight */
|
||||
min-height: 32px; /* Ensure minimum touch target size */
|
||||
min-width: 32px; /* Ensure minimum touch target size */
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
min-height: 32px;
|
||||
min-width: 32px;
|
||||
}
|
||||
|
||||
/* Only apply hover effects on non-touch devices */
|
||||
@media (hover: hover) {
|
||||
#theme-toggle:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
#theme-toggle:hover .icon-light:not(.dark .icon-light) {
|
||||
filter: drop-shadow-sm(0 0 2px rgba(251, 191, 36, 0.6));
|
||||
:global(:root:not(.dark)) #theme-toggle:hover .icon-light {
|
||||
filter: drop-shadow(0 0 2px rgba(251, 191, 36, 0.6));
|
||||
transform: scale(1.1) rotate(15deg);
|
||||
}
|
||||
|
||||
#theme-toggle:hover .icon-dark:not(:not(.dark) .icon-dark) {
|
||||
filter: drop-shadow-sm(0 0 2px rgba(129, 140, 248, 0.6));
|
||||
:global(:root.dark) #theme-toggle:hover .icon-dark {
|
||||
filter: drop-shadow(0 0 2px rgba(129, 140, 248, 0.6));
|
||||
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 */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.icon-light,
|
||||
62
src/components/cards/EducationCard.astro
Normal file
62
src/components/cards/EducationCard.astro
Normal file
@@ -0,0 +1,62 @@
|
||||
---
|
||||
import { Icon } from 'astro-icon/components';
|
||||
import Logo from '@components/ui/logos/Logo.astro';
|
||||
|
||||
interface Props {
|
||||
topic: string;
|
||||
area: string;
|
||||
date: string;
|
||||
url: string;
|
||||
logoUrlLight?: string;
|
||||
logoUrlDark?: string;
|
||||
logoIcon?: string;
|
||||
}
|
||||
|
||||
const { topic, area, date, url, logoUrlLight, logoUrlDark, logoIcon } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="smooth-reveal group flex flex-col">
|
||||
<a
|
||||
class="card-base flex items-center"
|
||||
href={url}
|
||||
>
|
||||
<div class="p-4 md:p-10">
|
||||
<div class="flex items-center">
|
||||
{logoUrlLight ? (
|
||||
<div class="card-hover-icon-scale mr-5">
|
||||
<Logo
|
||||
srcLight={logoUrlLight}
|
||||
srcDark={logoUrlDark}
|
||||
alt={`Logo of ${topic}`}
|
||||
/>
|
||||
</div>
|
||||
) : logoIcon ? (
|
||||
<div class="mr-5 text-header">
|
||||
<Icon name={logoIcon} class="card-hover-icon-scale h-12 w-12" />
|
||||
</div>
|
||||
) : null}
|
||||
<div class="grow text-left">
|
||||
<span class="card-text-title block text-lg">
|
||||
{topic}
|
||||
</span>
|
||||
<span class="card-text-description block mt-1 font-medium text-xs uppercase">
|
||||
{area} - {new Date(date).getFullYear()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-6 flex">
|
||||
<div class="relative inline-block">
|
||||
<div class="card-text-title card-hover-text-title flex relative mx-auto min-h-11 items-center font-semibold text-md sm:mx-0 sm:mt-4">
|
||||
<span class="relative inline-block overflow-hidden">
|
||||
Visit Page
|
||||
</span>
|
||||
<Icon
|
||||
name="mdi:keyboard-arrow-right"
|
||||
class="translate-y-0.5 transition duration-300 group-hover:translate-x-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
39
src/components/cards/FeaturesCard.astro
Normal file
39
src/components/cards/FeaturesCard.astro
Normal file
@@ -0,0 +1,39 @@
|
||||
---
|
||||
import { Icon } from 'astro-icon/components';
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
description?: string;
|
||||
url?: string;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
const { title, description, url, icon } = Astro.props;
|
||||
|
||||
const sizeClasses = 'h-30 w-100 md:w-[300px]';
|
||||
---
|
||||
|
||||
<div class="smooth-reveal-2 group flex flex-col">
|
||||
<a
|
||||
class={`card-base flex items-center ${sizeClasses}`}
|
||||
href={url}
|
||||
data-astro-prefetch
|
||||
>
|
||||
<div class="p-5 w-full">
|
||||
<div class="flex items-center">
|
||||
<Icon
|
||||
name={icon}
|
||||
class="card-hover-icon-scale shrink-0 h-6 w-6 md:h-8 md:w-8 "
|
||||
/>
|
||||
<div class="ms-5 grow text-left">
|
||||
<span class="card-text-title card-hover-text-title block text-lg">
|
||||
{title}
|
||||
</span>
|
||||
<span class="card-text-description block mt-1">
|
||||
{description}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
72
src/components/cards/HighlightsCard.astro
Normal file
72
src/components/cards/HighlightsCard.astro
Normal file
@@ -0,0 +1,72 @@
|
||||
---
|
||||
import { Icon } from 'astro-icon/components';
|
||||
|
||||
import Logo from '@components/ui/logos/Logo.astro';
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
description?: string;
|
||||
url?: string;
|
||||
logoUrlLight?: string;
|
||||
logoUrlDark?: string;
|
||||
highlights?: string[];
|
||||
visitSource?: boolean;
|
||||
}
|
||||
|
||||
const { title, description, url, logoUrlLight, logoUrlDark, highlights, visitSource } = Astro.props;
|
||||
|
||||
const visitText = visitSource ? 'Visit Source' : 'Visit Page';
|
||||
const visitClass = visitSource ? 'card-hover-text-gitea' : 'card-hover-text-title';
|
||||
---
|
||||
|
||||
<div class="smooth-reveal group flex flex-col">
|
||||
<a
|
||||
class="card-base flex items-center"
|
||||
href={url}
|
||||
>
|
||||
<div class="p-4 md:p-10">
|
||||
<div class="flex items-center mb-4">
|
||||
{logoUrlLight && (
|
||||
<div class="card-hover-icon-scale mr-5">
|
||||
<Logo
|
||||
srcLight={logoUrlLight}
|
||||
srcDark={logoUrlDark}
|
||||
alt={`Logo of ${title}`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div class="grow text-left">
|
||||
<span class="card-text-title block text-lg">
|
||||
{title}
|
||||
</span>
|
||||
<span class="card-text-description block mt-1">
|
||||
{description}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{highlights && (
|
||||
<ul class="card-text-description text-sm mt-1 flex flex-col list-disc gap-2 [&>li]:ml-4">
|
||||
{highlights.map((highlight) => (
|
||||
<li class="marker:text-accent">
|
||||
{highlight}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
<div class="ml-6 flex">
|
||||
<div class="relative inline-block">
|
||||
<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" />}
|
||||
<span class="relative inline-block overflow-hidden ml-2">
|
||||
{visitText}
|
||||
</span>
|
||||
<Icon
|
||||
name="mdi:keyboard-arrow-right"
|
||||
class="translate-y-0.5 transition duration-300 group-hover:translate-x-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
37
src/components/cards/WeatherCard.astro
Normal file
37
src/components/cards/WeatherCard.astro
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
interface Props {
|
||||
dayName: string;
|
||||
label: string;
|
||||
icon: string;
|
||||
temp: number;
|
||||
}
|
||||
|
||||
const { dayName, label, icon, temp } = Astro.props;
|
||||
|
||||
const sizeClasses = 'w-32 md:w-40';
|
||||
---
|
||||
|
||||
<div class="smooth-reveal-2 group flex flex-col">
|
||||
<div class={`card-base ${sizeClasses}`}>
|
||||
<div class="p-5 text-center">
|
||||
<span class="card-text-description block font-bold text-xs uppercase tracking-widest">
|
||||
{dayName}
|
||||
</span>
|
||||
<div class="flex justify-center my-2">
|
||||
<img
|
||||
src={`https://openweathermap.org/img/wn/${icon}@2x.png`}
|
||||
alt={label}
|
||||
class="card-hover-icon-scale h-12 w-12"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<span class="card-text-title card-hover-text-title block text-2xl">
|
||||
{temp}°F
|
||||
</span>
|
||||
<span class="card-text-description mt-1 block text-xs capitalize">
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
30
src/components/sections/ApplicationSection.astro
Normal file
30
src/components/sections/ApplicationSection.astro
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
import { readItems } from '@directus/sdk';
|
||||
|
||||
import type { Application } from '@lib/directusTypes';
|
||||
|
||||
import directus from '@lib/directus';
|
||||
import HighlightsCard from '@components/cards/HighlightsCard.astro';
|
||||
|
||||
const applications = ((await directus.request(
|
||||
readItems('site_applications' as any, {
|
||||
fields: ['*'],
|
||||
sort: ['-isActive'],
|
||||
})
|
||||
)) as unknown) as Application[];
|
||||
---
|
||||
|
||||
<section class:list={['mx-auto max-w-7xl px-4 py-10 sm:px-6 lg:px-8 lg:py-14', Astro.props.className]}>
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:gap-8 print:flex print:flex-col">
|
||||
{applications.map((application: Application) => (
|
||||
<HighlightsCard
|
||||
title={application.name}
|
||||
description={application.description}
|
||||
url={application.url}
|
||||
logoUrlLight={application.logoUrl}
|
||||
logoUrlDark={application.logoUrl}
|
||||
highlights={application.highlights}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
65
src/components/sections/EducationSection.astro
Normal file
65
src/components/sections/EducationSection.astro
Normal file
@@ -0,0 +1,65 @@
|
||||
---
|
||||
import { readItems } from '@directus/sdk';
|
||||
|
||||
import type { Education, Certificate} from '@lib/directusTypes';
|
||||
|
||||
import directus from '@lib/directus';
|
||||
import { getDirectusImageURL } from '@lib/directusFunctions';
|
||||
import EducationCard from '@components/cards/EducationCard.astro';
|
||||
|
||||
const educations = ((await directus.request(
|
||||
readItems('site_education' as any, {
|
||||
fields: ['*'],
|
||||
sort: ['-graduationDate'],
|
||||
})
|
||||
)) as unknown) as Education[];
|
||||
|
||||
const certificates = ((await directus.request(
|
||||
readItems('site_certificate' as any, {
|
||||
fields: ['*'],
|
||||
sort: ['-issuerDate'],
|
||||
})
|
||||
)) as unknown) as Certificate[];
|
||||
---
|
||||
|
||||
<section class:list={['flex flex-col gap-4', Astro.props.className]}>
|
||||
<h3 class="smooth-reveal card-text-header flex relative items-center w-full gap-3 pb-5">
|
||||
Education
|
||||
</h3>
|
||||
<div class="mx-8">
|
||||
<h4 class="smooth-reveal card-text-header-minor pt-5">
|
||||
College
|
||||
</h4>
|
||||
<div class="grid md:grid-cols-2 sm:grid-cols-1 gap-4 py-3">
|
||||
{educations.map((education: Education) => (
|
||||
<EducationCard
|
||||
topic={education.institution}
|
||||
area={education.area}
|
||||
date={education.graduationDate}
|
||||
url={education.url}
|
||||
logoUrlLight={getDirectusImageURL(education.logo)}
|
||||
logoUrlDark={getDirectusImageURL(education.logoDark)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{certificates.length > 0 && (
|
||||
<div class="mx-8">
|
||||
<h4 class="smooth-reveal card-text-header-minor pt-8">
|
||||
Certificates
|
||||
</h4>
|
||||
<div class="grid md:grid-cols-2 sm:grid-cols-1 gap-4 py-3">
|
||||
{certificates.map((certificate: Certificate) => (
|
||||
<EducationCard
|
||||
topic={certificate.name}
|
||||
area={certificate.issuer}
|
||||
date={certificate.issuerDate}
|
||||
url={certificate.url}
|
||||
logoIcon={certificate.logoName}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
159
src/components/sections/ExperienceSection.astro
Normal file
159
src/components/sections/ExperienceSection.astro
Normal 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>
|
||||
@@ -2,15 +2,13 @@
|
||||
import { readSingleton } from '@directus/sdk';
|
||||
|
||||
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'));
|
||||
---
|
||||
|
||||
<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="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="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="flex flex-wrap gap-6 sm:grid-cols-2 sm:gap-6 lg:grid-cols-3 justify-center">
|
||||
<FeaturesCard
|
||||
@@ -1,22 +1,20 @@
|
||||
---
|
||||
import GiteaBtn from '@components/ui/buttons/GiteaBtn.astro';
|
||||
|
||||
const { title, subTitle, url } = Astro.props;
|
||||
const btnTitle = 'Continue to Gitea';
|
||||
import GiteaButton from '@components/buttons/GiteaButton.astro';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
subTitle?: 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">
|
||||
<div
|
||||
class="smooth-reveal absolute top-[55%] left-0 scale-90 md:top-[20%] xl:top-[25%] xl:left-[10%]"
|
||||
>
|
||||
<!-- Animated shapes -->
|
||||
<div class="smooth-reveal absolute top-[55%] left-0 scale-90 md:top-[20%] xl:top-[25%] xl:left-[10%]">
|
||||
<svg
|
||||
class="animate-hover animate-hover-1"
|
||||
class="gitea-animate-hover gitea-animate-hover-1"
|
||||
width="64"
|
||||
height="64"
|
||||
fill="none"
|
||||
@@ -46,7 +44,7 @@ interface Props {
|
||||
</div>
|
||||
<div class="smooth-reveal absolute top-0 left-[85%] scale-75">
|
||||
<svg
|
||||
class="animate-hover animate-hover-2"
|
||||
class="gitea-animate-hover gitea-animate-hover-2"
|
||||
width="64"
|
||||
height="64"
|
||||
fill="none"
|
||||
@@ -80,11 +78,9 @@ interface Props {
|
||||
d="M10.5 19H9M15 19h-1.5"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="smooth-reveal absolute bottom-[5%] left-[60%] scale-[.6] xl:bottom-[15%] xl:left-[35%]"
|
||||
>
|
||||
<div class="smooth-reveal absolute bottom-[5%] left-[60%] scale-[.6] xl:bottom-[15%] xl:left-[35%]">
|
||||
<svg
|
||||
class="animate-hover animate-hover-3"
|
||||
class="gitea-animate-hover gitea-animate-hover-3"
|
||||
width="64"
|
||||
height="64"
|
||||
fill="none"
|
||||
@@ -106,59 +102,54 @@ interface Props {
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
<!-- Hero Section Heading -->
|
||||
<!-- Heading -->
|
||||
<div class="smooth-reveal-2 mx-auto mt-5 max-w-xl text-center">
|
||||
<h2
|
||||
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"
|
||||
>
|
||||
<h1 class="card-text-header block">
|
||||
{title}
|
||||
</h2>
|
||||
</h1>
|
||||
</div>
|
||||
<!-- Hero Section Sub-heading -->
|
||||
<!-- Sub-heading -->
|
||||
<div class="smooth-reveal-2 mx-auto mt-5 max-w-3xl text-center">
|
||||
{
|
||||
subTitle && (
|
||||
<p class="text-lg text-pretty text-neutral-600 dark:text-neutral-400">{subTitle}</p>
|
||||
)
|
||||
}
|
||||
{subTitle && (
|
||||
<p class="card-text-header-description">
|
||||
{subTitle}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<!-- Github Button -->
|
||||
{
|
||||
url && (
|
||||
<div class="smooth-reveal-2 mt-8 flex justify-center gap-3">
|
||||
<GiteaBtn url={url} title={btnTitle} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<!-- Gitea Button -->
|
||||
{url && (
|
||||
<div class="smooth-reveal-2 flex justify-center mt-8 gap-3">
|
||||
<GiteaButton url={url}/>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<style>
|
||||
@keyframes animate-hover {
|
||||
@keyframes gitea-animate-hover {
|
||||
from {
|
||||
transform: translateY(15px);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateY(-15px);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-hover {
|
||||
animation: animate-hover ease-in-out;
|
||||
.gitea-animate-hover {
|
||||
animation: gitea-animate-hover ease-in-out;
|
||||
|
||||
animation-iteration-count: infinite;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
|
||||
.animate-hover-1 {
|
||||
.gitea-animate-hover-1 {
|
||||
animation-duration: 5s;
|
||||
}
|
||||
|
||||
.animate-hover-2 {
|
||||
.gitea-animate-hover-2 {
|
||||
animation-duration: 5.5s;
|
||||
}
|
||||
|
||||
.animate-hover-3 {
|
||||
.gitea-animate-hover-3 {
|
||||
animation-duration: 6s;
|
||||
}
|
||||
</style>
|
||||
31
src/components/sections/HeaderSection.astro
Normal file
31
src/components/sections/HeaderSection.astro
Normal 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>
|
||||
56
src/components/sections/HeroSection.astro
Normal file
56
src/components/sections/HeroSection.astro
Normal 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>
|
||||
@@ -1,8 +1,9 @@
|
||||
---
|
||||
import { readItems } from '@directus/sdk';
|
||||
|
||||
import directus from '@lib/directus';
|
||||
import type { Post } from '@lib/directusTypes';
|
||||
|
||||
import directus from '@lib/directus';
|
||||
import BlogCard from '@components/blog/BlogCard.astro';
|
||||
|
||||
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">
|
||||
<div class="mx-auto mb-10 max-w-2xl text-center lg:mb-14">
|
||||
<h1
|
||||
class="smooth-reveal block text-4xl font-bold text-neutral-800 md:text-5xl md:leading-tight lg:text-5xl dark:text-neutral-200"
|
||||
>
|
||||
<h1 class="smooth-reveal card-text-header block">
|
||||
Latest Posts
|
||||
</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 class="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{recentPosts.map((b) => <BlogCard post={b} />)}
|
||||
32
src/components/sections/ProjectSection.astro
Normal file
32
src/components/sections/ProjectSection.astro
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
import { readItems } from '@directus/sdk';
|
||||
|
||||
import type { Project } from '@lib/directusTypes';
|
||||
|
||||
import directus from '@lib/directus';
|
||||
import HighlightsCard from '@components/cards/HighlightsCard.astro';
|
||||
|
||||
const projects = ((await directus.request(
|
||||
readItems('site_projects' as any, {
|
||||
fields: ['*'],
|
||||
sort: ['-isActive'],
|
||||
})
|
||||
)) as unknown) as Project[];
|
||||
---
|
||||
|
||||
<section class:list={['flex flex-col gap-y-8', Astro.props.className]}>
|
||||
<h3 class="smooth-reveal card-text-header flex relative items-center w-full gap-3 pb-5">
|
||||
Projects
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:gap-8 print:flex print:flex-col">
|
||||
{projects.map((project: Project) => (
|
||||
<HighlightsCard
|
||||
title={project.name}
|
||||
description={project.description}
|
||||
url={project.source}
|
||||
highlights={project.highlights}
|
||||
visitSource={true}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
@@ -6,86 +6,63 @@ import type { Skill } from '@lib/directusTypes';
|
||||
|
||||
import directus from '@lib/directus';
|
||||
|
||||
const skills = await directus.request(
|
||||
readItems('site_skills', {
|
||||
const skills = ((await directus.request(
|
||||
readItems('site_skills' as any, {
|
||||
fields: ['*'],
|
||||
sort: ['-date_created'],
|
||||
})
|
||||
);
|
||||
|
||||
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';
|
||||
)) as unknown) as Skill[];
|
||||
---
|
||||
|
||||
<section class:list={['flex flex-col gap-4', Astro.props.className]}>
|
||||
<h3
|
||||
class="relative flex w-full items-center gap-3 pb-4 text-5xl text-neutral-800 dark:text-neutral-200"
|
||||
>
|
||||
<h3 class="smooth-reveal card-text-header flex relative items-center w-full gap-3 pb-5">
|
||||
Skills
|
||||
</h3>
|
||||
<div class="">
|
||||
<div class="tech-stack-slider relative overflow-hidden py-4 sm:py-8">
|
||||
<!-- Main slider container -->
|
||||
<div class="slider-track animate-slide flex">
|
||||
{
|
||||
[...skills, ...skills, ...skills].map((skill: Skill) => {
|
||||
return (
|
||||
<div
|
||||
class={`skill-card transform rounded-xl transition-all duration-300 ${baseClasses} ${borderClasses} ${bgColorClasses} ${hoverClasses} ${shadowClasses}`}
|
||||
>
|
||||
<div class="p-4 sm:p-6">
|
||||
<div class="mb-4 flex items-center justify-between sm:mb-6">
|
||||
<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>
|
||||
{[...skills, ...skills, ...skills].map((skill: Skill) => {
|
||||
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 class="p-4 sm:p-6">
|
||||
<div class="flex items-center justify-between mb-4 sm:mb-6">
|
||||
<div class="flex items-center gap-2 sm:gap-4">
|
||||
<div class="flex items-center justify-center rounded-lg text-primary">
|
||||
<Icon name={skill.icon} class="h-8 w-8 sm:h-12 sm:w-12" />
|
||||
</div>
|
||||
<h3 class="text-neutral-900 dark:text-neutral-100 text-base font-semibold sm:text-xl">
|
||||
{skill.title}
|
||||
</h3>
|
||||
</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>
|
||||
|
||||
<!-- Gradient overlays -->
|
||||
<div
|
||||
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>
|
||||
<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 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"/>
|
||||
<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>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
document.addEventListener('astro:page-load', () => {
|
||||
// Create infinite scrolling effect
|
||||
function setupInfiniteScroll() {
|
||||
const cards = document.querySelectorAll('.skill-card');
|
||||
if (!cards.length) return;
|
||||
@@ -93,7 +70,6 @@ const shadowClasses = 'shadow-xs hover:shadow-lg';
|
||||
|
||||
setupInfiniteScroll();
|
||||
|
||||
// Add hover effects to cards - only on non-touch devices
|
||||
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
||||
const cards = document.querySelectorAll('.skill-card');
|
||||
|
||||
@@ -144,7 +120,7 @@ const shadowClasses = 'shadow-xs hover:shadow-lg';
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Tech Stack Slider */
|
||||
/* Specific css to enable sliding effect */
|
||||
.slider-track {
|
||||
width: fit-content;
|
||||
animation: scroll 40s linear infinite;
|
||||
@@ -155,7 +131,7 @@ const shadowClasses = 'shadow-xs hover:shadow-lg';
|
||||
transform: translateX(0);
|
||||
}
|
||||
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);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(calc(-280px * 6 - 32px * 6)); /* Card width + margin for desktop */
|
||||
transform: translateX(calc(-280px * 6 - 32px * 6));
|
||||
}
|
||||
}
|
||||
}
|
||||
36
src/components/sections/WeatherSection.astro
Normal file
36
src/components/sections/WeatherSection.astro
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
import WeatherCard from '@components/cards/WeatherCard.astro';
|
||||
import { getFiveDayForecast } from '@support/weather';
|
||||
|
||||
const { latitude = "44.95", longitude = "-93.09", cityName = "St. Paul, Minnesota", timezone = "America/Chicago" } = Astro.props;
|
||||
const { forecastDays, error } = await getFiveDayForecast(latitude, longitude, timezone);
|
||||
---
|
||||
|
||||
<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">
|
||||
<h1 class="smooth-reveal card-text-header block">
|
||||
Weather in my Area
|
||||
</h1>
|
||||
<div class="smooth-reveal mx-auto mt-5 max-w-3xl text-center">
|
||||
<span class="card-text-header-description">
|
||||
Five day forecast for {cityName}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{error ? (
|
||||
<div class="card-base p-10 text-accent text-center">
|
||||
{error}
|
||||
</div>
|
||||
) : (
|
||||
<div class="flex flex-wrap justify-center gap-4 lg:gap-6">
|
||||
{forecastDays.map((forecastDay) => (
|
||||
<WeatherCard
|
||||
dayName={forecastDay.dayName}
|
||||
label={forecastDay.label}
|
||||
icon={forecastDay.icon}
|
||||
temp={forecastDay.temp}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -1,46 +0,0 @@
|
||||
---
|
||||
import { Icon } from 'astro-icon/components';
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
description?: string;
|
||||
url?: string;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
const { title, description, url, icon } = Astro.props;
|
||||
|
||||
const baseClasses = 'smooth-reveal-2 group group-hover flex flex-col ';
|
||||
const borderClasses = 'border border-neutral-100 dark:border-stone-500/20';
|
||||
const bgColorClasses =
|
||||
'bg-neutral-100/80 hover:bg-neutral-100 dark:bg-neutral-800/60 dark:hover:bg-neutral-800/90';
|
||||
const shadowClasses = 'shadow-xs hover:shadow-md dark:shadow-md dark:hover:shadow-lg';
|
||||
const sizeClasses = 'h-30 w-100 md:w-[300px]';
|
||||
---
|
||||
|
||||
<div class={`${baseClasses}`}>
|
||||
<a
|
||||
class={`rounded-xl duration-300 transition-all ${sizeClasses} ${borderClasses} ${bgColorClasses} ${shadowClasses}`}
|
||||
href={url}
|
||||
data-astro-prefetch
|
||||
>
|
||||
<div class="p-4 md:p-5">
|
||||
<div class="flex">
|
||||
<Icon
|
||||
name={icon}
|
||||
class="group-hover:text-steel dark:group-hover:text-bermuda h-6 w-6 text-neutral-600 transition-all duration-300 md:h-8 md:w-8 dark:text-neutral-200"
|
||||
/>
|
||||
<div class="ms-5 grow">
|
||||
<span
|
||||
class="group-hover:text-steel dark:group-hover:text-bermuda block text-lg font-bold text-neutral-600 transition-all duration-300 dark:text-neutral-300"
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
<span class="mt-1 block text-neutral-500 dark:text-neutral-400">
|
||||
{description}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
@@ -1,71 +0,0 @@
|
||||
---
|
||||
import { Icon } from 'astro-icon/components';
|
||||
import { readItems } from '@directus/sdk';
|
||||
import Logo from '@components/ui/logos/Logo.astro';
|
||||
|
||||
import type { Application } from '@lib/directusTypes';
|
||||
|
||||
import directus from '@lib/directus';
|
||||
|
||||
const applications = await directus.request(
|
||||
readItems('site_applications', {
|
||||
fields: ['*'],
|
||||
sort: ['-isActive'],
|
||||
})
|
||||
);
|
||||
|
||||
const baseClasses = 'smooth-reveal-cards rounded-xl flex flex-col group group-hover';
|
||||
const borderClasses = 'border border-neutral-100 dark:border-stone-500/20';
|
||||
const bgColorClasses =
|
||||
'bg-neutral-100/80 hover:bg-neutral-100 dark:bg-neutral-800/60 dark:hover:bg-neutral-800/90';
|
||||
const shadowClasses = 'shadow-xs hover:shadow-md dark:shadow-md dark:hover:shadow-lg';
|
||||
---
|
||||
|
||||
<section class:list={['flex flex-col gap-4', Astro.props.className]}>
|
||||
<div class="grid grid-cols-1 gap-3 md:grid-cols-2 print:flex print:flex-col">
|
||||
{
|
||||
applications.map((application: Application) => {
|
||||
return (
|
||||
<div class={`${baseClasses}`}>
|
||||
<a
|
||||
class={`rounded-xl transition-all duration-300 ${borderClasses} ${bgColorClasses} ${shadowClasses}`}
|
||||
href={application.url}
|
||||
>
|
||||
<div class="p-4 md:p-10">
|
||||
<div class="flex items-center">
|
||||
<Logo
|
||||
srcLight={application.logoUrl}
|
||||
srcDark={application.logoUrl}
|
||||
alt={`Logo of ${application.name}`}
|
||||
/>
|
||||
<h3 class="text-lg font-bold text-gray-800 dark:text-white ml-4">
|
||||
{application.name}
|
||||
</h3>
|
||||
</div>
|
||||
<p class="mt-2 text-gray-500 dark:text-neutral-400">{application.description}</p>
|
||||
<ul class="mt-1 flex list-disc flex-col gap-2 text-sm text-gray-500 dark:text-neutral-400 [&>li]:ml-4">
|
||||
{application.highlights.map((highlight) => {
|
||||
return <li class="marker:text-yellow-500">{highlight}</li>;
|
||||
})}
|
||||
</ul>
|
||||
<div class="ml-6 flex">
|
||||
<div
|
||||
class="group-hover relative inline-block gap-x-1 rounded-lg border border-transparent disabled:pointer-events-none disabled:opacity-50"
|
||||
>
|
||||
<div class="group-hover:text-steel dark:group-hover:text-bermuda transition-text relative z-10 mx-auto flex min-h-11 items-center text-sm font-semibold text-neutral-600 decoration-2 duration-300 sm:mx-0 sm:mt-4 dark:text-neutral-300">
|
||||
<span class="relative inline-block overflow-hidden"> Visit Page </span>
|
||||
<Icon
|
||||
name="mdi:keyboard-arrow-right"
|
||||
class="translate-y-0.5 transition duration-300 group-hover:translate-x-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
@@ -1,135 +0,0 @@
|
||||
---
|
||||
import { Icon } from 'astro-icon/components';
|
||||
import { readItems } from '@directus/sdk';
|
||||
import Logo from '@components/ui/logos/Logo.astro';
|
||||
|
||||
import type { Education } from '@lib/directusTypes';
|
||||
|
||||
import directus from '@lib/directus';
|
||||
import { getDirectusImageURL } from '@lib/directusFunctions';
|
||||
|
||||
const education = await directus.request(
|
||||
readItems('site_education', {
|
||||
fields: ['*'],
|
||||
sort: ['-graduationDate'],
|
||||
})
|
||||
);
|
||||
|
||||
const certificate = await directus.request(
|
||||
readItems('site_certificate', {
|
||||
fields: ['*'],
|
||||
sort: ['-issuerDate'],
|
||||
})
|
||||
);
|
||||
|
||||
const baseClasses = 'rounded-xl flex flex-col group group-hover';
|
||||
const borderClasses = 'border border-neutral-100 dark:border-stone-500/20';
|
||||
const bgColorClasses =
|
||||
'bg-neutral-100/80 hover:bg-neutral-100 dark:bg-neutral-800/60 dark:hover:bg-neutral-800/90';
|
||||
const shadowClasses = 'shadow-xs hover:shadow-md dark:shadow-md dark:hover:shadow-lg';
|
||||
---
|
||||
|
||||
<section class:list={['order-first flex flex-col gap-4', Astro.props.className]}>
|
||||
<h3
|
||||
class="smooth-reveal-1 relative flex w-full items-center gap-3 pb-5 text-5xl text-neutral-800 dark:text-neutral-200"
|
||||
>
|
||||
Education
|
||||
</h3>
|
||||
<div class="ml-8">
|
||||
<h4 class="smooth-reveal-1 pt-5 text-2xl font-semibold text-neutral-800 dark:text-neutral-200">
|
||||
College
|
||||
</h4>
|
||||
<ul class="space-y-4 py-3">
|
||||
<div class="grid md:grid-cols-2 sm:grid-cols-1 gap-4">
|
||||
{
|
||||
education.map(({ institution, area, url, graduationDate, logo, logoDark }) => {
|
||||
return (
|
||||
<div class="smooth-reveal-cards mt-4 rounded-xl">
|
||||
<a
|
||||
class={`p-4 md:p-6 transition-all duration-300 ${shadowClasses} ${bgColorClasses} ${baseClasses} ${borderClasses}`}
|
||||
href={url}
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<Logo
|
||||
srcLight={getDirectusImageURL(logo)}
|
||||
srcDark={getDirectusImageURL(logoDark)}
|
||||
alt={`Logo of ${institution}`}
|
||||
/>
|
||||
<h3 class="text-lg font-bold text-neutral-800 dark:text-neutral-200 ml-4">
|
||||
{institution}
|
||||
</h3>
|
||||
</div>
|
||||
<p class="ml-16 text-xs font-medium text-neutral-600 uppercase dark:text-neutral-400">
|
||||
{area} - {new Date(graduationDate).getFullYear()}
|
||||
</p>
|
||||
<div class="ml-6 flex">
|
||||
<div
|
||||
class="group-hover relative inline-block gap-x-1 rounded-lg border border-transparent disabled:pointer-events-none disabled:opacity-50"
|
||||
>
|
||||
<div class="group-hover:text-steel dark:group-hover:text-bermuda transition-text relative z-10 mx-auto flex min-h-11 items-center text-sm font-semibold text-neutral-600 decoration-2 duration-300 sm:mx-0 sm:mt-4 dark:text-neutral-300">
|
||||
<span class="relative inline-block overflow-hidden"> Visit Page </span>
|
||||
<Icon
|
||||
name="mdi:keyboard-arrow-right"
|
||||
class="translate-y-0.5 transition duration-300 group-hover:translate-x-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{
|
||||
certificate.length > 0 && (
|
||||
<div class="ml-8">
|
||||
<h4 class="smooth-reveal-1 pt-8 text-2xl font-semibold text-neutral-800 dark:text-neutral-200">
|
||||
Certificates
|
||||
</h4>
|
||||
<ul class="space-y-4 py-3">
|
||||
<div class="grid md:grid-cols-2 sm:grid-cols-1 gap-4">
|
||||
{certificate.map(({ name, issuer, issuerDate, url, logoName }) => {
|
||||
return (
|
||||
<div class="smooth-reveal-cards mt-4 rounded-xl">
|
||||
<a
|
||||
class={`p-4 md:p-6 transition-all duration-300 ${shadowClasses} ${bgColorClasses} ${baseClasses} ${borderClasses}`}
|
||||
href={url}
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<div class="text-neutral-800 dark:text-neutral-200">
|
||||
<Icon name={logoName} class="h-12 w-12" />
|
||||
</div>
|
||||
<h3 class="text-lg font-bold text-neutral-800 dark:text-neutral-200 ml-4">
|
||||
{name}
|
||||
</h3>
|
||||
</div>
|
||||
<p class="ml-16 text-xs font-medium text-neutral-600 uppercase dark:text-neutral-400">
|
||||
{issuer} - {new Date(issuerDate).getFullYear()}
|
||||
</p>
|
||||
<div class="ml-6 flex">
|
||||
<div
|
||||
class="group-hover relative inline-block gap-x-1 rounded-lg border border-transparent disabled:pointer-events-none disabled:opacity-50"
|
||||
>
|
||||
<div class="group-hover:text-steel dark:group-hover:text-bermuda transition-text relative z-10 mx-auto flex min-h-11 items-center text-sm font-semibold text-neutral-600 decoration-2 duration-300 sm:mx-0 sm:mt-4 dark:text-neutral-300">
|
||||
<span class="relative inline-block overflow-hidden"> Visit Page </span>
|
||||
<Icon
|
||||
name="mdi:keyboard-arrow-right"
|
||||
class="translate-y-0.5 transition duration-300 group-hover:translate-x-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</section>
|
||||
@@ -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 smooth-reveal-1 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-2 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -1,67 +0,0 @@
|
||||
---
|
||||
import { Icon } from 'astro-icon/components';
|
||||
import { readItems } from '@directus/sdk';
|
||||
|
||||
import type { Project } from '@lib/directusTypes';
|
||||
|
||||
import directus from '@lib/directus';
|
||||
|
||||
const projects = await directus.request(
|
||||
readItems('site_projects', {
|
||||
fields: ['*'],
|
||||
sort: ['-isActive'],
|
||||
})
|
||||
);
|
||||
|
||||
const baseClasses = 'smooth-reveal-cards rounded-xl flex flex-col group group-hover';
|
||||
const borderClasses = 'border border-neutral-100 dark:border-stone-500/20';
|
||||
const bgColorClasses =
|
||||
'bg-neutral-100/80 hover:bg-neutral-100 dark:bg-neutral-800/60 dark:hover:bg-neutral-800/90';
|
||||
const shadowClasses = 'shadow-xs hover:shadow-md dark:shadow-md dark:hover:shadow-lg';
|
||||
---
|
||||
|
||||
<section class:list={['flex flex-col gap-4', Astro.props.className]}>
|
||||
<h3
|
||||
class="relative flex w-full items-center gap-3 pb-10 text-5xl text-neutral-800 dark:text-neutral-200"
|
||||
>
|
||||
Projects
|
||||
</h3>
|
||||
<div class="ml-8 grid grid-cols-1 gap-3 md:grid-cols-2 print:flex print:flex-col">
|
||||
{
|
||||
projects.map((project: Project) => {
|
||||
return (
|
||||
<div class={`${baseClasses}`}>
|
||||
<a
|
||||
class={`rounded-xl transition-all duration-300 ${borderClasses} ${bgColorClasses} ${shadowClasses}`}
|
||||
href={project.source}
|
||||
>
|
||||
<div class="p-4 md:p-10">
|
||||
<h3 class="text-lg font-bold text-gray-800 dark:text-white">{project.name}</h3>
|
||||
<p class="mt-2 text-gray-500 dark:text-neutral-400">{project.description}</p>
|
||||
<ul class="mt-1 flex list-disc flex-col gap-2 text-sm text-gray-500 dark:text-neutral-400 [&>li]:ml-4">
|
||||
{project.highlights.map((highlight) => {
|
||||
return <li class="marker:text-yellow-500">{highlight}</li>;
|
||||
})}
|
||||
</ul>
|
||||
<div class="flex">
|
||||
<div
|
||||
class="group group-hover relative inline-block gap-x-1 rounded-lg border border-transparent disabled:pointer-events-none disabled:opacity-50"
|
||||
>
|
||||
<div class="group-hover:text-gitea-primary dark:group-hover:text-gitea-primary transition-text text-md relative z-10 mx-auto flex min-h-11 items-center font-semibold text-neutral-600 decoration-2 duration-300 sm:mx-0 sm:mt-4 dark:text-neutral-300">
|
||||
<Icon name="pajamas:gitea" />
|
||||
<span class="relative inline-block overflow-hidden ml-2"> Visit Source </span>
|
||||
<Icon
|
||||
name="mdi:keyboard-arrow-right"
|
||||
class="translate-y-0.5 transition duration-300 group-hover:translate-x-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
@@ -1,58 +0,0 @@
|
||||
---
|
||||
import { getFiveDayForecast } from '@support/weather';
|
||||
|
||||
const { latitude = "44.95", longitude = "-93.09", cityName = "St. Paul, Minnesota", timezone = "America/Chicago" } = Astro.props;
|
||||
const { forecastDays, error } = await getFiveDayForecast(latitude, longitude, timezone);
|
||||
|
||||
const borderClasses = 'border border-neutral-100 dark:border-stone-500/20';
|
||||
const bgColorClasses = 'bg-neutral-100/80 hover:bg-neutral-100 dark:bg-neutral-800/60 dark:hover:bg-neutral-800/90';
|
||||
const shadowClasses = 'shadow-xs hover:shadow-md dark:shadow-md dark:hover:shadow-lg';
|
||||
---
|
||||
|
||||
<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">
|
||||
<h1 class="smooth-reveal block text-4xl font-bold text-neutral-800 md:text-5xl md:leading-tight lg:text-5xl dark:text-neutral-200">
|
||||
Weather in my Area
|
||||
</h1>
|
||||
<div class="smooth-reveal mx-auto mt-5 max-w-3xl text-center">
|
||||
<p class="text-lg text-pretty text-neutral-600 dark:text-neutral-400">
|
||||
Five day forecast for {cityName}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error ? (
|
||||
<div class={`rounded-xl p-10 text-center text-yellow-500 ${borderClasses} ${bgColorClasses}`}>
|
||||
{error}
|
||||
</div>
|
||||
) : (
|
||||
<div class="flex flex-wrap justify-center gap-4 lg:gap-6">
|
||||
{forecastDays.map((day) => (
|
||||
<div class="smooth-reveal-2 group flex flex-col">
|
||||
<div class={`rounded-xl duration-300 transition-all w-32 md:w-44 ${borderClasses} ${bgColorClasses} ${shadowClasses}`}>
|
||||
<div class="p-4 text-center">
|
||||
<span class="block text-xs font-bold tracking-widest text-neutral-500 uppercase dark:text-neutral-400">
|
||||
{day.dayName}
|
||||
</span>
|
||||
<div class="flex justify-center my-2">
|
||||
<img
|
||||
src={`https://openweathermap.org/img/wn/${day.icon}@2x.png`}
|
||||
alt={day.label}
|
||||
class="h-12 w-12 drop-shadow-sm group-hover:scale-110 transition-transform duration-300"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<span class="group-hover:text-steel dark:group-hover:text-bermuda transition-all duration-300 block text-2xl font-bold text-neutral-600 dark:text-neutral-300">
|
||||
{day.temp}°F
|
||||
</span>
|
||||
<span class="mt-1 block text-xs text-neutral-500 dark:text-neutral-400 capitalize">
|
||||
{day.label}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
@@ -53,6 +53,12 @@ const normalizeTitle = !title ? global.name : `${title} | ${global.name}`;
|
||||
}
|
||||
window.localStorage.setItem('theme', theme);
|
||||
</script>
|
||||
<!-- Rybbit Tracking Snippet -->
|
||||
<script
|
||||
src="https://rybbit.alexlebens.dev/api/script.js"
|
||||
data-site-id={global.rybbit_site_id}
|
||||
defer
|
||||
></script>
|
||||
</head>
|
||||
<body class="bg-stone-200 selection:bg-yellow-400 selection:text-neutral-700 dark:bg-stone-700">
|
||||
<div class="mx-auto w-full max-w-(--breakpoint-2xl) grow px-4 sm:px-6 lg:px-8">
|
||||
|
||||
@@ -8,6 +8,7 @@ export type Global = {
|
||||
initials: string;
|
||||
email: string;
|
||||
site_url: string;
|
||||
rybbit_site_id: string;
|
||||
logo: string;
|
||||
portrait: string;
|
||||
portrait_alt: string;
|
||||
|
||||
@@ -3,8 +3,8 @@ import { readSingleton } from '@directus/sdk';
|
||||
|
||||
import directus from '@lib/directus';
|
||||
import BaseLayout from '@layouts/BaseLayout.astro';
|
||||
import PrimaryCTA from '@components/ui/buttons/PrimaryCTA.astro';
|
||||
import GoBack from '@/components/ui/buttons/GoBack.astro';
|
||||
import GoBackButton from '@/components/buttons/GoBackButton.astro';
|
||||
import GoHomeButton from '@/components/buttons/GoHomeButton.astro';
|
||||
|
||||
const global = await directus.request(readSingleton('site_global'));
|
||||
---
|
||||
@@ -61,8 +61,8 @@ const global = await directus.request(readSingleton('site_global'));
|
||||
<div
|
||||
class="smooth-reveal mt-10 flex flex-col items-center justify-center gap-4 sm:flex-row"
|
||||
>
|
||||
<GoBack title="Go Back" />
|
||||
<PrimaryCTA title="Return Home" url={global.site_url} noArrow addHome />
|
||||
<GoBackButton/>
|
||||
<GoHomeButton url={global.site_url} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,11 +3,12 @@ import { readSingleton } from '@directus/sdk';
|
||||
|
||||
import directus from '@lib/directus';
|
||||
import BaseLayout from '@layouts/BaseLayout.astro';
|
||||
import HeroSection from '@components/ui/sections/HeroSection.astro';
|
||||
import Experience from '@components/ui/sections/Experience.astro';
|
||||
import Projects from '@components/ui/sections/Projects.astro';
|
||||
import Skills from '@components/ui/sections/Skills.astro';
|
||||
import Education from '@components/ui/sections/Education.astro';
|
||||
import HeroSection from '@components/sections/HeroSection.astro';
|
||||
import ExperienceSection from '@components/sections/ExperienceSection.astro';
|
||||
import EducationSection from '@components/sections/EducationSection.astro';
|
||||
import ProjectSection from '@components/sections/ProjectSection.astro';
|
||||
import SkillsSliderSection from '@components/sections/SkillsSliderSection.astro';
|
||||
|
||||
import portraitImg from '@images/portrait.avif';
|
||||
|
||||
const global = await directus.request(readSingleton('site_global'));
|
||||
@@ -32,6 +33,7 @@ const global = await directus.request(readSingleton('site_global'));
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
||||
<HeroSection
|
||||
title="About Me"
|
||||
subTitle={global.about}
|
||||
@@ -40,23 +42,21 @@ const global = await directus.request(readSingleton('site_global'));
|
||||
rounded={true}
|
||||
/>
|
||||
|
||||
<section class="mx-auto max-w-340 px-4 py-10 sm:px-6 lg:px-8 lg:py-14">
|
||||
<main class="relative grid grid-cols-1 md:grid-cols-6 gap-12 p-8 md:p-16 xl:gap-24 max-w-7xl mx-auto">
|
||||
<div class="space-y-12 col-span-1 md:col-span-6">
|
||||
<Experience className="smooth-reveal-2" />
|
||||
<Education className="smooth-reveal-2 mt-30" />
|
||||
<Projects className="smooth-reveal-2 mt-30" />
|
||||
<Skills className="smooth-reveal-2 mt-30" />
|
||||
</div>
|
||||
</main>
|
||||
<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">
|
||||
<ExperienceSection className="smooth-reveal" />
|
||||
<EducationSection className="smooth-reveal" />
|
||||
<ProjectSection className="smooth-reveal" />
|
||||
<SkillsSliderSection className="smooth-reveal" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</BaseLayout>
|
||||
|
||||
<script>
|
||||
// Add smooth reveal animations for content after loading
|
||||
document.addEventListener('astro:page-load', () => {
|
||||
const animateContent = () => {
|
||||
// Animate group 1
|
||||
const smoothReveal = document.querySelectorAll('.smooth-reveal');
|
||||
smoothReveal.forEach((el, index) => {
|
||||
setTimeout(
|
||||
@@ -67,28 +67,6 @@ const global = await directus.request(readSingleton('site_global'));
|
||||
);
|
||||
});
|
||||
|
||||
// Animate group 2
|
||||
const smoothReveal2 = document.querySelectorAll('.smooth-reveal-2');
|
||||
smoothReveal2.forEach((el, index) => {
|
||||
setTimeout(
|
||||
() => {
|
||||
el.classList.add('animate-reveal');
|
||||
},
|
||||
200 + index * 250
|
||||
);
|
||||
});
|
||||
|
||||
// Animate topic cards with staggered delay
|
||||
const smoothRevealCards = document.querySelectorAll('.smooth-reveal-cards');
|
||||
smoothRevealCards.forEach((el, index) => {
|
||||
setTimeout(
|
||||
() => {
|
||||
el.classList.add('animate-reveal');
|
||||
},
|
||||
400 + index * 250
|
||||
);
|
||||
});
|
||||
|
||||
// Animate with just fade in with staggered delay
|
||||
const smoothRevealFade = document.querySelectorAll('.smooth-reveal-fade');
|
||||
smoothRevealFade.forEach((el, index) => {
|
||||
|
||||
@@ -3,8 +3,9 @@ import { readSingleton } from '@directus/sdk';
|
||||
|
||||
import directus from '@lib/directus';
|
||||
import BaseLayout from '@layouts/BaseLayout.astro';
|
||||
import HeroSection from '@components/ui/sections/HeroSection.astro';
|
||||
import Applications from '@components/ui/sections/Applications.astro';
|
||||
import HeroSection from '@components/sections/HeroSection.astro';
|
||||
import ApplicationSection from '@components/sections/ApplicationSection.astro';
|
||||
|
||||
import applicationImg from '@images/cedar_tree.png';
|
||||
|
||||
const global = await directus.request(readSingleton('site_global'));
|
||||
@@ -29,6 +30,7 @@ const global = await directus.request(readSingleton('site_global'));
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
||||
<HeroSection
|
||||
title="Applications"
|
||||
subTitle={global.about_applications}
|
||||
@@ -36,13 +38,8 @@ const global = await directus.request(readSingleton('site_global'));
|
||||
alt={global.applications_image_alt}
|
||||
/>
|
||||
|
||||
<section class="mx-auto max-w-340 px-4 sm:px-6 lg:px-8 lg:py-14 pb-10">
|
||||
<main class="relative grid grid-cols-1 md:grid-cols-6 gap-12 p-2 md:p-16 xl:gap-24 max-w-7xl mx-auto">
|
||||
<div class="space-y-12 col-span-1 md:col-span-6">
|
||||
<Applications className="smooth-reveal-2" />
|
||||
</div>
|
||||
</main>
|
||||
</section>
|
||||
<ApplicationSection className="smooth-reveal-2" />
|
||||
|
||||
</BaseLayout>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { createHighlighter } from 'shiki';
|
||||
import { getDirectusImageURL } from '@lib/directusFunctions';
|
||||
import BaseLayout from '@layouts/BaseLayout.astro';
|
||||
import Image from '@components/ui/images/Image.astro';
|
||||
import SocialShareButton from '@components/buttons/SocialShareButton.astro';
|
||||
import { formatDateTime } from '@support/time';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
@@ -162,6 +163,9 @@ const content = marked.parse(post.content);
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<SocialShareButton
|
||||
pageTitle={post.title}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@ import directus from '@lib/directus';
|
||||
import BaseLayout from '@layouts/BaseLayout.astro';
|
||||
import BlogSelectedArticles from '@components/blog/BlogSelectedArticles.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';
|
||||
|
||||
const global = await directus.request(readSingleton('site_global'));
|
||||
|
||||
@@ -6,7 +6,7 @@ import directus from '@lib/directus';
|
||||
import type { Post } from '@lib/directusTypes';
|
||||
import BaseLayout from '@layouts/BaseLayout.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() {
|
||||
const categories = await getCollection('categories');
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { Post } from '@lib/directusTypes';
|
||||
import directus from '@lib/directus';
|
||||
import BaseLayout from '@layouts/BaseLayout.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 categoryImg from '@images/autumn_bench.png';
|
||||
|
||||
|
||||
@@ -3,11 +3,12 @@ import { readSingleton } from '@directus/sdk';
|
||||
|
||||
import directus from '@lib/directus';
|
||||
import BaseLayout from '@layouts/BaseLayout.astro';
|
||||
import HeroSection from '@components/ui/sections/HeroSection.astro';
|
||||
import FeaturesSection from '@components/ui/sections/FeaturesSection.astro';
|
||||
import WeatherSection from '@components/ui/sections/WeatherSection.astro';
|
||||
import LatestPosts from '@components/ui/sections/LatestPosts.astro';
|
||||
import HeroSectionAlt from '@components/ui/sections/HeroSectionAlt.astro';
|
||||
import HeroSection from '@components/sections/HeroSection.astro';
|
||||
import FeaturesSection from '@components/sections/FeaturesSection.astro';
|
||||
import WeatherSection from '@components/sections/WeatherSection.astro';
|
||||
import LatestPostsSection from '@components/sections/LatestPostsSection.astro';
|
||||
import GiteaSection from '@components/sections/GiteaSection.astro';
|
||||
|
||||
import homeImg from '@images/autumn_mountain.png';
|
||||
|
||||
const global = await directus.request(readSingleton('site_global'));
|
||||
@@ -53,9 +54,9 @@ const weather = await directus.request(readSingleton('site_weather'));
|
||||
>
|
||||
</WeatherSection>
|
||||
|
||||
<LatestPosts />
|
||||
<LatestPostsSection />
|
||||
|
||||
<HeroSectionAlt
|
||||
<GiteaSection
|
||||
title="Follow me on Gitea"
|
||||
subTitle="I love open source and have my code availabile on my Gitea server."
|
||||
url="https://gitea.alexlebens.dev"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
@import 'tailwindcss';
|
||||
@import 'preline/variants.css';
|
||||
@import './utilities.css';
|
||||
|
||||
@plugin '@tailwindcss/typography';
|
||||
@plugin '@tailwindcss/forms';
|
||||
|
||||
@@ -10,13 +12,27 @@
|
||||
/* Custom colors */
|
||||
@theme {
|
||||
--color-midnight: #0c354d;
|
||||
--color-turquoise: #0da797;
|
||||
--color-ocean: #134e70;
|
||||
|
||||
--color-cobalt: #6c9cb0;
|
||||
--color-steel: #4682b4;
|
||||
|
||||
--color-turquoise: #0da797;
|
||||
--color-bermuda: #7fbab4;
|
||||
|
||||
--color-desert: #f9deb2;
|
||||
--color-bronze: #9e7f5e;
|
||||
|
||||
--color-gitea-primary: #609926;
|
||||
--color-gitea-secondary: #4c7a33;
|
||||
|
||||
--color-main: light-dark(var(--color-steel), var(--color-bermuda));
|
||||
--color-accent: light-dark(var(--color-bronze), var(--color-desert));
|
||||
|
||||
--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-hover: light-dark(var(--color-neutral-800), var(--color-neutral-400));
|
||||
--color-secondary: light-dark(var(--color-neutral-500), var(--color-neutral-400));
|
||||
}
|
||||
|
||||
@layer base {
|
||||
@@ -24,6 +40,11 @@
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
--theme-transition: 0.3s ease;
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
:root:where(.dark, .dark *) {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
*,
|
||||
|
||||
110
src/styles/utilities.css
Normal file
110
src/styles/utilities.css
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user