Unify workspace dropdowns; Update layouts (#458)
Unified Account and Workspace drop-downs; Layout updates, now header lives within the PageBody component; Sidebars now use floating variant
This commit is contained in:
committed by
GitHub
parent
ca585e09be
commit
4bc8448a1d
@@ -38,6 +38,7 @@ NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_DELETION=true
|
||||
NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_BILLING=true
|
||||
NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS=true
|
||||
NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_CREATION=true
|
||||
NEXT_PUBLIC_ENABLE_TEAMS_ACCOUNTS_ONLY=false
|
||||
NEXT_PUBLIC_LANGUAGE_PRIORITY=application
|
||||
|
||||
# NEXTJS
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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" />,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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} />}
|
||||
/>
|
||||
@@ -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>
|
||||
@@ -6,8 +6,6 @@ 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 +73,4 @@ async function BlogPost({ params }: BlogPageProps) {
|
||||
);
|
||||
}
|
||||
|
||||
export default withI18n(BlogPost);
|
||||
export default BlogPost;
|
||||
@@ -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>
|
||||
@@ -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 (
|
||||
@@ -6,8 +6,6 @@ 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 +105,4 @@ async function ChangelogEntryPage({ params }: ChangelogEntryPageProps) {
|
||||
);
|
||||
}
|
||||
|
||||
export default withI18n(ChangelogEntryPage);
|
||||
export default ChangelogEntryPage;
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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),
|
||||
@@ -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
|
||||
.schema(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,
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -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;
|
||||
@@ -7,8 +7,6 @@ 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';
|
||||
|
||||
@@ -91,4 +89,4 @@ async function DocumentationPage({ params }: DocumentationPageProps) {
|
||||
);
|
||||
}
|
||||
|
||||
export default withI18n(DocumentationPage);
|
||||
export default DocumentationPage;
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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 (
|
||||
@@ -10,12 +10,12 @@ 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 { FloatingDocumentationNavigationButton } from './floating-docs-navigation-button';
|
||||
|
||||
function Node({
|
||||
node,
|
||||
@@ -85,13 +85,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>
|
||||
);
|
||||
}
|
||||
@@ -137,12 +135,10 @@ export function DocsNavigation({
|
||||
return (
|
||||
<>
|
||||
<Sidebar
|
||||
variant={'ghost'}
|
||||
className={
|
||||
'border-border/50 sticky z-1 mt-4 max-h-full overflow-y-auto pr-4'
|
||||
}
|
||||
variant={'sidebar'}
|
||||
className={'sticky z-1 mt-4 max-h-full overflow-y-auto pr-4'}
|
||||
>
|
||||
<SidebarGroup className="p-0">
|
||||
<SidebarGroup>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu className={'pb-48'}>
|
||||
<Tree pages={pages} level={0} prefix={prefix} />
|
||||
@@ -151,17 +147,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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { SidebarProvider } from '@kit/ui/shadcn-sidebar';
|
||||
import { getLocale } from 'next-intl/server';
|
||||
|
||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||
import { SidebarProvider } from '@kit/ui/sidebar';
|
||||
|
||||
// local imports
|
||||
import { DocsNavigation } from './_components/docs-navigation';
|
||||
@@ -8,8 +8,8 @@ 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 locale = await getLocale();
|
||||
const docs = await getDocs(locale);
|
||||
const tree = buildDocumentationTree(docs);
|
||||
|
||||
return (
|
||||
@@ -1,21 +1,21 @@
|
||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
import { getLocale, getTranslations } from 'next-intl/server';
|
||||
|
||||
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();
|
||||
const t = await getTranslations('marketing');
|
||||
|
||||
return {
|
||||
title: t('marketing:documentation'),
|
||||
title: t('documentation'),
|
||||
};
|
||||
};
|
||||
|
||||
async function DocsPage() {
|
||||
const { t, resolvedLanguage } = await createI18nServerInstance();
|
||||
const items = await getDocs(resolvedLanguage);
|
||||
const t = await getTranslations('marketing');
|
||||
const locale = await getLocale();
|
||||
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);
|
||||
@@ -23,8 +23,8 @@ async function DocsPage() {
|
||||
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')}
|
||||
title={t('documentation')}
|
||||
subtitle={t('documentationSubtitle')}
|
||||
/>
|
||||
|
||||
<div className={'relative flex size-full justify-center overflow-y-auto'}>
|
||||
@@ -34,4 +34,4 @@ async function DocsPage() {
|
||||
);
|
||||
}
|
||||
|
||||
export default withI18n(DocsPage);
|
||||
export default DocsPage;
|
||||
@@ -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,
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
@@ -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';
|
||||
@@ -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>
|
||||
@@ -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';
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
@@ -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,7 +29,6 @@ export function HomeAccountSelector(props: {
|
||||
return (
|
||||
<AccountSelector
|
||||
collapsed={!context?.open}
|
||||
collisionPadding={props.collisionPadding ?? 20}
|
||||
accounts={props.accounts}
|
||||
features={features}
|
||||
userId={props.userId}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
);
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Sidebar, SidebarContent, SidebarHeader } from '@kit/ui/sidebar';
|
||||
import { SidebarNavigation } from '@kit/ui/sidebar-navigation';
|
||||
|
||||
import { WorkspaceDropdown } from '~/components/workspace-dropdown';
|
||||
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-3'}>
|
||||
<WorkspaceDropdown
|
||||
user={user}
|
||||
accounts={accounts}
|
||||
workspace={workspace}
|
||||
/>
|
||||
|
||||
<div className={'group-data-[collapsible=icon]:hidden'}>
|
||||
<UserNotifications userId={user.id} />
|
||||
</div>
|
||||
</div>
|
||||
</SidebarHeader>
|
||||
|
||||
<SidebarContent>
|
||||
<SidebarNavigation config={personalAccountNavigationConfig} />
|
||||
</SidebarContent>
|
||||
</Sidebar>
|
||||
);
|
||||
}
|
||||
@@ -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={'common.planPickerAlertErrorTitle'} />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
<Trans i18nKey={'common:planPickerAlertErrorDescription'} />
|
||||
<Trans i18nKey={'common.planPickerAlertErrorDescription'} />
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { z } from 'zod';
|
||||
import * as z from 'zod';
|
||||
|
||||
export const PersonalAccountCheckoutSchema = z.object({
|
||||
planId: z.string().min(1),
|
||||
@@ -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
|
||||
.schema(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);
|
||||
});
|
||||
@@ -2,7 +2,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 +39,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);
|
||||
|
||||
105
apps/web/app/[locale]/home/(user)/billing/page.tsx
Normal file
105
apps/web/app/[locale]/home/(user)/billing/page.tsx
Normal 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;
|
||||
@@ -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('/');
|
||||
}
|
||||
|
||||
redirectIfTeamsOnly(workspace);
|
||||
|
||||
return (
|
||||
<UserWorkspaceContextProvider value={workspace}>
|
||||
<SidebarProvider defaultOpen={state.open}>
|
||||
@@ -63,6 +66,8 @@ async function SidebarLayout({ children }: React.PropsWithChildren) {
|
||||
function HeaderLayout({ children }: React.PropsWithChildren) {
|
||||
const workspace = use(loadUserWorkspace());
|
||||
|
||||
redirectIfTeamsOnly(workspace);
|
||||
|
||||
return (
|
||||
<UserWorkspaceContextProvider value={workspace}>
|
||||
<Page style={'header'}>
|
||||
@@ -94,6 +99,22 @@ function MobileNavigation({
|
||||
);
|
||||
}
|
||||
|
||||
function redirectIfTeamsOnly(
|
||||
workspace: Awaited<ReturnType<typeof loadUserWorkspace>>,
|
||||
) {
|
||||
if (featuresFlagConfig.enableTeamsOnly) {
|
||||
const firstTeam = workspace.accounts[0];
|
||||
|
||||
if (firstTeam?.value) {
|
||||
redirect(
|
||||
pathsConfig.app.accountHome.replace('[account]', firstTeam.value),
|
||||
);
|
||||
} else {
|
||||
redirect(pathsConfig.app.createTeam);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getLayoutState() {
|
||||
const cookieStore = await cookies();
|
||||
|
||||
29
apps/web/app/[locale]/home/(user)/page.tsx
Normal file
29
apps/web/app/[locale]/home/(user)/page.tsx
Normal 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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
@@ -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,12 @@ export function TeamAccountAccountsSelector(params: {
|
||||
userId={params.userId}
|
||||
collapsed={!ctx?.open}
|
||||
features={features}
|
||||
showPersonalAccount={!featureFlagsConfig.enableTeamsOnly}
|
||||
onAccountChange={(value) => {
|
||||
if (!value && featureFlagsConfig.enableTeamsOnly) {
|
||||
return;
|
||||
}
|
||||
|
||||
const path = value
|
||||
? pathsConfig.app.accountHome.replace('[account]', value)
|
||||
: pathsConfig.app.home;
|
||||
@@ -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,30 +144,31 @@ 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}
|
||||
@@ -1,12 +1,12 @@
|
||||
import { z } from 'zod';
|
||||
import * as z from 'zod';
|
||||
|
||||
import { NavigationConfigSchema } from '@kit/ui/navigation-schema';
|
||||
import { SidebarNavigation } from '@kit/ui/shadcn-sidebar';
|
||||
import { SidebarNavigation } from '@kit/ui/sidebar-navigation';
|
||||
|
||||
export function TeamAccountLayoutSidebarNavigation({
|
||||
config,
|
||||
}: React.PropsWithChildren<{
|
||||
config: z.infer<typeof NavigationConfigSchema>;
|
||||
config: z.output<typeof NavigationConfigSchema>;
|
||||
}>) {
|
||||
return <SidebarNavigation config={config} />;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { JWTUserData } from '@kit/supabase/types';
|
||||
import { Sidebar, SidebarContent, SidebarHeader } from '@kit/ui/sidebar';
|
||||
|
||||
import type { AccountModel } from '~/components/workspace-dropdown';
|
||||
import { WorkspaceDropdown } from '~/components/workspace-dropdown';
|
||||
import { getTeamAccountSidebarConfig } from '~/config/team-account-navigation.config';
|
||||
import { TeamAccountNotifications } from '~/home/[account]/_components/team-account-notifications';
|
||||
|
||||
import { TeamAccountLayoutSidebarNavigation } from './team-account-layout-sidebar-navigation';
|
||||
|
||||
export function TeamAccountLayoutSidebar(props: {
|
||||
account: string;
|
||||
accountId: string;
|
||||
accounts: AccountModel[];
|
||||
user: JWTUserData;
|
||||
}) {
|
||||
const { account, accounts, user } = props;
|
||||
|
||||
const config = getTeamAccountSidebarConfig(account);
|
||||
const collapsible = config.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}
|
||||
selectedAccount={account}
|
||||
/>
|
||||
|
||||
<div className={'group-data-[collapsible=icon]:hidden'}>
|
||||
<TeamAccountNotifications
|
||||
userId={user.id}
|
||||
accountId={props.accountId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</SidebarHeader>
|
||||
|
||||
<SidebarContent className="h-[calc(100%-160px)] overflow-y-auto">
|
||||
<TeamAccountLayoutSidebarNavigation config={config} />
|
||||
</SidebarContent>
|
||||
</Sidebar>
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user