2.3.0 Dev Tools (#180)

* 2.3.0 - Added new Dev Tools app
This commit is contained in:
Giancarlo Buomprisco
2025-02-21 13:29:42 +07:00
committed by GitHub
parent 59dfc0ad91
commit c185bcfa11
36 changed files with 3747 additions and 67 deletions

41
apps/dev-tool/.gitignore vendored Normal file
View File

@@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

27
apps/dev-tool/README.md Normal file
View File

@@ -0,0 +1,27 @@
# Dev Tool
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
The Dev Tools is an application that helps you manage your Makerkit environment variables and other settings.
## Getting Started
First, run the development server:
```bash
pnpm run --filter dev-tool dev
```
Open the link printed in the terminal to see the result.
## Testing production environment variables
To test your production environment variables, create a `.env.production.local` file in the `apps/web` directory and add your production environment variables.
This environment variables are not committed to the repository, so you can use them for testing purposes.
In the environment mode switcher, please select `Production` to test your production environment variables.
## Don't publish this app
This app is not intended to be published to the public. This is only meant to be used by for development purposes.

View File

@@ -0,0 +1,199 @@
'use client';
import Link from 'next/link';
import { EmailTesterFormSchema } from '@/app/emails/lib/email-tester-form-schema';
import { sendEmailAction } from '@/app/emails/lib/server-actions';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { Button } from '@kit/ui/button';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
} from '@kit/ui/form';
import { Input } from '@kit/ui/input';
import { toast } from '@kit/ui/sonner';
import { Switch } from '@kit/ui/switch';
export function EmailTesterForm(props: {
template: string;
settings: {
username: string;
password: string;
sender: string;
host: string;
port: number;
tls: boolean;
};
}) {
const form = useForm({
resolver: zodResolver(EmailTesterFormSchema),
defaultValues: {
username: props.settings.username,
password: props.settings.password,
sender: props.settings.sender,
host: props.settings.host,
port: props.settings.port,
tls: props.settings.tls,
to: '',
},
});
return (
<div className={'flex flex-col space-y-8'}>
<p className="text-muted-foreground text-sm">
The settings below were filled from your environment variables. You can
change them to test different scenarios.{' '}
<Link className={'underline'} href={'https://www.nodemailer.com'}>
Learn more about Nodemailer if you're not sure how to configure it.
</Link>
</p>
<Form {...form}>
<form
className="flex flex-col space-y-6"
onSubmit={form.handleSubmit(async (data) => {
const promise = sendEmailAction({
template: props.template,
settings: {
username: data.username,
password: data.password,
sender: data.sender,
host: data.host,
port: data.port,
tls: data.tls,
to: data.to,
},
});
toast.promise(promise, {
loading: 'Sending email...',
success: 'Email sent successfully',
error: 'Failed to send email',
});
})}
>
<div className={'flex items-center space-x-2'}>
<FormField
name={'sender'}
render={({ field }) => {
return (
<FormItem className={'w-full'}>
<FormLabel>Sender</FormLabel>
<FormControl>
<Input {...field} placeholder={'Sender'} />
</FormControl>
</FormItem>
);
}}
/>
<FormField
name={'to'}
render={({ field }) => {
return (
<FormItem className={'w-full'}>
<FormLabel>Recipient</FormLabel>
<FormControl>
<Input {...field} type={'email'} placeholder={'to'} />
</FormControl>
</FormItem>
);
}}
/>
</div>
<div className={'flex items-center space-x-2'}>
<FormField
name={'username'}
render={({ field }) => {
return (
<FormItem className={'w-full'}>
<FormLabel>Username</FormLabel>
<FormControl>
<Input {...field} placeholder={'Username'} />
</FormControl>
</FormItem>
);
}}
/>
<FormField
name={'password'}
render={({ field }) => {
return (
<FormItem className={'w-full'}>
<FormLabel>Password</FormLabel>
<FormControl>
<Input
{...field}
className={'w-full'}
placeholder={'Password'}
type={'password'}
/>
</FormControl>
</FormItem>
);
}}
/>
</div>
<div className={'flex items-center space-x-2'}>
<FormField
name={'host'}
render={({ field }) => {
return (
<FormItem className={'w-full'}>
<FormLabel>Host</FormLabel>
<FormControl>
<Input {...field} placeholder={'Host'} />
</FormControl>
</FormItem>
);
}}
/>
<FormField
name={'port'}
render={({ field }) => {
return (
<FormItem className={'w-full'}>
<FormLabel>Port</FormLabel>
<FormControl>
<Input {...field} placeholder={'Port'} />
</FormControl>
</FormItem>
);
}}
/>
<FormField
name={'tls'}
render={({ field }) => {
return (
<FormItem className={'w-full'}>
<FormLabel>Secure (TLS)</FormLabel>
<FormControl>
<Switch
{...field}
onCheckedChange={(value) => {
form.setValue('tls', value);
}}
/>
</FormControl>
</FormItem>
);
}}
/>
</div>
<Button type={'submit'}>Send Test Email</Button>
</form>
</Form>
</div>
);
}

View File

@@ -0,0 +1,104 @@
import { EmailTesterForm } from '@/app/emails/[id]/components/email-tester-form';
import { loadEmailTemplate } from '@/app/emails/lib/email-loader';
import { getVariable } from '@/app/variables/lib/env-scanner';
import { EnvMode } from '@/app/variables/lib/types';
import { EnvModeSelector } from '@/components/env-mode-selector';
import { IFrame } from '@/components/iframe';
import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs';
import { Button } from '@kit/ui/button';
import {
Dialog,
DialogContent,
DialogTitle,
DialogTrigger,
} from '@kit/ui/dialog';
import { Page, PageBody, PageHeader } from '@kit/ui/page';
type EmailPageProps = React.PropsWithChildren<{
params: Promise<{
id: string;
}>;
searchParams: Promise<{ mode?: EnvMode }>;
}>;
export const metadata = {
title: 'Email Template',
};
export default async function EmailPage(props: EmailPageProps) {
const { id } = await props.params;
const mode = (await props.searchParams).mode ?? 'development';
const template = await loadEmailTemplate(id);
const emailSettings = await getEmailSettings(mode);
return (
<Page style={'custom'}>
<PageHeader
displaySidebarTrigger={false}
description={
<AppBreadcrumbs
values={{
emails: 'Emails',
'invite-email': 'Invite Email',
'account-delete-email': 'Account Delete Email',
'confirm-email': 'Confirm Email',
'change-email-address-email': 'Change Email Address Email',
'reset-password-email': 'Reset Password Email',
'magic-link-email': 'Magic Link Email',
}}
/>
}
>
<EnvModeSelector mode={mode} />
</PageHeader>
<PageBody className={'flex flex-1 flex-col gap-y-4'}>
<p className={'text-muted-foreground py-1 text-xs'}>
Remember that the below is an approximation of the email. Always test
it in your inbox.{' '}
<Dialog>
<DialogTrigger asChild>
<Button variant={'link'} className="p-0 underline">
Test Email
</Button>
</DialogTrigger>
<DialogContent>
<DialogTitle>Send Test Email</DialogTitle>
<EmailTesterForm settings={emailSettings} template={id} />
</DialogContent>
</Dialog>
</p>
<IFrame className={'flex flex-1 flex-col'}>
<div
className={'flex flex-1 flex-col'}
dangerouslySetInnerHTML={{ __html: template.html }}
/>
</IFrame>
</PageBody>
</Page>
);
}
async function getEmailSettings(mode: EnvMode) {
const sender = await getVariable('EMAIL_SENDER', mode);
const host = await getVariable('EMAIL_HOST', mode);
const port = await getVariable('EMAIL_PORT', mode);
const tls = await getVariable('EMAIL_TLS', mode);
const username = await getVariable('EMAIL_USER', mode);
const password = await getVariable('EMAIL_PASSWORD', mode);
return {
sender,
host,
port: Number.isNaN(Number(port)) ? 487 : Number(port),
tls: tls === 'true',
username,
password,
};
}

View File

@@ -0,0 +1,57 @@
import {
renderAccountDeleteEmail,
renderInviteEmail,
} from '@kit/email-templates';
export async function loadEmailTemplate(id: string) {
if (id === 'account-delete-email') {
return renderAccountDeleteEmail({
productName: 'Makerkit',
userDisplayName: 'Giancarlo',
});
}
if (id === 'invite-email') {
return renderInviteEmail({
teamName: 'Makerkit',
teamLogo:
'',
inviter: 'Giancarlo',
invitedUserEmail: 'test@makerkit.dev',
link: 'https://makerkit.dev',
productName: 'Makerkit',
});
}
if (id === 'magic-link-email') {
return loadFromFileSystem('magic-link');
}
if (id === 'reset-password-email') {
return loadFromFileSystem('reset-password');
}
if (id === 'change-email-address-email') {
return loadFromFileSystem('change-email-address');
}
if (id === 'confirm-email') {
return loadFromFileSystem('confirm-email');
}
throw new Error(`Email template not found: ${id}`);
}
async function loadFromFileSystem(fileName: string) {
const { readFileSync } = await import('node:fs');
const { join } = await import('node:path');
const filePath = join(
process.cwd(),
`../web/supabase/templates/${fileName}.html`,
);
return {
html: readFileSync(filePath, 'utf8'),
};
}

View File

@@ -0,0 +1,11 @@
import { z } from 'zod';
export const EmailTesterFormSchema = z.object({
username: z.string().min(1),
password: z.string().min(1),
sender: z.string().min(1),
to: z.string().email(),
host: z.string().min(1),
port: z.number().min(1),
tls: z.boolean(),
});

View File

@@ -0,0 +1,38 @@
'use server';
import { loadEmailTemplate } from '@/app/emails/lib/email-loader';
export async function sendEmailAction(params: {
template: string;
settings: {
username: string;
password: string;
sender: string;
host: string;
to: string;
port: number;
tls: boolean;
};
}) {
const { settings } = params;
const { createTransport } = await import('nodemailer');
const transporter = createTransport({
host: settings.host,
port: settings.port,
secure: settings.tls,
auth: {
user: settings.username,
pass: settings.password,
},
});
const { html } = await loadEmailTemplate(params.template);
return transporter.sendMail({
html,
from: settings.sender,
to: settings.to,
subject: 'Test Email',
});
}

View File

@@ -0,0 +1,83 @@
import Link from 'next/link';
import {
CardButton,
CardButtonHeader,
CardButtonTitle,
} from '@kit/ui/card-button';
import { Heading } from '@kit/ui/heading';
import { Page, PageBody, PageHeader } from '@kit/ui/page';
export const metadata = {
title: 'Emails',
};
export default async function EmailsPage() {
return (
<Page style={'custom'}>
<PageHeader displaySidebarTrigger={false} description="Emails" />
<PageBody className={'gap-y-8'}>
<div className={'flex flex-col space-y-4'}>
<Heading level={5}>Supabase Auth Emails</Heading>
<div className={'grid grid-cols-1 gap-4 md:grid-cols-4'}>
<CardButton asChild>
<Link href={'/emails/confirm-email'}>
<CardButtonHeader>
<CardButtonTitle>Confirm Email</CardButtonTitle>
</CardButtonHeader>
</Link>
</CardButton>
<CardButton asChild>
<Link href={'/emails/change-email-address-email'}>
<CardButtonHeader>
<CardButtonTitle>Change Email Address Email</CardButtonTitle>
</CardButtonHeader>
</Link>
</CardButton>
<CardButton asChild>
<Link href={'/emails/reset-password-email'}>
<CardButtonHeader>
<CardButtonTitle>Reset Password Email</CardButtonTitle>
</CardButtonHeader>
</Link>
</CardButton>
<CardButton asChild>
<Link href={'/emails/magic-link-email'}>
<CardButtonHeader>
<CardButtonTitle>Magic Link Email</CardButtonTitle>
</CardButtonHeader>
</Link>
</CardButton>
</div>
</div>
<div className={'flex flex-col space-y-4'}>
<Heading level={5}>Transactional Emails</Heading>
<div className={'grid grid-cols-1 gap-4 md:grid-cols-4'}>
<CardButton asChild>
<Link href={'/emails/account-delete-email'}>
<CardButtonHeader>
<CardButtonTitle>Account Delete Email</CardButtonTitle>
</CardButtonHeader>
</Link>
</CardButton>
<CardButton asChild>
<Link href={'/emails/invite-email'}>
<CardButtonHeader>
<CardButtonTitle>Invite Email</CardButtonTitle>
</CardButtonHeader>
</Link>
</CardButton>
</div>
</div>
</PageBody>
</Page>
);
}

View File

@@ -0,0 +1,27 @@
import type { Metadata } from 'next';
import { DevToolLayout } from '@/components/app-layout';
import { RootProviders } from '@/components/root-providers';
import '../styles/globals.css';
export const metadata: Metadata = {
title: 'Makerkit | Dev Tool',
description: 'The dev tool for Makerkit',
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body>
<RootProviders>
<DevToolLayout>{children}</DevToolLayout>
</RootProviders>
</body>
</html>
);
}

View File

