Dev Tools improvements (#247)
* Refactor environment variables UI and update validation logic Enhanced the environment variables page layout for better responsiveness and structure by introducing new components and styles. Added `EnvListDisplay` for grouped variable display and adjusted several UI elements for clarity and consistency. Updated `NEXT_PUBLIC_SENTRY_ENVIRONMENT` validation to make it optional, aligning with updated requirements. * Add environment variable validation and enhance page headers Introduces robust validation for environment variables, ensuring correctness and contextual dependency checks. Updates page headers with titles and detailed descriptions for better usability and clarity. * Refactor variable page layout and improve code readability Rearranged className attributes in JSX for consistency and readability. Refactored map and enum validation logic for better formatting and maintainability. Applied minor corrections to types and formatting in other components. * Refactor styles and simplify component logic Updated badge variants to streamline styles and removed redundant hover states. Simplified logic in email page by extracting breadcrumb values and optimizing title rendering. Adjusted environment variables manager layout for cleaner rendering and removed unnecessary elements. * Add real-time translation updates with RxJS and UI improvements Introduced a Subject with debounce mechanism for handling translation updates, enhancing real-time editing in the translations comparison module. Improved UI components, including conditional rendering, better input handling, and layout adjustments. Implemented a server action for updating translations and streamlined type definitions in the emails page. * Enhance environment variable copying functionality and improve user feedback Updated the environment variables manager to copy structured environment variable data to the clipboard, improving usability. Adjusted toast notifications to provide clearer success and error messages during the copy process. Additionally, fixed a minor issue in the translations comparison component by ensuring proper filtering of keys based on the search input. * Add AI translation functionality and update dependencies Implemented a new action for translating missing strings using AI, enhancing the translations comparison component. Introduced a loading state during translation and improved error handling for translation updates. Updated package dependencies, including the addition of '@ai-sdk/openai' and 'ai' to facilitate AI-driven translations. Enhanced UI components for better user experience and streamlined translation management.
This commit is contained in:
committed by
GitHub
parent
cea46b06a1
commit
76bfeddd32
@@ -34,13 +34,7 @@ export default async function EmailPage(props: EmailPageProps) {
|
|||||||
const template = await loadEmailTemplate(id);
|
const template = await loadEmailTemplate(id);
|
||||||
const emailSettings = await getEmailSettings(mode);
|
const emailSettings = await getEmailSettings(mode);
|
||||||
|
|
||||||
return (
|
const values: Record<string, string> = {
|
||||||
<Page style={'custom'}>
|
|
||||||
<PageHeader
|
|
||||||
displaySidebarTrigger={false}
|
|
||||||
description={
|
|
||||||
<AppBreadcrumbs
|
|
||||||
values={{
|
|
||||||
emails: 'Emails',
|
emails: 'Emails',
|
||||||
'invite-email': 'Invite Email',
|
'invite-email': 'Invite Email',
|
||||||
'account-delete-email': 'Account Delete Email',
|
'account-delete-email': 'Account Delete Email',
|
||||||
@@ -49,9 +43,14 @@ export default async function EmailPage(props: EmailPageProps) {
|
|||||||
'reset-password-email': 'Reset Password Email',
|
'reset-password-email': 'Reset Password Email',
|
||||||
'magic-link-email': 'Magic Link Email',
|
'magic-link-email': 'Magic Link Email',
|
||||||
'otp-email': 'OTP Email',
|
'otp-email': 'OTP Email',
|
||||||
}}
|
};
|
||||||
/>
|
|
||||||
}
|
return (
|
||||||
|
<Page style={'custom'}>
|
||||||
|
<PageHeader
|
||||||
|
displaySidebarTrigger={false}
|
||||||
|
title={values[id]}
|
||||||
|
description={<AppBreadcrumbs values={values} />}
|
||||||
>
|
>
|
||||||
<EnvModeSelector mode={mode} />
|
<EnvModeSelector mode={mode} />
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|||||||
@@ -15,7 +15,11 @@ export const metadata = {
|
|||||||
export default async function EmailsPage() {
|
export default async function EmailsPage() {
|
||||||
return (
|
return (
|
||||||
<Page style={'custom'}>
|
<Page style={'custom'}>
|
||||||
<PageHeader displaySidebarTrigger={false} description="Emails" />
|
<PageHeader
|
||||||
|
displaySidebarTrigger={false}
|
||||||
|
title="Emails"
|
||||||
|
description={'Manage your application Email templates'}
|
||||||
|
/>
|
||||||
|
|
||||||
<PageBody className={'gap-y-8'}>
|
<PageBody className={'gap-y-8'}>
|
||||||
<div className={'flex flex-col space-y-4'}>
|
<div className={'flex flex-col space-y-4'}>
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ class ConnectivityService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
const response = await fetch(`${url}/auth/v1/health`, {
|
const response = await fetch(`${url}/auth/v1/health`, {
|
||||||
headers: {
|
headers: {
|
||||||
apikey: anonKey,
|
apikey: anonKey,
|
||||||
@@ -50,6 +51,12 @@ class ConnectivityService {
|
|||||||
status: 'success' as const,
|
status: 'success' as const,
|
||||||
message: 'Connected to Supabase',
|
message: 'Connected to Supabase',
|
||||||
};
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
status: 'error' as const,
|
||||||
|
message: `Failed to connect to Supabase. ${error}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkSupabaseAdminConnectivity() {
|
async checkSupabaseAdminConnectivity() {
|
||||||
@@ -85,6 +92,7 @@ class ConnectivityService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
const response = await fetch(endpoint, {
|
const response = await fetch(endpoint, {
|
||||||
headers: {
|
headers: {
|
||||||
apikey,
|
apikey,
|
||||||
@@ -114,6 +122,12 @@ class ConnectivityService {
|
|||||||
status: 'success' as const,
|
status: 'success' as const,
|
||||||
message: 'Connected to Supabase Admin',
|
message: 'Connected to Supabase Admin',
|
||||||
};
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
status: 'error' as const,
|
||||||
|
message: `Failed to connect to Supabase Admin. ${error}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkStripeWebhookEndpoints() {
|
async checkStripeWebhookEndpoints() {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export default async function DashboardPage(props: DashboardPageProps) {
|
|||||||
<Page style={'custom'}>
|
<Page style={'custom'}>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
displaySidebarTrigger={false}
|
displaySidebarTrigger={false}
|
||||||
|
title={'Dev Tool'}
|
||||||
description={'Check the status of your Supabase and Stripe services'}
|
description={'Check the status of your Supabase and Stripe services'}
|
||||||
>
|
>
|
||||||
<EnvModeSelector mode={mode} />
|
<EnvModeSelector mode={mode} />
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { ChevronDownIcon } from 'lucide-react';
|
import { ChevronDownIcon, Loader2Icon } from 'lucide-react';
|
||||||
|
import { Subject, debounceTime } from 'rxjs';
|
||||||
|
|
||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
import {
|
import {
|
||||||
@@ -11,6 +12,7 @@ import {
|
|||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@kit/ui/dropdown-menu';
|
} from '@kit/ui/dropdown-menu';
|
||||||
|
import { If } from '@kit/ui/if';
|
||||||
import { Input } from '@kit/ui/input';
|
import { Input } from '@kit/ui/input';
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
@@ -19,6 +21,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@kit/ui/select';
|
} from '@kit/ui/select';
|
||||||
|
import { toast } from '@kit/ui/sonner';
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@@ -30,6 +33,10 @@ import {
|
|||||||
import { cn } from '@kit/ui/utils';
|
import { cn } from '@kit/ui/utils';
|
||||||
|
|
||||||
import { defaultI18nNamespaces } from '../../../../web/lib/i18n/i18n.settings';
|
import { defaultI18nNamespaces } from '../../../../web/lib/i18n/i18n.settings';
|
||||||
|
import {
|
||||||
|
translateWithAIAction,
|
||||||
|
updateTranslationAction,
|
||||||
|
} from '../lib/server-actions';
|
||||||
import type { TranslationData, Translations } from '../lib/translations-loader';
|
import type { TranslationData, Translations } from '../lib/translations-loader';
|
||||||
|
|
||||||
function flattenTranslations(
|
function flattenTranslations(
|
||||||
@@ -58,31 +65,37 @@ export function TranslationsComparison({
|
|||||||
translations: Translations;
|
translations: Translations;
|
||||||
}) {
|
}) {
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const [selectedLocales, setSelectedLocales] = useState<Set<string>>();
|
const [isTranslating, setIsTranslating] = useState(false);
|
||||||
|
|
||||||
const [selectedNamespace, setSelectedNamespace] = useState(
|
const [selectedNamespace, setSelectedNamespace] = useState(
|
||||||
defaultI18nNamespaces[0] as string,
|
defaultI18nNamespaces[0] as string,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Create RxJS Subject for handling translation updates
|
||||||
|
const subject$ = useMemo(
|
||||||
|
() =>
|
||||||
|
new Subject<{
|
||||||
|
locale: string;
|
||||||
|
namespace: string;
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
}>(),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
const locales = Object.keys(translations);
|
const locales = Object.keys(translations);
|
||||||
|
|
||||||
if (locales.length === 0) {
|
|
||||||
return <div>No translations found</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseLocale = locales[0]!;
|
const baseLocale = locales[0]!;
|
||||||
|
|
||||||
// Initialize selected locales if not set
|
const [selectedLocales, setSelectedLocales] = useState<Set<string>>(
|
||||||
if (!selectedLocales) {
|
new Set(locales),
|
||||||
setSelectedLocales(new Set(locales));
|
);
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flatten translations for the selected namespace
|
// Flatten translations for the selected namespace
|
||||||
const flattenedTranslations: FlattenedTranslations = {};
|
const flattenedTranslations: FlattenedTranslations = {};
|
||||||
|
|
||||||
for (const locale of locales) {
|
for (const locale of locales) {
|
||||||
const namespaceData = translations[locale]?.[selectedNamespace];
|
const namespaceData = translations[locale]?.[selectedNamespace];
|
||||||
|
|
||||||
if (namespaceData) {
|
if (namespaceData) {
|
||||||
flattenedTranslations[locale] = flattenTranslations(namespaceData);
|
flattenedTranslations[locale] = flattenTranslations(namespaceData);
|
||||||
} else {
|
} else {
|
||||||
@@ -105,14 +118,6 @@ export function TranslationsComparison({
|
|||||||
selectedLocales.has(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 toggleLocale = (locale: string) => {
|
||||||
const newSelectedLocales = new Set(selectedLocales);
|
const newSelectedLocales = new Set(selectedLocales);
|
||||||
|
|
||||||
@@ -127,9 +132,83 @@ export function TranslationsComparison({
|
|||||||
setSelectedLocales(newSelectedLocales);
|
setSelectedLocales(newSelectedLocales);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleTranslateWithAI = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setIsTranslating(true);
|
||||||
|
|
||||||
|
// Get missing translations for the selected namespace
|
||||||
|
const missingTranslations: Record<string, string> = {};
|
||||||
|
const baseTranslations = flattenedTranslations[baseLocale] ?? {};
|
||||||
|
|
||||||
|
for (const locale of visibleLocales) {
|
||||||
|
if (locale === baseLocale) continue;
|
||||||
|
|
||||||
|
const localeTranslations = flattenedTranslations[locale] ?? {};
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(baseTranslations)) {
|
||||||
|
if (!localeTranslations[key]) {
|
||||||
|
missingTranslations[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(missingTranslations).length > 0) {
|
||||||
|
await translateWithAIAction({
|
||||||
|
sourceLocale: baseLocale,
|
||||||
|
targetLocale: locale,
|
||||||
|
namespace: selectedNamespace,
|
||||||
|
translations: missingTranslations,
|
||||||
|
});
|
||||||
|
|
||||||
|
toast.success(`Translated missing strings to ${locale}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Failed to translate: ' + (error as Error).message);
|
||||||
|
} finally {
|
||||||
|
setIsTranslating(false);
|
||||||
|
}
|
||||||
|
}, [flattenedTranslations, baseLocale, visibleLocales, selectedNamespace]);
|
||||||
|
|
||||||
|
// Calculate if there are any missing translations
|
||||||
|
const hasMissingTranslations = useMemo(() => {
|
||||||
|
if (!flattenedTranslations || !baseLocale || !visibleLocales) return false;
|
||||||
|
|
||||||
|
const baseTranslations = flattenedTranslations[baseLocale] ?? {};
|
||||||
|
|
||||||
|
return visibleLocales.some((locale) => {
|
||||||
|
if (locale === baseLocale) return false;
|
||||||
|
|
||||||
|
const localeTranslations = flattenedTranslations[locale] ?? {};
|
||||||
|
|
||||||
|
return Object.keys(baseTranslations).some(
|
||||||
|
(key) => !localeTranslations[key],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, [flattenedTranslations, baseLocale, visibleLocales]);
|
||||||
|
|
||||||
|
// Set up subscription to handle debounced updates
|
||||||
|
useEffect(() => {
|
||||||
|
const subscription = subject$.pipe(debounceTime(500)).subscribe((props) => {
|
||||||
|
updateTranslationAction(props)
|
||||||
|
.then(() => {
|
||||||
|
toast.success(`Updated translation for ${props.key}`);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
toast.error(`Failed to update translation: ${err.message}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => subscription.unsubscribe();
|
||||||
|
}, [subject$]);
|
||||||
|
|
||||||
|
if (locales.length === 0) {
|
||||||
|
return <div>No translations found</div>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4 pb-24">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex w-full items-center">
|
||||||
|
<div className="flex w-full items-center justify-between gap-2.5">
|
||||||
<div className="flex items-center gap-2.5">
|
<div className="flex items-center gap-2.5">
|
||||||
<Input
|
<Input
|
||||||
type="search"
|
type="search"
|
||||||
@@ -139,6 +218,7 @@ export function TranslationsComparison({
|
|||||||
className="max-w-sm"
|
className="max-w-sm"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<If condition={locales.length > 1}>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="outline" className="ml-auto">
|
<Button variant="outline" className="ml-auto">
|
||||||
@@ -154,7 +234,8 @@ export function TranslationsComparison({
|
|||||||
checked={selectedLocales.has(locale)}
|
checked={selectedLocales.has(locale)}
|
||||||
onCheckedChange={() => toggleLocale(locale)}
|
onCheckedChange={() => toggleLocale(locale)}
|
||||||
disabled={
|
disabled={
|
||||||
selectedLocales.size === 1 && selectedLocales.has(locale)
|
selectedLocales.size === 1 &&
|
||||||
|
selectedLocales.has(locale)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{locale}
|
{locale}
|
||||||
@@ -162,6 +243,7 @@ export function TranslationsComparison({
|
|||||||
))}
|
))}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
</If>
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
value={selectedNamespace}
|
value={selectedNamespace}
|
||||||
@@ -180,6 +262,23 @@ export function TranslationsComparison({
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
onClick={handleTranslateWithAI}
|
||||||
|
disabled={isTranslating || !hasMissingTranslations}
|
||||||
|
>
|
||||||
|
{isTranslating ? (
|
||||||
|
<>
|
||||||
|
<Loader2Icon className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
Translating...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'Translate missing with AI'
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded-md border">
|
<div className="rounded-md border">
|
||||||
@@ -196,7 +295,7 @@ export function TranslationsComparison({
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
{filteredKeys.map((key) => (
|
{filteredKeys.map((key) => (
|
||||||
<TableRow key={key}>
|
<TableRow key={key}>
|
||||||
<TableCell className="font-mono text-sm">
|
<TableCell width={350} className="text-sm">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span>{key}</span>
|
<span>{key}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -222,11 +321,33 @@ export function TranslationsComparison({
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span>
|
<Input
|
||||||
{value || (
|
defaultValue={value || ''}
|
||||||
<span className="text-destructive">Missing</span>
|
onChange={(e) => {
|
||||||
)}
|
const value = e.target.value.trim();
|
||||||
</span>
|
|
||||||
|
if (value === '') {
|
||||||
|
toast.error('Translation cannot be empty');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value === baseValue) {
|
||||||
|
toast.info('Translation is the same as base');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
subject$.next({
|
||||||
|
locale,
|
||||||
|
namespace: selectedNamespace,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="w-full font-mono text-sm"
|
||||||
|
placeholder={isMissing ? 'Missing translation' : ''}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
);
|
);
|
||||||
|
|||||||
161
apps/dev-tool/app/translations/lib/server-actions.ts
Normal file
161
apps/dev-tool/app/translations/lib/server-actions.ts
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
import { revalidatePath } from 'next/cache';
|
||||||
|
|
||||||
|
import { openai } from '@ai-sdk/openai';
|
||||||
|
import { generateText } from 'ai';
|
||||||
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||||
|
import { resolve } from 'node:url';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { getLogger } from '@kit/shared/logger';
|
||||||
|
|
||||||
|
const Schema = z.object({
|
||||||
|
locale: z.string().min(1),
|
||||||
|
namespace: z.string().min(1),
|
||||||
|
key: z.string().min(1),
|
||||||
|
value: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const TranslateSchema = z.object({
|
||||||
|
sourceLocale: z.string(),
|
||||||
|
targetLocale: z.string(),
|
||||||
|
namespace: z.string(),
|
||||||
|
translations: z.record(z.string(), z.string()),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a translation value in the specified locale and namespace.
|
||||||
|
* @param props
|
||||||
|
*/
|
||||||
|
export async function updateTranslationAction(props: z.infer<typeof Schema>) {
|
||||||
|
// Validate the input
|
||||||
|
const { locale, namespace, key, value } = Schema.parse(props);
|
||||||
|
|
||||||
|
const root = resolve(process.cwd(), '..');
|
||||||
|
const filePath = `${root}apps/web/public/locales/${locale}/${namespace}.json`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Read the current translations file
|
||||||
|
const translationsFile = readFileSync(filePath, 'utf-8');
|
||||||
|
const translations = JSON.parse(translationsFile) as Record<string, any>;
|
||||||
|
|
||||||
|
// Update the nested key value
|
||||||
|
const keys = key.split('.') as string[];
|
||||||
|
let current = translations;
|
||||||
|
|
||||||
|
// Navigate through nested objects until the second-to-last key
|
||||||
|
for (let i = 0; i < keys.length - 1; i++) {
|
||||||
|
const currentKey = keys[i] as string;
|
||||||
|
|
||||||
|
if (!current[currentKey]) {
|
||||||
|
current[currentKey] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
current = current[currentKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the value at the final key
|
||||||
|
const finalKey = keys[keys.length - 1] as string;
|
||||||
|
current[finalKey] = value;
|
||||||
|
|
||||||
|
// Write the updated translations back to the file
|
||||||
|
writeFileSync(filePath, JSON.stringify(translations, null, 2), 'utf-8');
|
||||||
|
|
||||||
|
revalidatePath(`/translations`);
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update translation:', error);
|
||||||
|
throw new Error('Failed to update translation');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const translateWithAIAction = async (
|
||||||
|
data: z.infer<typeof TranslateSchema>,
|
||||||
|
) => {
|
||||||
|
const logger = await getLogger();
|
||||||
|
|
||||||
|
z.string().min(1).parse(process.env.OPENAI_API_KEY);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { sourceLocale, targetLocale, namespace, translations } =
|
||||||
|
TranslateSchema.parse(data);
|
||||||
|
|
||||||
|
// if the path does not exist, create it using an empty object
|
||||||
|
const root = resolve(process.cwd(), '..');
|
||||||
|
const folderPath = `${root}apps/web/public/locales/${targetLocale}`;
|
||||||
|
|
||||||
|
if (!existsSync(folderPath)) {
|
||||||
|
// create the directory if it doesn't exist
|
||||||
|
mkdirSync(folderPath, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = `${folderPath}/${namespace}.json`;
|
||||||
|
|
||||||
|
if (!existsSync(filePath)) {
|
||||||
|
// create the file if it doesn't exist
|
||||||
|
writeFileSync(filePath, JSON.stringify({}, null, 2), 'utf-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
const results: Record<string, string> = {};
|
||||||
|
|
||||||
|
// Process translations in batches of 5 for efficiency
|
||||||
|
const entries = Object.entries(translations);
|
||||||
|
const batches = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < entries.length; i += 5) {
|
||||||
|
batches.push(entries.slice(i, i + 5));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const batch of batches) {
|
||||||
|
const batchPromises = batch.map(async ([key, value]) => {
|
||||||
|
const prompt = `Translate the following text from ${sourceLocale} to ${targetLocale}. Maintain any placeholders (like {name} or %{count}) and HTML tags. Only return the translated text, nothing else.
|
||||||
|
|
||||||
|
Original text: ${value}`;
|
||||||
|
|
||||||
|
const MODEL_NAME = process.env.LLM_MODEL_NAME ?? 'gpt-4o-mini';
|
||||||
|
const model = openai(MODEL_NAME);
|
||||||
|
|
||||||
|
const { text } = await generateText({
|
||||||
|
model,
|
||||||
|
prompt,
|
||||||
|
temperature: 0.3,
|
||||||
|
maxTokens: 200,
|
||||||
|
});
|
||||||
|
|
||||||
|
return [key, text.trim()] as [string, string];
|
||||||
|
});
|
||||||
|
|
||||||
|
const batchResults = await Promise.all(batchPromises);
|
||||||
|
|
||||||
|
for (const [key, translation] of batchResults) {
|
||||||
|
results[key] = translation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update each translation
|
||||||
|
for (const [key, translation] of Object.entries(results)) {
|
||||||
|
await updateTranslationAction({
|
||||||
|
locale: targetLocale,
|
||||||
|
namespace,
|
||||||
|
key,
|
||||||
|
value: translation,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('AI translation completed', {
|
||||||
|
sourceLocale,
|
||||||
|
targetLocale,
|
||||||
|
namespace,
|
||||||
|
count: Object.keys(results).length,
|
||||||
|
});
|
||||||
|
|
||||||
|
revalidatePath('/translations');
|
||||||
|
|
||||||
|
return { success: true, translations: results };
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('AI translation failed', { error });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -18,12 +18,9 @@ export default async function TranslationsPage() {
|
|||||||
<Page style={'custom'}>
|
<Page style={'custom'}>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
displaySidebarTrigger={false}
|
displaySidebarTrigger={false}
|
||||||
|
title={'Translations'}
|
||||||
description={
|
description={
|
||||||
<AppBreadcrumbs
|
'Compare translations across different languages. Ensure consistency and accuracy in your translations.'
|
||||||
values={{
|
|
||||||
translations: 'Translations',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,23 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Fragment, useCallback, useState } from 'react';
|
import { Fragment, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import Link from 'next/link';
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
|
||||||
import { envVariables } from '@/app/variables/lib/env-variables-model';
|
import { envVariables } from '@/app/variables/lib/env-variables-model';
|
||||||
|
import { updateEnvironmentVariableAction } from '@/app/variables/lib/server-actions';
|
||||||
import { EnvModeSelector } from '@/components/env-mode-selector';
|
import { EnvModeSelector } from '@/components/env-mode-selector';
|
||||||
import {
|
import {
|
||||||
ChevronDown,
|
|
||||||
ChevronUp,
|
|
||||||
ChevronsUpDownIcon,
|
ChevronsUpDownIcon,
|
||||||
Copy,
|
Copy,
|
||||||
|
CopyIcon,
|
||||||
Eye,
|
Eye,
|
||||||
EyeOff,
|
EyeOff,
|
||||||
EyeOffIcon,
|
EyeOffIcon,
|
||||||
InfoIcon,
|
InfoIcon,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import { Subject, debounceTime } from 'rxjs';
|
||||||
|
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||||
import { Badge } from '@kit/ui/badge';
|
import { Badge } from '@kit/ui/badge';
|
||||||
@@ -39,32 +41,77 @@ import {
|
|||||||
import { cn } from '@kit/ui/utils';
|
import { cn } from '@kit/ui/utils';
|
||||||
|
|
||||||
import { AppEnvState, EnvVariableState } from '../lib/types';
|
import { AppEnvState, EnvVariableState } from '../lib/types';
|
||||||
|
import { DynamicFormInput } from './dynamic-form-input';
|
||||||
type ValidationResult = {
|
|
||||||
success: boolean;
|
|
||||||
error?: {
|
|
||||||
issues: Array<{ message: string }>;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export function AppEnvironmentVariablesManager({
|
export function AppEnvironmentVariablesManager({
|
||||||
state,
|
state,
|
||||||
}: React.PropsWithChildren<{
|
}: React.PropsWithChildren<{
|
||||||
state: AppEnvState;
|
state: AppEnvState;
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return <EnvList appState={state} />;
|
||||||
<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'}>
|
function EnvListDisplay({
|
||||||
<EnvList appState={state} />
|
groups,
|
||||||
|
className,
|
||||||
|
hideSecret = false,
|
||||||
|
}: {
|
||||||
|
groups: Array<{
|
||||||
|
category: string;
|
||||||
|
variables: Array<EnvVariableState>;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
className: string;
|
||||||
|
hideSecret?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className={cn(className)}>
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
'text-muted-foreground relative flex h-full flex-col rounded-lg font-mono text-xs'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="bg-muted/50 sticky top-0 flex flex-col gap-y-1 rounded-lg p-4">
|
||||||
|
<div className={'sticky top-0 h-full overflow-auto pb-16 break-all'}>
|
||||||
|
{groups.map((group) => (
|
||||||
|
<div className="mb-4" key={group.category}>
|
||||||
|
<span># {group.category}</span>
|
||||||
|
|
||||||
|
{group.variables.map((variable) => {
|
||||||
|
const model = envVariables.find(
|
||||||
|
(item) => item.name === variable.key,
|
||||||
|
);
|
||||||
|
|
||||||
|
const isSecret = model?.secret;
|
||||||
|
const value =
|
||||||
|
isSecret && hideSecret
|
||||||
|
? '••••••••'
|
||||||
|
: variable.effectiveValue;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href={`#var_${variable.key.toLowerCase()}`}
|
||||||
|
className={cn('block transition-all hover:underline', {
|
||||||
|
['text-orange-500']: variable.isOverridden,
|
||||||
|
['text-destructive']: !variable.validation.success,
|
||||||
|
['opacity-20']: !variable.isVisible,
|
||||||
|
})}
|
||||||
|
key={variable.key}
|
||||||
|
>
|
||||||
|
<span>{variable.key}</span>: {value}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function EnvList({ appState }: { appState: AppEnvState }) {
|
function EnvList({ appState }: { appState: AppEnvState }) {
|
||||||
const [expandedVars, setExpandedVars] = useState<Record<string, boolean>>({});
|
|
||||||
const [showValues, setShowValues] = useState<Record<string, boolean>>({});
|
const [showValues, setShowValues] = useState<Record<string, boolean>>({});
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
@@ -75,13 +122,6 @@ function EnvList({ appState }: { appState: AppEnvState }) {
|
|||||||
const showOverriddenVars = searchParams.get('overridden') === 'true';
|
const showOverriddenVars = searchParams.get('overridden') === 'true';
|
||||||
const showInvalidVars = searchParams.get('invalid') === 'true';
|
const showInvalidVars = searchParams.get('invalid') === 'true';
|
||||||
|
|
||||||
const toggleExpanded = (key: string) => {
|
|
||||||
setExpandedVars((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[key]: !prev[key],
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleShowValue = (key: string) => {
|
const toggleShowValue = (key: string) => {
|
||||||
setShowValues((prev) => ({
|
setShowValues((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
@@ -89,175 +129,86 @@ function EnvList({ appState }: { appState: AppEnvState }) {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
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) => {
|
const renderValue = (value: string, isVisible: boolean) => {
|
||||||
if (!isVisible) {
|
if (!isVisible) {
|
||||||
|
if (!value) {
|
||||||
|
return `(empty)`;
|
||||||
|
}
|
||||||
|
|
||||||
return '••••••••';
|
return '••••••••';
|
||||||
}
|
}
|
||||||
|
|
||||||
return value || '(empty)';
|
return value;
|
||||||
};
|
};
|
||||||
|
|
||||||
const allVariables = getEffectiveVariablesValue(appState);
|
const allVariables = getEffectiveVariablesValue(appState);
|
||||||
|
|
||||||
// Create a map of all variables including missing ones that have contextual validation
|
const subject$ = useMemo(
|
||||||
const allVarsWithValidation = envVariables.reduce<
|
() =>
|
||||||
Record<string, EnvVariableState>
|
new Subject<{
|
||||||
>((acc, model) => {
|
name: string;
|
||||||
// If the variable exists in appState, use that
|
value: string;
|
||||||
const existingVar = appState.variables[model.name];
|
}>(),
|
||||||
if (existingVar) {
|
[],
|
||||||
acc[model.name] = existingVar;
|
);
|
||||||
} else if (
|
|
||||||
// Show missing variables if they:
|
useEffect(() => {
|
||||||
model.required || // Are marked as required
|
const subscription = subject$
|
||||||
model.contextualValidation // OR have contextual validation
|
.pipe(debounceTime(1000))
|
||||||
) {
|
.subscribe((props) => {
|
||||||
// If it doesn't exist but is required or has contextual validation, create an empty state
|
updateEnvironmentVariableAction({
|
||||||
acc[model.name] = {
|
...props,
|
||||||
key: model.name,
|
mode: appState.mode,
|
||||||
effectiveValue: '',
|
})
|
||||||
effectiveSource: 'MISSING',
|
.then((result) => {
|
||||||
category: model.category,
|
toast.success(result.message);
|
||||||
isOverridden: false,
|
})
|
||||||
definitions: [],
|
.catch((err) => {
|
||||||
|
toast.error(`Failed to update ${props.name}: ${err.message}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
return subscription.unsubscribe();
|
||||||
};
|
};
|
||||||
}
|
}, [subject$]);
|
||||||
return acc;
|
|
||||||
}, {});
|
const onValueChanged = useCallback(
|
||||||
|
(props: { value: string; name: string }) => {
|
||||||
|
subject$.next({
|
||||||
|
name: props.name,
|
||||||
|
value: props.value,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[subject$],
|
||||||
|
);
|
||||||
|
|
||||||
const renderVariable = (varState: EnvVariableState) => {
|
const renderVariable = (varState: EnvVariableState) => {
|
||||||
const isExpanded = expandedVars[varState.key] ?? false;
|
|
||||||
const isClientBundledValue = varState.key.startsWith('NEXT_PUBLIC_');
|
|
||||||
const isValueVisible = showValues[varState.key] ?? isClientBundledValue;
|
|
||||||
|
|
||||||
const model = envVariables.find(
|
const model = envVariables.find(
|
||||||
(variable) => variable.name === varState.key,
|
(variable) => variable.name === varState.key,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Enhanced validation logic to handle both regular and contextual validation
|
const isClientBundledValue = varState.key.startsWith('NEXT_PUBLIC_');
|
||||||
let validation: ValidationResult = {
|
const isValueVisible = showValues[varState.key] ?? !model?.secret;
|
||||||
success: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (model) {
|
|
||||||
// First check if it's required but missing
|
|
||||||
if (model.required && !varState.effectiveValue) {
|
|
||||||
validation = {
|
|
||||||
success: false,
|
|
||||||
error: {
|
|
||||||
issues: [
|
|
||||||
{
|
|
||||||
message: `This variable is required but missing from your environment files`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
} else if (model.contextualValidation) {
|
|
||||||
// Then check contextual validation
|
|
||||||
const dependenciesMet = model.contextualValidation.dependencies.some(
|
|
||||||
(dep) => {
|
|
||||||
const dependencyValue = allVariables[dep.variable] ?? '';
|
|
||||||
|
|
||||||
return dep.condition(dependencyValue, allVariables);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (dependenciesMet) {
|
|
||||||
// Only check for missing value or run validation if dependencies are met
|
|
||||||
if (!varState.effectiveValue) {
|
|
||||||
const dependencyErrors = model.contextualValidation.dependencies
|
|
||||||
.map((dep) => {
|
|
||||||
const dependencyValue = allVariables[dep.variable] ?? '';
|
|
||||||
|
|
||||||
const shouldValidate = dep.condition(
|
|
||||||
dependencyValue,
|
|
||||||
allVariables,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (shouldValidate) {
|
|
||||||
const { success } = model.contextualValidation!.validate({
|
|
||||||
value: varState.effectiveValue,
|
|
||||||
variables: allVariables,
|
|
||||||
mode: appState.mode,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return dep.message;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
})
|
|
||||||
.filter((message): message is string => message !== null);
|
|
||||||
|
|
||||||
validation = {
|
|
||||||
success: dependencyErrors.length === 0,
|
|
||||||
error: {
|
|
||||||
issues: dependencyErrors.map((message) => ({ message })),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
// If we have a value and dependencies are met, run contextual validation
|
|
||||||
const result = model.contextualValidation.validate({
|
|
||||||
value: varState.effectiveValue,
|
|
||||||
variables: allVariables,
|
|
||||||
mode: appState.mode,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!result.success) {
|
|
||||||
validation = {
|
|
||||||
success: false,
|
|
||||||
error: {
|
|
||||||
issues: result.error.issues.map((issue) => ({
|
|
||||||
message: issue.message,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (model.validate && varState.effectiveValue) {
|
|
||||||
// Only run regular validation if:
|
|
||||||
// 1. There's no contextual validation
|
|
||||||
// 2. There's a value to validate
|
|
||||||
const result = model.validate({
|
|
||||||
value: varState.effectiveValue,
|
|
||||||
variables: allVariables,
|
|
||||||
mode: appState.mode,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!result.success) {
|
|
||||||
validation = {
|
|
||||||
success: false,
|
|
||||||
error: {
|
|
||||||
issues: result.error.issues.map((issue) => ({
|
|
||||||
message: issue.message,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const canExpand = varState.definitions.length > 1 || !validation.success;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={varState.key} className="animate-in fade-in rounded-lg border">
|
<div
|
||||||
<div className="p-4">
|
id={`var_${varState.key.toLowerCase()}`}
|
||||||
|
key={varState.key}
|
||||||
|
className={cn('animate-in fade-in py-6 transition-all', {
|
||||||
|
hidden: !varState.isVisible,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div className={'flex flex-col space-y-2'}>
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<div className="flex-1 flex-col gap-y-4">
|
<div className="flex max-w-full flex-1 flex-col">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<span className="font-mono text-sm font-semibold">
|
<span
|
||||||
|
className={cn('font-mono text-sm font-semibold', {
|
||||||
|
'text-orange-500': varState.isOverridden,
|
||||||
|
'text-destructive': !varState.validation.success,
|
||||||
|
})}
|
||||||
|
>
|
||||||
{varState.key}
|
{varState.key}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
@@ -311,11 +262,26 @@ function EnvList({ appState }: { appState: AppEnvState }) {
|
|||||||
</If>
|
</If>
|
||||||
|
|
||||||
<div className="mt-2 flex items-center gap-2">
|
<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">
|
<If
|
||||||
|
condition={isValueVisible || !varState.effectiveValue}
|
||||||
|
fallback={
|
||||||
|
<div className="max-w-auto bg-muted text-muted-foreground flex h-9 w-auto flex-1 items-center overflow-x-auto rounded border px-2 py-2 font-mono text-xs">
|
||||||
{renderValue(varState.effectiveValue, isValueVisible)}
|
{renderValue(varState.effectiveValue, isValueVisible)}
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<DynamicFormInput
|
||||||
|
type={model?.type ?? 'string'}
|
||||||
|
name={varState.key}
|
||||||
|
value={varState.effectiveValue}
|
||||||
|
onChange={onValueChanged}
|
||||||
|
placeholder={`Set a value for ${varState.key}`}
|
||||||
|
enumValues={model?.values}
|
||||||
|
className="text-xs"
|
||||||
|
/>
|
||||||
|
</If>
|
||||||
|
|
||||||
<If condition={!isClientBundledValue}>
|
<If condition={model?.secret}>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size={'icon'}
|
size={'icon'}
|
||||||
@@ -325,6 +291,7 @@ function EnvList({ appState }: { appState: AppEnvState }) {
|
|||||||
</Button>
|
</Button>
|
||||||
</If>
|
</If>
|
||||||
|
|
||||||
|
<If condition={model && model.type !== 'boolean'}>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => copyToClipboard(varState.effectiveValue)}
|
onClick={() => copyToClipboard(varState.effectiveValue)}
|
||||||
@@ -332,23 +299,19 @@ function EnvList({ appState }: { appState: AppEnvState }) {
|
|||||||
>
|
>
|
||||||
<Copy size={16} />
|
<Copy size={16} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</If>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{canExpand && (
|
<If condition={model?.hint}>
|
||||||
<Button
|
{(hint) => (
|
||||||
size={'icon'}
|
<div className="mt-2 flex items-center gap-2">
|
||||||
variant="ghost"
|
<span className="text-muted-foreground text-xs font-normal">
|
||||||
className="ml-4 rounded p-1 hover:bg-gray-100"
|
{hint}
|
||||||
onClick={() => toggleExpanded(varState.key)}
|
</span>
|
||||||
>
|
</div>
|
||||||
{isExpanded ? (
|
|
||||||
<ChevronUp className="h-4 w-4" />
|
|
||||||
) : (
|
|
||||||
<ChevronDown className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
)}
|
)}
|
||||||
|
</If>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-2 flex gap-x-2">
|
<div className="mt-2 flex gap-x-2">
|
||||||
@@ -441,7 +404,7 @@ function EnvList({ appState }: { appState: AppEnvState }) {
|
|||||||
</Badge>
|
</Badge>
|
||||||
</If>
|
</If>
|
||||||
|
|
||||||
<If condition={!validation.success}>
|
<If condition={!varState.validation.success}>
|
||||||
<Badge variant="destructive">
|
<Badge variant="destructive">
|
||||||
Invalid Value
|
Invalid Value
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
@@ -461,34 +424,22 @@ function EnvList({ appState }: { appState: AppEnvState }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isExpanded && canExpand && (
|
<div className="flex w-full flex-col gap-y-2 py-4">
|
||||||
<div className="flex flex-col gap-y-2 border-t bg-gray-50 p-4">
|
<If condition={!varState.validation.success}>
|
||||||
<If condition={!validation.success}>
|
|
||||||
<div className={'flex flex-col space-y-2'}>
|
<div className={'flex flex-col space-y-2'}>
|
||||||
<Heading level={6} className="Errors">
|
|
||||||
Errors
|
|
||||||
</Heading>
|
|
||||||
|
|
||||||
<Alert variant="destructive">
|
<Alert variant="destructive">
|
||||||
<AlertTitle>
|
<AlertTitle>
|
||||||
{varState.effectiveSource === 'MISSING'
|
{varState.effectiveSource === 'MISSING'
|
||||||
? 'Missing Required Variable'
|
? `The variable ${varState.key} is required but missing`
|
||||||
: 'Invalid Value'}
|
: `The value for ${varState.key} is invalid`}
|
||||||
</AlertTitle>
|
</AlertTitle>
|
||||||
|
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div>
|
|
||||||
{varState.effectiveSource === 'MISSING'
|
|
||||||
? `The variable ${varState.key} is required but missing from your environment files:`
|
|
||||||
: `The value for ${varState.key} is invalid:`}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Enhanced error display */}
|
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{validation.error?.issues.map((issue, index) => (
|
{varState.validation.error?.issues.map((issue, index) => (
|
||||||
<div key={index} className="text-sm">
|
<div key={index} className="text-sm">
|
||||||
• {issue.message}
|
• {issue}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -501,8 +452,8 @@ function EnvList({ appState }: { appState: AppEnvState }) {
|
|||||||
{model.contextualValidation.dependencies.map(
|
{model.contextualValidation.dependencies.map(
|
||||||
(dep, index) => (
|
(dep, index) => (
|
||||||
<div key={index} className="text-sm">
|
<div key={index} className="text-sm">
|
||||||
• Requires valid {dep.variable.toUpperCase()}{' '}
|
• Requires valid {dep.variable.toUpperCase()} when{' '}
|
||||||
when {dep.message}
|
{dep.message}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
@@ -520,7 +471,7 @@ function EnvList({ appState }: { appState: AppEnvState }) {
|
|||||||
Override Chain
|
Override Chain
|
||||||
</Heading>
|
</Heading>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="w-full space-y-2">
|
||||||
{varState.definitions.map((def) => (
|
{varState.definitions.map((def) => (
|
||||||
<div
|
<div
|
||||||
key={`${def.key}-${def.source}`}
|
key={`${def.key}-${def.source}`}
|
||||||
@@ -534,17 +485,12 @@ function EnvList({ appState }: { appState: AppEnvState }) {
|
|||||||
>
|
>
|
||||||
{def.source}
|
{def.source}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|
||||||
<div className="font-mono text-sm">
|
|
||||||
{renderValue(def.value, isValueVisible)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</If>
|
</If>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -589,83 +535,20 @@ function EnvList({ appState }: { appState: AppEnvState }) {
|
|||||||
return varState.isOverridden;
|
return varState.isOverridden;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showInvalidVars) {
|
if (showInvalidVars && isInSearch) {
|
||||||
const allVariables = getEffectiveVariablesValue(appState);
|
return !varState.validation.success;
|
||||||
|
|
||||||
let hasError = false;
|
|
||||||
|
|
||||||
if (model) {
|
|
||||||
if (model.contextualValidation) {
|
|
||||||
// Check for missing or invalid dependencies
|
|
||||||
const dependencyErrors = model.contextualValidation.dependencies
|
|
||||||
.map((dep) => {
|
|
||||||
const dependencyValue = allVariables[dep.variable] ?? '';
|
|
||||||
|
|
||||||
const shouldValidate = dep.condition(
|
|
||||||
dependencyValue,
|
|
||||||
allVariables,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (shouldValidate) {
|
|
||||||
const { error } = model.contextualValidation!.validate({
|
|
||||||
value: varState.effectiveValue,
|
|
||||||
variables: allVariables,
|
|
||||||
mode: appState.mode,
|
|
||||||
});
|
|
||||||
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
})
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
if (dependencyErrors.length > 0) {
|
|
||||||
hasError = true;
|
|
||||||
}
|
|
||||||
} else if (model.validate) {
|
|
||||||
// Fall back to regular validation
|
|
||||||
const result = model.validate({
|
|
||||||
value: varState.effectiveValue,
|
|
||||||
variables: allVariables,
|
|
||||||
mode: appState.mode,
|
|
||||||
});
|
|
||||||
|
|
||||||
hasError = !result.success;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return hasError && isInSearch;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return isInSearch;
|
return isInSearch;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update groups to use allVarsWithValidation instead of appState.variables
|
// Update groups to use allVarsWithValidation instead of appState.variables
|
||||||
const groups = Object.values(allVarsWithValidation)
|
const groups = getGroups(appState, filterVariable);
|
||||||
.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 (
|
return (
|
||||||
<div className="flex flex-col gap-y-4">
|
<div className="flex h-full flex-1 flex-col gap-y-4">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<div className="flex w-full space-x-2">
|
<div className="flex w-full space-x-2 py-0.5">
|
||||||
<div>
|
<div>
|
||||||
<EnvModeSelector mode={appState.mode} />
|
<EnvModeSelector mode={appState.mode} />
|
||||||
</div>
|
</div>
|
||||||
@@ -688,60 +571,47 @@ function EnvList({ appState }: { appState: AppEnvState }) {
|
|||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
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>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-1 flex-col gap-y-4 overflow-hidden">
|
||||||
<Summary appState={appState} />
|
<Summary appState={appState} />
|
||||||
|
|
||||||
{groups.map((group) => (
|
<div className="flex w-full flex-1 space-x-4 overflow-hidden">
|
||||||
|
<div className="flex w-6/12 flex-1 flex-col overflow-y-auto">
|
||||||
|
<div className="flex flex-col gap-y-4">
|
||||||
|
{groups.map((group) => {
|
||||||
|
const visibleVariables = group.variables.filter(
|
||||||
|
(item) => item.isVisible,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (visibleVariables.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
key={group.category}
|
key={group.category}
|
||||||
className="flex flex-col gap-y-2.5 border-b border-dashed py-8 last:border-b-0"
|
className="flex flex-col rounded-lg border p-4"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<span className={'text-sm font-bold uppercase'}>
|
<span className={'text-lg font-bold'}>
|
||||||
{group.category}
|
{group.category}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col space-y-4">
|
<div className="flex flex-col">
|
||||||
{group.variables.map((item) => {
|
{group.variables.map((item) => {
|
||||||
return (
|
return (
|
||||||
<Fragment key={item.key}>{renderVariable(item)}</Fragment>
|
<Fragment key={item.key}>
|
||||||
|
{renderVariable(item)}
|
||||||
|
</Fragment>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
<If condition={groups.length === 0}>
|
<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="flex h-full flex-1 flex-col items-center justify-center gap-y-4 py-16">
|
||||||
@@ -752,24 +622,17 @@ function EnvList({ appState }: { appState: AppEnvState }) {
|
|||||||
</If>
|
</If>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<EnvListDisplay
|
||||||
|
className="sticky top-0 w-6/12 overflow-y-auto"
|
||||||
|
groups={groups}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</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: {
|
function FilterSwitcher(props: {
|
||||||
filters: {
|
filters: {
|
||||||
secret: boolean;
|
secret: boolean;
|
||||||
@@ -875,66 +738,15 @@ function FilterSwitcher(props: {
|
|||||||
|
|
||||||
function Summary({ appState }: { appState: AppEnvState }) {
|
function Summary({ appState }: { appState: AppEnvState }) {
|
||||||
const varsArray = Object.values(appState.variables);
|
const varsArray = Object.values(appState.variables);
|
||||||
const allVariables = getEffectiveVariablesValue(appState);
|
|
||||||
const overridden = varsArray.filter((variable) => variable.isOverridden);
|
const overridden = varsArray.filter((variable) => variable.isOverridden);
|
||||||
const handleFilterChange = useUpdateFilteredVariables();
|
const handleFilterChange = useUpdateFilteredVariables();
|
||||||
|
|
||||||
// Find all variables with errors (including missing required and contextual validation)
|
// Find all variables with errors (including missing required and contextual validation)
|
||||||
const errors = envVariables.reduce<string[]>((acc, model) => {
|
const variablesWithErrors = varsArray.filter((variable) => {
|
||||||
// Get the current value of this variable
|
return !variable.validation.success;
|
||||||
const varState = appState.variables[model.name];
|
|
||||||
const value = varState?.effectiveValue;
|
|
||||||
let hasError = false;
|
|
||||||
|
|
||||||
// Check if it's required but missing
|
|
||||||
if (model.required && !value) {
|
|
||||||
hasError = true;
|
|
||||||
} else if (model.contextualValidation) {
|
|
||||||
// Check if any dependency conditions are met
|
|
||||||
const dependenciesErrors = model.contextualValidation.dependencies.some(
|
|
||||||
(dep) => {
|
|
||||||
const dependencyValue = allVariables[dep.variable] ?? '';
|
|
||||||
|
|
||||||
const shouldValidate = dep.condition(dependencyValue, allVariables);
|
|
||||||
|
|
||||||
if (shouldValidate) {
|
|
||||||
const { error } = model.contextualValidation!.validate({
|
|
||||||
value: varState?.effectiveValue ?? '',
|
|
||||||
variables: allVariables,
|
|
||||||
mode: appState.mode,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return error;
|
const validVariables = varsArray.length - variablesWithErrors.length;
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (dependenciesErrors) {
|
|
||||||
hasError = true;
|
|
||||||
}
|
|
||||||
} else if (model.validate && value) {
|
|
||||||
// Only run regular validation if:
|
|
||||||
// 1. There's no contextual validation
|
|
||||||
// 2. There's a value to validate
|
|
||||||
const result = model.validate({
|
|
||||||
value,
|
|
||||||
variables: allVariables,
|
|
||||||
mode: appState.mode,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!result.success) {
|
|
||||||
hasError = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasError) {
|
|
||||||
acc.push(model.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const validVariables = varsArray.length - errors.length;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-between space-x-4">
|
<div className="flex justify-between space-x-4">
|
||||||
@@ -946,11 +758,11 @@ function Summary({ appState }: { appState: AppEnvState }) {
|
|||||||
<Badge
|
<Badge
|
||||||
variant={'outline'}
|
variant={'outline'}
|
||||||
className={cn({
|
className={cn({
|
||||||
'text-destructive': errors.length > 0,
|
'text-destructive': variablesWithErrors.length > 0,
|
||||||
'text-green-500': errors.length === 0,
|
'text-green-500': variablesWithErrors.length === 0,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{errors.length} Invalid
|
{variablesWithErrors.length} Invalid
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|
||||||
<If condition={overridden.length > 0}>
|
<If condition={overridden.length > 0}>
|
||||||
@@ -963,17 +775,59 @@ function Summary({ appState }: { appState: AppEnvState }) {
|
|||||||
</If>
|
</If>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div className={'flex items-center gap-x-2'}>
|
||||||
<If condition={errors.length > 0}>
|
<If condition={variablesWithErrors.length > 0}>
|
||||||
<Button
|
<Button
|
||||||
size={'sm'}
|
size={'sm'}
|
||||||
variant={'ghost'}
|
variant={'outline'}
|
||||||
onClick={() => handleFilterChange('invalid', true, true)}
|
onClick={() => handleFilterChange('invalid', true, true)}
|
||||||
>
|
>
|
||||||
<EyeOffIcon className="mr-2 h-3 w-3" />
|
<EyeOffIcon className="mr-2 h-3 w-3" />
|
||||||
Display Invalid only
|
Display Invalid only
|
||||||
</Button>
|
</Button>
|
||||||
</If>
|
</If>
|
||||||
|
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size={'sm'}
|
||||||
|
onClick={() => {
|
||||||
|
let data = '';
|
||||||
|
|
||||||
|
const groups = getGroups(appState, () => true);
|
||||||
|
|
||||||
|
groups.forEach((group) => {
|
||||||
|
data += `# ${group.category}\n`;
|
||||||
|
|
||||||
|
group.variables.forEach((variable) => {
|
||||||
|
data += `${variable.key}=${variable.effectiveValue}\n`;
|
||||||
|
});
|
||||||
|
|
||||||
|
data += '\n';
|
||||||
|
});
|
||||||
|
|
||||||
|
const promise = copyToClipboard(data);
|
||||||
|
|
||||||
|
toast.promise(promise, {
|
||||||
|
loading: 'Copying environment variables...',
|
||||||
|
success: 'Environment variables copied to clipboard.',
|
||||||
|
error: 'Failed to copy environment variables to clipboard',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CopyIcon className={'mr-2 h-4 w-4'} />
|
||||||
|
<span>Copy env file to clipboard</span>
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
|
||||||
|
<TooltipContent>
|
||||||
|
Copy environment variables to clipboard. You can place it in your
|
||||||
|
hosting provider to set up the full environment.
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -1027,3 +881,35 @@ function useUpdateFilteredVariables() {
|
|||||||
|
|
||||||
return useCallback(handleFilterChange, [router]);
|
return useCallback(handleFilterChange, [router]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function copyToClipboard(text: string) {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to copy:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getGroups(
|
||||||
|
appState: AppEnvState,
|
||||||
|
filterVariable: (variable: EnvVariableState) => boolean,
|
||||||
|
) {
|
||||||
|
return Object.values(appState.variables).reduce(
|
||||||
|
(acc, variable) => {
|
||||||
|
const group = acc.find((group) => group.category === variable.category);
|
||||||
|
variable.isVisible = filterVariable(variable);
|
||||||
|
|
||||||
|
if (!group) {
|
||||||
|
acc.push({
|
||||||
|
category: variable.category,
|
||||||
|
variables: [variable],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
group.variables.push(variable);
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
[] as Array<{ category: string; variables: Array<EnvVariableState> }>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
161
apps/dev-tool/app/variables/components/dynamic-form-input.tsx
Normal file
161
apps/dev-tool/app/variables/components/dynamic-form-input.tsx
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
import { Input } from '@kit/ui/input';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@kit/ui/select';
|
||||||
|
import { Switch } from '@kit/ui/switch';
|
||||||
|
import { Textarea } from '@kit/ui/textarea';
|
||||||
|
|
||||||
|
type ModelType =
|
||||||
|
| 'string'
|
||||||
|
| 'longString'
|
||||||
|
| 'number'
|
||||||
|
| 'boolean'
|
||||||
|
| 'enum'
|
||||||
|
| 'url'
|
||||||
|
| 'email';
|
||||||
|
|
||||||
|
interface DynamicFormInputProps {
|
||||||
|
type: ModelType;
|
||||||
|
value: string;
|
||||||
|
name: string;
|
||||||
|
onChange: (props: { name: string; value: string }) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
enumValues?: Array<string | null>;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DynamicFormInput({
|
||||||
|
type,
|
||||||
|
value,
|
||||||
|
name,
|
||||||
|
onChange,
|
||||||
|
placeholder,
|
||||||
|
enumValues = [],
|
||||||
|
className,
|
||||||
|
}: DynamicFormInputProps) {
|
||||||
|
const handleInputChange = useCallback(
|
||||||
|
(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||||
|
onChange({
|
||||||
|
name,
|
||||||
|
value: e.target.value,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[name, onChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSwitchChange = useCallback(
|
||||||
|
(checked: boolean) => {
|
||||||
|
onChange({
|
||||||
|
name,
|
||||||
|
value: checked ? 'true' : 'false',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[name, onChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSelectChange = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
onChange({
|
||||||
|
name,
|
||||||
|
value: value === '' ? 'none' : value,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[name, onChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'longString':
|
||||||
|
return (
|
||||||
|
<Textarea
|
||||||
|
defaultValue={value}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
placeholder={placeholder}
|
||||||
|
className={className}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'number':
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
defaultValue={value}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
placeholder={placeholder}
|
||||||
|
className={className}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'boolean':
|
||||||
|
return (
|
||||||
|
<label className="flex items-center gap-x-2">
|
||||||
|
<Switch
|
||||||
|
checked={value === 'true'}
|
||||||
|
onCheckedChange={handleSwitchChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span className={'text-sm uppercase'}>
|
||||||
|
{value === 'true' ? 'True' : 'False'}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'url':
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
type="url"
|
||||||
|
defaultValue={value}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
placeholder={placeholder}
|
||||||
|
className={className}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'email':
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
type="email"
|
||||||
|
defaultValue={value}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
placeholder={placeholder}
|
||||||
|
className={className}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'enum':
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
value={value === 'none' ? '' : value}
|
||||||
|
onValueChange={handleSelectChange}
|
||||||
|
>
|
||||||
|
<SelectTrigger className={className}>
|
||||||
|
<SelectValue placeholder={placeholder} />
|
||||||
|
</SelectTrigger>
|
||||||
|
|
||||||
|
<SelectContent>
|
||||||
|
{enumValues.map((enumValue) => (
|
||||||
|
<SelectItem key={enumValue} value={enumValue as string}>
|
||||||
|
{enumValue}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
defaultValue={value}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
placeholder={placeholder}
|
||||||
|
className={className}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -152,11 +152,18 @@ export function processEnvDefinitions(
|
|||||||
if (!variableMap[variable.key]) {
|
if (!variableMap[variable.key]) {
|
||||||
variableMap[variable.key] = {
|
variableMap[variable.key] = {
|
||||||
key: variable.key,
|
key: variable.key,
|
||||||
|
isVisible: true,
|
||||||
definitions: [],
|
definitions: [],
|
||||||
effectiveValue: variable.value,
|
effectiveValue: variable.value,
|
||||||
effectiveSource: variable.source,
|
effectiveSource: variable.source,
|
||||||
isOverridden: false,
|
isOverridden: false,
|
||||||
category: model ? model.category : 'Custom',
|
category: model ? model.category : 'Custom',
|
||||||
|
validation: {
|
||||||
|
success: true,
|
||||||
|
error: {
|
||||||
|
issues: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,6 +219,231 @@ export function processEnvDefinitions(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// after computing the effective values, we can check for errors
|
||||||
|
for (const key in variableMap) {
|
||||||
|
const model = envVariables.find((v) => key === v.name);
|
||||||
|
const varState = variableMap[key];
|
||||||
|
|
||||||
|
if (!varState) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let validation: {
|
||||||
|
success: boolean;
|
||||||
|
error: {
|
||||||
|
issues: string[];
|
||||||
|
};
|
||||||
|
} = { success: true, error: { issues: [] } };
|
||||||
|
|
||||||
|
if (model) {
|
||||||
|
const allVariables = Object.values(variableMap).reduce(
|
||||||
|
(acc, variable) => {
|
||||||
|
return {
|
||||||
|
...acc,
|
||||||
|
[variable.key]: variable.effectiveValue,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{} as Record<string, string>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// First check if it's required but missing
|
||||||
|
if (model.required && !varState.effectiveValue) {
|
||||||
|
validation = {
|
||||||
|
success: false,
|
||||||
|
error: {
|
||||||
|
issues: [
|
||||||
|
`This variable is required but missing from your environment files`,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else if (model.contextualValidation) {
|
||||||
|
// Then check contextual validation
|
||||||
|
const dependenciesMet = model.contextualValidation.dependencies.some(
|
||||||
|
(dep) => {
|
||||||
|
const dependencyValue = allVariables[dep.variable] ?? '';
|
||||||
|
|
||||||
|
return dep.condition(dependencyValue, allVariables);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (dependenciesMet) {
|
||||||
|
// Only check for missing value or run validation if dependencies are met
|
||||||
|
if (!varState.effectiveValue) {
|
||||||
|
const dependencyErrors = model.contextualValidation.dependencies
|
||||||
|
.map((dep) => {
|
||||||
|
const dependencyValue = allVariables[dep.variable] ?? '';
|
||||||
|
|
||||||
|
const shouldValidate = dep.condition(
|
||||||
|
dependencyValue,
|
||||||
|
allVariables,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (shouldValidate) {
|
||||||
|
const { success } = model.contextualValidation!.validate({
|
||||||
|
value: varState.effectiveValue,
|
||||||
|
variables: allVariables,
|
||||||
|
mode,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return dep.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
.filter((message): message is string => message !== null);
|
||||||
|
|
||||||
|
validation = {
|
||||||
|
success: dependencyErrors.length === 0,
|
||||||
|
error: {
|
||||||
|
issues: dependencyErrors
|
||||||
|
.map((message) => message)
|
||||||
|
.filter((message) => !!message),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// If we have a value and dependencies are met, run contextual validation
|
||||||
|
const result = model.contextualValidation.validate({
|
||||||
|
value: varState.effectiveValue,
|
||||||
|
variables: allVariables,
|
||||||
|
mode,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
validation = {
|
||||||
|
success: false,
|
||||||
|
error: {
|
||||||
|
issues: result.error.issues
|
||||||
|
.map((issue) => issue.message)
|
||||||
|
.filter((message) => !!message),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (model.validate && varState.effectiveValue) {
|
||||||
|
// Only run regular validation if:
|
||||||
|
// 1. There's no contextual validation
|
||||||
|
// 2. There's a value to validate
|
||||||
|
const result = model.validate({
|
||||||
|
value: varState.effectiveValue,
|
||||||
|
variables: allVariables,
|
||||||
|
mode,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
validation = {
|
||||||
|
success: false,
|
||||||
|
error: {
|
||||||
|
issues: result.error.issues
|
||||||
|
.map((issue) => issue.message)
|
||||||
|
.filter((message) => !!message),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
varState.validation = validation;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final pass: Validate missing variables that are marked as required
|
||||||
|
// or as having contextual validation
|
||||||
|
for (const model of envVariables) {
|
||||||
|
// If the variable exists in appState, use that
|
||||||
|
const existingVar = variableMap[model.name];
|
||||||
|
|
||||||
|
if (existingVar) {
|
||||||
|
// If the variable is already in the map, skip it
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (model.required || model.contextualValidation) {
|
||||||
|
if (model.contextualValidation) {
|
||||||
|
const allVariables = Object.values(variableMap).reduce(
|
||||||
|
(acc, variable) => {
|
||||||
|
return {
|
||||||
|
...acc,
|
||||||
|
[variable.key]: variable.effectiveValue,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{} as Record<string, string>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const errors =
|
||||||
|
model?.contextualValidation?.dependencies
|
||||||
|
.map((dep) => {
|
||||||
|
const dependencyValue = allVariables[dep.variable] ?? '';
|
||||||
|
const shouldValidate = dep.condition(
|
||||||
|
dependencyValue,
|
||||||
|
allVariables,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!shouldValidate) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const effectiveValue = allVariables[dep.variable] ?? '';
|
||||||
|
|
||||||
|
const validation = model.contextualValidation!.validate({
|
||||||
|
value: effectiveValue,
|
||||||
|
variables: allVariables,
|
||||||
|
mode,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (validation) {
|
||||||
|
return [dep.message];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
})
|
||||||
|
.flat() ?? ([] as string[]);
|
||||||
|
|
||||||
|
if (errors.length === 0) {
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
variableMap[model.name] = {
|
||||||
|
key: model.name,
|
||||||
|
effectiveValue: '',
|
||||||
|
effectiveSource: 'MISSING',
|
||||||
|
isVisible: true,
|
||||||
|
category: model.category,
|
||||||
|
isOverridden: false,
|
||||||
|
definitions: [],
|
||||||
|
validation: {
|
||||||
|
success: false,
|
||||||
|
error: {
|
||||||
|
issues: errors.map((error) => error),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it doesn't exist but is required or has contextual validation, create an empty state
|
||||||
|
variableMap[model.name] = {
|
||||||
|
key: model.name,
|
||||||
|
effectiveValue: '',
|
||||||
|
effectiveSource: 'MISSING',
|
||||||
|
isVisible: true,
|
||||||
|
category: model.category,
|
||||||
|
isOverridden: false,
|
||||||
|
definitions: [],
|
||||||
|
validation: {
|
||||||
|
success: false,
|
||||||
|
error: {
|
||||||
|
issues: [
|
||||||
|
`This variable is required but missing from your environment files`,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
appName: envInfo.appName,
|
appName: envInfo.appName,
|
||||||
filePath: envInfo.filePath,
|
filePath: envInfo.filePath,
|
||||||
@@ -227,11 +459,6 @@ export async function getEnvState(
|
|||||||
return envInfos.map((info) => processEnvDefinitions(info, options.mode));
|
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) {
|
export async function getVariable(key: string, mode: EnvMode) {
|
||||||
// Get the processed environment state for all apps (you can limit to 'web' via options)
|
// Get the processed environment state for all apps (you can limit to 'web' via options)
|
||||||
const envStates = await getEnvState({ mode, apps: ['web'] });
|
const envStates = await getEnvState({ mode, apps: ['web'] });
|
||||||
|
|||||||
@@ -1,34 +1,51 @@
|
|||||||
import { EnvMode } from '@/app/variables/lib/types';
|
import { EnvMode } from '@/app/variables/lib/types';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
type DependencyRule = {
|
type ModelType =
|
||||||
variable: string;
|
| 'string'
|
||||||
condition: (value: string, variables: Record<string, string>) => boolean;
|
| 'longString'
|
||||||
message: string;
|
| 'number'
|
||||||
};
|
| 'boolean'
|
||||||
|
| 'enum'
|
||||||
|
| 'url'
|
||||||
|
| 'email';
|
||||||
|
|
||||||
type ContextualValidation = {
|
type Values = Array<string | null>;
|
||||||
dependencies: DependencyRule[];
|
|
||||||
validate: (props: {
|
|
||||||
value: string;
|
|
||||||
variables: Record<string, string>;
|
|
||||||
mode: EnvMode;
|
|
||||||
}) => z.SafeParseReturnType<unknown, unknown>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type EnvVariableModel = {
|
export type EnvVariableModel = {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
hint?: string;
|
||||||
secret?: boolean;
|
secret?: boolean;
|
||||||
required?: boolean;
|
type?: ModelType;
|
||||||
|
values?: Values;
|
||||||
category: string;
|
category: string;
|
||||||
test?: (value: string) => Promise<boolean>;
|
required?: boolean;
|
||||||
validate?: (props: {
|
validate?: ({
|
||||||
|
value,
|
||||||
|
variables,
|
||||||
|
mode,
|
||||||
|
}: {
|
||||||
value: string;
|
value: string;
|
||||||
variables: Record<string, string>;
|
variables: Record<string, string>;
|
||||||
mode: EnvMode;
|
mode: EnvMode;
|
||||||
}) => z.SafeParseReturnType<unknown, unknown>;
|
}) => z.SafeParseReturnType<unknown, unknown>;
|
||||||
contextualValidation?: ContextualValidation;
|
contextualValidation?: {
|
||||||
|
dependencies: Array<{
|
||||||
|
variable: string;
|
||||||
|
condition: (value: string, variables: Record<string, string>) => boolean;
|
||||||
|
message: string;
|
||||||
|
}>;
|
||||||
|
validate: ({
|
||||||
|
value,
|
||||||
|
variables,
|
||||||
|
mode,
|
||||||
|
}: {
|
||||||
|
value: string;
|
||||||
|
variables: Record<string, string>;
|
||||||
|
mode: EnvMode;
|
||||||
|
}) => z.SafeParseReturnType<unknown, unknown>;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const envVariables: EnvVariableModel[] = [
|
export const envVariables: EnvVariableModel[] = [
|
||||||
@@ -38,6 +55,8 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
'The URL of your site, used for generating absolute URLs. Must include the protocol.',
|
'The URL of your site, used for generating absolute URLs. Must include the protocol.',
|
||||||
category: 'Site Configuration',
|
category: 'Site Configuration',
|
||||||
required: true,
|
required: true,
|
||||||
|
type: 'url',
|
||||||
|
hint: `Ex. https://example.com`,
|
||||||
validate: ({ value, mode }) => {
|
validate: ({ value, mode }) => {
|
||||||
if (mode === 'development') {
|
if (mode === 'development') {
|
||||||
return z
|
return z
|
||||||
@@ -65,7 +84,9 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
description:
|
description:
|
||||||
"Your product's name, used consistently across the application interface.",
|
"Your product's name, used consistently across the application interface.",
|
||||||
category: 'Site Configuration',
|
category: 'Site Configuration',
|
||||||
|
hint: `Ex. "My Product"`,
|
||||||
required: true,
|
required: true,
|
||||||
|
type: 'string',
|
||||||
validate: ({ value }) => {
|
validate: ({ value }) => {
|
||||||
return z
|
return z
|
||||||
.string()
|
.string()
|
||||||
@@ -82,6 +103,8 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
"The site's title tag content, crucial for SEO and browser display.",
|
"The site's title tag content, crucial for SEO and browser display.",
|
||||||
category: 'Site Configuration',
|
category: 'Site Configuration',
|
||||||
required: true,
|
required: true,
|
||||||
|
hint: `Ex. "My Product, the best product ever"`,
|
||||||
|
type: 'string',
|
||||||
validate: ({ value }) => {
|
validate: ({ value }) => {
|
||||||
return z
|
return z
|
||||||
.string()
|
.string()
|
||||||
@@ -94,6 +117,7 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'NEXT_PUBLIC_SITE_DESCRIPTION',
|
name: 'NEXT_PUBLIC_SITE_DESCRIPTION',
|
||||||
|
type: 'longString',
|
||||||
description:
|
description:
|
||||||
"Your site's meta description, important for SEO optimization.",
|
"Your site's meta description, important for SEO optimization.",
|
||||||
category: 'Site Configuration',
|
category: 'Site Configuration',
|
||||||
@@ -111,8 +135,10 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'NEXT_PUBLIC_DEFAULT_LOCALE',
|
name: 'NEXT_PUBLIC_DEFAULT_LOCALE',
|
||||||
|
type: 'string',
|
||||||
description: 'Sets the default language for your application.',
|
description: 'Sets the default language for your application.',
|
||||||
category: 'Localization',
|
category: 'Localization',
|
||||||
|
hint: `Ex. "en"`,
|
||||||
validate: ({ value }) => {
|
validate: ({ value }) => {
|
||||||
return z
|
return z
|
||||||
.string()
|
.string()
|
||||||
@@ -128,6 +154,7 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
name: 'NEXT_PUBLIC_AUTH_PASSWORD',
|
name: 'NEXT_PUBLIC_AUTH_PASSWORD',
|
||||||
description: 'Enables or disables password-based authentication.',
|
description: 'Enables or disables password-based authentication.',
|
||||||
category: 'Authentication',
|
category: 'Authentication',
|
||||||
|
type: 'boolean',
|
||||||
validate: ({ value }) => {
|
validate: ({ value }) => {
|
||||||
return z.coerce.boolean().optional().safeParse(value);
|
return z.coerce.boolean().optional().safeParse(value);
|
||||||
},
|
},
|
||||||
@@ -136,6 +163,7 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
name: 'NEXT_PUBLIC_AUTH_MAGIC_LINK',
|
name: 'NEXT_PUBLIC_AUTH_MAGIC_LINK',
|
||||||
description: 'Enables or disables magic link authentication.',
|
description: 'Enables or disables magic link authentication.',
|
||||||
category: 'Authentication',
|
category: 'Authentication',
|
||||||
|
type: 'boolean',
|
||||||
validate: ({ value }) => {
|
validate: ({ value }) => {
|
||||||
return z.coerce.boolean().optional().safeParse(value);
|
return z.coerce.boolean().optional().safeParse(value);
|
||||||
},
|
},
|
||||||
@@ -144,6 +172,7 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
name: 'NEXT_PUBLIC_CAPTCHA_SITE_KEY',
|
name: 'NEXT_PUBLIC_CAPTCHA_SITE_KEY',
|
||||||
description: 'Your Cloudflare Captcha site key for form protection.',
|
description: 'Your Cloudflare Captcha site key for form protection.',
|
||||||
category: 'Security',
|
category: 'Security',
|
||||||
|
type: 'string',
|
||||||
validate: ({ value }) => {
|
validate: ({ value }) => {
|
||||||
return z.string().optional().safeParse(value);
|
return z.string().optional().safeParse(value);
|
||||||
},
|
},
|
||||||
@@ -154,6 +183,7 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
'Your Cloudflare Captcha secret token for backend verification.',
|
'Your Cloudflare Captcha secret token for backend verification.',
|
||||||
category: 'Security',
|
category: 'Security',
|
||||||
secret: true,
|
secret: true,
|
||||||
|
type: 'string',
|
||||||
contextualValidation: {
|
contextualValidation: {
|
||||||
dependencies: [
|
dependencies: [
|
||||||
{
|
{
|
||||||
@@ -191,6 +221,8 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
description:
|
description:
|
||||||
'Controls user navigation layout. Options: sidebar, header, or custom.',
|
'Controls user navigation layout. Options: sidebar, header, or custom.',
|
||||||
category: 'Navigation',
|
category: 'Navigation',
|
||||||
|
type: 'enum',
|
||||||
|
values: ['sidebar', 'header', 'custom'],
|
||||||
validate: ({ value }) => {
|
validate: ({ value }) => {
|
||||||
return z
|
return z
|
||||||
.enum(['sidebar', 'header', 'custom'])
|
.enum(['sidebar', 'header', 'custom'])
|
||||||
@@ -202,6 +234,7 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
name: 'NEXT_PUBLIC_HOME_SIDEBAR_COLLAPSED',
|
name: 'NEXT_PUBLIC_HOME_SIDEBAR_COLLAPSED',
|
||||||
description: 'Sets the default state of the home sidebar.',
|
description: 'Sets the default state of the home sidebar.',
|
||||||
category: 'Navigation',
|
category: 'Navigation',
|
||||||
|
type: 'boolean',
|
||||||
validate: ({ value }) => {
|
validate: ({ value }) => {
|
||||||
return z.coerce.boolean().optional().safeParse(value);
|
return z.coerce.boolean().optional().safeParse(value);
|
||||||
},
|
},
|
||||||
@@ -211,6 +244,8 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
description:
|
description:
|
||||||
'Controls team navigation layout. Options: sidebar, header, or custom.',
|
'Controls team navigation layout. Options: sidebar, header, or custom.',
|
||||||
category: 'Navigation',
|
category: 'Navigation',
|
||||||
|
type: 'enum',
|
||||||
|
values: ['sidebar', 'header', 'custom'],
|
||||||
validate: ({ value }) => {
|
validate: ({ value }) => {
|
||||||
return z
|
return z
|
||||||
.enum(['sidebar', 'header', 'custom'])
|
.enum(['sidebar', 'header', 'custom'])
|
||||||
@@ -222,6 +257,7 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
name: 'NEXT_PUBLIC_TEAM_SIDEBAR_COLLAPSED',
|
name: 'NEXT_PUBLIC_TEAM_SIDEBAR_COLLAPSED',
|
||||||
description: 'Sets the default state of the team sidebar.',
|
description: 'Sets the default state of the team sidebar.',
|
||||||
category: 'Navigation',
|
category: 'Navigation',
|
||||||
|
type: 'boolean',
|
||||||
validate: ({ value }) => {
|
validate: ({ value }) => {
|
||||||
return z.coerce.boolean().optional().safeParse(value);
|
return z.coerce.boolean().optional().safeParse(value);
|
||||||
},
|
},
|
||||||
@@ -231,6 +267,8 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
description:
|
description:
|
||||||
'Defines sidebar collapse behavior. Options: offscreen, icon, or none.',
|
'Defines sidebar collapse behavior. Options: offscreen, icon, or none.',
|
||||||
category: 'Navigation',
|
category: 'Navigation',
|
||||||
|
type: 'enum',
|
||||||
|
values: ['offscreen', 'icon', 'none'],
|
||||||
validate: ({ value }) => {
|
validate: ({ value }) => {
|
||||||
return z.enum(['offscreen', 'icon', 'none']).optional().safeParse(value);
|
return z.enum(['offscreen', 'icon', 'none']).optional().safeParse(value);
|
||||||
},
|
},
|
||||||
@@ -240,6 +278,8 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
description:
|
description:
|
||||||
'Controls the default theme appearance. Options: light, dark, or system.',
|
'Controls the default theme appearance. Options: light, dark, or system.',
|
||||||
category: 'Theme',
|
category: 'Theme',
|
||||||
|
type: 'enum',
|
||||||
|
values: ['light', 'dark', 'system'],
|
||||||
validate: ({ value }) => {
|
validate: ({ value }) => {
|
||||||
return z.enum(['light', 'dark', 'system']).optional().safeParse(value);
|
return z.enum(['light', 'dark', 'system']).optional().safeParse(value);
|
||||||
},
|
},
|
||||||
@@ -248,6 +288,7 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
name: 'NEXT_PUBLIC_ENABLE_THEME_TOGGLE',
|
name: 'NEXT_PUBLIC_ENABLE_THEME_TOGGLE',
|
||||||
description: 'Controls visibility of the theme toggle feature.',
|
description: 'Controls visibility of the theme toggle feature.',
|
||||||
category: 'Theme',
|
category: 'Theme',
|
||||||
|
type: 'boolean',
|
||||||
validate: ({ value }) => {
|
validate: ({ value }) => {
|
||||||
return z.coerce.boolean().optional().safeParse(value);
|
return z.coerce.boolean().optional().safeParse(value);
|
||||||
},
|
},
|
||||||
@@ -256,6 +297,7 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
name: 'NEXT_PUBLIC_ENABLE_SIDEBAR_TRIGGER',
|
name: 'NEXT_PUBLIC_ENABLE_SIDEBAR_TRIGGER',
|
||||||
description: 'Controls visibility of the sidebar trigger feature.',
|
description: 'Controls visibility of the sidebar trigger feature.',
|
||||||
category: 'Navigation',
|
category: 'Navigation',
|
||||||
|
type: 'boolean',
|
||||||
validate: ({ value }) => {
|
validate: ({ value }) => {
|
||||||
return z.coerce.boolean().optional().safeParse(value);
|
return z.coerce.boolean().optional().safeParse(value);
|
||||||
},
|
},
|
||||||
@@ -264,6 +306,7 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
name: 'NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_DELETION',
|
name: 'NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_DELETION',
|
||||||
description: 'Allows users to delete their personal accounts.',
|
description: 'Allows users to delete their personal accounts.',
|
||||||
category: 'Features',
|
category: 'Features',
|
||||||
|
type: 'boolean',
|
||||||
validate: ({ value }) => {
|
validate: ({ value }) => {
|
||||||
return z.coerce.boolean().optional().safeParse(value);
|
return z.coerce.boolean().optional().safeParse(value);
|
||||||
},
|
},
|
||||||
@@ -272,6 +315,7 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
name: 'NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_BILLING',
|
name: 'NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_BILLING',
|
||||||
description: 'Enables billing features for personal accounts.',
|
description: 'Enables billing features for personal accounts.',
|
||||||
category: 'Features',
|
category: 'Features',
|
||||||
|
type: 'boolean',
|
||||||
validate: ({ value }) => {
|
validate: ({ value }) => {
|
||||||
return z.coerce.boolean().optional().safeParse(value);
|
return z.coerce.boolean().optional().safeParse(value);
|
||||||
},
|
},
|
||||||
@@ -280,6 +324,7 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
name: 'NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS',
|
name: 'NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS',
|
||||||
description: 'Master switch for team account functionality.',
|
description: 'Master switch for team account functionality.',
|
||||||
category: 'Features',
|
category: 'Features',
|
||||||
|
type: 'boolean',
|
||||||
validate: ({ value }) => {
|
validate: ({ value }) => {
|
||||||
return z.coerce.boolean().optional().safeParse(value);
|
return z.coerce.boolean().optional().safeParse(value);
|
||||||
},
|
},
|
||||||
@@ -288,6 +333,7 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
name: 'NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_CREATION',
|
name: 'NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_CREATION',
|
||||||
description: 'Controls ability to create new team accounts.',
|
description: 'Controls ability to create new team accounts.',
|
||||||
category: 'Features',
|
category: 'Features',
|
||||||
|
type: 'boolean',
|
||||||
validate: ({ value }) => {
|
validate: ({ value }) => {
|
||||||
return z.coerce.boolean().optional().safeParse(value);
|
return z.coerce.boolean().optional().safeParse(value);
|
||||||
},
|
},
|
||||||
@@ -296,6 +342,7 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
name: 'NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_DELETION',
|
name: 'NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_DELETION',
|
||||||
description: 'Allows team account deletion.',
|
description: 'Allows team account deletion.',
|
||||||
category: 'Features',
|
category: 'Features',
|
||||||
|
type: 'boolean',
|
||||||
validate: ({ value }) => {
|
validate: ({ value }) => {
|
||||||
return z.coerce.boolean().optional().safeParse(value);
|
return z.coerce.boolean().optional().safeParse(value);
|
||||||
},
|
},
|
||||||
@@ -304,6 +351,7 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
name: 'NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_BILLING',
|
name: 'NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_BILLING',
|
||||||
description: 'Enables billing features for team accounts.',
|
description: 'Enables billing features for team accounts.',
|
||||||
category: 'Features',
|
category: 'Features',
|
||||||
|
type: 'boolean',
|
||||||
validate: ({ value }) => {
|
validate: ({ value }) => {
|
||||||
return z.coerce.boolean().optional().safeParse(value);
|
return z.coerce.boolean().optional().safeParse(value);
|
||||||
},
|
},
|
||||||
@@ -312,6 +360,7 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
name: 'NEXT_PUBLIC_ENABLE_NOTIFICATIONS',
|
name: 'NEXT_PUBLIC_ENABLE_NOTIFICATIONS',
|
||||||
description: 'Controls the notification system.',
|
description: 'Controls the notification system.',
|
||||||
category: 'Notifications',
|
category: 'Notifications',
|
||||||
|
type: 'boolean',
|
||||||
validate: ({ value }) => {
|
validate: ({ value }) => {
|
||||||
return z.coerce.boolean().optional().safeParse(value);
|
return z.coerce.boolean().optional().safeParse(value);
|
||||||
},
|
},
|
||||||
@@ -320,6 +369,7 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
name: 'NEXT_PUBLIC_REALTIME_NOTIFICATIONS',
|
name: 'NEXT_PUBLIC_REALTIME_NOTIFICATIONS',
|
||||||
description: 'Enables real-time notifications using Supabase Realtime.',
|
description: 'Enables real-time notifications using Supabase Realtime.',
|
||||||
category: 'Notifications',
|
category: 'Notifications',
|
||||||
|
type: 'boolean',
|
||||||
validate: ({ value }) => {
|
validate: ({ value }) => {
|
||||||
return z.coerce.boolean().optional().safeParse(value);
|
return z.coerce.boolean().optional().safeParse(value);
|
||||||
},
|
},
|
||||||
@@ -328,7 +378,9 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
name: 'NEXT_PUBLIC_SUPABASE_URL',
|
name: 'NEXT_PUBLIC_SUPABASE_URL',
|
||||||
description: 'Your Supabase project URL.',
|
description: 'Your Supabase project URL.',
|
||||||
category: 'Supabase',
|
category: 'Supabase',
|
||||||
|
hint: `Ex. https://your-project.supabase.co`,
|
||||||
required: true,
|
required: true,
|
||||||
|
type: 'url',
|
||||||
validate: ({ value, mode }) => {
|
validate: ({ value, mode }) => {
|
||||||
if (mode === 'development') {
|
if (mode === 'development') {
|
||||||
return z
|
return z
|
||||||
@@ -356,6 +408,7 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
description: 'Your Supabase anonymous API key.',
|
description: 'Your Supabase anonymous API key.',
|
||||||
category: 'Supabase',
|
category: 'Supabase',
|
||||||
required: true,
|
required: true,
|
||||||
|
type: 'string',
|
||||||
validate: ({ value }) => {
|
validate: ({ value }) => {
|
||||||
return z
|
return z
|
||||||
.string()
|
.string()
|
||||||
@@ -372,6 +425,7 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
category: 'Supabase',
|
category: 'Supabase',
|
||||||
secret: true,
|
secret: true,
|
||||||
required: true,
|
required: true,
|
||||||
|
type: 'string',
|
||||||
validate: ({ value, variables }) => {
|
validate: ({ value, variables }) => {
|
||||||
return z
|
return z
|
||||||
.string()
|
.string()
|
||||||
@@ -396,6 +450,7 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
category: 'Supabase',
|
category: 'Supabase',
|
||||||
secret: true,
|
secret: true,
|
||||||
required: true,
|
required: true,
|
||||||
|
type: 'string',
|
||||||
validate: ({ value }) => {
|
validate: ({ value }) => {
|
||||||
return z
|
return z
|
||||||
.string()
|
.string()
|
||||||
@@ -413,6 +468,8 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
'Your chosen billing provider. Options: stripe or lemon-squeezy.',
|
'Your chosen billing provider. Options: stripe or lemon-squeezy.',
|
||||||
category: 'Billing',
|
category: 'Billing',
|
||||||
required: true,
|
required: true,
|
||||||
|
type: 'enum',
|
||||||
|
values: ['stripe', 'lemon-squeezy'],
|
||||||
validate: ({ value }) => {
|
validate: ({ value }) => {
|
||||||
return z.enum(['stripe', 'lemon-squeezy']).optional().safeParse(value);
|
return z.enum(['stripe', 'lemon-squeezy']).optional().safeParse(value);
|
||||||
},
|
},
|
||||||
@@ -420,7 +477,9 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
{
|
{
|
||||||
name: 'NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY',
|
name: 'NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY',
|
||||||
description: 'Your Stripe publishable key.',
|
description: 'Your Stripe publishable key.',
|
||||||
|
hint: `Ex. pk_test_123456789012345678901234`,
|
||||||
category: 'Billing',
|
category: 'Billing',
|
||||||
|
type: 'string',
|
||||||
contextualValidation: {
|
contextualValidation: {
|
||||||
dependencies: [
|
dependencies: [
|
||||||
{
|
{
|
||||||
@@ -463,7 +522,9 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
name: 'STRIPE_SECRET_KEY',
|
name: 'STRIPE_SECRET_KEY',
|
||||||
description: 'Your Stripe secret key.',
|
description: 'Your Stripe secret key.',
|
||||||
category: 'Billing',
|
category: 'Billing',
|
||||||
|
hint: `Ex. sk_test_123456789012345678901234`,
|
||||||
secret: true,
|
secret: true,
|
||||||
|
type: 'string',
|
||||||
contextualValidation: {
|
contextualValidation: {
|
||||||
dependencies: [
|
dependencies: [
|
||||||
{
|
{
|
||||||
@@ -500,7 +561,9 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
name: 'STRIPE_WEBHOOK_SECRET',
|
name: 'STRIPE_WEBHOOK_SECRET',
|
||||||
description: 'Your Stripe webhook secret.',
|
description: 'Your Stripe webhook secret.',
|
||||||
category: 'Billing',
|
category: 'Billing',
|
||||||
|
hint: `Ex. whsec_123456789012345678901234`,
|
||||||
secret: true,
|
secret: true,
|
||||||
|
type: 'string',
|
||||||
contextualValidation: {
|
contextualValidation: {
|
||||||
dependencies: [
|
dependencies: [
|
||||||
{
|
{
|
||||||
@@ -544,6 +607,7 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
description: 'Your Lemon Squeezy secret key.',
|
description: 'Your Lemon Squeezy secret key.',
|
||||||
category: 'Billing',
|
category: 'Billing',
|
||||||
secret: true,
|
secret: true,
|
||||||
|
type: 'string',
|
||||||
contextualValidation: {
|
contextualValidation: {
|
||||||
dependencies: [
|
dependencies: [
|
||||||
{
|
{
|
||||||
@@ -578,6 +642,7 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
name: 'LEMON_SQUEEZY_STORE_ID',
|
name: 'LEMON_SQUEEZY_STORE_ID',
|
||||||
description: 'Your Lemon Squeezy store ID.',
|
description: 'Your Lemon Squeezy store ID.',
|
||||||
category: 'Billing',
|
category: 'Billing',
|
||||||
|
type: 'string',
|
||||||
contextualValidation: {
|
contextualValidation: {
|
||||||
dependencies: [
|
dependencies: [
|
||||||
{
|
{
|
||||||
@@ -613,6 +678,7 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
description: 'Your Lemon Squeezy signing secret.',
|
description: 'Your Lemon Squeezy signing secret.',
|
||||||
category: 'Billing',
|
category: 'Billing',
|
||||||
secret: true,
|
secret: true,
|
||||||
|
type: 'string',
|
||||||
contextualValidation: {
|
contextualValidation: {
|
||||||
dependencies: [
|
dependencies: [
|
||||||
{
|
{
|
||||||
@@ -648,6 +714,8 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
description: 'Your email service provider. Options: nodemailer or resend.',
|
description: 'Your email service provider. Options: nodemailer or resend.',
|
||||||
category: 'Email',
|
category: 'Email',
|
||||||
required: true,
|
required: true,
|
||||||
|
type: 'enum',
|
||||||
|
values: ['nodemailer', 'resend'],
|
||||||
validate: ({ value }) => {
|
validate: ({ value }) => {
|
||||||
return z.enum(['nodemailer', 'resend']).safeParse(value);
|
return z.enum(['nodemailer', 'resend']).safeParse(value);
|
||||||
},
|
},
|
||||||
@@ -656,7 +724,9 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
name: 'EMAIL_SENDER',
|
name: 'EMAIL_SENDER',
|
||||||
description: 'Default sender email address.',
|
description: 'Default sender email address.',
|
||||||
category: 'Email',
|
category: 'Email',
|
||||||
|
hint: `Ex. "Makerkit <admin@makerkit.dev>"`,
|
||||||
required: true,
|
required: true,
|
||||||
|
type: 'string',
|
||||||
validate: ({ value }) => {
|
validate: ({ value }) => {
|
||||||
return z
|
return z
|
||||||
.string()
|
.string()
|
||||||
@@ -668,7 +738,9 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
name: 'CONTACT_EMAIL',
|
name: 'CONTACT_EMAIL',
|
||||||
description: 'Email address for contact form submissions.',
|
description: 'Email address for contact form submissions.',
|
||||||
category: 'Email',
|
category: 'Email',
|
||||||
|
hint: `Ex. "Makerkit <admin@makerkit.dev>"`,
|
||||||
required: true,
|
required: true,
|
||||||
|
type: 'email',
|
||||||
validate: ({ value }) => {
|
validate: ({ value }) => {
|
||||||
return z
|
return z
|
||||||
.string()
|
.string()
|
||||||
@@ -682,6 +754,7 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
description: 'Your Resend API key.',
|
description: 'Your Resend API key.',
|
||||||
category: 'Email',
|
category: 'Email',
|
||||||
secret: true,
|
secret: true,
|
||||||
|
type: 'string',
|
||||||
contextualValidation: {
|
contextualValidation: {
|
||||||
dependencies: [
|
dependencies: [
|
||||||
{
|
{
|
||||||
@@ -707,6 +780,8 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
name: 'EMAIL_HOST',
|
name: 'EMAIL_HOST',
|
||||||
description: 'SMTP host for Nodemailer configuration.',
|
description: 'SMTP host for Nodemailer configuration.',
|
||||||
category: 'Email',
|
category: 'Email',
|
||||||
|
type: 'string',
|
||||||
|
hint: `Ex. "smtp.example.com"`,
|
||||||
contextualValidation: {
|
contextualValidation: {
|
||||||
dependencies: [
|
dependencies: [
|
||||||
{
|
{
|
||||||
@@ -731,6 +806,8 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
name: 'EMAIL_PORT',
|
name: 'EMAIL_PORT',
|
||||||
description: 'SMTP port for Nodemailer configuration.',
|
description: 'SMTP port for Nodemailer configuration.',
|
||||||
category: 'Email',
|
category: 'Email',
|
||||||
|
type: 'number',
|
||||||
|
hint: `Ex. 587 or 465`,
|
||||||
contextualValidation: {
|
contextualValidation: {
|
||||||
dependencies: [
|
dependencies: [
|
||||||
{
|
{
|
||||||
@@ -756,6 +833,7 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
name: 'EMAIL_USER',
|
name: 'EMAIL_USER',
|
||||||
description: 'SMTP user for Nodemailer configuration.',
|
description: 'SMTP user for Nodemailer configuration.',
|
||||||
category: 'Email',
|
category: 'Email',
|
||||||
|
type: 'string',
|
||||||
contextualValidation: {
|
contextualValidation: {
|
||||||
dependencies: [
|
dependencies: [
|
||||||
{
|
{
|
||||||
@@ -782,6 +860,7 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
description: 'SMTP password for Nodemailer configuration.',
|
description: 'SMTP password for Nodemailer configuration.',
|
||||||
category: 'Email',
|
category: 'Email',
|
||||||
secret: true,
|
secret: true,
|
||||||
|
type: 'string',
|
||||||
contextualValidation: {
|
contextualValidation: {
|
||||||
dependencies: [
|
dependencies: [
|
||||||
{
|
{
|
||||||
@@ -804,8 +883,9 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'EMAIL_TLS',
|
name: 'EMAIL_TLS',
|
||||||
description: 'Whether to use TLS for SMTP connection.',
|
description: 'Whether to use TLS for SMTP connection. Please check this in your SMTP provider settings.',
|
||||||
category: 'Email',
|
category: 'Email',
|
||||||
|
type: 'boolean',
|
||||||
contextualValidation: {
|
contextualValidation: {
|
||||||
dependencies: [
|
dependencies: [
|
||||||
{
|
{
|
||||||
@@ -830,6 +910,8 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
name: 'CMS_CLIENT',
|
name: 'CMS_CLIENT',
|
||||||
description: 'Your chosen CMS system. Options: wordpress or keystatic.',
|
description: 'Your chosen CMS system. Options: wordpress or keystatic.',
|
||||||
category: 'CMS',
|
category: 'CMS',
|
||||||
|
type: 'enum',
|
||||||
|
values: ['wordpress', 'keystatic'],
|
||||||
validate: ({ value }) => {
|
validate: ({ value }) => {
|
||||||
return z.enum(['wordpress', 'keystatic']).optional().safeParse(value);
|
return z.enum(['wordpress', 'keystatic']).optional().safeParse(value);
|
||||||
},
|
},
|
||||||
@@ -838,6 +920,8 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
name: 'NEXT_PUBLIC_KEYSTATIC_STORAGE_KIND',
|
name: 'NEXT_PUBLIC_KEYSTATIC_STORAGE_KIND',
|
||||||
description: 'Your Keystatic storage kind. Options: local, cloud, github.',
|
description: 'Your Keystatic storage kind. Options: local, cloud, github.',
|
||||||
category: 'CMS',
|
category: 'CMS',
|
||||||
|
type: 'enum',
|
||||||
|
values: ['local', 'cloud', 'github'],
|
||||||
contextualValidation: {
|
contextualValidation: {
|
||||||
dependencies: [
|
dependencies: [
|
||||||
{
|
{
|
||||||
@@ -862,6 +946,7 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
name: 'NEXT_PUBLIC_KEYSTATIC_STORAGE_REPO',
|
name: 'NEXT_PUBLIC_KEYSTATIC_STORAGE_REPO',
|
||||||
description: 'Your Keystatic storage repo.',
|
description: 'Your Keystatic storage repo.',
|
||||||
category: 'CMS',
|
category: 'CMS',
|
||||||
|
type: 'string',
|
||||||
contextualValidation: {
|
contextualValidation: {
|
||||||
dependencies: [
|
dependencies: [
|
||||||
{
|
{
|
||||||
@@ -889,6 +974,7 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
description: 'Your Keystatic GitHub token.',
|
description: 'Your Keystatic GitHub token.',
|
||||||
category: 'CMS',
|
category: 'CMS',
|
||||||
secret: true,
|
secret: true,
|
||||||
|
type: 'string',
|
||||||
contextualValidation: {
|
contextualValidation: {
|
||||||
dependencies: [
|
dependencies: [
|
||||||
{
|
{
|
||||||
@@ -915,6 +1001,7 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
name: 'KEYSTATIC_PATH_PREFIX',
|
name: 'KEYSTATIC_PATH_PREFIX',
|
||||||
description: 'Your Keystatic path prefix.',
|
description: 'Your Keystatic path prefix.',
|
||||||
category: 'CMS',
|
category: 'CMS',
|
||||||
|
type: 'string',
|
||||||
contextualValidation: {
|
contextualValidation: {
|
||||||
dependencies: [
|
dependencies: [
|
||||||
{
|
{
|
||||||
@@ -933,6 +1020,7 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
name: 'NEXT_PUBLIC_KEYSTATIC_CONTENT_PATH',
|
name: 'NEXT_PUBLIC_KEYSTATIC_CONTENT_PATH',
|
||||||
description: 'Your Keystatic content path.',
|
description: 'Your Keystatic content path.',
|
||||||
category: 'CMS',
|
category: 'CMS',
|
||||||
|
type: 'string',
|
||||||
contextualValidation: {
|
contextualValidation: {
|
||||||
dependencies: [
|
dependencies: [
|
||||||
{
|
{
|
||||||
@@ -958,6 +1046,7 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
name: 'WORDPRESS_API_URL',
|
name: 'WORDPRESS_API_URL',
|
||||||
description: 'WordPress API URL when using WordPress as CMS.',
|
description: 'WordPress API URL when using WordPress as CMS.',
|
||||||
category: 'CMS',
|
category: 'CMS',
|
||||||
|
type: 'string',
|
||||||
contextualValidation: {
|
contextualValidation: {
|
||||||
dependencies: [
|
dependencies: [
|
||||||
{
|
{
|
||||||
@@ -981,6 +1070,7 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
name: 'NEXT_PUBLIC_LOCALES_PATH',
|
name: 'NEXT_PUBLIC_LOCALES_PATH',
|
||||||
description: 'The path to your locales folder.',
|
description: 'The path to your locales folder.',
|
||||||
category: 'Localization',
|
category: 'Localization',
|
||||||
|
type: 'string',
|
||||||
validate: ({ value }) => {
|
validate: ({ value }) => {
|
||||||
return z
|
return z
|
||||||
.string()
|
.string()
|
||||||
@@ -996,6 +1086,8 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
name: 'NEXT_PUBLIC_LANGUAGE_PRIORITY',
|
name: 'NEXT_PUBLIC_LANGUAGE_PRIORITY',
|
||||||
description: 'The priority setting as to how infer the language.',
|
description: 'The priority setting as to how infer the language.',
|
||||||
category: 'Localization',
|
category: 'Localization',
|
||||||
|
type: 'enum',
|
||||||
|
values: ['user', 'application'],
|
||||||
validate: ({ value }) => {
|
validate: ({ value }) => {
|
||||||
return z.enum(['user', 'application']).optional().safeParse(value);
|
return z.enum(['user', 'application']).optional().safeParse(value);
|
||||||
},
|
},
|
||||||
@@ -1005,6 +1097,7 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
description:
|
description:
|
||||||
'Enables the version updater to poll the latest version and notify the user.',
|
'Enables the version updater to poll the latest version and notify the user.',
|
||||||
category: 'Features',
|
category: 'Features',
|
||||||
|
type: 'boolean',
|
||||||
validate: ({ value }) => {
|
validate: ({ value }) => {
|
||||||
return z.coerce.boolean().optional().safeParse(value);
|
return z.coerce.boolean().optional().safeParse(value);
|
||||||
},
|
},
|
||||||
@@ -1013,6 +1106,7 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
name: 'NEXT_PUBLIC_VERSION_UPDATER_REFETCH_INTERVAL_SECONDS',
|
name: 'NEXT_PUBLIC_VERSION_UPDATER_REFETCH_INTERVAL_SECONDS',
|
||||||
description: 'The interval in seconds to check for updates.',
|
description: 'The interval in seconds to check for updates.',
|
||||||
category: 'Features',
|
category: 'Features',
|
||||||
|
type: 'number',
|
||||||
validate: ({ value }) => {
|
validate: ({ value }) => {
|
||||||
return z.coerce
|
return z.coerce
|
||||||
.number()
|
.number()
|
||||||
@@ -1032,6 +1126,7 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
name: `ENABLE_REACT_COMPILER`,
|
name: `ENABLE_REACT_COMPILER`,
|
||||||
description: 'Enables the React compiler [experimental]',
|
description: 'Enables the React compiler [experimental]',
|
||||||
category: 'Build',
|
category: 'Build',
|
||||||
|
type: 'boolean',
|
||||||
validate: ({ value }) => {
|
validate: ({ value }) => {
|
||||||
return z.coerce.boolean().optional().safeParse(value);
|
return z.coerce.boolean().optional().safeParse(value);
|
||||||
},
|
},
|
||||||
@@ -1040,7 +1135,8 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
name: 'NEXT_PUBLIC_MONITORING_PROVIDER',
|
name: 'NEXT_PUBLIC_MONITORING_PROVIDER',
|
||||||
description: 'The monitoring provider to use.',
|
description: 'The monitoring provider to use.',
|
||||||
category: 'Monitoring',
|
category: 'Monitoring',
|
||||||
required: true,
|
type: 'enum',
|
||||||
|
values: ['baselime', 'sentry', 'none'],
|
||||||
validate: ({ value }) => {
|
validate: ({ value }) => {
|
||||||
return z.enum(['baselime', 'sentry', '']).optional().safeParse(value);
|
return z.enum(['baselime', 'sentry', '']).optional().safeParse(value);
|
||||||
},
|
},
|
||||||
@@ -1049,6 +1145,7 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
name: 'NEXT_PUBLIC_SENTRY_DSN',
|
name: 'NEXT_PUBLIC_SENTRY_DSN',
|
||||||
description: 'The Sentry DSN to use.',
|
description: 'The Sentry DSN to use.',
|
||||||
category: 'Monitoring',
|
category: 'Monitoring',
|
||||||
|
type: `string`,
|
||||||
contextualValidation: {
|
contextualValidation: {
|
||||||
dependencies: [
|
dependencies: [
|
||||||
{
|
{
|
||||||
@@ -1073,7 +1170,7 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
name: 'NEXT_PUBLIC_SENTRY_ENVIRONMENT',
|
name: 'NEXT_PUBLIC_SENTRY_ENVIRONMENT',
|
||||||
description: 'The Sentry environment to use.',
|
description: 'The Sentry environment to use.',
|
||||||
category: 'Monitoring',
|
category: 'Monitoring',
|
||||||
required: true,
|
type: 'string',
|
||||||
validate: ({ value }) => {
|
validate: ({ value }) => {
|
||||||
return z.string().optional().safeParse(value);
|
return z.string().optional().safeParse(value);
|
||||||
},
|
},
|
||||||
@@ -1082,6 +1179,7 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
name: 'NEXT_PUBLIC_BASELIME_KEY',
|
name: 'NEXT_PUBLIC_BASELIME_KEY',
|
||||||
description: 'The Baselime key to use.',
|
description: 'The Baselime key to use.',
|
||||||
category: 'Monitoring',
|
category: 'Monitoring',
|
||||||
|
type: 'string',
|
||||||
contextualValidation: {
|
contextualValidation: {
|
||||||
dependencies: [
|
dependencies: [
|
||||||
{
|
{
|
||||||
@@ -1107,6 +1205,7 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
name: 'STRIPE_ENABLE_TRIAL_WITHOUT_CC',
|
name: 'STRIPE_ENABLE_TRIAL_WITHOUT_CC',
|
||||||
description: 'Enables trial plans without credit card.',
|
description: 'Enables trial plans without credit card.',
|
||||||
category: 'Billing',
|
category: 'Billing',
|
||||||
|
type: 'boolean',
|
||||||
validate: ({ value }) => {
|
validate: ({ value }) => {
|
||||||
return z.coerce.boolean().optional().safeParse(value);
|
return z.coerce.boolean().optional().safeParse(value);
|
||||||
},
|
},
|
||||||
@@ -1115,6 +1214,7 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
name: 'NEXT_PUBLIC_THEME_COLOR',
|
name: 'NEXT_PUBLIC_THEME_COLOR',
|
||||||
description: 'The default theme color.',
|
description: 'The default theme color.',
|
||||||
category: 'Theme',
|
category: 'Theme',
|
||||||
|
type: 'string',
|
||||||
required: true,
|
required: true,
|
||||||
validate: ({ value }) => {
|
validate: ({ value }) => {
|
||||||
return z
|
return z
|
||||||
@@ -1132,6 +1232,7 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
description: 'The default theme color for dark mode.',
|
description: 'The default theme color for dark mode.',
|
||||||
category: 'Theme',
|
category: 'Theme',
|
||||||
required: true,
|
required: true,
|
||||||
|
type: 'string',
|
||||||
validate: ({ value }) => {
|
validate: ({ value }) => {
|
||||||
return z
|
return z
|
||||||
.string()
|
.string()
|
||||||
@@ -1147,6 +1248,17 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
name: 'NEXT_PUBLIC_DISPLAY_TERMS_AND_CONDITIONS_CHECKBOX',
|
name: 'NEXT_PUBLIC_DISPLAY_TERMS_AND_CONDITIONS_CHECKBOX',
|
||||||
description: 'Whether to display the terms checkbox during sign-up.',
|
description: 'Whether to display the terms checkbox during sign-up.',
|
||||||
category: 'Features',
|
category: 'Features',
|
||||||
|
type: 'boolean',
|
||||||
|
validate: ({ value }) => {
|
||||||
|
return z.coerce.boolean().optional().safeParse(value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ENABLE_STRICT_CSP',
|
||||||
|
description: 'Enables strict Content Security Policy (CSP) headers.',
|
||||||
|
category: 'Security',
|
||||||
|
required: false,
|
||||||
|
type: 'boolean',
|
||||||
validate: ({ value }) => {
|
validate: ({ value }) => {
|
||||||
return z.coerce.boolean().optional().safeParse(value);
|
return z.coerce.boolean().optional().safeParse(value);
|
||||||
},
|
},
|
||||||
|
|||||||
84
apps/dev-tool/app/variables/lib/server-actions.ts
Normal file
84
apps/dev-tool/app/variables/lib/server-actions.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
import { revalidatePath } from 'next/cache';
|
||||||
|
|
||||||
|
import { envVariables } from '@/app/variables/lib/env-variables-model';
|
||||||
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
||||||
|
import { resolve } from 'node:url';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const Schema = z.object({
|
||||||
|
name: z.string().min(1),
|
||||||
|
value: z.string(),
|
||||||
|
mode: z.enum(['development', 'production']),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the environment variable in the specified file.
|
||||||
|
* @param props
|
||||||
|
*/
|
||||||
|
export async function updateEnvironmentVariableAction(
|
||||||
|
props: z.infer<typeof Schema>,
|
||||||
|
) {
|
||||||
|
// Validate the input
|
||||||
|
const { name, mode, value } = Schema.parse(props);
|
||||||
|
const root = resolve(process.cwd(), '..');
|
||||||
|
const model = envVariables.find((item) => item.name === name);
|
||||||
|
|
||||||
|
// Determine the source file based on the mode
|
||||||
|
const source = (() => {
|
||||||
|
const isSecret = model?.secret ?? true;
|
||||||
|
|
||||||
|
switch (mode) {
|
||||||
|
case 'development':
|
||||||
|
if (isSecret) {
|
||||||
|
return '.env.local';
|
||||||
|
} else {
|
||||||
|
return '.env.development';
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'production':
|
||||||
|
if (isSecret) {
|
||||||
|
return '.env.production.local';
|
||||||
|
} else {
|
||||||
|
return '.env.production';
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Invalid mode: ${mode}`);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// check file exists, if not, create it
|
||||||
|
const filePath = `${root}/apps/web/${source}`;
|
||||||
|
|
||||||
|
if (!existsSync(filePath)) {
|
||||||
|
writeFileSync(filePath, '', 'utf-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceEnvFile = readFileSync(`${root}apps/web/${source}`, 'utf-8');
|
||||||
|
|
||||||
|
let updatedEnvFile = '';
|
||||||
|
const isInSourceFile = sourceEnvFile.includes(name);
|
||||||
|
const isCommentedOut = sourceEnvFile.includes(`#${name}=`);
|
||||||
|
|
||||||
|
if (isInSourceFile && !isCommentedOut) {
|
||||||
|
updatedEnvFile = sourceEnvFile.replace(
|
||||||
|
new RegExp(`^${name}=.*`, 'm'),
|
||||||
|
`${name}=${value}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// if the key does not exist, append it to the end of the file
|
||||||
|
updatedEnvFile = `${sourceEnvFile}\n${name}=${value}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// write the updated content back to the file
|
||||||
|
writeFileSync(`${root}/apps/web/${source}`, updatedEnvFile, 'utf-8');
|
||||||
|
|
||||||
|
revalidatePath(`/variables`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `Updated ${name} in "${source}"`,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -19,6 +19,13 @@ export type EnvVariableState = {
|
|||||||
effectiveValue: string;
|
effectiveValue: string;
|
||||||
isOverridden: boolean;
|
isOverridden: boolean;
|
||||||
effectiveSource: string;
|
effectiveSource: string;
|
||||||
|
isVisible: boolean;
|
||||||
|
validation: {
|
||||||
|
success: boolean;
|
||||||
|
error: {
|
||||||
|
issues: string[];
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AppEnvState = {
|
export type AppEnvState = {
|
||||||
|
|||||||
@@ -25,19 +25,17 @@ export default function VariablesPage({ searchParams }: VariablesPageProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Page style={'custom'}>
|
<Page style={'custom'}>
|
||||||
|
<div className={'flex h-screen flex-col overflow-hidden'}>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
displaySidebarTrigger={false}
|
displaySidebarTrigger={false}
|
||||||
|
title={'Environment Variables'}
|
||||||
description={
|
description={
|
||||||
<AppBreadcrumbs
|
'Manage environment variables for your applications. Validate and set them up easily.'
|
||||||
values={{
|
|
||||||
variables: 'Environment Variables',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PageBody>
|
<PageBody className={'overflow-hidden'}>
|
||||||
<div className={'flex flex-col space-y-4 pb-16'}>
|
<div className={'flex h-full flex-1 flex-col space-y-4'}>
|
||||||
{apps.map((app) => {
|
{apps.map((app) => {
|
||||||
const appEnvState = processEnvDefinitions(app, mode);
|
const appEnvState = processEnvDefinitions(app, mode);
|
||||||
|
|
||||||
@@ -50,6 +48,7 @@ export default function VariablesPage({ searchParams }: VariablesPageProps) {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</PageBody>
|
</PageBody>
|
||||||
|
</div>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,13 +8,16 @@
|
|||||||
"format": "prettier --check --write \"**/*.{js,cjs,mjs,ts,tsx,md,json}\""
|
"format": "prettier --check --write \"**/*.{js,cjs,mjs,ts,tsx,md,json}\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@ai-sdk/openai": "^1.3.20",
|
||||||
"@hookform/resolvers": "^5.0.1",
|
"@hookform/resolvers": "^5.0.1",
|
||||||
"@tanstack/react-query": "5.74.4",
|
"@tanstack/react-query": "5.74.4",
|
||||||
|
"ai": "4.3.10",
|
||||||
"lucide-react": "^0.503.0",
|
"lucide-react": "^0.503.0",
|
||||||
"next": "15.3.1",
|
"next": "15.3.1",
|
||||||
"nodemailer": "^6.10.1",
|
"nodemailer": "^6.10.1",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0"
|
"react-dom": "19.1.0",
|
||||||
|
"rxjs": "^7.8.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@kit/email-templates": "workspace:*",
|
"@kit/email-templates": "workspace:*",
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const defaultLanguage = process.env.NEXT_PUBLIC_DEFAULT_LOCALE ?? 'en';
|
|||||||
* By default, only the default language is supported.
|
* By default, only the default language is supported.
|
||||||
* Add more languages here if needed.
|
* Add more languages here if needed.
|
||||||
*/
|
*/
|
||||||
export const languages: string[] = [defaultLanguage];
|
export const languages: string[] = [defaultLanguage, 'it'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The name of the cookie that stores the selected language.
|
* The name of the cookie that stores the selected language.
|
||||||
|
|||||||
@@ -59,6 +59,7 @@
|
|||||||
"@supabase/supabase-js": "2.49.4",
|
"@supabase/supabase-js": "2.49.4",
|
||||||
"@tanstack/react-query": "5.74.4",
|
"@tanstack/react-query": "5.74.4",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
|
"ai": "4.3.10",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"lucide-react": "^0.503.0",
|
"lucide-react": "^0.503.0",
|
||||||
"next": "15.3.1",
|
"next": "15.3.1",
|
||||||
|
|||||||
@@ -88,7 +88,7 @@
|
|||||||
"errorSendingCode": "Error sending code. Please try again."
|
"errorSendingCode": "Error sending code. Please try again."
|
||||||
},
|
},
|
||||||
"cookieBanner": {
|
"cookieBanner": {
|
||||||
"title": "Hey, we use cookies \uD83C\uDF6A",
|
"title": "Hey, we use cookies 🍪",
|
||||||
"description": "This website uses cookies to ensure you get the best experience on our website.",
|
"description": "This website uses cookies to ensure you get the best experience on our website.",
|
||||||
"reject": "Reject",
|
"reject": "Reject",
|
||||||
"accept": "Accept"
|
"accept": "Accept"
|
||||||
|
|||||||
@@ -5,22 +5,17 @@ import { type VariantProps, cva } from 'class-variance-authority';
|
|||||||
import { cn } from '../lib/utils';
|
import { cn } from '../lib/utils';
|
||||||
|
|
||||||
const badgeVariants = cva(
|
const badgeVariants = cva(
|
||||||
'focus:ring-ring inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:ring-2 focus:ring-offset-2 focus:outline-hidden',
|
'focus:ring-ring inline-flex items-center rounded-md border px-1.5 py-0.5 text-xs font-semibold transition-colors focus:ring-2 focus:ring-offset-2 focus:outline-hidden',
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default:
|
default: 'bg-primary text-primary-foreground border-transparent',
|
||||||
'bg-primary text-primary-foreground hover:bg-primary/80 border-transparent shadow-xs',
|
secondary: 'bg-secondary text-secondary-foreground border-transparent',
|
||||||
secondary:
|
destructive: 'text-destructive border-destructive',
|
||||||
'bg-secondary text-secondary-foreground hover:bg-secondary/80 border-transparent',
|
|
||||||
destructive:
|
|
||||||
'bg-destructive text-destructive-foreground hover:bg-destructive/80 border-transparent shadow-xs',
|
|
||||||
outline: 'text-foreground',
|
outline: 'text-foreground',
|
||||||
success:
|
success: 'border-green-500 text-green-500',
|
||||||
'border-transparent bg-green-50 text-green-500 hover:bg-green-50 dark:bg-green-500/20 dark:hover:bg-green-500/20',
|
warning: 'border-orange-500 text-orange-500',
|
||||||
warning:
|
info: 'border-blue-500 text-blue-500',
|
||||||
'border-transparent bg-orange-50 text-orange-500 hover:bg-orange-50 dark:bg-orange-500/20 dark:hover:bg-orange-500/20',
|
|
||||||
info: 'border-transparent bg-blue-50 text-blue-500 hover:bg-blue-50 dark:bg-blue-500/20 dark:hover:bg-blue-500/20',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
|
|||||||
307
pnpm-lock.yaml
generated
307
pnpm-lock.yaml
generated
@@ -33,12 +33,18 @@ importers:
|
|||||||
|
|
||||||
apps/dev-tool:
|
apps/dev-tool:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@ai-sdk/openai':
|
||||||
|
specifier: ^1.3.20
|
||||||
|
version: 1.3.20(zod@3.24.3)
|
||||||
'@hookform/resolvers':
|
'@hookform/resolvers':
|
||||||
specifier: ^5.0.1
|
specifier: ^5.0.1
|
||||||
version: 5.0.1(react-hook-form@7.56.1(react@19.1.0))
|
version: 5.0.1(react-hook-form@7.56.1(react@19.1.0))
|
||||||
'@tanstack/react-query':
|
'@tanstack/react-query':
|
||||||
specifier: 5.74.4
|
specifier: 5.74.4
|
||||||
version: 5.74.4(react@19.1.0)
|
version: 5.74.4(react@19.1.0)
|
||||||
|
ai:
|
||||||
|
specifier: 4.3.10
|
||||||
|
version: 4.3.10(react@19.1.0)(zod@3.24.3)
|
||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: ^0.503.0
|
specifier: ^0.503.0
|
||||||
version: 0.503.0(react@19.1.0)
|
version: 0.503.0(react@19.1.0)
|
||||||
@@ -54,6 +60,9 @@ importers:
|
|||||||
react-dom:
|
react-dom:
|
||||||
specifier: 19.1.0
|
specifier: 19.1.0
|
||||||
version: 19.1.0(react@19.1.0)
|
version: 19.1.0(react@19.1.0)
|
||||||
|
rxjs:
|
||||||
|
specifier: ^7.8.2
|
||||||
|
version: 7.8.2
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@kit/email-templates':
|
'@kit/email-templates':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
@@ -211,6 +220,9 @@ importers:
|
|||||||
'@tanstack/react-table':
|
'@tanstack/react-table':
|
||||||
specifier: ^8.21.3
|
specifier: ^8.21.3
|
||||||
version: 8.21.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
version: 8.21.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
ai:
|
||||||
|
specifier: 4.3.10
|
||||||
|
version: 4.3.10(react@19.1.0)(zod@3.24.3)
|
||||||
date-fns:
|
date-fns:
|
||||||
specifier: ^4.1.0
|
specifier: ^4.1.0
|
||||||
version: 4.1.0
|
version: 4.1.0
|
||||||
@@ -1559,13 +1571,13 @@ importers:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@trivago/prettier-plugin-sort-imports':
|
'@trivago/prettier-plugin-sort-imports':
|
||||||
specifier: 5.2.2
|
specifier: 5.2.2
|
||||||
version: 5.2.2(prettier@3.5.3)
|
version: 5.2.2(@vue/compiler-sfc@3.5.13)(prettier@3.5.3)(svelte@4.2.19)
|
||||||
prettier:
|
prettier:
|
||||||
specifier: ^3.5.3
|
specifier: ^3.5.3
|
||||||
version: 3.5.3
|
version: 3.5.3
|
||||||
prettier-plugin-tailwindcss:
|
prettier-plugin-tailwindcss:
|
||||||
specifier: ^0.6.11
|
specifier: ^0.6.11
|
||||||
version: 0.6.11(@trivago/prettier-plugin-sort-imports@5.2.2(prettier@3.5.3))(prettier@3.5.3)
|
version: 0.6.11(@trivago/prettier-plugin-sort-imports@5.2.2(@vue/compiler-sfc@3.5.13)(prettier@3.5.3)(svelte@4.2.19))(prettier@3.5.3)
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@kit/tsconfig':
|
'@kit/tsconfig':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
@@ -1588,6 +1600,38 @@ packages:
|
|||||||
graphql:
|
graphql:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@ai-sdk/openai@1.3.20':
|
||||||
|
resolution: {integrity: sha512-/DflUy7ROG9k6n6YTXMBFPbujBKnbGY58f3CwvicLvDar9nDAloVnUWd3LUoOxpSVnX8vtQ7ngxF52SLWO6RwQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
peerDependencies:
|
||||||
|
zod: ^3.0.0
|
||||||
|
|
||||||
|
'@ai-sdk/provider-utils@2.2.7':
|
||||||
|
resolution: {integrity: sha512-kM0xS3GWg3aMChh9zfeM+80vEZfXzR3JEUBdycZLtbRZ2TRT8xOj3WodGHPb06sUK5yD7pAXC/P7ctsi2fvUGQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
peerDependencies:
|
||||||
|
zod: ^3.23.8
|
||||||
|
|
||||||
|
'@ai-sdk/provider@1.1.3':
|
||||||
|
resolution: {integrity: sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
'@ai-sdk/react@1.2.9':
|
||||||
|
resolution: {integrity: sha512-/VYm8xifyngaqFDLXACk/1czDRCefNCdALUyp+kIX6DUIYUWTM93ISoZ+qJ8+3E+FiJAKBQz61o8lIIl+vYtzg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^18 || ^19 || ^19.0.0-rc
|
||||||
|
zod: ^3.23.8
|
||||||
|
peerDependenciesMeta:
|
||||||
|
zod:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@ai-sdk/ui-utils@1.2.8':
|
||||||
|
resolution: {integrity: sha512-nls/IJCY+ks3Uj6G/agNhXqQeLVqhNfoJbuNgCny+nX2veY5ADB91EcZUqVeQ/ionul2SeUswPY6Q/DxteY29Q==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
peerDependencies:
|
||||||
|
zod: ^3.23.8
|
||||||
|
|
||||||
'@alloc/quick-lru@5.2.0':
|
'@alloc/quick-lru@5.2.0':
|
||||||
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
|
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -4325,6 +4369,9 @@ packages:
|
|||||||
'@types/debug@4.1.12':
|
'@types/debug@4.1.12':
|
||||||
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
|
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
|
||||||
|
|
||||||
|
'@types/diff-match-patch@1.0.36':
|
||||||
|
resolution: {integrity: sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==}
|
||||||
|
|
||||||
'@types/eslint-scope@3.7.7':
|
'@types/eslint-scope@3.7.7':
|
||||||
resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==}
|
resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==}
|
||||||
|
|
||||||
@@ -4621,6 +4668,21 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@urql/core': ^5.0.0
|
'@urql/core': ^5.0.0
|
||||||
|
|
||||||
|
'@vue/compiler-core@3.5.13':
|
||||||
|
resolution: {integrity: sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==}
|
||||||
|
|
||||||
|
'@vue/compiler-dom@3.5.13':
|
||||||
|
resolution: {integrity: sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==}
|
||||||
|
|
||||||
|
'@vue/compiler-sfc@3.5.13':
|
||||||
|
resolution: {integrity: sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==}
|
||||||
|
|
||||||
|
'@vue/compiler-ssr@3.5.13':
|
||||||
|
resolution: {integrity: sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==}
|
||||||
|
|
||||||
|
'@vue/shared@3.5.13':
|
||||||
|
resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==}
|
||||||
|
|
||||||
'@webassemblyjs/ast@1.14.1':
|
'@webassemblyjs/ast@1.14.1':
|
||||||
resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==}
|
resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==}
|
||||||
|
|
||||||
@@ -4709,6 +4771,16 @@ packages:
|
|||||||
resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==}
|
resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
ai@4.3.10:
|
||||||
|
resolution: {integrity: sha512-jw+ahNu+T4SHj9gtraIKtYhanJI6gj2IZ5BFcfEHgoyQVMln5a5beGjzl/nQSX6FxyLqJ/UBpClRa279EEKK/Q==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^18 || ^19 || ^19.0.0-rc
|
||||||
|
zod: ^3.23.8
|
||||||
|
peerDependenciesMeta:
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
|
||||||
ajv-formats@2.1.1:
|
ajv-formats@2.1.1:
|
||||||
resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==}
|
resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -4930,6 +5002,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
chalk@5.4.1:
|
||||||
|
resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==}
|
||||||
|
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
|
||||||
|
|
||||||
change-case@3.1.0:
|
change-case@3.1.0:
|
||||||
resolution: {integrity: sha512-2AZp7uJZbYEzRPsFoa+ijKdvp9zsrnnt6+yFokfwEpeJm0xuJDVoxiRCAaTzyJND8GJkofo2IcKWaUZ/OECVzw==}
|
resolution: {integrity: sha512-2AZp7uJZbYEzRPsFoa+ijKdvp9zsrnnt6+yFokfwEpeJm0xuJDVoxiRCAaTzyJND8GJkofo2IcKWaUZ/OECVzw==}
|
||||||
|
|
||||||
@@ -5007,6 +5083,9 @@ packages:
|
|||||||
react: ^18 || ^19 || ^19.0.0-rc
|
react: ^18 || ^19 || ^19.0.0-rc
|
||||||
react-dom: ^18 || ^19 || ^19.0.0-rc
|
react-dom: ^18 || ^19 || ^19.0.0-rc
|
||||||
|
|
||||||
|
code-red@1.0.4:
|
||||||
|
resolution: {integrity: sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==}
|
||||||
|
|
||||||
color-convert@1.9.3:
|
color-convert@1.9.3:
|
||||||
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
|
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
|
||||||
|
|
||||||
@@ -5300,6 +5379,9 @@ packages:
|
|||||||
devlop@1.1.0:
|
devlop@1.1.0:
|
||||||
resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
|
resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
|
||||||
|
|
||||||
|
diff-match-patch@1.0.5:
|
||||||
|
resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==}
|
||||||
|
|
||||||
diff@4.0.2:
|
diff@4.0.2:
|
||||||
resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==}
|
resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==}
|
||||||
engines: {node: '>=0.3.1'}
|
engines: {node: '>=0.3.1'}
|
||||||
@@ -5582,6 +5664,9 @@ packages:
|
|||||||
estree-walker@2.0.2:
|
estree-walker@2.0.2:
|
||||||
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
|
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
|
||||||
|
|
||||||
|
estree-walker@3.0.3:
|
||||||
|
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
|
||||||
|
|
||||||
esutils@2.0.3:
|
esutils@2.0.3:
|
||||||
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
|
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -6129,6 +6214,9 @@ packages:
|
|||||||
is-reference@1.2.1:
|
is-reference@1.2.1:
|
||||||
resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==}
|
resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==}
|
||||||
|
|
||||||
|
is-reference@3.0.3:
|
||||||
|
resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==}
|
||||||
|
|
||||||
is-regex@1.2.1:
|
is-regex@1.2.1:
|
||||||
resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==}
|
resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -6242,6 +6330,9 @@ packages:
|
|||||||
json-schema-traverse@1.0.0:
|
json-schema-traverse@1.0.0:
|
||||||
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
|
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
|
||||||
|
|
||||||
|
json-schema@0.4.0:
|
||||||
|
resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==}
|
||||||
|
|
||||||
json-stable-stringify-without-jsonify@1.0.1:
|
json-stable-stringify-without-jsonify@1.0.1:
|
||||||
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
|
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
|
||||||
|
|
||||||
@@ -6254,6 +6345,11 @@ packages:
|
|||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
jsondiffpatch@0.6.0:
|
||||||
|
resolution: {integrity: sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==}
|
||||||
|
engines: {node: ^18.0.0 || >=20.0.0}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
jsonfile@6.1.0:
|
jsonfile@6.1.0:
|
||||||
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
|
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
|
||||||
|
|
||||||
@@ -6365,6 +6461,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==}
|
resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==}
|
||||||
engines: {node: '>=6.11.5'}
|
engines: {node: '>=6.11.5'}
|
||||||
|
|
||||||
|
locate-character@3.0.0:
|
||||||
|
resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==}
|
||||||
|
|
||||||
locate-path@6.0.0:
|
locate-path@6.0.0:
|
||||||
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
|
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -6938,6 +7037,9 @@ packages:
|
|||||||
peberminta@0.9.0:
|
peberminta@0.9.0:
|
||||||
resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==}
|
resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==}
|
||||||
|
|
||||||
|
periscopic@3.1.0:
|
||||||
|
resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==}
|
||||||
|
|
||||||
pg-int8@1.0.1:
|
pg-int8@1.0.1:
|
||||||
resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==}
|
resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==}
|
||||||
engines: {node: '>=4.0.0'}
|
engines: {node: '>=4.0.0'}
|
||||||
@@ -7851,6 +7953,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
svelte@4.2.19:
|
||||||
|
resolution: {integrity: sha512-IY1rnGr6izd10B0A8LqsBfmlT5OILVuZ7XsI0vdGPEvuonFV7NYEUK4dAkm9Zg2q0Um92kYjTpS1CAP3Nh/KWw==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
|
||||||
svgo@3.3.2:
|
svgo@3.3.2:
|
||||||
resolution: {integrity: sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==}
|
resolution: {integrity: sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==}
|
||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
@@ -7859,6 +7965,11 @@ packages:
|
|||||||
swap-case@1.1.2:
|
swap-case@1.1.2:
|
||||||
resolution: {integrity: sha512-BAmWG6/bx8syfc6qXPprof3Mn5vQgf5dwdUNJhsNqU9WdPt5P+ES/wQ5bxfijy8zwZgZZHslC3iAsxsuQMCzJQ==}
|
resolution: {integrity: sha512-BAmWG6/bx8syfc6qXPprof3Mn5vQgf5dwdUNJhsNqU9WdPt5P+ES/wQ5bxfijy8zwZgZZHslC3iAsxsuQMCzJQ==}
|
||||||
|
|
||||||
|
swr@2.3.3:
|
||||||
|
resolution: {integrity: sha512-dshNvs3ExOqtZ6kJBaAsabhPdHyeY4P2cKwRCniDVifBMoG/SVI7tfLWqPXriVspf2Rg4tPzXJTnwaihIeFw2A==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
|
||||||
tabbable@6.2.0:
|
tabbable@6.2.0:
|
||||||
resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==}
|
resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==}
|
||||||
|
|
||||||
@@ -7905,6 +8016,10 @@ packages:
|
|||||||
thread-stream@3.1.0:
|
thread-stream@3.1.0:
|
||||||
resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==}
|
resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==}
|
||||||
|
|
||||||
|
throttleit@2.1.0:
|
||||||
|
resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
through@2.3.8:
|
through@2.3.8:
|
||||||
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
|
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
|
||||||
|
|
||||||
@@ -8349,6 +8464,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
zod-to-json-schema@3.24.5:
|
||||||
|
resolution: {integrity: sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==}
|
||||||
|
peerDependencies:
|
||||||
|
zod: ^3.24.1
|
||||||
|
|
||||||
zod@3.24.3:
|
zod@3.24.3:
|
||||||
resolution: {integrity: sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==}
|
resolution: {integrity: sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==}
|
||||||
|
|
||||||
@@ -8361,6 +8481,40 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
graphql: 16.10.0
|
graphql: 16.10.0
|
||||||
|
|
||||||
|
'@ai-sdk/openai@1.3.20(zod@3.24.3)':
|
||||||
|
dependencies:
|
||||||
|
'@ai-sdk/provider': 1.1.3
|
||||||
|
'@ai-sdk/provider-utils': 2.2.7(zod@3.24.3)
|
||||||
|
zod: 3.24.3
|
||||||
|
|
||||||
|
'@ai-sdk/provider-utils@2.2.7(zod@3.24.3)':
|
||||||
|
dependencies:
|
||||||
|
'@ai-sdk/provider': 1.1.3
|
||||||
|
nanoid: 3.3.11
|
||||||
|
secure-json-parse: 2.7.0
|
||||||
|
zod: 3.24.3
|
||||||
|
|
||||||
|
'@ai-sdk/provider@1.1.3':
|
||||||
|
dependencies:
|
||||||
|
json-schema: 0.4.0
|
||||||
|
|
||||||
|
'@ai-sdk/react@1.2.9(react@19.1.0)(zod@3.24.3)':
|
||||||
|
dependencies:
|
||||||
|
'@ai-sdk/provider-utils': 2.2.7(zod@3.24.3)
|
||||||
|
'@ai-sdk/ui-utils': 1.2.8(zod@3.24.3)
|
||||||
|
react: 19.1.0
|
||||||
|
swr: 2.3.3(react@19.1.0)
|
||||||
|
throttleit: 2.1.0
|
||||||
|
optionalDependencies:
|
||||||
|
zod: 3.24.3
|
||||||
|
|
||||||
|
'@ai-sdk/ui-utils@1.2.8(zod@3.24.3)':
|
||||||
|
dependencies:
|
||||||
|
'@ai-sdk/provider': 1.1.3
|
||||||
|
'@ai-sdk/provider-utils': 2.2.7(zod@3.24.3)
|
||||||
|
zod: 3.24.3
|
||||||
|
zod-to-json-schema: 3.24.5(zod@3.24.3)
|
||||||
|
|
||||||
'@alloc/quick-lru@5.2.0': {}
|
'@alloc/quick-lru@5.2.0': {}
|
||||||
|
|
||||||
'@ampproject/remapping@2.3.0':
|
'@ampproject/remapping@2.3.0':
|
||||||
@@ -11741,7 +11895,7 @@ snapshots:
|
|||||||
|
|
||||||
'@tootallnate/quickjs-emscripten@0.23.0': {}
|
'@tootallnate/quickjs-emscripten@0.23.0': {}
|
||||||
|
|
||||||
'@trivago/prettier-plugin-sort-imports@5.2.2(prettier@3.5.3)':
|
'@trivago/prettier-plugin-sort-imports@5.2.2(@vue/compiler-sfc@3.5.13)(prettier@3.5.3)(svelte@4.2.19)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/generator': 7.27.0
|
'@babel/generator': 7.27.0
|
||||||
'@babel/parser': 7.27.0
|
'@babel/parser': 7.27.0
|
||||||
@@ -11750,6 +11904,9 @@ snapshots:
|
|||||||
javascript-natural-sort: 0.7.1
|
javascript-natural-sort: 0.7.1
|
||||||
lodash: 4.17.21
|
lodash: 4.17.21
|
||||||
prettier: 3.5.3
|
prettier: 3.5.3
|
||||||
|
optionalDependencies:
|
||||||
|
'@vue/compiler-sfc': 3.5.13
|
||||||
|
svelte: 4.2.19
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -11846,6 +12003,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/ms': 2.1.0
|
'@types/ms': 2.1.0
|
||||||
|
|
||||||
|
'@types/diff-match-patch@1.0.36': {}
|
||||||
|
|
||||||
'@types/eslint-scope@3.7.7':
|
'@types/eslint-scope@3.7.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/eslint': 9.6.1
|
'@types/eslint': 9.6.1
|
||||||
@@ -12189,6 +12348,43 @@ snapshots:
|
|||||||
'@urql/core': 5.1.1(graphql@16.10.0)
|
'@urql/core': 5.1.1(graphql@16.10.0)
|
||||||
wonka: 6.3.5
|
wonka: 6.3.5
|
||||||
|
|
||||||
|
'@vue/compiler-core@3.5.13':
|
||||||
|
dependencies:
|
||||||
|
'@babel/parser': 7.27.0
|
||||||
|
'@vue/shared': 3.5.13
|
||||||
|
entities: 4.5.0
|
||||||
|
estree-walker: 2.0.2
|
||||||
|
source-map-js: 1.2.1
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@vue/compiler-dom@3.5.13':
|
||||||
|
dependencies:
|
||||||
|
'@vue/compiler-core': 3.5.13
|
||||||
|
'@vue/shared': 3.5.13
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@vue/compiler-sfc@3.5.13':
|
||||||
|
dependencies:
|
||||||
|
'@babel/parser': 7.27.0
|
||||||
|
'@vue/compiler-core': 3.5.13
|
||||||
|
'@vue/compiler-dom': 3.5.13
|
||||||
|
'@vue/compiler-ssr': 3.5.13
|
||||||
|
'@vue/shared': 3.5.13
|
||||||
|
estree-walker: 2.0.2
|
||||||
|
magic-string: 0.30.17
|
||||||
|
postcss: 8.5.3
|
||||||
|
source-map-js: 1.2.1
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@vue/compiler-ssr@3.5.13':
|
||||||
|
dependencies:
|
||||||
|
'@vue/compiler-dom': 3.5.13
|
||||||
|
'@vue/shared': 3.5.13
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@vue/shared@3.5.13':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@webassemblyjs/ast@1.14.1':
|
'@webassemblyjs/ast@1.14.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@webassemblyjs/helper-numbers': 1.13.2
|
'@webassemblyjs/helper-numbers': 1.13.2
|
||||||
@@ -12300,6 +12496,18 @@ snapshots:
|
|||||||
clean-stack: 2.2.0
|
clean-stack: 2.2.0
|
||||||
indent-string: 4.0.0
|
indent-string: 4.0.0
|
||||||
|
|
||||||
|
ai@4.3.10(react@19.1.0)(zod@3.24.3):
|
||||||
|
dependencies:
|
||||||
|
'@ai-sdk/provider': 1.1.3
|
||||||
|
'@ai-sdk/provider-utils': 2.2.7(zod@3.24.3)
|
||||||
|
'@ai-sdk/react': 1.2.9(react@19.1.0)(zod@3.24.3)
|
||||||
|
'@ai-sdk/ui-utils': 1.2.8(zod@3.24.3)
|
||||||
|
'@opentelemetry/api': 1.9.0
|
||||||
|
jsondiffpatch: 0.6.0
|
||||||
|
zod: 3.24.3
|
||||||
|
optionalDependencies:
|
||||||
|
react: 19.1.0
|
||||||
|
|
||||||
ajv-formats@2.1.1(ajv@8.17.1):
|
ajv-formats@2.1.1(ajv@8.17.1):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
ajv: 8.17.1
|
ajv: 8.17.1
|
||||||
@@ -12565,6 +12773,8 @@ snapshots:
|
|||||||
ansi-styles: 4.3.0
|
ansi-styles: 4.3.0
|
||||||
supports-color: 7.2.0
|
supports-color: 7.2.0
|
||||||
|
|
||||||
|
chalk@5.4.1: {}
|
||||||
|
|
||||||
change-case@3.1.0:
|
change-case@3.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
camel-case: 3.0.0
|
camel-case: 3.0.0
|
||||||
@@ -12654,6 +12864,15 @@ snapshots:
|
|||||||
- '@types/react'
|
- '@types/react'
|
||||||
- '@types/react-dom'
|
- '@types/react-dom'
|
||||||
|
|
||||||
|
code-red@1.0.4:
|
||||||
|
dependencies:
|
||||||
|
'@jridgewell/sourcemap-codec': 1.5.0
|
||||||
|
'@types/estree': 1.0.7
|
||||||
|
acorn: 8.14.1
|
||||||
|
estree-walker: 3.0.3
|
||||||
|
periscopic: 3.1.0
|
||||||
|
optional: true
|
||||||
|
|
||||||
color-convert@1.9.3:
|
color-convert@1.9.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
color-name: 1.1.3
|
color-name: 1.1.3
|
||||||
@@ -12951,6 +13170,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
dequal: 2.0.3
|
dequal: 2.0.3
|
||||||
|
|
||||||
|
diff-match-patch@1.0.5: {}
|
||||||
|
|
||||||
diff@4.0.2: {}
|
diff@4.0.2: {}
|
||||||
|
|
||||||
dir-glob@3.0.1:
|
dir-glob@3.0.1:
|
||||||
@@ -13151,8 +13372,8 @@ snapshots:
|
|||||||
'@typescript-eslint/parser': 8.30.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)
|
'@typescript-eslint/parser': 8.30.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)
|
||||||
eslint: 9.25.1(jiti@2.4.2)
|
eslint: 9.25.1(jiti@2.4.2)
|
||||||
eslint-import-resolver-node: 0.3.9
|
eslint-import-resolver-node: 0.3.9
|
||||||
eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.30.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.1(jiti@2.4.2)))(eslint@9.25.1(jiti@2.4.2))
|
eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0)(eslint@9.25.1(jiti@2.4.2))
|
||||||
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.30.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.30.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.1(jiti@2.4.2)))(eslint@9.25.1(jiti@2.4.2)))(eslint@9.25.1(jiti@2.4.2))
|
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.1(jiti@2.4.2))
|
||||||
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.25.1(jiti@2.4.2))
|
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.25.1(jiti@2.4.2))
|
||||||
eslint-plugin-react: 7.37.5(eslint@9.25.1(jiti@2.4.2))
|
eslint-plugin-react: 7.37.5(eslint@9.25.1(jiti@2.4.2))
|
||||||
eslint-plugin-react-hooks: 5.2.0(eslint@9.25.1(jiti@2.4.2))
|
eslint-plugin-react-hooks: 5.2.0(eslint@9.25.1(jiti@2.4.2))
|
||||||
@@ -13177,7 +13398,7 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.30.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.1(jiti@2.4.2)))(eslint@9.25.1(jiti@2.4.2)):
|
eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0)(eslint@9.25.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
|
||||||
@@ -13188,22 +13409,22 @@ snapshots:
|
|||||||
tinyglobby: 0.2.13
|
tinyglobby: 0.2.13
|
||||||
unrs-resolver: 1.6.1
|
unrs-resolver: 1.6.1
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.30.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.30.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.1(jiti@2.4.2)))(eslint@9.25.1(jiti@2.4.2)))(eslint@9.25.1(jiti@2.4.2))
|
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.1(jiti@2.4.2))
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
eslint-module-utils@2.12.0(@typescript-eslint/parser@8.30.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.30.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.1(jiti@2.4.2)))(eslint@9.25.1(jiti@2.4.2)))(eslint@9.25.1(jiti@2.4.2)):
|
eslint-module-utils@2.12.0(@typescript-eslint/parser@8.30.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0)(eslint@9.25.1(jiti@2.4.2)):
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 3.2.7
|
debug: 3.2.7
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@typescript-eslint/parser': 8.30.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)
|
'@typescript-eslint/parser': 8.30.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)
|
||||||
eslint: 9.25.1(jiti@2.4.2)
|
eslint: 9.25.1(jiti@2.4.2)
|
||||||
eslint-import-resolver-node: 0.3.9
|
eslint-import-resolver-node: 0.3.9
|
||||||
eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.30.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.1(jiti@2.4.2)))(eslint@9.25.1(jiti@2.4.2))
|
eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0)(eslint@9.25.1(jiti@2.4.2))
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.30.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.30.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.1(jiti@2.4.2)))(eslint@9.25.1(jiti@2.4.2)))(eslint@9.25.1(jiti@2.4.2)):
|
eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.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
|
||||||
@@ -13214,7 +13435,7 @@ snapshots:
|
|||||||
doctrine: 2.1.0
|
doctrine: 2.1.0
|
||||||
eslint: 9.25.1(jiti@2.4.2)
|
eslint: 9.25.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.30.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.30.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.1(jiti@2.4.2)))(eslint@9.25.1(jiti@2.4.2)))(eslint@9.25.1(jiti@2.4.2))
|
eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.30.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0)(eslint@9.25.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
|
||||||
@@ -13226,7 +13447,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.30.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)
|
'@typescript-eslint/parser': 8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- eslint-import-resolver-typescript
|
- eslint-import-resolver-typescript
|
||||||
- eslint-import-resolver-webpack
|
- eslint-import-resolver-webpack
|
||||||
@@ -13368,6 +13589,11 @@ snapshots:
|
|||||||
|
|
||||||
estree-walker@2.0.2: {}
|
estree-walker@2.0.2: {}
|
||||||
|
|
||||||
|
estree-walker@3.0.3:
|
||||||
|
dependencies:
|
||||||
|
'@types/estree': 1.0.7
|
||||||
|
optional: true
|
||||||
|
|
||||||
esutils@2.0.3: {}
|
esutils@2.0.3: {}
|
||||||
|
|
||||||
event-target-shim@6.0.2: {}
|
event-target-shim@6.0.2: {}
|
||||||
@@ -13952,6 +14178,11 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree': 1.0.7
|
'@types/estree': 1.0.7
|
||||||
|
|
||||||
|
is-reference@3.0.3:
|
||||||
|
dependencies:
|
||||||
|
'@types/estree': 1.0.7
|
||||||
|
optional: true
|
||||||
|
|
||||||
is-regex@1.2.1:
|
is-regex@1.2.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bound: 1.0.4
|
call-bound: 1.0.4
|
||||||
@@ -14050,6 +14281,8 @@ snapshots:
|
|||||||
|
|
||||||
json-schema-traverse@1.0.0: {}
|
json-schema-traverse@1.0.0: {}
|
||||||
|
|
||||||
|
json-schema@0.4.0: {}
|
||||||
|
|
||||||
json-stable-stringify-without-jsonify@1.0.1: {}
|
json-stable-stringify-without-jsonify@1.0.1: {}
|
||||||
|
|
||||||
json5@1.0.2:
|
json5@1.0.2:
|
||||||
@@ -14058,6 +14291,12 @@ snapshots:
|
|||||||
|
|
||||||
json5@2.2.3: {}
|
json5@2.2.3: {}
|
||||||
|
|
||||||
|
jsondiffpatch@0.6.0:
|
||||||
|
dependencies:
|
||||||
|
'@types/diff-match-patch': 1.0.36
|
||||||
|
chalk: 5.4.1
|
||||||
|
diff-match-patch: 1.0.5
|
||||||
|
|
||||||
jsonfile@6.1.0:
|
jsonfile@6.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
universalify: 2.0.1
|
universalify: 2.0.1
|
||||||
@@ -14147,6 +14386,9 @@ snapshots:
|
|||||||
|
|
||||||
loader-runner@4.3.0: {}
|
loader-runner@4.3.0: {}
|
||||||
|
|
||||||
|
locate-character@3.0.0:
|
||||||
|
optional: true
|
||||||
|
|
||||||
locate-path@6.0.0:
|
locate-path@6.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
p-locate: 5.0.0
|
p-locate: 5.0.0
|
||||||
@@ -14969,6 +15211,13 @@ snapshots:
|
|||||||
|
|
||||||
peberminta@0.9.0: {}
|
peberminta@0.9.0: {}
|
||||||
|
|
||||||
|
periscopic@3.1.0:
|
||||||
|
dependencies:
|
||||||
|
'@types/estree': 1.0.7
|
||||||
|
estree-walker: 3.0.3
|
||||||
|
is-reference: 3.0.3
|
||||||
|
optional: true
|
||||||
|
|
||||||
pg-int8@1.0.1: {}
|
pg-int8@1.0.1: {}
|
||||||
|
|
||||||
pg-protocol@1.8.0: {}
|
pg-protocol@1.8.0: {}
|
||||||
@@ -15220,11 +15469,11 @@ snapshots:
|
|||||||
|
|
||||||
prelude-ls@1.2.1: {}
|
prelude-ls@1.2.1: {}
|
||||||
|
|
||||||
prettier-plugin-tailwindcss@0.6.11(@trivago/prettier-plugin-sort-imports@5.2.2(prettier@3.5.3))(prettier@3.5.3):
|
prettier-plugin-tailwindcss@0.6.11(@trivago/prettier-plugin-sort-imports@5.2.2(@vue/compiler-sfc@3.5.13)(prettier@3.5.3)(svelte@4.2.19))(prettier@3.5.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
prettier: 3.5.3
|
prettier: 3.5.3
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@trivago/prettier-plugin-sort-imports': 5.2.2(prettier@3.5.3)
|
'@trivago/prettier-plugin-sort-imports': 5.2.2(@vue/compiler-sfc@3.5.13)(prettier@3.5.3)(svelte@4.2.19)
|
||||||
|
|
||||||
prettier@3.5.3: {}
|
prettier@3.5.3: {}
|
||||||
|
|
||||||
@@ -15959,6 +16208,24 @@ snapshots:
|
|||||||
|
|
||||||
supports-preserve-symlinks-flag@1.0.0: {}
|
supports-preserve-symlinks-flag@1.0.0: {}
|
||||||
|
|
||||||
|
svelte@4.2.19:
|
||||||
|
dependencies:
|
||||||
|
'@ampproject/remapping': 2.3.0
|
||||||
|
'@jridgewell/sourcemap-codec': 1.5.0
|
||||||
|
'@jridgewell/trace-mapping': 0.3.25
|
||||||
|
'@types/estree': 1.0.7
|
||||||
|
acorn: 8.14.1
|
||||||
|
aria-query: 5.3.2
|
||||||
|
axobject-query: 4.1.0
|
||||||
|
code-red: 1.0.4
|
||||||
|
css-tree: 2.3.1
|
||||||
|
estree-walker: 3.0.3
|
||||||
|
is-reference: 3.0.3
|
||||||
|
locate-character: 3.0.0
|
||||||
|
magic-string: 0.30.17
|
||||||
|
periscopic: 3.1.0
|
||||||
|
optional: true
|
||||||
|
|
||||||
svgo@3.3.2:
|
svgo@3.3.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@trysound/sax': 0.2.0
|
'@trysound/sax': 0.2.0
|
||||||
@@ -15974,6 +16241,12 @@ snapshots:
|
|||||||
lower-case: 1.1.4
|
lower-case: 1.1.4
|
||||||
upper-case: 1.1.3
|
upper-case: 1.1.3
|
||||||
|
|
||||||
|
swr@2.3.3(react@19.1.0):
|
||||||
|
dependencies:
|
||||||
|
dequal: 2.0.3
|
||||||
|
react: 19.1.0
|
||||||
|
use-sync-external-store: 1.5.0(react@19.1.0)
|
||||||
|
|
||||||
tabbable@6.2.0: {}
|
tabbable@6.2.0: {}
|
||||||
|
|
||||||
tailwind-merge@3.2.0: {}
|
tailwind-merge@3.2.0: {}
|
||||||
@@ -16015,6 +16288,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
real-require: 0.2.0
|
real-require: 0.2.0
|
||||||
|
|
||||||
|
throttleit@2.1.0: {}
|
||||||
|
|
||||||
through@2.3.8: {}
|
through@2.3.8: {}
|
||||||
|
|
||||||
tiny-invariant@1.0.6: {}
|
tiny-invariant@1.0.6: {}
|
||||||
@@ -16519,6 +16794,10 @@ snapshots:
|
|||||||
|
|
||||||
yocto-queue@0.1.0: {}
|
yocto-queue@0.1.0: {}
|
||||||
|
|
||||||
|
zod-to-json-schema@3.24.5(zod@3.24.3):
|
||||||
|
dependencies:
|
||||||
|
zod: 3.24.3
|
||||||
|
|
||||||
zod@3.24.3: {}
|
zod@3.24.3: {}
|
||||||
|
|
||||||
zwitch@2.0.4: {}
|
zwitch@2.0.4: {}
|
||||||
|
|||||||
Reference in New Issue
Block a user