diff --git a/components/Cards/Card/Card.js b/components/Cards/Card/Card.tsx similarity index 67% rename from components/Cards/Card/Card.js rename to components/Cards/Card/Card.tsx index 4576efd36..03427f406 100644 --- a/components/Cards/Card/Card.js +++ b/components/Cards/Card/Card.tsx @@ -1,26 +1,20 @@ -import { bool, node, string } from 'prop-types'; import { twMerge } from 'tailwind-merge'; import { getDataAttributes } from 'common/utils/prop-utils'; -Card.propTypes = { - children: node.isRequired, - className: string, - hasAnimationOnHover: bool, +export type CardPropsType = { + children: React.ReactNode; + className?: string; + hasAnimationOnHover?: boolean; }; -Card.defaultProps = { - className: undefined, - hasAnimationOnHover: false, -}; - -function Card({ children, className, ...props }) { +function Card({ children, className, hasAnimationOnHover, ...props }: CardPropsType) { const customDataAttributes = getDataAttributes(props); return (
; - -// Default Card supplied with only required args -export const Default = Template.bind({}); -Default.args = { - children: descriptions.medium, -}; diff --git a/components/Cards/Card/__stories__/Card.stories.tsx b/components/Cards/Card/__stories__/Card.stories.tsx new file mode 100644 index 000000000..9269fca6e --- /dev/null +++ b/components/Cards/Card/__stories__/Card.stories.tsx @@ -0,0 +1,19 @@ +import { Meta, StoryObj } from '@storybook/react'; +import { descriptions } from 'common/constants/descriptions'; +import Card from '../Card'; + +type CardStoryType = StoryObj; + +const meta: Meta = { + title: 'Cards/Card', + component: Card, + args: { + children: descriptions.medium, + }, +}; + +export default meta; + +export const Default: CardStoryType = { + render: args => , +}; diff --git a/components/Cards/Card/__tests__/Card.test.js b/components/Cards/Card/__tests__/Card.test.tsx similarity index 99% rename from components/Cards/Card/__tests__/Card.test.js rename to components/Cards/Card/__tests__/Card.test.tsx index a2779cca7..82d967be4 100644 --- a/components/Cards/Card/__tests__/Card.test.js +++ b/components/Cards/Card/__tests__/Card.test.tsx @@ -1,5 +1,4 @@ import createSnapshotTest from 'test-utils/createSnapshotTest'; - import Card from '../Card'; describe('Card', () => { diff --git a/components/Cards/Card/__tests__/__snapshots__/Card.test.js.snap b/components/Cards/Card/__tests__/__snapshots__/Card.test.tsx.snap similarity index 100% rename from components/Cards/Card/__tests__/__snapshots__/Card.test.js.snap rename to components/Cards/Card/__tests__/__snapshots__/Card.test.tsx.snap diff --git a/components/Cards/ImageCard/ImageCard.js b/components/Cards/ImageCard/ImageCard.tsx similarity index 54% rename from components/Cards/ImageCard/ImageCard.js rename to components/Cards/ImageCard/ImageCard.tsx index e6da1c382..519bcde5e 100644 --- a/components/Cards/ImageCard/ImageCard.js +++ b/components/Cards/ImageCard/ImageCard.tsx @@ -1,26 +1,34 @@ -import { bool, node, string } from 'prop-types'; import { twMerge } from 'tailwind-merge'; import Image from 'next/image'; -import Card from 'components/Cards/Card/Card'; +import Card, { CardPropsType } from 'components/Cards/Card/Card'; -ImageCard.propTypes = { - alt: string.isRequired, - children: node.isRequired, - className: string, - imageSource: string.isRequired, - isImageFirst: bool, -}; +export type ImageCardPropsType = { + /** + * Includes alt text for the image. + */ + alt: string; + /** + * Url for the image. + */ + imageSource: string; + /** + * Sets whether the image is displayed inline before the content, or after. + * @default - true + */ + isImageFirst?: boolean; +} & Omit; -ImageCard.defaultProps = { - className: undefined, - isImageFirst: true, -}; - -function ImageCard({ alt, children, className, imageSource, isImageFirst }) { +function ImageCard({ + alt, + children, + className, + imageSource, + isImageFirst = true, +}: ImageCardPropsType) { const ImageComponent = {alt}; const ContentComponent = ( -
{children}
+
{children}
); return ( diff --git a/components/Cards/ImageCard/__stories__/ImageCard.stories.js b/components/Cards/ImageCard/__stories__/ImageCard.stories.js deleted file mode 100644 index 97f21d270..000000000 --- a/components/Cards/ImageCard/__stories__/ImageCard.stories.js +++ /dev/null @@ -1,18 +0,0 @@ -import { descriptions } from 'common/constants/descriptions'; -import { s3 } from 'common/constants/urls'; -import ImageCard from '../ImageCard'; - -export default { - component: ImageCard, - title: 'Cards/ImageCard', -}; - -const Template = arguments_ => ; - -// Default ImageCard supplied with only required args -export const Default = Template.bind({}); -Default.args = { - alt: 'Image Card', - children: descriptions.long, - imageSource: `${s3}redesign/heroBanners/about.jpg`, -}; diff --git a/components/Cards/ImageCard/__stories__/ImageCard.stories.tsx b/components/Cards/ImageCard/__stories__/ImageCard.stories.tsx new file mode 100644 index 000000000..62d91525c --- /dev/null +++ b/components/Cards/ImageCard/__stories__/ImageCard.stories.tsx @@ -0,0 +1,22 @@ +import { Meta, StoryObj } from '@storybook/react'; +import { descriptions } from 'common/constants/descriptions'; +import { s3 } from 'common/constants/urls'; +import ImageCard from '../ImageCard'; + +type ImageCardPropsType = StoryObj; + +const meta: Meta = { + title: 'Cards/ImageCard', + component: ImageCard, + args: { + alt: 'Image Card', + children: descriptions.long, + imageSource: `${s3}redesign/heroBanners/about.jpg`, + }, +}; + +export default meta; + +export const Default: ImageCardPropsType = { + render: args => +}; diff --git a/components/Cards/ImageCard/__tests__/ImageCard.test.js b/components/Cards/ImageCard/__tests__/ImageCard.test.tsx similarity index 99% rename from components/Cards/ImageCard/__tests__/ImageCard.test.js rename to components/Cards/ImageCard/__tests__/ImageCard.test.tsx index 1954ee8cf..761d20a5c 100644 --- a/components/Cards/ImageCard/__tests__/ImageCard.test.js +++ b/components/Cards/ImageCard/__tests__/ImageCard.test.tsx @@ -1,5 +1,4 @@ import createShallowSnapshotTest from 'test-utils/createShallowSnapshotTest'; - import ImageCard from '../ImageCard'; describe('ImageCard', () => { diff --git a/components/Cards/ImageCard/__tests__/__snapshots__/ImageCard.test.js.snap b/components/Cards/ImageCard/__tests__/__snapshots__/ImageCard.test.tsx.snap similarity index 83% rename from components/Cards/ImageCard/__tests__/__snapshots__/ImageCard.test.js.snap rename to components/Cards/ImageCard/__tests__/__snapshots__/ImageCard.test.tsx.snap index 245515295..be3d2c90d 100644 --- a/components/Cards/ImageCard/__tests__/__snapshots__/ImageCard.test.js.snap +++ b/components/Cards/ImageCard/__tests__/__snapshots__/ImageCard.test.tsx.snap @@ -3,11 +3,10 @@ exports[`ImageCard > should render with many props assigned 1`] = `

Testing! @@ -30,7 +29,6 @@ exports[`ImageCard > should render with many props assigned 1`] = ` exports[`ImageCard > should render with required props 1`] = `

should render with required props 1`] = ` />

Testing! diff --git a/components/Cards/ResourceCard/ResourceCard.js b/components/Cards/ResourceCard/ResourceCard.tsx similarity index 66% rename from components/Cards/ResourceCard/ResourceCard.js rename to components/Cards/ResourceCard/ResourceCard.tsx index 38fc9bdbf..afc8fac68 100644 --- a/components/Cards/ResourceCard/ResourceCard.js +++ b/components/Cards/ResourceCard/ResourceCard.tsx @@ -1,5 +1,4 @@ import { useState } from 'react'; -import { string, number, func, oneOf, oneOfType, array, bool } from 'prop-types'; import classNames from 'classnames'; import Accordion from 'components/Accordion/Accordion'; import OutboundLink from 'components/OutboundLink/OutboundLink'; @@ -18,16 +17,64 @@ import styles from './ResourceCard.module.css'; const DESKTOP_VOTING_BLOCK = 'desktopVotingBlock'; -VotingBlock.propTypes = { - blockID: string.isRequired, - resourceID: string.isRequired, - upVotes: number.isRequired, - downVotes: number.isRequired, - handleVote: func.isRequired, - setUpVotes: func.isRequired, - setDownVotes: func.isRequired, - didUpvote: bool.isRequired, - didDownvote: bool.isRequired, +type VoteDirectionType = 'upvote' | 'downvote'; + +type HandleVoteType = ( + /** + * Sets the vote to be up or down. + */ + voteDirection: VoteDirectionType, + /** + * Sets which resource is gets the vote. + */ + resourceID: number, + /** + * Function that sets the state value of "up" votes. + */ + setUpVotes: VotingBlockPropsType['setUpVotes'], + /** + * Function that sets the state value of "down" votes. + */ + setDownVotes: VotingBlockPropsType['setDownVotes'], +) => void; + +type VotingBlockPropsType = { + /** + * Applies an id. + */ + blockID: string; + /** + * Sets which resource is gets the vote. + */ + resourceID: number; + /** + * Number of "up" votes. + */ + upVotes: number; + /** + * Number of "down" votes. + */ + downVotes: number; + /** + * Function to handle the vote. + */ + handleVote: HandleVoteType | undefined; + /** + * Function that sets the state value of "up" votes. + */ + setUpVotes: React.Dispatch>; + /** + * Function that sets the state value of "down" votes. + */ + setDownVotes: React.Dispatch>; + /** + * Applies classes based on whether an "up" vote has occurred. + */ + didUpvote: boolean; + /** + * Applies classes based on whether an "down" vote has occurred. + */ + didDownvote: boolean; }; function VotingBlock({ @@ -40,8 +87,9 @@ function VotingBlock({ setDownVotes, didUpvote, didDownvote, -}) { - const onVote = voteDirection => handleVote(voteDirection, resourceID, setUpVotes, setDownVotes); +}: VotingBlockPropsType) { + const onVote = (voteDirection: VoteDirectionType) => + handleVote?.(voteDirection, resourceID, setUpVotes, setDownVotes); const onUpvote = () => onVote('upvote'); const onDownvote = () => onVote('downvote'); @@ -105,44 +153,63 @@ export const possibleUserVotes = { none: null, }; -ResourceCard.propTypes = { - description: string, - downvotes: number, - href: string.isRequired, - name: string.isRequired, - id: number.isRequired, - category: string, - languages: oneOfType([string, array]), - isFree: bool, - handleVote: func, - upvotes: number, - userVote: oneOf(Object.values(possibleUserVotes)), -}; - -ResourceCard.defaultProps = { - description: '', - downvotes: 0, - category: '', - languages: [], - isFree: false, - handleVote: () => {}, - upvotes: 0, - userVote: possibleUserVotes.none, +export type ResourceCardPropType = { + /** + * Url path for the link. + */ + href: string; + /** + * Name of the resource applied to the resource title link. + */ + name: string; + /** + * Applies an id to the component. + */ + id: number; + /** + * Optional description of the resource. + */ + description?: string; + /** + * Number of "down" votes. + */ + downvotes?: number; + /** + * Sets the category text. + */ + category?: string; + /** + * Applies the resource languages. + */ + languages?: string | string[]; + /** + * Sets indictor that resource is free. + */ + isFree?: boolean; + /** + * Function to handle the vote. + */ + handleVote?: () => void; + /** + * Number of "up" votes. + */ + upvotes?: number; + userVote?: keyof typeof possibleUserVotes | null; }; function ResourceCard({ - description, - downvotes, + description = '', + downvotes = 0, href, name, - category, - languages, - isFree, + category = '', + languages = [], + isFree = false, handleVote, - upvotes, - userVote, + upvotes = 0, + userVote = possibleUserVotes.none, id, -}) { +}: ResourceCardPropType) { const [upVotes, setUpVotes] = useState(upvotes); const [downVotes, setDownVotes] = useState(downvotes); const didUpvote = userVote === possibleUserVotes.upvote; @@ -157,7 +224,7 @@ function ResourceCard({

@@ -191,7 +258,8 @@ function ResourceCard({

- Languages: {languages.join(', ')} + Languages:{' '} + {Array.isArray(languages) ? languages.join(', ') : languages}

Category: {category} diff --git a/components/Cards/ResourceCard/ResourceSkeletonCard.js b/components/Cards/ResourceCard/ResourceSkeletonCard.tsx similarity index 64% rename from components/Cards/ResourceCard/ResourceSkeletonCard.js rename to components/Cards/ResourceCard/ResourceSkeletonCard.tsx index aac7dbb70..2e7ba1384 100644 --- a/components/Cards/ResourceCard/ResourceSkeletonCard.js +++ b/components/Cards/ResourceCard/ResourceSkeletonCard.tsx @@ -1,15 +1,17 @@ -import { number } from 'prop-types'; import { SKELETON_CARD } from '../../../common/constants/testIDs'; import styles from './ResourceSkeletonCard.module.css'; -ResourceSkeletonCard.propTypes = { - numberOfSkeletons: number.isRequired, +export type ResourceSkeletonCardPropsType = { + /** + * Sets the number of UI "skeletons" to render. + */ + numberOfSkeletons: number; }; -function ResourceSkeletonCard({ numberOfSkeletons }) { +function ResourceSkeletonCard({ numberOfSkeletons }: ResourceSkeletonCardPropsType) { return (

- {[...Array(numberOfSkeletons)].map((skeleton, index) => ( + {[...Array(numberOfSkeletons)].map((_skeleton, index) => ( // eslint-disable-next-line react/no-array-index-key
diff --git a/components/Cards/ResourceCard/__stories__/ResourceCard.stories.js b/components/Cards/ResourceCard/__stories__/ResourceCard.stories.js deleted file mode 100644 index 2e68fcc3b..000000000 --- a/components/Cards/ResourceCard/__stories__/ResourceCard.stories.js +++ /dev/null @@ -1,31 +0,0 @@ -import { descriptions } from 'common/constants/descriptions'; -import ResourceCard from '../ResourceCard'; - -export default { - component: ResourceCard, - title: 'Cards/ResourceCard', -}; - -const Template = arguments_ => ; - -// Default ResourceCard supplied with only required args -export const Default = Template.bind({}); -Default.args = { - href: 'https://google.com/', - name: 'Name of Resource', -}; - -// ResourceCard supplied with a really long name -export const WithLongName = Template.bind({}); -WithLongName.args = { - ...Default.args, - name: descriptions.medium, -}; - -// ResourceCard supplied with a number of upvotes/downvotes -export const WithVotes = Template.bind({}); -WithVotes.args = { - ...Default.args, - downvotes: 25, - upvotes: 25, -}; diff --git a/components/Cards/ResourceCard/__stories__/ResourceCard.stories.tsx b/components/Cards/ResourceCard/__stories__/ResourceCard.stories.tsx new file mode 100644 index 000000000..f56a9dd00 --- /dev/null +++ b/components/Cards/ResourceCard/__stories__/ResourceCard.stories.tsx @@ -0,0 +1,35 @@ +import { Meta, StoryObj } from '@storybook/react'; +import { descriptions } from 'common/constants/descriptions'; +import ResourceCard from '../ResourceCard'; + +type ResourceCardStoryType = StoryObj; + +const meta: Meta = { + title: 'Cards/ResourceCard', + component: ResourceCard, + args: { + href: 'https://google.com/', + name: 'Name of resource', + }, +}; + +export default meta; + +export const Default: ResourceCardStoryType = { + render: args => , +}; + +export const WithLongName: ResourceCardStoryType = { + ...Default, + args: { + name: descriptions.medium, + }, +}; + +export const WithVotes: ResourceCardStoryType = { + ...Default, + args: { + downvotes: 25, + upvotes: 25, + }, +}; diff --git a/components/Cards/ResourceCard/__stories__/ResourceSkeletonCard.stories.js b/components/Cards/ResourceCard/__stories__/ResourceSkeletonCard.stories.js deleted file mode 100644 index 49b2bebaf..000000000 --- a/components/Cards/ResourceCard/__stories__/ResourceSkeletonCard.stories.js +++ /dev/null @@ -1,15 +0,0 @@ -import ResourceSkeletonCard from '../ResourceSkeletonCard'; - -export default { - component: ResourceSkeletonCard, - title: 'Cards/ResourceSkeletonCard', -}; - -const Template = arguments_ => ; - -// Default ResourceSkeletonCard supplied with only required args -// Should mimic a list of results loading -export const Default = Template.bind({}); -Default.args = { - numberOfSkeletons: 10, -}; diff --git a/components/Cards/ResourceCard/__stories__/ResourceSkeletonCard.stories.tsx b/components/Cards/ResourceCard/__stories__/ResourceSkeletonCard.stories.tsx new file mode 100644 index 000000000..1d82790cb --- /dev/null +++ b/components/Cards/ResourceCard/__stories__/ResourceSkeletonCard.stories.tsx @@ -0,0 +1,16 @@ +import { Meta, StoryObj } from '@storybook/react'; +import ResourceSkeletonCard from '../ResourceSkeletonCard'; + +type ResourceSkeletonCardStoryType = StoryObj; + +const meta: Meta = { + title: 'Cards/ResourceSkeletonCard', + component: ResourceSkeletonCard, + args: { numberOfSkeletons: 10 }, +}; + +export default meta; + +export const Default: ResourceSkeletonCardStoryType = { + render: args => , +}; diff --git a/components/Cards/ResourceCard/__tests__/ResourceCard.test.js b/components/Cards/ResourceCard/__tests__/ResourceCard.test.tsx similarity index 94% rename from components/Cards/ResourceCard/__tests__/ResourceCard.test.js rename to components/Cards/ResourceCard/__tests__/ResourceCard.test.tsx index b497fd67b..80e84e586 100644 --- a/components/Cards/ResourceCard/__tests__/ResourceCard.test.js +++ b/components/Cards/ResourceCard/__tests__/ResourceCard.test.tsx @@ -1,6 +1,5 @@ import { fireEvent, render } from '@testing-library/react'; import { UPVOTE_BUTTON, DOWNVOTE_BUTTON } from 'common/constants/testIDs'; - import ResourceCard from '../ResourceCard'; describe('ResourceCard', () => { @@ -12,12 +11,13 @@ describe('ResourceCard', () => { it('fires appropriate method when upvote button clicked', () => { const handleVoteMock = vi.fn(); - const component = render(); - const UpvoteButton = component.queryByTestId(UPVOTE_BUTTON); + expect(handleVoteMock).not.toHaveBeenCalled(); - fireEvent.click(UpvoteButton); + + fireEvent.click(UpvoteButton!); + expect(handleVoteMock).toHaveBeenCalledTimes(1); expect(handleVoteMock).toHaveBeenCalledWith( 'upvote', @@ -29,12 +29,13 @@ describe('ResourceCard', () => { it('fires appropriate method when downvote button clicked', () => { const handleVoteMock = vi.fn(); - const component = render(); - const DownvoteButton = component.queryByTestId(DOWNVOTE_BUTTON); + expect(handleVoteMock).not.toHaveBeenCalled(); - fireEvent.click(DownvoteButton); + + fireEvent.click(DownvoteButton!); + expect(handleVoteMock).toHaveBeenCalledTimes(1); expect(handleVoteMock).toHaveBeenCalledWith( 'downvote', diff --git a/components/Cards/ResourceCard/__tests__/ResourceSkeletonCard.test.js b/components/Cards/ResourceCard/__tests__/ResourceSkeletonCard.test.tsx similarity index 99% rename from components/Cards/ResourceCard/__tests__/ResourceSkeletonCard.test.js rename to components/Cards/ResourceCard/__tests__/ResourceSkeletonCard.test.tsx index bae647c11..24fec7511 100644 --- a/components/Cards/ResourceCard/__tests__/ResourceSkeletonCard.test.js +++ b/components/Cards/ResourceCard/__tests__/ResourceSkeletonCard.test.tsx @@ -4,6 +4,7 @@ import ResourceSkeletonCard from '../ResourceSkeletonCard'; describe('ResourceSkeletonCard', () => { const numberOfSkeletons = 5; + it('renders the correct amount of skeletons', () => { const component = render(); const Skeletons = component.queryAllByTestId(SKELETON_CARD); diff --git a/components/Cards/ValueCard/ValueCard.js b/components/Cards/ValueCard/ValueCard.tsx similarity index 65% rename from components/Cards/ValueCard/ValueCard.js rename to components/Cards/ValueCard/ValueCard.tsx index a3faf5347..e771a84a3 100644 --- a/components/Cards/ValueCard/ValueCard.js +++ b/components/Cards/ValueCard/ValueCard.tsx @@ -1,12 +1,11 @@ -import { string } from 'prop-types'; import Card from 'components/Cards/Card/Card'; -ValueCard.propTypes = { - name: string.isRequired, - description: string.isRequired, +export type ValueCardPropsType = { + name: string; + description: string; }; -function ValueCard({ description, name }) { +function ValueCard({ description, name }: ValueCardPropsType) { return ( ; - -// Default ValueCard supplied with only required args -export const Default = Template.bind({}); -Default.args = { - name: 'Card name', - description: descriptions.short, -}; - -// ValueCard supplied with long description -export const WithLongDescription = Template.bind({}); -WithLongDescription.args = { - ...Default.args, - description: descriptions.long, -}; diff --git a/components/Cards/ValueCard/__stories__/ValueCard.stories.tsx b/components/Cards/ValueCard/__stories__/ValueCard.stories.tsx new file mode 100644 index 000000000..78cd2dcc3 --- /dev/null +++ b/components/Cards/ValueCard/__stories__/ValueCard.stories.tsx @@ -0,0 +1,27 @@ +import { Meta, StoryObj } from '@storybook/react'; +import { descriptions } from 'common/constants/descriptions'; +import ValueCard from '../ValueCard'; + +type ValueCardStoryType = StoryObj; + +const meta: Meta = { + title: 'Cards/ValueCard', + component: ValueCard, + args: { + name: 'Card name', + description: descriptions.short, + }, +}; + +export default meta; + +export const Default: ValueCardStoryType = { + render: args => , +}; + +export const WithLongDescription: ValueCardStoryType = { + ...Default, + args: { + description: descriptions.long, + }, +}; diff --git a/components/Cards/ValueCard/__tests__/ValueCard.test.js b/components/Cards/ValueCard/__tests__/ValueCard.test.tsx similarity index 99% rename from components/Cards/ValueCard/__tests__/ValueCard.test.js rename to components/Cards/ValueCard/__tests__/ValueCard.test.tsx index 2164d8c6d..ed671a1b3 100644 --- a/components/Cards/ValueCard/__tests__/ValueCard.test.js +++ b/components/Cards/ValueCard/__tests__/ValueCard.test.tsx @@ -1,5 +1,4 @@ import createSnapshotTest from 'test-utils/createSnapshotTest'; - import ValueCard from '../ValueCard'; describe('ValueCard', () => { diff --git a/components/Cards/ValueCard/__tests__/__snapshots__/ValueCard.test.js.snap b/components/Cards/ValueCard/__tests__/__snapshots__/ValueCard.test.tsx.snap similarity index 100% rename from components/Cards/ValueCard/__tests__/__snapshots__/ValueCard.test.js.snap rename to components/Cards/ValueCard/__tests__/__snapshots__/ValueCard.test.tsx.snap