Revert "Unify workspace dropdowns; Update layouts (#458)"
This reverts commit 4bc8448a1d.
This commit is contained in:
@@ -38,7 +38,6 @@ 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
|
||||
|
||||
30
apps/web/app/(marketing)/(legal)/cookie-policy/page.tsx
Normal file
30
apps/web/app/(marketing)/(legal)/cookie-policy/page.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
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);
|
||||
30
apps/web/app/(marketing)/(legal)/privacy-policy/page.tsx
Normal file
30
apps/web/app/(marketing)/(legal)/privacy-policy/page.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
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);
|
||||
30
apps/web/app/(marketing)/(legal)/terms-of-service/page.tsx
Normal file
30
apps/web/app/(marketing)/(legal)/terms-of-service/page.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
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);
|
||||
@@ -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,7 +31,6 @@ const MobileModeToggle = dynamic(
|
||||
|
||||
const paths = {
|
||||
home: pathsConfig.app.home,
|
||||
profileSettings: pathsConfig.app.personalAccountSettings,
|
||||
};
|
||||
|
||||
const features = {
|
||||
@@ -79,28 +78,26 @@ function AuthButtons() {
|
||||
|
||||
<div className={'flex items-center gap-x-2'}>
|
||||
<Button
|
||||
nativeButton={false}
|
||||
className={'hidden md:flex md:text-sm'}
|
||||
render={
|
||||
<Link href={pathsConfig.auth.signIn}>
|
||||
<Trans i18nKey={'auth.signIn'} />
|
||||
</Link>
|
||||
}
|
||||
asChild
|
||||
variant={'outline'}
|
||||
size={'sm'}
|
||||
/>
|
||||
>
|
||||
<Link href={pathsConfig.auth.signIn}>
|
||||
<Trans i18nKey={'auth:signIn'} />
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
nativeButton={false}
|
||||
render={
|
||||
<Link href={pathsConfig.auth.signUp}>
|
||||
<Trans i18nKey={'auth.signUp'} />
|
||||
</Link>
|
||||
}
|
||||
asChild
|
||||
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 className="mx-auto sm:mx-0" href="/" />}
|
||||
logo={<AppLogo />}
|
||||
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,14 +74,11 @@ function MobileDropdown() {
|
||||
const className = 'flex w-full h-full items-center';
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={item.path}
|
||||
render={
|
||||
<Link className={className} href={item.path}>
|
||||
<Trans i18nKey={item.label} />
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
<DropdownMenuItem key={item.path} asChild>
|
||||
<Link className={className} href={item.path}>
|
||||
<Trans i18nKey={item.label} />
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
@@ -6,6 +6,8 @@ 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 {
|
||||
@@ -73,4 +75,4 @@ async function BlogPost({ params }: BlogPageProps) {
|
||||
);
|
||||
}
|
||||
|
||||
export default BlogPost;
|
||||
export default withI18n(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,13 +2,14 @@ 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';
|
||||
@@ -23,8 +24,7 @@ const BLOG_POSTS_PER_PAGE = 10;
|
||||
export const generateMetadata = async (
|
||||
props: BlogPageProps,
|
||||
): Promise<Metadata> => {
|
||||
const t = await getTranslations('marketing');
|
||||
const resolvedLanguage = await getLocale();
|
||||
const { t, resolvedLanguage } = await createI18nServerInstance();
|
||||
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('blog'),
|
||||
description: t('blogSubtitle'),
|
||||
title: t('marketing:blog'),
|
||||
description: t('marketing:blogSubtitle'),
|
||||
pagination: {
|
||||
previous: page > 0 ? `/blog?page=${page - 1}` : undefined,
|
||||
next: offset + limit < total ? `/blog?page=${page + 1}` : undefined,
|
||||
@@ -67,8 +67,7 @@ const getContentItems = cache(
|
||||
);
|
||||
|
||||
async function BlogPage(props: BlogPageProps) {
|
||||
const t = await getTranslations('marketing');
|
||||
const language = await getLocale();
|
||||
const { t, resolvedLanguage: language } = await createI18nServerInstance();
|
||||
const searchParams = await props.searchParams;
|
||||
|
||||
const limit = BLOG_POSTS_PER_PAGE;
|
||||
@@ -83,12 +82,15 @@ async function BlogPage(props: BlogPageProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<SitePageHeader title={t('blog')} subtitle={t('blogSubtitle')} />
|
||||
<SitePageHeader
|
||||
title={t('marketing:blog')}
|
||||
subtitle={t('marketing: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) => {
|
||||
@@ -109,7 +111,7 @@ async function BlogPage(props: BlogPageProps) {
|
||||
);
|
||||
}
|
||||
|
||||
export default BlogPage;
|
||||
export default withI18n(BlogPage);
|
||||
|
||||
function PostsGridList({ children }: React.PropsWithChildren) {
|
||||
return (
|
||||
@@ -6,6 +6,8 @@ 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 {
|
||||
@@ -105,4 +107,4 @@ async function ChangelogEntryPage({ params }: ChangelogEntryPageProps) {
|
||||
);
|
||||
}
|
||||
|
||||
export default ChangelogEntryPage;
|
||||
export default withI18n(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,29 +22,24 @@ export function ChangelogPagination({
|
||||
return (
|
||||
<div className="flex justify-end gap-2">
|
||||
{canGoToPreviousPage && (
|
||||
<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 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>
|
||||
)}
|
||||
|
||||
{canGoToNextPage && (
|
||||
<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 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>
|
||||
)}
|
||||
</div>
|
||||
@@ -2,13 +2,14 @@ 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';
|
||||
@@ -22,8 +23,7 @@ const CHANGELOG_ENTRIES_PER_PAGE = 50;
|
||||
export const generateMetadata = async (
|
||||
props: ChangelogPageProps,
|
||||
): Promise<Metadata> => {
|
||||
const t = await getTranslations('marketing');
|
||||
const resolvedLanguage = await getLocale();
|
||||
const { t, resolvedLanguage } = await createI18nServerInstance();
|
||||
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('changelog'),
|
||||
description: t('changelogSubtitle'),
|
||||
title: t('marketing:changelog'),
|
||||
description: t('marketing:changelogSubtitle'),
|
||||
pagination: {
|
||||
previous: page > 0 ? `/changelog?page=${page - 1}` : undefined,
|
||||
next: offset + limit < total ? `/changelog?page=${page + 1}` : undefined,
|
||||
@@ -66,8 +66,7 @@ const getContentItems = cache(
|
||||
);
|
||||
|
||||
async function ChangelogPage(props: ChangelogPageProps) {
|
||||
const t = await getTranslations('marketing');
|
||||
const language = await getLocale();
|
||||
const { t, resolvedLanguage: language } = await createI18nServerInstance();
|
||||
const searchParams = await props.searchParams;
|
||||
|
||||
const limit = CHANGELOG_ENTRIES_PER_PAGE;
|
||||
@@ -83,14 +82,14 @@ async function ChangelogPage(props: ChangelogPageProps) {
|
||||
return (
|
||||
<>
|
||||
<SitePageHeader
|
||||
title={t('changelog')}
|
||||
subtitle={t('changelogSubtitle')}
|
||||
title={t('marketing:changelog')}
|
||||
subtitle={t('marketing: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) => {
|
||||
@@ -115,4 +114,4 @@ async function ChangelogPage(props: ChangelogPageProps) {
|
||||
);
|
||||
}
|
||||
|
||||
export default ChangelogPage;
|
||||
export default withI18n(ChangelogPage);
|
||||
@@ -1,9 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useState, useTransition } 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';
|
||||
@@ -24,20 +23,13 @@ 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: {
|
||||
@@ -60,7 +52,15 @@ export function ContactForm() {
|
||||
<form
|
||||
className={'flex flex-col space-y-4'}
|
||||
onSubmit={form.handleSubmit((data) => {
|
||||
execute(data);
|
||||
startTransition(async () => {
|
||||
try {
|
||||
await sendContactEmail(data);
|
||||
|
||||
setState({ success: true, error: false });
|
||||
} catch {
|
||||
setState({ error: true, success: false });
|
||||
}
|
||||
});
|
||||
})}
|
||||
>
|
||||
<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={isPending} type={'submit'}>
|
||||
<Trans i18nKey={'marketing.sendMessage'} />
|
||||
<Button disabled={pending} 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 * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ContactEmailSchema = z.object({
|
||||
name: z.string().min(1).max(200),
|
||||
@@ -1,29 +1,30 @@
|
||||
'use server';
|
||||
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { getMailer } from '@kit/mailers';
|
||||
import { publicActionClient } from '@kit/next/safe-action';
|
||||
import { enhanceAction } from '@kit/next/actions';
|
||||
|
||||
import { ContactEmailSchema } from '../contact-email.schema';
|
||||
|
||||
const contactEmail = z
|
||||
.string({
|
||||
error:
|
||||
description: `The email where you want to receive the contact form submissions.`,
|
||||
required_error:
|
||||
'Contact email is required. Please use the environment variable CONTACT_EMAIL.',
|
||||
})
|
||||
.parse(process.env.CONTACT_EMAIL);
|
||||
|
||||
const emailFrom = z
|
||||
.string({
|
||||
error:
|
||||
description: `The email sending address.`,
|
||||
required_error:
|
||||
'Sender email is required. Please use the environment variable EMAIL_SENDER.',
|
||||
})
|
||||
.parse(process.env.EMAIL_SENDER);
|
||||
|
||||
export const sendContactEmail = publicActionClient
|
||||
.schema(ContactEmailSchema)
|
||||
.action(async ({ parsedInput: data }) => {
|
||||
export const sendContactEmail = enhanceAction(
|
||||
async (data) => {
|
||||
const mailer = await getMailer();
|
||||
|
||||
await mailer.sendEmail({
|
||||
@@ -42,4 +43,9 @@ export const sendContactEmail = publicActionClient
|
||||
});
|
||||
|
||||
return {};
|
||||
});
|
||||
},
|
||||
{
|
||||
schema: ContactEmailSchema,
|
||||
auth: false,
|
||||
},
|
||||
);
|
||||
@@ -1,25 +1,28 @@
|
||||
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 getTranslations('marketing');
|
||||
const { t } = await createI18nServerInstance();
|
||||
|
||||
return {
|
||||
title: t('contact'),
|
||||
title: t('marketing:contact'),
|
||||
};
|
||||
}
|
||||
|
||||
async function ContactPage() {
|
||||
const t = await getTranslations('marketing');
|
||||
const { t } = await createI18nServerInstance();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SitePageHeader title={t(`contact`)} subtitle={t(`contactDescription`)} />
|
||||
<SitePageHeader
|
||||
title={t(`marketing:contact`)}
|
||||
subtitle={t(`marketing:contactDescription`)}
|
||||
/>
|
||||
|
||||
<div className={'container mx-auto'}>
|
||||
<div
|
||||
@@ -32,11 +35,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>
|
||||
|
||||
@@ -48,4 +51,4 @@ async function ContactPage() {
|
||||
);
|
||||
}
|
||||
|
||||
export default ContactPage;
|
||||
export default withI18n(ContactPage);
|
||||
@@ -7,6 +7,8 @@ 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';
|
||||
|
||||
@@ -89,4 +91,4 @@ async function DocumentationPage({ params }: DocumentationPageProps) {
|
||||
);
|
||||
}
|
||||
|
||||
export default DocumentationPage;
|
||||
export default withI18n(DocumentationPage);
|
||||
@@ -3,7 +3,7 @@
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
import { SidebarMenuButton, SidebarMenuItem } from '@kit/ui/sidebar';
|
||||
import { SidebarMenuButton, SidebarMenuItem } from '@kit/ui/shadcn-sidebar';
|
||||
import { cn, isRouteActive } from '@kit/ui/utils';
|
||||
|
||||
export function DocsNavLink({
|
||||
@@ -12,18 +12,20 @@ export function DocsNavLink({
|
||||
children,
|
||||
}: React.PropsWithChildren<{ label: string; url: string }>) {
|
||||
const currentPath = usePathname();
|
||||
const isCurrent = isRouteActive(url, currentPath);
|
||||
const isCurrent = isRouteActive(url, currentPath, true);
|
||||
|
||||
return (
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
render={<Link href={url} />}
|
||||
asChild
|
||||
isActive={isCurrent}
|
||||
className={cn('text-secondary-foreground transition-all')}
|
||||
>
|
||||
<span className="block max-w-full truncate">{label}</span>
|
||||
<Link href={url}>
|
||||
<span className="block max-w-full truncate">{label}</span>
|
||||
|
||||
{children}
|
||||
{children}
|
||||
</Link>
|
||||
</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),
|
||||
isRouteActive(prefix + '/' + child.url, currentPath, false),
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -10,12 +10,12 @@ import {
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuSub,
|
||||
} from '@kit/ui/sidebar';
|
||||
} from '@kit/ui/shadcn-sidebar';
|
||||
|
||||
import { DocsNavLink } from '~/(marketing)/docs/_components/docs-nav-link';
|
||||
import { DocsNavigationCollapsible } from '~/(marketing)/docs/_components/docs-navigation-collapsible';
|
||||
|
||||
import { FloatingDocumentationNavigationButton } from './floating-docs-navigation-button';
|
||||
import { FloatingDocumentationNavigation } from './floating-docs-navigation';
|
||||
|
||||
function Node({
|
||||
node,
|
||||
@@ -85,11 +85,13 @@ function NodeTrigger({
|
||||
}) {
|
||||
if (node.collapsible) {
|
||||
return (
|
||||
<CollapsibleTrigger render={<SidebarMenuItem />}>
|
||||
<SidebarMenuButton>
|
||||
{label}
|
||||
<ChevronDown className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-180" />
|
||||
</SidebarMenuButton>
|
||||
<CollapsibleTrigger asChild>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
{label}
|
||||
<ChevronDown className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-180" />
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</CollapsibleTrigger>
|
||||
);
|
||||
}
|
||||
@@ -135,10 +137,12 @@ export function DocsNavigation({
|
||||
return (
|
||||
<>
|
||||
<Sidebar
|
||||
variant={'sidebar'}
|
||||
className={'sticky z-1 mt-4 max-h-full overflow-y-auto pr-4'}
|
||||
variant={'ghost'}
|
||||
className={
|
||||
'border-border/50 sticky z-1 mt-4 max-h-full overflow-y-auto pr-4'
|
||||
}
|
||||
>
|
||||
<SidebarGroup>
|
||||
<SidebarGroup className="p-0">
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu className={'pb-48'}>
|
||||
<Tree pages={pages} level={0} prefix={prefix} />
|
||||
@@ -147,7 +151,17 @@ export function DocsNavigation({
|
||||
</SidebarGroup>
|
||||
</Sidebar>
|
||||
|
||||
<FloatingDocumentationNavigationButton />
|
||||
<div className={'lg:hidden'}>
|
||||
<FloatingDocumentationNavigation>
|
||||
<SidebarGroup>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
<Tree pages={pages} level={0} prefix={prefix} />
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
</FloatingDocumentationNavigation>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
'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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getLocale } from 'next-intl/server';
|
||||
import { SidebarProvider } from '@kit/ui/shadcn-sidebar';
|
||||
|
||||
import { SidebarProvider } from '@kit/ui/sidebar';
|
||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||
|
||||
// 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 locale = await getLocale();
|
||||
const docs = await getDocs(locale);
|
||||
const { resolvedLanguage } = await createI18nServerInstance();
|
||||
const docs = await getDocs(resolvedLanguage);
|
||||
const tree = buildDocumentationTree(docs);
|
||||
|
||||
return (
|
||||
@@ -1,21 +1,21 @@
|
||||
import { getLocale, getTranslations } from 'next-intl/server';
|
||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
import { SitePageHeader } from '../_components/site-page-header';
|
||||
import { DocsCards } from './_components/docs-cards';
|
||||
import { getDocs } from './_lib/server/docs.loader';
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
const t = await getTranslations('marketing');
|
||||
const { t } = await createI18nServerInstance();
|
||||
|
||||
return {
|
||||
title: t('documentation'),
|
||||
title: t('marketing:documentation'),
|
||||
};
|
||||
};
|
||||
|
||||
async function DocsPage() {
|
||||
const t = await getTranslations('marketing');
|
||||
const locale = await getLocale();
|
||||
const items = await getDocs(locale);
|
||||
const { t, resolvedLanguage } = await createI18nServerInstance();
|
||||
const items = await getDocs(resolvedLanguage);
|
||||
|
||||
// Filter out any docs that have a parentId, as these are children of other docs
|
||||
const cards = items.filter((item) => !item.parentId);
|
||||
@@ -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('documentation')}
|
||||
subtitle={t('documentationSubtitle')}
|
||||
title={t('marketing:documentation')}
|
||||
subtitle={t('marketing:documentationSubtitle')}
|
||||
/>
|
||||
|
||||
<div className={'relative flex size-full justify-center overflow-y-auto'}>
|
||||
@@ -34,4 +34,4 @@ async function DocsPage() {
|
||||
);
|
||||
}
|
||||
|
||||
export default DocsPage;
|
||||
export default withI18n(DocsPage);
|
||||
@@ -1,30 +1,31 @@
|
||||
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 getTranslations('marketing');
|
||||
const { t } = await createI18nServerInstance();
|
||||
|
||||
return {
|
||||
title: t('faq'),
|
||||
title: t('marketing:faq'),
|
||||
};
|
||||
};
|
||||
|
||||
async function FAQPage() {
|
||||
const t = await getTranslations('marketing');
|
||||
const { t } = await createI18nServerInstance();
|
||||
|
||||
// replace this content with translations
|
||||
const faqItems = [
|
||||
{
|
||||
// or: t('faq.question1')
|
||||
// or: t('marketing:faq.question1')
|
||||
question: `Do you offer a free trial?`,
|
||||
// or: t('faq.answer1')
|
||||
// or: t('marketing: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.`,
|
||||
},
|
||||
{
|
||||
@@ -73,7 +74,10 @@ async function FAQPage() {
|
||||
/>
|
||||
|
||||
<div className={'flex flex-col space-y-4 xl:space-y-8'}>
|
||||
<SitePageHeader title={t('faq')} subtitle={t('faqSubtitle')} />
|
||||
<SitePageHeader
|
||||
title={t('marketing:faq')}
|
||||
subtitle={t('marketing: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">
|
||||
@@ -83,16 +87,14 @@ async function FAQPage() {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button
|
||||
nativeButton={false}
|
||||
render={<Link href={'/contact'} />}
|
||||
variant={'link'}
|
||||
>
|
||||
<span>
|
||||
<Trans i18nKey={'marketing.contactFaq'} />
|
||||
</span>
|
||||
<Button asChild variant={'outline'}>
|
||||
<Link href={'/contact'}>
|
||||
<span>
|
||||
<Trans i18nKey={'marketing:contactFaq'} />
|
||||
</span>
|
||||
|
||||
<ArrowRight className={'ml-2 w-4'} />
|
||||
<ArrowRight className={'ml-2 w-4'} />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -101,7 +103,7 @@ async function FAQPage() {
|
||||
);
|
||||
}
|
||||
|
||||
export default FAQPage;
|
||||
export default withI18n(FAQPage);
|
||||
|
||||
function FaqItem({
|
||||
item,
|
||||
@@ -3,6 +3,7 @@ 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();
|
||||
@@ -19,4 +20,4 @@ async function SiteLayout(props: React.PropsWithChildren) {
|
||||
);
|
||||
}
|
||||
|
||||
export default SiteLayout;
|
||||
export default withI18n(SiteLayout);
|
||||
@@ -20,6 +20,7 @@ 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 (
|
||||
@@ -29,13 +30,11 @@ function Home() {
|
||||
pill={
|
||||
<Pill label={'New'}>
|
||||
<span>The SaaS Starter Kit for ambitious developers</span>
|
||||
<PillActionButton
|
||||
render={
|
||||
<Link href={'/auth/sign-up'}>
|
||||
<ArrowRightIcon className={'h-4 w-4'} />
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
<PillActionButton asChild>
|
||||
<Link href={'/auth/sign-up'}>
|
||||
<ArrowRightIcon className={'h-4 w-4'} />
|
||||
</Link>
|
||||
</PillActionButton>
|
||||
</Pill>
|
||||
}
|
||||
title={
|
||||
@@ -171,7 +170,7 @@ function Home() {
|
||||
);
|
||||
}
|
||||
|
||||
export default Home;
|
||||
export default withI18n(Home);
|
||||
|
||||
function MainCallToActionButton() {
|
||||
return (
|
||||
@@ -180,7 +179,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
|
||||
@@ -195,7 +194,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 getTranslations('marketing');
|
||||
const { t } = await createI18nServerInstance();
|
||||
|
||||
return {
|
||||
title: t('pricing'),
|
||||
title: t('marketing:pricing'),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -20,11 +20,14 @@ const paths = {
|
||||
};
|
||||
|
||||
async function PricingPage() {
|
||||
const t = await getTranslations('marketing');
|
||||
const { t } = await createI18nServerInstance();
|
||||
|
||||
return (
|
||||
<div className={'flex flex-col space-y-8'}>
|
||||
<SitePageHeader title={t('pricing')} subtitle={t('pricingSubtitle')} />
|
||||
<SitePageHeader
|
||||
title={t('marketing:pricing')}
|
||||
subtitle={t('marketing:pricingSubtitle')}
|
||||
/>
|
||||
|
||||
<div className={'container mx-auto pb-8 xl:pb-16'}>
|
||||
<PricingTable paths={paths} config={billingConfig} />
|
||||
@@ -33,4 +36,4 @@ async function PricingPage() {
|
||||
);
|
||||
}
|
||||
|
||||
export default PricingPage;
|
||||
export default withI18n(PricingPage);
|
||||
@@ -1,30 +0,0 @@
|
||||
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;
|
||||
@@ -1,30 +0,0 @@
|
||||
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;
|
||||
@@ -1,30 +0,0 @@
|
||||
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;
|
||||
@@ -1,22 +0,0 @@
|
||||
'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,40 +0,0 @@
|
||||
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,22 +0,0 @@
|
||||
'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,105 +0,0 @@
|
||||
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;
|
||||
@@ -1,29 +0,0 @@
|
||||
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,46 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
|
||||
import { BillingPortalCard } from '@kit/billing-gateway/components';
|
||||
|
||||
import { createBillingPortalSession } from '../_lib/server/server-actions';
|
||||
|
||||
export function TeamBillingPortalForm({
|
||||
accountId,
|
||||
slug,
|
||||
}: {
|
||||
accountId: string;
|
||||
slug: string;
|
||||
}) {
|
||||
const { execute } = useAction(createBillingPortalSession);
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
execute({ accountId, slug });
|
||||
}}
|
||||
>
|
||||
<BillingPortalCard />
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
import { PlusCircle } from 'lucide-react';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
import {
|
||||
AccountInvitationsTable,
|
||||
AccountMembersTable,
|
||||
InviteMembersDialogContainer,
|
||||
} from '@kit/team-accounts/components';
|
||||
import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@kit/ui/card';
|
||||
import { If } from '@kit/ui/if';
|
||||
import { PageBody } from '@kit/ui/page';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
// local imports
|
||||
import { TeamAccountLayoutPageHeader } from '../_components/team-account-layout-page-header';
|
||||
import { loadMembersPageData } from './_lib/server/members-page.loader';
|
||||
|
||||
interface TeamAccountMembersPageProps {
|
||||
params: Promise<{ account: string }>;
|
||||
}
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
const t = await getTranslations('teams');
|
||||
const title = t('members.pageTitle');
|
||||
|
||||
return {
|
||||
title,
|
||||
};
|
||||
};
|
||||
|
||||
async function TeamAccountMembersPage({ params }: TeamAccountMembersPageProps) {
|
||||
const client = getSupabaseServerClient();
|
||||
const slug = (await params).account;
|
||||
|
||||
const [members, invitations, canAddMember, { user, account }] =
|
||||
await loadMembersPageData(client, slug);
|
||||
|
||||
const canManageRoles = account.permissions.includes('roles.manage');
|
||||
const canManageInvitations = account.permissions.includes('invites.manage');
|
||||
|
||||
const isPrimaryOwner = account.primary_owner_user_id === user.id;
|
||||
const currentUserRoleHierarchy = account.role_hierarchy_level;
|
||||
|
||||
return (
|
||||
<PageBody>
|
||||
<TeamAccountLayoutPageHeader
|
||||
title={<Trans i18nKey={'common.routes.members'} />}
|
||||
description={<AppBreadcrumbs />}
|
||||
account={account.slug}
|
||||
/>
|
||||
|
||||
<div className={'flex w-full max-w-4xl flex-col space-y-4 pb-32'}>
|
||||
<Card>
|
||||
<CardHeader className={'flex flex-row justify-between'}>
|
||||
<div className={'flex flex-col space-y-1.5'}>
|
||||
<CardTitle>
|
||||
<Trans i18nKey={'common.accountMembers'} />
|
||||
</CardTitle>
|
||||
|
||||
<CardDescription>
|
||||
<Trans i18nKey={'common.membersTabDescription'} />
|
||||
</CardDescription>
|
||||
</div>
|
||||
|
||||
<If condition={canManageInvitations && canAddMember}>
|
||||
<InviteMembersDialogContainer
|
||||
userRoleHierarchy={currentUserRoleHierarchy}
|
||||
accountSlug={account.slug}
|
||||
>
|
||||
<Button size={'sm'} data-test={'invite-members-form-trigger'}>
|
||||
<PlusCircle className={'w-4'} />
|
||||
|
||||
<span>
|
||||
<Trans i18nKey={'teams.inviteMembersButton'} />
|
||||
</span>
|
||||
</Button>
|
||||
</InviteMembersDialogContainer>
|
||||
</If>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<AccountMembersTable
|
||||
userRoleHierarchy={currentUserRoleHierarchy}
|
||||
currentUserId={user.id}
|
||||
currentAccountId={account.id}
|
||||
members={members}
|
||||
isPrimaryOwner={isPrimaryOwner}
|
||||
canManageRoles={canManageRoles}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className={'flex flex-row justify-between'}>
|
||||
<div className={'flex flex-col space-y-1.5'}>
|
||||
<CardTitle>
|
||||
<Trans i18nKey={'teams.pendingInvitesHeading'} />
|
||||
</CardTitle>
|
||||
|
||||
<CardDescription>
|
||||
<Trans i18nKey={'teams.pendingInvitesDescription'} />
|
||||
</CardDescription>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<AccountInvitationsTable
|
||||
permissions={{
|
||||
canUpdateInvitation: canManageRoles,
|
||||
canRemoveInvitation: canManageRoles,
|
||||
currentUserRoleHierarchy,
|
||||
}}
|
||||
invitations={invitations}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</PageBody>
|
||||
);
|
||||
}
|
||||
|
||||
export default TeamAccountMembersPage;
|
||||
@@ -1,35 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
BorderedNavigationMenu,
|
||||
BorderedNavigationMenuItem,
|
||||
} from '@kit/ui/bordered-navigation-menu';
|
||||
|
||||
import pathsConfig from '~/config/paths.config';
|
||||
|
||||
export function SettingsSubNavigation(props: { account: string }) {
|
||||
const settingsPath = pathsConfig.app.accountSettings.replace(
|
||||
'[account]',
|
||||
props.account,
|
||||
);
|
||||
|
||||
const profilePath = pathsConfig.app.accountProfileSettings.replace(
|
||||
'[account]',
|
||||
props.account,
|
||||
);
|
||||
|
||||
return (
|
||||
<BorderedNavigationMenu>
|
||||
<BorderedNavigationMenuItem
|
||||
path={settingsPath}
|
||||
label={'common.routes.settings'}
|
||||
highlightMatch={`/home/${props.account}/settings$`}
|
||||
/>
|
||||
|
||||
<BorderedNavigationMenuItem
|
||||
path={profilePath}
|
||||
label={'common.routes.profile'}
|
||||
/>
|
||||
</BorderedNavigationMenu>
|
||||
);
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs';
|
||||
import { PageBody } from '@kit/ui/page';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import featuresFlagConfig from '~/config/feature-flags.config';
|
||||
|
||||
import { TeamAccountLayoutPageHeader } from '../_components/team-account-layout-page-header';
|
||||
import { SettingsSubNavigation } from './_components/settings-sub-navigation';
|
||||
|
||||
interface SettingsLayoutProps {
|
||||
children: React.ReactNode;
|
||||
params: Promise<{ account: string }>;
|
||||
}
|
||||
|
||||
async function SettingsLayout({ children, params }: SettingsLayoutProps) {
|
||||
const { account } = await params;
|
||||
|
||||
return (
|
||||
<PageBody>
|
||||
<div>
|
||||
<TeamAccountLayoutPageHeader
|
||||
account={account}
|
||||
title={<Trans i18nKey={'teams.settings.pageTitle'} />}
|
||||
description={<AppBreadcrumbs />}
|
||||
/>
|
||||
|
||||
{featuresFlagConfig.enableTeamsOnly && (
|
||||
<div className="mb-8">
|
||||
<SettingsSubNavigation account={account} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{children}
|
||||
</PageBody>
|
||||
);
|
||||
}
|
||||
|
||||
export default SettingsLayout;
|
||||
@@ -1,66 +0,0 @@
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
|
||||
import { PersonalAccountSettingsContainer } from '@kit/accounts/personal-account-settings';
|
||||
|
||||
import authConfig from '~/config/auth.config';
|
||||
import featureFlagsConfig from '~/config/feature-flags.config';
|
||||
import pathsConfig from '~/config/paths.config';
|
||||
import { requireUserInServerComponent } from '~/lib/server/require-user-in-server-component';
|
||||
|
||||
const showEmailOption =
|
||||
authConfig.providers.password ||
|
||||
authConfig.providers.magicLink ||
|
||||
authConfig.providers.otp;
|
||||
|
||||
const features = {
|
||||
showLinkEmailOption: showEmailOption,
|
||||
enablePasswordUpdate: authConfig.providers.password,
|
||||
enableAccountDeletion: featureFlagsConfig.enableAccountDeletion,
|
||||
enableAccountLinking: authConfig.enableIdentityLinking,
|
||||
};
|
||||
|
||||
const providers = authConfig.providers.oAuth;
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
const t = await getTranslations('account');
|
||||
const title = t('settingsTab');
|
||||
|
||||
return {
|
||||
title,
|
||||
};
|
||||
};
|
||||
|
||||
interface TeamProfileSettingsPageProps {
|
||||
params: Promise<{ account: string }>;
|
||||
}
|
||||
|
||||
async function TeamProfileSettingsPage({
|
||||
params,
|
||||
}: TeamProfileSettingsPageProps) {
|
||||
const [user, { account }] = await Promise.all([
|
||||
requireUserInServerComponent(),
|
||||
params,
|
||||
]);
|
||||
|
||||
const profilePath = pathsConfig.app.accountProfileSettings.replace(
|
||||
'[account]',
|
||||
account,
|
||||
);
|
||||
|
||||
const paths = {
|
||||
callback: pathsConfig.auth.callback + `?next=${profilePath}`,
|
||||
};
|
||||
|
||||
return (
|
||||
<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 TeamProfileSettingsPage;
|
||||
@@ -1,31 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { CreateTeamAccountForm } from '@kit/team-accounts/components';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@kit/ui/card';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
export function CreateFirstTeamForm() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
<Trans i18nKey={'teams.createFirstTeamHeading'} />
|
||||
</CardTitle>
|
||||
|
||||
<CardDescription>
|
||||
<Trans i18nKey={'teams.createFirstTeamDescription'} />
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<CreateTeamAccountForm submitLabel={'teams.getStarted'} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import { createAccountsApi } from '@kit/accounts/api';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
|
||||
import { AppLogo } from '~/components/app-logo';
|
||||
import featuresFlagConfig from '~/config/feature-flags.config';
|
||||
import pathsConfig from '~/config/paths.config';
|
||||
import { requireUserInServerComponent } from '~/lib/server/require-user-in-server-component';
|
||||
|
||||
import { CreateFirstTeamForm } from './_components/create-first-team-form';
|
||||
|
||||
async function CreateTeamPage() {
|
||||
const data = await loadData();
|
||||
|
||||
if (data.redirectTo) {
|
||||
redirect(data.redirectTo);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col items-center justify-center gap-y-8">
|
||||
<AppLogo />
|
||||
|
||||
<CreateFirstTeamForm />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreateTeamPage;
|
||||
|
||||
async function loadData() {
|
||||
await requireUserInServerComponent();
|
||||
|
||||
if (!featuresFlagConfig.enableTeamsOnly) {
|
||||
return { redirectTo: pathsConfig.app.home };
|
||||
}
|
||||
|
||||
const client = getSupabaseServerClient();
|
||||
const api = createAccountsApi(client);
|
||||
const accounts = await api.loadUserAccounts();
|
||||
|
||||
if (accounts.length > 0 && accounts[0]?.value) {
|
||||
return {
|
||||
redirectTo: pathsConfig.app.accountHome.replace(
|
||||
'[account]',
|
||||
accounts[0].value,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
return { redirectTo: null };
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
import { headers } from 'next/headers';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
import { hasLocale } from 'next-intl';
|
||||
import { getMessages } from 'next-intl/server';
|
||||
import { PublicEnvScript } from 'next-runtime-env';
|
||||
|
||||
import { routing } from '@kit/i18n/routing';
|
||||
import { Toaster } from '@kit/ui/sonner';
|
||||
import { cn } from '@kit/ui/utils';
|
||||
|
||||
import { RootProviders } from '~/components/root-providers';
|
||||
import { getFontsClassName } from '~/lib/fonts';
|
||||
import { generateRootMetadata } from '~/lib/root-metadata';
|
||||
import { getRootTheme } from '~/lib/root-theme';
|
||||
|
||||
export const generateMetadata = () => {
|
||||
return generateRootMetadata();
|
||||
};
|
||||
|
||||
interface LocaleLayoutProps {
|
||||
children: React.ReactNode;
|
||||
params: Promise<{ locale: string }>;
|
||||
}
|
||||
|
||||
export default async function LocaleLayout({
|
||||
children,
|
||||
params,
|
||||
}: LocaleLayoutProps) {
|
||||
const { locale } = await params;
|
||||
|
||||
if (!hasLocale(routing.locales, locale)) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const [theme, nonce, messages] = await Promise.all([
|
||||
getRootTheme(),
|
||||
getCspNonce(),
|
||||
getMessages({ locale }),
|
||||
]);
|
||||
|
||||
const className = getRootClassName(theme);
|
||||
|
||||
return (
|
||||
<html lang={locale} className={className} suppressHydrationWarning>
|
||||
<head>
|
||||
<PublicEnvScript nonce={nonce} />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<RootProviders
|
||||
theme={theme}
|
||||
locale={locale}
|
||||
nonce={nonce}
|
||||
messages={messages}
|
||||
>
|
||||
{children}
|
||||
|
||||
<Toaster richColors={true} theme={theme} position="top-center" />
|
||||
</RootProviders>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
function getRootClassName(theme: string) {
|
||||
const fontsClassName = getFontsClassName(theme);
|
||||
|
||||
return cn(
|
||||
'bg-background min-h-screen antialiased md:overscroll-y-none',
|
||||
fontsClassName,
|
||||
);
|
||||
}
|
||||
|
||||
async function getCspNonce() {
|
||||
const headersStore = await headers();
|
||||
|
||||
return headersStore.get('x-nonce') ?? undefined;
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
|
||||
import { ErrorPageContent } from '~/components/error-page-content';
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
const t = await getTranslations('common');
|
||||
const title = t('notFound');
|
||||
|
||||
return {
|
||||
title,
|
||||
};
|
||||
};
|
||||
|
||||
const NotFoundPage = async () => {
|
||||
return (
|
||||
<div className={'flex h-screen flex-1 flex-col'}>
|
||||
<ErrorPageContent
|
||||
statusCode={'common.pageNotFoundHeading'}
|
||||
heading={'common.pageNotFound'}
|
||||
subtitle={'common.pageNotFoundSubHeading'}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotFoundPage;
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
SidebarHeader,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
} from '@kit/ui/sidebar';
|
||||
} from '@kit/ui/shadcn-sidebar';
|
||||
|
||||
import { AppLogo } from '~/components/app-logo';
|
||||
import { ProfileAccountDropdownContainer } from '~/components/personal-account-dropdown-container';
|
||||
@@ -35,26 +35,25 @@ export function AdminSidebar() {
|
||||
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuButton
|
||||
isActive={path === '/admin'}
|
||||
render={<Link className={'flex gap-2.5'} href={'/admin'} />}
|
||||
>
|
||||
<LayoutDashboard className={'h-4'} />
|
||||
<span>Dashboard</span>
|
||||
<SidebarMenuButton isActive={path === '/admin'} asChild>
|
||||
<Link className={'flex gap-2.5'} href={'/admin'}>
|
||||
<LayoutDashboard className={'h-4'} />
|
||||
<span>Dashboard</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
|
||||
<SidebarMenuButton
|
||||
isActive={path.includes('/admin/accounts')}
|
||||
render={
|
||||
<Link
|
||||
className={'flex size-full gap-2.5'}
|
||||
href={'/admin/accounts'}
|
||||
>
|
||||
<Users className={'h-4'} />
|
||||
<span>Accounts</span>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
asChild
|
||||
>
|
||||
<Link
|
||||
className={'flex size-full gap-2.5'}
|
||||
href={'/admin/accounts'}
|
||||
>
|
||||
<Users className={'h-4'} />
|
||||
<span>Accounts</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</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/sidebar';
|
||||
import { SidebarProvider } from '@kit/ui/shadcn-sidebar';
|
||||
|
||||
import { AdminSidebar } from '~/admin/_components/admin-sidebar';
|
||||
import { AdminMobileNavigation } from '~/admin/_components/mobile-navigation';
|
||||
@@ -8,6 +8,7 @@ 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<{
|
||||
@@ -27,11 +28,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>
|
||||
|
||||
@@ -52,7 +53,6 @@ function AuthCallbackForm(props: {
|
||||
switch (props.code) {
|
||||
case 'otp_expired':
|
||||
return <ResendAuthLinkForm redirectPath={props.redirectPath} />;
|
||||
|
||||
default:
|
||||
return <SignInButton signInPath={props.signInPath} />;
|
||||
}
|
||||
@@ -60,15 +60,12 @@ function AuthCallbackForm(props: {
|
||||
|
||||
function SignInButton(props: { signInPath: string }) {
|
||||
return (
|
||||
<Button
|
||||
className={'w-full'}
|
||||
render={
|
||||
<Link href={props.signInPath}>
|
||||
<Trans i18nKey={'auth.signIn'} />
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
<Button className={'w-full'} asChild>
|
||||
<Link href={props.signInPath}>
|
||||
<Trans i18nKey={'auth:signIn'} />
|
||||
</Link>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export default AuthCallbackErrorPage;
|
||||
export default withI18n(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 getTranslations('auth');
|
||||
const { t } = await createI18nServerInstance();
|
||||
|
||||
return {
|
||||
title: t('passwordResetLabel'),
|
||||
title: t('auth: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,20 +37,15 @@ function PasswordResetPage() {
|
||||
<PasswordResetRequestContainer redirectPath={redirectPath} />
|
||||
|
||||
<div className={'flex justify-center text-xs'}>
|
||||
<Button
|
||||
nativeButton={false}
|
||||
variant={'link'}
|
||||
size={'sm'}
|
||||
render={
|
||||
<Link href={signIn}>
|
||||
<Trans i18nKey={'auth.passwordRecoveredQuestion'} />
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
<Button asChild variant={'link'} size={'sm'}>
|
||||
<Link href={signIn}>
|
||||
<Trans i18nKey={'auth:passwordRecoveredQuestion'} />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default PasswordResetPage;
|
||||
export default withI18n(PasswordResetPage);
|
||||
@@ -1,7 +1,5 @@
|
||||
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';
|
||||
@@ -10,6 +8,8 @@ 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 t = await getTranslations('auth');
|
||||
const i18n = await createI18nServerInstance();
|
||||
|
||||
return {
|
||||
title: t('signIn'),
|
||||
title: i18n.t('auth: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,19 +53,14 @@ async function SignInPage({ searchParams }: SignInPageProps) {
|
||||
/>
|
||||
|
||||
<div className={'flex justify-center'}>
|
||||
<Button
|
||||
nativeButton={false}
|
||||
variant={'link'}
|
||||
size={'sm'}
|
||||
render={
|
||||
<Link href={pathsConfig.auth.signUp} prefetch={true}>
|
||||
<Trans i18nKey={'auth.doNotHaveAccountYet'} />
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
<Button asChild variant={'link'} size={'sm'}>
|
||||
<Link href={pathsConfig.auth.signUp} prefetch={true}>
|
||||
<Trans i18nKey={'auth:doNotHaveAccountYet'} />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default SignInPage;
|
||||
export default withI18n(SignInPage);
|
||||
@@ -1,7 +1,5 @@
|
||||
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';
|
||||
@@ -9,12 +7,14 @@ 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 t = await getTranslations('auth');
|
||||
const i18n = await createI18nServerInstance();
|
||||
|
||||
return {
|
||||
title: t('signUp'),
|
||||
title: i18n.t('auth: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,19 +44,14 @@ async function SignUpPage() {
|
||||
/>
|
||||
|
||||
<div className={'flex justify-center'}>
|
||||
<Button
|
||||
render={
|
||||
<Link href={pathsConfig.auth.signIn} prefetch={true}>
|
||||
<Trans i18nKey={'auth.alreadyHaveAnAccount'} />
|
||||
</Link>
|
||||
}
|
||||
variant={'link'}
|
||||
size={'sm'}
|
||||
nativeButton={false}
|
||||
/>
|
||||
<Button asChild variant={'link'} size={'sm'}>
|
||||
<Link href={pathsConfig.auth.signIn} prefetch={true}>
|
||||
<Trans i18nKey={'auth:alreadyHaveAnAccount'} />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default SignUpPage;
|
||||
export default withI18n(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 t = await getTranslations('auth');
|
||||
const i18n = await createI18nServerInstance();
|
||||
|
||||
return {
|
||||
title: t('signIn'),
|
||||
title: i18n.t('auth:signIn'),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -51,4 +51,4 @@ async function VerifyPage(props: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
export default VerifyPage;
|
||||
export default withI18n(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>
|
||||
@@ -19,7 +19,7 @@ const GlobalErrorPage = ({
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<RootProviders messages={{}}>
|
||||
<RootProviders>
|
||||
<GlobalErrorContent reset={reset} />
|
||||
</RootProviders>
|
||||
</body>
|
||||
@@ -35,10 +35,10 @@ function GlobalErrorContent({ reset }: { reset: () => void }) {
|
||||
<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/sidebar';
|
||||
import { SidebarContext } from '@kit/ui/shadcn-sidebar';
|
||||
|
||||
import featureFlagsConfig from '~/config/feature-flags.config';
|
||||
import pathsConfig from '~/config/paths.config';
|
||||
@@ -22,6 +22,7 @@ export function HomeAccountSelector(props: {
|
||||
}>;
|
||||
|
||||
userId: string;
|
||||
collisionPadding?: number;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const context = useContext(SidebarContext);
|
||||
@@ -29,6 +30,7 @@ export function HomeAccountSelector(props: {
|
||||
return (
|
||||
<AccountSelector
|
||||
collapsed={!context?.open}
|
||||
collisionPadding={props.collisionPadding ?? 20}
|
||||
accounts={props.accounts}
|
||||
features={features}
|
||||
userId={props.userId}
|
||||
@@ -31,16 +31,13 @@ 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}
|
||||
render={
|
||||
<Link href={`/home/${account.value}`}>
|
||||
<CardButtonHeader>
|
||||
<CardButtonTitle>{account.label}</CardButtonTitle>
|
||||
</CardButtonHeader>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
<CardButton key={account.value} asChild>
|
||||
<Link href={`/home/${account.value}`}>
|
||||
<CardButtonHeader>
|
||||
<CardButtonTitle>{account.label}</CardButtonTitle>
|
||||
</CardButtonHeader>
|
||||
</Link>
|
||||
</CardButton>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -53,21 +50,17 @@ function HomeAccountsListEmptyState(props: {
|
||||
return (
|
||||
<div className={'flex flex-1'}>
|
||||
<EmptyState>
|
||||
<EmptyStateButton
|
||||
render={
|
||||
<HomeAddAccountButton
|
||||
className={'mt-4'}
|
||||
canCreateTeamAccount={props.canCreateTeamAccount}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<EmptyStateButton asChild>
|
||||
<HomeAddAccountButton
|
||||
className={'mt-4'}
|
||||
canCreateTeamAccount={props.canCreateTeamAccount}
|
||||
/>
|
||||
</EmptyStateButton>
|
||||
<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,10 +41,9 @@ export function HomeAddAccountButton(props: HomeAddAccountButtonProps) {
|
||||
{!canCreate && reason ? (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={<span className="cursor-not-allowed">{button}</span>}
|
||||
/>
|
||||
|
||||
<TooltipTrigger asChild>
|
||||
<span className="cursor-not-allowed">{button}</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<Trans i18nKey={reason} defaults={reason} />
|
||||
</TooltipContent>
|
||||
@@ -56,12 +56,13 @@ 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>
|
||||
|
||||
@@ -86,21 +87,18 @@ function DropdownLink(
|
||||
}>,
|
||||
) {
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
render={
|
||||
<Link
|
||||
href={props.path}
|
||||
className={'flex h-12 w-full items-center space-x-4'}
|
||||
>
|
||||
{props.Icon}
|
||||
<DropdownMenuItem asChild key={props.path}>
|
||||
<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>
|
||||
}
|
||||
key={props.path}
|
||||
/>
|
||||
<span>
|
||||
<Trans i18nKey={props.label} defaults={props.label} />
|
||||
</span>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -117,7 +115,7 @@ function SignOutDropdownItem(
|
||||
<LogOut className={'h-6'} />
|
||||
|
||||
<span>
|
||||
<Trans i18nKey={'common.signOut'} defaults={'Sign out'} />
|
||||
<Trans i18nKey={'common:signOut'} defaults={'Sign out'} />
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
61
apps/web/app/home/(user)/_components/home-sidebar.tsx
Normal file
61
apps/web/app/home/(user)/_components/home-sidebar.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import { If } from '@kit/ui/if';
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarHeader,
|
||||
SidebarNavigation,
|
||||
} from '@kit/ui/shadcn-sidebar';
|
||||
import { cn } from '@kit/ui/utils';
|
||||
|
||||
import { AppLogo } from '~/components/app-logo';
|
||||
import { ProfileAccountDropdownContainer } from '~/components/personal-account-dropdown-container';
|
||||
import featuresFlagConfig from '~/config/feature-flags.config';
|
||||
import { personalAccountNavigationConfig } from '~/config/personal-account-navigation.config';
|
||||
import { UserNotifications } from '~/home/(user)/_components/user-notifications';
|
||||
|
||||
// home imports
|
||||
import type { UserWorkspace } from '../_lib/server/load-user-workspace';
|
||||
import { HomeAccountSelector } from './home-account-selector';
|
||||
|
||||
interface HomeSidebarProps {
|
||||
workspace: UserWorkspace;
|
||||
}
|
||||
|
||||
export function HomeSidebar(props: HomeSidebarProps) {
|
||||
const { workspace, user, accounts } = props.workspace;
|
||||
const collapsible = personalAccountNavigationConfig.sidebarCollapsedStyle;
|
||||
|
||||
return (
|
||||
<Sidebar collapsible={collapsible}>
|
||||
<SidebarHeader className={'h-16 justify-center'}>
|
||||
<div className={'flex items-center justify-between gap-x-3'}>
|
||||
<If
|
||||
condition={featuresFlagConfig.enableTeamAccounts}
|
||||
fallback={
|
||||
<AppLogo
|
||||
className={cn(
|
||||
'p-2 group-data-[minimized=true]/sidebar:max-w-full group-data-[minimized=true]/sidebar:py-0',
|
||||
)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<HomeAccountSelector userId={user.id} accounts={accounts} />
|
||||
</If>
|
||||
|
||||
<div className={'group-data-[minimized=true]/sidebar:hidden'}>
|
||||
<UserNotifications userId={user.id} />
|
||||
</div>
|
||||
</div>
|
||||
</SidebarHeader>
|
||||
|
||||
<SidebarContent>
|
||||
<SidebarNavigation config={personalAccountNavigationConfig} />
|
||||
</SidebarContent>
|
||||
|
||||
<SidebarFooter>
|
||||
<ProfileAccountDropdownContainer user={user} account={workspace} />
|
||||
</SidebarFooter>
|
||||
</Sidebar>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useState, useTransition } from 'react';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
import { TriangleAlert } from 'lucide-react';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { ExclamationTriangleIcon } from '@radix-ui/react-icons';
|
||||
|
||||
import { PlanPicker } from '@kit/billing-gateway/components';
|
||||
import { useAppEvents } from '@kit/shared/events';
|
||||
@@ -40,6 +39,7 @@ const EmbeddedCheckout = dynamic(
|
||||
export function PersonalAccountCheckoutForm(props: {
|
||||
customerId: string | null | undefined;
|
||||
}) {
|
||||
const [pending, startTransition] = useTransition();
|
||||
const [error, setError] = useState(false);
|
||||
const appEvents = useAppEvents();
|
||||
|
||||
@@ -47,20 +47,6 @@ 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;
|
||||
|
||||
@@ -81,11 +67,11 @@ export function PersonalAccountCheckoutForm(props: {
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
<Trans i18nKey={'billing.planCardLabel'} />
|
||||
<Trans i18nKey={'common:planCardLabel'} />
|
||||
</CardTitle>
|
||||
|
||||
<CardDescription>
|
||||
<Trans i18nKey={'billing.planCardDescription'} />
|
||||
<Trans i18nKey={'common:planCardDescription'} />
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
@@ -95,18 +81,27 @@ export function PersonalAccountCheckoutForm(props: {
|
||||
</If>
|
||||
|
||||
<PlanPicker
|
||||
pending={isPending}
|
||||
pending={pending}
|
||||
config={billingConfig}
|
||||
canStartTrial={canStartTrial}
|
||||
onSubmit={({ planId, productId }) => {
|
||||
appEvents.emit({
|
||||
type: 'checkout.started',
|
||||
payload: { planId },
|
||||
});
|
||||
startTransition(async () => {
|
||||
try {
|
||||
appEvents.emit({
|
||||
type: 'checkout.started',
|
||||
payload: { planId },
|
||||
});
|
||||
|
||||
execute({
|
||||
planId,
|
||||
productId,
|
||||
const { checkoutToken } =
|
||||
await createPersonalAccountCheckoutSession({
|
||||
planId,
|
||||
productId,
|
||||
});
|
||||
|
||||
setCheckoutToken(checkoutToken);
|
||||
} catch {
|
||||
setError(true);
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
@@ -119,14 +114,14 @@ export function PersonalAccountCheckoutForm(props: {
|
||||
function ErrorAlert() {
|
||||
return (
|
||||
<Alert variant={'destructive'}>
|
||||
<TriangleAlert className={'h-4'} />
|
||||
<ExclamationTriangleIcon className={'h-4'} />
|
||||
|
||||
<AlertTitle>
|
||||
<Trans i18nKey={'common.planPickerAlertErrorTitle'} />
|
||||
<Trans i18nKey={'common:planPickerAlertErrorTitle'} />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
<Trans i18nKey={'common.planPickerAlertErrorDescription'} />
|
||||
<Trans i18nKey={'common:planPickerAlertErrorDescription'} />
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const PersonalAccountCheckoutSchema = z.object({
|
||||
planId: z.string().min(1),
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import { authActionClient } from '@kit/next/safe-action';
|
||||
import { enhanceAction } from '@kit/next/actions';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
|
||||
import featureFlagsConfig from '~/config/feature-flags.config';
|
||||
@@ -20,9 +20,8 @@ const enabled = featureFlagsConfig.enablePersonalAccountBilling;
|
||||
* @name createPersonalAccountCheckoutSession
|
||||
* @description Creates a checkout session for a personal account.
|
||||
*/
|
||||
export const createPersonalAccountCheckoutSession = authActionClient
|
||||
.schema(PersonalAccountCheckoutSchema)
|
||||
.action(async ({ parsedInput: data }) => {
|
||||
export const createPersonalAccountCheckoutSession = enhanceAction(
|
||||
async function (data) {
|
||||
if (!enabled) {
|
||||
throw new Error('Personal account billing is not enabled');
|
||||
}
|
||||
@@ -31,14 +30,18 @@ export const createPersonalAccountCheckoutSession = authActionClient
|
||||
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 =
|
||||
authActionClient.action(async () => {
|
||||
export const createPersonalAccountBillingPortalSession = enhanceAction(
|
||||
async () => {
|
||||
if (!enabled) {
|
||||
throw new Error('Personal account billing is not enabled');
|
||||
}
|
||||
@@ -49,5 +52,7 @@ export const createPersonalAccountBillingPortalSession =
|
||||
// get url to billing portal
|
||||
const url = await service.createBillingPortalSession();
|
||||
|
||||
redirect(url);
|
||||
});
|
||||
return redirect(url);
|
||||
},
|
||||
{},
|
||||
);
|
||||
@@ -2,7 +2,7 @@ import 'server-only';
|
||||
|
||||
import { SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
import * as z from 'zod';
|
||||
import { 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.output<typeof PersonalAccountCheckoutSchema>) {
|
||||
}: z.infer<typeof PersonalAccountCheckoutSchema>) {
|
||||
// get the authenticated user
|
||||
const { data: user, error } = await requireUser(this.client);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user