diff --git a/.changeset/tender-oranges-matter.md b/.changeset/tender-oranges-matter.md new file mode 100644 index 00000000..c10a77f3 --- /dev/null +++ b/.changeset/tender-oranges-matter.md @@ -0,0 +1,7 @@ +--- +'@snipcode/front': minor +'@snipcode/web': minor +'@snipcode/backend': patch +--- + +migrate to next.js app router diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index d581bb05..ad1ce0cd 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -34,13 +34,15 @@ jobs: run: yarn lint - name: Build the projects + env: + NEXT_PUBLIC_APP_URL: http://localhost:7500 # Required for the frontend build run: yarn build - name: Start MySQL server run: sudo systemctl start mysql.service - name: Run tests - run: yarn test -- --runInBand + run: yarn test version: runs-on: ubuntu-latest diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 112a4ba7..3d5a1fb0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -32,10 +32,12 @@ jobs: run: yarn lint - name: Build the projects + env: + NEXT_PUBLIC_APP_URL: http://localhost:7500 # Required for the frontend build run: yarn build - name: Run tests - run: yarn test -- --runInBand --coverage + run: yarn test -- --coverage should-preview-frontend: runs-on: ubuntu-latest diff --git a/apps/backend/Dockerfile b/apps/backend/Dockerfile index fe7f251c..34c0b1e6 100644 --- a/apps/backend/Dockerfile +++ b/apps/backend/Dockerfile @@ -52,9 +52,6 @@ COPY --chown=node:node --from=builder /app/packages/utils/dist ./packages/utils/ COPY --chown=node:node --from=builder /app/packages/embed/package.json ./packages/embed/package.json COPY --chown=node:node --from=builder /app/packages/embed/dist ./packages/embed/dist -COPY --chown=node:node --from=builder /app/packages/logger/package.json ./packages/logger/package.json -COPY --chown=node:node --from=builder /app/packages/logger/dist ./packages/logger/dist - RUN yarn workspaces focus --all --production && yarn cache clean --all COPY --chown=node:node --from=schema-builder /app/node_modules/.prisma/client ./node_modules/.prisma/client diff --git a/apps/web/.env.test b/apps/web/.env.test new file mode 100644 index 00000000..85f72bdd --- /dev/null +++ b/apps/web/.env.test @@ -0,0 +1,10 @@ +NEXT_PUBLIC_APP_ENV=development +NEXT_PUBLIC_GITHUB_CLIENT_ID=github-client-id +NEXT_PUBLIC_SERVER_URL=http://localhost:7501/graphql +NEXT_PUBLIC_APP_URL=http://localhost:7500 +NEXT_PUBLIC_SENTRY_DSN=https://dsn@o1288567.ingest.us.sentry.io/1234567 +NEXT_PUBLIC_SENTRY_ENABLED=false +SENTRY_AUTH_TOKEN= +SENTRY_RELEASE= +SHAREABLE_HOST=http://localhost:7500 +EMBEDDABLE_HOST=http://localhost:7502 diff --git a/apps/web/.eslintrc.js b/apps/web/.eslintrc.js index 1a433248..0a96bd95 100644 --- a/apps/web/.eslintrc.js +++ b/apps/web/.eslintrc.js @@ -8,16 +8,15 @@ module.exports = { }, }, ignorePatterns: [ - 'jest.config.ts', - '__mocks__', + 'mocks', 'next.config.js', 'tailwind.config.js', 'postcss.config.js', - 'next-sitemap.js', 'sentry.client.config.js', 'sentry.server.config.js', 'sentry.edge.config.js', '.eslintrc.js', + 'vitest.config.ts', ], parserOptions: { ecmaVersion: 2023, diff --git a/apps/web/env.d.ts b/apps/web/env.d.ts index 8888b90b..9f565c1e 100644 --- a/apps/web/env.d.ts +++ b/apps/web/env.d.ts @@ -9,7 +9,8 @@ export type EnvironmentVariables = { declare global { namespace NodeJS { - type ProcessEnv = EnvironmentVariables; + // eslint-disable-next-line @typescript-eslint/consistent-type-definitions + interface ProcessEnv extends EnvironmentVariables {} } } diff --git a/apps/web/jest.config.ts b/apps/web/jest.config.ts deleted file mode 100644 index f216048b..00000000 --- a/apps/web/jest.config.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { Config } from '@jest/types'; - -const config: Config.InitialOptions = { - collectCoverageFrom: ['**/*.{js,jsx,ts,tsx}', '!**/*.d.ts', '!**/node_modules/**'], - moduleNameMapper: { - '\\.(css|less)$': '/__mocks__/styleMock.js', - '^.+\\.(jpg|jpeg|png|gif|webp|avif|svg)$': '/__mocks__/fileMock.js', - '^@/components/(.*)$': '/src/components/$1', - '^@/containers/(.*)$': '/src/containers/$1', - '^@/hooks/(.*)$': '/src/hooks/$1', - '^@/styles/(.*)$': '/src/styles/$1', - '^@/utils/(.*)$': '/src/utils/$1', - }, - setupFilesAfterEnv: ['/__tests__/setup/jest.setup.ts'], - testEnvironment: 'jsdom', - testMatch: ['/__tests__/ui/**/*.(ts|tsx)'], - testPathIgnorePatterns: ['./.next/', './node_modules/'], - transform: { - '^.+\\.(js|jsx|ts|tsx)$': ['babel-jest', { presets: ['next/babel'] }], - }, - transformIgnorePatterns: ['/node_modules/(?!(uuid|@hookform/resolvers))'], -}; - -export default config; diff --git a/apps/web/next-sitemap.js b/apps/web/next-sitemap.js deleted file mode 100644 index 88913f9a..00000000 --- a/apps/web/next-sitemap.js +++ /dev/null @@ -1,14 +0,0 @@ -const SITE_URL = process.env.NEXT_PUBLIC_APP_URL; - -module.exports = { - siteUrl: SITE_URL, - generateRobotsTxt: true, - robotsTxtOptions: { - policies: [ - // { userAgent: '*', disallow: '/login' }, - { userAgent: '*', allow: '/' }, - ], - additionalSitemaps: [`${SITE_URL}/sitemap.xml`], - }, - exclude: ['/login', '/home', '/profile', '/browse'], -}; diff --git a/apps/web/next.config.js b/apps/web/next.config.js index 3869a5e7..36e77619 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -15,6 +15,6 @@ module.exports = withSentryConfig(nextConfigOptions, { project: 'frontend', // release: "my-project-name@2.3.12", authToken: process.env.SENTRY_AUTH_TOKEN, // An auth token is required for uploading source maps. - silent: false, + silent: true, telemetry: false, }); diff --git a/apps/web/package.json b/apps/web/package.json index 80e3e215..8c3f1d0b 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -8,11 +8,11 @@ "dev": "next dev --port 7500", "lint": "next lint", "start": "next start", - "test": "jest", - "postbuild": "next-sitemap --config next-sitemap.js" + "test": "dotenv -e .env.test -- vitest" }, "dependencies": { "@apollo/client": "3.10.6", + "@apollo/experimental-nextjs-app-support": "0.11.2", "@headlessui/react": "2.1.0", "@hookform/resolvers": "3.6.0", "@sentry/nextjs": "8.11.0", @@ -36,14 +36,17 @@ "@testing-library/user-event": "14.5.2", "@types/react": "18.3.3", "@types/react-dom": "18.3.0", + "@vitejs/plugin-react": "4.3.1", + "@vitest/coverage-v8": "2.0.2", "autoprefixer": "10.4.19", "eslint-config-next": "14.2.4", "eslint-import-resolver-typescript": "3.6.1", "eslint-plugin-jest-dom": "5.4.0", "eslint-plugin-testing-library": "6.2.2", - "next-router-mock": "0.9.13", - "next-sitemap": "4.2.3", + "jsdom": "24.1.0", "postcss": "8.4.38", - "tailwindcss": "3.4.4" + "tailwindcss": "3.4.4", + "vite-tsconfig-paths": "4.3.2", + "vitest": "2.0.2" } } diff --git a/apps/web/public/android-chrome-192x192.png b/apps/web/public/android-chrome-192x192.png new file mode 100644 index 00000000..cbbf9836 Binary files /dev/null and b/apps/web/public/android-chrome-192x192.png differ diff --git a/apps/web/public/android-chrome-512x512.png b/apps/web/public/android-chrome-512x512.png new file mode 100644 index 00000000..a14f2c00 Binary files /dev/null and b/apps/web/public/android-chrome-512x512.png differ diff --git a/apps/web/public/apple-touch-icon.png b/apps/web/public/apple-touch-icon.png new file mode 100644 index 00000000..c630c553 Binary files /dev/null and b/apps/web/public/apple-touch-icon.png differ diff --git a/apps/web/public/favicon-16x16.png b/apps/web/public/favicon-16x16.png new file mode 100644 index 00000000..81c2bb8d Binary files /dev/null and b/apps/web/public/favicon-16x16.png differ diff --git a/apps/web/public/favicon-32x32.png b/apps/web/public/favicon-32x32.png new file mode 100644 index 00000000..0b25a1e6 Binary files /dev/null and b/apps/web/public/favicon-32x32.png differ diff --git a/apps/web/public/favicon.ico b/apps/web/public/favicon.ico index d0947c99..2c6859a3 100644 Binary files a/apps/web/public/favicon.ico and b/apps/web/public/favicon.ico differ diff --git a/apps/web/src/containers/private/browse.tsx b/apps/web/src/app/(protected)/app/browse/container.tsx similarity index 54% rename from apps/web/src/containers/private/browse.tsx rename to apps/web/src/app/(protected)/app/browse/container.tsx index 2b385d33..72488b7a 100644 --- a/apps/web/src/containers/private/browse.tsx +++ b/apps/web/src/app/(protected)/app/browse/container.tsx @@ -1,13 +1,13 @@ +'use client'; + import { Button } from '@snipcode/front/forms/button'; import { SelectInput } from '@snipcode/front/forms/select-input'; import { ChevronDoubleLeftIcon, ChevronDoubleRightIcon, SearchIcon } from '@snipcode/front/icons'; import { usePublicSnippets } from '@snipcode/front/services'; import { SelectOption } from '@snipcode/front/typings/components'; import { PublicSnippetItem, PublicSnippetResult } from '@snipcode/front/typings/queries'; -import { NextSeo } from 'next-seo'; import { useState } from 'react'; -import { Layout } from '@/components/layout/private/layout'; import { PublicSnippet } from '@/components/snippets/public-snippet'; import { usePaginationToken } from '@/hooks/use-pagination-token'; @@ -22,7 +22,7 @@ const sortOptions: SelectOption[] = [ { id: 'recently_updated', label: 'Sort: recently updated' }, ]; -const Browse = ({ data }: Props) => { +export const BrowseContainer = ({ data }: Props) => { const [snippetList, setSnippetList] = useState(data.items); const [sortOption, setSortOption] = useState(sortOptions[0]); const [search, setSearch] = useState(); @@ -93,64 +93,59 @@ const Browse = ({ data }: Props) => { }; return ( - - -
-
-
-
-
- onSearchChange(e.target.value)} - /> -
- -
-
- +
+
+
+
+ onSearchChange(e.target.value)} /> -
-
-
- {snippetList.map((snippet) => ( - - ))} -
-
- - +
+
+
-
-
- +
+
+ {snippetList.map((snippet) => ( + + ))} +
+
+ + +
+
+
+
+
); }; - -export { Browse }; diff --git a/apps/web/src/app/(protected)/app/browse/lib/fetch-snippets.ts b/apps/web/src/app/(protected)/app/browse/lib/fetch-snippets.ts new file mode 100644 index 00000000..26b94218 --- /dev/null +++ b/apps/web/src/app/(protected)/app/browse/lib/fetch-snippets.ts @@ -0,0 +1,31 @@ +import { findPublicSnippetsQuery } from '@snipcode/front/graphql'; +import { PublicSnippetsQuery } from '@snipcode/front/graphql/generated'; +import { formatPublicSnippetsResult } from '@snipcode/front/services'; +import { SNIPPET_ITEM_PER_PAGE } from '@snipcode/front/utils/constants'; +import { cookies } from 'next/headers'; + +import { getApolloClient } from '@/lib/apollo/server'; +import { AUTH_COOKIE_NAME } from '@/lib/constants'; +import { logErrorToSentry } from '@/lib/errors'; + +export const fetchSnippets = async () => { + try { + const authToken = cookies().get(AUTH_COOKIE_NAME); + + const { data } = await getApolloClient().query({ + context: { + headers: { + Authorization: authToken?.value, + }, + }, + query: findPublicSnippetsQuery, + variables: { input: { itemPerPage: SNIPPET_ITEM_PER_PAGE } }, + }); + + return formatPublicSnippetsResult(data); + } catch (error: unknown) { + logErrorToSentry(error); + + throw new Error('Failed to fetch public snippets'); + } +}; diff --git a/apps/web/src/app/(protected)/app/browse/page.tsx b/apps/web/src/app/(protected)/app/browse/page.tsx new file mode 100644 index 00000000..0edac048 --- /dev/null +++ b/apps/web/src/app/(protected)/app/browse/page.tsx @@ -0,0 +1,22 @@ +import { generatePageMetadata } from '@/lib/seo'; + +import { BrowseContainer } from './container'; +import { fetchSnippets } from './lib/fetch-snippets'; + +export const dynamic = 'force-dynamic'; + +export const metadata = generatePageMetadata({ + title: 'Snipcode - Browse code snippets', +}); + +const BrowsePage = async () => { + const data = await fetchSnippets(); + + if (!data) { + throw new Error('Failed to fetch public snippets'); + } + + return ; +}; + +export default BrowsePage; diff --git a/apps/web/src/app/(protected)/app/folders/[id]/container.tsx b/apps/web/src/app/(protected)/app/folders/[id]/container.tsx new file mode 100644 index 00000000..6db305d9 --- /dev/null +++ b/apps/web/src/app/(protected)/app/folders/[id]/container.tsx @@ -0,0 +1,33 @@ +'use client'; + +import { Directory } from '@snipcode/front/components/directory'; +import { useFindFolder } from '@snipcode/front/services'; +import { useParams } from 'next/navigation'; + +import { useFolderDirectory } from '@/hooks/use-folder-directory'; + +export const ViewFolderContainer = () => { + const queryParams = useParams<{ id: string }>(); + const { handleBreadcrumbClick, navigateToFolder, openSnippet, rootFolderId } = useFolderDirectory(); + + const folderId = queryParams.id; + + const { data, isLoading } = useFindFolder(folderId); + + const isFolderFound = !isLoading && Boolean(data); + + return ( +
+ {isFolderFound && ( + + )} +
+ ); +}; diff --git a/apps/web/src/app/(protected)/app/folders/[id]/page.tsx b/apps/web/src/app/(protected)/app/folders/[id]/page.tsx new file mode 100644 index 00000000..dc0c43e7 --- /dev/null +++ b/apps/web/src/app/(protected)/app/folders/[id]/page.tsx @@ -0,0 +1,14 @@ +import { generatePageMetadata } from '@/lib/seo'; + +import { ViewFolderContainer } from './container'; + +export const metadata = generatePageMetadata({ + noIndex: true, + title: 'Snipcode - Folder', // TODO see how to make this dynamic +}); + +const ViewFolderPage = () => { + return ; +}; + +export default ViewFolderPage; diff --git a/apps/web/src/app/(protected)/app/home/container.tsx b/apps/web/src/app/(protected)/app/home/container.tsx new file mode 100644 index 00000000..02b32cbe --- /dev/null +++ b/apps/web/src/app/(protected)/app/home/container.tsx @@ -0,0 +1,24 @@ +'use client'; + +import { Directory } from '@snipcode/front/components/directory'; +import { useAuthenticatedUser } from '@snipcode/front/services'; + +import { useFolderDirectory } from '@/hooks/use-folder-directory'; + +export const HomeContainer = () => { + const { data: user } = useAuthenticatedUser(); + const { handleBreadcrumbClick, navigateToFolder, openSnippet, rootFolderId } = useFolderDirectory(); + + return ( +
+ +
+ ); +}; diff --git a/apps/web/src/app/(protected)/app/home/page.tsx b/apps/web/src/app/(protected)/app/home/page.tsx new file mode 100644 index 00000000..14917cfb --- /dev/null +++ b/apps/web/src/app/(protected)/app/home/page.tsx @@ -0,0 +1,14 @@ +import { generatePageMetadata } from '@/lib/seo'; + +import { HomeContainer } from './container'; + +export const metadata = generatePageMetadata({ + noIndex: true, + title: 'Home', +}); + +const PrivateHomePage = () => { + return ; +}; + +export default PrivateHomePage; diff --git a/apps/web/src/app/(protected)/app/profile/container.tsx b/apps/web/src/app/(protected)/app/profile/container.tsx new file mode 100644 index 00000000..8f521a64 --- /dev/null +++ b/apps/web/src/app/(protected)/app/profile/container.tsx @@ -0,0 +1,22 @@ +'use client'; + +export const ProfileContainer = () => { + return ( +
+
+
+

Profile

+
+
+
+
+ {/* Replace with your content */} +
+
+
+ {/* /End replace */} +
+
+
+ ); +}; diff --git a/apps/web/src/app/(protected)/app/profile/page.tsx b/apps/web/src/app/(protected)/app/profile/page.tsx new file mode 100644 index 00000000..57a98bc7 --- /dev/null +++ b/apps/web/src/app/(protected)/app/profile/page.tsx @@ -0,0 +1,14 @@ +import { generatePageMetadata } from '@/lib/seo'; + +import { ProfileContainer } from './container'; + +export const metadata = generatePageMetadata({ + noIndex: true, + title: 'Profile', +}); + +const ProfilePage = () => { + return ; +}; + +export default ProfilePage; diff --git a/apps/web/src/app/(protected)/app/snippets/[id]/container.tsx b/apps/web/src/app/(protected)/app/snippets/[id]/container.tsx new file mode 100644 index 00000000..83514d30 --- /dev/null +++ b/apps/web/src/app/(protected)/app/snippets/[id]/container.tsx @@ -0,0 +1,37 @@ +'use client'; + +import { BreadCrumb } from '@snipcode/front/components/directory/breadcrumb'; +import { ViewSnippet } from '@snipcode/front/components/directory/snippets/form/view-snippet'; +import { useFindSnippet } from '@snipcode/front/services'; +import { useParams } from 'next/navigation'; + +import { useFolderDirectory } from '@/hooks/use-folder-directory'; + +export const ViewSnippetContainer = () => { + const queryParams = useParams<{ id: string }>(); + const { handleBreadcrumbClick, rootFolderId } = useFolderDirectory(); + + const snippetId = queryParams.id; + + const { data, isLoading } = useFindSnippet(snippetId); + + const isSnippetFound = !isLoading && data; + + return ( +
+ {isSnippetFound && ( +
+ +
+ +
+
+ )} +
+ ); +}; diff --git a/apps/web/src/app/(protected)/app/snippets/[id]/page.tsx b/apps/web/src/app/(protected)/app/snippets/[id]/page.tsx new file mode 100644 index 00000000..a9a43407 --- /dev/null +++ b/apps/web/src/app/(protected)/app/snippets/[id]/page.tsx @@ -0,0 +1,14 @@ +import { generatePageMetadata } from '@/lib/seo'; + +import { ViewSnippetContainer } from './container'; + +export const metadata = generatePageMetadata({ + noIndex: true, + title: 'Snipcode - Snippet', // TODO see how to make this dynamic +}); + +const ViewSnippetPage = () => { + return ; +}; + +export default ViewSnippetPage; diff --git a/apps/web/src/app/(protected)/error.tsx b/apps/web/src/app/(protected)/error.tsx new file mode 100644 index 00000000..44bd7672 --- /dev/null +++ b/apps/web/src/app/(protected)/error.tsx @@ -0,0 +1,12 @@ +'use client'; + +import { ErrorContent } from '@/components/common/error'; + +type Props = { + error: Error & { digest?: string }; + reset: () => void; +}; + +export default function Error({ error, reset }: Props) { + return ; +} diff --git a/apps/web/src/app/(protected)/layout.tsx b/apps/web/src/app/(protected)/layout.tsx new file mode 100644 index 00000000..dfaa0efe --- /dev/null +++ b/apps/web/src/app/(protected)/layout.tsx @@ -0,0 +1,31 @@ +import { ToastProvider } from '@snipcode/front/components/toast/provider'; +import React, { PropsWithChildren } from 'react'; + +import { ApolloWrapper } from '@/lib/apollo/client'; +import { generatePageMetadata } from '@/lib/seo'; + +import { AuthenticatedLayout } from './layout/content'; + +import '@/styles/globals.css'; + +export const metadata = generatePageMetadata({ + noIndex: true, +}); + +const AppLayout = ({ children }: PropsWithChildren) => { + return ( + + +
+ + + {children} + + +
+ + + ); +}; + +export default AppLayout; diff --git a/apps/web/src/components/layout/private/layout.tsx b/apps/web/src/app/(protected)/layout/content.tsx similarity index 68% rename from apps/web/src/components/layout/private/layout.tsx rename to apps/web/src/app/(protected)/layout/content.tsx index 1474b9b6..c73c86ea 100644 --- a/apps/web/src/components/layout/private/layout.tsx +++ b/apps/web/src/app/(protected)/layout/content.tsx @@ -1,16 +1,18 @@ -import { ToastProvider } from '@snipcode/front/components/toast/provider'; +'use client'; + import { useAuthenticatedUser } from '@snipcode/front/services'; import { ReactNode } from 'react'; import { Loader } from '@/components/common/loader'; import { Redirect } from '@/components/common/redirect'; -import { Header } from '@/components/layout/private/header'; + +import { Header } from './header'; type Props = { children?: ReactNode; }; -const Layout = ({ children }: Props) => { +export const AuthenticatedLayout = ({ children }: Props) => { const { data, isLoading } = useAuthenticatedUser(); if (isLoading && !data) { @@ -27,12 +29,8 @@ const Layout = ({ children }: Props) => { return (
- -
- {children} - +
+ {children}
); }; - -export { Layout }; diff --git a/apps/web/src/components/layout/private/header.tsx b/apps/web/src/app/(protected)/layout/header.tsx similarity index 97% rename from apps/web/src/components/layout/private/header.tsx rename to apps/web/src/app/(protected)/layout/header.tsx index f0a99b93..f2c0f808 100644 --- a/apps/web/src/components/layout/private/header.tsx +++ b/apps/web/src/app/(protected)/layout/header.tsx @@ -1,10 +1,12 @@ +'use client'; + import { Disclosure, Menu, Transition } from '@snipcode/front'; import { Link } from '@snipcode/front/components/link'; import { UserAvatar } from '@snipcode/front/components/user-avatar'; import { LogoIcon, LogoLightIcon, MenuIcon, XIcon } from '@snipcode/front/icons'; import { useLogoutUser } from '@snipcode/front/services'; import { classNames } from '@snipcode/front/utils/classnames'; -import { useRouter } from 'next/router'; +import { usePathname } from 'next/navigation'; import { Fragment } from 'react'; import { useAuth } from '@/hooks/authentication/use-auth'; @@ -20,16 +22,16 @@ const isActive = (appPath: string, linkPath: string) => { return appPath.startsWith(linkPath); }; -const Header = () => { +export const Header = () => { const [logoutUserMutation] = useLogoutUser(); const { deleteToken, redirectToHome, user } = useAuth(); - const { pathname } = useRouter(); + const pathname = usePathname(); const logout = async () => { await logoutUserMutation({ onCompleted: async () => { await deleteToken(); - await redirectToHome(); + redirectToHome(); }, }); }; @@ -163,5 +165,3 @@ const Header = () => { ); }; - -export { Header }; diff --git a/apps/web/src/app/(protected)/not-found.tsx b/apps/web/src/app/(protected)/not-found.tsx new file mode 100644 index 00000000..438a8005 --- /dev/null +++ b/apps/web/src/app/(protected)/not-found.tsx @@ -0,0 +1,13 @@ +import { PageNotFound } from '@/components/common/page-not-found'; +import { generatePageMetadata } from '@/lib/seo'; + +export const metadata = generatePageMetadata({ + noIndex: true, + title: 'Page not found', +}); + +const NotFoundPage = () => { + return ; +}; + +export default NotFoundPage; diff --git a/apps/web/src/app/(public)/[...not-found]/page.tsx b/apps/web/src/app/(public)/[...not-found]/page.tsx new file mode 100644 index 00000000..b9ef157d --- /dev/null +++ b/apps/web/src/app/(public)/[...not-found]/page.tsx @@ -0,0 +1,8 @@ +import { notFound } from 'next/navigation'; + +// Workaround for handling not found route when using multiples root layouts: https://github.com/vercel/next.js/discussions/50034 +const NotFoundDummyPage = () => { + notFound(); +}; + +export default NotFoundDummyPage; diff --git a/apps/web/src/app/(public)/auth/fail/page.tsx b/apps/web/src/app/(public)/auth/fail/page.tsx new file mode 100644 index 00000000..3ce092f0 --- /dev/null +++ b/apps/web/src/app/(public)/auth/fail/page.tsx @@ -0,0 +1,31 @@ +import { AuthAlert } from '@/components/auth/auth-alert'; +import { generatePageMetadata } from '@/lib/seo'; + +export const metadata = generatePageMetadata({ + description: 'An error occurred while authenticating', + noIndex: true, + title: 'Snipcode - Authentication failed', +}); + +const AuthErrorPage = () => { + return ( + + An error occurred while authenticating +
+ Please contact the{' '} + + support + {' '} + if the error persist. + + } + redirectLink="/" + title="Authentication failed ❌" + /> + ); +}; + +export default AuthErrorPage; diff --git a/apps/web/src/app/(public)/auth/signup-success/page.tsx b/apps/web/src/app/(public)/auth/signup-success/page.tsx new file mode 100644 index 00000000..656a418e --- /dev/null +++ b/apps/web/src/app/(public)/auth/signup-success/page.tsx @@ -0,0 +1,27 @@ +import { AuthAlert } from '@/components/auth/auth-alert'; +import { generatePageMetadata } from '@/lib/seo'; + +export const metadata = generatePageMetadata({ + description: 'Your Snipcode account have been created successfully', + noIndex: true, + title: 'Snipcode - Sign up success', +}); + +const SignupSuccessPage = () => { + return ( + + We sent a confirmation link to your email address. +
+ Please, click on the link to activate your account and star managing your code snippets. + + } + redirectLink="/signin" + title="Your account have been created successfully 🎉" + /> + ); +}; + +export default SignupSuccessPage; diff --git a/apps/web/src/app/(public)/auth/success/container.tsx b/apps/web/src/app/(public)/auth/success/container.tsx new file mode 100644 index 00000000..961d7a0f --- /dev/null +++ b/apps/web/src/app/(public)/auth/success/container.tsx @@ -0,0 +1,23 @@ +'use client'; + +import { AuthAlert } from '@/components/auth/auth-alert'; +import { useSetAuthenticatedUser } from '@/hooks/authentication/use-set-authenticated-user'; + +export const AuthSuccessContainer = () => { + useSetAuthenticatedUser(); + + return ( + + We are applying some latest configuration +
+ You will be redirected in few seconds. + + } + redirectLink="/board" + title="Authenticated successfully 🎉" + /> + ); +}; diff --git a/apps/web/src/app/(public)/auth/success/page.tsx b/apps/web/src/app/(public)/auth/success/page.tsx new file mode 100644 index 00000000..dca66b24 --- /dev/null +++ b/apps/web/src/app/(public)/auth/success/page.tsx @@ -0,0 +1,24 @@ +import { Suspense } from 'react'; + +import { ApolloWrapper } from '@/lib/apollo/client'; +import { generatePageMetadata } from '@/lib/seo'; + +import { AuthSuccessContainer } from './container'; + +export const metadata = generatePageMetadata({ + description: 'You have been authenticated successfully.', + noIndex: true, + title: 'Authentication succeeded', +}); + +const AuthSuccessPage = () => { + return ( + + + + + + ); +}; + +export default AuthSuccessPage; diff --git a/apps/web/src/app/(public)/error.tsx b/apps/web/src/app/(public)/error.tsx new file mode 100644 index 00000000..44bd7672 --- /dev/null +++ b/apps/web/src/app/(public)/error.tsx @@ -0,0 +1,12 @@ +'use client'; + +import { ErrorContent } from '@/components/common/error'; + +type Props = { + error: Error & { digest?: string }; + reset: () => void; +}; + +export default function Error({ error, reset }: Props) { + return ; +} diff --git a/apps/web/src/app/(public)/layout.tsx b/apps/web/src/app/(public)/layout.tsx new file mode 100644 index 00000000..01293878 --- /dev/null +++ b/apps/web/src/app/(public)/layout.tsx @@ -0,0 +1,29 @@ +import React, { PropsWithChildren } from 'react'; + +import { ApolloWrapper } from '@/lib/apollo/client'; +import { generatePageMetadata } from '@/lib/seo'; + +import { PublicFooter } from './layout/footer'; +import { PublicHeader } from './layout/header'; + +import '@/styles/globals.css'; + +export const metadata = generatePageMetadata(); + +const RootLayout = ({ children }: PropsWithChildren) => { + return ( + + +
+ + + + {children} + +
+ + + ); +}; + +export default RootLayout; diff --git a/apps/web/src/components/layout/public/footer.tsx b/apps/web/src/app/(public)/layout/footer.tsx similarity index 100% rename from apps/web/src/components/layout/public/footer.tsx rename to apps/web/src/app/(public)/layout/footer.tsx diff --git a/apps/web/src/components/layout/public/header.tsx b/apps/web/src/app/(public)/layout/header.tsx similarity index 98% rename from apps/web/src/components/layout/public/header.tsx rename to apps/web/src/app/(public)/layout/header.tsx index e94bb55e..44dd5a2f 100644 --- a/apps/web/src/components/layout/public/header.tsx +++ b/apps/web/src/app/(public)/layout/header.tsx @@ -1,9 +1,12 @@ +'use client'; + import { LogoIcon } from '@snipcode/front/icons'; import Link from 'next/link'; import { MouseEvent, useState } from 'react'; import { useAuth } from '@/hooks/authentication/use-auth'; +// TODO - Refactor to server component const PublicHeader = () => { const [isExpanded, setIsExpanded] = useState(false); const { redirectToDashboard, redirectToSignin, redirectToSignup, user } = useAuth(); @@ -12,7 +15,7 @@ const PublicHeader = () => { e.preventDefault(); if (user?.email) { - await redirectToDashboard(); + redirectToDashboard(); return; } @@ -24,7 +27,7 @@ const PublicHeader = () => { e.preventDefault(); if (user?.email) { - await redirectToDashboard(); + redirectToDashboard(); return; } diff --git a/apps/web/src/app/(public)/not-found.tsx b/apps/web/src/app/(public)/not-found.tsx new file mode 100644 index 00000000..438a8005 --- /dev/null +++ b/apps/web/src/app/(public)/not-found.tsx @@ -0,0 +1,13 @@ +import { PageNotFound } from '@/components/common/page-not-found'; +import { generatePageMetadata } from '@/lib/seo'; + +export const metadata = generatePageMetadata({ + noIndex: true, + title: 'Page not found', +}); + +const NotFoundPage = () => { + return ; +}; + +export default NotFoundPage; diff --git a/apps/web/__tests__/ui/home.test.tsx b/apps/web/src/app/(public)/page.test.tsx similarity index 66% rename from apps/web/__tests__/ui/home.test.tsx rename to apps/web/src/app/(public)/page.test.tsx index ca77bc85..f78a7727 100644 --- a/apps/web/__tests__/ui/home.test.tsx +++ b/apps/web/src/app/(public)/page.test.tsx @@ -3,7 +3,7 @@ import { authenticatedUserQuery } from '@snipcode/front/graphql/users/queries/au import { render, screen } from '@testing-library/react'; import React from 'react'; -import Home from '@/containers/home'; +import Home from './page'; const mocks = [ { @@ -19,16 +19,22 @@ const mocks = [ }, ]; -jest.mock('next/router', () => require('next-router-mock')); - describe('Home Page', () => { test('Render the home page', () => { render( - - - , + ( + + {children} + + )} + />, ); + const text = screen.getByText(/Made for developers and content creators/i); + + expect(text).toBeInTheDocument(); + const button = screen.getByRole('button', { name: /Request Early Access/i, }); diff --git a/apps/web/src/app/(public)/page.tsx b/apps/web/src/app/(public)/page.tsx new file mode 100644 index 00000000..04e1dffa --- /dev/null +++ b/apps/web/src/app/(public)/page.tsx @@ -0,0 +1,36 @@ +import React from 'react'; + +import { FeatureSection } from '@/components/home/feature-section'; +import { HeroSection } from '@/components/home/hero-section'; +import { NewsletterForm } from '@/components/home/newsletter/newsletter-form'; +import { NewsletterSection } from '@/components/home/newsletter/newsletter-section'; +import { ApolloWrapper } from '@/lib/apollo/client'; +import { generatePageMetadata } from '@/lib/seo'; + +export const metadata = generatePageMetadata(); + +type PageProps = { + apolloWrapperElement?: (children: React.ReactNode) => React.ReactNode; +}; + +const HomePage = ({ apolloWrapperElement }: PageProps) => { + return ( + <> + + + ) + ) : ( + + + + ) + } + /> + + ); +}; + +export default HomePage; diff --git a/apps/web/src/app/(public)/signin/container.tsx b/apps/web/src/app/(public)/signin/container.tsx new file mode 100644 index 00000000..ac510e77 --- /dev/null +++ b/apps/web/src/app/(public)/signin/container.tsx @@ -0,0 +1,111 @@ +'use client'; + +import { yupResolver } from '@hookform/resolvers/yup'; +import { Alert } from '@snipcode/front/components/alert'; +import { Button } from '@snipcode/front/forms/button'; +import { TextInput } from '@snipcode/front/forms/text-input'; +import { GithubIcon, GoogleIcon } from '@snipcode/front/icons'; +import { useLoginUser } from '@snipcode/front/services'; +import Link from 'next/link'; +import { useState } from 'react'; +import { FormProvider, useForm } from 'react-hook-form'; +import * as yup from 'yup'; + +import { useAuth } from '@/hooks/authentication/use-auth'; +import { FORM_ERRORS } from '@/lib/constants'; + +const formSchema = yup.object().shape({ + email: yup.string().required(FORM_ERRORS.fieldRequired).email(FORM_ERRORS.emailInvalid), + password: yup.string().required(FORM_ERRORS.fieldRequired), +}); + +type FormValues = yup.InferType; + +export const SignInContainer = () => { + const [loginError, setLoginError] = useState(null); + const { redirectToDashboard, saveToken } = useAuth(); + const { authenticateUser, isLoading } = useLoginUser(); + + const formMethods = useForm({ + defaultValues: {}, + resolver: yupResolver(formSchema), + }); + + const handleLogin = async (values: FormValues) => { + await authenticateUser({ + input: { + email: values.email, + password: values.password, + }, + onError: (errorMessage) => { + setLoginError(errorMessage); + }, + onSuccess: async (token) => { + saveToken(token); + + await redirectToDashboard(); + }, + }); + }; + + return ( +
+
+
+
+
+
+ +
+
+
+

