diff --git a/LICENSE b/LICENSE index 2a0515d8..c374c640 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 Mark Teekman +Copyright (c) 2021-2025 Incluud / Mark Teekman Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/astro.config.mjs b/astro.config.mjs index 2446eaf0..c2483cee 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -1,17 +1,16 @@ import { defineConfig } from 'astro/config' -import mdx from '@astrojs/mdx' +import { fileURLToPath } from 'url' import compress from 'astro-compress' import icon from 'astro-icon' +import mdx from '@astrojs/mdx' +import sitemap from '@astrojs/sitemap' import tailwindcss from '@tailwindcss/vite' -import { fileURLToPath } from 'url' - -import sitemap from '@astrojs/sitemap'; // https://astro.build/config export default defineConfig({ compressHTML: true, site: 'https://accessible-astro-starter.incluud.dev', - integrations: [mdx(), icon(), compress(), sitemap()], + integrations: [compress(), icon(), mdx(), sitemap()], vite: { css: { preprocessorOptions: { @@ -36,4 +35,4 @@ export default defineConfig({ }, }, }, -}) \ No newline at end of file +}) diff --git a/src/components/PageHeader.astro b/src/components/PageHeader.astro index 16256d1d..c34859aa 100644 --- a/src/components/PageHeader.astro +++ b/src/components/PageHeader.astro @@ -44,9 +44,40 @@ interface Props { * Whether to show the breadcrumbs */ showBreadcrumbs?: boolean + /** + * Custom breadcrumb labels to override the default URL-based labels + * @description Object mapping URL segments to their display labels + */ + customBreadcrumbLabels?: Record + + /** + * Whether to join the last breadcrumb + */ + joinLastBreadcrumb?: boolean } -const { class: className, title, subtitle, bgType, showBreadcrumbs = true, featuredImage, author } = Astro.props +const { + class: className, + title, + subtitle, + bgType, + showBreadcrumbs = true, + featuredImage, + author, + customBreadcrumbLabels, + joinLastBreadcrumb = false, +} = Astro.props + +// Helper function to format segment names +const formatSegment = (segment: string): string => { + if (customBreadcrumbLabels?.[segment]) { + return customBreadcrumbLabels[segment] + } + return segment + .split('-') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' ') +} // Sanitize subtitle HTML if present const sanitizedSubtitle: string = subtitle @@ -69,19 +100,29 @@ const sanitizedSubtitle: string = subtitle const segments = path.split('/').filter(Boolean) if (segments.length === 0) { - return null // No breadcrumbs on homepage + return null } return ( {segments.map((segment, index) => { + if (joinLastBreadcrumb && index === segments.length - 1) { + return null + } + const url = `/${segments.slice(0, index + 1).join('/')}` - const isLast = index === segments.length - 1 - const formattedName = segment - .split('-') - .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) - .join(' ') + const isLast = index === segments.length - 1 || (joinLastBreadcrumb && index === segments.length - 2) + + const formattedName = (() => { + if (joinLastBreadcrumb && isLast) { + const prevSegment = segments[segments.length - 2] + const lastSegment = segments[segments.length - 1] + return `${formatSegment(prevSegment)}: ${formatSegment(lastSegment)}` + } + + return formatSegment(segment) + })() return })} diff --git a/src/content.config.ts b/src/content.config.ts index 752e2adb..37286a96 100644 --- a/src/content.config.ts +++ b/src/content.config.ts @@ -11,6 +11,7 @@ const projects = defineCollection({ title: z.string(), author: z.string(), description: z.string(), + tags: z.array(z.string()).default([]), }), }) diff --git a/src/content/projects/project-01.mdx b/src/content/projects/project-01.mdx index a646d7ce..542dab65 100644 --- a/src/content/projects/project-01.mdx +++ b/src/content/projects/project-01.mdx @@ -2,6 +2,7 @@ title: Voice-First Smart Home Assistant author: John Doe description: A comprehensive smart home solution designed specifically for users with visual impairments, featuring voice-first interactions and haptic feedback +tags: ["Accessibility", "Smart-Home", "IoT"] --- ## Project Overview diff --git a/src/content/projects/project-02.mdx b/src/content/projects/project-02.mdx index 0d13643e..0c244199 100644 --- a/src/content/projects/project-02.mdx +++ b/src/content/projects/project-02.mdx @@ -2,6 +2,7 @@ title: Sign Language Bridge author: Jane Smith description: An AI-powered real-time sign language translation system that enables seamless communication between deaf and hearing individuals +tags: ["Accessibility", "AI", "Computer Vision"] --- ## Project Overview diff --git a/src/content/projects/project-03.mdx b/src/content/projects/project-03.mdx index f8251328..9f048ead 100644 --- a/src/content/projects/project-03.mdx +++ b/src/content/projects/project-03.mdx @@ -2,6 +2,7 @@ title: AccessGuard author: Alex Rivera description: A comprehensive web security solution that ensures both robust protection and WCAG 2.1 Level AAA compliance for enterprise websites +tags: ["Security", "Accessibility"] --- ## Project Overview diff --git a/src/content/projects/project-04.mdx b/src/content/projects/project-04.mdx index de7a61af..e613c43a 100644 --- a/src/content/projects/project-04.mdx +++ b/src/content/projects/project-04.mdx @@ -2,6 +2,7 @@ title: Accessible Learning Hub author: Sarah Johnson description: An inclusive e-learning platform that adapts to various learning disabilities and provides personalized learning experiences for all students +tags: ["Accessibility", "Education"] --- ## Project Overview diff --git a/src/content/projects/project-05.mdx b/src/content/projects/project-05.mdx index 32793887..51ca1fcc 100644 --- a/src/content/projects/project-05.mdx +++ b/src/content/projects/project-05.mdx @@ -2,6 +2,7 @@ title: FinanceForAll author: Viktor Nakamoto description: A fully accessible financial management platform that makes banking and investment tools available to users with various disabilities +tags: ["Accessibility", "Finance"] --- ## Project Overview diff --git a/src/content/projects/project-06.mdx b/src/content/projects/project-06.mdx index b89811ed..c09e8a6a 100644 --- a/src/content/projects/project-06.mdx +++ b/src/content/projects/project-06.mdx @@ -2,6 +2,7 @@ title: HealthAccess author: Dr. Rachel Zhang description: An accessible healthcare management system designed for patients with various disabilities to independently manage their medical care +tags: ["Accessibility", "Healthcare"] --- ## Project Overview diff --git a/src/content/projects/project-07.mdx b/src/content/projects/project-07.mdx index a5ebb1e3..eb5d0bb5 100644 --- a/src/content/projects/project-07.mdx +++ b/src/content/projects/project-07.mdx @@ -2,6 +2,7 @@ title: SmartHome Access author: Emma Green description: An inclusive smart home automation system that enables people with mobility impairments to control their entire home environment independently +tags: ["Accessibility", "Smart-Home", "IoT"] --- ## Project Overview diff --git a/src/layouts/DefaultLayout.astro b/src/layouts/DefaultLayout.astro index 3d207278..187ddc1a 100644 --- a/src/layouts/DefaultLayout.astro +++ b/src/layouts/DefaultLayout.astro @@ -1,9 +1,9 @@ --- import '../styles/tailwind.css' import '../assets/scss/index.scss' -import SiteMeta from '../components/SiteMeta.astro' -import Header from '../components/Header.astro' -import Footer from '../components/Footer.astro' +import SiteMeta from '@components/SiteMeta.astro' +import Header from '@components/Header.astro' +import Footer from '@components/Footer.astro' import { ClientRouter } from 'astro:transitions' const { diff --git a/src/pages/blog/[...page].astro b/src/pages/blog/[...page].astro index 28ce2e4b..95ab9b39 100644 --- a/src/pages/blog/[...page].astro +++ b/src/pages/blog/[...page].astro @@ -84,17 +84,21 @@ const { page } = Astro.props as Props )) } -
- -
+ { + page && page.lastPage > 1 && ( +
+ +
+ ) + } diff --git a/src/pages/portfolio/[...page].astro b/src/pages/portfolio/[...page].astro index d496697c..485fa0ef 100644 --- a/src/pages/portfolio/[...page].astro +++ b/src/pages/portfolio/[...page].astro @@ -1,10 +1,11 @@ --- import DefaultLayout from '@layouts/DefaultLayout.astro' import PageHeader from '@components/PageHeader.astro' -import { Card, Pagination } from 'accessible-astro-components' +import { Badge, Card, Heading, Link, Pagination } from 'accessible-astro-components' import { getCollection } from 'astro:content' import type { GetStaticPaths, Page } from 'astro' import type { CollectionEntry } from 'astro:content' +import { slugify } from '@/utils/slugify' // Import images directly for optimization import projectImage1 from '@assets/images/projects/project-image-1.png' @@ -16,6 +17,12 @@ import projectImage6 from '@assets/images/projects/project-image-6.png' export const getStaticPaths = (async ({ paginate }) => { const projects = await getCollection('projects') + const allTags = projects.map((project) => project.data.tags).flat() + const uniqueTagLabels = [...new Set(allTags)].sort((a, b) => a.localeCompare(b)) + const uniqueTags = uniqueTagLabels.map((label) => ({ + label, + slug: slugify(label), + })) const projectImages = [projectImage1, projectImage2, projectImage3, projectImage4, projectImage5, projectImage6] @@ -26,14 +33,20 @@ export const getStaticPaths = (async ({ paginate }) => { } }) - return paginate(projectsWithImages, { pageSize: 6 }) + return paginate(projectsWithImages, { + pageSize: 6, + props: { uniqueTags }, + }) }) satisfies GetStaticPaths interface Props { page: Page & { featuredImage: any }> + uniqueTags: { label: string; slug: string }[] } -const { page } = Astro.props +const { page, uniqueTags } = Astro.props + +console.log(page.lastPage) ---
-

