Next.js Supabase V3 (#463)

Version 3 of the kit:
- Radix UI replaced with Base UI (using the Shadcn UI patterns)
- next-intl replaces react-i18next
- enhanceAction deprecated; usage moved to next-safe-action
- main layout now wrapped with [locale] path segment
- Teams only mode
- Layout updates
- Zod v4
- Next.js 16.2
- Typescript 6
- All other dependencies updated
- Removed deprecated Edge CSRF
- Dynamic Github Action runner
This commit is contained in:
Giancarlo Buomprisco
2026-03-24 13:40:38 +08:00
committed by GitHub
parent 4912e402a3
commit 7ebff31475
840 changed files with 71395 additions and 20095 deletions

View File

@@ -1,30 +0,0 @@
import { SitePageHeader } from '~/(marketing)/_components/site-page-header';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
export async function generateMetadata() {
const { t } = await createI18nServerInstance();
return {
title: t('marketing:cookiePolicy'),
};
}
async function CookiePolicyPage() {
const { t } = await createI18nServerInstance();
return (
<div>
<SitePageHeader
title={t(`marketing:cookiePolicy`)}
subtitle={t(`marketing:cookiePolicyDescription`)}
/>
<div className={'container mx-auto py-8'}>
<div>Your terms of service content here</div>
</div>
</div>
);
}
export default withI18n(CookiePolicyPage);

View File

@@ -1,30 +0,0 @@
import { SitePageHeader } from '~/(marketing)/_components/site-page-header';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
export async function generateMetadata() {
const { t } = await createI18nServerInstance();
return {
title: t('marketing:privacyPolicy'),
};
}
async function PrivacyPolicyPage() {
const { t } = await createI18nServerInstance();
return (
<div>
<SitePageHeader
title={t('marketing:privacyPolicy')}
subtitle={t('marketing:privacyPolicyDescription')}
/>
<div className={'container mx-auto py-8'}>
<div>Your terms of service content here</div>
</div>
</div>
);
}
export default withI18n(PrivacyPolicyPage);

View File

@@ -1,30 +0,0 @@
import { SitePageHeader } from '~/(marketing)/_components/site-page-header';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
export async function generateMetadata() {
const { t } = await createI18nServerInstance();
return {
title: t('marketing:termsOfService'),
};
}
async function TermsOfServicePage() {
const { t } = await createI18nServerInstance();
return (
<div>
<SitePageHeader
title={t(`marketing:termsOfService`)}
subtitle={t(`marketing:termsOfServiceDescription`)}
/>
<div className={'container mx-auto py-8'}>
<div>Your terms of service content here</div>
</div>
</div>
);
}
export default withI18n(TermsOfServicePage);

View File

@@ -1,72 +0,0 @@
'use client';
import { useEffect, useEffectEvent, useMemo, useState } from 'react';
import { usePathname } from 'next/navigation';
import { Menu } from 'lucide-react';
import { isBrowser } from '@kit/shared/utils';
import { Button } from '@kit/ui/button';
import { If } from '@kit/ui/if';
export function FloatingDocumentationNavigation(
props: React.PropsWithChildren,
) {
const activePath = usePathname();
const body = useMemo(() => {
return isBrowser() ? document.body : null;
}, []);
const [isVisible, setIsVisible] = useState(false);
const enableScrolling = useEffectEvent(
() => body && (body.style.overflowY = ''),
);
const disableScrolling = useEffectEvent(
() => body && (body.style.overflowY = 'hidden'),
);
// enable/disable body scrolling when the docs are toggled
useEffect(() => {
if (isVisible) {
disableScrolling();
} else {
enableScrolling();
}
}, [isVisible]);
// hide docs when navigating to another page
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
setIsVisible(false);
}, [activePath]);
const onClick = () => {
setIsVisible(!isVisible);
};
return (
<>
<If condition={isVisible}>
<div
className={
'fixed top-0 left-0 z-10 h-screen w-full p-4' +
' dark:bg-background flex flex-col space-y-4 overflow-auto bg-white'
}
>
{props.children}
</div>
</If>
<Button
className={'fixed right-5 bottom-5 z-10 h-16 w-16 rounded-full'}
onClick={onClick}
>
<Menu className={'h-8'} />
</Button>
</>
);
}

View File

@@ -1,45 +0,0 @@
import { SidebarProvider } from '@kit/ui/shadcn-sidebar';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
// local imports
import { DocsNavigation } from './_components/docs-navigation';
import { getDocs } from './_lib/server/docs.loader';
import { buildDocumentationTree } from './_lib/utils';
async function DocsLayout({ children }: React.PropsWithChildren) {
const { resolvedLanguage } = await createI18nServerInstance();
const docs = await getDocs(resolvedLanguage);
const tree = buildDocumentationTree(docs);
return (
<div className={'container h-[calc(100vh-56px)] overflow-y-hidden'}>
<SidebarProvider
className="lg:gap-x-6"
style={{ '--sidebar-width': '17em' } as React.CSSProperties}
>
<HideFooterStyles />
<DocsNavigation pages={tree} />
{children}
</SidebarProvider>
</div>
);
}
function HideFooterStyles() {
return (
<style
dangerouslySetInnerHTML={{
__html: `
.site-footer {
display: none;
}
`,
}}
/>
);
}
export default DocsLayout;

View File

@@ -1,37 +0,0 @@
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
import { SitePageHeader } from '../_components/site-page-header';
import { DocsCards } from './_components/docs-cards';
import { getDocs } from './_lib/server/docs.loader';
export const generateMetadata = async () => {
const { t } = await createI18nServerInstance();
return {
title: t('marketing:documentation'),
};
};
async function DocsPage() {
const { t, resolvedLanguage } = await createI18nServerInstance();
const items = await getDocs(resolvedLanguage);
// Filter out any docs that have a parentId, as these are children of other docs
const cards = items.filter((item) => !item.parentId);
return (
<div className={'flex w-full flex-1 flex-col gap-y-6 xl:gap-y-8'}>
<SitePageHeader
title={t('marketing:documentation')}
subtitle={t('marketing:documentationSubtitle')}
/>
<div className={'relative flex size-full justify-center overflow-y-auto'}>
<DocsCards cards={cards} />
</div>
</div>
);
}
export default withI18n(DocsPage);

View File

@@ -0,0 +1,30 @@
import { getTranslations } from 'next-intl/server';
import { SitePageHeader } from '~/(marketing)/_components/site-page-header';
export async function generateMetadata() {
const t = await getTranslations('marketing');
return {
title: t('cookiePolicy'),
};
}
async function CookiePolicyPage() {
const t = await getTranslations('marketing');
return (
<div>
<SitePageHeader
title={t(`marketing.cookiePolicy`)}
subtitle={t(`marketing.cookiePolicyDescription`)}
/>
<div className={'container mx-auto py-8'}>
<div>Your terms of service content here</div>
</div>
</div>
);
}
export default CookiePolicyPage;

View File

@@ -0,0 +1,30 @@
import { getTranslations } from 'next-intl/server';
import { SitePageHeader } from '~/(marketing)/_components/site-page-header';
export async function generateMetadata() {
const t = await getTranslations('marketing');
return {
title: t('privacyPolicy'),
};
}
async function PrivacyPolicyPage() {
const t = await getTranslations('marketing');
return (
<div>
<SitePageHeader
title={t('privacyPolicy')}
subtitle={t('privacyPolicyDescription')}
/>
<div className={'container mx-auto py-8'}>
<div>Your terms of service content here</div>
</div>
</div>
);
}
export default PrivacyPolicyPage;

View File

@@ -0,0 +1,30 @@
import { getTranslations } from 'next-intl/server';
import { SitePageHeader } from '~/(marketing)/_components/site-page-header';
export async function generateMetadata() {
const t = await getTranslations('marketing');
return {
title: t('termsOfService'),
};
}
async function TermsOfServicePage() {
const t = await getTranslations('marketing');
return (
<div>
<SitePageHeader
title={t(`marketing.termsOfService`)}
subtitle={t(`marketing.termsOfServiceDescription`)}
/>
<div className={'container mx-auto py-8'}>
<div>Your terms of service content here</div>
</div>
</div>
);
}
export default TermsOfServicePage;

View File

