diff --git a/apps/charterafrica/src/assets/icons/Type=discord, Size=25, Color=Black.svg b/apps/charterafrica/src/assets/icons/Type=discord, Size=25, Color=Black.svg new file mode 100644 index 000000000..1ac8b1261 --- /dev/null +++ b/apps/charterafrica/src/assets/icons/Type=discord, Size=25, Color=Black.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/charterafrica/src/assets/icons/Type=instagram, Size=24, Color=Black.svg b/apps/charterafrica/src/assets/icons/Type=instagram, Size=24, Color=Black.svg new file mode 100644 index 000000000..b7f68a541 --- /dev/null +++ b/apps/charterafrica/src/assets/icons/Type=instagram, Size=24, Color=Black.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/apps/charterafrica/src/assets/icons/Type=telegram, Size=25, Color=Black.svg b/apps/charterafrica/src/assets/icons/Type=telegram, Size=25, Color=Black.svg new file mode 100644 index 000000000..747673fa2 --- /dev/null +++ b/apps/charterafrica/src/assets/icons/Type=telegram, Size=25, Color=Black.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/charterafrica/src/assets/icons/Type=tiktok, Size=25, Color=Black.svg b/apps/charterafrica/src/assets/icons/Type=tiktok, Size=25, Color=Black.svg new file mode 100644 index 000000000..d5c9f1f92 --- /dev/null +++ b/apps/charterafrica/src/assets/icons/Type=tiktok, Size=25, Color=Black.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/charterafrica/src/assets/icons/Type=whatsapp, Size=25, Color=Black.svg b/apps/charterafrica/src/assets/icons/Type=whatsapp, Size=25, Color=Black.svg new file mode 100644 index 000000000..6cde08baf --- /dev/null +++ b/apps/charterafrica/src/assets/icons/Type=whatsapp, Size=25, Color=Black.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/apps/charterafrica/src/assets/icons/Type=youtube, Size=24, Color=Black.svg b/apps/charterafrica/src/assets/icons/Type=youtube, Size=24, Color=Black.svg new file mode 100644 index 000000000..3b461f2d6 --- /dev/null +++ b/apps/charterafrica/src/assets/icons/Type=youtube, Size=24, Color=Black.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/charterafrica/src/components/Dataset/Dataset.js b/apps/charterafrica/src/components/Dataset/Dataset.js index 9e2298c32..64d5e4d1d 100644 --- a/apps/charterafrica/src/components/Dataset/Dataset.js +++ b/apps/charterafrica/src/components/Dataset/Dataset.js @@ -122,6 +122,19 @@ function Dataset({ ))} ) : null} + {labels.backToDatasets} - renders unchanged 1`] = ` +
@@ -69,19 +82,6 @@ exports[` renders unchanged 1`] = ` > Back to Datasets -
diff --git a/apps/charterafrica/src/components/Entity/Entity.js b/apps/charterafrica/src/components/Entity/Entity.js index dbab1de15..30d307ff7 100644 --- a/apps/charterafrica/src/components/Entity/Entity.js +++ b/apps/charterafrica/src/components/Entity/Entity.js @@ -1,16 +1,23 @@ import { Section, RichTypography } from "@commons-ui/core"; import { Figure, Link } from "@commons-ui/next"; -import { Grid, SvgIcon, Box } from "@mui/material"; +import { Grid, SvgIcon, Box, Container } from "@mui/material"; import PropTypes from "prop-types"; import React from "react"; +import DiscordIcon from "@/charterafrica/assets/icons/Type=discord, Size=25, Color=Black.svg"; import FacebookIcon from "@/charterafrica/assets/icons/Type=facebook, Size=24, Color=CurrentColor.svg"; import GithubIcon from "@/charterafrica/assets/icons/Type=github, Size=24, Color=CurrentColor.svg"; +import InstagramIcon from "@/charterafrica/assets/icons/Type=instagram, Size=24, Color=Black.svg"; import LinkedInIcon from "@/charterafrica/assets/icons/Type=linkedin, Size=24, Color=CurrentColor.svg"; import EmailIcon from "@/charterafrica/assets/icons/Type=mail, Size=24, Color=CurrentColor.svg"; import SlackIcon from "@/charterafrica/assets/icons/Type=slack, Size=24, Color=CurrentColor.svg"; -import TelegramIcon from "@/charterafrica/assets/icons/Type=telegram, Size=24, Color=CurrentColor.svg"; +import TelegramIcon from "@/charterafrica/assets/icons/Type=telegram, Size=25, Color=Black.svg"; +import TikTokIcon from "@/charterafrica/assets/icons/Type=tiktok, Size=25, Color=Black.svg"; import TwitterIcon from "@/charterafrica/assets/icons/Type=twitter, Size=24, Color=CurrentColor.svg"; +import WhatsAppIcon from "@/charterafrica/assets/icons/Type=whatsapp, Size=25, Color=Black.svg"; +import YouTubeIcon from "@/charterafrica/assets/icons/Type=youtube, Size=24, Color=Black.svg"; +import { OrganisationImageLink } from "@/charterafrica/components/OrganisationCard"; +import Repository from "@/charterafrica/components/Repository"; import ToolCard from "@/charterafrica/components/ToolCard"; function getIcons({ socialMedia, email, github }) { @@ -43,12 +50,25 @@ const SocialMediaLink = React.forwardRef(function SocialMediaLink(props, ref) { slack: SlackIcon, linkedin: LinkedInIcon, telegram: TelegramIcon, + discord: DiscordIcon, + tiktok: TikTokIcon, + whatsapp: WhatsAppIcon, + instagram: InstagramIcon, + youtube: YouTubeIcon, }; + const largeIconSizes = ["discord", "telegram", "whatsapp"]; return href && icons[variant] ? ( - + -
- - +
+ +
{name} - {location} + {role} + {currentOrganisation ? ( + + {currentOrganisation} + + ) : null} + {location} + + {description} @@ -122,18 +178,63 @@ const Entity = React.forwardRef(function Entity(props, ref) { item xs="auto" container - sx={{ mt: 3, width: "100%" }} - justifyContent={{ xs: "center", sm: "flex-start" }} + sx={{ width: "100%" }} + justifyContent="left" columnSpacing={2} > {icons.map((icon) => ( - + ))} + {organisations.length ? ( + <> + + {organisationsTitle} + + + {organisations.map((org) => ( + + + + ))} + + + ) : null} + {repositories.length ? ( + + + {repositoriesTitle} + + + {repositories.map((repo) => ( + + + + ))} + + + ) : null} {tools.length ? ( <> renders unchanged 1`] = ` class="MuiBox-root css-1k9ek97" >
renders unchanged 1`] = `
John Doe
+ Developer +
+
+ Charter Africa +
+
San Francisco, CA
Full-stack web developer with over 5 years of experience.
+
+
+ Repositories +
+
+
+ + + diff --git a/apps/charterafrica/src/components/Entity/Entity.test.js b/apps/charterafrica/src/components/Entity/Entity.test.js index 54590acc6..be8c92c5b 100644 --- a/apps/charterafrica/src/components/Entity/Entity.test.js +++ b/apps/charterafrica/src/components/Entity/Entity.test.js @@ -12,8 +12,18 @@ const defaultProps = { name: "John Doe", location: "San Francisco, CA", description: "Full-stack web developer with over 5 years of experience.", - twitter: "https://twitter.com/johndoe", - github: "https://github.com/johndoe", + socialMedia: [ + { + name: "twitter", + link: "https://twitter.com/johndoe", + id: 1, + }, + { + name: "github", + link: "https://github.com/johndoe", + id: 2, + }, + ], email: "johndoe@example.com", image: "/static/images/avatar/1.jpg", tools: [ @@ -36,6 +46,28 @@ const defaultProps = { }, ], toolsTitle: "Favorite Tools", + role: "Developer", + currentOrganisation: "Charter Africa", + repositories: Array.from({ length: 3 }, (_, i) => ({ + id: 1, + name: `Repository ${i}`, + stargazers: 100, + visibility: "PUBLIC", + description: "Charter Africa website", + url: "https://charter.africa", + updatedAt: "2021-10-01T00:00:00Z", + techSkills: "React, Next.js, TypeScript", + })), + repositoriesTitle: "Repositories", + organisations: Array.from({ length: 3 }, (_, i) => ({ + id: 1, + name: `Organisation ${i}`, + avatarUrl: "/static/images/charterafrica.png", + link: { + href: "https://charter.africa", + }, + })), + organisationsTitle: "Organisations", }; describe("", () => { diff --git a/apps/charterafrica/src/components/OrganisationCard/OrganisationImageLink.js b/apps/charterafrica/src/components/OrganisationCard/OrganisationImageLink.js new file mode 100644 index 000000000..13b8e0451 --- /dev/null +++ b/apps/charterafrica/src/components/OrganisationCard/OrganisationImageLink.js @@ -0,0 +1,58 @@ +import { RichTypography } from "@commons-ui/core"; +import { Link, Figure } from "@commons-ui/next"; +import { Button, Stack } from "@mui/material"; +import PropTypes from "prop-types"; +import React from "react"; + +const OrganisationImageLink = React.forwardRef( + function OrganisationImageLink(props, ref) { + const { avatarUrl: image, link, name } = props; + + return ( + + ); + }, +); + +OrganisationImageLink.propTypes = { + name: PropTypes.string, + avatarUrl: PropTypes.string, +}; + +OrganisationImageLink.defaultProps = { + name: undefined, + avatarUrl: undefined, +}; + +export default OrganisationImageLink; diff --git a/apps/charterafrica/src/components/OrganisationCard/OrganisationImageLink.snap.js b/apps/charterafrica/src/components/OrganisationCard/OrganisationImageLink.snap.js new file mode 100644 index 000000000..9faad6639 --- /dev/null +++ b/apps/charterafrica/src/components/OrganisationCard/OrganisationImageLink.snap.js @@ -0,0 +1,38 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders unchanged 1`] = ` + +`; diff --git a/apps/charterafrica/src/components/OrganisationCard/OrganisationImageLink.test.js b/apps/charterafrica/src/components/OrganisationCard/OrganisationImageLink.test.js new file mode 100644 index 000000000..a1f31844a --- /dev/null +++ b/apps/charterafrica/src/components/OrganisationCard/OrganisationImageLink.test.js @@ -0,0 +1,24 @@ +import { createRender } from "@commons-ui/testing-library"; +import React from "react"; + +import OrganisationImageLink from "./OrganisationImageLink"; + +import theme from "@/charterafrica/theme"; + +// eslint-disable-next-line testing-library/render-result-naming-convention +const render = createRender({ theme }); + +const defaultProps = { + name: "Organisation Name", + avatarUrl: "/static/images/avatar/1.jpg", + link: { + href: "https://charter.africa", + }, +}; + +describe("", () => { + it("renders unchanged", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/apps/charterafrica/src/components/OrganisationCard/index.js b/apps/charterafrica/src/components/OrganisationCard/index.js index 8b775635d..ed50bf468 100644 --- a/apps/charterafrica/src/components/OrganisationCard/index.js +++ b/apps/charterafrica/src/components/OrganisationCard/index.js @@ -1,3 +1,5 @@ import OrganisationCard from "./OrganisationCard"; +import OrganisationImageLink from "./OrganisationImageLink"; export default OrganisationCard; +export { OrganisationImageLink }; diff --git a/apps/charterafrica/src/components/Repository/Repository.js b/apps/charterafrica/src/components/Repository/Repository.js new file mode 100644 index 000000000..9606b774e --- /dev/null +++ b/apps/charterafrica/src/components/Repository/Repository.js @@ -0,0 +1,72 @@ +import { RichTypography } from "@commons-ui/core"; +import { Link } from "@commons-ui/next"; +import { Grid, Chip } from "@mui/material"; +import React from "react"; + +import StarIcon from "@/charterafrica/assets/icons/Type=Star, Size=24, Color=CurrentColor.svg"; +import { neutral } from "@/charterafrica/colors"; +import formatDateTime from "@/charterafrica/utils/formatDate"; + +const Repository = React.forwardRef(function Tools(props, ref) { + const { + name, + stargazers, + visibility, + description, + url, + updatedAt, + techSkills, + sx, + } = props; + const updatedDate = formatDateTime(updatedAt, { includeTime: false }); + + return ( + + + + + {name} + + + {description} + + + {techSkills} + + + {updatedDate} + + + + + + {stargazers} + + + + + + ); +}); + +export default Repository; diff --git a/apps/charterafrica/src/components/Repository/Repository.snap.js b/apps/charterafrica/src/components/Repository/Repository.snap.js new file mode 100644 index 000000000..1aadefc2d --- /dev/null +++ b/apps/charterafrica/src/components/Repository/Repository.snap.js @@ -0,0 +1,66 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders unchanged 1`] = ` +
+ + +`; diff --git a/apps/charterafrica/src/components/Repository/Repository.test.js b/apps/charterafrica/src/components/Repository/Repository.test.js new file mode 100644 index 000000000..38010a89b --- /dev/null +++ b/apps/charterafrica/src/components/Repository/Repository.test.js @@ -0,0 +1,27 @@ +import { createRender } from "@commons-ui/testing-library"; +import React from "react"; + +import Repository from "./Repository"; + +import theme from "@/charterafrica/theme"; + +// eslint-disable-next-line testing-library/render-result-naming-convention +const render = createRender({ theme }); + +const defaultProps = { + id: 1, + name: "Repository 1", + stargazers: 100, + visibility: "PUBLIC", + description: "Charter Africa website", + url: "https://charter.africa", + updatedAt: "2021-10-01T00:00:00Z", + techSkills: "React, Next.js, TypeScript", +}; + +describe("", () => { + it("renders unchanged", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/apps/charterafrica/src/components/Repository/index.js b/apps/charterafrica/src/components/Repository/index.js new file mode 100644 index 000000000..ca287922a --- /dev/null +++ b/apps/charterafrica/src/components/Repository/index.js @@ -0,0 +1,3 @@ +import Repository from "./Repository"; + +export default Repository; diff --git a/apps/charterafrica/src/lib/data/common/processPageContributors.js b/apps/charterafrica/src/lib/data/common/processPageContributors.js index 7fafbd62c..415f2fca5 100644 --- a/apps/charterafrica/src/lib/data/common/processPageContributors.js +++ b/apps/charterafrica/src/lib/data/common/processPageContributors.js @@ -69,9 +69,7 @@ async function processPagePerson(page, api, context) { return null; } - const block = blocks.findIndex( - ({ slug: bSlug }) => bSlug === "our-contributors", - ); + const block = blocks.find(({ slug: bSlug }) => bSlug === "our-contributors"); const contributor = docs[0] || {}; const { docs: toolDocs } = await api.getCollection(TOOL_COLLECTION, { locale, @@ -91,6 +89,22 @@ async function processPagePerson(page, api, context) { }; }); + const { socialMedia = [] } = contributor; + if (contributor.source === "github") { + const github = { + link: `https://github.com/${contributor?.externalId || ""}`, + name: "gitHub", + }; + socialMedia.push(github); + } + if (contributor.email) { + const email = { + link: `mailto:${contributor.email}`, + name: "email", + }; + socialMedia.push(email); + } + return { ...page, blocks: [ @@ -99,18 +113,20 @@ async function processPagePerson(page, api, context) { id: block.id ?? null, image: contributor.avatarUrl ?? null, name: contributor?.fullName ?? contributor?.externalId ?? null, + role: contributor.role ?? null, + currentOrganisation: contributor.currentOrganisation ?? null, location: contributor.location ?? null, description: contributor.description ?? null, - email: contributor.email ?? null, toolsTitle: block?.toolsTitle ?? null, lastActive: contributor.lastActive ? formatDateTime(contributor.lastActive, {}) : null, - github: - contributor.source === "github" - ? `https://github.com/${contributor?.externalId || ""}` - : "", tools, + socialMedia, + repositories: contributor.repositories ?? [], + repositoriesTitle: block?.repositoriesTitle ?? null, + organisations: contributor.organisations ?? [], + organisationsTitle: block?.organisationsTitle ?? null, }, ], }; diff --git a/apps/charterafrica/src/lib/ecosystem/airtable/processData.js b/apps/charterafrica/src/lib/ecosystem/airtable/processData.js index 140e19fa6..7880e3f32 100644 --- a/apps/charterafrica/src/lib/ecosystem/airtable/processData.js +++ b/apps/charterafrica/src/lib/ecosystem/airtable/processData.js @@ -153,6 +153,7 @@ export function processContributor(item, config) { contributorTableColumns.socialMediaColumns, data, ); + const organisations = getValue(data, contributorTableColumns.organisations); const foundDescription = locales.reduce((acc, curr) => { const val = getValue(data, contributorTableColumns.description[curr]); if (val) { @@ -170,10 +171,16 @@ export function processContributor(item, config) { return { airtableId: data.id, classification: getValue(data, contributorTableColumns.classification), + role: getValue(data, contributorTableColumns.role), + currentOrganisation: getValue( + data, + contributorTableColumns.currentOrganisation, + ), externalId, repoLink, socialMedia, description, + organisations, }; } diff --git a/apps/charterafrica/src/lib/ecosystem/ecosystem/processData.js b/apps/charterafrica/src/lib/ecosystem/ecosystem/processData.js index 2b4854a0a..39d1d9473 100644 --- a/apps/charterafrica/src/lib/ecosystem/ecosystem/processData.js +++ b/apps/charterafrica/src/lib/ecosystem/ecosystem/processData.js @@ -15,7 +15,16 @@ export async function prepareContributors(airtableData, config) { const { contributors } = airtableData; await bulkMarkDeleted(CONTRIBUTORS_COLLECTION, contributors); const toProcess = airtableData?.contributors?.map(async (item) => { - return createCollection(CONTRIBUTORS_COLLECTION, item, config); + const rawOrganisations = item?.organisations || []; + const organisations = await getCollectionIdsPerAirtableId( + ORGANIZATION_COLLECTION, + rawOrganisations, + ); + const toCreate = { + ...item, + organisations, + }; + return createCollection(CONTRIBUTORS_COLLECTION, toCreate, config); }); return Promise.allSettled(toProcess); } @@ -56,18 +65,24 @@ export async function prepareTools(airtableData, config) { return Promise.allSettled(toProcess); } -export async function updateContributor(forceUpdate) { - const { docs } = await api.getCollection(CONTRIBUTORS_COLLECTION); +export async function updateContributor() { + const { docs } = await api.getCollection(CONTRIBUTORS_COLLECTION, { + pagination: false, + }); + const githubContributors = await github.bulkFetchContributors( + docs.map(({ externalId }) => externalId), + ); const updatePromises = docs.map(async (item) => { - const itemToFetch = forceUpdate ? { ...item, eTag: null } : item; - const updated = await github.fetchContributor(itemToFetch); + const updated = githubContributors[item.externalId]; return api.updateCollection(CONTRIBUTORS_COLLECTION, item.id, updated); }); return Promise.allSettled(updatePromises); } export async function updateOrganisation(forceUpdate) { - const { docs } = await api.getCollection(ORGANIZATION_COLLECTION); + const { docs } = await api.getCollection(ORGANIZATION_COLLECTION, { + pagination: false, + }); const updatePromises = docs.map(async (item) => { const itemToFetch = forceUpdate ? { ...item, eTag: null } : item; const updated = await github.fetchOrganisation(itemToFetch); @@ -77,7 +92,9 @@ export async function updateOrganisation(forceUpdate) { } export async function updateTool(forceUpdate) { - const { docs } = await api.getCollection(TOOL_COLLECTION); + const { docs } = await api.getCollection(TOOL_COLLECTION, { + pagination: false, + }); const updatePromises = docs.map(async (item) => { const itemToFetch = forceUpdate ? { ...item, eTag: null } : item; const updated = await github.fetchTool(itemToFetch); diff --git a/apps/charterafrica/src/lib/ecosystem/github/github.js b/apps/charterafrica/src/lib/ecosystem/github/github.js new file mode 100644 index 000000000..b20de6018 --- /dev/null +++ b/apps/charterafrica/src/lib/ecosystem/github/github.js @@ -0,0 +1,58 @@ +import * as Sentry from "@sentry/nextjs"; + +import fetchJson, { FetchError } from "@/charterafrica/utils/fetchJson"; + +const BASE_URL = "https://api.github.com"; + +async function graphQuery(query, variables) { + const url = `${BASE_URL}/graphql`; + const headers = { + "Content-Type": "application/json", + Authorization: `Bearer ${process.env.GITHUB_TOKEN}`, + }; + const data = { + variables, + query, + }; + const res = await fetchJson.post(url, { data, headers }); + if (res?.data) { + return res.data; + } + const message = `Unable to fetch from github errors ${JSON.stringify( + res.errors, + )}`; + Sentry.captureException(message); + throw new FetchError(message, res.errors, 500); +} + +async function restQuery(path, tag) { + const url = `${BASE_URL}/${path}`; + const headers = { + "If-None-Match": tag, + Authorization: `Bearer ${process.env.GITHUB_TOKEN}`, + }; + try { + const res = await fetch(url, { headers }); + if (res.ok) { + const response = await res.json(); + const eTag = res.headers.get("ETag"); + return { ...response, eTag }; + } + if (res.status !== 304) { + const response = await res.json(); + const message = `Error fetching "${url}" from github errors ${JSON.stringify( + response, + )}`; + throw new FetchError(message, res, 500); + } + return null; + } catch (e) { + Sentry.captureException(e); + return null; + } +} + +export default { + graphQuery, + restQuery, +}; diff --git a/apps/charterafrica/src/lib/ecosystem/github/index.js b/apps/charterafrica/src/lib/ecosystem/github/index.js index a07565e17..db76597f0 100644 --- a/apps/charterafrica/src/lib/ecosystem/github/index.js +++ b/apps/charterafrica/src/lib/ecosystem/github/index.js @@ -1,7 +1,11 @@ -import { fetchTool, fetchOrganisation, fetchContributor } from "./processData"; +import { + bulkFetchContributors, + fetchTool, + fetchOrganisation, +} from "./processData"; export default { fetchTool, fetchOrganisation, - fetchContributor, + bulkFetchContributors, }; diff --git a/apps/charterafrica/src/lib/ecosystem/github/processData.js b/apps/charterafrica/src/lib/ecosystem/github/processData.js index e113a7725..000299153 100644 --- a/apps/charterafrica/src/lib/ecosystem/github/processData.js +++ b/apps/charterafrica/src/lib/ecosystem/github/processData.js @@ -1,8 +1,7 @@ import * as Sentry from "@sentry/nextjs"; -import fetchJson, { FetchError } from "@/charterafrica/utils/fetchJson"; +import github from "./github"; -const BASE_URL = "https://api.github.com"; const GET_REPOSITORY = `query($repositoryOwner: String!, $repositoryName: String!) { repository(owner: $repositoryOwner, name: $repositoryName) { name @@ -47,52 +46,37 @@ const GET_REPOSITORY = `query($repositoryOwner: String!, $repositoryName: String } }`; -async function fetchRepository(variables) { - const url = `${BASE_URL}/graphql`; - const headers = { - "Content-Type": "application/json", - Authorization: `Bearer ${process.env.GITHUB_TOKEN}`, - }; - const data = { - variables, - query: GET_REPOSITORY, - }; - const res = await fetchJson.post(url, { data, headers }); - if (res?.data?.repository) { - return res.data.repository; - } - const message = `Unable to fetch ${variables.repositoryOwner}/${ - variables.repositoryName - } from github errors ${JSON.stringify(res.errors)}`; - Sentry.captureException(message); - throw new FetchError(message, res.errors, 500); -} - -async function fetchGithubApi(path, tag) { - const url = `${BASE_URL}/${path}`; - const headers = { - "If-None-Match": tag, - Authorization: `Bearer ${process.env.GITHUB_TOKEN}`, - }; - try { - const res = await fetch(url, { headers }); - if (res.ok) { - const response = await res.json(); - const eTag = res.headers.get("ETag"); - return { ...response, eTag }; - } - if (res.status !== 304) { - const response = await res.json(); - const message = `Error fetching "${url}" from github errors ${JSON.stringify( - response, - )}`; - throw new FetchError(message, res, 500); +function fetchUserQuery(username) { + return `user(login: $${username}) { + name + avatarUrl + url + email + location + login + websiteUrl + repositories(first: 3, orderBy: {field: STARGAZERS, direction: DESC}) { + edges { + node { + name + description + stargazers { + totalCount + } + visibility + url + updatedAt + languages(first:5) { + edges { + node { + name + } + } + } + } } - return null; - } catch (e) { - Sentry.captureException(e); - return null; } +}`; } export async function fetchTool({ externalId }) { @@ -108,34 +92,36 @@ export async function fetchTool({ externalId }) { return null; } - const data = await fetchRepository({ + const data = await github.graphQuery(GET_REPOSITORY, { repositoryOwner, repositoryName, }); - const techSkills = data.languages?.nodes?.map((language) => ({ + const { repository } = data; + + const techSkills = repository.languages?.nodes?.map((language) => ({ language: language?.name, })); - const commit = data?.defaultBranchRef?.target.history.edges?.[0]?.node; + const commit = repository?.defaultBranchRef?.target.history.edges?.[0]?.node; return { externalId, - repoLink: data.url, - link: data.homepageUrl, + repoLink: repository.url, + link: repository.homepageUrl, techSkills, lastCommit: { author: commit?.author?.name, committedDate: commit?.committedDate, message: commit?.message, }, - stars: data?.stargazers?.totalCount, - views: data?.watchers?.totalCount, - forks: data?.forks?.totalCount, + stars: repository?.stargazers?.totalCount, + views: repository?.watchers?.totalCount, + forks: repository?.forks?.totalCount, source: "github", - sourceUpdatedAt: new Date(data.updatedAt), + sourceUpdatedAt: new Date(repository.updatedAt), }; } export async function fetchOrganisation({ externalId, eTag }) { - const data = await fetchGithubApi(`orgs/${externalId}`, eTag); + const data = await github.restQuery(`orgs/${externalId}`, eTag); if (!data) { return null; } @@ -150,19 +136,97 @@ export async function fetchOrganisation({ externalId, eTag }) { }; } -export async function fetchContributor({ externalId, eTag }) { - const data = await fetchGithubApi(`users/${externalId}`, eTag); +function processGithubContributor(data) { if (!data) { return null; } + const { name, avatarUrl, url, email, location, websiteUrl, repositories } = + data; + + const repos = repositories?.edges?.map((edge) => { + const { + name: repoName, + description, + stargazers, + visibility, + url: repoURL, + updatedAt, + languages, + } = edge?.node ?? {}; + + const techSkills = languages?.edges + ?.map((language) => language?.node?.name) + .join(", "); + + return { + name: repoName, + description, + stargazers: stargazers?.totalCount, + visibility, + url: repoURL, + updatedAt, + techSkills, + }; + }); return { - fullName: data.name, - avatarUrl: data.avatar_url, - repoLink: data.html_url, - location: data.location, - website: data.blog, - email: data.email, - eTag: data.eTag, + fullName: name, + avatarUrl, + repoLink: url, + location, + website: websiteUrl, + email, + repositories: repos, }; } + +function chunkArray(array, chunkSize) { + return Array.from( + { length: Math.ceil(array.length / chunkSize) }, + (_, index) => array.slice(index * chunkSize, (index + 1) * chunkSize), + ); +} + +function constructGraphQLQuery(ids) { + const queryParts = ids.map((id) => { + const queryPart = fetchUserQuery(id); + return `${id}: ${queryPart}`; + }); + return `query(${ids + .map((id) => `$${id}: String!`) + .join(", ")}){\n${queryParts.join("\n")}\n}`; +} + +async function fetchContributorsData(users) { + const variables = users.reduce((acc, id) => { + const sanitizedId = id.replaceAll("-", "_"); + acc[sanitizedId] = id; + return acc; + }, {}); + + const query = constructGraphQLQuery(Object.keys(variables)); + const data = await github.graphQuery(query, variables); + + return Object.keys(data || {}).reduce((acc, key) => { + acc[variables[key]] = processGithubContributor(data[key]); + return acc; + }, {}); +} + +const USER_QUERY_LIMIT = 80; + +export async function bulkFetchContributors(externalIds) { + const contributors = {}; + const chunkedArrays = chunkArray(externalIds, USER_QUERY_LIMIT); + + const promises = chunkedArrays.map(async (arr) => { + try { + const data = await fetchContributorsData(arr); + Object.assign(contributors, data); + } catch (error) { + Sentry.captureMessage(error.message); + } + }); + await Promise.allSettled(promises); + return contributors; +} diff --git a/apps/charterafrica/src/payload/blocks/Contributors.js b/apps/charterafrica/src/payload/blocks/Contributors.js index 23047d40b..5d6e1defb 100644 --- a/apps/charterafrica/src/payload/blocks/Contributors.js +++ b/apps/charterafrica/src/payload/blocks/Contributors.js @@ -56,6 +56,28 @@ const Contributors = { required: true, localized: true, }, + { + type: "text", + label: { + en: "Repositories Title", + fr: "Titre des dépôts", + pt: "Título dos repositórios", + }, + name: "repositoriesTitle", + required: true, + localized: true, + }, + { + type: "text", + label: { + en: "Organisations Title", + fr: "Titre des organisations", + pt: "Título das organizações", + }, + name: "organisationsTitle", + required: true, + localized: true, + }, ], }; export default Contributors; diff --git a/apps/charterafrica/src/payload/collections/Contributors.js b/apps/charterafrica/src/payload/collections/Contributors.js index 62faec402..f339b617b 100644 --- a/apps/charterafrica/src/payload/collections/Contributors.js +++ b/apps/charterafrica/src/payload/collections/Contributors.js @@ -2,7 +2,10 @@ import avatarUrl from "../fields/avatarUrl"; import dateField from "../fields/dateField"; import slug from "../fields/slug"; import source from "../fields/source"; -import { CONTRIBUTORS_COLLECTION } from "../utils/collections"; +import { + CONTRIBUTORS_COLLECTION, + ORGANIZATION_COLLECTION, +} from "../utils/collections"; import nestCollectionUnderPage from "../utils/nestCollectionUnderPage"; function useFullNameOrExternalId({ doc }) { @@ -48,6 +51,26 @@ const Contributors = { readOnly: true, }, }, + { + name: "role", + type: "text", + label: { en: "Role", fr: "Rôle", pt: "Função" }, + admin: { + readOnly: true, + }, + }, + { + name: "currentOrganisation", + type: "text", + label: { + en: "Current Organization", + fr: "Organisation actuelle", + pt: "Organização atual", + }, + admin: { + readOnly: true, + }, + }, { name: "classification", type: "text", @@ -83,18 +106,6 @@ const Contributors = { readOnly: true, }, }, - { - name: "twitter", - type: "text", - label: { - en: "Twitter handle", - fr: "Twitter de la personne", - pt: "Twitter da Pessoa", - }, - admin: { - readOnly: true, - }, - }, { name: "email", type: "email", @@ -173,6 +184,78 @@ const Contributors = { position: "sidebar", }, }, + { + name: "organisations", + type: "relationship", + hasMany: true, + admin: { + readOnly: true, + }, + relationTo: ORGANIZATION_COLLECTION, + label: { + en: "Organizations", + fr: "Organisations", + pt: "Organizações", + }, + }, + { + name: "repositories", + type: "array", + admin: { + readOnly: true, + initCollapsed: true, + }, + label: { + en: "Repositories", + fr: "Dépôts", + pt: "Repositórios", + }, + fields: [ + { + name: "name", + type: "text", + label: { en: "Name", fr: "Nom", pt: "Nome" }, + }, + { + name: "description", + type: "textarea", + label: { en: "Description", fr: "Description", pt: "Descrição" }, + }, + { + name: "stargazers", + type: "number", + label: { en: "Stargazers", fr: "Stargazers", pt: "Stargazers" }, + }, + { + name: "visibility", + type: "text", + label: { en: "Visibility", fr: "Visibilité", pt: "Visibilidade" }, + }, + { + name: "url", + type: "text", + label: { en: "URL", fr: "URL", pt: "URL" }, + }, + { + name: "techSkills", + type: "text", + label: { + en: "Tech Skills", + fr: "Compétences techniques", + pt: "Habilidades técnicas", + }, + }, + { + name: "updatedAt", + type: "date", + label: { + en: "Updated At", + fr: "Mis à jour", + pt: "Atualizado em", + }, + }, + ], + }, ], hooks: { afterRead: [ diff --git a/apps/charterafrica/src/payload/globals/Ecosystem.js b/apps/charterafrica/src/payload/globals/Ecosystem.js index 191bb54dc..7811d37f8 100644 --- a/apps/charterafrica/src/payload/globals/Ecosystem.js +++ b/apps/charterafrica/src/payload/globals/Ecosystem.js @@ -369,6 +369,38 @@ const Ecosystem = { }, }, }), + airtableColumnSelect({ + schema, + tableField: "contributorTableId", + overrides: { + name: "role", + label: { en: "Role", fr: "Rôle", pt: "Função" }, + }, + }), + airtableColumnSelect({ + schema, + tableField: "contributorTableId", + overrides: { + name: "currentOrganisation", + label: { + en: "Current Organisation", + fr: "Organisation actuelle", + pt: "Organização atual", + }, + }, + }), + airtableColumnSelect({ + schema, + tableField: "contributorTableId", + overrides: { + name: "organisations", + label: { + en: "Organisations", + fr: "Organisations", + pt: "Organizações", + }, + }, + }), airtableColumnSelect({ schema, tableField: "contributorTableId", diff --git a/apps/charterafrica/src/theme/index.js b/apps/charterafrica/src/theme/index.js index 7e5ff961d..893441e04 100644 --- a/apps/charterafrica/src/theme/index.js +++ b/apps/charterafrica/src/theme/index.js @@ -110,6 +110,7 @@ const theme = createTheme({ p2SemiBold: initializeTypographyVariant(16, 19, 600), p3: initializeTypographyVariant(18, 21.6), p3SemiBold: initializeTypographyVariant(18, 21.6, 600), + p4: initializeTypographyVariant(23, 28), body1: undefined, body2: undefined, caption: initializeTypographyVariant(12, 14),