diff --git a/next.config.mjs b/next.config.mjs index 3960dfb1..8594af34 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,25 +22,21 @@ 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. * @@ -48,7 +44,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 1dcf2b19..0e5506c7 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 d52c446c..fcbc0b49 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -1,75 +1,70 @@ /* 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 cef93db9..9eaf9473 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 5e42e899..34b6e1b7 100644 --- a/src/components/Navbar/Navbar.tsx +++ b/src/components/Navbar/Navbar.tsx @@ -1,19 +1,10 @@ -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: { @@ -21,102 +12,92 @@ 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 = () => ( // @@ -128,30 +109,27 @@ const NavLinks = ({ // 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, @@ -163,34 +141,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 (
@@ -198,5 +176,5 @@ export const Navbar = ({ path }: pathProp) => {
- ); -}; + ) +} diff --git a/src/layouts/ArtItem.tsx b/src/layouts/ArtItem.tsx index 26ed6901..d3d4cb49 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,100 +9,91 @@ 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)`, }, })} @@ -135,9 +126,9 @@ export const ArtItem = ({ src={src} alt={alt} style={{ - objectFit: "contain", - maxWidth: "90%", - margin: "0 auto", + objectFit: 'contain', + maxWidth: '90%', + margin: '0 auto', }} /> @@ -147,17 +138,17 @@ export const ArtItem = ({ ({ 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)`, }, })} @@ -169,37 +160,31 @@ export const ArtItem = ({ - + <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 a403a3d5..4eeddbc3 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -1,79 +1,50 @@ -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')} @@ -122,7 +90,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 376153a7..d2482d96 100644 --- a/src/pages/category/[tag]/[[...storyId]].tsx +++ b/src/pages/category/[tag]/[[...storyId]].tsx @@ -1,99 +1,82 @@ -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')} @@ -101,8 +84,8 @@ export const CategoryPage = ({}: CategoryPageProps) => { fw={100} order={1} fz={30} - align="center" - tt="uppercase" + align='center' + tt='uppercase' style={fontIbmPlexSans.style} lts={5} > @@ -112,19 +95,17 @@ 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 && ( @@ -160,49 +141,42 @@ 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, @@ -211,7 +185,7 @@ export const getStaticPaths: GetStaticPaths<{ where: { story: { published: true } }, }, }, - }); + }) const paths = categories.flatMap(({ tag, stories }) => [ ...locales.map((locale) => ({ @@ -222,14 +196,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 dedd0e17..c177456f 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,34 +105,28 @@ 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)`, }, })} @@ -158,8 +152,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)`, @@ -174,35 +168,33 @@ 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')} @@ -210,38 +202,33 @@ 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) => ( @@ -254,16 +241,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 @@ -282,22 +269,20 @@ 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 ed08d6b5..58f12a38 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,169 +41,137 @@ 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 21440f27..ba0213e3 100644 --- a/src/pages/story/[id].tsx +++ b/src/pages/story/[id].tsx @@ -1,33 +1,28 @@ -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, @@ -35,57 +30,49 @@ 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< - 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 }; +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 } 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 850a2cba..1f4e08c6 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,143 +76,130 @@ 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( () => @@ -220,13 +207,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: ., @@ -259,198 +246,133 @@ 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. */} -