diff --git a/frontend/package-lock.json b/frontend/package-lock.json index fa755b1ee..1390c9560 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -26,6 +26,7 @@ "lodash": "^4.17.21", "lucide-react": "^0.469.0", "markdown-it": "^14.1.0", + "millify": "^6.1.0", "react": "^19.0.0", "react-dom": "^19.0.0", "react-icons": "^5.3.0", @@ -4509,7 +4510,6 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, "license": "ISC", "dependencies": { "string-width": "^4.2.0", @@ -4524,14 +4524,12 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, "license": "MIT" }, "node_modules/cliui/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -4541,7 +4539,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -4556,7 +4553,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -5394,7 +5390,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -6339,7 +6334,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -9173,6 +9167,17 @@ "node": ">=8.6" } }, + "node_modules/millify": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/millify/-/millify-6.1.0.tgz", + "integrity": "sha512-H/E3J6t+DQs/F2YgfDhxUVZz/dF8JXPPKTLHL/yHCcLZLtCXJDUaqvhJXQwqOVBvbyNn4T0WjLpIHd7PAw7fBA==", + "dependencies": { + "yargs": "^17.0.1" + }, + "bin": { + "millify": "bin/millify" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -10460,7 +10465,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -11228,7 +11232,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -12469,7 +12472,6 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, "license": "ISC", "engines": { "node": ">=10" @@ -12499,7 +12501,6 @@ "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, "license": "MIT", "dependencies": { "cliui": "^8.0.1", @@ -12518,7 +12519,6 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, "license": "ISC", "engines": { "node": ">=12" @@ -12528,14 +12528,12 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, "license": "MIT" }, "node_modules/yargs/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -12545,7 +12543,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", diff --git a/frontend/package.json b/frontend/package.json index 18ee104e9..a67028026 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -36,6 +36,7 @@ "lodash": "^4.17.21", "lucide-react": "^0.469.0", "markdown-it": "^14.1.0", + "millify": "^6.1.0", "react": "^19.0.0", "react-dom": "^19.0.0", "react-icons": "^5.3.0", diff --git a/frontend/src/components/Card.tsx b/frontend/src/components/Card.tsx index 4dbe8bfac..41acfc927 100644 --- a/frontend/src/components/Card.tsx +++ b/frontend/src/components/Card.tsx @@ -1,18 +1,19 @@ import { FontAwesomeIcon, FontAwesomeIconProps } from '@fortawesome/react-fontawesome' -import { useState } from 'react' +import { useState, useEffect } from 'react' import { Tooltip } from 'react-tooltip' - import { CardProps, tooltipStyle } from 'lib/constants' import FontAwesomeIconWrapper from 'lib/FontAwesomeIconWrapper' import { cn } from 'lib/utils' import ActionButton from 'components/ActionButton' import ContributorAvatar from 'components/ContributorAvatar' - import { Icons } from 'components/data' import DisplayIcon from 'components/DisplayIcon' import Markdown from 'components/MarkdownWrapper' import TopicBadge from 'components/TopicBadge' +// Initial check for mobile screen size +const isMobileInitial = typeof window !== 'undefined' && window.innerWidth < 768 + const Card = ({ title, url, @@ -29,28 +30,39 @@ const Card = ({ social, tooltipLabel, }: CardProps) => { - const [visibleLanguages, setVisibleLanguages] = useState(18) - const [visibleTopics, setVisibleTopics] = useState(18) + const [visibleLanguages, setVisibleLanguages] = useState(isMobileInitial ? 4 : 18) + const [visibleTopics, setVisibleTopics] = useState(isMobileInitial ? 4 : 18) + const [toggleLanguages, setToggleLanguages] = useState(false) + const [toggleTopics, setToggleTopics] = useState(false) + const [isMobile, setIsMobile] = useState(isMobileInitial) - const [toggleLanguages, setToggleLanguages] = useState(true) - const [toggleTopics, setToggleTopics] = useState(true) + // Resize listener to adjust display based on screen width + useEffect(() => { + const checkMobile = () => { + const mobile = window.innerWidth < 768 + setIsMobile(mobile) + setVisibleLanguages(mobile ? 4 : 18) + setVisibleTopics(mobile ? 4 : 18) + } + window.addEventListener('resize', checkMobile) + return () => window.removeEventListener('resize', checkMobile) + }, []) const loadMoreLanguages = () => { - if (toggleLanguages) setVisibleLanguages(languages?.length as number) - else setVisibleLanguages(18) + setVisibleLanguages(toggleLanguages ? (isMobile ? 4 : 18) : languages?.length || 0) setToggleLanguages(!toggleLanguages) } const loadMoreTopics = () => { - if (toggleTopics) setVisibleTopics(topics?.length as number) - else setVisibleTopics(18) + setVisibleTopics(toggleTopics ? (isMobile ? 4 : 18) : topics?.length || 0) setToggleTopics(!toggleTopics) } return ( -
-
-
+
+
+
+ {/* Display project level badge (if available) */} {level && ( )} - -

{title}

+ {/* Project title and link */} +
+

+ {title} +

- -
+ {/* Icons associated with the project */} +
{icons && Object.keys(Icons).map((key, index) => - icons[key] !== undefined ? ( + icons[key] ? ( e == key)} + icons={Object.fromEntries(Object.entries(icons).filter(([_, value]) => value))} // only pass in truthy meta data + idx={ + Object.keys(icons).findIndex((e) => e === key) === + Object.keys(icons).filter((key) => icons[key]).length - 1 + ? -1 + : Object.keys(icons).findIndex((e) => e === key) + } /> ) : null )}
+ {/* Link to project name if provided */} {projectName && ( - + {projectName} )} - -

- {leaders && ( + {/* Render project summary using Markdown */} + + {/* Display leaders of the project */} + {leaders && ( +

{leaders.length > 1 ? 'Leaders: ' : 'Leader: '} - )} - {leaders && - leaders.map((leader, index) => ( + {leaders.map((leader, index) => ( - {index !== leaders.length - 1 ? `${leader}, ` : `${leader}`} + {index !== leaders.length - 1 ? `${leader}, ` : leader} ))} -

-
-
-
- {topContributors && - topContributors.map((contributor, index) => ( - - ))} -
- - {(languages || (topics && topics.length > 0) || (social && social.length > 0)) && ( -
-
- {languages && ( -
- {languages && - languages - .slice(0, visibleLanguages) - .map((topic, index) => ( - - ))} - {languages && languages.length > 18 && ( - - )} -
- )} - {topics && topics.length > 0 && ( -
- {topics && - topics - .slice(0, visibleTopics) - .map((topic, index) => ( - - ))} - - {topics && topics.length > 18 && ( - - )} -
+

+ )} +
+ {/* Render top contributors as avatars */} +
+ {topContributors?.map((contributor, index) => ( + + ))} +
+
+ {/* Render languages and topics with load more functionality */} +
+ {languages && ( +
+ {languages.slice(0, visibleLanguages).map((language, index) => ( + + ))} + {languages.length > 8 && ( + )} - {social && social.length > 0 && ( -
- {social && - social.map((item) => ( - - - - ))} -
+
+ )} + {topics && topics.length > 0 && ( +
+ {topics.slice(0, visibleTopics).map((topic, index) => ( + + ))} + {topics.length > 18 && ( + )}
-
- )} -
-
- - {button.icon} - {button.label} - + )} + {/* Render social links if available */} + {social && social.length > 0 && ( +
+ {social.map((item) => ( + + + + ))} +
+ )} +
+ {/* Action button */} +
+ + {button.icon} + {button.label} + +
diff --git a/frontend/src/components/DisplayIcon.tsx b/frontend/src/components/DisplayIcon.tsx index a7fcf9452..ecb65cee3 100644 --- a/frontend/src/components/DisplayIcon.tsx +++ b/frontend/src/components/DisplayIcon.tsx @@ -1,8 +1,7 @@ +import { millify } from 'millify' import { Tooltip } from 'react-tooltip' - import { IconType, tooltipStyle } from 'lib/constants' import FontAwesomeIconWrapper from 'lib/FontAwesomeIconWrapper' - import { IconKeys, Icons } from './data' export default function DisplayIcon({ @@ -18,15 +17,21 @@ export default function DisplayIcon({
- {icons[item]} + {/* Display formatted number if the value is a number */} + + {typeof icons[item] === 'number' + ? millify(icons[item], { precision: 1 }) // Format large numbers using 'millify' library + : icons[item]} + + {/* Tooltip for showing more info when hovering */}
) : null diff --git a/frontend/src/components/Footer.tsx b/frontend/src/components/Footer.tsx index 50d2ddffa..63be9aeab 100644 --- a/frontend/src/components/Footer.tsx +++ b/frontend/src/components/Footer.tsx @@ -1,17 +1,49 @@ -import { footerSections } from 'utils/constants' -import { Section } from 'utils/constants' +import { ChevronDown, ChevronUp } from 'lucide-react' +import { useState, useCallback } from 'react' +import { footerSections, Section } from '../utils/constants' export default function Footer() { + // State to keep track of the open section in the footer + const [openSection, setOpenSection] = useState(null) + + // Function to toggle the section open/closed + const toggleSection = useCallback((title: string) => { + // If the section is already open, close it, otherwise open it + setOpenSection((prev) => (prev === title ? null : title)) + }, []) + return (