Update theme toggle functionality and UI components

Implemented a new ModeToggle feature for theme switching in personal account dropdown. The changes also made adjustments to several UI components, such as transforming Dialog to AlertDialog in transfer-ownership-dialog, and introducing invitation-submit-button in team-accounts. Some minor amendments include text changes and styling modifications.
This commit is contained in:
giancarlo
2024-03-28 20:29:54 +08:00
parent caca7c12f6
commit f6d1b500da
30 changed files with 1318 additions and 810 deletions

View File

@@ -17,7 +17,7 @@ import {
import { Trans } from '@kit/ui/trans';
import { cn } from '@kit/ui/utils';
import { ProfileDropdownContainer } from '~/(dashboard)/home/_components/personal-account-dropdown';
import { ProfileAccountDropdownContainer } from '~/(dashboard)/home/_components/personal-account-dropdown';
import featureFlagsConfig from '~/config/feature-flags.config';
import pathsConfig from '~/config/paths.config';
@@ -89,7 +89,7 @@ function SidebarContainer(props: {
<div className={'absolute bottom-4 left-0 w-full'}>
<SidebarContent>
<ProfileDropdownContainer
<ProfileAccountDropdownContainer
session={props.session}
collapsed={props.collapsed}
/>

View File

@@ -5,7 +5,7 @@ import { cookies } from 'next/headers';
import { Sidebar, SidebarContent, SidebarNavigation } from '@kit/ui/sidebar';
import { HomeSidebarAccountSelector } from '~/(dashboard)/home/_components/home-sidebar-account-selector';
import { ProfileDropdownContainer } from '~/(dashboard)/home/_components/personal-account-dropdown';
import { ProfileAccountDropdownContainer } from '~/(dashboard)/home/_components/personal-account-dropdown';
import { loadUserWorkspace } from '~/(dashboard)/home/_lib/load-user-workspace';
import { personalAccountSidebarConfig } from '~/config/personal-account-sidebar.config';
@@ -25,7 +25,10 @@ export function HomeSidebar() {
<div className={'absolute bottom-4 left-0 w-full'}>
<SidebarContent>
<ProfileDropdownContainer session={session} collapsed={collapsed} />
<ProfileAccountDropdownContainer
session={session}
collapsed={collapsed}
/>
</SidebarContent>
</div>
</Sidebar>

View File

@@ -5,9 +5,10 @@ import type { Session } from '@supabase/supabase-js';
import { PersonalAccountDropdown } from '@kit/accounts/personal-account-dropdown';
import { useSignOut } from '@kit/supabase/hooks/use-sign-out';
import featuresFlagConfig from '~/config/feature-flags.config';
import pathsConfig from '~/config/paths.config';
export function ProfileDropdownContainer(props: {
export function ProfileAccountDropdownContainer(props: {
collapsed: boolean;
session: Session | null;
}) {
@@ -19,6 +20,9 @@ export function ProfileDropdownContainer(props: {
paths={{
home: pathsConfig.app.home,
}}
features={{
enableThemeToggle: featuresFlagConfig.enableThemeToggle,
}}
className={'w-full'}
showProfileName={!props.collapsed}
session={props.session}

View File

@@ -13,6 +13,7 @@ import { Button } from '@kit/ui/button';
import { If } from '@kit/ui/if';
import { Trans } from '@kit/ui/trans';
import featuresFlagConfig from '~/config/feature-flags.config';
import pathsConfig from '~/config/paths.config';
export function SiteHeaderAccountSection(
@@ -38,6 +39,9 @@ function SuspendedPersonalAccountDropdown(props: { session: Session | null }) {
paths={{
home: pathsConfig.app.home,
}}
features={{
enableThemeToggle: featuresFlagConfig.enableThemeToggle,
}}
session={session}
signOutRequested={() => signOut.mutateAsync()}
/>

View File

@@ -1,5 +1,7 @@
import type { Session } from '@supabase/supabase-js';
import { ModeToggle } from '@kit/ui/mode-toggle';
import { SiteHeaderAccountSection } from '~/(marketing)/_components/site-header-account-section';
import { SiteNavigation } from '~/(marketing)/_components/site-navigation';
import { AppLogo } from '~/components/app-logo';
@@ -17,13 +19,11 @@ export function SiteHeader(props: { session?: Session | null }) {
</div>
<div className={'flex flex-1 items-center justify-end space-x-4'}>
<div className={'flex items-center'}></div>
<div className={'flex items-center'}>
<ModeToggle />
</div>
<SiteHeaderAccountSection session={props.session ?? null} />
<div className={'flex lg:hidden'}>
<SiteNavigation />
</div>
</div>
</div>
</div>

View File

@@ -1,7 +1,11 @@
import { notFound } from 'next/navigation';
import { notFound, redirect } from 'next/navigation';
import { Logger } from '@kit/shared/logger';
import { requireAuth } from '@kit/supabase/require-auth';
import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client';
import { AcceptInvitationContainer } from '@kit/team-accounts/components';
import pathsConfig from '~/config/paths.config';
import { withI18n } from '~/lib/i18n/with-i18n';
interface Context {
@@ -10,23 +14,98 @@ interface Context {
};
}
export const metadata = {
title: `Join Organization`,
export const generateMetadata = () => {
return {
title: 'Join Team Account',
};
};
async function JoinTeamAccountPage({ searchParams }: Context) {
const token = searchParams.invite_token;
const data = await getInviteDataFromInviteToken(token);
if (!data) {
// no token, redirect to 404
if (!token) {
notFound();
}
return <></>;
const client = getSupabaseServerComponentClient();
const session = await requireAuth(client);
// if the user is not logged in or there is an error
// redirect to the sign up page with the invite token
// so that they will get back to this page after signing up
if (session.error ?? !session.data) {
redirect(pathsConfig.auth.signUp + '?invite_token=' + token);
}
// the user is logged in, we can now check if the token is valid
const invitation = await getInviteDataFromInviteToken(token);
if (!invitation) {
notFound();
}
// we need to verify the user isn't already in the account
const isInAccount = await isCurrentUserAlreadyInAccount(
invitation.account.id,
);
if (isInAccount) {
Logger.warn(
{
name: 'join-team-account',
accountId: invitation.account.id,
userId: session.data.user.id,
},
'User is already in the account. Redirecting to account page.',
);
// if the user is already in the account redirect to the home page
redirect(pathsConfig.app.home);
}
// if the user decides to sign in with a different account
// we redirect them to the sign in page with the invite token
const signOutNext = pathsConfig.auth.signIn + '?invite_token=' + token;
// once the user accepts the invitation, we redirect them to the account home page
const accountHome = pathsConfig.app.accountHome.replace(
'[account]',
invitation.account.slug,
);
return (
<AcceptInvitationContainer
inviteToken={token}
invitation={invitation}
paths={{
signOutNext,
accountHome,
}}
/>
);
}
export default withI18n(JoinTeamAccountPage);
/**
* Verifies that the current user is not already in the account by
* reading the document from the `accounts` table. If the user can read it
* it means they are already in the account.
* @param accountId
*/
async function isCurrentUserAlreadyInAccount(accountId: string) {
const client = getSupabaseServerComponentClient();
const { data } = await client
.from('accounts')
.select('id')
.eq('id', accountId)
.maybeSingle();
return !!data?.id;
}
async function getInviteDataFromInviteToken(token: string) {
// we use an admin client to be able to read the pending membership
// without having to be logged in
@@ -34,7 +113,18 @@ async function getInviteDataFromInviteToken(token: string) {
const { data: invitation, error } = await adminClient
.from('invitations')
.select()
.select<
string,
{
id: string;
account: {
id: string;
name: string;
slug: string;
picture_url: string;
};
}
>('id, account: account_id !inner (id, name, slug, picture_url)')
.eq('invite_token', token)
.single();

View File

@@ -40,7 +40,7 @@ function getClassName() {
const theme = themeCookie ?? appConfig.theme;
const dark = theme === 'dark';
return cn('antialiased', {
return cn('min-h-screen bg-background antialiased', {
dark,
[sans.className]: true,
});

View File

@@ -2,10 +2,12 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryStreamedHydration } from '@tanstack/react-query-next-experimental';
import { ThemeProvider } from 'next-themes';
import { I18nProvider } from '@kit/i18n/provider';
import { AuthChangeListener } from '@kit/supabase/components/auth-change-listener';
import appConfig from '~/config/app.config';
import pathsConfig from '~/config/paths.config';
import { i18nResolver } from '~/lib/i18n/i18n.resolver';
@@ -22,7 +24,14 @@ export function RootProviders({
<ReactQueryStreamedHydration>
<AuthChangeListener appHomePath={pathsConfig.app.home}>
<I18nProvider lang={lang} resolver={i18nResolver}>
{children}
<ThemeProvider
attribute="class"
enableSystem
disableTransitionOnChange
defaultTheme={appConfig.theme}
>
{children}
</ThemeProvider>
</I18nProvider>
</AuthChangeListener>
</ReactQueryStreamedHydration>

View File

@@ -1,7 +1,7 @@
import { z } from 'zod';
const FeatureFlagsSchema = z.object({
enableThemeSwitcher: z.boolean(),
enableThemeToggle: z.boolean(),
enableAccountDeletion: z.boolean(),
enableTeamDeletion: z.boolean(),
enableTeamAccounts: z.boolean(),
@@ -11,7 +11,7 @@ const FeatureFlagsSchema = z.object({
});
const featuresFlagConfig = FeatureFlagsSchema.parse({
enableThemeSwitcher: true,
enableThemeToggle: true,
enableAccountDeletion: getBoolean(
process.env.NEXT_PUBLIC_ENABLE_ACCOUNT_DELETION,
false,

View File

@@ -121,5 +121,12 @@
"updateRoleErrorMessage": "We encountered an error updating the role of the selected member. Please try again.",
"searchInvitations": "Search Invitations",
"updateInvitation": "Update Invitation",
"removeInvitation": "Remove Invitation"
"removeInvitation": "Remove Invitation",
"acceptInvitation": "Accept Invitation",
"signInWithDifferentAccount": "Sign in with a different account",
"signInWithDifferentAccountDescription": "If you wish to accept the invitation with a different account, please sign out and back in with the account you wish to use.",
"acceptInvitationHeading": "Accept Invitation to join {{accountName}}",
"acceptInvitationDescription": "You have been invited to join the team {{accountName}}. If you wish to accept the invitation, please click the button below.",
"joinTeam": "Join {{accountName}}",
"joiningTeam": "Joining team..."
}