Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/components/AwardCard.astro
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
2 changes: 1 addition & 1 deletion src/components/CertificationCard.astro
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
2 changes: 1 addition & 1 deletion src/components/EducationCard.astro
Original file line number Diff line number Diff line change
Expand Up @@ -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) */
Expand Down
10 changes: 1 addition & 9 deletions src/components/FeaturedProjectCard.astro
Original file line number Diff line number Diff line change
Expand Up @@ -111,13 +111,5 @@ const {project, slug, class: className, ...attrs} = Astro.props;
</article>

<script>
// Stop propagation on GitHub links to prevent card navigation
// Uses event delegation per project security standards (no inline handlers)
document.addEventListener('click', (event) => {
const target = event.target as HTMLElement;
const githubLink = target.closest('[data-github-link]');
if (githubLink) {
event.stopPropagation();
}
});
import '../scripts/featured-project-card';
</script>
2 changes: 1 addition & 1 deletion src/components/Hero.astro
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import ExternalLink from './ExternalLink.astro';
* <Hero class="mt-20" showImage={false} />
* ```
*/
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) */
Expand Down
2 changes: 1 addition & 1 deletion src/components/Navigation.astro
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {SOCIAL_LINKS} from '../data/profile';
* <Navigation class="border-b border-border" />
* ```
*/
interface Props extends HTMLAttributes<'header'> {
export interface Props extends HTMLAttributes<'header'> {
/** Current page path for active link highlighting */
currentPath?: string;
}
Expand Down
32 changes: 3 additions & 29 deletions src/components/PublicationCard.astro
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand Down Expand Up @@ -64,7 +65,7 @@ const hasLinks = pdfUrl || codeUrl || doiUrl;

<article
data-component="publication-card"
class={cardSurfaceContainerClasses(`publication-card ${className || ''}`)}
class={cardSurfaceContainerClasses(cn('publication-card', className))}
{...attrs}
>
<!-- Header: Year and Venue -->
Expand Down Expand Up @@ -228,32 +229,5 @@ const hasLinks = pdfUrl || codeUrl || doiUrl;
</style>

<script>
// Progressive enhancement for abstract toggle
// Abstracts are visible by default (expanded), JS collapses them initially
document.addEventListener('DOMContentLoaded', () => {
document
.querySelectorAll('[data-component="publication-card"]')
.forEach((card) => {
const toggle = card.querySelector('[data-toggle]') as HTMLButtonElement;
const abstract = card.querySelector('[data-abstract]') as HTMLElement;
const toggleText = card.querySelector(
'[data-toggle-text]'
) as HTMLElement;

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';
});
});
});
import '../scripts/publication-card';
</script>
2 changes: 1 addition & 1 deletion src/layouts/BaseLayout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import Footer from '../components/Footer.astro';
* </BaseLayout>
* ```
*/
interface Props extends HTMLAttributes<'html'> {
export interface Props extends HTMLAttributes<'html'> {
/** Page title (will have " | Cris Benge" appended) */
title: string;
/** Meta description for SEO */
Expand Down
28 changes: 28 additions & 0 deletions src/scripts/featured-project-card.ts
Original file line number Diff line number Diff line change
@@ -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();
}
42 changes: 42 additions & 0 deletions src/scripts/publication-card.ts
Original file line number Diff line number Diff line change
@@ -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();
}
39 changes: 4 additions & 35 deletions src/styles/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -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:
<p class="line-clamp-2">Long text that will be truncated after 2 lines...</p>

Browser Support:
- All modern browsers (Chrome, Firefox, Safari, Edge)
- Degrades gracefully to showing full text in unsupported browsers
Usage: <p class="line-clamp-2">Long text...</p>
============================================================================= */

.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
==============================================================================
Expand Down
15 changes: 4 additions & 11 deletions src/utils/project-categories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -61,15 +63,6 @@ export const CATEGORY_LABELS: Record<ProjectCategory, string> = {
/**
* 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'];
75 changes: 75 additions & 0 deletions test/fixtures/props/award.ts
Original file line number Diff line number Diff line change
@@ -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> = {}): 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,
}),
};
47 changes: 47 additions & 0 deletions test/fixtures/props/certification.ts
Original file line number Diff line number Diff line change
@@ -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> = {},
): 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,
}),
};
Loading