diff --git a/.prettierrc b/.prettierrc index b81bd4d..75ebcfc 100644 --- a/.prettierrc +++ b/.prettierrc @@ -2,6 +2,5 @@ "singleQuote": true, "jsxSingleQuote": true, "trailingComma": "all", - "useTabs": true, "tabWidth": 2 } diff --git a/eslint.config.js b/eslint.config.js index 8d6c546..da3adf5 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -3,51 +3,51 @@ import tseslint from 'typescript-eslint'; import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; const compat = new FlatCompat({ - baseDirectory: import.meta.dirname, + baseDirectory: import.meta.dirname, }); export default tseslint.config( - { - ignores: ['.next'], - }, - ...compat.extends('next/core-web-vitals'), - { - files: ['**/*.ts', '**/*.tsx'], - extends: [ - ...tseslint.configs.recommended, - ...tseslint.configs.recommendedTypeChecked, - ...tseslint.configs.stylisticTypeChecked, - eslintPluginPrettierRecommended, - ], - rules: { - '@typescript-eslint/array-type': 'off', - '@typescript-eslint/consistent-type-definitions': 'off', - '@typescript-eslint/consistent-type-imports': [ - 'warn', - { prefer: 'type-imports', fixStyle: 'inline-type-imports' }, - ], - '@typescript-eslint/no-unused-vars': [ - 'warn', - { argsIgnorePattern: '^_' }, - ], - '@typescript-eslint/require-await': 'off', - '@typescript-eslint/no-misused-promises': [ - 'error', - { checksVoidReturn: { attributes: false } }, - ], - '@typescript-eslint/no-explicit-any': 'warn', - '@typescript-eslint/no-floating-promises': 'warn', - '@typescript-eslint/no-unsafe-argument': 'warn', - }, - }, - { - linterOptions: { - reportUnusedDisableDirectives: true, - }, - languageOptions: { - parserOptions: { - projectService: true, - }, - }, - }, + { + ignores: ['.next'], + }, + ...compat.extends('next/core-web-vitals'), + { + files: ['**/*.ts', '**/*.tsx'], + extends: [ + ...tseslint.configs.recommended, + ...tseslint.configs.recommendedTypeChecked, + ...tseslint.configs.stylisticTypeChecked, + //eslintPluginPrettierRecommended, + ], + rules: { + '@typescript-eslint/array-type': 'off', + '@typescript-eslint/consistent-type-definitions': 'off', + '@typescript-eslint/consistent-type-imports': [ + 'warn', + { prefer: 'type-imports', fixStyle: 'inline-type-imports' }, + ], + '@typescript-eslint/no-unused-vars': [ + 'warn', + { argsIgnorePattern: '^_' }, + ], + '@typescript-eslint/require-await': 'off', + '@typescript-eslint/no-misused-promises': [ + 'error', + { checksVoidReturn: { attributes: false } }, + ], + '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/no-floating-promises': 'warn', + '@typescript-eslint/no-unsafe-argument': 'warn', + }, + }, + { + linterOptions: { + reportUnusedDisableDirectives: true, + }, + languageOptions: { + parserOptions: { + projectService: true, + }, + }, + }, ); diff --git a/next.config.js b/next.config.js index 18b0525..937da13 100644 --- a/next.config.js +++ b/next.config.js @@ -3,65 +3,68 @@ */ import './src/env.js'; import { withSentryConfig } from '@sentry/nextjs'; +import { withPlausibleProxy } from 'next-plausible'; /** @type {import("next").NextConfig} */ -const config = { - output: 'standalone', - images: { - remotePatterns: [ - { - protocol: 'https', - hostname: '*.gbrown.org', - }, - ], - }, - serverExternalPackages: ['require-in-the-middle'], - experimental: { - serverActions: { - bodySizeLimit: '10mb', - }, - }, - turbopack: { - rules: { - '*.svg': { - loaders: [ - { - loader: '@svgr/webpack', - options: { - icon: true, - }, - }, - ], - as: '*.js', - }, - }, - }, -}; +const config = withPlausibleProxy({ + customDomain: 'https://plausible.gbrown.org', +})({ + output: 'standalone', + images: { + remotePatterns: [ + { + protocol: 'https', + hostname: '*.gbrown.org', + }, + ], + }, + serverExternalPackages: ['require-in-the-middle'], + experimental: { + serverActions: { + bodySizeLimit: '10mb', + }, + }, + turbopack: { + rules: { + '*.svg': { + loaders: [ + { + loader: '@svgr/webpack', + options: { + icon: true, + }, + }, + ], + as: '*.js', + }, + }, + }, +}); const sentryConfig = { - // For all available options, see: - // https://www.npmjs.com/package/@sentry/webpack-plugin#options - org: 'gib', - project: 't3-supabase-template', - sentryUrl: process.env.NEXT_PUBLIC_SENTRY_URL, - authToken: process.env.SENTRY_AUTH_TOKEN, - // Only print logs for uploading source maps in CI - silent: !process.env.CI, - // For all available options, see: - // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/ - // Upload a larger set of source maps for prettier stack traces (increases build time) - widenClientFileUpload: true, - // Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers. - // This can increase your server load as well as your hosting bill. - // Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client- - // side errors will fail. - tunnelRoute: '/monitoring', - // Automatically tree-shake Sentry logger statements to reduce bundle size - disableLogger: true, - // Capture React Component Names - reactComponentAnnotation: { - enabled: true, - }, + // For all available options, see: + // https://www.npmjs.com/package/@sentry/webpack-plugin#options + org: 'gib', + project: 't3-supabase-template', + sentryUrl: process.env.NEXT_PUBLIC_SENTRY_URL, + authToken: process.env.SENTRY_AUTH_TOKEN, + // Only print logs for uploading source maps in CI + silent: !process.env.CI, + // For all available options, see: + // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/ + // Upload a larger set of source maps for prettier stack traces (increases build time) + widenClientFileUpload: true, + // Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers. + // This can increase your server load as well as your hosting bill. + // Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client- + // side errors will fail. + tunnelRoute: '/monitoring', + // Automatically tree-shake Sentry logger statements to reduce bundle size + disableLogger: true, + // Capture React Component Names + reactComponentAnnotation: { + enabled: true, + }, }; export default withSentryConfig(config, sentryConfig); diff --git a/package.json b/package.json index 23b0397..4c99e27 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "clsx": "^2.1.1", "lucide-react": "^0.510.0", "next": "^15.3.3", + "next-plausible": "^3.12.4", "next-themes": "^0.4.6", "react": "^19.1.0", "react-dom": "^19.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2a77fd9..99803b3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,6 +53,9 @@ importers: next: specifier: ^15.3.3 version: 15.3.3(@babel/core@7.27.4)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + next-plausible: + specifier: ^3.12.4 + version: 3.12.4(next@15.3.3(@babel/core@7.27.4)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -2758,6 +2761,13 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + next-plausible@3.12.4: + resolution: {integrity: sha512-cD3+ixJxf8yBYvsideTxqli3fvrB7R4BXcvsNJz8Sm2X1QN039WfiXjCyNWkub4h5++rRs6fHhchUMnOuJokcg==} + peerDependencies: + next: '^11.1.0 || ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 ' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + next-themes@0.4.6: resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==} peerDependencies: @@ -6327,6 +6337,12 @@ snapshots: neo-async@2.6.2: {} + next-plausible@3.12.4(next@15.3.3(@babel/core@7.27.4)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + next: 15.3.3(@babel/core@7.27.4)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + next-themes@0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: react: 19.1.0 diff --git a/postcss.config.js b/postcss.config.js index 1970487..a34a3d5 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -1,5 +1,5 @@ export default { - plugins: { - '@tailwindcss/postcss': {}, - }, + plugins: { + '@tailwindcss/postcss': {}, + }, }; diff --git a/prettier.config.js b/prettier.config.js index 4b043fd..09f0482 100644 --- a/prettier.config.js +++ b/prettier.config.js @@ -1,4 +1,4 @@ /** @type {import('prettier').Config & import('prettier-plugin-tailwindcss').PluginOptions} */ export default { - plugins: ['prettier-plugin-tailwindcss'], + plugins: ['prettier-plugin-tailwindcss'], }; diff --git a/public/icons/gitea.svg b/public/icons/gitea.svg new file mode 100644 index 0000000..d9eb11a --- /dev/null +++ b/public/icons/gitea.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/scripts/next.config.build.js b/scripts/next.config.build.js index 39d14aa..6bcbd94 100644 --- a/scripts/next.config.build.js +++ b/scripts/next.config.build.js @@ -7,61 +7,61 @@ import { withSentryConfig } from '@sentry/nextjs'; /** @type {import("next").NextConfig} */ const config = { - output: 'standalone', - images: { - remotePatterns: [ - { - protocol: 'https', - hostname: '*.gbrown.org', - }, - ], - }, - serverExternalPackages: ['require-in-the-middle'], - experimental: { - serverActions: { - bodySizeLimit: '10mb', - }, - }, - typescript: { - ignoreBuildErrors: true, - }, - eslint: { - ignoreDuringBuilds: true, - }, - //turbopack: { - //rules: { - //'*.svg': { - //loaders: ['@svgr/webpack'], - //as: '*.js', - //}, - //}, - //}, + output: 'standalone', + images: { + remotePatterns: [ + { + protocol: 'https', + hostname: '*.gbrown.org', + }, + ], + }, + serverExternalPackages: ['require-in-the-middle'], + experimental: { + serverActions: { + bodySizeLimit: '10mb', + }, + }, + typescript: { + ignoreBuildErrors: true, + }, + eslint: { + ignoreDuringBuilds: true, + }, + //turbopack: { + //rules: { + //'*.svg': { + //loaders: ['@svgr/webpack'], + //as: '*.js', + //}, + //}, + //}, }; const sentryConfig = { - // For all available options, see: - // https://www.npmjs.com/package/@sentry/webpack-plugin#options - org: 'gib', - project: 't3-supabase-template', - sentryUrl: process.env.SENTRY_URL, - authToken: process.env.SENTRY_AUTH_TOKEN, - // Only print logs for uploading source maps in CI - silent: !process.env.CI, - // For all available options, see: - // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/ - // Upload a larger set of source maps for prettier stack traces (increases build time) - widenClientFileUpload: true, - // Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers. - // This can increase your server load as well as your hosting bill. - // Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client- - // side errors will fail. - tunnelRoute: '/monitoring', - // Automatically tree-shake Sentry logger statements to reduce bundle size - disableLogger: true, - // Capture React Component Names - reactComponentAnnotation: { - enabled: true, - }, + // For all available options, see: + // https://www.npmjs.com/package/@sentry/webpack-plugin#options + org: 'gib', + project: 't3-supabase-template', + sentryUrl: process.env.SENTRY_URL, + authToken: process.env.SENTRY_AUTH_TOKEN, + // Only print logs for uploading source maps in CI + silent: !process.env.CI, + // For all available options, see: + // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/ + // Upload a larger set of source maps for prettier stack traces (increases build time) + widenClientFileUpload: true, + // Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers. + // This can increase your server load as well as your hosting bill. + // Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client- + // side errors will fail. + tunnelRoute: '/monitoring', + // Automatically tree-shake Sentry logger statements to reduce bundle size + disableLogger: true, + // Capture React Component Names + reactComponentAnnotation: { + enabled: true, + }, }; export default withSentryConfig(config, sentryConfig); diff --git a/scripts/next.config.default.js b/scripts/next.config.default.js index dd1537d..d1cec6a 100644 --- a/scripts/next.config.default.js +++ b/scripts/next.config.default.js @@ -7,55 +7,55 @@ import { withSentryConfig } from '@sentry/nextjs'; /** @type {import("next").NextConfig} */ const config = { - output: 'standalone', - images: { - remotePatterns: [ - { - protocol: 'https', - hostname: '*.gbrown.org', - }, - ], - }, - serverExternalPackages: ['require-in-the-middle'], - experimental: { - serverActions: { - bodySizeLimit: '10mb', - }, - }, - //turbopack: { - //rules: { - //'*.svg': { - //loaders: ['@svgr/webpack'], - //as: '*.js', - //}, - //}, - //}, + output: 'standalone', + images: { + remotePatterns: [ + { + protocol: 'https', + hostname: '*.gbrown.org', + }, + ], + }, + serverExternalPackages: ['require-in-the-middle'], + experimental: { + serverActions: { + bodySizeLimit: '10mb', + }, + }, + //turbopack: { + //rules: { + //'*.svg': { + //loaders: ['@svgr/webpack'], + //as: '*.js', + //}, + //}, + //}, }; const sentryConfig = { - // For all available options, see: - // https://www.npmjs.com/package/@sentry/webpack-plugin#options - org: 'gib', - project: 't3-supabase-template', - sentryUrl: process.env.SENTRY_URL, - authToken: process.env.SENTRY_AUTH_TOKEN, - // Only print logs for uploading source maps in CI - silent: !process.env.CI, - // For all available options, see: - // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/ - // Upload a larger set of source maps for prettier stack traces (increases build time) - widenClientFileUpload: true, - // Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers. - // This can increase your server load as well as your hosting bill. - // Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client- - // side errors will fail. - tunnelRoute: '/monitoring', - // Automatically tree-shake Sentry logger statements to reduce bundle size - disableLogger: true, - // Capture React Component Names - reactComponentAnnotation: { - enabled: true, - }, + // For all available options, see: + // https://www.npmjs.com/package/@sentry/webpack-plugin#options + org: 'gib', + project: 't3-supabase-template', + sentryUrl: process.env.SENTRY_URL, + authToken: process.env.SENTRY_AUTH_TOKEN, + // Only print logs for uploading source maps in CI + silent: !process.env.CI, + // For all available options, see: + // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/ + // Upload a larger set of source maps for prettier stack traces (increases build time) + widenClientFileUpload: true, + // Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers. + // This can increase your server load as well as your hosting bill. + // Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client- + // side errors will fail. + tunnelRoute: '/monitoring', + // Automatically tree-shake Sentry logger statements to reduce bundle size + disableLogger: true, + // Capture React Component Names + reactComponentAnnotation: { + enabled: true, + }, }; export default withSentryConfig(config, sentryConfig); diff --git a/sentry.server.config.ts b/sentry.server.config.ts index 098e696..86a556b 100644 --- a/sentry.server.config.ts +++ b/sentry.server.config.ts @@ -5,11 +5,11 @@ import * as Sentry from '@sentry/nextjs'; Sentry.init({ - dsn: 'https://0468176d5291bc2b914261147bfef117@sentry.gbrown.org/6', + dsn: 'https://0468176d5291bc2b914261147bfef117@sentry.gbrown.org/6', - // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control. - tracesSampleRate: 1, + // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control. + tracesSampleRate: 1, - // Setting this option to true will print useful information to the console while you're setting up Sentry. - debug: false, + // Setting this option to true will print useful information to the console while you're setting up Sentry. + debug: false, }); diff --git a/src/app/(auth-pages)/auth/callback/route.ts b/src/app/(auth-pages)/auth/callback/route.ts index 4f89b38..e810e51 100644 --- a/src/app/(auth-pages)/auth/callback/route.ts +++ b/src/app/(auth-pages)/auth/callback/route.ts @@ -6,38 +6,38 @@ import { type NextRequest } from 'next/server'; import { redirect } from 'next/navigation'; export const GET = async (request: NextRequest) => { - const { searchParams, origin } = new URL(request.url); - const code = searchParams.get('code'); - const token_hash = searchParams.get('token'); - const type = searchParams.get('type') as EmailOtpType | null; - const redirectTo = searchParams.get('redirect_to') ?? '/'; - const supabase = await createServerClient(); + const { searchParams, origin } = new URL(request.url); + const code = searchParams.get('code'); + const token_hash = searchParams.get('token'); + const type = searchParams.get('type') as EmailOtpType | null; + const redirectTo = searchParams.get('redirect_to') ?? '/'; + const supabase = await createServerClient(); - if (code) { - const { error } = await supabase.auth.exchangeCodeForSession(code); - if (error) { - console.error('OAuth error:', error); - return redirect(`/sign-in?error=${encodeURIComponent(error.message)}`); - } - return redirect(redirectTo); - } + if (code) { + const { error } = await supabase.auth.exchangeCodeForSession(code); + if (error) { + console.error('OAuth error:', error); + return redirect(`/sign-in?error=${encodeURIComponent(error.message)}`); + } + return redirect(redirectTo); + } - if (token_hash && type) { - const { error } = await supabase.auth.verifyOtp({ - type, - token_hash, - }); - if (!error) { - if (type === 'signup' || type === 'magiclink' || type === 'email') - return redirect('/'); - if (type === 'recovery' || type === 'email_change') - return redirect('/profile'); - if (type === 'invite') return redirect('/sign-up'); - } - return redirect( - `/?error=${encodeURIComponent(error?.message ?? 'Unknown error')}`, - ); - } + if (token_hash && type) { + const { error } = await supabase.auth.verifyOtp({ + type, + token_hash, + }); + if (!error) { + if (type === 'signup' || type === 'magiclink' || type === 'email') + return redirect('/'); + if (type === 'recovery' || type === 'email_change') + return redirect('/profile'); + if (type === 'invite') return redirect('/sign-up'); + } + return redirect( + `/?error=${encodeURIComponent(error?.message ?? 'Unknown error')}`, + ); + } - return redirect('/'); + return redirect('/'); }; diff --git a/src/app/(auth-pages)/auth/success/page.tsx b/src/app/(auth-pages)/auth/success/page.tsx index 673b7e4..e6660ab 100644 --- a/src/app/(auth-pages)/auth/success/page.tsx +++ b/src/app/(auth-pages)/auth/success/page.tsx @@ -6,34 +6,34 @@ import { useEffect } from 'react'; import { Loader2 } from 'lucide-react'; const AuthSuccessPage = () => { - const { refreshUserData } = useAuth(); - const router = useRouter(); + const { refreshUserData } = useAuth(); + const router = useRouter(); - useEffect(() => { - const handleAuthSuccess = async () => { - // Refresh the auth context to pick up the new session - await refreshUserData(); + useEffect(() => { + const handleAuthSuccess = async () => { + // Refresh the auth context to pick up the new session + await refreshUserData(); - // Small delay to ensure state is updated - setTimeout(() => { - router.push('/'); - }, 100); - }; + // Small delay to ensure state is updated + setTimeout(() => { + router.push('/'); + }, 100); + }; - handleAuthSuccess().catch((error) => { - console.error(`Error: ${error instanceof Error ? error.message : error}`); - }); - }, [refreshUserData, router]); + handleAuthSuccess().catch((error) => { + console.error(`Error: ${error instanceof Error ? error.message : error}`); + }); + }, [refreshUserData, router]); - // Show loading while processing - return ( -
Completing sign in...
-Completing sign in...
+