Next.js 15 Update (#26)

* Update Next.js and React versions in all packages
* Replace onRedirect function with next/link in BillingSessionStatus, since it's no longer cached by default
* Remove unused revalidatePath import in billing return page, since it's no longer cached by default
* Add Turbopack module aliases to improve development server speed
* Converted new Dynamic APIs to be Promise-based
* Adjust mobile layout
* Use ENABLE_REACT_COMPILER to enable the React Compiler in Next.js 15
* Report Errors using the new onRequestError hook
This commit is contained in:
Giancarlo Buomprisco
2024-10-22 08:39:21 +02:00
committed by GitHub
parent 93cb011260
commit 5b9285a575
109 changed files with 5143 additions and 5545 deletions

View File

@@ -12,8 +12,8 @@
"author": "",
"license": "ISC",
"devDependencies": {
"@playwright/test": "^1.47.2",
"@types/node": "^22.5.5",
"@playwright/test": "^1.48.1",
"@types/node": "^22.7.8",
"node-html-parser": "^6.1.13"
}
}

View File

@@ -1,5 +1,3 @@
import { PageBody } from '@kit/ui/page';
import { SitePageHeader } from '~/(marketing)/_components/site-page-header';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';

View File

@@ -18,7 +18,10 @@ import featuresFlagConfig from '~/config/feature-flags.config';
import pathsConfig from '~/config/paths.config';
const ModeToggle = dynamic(
() => import('@kit/ui/mode-toggle').then((mod) => mod.ModeToggle),
() =>
import('@kit/ui/mode-toggle').then((mod) => ({
default: mod.ModeToggle,
})),
{
ssr: false,
},

View File

@@ -10,10 +10,10 @@ const getClassName = (path: string, currentPathName: string) => {
const isActive = isRouteActive(path, currentPathName);
return cn(
`text-sm font-medium transition-colors duration-300 inline-flex w-max`,
`inline-flex w-max text-sm font-medium transition-colors duration-300`,
{
'dark:text-gray-300 dark:hover:text-white': !isActive,
'dark:text-white text-current': isActive,
'text-current dark:text-white': isActive,
},
);
};

View File

@@ -10,6 +10,10 @@ import { withI18n } from '~/lib/i18n/with-i18n';
import { Post } from '../../blog/_components/post';
interface BlogPageProps {
params: Promise<{ slug: string }>;
}
const getPostBySlug = cache(postLoader);
async function postLoader(slug: string) {
@@ -20,10 +24,9 @@ async function postLoader(slug: string) {
export async function generateMetadata({
params,
}: {
params: { slug: string };
}): Promise<Metadata | undefined> {
const post = await getPostBySlug(params.slug);
}: BlogPageProps): Promise<Metadata> {
const slug = (await params).slug;
const post = await getPostBySlug(slug);
if (!post) {
notFound();
@@ -57,8 +60,9 @@ export async function generateMetadata({
});
}
async function BlogPost({ params }: { params: { slug: string } }) {
const post = await getPostBySlug(params.slug);
async function BlogPost({ params }: BlogPageProps) {
const slug = (await params).slug;
const post = await getPostBySlug(slug);
if (!post) {
notFound();

View File

@@ -13,6 +13,10 @@ import { SitePageHeader } from '../_components/site-page-header';
import { BlogPagination } from './_components/blog-pagination';
import { PostPreview } from './_components/post-preview';
interface BlogPageProps {
searchParams: Promise<{ page?: string }>;
}
export const generateMetadata = async () => {
const { t } = await createI18nServerInstance();
@@ -44,8 +48,9 @@ const getContentItems = cache(
},
);
async function BlogPage({ searchParams }: { searchParams: { page: string } }) {
async function BlogPage(props: BlogPageProps) {
const { t, resolvedLanguage: language } = await createI18nServerInstance();
const searchParams = await props.searchParams;
const page = searchParams.page ? parseInt(searchParams.page) : 0;
const limit = 10;

View File

@@ -57,7 +57,7 @@ export function ContactForm() {
await sendContactEmail(data);
setState({ success: true, error: false });
} catch (error) {
} catch {
setState({ error: true, success: false });
}
});

View File

@@ -14,20 +14,19 @@ import { DocsCards } from '../_components/docs-cards';
const getPageBySlug = cache(pageLoader);
interface DocumentationPageProps {
params: Promise<{ slug: string[] }>;
}
async function pageLoader(slug: string) {
const client = await createCmsClient();
return client.getContentItemBySlug({ slug, collection: 'documentation' });
}
interface PageParams {
params: {
slug: string[];
};
}
export const generateMetadata = async ({ params }: PageParams) => {
const page = await getPageBySlug(params.slug.join('/'));
export const generateMetadata = async ({ params }: DocumentationPageProps) => {
const slug = (await params).slug.join('/');
const page = await getPageBySlug(slug);
if (!page) {
notFound();
@@ -41,8 +40,9 @@ export const generateMetadata = async ({ params }: PageParams) => {
};
};
async function DocumentationPage({ params }: PageParams) {
const page = await getPageBySlug(params.slug.join('/'));
async function DocumentationPage({ params }: DocumentationPageProps) {
const slug = (await params).slug.join('/');
const page = await getPageBySlug(slug);
if (!page) {
notFound();

View File

@@ -175,7 +175,6 @@ function Home() {
);
}
export default withI18n(Home);
function MainCallToActionButton() {

View File

@@ -1,5 +1,3 @@
import { User } from '@supabase/supabase-js';
import { Home, Users } from 'lucide-react';
import {
@@ -12,7 +10,7 @@ import {
import { AppLogo } from '~/components/app-logo';
import { ProfileAccountDropdownContainer } from '~/components/personal-account-dropdown-container';
export function AdminSidebar(props: { user: User }) {
export function AdminSidebar() {
return (
<Sidebar>
<SidebarContent className={'py-4'}>
@@ -35,7 +33,7 @@ export function AdminSidebar(props: { user: User }) {
</SidebarContent>
<SidebarContent className={'absolute bottom-4'}>
<ProfileAccountDropdownContainer user={props.user} />
<ProfileAccountDropdownContainer />
</SidebarContent>
</Sidebar>
);

View File

@@ -6,12 +6,13 @@ import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client'
import { PageBody } from '@kit/ui/page';
interface Params {
params: {
params: Promise<{
id: string;
};
}>;
}
export const generateMetadata = async ({ params }: Params) => {
export const generateMetadata = async (props: Params) => {
const params = await props.params;
const account = await loadAccount(params.id);
return {
@@ -19,7 +20,8 @@ export const generateMetadata = async ({ params }: Params) => {
};
};
async function AccountPage({ params }: Params) {
async function AccountPage(props: Params) {
const params = await props.params;
const account = await loadAccount(params.id);
return (

View File

@@ -11,12 +11,17 @@ interface SearchParams {
query?: string;
}
interface AdminAccountsPageProps {
searchParams: Promise<SearchParams>;
}
export const metadata = {
title: `Accounts`,
};
function AccountsPage({ searchParams }: { searchParams: SearchParams }) {
async function AccountsPage(props: AdminAccountsPageProps) {
const client = getSupabaseServerAdminClient();
const searchParams = await props.searchParams;
const page = searchParams.page ? parseInt(searchParams.page) : 1;
const filters = getFilters(searchParams);

View File

@@ -2,19 +2,16 @@ import { Page, PageMobileNavigation, PageNavigation } from '@kit/ui/page';
import { AdminSidebar } from '~/admin/_components/admin-sidebar';
import { AdminMobileNavigation } from '~/admin/_components/mobile-navigation';
import { requireUserInServerComponent } from '~/lib/server/require-user-in-server-component';
export const metadata = {
title: `Super Admin`,
};
export default async function AdminLayout(props: React.PropsWithChildren) {
const user = await requireUserInServerComponent();
export default function AdminLayout(props: React.PropsWithChildren) {
return (
<Page style={'sidebar'}>
<PageNavigation>
<AdminSidebar user={user} />
<AdminSidebar />
</PageNavigation>
<PageMobileNavigation>

View File

@@ -15,7 +15,7 @@ export const POST = enhanceRouteHandler(
// return a successful response
return new Response(null, { status: 200 });
} catch (error) {
} catch {
// return an error response
return new Response(null, { status: 500 });
}

View File

@@ -8,15 +8,15 @@ import { Trans } from '@kit/ui/trans';
import pathsConfig from '~/config/paths.config';
import { withI18n } from '~/lib/i18n/with-i18n';
interface Params {
searchParams: {
interface AuthCallbackErrorPageProps {
searchParams: Promise<{
error: string;
invite_token: string;
};
}>;
}
function AuthCallbackErrorPage({ searchParams }: Params) {
const { error, invite_token } = searchParams;
async function AuthCallbackErrorPage(props: AuthCallbackErrorPageProps) {
const { error, invite_token } = await props.searchParams;
const queryParam = invite_token ? `?invite_token=${invite_token}` : '';
const signInPath = pathsConfig.auth.signIn + queryParam;

View File

@@ -11,9 +11,9 @@ import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
interface SignInPageProps {
searchParams: {
searchParams: Promise<{
invite_token?: string;
};
}>;
}
export const generateMetadata = async () => {
@@ -30,8 +30,8 @@ const paths = {
joinTeam: pathsConfig.app.joinTeam,
};
function SignInPage({ searchParams }: SignInPageProps) {
const inviteToken = searchParams.invite_token;
async function SignInPage({ searchParams }: SignInPageProps) {
const inviteToken = (await searchParams).invite_token;
const signUpPath =
pathsConfig.auth.signUp +

View File

@@ -19,9 +19,9 @@ export const generateMetadata = async () => {
};
interface Props {
searchParams: {
searchParams: Promise<{
invite_token?: string;
};
}>;
}
const paths = {
@@ -29,8 +29,8 @@ const paths = {
appHome: pathsConfig.app.home,
};
function SignUpPage({ searchParams }: Props) {
const inviteToken = searchParams.invite_token;
async function SignUpPage({ searchParams }: Props) {
const inviteToken = (await searchParams).invite_token;
const signInPath =
pathsConfig.auth.signIn +

View File

@@ -9,9 +9,9 @@ import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
interface Props {
searchParams: {
searchParams: Promise<{
next?: string;
};
}>;
}
export const generateMetadata = async () => {
@@ -39,7 +39,8 @@ async function VerifyPage(props: Props) {
redirect(pathsConfig.auth.signIn);
}
const redirectPath = props.searchParams.next ?? pathsConfig.app.home;
const nextPath = (await props.searchParams).next;
const redirectPath = nextPath ?? pathsConfig.app.home;
return (
<MultiFactorChallengeContainer

View File

@@ -22,6 +22,7 @@ export function HomeAccountSelector(props: {
}>;
userId: string;
collisionPadding?: number;
}) {
const router = useRouter();
const { collapsed } = useContext(SidebarContext);
@@ -29,6 +30,7 @@ export function HomeAccountSelector(props: {
return (
<AccountSelector
collapsed={collapsed}
collisionPadding={props.collisionPadding ?? 20}
accounts={props.accounts}
features={features}
userId={props.userId}

View File

@@ -50,18 +50,12 @@ export function HomeMenuNavigation(props: { workspace: UserWorkspace }) {
<div className={'flex justify-end space-x-2.5'}>
<If condition={featuresFlagConfig.enableTeamAccounts}>
<HomeAccountSelector
userId={user.id}
accounts={accounts}
/>
<HomeAccountSelector userId={user.id} accounts={accounts} />
</If>
<UserNotifications userId={user.id} />
<ProfileAccountDropdownContainer
user={user}
account={workspace}
/>
<ProfileAccountDropdownContainer user={user} account={workspace} />
</div>
</div>
);

View File

@@ -71,6 +71,7 @@ export function HomeMobileNavigation(props: { workspace: UserWorkspace }) {
<HomeAccountSelector
userId={props.workspace.user.id}
accounts={props.workspace.accounts}
collisionPadding={0}
/>
</DropdownMenuGroup>

View File

@@ -99,7 +99,7 @@ export function PersonalAccountCheckoutForm(props: {
});
setCheckoutToken(checkoutToken);
} catch (e) {
} catch {
setError(true);
}
});

View File

@@ -23,7 +23,7 @@ import { loadUserWorkspace } from './_lib/server/load-user-workspace';
function UserHomeLayout({ children }: React.PropsWithChildren) {
const workspace = use(loadUserWorkspace());
const style = getLayoutStyle();
const style = use(getLayoutStyle());
return (
<Page style={style}>
@@ -52,9 +52,11 @@ function UserHomeLayout({ children }: React.PropsWithChildren) {
export default withI18n(UserHomeLayout);
function getLayoutStyle() {
async function getLayoutStyle() {
const cookieStore = await cookies();
return (
(cookies().get('layout-style')?.value as PageLayoutStyle) ??
(cookieStore.get('layout-style')?.value as PageLayoutStyle) ??
personalAccountNavigationConfig.style
);
}
}

View File

@@ -6,17 +6,20 @@ import { PageBody } from '@kit/ui/page';
import authConfig from '~/config/auth.config';
import featureFlagsConfig from '~/config/feature-flags.config';
import pathsConfig from '~/config/paths.config';
import { loadUserWorkspace } from '~/home/(user)/_lib/server/load-user-workspace';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
import { requireUserInServerComponent } from '~/lib/server/require-user-in-server-component';
const features = {
enableAccountDeletion: featureFlagsConfig.enableAccountDeletion,
enablePasswordUpdate: authConfig.providers.password,
};
const callbackPath = pathsConfig.auth.callback;
const accountHomePath = pathsConfig.app.accountHome;
const paths = {
callback: pathsConfig.auth.callback + `?next=${pathsConfig.app.accountHome}`,
callback: callbackPath + `?next=${accountHomePath}`,
};
export const generateMetadata = async () => {
@@ -29,7 +32,7 @@ export const generateMetadata = async () => {
};
function PersonalAccountSettingsPage() {
const { user } = use(loadUserWorkspace());
const user = use(requireUserInServerComponent());
return (
<PageBody>

View File

@@ -0,0 +1,896 @@
'use client';
import { useMemo, useState } from 'react';
import { ArrowDown, ArrowUp, Menu, TrendingUp } from 'lucide-react';
import {
Area,
AreaChart,
Bar,
BarChart,
CartesianGrid,
Line,
LineChart,
XAxis,
} from 'recharts';
import { Badge } from '@kit/ui/badge';
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@kit/ui/card';
import {
ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from '@kit/ui/chart';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@kit/ui/table';
export default function DashboardDemo() {
const mrr = useMemo(() => generateDemoData(), []);
const netRevenue = useMemo(() => generateDemoData(), []);
const fees = useMemo(() => generateDemoData(), []);
const newCustomers = useMemo(() => generateDemoData(), []);
return (
<div
className={
'flex flex-col space-y-4 pb-36 duration-500 animate-in fade-in'
}
>
<div
className={
'grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4'
}
>
<Card>
<CardHeader>
<CardTitle className={'flex items-center gap-2.5'}>
<span>MRR</span>
<Trend trend={'up'}>20%</Trend>
</CardTitle>
<CardDescription>
<span>Monthly recurring revenue</span>
</CardDescription>
<div>
<Figure>{`$${mrr[1]}`}</Figure>
</div>
</CardHeader>
<CardContent className={'space-y-4'}>
<Chart data={mrr[0]} />
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className={'flex items-center gap-2.5'}>
<span>Revenue</span>
<Trend trend={'up'}>12%</Trend>
</CardTitle>
<CardDescription>
<span>Total revenue including fees</span>
</CardDescription>
<div>
<Figure>{`$${netRevenue[1]}`}</Figure>
</div>
</CardHeader>
<CardContent>
<Chart data={netRevenue[0]} />
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className={'flex items-center gap-2.5'}>
<span>Fees</span>
<Trend trend={'up'}>9%</Trend>
</CardTitle>
<CardDescription>
<span>Total fees collected</span>
</CardDescription>
<div>
<Figure>{`$${fees[1]}`}</Figure>
</div>
</CardHeader>
<CardContent>
<Chart data={fees[0]} />
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className={'flex items-center gap-2.5'}>
<span>New Customers</span>
<Trend trend={'down'}>-25%</Trend>
</CardTitle>
<CardDescription>
<span>Customers who signed up this month</span>
</CardDescription>
<div>
<Figure>{`${Number(newCustomers[1]).toFixed(0)}`}</Figure>
</div>
</CardHeader>
<CardContent>
<Chart data={newCustomers[0]} />
</CardContent>
</Card>
</div>
<VisitorsChart />
<PageViewsChart />
<div>
<Card>
<CardHeader>
<CardTitle>Best Customers</CardTitle>
<CardDescription>Showing the top customers by MRR</CardDescription>
</CardHeader>
<CardContent>
<CustomersTable />
</CardContent>
</Card>
</div>
</div>
);
}
function generateDemoData() {
const today = new Date();
const formatter = new Intl.DateTimeFormat('en-us', {
month: 'long',
year: '2-digit',
});
const data: { value: string; name: string }[] = [];
for (let n = 8; n > 0; n -= 1) {
const date = new Date(today.getFullYear(), today.getMonth() - n, 1);
data.push({
name: formatter.format(date),
value: (Math.random() * 10).toFixed(1),
});
}
const lastValue = data[data.length - 1]?.value;
return [data, lastValue] as [typeof data, string];
}
function Chart(
props: React.PropsWithChildren<{ data: { value: string; name: string }[] }>,
) {
const chartConfig = {
desktop: {
label: 'Desktop',
color: 'hsl(var(--chart-1))',
},
mobile: {
label: 'Mobile',
color: 'hsl(var(--chart-2))',
},
} satisfies ChartConfig;
return (
<ChartContainer config={chartConfig}>
<LineChart accessibilityLayer data={props.data}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="name"
tickLine={false}
axisLine={false}
tickMargin={8}
/>
<ChartTooltip
cursor={false}
content={<ChartTooltipContent hideLabel />}
/>
<Line
dataKey="value"
type="natural"
stroke="var(--color-desktop)"
strokeWidth={2}
dot={false}
/>
</LineChart>
</ChartContainer>
);
}
function CustomersTable() {
const customers = [
{
name: 'John Doe',
email: 'john@makerkit.dev',
plan: 'Pro',
mrr: '$120.5',
logins: 1020,
status: 'Healthy',
trend: 'up',
},
{
name: 'Emma Smith',
email: 'emma@makerit.dev',
plan: 'Basic',
mrr: '$65.4',
logins: 570,
status: 'Possible Churn',
trend: 'stale',
},
{
name: 'Robert Johnson',
email: 'robert@makerkit.dev',
plan: 'Pro',
mrr: '$500.1',
logins: 2050,
status: 'Healthy',
trend: 'up',
},
{
name: 'Olivia Brown',
email: 'olivia@makerkit.dev',
plan: 'Basic',
mrr: '$10',
logins: 50,
status: 'Churn',
trend: 'down',
},
{
name: 'Michael Davis',
email: 'michael@makerkit.dev',
plan: 'Pro',
mrr: '$300.2',
logins: 1520,
status: 'Healthy',
trend: 'up',
},
{
name: 'Emily Jones',
email: 'emily@makerkit.dev',
plan: 'Pro',
mrr: '$75.7',
logins: 780,
status: 'Healthy',
trend: 'up',
},
{
name: 'Daniel Garcia',
email: 'daniel@makerkit.dev',
plan: 'Basic',
mrr: '$50',
logins: 320,
status: 'Possible Churn',
trend: 'stale',
},
{
name: 'Liam Miller',
email: 'liam@makerkit.dev',
plan: 'Pro',
mrr: '$90.8',
logins: 1260,
status: 'Healthy',
trend: 'up',
},
{
name: 'Emma Clark',
email: 'emma@makerkit.dev',
plan: 'Basic',
mrr: '$0',
logins: 20,
status: 'Churn',
trend: 'down',
},
{
name: 'Elizabeth Rodriguez',
email: 'liz@makerkit.dev',
plan: 'Pro',
mrr: '$145.3',
logins: 1380,
status: 'Healthy',
trend: 'up',
},
{
name: 'James Martinez',
email: 'james@makerkit.dev',
plan: 'Pro',
mrr: '$120.5',
logins: 940,
status: 'Healthy',
trend: 'up',
},
{
name: 'Charlotte Ryan',
email: 'carlotte@makerkit.dev',
plan: 'Basic',
mrr: '$80.6',
logins: 460,
status: 'Possible Churn',
trend: 'stale',
},
{
name: 'Lucas Evans',
email: 'lucas@makerkit.dev',
plan: 'Pro',
mrr: '$210.3',
logins: 1850,
status: 'Healthy',
trend: 'up',
},
{
name: 'Sophia Wilson',
email: 'sophia@makerkit.dev',
plan: 'Basic',
mrr: '$10',
logins: 35,
status: 'Churn',
trend: 'down',
},
{
name: 'William Kelly',
email: 'will@makerkit.dev',
plan: 'Pro',
mrr: '$350.2',
logins: 1760,
status: 'Healthy',
trend: 'up',
},
{
name: 'Oliver Thomas',
email: 'olly@makerkit.dev',
plan: 'Pro',
mrr: '$145.6',
logins: 1350,
status: 'Healthy',
trend: 'up',
},
{
name: 'Samantha White',
email: 'sam@makerkit.dev',
plan: 'Basic',
mrr: '$60.3',
logins: 425,
status: 'Possible Churn',
trend: 'stale',
},
{
name: 'Benjamin Lewis',
email: 'ben@makerkit.dev',
plan: 'Pro',
mrr: '$175.8',
logins: 1600,
status: 'Healthy',
trend: 'up',
},
{
name: 'Zoe Harris',
email: 'zoe@makerkit.dev',
plan: 'Basic',
mrr: '$0',
logins: 18,
status: 'Churn',
trend: 'down',
},
{
name: 'Zachary Nelson',
email: 'zac@makerkit.dev',
plan: 'Pro',
mrr: '$255.9',
logins: 1785,
status: 'Healthy',
trend: 'up',
},
];
return (
<Table>
<TableHeader>
<TableRow>
<TableHead>Customer</TableHead>
<TableHead>Plan</TableHead>
<TableHead>MRR</TableHead>
<TableHead>Logins</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{customers.map((customer) => (
<TableRow key={customer.name}>
<TableCell className={'flex flex-col'}>
<span>{customer.name}</span>
<span className={'text-sm text-muted-foreground'}>
{customer.email}
</span>
</TableCell>
<TableCell>{customer.plan}</TableCell>
<TableCell>{customer.mrr}</TableCell>
<TableCell>{customer.logins}</TableCell>
<TableCell>
<BadgeWithTrend trend={customer.trend}>
{customer.status}
</BadgeWithTrend>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
}
function BadgeWithTrend(props: React.PropsWithChildren<{ trend: string }>) {
const className = useMemo(() => {
switch (props.trend) {
case 'up':
return 'text-green-500';
case 'down':
return 'text-destructive';
case 'stale':
return 'text-orange-500';
}
}, [props.trend]);
return (
<Badge
variant={'outline'}
className={'border-transparent px-1.5 font-normal'}
>
<span className={className}>{props.children}</span>
</Badge>
);
}
function Figure(props: React.PropsWithChildren) {
return (
<div className={'font-heading text-2xl font-semibold'}>
{props.children}
</div>
);
}
function Trend(
props: React.PropsWithChildren<{
trend: 'up' | 'down' | 'stale';
}>,
) {
const Icon = useMemo(() => {
switch (props.trend) {
case 'up':
return <ArrowUp className={'h-3 w-3 text-green-500'} />;
case 'down':
return <ArrowDown className={'h-3 w-3 text-destructive'} />;
case 'stale':
return <Menu className={'h-3 w-3 text-orange-500'} />;
}
}, [props.trend]);
return (
<div>
<BadgeWithTrend trend={props.trend}>
<span className={'flex items-center space-x-1'}>
{Icon}
<span>{props.children}</span>
</span>
</BadgeWithTrend>
</div>
);
}
export function VisitorsChart() {
const chartData = useMemo(
() => [
{ date: '2024-04-01', desktop: 222, mobile: 150 },
{ date: '2024-04-02', desktop: 97, mobile: 180 },
{ date: '2024-04-03', desktop: 167, mobile: 120 },
{ date: '2024-04-04', desktop: 242, mobile: 260 },
{ date: '2024-04-05', desktop: 373, mobile: 290 },
{ date: '2024-04-06', desktop: 301, mobile: 340 },
{ date: '2024-04-07', desktop: 245, mobile: 180 },
{ date: '2024-04-08', desktop: 409, mobile: 320 },
{ date: '2024-04-09', desktop: 59, mobile: 110 },
{ date: '2024-04-10', desktop: 261, mobile: 190 },
{ date: '2024-04-11', desktop: 327, mobile: 350 },
{ date: '2024-04-12', desktop: 292, mobile: 210 },
{ date: '2024-04-13', desktop: 342, mobile: 380 },
{ date: '2024-04-14', desktop: 137, mobile: 220 },
{ date: '2024-04-15', desktop: 120, mobile: 170 },
{ date: '2024-04-16', desktop: 138, mobile: 190 },
{ date: '2024-04-17', desktop: 446, mobile: 360 },
{ date: '2024-04-18', desktop: 364, mobile: 410 },
{ date: '2024-04-19', desktop: 243, mobile: 180 },
{ date: '2024-04-20', desktop: 89, mobile: 150 },
{ date: '2024-04-21', desktop: 137, mobile: 200 },
{ date: '2024-04-22', desktop: 224, mobile: 170 },
{ date: '2024-04-23', desktop: 138, mobile: 230 },
{ date: '2024-04-24', desktop: 387, mobile: 290 },
{ date: '2024-04-25', desktop: 215, mobile: 250 },
{ date: '2024-04-26', desktop: 75, mobile: 130 },
{ date: '2024-04-27', desktop: 383, mobile: 420 },
{ date: '2024-04-28', desktop: 122, mobile: 180 },
{ date: '2024-04-29', desktop: 315, mobile: 240 },
{ date: '2024-04-30', desktop: 454, mobile: 380 },
{ date: '2024-05-01', desktop: 165, mobile: 220 },
{ date: '2024-05-02', desktop: 293, mobile: 310 },
{ date: '2024-05-03', desktop: 247, mobile: 190 },
{ date: '2024-05-04', desktop: 385, mobile: 420 },
{ date: '2024-05-05', desktop: 481, mobile: 390 },
{ date: '2024-05-06', desktop: 498, mobile: 520 },
{ date: '2024-05-07', desktop: 388, mobile: 300 },
{ date: '2024-05-08', desktop: 149, mobile: 210 },
{ date: '2024-05-09', desktop: 227, mobile: 180 },
{ date: '2024-05-10', desktop: 293, mobile: 330 },
{ date: '2024-05-11', desktop: 335, mobile: 270 },
{ date: '2024-05-12', desktop: 197, mobile: 240 },
{ date: '2024-05-13', desktop: 197, mobile: 160 },
{ date: '2024-05-14', desktop: 448, mobile: 490 },
{ date: '2024-05-15', desktop: 473, mobile: 380 },
{ date: '2024-05-16', desktop: 338, mobile: 400 },
{ date: '2024-05-17', desktop: 499, mobile: 420 },
{ date: '2024-05-18', desktop: 315, mobile: 350 },
{ date: '2024-05-19', desktop: 235, mobile: 180 },
{ date: '2024-05-20', desktop: 177, mobile: 230 },
{ date: '2024-05-21', desktop: 82, mobile: 140 },
{ date: '2024-05-22', desktop: 81, mobile: 120 },
{ date: '2024-05-23', desktop: 252, mobile: 290 },
{ date: '2024-05-24', desktop: 294, mobile: 220 },
{ date: '2024-05-25', desktop: 201, mobile: 250 },
{ date: '2024-05-26', desktop: 213, mobile: 170 },
{ date: '2024-05-27', desktop: 420, mobile: 460 },
{ date: '2024-05-28', desktop: 233, mobile: 190 },
{ date: '2024-05-29', desktop: 78, mobile: 130 },
{ date: '2024-05-30', desktop: 340, mobile: 280 },
{ date: '2024-05-31', desktop: 178, mobile: 230 },
{ date: '2024-06-01', desktop: 178, mobile: 200 },
{ date: '2024-06-02', desktop: 470, mobile: 410 },
{ date: '2024-06-03', desktop: 103, mobile: 160 },
{ date: '2024-06-04', desktop: 439, mobile: 380 },
{ date: '2024-06-05', desktop: 88, mobile: 140 },
{ date: '2024-06-06', desktop: 294, mobile: 250 },
{ date: '2024-06-07', desktop: 323, mobile: 370 },
{ date: '2024-06-08', desktop: 385, mobile: 320 },
{ date: '2024-06-09', desktop: 438, mobile: 480 },
{ date: '2024-06-10', desktop: 155, mobile: 200 },
{ date: '2024-06-11', desktop: 92, mobile: 150 },
{ date: '2024-06-12', desktop: 492, mobile: 420 },
{ date: '2024-06-13', desktop: 81, mobile: 130 },
{ date: '2024-06-14', desktop: 426, mobile: 380 },
{ date: '2024-06-15', desktop: 307, mobile: 350 },
{ date: '2024-06-16', desktop: 371, mobile: 310 },
{ date: '2024-06-17', desktop: 475, mobile: 520 },
{ date: '2024-06-18', desktop: 107, mobile: 170 },
{ date: '2024-06-19', desktop: 341, mobile: 290 },
{ date: '2024-06-20', desktop: 408, mobile: 450 },
{ date: '2024-06-21', desktop: 169, mobile: 210 },
{ date: '2024-06-22', desktop: 317, mobile: 270 },
{ date: '2024-06-23', desktop: 480, mobile: 530 },
{ date: '2024-06-24', desktop: 132, mobile: 180 },
{ date: '2024-06-25', desktop: 141, mobile: 190 },
{ date: '2024-06-26', desktop: 434, mobile: 380 },
{ date: '2024-06-27', desktop: 448, mobile: 490 },
{ date: '2024-06-28', desktop: 149, mobile: 200 },
{ date: '2024-06-29', desktop: 103, mobile: 160 },
{ date: '2024-06-30', desktop: 446, mobile: 400 },
],
[],
);
const chartConfig = {
visitors: {
label: 'Visitors',
},
desktop: {
label: 'Desktop',
color: 'hsl(var(--chart-1))',
},
mobile: {
label: 'Mobile',
color: 'hsl(var(--chart-2))',
},
} satisfies ChartConfig;
return (
<Card>
<CardHeader>
<CardTitle>Visitors</CardTitle>
<CardDescription>
Showing total visitors for the last 6 months
</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer className={'h-64 w-full'} config={chartConfig}>
<AreaChart accessibilityLayer data={chartData}>
<defs>
<linearGradient id="fillDesktop" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="var(--color-desktop)"
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor="var(--color-desktop)"
stopOpacity={0.1}
/>
</linearGradient>
<linearGradient id="fillMobile" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="var(--color-mobile)"
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor="var(--color-mobile)"
stopOpacity={0.1}
/>
</linearGradient>
</defs>
<CartesianGrid vertical={false} />
<XAxis
dataKey="month"
tickLine={false}
axisLine={false}
tickMargin={8}
tickFormatter={(value: string) => value.slice(0, 3)}
/>
<ChartTooltip
cursor={false}
content={<ChartTooltipContent indicator="dot" />}
/>
<Area
dataKey="mobile"
type="natural"
fill="url(#fillMobile)"
fillOpacity={0.4}
stroke="var(--color-mobile)"
stackId="a"
/>
<Area
dataKey="desktop"
type="natural"
fill="url(#fillDesktop)"
fillOpacity={0.4}
stroke="var(--color-desktop)"
stackId="a"
/>
</AreaChart>
</ChartContainer>
</CardContent>
<CardFooter>
<div className="flex w-full items-start gap-2 text-sm">
<div className="grid gap-2">
<div className="flex items-center gap-2 font-medium leading-none">
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
</div>
<div className="flex items-center gap-2 leading-none text-muted-foreground">
January - June 2024
</div>
</div>
</div>
</CardFooter>
</Card>
);
}
export function PageViewsChart() {
const [activeChart, setActiveChart] =
useState<keyof typeof chartConfig>('desktop');
const chartData = [
{ date: '2024-04-01', desktop: 222, mobile: 150 },
{ date: '2024-04-02', desktop: 97, mobile: 180 },
{ date: '2024-04-03', desktop: 167, mobile: 120 },
{ date: '2024-04-04', desktop: 242, mobile: 260 },
{ date: '2024-04-05', desktop: 373, mobile: 290 },
{ date: '2024-04-06', desktop: 301, mobile: 340 },
{ date: '2024-04-07', desktop: 245, mobile: 180 },
{ date: '2024-04-08', desktop: 409, mobile: 320 },
{ date: '2024-04-09', desktop: 59, mobile: 110 },
{ date: '2024-04-10', desktop: 261, mobile: 190 },
{ date: '2024-04-11', desktop: 327, mobile: 350 },
{ date: '2024-04-12', desktop: 292, mobile: 210 },
{ date: '2024-04-13', desktop: 342, mobile: 380 },
{ date: '2024-04-14', desktop: 137, mobile: 220 },
{ date: '2024-04-15', desktop: 120, mobile: 170 },
{ date: '2024-04-16', desktop: 138, mobile: 190 },
{ date: '2024-04-17', desktop: 446, mobile: 360 },
{ date: '2024-04-18', desktop: 364, mobile: 410 },
{ date: '2024-04-19', desktop: 243, mobile: 180 },
{ date: '2024-04-20', desktop: 89, mobile: 150 },
{ date: '2024-04-21', desktop: 137, mobile: 200 },
{ date: '2024-04-22', desktop: 224, mobile: 170 },
{ date: '2024-04-23', desktop: 138, mobile: 230 },
{ date: '2024-04-24', desktop: 387, mobile: 290 },
{ date: '2024-04-25', desktop: 215, mobile: 250 },
{ date: '2024-04-26', desktop: 75, mobile: 130 },
{ date: '2024-04-27', desktop: 383, mobile: 420 },
{ date: '2024-04-28', desktop: 122, mobile: 180 },
{ date: '2024-04-29', desktop: 315, mobile: 240 },
{ date: '2024-04-30', desktop: 454, mobile: 380 },
{ date: '2024-05-01', desktop: 165, mobile: 220 },
{ date: '2024-05-02', desktop: 293, mobile: 310 },
{ date: '2024-05-03', desktop: 247, mobile: 190 },
{ date: '2024-05-04', desktop: 385, mobile: 420 },
{ date: '2024-05-05', desktop: 481, mobile: 390 },
{ date: '2024-05-06', desktop: 498, mobile: 520 },
{ date: '2024-05-07', desktop: 388, mobile: 300 },
{ date: '2024-05-08', desktop: 149, mobile: 210 },
{ date: '2024-05-09', desktop: 227, mobile: 180 },
{ date: '2024-05-10', desktop: 293, mobile: 330 },
{ date: '2024-05-11', desktop: 335, mobile: 270 },
{ date: '2024-05-12', desktop: 197, mobile: 240 },
{ date: '2024-05-13', desktop: 197, mobile: 160 },
{ date: '2024-05-14', desktop: 448, mobile: 490 },
{ date: '2024-05-15', desktop: 473, mobile: 380 },
{ date: '2024-05-16', desktop: 338, mobile: 400 },
{ date: '2024-05-17', desktop: 499, mobile: 420 },
{ date: '2024-05-18', desktop: 315, mobile: 350 },
{ date: '2024-05-19', desktop: 235, mobile: 180 },
{ date: '2024-05-20', desktop: 177, mobile: 230 },
{ date: '2024-05-21', desktop: 82, mobile: 140 },
{ date: '2024-05-22', desktop: 81, mobile: 120 },
{ date: '2024-05-23', desktop: 252, mobile: 290 },
{ date: '2024-05-24', desktop: 294, mobile: 220 },
{ date: '2024-05-25', desktop: 201, mobile: 250 },
{ date: '2024-05-26', desktop: 213, mobile: 170 },
{ date: '2024-05-27', desktop: 420, mobile: 460 },
{ date: '2024-05-28', desktop: 233, mobile: 190 },
{ date: '2024-05-29', desktop: 78, mobile: 130 },
{ date: '2024-05-30', desktop: 340, mobile: 280 },
{ date: '2024-05-31', desktop: 178, mobile: 230 },
{ date: '2024-06-01', desktop: 178, mobile: 200 },
{ date: '2024-06-02', desktop: 470, mobile: 410 },
{ date: '2024-06-03', desktop: 103, mobile: 160 },
{ date: '2024-06-04', desktop: 439, mobile: 380 },
{ date: '2024-06-05', desktop: 88, mobile: 140 },
{ date: '2024-06-06', desktop: 294, mobile: 250 },
{ date: '2024-06-07', desktop: 323, mobile: 370 },
{ date: '2024-06-08', desktop: 385, mobile: 320 },
{ date: '2024-06-09', desktop: 438, mobile: 480 },
{ date: '2024-06-10', desktop: 155, mobile: 200 },
{ date: '2024-06-11', desktop: 92, mobile: 150 },
{ date: '2024-06-12', desktop: 492, mobile: 420 },
{ date: '2024-06-13', desktop: 81, mobile: 130 },
{ date: '2024-06-14', desktop: 426, mobile: 380 },
{ date: '2024-06-15', desktop: 307, mobile: 350 },
{ date: '2024-06-16', desktop: 371, mobile: 310 },
{ date: '2024-06-17', desktop: 475, mobile: 520 },
{ date: '2024-06-18', desktop: 107, mobile: 170 },
{ date: '2024-06-19', desktop: 341, mobile: 290 },
{ date: '2024-06-20', desktop: 408, mobile: 450 },
{ date: '2024-06-21', desktop: 169, mobile: 210 },
{ date: '2024-06-22', desktop: 317, mobile: 270 },
{ date: '2024-06-23', desktop: 480, mobile: 530 },
{ date: '2024-06-24', desktop: 132, mobile: 180 },
{ date: '2024-06-25', desktop: 141, mobile: 190 },
{ date: '2024-06-26', desktop: 434, mobile: 380 },
{ date: '2024-06-27', desktop: 448, mobile: 490 },
{ date: '2024-06-28', desktop: 149, mobile: 200 },
{ date: '2024-06-29', desktop: 103, mobile: 160 },
{ date: '2024-06-30', desktop: 446, mobile: 400 },
];
const chartConfig = {
views: {
label: 'Page Views',
},
desktop: {
label: 'Desktop',
color: 'hsl(var(--chart-1))',
},
mobile: {
label: 'Mobile',
color: 'hsl(var(--chart-2))',
},
} satisfies ChartConfig;
const total = useMemo(
() => ({
desktop: chartData.reduce((acc, curr) => acc + curr.desktop, 0),
mobile: chartData.reduce((acc, curr) => acc + curr.mobile, 0),
}),
[],
);
return (
<Card>
<CardHeader className="flex flex-col items-stretch space-y-0 border-b p-0 sm:flex-row">
<div className="flex flex-1 flex-col justify-center gap-1 px-6 py-5 sm:py-6">
<CardTitle>Page Views</CardTitle>
<CardDescription>
Showing total visitors for the last 3 months
</CardDescription>
</div>
<div className="flex">
{['desktop', 'mobile'].map((key) => {
const chart = key as keyof typeof chartConfig;
return (
<button
key={chart}
data-active={activeChart === chart}
className="relative z-30 flex flex-1 flex-col justify-center gap-1 border-t px-6 py-4 text-left even:border-l data-[active=true]:bg-muted/50 sm:border-l sm:border-t-0 sm:px-8 sm:py-6"
onClick={() => setActiveChart(chart)}
>
<span className="text-xs text-muted-foreground">
{chartConfig[chart].label}
</span>
<span className="text-lg font-bold leading-none sm:text-3xl">
{total[key as keyof typeof total].toLocaleString()}
</span>
</button>
);
})}
</div>
</CardHeader>
<CardContent className="px-2 sm:p-6">
<ChartContainer
config={chartConfig}
className="aspect-auto h-64 w-full"
>
<BarChart accessibilityLayer data={chartData}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="date"
tickLine={false}
axisLine={false}
tickMargin={8}
minTickGap={32}
tickFormatter={(value) => {
const date = new Date(value);
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
});
}}
/>
<ChartTooltip
content={
<ChartTooltipContent
className="w-[150px]"
nameKey="views"
labelFormatter={(value) => {
return new Date(value).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
});
}}
/>
}
/>
<Bar dataKey={activeChart} fill={`var(--color-${activeChart})`} />
</BarChart>
</ChartContainer>
</CardContent>
</Card>
);
}

View File

@@ -1,896 +1,17 @@
'use client';
import { useMemo, useState } from 'react';
import dynamic from 'next/dynamic';
import { ArrowDown, ArrowUp, Menu, TrendingUp } from 'lucide-react';
import {
Area,
AreaChart,
Bar,
BarChart,
CartesianGrid,
Line,
LineChart,
XAxis,
} from 'recharts';
import { LoadingOverlay } from '@kit/ui/loading-overlay';
import { Trans } from '@kit/ui/trans';
import { Badge } from '@kit/ui/badge';
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@kit/ui/card';
import {
ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from '@kit/ui/chart';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@kit/ui/table';
export default function DashboardDemo() {
const mrr = useMemo(() => generateDemoData(), []);
const netRevenue = useMemo(() => generateDemoData(), []);
const fees = useMemo(() => generateDemoData(), []);
const newCustomers = useMemo(() => generateDemoData(), []);
return (
<div
className={
'flex flex-col space-y-4 pb-36 duration-500 animate-in fade-in'
}
>
<div
className={
'grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4'
}
>
<Card>
<CardHeader>
<CardTitle className={'flex items-center gap-2.5'}>
<span>MRR</span>
<Trend trend={'up'}>20%</Trend>
</CardTitle>
<CardDescription>
<span>Monthly recurring revenue</span>
</CardDescription>
<div>
<Figure>{`$${mrr[1]}`}</Figure>
</div>
</CardHeader>
<CardContent className={'space-y-4'}>
<Chart data={mrr[0]} />
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className={'flex items-center gap-2.5'}>
<span>Revenue</span>
<Trend trend={'up'}>12%</Trend>
</CardTitle>
<CardDescription>
<span>Total revenue including fees</span>
</CardDescription>
<div>
<Figure>{`$${netRevenue[1]}`}</Figure>
</div>
</CardHeader>
<CardContent>
<Chart data={netRevenue[0]} />
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className={'flex items-center gap-2.5'}>
<span>Fees</span>
<Trend trend={'up'}>9%</Trend>
</CardTitle>
<CardDescription>
<span>Total fees collected</span>
</CardDescription>
<div>
<Figure>{`$${fees[1]}`}</Figure>
</div>
</CardHeader>
<CardContent>
<Chart data={fees[0]} />
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className={'flex items-center gap-2.5'}>
<span>New Customers</span>
<Trend trend={'down'}>-25%</Trend>
</CardTitle>
<CardDescription>
<span>Customers who signed up this month</span>
</CardDescription>
<div>
<Figure>{`${Number(newCustomers[1]).toFixed(0)}`}</Figure>
</div>
</CardHeader>
<CardContent>
<Chart data={newCustomers[0]} />
</CardContent>
</Card>
</div>
<VisitorsChart />
<PageViewsChart />
<div>
<Card>
<CardHeader>
<CardTitle>Best Customers</CardTitle>
<CardDescription>Showing the top customers by MRR</CardDescription>
</CardHeader>
<CardContent>
<CustomersTable />
</CardContent>
</Card>
</div>
</div>
);
}
function generateDemoData() {
const today = new Date();
const formatter = new Intl.DateTimeFormat('en-us', {
month: 'long',
year: '2-digit',
});
const data: { value: string; name: string }[] = [];
for (let n = 8; n > 0; n -= 1) {
const date = new Date(today.getFullYear(), today.getMonth() - n, 1);
data.push({
name: formatter.format(date),
value: (Math.random() * 10).toFixed(1),
});
}
const lastValue = data[data.length - 1]?.value;
return [data, lastValue] as [typeof data, string];
}
function Chart(
props: React.PropsWithChildren<{ data: { value: string; name: string }[] }>,
) {
const chartConfig = {
desktop: {
label: 'Desktop',
color: 'hsl(var(--chart-1))',
},
mobile: {
label: 'Mobile',
color: 'hsl(var(--chart-2))',
},
} satisfies ChartConfig;
return (
<ChartContainer config={chartConfig}>
<LineChart accessibilityLayer data={props.data}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="name"
tickLine={false}
axisLine={false}
tickMargin={8}
/>
<ChartTooltip
cursor={false}
content={<ChartTooltipContent hideLabel />}
/>
<Line
dataKey="value"
type="natural"
stroke="var(--color-desktop)"
strokeWidth={2}
dot={false}
/>
</LineChart>
</ChartContainer>
);
}
function CustomersTable() {
const customers = [
{
name: 'John Doe',
email: 'john@makerkit.dev',
plan: 'Pro',
mrr: '$120.5',
logins: 1020,
status: 'Healthy',
trend: 'up',
},
{
name: 'Emma Smith',
email: 'emma@makerit.dev',
plan: 'Basic',
mrr: '$65.4',
logins: 570,
status: 'Possible Churn',
trend: 'stale',
},
{
name: 'Robert Johnson',
email: 'robert@makerkit.dev',
plan: 'Pro',
mrr: '$500.1',
logins: 2050,
status: 'Healthy',
trend: 'up',
},
{
name: 'Olivia Brown',
email: 'olivia@makerkit.dev',
plan: 'Basic',
mrr: '$10',
logins: 50,
status: 'Churn',
trend: 'down',
},
{
name: 'Michael Davis',
email: 'michael@makerkit.dev',
plan: 'Pro',
mrr: '$300.2',
logins: 1520,
status: 'Healthy',
trend: 'up',
},
{
name: 'Emily Jones',
email: 'emily@makerkit.dev',
plan: 'Pro',
mrr: '$75.7',
logins: 780,
status: 'Healthy',
trend: 'up',
},
{
name: 'Daniel Garcia',
email: 'daniel@makerkit.dev',
plan: 'Basic',
mrr: '$50',
logins: 320,
status: 'Possible Churn',
trend: 'stale',
},
{
name: 'Liam Miller',
email: 'liam@makerkit.dev',
plan: 'Pro',
mrr: '$90.8',
logins: 1260,
status: 'Healthy',
trend: 'up',
},
{
name: 'Emma Clark',
email: 'emma@makerkit.dev',
plan: 'Basic',
mrr: '$0',
logins: 20,
status: 'Churn',
trend: 'down',
},
{
name: 'Elizabeth Rodriguez',
email: 'liz@makerkit.dev',
plan: 'Pro',
mrr: '$145.3',
logins: 1380,
status: 'Healthy',
trend: 'up',
},
{
name: 'James Martinez',
email: 'james@makerkit.dev',
plan: 'Pro',
mrr: '$120.5',
logins: 940,
status: 'Healthy',
trend: 'up',
},
{
name: 'Charlotte Ryan',
email: 'carlotte@makerkit.dev',
plan: 'Basic',
mrr: '$80.6',
logins: 460,
status: 'Possible Churn',
trend: 'stale',
},
{
name: 'Lucas Evans',
email: 'lucas@makerkit.dev',
plan: 'Pro',
mrr: '$210.3',
logins: 1850,
status: 'Healthy',
trend: 'up',
},
{
name: 'Sophia Wilson',
email: 'sophia@makerkit.dev',
plan: 'Basic',
mrr: '$10',
logins: 35,
status: 'Churn',
trend: 'down',
},
{
name: 'William Kelly',
email: 'will@makerkit.dev',
plan: 'Pro',
mrr: '$350.2',
logins: 1760,
status: 'Healthy',
trend: 'up',
},
{
name: 'Oliver Thomas',
email: 'olly@makerkit.dev',
plan: 'Pro',
mrr: '$145.6',
logins: 1350,
status: 'Healthy',
trend: 'up',
},
{
name: 'Samantha White',
email: 'sam@makerkit.dev',
plan: 'Basic',
mrr: '$60.3',
logins: 425,
status: 'Possible Churn',
trend: 'stale',
},
{
name: 'Benjamin Lewis',
email: 'ben@makerkit.dev',
plan: 'Pro',
mrr: '$175.8',
logins: 1600,
status: 'Healthy',
trend: 'up',
},
{
name: 'Zoe Harris',
email: 'zoe@makerkit.dev',
plan: 'Basic',
mrr: '$0',
logins: 18,
status: 'Churn',
trend: 'down',
},
{
name: 'Zachary Nelson',
email: 'zac@makerkit.dev',
plan: 'Pro',
mrr: '$255.9',
logins: 1785,
status: 'Healthy',
trend: 'up',
},
];
return (
<Table>
<TableHeader>
<TableRow>
<TableHead>Customer</TableHead>
<TableHead>Plan</TableHead>
<TableHead>MRR</TableHead>
<TableHead>Logins</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{customers.map((customer) => (
<TableRow key={customer.name}>
<TableCell className={'flex flex-col'}>
<span>{customer.name}</span>
<span className={'text-sm text-muted-foreground'}>
{customer.email}
</span>
</TableCell>
<TableCell>{customer.plan}</TableCell>
<TableCell>{customer.mrr}</TableCell>
<TableCell>{customer.logins}</TableCell>
<TableCell>
<BadgeWithTrend trend={customer.trend}>
{customer.status}
</BadgeWithTrend>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
}
function BadgeWithTrend(props: React.PropsWithChildren<{ trend: string }>) {
const className = useMemo(() => {
switch (props.trend) {
case 'up':
return 'text-green-500';
case 'down':
return 'text-destructive';
case 'stale':
return 'text-orange-500';
}
}, [props.trend]);
return (
<Badge
variant={'outline'}
className={'border-transparent px-1.5 font-normal'}
>
<span className={className}>{props.children}</span>
</Badge>
);
}
function Figure(props: React.PropsWithChildren) {
return (
<div className={'font-heading text-2xl font-semibold'}>
{props.children}
</div>
);
}
function Trend(
props: React.PropsWithChildren<{
trend: 'up' | 'down' | 'stale';
}>,
) {
const Icon = useMemo(() => {
switch (props.trend) {
case 'up':
return <ArrowUp className={'h-3 w-3 text-green-500'} />;
case 'down':
return <ArrowDown className={'h-3 w-3 text-destructive'} />;
case 'stale':
return <Menu className={'h-3 w-3 text-orange-500'} />;
}
}, [props.trend]);
return (
<div>
<BadgeWithTrend trend={props.trend}>
<span className={'flex items-center space-x-1'}>
{Icon}
<span>{props.children}</span>
</span>
</BadgeWithTrend>
</div>
);
}
export function VisitorsChart() {
const chartData = useMemo(
() => [
{ date: '2024-04-01', desktop: 222, mobile: 150 },
{ date: '2024-04-02', desktop: 97, mobile: 180 },
{ date: '2024-04-03', desktop: 167, mobile: 120 },
{ date: '2024-04-04', desktop: 242, mobile: 260 },
{ date: '2024-04-05', desktop: 373, mobile: 290 },
{ date: '2024-04-06', desktop: 301, mobile: 340 },
{ date: '2024-04-07', desktop: 245, mobile: 180 },
{ date: '2024-04-08', desktop: 409, mobile: 320 },
{ date: '2024-04-09', desktop: 59, mobile: 110 },
{ date: '2024-04-10', desktop: 261, mobile: 190 },
{ date: '2024-04-11', desktop: 327, mobile: 350 },
{ date: '2024-04-12', desktop: 292, mobile: 210 },
{ date: '2024-04-13', desktop: 342, mobile: 380 },
{ date: '2024-04-14', desktop: 137, mobile: 220 },
{ date: '2024-04-15', desktop: 120, mobile: 170 },
{ date: '2024-04-16', desktop: 138, mobile: 190 },
{ date: '2024-04-17', desktop: 446, mobile: 360 },
{ date: '2024-04-18', desktop: 364, mobile: 410 },
{ date: '2024-04-19', desktop: 243, mobile: 180 },
{ date: '2024-04-20', desktop: 89, mobile: 150 },
{ date: '2024-04-21', desktop: 137, mobile: 200 },
{ date: '2024-04-22', desktop: 224, mobile: 170 },
{ date: '2024-04-23', desktop: 138, mobile: 230 },
{ date: '2024-04-24', desktop: 387, mobile: 290 },
{ date: '2024-04-25', desktop: 215, mobile: 250 },
{ date: '2024-04-26', desktop: 75, mobile: 130 },
{ date: '2024-04-27', desktop: 383, mobile: 420 },
{ date: '2024-04-28', desktop: 122, mobile: 180 },
{ date: '2024-04-29', desktop: 315, mobile: 240 },
{ date: '2024-04-30', desktop: 454, mobile: 380 },
{ date: '2024-05-01', desktop: 165, mobile: 220 },
{ date: '2024-05-02', desktop: 293, mobile: 310 },
{ date: '2024-05-03', desktop: 247, mobile: 190 },
{ date: '2024-05-04', desktop: 385, mobile: 420 },
{ date: '2024-05-05', desktop: 481, mobile: 390 },
{ date: '2024-05-06', desktop: 498, mobile: 520 },
{ date: '2024-05-07', desktop: 388, mobile: 300 },
{ date: '2024-05-08', desktop: 149, mobile: 210 },
{ date: '2024-05-09', desktop: 227, mobile: 180 },
{ date: '2024-05-10', desktop: 293, mobile: 330 },
{ date: '2024-05-11', desktop: 335, mobile: 270 },
{ date: '2024-05-12', desktop: 197, mobile: 240 },
{ date: '2024-05-13', desktop: 197, mobile: 160 },
{ date: '2024-05-14', desktop: 448, mobile: 490 },
{ date: '2024-05-15', desktop: 473, mobile: 380 },
{ date: '2024-05-16', desktop: 338, mobile: 400 },
{ date: '2024-05-17', desktop: 499, mobile: 420 },
{ date: '2024-05-18', desktop: 315, mobile: 350 },
{ date: '2024-05-19', desktop: 235, mobile: 180 },
{ date: '2024-05-20', desktop: 177, mobile: 230 },
{ date: '2024-05-21', desktop: 82, mobile: 140 },
{ date: '2024-05-22', desktop: 81, mobile: 120 },
{ date: '2024-05-23', desktop: 252, mobile: 290 },
{ date: '2024-05-24', desktop: 294, mobile: 220 },
{ date: '2024-05-25', desktop: 201, mobile: 250 },
{ date: '2024-05-26', desktop: 213, mobile: 170 },
{ date: '2024-05-27', desktop: 420, mobile: 460 },
{ date: '2024-05-28', desktop: 233, mobile: 190 },
{ date: '2024-05-29', desktop: 78, mobile: 130 },
{ date: '2024-05-30', desktop: 340, mobile: 280 },
{ date: '2024-05-31', desktop: 178, mobile: 230 },
{ date: '2024-06-01', desktop: 178, mobile: 200 },
{ date: '2024-06-02', desktop: 470, mobile: 410 },
{ date: '2024-06-03', desktop: 103, mobile: 160 },
{ date: '2024-06-04', desktop: 439, mobile: 380 },
{ date: '2024-06-05', desktop: 88, mobile: 140 },
{ date: '2024-06-06', desktop: 294, mobile: 250 },
{ date: '2024-06-07', desktop: 323, mobile: 370 },
{ date: '2024-06-08', desktop: 385, mobile: 320 },
{ date: '2024-06-09', desktop: 438, mobile: 480 },
{ date: '2024-06-10', desktop: 155, mobile: 200 },
{ date: '2024-06-11', desktop: 92, mobile: 150 },
{ date: '2024-06-12', desktop: 492, mobile: 420 },
{ date: '2024-06-13', desktop: 81, mobile: 130 },
{ date: '2024-06-14', desktop: 426, mobile: 380 },
{ date: '2024-06-15', desktop: 307, mobile: 350 },
{ date: '2024-06-16', desktop: 371, mobile: 310 },
{ date: '2024-06-17', desktop: 475, mobile: 520 },
{ date: '2024-06-18', desktop: 107, mobile: 170 },
{ date: '2024-06-19', desktop: 341, mobile: 290 },
{ date: '2024-06-20', desktop: 408, mobile: 450 },
{ date: '2024-06-21', desktop: 169, mobile: 210 },
{ date: '2024-06-22', desktop: 317, mobile: 270 },
{ date: '2024-06-23', desktop: 480, mobile: 530 },
{ date: '2024-06-24', desktop: 132, mobile: 180 },
{ date: '2024-06-25', desktop: 141, mobile: 190 },
{ date: '2024-06-26', desktop: 434, mobile: 380 },
{ date: '2024-06-27', desktop: 448, mobile: 490 },
{ date: '2024-06-28', desktop: 149, mobile: 200 },
{ date: '2024-06-29', desktop: 103, mobile: 160 },
{ date: '2024-06-30', desktop: 446, mobile: 400 },
],
[],
);
const chartConfig = {
visitors: {
label: 'Visitors',
},
desktop: {
label: 'Desktop',
color: 'hsl(var(--chart-1))',
},
mobile: {
label: 'Mobile',
color: 'hsl(var(--chart-2))',
},
} satisfies ChartConfig;
return (
<Card>
<CardHeader>
<CardTitle>Visitors</CardTitle>
<CardDescription>
Showing total visitors for the last 6 months
</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer className={'h-64 w-full'} config={chartConfig}>
<AreaChart accessibilityLayer data={chartData}>
<defs>
<linearGradient id="fillDesktop" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="var(--color-desktop)"
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor="var(--color-desktop)"
stopOpacity={0.1}
/>
</linearGradient>
<linearGradient id="fillMobile" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="var(--color-mobile)"
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor="var(--color-mobile)"
stopOpacity={0.1}
/>
</linearGradient>
</defs>
<CartesianGrid vertical={false} />
<XAxis
dataKey="month"
tickLine={false}
axisLine={false}
tickMargin={8}
tickFormatter={(value) => value.slice(0, 3)}
/>
<ChartTooltip
cursor={false}
content={<ChartTooltipContent indicator="dot" />}
/>
<Area
dataKey="mobile"
type="natural"
fill="url(#fillMobile)"
fillOpacity={0.4}
stroke="var(--color-mobile)"
stackId="a"
/>
<Area
dataKey="desktop"
type="natural"
fill="url(#fillDesktop)"
fillOpacity={0.4}
stroke="var(--color-desktop)"
stackId="a"
/>
</AreaChart>
</ChartContainer>
</CardContent>
<CardFooter>
<div className="flex w-full items-start gap-2 text-sm">
<div className="grid gap-2">
<div className="flex items-center gap-2 font-medium leading-none">
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
</div>
<div className="flex items-center gap-2 leading-none text-muted-foreground">
January - June 2024
</div>
</div>
</div>
</CardFooter>
</Card>
);
}
export function PageViewsChart() {
const [activeChart, setActiveChart] =
useState<keyof typeof chartConfig>('desktop');
const chartData = [
{ date: '2024-04-01', desktop: 222, mobile: 150 },
{ date: '2024-04-02', desktop: 97, mobile: 180 },
{ date: '2024-04-03', desktop: 167, mobile: 120 },
{ date: '2024-04-04', desktop: 242, mobile: 260 },
{ date: '2024-04-05', desktop: 373, mobile: 290 },
{ date: '2024-04-06', desktop: 301, mobile: 340 },
{ date: '2024-04-07', desktop: 245, mobile: 180 },
{ date: '2024-04-08', desktop: 409, mobile: 320 },
{ date: '2024-04-09', desktop: 59, mobile: 110 },
{ date: '2024-04-10', desktop: 261, mobile: 190 },
{ date: '2024-04-11', desktop: 327, mobile: 350 },
{ date: '2024-04-12', desktop: 292, mobile: 210 },
{ date: '2024-04-13', desktop: 342, mobile: 380 },
{ date: '2024-04-14', desktop: 137, mobile: 220 },
{ date: '2024-04-15', desktop: 120, mobile: 170 },
{ date: '2024-04-16', desktop: 138, mobile: 190 },
{ date: '2024-04-17', desktop: 446, mobile: 360 },
{ date: '2024-04-18', desktop: 364, mobile: 410 },
{ date: '2024-04-19', desktop: 243, mobile: 180 },
{ date: '2024-04-20', desktop: 89, mobile: 150 },
{ date: '2024-04-21', desktop: 137, mobile: 200 },
{ date: '2024-04-22', desktop: 224, mobile: 170 },
{ date: '2024-04-23', desktop: 138, mobile: 230 },
{ date: '2024-04-24', desktop: 387, mobile: 290 },
{ date: '2024-04-25', desktop: 215, mobile: 250 },
{ date: '2024-04-26', desktop: 75, mobile: 130 },
{ date: '2024-04-27', desktop: 383, mobile: 420 },
{ date: '2024-04-28', desktop: 122, mobile: 180 },
{ date: '2024-04-29', desktop: 315, mobile: 240 },
{ date: '2024-04-30', desktop: 454, mobile: 380 },
{ date: '2024-05-01', desktop: 165, mobile: 220 },
{ date: '2024-05-02', desktop: 293, mobile: 310 },
{ date: '2024-05-03', desktop: 247, mobile: 190 },
{ date: '2024-05-04', desktop: 385, mobile: 420 },
{ date: '2024-05-05', desktop: 481, mobile: 390 },
{ date: '2024-05-06', desktop: 498, mobile: 520 },
{ date: '2024-05-07', desktop: 388, mobile: 300 },
{ date: '2024-05-08', desktop: 149, mobile: 210 },
{ date: '2024-05-09', desktop: 227, mobile: 180 },
{ date: '2024-05-10', desktop: 293, mobile: 330 },
{ date: '2024-05-11', desktop: 335, mobile: 270 },
{ date: '2024-05-12', desktop: 197, mobile: 240 },
{ date: '2024-05-13', desktop: 197, mobile: 160 },
{ date: '2024-05-14', desktop: 448, mobile: 490 },
{ date: '2024-05-15', desktop: 473, mobile: 380 },
{ date: '2024-05-16', desktop: 338, mobile: 400 },
{ date: '2024-05-17', desktop: 499, mobile: 420 },
{ date: '2024-05-18', desktop: 315, mobile: 350 },
{ date: '2024-05-19', desktop: 235, mobile: 180 },
{ date: '2024-05-20', desktop: 177, mobile: 230 },
{ date: '2024-05-21', desktop: 82, mobile: 140 },
{ date: '2024-05-22', desktop: 81, mobile: 120 },
{ date: '2024-05-23', desktop: 252, mobile: 290 },
{ date: '2024-05-24', desktop: 294, mobile: 220 },
{ date: '2024-05-25', desktop: 201, mobile: 250 },
{ date: '2024-05-26', desktop: 213, mobile: 170 },
{ date: '2024-05-27', desktop: 420, mobile: 460 },
{ date: '2024-05-28', desktop: 233, mobile: 190 },
{ date: '2024-05-29', desktop: 78, mobile: 130 },
{ date: '2024-05-30', desktop: 340, mobile: 280 },
{ date: '2024-05-31', desktop: 178, mobile: 230 },
{ date: '2024-06-01', desktop: 178, mobile: 200 },
{ date: '2024-06-02', desktop: 470, mobile: 410 },
{ date: '2024-06-03', desktop: 103, mobile: 160 },
{ date: '2024-06-04', desktop: 439, mobile: 380 },
{ date: '2024-06-05', desktop: 88, mobile: 140 },
{ date: '2024-06-06', desktop: 294, mobile: 250 },
{ date: '2024-06-07', desktop: 323, mobile: 370 },
{ date: '2024-06-08', desktop: 385, mobile: 320 },
{ date: '2024-06-09', desktop: 438, mobile: 480 },
{ date: '2024-06-10', desktop: 155, mobile: 200 },
{ date: '2024-06-11', desktop: 92, mobile: 150 },
{ date: '2024-06-12', desktop: 492, mobile: 420 },
{ date: '2024-06-13', desktop: 81, mobile: 130 },
{ date: '2024-06-14', desktop: 426, mobile: 380 },
{ date: '2024-06-15', desktop: 307, mobile: 350 },
{ date: '2024-06-16', desktop: 371, mobile: 310 },
{ date: '2024-06-17', desktop: 475, mobile: 520 },
{ date: '2024-06-18', desktop: 107, mobile: 170 },
{ date: '2024-06-19', desktop: 341, mobile: 290 },
{ date: '2024-06-20', desktop: 408, mobile: 450 },
{ date: '2024-06-21', desktop: 169, mobile: 210 },
{ date: '2024-06-22', desktop: 317, mobile: 270 },
{ date: '2024-06-23', desktop: 480, mobile: 530 },
{ date: '2024-06-24', desktop: 132, mobile: 180 },
{ date: '2024-06-25', desktop: 141, mobile: 190 },
{ date: '2024-06-26', desktop: 434, mobile: 380 },
{ date: '2024-06-27', desktop: 448, mobile: 490 },
{ date: '2024-06-28', desktop: 149, mobile: 200 },
{ date: '2024-06-29', desktop: 103, mobile: 160 },
{ date: '2024-06-30', desktop: 446, mobile: 400 },
];
const chartConfig = {
views: {
label: 'Page Views',
},
desktop: {
label: 'Desktop',
color: 'hsl(var(--chart-1))',
},
mobile: {
label: 'Mobile',
color: 'hsl(var(--chart-2))',
},
} satisfies ChartConfig;
const total = useMemo(
() => ({
desktop: chartData.reduce((acc, curr) => acc + curr.desktop, 0),
mobile: chartData.reduce((acc, curr) => acc + curr.mobile, 0),
}),
[],
);
return (
<Card>
<CardHeader className="flex flex-col items-stretch space-y-0 border-b p-0 sm:flex-row">
<div className="flex flex-1 flex-col justify-center gap-1 px-6 py-5 sm:py-6">
<CardTitle>Page Views</CardTitle>
<CardDescription>
Showing total visitors for the last 3 months
</CardDescription>
</div>
<div className="flex">
{['desktop', 'mobile'].map((key) => {
const chart = key as keyof typeof chartConfig;
return (
<button
key={chart}
data-active={activeChart === chart}
className="relative z-30 flex flex-1 flex-col justify-center gap-1 border-t px-6 py-4 text-left even:border-l data-[active=true]:bg-muted/50 sm:border-l sm:border-t-0 sm:px-8 sm:py-6"
onClick={() => setActiveChart(chart)}
>
<span className="text-xs text-muted-foreground">
{chartConfig[chart].label}
</span>
<span className="text-lg font-bold leading-none sm:text-3xl">
{total[key as keyof typeof total].toLocaleString()}
</span>
</button>
);
})}
</div>
</CardHeader>
<CardContent className="px-2 sm:p-6">
<ChartContainer
config={chartConfig}
className="aspect-auto h-64 w-full"
>
<BarChart accessibilityLayer data={chartData}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="date"
tickLine={false}
axisLine={false}
tickMargin={8}
minTickGap={32}
tickFormatter={(value) => {
const date = new Date(value);
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
});
}}
/>
<ChartTooltip
content={
<ChartTooltipContent
className="w-[150px]"
nameKey="views"
labelFormatter={(value) => {
return new Date(value).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
});
}}
/>
}
/>
<Bar dataKey={activeChart} fill={`var(--color-${activeChart})`} />
</BarChart>
</ChartContainer>
</CardContent>
</Card>
);
}
export const DashboardDemo = dynamic(() => import('./dashboard-demo-charts'), {
ssr: false,
loading: () => (
<LoadingOverlay>
<span className={'text-muted-foreground'}>
<Trans i18nKey={'common:loading'} />
</span>
</LoadingOverlay>
),
});

View File

@@ -166,6 +166,7 @@ function TeamAccountsModal(props: { accounts: Accounts; userId: string }) {
<div className={'py-16'}>
<AccountSelector
className={'w-full max-w-full'}
collisionPadding={0}
userId={props.userId}
onAccountChange={(value) => {
const path = value

View File

@@ -87,9 +87,7 @@ function SidebarContainer(props: {
<div className={'absolute bottom-4 left-0 w-full'}>
<SidebarContent>
<ProfileAccountDropdownContainer
user={props.user}
/>
<ProfileAccountDropdownContainer user={props.user} />
</SidebarContent>
</div>
</>

View File

@@ -61,10 +61,7 @@ export function TeamAccountNavigationMenu(props: {
<TeamAccountNotifications accountId={account.id} userId={user.id} />
<ProfileAccountDropdownContainer
user={user}
account={account}
/>
<ProfileAccountDropdownContainer user={user} account={account} />
</div>
</div>
);

View File

@@ -0,0 +1,14 @@
'use client';
import dynamic from 'next/dynamic';
export const EmbeddedCheckoutForm = dynamic(
async () => {
const { EmbeddedCheckout } = await import('@kit/billing-gateway/checkout');
return EmbeddedCheckout;
},
{
ssr: false,
},
);

View File

@@ -25,7 +25,7 @@ const enabled = featureFlagsConfig.enableTeamAccountBilling;
* @description Creates a checkout session for a team account.
*/
export const createTeamAccountCheckoutSession = enhanceAction(
(data) => {
async (data) => {
if (!enabled) {
throw new Error('Team account billing is not enabled');
}

View File

@@ -229,6 +229,7 @@ class TeamBillingService {
customerId,
accountId,
name: this.namespace,
error,
},
`Billing Portal session was not created`,
);
@@ -288,7 +289,7 @@ class TeamBillingService {
`Encountered an error while fetching the number of existing seats`,
);
return Promise.reject(error);
return Promise.reject(error as Error);
}
}
}

View File

@@ -23,10 +23,8 @@ import { loadTeamWorkspace } from '../_lib/server/team-account-workspace.loader'
import { TeamAccountCheckoutForm } from './_components/team-account-checkout-form';
import { createBillingPortalSession } from './_lib/server/server-actions';
interface Params {
params: {
account: string;
};
interface TeamAccountBillingPageProps {
params: Promise<{ account: string }>;
}
export const generateMetadata = async () => {
@@ -38,8 +36,9 @@ export const generateMetadata = async () => {
};
};
async function TeamAccountBillingPage({ params }: Params) {
const workspace = await loadTeamWorkspace(params.account);
async function TeamAccountBillingPage({ params }: TeamAccountBillingPageProps) {
const account = (await params).account;
const workspace = await loadTeamWorkspace(account);
const accountId = workspace.account.id;
const [data, customerId] = await loadTeamAccountBillingPage(accountId);
@@ -65,7 +64,7 @@ async function TeamAccountBillingPage({ params }: Params) {
return (
<form action={createBillingPortalSession}>
<input type="hidden" name={'accountId'} value={accountId} />
<input type="hidden" name={'slug'} value={params.account} />
<input type="hidden" name={'slug'} value={account} />
<BillingPortalCard />
</form>
@@ -75,7 +74,7 @@ async function TeamAccountBillingPage({ params }: Params) {
return (
<>
<TeamAccountLayoutPageHeader
account={params.account}
account={account}
title={<Trans i18nKey={'common:routes.billing'} />}
description={<AppBreadcrumbs />}
/>

View File

@@ -1,5 +1,3 @@
import { revalidatePath } from 'next/cache';
import dynamic from 'next/dynamic';
import { notFound, redirect } from 'next/navigation';
import { getBillingGatewayProvider } from '@kit/billing-gateway';
@@ -10,25 +8,16 @@ import billingConfig from '~/config/billing.config';
import { withI18n } from '~/lib/i18n/with-i18n';
import { requireUserInServerComponent } from '~/lib/server/require-user-in-server-component';
import { EmbeddedCheckoutForm } from '../_components/embedded-checkout-form';
interface SessionPageProps {
searchParams: {
searchParams: Promise<{
session_id: string;
};
}>;
}
const LazyEmbeddedCheckout = dynamic(
async () => {
const { EmbeddedCheckout } = await import('@kit/billing-gateway/checkout');
return EmbeddedCheckout;
},
{
ssr: false,
},
);
async function ReturnCheckoutSessionPage({ searchParams }: SessionPageProps) {
const sessionId = searchParams.session_id;
const sessionId = (await searchParams).session_id;
if (!sessionId) {
redirect('../');
@@ -38,7 +27,7 @@ async function ReturnCheckoutSessionPage({ searchParams }: SessionPageProps) {
if (checkoutToken) {
return (
<LazyEmbeddedCheckout
<EmbeddedCheckoutForm
checkoutToken={checkoutToken}
provider={billingConfig.provider}
/>
@@ -49,7 +38,7 @@ async function ReturnCheckoutSessionPage({ searchParams }: SessionPageProps) {
<>
<div className={'fixed left-0 top-48 z-50 mx-auto w-full'}>
<BillingSessionStatus
onRedirect={onRedirect}
redirectPath={'../billing'}
customerEmail={customerEmail ?? ''}
/>
</div>
@@ -96,19 +85,3 @@ async function loadCheckoutSession(sessionId: string) {
checkoutToken,
};
}
/**
* Revalidates the layout to update cached pages
* and redirects back to the home page.
*/
// eslint-disable-next-line @typescript-eslint/require-await
async function onRedirect() {
'use server';
// revalidate the home page to update cached pages
// which may have changed due to the billing session
revalidatePath('/home', 'layout');
// redirect back to billing page
redirect('../billing');
}

View File

@@ -21,18 +21,14 @@ import { TeamAccountLayoutSidebar } from './_components/team-account-layout-side
import { TeamAccountNavigationMenu } from './_components/team-account-navigation-menu';
import { loadTeamWorkspace } from './_lib/server/team-account-workspace.loader';
interface TeamWorkspaceLayoutParams {
account: string;
}
type TeamWorkspaceLayoutProps = React.PropsWithChildren<{
params: Promise<{ account: string }>;
}>;
function TeamWorkspaceLayout({
children,
params,
}: React.PropsWithChildren<{
params: TeamWorkspaceLayoutParams;
}>) {
const data = use(loadTeamWorkspace(params.account));
const style = getLayoutStyle(params.account);
function TeamWorkspaceLayout({ children, params }: TeamWorkspaceLayoutProps) {
const account = use(params).account;
const data = use(loadTeamWorkspace(account));
const style = use(getLayoutStyle(account));
const accounts = data.accounts.map(({ name, slug, picture_url }) => ({
label: name,
@@ -45,7 +41,7 @@ function TeamWorkspaceLayout({
<PageNavigation>
<If condition={style === 'sidebar'}>
<TeamAccountLayoutSidebar
account={params.account}
account={account}
accountId={data.account.id}
accounts={accounts}
user={data.user}
@@ -64,7 +60,7 @@ function TeamWorkspaceLayout({
<TeamAccountLayoutMobileNavigation
userId={data.user.id}
accounts={accounts}
account={params.account}
account={account}
/>
</div>
</PageMobileNavigation>
@@ -76,9 +72,11 @@ function TeamWorkspaceLayout({
);
}
function getLayoutStyle(account: string) {
async function getLayoutStyle(account: string) {
const cookieStore = await cookies();
return (
(cookies().get('layout-style')?.value as PageLayoutStyle) ??
(cookieStore.get('layout-style')?.value as PageLayoutStyle) ??
getTeamAccountSidebarConfig(account).style
);
}

View File

@@ -26,10 +26,8 @@ import { withI18n } from '~/lib/i18n/with-i18n';
import { TeamAccountLayoutPageHeader } from '../_components/team-account-layout-page-header';
import { loadMembersPageData } from './_lib/server/members-page.loader';
interface Params {
params: {
account: string;
};
interface TeamAccountMembersPageProps {
params: Promise<{ account: string }>;
}
export const generateMetadata = async () => {
@@ -41,11 +39,12 @@ export const generateMetadata = async () => {
};
};
async function TeamAccountMembersPage({ params }: Params) {
async function TeamAccountMembersPage({ params }: TeamAccountMembersPageProps) {
const client = getSupabaseServerClient();
const slug = (await params).account;
const [members, invitations, canAddMember, { user, account }] =
await loadMembersPageData(client, params.account);
await loadMembersPageData(client, slug);
const canManageRoles = account.permissions.includes('roles.manage');
const canManageInvitations = account.permissions.includes('invites.manage');

View File

@@ -1,33 +1,19 @@
import loadDynamic from 'next/dynamic';
import { use } from 'react';
import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs';
import { LoadingOverlay } from '@kit/ui/loading-overlay';
import { PageBody } from '@kit/ui/page';
import { Trans } from '@kit/ui/trans';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
import { DashboardDemo } from './_components/dashboard-demo';
import { TeamAccountLayoutPageHeader } from './_components/team-account-layout-page-header';
interface Params {
account: string;
interface TeamAccountHomePageProps {
params: Promise<{ account: string }>;
}
const DashboardDemo = loadDynamic(
() => import('./_components/dashboard-demo'),
{
ssr: false,
loading: () => (
<LoadingOverlay>
<span className={'text-muted-foreground'}>
<Trans i18nKey={'common:loading'} />
</span>
</LoadingOverlay>
),
},
);
export const generateMetadata = async () => {
const i18n = await createI18nServerInstance();
const title = i18n.t('teams:home.pageTitle');
@@ -37,11 +23,13 @@ export const generateMetadata = async () => {
};
};
function TeamAccountHomePage({ params }: { params: Params }) {
function TeamAccountHomePage({ params }: TeamAccountHomePageProps) {
const account = use(params).account;
return (
<>
<TeamAccountLayoutPageHeader
account={params.account}
account={account}
title={<Trans i18nKey={'common:routes.dashboard'} />}
description={<AppBreadcrumbs />}
/>

View File

@@ -21,19 +21,18 @@ export const generateMetadata = async () => {
};
};
interface Props {
params: {
account: string;
};
interface TeamAccountSettingsPageProps {
params: Promise<{ account: string }>;
}
const paths = {
teamAccountSettings: pathsConfig.app.accountSettings,
};
async function TeamAccountSettingsPage(props: Props) {
async function TeamAccountSettingsPage(props: TeamAccountSettingsPageProps) {
const api = createTeamAccountsApi(getSupabaseServerClient());
const data = await api.getTeamAccount(props.params.account);
const slug = (await props.params).account;
const data = await api.getTeamAccount(slug);
const account = {
id: data.id,

View File

@@ -18,11 +18,11 @@ import pathsConfig from '~/config/paths.config';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
interface Context {
searchParams: {
interface JoinTeamAccountPageProps {
searchParams: Promise<{
invite_token?: string;
email?: string;
};
}>;
}
export const generateMetadata = async () => {
@@ -33,7 +33,8 @@ export const generateMetadata = async () => {
};
};
async function JoinTeamAccountPage({ searchParams }: Context) {
async function JoinTeamAccountPage(props: JoinTeamAccountPageProps) {
const searchParams = await props.searchParams;
const token = searchParams.invite_token;
// no token, redirect to 404

View File

@@ -16,7 +16,7 @@ export default async function RootLayout({
children: React.ReactNode;
}) {
const { language } = await createI18nServerInstance();
const theme = getTheme();
const theme = await getTheme();
const className = getClassName(theme);
return (
@@ -51,8 +51,9 @@ function getClassName(theme?: string) {
});
}
function getTheme() {
return cookies().get('theme')?.value as 'light' | 'dark' | 'system';
async function getTheme() {
const cookiesStore = await cookies();
return cookiesStore.get('theme')?.value as 'light' | 'dark' | 'system';
}
export const generateMetadata = generateRootMetadata;

View File

@@ -2,15 +2,15 @@
import { useEffect } from 'react';
import { usePathname, useSearchParams } from 'next/navigation';
import { analytics } from '@kit/analytics';
import { AppEvent, AppEventType, ConsumerProvidedEventTypes, useAppEvents } from '@kit/shared/events';
import {
AppEvent,
AppEventType,
ConsumerProvidedEventTypes,
useAppEvents,
} from '@kit/shared/events';
type AnalyticsMapping<
T extends ConsumerProvidedEventTypes = NonNullable<unknown>,

View File

@@ -18,7 +18,7 @@ const features = {
};
export function ProfileAccountDropdownContainer(props: {
user: User;
user?: User;
account?: {
id: string | null;
@@ -28,7 +28,11 @@ export function ProfileAccountDropdownContainer(props: {
}) {
const signOut = useSignOut();
const user = useUser(props.user);
const userData = user.data as User;
const userData = user.data;
if (!userData) {
return null;
}
return (
<PersonalAccountDropdown

View File

@@ -2,6 +2,8 @@
* This file is used to register monitoring instrumentation
* for your Next.js application.
*/
import { type Instrumentation } from 'next';
export async function register() {
const { registerMonitoringInstrumentation } = await import(
'@kit/monitoring/instrumentation'
@@ -11,3 +13,17 @@ export async function register() {
// based on the MONITORING_PROVIDER environment variable.
await registerMonitoringInstrumentation();
}
/**
* @name onRequestError
* @description This function is called when an error occurs during the request lifecycle.
* It is used to capture the error and send it to the monitoring service.
* @param err
*/
export const onRequestError: Instrumentation.onRequestError = async (err) => {
const { getServerMonitoringService } = await import('@kit/monitoring/server');
const service = await getServerMonitoringService();
await service.captureException(err as Error);
};

View File

@@ -0,0 +1,12 @@
/*
Mock modules for development.
This file is used to mock the modules that are not needed during development (unless they are used).
It allows the development server to load faster by not loading the modules that are not needed.
*/
export const Turnstile = undefined;
export const TurnstileProps = {};
export const useBaselimeRum = undefined;
export const BaselimeRum = undefined;

View File

@@ -30,8 +30,9 @@ const priority = featuresFlagConfig.languagePriority;
*
* Initialize the i18n instance for every RSC server request (eg. each page/layout)
*/
function createInstance() {
const cookie = cookies().get(I18N_COOKIE_NAME)?.value;
async function createInstance() {
const cookieStore = await cookies();
const cookie = cookieStore.get(I18N_COOKIE_NAME)?.value;
let selectedLanguage: string | undefined = undefined;
@@ -43,7 +44,7 @@ function createInstance() {
// if not, check if the language priority is set to user and
// use the user's preferred language
if (!selectedLanguage && priority === 'user') {
const userPreferredLanguage = getPreferredLanguageFromBrowser();
const userPreferredLanguage = await getPreferredLanguageFromBrowser();
selectedLanguage = getLanguageOrFallback(userPreferredLanguage);
}
@@ -55,8 +56,9 @@ function createInstance() {
export const createI18nServerInstance = cache(createInstance);
function getPreferredLanguageFromBrowser() {
const acceptLanguage = headers().get('accept-language');
async function getPreferredLanguageFromBrowser() {
const headersStore = await headers();
const acceptLanguage = headersStore.get('accept-language');
if (!acceptLanguage) {
return;

View File

@@ -8,8 +8,9 @@ import appConfig from '~/config/app.config';
* @name generateRootMetadata
* @description Generates the root metadata for the application
*/
export const generateRootMetadata = (): Metadata => {
const csrfToken = headers().get('x-csrf-token') ?? '';
export const generateRootMetadata = async (): Promise<Metadata> => {
const headersStore = await headers();
const csrfToken = headersStore.get('x-csrf-token') ?? '';
return {
title: appConfig.title,

View File

@@ -1,7 +1,6 @@
import withBundleAnalyzer from '@next/bundle-analyzer';
const IS_PRODUCTION = process.env.NODE_ENV === 'production';
const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL;
const ENABLE_REACT_COMPILER = process.env.ENABLE_REACT_COMPILER === 'true';
const INTERNAL_PACKAGES = [
'@kit/ui',
@@ -35,15 +34,17 @@ const config = {
fullUrl: true,
},
},
serverExternalPackages: [],
// needed for supporting dynamic imports for local content
outputFileTracingIncludes: {
'/*': ['./content/**/*'],
},
experimental: {
mdxRs: true,
instrumentationHook: true,
reactCompiler: ENABLE_REACT_COMPILER,
turbo: {
resolveExtensions: ['.ts', '.tsx', '.js', '.jsx'],
},
// needed for supporting dynamic imports for local content
outputFileTracingIncludes: {
'/*': ['./content/**/*'],
resolveAlias: getModulesAliases(),
},
optimizePackageImports: [
'recharts',
@@ -65,13 +66,10 @@ const config = {
typescript: { ignoreBuildErrors: true },
};
export default withBundleAnalyzer({
enabled: process.env.ANALYZE === 'true',
})(config);
export default config;
function getRemotePatterns() {
/** @type {import('next').NextConfig['remotePatterns']} */
// add here the remote patterns for your images
const remotePatterns = [];
if (SUPABASE_URL) {
@@ -96,3 +94,55 @@ function getRemotePatterns() {
},
];
}
/**
* @description Aliases modules based on the environment variables
* This will speed up the development server by not loading the modules that are not needed
* @returns {Record<string, string>}
*/
function getModulesAliases() {
if (process.env.NODE_ENV === 'production') {
return {};
}
const monitoringProvider = process.env.NEXT_PUBLIC_MONITORING_PROVIDER;
const billingProvider = process.env.NEXT_PUBLIC_BILLING_PROVIDER;
const mailerProvider = process.env.MAILER_PROVIDER;
const captchaProvider = process.env.NEXT_PUBLIC_CAPTCHA_SITE_KEY;
// exclude the modules that are not needed
const excludeSentry = monitoringProvider !== 'sentry';
const excludeBaselime = monitoringProvider !== 'baselime';
const excludeStripe = billingProvider !== 'stripe';
const excludeNodemailer = mailerProvider !== 'nodemailer';
const excludeTurnstile = !captchaProvider;
/** @type {Record<string, string>} */
const aliases = {};
// the path to the noop module
const noopPath = '~/lib/dev-mock-modules';
if (excludeSentry) {
aliases['@sentry/nextjs'] = noopPath;
}
if (excludeBaselime) {
aliases['@baselime/react-rum'] = noopPath;
}
if (excludeStripe) {
aliases['stripe'] = noopPath;
aliases['@stripe/stripe-js'] = noopPath;
}
if (excludeNodemailer) {
aliases['nodemailer'] = noopPath;
}
if (excludeTurnstile) {
aliases['@marsidev/react-turnstile'] = noopPath;
}
return aliases;
}

View File

@@ -10,7 +10,7 @@
"build:test": "NODE_ENV=test pnpm with-env:test next build",
"clean": "git clean -xdf .next .turbo node_modules",
"dev": "pnpm with-env next dev --turbo",
"next:lint": "next lint",
"lint": "next lint && eslint .",
"format": "prettier --check \"**/*.{js,cjs,mjs,ts,tsx,md,json}\"",
"start": "pnpm with-env next start",
"start:test": "NODE_ENV=test pnpm with-env:test next start",
@@ -55,21 +55,21 @@
"@makerkit/data-loader-supabase-nextjs": "^1.2.3",
"@marsidev/react-turnstile": "^1.0.2",
"@radix-ui/react-icons": "^1.3.0",
"@supabase/supabase-js": "^2.45.4",
"@tanstack/react-query": "5.56.2",
"@supabase/supabase-js": "^2.45.6",
"@tanstack/react-query": "5.59.15",
"@tanstack/react-table": "^8.20.5",
"date-fns": "^4.1.0",
"lucide-react": "^0.446.0",
"next": "14.2.13",
"lucide-react": "^0.453.0",
"next": "15.0.0",
"next-sitemap": "^4.2.3",
"next-themes": "0.3.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-hook-form": "^7.53.0",
"react-i18next": "^15.0.2",
"recharts": "^2.12.7",
"react": "19.0.0-rc-69d4b800-20241021",
"react-dom": "19.0.0-rc-69d4b800-20241021",
"react-hook-form": "^7.53.1",
"react-i18next": "^15.1.0",
"recharts": "2.13.0",
"sonner": "^1.5.0",
"tailwind-merge": "^2.5.2",
"tailwind-merge": "^2.5.4",
"zod": "^3.23.8"
},
"devDependencies": {
@@ -77,18 +77,21 @@
"@kit/prettier-config": "workspace:^",
"@kit/tailwind-config": "workspace:^",
"@kit/tsconfig": "workspace:^",
"@next/bundle-analyzer": "14.2.13",
"@next/bundle-analyzer": "15.0.0",
"@types/mdx": "^2.0.13",
"@types/node": "^22.5.5",
"@types/react": "^18.3.10",
"@types/react-dom": "^18.3.0",
"@types/node": "^22.7.8",
"@types/react": "npm:types-react@19.0.0-rc.1",
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
"autoprefixer": "^10.4.20",
"babel-plugin-react-compiler": "19.0.0-beta-8a03594-20241020",
"dotenv-cli": "^7.4.2",
"eslint": "^8.57.0",
"import-in-the-middle": "1.11.2",
"prettier": "^3.3.3",
"supabase": "^1.204.1",
"tailwindcss": "3.4.13",
"typescript": "^5.6.2"
"require-in-the-middle": "7.4.0",
"supabase": "^1.207.8",
"tailwindcss": "3.4.14",
"typescript": "^5.6.3"
},
"eslintConfig": {
"root": true,

View File

@@ -5,6 +5,6 @@ import baseConfig from '@kit/tailwind-config';
export default {
// We need to append the path to the UI package to the content array so that
// those classes are included correctly.
content: [...baseConfig.content],
content: [...baseConfig.content, './components/**/*.tsx', './app/**/*.tsx'],
presets: [baseConfig],
} satisfies Config;

View File

@@ -11,6 +11,7 @@
},
"scripts": {
"postinstall": "manypkg fix",
"run-script": "pnpm dlx tsx --tsconfig ./tsconfig.json apps/web/script.ts",
"build": "turbo build --cache-dir=.turbo",
"clean": "git clean -xdf node_modules dist .next",
"clean:workspaces": "turbo clean",
@@ -32,19 +33,20 @@
},
"prettier": "@kit/prettier-config",
"dependencies": {
"@manypkg/cli": "^0.21.4",
"@turbo/gen": "^2.1.3",
"@manypkg/cli": "^0.22.0",
"@turbo/gen": "^2.2.3",
"cross-env": "^7.0.3",
"pnpm": "^9.11.0",
"pnpm": "^9.12.2",
"prettier": "^3.3.3",
"turbo": "2.1.3",
"typescript": "^5.6.2"
"turbo": "2.2.3",
"typescript": "^5.6.3"
},
"packageManager": "pnpm@9.1.4",
"pnpm": {
"overrides": {
"react": "18.3.1",
"react-dom": "18.3.1"
"react-is": "rc",
"@types/react": "npm:types-react@19.0.0-rc.1",
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1"
}
},
"packageManager": "pnpm@9.1.4"
}
}

View File

@@ -17,7 +17,7 @@
"@kit/prettier-config": "workspace:*",
"@kit/tailwind-config": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@types/node": "^22.5.5"
"@types/node": "^22.7.8"
},
"eslintConfig": {
"root": true,

View File

@@ -27,14 +27,14 @@
"@kit/tailwind-config": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@kit/ui": "workspace:^",
"@supabase/supabase-js": "^2.45.4",
"@types/react": "^18.3.10",
"@supabase/supabase-js": "^2.45.6",
"@types/react": "^18.3.11",
"date-fns": "^4.1.0",
"lucide-react": "^0.446.0",
"next": "14.2.13",
"react": "18.3.1",
"react-hook-form": "^7.53.0",
"react-i18next": "^15.0.2",
"lucide-react": "^0.453.0",
"next": "15.0.0",
"react": "19.0.0-rc-69d4b800-20241021",
"react-hook-form": "^7.53.1",
"react-i18next": "^15.1.0",
"zod": "^3.23.8"
},
"eslintConfig": {

View File

@@ -1,3 +1,5 @@
import Link from 'next/link';
import { Check, ChevronRight } from 'lucide-react';
import { Button } from '@kit/ui/button';
@@ -11,10 +13,10 @@ import { Trans } from '@kit/ui/trans';
**/
export function BillingSessionStatus({
customerEmail,
onRedirect,
redirectPath,
}: React.PropsWithChildren<{
customerEmail: string;
onRedirect: () => void;
redirectPath: string;
}>) {
return (
<section
@@ -53,18 +55,17 @@ export function BillingSessionStatus({
</p>
</div>
<form>
<Button
data-test={'checkout-success-back-link'}
formAction={onRedirect}
>
<span>
<Trans i18nKey={'billing:checkoutSuccessBackButton'} />
</span>
<div>
<Button data-test={'checkout-success-back-link'} asChild>
<Link href={redirectPath}>
<span>
<Trans i18nKey={'billing:checkoutSuccessBackButton'} />
</span>
<ChevronRight className={'h-4'} />
<ChevronRight className={'h-4'} />
</Link>
</Button>
</form>
</div>
</div>
</section>
);

View File

@@ -14,7 +14,7 @@ import { Trans } from '@kit/ui/trans';
import { CurrentPlanBadge } from './current-plan-badge';
import { LineItemDetails } from './line-item-details';
type Order = Tables<'orders'>
type Order = Tables<'orders'>;
type LineItem = Tables<'order_items'>;
interface Props {

View File

@@ -4,7 +4,7 @@ import { Trans } from '@kit/ui/trans';
export function CurrentPlanAlert(
props: React.PropsWithoutRef<{
status: Enums<'subscription_status'>
status: Enums<'subscription_status'>;
}>,
) {
let variant: 'success' | 'warning' | 'destructive';

View File

@@ -397,12 +397,12 @@ function PlanIntervalSwitcher(
const selected = plan === props.interval;
const className = cn(
'focus:!ring-0 !outline-none animate-in transition-all fade-in',
'animate-in fade-in !outline-none transition-all focus:!ring-0',
{
'rounded-r-none border-r-transparent': index === 0,
'rounded-l-none': index === props.intervals.length - 1,
['hover:text-primary border text-muted-foreground']: !selected,
['font-semibold cursor-default hover:text-initial hover:bg-background']:
['hover:text-primary text-muted-foreground border']: !selected,
['hover:text-initial hover:bg-background cursor-default font-semibold']:
selected,
},
);

View File

@@ -4,7 +4,7 @@ import { Tables } from '@kit/supabase/database';
import { createBillingGatewayService } from '../billing-gateway/billing-gateway.service';
type Subscription = Tables<'subscriptions'>
type Subscription = Tables<'subscriptions'>;
export function createBillingWebhooksService() {
return new BillingWebhooksService();

View File

@@ -25,9 +25,9 @@
"@kit/tailwind-config": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@kit/ui": "workspace:^",
"@types/react": "^18.3.10",
"next": "14.2.13",
"react": "18.3.1",
"@types/react": "^18.3.11",
"next": "15.0.0",
"react": "19.0.0-rc-69d4b800-20241021",
"zod": "^3.23.8"
},
"eslintConfig": {

View File

@@ -15,9 +15,9 @@
"./components": "./src/components/index.ts"
},
"dependencies": {
"@stripe/react-stripe-js": "^2.8.0",
"@stripe/stripe-js": "^4.6.0",
"stripe": "^16.12.0"
"@stripe/react-stripe-js": "^2.8.1",
"@stripe/stripe-js": "^4.9.0",
"stripe": "^17.2.1"
},
"devDependencies": {
"@kit/billing": "workspace:^",
@@ -28,10 +28,10 @@
"@kit/tailwind-config": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@kit/ui": "workspace:^",
"@types/react": "^18.3.10",
"@types/react": "^18.3.11",
"date-fns": "^4.1.0",
"next": "14.2.13",
"react": "18.3.1",
"next": "15.0.0",
"react": "19.0.0-rc-69d4b800-20241021",
"zod": "^3.23.8"
},
"eslintConfig": {

View File

@@ -34,13 +34,16 @@ export async function createStripeCheckout(
const isSubscription = mode === 'subscription';
const trialSettings = params.plan.trialDays && enableTrialWithoutCreditCard ? {
trial_settings: {
end_behavior: {
missing_payment_method: 'cancel' as const,
},
},
} : {};
const trialSettings =
params.plan.trialDays && enableTrialWithoutCreditCard
? {
trial_settings: {
end_behavior: {
missing_payment_method: 'cancel' as const,
},
},
}
: {};
// this should only be set if the mode is 'subscription'
const subscriptionData:
@@ -96,11 +99,12 @@ export async function createStripeCheckout(
};
});
const paymentCollectionMethod = enableTrialWithoutCreditCard && params.plan.trialDays
? {
payment_method_collection: 'if_required' as const,
}
: {};
const paymentCollectionMethod =
enableTrialWithoutCreditCard && params.plan.trialDays
? {
payment_method_collection: 'if_required' as const,
}
: {};
return stripe.checkout.sessions.create({
mode,

View File

@@ -2,7 +2,7 @@ import 'server-only';
import { StripeServerEnvSchema } from '../schema/stripe-server-env.schema';
const STRIPE_API_VERSION = '2024-06-20';
const STRIPE_API_VERSION = '2024-09-30.acacia';
/**
* @description returns a Stripe instance

View File

@@ -38,8 +38,7 @@ export class StripeWebhookHandlerService
constructor(private readonly config: BillingConfig) {}
private readonly provider: BillingProvider =
'stripe';
private readonly provider: BillingProvider = 'stripe';
private readonly namespace = 'billing.stripe';

View File

@@ -19,7 +19,7 @@
"@kit/prettier-config": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@kit/wordpress": "workspace:^",
"@types/node": "^22.5.5"
"@types/node": "^22.7.8"
},
"eslintConfig": {
"root": true,

View File

@@ -16,7 +16,7 @@
"./route-handler": "./src/keystatic-route-handler.ts"
},
"dependencies": {
"@keystatic/core": "0.5.33",
"@keystatic/core": "0.5.38",
"@keystatic/next": "^5.0.1",
"@markdoc/markdoc": "^0.4.0"
},
@@ -26,9 +26,9 @@
"@kit/prettier-config": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@kit/ui": "workspace:^",
"@types/node": "^22.5.5",
"@types/react": "^18.3.10",
"react": "18.3.1",
"@types/node": "^22.7.8",
"@types/react": "^18.3.11",
"react": "19.0.0-rc-69d4b800-20241021",
"zod": "^3.23.8"
},
"eslintConfig": {

View File

@@ -1 +1 @@
export * from './create-keystatic-cms';
export * from './create-keystatic-cms';

View File

@@ -11,19 +11,21 @@ type ZodOutputFor<T> = z.ZodType<T, z.ZodTypeDef, unknown>;
* The previous environment variable `KEYSTATIC_STORAGE_KIND` is deprecated - as Keystatic may need this to be available in the client-side.
*
*/
const STORAGE_KIND = process.env.NEXT_PUBLIC_KEYSTATIC_STORAGE_KIND ??
/* @deprecated */
process.env.KEYSTATIC_STORAGE_KIND ??
'local';
const STORAGE_KIND =
process.env.NEXT_PUBLIC_KEYSTATIC_STORAGE_KIND ??
/* @deprecated */
process.env.KEYSTATIC_STORAGE_KIND ??
'local';
/**
* @name REPO
* @description The repository to use for the GitHub storage.
* This can be provided through the `NEXT_PUBLIC_KEYSTATIC_STORAGE_REPO` environment variable. The previous environment variable `KEYSTATIC_STORAGE_REPO` is deprecated.
*/
const REPO = process.env.NEXT_PUBLIC_KEYSTATIC_STORAGE_REPO ??
/* @deprecated */
process.env.KEYSTATIC_STORAGE_REPO;
const REPO =
process.env.NEXT_PUBLIC_KEYSTATIC_STORAGE_REPO ??
/* @deprecated */
process.env.KEYSTATIC_STORAGE_REPO;
const BRANCH_PREFIX = process.env.KEYSTATIC_STORAGE_BRANCH_PREFIX;
const PATH_PREFIX = process.env.KEYSTATIC_PATH_PREFIX;
@@ -43,9 +45,11 @@ const local = z.object({
*/
const cloud = z.object({
kind: z.literal('cloud'),
project: z.string({
description: `The Keystatic Cloud project. Please provide the value through the "KEYSTATIC_STORAGE_PROJECT" environment variable.`,
}).min(1),
project: z
.string({
description: `The Keystatic Cloud project. Please provide the value through the "KEYSTATIC_STORAGE_PROJECT" environment variable.`,
})
.min(1),
branchPrefix: z.string().optional(),
pathPrefix: z.string().optional(),
}) satisfies ZodOutputFor<CloudConfig['storage']>;
@@ -72,4 +76,4 @@ export const KeystaticStorage = z.union([local, cloud, github]).parse({
repo: REPO,
branchPrefix: BRANCH_PREFIX,
pathPrefix: PATH_PREFIX,
});
});

View File

@@ -1,2 +1,2 @@
export * from './cms-client';
export * from './cms.type';
export * from './cms.type';

View File

@@ -20,8 +20,8 @@
"@kit/prettier-config": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@kit/ui": "workspace:^",
"@types/node": "^22.5.5",
"@types/react": "^18.3.10",
"@types/node": "^22.7.8",
"@types/react": "^18.3.11",
"wp-types": "^4.66.1"
},
"eslintConfig": {

View File

@@ -1 +1 @@
export * from './wp-client';
export * from './wp-client';

View File

@@ -23,7 +23,7 @@
"@kit/tailwind-config": "workspace:*",
"@kit/team-accounts": "workspace:^",
"@kit/tsconfig": "workspace:*",
"@supabase/supabase-js": "^2.45.4",
"@supabase/supabase-js": "^2.45.6",
"zod": "^3.23.8"
},
"eslintConfig": {

View File

@@ -34,17 +34,17 @@
"@kit/tsconfig": "workspace:*",
"@kit/ui": "workspace:^",
"@radix-ui/react-icons": "^1.3.0",
"@supabase/supabase-js": "^2.45.4",
"@tanstack/react-query": "5.56.2",
"@types/react": "^18.3.10",
"@types/react-dom": "^18.3.0",
"lucide-react": "^0.446.0",
"next": "14.2.13",
"@supabase/supabase-js": "^2.45.6",
"@tanstack/react-query": "5.59.15",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.3.1",
"lucide-react": "^0.453.0",
"next": "15.0.0",
"next-themes": "0.3.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-hook-form": "^7.53.0",
"react-i18next": "^15.0.2",
"react": "19.0.0-rc-69d4b800-20241021",
"react-dom": "19.0.0-rc-69d4b800-20241021",
"react-hook-form": "^7.53.1",
"react-i18next": "^15.1.0",
"sonner": "^1.5.0",
"zod": "^3.23.8"
},

View File

@@ -40,6 +40,7 @@ interface AccountSelectorProps {
selectedAccount?: string;
collapsed?: boolean;
className?: string;
collisionPadding?: number;
onAccountChange: (value: string | undefined) => void;
}
@@ -56,6 +57,7 @@ export function AccountSelector({
enableTeamCreation: true,
},
collapsed = false,
collisionPadding = 20,
}: React.PropsWithChildren<AccountSelectorProps>) {
const [open, setOpen] = useState<boolean>(false);
const [isCreatingAccount, setIsCreatingAccount] = useState<boolean>(false);
@@ -154,7 +156,7 @@ export function AccountSelector({
<PopoverContent
data-test={'account-selector-content'}
className="w-full p-0"
collisionPadding={20}
collisionPadding={collisionPadding}
>
<Command>
<CommandInput placeholder={t('searchAccount')} className="h-9" />

View File

@@ -6,12 +6,11 @@ import { useSupabase } from '@kit/supabase/hooks/use-supabase';
export function usePersonalAccountData(
userId: string,
partialAccount?:
| {
id: string | null;
name: string | null;
picture_url: string | null;
}
partialAccount?: {
id: string | null;
name: string | null;
picture_url: string | null;
},
) {
const client = useSupabase();
const queryKey = ['account:data', userId];

View File

@@ -5,7 +5,7 @@
"scripts": {
"clean": "git clean -xdf .turbo node_modules",
"format": "prettier --check \"**/*.{ts,tsx}\"",
"lint": "eslint ",
"lint": "eslint .",
"typecheck": "tsc --noEmit"
},
"prettier": "@kit/prettier-config",
@@ -21,15 +21,15 @@
"@kit/ui": "workspace:^",
"@makerkit/data-loader-supabase-core": "^0.0.8",
"@makerkit/data-loader-supabase-nextjs": "^1.2.3",
"@supabase/supabase-js": "^2.45.4",
"@tanstack/react-query": "5.56.2",
"@supabase/supabase-js": "^2.45.6",
"@tanstack/react-query": "5.59.15",
"@tanstack/react-table": "^8.20.5",
"@types/react": "^18.3.10",
"lucide-react": "^0.446.0",
"next": "14.2.13",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-hook-form": "^7.53.0",
"@types/react": "^18.3.11",
"lucide-react": "^0.453.0",
"next": "15.0.0",
"react": "19.0.0-rc-69d4b800-20241021",
"react-dom": "19.0.0-rc-69d4b800-20241021",
"react-hook-form": "^7.53.1",
"zod": "^3.23.8"
},
"exports": {

View File

@@ -21,7 +21,6 @@ import {
} from '@kit/ui/dropdown-menu';
import { DataTable } from '@kit/ui/enhanced-data-table';
import { Form, FormControl, FormField, FormItem } from '@kit/ui/form';
import { Heading } from '@kit/ui/heading';
import { If } from '@kit/ui/if';
import { Input } from '@kit/ui/input';
import {

View File

@@ -28,13 +28,13 @@
"@kit/ui": "workspace:^",
"@marsidev/react-turnstile": "^1.0.2",
"@radix-ui/react-icons": "^1.3.0",
"@supabase/supabase-js": "^2.45.4",
"@tanstack/react-query": "5.56.2",
"@types/react": "^18.3.10",
"lucide-react": "^0.446.0",
"next": "14.2.13",
"react-hook-form": "^7.53.0",
"react-i18next": "^15.0.2",
"@supabase/supabase-js": "^2.45.6",
"@tanstack/react-query": "5.59.15",
"@types/react": "^18.3.11",
"lucide-react": "^0.453.0",
"next": "15.0.0",
"react-hook-form": "^7.53.1",
"react-i18next": "^15.1.0",
"sonner": "^1.5.0",
"zod": "^3.23.8"
},

View File

@@ -20,13 +20,13 @@
"@kit/tailwind-config": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@kit/ui": "workspace:*",
"@supabase/supabase-js": "^2.45.4",
"@tanstack/react-query": "5.56.2",
"@types/react": "^18.3.10",
"lucide-react": "^0.446.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-i18next": "^15.0.2"
"@supabase/supabase-js": "^2.45.6",
"@tanstack/react-query": "5.59.15",
"@types/react": "^18.3.11",
"lucide-react": "^0.453.0",
"react": "19.0.0-rc-69d4b800-20241021",
"react-dom": "19.0.0-rc-69d4b800-20241021",
"react-i18next": "^15.1.0"
},
"prettier": "@kit/prettier-config",
"eslintConfig": {

View File

@@ -32,19 +32,19 @@
"@kit/tailwind-config": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@kit/ui": "workspace:^",
"@supabase/supabase-js": "^2.45.4",
"@tanstack/react-query": "5.56.2",
"@supabase/supabase-js": "^2.45.6",
"@tanstack/react-query": "5.59.15",
"@tanstack/react-table": "^8.20.5",
"@types/react": "^18.3.10",
"@types/react-dom": "^18.3.0",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.3.1",
"class-variance-authority": "^0.7.0",
"date-fns": "^4.1.0",
"lucide-react": "^0.446.0",
"next": "14.2.13",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-hook-form": "^7.53.0",
"react-i18next": "^15.0.2",
"lucide-react": "^0.453.0",
"next": "15.0.0",
"react": "19.0.0-rc-69d4b800-20241021",
"react-dom": "19.0.0-rc-69d4b800-20241021",
"react-hook-form": "^7.53.1",
"react-i18next": "^15.1.0",
"sonner": "^1.5.0",
"zod": "^3.23.8"
},

View File

@@ -21,13 +21,16 @@
"@kit/shared": "workspace:^",
"@kit/tailwind-config": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@tanstack/react-query": "5.56.2",
"react-i18next": "^15.0.2"
"@tanstack/react-query": "5.59.15",
"react-i18next": "^15.1.0"
},
"dependencies": {
"i18next": "^23.15.2",
"i18next": "^23.16.2",
"i18next-browser-languagedetector": "8.0.0",
"i18next-resources-to-backend": "^1.2.1"
"i18next-resources-to-backend": "^1.2.1",
"next": "15.0.0",
"react": "19.0.0-rc-69d4b800-20241021",
"react-dom": "19.0.0-rc-69d4b800-20241021"
},
"eslintConfig": {
"root": true,

View File

@@ -19,7 +19,7 @@
"@kit/resend": "workspace:^",
"@kit/tailwind-config": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@types/node": "^22.5.5",
"@types/node": "^22.7.8",
"zod": "^3.23.8"
},
"eslintConfig": {

View File

@@ -34,4 +34,4 @@ async function getNodemailer() {
'Nodemailer is not available on the edge runtime. Please use another mailer.',
);
}
}
}

View File

@@ -18,7 +18,7 @@
"@kit/prettier-config": "workspace:*",
"@kit/tailwind-config": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@types/node": "^22.5.5",
"@types/node": "^22.7.8",
"zod": "^3.23.8"
},
"eslintConfig": {

View File

@@ -1,3 +1,3 @@
export * from './schema/mailer.schema';
export * from './schema/smtp-config.schema';
export * from './mailer';
export * from './mailer';

View File

@@ -24,8 +24,8 @@
"@kit/sentry": "workspace:*",
"@kit/tailwind-config": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@types/react": "^18.3.10",
"react": "18.3.1"
"@types/react": "^18.3.11",
"react": "19.0.0-rc-69d4b800-20241021"
},
"eslintConfig": {
"root": true,

View File

@@ -25,8 +25,8 @@
"@kit/prettier-config": "workspace:*",
"@kit/tailwind-config": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@types/react": "^18.3.10",
"react": "18.3.1",
"@types/react": "^18.3.11",
"react": "19.0.0-rc-69d4b800-20241021",
"zod": "^3.23.8"
},
"eslintConfig": {

View File

@@ -18,8 +18,8 @@
"@kit/prettier-config": "workspace:*",
"@kit/tailwind-config": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@types/react": "^18.3.10",
"react": "18.3.1"
"@types/react": "^18.3.11",
"react": "19.0.0-rc-69d4b800-20241021"
},
"eslintConfig": {
"root": true,

View File

@@ -16,7 +16,7 @@
"./config/server": "./src/sentry.client.server.ts"
},
"dependencies": {
"@sentry/nextjs": "^8.32.0"
"@sentry/nextjs": "^8.35.0"
},
"devDependencies": {
"@kit/eslint-config": "workspace:*",
@@ -24,8 +24,8 @@
"@kit/prettier-config": "workspace:*",
"@kit/tailwind-config": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@types/react": "^18.3.10",
"react": "18.3.1"
"@types/react": "^18.3.11",
"react": "19.0.0-rc-69d4b800-20241021"
},
"eslintConfig": {
"root": true,

View File

@@ -21,8 +21,8 @@
"@kit/supabase": "workspace:^",
"@kit/tailwind-config": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@supabase/supabase-js": "^2.45.4",
"next": "14.2.13",
"@supabase/supabase-js": "^2.45.6",
"next": "15.0.0",
"zod": "^3.23.8"
},
"eslintConfig": {

View File

@@ -1,6 +1,5 @@
import 'server-only';
import { isRedirectError } from 'next/dist/client/components/redirect';
import { redirect } from 'next/navigation';
import type { User } from '@supabase/supabase-js';
@@ -11,7 +10,7 @@ import { verifyCaptchaToken } from '@kit/auth/captcha/server';
import { requireUser } from '@kit/supabase/require-user';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { captureException, zodParseFactory } from '../utils';
import { zodParseFactory } from '../utils';
/**
* @name enhanceAction
@@ -23,7 +22,6 @@ export function enhanceAction<
Config extends {
auth?: boolean;
captcha?: boolean;
captureException?: boolean;
schema?: z.ZodType<
Config['captcha'] extends true ? Args & { captchaToken: string } : Args,
z.ZodTypeDef
@@ -73,28 +71,6 @@ export function enhanceAction<
user = auth.data as UserParam;
}
// capture exceptions if required
const shouldCaptureException = config.captureException ?? true;
// if the action should capture exceptions, wrap the action in a try/catch block
if (shouldCaptureException) {
try {
// pass the data to the action
return await fn(data, user);
} catch (error) {
if (isRedirectError(error)) {
throw error;
}
// capture the exception
await captureException(error);
// re-throw the error
throw error;
}
} else {
// no need to capture exceptions, just pass the data to the action
return fn(data, user);
}
return fn(data, user);
};
}

View File

@@ -1,6 +1,5 @@
import 'server-only';
import { isRedirectError } from 'next/dist/client/components/redirect';
import { redirect } from 'next/navigation';
import { NextRequest, NextResponse } from 'next/server';
@@ -12,12 +11,11 @@ import { verifyCaptchaToken } from '@kit/auth/captcha/server';
import { requireUser } from '@kit/supabase/require-user';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { captureException, zodParseFactory } from '../utils';
import { zodParseFactory } from '../utils';
interface Config<Schema> {
auth?: boolean;
captcha?: boolean;
captureException?: boolean;
schema?: Schema;
}
@@ -124,35 +122,12 @@ export const enhanceRouteHandler = <
body = zodParseFactory(params.schema)(json);
}
const shouldCaptureException = params?.captureException ?? true;
if (shouldCaptureException) {
try {
return await handler({
request,
body,
user,
params: routeParams.params,
});
} catch (error) {
if (isRedirectError(error)) {
throw error;
}
// capture the exception
await captureException(error);
throw error;
}
} else {
// all good, call the handler with the request, body and user
return handler({
request,
body,
user,
params: routeParams.params,
});
}
return handler({
request,
body,
user,
params: routeParams.params,
});
};
};

View File

@@ -12,16 +12,3 @@ export const zodParseFactory =
throw new Error(`Invalid data: ${err as string}`);
}
};
export async function captureException(exception: unknown) {
const { getServerMonitoringService } = await import('@kit/monitoring/server');
const service = await getServerMonitoringService();
await service.ready();
const error =
exception instanceof Error ? exception : new Error(exception as string);
return service.captureException(error);
}

View File

@@ -20,10 +20,10 @@
"@kit/prettier-config": "workspace:*",
"@kit/tailwind-config": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@types/react": "^18.3.10"
"@types/react": "^18.3.11"
},
"dependencies": {
"pino": "^9.4.0"
"pino": "^9.5.0"
},
"eslintConfig": {
"root": true,

View File

@@ -29,11 +29,11 @@
"@kit/tailwind-config": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@supabase/ssr": "^0.5.1",
"@supabase/supabase-js": "^2.45.4",
"@tanstack/react-query": "5.56.2",
"@types/react": "^18.3.10",
"next": "14.2.13",
"react": "18.3.1",
"@supabase/supabase-js": "^2.45.6",
"@tanstack/react-query": "5.59.15",
"@types/react": "^18.3.11",
"next": "15.0.0",
"react": "19.0.0-rc-69d4b800-20241021",
"server-only": "^0.0.1",
"zod": "^3.23.8"
},

View File

@@ -48,7 +48,8 @@ class AuthCallbackService {
const token_hash = searchParams.get('token_hash');
const type = searchParams.get('type') as EmailOtpType | null;
const callbackParam = searchParams.get('next') ?? searchParams.get('callback');
const callbackParam =
searchParams.get('next') ?? searchParams.get('callback');
let nextPath: string | null = null;
const callbackUrl = callbackParam ? new URL(callbackParam) : null;

View File

@@ -1,6 +1,5 @@
import 'server-only';
import { unstable_noStore as noStore } from 'next/cache';
import { cookies } from 'next/headers';
import { createClient } from '@supabase/supabase-js';
@@ -28,9 +27,6 @@ export function getSupabaseRouteHandlerClient<GenericSchema = Database>(
admin: false,
},
) {
// prevent any caching (to be removed in Next v15)
noStore();
if (params.admin) {
warnServiceRoleKeyUsage();
@@ -49,16 +45,20 @@ export function getSupabaseRouteHandlerClient<GenericSchema = Database>(
}
function getCookiesStrategy() {
const cookieStore = cookies();
return {
set: (name: string, value: string, options: CookieOptions) => {
set: async (name: string, value: string, options: CookieOptions) => {
const cookieStore = await cookies();
cookieStore.set({ name, value, ...options });
},
get: (name: string) => {
get: async (name: string) => {
const cookieStore = await cookies();
return cookieStore.get(name)?.value;
},
remove: (name: string, options: CookieOptions) => {
remove: async (name: string, options: CookieOptions) => {
const cookieStore = await cookies();
cookieStore.set({ name, value: '', ...options });
},
};

View File

@@ -1,6 +1,5 @@
import 'server-only';
import { unstable_noStore as noStore } from 'next/cache';
import { cookies } from 'next/headers';
import { createClient } from '@supabase/supabase-js';
@@ -33,9 +32,6 @@ function createServerSupabaseClient<
export function getSupabaseServerActionClient<
GenericSchema extends Database = Database,
>(params?: { admin: boolean }) {
// prevent any caching (to be removed in Next v15)
noStore();
const keys = getSupabaseClientKeys();
const admin = params?.admin ?? false;
@@ -55,16 +51,21 @@ export function getSupabaseServerActionClient<
}
function getCookiesStrategy() {
const cookieStore = cookies();
return {
get: (name: string) => {
return cookieStore.get(name)?.value;
get: async (name: string) => {
const cookieStore = await cookies();
const cookie = cookieStore.get(name);
return cookie?.value;
},
set: (name: string, value: string, options: object) => {
set: async (name: string, value: string, options: object) => {
const cookieStore = await cookies();
cookieStore.set({ name, value, ...options });
},
remove: (name: string, options: object) => {
remove: async (name: string, options: object) => {
const cookieStore = await cookies();
cookieStore.set({
name,
value: '',

View File

@@ -1,7 +1,5 @@
import 'server-only';
import { unstable_noStore as noStore } from 'next/cache';
import { createClient } from '@supabase/supabase-js';
import { Database } from '../database.types';
@@ -16,7 +14,6 @@ import { getSupabaseClientKeys } from '../get-supabase-client-keys';
* @description Get a Supabase client for use in the Server with admin access to the database.
*/
export function getSupabaseServerAdminClient<GenericSchema = Database>() {
noStore();
warnServiceRoleKeyUsage();
const url = getSupabaseClientKeys().url;

View File

@@ -1,6 +1,5 @@
import 'server-only';
import { unstable_noStore as noStore } from 'next/cache';
import { cookies } from 'next/headers';
import { createServerClient } from '@supabase/ssr';
@@ -13,17 +12,18 @@ import { getSupabaseClientKeys } from '../get-supabase-client-keys';
* @description Creates a Supabase client for use in the Server.
*/
export function getSupabaseServerClient<GenericSchema = Database>() {
noStore();
const cookieStore = cookies();
const keys = getSupabaseClientKeys();
return createServerClient<GenericSchema>(keys.url, keys.anonKey, {
cookies: {
getAll() {
async getAll() {
const cookieStore = await cookies();
return cookieStore.getAll();
},
setAll(cookiesToSet) {
async setAll(cookiesToSet) {
const cookieStore = await cookies();
try {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options),

View File

@@ -1,6 +1,5 @@
import 'server-only';
import { unstable_noStore as noStore } from 'next/cache';
import { cookies } from 'next/headers';
import { createClient } from '@supabase/supabase-js';
@@ -26,9 +25,6 @@ export function getSupabaseServerComponentClient<GenericSchema = Database>(
admin: false,
},
) {
// prevent any caching (to be removed in Next v15)
noStore();
if (params.admin) {
warnServiceRoleKeyUsage();
@@ -47,10 +43,10 @@ export function getSupabaseServerComponentClient<GenericSchema = Database>(
}
function getCookiesStrategy() {
const cookieStore = cookies();
return {
get: (name: string) => {
get: async (name: string) => {
const cookieStore = await cookies();
return cookieStore.get(name)?.value;
},
};

View File

@@ -10,31 +10,31 @@
},
"dependencies": {
"@hookform/resolvers": "^3.9.0",
"@radix-ui/react-accordion": "1.2.0",
"@radix-ui/react-alert-dialog": "^1.1.1",
"@radix-ui/react-avatar": "^1.1.0",
"@radix-ui/react-checkbox": "^1.1.1",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-accordion": "1.2.1",
"@radix-ui/react-alert-dialog": "^1.1.2",
"@radix-ui/react-avatar": "^1.1.1",
"@radix-ui/react-checkbox": "^1.1.2",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-dropdown-menu": "^2.1.2",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-navigation-menu": "^1.2.0",
"@radix-ui/react-popover": "^1.1.1",
"@radix-ui/react-radio-group": "^1.2.0",
"@radix-ui/react-scroll-area": "^1.1.0",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-navigation-menu": "^1.2.1",
"@radix-ui/react-popover": "^1.1.2",
"@radix-ui/react-radio-group": "^1.2.1",
"@radix-ui/react-scroll-area": "^1.2.0",
"@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.0",
"@radix-ui/react-toast": "^1.2.1",
"@radix-ui/react-tooltip": "1.1.2",
"@radix-ui/react-switch": "^1.1.1",
"@radix-ui/react-tabs": "^1.1.1",
"@radix-ui/react-toast": "^1.2.2",
"@radix-ui/react-tooltip": "1.1.3",
"clsx": "^2.1.1",
"cmdk": "1.0.0",
"input-otp": "1.2.4",
"lucide-react": "^0.446.0",
"lucide-react": "^0.453.0",
"react-top-loading-bar": "2.3.1",
"recharts": "^2.12.7",
"tailwind-merge": "^2.5.2"
"recharts": "2.13.0",
"tailwind-merge": "^2.5.4"
},
"devDependencies": {
"@kit/eslint-config": "workspace:*",
@@ -42,23 +42,23 @@
"@kit/tailwind-config": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@radix-ui/react-icons": "^1.3.0",
"@tanstack/react-query": "5.56.2",
"@tanstack/react-query": "5.59.15",
"@tanstack/react-table": "^8.20.5",
"@types/react": "^18.3.10",
"@types/react-dom": "^18.3.0",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.3.1",
"class-variance-authority": "^0.7.0",
"date-fns": "^4.1.0",
"eslint": "^8.57.0",
"next": "14.2.13",
"next": "15.0.0",
"next-themes": "0.3.0",
"prettier": "^3.3.3",
"react-day-picker": "^8.10.1",
"react-hook-form": "^7.53.0",
"react-i18next": "^15.0.2",
"react-hook-form": "^7.53.1",
"react-i18next": "^15.1.0",
"sonner": "^1.5.0",
"tailwindcss": "3.4.13",
"tailwindcss": "3.4.14",
"tailwindcss-animate": "^1.0.7",
"typescript": "^5.6.2",
"typescript": "^5.6.3",
"zod": "^3.23.8"
},
"eslintConfig": {

Some files were not shown because too many files have changed in this diff Show More