Project {page.start + 1} through {page.end + 1} of {page.total} total projects

-
    + Tags +
    + { + uniqueTags.map((tag: { label: string; slug: string }) => ( + {tag.label} + )) + } +
    +

    + Project {page.start + 1} through {page.end + 1} of {page.total} total projects +

    +
      { page.data.map((project) => (
    • @@ -60,23 +83,32 @@ const { page } = Astro.props footer={'Author: ' + project.data.author} fullHeight={true} > + + {project.data.tags.map((tag) => ( + {tag} + ))} + {project.data.description}
    • )) }
    -
    - -
    + { + page && page.lastPage > 1 && ( +
    + +
    + ) + }
diff --git a/src/pages/portfolio/tag/[tag]/[...page].astro b/src/pages/portfolio/tag/[tag]/[...page].astro new file mode 100644 index 00000000..9dbeceb8 --- /dev/null +++ b/src/pages/portfolio/tag/[tag]/[...page].astro @@ -0,0 +1,149 @@ +--- +import DefaultLayout from '@layouts/DefaultLayout.astro' +import PageHeader from '@components/PageHeader.astro' +import { Badge, Card, Heading, Link, Pagination } from 'accessible-astro-components' +import { getCollection } from 'astro:content' +import type { GetStaticPaths, Page } from 'astro' +import type { CollectionEntry } from 'astro:content' +import { slugify } from '@/utils/slugify' + +// Import images directly for optimization +import projectImage1 from '@assets/images/projects/project-image-1.png' +import projectImage2 from '@assets/images/projects/project-image-2.png' +import projectImage3 from '@assets/images/projects/project-image-3.png' +import projectImage4 from '@assets/images/projects/project-image-4.png' +import projectImage5 from '@assets/images/projects/project-image-5.png' +import projectImage6 from '@assets/images/projects/project-image-6.png' + +export const getStaticPaths = (async ({ paginate }) => { + const projects = await getCollection('projects') + const allTags = projects.map((project) => project.data.tags).flat() + const uniqueTagLabels = [...new Set(allTags)].sort((a, b) => a.localeCompare(b)) + const uniqueTags = uniqueTagLabels.map((label) => ({ + label, + slug: slugify(label), + })) + + const projectImages = [projectImage1, projectImage2, projectImage3, projectImage4, projectImage5, projectImage6] + + const projectsWithImages = projects.map((project, index) => { + return { + ...project, + featuredImage: projectImages[index % projectImages.length], + } + }) + + // Generate paginated pages for each tag + const allPaginatedPages = [] + + for (const tag of uniqueTags) { + const filteredProjects = projectsWithImages.filter((project) => + project.data.tags.map((tag) => slugify(tag)).includes(tag.slug), + ) + + const paginatedPages = paginate(filteredProjects, { + pageSize: 6, + params: { tag: tag.slug }, + props: { + currentTag: tag, + uniqueTags, + }, + }) + + allPaginatedPages.push(...paginatedPages) + } + + return allPaginatedPages +}) satisfies GetStaticPaths + +interface Props { + page: Page & { featuredImage: any }> + currentTag: { label: string; slug: string } + uniqueTags: { label: string; slug: string }[] +} + +const { page, currentTag, uniqueTags } = Astro.props +--- + + + View all projects or filter by other tags below.`} + bgType="bordered" + customBreadcrumbLabels={{ + [currentTag.slug]: currentTag.label, + }} + joinLastBreadcrumb={true} + /> +
+
+ Tags +
+ All projects + { + uniqueTags.map((tag: { label: string; slug: string }) => { + const isCurrentTag = tag.slug === currentTag.slug + return isCurrentTag ? ( + + {tag.label} + + ) : ( + {tag.label} + ) + }) + } +
+

