From 7349e92b80e50a7737888f1cde81c16707aa11c8 Mon Sep 17 00:00:00 2001 From: Eric Cabrel TIOGO Date: Sun, 14 Jul 2024 06:38:01 +0200 Subject: [PATCH] chore(web): migrate to app router (#89) * feat(web): migrate to nextjs app router * fix: lint * fix: frontendbuild * fix: frontend test * fix: frontend test * fix: frontend test * fix: frontend test * fix: frontend test --- .changeset/tender-oranges-matter.md | 7 + .github/workflows/build-release.yml | 4 +- .github/workflows/build.yml | 4 +- apps/backend/Dockerfile | 3 - apps/web/.env.test | 10 + apps/web/.eslintrc.js | 5 +- apps/web/env.d.ts | 3 +- apps/web/jest.config.ts | 24 - apps/web/next-sitemap.js | 14 - apps/web/next.config.js | 2 +- apps/web/package.json | 13 +- apps/web/public/android-chrome-192x192.png | Bin 0 -> 15660 bytes apps/web/public/android-chrome-512x512.png | Bin 0 -> 40481 bytes apps/web/public/apple-touch-icon.png | Bin 0 -> 14139 bytes apps/web/public/favicon-16x16.png | Bin 0 -> 550 bytes apps/web/public/favicon-32x32.png | Bin 0 -> 1488 bytes apps/web/public/favicon.ico | Bin 67646 -> 15406 bytes .../(protected)/app/browse/container.tsx} | 111 +- .../app/browse/lib/fetch-snippets.ts | 31 + .../src/app/(protected)/app/browse/page.tsx | 22 + .../app/folders/[id]/container.tsx | 33 + .../app/(protected)/app/folders/[id]/page.tsx | 14 + .../app/(protected)/app/home/container.tsx | 24 + .../web/src/app/(protected)/app/home/page.tsx | 14 + .../app/(protected)/app/profile/container.tsx | 22 + .../src/app/(protected)/app/profile/page.tsx | 14 + .../app/snippets/[id]/container.tsx | 37 + .../(protected)/app/snippets/[id]/page.tsx | 14 + apps/web/src/app/(protected)/error.tsx | 12 + apps/web/src/app/(protected)/layout.tsx | 31 + .../(protected)/layout/content.tsx} | 16 +- .../(protected)/layout}/header.tsx | 12 +- apps/web/src/app/(protected)/not-found.tsx | 13 + .../src/app/(public)/[...not-found]/page.tsx | 8 + apps/web/src/app/(public)/auth/fail/page.tsx | 31 + .../app/(public)/auth/signup-success/page.tsx | 27 + .../app/(public)/auth/success/container.tsx | 23 + .../src/app/(public)/auth/success/page.tsx | 24 + apps/web/src/app/(public)/error.tsx | 12 + apps/web/src/app/(public)/layout.tsx | 29 + .../public => app/(public)/layout}/footer.tsx | 0 .../public => app/(public)/layout}/header.tsx | 7 +- apps/web/src/app/(public)/not-found.tsx | 13 + .../app/(public)/page.test.tsx} | 18 +- apps/web/src/app/(public)/page.tsx | 36 + .../web/src/app/(public)/signin/container.tsx | 111 ++ apps/web/src/app/(public)/signin/page.tsx | 19 + .../web/src/app/(public)/signup/container.tsx | 137 ++ apps/web/src/app/(public)/signup/page.tsx | 19 + apps/web/src/app/global-error.tsx | 28 + apps/web/src/app/manifest.ts | 27 + apps/web/src/app/robot.ts | 18 + apps/web/src/app/sitemap.ts | 16 + apps/web/src/components/auth/auth-alert.tsx | 4 +- apps/web/src/components/common/error.tsx | 52 + apps/web/src/components/common/loader.tsx | 8 +- .../common}/page-not-found.tsx | 6 +- apps/web/src/components/common/redirect.tsx | 10 +- .../src/components/home/feature-section.tsx | 4 +- apps/web/src/components/home/hero-section.tsx | 4 +- .../home/newsletter/newsletter-alert.tsx | 4 +- .../home/newsletter/newsletter-form.test.tsx} | 22 +- .../home/newsletter/newsletter-form.tsx | 11 +- .../home/newsletter/newsletter-section.tsx | 12 +- apps/web/src/components/layout/main.tsx | 22 - .../layout/public/public-layout.tsx | 20 - apps/web/src/components/seo/seo.tsx | 48 - .../components/snippets/public-snippet.tsx | 4 +- apps/web/src/containers/auth/login.tsx | 116 -- apps/web/src/containers/auth/signup.tsx | 143 --- apps/web/src/containers/home/index.tsx | 19 - .../src/containers/private/folders/view.tsx | 38 - apps/web/src/containers/private/home.tsx | 29 - apps/web/src/containers/private/profile.tsx | 29 - .../src/containers/private/snippets/view.tsx | 42 - apps/web/src/hooks/authentication/use-auth.ts | 14 +- .../use-set-authenticated-user.ts | 12 +- apps/web/src/hooks/use-boolean-state.ts | 4 +- apps/web/src/hooks/use-click-outside.ts | 4 +- apps/web/src/hooks/use-folder-directory.ts | 4 +- apps/web/src/lib/apollo/client.tsx | 59 + apps/web/src/lib/apollo/server.ts | 33 + apps/web/src/{utils => lib}/constants.ts | 4 +- apps/web/src/lib/errors.ts | 7 + apps/web/src/{utils => lib}/forms.ts | 0 apps/web/src/lib/seo.ts | 70 + apps/web/src/pages/404.tsx | 15 - apps/web/src/pages/_app.tsx | 19 - apps/web/src/pages/_error.tsx | 64 - apps/web/src/pages/app/browse.tsx | 42 - apps/web/src/pages/app/folders/[id].tsx | 9 - apps/web/src/pages/app/home.tsx | 9 - apps/web/src/pages/app/profile.tsx | 9 - apps/web/src/pages/app/snippets/[id].tsx | 9 - apps/web/src/pages/auth/fail.tsx | 31 - apps/web/src/pages/auth/signup-success.tsx | 27 - apps/web/src/pages/auth/success.tsx | 30 - apps/web/src/pages/index.tsx | 10 - apps/web/src/pages/signin.tsx | 29 - apps/web/src/pages/signup.tsx | 10 - apps/web/src/utils/apollo-client.ts | 120 -- apps/web/tailwind.config.js | 13 +- .../{__mocks__ => tests/mocks}/fileMock.js | 0 .../{__mocks__ => tests/mocks}/styleMock.js | 0 .../setup/jest.setup.ts => tests/setup.ts} | 0 apps/web/tsconfig.json | 38 +- apps/web/vitest.config.ts | 19 + packages/front/components/directory/index.tsx | 2 + packages/front/components/toast/provider.tsx | 2 + .../services/snippets/public-snippets.ts | 6 +- yarn.lock | 1140 ++++++++++++++++- 111 files changed, 2392 insertions(+), 1231 deletions(-) create mode 100644 .changeset/tender-oranges-matter.md create mode 100644 apps/web/.env.test delete mode 100644 apps/web/jest.config.ts delete mode 100644 apps/web/next-sitemap.js create mode 100644 apps/web/public/android-chrome-192x192.png create mode 100644 apps/web/public/android-chrome-512x512.png create mode 100644 apps/web/public/apple-touch-icon.png create mode 100644 apps/web/public/favicon-16x16.png create mode 100644 apps/web/public/favicon-32x32.png rename apps/web/src/{containers/private/browse.tsx => app/(protected)/app/browse/container.tsx} (54%) create mode 100644 apps/web/src/app/(protected)/app/browse/lib/fetch-snippets.ts create mode 100644 apps/web/src/app/(protected)/app/browse/page.tsx create mode 100644 apps/web/src/app/(protected)/app/folders/[id]/container.tsx create mode 100644 apps/web/src/app/(protected)/app/folders/[id]/page.tsx create mode 100644 apps/web/src/app/(protected)/app/home/container.tsx create mode 100644 apps/web/src/app/(protected)/app/home/page.tsx create mode 100644 apps/web/src/app/(protected)/app/profile/container.tsx create mode 100644 apps/web/src/app/(protected)/app/profile/page.tsx create mode 100644 apps/web/src/app/(protected)/app/snippets/[id]/container.tsx create mode 100644 apps/web/src/app/(protected)/app/snippets/[id]/page.tsx create mode 100644 apps/web/src/app/(protected)/error.tsx create mode 100644 apps/web/src/app/(protected)/layout.tsx rename apps/web/src/{components/layout/private/layout.tsx => app/(protected)/layout/content.tsx} (68%) rename apps/web/src/{components/layout/private => app/(protected)/layout}/header.tsx (97%) create mode 100644 apps/web/src/app/(protected)/not-found.tsx create mode 100644 apps/web/src/app/(public)/[...not-found]/page.tsx create mode 100644 apps/web/src/app/(public)/auth/fail/page.tsx create mode 100644 apps/web/src/app/(public)/auth/signup-success/page.tsx create mode 100644 apps/web/src/app/(public)/auth/success/container.tsx create mode 100644 apps/web/src/app/(public)/auth/success/page.tsx create mode 100644 apps/web/src/app/(public)/error.tsx create mode 100644 apps/web/src/app/(public)/layout.tsx rename apps/web/src/{components/layout/public => app/(public)/layout}/footer.tsx (100%) rename apps/web/src/{components/layout/public => app/(public)/layout}/header.tsx (98%) create mode 100644 apps/web/src/app/(public)/not-found.tsx rename apps/web/{__tests__/ui/home.test.tsx => src/app/(public)/page.test.tsx} (66%) create mode 100644 apps/web/src/app/(public)/page.tsx create mode 100644 apps/web/src/app/(public)/signin/container.tsx create mode 100644 apps/web/src/app/(public)/signin/page.tsx create mode 100644 apps/web/src/app/(public)/signup/container.tsx create mode 100644 apps/web/src/app/(public)/signup/page.tsx create mode 100644 apps/web/src/app/global-error.tsx create mode 100644 apps/web/src/app/manifest.ts create mode 100644 apps/web/src/app/robot.ts create mode 100644 apps/web/src/app/sitemap.ts create mode 100644 apps/web/src/components/common/error.tsx rename apps/web/src/{containers => components/common}/page-not-found.tsx (95%) rename apps/web/{__tests__/ui/newsletter.test.tsx => src/components/home/newsletter/newsletter-form.test.tsx} (78%) delete mode 100644 apps/web/src/components/layout/main.tsx delete mode 100644 apps/web/src/components/layout/public/public-layout.tsx delete mode 100644 apps/web/src/components/seo/seo.tsx delete mode 100644 apps/web/src/containers/auth/login.tsx delete mode 100644 apps/web/src/containers/auth/signup.tsx delete mode 100644 apps/web/src/containers/home/index.tsx delete mode 100644 apps/web/src/containers/private/folders/view.tsx delete mode 100644 apps/web/src/containers/private/home.tsx delete mode 100644 apps/web/src/containers/private/profile.tsx delete mode 100644 apps/web/src/containers/private/snippets/view.tsx create mode 100644 apps/web/src/lib/apollo/client.tsx create mode 100644 apps/web/src/lib/apollo/server.ts rename apps/web/src/{utils => lib}/constants.ts (86%) create mode 100644 apps/web/src/lib/errors.ts rename apps/web/src/{utils => lib}/forms.ts (100%) create mode 100644 apps/web/src/lib/seo.ts delete mode 100644 apps/web/src/pages/404.tsx delete mode 100644 apps/web/src/pages/_app.tsx delete mode 100644 apps/web/src/pages/_error.tsx delete mode 100644 apps/web/src/pages/app/browse.tsx delete mode 100644 apps/web/src/pages/app/folders/[id].tsx delete mode 100644 apps/web/src/pages/app/home.tsx delete mode 100644 apps/web/src/pages/app/profile.tsx delete mode 100644 apps/web/src/pages/app/snippets/[id].tsx delete mode 100644 apps/web/src/pages/auth/fail.tsx delete mode 100644 apps/web/src/pages/auth/signup-success.tsx delete mode 100644 apps/web/src/pages/auth/success.tsx delete mode 100644 apps/web/src/pages/index.tsx delete mode 100644 apps/web/src/pages/signin.tsx delete mode 100644 apps/web/src/pages/signup.tsx delete mode 100644 apps/web/src/utils/apollo-client.ts rename apps/web/{__mocks__ => tests/mocks}/fileMock.js (100%) rename apps/web/{__mocks__ => tests/mocks}/styleMock.js (100%) rename apps/web/{__tests__/setup/jest.setup.ts => tests/setup.ts} (100%) create mode 100644 apps/web/vitest.config.ts 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 0000000000000000000000000000000000000000..cbbf9836d4e85db35e2362d302f07ba8d39a3267 GIT binary patch literal 15660 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 literal 0 HcmV?d00001 diff --git a/apps/web/public/android-chrome-512x512.png b/apps/web/public/android-chrome-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..a14f2c006a18a6f8d320668c8a73e74cb7ebdf92 GIT binary patch literal 40481 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 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"