diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 5bbb1c0..556f867 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -2,8 +2,6 @@ name: Playwright Tests on: push: branches: [ main ] - pull_request: - branches: [ main ] jobs: test: timeout-minutes: 60 diff --git a/e2e/team.spec.ts b/e2e/team.spec.ts new file mode 100644 index 0000000..23e4912 --- /dev/null +++ b/e2e/team.spec.ts @@ -0,0 +1,75 @@ +import { test, expect, Locator } from '@playwright/test' + +/** Verifies visibility of the name, title, and photo + * + * @param card The Locator for the TeamMember Card + * @param name The name to find + * @param title The title to find, if defined + */ +async function verifyTeamMemberCard(card: Locator, name: string, title?: string) { + await expect(card.getByText(name)).toBeVisible() + if (title) { + await expect(card.getByText(title)).toBeVisible() + } + const photo = card.getByRole('img').first() + await expect(photo).toBeVisible() + await expect(photo).toHaveAttribute('alt-text', `${name} photo`) +} + +test('shows the team in English', async ({ page }) => { + await page.goto('/#/team') + + const teamContainer = page.getByLabel('team-container') + + const heading = teamContainer.getByText('✨ Leadership Team ✨') + await expect(heading).toBeVisible() + + const cards = await teamContainer.getByLabel('team-member-card').all() + expect(cards).toHaveLength(7) + + await verifyTeamMemberCard(cards[0], 'Ann Kilzer', 'Director') + await verifyTeamMemberCard(cards[1], 'Paty Cortez', 'Director') + await verifyTeamMemberCard(cards[2], 'Maria Tenorio', 'Lead') + await verifyTeamMemberCard(cards[3], 'Daria Vazhenina', 'ML & Data Science Lead') + await verifyTeamMemberCard(cards[4], 'Krizza Bullecer', 'Lead') + await verifyTeamMemberCard(cards[5], 'Ania Nakayama', 'Lead') + await verifyTeamMemberCard(cards[6], 'Aidan Fournier', 'Lead') + + // verify link + const links = await page.getByLabel('link-wrapper').all() + expect(links).toHaveLength(1) + const annLink = links[0] + await expect(annLink).toBeVisible() + await expect(annLink).toHaveRole('link') + await expect(annLink).toHaveAttribute('href', 'https://annkilzer.net') + await expect(annLink).toHaveAttribute('target', '_blank') +}) + +test('shows the team in Japanese', async ({ page }) => { + await page.goto('/#/team') + + // switch locale to Japanese + const hamburger = page.getByLabel('drawer-toggle-button') + await hamburger.click() + const japanese = page.getByLabel('drawer').getByText('日本語') + await japanese.click() + + const teamContainer = page.getByLabel('team-container') + + // click off to close sidebar + await teamContainer.click({ force: true }) + + const heading = teamContainer.getByText('✨ リーダーシップ・チーム ✨') + await expect(heading).toBeVisible() + + const cards = await teamContainer.getByLabel('team-member-card').all() + expect(cards).toHaveLength(7) + + await verifyTeamMemberCard(cards[0], 'キルザー·杏', 'ディレクター') + await verifyTeamMemberCard(cards[1], 'Paty Cortez', 'ディレクター') + await verifyTeamMemberCard(cards[2], 'Maria Tenorio', 'リード') + await verifyTeamMemberCard(cards[3], 'バジェニナ・ダリヤ', 'ML&データサイエンス・リード') + await verifyTeamMemberCard(cards[4], 'ブレサー クリザ', 'リード') + await verifyTeamMemberCard(cards[5], 'Ania Nakayama', 'リード') + await verifyTeamMemberCard(cards[6], 'エイデン・フォニエ', 'リード') +}) diff --git a/public/Aidan.jpg b/public/Aidan.jpg new file mode 100644 index 0000000..6b60434 Binary files /dev/null and b/public/Aidan.jpg differ diff --git a/public/Ann.jpg b/public/Ann.jpg new file mode 100644 index 0000000..b3f78d7 Binary files /dev/null and b/public/Ann.jpg differ diff --git a/public/Daria.jpg b/public/Daria.jpg new file mode 100644 index 0000000..e6dd23a Binary files /dev/null and b/public/Daria.jpg differ diff --git a/public/Krizza.jpg b/public/Krizza.jpg new file mode 100644 index 0000000..f7b0f98 Binary files /dev/null and b/public/Krizza.jpg differ diff --git a/public/Maria.jpg b/public/Maria.jpg new file mode 100644 index 0000000..f627135 Binary files /dev/null and b/public/Maria.jpg differ diff --git a/public/Paty.jpg b/public/Paty.jpg new file mode 100644 index 0000000..dc447b4 Binary files /dev/null and b/public/Paty.jpg differ diff --git a/public/Placeholder.png b/public/Placeholder.png new file mode 100644 index 0000000..33d2576 Binary files /dev/null and b/public/Placeholder.png differ diff --git a/src/components/Header/DesktopHeader.tsx b/src/components/Header/DesktopHeader.tsx index 883c1da..49fe0e7 100644 --- a/src/components/Header/DesktopHeader.tsx +++ b/src/components/Header/DesktopHeader.tsx @@ -32,7 +32,9 @@ const DesktopHeader: FC = () => { WiSE Japan {t('header.subtitle')} - + + {t('header.team')} + {t('header.codeOfConduct')} diff --git a/src/components/Header/__test__/DesktopToolbar.test.tsx b/src/components/Header/__test__/DesktopToolbar.test.tsx index 83743e8..bcf4ec1 100644 --- a/src/components/Header/__test__/DesktopToolbar.test.tsx +++ b/src/components/Header/__test__/DesktopToolbar.test.tsx @@ -10,5 +10,11 @@ describe('Header', () => { expect(title).toBeVisible() }) - it.todo('should show navigation links') + it('should show navigation links', async () => { + render() + const team = await screen.findByText('Team') + expect(team).toBeVisible() + const codeOfConduct = await screen.findByText('Code of Conduct') + expect(codeOfConduct).toBeVisible() + }) }) diff --git a/src/components/OptionalLinkWrapper/OptionalLinkWrapper.tsx b/src/components/OptionalLinkWrapper/OptionalLinkWrapper.tsx new file mode 100644 index 0000000..37fbd5f --- /dev/null +++ b/src/components/OptionalLinkWrapper/OptionalLinkWrapper.tsx @@ -0,0 +1,20 @@ +import { FC, ReactNode } from 'react' + +interface OptionalLinkWrapperProps { + url?: string + children: ReactNode +} + +/** + * If url is falsy, returns children. If url is truthy, returns the component wrapped in a link + */ +const OptionalLinkWrapper: FC = ({ url, children }) => { + if (url) { + return + {children} + + } + return children +} + +export default OptionalLinkWrapper diff --git a/src/components/OptionalLinkWrapper/__test__/OptionalLinkWrapper.test.tsx b/src/components/OptionalLinkWrapper/__test__/OptionalLinkWrapper.test.tsx new file mode 100644 index 0000000..9999e64 --- /dev/null +++ b/src/components/OptionalLinkWrapper/__test__/OptionalLinkWrapper.test.tsx @@ -0,0 +1,33 @@ +import { describe, expect, it } from 'vitest' +import { render } from '@/tests/customRender' +import { screen } from '@testing-library/react' +import OptionalLinkWrapper from '../OptionalLinkWrapper' + + +describe('OptionalLinkWrapper', () => { + const exampleURL = 'https://example.com' + + it('should render the link when URL is set', async () => { + render( + child + ) + + const link = await screen.findByRole('link') + expect(link).toHaveAttribute('href', exampleURL) + + const child = await screen.findByText('child') + expect(child).toBeVisible() + }) + + it.each(['', undefined])('should render the child component when URL is %i', async (url: string | undefined) => { + render( + child + ) + + const child = await screen.findByText('child') + expect(child).toBeVisible() + + const link = screen.queryByRole('link') + expect(link).toBeNull() + }) +}) diff --git a/src/components/SideDrawer/DrawerContents.tsx b/src/components/SideDrawer/DrawerContents.tsx index 6095483..e21c398 100644 --- a/src/components/SideDrawer/DrawerContents.tsx +++ b/src/components/SideDrawer/DrawerContents.tsx @@ -7,9 +7,11 @@ import { useTheme } from '@mui/material/styles' import useMediaQuery from '@mui/material/useMediaQuery' import StyledNavLink from '../StyledNavLink/StyledNavLink' import LocaleToggle from '../LocaleToggle/LocaleToggle' +import { useTranslation } from 'react-i18next' const DrawerContents: FC = () => { const theme = useTheme() + const { t } = useTranslation() let navList = <> if (useMediaQuery(theme.breakpoints.down('sm'))) { navList = (<> @@ -17,7 +19,10 @@ const DrawerContents: FC = () => { Home - Code of Conduct + {t('sidebar.team')} + + + {t('sidebar.codeOfConduct')} ) diff --git a/src/components/TeamMemberCard/TeamMemberCard.tsx b/src/components/TeamMemberCard/TeamMemberCard.tsx new file mode 100644 index 0000000..9c90540 --- /dev/null +++ b/src/components/TeamMemberCard/TeamMemberCard.tsx @@ -0,0 +1,51 @@ +import { FC, useEffect, useState } from 'react' +import Card from '@mui/material/Card' +import CardContent from '@mui/material/CardContent' +import CardMedia from '@mui/material/CardMedia' +import Typography from '@mui/material/Typography' +import TeamMember from '@/types/TeamMember' +import { getI18n } from 'react-i18next' +import OptionalLinkWrapper from '../OptionalLinkWrapper/OptionalLinkWrapper' + +interface TeamMemberCardProps { + member: TeamMember +} + +const TeamMemberCard: FC = ({ member }) => { + const [name, setName] = useState(member.nameEN) + const [title, setTitle] = useState(member.titleEN) + + const i18n = getI18n() + + useEffect(() => { + if (i18n.language === 'en') { + setName(member.nameEN) + setTitle(member.titleEN) + } else if (i18n.language === 'ja') { + setName(member.nameJA || member.nameEN) + setTitle(member.titleJA || member.titleEN) + } + }, [member, i18n.language]) + + return + + + + + {name} + + + {title} + + + + + +} + +export default TeamMemberCard diff --git a/src/components/TeamMemberCard/__team__/TeamMemberCard.test.tsx b/src/components/TeamMemberCard/__team__/TeamMemberCard.test.tsx new file mode 100644 index 0000000..7363e0e --- /dev/null +++ b/src/components/TeamMemberCard/__team__/TeamMemberCard.test.tsx @@ -0,0 +1,68 @@ +import { describe, expect, it } from 'vitest' +import { render } from '@/tests/customRender' +import { screen } from '@testing-library/react' +import TeamMemberCard from '../TeamMemberCard' +import TeamMember from '@/types/TeamMember' +import '@/i18n/config' +import i18next from 'i18next' + + +describe('TeamMemberCard', () => { + const member = { + nameEN: 'Alice', + nameJA: 'アリス', + titleEN: 'Lead', + titleJA: 'リード', + image: 'example.png', + url: 'https://example.com' + } as TeamMember + + it('should render a TeamMemberCard in English', async () => { + render() + const name = await screen.findByText(member.nameEN) + expect(name).toBeVisible() + const title = await screen.findByText(member.titleEN) + expect(title).toBeVisible() + const link = await screen.findByRole('link') + expect(link).toHaveAttribute('href', member.url) + const image = await screen.findByRole('img') + expect(image).toBeVisible() + }) + + it('should render a TeamMemberCard in Japanese', async () => { + await i18next.changeLanguage('ja') + render() + + const name = await screen.findByText(member.nameJA) + expect(name).toBeVisible() + const title = await screen.findByText(member.titleJA) + expect(title).toBeVisible() + const link = await screen.findByRole('link') + expect(link).toHaveAttribute('href', member.url) + const image = await screen.findByRole('img') + expect(image).toBeVisible() + }) + + const partialMember = { + nameEN: 'Alice', + nameJA: '', + titleEN: 'Lead', + titleJA: '', + image: 'example.png', + url: 'https://example.com' + } as TeamMember + + it('should render a TeamMemberCard in Japanese and fall back to English when fields are unset', async () => { + await i18next.changeLanguage('ja') + render() + + const name = await screen.findByText(partialMember.nameEN) + expect(name).toBeVisible() + const title = await screen.findByText(partialMember.titleEN) + expect(title).toBeVisible() + const link = await screen.findByRole('link') + expect(link).toHaveAttribute('href', member.url) + const image = await screen.findByRole('img') + expect(image).toBeVisible() + }) +}) diff --git a/src/i18n/config.ts b/src/i18n/config.ts index 6ede738..6c42f66 100644 --- a/src/i18n/config.ts +++ b/src/i18n/config.ts @@ -21,3 +21,5 @@ i18next.use(initReactI18next).init({ // set returnNull to false (and also in the i18next.d.ts options) // returnNull: false, }) + +export default i18next diff --git a/src/i18n/en/translation.json b/src/i18n/en/translation.json index be95bb6..fd0eaa1 100644 --- a/src/i18n/en/translation.json +++ b/src/i18n/en/translation.json @@ -1,7 +1,8 @@ { "header": { "codeOfConduct": "Code of Conduct", - "subtitle": "Women in Software Engineering Japan" + "subtitle": "Women in Software Engineering Japan", + "team": "Team" }, "home": { "helloWorld": "✨ Hello World ✨", @@ -9,5 +10,12 @@ "paragraph1": "Many of us were saddened to hear of the sudden closure of Women Who Code. There is a need for an organization to empower diverse women in technology careers in Tokyo and across Japan.", "paragraph2": "We are not giving up on this mission. Please join us in rebuilding community so that we can empower women in Japan in Software Engineering careers.", "paragraph3": "Our events target professional women in software careers with 2+ years of experience. Beginners to seasoned professionals are welcome to participate. While this organization focuses on women, all genders are welcome at our events. Software-adjacent roles like Data Science, Product, UI/UX, Machine Learning, etc., are welcome, too." + }, + "sidebar": { + "codeOfConduct": "Code of Conduct", + "team": "Team" + }, + "team": { + "title": "✨ Leadership Team ✨" } -} \ No newline at end of file +} diff --git a/src/i18n/ja/translation.json b/src/i18n/ja/translation.json index 27022fc..f3cad98 100644 --- a/src/i18n/ja/translation.json +++ b/src/i18n/ja/translation.json @@ -1,7 +1,8 @@ { "header": { "codeOfConduct": "行動規範", - "subtitle": "ウーマン・イン・ソフトウェアエンジニアリング" + "subtitle": "ウーマン・イン・ソフトウェアエンジニアリング", + "team": "チーム" }, "home": { "helloWorld": "✨ Hello 世界 ✨", @@ -9,5 +10,12 @@ "paragraph1": "Women Who Code(WWCode)の活動中止のニュースに対し、多くの人々が悲しみました。東京や日本全体で、IT業界の多様な女性を支援する組織が必要とされています。", "paragraph2": "この使命を諦めるつもりはありません。WWCodeの志を引き継ぐために、日本の女性がソフトウェアエンジニアとしてキャリアを築けるよう、私たちと共にコミュニティを再構築していきます。", "paragraph3": "私たちのイベントは、2年以上の経験を持つITプロフェッショナルの女性を中心に、新卒からベテランまでどなたでも参加できるイベントをやっています。この組織は女性に焦点を当てていますが、イベントにはすべてのジェンダーの方が歓迎されています。また、データサイエンティスト、プロダクトマネジャー、UI/UXデザイナー、機械学習エンジニアなどのソフトウェアに関連した役割も歓迎いたします。" + }, + "sidebar": { + "codeOfConduct": "Code of Conduct", + "team": "チーム" + }, + "team": { + "title": "✨ リーダーシップ・チーム ✨" } -} \ No newline at end of file +} diff --git a/src/routes/Router.tsx b/src/routes/Router.tsx index b1cb05f..fa13212 100644 --- a/src/routes/Router.tsx +++ b/src/routes/Router.tsx @@ -8,6 +8,7 @@ import Home from './Home/Home' import BaseLayout from './BaseLayout' import NotFound from './NotFound/NotFound' import CodeOfConduct from './CodeOfConduct/CodeOfConduct' +import Team from './Team/Team' const browserRouter = createHashRouter([{ element: , @@ -20,6 +21,10 @@ const browserRouter = createHashRouter([{ path: 'codeofconduct', element: }, + { + path: 'team', + element: + }, { path: 'theme', element: diff --git a/src/routes/Team/Team.tsx b/src/routes/Team/Team.tsx new file mode 100644 index 0000000..e0e8744 --- /dev/null +++ b/src/routes/Team/Team.tsx @@ -0,0 +1,32 @@ +import { FC, ReactNode } from 'react' +import Container from '@mui/material/Container' +import Grid from '@mui/material/Grid' +import Stack from '@mui/material/Stack' +import Typography from '@mui/material/Typography' +import { useTranslation } from 'react-i18next' +import TeamMember from '@/types/TeamMember' +import TeamMemberCard from '@/components/TeamMemberCard/TeamMemberCard' +import team from './team.json' + + +const Team: FC = () => { + const { t } = useTranslation() + + const teamGrid: ReactNode[] = [] + team.forEach((member: TeamMember) => { + teamGrid.push( + + ) + }) + + return + + {t('team.title')} + + {teamGrid} + + + +} + +export default Team diff --git a/src/routes/Team/__test__/Team.test.tsx b/src/routes/Team/__test__/Team.test.tsx new file mode 100644 index 0000000..dc16a51 --- /dev/null +++ b/src/routes/Team/__test__/Team.test.tsx @@ -0,0 +1,12 @@ +import { describe, expect, it } from 'vitest' +import { render } from '@/tests/customRender' +import { screen } from '@testing-library/react' +import Team from '../Team' + +describe('Team', () => { + it('should render the Team page', async () => { + render() + const title = await screen.findByText('✨ Leadership Team ✨') + expect(title).toBeVisible() + }) +}) diff --git a/src/routes/Team/team.json b/src/routes/Team/team.json new file mode 100644 index 0000000..ac5fdb8 --- /dev/null +++ b/src/routes/Team/team.json @@ -0,0 +1,58 @@ +[ + { + "nameEN": "Ann Kilzer", + "nameJA": "キルザー·杏", + "titleEN": "Director", + "titleJA": "ディレクター", + "image": "Ann.jpg", + "url": "https://annkilzer.net" + }, + { + "nameEN": "Paty Cortez", + "nameJA": "", + "titleEN": "Director", + "titleJA": "ディレクター", + "image": "Paty.jpg", + "url": "" + }, + { + "nameEN": "Maria Tenorio", + "nameJA": "", + "titleEN": "Lead", + "titleJA": "リード", + "image": "Maria.jpg", + "url": "" + }, + { + "nameEN": "Daria Vazhenina", + "nameJA": "バジェニナ・ダリヤ", + "titleEN": "ML & Data Science Lead", + "titleJA": "ML&データサイエンス・リード", + "image": "Daria.jpg", + "url": "" + }, + { + "nameEN": "Krizza Bullecer", + "nameJA": "ブレサー クリザ", + "titleEN": "Lead", + "titleJA": "リード", + "image": "Krizza.jpg", + "url": "" + }, + { + "nameEN": "Ania Nakayama", + "nameJA": "", + "titleEN": "Lead", + "titleJA": "リード", + "image": "", + "url": "" + }, + { + "nameEN": "Aidan Fournier", + "nameJA": "エイデン・フォニエ", + "titleEN": "Lead", + "titleJA": "リード", + "image": "Aidan.jpg", + "url": "" + } +] \ No newline at end of file diff --git a/src/types/TeamMember.ts b/src/types/TeamMember.ts new file mode 100644 index 0000000..f46224e --- /dev/null +++ b/src/types/TeamMember.ts @@ -0,0 +1,10 @@ +type TeamMember = { + nameEN: string + nameJA: string + titleEN: string + titleJA: string + image: string + url: string +} + +export default TeamMember