-
-
Notifications
You must be signed in to change notification settings - Fork 657
card component enhanced #974
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 9 commits
1e8e1a7
6a15a66
b1b11fd
e1d9912
101a65d
63fe7aa
fcee7c1
a236501
d2472b3
735ae39
9b98402
d6d47c1
5e51ee9
30db1ca
7e3e27e
8c9f2a1
f19d93d
07b68ca
677f1da
6b3187d
02cf0df
77d2cd0
f21c6f0
673c359
93e2b57
3a37370
d0d4023
df24d38
1fd224a
3493b43
8e72351
1018a2b
801b11b
57b7272
88b7f2c
27b1476
33a6c04
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,6 @@ | ||
| import { HStack, Link } from '@chakra-ui/react' | ||
| import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' | ||
| import { useState, useEffect } from 'react' | ||
| import { CardProps } from 'types/card' | ||
| import { desktopViewMinWidth } from 'utils/constants' | ||
| import { Icons } from 'utils/data' | ||
| import { TooltipRecipe } from 'utils/theme' | ||
| import { getSocialIcon } from 'utils/urlIconMappings' | ||
|
|
@@ -14,9 +12,6 @@ import DisplayIcon from 'components/DisplayIcon' | |
| import Markdown from 'components/MarkdownWrapper' | ||
| import { Tooltip } from 'components/ui/tooltip' | ||
|
|
||
| // Initial check for mobile screen size | ||
| const isMobileInitial = typeof window !== 'undefined' && window.innerWidth < desktopViewMinWidth | ||
|
|
||
| const Card = ({ | ||
| title, | ||
| url, | ||
|
|
@@ -30,23 +25,24 @@ const Card = ({ | |
| social, | ||
| tooltipLabel, | ||
| }: CardProps) => { | ||
| const [isMobile, setIsMobile] = useState(isMobileInitial) | ||
| const hasSocial = social && social.length > 0 | ||
| const hasContributors = topContributors && topContributors.length > 0 | ||
|
|
||
| // Resize listener to adjust display based on screen width | ||
| useEffect(() => { | ||
| const checkMobile = () => { | ||
| const mobile = window.innerWidth < desktopViewMinWidth | ||
| setIsMobile(mobile) | ||
| } | ||
| window.addEventListener('resize', checkMobile) | ||
| return () => window.removeEventListener('resize', checkMobile) | ||
| }, []) | ||
| // Helper function to safely handle icons | ||
| const safeIcon = (icon: any) => { | ||
| if (typeof icon === 'string') return icon; | ||
| // For test environment, return a fallback icon name | ||
| return 'circle'; | ||
| }; | ||
|
|
||
| return ( | ||
| <div className="mb-2 mt-4 flex w-full flex-col items-start rounded-md border border-border bg-white pb-4 pl-4 dark:bg-[#212529] md:max-w-6xl"> | ||
| <div className="mt-2 flex w-full flex-col items-start gap-4 pt-2 sm:flex-col sm:gap-4 md:pt-0"> | ||
| <div className="flex items-center gap-3"> | ||
| {/* Display project level badge (if available) */} | ||
| <div | ||
| className={cn( | ||
| 'relative mb-4 mt-6 flex w-full flex-col items-start rounded-lg border border-border bg-white p-2 shadow-sm transition-all duration-300 dark:bg-slate-800 sm:p-3 md:max-w-6xl' | ||
| )} | ||
| > | ||
| <div className="flex w-full flex-col items-start gap-1.5"> | ||
| <div className="flex w-full items-center gap-2"> | ||
| {level && ( | ||
| <Tooltip | ||
| id="level-tooltip" | ||
|
|
@@ -58,29 +54,43 @@ const Card = ({ | |
| > | ||
| <span | ||
| className={cn( | ||
| 'flex h-8 w-8 items-center justify-center rounded-full text-xs shadow' | ||
| 'flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full text-xs shadow-md ring-2 ring-white dark:ring-slate-700 sm:text-sm' | ||
| )} | ||
| style={{ backgroundColor: level.color }} | ||
| > | ||
| <FontAwesomeIconWrapper icon={level.icon} className="text-white" /> | ||
| {/* Use safeIcon to handle potential object icons */} | ||
| <FontAwesomeIconWrapper icon={safeIcon(level.icon)} className="text-white" /> | ||
| </span> | ||
| </Tooltip> | ||
| )} | ||
| {/* Project title and link */} | ||
| <Link href={url} target="_blank" rel="noopener noreferrer" className="flex-1"> | ||
| <h1 | ||
| className="max-w-full break-words text-base font-semibold dark:text-sky-600 sm:break-normal sm:text-lg lg:text-2xl" | ||
| style={{ | ||
| transition: 'color 0.3s ease', | ||
| }} | ||
| > | ||
|
|
||
| {/* Project Title */} | ||
| <Link | ||
| href={url} | ||
| target="_blank" | ||
| rel="noopener noreferrer" | ||
| className="group w-full flex-1 no-underline" | ||
| > | ||
| <h1 className="max-w-full text-lg font-bold text-gray-800 transition-colors duration-300 group-hover:text-blue-600 dark:text-white dark:group-hover:text-blue-400 sm:text-xl md:text-2xl"> | ||
| {title} | ||
| </h1> | ||
| </Link> | ||
| </div> | ||
| {/* Icons associated with the project */} | ||
| {icons && Object.keys(Icons).some((key) => icons[key]) ? ( | ||
| <div className="-ml-1.5 flex flex-grow"> | ||
|
|
||
| {projectName && ( | ||
| <Link | ||
| href={projectLink} | ||
| rel="noopener noreferrer" | ||
| className="inline-flex items-center gap-1 text-xs font-medium text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 sm:text-sm" | ||
| > | ||
| <FontAwesomeIconWrapper icon="link" className="h-2.5 w-2.5 sm:h-3 sm:w-3" /> | ||
| {projectName} | ||
| </Link> | ||
| )} | ||
|
|
||
| {/* Technology Icons*/} | ||
| {icons && Object.keys(Icons).some((key) => icons[key]) && ( | ||
| <div className="-ml-1 mt-1 flex flex-wrap overflow-x-auto py-1 sm:-ml-1.5"> | ||
| {Object.keys(Icons).map((key, index) => | ||
| icons[key] ? ( | ||
| <DisplayIcon | ||
|
|
@@ -91,81 +101,94 @@ const Card = ({ | |
| ) : null | ||
| )} | ||
| </div> | ||
| ) : null} | ||
| </div> | ||
| {/* Link to project name if provided */} | ||
| {projectName && ( | ||
| <Link href={projectLink} rel="noopener noreferrer" className="mt-2 font-medium"> | ||
| {projectName} | ||
| </Link> | ||
| )} | ||
| {/* Render project summary using Markdown */} | ||
| <Markdown content={summary} className="py-2 pr-4 text-gray-600 dark:text-gray-300" /> | ||
| <div | ||
| className={ | ||
| social && social.length > 0 | ||
| ? 'flex w-full flex-col gap-2 pr-4' | ||
| : 'flex w-full items-center justify-between' | ||
| } | ||
| > | ||
| {/* Render top contributors as avatars */} | ||
| <div className="mt-3 flex w-full flex-wrap items-center gap-2"> | ||
| {topContributors?.map((contributor, index) => ( | ||
| <ContributorAvatar | ||
| key={contributor.login || `contributor-${index}`} | ||
| contributor={contributor} | ||
| /> | ||
| ))} | ||
| )} | ||
|
|
||
| {/* Summary section */} | ||
| <div className={cn('my-1 w-full rounded-md bg-gray-50 p-1 dark:bg-slate-700/30 sm:p-3')}> | ||
| <Markdown | ||
| content={summary} | ||
| className="prose prose-xs sm:prose-sm dark:prose-invert max-w-none text-gray-700 dark:text-gray-200" | ||
| /> | ||
| </div> | ||
| {!social || social.length === 0 ? ( | ||
| <div | ||
| className={cn( | ||
| 'mt-3 flex items-center pr-4', | ||
| isMobile && 'mt-4 w-full justify-end pr-4' | ||
| )} | ||
| > | ||
| <ActionButton tooltipLabel={tooltipLabel} url={button.url} onClick={button.onclick}> | ||
| {button.icon} | ||
| {button.label} | ||
| </ActionButton> | ||
| </div> | ||
| ) : ( | ||
| <div | ||
| className={cn( | ||
| 'flex w-full flex-wrap items-center justify-between gap-6', | ||
| isMobile && 'items-start' | ||
|
|
||
| {/* Footer section */} | ||
| {hasSocial ? ( | ||
| <div className="mt-2 w-full"> | ||
| {/* First row: Contributors only when social links are present */} | ||
| {hasContributors && ( | ||
| <div className="mb-2"> | ||
| <div className="flex flex-col gap-1"> | ||
| <span className="text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400"> | ||
| Contributors | ||
| </span> | ||
| <div className="flex flex-wrap items-center gap-1"> | ||
| {topContributors.map((contributor, index) => ( | ||
| <ContributorAvatar | ||
| key={contributor.login || `contributor-${index}`} | ||
| contributor={contributor} | ||
| /> | ||
| ))} | ||
| </div> | ||
| </div> | ||
| </div> | ||
|
Comment on lines
+119
to
+135
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Extract contributor avatar section to avoid duplication. There's duplicate code for rendering contributors in both branches of the conditional rendering. This violates the DRY principle and could be extracted to a reusable component or function. Consider creating a separate const ContributorsSection = ({ contributors }) => (
<div className="flex flex-col gap-1">
<span className="text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
Contributors
</span>
<div className="flex flex-wrap items-center gap-1">
{contributors.map((contributor, index) => (
<ContributorAvatar
key={contributor.login || `contributor-${index}`}
contributor={contributor}
/>
))}
</div>
</div>
);Then use it in both places: {/* First row: Contributors only when social links are present */}
{hasContributors && (
<div className="mb-2">
- <div className="flex flex-col gap-1">
- <span className="text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
- Contributors
- </span>
- <div className="flex flex-wrap items-center gap-1">
- {topContributors.map((contributor, index) => (
- <ContributorAvatar
- key={contributor.login || `contributor-${index}`}
- contributor={contributor}
- />
- ))}
- </div>
- </div>
+ <ContributorsSection contributors={topContributors} />
</div>
)}
// ...
{hasContributors ? (
- <div className="flex flex-col gap-1">
- <span className="text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
- Contributors
- </span>
- <div className="flex flex-wrap items-center gap-1">
- {topContributors.map((contributor, index) => (
- <ContributorAvatar
- key={contributor.login || `contributor-${index}`}
- contributor={contributor}
- />
- ))}
- </div>
- </div>
+ <ContributorsSection contributors={topContributors} />
) : (Also applies to: 167-180 |
||
| )} | ||
| > | ||
| <div | ||
| className={cn('flex w-full items-center justify-between', isMobile && 'flex-wrap')} | ||
| > | ||
| {/* Render social links if available */} | ||
| {social && social.length > 0 && ( | ||
| <HStack id="social" mt={2}> | ||
|
|
||
| {/* Second row: Social links and button on the same line */} | ||
| <div className="flex w-full items-end justify-between"> | ||
| <div className="flex flex-col gap-1"> | ||
| <span className="text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400"> | ||
| Connect | ||
| </span> | ||
| <HStack className="flex-wrap"> | ||
| {social.map((item) => ( | ||
| <Link | ||
| key={`${item.title}-${item.url}`} | ||
| href={item.url} | ||
| target="_blank" | ||
| rel="noopener noreferrer" | ||
| display="flex" | ||
| alignItems="center" | ||
| gap={2} | ||
| className="flex h-7 w-7 items-center justify-center rounded-full bg-gray-100 text-gray-600 transition-colors hover:bg-blue-100 hover:text-blue-600 dark:bg-slate-700 dark:text-gray-300 dark:hover:bg-blue-900 dark:hover:text-blue-300 sm:h-8 sm:w-8" | ||
| > | ||
| <FontAwesomeIcon icon={getSocialIcon(item.url)} className="h-5 w-5" /> | ||
| </Link> | ||
| ))} | ||
| </HStack> | ||
| )} | ||
| {/* Action Button */} | ||
| <div className="flex items-center"> | ||
| </div> | ||
| <div className="flex-shrink-0 self-end"> | ||
| <ActionButton tooltipLabel={tooltipLabel} url={button.url} onClick={button.onclick}> | ||
| {button.icon} | ||
| {/* Handle button icon safely */} | ||
| {typeof button.icon === 'string' ? <FontAwesomeIconWrapper icon={button.icon} /> : button.icon} | ||
| {button.label} | ||
| </ActionButton> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ) : ( | ||
| <div className="mt-2 flex w-full items-end justify-between"> | ||
| {hasContributors ? ( | ||
| <div className="flex flex-col gap-1"> | ||
| <span className="text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400"> | ||
| Contributors | ||
| </span> | ||
| <div className="flex flex-wrap items-center gap-1"> | ||
| {topContributors.map((contributor, index) => ( | ||
| <ContributorAvatar | ||
| key={contributor.login || `contributor-${index}`} | ||
| contributor={contributor} | ||
| /> | ||
| ))} | ||
| </div> | ||
| </div> | ||
| ) : ( | ||
| <div></div> | ||
| )} | ||
| <div className="flex-shrink-0 self-end"> | ||
| <ActionButton tooltipLabel={tooltipLabel} url={button.url} onClick={button.onclick}> | ||
| {/* Handle button icon safely */} | ||
| {typeof button.icon === 'string' ? <FontAwesomeIconWrapper icon={button.icon} /> : button.icon} | ||
| {button.label} | ||
| </ActionButton> | ||
| </div> | ||
| </div> | ||
| )} | ||
| </div> | ||
| </div> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix TypeScript
anytype warning.The
safeIconhelper function is a good safety check, but it's using an explicitanytype which triggered a linter warning.📝 Committable suggestion
🧰 Tools
🪛 GitHub Actions: Run CI/CD
[warning] 32-32: Unexpected any. Specify a different type @typescript-eslint/no-explicit-any