diff --git a/apps/studio/app/layout.tsx b/apps/studio/app/layout.tsx new file mode 100644 index 00000000000..39d9c8577ba --- /dev/null +++ b/apps/studio/app/layout.tsx @@ -0,0 +1,215 @@ +'use client' + +import 'react-data-grid/lib/styles.css' +import 'styles/code.scss' +import 'styles/contextMenu.scss' +import 'styles/date-picker.scss' +import 'styles/editor.scss' +import 'styles/graphiql-base.scss' +import 'styles/grid.scss' +import 'styles/main.scss' +import 'styles/markdown-preview.scss' +import 'styles/monaco.scss' +import 'styles/react-data-grid-logs.scss' +import 'styles/reactflow.scss' +import 'styles/storage.scss' +import 'styles/stripe.scss' +import 'styles/toast.scss' +import 'styles/ui.scss' +import 'ui/build/css/themes/dark.css' +import 'ui/build/css/themes/light.css' + +import { loader } from '@monaco-editor/react' +import { TooltipProvider } from '@radix-ui/react-tooltip' +import * as Sentry from '@sentry/nextjs' +import { SessionContextProvider } from '@supabase/auth-helpers-react' +import { createClient } from '@supabase/supabase-js' +// import { HydrationBoundary, QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { ReactQueryDevtools } from '@tanstack/react-query-devtools' +import { ThemeProvider, useThemeSandbox } from 'common' +import dayjs from 'dayjs' +import customParseFormat from 'dayjs/plugin/customParseFormat' +import relativeTime from 'dayjs/plugin/relativeTime' +import timezone from 'dayjs/plugin/timezone' +import utc from 'dayjs/plugin/utc' +import Head from 'next/head' +import { ErrorInfo, useEffect, useMemo, useRef, useState } from 'react' +import { ErrorBoundary } from 'react-error-boundary' +import { toast } from 'sonner' + +import MetaFaviconsAppRouter from 'common/MetaFavicons/app-router-head' +import { AppBannerWrapper, RouteValidationWrapper } from 'components/interfaces/App' +import { AppBannerContextProvider } from 'components/interfaces/App/AppBannerWrapperContext' +import { StudioCommandMenu } from 'components/interfaces/App/CommandMenu' +import { FeaturePreviewContextProvider } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' +import FeaturePreviewModal from 'components/interfaces/App/FeaturePreview/FeaturePreviewModal' +import { GenerateSql } from 'components/interfaces/SqlGenerator/SqlGenerator' +import { ErrorBoundaryState } from 'components/ui/ErrorBoundaryState' +import FlagProvider from 'components/ui/Flag/FlagProvider' +import PageTelemetry from 'components/ui/PageTelemetry' +import { useRootQueryClient } from 'data/query-client' +import { AuthProvider } from 'lib/auth' +import { BASE_PATH, IS_PLATFORM, LOCAL_STORAGE_KEYS } from 'lib/constants' +import { ProfileProvider } from 'lib/profile' +import { useAppStateSnapshot } from 'state/app-state' +import HCaptchaLoadedStore from 'stores/hcaptcha-loaded-store' +import { AppPropsWithLayout } from 'types' +import { SonnerToaster } from 'ui' +import { CommandProvider } from 'ui-patterns/CommandMenu' +import { ConsentToast } from 'ui-patterns/ConsentToast' +import { ReactQueryProvider } from './react-query-provider' +import { NextIntlClientProvider } from 'next-intl' + +dayjs.extend(customParseFormat) +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(relativeTime) + +loader.config({ + // [Joshen] Attempt for offline support/bypass ISP issues is to store the assets required for monaco + // locally. We're however, only storing the assets which we need (based on what the network tab loads + // while using monaco). If we end up facing more effort trying to maintain this, probably to either + // use cloudflare or find some way to pull all the files from a CDN via a CLI, rather than tracking individual files + // The alternative was to import * as monaco from 'monaco-editor' but i couldn't get it working + paths: { + vs: IS_PLATFORM + ? 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.37.0/min/vs' + : `${BASE_PATH}/monaco-editor`, + }, +}) + +// [Joshen TODO] Once we settle on the new nav layout - we'll need a lot of clean up in terms of our layout components +// a lot of them are unnecessary and introduce way too many cluttered CSS especially with the height styles that make +// debugging way too difficult. Ideal scenario is we just have one AppLayout to control the height and scroll areas of +// the dashboard, all other layout components should not be doing that + +function RootLayout({ children }: { children: React.ReactNode }) { + // const [queryClient] = useState(() => new QueryClient()) + + const snap = useAppStateSnapshot() + const consentToastId = useRef() + + // [Joshen] Some issues with using createBrowserSupabaseClient + const [supabase] = useState(() => + IS_PLATFORM + ? createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL as string, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY as string + ) + : undefined + ) + + const AuthContainer = useMemo( + // eslint-disable-next-line react/display-name + () => (props: any) => { + return IS_PLATFORM ? ( + + {props.children} + + ) : ( + {props.children} + ) + }, + [supabase] + ) + + const errorBoundaryHandler = (error: Error, info: ErrorInfo) => { + Sentry.withScope(function (scope) { + scope.setTag('globalErrorBoundary', true) + Sentry.captureException(error) + }) + + console.error(error.stack) + } + + useEffect(() => { + // Check for telemetry consent + if (typeof window !== 'undefined') { + const onAcceptConsent = () => { + snap.setIsOptedInTelemetry(true) + if (consentToastId.current) toast.dismiss(consentToastId.current) + } + + const onOptOut = () => { + snap.setIsOptedInTelemetry(false) + if (consentToastId.current) toast.dismiss(consentToastId.current) + } + + const hasAcknowledgedConsent = localStorage.getItem(LOCAL_STORAGE_KEYS.TELEMETRY_CONSENT) + if (IS_PLATFORM && hasAcknowledgedConsent === null) { + setTimeout(() => { + consentToastId.current = toast( + , + { + id: 'consent-toast', + position: 'bottom-right', + duration: Infinity, + } + ) + }, 300) + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + useThemeSandbox() + + const isTestEnv = process.env.NEXT_PUBLIC_NODE_ENV === 'test' + + return ( + + + Supabase + + + + + + + + + + + + + + + + + + + {children} + + + + + + + + + + + + + + {!isTestEnv && } + {!isTestEnv && ( + + )} + + + + + + + + + ) +} + +export default RootLayout diff --git a/apps/studio/app/projects/page.tsx b/apps/studio/app/projects/page.tsx new file mode 100644 index 00000000000..6e126d729ec --- /dev/null +++ b/apps/studio/app/projects/page.tsx @@ -0,0 +1,108 @@ +'use client' + +import { useRouter } from 'next/router' +import { useEffect, useState } from 'react' + +import { ProjectList } from 'components/interfaces/Home/ProjectList' +import HomePageActions from 'components/interfaces/HomePageActions' +import AccountLayout from 'components/layouts/AccountLayout/AccountLayout' +import AlertError from 'components/ui/AlertError' +import { Loading } from 'components/ui/Loading' +import { useOrganizationsQuery } from 'data/organizations/organizations-query' +import { useAutoProjectsPrefetch } from 'data/projects/projects-query' +import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' +import { useFlag } from 'hooks/ui/useFlag' +import { IS_PLATFORM, LOCAL_STORAGE_KEYS, PROJECT_STATUS } from 'lib/constants' +import type { NextPageWithLayout } from 'types' + +const ProjectsPage = () => { + // const router = useRouter() + const [search, setSearch] = useState('') + const [filterStatus, setFilterStatus] = useState([ + PROJECT_STATUS.ACTIVE_HEALTHY, + PROJECT_STATUS.INACTIVE, + ]) + const { data: organizations, isError, isSuccess } = useOrganizationsQuery() + useAutoProjectsPrefetch() + + const projectCreationEnabled = useIsFeatureEnabled('projects:create') + + const navLayoutV2 = useFlag('navigationLayoutV2') + const hasWindowLoaded = typeof window !== 'undefined' + + useEffect(() => { + if (navLayoutV2 && isSuccess && hasWindowLoaded) { + const localStorageSlug = localStorage.getItem( + LOCAL_STORAGE_KEYS.RECENTLY_VISITED_ORGANIZATION + ) + const verifiedSlug = organizations.some((org) => org.slug === localStorageSlug) + + // if (organizations.length === 0) router.push('/new') + // else if (localStorageSlug && verifiedSlug) router.push(`/org/${localStorageSlug}`) + // else router.push(`/org/${organizations[0].slug}`) + } + + if (!navLayoutV2 && isSuccess && hasWindowLoaded) { + // navigate to new page exactly once + const hasNoOrg = organizations.length === 0 + const hasShownNewPage = localStorage.getItem(LOCAL_STORAGE_KEYS.UI_ONBOARDING_NEW_PAGE_SHOWN) + if (hasNoOrg && !hasShownNewPage) { + localStorage.setItem(LOCAL_STORAGE_KEYS.UI_ONBOARDING_NEW_PAGE_SHOWN, 'true') + // router.push('/new') + } + } + }, [navLayoutV2, isSuccess, hasWindowLoaded]) + + return ( + <> + {isError && ( +
+ +
+ )} + + {navLayoutV2 && ( +
+ +
+ )} + {!navLayoutV2 && ( +
+ {IS_PLATFORM && projectCreationEnabled && isSuccess && ( + + )} +
+ setFilterStatus(['ACTIVE_HEALTHY', 'INACTIVE'])} + /> +
+
+ )} + + ) +} + +// ProjectsPage.getLayout = (page) => +// // +// ({ page }) +// // + +export default ProjectsPage diff --git a/apps/studio/app/react-query-provider.tsx b/apps/studio/app/react-query-provider.tsx new file mode 100644 index 00000000000..66cec8c2314 --- /dev/null +++ b/apps/studio/app/react-query-provider.tsx @@ -0,0 +1,11 @@ +// app/providers.jsx +'use client' + +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { useState } from 'react' + +export function ReactQueryProvider({ children }: { children: React.ReactNode }) { + const [queryClient] = useState(() => new QueryClient()) + + return {children} +} diff --git a/apps/studio/app/something/page.tsx b/apps/studio/app/something/page.tsx new file mode 100644 index 00000000000..2ea2f570834 --- /dev/null +++ b/apps/studio/app/something/page.tsx @@ -0,0 +1,7 @@ +// make a normal page in nextjs app router + +import { Button } from 'ui' + +export default function Something() { + return +} diff --git a/apps/studio/components/interfaces/App/RouteValidationWrapper.tsx b/apps/studio/components/interfaces/App/RouteValidationWrapper.tsx index a81f971a346..175346ec457 100644 --- a/apps/studio/components/interfaces/App/RouteValidationWrapper.tsx +++ b/apps/studio/components/interfaces/App/RouteValidationWrapper.tsx @@ -1,4 +1,3 @@ -import { useRouter } from 'next/router' import { PropsWithChildren, useEffect } from 'react' import { toast } from 'sonner' @@ -9,10 +8,12 @@ import useLatest from 'hooks/misc/useLatest' import { useFlag } from 'hooks/ui/useFlag' import { DEFAULT_HOME, IS_PLATFORM, LOCAL_STORAGE_KEYS } from 'lib/constants' import { useAppStateSnapshot } from 'state/app-state' +import { usePathname, useRouter } from 'next/navigation' // Ideally these could all be within a _middleware when we use Next 12 const RouteValidationWrapper = ({ children }: PropsWithChildren<{}>) => { const router = useRouter() + const pathName = usePathname() const { ref, slug, id } = useParams() const navLayoutV2 = useFlag('navigationLayoutV2') @@ -37,7 +38,7 @@ const RouteValidationWrapper = ({ children }: PropsWithChildren<{}>) => { * @returns a boolean */ function isExceptUrl() { - return excemptUrls.includes(router?.pathname) + return excemptUrls.includes(pathName ?? '') } const { data: organizations, isSuccess: orgsInitialized } = useOrganizationsQuery({ @@ -57,6 +58,7 @@ const RouteValidationWrapper = ({ children }: PropsWithChildren<{}>) => { if (!isValidOrg) { toast.error('This organization does not exist') router.push(navLayoutV2 ? `/org/${organizations[0].slug}` : DEFAULT_HOME) + return } } @@ -113,10 +115,10 @@ const RouteValidationWrapper = ({ children }: PropsWithChildren<{}>) => { useEffect(() => { if (ref !== undefined && id !== undefined) { - if (router.pathname.endsWith('/sql/[id]') && id !== 'new') { + if (pathName?.endsWith('/sql/[id]') && id !== 'new') { snap.setDashboardHistory(ref, 'sql', id) } - if (router.pathname.endsWith('/editor/[id]')) { + if (pathName?.endsWith('/editor/[id]')) { snap.setDashboardHistory(ref, 'editor', id) } } diff --git a/apps/studio/components/interfaces/HomePageActions.tsx b/apps/studio/components/interfaces/HomePageActions.tsx index 30896319192..9536a8dc80c 100644 --- a/apps/studio/components/interfaces/HomePageActions.tsx +++ b/apps/studio/components/interfaces/HomePageActions.tsx @@ -1,6 +1,6 @@ import { Filter, Search } from 'lucide-react' import Link from 'next/link' -import { useRouter } from 'next/router' +import { useRouter } from 'next/navigation' import { IS_PLATFORM } from 'common' import { useOrganizationsQuery } from 'data/organizations/organizations-query' diff --git a/apps/studio/components/ui/ComputeBadgeWrapper.tsx b/apps/studio/components/ui/ComputeBadgeWrapper.tsx index 511e9f4ccc6..1c885c6c674 100644 --- a/apps/studio/components/ui/ComputeBadgeWrapper.tsx +++ b/apps/studio/components/ui/ComputeBadgeWrapper.tsx @@ -1,4 +1,4 @@ -import { useRouter } from 'next/router' +import { useRouter } from 'next/navigation' import { MouseEvent, useState } from 'react' import { components } from 'api-types' diff --git a/apps/studio/components/ui/ErrorBoundaryState.tsx b/apps/studio/components/ui/ErrorBoundaryState.tsx index dfbaf1f3485..0fabe5baf75 100644 --- a/apps/studio/components/ui/ErrorBoundaryState.tsx +++ b/apps/studio/components/ui/ErrorBoundaryState.tsx @@ -1,14 +1,14 @@ import { ExternalLink } from 'lucide-react' import Link from 'next/link' -import { useRouter } from 'next/router' +import { usePathname } from 'next/navigation' import type { FallbackProps } from 'react-error-boundary' import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button } from 'ui' import { WarningIcon } from 'ui' export const ErrorBoundaryState = ({ error, resetErrorBoundary }: FallbackProps) => { - const router = useRouter() - const message = `Path name: ${router.pathname}\n\n${error.stack}` + const pathname = usePathname() + const message = `Path name: ${pathname}\n\n${error.stack}` const isRemoveChildError = error.message.includes("Failed to execute 'removeChild' on 'Node'") return ( @@ -61,7 +61,10 @@ export const ErrorBoundaryState = ({ error, resetErrorBoundary }: FallbackProps) Return to dashboard ) : ( - )} diff --git a/apps/studio/components/ui/PageTelemetry.tsx b/apps/studio/components/ui/PageTelemetry.tsx index 88e10f0beab..77097968b99 100644 --- a/apps/studio/components/ui/PageTelemetry.tsx +++ b/apps/studio/components/ui/PageTelemetry.tsx @@ -1,6 +1,6 @@ import { Sha256 } from '@aws-crypto/sha256-browser' import * as Sentry from '@sentry/nextjs' -import { useRouter } from 'next/router' +import { useRouter, usePathname } from 'next/navigation' import { PropsWithChildren, useEffect, useState } from 'react' import { useParams, useUser } from 'common' @@ -24,13 +24,14 @@ const getAnonId = async (id: string) => { } const PageTelemetry = ({ children }: PropsWithChildren<{}>) => { + const pathname = usePathname() const user = useUser() const router = useRouter() const { ref, slug } = useParams() const snap = useAppStateSnapshot() const organization = useSelectedOrganization() - const previousPathname = usePrevious(router.pathname) + const previousPathname = usePrevious(pathname) const enablePostHogTelemetry = useFlag('enablePosthogChanges') const consent = typeof window !== 'undefined' @@ -54,26 +55,30 @@ const PageTelemetry = ({ children }: PropsWithChildren<{}>) => { useEffect(() => { function handleRouteChange() { - if (snap.isOptedInTelemetry) handlePageTelemetry(window.location.href) + // Check telemetry consent and send page telemetry if opted in + if (snap.isOptedInTelemetry) { + handlePageTelemetry(window.location.href) + } } - // Listen for page changes after a navigation or when the query changes - router.events.on('routeChangeComplete', handleRouteChange) + // Send telemetry on route change or when query changes + handleRouteChange() + return () => { - router.events.off('routeChangeComplete', handleRouteChange) + // No need for event cleanup here in App Router } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [router, snap.isOptedInTelemetry]) + }, [pathname, snap.isOptedInTelemetry]) useEffect(() => { // Send page telemetry on first page load - // Waiting for router ready before sending page_view - // if not the path will be dynamic route instead of the browser url - if (router.isReady && snap.isOptedInTelemetry) { + // Waiting for pathname to be available before sending page_view + // If not, the path might be a dynamic route instead of the browser URL + if (pathname && snap.isOptedInTelemetry) { handlePageTelemetry(window.location.href) } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [router.isReady, snap.isOptedInTelemetry]) + }, [pathname, snap.isOptedInTelemetry]) useEffect(() => { // don't set the sentry user id if the user hasn't logged in (so that Sentry errors show null user id instead of anonymous id) @@ -95,19 +100,17 @@ const PageTelemetry = ({ children }: PropsWithChildren<{}>) => { }, [user?.id]) useEffect(() => { - const isLandingOnProjectRoute = - router.pathname.includes('[ref]') && previousPathname === router.pathname + const isLandingOnProjectRoute = pathname?.includes('[ref]') && previousPathname === pathname const isEnteringProjectRoute = - !(previousPathname ?? '').includes('[ref]') && router.pathname.includes('[ref]') + !(previousPathname ?? '').includes('[ref]') && pathname?.includes('[ref]') const isLeavingProjectRoute = - (previousPathname ?? '').includes('[ref]') && !router.pathname.includes('[ref]') + (previousPathname ?? '').includes('[ref]') && !pathname?.includes('[ref]') - const isLandingOnOrgRoute = - router.pathname.includes('[slug]') && previousPathname === router.pathname + const isLandingOnOrgRoute = pathname?.includes('[slug]') && previousPathname === pathname const isEnteringOrgRoute = - !(previousPathname ?? '').includes('[slug]') && router.pathname.includes('[slug]') + !(previousPathname ?? '').includes('[slug]') && pathname?.includes('[slug]') const isLeavingOrgRoute = - (previousPathname ?? '').includes('[slug]') && !router.pathname.includes('[slug]') + (previousPathname ?? '').includes('[slug]') && !pathname?.includes('[slug]') if (trackTelemetryPH) { if (ref && (isLandingOnProjectRoute || isEnteringProjectRoute)) { @@ -123,7 +126,7 @@ const PageTelemetry = ({ children }: PropsWithChildren<{}>) => { } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [trackTelemetryPH, slug, ref, router.pathname]) + }, [trackTelemetryPH, slug, ref, pathname]) useEffect(() => { const handleBeforeUnload = async () => { diff --git a/apps/studio/data/telemetry/send-event-mutation.ts b/apps/studio/data/telemetry/send-event-mutation.ts index 6fda99f2f3a..17829c43d77 100644 --- a/apps/studio/data/telemetry/send-event-mutation.ts +++ b/apps/studio/data/telemetry/send-event-mutation.ts @@ -5,7 +5,8 @@ import { isBrowser } from 'common' import { handleError, post } from 'data/fetchers' import { useFlag } from 'hooks/ui/useFlag' import { LOCAL_STORAGE_KEYS } from 'lib/constants' -import { useRouter } from 'next/router' +import { usePathname } from 'next/navigation' +import { useLocale } from 'next-intl' import type { ResponseError } from 'types' type SendEventGA = components['schemas']['TelemetryEventBody'] @@ -47,7 +48,9 @@ export const useSendEventMutation = ({ UseMutationOptions, 'mutationFn' > = {}) => { - const router = useRouter() + const pathName = usePathname() + const locale = useLocale() + const usePostHogParameters = useFlag('enablePosthogChanges') const title = typeof document !== 'undefined' ? document?.title : '' @@ -57,10 +60,10 @@ export const useSendEventMutation = ({ ? ({ page_url: window.location.href, page_title: title, - pathname: router.pathname, + pathname: pathName, ph: { referrer, - language: router?.locale ?? 'en-US', + language: locale ?? 'en-US', user_agent: navigator.userAgent, search: window.location.search, viewport_height: isBrowser ? window.innerHeight : 0, @@ -71,10 +74,10 @@ export const useSendEventMutation = ({ : ({ page_referrer: referrer, page_title: title, - page_location: router.asPath.split('#')[0], + page_location: pathName?.split('#')[0], ga: { screen_resolution: isBrowser ? `${window.innerWidth}x${window.innerHeight}` : undefined, - language: router?.locale ?? 'en-US', + language: locale ?? 'en-US', }, } as SendEventGA) diff --git a/apps/studio/data/telemetry/send-identify-mutation.ts b/apps/studio/data/telemetry/send-identify-mutation.ts index b17b26ddda4..3e65917d3e8 100644 --- a/apps/studio/data/telemetry/send-identify-mutation.ts +++ b/apps/studio/data/telemetry/send-identify-mutation.ts @@ -6,7 +6,7 @@ import { handleError, post } from 'data/fetchers' import { Profile } from 'data/profile/types' import { useFlag } from 'hooks/ui/useFlag' import { LOCAL_STORAGE_KEYS } from 'lib/constants' -import { useRouter } from 'next/router' +import { useLocale } from 'next-intl' import type { ResponseError } from 'types' type SendIdentifyGA = components['schemas']['TelemetryIdentifyBody'] @@ -47,7 +47,7 @@ export const useSendIdentifyMutation = ({ UseMutationOptions, 'mutationFn' > = {}) => { - const router = useRouter() + const locale = useLocale() const usePostHogParameters = useFlag('enablePosthogChanges') const payload = usePostHogParameters @@ -55,7 +55,7 @@ export const useSendIdentifyMutation = ({ : ({ ga: { screen_resolution: isBrowser ? `${window.innerWidth}x${window.innerHeight}` : undefined, - language: router?.locale ?? 'en-US', + language: locale ?? 'en-US', }, } as SendIdentifyGA) diff --git a/apps/studio/data/telemetry/send-page-leave-mutation.ts b/apps/studio/data/telemetry/send-page-leave-mutation.ts index 9eaa46fcdd4..cc2e2982efd 100644 --- a/apps/studio/data/telemetry/send-page-leave-mutation.ts +++ b/apps/studio/data/telemetry/send-page-leave-mutation.ts @@ -2,7 +2,7 @@ import { useMutation, UseMutationOptions } from '@tanstack/react-query' import { components } from 'api-types' import { handleError, post } from 'data/fetchers' -import { useRouter } from 'next/router' +import { usePathname } from 'next/navigation' import type { ResponseError } from 'types' type SendPageLeaveBody = components['schemas']['TelemetryPageLeaveBody'] @@ -23,14 +23,14 @@ export const useSendPageLeaveMutation = ({ onError, ...options }: Omit, 'mutationFn'> = {}) => { - const router = useRouter() + const pathname = usePathname() const url = typeof window !== 'undefined' ? window.location.href : '' const title = typeof document !== 'undefined' ? document?.title : '' const body = { page_url: url, page_title: title, - pathname: router.pathname, + pathname: pathname, } as SendPageLeaveBody return useMutation((vars) => sendPageLeave(body), { diff --git a/apps/studio/data/telemetry/send-page-mutation.ts b/apps/studio/data/telemetry/send-page-mutation.ts index 5bf477abe06..8d15505c32b 100644 --- a/apps/studio/data/telemetry/send-page-mutation.ts +++ b/apps/studio/data/telemetry/send-page-mutation.ts @@ -4,7 +4,8 @@ import { components } from 'api-types' import { isBrowser } from 'common' import { handleError, post } from 'data/fetchers' import { useFlag } from 'hooks/ui/useFlag' -import { useRouter } from 'next/router' +import { useLocale } from 'next-intl' +import { usePathname } from 'next/navigation' import type { ResponseError } from 'types' type SendPageGA = components['schemas']['TelemetryPageBody'] @@ -34,7 +35,8 @@ export const useSendPageMutation = ({ onError, ...options }: Omit, 'mutationFn'> = {}) => { - const router = useRouter() + const pathname = usePathname() + const locale = useLocale() const usePostHogParameters = useFlag('enablePosthogChanges') const title = typeof document !== 'undefined' ? document?.title : '' @@ -43,10 +45,10 @@ export const useSendPageMutation = ({ const payload = usePostHogParameters ? ({ page_title: title, - pathname: router.pathname, + pathname: pathname, ph: { referrer, - language: router?.locale ?? 'en-US', + language: locale ?? 'en-US', user_agent: navigator.userAgent, search: window.location.search, viewport_height: isBrowser ? window.innerHeight : 0, @@ -58,7 +60,7 @@ export const useSendPageMutation = ({ referrer, ga: { screen_resolution: isBrowser ? `${window.innerWidth}x${window.innerHeight}` : undefined, - language: router?.locale ?? 'en-US', + language: locale ?? 'en-US', }, } as SendPageGA) diff --git a/apps/studio/hooks/ui/useFlag.ts b/apps/studio/hooks/ui/useFlag.ts index 439e1800416..8701a693aa7 100644 --- a/apps/studio/hooks/ui/useFlag.ts +++ b/apps/studio/hooks/ui/useFlag.ts @@ -1,3 +1,5 @@ +'use client' + import { useContext } from 'react' import FlagContext from 'components/ui/Flag/FlagContext' diff --git a/apps/studio/lib/profile.tsx b/apps/studio/lib/profile.tsx index eab9cd5e4ff..2907a681b14 100644 --- a/apps/studio/lib/profile.tsx +++ b/apps/studio/lib/profile.tsx @@ -83,6 +83,8 @@ export const ProfileProvider = ({ children }: PropsWithChildren<{}>) => { isSuccess, ]) + // return <>{children} + return {children} } diff --git a/apps/studio/next-env.d.ts b/apps/studio/next-env.d.ts index a4a7b3f5cfa..725dd6f2451 100644 --- a/apps/studio/next-env.d.ts +++ b/apps/studio/next-env.d.ts @@ -1,5 +1,6 @@ /// /// +/// // NOTE: This file should not be edited -// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/apps/studio/package.json b/apps/studio/package.json index 55ad9b69b3d..0f6c9b82120 100644 --- a/apps/studio/package.json +++ b/apps/studio/package.json @@ -77,6 +77,7 @@ "monaco-editor": "0.33.0", "motion-number": "^0.1.7", "next": "^14.2.10", + "next-intl": "^3.21.1", "next-themes": "^0.3.0", "nuqs": "^1.17.8", "openai": "^4.20.1", diff --git a/apps/studio/pages/_app.tsx b/apps/studio/pages/_app.tsx index 8e5989df167..d6364050cb2 100644 --- a/apps/studio/pages/_app.tsx +++ b/apps/studio/pages/_app.tsx @@ -55,6 +55,7 @@ import { AppPropsWithLayout } from 'types' import { SonnerToaster } from 'ui' import { CommandProvider } from 'ui-patterns/CommandMenu' import { ConsentToast } from 'ui-patterns/ConsentToast' +import { NextIntlClientProvider } from 'next-intl' dayjs.extend(customParseFormat) dayjs.extend(utc) @@ -159,42 +160,46 @@ function CustomApp({ Component, pageProps }: AppPropsWithLayout) { - - - Supabase - - - - - - - - - - - - {getLayout()} - - - - - - - - - - - - + + + + Supabase + + + + + + + + + + + + {getLayout()} + + + + + + + + + + + + - {!isTestEnv && } - {!isTestEnv && } - + {!isTestEnv && } + {!isTestEnv && ( + + )} + + diff --git a/apps/studio/pages/projects.tsx b/apps/studio/pages/projects-old.tsx similarity index 100% rename from apps/studio/pages/projects.tsx rename to apps/studio/pages/projects-old.tsx diff --git a/package-lock.json b/package-lock.json index 884657ab1ac..55d020402c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1863,6 +1863,7 @@ "monaco-editor": "0.33.0", "motion-number": "^0.1.7", "next": "^14.2.10", + "next-intl": "^3.21.1", "next-themes": "^0.3.0", "nuqs": "^1.17.8", "openai": "^4.20.1", @@ -6903,6 +6904,51 @@ "version": "0.1.6", "license": "MIT" }, + "node_modules/@formatjs/ecma402-abstract": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.1.0.tgz", + "integrity": "sha512-SE2V2PE03K9U/YQZ3nxEOysRkQ/CfSwLHR789Uk9N0PTiWT6I+17UTDI97zYEwC1mbnjefqmtjbL8nunjPwGjw==", + "dependencies": { + "@formatjs/fast-memoize": "2.2.0", + "@formatjs/intl-localematcher": "0.5.4", + "tslib": "^2.4.0" + } + }, + "node_modules/@formatjs/fast-memoize": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.0.tgz", + "integrity": "sha512-hnk/nY8FyrL5YxwP9e4r9dqeM6cAbo8PeU9UjyXojZMNvVad2Z06FAVHyR3Ecw6fza+0GH7vdJgiKIVXTMbSBA==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "2.7.9", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.7.9.tgz", + "integrity": "sha512-9Z5buDRMsTbplXknvRlDmnpWhZrayNVcVvkH0+SSz8Ll4XD/7Tcn8m1IjxM3iBJSwQbxwxb7/g0Fkx3d4j2osw==", + "dependencies": { + "@formatjs/ecma402-abstract": "2.1.0", + "@formatjs/icu-skeleton-parser": "1.8.3", + "tslib": "^2.4.0" + } + }, + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.3.tgz", + "integrity": "sha512-TsKAP013ayZFbWWR2KWy+f9QVZh0yDFTPK3yE4OqU2gnzafvmKTodRtJLVpfZmpXWJ5y7BWD1AsyT14mcbLzig==", + "dependencies": { + "@formatjs/ecma402-abstract": "2.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@formatjs/intl-localematcher": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.4.tgz", + "integrity": "sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g==", + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", @@ -24327,6 +24373,17 @@ "node": ">=12" } }, + "node_modules/intl-messageformat": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.6.0.tgz", + "integrity": "sha512-AYKl/DY1nl75pJU8EK681JOVL40uQTNJe3yEMXKfydDFoz+5hNrM/PqjchueSMKGKCZKBVgeexqZwy3uC2B36Q==", + "dependencies": { + "@formatjs/ecma402-abstract": "2.1.0", + "@formatjs/fast-memoize": "2.2.0", + "@formatjs/icu-messageformat-parser": "2.7.9", + "tslib": "^2.4.0" + } + }, "node_modules/invariant": { "version": "2.2.4", "license": "MIT", @@ -32344,6 +32401,26 @@ } } }, + "node_modules/next-intl": { + "version": "3.21.1", + "resolved": "https://registry.npmjs.org/next-intl/-/next-intl-3.21.1.tgz", + "integrity": "sha512-hQm4Wgq5i1lfOHAWmXBVl5d2/XAeddcjsrUmjotXEESzPSvW5j2t0Pr8AV8WorTILgqU748aXuenBhz5P78tdw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/amannn" + } + ], + "dependencies": { + "@formatjs/intl-localematcher": "^0.5.4", + "negotiator": "^0.6.3", + "use-intl": "^3.21.1" + }, + "peerDependencies": { + "next": "^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/next-mdx-remote": { "version": "4.4.1", "license": "MPL-2.0", @@ -41509,6 +41586,18 @@ "react": ">=16.8.0" } }, + "node_modules/use-intl": { + "version": "3.21.1", + "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-3.21.1.tgz", + "integrity": "sha512-52kYgcydYkG9SX0ZZGt7W6WD2Va01hwe15bDgkXuaTdSxrF9fDu6hHTV5DxIuSmSSf/FEcBo/nodpw3ZhY31Lw==", + "dependencies": { + "@formatjs/fast-memoize": "^2.2.0", + "intl-messageformat": "^10.5.14" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/use-memo-one": { "version": "1.1.3", "license": "MIT", diff --git a/packages/common/MetaFavicons/app-router-head.tsx b/packages/common/MetaFavicons/app-router-head.tsx new file mode 100644 index 00000000000..206fc2c4e45 --- /dev/null +++ b/packages/common/MetaFavicons/app-router-head.tsx @@ -0,0 +1,90 @@ +import Head from 'next/head' +import React from 'react' + +export const DEFAULT_FAVICON_THEME_COLOR = '1E1E1E' +export const DEFAULT_FAVICON_ROUTE = '/favicon' + +const MetaFaviconsAppRouter = ({ + applicationName, + route = DEFAULT_FAVICON_ROUTE, + themeColor = DEFAULT_FAVICON_THEME_COLOR, + includeRssXmlFeed = false, + includeManifest = false, + includeMsApplicationConfig = false, +}: { + applicationName: string + // alternative route to use for the favicons + route?: string + // theme color for the browser + themeColor?: string + // include RSS feed + includeRssXmlFeed?: boolean + // include manifest.json + includeManifest?: boolean + // include browserconfig.xml + includeMsApplicationConfig?: boolean +}) => { + const basePath = process.env.NEXT_PUBLIC_BASE_PATH + + return ( + + {/* prettier-ignore */} + + {/* prettier-ignore */} + + {/* prettier-ignore */} + + {/* prettier-ignore */} + + {/* prettier-ignore */} + + {/* prettier-ignore */} + + {/* prettier-ignore */} + + {/* prettier-ignore */} + + {/* prettier-ignore */} + + {/* prettier-ignore */} + + {/* prettier-ignore */} + + {/* prettier-ignore */} + + {/* prettier-ignore */} + + {/* prettier-ignore */} + + {/* prettier-ignore */} + + {/* prettier-ignore */} + + {/* prettier-ignore */} + + {/* prettier-ignore */} + + {/* prettier-ignore */} + + {/* prettier-ignore */} + + {/* prettier-ignore */} + + {/* prettier-ignore */} + + + + + + {includeRssXmlFeed && ( + + )} + {includeManifest && } + {includeMsApplicationConfig && ( + + )} + + ) +} + +export default MetaFaviconsAppRouter