diff --git a/next.config.mjs b/next.config.mjs index 8594af34..3960dfb1 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,20 +1,20 @@ /* eslint-disable turbo/no-undeclared-env-vars */ // @ts-check -import bundleAnalyze from '@next/bundle-analyzer' -import nextRoutes from 'nextjs-routes/config' +import bundleAnalyze from "@next/bundle-analyzer"; +import nextRoutes from "nextjs-routes/config"; -import i18nConfig from './next-i18next.config.js' +import i18nConfig from "./next-i18next.config.js"; /** * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful for * Docker builds. */ -!process.env.SKIP_ENV_VALIDATION && (await import('./src/env/server.mjs')) +!process.env.SKIP_ENV_VALIDATION && (await import("./src/env/server.mjs")); -const withRoutes = nextRoutes({ outDir: 'src/types' }) +const withRoutes = nextRoutes({ outDir: "src/types" }); const withBundleAnalyzer = bundleAnalyze({ - enabled: process.env.ANALYZE === 'true', -}) + enabled: process.env.ANALYZE === "true", +}); /** @type {import('next').NextConfig} */ const config = { @@ -22,21 +22,25 @@ const config = { reactStrictMode: true, swcMinify: true, compiler: { - ...(process.env.VERCEL_ENV === 'production' ? { removeConsole: { exclude: ['error'] } } : {}), + ...(process.env.VERCEL_ENV === "production" + ? { removeConsole: { exclude: ["error"] } } + : {}), }, images: { - remotePatterns: [{ protocol: 'https', hostname: 'placehold.co', pathname: '/**' }], + remotePatterns: [ + { protocol: "https", hostname: "placehold.co", pathname: "/**" }, + ], // domains: ['placehold.co'], }, experimental: { outputFileTracingExcludes: { - '*': ['**swc+core**', '**esbuild**'], + "*": ["**swc+core**", "**esbuild**"], }, webpackBuildWorker: true, }, - eslint: { ignoreDuringBuilds: process.env.VERCEL_ENV !== 'production' }, - typescript: { ignoreBuildErrors: process.env.VERCEL_ENV !== 'production' }, -} + eslint: { ignoreDuringBuilds: process.env.VERCEL_ENV !== "production" }, + typescript: { ignoreBuildErrors: process.env.VERCEL_ENV !== "production" }, +}; /** * Wraps NextJS config with the Bundle Analyzer config. * @@ -44,7 +48,7 @@ const config = { * @returns {typeof config} */ function defineNextConfig(config) { - return withBundleAnalyzer(withRoutes(config)) + return withBundleAnalyzer(withRoutes(config)); } -export default defineNextConfig(config) +export default defineNextConfig(config); diff --git a/prisma/dataMigrationRunner.ts b/prisma/dataMigrationRunner.ts index 0e5506c7..1dcf2b19 100644 --- a/prisma/dataMigrationRunner.ts +++ b/prisma/dataMigrationRunner.ts @@ -7,9 +7,9 @@ import { type ListrTask as ListrTaskObj, type ListrTaskWrapper, PRESET_TIMER, -} from 'listr2' +} from "listr2"; -import * as jobList from './data-migrations' +import * as jobList from "./data-migrations"; /** * Job Runner @@ -21,16 +21,16 @@ const renderOptions = { bottomBar: 10, persistentOutput: true, timer: PRESET_TIMER, -} satisfies ListrJob['options'] +} satisfies ListrJob["options"]; const injectOptions = (job: ListrJob): ListrJob => ({ ...job, options: renderOptions, -}) +}); const jobs = new Listr( Object.values(jobList).map((job) => injectOptions(job)), { rendererOptions: { - formatOutput: 'wrap', + formatOutput: "wrap", timer: PRESET_TIMER, suffixSkips: true, }, @@ -39,17 +39,17 @@ const jobs = new Listr( }, exitOnError: false, forceColor: true, - } -) + }, +); -jobs.run() +jobs.run(); export type Context = { - error?: boolean -} -export type PassedTask = ListrTaskWrapper -export type ListrJob = ListrTaskObj + error?: boolean; +}; +export type PassedTask = ListrTaskWrapper; +export type ListrJob = ListrTaskObj; export type ListrTask = ( ctx: Context, - task: PassedTask -) => void | Promise> | Listr + task: PassedTask, +) => void | Promise> | Listr; diff --git a/prisma/seed.ts b/prisma/seed.ts index fcbc0b49..d52c446c 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -1,70 +1,75 @@ /* eslint-disable import/no-unused-modules */ -import { PrismaClient } from '@prisma/client' +import { PrismaClient } from "@prisma/client"; -import fs from 'fs' -import path from 'path' +import fs from "fs"; +import path from "path"; -import { categories } from './seedData/categories' -import { partnerData } from './seedData/partners' -import { pronouns } from './seedData/pronouns' +import { categories } from "./seedData/categories"; +import { partnerData } from "./seedData/partners"; +import { pronouns } from "./seedData/pronouns"; -const prisma = new PrismaClient() +const prisma = new PrismaClient(); async function main() { const categoryResult = await prisma.storyCategory.createMany({ data: categories, skipDuplicates: true, - }) - console.log(`Categories created: ${categoryResult.count}`) + }); + console.log(`Categories created: ${categoryResult.count}`); const pronounResult = await prisma.pronouns.createMany({ data: pronouns, skipDuplicates: true, - }) - console.log(`Pronoun records created: ${pronounResult.count}`) + }); + console.log(`Pronoun records created: ${pronounResult.count}`); const partnerResult = await prisma.partnerOrg.createMany({ data: partnerData, skipDuplicates: true, - }) - console.log(`Partner records created: ${partnerResult.count}`) + }); + console.log(`Partner records created: ${partnerResult.count}`); const output: Record = { categories: await prisma.storyCategory.findMany(), pronouns: await prisma.pronouns.findMany(), partners: await prisma.partnerOrg.findMany(), - } + }; - if (fs.existsSync(path.resolve(__dirname, './seedData/stories.ts'))) { - const stories = await import('./seedData/stories') + if (fs.existsSync(path.resolve(__dirname, "./seedData/stories.ts"))) { + const stories = await import("./seedData/stories"); const storiesResult = await prisma.story.createMany({ data: stories.stories, skipDuplicates: true, - }) - console.log(`Stories created: ${storiesResult.count}`) + }); + console.log(`Stories created: ${storiesResult.count}`); const linkCategories = await prisma.storyToCategory.createMany({ data: stories.links.categories, skipDuplicates: true, - }) + }); const linkPronouns = await prisma.pronounsToStory.createMany({ data: stories.links.pronouns, skipDuplicates: true, - }) - console.log(`Links -> categories: ${linkCategories.count}, pronouns: ${linkPronouns.count}`) + }); + console.log( + `Links -> categories: ${linkCategories.count}, pronouns: ${linkPronouns.count}`, + ); output.storiesResult = await prisma.story.findMany({ select: { id: true, name: true }, - }) + }); } - fs.writeFileSync(path.resolve(__dirname, 'seedresult.json'), JSON.stringify(output)) + fs.writeFileSync( + path.resolve(__dirname, "seedresult.json"), + JSON.stringify(output), + ); } main() .then(async () => { - await prisma.$disconnect() + await prisma.$disconnect(); }) .catch(async (e) => { - console.error(e) - await prisma.$disconnect() - process.exit(1) - }) + console.error(e); + await prisma.$disconnect(); + process.exit(1); + }); diff --git a/prisma/seedData/pronouns.ts b/prisma/seedData/pronouns.ts index 9eaf9473..cef93db9 100644 --- a/prisma/seedData/pronouns.ts +++ b/prisma/seedData/pronouns.ts @@ -1,28 +1,28 @@ -import { type Prisma } from '@prisma/client' +import { type Prisma } from "@prisma/client"; export const pronouns: Prisma.PronounsCreateManyInput[] = [ { - id: 'clienra200007pexbweivoffq', - pronounsEN: 'They/Them/Theirs', - pronounsES: 'Elle', - tag: 'they', + id: "clienra200007pexbweivoffq", + pronounsEN: "They/Them/Theirs", + pronounsES: "Elle", + tag: "they", }, { - id: 'clienra200008pexbpobaxztr', - pronounsEN: 'He/Him/His', - pronounsES: 'Él', - tag: 'he', + id: "clienra200008pexbpobaxztr", + pronounsEN: "He/Him/His", + pronounsES: "Él", + tag: "he", }, { - id: 'clienra200009pexb5wyo4bkt', - pronounsEN: 'Any pronouns', - pronounsES: 'Cualquier pronombre', - tag: 'any', + id: "clienra200009pexb5wyo4bkt", + pronounsEN: "Any pronouns", + pronounsES: "Cualquier pronombre", + tag: "any", }, { - id: 'clienra20000apexb4zhmhq3d', - pronounsEN: 'No pronouns', - pronounsES: 'No pronombres', - tag: 'none', + id: "clienra20000apexb4zhmhq3d", + pronounsEN: "No pronouns", + pronounsES: "No pronombres", + tag: "none", }, -] +]; diff --git a/src/components/Navbar/Navbar.tsx b/src/components/Navbar/Navbar.tsx index 34b6e1b7..5e42e899 100644 --- a/src/components/Navbar/Navbar.tsx +++ b/src/components/Navbar/Navbar.tsx @@ -1,10 +1,19 @@ -import { Anchor, Burger, Container, createStyles, Drawer, Header, rem, Text } from '@mantine/core' -import Link from 'next/link' -import { useRouter } from 'next/router' -import { useTranslation } from 'next-i18next' -import { type Dispatch, type SetStateAction, useEffect, useState } from 'react' - -const HEADER_HEIGHT = 75 +import { + Anchor, + Burger, + Container, + createStyles, + Drawer, + Header, + rem, + Text, +} from "@mantine/core"; +import Link from "next/link"; +import { useRouter } from "next/router"; +import { useTranslation } from "next-i18next"; +import { type Dispatch, type SetStateAction, useEffect, useState } from "react"; + +const HEADER_HEIGHT = 75; export const useStyles = createStyles((theme) => ({ glaadGray: { @@ -12,92 +21,102 @@ export const useStyles = createStyles((theme) => ({ }, navbar: { - display: 'flex', - justifyContent: 'space-around', - alignItems: 'center', - height: '100%', + display: "flex", + justifyContent: "space-around", + alignItems: "center", + height: "100%", - ['& a']: { + ["& a"]: { color: theme.colors.gray[0], - flexDirection: 'row', - textAlign: 'center', - justifyContent: 'center', - alignItems: 'center', + flexDirection: "row", + textAlign: "center", + justifyContent: "center", + alignItems: "center", fontSize: theme.fontSizes.lg, }, - [theme.fn.smallerThan('md')]: { - display: 'none', + [theme.fn.smallerThan("md")]: { + display: "none", }, }, burger: { - display: 'flex', - alignItems: 'center', - height: '100%', + display: "flex", + alignItems: "center", + height: "100%", - [theme.fn.largerThan('md')]: { - display: 'none', + [theme.fn.largerThan("md")]: { + display: "none", }, }, navlink: { color: theme.colors.gray[0], - display: 'flex', - width: '100%', - height: '100%', + display: "flex", + width: "100%", + height: "100%", fontWeight: 600, fontSize: rem(32), - fontStyle: 'italic', - flexDirection: 'column', + fontStyle: "italic", + flexDirection: "column", marginTop: theme.spacing.md, marginBottom: theme.spacing.md, - textDecoration: 'none', + textDecoration: "none", - ['&:active, &:hover']: { - textDecoration: 'underline', + ["&:active, &:hover"]: { + textDecoration: "underline", }, }, -})) - -const NavLinks = ({ setOpened }: { setOpened?: Dispatch> }) => { - const { classes } = useStyles() - const { t } = useTranslation() - const router = useRouter() +})); + +const NavLinks = ({ + setOpened, +}: { + setOpened?: Dispatch>; +}) => { + const { classes } = useStyles(); + const { t } = useTranslation(); + const router = useRouter(); const linksInfo = [ - { key: 'nav.home', href: '/' as const }, - { key: 'nav.gallery', href: '/gallery' as const }, - { key: 'nav.act', href: '/act' as const }, - { key: 'nav.about', href: '/about' as const }, - { key: 'nav.share', href: '/share' as const }, - { key: 'nav.find-resources', href: 'https://app.inreach.org' as const }, - ] //satisfies Array> + { key: "nav.home", href: "/" as const }, + { key: "nav.gallery", href: "/gallery" as const }, + { key: "nav.act", href: "/act" as const }, + { key: "nav.about", href: "/about" as const }, + { key: "nav.share", href: "/share" as const }, + { key: "nav.find-resources", href: "https://app.inreach.org" as const }, + ]; //satisfies Array> useEffect(() => { - router.events.on('routeChangeComplete', () => setOpened && setOpened(false)) - - return router.events.off('routeChangeComplete', () => setOpened && setOpened(false)) + router.events.on( + "routeChangeComplete", + () => setOpened && setOpened(false), + ); + + return router.events.off( + "routeChangeComplete", + () => setOpened && setOpened(false), + ); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [router.asPath]) + }, [router.asPath]); const links = linksInfo.map(({ key, href }) => { - if (href === 'https://app.inreach.org') { + if (href === "https://app.inreach.org") { return ( {t(key).toLocaleUpperCase()} - ) + ); } return ( {t(key).toLocaleUpperCase()} - ) - }) + ); + }); - return <>{links} -} + return <>{links}; +}; // const HomeButton = () => ( // @@ -109,27 +128,30 @@ const NavLinks = ({ setOpened }: { setOpened?: Dispatch> // This type is only needed when trying to make a story for a page // to check whether the button to go to the main page works -type pathProp = { path?: string } +type pathProp = { path?: string }; const HamburgerMenu = ({ path }: pathProp) => { - const [opened, setOpened] = useState(false) - const { classes } = useStyles() - const router = useRouter() - const { asPath, pathname, query, locale } = router - const { t } = useTranslation() + const [opened, setOpened] = useState(false); + const { classes } = useStyles(); + const router = useRouter(); + const { asPath, pathname, query, locale } = router; + const { t } = useTranslation(); return ( - + setOpened(false)} title={ - - {'InReach X GLAAD'} + + {"InReach X GLAAD"} } - size='sm' - padding='xl' + size="sm" + padding="xl" styles={(theme) => ({ content: { backgroundColor: theme.other.colors.glaadGray, @@ -141,34 +163,34 @@ const HamburgerMenu = ({ path }: pathProp) => { > router.replace({ pathname, query }, asPath, { - locale: locale === 'en' ? 'es' : 'en', + locale: locale === "en" ? "es" : "en", }) } > - {t('nav.switch-lang-short')} + {t("nav.switch-lang-short")} {/* {path !== '/' ? : undefined} */} setOpened((o) => !o)} - size='lg' - color='#FEFEFF' - aria-label='burgerButton' - title='Open sidenav' + size="lg" + color="#FEFEFF" + aria-label="burgerButton" + title="Open sidenav" /> - ) -} + ); +}; export const Navbar = ({ path }: pathProp) => { - const { classes } = useStyles() - const router = useRouter() + const { classes } = useStyles(); + const router = useRouter(); return (
@@ -176,5 +198,5 @@ export const Navbar = ({ path }: pathProp) => {
- ) -} + ); +}; diff --git a/src/layouts/ArtItem.tsx b/src/layouts/ArtItem.tsx index d3d4cb49..26ed6901 100644 --- a/src/layouts/ArtItem.tsx +++ b/src/layouts/ArtItem.tsx @@ -1,4 +1,4 @@ -import { type Embla, useAnimationOffsetEffect } from '@mantine/carousel' +import { type Embla, useAnimationOffsetEffect } from "@mantine/carousel"; import { AspectRatio, createStyles, @@ -9,91 +9,100 @@ import { Text, Title, useMantineTheme, -} from '@mantine/core' -import { useMediaQuery } from '@mantine/hooks' -import Image, { type StaticImageData } from 'next/image' -import { useState } from 'react' +} from "@mantine/core"; +import { useMediaQuery } from "@mantine/hooks"; +import Image, { type StaticImageData } from "next/image"; +import { useState } from "react"; -import { StoryPreviewCarousel } from '~/components' +import { StoryPreviewCarousel } from "~/components"; const useStyles = createStyles((theme, { isModal }: { isModal?: boolean }) => ({ story: { - [theme.fn.smallerThan('md')]: { - flexDirection: 'column', + [theme.fn.smallerThan("md")]: { + flexDirection: "column", }, }, content: { padding: isModal ? rem(0) : rem(20), - [theme.fn.largerThan('md')]: { + [theme.fn.largerThan("md")]: { padding: isModal ? `${rem(0)} ${rem(0)} ${rem(40)} ${rem(0)}` : rem(40), - maxWidth: '40%', + maxWidth: "40%", }, - [theme.fn.largerThan('lg')]: { - maxWidth: '50%', + [theme.fn.largerThan("lg")]: { + maxWidth: "50%", }, }, label: { fontSize: rem(18), - fontStyle: 'italic', + fontStyle: "italic", paddingBottom: rem(8), fontWeight: 500, }, text: {}, imageContainer: { - [theme.fn.largerThan('md')]: { - maxWidth: '90%', + [theme.fn.largerThan("md")]: { + maxWidth: "90%", }, - [theme.fn.largerThan('lg')]: { - maxWidth: '50%', + [theme.fn.largerThan("lg")]: { + maxWidth: "50%", }, }, indicator: { - pointerEvents: 'all', + pointerEvents: "all", width: rem(25), height: rem(5), borderRadius: theme.radius.xl, backgroundColor: theme.black, boxShadow: theme.shadows.sm, opacity: 0.6, - transition: `opacity 150ms ${theme.transitionTimingFunction ?? 'ease-in'}`, + transition: `opacity 150ms ${theme.transitionTimingFunction ?? "ease-in"}`, - '&[data-active]': { + "&[data-active]": { opacity: 1, }, }, indicators: { - position: 'absolute', + position: "absolute", bottom: `calc(${theme.spacing.md} * -1)`, top: undefined, left: 0, right: 0, - display: 'flex', - flexDirection: 'row', - justifyContent: 'center', + display: "flex", + flexDirection: "row", + justifyContent: "center", gap: rem(8), - pointerEvents: 'none', + pointerEvents: "none", }, slide: { - objectFit: 'scale-down', + objectFit: "scale-down", }, -})) +})); -export const ArtItem = ({ image, name, description, alt, isModal }: IndividualStoryProps) => { - const { classes } = useStyles({ isModal }) - const theme = useMantineTheme() - const isMobile = useMediaQuery(`(max-width: ${theme.breakpoints.sm})`) - const [embla, setEmbla] = useState(null) - useAnimationOffsetEffect(embla, 200) +export const ArtItem = ({ + image, + name, + description, + alt, + isModal, +}: IndividualStoryProps) => { + const { classes } = useStyles({ isModal }); + const theme = useMantineTheme(); + const isMobile = useMediaQuery(`(max-width: ${theme.breakpoints.sm})`); + const [embla, setEmbla] = useState(null); + useAnimationOffsetEffect(embla, 200); return ( - - + + {Array.isArray(image) ? ( ({ width: `min(${src.width}px, 95vw)`, - [theme.fn.largerThan('sm')]: { + [theme.fn.largerThan("sm")]: { width: `min(${src.width}px, 50vw)`, }, - [theme.fn.largerThan('md')]: { + [theme.fn.largerThan("md")]: { width: `min(${src.width}px, 40vw)`, }, - [theme.fn.largerThan('lg')]: { + [theme.fn.largerThan("lg")]: { width: `min(${src.width}px, 25vw)`, }, })} @@ -126,9 +135,9 @@ export const ArtItem = ({ image, name, description, alt, isModal }: IndividualSt src={src} alt={alt} style={{ - objectFit: 'contain', - maxWidth: '90%', - margin: '0 auto', + objectFit: "contain", + maxWidth: "90%", + margin: "0 auto", }} /> @@ -138,17 +147,17 @@ export const ArtItem = ({ image, name, description, alt, isModal }: IndividualSt ({ width: `min(${image.width}px, 95vw)`, - [theme.fn.largerThan('xs')]: { + [theme.fn.largerThan("xs")]: { width: `min(${image.width}px, 66vw)`, }, - [theme.fn.largerThan('sm')]: { + [theme.fn.largerThan("sm")]: { width: `min(${image.width}px, 40vw)`, }, - [theme.fn.largerThan('md')]: { + [theme.fn.largerThan("md")]: { width: `min(${image.width}px, 25vw)`, }, })} @@ -160,31 +169,37 @@ export const ArtItem = ({ image, name, description, alt, isModal }: IndividualSt - + <Title + order={2} + tt="uppercase" + fw={700} + fz={{ base: 24, lg: 40 }} + pt={{ base: 20, lg: 0 }} + > {name} - + {description ? ( -
+
{description}
) : alt ? ( -
+
{alt}
) : null} - ) -} + ); +}; export interface IndividualStoryProps { - image: StaticImageData | StaticImageData[] - name: string - description?: string - alt: string - isModal?: boolean + image: StaticImageData | StaticImageData[]; + name: string; + description?: string; + alt: string; + isModal?: boolean; } diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 4eeddbc3..a403a3d5 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -1,50 +1,79 @@ -import { Anchor, Button, createStyles, Group, MantineProvider, Text } from '@mantine/core' -import { ReactQueryDevtools } from '@tanstack/react-query-devtools' -import { Analytics } from '@vercel/analytics/react' -import { SpeedInsights } from '@vercel/speed-insights/next' -import { type AppType } from 'next/app' -import Head from 'next/head' -import Image from 'next/image' -import { useRouter } from 'next/router' -import { appWithTranslation, useTranslation } from 'next-i18next' +import { + Anchor, + Button, + createStyles, + Group, + MantineProvider, + Text, +} from "@mantine/core"; +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; +import { Analytics } from "@vercel/analytics/react"; +import { SpeedInsights } from "@vercel/speed-insights/next"; +import { type AppType } from "next/app"; +import Head from "next/head"; +import Image from "next/image"; +import { useRouter } from "next/router"; +import { appWithTranslation, useTranslation } from "next-i18next"; -import { Navbar } from '~/components/Navbar/Navbar' -import { fontWorkSans, styleCache, theme } from '~/styles' -import { api } from '~/utils/api' +import { Navbar } from "~/components/Navbar/Navbar"; +import { fontWorkSans, styleCache, theme } from "~/styles"; +import { api } from "~/utils/api"; -import i18nConfig from '../../next-i18next.config' -import VercelLogo from '../../public/powered-by-vercel.svg' +import i18nConfig from "../../next-i18next.config"; +import VercelLogo from "../../public/powered-by-vercel.svg"; -const useStyles = createStyles((theme, { showButton, isHome }: { showButton: boolean; isHome: boolean }) => ({ - homeButton: { - opacity: showButton ? '1' : '0', - [theme.fn.smallerThan('md')]: { - opacity: 0, - display: 'none', +const useStyles = createStyles( + ( + theme, + { showButton, isHome }: { showButton: boolean; isHome: boolean }, + ) => ({ + homeButton: { + opacity: showButton ? "1" : "0", + [theme.fn.smallerThan("md")]: { + opacity: 0, + display: "none", + }, }, - }, - artistCredit: { - opacity: isHome ? '1' : '0', - }, -})) + artistCredit: { + opacity: isHome ? "1" : "0", + }, + }), +); const MyApp: AppType = ({ Component, pageProps }) => { - const router = useRouter() - const { asPath, pathname, query, locale } = router - const { t } = useTranslation() + const router = useRouter(); + const { asPath, pathname, query, locale } = router; + const { t } = useTranslation(); const { classes } = useStyles({ - showButton: router.pathname !== '/', - isHome: router.pathname === '/', - }) + showButton: router.pathname !== "/", + isHome: router.pathname === "/", + }); return ( <> - {t('page-title.general')} - - - - - + {t("page-title.general")} + + + + + { > - + - {t('artist-credit')} + {t("artist-credit")} router.replace({ pathname, query }, asPath, { - locale: locale === 'en' ? 'es' : 'en', + locale: locale === "en" ? "es" : "en", }) } > - {t('nav.switch-lang')} + {t("nav.switch-lang")} - - - {t('vercel')} + + + {t("vercel")} @@ -90,7 +122,7 @@ const MyApp: AppType = ({ Component, pageProps }) => { - ) -} + ); +}; -export default api.withTRPC(appWithTranslation(MyApp, i18nConfig)) +export default api.withTRPC(appWithTranslation(MyApp, i18nConfig)); diff --git a/src/pages/category/[tag]/[[...storyId]].tsx b/src/pages/category/[tag]/[[...storyId]].tsx index d2482d96..376153a7 100644 --- a/src/pages/category/[tag]/[[...storyId]].tsx +++ b/src/pages/category/[tag]/[[...storyId]].tsx @@ -1,82 +1,99 @@ -import { AspectRatio, Button, Container, Grid, Loader, Modal, Title, useMantineTheme } from '@mantine/core' -import { useMediaQuery } from '@mantine/hooks' -import { type GetStaticPaths, type GetStaticProps } from 'next' -import Head from 'next/head' -import Image from 'next/image' -import Link from 'next/link' -import { useRouter } from 'next/router' -import { useTranslation } from 'next-i18next' -import { type RoutedQuery } from 'nextjs-routes' -import { useMemo } from 'react' - -import { PreviewCard } from '~/components/storyPreviewCard/PreviewCard' -import { getCategoryImage } from '~/data/categoryImages' -import { CardDisplay } from '~/layouts/CardDisplay' -import { IndividualStory } from '~/layouts/IndividualStory/IndividualStory' -import { prisma } from '~/server/db' -import { getServerSideTranslations } from '~/server/i18n' -import { fontIbmPlexSans } from '~/styles' -import { api, type RouterOutputs } from '~/utils/api' -import { trpcServerClient } from '~/utils/ssr' -import Logo from '~public/assets/tmf-logo-rect-bw-cropped.png' +import { + AspectRatio, + Button, + Container, + Grid, + Loader, + Modal, + Title, + useMantineTheme, +} from "@mantine/core"; +import { useMediaQuery } from "@mantine/hooks"; +import { type GetStaticPaths, type GetStaticProps } from "next"; +import Head from "next/head"; +import Image from "next/image"; +import Link from "next/link"; +import { useRouter } from "next/router"; +import { useTranslation } from "next-i18next"; +import { type RoutedQuery } from "nextjs-routes"; +import { useMemo } from "react"; + +import { PreviewCard } from "~/components/storyPreviewCard/PreviewCard"; +import { getCategoryImage } from "~/data/categoryImages"; +import { CardDisplay } from "~/layouts/CardDisplay"; +import { IndividualStory } from "~/layouts/IndividualStory/IndividualStory"; +import { prisma } from "~/server/db"; +import { getServerSideTranslations } from "~/server/i18n"; +import { fontIbmPlexSans } from "~/styles"; +import { api, type RouterOutputs } from "~/utils/api"; +import { trpcServerClient } from "~/utils/ssr"; +import Logo from "~public/assets/tmf-logo-rect-bw-cropped.png"; export const CategoryPage = ({}: CategoryPageProps) => { - const router = useRouter<'/category/[tag]/[[...storyId]]'>() - const theme = useMantineTheme() - const isMobile = useMediaQuery(`(max-width: ${theme.breakpoints.xs})`) - const category = router.query.tag - const locale = ['en', 'es'].includes(router.locale) ? router.locale : 'en' + const router = useRouter<"/category/[tag]/[[...storyId]]">(); + const theme = useMantineTheme(); + const isMobile = useMediaQuery(`(max-width: ${theme.breakpoints.xs})`); + const category = router.query.tag; + const locale = ["en", "es"].includes(router.locale) ? router.locale : "en"; const popupStory = useMemo( () => Array.isArray(router.query.storyId) && router.query.storyId.length ? router.query.storyId.at(0) - : typeof router.query.storyId === 'string' + : typeof router.query.storyId === "string" ? router.query.storyId : undefined, - [router.query.storyId] - ) + [router.query.storyId], + ); const { data: stories } = api.story.getByCategory.useQuery( - { tag: router.query.tag ?? '', locale }, - { enabled: Boolean(router.query.tag) } - ) + { tag: router.query.tag ?? "", locale }, + { enabled: Boolean(router.query.tag) }, + ); const { data: singleStory } = api.story.getStoryById.useQuery( - { id: popupStory ?? '', locale }, - { enabled: typeof popupStory === 'string' } - ) - - const { t } = useTranslation() - - if (!category || !stories) return - - const previewCards = stories.map(({ name, pronouns, response1, response2, id }, i) => { - const pronounList = pronouns.map(({ pronoun }) => pronoun) - const storyText = response1 ?? response2 - return ( - - - - ) - }) + { id: popupStory ?? "", locale }, + { enabled: typeof popupStory === "string" }, + ); + + const { t } = useTranslation(); + + if (!category || !stories) return ; + + const previewCards = stories.map( + ({ name, pronouns, response1, response2, id }, i) => { + const pronounList = pronouns.map(({ pronoun }) => pronoun); + const storyText = response1 ?? response2; + return ( + + + + ); + }, + ); return ( - {t('page-title.general-template', { page: '$t(nav.stories)' })} + + {t("page-title.general-template", { page: "$t(nav.stories)" })} + - + - - {t('logo-alt')} + + {t("logo-alt")} @@ -84,8 +101,8 @@ export const CategoryPage = ({}: CategoryPageProps) => { fw={100} order={1} fz={30} - align='center' - tt='uppercase' + align="center" + tt="uppercase" style={fontIbmPlexSans.style} lts={5} > @@ -95,17 +112,19 @@ export const CategoryPage = ({}: CategoryPageProps) => { - {Boolean(previewCards.length) && {previewCards}} + {Boolean(previewCards.length) && ( + {previewCards} + )} { onClose={() => router.replace( { - pathname: '/category/[tag]/[[...storyId]]', - query: { tag: router.query.tag ?? '' }, + pathname: "/category/[tag]/[[...storyId]]", + query: { tag: router.query.tag ?? "" }, }, undefined, { shallow: true, scroll: false, - } + }, ) } - size='75vw' + size="75vw" centered overlayProps={{ blur: 2 }} - closeButtonProps={{ size: isMobile ? 'xl' : 'lg' }} - radius='xl' + closeButtonProps={{ size: isMobile ? "xl" : "lg" }} + radius="xl" fullScreen={isMobile} > {singleStory && ( @@ -141,42 +160,49 @@ export const CategoryPage = ({}: CategoryPageProps) => { )} - ) -} + ); +}; type CategoryPageProps = { - stories: RouterOutputs['story']['getByCategory'] - category: string -} + stories: RouterOutputs["story"]["getByCategory"]; + category: string; +}; export const getStaticProps: GetStaticProps< Record, - RoutedQuery<'/category/[tag]/[[...storyId]]'> + RoutedQuery<"/category/[tag]/[[...storyId]]"> > = async ({ locale: ssrLocale, params }) => { - const locale = (['en', 'es'].includes(ssrLocale ?? '') ? ssrLocale : 'en') as 'en' | 'es' - const ssg = trpcServerClient() - if (!params?.tag) return { notFound: true } + const locale = (["en", "es"].includes(ssrLocale ?? "") ? ssrLocale : "en") as + | "en" + | "es"; + const ssg = trpcServerClient(); + if (!params?.tag) return { notFound: true }; - const storyId = Array.isArray(params.storyId) && params.storyId.length ? params.storyId.at(0) : undefined + const storyId = + Array.isArray(params.storyId) && params.storyId.length + ? params.storyId.at(0) + : undefined; const [i18n] = await Promise.allSettled([ getServerSideTranslations(locale), ssg.story.getByCategory.prefetch({ tag: params?.tag, locale }), - ...(storyId ? [ssg.story.getStoryById.prefetch({ id: storyId, locale })] : []), - ]) + ...(storyId + ? [ssg.story.getStoryById.prefetch({ id: storyId, locale })] + : []), + ]); return { props: { trpcState: ssg.dehydrate(), - ...(i18n.status === 'fulfilled' ? i18n.value : {}), + ...(i18n.status === "fulfilled" ? i18n.value : {}), }, // revalidate: 604800, // 1 week - } -} + }; +}; export const getStaticPaths: GetStaticPaths<{ - tag: string - storyId?: string[] -}> = async ({ locales = ['en', 'es'] }) => { + tag: string; + storyId?: string[]; +}> = async ({ locales = ["en", "es"] }) => { const categories = await prisma.storyCategory.findMany({ select: { tag: true, @@ -185,7 +211,7 @@ export const getStaticPaths: GetStaticPaths<{ where: { story: { published: true } }, }, }, - }) + }); const paths = categories.flatMap(({ tag, stories }) => [ ...locales.map((locale) => ({ @@ -196,14 +222,14 @@ export const getStaticPaths: GetStaticPaths<{ locales.map((locale) => ({ params: { tag, storyId: [storyId] }, locale, - })) + })), ), - ]) + ]); return { paths, - fallback: 'blocking', - } -} + fallback: "blocking", + }; +}; -export default CategoryPage +export default CategoryPage; diff --git a/src/pages/gallery/index.tsx b/src/pages/gallery/index.tsx index c177456f..dedd0e17 100644 --- a/src/pages/gallery/index.tsx +++ b/src/pages/gallery/index.tsx @@ -12,36 +12,36 @@ import { Stack, Title, useMantineTheme, -} from '@mantine/core' -import { useHover, useMediaQuery } from '@mantine/hooks' -import { type GetStaticProps } from 'next' -import Head from 'next/head' -import NextImage, { type ImageProps as NextImageProps } from 'next/image' -import Link from 'next/link' -import { useRouter } from 'next/router' -import { useTranslation } from 'next-i18next' -import { forwardRef, useMemo } from 'react' +} from "@mantine/core"; +import { useHover, useMediaQuery } from "@mantine/hooks"; +import { type GetStaticProps } from "next"; +import Head from "next/head"; +import NextImage, { type ImageProps as NextImageProps } from "next/image"; +import Link from "next/link"; +import { useRouter } from "next/router"; +import { useTranslation } from "next-i18next"; +import { forwardRef, useMemo } from "react"; -import { StoryPreviewCarousel } from '~/components' -import { artData, getArtData } from '~/data/artwork' -import { ArtItem } from '~/layouts/ArtItem' -import { getServerSideTranslations } from '~/server/i18n' -import { fontIbmPlexSans } from '~/styles' -import Logo from '~public/assets/tmf-logo-rect-bw-cropped.png' +import { StoryPreviewCarousel } from "~/components"; +import { artData, getArtData } from "~/data/artwork"; +import { ArtItem } from "~/layouts/ArtItem"; +import { getServerSideTranslations } from "~/server/i18n"; +import { fontIbmPlexSans } from "~/styles"; +import Logo from "~public/assets/tmf-logo-rect-bw-cropped.png"; const uesImageStyles = createStyles((theme) => ({ root: {}, -})) +})); interface ImageProps extends DefaultProps, NextImageProps {} const Image = forwardRef( ({ classNames, styles, unstyled, className, alt, src, ...props }, ref) => { const { classes, cx } = uesImageStyles(undefined, { - name: 'NextImage', + name: "NextImage", classNames, styles, unstyled, - }) + }); return ( ( className={cx(classes.root, className)} {...props} /> - ) - } -) -Image.displayName = 'NextImage' + ); + }, +); +Image.displayName = "NextImage"; const Gallery = () => { - const router = useRouter() - const theme = useMantineTheme() - const isMobile = useMediaQuery(`(max-width: ${theme.breakpoints.sm})`) - const { hovered, ref } = useHover() - const isEnglish = router.locale === 'en' - const { t } = useTranslation() + const router = useRouter(); + const theme = useMantineTheme(); + const isMobile = useMediaQuery(`(max-width: ${theme.breakpoints.sm})`); + const { hovered, ref } = useHover(); + const isEnglish = router.locale === "en"; + const { t } = useTranslation(); const popupArt = useMemo( () => - router.query.artist && typeof router.query.artist === 'string' + router.query.artist && typeof router.query.artist === "string" ? getArtData(router.query.artist) : Array.isArray(router.query.artist) && router.query.artist.length ? getArtData(router.query.artist.at(0)) : undefined, - [router.query.artist] - ) + [router.query.artist], + ); const slides = artData.map((art) => { if (!Array.isArray(art.src)) { - const { height, src, width } = art.src + const { height, src, width } = art.src; return ( - + { sx={(theme) => ({ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions width: `min(${width}px, 80vw)`, - [theme.fn.largerThan('xs')]: { + [theme.fn.largerThan("xs")]: { width: `min(${width}px, 40vw)`, }, - [theme.fn.largerThan('sm')]: { + [theme.fn.largerThan("sm")]: { // eslint-disable-next-line @typescript-eslint/restrict-template-expressions width: `min(${width}px, 25vw)`, }, @@ -105,28 +105,34 @@ const Gallery = () => { - + <Title order={3} tt="uppercase" py={40}> {art.artist} - ) + ); } return ( - + - + {art.src.map((image, i, arr) => ( { sx={(theme) => ({ width: `min(${image.width}px, 80vw)`, zIndex: 10 - i, - position: i === 0 ? 'relative' : 'absolute', + position: i === 0 ? "relative" : "absolute", top: 0, - [theme.fn.largerThan('xs')]: { + [theme.fn.largerThan("xs")]: { width: `min(${image.width}px, 40vw)`, }, - [theme.fn.largerThan('sm')]: { + [theme.fn.largerThan("sm")]: { width: `min(${image.width}px, 25vw)`, }, })} @@ -152,8 +158,8 @@ const Gallery = () => { transform: `translateX(${20 * i}px) translateY(${10 * i}px) rotate(-${ (arr.length - i) * 2 }deg)`, - transition: 'transform .5s ease-in-out', - '&[data-hovered=true]': { + transition: "transform .5s ease-in-out", + "&[data-hovered=true]": { transform: `translateX(${30 * i}px) translateY(${15 * i}px) rotate(-${ (arr.length - i) * 4 }deg)`, @@ -168,33 +174,35 @@ const Gallery = () => { - + <Title order={3} tt="uppercase" py={40}> {art.artist} - ) - }) + ); + }); return ( - {t('page-title.general-template', { page: '$t(nav.gallery)' })} + + {t("page-title.general-template", { page: "$t(nav.gallery)" })} + - + - - {t('logo-alt')} + + {t("logo-alt")} @@ -202,33 +210,38 @@ const Gallery = () => { fw={300} order={1} fz={30} - align='center' - tt='uppercase' + align="center" + tt="uppercase" lts={5} style={fontIbmPlexSans.style} > - {t('page-title.gallery')} + {t("page-title.gallery")} - - + + {slides} - + {slides.map((slide, i) => ( @@ -241,16 +254,16 @@ const Gallery = () => { opened={!!popupArt} // eslint-disable-next-line @typescript-eslint/no-misused-promises onClose={() => - router.replace({ pathname: '/gallery' }, undefined, { + router.replace({ pathname: "/gallery" }, undefined, { shallow: true, scroll: false, }) } - size='75vw' + size="75vw" centered overlayProps={{ blur: 2 }} - closeButtonProps={{ size: isMobile ? 'xl' : 'lg' }} - radius='xl' + closeButtonProps={{ size: isMobile ? "xl" : "lg" }} + radius="xl" fullScreen={isMobile} styles={(theme) => ({ content: isMobile @@ -269,20 +282,22 @@ const Gallery = () => { name={popupArt.artist} image={popupArt.src} alt={isEnglish ? popupArt.altEN : popupArt.altES} - description={isEnglish ? popupArt.descriptionEN : popupArt.descriptionES} + description={ + isEnglish ? popupArt.descriptionEN : popupArt.descriptionES + } /> )} - ) -} + ); +}; export const getStaticProps: GetStaticProps = async ({ locale }) => { return { props: { ...(await getServerSideTranslations(locale)), }, revalidate: 60 * 60 * 24 * 7, // 1 week - } -} + }; +}; -export default Gallery +export default Gallery; diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 58f12a38..ed08d6b5 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -12,26 +12,26 @@ import { rem, Stack, Title, -} from '@mantine/core' -import { type GetStaticProps, type NextPage } from 'next' -import Image from 'next/image' -import Link from 'next/link' -import { useRouter } from 'next/router' -import { Trans, useTranslation } from 'next-i18next' +} from "@mantine/core"; +import { type GetStaticProps, type NextPage } from "next"; +import Image from "next/image"; +import Link from "next/link"; +import { useRouter } from "next/router"; +import { Trans, useTranslation } from "next-i18next"; -import { categoryImages, isValidCategoryImage } from '~/data/categoryImages' -import { getServerSideTranslations } from '~/server/i18n' -import { trpcServerClient } from '~/utils/ssr' -import Logo from '~public/assets/tmf-logo-rect-bw.png' +import { categoryImages, isValidCategoryImage } from "~/data/categoryImages"; +import { getServerSideTranslations } from "~/server/i18n"; +import { trpcServerClient } from "~/utils/ssr"; +import Logo from "~public/assets/tmf-logo-rect-bw.png"; -import { api, type RouterOutputs } from '../utils/api' +import { api, type RouterOutputs } from "../utils/api"; const useStyles = createStyles((theme) => { return { cardGroup: { [theme.fn.smallerThan(rem(850))]: { - flexDirection: 'column', - margin: '0 auto', + flexDirection: "column", + margin: "0 auto", }, }, categoryImage: { @@ -41,137 +41,169 @@ const useStyles = createStyles((theme) => { }), }, header: { - justifyContent: 'center', + justifyContent: "center", }, - } -}) + }; +}); export const MainPage = ({ categories }: MainPageProps) => { - const { classes } = useStyles() - const { t } = useTranslation() - const previewCards = categories.map(({ category, id, image, imageAlt, tag }, i) => { - // aspect ratio 0.55 + const { classes } = useStyles(); + const { t } = useTranslation(); + const previewCards = categories.map( + ({ category, id, image, imageAlt, tag }, i) => { + // aspect ratio 0.55 - const imageSrc = isValidCategoryImage(image) - ? categoryImages[image] - : `https://placehold.co/300x${Math.round(300 * 0.55)}` - const altText = imageAlt ?? '' - const categoryName = category - return ( - - - {altText} - -
- + {altText} -
-
- ) - }) +
+ + + +
+
+ ); + }, + ); return ( - - - - {t('logo-alt')} + + + + {t("logo-alt")} - - {t('main-page.tagline1')} + <Title order={3} ta="center" py={0} fw={500} lts={2} fs="oblique"> + {t("main-page.tagline1")} - - {t('main-page.tagline2')} + <Title + order={3} + ta="center" + pb={0} + pt={8} + fw={500} + lts={2} + fs="oblique" + > + {t("main-page.tagline2")} - - + {previewCards.map((card, i) => ( - + {card} ))} - ) -} + ); +}; export type MainPageProps = { - categories: RouterOutputs['story']['getCategories'] -} + categories: RouterOutputs["story"]["getCategories"]; +}; const Home: NextPage = () => { - const router = useRouter() + const router = useRouter(); const { data, status } = api.story.getCategories.useQuery({ locale: router.locale, - }) + }); if (data === undefined) { return ( -
- {status === 'error' ? ( - - - {'Ooops something went wrong :('} + <Center style={{ width: "100%", height: "100%" }}> + {status === "error" ? ( + <Flex direction={"column"} align="center"> + <Title order={1} ta="center"> + {"Ooops something went wrong :("} - - {'Try refreshing the page'} + <Title order={2} ta="center"> + {"Try refreshing the page"} ) : ( - + )}
- ) + ); } else { - return + return ; } -} +}; -export default Home +export default Home; export const getStaticProps: GetStaticProps = async ({ locale: ssrLocale }) => { - const locale = (['en', 'es'].includes(ssrLocale ?? '') ? ssrLocale : 'en') as 'en' | 'es' - const ssg = trpcServerClient() + const locale = (["en", "es"].includes(ssrLocale ?? "") ? ssrLocale : "en") as + | "en" + | "es"; + const ssg = trpcServerClient(); const [i18n] = await Promise.allSettled([ getServerSideTranslations(locale), ssg.story.getCategories.prefetch({ locale }), - ]) + ]); return { props: { trpcState: ssg.dehydrate(), - ...(i18n.status === 'fulfilled' ? i18n.value : {}), + ...(i18n.status === "fulfilled" ? i18n.value : {}), }, revalidate: 60 * 60 * 24 * 7, // 1 week - } -} + }; +}; diff --git a/src/pages/story/[id].tsx b/src/pages/story/[id].tsx index ba0213e3..21440f27 100644 --- a/src/pages/story/[id].tsx +++ b/src/pages/story/[id].tsx @@ -1,28 +1,33 @@ -import { type GetStaticPaths, type GetStaticProps } from 'next' -import Head from 'next/head' -import { useRouter } from 'next/router' -import { useTranslation } from 'next-i18next' -import { type RoutedQuery } from 'nextjs-routes' +import { type GetStaticPaths, type GetStaticProps } from "next"; +import Head from "next/head"; +import { useRouter } from "next/router"; +import { useTranslation } from "next-i18next"; +import { type RoutedQuery } from "nextjs-routes"; -import { getCategoryImage } from '~/data/categoryImages' -import { IndividualStory, type IndividualStoryProps } from '~/layouts/IndividualStory/IndividualStory' -import { prisma } from '~/server/db' -import { getServerSideTranslations } from '~/server/i18n' -import { api } from '~/utils/api' -import { trpcServerClient } from '~/utils/ssr' +import { getCategoryImage } from "~/data/categoryImages"; +import { + IndividualStory, + type IndividualStoryProps, +} from "~/layouts/IndividualStory/IndividualStory"; +import { prisma } from "~/server/db"; +import { getServerSideTranslations } from "~/server/i18n"; +import { api } from "~/utils/api"; +import { trpcServerClient } from "~/utils/ssr"; const Story = () => { - const router = useRouter<'/story/[id]'>() - const locale = ['en', 'es'].includes(router.locale) ? router.locale : 'en' + const router = useRouter<"/story/[id]">(); + const locale = ["en", "es"].includes(router.locale) ? router.locale : "en"; const { data, isLoading } = api.story.getStoryById.useQuery({ - id: router.query.id ?? '', + id: router.query.id ?? "", locale, - }) - const { t } = useTranslation() - if (!data || isLoading) return <>Loading... + }); + const { t } = useTranslation(); + if (!data || isLoading) return <>Loading...; - const randomImage = data.categories.at(Math.floor(Math.random() * data.categories.length))?.category?.image - const image = getCategoryImage(randomImage ?? '') + const randomImage = data.categories.at( + Math.floor(Math.random() * data.categories.length), + )?.category?.image; + const image = getCategoryImage(randomImage ?? ""); const storyProps: IndividualStoryProps = { name: data.name, @@ -30,49 +35,57 @@ const Story = () => { pronouns: data.pronouns.map(({ pronoun }) => pronoun), response1: data.response1, response2: data.response2, - } + }; return ( <> - {t('page-title.general-template', { page: '$t(nav.stories)' })} + + {t("page-title.general-template", { page: "$t(nav.stories)" })} + - ) -} + ); +}; -export const getStaticProps: GetStaticProps, RoutedQuery<'/story/[id]'>> = async ({ - locale: ssrLocale, - params, -}) => { - const locale = (['en', 'es'].includes(ssrLocale ?? '') ? ssrLocale : 'en') as 'en' | 'es' - const ssg = trpcServerClient() - if (!params?.id) return { notFound: true } +export const getStaticProps: GetStaticProps< + Record, + RoutedQuery<"/story/[id]"> +> = async ({ locale: ssrLocale, params }) => { + const locale = (["en", "es"].includes(ssrLocale ?? "") ? ssrLocale : "en") as + | "en" + | "es"; + const ssg = trpcServerClient(); + if (!params?.id) return { notFound: true }; const [i18n] = await Promise.allSettled([ getServerSideTranslations(locale), ssg.story.getStoryById.prefetch({ id: params.id, locale }), - ]) + ]); return { props: { trpcState: ssg.dehydrate(), - ...(i18n.status === 'fulfilled' ? i18n.value : {}), + ...(i18n.status === "fulfilled" ? i18n.value : {}), }, // revalidate: 60 * 60 * 24 * 7, // 1 week - } -} -export const getStaticPaths: GetStaticPaths = async ({ locales = ['en', 'es'] }) => { + }; +}; +export const getStaticPaths: GetStaticPaths = async ({ + locales = ["en", "es"], +}) => { const stories = await prisma.story.findMany({ select: { id: true }, where: { published: true }, - }) + }); return { - paths: stories.flatMap(({ id }) => locales.map((locale) => ({ params: { id }, locale }))), - fallback: 'blocking', - } -} + paths: stories.flatMap(({ id }) => + locales.map((locale) => ({ params: { id }, locale })), + ), + fallback: "blocking", + }; +}; -export default Story +export default Story; diff --git a/src/pages/survey.tsx b/src/pages/survey.tsx index 1f4e08c6..850a2cba 100644 --- a/src/pages/survey.tsx +++ b/src/pages/survey.tsx @@ -14,20 +14,20 @@ import { Text, Textarea, TextInput, -} from '@mantine/core' -import { useForm, zodResolver } from '@mantine/form' -import { type GetStaticProps } from 'next' -import Image from 'next/image' -import { useRouter } from 'next/router' -import { Trans, useTranslation } from 'next-i18next' -import { useEffect, useMemo, useRef, useState } from 'react' -import { z } from 'zod' +} from "@mantine/core"; +import { useForm, zodResolver } from "@mantine/form"; +import { type GetStaticProps } from "next"; +import Image from "next/image"; +import { useRouter } from "next/router"; +import { Trans, useTranslation } from "next-i18next"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { z } from "zod"; -import { Banner } from '~/components' -import { stateOptions } from '~/data/states' -import { getServerSideTranslations } from '~/server/i18n' -import { api } from '~/utils/api' -import ShareWide from '~public/assets/share-wide.jpg' +import { Banner } from "~/components"; +import { stateOptions } from "~/data/states"; +import { getServerSideTranslations } from "~/server/i18n"; +import { api } from "~/utils/api"; +import ShareWide from "~public/assets/share-wide.jpg"; export const SurveySchema = (ts?: (key: string) => string) => z @@ -35,33 +35,33 @@ export const SurveySchema = (ts?: (key: string) => string) => q1: z .string() .array() - .length(3, ts ? ts('errors.verify-all-3') : undefined), + .length(3, ts ? ts("errors.verify-all-3") : undefined), q2: z .string() .array() - .length(3, ts ? ts('errors.consent-all-3') : undefined), + .length(3, ts ? ts("errors.consent-all-3") : undefined), q3: z.string().email().optional(), q4: z.string().min(1), q5: z .string() .array() - .min(1, ts ? ts('errors.select-min-one') : undefined), + .min(1, ts ? ts("errors.select-min-one") : undefined), q6: z.coerce .number() - .min(21, ts ? ts('errors.min-21') : undefined) + .min(21, ts ? ts("errors.min-21") : undefined) .or( z .string() .refine( (val) => !isNaN(parseInt(val)) && parseInt(val) >= 21, - ts ? ts('errors.min-21') : undefined - ) + ts ? ts("errors.min-21") : undefined, + ), ) .optional(), q7: z .string() .array() - .min(1, ts ? ts('errors.select-min-one') : undefined), + .min(1, ts ? ts("errors.select-min-one") : undefined), q7other: z.string().optional(), q8: z.string().min(1), q9: z.string().min(1), @@ -76,130 +76,143 @@ export const SurveySchema = (ts?: (key: string) => string) => q15: z.string().array().optional(), }) .superRefine((val, ctx) => { - if (val.q7.includes('other') && !val.q7other) { + if (val.q7.includes("other") && !val.q7other) { return ctx.addIssue({ code: z.ZodIssueCode.custom, - message: ts ? ts('errors.other-val') : undefined, - }) + message: ts ? ts("errors.other-val") : undefined, + }); } - if (val.q10?.includes('other') && !val.q10other) { + if (val.q10?.includes("other") && !val.q10other) { return ctx.addIssue({ code: z.ZodIssueCode.custom, - message: ts ? ts('errors.other-val') : undefined, - }) + message: ts ? ts("errors.other-val") : undefined, + }); } - if (val.q11?.includes('other') && !val.q11other) { + if (val.q11?.includes("other") && !val.q11other) { return ctx.addIssue({ code: z.ZodIssueCode.custom, - message: ts ? ts('errors.other-val') : undefined, - }) + message: ts ? ts("errors.other-val") : undefined, + }); } - if (val.q12?.includes('other') && !val.q12other) { + if (val.q12?.includes("other") && !val.q12other) { return ctx.addIssue({ code: z.ZodIssueCode.custom, - message: ts ? ts('errors.other-val') : undefined, - }) + message: ts ? ts("errors.other-val") : undefined, + }); } - }) + }); const Survey = () => { - const { t } = useTranslation() + const { t } = useTranslation(); - const ts = (key: string) => t(key) satisfies string + const ts = (key: string) => t(key) satisfies string; const form = useForm({ validate: zodResolver(SurveySchema(ts)), validateInputOnBlur: true, initialValues: { q1: [], q2: [], - q4: '', + q4: "", q5: [], q7: [], - q8: '', - q9: '', + q8: "", + q9: "", }, - }) + }); - const [activeStep, setActiveStep] = useState(0) - const scrollRef = useRef(null) + const [activeStep, setActiveStep] = useState(0); + const scrollRef = useRef(null); const nextStep = () => { - setActiveStep((current) => (current < 3 ? current + 1 : current)) - scrollRef.current?.scrollIntoView({ behavior: 'smooth' }) - } + setActiveStep((current) => (current < 3 ? current + 1 : current)); + scrollRef.current?.scrollIntoView({ behavior: "smooth" }); + }; const prevStep = () => { - setActiveStep((current) => (current >= 0 ? current - 1 : current)) - scrollRef.current?.scrollIntoView({ behavior: 'smooth' }) - } - const router = useRouter() - const isEnglish = router.locale === 'en' + setActiveStep((current) => (current >= 0 ? current - 1 : current)); + scrollRef.current?.scrollIntoView({ behavior: "smooth" }); + }; + const router = useRouter(); + const isEnglish = router.locale === "en"; const okToProceed = useMemo(() => { if (activeStep === 0) { - return ['q1', 'q2', 'q3'].every((q) => form.isValid(q)) + return ["q1", "q2", "q3"].every((q) => form.isValid(q)); } if (activeStep === 1) { - return ['q4', 'q5', 'q6', 'q7', 'q7other', 'q8', 'q9'].every((q) => form.isValid(q)) + return ["q4", "q5", "q6", "q7", "q7other", "q8", "q9"].every((q) => + form.isValid(q), + ); } - return true + return true; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [form.values, activeStep]) + }, [form.values, activeStep]); useEffect(() => { - if (form.values?.q12?.includes('no-answer')) { + if (form.values?.q12?.includes("no-answer")) { if (form.values.q12.length !== 1) { - form.setFieldValue('q12', ['no-answer']) - form.setFieldValue('q12other', undefined) + form.setFieldValue("q12", ["no-answer"]); + form.setFieldValue("q12other", undefined); } } - if (form.values?.q15?.includes('no-answer')) { + if (form.values?.q15?.includes("no-answer")) { if (form.values.q15.length !== 1) { - form.setFieldValue('q15', ['no-answer']) + form.setFieldValue("q15", ["no-answer"]); } } - if (form.values?.q15?.includes('none')) { + if (form.values?.q15?.includes("none")) { if (form.values.q15.length !== 1) { - form.setFieldValue('q15', ['none']) + form.setFieldValue("q15", ["none"]); } } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [form.values.q12, form.values.q15]) + }, [form.values.q12, form.values.q15]); const submitStory = api.story.submit.useMutation({ onSuccess: () => { - nextStep() + nextStep(); }, - }) + }); const ControlButtons = () => { - const isFirst = activeStep === 0 - const isLast = activeStep === 3 + const isFirst = activeStep === 0; + const isLast = activeStep === 3; return ( - - - ) - } + ); + }; - const [statePNA, setStatePNA] = useState(false) + const [statePNA, setStatePNA] = useState(false); const stateSelectOptions = useMemo( () => @@ -207,13 +220,13 @@ const Survey = () => { value, label: isEnglish ? labelEN : labelES, })), - [isEnglish] - ) + [isEnglish], + ); return ( <> - + { breakpoint={800} allowNextStepsSelect={false} > - + - {t('survey-form.intro')} + {t("survey-form.intro")} - router.replace({ pathname: '/survey' }, undefined, { - locale: isEnglish ? 'es' : 'en', + router.replace({ pathname: "/survey" }, undefined, { + locale: isEnglish ? "es" : "en", scroll: false, }) } > - {t('survey-form.switch-lang')} + {t("survey-form.switch-lang")} , List: ., @@ -246,133 +259,198 @@ const Survey = () => { }} /> {/* You verify that the following is true (check all that apply): */} - - {Object.entries(t('survey-form.q1-opts', { returnObjects: true })).map(([key, value], i) => ( + + {Object.entries( + t("survey-form.q1-opts", { returnObjects: true }), + ).map(([key, value], i) => ( ))} {/* I consent to having my submission shared by InReach via */} - - {Object.entries(t('survey-form.q2-opts', { returnObjects: true })).map(([key, value], i) => ( + + {Object.entries( + t("survey-form.q2-opts", { returnObjects: true }), + ).map(([key, value], i) => ( ))} {/* Please provide your email address if you would like to stay updated on the status of the #TransmascFutures Campaign. */} - + - + - + {/* Please enter your name as you would like it to appear on the campaign website. */} - } {...form.getInputProps('q4')} required /> + } + {...form.getInputProps("q4")} + required + /> {/* I identify as: */} . }} />} + {...form.getInputProps("q5")} + label={ + . }} + /> + } required > - {Object.entries(t('survey-form.q5-opts', { returnObjects: true })).map(([key, value]) => ( + {Object.entries( + t("survey-form.q5-opts", { returnObjects: true }), + ).map(([key, value]) => ( ))} {/* How old are you */} - + {/* Select pronouns */} - - {Object.entries(t('survey-form.q7-opts', { returnObjects: true })).map( - ([key, value], i) => ( - - ) - )} + + {Object.entries( + t("survey-form.q7-opts", { returnObjects: true }), + ).map(([key, value], i) => ( + + ))} - {form.values?.q7?.includes('other') && ( - + {form.values?.q7?.includes("other") && ( + )} {/* Describe the first moment you could see your future as a trans man or transmasculine person. */} -