a11y
All checks were successful
Docker Deploy / build-and-push (push) Successful in 3m36s

This commit is contained in:
2025-06-12 16:16:38 -06:00
parent 2ad6585a07
commit df33b94e38
12 changed files with 284 additions and 336 deletions

View File

@ -101,9 +101,14 @@ export default function NavigationBar({ currentPath }: NavigationBarProps) {
return ( return (
<li key={item.id} class="mx-0.5 sm:mx-1"> <li key={item.id} class="mx-0.5 sm:mx-1">
<a href={item.path} class={isActive ? "menu-active" : ""}> <a
<div class="tooltip" data-tip={item.tooltip}> href={item.path}
class={`min-h-[44px] min-w-[44px] inline-flex items-center justify-center ${isActive ? "menu-active" : ""}`}
aria-label={item.tooltip}
>
<div class="tooltip md:before:-translate-x-1/2 md:before:-translate-y-full md:before:top-auto md:before:bottom-full md:after:-translate-x-1/2 md:after:-translate-y-full md:after:top-auto md:after:bottom-full" data-tip={item.tooltip}>
<Icon size={18} class="sm:w-5 sm:h-5" /> <Icon size={18} class="sm:w-5 sm:h-5" />
<span class="sr-only">{item.name}</span>
</div> </div>
</a> </a>
</li> </li>

View File

@ -11,7 +11,7 @@ const { slug } = post;
--- ---
<div class="card bg-accent shadow-lg w-full sm:w-[calc(50%-1rem)] md:w-96 min-w-[280px] max-w-sm shrink"> <div class="card bg-accent shadow-lg w-full sm:w-[calc(50%-1rem)] md:w-96 min-w-[280px] max-w-sm shrink">
<div class="card-body p-3"> <div class="card-body p-3 break-words">
<h2 class="card-title text-base-100 justify-center text-center break-words font-bold text-lg mb-2"> <h2 class="card-title text-base-100 justify-center text-center break-words font-bold text-lg mb-2">
{title} {title}
</h2> </h2>

View File

@ -10,7 +10,7 @@ const { project } = Astro.props;
--- ---
<div class="card bg-accent shadow-lg w-full sm:w-[calc(50%-1rem)] md:w-96 min-w-[280px] max-w-sm shrink"> <div class="card bg-accent shadow-lg w-full sm:w-[calc(50%-1rem)] md:w-96 min-w-[280px] max-w-sm shrink">
<div class="card-body p-6"> <div class="card-body p-6 break-words">
<h2 class="card-title text-xl md:text-2xl font-bold justify-center text-center break-words text-base-100"> <h2 class="card-title text-xl md:text-2xl font-bold justify-center text-center break-words text-base-100">
{project.name} {project.name}
</h2> </h2>

View File

@ -21,7 +21,6 @@ export default function ResumeSkills({ skills }: ResumeSkillsProps) {
entries.forEach((entry) => { entries.forEach((entry) => {
if (entry.isIntersecting && !hasAnimated.value) { if (entry.isIntersecting && !hasAnimated.value) {
hasAnimated.value = true; hasAnimated.value = true;
// Start animation for all skills
skills.forEach((skill) => { skills.forEach((skill) => {
animateSkill(skill.id, skill.level); animateSkill(skill.id, skill.level);
}); });
@ -67,19 +66,23 @@ export default function ResumeSkills({ skills }: ResumeSkillsProps) {
<div id="skills-section" class="grid grid-cols-1 md:grid-cols-2 gap-3 sm:gap-4"> <div id="skills-section" class="grid grid-cols-1 md:grid-cols-2 gap-3 sm:gap-4">
{skills.map((skill) => { {skills.map((skill) => {
const currentLevel = animatedLevels.value[skill.id] || 0; const currentLevel = animatedLevels.value[skill.id] || 0;
const progressValue = currentLevel * 20; // Convert 1-5 scale to 0-100 const progressValue = currentLevel * 20;
return ( return (
<div key={skill.id}> <div key={skill.id}>
<label class="label p-1 sm:p-2"> <div class="flex justify-between items-center p-1 sm:p-2">
<span class="label-text text-sm sm:text-base"> <span class="text-sm sm:text-base font-medium">
{skill.name} {skill.name}
</span> </span>
</label> <span class="text-xs sm:text-sm text-base-content/70">
{Math.round(currentLevel)}/5
</span>
</div>
<progress <progress
class="progress progress-primary w-full h-2 sm:h-3 transition-all duration-100 ease-out" class="progress progress-primary w-full h-2 sm:h-3 min-h-2 transition-all duration-100 ease-out"
value={progressValue} value={progressValue}
max="100" max="100"
aria-label={`${skill.name} skill level: ${Math.round(currentLevel)} out of 5`}
></progress> ></progress>
</div> </div>
); );

View File

@ -31,7 +31,7 @@ export default function ScrollUpButton() {
type="button" type="button"
onClick={scrollToTop} onClick={scrollToTop}
class={`fixed bottom-20 right-4 z-20 bg-secondary hover:bg-primary class={`fixed bottom-20 right-4 z-20 bg-secondary hover:bg-primary
p-3 rounded-full shadow-lg transition-all duration-300 p-3 rounded-full shadow-lg transition-all duration-300 min-h-[44px] min-w-[44px] inline-flex items-center justify-center
${ ${
isVisible.value isVisible.value
? "opacity-70 translate-y-0" ? "opacity-70 translate-y-0"

View File

@ -12,7 +12,7 @@ const { talk } = Astro.props;
<div <div
class="card bg-accent shadow-lg w-full sm:w-[calc(50%-1rem)] md:w-96 min-w-[280px] max-w-sm shrink" class="card bg-accent shadow-lg w-full sm:w-[calc(50%-1rem)] md:w-96 min-w-[280px] max-w-sm shrink"
> >
<div class="card-body p-6"> <div class="card-body p-6 break-words">
<h2 <h2
class="card-title text-xl md:text-2xl font-bold justify-center text-center break-words text-base-100" class="card-title text-xl md:text-2xl font-bold justify-center text-center break-words text-base-100"
> >

View File

@ -36,12 +36,6 @@ const Terminal = () => {
} }
}, [commandHistory]); }, [commandHistory]);
useEffect(() => {
if (inputRef.current) {
inputRef.current.focus();
}
}, []);
// Load command history from localStorage // Load command history from localStorage
useEffect(() => { useEffect(() => {
const history = loadCommandHistory(); const history = loadCommandHistory();
@ -219,7 +213,6 @@ __/ =| o |=-O=====O=====O=====O \\ ____Y___________|__|_________________________
} }
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
className="flex-1 bg-transparent border-none outline-none text-accent ml-1" className="flex-1 bg-transparent border-none outline-none text-accent ml-1"
autoFocus
spellcheck={false} spellcheck={false}
/> />
</form> </form>

View File

@ -289,7 +289,7 @@ export const navigationItems: NavigationItem[] = [
path: "/resume", path: "/resume",
tooltip: "Resume", tooltip: "Resume",
icon: BriefcaseBusiness, icon: BriefcaseBusiness,
enabled: true enabled: !!(resumeConfig.jsonFile && resumeConfig.jsonFile.trim())
}, },
{ {
id: "projects", id: "projects",

View File

@ -29,7 +29,7 @@ const pageDescription = description || siteConfig.meta.description;
<title>{pageTitle}</title> <title>{pageTitle}</title>
<ClientRouter /> <ClientRouter />
</head> </head>
<body class="flex flex-col min-h-screen"> <body class="flex flex-col min-h-screen overflow-x-hidden">
<main class="flex-grow flex flex-col gap-4 items-center justify-center pb-24 sm:pb-20"> <main class="flex-grow flex flex-col gap-4 items-center justify-center pb-24 sm:pb-20">
<slot /> <slot />
</main> </main>

View File

@ -12,6 +12,7 @@ import { personalInfo, homepageSections } from "../config/data";
alt={personalInfo.profileImage.alt} alt={personalInfo.profileImage.alt}
height={personalInfo.profileImage.height} height={personalInfo.profileImage.height}
width={personalInfo.profileImage.width} width={personalInfo.profileImage.width}
loading="eager"
/> />
<h1 <h1

View File

@ -63,11 +63,16 @@ interface ResumeData {
let resumeData: ResumeData | undefined = undefined; let resumeData: ResumeData | undefined = undefined;
let fetchError: string | null = null; let fetchError: string | null = null;
// Check if resume JSON file is configured before attempting to fetch
if (!siteConfig.resume.jsonFile || !siteConfig.resume.jsonFile.trim()) {
return Astro.redirect('/');
}
try { try {
// Get the base URL for the current request // Get the base URL
const baseUrl = Astro.url.origin; const baseUrl = Astro.url.origin;
// Fetch the JSON file from the public directory using config // Fetch the JSON file from the public directory
const response = await fetch(`${baseUrl}${siteConfig.resume.jsonFile}`); const response = await fetch(`${baseUrl}${siteConfig.resume.jsonFile}`);
if (!response.ok) { if (!response.ok) {
@ -91,31 +96,20 @@ try {
} }
} catch (error) { } catch (error) {
console.error("Error loading resume data:", error); console.error("Error loading resume data:", error);
fetchError = // Return to home page when resume data cannot be loaded
"Unable to load resume data. Please make sure the resume.json file exists in /public/files/"; return Astro.redirect('/');
resumeData = undefined;
} }
const data = resumeData; const data = resumeData;
const resumeConfig = siteConfig.resume; const resumeConfig = siteConfig.resume;
// At this point, data is guaranteed to exist since we redirect on error
if (!data) {
return Astro.redirect('/');
}
--- ---
{ <Layout title="Resume">
(!data || fetchError) && (
<Layout title="Resume">
<div class="container mx-auto p-4 sm:p-6 lg:p-8 max-w-4xl text-center w-full">
<h1 class="text-2xl font-bold text-red-600">
Error loading resume data.
</h1>
<p>{fetchError || "Please try refreshing the page."}</p>
</div>
</Layout>
)
}
{
data && !fetchError && (
<Layout title="Resume">
<div class="container mx-auto p-4 sm:p-6 lg:p-8 max-w-4xl w-full"> <div class="container mx-auto p-4 sm:p-6 lg:p-8 max-w-4xl w-full">
<h1 class="text-3xl sm:text-4xl font-bold mb-4 sm:mb-6 text-center"> <h1 class="text-3xl sm:text-4xl font-bold mb-4 sm:mb-6 text-center">
{data.basics.name} {data.basics.name}
@ -164,6 +158,7 @@ const resumeConfig = siteConfig.resume;
)} )}
</div> </div>
{resumeConfig.pdfFile?.path && (
<div class="text-center mb-6 sm:mb-8"> <div class="text-center mb-6 sm:mb-8">
<a <a
href={resumeConfig.pdfFile.path} href={resumeConfig.pdfFile.path}
@ -173,10 +168,11 @@ const resumeConfig = siteConfig.resume;
<Icon name="mdi:download" /> {resumeConfig.pdfFile.displayText} <Icon name="mdi:download" /> {resumeConfig.pdfFile.displayText}
</a> </a>
</div> </div>
)}
{data.sections.summary && resumeConfig.sections.summary?.enabled && ( {data.sections.summary && resumeConfig.sections.summary?.enabled && (
<div class="card bg-base-200 shadow-xl mb-4 sm:mb-6"> <div class="card bg-base-200 shadow-xl mb-4 sm:mb-6">
<div class="card-body p-4 sm:p-6"> <div class="card-body p-4 sm:p-6 break-words">
<h2 class="card-title text-xl sm:text-2xl"> <h2 class="card-title text-xl sm:text-2xl">
{resumeConfig.sections.summary.title || data.sections.summary.name || "Summary"} {resumeConfig.sections.summary.title || data.sections.summary.name || "Summary"}
</h2> </h2>
@ -190,7 +186,7 @@ const resumeConfig = siteConfig.resume;
data.sections.profiles.items.length > 0 && data.sections.profiles.items.length > 0 &&
resumeConfig.sections.profiles?.enabled && ( resumeConfig.sections.profiles?.enabled && (
<div class="card bg-base-200 shadow-xl mb-4 sm:mb-6"> <div class="card bg-base-200 shadow-xl mb-4 sm:mb-6">
<div class="card-body p-4 sm:p-6"> <div class="card-body p-4 sm:p-6 break-words">
<h2 class="card-title text-xl sm:text-2xl"> <h2 class="card-title text-xl sm:text-2xl">
{resumeConfig.sections.profiles.title || data.sections.profiles.name || "Profiles"} {resumeConfig.sections.profiles.title || data.sections.profiles.name || "Profiles"}
</h2> </h2>
@ -241,7 +237,7 @@ const resumeConfig = siteConfig.resume;
data.sections.skills.items.length > 0 && data.sections.skills.items.length > 0 &&
resumeConfig.sections.skills?.enabled && ( resumeConfig.sections.skills?.enabled && (
<div class="card bg-base-200 shadow-xl mb-4 sm:mb-6"> <div class="card bg-base-200 shadow-xl mb-4 sm:mb-6">
<div class="card-body p-4 sm:p-6"> <div class="card-body p-4 sm:p-6 break-words">
<h2 class="card-title text-xl sm:text-2xl"> <h2 class="card-title text-xl sm:text-2xl">
{resumeConfig.sections.skills.title || data.sections.skills.name || "Skills"} {resumeConfig.sections.skills.title || data.sections.skills.name || "Skills"}
</h2> </h2>
@ -258,7 +254,7 @@ const resumeConfig = siteConfig.resume;
data.sections.experience.items.length > 0 && data.sections.experience.items.length > 0 &&
resumeConfig.sections.experience?.enabled && ( resumeConfig.sections.experience?.enabled && (
<div class="card bg-base-200 shadow-xl mb-4 sm:mb-6"> <div class="card bg-base-200 shadow-xl mb-4 sm:mb-6">
<div class="card-body p-4 sm:p-6"> <div class="card-body p-4 sm:p-6 break-words">
<h2 class="card-title text-xl sm:text-2xl"> <h2 class="card-title text-xl sm:text-2xl">
{resumeConfig.sections.experience.title || data.sections.experience.name || "Experience"} {resumeConfig.sections.experience.title || data.sections.experience.name || "Experience"}
</h2> </h2>
@ -306,7 +302,7 @@ const resumeConfig = siteConfig.resume;
data.sections.education.items.length > 0 && data.sections.education.items.length > 0 &&
resumeConfig.sections.education?.enabled && ( resumeConfig.sections.education?.enabled && (
<div class="card bg-base-200 shadow-xl mb-4 sm:mb-6"> <div class="card bg-base-200 shadow-xl mb-4 sm:mb-6">
<div class="card-body p-4 sm:p-6"> <div class="card-body p-4 sm:p-6 break-words">
<h2 class="card-title text-xl sm:text-2xl"> <h2 class="card-title text-xl sm:text-2xl">
{resumeConfig.sections.education.title || data.sections.education.name || "Education"} {resumeConfig.sections.education.title || data.sections.education.name || "Education"}
</h2> </h2>
@ -345,7 +341,7 @@ const resumeConfig = siteConfig.resume;
data.sections.volunteer.items.length > 0 && data.sections.volunteer.items.length > 0 &&
resumeConfig.sections.volunteer?.enabled && ( resumeConfig.sections.volunteer?.enabled && (
<div class="card bg-base-200 shadow-xl mb-4 sm:mb-6"> <div class="card bg-base-200 shadow-xl mb-4 sm:mb-6">
<div class="card-body p-4 sm:p-6"> <div class="card-body p-4 sm:p-6 break-words">
<h2 class="card-title text-xl sm:text-2xl"> <h2 class="card-title text-xl sm:text-2xl">
{resumeConfig.sections.volunteer.title || data.sections.volunteer.name || "Volunteer Work"} {resumeConfig.sections.volunteer.title || data.sections.volunteer.name || "Volunteer Work"}
</h2> </h2>
@ -372,6 +368,4 @@ const resumeConfig = siteConfig.resume;
</div> </div>
)} )}
</div> </div>
</Layout> </Layout>
)
}

View File

@ -35,51 +35,3 @@
--depth: 1; --depth: 1;
--noise: 1; --noise: 1;
} }
/* Ensure better text scaling and overflow handling */
* {
/* Allow text to scale with user preferences */
text-size-adjust: 100%;
}
/* Prevent horizontal overflow on smaller screens or when zoomed */
body {
overflow-x: hidden;
}
/* Ensure links and buttons remain accessible at all zoom levels */
a, button {
min-height: 44px;
min-width: 44px;
display: inline-flex;
align-items: center;
justify-content: center;
}
/* Make sure card content doesn't overflow */
.card-body {
overflow-wrap: break-word;
word-break: break-word;
}
/* Ensure progress bars scale properly */
.progress {
min-height: 0.5rem;
}
/* Responsive navigation improvements */
@media (max-width: 640px) {
.menu-horizontal .menu li {
margin: 0 0.125rem;
}
}
/* Better tooltip positioning for mobile */
@media (max-width: 768px) {
.tooltip:before,
.tooltip:after {
transform: translateX(-50%) translateY(-100%);
top: auto;
bottom: 100%;
}
}