diff --git a/.babelrc b/.babelrc deleted file mode 100644 index a5482f3..0000000 --- a/.babelrc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "presets": ["next/babel"], - "plugins": [ - "superjson-next" // 👈 - ] -} \ No newline at end of file diff --git a/.env.template b/.env.template index 6d810c9..ab59f5f 100644 --- a/.env.template +++ b/.env.template @@ -6,4 +6,5 @@ # Prisma supports the native connection string format for PostgreSQL, MySQL and SQLite. # See the documentation for all the connection string options: https://pris.ly/d/connection-strings -DATABASE_URL="postgresql://postgres:mycovidstory@0.0.0.0:5432/postgres" \ No newline at end of file +DATABASE_URL="postgresql://postgres:mycovidstory@0.0.0.0:5432/postgres" +NEXTAUTH_SECRET= # run `openssl rand -base64 32` to generate your secret diff --git a/.eslintrc.js b/.eslintrc.js index e77e9fe..5cae55e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -5,45 +5,30 @@ module.exports = { es6: true, }, parserOptions: { ecmaVersion: 8 }, // to enable features such as async/await - ignorePatterns: ['node_modules/*', '.next/*', '.out/*', '!.prettierrc.js'], // We don't want to lint generated files nor node_modules, but we want to lint .prettierrc.js (ignored by default by eslint) - extends: ['eslint:recommended'], + ignorePatterns: ['node_modules/*', '.next/*', '.out/*', '!.prettierrc.js'], + plugins: ['@typescript-eslint'], + extends: [ + 'next/core-web-vitals', + 'plugin:@typescript-eslint/recommended', + 'prettier', + ], overrides: [ // This configuration will apply only to TypeScript files { files: ['**/*.ts', '**/*.tsx'], - parser: '@typescript-eslint/parser', settings: { react: { version: 'detect' } }, env: { browser: true, node: true, es6: true, }, - extends: [ - 'eslint:recommended', - 'plugin:@typescript-eslint/recommended', // TypeScript rules - 'plugin:react/recommended', // React rules - 'plugin:react-hooks/recommended', // React hooks rules - 'plugin:jsx-a11y/recommended', // Accessibility rules - 'plugin:prettier/recommended', // Prettier plugin - ], rules: { // We will use TypeScript's types for component props instead 'react/prop-types': 'off', - - // No need to import React when using Next.js - 'react/react-in-jsx-scope': 'off', - - // This rule is not compatible with Next.js's components - 'jsx-a11y/anchor-is-valid': 'off', - - // Why would you want unused vars? - '@typescript-eslint/no-unused-vars': ['error'], - - // I suggest this setting for requiring return types on functions only where useful + '@typescript-eslint/no-unused-vars': 'error', + '@typescript-eslint/no-explicit-any': 'error', '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', - - 'prettier/prettier': ['error', { endOfLine: 'auto' }, { usePrettierrc: true }], // Includes .prettierrc.js rules }, }, ], diff --git a/.gitignore b/.gitignore index ef31a29..8bd4663 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,6 @@ prisma/dev.db # sitemap - these get built automatically public/sitemap.xml public/robots.txt + +# Sentry +.sentryclirc diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..ca4a60d --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v14.18.2 diff --git a/.prettierrc.js b/.prettierrc.js index ba74583..bdf21fe 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -2,9 +2,7 @@ module.exports = { // Change your rules accordingly to your coding style preferences. // https://prettier.io/docs/en/options.html semi: false, - trailingComma: 'es5', singleQuote: true, - printWidth: 100, tabWidth: 2, useTabs: false, } diff --git a/components/admin/AdminNav.tsx b/components/admin/AdminNav.tsx index 5f074f9..b832de5 100644 --- a/components/admin/AdminNav.tsx +++ b/components/admin/AdminNav.tsx @@ -13,7 +13,7 @@ import { } from '@chakra-ui/react' import { CloseIcon, HamburgerIcon } from '@chakra-ui/icons' -import { signIn, signOut, useSession } from 'next-auth/client' +import { signIn, signOut, useSession } from 'next-auth/react' import { RESPONSIVE_PADDING } from '../common/ContentBox' const Links = [ @@ -52,7 +52,8 @@ interface NavLinkProps extends LinkDisplayProps { } const NavLink = ({ alwaysDisplay = false, ...props }: NavLinkProps) => { - const [session, loading] = useSession() + const { data: session, status } = useSession() + const loading = status === 'loading' if (alwaysDisplay) { return @@ -72,7 +73,7 @@ const NavLink = ({ alwaysDisplay = false, ...props }: NavLinkProps) => { export default function AdminNav() { const { isOpen, onOpen, onClose } = useDisclosure() - const [session] = useSession() + const { data: session } = useSession() return ( <> @@ -87,14 +88,24 @@ export default function AdminNav() { : } + icon={ + isOpen ? ( + + ) : ( + + ) + } aria-label={'Open Menu'} display={{ lg: 'none' }} onClick={isOpen ? onClose : onOpen} /> - + {Links.map((link) => ( ))} @@ -105,7 +116,11 @@ export default function AdminNav() { alignItems={{ base: 'flex-end', sm: 'center' }} direction={{ base: 'column', sm: 'row' }} > - + {session?.user.email ?? ''} { +const updateStory = async ({ + id, + approved, + deleted, + contentWarning, +}: UpdateStoryProps) => { const res = await fetch('/api/admin/update', { method: 'PATCH', headers: { @@ -67,11 +72,19 @@ export default function StoryCard({ story, filteredView }: StoryCardProps) { const riding = ridings?.[0]?.riding const { isOpen, onOpen, onClose } = useDisclosure() - const [{ deleted, approved, contentWarning }, setCardStatus] = useState({ ...rest }) + const [{ deleted, approved, contentWarning }, setCardStatus] = useState({ + ...rest, + }) const [interacted, setInteracted] = useState(false) const handleDeleteInteraction = async (props) => { - if (window.confirm(`Are you sure you want to ${props.deleted ? 'undelete' : 'delete'} this?`)) { + if ( + window.confirm( + `Are you sure you want to ${ + props.deleted ? 'undelete' : 'delete' + } this?` + ) + ) { const { approved, deleted, ...rest } = await updateStory(props) setCardStatus({ approved, deleted, ...rest }) setInteracted(approved || deleted) @@ -92,7 +105,11 @@ export default function StoryCard({ story, filteredView }: StoryCardProps) { {content} - + ) diff --git a/components/common/FloatingRibbon.tsx b/components/common/FloatingRibbon.tsx index 03215eb..59b5c82 100644 --- a/components/common/FloatingRibbon.tsx +++ b/components/common/FloatingRibbon.tsx @@ -1,4 +1,9 @@ -import { Button as BaseButton, ButtonProps, Center, CenterProps } from '@chakra-ui/react' +import { + Button as BaseButton, + ButtonProps, + Center, + CenterProps, +} from '@chakra-ui/react' import { RESPONSIVE_PADDING } from './ContentBox' export default function FloatingRibbon({ children }: CenterProps) { diff --git a/components/common/Footer.tsx b/components/common/Footer.tsx index 269c98c..9f18005 100644 --- a/components/common/Footer.tsx +++ b/components/common/Footer.tsx @@ -13,7 +13,11 @@ export default function Footer() { textAlign="left" > - + Home @@ -47,7 +51,11 @@ export default function Footer() { > Analytics - + Email Us Made with love by Canadians{' '} - + ❤️ diff --git a/components/common/HeadTags.tsx b/components/common/HeadTags.tsx index 15fa3e0..f715cef 100644 --- a/components/common/HeadTags.tsx +++ b/components/common/HeadTags.tsx @@ -22,21 +22,44 @@ export default function HeadTags({ previewImage = defaultPreviewImage, }: HeadTagsProps) { const generatedTitle = generateTitle(title, TITLE_SUFFIX) - const generatedDescription = generateDescription(description, DESCRIPTION_LENGTH) + const generatedDescription = generateDescription( + description, + DESCRIPTION_LENGTH + ) const generatedPreviewImage = generatePreviewImageUrl(previewImage) return ( {generatedTitle} - + - - + + - - + + {children} ) diff --git a/components/common/Label.tsx b/components/common/Label.tsx index 54e4551..91e2e89 100644 --- a/components/common/Label.tsx +++ b/components/common/Label.tsx @@ -20,7 +20,9 @@ export default function Label({ opaque, children, ...props }: LabelProps) { const theme = useTheme() const color = (props.color as string) || (opaque ? 'primary.100' : 'white') - const background = opaque ? blendWhite(getColor(theme, color), 0.1) : undefined + const background = opaque + ? blendWhite(getColor(theme, color), 0.1) + : undefined return ( ( - + Menu @@ -56,7 +61,12 @@ const MenuDrawer = ({ isOpen, onClose }: MenuDrawerProps) => { const buttonRef = useRef(null) return ( - + { Home About Us FAQ - + Media @@ -105,15 +118,27 @@ const NavLinks = ({ menu = false }) => { mx={-1} fontSize={menu ? '2xl' : 'md'} > - + Twitter @MyCOVIDStory_CA - + Facebook @MyCovidStoryCA - + Instagram @mycovidstory_ca diff --git a/components/common/SimpleLink.tsx b/components/common/SimpleLink.tsx index ccdba9b..90a2449 100644 --- a/components/common/SimpleLink.tsx +++ b/components/common/SimpleLink.tsx @@ -16,9 +16,18 @@ interface SimpleLinkProps extends LinkProps { * @param props.children the content to be projected inside the link * @example About us */ -export default function SimpleLink({ href, asHref, undecorated, ...props }: SimpleLinkProps) { +export default function SimpleLink({ + href, + asHref, + undecorated, + ...props +}: SimpleLinkProps) { if (undecorated === true) { - props = { textDecoration: 'none', _hover: { textDecoration: 'none' }, ...props } + props = { + textDecoration: 'none', + _hover: { textDecoration: 'none' }, + ...props, + } } return ( diff --git a/components/stories/StoryDetail.tsx b/components/stories/StoryDetail.tsx index be10e6b..a950c76 100644 --- a/components/stories/StoryDetail.tsx +++ b/components/stories/StoryDetail.tsx @@ -25,14 +25,27 @@ interface StoryDetailProps { onShare: () => void } -export default function StoryDetail({ story, onClose, onShare }: StoryDetailProps) { +export default function StoryDetail({ + story, + onClose, + onShare, +}: StoryDetailProps) { return ( - + - won't be published. @@ -295,7 +333,11 @@ export default function StoryForm() { isInvalid={form.errors.email && form.touched.email} > Email - + {form.errors.email} )} @@ -333,8 +375,8 @@ export default function StoryForm() { Declaration and Consent - I confirm this story is true and I have consent to share it and any photo - submitted. + I confirm this story is true and I have consent to share it + and any photo submitted. {form.errors.consent} diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..802d897 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,10 @@ +const nextJest = require('next/jest') + +// Providing the path to your Next.js app which will enable loading next.config.js and .env files +const createJestConfig = nextJest() + +// Any custom config you want to pass to Jest +const customJestConfig = {} + +// createJestConfig is exported in this way to ensure that next/jest can load the Next.js configuration, which is async +module.exports = createJestConfig(customJestConfig) diff --git a/layouts/Admin.tsx b/layouts/Admin.tsx index 8fb09b8..7cabc35 100644 --- a/layouts/Admin.tsx +++ b/layouts/Admin.tsx @@ -13,7 +13,12 @@ export default function AdminLayout({ children }: SiteLayoutProps) { return (