committed by
GitHub
parent
59dfc0ad91
commit
c185bcfa11
41
apps/dev-tool/.gitignore
vendored
Normal file
41
apps/dev-tool/.gitignore
vendored
Normal 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
27
apps/dev-tool/README.md
Normal 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.
|
||||||
199
apps/dev-tool/app/emails/[id]/components/email-tester-form.tsx
Normal file
199
apps/dev-tool/app/emails/[id]/components/email-tester-form.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
104
apps/dev-tool/app/emails/[id]/page.tsx
Normal file
104
apps/dev-tool/app/emails/[id]/page.tsx
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
57
apps/dev-tool/app/emails/lib/email-loader.tsx
Normal file
57
apps/dev-tool/app/emails/lib/email-loader.tsx
Normal 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'),
|
||||||
|
};
|
||||||
|
}
|
||||||
11
apps/dev-tool/app/emails/lib/email-tester-form-schema.ts
Normal file
11
apps/dev-tool/app/emails/lib/email-tester-form-schema.ts
Normal 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(),
|
||||||
|
});
|
||||||
38
apps/dev-tool/app/emails/lib/server-actions.ts
Normal file
38
apps/dev-tool/app/emails/lib/server-actions.ts
Normal 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',
|
||||||
|
});
|
||||||
|
}
|
||||||
83
apps/dev-tool/app/emails/page.tsx
Normal file
83
apps/dev-tool/app/emails/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
27
apps/dev-tool/app/layout.tsx
Normal file
27
apps/dev-tool/app/layout.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
213
apps/dev-tool/app/lib/connectivity-service.ts
Normal file
213
apps/dev-tool/app/lib/connectivity-service.ts
Normal 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',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
48
apps/dev-tool/app/page.tsx
Normal file
48
apps/dev-tool/app/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
46
apps/dev-tool/app/translations/lib/translations-loader.ts
Normal file
46
apps/dev-tool/app/translations/lib/translations-loader.ts
Normal 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;
|
||||||
|
}
|
||||||
35
apps/dev-tool/app/translations/page.tsx
Normal file
35
apps/dev-tool/app/translations/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
244
apps/dev-tool/app/variables/lib/env-scanner.ts
Normal file
244
apps/dev-tool/app/variables/lib/env-scanner.ts
Normal 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 ?? '';
|
||||||
|
}
|
||||||
770
apps/dev-tool/app/variables/lib/env-variables-model.ts
Normal file
770
apps/dev-tool/app/variables/lib/env-variables-model.ts
Normal 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);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
40
apps/dev-tool/app/variables/lib/types.ts
Normal file
40
apps/dev-tool/app/variables/lib/types.ts
Normal 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;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
55
apps/dev-tool/app/variables/page.tsx
Normal file
55
apps/dev-tool/app/variables/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
13
apps/dev-tool/components/app-layout.tsx
Normal file
13
apps/dev-tool/components/app-layout.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
79
apps/dev-tool/components/app-sidebar.tsx
Normal file
79
apps/dev-tool/components/app-sidebar.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
41
apps/dev-tool/components/env-mode-selector.tsx
Normal file
41
apps/dev-tool/components/env-mode-selector.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
35
apps/dev-tool/components/iframe.tsx
Normal file
35
apps/dev-tool/components/iframe.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
32
apps/dev-tool/components/root-providers.tsx
Normal file
32
apps/dev-tool/components/root-providers.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
57
apps/dev-tool/components/status-tile.tsx
Normal file
57
apps/dev-tool/components/status-tile.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
16
apps/dev-tool/next.config.ts
Normal file
16
apps/dev-tool/next.config.ts
Normal 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;
|
||||||
44
apps/dev-tool/package.json
Normal file
44
apps/dev-tool/package.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
5
apps/dev-tool/postcss.config.mjs
Normal file
5
apps/dev-tool/postcss.config.mjs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
'@tailwindcss/postcss': {},
|
||||||
|
},
|
||||||
|
};
|
||||||
43
apps/dev-tool/styles/globals.css
Normal file
43
apps/dev-tool/styles/globals.css
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
104
apps/dev-tool/styles/shadcn-ui.css
Normal file
104
apps/dev-tool/styles/shadcn-ui.css
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
116
apps/dev-tool/styles/theme.css
Normal file
116
apps/dev-tool/styles/theme.css
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
5
apps/dev-tool/styles/theme.utilities.css
Normal file
5
apps/dev-tool/styles/theme.utilities.css
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
@utility container {
|
||||||
|
margin-inline: auto;
|
||||||
|
|
||||||
|
@apply xl:max-w-[80rem] px-8;
|
||||||
|
}
|
||||||
28
apps/dev-tool/tsconfig.json
Normal file
28
apps/dev-tool/tsconfig.json
Normal 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"]
|
||||||
|
}
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
193
pnpm-lock.yaml
generated
@@ -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'
|
||||||
|
|||||||
Reference in New Issue
Block a user