+ Project {page.start + 1} through {page.end + 1} of {page.total} total projects tagged with "{ + currentTag.label + }" +

+
    + { + page.data.map((project) => ( +
  • + + + {project.data.tags.map((tag) => { + const isCurrentTag = tag === currentTag.label + return isCurrentTag ? {tag} : {tag} + })} + + {project.data.description} + +
  • + )) + } +
+ + + { + page && page.lastPage > 1 && ( +
+ +
+ ) + } +
+
+
diff --git a/src/utils/slugify.ts b/src/utils/slugify.ts new file mode 100644 index 00000000..fe031a5f --- /dev/null +++ b/src/utils/slugify.ts @@ -0,0 +1,12 @@ +/** + * Convert a string to a URL-friendly slug + * @param text - The text to convert to a slug + * @returns A slugified string + */ +export function slugify(text: string): string { + return text + .toLowerCase() + .trim() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, '') +} diff --git a/tsconfig.json b/tsconfig.json index 7dc61629..1232276c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,9 +3,18 @@ "compilerOptions": { "baseUrl": ".", "paths": { - "@components/*": ["src/components/*"], - "@layouts/*": ["src/layouts/*"], - "@assets/*": ["src/assets/*"] + "@components/*": [ + "src/components/*" + ], + "@layouts/*": [ + "src/layouts/*" + ], + "@assets/*": [ + "src/assets/*" + ], + "@/utils/*": [ + "src/utils/*" + ] } } -} +} \ No newline at end of file