@@ -8,10 +8,10 @@ export function SiteFooter() {
return (
<Footer
logo={<AppLogo className="w-[85px] md:w-[95px]" />}
description={<Trans i18nKey="marketing:footerDescription" />}
description={<Trans i18nKey="marketing.footerDescription" />}
copyright={
<Trans
i18nKey="marketing:copyright"
i18nKey="marketing.copyright"
values={{
product: appConfig.name,
year: new Date().getFullYear(),
@@ -20,35 +20,35 @@ export function SiteFooter() {
}
sections={[
{
heading: <Trans i18nKey="marketing:about" />,
heading: <Trans i18nKey="marketing.about" />,
links: [
{ href: '/blog', label: <Trans i18nKey="marketing:blog" /> },
{ href: '/contact', label: <Trans i18nKey="marketing:contact" /> },
{ href: '/blog', label: <Trans i18nKey="marketing.blog" /> },
{ href: '/contact', label: <Trans i18nKey="marketing.contact" /> },
],
},
{
heading: <Trans i18nKey="marketing:product" />,
heading: <Trans i18nKey="marketing.product" />,
links: [
{
href: '/docs',
label: <Trans i18nKey="marketing:documentation" />,
label: <Trans i18nKey="marketing.documentation" />,
},
],
},
{
heading: <Trans i18nKey="marketing:legal" />,
heading: <Trans i18nKey="marketing.legal" />,
links: [
{
href: '/terms-of-service',
label: <Trans i18nKey="marketing:termsOfService" />,
label: <Trans i18nKey="marketing.termsOfService" />,
},
{
href: '/privacy-policy',
label: <Trans i18nKey="marketing:privacyPolicy" />,
label: <Trans i18nKey="marketing.privacyPolicy" />,
},
{
href: '/cookie-policy',
label: <Trans i18nKey="marketing:cookiePolicy" />,
label: <Trans i18nKey="marketing.cookiePolicy" />,
},
],
},

View File

@@ -31,6 +31,7 @@ const MobileModeToggle = dynamic(
const paths = {
home: pathsConfig.app.home,
profileSettings: pathsConfig.app.personalAccountSettings,
};
const features = {
@@ -78,26 +79,28 @@ function AuthButtons() {
<div className={'flex items-center gap-x-2'}>
<Button
nativeButton={false}
className={'hidden md:flex md:text-sm'}
asChild
render={
<Link href={pathsConfig.auth.signIn}>
<Trans i18nKey={'auth.signIn'} />
</Link>
}
variant={'outline'}
size={'sm'}
>
<Link href={pathsConfig.auth.signIn}>
<Trans i18nKey={'auth:signIn'} />
</Link>
</Button>
/>
<Button
asChild
nativeButton={false}
render={
<Link href={pathsConfig.auth.signUp}>
<Trans i18nKey={'auth.signUp'} />
</Link>
}
className="text-xs md:text-sm"
variant={'default'}
size={'sm'}
>
<Link href={pathsConfig.auth.signUp}>
<Trans i18nKey={'auth:signUp'} />
</Link>
</Button>
/>
</div>
</div>
);

View File

@@ -9,7 +9,7 @@ import { SiteNavigation } from './site-navigation';
export function SiteHeader(props: { user?: JWTUserData | null }) {
return (
<Header
logo={<AppLogo />}
logo={<AppLogo className="mx-auto sm:mx-0" href="/" />}
navigation={<SiteNavigation />}
actions={<SiteHeaderAccountSection user={props.user ?? null} />}
/>

View File

@@ -15,23 +15,23 @@ import { SiteNavigationItem } from './site-navigation-item';
const links = {
Blog: {
label: 'marketing:blog',
label: 'marketing.blog',
path: '/blog',
},
Changelog: {
label: 'marketing:changelog',
label: 'marketing.changelog',
path: '/changelog',
},
Docs: {
label: 'marketing:documentation',
label: 'marketing.documentation',
path: '/docs',
},
Pricing: {
label: 'marketing:pricing',
label: 'marketing.pricing',
path: '/pricing',
},
FAQ: {
label: 'marketing:faq',
label: 'marketing.faq',
path: '/faq',
},
};
@@ -74,11 +74,14 @@ function MobileDropdown() {
const className = 'flex w-full h-full items-center';
return (
<DropdownMenuItem key={item.path} asChild>
<Link className={className} href={item.path}>
<Trans i18nKey={item.label} />
</Link>
</DropdownMenuItem>
<DropdownMenuItem
key={item.path}
render={
<Link className={className} href={item.path}>
<Trans i18nKey={item.label} />
</Link>
}
/>
);
})}
</DropdownMenuContent>

View File

@@ -1,13 +1,10 @@
import { cache } from 'react';
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { createCmsClient } from '@kit/cms';
import { withI18n } from '~/lib/i18n/with-i18n';
import { Post } from '../../blog/_components/post';
interface BlogPageProps {
@@ -75,4 +72,4 @@ async function BlogPost({ params }: BlogPageProps) {
);
}
export default withI18n(BlogPost);
export default BlogPost;

View File

@@ -25,7 +25,7 @@ export function BlogPagination(props: {
}}
>
<ArrowLeft className={'mr-2 h-4'} />
<Trans i18nKey={'marketing:blogPaginationPrevious'} />
<Trans i18nKey={'marketing.blogPaginationPrevious'} />
</Button>
</If>
@@ -36,7 +36,7 @@ export function BlogPagination(props: {
navigate(props.currentPage + 1);
}}
>
<Trans i18nKey={'marketing:blogPaginationNext'} />
<Trans i18nKey={'marketing.blogPaginationNext'} />
<ArrowRight className={'ml-2 h-4'} />
</Button>
</If>

View File

@@ -2,14 +2,13 @@ import { cache } from 'react';
import type { Metadata } from 'next';
import { getLocale, getTranslations } from 'next-intl/server';
import { createCmsClient } from '@kit/cms';
import { getLogger } from '@kit/shared/logger';
import { If } from '@kit/ui/if';
import { Trans } from '@kit/ui/trans';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
// local imports
import { SitePageHeader } from '../_components/site-page-header';
import { BlogPagination } from './_components/blog-pagination';
@@ -24,7 +23,8 @@ const BLOG_POSTS_PER_PAGE = 10;
export const generateMetadata = async (
props: BlogPageProps,
): Promise<Metadata> => {
const { t, resolvedLanguage } = await createI18nServerInstance();
const t = await getTranslations('marketing');
const resolvedLanguage = await getLocale();
const searchParams = await props.searchParams;
const limit = BLOG_POSTS_PER_PAGE;
@@ -34,8 +34,8 @@ export const generateMetadata = async (
const { total } = await getContentItems(resolvedLanguage, limit, offset);
return {
title: t('marketing:blog'),
description: t('marketing:blogSubtitle'),
title: t('blog'),
description: t('blogSubtitle'),
pagination: {
previous: page > 0 ? `/blog?page=${page - 1}` : undefined,
next: offset + limit < total ? `/blog?page=${page + 1}` : undefined,
@@ -67,7 +67,8 @@ const getContentItems = cache(
);
async function BlogPage(props: BlogPageProps) {
const { t, resolvedLanguage: language } = await createI18nServerInstance();
const t = await getTranslations('marketing');
const language = await getLocale();
const searchParams = await props.searchParams;
const limit = BLOG_POSTS_PER_PAGE;
@@ -82,15 +83,12 @@ async function BlogPage(props: BlogPageProps) {
return (
<>
<SitePageHeader
title={t('marketing:blog')}
subtitle={t('marketing:blogSubtitle')}
/>
<SitePageHeader title={t('blog')} subtitle={t('blogSubtitle')} />
<div className={'container flex flex-col space-y-6 py-8'}>
<If
condition={posts.length > 0}
fallback={<Trans i18nKey="marketing:noPosts" />}
fallback={<Trans i18nKey="marketing.noPosts" />}
>
<PostsGridList>
{posts.map((post, idx) => {
@@ -111,7 +109,7 @@ async function BlogPage(props: BlogPageProps) {
);
}
export default withI18n(BlogPage);
export default BlogPage;
function PostsGridList({ children }: React.PropsWithChildren) {
return (

View File

@@ -1,13 +1,10 @@
import { cache } from 'react';
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { createCmsClient } from '@kit/cms';
import { withI18n } from '~/lib/i18n/with-i18n';
import { ChangelogDetail } from '../_components/changelog-detail';
interface ChangelogEntryPageProps {
@@ -107,4 +104,4 @@ async function ChangelogEntryPage({ params }: ChangelogEntryPageProps) {
);
}
export default withI18n(ChangelogEntryPage);
export default ChangelogEntryPage;

View File

@@ -22,7 +22,7 @@ export function ChangelogHeader({ entry }: { entry: Cms.ContentItem }) {
className="text-muted-foreground hover:text-primary flex items-center gap-1.5 text-sm font-medium transition-colors"
>
<ChevronLeft className="h-4 w-4" />
<Trans i18nKey="marketing:changelog" />
<Trans i18nKey="marketing.changelog" />
</Link>
</div>
</div>

View File

@@ -24,8 +24,8 @@ function NavLink({ entry, direction }: NavLinkProps) {
const Icon = isPrevious ? ChevronLeft : ChevronRight;
const i18nKey = isPrevious
? 'marketing:changelogNavigationPrevious'
: 'marketing:changelogNavigationNext';
? 'marketing.changelogNavigationPrevious'
: 'marketing.changelogNavigationNext';
return (
<Link

View File

@@ -22,24 +22,29 @@ export function ChangelogPagination({
return (
<div className="flex justify-end gap-2">
{canGoToPreviousPage && (
<Button asChild variant="outline" size="sm">
<Link href={`/changelog?page=${previousPage}`}>
<ArrowLeft className="mr-2 h-3 w-3" />
<span>
<Trans i18nKey="marketing:changelogPaginationPrevious" />
</span>
</Link>
<Button
render={<Link href={`/changelog?page=${previousPage}`} />}
variant="outline"
size="sm"
>
<ArrowLeft className="mr-2 h-3 w-3" />
<span>
<Trans i18nKey="marketing.changelogPaginationPrevious" />
</span>
</Button>
)}
{canGoToNextPage && (
<Button asChild variant="outline" size="sm">
<Link href={`/changelog?page=${nextPage}`}>
<span>
<Trans i18nKey="marketing:changelogPaginationNext" />
</span>
<ArrowRight className="ml-2 h-3 w-3" />
</Link>
<Button
render={<Link href={`/changelog?page=${nextPage}`} />}
variant="outline"
size="sm"
>
<span>
<Trans i18nKey="marketing.changelogPaginationNext" />
</span>
<ArrowRight className="ml-2 h-3 w-3" />
</Button>
)}
</div>

View File

@@ -2,14 +2,13 @@ import { cache } from 'react';
import type { Metadata } from 'next';
import { getLocale, getTranslations } from 'next-intl/server';
import { createCmsClient } from '@kit/cms';
import { getLogger } from '@kit/shared/logger';
import { If } from '@kit/ui/if';
import { Trans } from '@kit/ui/trans';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
import { SitePageHeader } from '../_components/site-page-header';
import { ChangelogEntry } from './_components/changelog-entry';
import { ChangelogPagination } from './_components/changelog-pagination';
@@ -23,7 +22,8 @@ const CHANGELOG_ENTRIES_PER_PAGE = 50;
export const generateMetadata = async (
props: ChangelogPageProps,
): Promise<Metadata> => {
const { t, resolvedLanguage } = await createI18nServerInstance();
const t = await getTranslations('marketing');
const resolvedLanguage = await getLocale();
const searchParams = await props.searchParams;
const limit = CHANGELOG_ENTRIES_PER_PAGE;
@@ -33,8 +33,8 @@ export const generateMetadata = async (
const { total } = await getContentItems(resolvedLanguage, limit, offset);
return {
title: t('marketing:changelog'),
description: t('marketing:changelogSubtitle'),
title: t('changelog'),
description: t('changelogSubtitle'),
pagination: {
previous: page > 0 ? `/changelog?page=${page - 1}` : undefined,
next: offset + limit < total ? `/changelog?page=${page + 1}` : undefined,
@@ -66,7 +66,8 @@ const getContentItems = cache(
);
async function ChangelogPage(props: ChangelogPageProps) {
const { t, resolvedLanguage: language } = await createI18nServerInstance();
const t = await getTranslations('marketing');
const language = await getLocale();
const searchParams = await props.searchParams;
const limit = CHANGELOG_ENTRIES_PER_PAGE;
@@ -82,14 +83,14 @@ async function ChangelogPage(props: ChangelogPageProps) {
return (
<>
<SitePageHeader
title={t('marketing:changelog')}
subtitle={t('marketing:changelogSubtitle')}
title={t('changelog')}
subtitle={t('changelogSubtitle')}
/>
<div className="container flex max-w-4xl flex-col space-y-12 py-12">
<If
condition={entries.length > 0}
fallback={<Trans i18nKey="marketing:noChangelogEntries" />}
fallback={<Trans i18nKey="marketing.noChangelogEntries" />}
>
<div className="space-y-0">
{entries.map((entry, index) => {
@@ -114,4 +115,4 @@ async function ChangelogPage(props: ChangelogPageProps) {
);
}
export default withI18n(ChangelogPage);
export default ChangelogPage;

View File

@@ -1,8 +1,9 @@
'use client';
import { useState, useTransition } from 'react';
import { useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { useAction } from 'next-safe-action/hooks';
import { useForm } from 'react-hook-form';
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
@@ -23,13 +24,20 @@ import { ContactEmailSchema } from '~/(marketing)/contact/_lib/contact-email.sch
import { sendContactEmail } from '~/(marketing)/contact/_lib/server/server-actions';
export function ContactForm() {
const [pending, startTransition] = useTransition();
const [state, setState] = useState({
success: false,
error: false,
});
const { execute, isPending } = useAction(sendContactEmail, {
onSuccess: () => {
setState({ success: true, error: false });
},
onError: () => {
setState({ error: true, success: false });
},
});
const form = useForm({
resolver: zodResolver(ContactEmailSchema),
defaultValues: {
@@ -52,15 +60,7 @@ export function ContactForm() {
<form
className={'flex flex-col space-y-4'}
onSubmit={form.handleSubmit((data) => {
startTransition(async () => {
try {
await sendContactEmail(data);
setState({ success: true, error: false });
} catch {
setState({ error: true, success: false });
}
});
execute(data);
})}
>
<FormField
@@ -69,7 +69,7 @@ export function ContactForm() {
return (
<FormItem>
<FormLabel>
<Trans i18nKey={'marketing:contactName'} />
<Trans i18nKey={'marketing.contactName'} />
</FormLabel>
<FormControl>
@@ -88,7 +88,7 @@ export function ContactForm() {
return (
<FormItem>
<FormLabel>
<Trans i18nKey={'marketing:contactEmail'} />
<Trans i18nKey={'marketing.contactEmail'} />
</FormLabel>
<FormControl>
@@ -107,7 +107,7 @@ export function ContactForm() {
return (
<FormItem>
<FormLabel>
<Trans i18nKey={'marketing:contactMessage'} />
<Trans i18nKey={'marketing.contactMessage'} />
</FormLabel>
<FormControl>
@@ -124,8 +124,8 @@ export function ContactForm() {
}}
/>
<Button disabled={pending} type={'submit'}>
<Trans i18nKey={'marketing:sendMessage'} />
<Button disabled={isPending} type={'submit'}>
<Trans i18nKey={'marketing.sendMessage'} />
</Button>
</form>
</Form>
@@ -136,11 +136,11 @@ function SuccessAlert() {
return (
<Alert variant={'success'}>
<AlertTitle>
<Trans i18nKey={'marketing:contactSuccess'} />
<Trans i18nKey={'marketing.contactSuccess'} />
</AlertTitle>
<AlertDescription>
<Trans i18nKey={'marketing:contactSuccessDescription'} />
<Trans i18nKey={'marketing.contactSuccessDescription'} />
</AlertDescription>
</Alert>
);
@@ -150,11 +150,11 @@ function ErrorAlert() {
return (
<Alert variant={'destructive'}>
<AlertTitle>
<Trans i18nKey={'marketing:contactError'} />
<Trans i18nKey={'marketing.contactError'} />
</AlertTitle>
<AlertDescription>
<Trans i18nKey={'marketing:contactErrorDescription'} />
<Trans i18nKey={'marketing.contactErrorDescription'} />
</AlertDescription>
</Alert>
);

View File

@@ -1,4 +1,4 @@
import { z } from 'zod';
import * as z from 'zod';
export const ContactEmailSchema = z.object({
name: z.string().min(1).max(200),

View File

@@ -1,30 +1,29 @@
'use server';
import { z } from 'zod';
import * as z from 'zod';
import { getMailer } from '@kit/mailers';
import { enhanceAction } from '@kit/next/actions';
import { publicActionClient } from '@kit/next/safe-action';
import { ContactEmailSchema } from '../contact-email.schema';
const contactEmail = z
.string({
description: `The email where you want to receive the contact form submissions.`,
required_error:
error:
'Contact email is required. Please use the environment variable CONTACT_EMAIL.',
})
.parse(process.env.CONTACT_EMAIL);
const emailFrom = z
.string({
description: `The email sending address.`,
required_error:
error:
'Sender email is required. Please use the environment variable EMAIL_SENDER.',
})
.parse(process.env.EMAIL_SENDER);
export const sendContactEmail = enhanceAction(
async (data) => {
export const sendContactEmail = publicActionClient
.inputSchema(ContactEmailSchema)
.action(async ({ parsedInput: data }) => {
const mailer = await getMailer();
await mailer.sendEmail({
@@ -43,9 +42,4 @@ export const sendContactEmail = enhanceAction(
});
return {};
},
{
schema: ContactEmailSchema,
auth: false,
},
);
});

View File

@@ -1,28 +1,25 @@
import { getTranslations } from 'next-intl/server';
import { Heading } from '@kit/ui/heading';
import { Trans } from '@kit/ui/trans';
import { SitePageHeader } from '~/(marketing)/_components/site-page-header';
import { ContactForm } from '~/(marketing)/contact/_components/contact-form';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
export async function generateMetadata() {
const { t } = await createI18nServerInstance();
const t = await getTranslations('marketing');
return {
title: t('marketing:contact'),
title: t('contact'),
};
}
async function ContactPage() {
const { t } = await createI18nServerInstance();
const t = await getTranslations('marketing');
return (
<div>
<SitePageHeader
title={t(`marketing:contact`)}
subtitle={t(`marketing:contactDescription`)}
/>
<SitePageHeader title={t(`contact`)} subtitle={t(`contactDescription`)} />
<div className={'container mx-auto'}>
<div
@@ -35,11 +32,11 @@ async function ContactPage() {
>
<div>
<Heading level={3}>
<Trans i18nKey={'marketing:contactHeading'} />
<Trans i18nKey={'marketing.contactHeading'} />
</Heading>
<p className={'text-muted-foreground'}>
<Trans i18nKey={'marketing:contactSubheading'} />
<Trans i18nKey={'marketing.contactSubheading'} />
</p>
</div>
@@ -51,4 +48,4 @@ async function ContactPage() {
);
}
export default withI18n(ContactPage);
export default ContactPage;

View File

@@ -1,31 +1,30 @@
import Link from 'next/link';
import { ArrowRight, ChevronDown } from 'lucide-react';
import { getTranslations } from 'next-intl/server';
import { Button } from '@kit/ui/button';
import { Trans } from '@kit/ui/trans';
import { SitePageHeader } from '~/(marketing)/_components/site-page-header';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
export const generateMetadata = async () => {
const { t } = await createI18nServerInstance();
const t = await getTranslations('marketing');
return {
title: t('marketing:faq'),
title: t('faq'),
};
};
async function FAQPage() {
const { t } = await createI18nServerInstance();
const t = await getTranslations('marketing');
// replace this content with translations
const faqItems = [
{
// or: t('marketing:faq.question1')
// or: t('faq.question1')
question: `Do you offer a free trial?`,
// or: t('marketing:faq.answer1')
// or: t('faq.answer1')
answer: `Yes, we offer a 14-day free trial. You can cancel at any time during the trial period and you won't be charged.`,
},
{
@@ -74,10 +73,7 @@ async function FAQPage() {
/>
<div className={'flex flex-col space-y-4 xl:space-y-8'}>
<SitePageHeader
title={t('marketing:faq')}
subtitle={t('marketing:faqSubtitle')}
/>
<SitePageHeader title={t('faq')} subtitle={t('faqSubtitle')} />
<div className={'container flex flex-col items-center space-y-8 pb-16'}>
<div className="divide-border flex w-full max-w-xl flex-col divide-y divide-dashed rounded-md border">
@@ -87,14 +83,16 @@ async function FAQPage() {
</div>
<div>
<Button asChild variant={'outline'}>
<Link href={'/contact'}>
<span>
<Trans i18nKey={'marketing:contactFaq'} />
</span>
<Button
nativeButton={false}
render={<Link href={'/contact'} />}
variant={'link'}
>
<span>
<Trans i18nKey={'marketing.contactFaq'} />
</span>
<ArrowRight className={'ml-2 w-4'} />
</Link>
<ArrowRight className={'ml-2 w-4'} />
</Button>
</div>
</div>
@@ -103,7 +101,7 @@ async function FAQPage() {
);
}
export default withI18n(FAQPage);
export default FAQPage;
function FaqItem({
item,

View File

@@ -3,7 +3,6 @@ import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { SiteFooter } from '~/(marketing)/_components/site-footer';
import { SiteHeader } from '~/(marketing)/_components/site-header';
import { withI18n } from '~/lib/i18n/with-i18n';
async function SiteLayout(props: React.PropsWithChildren) {
const client = getSupabaseServerClient();
@@ -20,4 +19,4 @@ async function SiteLayout(props: React.PropsWithChildren) {
);
}
export default withI18n(SiteLayout);
export default SiteLayout;

View File

@@ -20,7 +20,6 @@ import { Trans } from '@kit/ui/trans';
import billingConfig from '~/config/billing.config';
import pathsConfig from '~/config/paths.config';
import { withI18n } from '~/lib/i18n/with-i18n';
function Home() {
return (
@@ -30,11 +29,13 @@ function Home() {
pill={
<Pill label={'New'}>
<span>The SaaS Starter Kit for ambitious developers</span>
<PillActionButton asChild>
<Link href={'/auth/sign-up'}>
<ArrowRightIcon className={'h-4 w-4'} />
</Link>
</PillActionButton>
<PillActionButton
render={
<Link href={'/auth/sign-up'}>
<ArrowRightIcon className={'h-4 w-4'} />
</Link>
}
/>
</Pill>
}
title={
@@ -170,7 +171,7 @@ function Home() {
);
}
export default withI18n(Home);
export default Home;
function MainCallToActionButton() {
return (
@@ -179,7 +180,7 @@ function MainCallToActionButton() {
<Link href={'/auth/sign-up'}>
<span className={'flex items-center space-x-0.5'}>
<span>
<Trans i18nKey={'common:getStarted'} />
<Trans i18nKey={'common.getStarted'} />
</span>
<ArrowRightIcon
@@ -194,7 +195,7 @@ function MainCallToActionButton() {
<CtaButton variant={'link'} className="h-10 text-sm">
<Link href={'/pricing'}>
<Trans i18nKey={'common:pricing'} />
<Trans i18nKey={'common.pricing'} />
</Link>
</CtaButton>
</div>

View File

@@ -1,16 +1,16 @@
import { getTranslations } from 'next-intl/server';
import { PricingTable } from '@kit/billing-gateway/marketing';
import { SitePageHeader } from '~/(marketing)/_components/site-page-header';
import billingConfig from '~/config/billing.config';
import pathsConfig from '~/config/paths.config';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
export const generateMetadata = async () => {
const { t } = await createI18nServerInstance();
const t = await getTranslations('marketing');
return {
title: t('marketing:pricing'),
title: t('pricing'),
};
};
@@ -20,14 +20,11 @@ const paths = {
};
async function PricingPage() {
const { t } = await createI18nServerInstance();
const t = await getTranslations('marketing');
return (
<div className={'flex flex-col space-y-8'}>
<SitePageHeader
title={t('marketing:pricing')}
subtitle={t('marketing:pricingSubtitle')}
/>
<SitePageHeader title={t('pricing')} subtitle={t('pricingSubtitle')} />
<div className={'container mx-auto pb-8 xl:pb-16'}>
<PricingTable paths={paths} config={billingConfig} />
@@ -36,4 +33,4 @@ async function PricingPage() {
);
}
export default withI18n(PricingPage);
export default PricingPage;

View File

@@ -15,7 +15,7 @@ import {
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
} from '@kit/ui/shadcn-sidebar';
} from '@kit/ui/sidebar';
import { AppLogo } from '~/components/app-logo';
import { ProfileAccountDropdownContainer } from '~/components/personal-account-dropdown-container';
@@ -24,7 +24,7 @@ export function AdminSidebar() {
const path = usePathname();
return (
<Sidebar collapsible="icon">
<Sidebar variant="floating" collapsible="icon">
<SidebarHeader className={'m-2'}>
<AppLogo href={'/admin'} className="max-w-full" />
</SidebarHeader>
@@ -35,25 +35,26 @@ export function AdminSidebar() {
<SidebarGroupContent>
<SidebarMenu>
<SidebarMenuButton isActive={path === '/admin'} asChild>
<Link className={'flex gap-2.5'} href={'/admin'}>
<LayoutDashboard className={'h-4'} />
<span>Dashboard</span>
</Link>
<SidebarMenuButton
isActive={path === '/admin'}
render={<Link className={'flex gap-2.5'} href={'/admin'} />}
>
<LayoutDashboard className={'h-4'} />
<span>Dashboard</span>
</SidebarMenuButton>
<SidebarMenuButton
isActive={path.includes('/admin/accounts')}
asChild
>
<Link
className={'flex size-full gap-2.5'}
href={'/admin/accounts'}
>
<Users className={'h-4'} />
<span>Accounts</span>
</Link>
</SidebarMenuButton>
render={
<Link
className={'flex size-full gap-2.5'}
href={'/admin/accounts'}
>
<Users className={'h-4'} />
<span>Accounts</span>
</Link>
}
/>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>

View File

@@ -28,7 +28,7 @@ async function AccountsPage(props: AdminAccountsPageProps) {
const page = searchParams.page ? parseInt(searchParams.page) : 1;
return (
<>
<PageBody>
<PageHeader description={<AppBreadcrumbs />}>
<div className="flex justify-end">
<AdminCreateUserDialog>
@@ -37,42 +37,40 @@ async function AccountsPage(props: AdminAccountsPageProps) {
</div>
</PageHeader>
<PageBody>
<ServerDataLoader
table={'accounts'}
client={client}
page={page}
where={(queryBuilder) => {
const { account_type: type, query } = searchParams;
<ServerDataLoader
table={'accounts'}
client={client}
page={page}
where={(queryBuilder) => {
const { account_type: type, query } = searchParams;
if (type && type !== 'all') {
queryBuilder.eq('is_personal_account', type === 'personal');
}
if (type && type !== 'all') {
queryBuilder.eq('is_personal_account', type === 'personal');
}
if (query) {
queryBuilder.or(`name.ilike.%${query}%,email.ilike.%${query}%`);
}
if (query) {
queryBuilder.or(`name.ilike.%${query}%,email.ilike.%${query}%`);
}
return queryBuilder;
}}
>
{({ data, page, pageSize, pageCount }) => {
return (
<AdminAccountsTable
page={page}
pageSize={pageSize}
pageCount={pageCount}
data={data}
filters={{
type: searchParams.account_type ?? 'all',
query: searchParams.query ?? '',
}}
/>
);
}}
</ServerDataLoader>
</PageBody>
</>
return queryBuilder;
}}
>
{({ data, page, pageSize, pageCount }) => {
return (
<AdminAccountsTable
page={page}
pageSize={pageSize}
pageCount={pageCount}
data={data}
filters={{
type: searchParams.account_type ?? 'all',
query: searchParams.query ?? '',
}}
/>
);
}}
</ServerDataLoader>
</PageBody>
);
}

View File

@@ -3,7 +3,7 @@ import { use } from 'react';
import { cookies } from 'next/headers';
import { Page, PageMobileNavigation, PageNavigation } from '@kit/ui/page';
import { SidebarProvider } from '@kit/ui/shadcn-sidebar';
import { SidebarProvider } from '@kit/ui/sidebar';
import { AdminSidebar } from '~/admin/_components/admin-sidebar';
import { AdminMobileNavigation } from '~/admin/_components/mobile-navigation';
@@ -36,9 +36,9 @@ export default function AdminLayout(props: React.PropsWithChildren) {
async function getLayoutState() {
const cookieStore = await cookies();
const sidebarOpenCookie = cookieStore.get('sidebar:state');
const sidebarOpenCookie = cookieStore.get('sidebar_state');
return {
open: sidebarOpenCookie?.value !== 'true',
open: sidebarOpenCookie?.value === 'true',
};
}

View File

@@ -4,13 +4,11 @@ import { PageBody, PageHeader } from '@kit/ui/page';
function AdminPage() {
return (
<>
<PageBody>
<PageHeader description={`Super Admin`} />
<PageBody>
<AdminDashboard />
</PageBody>
</>
<AdminDashboard />
</PageBody>
);
}

View File

@@ -8,7 +8,6 @@ import { Button } from '@kit/ui/button';
import { Trans } from '@kit/ui/trans';
import pathsConfig from '~/config/paths.config';
import { withI18n } from '~/lib/i18n/with-i18n';
interface AuthCallbackErrorPageProps {
searchParams: Promise<{
@@ -28,11 +27,11 @@ async function AuthCallbackErrorPage(props: AuthCallbackErrorPageProps) {
<div className={'flex flex-col space-y-4 py-4'}>
<Alert variant={'warning'}>
<AlertTitle>
<Trans i18nKey={'auth:authenticationErrorAlertHeading'} />
<Trans i18nKey={'auth.authenticationErrorAlertHeading'} />
</AlertTitle>
<AlertDescription>
<Trans i18nKey={error ?? 'auth:authenticationErrorAlertBody'} />
<Trans i18nKey={error ?? 'auth.authenticationErrorAlertBody'} />
</AlertDescription>
</Alert>
@@ -53,6 +52,7 @@ function AuthCallbackForm(props: {
switch (props.code) {
case 'otp_expired':
return <ResendAuthLinkForm redirectPath={props.redirectPath} />;
default:
return <SignInButton signInPath={props.signInPath} />;
}
@@ -60,12 +60,15 @@ function AuthCallbackForm(props: {
function SignInButton(props: { signInPath: string }) {
return (
<Button className={'w-full'} asChild>
<Link href={props.signInPath}>
<Trans i18nKey={'auth:signIn'} />
</Link>
</Button>
<Button
className={'w-full'}
render={
<Link href={props.signInPath}>
<Trans i18nKey={'auth.signIn'} />
</Link>
}
/>
);
}
export default withI18n(AuthCallbackErrorPage);
export default AuthCallbackErrorPage;

View File

@@ -1,19 +1,19 @@
import Link from 'next/link';
import { getTranslations } from 'next-intl/server';
import { PasswordResetRequestContainer } from '@kit/auth/password-reset';
import { Button } from '@kit/ui/button';
import { Heading } from '@kit/ui/heading';
import { Trans } from '@kit/ui/trans';
import pathsConfig from '~/config/paths.config';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
export const generateMetadata = async () => {
const { t } = await createI18nServerInstance();
const t = await getTranslations('auth');
return {
title: t('auth:passwordResetLabel'),
title: t('passwordResetLabel'),
};
};
@@ -25,11 +25,11 @@ function PasswordResetPage() {
<>
<div className={'flex flex-col items-center gap-1'}>
<Heading level={4} className={'tracking-tight'}>
<Trans i18nKey={'auth:passwordResetLabel'} />
<Trans i18nKey={'auth.passwordResetLabel'} />
</Heading>
<p className={'text-muted-foreground text-sm'}>
<Trans i18nKey={'auth:passwordResetSubheading'} />
<Trans i18nKey={'auth.passwordResetSubheading'} />
</p>
</div>
@@ -37,15 +37,20 @@ function PasswordResetPage() {
<PasswordResetRequestContainer redirectPath={redirectPath} />
<div className={'flex justify-center text-xs'}>
<Button asChild variant={'link'} size={'sm'}>
<Link href={signIn}>
<Trans i18nKey={'auth:passwordRecoveredQuestion'} />
</Link>
</Button>
<Button
nativeButton={false}
variant={'link'}
size={'sm'}
render={
<Link href={signIn}>
<Trans i18nKey={'auth.passwordRecoveredQuestion'} />
</Link>
}
/>
</div>
</div>
</>
);
}
export default withI18n(PasswordResetPage);
export default PasswordResetPage;

View File

@@ -1,5 +1,7 @@
import Link from 'next/link';
import { getTranslations } from 'next-intl/server';
import { SignInMethodsContainer } from '@kit/auth/sign-in';
import { getSafeRedirectPath } from '@kit/shared/utils';
import { Button } from '@kit/ui/button';
@@ -8,8 +10,6 @@ import { Trans } from '@kit/ui/trans';
import authConfig from '~/config/auth.config';
import pathsConfig from '~/config/paths.config';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
interface SignInPageProps {
searchParams: Promise<{
@@ -18,10 +18,10 @@ interface SignInPageProps {
}
export const generateMetadata = async () => {
const i18n = await createI18nServerInstance();
const t = await getTranslations('auth');
return {
title: i18n.t('auth:signIn'),
title: t('signIn'),
};
};
@@ -38,11 +38,11 @@ async function SignInPage({ searchParams }: SignInPageProps) {
<>
<div className={'flex flex-col items-center gap-1'}>
<Heading level={4} className={'tracking-tight'}>
<Trans i18nKey={'auth:signInHeading'} />
<Trans i18nKey={'auth.signInHeading'} />
</Heading>
<p className={'text-muted-foreground text-sm'}>
<Trans i18nKey={'auth:signInSubheading'} />
<Trans i18nKey={'auth.signInSubheading'} />
</p>
</div>
@@ -53,14 +53,19 @@ async function SignInPage({ searchParams }: SignInPageProps) {
/>
<div className={'flex justify-center'}>
<Button asChild variant={'link'} size={'sm'}>
<Link href={pathsConfig.auth.signUp} prefetch={true}>
<Trans i18nKey={'auth:doNotHaveAccountYet'} />
</Link>
</Button>
<Button
nativeButton={false}
variant={'link'}
size={'sm'}
render={
<Link href={pathsConfig.auth.signUp} prefetch={true}>
<Trans i18nKey={'auth.doNotHaveAccountYet'} />
</Link>
}
/>
</div>
</>
);
}
export default withI18n(SignInPage);
export default SignInPage;

View File

@@ -1,5 +1,7 @@
import Link from 'next/link';
import { getTranslations } from 'next-intl/server';
import { SignUpMethodsContainer } from '@kit/auth/sign-up';
import { Button } from '@kit/ui/button';
import { Heading } from '@kit/ui/heading';
@@ -7,14 +9,12 @@ import { Trans } from '@kit/ui/trans';
import authConfig from '~/config/auth.config';
import pathsConfig from '~/config/paths.config';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
export const generateMetadata = async () => {
const i18n = await createI18nServerInstance();
const t = await getTranslations('auth');
return {
title: i18n.t('auth:signUp'),
title: t('signUp'),
};
};
@@ -28,11 +28,11 @@ async function SignUpPage() {
<>
<div className={'flex flex-col items-center gap-1'}>
<Heading level={4} className={'tracking-tight'}>
<Trans i18nKey={'auth:signUpHeading'} />
<Trans i18nKey={'auth.signUpHeading'} />
</Heading>
<p className={'text-muted-foreground text-sm'}>
<Trans i18nKey={'auth:signUpSubheading'} />
<Trans i18nKey={'auth.signUpSubheading'} />
</p>
</div>
@@ -44,14 +44,19 @@ async function SignUpPage() {
/>
<div className={'flex justify-center'}>
<Button asChild variant={'link'} size={'sm'}>
<Link href={pathsConfig.auth.signIn} prefetch={true}>
<Trans i18nKey={'auth:alreadyHaveAnAccount'} />
</Link>
</Button>
<Button
render={
<Link href={pathsConfig.auth.signIn} prefetch={true}>
<Trans i18nKey={'auth.alreadyHaveAnAccount'} />
</Link>
}
variant={'link'}
size={'sm'}
nativeButton={false}
/>
</div>
</>
);
}
export default withI18n(SignUpPage);
export default SignUpPage;

View File

@@ -1,13 +1,13 @@
import { redirect } from 'next/navigation';
import { getTranslations } from 'next-intl/server';
import { MultiFactorChallengeContainer } from '@kit/auth/mfa';
import { getSafeRedirectPath } from '@kit/shared/utils';
import { checkRequiresMultiFactorAuthentication } from '@kit/supabase/check-requires-mfa';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import pathsConfig from '~/config/paths.config';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
interface Props {
searchParams: Promise<{
@@ -16,10 +16,10 @@ interface Props {
}
export const generateMetadata = async () => {
const i18n = await createI18nServerInstance();
const t = await getTranslations('auth');
return {
title: i18n.t('auth:signIn'),
title: t('signIn'),
};
};
@@ -51,4 +51,4 @@ async function VerifyPage(props: Props) {
);
}
export default withI18n(VerifyPage);
export default VerifyPage;

View File

@@ -1,17 +1,11 @@
import { cache } from 'react';
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { ContentRenderer, createCmsClient } from '@kit/cms';
import { If } from '@kit/ui/if';
import { Separator } from '@kit/ui/separator';
import { cn } from '@kit/ui/utils';
import { withI18n } from '~/lib/i18n/with-i18n';
// local imports
import { DocsCards } from '../_components/docs-cards';
const getPageBySlug = cache(pageLoader);
interface DocumentationPageProps {
@@ -24,7 +18,9 @@ async function pageLoader(slug: string) {
return client.getContentItemBySlug({ slug, collection: 'documentation' });
}
export const generateMetadata = async ({ params }: DocumentationPageProps) => {
export async function generateMetadata({
params,
}: DocumentationPageProps): Promise<Metadata> {
const slug = (await params).slug.join('/');
const page = await getPageBySlug(slug);
@@ -38,7 +34,7 @@ export const generateMetadata = async ({ params }: DocumentationPageProps) => {
title,
description,
};
};
}
async function DocumentationPage({ params }: DocumentationPageProps) {
const slug = (await params).slug.join('/');
@@ -51,17 +47,13 @@ async function DocumentationPage({ params }: DocumentationPageProps) {
const description = page?.description ?? '';
return (
<div className={'flex flex-1 flex-col gap-y-4 overflow-y-hidden'}>
<div className={'flex size-full overflow-y-hidden'}>
<div className="relative size-full">
<div className={'container flex flex-1 flex-col gap-y-4'}>
<div className={'flex flex-1'}>
<div className="relative mx-auto max-w-3xl flex-1 flex-col overflow-x-hidden">
<article
className={cn(
'absolute size-full w-full gap-y-12 overflow-y-auto pt-4 pb-36',
)}
className={cn('mx-auto h-full w-full flex-1 gap-y-12 pt-4 pb-36')}
>
<section
className={'flex flex-col gap-y-1 border-b border-dashed pb-4'}
>
<section className={'mt-4 flex flex-col gap-y-1 pb-4'}>
<h1
className={
'text-foreground text-3xl font-semibold tracking-tighter'
@@ -81,14 +73,8 @@ async function DocumentationPage({ params }: DocumentationPageProps) {
</article>
</div>
</div>
<If condition={page.children.length > 0}>
<Separator />
<DocsCards cards={page.children ?? []} />
</If>
</div>
);
}
export default withI18n(DocumentationPage);
export default DocumentationPage;

View File

@@ -0,0 +1,33 @@
'use client';
import Link from 'next/link';
import { useSearchParams } from 'next/navigation';
import { ArrowLeftIcon } from 'lucide-react';
import { getSafeRedirectPath } from '@kit/shared/utils';
import { Button } from '@kit/ui/button';
import { Trans } from '@kit/ui/trans';
import appConfig from '~/config/app.config';
export function DocsBackButton() {
const searchParams = useSearchParams();
const returnPath = searchParams.get('returnPath');
const parsedPath = getSafeRedirectPath(returnPath, '/');
return (
<Button
nativeButton={false}
variant="link"
render={
<Link href={parsedPath || '/'}>
<ArrowLeftIcon className="size-4" />{' '}
<span className={'hidden sm:block'}>
<Trans i18nKey="common.back" values={{ product: appConfig.name }} />
</span>
</Link>
}
/>
);
}

View File

@@ -0,0 +1,36 @@
import Link from 'next/link';
import { Header } from '@kit/ui/marketing';
import { Separator } from '@kit/ui/separator';
import { Trans } from '@kit/ui/trans';
import { AppLogo } from '~/components/app-logo';
import { DocsBackButton } from './docs-back-button';
export function DocsHeader() {
return (
<Header
logo={
<div className={'flex w-full flex-1 justify-between'}>
<div className="flex items-center gap-x-4">
<AppLogo href="/" />
<Separator orientation="vertical" />
<Link
href="/help"
className="font-semibold tracking-tight hover:underline"
>
<Trans i18nKey="marketing.documentation" />
</Link>
</div>
<DocsBackButton />
</div>
}
centered={false}
className="border-border/50 border-b px-4"
/>
);
}

View File

@@ -3,7 +3,7 @@
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { SidebarMenuButton, SidebarMenuItem } from '@kit/ui/shadcn-sidebar';
import { SidebarMenuButton, SidebarMenuItem } from '@kit/ui/sidebar';
import { cn, isRouteActive } from '@kit/ui/utils';
export function DocsNavLink({
@@ -12,20 +12,18 @@ export function DocsNavLink({
children,
}: React.PropsWithChildren<{ label: string; url: string }>) {
const currentPath = usePathname();
const isCurrent = isRouteActive(url, currentPath, true);
const isCurrent = isRouteActive(url, currentPath);
return (
<SidebarMenuItem>
<SidebarMenuButton
asChild
render={<Link href={url} />}
isActive={isCurrent}
className={cn('text-secondary-foreground transition-all')}
>
<Link href={url}>
<span className="block max-w-full truncate">{label}</span>
<span className="block max-w-full truncate">{label}</span>
{children}
</Link>
{children}
</SidebarMenuButton>
</SidebarMenuItem>
);

View File

@@ -16,7 +16,7 @@ export function DocsNavigationCollapsible(
const prefix = props.prefix;
const isChildActive = props.node.children.some((child) =>
isRouteActive(prefix + '/' + child.url, currentPath, false),
isRouteActive(prefix + '/' + child.url, currentPath),
);
return (

View File

@@ -10,12 +10,11 @@ import {
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
} from '@kit/ui/shadcn-sidebar';
} from '@kit/ui/sidebar';
import { DocsNavLink } from '~/(marketing)/docs/_components/docs-nav-link';
import { DocsNavigationCollapsible } from '~/(marketing)/docs/_components/docs-navigation-collapsible';
import { FloatingDocumentationNavigation } from './floating-docs-navigation';
import { DocsNavLink } from '../_components/docs-nav-link';
import { DocsNavigationCollapsible } from '../_components/docs-navigation-collapsible';
import { FloatingDocumentationNavigationButton } from './floating-docs-navigation-button';
function Node({
node,
@@ -85,13 +84,11 @@ function NodeTrigger({
}) {
if (node.collapsible) {
return (
<CollapsibleTrigger asChild>
<SidebarMenuItem>
<SidebarMenuButton>
{label}
<ChevronDown className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-180" />
</SidebarMenuButton>
</SidebarMenuItem>
<CollapsibleTrigger render={<SidebarMenuItem />}>
<SidebarMenuButton>
{label}
<ChevronDown className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-180" />
</SidebarMenuButton>
</CollapsibleTrigger>
);
}
@@ -136,13 +133,8 @@ export function DocsNavigation({
}) {
return (
<>
<Sidebar
variant={'ghost'}
className={
'border-border/50 sticky z-1 mt-4 max-h-full overflow-y-auto pr-4'
}
>
<SidebarGroup className="p-0">
<Sidebar variant={'floating'}>
<SidebarGroup>
<SidebarGroupContent>
<SidebarMenu className={'pb-48'}>
<Tree pages={pages} level={0} prefix={prefix} />
@@ -151,17 +143,7 @@ export function DocsNavigation({
</SidebarGroup>
</Sidebar>
<div className={'lg:hidden'}>
<FloatingDocumentationNavigation>
<SidebarGroup>
<SidebarGroupContent>
<SidebarMenu>
<Tree pages={pages} level={0} prefix={prefix} />
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</FloatingDocumentationNavigation>
</div>
<FloatingDocumentationNavigationButton />
</>
);
}

View File

@@ -0,0 +1,22 @@
'use client';
import { Menu } from 'lucide-react';
import { Button } from '@kit/ui/button';
import { useSidebar } from '@kit/ui/sidebar';
export function FloatingDocumentationNavigationButton() {
const { toggleSidebar } = useSidebar();
return (
<Button
size="custom"
variant="custom"
className={
'bg-primary fixed right-5 bottom-5 z-10 h-16! w-16! rounded-full! lg:hidden'
}
onClick={toggleSidebar}
>
<Menu className={'text-primary-foreground size-6'} />
</Button>
);
}

View File

@@ -0,0 +1,49 @@
import { getLocale } from 'next-intl/server';
import { SidebarInset, SidebarProvider } from '@kit/ui/sidebar';
import { DocsHeader } from './_components/docs-header';
// local imports
import { DocsNavigation } from './_components/docs-navigation';
import { getDocs } from './_lib/server/docs.loader';
import { buildDocumentationTree } from './_lib/utils';
type DocsLayoutProps = React.PropsWithChildren<{
params: Promise<{ locale?: string }>;
}>;
async function DocsLayout({ children, params }: DocsLayoutProps) {
let { locale } = await params;
if (!locale) {
locale = await getLocale();
}
return (
<SidebarProvider
defaultOpen={true}
style={
{
'--sidebar-width': '300px',
} as React.CSSProperties
}
>
<DocsSidebar locale={locale} />
<SidebarInset className="h-screen overflow-y-auto overscroll-y-none">
<DocsHeader />
{children}
</SidebarInset>
</SidebarProvider>
);
}
async function DocsSidebar({ locale }: { locale: string }) {
const pages = await getDocs(locale);
const tree = buildDocumentationTree(pages);
return <DocsNavigation pages={tree} />;
}
export default DocsLayout;

View File

@@ -0,0 +1,54 @@
import { getLocale, getTranslations } from 'next-intl/server';
import { SitePageHeader } from '../(marketing)/_components/site-page-header';
import { DocsCards } from './_components/docs-cards';
import { getDocs } from './_lib/server/docs.loader';
type DocsPageProps = {
params: Promise<{ locale?: string }>;
};
export const generateMetadata = async () => {
const t = await getTranslations('marketing');
return {
title: t('documentation'),
};
};
async function DocsPage({ params }: DocsPageProps) {
const t = await getTranslations('marketing');
let { locale } = await params;
if (!locale) {
locale = await getLocale();
}
return (
<div className={'flex w-full flex-1 flex-col gap-y-6 xl:gap-y-8'}>
<SitePageHeader
title={t('documentation')}
subtitle={t('documentationSubtitle')}
/>
<div
className={
'relative container flex size-full justify-center overflow-y-auto'
}
>
<DocaCardsList locale={locale} />
</div>
</div>
);
}
async function DocaCardsList({ locale }: { locale: string }) {
const items = await getDocs(locale);
// Filter out any docs that have a parentId, as these are children of other docs
const cards = items.filter((item) => !item.parentId);
return <DocsCards cards={cards} />;
}
export default DocsPage;

View File

@@ -22,10 +22,10 @@ const ErrorPage = ({
<SiteHeader user={user.data} />
<ErrorPageContent
statusCode={'common:errorPageHeading'}
heading={'common:genericError'}
subtitle={'common:genericErrorSubHeading'}
backLabel={'common:goBack'}
statusCode={'common.errorPageHeading'}
heading={'common.genericError'}
subtitle={'common.genericErrorSubHeading'}
backLabel={'common.goBack'}
reset={reset}
/>
</div>

View File

@@ -5,7 +5,7 @@ import { useContext } from 'react';
import { useRouter } from 'next/navigation';
import { AccountSelector } from '@kit/accounts/account-selector';
import { SidebarContext } from '@kit/ui/shadcn-sidebar';
import { SidebarContext } from '@kit/ui/sidebar';
import featureFlagsConfig from '~/config/feature-flags.config';
import pathsConfig from '~/config/paths.config';
@@ -22,7 +22,6 @@ export function HomeAccountSelector(props: {
}>;
userId: string;
collisionPadding?: number;
}) {
const router = useRouter();
const context = useContext(SidebarContext);
@@ -30,12 +29,13 @@ export function HomeAccountSelector(props: {
return (
<AccountSelector
collapsed={!context?.open}
collisionPadding={props.collisionPadding ?? 20}
accounts={props.accounts}
features={features}
userId={props.userId}
onAccountChange={(value) => {
if (value) {
document.cookie = `last-selected-team=${encodeURIComponent(value)}; path=/; max-age=${60 * 60 * 24 * 30}; SameSite=Lax`;
const path = pathsConfig.app.accountHome.replace('[account]', value);
router.replace(path);
}

View File

@@ -31,13 +31,16 @@ export function HomeAccountsList() {
<div className="flex flex-col">
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{accounts.map((account) => (
<CardButton key={account.value} asChild>
<Link href={`/home/${account.value}`}>
<CardButtonHeader>
<CardButtonTitle>{account.label}</CardButtonTitle>
</CardButtonHeader>
</Link>
</CardButton>
<CardButton
key={account.value}
render={
<Link href={`/home/${account.value}`}>
<CardButtonHeader>
<CardButtonTitle>{account.label}</CardButtonTitle>
</CardButtonHeader>
</Link>
}
/>
))}
</div>
</div>
@@ -50,17 +53,21 @@ function HomeAccountsListEmptyState(props: {
return (
<div className={'flex flex-1'}>
<EmptyState>
<EmptyStateButton asChild>
<HomeAddAccountButton
className={'mt-4'}
canCreateTeamAccount={props.canCreateTeamAccount}
/>
</EmptyStateButton>
<EmptyStateButton
render={
<HomeAddAccountButton
className={'mt-4'}
canCreateTeamAccount={props.canCreateTeamAccount}
/>
}
/>
<EmptyStateHeading>
<Trans i18nKey={'account:noTeamsYet'} />
<Trans i18nKey={'account.noTeamsYet'} />
</EmptyStateHeading>
<EmptyStateText>
<Trans i18nKey={'account:createTeam'} />
<Trans i18nKey={'account.createTeam'} />
</EmptyStateText>
</EmptyState>
</div>

View File

@@ -32,7 +32,7 @@ export function HomeAddAccountButton(props: HomeAddAccountButtonProps) {
onClick={() => setIsAddingAccount(true)}
disabled={!canCreate}
>
<Trans i18nKey={'account:createTeamButtonLabel'} />
<Trans i18nKey={'account.createTeamButtonLabel'} />
</Button>
);
@@ -41,9 +41,10 @@ export function HomeAddAccountButton(props: HomeAddAccountButtonProps) {
{!canCreate && reason ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span className="cursor-not-allowed">{button}</span>
</TooltipTrigger>
<TooltipTrigger
render={<span className="cursor-not-allowed">{button}</span>}
/>
<TooltipContent>
<Trans i18nKey={reason} defaults={reason} />
</TooltipContent>

View File

@@ -49,7 +49,9 @@ export function HomeMenuNavigation(props: { workspace: UserWorkspace }) {
</div>
<div className={'flex justify-end space-x-2.5'}>
<UserNotifications userId={user.id} />
<If condition={featuresFlagConfig.enableNotifications}>
<UserNotifications userId={user.id} />
</If>
<If condition={featuresFlagConfig.enableTeamAccounts}>
<HomeAccountSelector userId={user.id} accounts={accounts} />

View File

@@ -56,13 +56,12 @@ export function HomeMobileNavigation(props: { workspace: UserWorkspace }) {
<If condition={featuresFlagConfig.enableTeamAccounts}>
<DropdownMenuGroup>
<DropdownMenuLabel>
<Trans i18nKey={'common:yourAccounts'} />
<Trans i18nKey={'common.yourAccounts'} />
</DropdownMenuLabel>
<HomeAccountSelector
userId={props.workspace.user.id}
accounts={props.workspace.accounts}
collisionPadding={0}
/>
</DropdownMenuGroup>
@@ -87,18 +86,21 @@ function DropdownLink(
}>,
) {
return (
<DropdownMenuItem asChild key={props.path}>
<Link
href={props.path}
className={'flex h-12 w-full items-center space-x-4'}
>
{props.Icon}
<DropdownMenuItem
render={
<Link
href={props.path}
className={'flex h-12 w-full items-center space-x-4'}
>
{props.Icon}
<span>
<Trans i18nKey={props.label} defaults={props.label} />
</span>
</Link>
</DropdownMenuItem>
<span>
<Trans i18nKey={props.label} defaults={props.label} />
</span>
</Link>
}
key={props.path}
/>
);
}
@@ -115,7 +117,7 @@ function SignOutDropdownItem(
<LogOut className={'h-6'} />
<span>
<Trans i18nKey={'common:signOut'} defaults={'Sign out'} />
<Trans i18nKey={'common.signOut'} defaults={'Sign out'} />
</span>
</DropdownMenuItem>
);

View File

@@ -0,0 +1,44 @@
import { If } from '@kit/ui/if';
import { Sidebar, SidebarContent, SidebarHeader } from '@kit/ui/sidebar';
import { SidebarNavigation } from '@kit/ui/sidebar-navigation';
import { WorkspaceDropdown } from '~/components/workspace-dropdown';
import featuresFlagConfig from '~/config/feature-flags.config';
import { personalAccountNavigationConfig } from '~/config/personal-account-navigation.config';
import { UserNotifications } from '~/home/(user)/_components/user-notifications';
// home imports
import type { UserWorkspace } from '../_lib/server/load-user-workspace';
interface HomeSidebarProps {
workspace: UserWorkspace;
}
export function HomeSidebar(props: HomeSidebarProps) {
const { workspace, user, accounts } = props.workspace;
const collapsible = personalAccountNavigationConfig.sidebarCollapsedStyle;
return (
<Sidebar variant="floating" collapsible={collapsible}>
<SidebarHeader className={'h-16 justify-center'}>
<div className={'flex items-center justify-between gap-x-1'}>
<WorkspaceDropdown
user={user}
accounts={accounts}
workspace={workspace}
/>
<If condition={featuresFlagConfig.enableNotifications}>
<div className={'group-data-[collapsible=icon]:hidden'}>
<UserNotifications userId={user.id} />
</div>
</If>
</div>
</SidebarHeader>
<SidebarContent>
<SidebarNavigation config={personalAccountNavigationConfig} />
</SidebarContent>
</Sidebar>
);
}

View File

@@ -1,10 +1,11 @@
'use client';
import { useState, useTransition } from 'react';
import { useState } from 'react';
import dynamic from 'next/dynamic';
import { ExclamationTriangleIcon } from '@radix-ui/react-icons';
import { TriangleAlert } from 'lucide-react';
import { useAction } from 'next-safe-action/hooks';
import { PlanPicker } from '@kit/billing-gateway/components';
import { useAppEvents } from '@kit/shared/events';
@@ -39,7 +40,6 @@ const EmbeddedCheckout = dynamic(
export function PersonalAccountCheckoutForm(props: {
customerId: string | null | undefined;
}) {
const [pending, startTransition] = useTransition();
const [error, setError] = useState(false);
const appEvents = useAppEvents();
@@ -47,6 +47,20 @@ export function PersonalAccountCheckoutForm(props: {
undefined,
);
const { execute, isPending } = useAction(
createPersonalAccountCheckoutSession,
{
onSuccess: ({ data }) => {
if (data?.checkoutToken) {
setCheckoutToken(data.checkoutToken);
}
},
onError: () => {
setError(true);
},
},
);
// only allow trial if the user is not already a customer
const canStartTrial = !props.customerId;
@@ -67,11 +81,11 @@ export function PersonalAccountCheckoutForm(props: {
<Card>
<CardHeader>
<CardTitle>
<Trans i18nKey={'common:planCardLabel'} />
<Trans i18nKey={'billing.planCardLabel'} />
</CardTitle>
<CardDescription>
<Trans i18nKey={'common:planCardDescription'} />
<Trans i18nKey={'billing.planCardDescription'} />
</CardDescription>
</CardHeader>
@@ -81,27 +95,18 @@ export function PersonalAccountCheckoutForm(props: {
</If>
<PlanPicker
pending={pending}
pending={isPending}
config={billingConfig}
canStartTrial={canStartTrial}
onSubmit={({ planId, productId }) => {
startTransition(async () => {
try {
appEvents.emit({
type: 'checkout.started',
payload: { planId },
});
appEvents.emit({
type: 'checkout.started',
payload: { planId },
});
const { checkoutToken } =
await createPersonalAccountCheckoutSession({
planId,
productId,
});
setCheckoutToken(checkoutToken);
} catch {
setError(true);
}
execute({
planId,
productId,
});
}}
/>
@@ -114,14 +119,14 @@ export function PersonalAccountCheckoutForm(props: {
function ErrorAlert() {
return (
<Alert variant={'destructive'}>
<ExclamationTriangleIcon className={'h-4'} />
<TriangleAlert className={'h-4'} />
<AlertTitle>
<Trans i18nKey={'common:planPickerAlertErrorTitle'} />
<Trans i18nKey={'billing.planPickerAlertErrorTitle'} />
</AlertTitle>
<AlertDescription>
<Trans i18nKey={'common:planPickerAlertErrorDescription'} />
<Trans i18nKey={'billing.planPickerAlertErrorDescription'} />
</AlertDescription>
</Alert>
);

View File

@@ -0,0 +1,22 @@
'use client';
import { useAction } from 'next-safe-action/hooks';
import { BillingPortalCard } from '@kit/billing-gateway/components';
import { createPersonalAccountBillingPortalSession } from '../_lib/server/server-actions';
export function PersonalBillingPortalForm() {
const { execute } = useAction(createPersonalAccountBillingPortalSession);
return (
<form
onSubmit={(e) => {
e.preventDefault();
execute();
}}
>
<BillingPortalCard />
</form>
);
}

View File

@@ -1,4 +1,4 @@
import { z } from 'zod';
import * as z from 'zod';
export const PersonalAccountCheckoutSchema = z.object({
planId: z.string().min(1),

View File

@@ -1,5 +1,4 @@
import 'server-only';
import { cache } from 'react';
import { createAccountsApi } from '@kit/accounts/api';

View File

@@ -2,7 +2,7 @@
import { redirect } from 'next/navigation';
import { enhanceAction } from '@kit/next/actions';
import { authActionClient } from '@kit/next/safe-action';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import featureFlagsConfig from '~/config/feature-flags.config';
@@ -20,8 +20,9 @@ const enabled = featureFlagsConfig.enablePersonalAccountBilling;
* @name createPersonalAccountCheckoutSession
* @description Creates a checkout session for a personal account.
*/
export const createPersonalAccountCheckoutSession = enhanceAction(
async function (data) {
export const createPersonalAccountCheckoutSession = authActionClient
.inputSchema(PersonalAccountCheckoutSchema)
.action(async ({ parsedInput: data }) => {
if (!enabled) {
throw new Error('Personal account billing is not enabled');
}
@@ -30,18 +31,14 @@ export const createPersonalAccountCheckoutSession = enhanceAction(
const service = createUserBillingService(client);
return await service.createCheckoutSession(data);
},
{
schema: PersonalAccountCheckoutSchema,
},
);
});
/**
* @name createPersonalAccountBillingPortalSession
* @description Creates a billing Portal session for a personal account
*/
export const createPersonalAccountBillingPortalSession = enhanceAction(
async () => {
export const createPersonalAccountBillingPortalSession =
authActionClient.action(async () => {
if (!enabled) {
throw new Error('Personal account billing is not enabled');
}
@@ -52,7 +49,5 @@ export const createPersonalAccountBillingPortalSession = enhanceAction(
// get url to billing portal
const url = await service.createBillingPortalSession();
return redirect(url);
},
{},
);
redirect(url);
});

View File

@@ -1,8 +1,7 @@
import 'server-only';
import { SupabaseClient } from '@supabase/supabase-js';
import { z } from 'zod';
import * as z from 'zod';
import { createAccountsApi } from '@kit/accounts/api';
import { getProductPlanPair } from '@kit/billing';
@@ -39,7 +38,7 @@ class UserBillingService {
async createCheckoutSession({
planId,
productId,
}: z.infer<typeof PersonalAccountCheckoutSchema>) {
}: z.output<typeof PersonalAccountCheckoutSchema>) {
// get the authenticated user
const { data: user, error } = await requireUser(this.client);

View File

@@ -0,0 +1,105 @@
import { getTranslations } from 'next-intl/server';
import { resolveProductPlan } from '@kit/billing-gateway';
import {
CurrentLifetimeOrderCard,
CurrentSubscriptionCard,
} from '@kit/billing-gateway/components';
import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs';
import { If } from '@kit/ui/if';
import { PageBody } from '@kit/ui/page';
import { Trans } from '@kit/ui/trans';
import billingConfig from '~/config/billing.config';
import { requireUserInServerComponent } from '~/lib/server/require-user-in-server-component';
// local imports
import { HomeLayoutPageHeader } from '../_components/home-page-header';
import { PersonalAccountCheckoutForm } from './_components/personal-account-checkout-form';
import { PersonalBillingPortalForm } from './_components/personal-billing-portal-form';
import { loadPersonalAccountBillingPageData } from './_lib/server/personal-account-billing-page.loader';
export const generateMetadata = async () => {
const t = await getTranslations('account');
const title = t('billingTab');
return {
title,
};
};
async function PersonalAccountBillingPage() {
const user = await requireUserInServerComponent();
const [subscription, order, customerId] =
await loadPersonalAccountBillingPageData(user.id);
const subscriptionVariantId = subscription?.items[0]?.variant_id;
const orderVariantId = order?.items[0]?.variant_id;
const subscriptionProductPlan =
subscription && subscriptionVariantId
? await resolveProductPlan(
billingConfig,
subscriptionVariantId,
subscription.currency,
)
: undefined;
const orderProductPlan =
order && orderVariantId
? await resolveProductPlan(billingConfig, orderVariantId, order.currency)
: undefined;
const hasBillingData = subscription || order;
return (
<PageBody>
<HomeLayoutPageHeader
title={<Trans i18nKey={'common.routes.billing'} />}
description={<AppBreadcrumbs />}
/>
<div className={'flex max-w-2xl flex-col space-y-4'}>
<If
condition={hasBillingData}
fallback={
<>
<PersonalAccountCheckoutForm customerId={customerId} />
</>
}
>
<div className={'flex w-full flex-col space-y-6'}>
<If condition={subscription}>
{(subscription) => {
return (
<CurrentSubscriptionCard
subscription={subscription}
product={subscriptionProductPlan!.product}
plan={subscriptionProductPlan!.plan}
/>
);
}}
</If>
<If condition={order}>
{(order) => {
return (
<CurrentLifetimeOrderCard
order={order}
product={orderProductPlan!.product}
plan={orderProductPlan!.plan}
/>
);
}}
</If>
</div>
</If>
<If condition={customerId}>{() => <PersonalBillingPortalForm />}</If>
</div>
</PageBody>
);
}
export default PersonalAccountBillingPage;

View File

@@ -3,15 +3,16 @@ import { use } from 'react';
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
import { z } from 'zod';
import * as z from 'zod';
import { UserWorkspaceContextProvider } from '@kit/accounts/components';
import { Page, PageMobileNavigation, PageNavigation } from '@kit/ui/page';
import { SidebarProvider } from '@kit/ui/shadcn-sidebar';
import { SidebarProvider } from '@kit/ui/sidebar';
import { AppLogo } from '~/components/app-logo';
import featuresFlagConfig from '~/config/feature-flags.config';
import pathsConfig from '~/config/paths.config';
import { personalAccountNavigationConfig } from '~/config/personal-account-navigation.config';
import { withI18n } from '~/lib/i18n/with-i18n';
// home imports
import { HomeMenuNavigation } from './_components/home-menu-navigation';
@@ -29,7 +30,7 @@ function UserHomeLayout({ children }: React.PropsWithChildren) {
return <HeaderLayout>{children}</HeaderLayout>;
}
export default withI18n(UserHomeLayout);
export default UserHomeLayout;
async function SidebarLayout({ children }: React.PropsWithChildren) {
const [workspace, state] = await Promise.all([
@@ -41,6 +42,8 @@ async function SidebarLayout({ children }: React.PropsWithChildren) {
redirect('/');
}
await redirectIfTeamsOnly(workspace);
return (
<UserWorkspaceContextProvider value={workspace}>
<SidebarProvider defaultOpen={state.open}>
@@ -60,8 +63,10 @@ async function SidebarLayout({ children }: React.PropsWithChildren) {
);
}
function HeaderLayout({ children }: React.PropsWithChildren) {
const workspace = use(loadUserWorkspace());
async function HeaderLayout({ children }: React.PropsWithChildren) {
const workspace = await loadUserWorkspace();
await redirectIfTeamsOnly(workspace);
return (
<UserWorkspaceContextProvider value={workspace}>
@@ -94,16 +99,39 @@ function MobileNavigation({
);
}
async function redirectIfTeamsOnly(
workspace: Awaited<ReturnType<typeof loadUserWorkspace>>,
) {
if (featuresFlagConfig.enableTeamsOnly) {
const firstTeam = workspace.accounts[0];
if (firstTeam?.value) {
const cookieStore = await cookies();
const lastSelected = cookieStore.get('last-selected-team')?.value;
const preferred = lastSelected
? workspace.accounts.find((a) => a.value === lastSelected)
: undefined;
const team = preferred ?? firstTeam;
redirect(pathsConfig.app.accountHome.replace('[account]', team.value!));
} else {
redirect(pathsConfig.app.createTeam);
}
}
}
async function getLayoutState() {
const cookieStore = await cookies();
const LayoutStyleSchema = z.enum(['sidebar', 'header', 'custom']);
const layoutStyleCookie = cookieStore.get('layout-style');
const sidebarOpenCookie = cookieStore.get('sidebar:state');
const sidebarOpenCookie = cookieStore.get('sidebar_state');
const sidebarOpen = sidebarOpenCookie
? sidebarOpenCookie.value === 'false'
? sidebarOpenCookie.value === 'true'
: !personalAccountNavigationConfig.sidebarCollapsed;
const parsedStyle = LayoutStyleSchema.safeParse(layoutStyleCookie?.value);

View File

@@ -0,0 +1,29 @@
import { getTranslations } from 'next-intl/server';
import { PageBody } from '@kit/ui/page';
import { Trans } from '@kit/ui/trans';
// local imports
import { HomeLayoutPageHeader } from './_components/home-page-header';
export const generateMetadata = async () => {
const t = await getTranslations('account');
const title = t('homePage');
return {
title,
};
};
function UserHomePage() {
return (
<PageBody>
<HomeLayoutPageHeader
title={<Trans i18nKey={'common.routes.home'} />}
description={<Trans i18nKey={'common.homeTabDescription'} />}
/>
</PageBody>
);
}
export default UserHomePage;

View File

@@ -1,22 +1,21 @@
import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs';
import { PageBody } from '@kit/ui/page';
import { Trans } from '@kit/ui/trans';
import { withI18n } from '~/lib/i18n/with-i18n';
// local imports
import { HomeLayoutPageHeader } from '../_components/home-page-header';
function UserSettingsLayout(props: React.PropsWithChildren) {
return (
<>
<PageBody>
<HomeLayoutPageHeader
title={<Trans i18nKey={'account:routes.settings'} />}
title={<Trans i18nKey={'account.routes.settings'} />}
description={<AppBreadcrumbs />}
/>
{props.children}
</>
</PageBody>
);
}
export default withI18n(UserSettingsLayout);
export default UserSettingsLayout;

View File

@@ -1,13 +1,12 @@
import { use } from 'react';
import { getTranslations } from 'next-intl/server';
import { PersonalAccountSettingsContainer } from '@kit/accounts/personal-account-settings';
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 { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
import { requireUserInServerComponent } from '~/lib/server/require-user-in-server-component';
// Show email option if password, magic link, or OTP is enabled
@@ -33,8 +32,8 @@ const paths = {
};
export const generateMetadata = async () => {
const i18n = await createI18nServerInstance();
const title = i18n.t('account:settingsTab');
const t = await getTranslations('account');
const title = t('settingsTab');
return {
title,
@@ -45,17 +44,15 @@ function PersonalAccountSettingsPage() {
const user = use(requireUserInServerComponent());
return (
<PageBody>
<div className={'flex w-full flex-1 flex-col lg:max-w-2xl'}>
<PersonalAccountSettingsContainer
userId={user.id}
features={features}
paths={paths}
providers={providers}
/>
</div>
</PageBody>
<div className={'flex w-full flex-1 flex-col lg:max-w-2xl'}>
<PersonalAccountSettingsContainer
userId={user.id}
features={features}
paths={paths}
providers={providers}
/>
</div>
);
}
export default withI18n(PersonalAccountSettingsPage);
export default PersonalAccountSettingsPage;

View File

@@ -29,6 +29,7 @@ import {
ChartTooltip,
ChartTooltipContent,
} from '@kit/ui/chart';
import { useIsMobile } from '@kit/ui/hooks/use-mobile';
import {
Table,
TableBody,
@@ -205,7 +206,7 @@ function Chart(
/>
<ChartTooltip
cursor={false}
content={<ChartTooltipContent hideLabel />}
content={(props) => <ChartTooltipContent hideLabel {...props} />}
/>
<Line
dataKey="value"
@@ -497,6 +498,8 @@ function Trend(
}
export function VisitorsChart() {
const isMobile = useIsMobile();
const chartData = useMemo(
() => [
{ date: '2024-04-01', desktop: 222, mobile: 150 },
@@ -618,14 +621,17 @@ export function VisitorsChart() {
</CardHeader>
<CardContent>
<ChartContainer className={'h-64 w-full'} config={chartConfig}>
<AreaChart accessibilityLayer data={chartData}>
<ChartContainer
config={chartConfig}
className="aspect-auto h-[250px] w-full"
>
<AreaChart data={chartData}>
<defs>
<linearGradient id="fillDesktop" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="var(--color-desktop)"
stopOpacity={0.8}
stopOpacity={1.0}
/>
<stop
offset="95%"
@@ -648,21 +654,41 @@ export function VisitorsChart() {
</defs>
<CartesianGrid vertical={false} />
<XAxis
dataKey="month"
dataKey="date"
tickLine={false}
axisLine={false}
tickMargin={8}
tickFormatter={(value: string) => value.slice(0, 3)}
minTickGap={32}
tickFormatter={(value) => {
const date = new Date(value);
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
});
}}
/>
<ChartTooltip
cursor={false}
content={<ChartTooltipContent indicator="dot" />}
defaultIndex={isMobile ? -1 : 10}
content={(props) => (
<ChartTooltipContent
{...props}
labelFormatter={(value) => {
return new Date(value).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
});
}}
indicator="dot"
/>
)}
/>
<Area
dataKey="mobile"
type="natural"
fill="url(#fillMobile)"
fillOpacity={0.4}
stroke="var(--color-mobile)"
stackId="a"
/>
@@ -670,7 +696,6 @@ export function VisitorsChart() {
dataKey="desktop"
type="natural"
fill="url(#fillDesktop)"
fillOpacity={0.4}
stroke="var(--color-desktop)"
stackId="a"
/>
@@ -698,100 +723,102 @@ export function PageViewsChart() {
const [activeChart, setActiveChart] =
useState<keyof typeof chartConfig>('desktop');
// eslint-disable-next-line react-hooks/exhaustive-deps
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 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 = {
views: {
@@ -870,8 +897,9 @@ export function PageViewsChart() {
}}
/>
<ChartTooltip
content={
content={(props) => (
<ChartTooltipContent
{...props}
className="w-[150px]"
nameKey="views"
labelFormatter={(value) => {
@@ -882,7 +910,7 @@ export function PageViewsChart() {
});
}}
/>
}
)}
/>
<Bar dataKey={activeChart} fill={`var(--color-${activeChart})`} />
</BarChart>

View File

@@ -1,11 +1,9 @@
'use client';
import { useContext } from 'react';
import { useRouter } from 'next/navigation';
import { AccountSelector } from '@kit/accounts/account-selector';
import { SidebarContext } from '@kit/ui/shadcn-sidebar';
import { useSidebar } from '@kit/ui/sidebar';
import featureFlagsConfig from '~/config/feature-flags.config';
import pathsConfig from '~/config/paths.config';
@@ -25,7 +23,7 @@ export function TeamAccountAccountsSelector(params: {
}>;
}) {
const router = useRouter();
const ctx = useContext(SidebarContext);
const ctx = useSidebar();
return (
<AccountSelector
@@ -34,7 +32,16 @@ export function TeamAccountAccountsSelector(params: {
userId={params.userId}
collapsed={!ctx?.open}
features={features}
showPersonalAccount={!featureFlagsConfig.enableTeamsOnly}
onAccountChange={(value) => {
if (!value && featureFlagsConfig.enableTeamsOnly) {
return;
}
if (value) {
document.cookie = `last-selected-team=${encodeURIComponent(value)}; path=/; max-age=${60 * 60 * 24 * 30}; SameSite=Lax`;
}
const path = value
? pathsConfig.app.accountHome.replace('[account]', value)
: pathsConfig.app.home;

View File

@@ -99,18 +99,20 @@ function DropdownLink(
}>,
) {
return (
<DropdownMenuItem asChild>
<Link
href={props.path}
className={'flex h-12 w-full items-center gap-x-3 px-3'}
>
{props.Icon}
<DropdownMenuItem
render={
<Link
href={props.path}
className={'flex h-12 w-full items-center gap-x-3 px-3'}
>
{props.Icon}
<span>
<Trans i18nKey={props.label} defaults={props.label} />
</span>
</Link>
</DropdownMenuItem>
<span>
<Trans i18nKey={props.label} defaults={props.label} />
</span>
</Link>
}
/>
);
}
@@ -127,7 +129,7 @@ function SignOutDropdownItem(
<LogOut className={'h-4'} />
<span>
<Trans i18nKey={'common:signOut'} />
<Trans i18nKey={'common.signOut'} />
</span>
</DropdownMenuItem>
);
@@ -142,35 +144,40 @@ function TeamAccountsModal(props: {
return (
<Dialog>
<DialogTrigger asChild>
<DropdownMenuItem
className={'flex h-12 w-full items-center space-x-2'}
onSelect={(e) => e.preventDefault()}
>
<Home className={'h-4'} />
<DialogTrigger
render={
<DropdownMenuItem
className={'flex h-12 w-full items-center space-x-2'}
onSelect={(e) => e.preventDefault()}
>
<Home className={'h-4'} />
<span>
<Trans i18nKey={'common:yourAccounts'} />
</span>
</DropdownMenuItem>
</DialogTrigger>
<span>
<Trans i18nKey={'common.yourAccounts'} />
</span>
</DropdownMenuItem>
}
/>
<DialogContent>
<DialogHeader>
<DialogTitle>
<Trans i18nKey={'common:yourAccounts'} />
<Trans i18nKey={'common.yourAccounts'} />
</DialogTitle>
</DialogHeader>
<div className={'py-6'}>
<AccountSelector
className={'w-full max-w-full'}
collisionPadding={0}
userId={props.userId}
accounts={props.accounts}
features={features}
selectedAccount={props.account}
onAccountChange={(value) => {
if (value) {
document.cookie = `last-selected-team=${encodeURIComponent(value)}; path=/; max-age=${60 * 60 * 24 * 30}; SameSite=Lax`;
}
const path = value
? pathsConfig.app.accountHome.replace('[account]', value)
: pathsConfig.app.home;

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