@@ -0,0 +1,213 @@
import { EnvMode } from '@/app/variables/lib/types';
import { getVariable } from '../variables/lib/env-scanner';
export function createConnectivityService(mode: EnvMode) {
return new ConnectivityService(mode);
}
class ConnectivityService {
constructor(private mode: EnvMode = 'development') {}
async checkSupabaseConnectivity() {
const url = await getVariable('NEXT_PUBLIC_SUPABASE_URL', this.mode);
if (!url) {
return {
status: 'error' as const,
message: 'No Supabase URL found in environment variables',
};
}
const anonKey = await getVariable(
'NEXT_PUBLIC_SUPABASE_ANON_KEY',
this.mode,
);
if (!anonKey) {
return {
status: 'error' as const,
message: 'No Supabase Anon Key found in environment variables',
};
}
const response = await fetch(`${url}/auth/v1/health`, {
headers: {
apikey: anonKey,
Authorization: `Bearer ${anonKey}`,
},
});
if (!response.ok) {
return {
status: 'error' as const,
message:
'Failed to connect to Supabase. The Supabase Anon Key or URL is not valid.',
};
}
return {
status: 'success' as const,
message: 'Connected to Supabase',
};
}
async checkSupabaseAdminConnectivity() {
const url = await getVariable('NEXT_PUBLIC_SUPABASE_URL', this.mode);
if (!url) {
return {
status: 'error' as const,
message: 'No Supabase URL found in environment variables',
};
}
const endpoint = `${url}/rest/v1/accounts`;
const apikey = await getVariable(
'NEXT_PUBLIC_SUPABASE_ANON_KEY',
this.mode,
);
if (!apikey) {
return {
status: 'error' as const,
message: 'No Supabase Anon Key found in environment variables',
};
}
const adminKey = await getVariable('SUPABASE_SERVICE_ROLE_KEY', this.mode);
if (!adminKey) {
return {
status: 'error' as const,
message: 'No Supabase Service Role Key found in environment variables',
};
}
const response = await fetch(endpoint, {
headers: {
apikey,
Authorization: `Bearer ${adminKey}`,
},
});
if (!response.ok) {
return {
status: 'error' as const,
message:
'Failed to connect to Supabase Admin. The Supabase Service Role Key is not valid.',
};
}
const data = await response.json();
if (data.length === 0) {
return {
status: 'error' as const,
message: 'No accounts found in Supabase Admin',
};
}
return {
status: 'success' as const,
message: 'Connected to Supabase Admin',
};
}
async checkStripeWebhookEndpoints() {
const secretKey = await getVariable('STRIPE_SECRET_KEY', this.mode);
if (!secretKey) {
return {
status: 'error' as const,
message: 'No Stripe Secret Key found in environment variables',
};
}
const webhooksSecret = await getVariable(
'STRIPE_WEBHOOKS_SECRET',
this.mode,
);
if (!webhooksSecret) {
return {
status: 'error' as const,
message: 'No Webhooks secret found in environment variables',
};
}
const url = `https://api.stripe.com`;
const request = await fetch(`${url}/v1/webhook_endpoints`, {
headers: {
Authorization: `Bearer ${secretKey}`,
},
});
if (!request.ok) {
return {
status: 'error' as const,
message:
'Failed to connect to Stripe. The Stripe Webhook Secret is not valid.',
};
}
const webhooks = await request.json();
if (webhooks.length === 0) {
return {
status: 'error' as const,
message: 'No webhooks found in Stripe',
};
}
const allWebhooksShareTheSameSecret = webhooks.every(
(webhook: any) => webhook.secret === webhooksSecret,
);
if (!allWebhooksShareTheSameSecret) {
return {
status: 'error' as const,
message: 'All webhooks do not share the same secret',
};
}
return {
status: 'success' as const,
message: 'All webhooks share the same Webhooks secret',
};
}
async checkStripeConnected() {
const secretKey = await getVariable('STRIPE_SECRET_KEY', this.mode);
if (!secretKey) {
return {
status: 'error' as const,
message: 'No Stripe Secret Key found in environment variables',
};
}
const url = `https://api.stripe.com`;
const request = await fetch(`${url}/v1/prices`, {
headers: {
Authorization: `Bearer ${secretKey}`,
},
});
if (!request.ok) {
return {
status: 'error' as const,
message:
'Failed to connect to Stripe. The Stripe Secret Key is not valid.',
};
}
return {
status: 'success' as const,
message: 'Connected to Stripe',
};
}
}

View File

@@ -0,0 +1,48 @@
import { ServiceCard } from '@/components/status-tile';
import { Page, PageBody, PageHeader } from '@kit/ui/page';
import { createConnectivityService } from './lib/connectivity-service';
import {EnvMode} from "@/app/variables/lib/types";
import {EnvModeSelector} from "@/components/env-mode-selector";
type DashboardPageProps = React.PropsWithChildren<{
searchParams: Promise<{ mode?: EnvMode }>;
}>;
export default async function DashboardPage(props: DashboardPageProps) {
const mode = (await props.searchParams).mode ?? 'development';
const connectivityService = createConnectivityService(mode);
const [
supabaseStatus,
supabaseAdminStatus,
stripeStatus,
stripeWebhookStatus,
] = await Promise.all([
connectivityService.checkSupabaseConnectivity(),
connectivityService.checkSupabaseAdminConnectivity(),
connectivityService.checkStripeConnected(),
connectivityService.checkStripeWebhookEndpoints(),
]);
return (
<Page style={'custom'}>
<PageHeader
displaySidebarTrigger={false}
description={'Check the status of your Supabase and Stripe services'}
>
<EnvModeSelector mode={mode} />
</PageHeader>
<PageBody className={'py-2'}>
<div className="grid grid-cols-1 gap-4 md:grid-cols-4">
<ServiceCard name={'Supabase API'} status={supabaseStatus} />
<ServiceCard name={'Supabase Admin'} status={supabaseAdminStatus} />
<ServiceCard name={'Stripe API'} status={stripeStatus} />
<ServiceCard name={'Stripe Webhook'} status={stripeWebhookStatus} />
</div>
</PageBody>
</Page>
);
}

View File

