Changelog (#399)
* feat: add changelog feature and update site navigation - Introduced a new Changelog page with pagination and detailed entry views. - Added components for displaying changelog entries, pagination, and entry details. - Updated site navigation to include a link to the new Changelog page. - Enhanced localization for changelog-related texts in marketing.json. * refactor: enhance Changelog page layout and entry display - Increased the number of changelog entries displayed per page from 2 to 20 for better visibility. - Improved the layout of the Changelog page by adjusting the container styles and removing unnecessary divs. - Updated the ChangelogEntry component to enhance the visual presentation of entries, including a new date badge with an icon. - Refined the CSS styles for Markdoc headings to improve typography and spacing. * refactor: enhance Changelog page functionality and layout - Increased the number of changelog entries displayed per page from 20 to 50 for improved user experience. - Updated ChangelogEntry component to make the highlight prop optional and refined the layout for better visual clarity. - Adjusted styles in ChangelogHeader and ChangelogPagination components for a more cohesive design. - Removed unnecessary order fields from changelog markdown files to streamline content management. * feat: enhance Changelog entry navigation and data loading - Refactored ChangelogEntry page to load previous and next entries for improved navigation. - Introduced ChangelogNavigation component to facilitate navigation between changelog entries. - Updated ChangelogDetail component to display navigation links and entry details. - Enhanced data fetching logic to retrieve all changelog entries alongside the current entry. - Added localization keys for navigation text in marketing.json. * Update package dependencies and enhance documentation layout - Upgraded various packages including @turbo/gen and turbo to version 2.6.0, and react-hook-form to version 7.66.0. - Updated lucide-react to version 0.552.0 across multiple packages. - Refactored documentation layout components for improved styling and structure. - Removed deprecated loading components and adjusted navigation elements for better user experience. - Added placeholder notes in changelog entries for clarity.
This commit is contained in:
committed by
GitHub
parent
a920dea2b3
commit
116d41a284
@@ -8,12 +8,12 @@
|
|||||||
"format": "prettier --check --write \"**/*.{ts,tsx}\" --ignore-path=\"../../.prettierignore\""
|
"format": "prettier --check --write \"**/*.{ts,tsx}\" --ignore-path=\"../../.prettierignore\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/openai": "^2.0.58",
|
"@ai-sdk/openai": "^2.0.59",
|
||||||
"@faker-js/faker": "^10.1.0",
|
"@faker-js/faker": "^10.1.0",
|
||||||
"@hookform/resolvers": "^5.2.2",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
"@tanstack/react-query": "5.90.5",
|
"@tanstack/react-query": "5.90.5",
|
||||||
"ai": "5.0.83",
|
"ai": "5.0.86",
|
||||||
"lucide-react": "^0.548.0",
|
"lucide-react": "^0.552.0",
|
||||||
"next": "catalog:",
|
"next": "catalog:",
|
||||||
"nodemailer": "^7.0.10",
|
"nodemailer": "^7.0.10",
|
||||||
"react": "19.2.0",
|
"react": "19.2.0",
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
"@types/react-dom": "19.2.2",
|
"@types/react-dom": "19.2.2",
|
||||||
"babel-plugin-react-compiler": "1.0.0",
|
"babel-plugin-react-compiler": "1.0.0",
|
||||||
"pino-pretty": "13.0.0",
|
"pino-pretty": "13.0.0",
|
||||||
"react-hook-form": "^7.65.0",
|
"react-hook-form": "^7.66.0",
|
||||||
"recharts": "2.15.3",
|
"recharts": "2.15.3",
|
||||||
"tailwindcss": "4.1.16",
|
"tailwindcss": "4.1.16",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ const links = {
|
|||||||
label: 'marketing:blog',
|
label: 'marketing:blog',
|
||||||
path: '/blog',
|
path: '/blog',
|
||||||
},
|
},
|
||||||
|
Changelog: {
|
||||||
|
label: 'marketing:changelog',
|
||||||
|
path: '/changelog',
|
||||||
|
},
|
||||||
Docs: {
|
Docs: {
|
||||||
label: 'marketing:documentation',
|
label: 'marketing:documentation',
|
||||||
path: '/docs',
|
path: '/docs',
|
||||||
@@ -30,10 +34,6 @@ const links = {
|
|||||||
label: 'marketing:faq',
|
label: 'marketing:faq',
|
||||||
path: '/faq',
|
path: '/faq',
|
||||||
},
|
},
|
||||||
Contact: {
|
|
||||||
label: 'marketing:contact',
|
|
||||||
path: '/contact',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function SiteNavigation() {
|
export function SiteNavigation() {
|
||||||
|
|||||||
110
apps/web/app/(marketing)/changelog/[slug]/page.tsx
Normal file
110
apps/web/app/(marketing)/changelog/[slug]/page.tsx
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import { cache } from 'react';
|
||||||
|
|
||||||
|
import type { Metadata } from 'next';
|
||||||
|
|
||||||
|
import { notFound } from 'next/navigation';
|
||||||
|
|
||||||
|
import { createCmsClient } from '@kit/cms';
|
||||||
|
|
||||||
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
|
||||||
|
import { ChangelogDetail } from '../_components/changelog-detail';
|
||||||
|
|
||||||
|
interface ChangelogEntryPageProps {
|
||||||
|
params: Promise<{ slug: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getChangelogData = cache(changelogEntryLoader);
|
||||||
|
|
||||||
|
async function changelogEntryLoader(slug: string) {
|
||||||
|
const client = await createCmsClient();
|
||||||
|
|
||||||
|
const [entry, allEntries] = await Promise.all([
|
||||||
|
client.getContentItemBySlug({ slug, collection: 'changelog' }),
|
||||||
|
client.getContentItems({
|
||||||
|
collection: 'changelog',
|
||||||
|
sortBy: 'publishedAt',
|
||||||
|
sortDirection: 'desc',
|
||||||
|
content: false,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!entry) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find previous and next entries in the timeline
|
||||||
|
const currentIndex = allEntries.items.findIndex((item) => item.slug === slug);
|
||||||
|
const previousEntry =
|
||||||
|
currentIndex > 0 ? allEntries.items[currentIndex - 1] : null;
|
||||||
|
const nextEntry =
|
||||||
|
currentIndex < allEntries.items.length - 1
|
||||||
|
? allEntries.items[currentIndex + 1]
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
entry,
|
||||||
|
previousEntry,
|
||||||
|
nextEntry,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateMetadata({
|
||||||
|
params,
|
||||||
|
}: ChangelogEntryPageProps): Promise<Metadata> {
|
||||||
|
const slug = (await params).slug;
|
||||||
|
const data = await getChangelogData(slug);
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
const { title, publishedAt, description, image } = data.entry;
|
||||||
|
|
||||||
|
return Promise.resolve({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
openGraph: {
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
type: 'article',
|
||||||
|
publishedTime: publishedAt,
|
||||||
|
url: data.entry.url,
|
||||||
|
images: image
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
url: image,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: 'summary_large_image',
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
images: image ? [image] : [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ChangelogEntryPage({ params }: ChangelogEntryPageProps) {
|
||||||
|
const slug = (await params).slug;
|
||||||
|
const data = await getChangelogData(slug);
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container sm:max-w-none sm:p-0">
|
||||||
|
<ChangelogDetail
|
||||||
|
entry={data.entry}
|
||||||
|
content={data.entry.content}
|
||||||
|
previousEntry={data.previousEntry ?? null}
|
||||||
|
nextEntry={data.nextEntry ?? null}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withI18n(ChangelogEntryPage);
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import type { Cms } from '@kit/cms';
|
||||||
|
import { ContentRenderer } from '@kit/cms';
|
||||||
|
|
||||||
|
import { ChangelogHeader } from './changelog-header';
|
||||||
|
import { ChangelogNavigation } from './changelog-navigation';
|
||||||
|
|
||||||
|
interface ChangelogDetailProps {
|
||||||
|
entry: Cms.ContentItem;
|
||||||
|
content: unknown;
|
||||||
|
previousEntry: Cms.ContentItem | null;
|
||||||
|
nextEntry: Cms.ContentItem | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ChangelogDetail({
|
||||||
|
entry,
|
||||||
|
content,
|
||||||
|
previousEntry,
|
||||||
|
nextEntry,
|
||||||
|
}: ChangelogDetailProps) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<ChangelogHeader entry={entry} />
|
||||||
|
|
||||||
|
<div className="mx-auto flex max-w-3xl flex-col space-y-6 py-8">
|
||||||
|
<article className="markdoc">
|
||||||
|
<ContentRenderer content={content} />
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ChangelogNavigation
|
||||||
|
previousEntry={previousEntry}
|
||||||
|
nextEntry={nextEntry}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
import { type Cms } from '@kit/cms';
|
||||||
|
import { If } from '@kit/ui/if';
|
||||||
|
import { cn } from '@kit/ui/utils';
|
||||||
|
|
||||||
|
import { DateBadge } from './date-badge';
|
||||||
|
|
||||||
|
interface ChangelogEntryProps {
|
||||||
|
entry: Cms.ContentItem;
|
||||||
|
highlight?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ChangelogEntry({
|
||||||
|
entry,
|
||||||
|
highlight = false,
|
||||||
|
}: ChangelogEntryProps) {
|
||||||
|
const { title, slug, publishedAt, description } = entry;
|
||||||
|
const entryUrl = `/changelog/${slug}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex gap-6 md:gap-8">
|
||||||
|
<div className="md:border-border relative flex flex-1 flex-col space-y-0 gap-y-2.5 border-l border-dashed border-transparent pb-4 md:pl-8 lg:pl-12">
|
||||||
|
{highlight ? (
|
||||||
|
<span className="absolute top-5.5 left-0 hidden h-2.5 w-2.5 -translate-x-1/2 md:flex">
|
||||||
|
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-red-400 opacity-75"></span>
|
||||||
|
<span className="relative inline-flex h-2.5 w-2.5 rounded-full bg-red-400"></span>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'bg-muted absolute top-5.5 left-0 hidden h-2.5 w-2.5 -translate-x-1/2 rounded-full md:block',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="hover:bg-muted/50 active:bg-muted rounded-md transition-colors">
|
||||||
|
<Link href={entryUrl} className="block space-y-2 p-4">
|
||||||
|
<div>
|
||||||
|
<DateBadge date={publishedAt} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 className="text-xl leading-tight font-semibold tracking-tight group-hover/link:underline">
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<If condition={description}>
|
||||||
|
{(desc) => (
|
||||||
|
<p className="text-muted-foreground text-sm leading-relaxed">
|
||||||
|
{desc}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</If>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
import { ChevronLeft } from 'lucide-react';
|
||||||
|
|
||||||
|
import { Cms } from '@kit/cms';
|
||||||
|
import { If } from '@kit/ui/if';
|
||||||
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
import { cn } from '@kit/ui/utils';
|
||||||
|
|
||||||
|
import { CoverImage } from '../../blog/_components/cover-image';
|
||||||
|
import { DateFormatter } from '../../blog/_components/date-formatter';
|
||||||
|
|
||||||
|
export function ChangelogHeader({ entry }: { entry: Cms.ContentItem }) {
|
||||||
|
const { title, publishedAt, description, image } = entry;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-1 flex-col">
|
||||||
|
<div className="border-border/50 border-b py-4">
|
||||||
|
<div className="mx-auto flex max-w-3xl items-center justify-between">
|
||||||
|
<Link
|
||||||
|
href="/changelog"
|
||||||
|
className="text-muted-foreground hover:text-primary flex items-center gap-1.5 text-sm font-medium transition-colors"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="h-4 w-4" />
|
||||||
|
<Trans i18nKey="marketing:changelog" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={cn('border-border/50 border-b py-8')}>
|
||||||
|
<div className="mx-auto flex max-w-3xl flex-col gap-y-2.5">
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground text-xs">
|
||||||
|
<DateFormatter dateString={publishedAt} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 className="font-heading text-2xl font-medium tracking-tighter xl:text-4xl dark:text-white">
|
||||||
|
{title}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
{description && (
|
||||||
|
<h2
|
||||||
|
className="text-muted-foreground text-base"
|
||||||
|
dangerouslySetInnerHTML={{ __html: description }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<If condition={image}>
|
||||||
|
{(imageUrl) => (
|
||||||
|
<div className="relative mx-auto mt-8 flex h-[378px] w-full max-w-3xl justify-center">
|
||||||
|
<CoverImage
|
||||||
|
preloadImage
|
||||||
|
className="rounded-md"
|
||||||
|
title={title}
|
||||||
|
src={imageUrl}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</If>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||||||
|
|
||||||
|
import type { Cms } from '@kit/cms';
|
||||||
|
import { If } from '@kit/ui/if';
|
||||||
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
import { cn } from '@kit/ui/utils';
|
||||||
|
|
||||||
|
import { DateFormatter } from '../../blog/_components/date-formatter';
|
||||||
|
|
||||||
|
interface ChangelogNavigationProps {
|
||||||
|
previousEntry: Cms.ContentItem | null;
|
||||||
|
nextEntry: Cms.ContentItem | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NavLinkProps {
|
||||||
|
entry: Cms.ContentItem;
|
||||||
|
direction: 'previous' | 'next';
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavLink({ entry, direction }: NavLinkProps) {
|
||||||
|
const isPrevious = direction === 'previous';
|
||||||
|
|
||||||
|
const Icon = isPrevious ? ChevronLeft : ChevronRight;
|
||||||
|
const i18nKey = isPrevious
|
||||||
|
? 'marketing:changelogNavigationPrevious'
|
||||||
|
: 'marketing:changelogNavigationNext';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href={`/changelog/${entry.slug}`}
|
||||||
|
className={cn(
|
||||||
|
'border-border/50 hover:bg-muted/50 group flex flex-col gap-2 rounded-lg border p-4 transition-all',
|
||||||
|
!isPrevious && 'text-right md:items-end',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="text-muted-foreground flex items-center gap-2 text-xs">
|
||||||
|
{isPrevious && <Icon className="h-3 w-3" />}
|
||||||
|
|
||||||
|
<span className="font-medium tracking-wider uppercase">
|
||||||
|
<Trans i18nKey={i18nKey} />
|
||||||
|
</span>
|
||||||
|
{!isPrevious && <Icon className="h-3 w-3" />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<h3 className="group-hover:text-primary text-sm leading-tight font-semibold transition-colors">
|
||||||
|
{entry.title}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="text-muted-foreground text-xs">
|
||||||
|
<DateFormatter dateString={entry.publishedAt} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ChangelogNavigation({
|
||||||
|
previousEntry,
|
||||||
|
nextEntry,
|
||||||
|
}: ChangelogNavigationProps) {
|
||||||
|
return (
|
||||||
|
<div className="border-border/50 border-t py-8">
|
||||||
|
<div className="mx-auto max-w-3xl">
|
||||||
|
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||||
|
<If condition={previousEntry} fallback={<div />}>
|
||||||
|
{(prev) => <NavLink entry={prev} direction="previous" />}
|
||||||
|
</If>
|
||||||
|
|
||||||
|
<If condition={nextEntry} fallback={<div />}>
|
||||||
|
{(next) => <NavLink entry={next} direction="next" />}
|
||||||
|
</If>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
import { ArrowLeft, ArrowRight } from 'lucide-react';
|
||||||
|
|
||||||
|
import { Button } from '@kit/ui/button';
|
||||||
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
|
interface ChangelogPaginationProps {
|
||||||
|
currentPage: number;
|
||||||
|
canGoToNextPage: boolean;
|
||||||
|
canGoToPreviousPage: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ChangelogPagination({
|
||||||
|
currentPage,
|
||||||
|
canGoToNextPage,
|
||||||
|
canGoToPreviousPage,
|
||||||
|
}: ChangelogPaginationProps) {
|
||||||
|
const nextPage = currentPage + 1;
|
||||||
|
const previousPage = currentPage - 1;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
{canGoToPreviousPage && (
|
||||||
|
<Button asChild variant="outline" size="sm">
|
||||||
|
<Link href={`/changelog?page=${previousPage}`}>
|
||||||
|
<ArrowLeft className="mr-2 h-3 w-3" />
|
||||||
|
<span>
|
||||||
|
<Trans i18nKey="marketing:changelogPaginationPrevious" />
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{canGoToNextPage && (
|
||||||
|
<Button asChild variant="outline" size="sm">
|
||||||
|
<Link href={`/changelog?page=${nextPage}`}>
|
||||||
|
<span>
|
||||||
|
<Trans i18nKey="marketing:changelogPaginationNext" />
|
||||||
|
</span>
|
||||||
|
<ArrowRight className="ml-2 h-3 w-3" />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { format } from 'date-fns';
|
||||||
|
import { CalendarIcon } from 'lucide-react';
|
||||||
|
|
||||||
|
interface DateBadgeProps {
|
||||||
|
date: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DateBadge({ date }: DateBadgeProps) {
|
||||||
|
const formattedDate = format(new Date(date), 'MMMM d, yyyy');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="text-muted-foreground flex flex-shrink-0 items-center gap-2 text-sm">
|
||||||
|
<CalendarIcon className="size-3" />
|
||||||
|
<span>{formattedDate}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
117
apps/web/app/(marketing)/changelog/page.tsx
Normal file
117
apps/web/app/(marketing)/changelog/page.tsx
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import { cache } from 'react';
|
||||||
|
|
||||||
|
import type { Metadata } from 'next';
|
||||||
|
|
||||||
|
import { createCmsClient } from '@kit/cms';
|
||||||
|
import { getLogger } from '@kit/shared/logger';
|
||||||
|
import { If } from '@kit/ui/if';
|
||||||
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
|
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||||
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
|
||||||
|
import { SitePageHeader } from '../_components/site-page-header';
|
||||||
|
import { ChangelogEntry } from './_components/changelog-entry';
|
||||||
|
import { ChangelogPagination } from './_components/changelog-pagination';
|
||||||
|
|
||||||
|
interface ChangelogPageProps {
|
||||||
|
searchParams: Promise<{ page?: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CHANGELOG_ENTRIES_PER_PAGE = 50;
|
||||||
|
|
||||||
|
export const generateMetadata = async (
|
||||||
|
props: ChangelogPageProps,
|
||||||
|
): Promise<Metadata> => {
|
||||||
|
const { t, resolvedLanguage } = await createI18nServerInstance();
|
||||||
|
const searchParams = await props.searchParams;
|
||||||
|
const limit = CHANGELOG_ENTRIES_PER_PAGE;
|
||||||
|
|
||||||
|
const page = searchParams.page ? parseInt(searchParams.page) : 0;
|
||||||
|
const offset = page * limit;
|
||||||
|
|
||||||
|
const { total } = await getContentItems(resolvedLanguage, limit, offset);
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: t('marketing:changelog'),
|
||||||
|
description: t('marketing:changelogSubtitle'),
|
||||||
|
pagination: {
|
||||||
|
previous: page > 0 ? `/changelog?page=${page - 1}` : undefined,
|
||||||
|
next: offset + limit < total ? `/changelog?page=${page + 1}` : undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getContentItems = cache(
|
||||||
|
async (language: string | undefined, limit: number, offset: number) => {
|
||||||
|
const client = await createCmsClient();
|
||||||
|
const logger = await getLogger();
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await client.getContentItems({
|
||||||
|
collection: 'changelog',
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
content: false,
|
||||||
|
language,
|
||||||
|
sortBy: 'publishedAt',
|
||||||
|
sortDirection: 'desc',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error({ error }, 'Failed to load changelog entries');
|
||||||
|
|
||||||
|
return { total: 0, items: [] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
async function ChangelogPage(props: ChangelogPageProps) {
|
||||||
|
const { t, resolvedLanguage: language } = await createI18nServerInstance();
|
||||||
|
const searchParams = await props.searchParams;
|
||||||
|
|
||||||
|
const limit = CHANGELOG_ENTRIES_PER_PAGE;
|
||||||
|
const page = searchParams.page ? parseInt(searchParams.page) : 0;
|
||||||
|
const offset = page * limit;
|
||||||
|
|
||||||
|
const { total, items: entries } = await getContentItems(
|
||||||
|
language,
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SitePageHeader
|
||||||
|
title={t('marketing:changelog')}
|
||||||
|
subtitle={t('marketing:changelogSubtitle')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="container flex max-w-4xl flex-col space-y-12 py-12">
|
||||||
|
<If
|
||||||
|
condition={entries.length > 0}
|
||||||
|
fallback={<Trans i18nKey="marketing:noChangelogEntries" />}
|
||||||
|
>
|
||||||
|
<div className="space-y-0">
|
||||||
|
{entries.map((entry, index) => {
|
||||||
|
return (
|
||||||
|
<ChangelogEntry
|
||||||
|
key={entry.id}
|
||||||
|
entry={entry}
|
||||||
|
highlight={index === 0}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ChangelogPagination
|
||||||
|
currentPage={page}
|
||||||
|
canGoToNextPage={offset + limit < total}
|
||||||
|
canGoToPreviousPage={page > 0}
|
||||||
|
/>
|
||||||
|
</If>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withI18n(ChangelogPage);
|
||||||
@@ -11,8 +11,6 @@ import { withI18n } from '~/lib/i18n/with-i18n';
|
|||||||
|
|
||||||
// local imports
|
// local imports
|
||||||
import { DocsCards } from '../_components/docs-cards';
|
import { DocsCards } from '../_components/docs-cards';
|
||||||
import { DocsTableOfContents } from '../_components/docs-table-of-contents';
|
|
||||||
import { extractHeadingsFromJSX } from '../_lib/utils';
|
|
||||||
|
|
||||||
const getPageBySlug = cache(pageLoader);
|
const getPageBySlug = cache(pageLoader);
|
||||||
|
|
||||||
@@ -52,38 +50,36 @@ async function DocumentationPage({ params }: DocumentationPageProps) {
|
|||||||
|
|
||||||
const description = page?.description ?? '';
|
const description = page?.description ?? '';
|
||||||
|
|
||||||
const headings = extractHeadingsFromJSX(
|
|
||||||
page.content as {
|
|
||||||
props: { children: React.ReactElement[] };
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'flex flex-1 flex-col gap-y-4 overflow-y-hidden py-4'}>
|
<div className={'flex flex-1 flex-col gap-y-4 overflow-y-hidden'}>
|
||||||
<div className={'flex overflow-y-hidden'}>
|
<div className={'flex size-full overflow-y-hidden'}>
|
||||||
<article className={cn('gap-y-12 overflow-y-auto px-4')}>
|
<div className="relative size-full">
|
||||||
<section
|
<article
|
||||||
className={'flex flex-col gap-y-1 border-b border-dashed pb-4'}
|
className={cn(
|
||||||
|
'absolute size-full w-full gap-y-12 overflow-y-auto pt-4 pb-36',
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<h1
|
<section
|
||||||
className={
|
className={'flex flex-col gap-y-1 border-b border-dashed pb-4'}
|
||||||
'text-foreground text-3xl font-semibold tracking-tighter'
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{page.title}
|
<h1
|
||||||
</h1>
|
className={
|
||||||
|
'text-foreground text-3xl font-semibold tracking-tighter'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{page.title}
|
||||||
|
</h1>
|
||||||
|
|
||||||
<h2 className={'text-secondary-foreground/80 text-lg'}>
|
<h2 className={'text-secondary-foreground/80 text-lg'}>
|
||||||
{description}
|
{description}
|
||||||
</h2>
|
</h2>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div className={'markdoc'}>
|
<div className={'markdoc'}>
|
||||||
<ContentRenderer content={page.content} />
|
<ContentRenderer content={page.content} />
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
</div>
|
||||||
<DocsTableOfContents data={headings} />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<If condition={page.children.length > 0}>
|
<If condition={page.children.length > 0}>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export function DocsCard({
|
|||||||
return (
|
return (
|
||||||
<Link href={link.url} className="flex flex-col">
|
<Link href={link.url} className="flex flex-col">
|
||||||
<div
|
<div
|
||||||
className={`bg-muted/50 hover:bg-muted/70 flex grow flex-col gap-y-2 rounded p-4`}
|
className={`hover:bg-muted/70 flex grow flex-col gap-y-0.5 rounded border p-4`}
|
||||||
>
|
>
|
||||||
<h3 className="mt-0 text-lg font-medium hover:underline dark:text-white">
|
<h3 className="mt-0 text-lg font-medium hover:underline dark:text-white">
|
||||||
{title}
|
{title}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export function DocsCards({ cards }: { cards: Cms.ContentItem[] }) {
|
|||||||
const cardsSortedByOrder = [...cards].sort((a, b) => a.order - b.order);
|
const cardsSortedByOrder = [...cards].sort((a, b) => a.order - b.order);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'grid grid-cols-1 gap-4 lg:grid-cols-2'}>
|
<div className={'absolute flex w-full flex-col gap-4 pb-48 lg:max-w-2xl'}>
|
||||||
{cardsSortedByOrder.map((item) => {
|
{cardsSortedByOrder.map((item) => {
|
||||||
return (
|
return (
|
||||||
<DocsCard
|
<DocsCard
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useRef } from 'react';
|
|
||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
|
|
||||||
@@ -14,7 +12,6 @@ export function DocsNavLink({
|
|||||||
children,
|
children,
|
||||||
}: React.PropsWithChildren<{ label: string; url: string }>) {
|
}: React.PropsWithChildren<{ label: string; url: string }>) {
|
||||||
const currentPath = usePathname();
|
const currentPath = usePathname();
|
||||||
const ref = useRef<HTMLElement>(null);
|
|
||||||
const isCurrent = isRouteActive(url, currentPath, true);
|
const isCurrent = isRouteActive(url, currentPath, true);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -22,14 +19,10 @@ export function DocsNavLink({
|
|||||||
<SidebarMenuButton
|
<SidebarMenuButton
|
||||||
asChild
|
asChild
|
||||||
isActive={isCurrent}
|
isActive={isCurrent}
|
||||||
className={cn('transition-background font-normal!', {
|
className={cn('text-secondary-foreground transition-all')}
|
||||||
'text-secondary-foreground font-bold': isCurrent,
|
|
||||||
})}
|
|
||||||
>
|
>
|
||||||
<Link href={url}>
|
<Link href={url}>
|
||||||
<span ref={ref} className="block max-w-full truncate">
|
<span className="block max-w-full truncate">{label}</span>
|
||||||
{label}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { usePathname } from 'next/navigation';
|
|||||||
|
|
||||||
import { Cms } from '@kit/cms';
|
import { Cms } from '@kit/cms';
|
||||||
import { Collapsible } from '@kit/ui/collapsible';
|
import { Collapsible } from '@kit/ui/collapsible';
|
||||||
import { isRouteActive } from '@kit/ui/utils';
|
import { cn, isRouteActive } from '@kit/ui/utils';
|
||||||
|
|
||||||
export function DocsNavigationCollapsible(
|
export function DocsNavigationCollapsible(
|
||||||
props: React.PropsWithChildren<{
|
props: React.PropsWithChildren<{
|
||||||
@@ -21,7 +21,9 @@ export function DocsNavigationCollapsible(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Collapsible
|
<Collapsible
|
||||||
className={'group/collapsible'}
|
className={cn('group/collapsible', {
|
||||||
|
'group/active': isChildActive,
|
||||||
|
})}
|
||||||
defaultOpen={isChildActive ? true : !props.node.collapsed}
|
defaultOpen={isChildActive ? true : !props.node.collapsed}
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
|
|||||||
@@ -139,12 +139,12 @@ export function DocsNavigation({
|
|||||||
<Sidebar
|
<Sidebar
|
||||||
variant={'ghost'}
|
variant={'ghost'}
|
||||||
className={
|
className={
|
||||||
'sticky z-1 mt-4 max-h-full overflow-y-auto border-r-transparent'
|
'border-border/50 sticky z-1 mt-4 max-h-full overflow-y-auto pr-4'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SidebarGroup>
|
<SidebarGroup className="p-0">
|
||||||
<SidebarGroupContent>
|
<SidebarGroupContent>
|
||||||
<SidebarMenu>
|
<SidebarMenu className={'pb-48'}>
|
||||||
<Tree pages={pages} level={0} prefix={prefix} />
|
<Tree pages={pages} level={0} prefix={prefix} />
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
</SidebarGroupContent>
|
</SidebarGroupContent>
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
import Link from 'next/link';
|
|
||||||
|
|
||||||
import { If } from '@kit/ui/if';
|
|
||||||
import { cn } from '@kit/ui/utils';
|
|
||||||
|
|
||||||
export function DocsPageLink({
|
|
||||||
page,
|
|
||||||
before,
|
|
||||||
after,
|
|
||||||
}: React.PropsWithChildren<{
|
|
||||||
page: {
|
|
||||||
url: string;
|
|
||||||
title: string;
|
|
||||||
};
|
|
||||||
before?: React.ReactNode;
|
|
||||||
after?: React.ReactNode;
|
|
||||||
}>) {
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
className={cn(
|
|
||||||
`ring-muted hover:ring-primary flex w-full items-center space-x-8 rounded-xl p-6 font-medium text-current ring-2 transition-all`,
|
|
||||||
{
|
|
||||||
'justify-start': before,
|
|
||||||
'justify-end self-end': after,
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
href={page.url}
|
|
||||||
>
|
|
||||||
<If condition={before}>{(node) => <>{node}</>}</If>
|
|
||||||
|
|
||||||
<span className={'flex flex-col space-y-1.5'}>
|
|
||||||
<span
|
|
||||||
className={'text-muted-foreground text-xs font-semibold uppercase'}
|
|
||||||
>
|
|
||||||
{before ? `Previous` : ``}
|
|
||||||
{after ? `Next` : ``}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span>{page.title}</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<If condition={after}>{(node) => <>{node}</>}</If>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import Link from 'next/link';
|
|
||||||
|
|
||||||
interface NavItem {
|
|
||||||
text: string;
|
|
||||||
level: number;
|
|
||||||
href: string;
|
|
||||||
children: NavItem[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DocsTableOfContents(props: { data: NavItem[] }) {
|
|
||||||
const navData = props.data;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="bg-background sticky inset-y-0 hidden h-svh max-h-full min-w-[14em] p-2.5 lg:block">
|
|
||||||
<ol
|
|
||||||
role="list"
|
|
||||||
className="relative text-sm text-gray-600 dark:text-gray-400"
|
|
||||||
>
|
|
||||||
{navData.map((item) => (
|
|
||||||
<li key={item.href} className="group/item relative mt-3 first:mt-0">
|
|
||||||
<a
|
|
||||||
href={item.href}
|
|
||||||
className="block transition-colors **:[font:inherit] hover:text-gray-950 dark:hover:text-white"
|
|
||||||
>
|
|
||||||
{item.text}
|
|
||||||
</a>
|
|
||||||
{item.children && (
|
|
||||||
<ol role="list" className="relative mt-3 pl-4">
|
|
||||||
{item.children.map((child) => (
|
|
||||||
<li
|
|
||||||
key={child.href}
|
|
||||||
className="group/subitem relative mt-3 first:mt-0"
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
href={child.href}
|
|
||||||
className="block transition-colors **:[font:inherit] hover:text-gray-950 dark:hover:text-white"
|
|
||||||
>
|
|
||||||
{child.text}
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ol>
|
|
||||||
)}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,5 @@
|
|||||||
import { Cms } from '@kit/cms';
|
import { Cms } from '@kit/cms';
|
||||||
|
|
||||||
interface HeadingNode {
|
|
||||||
text: string;
|
|
||||||
level: number;
|
|
||||||
href: string;
|
|
||||||
children: HeadingNode[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @name buildDocumentationTree
|
* @name buildDocumentationTree
|
||||||
* @description Build a tree structure for the documentation pages.
|
* @description Build a tree structure for the documentation pages.
|
||||||
@@ -38,109 +31,3 @@ export function buildDocumentationTree(pages: Cms.ContentItem[]) {
|
|||||||
|
|
||||||
return tree.sort((a, b) => a.order - b.order);
|
return tree.sort((a, b) => a.order - b.order);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @name extractHeadingsFromJSX
|
|
||||||
* @description Extract headings from JSX. This is used to generate the table of contents for the documentation pages.
|
|
||||||
* @param jsx
|
|
||||||
*/
|
|
||||||
export function extractHeadingsFromJSX(jsx: {
|
|
||||||
props: { children: React.ReactElement[] };
|
|
||||||
}) {
|
|
||||||
const headings: HeadingNode[] = [];
|
|
||||||
let currentH2: HeadingNode | null = null;
|
|
||||||
|
|
||||||
function getTextContent(
|
|
||||||
children: React.ReactElement[] | string | React.ReactElement,
|
|
||||||
): string {
|
|
||||||
try {
|
|
||||||
if (typeof children === 'string') {
|
|
||||||
return children;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(children)) {
|
|
||||||
return children.map((child) => getTextContent(child)).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
(
|
|
||||||
children.props as {
|
|
||||||
children: React.ReactElement;
|
|
||||||
}
|
|
||||||
).children
|
|
||||||
) {
|
|
||||||
return getTextContent(
|
|
||||||
(children.props as { children: React.ReactElement }).children,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
|
||||||
} catch {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
jsx.props.children.forEach((node) => {
|
|
||||||
if (!node || typeof node !== 'object' || !('type' in node)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nodeType = node.type as string;
|
|
||||||
|
|
||||||
const text = getTextContent(
|
|
||||||
(
|
|
||||||
node.props as {
|
|
||||||
children: React.ReactElement[];
|
|
||||||
}
|
|
||||||
).children,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (nodeType === 'h1') {
|
|
||||||
const slug = generateSlug(text);
|
|
||||||
|
|
||||||
headings.push({
|
|
||||||
text,
|
|
||||||
level: 1,
|
|
||||||
href: `#${slug}`,
|
|
||||||
children: [],
|
|
||||||
});
|
|
||||||
} else if (nodeType === 'h2') {
|
|
||||||
const slug = generateSlug(text);
|
|
||||||
|
|
||||||
currentH2 = {
|
|
||||||
text,
|
|
||||||
level: 2,
|
|
||||||
href: `#${slug}`,
|
|
||||||
children: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
if (headings.length > 0) {
|
|
||||||
headings[headings.length - 1]!.children.push(currentH2);
|
|
||||||
} else {
|
|
||||||
headings.push(currentH2);
|
|
||||||
}
|
|
||||||
} else if (nodeType === 'h3' && currentH2) {
|
|
||||||
const slug = generateSlug(text);
|
|
||||||
|
|
||||||
currentH2.children.push({
|
|
||||||
text,
|
|
||||||
level: 3,
|
|
||||||
href: `#${slug}`,
|
|
||||||
children: [],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return headings;
|
|
||||||
} catch {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateSlug(text: string): string {
|
|
||||||
return text
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/[^a-z0-9]+/g, '-')
|
|
||||||
.replace(/(^-|-$)/g, '');
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -13,14 +13,32 @@ async function DocsLayout({ children }: React.PropsWithChildren) {
|
|||||||
const tree = buildDocumentationTree(docs);
|
const tree = buildDocumentationTree(docs);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarProvider
|
<div className={'container h-[calc(100vh-56px)] overflow-y-hidden'}>
|
||||||
style={{ '--sidebar-width': '18em' } as React.CSSProperties}
|
<SidebarProvider
|
||||||
className={'h-[calc(100vh-72px)] overflow-y-hidden lg:container'}
|
className="lg:gap-x-6"
|
||||||
>
|
style={{ '--sidebar-width': '17em' } as React.CSSProperties}
|
||||||
<DocsNavigation pages={tree} />
|
>
|
||||||
|
<HideFooterStyles />
|
||||||
|
|
||||||
{children}
|
<DocsNavigation pages={tree} />
|
||||||
</SidebarProvider>
|
|
||||||
|
{children}
|
||||||
|
</SidebarProvider>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function HideFooterStyles() {
|
||||||
|
return (
|
||||||
|
<style
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: `
|
||||||
|
.site-footer {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
import { GlobalLoader } from '@kit/ui/global-loader';
|
|
||||||
|
|
||||||
export default GlobalLoader;
|
|
||||||
@@ -21,16 +21,14 @@ async function DocsPage() {
|
|||||||
const cards = items.filter((item) => !item.parentId);
|
const cards = items.filter((item) => !item.parentId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'flex flex-col gap-y-6 xl:gap-y-8'}>
|
<div className={'flex w-full flex-1 flex-col gap-y-6 xl:gap-y-8'}>
|
||||||
<SitePageHeader
|
<SitePageHeader
|
||||||
title={t('marketing:documentation')}
|
title={t('marketing:documentation')}
|
||||||
subtitle={t('marketing:documentationSubtitle')}
|
subtitle={t('marketing:documentationSubtitle')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={'flex flex-col items-center'}>
|
<div className={'relative flex size-full justify-center overflow-y-auto'}>
|
||||||
<div className={'container mx-auto max-w-5xl'}>
|
<DocsCards cards={cards} />
|
||||||
<DocsCards cards={cards} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
import { GlobalLoader } from '@kit/ui/global-loader';
|
|
||||||
|
|
||||||
export default GlobalLoader;
|
|
||||||
@@ -25,7 +25,7 @@ export default async function RootLayout({
|
|||||||
const nonce = await getCspNonce();
|
const nonce = await getCspNonce();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang={language} className={className}>
|
<html lang={language} className={`${className} overscroll-y-none`}>
|
||||||
<body>
|
<body>
|
||||||
<RootProviders theme={theme} lang={language} nonce={nonce}>
|
<RootProviders theme={theme} lang={language} nonce={nonce}>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
35
apps/web/content/changelog/advanced-analytics-dashboard.mdoc
Normal file
35
apps/web/content/changelog/advanced-analytics-dashboard.mdoc
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
title: "Advanced analytics dashboard"
|
||||||
|
description: "Get deeper insights into your data with our new analytics dashboard featuring real-time metrics, custom reports, and exportable data visualizations."
|
||||||
|
categories: []
|
||||||
|
tags: []
|
||||||
|
image: ""
|
||||||
|
publishedAt: 2025-10-08
|
||||||
|
status: "published"
|
||||||
|
---
|
||||||
|
|
||||||
|
> **Note:** This is mock/placeholder content for demonstration purposes.
|
||||||
|
|
||||||
|
We're excited to introduce our new advanced analytics dashboard that gives you deeper insights into your data and usage patterns.
|
||||||
|
|
||||||
|
## Real-time Metrics
|
||||||
|
|
||||||
|
Monitor your key performance indicators in real-time with live-updating charts and graphs:
|
||||||
|
|
||||||
|
- **Active users** - See who's online and what they're working on
|
||||||
|
- **Response times** - Track system performance metrics
|
||||||
|
- **Usage patterns** - Understand how your team uses the platform
|
||||||
|
|
||||||
|
## Custom Reports
|
||||||
|
|
||||||
|
Create custom reports tailored to your specific needs:
|
||||||
|
|
||||||
|
- Drag-and-drop report builder
|
||||||
|
- Save and share reports with your team
|
||||||
|
- Schedule automated report generation
|
||||||
|
|
||||||
|
## Data Export
|
||||||
|
|
||||||
|
Export your data in multiple formats including CSV, JSON, and Excel. All exports maintain full data integrity and include comprehensive metadata.
|
||||||
|
|
||||||
|
Access the new analytics dashboard from your account settings.
|
||||||
38
apps/web/content/changelog/api-improvements.mdoc
Normal file
38
apps/web/content/changelog/api-improvements.mdoc
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
title: "API improvements and rate limiting"
|
||||||
|
description: "We've upgraded our API with better performance, new endpoints, and smarter rate limiting to help you build more powerful integrations."
|
||||||
|
categories: []
|
||||||
|
tags: []
|
||||||
|
image: ""
|
||||||
|
publishedAt: 2025-09-15
|
||||||
|
status: "published"
|
||||||
|
---
|
||||||
|
|
||||||
|
> **Note:** This is mock/placeholder content for demonstration purposes.
|
||||||
|
|
||||||
|
Our API has received significant upgrades to provide better performance and more flexibility for developers building integrations.
|
||||||
|
|
||||||
|
## New REST Endpoints
|
||||||
|
|
||||||
|
We've added 15 new REST endpoints covering:
|
||||||
|
- Advanced search and filtering
|
||||||
|
- Bulk operations
|
||||||
|
- Webhook management
|
||||||
|
- Team administration
|
||||||
|
|
||||||
|
## GraphQL Support
|
||||||
|
|
||||||
|
You can now query our API using GraphQL, giving you more control over the data you receive and reducing the number of requests needed.
|
||||||
|
|
||||||
|
## Smarter Rate Limiting
|
||||||
|
|
||||||
|
We've implemented a new token bucket rate limiting system that's more fair and predictable:
|
||||||
|
- Burst allowances for occasional spikes
|
||||||
|
- Per-endpoint rate limits
|
||||||
|
- Clear rate limit headers in responses
|
||||||
|
|
||||||
|
## Improved Documentation
|
||||||
|
|
||||||
|
Our API documentation has been completely rewritten with interactive examples, code snippets in multiple languages, and detailed error response information.
|
||||||
|
|
||||||
|
Check out the [API documentation](/docs/api) to get started.
|
||||||
39
apps/web/content/changelog/custom-workflows.mdoc
Normal file
39
apps/web/content/changelog/custom-workflows.mdoc
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
---
|
||||||
|
title: "Custom workflows and automation"
|
||||||
|
description: "Build powerful custom workflows with our new automation engine. Create multi-step workflows that respond to events and automate repetitive tasks."
|
||||||
|
categories: []
|
||||||
|
tags: []
|
||||||
|
image: ""
|
||||||
|
publishedAt: 2025-09-01
|
||||||
|
status: "published"
|
||||||
|
---
|
||||||
|
|
||||||
|
> **Note:** This is mock/placeholder content for demonstration purposes.
|
||||||
|
|
||||||
|
We're launching a powerful new automation engine that lets you create custom workflows without writing code.
|
||||||
|
|
||||||
|
## Visual Workflow Builder
|
||||||
|
|
||||||
|
Our drag-and-drop workflow builder makes it easy to create complex automation:
|
||||||
|
- Connect triggers to actions
|
||||||
|
- Add conditional logic
|
||||||
|
- Transform data between steps
|
||||||
|
- Test workflows before deploying
|
||||||
|
|
||||||
|
## 100+ Integrations
|
||||||
|
|
||||||
|
Connect to popular services including:
|
||||||
|
- Slack, Discord, and Teams for notifications
|
||||||
|
- GitHub, GitLab, and Bitbucket for development workflows
|
||||||
|
- Zapier and Make for extended integration possibilities
|
||||||
|
- Custom webhooks for any service
|
||||||
|
|
||||||
|
## Templates
|
||||||
|
|
||||||
|
Get started quickly with pre-built workflow templates:
|
||||||
|
- Automated project creation
|
||||||
|
- Issue escalation rules
|
||||||
|
- Status update notifications
|
||||||
|
- Daily team summaries
|
||||||
|
|
||||||
|
Workflows are available on Pro and Enterprise plans. Visit the [Automation](/automation) section to start building.
|
||||||
27
apps/web/content/changelog/mobile-app-redesign.mdoc
Normal file
27
apps/web/content/changelog/mobile-app-redesign.mdoc
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
title: "Mobile app redesign"
|
||||||
|
description: "We've refreshed our iOS and Android apps with a new visual design system. Both now use a custom frosted glass material that adds depth and contrast to the UI."
|
||||||
|
categories: []
|
||||||
|
tags: []
|
||||||
|
image: ""
|
||||||
|
publishedAt: 2025-10-16
|
||||||
|
status: "published"
|
||||||
|
---
|
||||||
|
|
||||||
|
> **Note:** This is mock/placeholder content for demonstration purposes.
|
||||||
|
|
||||||
|
We've refreshed our iOS and Android apps with a new visual design system. Both now use a custom frosted glass material that adds depth and contrast to the UI.
|
||||||
|
|
||||||
|
Navigation has been rebuilt with a new bottom toolbar for quick access to core workflows. You'll also now find a "Create Issue" button at the top of every screen.
|
||||||
|
|
||||||
|
## Factory agent
|
||||||
|
|
||||||
|
Users of Factory AI coding agents can now access them directly within the app. Delegate an issue to Factory to spin up a remote workspace and launch a coding agent.
|
||||||
|
|
||||||
|
Factory's remote workspaces allow you to create custom development environments for coding agents, so you can delegate several issues simultaneously to run in consistent, isolated environments.
|
||||||
|
|
||||||
|
## Issue SLAs now available on Business plans
|
||||||
|
|
||||||
|
We've made issue SLAs available on Business plans. You can now set response and resolution time targets for different issue types and priorities. Track compliance and get alerts when issues are at risk of breaching their SLA.
|
||||||
|
|
||||||
|
Update now in the [App Store](https://apps.apple.com) and [Play Store](https://play.google.com/store).
|
||||||
45
apps/web/content/changelog/security-enhancements.mdoc
Normal file
45
apps/web/content/changelog/security-enhancements.mdoc
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
title: "Security enhancements and SSO"
|
||||||
|
description: "Major security updates including SAML SSO, audit logs, and enhanced access controls to keep your data safe and compliant."
|
||||||
|
categories: []
|
||||||
|
tags: []
|
||||||
|
image: ""
|
||||||
|
publishedAt: 2025-08-20
|
||||||
|
status: "published"
|
||||||
|
---
|
||||||
|
|
||||||
|
> **Note:** This is mock/placeholder content for demonstration purposes.
|
||||||
|
|
||||||
|
We've implemented comprehensive security improvements to give you greater control and visibility over your data.
|
||||||
|
|
||||||
|
## SAML Single Sign-On
|
||||||
|
|
||||||
|
Enterprise customers can now configure SAML SSO with popular identity providers:
|
||||||
|
- Okta
|
||||||
|
- Azure Active Directory
|
||||||
|
- Google Workspace
|
||||||
|
- OneLogin
|
||||||
|
|
||||||
|
## Advanced Audit Logs
|
||||||
|
|
||||||
|
Track every action in your account with detailed audit logs:
|
||||||
|
- User authentication events
|
||||||
|
- Data access and modifications
|
||||||
|
- Configuration changes
|
||||||
|
- API requests
|
||||||
|
|
||||||
|
Export logs in JSON or CSV format for compliance and security analysis.
|
||||||
|
|
||||||
|
## Enhanced Access Controls
|
||||||
|
|
||||||
|
New granular permissions allow you to:
|
||||||
|
- Create custom roles with specific permissions
|
||||||
|
- Set IP allowlists for account access
|
||||||
|
- Require 2FA for sensitive operations
|
||||||
|
- Configure session timeout policies
|
||||||
|
|
||||||
|
## Security Webhooks
|
||||||
|
|
||||||
|
Receive real-time notifications about security events like failed login attempts, permission changes, or suspicious activity.
|
||||||
|
|
||||||
|
Contact our sales team to enable Enterprise security features.
|
||||||
31
apps/web/content/changelog/team-collaboration-features.mdoc
Normal file
31
apps/web/content/changelog/team-collaboration-features.mdoc
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
title: "Team collaboration features"
|
||||||
|
description: "Introducing real-time collaboration features including live cursors, team presence, and collaborative editing for a more connected team experience."
|
||||||
|
categories: []
|
||||||
|
tags: []
|
||||||
|
image: ""
|
||||||
|
publishedAt: 2025-09-28
|
||||||
|
status: "published"
|
||||||
|
---
|
||||||
|
|
||||||
|
> **Note:** This is mock/placeholder content for demonstration purposes.
|
||||||
|
|
||||||
|
We're excited to introduce a suite of new team collaboration features designed to help your team work together more effectively.
|
||||||
|
|
||||||
|
## Live Presence
|
||||||
|
|
||||||
|
See who's online and what they're working on in real-time. Team member avatars now appear throughout the app, showing you who's viewing the same page or editing the same document.
|
||||||
|
|
||||||
|
## Collaborative Editing
|
||||||
|
|
||||||
|
Multiple team members can now edit documents simultaneously. See real-time cursors and changes as your teammates type, making collaboration seamless and efficient.
|
||||||
|
|
||||||
|
## Activity Feed
|
||||||
|
|
||||||
|
A new activity feed shows recent changes and updates from your team, helping you stay informed about what's happening across your projects.
|
||||||
|
|
||||||
|
## Team Chat Integration
|
||||||
|
|
||||||
|
We've integrated team chat directly into the platform, so you can discuss issues, share ideas, and coordinate work without leaving the app.
|
||||||
|
|
||||||
|
These features are available now for all team accounts.
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
title: "Authentication"
|
title: "Authentication Overview"
|
||||||
description: "Learn how to set up authentication in your MakerKit application."
|
description: "Learn how to set up authentication in your MakerKit application."
|
||||||
publishedAt: 2024-04-11
|
publishedAt: 2024-04-11
|
||||||
order: 1
|
order: 1
|
||||||
|
|||||||
@@ -0,0 +1,382 @@
|
|||||||
|
---
|
||||||
|
title: "Email & Password"
|
||||||
|
description: "Traditional email and password authentication."
|
||||||
|
publishedAt: 2024-04-11
|
||||||
|
order: 2
|
||||||
|
status: "published"
|
||||||
|
---
|
||||||
|
|
||||||
|
> **Note:** This is mock/placeholder content for demonstration purposes.
|
||||||
|
|
||||||
|
Email and password authentication is the traditional way users sign up and sign in.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Email/password authentication provides:
|
||||||
|
- User registration with email verification
|
||||||
|
- Secure password storage
|
||||||
|
- Password reset functionality
|
||||||
|
- Session management
|
||||||
|
|
||||||
|
## Sign Up Flow
|
||||||
|
|
||||||
|
### User Registration
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { signUpAction } from '~/lib/auth/actions';
|
||||||
|
|
||||||
|
const result = await signUpAction({
|
||||||
|
email: 'user@example.com',
|
||||||
|
password: 'SecurePassword123!',
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Server Action Implementation
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
'use server';
|
||||||
|
|
||||||
|
import { enhanceAction } from '@kit/next/actions';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const SignUpSchema = z.object({
|
||||||
|
email: z.string().email(),
|
||||||
|
password: z.string().min(8),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const signUpAction = enhanceAction(
|
||||||
|
async (data) => {
|
||||||
|
const client = getSupabaseServerClient();
|
||||||
|
|
||||||
|
const { data: authData, error } = await client.auth.signUp({
|
||||||
|
email: data.email,
|
||||||
|
password: data.password,
|
||||||
|
options: {
|
||||||
|
emailRedirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/callback`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
return { success: true, data: authData };
|
||||||
|
},
|
||||||
|
{ schema: SignUpSchema }
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sign Up Component
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { signUpAction } from '../_lib/actions';
|
||||||
|
|
||||||
|
export function SignUpForm() {
|
||||||
|
const { register, handleSubmit, formState: { errors } } = useForm();
|
||||||
|
|
||||||
|
const onSubmit = async (data) => {
|
||||||
|
const result = await signUpAction(data);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
toast.success('Check your email to confirm your account');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<div>
|
||||||
|
<label>Email</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
{...register('email', { required: true })}
|
||||||
|
/>
|
||||||
|
{errors.email && <span>Email is required</span>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label>Password</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
{...register('password', { required: true, minLength: 8 })}
|
||||||
|
/>
|
||||||
|
{errors.password && <span>Password must be 8+ characters</span>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit">Sign Up</button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sign In Flow
|
||||||
|
|
||||||
|
### User Login
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const signInAction = enhanceAction(
|
||||||
|
async (data) => {
|
||||||
|
const client = getSupabaseServerClient();
|
||||||
|
|
||||||
|
const { error } = await client.auth.signInWithPassword({
|
||||||
|
email: data.email,
|
||||||
|
password: data.password,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
redirect('/home');
|
||||||
|
},
|
||||||
|
{ schema: SignInSchema }
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sign In Component
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
export function SignInForm() {
|
||||||
|
const { register, handleSubmit } = useForm();
|
||||||
|
|
||||||
|
const onSubmit = async (data) => {
|
||||||
|
try {
|
||||||
|
await signInAction(data);
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Invalid email or password');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
{...register('email')}
|
||||||
|
placeholder="Email"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
{...register('password')}
|
||||||
|
placeholder="Password"
|
||||||
|
/>
|
||||||
|
<button type="submit">Sign In</button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Email Verification
|
||||||
|
|
||||||
|
### Requiring Email Confirmation
|
||||||
|
|
||||||
|
Configure in Supabase dashboard or config:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// config/auth.config.ts
|
||||||
|
export const authConfig = {
|
||||||
|
requireEmailConfirmation: true,
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Handling Unconfirmed Emails
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const signInAction = enhanceAction(
|
||||||
|
async (data) => {
|
||||||
|
const client = getSupabaseServerClient();
|
||||||
|
|
||||||
|
const { data: authData, error } = await client.auth.signInWithPassword({
|
||||||
|
email: data.email,
|
||||||
|
password: data.password,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
if (error.message.includes('Email not confirmed')) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Please confirm your email before signing in',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
redirect('/home');
|
||||||
|
},
|
||||||
|
{ schema: SignInSchema }
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Password Reset
|
||||||
|
|
||||||
|
### Request Password Reset
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const requestPasswordResetAction = enhanceAction(
|
||||||
|
async (data) => {
|
||||||
|
const client = getSupabaseServerClient();
|
||||||
|
|
||||||
|
const { error } = await client.auth.resetPasswordForEmail(data.email, {
|
||||||
|
redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/reset-password`,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'Check your email for reset instructions',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
schema: z.object({
|
||||||
|
email: z.string().email(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reset Password Form
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
export function PasswordResetRequestForm() {
|
||||||
|
const { register, handleSubmit } = useForm();
|
||||||
|
|
||||||
|
const onSubmit = async (data) => {
|
||||||
|
const result = await requestPasswordResetAction(data);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
toast.success(result.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
{...register('email')}
|
||||||
|
placeholder="Enter your email"
|
||||||
|
/>
|
||||||
|
<button type="submit">Send Reset Link</button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update Password
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const updatePasswordAction = enhanceAction(
|
||||||
|
async (data) => {
|
||||||
|
const client = getSupabaseServerClient();
|
||||||
|
|
||||||
|
const { error } = await client.auth.updateUser({
|
||||||
|
password: data.newPassword,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
redirect('/home');
|
||||||
|
},
|
||||||
|
{
|
||||||
|
schema: z.object({
|
||||||
|
newPassword: z.string().min(8),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Password Requirements
|
||||||
|
|
||||||
|
### Validation Schema
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const PasswordSchema = z
|
||||||
|
.string()
|
||||||
|
.min(8, 'Password must be at least 8 characters')
|
||||||
|
.regex(/[A-Z]/, 'Password must contain an uppercase letter')
|
||||||
|
.regex(/[a-z]/, 'Password must contain a lowercase letter')
|
||||||
|
.regex(/[0-9]/, 'Password must contain a number')
|
||||||
|
.regex(/[^A-Za-z0-9]/, 'Password must contain a special character');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Password Strength Indicator
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
export function PasswordInput() {
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const strength = calculatePasswordStrength(password);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
<div className="flex gap-1">
|
||||||
|
{[1, 2, 3, 4].map((level) => (
|
||||||
|
<div
|
||||||
|
key={level}
|
||||||
|
className={cn(
|
||||||
|
'h-1 flex-1 rounded',
|
||||||
|
strength >= level ? 'bg-green-500' : 'bg-gray-200'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<span className="text-sm">
|
||||||
|
{strength === 4 && 'Strong password'}
|
||||||
|
{strength === 3 && 'Good password'}
|
||||||
|
{strength === 2 && 'Fair password'}
|
||||||
|
{strength === 1 && 'Weak password'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Session Management
|
||||||
|
|
||||||
|
### Checking Authentication Status
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||||
|
|
||||||
|
export async function requireAuth() {
|
||||||
|
const client = getSupabaseServerClient();
|
||||||
|
const { data: { user } } = await client.auth.getUser();
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
redirect('/auth/sign-in');
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sign Out
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const signOutAction = enhanceAction(
|
||||||
|
async () => {
|
||||||
|
const client = getSupabaseServerClient();
|
||||||
|
await client.auth.signOut();
|
||||||
|
redirect('/auth/sign-in');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Best Practices
|
||||||
|
|
||||||
|
1. **Enforce strong passwords** - Minimum 8 characters, mixed case, numbers, symbols
|
||||||
|
2. **Rate limit login attempts** - Prevent brute force attacks
|
||||||
|
3. **Use HTTPS only** - Encrypt data in transit
|
||||||
|
4. **Enable email verification** - Confirm email ownership
|
||||||
|
5. **Implement account lockout** - After failed attempts
|
||||||
|
6. **Log authentication events** - Track sign-ins and failures
|
||||||
|
7. **Support 2FA** - Add extra security layer
|
||||||
392
apps/web/content/documentation/authentication/magic-links.mdoc
Normal file
392
apps/web/content/documentation/authentication/magic-links.mdoc
Normal file
@@ -0,0 +1,392 @@
|
|||||||
|
---
|
||||||
|
title: "Magic Links"
|
||||||
|
description: "Passwordless authentication with email magic links."
|
||||||
|
publishedAt: 2024-04-11
|
||||||
|
order: 4
|
||||||
|
status: "published"
|
||||||
|
---
|
||||||
|
|
||||||
|
> **Note:** This is mock/placeholder content for demonstration purposes.
|
||||||
|
|
||||||
|
Magic links provide passwordless authentication by sending a one-time link to the user's email.
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
1. User enters their email address
|
||||||
|
2. System sends an email with a unique link
|
||||||
|
3. User clicks the link in their email
|
||||||
|
4. User is automatically signed in
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
|
||||||
|
- **No password to remember** - Better UX
|
||||||
|
- **More secure** - No password to steal
|
||||||
|
- **Lower friction** - Faster sign-up process
|
||||||
|
- **Email verification** - Confirms email ownership
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
### Magic Link Form
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { sendMagicLinkAction } from '../_lib/actions';
|
||||||
|
|
||||||
|
export function MagicLinkForm() {
|
||||||
|
const { register, handleSubmit, formState: { isSubmitting } } = useForm();
|
||||||
|
const [sent, setSent] = useState(false);
|
||||||
|
|
||||||
|
const onSubmit = async (data) => {
|
||||||
|
const result = await sendMagicLinkAction(data);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
setSent(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (sent) {
|
||||||
|
return (
|
||||||
|
<div className="text-center">
|
||||||
|
<h2>Check your email</h2>
|
||||||
|
<p>We've sent you a magic link to sign in.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<div>
|
||||||
|
<label>Email address</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
{...register('email', { required: true })}
|
||||||
|
placeholder="you@example.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" disabled={isSubmitting}>
|
||||||
|
{isSubmitting ? 'Sending...' : 'Send magic link'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Server Action
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
'use server';
|
||||||
|
|
||||||
|
import { enhanceAction } from '@kit/next/actions';
|
||||||
|
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const sendMagicLinkAction = enhanceAction(
|
||||||
|
async (data) => {
|
||||||
|
const client = getSupabaseServerClient();
|
||||||
|
const origin = process.env.NEXT_PUBLIC_SITE_URL!;
|
||||||
|
|
||||||
|
const { error } = await client.auth.signInWithOtp({
|
||||||
|
email: data.email,
|
||||||
|
options: {
|
||||||
|
emailRedirectTo: `${origin}/auth/callback`,
|
||||||
|
shouldCreateUser: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'Check your email for the magic link',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
schema: z.object({
|
||||||
|
email: z.string().email(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Enable in Supabase
|
||||||
|
|
||||||
|
1. Go to **Authentication** → **Providers** → **Email**
|
||||||
|
2. Enable "Enable Email Provider"
|
||||||
|
3. Enable "Enable Email Confirmations"
|
||||||
|
|
||||||
|
### Configure Email Template
|
||||||
|
|
||||||
|
Customize the magic link email in Supabase Dashboard:
|
||||||
|
|
||||||
|
1. Go to **Authentication** → **Email Templates**
|
||||||
|
2. Select "Magic Link"
|
||||||
|
3. Customize the template:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<h2>Sign in to {{ .SiteURL }}</h2>
|
||||||
|
<p>Click the link below to sign in:</p>
|
||||||
|
<p><a href="{{ .ConfirmationURL }}">Sign in</a></p>
|
||||||
|
<p>This link expires in {{ .TokenExpiryHours }} hours.</p>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Callback Handler
|
||||||
|
|
||||||
|
Handle the magic link callback:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// app/auth/callback/route.ts
|
||||||
|
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs';
|
||||||
|
import { cookies } from 'next/headers';
|
||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
export async function GET(request: Request) {
|
||||||
|
const requestUrl = new URL(request.url);
|
||||||
|
const token_hash = requestUrl.searchParams.get('token_hash');
|
||||||
|
const type = requestUrl.searchParams.get('type');
|
||||||
|
|
||||||
|
if (token_hash && type === 'magiclink') {
|
||||||
|
const cookieStore = cookies();
|
||||||
|
const supabase = createRouteHandlerClient({ cookies: () => cookieStore });
|
||||||
|
|
||||||
|
const { error } = await supabase.auth.verifyOtp({
|
||||||
|
token_hash,
|
||||||
|
type: 'magiclink',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!error) {
|
||||||
|
return NextResponse.redirect(new URL('/home', request.url));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return error if verification failed
|
||||||
|
return NextResponse.redirect(
|
||||||
|
new URL('/auth/sign-in?error=invalid_link', request.url)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Advanced Features
|
||||||
|
|
||||||
|
### Custom Redirect
|
||||||
|
|
||||||
|
Specify where users go after clicking the link:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
await client.auth.signInWithOtp({
|
||||||
|
email: data.email,
|
||||||
|
options: {
|
||||||
|
emailRedirectTo: `${origin}/onboarding`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Disable Auto Sign-Up
|
||||||
|
|
||||||
|
Require users to sign up first:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
await client.auth.signInWithOtp({
|
||||||
|
email: data.email,
|
||||||
|
options: {
|
||||||
|
shouldCreateUser: false, // Don't create new users
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Token Expiry
|
||||||
|
|
||||||
|
Configure link expiration (default: 1 hour):
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- In Supabase SQL Editor
|
||||||
|
ALTER TABLE auth.users
|
||||||
|
SET default_token_lifetime = '15 minutes';
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rate Limiting
|
||||||
|
|
||||||
|
Prevent abuse by rate limiting magic link requests:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ratelimit } from '~/lib/rate-limit';
|
||||||
|
|
||||||
|
export const sendMagicLinkAction = enhanceAction(
|
||||||
|
async (data, user, request) => {
|
||||||
|
// Rate limit by IP
|
||||||
|
const ip = request.headers.get('x-forwarded-for') || 'unknown';
|
||||||
|
const { success } = await ratelimit.limit(ip);
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
throw new Error('Too many requests. Please try again later.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = getSupabaseServerClient();
|
||||||
|
|
||||||
|
await client.auth.signInWithOtp({
|
||||||
|
email: data.email,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
},
|
||||||
|
{ schema: EmailSchema }
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
### Link Expiration
|
||||||
|
|
||||||
|
Magic links should expire quickly:
|
||||||
|
- Default: 1 hour
|
||||||
|
- Recommended: 15-30 minutes for production
|
||||||
|
- Shorter for sensitive actions
|
||||||
|
|
||||||
|
### One-Time Use
|
||||||
|
|
||||||
|
Links should be invalidated after use:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Supabase handles this automatically
|
||||||
|
// Each link can only be used once
|
||||||
|
```
|
||||||
|
|
||||||
|
### Email Verification
|
||||||
|
|
||||||
|
Ensure emails are verified:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { data: { user } } = await client.auth.getUser();
|
||||||
|
|
||||||
|
if (!user.email_confirmed_at) {
|
||||||
|
redirect('/verify-email');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## User Experience
|
||||||
|
|
||||||
|
### Loading State
|
||||||
|
|
||||||
|
Show feedback while sending:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
export function MagicLinkForm() {
|
||||||
|
const [status, setStatus] = useState<'idle' | 'sending' | 'sent'>('idle');
|
||||||
|
|
||||||
|
const onSubmit = async (data) => {
|
||||||
|
setStatus('sending');
|
||||||
|
await sendMagicLinkAction(data);
|
||||||
|
setStatus('sent');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{status === 'idle' && <EmailForm onSubmit={onSubmit} />}
|
||||||
|
{status === 'sending' && <SendingMessage />}
|
||||||
|
{status === 'sent' && <CheckEmailMessage />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Resend Link
|
||||||
|
|
||||||
|
Allow users to request a new link:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
export function ResendMagicLink({ email }: { email: string }) {
|
||||||
|
const [canResend, setCanResend] = useState(false);
|
||||||
|
const [countdown, setCountdown] = useState(60);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (countdown > 0) {
|
||||||
|
const timer = setTimeout(() => setCountdown(countdown - 1), 1000);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
} else {
|
||||||
|
setCanResend(true);
|
||||||
|
}
|
||||||
|
}, [countdown]);
|
||||||
|
|
||||||
|
const handleResend = async () => {
|
||||||
|
await sendMagicLinkAction({ email });
|
||||||
|
setCountdown(60);
|
||||||
|
setCanResend(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button onClick={handleResend} disabled={!canResend}>
|
||||||
|
{canResend ? 'Resend link' : `Resend in ${countdown}s`}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Email Deliverability
|
||||||
|
|
||||||
|
### SPF, DKIM, DMARC
|
||||||
|
|
||||||
|
Configure email authentication:
|
||||||
|
1. Add SPF record to DNS
|
||||||
|
2. Enable DKIM signing
|
||||||
|
3. Set up DMARC policy
|
||||||
|
|
||||||
|
### Custom Email Domain
|
||||||
|
|
||||||
|
Use your own domain for better deliverability:
|
||||||
|
|
||||||
|
1. Go to **Project Settings** → **Auth**
|
||||||
|
2. Configure custom SMTP
|
||||||
|
3. Verify domain ownership
|
||||||
|
|
||||||
|
### Monitor Bounces
|
||||||
|
|
||||||
|
Track email delivery issues:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Handle email bounces
|
||||||
|
export async function handleEmailBounce(email: string) {
|
||||||
|
await client.from('email_bounces').insert({
|
||||||
|
email,
|
||||||
|
bounced_at: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Notify user via other channel
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Local Development
|
||||||
|
|
||||||
|
In development, emails go to InBucket:
|
||||||
|
|
||||||
|
```
|
||||||
|
http://localhost:54324
|
||||||
|
```
|
||||||
|
|
||||||
|
Check this URL to see magic link emails during testing.
|
||||||
|
|
||||||
|
### Test Mode
|
||||||
|
|
||||||
|
Create a test link without sending email:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.log('Magic link URL:', confirmationUrl);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Clear communication** - Tell users to check spam
|
||||||
|
2. **Short expiry** - 15-30 minutes for security
|
||||||
|
3. **Rate limiting** - Prevent abuse
|
||||||
|
4. **Fallback option** - Offer password auth as backup
|
||||||
|
5. **Custom domain** - Better deliverability
|
||||||
|
6. **Monitor delivery** - Track bounces and failures
|
||||||
|
7. **Resend option** - Let users request new link
|
||||||
|
8. **Mobile-friendly** - Ensure links work on mobile
|
||||||
@@ -0,0 +1,395 @@
|
|||||||
|
---
|
||||||
|
title: "OAuth"
|
||||||
|
description: "Sign in with Google, GitHub, and other OAuth providers."
|
||||||
|
publishedAt: 2024-04-11
|
||||||
|
order: 3
|
||||||
|
status: "published"
|
||||||
|
---
|
||||||
|
|
||||||
|
> **Note:** This is mock/placeholder content for demonstration purposes.
|
||||||
|
|
||||||
|
Allow users to sign in with their existing accounts from Google, GitHub, and other providers.
|
||||||
|
|
||||||
|
## Supported Providers
|
||||||
|
|
||||||
|
Supabase supports many OAuth providers:
|
||||||
|
- Google
|
||||||
|
- GitHub
|
||||||
|
- GitLab
|
||||||
|
- Bitbucket
|
||||||
|
- Azure
|
||||||
|
- Facebook
|
||||||
|
- Twitter
|
||||||
|
- Discord
|
||||||
|
- Slack
|
||||||
|
- And more...
|
||||||
|
|
||||||
|
## Setting Up OAuth
|
||||||
|
|
||||||
|
### Configure in Supabase Dashboard
|
||||||
|
|
||||||
|
1. Go to **Authentication** → **Providers**
|
||||||
|
2. Enable your desired provider (e.g., Google)
|
||||||
|
3. Add your OAuth credentials:
|
||||||
|
- **Client ID**
|
||||||
|
- **Client Secret**
|
||||||
|
- **Redirect URL**: `https://your-project.supabase.co/auth/v1/callback`
|
||||||
|
|
||||||
|
### Google OAuth Setup
|
||||||
|
|
||||||
|
1. Go to [Google Cloud Console](https://console.cloud.google.com)
|
||||||
|
2. Create a new project or select existing
|
||||||
|
3. Enable Google+ API
|
||||||
|
4. Create OAuth 2.0 credentials
|
||||||
|
5. Add authorized redirect URIs:
|
||||||
|
- Production: `https://your-project.supabase.co/auth/v1/callback`
|
||||||
|
- Development: `http://localhost:54321/auth/v1/callback`
|
||||||
|
|
||||||
|
### GitHub OAuth Setup
|
||||||
|
|
||||||
|
1. Go to GitHub Settings → Developer Settings → OAuth Apps
|
||||||
|
2. Click "New OAuth App"
|
||||||
|
3. Fill in details:
|
||||||
|
- **Application name**: Your App
|
||||||
|
- **Homepage URL**: `https://yourapp.com`
|
||||||
|
- **Authorization callback URL**: `https://your-project.supabase.co/auth/v1/callback`
|
||||||
|
4. Copy Client ID and Client Secret to Supabase
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
### OAuth Sign In Button
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { signInWithOAuthAction } from '../_lib/actions';
|
||||||
|
|
||||||
|
export function OAuthButtons() {
|
||||||
|
const handleGoogleSignIn = async () => {
|
||||||
|
await signInWithOAuthAction('google');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGitHubSignIn = async () => {
|
||||||
|
await signInWithOAuthAction('github');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<button
|
||||||
|
onClick={handleGoogleSignIn}
|
||||||
|
className="w-full flex items-center justify-center gap-2 border rounded-lg p-2"
|
||||||
|
>
|
||||||
|
<GoogleIcon />
|
||||||
|
Continue with Google
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleGitHubSignIn}
|
||||||
|
className="w-full flex items-center justify-center gap-2 border rounded-lg p-2"
|
||||||
|
>
|
||||||
|
<GitHubIcon />
|
||||||
|
Continue with GitHub
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Server Action
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
'use server';
|
||||||
|
|
||||||
|
import { enhanceAction } from '@kit/next/actions';
|
||||||
|
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const OAuthProviderSchema = z.enum([
|
||||||
|
'google',
|
||||||
|
'github',
|
||||||
|
'gitlab',
|
||||||
|
'azure',
|
||||||
|
'facebook',
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const signInWithOAuthAction = enhanceAction(
|
||||||
|
async (provider) => {
|
||||||
|
const client = getSupabaseServerClient();
|
||||||
|
const origin = process.env.NEXT_PUBLIC_SITE_URL!;
|
||||||
|
|
||||||
|
const { data, error } = await client.auth.signInWithOAuth({
|
||||||
|
provider,
|
||||||
|
options: {
|
||||||
|
redirectTo: `${origin}/auth/callback`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
// Redirect to OAuth provider
|
||||||
|
redirect(data.url);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
schema: OAuthProviderSchema,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### OAuth Callback Handler
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// app/auth/callback/route.ts
|
||||||
|
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs';
|
||||||
|
import { cookies } from 'next/headers';
|
||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
export async function GET(request: Request) {
|
||||||
|
const requestUrl = new URL(request.url);
|
||||||
|
const code = requestUrl.searchParams.get('code');
|
||||||
|
|
||||||
|
if (code) {
|
||||||
|
const cookieStore = cookies();
|
||||||
|
const supabase = createRouteHandlerClient({ cookies: () => cookieStore });
|
||||||
|
|
||||||
|
await supabase.auth.exchangeCodeForSession(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect to home page
|
||||||
|
return NextResponse.redirect(new URL('/home', request.url));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Customizing OAuth Flow
|
||||||
|
|
||||||
|
### Scopes
|
||||||
|
|
||||||
|
Request specific permissions:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
await client.auth.signInWithOAuth({
|
||||||
|
provider: 'google',
|
||||||
|
options: {
|
||||||
|
scopes: 'email profile https://www.googleapis.com/auth/calendar',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Query Parameters
|
||||||
|
|
||||||
|
Pass custom parameters:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
await client.auth.signInWithOAuth({
|
||||||
|
provider: 'azure',
|
||||||
|
options: {
|
||||||
|
queryParams: {
|
||||||
|
prompt: 'consent',
|
||||||
|
access_type: 'offline',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Skip Browser Redirect
|
||||||
|
|
||||||
|
For mobile apps or custom flows:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { data } = await client.auth.signInWithOAuth({
|
||||||
|
provider: 'google',
|
||||||
|
options: {
|
||||||
|
skipBrowserRedirect: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// data.url contains the OAuth URL
|
||||||
|
// Handle redirect manually
|
||||||
|
```
|
||||||
|
|
||||||
|
## Account Linking
|
||||||
|
|
||||||
|
### Linking Additional Providers
|
||||||
|
|
||||||
|
Allow users to link multiple OAuth accounts:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const linkOAuthProviderAction = enhanceAction(
|
||||||
|
async (provider) => {
|
||||||
|
const client = getSupabaseServerClient();
|
||||||
|
const user = await requireAuth();
|
||||||
|
|
||||||
|
const { data, error } = await client.auth.linkIdentity({
|
||||||
|
provider,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
redirect(data.url);
|
||||||
|
},
|
||||||
|
{ schema: OAuthProviderSchema, auth: true }
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Unlinking Providers
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const unlinkOAuthProviderAction = enhanceAction(
|
||||||
|
async ({ provider, identityId }) => {
|
||||||
|
const client = getSupabaseServerClient();
|
||||||
|
|
||||||
|
const { error } = await client.auth.unlinkIdentity({
|
||||||
|
identity_id: identityId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
revalidatePath('/settings/security');
|
||||||
|
},
|
||||||
|
{
|
||||||
|
schema: z.object({
|
||||||
|
provider: z.string(),
|
||||||
|
identityId: z.string(),
|
||||||
|
}),
|
||||||
|
auth: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Viewing Linked Identities
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||||
|
|
||||||
|
export async function getLinkedIdentities() {
|
||||||
|
const client = getSupabaseServerClient();
|
||||||
|
const { data: { user } } = await client.auth.getUser();
|
||||||
|
|
||||||
|
return user?.identities || [];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## User Data from OAuth
|
||||||
|
|
||||||
|
### Accessing Provider Data
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { data: { user } } = await client.auth.getUser();
|
||||||
|
|
||||||
|
// User metadata from provider
|
||||||
|
const {
|
||||||
|
full_name,
|
||||||
|
avatar_url,
|
||||||
|
email,
|
||||||
|
} = user.user_metadata;
|
||||||
|
|
||||||
|
// Provider-specific data
|
||||||
|
const identities = user.identities || [];
|
||||||
|
const googleIdentity = identities.find(i => i.provider === 'google');
|
||||||
|
|
||||||
|
console.log(googleIdentity?.identity_data);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Storing Additional Data
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const completeOAuthProfileAction = enhanceAction(
|
||||||
|
async (data) => {
|
||||||
|
const client = getSupabaseServerClient();
|
||||||
|
const user = await requireAuth();
|
||||||
|
|
||||||
|
// Update user metadata
|
||||||
|
await client.auth.updateUser({
|
||||||
|
data: {
|
||||||
|
username: data.username,
|
||||||
|
bio: data.bio,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update profile in database
|
||||||
|
await client.from('profiles').upsert({
|
||||||
|
id: user.id,
|
||||||
|
username: data.username,
|
||||||
|
bio: data.bio,
|
||||||
|
avatar_url: user.user_metadata.avatar_url,
|
||||||
|
});
|
||||||
|
|
||||||
|
redirect('/home');
|
||||||
|
},
|
||||||
|
{ schema: ProfileSchema, auth: true }
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Enable OAuth in Config
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// config/auth.config.ts
|
||||||
|
export const authConfig = {
|
||||||
|
providers: {
|
||||||
|
emailPassword: true,
|
||||||
|
oAuth: ['google', 'github'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Conditional Rendering
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { authConfig } from '~/config/auth.config';
|
||||||
|
|
||||||
|
export function AuthProviders() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{authConfig.providers.emailPassword && <EmailPasswordForm />}
|
||||||
|
|
||||||
|
{authConfig.providers.oAuth?.includes('google') && (
|
||||||
|
<GoogleSignInButton />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{authConfig.providers.oAuth?.includes('github') && (
|
||||||
|
<GitHubSignInButton />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Redirect URI Mismatch
|
||||||
|
|
||||||
|
Ensure redirect URIs match exactly:
|
||||||
|
- Check Supabase Dashboard → Authentication → URL Configuration
|
||||||
|
- Verify OAuth app settings in provider console
|
||||||
|
- Use exact URLs (including http/https)
|
||||||
|
|
||||||
|
### Missing Email
|
||||||
|
|
||||||
|
Some providers don't share email by default:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { data: { user } } = await client.auth.getUser();
|
||||||
|
|
||||||
|
if (!user.email) {
|
||||||
|
// Request email separately or prompt user
|
||||||
|
redirect('/auth/complete-profile');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rate Limiting
|
||||||
|
|
||||||
|
OAuth providers may rate limit requests:
|
||||||
|
- Cache OAuth tokens appropriately
|
||||||
|
- Don't make excessive authorization requests
|
||||||
|
- Handle rate limit errors gracefully
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Request minimum scopes** - Only ask for what you need
|
||||||
|
2. **Handle errors gracefully** - OAuth can fail for many reasons
|
||||||
|
3. **Verify email addresses** - Some providers don't verify emails
|
||||||
|
4. **Support account linking** - Let users connect multiple providers
|
||||||
|
5. **Provide fallback** - Always offer email/password as backup
|
||||||
|
6. **Log OAuth events** - Track sign-ins and linking attempts
|
||||||
|
7. **Test thoroughly** - Test with real provider accounts
|
||||||
15
apps/web/content/documentation/billing/billing.mdoc
Normal file
15
apps/web/content/documentation/billing/billing.mdoc
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
title: "Billing & Payments"
|
||||||
|
description: "Learn how to set up billing and payment processing in your MakerKit application."
|
||||||
|
publishedAt: 2024-04-11
|
||||||
|
order: 5
|
||||||
|
status: "published"
|
||||||
|
---
|
||||||
|
|
||||||
|
MakerKit integrates with popular payment providers to handle subscriptions and billing.
|
||||||
|
|
||||||
|
This section covers:
|
||||||
|
- Setting up payment providers
|
||||||
|
- Managing subscriptions
|
||||||
|
- Handling webhooks
|
||||||
|
- Pricing plans and tiers
|
||||||
186
apps/web/content/documentation/billing/pricing-plans.mdoc
Normal file
186
apps/web/content/documentation/billing/pricing-plans.mdoc
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
---
|
||||||
|
title: "Pricing Plans"
|
||||||
|
description: "How to configure and customize pricing plans for your SaaS application."
|
||||||
|
publishedAt: 2024-04-11
|
||||||
|
order: 1
|
||||||
|
status: "published"
|
||||||
|
---
|
||||||
|
|
||||||
|
> **Note:** This is mock/placeholder content for demonstration purposes.
|
||||||
|
|
||||||
|
Configure your pricing structure to match your business model.
|
||||||
|
|
||||||
|
## Plan Structure
|
||||||
|
|
||||||
|
Each pricing plan consists of:
|
||||||
|
- **ID** - Unique identifier
|
||||||
|
- **Name** - Display name
|
||||||
|
- **Price** - Amount in your currency
|
||||||
|
- **Interval** - Billing frequency (month, year)
|
||||||
|
- **Features** - List of included features
|
||||||
|
- **Limits** - Usage constraints
|
||||||
|
|
||||||
|
## Example Configuration
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// config/billing.config.ts
|
||||||
|
export const billingConfig = {
|
||||||
|
provider: 'stripe', // or 'paddle'
|
||||||
|
currency: 'usd',
|
||||||
|
plans: [
|
||||||
|
{
|
||||||
|
id: 'free',
|
||||||
|
name: 'Free',
|
||||||
|
description: 'Perfect for getting started',
|
||||||
|
price: 0,
|
||||||
|
features: [
|
||||||
|
'5 projects',
|
||||||
|
'Basic analytics',
|
||||||
|
'Community support',
|
||||||
|
],
|
||||||
|
limits: {
|
||||||
|
projects: 5,
|
||||||
|
members: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'starter',
|
||||||
|
name: 'Starter',
|
||||||
|
description: 'For small teams',
|
||||||
|
price: 19,
|
||||||
|
interval: 'month',
|
||||||
|
features: [
|
||||||
|
'25 projects',
|
||||||
|
'Advanced analytics',
|
||||||
|
'Email support',
|
||||||
|
'API access',
|
||||||
|
],
|
||||||
|
limits: {
|
||||||
|
projects: 25,
|
||||||
|
members: 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'pro',
|
||||||
|
name: 'Professional',
|
||||||
|
description: 'For growing businesses',
|
||||||
|
price: 49,
|
||||||
|
interval: 'month',
|
||||||
|
popular: true,
|
||||||
|
features: [
|
||||||
|
'Unlimited projects',
|
||||||
|
'Advanced analytics',
|
||||||
|
'Priority support',
|
||||||
|
'API access',
|
||||||
|
'Custom integrations',
|
||||||
|
],
|
||||||
|
limits: {
|
||||||
|
projects: -1, // unlimited
|
||||||
|
members: 20,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Feature Gating
|
||||||
|
|
||||||
|
Restrict features based on subscription plan:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { hasFeature } from '~/lib/billing/features';
|
||||||
|
|
||||||
|
async function createProject() {
|
||||||
|
const subscription = await getSubscription(accountId);
|
||||||
|
|
||||||
|
if (!hasFeature(subscription, 'api_access')) {
|
||||||
|
throw new Error('API access requires Pro plan');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create project
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage Limits
|
||||||
|
|
||||||
|
Enforce usage limits per plan:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { checkLimit } from '~/lib/billing/limits';
|
||||||
|
|
||||||
|
async function addTeamMember() {
|
||||||
|
const canAdd = await checkLimit(accountId, 'members');
|
||||||
|
|
||||||
|
if (!canAdd) {
|
||||||
|
throw new Error('Member limit reached. Upgrade to add more members.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add member
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Annual Billing
|
||||||
|
|
||||||
|
Offer discounted annual plans:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
id: 'pro-annual',
|
||||||
|
name: 'Professional Annual',
|
||||||
|
price: 470, // ~20% discount
|
||||||
|
interval: 'year',
|
||||||
|
discount: '20% off',
|
||||||
|
features: [ /* same as monthly */ ],
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Trial Periods
|
||||||
|
|
||||||
|
Configure free trial periods:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const trialConfig = {
|
||||||
|
enabled: true,
|
||||||
|
duration: 14, // days
|
||||||
|
plans: ['starter', 'pro'], // plans eligible for trial
|
||||||
|
requirePaymentMethod: true,
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Customizing the Pricing Page
|
||||||
|
|
||||||
|
The pricing page automatically generates from your configuration:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { billingConfig } from '~/config/billing.config';
|
||||||
|
|
||||||
|
export default function PricingPage() {
|
||||||
|
return (
|
||||||
|
<PricingTable
|
||||||
|
plans={billingConfig.plans}
|
||||||
|
currency={billingConfig.currency}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Adding Custom Features
|
||||||
|
|
||||||
|
Extend plan features with custom attributes:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
id: 'enterprise',
|
||||||
|
name: 'Enterprise',
|
||||||
|
price: null, // Contact for pricing
|
||||||
|
custom: true,
|
||||||
|
features: [
|
||||||
|
'Everything in Pro',
|
||||||
|
'Dedicated support',
|
||||||
|
'Custom SLA',
|
||||||
|
'On-premise deployment',
|
||||||
|
],
|
||||||
|
ctaText: 'Contact Sales',
|
||||||
|
ctaUrl: '/contact',
|
||||||
|
}
|
||||||
|
```
|
||||||
143
apps/web/content/documentation/billing/subscriptions.mdoc
Normal file
143
apps/web/content/documentation/billing/subscriptions.mdoc
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
---
|
||||||
|
title: "Billing Overview"
|
||||||
|
description: "Learn how to manage subscriptions and billing in your application."
|
||||||
|
publishedAt: 2024-04-11
|
||||||
|
order: 0
|
||||||
|
status: "published"
|
||||||
|
---
|
||||||
|
|
||||||
|
> **Note:** This is mock/placeholder content for demonstration purposes.
|
||||||
|
|
||||||
|
The billing system supports subscription-based pricing with multiple tiers and payment providers.
|
||||||
|
|
||||||
|
## Supported Providers
|
||||||
|
|
||||||
|
### Stripe
|
||||||
|
Industry-standard payment processing with comprehensive features:
|
||||||
|
- Credit card payments
|
||||||
|
- Subscription management
|
||||||
|
- Invoice generation
|
||||||
|
- Tax calculation
|
||||||
|
- Customer portal
|
||||||
|
|
||||||
|
### Paddle
|
||||||
|
Merchant of record solution that handles:
|
||||||
|
- Global tax compliance
|
||||||
|
- Payment processing
|
||||||
|
- Subscription billing
|
||||||
|
- Revenue recovery
|
||||||
|
|
||||||
|
## Subscription Tiers
|
||||||
|
|
||||||
|
Define your subscription tiers in the billing configuration:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const plans = [
|
||||||
|
{
|
||||||
|
id: 'free',
|
||||||
|
name: 'Free',
|
||||||
|
price: 0,
|
||||||
|
features: ['Feature 1', 'Feature 2'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'pro',
|
||||||
|
name: 'Professional',
|
||||||
|
price: 29,
|
||||||
|
interval: 'month',
|
||||||
|
features: ['All Free features', 'Feature 3', 'Feature 4'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'enterprise',
|
||||||
|
name: 'Enterprise',
|
||||||
|
price: 99,
|
||||||
|
interval: 'month',
|
||||||
|
features: ['All Pro features', 'Feature 5', 'Priority support'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
## Subscription Lifecycle
|
||||||
|
|
||||||
|
1. **Customer selects plan** - User chooses subscription tier
|
||||||
|
2. **Payment processed** - Provider handles payment collection
|
||||||
|
3. **Webhook received** - Your app receives confirmation
|
||||||
|
4. **Subscription activated** - User gains access to features
|
||||||
|
5. **Recurring billing** - Automatic renewal each period
|
||||||
|
6. **Cancellation** - User can cancel anytime
|
||||||
|
|
||||||
|
## Managing Subscriptions
|
||||||
|
|
||||||
|
### Creating a Subscription
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createCheckoutSession } from '~/lib/billing/checkout';
|
||||||
|
|
||||||
|
const session = await createCheckoutSession({
|
||||||
|
accountId: user.accountId,
|
||||||
|
planId: 'pro',
|
||||||
|
returnUrl: '/dashboard',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Redirect user to payment page
|
||||||
|
redirect(session.url);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Checking Subscription Status
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { getSubscription } from '~/lib/billing/subscription';
|
||||||
|
|
||||||
|
const subscription = await getSubscription(accountId);
|
||||||
|
|
||||||
|
if (subscription.status === 'active') {
|
||||||
|
// User has active subscription
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Canceling a Subscription
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { cancelSubscription } from '~/lib/billing/subscription';
|
||||||
|
|
||||||
|
await cancelSubscription(subscriptionId);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Webhook Handling
|
||||||
|
|
||||||
|
Webhooks notify your application of billing events:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
const signature = request.headers.get('stripe-signature');
|
||||||
|
const payload = await request.text();
|
||||||
|
|
||||||
|
const event = stripe.webhooks.constructEvent(
|
||||||
|
payload,
|
||||||
|
signature,
|
||||||
|
process.env.STRIPE_WEBHOOK_SECRET
|
||||||
|
);
|
||||||
|
|
||||||
|
switch (event.type) {
|
||||||
|
case 'customer.subscription.created':
|
||||||
|
await handleSubscriptionCreated(event.data.object);
|
||||||
|
break;
|
||||||
|
case 'customer.subscription.updated':
|
||||||
|
await handleSubscriptionUpdated(event.data.object);
|
||||||
|
break;
|
||||||
|
case 'customer.subscription.deleted':
|
||||||
|
await handleSubscriptionCanceled(event.data.object);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response('OK');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Use test mode credentials for development:
|
||||||
|
- Test card: 4242 4242 4242 4242
|
||||||
|
- Any future expiry date
|
||||||
|
- Any CVC
|
||||||
|
|
||||||
|
All test transactions will appear in your provider's test dashboard.
|
||||||
194
apps/web/content/documentation/billing/webhooks.mdoc
Normal file
194
apps/web/content/documentation/billing/webhooks.mdoc
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
---
|
||||||
|
title: "Webhook Integration"
|
||||||
|
description: "Setting up and handling payment provider webhooks for subscription events."
|
||||||
|
publishedAt: 2024-04-11
|
||||||
|
order: 2
|
||||||
|
status: "published"
|
||||||
|
---
|
||||||
|
|
||||||
|
> **Note:** This is mock/placeholder content for demonstration purposes.
|
||||||
|
|
||||||
|
Webhooks notify your application when billing events occur, ensuring your app stays synchronized with your payment provider.
|
||||||
|
|
||||||
|
## Why Webhooks?
|
||||||
|
|
||||||
|
Webhooks are essential for:
|
||||||
|
- **Real-time updates** - Instant notification of payment events
|
||||||
|
- **Reliability** - Handles events even if users close their browser
|
||||||
|
- **Security** - Server-to-server communication
|
||||||
|
- **Automation** - Automatic subscription status updates
|
||||||
|
|
||||||
|
## Webhook Endpoint
|
||||||
|
|
||||||
|
Your webhook endpoint receives events from the payment provider:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// app/api/billing/webhook/route.ts
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
const body = await request.text();
|
||||||
|
const signature = request.headers.get('stripe-signature');
|
||||||
|
|
||||||
|
// Verify webhook signature
|
||||||
|
const event = stripe.webhooks.constructEvent(
|
||||||
|
body,
|
||||||
|
signature,
|
||||||
|
process.env.STRIPE_WEBHOOK_SECRET
|
||||||
|
);
|
||||||
|
|
||||||
|
// Handle the event
|
||||||
|
await handleBillingEvent(event);
|
||||||
|
|
||||||
|
return new Response('OK', { status: 200 });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Events
|
||||||
|
|
||||||
|
### Subscription Created
|
||||||
|
```typescript
|
||||||
|
case 'customer.subscription.created':
|
||||||
|
await prisma.subscription.create({
|
||||||
|
data: {
|
||||||
|
id: event.data.object.id,
|
||||||
|
accountId: event.data.object.metadata.accountId,
|
||||||
|
status: 'active',
|
||||||
|
planId: event.data.object.items.data[0].price.id,
|
||||||
|
currentPeriodEnd: new Date(event.data.object.current_period_end * 1000),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Subscription Updated
|
||||||
|
```typescript
|
||||||
|
case 'customer.subscription.updated':
|
||||||
|
await prisma.subscription.update({
|
||||||
|
where: { id: event.data.object.id },
|
||||||
|
data: {
|
||||||
|
status: event.data.object.status,
|
||||||
|
planId: event.data.object.items.data[0].price.id,
|
||||||
|
currentPeriodEnd: new Date(event.data.object.current_period_end * 1000),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Subscription Deleted
|
||||||
|
```typescript
|
||||||
|
case 'customer.subscription.deleted':
|
||||||
|
await prisma.subscription.update({
|
||||||
|
where: { id: event.data.object.id },
|
||||||
|
data: {
|
||||||
|
status: 'canceled',
|
||||||
|
canceledAt: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Payment Failed
|
||||||
|
```typescript
|
||||||
|
case 'invoice.payment_failed':
|
||||||
|
const subscription = await prisma.subscription.findUnique({
|
||||||
|
where: { id: event.data.object.subscription },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send payment failure notification
|
||||||
|
await sendPaymentFailureEmail(subscription.accountId);
|
||||||
|
break;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setting Up Webhooks
|
||||||
|
|
||||||
|
### Stripe
|
||||||
|
|
||||||
|
1. **Local Development** (using Stripe CLI):
|
||||||
|
```bash
|
||||||
|
stripe listen --forward-to localhost:3000/api/billing/webhook
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Production**:
|
||||||
|
- Go to Stripe Dashboard → Developers → Webhooks
|
||||||
|
- Add endpoint: `https://yourdomain.com/api/billing/webhook`
|
||||||
|
- Select events to listen to
|
||||||
|
- Copy webhook signing secret to your `.env`
|
||||||
|
|
||||||
|
### Paddle
|
||||||
|
|
||||||
|
1. **Configure webhook URL** in Paddle dashboard
|
||||||
|
2. **Add webhook secret** to environment variables
|
||||||
|
3. **Verify webhook signature**:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const signature = request.headers.get('paddle-signature');
|
||||||
|
const verified = paddle.webhooks.verify(body, signature);
|
||||||
|
|
||||||
|
if (!verified) {
|
||||||
|
return new Response('Invalid signature', { status: 401 });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Best Practices
|
||||||
|
|
||||||
|
1. **Always verify signatures** - Prevents unauthorized requests
|
||||||
|
2. **Use HTTPS** - Encrypts webhook data in transit
|
||||||
|
3. **Validate event data** - Check for required fields
|
||||||
|
4. **Handle idempotently** - Process duplicate events safely
|
||||||
|
5. **Return 200 quickly** - Acknowledge receipt, process async
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async function handleBillingEvent(event: Event) {
|
||||||
|
try {
|
||||||
|
await processEvent(event);
|
||||||
|
} catch (error) {
|
||||||
|
// Log error for debugging
|
||||||
|
console.error('Webhook error:', error);
|
||||||
|
|
||||||
|
// Store failed event for retry
|
||||||
|
await prisma.failedWebhook.create({
|
||||||
|
data: {
|
||||||
|
eventId: event.id,
|
||||||
|
type: event.type,
|
||||||
|
payload: event,
|
||||||
|
error: error.message,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Throw to trigger provider retry
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Webhooks
|
||||||
|
|
||||||
|
### Using Provider's CLI Tools
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Stripe
|
||||||
|
stripe trigger customer.subscription.created
|
||||||
|
|
||||||
|
# Test specific scenarios
|
||||||
|
stripe trigger payment_intent.payment_failed
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST https://your-app.com/api/billing/webhook \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "stripe-signature: test_signature" \
|
||||||
|
-d @test-event.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Monitoring
|
||||||
|
|
||||||
|
Track webhook delivery:
|
||||||
|
- Response times
|
||||||
|
- Success/failure rates
|
||||||
|
- Event processing duration
|
||||||
|
- Failed events requiring manual intervention
|
||||||
|
|
||||||
|
Most providers offer webhook monitoring dashboards showing delivery attempts and failures.
|
||||||
16
apps/web/content/documentation/database/database.mdoc
Normal file
16
apps/web/content/documentation/database/database.mdoc
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "Database"
|
||||||
|
description: "Learn how to work with the Supabase database in your MakerKit application."
|
||||||
|
publishedAt: 2024-04-11
|
||||||
|
order: 2
|
||||||
|
status: "published"
|
||||||
|
---
|
||||||
|
|
||||||
|
MakerKit uses Supabase Postgres for database management with built-in security and performance.
|
||||||
|
|
||||||
|
This section covers:
|
||||||
|
- Database schema and structure
|
||||||
|
- Running migrations
|
||||||
|
- Row Level Security (RLS)
|
||||||
|
- Querying data
|
||||||
|
- Functions and triggers
|
||||||
446
apps/web/content/documentation/database/functions-triggers.mdoc
Normal file
446
apps/web/content/documentation/database/functions-triggers.mdoc
Normal file
@@ -0,0 +1,446 @@
|
|||||||
|
---
|
||||||
|
title: "Functions & Triggers"
|
||||||
|
description: "Create database functions and triggers for automated logic."
|
||||||
|
publishedAt: 2024-04-11
|
||||||
|
order: 4
|
||||||
|
status: "published"
|
||||||
|
---
|
||||||
|
|
||||||
|
> **Note:** This is mock/placeholder content for demonstration purposes.
|
||||||
|
|
||||||
|
Database functions and triggers enable server-side logic and automation.
|
||||||
|
|
||||||
|
## Database Functions
|
||||||
|
|
||||||
|
### Creating a Function
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE OR REPLACE FUNCTION get_user_projects(user_id UUID)
|
||||||
|
RETURNS TABLE (
|
||||||
|
id UUID,
|
||||||
|
name TEXT,
|
||||||
|
created_at TIMESTAMPTZ
|
||||||
|
)
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SECURITY DEFINER
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT p.id, p.name, p.created_at
|
||||||
|
FROM projects p
|
||||||
|
INNER JOIN accounts_memberships am ON am.account_id = p.account_id
|
||||||
|
WHERE am.user_id = get_user_projects.user_id;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Calling from TypeScript
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { data, error } = await client.rpc('get_user_projects', {
|
||||||
|
user_id: userId,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Function Patterns
|
||||||
|
|
||||||
|
### Get User Accounts
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE OR REPLACE FUNCTION get_user_accounts(user_id UUID)
|
||||||
|
RETURNS TABLE (account_id UUID)
|
||||||
|
LANGUAGE sql
|
||||||
|
SECURITY DEFINER
|
||||||
|
AS $$
|
||||||
|
SELECT account_id
|
||||||
|
FROM accounts_memberships
|
||||||
|
WHERE user_id = $1;
|
||||||
|
$$;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check Permission
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE OR REPLACE FUNCTION has_permission(
|
||||||
|
user_id UUID,
|
||||||
|
account_id UUID,
|
||||||
|
required_role TEXT
|
||||||
|
)
|
||||||
|
RETURNS BOOLEAN
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SECURITY DEFINER
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
user_role TEXT;
|
||||||
|
BEGIN
|
||||||
|
SELECT role INTO user_role
|
||||||
|
FROM accounts_memberships
|
||||||
|
WHERE user_id = has_permission.user_id
|
||||||
|
AND account_id = has_permission.account_id;
|
||||||
|
|
||||||
|
RETURN user_role = required_role OR user_role = 'owner';
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Search Function
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE OR REPLACE FUNCTION search_projects(
|
||||||
|
search_term TEXT,
|
||||||
|
account_id UUID
|
||||||
|
)
|
||||||
|
RETURNS TABLE (
|
||||||
|
id UUID,
|
||||||
|
name TEXT,
|
||||||
|
description TEXT,
|
||||||
|
relevance REAL
|
||||||
|
)
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SECURITY DEFINER
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT
|
||||||
|
p.id,
|
||||||
|
p.name,
|
||||||
|
p.description,
|
||||||
|
ts_rank(
|
||||||
|
to_tsvector('english', p.name || ' ' || COALESCE(p.description, '')),
|
||||||
|
plainto_tsquery('english', search_term)
|
||||||
|
) AS relevance
|
||||||
|
FROM projects p
|
||||||
|
WHERE p.account_id = search_projects.account_id
|
||||||
|
AND (
|
||||||
|
to_tsvector('english', p.name || ' ' || COALESCE(p.description, ''))
|
||||||
|
@@ plainto_tsquery('english', search_term)
|
||||||
|
)
|
||||||
|
ORDER BY relevance DESC;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Triggers
|
||||||
|
|
||||||
|
### Auto-Update Timestamp
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Create trigger function
|
||||||
|
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.updated_at = NOW();
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- Attach to table
|
||||||
|
CREATE TRIGGER update_projects_updated_at
|
||||||
|
BEFORE UPDATE ON projects
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION update_updated_at_column();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Audit Log Trigger
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Create audit log table
|
||||||
|
CREATE TABLE audit_log (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
table_name TEXT NOT NULL,
|
||||||
|
record_id UUID NOT NULL,
|
||||||
|
action TEXT NOT NULL,
|
||||||
|
old_data JSONB,
|
||||||
|
new_data JSONB,
|
||||||
|
user_id UUID,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create trigger function
|
||||||
|
CREATE OR REPLACE FUNCTION log_changes()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
IF TG_OP = 'INSERT' THEN
|
||||||
|
INSERT INTO audit_log (table_name, record_id, action, new_data, user_id)
|
||||||
|
VALUES (TG_TABLE_NAME, NEW.id, 'INSERT', to_jsonb(NEW), auth.uid());
|
||||||
|
RETURN NEW;
|
||||||
|
ELSIF TG_OP = 'UPDATE' THEN
|
||||||
|
INSERT INTO audit_log (table_name, record_id, action, old_data, new_data, user_id)
|
||||||
|
VALUES (TG_TABLE_NAME, NEW.id, 'UPDATE', to_jsonb(OLD), to_jsonb(NEW), auth.uid());
|
||||||
|
RETURN NEW;
|
||||||
|
ELSIF TG_OP = 'DELETE' THEN
|
||||||
|
INSERT INTO audit_log (table_name, record_id, action, old_data, user_id)
|
||||||
|
VALUES (TG_TABLE_NAME, OLD.id, 'DELETE', to_jsonb(OLD), auth.uid());
|
||||||
|
RETURN OLD;
|
||||||
|
END IF;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- Attach to table
|
||||||
|
CREATE TRIGGER audit_projects
|
||||||
|
AFTER INSERT OR UPDATE OR DELETE ON projects
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION log_changes();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cascade Soft Delete
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE OR REPLACE FUNCTION soft_delete_cascade()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
-- Soft delete related tasks
|
||||||
|
UPDATE tasks
|
||||||
|
SET deleted_at = NOW()
|
||||||
|
WHERE project_id = OLD.id
|
||||||
|
AND deleted_at IS NULL;
|
||||||
|
|
||||||
|
RETURN OLD;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE TRIGGER soft_delete_project_tasks
|
||||||
|
BEFORE DELETE ON projects
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION soft_delete_cascade();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validation Triggers
|
||||||
|
|
||||||
|
### Enforce Business Rules
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE OR REPLACE FUNCTION validate_project_budget()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
IF NEW.budget < 0 THEN
|
||||||
|
RAISE EXCEPTION 'Budget cannot be negative';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF NEW.budget > 1000000 THEN
|
||||||
|
RAISE EXCEPTION 'Budget cannot exceed 1,000,000';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE TRIGGER check_project_budget
|
||||||
|
BEFORE INSERT OR UPDATE ON projects
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION validate_project_budget();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Prevent Orphaned Records
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE OR REPLACE FUNCTION prevent_owner_removal()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
owner_count INTEGER;
|
||||||
|
BEGIN
|
||||||
|
IF OLD.role = 'owner' THEN
|
||||||
|
SELECT COUNT(*) INTO owner_count
|
||||||
|
FROM accounts_memberships
|
||||||
|
WHERE account_id = OLD.account_id
|
||||||
|
AND role = 'owner'
|
||||||
|
AND id != OLD.id;
|
||||||
|
|
||||||
|
IF owner_count = 0 THEN
|
||||||
|
RAISE EXCEPTION 'Cannot remove the last owner of an account';
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN OLD;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE TRIGGER check_owner_before_delete
|
||||||
|
BEFORE DELETE ON accounts_memberships
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION prevent_owner_removal();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Computed Columns
|
||||||
|
|
||||||
|
### Virtual Column with Function
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE OR REPLACE FUNCTION project_task_count(project_id UUID)
|
||||||
|
RETURNS INTEGER
|
||||||
|
LANGUAGE sql
|
||||||
|
STABLE
|
||||||
|
AS $$
|
||||||
|
SELECT COUNT(*)::INTEGER
|
||||||
|
FROM tasks
|
||||||
|
WHERE project_id = $1
|
||||||
|
AND deleted_at IS NULL;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- Use in queries
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
project_task_count(id) as task_count
|
||||||
|
FROM projects;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Event Notifications
|
||||||
|
|
||||||
|
### Notify on Changes
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE OR REPLACE FUNCTION notify_project_change()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
PERFORM pg_notify(
|
||||||
|
'project_changes',
|
||||||
|
json_build_object(
|
||||||
|
'operation', TG_OP,
|
||||||
|
'record', NEW
|
||||||
|
)::text
|
||||||
|
);
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE TRIGGER project_change_notification
|
||||||
|
AFTER INSERT OR UPDATE ON projects
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION notify_project_change();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Listen in TypeScript
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const channel = client
|
||||||
|
.channel('project_changes')
|
||||||
|
.on(
|
||||||
|
'postgres_changes',
|
||||||
|
{
|
||||||
|
event: '*',
|
||||||
|
schema: 'public',
|
||||||
|
table: 'projects',
|
||||||
|
},
|
||||||
|
(payload) => {
|
||||||
|
console.log('Project changed:', payload);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Functions
|
||||||
|
|
||||||
|
### Row Level Security Helper
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE OR REPLACE FUNCTION is_account_member(account_id UUID)
|
||||||
|
RETURNS BOOLEAN
|
||||||
|
LANGUAGE sql
|
||||||
|
SECURITY DEFINER
|
||||||
|
STABLE
|
||||||
|
AS $$
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM accounts_memberships
|
||||||
|
WHERE account_id = $1
|
||||||
|
AND user_id = auth.uid()
|
||||||
|
);
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- Use in RLS policy
|
||||||
|
CREATE POLICY "Users can access their account's projects"
|
||||||
|
ON projects FOR ALL
|
||||||
|
USING (is_account_member(account_id));
|
||||||
|
```
|
||||||
|
|
||||||
|
## Scheduled Functions
|
||||||
|
|
||||||
|
### Using pg_cron Extension
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Enable pg_cron extension
|
||||||
|
CREATE EXTENSION IF NOT EXISTS pg_cron;
|
||||||
|
|
||||||
|
-- Schedule cleanup job
|
||||||
|
SELECT cron.schedule(
|
||||||
|
'cleanup-old-sessions',
|
||||||
|
'0 2 * * *', -- Every day at 2 AM
|
||||||
|
$$
|
||||||
|
DELETE FROM sessions
|
||||||
|
WHERE expires_at < NOW();
|
||||||
|
$$
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Use SECURITY DEFINER carefully** - Can bypass RLS
|
||||||
|
2. **Add error handling** - Use EXCEPTION blocks
|
||||||
|
3. **Keep functions simple** - One responsibility per function
|
||||||
|
4. **Document functions** - Add comments
|
||||||
|
5. **Test thoroughly** - Unit test database functions
|
||||||
|
6. **Use STABLE/IMMUTABLE** - Performance optimization
|
||||||
|
7. **Avoid side effects** - Make functions predictable
|
||||||
|
8. **Return proper types** - Use RETURNS TABLE for clarity
|
||||||
|
|
||||||
|
## Testing Functions
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Test function
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
result INTEGER;
|
||||||
|
BEGIN
|
||||||
|
SELECT project_task_count('some-uuid') INTO result;
|
||||||
|
|
||||||
|
ASSERT result >= 0, 'Task count should not be negative';
|
||||||
|
|
||||||
|
RAISE NOTICE 'Test passed: task count = %', result;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Debugging
|
||||||
|
|
||||||
|
### Enable Function Logging
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE OR REPLACE FUNCTION debug_function()
|
||||||
|
RETURNS void
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
RAISE NOTICE 'Debug: Processing started';
|
||||||
|
RAISE NOTICE 'Debug: Current user is %', auth.uid();
|
||||||
|
-- Your function logic
|
||||||
|
RAISE NOTICE 'Debug: Processing completed';
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check Function Execution
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- View function execution stats
|
||||||
|
SELECT
|
||||||
|
schemaname,
|
||||||
|
funcname,
|
||||||
|
calls,
|
||||||
|
total_time,
|
||||||
|
self_time
|
||||||
|
FROM pg_stat_user_functions
|
||||||
|
ORDER BY total_time DESC;
|
||||||
|
```
|
||||||
68
apps/web/content/documentation/database/migrations.mdoc
Normal file
68
apps/web/content/documentation/database/migrations.mdoc
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
---
|
||||||
|
title: "Migrations"
|
||||||
|
description: "Learn how to create and manage database migrations in your application."
|
||||||
|
publishedAt: 2024-04-11
|
||||||
|
order: 1
|
||||||
|
status: "published"
|
||||||
|
---
|
||||||
|
|
||||||
|
> **Note:** This is mock/placeholder content for demonstration purposes.
|
||||||
|
|
||||||
|
Database migrations allow you to version control your database schema changes and apply them consistently across environments.
|
||||||
|
|
||||||
|
## Creating a Migration
|
||||||
|
|
||||||
|
To create a new migration, use the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm --filter web supabase:db:diff
|
||||||
|
```
|
||||||
|
|
||||||
|
This will generate a new migration file in the `apps/web/supabase/migrations` directory based on the differences between your local database and the schema files.
|
||||||
|
|
||||||
|
## Applying Migrations
|
||||||
|
|
||||||
|
To apply migrations to your local database:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm --filter web supabase migrations up
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration Best Practices
|
||||||
|
|
||||||
|
1. **Always test migrations locally first** before applying to production
|
||||||
|
2. **Make migrations reversible** when possible by including DOWN statements
|
||||||
|
3. **Use transactions** to ensure atomic operations
|
||||||
|
4. **Add indexes** for foreign keys and frequently queried columns
|
||||||
|
5. **Include RLS policies** in the same migration as table creation
|
||||||
|
|
||||||
|
## Example Migration
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Create a new table
|
||||||
|
CREATE TABLE tasks (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
account_id UUID NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
completed BOOLEAN DEFAULT false,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Add RLS
|
||||||
|
ALTER TABLE tasks ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
-- Create policies
|
||||||
|
CREATE POLICY "Users can view their account tasks"
|
||||||
|
ON tasks FOR SELECT
|
||||||
|
USING (account_id IN (SELECT get_user_accounts(auth.uid())));
|
||||||
|
```
|
||||||
|
|
||||||
|
## Resetting the Database
|
||||||
|
|
||||||
|
To completely reset your local database with the latest schema:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm supabase:web:reset
|
||||||
|
```
|
||||||
|
|
||||||
|
This will drop all tables and reapply all migrations from scratch.
|
||||||
430
apps/web/content/documentation/database/querying-data.mdoc
Normal file
430
apps/web/content/documentation/database/querying-data.mdoc
Normal file
@@ -0,0 +1,430 @@
|
|||||||
|
---
|
||||||
|
title: "Querying Data"
|
||||||
|
description: "Learn how to query and filter data from your database."
|
||||||
|
publishedAt: 2024-04-11
|
||||||
|
order: 3
|
||||||
|
status: "published"
|
||||||
|
---
|
||||||
|
|
||||||
|
> **Note:** This is mock/placeholder content for demonstration purposes.
|
||||||
|
|
||||||
|
Efficiently query and filter data using Supabase's query builder.
|
||||||
|
|
||||||
|
## Basic Queries
|
||||||
|
|
||||||
|
### Select All
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { data, error } = await client
|
||||||
|
.from('projects')
|
||||||
|
.select('*');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Select Specific Columns
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { data, error } = await client
|
||||||
|
.from('projects')
|
||||||
|
.select('id, name, created_at');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Select with Related Data
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { data, error } = await client
|
||||||
|
.from('projects')
|
||||||
|
.select(`
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
account:accounts(id, name),
|
||||||
|
tasks(id, title, completed)
|
||||||
|
`);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Filtering
|
||||||
|
|
||||||
|
### Equal
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { data } = await client
|
||||||
|
.from('projects')
|
||||||
|
.select('*')
|
||||||
|
.eq('status', 'active');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Not Equal
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { data } = await client
|
||||||
|
.from('projects')
|
||||||
|
.select('*')
|
||||||
|
.neq('status', 'deleted');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Greater Than / Less Than
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { data } = await client
|
||||||
|
.from('projects')
|
||||||
|
.select('*')
|
||||||
|
.gt('created_at', '2024-01-01')
|
||||||
|
.lt('budget', 10000);
|
||||||
|
```
|
||||||
|
|
||||||
|
### In Array
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { data } = await client
|
||||||
|
.from('projects')
|
||||||
|
.select('*')
|
||||||
|
.in('status', ['active', 'pending']);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Like (Pattern Matching)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { data } = await client
|
||||||
|
.from('projects')
|
||||||
|
.select('*')
|
||||||
|
.like('name', '%website%');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Full-Text Search
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { data } = await client
|
||||||
|
.from('projects')
|
||||||
|
.select('*')
|
||||||
|
.textSearch('description', 'design & development');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ordering
|
||||||
|
|
||||||
|
### Order By
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { data } = await client
|
||||||
|
.from('projects')
|
||||||
|
.select('*')
|
||||||
|
.order('created_at', { ascending: false });
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multiple Order By
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { data } = await client
|
||||||
|
.from('projects')
|
||||||
|
.select('*')
|
||||||
|
.order('status')
|
||||||
|
.order('created_at', { ascending: false });
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pagination
|
||||||
|
|
||||||
|
### Limit
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { data } = await client
|
||||||
|
.from('projects')
|
||||||
|
.select('*')
|
||||||
|
.limit(10);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Range (Offset)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const page = 2;
|
||||||
|
const pageSize = 10;
|
||||||
|
const from = (page - 1) * pageSize;
|
||||||
|
const to = from + pageSize - 1;
|
||||||
|
|
||||||
|
const { data, count } = await client
|
||||||
|
.from('projects')
|
||||||
|
.select('*', { count: 'exact' })
|
||||||
|
.range(from, to);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Aggregations
|
||||||
|
|
||||||
|
### Count
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { count } = await client
|
||||||
|
.from('projects')
|
||||||
|
.select('*', { count: 'exact', head: true });
|
||||||
|
```
|
||||||
|
|
||||||
|
### Count with Filters
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { count } = await client
|
||||||
|
.from('projects')
|
||||||
|
.select('*', { count: 'exact', head: true })
|
||||||
|
.eq('status', 'active');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Advanced Queries
|
||||||
|
|
||||||
|
### Multiple Filters
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { data } = await client
|
||||||
|
.from('projects')
|
||||||
|
.select('*')
|
||||||
|
.eq('account_id', accountId)
|
||||||
|
.eq('status', 'active')
|
||||||
|
.gte('created_at', startDate)
|
||||||
|
.lte('created_at', endDate)
|
||||||
|
.order('created_at', { ascending: false })
|
||||||
|
.limit(20);
|
||||||
|
```
|
||||||
|
|
||||||
|
### OR Conditions
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { data } = await client
|
||||||
|
.from('projects')
|
||||||
|
.select('*')
|
||||||
|
.or('status.eq.active,status.eq.pending');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Nested OR
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { data } = await client
|
||||||
|
.from('projects')
|
||||||
|
.select('*')
|
||||||
|
.or('and(status.eq.active,priority.eq.high),status.eq.urgent');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Joins
|
||||||
|
|
||||||
|
### Inner Join
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { data } = await client
|
||||||
|
.from('projects')
|
||||||
|
.select(`
|
||||||
|
*,
|
||||||
|
account:accounts!inner(
|
||||||
|
id,
|
||||||
|
name
|
||||||
|
)
|
||||||
|
`)
|
||||||
|
.eq('account.name', 'Acme Corp');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Left Join
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { data } = await client
|
||||||
|
.from('projects')
|
||||||
|
.select(`
|
||||||
|
*,
|
||||||
|
tasks(*)
|
||||||
|
`);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Null Handling
|
||||||
|
|
||||||
|
### Is Null
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { data } = await client
|
||||||
|
.from('projects')
|
||||||
|
.select('*')
|
||||||
|
.is('completed_at', null);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Not Null
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { data} = await client
|
||||||
|
.from('projects')
|
||||||
|
.select('*')
|
||||||
|
.not('completed_at', 'is', null);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Insert Data
|
||||||
|
|
||||||
|
### Single Insert
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { data, error } = await client
|
||||||
|
.from('projects')
|
||||||
|
.insert({
|
||||||
|
name: 'New Project',
|
||||||
|
account_id: accountId,
|
||||||
|
status: 'active',
|
||||||
|
})
|
||||||
|
.select()
|
||||||
|
.single();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multiple Insert
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { data, error } = await client
|
||||||
|
.from('projects')
|
||||||
|
.insert([
|
||||||
|
{ name: 'Project 1', account_id: accountId },
|
||||||
|
{ name: 'Project 2', account_id: accountId },
|
||||||
|
])
|
||||||
|
.select();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Update Data
|
||||||
|
|
||||||
|
### Update with Filter
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { data, error } = await client
|
||||||
|
.from('projects')
|
||||||
|
.update({ status: 'completed' })
|
||||||
|
.eq('id', projectId)
|
||||||
|
.select()
|
||||||
|
.single();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update Multiple Rows
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { data, error } = await client
|
||||||
|
.from('projects')
|
||||||
|
.update({ status: 'archived' })
|
||||||
|
.eq('account_id', accountId)
|
||||||
|
.lt('updated_at', oldDate);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Delete Data
|
||||||
|
|
||||||
|
### Delete with Filter
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { error } = await client
|
||||||
|
.from('projects')
|
||||||
|
.delete()
|
||||||
|
.eq('id', projectId);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Delete Multiple
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { error } = await client
|
||||||
|
.from('projects')
|
||||||
|
.delete()
|
||||||
|
.in('id', projectIds);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Upsert
|
||||||
|
|
||||||
|
### Insert or Update
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { data, error } = await client
|
||||||
|
.from('projects')
|
||||||
|
.upsert({
|
||||||
|
id: projectId,
|
||||||
|
name: 'Updated Name',
|
||||||
|
status: 'active',
|
||||||
|
})
|
||||||
|
.select()
|
||||||
|
.single();
|
||||||
|
```
|
||||||
|
|
||||||
|
## RPC (Stored Procedures)
|
||||||
|
|
||||||
|
### Call Database Function
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { data, error } = await client
|
||||||
|
.rpc('get_user_projects', {
|
||||||
|
user_id: userId,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### With Complex Parameters
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { data, error } = await client
|
||||||
|
.rpc('search_projects', {
|
||||||
|
search_term: 'design',
|
||||||
|
account_ids: [1, 2, 3],
|
||||||
|
min_budget: 5000,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Basic Error Handling
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { data, error } = await client
|
||||||
|
.from('projects')
|
||||||
|
.select('*');
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error('Error fetching projects:', error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Typed Error Handling
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { PostgrestError } from '@supabase/supabase-js';
|
||||||
|
|
||||||
|
function handleDatabaseError(error: PostgrestError) {
|
||||||
|
switch (error.code) {
|
||||||
|
case '23505': // unique_violation
|
||||||
|
throw new Error('A project with this name already exists');
|
||||||
|
case '23503': // foreign_key_violation
|
||||||
|
throw new Error('Invalid account reference');
|
||||||
|
default:
|
||||||
|
throw new Error('Database error: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## TypeScript Types
|
||||||
|
|
||||||
|
### Generated Types
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Database } from '~/types/database.types';
|
||||||
|
|
||||||
|
type Project = Database['public']['Tables']['projects']['Row'];
|
||||||
|
type ProjectInsert = Database['public']['Tables']['projects']['Insert'];
|
||||||
|
type ProjectUpdate = Database['public']['Tables']['projects']['Update'];
|
||||||
|
```
|
||||||
|
|
||||||
|
### Typed Queries
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { data } = await client
|
||||||
|
.from('projects')
|
||||||
|
.select('*')
|
||||||
|
.returns<Project[]>();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Tips
|
||||||
|
|
||||||
|
1. **Select only needed columns** - Don't use `select('*')` unnecessarily
|
||||||
|
2. **Use indexes** - Create indexes on frequently filtered columns
|
||||||
|
3. **Limit results** - Always paginate large datasets
|
||||||
|
4. **Avoid N+1 queries** - Use joins instead of multiple queries
|
||||||
|
5. **Use RPC for complex queries** - Move logic to database
|
||||||
|
6. **Cache when possible** - Use React Query or similar
|
||||||
|
7. **Profile queries** - Use `EXPLAIN ANALYZE` in SQL
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Always handle errors** - Check error responses
|
||||||
|
2. **Validate input** - Use Zod or similar
|
||||||
|
3. **Use TypeScript** - Generate and use types
|
||||||
|
4. **Consistent naming** - Follow database naming conventions
|
||||||
|
5. **Document complex queries** - Add comments
|
||||||
|
6. **Test queries** - Unit test database operations
|
||||||
|
7. **Monitor performance** - Track slow queries
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
---
|
||||||
|
title: "Row Level Security"
|
||||||
|
description: "Understanding and implementing Row Level Security (RLS) for data protection."
|
||||||
|
publishedAt: 2024-04-11
|
||||||
|
order: 2
|
||||||
|
status: "published"
|
||||||
|
---
|
||||||
|
|
||||||
|
> **Note:** This is mock/placeholder content for demonstration purposes.
|
||||||
|
|
||||||
|
Row Level Security (RLS) is PostgreSQL's built-in authorization system that controls which rows users can access in database tables.
|
||||||
|
|
||||||
|
## Why RLS?
|
||||||
|
|
||||||
|
RLS provides several advantages:
|
||||||
|
- **Database-level security** - Protection even if application code has bugs
|
||||||
|
- **Automatic enforcement** - No need for manual authorization checks
|
||||||
|
- **Multi-tenant isolation** - Ensures users only see their own data
|
||||||
|
- **Performance** - Optimized at the database level
|
||||||
|
|
||||||
|
## Enabling RLS
|
||||||
|
|
||||||
|
All tables should have RLS enabled:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
ALTER TABLE your_table ENABLE ROW LEVEL SECURITY;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Policy Patterns
|
||||||
|
|
||||||
|
### Personal Account Access
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE POLICY "Users can access their personal account data"
|
||||||
|
ON your_table FOR ALL
|
||||||
|
USING (account_id = auth.uid());
|
||||||
|
```
|
||||||
|
|
||||||
|
### Team Account Access
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE POLICY "Users can access their team account data"
|
||||||
|
ON your_table FOR ALL
|
||||||
|
USING (
|
||||||
|
account_id IN (
|
||||||
|
SELECT account_id FROM accounts_memberships
|
||||||
|
WHERE user_id = auth.uid()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Read vs Write Permissions
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- All members can read
|
||||||
|
CREATE POLICY "Team members can view data"
|
||||||
|
ON your_table FOR SELECT
|
||||||
|
USING (account_id IN (SELECT get_user_accounts(auth.uid())));
|
||||||
|
|
||||||
|
-- Only owners can modify
|
||||||
|
CREATE POLICY "Only owners can modify data"
|
||||||
|
ON your_table FOR UPDATE
|
||||||
|
USING (
|
||||||
|
account_id IN (
|
||||||
|
SELECT account_id FROM accounts_memberships
|
||||||
|
WHERE user_id = auth.uid() AND role = 'owner'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing RLS Policies
|
||||||
|
|
||||||
|
Always test your RLS policies to ensure they work correctly:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Test as specific user
|
||||||
|
SET request.jwt.claims.sub = 'user-uuid-here';
|
||||||
|
|
||||||
|
-- Try to select data
|
||||||
|
SELECT * FROM your_table;
|
||||||
|
|
||||||
|
-- Reset
|
||||||
|
RESET request.jwt.claims.sub;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Admin Bypass
|
||||||
|
|
||||||
|
Service role keys bypass RLS. Use with extreme caution and always implement manual authorization checks when using the admin client.
|
||||||
43
apps/web/content/documentation/database/schema.mdoc
Normal file
43
apps/web/content/documentation/database/schema.mdoc
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
---
|
||||||
|
title: "Database Overview"
|
||||||
|
description: "Understanding the database schema and table structure in your application."
|
||||||
|
publishedAt: 2024-04-11
|
||||||
|
order: 0
|
||||||
|
status: "published"
|
||||||
|
---
|
||||||
|
|
||||||
|
> **Note:** This is mock/placeholder content for demonstration purposes.
|
||||||
|
|
||||||
|
The database schema is designed with a multi-tenant architecture that supports both personal and team accounts.
|
||||||
|
|
||||||
|
## Core Tables
|
||||||
|
|
||||||
|
### Users Table
|
||||||
|
The `users` table stores user authentication data and is managed by Supabase Auth:
|
||||||
|
- `id` - Unique user identifier
|
||||||
|
- `email` - User's email address
|
||||||
|
- `created_at` - Account creation timestamp
|
||||||
|
|
||||||
|
### Accounts Table
|
||||||
|
The `accounts` table represents both personal and team accounts:
|
||||||
|
- `id` - Unique account identifier
|
||||||
|
- `name` - Account display name
|
||||||
|
- `slug` - URL-friendly identifier
|
||||||
|
- `is_personal_account` - Boolean flag for personal vs team accounts
|
||||||
|
|
||||||
|
### Projects Table
|
||||||
|
Store your application's project data:
|
||||||
|
- `id` - Unique project identifier
|
||||||
|
- `account_id` - Foreign key to accounts table
|
||||||
|
- `name` - Project name
|
||||||
|
- `description` - Project description
|
||||||
|
- `created_at` - Creation timestamp
|
||||||
|
|
||||||
|
## Relationships
|
||||||
|
|
||||||
|
All data in the application is tied to accounts through foreign key relationships. This ensures proper data isolation and access control through Row Level Security (RLS).
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- Learn about [migrations](/docs/database/migrations)
|
||||||
|
- Understand [RLS policies](/docs/database/row-level-security)
|
||||||
279
apps/web/content/documentation/features/email.mdoc
Normal file
279
apps/web/content/documentation/features/email.mdoc
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
---
|
||||||
|
title: "Features Overview"
|
||||||
|
description: "Send emails and notifications to your users."
|
||||||
|
publishedAt: 2024-04-11
|
||||||
|
order: 0
|
||||||
|
status: "published"
|
||||||
|
---
|
||||||
|
|
||||||
|
> **Note:** This is mock/placeholder content for demonstration purposes.
|
||||||
|
|
||||||
|
The application includes email functionality for transactional messages and user notifications.
|
||||||
|
|
||||||
|
## Email Configuration
|
||||||
|
|
||||||
|
### Supabase Email (Default)
|
||||||
|
|
||||||
|
By default, emails are sent through Supabase:
|
||||||
|
- Authentication emails (sign-up, password reset, magic links)
|
||||||
|
- Email verification
|
||||||
|
- Email change confirmation
|
||||||
|
|
||||||
|
### Custom SMTP
|
||||||
|
|
||||||
|
For transactional emails, configure your own SMTP provider:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# .env
|
||||||
|
SMTP_HOST=smtp.example.com
|
||||||
|
SMTP_PORT=587
|
||||||
|
SMTP_USER=your-username
|
||||||
|
SMTP_PASSWORD=your-password
|
||||||
|
SMTP_FROM_EMAIL=noreply@yourdomain.com
|
||||||
|
SMTP_FROM_NAME=Your App Name
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sending Emails
|
||||||
|
|
||||||
|
### Using the Email Service
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { sendEmail } from '~/lib/email/send-email';
|
||||||
|
|
||||||
|
await sendEmail({
|
||||||
|
to: 'user@example.com',
|
||||||
|
subject: 'Welcome to Our App',
|
||||||
|
html: '<h1>Welcome!</h1><p>Thanks for signing up.</p>',
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Email Templates
|
||||||
|
|
||||||
|
Create reusable email templates:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// lib/email/templates/welcome-email.tsx
|
||||||
|
import { EmailTemplate } from '~/lib/email/email-template';
|
||||||
|
|
||||||
|
interface WelcomeEmailProps {
|
||||||
|
name: string;
|
||||||
|
loginUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WelcomeEmail({ name, loginUrl }: WelcomeEmailProps) {
|
||||||
|
return (
|
||||||
|
<EmailTemplate>
|
||||||
|
<h1>Welcome, {name}!</h1>
|
||||||
|
<p>We're excited to have you on board.</p>
|
||||||
|
<a href={loginUrl}>Get Started</a>
|
||||||
|
</EmailTemplate>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the email
|
||||||
|
import { render } from '@react-email/render';
|
||||||
|
import { WelcomeEmail } from '~/lib/email/templates/welcome-email';
|
||||||
|
|
||||||
|
const html = render(
|
||||||
|
<WelcomeEmail name="John" loginUrl="https://app.com/login" />
|
||||||
|
);
|
||||||
|
|
||||||
|
await sendEmail({
|
||||||
|
to: 'john@example.com',
|
||||||
|
subject: 'Welcome to Our App',
|
||||||
|
html,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Email Types
|
||||||
|
|
||||||
|
### Transactional Emails
|
||||||
|
|
||||||
|
Emails triggered by user actions:
|
||||||
|
- Welcome emails
|
||||||
|
- Order confirmations
|
||||||
|
- Password resets
|
||||||
|
- Account notifications
|
||||||
|
- Billing updates
|
||||||
|
|
||||||
|
### Marketing Emails
|
||||||
|
|
||||||
|
Promotional and engagement emails:
|
||||||
|
- Product updates
|
||||||
|
- Feature announcements
|
||||||
|
- Newsletters
|
||||||
|
- Onboarding sequences
|
||||||
|
|
||||||
|
## Email Providers
|
||||||
|
|
||||||
|
### Recommended Providers
|
||||||
|
|
||||||
|
**Resend** - Developer-friendly email API
|
||||||
|
```bash
|
||||||
|
npm install resend
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Resend } from 'resend';
|
||||||
|
|
||||||
|
const resend = new Resend(process.env.RESEND_API_KEY);
|
||||||
|
|
||||||
|
await resend.emails.send({
|
||||||
|
from: 'noreply@yourdomain.com',
|
||||||
|
to: 'user@example.com',
|
||||||
|
subject: 'Welcome',
|
||||||
|
html: emailHtml,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**SendGrid** - Comprehensive email platform
|
||||||
|
```bash
|
||||||
|
npm install @sendgrid/mail
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import sgMail from '@sendgrid/mail';
|
||||||
|
|
||||||
|
sgMail.setApiKey(process.env.SENDGRID_API_KEY);
|
||||||
|
|
||||||
|
await sgMail.send({
|
||||||
|
to: 'user@example.com',
|
||||||
|
from: 'noreply@yourdomain.com',
|
||||||
|
subject: 'Welcome',
|
||||||
|
html: emailHtml,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Postmark** - Fast transactional email
|
||||||
|
```bash
|
||||||
|
npm install postmark
|
||||||
|
```
|
||||||
|
|
||||||
|
## In-App Notifications
|
||||||
|
|
||||||
|
### Notification System
|
||||||
|
|
||||||
|
Send in-app notifications to users:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createNotification } from '~/lib/notifications';
|
||||||
|
|
||||||
|
await createNotification({
|
||||||
|
userId: user.id,
|
||||||
|
title: 'New Message',
|
||||||
|
message: 'You have a new message from John',
|
||||||
|
type: 'info',
|
||||||
|
link: '/messages/123',
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Notification Types
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type NotificationType = 'info' | 'success' | 'warning' | 'error';
|
||||||
|
|
||||||
|
await createNotification({
|
||||||
|
userId: user.id,
|
||||||
|
title: 'Payment Successful',
|
||||||
|
message: 'Your subscription has been renewed',
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fetching Notifications
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { getUserNotifications } from '~/lib/notifications';
|
||||||
|
|
||||||
|
const notifications = await getUserNotifications(userId, {
|
||||||
|
limit: 10,
|
||||||
|
unreadOnly: true,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Marking as Read
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { markNotificationAsRead } from '~/lib/notifications';
|
||||||
|
|
||||||
|
await markNotificationAsRead(notificationId);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Real-time Notifications
|
||||||
|
|
||||||
|
Use Supabase Realtime for instant notifications:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
|
||||||
|
|
||||||
|
export function NotificationListener() {
|
||||||
|
const supabase = useSupabase();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const channel = supabase
|
||||||
|
.channel('notifications')
|
||||||
|
.on(
|
||||||
|
'postgres_changes',
|
||||||
|
{
|
||||||
|
event: 'INSERT',
|
||||||
|
schema: 'public',
|
||||||
|
table: 'notifications',
|
||||||
|
filter: `user_id=eq.${userId}`,
|
||||||
|
},
|
||||||
|
(payload) => {
|
||||||
|
// Show toast notification
|
||||||
|
toast.info(payload.new.title);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
supabase.removeChannel(channel);
|
||||||
|
};
|
||||||
|
}, [supabase]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Email Templates Best Practices
|
||||||
|
|
||||||
|
1. **Keep it simple** - Plain text and minimal HTML
|
||||||
|
2. **Mobile responsive** - Most emails are read on mobile
|
||||||
|
3. **Clear CTAs** - Make action buttons prominent
|
||||||
|
4. **Personalize** - Use user's name and relevant data
|
||||||
|
5. **Test rendering** - Check across email clients
|
||||||
|
6. **Include plain text** - Always provide text alternative
|
||||||
|
7. **Unsubscribe link** - Required for marketing emails
|
||||||
|
|
||||||
|
## Testing Emails
|
||||||
|
|
||||||
|
### Local Development
|
||||||
|
|
||||||
|
In development, emails are caught by InBucket:
|
||||||
|
|
||||||
|
```
|
||||||
|
http://localhost:54324
|
||||||
|
```
|
||||||
|
|
||||||
|
### Preview Emails
|
||||||
|
|
||||||
|
Use React Email to preview templates:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run email:dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Visit `http://localhost:3001` to see email previews.
|
||||||
|
|
||||||
|
## Deliverability Tips
|
||||||
|
|
||||||
|
1. **Authenticate your domain** - Set up SPF, DKIM, DMARC
|
||||||
|
2. **Warm up your domain** - Start with low volumes
|
||||||
|
3. **Monitor bounce rates** - Keep below 5%
|
||||||
|
4. **Avoid spam triggers** - Don't use all caps, excessive punctuation
|
||||||
|
5. **Provide value** - Only send relevant, useful emails
|
||||||
|
6. **Easy unsubscribe** - Make it one-click simple
|
||||||
16
apps/web/content/documentation/features/features.mdoc
Normal file
16
apps/web/content/documentation/features/features.mdoc
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "Features"
|
||||||
|
description: "Learn about the built-in features available in MakerKit."
|
||||||
|
publishedAt: 2024-04-11
|
||||||
|
order: 3
|
||||||
|
status: "published"
|
||||||
|
---
|
||||||
|
|
||||||
|
MakerKit comes with a rich set of features to help you build your SaaS quickly.
|
||||||
|
|
||||||
|
This section covers:
|
||||||
|
- Team collaboration
|
||||||
|
- File uploads
|
||||||
|
- Email functionality
|
||||||
|
- User management
|
||||||
|
- And more built-in features
|
||||||
398
apps/web/content/documentation/features/file-uploads.mdoc
Normal file
398
apps/web/content/documentation/features/file-uploads.mdoc
Normal file
@@ -0,0 +1,398 @@
|
|||||||
|
---
|
||||||
|
title: "File Uploads"
|
||||||
|
description: "Handle file uploads with Supabase Storage."
|
||||||
|
publishedAt: 2024-04-11
|
||||||
|
order: 2
|
||||||
|
status: "published"
|
||||||
|
---
|
||||||
|
|
||||||
|
> **Note:** This is mock/placeholder content for demonstration purposes.
|
||||||
|
|
||||||
|
Enable users to upload and manage files using Supabase Storage.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
### Create Storage Bucket
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Create a public bucket for avatars
|
||||||
|
INSERT INTO storage.buckets (id, name, public)
|
||||||
|
VALUES ('avatars', 'avatars', true);
|
||||||
|
|
||||||
|
-- Create a private bucket for documents
|
||||||
|
INSERT INTO storage.buckets (id, name, public)
|
||||||
|
VALUES ('documents', 'documents', false);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Set Storage Policies
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Allow users to upload their own avatars
|
||||||
|
CREATE POLICY "Users can upload their own avatar"
|
||||||
|
ON storage.objects FOR INSERT
|
||||||
|
WITH CHECK (
|
||||||
|
bucket_id = 'avatars' AND
|
||||||
|
auth.uid()::text = (storage.foldername(name))[1]
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Allow users to view their own avatars
|
||||||
|
CREATE POLICY "Users can view their own avatar"
|
||||||
|
ON storage.objects FOR SELECT
|
||||||
|
USING (
|
||||||
|
bucket_id = 'avatars' AND
|
||||||
|
auth.uid()::text = (storage.foldername(name))[1]
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Allow users to delete their own avatars
|
||||||
|
CREATE POLICY "Users can delete their own avatar"
|
||||||
|
ON storage.objects FOR DELETE
|
||||||
|
USING (
|
||||||
|
bucket_id = 'avatars' AND
|
||||||
|
auth.uid()::text = (storage.foldername(name))[1]
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Upload Component
|
||||||
|
|
||||||
|
### Basic File Upload
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { uploadFileAction } from '../_lib/actions';
|
||||||
|
|
||||||
|
export function FileUpload() {
|
||||||
|
const [uploading, setUploading] = useState(false);
|
||||||
|
const [file, setFile] = useState<File | null>(null);
|
||||||
|
|
||||||
|
const handleUpload = async () => {
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
setUploading(true);
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
const result = await uploadFileAction(formData);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
toast.success('File uploaded successfully');
|
||||||
|
}
|
||||||
|
|
||||||
|
setUploading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
onChange={(e) => setFile(e.files?.[0] || null)}
|
||||||
|
accept="image/*"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={handleUpload}
|
||||||
|
disabled={!file || uploading}
|
||||||
|
>
|
||||||
|
{uploading ? 'Uploading...' : 'Upload'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Server Action
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
'use server';
|
||||||
|
|
||||||
|
import { enhanceAction } from '@kit/next/actions';
|
||||||
|
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||||
|
|
||||||
|
export const uploadFileAction = enhanceAction(
|
||||||
|
async (formData: FormData, user) => {
|
||||||
|
const file = formData.get('file') as File;
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
throw new Error('No file provided');
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = getSupabaseServerClient();
|
||||||
|
const fileExt = file.name.split('.').pop();
|
||||||
|
const fileName = `${user.id}/${Date.now()}.${fileExt}`;
|
||||||
|
|
||||||
|
const { data, error } = await client.storage
|
||||||
|
.from('avatars')
|
||||||
|
.upload(fileName, file, {
|
||||||
|
cacheControl: '3600',
|
||||||
|
upsert: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
// Get public URL
|
||||||
|
const { data: { publicUrl } } = client.storage
|
||||||
|
.from('avatars')
|
||||||
|
.getPublicUrl(fileName);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
url: publicUrl,
|
||||||
|
path: data.path,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{ auth: true }
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Drag and Drop Upload
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { useDropzone } from 'react-dropzone';
|
||||||
|
|
||||||
|
export function DragDropUpload() {
|
||||||
|
const onDrop = useCallback(async (acceptedFiles: File[]) => {
|
||||||
|
for (const file of acceptedFiles) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
await uploadFileAction(formData);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||||
|
onDrop,
|
||||||
|
accept: {
|
||||||
|
'image/*': ['.png', '.jpg', '.jpeg', '.gif'],
|
||||||
|
},
|
||||||
|
maxSize: 5 * 1024 * 1024, // 5MB
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
{...getRootProps()}
|
||||||
|
className={cn(
|
||||||
|
'border-2 border-dashed rounded-lg p-8 text-center cursor-pointer',
|
||||||
|
isDragActive && 'border-primary bg-primary/10'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<input {...getInputProps()} />
|
||||||
|
{isDragActive ? (
|
||||||
|
<p>Drop files here...</p>
|
||||||
|
) : (
|
||||||
|
<p>Drag and drop files here, or click to select</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## File Validation
|
||||||
|
|
||||||
|
### Client-Side Validation
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function validateFile(file: File) {
|
||||||
|
const maxSize = 5 * 1024 * 1024; // 5MB
|
||||||
|
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
|
||||||
|
|
||||||
|
if (file.size > maxSize) {
|
||||||
|
throw new Error('File size must be less than 5MB');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!allowedTypes.includes(file.type)) {
|
||||||
|
throw new Error('File type must be JPEG, PNG, or GIF');
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Server-Side Validation
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const uploadFileAction = enhanceAction(
|
||||||
|
async (formData: FormData, user) => {
|
||||||
|
const file = formData.get('file') as File;
|
||||||
|
|
||||||
|
// Validate file size
|
||||||
|
if (file.size > 5 * 1024 * 1024) {
|
||||||
|
throw new Error('File too large');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate file type
|
||||||
|
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
|
||||||
|
if (!allowedTypes.includes(file.type)) {
|
||||||
|
throw new Error('Invalid file type');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate dimensions for images
|
||||||
|
if (file.type.startsWith('image/')) {
|
||||||
|
const dimensions = await getImageDimensions(file);
|
||||||
|
if (dimensions.width > 4000 || dimensions.height > 4000) {
|
||||||
|
throw new Error('Image dimensions too large');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continue with upload...
|
||||||
|
},
|
||||||
|
{ auth: true }
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Image Optimization
|
||||||
|
|
||||||
|
### Resize on Upload
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import sharp from 'sharp';
|
||||||
|
|
||||||
|
export const uploadAvatarAction = enhanceAction(
|
||||||
|
async (formData: FormData, user) => {
|
||||||
|
const file = formData.get('file') as File;
|
||||||
|
const buffer = Buffer.from(await file.arrayBuffer());
|
||||||
|
|
||||||
|
// Resize image
|
||||||
|
const resized = await sharp(buffer)
|
||||||
|
.resize(200, 200, {
|
||||||
|
fit: 'cover',
|
||||||
|
position: 'center',
|
||||||
|
})
|
||||||
|
.jpeg({ quality: 90 })
|
||||||
|
.toBuffer();
|
||||||
|
|
||||||
|
const client = getSupabaseServerClient();
|
||||||
|
const fileName = `${user.id}/avatar.jpg`;
|
||||||
|
|
||||||
|
const { error } = await client.storage
|
||||||
|
.from('avatars')
|
||||||
|
.upload(fileName, resized, {
|
||||||
|
contentType: 'image/jpeg',
|
||||||
|
upsert: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
},
|
||||||
|
{ auth: true }
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Progress Tracking
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
export function UploadWithProgress() {
|
||||||
|
const [progress, setProgress] = useState(0);
|
||||||
|
|
||||||
|
const handleUpload = async (file: File) => {
|
||||||
|
const client = getSupabaseBrowserClient();
|
||||||
|
|
||||||
|
const { error } = await client.storage
|
||||||
|
.from('documents')
|
||||||
|
.upload(`uploads/${file.name}`, file, {
|
||||||
|
onUploadProgress: (progressEvent) => {
|
||||||
|
const percent = (progressEvent.loaded / progressEvent.total) * 100;
|
||||||
|
setProgress(Math.round(percent));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<input type="file" onChange={(e) => handleUpload(e.target.files![0])} />
|
||||||
|
{progress > 0 && (
|
||||||
|
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||||
|
<div
|
||||||
|
className="bg-primary h-2 rounded-full transition-all"
|
||||||
|
style={{ width: `${progress}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Downloading Files
|
||||||
|
|
||||||
|
### Get Public URL
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { data } = client.storage
|
||||||
|
.from('avatars')
|
||||||
|
.getPublicUrl('user-id/avatar.jpg');
|
||||||
|
|
||||||
|
console.log(data.publicUrl);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Download Private File
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { data, error } = await client.storage
|
||||||
|
.from('documents')
|
||||||
|
.download('private-file.pdf');
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
const url = URL.createObjectURL(data);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = 'file.pdf';
|
||||||
|
a.click();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Generate Signed URL
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { data, error } = await client.storage
|
||||||
|
.from('documents')
|
||||||
|
.createSignedUrl('private-file.pdf', 3600); // 1 hour
|
||||||
|
|
||||||
|
console.log(data.signedUrl);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deleting Files
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const deleteFileAction = enhanceAction(
|
||||||
|
async (data, user) => {
|
||||||
|
const client = getSupabaseServerClient();
|
||||||
|
|
||||||
|
const { error } = await client.storage
|
||||||
|
.from('avatars')
|
||||||
|
.remove([data.path]);
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
},
|
||||||
|
{
|
||||||
|
schema: z.object({
|
||||||
|
path: z.string(),
|
||||||
|
}),
|
||||||
|
auth: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Validate on both sides** - Client and server
|
||||||
|
2. **Limit file sizes** - Prevent abuse
|
||||||
|
3. **Sanitize filenames** - Remove special characters
|
||||||
|
4. **Use unique names** - Prevent collisions
|
||||||
|
5. **Optimize images** - Resize before upload
|
||||||
|
6. **Set storage policies** - Control access
|
||||||
|
7. **Monitor usage** - Track storage costs
|
||||||
|
8. **Clean up unused files** - Regular maintenance
|
||||||
|
9. **Use CDN** - For public files
|
||||||
|
10. **Implement virus scanning** - For user uploads
|
||||||
276
apps/web/content/documentation/features/team-collaboration.mdoc
Normal file
276
apps/web/content/documentation/features/team-collaboration.mdoc
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
---
|
||||||
|
title: "Team Collaboration"
|
||||||
|
description: "Manage team members, roles, and permissions in your application."
|
||||||
|
publishedAt: 2024-04-11
|
||||||
|
order: 1
|
||||||
|
status: "published"
|
||||||
|
---
|
||||||
|
|
||||||
|
> **Note:** This is mock/placeholder content for demonstration purposes.
|
||||||
|
|
||||||
|
Enable teams to collaborate effectively with built-in team management features.
|
||||||
|
|
||||||
|
## Team Accounts
|
||||||
|
|
||||||
|
The application supports multi-tenant team accounts where multiple users can collaborate.
|
||||||
|
|
||||||
|
### Creating a Team
|
||||||
|
|
||||||
|
Users can create new team accounts:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createTeamAccount } from '~/lib/teams/create-team';
|
||||||
|
|
||||||
|
const team = await createTeamAccount({
|
||||||
|
name: 'Acme Corp',
|
||||||
|
slug: 'acme-corp',
|
||||||
|
ownerId: currentUser.id,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Team Workspace
|
||||||
|
|
||||||
|
Each team has its own workspace with isolated data:
|
||||||
|
- Projects and resources
|
||||||
|
- Team-specific settings
|
||||||
|
- Billing and subscription
|
||||||
|
- Activity logs
|
||||||
|
|
||||||
|
## Inviting Members
|
||||||
|
|
||||||
|
### Send Invitations
|
||||||
|
|
||||||
|
Invite new members to your team:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { inviteTeamMember } from '~/lib/teams/invitations';
|
||||||
|
|
||||||
|
await inviteTeamMember({
|
||||||
|
teamId: team.id,
|
||||||
|
email: 'member@example.com',
|
||||||
|
role: 'member',
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Invitation Flow
|
||||||
|
|
||||||
|
1. Owner sends invitation via email
|
||||||
|
2. Recipient receives email with invitation link
|
||||||
|
3. Recipient accepts invitation
|
||||||
|
4. Member gains access to team workspace
|
||||||
|
|
||||||
|
### Managing Invitations
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { PendingInvitations } from '~/components/teams/pending-invitations';
|
||||||
|
|
||||||
|
<PendingInvitations teamId={team.id} />
|
||||||
|
```
|
||||||
|
|
||||||
|
## Roles and Permissions
|
||||||
|
|
||||||
|
### Default Roles
|
||||||
|
|
||||||
|
**Owner**
|
||||||
|
- Full access to team and settings
|
||||||
|
- Manage billing and subscriptions
|
||||||
|
- Invite and remove members
|
||||||
|
- Delete team
|
||||||
|
|
||||||
|
**Admin**
|
||||||
|
- Manage team members
|
||||||
|
- Manage team resources
|
||||||
|
- Cannot access billing
|
||||||
|
- Cannot delete team
|
||||||
|
|
||||||
|
**Member**
|
||||||
|
- View team resources
|
||||||
|
- Create and edit own content
|
||||||
|
- Limited team settings access
|
||||||
|
|
||||||
|
### Custom Roles
|
||||||
|
|
||||||
|
Define custom roles with specific permissions:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const customRole = {
|
||||||
|
name: 'Editor',
|
||||||
|
permissions: [
|
||||||
|
'read:projects',
|
||||||
|
'write:projects',
|
||||||
|
'read:members',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Checking Permissions
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { checkPermission } from '~/lib/teams/permissions';
|
||||||
|
|
||||||
|
const canEdit = await checkPermission(userId, teamId, 'write:projects');
|
||||||
|
|
||||||
|
if (!canEdit) {
|
||||||
|
throw new Error('Insufficient permissions');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Member Management
|
||||||
|
|
||||||
|
### Listing Members
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { getTeamMembers } from '~/lib/teams/members';
|
||||||
|
|
||||||
|
const members = await getTeamMembers(teamId);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Updating Member Role
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { updateMemberRole } from '~/lib/teams/members';
|
||||||
|
|
||||||
|
await updateMemberRole({
|
||||||
|
memberId: member.id,
|
||||||
|
role: 'admin',
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Removing Members
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { removeMember } from '~/lib/teams/members';
|
||||||
|
|
||||||
|
await removeMember(memberId);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Team Settings
|
||||||
|
|
||||||
|
### Updating Team Info
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { updateTeamAction } from '../_lib/server/actions';
|
||||||
|
|
||||||
|
export function TeamSettingsForm({ team }) {
|
||||||
|
const { register, handleSubmit } = useForm({
|
||||||
|
defaultValues: {
|
||||||
|
name: team.name,
|
||||||
|
description: team.description,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = async (data) => {
|
||||||
|
await updateTeamAction({ teamId: team.id, ...data });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<input {...register('name')} placeholder="Team name" />
|
||||||
|
<textarea {...register('description')} placeholder="Description" />
|
||||||
|
<button type="submit">Save Changes</button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Team Avatar
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { uploadTeamAvatar } from '~/lib/teams/avatar';
|
||||||
|
|
||||||
|
const avatarUrl = await uploadTeamAvatar({
|
||||||
|
teamId: team.id,
|
||||||
|
file: avatarFile,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Activity Log
|
||||||
|
|
||||||
|
Track team activity for transparency:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { logActivity } from '~/lib/teams/activity';
|
||||||
|
|
||||||
|
await logActivity({
|
||||||
|
teamId: team.id,
|
||||||
|
userId: user.id,
|
||||||
|
action: 'member_invited',
|
||||||
|
metadata: {
|
||||||
|
invitedEmail: 'new@example.com',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Viewing Activity
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { getTeamActivity } from '~/lib/teams/activity';
|
||||||
|
|
||||||
|
const activities = await getTeamActivity(teamId, {
|
||||||
|
limit: 50,
|
||||||
|
offset: 0,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Team Switching
|
||||||
|
|
||||||
|
Allow users to switch between their teams:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useTeamAccountWorkspace } from '@kit/team-accounts/hooks/use-team-account-workspace';
|
||||||
|
|
||||||
|
export function TeamSwitcher() {
|
||||||
|
const { accounts, account } = useTeamAccountWorkspace();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<select
|
||||||
|
value={account.id}
|
||||||
|
onChange={(e) => switchTeam(e.target.value)}
|
||||||
|
>
|
||||||
|
{accounts.map((team) => (
|
||||||
|
<option key={team.id} value={team.id}>
|
||||||
|
{team.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notifications
|
||||||
|
|
||||||
|
### Member Joined
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
await createNotification({
|
||||||
|
teamId: team.id,
|
||||||
|
title: 'New Member',
|
||||||
|
message: `${user.name} joined the team`,
|
||||||
|
type: 'info',
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Role Changed
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
await createNotification({
|
||||||
|
userId: member.userId,
|
||||||
|
title: 'Role Updated',
|
||||||
|
message: `Your role was changed to ${newRole}`,
|
||||||
|
type: 'info',
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Clear role hierarchy** - Define roles that make sense for your use case
|
||||||
|
2. **Principle of least privilege** - Give minimum required permissions
|
||||||
|
3. **Audit trail** - Log important team actions
|
||||||
|
4. **Easy onboarding** - Simple invitation process
|
||||||
|
5. **Self-service** - Let members manage their own settings
|
||||||
|
6. **Transparent billing** - Show usage and costs clearly
|
||||||
@@ -0,0 +1,350 @@
|
|||||||
|
---
|
||||||
|
title: "Configuration"
|
||||||
|
description: "Configure your application settings and feature flags."
|
||||||
|
publishedAt: 2024-04-11
|
||||||
|
order: 4
|
||||||
|
status: "published"
|
||||||
|
---
|
||||||
|
|
||||||
|
> **Note:** This is mock/placeholder content for demonstration purposes.
|
||||||
|
|
||||||
|
Customize your application behavior through configuration files.
|
||||||
|
|
||||||
|
## Configuration Files
|
||||||
|
|
||||||
|
All configuration files are located in `apps/web/config/`:
|
||||||
|
|
||||||
|
```
|
||||||
|
config/
|
||||||
|
├── paths.config.ts # Route paths
|
||||||
|
├── billing.config.ts # Billing & pricing
|
||||||
|
├── feature-flags.config.ts # Feature toggles
|
||||||
|
├── personal-account-navigation.config.tsx
|
||||||
|
├── team-account-navigation.config.tsx
|
||||||
|
└── i18n.settings.ts # Internationalization
|
||||||
|
```
|
||||||
|
|
||||||
|
## Feature Flags
|
||||||
|
|
||||||
|
Control feature availability:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// config/feature-flags.config.ts
|
||||||
|
export const featureFlags = {
|
||||||
|
enableTeamAccounts: true,
|
||||||
|
enableBilling: true,
|
||||||
|
enableNotifications: true,
|
||||||
|
enableFileUploads: false,
|
||||||
|
enableAnalytics: true,
|
||||||
|
enableChat: false,
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Feature Flags
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { featureFlags } from '~/config/feature-flags.config';
|
||||||
|
|
||||||
|
export function ConditionalFeature() {
|
||||||
|
if (!featureFlags.enableChat) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <ChatWidget />;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Path Configuration
|
||||||
|
|
||||||
|
Define application routes:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// config/paths.config.ts
|
||||||
|
export const pathsConfig = {
|
||||||
|
auth: {
|
||||||
|
signIn: '/auth/sign-in',
|
||||||
|
signUp: '/auth/sign-up',
|
||||||
|
passwordReset: '/auth/password-reset',
|
||||||
|
callback: '/auth/callback',
|
||||||
|
},
|
||||||
|
app: {
|
||||||
|
home: '/home',
|
||||||
|
personalAccount: '/home',
|
||||||
|
teamAccount: '/home/[account]',
|
||||||
|
settings: '/home/settings',
|
||||||
|
billing: '/home/settings/billing',
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
home: '/admin',
|
||||||
|
users: '/admin/users',
|
||||||
|
analytics: '/admin/analytics',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Paths
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { pathsConfig } from '~/config/paths.config';
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
<Link href={pathsConfig.app.settings}>
|
||||||
|
Settings
|
||||||
|
</Link>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Navigation Configuration
|
||||||
|
|
||||||
|
### Personal Account Navigation
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// config/personal-account-navigation.config.tsx
|
||||||
|
import { HomeIcon, SettingsIcon } from 'lucide-react';
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
label: 'common:routes.home',
|
||||||
|
path: pathsConfig.app.personalAccount,
|
||||||
|
Icon: <HomeIcon className="w-4" />,
|
||||||
|
end: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'common:routes.settings',
|
||||||
|
path: pathsConfig.app.settings,
|
||||||
|
Icon: <SettingsIcon className="w-4" />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
### Team Account Navigation
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// config/team-account-navigation.config.tsx
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
label: 'common:routes.dashboard',
|
||||||
|
path: createPath(pathsConfig.app.teamAccount, account),
|
||||||
|
Icon: <LayoutDashboardIcon className="w-4" />,
|
||||||
|
end: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'common:routes.projects',
|
||||||
|
path: createPath(pathsConfig.app.projects, account),
|
||||||
|
Icon: <FolderIcon className="w-4" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'common:routes.members',
|
||||||
|
path: createPath(pathsConfig.app.members, account),
|
||||||
|
Icon: <UsersIcon className="w-4" />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
## Billing Configuration
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// config/billing.config.ts
|
||||||
|
export const billingConfig = {
|
||||||
|
provider: 'stripe', // 'stripe' | 'paddle'
|
||||||
|
enableTrial: true,
|
||||||
|
trialDays: 14,
|
||||||
|
|
||||||
|
plans: [
|
||||||
|
{
|
||||||
|
id: 'free',
|
||||||
|
name: 'Free',
|
||||||
|
price: 0,
|
||||||
|
features: ['5 projects', 'Basic support'],
|
||||||
|
limits: {
|
||||||
|
projects: 5,
|
||||||
|
members: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'pro',
|
||||||
|
name: 'Professional',
|
||||||
|
price: 29,
|
||||||
|
interval: 'month',
|
||||||
|
features: ['Unlimited projects', 'Priority support'],
|
||||||
|
limits: {
|
||||||
|
projects: -1, // unlimited
|
||||||
|
members: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Internationalization
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// lib/i18n/i18n.settings.ts
|
||||||
|
export const i18nSettings = {
|
||||||
|
defaultLocale: 'en',
|
||||||
|
locales: ['en', 'es', 'fr', 'de'],
|
||||||
|
defaultNamespace: 'common',
|
||||||
|
namespaces: ['common', 'auth', 'billing', 'errors'],
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Email Configuration
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// config/email.config.ts
|
||||||
|
export const emailConfig = {
|
||||||
|
from: {
|
||||||
|
email: process.env.EMAIL_FROM || 'noreply@example.com',
|
||||||
|
name: process.env.EMAIL_FROM_NAME || 'Your App',
|
||||||
|
},
|
||||||
|
provider: 'resend', // 'resend' | 'sendgrid' | 'postmark'
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## SEO Configuration
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// config/seo.config.ts
|
||||||
|
export const seoConfig = {
|
||||||
|
title: 'Your App Name',
|
||||||
|
description: 'Your app description',
|
||||||
|
ogImage: '/images/og-image.png',
|
||||||
|
twitterHandle: '@yourapp',
|
||||||
|
locale: 'en_US',
|
||||||
|
|
||||||
|
// Per-page overrides
|
||||||
|
pages: {
|
||||||
|
home: {
|
||||||
|
title: 'Home - Your App',
|
||||||
|
description: 'Welcome to your app',
|
||||||
|
},
|
||||||
|
pricing: {
|
||||||
|
title: 'Pricing - Your App',
|
||||||
|
description: 'Simple, transparent pricing',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Theme Configuration
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// config/theme.config.ts
|
||||||
|
export const themeConfig = {
|
||||||
|
defaultTheme: 'system', // 'light' | 'dark' | 'system'
|
||||||
|
enableColorSchemeToggle: true,
|
||||||
|
|
||||||
|
colors: {
|
||||||
|
primary: 'blue',
|
||||||
|
accent: 'purple',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Analytics Configuration
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// config/analytics.config.ts
|
||||||
|
export const analyticsConfig = {
|
||||||
|
googleAnalytics: {
|
||||||
|
enabled: true,
|
||||||
|
measurementId: process.env.NEXT_PUBLIC_GA_ID,
|
||||||
|
},
|
||||||
|
|
||||||
|
posthog: {
|
||||||
|
enabled: false,
|
||||||
|
apiKey: process.env.NEXT_PUBLIC_POSTHOG_KEY,
|
||||||
|
},
|
||||||
|
|
||||||
|
plausible: {
|
||||||
|
enabled: false,
|
||||||
|
domain: process.env.NEXT_PUBLIC_PLAUSIBLE_DOMAIN,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rate Limiting
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// config/rate-limit.config.ts
|
||||||
|
export const rateLimitConfig = {
|
||||||
|
api: {
|
||||||
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||||
|
max: 100, // requests per window
|
||||||
|
},
|
||||||
|
|
||||||
|
auth: {
|
||||||
|
windowMs: 15 * 60 * 1000,
|
||||||
|
max: 5, // login attempts
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Upload Configuration
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// config/upload.config.ts
|
||||||
|
export const uploadConfig = {
|
||||||
|
maxFileSize: 5 * 1024 * 1024, // 5MB
|
||||||
|
allowedMimeTypes: [
|
||||||
|
'image/jpeg',
|
||||||
|
'image/png',
|
||||||
|
'image/gif',
|
||||||
|
'image/webp',
|
||||||
|
'application/pdf',
|
||||||
|
],
|
||||||
|
|
||||||
|
storage: {
|
||||||
|
provider: 'supabase', // 'supabase' | 's3' | 'cloudinary'
|
||||||
|
bucket: 'uploads',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment-Specific Config
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// config/app.config.ts
|
||||||
|
const isDev = process.env.NODE_ENV === 'development';
|
||||||
|
const isProd = process.env.NODE_ENV === 'production';
|
||||||
|
|
||||||
|
export const appConfig = {
|
||||||
|
environment: process.env.NODE_ENV,
|
||||||
|
apiUrl: isProd
|
||||||
|
? 'https://api.yourapp.com'
|
||||||
|
: 'http://localhost:3000/api',
|
||||||
|
|
||||||
|
features: {
|
||||||
|
enableDebugTools: isDev,
|
||||||
|
enableErrorReporting: isProd,
|
||||||
|
enableAnalytics: isProd,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Use environment variables** for secrets
|
||||||
|
2. **Type your configs** for autocomplete and safety
|
||||||
|
3. **Document options** with comments
|
||||||
|
4. **Validate on startup** to catch errors early
|
||||||
|
5. **Keep configs simple** - avoid complex logic
|
||||||
|
6. **Use feature flags** for gradual rollouts
|
||||||
|
7. **Environment-specific values** for dev/prod differences
|
||||||
|
|
||||||
|
## Loading Configuration
|
||||||
|
|
||||||
|
Configs are automatically loaded but you can validate:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// lib/config/validate-config.ts
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const ConfigSchema = z.object({
|
||||||
|
apiUrl: z.string().url(),
|
||||||
|
enableFeatureX: z.boolean(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export function validateConfig(config: unknown) {
|
||||||
|
return ConfigSchema.parse(config);
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
---
|
---
|
||||||
title: "Getting started with Makerkit"
|
title: "Introduction"
|
||||||
description: "Makerkit is a SaaS Starter Kit that helps you build a SaaS. Learn how to get started with Makerkit."
|
description: "Makerkit is a SaaS Starter Kit that helps you build a SaaS. Learn how to get started with Makerkit."
|
||||||
publishedAt: 2024-04-11
|
publishedAt: 2024-04-11
|
||||||
order: 0
|
order: 0
|
||||||
status: "published"
|
status: "published"
|
||||||
---
|
---
|
||||||
|
|
||||||
|
> **Note:** This is mock/placeholder content for demonstration purposes.
|
||||||
|
|
||||||
Makerkit is a SaaS Starter Kit that helps you build a SaaS. It provides you with a set of tools and best practices to help you build a SaaS quickly and efficiently.
|
Makerkit is a SaaS Starter Kit that helps you build a SaaS. It provides you with a set of tools and best practices to help you build a SaaS quickly and efficiently.
|
||||||
|
|
||||||
## Getting started
|
## Getting started
|
||||||
|
|||||||
@@ -2,10 +2,12 @@
|
|||||||
title: "Installing Dependencies"
|
title: "Installing Dependencies"
|
||||||
description: "Learn how to install dependencies for your project."
|
description: "Learn how to install dependencies for your project."
|
||||||
publishedAt: 2024-04-11
|
publishedAt: 2024-04-11
|
||||||
order: 0
|
order: 1
|
||||||
status: "published"
|
status: "published"
|
||||||
---
|
---
|
||||||
|
|
||||||
|
> **Note:** This is mock/placeholder content for demonstration purposes.
|
||||||
|
|
||||||
To install dependencies in your project, please install `pnpm` by running the following command:
|
To install dependencies in your project, please install `pnpm` by running the following command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -0,0 +1,247 @@
|
|||||||
|
---
|
||||||
|
title: "Project Structure"
|
||||||
|
description: "Understanding the monorepo structure and organization."
|
||||||
|
publishedAt: 2024-04-11
|
||||||
|
order: 3
|
||||||
|
status: "published"
|
||||||
|
---
|
||||||
|
|
||||||
|
> **Note:** This is mock/placeholder content for demonstration purposes.
|
||||||
|
|
||||||
|
Learn how the codebase is organized and where to find things.
|
||||||
|
|
||||||
|
## Monorepo Overview
|
||||||
|
|
||||||
|
This project uses Turborepo to manage a monorepo with multiple apps and packages.
|
||||||
|
|
||||||
|
```
|
||||||
|
project-root/
|
||||||
|
├── apps/ # Applications
|
||||||
|
│ ├── web/ # Main Next.js app
|
||||||
|
│ ├── e2e/ # Playwright E2E tests
|
||||||
|
│ └── dev-tool/ # Development utilities
|
||||||
|
├── packages/ # Shared packages
|
||||||
|
│ ├── features/ # Feature packages
|
||||||
|
│ ├── ui/ # UI components
|
||||||
|
│ ├── supabase/ # Supabase utilities
|
||||||
|
│ └── billing/ # Billing integrations
|
||||||
|
├── tooling/ # Development tools
|
||||||
|
├── supabase/ # Database schema & migrations
|
||||||
|
└── docs/ # Documentation
|
||||||
|
```
|
||||||
|
|
||||||
|
## Main Application (`apps/web`)
|
||||||
|
|
||||||
|
The primary Next.js application:
|
||||||
|
|
||||||
|
```
|
||||||
|
apps/web/
|
||||||
|
├── app/ # Next.js App Router
|
||||||
|
│ ├── (marketing)/ # Public pages
|
||||||
|
│ ├── (auth)/ # Authentication
|
||||||
|
│ ├── home/ # Main application
|
||||||
|
│ │ ├── (user)/ # Personal account
|
||||||
|
│ │ └── [account]/ # Team accounts
|
||||||
|
│ ├── admin/ # Admin panel
|
||||||
|
│ └── api/ # API routes
|
||||||
|
├── components/ # Shared components
|
||||||
|
├── config/ # Configuration files
|
||||||
|
├── lib/ # Utility functions
|
||||||
|
├── public/ # Static assets
|
||||||
|
└── supabase/ # Supabase setup
|
||||||
|
```
|
||||||
|
|
||||||
|
## Route Structure
|
||||||
|
|
||||||
|
### Marketing Routes (`(marketing)`)
|
||||||
|
|
||||||
|
Public-facing pages:
|
||||||
|
|
||||||
|
```
|
||||||
|
app/(marketing)/
|
||||||
|
├── page.tsx # Landing page
|
||||||
|
├── pricing/ # Pricing page
|
||||||
|
├── blog/ # Blog
|
||||||
|
└── docs/ # Documentation
|
||||||
|
```
|
||||||
|
|
||||||
|
### Auth Routes (`(auth)`)
|
||||||
|
|
||||||
|
Authentication pages:
|
||||||
|
|
||||||
|
```
|
||||||
|
app/(auth)/
|
||||||
|
├── sign-in/
|
||||||
|
├── sign-up/
|
||||||
|
├── password-reset/
|
||||||
|
└── verify/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Application Routes (`home`)
|
||||||
|
|
||||||
|
Main application:
|
||||||
|
|
||||||
|
```
|
||||||
|
app/home/
|
||||||
|
├── (user)/ # Personal account context
|
||||||
|
│ ├── page.tsx # Personal dashboard
|
||||||
|
│ ├── settings/ # User settings
|
||||||
|
│ └── projects/ # Personal projects
|
||||||
|
└── [account]/ # Team account context
|
||||||
|
├── page.tsx # Team dashboard
|
||||||
|
├── settings/ # Team settings
|
||||||
|
├── projects/ # Team projects
|
||||||
|
└── members/ # Team members
|
||||||
|
```
|
||||||
|
|
||||||
|
## Packages Structure
|
||||||
|
|
||||||
|
### Feature Packages (`packages/features/`)
|
||||||
|
|
||||||
|
Modular features:
|
||||||
|
|
||||||
|
```
|
||||||
|
packages/features/
|
||||||
|
├── accounts/ # Account management
|
||||||
|
├── auth/ # Authentication
|
||||||
|
├── team-accounts/ # Team features
|
||||||
|
├── billing/ # Billing & subscriptions
|
||||||
|
├── admin/ # Admin features
|
||||||
|
└── notifications/ # Notification system
|
||||||
|
```
|
||||||
|
|
||||||
|
### UI Package (`packages/ui/`)
|
||||||
|
|
||||||
|
Shared UI components:
|
||||||
|
|
||||||
|
```
|
||||||
|
packages/ui/
|
||||||
|
└── src/
|
||||||
|
├── components/ # Shadcn UI components
|
||||||
|
│ ├── button.tsx
|
||||||
|
│ ├── input.tsx
|
||||||
|
│ ├── dialog.tsx
|
||||||
|
│ └── ...
|
||||||
|
└── utils/ # UI utilities
|
||||||
|
```
|
||||||
|
|
||||||
|
### Supabase Package (`packages/supabase/`)
|
||||||
|
|
||||||
|
Database utilities:
|
||||||
|
|
||||||
|
```
|
||||||
|
packages/supabase/
|
||||||
|
├── schema/ # Declarative schemas
|
||||||
|
│ ├── accounts.schema.ts
|
||||||
|
│ ├── auth.schema.ts
|
||||||
|
│ └── ...
|
||||||
|
├── src/
|
||||||
|
│ ├── clients/ # Supabase clients
|
||||||
|
│ ├── hooks/ # React hooks
|
||||||
|
│ └── middleware/ # Auth middleware
|
||||||
|
└── migrations/ # SQL migrations
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration Files
|
||||||
|
|
||||||
|
### Root Level
|
||||||
|
|
||||||
|
```
|
||||||
|
project-root/
|
||||||
|
├── package.json # Root package.json
|
||||||
|
├── turbo.json # Turborepo config
|
||||||
|
├── pnpm-workspace.yaml # PNPM workspace
|
||||||
|
└── tsconfig.json # Base TypeScript config
|
||||||
|
```
|
||||||
|
|
||||||
|
### Application Level
|
||||||
|
|
||||||
|
```
|
||||||
|
apps/web/
|
||||||
|
├── next.config.js # Next.js configuration
|
||||||
|
├── tailwind.config.ts # Tailwind CSS
|
||||||
|
├── tsconfig.json # TypeScript config
|
||||||
|
└── .env.local # Environment variables
|
||||||
|
```
|
||||||
|
|
||||||
|
### Feature Configuration
|
||||||
|
|
||||||
|
```
|
||||||
|
apps/web/config/
|
||||||
|
├── paths.config.ts # Route paths
|
||||||
|
├── billing.config.ts # Billing settings
|
||||||
|
├── feature-flags.config.ts # Feature flags
|
||||||
|
├── personal-account-navigation.config.tsx
|
||||||
|
└── team-account-navigation.config.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
## Naming Conventions
|
||||||
|
|
||||||
|
### Files
|
||||||
|
|
||||||
|
- **Pages**: `page.tsx` (Next.js convention)
|
||||||
|
- **Layouts**: `layout.tsx`
|
||||||
|
- **Components**: `kebab-case.tsx`
|
||||||
|
- **Utilities**: `kebab-case.ts`
|
||||||
|
- **Types**: `types.ts` or `component-name.types.ts`
|
||||||
|
|
||||||
|
### Directories
|
||||||
|
|
||||||
|
- **Route segments**: `[param]` for dynamic
|
||||||
|
- **Route groups**: `(group)` for organization
|
||||||
|
- **Private folders**: `_components`, `_lib`
|
||||||
|
- **Parallel routes**: `@folder`
|
||||||
|
|
||||||
|
### Code Organization
|
||||||
|
|
||||||
|
```
|
||||||
|
feature/
|
||||||
|
├── page.tsx # Route page
|
||||||
|
├── layout.tsx # Route layout
|
||||||
|
├── loading.tsx # Loading state
|
||||||
|
├── error.tsx # Error boundary
|
||||||
|
├── _components/ # Private components
|
||||||
|
│ ├── feature-list.tsx
|
||||||
|
│ └── feature-form.tsx
|
||||||
|
└── _lib/ # Private utilities
|
||||||
|
├── server/ # Server-side code
|
||||||
|
│ ├── loaders.ts
|
||||||
|
│ └── actions.ts
|
||||||
|
└── schemas/ # Validation schemas
|
||||||
|
└── feature.schema.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Import Paths
|
||||||
|
|
||||||
|
Use TypeScript path aliases:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Absolute imports from packages
|
||||||
|
import { Button } from '@kit/ui/button';
|
||||||
|
import { createClient } from '@kit/supabase/server-client';
|
||||||
|
|
||||||
|
// Relative imports within app
|
||||||
|
import { FeatureList } from './_components/feature-list';
|
||||||
|
import { loadData } from './_lib/server/loaders';
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Keep route-specific code private** - Use `_components` and `_lib`
|
||||||
|
2. **Share reusable code** - Extract to packages
|
||||||
|
3. **Collocate related files** - Keep files near where they're used
|
||||||
|
4. **Use consistent naming** - Follow established patterns
|
||||||
|
5. **Organize by feature** - Not by file type
|
||||||
|
|
||||||
|
## Finding Your Way
|
||||||
|
|
||||||
|
| Looking for... | Location |
|
||||||
|
|----------------|----------|
|
||||||
|
| UI Components | `packages/ui/src/components/` |
|
||||||
|
| Database Schema | `packages/supabase/schema/` |
|
||||||
|
| API Routes | `apps/web/app/api/` |
|
||||||
|
| Auth Logic | `packages/features/auth/` |
|
||||||
|
| Billing Code | `packages/features/billing/` |
|
||||||
|
| Team Features | `packages/features/team-accounts/` |
|
||||||
|
| Config Files | `apps/web/config/` |
|
||||||
|
| Types | `*.types.ts` files throughout |
|
||||||
133
apps/web/content/documentation/getting-started/quick-start.mdoc
Normal file
133
apps/web/content/documentation/getting-started/quick-start.mdoc
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
---
|
||||||
|
title: "Quick Start"
|
||||||
|
description: "Get your application running in minutes with this quick start guide."
|
||||||
|
publishedAt: 2024-04-11
|
||||||
|
order: 2
|
||||||
|
status: "published"
|
||||||
|
---
|
||||||
|
|
||||||
|
> **Note:** This is mock/placeholder content for demonstration purposes.
|
||||||
|
|
||||||
|
Get your development environment up and running quickly.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Before you begin, ensure you have:
|
||||||
|
- **Node.js** 18.x or higher
|
||||||
|
- **pnpm** 8.x or higher
|
||||||
|
- **Git** for version control
|
||||||
|
- A **Supabase** account (free tier works great)
|
||||||
|
|
||||||
|
## Step 1: Clone the Repository
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/yourorg/yourapp.git
|
||||||
|
cd yourapp
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 2: Install Dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
This will install all required dependencies across the monorepo.
|
||||||
|
|
||||||
|
## Step 3: Set Up Environment Variables
|
||||||
|
|
||||||
|
Copy the example environment file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp apps/web/.env.example apps/web/.env.local
|
||||||
|
```
|
||||||
|
|
||||||
|
Update the following variables:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Supabase Configuration
|
||||||
|
NEXT_PUBLIC_SUPABASE_URL=http://localhost:54321
|
||||||
|
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key-here
|
||||||
|
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key-here
|
||||||
|
|
||||||
|
# Application
|
||||||
|
NEXT_PUBLIC_SITE_URL=http://localhost:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 4: Start Supabase
|
||||||
|
|
||||||
|
Start your local Supabase instance:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm supabase:web:start
|
||||||
|
```
|
||||||
|
|
||||||
|
This will:
|
||||||
|
- Start PostgreSQL database
|
||||||
|
- Start Supabase Studio (localhost:54323)
|
||||||
|
- Apply all migrations
|
||||||
|
- Seed initial data
|
||||||
|
|
||||||
|
## Step 5: Start Development Server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Your application will be available at:
|
||||||
|
- **App**: http://localhost:3000
|
||||||
|
- **Supabase Studio**: http://localhost:54323
|
||||||
|
- **Email Testing**: http://localhost:54324
|
||||||
|
|
||||||
|
## Step 6: Create Your First User
|
||||||
|
|
||||||
|
1. Navigate to http://localhost:3000/auth/sign-up
|
||||||
|
2. Enter your email and password
|
||||||
|
3. Check http://localhost:54324 for the confirmation email
|
||||||
|
4. Click the confirmation link
|
||||||
|
5. You're ready to go!
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
Now that your app is running:
|
||||||
|
|
||||||
|
1. **Explore the Dashboard** - Check out the main features
|
||||||
|
2. **Review the Code** - Familiarize yourself with the structure
|
||||||
|
3. **Read the Docs** - Learn about key concepts
|
||||||
|
4. **Build Your Feature** - Start customizing
|
||||||
|
|
||||||
|
## Common Issues
|
||||||
|
|
||||||
|
### Port Already in Use
|
||||||
|
|
||||||
|
If port 3000 is already in use:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Find and kill the process
|
||||||
|
lsof -i :3000
|
||||||
|
kill -9 <PID>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Supabase Won't Start
|
||||||
|
|
||||||
|
Try resetting Supabase:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm supabase:web:stop
|
||||||
|
docker system prune -a # Clean Docker
|
||||||
|
pnpm supabase:web:start
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Connection Error
|
||||||
|
|
||||||
|
Ensure Docker is running and restart Supabase:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker ps # Check Docker is running
|
||||||
|
pnpm supabase:web:reset
|
||||||
|
```
|
||||||
|
|
||||||
|
## What's Next?
|
||||||
|
|
||||||
|
- Learn about the [project structure](/docs/getting-started/project-structure)
|
||||||
|
- Understand [configuration options](/docs/getting-started/configuration)
|
||||||
|
- Follow [best practices](/docs/development/workflow)
|
||||||
@@ -8,6 +8,8 @@ publishedAt: 2024-04-11
|
|||||||
status: "published"
|
status: "published"
|
||||||
---
|
---
|
||||||
|
|
||||||
|
> **Note:** This is mock/placeholder content for demonstration purposes.
|
||||||
|
|
||||||
When it comes to choosing a SaaS starter kit for your business, it's essential to look for certain features that will ensure the smooth functioning and growth of your operations. Here are five must-have features that every SaaS starter kit should include:
|
When it comes to choosing a SaaS starter kit for your business, it's essential to look for certain features that will ensure the smooth functioning and growth of your operations. Here are five must-have features that every SaaS starter kit should include:
|
||||||
|
|
||||||
## 1. User Management
|
## 1. User Management
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ publishedAt: 2024-04-12
|
|||||||
status: "published"
|
status: "published"
|
||||||
---
|
---
|
||||||
|
|
||||||
## Niche Service Solutions
|
> **Note:** This is mock/placeholder content for demonstration purposes.
|
||||||
|
|
||||||
Consider identifying a specific niche or industry with underserved needs. Whether it's project management tools tailored for creative freelancers or appointment scheduling software for niche healthcare practitioners, targeting a specific market can lead to a loyal customer base hungry for tailored solutions.
|
Consider identifying a specific niche or industry with underserved needs. Whether it's project management tools tailored for creative freelancers or appointment scheduling software for niche healthcare practitioners, targeting a specific market can lead to a loyal customer base hungry for tailored solutions.
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ publishedAt: 2024-04-10
|
|||||||
status: "published"
|
status: "published"
|
||||||
---
|
---
|
||||||
|
|
||||||
|
> **Note:** This is mock/placeholder content for demonstration purposes.
|
||||||
|
|
||||||
In the dynamic world of entrepreneurship, the Software as a Service (SaaS) model has emerged as a beacon of opportunity for indie hackers – individuals or small teams with big dreams of creating impactful software solutions. With the right tools and strategies, launching a successful SaaS startup is within reach for anyone with passion, dedication, and a clear vision. To empower aspiring entrepreneurs on this journey, we've curated a comprehensive starter kit tailored specifically for SaaS and indie hackers.
|
In the dynamic world of entrepreneurship, the Software as a Service (SaaS) model has emerged as a beacon of opportunity for indie hackers – individuals or small teams with big dreams of creating impactful software solutions. With the right tools and strategies, launching a successful SaaS startup is within reach for anyone with passion, dedication, and a clear vision. To empower aspiring entrepreneurs on this journey, we've curated a comprehensive starter kit tailored specifically for SaaS and indie hackers.
|
||||||
|
|
||||||
## 1. Idea Generation and Validation
|
## 1. Idea Generation and Validation
|
||||||
|
|||||||
@@ -60,13 +60,13 @@
|
|||||||
"@tanstack/react-query": "5.90.5",
|
"@tanstack/react-query": "5.90.5",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"lucide-react": "^0.548.0",
|
"lucide-react": "^0.552.0",
|
||||||
"next": "catalog:",
|
"next": "catalog:",
|
||||||
"next-sitemap": "^4.2.3",
|
"next-sitemap": "^4.2.3",
|
||||||
"next-themes": "0.4.6",
|
"next-themes": "0.4.6",
|
||||||
"react": "19.2.0",
|
"react": "19.2.0",
|
||||||
"react-dom": "19.2.0",
|
"react-dom": "19.2.0",
|
||||||
"react-hook-form": "^7.65.0",
|
"react-hook-form": "^7.66.0",
|
||||||
"react-i18next": "^16.2.3",
|
"react-i18next": "^16.2.3",
|
||||||
"recharts": "2.15.3",
|
"recharts": "2.15.3",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
{
|
{
|
||||||
"blog": "Blog",
|
"blog": "Blog",
|
||||||
"blogSubtitle": "News and updates about the platform",
|
"blogSubtitle": "News and updates about the platform",
|
||||||
|
"changelog": "Changelog",
|
||||||
|
"changelogSubtitle": "Latest updates and improvements to the platform",
|
||||||
|
"noChangelogEntries": "No changelog entries found",
|
||||||
|
"changelogPaginationNext": "Next Page",
|
||||||
|
"changelogPaginationPrevious": "Previous Page",
|
||||||
|
"changelogNavigationPrevious": "Previous",
|
||||||
|
"changelogNavigationNext": "Next",
|
||||||
"documentation": "Documentation",
|
"documentation": "Documentation",
|
||||||
"documentationSubtitle": "Tutorials and guide to get started with the platform",
|
"documentationSubtitle": "Tutorials and guide to get started with the platform",
|
||||||
"faq": "FAQ",
|
"faq": "FAQ",
|
||||||
|
|||||||
@@ -15,15 +15,15 @@
|
|||||||
@import './makerkit.css';
|
@import './makerkit.css';
|
||||||
|
|
||||||
/* plugins - update the below if you add a new plugin */
|
/* plugins - update the below if you add a new plugin */
|
||||||
@plugin "tailwindcss-animate";
|
@plugin 'tailwindcss-animate';
|
||||||
|
|
||||||
/* content sources - update the below if you add a new path */
|
/* content sources - update the below if you add a new path */
|
||||||
@source "../../../packages/*/src/**/*.{ts,tsx}";
|
@source '../../../packages/*/src/**/*.{ts,tsx}';
|
||||||
@source "../../../packages/features/*/src/**/*.{ts,tsx}";
|
@source '../../../packages/features/*/src/**/*.{ts,tsx}';
|
||||||
@source "../../../packages/billing/*/src/**/*.{ts,tsx}";
|
@source '../../../packages/billing/*/src/**/*.{ts,tsx}';
|
||||||
@source "../../../packages/plugins/*/src/**/*.{ts,tsx}";
|
@source '../../../packages/plugins/*/src/**/*.{ts,tsx}';
|
||||||
@source "../../../packages/cms/*/src/**/*.{ts,tsx}";
|
@source '../../../packages/cms/*/src/**/*.{ts,tsx}';
|
||||||
@source "../{app,components,config,lib}/**/*.{ts,tsx}";
|
@source '../{app,components,config,lib}/**/*.{ts,tsx}';
|
||||||
|
|
||||||
/* variants - update the below if you add a new variant */
|
/* variants - update the below if you add a new variant */
|
||||||
@variant dark (&:where(.dark, .dark *));
|
@variant dark (&:where(.dark, .dark *));
|
||||||
@@ -36,6 +36,15 @@
|
|||||||
'calt' 1;
|
'calt' 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
scroll-padding-top: 56px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[id] {
|
||||||
|
scroll-margin-top: 56px;
|
||||||
|
}
|
||||||
|
|
||||||
*,
|
*,
|
||||||
::after,
|
::after,
|
||||||
::before,
|
::before,
|
||||||
|
|||||||
@@ -5,28 +5,24 @@
|
|||||||
* the blog post, documentation, etc.
|
* the blog post, documentation, etc.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.markdoc {
|
|
||||||
@apply text-foreground;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdoc h1 {
|
.markdoc h1 {
|
||||||
@apply font-heading text-foreground text-3xl font-medium tracking-tight lg:mt-14 dark:text-white;
|
@apply font-heading text-foreground mt-14 text-3xl font-semibold tracking-tight dark:text-white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdoc h2 {
|
.markdoc h2 {
|
||||||
@apply font-heading text-foreground text-2xl font-medium tracking-tight lg:mt-6 lg:mb-3 dark:text-white;
|
@apply font-heading text-foreground mt-10 mb-2 text-xl font-semibold tracking-tight dark:text-white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdoc h3 {
|
.markdoc h3 {
|
||||||
@apply font-heading text-foreground text-xl font-medium tracking-tight lg:mt-12 dark:text-white;
|
@apply font-heading text-foreground mt-8 mb-4 text-lg font-semibold tracking-tight dark:text-white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdoc h4 {
|
.markdoc h4 {
|
||||||
@apply text-foreground mt-4 text-lg font-medium tracking-tight lg:mt-8 dark:text-white;
|
@apply text-foreground mt-6 text-base font-medium tracking-tight dark:text-white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdoc h5 {
|
.markdoc h5 {
|
||||||
@apply text-foreground mt-3 text-base font-medium tracking-tight lg:mt-6 dark:text-white;
|
@apply text-foreground mt-5 text-sm font-medium tracking-tight dark:text-white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdoc h6 {
|
.markdoc h6 {
|
||||||
@@ -34,11 +30,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.markdoc p {
|
.markdoc p {
|
||||||
@apply text-muted-foreground my-3 text-base leading-7;
|
@apply mt-7 text-base/7 text-gray-700 dark:text-gray-300;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdoc li {
|
.markdoc li {
|
||||||
@apply text-muted-foreground relative my-0.5 text-base leading-7;
|
@apply relative mt-2 text-base/7 text-gray-700 dark:text-gray-300;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdoc ul > li:before {
|
.markdoc ul > li:before {
|
||||||
@@ -86,11 +82,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.markdoc pre {
|
.markdoc pre {
|
||||||
@apply bg-muted/50 border-border text-foreground my-4 overflow-x-auto rounded-md border p-4 font-mono text-sm;
|
@apply bg-muted/50 border-border text-foreground my-4 overflow-x-auto rounded-md border px-4 py-2 font-mono text-sm;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdoc blockquote {
|
.markdoc blockquote {
|
||||||
@apply border-primary text-muted-foreground my-4 border border-l-8 px-6 py-4 text-lg font-medium;
|
@apply my-4 rounded-md border px-4 py-2 text-base/7 text-gray-700 dark:text-gray-300;
|
||||||
|
|
||||||
|
p {
|
||||||
|
@apply mt-0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdoc a {
|
.markdoc a {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "next-supabase-saas-kit-turbo",
|
"name": "next-supabase-saas-kit-turbo",
|
||||||
"version": "2.19.3",
|
"version": "2.20.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -40,10 +40,10 @@
|
|||||||
"packageManager": "pnpm@10.19.0",
|
"packageManager": "pnpm@10.19.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@manypkg/cli": "^0.25.1",
|
"@manypkg/cli": "^0.25.1",
|
||||||
"@turbo/gen": "^2.5.8",
|
"@turbo/gen": "^2.6.0",
|
||||||
"cross-env": "^10.0.0",
|
"cross-env": "^10.0.0",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"turbo": "2.5.8",
|
"turbo": "2.6.0",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,10 +29,10 @@
|
|||||||
"@supabase/supabase-js": "2.78.0",
|
"@supabase/supabase-js": "2.78.0",
|
||||||
"@types/react": "catalog:",
|
"@types/react": "catalog:",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"lucide-react": "^0.548.0",
|
"lucide-react": "^0.552.0",
|
||||||
"next": "catalog:",
|
"next": "catalog:",
|
||||||
"react": "19.2.0",
|
"react": "19.2.0",
|
||||||
"react-hook-form": "^7.65.0",
|
"react-hook-form": "^7.66.0",
|
||||||
"react-i18next": "^16.2.3",
|
"react-i18next": "^16.2.3",
|
||||||
"zod": "^3.25.74"
|
"zod": "^3.25.74"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -290,7 +290,7 @@ class KeystaticClient implements CmsClient {
|
|||||||
publishedAt: publishedAt.toISOString(),
|
publishedAt: publishedAt.toISOString(),
|
||||||
content: html as string,
|
content: html as string,
|
||||||
image: item.entry.image ?? undefined,
|
image: item.entry.image ?? undefined,
|
||||||
status: item.entry.status,
|
status: item.entry.status as Cms.ContentItemStatus,
|
||||||
collapsible: item.entry.collapsible,
|
collapsible: item.entry.collapsible,
|
||||||
collapsed: item.entry.collapsed,
|
collapsed: item.entry.collapsed,
|
||||||
categories:
|
categories:
|
||||||
@@ -344,7 +344,7 @@ class KeystaticClient implements CmsClient {
|
|||||||
publishedAt: publishedAt.toISOString(),
|
publishedAt: publishedAt.toISOString(),
|
||||||
content: html as string,
|
content: html as string,
|
||||||
image: item.entry.image ?? undefined,
|
image: item.entry.image ?? undefined,
|
||||||
status: item.entry.status,
|
status: item.entry.status as Cms.ContentItemStatus,
|
||||||
categories:
|
categories:
|
||||||
(item.entry.categories ?? []).map((item) => {
|
(item.entry.categories ?? []).map((item) => {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -43,6 +43,10 @@ export type DocumentationEntryProps = Entry<
|
|||||||
(typeof keyStaticConfig)['collections']['documentation']
|
(typeof keyStaticConfig)['collections']['documentation']
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
export type ChangelogEntryProps = Entry<
|
||||||
|
(typeof keyStaticConfig)['collections']['changelog']
|
||||||
|
>;
|
||||||
|
|
||||||
function createKeyStaticConfig(path = '') {
|
function createKeyStaticConfig(path = '') {
|
||||||
if (path && !path.endsWith('/')) {
|
if (path && !path.endsWith('/')) {
|
||||||
path += '/';
|
path += '/';
|
||||||
@@ -62,6 +66,19 @@ function createKeyStaticConfig(path = '') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getKeystaticCollections(path: string) {
|
function getKeystaticCollections(path: string) {
|
||||||
|
const statusOptions = [
|
||||||
|
{ label: 'Draft', value: 'draft' },
|
||||||
|
{ label: 'Published', value: 'published' },
|
||||||
|
{ label: 'Review', value: 'review' },
|
||||||
|
{ label: 'Pending', value: 'pending' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const imageField = fields.image({
|
||||||
|
label: 'Image',
|
||||||
|
directory: 'public/site/images',
|
||||||
|
publicPath: '/site/images',
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
posts: collection({
|
posts: collection({
|
||||||
label: 'Posts',
|
label: 'Posts',
|
||||||
@@ -74,11 +91,7 @@ function getKeystaticCollections(path: string) {
|
|||||||
label: 'Label',
|
label: 'Label',
|
||||||
validation: { isRequired: false },
|
validation: { isRequired: false },
|
||||||
}),
|
}),
|
||||||
image: fields.image({
|
image: imageField,
|
||||||
label: 'Image',
|
|
||||||
directory: 'public/site/images',
|
|
||||||
publicPath: '/site/images',
|
|
||||||
}),
|
|
||||||
categories: fields.array(fields.text({ label: 'Category' })),
|
categories: fields.array(fields.text({ label: 'Category' })),
|
||||||
tags: fields.array(fields.text({ label: 'Tag' })),
|
tags: fields.array(fields.text({ label: 'Tag' })),
|
||||||
description: fields.text({ label: 'Description' }),
|
description: fields.text({ label: 'Description' }),
|
||||||
@@ -93,12 +106,7 @@ function getKeystaticCollections(path: string) {
|
|||||||
status: fields.select({
|
status: fields.select({
|
||||||
defaultValue: 'draft',
|
defaultValue: 'draft',
|
||||||
label: 'Status',
|
label: 'Status',
|
||||||
options: [
|
options: statusOptions,
|
||||||
{ label: 'Draft', value: 'draft' },
|
|
||||||
{ label: 'Published', value: 'published' },
|
|
||||||
{ label: 'Review', value: 'review' },
|
|
||||||
{ label: 'Pending', value: 'pending' },
|
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -114,11 +122,7 @@ function getKeystaticCollections(path: string) {
|
|||||||
validation: { isRequired: false },
|
validation: { isRequired: false },
|
||||||
}),
|
}),
|
||||||
content: getContentField(),
|
content: getContentField(),
|
||||||
image: fields.image({
|
image: imageField,
|
||||||
label: 'Image',
|
|
||||||
directory: 'public/site/images',
|
|
||||||
publicPath: '/site/images',
|
|
||||||
}),
|
|
||||||
description: fields.text({ label: 'Description' }),
|
description: fields.text({ label: 'Description' }),
|
||||||
publishedAt: fields.date({ label: 'Published At' }),
|
publishedAt: fields.date({ label: 'Published At' }),
|
||||||
order: fields.number({ label: 'Order' }),
|
order: fields.number({ label: 'Order' }),
|
||||||
@@ -132,12 +136,7 @@ function getKeystaticCollections(path: string) {
|
|||||||
status: fields.select({
|
status: fields.select({
|
||||||
defaultValue: 'draft',
|
defaultValue: 'draft',
|
||||||
label: 'Status',
|
label: 'Status',
|
||||||
options: [
|
options: statusOptions,
|
||||||
{ label: 'Draft', value: 'draft' },
|
|
||||||
{ label: 'Published', value: 'published' },
|
|
||||||
{ label: 'Review', value: 'review' },
|
|
||||||
{ label: 'Pending', value: 'pending' },
|
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
collapsible: fields.checkbox({
|
collapsible: fields.checkbox({
|
||||||
label: 'Collapsible',
|
label: 'Collapsible',
|
||||||
@@ -149,5 +148,34 @@ function getKeystaticCollections(path: string) {
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
changelog: collection({
|
||||||
|
label: 'Changelog',
|
||||||
|
slugField: 'title',
|
||||||
|
path: `${path}changelog/*`,
|
||||||
|
format: { contentField: 'content' },
|
||||||
|
schema: {
|
||||||
|
title: fields.slug({ name: { label: 'Title' } }),
|
||||||
|
description: fields.text({
|
||||||
|
label: 'Description',
|
||||||
|
multiline: true,
|
||||||
|
}),
|
||||||
|
image: imageField,
|
||||||
|
categories: fields.array(fields.text({ label: 'Category' })),
|
||||||
|
tags: fields.array(fields.text({ label: 'Tag' })),
|
||||||
|
publishedAt: fields.date({ label: 'Published At' }),
|
||||||
|
parent: fields.relationship({
|
||||||
|
label: 'Parent',
|
||||||
|
collection: 'changelog',
|
||||||
|
}),
|
||||||
|
language: fields.text({ label: 'Language' }),
|
||||||
|
order: fields.number({ label: 'Order' }),
|
||||||
|
content: getContentField(),
|
||||||
|
status: fields.select({
|
||||||
|
defaultValue: 'draft',
|
||||||
|
label: 'Status',
|
||||||
|
options: statusOptions,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,12 +38,12 @@
|
|||||||
"@tanstack/react-query": "5.90.5",
|
"@tanstack/react-query": "5.90.5",
|
||||||
"@types/react": "catalog:",
|
"@types/react": "catalog:",
|
||||||
"@types/react-dom": "19.2.2",
|
"@types/react-dom": "19.2.2",
|
||||||
"lucide-react": "^0.548.0",
|
"lucide-react": "^0.552.0",
|
||||||
"next": "catalog:",
|
"next": "catalog:",
|
||||||
"next-themes": "0.4.6",
|
"next-themes": "0.4.6",
|
||||||
"react": "19.2.0",
|
"react": "19.2.0",
|
||||||
"react-dom": "19.2.0",
|
"react-dom": "19.2.0",
|
||||||
"react-hook-form": "^7.65.0",
|
"react-hook-form": "^7.66.0",
|
||||||
"react-i18next": "^16.2.3",
|
"react-i18next": "^16.2.3",
|
||||||
"zod": "^3.25.74"
|
"zod": "^3.25.74"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -24,11 +24,11 @@
|
|||||||
"@tanstack/react-query": "5.90.5",
|
"@tanstack/react-query": "5.90.5",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"@types/react": "catalog:",
|
"@types/react": "catalog:",
|
||||||
"lucide-react": "^0.548.0",
|
"lucide-react": "^0.552.0",
|
||||||
"next": "catalog:",
|
"next": "catalog:",
|
||||||
"react": "19.2.0",
|
"react": "19.2.0",
|
||||||
"react-dom": "19.2.0",
|
"react-dom": "19.2.0",
|
||||||
"react-hook-form": "^7.65.0",
|
"react-hook-form": "^7.66.0",
|
||||||
"zod": "^3.25.74"
|
"zod": "^3.25.74"
|
||||||
},
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -32,9 +32,9 @@
|
|||||||
"@supabase/supabase-js": "2.78.0",
|
"@supabase/supabase-js": "2.78.0",
|
||||||
"@tanstack/react-query": "5.90.5",
|
"@tanstack/react-query": "5.90.5",
|
||||||
"@types/react": "catalog:",
|
"@types/react": "catalog:",
|
||||||
"lucide-react": "^0.548.0",
|
"lucide-react": "^0.552.0",
|
||||||
"next": "catalog:",
|
"next": "catalog:",
|
||||||
"react-hook-form": "^7.65.0",
|
"react-hook-form": "^7.66.0",
|
||||||
"react-i18next": "^16.2.3",
|
"react-i18next": "^16.2.3",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"zod": "^3.25.74"
|
"zod": "^3.25.74"
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
"@supabase/supabase-js": "2.78.0",
|
"@supabase/supabase-js": "2.78.0",
|
||||||
"@tanstack/react-query": "5.90.5",
|
"@tanstack/react-query": "5.90.5",
|
||||||
"@types/react": "catalog:",
|
"@types/react": "catalog:",
|
||||||
"lucide-react": "^0.548.0",
|
"lucide-react": "^0.552.0",
|
||||||
"react": "19.2.0",
|
"react": "19.2.0",
|
||||||
"react-dom": "19.2.0",
|
"react-dom": "19.2.0",
|
||||||
"react-i18next": "^16.2.3"
|
"react-i18next": "^16.2.3"
|
||||||
|
|||||||
@@ -43,11 +43,11 @@
|
|||||||
"@types/react-dom": "19.2.2",
|
"@types/react-dom": "19.2.2",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"lucide-react": "^0.548.0",
|
"lucide-react": "^0.552.0",
|
||||||
"next": "catalog:",
|
"next": "catalog:",
|
||||||
"react": "19.2.0",
|
"react": "19.2.0",
|
||||||
"react-dom": "19.2.0",
|
"react-dom": "19.2.0",
|
||||||
"react-hook-form": "^7.65.0",
|
"react-hook-form": "^7.66.0",
|
||||||
"react-i18next": "^16.2.3",
|
"react-i18next": "^16.2.3",
|
||||||
"zod": "^3.25.74"
|
"zod": "^3.25.74"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
"@types/react-dom": "19.2.2",
|
"@types/react-dom": "19.2.2",
|
||||||
"react": "19.2.0",
|
"react": "19.2.0",
|
||||||
"react-dom": "19.2.0",
|
"react-dom": "19.2.0",
|
||||||
"react-hook-form": "^7.65.0",
|
"react-hook-form": "^7.66.0",
|
||||||
"zod": "^3.25.74"
|
"zod": "^3.25.74"
|
||||||
},
|
},
|
||||||
"typesVersions": {
|
"typesVersions": {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "1.1.1",
|
"cmdk": "1.1.1",
|
||||||
"input-otp": "1.4.2",
|
"input-otp": "1.4.2",
|
||||||
"lucide-react": "^0.548.0",
|
"lucide-react": "^0.552.0",
|
||||||
"radix-ui": "1.4.3",
|
"radix-ui": "1.4.3",
|
||||||
"react-dropzone": "^14.3.8",
|
"react-dropzone": "^14.3.8",
|
||||||
"react-top-loading-bar": "3.0.2",
|
"react-top-loading-bar": "3.0.2",
|
||||||
@@ -32,12 +32,12 @@
|
|||||||
"@types/react-dom": "19.2.2",
|
"@types/react-dom": "19.2.2",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"eslint": "^9.38.0",
|
"eslint": "^9.39.0",
|
||||||
"next": "catalog:",
|
"next": "catalog:",
|
||||||
"next-themes": "0.4.6",
|
"next-themes": "0.4.6",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"react-day-picker": "^9.11.1",
|
"react-day-picker": "^9.11.1",
|
||||||
"react-hook-form": "^7.65.0",
|
"react-hook-form": "^7.66.0",
|
||||||
"react-i18next": "^16.2.3",
|
"react-i18next": "^16.2.3",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwindcss": "4.1.16",
|
"tailwindcss": "4.1.16",
|
||||||
|
|||||||
408
pnpm-lock.yaml
generated
408
pnpm-lock.yaml
generated
@@ -45,8 +45,8 @@ importers:
|
|||||||
specifier: ^0.25.1
|
specifier: ^0.25.1
|
||||||
version: 0.25.1
|
version: 0.25.1
|
||||||
'@turbo/gen':
|
'@turbo/gen':
|
||||||
specifier: ^2.5.8
|
specifier: ^2.6.0
|
||||||
version: 2.5.8(@types/node@24.9.2)(typescript@5.9.3)
|
version: 2.6.0(@types/node@24.9.2)(typescript@5.9.3)
|
||||||
cross-env:
|
cross-env:
|
||||||
specifier: ^10.0.0
|
specifier: ^10.0.0
|
||||||
version: 10.1.0
|
version: 10.1.0
|
||||||
@@ -54,8 +54,8 @@ importers:
|
|||||||
specifier: ^3.6.2
|
specifier: ^3.6.2
|
||||||
version: 3.6.2
|
version: 3.6.2
|
||||||
turbo:
|
turbo:
|
||||||
specifier: 2.5.8
|
specifier: 2.6.0
|
||||||
version: 2.5.8
|
version: 2.6.0
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.9.3
|
specifier: ^5.9.3
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
@@ -63,23 +63,23 @@ importers:
|
|||||||
apps/dev-tool:
|
apps/dev-tool:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ai-sdk/openai':
|
'@ai-sdk/openai':
|
||||||
specifier: ^2.0.58
|
specifier: ^2.0.59
|
||||||
version: 2.0.58(zod@3.25.76)
|
version: 2.0.59(zod@3.25.76)
|
||||||
'@faker-js/faker':
|
'@faker-js/faker':
|
||||||
specifier: ^10.1.0
|
specifier: ^10.1.0
|
||||||
version: 10.1.0
|
version: 10.1.0
|
||||||
'@hookform/resolvers':
|
'@hookform/resolvers':
|
||||||
specifier: ^5.2.2
|
specifier: ^5.2.2
|
||||||
version: 5.2.2(react-hook-form@7.65.0(react@19.2.0))
|
version: 5.2.2(react-hook-form@7.66.0(react@19.2.0))
|
||||||
'@tanstack/react-query':
|
'@tanstack/react-query':
|
||||||
specifier: 5.90.5
|
specifier: 5.90.5
|
||||||
version: 5.90.5(react@19.2.0)
|
version: 5.90.5(react@19.2.0)
|
||||||
ai:
|
ai:
|
||||||
specifier: 5.0.83
|
specifier: 5.0.86
|
||||||
version: 5.0.83(zod@3.25.76)
|
version: 5.0.86(zod@3.25.76)
|
||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: ^0.548.0
|
specifier: ^0.552.0
|
||||||
version: 0.548.0(react@19.2.0)
|
version: 0.552.0(react@19.2.0)
|
||||||
next:
|
next:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 16.0.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
version: 16.0.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
@@ -142,8 +142,8 @@ importers:
|
|||||||
specifier: 13.0.0
|
specifier: 13.0.0
|
||||||
version: 13.0.0
|
version: 13.0.0
|
||||||
react-hook-form:
|
react-hook-form:
|
||||||
specifier: ^7.65.0
|
specifier: ^7.66.0
|
||||||
version: 7.65.0(react@19.2.0)
|
version: 7.66.0(react@19.2.0)
|
||||||
recharts:
|
recharts:
|
||||||
specifier: 2.15.3
|
specifier: 2.15.3
|
||||||
version: 2.15.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
version: 2.15.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
@@ -188,7 +188,7 @@ importers:
|
|||||||
version: 2.5.3-cloudflare-rc1(next@16.0.1(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))
|
version: 2.5.3-cloudflare-rc1(next@16.0.1(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))
|
||||||
'@hookform/resolvers':
|
'@hookform/resolvers':
|
||||||
specifier: ^5.2.2
|
specifier: ^5.2.2
|
||||||
version: 5.2.2(react-hook-form@7.65.0(react@19.2.0))
|
version: 5.2.2(react-hook-form@7.66.0(react@19.2.0))
|
||||||
'@kit/accounts':
|
'@kit/accounts':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../packages/features/accounts
|
version: link:../../packages/features/accounts
|
||||||
@@ -271,8 +271,8 @@ importers:
|
|||||||
specifier: ^4.1.0
|
specifier: ^4.1.0
|
||||||
version: 4.1.0
|
version: 4.1.0
|
||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: ^0.548.0
|
specifier: ^0.552.0
|
||||||
version: 0.548.0(react@19.2.0)
|
version: 0.552.0(react@19.2.0)
|
||||||
next:
|
next:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 16.0.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
version: 16.0.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
@@ -289,8 +289,8 @@ importers:
|
|||||||
specifier: 19.2.0
|
specifier: 19.2.0
|
||||||
version: 19.2.0(react@19.2.0)
|
version: 19.2.0(react@19.2.0)
|
||||||
react-hook-form:
|
react-hook-form:
|
||||||
specifier: ^7.65.0
|
specifier: ^7.66.0
|
||||||
version: 7.65.0(react@19.2.0)
|
version: 7.66.0(react@19.2.0)
|
||||||
react-i18next:
|
react-i18next:
|
||||||
specifier: ^16.2.3
|
specifier: ^16.2.3
|
||||||
version: 16.2.3(i18next@25.6.0(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
version: 16.2.3(i18next@25.6.0(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||||
@@ -396,7 +396,7 @@ importers:
|
|||||||
devDependencies:
|
devDependencies:
|
||||||
'@hookform/resolvers':
|
'@hookform/resolvers':
|
||||||
specifier: ^5.2.2
|
specifier: ^5.2.2
|
||||||
version: 5.2.2(react-hook-form@7.65.0(react@19.2.0))
|
version: 5.2.2(react-hook-form@7.66.0(react@19.2.0))
|
||||||
'@kit/billing':
|
'@kit/billing':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../core
|
version: link:../core
|
||||||
@@ -434,8 +434,8 @@ importers:
|
|||||||
specifier: ^4.1.0
|
specifier: ^4.1.0
|
||||||
version: 4.1.0
|
version: 4.1.0
|
||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: ^0.548.0
|
specifier: ^0.552.0
|
||||||
version: 0.548.0(react@19.2.0)
|
version: 0.552.0(react@19.2.0)
|
||||||
next:
|
next:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 16.0.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
version: 16.0.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
@@ -443,8 +443,8 @@ importers:
|
|||||||
specifier: 19.2.0
|
specifier: 19.2.0
|
||||||
version: 19.2.0
|
version: 19.2.0
|
||||||
react-hook-form:
|
react-hook-form:
|
||||||
specifier: ^7.65.0
|
specifier: ^7.66.0
|
||||||
version: 7.65.0(react@19.2.0)
|
version: 7.66.0(react@19.2.0)
|
||||||
react-i18next:
|
react-i18next:
|
||||||
specifier: ^16.2.3
|
specifier: ^16.2.3
|
||||||
version: 16.2.3(i18next@25.6.0(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
version: 16.2.3(i18next@25.6.0(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||||
@@ -707,7 +707,7 @@ importers:
|
|||||||
devDependencies:
|
devDependencies:
|
||||||
'@hookform/resolvers':
|
'@hookform/resolvers':
|
||||||
specifier: ^5.2.2
|
specifier: ^5.2.2
|
||||||
version: 5.2.2(react-hook-form@7.65.0(react@19.2.0))
|
version: 5.2.2(react-hook-form@7.66.0(react@19.2.0))
|
||||||
'@kit/billing-gateway':
|
'@kit/billing-gateway':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../billing/gateway
|
version: link:../../billing/gateway
|
||||||
@@ -760,8 +760,8 @@ importers:
|
|||||||
specifier: 19.2.2
|
specifier: 19.2.2
|
||||||
version: 19.2.2(@types/react@19.2.2)
|
version: 19.2.2(@types/react@19.2.2)
|
||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: ^0.548.0
|
specifier: ^0.552.0
|
||||||
version: 0.548.0(react@19.2.0)
|
version: 0.552.0(react@19.2.0)
|
||||||
next:
|
next:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 16.0.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
version: 16.0.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
@@ -775,8 +775,8 @@ importers:
|
|||||||
specifier: 19.2.0
|
specifier: 19.2.0
|
||||||
version: 19.2.0(react@19.2.0)
|
version: 19.2.0(react@19.2.0)
|
||||||
react-hook-form:
|
react-hook-form:
|
||||||
specifier: ^7.65.0
|
specifier: ^7.66.0
|
||||||
version: 7.65.0(react@19.2.0)
|
version: 7.66.0(react@19.2.0)
|
||||||
react-i18next:
|
react-i18next:
|
||||||
specifier: ^16.2.3
|
specifier: ^16.2.3
|
||||||
version: 16.2.3(i18next@25.6.0(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
version: 16.2.3(i18next@25.6.0(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||||
@@ -788,7 +788,7 @@ importers:
|
|||||||
devDependencies:
|
devDependencies:
|
||||||
'@hookform/resolvers':
|
'@hookform/resolvers':
|
||||||
specifier: ^5.2.2
|
specifier: ^5.2.2
|
||||||
version: 5.2.2(react-hook-form@7.65.0(react@19.2.0))
|
version: 5.2.2(react-hook-form@7.66.0(react@19.2.0))
|
||||||
'@kit/eslint-config':
|
'@kit/eslint-config':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../../tooling/eslint
|
version: link:../../../tooling/eslint
|
||||||
@@ -829,8 +829,8 @@ importers:
|
|||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 19.2.2
|
version: 19.2.2
|
||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: ^0.548.0
|
specifier: ^0.552.0
|
||||||
version: 0.548.0(react@19.2.0)
|
version: 0.552.0(react@19.2.0)
|
||||||
next:
|
next:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 16.0.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
version: 16.0.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
@@ -841,8 +841,8 @@ importers:
|
|||||||
specifier: 19.2.0
|
specifier: 19.2.0
|
||||||
version: 19.2.0(react@19.2.0)
|
version: 19.2.0(react@19.2.0)
|
||||||
react-hook-form:
|
react-hook-form:
|
||||||
specifier: ^7.65.0
|
specifier: ^7.66.0
|
||||||
version: 7.65.0(react@19.2.0)
|
version: 7.66.0(react@19.2.0)
|
||||||
zod:
|
zod:
|
||||||
specifier: ^3.25.74
|
specifier: ^3.25.74
|
||||||
version: 3.25.76
|
version: 3.25.76
|
||||||
@@ -851,7 +851,7 @@ importers:
|
|||||||
devDependencies:
|
devDependencies:
|
||||||
'@hookform/resolvers':
|
'@hookform/resolvers':
|
||||||
specifier: ^5.2.2
|
specifier: ^5.2.2
|
||||||
version: 5.2.2(react-hook-form@7.65.0(react@19.2.0))
|
version: 5.2.2(react-hook-form@7.66.0(react@19.2.0))
|
||||||
'@kit/eslint-config':
|
'@kit/eslint-config':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../../tooling/eslint
|
version: link:../../../tooling/eslint
|
||||||
@@ -886,14 +886,14 @@ importers:
|
|||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 19.2.2
|
version: 19.2.2
|
||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: ^0.548.0
|
specifier: ^0.552.0
|
||||||
version: 0.548.0(react@19.2.0)
|
version: 0.552.0(react@19.2.0)
|
||||||
next:
|
next:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 16.0.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
version: 16.0.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
react-hook-form:
|
react-hook-form:
|
||||||
specifier: ^7.65.0
|
specifier: ^7.66.0
|
||||||
version: 7.65.0(react@19.2.0)
|
version: 7.66.0(react@19.2.0)
|
||||||
react-i18next:
|
react-i18next:
|
||||||
specifier: ^16.2.3
|
specifier: ^16.2.3
|
||||||
version: 16.2.3(i18next@25.6.0(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
version: 16.2.3(i18next@25.6.0(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||||
@@ -935,8 +935,8 @@ importers:
|
|||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 19.2.2
|
version: 19.2.2
|
||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: ^0.548.0
|
specifier: ^0.552.0
|
||||||
version: 0.548.0(react@19.2.0)
|
version: 0.552.0(react@19.2.0)
|
||||||
react:
|
react:
|
||||||
specifier: 19.2.0
|
specifier: 19.2.0
|
||||||
version: 19.2.0
|
version: 19.2.0
|
||||||
@@ -955,7 +955,7 @@ importers:
|
|||||||
devDependencies:
|
devDependencies:
|
||||||
'@hookform/resolvers':
|
'@hookform/resolvers':
|
||||||
specifier: ^5.2.2
|
specifier: ^5.2.2
|
||||||
version: 5.2.2(react-hook-form@7.65.0(react@19.2.0))
|
version: 5.2.2(react-hook-form@7.66.0(react@19.2.0))
|
||||||
'@kit/accounts':
|
'@kit/accounts':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../accounts
|
version: link:../accounts
|
||||||
@@ -1020,8 +1020,8 @@ importers:
|
|||||||
specifier: ^4.1.0
|
specifier: ^4.1.0
|
||||||
version: 4.1.0
|
version: 4.1.0
|
||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: ^0.548.0
|
specifier: ^0.552.0
|
||||||
version: 0.548.0(react@19.2.0)
|
version: 0.552.0(react@19.2.0)
|
||||||
next:
|
next:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 16.0.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
version: 16.0.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
@@ -1032,8 +1032,8 @@ importers:
|
|||||||
specifier: 19.2.0
|
specifier: 19.2.0
|
||||||
version: 19.2.0(react@19.2.0)
|
version: 19.2.0(react@19.2.0)
|
||||||
react-hook-form:
|
react-hook-form:
|
||||||
specifier: ^7.65.0
|
specifier: ^7.66.0
|
||||||
version: 7.65.0(react@19.2.0)
|
version: 7.66.0(react@19.2.0)
|
||||||
react-i18next:
|
react-i18next:
|
||||||
specifier: ^16.2.3
|
specifier: ^16.2.3
|
||||||
version: 16.2.3(i18next@25.6.0(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
version: 16.2.3(i18next@25.6.0(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||||
@@ -1300,7 +1300,7 @@ importers:
|
|||||||
devDependencies:
|
devDependencies:
|
||||||
'@hookform/resolvers':
|
'@hookform/resolvers':
|
||||||
specifier: ^5.2.2
|
specifier: ^5.2.2
|
||||||
version: 5.2.2(react-hook-form@7.65.0(react@19.2.0))
|
version: 5.2.2(react-hook-form@7.66.0(react@19.2.0))
|
||||||
'@kit/email-templates':
|
'@kit/email-templates':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../email-templates
|
version: link:../email-templates
|
||||||
@@ -1347,8 +1347,8 @@ importers:
|
|||||||
specifier: 19.2.0
|
specifier: 19.2.0
|
||||||
version: 19.2.0(react@19.2.0)
|
version: 19.2.0(react@19.2.0)
|
||||||
react-hook-form:
|
react-hook-form:
|
||||||
specifier: ^7.65.0
|
specifier: ^7.66.0
|
||||||
version: 7.65.0(react@19.2.0)
|
version: 7.66.0(react@19.2.0)
|
||||||
zod:
|
zod:
|
||||||
specifier: ^3.25.74
|
specifier: ^3.25.74
|
||||||
version: 3.25.76
|
version: 3.25.76
|
||||||
@@ -1430,7 +1430,7 @@ importers:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@hookform/resolvers':
|
'@hookform/resolvers':
|
||||||
specifier: ^5.2.2
|
specifier: ^5.2.2
|
||||||
version: 5.2.2(react-hook-form@7.65.0(react@19.2.0))
|
version: 5.2.2(react-hook-form@7.66.0(react@19.2.0))
|
||||||
'@radix-ui/react-icons':
|
'@radix-ui/react-icons':
|
||||||
specifier: ^1.3.2
|
specifier: ^1.3.2
|
||||||
version: 1.3.2(react@19.2.0)
|
version: 1.3.2(react@19.2.0)
|
||||||
@@ -1444,8 +1444,8 @@ importers:
|
|||||||
specifier: 1.4.2
|
specifier: 1.4.2
|
||||||
version: 1.4.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
version: 1.4.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: ^0.548.0
|
specifier: ^0.552.0
|
||||||
version: 0.548.0(react@19.2.0)
|
version: 0.552.0(react@19.2.0)
|
||||||
radix-ui:
|
radix-ui:
|
||||||
specifier: 1.4.3
|
specifier: 1.4.3
|
||||||
version: 1.4.3(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
version: 1.4.3(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
@@ -1493,8 +1493,8 @@ importers:
|
|||||||
specifier: ^4.1.0
|
specifier: ^4.1.0
|
||||||
version: 4.1.0
|
version: 4.1.0
|
||||||
eslint:
|
eslint:
|
||||||
specifier: ^9.38.0
|
specifier: ^9.39.0
|
||||||
version: 9.38.0(jiti@2.6.1)
|
version: 9.39.0(jiti@2.6.1)
|
||||||
next:
|
next:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 16.0.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
version: 16.0.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
@@ -1508,8 +1508,8 @@ importers:
|
|||||||
specifier: ^9.11.1
|
specifier: ^9.11.1
|
||||||
version: 9.11.1(react@19.2.0)
|
version: 9.11.1(react@19.2.0)
|
||||||
react-hook-form:
|
react-hook-form:
|
||||||
specifier: ^7.65.0
|
specifier: ^7.66.0
|
||||||
version: 7.65.0(react@19.2.0)
|
version: 7.66.0(react@19.2.0)
|
||||||
react-i18next:
|
react-i18next:
|
||||||
specifier: ^16.2.3
|
specifier: ^16.2.3
|
||||||
version: 16.2.3(i18next@25.6.0(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
version: 16.2.3(i18next@25.6.0(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||||
@@ -1539,17 +1539,17 @@ importers:
|
|||||||
version: 9.6.1
|
version: 9.6.1
|
||||||
eslint-config-next:
|
eslint-config-next:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 16.0.1(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)
|
version: 16.0.1(@typescript-eslint/parser@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3)
|
||||||
eslint-config-turbo:
|
eslint-config-turbo:
|
||||||
specifier: ^2.5.8
|
specifier: ^2.6.0
|
||||||
version: 2.5.8(eslint@9.38.0(jiti@2.6.1))(turbo@2.5.8)
|
version: 2.6.0(eslint@9.39.0(jiti@2.6.1))(turbo@2.6.0)
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@kit/prettier-config':
|
'@kit/prettier-config':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../prettier
|
version: link:../prettier
|
||||||
eslint:
|
eslint:
|
||||||
specifier: ^9.38.0
|
specifier: ^9.39.0
|
||||||
version: 9.38.0(jiti@2.6.1)
|
version: 9.39.0(jiti@2.6.1)
|
||||||
|
|
||||||
tooling/prettier:
|
tooling/prettier:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -1584,20 +1584,20 @@ packages:
|
|||||||
graphql:
|
graphql:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@ai-sdk/gateway@2.0.4':
|
'@ai-sdk/gateway@2.0.5':
|
||||||
resolution: {integrity: sha512-YakzaijPayJI244AOAXX+29WY+6i40vbvyMggfBB5D75ui8GyJPHvD/1TCSdD8lUyD4/QJvi9xSQ26QnPjmhag==}
|
resolution: {integrity: sha512-5TTDSl0USWY6YGnb4QmJGplFZhk+p9OT7hZevAaER6OGiZ17LB1GypsGYDpNo/MiVMklk8kX4gk6p1/R/EiJ8Q==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
zod: ^3.25.76 || ^4.1.8
|
zod: ^3.25.76 || ^4.1.8
|
||||||
|
|
||||||
'@ai-sdk/openai@2.0.58':
|
'@ai-sdk/openai@2.0.59':
|
||||||
resolution: {integrity: sha512-eBEpuO6DF4j+ZESs0JPtT9+xjkdnTvOqEHZ3D8JTnCk0HZ/xMR3WQGXqolzckcQS+qzMXCiefDX2gk8cTP9M7A==}
|
resolution: {integrity: sha512-ylaL91BrMyqHsprEI0+brvvGwDjRlKxsJTRL1kpzx6AhG37JqpECQwuqNk7aq+T5Qww9w7dqn1JMUuVPvyZtsA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
zod: ^3.25.76 || ^4.1.8
|
zod: ^3.25.76 || ^4.1.8
|
||||||
|
|
||||||
'@ai-sdk/provider-utils@3.0.14':
|
'@ai-sdk/provider-utils@3.0.15':
|
||||||
resolution: {integrity: sha512-CYRU6L7IlR7KslSBVxvlqlybQvXJln/PI57O8swhOaDIURZbjRP2AY3igKgUsrmWqqnFFUHP+AwTN8xqJAknnA==}
|
resolution: {integrity: sha512-kOc6Pxb7CsRlNt+sLZKL7/VGQUd7ccl3/tIK+Bqf5/QhHR0Qm3qRBMz1IwU1RmjJEZA73x+KB5cUckbDl2WF7Q==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
zod: ^3.25.76 || ^4.1.8
|
zod: ^3.25.76 || ^4.1.8
|
||||||
@@ -1911,10 +1911,6 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
|
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
|
||||||
|
|
||||||
'@eslint-community/regexpp@4.12.1':
|
|
||||||
resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==}
|
|
||||||
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
|
|
||||||
|
|
||||||
'@eslint-community/regexpp@4.12.2':
|
'@eslint-community/regexpp@4.12.2':
|
||||||
resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==}
|
resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==}
|
||||||
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
|
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
|
||||||
@@ -1923,28 +1919,28 @@ packages:
|
|||||||
resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==}
|
resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
'@eslint/config-helpers@0.4.1':
|
'@eslint/config-helpers@0.4.2':
|
||||||
resolution: {integrity: sha512-csZAzkNhsgwb0I/UAV6/RGFTbiakPCf0ZrGmrIxQpYvGZ00PhTkSnyKNolphgIvmnJeGw6rcGVEXfTzUnFuEvw==}
|
resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
'@eslint/core@0.16.0':
|
'@eslint/core@0.17.0':
|
||||||
resolution: {integrity: sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==}
|
resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
'@eslint/eslintrc@3.3.1':
|
'@eslint/eslintrc@3.3.1':
|
||||||
resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==}
|
resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
'@eslint/js@9.38.0':
|
'@eslint/js@9.39.0':
|
||||||
resolution: {integrity: sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A==}
|
resolution: {integrity: sha512-BIhe0sW91JGPiaF1mOuPy5v8NflqfjIcDNpC+LbW9f609WVRX1rArrhi6Z2ymvrAry9jw+5POTj4t2t62o8Bmw==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
'@eslint/object-schema@2.1.7':
|
'@eslint/object-schema@2.1.7':
|
||||||
resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==}
|
resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
'@eslint/plugin-kit@0.4.0':
|
'@eslint/plugin-kit@0.4.1':
|
||||||
resolution: {integrity: sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==}
|
resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
'@faker-js/faker@10.1.0':
|
'@faker-js/faker@10.1.0':
|
||||||
@@ -4585,12 +4581,12 @@ packages:
|
|||||||
'@tsconfig/node16@1.0.4':
|
'@tsconfig/node16@1.0.4':
|
||||||
resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==}
|
resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==}
|
||||||
|
|
||||||
'@turbo/gen@2.5.8':
|
'@turbo/gen@2.6.0':
|
||||||
resolution: {integrity: sha512-PknlZnl4NzARv9p2KnRIA2q9cGWzrvv2G5moWLoZRTLspoE7jL2XtejzwbclS2iXGbXQWk27BfIugv98tS2s7w==}
|
resolution: {integrity: sha512-Bwg2HnzW9LUnFwM39CkiCTLFRWcgzA6UAPfbRRCGQzZGMvA/bdAq2kx/bSbknpbn2oNd1qwtWYBgBPrVVtZkTw==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
'@turbo/workspaces@2.5.8':
|
'@turbo/workspaces@2.6.0':
|
||||||
resolution: {integrity: sha512-EE/27azLteK24It0B0IrjA7yWFC6jYZoTTUzL7R7HgiN0BWBPrTp6Ugpn0iE6+Bn9fFcjSp/IBBG8D8c7vXD1g==}
|
resolution: {integrity: sha512-Kh6KBcHgEUy+dPzePzGxhxDVY4QsxD7PKAJM3srbHESOILTh2LahU7IhwDFUvr1SzCQj2y0DSFUQ4h+Vu6fYKQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
'@tybys/wasm-util@0.10.1':
|
'@tybys/wasm-util@0.10.1':
|
||||||
@@ -4999,8 +4995,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==}
|
resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
ai@5.0.83:
|
ai@5.0.86:
|
||||||
resolution: {integrity: sha512-tMc1wqc9khzETBN0X4/MHw9RbeP5uKFdoG5sUIUJ3p7lSgfDU6yZMwT6jrnA3Nk2AvxM0T43P1h2LfPlBSwGKA==}
|
resolution: {integrity: sha512-ooHwNTkLdedFf98iQhtSc5btc/P4UuXuOpYneoifq0190vqosLunNdW8Hs6CiE0Am7YOGNplDK56JIPlHZIL4w==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
zod: ^3.25.76 || ^4.1.8
|
zod: ^3.25.76 || ^4.1.8
|
||||||
@@ -5250,8 +5246,8 @@ packages:
|
|||||||
chardet@0.7.0:
|
chardet@0.7.0:
|
||||||
resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==}
|
resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==}
|
||||||
|
|
||||||
chardet@2.1.0:
|
chardet@2.1.1:
|
||||||
resolution: {integrity: sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==}
|
resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==}
|
||||||
|
|
||||||
chokidar@3.6.0:
|
chokidar@3.6.0:
|
||||||
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
||||||
@@ -5776,8 +5772,8 @@ packages:
|
|||||||
typescript:
|
typescript:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
eslint-config-turbo@2.5.8:
|
eslint-config-turbo@2.6.0:
|
||||||
resolution: {integrity: sha512-wzxmN7dJNFGDwOvR/4j8U2iaIH/ruYez8qg/sCKrezJ3+ljbFMvJLmgKKt/1mDuyU9wj5aZqO6VijP3QH169FA==}
|
resolution: {integrity: sha512-WdNSKL1vTl5IGyovn8w3xkaEKsK2n+l9Ybk+4oISptcom/vz9sO/9QcjEKuZSXG0zxrj9Oyx/UZA04hRxmOe+Q==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
eslint: '>6.6.0'
|
eslint: '>6.6.0'
|
||||||
turbo: '>2.0.0'
|
turbo: '>2.0.0'
|
||||||
@@ -5847,8 +5843,8 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7
|
eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7
|
||||||
|
|
||||||
eslint-plugin-turbo@2.5.8:
|
eslint-plugin-turbo@2.6.0:
|
||||||
resolution: {integrity: sha512-bVjx4vTH0oTKIyQ7EGFAXnuhZMrKIfu17qlex/dps7eScPnGQLJ3r1/nFq80l8xA+8oYjsSirSQ2tXOKbz3kEw==}
|
resolution: {integrity: sha512-04TohZhq6YQVXBZVRvrn8ZTj1sUQYZmjUWsfwgFAlaM5Kbk5Fdh5mLBKfhGGzekB55E+Ut9qNzAGh+JW4rjiuA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
eslint: '>6.6.0'
|
eslint: '>6.6.0'
|
||||||
turbo: '>2.0.0'
|
turbo: '>2.0.0'
|
||||||
@@ -5869,8 +5865,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==}
|
resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
eslint@9.38.0:
|
eslint@9.39.0:
|
||||||
resolution: {integrity: sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==}
|
resolution: {integrity: sha512-iy2GE3MHrYTL5lrCtMZ0X1KLEKKUjmK0kzwcnefhR66txcEmXZD2YWgR5GNdcEwkNx3a0siYkSvl0vIC+Svjmg==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -6800,8 +6796,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==}
|
resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
lucide-react@0.548.0:
|
lucide-react@0.552.0:
|
||||||
resolution: {integrity: sha512-63b16z63jM9yc1MwxajHeuu0FRZFsDtljtDjYm26Kd86UQ5HQzu9ksEtoUUw4RBuewodw/tGFmvipePvRsKeDA==}
|
resolution: {integrity: sha512-g9WCjmfwqbexSnZE+2cl21PCfXOcqnGeWeMTNAOGEfpPbm/ZF4YIq77Z8qWrxbu660EKuLB4nSLggoKnCb+isw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
|
||||||
@@ -7766,8 +7762,8 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: '>= 16.8 || 18.0.0'
|
react: '>= 16.8 || 18.0.0'
|
||||||
|
|
||||||
react-hook-form@7.65.0:
|
react-hook-form@7.66.0:
|
||||||
resolution: {integrity: sha512-xtOzDz063WcXvGWaHgLNrNzlsdFgtUWcb32E6WFaGTd7kPZG3EeDusjdZfUsPwKCKVXy1ZlntifaHZ4l8pAsmw==}
|
resolution: {integrity: sha512-xXBqsWGKrY46ZqaHDo+ZUYiMUgi8suYu5kdrS20EG8KiL7VRQitEbNjm+UcrDYrNi1YLyfpmAeGjCZYXLT9YBw==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^16.8.0 || ^17 || ^18 || ^19
|
react: ^16.8.0 || ^17 || ^18 || ^19
|
||||||
@@ -8432,38 +8428,38 @@ packages:
|
|||||||
tslib@2.8.1:
|
tslib@2.8.1:
|
||||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||||
|
|
||||||
turbo-darwin-64@2.5.8:
|
turbo-darwin-64@2.6.0:
|
||||||
resolution: {integrity: sha512-Dh5bCACiHO8rUXZLpKw+m3FiHtAp2CkanSyJre+SInEvEr5kIxjGvCK/8MFX8SFRjQuhjtvpIvYYZJB4AGCxNQ==}
|
resolution: {integrity: sha512-6vHnLAubHj8Ib45Knu+oY0ZVCLO7WcibzAvt5b1E72YHqAs4y8meMAGMZM0jLqWPh/9maHDc16/qBCMxtW4pXg==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
turbo-darwin-arm64@2.5.8:
|
turbo-darwin-arm64@2.6.0:
|
||||||
resolution: {integrity: sha512-f1H/tQC9px7+hmXn6Kx/w8Jd/FneIUnvLlcI/7RGHunxfOkKJKvsoiNzySkoHQ8uq1pJnhJ0xNGTlYM48ZaJOQ==}
|
resolution: {integrity: sha512-IU+gWMEXNBw8H0pxvE7nPEa5p6yahxbN8g/Q4Bf0AHymsAFqsScgV0peeNbWybdmY9jk1LPbALOsF2kY1I7ZiQ==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
turbo-linux-64@2.5.8:
|
turbo-linux-64@2.6.0:
|
||||||
resolution: {integrity: sha512-hMyvc7w7yadBlZBGl/bnR6O+dJTx3XkTeyTTH4zEjERO6ChEs0SrN8jTFj1lueNXKIHh1SnALmy6VctKMGnWfw==}
|
resolution: {integrity: sha512-CKoiJ2ZFJLCDsWdRlZg+ew1BkGn8iCEGdePhISVpjsGwkJwSVhVu49z2zKdBeL1IhcSKS2YALwp9ellNZANJxw==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
turbo-linux-arm64@2.5.8:
|
turbo-linux-arm64@2.6.0:
|
||||||
resolution: {integrity: sha512-LQELGa7bAqV2f+3rTMRPnj5G/OHAe2U+0N9BwsZvfMvHSUbsQ3bBMWdSQaYNicok7wOZcHjz2TkESn1hYK6xIQ==}
|
resolution: {integrity: sha512-WroVCdCvJbrhNxNdw7XB7wHAfPPJPV+IXY+ZKNed+9VdfBu/2mQNfKnvqTuFTH7n+Pdpv8to9qwhXRTJe26upg==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
turbo-windows-64@2.5.8:
|
turbo-windows-64@2.6.0:
|
||||||
resolution: {integrity: sha512-3YdcaW34TrN1AWwqgYL9gUqmZsMT4T7g8Y5Azz+uwwEJW+4sgcJkIi9pYFyU4ZBSjBvkfuPZkGgfStir5BBDJQ==}
|
resolution: {integrity: sha512-7pZo5aGQPR+A7RMtWCZHusarJ6y15LQ+o3jOmpMxTic/W6Bad+jSeqo07TWNIseIWjCVzrSv27+0odiYRYtQdA==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
turbo-windows-arm64@2.5.8:
|
turbo-windows-arm64@2.6.0:
|
||||||
resolution: {integrity: sha512-eFC5XzLmgXJfnAK3UMTmVECCwuBcORrWdewoiXBnUm934DY6QN8YowC/srhNnROMpaKaqNeRpoB5FxCww3eteQ==}
|
resolution: {integrity: sha512-1Ty+NwIksQY7AtFUCPrTpcKQE7zmd/f7aRjdT+qkqGFQjIjFYctEtN7qo4vpQPBgCfS1U3ka83A2u/9CfJQ3wQ==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
turbo@2.5.8:
|
turbo@2.6.0:
|
||||||
resolution: {integrity: sha512-5c9Fdsr9qfpT3hA0EyYSFRZj1dVVsb6KIWubA9JBYZ/9ZEAijgUEae0BBR/Xl/wekt4w65/lYLTFaP3JmwSO8w==}
|
resolution: {integrity: sha512-kC5VJqOXo50k0/0jnJDDjibLAXalqT9j7PQ56so0pN+81VR4Fwb2QgIE9dTzT3phqOTQuEXkPh3sCpnv5Isz2g==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
type-check@0.4.0:
|
type-check@0.4.0:
|
||||||
@@ -8821,20 +8817,20 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
graphql: 16.11.0
|
graphql: 16.11.0
|
||||||
|
|
||||||
'@ai-sdk/gateway@2.0.4(zod@3.25.76)':
|
'@ai-sdk/gateway@2.0.5(zod@3.25.76)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ai-sdk/provider': 2.0.0
|
'@ai-sdk/provider': 2.0.0
|
||||||
'@ai-sdk/provider-utils': 3.0.14(zod@3.25.76)
|
'@ai-sdk/provider-utils': 3.0.15(zod@3.25.76)
|
||||||
'@vercel/oidc': 3.0.3
|
'@vercel/oidc': 3.0.3
|
||||||
zod: 3.25.76
|
zod: 3.25.76
|
||||||
|
|
||||||
'@ai-sdk/openai@2.0.58(zod@3.25.76)':
|
'@ai-sdk/openai@2.0.59(zod@3.25.76)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ai-sdk/provider': 2.0.0
|
'@ai-sdk/provider': 2.0.0
|
||||||
'@ai-sdk/provider-utils': 3.0.14(zod@3.25.76)
|
'@ai-sdk/provider-utils': 3.0.15(zod@3.25.76)
|
||||||
zod: 3.25.76
|
zod: 3.25.76
|
||||||
|
|
||||||
'@ai-sdk/provider-utils@3.0.14(zod@3.25.76)':
|
'@ai-sdk/provider-utils@3.0.15(zod@3.25.76)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ai-sdk/provider': 2.0.0
|
'@ai-sdk/provider': 2.0.0
|
||||||
'@standard-schema/spec': 1.0.0
|
'@standard-schema/spec': 1.0.0
|
||||||
@@ -9468,13 +9464,11 @@ snapshots:
|
|||||||
|
|
||||||
'@epic-web/invariant@1.0.0': {}
|
'@epic-web/invariant@1.0.0': {}
|
||||||
|
|
||||||
'@eslint-community/eslint-utils@4.9.0(eslint@9.38.0(jiti@2.6.1))':
|
'@eslint-community/eslint-utils@4.9.0(eslint@9.39.0(jiti@2.6.1))':
|
||||||
dependencies:
|
dependencies:
|
||||||
eslint: 9.38.0(jiti@2.6.1)
|
eslint: 9.39.0(jiti@2.6.1)
|
||||||
eslint-visitor-keys: 3.4.3
|
eslint-visitor-keys: 3.4.3
|
||||||
|
|
||||||
'@eslint-community/regexpp@4.12.1': {}
|
|
||||||
|
|
||||||
'@eslint-community/regexpp@4.12.2': {}
|
'@eslint-community/regexpp@4.12.2': {}
|
||||||
|
|
||||||
'@eslint/config-array@0.21.1':
|
'@eslint/config-array@0.21.1':
|
||||||
@@ -9485,11 +9479,11 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@eslint/config-helpers@0.4.1':
|
'@eslint/config-helpers@0.4.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint/core': 0.16.0
|
'@eslint/core': 0.17.0
|
||||||
|
|
||||||
'@eslint/core@0.16.0':
|
'@eslint/core@0.17.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/json-schema': 7.0.15
|
'@types/json-schema': 7.0.15
|
||||||
|
|
||||||
@@ -9507,13 +9501,13 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@eslint/js@9.38.0': {}
|
'@eslint/js@9.39.0': {}
|
||||||
|
|
||||||
'@eslint/object-schema@2.1.7': {}
|
'@eslint/object-schema@2.1.7': {}
|
||||||
|
|
||||||
'@eslint/plugin-kit@0.4.0':
|
'@eslint/plugin-kit@0.4.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint/core': 0.16.0
|
'@eslint/core': 0.17.0
|
||||||
levn: 0.4.1
|
levn: 0.4.1
|
||||||
|
|
||||||
'@faker-js/faker@10.1.0': {}
|
'@faker-js/faker@10.1.0': {}
|
||||||
@@ -9573,10 +9567,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
graphql: 16.11.0
|
graphql: 16.11.0
|
||||||
|
|
||||||
'@hookform/resolvers@5.2.2(react-hook-form@7.65.0(react@19.2.0))':
|
'@hookform/resolvers@5.2.2(react-hook-form@7.66.0(react@19.2.0))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@standard-schema/utils': 0.3.0
|
'@standard-schema/utils': 0.3.0
|
||||||
react-hook-form: 7.65.0(react@19.2.0)
|
react-hook-form: 7.66.0(react@19.2.0)
|
||||||
|
|
||||||
'@humanfs/core@0.19.1': {}
|
'@humanfs/core@0.19.1': {}
|
||||||
|
|
||||||
@@ -9680,7 +9674,7 @@ snapshots:
|
|||||||
|
|
||||||
'@inquirer/external-editor@1.0.2(@types/node@24.9.2)':
|
'@inquirer/external-editor@1.0.2(@types/node@24.9.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
chardet: 2.1.0
|
chardet: 2.1.1
|
||||||
iconv-lite: 0.7.0
|
iconv-lite: 0.7.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 24.9.2
|
'@types/node': 24.9.2
|
||||||
@@ -12911,9 +12905,9 @@ snapshots:
|
|||||||
|
|
||||||
'@tsconfig/node16@1.0.4': {}
|
'@tsconfig/node16@1.0.4': {}
|
||||||
|
|
||||||
'@turbo/gen@2.5.8(@types/node@24.9.2)(typescript@5.9.3)':
|
'@turbo/gen@2.6.0(@types/node@24.9.2)(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@turbo/workspaces': 2.5.8(@types/node@24.9.2)
|
'@turbo/workspaces': 2.6.0(@types/node@24.9.2)
|
||||||
commander: 10.0.1
|
commander: 10.0.1
|
||||||
fs-extra: 10.1.0
|
fs-extra: 10.1.0
|
||||||
inquirer: 8.2.7(@types/node@24.9.2)
|
inquirer: 8.2.7(@types/node@24.9.2)
|
||||||
@@ -12931,7 +12925,7 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- typescript
|
- typescript
|
||||||
|
|
||||||
'@turbo/workspaces@2.5.8(@types/node@24.9.2)':
|
'@turbo/workspaces@2.6.0(@types/node@24.9.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
commander: 10.0.1
|
commander: 10.0.1
|
||||||
execa: 5.1.1
|
execa: 5.1.1
|
||||||
@@ -13101,15 +13095,15 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 24.9.2
|
'@types/node': 24.9.2
|
||||||
|
|
||||||
'@typescript-eslint/eslint-plugin@8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)':
|
'@typescript-eslint/eslint-plugin@8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/regexpp': 4.12.2
|
'@eslint-community/regexpp': 4.12.2
|
||||||
'@typescript-eslint/parser': 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)
|
'@typescript-eslint/parser': 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3)
|
||||||
'@typescript-eslint/scope-manager': 8.46.2
|
'@typescript-eslint/scope-manager': 8.46.2
|
||||||
'@typescript-eslint/type-utils': 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)
|
'@typescript-eslint/type-utils': 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3)
|
||||||
'@typescript-eslint/utils': 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)
|
'@typescript-eslint/utils': 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3)
|
||||||
'@typescript-eslint/visitor-keys': 8.46.2
|
'@typescript-eslint/visitor-keys': 8.46.2
|
||||||
eslint: 9.38.0(jiti@2.6.1)
|
eslint: 9.39.0(jiti@2.6.1)
|
||||||
graphemer: 1.4.0
|
graphemer: 1.4.0
|
||||||
ignore: 7.0.5
|
ignore: 7.0.5
|
||||||
natural-compare: 1.4.0
|
natural-compare: 1.4.0
|
||||||
@@ -13118,14 +13112,14 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)':
|
'@typescript-eslint/parser@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/scope-manager': 8.46.2
|
'@typescript-eslint/scope-manager': 8.46.2
|
||||||
'@typescript-eslint/types': 8.46.2
|
'@typescript-eslint/types': 8.46.2
|
||||||
'@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3)
|
'@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3)
|
||||||
'@typescript-eslint/visitor-keys': 8.46.2
|
'@typescript-eslint/visitor-keys': 8.46.2
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
eslint: 9.38.0(jiti@2.6.1)
|
eslint: 9.39.0(jiti@2.6.1)
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -13148,13 +13142,13 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
|
|
||||||
'@typescript-eslint/type-utils@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)':
|
'@typescript-eslint/type-utils@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/types': 8.46.2
|
'@typescript-eslint/types': 8.46.2
|
||||||
'@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3)
|
'@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3)
|
||||||
'@typescript-eslint/utils': 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)
|
'@typescript-eslint/utils': 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3)
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
eslint: 9.38.0(jiti@2.6.1)
|
eslint: 9.39.0(jiti@2.6.1)
|
||||||
ts-api-utils: 2.1.0(typescript@5.9.3)
|
ts-api-utils: 2.1.0(typescript@5.9.3)
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -13178,13 +13172,13 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@typescript-eslint/utils@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)':
|
'@typescript-eslint/utils@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/eslint-utils': 4.9.0(eslint@9.38.0(jiti@2.6.1))
|
'@eslint-community/eslint-utils': 4.9.0(eslint@9.39.0(jiti@2.6.1))
|
||||||
'@typescript-eslint/scope-manager': 8.46.2
|
'@typescript-eslint/scope-manager': 8.46.2
|
||||||
'@typescript-eslint/types': 8.46.2
|
'@typescript-eslint/types': 8.46.2
|
||||||
'@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3)
|
'@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3)
|
||||||
eslint: 9.38.0(jiti@2.6.1)
|
eslint: 9.39.0(jiti@2.6.1)
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -13396,11 +13390,11 @@ snapshots:
|
|||||||
clean-stack: 2.2.0
|
clean-stack: 2.2.0
|
||||||
indent-string: 4.0.0
|
indent-string: 4.0.0
|
||||||
|
|
||||||
ai@5.0.83(zod@3.25.76):
|
ai@5.0.86(zod@3.25.76):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ai-sdk/gateway': 2.0.4(zod@3.25.76)
|
'@ai-sdk/gateway': 2.0.5(zod@3.25.76)
|
||||||
'@ai-sdk/provider': 2.0.0
|
'@ai-sdk/provider': 2.0.0
|
||||||
'@ai-sdk/provider-utils': 3.0.14(zod@3.25.76)
|
'@ai-sdk/provider-utils': 3.0.15(zod@3.25.76)
|
||||||
'@opentelemetry/api': 1.9.0
|
'@opentelemetry/api': 1.9.0
|
||||||
zod: 3.25.76
|
zod: 3.25.76
|
||||||
|
|
||||||
@@ -13709,7 +13703,7 @@ snapshots:
|
|||||||
|
|
||||||
chardet@0.7.0: {}
|
chardet@0.7.0: {}
|
||||||
|
|
||||||
chardet@2.1.0: {}
|
chardet@2.1.1: {}
|
||||||
|
|
||||||
chokidar@3.6.0:
|
chokidar@3.6.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -14267,18 +14261,18 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
source-map: 0.6.1
|
source-map: 0.6.1
|
||||||
|
|
||||||
eslint-config-next@16.0.1(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3):
|
eslint-config-next@16.0.1(@typescript-eslint/parser@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@next/eslint-plugin-next': 16.0.1
|
'@next/eslint-plugin-next': 16.0.1
|
||||||
eslint: 9.38.0(jiti@2.6.1)
|
eslint: 9.39.0(jiti@2.6.1)
|
||||||
eslint-import-resolver-node: 0.3.9
|
eslint-import-resolver-node: 0.3.9
|
||||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.38.0(jiti@2.6.1)))(eslint@9.38.0(jiti@2.6.1))
|
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.0(jiti@2.6.1)))(eslint@9.39.0(jiti@2.6.1))
|
||||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.38.0(jiti@2.6.1)))(eslint@9.38.0(jiti@2.6.1)))(eslint@9.38.0(jiti@2.6.1))
|
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.0(jiti@2.6.1)))(eslint@9.39.0(jiti@2.6.1)))(eslint@9.39.0(jiti@2.6.1))
|
||||||
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.38.0(jiti@2.6.1))
|
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.0(jiti@2.6.1))
|
||||||
eslint-plugin-react: 7.37.5(eslint@9.38.0(jiti@2.6.1))
|
eslint-plugin-react: 7.37.5(eslint@9.39.0(jiti@2.6.1))
|
||||||
eslint-plugin-react-hooks: 7.0.1(eslint@9.38.0(jiti@2.6.1))
|
eslint-plugin-react-hooks: 7.0.1(eslint@9.39.0(jiti@2.6.1))
|
||||||
globals: 16.4.0
|
globals: 16.4.0
|
||||||
typescript-eslint: 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)
|
typescript-eslint: 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -14287,11 +14281,11 @@ snapshots:
|
|||||||
- eslint-plugin-import-x
|
- eslint-plugin-import-x
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
eslint-config-turbo@2.5.8(eslint@9.38.0(jiti@2.6.1))(turbo@2.5.8):
|
eslint-config-turbo@2.6.0(eslint@9.39.0(jiti@2.6.1))(turbo@2.6.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
eslint: 9.38.0(jiti@2.6.1)
|
eslint: 9.39.0(jiti@2.6.1)
|
||||||
eslint-plugin-turbo: 2.5.8(eslint@9.38.0(jiti@2.6.1))(turbo@2.5.8)
|
eslint-plugin-turbo: 2.6.0(eslint@9.39.0(jiti@2.6.1))(turbo@2.6.0)
|
||||||
turbo: 2.5.8
|
turbo: 2.6.0
|
||||||
|
|
||||||
eslint-import-resolver-node@0.3.9:
|
eslint-import-resolver-node@0.3.9:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -14301,33 +14295,33 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.38.0(jiti@2.6.1)))(eslint@9.38.0(jiti@2.6.1)):
|
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.0(jiti@2.6.1)))(eslint@9.39.0(jiti@2.6.1)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nolyfill/is-core-module': 1.0.39
|
'@nolyfill/is-core-module': 1.0.39
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
eslint: 9.38.0(jiti@2.6.1)
|
eslint: 9.39.0(jiti@2.6.1)
|
||||||
get-tsconfig: 4.13.0
|
get-tsconfig: 4.13.0
|
||||||
is-bun-module: 2.0.0
|
is-bun-module: 2.0.0
|
||||||
stable-hash: 0.0.5
|
stable-hash: 0.0.5
|
||||||
tinyglobby: 0.2.15
|
tinyglobby: 0.2.15
|
||||||
unrs-resolver: 1.11.1
|
unrs-resolver: 1.11.1
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.38.0(jiti@2.6.1)))(eslint@9.38.0(jiti@2.6.1)))(eslint@9.38.0(jiti@2.6.1))
|
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.0(jiti@2.6.1)))(eslint@9.39.0(jiti@2.6.1)))(eslint@9.39.0(jiti@2.6.1))
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.38.0(jiti@2.6.1)))(eslint@9.38.0(jiti@2.6.1)))(eslint@9.38.0(jiti@2.6.1)):
|
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.0(jiti@2.6.1)))(eslint@9.39.0(jiti@2.6.1)))(eslint@9.39.0(jiti@2.6.1)):
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 3.2.7
|
debug: 3.2.7
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@typescript-eslint/parser': 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)
|
'@typescript-eslint/parser': 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3)
|
||||||
eslint: 9.38.0(jiti@2.6.1)
|
eslint: 9.39.0(jiti@2.6.1)
|
||||||
eslint-import-resolver-node: 0.3.9
|
eslint-import-resolver-node: 0.3.9
|
||||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.38.0(jiti@2.6.1)))(eslint@9.38.0(jiti@2.6.1))
|
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.0(jiti@2.6.1)))(eslint@9.39.0(jiti@2.6.1))
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.38.0(jiti@2.6.1)))(eslint@9.38.0(jiti@2.6.1)))(eslint@9.38.0(jiti@2.6.1)):
|
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.0(jiti@2.6.1)))(eslint@9.39.0(jiti@2.6.1)))(eslint@9.39.0(jiti@2.6.1)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@rtsao/scc': 1.1.0
|
'@rtsao/scc': 1.1.0
|
||||||
array-includes: 3.1.9
|
array-includes: 3.1.9
|
||||||
@@ -14336,9 +14330,9 @@ snapshots:
|
|||||||
array.prototype.flatmap: 1.3.3
|
array.prototype.flatmap: 1.3.3
|
||||||
debug: 3.2.7
|
debug: 3.2.7
|
||||||
doctrine: 2.1.0
|
doctrine: 2.1.0
|
||||||
eslint: 9.38.0(jiti@2.6.1)
|
eslint: 9.39.0(jiti@2.6.1)
|
||||||
eslint-import-resolver-node: 0.3.9
|
eslint-import-resolver-node: 0.3.9
|
||||||
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.38.0(jiti@2.6.1)))(eslint@9.38.0(jiti@2.6.1)))(eslint@9.38.0(jiti@2.6.1))
|
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.0(jiti@2.6.1)))(eslint@9.39.0(jiti@2.6.1)))(eslint@9.39.0(jiti@2.6.1))
|
||||||
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
|
||||||
@@ -14350,13 +14344,13 @@ 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.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)
|
'@typescript-eslint/parser': 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- eslint-import-resolver-typescript
|
- eslint-import-resolver-typescript
|
||||||
- eslint-import-resolver-webpack
|
- eslint-import-resolver-webpack
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
eslint-plugin-jsx-a11y@6.10.2(eslint@9.38.0(jiti@2.6.1)):
|
eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.0(jiti@2.6.1)):
|
||||||
dependencies:
|
dependencies:
|
||||||
aria-query: 5.3.2
|
aria-query: 5.3.2
|
||||||
array-includes: 3.1.9
|
array-includes: 3.1.9
|
||||||
@@ -14366,7 +14360,7 @@ snapshots:
|
|||||||
axobject-query: 4.1.0
|
axobject-query: 4.1.0
|
||||||
damerau-levenshtein: 1.0.8
|
damerau-levenshtein: 1.0.8
|
||||||
emoji-regex: 9.2.2
|
emoji-regex: 9.2.2
|
||||||
eslint: 9.38.0(jiti@2.6.1)
|
eslint: 9.39.0(jiti@2.6.1)
|
||||||
hasown: 2.0.2
|
hasown: 2.0.2
|
||||||
jsx-ast-utils: 3.3.5
|
jsx-ast-utils: 3.3.5
|
||||||
language-tags: 1.0.9
|
language-tags: 1.0.9
|
||||||
@@ -14375,18 +14369,18 @@ snapshots:
|
|||||||
safe-regex-test: 1.1.0
|
safe-regex-test: 1.1.0
|
||||||
string.prototype.includes: 2.0.1
|
string.prototype.includes: 2.0.1
|
||||||
|
|
||||||
eslint-plugin-react-hooks@7.0.1(eslint@9.38.0(jiti@2.6.1)):
|
eslint-plugin-react-hooks@7.0.1(eslint@9.39.0(jiti@2.6.1)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.28.5
|
'@babel/core': 7.28.5
|
||||||
'@babel/parser': 7.28.5
|
'@babel/parser': 7.28.5
|
||||||
eslint: 9.38.0(jiti@2.6.1)
|
eslint: 9.39.0(jiti@2.6.1)
|
||||||
hermes-parser: 0.25.1
|
hermes-parser: 0.25.1
|
||||||
zod: 3.25.76
|
zod: 3.25.76
|
||||||
zod-validation-error: 4.0.2(zod@3.25.76)
|
zod-validation-error: 4.0.2(zod@3.25.76)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
eslint-plugin-react@7.37.5(eslint@9.38.0(jiti@2.6.1)):
|
eslint-plugin-react@7.37.5(eslint@9.39.0(jiti@2.6.1)):
|
||||||
dependencies:
|
dependencies:
|
||||||
array-includes: 3.1.9
|
array-includes: 3.1.9
|
||||||
array.prototype.findlast: 1.2.5
|
array.prototype.findlast: 1.2.5
|
||||||
@@ -14394,7 +14388,7 @@ snapshots:
|
|||||||
array.prototype.tosorted: 1.1.4
|
array.prototype.tosorted: 1.1.4
|
||||||
doctrine: 2.1.0
|
doctrine: 2.1.0
|
||||||
es-iterator-helpers: 1.2.1
|
es-iterator-helpers: 1.2.1
|
||||||
eslint: 9.38.0(jiti@2.6.1)
|
eslint: 9.39.0(jiti@2.6.1)
|
||||||
estraverse: 5.3.0
|
estraverse: 5.3.0
|
||||||
hasown: 2.0.2
|
hasown: 2.0.2
|
||||||
jsx-ast-utils: 3.3.5
|
jsx-ast-utils: 3.3.5
|
||||||
@@ -14408,11 +14402,11 @@ snapshots:
|
|||||||
string.prototype.matchall: 4.0.12
|
string.prototype.matchall: 4.0.12
|
||||||
string.prototype.repeat: 1.0.0
|
string.prototype.repeat: 1.0.0
|
||||||
|
|
||||||
eslint-plugin-turbo@2.5.8(eslint@9.38.0(jiti@2.6.1))(turbo@2.5.8):
|
eslint-plugin-turbo@2.6.0(eslint@9.39.0(jiti@2.6.1))(turbo@2.6.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
dotenv: 16.0.3
|
dotenv: 16.0.3
|
||||||
eslint: 9.38.0(jiti@2.6.1)
|
eslint: 9.39.0(jiti@2.6.1)
|
||||||
turbo: 2.5.8
|
turbo: 2.6.0
|
||||||
|
|
||||||
eslint-scope@5.1.1:
|
eslint-scope@5.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -14428,16 +14422,16 @@ snapshots:
|
|||||||
|
|
||||||
eslint-visitor-keys@4.2.1: {}
|
eslint-visitor-keys@4.2.1: {}
|
||||||
|
|
||||||
eslint@9.38.0(jiti@2.6.1):
|
eslint@9.39.0(jiti@2.6.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/eslint-utils': 4.9.0(eslint@9.38.0(jiti@2.6.1))
|
'@eslint-community/eslint-utils': 4.9.0(eslint@9.39.0(jiti@2.6.1))
|
||||||
'@eslint-community/regexpp': 4.12.1
|
'@eslint-community/regexpp': 4.12.2
|
||||||
'@eslint/config-array': 0.21.1
|
'@eslint/config-array': 0.21.1
|
||||||
'@eslint/config-helpers': 0.4.1
|
'@eslint/config-helpers': 0.4.2
|
||||||
'@eslint/core': 0.16.0
|
'@eslint/core': 0.17.0
|
||||||
'@eslint/eslintrc': 3.3.1
|
'@eslint/eslintrc': 3.3.1
|
||||||
'@eslint/js': 9.38.0
|
'@eslint/js': 9.39.0
|
||||||
'@eslint/plugin-kit': 0.4.0
|
'@eslint/plugin-kit': 0.4.1
|
||||||
'@humanfs/node': 0.16.7
|
'@humanfs/node': 0.16.7
|
||||||
'@humanwhocodes/module-importer': 1.0.1
|
'@humanwhocodes/module-importer': 1.0.1
|
||||||
'@humanwhocodes/retry': 0.4.3
|
'@humanwhocodes/retry': 0.4.3
|
||||||
@@ -15398,7 +15392,7 @@ snapshots:
|
|||||||
|
|
||||||
lru-cache@7.18.3: {}
|
lru-cache@7.18.3: {}
|
||||||
|
|
||||||
lucide-react@0.548.0(react@19.2.0):
|
lucide-react@0.552.0(react@19.2.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.2.0
|
react: 19.2.0
|
||||||
|
|
||||||
@@ -16608,7 +16602,7 @@ snapshots:
|
|||||||
prop-types: 15.8.1
|
prop-types: 15.8.1
|
||||||
react: 19.2.0
|
react: 19.2.0
|
||||||
|
|
||||||
react-hook-form@7.65.0(react@19.2.0):
|
react-hook-form@7.66.0(react@19.2.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.2.0
|
react: 19.2.0
|
||||||
|
|
||||||
@@ -17388,32 +17382,32 @@ snapshots:
|
|||||||
|
|
||||||
tslib@2.8.1: {}
|
tslib@2.8.1: {}
|
||||||
|
|
||||||
turbo-darwin-64@2.5.8:
|
turbo-darwin-64@2.6.0:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
turbo-darwin-arm64@2.5.8:
|
turbo-darwin-arm64@2.6.0:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
turbo-linux-64@2.5.8:
|
turbo-linux-64@2.6.0:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
turbo-linux-arm64@2.5.8:
|
turbo-linux-arm64@2.6.0:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
turbo-windows-64@2.5.8:
|
turbo-windows-64@2.6.0:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
turbo-windows-arm64@2.5.8:
|
turbo-windows-arm64@2.6.0:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
turbo@2.5.8:
|
turbo@2.6.0:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
turbo-darwin-64: 2.5.8
|
turbo-darwin-64: 2.6.0
|
||||||
turbo-darwin-arm64: 2.5.8
|
turbo-darwin-arm64: 2.6.0
|
||||||
turbo-linux-64: 2.5.8
|
turbo-linux-64: 2.6.0
|
||||||
turbo-linux-arm64: 2.5.8
|
turbo-linux-arm64: 2.6.0
|
||||||
turbo-windows-64: 2.5.8
|
turbo-windows-64: 2.6.0
|
||||||
turbo-windows-arm64: 2.5.8
|
turbo-windows-arm64: 2.6.0
|
||||||
|
|
||||||
type-check@0.4.0:
|
type-check@0.4.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -17462,13 +17456,13 @@ snapshots:
|
|||||||
possible-typed-array-names: 1.1.0
|
possible-typed-array-names: 1.1.0
|
||||||
reflect.getprototypeof: 1.0.10
|
reflect.getprototypeof: 1.0.10
|
||||||
|
|
||||||
typescript-eslint@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3):
|
typescript-eslint@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/eslint-plugin': 8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)
|
'@typescript-eslint/eslint-plugin': 8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3)
|
||||||
'@typescript-eslint/parser': 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)
|
'@typescript-eslint/parser': 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3)
|
||||||
'@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3)
|
'@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3)
|
||||||
'@typescript-eslint/utils': 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)
|
'@typescript-eslint/utils': 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3)
|
||||||
eslint: 9.38.0(jiti@2.6.1)
|
eslint: 9.39.0(jiti@2.6.1)
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|||||||
@@ -16,11 +16,11 @@
|
|||||||
"@next/eslint-plugin-next": "catalog:",
|
"@next/eslint-plugin-next": "catalog:",
|
||||||
"@types/eslint": "9.6.1",
|
"@types/eslint": "9.6.1",
|
||||||
"eslint-config-next": "catalog:",
|
"eslint-config-next": "catalog:",
|
||||||
"eslint-config-turbo": "^2.5.8"
|
"eslint-config-turbo": "^2.6.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@kit/prettier-config": "workspace:*",
|
"@kit/prettier-config": "workspace:*",
|
||||||
"eslint": "^9.38.0"
|
"eslint": "^9.39.0"
|
||||||
},
|
},
|
||||||
"prettier": "@kit/prettier-config"
|
"prettier": "@kit/prettier-config"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user