Sign in for Snipcode

+ +
+ + + +
+ +

or sign in with email

+
+ + +
+ {loginError && } + + + + + + + +
+ +

+ Don't have an account?{' '} + + Create an account now + +

+
+
+
+
+
+ ); +}; diff --git a/apps/web/src/app/(public)/signin/page.tsx b/apps/web/src/app/(public)/signin/page.tsx new file mode 100644 index 00000000..2c0bcaa9 --- /dev/null +++ b/apps/web/src/app/(public)/signin/page.tsx @@ -0,0 +1,19 @@ +import { ApolloWrapper } from '@/lib/apollo/client'; +import { generatePageMetadata } from '@/lib/seo'; + +import { SignInContainer } from './container'; + +export const metadata = generatePageMetadata({ + description: 'Sign in to your account', + title: 'Snipcode - Sign in', +}); + +const SignInPage = () => { + return ( + + + + ); +}; + +export default SignInPage; diff --git a/apps/web/src/app/(public)/signup/container.tsx b/apps/web/src/app/(public)/signup/container.tsx new file mode 100644 index 00000000..3af9480a --- /dev/null +++ b/apps/web/src/app/(public)/signup/container.tsx @@ -0,0 +1,137 @@ +'use client'; + +import { yupResolver } from '@hookform/resolvers/yup'; +import { Alert } from '@snipcode/front/components/alert'; +import { Link } from '@snipcode/front/components/link'; +import { Button } from '@snipcode/front/forms/button'; +import { TextInput } from '@snipcode/front/forms/text-input'; +import { GithubIcon, GoogleIcon } from '@snipcode/front/icons'; +import { useSignupUser } from '@snipcode/front/services'; +import { useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { FormProvider, useForm } from 'react-hook-form'; +import * as yup from 'yup'; + +import { FORM_ERRORS } from '@/lib/constants'; + +const MIN_PASSWORD_LENGTH = 8; +const MIN_NAME_LENGTH = 2; +const formSchema = yup.object().shape({ + confirmPassword: yup.string().oneOf([yup.ref('password'), ''], FORM_ERRORS.passwordNotMatch), + email: yup.string().required(FORM_ERRORS.fieldRequired).email(FORM_ERRORS.emailInvalid), + name: yup + .string() + .required(FORM_ERRORS.fieldRequired) + .min(MIN_NAME_LENGTH, FORM_ERRORS.minCharacters(MIN_NAME_LENGTH)), + password: yup + .string() + .required(FORM_ERRORS.fieldRequired) + .min(MIN_PASSWORD_LENGTH, FORM_ERRORS.minCharacters(MIN_PASSWORD_LENGTH)), +}); + +type FormValues = yup.InferType; + +export const SignupContainer = () => { + const router = useRouter(); + const [signupError, setSignupError] = useState(null); + const { isLoading, signupUser } = useSignupUser(); + + const formMethods = useForm({ + resolver: yupResolver(formSchema), + }); + + const handleSignup = async (values: FormValues) => { + console.log(values); + + setSignupError(null); + + await signupUser({ + input: { + email: values.email, + name: values.name, + password: values.password, + }, + onError: (errorMessage) => { + setSignupError(errorMessage); + }, + onSuccess: () => { + router.push('/auth/signup-success'); + }, + }); + }; + + return ( +
+
+
+
+
+
+ +
+
+
+

Sign up for Snipcode

+ +
+ + + +
+ +

or sign up with email

+
+ + +
+ {signupError && } + + + + + + + + + + + +
+ +

+ Already have an account?{' '} + + Sign in now + +

+
+
+
+
+
+ ); +}; diff --git a/apps/web/src/app/(public)/signup/page.tsx b/apps/web/src/app/(public)/signup/page.tsx new file mode 100644 index 00000000..14d24069 --- /dev/null +++ b/apps/web/src/app/(public)/signup/page.tsx @@ -0,0 +1,19 @@ +import { ApolloWrapper } from '@/lib/apollo/client'; +import { generatePageMetadata } from '@/lib/seo'; + +import { SignupContainer } from './container'; + +export const metadata = generatePageMetadata({ + description: 'Create an account to access all features', + title: 'Snipcode - Sign up', +}); + +const SignupPage = () => { + return ( + + + + ); +}; + +export default SignupPage; diff --git a/apps/web/src/app/global-error.tsx b/apps/web/src/app/global-error.tsx new file mode 100644 index 00000000..c06a8140 --- /dev/null +++ b/apps/web/src/app/global-error.tsx @@ -0,0 +1,28 @@ +'use client'; + +import * as Sentry from '@sentry/nextjs'; +import NextError from 'next/error'; +import { useEffect } from 'react'; + +// is only enabled in production +const GlobalError = ({ error }: { error: Error & { digest?: string } }) => { + useEffect(() => { + if (process.env.NEXT_PUBLIC_SENTRY_ENABLED === 'true') { + Sentry.captureException(error); + } + }, [error]); + + return ( + + + {/* `NextError` is the default Next.js error page component. Its type + definition requires a `statusCode` prop. However, since the App Router + does not expose status codes for errors, we simply pass 0 to render a + generic error message. */} + + + + ); +}; + +export default GlobalError; diff --git a/apps/web/src/app/manifest.ts b/apps/web/src/app/manifest.ts new file mode 100644 index 00000000..6d27839c --- /dev/null +++ b/apps/web/src/app/manifest.ts @@ -0,0 +1,27 @@ +import { MetadataRoute } from 'next'; + +export default function manifest(): MetadataRoute.Manifest { + return { + background_color: '#ffffff', + description: 'Snipcode – The code snippets management tools for developers', + display: 'standalone', + icons: [ + { + purpose: 'maskable', + sizes: '192x192', + src: '/android-chrome-192x192.png', + type: 'image/png', + }, + { + purpose: 'maskable', + sizes: '512x512', + src: '/android-chrome-512x512.png', + type: 'image/png', + }, + ], + name: 'Snipcode App', + short_name: 'Snipcode', + start_url: '/', + theme_color: '#ffffff', + }; +} diff --git a/apps/web/src/app/robot.ts b/apps/web/src/app/robot.ts new file mode 100644 index 00000000..9e178d97 --- /dev/null +++ b/apps/web/src/app/robot.ts @@ -0,0 +1,18 @@ +import { MetadataRoute } from 'next'; +import { headers } from 'next/headers'; + +export default function robots(): MetadataRoute.Robots { + const headersList = headers(); + const domain = headersList.get('host') as string; + + return { + rules: [ + { + allow: '/', + disallow: '/app/', + userAgent: '*', + }, + ], + sitemap: `https://${domain}/sitemap.xml`, + }; +} diff --git a/apps/web/src/app/sitemap.ts b/apps/web/src/app/sitemap.ts new file mode 100644 index 00000000..824874a3 --- /dev/null +++ b/apps/web/src/app/sitemap.ts @@ -0,0 +1,16 @@ +import { MetadataRoute } from 'next'; + +import { APP_URL } from '@/lib/constants'; + +export default function sitemap(): MetadataRoute.Sitemap { + return [ + { + lastModified: new Date(), + url: `${APP_URL}/signup`, + }, + { + lastModified: new Date(), + url: `${APP_URL}/signin`, + }, + ]; +} diff --git a/apps/web/src/components/auth/auth-alert.tsx b/apps/web/src/components/auth/auth-alert.tsx index 4dff0b9f..f5cd7ef9 100644 --- a/apps/web/src/components/auth/auth-alert.tsx +++ b/apps/web/src/components/auth/auth-alert.tsx @@ -8,7 +8,7 @@ type Props = { title: string; }; -const AuthAlert = ({ ctaLabel, descriptionElement, redirectLink, title }: Props) => { +export const AuthAlert = ({ ctaLabel, descriptionElement, redirectLink, title }: Props) => { return (
@@ -30,5 +30,3 @@ const AuthAlert = ({ ctaLabel, descriptionElement, redirectLink, title }: Props)
); }; - -export { AuthAlert }; diff --git a/apps/web/src/components/common/error.tsx b/apps/web/src/components/common/error.tsx new file mode 100644 index 00000000..feafa059 --- /dev/null +++ b/apps/web/src/components/common/error.tsx @@ -0,0 +1,52 @@ +import Link from 'next/link'; + +type Props = { + error: Error; + onReset: () => void; +}; + +export const ErrorContent = ({ error, onReset }: Props) => { + return ( +
+
+
+
+

An error occurred

+

{error.message}

+

Please contact the support if the error persist

+
+ +
+
+
+
+
+ +
+
+ ); +}; diff --git a/apps/web/src/components/common/loader.tsx b/apps/web/src/components/common/loader.tsx index 362e81fc..aaf46551 100644 --- a/apps/web/src/components/common/loader.tsx +++ b/apps/web/src/components/common/loader.tsx @@ -1,3 +1,5 @@ +'use client'; + import classNames from 'classnames'; type LoaderProps = { @@ -8,7 +10,7 @@ type SpinnerProps = { text?: string; }; -const Spinner = ({ text = 'Loading...' }: SpinnerProps) => { +export const Spinner = ({ text = 'Loading...' }: SpinnerProps) => { return ( <>
@@ -17,7 +19,7 @@ const Spinner = ({ text = 'Loading...' }: SpinnerProps) => { ); }; -const Loader = ({ scope = 'component' }: LoaderProps) => { +export const Loader = ({ scope = 'component' }: LoaderProps) => { const loaderClasses = classNames( 'top-0 left-0 right-0 bottom-0 w-full h-screen z-50 overflow-hidden bg-gray-200 opacity-75 flex flex-col items-center justify-center', { @@ -32,5 +34,3 @@ const Loader = ({ scope = 'component' }: LoaderProps) => {
); }; - -export { Loader, Spinner }; diff --git a/apps/web/src/containers/page-not-found.tsx b/apps/web/src/components/common/page-not-found.tsx similarity index 95% rename from apps/web/src/containers/page-not-found.tsx rename to apps/web/src/components/common/page-not-found.tsx index dbbf7945..fdc9271f 100644 --- a/apps/web/src/containers/page-not-found.tsx +++ b/apps/web/src/components/common/page-not-found.tsx @@ -1,12 +1,12 @@ import { LogoIcon } from '@snipcode/front/icons'; import Link from 'next/link'; -const PageNotFound = () => { +export const PageNotFound = () => { return (
@@ -51,5 +51,3 @@ const PageNotFound = () => {
); }; - -export default PageNotFound; diff --git a/apps/web/src/components/common/redirect.tsx b/apps/web/src/components/common/redirect.tsx index 665e81f1..4a5ca6fd 100644 --- a/apps/web/src/components/common/redirect.tsx +++ b/apps/web/src/components/common/redirect.tsx @@ -1,18 +1,18 @@ -import { useRouter } from 'next/router'; +'use client'; + +import { useRouter } from 'next/navigation'; import { useEffect } from 'react'; const useRedirectToPath = (path: string) => { const router = useRouter(); useEffect(() => { - void router.push(path); + router.push(path); }, [path, router]); }; -const Redirect = ({ path }: { path: string }) => { +export const Redirect = ({ path }: { path: string }) => { useRedirectToPath(path); return <>; }; - -export { Redirect }; diff --git a/apps/web/src/components/home/feature-section.tsx b/apps/web/src/components/home/feature-section.tsx index 16e1de4b..297fbef1 100644 --- a/apps/web/src/components/home/feature-section.tsx +++ b/apps/web/src/components/home/feature-section.tsx @@ -46,7 +46,7 @@ const features = [ }, ]; -const FeatureSection = () => { +export const FeatureSection = () => { return (
@@ -72,5 +72,3 @@ const FeatureSection = () => {
); }; - -export { FeatureSection }; diff --git a/apps/web/src/components/home/hero-section.tsx b/apps/web/src/components/home/hero-section.tsx index 7c480e44..1c01bb24 100644 --- a/apps/web/src/components/home/hero-section.tsx +++ b/apps/web/src/components/home/hero-section.tsx @@ -1,4 +1,4 @@ -const HeroSection = () => { +export const HeroSection = () => { return (
@@ -58,5 +58,3 @@ const HeroSection = () => {
); }; - -export { HeroSection }; diff --git a/apps/web/src/components/home/newsletter/newsletter-alert.tsx b/apps/web/src/components/home/newsletter/newsletter-alert.tsx index f3da1c83..fe9b2b4c 100644 --- a/apps/web/src/components/home/newsletter/newsletter-alert.tsx +++ b/apps/web/src/components/home/newsletter/newsletter-alert.tsx @@ -33,7 +33,7 @@ const alertContentMap: Record = { }, }; -const NewsletterAlert = ({ handleClose, state = 'failure' }: Props) => { +export const NewsletterAlert = ({ handleClose, state = 'failure' }: Props) => { const [isOpen, setIsOpen] = useState(true); const titleClasses = classNames('flex flex-col items-center text-2xl mb-6', { @@ -101,5 +101,3 @@ const NewsletterAlert = ({ handleClose, state = 'failure' }: Props) => { ); }; - -export { NewsletterAlert }; diff --git a/apps/web/__tests__/ui/newsletter.test.tsx b/apps/web/src/components/home/newsletter/newsletter-form.test.tsx similarity index 78% rename from apps/web/__tests__/ui/newsletter.test.tsx rename to apps/web/src/components/home/newsletter/newsletter-form.test.tsx index 82630b8a..e1b827db 100644 --- a/apps/web/__tests__/ui/newsletter.test.tsx +++ b/apps/web/src/components/home/newsletter/newsletter-form.test.tsx @@ -4,29 +4,9 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React, { act } from 'react'; -import { NewsletterForm } from '@/components/home/newsletter/newsletter-form'; - -jest.mock('next/router', () => require('next-router-mock')); +import { NewsletterForm } from './newsletter-form'; describe('Newsletter Form', () => { - beforeEach(() => { - // IntersectionObserver isn't available in test environment - const mockIntersectionObserver = jest.fn(); - - mockIntersectionObserver.mockReturnValue({ - disconnect: jest.fn().mockReturnValue(null), - observe: jest.fn().mockReturnValue(null), - unobserve: jest.fn().mockReturnValue(null), - }); - window.IntersectionObserver = mockIntersectionObserver; - - global.ResizeObserver = class MockedResizeObserver { - observe = jest.fn(); - unobserve = jest.fn(); - disconnect = jest.fn(); - }; - }); - test('Subscribe successfully to the newsletter', async () => { const mocks = [ { diff --git a/apps/web/src/components/home/newsletter/newsletter-form.tsx b/apps/web/src/components/home/newsletter/newsletter-form.tsx index f627b8b2..6c77ffb8 100644 --- a/apps/web/src/components/home/newsletter/newsletter-form.tsx +++ b/apps/web/src/components/home/newsletter/newsletter-form.tsx @@ -1,14 +1,17 @@ +'use client'; + import { SpinnerIcon } from '@snipcode/front/icons'; import { useSubscribeToNewsletter } from '@snipcode/front/services'; import { useState } from 'react'; -import { NewsletterAlert } from '@/components/home/newsletter/newsletter-alert'; import { useBooleanState } from '@/hooks/use-boolean-state'; -import { REGEX_EMAIL } from '@/utils/constants'; +import { REGEX_EMAIL } from '@/lib/constants'; + +import { NewsletterAlert } from './newsletter-alert'; const isEmailValid = (email: string) => REGEX_EMAIL.test(email); -const NewsletterForm = () => { +export const NewsletterForm = () => { const [email, setEmail] = useState(''); const [subscriptionState, setSubscriptionState] = useState<'success' | 'failure' | undefined>(); const [isAlertOpened, openAlert, closeAlert] = useBooleanState(false); @@ -62,5 +65,3 @@ const NewsletterForm = () => {
); }; - -export { NewsletterForm }; diff --git a/apps/web/src/components/home/newsletter/newsletter-section.tsx b/apps/web/src/components/home/newsletter/newsletter-section.tsx index 40bcc635..ee55c604 100644 --- a/apps/web/src/components/home/newsletter/newsletter-section.tsx +++ b/apps/web/src/components/home/newsletter/newsletter-section.tsx @@ -1,6 +1,10 @@ -import { NewsletterForm } from '@/components/home/newsletter/newsletter-form'; +import React from 'react'; -const NewsletterSection = () => { +type Props = { + newsletterFormElement: React.ReactNode; +}; + +export const NewsletterSection = ({ newsletterFormElement }: Props) => { return (
@@ -21,7 +25,7 @@ const NewsletterSection = () => { }} />
- + {newsletterFormElement}

@@ -31,5 +35,3 @@ const NewsletterSection = () => { ); }; - -export { NewsletterSection }; diff --git a/apps/web/src/components/layout/main.tsx b/apps/web/src/components/layout/main.tsx deleted file mode 100644 index 65152879..00000000 --- a/apps/web/src/components/layout/main.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import Head from 'next/head'; -import { Fragment, ReactNode } from 'react'; - -type Props = { - children: ReactNode; -}; - -const MainLayout = ({ children }: Props) => { - return ( - - - Snipcode - - - - -

{children}
- - ); -}; - -export { MainLayout }; diff --git a/apps/web/src/components/layout/public/public-layout.tsx b/apps/web/src/components/layout/public/public-layout.tsx deleted file mode 100644 index 9f272d44..00000000 --- a/apps/web/src/components/layout/public/public-layout.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { ReactNode } from 'react'; - -import { PublicFooter } from '@/components/layout/public/footer'; -import { PublicHeader } from '@/components/layout/public/header'; - -type Props = { - children: ReactNode; -}; - -const PublicLayout = ({ children }: Props) => { - return ( -
- - {children} - -
- ); -}; - -export { PublicLayout }; diff --git a/apps/web/src/components/seo/seo.tsx b/apps/web/src/components/seo/seo.tsx deleted file mode 100644 index 78e6a879..00000000 --- a/apps/web/src/components/seo/seo.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { DefaultSeo } from 'next-seo'; - -const baseUrl = process.env.NEXT_PUBLIC_APP_URL; - -const GlobalSeo = () => { - const description = - 'Easily create and organize your code snippets. Share them with others developers around the world.'; - const title = 'Snipcode'; - - return ( - - ); -}; - -export { GlobalSeo }; diff --git a/apps/web/src/components/snippets/public-snippet.tsx b/apps/web/src/components/snippets/public-snippet.tsx index 687c6cf0..b658f544 100644 --- a/apps/web/src/components/snippets/public-snippet.tsx +++ b/apps/web/src/components/snippets/public-snippet.tsx @@ -6,7 +6,7 @@ type Props = { snippet: PublicSnippetResult['items'][number]; }; -const PublicSnippet = ({ snippet }: Props) => { +export const PublicSnippet = ({ snippet }: Props) => { const { user } = snippet; const htmlCode = snippet.content; @@ -39,5 +39,3 @@ const PublicSnippet = ({ snippet }: Props) => { ); }; - -export { PublicSnippet }; diff --git a/apps/web/src/containers/auth/login.tsx b/apps/web/src/containers/auth/login.tsx deleted file mode 100644 index fe4220ad..00000000 --- a/apps/web/src/containers/auth/login.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import { yupResolver } from '@hookform/resolvers/yup'; -import { Alert } from '@snipcode/front/components/alert'; -import { Button } from '@snipcode/front/forms/button'; -import { TextInput } from '@snipcode/front/forms/text-input'; -import { GithubIcon, GoogleIcon } from '@snipcode/front/icons'; -import { useLoginUser } from '@snipcode/front/services'; -import Link from 'next/link'; -import { NextSeo } from 'next-seo'; -import { useState } from 'react'; -import { FormProvider, useForm } from 'react-hook-form'; -import * as yup from 'yup'; - -import { PublicLayout } from '@/components/layout/public/public-layout'; -import { useAuth } from '@/hooks/authentication/use-auth'; -import { FORM_ERRORS } from '@/utils/constants'; - -const formSchema = yup.object().shape({ - email: yup.string().required(FORM_ERRORS.fieldRequired).email(FORM_ERRORS.emailInvalid), - password: yup.string().required(FORM_ERRORS.fieldRequired), -}); - -type FormValues = yup.InferType; - -const Login = () => { - const [loginError, setLoginError] = useState(null); - const { redirectToDashboard, saveToken } = useAuth(); - const { authenticateUser, isLoading } = useLoginUser(); - - const formMethods = useForm({ - defaultValues: {}, - resolver: yupResolver(formSchema), - }); - - const handleLogin = async (values: FormValues) => { - await authenticateUser({ - input: { - email: values.email, - password: values.password, - }, - onError: (errorMessage) => { - setLoginError(errorMessage); - }, - onSuccess: async (token) => { - saveToken(token); - - await redirectToDashboard(); - }, - }); - }; - - return ( - - -
-
-
-
-
-
- -
-
-
-

Sign in for Snipcode

- -
- - - -
- -

or sign in with email

-
- - -
- {loginError && } - - - - - - - -
- -

- Don't have an account?{' '} - - Create an account now - -

-
-
-
-
-
-
- ); -}; - -export default Login; diff --git a/apps/web/src/containers/auth/signup.tsx b/apps/web/src/containers/auth/signup.tsx deleted file mode 100644 index 4b2a5e8b..00000000 --- a/apps/web/src/containers/auth/signup.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import { yupResolver } from '@hookform/resolvers/yup'; -import { Alert } from '@snipcode/front/components/alert'; -import { Link } from '@snipcode/front/components/link'; -import { Button } from '@snipcode/front/forms/button'; -import { TextInput } from '@snipcode/front/forms/text-input'; -import { GithubIcon, GoogleIcon } from '@snipcode/front/icons'; -import { useSignupUser } from '@snipcode/front/services'; -import { useRouter } from 'next/router'; -import { NextSeo } from 'next-seo'; -import { useState } from 'react'; -import { FormProvider, useForm } from 'react-hook-form'; -import * as yup from 'yup'; - -import { PublicLayout } from '@/components/layout/public/public-layout'; -import { FORM_ERRORS } from '@/utils/constants'; - -const MIN_PASSWORD_LENGTH = 8; -const MIN_NAME_LENGTH = 2; -const formSchema = yup.object().shape({ - confirmPassword: yup.string().oneOf([yup.ref('password'), ''], FORM_ERRORS.passwordNotMatch), - email: yup.string().required(FORM_ERRORS.fieldRequired).email(FORM_ERRORS.emailInvalid), - name: yup - .string() - .required(FORM_ERRORS.fieldRequired) - .min(MIN_NAME_LENGTH, FORM_ERRORS.minCharacters(MIN_NAME_LENGTH)), - password: yup - .string() - .required(FORM_ERRORS.fieldRequired) - .min(MIN_PASSWORD_LENGTH, FORM_ERRORS.minCharacters(MIN_PASSWORD_LENGTH)), -}); - -type FormValues = yup.InferType; - -const Signup = () => { - const router = useRouter(); - const [signupError, setSignupError] = useState(null); - const { isLoading, signupUser } = useSignupUser(); - - const formMethods = useForm({ - resolver: yupResolver(formSchema), - }); - - const handleSignup = async (values: FormValues) => { - console.log(values); - - setSignupError(null); - - await signupUser({ - input: { - email: values.email, - name: values.name, - password: values.password, - }, - onError: (errorMessage) => { - setSignupError(errorMessage); - }, - onSuccess: async () => { - await router.push('/auth/signup-success'); - }, - }); - }; - - return ( - - - -
-
-
-
-
-
- -
-
-
-

Sign up for Snipcode

- -
- - - -
- -

or sign up with email

-
- - -
- {signupError && } - - - - - - - - - - - -
- -

- Already have an account?{' '} - - Sign in now - -

-
-
-
-
-
-
- ); -}; - -export default Signup; diff --git a/apps/web/src/containers/home/index.tsx b/apps/web/src/containers/home/index.tsx deleted file mode 100644 index 59489273..00000000 --- a/apps/web/src/containers/home/index.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { NextSeo } from 'next-seo'; - -import { FeatureSection } from '@/components/home/feature-section'; -import { HeroSection } from '@/components/home/hero-section'; -import { NewsletterSection } from '@/components/home/newsletter/newsletter-section'; -import { PublicLayout } from '@/components/layout/public/public-layout'; - -const Home = () => { - return ( - - - - - - - ); -}; - -export default Home; diff --git a/apps/web/src/containers/private/folders/view.tsx b/apps/web/src/containers/private/folders/view.tsx deleted file mode 100644 index f54a0094..00000000 --- a/apps/web/src/containers/private/folders/view.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { Directory } from '@snipcode/front/components/directory'; -import { useFindFolder } from '@snipcode/front/services'; -import { useRouter } from 'next/router'; -import { NextSeo } from 'next-seo'; - -import { Layout } from '@/components/layout/private/layout'; -import { useFolderDirectory } from '@/hooks/use-folder-directory'; - -const FolderView = () => { - const router = useRouter(); - const { handleBreadcrumbClick, navigateToFolder, openSnippet, rootFolderId } = useFolderDirectory(); - - const folderId = router.query.id as string; - - const { data, isLoading } = useFindFolder(folderId); - - const isFolderFound = !isLoading && Boolean(data); - - return ( - - -
- {isFolderFound && ( - - )} -
-
- ); -}; - -export { FolderView }; diff --git a/apps/web/src/containers/private/home.tsx b/apps/web/src/containers/private/home.tsx deleted file mode 100644 index 478fbdb2..00000000 --- a/apps/web/src/containers/private/home.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { Directory } from '@snipcode/front/components/directory'; -import { useAuthenticatedUser } from '@snipcode/front/services'; -import { NextSeo } from 'next-seo'; - -import { Layout } from '@/components/layout/private/layout'; -import { useFolderDirectory } from '@/hooks/use-folder-directory'; - -const PrivateHome = () => { - const { data: user } = useAuthenticatedUser(); - const { handleBreadcrumbClick, navigateToFolder, openSnippet, rootFolderId } = useFolderDirectory(); - - return ( - - -
- -
-
- ); -}; - -export { PrivateHome }; diff --git a/apps/web/src/containers/private/profile.tsx b/apps/web/src/containers/private/profile.tsx deleted file mode 100644 index ed26a17b..00000000 --- a/apps/web/src/containers/private/profile.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { NextSeo } from 'next-seo'; - -import { Layout } from '@/components/layout/private/layout'; - -const Profile = () => { - return ( - - -
-
-
-

Profile

-
-
-
-
- {/* Replace with your content */} -
-
-
- {/* /End replace */} -
-
-
-
- ); -}; - -export { Profile }; diff --git a/apps/web/src/containers/private/snippets/view.tsx b/apps/web/src/containers/private/snippets/view.tsx deleted file mode 100644 index a249f9ce..00000000 --- a/apps/web/src/containers/private/snippets/view.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { BreadCrumb } from '@snipcode/front/components/directory/breadcrumb'; -import { ViewSnippet } from '@snipcode/front/components/directory/snippets/form/view-snippet'; -import { useFindSnippet } from '@snipcode/front/services'; -import { useRouter } from 'next/router'; -import { NextSeo } from 'next-seo'; - -import { Layout } from '@/components/layout/private/layout'; -import { useFolderDirectory } from '@/hooks/use-folder-directory'; - -const SnippetView = () => { - const router = useRouter(); - const { handleBreadcrumbClick, rootFolderId } = useFolderDirectory(); - - const snippetId = router.query.id as string; - - const { data, isLoading } = useFindSnippet(snippetId); - - const isSnippetFound = !isLoading && data; - - return ( - - -
- {isSnippetFound && ( -
- -
- -
-
- )} -
-
- ); -}; - -export { SnippetView }; diff --git a/apps/web/src/hooks/authentication/use-auth.ts b/apps/web/src/hooks/authentication/use-auth.ts index 4a1431e8..3f5731d7 100644 --- a/apps/web/src/hooks/authentication/use-auth.ts +++ b/apps/web/src/hooks/authentication/use-auth.ts @@ -1,13 +1,13 @@ import { useApolloClient } from '@apollo/client'; import { useAuthenticatedUser } from '@snipcode/front/services'; import { addDayToDate } from '@snipcode/utils'; -import { useRouter } from 'next/router'; +import { useRouter } from 'next/navigation'; import { useCookies } from 'react-cookie'; -import { COOKIE_NAME } from '@/utils/constants'; +import { AUTH_COOKIE_NAME } from '@/lib/constants'; -const useAuth = () => { - const [, setCookie, removeCookie] = useCookies([COOKIE_NAME]); +export const useAuth = () => { + const [, setCookie, removeCookie] = useCookies([AUTH_COOKIE_NAME]); const apolloClient = useApolloClient(); const router = useRouter(); @@ -16,7 +16,7 @@ const useAuth = () => { const saveToken = (token: string) => { const currentDate = new Date(); - setCookie(COOKIE_NAME, token, { + setCookie(AUTH_COOKIE_NAME, token, { expires: addDayToDate(currentDate, 90), path: '/', sameSite: 'none', @@ -27,7 +27,7 @@ const useAuth = () => { const deleteToken = async () => { await apolloClient.clearStore(); - removeCookie(COOKIE_NAME, { path: '/' }); + removeCookie(AUTH_COOKIE_NAME, { path: '/' }); }; const redirectToDashboard = () => router.push('/app/home'); @@ -49,5 +49,3 @@ const useAuth = () => { user: data, }; }; - -export { useAuth }; diff --git a/apps/web/src/hooks/authentication/use-set-authenticated-user.ts b/apps/web/src/hooks/authentication/use-set-authenticated-user.ts index 2d06caf6..1e9525de 100644 --- a/apps/web/src/hooks/authentication/use-set-authenticated-user.ts +++ b/apps/web/src/hooks/authentication/use-set-authenticated-user.ts @@ -1,21 +1,19 @@ -import { useRouter } from 'next/router'; +import { useSearchParams } from 'next/navigation'; import { useEffect } from 'react'; import { useAuth } from '@/hooks/authentication/use-auth'; -const useSetAuthenticatedUser = () => { - const router = useRouter(); +export const useSetAuthenticatedUser = () => { + const queryParams = useSearchParams(); const { redirectToDashboard, saveToken } = useAuth(); useEffect(() => { - const { token } = router.query; + const token = queryParams.get('token'); if (token) { saveToken(token as string); } void redirectToDashboard(); - }, [redirectToDashboard, router.query, saveToken]); + }, [redirectToDashboard, queryParams, saveToken]); }; - -export { useSetAuthenticatedUser }; diff --git a/apps/web/src/hooks/use-boolean-state.ts b/apps/web/src/hooks/use-boolean-state.ts index 36eec17d..c69e0bc5 100644 --- a/apps/web/src/hooks/use-boolean-state.ts +++ b/apps/web/src/hooks/use-boolean-state.ts @@ -1,6 +1,6 @@ import { useCallback, useState } from 'react'; -const useBooleanState = (initialState = false) => { +export const useBooleanState = (initialState = false) => { const [boolean, setBoolean] = useState(initialState); const setToTrue = useCallback(() => setBoolean(true), []); @@ -9,5 +9,3 @@ const useBooleanState = (initialState = false) => { return [boolean, setToTrue, setToFalse] as const; }; - -export { useBooleanState }; diff --git a/apps/web/src/hooks/use-click-outside.ts b/apps/web/src/hooks/use-click-outside.ts index bf940f4a..e6989559 100644 --- a/apps/web/src/hooks/use-click-outside.ts +++ b/apps/web/src/hooks/use-click-outside.ts @@ -5,7 +5,7 @@ import { MutableRefObject, useEffect } from 'react'; * @param ref * @param handler */ -const useClickOutside = (ref: MutableRefObject, handler: (event: any) => void) => { +export const useClickOutside = (ref: MutableRefObject, handler: (event: any) => void) => { useEffect(() => { const listener = (event: any) => { // Do nothing if clicking ref's element or descendent elements @@ -25,5 +25,3 @@ const useClickOutside = (ref: MutableRefObject, handler: (event: any) => vo }; }, [ref, handler]); }; - -export { useClickOutside }; diff --git a/apps/web/src/hooks/use-folder-directory.ts b/apps/web/src/hooks/use-folder-directory.ts index d6165e42..7d856bb7 100644 --- a/apps/web/src/hooks/use-folder-directory.ts +++ b/apps/web/src/hooks/use-folder-directory.ts @@ -1,5 +1,5 @@ import { useAuthenticatedUser, useLazyListDirectory } from '@snipcode/front/services'; -import { useRouter } from 'next/router'; +import { useRouter } from 'next/navigation'; export const useFolderDirectory = () => { const router = useRouter(); @@ -22,7 +22,7 @@ export const useFolderDirectory = () => { }, }); - await router.push(path); + router.push(path); }; return { diff --git a/apps/web/src/lib/apollo/client.tsx b/apps/web/src/lib/apollo/client.tsx new file mode 100644 index 00000000..bb40c75c --- /dev/null +++ b/apps/web/src/lib/apollo/client.tsx @@ -0,0 +1,59 @@ +'use client'; + +import { ApolloLink, HttpLink, from } from '@apollo/client'; +import { setContext } from '@apollo/client/link/context'; +import { + ApolloClient, + ApolloNextAppProvider, + InMemoryCache, + SSRMultipartLink, +} from '@apollo/experimental-nextjs-app-support'; +import React, { PropsWithChildren } from 'react'; +import { useCookies } from 'react-cookie'; + +import { AUTH_COOKIE_NAME } from '@/lib/constants'; + +const generateAuthorizationLink = (token: string) => { + return setContext((_, { headers }) => { + return { + headers: { + ...headers, + authorization: token, + }, + }; + }); +}; + +const createIsomorphicLink = (token: string) => { + const httpLink = new HttpLink({ + credentials: 'include', + uri: process.env.NEXT_PUBLIC_SERVER_URL, + }); + + return from([generateAuthorizationLink(token), httpLink]); +}; + +const makeClient = (token: string) => () => { + const httpLink = createIsomorphicLink(token); + + return new ApolloClient({ + cache: new InMemoryCache(), + link: + typeof window === 'undefined' + ? ApolloLink.from([ + new SSRMultipartLink({ + stripDefer: true, + }), + httpLink, + ]) + : httpLink, + }); +}; + +export const ApolloWrapper = ({ children }: PropsWithChildren) => { + const [cookies] = useCookies(); + + const token = cookies[AUTH_COOKIE_NAME]; + + return {children}; +}; diff --git a/apps/web/src/lib/apollo/server.ts b/apps/web/src/lib/apollo/server.ts new file mode 100644 index 00000000..1ce9ea3b --- /dev/null +++ b/apps/web/src/lib/apollo/server.ts @@ -0,0 +1,33 @@ +import { ApolloClient, HttpLink, InMemoryCache, from } from '@apollo/client'; +import { onError } from '@apollo/client/link/error'; +import { registerApolloClient } from '@apollo/experimental-nextjs-app-support'; + +const customErrorHandler = () => { + return onError(({ graphQLErrors, networkError }) => { + if (graphQLErrors) { + graphQLErrors.forEach(({ locations, message, path }) => + console.log(`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`), + ); + } + + if (networkError) { + console.log(`[Network error]: ${networkError}. Backend is unreachable`); + } + }); +}; + +const createIsomorphicLink = () => { + const httpLink = new HttpLink({ + credentials: 'include', + uri: process.env.NEXT_PUBLIC_SERVER_URL, + }); + + return from([customErrorHandler(), httpLink]); +}; + +export const { getClient: getApolloClient } = registerApolloClient(() => { + return new ApolloClient({ + cache: new InMemoryCache(), + link: createIsomorphicLink(), + }); +}); diff --git a/apps/web/src/utils/constants.ts b/apps/web/src/lib/constants.ts similarity index 86% rename from apps/web/src/utils/constants.ts rename to apps/web/src/lib/constants.ts index 85c07404..7538296e 100644 --- a/apps/web/src/utils/constants.ts +++ b/apps/web/src/lib/constants.ts @@ -1,4 +1,4 @@ -export const COOKIE_NAME = 'snpc_guid'; +export const AUTH_COOKIE_NAME = 'snpc_guid'; export const REGEX_EMAIL = /^\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/; export const IS_DEV = process.env.NEXT_PUBLIC_APP_ENV === 'development'; export const IS_PROD = process.env.NEXT_PUBLIC_APP_ENV === 'production'; @@ -12,3 +12,5 @@ export const FORM_ERRORS = { }; export const BAD_LOGIN_MESSAGE = 'The email or password is invalid.'; + +export const APP_URL = process.env.NEXT_PUBLIC_APP_URL; diff --git a/apps/web/src/lib/errors.ts b/apps/web/src/lib/errors.ts new file mode 100644 index 00000000..d0a9fdbf --- /dev/null +++ b/apps/web/src/lib/errors.ts @@ -0,0 +1,7 @@ +import * as Sentry from '@sentry/nextjs'; + +export const logErrorToSentry = (error: unknown, extra: Record = {}) => { + if (process.env.NEXT_PUBLIC_SENTRY_ENABLED === 'true') { + Sentry.captureException(error, { extra }); + } +}; diff --git a/apps/web/src/utils/forms.ts b/apps/web/src/lib/forms.ts similarity index 100% rename from apps/web/src/utils/forms.ts rename to apps/web/src/lib/forms.ts diff --git a/apps/web/src/lib/seo.ts b/apps/web/src/lib/seo.ts new file mode 100644 index 00000000..0e3dbb9f --- /dev/null +++ b/apps/web/src/lib/seo.ts @@ -0,0 +1,70 @@ +import { Metadata } from 'next'; + +import { APP_URL } from '@/lib/constants'; + +type GeneratePageMetadataOptions = { + description?: string; + icons?: Metadata['icons']; + image?: string | null; + noIndex?: boolean; + title?: string; +}; + +const DESCRIPTION = + 'Snipcode is the code snippets management tools for developers to easily create, organize and share your code snippets with others developers.'; +const TITLE = 'Snipcode - code snippets management tools for developers'; + +const DEFAULT_ICONS = [ + { + rel: 'apple-touch-icon', + sizes: '32x32', + url: '/apple-touch-icon.png', + }, + { + rel: 'icon', + sizes: '32x32', + type: 'image/png', + url: '/favicon-32x32.png', + }, + { + rel: 'icon', + sizes: '16x16', + type: 'image/png', + url: '/favicon-16x16.png', + }, +]; + +export const generatePageMetadata = ({ + description = DESCRIPTION, + icons = DEFAULT_ICONS, + image = `${APP_URL}/assets/og.png`, + noIndex = false, + title = TITLE, +}: GeneratePageMetadataOptions = {}): Metadata => { + const images = image ? [{ height: 720, url: image, width: 1280 }] : []; + const twitterImage = image ? { card: 'summary_large_image', images: [image] } : {}; + const pageIndex = noIndex ? { follow: false, index: false } : {}; + + return { + description, + icons, + metadataBase: new URL(APP_URL), + openGraph: { + description, + images, + locale: 'en-US', + siteName: title, + title, + type: 'website', + url: APP_URL, + }, + title, + twitter: { + creator: '@snipcode_dev', + description, + title, + ...twitterImage, + }, + ...pageIndex, + }; +}; diff --git a/apps/web/src/pages/404.tsx b/apps/web/src/pages/404.tsx deleted file mode 100644 index 05e932b4..00000000 --- a/apps/web/src/pages/404.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import dynamic from 'next/dynamic'; -import { NextSeo } from 'next-seo'; - -const PageNotFound = dynamic(() => import('@/containers/page-not-found')); - -const NotFoundPage = () => { - return ( - <> - - - - ); -}; - -export default NotFoundPage; diff --git a/apps/web/src/pages/_app.tsx b/apps/web/src/pages/_app.tsx deleted file mode 100644 index c25ee759..00000000 --- a/apps/web/src/pages/_app.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { ApolloProvider } from '@apollo/client'; -import type { AppProps } from 'next/app'; - -import { GlobalSeo } from '@/components/seo/seo'; -import { useApollo } from '@/utils/apollo-client'; -import '@/styles/globals.css'; - -const SnipcodeApp = ({ Component, pageProps }: AppProps) => { - const apolloClient = useApollo(pageProps); - - return ( - - - - - ); -}; - -export default SnipcodeApp; diff --git a/apps/web/src/pages/_error.tsx b/apps/web/src/pages/_error.tsx deleted file mode 100644 index e840307c..00000000 --- a/apps/web/src/pages/_error.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import * as Sentry from '@sentry/nextjs'; -import { NextPageContext } from 'next'; -import NextErrorComponent from 'next/error'; - -const MyError = ({ err, hasGetInitialPropsRun, statusCode }: any) => { - if (!hasGetInitialPropsRun && err) { - // getInitialProps is not called in case of - // https://github.com/vercel/next.js/issues/8592. As a workaround, we pass - // err via _app.js so it can be captured - Sentry.captureException(err); - // Flushing is not required in this case as it only happens on the client - } - - return ; -}; - -MyError.getInitialProps = async (context: NextPageContext) => { - const errorInitialProps = await NextErrorComponent.getInitialProps(context); - - const { asPath, err, res } = context; - - // Workaround for https://github.com/vercel/next.js/issues/8592, mark when - // getInitialProps has run - // @ts-expect-error - errorInitialProps.hasGetInitialPropsRun = true; - - // Returning early because we don't want to log 404 errors to Sentry. - if (res?.statusCode === 404) { - return errorInitialProps; - } - - // Running on the server, the response object (`res`) is available. - // - // Next.js will pass an err on the server if a page's data fetching methods - // threw or returned a Promise that rejected - // - // Running on the client (browser), Next.js will provide an err if: - // - // - a page's `getInitialProps` threw or returned a Promise that rejected - // - an exception was thrown somewhere in the React lifecycle (render, - // componentDidMount, etc) that was caught by Next.js's React Error - // Boundary. Read more about what types of exceptions are caught by Error - // Boundaries: https://reactjs.org/docs/error-boundaries.html - - if (err) { - Sentry.captureException(err); - - // Flushing before returning is necessary if deploying to Vercel, see - // https://vercel.com/docs/platform/limits#streaming-responses - await Sentry.flush(2000); - - return errorInitialProps; - } - - // If this point is reached, getInitialProps was called without any - // information about what the error might be. This is unexpected and may - // indicate a bug introduced in Next.js, so record it in Sentry - Sentry.captureException(new Error(`_error.js getInitialProps missing data at path: ${asPath}`)); - await Sentry.flush(2000); - - return errorInitialProps; -}; - -export default MyError; diff --git a/apps/web/src/pages/app/browse.tsx b/apps/web/src/pages/app/browse.tsx deleted file mode 100644 index a10ae2ac..00000000 --- a/apps/web/src/pages/app/browse.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { findPublicSnippetsQuery } from '@snipcode/front/graphql'; -import { formatPublicSnippetsResult } from '@snipcode/front/services'; -import { PublicSnippetResult } from '@snipcode/front/typings/queries'; -import { SNIPPET_ITEM_PER_PAGE } from '@snipcode/front/utils/constants'; -import type { GetServerSidePropsContext, NextPage } from 'next'; - -import { Browse } from '@/containers/private/browse'; -import { addApolloState, initializeApollo } from '@/utils/apollo-client'; - -type Props = { - data: PublicSnippetResult; -}; - -const BrowsePage: NextPage = ({ data }: Props) => { - return ; -}; - -export const getServerSideProps = async (context: GetServerSidePropsContext) => { - const apolloClient = initializeApollo({ context }); - - try { - const queryResult = await apolloClient.query({ - query: findPublicSnippetsQuery, - variables: { input: { itemPerPage: SNIPPET_ITEM_PER_PAGE } }, - }); - - return addApolloState(apolloClient, { - props: { - data: formatPublicSnippetsResult(queryResult.data), - }, - }); - } catch (err) { - // TODO send to sentry - console.log(err); - - return { - props: {}, - }; - } -}; - -export default BrowsePage; diff --git a/apps/web/src/pages/app/folders/[id].tsx b/apps/web/src/pages/app/folders/[id].tsx deleted file mode 100644 index e609fcf5..00000000 --- a/apps/web/src/pages/app/folders/[id].tsx +++ /dev/null @@ -1,9 +0,0 @@ -import type { NextPage } from 'next'; - -import { FolderView } from '@/containers/private/folders/view'; - -const PrivateFolderViewPage: NextPage = () => { - return ; -}; - -export default PrivateFolderViewPage; diff --git a/apps/web/src/pages/app/home.tsx b/apps/web/src/pages/app/home.tsx deleted file mode 100644 index afe2eb9e..00000000 --- a/apps/web/src/pages/app/home.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import type { NextPage } from 'next'; - -import { PrivateHome } from '@/containers/private/home'; - -const PrivateHomePage: NextPage = () => { - return ; -}; - -export default PrivateHomePage; diff --git a/apps/web/src/pages/app/profile.tsx b/apps/web/src/pages/app/profile.tsx deleted file mode 100644 index 78ae973b..00000000 --- a/apps/web/src/pages/app/profile.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import type { NextPage } from 'next'; - -import { Profile } from '@/containers/private/profile'; - -const ProfilePage: NextPage = () => { - return ; -}; - -export default ProfilePage; diff --git a/apps/web/src/pages/app/snippets/[id].tsx b/apps/web/src/pages/app/snippets/[id].tsx deleted file mode 100644 index 5f793588..00000000 --- a/apps/web/src/pages/app/snippets/[id].tsx +++ /dev/null @@ -1,9 +0,0 @@ -import type { NextPage } from 'next'; - -import { SnippetView } from '@/containers/private/snippets/view'; - -const PrivateSnippetViewPage: NextPage = () => { - return ; -}; - -export default PrivateSnippetViewPage; diff --git a/apps/web/src/pages/auth/fail.tsx b/apps/web/src/pages/auth/fail.tsx deleted file mode 100644 index 8716e47d..00000000 --- a/apps/web/src/pages/auth/fail.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import type { NextPage } from 'next'; -import { NextSeo } from 'next-seo'; - -import { AuthAlert } from '@/components/auth/auth-alert'; -import { PublicLayout } from '@/components/layout/public/public-layout'; - -const AuthErrorPage: NextPage = () => { - return ( - - - - An error occurred while authenticating -
- Please contact the{' '} - - support - {' '} - if the error persist. - - } - redirectLink="/" - title="Authentication failed ❌" - /> -
- ); -}; - -export default AuthErrorPage; diff --git a/apps/web/src/pages/auth/signup-success.tsx b/apps/web/src/pages/auth/signup-success.tsx deleted file mode 100644 index ed346d53..00000000 --- a/apps/web/src/pages/auth/signup-success.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import type { NextPage } from 'next'; -import { NextSeo } from 'next-seo'; - -import { AuthAlert } from '@/components/auth/auth-alert'; -import { PublicLayout } from '@/components/layout/public/public-layout'; - -const SignupSuccessPage: NextPage = () => { - return ( - - - - We sent a confirmation link to your email address. -
- Please, click on the link to activate your account and star managing your code snippets. - - } - redirectLink="/signin" - title="Your account have been created successfully 🎉" - /> -
- ); -}; - -export default SignupSuccessPage; diff --git a/apps/web/src/pages/auth/success.tsx b/apps/web/src/pages/auth/success.tsx deleted file mode 100644 index e59d40cf..00000000 --- a/apps/web/src/pages/auth/success.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import type { NextPage } from 'next'; -import { NextSeo } from 'next-seo'; - -import { AuthAlert } from '@/components/auth/auth-alert'; -import { PublicLayout } from '@/components/layout/public/public-layout'; -import { useSetAuthenticatedUser } from '@/hooks/authentication/use-set-authenticated-user'; - -const AuthSuccessPage: NextPage = () => { - useSetAuthenticatedUser(); - - return ( - - - - We are applying some latest configuration -
- You will be redirected in few seconds. - - } - redirectLink="/board" - title="Authenticated successfully 🎉" - /> -
- ); -}; - -export default AuthSuccessPage; diff --git a/apps/web/src/pages/index.tsx b/apps/web/src/pages/index.tsx deleted file mode 100644 index df144dfa..00000000 --- a/apps/web/src/pages/index.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import type { NextPage } from 'next'; -import dynamic from 'next/dynamic'; - -const Home = dynamic(() => import('@/containers/home')); - -const HomePage: NextPage = () => { - return ; -}; - -export default HomePage; diff --git a/apps/web/src/pages/signin.tsx b/apps/web/src/pages/signin.tsx deleted file mode 100644 index 16efad06..00000000 --- a/apps/web/src/pages/signin.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { authenticatedUserQuery } from '@snipcode/front/graphql'; -import { GetServerSidePropsContext } from 'next'; -import type { NextPage } from 'next'; -import dynamic from 'next/dynamic'; - -import { addApolloState, initializeApollo } from '@/utils/apollo-client'; - -const SignIn = dynamic(() => import('@/containers/auth/login')); - -const SignInPage: NextPage = () => { - return ; -}; - -export async function getServerSideProps(context: GetServerSidePropsContext) { - const apolloClient = initializeApollo({ context }); - - try { - await apolloClient.query({ - query: authenticatedUserQuery, - variables: {}, - }); - } catch (err) {} - - return addApolloState(apolloClient, { - props: {}, - }); -} - -export default SignInPage; diff --git a/apps/web/src/pages/signup.tsx b/apps/web/src/pages/signup.tsx deleted file mode 100644 index 493e7ff6..00000000 --- a/apps/web/src/pages/signup.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import type { NextPage } from 'next'; -import dynamic from 'next/dynamic'; - -const Signup = dynamic(() => import('@/containers/auth/signup')); - -const SignupPage: NextPage = () => { - return ; -}; - -export default SignupPage; diff --git a/apps/web/src/utils/apollo-client.ts b/apps/web/src/utils/apollo-client.ts deleted file mode 100644 index 2a721f84..00000000 --- a/apps/web/src/utils/apollo-client.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { ApolloClient, HttpLink, InMemoryCache, NormalizedCacheObject, from } from '@apollo/client'; -import { setContext } from '@apollo/client/link/context'; -import { onError } from '@apollo/client/link/error'; -import merge from 'deepmerge'; -import isEqual from 'lodash/isEqual'; -import { useMemo } from 'react'; -import { Cookies } from 'react-cookie'; - -import { COOKIE_NAME, IS_DEV } from '@/utils/constants'; - -type AppContext = { - req?: { - cookies: Partial<{ [key: string]: string }>; - }; -}; - -type InitApollo = { - context?: AppContext; - initialState?: any; -}; - -export const APOLLO_STATE_PROP_NAME = '__APOLLO_STATE__'; - -let apolloClient: ApolloClient | null = null; - -const isSSR = () => typeof window === 'undefined'; - -const getAuthorizationToken = (context?: AppContext) => { - if (isSSR()) { - return context?.req?.cookies[COOKIE_NAME]; - } - - const cookie = new Cookies(); - - return cookie.get(COOKIE_NAME); -}; - -const generateAuthorizationLink = (context?: AppContext) => { - return setContext((_, { headers }) => { - return { - headers: { - ...headers, - authorization: getAuthorizationToken(context), - }, - }; - }); -}; - -const customErrorHandler = () => { - return onError(({ graphQLErrors, networkError }) => { - if (graphQLErrors) { - graphQLErrors.forEach(({ locations, message, path }) => - console.log(`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`), - ); - } - - if (networkError) { - console.log(`[Network error]: ${networkError}. Backend is unreachable`); - } - }); -}; - -const createIsomorphicLink = (context?: AppContext) => { - const httpLink = new HttpLink({ - credentials: 'include', - uri: process.env.NEXT_PUBLIC_SERVER_URL, - }); - - return from([generateAuthorizationLink(context), customErrorHandler(), httpLink]); -}; - -const createApolloClient = (ctx?: AppContext) => { - return new ApolloClient({ - cache: new InMemoryCache(), - connectToDevTools: IS_DEV, - link: createIsomorphicLink(ctx), - ssrMode: typeof window === 'undefined', - }); -}; - -export const initializeApollo = ({ context, initialState }: InitApollo) => { - const _apolloClient = apolloClient ?? createApolloClient(context); - - // If your page has Next.js data fetching methods that use Apollo Client, the initial state - // gets hydrated here - if (initialState) { - // Get existing cache, loaded during client side data fetching - const existingCache = _apolloClient.extract(); - - // Merge the initialState from getStaticProps/getServerSideProps in the existing cache - const data = merge(existingCache, initialState, { - // combine arrays using object equality (like in sets) - arrayMerge: (destinationArray, sourceArray) => [ - ...sourceArray, - ...destinationArray.filter((d) => sourceArray.every((s) => !isEqual(d, s))), - ], - }); - - // Restore the cache with the merged data - _apolloClient.cache.restore(data); - } - // For SSG and SSR always create a new Apollo Client - if (isSSR()) return _apolloClient; - // Create the Apollo Client once in the client - if (!apolloClient) apolloClient = _apolloClient; - - return _apolloClient; -}; - -export const addApolloState = (client: ApolloClient, pageProps: { props: any }) => { - pageProps.props[APOLLO_STATE_PROP_NAME] = client.cache.extract(); - - return pageProps; -}; - -export const useApollo = (pageProps: any) => { - const state = pageProps[APOLLO_STATE_PROP_NAME]; - - return useMemo(() => initializeApollo({ initialState: state }), [state]); -}; diff --git a/apps/web/tailwind.config.js b/apps/web/tailwind.config.js index dbc41ad8..25234a6d 100644 --- a/apps/web/tailwind.config.js +++ b/apps/web/tailwind.config.js @@ -2,10 +2,9 @@ const defaultTheme = require('tailwindcss/defaultTheme'); module.exports = { content: [ - "./src/pages/**/*.{js,ts,jsx,tsx}", - "./src/components/**/*.{js,ts,jsx,tsx}", - "./src/containers/**/*.{js,ts,jsx,tsx}", - "../../packages/front/**/*.{js,ts,jsx,tsx}", + './src/app/**/*.{js,ts,jsx,tsx}', + './src/components/**/*.{js,ts,jsx,tsx}', + '../../packages/front/**/*.{js,ts,jsx,tsx}', ], darkMode: 'class', theme: { @@ -22,9 +21,9 @@ module.exports = { }, }, screens: { - 'xs': {'max': '639px'}, + xs: { max: '639px' }, ...defaultTheme.screens, - } + }, }, plugins: [require('@tailwindcss/forms')], -} +}; diff --git a/apps/web/__mocks__/fileMock.js b/apps/web/tests/mocks/fileMock.js similarity index 100% rename from apps/web/__mocks__/fileMock.js rename to apps/web/tests/mocks/fileMock.js diff --git a/apps/web/__mocks__/styleMock.js b/apps/web/tests/mocks/styleMock.js similarity index 100% rename from apps/web/__mocks__/styleMock.js rename to apps/web/tests/mocks/styleMock.js diff --git a/apps/web/__tests__/setup/jest.setup.ts b/apps/web/tests/setup.ts similarity index 100% rename from apps/web/__tests__/setup/jest.setup.ts rename to apps/web/tests/setup.ts diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index e19a1907..c51f1ddb 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -1,7 +1,11 @@ { "compilerOptions": { "target": "es5", - "lib": ["dom", "dom.iterable", "esnext"], + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -16,11 +20,31 @@ "incremental": true, "baseUrl": ".", "paths": { - "@/*": ["./src/*"], - "@/public/*": ["./public/*"] - } + "@/*": [ + "./src/*" + ], + "@/public/*": [ + "./public/*" + ] + }, + "plugins": [ + { + "name": "next" + } + ] }, - "files": ["env.d.ts"], - "include": ["next-env.d.ts", "src", "__tests__"], - "exclude": ["node_modules", ".next", ".turbo"] + "files": [ + "env.d.ts" + ], + "include": [ + "next-env.d.ts", + "src", + "tests", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules", + ".next", + ".turbo" + ] } diff --git a/apps/web/vitest.config.ts b/apps/web/vitest.config.ts new file mode 100644 index 00000000..5b010465 --- /dev/null +++ b/apps/web/vitest.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from 'vitest/config'; +import react from '@vitejs/plugin-react'; +import tsconfigPaths from 'vite-tsconfig-paths'; + +export default defineConfig({ + plugins: [react(), tsconfigPaths()], + test: { + environment: 'jsdom', + globals: true, + setupFiles: ['./tests/setup.ts'], + include: ['./src/**/*.test.(ts|tsx)'], + exclude: ['./.next/', './node_modules/'], + coverage: { + provider: 'v8', + include: ['**/*.{js,jsx,ts,tsx}', '!**/*.d.ts', '!**/node_modules/**'], + exclude: ['./.next/', './node_modules/'], + }, + }, +}); diff --git a/packages/front/components/directory/index.tsx b/packages/front/components/directory/index.tsx index 27c235af..0f426ad1 100644 --- a/packages/front/components/directory/index.tsx +++ b/packages/front/components/directory/index.tsx @@ -1,3 +1,5 @@ +'use client'; + import { useState } from 'react'; import { BreadCrumb } from './breadcrumb'; diff --git a/packages/front/components/toast/provider.tsx b/packages/front/components/toast/provider.tsx index f0478990..73cadacf 100644 --- a/packages/front/components/toast/provider.tsx +++ b/packages/front/components/toast/provider.tsx @@ -1,3 +1,5 @@ +'use client'; + import { PropsWithChildren, createContext, useContext } from 'react'; import { Toast } from './toast'; diff --git a/packages/front/services/snippets/public-snippets.ts b/packages/front/services/snippets/public-snippets.ts index 2db12cb7..4ccbd260 100644 --- a/packages/front/services/snippets/public-snippets.ts +++ b/packages/front/services/snippets/public-snippets.ts @@ -9,12 +9,12 @@ type FindPublicSnippetsArgs = { nextToken?: string | null; sortMethod: 'recently_updated' | 'recently_created'; }; - onCompleted: (result?: PublicSnippetResult) => void; + onCompleted: (result: PublicSnippetResult | null) => void; }; -export const formatPublicSnippetsResult = (data?: PublicSnippetsQuery): PublicSnippetResult | undefined => { +export const formatPublicSnippetsResult = (data?: PublicSnippetsQuery): PublicSnippetResult | null => { if (!data?.publicSnippets) { - return; + return null; } const { hasMore, itemPerPage, items, nextToken } = data.publicSnippets; diff --git a/yarn.lock b/yarn.lock index f72ee051..75eb79f0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -36,6 +36,16 @@ __metadata: languageName: node linkType: hard +"@ampproject/remapping@npm:^2.3.0": + version: 2.3.0 + resolution: "@ampproject/remapping@npm:2.3.0" + dependencies: + "@jridgewell/gen-mapping": "npm:^0.3.5" + "@jridgewell/trace-mapping": "npm:^0.3.24" + checksum: 10/f3451525379c68a73eb0a1e65247fbf28c0cccd126d93af21c75fceff77773d43c0d4a2d51978fb131aff25b5f2cb41a9fe48cc296e61ae65e679c4f6918b0ab + languageName: node + linkType: hard + "@angular-devkit/core@npm:17.1.2": version: 17.1.2 resolution: "@angular-devkit/core@npm:17.1.2" @@ -93,6 +103,18 @@ __metadata: languageName: node linkType: hard +"@apollo/client-react-streaming@npm:0.11.2": + version: 0.11.2 + resolution: "@apollo/client-react-streaming@npm:0.11.2" + dependencies: + ts-invariant: "npm:^0.10.3" + peerDependencies: + "@apollo/client": ^3.10.4 + react: ^18 + checksum: 10/cfd5cf18416359e1cf79471d9609e9c1ceddb156a4887f1db60079f0bea36e256c416d2cd7cc1e1f5bdda76296b1c818600d2a468236fe22b7c26f60d3d287dc + languageName: node + linkType: hard + "@apollo/client@npm:3.10.6": version: 3.10.6 resolution: "@apollo/client@npm:3.10.6" @@ -130,6 +152,19 @@ __metadata: languageName: node linkType: hard +"@apollo/experimental-nextjs-app-support@npm:0.11.2": + version: 0.11.2 + resolution: "@apollo/experimental-nextjs-app-support@npm:0.11.2" + dependencies: + "@apollo/client-react-streaming": "npm:0.11.2" + peerDependencies: + "@apollo/client": ^3.10.4 + next: ^13.4.1 || ^14.0.0 || 15.0.0-rc.0 + react: ^18 + checksum: 10/c243ca09b6b8ecff785128d7e45f735bfc0a6b99f044c9c0851c6799c12e67a61f4e13fc29c86ca0843b52643d40ff57da4596e820d21a6a3782e123749b302c + languageName: node + linkType: hard + "@apollo/protobufjs@npm:1.2.7": version: 1.2.7 resolution: "@apollo/protobufjs@npm:1.2.7" @@ -424,6 +459,13 @@ __metadata: languageName: node linkType: hard +"@babel/compat-data@npm:^7.24.8": + version: 7.24.8 + resolution: "@babel/compat-data@npm:7.24.8" + checksum: 10/6989b8a61782d9c6c7a1fc58b4efd4fb68e5f5a5b6be3463a3de3752f39a30d21438b8b4485c18cb6b8d7f29e07f79d79639caa08737fae57838e81d7da055c0 + languageName: node + linkType: hard + "@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.14.0": version: 7.23.7 resolution: "@babel/core@npm:7.23.7" @@ -493,6 +535,29 @@ __metadata: languageName: node linkType: hard +"@babel/core@npm:^7.24.5": + version: 7.24.8 + resolution: "@babel/core@npm:7.24.8" + dependencies: + "@ampproject/remapping": "npm:^2.2.0" + "@babel/code-frame": "npm:^7.24.7" + "@babel/generator": "npm:^7.24.8" + "@babel/helper-compilation-targets": "npm:^7.24.8" + "@babel/helper-module-transforms": "npm:^7.24.8" + "@babel/helpers": "npm:^7.24.8" + "@babel/parser": "npm:^7.24.8" + "@babel/template": "npm:^7.24.7" + "@babel/traverse": "npm:^7.24.8" + "@babel/types": "npm:^7.24.8" + convert-source-map: "npm:^2.0.0" + debug: "npm:^4.1.0" + gensync: "npm:^1.0.0-beta.2" + json5: "npm:^2.2.3" + semver: "npm:^6.3.1" + checksum: 10/79818e6e8ecd5f50ffbfb8dfb1748928e6e17b198bd8da0d6f725bf67aece5141020cd3aba56dc425a33e118c0e80e5569ad6fa615897e49726087dd875279d7 + languageName: node + linkType: hard + "@babel/generator@npm:^7.14.0, @babel/generator@npm:^7.18.13, @babel/generator@npm:^7.23.6, @babel/generator@npm:^7.7.2": version: 7.23.6 resolution: "@babel/generator@npm:7.23.6" @@ -517,6 +582,18 @@ __metadata: languageName: node linkType: hard +"@babel/generator@npm:^7.24.8": + version: 7.24.8 + resolution: "@babel/generator@npm:7.24.8" + dependencies: + "@babel/types": "npm:^7.24.8" + "@jridgewell/gen-mapping": "npm:^0.3.5" + "@jridgewell/trace-mapping": "npm:^0.3.25" + jsesc: "npm:^2.5.1" + checksum: 10/dc1bd931120f93e7a5b35fdf66c13ca56b966b07ee9ba124f7e24b1905cbcf7d7891cc7c281961876eff9fcff67c46652cce89847665e263bc04d283d4343164 + languageName: node + linkType: hard + "@babel/helper-annotate-as-pure@npm:^7.22.5": version: 7.22.5 resolution: "@babel/helper-annotate-as-pure@npm:7.22.5" @@ -552,6 +629,19 @@ __metadata: languageName: node linkType: hard +"@babel/helper-compilation-targets@npm:^7.24.8": + version: 7.24.8 + resolution: "@babel/helper-compilation-targets@npm:7.24.8" + dependencies: + "@babel/compat-data": "npm:^7.24.8" + "@babel/helper-validator-option": "npm:^7.24.8" + browserslist: "npm:^4.23.1" + lru-cache: "npm:^5.1.1" + semver: "npm:^6.3.1" + checksum: 10/3489280d07b871af565b32f9b11946ff9a999fac0db9bec5df960760f6836c7a4b52fccb9d64229ccce835d37a43afb85659beb439ecedde04dcea7eb062a143 + languageName: node + linkType: hard + "@babel/helper-create-class-features-plugin@npm:^7.18.6": version: 7.23.7 resolution: "@babel/helper-create-class-features-plugin@npm:7.23.7" @@ -683,6 +773,21 @@ __metadata: languageName: node linkType: hard +"@babel/helper-module-transforms@npm:^7.24.8": + version: 7.24.8 + resolution: "@babel/helper-module-transforms@npm:7.24.8" + dependencies: + "@babel/helper-environment-visitor": "npm:^7.24.7" + "@babel/helper-module-imports": "npm:^7.24.7" + "@babel/helper-simple-access": "npm:^7.24.7" + "@babel/helper-split-export-declaration": "npm:^7.24.7" + "@babel/helper-validator-identifier": "npm:^7.24.7" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 10/912ad994da126c3150d8f8702030380849608094a7a352523ffa8e697080da9358d63af2582d38902c929839f394bbc6f1ae4921ba132ba3f65f27f0696aa2c7 + languageName: node + linkType: hard + "@babel/helper-optimise-call-expression@npm:^7.22.5": version: 7.22.5 resolution: "@babel/helper-optimise-call-expression@npm:7.22.5" @@ -699,6 +804,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-plugin-utils@npm:^7.24.7": + version: 7.24.8 + resolution: "@babel/helper-plugin-utils@npm:7.24.8" + checksum: 10/adbc9fc1142800a35a5eb0793296924ee8057fe35c61657774208670468a9fbfbb216f2d0bc46c680c5fefa785e5ff917cc1674b10bd75cdf9a6aa3444780630 + languageName: node + linkType: hard + "@babel/helper-replace-supers@npm:^7.22.20": version: 7.22.20 resolution: "@babel/helper-replace-supers@npm:7.22.20" @@ -772,6 +884,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-string-parser@npm:^7.24.8": + version: 7.24.8 + resolution: "@babel/helper-string-parser@npm:7.24.8" + checksum: 10/6d1bf8f27dd725ce02bdc6dffca3c95fb9ab8a06adc2edbd9c1c9d68500274230d1a609025833ed81981eff560045b6b38f7b4c6fb1ab19fc90e5004e3932535 + languageName: node + linkType: hard + "@babel/helper-validator-identifier@npm:^7.22.20": version: 7.22.20 resolution: "@babel/helper-validator-identifier@npm:7.22.20" @@ -807,6 +926,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-validator-option@npm:^7.24.8": + version: 7.24.8 + resolution: "@babel/helper-validator-option@npm:7.24.8" + checksum: 10/a52442dfa74be6719c0608fee3225bd0493c4057459f3014681ea1a4643cd38b68ff477fe867c4b356da7330d085f247f0724d300582fa4ab9a02efaf34d107c + languageName: node + linkType: hard + "@babel/helpers@npm:^7.23.7": version: 7.23.8 resolution: "@babel/helpers@npm:7.23.8" @@ -839,6 +965,16 @@ __metadata: languageName: node linkType: hard +"@babel/helpers@npm:^7.24.8": + version: 7.24.8 + resolution: "@babel/helpers@npm:7.24.8" + dependencies: + "@babel/template": "npm:^7.24.7" + "@babel/types": "npm:^7.24.8" + checksum: 10/61c08a2baa87382a87c7110e9b5574c782603e247b7e6267769ee0e8b7b54b70ff05f16466f05bb318622b7ac28e79b449edff565abf5adcb1adb1b0f42fee9c + languageName: node + linkType: hard + "@babel/highlight@npm:^7.23.4": version: 7.23.4 resolution: "@babel/highlight@npm:7.23.4" @@ -892,6 +1028,15 @@ __metadata: languageName: node linkType: hard +"@babel/parser@npm:^7.24.4, @babel/parser@npm:^7.24.8": + version: 7.24.8 + resolution: "@babel/parser@npm:7.24.8" + bin: + parser: ./bin/babel-parser.js + checksum: 10/e44b8327da46e8659bc9fb77f66e2dc4364dd66495fb17d046b96a77bf604f0446f1e9a89cf2f011d78fc3f5cdfbae2e9e0714708e1c985988335683b2e781ef + languageName: node + linkType: hard + "@babel/parser@npm:^7.24.7": version: 7.24.7 resolution: "@babel/parser@npm:7.24.7" @@ -1295,6 +1440,28 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-react-jsx-self@npm:^7.24.5": + version: 7.24.7 + resolution: "@babel/plugin-transform-react-jsx-self@npm:7.24.7" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.24.7" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/56115b4a6c006ce82846f1ab21e5ba713ee8f57a166c96c94fc632cdfbc8b9cebbf20b7cd9b8076439dabecdbf0f8ca4c2cb1bed1bf0b15cb44505a429f6a92f + languageName: node + linkType: hard + +"@babel/plugin-transform-react-jsx-source@npm:^7.24.1": + version: 7.24.7 + resolution: "@babel/plugin-transform-react-jsx-source@npm:7.24.7" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.24.7" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/682e2ae15d788453d8ab34cf0dcc29c093faf7c7cf1d60110c43f33e6477f916cf301456b314fc496fadc07123f7978225f41ac286ed0bfbad9c8e76392fdb6d + languageName: node + linkType: hard + "@babel/plugin-transform-react-jsx@npm:^7.0.0": version: 7.23.4 resolution: "@babel/plugin-transform-react-jsx@npm:7.23.4" @@ -1449,6 +1616,24 @@ __metadata: languageName: node linkType: hard +"@babel/traverse@npm:^7.24.8": + version: 7.24.8 + resolution: "@babel/traverse@npm:7.24.8" + dependencies: + "@babel/code-frame": "npm:^7.24.7" + "@babel/generator": "npm:^7.24.8" + "@babel/helper-environment-visitor": "npm:^7.24.7" + "@babel/helper-function-name": "npm:^7.24.7" + "@babel/helper-hoist-variables": "npm:^7.24.7" + "@babel/helper-split-export-declaration": "npm:^7.24.7" + "@babel/parser": "npm:^7.24.8" + "@babel/types": "npm:^7.24.8" + debug: "npm:^4.3.1" + globals: "npm:^11.1.0" + checksum: 10/47d8ecf8cfff58fe621fc4d8454b82c97c407816d8f9c435caa0c849ea7c357b91119a06f3c69f21a0228b5d06ac0b44f49d1f78cff032d6266317707f1fe615 + languageName: node + linkType: hard + "@babel/types@npm:^7.0.0, @babel/types@npm:^7.16.8, @babel/types@npm:^7.18.13, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.23.4, @babel/types@npm:^7.23.6, @babel/types@npm:^7.3.3, @babel/types@npm:^7.8.3": version: 7.23.6 resolution: "@babel/types@npm:7.23.6" @@ -1471,6 +1656,17 @@ __metadata: languageName: node linkType: hard +"@babel/types@npm:^7.24.0, @babel/types@npm:^7.24.8": + version: 7.24.8 + resolution: "@babel/types@npm:7.24.8" + dependencies: + "@babel/helper-string-parser": "npm:^7.24.8" + "@babel/helper-validator-identifier": "npm:^7.24.7" + to-fast-properties: "npm:^2.0.0" + checksum: 10/29b080b2753c22ee5e2455ff767a971443245d945dea4d1b3130e036dcdf0949a89539a581753c68d03d2f2f2325244ee0f91fb83dabee1cbac5db5246838137 + languageName: node + linkType: hard + "@babel/types@npm:^7.24.7": version: 7.24.7 resolution: "@babel/types@npm:7.24.7" @@ -1962,13 +2158,6 @@ __metadata: languageName: node linkType: hard -"@corex/deepmerge@npm:^4.0.43": - version: 4.0.43 - resolution: "@corex/deepmerge@npm:4.0.43" - checksum: 10/c9ac6163e982e81e3216a9fc7c68cd60b9788ad3b23d7387c9e9741b0274b42dfc332ae74b993e550c95e4256be5ce68045fc55d363aa083344392dc95b50d8b - languageName: node - linkType: hard - "@cspotcode/source-map-support@npm:^0.8.0": version: 0.8.1 resolution: "@cspotcode/source-map-support@npm:0.8.1" @@ -3389,7 +3578,7 @@ __metadata: languageName: node linkType: hard -"@jridgewell/trace-mapping@npm:^0.3.20, @jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25": +"@jridgewell/trace-mapping@npm:^0.3.20, @jridgewell/trace-mapping@npm:^0.3.23, @jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25": version: 0.3.25 resolution: "@jridgewell/trace-mapping@npm:0.3.25" dependencies: @@ -3715,13 +3904,6 @@ __metadata: languageName: node linkType: hard -"@next/env@npm:^13.4.3": - version: 13.5.6 - resolution: "@next/env@npm:13.5.6" - checksum: 10/c81bd6052db366407da701e4e431becbc80ef36a88bec7883b0266cdfeb45a7da959d37c38e1a816006cd2da287e5ff5b928bdb71025e3d4aa59e07dea3edd59 - languageName: node - linkType: hard - "@next/eslint-plugin-next@npm:14.2.4": version: 14.2.4 resolution: "@next/eslint-plugin-next@npm:14.2.4" @@ -4649,6 +4831,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-android-arm-eabi@npm:4.18.1": + version: 4.18.1 + resolution: "@rollup/rollup-android-arm-eabi@npm:4.18.1" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + "@rollup/rollup-android-arm64@npm:4.13.0": version: 4.13.0 resolution: "@rollup/rollup-android-arm64@npm:4.13.0" @@ -4656,6 +4845,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-android-arm64@npm:4.18.1": + version: 4.18.1 + resolution: "@rollup/rollup-android-arm64@npm:4.18.1" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-darwin-arm64@npm:4.13.0": version: 4.13.0 resolution: "@rollup/rollup-darwin-arm64@npm:4.13.0" @@ -4663,6 +4859,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-darwin-arm64@npm:4.18.1": + version: 4.18.1 + resolution: "@rollup/rollup-darwin-arm64@npm:4.18.1" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-darwin-x64@npm:4.13.0": version: 4.13.0 resolution: "@rollup/rollup-darwin-x64@npm:4.13.0" @@ -4670,6 +4873,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-darwin-x64@npm:4.18.1": + version: 4.18.1 + resolution: "@rollup/rollup-darwin-x64@npm:4.18.1" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@rollup/rollup-linux-arm-gnueabihf@npm:4.13.0": version: 4.13.0 resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.13.0" @@ -4677,6 +4887,20 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-arm-gnueabihf@npm:4.18.1": + version: 4.18.1 + resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.18.1" + conditions: os=linux & cpu=arm & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-arm-musleabihf@npm:4.18.1": + version: 4.18.1 + resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.18.1" + conditions: os=linux & cpu=arm & libc=musl + languageName: node + linkType: hard + "@rollup/rollup-linux-arm64-gnu@npm:4.13.0": version: 4.13.0 resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.13.0" @@ -4684,6 +4908,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-arm64-gnu@npm:4.18.1": + version: 4.18.1 + resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.18.1" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-arm64-musl@npm:4.13.0": version: 4.13.0 resolution: "@rollup/rollup-linux-arm64-musl@npm:4.13.0" @@ -4691,6 +4922,20 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-arm64-musl@npm:4.18.1": + version: 4.18.1 + resolution: "@rollup/rollup-linux-arm64-musl@npm:4.18.1" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-linux-powerpc64le-gnu@npm:4.18.1": + version: 4.18.1 + resolution: "@rollup/rollup-linux-powerpc64le-gnu@npm:4.18.1" + conditions: os=linux & cpu=ppc64 & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-riscv64-gnu@npm:4.13.0": version: 4.13.0 resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.13.0" @@ -4698,6 +4943,20 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-riscv64-gnu@npm:4.18.1": + version: 4.18.1 + resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.18.1" + conditions: os=linux & cpu=riscv64 & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-s390x-gnu@npm:4.18.1": + version: 4.18.1 + resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.18.1" + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-x64-gnu@npm:4.13.0": version: 4.13.0 resolution: "@rollup/rollup-linux-x64-gnu@npm:4.13.0" @@ -4705,6 +4964,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-x64-gnu@npm:4.18.1": + version: 4.18.1 + resolution: "@rollup/rollup-linux-x64-gnu@npm:4.18.1" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-x64-musl@npm:4.13.0": version: 4.13.0 resolution: "@rollup/rollup-linux-x64-musl@npm:4.13.0" @@ -4712,6 +4978,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-x64-musl@npm:4.18.1": + version: 4.18.1 + resolution: "@rollup/rollup-linux-x64-musl@npm:4.18.1" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + "@rollup/rollup-win32-arm64-msvc@npm:4.13.0": version: 4.13.0 resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.13.0" @@ -4719,6 +4992,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-win32-arm64-msvc@npm:4.18.1": + version: 4.18.1 + resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.18.1" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-win32-ia32-msvc@npm:4.13.0": version: 4.13.0 resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.13.0" @@ -4726,6 +5006,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-win32-ia32-msvc@npm:4.18.1": + version: 4.18.1 + resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.18.1" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + "@rollup/rollup-win32-x64-msvc@npm:4.13.0": version: 4.13.0 resolution: "@rollup/rollup-win32-x64-msvc@npm:4.13.0" @@ -4733,6 +5020,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-win32-x64-msvc@npm:4.18.1": + version: 4.18.1 + resolution: "@rollup/rollup-win32-x64-msvc@npm:4.18.1" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@rushstack/eslint-patch@npm:^1.3.3": version: 1.7.2 resolution: "@rushstack/eslint-patch@npm:1.7.2" @@ -5203,6 +5497,7 @@ __metadata: resolution: "@snipcode/web@workspace:apps/web" dependencies: "@apollo/client": "npm:3.10.6" + "@apollo/experimental-nextjs-app-support": "npm:0.11.2" "@headlessui/react": "npm:2.1.0" "@hookform/resolvers": "npm:3.6.0" "@sentry/nextjs": "npm:8.11.0" @@ -5215,6 +5510,8 @@ __metadata: "@testing-library/user-event": "npm:14.5.2" "@types/react": "npm:18.3.3" "@types/react-dom": "npm:18.3.0" + "@vitejs/plugin-react": "npm:4.3.1" + "@vitest/coverage-v8": "npm:2.0.2" autoprefixer: "npm:10.4.19" classnames: "npm:2.5.1" eslint-config-next: "npm:14.2.4" @@ -5222,16 +5519,17 @@ __metadata: eslint-plugin-jest-dom: "npm:5.4.0" eslint-plugin-testing-library: "npm:6.2.2" graphql: "npm:16.9.0" + jsdom: "npm:24.1.0" next: "npm:14.2.4" - next-router-mock: "npm:0.9.13" next-seo: "npm:6.5.0" - next-sitemap: "npm:4.2.3" postcss: "npm:8.4.38" react: "npm:18.3.1" react-cookie: "npm:7.1.4" react-dom: "npm:18.3.1" react-hook-form: "npm:7.52.0" tailwindcss: "npm:3.4.4" + vite-tsconfig-paths: "npm:4.3.2" + vitest: "npm:2.0.2" yup: "npm:1.4.0" languageName: unknown linkType: soft @@ -5433,7 +5731,7 @@ __metadata: languageName: node linkType: hard -"@types/babel__core@npm:^7.1.14": +"@types/babel__core@npm:^7.1.14, @types/babel__core@npm:^7.20.5": version: 7.20.5 resolution: "@types/babel__core@npm:7.20.5" dependencies: @@ -6385,6 +6683,107 @@ __metadata: languageName: node linkType: hard +"@vitejs/plugin-react@npm:4.3.1": + version: 4.3.1 + resolution: "@vitejs/plugin-react@npm:4.3.1" + dependencies: + "@babel/core": "npm:^7.24.5" + "@babel/plugin-transform-react-jsx-self": "npm:^7.24.5" + "@babel/plugin-transform-react-jsx-source": "npm:^7.24.1" + "@types/babel__core": "npm:^7.20.5" + react-refresh: "npm:^0.14.2" + peerDependencies: + vite: ^4.2.0 || ^5.0.0 + checksum: 10/a9d1eb30c968bf719a3277067211493746579aee14a7af8c0edb2cde38e8e5bbd461e62a41c3590e2c6eb04a047114eb3e97dcd591967625fbbc7aead8dfaf90 + languageName: node + linkType: hard + +"@vitest/coverage-v8@npm:2.0.2": + version: 2.0.2 + resolution: "@vitest/coverage-v8@npm:2.0.2" + dependencies: + "@ampproject/remapping": "npm:^2.3.0" + "@bcoe/v8-coverage": "npm:^0.2.3" + debug: "npm:^4.3.5" + istanbul-lib-coverage: "npm:^3.2.2" + istanbul-lib-report: "npm:^3.0.1" + istanbul-lib-source-maps: "npm:^5.0.6" + istanbul-reports: "npm:^3.1.7" + magic-string: "npm:^0.30.10" + magicast: "npm:^0.3.4" + std-env: "npm:^3.7.0" + strip-literal: "npm:^2.1.0" + test-exclude: "npm:^7.0.1" + tinyrainbow: "npm:^1.2.0" + peerDependencies: + vitest: 2.0.2 + checksum: 10/742add2db7c427ba1b3483c7b7eb13d86f38c33b1484144a6a62479630c6e688d34b6a3c3f56e25205c171e5e1e3379090707d0ec322948cfce836e97d5f7ad7 + languageName: node + linkType: hard + +"@vitest/expect@npm:2.0.2": + version: 2.0.2 + resolution: "@vitest/expect@npm:2.0.2" + dependencies: + "@vitest/spy": "npm:2.0.2" + "@vitest/utils": "npm:2.0.2" + chai: "npm:^5.1.1" + tinyrainbow: "npm:^1.2.0" + checksum: 10/67ebe5dcc083cbaf152fa1845da5ab4cd5a37fcc8657caaec214878c145516cf270998ad300ab9c3e7d8b4fc9ab41cbc4606af3341ae06d08c5cf44354ba5a56 + languageName: node + linkType: hard + +"@vitest/pretty-format@npm:2.0.2, @vitest/pretty-format@npm:^2.0.2": + version: 2.0.2 + resolution: "@vitest/pretty-format@npm:2.0.2" + dependencies: + tinyrainbow: "npm:^1.2.0" + checksum: 10/30ae021ea3b36271e00aac5a49084de9403900ae574b1ce1c26385ee792a7fed700f2deb2cd841b64724a4e428e908a5d3ffc1b4e6ca83daa351d76de925e9a6 + languageName: node + linkType: hard + +"@vitest/runner@npm:2.0.2": + version: 2.0.2 + resolution: "@vitest/runner@npm:2.0.2" + dependencies: + "@vitest/utils": "npm:2.0.2" + pathe: "npm:^1.1.2" + checksum: 10/f3f9f15b5a3d0b5fe5815ed0ad04bd3fceab0768c441baf20931d78f2599261c172724955e9de35020ff79950e1fd5398d0d5aad2c5ee8a91e4cc2b85943ac81 + languageName: node + linkType: hard + +"@vitest/snapshot@npm:2.0.2": + version: 2.0.2 + resolution: "@vitest/snapshot@npm:2.0.2" + dependencies: + "@vitest/pretty-format": "npm:2.0.2" + magic-string: "npm:^0.30.10" + pathe: "npm:^1.1.2" + checksum: 10/c0d41c3ff71ada909b34a8cbfe4ae9d59126fdae243b89e4eba5110db8eeb41234897159de20050a18aac2cbb7694e3fddd94bf7c79c1e9b169f1f4cf642bf07 + languageName: node + linkType: hard + +"@vitest/spy@npm:2.0.2": + version: 2.0.2 + resolution: "@vitest/spy@npm:2.0.2" + dependencies: + tinyspy: "npm:^3.0.0" + checksum: 10/feca3d26b824350d2f4f11a1e5881f1c7eeba5b903399ee8fbc2aceb4bf4201da61088783cf56bd5a2850b3e2380905f69128106655d7d849c62c52861b5af1a + languageName: node + linkType: hard + +"@vitest/utils@npm:2.0.2": + version: 2.0.2 + resolution: "@vitest/utils@npm:2.0.2" + dependencies: + "@vitest/pretty-format": "npm:2.0.2" + estree-walker: "npm:^3.0.3" + loupe: "npm:^3.1.1" + tinyrainbow: "npm:^1.2.0" + checksum: 10/771a1579c9d11bf02ed5d641619bdb9ee06f4096a2965183298c8610476316f899561dabf48e589eecccd76c75155131dc7a90d98d7519e07483b7ed09e0a5b9 + languageName: node + linkType: hard + "@webassemblyjs/ast@npm:1.12.1, @webassemblyjs/ast@npm:^1.11.5": version: 1.12.1 resolution: "@webassemblyjs/ast@npm:1.12.1" @@ -7134,6 +7533,13 @@ __metadata: languageName: node linkType: hard +"assertion-error@npm:^2.0.1": + version: 2.0.1 + resolution: "assertion-error@npm:2.0.1" + checksum: 10/a0789dd882211b87116e81e2648ccb7f60340b34f19877dd020b39ebb4714e475eb943e14ba3e22201c221ef6645b7bfe10297e76b6ac95b48a9898c1211ce66 + languageName: node + linkType: hard + "ast-types-flow@npm:^0.0.8": version: 0.0.8 resolution: "ast-types-flow@npm:0.0.8" @@ -7517,6 +7923,20 @@ __metadata: languageName: node linkType: hard +"browserslist@npm:^4.23.1": + version: 4.23.2 + resolution: "browserslist@npm:4.23.2" + dependencies: + caniuse-lite: "npm:^1.0.30001640" + electron-to-chromium: "npm:^1.4.820" + node-releases: "npm:^2.0.14" + update-browserslist-db: "npm:^1.1.0" + bin: + browserslist: cli.js + checksum: 10/326a98b1c39bcc9a99b197f15790dc28e122b1aead3257c837421899377ac96239123f26868698085b3d9be916d72540602738e1f857e86a387e810af3fda6e5 + languageName: node + linkType: hard + "bs-logger@npm:0.x": version: 0.2.6 resolution: "bs-logger@npm:0.2.6" @@ -7593,7 +8013,7 @@ __metadata: languageName: node linkType: hard -"cac@npm:^6.7.12": +"cac@npm:^6.7.12, cac@npm:^6.7.14": version: 6.7.14 resolution: "cac@npm:6.7.14" checksum: 10/002769a0fbfc51c062acd2a59df465a2a947916b02ac50b56c69ec6c018ee99ac3e7f4dd7366334ea847f1ecacf4defaa61bcd2ac283db50156ce1f1d8c8ad42 @@ -7721,6 +8141,13 @@ __metadata: languageName: node linkType: hard +"caniuse-lite@npm:^1.0.30001640": + version: 1.0.30001642 + resolution: "caniuse-lite@npm:1.0.30001642" + checksum: 10/8d80ea82be453ae0fdfea8766d82740a4945c1b99189650f29bfc458d4e235d7e99027a8f8bc5a4228d8c4457ba896315284b0703f300353ad5f09d8e693de10 + languageName: node + linkType: hard + "capital-case@npm:^1.0.4": version: 1.0.4 resolution: "capital-case@npm:1.0.4" @@ -7732,6 +8159,19 @@ __metadata: languageName: node linkType: hard +"chai@npm:^5.1.1": + version: 5.1.1 + resolution: "chai@npm:5.1.1" + dependencies: + assertion-error: "npm:^2.0.1" + check-error: "npm:^2.1.1" + deep-eql: "npm:^5.0.1" + loupe: "npm:^3.1.0" + pathval: "npm:^2.0.0" + checksum: 10/ee67279a5613bd36dc1dc13660042429ae2f1dc5a9030a6abcf381345866dfb5bce7bc10b9d74c8de86b6f656489f654bbbef3f3361e06925591e6a00c72afff + languageName: node + linkType: hard + "chalk-template@npm:0.4.0": version: 0.4.0 resolution: "chalk-template@npm:0.4.0" @@ -7838,6 +8278,13 @@ __metadata: languageName: node linkType: hard +"check-error@npm:^2.1.1": + version: 2.1.1 + resolution: "check-error@npm:2.1.1" + checksum: 10/d785ed17b1d4a4796b6e75c765a9a290098cf52ff9728ce0756e8ffd4293d2e419dd30c67200aee34202463b474306913f2fcfaf1890641026d9fc6966fea27a + languageName: node + linkType: hard + "chokidar@npm:3.6.0": version: 3.6.0 resolution: "chokidar@npm:3.6.0" @@ -8491,6 +8938,15 @@ __metadata: languageName: node linkType: hard +"cssstyle@npm:^4.0.1": + version: 4.0.1 + resolution: "cssstyle@npm:4.0.1" + dependencies: + rrweb-cssom: "npm:^0.6.0" + checksum: 10/180d4e6b406c30811e55a64add32a2111c9c5da4ed2dc67638ddb55c29b877ec1ed71e2e70a34f59c3523dbee35b0d35aa13b963db1ca8cb929d69c7ce81e3b0 + languageName: node + linkType: hard + "csstype@npm:^3.0.2": version: 3.1.3 resolution: "csstype@npm:3.1.3" @@ -8556,6 +9012,16 @@ __metadata: languageName: node linkType: hard +"data-urls@npm:^5.0.0": + version: 5.0.0 + resolution: "data-urls@npm:5.0.0" + dependencies: + whatwg-mimetype: "npm:^4.0.0" + whatwg-url: "npm:^14.0.0" + checksum: 10/5c40568c31b02641a70204ff233bc4e42d33717485d074244a98661e5f2a1e80e38fe05a5755dfaf2ee549f2ab509d6a3af2a85f4b2ad2c984e5d176695eaf46 + languageName: node + linkType: hard + "dataloader@npm:^2.2.2": version: 2.2.2 resolution: "dataloader@npm:2.2.2" @@ -8607,7 +9073,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:^4": +"debug@npm:^4, debug@npm:^4.3.5": version: 4.3.5 resolution: "debug@npm:4.3.5" dependencies: @@ -8636,7 +9102,7 @@ __metadata: languageName: node linkType: hard -"decimal.js@npm:^10.4.2": +"decimal.js@npm:^10.4.2, decimal.js@npm:^10.4.3": version: 10.4.3 resolution: "decimal.js@npm:10.4.3" checksum: 10/de663a7bc4d368e3877db95fcd5c87b965569b58d16cdc4258c063d231ca7118748738df17cd638f7e9dd0be8e34cec08d7234b20f1f2a756a52fc5a38b188d0 @@ -8655,6 +9121,13 @@ __metadata: languageName: node linkType: hard +"deep-eql@npm:^5.0.1": + version: 5.0.2 + resolution: "deep-eql@npm:5.0.2" + checksum: 10/a529b81e2ef8821621d20a36959a0328873a3e49d393ad11f8efe8559f31239494c2eb889b80342808674c475802ba95b9d6c4c27641b9a029405104c1b59fcf + languageName: node + linkType: hard + "deep-extend@npm:^0.6.0": version: 0.6.0 resolution: "deep-extend@npm:0.6.0" @@ -8967,7 +9440,14 @@ __metadata: languageName: node linkType: hard -"emittery@npm:^0.13.1": +"electron-to-chromium@npm:^1.4.820": + version: 1.4.827 + resolution: "electron-to-chromium@npm:1.4.827" + checksum: 10/7fa44aeebc5548874d33e417579d998d8e9a3d7b07fae22429ee7de5866c73b3158d56969146df3dcf44a222dcd91972ee786d0427f461e0c98bff79e408e782 + languageName: node + linkType: hard + +"emittery@npm:^0.13.1": version: 0.13.1 resolution: "emittery@npm:0.13.1" checksum: 10/fbe214171d878b924eedf1757badf58a5dce071cd1fa7f620fa841a0901a80d6da47ff05929d53163105e621ce11a71b9d8acb1148ffe1745e045145f6e69521 @@ -9187,7 +9667,7 @@ __metadata: languageName: node linkType: hard -"esbuild@npm:^0.21.4": +"esbuild@npm:^0.21.3, esbuild@npm:^0.21.4": version: 0.21.5 resolution: "esbuild@npm:0.21.5" dependencies: @@ -9274,6 +9754,13 @@ __metadata: languageName: node linkType: hard +"escalade@npm:^3.1.2": + version: 3.1.2 + resolution: "escalade@npm:3.1.2" + checksum: 10/a1e07fea2f15663c30e40b9193d658397846ffe28ce0a3e4da0d8e485fedfeca228ab846aee101a05015829adf39f9934ff45b2a3fca47bed37a29646bd05cd3 + languageName: node + linkType: hard + "escape-html@npm:~1.0.3": version: 1.0.3 resolution: "escape-html@npm:1.0.3" @@ -9768,6 +10255,15 @@ __metadata: languageName: node linkType: hard +"estree-walker@npm:^3.0.3": + version: 3.0.3 + resolution: "estree-walker@npm:3.0.3" + dependencies: + "@types/estree": "npm:^1.0.0" + checksum: 10/a65728d5727b71de172c5df323385755a16c0fdab8234dc756c3854cfee343261ddfbb72a809a5660fac8c75d960bb3e21aa898c2d7e9b19bb298482ca58a3af + languageName: node + linkType: hard + "esutils@npm:^2.0.2": version: 2.0.3 resolution: "esutils@npm:2.0.3" @@ -9942,7 +10438,7 @@ __metadata: languageName: node linkType: hard -"fast-glob@npm:3.3.2, fast-glob@npm:^3.2.12, fast-glob@npm:^3.2.9, fast-glob@npm:^3.3.0, fast-glob@npm:^3.3.1, fast-glob@npm:^3.3.2": +"fast-glob@npm:3.3.2, fast-glob@npm:^3.2.9, fast-glob@npm:^3.3.0, fast-glob@npm:^3.3.1, fast-glob@npm:^3.3.2": version: 3.3.2 resolution: "fast-glob@npm:3.3.2" dependencies: @@ -10314,7 +10810,7 @@ __metadata: languageName: node linkType: hard -"fsevents@npm:^2.3.2, fsevents@npm:~2.3.2": +"fsevents@npm:^2.3.2, fsevents@npm:~2.3.2, fsevents@npm:~2.3.3": version: 2.3.3 resolution: "fsevents@npm:2.3.3" dependencies: @@ -10324,7 +10820,7 @@ __metadata: languageName: node linkType: hard -"fsevents@patch:fsevents@npm%3A^2.3.2#optional!builtin, fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin": +"fsevents@patch:fsevents@npm%3A^2.3.2#optional!builtin, fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin, fsevents@patch:fsevents@npm%3A~2.3.3#optional!builtin": version: 2.3.3 resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin::version=2.3.3&hash=df0bf1" dependencies: @@ -10382,6 +10878,13 @@ __metadata: languageName: node linkType: hard +"get-func-name@npm:^2.0.1": + version: 2.0.2 + resolution: "get-func-name@npm:2.0.2" + checksum: 10/3f62f4c23647de9d46e6f76d2b3eafe58933a9b3830c60669e4180d6c601ce1b4aa310ba8366143f55e52b139f992087a9f0647274e8745621fa2af7e0acf13b + languageName: node + linkType: hard + "get-intrinsic@npm:^1.0.2, get-intrinsic@npm:^1.1.1, get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.0, get-intrinsic@npm:^1.2.1, get-intrinsic@npm:^1.2.2": version: 1.2.2 resolution: "get-intrinsic@npm:1.2.2" @@ -10590,6 +11093,13 @@ __metadata: languageName: node linkType: hard +"globrex@npm:^0.1.2": + version: 0.1.2 + resolution: "globrex@npm:0.1.2" + checksum: 10/81ce62ee6f800d823d6b7da7687f841676d60ee8f51f934ddd862e4057316d26665c4edc0358d4340a923ac00a514f8b67c787e28fe693aae16350f4e60d55e9 + languageName: node + linkType: hard + "gopd@npm:^1.0.1": version: 1.0.1 resolution: "gopd@npm:1.0.1" @@ -10834,6 +11344,15 @@ __metadata: languageName: node linkType: hard +"html-encoding-sniffer@npm:^4.0.0": + version: 4.0.0 + resolution: "html-encoding-sniffer@npm:4.0.0" + dependencies: + whatwg-encoding: "npm:^3.1.1" + checksum: 10/e86efd493293a5671b8239bd099d42128433bb3c7b0fdc7819282ef8e118a21f5dead0ad6f358e024a4e5c84f17ebb7a9b36075220fac0a6222b207248bede6f + languageName: node + linkType: hard + "html-escaper@npm:^2.0.0": version: 2.0.2 resolution: "html-escaper@npm:2.0.2" @@ -10882,6 +11401,16 @@ __metadata: languageName: node linkType: hard +"http-proxy-agent@npm:^7.0.2": + version: 7.0.2 + resolution: "http-proxy-agent@npm:7.0.2" + dependencies: + agent-base: "npm:^7.1.0" + debug: "npm:^4.3.4" + checksum: 10/d062acfa0cb82beeb558f1043c6ba770ea892b5fb7b28654dbc70ea2aeea55226dd34c02a294f6c1ca179a5aa483c4ea641846821b182edbd9cc5d89b54c6848 + languageName: node + linkType: hard + "https-proxy-agent@npm:^5.0.0, https-proxy-agent@npm:^5.0.1": version: 5.0.1 resolution: "https-proxy-agent@npm:5.0.1" @@ -10912,6 +11441,16 @@ __metadata: languageName: node linkType: hard +"https-proxy-agent@npm:^7.0.4": + version: 7.0.5 + resolution: "https-proxy-agent@npm:7.0.5" + dependencies: + agent-base: "npm:^7.0.2" + debug: "npm:4" + checksum: 10/6679d46159ab3f9a5509ee80c3a3fc83fba3a920a5e18d32176c3327852c3c00ad640c0c4210a8fd70ea3c4a6d3a1b375bf01942516e7df80e2646bdc77658ab + languageName: node + linkType: hard + "human-id@npm:^1.0.2": version: 1.0.2 resolution: "human-id@npm:1.0.2" @@ -11674,7 +12213,7 @@ __metadata: languageName: node linkType: hard -"istanbul-lib-coverage@npm:^3.0.0, istanbul-lib-coverage@npm:^3.2.0": +"istanbul-lib-coverage@npm:^3.0.0, istanbul-lib-coverage@npm:^3.2.0, istanbul-lib-coverage@npm:^3.2.2": version: 3.2.2 resolution: "istanbul-lib-coverage@npm:3.2.2" checksum: 10/40bbdd1e937dfd8c830fa286d0f665e81b7a78bdabcd4565f6d5667c99828bda3db7fb7ac6b96a3e2e8a2461ddbc5452d9f8bc7d00cb00075fa6a3e99f5b6a81 @@ -11707,7 +12246,7 @@ __metadata: languageName: node linkType: hard -"istanbul-lib-report@npm:^3.0.0": +"istanbul-lib-report@npm:^3.0.0, istanbul-lib-report@npm:^3.0.1": version: 3.0.1 resolution: "istanbul-lib-report@npm:3.0.1" dependencies: @@ -11729,6 +12268,17 @@ __metadata: languageName: node linkType: hard +"istanbul-lib-source-maps@npm:^5.0.6": + version: 5.0.6 + resolution: "istanbul-lib-source-maps@npm:5.0.6" + dependencies: + "@jridgewell/trace-mapping": "npm:^0.3.23" + debug: "npm:^4.1.1" + istanbul-lib-coverage: "npm:^3.0.0" + checksum: 10/569dd0a392ee3464b1fe1accbaef5cc26de3479eacb5b91d8c67ebb7b425d39fd02247d85649c3a0e9c29b600809fa60b5af5a281a75a89c01f385b1e24823a2 + languageName: node + linkType: hard + "istanbul-reports@npm:^3.1.3": version: 3.1.6 resolution: "istanbul-reports@npm:3.1.6" @@ -11739,6 +12289,16 @@ __metadata: languageName: node linkType: hard +"istanbul-reports@npm:^3.1.7": + version: 3.1.7 + resolution: "istanbul-reports@npm:3.1.7" + dependencies: + html-escaper: "npm:^2.0.0" + istanbul-lib-report: "npm:^3.0.0" + checksum: 10/f1faaa4684efaf57d64087776018d7426312a59aa6eeb4e0e3a777347d23cd286ad18f427e98f0e3dee666103d7404c9d7abc5f240406a912fa16bd6695437fa + languageName: node + linkType: hard + "iterall@npm:1.3.0, iterall@npm:^1.2.1": version: 1.3.0 resolution: "iterall@npm:1.3.0" @@ -12314,6 +12874,13 @@ __metadata: languageName: node linkType: hard +"js-tokens@npm:^9.0.0": + version: 9.0.0 + resolution: "js-tokens@npm:9.0.0" + checksum: 10/65e7a55a1a18d61f1cf94bfd7704da870b74337fa08d4c58118e69a8b10225b5ad887ff3ae595d720301b0924811a9b0594c679621a85ecbac6e3aac8533c53b + languageName: node + linkType: hard + "js-yaml@npm:^3.13.0, js-yaml@npm:^3.13.1, js-yaml@npm:^3.6.1": version: 3.14.1 resolution: "js-yaml@npm:3.14.1" @@ -12337,6 +12904,40 @@ __metadata: languageName: node linkType: hard +"jsdom@npm:24.1.0": + version: 24.1.0 + resolution: "jsdom@npm:24.1.0" + dependencies: + cssstyle: "npm:^4.0.1" + data-urls: "npm:^5.0.0" + decimal.js: "npm:^10.4.3" + form-data: "npm:^4.0.0" + html-encoding-sniffer: "npm:^4.0.0" + http-proxy-agent: "npm:^7.0.2" + https-proxy-agent: "npm:^7.0.4" + is-potential-custom-element-name: "npm:^1.0.1" + nwsapi: "npm:^2.2.10" + parse5: "npm:^7.1.2" + rrweb-cssom: "npm:^0.7.0" + saxes: "npm:^6.0.0" + symbol-tree: "npm:^3.2.4" + tough-cookie: "npm:^4.1.4" + w3c-xmlserializer: "npm:^5.0.0" + webidl-conversions: "npm:^7.0.0" + whatwg-encoding: "npm:^3.1.1" + whatwg-mimetype: "npm:^4.0.0" + whatwg-url: "npm:^14.0.0" + ws: "npm:^8.17.0" + xml-name-validator: "npm:^5.0.0" + peerDependencies: + canvas: ^2.11.2 + peerDependenciesMeta: + canvas: + optional: true + checksum: 10/0821daf73ea4b486f93a51d304037e3864ef3ca515e4646afa997b4f7f6054e6a62aabf34e2e3f2d7e0e76d3ff3d70aa81df07e96145a37988e47318e976242d + languageName: node + linkType: hard + "jsdom@npm:^20.0.0": version: 20.0.3 resolution: "jsdom@npm:20.0.3" @@ -12925,6 +13526,15 @@ __metadata: languageName: node linkType: hard +"loupe@npm:^3.1.0, loupe@npm:^3.1.1": + version: 3.1.1 + resolution: "loupe@npm:3.1.1" + dependencies: + get-func-name: "npm:^2.0.1" + checksum: 10/56d71d64c5af109aaf2b5343668ea5952eed468ed2ff837373810e417bf8331f14491c6e4d38e08ff84a29cb18906e06e58ba660c53bd00f2989e1873fa2f54c + languageName: node + linkType: hard + "lower-case-first@npm:^2.0.2": version: 2.0.2 resolution: "lower-case-first@npm:2.0.2" @@ -13026,7 +13636,7 @@ __metadata: languageName: node linkType: hard -"magic-string@npm:^0.30.3": +"magic-string@npm:^0.30.10, magic-string@npm:^0.30.3": version: 0.30.10 resolution: "magic-string@npm:0.30.10" dependencies: @@ -13035,6 +13645,17 @@ __metadata: languageName: node linkType: hard +"magicast@npm:^0.3.4": + version: 0.3.4 + resolution: "magicast@npm:0.3.4" + dependencies: + "@babel/parser": "npm:^7.24.4" + "@babel/types": "npm:^7.24.0" + source-map-js: "npm:^1.2.0" + checksum: 10/704f86639b01c8e063155408cb181d89d4444db3a4a473fb501107f30f19d9c39a159dd315ef9e54a22291c090170044efd9b49a9b3ab8d6deb948a9c99d90b3 + languageName: node + linkType: hard + "make-dir@npm:^4.0.0": version: 4.0.0 resolution: "make-dir@npm:4.0.0" @@ -13641,16 +14262,6 @@ __metadata: languageName: node linkType: hard -"next-router-mock@npm:0.9.13": - version: 0.9.13 - resolution: "next-router-mock@npm:0.9.13" - peerDependencies: - next: ">=10.0.0" - react: ">=17.0.0" - checksum: 10/582858b4521b987ff7299d97b81b526ba5debdb93b7ada83a595927ca1138082e902ff8dbb7c6cb9db79d65b1a7aa4d86fbbbf24ea73a033d317d964ca3ea95b - languageName: node - linkType: hard - "next-seo@npm:6.5.0": version: 6.5.0 resolution: "next-seo@npm:6.5.0" @@ -13662,23 +14273,6 @@ __metadata: languageName: node linkType: hard -"next-sitemap@npm:4.2.3": - version: 4.2.3 - resolution: "next-sitemap@npm:4.2.3" - dependencies: - "@corex/deepmerge": "npm:^4.0.43" - "@next/env": "npm:^13.4.3" - fast-glob: "npm:^3.2.12" - minimist: "npm:^1.2.8" - peerDependencies: - next: "*" - bin: - next-sitemap: bin/next-sitemap.mjs - next-sitemap-cjs: bin/next-sitemap.cjs - checksum: 10/8e88c941b5e487584abaa21a31a94d888c8d37e95892cd6b5bdbc121f49435f75c279e97508a7a99d3de0010e833f3769d0c2d0888d9228be4dbd48e031b831c - languageName: node - linkType: hard - "next@npm:14.2.4": version: 14.2.4 resolution: "next@npm:14.2.4" @@ -13924,6 +14518,13 @@ __metadata: languageName: node linkType: hard +"nwsapi@npm:^2.2.10": + version: 2.2.12 + resolution: "nwsapi@npm:2.2.12" + checksum: 10/172119e9ef492467ebfb337f9b5fd12a94d2b519377cde3f6ec2f74a86f6d5c00ef3873539bed7142f908ffca4e35383179be2319d04a563071d146bfa3f1673 + languageName: node + linkType: hard + "nwsapi@npm:^2.2.2": version: 2.2.7 resolution: "nwsapi@npm:2.2.7" @@ -14286,7 +14887,7 @@ __metadata: languageName: node linkType: hard -"parse5@npm:^7.0.0, parse5@npm:^7.1.1": +"parse5@npm:^7.0.0, parse5@npm:^7.1.1, parse5@npm:^7.1.2": version: 7.1.2 resolution: "parse5@npm:7.1.2" dependencies: @@ -14449,6 +15050,20 @@ __metadata: languageName: node linkType: hard +"pathe@npm:^1.1.2": + version: 1.1.2 + resolution: "pathe@npm:1.1.2" + checksum: 10/f201d796351bf7433d147b92c20eb154a4e0ea83512017bf4ec4e492a5d6e738fb45798be4259a61aa81270179fce11026f6ff0d3fa04173041de044defe9d80 + languageName: node + linkType: hard + +"pathval@npm:^2.0.0": + version: 2.0.0 + resolution: "pathval@npm:2.0.0" + checksum: 10/b91575bf9cdf01757afd7b5e521eb8a0b874a49bc972d08e0047cfea0cd3c019f5614521d4bc83d2855e3fcc331db6817dfd533dd8f3d90b16bc76fad2450fc1 + languageName: node + linkType: hard + "pg-int8@npm:1.0.1": version: 1.0.1 resolution: "pg-int8@npm:1.0.1" @@ -14505,6 +15120,13 @@ __metadata: languageName: node linkType: hard +"picocolors@npm:^1.0.1": + version: 1.0.1 + resolution: "picocolors@npm:1.0.1" + checksum: 10/fa68166d1f56009fc02a34cdfd112b0dd3cf1ef57667ac57281f714065558c01828cdf4f18600ad6851cbe0093952ed0660b1e0156bddf2184b6aaf5817553a5 + languageName: node + linkType: hard + "picomatch@npm:3.0.1": version: 3.0.1 resolution: "picomatch@npm:3.0.1" @@ -14659,6 +15281,17 @@ __metadata: languageName: node linkType: hard +"postcss@npm:^8.4.39": + version: 8.4.39 + resolution: "postcss@npm:8.4.39" + dependencies: + nanoid: "npm:^3.3.7" + picocolors: "npm:^1.0.1" + source-map-js: "npm:^1.2.0" + checksum: 10/ad9c1add892c96433b9a5502878201ede4a20c4ce02d056251f61f8d9a3e5426dab3683fe5a086edfa78a1a19f2b4988c8cea02c5122136d29758cb5a17e2621 + languageName: node + linkType: hard + "postgres-array@npm:~2.0.0": version: 2.0.0 resolution: "postgres-array@npm:2.0.0" @@ -14925,7 +15558,7 @@ __metadata: languageName: node linkType: hard -"punycode@npm:^2.1.0, punycode@npm:^2.1.1": +"punycode@npm:^2.1.0, punycode@npm:^2.1.1, punycode@npm:^2.3.1": version: 2.3.1 resolution: "punycode@npm:2.3.1" checksum: 10/febdc4362bead22f9e2608ff0171713230b57aff9dddc1c273aa2a651fbd366f94b7d6a71d78342a7c0819906750351ca7f2edd26ea41b626d87d6a13d1bd059 @@ -15098,6 +15731,13 @@ __metadata: languageName: node linkType: hard +"react-refresh@npm:^0.14.2": + version: 0.14.2 + resolution: "react-refresh@npm:0.14.2" + checksum: 10/512abf97271ab8623486061be04b608c39d932e3709f9af1720b41573415fa4993d0009fa5138b6705b60a98f4102f744d4e26c952b14f41a0e455521c6be4cc + languageName: node + linkType: hard + "react-simple-code-editor@npm:0.13.1": version: 0.13.1 resolution: "react-simple-code-editor@npm:0.13.1" @@ -15597,6 +16237,83 @@ __metadata: languageName: node linkType: hard +"rollup@npm:^4.13.0": + version: 4.18.1 + resolution: "rollup@npm:4.18.1" + dependencies: + "@rollup/rollup-android-arm-eabi": "npm:4.18.1" + "@rollup/rollup-android-arm64": "npm:4.18.1" + "@rollup/rollup-darwin-arm64": "npm:4.18.1" + "@rollup/rollup-darwin-x64": "npm:4.18.1" + "@rollup/rollup-linux-arm-gnueabihf": "npm:4.18.1" + "@rollup/rollup-linux-arm-musleabihf": "npm:4.18.1" + "@rollup/rollup-linux-arm64-gnu": "npm:4.18.1" + "@rollup/rollup-linux-arm64-musl": "npm:4.18.1" + "@rollup/rollup-linux-powerpc64le-gnu": "npm:4.18.1" + "@rollup/rollup-linux-riscv64-gnu": "npm:4.18.1" + "@rollup/rollup-linux-s390x-gnu": "npm:4.18.1" + "@rollup/rollup-linux-x64-gnu": "npm:4.18.1" + "@rollup/rollup-linux-x64-musl": "npm:4.18.1" + "@rollup/rollup-win32-arm64-msvc": "npm:4.18.1" + "@rollup/rollup-win32-ia32-msvc": "npm:4.18.1" + "@rollup/rollup-win32-x64-msvc": "npm:4.18.1" + "@types/estree": "npm:1.0.5" + fsevents: "npm:~2.3.2" + dependenciesMeta: + "@rollup/rollup-android-arm-eabi": + optional: true + "@rollup/rollup-android-arm64": + optional: true + "@rollup/rollup-darwin-arm64": + optional: true + "@rollup/rollup-darwin-x64": + optional: true + "@rollup/rollup-linux-arm-gnueabihf": + optional: true + "@rollup/rollup-linux-arm-musleabihf": + optional: true + "@rollup/rollup-linux-arm64-gnu": + optional: true + "@rollup/rollup-linux-arm64-musl": + optional: true + "@rollup/rollup-linux-powerpc64le-gnu": + optional: true + "@rollup/rollup-linux-riscv64-gnu": + optional: true + "@rollup/rollup-linux-s390x-gnu": + optional: true + "@rollup/rollup-linux-x64-gnu": + optional: true + "@rollup/rollup-linux-x64-musl": + optional: true + "@rollup/rollup-win32-arm64-msvc": + optional: true + "@rollup/rollup-win32-ia32-msvc": + optional: true + "@rollup/rollup-win32-x64-msvc": + optional: true + fsevents: + optional: true + bin: + rollup: dist/bin/rollup + checksum: 10/7a5f110d216e8599dc3cb11cf570316d989abae00785d99c2bcb6027287fe60d2eaed70e457d88a036622e7fc67e8db6e730d3c784aa90a258bd4c020676ad44 + languageName: node + linkType: hard + +"rrweb-cssom@npm:^0.6.0": + version: 0.6.0 + resolution: "rrweb-cssom@npm:0.6.0" + checksum: 10/5411836a4a78d6b68480767b8312de291f32d5710a278343954a778e5b420eaf13c90d9d2a942acf4718ddf497baa75ce653a314b332a380b6eaae1dee72257e + languageName: node + linkType: hard + +"rrweb-cssom@npm:^0.7.0": + version: 0.7.1 + resolution: "rrweb-cssom@npm:0.7.1" + checksum: 10/e80cf25c223a823921d7ab57c0ce78f5b7ebceab857b400cce99dd4913420ce679834bc5707e8ada47d062e21ad368108a9534c314dc8d72c20aa4a4fa0ed16a + languageName: node + linkType: hard + "run-async@npm:^2.4.0": version: 2.4.1 resolution: "run-async@npm:2.4.1" @@ -16003,6 +16720,13 @@ __metadata: languageName: node linkType: hard +"siginfo@npm:^2.0.0": + version: 2.0.0 + resolution: "siginfo@npm:2.0.0" + checksum: 10/e93ff66c6531a079af8fb217240df01f980155b5dc408d2d7bebc398dd284e383eb318153bf8acd4db3c4fe799aa5b9a641e38b0ba3b1975700b1c89547ea4e7 + languageName: node + linkType: hard + "signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.3, signal-exit@npm:^3.0.7": version: 3.0.7 resolution: "signal-exit@npm:3.0.7" @@ -16308,6 +17032,13 @@ __metadata: languageName: node linkType: hard +"stackback@npm:0.0.2": + version: 0.0.2 + resolution: "stackback@npm:0.0.2" + checksum: 10/2d4dc4e64e2db796de4a3c856d5943daccdfa3dd092e452a1ce059c81e9a9c29e0b9badba91b43ef0d5ff5c04ee62feb3bcc559a804e16faf447bac2d883aa99 + languageName: node + linkType: hard + "stacktrace-parser@npm:^0.1.10": version: 0.1.10 resolution: "stacktrace-parser@npm:0.1.10" @@ -16324,6 +17055,13 @@ __metadata: languageName: node linkType: hard +"std-env@npm:^3.7.0": + version: 3.7.0 + resolution: "std-env@npm:3.7.0" + checksum: 10/6ee0cca1add3fd84656b0002cfbc5bfa20340389d9ba4720569840f1caa34bce74322aef4c93f046391583e50649d0cf81a5f8fe1d411e50b659571690a45f12 + languageName: node + linkType: hard + "stream-transform@npm:^2.1.3": version: 2.1.3 resolution: "stream-transform@npm:2.1.3" @@ -16523,6 +17261,15 @@ __metadata: languageName: node linkType: hard +"strip-literal@npm:^2.1.0": + version: 2.1.0 + resolution: "strip-literal@npm:2.1.0" + dependencies: + js-tokens: "npm:^9.0.0" + checksum: 10/21c813aa1e669944e7e2318c8c927939fb90b0c52f53f57282bfc3dd6e19d53f70004f1f1693e33e5e790ad5ef102b0fce2b243808229d1ce07ae71f326c0e82 + languageName: node + linkType: hard + "styled-jsx@npm:5.1.1": version: 5.1.1 resolution: "styled-jsx@npm:5.1.1" @@ -16788,6 +17535,17 @@ __metadata: languageName: node linkType: hard +"test-exclude@npm:^7.0.1": + version: 7.0.1 + resolution: "test-exclude@npm:7.0.1" + dependencies: + "@istanbuljs/schema": "npm:^0.1.2" + glob: "npm:^10.4.1" + minimatch: "npm:^9.0.4" + checksum: 10/e6f6f4e1df2e7810e082e8d7dfc53be51a931e6e87925f5e1c2ef92cc1165246ba3bf2dae6b5d86251c16925683dba906bd41e40169ebc77120a2d1b5a0dbbe0 + languageName: node + linkType: hard + "text-extensions@npm:^2.0.0": version: 2.4.0 resolution: "text-extensions@npm:2.4.0" @@ -16834,6 +17592,34 @@ __metadata: languageName: node linkType: hard +"tinybench@npm:^2.8.0": + version: 2.8.0 + resolution: "tinybench@npm:2.8.0" + checksum: 10/9731d070bedee6d44f3bb565862c284776e6adfd70d81a051a5c79b77479408509b448ad8d467d538d18bc0ae857b3ead8168d7e98d7f1355f8a0b01aa2f163b + languageName: node + linkType: hard + +"tinypool@npm:^1.0.0": + version: 1.0.0 + resolution: "tinypool@npm:1.0.0" + checksum: 10/4041a9ae62200626dceedbf4e58589d067a203eadcb88588d5681369b9a3c68987de14ce220b32a7e4ebfabaaf51ab9fa69408a7758827b7873f8204cdc79aa1 + languageName: node + linkType: hard + +"tinyrainbow@npm:^1.2.0": + version: 1.2.0 + resolution: "tinyrainbow@npm:1.2.0" + checksum: 10/2924444db6804355e5ba2b6e586c7f77329d93abdd7257a069a0f4530dff9f16de484e80479094e3f39273462541b003a65ee3a6afc2d12555aa745132deba5d + languageName: node + linkType: hard + +"tinyspy@npm:^3.0.0": + version: 3.0.0 + resolution: "tinyspy@npm:3.0.0" + checksum: 10/b5b686acff2b88de60ff8ecf89a2042320406aaeee2fba1828a7ea8a925fad3ed9f5e4d7a068154a9134473c472aa03da8ca92ee994bc57a741c5ede5fa7de4d + languageName: node + linkType: hard + "title-case@npm:^3.0.3": version: 3.0.3 resolution: "title-case@npm:3.0.3" @@ -16912,6 +17698,18 @@ __metadata: languageName: node linkType: hard +"tough-cookie@npm:^4.1.4": + version: 4.1.4 + resolution: "tough-cookie@npm:4.1.4" + dependencies: + psl: "npm:^1.1.33" + punycode: "npm:^2.1.1" + universalify: "npm:^0.2.0" + url-parse: "npm:^1.5.3" + checksum: 10/75663f4e2cd085f16af0b217e4218772adf0617fb3227171102618a54ce0187a164e505d61f773ed7d65988f8ff8a8f935d381f87da981752c1171b076b4afac + languageName: node + linkType: hard + "tr46@npm:^1.0.1": version: 1.0.1 resolution: "tr46@npm:1.0.1" @@ -16930,6 +17728,15 @@ __metadata: languageName: node linkType: hard +"tr46@npm:^5.0.0": + version: 5.0.0 + resolution: "tr46@npm:5.0.0" + dependencies: + punycode: "npm:^2.3.1" + checksum: 10/29155adb167d048d3c95d181f7cb5ac71948b4e8f3070ec455986e1f34634acae50ae02a3c8d448121c3afe35b76951cd46ed4c128fd80264280ca9502237a3e + languageName: node + linkType: hard + "tr46@npm:~0.0.3": version: 0.0.3 resolution: "tr46@npm:0.0.3" @@ -17106,6 +17913,20 @@ __metadata: languageName: node linkType: hard +"tsconfck@npm:^3.0.3": + version: 3.1.1 + resolution: "tsconfck@npm:3.1.1" + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + bin: + tsconfck: bin/tsconfck.js + checksum: 10/a4456577f540212516d7eb530005893739aadd6da00787914a8ed9aa19c3f2f306b8912920aa440b9b8978f10c9dadbd062b8c2a2f0ff1f6c2d4272b5be2ef34 + languageName: node + linkType: hard + "tsconfig-paths-webpack-plugin@npm:4.1.0": version: 4.1.0 resolution: "tsconfig-paths-webpack-plugin@npm:4.1.0" @@ -17622,6 +18443,20 @@ __metadata: languageName: node linkType: hard +"update-browserslist-db@npm:^1.1.0": + version: 1.1.0 + resolution: "update-browserslist-db@npm:1.1.0" + dependencies: + escalade: "npm:^3.1.2" + picocolors: "npm:^1.0.1" + peerDependencies: + browserslist: ">= 4.21.0" + bin: + update-browserslist-db: cli.js + checksum: 10/d70b9efeaf4601aadb1a4f6456a7a5d9118e0063d995866b8e0c5e0cf559482671dab6ce7b079f9536b06758a344fbd83f974b965211e1c6e8d1958540b0c24c + languageName: node + linkType: hard + "update-check@npm:1.5.4": version: 1.5.4 resolution: "update-check@npm:1.5.4" @@ -17766,6 +18601,126 @@ __metadata: languageName: node linkType: hard +"vite-node@npm:2.0.2": + version: 2.0.2 + resolution: "vite-node@npm:2.0.2" + dependencies: + cac: "npm:^6.7.14" + debug: "npm:^4.3.5" + pathe: "npm:^1.1.2" + tinyrainbow: "npm:^1.2.0" + vite: "npm:^5.0.0" + bin: + vite-node: vite-node.mjs + checksum: 10/9335168dc5a20c1d0c6b53cf20f098875c7556b0eb1e1ae871aedcc796edd5906f06ab259d9b57ec12719041838cac8186e54e597c0012ee77b03a4e2be84722 + languageName: node + linkType: hard + +"vite-tsconfig-paths@npm:4.3.2": + version: 4.3.2 + resolution: "vite-tsconfig-paths@npm:4.3.2" + dependencies: + debug: "npm:^4.1.1" + globrex: "npm:^0.1.2" + tsconfck: "npm:^3.0.3" + peerDependencies: + vite: "*" + peerDependenciesMeta: + vite: + optional: true + checksum: 10/c12e2087fd01ac8a694850c649b79d5b9798cdba0ef9ab4116f669d8ffa1a9a3195c5a14410d3d9a12d2f08cd35ddd74f03d9c7b13a2d590d002055cdaab45c0 + languageName: node + linkType: hard + +"vite@npm:^5.0.0": + version: 5.3.3 + resolution: "vite@npm:5.3.3" + dependencies: + esbuild: "npm:^0.21.3" + fsevents: "npm:~2.3.3" + postcss: "npm:^8.4.39" + rollup: "npm:^4.13.0" + peerDependencies: + "@types/node": ^18.0.0 || >=20.0.0 + less: "*" + lightningcss: ^1.21.0 + sass: "*" + stylus: "*" + sugarss: "*" + terser: ^5.4.0 + dependenciesMeta: + fsevents: + optional: true + peerDependenciesMeta: + "@types/node": + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + bin: + vite: bin/vite.js + checksum: 10/e7a094cefedad9e204b715588502118e07d1b9c00c617f55b810169181907f55144f0a82f650995d6a74f12e3695fca65afc348b475b91a81dcbd0274d30a088 + languageName: node + linkType: hard + +"vitest@npm:2.0.2": + version: 2.0.2 + resolution: "vitest@npm:2.0.2" + dependencies: + "@ampproject/remapping": "npm:^2.3.0" + "@vitest/expect": "npm:2.0.2" + "@vitest/pretty-format": "npm:^2.0.2" + "@vitest/runner": "npm:2.0.2" + "@vitest/snapshot": "npm:2.0.2" + "@vitest/spy": "npm:2.0.2" + "@vitest/utils": "npm:2.0.2" + chai: "npm:^5.1.1" + debug: "npm:^4.3.5" + execa: "npm:^8.0.1" + magic-string: "npm:^0.30.10" + pathe: "npm:^1.1.2" + std-env: "npm:^3.7.0" + tinybench: "npm:^2.8.0" + tinypool: "npm:^1.0.0" + tinyrainbow: "npm:^1.2.0" + vite: "npm:^5.0.0" + vite-node: "npm:2.0.2" + why-is-node-running: "npm:^2.2.2" + peerDependencies: + "@edge-runtime/vm": "*" + "@types/node": ^18.0.0 || >=20.0.0 + "@vitest/browser": 2.0.2 + "@vitest/ui": 2.0.2 + happy-dom: "*" + jsdom: "*" + peerDependenciesMeta: + "@edge-runtime/vm": + optional: true + "@types/node": + optional: true + "@vitest/browser": + optional: true + "@vitest/ui": + optional: true + happy-dom: + optional: true + jsdom: + optional: true + bin: + vitest: vitest.mjs + checksum: 10/d92053b0d6e3e800d56cbe5eb860625fb9d50e66857da189ac19a68e511bbb0c59baf6a6b3a8ecb0b46c011567723e16e550136655e93767f228fb91caf4e16f + languageName: node + linkType: hard + "vscode-oniguruma@npm:^1.7.0": version: 1.7.0 resolution: "vscode-oniguruma@npm:1.7.0" @@ -17789,6 +18744,15 @@ __metadata: languageName: node linkType: hard +"w3c-xmlserializer@npm:^5.0.0": + version: 5.0.0 + resolution: "w3c-xmlserializer@npm:5.0.0" + dependencies: + xml-name-validator: "npm:^5.0.0" + checksum: 10/d78f59e6b4f924aa53b6dfc56949959229cae7fe05ea9374eb38d11edcec01398b7f5d7a12576bd5acc57ff446abb5c9115cd83b9d882555015437cf858d42f0 + languageName: node + linkType: hard + "walker@npm:^1.0.8": version: 1.0.8 resolution: "walker@npm:1.0.8" @@ -17925,6 +18889,15 @@ __metadata: languageName: node linkType: hard +"whatwg-encoding@npm:^3.1.1": + version: 3.1.1 + resolution: "whatwg-encoding@npm:3.1.1" + dependencies: + iconv-lite: "npm:0.6.3" + checksum: 10/bbef815eb67f91487c7f2ef96329743f5fd8357d7d62b1119237d25d41c7e452dff8197235b2d3c031365a17f61d3bb73ca49d0ed1582475aa4a670815e79534 + languageName: node + linkType: hard + "whatwg-mimetype@npm:^3.0.0": version: 3.0.0 resolution: "whatwg-mimetype@npm:3.0.0" @@ -17932,6 +18905,13 @@ __metadata: languageName: node linkType: hard +"whatwg-mimetype@npm:^4.0.0": + version: 4.0.0 + resolution: "whatwg-mimetype@npm:4.0.0" + checksum: 10/894a618e2d90bf444b6f309f3ceb6e58cf21b2beaa00c8b333696958c4076f0c7b30b9d33413c9ffff7c5832a0a0c8569e5bb347ef44beded72aeefd0acd62e8 + languageName: node + linkType: hard + "whatwg-url@npm:^11.0.0": version: 11.0.0 resolution: "whatwg-url@npm:11.0.0" @@ -17942,6 +18922,16 @@ __metadata: languageName: node linkType: hard +"whatwg-url@npm:^14.0.0": + version: 14.0.0 + resolution: "whatwg-url@npm:14.0.0" + dependencies: + tr46: "npm:^5.0.0" + webidl-conversions: "npm:^7.0.0" + checksum: 10/67ea7a359a90663b28c816d76379b4be62d13446e9a4c0ae0b5ae0294b1c22577750fcdceb40827bb35a61777b7093056953c856604a28b37d6a209ba59ad062 + languageName: node + linkType: hard + "whatwg-url@npm:^5.0.0": version: 5.0.0 resolution: "whatwg-url@npm:5.0.0" @@ -18071,6 +19061,18 @@ __metadata: languageName: node linkType: hard +"why-is-node-running@npm:^2.2.2": + version: 2.3.0 + resolution: "why-is-node-running@npm:2.3.0" + dependencies: + siginfo: "npm:^2.0.0" + stackback: "npm:0.0.2" + bin: + why-is-node-running: cli.js + checksum: 10/0de6e6cd8f2f94a8b5ca44e84cf1751eadcac3ebedcdc6e5fbbe6c8011904afcbc1a2777c53496ec02ced7b81f2e7eda61e76bf8262a8bc3ceaa1f6040508051 + languageName: node + linkType: hard + "widest-line@npm:^4.0.1": version: 4.0.1 resolution: "widest-line@npm:4.0.1" @@ -18160,6 +19162,21 @@ __metadata: languageName: node linkType: hard +"ws@npm:^8.17.0": + version: 8.18.0 + resolution: "ws@npm:8.18.0" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 10/70dfe53f23ff4368d46e4c0b1d4ca734db2c4149c6f68bc62cb16fc21f753c47b35fcc6e582f3bdfba0eaeb1c488cddab3c2255755a5c3eecb251431e42b3ff6 + languageName: node + linkType: hard + "xml-name-validator@npm:^4.0.0": version: 4.0.0 resolution: "xml-name-validator@npm:4.0.0" @@ -18167,6 +19184,13 @@ __metadata: languageName: node linkType: hard +"xml-name-validator@npm:^5.0.0": + version: 5.0.0 + resolution: "xml-name-validator@npm:5.0.0" + checksum: 10/43f30f3f6786e406dd665acf08cd742d5f8a46486bd72517edb04b27d1bcd1599664c2a4a99fc3f1e56a3194bff588b12f178b7972bc45c8047bdc4c3ac8d4a1 + languageName: node + linkType: hard + "xmlchars@npm:^2.2.0": version: 2.2.0 resolution: "xmlchars@npm:2.2.0"