first commit
This commit is contained in:
166
src/components/ui/Button.astro
Normal file
166
src/components/ui/Button.astro
Normal 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>
|
||||
87
src/components/ui/Capsule.astro
Normal file
87
src/components/ui/Capsule.astro
Normal 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>
|
||||
)
|
||||
}
|
||||
35
src/components/ui/Heading.astro
Normal file
35
src/components/ui/Heading.astro
Normal 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>
|
||||
10
src/components/ui/ReadMore.astro
Normal file
10
src/components/ui/ReadMore.astro
Normal 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>
|
||||
41
src/components/ui/Share.astro
Normal file
41
src/components/ui/Share.astro
Normal 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>
|
||||
26
src/components/ui/Social.astro
Normal file
26
src/components/ui/Social.astro
Normal 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>
|
||||
10
src/components/ui/Tag.astro
Normal file
10
src/components/ui/Tag.astro
Normal 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>
|
||||
75
src/components/ui/ThemeIcon.astro
Normal file
75
src/components/ui/ThemeIcon.astro
Normal 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>
|
||||
Reference in New Issue
Block a user