From 35d19192318f63e2d2e69b5eabdfb6f429d5b8a4 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Wed, 11 Nov 2020 20:00:49 -0500 Subject: [PATCH 01/10] Add prisma --- .gitignore | 1 + package.json | 2 ++ prisma/schema.prisma | 51 ++++++++++++++++++++++++++++++++++++++++++++ yarn.lock | 30 ++++++++++++++++++++++++++ 4 files changed, 84 insertions(+) create mode 100644 prisma/schema.prisma diff --git a/.gitignore b/.gitignore index f9bde29..06bdf7f 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ node_modules vars.yml .hokusai-tmp .google-api-creds.json +prisma/*.db diff --git a/package.json b/package.json index 112111b..5dff05c 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "dependencies": { "@artsy/next-layout": "^0.2.0", "@artsy/next-palette": "^1.0.0", + "@prisma/client": "2.11.0", "@react-hook/window-size": "^3.0.7", "await-to-js": "^2.1.1", "aws-sdk": "^2.668.0", @@ -40,6 +41,7 @@ "devDependencies": { "@next/bundle-analyzer": "^10.0.0", "@now/node": "^1.5.1", + "@prisma/cli": "2.11.0", "@types/cookie": "^0.3.3", "@types/csvtojson": "^1.1.5", "@types/jwt-decode": "^2.2.1", diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..f6f0029 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,51 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +datasource db { + provider = "sqlite" + url = "file:team.db" +} + +generator client { + provider = "prisma-client-js" +} + +model Member { + id Int @id @default(autoincrement()) + name String @unique + title String + email String @unique + city String? + country String? + floor String? + startDate DateTime + headshot String? + preferredPronouns String? + + orgs Organization[] + teams Team[] + subteams Subteam[] + + manager Member? @relation("MemberToMember", fields: [managerId], references: [id]) + managerId Int? + reports Member[] @relation("MemberToMember") + +} + +model Organization { + id Int @id @default(autoincrement()) + name String @unique + members Member[] +} + +model Team { + id Int @id @default(autoincrement()) + name String @unique + members Member[] +} + +model Subteam { + id Int @id @default(autoincrement()) + name String @unique + members Member[] +} diff --git a/yarn.lock b/yarn.lock index 8e5fd1e..17e38c3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1268,6 +1268,36 @@ dependencies: mkdirp "^1.0.4" +"@prisma/bar@0.0.1": + version "0.0.1" + resolved "https://registry.yarnpkg.com/@prisma/bar/-/bar-0.0.1.tgz#088c4fbbb79c588391437ade9fd3a85527e753b3" + integrity sha512-FVLhwVkbfhXlBhroWfIXMLi+3Jh9IEzYp+9z+MUUiw3ZsbcoAil7CN9/QIjHc4/TcCRyRfuSmT7qCnn4O+TjJw== + +"@prisma/cli@2.11.0": + version "2.11.0" + resolved "https://registry.yarnpkg.com/@prisma/cli/-/cli-2.11.0.tgz#34bdc5573ac40edae336b65fa73a164cae437622" + integrity sha512-RphW+1SPrEKgpuE5RFM0mv3BeVTF8MCRIyBt35Z9Z/E4YI30qgEWfZu6VfsNDarHRsFiJRKC73wx/aMQ2rLp4g== + dependencies: + "@prisma/bar" "0.0.1" + "@prisma/engines" "2.11.0-10.58369335532e47bdcec77a2f1e7c1fb83a463918" + +"@prisma/client@2.11.0": + version "2.11.0" + resolved "https://registry.yarnpkg.com/@prisma/client/-/client-2.11.0.tgz#574c1aa3b571ea01c0fa8dca348c6ba5db41dcc9" + integrity sha512-BF7K/yi5fAnrt7MelQqUueJyl06IGmIxf+7f5RxFSvyO6xZMbOYxhW21kV2wt10mOIS0khQbo0xY6w/8jViJuQ== + dependencies: + "@prisma/engines-version" "2.11.0-10.58369335532e47bdcec77a2f1e7c1fb83a463918" + +"@prisma/engines-version@2.11.0-10.58369335532e47bdcec77a2f1e7c1fb83a463918": + version "2.11.0-10.58369335532e47bdcec77a2f1e7c1fb83a463918" + resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-2.11.0-10.58369335532e47bdcec77a2f1e7c1fb83a463918.tgz#840bb5ca8707ed3b852d250c1bac9c75098682ee" + integrity sha512-qlkW4dKoW1dUnperWPuhFriZ/NTHlsKLhBbebxRa8qMuD3o37SvWIDGLjFOQx1N0Eb4H04rI3XxgjkWLFVlZCw== + +"@prisma/engines@2.11.0-10.58369335532e47bdcec77a2f1e7c1fb83a463918": + version "2.11.0-10.58369335532e47bdcec77a2f1e7c1fb83a463918" + resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-2.11.0-10.58369335532e47bdcec77a2f1e7c1fb83a463918.tgz#5f02f311ce48297ef3fa9861dcab5ec3e52f1371" + integrity sha512-0WaUybWM7J5zQuG/zYLbV+ZKx9/nzS7Ruu7Y0K2lXJKy3Z9koeVttq+Xt7tVmUX9TLgI1Rwhb9R2e1JMNDWbsw== + "@react-hook/debounce@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@react-hook/debounce/-/debounce-3.0.0.tgz#9eea8b5d81d4cb67cd72dd8657b3ff724afc7cad" From ed191ea6d09856d34dd4f4ad756f79cbe9db3a14 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Fri, 13 Nov 2020 18:48:00 -0500 Subject: [PATCH 02/10] Integrate sqlite via prisma, syncing from sheet --- prisma/schema.prisma | 33 ++- src/components/MemberDetails.tsx | 68 +++--- src/components/Sidebar/LinkSection.tsx | 5 +- src/components/Sidebar/index.tsx | 64 ++---- src/components/TeamMember.tsx | 10 +- src/data/locations.ts | 0 src/data/sidebar.ts | 59 +++++ src/data/teamMember.ts | 57 +++++ src/pages/_layout.tsx | 5 +- src/pages/api/sync.ts | 305 +++++++++++++++++++++++++ src/pages/index.tsx | 46 +--- src/pages/location/[location].tsx | 56 +++-- src/pages/member/[member].tsx | 94 +++++--- src/pages/org/[org].tsx | 87 ++++--- src/pages/subteam/[subteam].tsx | 69 +++--- src/pages/team/[team].tsx | 65 +++--- src/utils/index.ts | 5 +- src/utils/type-helpers.ts | 1 + 18 files changed, 723 insertions(+), 306 deletions(-) create mode 100644 src/data/locations.ts create mode 100644 src/data/sidebar.ts create mode 100644 src/data/teamMember.ts create mode 100644 src/pages/api/sync.ts create mode 100644 src/utils/type-helpers.ts diff --git a/prisma/schema.prisma b/prisma/schema.prisma index f6f0029..879579f 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -11,41 +11,52 @@ generator client { } model Member { - id Int @id @default(autoincrement()) + slug String @id name String @unique - title String email String @unique - city String? - country String? - floor String? + title String + slack String? + github String? startDate DateTime headshot String? preferredPronouns String? + introEmail String? + roleText String? orgs Organization[] teams Team[] subteams Subteam[] - manager Member? @relation("MemberToMember", fields: [managerId], references: [id]) - managerId Int? - reports Member[] @relation("MemberToMember") + location Location? @relation(fields: [locationSlug], references: [slug]) + locationSlug String? + manager Member? @relation("MemberToMember", fields: [managerSlug], references: [slug]) + reports Member[] @relation("MemberToMember") + managerSlug String? } model Organization { - id Int @id @default(autoincrement()) + slug String @id name String @unique members Member[] } model Team { - id Int @id @default(autoincrement()) + slug String @id name String @unique members Member[] } model Subteam { - id Int @id @default(autoincrement()) + slug String @id name String @unique members Member[] } + +model Location { + slug String @id + city String + country String + floor String? + members Member[] +} diff --git a/src/components/MemberDetails.tsx b/src/components/MemberDetails.tsx index d1d5e20..bd6fb38 100644 --- a/src/components/MemberDetails.tsx +++ b/src/components/MemberDetails.tsx @@ -1,17 +1,30 @@ -import { Member } from "pages"; import { Serif, Link } from "@artsy/palette"; import { formatDistanceToNow } from "date-fns"; import { capitalize } from "lodash-es"; import RouterLink from "next/link"; import { normalizeParam } from "utils"; -import { Fragment } from "react"; +import { FC, Fragment } from "react"; import { Grid } from "components/Grid"; +import { Location, Member, Organization, Subteam, Team } from "@prisma/client"; -interface MemberDetailsProps { - member: Member; +interface MemberDetails { + member: Member & { + orgs: Organization[]; + locations: Location[]; + teams: Team[]; + subteams: Subteam[]; + manager: { + name: string; + slug: string; + }; + reports: { + name: string; + slug: string; + }[]; + }; } -export function MemberDetails({ member }: MemberDetailsProps) { +export function MemberDetails({ member }: MemberDetails) { const { manager, reports, orgs, teams, subteams } = member; const showOrgs = member.orgs.length > 0; const showTeams = @@ -31,14 +44,14 @@ export function MemberDetails({ member }: MemberDetailsProps) { > <> {/* Joined time with email link */} - {member.start_date && ( + {member.startDate && ( <> Joined: - + - {capitalize(formatDistanceToNow(new Date(member.start_date)))}{" "} + {capitalize(formatDistanceToNow(new Date(member.startDate)))}{" "} ago @@ -46,12 +59,12 @@ export function MemberDetails({ member }: MemberDetailsProps) { )} {/* Preferred pronouns */} - {member.preferred_pronouns && ( + {member.preferredPronouns && ( <> Pronouns: - {member.preferred_pronouns} + {member.preferredPronouns} )} @@ -63,10 +76,10 @@ export function MemberDetails({ member }: MemberDetailsProps) { {orgs.map((org) => ( - - + + - {org} + {org.name} @@ -84,10 +97,10 @@ export function MemberDetails({ member }: MemberDetailsProps) { {teams.map((team) => ( - - + + - {team} + {team.name} @@ -104,13 +117,10 @@ export function MemberDetails({ member }: MemberDetailsProps) { {subteams.map((subteam) => ( - - + + - {subteam} + {subteam.name} @@ -125,11 +135,7 @@ export function MemberDetails({ member }: MemberDetailsProps) { Manager: - + {manager.name} @@ -145,12 +151,8 @@ export function MemberDetails({ member }: MemberDetailsProps) { {reports.map((report) => ( - - + + {report.name} diff --git a/src/components/Sidebar/LinkSection.tsx b/src/components/Sidebar/LinkSection.tsx index 0f14bd2..31fd025 100644 --- a/src/components/Sidebar/LinkSection.tsx +++ b/src/components/Sidebar/LinkSection.tsx @@ -8,7 +8,6 @@ export interface LinkConfig { external?: boolean; count?: number; href: string; - as?: string; } interface LinkSectionProps { @@ -24,7 +23,7 @@ export const LinkSection = ({ title, links }: LinkSectionProps) => { {links.map((link) => { return ( - + {link.external ? ( <> @@ -33,7 +32,7 @@ export const LinkSection = ({ title, links }: LinkSectionProps) => { ) : ( - + { const searchParam = encodeURI(searchTerm); @@ -39,37 +39,6 @@ const search = debounce((router: NextRouter, searchTerm: string) => { : router.push(pathname, as); }, 200); -const aggregateMemberLinks = ( - members: Member[], - field: keyof Member, - prefix: string -) => { - const isArrayType = Array.isArray(members[0][field]); - let aggregateGroup: [fieldValue: string, group: Member[]][] = []; - if (isArrayType) { - let fieldAggregate: string[] = Array.from( - new Set(members.flatMap((member) => member[field])) - ) as string[]; - aggregateGroup = fieldAggregate.map((fieldValue) => [ - fieldValue, - members.filter((member) => - (member[field] as string[]).includes(fieldValue) - ), - ]); - } - - return (isArrayType - ? aggregateGroup - : Object.entries(groupBy(members, field)) - ) - .map(([fieldValue, group]) => ({ - text: fieldValue, - count: (group as any)?.length, - href: `/${prefix}/${normalizeParam(fieldValue)}`, - })) - .filter(({ text }) => text); -}; - const helpfulLinks: LinkConfig[] = [ { text: "Atlas", @@ -103,7 +72,7 @@ const SidebarContainer = styled(Flex)` `; interface SidebarProps { - data?: any; + data: SidebarData; } export const Sidebar = ({ data }: SidebarProps) => { @@ -162,18 +131,19 @@ export const Sidebar = ({ data }: SidebarProps) => { {/* This spacer should have an mb of the height above + 30px */} - - - + {data.map(([title, prefix, entries]) => { + return ( + ({ + text, + href: `/${prefix}/${slug}`, + count, + }))} + /> + ); + })} ); }; diff --git a/src/components/TeamMember.tsx b/src/components/TeamMember.tsx index 93d8138..264048a 100644 --- a/src/components/TeamMember.tsx +++ b/src/components/TeamMember.tsx @@ -44,7 +44,7 @@ export const TeamMember: FC = (props) => { {showAvatar && ( <> - {member.start_date && isWeekOf(new Date(member.start_date)) && ( + {member.startDate && isWeekOf(new Date(member.startDate)) && ( = (props) => { zIndex="10" /> )} - {member.avatar ? ( + {member.headshot ? ( ( @@ -74,9 +74,9 @@ export const TeamMember: FC = (props) => { {member.name} - {member.preferred_pronouns && ( + {member.preferredPronouns && ( - {member.preferred_pronouns} + {member.preferredPronouns} )} diff --git a/src/data/locations.ts b/src/data/locations.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/data/sidebar.ts b/src/data/sidebar.ts new file mode 100644 index 0000000..48c2d7e --- /dev/null +++ b/src/data/sidebar.ts @@ -0,0 +1,59 @@ +import { PrismaClient } from "@prisma/client"; + +export async function getLocationAggregate() { + const prisma = new PrismaClient(); + const results = await prisma.location.findMany({ + include: { + members: true, + }, + }); + return results.map(({ slug, city, members }) => ({ + slug, + name: city, + count: members.length, + })); +} + +export async function getOrgAggregate() { + const prisma = new PrismaClient(); + const results = await prisma.organization.findMany({ + include: { + members: true, + }, + }); + + return results.map(({ slug, name, members }) => ({ + slug, + name, + count: members.length, + })); +} + +export async function getTeamAggregate() { + const prisma = new PrismaClient(); + const results = await prisma.team.findMany({ + include: { + members: true, + }, + }); + + return results.map(({ slug, name, members }) => ({ + slug, + name, + count: members.length, + })); +} + +export type SidebarData = [ + title: string, + prefix: string, + entries: { slug: string; name: string; count: number }[] +][]; + +export const getSidebarData = async (): Promise => { + return [ + ["Locations", "location", await getLocationAggregate()], + ["Organizations", "org", await getOrgAggregate()], + ["Teams", "team", await getTeamAggregate()], + ]; +}; diff --git a/src/data/teamMember.ts b/src/data/teamMember.ts new file mode 100644 index 0000000..e3b2639 --- /dev/null +++ b/src/data/teamMember.ts @@ -0,0 +1,57 @@ +import { + MemberCreateInput, + MemberGetPayload, + MemberSelect, + MemberWhereInput, + PrismaClient, +} from "@prisma/client"; +import { UnWrapPromise } from "utils/type-helpers"; + +export type MemberIndexListing = UnWrapPromise< + ReturnType +>; +export async function getMembersIndex(where: MemberWhereInput = {}) { + const prisma = new PrismaClient(); + const data = await prisma.member.findMany({ + select: { + name: true, + startDate: true, + title: true, + headshot: true, + orgs: { + select: { + name: true, + }, + }, + teams: { + select: { + name: true, + }, + }, + }, + where, + }); + return data.map((datum) => ({ + ...datum, + startDate: datum.startDate.toISOString(), + orgs: datum.orgs.map(({ name }) => name), + teams: datum.teams.map(({ name }) => name), + })); +} + +export async function getMemberField( + field: keyof MemberSelect +): Promise { + const prisma = new PrismaClient(); + const data = ( + await prisma.member.findMany({ + select: { + [field]: true, + }, + distinct: [field as any], + }) + ) + .map((d: any) => d[field]) + .filter((d) => !!d); + return data; +} diff --git a/src/pages/_layout.tsx b/src/pages/_layout.tsx index 1e8a332..f3cff5d 100644 --- a/src/pages/_layout.tsx +++ b/src/pages/_layout.tsx @@ -12,18 +12,19 @@ const PageContainer = styled(Box)` interface TeamProps { mdx?: boolean; data?: any; + sidebarData?: any; errorCode?: number; errorMessage?: string; } export const Layout: React.FC = ({ children, ...props }) => { - const { errorCode, data, errorMessage } = props; + const { errorCode, data, sidebarData, errorMessage } = props; if (errorCode) { return ; } return ( - + { + const values = cellValue + .split(";") + .map((v) => v.trim()) + .filter((v) => v.length > 0); + + return values.length > 0 ? values : undefined; +}; + +export default async function sync(req: NowRequest, res: NowResponse) { + // 1. grab sheet + const { data } = await sheets.spreadsheets.values.get({ + spreadsheetId: process.env.SHEET_ID as string, + range: "Team Members!A1:AA500", + }); + + if (!data.values) { + res.status(500).send("Sheet values empty").end(); + return; + } + + const [keys, ...memberRowsWithWhitespace] = data.values; + const memberRows = memberRowsWithWhitespace.map((row) => + row.map((cell) => cell.trim()) + ); + + /** Represents the mapping of the gsheet column names to the returned data */ + const column = zipObject(keys, range(keys.length)); + + /** + * 2. Create many mapped non-member entities + * Many-to-many relationships require some extra boilerplate... + */ + + const allOrgs = Array.from( + new Set(memberRows.flatMap((row) => splitMultiValueCell(row[column.org]))) + ); + const allTeams = Array.from( + new Set(memberRows.flatMap((row) => splitMultiValueCell(row[column.team]))) + ); + const allSubteams = Array.from( + new Set( + memberRows.flatMap((row) => splitMultiValueCell(row[column.subteam])) + ) + ); + + const body = (slug: string, name: string) => ({ + create: { + slug, + name, + }, + update: { + name, + }, + where: { + slug, + }, + }); + + // Create or update orgs + for (let org of allOrgs) { + if (!org) continue; + const slug = normalizeParam(org); + await prisma.organization.upsert(body(slug, org)); + } + + // Create or update teams + for (let team of allTeams) { + if (!team) continue; + const slug = normalizeParam(team); + await prisma.team.upsert(body(slug, team)); + } + + // Create or update subteams + for (let subteam of allSubteams) { + if (!subteam) continue; + const slug = normalizeParam(subteam); + await prisma.subteam.upsert(body(slug, subteam)); + } + + /** + * An array of cells from one row of the gsheet. To access the data use + * the following pattern: + * + * `row[column.]` + * + * e.g. + * + * `row[column.email]` + * */ + let row; + + // 3. Create or update team members + for (row of memberRows) { + row = row.map((r) => r.trim()); + const orgs = splitMultiValueCell(row[column.org]); + const teams = splitMultiValueCell(row[column.team]); + const subteams = splitMultiValueCell(row[column.subteam]); + + const [err] = await to( + prisma.member.upsert({ + create: { + name: row[column.name], + slug: normalizeParam(row[column.name]), + title: row[column.title], + email: row[column.email], + headshot: row[column.headshot], + preferredPronouns: row[column.preferred_pronouns], + introEmail: row[column.intro_email], + roleText: row[column.role_text], + startDate: new Date(row[column.start_date]), + slack: row[column.slack_handle], + github: row[column.github_handle], + location: { + connectOrCreate: { + where: { + slug: `${normalizeParam(row[column.city])}-${normalizeParam( + row[column.country] + )}`, + }, + create: { + slug: `${normalizeParam(row[column.city])}-${normalizeParam( + row[column.country] + )}`, + city: row[column.city], + country: row[column.country], + floor: row[column.floor], + }, + }, + }, + orgs: { + connect: orgs?.map((org) => ({ + slug: normalizeParam(org), + })), + }, + teams: { + connect: teams?.map((team) => ({ + slug: normalizeParam(team), + })), + }, + subteams: { + connect: subteams?.map((subteam) => ({ + slug: normalizeParam(subteam), + })), + }, + }, + update: { + name: row[column.name], + slug: normalizeParam(row[column.name]), + title: row[column.title], + email: row[column.email], + headshot: row[column.headshot], + preferredPronouns: row[column.preferred_pronouns], + introEmail: row[column.intro_email], + roleText: row[column.role_text], + startDate: new Date(row[column.start_date]), + slack: row[column.slack_handle], + github: row[column.github_handle], + location: { + connectOrCreate: { + where: { + slug: `${normalizeParam(row[column.city])}-${normalizeParam( + row[column.country] + )}`, + }, + create: { + slug: `${normalizeParam(row[column.city])}-${normalizeParam( + row[column.country] + )}`, + city: row[column.city], + country: row[column.country], + floor: row[column.floor], + }, + }, + }, + orgs: { + set: + orgs?.map((value) => ({ + slug: normalizeParam(value), + })) ?? [], + }, + teams: { + set: + teams?.map((value) => ({ + slug: normalizeParam(value), + })) ?? [], + }, + subteams: { + set: + subteams?.map((value) => ({ + slug: normalizeParam(value), + })) ?? [], + }, + }, + where: { + slug: normalizeParam(row[column.name]), + }, + }) + ); + + if (err) { + console.error(`Unable to load ${row[column.name]}`); + console.error(err); + } + } + + // 4. Associate reports relationship + for (let row of memberRows) { + if (row[column.reports_to] === "") continue; + const [err] = await to( + prisma.member.update({ + where: { + name: row[column.name], + }, + data: { + manager: { + connect: { name: row[column.reports_to] }, + }, + }, + }) + ); + + if (err) { + console.error(`unable to assign manager for ${row[column.name]}`); + } + } + + // 5. Delete any unused entities + const slug = { + slug: true, + members: { + select: { + slug: true, + }, + take: 1, + }, + } as const; + + const unused = (selection: { slug: string; members: { slug: string }[] }[]) => + selection + .filter((select) => select.members.length === 0) + .map(({ slug }) => ({ slug })); + + const unusedLocations = unused( + await prisma.location.findMany({ select: slug }) + ); + if (unusedLocations.length > 0) { + const deletedLocations = await prisma.location.deleteMany({ + where: { OR: unusedLocations }, + }); + console.log("locations deleted:", deletedLocations); + } + + const unusedOrgs = unused( + await prisma.organization.findMany({ select: slug }) + ); + if (unusedOrgs.length > 0) { + const deletedOrgs = await prisma.organization.deleteMany({ + where: { OR: unusedOrgs }, + }); + console.log("orgs deleted:", deletedOrgs); + } + + const unusedTeams = unused(await prisma.team.findMany({ select: slug })); + if (unusedTeams.length > 0) { + const deletedTeams = await prisma.team.deleteMany({ + where: { OR: unusedTeams }, + }); + console.log("teams deleted:", deletedTeams); + } + + const unusedSubteams = unused( + await prisma.subteam.findMany({ select: slug }) + ); + if (unusedSubteams.length > 0) { + const deletedSubteams = await prisma.subteam.deleteMany({ + where: { OR: unusedSubteams }, + }); + console.log("subteams deleted:", deletedSubteams); + } + + res.status(200).end(); +} diff --git a/src/pages/index.tsx b/src/pages/index.tsx index adf21b7..7931042 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -5,53 +5,21 @@ import { NoResults as DefaultNoResults } from "components/NoResults"; import { FC } from "react"; import Error from "next/error"; import { useSearchParam } from "utils"; -import { getMembers } from "../data/team"; import { TeamMember } from "../components/TeamMember"; - -export interface Member { - name: string; - title?: string; - /** @deprecated prefer `orgs` */ - org?: string; - orgs: string[]; - /** @deprecated prefer `teams` */ - team?: string; - teams: string[]; - /** @deprecated prefer `subteams` */ - subteam?: string; - subteams: string[]; - reports_to?: string; - team_rank?: number; - email?: string; - city?: string; - country?: string; - floor?: string; - phone?: string; - start_date?: string; - headshot?: string; - avatar?: string; - role_text?: string; - intro_email?: string; - slack_handle?: string; - github_handle?: string; - seat?: string; - preferred_pronouns?: string; - profileImage?: string; - manager?: Member; - reports?: Member[]; -} +import { getMembersIndex, MemberIndexListing } from "data/teamMember"; +import { getSidebarData } from "data/sidebar"; export interface ServerProps { - data?: Member[]; + data?: MemberIndexListing; title?: string; NoResults?: typeof DefaultNoResults; } -export const getStaticProps: GetStaticProps = async () => { - const members = await getMembers(); +export const getStaticProps: GetStaticProps = async () => { return { props: { - data: members, + data: await getMembersIndex(), + sidebarData: await getSidebarData(), }, }; }; @@ -68,7 +36,7 @@ const TeamNav: FC = (props) => { return ; } - const group: { [groupLetter: string]: Member[] } = {}; + const group: { [groupLetter: string]: MemberIndexListing } = {}; data .filter((member) => normalizeSearchTerm(member.name).includes( diff --git a/src/pages/location/[location].tsx b/src/pages/location/[location].tsx index 4fc7117..04ddb29 100644 --- a/src/pages/location/[location].tsx +++ b/src/pages/location/[location].tsx @@ -1,19 +1,22 @@ -import { useRouter } from "next/router"; -import TeamNav, { ServerProps, Member } from "../index"; -import { Spinner } from "@artsy/palette"; -import { normalizeParam } from "utils"; +import TeamNav from "../index"; import { NoResults } from "components/NoResults"; import { FC } from "react"; -import Error from "next/error"; import { GetStaticProps, GetStaticPaths } from "next"; -import { getMembers, getMemberProperty } from "../../data/team"; +import { getMembersIndex, MemberIndexListing } from "data/teamMember"; +import { getSidebarData } from "data/sidebar"; +import { PrismaClient } from "@prisma/client"; export const getStaticPaths: GetStaticPaths = async () => { - const locations = await getMemberProperty("city"); + const prisma = new PrismaClient(); + const locations = await prisma.location.findMany({ + select: { + slug: true, + }, + }); return { - paths: locations.map((location) => ({ + paths: locations.map(({ slug }) => ({ params: { - location: normalizeParam(location!), + location: slug, }, })), fallback: false, @@ -21,43 +24,38 @@ export const getStaticPaths: GetStaticPaths = async () => { }; export const getStaticProps: GetStaticProps = async ({ params }) => { - const members = await getMembers(); + const locationSlug = params?.location as string; + const prisma = new PrismaClient(); + const members = await getMembersIndex({ + locationSlug, + }); + const location = await prisma.location.findFirst({ + select: { city: true }, + where: { slug: locationSlug }, + }); return { props: { data: members, - location: params?.location, + sidebarData: await getSidebarData(), + location: location?.city, }, }; }; interface LocationProps { - data: Member[]; + data: MemberIndexListing; location: string; } const Location: FC = (props) => { - const { location } = props; - - if (!props.location) { - return ; - } - - let formattedLocation = ""; - - const data = props.data.filter((member) => { - if (member.city && normalizeParam(member.city) === location) { - formattedLocation = member.city; - return true; - } - return false; - }); + const { data, location } = props; return ( } + title={location} + NoResults={() => } /> ); }; diff --git a/src/pages/member/[member].tsx b/src/pages/member/[member].tsx index bad2be0..0e984af 100644 --- a/src/pages/member/[member].tsx +++ b/src/pages/member/[member].tsx @@ -3,40 +3,72 @@ import Error from "next/error"; import { normalizeParam, useDelay } from "utils"; import { FC, useEffect, useState } from "react"; import { H1 } from "components/Typography"; -import { Member as MemberType } from "../index"; import { GetStaticProps, GetStaticPaths } from "next"; -import { getMembers, getMemberProperty } from "../../data/team"; -import { MemberDetails } from "components/MemberDetails"; import { useAreaGrid } from "components/Grid"; import { isWeekOf, relativeDaysTillAnniversary, isDayOf } from "utils/date"; import { AwardIcon } from "components/AwardIcon"; +import { MemberDetails } from "components/MemberDetails"; import { useWindowSize } from "@react-hook/window-size"; import Confetti from "react-confetti"; +import { PrismaClient } from "@prisma/client"; +import { getSidebarData } from "data/sidebar"; +import { UnWrapPromise } from "utils/type-helpers"; -export const getStaticProps: GetStaticProps = async ({ params }) => { - const members = await getMembers(); - return { - props: { - data: members, - member: members.find( - (member) => normalizeParam(member.name) === params?.member - ), +export const getStaticPaths: GetStaticPaths<{ member: string }> = async () => { + const prisma = new PrismaClient(); + const members = await prisma.member.findMany({ + select: { + slug: true, }, - }; -}; - -export const getStaticPaths: GetStaticPaths = async () => { - const names = await getMemberProperty("name"); + }); return { - paths: names.map((name) => ({ + paths: members.map(({ slug }) => ({ params: { - member: normalizeParam(name), + member: slug, }, })), fallback: false, }; }; +export const getStaticProps = async ({ + params, +}: { + params: { member: string }; +}) => { + const prisma = new PrismaClient(); + const memberSlug = params.member as string; + const member = await prisma.member.findFirst({ + include: { + location: true, + orgs: true, + teams: true, + subteams: true, + manager: { + select: { + slug: true, + name: true, + }, + }, + reports: { + select: { + slug: true, + name: true, + }, + }, + }, + where: { + slug: memberSlug, + }, + }); + return { + props: { + sidebarData: await getSidebarData(), + member: JSON.parse(JSON.stringify(member)), + }, + }; +}; + const area = ["Heading", "Image", "Details", "Summary"] as const; type AreaType = typeof area[number]; @@ -53,8 +85,8 @@ const largeLayout: AreaType[][] = [ ["Summary", "Details"], ]; -interface MemberProps { - member: MemberType; +export interface MemberProps { + member: UnWrapPromise>["props"]["member"]; } const Member: FC = ({ member }) => { @@ -70,14 +102,14 @@ const Member: FC = ({ member }) => { } useEffect(() => { - if (member.start_date && !finished) { + if (member.startDate && !finished) { setShowConfetti(true); } - }, [setShowConfetti, member.start_date, finished]); + }, [setShowConfetti, member.startDate, finished]); return ( <> - {showConfetti && isDayOf(new Date(member.start_date!)) && ( + {showConfetti && isDayOf(new Date(member.startDate!)) && ( = ({ member }) => {

{member.name}

- {member.start_date && isWeekOf(new Date(member.start_date)) && ( + {member.startDate && isWeekOf(new Date(member.startDate)) && ( <> Artsyversary{" "} - {relativeDaysTillAnniversary(new Date(member.start_date))} + {relativeDaysTillAnniversary(new Date(member.startDate))} @@ -110,9 +142,7 @@ const Member: FC = ({ member }) => {
- {member.profileImage && ( - - )} + {member.headshot && } @@ -121,14 +151,14 @@ const Member: FC = ({ member }) => { {member.title} )} - {member.city && ( + {member.location?.city && ( - {member.city} + {member.location.city} )} - {member.role_text && ( + {member.roleText && ( - {member.role_text} + {member.roleText} )} diff --git a/src/pages/org/[org].tsx b/src/pages/org/[org].tsx index 97d503f..25d873d 100644 --- a/src/pages/org/[org].tsx +++ b/src/pages/org/[org].tsx @@ -1,70 +1,67 @@ -import { useRouter } from "next/router"; -import TeamNav, { ServerProps } from "../index"; -import { Spinner } from "@artsy/palette"; -import { normalizeParam } from "utils"; +import TeamNav from "../index"; import { NoResults } from "components/NoResults"; import { FC } from "react"; -import Error from "next/error"; -import { getMembers, getMemberProperty } from "../../data/team"; import { GetStaticProps, GetStaticPaths } from "next"; +import { PrismaClient } from "@prisma/client"; +import { getMembersIndex, MemberIndexListing } from "data/teamMember"; +import { getSidebarData } from "data/sidebar"; -export const getStaticPaths: GetStaticPaths = async () => { - const orgs = await getMemberProperty("orgs"); +export const getStaticPaths: GetStaticPaths<{ org: string }> = async () => { + const prisma = new PrismaClient(); + const orgs = await prisma.organization.findMany({ + select: { + slug: true, + }, + }); return { - paths: orgs.flat().map((org) => ({ + paths: orgs.map(({ slug }) => ({ params: { - org: normalizeParam(org), + org: slug, }, })), fallback: false, }; }; -export const getStaticProps: GetStaticProps = async () => { - const members = await getMembers(); +export const getStaticProps: GetStaticProps = async ({ params }) => { + const orgSlug = params?.org as string; + const prisma = new PrismaClient(); + const members = await getMembersIndex({ + orgs: { + some: { + slug: orgSlug, + }, + }, + }); + const org = await prisma.organization.findFirst({ + select: { name: true }, + where: { slug: orgSlug }, + }); return { props: { data: members, + sidebarData: await getSidebarData(), + org: org?.name, }, }; }; -const Organization: FC = (props) => { - const router = useRouter(); - - if (router.isFallback) { - return ; - } - - if (!props.data) { - return ; - } - const org = Array.isArray(router.query.org) - ? router.query.org[0] - : router.query.org; - let formattedOrg = ""; +interface OrgPageProps { + data: MemberIndexListing; + org?: string; +} - if (!org) { - return ; - } - - const data = props.data.filter((member) => { - const possibleOrg = member.orgs.find((t) => normalizeParam(t) === org); - if (!possibleOrg) return false; - formattedOrg = possibleOrg; - return true; - }); +const Org: FC = (props) => { + const { data, org } = props; return ( - <> - } - /> - + } + /> ); }; -export default Organization; +export default Org; diff --git a/src/pages/subteam/[subteam].tsx b/src/pages/subteam/[subteam].tsx index d9e2238..b32d877 100644 --- a/src/pages/subteam/[subteam].tsx +++ b/src/pages/subteam/[subteam].tsx @@ -1,60 +1,67 @@ -import { useRouter } from "next/router"; -import TeamNav, { ServerProps } from "../index"; -import { normalizeParam } from "utils"; +import TeamNav from "../index"; import { NoResults } from "components/NoResults"; import { FC } from "react"; -import Error from "next/error"; import { GetStaticProps, GetStaticPaths } from "next"; -import { getMembers, getMemberProperty } from "../../data/team"; +import { PrismaClient } from "@prisma/client"; +import { getMembersIndex, MemberIndexListing } from "data/teamMember"; +import { getSidebarData } from "data/sidebar"; -export const getStaticPaths: GetStaticPaths = async () => { - const subteams = await getMemberProperty("subteam"); +export const getStaticPaths: GetStaticPaths<{ subteam: string }> = async () => { + const prisma = new PrismaClient(); + const subteams = await prisma.subteam.findMany({ + select: { + slug: true, + }, + }); return { - paths: subteams.map((subteam) => ({ + paths: subteams.map(({ slug }) => ({ params: { - subteam: normalizeParam(subteam), + subteam: slug, }, })), fallback: false, }; }; -export const getStaticProps: GetStaticProps = async () => { - const members = await getMembers(); +export const getStaticProps: GetStaticProps = async ({ params }) => { + const subteamSlug = params?.subteam as string; + const prisma = new PrismaClient(); + const members = await getMembersIndex({ + subteams: { + some: { + slug: subteamSlug, + }, + }, + }); + const subteam = await prisma.subteam.findFirst({ + select: { name: true }, + where: { slug: subteamSlug }, + }); return { props: { data: members, + sidebarData: await getSidebarData(), + subteam: subteam?.name, }, }; }; -const Subteam: FC = (props) => { - const router = useRouter(); - - if (!props.data) { - return ; - } +interface SubteamPageProps { + data: MemberIndexListing; + subteam?: string; +} - const subteam = router.query.subteam; - let formattedSubteam = ""; - - const data = props.data.filter((member) => { - const possibleSubteam = member.subteams.find( - (t) => normalizeParam(t) === subteam - ); - if (!possibleSubteam) return false; - formattedSubteam = possibleSubteam; - return true; - }); +const Team: FC = (props) => { + const { data, subteam } = props; return ( } + NoResults={() => } /> ); }; -export default Subteam; +export default Team; diff --git a/src/pages/team/[team].tsx b/src/pages/team/[team].tsx index bcebf10..eb3fcd1 100644 --- a/src/pages/team/[team].tsx +++ b/src/pages/team/[team].tsx @@ -1,56 +1,65 @@ -import { useRouter } from "next/router"; -import TeamNav, { ServerProps } from "../index"; -import { normalizeParam } from "utils"; +import TeamNav from "../index"; import { NoResults } from "components/NoResults"; import { FC } from "react"; -import Error from "next/error"; import { GetStaticProps, GetStaticPaths } from "next"; -import { getMembers, getMemberProperty } from "../../data/team"; +import { PrismaClient } from "@prisma/client"; +import { getMembersIndex, MemberIndexListing } from "data/teamMember"; +import { getSidebarData } from "data/sidebar"; -export const getStaticPaths: GetStaticPaths = async () => { - const teams = await getMemberProperty("team"); +export const getStaticPaths: GetStaticPaths<{ team: string }> = async () => { + const prisma = new PrismaClient(); + const teams = await prisma.team.findMany({ + select: { + slug: true, + }, + }); return { - paths: teams.map((team) => ({ + paths: teams.map(({ slug }) => ({ params: { - team: normalizeParam(team), + team: slug, }, })), fallback: false, }; }; -export const getStaticProps: GetStaticProps = async () => { - const members = await getMembers(); +export const getStaticProps: GetStaticProps = async ({ params }) => { + const teamSlug = params?.team as string; + const prisma = new PrismaClient(); + const members = await getMembersIndex({ + teams: { + some: { + slug: teamSlug, + }, + }, + }); + const team = await prisma.team.findFirst({ + select: { name: true }, + where: { slug: teamSlug }, + }); return { props: { data: members, + sidebarData: await getSidebarData(), + team: team?.name, }, }; }; -const Team: FC = (props) => { - const router = useRouter(); - - if (!props.data) { - return ; - } +interface TeamPageProps { + data: MemberIndexListing; + team?: string; +} - const team = router.query.team; - let formattedTeam = ""; - - const data = props.data.filter((member) => { - const possibleTeam = member.teams.find((t) => normalizeParam(t) === team); - if (!possibleTeam) return false; - formattedTeam = possibleTeam; - return true; - }); +const Team: FC = (props) => { + const { data, team } = props; return ( } + NoResults={() => } /> ); }; diff --git a/src/utils/index.ts b/src/utils/index.ts index 51ce6fd..d4ead67 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -14,7 +14,10 @@ export const firstIfMany = (arg: T | T[]): T => { }; export const normalizeParam = (param: string) => - param.replace(/[\W_]+/g, "_").toLowerCase(); + param + .trim() + .replace(/[\W_]+/g, "_") + .toLowerCase(); export const urlFromReq = (req: IncomingMessage) => { const host = req.headers.host ?? "localhost:3000"; diff --git a/src/utils/type-helpers.ts b/src/utils/type-helpers.ts new file mode 100644 index 0000000..769c3ed --- /dev/null +++ b/src/utils/type-helpers.ts @@ -0,0 +1 @@ +export type UnWrapPromise = T extends PromiseLike ? U : T; From 81a37a295120a93eb5b9d965b90c85d0635affde Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Sat, 14 Nov 2020 13:59:53 -0500 Subject: [PATCH 03/10] Delete orphaned members --- src/pages/api/sync.ts | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/src/pages/api/sync.ts b/src/pages/api/sync.ts index 98c42e8..42909ed 100644 --- a/src/pages/api/sync.ts +++ b/src/pages/api/sync.ts @@ -1,7 +1,7 @@ import { NowRequest, NowResponse } from "@now/node"; import { google } from "googleapis"; import { MemberUpsertArgs, PrismaClient } from "@prisma/client"; -import { range, zipObject } from "lodash-es"; +import { range, zipObject, difference } from "lodash-es"; import to from "await-to-js"; import { normalizeParam } from "utils"; @@ -100,6 +100,27 @@ export default async function sync(req: NowRequest, res: NowResponse) { await prisma.subteam.upsert(body(slug, subteam)); } + // 3. Delete any orphaned members + const memberSlugsFromSheets = memberRows.map((row) => + normalizeParam(row[column.name]) + ); + const memberSlugsFromTable = await prisma.member + .findMany({ + select: { + slug: true, + }, + }) + .then((slugs) => slugs.map(({ slug }) => slug)); + const orphanedMembers = difference( + memberSlugsFromTable, + memberSlugsFromSheets + ); + if (orphanedMembers.length > 0) { + await prisma.member.deleteMany({ + where: { OR: orphanedMembers }, + }); + } + /** * An array of cells from one row of the gsheet. To access the data use * the following pattern: @@ -112,7 +133,7 @@ export default async function sync(req: NowRequest, res: NowResponse) { * */ let row; - // 3. Create or update team members + // 4. Create or update team members for (row of memberRows) { row = row.map((r) => r.trim()); const orgs = splitMultiValueCell(row[column.org]); @@ -226,7 +247,7 @@ export default async function sync(req: NowRequest, res: NowResponse) { } } - // 4. Associate reports relationship + // 5. Associate reports relationship for (let row of memberRows) { if (row[column.reports_to] === "") continue; const [err] = await to( @@ -247,7 +268,7 @@ export default async function sync(req: NowRequest, res: NowResponse) { } } - // 5. Delete any unused entities + // 6. Delete any unused entities const slug = { slug: true, members: { From a8268a06bb30f63d43b123a9dc0772f6ad6bb9a4 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Sat, 14 Nov 2020 22:26:40 -0500 Subject: [PATCH 04/10] Remove empty reports rendering, clean unused imports --- src/components/MemberDetails.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/MemberDetails.tsx b/src/components/MemberDetails.tsx index bd6fb38..5eaa981 100644 --- a/src/components/MemberDetails.tsx +++ b/src/components/MemberDetails.tsx @@ -2,8 +2,7 @@ import { Serif, Link } from "@artsy/palette"; import { formatDistanceToNow } from "date-fns"; import { capitalize } from "lodash-es"; import RouterLink from "next/link"; -import { normalizeParam } from "utils"; -import { FC, Fragment } from "react"; +import { Fragment } from "react"; import { Grid } from "components/Grid"; import { Location, Member, Organization, Subteam, Team } from "@prisma/client"; @@ -144,7 +143,7 @@ export function MemberDetails({ member }: MemberDetails) { )} {/* Show reports */} - {reports && ( + {reports?.length > 0 && ( <> Reports: From edd23333b2f45b5049c2ef41ab6bcc5ca6d480a7 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Sat, 14 Nov 2020 22:27:04 -0500 Subject: [PATCH 05/10] Ignore yarn error log --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 06bdf7f..5386188 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ vars.yml .hokusai-tmp .google-api-creds.json prisma/*.db +yarn-error.log From 86369fb7f83cb32890b354843ae3f53254410314 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Sat, 14 Nov 2020 22:29:44 -0500 Subject: [PATCH 06/10] Improve prisma connection handling --- src/data/prisma.ts | 30 ++++++++++++++++++++++++++++++ src/data/sidebar.ts | 17 +++++++---------- src/data/teamMember.ts | 11 ++--------- src/pages/location/[location].tsx | 4 +--- src/pages/member/[member].tsx | 8 +++----- src/pages/org/[org].tsx | 4 +--- src/pages/subteam/[subteam].tsx | 4 +--- src/pages/team/[team].tsx | 4 +--- 8 files changed, 46 insertions(+), 36 deletions(-) create mode 100644 src/data/prisma.ts diff --git a/src/data/prisma.ts b/src/data/prisma.ts new file mode 100644 index 0000000..aeef8b8 --- /dev/null +++ b/src/data/prisma.ts @@ -0,0 +1,30 @@ +import { PrismaClient } from "@prisma/client"; + +interface PrismaClientSingleton { + _prisma: PrismaClient | null; +} + +const prismaClientSingleton: PrismaClientSingleton = { _prisma: null }; + +export const prisma = (new Proxy(prismaClientSingleton, { + get(target, prop: keyof PrismaClient) { + if (!target._prisma) { + target._prisma = new PrismaClient(); + } + return target._prisma[prop]; + }, +}) as unknown) as PrismaClient; + +export const disconnect = async () => { + if (prismaClientSingleton._prisma) { + await prismaClientSingleton._prisma.$disconnect(); + } +}; + +process.on("disconnect", async () => { + await disconnect(); +}); + +process.on("beforeExit", async () => { + await disconnect(); +}); diff --git a/src/data/sidebar.ts b/src/data/sidebar.ts index 48c2d7e..4016e65 100644 --- a/src/data/sidebar.ts +++ b/src/data/sidebar.ts @@ -1,7 +1,6 @@ -import { PrismaClient } from "@prisma/client"; +import { prisma } from "data/prisma"; -export async function getLocationAggregate() { - const prisma = new PrismaClient(); +async function getLocationLinksData() { const results = await prisma.location.findMany({ include: { members: true, @@ -14,8 +13,7 @@ export async function getLocationAggregate() { })); } -export async function getOrgAggregate() { - const prisma = new PrismaClient(); +async function getOrgLinksData() { const results = await prisma.organization.findMany({ include: { members: true, @@ -29,8 +27,7 @@ export async function getOrgAggregate() { })); } -export async function getTeamAggregate() { - const prisma = new PrismaClient(); +async function getTeamLinksData() { const results = await prisma.team.findMany({ include: { members: true, @@ -52,8 +49,8 @@ export type SidebarData = [ export const getSidebarData = async (): Promise => { return [ - ["Locations", "location", await getLocationAggregate()], - ["Organizations", "org", await getOrgAggregate()], - ["Teams", "team", await getTeamAggregate()], + ["Locations", "location", await getLocationLinksData()], + ["Organizations", "org", await getOrgLinksData()], + ["Teams", "team", await getTeamLinksData()], ]; }; diff --git a/src/data/teamMember.ts b/src/data/teamMember.ts index e3b2639..01f92f2 100644 --- a/src/data/teamMember.ts +++ b/src/data/teamMember.ts @@ -1,17 +1,11 @@ -import { - MemberCreateInput, - MemberGetPayload, - MemberSelect, - MemberWhereInput, - PrismaClient, -} from "@prisma/client"; +import { MemberSelect, MemberWhereInput } from "@prisma/client"; import { UnWrapPromise } from "utils/type-helpers"; +import { prisma } from "data/prisma"; export type MemberIndexListing = UnWrapPromise< ReturnType >; export async function getMembersIndex(where: MemberWhereInput = {}) { - const prisma = new PrismaClient(); const data = await prisma.member.findMany({ select: { name: true, @@ -42,7 +36,6 @@ export async function getMembersIndex(where: MemberWhereInput = {}) { export async function getMemberField( field: keyof MemberSelect ): Promise { - const prisma = new PrismaClient(); const data = ( await prisma.member.findMany({ select: { diff --git a/src/pages/location/[location].tsx b/src/pages/location/[location].tsx index 04ddb29..27a3846 100644 --- a/src/pages/location/[location].tsx +++ b/src/pages/location/[location].tsx @@ -4,10 +4,9 @@ import { FC } from "react"; import { GetStaticProps, GetStaticPaths } from "next"; import { getMembersIndex, MemberIndexListing } from "data/teamMember"; import { getSidebarData } from "data/sidebar"; -import { PrismaClient } from "@prisma/client"; +import { prisma } from "data/prisma"; export const getStaticPaths: GetStaticPaths = async () => { - const prisma = new PrismaClient(); const locations = await prisma.location.findMany({ select: { slug: true, @@ -25,7 +24,6 @@ export const getStaticPaths: GetStaticPaths = async () => { export const getStaticProps: GetStaticProps = async ({ params }) => { const locationSlug = params?.location as string; - const prisma = new PrismaClient(); const members = await getMembersIndex({ locationSlug, }); diff --git a/src/pages/member/[member].tsx b/src/pages/member/[member].tsx index 0e984af..a5e2ad8 100644 --- a/src/pages/member/[member].tsx +++ b/src/pages/member/[member].tsx @@ -1,21 +1,20 @@ import { Flex, Box, Serif, ResponsiveImage, Separator } from "@artsy/palette"; import Error from "next/error"; -import { normalizeParam, useDelay } from "utils"; +import { useDelay } from "utils"; import { FC, useEffect, useState } from "react"; import { H1 } from "components/Typography"; -import { GetStaticProps, GetStaticPaths } from "next"; +import { GetStaticPaths } from "next"; import { useAreaGrid } from "components/Grid"; import { isWeekOf, relativeDaysTillAnniversary, isDayOf } from "utils/date"; import { AwardIcon } from "components/AwardIcon"; import { MemberDetails } from "components/MemberDetails"; import { useWindowSize } from "@react-hook/window-size"; import Confetti from "react-confetti"; -import { PrismaClient } from "@prisma/client"; +import { prisma } from "data/prisma"; import { getSidebarData } from "data/sidebar"; import { UnWrapPromise } from "utils/type-helpers"; export const getStaticPaths: GetStaticPaths<{ member: string }> = async () => { - const prisma = new PrismaClient(); const members = await prisma.member.findMany({ select: { slug: true, @@ -36,7 +35,6 @@ export const getStaticProps = async ({ }: { params: { member: string }; }) => { - const prisma = new PrismaClient(); const memberSlug = params.member as string; const member = await prisma.member.findFirst({ include: { diff --git a/src/pages/org/[org].tsx b/src/pages/org/[org].tsx index 25d873d..c46c6ee 100644 --- a/src/pages/org/[org].tsx +++ b/src/pages/org/[org].tsx @@ -2,12 +2,11 @@ import TeamNav from "../index"; import { NoResults } from "components/NoResults"; import { FC } from "react"; import { GetStaticProps, GetStaticPaths } from "next"; -import { PrismaClient } from "@prisma/client"; import { getMembersIndex, MemberIndexListing } from "data/teamMember"; import { getSidebarData } from "data/sidebar"; +import { prisma } from "data/prisma"; export const getStaticPaths: GetStaticPaths<{ org: string }> = async () => { - const prisma = new PrismaClient(); const orgs = await prisma.organization.findMany({ select: { slug: true, @@ -25,7 +24,6 @@ export const getStaticPaths: GetStaticPaths<{ org: string }> = async () => { export const getStaticProps: GetStaticProps = async ({ params }) => { const orgSlug = params?.org as string; - const prisma = new PrismaClient(); const members = await getMembersIndex({ orgs: { some: { diff --git a/src/pages/subteam/[subteam].tsx b/src/pages/subteam/[subteam].tsx index b32d877..5e4d6df 100644 --- a/src/pages/subteam/[subteam].tsx +++ b/src/pages/subteam/[subteam].tsx @@ -2,12 +2,11 @@ import TeamNav from "../index"; import { NoResults } from "components/NoResults"; import { FC } from "react"; import { GetStaticProps, GetStaticPaths } from "next"; -import { PrismaClient } from "@prisma/client"; import { getMembersIndex, MemberIndexListing } from "data/teamMember"; import { getSidebarData } from "data/sidebar"; +import { prisma } from "data/prisma"; export const getStaticPaths: GetStaticPaths<{ subteam: string }> = async () => { - const prisma = new PrismaClient(); const subteams = await prisma.subteam.findMany({ select: { slug: true, @@ -25,7 +24,6 @@ export const getStaticPaths: GetStaticPaths<{ subteam: string }> = async () => { export const getStaticProps: GetStaticProps = async ({ params }) => { const subteamSlug = params?.subteam as string; - const prisma = new PrismaClient(); const members = await getMembersIndex({ subteams: { some: { diff --git a/src/pages/team/[team].tsx b/src/pages/team/[team].tsx index eb3fcd1..e82f24b 100644 --- a/src/pages/team/[team].tsx +++ b/src/pages/team/[team].tsx @@ -2,12 +2,11 @@ import TeamNav from "../index"; import { NoResults } from "components/NoResults"; import { FC } from "react"; import { GetStaticProps, GetStaticPaths } from "next"; -import { PrismaClient } from "@prisma/client"; import { getMembersIndex, MemberIndexListing } from "data/teamMember"; import { getSidebarData } from "data/sidebar"; +import { prisma } from "data/prisma"; export const getStaticPaths: GetStaticPaths<{ team: string }> = async () => { - const prisma = new PrismaClient(); const teams = await prisma.team.findMany({ select: { slug: true, @@ -25,7 +24,6 @@ export const getStaticPaths: GetStaticPaths<{ team: string }> = async () => { export const getStaticProps: GetStaticProps = async ({ params }) => { const teamSlug = params?.team as string; - const prisma = new PrismaClient(); const members = await getMembersIndex({ teams: { some: { From 41b0dd0a731e63caf348e6f6ef1bcae69b203243 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Sat, 14 Nov 2020 22:31:22 -0500 Subject: [PATCH 07/10] Revamp image handling; fix rendering issues --- next.config.js | 9 +- package.json | 25 ++-- scripts/prime-cache.ts | 16 ++ scripts/tsconfig.json | 20 +++ scripts/write-credentials.js | 9 -- scripts/write-credentials.ts | 16 ++ src/components/TeamMember.tsx | 51 +++++-- src/data/image.ts | 107 +++++++++++-- src/data/locations.ts | 0 src/data/team.ts | 104 ------------- src/data/teamMember.ts | 6 + src/pages/api/image/delete.ts | 14 -- src/pages/api/image/list.ts | 11 -- src/pages/api/sync.ts | 50 +++++-- src/pages/edit-images.tsx | 54 ------- src/utils/index.ts | 21 +++ src/utils/models.ts | 97 ------------ yarn.lock | 272 ++++++++++++---------------------- 18 files changed, 362 insertions(+), 520 deletions(-) create mode 100755 scripts/prime-cache.ts create mode 100644 scripts/tsconfig.json delete mode 100644 scripts/write-credentials.js create mode 100644 scripts/write-credentials.ts delete mode 100644 src/data/locations.ts delete mode 100644 src/data/team.ts delete mode 100644 src/pages/api/image/delete.ts delete mode 100644 src/pages/api/image/list.ts delete mode 100644 src/pages/edit-images.tsx delete mode 100644 src/utils/models.ts diff --git a/next.config.js b/next.config.js index 8079581..1e6ead4 100644 --- a/next.config.js +++ b/next.config.js @@ -3,4 +3,11 @@ const withBundleAnalyzer = require("@next/bundle-analyzer")({ enabled: process.env.ANALYZE === "true", }); -module.exports = withBundleAnalyzer(withTM({})); +module.exports = withBundleAnalyzer( + withTM({ + reactStrictMode: true, + images: { + domains: [`${process.env.IMAGE_BUCKET}.s3.amazonaws.com`], + }, + }) +); diff --git a/package.json b/package.json index 5dff05c..ac1287e 100644 --- a/package.json +++ b/package.json @@ -3,11 +3,12 @@ "private": true, "description": "", "scripts": { - "write-creds": "node ./scripts/write-credentials", + "write-creds": "ts-node --dir ./scripts write-credentials.ts", + "prime-cache": "ts-node --dir ./scripts prime-cache.ts", "start": "next start", - "predev": "dotenv yarn write-creds", + "predev": "yarn write-creds", "dev": "next dev", - "prebuild": "dotenv yarn write-creds", + "prebuild": "yarn write-creds && yarn prime-cache", "build": "next build", "type-check": "tsc", "logs:staging": "hokusai staging logs | grep '^{' | npx miami-vice", @@ -20,22 +21,22 @@ "@react-hook/window-size": "^3.0.7", "await-to-js": "^2.1.1", "aws-sdk": "^2.668.0", - "csvtojson": "^2.0.10", "date-fns": "^2.12.0", "googleapis": "^59.0.0", "isomorphic-unfetch": "^3.0.0", - "isotopes": "^0.6.0", "jwt-decode": "^2.2.0", "lodash-es": "^4.17.15", - "next": "^10.0.0", + "module-alias": "^2.2.2", + "next": "^10.0.1", "node-cookie": "^2.1.2", + "p-memoize": "^4.0.1", "pino": "^6.5.1", "pino-http": "^5.2.0", "react": "^17.0.1", "react-bytesize-icons": "^0.11.1", "react-confetti": "^6.0.0", "react-dom": "^17.0.1", - "sharp": "^0.25.2", + "sharp": "^0.26.2", "styled-components": "4" }, "devDependencies": { @@ -43,7 +44,6 @@ "@now/node": "^1.5.1", "@prisma/cli": "2.11.0", "@types/cookie": "^0.3.3", - "@types/csvtojson": "^1.1.5", "@types/jwt-decode": "^2.2.1", "@types/lodash-es": "^4.17.3", "@types/mdx-js__react": "^1.5.1", @@ -54,9 +54,16 @@ "@types/sharp": "^0.25.0", "@types/styled-components": "^4", "@types/styled-system": "^5.1.10", - "dotenv-cli": "^4.0.0", + "dotenv": "^8.2.0", "next-transpile-modules": "^4.1.0", + "ts-node": "^9.0.0", "typescript": "^4.0.5", "typescript-styled-plugin": "^0.15.0" + }, + "_moduleAliases": { + "lodash-es": "node_modules/lodash", + "utils": "src/utils", + "components": "src/components", + "data": "src/data" } } diff --git a/scripts/prime-cache.ts b/scripts/prime-cache.ts new file mode 100755 index 0000000..b42516a --- /dev/null +++ b/scripts/prime-cache.ts @@ -0,0 +1,16 @@ +#!/usr/bin/env ts-node-script +import "module-alias/register"; +import { sync } from "../src/pages/api/sync"; +import dotenv from "dotenv"; +import { join } from "path"; +import { disconnect } from "data/prisma"; + +dotenv.config({ + path: join(__dirname, "..", ".env"), +}); + +(async () => { + const result = await sync(); + console.log(result); + await disconnect(); +})(); diff --git a/scripts/tsconfig.json b/scripts/tsconfig.json new file mode 100644 index 0000000..df48b5d --- /dev/null +++ b/scripts/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es6", + "lib": ["dom", "dom.iterable", "esnext"], + "module": "commonjs", + "baseUrl": "../src", + "moduleResolution": "node", + "skipLibCheck": true, + "strict": false, + "paths": { + "../src/*": ["*"], + "lodash-es": ["lodash"] + }, + "allowJs": true, + "resolveJsonModule": true, + "esModuleInterop": true, + }, + "include": ["./*.ts", "../src/**/*.ts", "../src/**/*.tsx"], + "exclude": ["../node_modules"], +} \ No newline at end of file diff --git a/scripts/write-credentials.js b/scripts/write-credentials.js deleted file mode 100644 index 52a5f06..0000000 --- a/scripts/write-credentials.js +++ /dev/null @@ -1,9 +0,0 @@ -/** - * This file writes out the google credentials required to access the google drive api to read our team images. - */ -const fs = require("fs"); - -fs.writeFileSync( - ".google-api-creds.json", - Buffer.from(process.env.G_CREDS, "base64").toString("ascii") -); diff --git a/scripts/write-credentials.ts b/scripts/write-credentials.ts new file mode 100644 index 0000000..7cfe1d5 --- /dev/null +++ b/scripts/write-credentials.ts @@ -0,0 +1,16 @@ +/** + * This file writes out the google credentials required to access the google drive api to read our team images. + */ +import { writeFileSync } from "fs"; + +import dotenv from "dotenv"; +import { join } from "path"; + +dotenv.config({ + path: join(__dirname, "..", ".env"), +}); + +writeFileSync( + ".google-api-creds.json", + Buffer.from(process.env.G_CREDS as string, "base64").toString("ascii") +); diff --git a/src/components/TeamMember.tsx b/src/components/TeamMember.tsx index 264048a..cea30ed 100644 --- a/src/components/TeamMember.tsx +++ b/src/components/TeamMember.tsx @@ -1,13 +1,13 @@ -import { Member } from "../pages"; -import { FC } from "react"; +import { FC, useState } from "react"; import { normalizeParam } from "../utils"; import { AvatarFallback } from "./AvatarFallback"; import RouterLink from "next/link"; -import { Avatar, Box, Serif, Flex, Link } from "@artsy/palette"; -import styled from "styled-components"; -import { color } from "styled-system"; +import { Box, Serif, Flex, Link, color } from "@artsy/palette"; +import styled, { keyframes } from "styled-components"; import { AwardIcon } from "./AwardIcon"; import { isWeekOf } from "utils/date"; +import Image from "next/image"; +import { Member, Location } from "@prisma/client"; const TeamMemberContainer = styled(Flex)` border-radius: 5px; @@ -18,20 +18,43 @@ const TeamMemberContainer = styled(Flex)` } `; +const pulse = keyframes` + 0% { background-color: ${color("black10")}; } + 50% { background-color: ${color("black5")}; } + 100% { background-color: ${color("black10")}; } +`; + const AvatarContainer = styled(Box)` flex-shrink: 0; position: relative; + width: 100px; + height: 100px; + background-color: ${color("black10")}; + animation: ${pulse} 2s ease-in-out infinite; + border-radius: 50%; +`; + +const Avatar = styled(Image)` + border-radius: 50%; + opacity: ${(props) => (props as any).opacity}; + transition: opacity 0.3s; `; -const location = ({ city, floor }: { city?: string; floor?: string }) => - [city, floor && `Fl. ${floor}`].filter((v) => v).join(", "); +const location = (member: TeamMemberProps["member"]) => + [ + member.location?.city, + member.location?.floor && `Fl. ${member.location.floor}`, + ] + .filter((v) => v) + .join(", "); export interface TeamMemberProps { - member: Member; + member: Member & { location: Location }; showAvatar?: boolean; } export const TeamMember: FC = (props) => { const { member, showAvatar = true } = props; + const [opacity, setOpacity] = useState(0); return ( = (props) => { )} {member.headshot ? ( ( - - )} + sizes="100px" + onLoad={() => setOpacity(1)} + opacity={opacity} /> ) : ( diff --git a/src/data/image.ts b/src/data/image.ts index c2c75f4..fe7fcfc 100644 --- a/src/data/image.ts +++ b/src/data/image.ts @@ -2,6 +2,14 @@ import sharp from "sharp"; import S3 from "aws-sdk/clients/s3"; import stream from "stream"; import { google } from "googleapis"; +import memoize from "p-memoize"; +import { extractFirstPartialMatch, hash } from "utils"; +import to from "await-to-js"; +import { log } from "utils/logger"; + +const SECOND = 1_000; +const MINUTE = 60 * SECOND; +const HOUR = 60 * MINUTE; const drive = google.drive({ version: "v3", @@ -19,8 +27,31 @@ const drive = google.drive({ }), }); +const s3 = new S3({ + accessKeyId: process.env.ACCESS_KEY_ID, + secretAccessKey: process.env.SECRET_ACCESS_KEY, + region: process.env.REGION, +}); + +/** + * Returns a list of all images stored in the currently configured + * S3 bucket. This method is cached for an hour and can be cleared by + * calling `memoize.clear(listS3Images)`. + */ +export const listS3Images = memoize( + (bucketPrefix: string) => { + return s3 + .listObjectsV2({ + Bucket: process.env.IMAGE_BUCKET as string, + Delimiter: "/", + Prefix: bucketPrefix, + }) + .promise(); + }, + { maxAge: 1 * HOUR } +); + const streamToS3 = ( - s3: S3, key: string, done: (err: Error, data: S3.ManagedUpload.SendData) => void ) => { @@ -35,29 +66,70 @@ const streamToS3 = ( return pass; }; -export async function resizeImage( - imageUrl: URL, - size: number +export async function uploadUserImage( + userSlug: string, + imageUrl: URL ): Promise { - const s3 = new S3({ - accessKeyId: process.env.ACCESS_KEY_ID, - secretAccessKey: process.env.SECRET_ACCESS_KEY, - region: process.env.REGION, - }); + const bucketPrefix = "team-full/"; + const Bucket = process.env.IMAGE_BUCKET as string; if (!imageUrl.href.startsWith("https://drive.google.com")) { throw new Error(`Error processing ${imageUrl.href}, not a drive url`); } const imageId = imageUrl.href.split("/file/d/")[1]?.split("/")[0]; - if (!imageId) { throw new Error( `Invalid formatted google drive image url: ${imageUrl.href}` ); } - const resizer = sharp().rotate().resize(size, size); + /** + * Used to determine the uniqueness of images. Any images w/ the same + * hash for the same user won't be overwrote. If changes to the image + * processing are done you can update the salt value to regenerate all + * the hashes. + */ + const imageUrlHash = hash(imageUrl.toString() + "salt2"); + const userImageName = `${bucketPrefix}${userSlug}-${imageUrlHash}`; + + const [ErrorAccessingS3Images, S3Images] = await to( + listS3Images(bucketPrefix) + ); + if (ErrorAccessingS3Images || !S3Images) { + log.error("Unable to access images on S3", ErrorAccessingS3Images); + } else { + const userImages: string[] = + S3Images.Contents?.map((image) => image.Key ?? "").filter((image) => + image.includes(`/${userSlug}-`) + ) ?? []; + const [currentUserImage, oldUserImages] = extractFirstPartialMatch( + userImageName, + userImages + ); + // Delete any old images + for (let oldImage of oldUserImages) { + const [deleteError] = await to( + s3 + .deleteObject({ + Bucket, + Key: oldImage, + }) + .promise() + ); + if (deleteError) { + log.error(`Failed to delete old user image ${oldImage}`, deleteError); + } + } + if (currentUserImage) { + // This image has already been uploaded, let's skip the rest + return `https://${Bucket}.s3.amazonaws.com/${currentUserImage}`; + } + } + + log.info(`Uploading image for ${userSlug}`); + + const normalizer = sharp().rotate().resize(500, 500); const file = await drive.files.get( { @@ -74,9 +146,16 @@ export async function resizeImage( if (!extension) throw new Error(`No valid extension for ${imageUrl.href}`); return new Promise((resolve, reject) => { - file.data.pipe(resizer).pipe( - streamToS3(s3, `team/${imageId}-${size}.${extension}`, (err, data) => { - err ? reject(err) : resolve(data.Location); + file.data.pipe(normalizer).pipe( + streamToS3(`${userImageName}.${extension}`, (err, data) => { + if (err) { + reject(err); + } else { + log.info(`successfully uploaded ${userSlug} image`, { + url: data.Location, + }); + resolve(data.Location); + } }) ); }); diff --git a/src/data/locations.ts b/src/data/locations.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/data/team.ts b/src/data/team.ts deleted file mode 100644 index 9f5d31d..0000000 --- a/src/data/team.ts +++ /dev/null @@ -1,104 +0,0 @@ -import csv from "csvtojson"; -import pLimit from "p-limit"; -import { imageCache } from "utils/models"; -import { hash } from "utils"; -import { capitalize, isEmpty, omit, union, memoize, uniqBy } from "lodash-es"; -import { Member } from "../pages/index"; -import { resizeImage } from "./image"; -import { log } from "utils/logger"; - -const limit = pLimit(10); - -const getResizedImageUrl = async ( - imageUrl: string, - size: number -): Promise => { - const cacheKey = hash(imageUrl + (size ? `&size=${size}` : "")); - const cachedImage = await imageCache.get(cacheKey); - if (cachedImage) { - return cachedImage; - } - return limit(() => resizeImage(new URL(imageUrl), size)).then( - async function afterResizingImage(resizedImageUrl) { - if (!resizedImageUrl) { - log.error(`No image URL provided`); - return; - } - log.info(`resized ${imageUrl} to ${size}`); - log.info(resizedImageUrl); - await imageCache.set(cacheKey, resizedImageUrl); - return resizedImageUrl; - } - ); -}; - -export const getMembers = memoize(async () => { - const { SHEETS_URL } = process.env; - - if (typeof SHEETS_URL !== "string") { - throw new Error("Expected SHEETS_URL env var to be defined"); - } - - const parsed: Member[] = await fetch(SHEETS_URL) - .then((res) => res.text()) - .then((csvContent) => csv().fromString(csvContent)); - - const promisedMembers = parsed - .sort((a, b) => (a.name > b.name ? 1 : a.name < b.name ? -1 : 0)) // sort alphabetically - .map(async (member) => { - member.orgs ??= member.org?.split(";") ?? []; - member.orgs = member.orgs - .map((org) => org.trim()) - .filter((org) => org.length > 0); - member.teams ??= member.team?.split(";") ?? []; - member.teams = member.teams - .map((team) => team.trim()) - .filter((team) => team.length > 0); - member.subteams ??= member.subteam?.split(";") ?? []; - member.subteams = member.subteams - .map((subteam) => subteam.trim()) - .filter((subteam) => subteam.length > 0); - if (member.preferred_pronouns) { - member.preferred_pronouns = member.preferred_pronouns - .split("/") - .map((p: string) => capitalize(p)) - .join("/"); - } - if (member.headshot) { - try { - member.profileImage = await getResizedImageUrl(member.headshot, 500); - member.avatar = await getResizedImageUrl(member.headshot, 200); - } catch (error) { - log.error(error); - member.profileImage = ""; - member.avatar = ""; - } - } - if (member.reports_to) { - const manager = parsed.find((m) => m.name === member.reports_to); - if (!isEmpty(manager)) { - member.manager = omit(manager, "manager"); - } - } - const reports = uniqBy( - parsed - .filter((m) => m.reports_to === member.name) - .map((report) => omit(report, ["manager", "reports"])), - "name" - ); - - if (reports.length > 0) { - member.reports = reports; - } - return member; - }); - return await Promise.all(promisedMembers); -}); - -export const getMemberProperty = memoize(async (property: keyof Member) => { - return union( - (await getMembers()) - .map((member) => member[property]) - .filter((prop) => !isEmpty(prop)) - ) as string[]; -}); diff --git a/src/data/teamMember.ts b/src/data/teamMember.ts index 01f92f2..07569dc 100644 --- a/src/data/teamMember.ts +++ b/src/data/teamMember.ts @@ -12,6 +12,12 @@ export async function getMembersIndex(where: MemberWhereInput = {}) { startDate: true, title: true, headshot: true, + location: { + select: { + city: true, + floor: true, + }, + }, orgs: { select: { name: true, diff --git a/src/pages/api/image/delete.ts b/src/pages/api/image/delete.ts deleted file mode 100644 index 549dfb1..0000000 --- a/src/pages/api/image/delete.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { imageCache } from "utils/models"; -import { firstIfMany } from "utils"; -import to from "await-to-js"; -import { NowResponse, NowRequest } from "@now/node"; - -export default async (req: NowRequest, res: NowResponse) => { - const { id } = req.query; - const [error] = await to(imageCache.delete(firstIfMany(id))); - if (error) { - res.status(500).send(error); - } else { - res.status(200).send(`Image ${id} deleted`); - } -}; diff --git a/src/pages/api/image/list.ts b/src/pages/api/image/list.ts deleted file mode 100644 index bfa3cbd..0000000 --- a/src/pages/api/image/list.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { imageCache } from "utils/models"; -import { NowResponse } from "@now/node"; - -export default async (_: never, res: NowResponse) => { - const results = await imageCache.list(); - if (results) { - res.status(200).send(JSON.stringify(results)); - } else { - res.status(404).send("No results"); - } -}; diff --git a/src/pages/api/sync.ts b/src/pages/api/sync.ts index 42909ed..339e32d 100644 --- a/src/pages/api/sync.ts +++ b/src/pages/api/sync.ts @@ -1,11 +1,12 @@ import { NowRequest, NowResponse } from "@now/node"; import { google } from "googleapis"; -import { MemberUpsertArgs, PrismaClient } from "@prisma/client"; +import { MemberUpsertArgs } from "@prisma/client"; import { range, zipObject, difference } from "lodash-es"; import to from "await-to-js"; import { normalizeParam } from "utils"; - -const prisma = new PrismaClient(); +import { uploadUserImage } from "data/image"; +import { log } from "utils/logger"; +import { prisma } from "data/prisma"; const sheets = google.sheets({ version: "v4", @@ -29,7 +30,7 @@ const splitMultiValueCell = (cellValue: string): string[] | undefined => { return values.length > 0 ? values : undefined; }; -export default async function sync(req: NowRequest, res: NowResponse) { +export async function sync() { // 1. grab sheet const { data } = await sheets.spreadsheets.values.get({ spreadsheetId: process.env.SHEET_ID as string, @@ -37,8 +38,7 @@ export default async function sync(req: NowRequest, res: NowResponse) { }); if (!data.values) { - res.status(500).send("Sheet values empty").end(); - return; + throw new Error("Sheet values empty"); } const [keys, ...memberRowsWithWhitespace] = data.values; @@ -139,15 +139,30 @@ export default async function sync(req: NowRequest, res: NowResponse) { const orgs = splitMultiValueCell(row[column.org]); const teams = splitMultiValueCell(row[column.team]); const subteams = splitMultiValueCell(row[column.subteam]); + const memberSlug = normalizeParam(row[column.name]); + let headshot = row[column.headshot]; + if (headshot) { + const [imageUploadError, headshotURL] = await to( + uploadUserImage(memberSlug, new URL(headshot)) + ); + if (imageUploadError || !headshotURL) { + log.error(`Failed to upload image for ${memberSlug}`, { + imageUploadError, + }); + headshot = ""; + } else { + headshot = headshotURL; + } + } const [err] = await to( prisma.member.upsert({ create: { name: row[column.name], - slug: normalizeParam(row[column.name]), + slug: memberSlug, title: row[column.title], email: row[column.email], - headshot: row[column.headshot], + headshot, preferredPronouns: row[column.preferred_pronouns], introEmail: row[column.intro_email], roleText: row[column.role_text], @@ -189,10 +204,9 @@ export default async function sync(req: NowRequest, res: NowResponse) { }, update: { name: row[column.name], - slug: normalizeParam(row[column.name]), title: row[column.title], email: row[column.email], - headshot: row[column.headshot], + headshot, preferredPronouns: row[column.preferred_pronouns], introEmail: row[column.intro_email], roleText: row[column.role_text], @@ -291,7 +305,7 @@ export default async function sync(req: NowRequest, res: NowResponse) { const deletedLocations = await prisma.location.deleteMany({ where: { OR: unusedLocations }, }); - console.log("locations deleted:", deletedLocations); + log.info("locations deleted", { deletedLocations }); } const unusedOrgs = unused( @@ -301,7 +315,7 @@ export default async function sync(req: NowRequest, res: NowResponse) { const deletedOrgs = await prisma.organization.deleteMany({ where: { OR: unusedOrgs }, }); - console.log("orgs deleted:", deletedOrgs); + log.info("orgs deleted", { deletedOrgs }); } const unusedTeams = unused(await prisma.team.findMany({ select: slug })); @@ -309,7 +323,7 @@ export default async function sync(req: NowRequest, res: NowResponse) { const deletedTeams = await prisma.team.deleteMany({ where: { OR: unusedTeams }, }); - console.log("teams deleted:", deletedTeams); + log.info("teams deleted", { deletedTeams }); } const unusedSubteams = unused( @@ -319,8 +333,14 @@ export default async function sync(req: NowRequest, res: NowResponse) { const deletedSubteams = await prisma.subteam.deleteMany({ where: { OR: unusedSubteams }, }); - console.log("subteams deleted:", deletedSubteams); + log.info("subteams deleted", { deletedSubteams }); } - res.status(200).end(); + return "successful"; +} + +export default async function (req: NowRequest, res: NowResponse) { + const [err, result] = await to(sync()); + err ? res.status(500).send(err) : res.status(200).send(result); + res.end(); } diff --git a/src/pages/edit-images.tsx b/src/pages/edit-images.tsx deleted file mode 100644 index 2b4d419..0000000 --- a/src/pages/edit-images.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { GetServerSideProps } from "next"; -import { urlFromReq } from "utils"; -import { Flex, Box, Avatar } from "@artsy/palette"; -import { AvatarFallback } from "components/AvatarFallback"; -import { useState } from "react"; -import { ImageCacheModel } from "utils/models"; - -export const getServerSideProps: GetServerSideProps = async (ctx) => { - const res = await fetch(`${urlFromReq(ctx.req)}/api/image/list`); - if (!res.ok) { - return { props: { errorCode: res.status, errorMessage: res.statusText } }; - } - return { props: { cache: await res.json() } }; -}; - -export const EditImages = ({ cache }: { cache: ImageCacheModel[] }) => { - const [deleted, setDeleted] = useState([]); - return ( - - {cache - .filter(({ id }) => !deleted.includes(id)) - .map(({ id, imageUrl }: any) => ( - { - const res = await fetch( - `http://localhost:3000/api/image/delete?id=${id}` - ); - if (res.status === 200) { - setDeleted(deleted.concat(id)); - } - }} - > - ( - - )} - /> - - ))} - - ); -}; - -export default EditImages; diff --git a/src/utils/index.ts b/src/utils/index.ts index d4ead67..1fdd3e1 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -48,3 +48,24 @@ export const useDelay = (delay: number) => { return finished; }; + +/** + * Pulls the sought after element out of the array (if it exists) + * and returns a deduplicate set of remaining items. + * @param toExtract The item to extract from the array + * @param sourceArray The array in which to do the extraction from + */ +export const extractFirstPartialMatch = ( + toExtract: string, + sourceArray: string[] +): [match: string | null, remainder: string[]] => { + const remainingArray = Array.from(new Set(sourceArray)); + const extractionIndex = remainingArray.findIndex((t) => + t.includes(toExtract) + ); + if (extractionIndex === -1) { + return [null, remainingArray]; + } + const [match] = remainingArray.splice(extractionIndex, 1); + return [match, remainingArray]; +}; diff --git a/src/utils/models.ts b/src/utils/models.ts deleted file mode 100644 index a0fc6fd..0000000 --- a/src/utils/models.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { Isotope, IsotopeSelect, IsotopeResult } from "isotopes"; - -const createModel = (name: string) => - new Isotope( - { - domain: `artsy-studio-${name}`, - key: "id", - }, - { - accessKeyId: process.env.ACCESS_KEY_ID, - secretAccessKey: process.env.SECRET_ACCESS_KEY, - region: process.env.REGION, - } - ); - -export interface Model { - id: string; -} - -export interface ImageCacheModel extends Model { - type: "image"; - imageUrl: string; -} - -class Metadata { - private model: Isotope< - ImageCacheModel, - ImageCacheModel, - ImageCacheModel - > = createModel("metadata"); - private init() { - if (!this.model) { - this.model = createModel("metadata"); - return this.model.create(); - } - return this.model; - } - async erase() { - await this.init(); - await this.model.destroy(); - // Free the model reference - this.model = null as any; - await this.init(); - } - async get(id?: string) { - await this.init(); - return this.model.get(id as string); - } - async set(data: ImageCacheModel) { - await this.init(); - return this.model.put(data); - } - async delete(id: string) { - await this.init(); - return this.model.delete(id); - } - async select( - callback: ( - query: IsotopeSelect - ) => IsotopeSelect, - cursor?: string - ): Promise> { - await this.init(); - return this.model.select(callback(this.model.getQueryBuilder()), cursor); - } -} - -export const metadata = new Metadata(); - -export const imageCache = { - async list() { - let allItems: ImageCacheModel[] = []; - let cursor: string | undefined; - do { - const { items, next } = await metadata.select( - (query) => query.where("`type` LIKE ?", "%image%"), - cursor - ); - allItems = allItems.concat(items); - cursor = next; - } while (cursor); - return allItems; - }, - clear() { - return this.list(); - }, - async get(id: string): Promise { - return (await metadata.get(`image-${id}`))?.imageUrl; - }, - set(id: string, imageUrl: string) { - return metadata.set({ id: `image-${id}`, type: "image", imageUrl }); - }, - delete(id: string) { - console.log(`Deleting... ${id}`); - return metadata.delete(id.startsWith("image-") ? id : `image-${id}`); - }, -}; diff --git a/yarn.lock b/yarn.lock index 17e38c3..7a5386e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1215,26 +1215,26 @@ integrity sha512-i9YbZPN3QgfighY/1X1Pu118VUz2Fmmhd6b2n0/O8YVgGGfw0FbUYoA97k7FkpGJ+pLCFEDLUmAPPV4D1kpeFw== "@next/bundle-analyzer@^10.0.0": - version "10.0.0" - resolved "https://registry.yarnpkg.com/@next/bundle-analyzer/-/bundle-analyzer-10.0.0.tgz#1413310416ddffce4519ea914975c2c05624c285" - integrity sha512-FXww8pzupSshne1EGvfCfLff5MWJz9eXN1DNqqLRCtv/eytfIEH4xjUsqxWrzph+m0DQxdBf3Lv7/zV2Ra/b1A== + version "10.0.1" + resolved "https://registry.yarnpkg.com/@next/bundle-analyzer/-/bundle-analyzer-10.0.1.tgz#4bcb376c1163b9e6e2866e1112bc848194648dce" + integrity sha512-1UMWnaeZSm+HQTqkje5ToqzCO3kXh7tqeJQBRjWz4+bbTdZEWPO8JPB/JFNpckU3FzYcnG6oeeYmUzHufZp3sg== dependencies: webpack-bundle-analyzer "3.6.1" -"@next/env@10.0.0": - version "10.0.0" - resolved "https://registry.yarnpkg.com/@next/env/-/env-10.0.0.tgz#eb0239062226a9c8b604d58d4a4204e26c22eb16" - integrity sha512-59+6BnOxPoMY64Qy2crDGHtvQgHwIL1SIkWeNiEud1V6ASs59SM9oDGN+Bo/EswII1nn+wQRpMvax0IIN2j+VQ== +"@next/env@10.0.1": + version "10.0.1" + resolved "https://registry.yarnpkg.com/@next/env/-/env-10.0.1.tgz#5f49329fcc4fe8948737aeb8108c9d7d75155f93" + integrity sha512-6dwx5YXKG88IR9Q1aai+pprF7WKcmtl0ShQy/iENj5yMWMzsQCem6hxe198u9j7z1IsWyGDXZPsaLEJEatOpeQ== -"@next/polyfill-module@10.0.0": - version "10.0.0" - resolved "https://registry.yarnpkg.com/@next/polyfill-module/-/polyfill-module-10.0.0.tgz#17f59cb7325a03f23b66b979fccc56d133411b0a" - integrity sha512-FLSwwWQaP/sXjlS7w4YFu+oottbo/bjoh+L+YED7dblsaRJT89ifV+h8zvLvh1hCL7FJUYVar4rehvj/VO5T9w== +"@next/polyfill-module@10.0.1": + version "10.0.1" + resolved "https://registry.yarnpkg.com/@next/polyfill-module/-/polyfill-module-10.0.1.tgz#483c8f8692d842800f6badb59f8de74b540580a9" + integrity sha512-Vf8HYy74jx8aQgv/ftFXQtD/udJY4/OXYiiBepqrxC0T3PPl4cns1cbrr5f15xjPELMfcqulxwMYEurioBmv+w== -"@next/react-dev-overlay@10.0.0": - version "10.0.0" - resolved "https://registry.yarnpkg.com/@next/react-dev-overlay/-/react-dev-overlay-10.0.0.tgz#ba1acc79bc9d874f1801a0b312e6a45de74bf425" - integrity sha512-HJ44TJXtaGfGxVtljPECZvqw+GctVvBr60Rsedo5A+wU2GIiycJ8n5yUSdc9UiYTnPuxfJFicJec6kgR6GSWKA== +"@next/react-dev-overlay@10.0.1": + version "10.0.1" + resolved "https://registry.yarnpkg.com/@next/react-dev-overlay/-/react-dev-overlay-10.0.1.tgz#38f99f78316e9e7fa61a95e1d883e90d6d1fa4d0" + integrity sha512-VYwwGBtV9hqpqYoVABvZEFHBt3oL6PMpiLrDmnHaOwsPDQ+kQsiWd1L8tsYvAC2dgu/x/a/TG0D81FGF77Tohw== dependencies: "@babel/code-frame" "7.10.4" ally.js "1.4.1" @@ -1247,15 +1247,15 @@ stacktrace-parser "0.1.10" strip-ansi "6.0.0" -"@next/react-refresh-utils@10.0.0": - version "10.0.0" - resolved "https://registry.yarnpkg.com/@next/react-refresh-utils/-/react-refresh-utils-10.0.0.tgz#45cdd1ad3b55ac442f8431cdc43ff53c3dc44d16" - integrity sha512-V1/oiDWb2C1Do0eZONsKX1aqGNkqCUqxUahIiCjwKFu9c3bps+Ygg4JjtaCd9oycv0KzYImUZnU+nqveFUjxUw== +"@next/react-refresh-utils@10.0.1": + version "10.0.1" + resolved "https://registry.yarnpkg.com/@next/react-refresh-utils/-/react-refresh-utils-10.0.1.tgz#ea6f808a9a6242d2da85138edee5562f17c0243a" + integrity sha512-K3thGrgD0uic/x4PZh9oRK/+LWTsn6zmDSHoEXgdft1gtlOjQIVGz7yuPMvuEB9oXDl+giuvRbU+JRlhtSf/eQ== "@now/node@^1.5.1": - version "1.8.4" - resolved "https://registry.yarnpkg.com/@now/node/-/node-1.8.4.tgz#05d05ef9e37f2ed07402af5d541df547cefbf4a4" - integrity sha512-OVsE/PFl10hTOAounUgX23ri2IvCLpL7JfmIrGfiInWCyC4eQqaFGTbsP+HiSiCfhPBnFiEzfstM1/pvMnSbaA== + version "1.8.5" + resolved "https://registry.yarnpkg.com/@now/node/-/node-1.8.5.tgz#471196dbe8221c9f20692e13cf0f4272c6cf3def" + integrity sha512-dZ/ZzOLug0ap/jiLVnmwS93PpbNjGR/SkDQjQ+BFRTTi2vUZ768CGUpmgoZy81uxLjZL9xlMIhmTVHwjfxu7WA== dependencies: "@types/node" "*" ts-node "8.9.1" @@ -1441,13 +1441,6 @@ resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.3.3.tgz#85bc74ba782fb7aa3a514d11767832b0e3bc6803" integrity sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow== -"@types/csvtojson@^1.1.5": - version "1.1.5" - resolved "https://registry.yarnpkg.com/@types/csvtojson/-/csvtojson-1.1.5.tgz#b1500849bd0a1ef58d93b03a61d889e2d40a912b" - integrity sha512-REZcfXN1h2JLoSc066GAdUKP2OpNiG3mIpgyjZWB5RKhBOZc5xmAZwavbP6O7vKMZ+jYTqbwUNucMBl7dw68JA== - dependencies: - "@types/node" "*" - "@types/hoist-non-react-statics@*": version "3.3.1" resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" @@ -1494,9 +1487,9 @@ form-data "^3.0.0" "@types/node@*": - version "14.11.10" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.11.10.tgz#8c102aba13bf5253f35146affbf8b26275069bef" - integrity sha512-yV1nWZPlMFpoXyoknm4S56y2nlTAuFYaJuQtYRAOU7xA/FJ9RY0Xm7QOkaYMMmr8ESdHIuUb6oQgR/0+2NqlyA== + version "14.14.7" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.7.tgz#8ea1e8f8eae2430cf440564b98c6dfce1ec5945d" + integrity sha512-Zw1vhUSQZYw+7u5dAwNbIA9TuTotpzY/OF7sJM9FqPOF3SPjKnxrjoTktXDZgUjybf4cWVBP7O8wvKdSaGHweg== "@types/pino-http@^5.0.4": version "5.0.5" @@ -1549,9 +1542,9 @@ csstype "^3.0.2" "@types/react@^16.9.55": - version "16.9.55" - resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.55.tgz#47078587f5bfe028a23b6b46c7b94ac0d436acff" - integrity sha512-6KLe6lkILeRwyyy7yG9rULKJ0sXplUsl98MGoCfpteXf9sPWFWWMknDcsvubcpaTdBuxtsLF6HDUwdApZL/xIg== + version "16.9.56" + resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.56.tgz#ea25847b53c5bec064933095fc366b1462e2adf0" + integrity sha512-gIkl4J44G/qxbuC6r2Xh+D3CGZpJ+NdWTItAPmZbR5mUS+JQ8Zvzpl0ea5qT/ZT3ZNTUcDKUVqV3xBE8wv/DyQ== dependencies: "@types/prop-types" "*" csstype "^3.0.2" @@ -2148,7 +2141,7 @@ bl@^4.0.1: inherits "^2.0.4" readable-stream "^3.4.0" -bluebird@^3.5.1, bluebird@^3.5.5: +bluebird@^3.5.5: version "3.7.2" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== @@ -2832,15 +2825,6 @@ cross-fetch@3.0.6: dependencies: node-fetch "2.6.1" -cross-spawn@^7.0.1: - version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" - integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== - dependencies: - path-key "^3.1.0" - shebang-command "^2.0.0" - which "^2.0.1" - crypto-browserify@3.12.0, crypto-browserify@^3.11.0: version "3.12.0" resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" @@ -2940,18 +2924,9 @@ csstype@^2.2.0: integrity sha512-ul26pfSQTZW8dcOnD2iiJssfXw0gdNVX9IJDH/X3K5DGPfj+fUYe3kB+swUY6BF3oZDxaID3AJt+9/ojSAE05A== csstype@^3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.3.tgz#2b410bbeba38ba9633353aff34b05d9755d065f8" - integrity sha512-jPl+wbWPOWJ7SXsWyqGRk3lGecbar0Cb0OvZF/r/ZU011R4YqiRehgkQ9p4eQfo9DSDLqLL3wHwfxeJiuIsNag== - -csvtojson@^2.0.10: - version "2.0.10" - resolved "https://registry.yarnpkg.com/csvtojson/-/csvtojson-2.0.10.tgz#11e7242cc630da54efce7958a45f443210357574" - integrity sha512-lUWFxGKyhraKCW8Qghz6Z0f2l/PqB1W3AO0HKJzGIQ5JRSlR651ekJDiGJbBT4sRNNv5ddnSGVEnsxP9XRCVpQ== - dependencies: - bluebird "^3.5.1" - lodash "^4.17.3" - strip-bom "^2.0.0" + version "3.0.5" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.5.tgz#7fdec6a28a67ae18647c51668a9ff95bb2fa7bb8" + integrity sha512-uVDi8LpBUKQj6sdxNaTetL6FpeCqTjOvAQuQUa/qAqq8oOd4ivkbhgnqayl0dnPal8Tb/yB1tF+gOvCBiicaiQ== cyclist@^1.0.1: version "1.0.1" @@ -3212,22 +3187,7 @@ domutils@^2.0.0: domelementtype "^2.0.1" domhandler "^3.3.0" -dotenv-cli@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/dotenv-cli/-/dotenv-cli-4.0.0.tgz#3cdd68b87ccd63c78dbfa72aab2f639bbeba5f4b" - integrity sha512-ByKEec+ashePEXthZaA1fif9XDtcaRnkN7eGdBDx3HHRjwZ/rA1go83Cbs4yRrx3JshsCf96FjAyIA2M672+CQ== - dependencies: - cross-spawn "^7.0.1" - dotenv "^8.1.0" - dotenv-expand "^5.1.0" - minimist "^1.1.3" - -dotenv-expand@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-5.1.0.tgz#3fbaf020bfd794884072ea26b1e9791d45a629f0" - integrity sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA== - -dotenv@^8.1.0: +dotenv@^8.2.0: version "8.2.0" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a" integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw== @@ -4378,11 +4338,6 @@ is-symbol@^1.0.2: dependencies: has-symbols "^1.0.1" -is-utf8@^0.2.0: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" - integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI= - is-what@^3.3.1: version "3.8.0" resolved "https://registry.yarnpkg.com/is-what/-/is-what-3.8.0.tgz#610bc46a524355f2424eb85eedc6ebbbf7e1ff8c" @@ -4403,11 +4358,6 @@ isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= - isobject@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" @@ -4436,14 +4386,6 @@ isomorphic-unfetch@^3.0.0: node-fetch "^2.6.1" unfetch "^4.2.0" -isotopes@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/isotopes/-/isotopes-0.6.0.tgz#115bef7b13716e376431631dc74636d5860c600f" - integrity sha512-KiaoD56w1bZYZ6kj73oIj4y0GdqtIEFtNOA53JL6K1hTba//HP13xdaue9Ts6HnwGOvpR9NFTzuJskt5jl2gSw== - dependencies: - lodash "^4.17.11" - squel "^5.12.2" - jest-worker@24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-24.9.0.tgz#5dbfdb5b2d322e98567898238a9697bcce67b3e5" @@ -4646,7 +4588,7 @@ lodash.throttle@^4.1.1: resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4" integrity sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ= -lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.3: +lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.15, lodash@^4.17.19: version "4.17.20" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== @@ -4704,6 +4646,13 @@ make-error@^1.1.1: resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== +map-age-cleaner@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a" + integrity sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w== + dependencies: + p-defer "^1.0.0" + map-cache@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" @@ -4735,6 +4684,14 @@ media-typer@0.3.0: resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= +mem@^6.0.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/mem/-/mem-6.1.1.tgz#ea110c2ebc079eca3022e6b08c85a795e77f6318" + integrity sha512-Ci6bIfq/UgcxPTYa8dQQ5FY3BzKkT894bwXWXxC/zqs0XgMO2cT20CGkOqda7gZNkmK5VP4x89IGZ6K7hfbn3Q== + dependencies: + map-age-cleaner "^0.1.3" + mimic-fn "^3.0.0" + memoize-one@^5.0.0: version "5.1.1" resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0" @@ -4835,6 +4792,11 @@ mime@^2.2.0: resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.6.tgz#e5b407c90db442f2beb5b162373d07b69affa4d1" integrity sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA== +mimic-fn@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-3.1.0.tgz#65755145bbf3e36954b949c16450427451d5ca74" + integrity sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ== + mimic-response@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.1.0.tgz#d13763d35f613d09ec37ebb30bac0469c0ee8f43" @@ -4862,7 +4824,7 @@ minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" -minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5: +minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== @@ -4951,6 +4913,11 @@ mkdirp@^1.0.3, mkdirp@^1.0.4: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== +module-alias@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/module-alias/-/module-alias-2.2.2.tgz#151cdcecc24e25739ff0aa6e51e1c5716974c0e0" + integrity sha512-A/78XjoX2EmNvppVWEhM2oGk3x4lLxnkEA4jTbaK97QKSDjkIoOsKQlfylt/d3kKKi596Qy3NP5XrXJ6fZIC9Q== + move-concurrently@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92" @@ -5045,10 +5012,10 @@ next-transpile-modules@^4.1.0: micromatch "^4.0.2" slash "^3.0.0" -next@^10.0.0: - version "10.0.0" - resolved "https://registry.yarnpkg.com/next/-/next-10.0.0.tgz#fbc82fa69f05bf82fb5c4e160151f38fb9615e99" - integrity sha512-hpJkikt6tqwj7DfD5Mizwc1kDsaaS73TQK6lJL+++Ht8QXIEs+KUqTZULgdMk80mDV2Zhzo9/JYMEranWwAFLA== +next@^10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/next/-/next-10.0.1.tgz#e1366b41b8547dfc7e9a14d1f7dd8a8584dcf0b2" + integrity sha512-EFlcWe82CQc1QkeIhNogcdC25KBz4CBwjyzfRHhig+wp28GW1O2P/mX+2a7EG1wXg69GyW1JYXOkKk2/VjIwVg== dependencies: "@ampproject/toolbox-optimizer" "2.7.0-alpha.1" "@babel/code-frame" "7.10.4" @@ -5069,10 +5036,10 @@ next@^10.0.0: "@babel/runtime" "7.11.2" "@babel/types" "7.11.5" "@hapi/accept" "5.0.1" - "@next/env" "10.0.0" - "@next/polyfill-module" "10.0.0" - "@next/react-dev-overlay" "10.0.0" - "@next/react-refresh-utils" "10.0.0" + "@next/env" "10.0.1" + "@next/polyfill-module" "10.0.1" + "@next/react-dev-overlay" "10.0.1" + "@next/react-refresh-utils" "10.0.1" ast-types "0.13.2" babel-plugin-transform-define "2.0.0" babel-plugin-transform-react-remove-prop-types "0.4.24" @@ -5120,11 +5087,6 @@ node-abi@^2.7.0: dependencies: semver "^5.4.1" -node-addon-api@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.0.0.tgz#812446a1001a54f71663bed188314bba07e09247" - integrity sha512-sSHCgWfJ+Lui/u+0msF3oyCgvdkhxDbkCS6Q8uiJquzOimkJBvX6hl5aSSA7DR1XbMpdM8r7phjcF63sF4rkKg== - node-addon-api@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.0.2.tgz#04bc7b83fd845ba785bb6eae25bc857e1ef75681" @@ -5317,6 +5279,11 @@ os-homedir@^2.0.0: resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-2.0.0.tgz#a0c76bb001a8392a503cbd46e7e650b3423a923c" integrity sha512-saRNz0DSC5C/I++gFIaJTXoFJMRwiP5zHar5vV3xQ2TkgEw6hDCcU5F272JjUylpiVgBrZNQHnfjkLabTfb92Q== +p-defer@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" + integrity sha1-n26xgvbJqozXQwBKfU+WsZaw+ww= + p-limit@^2.0.0, p-limit@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" @@ -5345,6 +5312,14 @@ p-map@^4.0.0: dependencies: aggregate-error "^3.0.0" +p-memoize@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/p-memoize/-/p-memoize-4.0.1.tgz#6f4231857fec10de2504611fe820c808fa8c5f8b" + integrity sha512-km0sP12uE0dOZ5qP+s7kGVf07QngxyG0gS8sYFvFWhqlgzOsSy+m71aUejf/0akxj5W7gE//2G74qTv6b4iMog== + dependencies: + mem "^6.0.1" + mimic-fn "^3.0.0" + p-try@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" @@ -5415,11 +5390,6 @@ path-is-absolute@^1.0.0: resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= -path-key@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" - integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== - path-parse@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" @@ -5610,27 +5580,6 @@ postcss@^7.0.14, postcss@^7.0.26, postcss@^7.0.32, postcss@^7.0.5, postcss@^7.0. source-map "^0.6.1" supports-color "^6.1.0" -prebuild-install@^5.3.4: - version "5.3.5" - resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-5.3.5.tgz#e7e71e425298785ea9d22d4f958dbaccf8bb0e1b" - integrity sha512-YmMO7dph9CYKi5IR/BzjOJlRzpxGGVo1EsLSUZ0mt/Mq0HWZIHOKHHcHdT69yG54C9m6i45GpItwRHpk0Py7Uw== - dependencies: - detect-libc "^1.0.3" - expand-template "^2.0.3" - github-from-package "0.0.0" - minimist "^1.2.3" - mkdirp "^0.5.1" - napi-build-utils "^1.0.1" - node-abi "^2.7.0" - noop-logger "^0.1.1" - npmlog "^4.0.1" - pump "^3.0.0" - rc "^1.2.7" - simple-get "^3.0.3" - tar-fs "^2.0.0" - tunnel-agent "^0.6.0" - which-pm-runs "^1.0.0" - prebuild-install@^5.3.5: version "5.3.6" resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-5.3.6.tgz#7c225568d864c71d89d07f8796042733a3f54291" @@ -6397,7 +6346,7 @@ shallowequal@^1.1.0: resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8" integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ== -sharp@0.26.2: +sharp@0.26.2, sharp@^0.26.2: version "0.26.2" resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.26.2.tgz#3d5777d246ae32890afe82a783c1cbb98456a88c" integrity sha512-bGBPCxRAvdK9bX5HokqEYma4j/Q5+w8Nrmb2/sfgQCLEUx/HblcpmOfp59obL3+knIKnOhyKmDb4tEOhvFlp6Q== @@ -6412,33 +6361,6 @@ sharp@0.26.2: tar-fs "^2.1.0" tunnel-agent "^0.6.0" -sharp@^0.25.2: - version "0.25.4" - resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.25.4.tgz#1a8e542144a07ab7e9316ab89de80182b827c363" - integrity sha512-umSzJJ1oBwIOfwFFt/fJ7JgCva9FvrEU2cbbm7u/3hSDZhXvkME8WE5qpaJqLIe2Har5msF5UG4CzYlEg5o3BQ== - dependencies: - color "^3.1.2" - detect-libc "^1.0.3" - node-addon-api "^3.0.0" - npmlog "^4.1.2" - prebuild-install "^5.3.4" - semver "^7.3.2" - simple-get "^4.0.0" - tar "^6.0.2" - tunnel-agent "^0.6.0" - -shebang-command@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" - integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== - dependencies: - shebang-regex "^3.0.0" - -shebang-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" - integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== - shell-quote@1.7.2: version "1.7.2" resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.2.tgz#67a7d02c76c9da24f99d20808fcaded0e0e04be2" @@ -6592,11 +6514,6 @@ split-string@^3.0.1, split-string@^3.0.2: dependencies: extend-shallow "^3.0.0" -squel@^5.12.2: - version "5.13.0" - resolved "https://registry.yarnpkg.com/squel/-/squel-5.13.0.tgz#09cc73e91f0d0e326482605ee76e3b7ac881ddf6" - integrity sha512-Fzd8zqbuqNwzodO3yO6MkX8qiDoVBuwqAaa3eKNz4idhBf24IQHbatBhLUiHAGGl962eGvPVRxzRuFWZlSf49w== - ssri@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.1.tgz#2a3c41b28dd45b62b63676ecb74001265ae9edd8" @@ -6744,13 +6661,6 @@ strip-ansi@^4.0.0: dependencies: ansi-regex "^3.0.0" -strip-bom@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" - integrity sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4= - dependencies: - is-utf8 "^0.2.0" - strip-json-comments@~2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" @@ -7032,6 +6942,17 @@ ts-node@8.9.1: source-map-support "^0.5.17" yn "3.1.1" +ts-node@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-9.0.0.tgz#e7699d2a110cc8c0d3b831715e417688683460b3" + integrity sha512-/TqB4SnererCDR/vb4S/QvSZvzQMJN8daAslg7MeaiHvD8rDZsSfXmNeNumyZZzMned72Xoq/isQljYSt8Ynfg== + dependencies: + arg "^4.1.0" + diff "^4.0.1" + make-error "^1.1.1" + source-map-support "^0.5.17" + yn "3.1.1" + ts-pnp@^1.1.6: version "1.2.0" resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.2.0.tgz#a500ad084b0798f1c3071af391e65912c86bca92" @@ -7468,13 +7389,6 @@ which-pm-runs@^1.0.0: resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.0.0.tgz#670b3afbc552e0b55df6b7780ca74615f23ad1cb" integrity sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs= -which@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" - integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== - dependencies: - isexe "^2.0.0" - wide-align@^1.1.0: version "1.1.3" resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" From de9887bff06beb858bd6614c773532df30fcbc50 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Sat, 14 Nov 2020 22:57:53 -0500 Subject: [PATCH 08/10] Add revalidation to all the pages --- src/pages/index.tsx | 2 ++ src/pages/location/[location].tsx | 2 ++ src/pages/member/[member].tsx | 2 ++ src/pages/org/[org].tsx | 2 ++ src/pages/subteam/[subteam].tsx | 2 ++ src/pages/team/[team].tsx | 2 ++ 6 files changed, 12 insertions(+) diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 7931042..f4d5741 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -21,6 +21,8 @@ export const getStaticProps: GetStaticProps = async () => { data: await getMembersIndex(), sidebarData: await getSidebarData(), }, + // page revalidates at most every 5 minutes + revalidate: 1 * 60 * 5, }; }; diff --git a/src/pages/location/[location].tsx b/src/pages/location/[location].tsx index 27a3846..6e1e3f8 100644 --- a/src/pages/location/[location].tsx +++ b/src/pages/location/[location].tsx @@ -37,6 +37,8 @@ export const getStaticProps: GetStaticProps = async ({ params }) => { sidebarData: await getSidebarData(), location: location?.city, }, + // page revalidates at most every 5 minutes + revalidate: 1 * 60 * 5, }; }; diff --git a/src/pages/member/[member].tsx b/src/pages/member/[member].tsx index a5e2ad8..61737c7 100644 --- a/src/pages/member/[member].tsx +++ b/src/pages/member/[member].tsx @@ -64,6 +64,8 @@ export const getStaticProps = async ({ sidebarData: await getSidebarData(), member: JSON.parse(JSON.stringify(member)), }, + // page revalidates at most every 5 minutes + revalidate: 1 * 60 * 5, }; }; diff --git a/src/pages/org/[org].tsx b/src/pages/org/[org].tsx index c46c6ee..e281c2e 100644 --- a/src/pages/org/[org].tsx +++ b/src/pages/org/[org].tsx @@ -41,6 +41,8 @@ export const getStaticProps: GetStaticProps = async ({ params }) => { sidebarData: await getSidebarData(), org: org?.name, }, + // page revalidates at most every 5 minutes + revalidate: 1 * 60 * 5, }; }; diff --git a/src/pages/subteam/[subteam].tsx b/src/pages/subteam/[subteam].tsx index 5e4d6df..0797686 100644 --- a/src/pages/subteam/[subteam].tsx +++ b/src/pages/subteam/[subteam].tsx @@ -41,6 +41,8 @@ export const getStaticProps: GetStaticProps = async ({ params }) => { sidebarData: await getSidebarData(), subteam: subteam?.name, }, + // page revalidates at most every 5 minutes + revalidate: 1 * 60 * 5, }; }; diff --git a/src/pages/team/[team].tsx b/src/pages/team/[team].tsx index e82f24b..27f5f7d 100644 --- a/src/pages/team/[team].tsx +++ b/src/pages/team/[team].tsx @@ -41,6 +41,8 @@ export const getStaticProps: GetStaticProps = async ({ params }) => { sidebarData: await getSidebarData(), team: team?.name, }, + // page revalidates at most every 5 minutes + revalidate: 1 * 60 * 5, }; }; From 15ad86a4c6eb316e3fac97e363d0156f69410f89 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Sat, 14 Nov 2020 23:56:17 -0500 Subject: [PATCH 09/10] Make fallback blocking so 404 pages work --- src/components/Sidebar/index.tsx | 2 +- src/pages/_layout.tsx | 2 +- src/pages/index.tsx | 7 +------ src/pages/location/[location].tsx | 3 ++- src/pages/member/[member].tsx | 3 ++- src/pages/org/[org].tsx | 3 ++- src/pages/subteam/[subteam].tsx | 3 ++- src/pages/team/[team].tsx | 3 ++- 8 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/components/Sidebar/index.tsx b/src/components/Sidebar/index.tsx index 82614a8..70d726d 100644 --- a/src/components/Sidebar/index.tsx +++ b/src/components/Sidebar/index.tsx @@ -131,7 +131,7 @@ export const Sidebar = ({ data }: SidebarProps) => { {/* This spacer should have an mb of the height above + 30px */} - {data.map(([title, prefix, entries]) => { + {data?.map(([title, prefix, entries]) => { return ( = ({ children, ...props }) => { - const { errorCode, data, sidebarData, errorMessage } = props; + const { errorCode, data, errorMessage } = props; if (errorCode) { return ; } diff --git a/src/pages/index.tsx b/src/pages/index.tsx index f4d5741..fafc2e7 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -3,7 +3,6 @@ import { GetStaticProps } from "next"; import { H1 } from "components/Typography"; import { NoResults as DefaultNoResults } from "components/NoResults"; import { FC } from "react"; -import Error from "next/error"; import { useSearchParam } from "utils"; import { TeamMember } from "../components/TeamMember"; import { getMembersIndex, MemberIndexListing } from "data/teamMember"; @@ -34,13 +33,9 @@ const TeamNav: FC = (props) => { const { title, data, NoResults = DefaultNoResults } = props; const searchParam = useSearchParam(); - if (!data) { - return ; - } - const group: { [groupLetter: string]: MemberIndexListing } = {}; data - .filter((member) => + ?.filter((member) => normalizeSearchTerm(member.name).includes( normalizeSearchTerm(searchParam) ) diff --git a/src/pages/location/[location].tsx b/src/pages/location/[location].tsx index 6e1e3f8..8227571 100644 --- a/src/pages/location/[location].tsx +++ b/src/pages/location/[location].tsx @@ -18,7 +18,7 @@ export const getStaticPaths: GetStaticPaths = async () => { location: slug, }, })), - fallback: false, + fallback: "blocking", }; }; @@ -32,6 +32,7 @@ export const getStaticProps: GetStaticProps = async ({ params }) => { where: { slug: locationSlug }, }); return { + notFound: !location, props: { data: members, sidebarData: await getSidebarData(), diff --git a/src/pages/member/[member].tsx b/src/pages/member/[member].tsx index 61737c7..dc64825 100644 --- a/src/pages/member/[member].tsx +++ b/src/pages/member/[member].tsx @@ -26,7 +26,7 @@ export const getStaticPaths: GetStaticPaths<{ member: string }> = async () => { member: slug, }, })), - fallback: false, + fallback: "blocking", }; }; @@ -60,6 +60,7 @@ export const getStaticProps = async ({ }, }); return { + notFound: !member, props: { sidebarData: await getSidebarData(), member: JSON.parse(JSON.stringify(member)), diff --git a/src/pages/org/[org].tsx b/src/pages/org/[org].tsx index e281c2e..d2f6f6c 100644 --- a/src/pages/org/[org].tsx +++ b/src/pages/org/[org].tsx @@ -18,7 +18,7 @@ export const getStaticPaths: GetStaticPaths<{ org: string }> = async () => { org: slug, }, })), - fallback: false, + fallback: "blocking", }; }; @@ -36,6 +36,7 @@ export const getStaticProps: GetStaticProps = async ({ params }) => { where: { slug: orgSlug }, }); return { + notFound: !org, props: { data: members, sidebarData: await getSidebarData(), diff --git a/src/pages/subteam/[subteam].tsx b/src/pages/subteam/[subteam].tsx index 0797686..7626cf6 100644 --- a/src/pages/subteam/[subteam].tsx +++ b/src/pages/subteam/[subteam].tsx @@ -18,7 +18,7 @@ export const getStaticPaths: GetStaticPaths<{ subteam: string }> = async () => { subteam: slug, }, })), - fallback: false, + fallback: "blocking", }; }; @@ -36,6 +36,7 @@ export const getStaticProps: GetStaticProps = async ({ params }) => { where: { slug: subteamSlug }, }); return { + notFound: !subteam, props: { data: members, sidebarData: await getSidebarData(), diff --git a/src/pages/team/[team].tsx b/src/pages/team/[team].tsx index 27f5f7d..5331810 100644 --- a/src/pages/team/[team].tsx +++ b/src/pages/team/[team].tsx @@ -18,7 +18,7 @@ export const getStaticPaths: GetStaticPaths<{ team: string }> = async () => { team: slug, }, })), - fallback: false, + fallback: "blocking", }; }; @@ -36,6 +36,7 @@ export const getStaticProps: GetStaticProps = async ({ params }) => { where: { slug: teamSlug }, }); return { + notFound: !team, props: { data: members, sidebarData: await getSidebarData(), From 3dacf64f16ec301c61f74038a32a8c40f55743c0 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Sun, 15 Nov 2020 00:06:30 -0500 Subject: [PATCH 10/10] Add cron job to refresh local cache --- hokusai/production.yml | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/hokusai/production.yml b/hokusai/production.yml index c9473fa..5ddc12a 100644 --- a/hokusai/production.yml +++ b/hokusai/production.yml @@ -133,3 +133,34 @@ spec: backend: serviceName: team-web-internal servicePort: 3000 +--- +apiVersion: batch/v2alpha1 +kind: CronJob +metadata: + name: team-data-sync-cron +spec: + schedule: "0 0 8-19 ? * MON,TUE,WED,THU,FRI *" + concurrencyPolicy: Forbid + jobTemplate: + spec: + template: + spec: + containers: + - name: team-data-sync-cron + image: 585031190124.dkr.ecr.us-east-1.amazonaws.com/team:production + args: ["yarn", "prime-cache"] + imagePullPolicy: Always + envFrom: + - configMapRef: + name: team-environment + restartPolicy: Never + affinity: + nodeAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 1 + preference: + matchExpressions: + - key: tier + operator: In + values: + - background