first commit

This commit is contained in:
EFEELE
2025-04-07 15:50:13 -06:00
commit c2421d79c5
124 changed files with 12129 additions and 0 deletions

View File

@@ -0,0 +1,166 @@
---
type ButtonVariant = "default" | "big" | "dark";
interface Props {
link: string;
text: string;
iconName?: string;
variant?: ButtonVariant;
ariaLabel?: string;
disabled?: boolean;
isLoading?: boolean;
isExternal?: boolean;
isActive?: boolean;
class?: string;
}
const {
link,
text,
iconName,
variant = "default",
ariaLabel,
disabled = false,
isLoading = false,
isExternal = false,
isActive = false,
class: className = ""
} = Astro.props as Props;
import { Icon } from "astro-icon/components";
// Common base classes for all buttons
const baseClasses = `
z-2
text-center
cursor-pointer
leading-none
hover:scale-110
w-fit
font-medium
flex
gap-2
transition-all
ease-in-out
justify-center
items-center
rounded-full
disabled:opacity-50
disabled:cursor-not-allowed
disabled:hover:scale-100
disabled:hover:bg-none
disabled:hover:shadow-none
aria-disabled:opacity-50
aria-disabled:cursor-not-allowed
aria-disabled:hover:scale-100
aria-disabled:hover:bg-none
aria-disabled:hover:shadow-none
`;
// Specific classes for each variant
const variantClasses = {
default: `
text-blacktext/90
dark:text-mint-50
dark:hover:text-white
px-6
py-4
max-xl:px-5
max-sm:py-2
max-sm:px-3
text-lg
max-xl:text-base
max-sm:text-sm
hover:bg-gradient-to-l
bg-gradient-to-r
from-riptide-300
to-mint-300
dark:from-riptide-500
dark:to-mint-500
`,
big: `
font-normal
gap-3
max-md:gap-1
text-blacktext
dark:text-mint-50
dark:hover:text-white
px-8
max-sm:py-3
py-5
max-xl:px-6
max-sm:px-3
text-2xl
max-xl:text-xl
max-sm:text-lg
hover:bg-gradient-to-l
bg-gradient-to-r
from-riptide-200
to-mint-200
dark:from-riptide-500
dark:to-mint-500
`,
dark: `
group
dark:text-mint-50
text-blacktext
dark:hover:text-white
px-6
py-4
max-xl:px-5
max-sm:py-2
max-sm:px-3
text-lg
max-xl:text-base
max-sm:text-sm
dark:bg-zinc-800
bg-white
dark:hover:bg-mint-500
`,
};
// Icon classes for each variant
const iconClasses = {
default: "size-5",
big: "size-5",
dark: "size-5 dark:text-mint-300 text-blacktext group-hover:text-blacktext dark:group-hover:text-white transition-colors",
};
// Combine base classes with variant-specific classes
const buttonClasses = `${baseClasses} ${variantClasses[variant]}`;
// Generate aria-label if not provided
const buttonAriaLabel = ariaLabel || (iconName ? `${text} ${iconName}` : text);
// Determine if it's an external link
const isExternalLink = isExternal || link.startsWith('http');
// Additional attributes for external links
const externalAttributes = isExternalLink ? {
target: '_blank',
rel: 'noopener noreferrer'
} : {};
// Loading state
const loadingText = isLoading ? 'Loading...' : text;
const loadingAriaLabel = isLoading ? `${buttonAriaLabel} - Loading` : buttonAriaLabel;
---
<div
class="w-fit h-fit from-transparent via-riptide-300 to-transparent dark:from-transparent dark:via-mint-500 dark:to-transparent from-40% to-60% animate-rotate-border bg-conic/[from_var(--border-angle)] p-px hover:shadow-lg
hover:shadow-mint-500/30 rounded-full"
>
<a
class:list={[buttonClasses, className]}
href={disabled ? '#' : link}
aria-label={loadingAriaLabel}
role="button"
aria-disabled={disabled}
aria-busy={isLoading}
aria-pressed={isActive}
{...externalAttributes}
>
{iconName && <Icon name={iconName} class={iconClasses[variant]} aria-hidden="true" />}
{isLoading && <Icon name="mdi:loading" class="animate-spin" aria-hidden="true" />}
{loadingText}
</a>
</div>

View File

@@ -0,0 +1,87 @@
---
import { Icon } from "astro-icon/components";
import { getLanguage } from "../../utils/languages";
interface Props {
lang: string;
size?: "xs" | "md";
linkEnabled?: boolean;
}
const { size = "md", lang, linkEnabled = true } = Astro.props as Props;
const baseClasses = {
container: [
"flex items-center w-fit",
"pl-2 pr-2 py-0.5 gap-1",
"text-sm font-semibold leading-3",
"bg-white shadow rounded-full",
"transition-all duration-300 ease-in-out hover:bg-zinc-800 hover:text-white",
"max-sm:pl-1 max-sm:pr-1.5 max-sm:text-xs max-sm:gap-0.5",
].join(" "),
iconContainer: [
"flex items-center justify-center",
"aspect-square",
"bg-black rounded-full p-1",
].join(" "),
};
const sizeClasses = {
xs: "size-5",
md: "size-7",
};
const selectedLanguage = getLanguage(lang);
const getContainerClasses = () => {
const textSize = selectedLanguage.name.length > 10 ? "text-sm" : "text-base";
return `${baseClasses.container} ${textSize}`;
};
const getIconContainerClasses = () => {
return `${baseClasses.iconContainer} ${sizeClasses[size]} max-lg:size-6 max-sm:size-5 ${
selectedLanguage.className ? selectedLanguage.className : ""
}`;
};
---
{
linkEnabled ? (
<a
class="cursor-pointer"
href={`/blog/techs/${lang}`}
aria-label={`View articles about ${selectedLanguage.name}`}
role="link"
>
<span
class={getContainerClasses()}
role="presentation"
aria-hidden="true"
>
<div
class={getIconContainerClasses()}
role="img"
aria-label={`${selectedLanguage.name} icon`}
>
<Icon class="!w-full" name={selectedLanguage.iconName} />
</div>
{selectedLanguage.name}
</span>
</a>
) : (
<span
class={`${getContainerClasses()} cursor-default`}
role="presentation"
aria-hidden="true"
>
<div
class={getIconContainerClasses()}
role="img"
aria-label={`${selectedLanguage.name} icon`}
>
<Icon class="!w-full" name={selectedLanguage.iconName} />
</div>
{selectedLanguage.name}
</span>
)
}

