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": {
|
||||
|
||||
@@ -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']>;
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -34,7 +34,7 @@ export const ImageUploadInput = forwardRef<React.ElementRef<'input'>, Props>(
|
||||
},
|
||||
forwardedRef,
|
||||
) {
|
||||
const localRef = useRef<HTMLInputElement>();
|
||||
const localRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const [state, setState] = useState({
|
||||
image,
|
||||
|
||||
@@ -30,21 +30,24 @@ function PageWithSidebar(props: PageProps) {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn('flex bg-gray-50/95 dark:bg-background/85', props.className)}
|
||||
className={cn(
|
||||
'flex bg-gray-50/95 dark:bg-background/85',
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
{Navigation}
|
||||
|
||||
<div
|
||||
className={
|
||||
props.contentContainerClassName ??
|
||||
'mx-auto flex h-screen w-full flex-col overflow-y-auto px-4 lg:px-0 bg-inherit'
|
||||
'mx-auto flex h-screen w-full flex-col overflow-y-auto bg-inherit'
|
||||
}
|
||||
>
|
||||
{MobileNavigation}
|
||||
|
||||
<div
|
||||
className={
|
||||
'flex flex-1 flex-col overflow-y-auto bg-background lg:m-1.5 lg:ml-0 lg:rounded-lg lg:border'
|
||||
'flex flex-1 flex-col overflow-y-auto bg-background px-4 lg:m-1.5 lg:ml-0 lg:rounded-lg lg:border lg:px-0'
|
||||
}
|
||||
>
|
||||
{Children}
|
||||
@@ -62,7 +65,7 @@ export function PageMobileNavigation(
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex w-full items-center border-b py-2 lg:hidden',
|
||||
'flex w-full items-center border-b px-4 py-2 lg:hidden lg:px-0',
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
@@ -115,7 +118,9 @@ export function PageBody(
|
||||
}
|
||||
|
||||
export function PageNavigation(props: React.PropsWithChildren) {
|
||||
return <div className={'hidden flex-1 lg:flex bg-inherit'}>{props.children}</div>;
|
||||
return (
|
||||
<div className={'hidden flex-1 bg-inherit lg:flex'}>{props.children}</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function PageDescription(props: React.PropsWithChildren) {
|
||||
|
||||
@@ -17,7 +17,7 @@ type ProfileAvatarProps = (SessionProps | TextProps) & {
|
||||
export function ProfileAvatar(props: ProfileAvatarProps) {
|
||||
const avatarClassName = cn(
|
||||
props.className,
|
||||
'mx-auto w-9 h-9 group-focus:ring-2',
|
||||
'mx-auto h-9 w-9 group-focus:ring-2',
|
||||
);
|
||||
|
||||
if ('text' in props) {
|
||||
|
||||
@@ -42,7 +42,7 @@ export function Stepper(props: {
|
||||
const isDotsVariant = variant === 'dots';
|
||||
|
||||
const labelClassName = cn({
|
||||
['text-xs px-1.5 py-2']: !isNumberVariant,
|
||||
['px-1.5 py-2 text-xs']: !isNumberVariant,
|
||||
['hidden']: isDotsVariant,
|
||||
});
|
||||
|
||||
@@ -182,15 +182,15 @@ function StepDivider({
|
||||
selected: boolean;
|
||||
complete: boolean;
|
||||
}>) {
|
||||
const spanClassName = cn('font-medium text-sm min-w-max', {
|
||||
['text-muted-foreground hidden sm:flex']: !selected,
|
||||
const spanClassName = cn('min-w-max text-sm font-medium', {
|
||||
['hidden text-muted-foreground sm:flex']: !selected,
|
||||
['text-secondary-foreground']: selected || complete,
|
||||
['font-medium']: selected,
|
||||
});
|
||||
|
||||
const className = cn(
|
||||
'flex flex-1 last:flex-[0_0_0] items-center h-9 justify-center' +
|
||||
' items-center w-full group px-3 flex space-x-3',
|
||||
'flex h-9 flex-1 items-center justify-center last:flex-[0_0_0]' +
|
||||
' group flex w-full items-center space-x-3 px-3',
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
7776
pnpm-lock.yaml
generated
7776
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user