Skip to content

Commit

Permalink
feat: compute card height dynamically; add aria-labels and link icons
Browse files Browse the repository at this point in the history
  • Loading branch information
martapanc committed Sep 12, 2023
1 parent 60d6937 commit ead1bba
Show file tree
Hide file tree
Showing 2 changed files with 121 additions and 73 deletions.
2 changes: 1 addition & 1 deletion src/app/(public)/projects/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const ProjectsPage = async () => {
<div className='layout relative flex flex-col py-12'>
<h1 className='mb-5'>Projects</h1>

<div className='grid grid-cols-1 gap-6 md:grid-cols-2 md:gap-5 xl:grid-cols-3'>
<div className='grid gap-4 sm:grid-cols-2 xl:grid-cols-3'>
{projects.map((project) => (
<ProjectCard key={project.id} project={project} />
))}
Expand Down
192 changes: 120 additions & 72 deletions src/components/organisms/projects/ProjectCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import { Button } from '@mui/material';
import Image from 'next/image';
import { useTheme } from 'next-themes';
import * as React from 'react';
import { useState } from 'react';
import { RefObject, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { IconContext } from 'react-icons';
import { AiOutlineCloud } from 'react-icons/ai';
import { AiFillGithub, AiOutlineCloud } from 'react-icons/ai';
import { BiNews } from 'react-icons/bi';
import { DiAndroid } from 'react-icons/di';
import {
FaAngular,
Expand All @@ -32,11 +33,9 @@ import {
SiTypescript,
SiVercel,
} from 'react-icons/si';
import { TbBrandKotlin } from 'react-icons/tb';
import { TbBrandKotlin, TbWorldShare } from 'react-icons/tb';
import ReactMarkdown from 'react-markdown';

import clsxm from '@/lib/clsxm';

import UnstyledLink from '@/components/links/UnstyledLink';

import { Project } from '@/types/Project';
Expand All @@ -51,95 +50,138 @@ const ProjectCard = ({ project }: ProjectCardProps) => {
const { theme } = useTheme();

const [showDescription, setShowDescription] = useState(false);
const [cardHeight, setCardHeight] = useState<number | null>(null);
const cardRef: RefObject<HTMLDivElement> = useRef(null);

useEffect(() => {
if (cardRef.current && !cardHeight) {
// Calculate and set the initial height of the card content
setCardHeight(cardRef.current.offsetHeight);
}

window.addEventListener('resize', () => setCardHeight(null));
}, [cardHeight]);

const toggleDescription = () => {
setShowDescription(!showDescription);

if (!showDescription) {
// If showing long description, set the card height to the long description's height
setCardHeight(cardRef.current ? cardRef.current.offsetHeight : null);
} else {
// If showing short description, set the card height back to the initial height
setCardHeight(null);
}
};

const iconColor = theme === 'dark' ? 'white' : 'black';

return (
<div className='rounded p-4 shadow-md dark:bg-slate-900 dark:drop-shadow-md md:w-full xl:h-[25rem] xl:w-[22rem] items-center'>
<div
className={clsxm(
'flex flex-col h-full my-1 transition-all duration-500 overflow-y-hidden',
{
'max-h-0 opacity-0': showDescription,
'max-h-full opacity-100': !showDescription,
},
)}
>
<h3 className='mb-2'>{project.title}</h3>

<div className='text-justify font-light text-sm mb-auto'>
<ReactMarkdown>{project.shortDescription}</ReactMarkdown>
</div>
<div
ref={cardRef}
className='rounded p-4 shadow-md dark:bg-slate-900 dark:drop-shadow-md'
style={
showDescription ? { height: `${cardHeight}px` } : { height: `100%` }
}
>
{!showDescription && (
<div className='flex flex-col my-1 h-full'>
<h3 className='mb-2' aria-label='Project title'>
{project.title}
</h3>

<div
className='text-justify md:font-light md:text-sm mb-auto'
aria-label='Project short description'
>
<ReactMarkdown>{project.shortDescription}</ReactMarkdown>
</div>

<div className='flex flex-row mb-3'>
<IconContext.Provider value={{ color: iconColor, size: '24px' }}>
{project.tools.map((tool: string) => {
const IconComponent = iconMapping[tool];
return (
IconComponent && (
<span key={tool} className='me-1'>
<IconComponent />
</span>
)
);
})}
</IconContext.Provider>
</div>
<div className='flex flex-row mb-3' aria-label='Project tools'>
<IconContext.Provider value={{ color: iconColor, size: '24px' }}>
{project.tools.map((tool: string) => {
const IconComponent = toolIconMapping[tool];
return (
IconComponent && (
<span key={tool} className='me-1' aria-label={tool}>
<IconComponent />
</span>
)
);
})}
</IconContext.Provider>
</div>

<div className='w-full mb-3' aria-label='Project image'>
<Image
className='w-full h-auto'
src={project.image.url}
alt={project.image.alternativeText || 'Project Image'}
width={320}
height={180}
/>
</div>

<div className='w-full mb-3'>
<Image
className='w-full h-auto'
src={project.image.url}
alt={project.image.alternativeText || 'Project Image'}
width={320}
height={180}
/>
<div className='flex flex-row justify-between' aria-label='Read More'>
{project.longDescription && (
<Button onClick={toggleDescription}>
{t('projects.readMore')}
</Button>
)}

{!project.longDescription && (
<UnstyledLink href=''>{t('projects.readMore')}</UnstyledLink>
)}

<div className='flex flex-row p-2'>
<IconContext.Provider value={{ color: iconColor, size: '24px' }}>
{Object.entries(project.links).map(([key, url]) => {
const Icon = linkIconMapping[key];
return (
<UnstyledLink
className='me-1'
key={key}
href={url}
aria-label={`${key}-link`}
>
<Icon />
</UnstyledLink>
);
})}
</IconContext.Provider>
</div>
</div>
</div>
)}

<div className=''>
{showDescription && (
<div className='flex flex-col justify-between my-1 overflow-y-scroll h-full'>
{project.longDescription && (
<Button onClick={toggleDescription}>
{t('projects.readMore')}
</Button>
)}

{!project.longDescription && (
<UnstyledLink href=''>{t('projects.readMore')}</UnstyledLink>
<div
className='mb-2 text-lg md:text-base'
aria-label='Project full description'
>
<ReactMarkdown>{project.longDescription}</ReactMarkdown>
</div>
)}
</div>
</div>

<div
className={clsxm(
'flex flex-col h-[95%] justify-between mt-4 transition-all duration-500 overflow-y-scroll',
{
'max-h-0 opacity-0': !showDescription,
'max-h-full opacity-100': showDescription,
},
)}
>
{project.longDescription && (
<div className='mb-2'>
<ReactMarkdown>{project.longDescription}</ReactMarkdown>
<div className='flex w-full justify-end'>
<Button
className='button py-2'
onClick={toggleDescription}
aria-label='Close'
>
<RxCross1 size='16px' />
</Button>
</div>
)}
<div className='flex w-full justify-end'>
<Button className='button' onClick={toggleDescription}>
<RxCross1 size='16px' />
</Button>
</div>
</div>
)}
</div>
);
};

export default ProjectCard;

const iconMapping: Record<string, React.ComponentType> = {
const toolIconMapping: Record<string, React.ComponentType> = {
android: DiAndroid,
angular: FaAngular,
aws: FaAws,
Expand All @@ -162,3 +204,9 @@ const iconMapping: Record<string, React.ComponentType> = {
typescript: SiTypescript,
vercel: SiVercel,
};

const linkIconMapping: Record<string, React.ComponentType> = {
article: BiNews,
github: AiFillGithub,
publicUrl: TbWorldShare,
};

0 comments on commit ead1bba

Please sign in to comment.