New Layout (#22)

New layout
This commit is contained in:
Giancarlo Buomprisco
2024-04-30 22:54:33 +07:00
committed by GitHub
parent 9eded69f15
commit 5e8e01e340
80 changed files with 8880 additions and 10095 deletions

View File

@@ -1,54 +0,0 @@
import { use } from 'react';
import { cookies } from 'next/headers';
import { If } from '@kit/ui/if';
import { Sidebar, SidebarContent, SidebarNavigation } from '@kit/ui/sidebar';
import { loadUserWorkspace } from '~/(dashboard)/home/(user)/_lib/server/load-user-workspace';
import { AppLogo } from '~/components/app-logo';
import { ProfileAccountDropdownContainer } from '~/components/personal-account-dropdown-container';
import featuresFlagConfig from '~/config/feature-flags.config';
import { personalAccountSidebarConfig } from '~/config/personal-account-sidebar.config';
// home imports
import { HomeSidebarAccountSelector } from '../_components/home-sidebar-account-selector';
export function HomeSidebar() {
const collapsed = getSidebarCollapsed();
const { accounts, user, workspace } = use(loadUserWorkspace());
return (
<Sidebar collapsed={collapsed}>
<SidebarContent className={'h-16 justify-center'}>
<If
condition={featuresFlagConfig.enableTeamAccounts}
fallback={<AppLogo className={'py-2'} />}
>
<HomeSidebarAccountSelector
collapsed={collapsed}
accounts={accounts}
/>
</If>
</SidebarContent>
<SidebarContent className={`mt-5 h-[calc(100%-160px)] overflow-y-auto`}>
<SidebarNavigation config={personalAccountSidebarConfig} />
</SidebarContent>
<div className={'absolute bottom-4 left-0 w-full'}>
<SidebarContent>
<ProfileAccountDropdownContainer
collapsed={collapsed}
user={user}
account={workspace}
/>
</SidebarContent>
</div>
</Sidebar>
);
}
function getSidebarCollapsed() {
return cookies().get('sidebar-collapsed')?.value === 'true';
}

View File

@@ -1,26 +0,0 @@
import { PageHeader } from '@kit/ui/page';
import { UserNotifications } from '~/(dashboard)/home/(user)/_components/user-notifications';
import { UserLayoutMobileNavigation } from './user-layout-mobile-navigation';
export function UserAccountHeader(
props: React.PropsWithChildren<{
title: string | React.ReactNode;
description?: string | React.ReactNode;
}>,
) {
return (
<PageHeader
title={props.title}
description={props.description}
mobileNavigation={<UserLayoutMobileNavigation />}
>
<div className={'flex space-x-4'}>
{props.children}
<UserNotifications />
</div>
</PageHeader>
);
}

View File

@@ -1,11 +0,0 @@
import { Page } from '@kit/ui/page';
import { withI18n } from '~/lib/i18n/with-i18n';
import { HomeSidebar } from './_components/home-sidebar';
function UserHomeLayout({ children }: React.PropsWithChildren) {
return <Page sidebar={<HomeSidebar />}>{children}</Page>;
}
export default withI18n(UserHomeLayout);

View File

@@ -1,30 +0,0 @@
import { PageHeader } from '@kit/ui/page';
import { AccountNotifications } from '~/(dashboard)/home/[account]/_components/account-notifications';
import { AccountLayoutMobileNavigation } from './account-layout-mobile-navigation';
export function AccountLayoutHeader({
children,
title,
description,
account,
}: React.PropsWithChildren<{
title: string | React.ReactNode;
description?: string | React.ReactNode;
account: string;
}>) {
return (
<PageHeader
title={title}
description={description}
mobileNavigation={<AccountLayoutMobileNavigation account={account} />}
>
<div className={'flex space-x-4'}>
{children}
<AccountNotifications accountId={account} />
</div>
</PageHeader>
);
}

View File

@@ -1,44 +0,0 @@
import { use } from 'react';
import { Page } from '@kit/ui/page';
import { withI18n } from '~/lib/i18n/with-i18n';
import { AccountLayoutSidebar } from './_components/account-layout-sidebar';
import { loadTeamWorkspace } from './_lib/server/team-account-workspace.loader';
interface Params {
account: string;
}
function TeamWorkspaceLayout({
children,
params,
}: React.PropsWithChildren<{
params: Params;
}>) {
const data = use(loadTeamWorkspace(params.account));
const accounts = data.accounts.map(({ name, slug, picture_url }) => ({
label: name,
value: slug,
image: picture_url,
}));
return (
<Page
sidebar={
<AccountLayoutSidebar
collapsed={false}
account={params.account}
accounts={accounts}
user={data?.user ?? null}
/>
}
>
{children}
</Page>
);
}
export default withI18n(TeamWorkspaceLayout);

View File

@@ -1,4 +1,10 @@
import { Page, PageBody, PageHeader } from '@kit/ui/page';
import {
Page,
PageBody,
PageHeader,
PageMobileNavigation,
PageNavigation,
} from '@kit/ui/page';
import { AdminSidebar } from '~/admin/_components/admin-sidebar';
import { AdminMobileNavigation } from '~/admin/_components/mobile-navigation';
@@ -9,13 +15,20 @@ export const metadata = {
export default function AdminLayout(props: React.PropsWithChildren) {
return (
<Page sidebar={<AdminSidebar />}>
<Page style={'sidebar'}>
<PageHeader
mobileNavigation={<AdminMobileNavigation />}
title={'Super Admin'}
description={`Your SaaS stats at a glance`}
/>
<PageNavigation>
<AdminSidebar />
</PageNavigation>
<PageMobileNavigation>
<AdminMobileNavigation />
</PageMobileNavigation>
<PageBody>{props.children}</PageBody>
</Page>
);

View File

@@ -24,7 +24,7 @@ const ErrorPage = ({
<div
className={
'm-auto flex w-full flex-1 flex-col items-center justify-center'
'container m-auto flex w-full flex-1 flex-col items-center justify-center'
}
>
<div className={'flex flex-col items-center space-y-16'}>

View File

@@ -11,7 +11,7 @@ const features = {
enableTeamCreation: featureFlagsConfig.enableTeamCreation,
};
export function HomeSidebarAccountSelector(props: {
export function HomeAccountSelector(props: {
accounts: Array<{
label: string | null;
value: string | null;

View File

@@ -0,0 +1,62 @@
import {
BorderedNavigationMenu,
BorderedNavigationMenuItem,
} from '@kit/ui/bordered-navigation-menu';
import { AppLogo } from '~/components/app-logo';
import { ProfileAccountDropdownContainer } from '~/components/personal-account-dropdown-container';
import { personalAccountNavigationConfig } from '~/config/personal-account-navigation.config';
// home imports
import { HomeAccountSelector } from '../_components/home-account-selector';
import { UserNotifications } from '../_components/user-notifications';
import { type UserWorkspace } from '../_lib/server/load-user-workspace';
export function HomeMenuNavigation(props: { workspace: UserWorkspace }) {
const { workspace, user, accounts } = props.workspace;
const routes = personalAccountNavigationConfig.routes.reduce<
Array<{
path: string;
label: string;
Icon?: React.ReactNode;
end?: boolean | undefined;
}>
>((acc, item) => {
if ('children' in item) {
return [...acc, ...item.children];
}
if ('divider' in item) {
return acc;
}
return [...acc, item];
}, []);
return (
<div className={'flex w-full flex-1 justify-between'}>
<div className={'flex items-center space-x-8'}>
<AppLogo />
<BorderedNavigationMenu>
{routes.map((route) => (
<BorderedNavigationMenuItem {...route} key={route.path} />
))}
</BorderedNavigationMenu>
</div>
<div className={'flex justify-end space-x-2.5'}>
<HomeAccountSelector accounts={accounts} collapsed={false} />
<UserNotifications userId={user.id} />
<ProfileAccountDropdownContainer
collapsed={true}
user={user}
account={workspace}
/>
</div>
</div>
);
}

View File

@@ -8,18 +8,26 @@ import { useSignOut } from '@kit/supabase/hooks/use-sign-out';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@kit/ui/dropdown-menu';
import { If } from '@kit/ui/if';
import { Trans } from '@kit/ui/trans';
import { personalAccountSidebarConfig } from '~/config/personal-account-sidebar.config';
import featuresFlagConfig from '~/config/feature-flags.config';
import { personalAccountNavigationConfig } from '~/config/personal-account-navigation.config';
export function UserLayoutMobileNavigation() {
// home imports
import { HomeAccountSelector } from '../_components/home-account-selector';
import type { UserWorkspace } from '../_lib/server/load-user-workspace';
export function HomeMobileNavigation(props: { workspace: UserWorkspace }) {
const signOut = useSignOut();
const Links = personalAccountSidebarConfig.routes.map((item, index) => {
const Links = personalAccountNavigationConfig.routes.map((item, index) => {
if ('children' in item) {
return item.children.map((child) => {
return (
@@ -54,7 +62,22 @@ export function UserLayoutMobileNavigation() {
</DropdownMenuTrigger>
<DropdownMenuContent sideOffset={10} className={'w-screen rounded-none'}>
{Links}
<If condition={featuresFlagConfig.enableTeamAccounts}>
<DropdownMenuGroup>
<DropdownMenuLabel>
<Trans i18nKey={'common:yourAccounts'} />
</DropdownMenuLabel>
<HomeAccountSelector
accounts={props.workspace.accounts}
collapsed={false}
/>
</DropdownMenuGroup>
<DropdownMenuSeparator />
</If>
<DropdownMenuGroup>{Links}</DropdownMenuGroup>
<DropdownMenuSeparator />

View File

@@ -0,0 +1,14 @@
import { PageHeader } from '@kit/ui/page';
export function HomeLayoutPageHeader(
props: React.PropsWithChildren<{
title: string | React.ReactNode;
description: string | React.ReactNode;
}>,
) {
return (
<PageHeader title={props.title} description={props.description}>
{props.children}
</PageHeader>
);
}

View File

@@ -0,0 +1,47 @@
import { If } from '@kit/ui/if';
import { Sidebar, SidebarContent, SidebarNavigation } from '@kit/ui/sidebar';
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';
export function HomeSidebar(props: { workspace: UserWorkspace }) {
const { workspace, user, accounts } = props.workspace;
return (
<Sidebar>
<SidebarContent className={'h-16 justify-center'}>
<div className={'flex items-center justify-between'}>
<If
condition={featuresFlagConfig.enableTeamAccounts}
fallback={<AppLogo className={'py-2'} />}
>
<HomeAccountSelector collapsed={false} accounts={accounts} />
</If>
<UserNotifications userId={user.id} />
</div>
</SidebarContent>
<SidebarContent className={`mt-5 h-[calc(100%-160px)] overflow-y-auto`}>
<SidebarNavigation config={personalAccountNavigationConfig} />
</SidebarContent>
<div className={'absolute bottom-4 left-0 w-full'}>
<SidebarContent>
<ProfileAccountDropdownContainer
collapsed={false}
user={user}
account={workspace}
/>
</SidebarContent>
</div>
</Sidebar>
);
}

View File

@@ -1,21 +1,15 @@
import { use } from 'react';
import { NotificationsPopover } from '@kit/notifications/components';
import featuresFlagConfig from '~/config/feature-flags.config';
import { loadUserWorkspace } from '../_lib/server/load-user-workspace';
export function UserNotifications() {
const { user } = use(loadUserWorkspace());
export function UserNotifications(props: { userId: string }) {
if (!featuresFlagConfig.enableNotifications) {
return null;
}
return (
<NotificationsPopover
accountIds={[user.id]}
accountIds={[props.userId]}
realtime={featuresFlagConfig.realtimeNotifications}
/>
);

View File

@@ -7,6 +7,8 @@ import featureFlagsConfig from '~/config/feature-flags.config';
const shouldLoadAccounts = featureFlagsConfig.enableTeamAccounts;
export type UserWorkspace = Awaited<ReturnType<typeof loadUserWorkspace>>;
/**
* @name loadUserWorkspace
* @description

View File

@@ -2,6 +2,6 @@
// We reuse the page from the billing module
// as there is no need to create a new one.
import BillingErrorPage from '~/(dashboard)/home/[account]/billing/error';
import BillingErrorPage from '~/home/[account]/billing/error';
export default BillingErrorPage;

View File

@@ -15,10 +15,10 @@ import billingConfig from '~/config/billing.config';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
import { UserAccountHeader } from '../_components/user-account-header';
// local imports
import { HomeLayoutPageHeader } from '../_components/home-page-header';
import { createPersonalAccountBillingPortalSession } from '../billing/_lib/server/server-actions';
import { PersonalAccountCheckoutForm } from './_components/personal-account-checkout-form';
// user billing imports
import { loadPersonalAccountBillingPageData } from './_lib/server/personal-account-billing-page.loader';
export const generateMetadata = async () => {
@@ -44,7 +44,7 @@ async function PersonalAccountBillingPage() {
return (
<>
<UserAccountHeader
<HomeLayoutPageHeader
title={<Trans i18nKey={'common:billingTabLabel'} />}
description={<Trans i18nKey={'common:billingTabDescription'} />}
/>

View File

@@ -1,5 +1,5 @@
// We reuse the page from the billing module
// as there is no need to create a new one.
import ReturnCheckoutSessionPage from '~/(dashboard)/home/[account]/billing/return/page';
import ReturnCheckoutSessionPage from '~/home/[account]/billing/return/page';
export default ReturnCheckoutSessionPage;

View File

@@ -0,0 +1,43 @@
import { use } from 'react';
import { If } from '@kit/ui/if';
import { Page, PageMobileNavigation, PageNavigation } from '@kit/ui/page';
import { AppLogo } from '~/components/app-logo';
import { personalAccountNavigationConfig } from '~/config/personal-account-navigation.config';
import { withI18n } from '~/lib/i18n/with-i18n';
// home imports
import { HomeMenuNavigation } from './_components/home-menu-navigation';
import { HomeMobileNavigation } from './_components/home-mobile-navigation';
import { HomeSidebar } from './_components/home-sidebar';
import { loadUserWorkspace } from './_lib/server/load-user-workspace';
const style = personalAccountNavigationConfig.style;
function UserHomeLayout({ children }: React.PropsWithChildren) {
const workspace = use(loadUserWorkspace());
return (
<Page style={style}>
<PageNavigation>
<If condition={style === 'header'}>
<HomeMenuNavigation workspace={workspace} />
</If>
<If condition={style === 'sidebar'}>
<HomeSidebar workspace={workspace} />
</If>
</PageNavigation>
<PageMobileNavigation className={'flex items-center justify-between'}>
<AppLogo />
<HomeMobileNavigation workspace={workspace} />
</PageMobileNavigation>
{children}
</Page>
);
}
export default withI18n(UserHomeLayout);

View File

@@ -1,10 +1,12 @@
import { PageBody } from '@kit/ui/page';
import { Trans } from '@kit/ui/trans';
import { UserAccountHeader } from '~/(dashboard)/home/(user)/_components/user-account-header';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
// local imports
import { HomeLayoutPageHeader } from './_components/home-page-header';
export const generateMetadata = async () => {
const i18n = await createI18nServerInstance();
const title = i18n.t('account:homePage');
@@ -17,7 +19,7 @@ export const generateMetadata = async () => {
function UserHomePage() {
return (
<>
<UserAccountHeader
<HomeLayoutPageHeader
title={<Trans i18nKey={'common:homeTabLabel'} />}
description={<Trans i18nKey={'common:homeTabDescription'} />}
/>

View File

@@ -1,12 +1,14 @@
import { Trans } from '@kit/ui/trans';
import { UserAccountHeader } from '~/(dashboard)/home/(user)/_components/user-account-header';
import { withI18n } from '~/lib/i18n/with-i18n';
// local imports
import { HomeLayoutPageHeader } from '../_components/home-page-header';
function UserSettingsLayout(props: React.PropsWithChildren) {
return (
<>
<UserAccountHeader
<HomeLayoutPageHeader
title={<Trans i18nKey={'account:accountTabLabel'} />}
description={<Trans i18nKey={'account:accountTabDescription'} />}
/>

View File

@@ -26,7 +26,7 @@ export const generateMetadata = async () => {
function PersonalAccountSettingsPage() {
return (
<PageBody>
<div className={'mx-auto flex w-full flex-1 flex-col lg:max-w-2xl'}>
<div className={'flex w-full flex-1 flex-col lg:max-w-2xl'}>
<PersonalAccountSettingsContainer features={features} paths={paths} />
</div>
</PageBody>

View File

@@ -0,0 +1,39 @@
'use client';
import { useRouter } from 'next/navigation';
import { AccountSelector } from '@kit/accounts/account-selector';
import featureFlagsConfig from '~/config/feature-flags.config';
import pathsConfig from '~/config/paths.config';
const features = {
enableTeamCreation: featureFlagsConfig.enableTeamCreation,
};
export function TeamAccountAccountsSelector(params: {
selectedAccount: string;
accounts: Array<{
label: string | null;
value: string | null;
image: string | null;
}>;
}) {
const router = useRouter();
return (
<AccountSelector
selectedAccount={params.selectedAccount}
accounts={params.accounts}
collapsed={false}
features={features}
onAccountChange={(value) => {
const path = value
? pathsConfig.app.accountHome.replace('[account]', value)
: pathsConfig.app.home;
router.replace(path);
}}
/>
);
}

View File

@@ -25,14 +25,14 @@ import { Trans } from '@kit/ui/trans';
import featureFlagsConfig from '~/config/feature-flags.config';
import pathsConfig from '~/config/paths.config';
import { getTeamAccountSidebarConfig } from '~/config/team-account-sidebar.config';
import { getTeamAccountSidebarConfig } from '~/config/team-account-navigation.config';
const features = {
enableTeamAccounts: featureFlagsConfig.enableTeamAccounts,
enableTeamCreation: featureFlagsConfig.enableTeamCreation,
};
export const AccountLayoutMobileNavigation = (
export const TeamAccountLayoutMobileNavigation = (
props: React.PropsWithChildren<{
account: string;
}>,

View File

@@ -0,0 +1,15 @@
import { PageHeader } from '@kit/ui/page';
export function TeamAccountLayoutPageHeader(
props: React.PropsWithChildren<{
title: string | React.ReactNode;
description: string | React.ReactNode;
account: string;
}>,
) {
return (
<PageHeader title={props.title} description={props.description}>
{props.children}
</PageHeader>
);
}

View File

@@ -1,9 +1,9 @@
import { SidebarDivider, SidebarGroup, SidebarItem } from '@kit/ui/sidebar';
import { Trans } from '@kit/ui/trans';
import { getTeamAccountSidebarConfig } from '~/config/team-account-sidebar.config';
import { getTeamAccountSidebarConfig } from '~/config/team-account-navigation.config';
export function AccountLayoutSidebarNavigation({
export function TeamAccountLayoutSidebarNavigation({
account,
}: React.PropsWithChildren<{
account: string;

View File

@@ -1,12 +1,9 @@
'use client';
import { useRouter } from 'next/navigation';
import { User } from '@supabase/supabase-js';
import { ArrowLeftCircle, ArrowRightCircle } from 'lucide-react';
import { AccountSelector } from '@kit/accounts/account-selector';
import { If } from '@kit/ui/if';
import { Sidebar, SidebarContent } from '@kit/ui/sidebar';
import {
@@ -19,10 +16,10 @@ import { Trans } from '@kit/ui/trans';
import { cn } from '@kit/ui/utils';
import { ProfileAccountDropdownContainer } from '~/components//personal-account-dropdown-container';
import featureFlagsConfig from '~/config/feature-flags.config';
import pathsConfig from '~/config/paths.config';
import { TeamAccountNotifications } from '~/home/[account]/_components/team-account-notifications';
import { AccountLayoutSidebarNavigation } from './account-layout-sidebar-navigation';
import { TeamAccountAccountsSelector } from '../_components/team-account-accounts-selector';
import { TeamAccountLayoutSidebarNavigation } from './team-account-layout-sidebar-navigation';
type AccountModel = {
label: string | null;
@@ -30,16 +27,11 @@ type AccountModel = {
image: string | null;
};
const features = {
enableTeamAccounts: featureFlagsConfig.enableTeamAccounts,
enableTeamCreation: featureFlagsConfig.enableTeamCreation,
};
export function AccountLayoutSidebar(props: {
export function TeamAccountLayoutSidebar(props: {
account: string;
accounts: AccountModel[];
collapsed: boolean;
user: User | null;
user: User;
}) {
return (
<Sidebar collapsed={props.collapsed}>
@@ -62,31 +54,28 @@ function SidebarContainer(props: {
collapsed: boolean;
setCollapsed: (collapsed: boolean) => void;
collapsible?: boolean;
user: User | null;
user: User;
}) {
const { account, accounts } = props;
const router = useRouter();
return (
<>
<SidebarContent className={'h-16 justify-center'}>
<AccountSelector
selectedAccount={account}
accounts={accounts}
collapsed={props.collapsed}
features={features}
onAccountChange={(value) => {
const path = value
? pathsConfig.app.accountHome.replace('[account]', value)
: pathsConfig.app.home;
<SidebarContent className={'mt-4'}>
<div className={'flex items-center justify-between'}>
<TeamAccountAccountsSelector
selectedAccount={account}
accounts={accounts}
/>
router.replace(path);
}}
/>
<TeamAccountNotifications
userId={props.user.id}
accountId={account}
/>
</div>
</SidebarContent>
<SidebarContent className={`mt-5 h-[calc(100%-160px)] overflow-y-auto`}>
<AccountLayoutSidebarNavigation account={account} />
<TeamAccountLayoutSidebarNavigation account={account} />
</SidebarContent>
<div className={'absolute bottom-4 left-0 w-full'}>

View File

@@ -0,0 +1,71 @@
import {
BorderedNavigationMenu,
BorderedNavigationMenuItem,
} from '@kit/ui/bordered-navigation-menu';
import { AppLogo } from '~/components/app-logo';
import { ProfileAccountDropdownContainer } from '~/components/personal-account-dropdown-container';
import { getTeamAccountSidebarConfig } from '~/config/team-account-navigation.config';
import { TeamAccountAccountsSelector } from '~/home/[account]/_components/team-account-accounts-selector';
// local imports
import { TeamAccountWorkspace } from '../_lib/server/team-account-workspace.loader';
import { TeamAccountNotifications } from './team-account-notifications';
export function TeamAccountNavigationMenu(props: {
workspace: TeamAccountWorkspace;
}) {
const { account, user, accounts } = props.workspace;
const routes = getTeamAccountSidebarConfig(account.slug).routes.reduce<
Array<{
path: string;
label: string;
Icon?: React.ReactNode;
end?: boolean | undefined;
}>
>((acc, item) => {
if ('children' in item) {
return [...acc, ...item.children];
}
if ('divider' in item) {
return acc;
}
return [...acc, item];
}, []);
return (
<div className={'flex w-full flex-1 justify-between'}>
<div className={'flex items-center space-x-8'}>
<AppLogo />
<BorderedNavigationMenu>
{routes.map((route) => (
<BorderedNavigationMenuItem {...route} key={route.path} />
))}
</BorderedNavigationMenu>
</div>
<div className={'flex justify-end space-x-2.5'}>
<TeamAccountAccountsSelector
selectedAccount={account.id}
accounts={accounts.map((account) => ({
label: account.name,
value: account.id,
image: account.picture_url,
}))}
/>
<TeamAccountNotifications accountId={account.id} userId={user.id} />
<ProfileAccountDropdownContainer
collapsed={true}
user={user}
account={account}
/>
</div>
</div>
);
}

View File

@@ -1,21 +1,18 @@
import { use } from 'react';
import { NotificationsPopover } from '@kit/notifications/components';
import featuresFlagConfig from '~/config/feature-flags.config';
import { loadTeamWorkspace } from '../_lib/server/team-account-workspace.loader';
export function AccountNotifications(params: { accountId: string }) {
const { user, account } = use(loadTeamWorkspace(params.accountId));
export function TeamAccountNotifications(params: {
userId: string;
accountId: string;
}) {
if (!featuresFlagConfig.enableNotifications) {
return null;
}
return (
<NotificationsPopover
accountIds={[user.id, account.id]}
accountIds={[params.userId, params.accountId]}
realtime={featuresFlagConfig.realtimeNotifications}
/>
);

View File

@@ -9,6 +9,10 @@ import { createTeamAccountsApi } from '@kit/team-accounts/api';
import pathsConfig from '~/config/paths.config';
export type TeamAccountWorkspace = Awaited<
ReturnType<typeof loadTeamWorkspace>
>;
/**
* Load the account workspace data.
* We place this function into a separate file so it can be reused in multiple places across the server components.

View File

@@ -15,7 +15,8 @@ import billingConfig from '~/config/billing.config';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
import { AccountLayoutHeader } from '../_components/account-layout-header';
// local imports
import { TeamAccountLayoutPageHeader } from '../_components/team-account-layout-page-header';
import { loadTeamAccountBillingPage } from '../_lib/server/team-account-billing-page.loader';
import { loadTeamWorkspace } from '../_lib/server/team-account-workspace.loader';
import { TeamAccountCheckoutForm } from './_components/team-account-checkout-form';
@@ -72,10 +73,10 @@ async function TeamAccountBillingPage({ params }: Params) {
return (
<>
<AccountLayoutHeader
<TeamAccountLayoutPageHeader
account={params.account}
title={<Trans i18nKey={'common:billingTabLabel'} />}
description={<Trans i18nKey={'common:billingTabDescription'} />}
account={params.account}
/>
<PageBody>

View File

@@ -0,0 +1,65 @@
import { use } from 'react';
import { If } from '@kit/ui/if';
import { Page, PageMobileNavigation, PageNavigation } from '@kit/ui/page';
import { AppLogo } from '~/components/app-logo';
import { getTeamAccountSidebarConfig } from '~/config/team-account-navigation.config';
import { withI18n } from '~/lib/i18n/with-i18n';
// local imports
import { TeamAccountLayoutMobileNavigation } from './_components/team-account-layout-mobile-navigation';
import { TeamAccountLayoutSidebar } from './_components/team-account-layout-sidebar';
import { TeamAccountNavigationMenu } from './_components/team-account-navigation-menu';
import { loadTeamWorkspace } from './_lib/server/team-account-workspace.loader';
interface Params {
account: string;
}
function TeamWorkspaceLayout({
children,
params,
}: React.PropsWithChildren<{
params: Params;
}>) {
const data = use(loadTeamWorkspace(params.account));
const style = getTeamAccountSidebarConfig(params.account).style;
const accounts = data.accounts.map(({ name, slug, picture_url }) => ({
label: name,
value: slug,
image: picture_url,
}));
return (
<Page style={style}>
<PageNavigation>
<If condition={style === 'sidebar'}>
<TeamAccountLayoutSidebar
collapsed={false}
account={params.account}
accounts={accounts}
user={data.user}
/>
</If>
<If condition={style === 'header'}>
<TeamAccountNavigationMenu workspace={data} />
</If>
</PageNavigation>
<PageMobileNavigation className={'flex items-center justify-between'}>
<AppLogo />
<div className={'flex space-x-4'}>
<TeamAccountLayoutMobileNavigation account={params.account} />
</div>
</PageMobileNavigation>
{children}
</Page>
);
}
export default withI18n(TeamWorkspaceLayout);

View File

@@ -21,7 +21,8 @@ import { Trans } from '@kit/ui/trans';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
import { AccountLayoutHeader } from '../_components/account-layout-header';
// local imports
import { TeamAccountLayoutPageHeader } from '../_components/team-account-layout-page-header';
import { loadMembersPageData } from './_lib/server/members-page.loader';
interface Params {
@@ -53,16 +54,14 @@ async function TeamAccountMembersPage({ params }: Params) {
return (
<>
<AccountLayoutHeader
<TeamAccountLayoutPageHeader
title={<Trans i18nKey={'common:membersTabLabel'} />}
description={<Trans i18nKey={'common:membersTabDescription'} />}
account={params.account}
account={account.slug}
/>
<PageBody>
<div
className={'mx-auto flex w-full max-w-3xl flex-col space-y-6 pb-32'}
>
<div className={'flex w-full max-w-4xl flex-col space-y-6 pb-32'}>
<Card>
<CardHeader className={'flex flex-row justify-between'}>
<div className={'flex flex-col space-y-1.5'}>

View File

@@ -4,13 +4,18 @@ import { PlusCircle } from 'lucide-react';
import { Button } from '@kit/ui/button';
import { PageBody } from '@kit/ui/page';
import Spinner from '@kit/ui/spinner';
import { Spinner } from '@kit/ui/spinner';
import { Trans } from '@kit/ui/trans';
import { AccountLayoutHeader } from '~/(dashboard)/home/[account]/_components/account-layout-header';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
import { TeamAccountLayoutPageHeader } from './_components/team-account-layout-page-header';
interface Params {
account: string;
}
const DashboardDemo = loadDynamic(
() => import('./_components/dashboard-demo'),
{
@@ -41,25 +46,19 @@ export const generateMetadata = async () => {
};
};
function TeamAccountHomePage({
params,
}: {
params: {
account: string;
};
}) {
function TeamAccountHomePage({ params }: { params: Params }) {
return (
<>
<AccountLayoutHeader
<TeamAccountLayoutPageHeader
account={params.account}
title={<Trans i18nKey={'common:dashboardTabLabel'} />}
description={<Trans i18nKey={'common:dashboardTabDescription'} />}
account={params.account}
>
<Button size={'sm'}>
<Button>
<PlusCircle className={'mr-1 h-4'} />
<span>Add Widget</span>
</Button>
</AccountLayoutHeader>
</TeamAccountLayoutPageHeader>
<PageBody>
<DashboardDemo />

View File

@@ -5,7 +5,8 @@ import { Trans } from '@kit/ui/trans';
import pathsConfig from '~/config/paths.config';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { AccountLayoutHeader } from '../_components/account-layout-header';
// local imports
import { TeamAccountLayoutPageHeader } from '../_components/team-account-layout-page-header';
import { loadTeamWorkspace } from '../_lib/server/team-account-workspace.loader';
export const generateMetadata = async () => {
@@ -40,18 +41,14 @@ async function TeamAccountSettingsPage(props: Props) {
return (
<>
<AccountLayoutHeader
<TeamAccountLayoutPageHeader
account={account.slug}
title={<Trans i18nKey={'teams:settings.pageTitle'} />}
description={<Trans i18nKey={'teams:settings.pageDescription'} />}
account={props.params.account}
/>
<PageBody>
<div
className={
'container flex w-full max-w-2xl flex-1 flex-col items-center p-0'
}
>
<div className={'flex max-w-2xl flex-1 flex-col'}>
<TeamAccountSettingsContainer account={account} paths={paths} />
</div>
</PageBody>

View File

@@ -33,12 +33,12 @@ const NotFoundPage = async () => {
<div
className={
'm-auto flex w-full flex-1 flex-col items-center justify-center'
'container m-auto flex w-full flex-1 flex-col items-center justify-center'
}
>
<div className={'flex flex-col items-center space-y-12'}>
<div>
<h1 className={'font-heading text-9xl font-extrabold'}>
<h1 className={'font-heading text-8xl font-extrabold xl:text-9xl'}>
<Trans i18nKey={'common:pageNotFoundHeading'} />
</h1>
</div>

View File

@@ -15,37 +15,10 @@ const LogoImage: React.FC<{
xmlns="http://www.w3.org/2000/svg"
>
<path
className={'dark:hidden'}
className={'fill-primary dark:fill-white'}
d="M119.081 138V73.209C119.081 67.551 117.08 62.79 113.078 58.926C109.214 55.062 104.453 53.13 98.7951 53.13C93.2751 53.13 88.4451 55.062 84.3051 58.926C80.3031 62.652 78.3021 67.344 78.3021 73.002V138H59.4651V73.002C59.4651 67.206 57.5331 62.514 53.6691 58.926C49.5291 55.062 44.6301 53.13 38.9721 53.13C33.4521 53.13 28.7601 55.062 24.8961 58.926C20.7561 63.066 18.6861 67.965 18.6861 73.623V138H0.0560548V36.984H18.6861V44.643C21.0321 41.745 24.0681 39.33 27.7941 37.398C31.6581 35.466 35.3841 34.5 38.9721 34.5C45.0441 34.5 50.5641 35.742 55.5321 38.226C60.6381 40.572 65.0541 43.884 68.7801 48.162C72.5061 43.884 76.9221 40.572 82.0281 38.226C87.1341 35.742 92.7231 34.5 98.7951 34.5C104.177 34.5 109.214 35.466 113.906 37.398C118.598 39.33 122.738 42.09 126.326 45.678C129.914 49.266 132.674 53.475 134.606 58.305C136.676 62.997 137.711 67.965 137.711 73.209V138H119.081ZM242.173 138V122.268C237.757 127.374 232.651 131.445 226.855 134.481C221.059 137.517 214.918 139.035 208.432 139.035C201.256 139.035 194.494 137.724 188.146 135.102C181.936 132.48 176.416 128.754 171.586 123.924C166.756 119.232 162.961 113.712 160.201 107.364C157.579 100.878 156.268 94.116 156.268 87.078C156.268 80.04 157.579 73.347 160.201 66.999C162.961 60.513 166.756 54.855 171.586 50.025C176.416 45.195 181.936 41.469 188.146 38.847C194.494 36.225 201.256 34.914 208.432 34.914C215.056 34.914 221.266 36.294 227.062 39.054C232.996 41.814 238.033 45.678 242.173 50.646V36.984H260.803V138H242.173ZM208.432 53.337C203.878 53.337 199.462 54.234 195.184 56.028C191.044 57.684 187.456 60.03 184.42 63.066C181.384 66.102 178.969 69.759 177.175 74.037C175.519 78.177 174.691 82.524 174.691 87.078C174.691 91.632 175.519 95.979 177.175 100.119C178.969 104.259 181.384 107.847 184.42 110.883C187.456 113.919 191.044 116.334 195.184 118.128C199.462 119.784 203.878 120.612 208.432 120.612C212.986 120.612 217.333 119.784 221.473 118.128C225.613 116.334 229.201 113.919 232.237 110.883C235.273 107.847 237.619 104.259 239.275 100.119C241.069 95.979 241.966 91.632 241.966 87.078C241.966 82.524 241.069 78.177 239.275 74.037C237.619 69.759 235.273 66.102 232.237 63.066C229.201 60.03 225.613 57.684 221.473 56.028C217.333 54.234 212.986 53.337 208.432 53.337ZM331.127 138L299.663 99.705V138H281.447V0.344996H299.663V59.754L327.815 33.258H354.932L305.873 78.798L355.139 138H331.127ZM379.299 94.116C379.299 97.428 380.472 100.878 382.818 104.466C385.302 108.054 388.131 111.09 391.305 113.574C397.101 118.128 403.863 120.405 411.591 120.405C423.873 120.405 433.878 114.471 441.606 102.603L457.338 111.918C451.956 120.612 445.332 127.305 437.466 131.997C429.6 136.689 420.975 139.035 411.591 139.035C404.553 139.035 397.86 137.724 391.512 135.102C385.164 132.342 379.575 128.547 374.745 123.717C369.915 118.887 366.12 113.298 363.36 106.95C360.738 100.602 359.427 93.909 359.427 86.871C359.427 79.833 360.738 73.14 363.36 66.792C366.12 60.306 369.915 54.648 374.745 49.818C379.437 44.988 384.957 41.262 391.305 38.64C397.791 36.018 404.553 34.707 411.591 34.707C418.629 34.707 425.322 36.018 431.67 38.64C438.156 41.262 443.745 44.988 448.437 49.818C458.649 60.306 463.755 72.45 463.755 86.25C463.755 88.734 463.548 91.356 463.134 94.116H379.299ZM411.591 51.681C405.933 51.681 400.62 52.923 395.652 55.407C390.684 57.891 386.682 61.203 383.646 65.343C380.748 69.345 379.299 73.623 379.299 78.177H443.883C443.883 73.623 442.365 69.345 439.329 65.343C436.431 61.203 432.498 57.891 427.53 55.407C422.562 52.923 417.249 51.681 411.591 51.681ZM528.543 54.372C525.231 52.854 522.264 52.095 519.642 52.095C514.122 52.095 509.568 54.027 505.98 57.891C502.116 62.031 500.184 66.792 500.184 72.174V138H482.382V72.174C482.382 64.722 484.245 57.891 487.971 51.681C491.835 45.471 497.079 40.641 503.703 37.191C508.671 34.845 513.984 33.672 519.642 33.672C524.196 33.672 528.543 34.5 532.683 36.156C536.823 37.812 541.17 40.503 545.724 44.229L528.543 54.372ZM610.092 138L578.628 99.705V138H560.412V0.344996H578.628V59.754L606.78 33.258H633.897L584.838 78.798L634.104 138H610.092ZM656.049 19.596C653.427 19.596 651.15 18.699 649.218 16.905C647.424 14.973 646.527 12.696 646.527 10.074C646.527 7.45199 647.424 5.24399 649.218 3.44999C651.15 1.51799 653.427 0.551993 656.049 0.551993C658.671 0.551993 660.879 1.51799 662.673 3.44999C664.605 5.24399 665.571 7.45199 665.571 10.074C665.571 12.696 664.605 14.973 662.673 16.905C660.879 18.699 658.671 19.596 656.049 19.596ZM647.562 138V34.5H664.95V138H647.562ZM717.4 53.13V138H699.805V53.13H684.28V34.5H699.805V0.344996H717.4V34.5H732.925V53.13H717.4Z"
fill="url(#paint0_linear_1666_2)"
/>
<path
className={'hidden dark:block'}
d="M119.081 138V73.209C119.081 67.551 117.08 62.79 113.078 58.926C109.214 55.062 104.453 53.13 98.7951 53.13C93.2751 53.13 88.4451 55.062 84.3051 58.926C80.3031 62.652 78.3021 67.344 78.3021 73.002V138H59.4651V73.002C59.4651 67.206 57.5331 62.514 53.6691 58.926C49.5291 55.062 44.6301 53.13 38.9721 53.13C33.4521 53.13 28.7601 55.062 24.8961 58.926C20.7561 63.066 18.6861 67.965 18.6861 73.623V138H0.0560548V36.984H18.6861V44.643C21.0321 41.745 24.0681 39.33 27.7941 37.398C31.6581 35.466 35.3841 34.5 38.9721 34.5C45.0441 34.5 50.5641 35.742 55.5321 38.226C60.6381 40.572 65.0541 43.884 68.7801 48.162C72.5061 43.884 76.9221 40.572 82.0281 38.226C87.1341 35.742 92.7231 34.5 98.7951 34.5C104.177 34.5 109.214 35.466 113.906 37.398C118.598 39.33 122.738 42.09 126.326 45.678C129.914 49.266 132.674 53.475 134.606 58.305C136.676 62.997 137.711 67.965 137.711 73.209V138H119.081ZM242.173 138V122.268C237.757 127.374 232.651 131.445 226.855 134.481C221.059 137.517 214.918 139.035 208.432 139.035C201.256 139.035 194.494 137.724 188.146 135.102C181.936 132.48 176.416 128.754 171.586 123.924C166.756 119.232 162.961 113.712 160.201 107.364C157.579 100.878 156.268 94.116 156.268 87.078C156.268 80.04 157.579 73.347 160.201 66.999C162.961 60.513 166.756 54.855 171.586 50.025C176.416 45.195 181.936 41.469 188.146 38.847C194.494 36.225 201.256 34.914 208.432 34.914C215.056 34.914 221.266 36.294 227.062 39.054C232.996 41.814 238.033 45.678 242.173 50.646V36.984H260.803V138H242.173ZM208.432 53.337C203.878 53.337 199.462 54.234 195.184 56.028C191.044 57.684 187.456 60.03 184.42 63.066C181.384 66.102 178.969 69.759 177.175 74.037C175.519 78.177 174.691 82.524 174.691 87.078C174.691 91.632 175.519 95.979 177.175 100.119C178.969 104.259 181.384 107.847 184.42 110.883C187.456 113.919 191.044 116.334 195.184 118.128C199.462 119.784 203.878 120.612 208.432 120.612C212.986 120.612 217.333 119.784 221.473 118.128C225.613 116.334 229.201 113.919 232.237 110.883C235.273 107.847 237.619 104.259 239.275 100.119C241.069 95.979 241.966 91.632 241.966 87.078C241.966 82.524 241.069 78.177 239.275 74.037C237.619 69.759 235.273 66.102 232.237 63.066C229.201 60.03 225.613 57.684 221.473 56.028C217.333 54.234 212.986 53.337 208.432 53.337ZM331.127 138L299.663 99.705V138H281.447V0.344996H299.663V59.754L327.815 33.258H354.932L305.873 78.798L355.139 138H331.127ZM379.299 94.116C379.299 97.428 380.472 100.878 382.818 104.466C385.302 108.054 388.131 111.09 391.305 113.574C397.101 118.128 403.863 120.405 411.591 120.405C423.873 120.405 433.878 114.471 441.606 102.603L457.338 111.918C451.956 120.612 445.332 127.305 437.466 131.997C429.6 136.689 420.975 139.035 411.591 139.035C404.553 139.035 397.86 137.724 391.512 135.102C385.164 132.342 379.575 128.547 374.745 123.717C369.915 118.887 366.12 113.298 363.36 106.95C360.738 100.602 359.427 93.909 359.427 86.871C359.427 79.833 360.738 73.14 363.36 66.792C366.12 60.306 369.915 54.648 374.745 49.818C379.437 44.988 384.957 41.262 391.305 38.64C397.791 36.018 404.553 34.707 411.591 34.707C418.629 34.707 425.322 36.018 431.67 38.64C438.156 41.262 443.745 44.988 448.437 49.818C458.649 60.306 463.755 72.45 463.755 86.25C463.755 88.734 463.548 91.356 463.134 94.116H379.299ZM411.591 51.681C405.933 51.681 400.62 52.923 395.652 55.407C390.684 57.891 386.682 61.203 383.646 65.343C380.748 69.345 379.299 73.623 379.299 78.177H443.883C443.883 73.623 442.365 69.345 439.329 65.343C436.431 61.203 432.498 57.891 427.53 55.407C422.562 52.923 417.249 51.681 411.591 51.681ZM528.543 54.372C525.231 52.854 522.264 52.095 519.642 52.095C514.122 52.095 509.568 54.027 505.98 57.891C502.116 62.031 500.184 66.792 500.184 72.174V138H482.382V72.174C482.382 64.722 484.245 57.891 487.971 51.681C491.835 45.471 497.079 40.641 503.703 37.191C508.671 34.845 513.984 33.672 519.642 33.672C524.196 33.672 528.543 34.5 532.683 36.156C536.823 37.812 541.17 40.503 545.724 44.229L528.543 54.372ZM610.092 138L578.628 99.705V138H560.412V0.344996H578.628V59.754L606.78 33.258H633.897L584.838 78.798L634.104 138H610.092ZM656.049 19.596C653.427 19.596 651.15 18.699 649.218 16.905C647.424 14.973 646.527 12.696 646.527 10.074C646.527 7.45199 647.424 5.24399 649.218 3.44999C651.15 1.51799 653.427 0.551993 656.049 0.551993C658.671 0.551993 660.879 1.51799 662.673 3.44999C664.605 5.24399 665.571 7.45199 665.571 10.074C665.571 12.696 664.605 14.973 662.673 16.905C660.879 18.699 658.671 19.596 656.049 19.596ZM647.562 138V34.5H664.95V138H647.562ZM717.4 53.13V138H699.805V53.13H684.28V34.5H699.805V0.344996H717.4V34.5H732.925V53.13H717.4Z"
fill="url(#paint0_linear_1666_2_dark)"
/>
<defs>
<linearGradient
id="paint0_linear_1666_2"
x1="1.12419"
y1="78"
x2="724.566"
y2="63.6614"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#000" />
</linearGradient>
<linearGradient
id="paint0_linear_1666_2_dark"
x1="1.12419"
y1="78"
x2="724.566"
y2="63.6614"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#fff" />
</linearGradient>
</defs>
</svg>
);
};

View File

@@ -1,5 +1,7 @@
import { z } from 'zod';
type LanguagePriority = 'user' | 'application';
const FeatureFlagsSchema = z.object({
enableThemeToggle: z.boolean({
description: 'Enable theme toggle in the user interface.',
@@ -83,9 +85,8 @@ const featuresFlagConfig = FeatureFlagsSchema.parse({
process.env.NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_BILLING,
false,
),
languagePriority: process.env.NEXT_PUBLIC_LANGUAGE_PRIORITY as
| 'user'
| 'application',
languagePriority: process.env
.NEXT_PUBLIC_LANGUAGE_PRIORITY as LanguagePriority,
enableNotifications: getBoolean(
process.env.NEXT_PUBLIC_ENABLE_NOTIFICATIONS,
true,

View File

@@ -1,6 +1,6 @@
import { CreditCard, Home, User } from 'lucide-react';
import { SidebarConfigSchema } from '@kit/ui/sidebar-schema';
import { NavigationConfigSchema } from '@kit/ui/navigation-schema';
import featureFlagsConfig from '~/config/feature-flags.config';
import pathsConfig from '~/config/paths.config';
@@ -29,6 +29,7 @@ if (featureFlagsConfig.enablePersonalAccountBilling) {
});
}
export const personalAccountSidebarConfig = SidebarConfigSchema.parse({
export const personalAccountNavigationConfig = NavigationConfigSchema.parse({
routes,
style: process.env.NEXT_PUBLIC_USER_NAVIGATION_STYLE,
});

View File

@@ -1,6 +1,6 @@
import { CreditCard, LayoutDashboard, Settings, Users } from 'lucide-react';
import { SidebarConfigSchema } from '@kit/ui/sidebar-schema';
import { NavigationConfigSchema } from '@kit/ui/navigation-schema';
import featureFlagsConfig from '~/config/feature-flags.config';
import pathsConfig from '~/config/paths.config';
@@ -40,8 +40,9 @@ const getRoutes = (account: string) => [
];
export function getTeamAccountSidebarConfig(account: string) {
return SidebarConfigSchema.parse({
return NavigationConfigSchema.parse({
routes: getRoutes(account),
style: process.env.NEXT_PUBLIC_TEAM_NAVIGATION_STYLE,
});
}

File diff suppressed because it is too large Load Diff

View File

@@ -19,7 +19,7 @@ const INTERNAL_PACKAGES = [
'@kit/cms',
'@kit/monitoring',
'@kit/next',
'@kit/notifications'
'@kit/notifications',
];
/** @type {import('next').NextConfig} */

View File

@@ -98,7 +98,7 @@ export function AccountSelector({
variant="ghost"
role="combobox"
aria-expanded={open}
className={cn('dark:shadow-primary/10 group w-full border px-4', {
className={cn('dark:shadow-primary/10 group px-2', {
'justify-between': !collapsed,
'justify-center': collapsed,
})}
@@ -144,11 +144,11 @@ export function AccountSelector({
)}
</If>
<CaretSortIcon className="h-4 w-4 shrink-0 opacity-50" />
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0">
<PopoverContent className="w-full p-0" collisionPadding={20}>
<Command>
<CommandInput placeholder={t('searchAccount')} className="h-9" />

View File

@@ -115,15 +115,15 @@ export function PersonalAccountDropdown({
</div>
<EllipsisVertical
className={'text-muted-foreground hidden h-8 group-hover:flex'}
className={'text-muted-foreground mr-1 hidden h-8 group-hover:flex'}
/>
</If>
</DropdownMenuTrigger>
<DropdownMenuContent
className={'!min-w-[15rem]'}
className={'xl:!min-w-[15rem]'}
collisionPadding={{ right: 20, left: 20 }}
sideOffset={20}
sideOffset={10}
>
<DropdownMenuItem className={'!h-10 rounded-none'}>
<div

View File

@@ -1,3 +1,5 @@
'use client';
import { useCallback, useState } from 'react';
import type { Factor } from '@supabase/supabase-js';
@@ -25,7 +27,7 @@ import {
import { Badge } from '@kit/ui/badge';
import { Button } from '@kit/ui/button';
import { If } from '@kit/ui/if';
import Spinner from '@kit/ui/spinner';
import { Spinner } from '@kit/ui/spinner';
import {
Table,
TableBody,

View File

@@ -30,7 +30,7 @@ import {
InputOTPSeparator,
InputOTPSlot,
} from '@kit/ui/input-otp';
import Spinner from '@kit/ui/spinner';
import { Spinner } from '@kit/ui/spinner';
import { Trans } from '@kit/ui/trans';
export function MultiFactorChallengeContainer({

View File

@@ -21,6 +21,7 @@
"@kit/tsconfig": "workspace:*",
"@kit/ui": "workspace:*",
"@supabase/supabase-js": "^2.42.7",
"@tanstack/react-query": "5.32.0",
"@types/react": "^18.3.1",
"lucide-react": "^0.376.0",
"react": "18.3.1",

View File

@@ -111,12 +111,12 @@ export function NotificationsPopover(params: {
return (
<Popover modal open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button className={'h-8 w-8'} variant={'ghost'}>
<Button className={'h-9 w-9'} variant={'ghost'}>
<Bell className={'min-h-5 min-w-5'} />
<span
className={cn(
`fade-in animate-in zoom-in absolute right-5 top-5 flex h-3.5 w-3.5 items-center justify-center rounded-full bg-red-500 text-[0.65rem] text-white`,
`fade-in animate-in zoom-in absolute right-5 top-5 mt-0 flex h-3.5 w-3.5 items-center justify-center rounded-full bg-red-500 text-[0.65rem] text-white`,
{
hidden: !unread.length,
},
@@ -128,8 +128,10 @@ export function NotificationsPopover(params: {
</PopoverTrigger>
<PopoverContent
className={'flex flex-col p-0'}
collisionPadding={{ right: 20 }}
className={'flex w-full flex-col p-0 lg:min-w-64'}
align={'start'}
collisionPadding={20}
sideOffset={10}
>
<div className={'flex items-center px-3 py-2 text-sm font-semibold'}>
{t('common:notifications')}

View File

@@ -1,4 +1,6 @@
import { useEffect, useRef } from 'react';
import { useEffect } from 'react';
import { useQuery } from '@tanstack/react-query';
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
@@ -20,8 +22,8 @@ export function useFetchNotifications({
accountIds: string[];
realtime: boolean;
}) {
const { data: notifications } = useFetchInitialNotifications({ accountIds });
const client = useSupabase();
const didFetchInitialData = useRef(false);
useEffect(() => {
let realtimeSubscription: { unsubscribe: () => void } | null = null;
@@ -45,10 +47,26 @@ export function useFetchNotifications({
.subscribe();
}
if (!didFetchInitialData.current) {
const now = new Date().toISOString();
if (notifications) {
onNotifications(notifications);
}
const initialFetch = client
return () => {
if (realtimeSubscription) {
realtimeSubscription.unsubscribe();
}
};
}, [client, onNotifications, accountIds, realtime, notifications]);
}
function useFetchInitialNotifications(props: { accountIds: string[] }) {
const client = useSupabase();
const now = new Date().toISOString();
return useQuery({
queryKey: ['notifications', ...props.accountIds],
queryFn: async () => {
const { data } = await client
.from('notifications')
.select(
`id,
@@ -59,29 +77,14 @@ export function useFetchNotifications({
link
`,
)
.in('account_id', accountIds)
.in('account_id', props.accountIds)
.eq('dismissed', false)
.gt('expires_at', now)
.order('created_at', { ascending: false })
.limit(10);
didFetchInitialData.current = true;
void initialFetch.then(({ data, error }) => {
if (error) {
throw error;
}
if (data) {
onNotifications(data);
}
});
}
return () => {
if (realtimeSubscription) {
realtimeSubscription.unsubscribe();
}
};
}, [client, onNotifications, accountIds, realtime]);
return data;
},
refetchOnMount: false,
});
}

View File

@@ -217,7 +217,6 @@ function ActionsDropdown({
<UpdateInvitationDialog
isOpen
setIsOpen={setIsUpdatingRole}
account={invitation.account_id}
invitationId={invitation.id}
userRole={invitation.role}
userRoleHierarchy={permissions.currentUserRoleHierarchy}

View File

@@ -36,17 +36,9 @@ export const UpdateInvitationDialog: React.FC<{
isOpen: boolean;
setIsOpen: (isOpen: boolean) => void;
invitationId: number;
account: string;
userRole: Role;
userRoleHierarchy: number;
}> = ({
isOpen,
setIsOpen,
invitationId,
userRole,
userRoleHierarchy,
account,
}) => {
}> = ({ isOpen, setIsOpen, invitationId, userRole, userRoleHierarchy }) => {
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent>
@@ -61,7 +53,6 @@ export const UpdateInvitationDialog: React.FC<{
</DialogHeader>
<UpdateInvitationForm
account={account}
invitationId={invitationId}
userRole={userRole}
userRoleHierarchy={userRoleHierarchy}
@@ -73,13 +64,11 @@ export const UpdateInvitationDialog: React.FC<{
};
function UpdateInvitationForm({
account,
invitationId,
userRole,
userRoleHierarchy,
setIsOpen,
}: React.PropsWithChildren<{
account: string;
invitationId: number;
userRole: Role;
userRoleHierarchy: number;

File diff suppressed because it is too large Load Diff

View File

@@ -104,7 +104,8 @@
"./trans": "./src/makerkit/trans.tsx",
"./divider": "./src/makerkit/divider.tsx",
"./sidebar": "./src/makerkit/sidebar.tsx",
"./sidebar-schema": "./src/makerkit/sidebar-schema.ts",
"./navigation-schema": "./src/makerkit/navigation-config.schema.ts",
"./bordered-navigation-menu": "./src/makerkit/bordered-navigation-menu.tsx",
"./spinner": "./src/makerkit/spinner.tsx",
"./page": "./src/makerkit/page.tsx",
"./image-uploader": "./src/makerkit/image-uploader.tsx",

View File

@@ -0,0 +1,50 @@
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { Button } from '../shadcn/button';
import {
NavigationMenu,
NavigationMenuItem,
NavigationMenuList,
} from '../shadcn/navigation-menu';
import { cn, isRouteActive } from '../utils';
import { Trans } from './trans';
export function BorderedNavigationMenu(props: React.PropsWithChildren) {
return (
<NavigationMenu className={'h-full'}>
<NavigationMenuList className={'relative h-full space-x-2'}>
{props.children}
</NavigationMenuList>
</NavigationMenu>
);
}
export function BorderedNavigationMenuItem(props: {
path: string;
label: string;
active?: boolean;
}) {
const pathname = usePathname();
const active = props.active ?? isRouteActive(pathname, props.path);
return (
<NavigationMenuItem>
<Button asChild variant={'ghost'} className={'relative'}>
<Link href={props.path} className={'text-sm'}>
<Trans i18nKey={props.label} defaults={props.label} />
{active ? (
<span
className={cn(
'absolute -bottom-2.5 left-0 h-1 w-full bg-primary animate-in fade-in zoom-in-90',
)}
/>
) : null}
</Link>
</Button>
</NavigationMenuItem>
);
}

View File

@@ -1,7 +1,7 @@
import type { PropsWithChildren } from 'react';
import { cn } from '../utils';
import Spinner from './spinner';
import { Spinner } from './spinner';
export function LoadingOverlay({
children,

View File

@@ -1,6 +1,7 @@
import { z } from 'zod';
export const SidebarConfigSchema = z.object({
export const NavigationConfigSchema = z.object({
style: z.enum(['custom', 'sidebar', 'header']).default('sidebar'),
routes: z.array(
z.union([
z.object({

View File

@@ -1,23 +1,98 @@
import * as React from 'react';
import { cn } from '../utils';
export function Page(
props: React.PropsWithChildren<{
sidebar?: React.ReactNode;
contentContainerClassName?: string;
className?: string;
}>,
) {
type PageStyle = 'sidebar' | 'header' | 'custom';
type PageProps = React.PropsWithChildren<{
style?: PageStyle;
contentContainerClassName?: string;
className?: string;
sticky?: boolean;
}>;
export function Page(props: PageProps) {
switch (props.style) {
case 'sidebar':
return <PageWithSidebar {...props} />;
case 'header':
return <PageWithHeader {...props} />;
case 'custom':
return props.children;
default:
return <PageWithSidebar {...props} />;
}
}
function PageWithSidebar(props: PageProps) {
const { Navigation, Children, MobileNavigation } = getSlotsFromPage(props);
return (
<div className={cn('flex', props.className)}>
<div className={'hidden lg:block'}>{props.sidebar}</div>
{Navigation}
<div
className={
props.contentContainerClassName ??
'mx-auto flex h-screen w-full flex-col space-y-4 overflow-y-auto'
'mx-auto flex h-screen w-full flex-col overflow-y-auto px-4 lg:px-0'
}
>
{props.children}
{MobileNavigation}
<div className={'flex flex-col space-y-4 lg:mt-4 lg:px-4'}>
{Children}
</div>
</div>
</div>
);
}
export function PageMobileNavigation(
props: React.PropsWithChildren<{
className?: string;
}>,
) {
return (
<div className={cn('w-full py-2 lg:hidden', props.className)}>
{props.children}
</div>
);
}
function PageWithHeader(props: PageProps) {
const { Navigation, Children, MobileNavigation } = getSlotsFromPage(props);
return (
<div className={cn('flex flex-1 flex-col', props.className)}>
<div
className={
props.contentContainerClassName ?? 'flex flex-1 flex-col space-y-4'
}
>
<div
className={cn(
'dark:border-primary-900 flex h-14 items-center justify-between px-4 shadow-sm dark:shadow-primary/10 lg:justify-start',
{
'sticky top-0 z-10 bg-background/80 backdrop-blur-md':
props.sticky ?? true,
},
)}
>
<div
className={'hidden w-full flex-1 items-center space-x-8 lg:flex'}
>
{Navigation}
</div>
{MobileNavigation}
</div>
<div className={'flex flex-col space-y-8 px-4 py-4 lg:container'}>
{Children}
</div>
</div>
</div>
);
@@ -28,50 +103,54 @@ export function PageBody(
className?: string;
}>,
) {
const className = cn('w-full px-4 flex flex-col flex-1', props.className);
const className = cn('w-full flex flex-col flex-1', props.className);
return <div className={className}>{props.children}</div>;
}
export function PageNavigation(props: React.PropsWithChildren) {
return <div className={'hidden flex-1 lg:flex'}>{props.children}</div>;
}
export function PageDescription(props: React.PropsWithChildren) {
return (
<h2 className={'hidden lg:block'}>
<span
className={'text-base font-medium leading-none text-muted-foreground'}
>
{props.children}
</span>
</h2>
);
}
export function PageTitle(props: React.PropsWithChildren) {
return (
<h1
className={
'font-heading text-2xl font-semibold leading-none dark:text-white'
}
>
{props.children}
</h1>
);
}
export function PageHeader({
children,
title,
description,
mobileNavigation,
}: React.PropsWithChildren<{
title?: string | React.ReactNode;
description?: string | React.ReactNode;
mobileNavigation?: React.ReactNode;
}>) {
return (
<div className={'flex min-h-[4.5rem] items-center justify-between px-4'}>
<div className={'flex items-center justify-between'}>
{title ? (
<div
className={
'flex items-center space-x-4 lg:flex-col lg:items-start lg:space-x-0 lg:space-y-0.5'
}
>
<div className={'flex items-center lg:hidden'}>
{mobileNavigation}
</div>
<div className={'flex flex-col space-y-1.5'}>
<PageTitle>{title}</PageTitle>
<h1
className={
'font-heading text-xl font-semibold leading-none dark:text-white'
}
>
{title}
</h1>
<h2 className={'hidden lg:block'}>
<span
className={
'text-base font-medium leading-none text-muted-foreground'
}
>
{description}
</span>
</h2>
<PageDescription>{description}</PageDescription>
</div>
) : null}
@@ -79,3 +158,41 @@ export function PageHeader({
</div>
);
}
function getSlotsFromPage(props: React.PropsWithChildren) {
return React.Children.toArray(props.children).reduce<{
Children: React.ReactElement | null;
Navigation: React.ReactElement | null;
MobileNavigation: React.ReactElement | null;
}>(
(acc, child) => {
if (!React.isValidElement(child)) {
return acc;
}
if (child.type === PageNavigation) {
return {
...acc,
Navigation: child,
};
}
if (child.type === PageMobileNavigation) {
return {
...acc,
MobileNavigation: child,
};
}
return {
...acc,
Children: child,
};
},
{
Children: null,
Navigation: null,
MobileNavigation: null,
},
);
}

View File

@@ -18,13 +18,13 @@ export function ProfileAvatar(props: ProfileAvatarProps) {
return (
<Avatar className={avatarClassName}>
<AvatarFallback>
<span className={'uppercase'}>{props.text.slice(0, 2)}</span>
<span className={'uppercase'}>{props.text.slice(0, 1)}</span>
</AvatarFallback>
</Avatar>
);
}
const initials = props.displayName?.slice(0, 2);
const initials = props.displayName?.slice(0, 1);
return (
<Avatar className={avatarClassName}>

View File

@@ -19,10 +19,10 @@ import {
import { cn, isRouteActive } from '../utils';
import { SidebarContext } from './context/sidebar.context';
import { If } from './if';
import { SidebarConfigSchema } from './sidebar-schema';
import { NavigationConfigSchema } from './navigation-config.schema';
import { Trans } from './trans';
export type SidebarConfig = z.infer<typeof SidebarConfigSchema>;
export type SidebarConfig = z.infer<typeof NavigationConfigSchema>;
export function Sidebar(props: {
collapsed?: boolean;
@@ -167,7 +167,7 @@ export function SidebarItem({
<Button
asChild
className={cn('flex w-full text-sm shadow-none', {
'justify-start space-x-2': !collapsed,
'justify-start space-x-2.5': !collapsed,
})}
size={size}
variant={variant}

View File

@@ -1,6 +1,6 @@
import { cn } from '../utils/cn';
import { cn } from '../utils';
function Spinner(
export function Spinner(
props: React.PropsWithChildren<{
className?: string;
}>,
@@ -29,5 +29,3 @@ function Spinner(
</div>
);
}
export default Spinner;

View File

@@ -1,9 +1,10 @@
"use client"
'use client';
import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch"
import * as React from 'react';
import { cn } from "@kit/ui/utils"
import * as SwitchPrimitives from '@radix-ui/react-switch';
import { cn } from '@kit/ui/utils';
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
@@ -11,19 +12,19 @@ const Switch = React.forwardRef<
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className
'peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input',
className,
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
'pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0',
)}
/>
</SwitchPrimitives.Root>
))
Switch.displayName = SwitchPrimitives.Root.displayName
));
Switch.displayName = SwitchPrimitives.Root.displayName;
export { Switch }
export { Switch };

13608
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff