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:
committed by
GitHub
parent
93cb011260
commit
5b9285a575
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -57,7 +57,7 @@ export function ContactForm() {
|
||||
await sendContactEmail(data);
|
||||
|
||||
setState({ success: true, error: false });
|
||||
} catch (error) {
|
||||
} catch {
|
||||
setState({ error: true, success: false });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -175,7 +175,6 @@ function Home() {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default withI18n(Home);
|
||||
|
||||
function MainCallToActionButton() {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 +
|
||||
|
||||
@@ -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 +
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -71,6 +71,7 @@ export function HomeMobileNavigation(props: { workspace: UserWorkspace }) {
|
||||
<HomeAccountSelector
|
||||
userId={props.workspace.user.id}
|
||||
accounts={props.workspace.accounts}
|
||||
collisionPadding={0}
|
||||
/>
|
||||
</DropdownMenuGroup>
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ export function PersonalAccountCheckoutForm(props: {
|
||||
});
|
||||
|
||||
setCheckoutToken(checkoutToken);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
setError(true);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
),
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
);
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 />}
|
||||
/>
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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 />}
|
||||
/>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
12
apps/web/lib/dev-mock-modules.ts
Normal file
12
apps/web/lib/dev-mock-modules.ts
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
20
package.json
20
package.json
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -1 +1 @@
|
||||
export * from './create-keystatic-cms';
|
||||
export * from './create-keystatic-cms';
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from './cms-client';
|
||||
export * from './cms.type';
|
||||
export * from './cms.type';
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -1 +1 @@
|
||||
export * from './wp-client';
|
||||
export * from './wp-client';
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -34,4 +34,4 @@ async function getNodemailer() {
|
||||
'Nodemailer is not available on the edge runtime. Please use another mailer.',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export * from './schema/mailer.schema';
|
||||
export * from './schema/smtp-config.schema';
|
||||
export * from './mailer';
|
||||
export * from './mailer';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 });
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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: '',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user