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:
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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()}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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..."
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user