View File

@@ -0,0 +1,35 @@
---
interface Props {
text?: string;
textGradient?: string;
level?: 1 | 2 | 3;
className?: string;
}
const { text = "", textGradient = "", level = 1, className = "" } = Astro.props;
const Tag = `h${level}`;
---
<Tag class:list={[
"font-extrabold text-4xl max-xl:text-3xl",
level === 2 && "max-md:text-3xl ",
level === 3 && "max-md:text-xl ",
"dark:text-white text-blacktext",
className
]}>
{text && (
<>
{text}
{textGradient && " "}
</>
)}
{textGradient && (
<b
class:list={[
"bg-gradient-to-r from-riptide-500 to-mint-400 dark:from-riptide-500 dark:to-mint-500 text-transparent bg-clip-text",
level === 1
]}
>{textGradient}</b>
)}
</Tag>

View File

@@ -0,0 +1,10 @@
---
const {class: className} = Astro.props;
import { Icon } from "astro-icon/components";
---
<div
class={`flex justify-start items-center gap-2 text-blacktext dark:text-mint-50 ${className || ''}`}
>
<span class="font-medium tracking-wider">Read more </span>
<Icon name="arrow-left" class="size-4 rotate-180" />
</div>

View File

@@ -0,0 +1,41 @@
---
import { Icon } from "astro-icon/components";
const { tweetText, currentUrl } = Astro.props;
const shareLinks = [
{
name: "Twitter",
icon: "twitter",
href: `https://twitter.com/intent/tweet?text=${tweetText}&url=${currentUrl}`,
},
{
name: "Facebook",
icon: "facebook",
href: `https://www.facebook.com/sharer/sharer.php?u=${currentUrl}`,
},
{
name: "WhatsApp",
icon: "whatsapp",
href: `https://api.whatsapp.com/send?text=${encodeURIComponent(`${currentUrl}`)}`,
},
];
---
<div class="flex items-center gap-5 text-lg" role="group" aria-label="Share on social media">
<span class="text-base font-bold text-blacktext dark:text-white">Share </span>
{
shareLinks.map((link) => (
<a
href={link.href}
class="bg-blacktext rounded-full p-2"
target="_blank"
rel="noopener noreferrer"
aria-label={`Share on ${link.name}`}
title={`Share on ${link.name}`}
>
<Icon name={link.icon} />
</a>
))
}
</div>

View File

@@ -0,0 +1,26 @@
---
import { Icon } from "astro-icon/components"
interface Props {
iconName: string;
link: string;
label?: string;
}
const { iconName, link, label = `Link to ${iconName}` } = Astro.props;
// Validate required props
if (!iconName || !link) {
throw new Error('Social component requires both iconName and link props');
}
---
<a
class="hover:text-mint-300 hover:scale-150 transition-all"
target="_blank"
href={link}
rel="noopener noreferrer"
aria-label={label}
>
<Icon name={iconName} aria-hidden="true" />
</a>

View File

@@ -0,0 +1,10 @@
---
const { tag, forceDark = false } = Astro.props;
---
<a
href={`/blog/tags/${tag}`}
class={`max-md:text-xs text-sm font-medium ${forceDark ? 'text-neutral-400 bg-zinc-800' : 'text-zinc-500 dark:text-neutral-400 hover:text-blacktext'} transition-all ease-in-out duration-300 px-4 py-1 max-md:px-3 rounded-full ${forceDark ? 'bg-zinc-800' : 'bg-mint-950/5 dark:bg-zinc-800 hover:bg-mint-200'}`}
>
{tag}
</a>

View File

@@ -0,0 +1,75 @@
---
import { Icon } from "astro-icon/components";
---
<button id="themeToggle" class="hover:cursor-pointer hover:text-mint-400 transition-all">
<Icon name="sun" class="sun"/>
<Icon name="moon" class="moon"/>
</button>
<style>
#themeToggle {
border: 0;
background: none;
}
.sun {
display: block;
}
.moon {
display: none;
}
:global(.dark) .sun {
display: none;
}
:global(.dark) .moon {
display: block;
}
</style>
<script is:inline>
// Execute immediately before the page loads
(function() {
const theme = (() => {
if (typeof localStorage !== "undefined" && localStorage.getItem("theme")) {
return localStorage.getItem("theme");
}
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
return "dark";
}
return "light";
})();
if (theme === "light") {
document.documentElement.classList.remove("dark");
} else {
document.documentElement.classList.add("dark");
}
window.localStorage.setItem("theme", theme);
})();
// Listen for changes in system preferences
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (e) => {
if (!localStorage.getItem("theme")) {
if (e.matches) {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
}
});
const handleToggleClick = () => {
const element = document.documentElement;
element.classList.toggle("dark");
const isDark = element.classList.contains("dark");
localStorage.setItem("theme", isDark ? "dark" : "light");
};
document
.getElementById("themeToggle")
.addEventListener("click", handleToggleClick);
</script>