@@ -0,0 +1,241 @@
'use client';
import { useState } from 'react';
import { ChevronDownIcon } from 'lucide-react';
import { Button } from '@kit/ui/button';
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuTrigger,
} from '@kit/ui/dropdown-menu';
import { Input } from '@kit/ui/input';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@kit/ui/select';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@kit/ui/table';
import { cn } from '@kit/ui/utils';
import { defaultI18nNamespaces } from '../../../../web/lib/i18n/i18n.settings';
import type { TranslationData, Translations } from '../lib/translations-loader';
function flattenTranslations(
obj: TranslationData,
prefix = '',
result: Record<string, string> = {},
) {
for (const [key, value] of Object.entries(obj)) {
const newKey = prefix ? `${prefix}.${key}` : key;
if (typeof value === 'string') {
result[newKey] = value;
} else {
flattenTranslations(value, newKey, result);
}
}
return result;
}
type FlattenedTranslations = Record<string, Record<string, string>>;
export function TranslationsComparison({
translations,
}: {
translations: Translations;
}) {
const [search, setSearch] = useState('');
const [selectedLocales, setSelectedLocales] = useState<Set<string>>();
const [selectedNamespace, setSelectedNamespace] = useState(
defaultI18nNamespaces[0] as string,
);
const locales = Object.keys(translations);
if (locales.length === 0) {
return <div>No translations found</div>;
}
const baseLocale = locales[0]!;
// Initialize selected locales if not set
if (!selectedLocales) {
setSelectedLocales(new Set(locales));
return null;
}
// Flatten translations for the selected namespace
const flattenedTranslations: FlattenedTranslations = {};
for (const locale of locales) {
const namespaceData = translations[locale]?.[selectedNamespace];
if (namespaceData) {
flattenedTranslations[locale] = flattenTranslations(namespaceData);
} else {
flattenedTranslations[locale] = {};
}
}
// Get all unique keys across all translations
const allKeys = Array.from(
new Set(
Object.values(flattenedTranslations).flatMap((data) => Object.keys(data)),
),
).sort();
const filteredKeys = allKeys.filter((key) =>
key.toLowerCase().includes(search.toLowerCase()),
);
const visibleLocales = locales.filter((locale) =>
selectedLocales.has(locale),
);
const copyTranslation = async (text: string) => {
try {
await navigator.clipboard.writeText(text);
} catch (error) {
console.error('Failed to copy text:', error);
}
};
const toggleLocale = (locale: string) => {
const newSelectedLocales = new Set(selectedLocales);
if (newSelectedLocales.has(locale)) {
if (newSelectedLocales.size > 1) {
newSelectedLocales.delete(locale);
}
} else {
newSelectedLocales.add(locale);
}
setSelectedLocales(newSelectedLocales);
};
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2.5">
<Input
type="search"
placeholder="Search translations..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="max-w-sm"
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="ml-auto">
Select Languages
<ChevronDownIcon className="ml-2 h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[200px]">
{locales.map((locale) => (
<DropdownMenuCheckboxItem
key={locale}
checked={selectedLocales.has(locale)}
onCheckedChange={() => toggleLocale(locale)}
disabled={
selectedLocales.size === 1 && selectedLocales.has(locale)
}
>
{locale}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
</DropdownMenu>
<Select
value={selectedNamespace}
onValueChange={setSelectedNamespace}
>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Select namespace" />
</SelectTrigger>
<SelectContent>
{defaultI18nNamespaces.map((namespace: string) => (
<SelectItem key={namespace} value={namespace}>
{namespace}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Key</TableHead>
{visibleLocales.map((locale) => (
<TableHead key={locale}>{locale}</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{filteredKeys.map((key) => (
<TableRow key={key}>
<TableCell className="font-mono text-sm">
<div className="flex items-center justify-between">
<span>{key}</span>
</div>
</TableCell>
{visibleLocales.map((locale) => {
const translations = flattenedTranslations[locale] ?? {};
const baseTranslations =
flattenedTranslations[baseLocale] ?? {};
const value = translations[key];
const baseValue = baseTranslations[key];
const isMissing = !value;
const isDifferent = value !== baseValue;
return (
<TableCell
key={locale}
className={cn({
'bg-destructive/10': isMissing,
'bg-warning/10': !isMissing && isDifferent,
})}
>
<div className="flex items-center justify-between">
<span>
{value || (
<span className="text-destructive">Missing</span>
)}
</span>
</div>
</TableCell>
);
})}
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
);
}

View File

@@ -0,0 +1,46 @@
import { readFileSync, readdirSync } from 'node:fs';
import { join } from 'node:path';
const defaultI18nNamespaces = [
'common',
'auth',
'account',
'teams',
'billing',
'marketing',
];
export type TranslationData = {
[key: string]: string | TranslationData;
};
export type Translations = {
[locale: string]: {
[namespace: string]: TranslationData;
};
};
export async function loadTranslations() {
const localesPath = join(process.cwd(), '../web/public/locales');
const locales = readdirSync(localesPath);
const translations: Translations = {};
for (const locale of locales) {
translations[locale] = {};
for (const namespace of defaultI18nNamespaces) {
try {
const filePath = join(localesPath, locale, `${namespace}.json`);
const content = readFileSync(filePath, 'utf8');
translations[locale][namespace] = JSON.parse(content);
} catch (error) {
console.warn(
`Warning: Translation file not found for locale "${locale}" and namespace "${namespace}"`,
);
translations[locale][namespace] = {};
}
}
}
return translations;
}

View File

@@ -0,0 +1,35 @@
import { Metadata } from 'next';
import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs';
import { Page, PageBody, PageHeader } from '@kit/ui/page';
import { TranslationsComparison } from './components/translations-comparison';
import { loadTranslations } from './lib/translations-loader';
export const metadata: Metadata = {
title: 'Translations Comparison',
description: 'Compare translations across different languages',
};
export default async function TranslationsPage() {
const translations = await loadTranslations();
return (
<Page style={'custom'}>
<PageHeader
displaySidebarTrigger={false}
description={
<AppBreadcrumbs
values={{
translations: 'Translations',
}}
/>
}
/>
<PageBody className={'py-4'}>
<TranslationsComparison translations={translations} />
</PageBody>
</Page>
);
}

View File

@@ -0,0 +1,721 @@
'use client';
import { Fragment, useState } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { envVariables } from '@/app/variables/lib/env-variables-model';
import { EnvModeSelector } from '@/components/env-mode-selector';
import {
ChevronDown,
ChevronUp,
ChevronsUpDownIcon,
Copy,
Eye,
EyeOff,
InfoIcon,
} from 'lucide-react';
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
import { Badge } from '@kit/ui/badge';
import { Button } from '@kit/ui/button';
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuTrigger,
} from '@kit/ui/dropdown-menu';
import { Heading } from '@kit/ui/heading';
import { If } from '@kit/ui/if';
import { Input } from '@kit/ui/input';
import { toast } from '@kit/ui/sonner';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@kit/ui/tooltip';
import { cn } from '@kit/ui/utils';
import { AppEnvState, EnvVariableState } from '../lib/types';
export function AppEnvironmentVariablesManager({
state,
}: React.PropsWithChildren<{
state: AppEnvState;
}>) {
return (
<div className="flex flex-1 flex-col gap-y-4">
<Heading level={5}>Application: {state.appName}</Heading>
<div className={'flex flex-col space-y-4'}>
<EnvList appState={state} />
</div>
</div>
);
}
function EnvList({ appState }: { appState: AppEnvState }) {
const [expandedVars, setExpandedVars] = useState<Record<string, boolean>>({});
const [showValues, setShowValues] = useState<Record<string, boolean>>({});
const [search, setSearch] = useState('');
const searchParams = useSearchParams();
const secretVars = searchParams.get('secret') === 'true';
const publicVars = searchParams.get('public') === 'true';
const privateVars = searchParams.get('private') === 'true';
const overriddenVars = searchParams.get('overridden') === 'true';
const invalidVars = searchParams.get('invalid') === 'true';
const toggleExpanded = (key: string) => {
setExpandedVars((prev) => ({
...prev,
[key]: !prev[key],
}));
};
const toggleShowValue = (key: string) => {
setShowValues((prev) => ({
...prev,
[key]: !prev[key],
}));
};
const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text);
} catch (err) {
console.error('Failed to copy:', err);
}
};
const renderValue = (value: string, isVisible: boolean) => {
if (!isVisible) {
return '••••••••';
}
return value || '(empty)';
};
const renderVariable = (varState: EnvVariableState) => {
const isExpanded = expandedVars[varState.key] ?? false;
const isClientBundledValue = varState.key.startsWith('NEXT_PUBLIC_');
// public variables are always visible
const isValueVisible = showValues[varState.key] ?? isClientBundledValue;
// grab model is it's a kit variable
const model = envVariables.find(
(variable) => variable.name === varState.key,
);
const allVariables = Object.values(appState.variables).reduce(
(acc, variable) => ({
...acc,
[variable.key]: variable.effectiveValue,
}),
{},
);
const validation = model?.validate
? model.validate({
value: varState.effectiveValue,
variables: allVariables,
mode: appState.mode,
})
: {
success: true,
error: undefined,
};
const canExpand = varState.definitions.length > 1 || !validation.success;
return (
<div key={varState.key} className="animate-in fade-in rounded-lg border">
<div className="p-4">
<div className="flex items-start justify-between">
<div className="flex-1 flex-col gap-y-1">
<div className="flex items-center gap-2">
<span className="font-mono text-sm font-semibold">
{varState.key}
</span>
{varState.isOverridden && (
<Badge variant="warning">Overridden</Badge>
)}
</div>
<If condition={model}>
{(model) => (
<div className="flex items-center gap-2">
<span className="text-muted-foreground text-xs font-normal">
{model.description}
</span>
</div>
)}
</If>
<div className="mt-2 flex items-center gap-2">
<div className="bg-muted text-muted-foreground flex-1 rounded px-2 py-2 font-mono text-xs">
{renderValue(varState.effectiveValue, isValueVisible)}
</div>
<If condition={!isClientBundledValue}>
<Button
variant="ghost"
size={'icon'}
onClick={() => toggleShowValue(varState.key)}
>
{isValueVisible ? <EyeOff size={16} /> : <Eye size={16} />}
</Button>
</If>
<Button
variant="ghost"
onClick={() => copyToClipboard(varState.effectiveValue)}
size={'icon'}
>
<Copy size={16} />
</Button>
</div>
</div>
{canExpand && (
<Button
size={'icon'}
variant="ghost"
className="ml-4 rounded p-1 hover:bg-gray-100"
onClick={() => toggleExpanded(varState.key)}
>
{isExpanded ? (
<ChevronUp className="h-4 w-4" />
) : (
<ChevronDown className="h-4 w-4" />
)}
</Button>
)}
</div>
<div className="mt-2 flex gap-x-2">
<Badge
variant="outline"
className={cn({
'text-orange-500': !isClientBundledValue,
'text-green-500': isClientBundledValue,
})}
>
{isClientBundledValue ? `Public variable` : `Private variable`}
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<InfoIcon className="ml-2 h-3 w-3" />
</TooltipTrigger>
<TooltipContent>
{isClientBundledValue
? `This variable will be bundled into the client side. If this is a private variable, do not use "NEXT_PUBLIC".`
: `This variable is private and will not be bundled client side, so you cannot access it from React components rendered client side`}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</Badge>
<If condition={model?.secret}>
<Badge variant="outline" className={'text-destructive'}>
Secret Variable
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<InfoIcon className="ml-2 h-3 w-3" />
</TooltipTrigger>
<TooltipContent>
This is a secret key. Keep it safe!
</TooltipContent>
</Tooltip>
</TooltipProvider>
</Badge>
</If>
<Badge
variant={'outline'}
className={cn({
'text-destructive':
varState.effectiveSource === '.env.production',
})}
>
{varState.effectiveSource}
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<InfoIcon className="ml-2 h-3 w-3" />
</TooltipTrigger>
<TooltipContent>
{varState.effectiveSource === '.env.local'
? `These variables are specific to this machine and are not committed`
: varState.effectiveSource === '.env.development'
? `These variables are only being used during development`
: varState.effectiveSource === '.env'
? `These variables are shared under all modes`
: `These variables are only used in production mode`}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</Badge>
<If condition={varState.isOverridden}>
<Badge variant="warning">
Overridden in {varState.effectiveSource}
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<InfoIcon className="ml-2 h-3 w-3" />
</TooltipTrigger>
<TooltipContent>
This variable was overridden by a variable in{' '}
{varState.effectiveSource}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</Badge>
</If>
<If condition={!validation.success}>
<Badge variant="destructive">
Invalid Value
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<InfoIcon className="ml-2 h-3 w-3" />
</TooltipTrigger>
<TooltipContent>
This variable has an invalid value. Drop down to view the
errors.
</TooltipContent>
</Tooltip>
</TooltipProvider>
</Badge>
</If>
</div>
</div>
{isExpanded && canExpand && (
<div className="flex flex-col gap-y-2 border-t bg-gray-50 p-4">
<If condition={!validation.success}>
<div className={'flex flex-col space-y-2'}>
<Heading level={6} className="Errors">
Errors
</Heading>
<Alert variant="destructive">
<AlertTitle>Invalid Value</AlertTitle>
<AlertDescription>
The value for {varState.key} is invalid:
<pre>
<code>{JSON.stringify(validation, null, 2)}</code>
</pre>
</AlertDescription>
</Alert>
</div>
</If>
<If condition={varState.definitions.length > 1}>
<div className={'flex flex-col space-y-2'}>
<Heading level={6} className="text-sm font-medium">
Override Chain
</Heading>
<div className="space-y-2">
{varState.definitions.map((def) => (
<div
key={`${def.key}-${def.source}`}
className="flex items-center gap-2"
>
<Badge
variant={'outline'}
className={cn({
'text-destructive': def.source === '.env.production',
})}
>
{def.source}
</Badge>
<div className="font-mono text-sm">
{renderValue(def.value, isValueVisible)}
</div>
</div>
))}
</div>
</div>
</If>
</div>
)}
</div>
);
};
const filterVariable = (varState: EnvVariableState) => {
const model = envVariables.find(
(variable) => variable.name === varState.key,
);
if (
!search &&
!secretVars &&
!publicVars &&
!privateVars &&
!invalidVars &&
!overriddenVars
) {
return true;
}
const isSecret = model?.secret;
const isPublic = varState.key.startsWith('NEXT_PUBLIC_');
const isPrivate = !isPublic;
const isInSearch = search
? varState.key.toLowerCase().includes(search.toLowerCase())
: true;
if (isPublic && publicVars && isInSearch) {
return true;
}
if (isSecret && secretVars && isInSearch) {
return true;
}
if (isPrivate && privateVars && isInSearch) {
return true;
}
if (overriddenVars && varState.isOverridden && isInSearch) {
return true;
}
if (invalidVars) {
const allVariables = Object.values(appState.variables).reduce(
(acc, variable) => ({
...acc,
[variable.key]: variable.effectiveValue,
}),
{},
);
const hasError =
model && model.validate
? !model.validate({
value: varState.effectiveValue,
variables: allVariables,
mode: appState.mode,
}).success
: false;
if (hasError && isInSearch) return true;
}
return false;
};
const groups = Object.values(appState.variables)
.filter(filterVariable)
.reduce(
(acc, variable) => {
const group = acc.find((group) => group.category === variable.category);
if (!group) {
acc.push({
category: variable.category,
variables: [variable],
});
} else {
group.variables.push(variable);
}
return acc;
},
[] as Array<{ category: string; variables: Array<EnvVariableState> }>,
);
return (
<div className="flex flex-col gap-y-8">
<div className="flex items-center">
<div className="flex w-full space-x-2">
<div>
<EnvModeSelector mode={appState.mode} />
</div>
<div>
<FilterSwitcher
filters={{
secret: secretVars,
public: publicVars,
overridden: overriddenVars,
private: privateVars,
invalid: invalidVars,
}}
/>
</div>
<Input
className={'w-full'}
placeholder="Search variables"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="secondary"
onClick={() => {
const report = createReportFromEnvState(appState);
const promise = copyToClipboard(report);
toast.promise(promise, {
loading: 'Copying report...',
success:
'Report copied to clipboard. Please paste it in your ticket.',
error: 'Failed to copy report to clipboard',
});
}}
>
Copy to Clipboard
</Button>
</TooltipTrigger>
<TooltipContent>
Create a report from the environment variables. Useful for
creating support tickets.
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</div>
<div className="flex flex-col gap-1">
<Summary appState={appState} />
{groups.map((group) => (
<div
key={group.category}
className="flex flex-col gap-y-2.5 border-b border-dashed py-8 last:border-b-0"
>
<div>
<span className={'text-sm font-bold uppercase'}>
{group.category}
</span>
</div>
<div className="flex flex-col space-y-4">
{group.variables.map((item) => {
return (
<Fragment key={item.key}>{renderVariable(item)}</Fragment>
);
})}
</div>
</div>
))}
<If condition={groups.length === 0}>
<div className="flex h-full flex-1 flex-col items-center justify-center gap-y-4 py-16">
<div className="text-muted-foreground text-sm">
No variables found
</div>
</div>
</If>
</div>
</div>
);
}
function createReportFromEnvState(state: AppEnvState) {
let report = ``;
for (const key in state.variables) {
const variable = state.variables[key];
const variableReport = `${key}: ${JSON.stringify(variable, null, 2)}`;
``;
report += variableReport + '\n';
}
return report;
}
function FilterSwitcher(props: {
filters: {
secret: boolean;
public: boolean;
overridden: boolean;
private: boolean;
invalid: boolean;
};
}) {
const router = useRouter();
const secretVars = props.filters.secret;
const publicVars = props.filters.public;
const overriddenVars = props.filters.overridden;
const privateVars = props.filters.private;
const invalidVars = props.filters.invalid;
const handleFilterChange = (key: string, value: boolean) => {
const searchParams = new URLSearchParams(window.location.search);
const path = window.location.pathname;
if (key === 'all' && value) {
searchParams.delete('secret');
searchParams.delete('public');
searchParams.delete('overridden');
searchParams.delete('private');
searchParams.delete('invalid');
} else {
if (!value) {
searchParams.delete(key);
} else {
searchParams.set(key, 'true');
}
}
router.push(`${path}?${searchParams.toString()}`);
};
const buttonLabel = () => {
const filters = [];
if (secretVars) filters.push('Secret');
if (publicVars) filters.push('Public');
if (overriddenVars) filters.push('Overridden');
if (privateVars) filters.push('Private');
if (invalidVars) filters.push('Invalid');
if (filters.length === 0) return 'Filter variables';
return filters.join(', ');
};
const allSelected =
!secretVars && !publicVars && !overriddenVars && !invalidVars;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="font-normal">
{buttonLabel()}
<ChevronsUpDownIcon className="text-muted-foreground ml-1 h-3 w-3" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuCheckboxItem
checked={allSelected}
onCheckedChange={() => {
handleFilterChange('all', true);
}}
>
All
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={secretVars}
onCheckedChange={() => {
handleFilterChange('secret', !secretVars);
}}
>
Secret
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={privateVars}
onCheckedChange={() => {
handleFilterChange('private', !privateVars);
}}
>
Private
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={publicVars}
onCheckedChange={() => {
handleFilterChange('public', !publicVars);
}}
>
Public
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={invalidVars}
onCheckedChange={() => {
handleFilterChange('invalid', !invalidVars);
}}
>
Invalid
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={overriddenVars}
onCheckedChange={() => {
handleFilterChange('overridden', !overriddenVars);
}}
>
Overridden
</DropdownMenuCheckboxItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
function Summary({ appState }: { appState: AppEnvState }) {
const varsArray = Object.values(appState.variables);
const overridden = varsArray.filter((variable) => variable.isOverridden);
const allVariables = varsArray.reduce(
(acc, variable) => ({
...acc,
[variable.key]: variable.effectiveValue,
}),
{},
);
const errors = varsArray.filter((variable) => {
const model = envVariables.find((v) => variable.key === v.name);
const validation =
model && model.validate
? model.validate({
value: variable.effectiveValue,
variables: allVariables,
mode: appState.mode,
})
: {
success: true,
};
return !validation.success;
});
return (
<div className="flex flex-col space-y-4">
<div className="flex items-center gap-x-2">
<Badge variant={errors.length === 0 ? 'success' : 'destructive'}>
{errors.length} Errors
</Badge>
<Badge variant={overridden.length === 0 ? 'success' : 'warning'}>
{overridden.length} Overridden Variables
</Badge>
</div>
</div>
);
}

View File

@@ -0,0 +1,244 @@
import 'server-only';
import { envVariables } from '@/app/variables/lib/env-variables-model';
import fs from 'fs/promises';
import path from 'path';
import {
AppEnvState,
EnvFileInfo,
EnvMode,
EnvVariableState,
ScanOptions,
} from './types';
// Define precedence order for each mode
const ENV_FILE_PRECEDENCE: Record<EnvMode, string[]> = {
development: [
'.env',
'.env.development',
'.env.local',
'.env.development.local',
],
production: [
'.env',
'.env.production',
'.env.local',
'.env.production.local',
],
};
function getSourcePrecedence(source: string, mode: EnvMode): number {
return ENV_FILE_PRECEDENCE[mode].indexOf(source);
}
export async function scanMonorepoEnv(
options: ScanOptions,
): Promise<EnvFileInfo[]> {
const {
rootDir = path.resolve(process.cwd(), '../..'),
apps = ['web'],
mode,
} = options;
const envTypes = ENV_FILE_PRECEDENCE[mode];
const appsDir = path.join(rootDir, 'apps');
const results: EnvFileInfo[] = [];
try {
const appDirs = await fs.readdir(appsDir);
for (const appName of appDirs) {
if (apps.length > 0 && !apps.includes(appName)) {
continue;
}
const appDir = path.join(appsDir, appName);
const stat = await fs.stat(appDir);
if (!stat.isDirectory()) {
continue;
}
const appInfo: EnvFileInfo = {
appName,
filePath: appDir,
variables: [],
};
for (const envType of envTypes) {
const envPath = path.join(appDir, envType);
try {
const content = await fs.readFile(envPath, 'utf-8');
const vars = parseEnvFile(content, envType);
appInfo.variables.push(...vars);
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
console.warn(`Error reading ${envPath}:`, error);
}
}
}
if (appInfo.variables.length > 0) {
results.push(appInfo);
}
}
} catch (error) {
console.error('Error scanning monorepo:', error);
throw error;
}
return results;
}
function parseEnvFile(content: string, source: string) {
const variables: Array<{ key: string; value: string; source: string }> = [];
const lines = content.split('\n');
for (const line of lines) {
// Skip comments and empty lines
if (line.trim().startsWith('#') || !line.trim()) {
continue;
}
// Match KEY=VALUE pattern, handling quotes
const match = line.match(/^([^=]+)=(.*)$/);
if (match) {
const [, key = '', rawValue] = match;
let value = rawValue ?? '';
// Remove quotes if present
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
// Handle escaped quotes within the value
value = value
.replace(/\\"/g, '"')
.replace(/\\'/g, "'")
.replace(/\\\\/g, '\\');
variables.push({
key: key.trim(),
value: value.trim(),
source,
});
}
}
return variables;
}
export function processEnvDefinitions(
envInfo: EnvFileInfo,
mode: EnvMode,
): AppEnvState {
const variableMap: Record<string, EnvVariableState> = {};
// First pass: Collect all definitions
for (const variable of envInfo.variables) {
if (!variable) {
continue;
}
const model = envVariables.find((v) => variable.key === v.name);
if (!variableMap[variable.key]) {
variableMap[variable.key] = {
key: variable.key,
definitions: [],
effectiveValue: variable.value,
effectiveSource: variable.source,
isOverridden: false,
category: model ? model.category : 'Custom',
};
}
const varState = variableMap[variable.key];
if (!varState) {
continue;
}
varState.definitions.push({
key: variable.key,
value: variable.value,
source: variable.source,
});
}
// Second pass: Determine effective values and override status
for (const key in variableMap) {
const varState = variableMap[key];
if (!varState) {
continue;
}
// Sort definitions by mode-specific precedence
varState.definitions.sort(
(a, b) =>
getSourcePrecedence(a.source, mode) -
getSourcePrecedence(b.source, mode),
);
if (varState.definitions.length > 1) {
const lastDef = varState.definitions[varState.definitions.length - 1];
if (!lastDef) {
continue;
}
const highestPrecedence = getSourcePrecedence(lastDef.source, mode);
varState.isOverridden = true;
varState.effectiveValue = lastDef.value;
varState.effectiveSource = lastDef.source;
// Check for conflicts at highest precedence
const conflictingDefs = varState.definitions.filter(
(def) => getSourcePrecedence(def.source, mode) === highestPrecedence,
);
if (conflictingDefs.length > 1) {
varState.effectiveSource = `${varState.effectiveSource}`;
}
}
}
return {
appName: envInfo.appName,
filePath: envInfo.filePath,
mode,
variables: variableMap,
};
}
export async function getEnvState(
options: ScanOptions,
): Promise<AppEnvState[]> {
const envInfos = await scanMonorepoEnv(options);
return envInfos.map((info) => processEnvDefinitions(info, options.mode));
}
// Utility function to get list of env files for current mode
export function getEnvFilesForMode(mode: EnvMode): string[] {
return ENV_FILE_PRECEDENCE[mode];
}
export async function getVariable(key: string, mode: EnvMode) {
// Get the processed environment state for all apps (you can limit to 'web' via options)
const envStates = await getEnvState({ mode, apps: ['web'] });
// Find the state for the "web" app.
const webState = envStates.find((state) => state.appName === 'web');
// Return the effectiveValue based on override status.
return webState?.variables[key]?.effectiveValue ?? '';
}

View File

@@ -0,0 +1,770 @@
import { EnvMode } from '@/app/variables/lib/types';
import { z } from 'zod';
export type EnvVariableModel = {
name: string;
description: string;
secret?: boolean;
category: string;
test?: (value: string) => Promise<boolean>;
validate?: (props: {
value: string;
variables: Record<string, string>;
mode: EnvMode;
}) => z.SafeParseReturnType<unknown, unknown>;
};
export const envVariables: EnvVariableModel[] = [
{
name: 'NEXT_PUBLIC_SITE_URL',
description:
'The URL of your site, used for generating absolute URLs. Must include the protocol.',
category: 'Site Configuration',
validate: ({ value, mode }) => {
if (mode === 'development') {
return z
.string()
.url({
message: `The NEXT_PUBLIC_SITE_URL variable must be a valid URL`,
})
.safeParse(value);
}
return z
.string()
.url({
message: `The NEXT_PUBLIC_SITE_URL variable must be a valid URL`,
})
.startsWith(
'https',
`The NEXT_PUBLIC_SITE_URL variable must start with https`,
)
.safeParse(value);
},
},
{
name: 'NEXT_PUBLIC_PRODUCT_NAME',
description:
"Your product's name, used consistently across the application interface.",
category: 'Site Configuration',
validate: ({ value }) => {
return z
.string()
.min(
1,
`The NEXT_PUBLIC_PRODUCT_NAME variable must be at least 1 character`,
)
.safeParse(value);
},
},
{
name: 'NEXT_PUBLIC_SITE_TITLE',
description:
"The site's title tag content, crucial for SEO and browser display.",
category: 'Site Configuration',
validate: ({ value }) => {
return z
.string()
.min(
1,
`The NEXT_PUBLIC_SITE_TITLE variable must be at least 1 character`,
)
.safeParse(value);
},
},
{
name: 'NEXT_PUBLIC_SITE_DESCRIPTION',
description:
"Your site's meta description, important for SEO optimization.",
category: 'Site Configuration',
validate: ({ value }) => {
return z
.string()
.min(
1,
`The NEXT_PUBLIC_SITE_DESCRIPTION variable must be at least 1 character`,
)
.optional()
.safeParse(value);
},
},
{
name: 'NEXT_PUBLIC_DEFAULT_LOCALE',
description: 'Sets the default language for your application.',
category: 'Localization',
validate: ({ value }) => {
return z
.string()
.min(
1,
`The NEXT_PUBLIC_DEFAULT_LOCALE variable must be at least 1 character`,
)
.optional()
.safeParse(value);
},
},
{
name: 'NEXT_PUBLIC_AUTH_PASSWORD',
description: 'Enables or disables password-based authentication.',
category: 'Authentication',
validate: ({ value }) => {
return z.coerce.boolean().optional().safeParse(value);
},
},
{
name: 'NEXT_PUBLIC_AUTH_MAGIC_LINK',
description: 'Enables or disables magic link authentication.',
category: 'Authentication',
validate: ({ value }) => {
return z.coerce.boolean().optional().safeParse(value);
},
},
{
name: 'NEXT_PUBLIC_CAPTCHA_SITE_KEY',
description: 'Your Cloudflare Captcha site key for form protection.',
category: 'Security',
validate: ({ value }) => {
return z.string().optional().safeParse(value);
},
},
{
name: 'CAPTCHA_SECRET_TOKEN',
description:
'Your Cloudflare Captcha secret token for backend verification.',
category: 'Security',
secret: true,
validate: ({ value }) => {
return z
.string()
.min(
1,
`The CAPTCHA_SECRET_TOKEN variable must be at least 1 character`,
)
.optional()
.safeParse(value);
},
},
{
name: 'NEXT_PUBLIC_USER_NAVIGATION_STYLE',
description:
'Controls user navigation layout. Options: sidebar, header, or custom.',
category: 'Navigation',
validate: ({ value }) => {
return z
.enum(['sidebar', 'header', 'custom'])
.optional()
.safeParse(value);
},
},
{
name: 'NEXT_PUBLIC_HOME_SIDEBAR_COLLAPSED',
description: 'Sets the default state of the home sidebar.',
category: 'Navigation',
validate: ({ value }) => {
return z.coerce.boolean().optional().safeParse(value);
},
},
{
name: 'NEXT_PUBLIC_TEAM_NAVIGATION_STYLE',
description:
'Controls team navigation layout. Options: sidebar, header, or custom.',
category: 'Navigation',
validate: ({ value }) => {
return z
.enum(['sidebar', 'header', 'custom'])
.optional()
.safeParse(value);
},
},
{
name: 'NEXT_PUBLIC_TEAM_SIDEBAR_COLLAPSED',
description: 'Sets the default state of the team sidebar.',
category: 'Navigation',
validate: ({ value }) => {
return z.coerce.boolean().optional().safeParse(value);
},
},
{
name: 'NEXT_PUBLIC_SIDEBAR_COLLAPSIBLE_STYLE',
description:
'Defines sidebar collapse behavior. Options: offscreen, icon, or none.',
category: 'Navigation',
validate: ({ value }) => {
return z.enum(['offscreen', 'icon', 'none']).optional().safeParse(value);
},
},
{
name: 'NEXT_PUBLIC_DEFAULT_THEME_MODE',
description:
'Controls the default theme appearance. Options: light, dark, or system.',
category: 'Theme',
validate: ({ value }) => {
return z.enum(['light', 'dark', 'system']).optional().safeParse(value);
},
},
{
name: 'NEXT_PUBLIC_ENABLE_THEME_TOGGLE',
description: 'Controls visibility of the theme toggle feature.',
category: 'Theme',
validate: ({ value }) => {
return z.coerce.boolean().optional().safeParse(value);
},
},
{
name: 'NEXT_PUBLIC_ENABLE_SIDEBAR_TRIGGER',
description: 'Controls visibility of the sidebar trigger feature.',
category: 'Navigation',
validate: ({ value }) => {
return z.coerce.boolean().optional().safeParse(value);
},
},
{
name: 'NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_DELETION',
description: 'Allows users to delete their personal accounts.',
category: 'Features',
validate: ({ value }) => {
return z.coerce.boolean().optional().safeParse(value);
},
},
{
name: 'NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_BILLING',
description: 'Enables billing features for personal accounts.',
category: 'Features',
validate: ({ value }) => {
return z.coerce.boolean().optional().safeParse(value);
},
},
{
name: 'NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS',
description: 'Master switch for team account functionality.',
category: 'Features',
validate: ({ value }) => {
return z.coerce.boolean().optional().safeParse(value);
},
},
{
name: 'NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_CREATION',
description: 'Controls ability to create new team accounts.',
category: 'Features',
validate: ({ value }) => {
return z.coerce.boolean().optional().safeParse(value);
},
},
{
name: 'NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_DELETION',
description: 'Allows team account deletion.',
category: 'Features',
validate: ({ value }) => {
return z.coerce.boolean().optional().safeParse(value);
},
},
{
name: 'NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_BILLING',
description: 'Enables billing features for team accounts.',
category: 'Features',
validate: ({ value }) => {
return z.coerce.boolean().optional().safeParse(value);
},
},
{
name: 'NEXT_PUBLIC_ENABLE_NOTIFICATIONS',
description: 'Controls the notification system.',
category: 'Notifications',
validate: ({ value }) => {
return z.coerce.boolean().optional().safeParse(value);
},
},
{
name: 'NEXT_PUBLIC_REALTIME_NOTIFICATIONS',
description: 'Enables real-time notifications using Supabase Realtime.',
category: 'Notifications',
validate: ({ value }) => {
return z.coerce.boolean().optional().safeParse(value);
},
},
{
name: 'NEXT_PUBLIC_SUPABASE_URL',
description: 'Your Supabase project URL.',
category: 'Supabase',
validate: ({ value, mode }) => {
if (mode === 'development') {
return z
.string()
.url({
message: `The NEXT_PUBLIC_SUPABASE_URL variable must be a valid URL`,
})
.safeParse(value);
}
return z
.string()
.url({
message: `The NEXT_PUBLIC_SUPABASE_URL variable must be a valid URL`,
})
.startsWith(
'https',
`The NEXT_PUBLIC_SUPABASE_URL variable must start with https`,
)
.safeParse(value);
},
},
{
name: 'NEXT_PUBLIC_SUPABASE_ANON_KEY',
description: 'Your Supabase anonymous API key.',
category: 'Supabase',
validate: ({ value }) => {
return z
.string()
.min(
1,
`The NEXT_PUBLIC_SUPABASE_ANON_KEY variable must be at least 1 character`,
)
.optional()
.safeParse(value);
},
},
{
name: 'SUPABASE_SERVICE_ROLE_KEY',
description: 'Your Supabase service role key (keep this secret!).',
category: 'Supabase',
secret: true,
validate: ({ value, variables }) => {
return z
.string()
.min(
1,
`The SUPABASE_SERVICE_ROLE_KEY variable must be at least 1 character`,
)
.optional()
.refine(
(value) => {
return value !== variables['NEXT_PUBLIC_SUPABASE_ANON_KEY'];
},
{
message: `The SUPABASE_SERVICE_ROLE_KEY variable must be different from NEXT_PUBLIC_SUPABASE_ANON_KEY`,
},
)
.safeParse(value);
},
},
{
name: 'SUPABASE_DB_WEBHOOK_SECRET',
description: 'Secret key for Supabase webhook verification.',
category: 'Supabase',
secret: true,
validate: ({ value }) => {
return z
.string()
.min(
1,
`The SUPABASE_DB_WEBHOOK_SECRET variable must be at least 1 character`,
)
.optional()
.safeParse(value);
},
},
{
name: 'NEXT_PUBLIC_BILLING_PROVIDER',
description:
'Your chosen billing provider. Options: stripe or lemon-squeezy.',
category: 'Billing',
validate: ({ value }) => {
return z.enum(['stripe', 'lemon-squeezy']).optional().safeParse(value);
},
},
{
name: 'NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY',
description: 'Your Stripe publishable key.',
category: 'Billing',
validate: ({ value }) => {
return z
.string()
.min(
1,
`The NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY variable must be at least 1 character`,
)
.optional()
.safeParse(value);
},
},
{
name: 'STRIPE_SECRET_KEY',
description: 'Your Stripe secret key.',
category: 'Billing',
secret: true,
validate: ({ value }) => {
return z
.string()
.min(1, `The STRIPE_SECRET_KEY variable must be at least 1 character`)
.optional()
.safeParse(value);
},
},
{
name: 'STRIPE_WEBHOOK_SECRET',
description: 'Your Stripe webhook secret.',
category: 'Billing',
secret: true,
validate: ({ value }) => {
return z
.string()
.min(
1,
`The STRIPE_WEBHOOK_SECRET variable must be at least 1 character`,
)
.optional()
.safeParse(value);
},
},
{
name: 'LEMON_SQUEEZY_SECRET_KEY',
description: 'Your Lemon Squeezy secret key.',
category: 'Billing',
secret: true,
validate: ({ value }) => {
return z
.string()
.min(
1,
`The LEMON_SQUEEZY_SECRET_KEY variable must be at least 1 character`,
)
.optional()
.safeParse(value);
},
},
{
name: 'LEMON_SQUEEZY_STORE_ID',
description: 'Your Lemon Squeezy store ID.',
category: 'Billing',
validate: ({ value }) => {
return z
.string()
.min(
1,
`The LEMON_SQUEEZY_STORE_ID variable must be at least 1 character`,
)
.optional()
.safeParse(value);
},
},
{
name: 'LEMON_SQUEEZY_SIGNING_SECRET',
description: 'Your Lemon Squeezy signing secret.',
category: 'Billing',
secret: true,
validate: ({ value }) => {
return z
.string()
.min(
1,
`The LEMON_SQUEEZY_SIGNING_SECRET variable must be at least 1 character`,
)
.optional()
.safeParse(value);
},
},
{
name: 'MAILER_PROVIDER',
description: 'Your email service provider. Options: nodemailer or resend.',
category: 'Email',
validate: ({ value }) => {
return z.enum(['nodemailer', 'resend']).optional().safeParse(value);
},
},
{
name: 'EMAIL_SENDER',
description: 'Default sender email address.',
category: 'Email',
validate: ({ value }) => {
return z
.string()
.min(1, `The EMAIL_SENDER variable must be at least 1 character`)
.safeParse(value);
},
},
{
name: 'CONTACT_EMAIL',
description: 'Email address for contact form submissions.',
category: 'Email',
validate: ({ value }) => {
return z
.string()
.email()
.min(1, `The CONTACT_EMAIL variable must be at least 1 character`)
.safeParse(value);
},
},
{
name: 'RESEND_API_KEY',
description: 'Your Resend API key.',
category: 'Email',
secret: true,
validate: ({ value }) => {
return z
.string()
.min(1, `The RESEND_API_KEY variable must be at least 1 character`)
.safeParse(value);
},
},
{
name: 'EMAIL_HOST',
description: 'SMTP host for Nodemailer configuration.',
category: 'Email',
validate: ({ value }) => {
return z.string().safeParse(value);
},
},
{
name: 'EMAIL_PORT',
description: 'SMTP port for Nodemailer configuration.',
category: 'Email',
validate: ({ value }) => {
return z.coerce
.number()
.min(1, `The EMAIL_PORT variable must be at least 1 character`)
.max(65535, `The EMAIL_PORT variable must be at most 65535`)
.safeParse(value);
},
},
{
name: 'EMAIL_USER',
description: 'SMTP user for Nodemailer configuration.',
category: 'Email',
validate: ({ value }) => {
return z
.string()
.min(1, `The EMAIL_USER variable must be at least 1 character`)
.safeParse(value);
},
},
{
name: 'EMAIL_PASSWORD',
description: 'SMTP password for Nodemailer configuration.',
category: 'Email',
secret: true,
validate: ({ value }) => {
return z
.string()
.min(1, `The EMAIL_PASSWORD variable must be at least 1 character`)
.safeParse(value);
},
},
{
name: 'EMAIL_TLS',
description: 'Whether to use TLS for SMTP connection.',
category: 'Email',
validate: ({ value }) => {
return z.coerce.boolean().optional().safeParse(value);
},
},
{
name: 'CMS_CLIENT',
description: 'Your chosen CMS system. Options: wordpress or keystatic.',
category: 'CMS',
validate: ({ value }) => {
return z.enum(['wordpress', 'keystatic']).optional().safeParse(value);
},
},
{
name: 'NEXT_PUBLIC_KEYSTATIC_STORAGE_KIND',
description: 'Your Keystatic storage kind. Options: local, cloud, github.',
category: 'CMS',
validate: ({ value }) => {
return z.enum(['local', 'cloud', 'github']).optional().safeParse(value);
},
},
{
name: 'NEXT_PUBLIC_KEYSTATIC_STORAGE_REPO',
description: 'Your Keystatic storage repo.',
category: 'CMS',
validate: ({ value }) => {
return z
.string()
.min(
1,
`The NEXT_PUBLIC_KEYSTATIC_STORAGE_REPO variable must be at least 1 character`,
)
.optional()
.safeParse(value);
},
},
{
name: 'KEYSTATIC_GITHUB_TOKEN',
description: 'Your Keystatic GitHub token.',
category: 'CMS',
secret: true,
validate: ({ value }) => {
return z
.string()
.min(
1,
`The KEYSTATIC_GITHUB_TOKEN variable must be at least 1 character`,
)
.optional()
.safeParse(value);
},
},
{
name: 'KEYSTATIC_PATH_PREFIX',
description: 'Your Keystatic path prefix.',
category: 'CMS',
validate: ({ value }) => {
return z
.string()
.min(
1,
`The KEYSTATIC_PATH_PREFIX variable must be at least 1 character`,
)
.optional()
.safeParse(value);
},
},
{
name: 'NEXT_PUBLIC_KEYSTATIC_CONTENT_PATH',
description: 'Your Keystatic content path.',
category: 'CMS',
validate: ({ value }) => {
return z
.string()
.min(
1,
`The NEXT_PUBLIC_KEYSTATIC_CONTENT_PATH variable must be at least 1 character`,
)
.optional()
.safeParse(value);
},
},
{
name: 'WORDPRESS_API_URL',
description: 'WordPress API URL when using WordPress as CMS.',
category: 'CMS',
validate: ({ value }) => {
return z
.string()
.url({
message: `The WORDPRESS_API_URL variable must be a valid URL`,
})
.safeParse(value);
},
},
{
name: 'NEXT_PUBLIC_LOCALES_PATH',
description: 'The path to your locales folder.',
category: 'Localization',
validate: ({ value }) => {
return z
.string()
.min(
1,
`The NEXT_PUBLIC_LOCALES_PATH variable must be at least 1 character`,
)
.optional()
.safeParse(value);
},
},
{
name: 'NEXT_PUBLIC_LANGUAGE_PRIORITY',
description: 'The priority setting as to how infer the language.',
category: 'Localization',
validate: ({ value }) => {
return z.enum(['user', 'application']).optional().safeParse(value);
},
},
{
name: 'NEXT_PUBLIC_ENABLE_VERSION_UPDATER',
description:
'Enables the version updater to poll the latest version and notify the user.',
category: 'Features',
validate: ({ value }) => {
return z.coerce.boolean().optional().safeParse(value);
},
},
{
name: `ENABLE_REACT_COMPILER`,
description: 'Enables the React compiler [experimental]',
category: 'Features',
validate: ({ value }) => {
return z.coerce.boolean().optional().safeParse(value);
},
},
{
name: 'NEXT_PUBLIC_MONITORING_PROVIDER',
description: 'The monitoring provider to use.',
category: 'Monitoring',
validate: ({ value }) => {
return z.enum(['baselime', 'sentry']).optional().safeParse(value);
},
},
{
name: 'NEXT_PUBLIC_BASELIME_KEY',
description: 'The Baselime key to use.',
category: 'Monitoring',
validate: ({ value }) => {
return z
.string()
.min(
1,
`The NEXT_PUBLIC_BASELIME_KEY variable must be at least 1 character`,
)
.optional()
.safeParse(value);
},
},
{
name: 'STRIPE_ENABLE_TRIAL_WITHOUT_CC',
description: 'Enables trial plans without credit card.',
category: 'Billing',
validate: ({ value }) => {
return z.coerce.boolean().optional().safeParse(value);
},
},
{
name: 'NEXT_PUBLIC_VERSION_UPDATER_REFETCH_INTERVAL_SECONDS',
description: 'The interval in seconds to check for updates.',
category: 'Features',
validate: ({ value }) => {
return z.coerce
.number()
.min(
1,
`The NEXT_PUBLIC_VERSION_UPDATER_REFETCH_INTERVAL_SECONDS variable must be at least 1 character`,
)
.max(
86400,
`The NEXT_PUBLIC_VERSION_UPDATER_REFETCH_INTERVAL_SECONDS variable must be at most 86400`,
)
.optional()
.safeParse(value);
},
},
{
name: 'NEXT_PUBLIC_THEME_COLOR',
description: 'The default theme color.',
category: 'Theme',
validate: ({ value }) => {
return z
.string()
.min(
1,
`The NEXT_PUBLIC_THEME_COLOR variable must be at least 1 character`,
)
.optional()
.safeParse(value);
},
},
{
name: 'NEXT_PUBLIC_THEME_COLOR_DARK',
description: 'The default theme color for dark mode.',
category: 'Theme',
validate: ({ value }) => {
return z
.string()
.min(
1,
`The NEXT_PUBLIC_THEME_COLOR_DARK variable must be at least 1 character`,
)
.optional()
.safeParse(value);
},
},
];

View File

@@ -0,0 +1,40 @@
export type EnvMode = 'development' | 'production';
export type ScanOptions = {
apps?: string[];
rootDir?: string;
mode: EnvMode;
};
export type EnvDefinition = {
key: string;
value: string;
source: string;
};
export type EnvVariableState = {
key: string;
category: string;
definitions: EnvDefinition[];
effectiveValue: string;
isOverridden: boolean;
effectiveSource: string;
};
export type AppEnvState = {
appName: string;
filePath: string;
mode: EnvMode;
variables: Record<string, EnvVariableState>;
};
export type EnvFileInfo = {
appName: string;
filePath: string;
variables: Array<{
key: string;
value: string;
source: string;
}>;
};

View File

@@ -0,0 +1,55 @@
import { use } from 'react';
import {
processEnvDefinitions,
scanMonorepoEnv,
} from '@/app/variables/lib/env-scanner';
import { EnvMode } from '@/app/variables/lib/types';
import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs';
import { Page, PageBody, PageHeader } from '@kit/ui/page';
import { AppEnvironmentVariablesManager } from './components/app-environment-variables-manager';
type VariablesPageProps = {
searchParams: Promise<{ mode?: EnvMode }>;
};
export const metadata = {
title: 'Environment Variables',
};
export default function VariablesPage({ searchParams }: VariablesPageProps) {
const { mode = 'development' } = use(searchParams);
const apps = use(scanMonorepoEnv({ mode }));
return (
<Page style={'custom'}>
<PageHeader
displaySidebarTrigger={false}
description={
<AppBreadcrumbs
values={{
variables: 'Environment Variables',
}}
/>
}
/>
<PageBody>
<div className={'flex flex-col space-y-4 pb-16'}>
{apps.map((app) => {
const appEnvState = processEnvDefinitions(app, mode);
return (
<AppEnvironmentVariablesManager
key={app.appName}
state={appEnvState}
/>
);
})}
</div>
</PageBody>
</Page>
);
}

View File

@@ -0,0 +1,13 @@
import { DevToolSidebar } from '@/components/app-sidebar';
import { SidebarInset, SidebarProvider } from '@kit/ui/shadcn-sidebar';
export function DevToolLayout(props: React.PropsWithChildren) {
return (
<SidebarProvider>
<DevToolSidebar />
<SidebarInset>{props.children}</SidebarInset>
</SidebarProvider>
);
}

View File

@@ -0,0 +1,79 @@
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import {
BoltIcon,
LanguagesIcon,
LayoutDashboardIcon,
MailIcon,
} from 'lucide-react';
import {
Sidebar,
SidebarGroup,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from '@kit/ui/shadcn-sidebar';
import { isRouteActive } from '@kit/ui/utils';
const routes = [
{
label: 'Dashboard',
path: '/',
Icon: LayoutDashboardIcon,
},
{
label: 'Environment Variables',
path: '/variables',
Icon: BoltIcon,
},
{
label: 'Emails',
path: '/emails',
Icon: MailIcon,
},
{
label: 'Translations',
path: '/translations',
Icon: LanguagesIcon,
},
];
export function DevToolSidebar({
...props
}: React.ComponentProps<typeof Sidebar>) {
const pathname = usePathname();
return (
<Sidebar collapsible="icon" {...props}>
<SidebarHeader>
<b className="p-1 font-mono text-xs font-semibold">Makerkit Dev Tool</b>
</SidebarHeader>
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
<SidebarGroupLabel>Dev Tools</SidebarGroupLabel>
<SidebarMenu>
{routes.map((route) => (
<SidebarMenuItem key={route.path}>
<SidebarMenuButton
isActive={isRouteActive(route.path, pathname, false)}
asChild
>
<Link href={route.path}>
<route.Icon className="h-4 w-4" />
<span>{route.label}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroup>
</Sidebar>
);
}

View File

@@ -0,0 +1,41 @@
'use client';
import { useRouter } from 'next/navigation';
import { EnvMode } from '@/app/variables/lib/types';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@kit/ui/select';
export function EnvModeSelector({ mode }: { mode: EnvMode }) {
const router = useRouter();
const handleModeChange = (value: EnvMode) => {
const searchParams = new URLSearchParams(window.location.search);
const path = window.location.pathname;
searchParams.set('mode', value);
router.push(`${path}?${searchParams.toString()}`);
};
return (
<div>
<Select name={'mode'} defaultValue={mode} onValueChange={handleModeChange}>
<SelectTrigger>
<SelectValue placeholder="Select Mode" />
</SelectTrigger>
<SelectContent>
<SelectItem value="development">Development</SelectItem>
<SelectItem value="production">Production</SelectItem>
</SelectContent>
</Select>
</div>
);
}

View File

@@ -0,0 +1,35 @@
'use client';
import { useState } from 'react';
import { createPortal } from 'react-dom';
export const IFrame: React.FC<
React.IframeHTMLAttributes<unknown> & {
setInnerRef?: (ref: HTMLIFrameElement | undefined) => void;
appendStyles?: boolean;
theme?: 'light' | 'dark';
transparent?: boolean;
}
> = ({ children, setInnerRef, appendStyles = true, theme, ...props }) => {
const [ref, setRef] = useState<HTMLIFrameElement | null>();
const doc = ref?.contentWindow?.document as Document;
const mountNode = doc?.body;
return (
<iframe
{...props}
ref={(ref) => {
if (ref) {
setRef(ref);
if (setInnerRef) {
setInnerRef(ref);
}
}
}}
>
{mountNode ? createPortal(children, mountNode) : null}
</iframe>
);
};

View File

@@ -0,0 +1,32 @@
'use client';
import { useState } from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { Toaster } from '@kit/ui/sonner';
export function RootProviders({ children }: React.PropsWithChildren) {
return <ReactQueryProvider>{children}</ReactQueryProvider>;
}
function ReactQueryProvider(props: React.PropsWithChildren) {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000,
},
},
}),
);
return (
<QueryClientProvider client={queryClient}>
{props.children}
<Toaster position="top-center" />
</QueryClientProvider>
);
}

View File

@@ -0,0 +1,57 @@
'use client';
import { AlertCircle, CheckCircle2, XCircle } from 'lucide-react';
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
import { Card, CardContent } from '@kit/ui/card';
export const ServiceStatus = {
CHECKING: 'checking',
SUCCESS: 'success',
ERROR: 'error',
} as const;
type ServiceStatusType = (typeof ServiceStatus)[keyof typeof ServiceStatus];
const StatusIcons = {
[ServiceStatus.CHECKING]: <AlertCircle className="h-6 w-6 text-yellow-500" />,
[ServiceStatus.SUCCESS]: <CheckCircle2 className="h-6 w-6 text-green-500" />,
[ServiceStatus.ERROR]: <XCircle className="h-6 w-6 text-red-500" />,
};
interface ServiceCardProps {
name: string;
status: {
status: ServiceStatusType;
message?: string;
};
}
export const ServiceCard = ({ name, status }: ServiceCardProps) => {
return (
<Card className="w-full max-w-2xl">
<CardContent className="pt-6">
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
{StatusIcons[status.status]}
<div>
<h3 className="font-medium">{name}</h3>
<p className="text-sm text-gray-500">
{status.message ??
(status.status === ServiceStatus.CHECKING
? 'Checking connection...'
: status.status === ServiceStatus.SUCCESS
? 'Connected successfully'
: 'Connection failed')}
</p>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
);
};

View File

@@ -0,0 +1,16 @@
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
reactStrictMode: true,
transpilePackages: ['@kit/ui', '@kit/shared'],
experimental: {
reactCompiler: true,
},
logging: {
fetches: {
fullUrl: true,
},
},
};
export default nextConfig;

View File

@@ -0,0 +1,44 @@
{
"name": "dev-tool",
"version": "0.1.0",
"private": true,
"scripts": {
"clean": "git clean -xdf .next .turbo node_modules",
"dev": "next dev --turbo | pino-pretty -c",
"format": "prettier --check --write \"**/*.{js,cjs,mjs,ts,tsx,md,json}\""
},
"dependencies": {
"@hookform/resolvers": "^4.1.0",
"@tanstack/react-query": "5.66.7",
"lucide-react": "^0.475.0",
"next": "15.1.7",
"nodemailer": "^6.10.0",
"react": "19.0.0",
"react-dom": "19.0.0"
},
"devDependencies": {
"@kit/email-templates": "workspace:*",
"@kit/prettier-config": "workspace:*",
"@kit/shared": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@kit/ui": "workspace:*",
"@tailwindcss/postcss": "^4.0.7",
"@types/node": "^22.13.4",
"@types/nodemailer": "6.4.17",
"@types/react": "19.0.10",
"@types/react-dom": "19.0.4",
"babel-plugin-react-compiler": "beta",
"pino-pretty": "^13.0.0",
"react-hook-form": "^7.54.2",
"tailwindcss": "4.0.7",
"tailwindcss-animate": "^1.0.7",
"typescript": "^5.7.3",
"zod": "^3.24.2"
},
"prettier": "@kit/prettier-config",
"browserslist": [
"last 1 versions",
"> 0.7%",
"not dead"
]
}

View File

@@ -0,0 +1,5 @@
export default {
plugins: {
'@tailwindcss/postcss': {},
},
};

View File

@@ -0,0 +1,43 @@
/*
* global.css
*
* Global styles for the entire application
*/
/* Tailwind CSS */
@import 'tailwindcss';
/* local styles - update the below if you add a new style */
@import './theme.css';
@import './theme.utilities.css';
@import './shadcn-ui.css';
/* plugins - update the below if you add a new plugin */
@plugin "tailwindcss-animate";
/* content sources - update the below if you add a new path */
@source "../../../packages/ui/src/**/*.{ts,tsx}";
@source "../{app,components}/**/*.{ts,tsx}";
/* variants - update the below if you add a new variant */
@variant dark (&:where(.dark, .dark *));
@layer base {
body {
@apply bg-background text-foreground;
font-feature-settings: "rlig" 1, "calt" 1;
}
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--border, currentColor);
}
input::placeholder,
textarea::placeholder {
color: theme(--color-muted-foreground);
}
}

View File

@@ -0,0 +1,104 @@
/*
* shadcn-ui.css
*
* Update the below to customize your Shadcn UI CSS Colors.
* Refer to https://ui.shadcn.com/themes for applying new colors.
* NB: apply the hsl function to the colors copied from the theme.
*/
@layer base {
:root {
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
--font-heading: var(--font-sans);
--background: var(--color-white);
--foreground: var(--color-neutral-950);
--card: var(--color-white);
--card-foreground: var(--color-neutral-950);
--popover: var(--color-white);
--popover-foreground: var(--color-neutral-950);
--primary: var(--color-neutral-950);
--primary-foreground: var(--color-white);
--secondary: oklch(96.76% 0.0013 286.38);
--secondary-foreground: oklch(21.03% 0.0318 264.65);
--muted: oklch(96.71% 0.0029 264.54);
--muted-foreground: oklch(55.13% 0.0233 264.36);
--accent: oklch(96.76% 0.0013 286.38);
--accent-foreground: oklch(21.03% 0.0318 264.65);
--destructive: var(--color-red-500);
--destructive-foreground: var(--color-white);
--border: var(--color-gray-100);
--input: var(--color-gray-200);
--ring: var(--color-neutral-800);
--radius: 0.5rem;
--chart-1: var(--color-orange-400);
--chart-2: var(--color-teal-600);
--chart-3: var(--color-green-800);
--chart-4: var(--color-yellow-200);
--chart-5: var(--color-orange-200);
--sidebar-background: var(--color-neutral-50);
--sidebar-foreground: oklch(37.05% 0.012 285.8);
--sidebar-primary: var(--color-neutral-950);
--sidebar-primary-foreground: var(--color-white);
--sidebar-accent: var(--color-neutral-100);
--sidebar-accent-foreground: var(--color-neutral-950);
--sidebar-border: var(--border);
--sidebar-ring: var(--color-blue-500);
}
.dark {
--background: var(--color-neutral-900);
--foreground: var(--color-white);
--card: var(--color-neutral-900);
--card-foreground: var(--color-white);
--popover: var(--color-neutral-900);
--popover-foreground: var(--color-white);
--primary: var(--color-white);
--primary-foreground: var(--color-neutral-900);
--secondary: var(--color-neutral-800);
--secondary-foreground: oklch(98.43% 0.0017 247.84);
--muted: var(--color-neutral-800);
--muted-foreground: oklch(71.19% 0.0129 286.07);
--accent: var(--color-neutral-800);
--accent-foreground: oklch(98.48% 0 0);
--destructive: var(--color-red-700);
--destructive-foreground: var(--color-white);
--border: var(--color-neutral-800);
--input: var(--color-neutral-700);
--ring: oklch(87.09% 0.0055 286.29);
--chart-1: var(--color-blue-600);
--chart-2: var(--color-emerald-400);
--chart-3: var(--color-orange-400);
--chart-4: var(--color-purple-500);
--chart-5: var(--color-pink-500);
--sidebar-background: var(--color-neutral-900);
--sidebar-foreground: var(--color-white);
--sidebar-primary: var(--color-blue-500);
--sidebar-primary-foreground: var(--color-white);
--sidebar-accent: var(--color-neutral-800);
--sidebar-accent-foreground: var(--color-white);
--sidebar-border: var(--border);
--sidebar-ring: var(--color-blue-500);
}
}

View File

@@ -0,0 +1,116 @@
/*
* theme.css
*
* Shadcn UI theme
* Use this file to add any custom styles or override existing Shadcn UI styles
*/
/* container utility */
/* Shadcn UI theme */
@theme {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--radius-radius: var(--radius);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--font-sans: -apple-system, var(--font-sans);
--font-heading: var(--font-heading);
--color-sidebar: var(--sidebar-background);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--animate-fade-up: fade-up 0.5s;
--animate-fade-down: fade-down 0.5s;
--animate-accordion-down: accordion-down 0.2s ease-out;
--animate-accordion-up: accordion-up 0.2s ease-out;
@keyframes accordion-down {
from {
height: 0;
}
to {
height: var(--radix-accordion-content-height);
}
}
@keyframes accordion-up {
from {
height: var(--radix-accordion-content-height);
}
to {
height: 0;
}
}
@keyframes fade-up {
0% {
opacity: 0;
transform: translateY(10px);
}
80% {
opacity: 0.6;
}
100% {
opacity: 1;
transform: translateY(0px);
}
}
@keyframes fade-down {
0% {
opacity: 0;
transform: translateY(-10px);
}
80% {
opacity: 0.6;
}
100% {
opacity: 1;
transform: translateY(0px);
}
}
}

View File

@@ -0,0 +1,5 @@
@utility container {
margin-inline: auto;
@apply xl:max-w-[80rem] px-8;
}

View File

@@ -0,0 +1,28 @@
{
"extends": "@kit/tsconfig/base.json",
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}

View File

@@ -79,7 +79,6 @@
"@kit/tsconfig": "workspace:*", "@kit/tsconfig": "workspace:*",
"@next/bundle-analyzer": "15.1.7", "@next/bundle-analyzer": "15.1.7",
"@tailwindcss/postcss": "^4.0.7", "@tailwindcss/postcss": "^4.0.7",
"@types/mdx": "^2.0.13",
"@types/node": "^22.13.4", "@types/node": "^22.13.4",
"@types/react": "19.0.10", "@types/react": "19.0.10",
"@types/react-dom": "19.0.4", "@types/react-dom": "19.0.4",

View File

@@ -1,6 +1,6 @@
{ {
"name": "next-supabase-saas-kit-turbo", "name": "next-supabase-saas-kit-turbo",
"version": "2.2.0", "version": "2.3.0",
"private": true, "private": true,
"sideEffects": false, "sideEffects": false,
"engines": { "engines": {

193
pnpm-lock.yaml generated
View File

@@ -30,6 +30,82 @@ importers:
specifier: ^5.7.3 specifier: ^5.7.3
version: 5.7.3 version: 5.7.3
apps/dev-tool:
dependencies:
'@hookform/resolvers':
specifier: ^4.1.0
version: 4.1.0(react-hook-form@7.54.2(react@19.0.0))
'@tanstack/react-query':
specifier: 5.66.7
version: 5.66.7(react@19.0.0)
lucide-react:
specifier: ^0.475.0
version: 0.475.0(react@19.0.0)
next:
specifier: 15.1.7
version: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
nodemailer:
specifier: ^6.10.0
version: 6.10.0
react:
specifier: 19.0.0
version: 19.0.0
react-dom:
specifier: 19.0.0
version: 19.0.0(react@19.0.0)
devDependencies:
'@kit/email-templates':
specifier: workspace:*
version: link:../../packages/email-templates
'@kit/prettier-config':
specifier: workspace:*
version: link:../../tooling/prettier
'@kit/shared':
specifier: workspace:*
version: link:../../packages/shared
'@kit/tsconfig':
specifier: workspace:*
version: link:../../tooling/typescript
'@kit/ui':
specifier: workspace:*
version: link:../../packages/ui
'@tailwindcss/postcss':
specifier: ^4.0.7
version: 4.0.7
'@types/node':
specifier: ^22.13.4
version: 22.13.4
'@types/nodemailer':
specifier: 6.4.17
version: 6.4.17
'@types/react':
specifier: 19.0.10
version: 19.0.10
'@types/react-dom':
specifier: 19.0.4
version: 19.0.4(@types/react@19.0.10)
babel-plugin-react-compiler:
specifier: beta
version: 19.0.0-beta-21e868a-20250216
pino-pretty:
specifier: ^13.0.0
version: 13.0.0
react-hook-form:
specifier: ^7.54.2
version: 7.54.2(react@19.0.0)
tailwindcss:
specifier: 4.0.7
version: 4.0.7
tailwindcss-animate:
specifier: ^1.0.7
version: 1.0.7(tailwindcss@4.0.7)
typescript:
specifier: ^5.7.3
version: 5.7.3
zod:
specifier: ^3.24.2
version: 3.24.2
apps/e2e: apps/e2e:
devDependencies: devDependencies:
'@playwright/test': '@playwright/test':
@@ -46,7 +122,7 @@ importers:
dependencies: dependencies:
'@edge-csrf/nextjs': '@edge-csrf/nextjs':
specifier: 2.5.3-cloudflare-rc1 specifier: 2.5.3-cloudflare-rc1
version: 2.5.3-cloudflare-rc1(next@15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)) version: 2.5.3-cloudflare-rc1(next@15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))
'@hookform/resolvers': '@hookform/resolvers':
specifier: ^4.1.0 specifier: ^4.1.0
version: 4.1.0(react-hook-form@7.54.2(react@19.0.0)) version: 4.1.0(react-hook-form@7.54.2(react@19.0.0))
@@ -109,7 +185,7 @@ importers:
version: 0.0.8(@supabase/postgrest-js@1.18.1)(@supabase/supabase-js@2.48.1) version: 0.0.8(@supabase/postgrest-js@1.18.1)(@supabase/supabase-js@2.48.1)
'@makerkit/data-loader-supabase-nextjs': '@makerkit/data-loader-supabase-nextjs':
specifier: ^1.2.3 specifier: ^1.2.3
version: 1.2.3(@supabase/postgrest-js@1.18.1)(@supabase/supabase-js@2.48.1)(@tanstack/react-query@5.66.7(react@19.0.0))(next@15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0) version: 1.2.3(@supabase/postgrest-js@1.18.1)(@supabase/supabase-js@2.48.1)(@tanstack/react-query@5.66.7(react@19.0.0))(next@15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)
'@marsidev/react-turnstile': '@marsidev/react-turnstile':
specifier: ^1.1.0 specifier: ^1.1.0
version: 1.1.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) version: 1.1.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
@@ -133,10 +209,10 @@ importers:
version: 0.475.0(react@19.0.0) version: 0.475.0(react@19.0.0)
next: next:
specifier: 15.1.7 specifier: 15.1.7
version: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) version: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
next-sitemap: next-sitemap:
specifier: ^4.2.3 specifier: ^4.2.3
version: 4.2.3(next@15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)) version: 4.2.3(next@15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))
next-themes: next-themes:
specifier: 0.4.4 specifier: 0.4.4
version: 0.4.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0) version: 0.4.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
@@ -180,9 +256,6 @@ importers:
'@tailwindcss/postcss': '@tailwindcss/postcss':
specifier: ^4.0.7 specifier: ^4.0.7
version: 4.0.7 version: 4.0.7
'@types/mdx':
specifier: ^2.0.13
version: 2.0.13
'@types/node': '@types/node':
specifier: ^22.13.4 specifier: ^22.13.4
version: 22.13.4 version: 22.13.4
@@ -197,7 +270,7 @@ importers:
version: 10.4.20(postcss@8.5.2) version: 10.4.20(postcss@8.5.2)
babel-plugin-react-compiler: babel-plugin-react-compiler:
specifier: beta specifier: beta
version: 19.0.0-beta-714736e-20250131 version: 19.0.0-beta-21e868a-20250216
dotenv-cli: dotenv-cli:
specifier: ^8.0.0 specifier: ^8.0.0
version: 8.0.0 version: 8.0.0
@@ -302,7 +375,7 @@ importers:
version: 0.475.0(react@19.0.0) version: 0.475.0(react@19.0.0)
next: next:
specifier: 15.1.7 specifier: 15.1.7
version: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) version: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
react: react:
specifier: 19.0.0 specifier: 19.0.0
version: 19.0.0 version: 19.0.0
@@ -348,7 +421,7 @@ importers:
version: 19.0.10 version: 19.0.10
next: next:
specifier: 15.1.7 specifier: 15.1.7
version: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) version: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
react: react:
specifier: 19.0.0 specifier: 19.0.0
version: 19.0.0 version: 19.0.0
@@ -397,7 +470,7 @@ importers:
version: 4.1.0 version: 4.1.0
next: next:
specifier: 15.1.7 specifier: 15.1.7
version: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) version: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
react: react:
specifier: 19.0.0 specifier: 19.0.0
version: 19.0.0 version: 19.0.0
@@ -628,7 +701,7 @@ importers:
version: 0.475.0(react@19.0.0) version: 0.475.0(react@19.0.0)
next: next:
specifier: 15.1.7 specifier: 15.1.7
version: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) version: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
next-themes: next-themes:
specifier: 0.4.4 specifier: 0.4.4
version: 0.4.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0) version: 0.4.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
@@ -682,7 +755,7 @@ importers:
version: 0.0.8(@supabase/postgrest-js@1.18.1)(@supabase/supabase-js@2.48.1) version: 0.0.8(@supabase/postgrest-js@1.18.1)(@supabase/supabase-js@2.48.1)
'@makerkit/data-loader-supabase-nextjs': '@makerkit/data-loader-supabase-nextjs':
specifier: ^1.2.3 specifier: ^1.2.3
version: 1.2.3(@supabase/postgrest-js@1.18.1)(@supabase/supabase-js@2.48.1)(@tanstack/react-query@5.66.7(react@19.0.0))(next@15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0) version: 1.2.3(@supabase/postgrest-js@1.18.1)(@supabase/supabase-js@2.48.1)(@tanstack/react-query@5.66.7(react@19.0.0))(next@15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)
'@supabase/supabase-js': '@supabase/supabase-js':
specifier: 2.48.1 specifier: 2.48.1
version: 2.48.1 version: 2.48.1
@@ -700,7 +773,7 @@ importers:
version: 0.475.0(react@19.0.0) version: 0.475.0(react@19.0.0)
next: next:
specifier: 15.1.7 specifier: 15.1.7
version: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) version: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
react: react:
specifier: 19.0.0 specifier: 19.0.0
version: 19.0.0 version: 19.0.0
@@ -757,7 +830,7 @@ importers:
version: 0.475.0(react@19.0.0) version: 0.475.0(react@19.0.0)
next: next:
specifier: 15.1.7 specifier: 15.1.7
version: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) version: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
react-hook-form: react-hook-form:
specifier: ^7.54.2 specifier: ^7.54.2
version: 7.54.2(react@19.0.0) version: 7.54.2(react@19.0.0)
@@ -881,7 +954,7 @@ importers:
version: 0.475.0(react@19.0.0) version: 0.475.0(react@19.0.0)
next: next:
specifier: 15.1.7 specifier: 15.1.7
version: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) version: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
react: react:
specifier: 19.0.0 specifier: 19.0.0
version: 19.0.0 version: 19.0.0
@@ -930,7 +1003,7 @@ importers:
version: 5.66.7(react@19.0.0) version: 5.66.7(react@19.0.0)
next: next:
specifier: 15.1.7 specifier: 15.1.7
version: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) version: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
react: react:
specifier: 19.0.0 specifier: 19.0.0
version: 19.0.0 version: 19.0.0
@@ -1167,7 +1240,7 @@ importers:
version: 2.48.1 version: 2.48.1
next: next:
specifier: 15.1.7 specifier: 15.1.7
version: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) version: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
zod: zod:
specifier: ^3.24.2 specifier: ^3.24.2
version: 3.24.2 version: 3.24.2
@@ -1216,7 +1289,7 @@ importers:
version: 19.0.10 version: 19.0.10
next: next:
specifier: 15.1.7 specifier: 15.1.7
version: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) version: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
react: react:
specifier: 19.0.0 specifier: 19.0.0
version: 19.0.0 version: 19.0.0
@@ -1349,7 +1422,7 @@ importers:
version: 9.20.1(jiti@2.4.2) version: 9.20.1(jiti@2.4.2)
next: next:
specifier: 15.1.7 specifier: 15.1.7
version: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) version: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
next-themes: next-themes:
specifier: 0.4.4 specifier: 0.4.4
version: 0.4.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0) version: 0.4.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
@@ -2628,7 +2701,7 @@ packages:
'@radix-ui/react-context@1.1.1': '@radix-ui/react-context@1.1.1':
resolution: {integrity: sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==} resolution: {integrity: sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==}
peerDependencies: peerDependencies:
'@types/react': '*' '@types/react': npm:types-react@19.0.0-rc.1
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta: peerDependenciesMeta:
'@types/react': '@types/react':
@@ -2685,7 +2758,7 @@ packages:
'@radix-ui/react-focus-guards@1.1.1': '@radix-ui/react-focus-guards@1.1.1':
resolution: {integrity: sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==} resolution: {integrity: sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==}
peerDependencies: peerDependencies:
'@types/react': '*' '@types/react': npm:types-react@19.0.0-rc.1
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta: peerDependenciesMeta:
'@types/react': '@types/react':
@@ -3013,7 +3086,7 @@ packages:
'@radix-ui/react-use-layout-effect@1.1.0': '@radix-ui/react-use-layout-effect@1.1.0':
resolution: {integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==} resolution: {integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==}
peerDependencies: peerDependencies:
'@types/react': '*' '@types/react': npm:types-react@19.0.0-rc.1
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta: peerDependenciesMeta:
'@types/react': '@types/react':
@@ -3022,7 +3095,7 @@ packages:
'@radix-ui/react-use-previous@1.1.0': '@radix-ui/react-use-previous@1.1.0':
resolution: {integrity: sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==} resolution: {integrity: sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==}
peerDependencies: peerDependencies:
'@types/react': '*' '@types/react': npm:types-react@19.0.0-rc.1
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta: peerDependenciesMeta:
'@types/react': '@types/react':
@@ -3040,7 +3113,7 @@ packages:
'@radix-ui/react-use-size@1.1.0': '@radix-ui/react-use-size@1.1.0':
resolution: {integrity: sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==} resolution: {integrity: sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==}
peerDependencies: peerDependencies:
'@types/react': '*' '@types/react': npm:types-react@19.0.0-rc.1
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta: peerDependenciesMeta:
'@types/react': '@types/react':
@@ -4141,9 +4214,6 @@ packages:
'@types/mdurl@2.0.0': '@types/mdurl@2.0.0':
resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==}
'@types/mdx@2.0.13':
resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==}
'@types/minimatch@5.1.2': '@types/minimatch@5.1.2':
resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==}
@@ -4545,8 +4615,8 @@ packages:
resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==}
engines: {node: '>=10', npm: '>=6'} engines: {node: '>=10', npm: '>=6'}
babel-plugin-react-compiler@19.0.0-beta-714736e-20250131: babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216:
resolution: {integrity: sha512-frj2l6fRWVi26iw9WthFKyFyE4u5ZSHH3KdKiscOOwpz210seTtwnp0QbJmi8Zoa5HK7Fk2fH40JffN2y8GvLg==} resolution: {integrity: sha512-WDOBsm9t9P0RADm8CSlav5OqWvs+3mZFvrBo/qf3vuNtdz78OG5TFxOy7De8ePR3rA6qg1Qmcjjae6nR1pOpCA==}
balanced-match@1.0.2: balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
@@ -4628,9 +4698,6 @@ packages:
caniuse-lite@1.0.30001677: caniuse-lite@1.0.30001677:
resolution: {integrity: sha512-fmfjsOlJUpMWu+mAAtZZZHz7UEwsUxIIvu1TJfO1HqFQvB/B+ii0xr9B5HpbZY/mC4XZ8SvjHJqtAY6pDPQEog==} resolution: {integrity: sha512-fmfjsOlJUpMWu+mAAtZZZHz7UEwsUxIIvu1TJfO1HqFQvB/B+ii0xr9B5HpbZY/mC4XZ8SvjHJqtAY6pDPQEog==}
caniuse-lite@1.0.30001699:
resolution: {integrity: sha512-b+uH5BakXZ9Do9iK+CkDmctUSEqZl+SP056vc5usa0PL+ev5OHw003rZXcnjNDv3L8P5j6rwT6C0BPKSikW08w==}
caniuse-lite@1.0.30001700: caniuse-lite@1.0.30001700:
resolution: {integrity: sha512-2S6XIXwaE7K7erT8dY+kLQcpa5ms63XlRkMkReXjle+kf6c5g38vyMl+Z5y8dSxOFDhcFe+nxnn261PLxBSQsQ==} resolution: {integrity: sha512-2S6XIXwaE7K7erT8dY+kLQcpa5ms63XlRkMkReXjle+kf6c5g38vyMl+Z5y8dSxOFDhcFe+nxnn261PLxBSQsQ==}
@@ -8118,7 +8185,7 @@ snapshots:
'@babel/generator@7.26.5': '@babel/generator@7.26.5':
dependencies: dependencies:
'@babel/parser': 7.26.7 '@babel/parser': 7.26.7
'@babel/types': 7.26.7 '@babel/types': 7.26.8
'@jridgewell/gen-mapping': 0.3.8 '@jridgewell/gen-mapping': 0.3.8
'@jridgewell/trace-mapping': 0.3.25 '@jridgewell/trace-mapping': 0.3.25
jsesc: 3.1.0 jsesc: 3.1.0
@@ -8168,11 +8235,11 @@ snapshots:
'@babel/parser@7.26.2': '@babel/parser@7.26.2':
dependencies: dependencies:
'@babel/types': 7.26.7 '@babel/types': 7.26.8
'@babel/parser@7.26.7': '@babel/parser@7.26.7':
dependencies: dependencies:
'@babel/types': 7.26.7 '@babel/types': 7.26.8
'@babel/parser@7.26.8': '@babel/parser@7.26.8':
dependencies: dependencies:
@@ -8199,7 +8266,7 @@ snapshots:
dependencies: dependencies:
'@babel/code-frame': 7.26.2 '@babel/code-frame': 7.26.2
'@babel/parser': 7.26.2 '@babel/parser': 7.26.2
'@babel/types': 7.26.7 '@babel/types': 7.26.8
'@babel/template@7.26.8': '@babel/template@7.26.8':
dependencies: dependencies:
@@ -8213,7 +8280,7 @@ snapshots:
'@babel/generator': 7.26.5 '@babel/generator': 7.26.5
'@babel/parser': 7.26.7 '@babel/parser': 7.26.7
'@babel/template': 7.25.9 '@babel/template': 7.25.9
'@babel/types': 7.26.7 '@babel/types': 7.26.8
debug: 4.4.0 debug: 4.4.0
globals: 11.12.0 globals: 11.12.0
transitivePeerDependencies: transitivePeerDependencies:
@@ -8277,9 +8344,9 @@ snapshots:
'@discoveryjs/json-ext@0.5.7': {} '@discoveryjs/json-ext@0.5.7': {}
'@edge-csrf/nextjs@2.5.3-cloudflare-rc1(next@15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))': '@edge-csrf/nextjs@2.5.3-cloudflare-rc1(next@15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))':
dependencies: dependencies:
next: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) next: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@emnapi/runtime@1.3.1': '@emnapi/runtime@1.3.1':
dependencies: dependencies:
@@ -8702,7 +8769,7 @@ snapshots:
react: 19.0.0 react: 19.0.0
react-dom: 19.0.0(react@19.0.0) react-dom: 19.0.0(react@19.0.0)
optionalDependencies: optionalDependencies:
next: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) next: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -8790,7 +8857,7 @@ snapshots:
'@keystatic/core': 0.5.45(next@15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@keystatic/core': 0.5.45(next@15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@types/react': 19.0.10 '@types/react': 19.0.10
chokidar: 3.6.0 chokidar: 3.6.0
next: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) next: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
react: 19.0.0 react: 19.0.0
react-dom: 19.0.0(react@19.0.0) react-dom: 19.0.0(react@19.0.0)
server-only: 0.0.1 server-only: 0.0.1
@@ -8803,12 +8870,12 @@ snapshots:
'@supabase/supabase-js': 2.48.1 '@supabase/supabase-js': 2.48.1
ts-case-convert: 2.1.0 ts-case-convert: 2.1.0
'@makerkit/data-loader-supabase-nextjs@1.2.3(@supabase/postgrest-js@1.18.1)(@supabase/supabase-js@2.48.1)(@tanstack/react-query@5.66.7(react@19.0.0))(next@15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)': '@makerkit/data-loader-supabase-nextjs@1.2.3(@supabase/postgrest-js@1.18.1)(@supabase/supabase-js@2.48.1)(@tanstack/react-query@5.66.7(react@19.0.0))(next@15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)':
dependencies: dependencies:
'@makerkit/data-loader-supabase-core': 0.0.8(@supabase/postgrest-js@1.18.1)(@supabase/supabase-js@2.48.1) '@makerkit/data-loader-supabase-core': 0.0.8(@supabase/postgrest-js@1.18.1)(@supabase/supabase-js@2.48.1)
'@supabase/supabase-js': 2.48.1 '@supabase/supabase-js': 2.48.1
'@tanstack/react-query': 5.66.7(react@19.0.0) '@tanstack/react-query': 5.66.7(react@19.0.0)
next: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) next: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
react: 19.0.0 react: 19.0.0
transitivePeerDependencies: transitivePeerDependencies:
- '@supabase/postgrest-js' - '@supabase/postgrest-js'
@@ -11616,8 +11683,6 @@ snapshots:
'@types/mdurl@2.0.0': '@types/mdurl@2.0.0':
optional: true optional: true
'@types/mdx@2.0.13': {}
'@types/minimatch@5.1.2': {} '@types/minimatch@5.1.2': {}
'@types/ms@2.1.0': {} '@types/ms@2.1.0': {}
@@ -12146,9 +12211,9 @@ snapshots:
cosmiconfig: 7.1.0 cosmiconfig: 7.1.0
resolve: 1.22.10 resolve: 1.22.10
babel-plugin-react-compiler@19.0.0-beta-714736e-20250131: babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216:
dependencies: dependencies:
'@babel/types': 7.26.7 '@babel/types': 7.26.8
balanced-match@1.0.2: {} balanced-match@1.0.2: {}
@@ -12246,8 +12311,6 @@ snapshots:
caniuse-lite@1.0.30001677: {} caniuse-lite@1.0.30001677: {}
caniuse-lite@1.0.30001699: {}
caniuse-lite@1.0.30001700: {} caniuse-lite@1.0.30001700: {}
ccount@2.0.1: {} ccount@2.0.1: {}
@@ -12899,8 +12962,8 @@ snapshots:
'@typescript-eslint/parser': 8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3) '@typescript-eslint/parser': 8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3)
eslint: 9.20.1(jiti@2.4.2) eslint: 9.20.1(jiti@2.4.2)
eslint-import-resolver-node: 0.3.9 eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.7.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint@9.20.1(jiti@2.4.2)))(eslint@9.20.1(jiti@2.4.2)) eslint-import-resolver-typescript: 3.7.0(eslint-plugin-import@2.31.0)(eslint@9.20.1(jiti@2.4.2))
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint@9.20.1(jiti@2.4.2)))(eslint@9.20.1(jiti@2.4.2)))(eslint@9.20.1(jiti@2.4.2)) eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.24.1(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint@9.20.1(jiti@2.4.2))
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.20.1(jiti@2.4.2)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.20.1(jiti@2.4.2))
eslint-plugin-react: 7.37.4(eslint@9.20.1(jiti@2.4.2)) eslint-plugin-react: 7.37.4(eslint@9.20.1(jiti@2.4.2))
eslint-plugin-react-hooks: 5.1.0(eslint@9.20.1(jiti@2.4.2)) eslint-plugin-react-hooks: 5.1.0(eslint@9.20.1(jiti@2.4.2))
@@ -12925,7 +12988,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint@9.20.1(jiti@2.4.2)))(eslint@9.20.1(jiti@2.4.2)): eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0)(eslint@9.20.1(jiti@2.4.2)):
dependencies: dependencies:
'@nolyfill/is-core-module': 1.0.39 '@nolyfill/is-core-module': 1.0.39
debug: 4.4.0 debug: 4.4.0
@@ -12937,22 +13000,22 @@ snapshots:
is-glob: 4.0.3 is-glob: 4.0.3
stable-hash: 0.0.4 stable-hash: 0.0.4
optionalDependencies: optionalDependencies:
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint@9.20.1(jiti@2.4.2)))(eslint@9.20.1(jiti@2.4.2)))(eslint@9.20.1(jiti@2.4.2)) eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.24.1(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint@9.20.1(jiti@2.4.2))
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
eslint-module-utils@2.12.0(@typescript-eslint/parser@8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint@9.20.1(jiti@2.4.2)))(eslint@9.20.1(jiti@2.4.2)))(eslint@9.20.1(jiti@2.4.2)): eslint-module-utils@2.12.0(@typescript-eslint/parser@8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0)(eslint@9.20.1(jiti@2.4.2)):
dependencies: dependencies:
debug: 3.2.7 debug: 3.2.7
optionalDependencies: optionalDependencies:
'@typescript-eslint/parser': 8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3) '@typescript-eslint/parser': 8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3)
eslint: 9.20.1(jiti@2.4.2) eslint: 9.20.1(jiti@2.4.2)
eslint-import-resolver-node: 0.3.9 eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.7.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint@9.20.1(jiti@2.4.2)))(eslint@9.20.1(jiti@2.4.2)) eslint-import-resolver-typescript: 3.7.0(eslint-plugin-import@2.31.0)(eslint@9.20.1(jiti@2.4.2))
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint@9.20.1(jiti@2.4.2)))(eslint@9.20.1(jiti@2.4.2)))(eslint@9.20.1(jiti@2.4.2)): eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.24.1(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint@9.20.1(jiti@2.4.2)):
dependencies: dependencies:
'@rtsao/scc': 1.1.0 '@rtsao/scc': 1.1.0
array-includes: 3.1.8 array-includes: 3.1.8
@@ -12963,7 +13026,7 @@ snapshots:
doctrine: 2.1.0 doctrine: 2.1.0
eslint: 9.20.1(jiti@2.4.2) eslint: 9.20.1(jiti@2.4.2)
eslint-import-resolver-node: 0.3.9 eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint@9.20.1(jiti@2.4.2)))(eslint@9.20.1(jiti@2.4.2)))(eslint@9.20.1(jiti@2.4.2)) eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0)(eslint@9.20.1(jiti@2.4.2))
hasown: 2.0.2 hasown: 2.0.2
is-core-module: 2.16.1 is-core-module: 2.16.1
is-glob: 4.0.3 is-glob: 4.0.3
@@ -12975,7 +13038,7 @@ snapshots:
string.prototype.trimend: 1.0.9 string.prototype.trimend: 1.0.9
tsconfig-paths: 3.15.0 tsconfig-paths: 3.15.0
optionalDependencies: optionalDependencies:
'@typescript-eslint/parser': 8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3) '@typescript-eslint/parser': 8.24.1(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3)
transitivePeerDependencies: transitivePeerDependencies:
- eslint-import-resolver-typescript - eslint-import-resolver-typescript
- eslint-import-resolver-webpack - eslint-import-resolver-webpack
@@ -14513,13 +14576,13 @@ snapshots:
netmask@2.0.2: {} netmask@2.0.2: {}
next-sitemap@4.2.3(next@15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)): next-sitemap@4.2.3(next@15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)):
dependencies: dependencies:
'@corex/deepmerge': 4.0.43 '@corex/deepmerge': 4.0.43
'@next/env': 13.5.7 '@next/env': 13.5.7
fast-glob: 3.3.2 fast-glob: 3.3.2
minimist: 1.2.8 minimist: 1.2.8
next: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) next: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
next-themes@0.4.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0): next-themes@0.4.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
dependencies: dependencies:
@@ -14532,7 +14595,7 @@ snapshots:
'@swc/counter': 0.1.3 '@swc/counter': 0.1.3
'@swc/helpers': 0.5.15 '@swc/helpers': 0.5.15
busboy: 1.6.0 busboy: 1.6.0
caniuse-lite: 1.0.30001699 caniuse-lite: 1.0.30001700
postcss: 8.4.31 postcss: 8.4.31
react: 19.0.0 react: 19.0.0
react-dom: 19.0.0(react@19.0.0) react-dom: 19.0.0(react@19.0.0)
@@ -14553,13 +14616,13 @@ snapshots:
- '@babel/core' - '@babel/core'
- babel-plugin-macros - babel-plugin-macros
next@15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): next@15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
dependencies: dependencies:
'@next/env': 15.1.7 '@next/env': 15.1.7
'@swc/counter': 0.1.3 '@swc/counter': 0.1.3
'@swc/helpers': 0.5.15 '@swc/helpers': 0.5.15
busboy: 1.6.0 busboy: 1.6.0
caniuse-lite: 1.0.30001699 caniuse-lite: 1.0.30001700
postcss: 8.4.31 postcss: 8.4.31
react: 19.0.0 react: 19.0.0
react-dom: 19.0.0(react@19.0.0) react-dom: 19.0.0(react@19.0.0)
@@ -14575,7 +14638,7 @@ snapshots:
'@next/swc-win32-x64-msvc': 15.1.7 '@next/swc-win32-x64-msvc': 15.1.7
'@opentelemetry/api': 1.9.0 '@opentelemetry/api': 1.9.0
'@playwright/test': 1.50.1 '@playwright/test': 1.50.1
babel-plugin-react-compiler: 19.0.0-beta-714736e-20250131 babel-plugin-react-compiler: 19.0.0-beta-21e868a-20250216
sharp: 0.33.5 sharp: 0.33.5
transitivePeerDependencies: transitivePeerDependencies:
- '@babel/core' - '@babel/core'