From 046efd60e91424466dda2cc37e6683284d977e1a Mon Sep 17 00:00:00 2001 From: cbenge509 Date: Fri, 16 Jan 2026 19:29:28 -0500 Subject: [PATCH] refactor: complete code simplification pass HIGH PRIORITY: - Fix PublicationCard class concatenation to use cn() consistently - Standardize interface Props declarations with export keyword - Add shared test fixtures for publications, patents, certifications, awards MEDIUM PRIORITY: - Extract ProjectData type from Astro content collection - Document scroll-reveal script pattern in CLAUDE.md LOW PRIORITY: - Review and remove custom line-clamp utilities (using Tailwind built-ins) - Document dual class composition approach (cn vs class:list) - Extract inline scripts to external files for deduplication Co-Authored-By: Claude Opus 4.5 --- src/components/AwardCard.astro | 2 +- src/components/CertificationCard.astro | 2 +- src/components/EducationCard.astro | 2 +- src/components/FeaturedProjectCard.astro | 10 +--- src/components/Hero.astro | 2 +- src/components/Navigation.astro | 2 +- src/components/PublicationCard.astro | 32 +--------- src/layouts/BaseLayout.astro | 2 +- src/scripts/featured-project-card.ts | 28 +++++++++ src/scripts/publication-card.ts | 42 +++++++++++++ src/styles/global.css | 39 ++---------- src/utils/project-categories.ts | 15 ++--- test/fixtures/props/award.ts | 75 ++++++++++++++++++++++++ test/fixtures/props/certification.ts | 47 +++++++++++++++ test/fixtures/props/index.ts | 4 ++ test/fixtures/props/patent.ts | 51 ++++++++++++++++ test/fixtures/props/publication.ts | 45 ++++++++++++++ 17 files changed, 310 insertions(+), 90 deletions(-) create mode 100644 src/scripts/featured-project-card.ts create mode 100644 src/scripts/publication-card.ts create mode 100644 test/fixtures/props/award.ts create mode 100644 test/fixtures/props/certification.ts create mode 100644 test/fixtures/props/patent.ts create mode 100644 test/fixtures/props/publication.ts diff --git a/src/components/AwardCard.astro b/src/components/AwardCard.astro index 8ce67405..1d086855 100644 --- a/src/components/AwardCard.astro +++ b/src/components/AwardCard.astro @@ -43,7 +43,7 @@ import {cn} from '../utils/cn'; * /> * ``` */ -interface Props extends HTMLAttributes<'article'> { +export interface Props extends HTMLAttributes<'article'> { /** Award or competition title */ title: string; /** Year received */ diff --git a/src/components/CertificationCard.astro b/src/components/CertificationCard.astro index 3143485c..45090528 100644 --- a/src/components/CertificationCard.astro +++ b/src/components/CertificationCard.astro @@ -25,7 +25,7 @@ import ExternalLink from './ExternalLink.astro'; * /> * ``` */ -interface Props extends HTMLAttributes<'article'> { +export interface Props extends HTMLAttributes<'article'> { /** Certification name */ name: string; /** Issuing organization */ diff --git a/src/components/EducationCard.astro b/src/components/EducationCard.astro index 043ebe5e..35aa5df9 100644 --- a/src/components/EducationCard.astro +++ b/src/components/EducationCard.astro @@ -26,7 +26,7 @@ import ExternalLink from './ExternalLink.astro'; * /> * ``` */ -interface Props extends HTMLAttributes<'article'> { +export interface Props extends HTMLAttributes<'article'> { /** Name of the institution */ institution: string; /** Degree type (e.g., M.S., B.S., MIDS) */ diff --git a/src/components/FeaturedProjectCard.astro b/src/components/FeaturedProjectCard.astro index dacf278d..3217fce1 100644 --- a/src/components/FeaturedProjectCard.astro +++ b/src/components/FeaturedProjectCard.astro @@ -111,13 +111,5 @@ const {project, slug, class: className, ...attrs} = Astro.props; diff --git a/src/components/Hero.astro b/src/components/Hero.astro index 76f415dd..e00aa760 100644 --- a/src/components/Hero.astro +++ b/src/components/Hero.astro @@ -21,7 +21,7 @@ import ExternalLink from './ExternalLink.astro'; * * ``` */ -interface Props extends HTMLAttributes<'section'> { +export interface Props extends HTMLAttributes<'section'> { /** Optional class name for additional styling */ class?: string; /** Whether to show the profile image (defaults to true) */ diff --git a/src/components/Navigation.astro b/src/components/Navigation.astro index 56f0de33..e5313a50 100644 --- a/src/components/Navigation.astro +++ b/src/components/Navigation.astro @@ -19,7 +19,7 @@ import {SOCIAL_LINKS} from '../data/profile'; * * ``` */ -interface Props extends HTMLAttributes<'header'> { +export interface Props extends HTMLAttributes<'header'> { /** Current page path for active link highlighting */ currentPath?: string; } diff --git a/src/components/PublicationCard.astro b/src/components/PublicationCard.astro index 449e4951..c04a3b81 100644 --- a/src/components/PublicationCard.astro +++ b/src/components/PublicationCard.astro @@ -2,6 +2,7 @@ import type {HTMLAttributes} from 'astro/types'; import {cardSurfaceContainerClasses, BULLET_SEPARATOR} from '../utils/card-styles'; import {toSlugId} from '../utils/id-utils'; +import {cn} from '../utils/cn'; import ExternalLink from './ExternalLink.astro'; /** @@ -64,7 +65,7 @@ const hasLinks = pdfUrl || codeUrl || doiUrl;
@@ -228,32 +229,5 @@ const hasLinks = pdfUrl || codeUrl || doiUrl; diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index 5d7d23b9..48fae832 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -27,7 +27,7 @@ import Footer from '../components/Footer.astro'; * * ``` */ -interface Props extends HTMLAttributes<'html'> { +export interface Props extends HTMLAttributes<'html'> { /** Page title (will have " | Cris Benge" appended) */ title: string; /** Meta description for SEO */ diff --git a/src/scripts/featured-project-card.ts b/src/scripts/featured-project-card.ts new file mode 100644 index 00000000..b8a9d3b4 --- /dev/null +++ b/src/scripts/featured-project-card.ts @@ -0,0 +1,28 @@ +/** + * Featured project card GitHub link handling. + * Stops propagation on GitHub links to prevent card navigation. + */ + +let initialized = false; + +function initFeaturedProjectCards(): void { + // Prevent multiple initializations + if (initialized) return; + initialized = true; + + // Use event delegation for efficiency + document.addEventListener('click', event => { + const target = event.target as HTMLElement; + const githubLink = target.closest('[data-github-link]'); + if (githubLink) { + event.stopPropagation(); + } + }); +} + +// Initialize on DOMContentLoaded +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initFeaturedProjectCards); +} else { + initFeaturedProjectCards(); +} diff --git a/src/scripts/publication-card.ts b/src/scripts/publication-card.ts new file mode 100644 index 00000000..7775bbfa --- /dev/null +++ b/src/scripts/publication-card.ts @@ -0,0 +1,42 @@ +/** + * Publication card abstract toggle functionality. + * Progressive enhancement: abstracts visible by default, JS collapses them initially. + */ + +function initPublicationCards(): void { + document + .querySelectorAll('[data-component="publication-card"]') + .forEach(card => { + const toggle = card.querySelector( + '[data-toggle]', + ) as HTMLButtonElement | null; + const abstract = card.querySelector( + '[data-abstract]', + ) as HTMLElement | null; + const toggleText = card.querySelector( + '[data-toggle-text]', + ) as HTMLElement | null; + + if (!toggle || !abstract || !toggleText) return; + + // Initially collapse abstracts (JS enhancement) + abstract.classList.remove('expanded'); + toggle.setAttribute('aria-expanded', 'false'); + toggleText.textContent = 'Show Abstract'; + + toggle.addEventListener('click', () => { + const isExpanded = toggle.getAttribute('aria-expanded') === 'true'; + + toggle.setAttribute('aria-expanded', String(!isExpanded)); + abstract.classList.toggle('expanded'); + toggleText.textContent = isExpanded ? 'Show Abstract' : 'Hide Abstract'; + }); + }); +} + +// Initialize on DOMContentLoaded +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initPublicationCards); +} else { + initPublicationCards(); +} diff --git a/src/styles/global.css b/src/styles/global.css index 7f249b9e..7cf64f12 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -541,45 +541,14 @@ (--spacing-section, --spacing-card-gap) remain available for direct use. */ /* ============================================================================= - 8. LINE CLAMP UTILITIES - Text Truncation + 8. LINE CLAMP UTILITIES - Now Using Tailwind Built-ins ============================================================================== - Multi-line text truncation using -webkit-line-clamp. - Provides consistent truncation behavior across card components. + Line-clamp utilities (line-clamp-1, line-clamp-2, line-clamp-3) are now + provided by Tailwind CSS v4 out of the box. No custom CSS needed. - Classes: - - .line-clamp-1: Truncate to 1 line - - .line-clamp-2: Truncate to 2 lines - - .line-clamp-3: Truncate to 3 lines - - Usage: -

Long text that will be truncated after 2 lines...

- - Browser Support: - - All modern browsers (Chrome, Firefox, Safari, Edge) - - Degrades gracefully to showing full text in unsupported browsers + Usage:

Long text...

============================================================================= */ -.line-clamp-1 { - display: -webkit-box; - -webkit-line-clamp: 1; - -webkit-box-orient: vertical; - overflow: hidden; -} - -.line-clamp-2 { - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; -} - -.line-clamp-3 { - display: -webkit-box; - -webkit-line-clamp: 3; - -webkit-box-orient: vertical; - overflow: hidden; -} - /* ============================================================================= 9. CARD ANIMATION UTILITIES ============================================================================== diff --git a/src/utils/project-categories.ts b/src/utils/project-categories.ts index 025c0687..e514404a 100644 --- a/src/utils/project-categories.ts +++ b/src/utils/project-categories.ts @@ -6,6 +6,8 @@ * consistent category-to-variant mapping and display labels. */ +import type {CollectionEntry} from 'astro:content'; + /** * Project category type from content collection schema. */ @@ -61,15 +63,6 @@ export const CATEGORY_LABELS: Record = { /** * Project data shape from content collection. * Used by FeaturedProjectCard and SecondaryProjectCard components. + * Derived from the projects collection schema for type safety. */ -export interface ProjectData { - title: string; - description: string; - image: ImageMetadata; - category: ProjectCategory; - skills: string[]; - tools: string[]; - githubUrl?: string; - achievement?: string; - affiliation?: string; -} +export type ProjectData = CollectionEntry<'projects'>['data']; diff --git a/test/fixtures/props/award.ts b/test/fixtures/props/award.ts new file mode 100644 index 00000000..aea2afd6 --- /dev/null +++ b/test/fixtures/props/award.ts @@ -0,0 +1,75 @@ +/** + * Shared test fixtures for award-related components + */ + +export interface MockImageMetadata { + src: string; + width: number; + height: number; + format: string; +} + +export interface MockAward { + title: string; + year: number; + category: 'competition' | 'professional'; + description: string; + placement?: string; + organization?: string; + logoImage?: MockImageMetadata; +} + +export const mockLogoImage: MockImageMetadata = { + src: '/mock-logo.svg', + width: 64, + height: 64, + format: 'svg', +}; + +export function createMockAward(overrides: Partial = {}): MockAward { + return { + title: 'Test Award', + year: 2020, + category: 'competition', + description: 'Test award description.', + ...overrides, + }; +} + +export const mockAwards = { + competition: createMockAward({ + category: 'competition', + placement: '1st Place', + organization: 'DrivenData', + }), + professional: createMockAward({ + category: 'professional', + organization: 'Microsoft', + }), + withLogo: createMockAward({ + category: 'professional', + organization: 'Microsoft', + logoImage: mockLogoImage, + }), + firstPlace: createMockAward({ + category: 'competition', + placement: '1st Place', + }), + secondPlace: createMockAward({ + category: 'competition', + placement: '2nd Place', + }), + thirdPlace: createMockAward({ + category: 'competition', + placement: '3rd Place', + }), + topPercent: createMockAward({ + category: 'competition', + placement: 'Top 12%', + }), + minimal: createMockAward({ + placement: undefined, + organization: undefined, + logoImage: undefined, + }), +}; diff --git a/test/fixtures/props/certification.ts b/test/fixtures/props/certification.ts new file mode 100644 index 00000000..b4553909 --- /dev/null +++ b/test/fixtures/props/certification.ts @@ -0,0 +1,47 @@ +/** + * Shared test fixtures for certification-related components + */ + +export interface MockCertification { + name: string; + issuer: string; + year: number; + category: 'cloud' | 'data' | 'database' | 'other'; + verificationUrl?: string; +} + +export function createMockCertification( + overrides: Partial = {}, +): MockCertification { + return { + name: 'Azure Certified Solution Architect Expert', + issuer: 'Microsoft', + year: 2023, + category: 'cloud', + ...overrides, + }; +} + +export const mockCertifications = { + cloud: createMockCertification({ + category: 'cloud', + }), + data: createMockCertification({ + name: 'Data Engineering Professional', + category: 'data', + }), + database: createMockCertification({ + name: 'Database Administrator', + category: 'database', + }), + other: createMockCertification({ + name: 'Project Management Professional', + category: 'other', + }), + withVerification: createMockCertification({ + verificationUrl: 'https://example.com/verify', + }), + minimal: createMockCertification({ + verificationUrl: undefined, + }), +}; diff --git a/test/fixtures/props/index.ts b/test/fixtures/props/index.ts index bc10845e..c253ccbe 100644 --- a/test/fixtures/props/index.ts +++ b/test/fixtures/props/index.ts @@ -5,3 +5,7 @@ export * from './project'; export * from './education'; +export * from './publication'; +export * from './patent'; +export * from './certification'; +export * from './award'; diff --git a/test/fixtures/props/patent.ts b/test/fixtures/props/patent.ts new file mode 100644 index 00000000..4e61cf4c --- /dev/null +++ b/test/fixtures/props/patent.ts @@ -0,0 +1,51 @@ +/** + * Shared test fixtures for patent-related components + */ + +export interface MockPatent { + title: string; + patentNumber: string; + filingDate: Date; + grantDate?: Date; + status: 'filed' | 'pending' | 'granted'; + url?: string; + description?: string; +} + +export function createMockPatent( + overrides: Partial = {}, +): MockPatent { + return { + title: 'Test Patent Title', + patentNumber: 'US 10,123,456', + filingDate: new Date('2020-01-15T12:00:00Z'), + status: 'granted', + ...overrides, + }; +} + +export const mockPatents = { + granted: createMockPatent({ + status: 'granted', + grantDate: new Date('2022-06-20T12:00:00Z'), + }), + pending: createMockPatent({ + status: 'pending', + grantDate: undefined, + }), + filed: createMockPatent({ + status: 'filed', + grantDate: undefined, + }), + withUrl: createMockPatent({ + url: 'https://patents.google.com/patent/US10123456', + }), + withDescription: createMockPatent({ + description: 'A method for efficiently processing data.', + }), + minimal: createMockPatent({ + grantDate: undefined, + url: undefined, + description: undefined, + }), +}; diff --git a/test/fixtures/props/publication.ts b/test/fixtures/props/publication.ts new file mode 100644 index 00000000..6b9365b7 --- /dev/null +++ b/test/fixtures/props/publication.ts @@ -0,0 +1,45 @@ +/** + * Shared test fixtures for publication-related components + */ + +export interface MockPublication { + title: string; + authors: string[]; + venue: string; + year: number; + abstract?: string; + pdfUrl?: string; + codeUrl?: string; + doiUrl?: string; +} + +export function createMockPublication( + overrides: Partial = {}, +): MockPublication { + return { + title: 'Test Paper Title', + authors: ['Author One', 'Author Two'], + venue: 'ICML 2023', + year: 2023, + abstract: 'Test abstract content for the publication.', + ...overrides, + }; +} + +export const mockPublications = { + basic: createMockPublication(), + withLinks: createMockPublication({ + pdfUrl: 'https://example.com/paper.pdf', + codeUrl: 'https://github.com/example/repo', + doiUrl: 'https://doi.org/10.1234/example', + }), + withCris: createMockPublication({ + authors: ['Cris Benge', 'Jane Doe'], + }), + minimal: createMockPublication({ + abstract: undefined, + pdfUrl: undefined, + codeUrl: undefined, + doiUrl: undefined, + }), +};