@lobehub/chat 1.42.6 → 1.43.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/changelog/v1.json +21 -0
  3. package/docs/.cdn.cache.json +1 -0
  4. package/docs/changelog/2025-01-03-user-profile.mdx +27 -0
  5. package/docs/changelog/2025-01-03-user-profile.zh-CN.mdx +26 -0
  6. package/docs/self-hosting/advanced/auth/next-auth/wechat.mdx +3 -1
  7. package/docs/self-hosting/advanced/auth/next-auth/wechat.zh-CN.mdx +2 -2
  8. package/locales/ar/auth.json +83 -4
  9. package/locales/bg-BG/auth.json +82 -3
  10. package/locales/de-DE/auth.json +85 -6
  11. package/locales/en-US/auth.json +85 -6
  12. package/locales/es-ES/auth.json +82 -3
  13. package/locales/fa-IR/auth.json +84 -5
  14. package/locales/fr-FR/auth.json +85 -6
  15. package/locales/it-IT/auth.json +83 -4
  16. package/locales/ja-JP/auth.json +83 -4
  17. package/locales/ko-KR/auth.json +82 -3
  18. package/locales/nl-NL/auth.json +83 -4
  19. package/locales/pl-PL/auth.json +83 -4
  20. package/locales/pt-BR/auth.json +83 -4
  21. package/locales/ru-RU/auth.json +82 -3
  22. package/locales/tr-TR/auth.json +82 -3
  23. package/locales/vi-VN/auth.json +82 -3
  24. package/locales/zh-CN/auth.json +82 -3
  25. package/locales/zh-TW/auth.json +82 -3
  26. package/package.json +4 -3
  27. package/src/app/(main)/(mobile)/me/(home)/__tests__/UserBanner.test.tsx +4 -0
  28. package/src/app/(main)/(mobile)/me/(home)/__tests__/useCategory.test.tsx +0 -46
  29. package/src/app/(main)/(mobile)/me/(home)/features/UserBanner.tsx +11 -14
  30. package/src/app/(main)/(mobile)/me/(home)/features/useCategory.tsx +6 -21
  31. package/src/app/(main)/(mobile)/me/profile/features/Category.tsx +38 -21
  32. package/src/app/(main)/(mobile)/me/profile/layout.tsx +0 -3
  33. package/src/app/(main)/(mobile)/me/profile/page.tsx +3 -3
  34. package/src/app/(main)/chat/loading.tsx +2 -2
  35. package/src/app/(main)/discover/loading.tsx +2 -8
  36. package/src/app/(main)/files/loading.tsx +2 -2
  37. package/src/app/(main)/profile/(home)/Client.tsx +53 -0
  38. package/src/app/(main)/profile/(home)/[[...slugs]]/page.tsx +38 -0
  39. package/src/app/(main)/profile/@category/default.tsx +9 -0
  40. package/src/app/(main)/profile/@category/features/CategoryContent.tsx +38 -0
  41. package/src/app/(main)/profile/_layout/Desktop/Header.tsx +85 -0
  42. package/src/app/(main)/profile/_layout/Desktop/SideBar.tsx +42 -0
  43. package/src/app/(main)/profile/_layout/Desktop/index.tsx +48 -0
  44. package/src/app/(main)/profile/_layout/Mobile/Header.tsx +23 -5
  45. package/src/app/(main)/profile/_layout/Mobile/index.tsx +12 -5
  46. package/src/app/(main)/profile/_layout/type.ts +6 -0
  47. package/src/app/(main)/profile/error.tsx +5 -0
  48. package/src/app/(main)/profile/features/ClerkProfile.tsx +72 -0
  49. package/src/app/(main)/profile/hooks/useCategory.tsx +51 -0
  50. package/src/app/(main)/profile/layout.tsx +7 -17
  51. package/src/app/(main)/profile/loading.tsx +2 -22
  52. package/src/app/(main)/profile/not-found.tsx +3 -0
  53. package/src/app/(main)/profile/security/page.tsx +34 -0
  54. package/src/app/(main)/profile/stats/Client.tsx +52 -0
  55. package/src/app/(main)/profile/stats/features/AiHeatmaps.tsx +130 -0
  56. package/src/app/(main)/profile/stats/features/AssistantsRank.tsx +115 -0
  57. package/src/app/(main)/profile/stats/features/ModelsRank.tsx +84 -0
  58. package/src/app/(main)/profile/stats/features/ShareButton/Preview.tsx +159 -0
  59. package/src/app/(main)/profile/stats/features/ShareButton/ShareModal.tsx +87 -0
  60. package/src/app/(main)/profile/stats/features/ShareButton/TotalCard.tsx +39 -0
  61. package/src/app/(main)/profile/stats/features/ShareButton/index.tsx +26 -0
  62. package/src/app/(main)/profile/stats/features/TimeLabel.tsx +30 -0
  63. package/src/app/(main)/profile/stats/features/TopicsRank.tsx +104 -0
  64. package/src/app/(main)/profile/stats/features/TotalAssistants.tsx +56 -0
  65. package/src/app/(main)/profile/stats/features/TotalMessages.tsx +56 -0
  66. package/src/app/(main)/profile/stats/features/TotalTopics.tsx +53 -0
  67. package/src/app/(main)/profile/stats/features/TotalWords.tsx +54 -0
  68. package/src/app/(main)/profile/stats/features/Welcome.tsx +86 -0
  69. package/src/app/(main)/profile/{[[...slugs]] → stats}/page.tsx +4 -5
  70. package/src/app/(main)/repos/[id]/evals/dataset/page.tsx +2 -2
  71. package/src/app/(main)/repos/[id]/evals/evaluation/page.tsx +2 -2
  72. package/src/app/(main)/settings/@category/features/CategoryContent.tsx +1 -1
  73. package/src/app/(main)/settings/_layout/Desktop/index.tsx +1 -1
  74. package/src/app/(main)/settings/_layout/Mobile/Header.tsx +1 -1
  75. package/src/app/(main)/settings/_layout/Mobile/index.tsx +2 -0
  76. package/src/app/(main)/settings/common/features/Theme/index.tsx +2 -17
  77. package/src/app/(main)/settings/loading.tsx +2 -2
  78. package/src/components/Loading/BrandTextLoading/index.tsx +2 -2
  79. package/src/components/Statistic/index.tsx +15 -0
  80. package/src/components/StatisticCard/TitleWithPercentage.tsx +80 -0
  81. package/src/components/StatisticCard/growthPercentage.tsx +8 -0
  82. package/src/components/StatisticCard/index.tsx +209 -0
  83. package/src/const/url.ts +3 -3
  84. package/src/database/server/models/__tests__/message.test.ts +346 -35
  85. package/src/database/server/models/__tests__/session.test.ts +185 -2
  86. package/src/database/server/models/__tests__/topic.test.ts +136 -0
  87. package/src/database/server/models/__tests__/user.test.ts +140 -1
  88. package/src/database/server/models/message.ts +109 -14
  89. package/src/database/server/models/session.ts +76 -4
  90. package/src/database/server/models/topic.ts +44 -3
  91. package/src/database/server/models/user.ts +22 -0
  92. package/src/database/utils/genWhere.ts +39 -0
  93. package/src/features/ShareModal/ShareImage/index.tsx +11 -24
  94. package/src/features/ShareModal/ShareImage/type.ts +1 -6
  95. package/src/features/User/DataStatistics.tsx +21 -14
  96. package/src/features/User/UserPanel/PanelContent.tsx +12 -16
  97. package/src/features/User/UserPanel/useMenu.tsx +4 -6
  98. package/src/features/User/__tests__/PanelContent.test.tsx +4 -0
  99. package/src/features/User/__tests__/useMenu.test.tsx +1 -21
  100. package/src/hooks/useActiveTabKey.ts +34 -1
  101. package/src/{features/ShareModal/ShareImage → hooks}/useScreenshot.ts +51 -6
  102. package/src/locales/default/auth.ts +81 -2
  103. package/src/server/ld.test.ts +1 -1
  104. package/src/server/modules/AssistantStore/index.ts +3 -2
  105. package/src/server/routers/lambda/message.ts +35 -6
  106. package/src/server/routers/lambda/session.ts +17 -3
  107. package/src/server/routers/lambda/topic.ts +17 -3
  108. package/src/server/routers/lambda/user.ts +4 -0
  109. package/src/server/services/changelog/index.ts +1 -1
  110. package/src/services/message/_deprecated.ts +16 -0
  111. package/src/services/message/client.test.ts +0 -18
  112. package/src/services/message/client.ts +12 -9
  113. package/src/services/message/server.ts +12 -4
  114. package/src/services/message/type.ts +15 -3
  115. package/src/services/session/_deprecated.ts +5 -0
  116. package/src/services/session/client.ts +6 -2
  117. package/src/services/session/server.ts +6 -2
  118. package/src/services/session/type.ts +7 -1
  119. package/src/services/topic/_deprecated.ts +5 -0
  120. package/src/services/topic/client.ts +6 -2
  121. package/src/services/topic/server.ts +7 -1
  122. package/src/services/topic/type.ts +7 -2
  123. package/src/services/user/_deprecated.ts +4 -0
  124. package/src/services/user/client.ts +4 -0
  125. package/src/services/user/server.ts +4 -0
  126. package/src/services/user/type.ts +5 -0
  127. package/src/store/global/initialState.ts +6 -0
  128. package/src/store/user/slices/auth/action.test.ts +1 -33
  129. package/src/store/user/slices/auth/action.ts +0 -9
  130. package/src/store/user/slices/common/action.test.ts +2 -2
  131. package/src/types/message/index.ts +5 -0
  132. package/src/types/session/index.ts +8 -0
  133. package/src/types/topic/topic.ts +7 -0
  134. package/src/utils/format.ts +1 -1
  135. package/src/utils/time.ts +23 -0
  136. package/src/app/(main)/profile/[[...slugs]]/Client.tsx +0 -76
  137. package/src/components/Loading/BrandTextLoading/LobeChatText/SVG.tsx +0 -44
  138. package/src/components/Loading/BrandTextLoading/LobeChatText/index.tsx +0 -6
  139. package/src/components/Loading/BrandTextLoading/LobeChatText/style.css +0 -32
  140. package/src/hooks/useActiveSettingsKey.ts +0 -20
@@ -1,9 +1,11 @@
1
1
  'use client';
2
2
 
3
+ import Link from 'next/link';
3
4
  import { useRouter } from 'next/navigation';
4
5
  import { memo } from 'react';
5
6
  import { Flexbox } from 'react-layout-kit';
6
7
 
8
+ import { isDeprecatedEdition } from '@/const/version';
7
9
  import DataStatistics from '@/features/User/DataStatistics';
8
10
  import UserInfo from '@/features/User/UserInfo';
9
11
  import UserLoginOrSignup from '@/features/User/UserLoginOrSignup/Community';
@@ -21,21 +23,16 @@ const UserBanner = memo(() => {
21
23
 
22
24
  return (
23
25
  <Flexbox gap={12} paddingBlock={8}>
24
- {!enableAuth ? (
26
+ {!enableAuth || (enableAuth && isLoginWithAuth) ? (
25
27
  <>
26
- <UserInfo />
27
- <DataStatistics paddingInline={12} />
28
- </>
29
- ) : isLoginWithAuth ? (
30
- <>
31
- <UserInfo
32
- onClick={() => {
33
- // Profile page only works with Clerk
34
- if (enabledNextAuth) return;
35
- router.push('/me/profile');
36
- }}
37
- />
38
- <DataStatistics paddingInline={12} />
28
+ <Link href={'/profile'} style={{ color: 'inherit' }}>
29
+ <UserInfo />
30
+ </Link>
31
+ {!isDeprecatedEdition && (
32
+ <Link href={'/profile/stats'} style={{ color: 'inherit' }}>
33
+ <DataStatistics paddingInline={12} />
34
+ </Link>
35
+ )}
39
36
  </>
40
37
  ) : (
41
38
  <UserLoginOrSignup
@@ -6,7 +6,6 @@ import {
6
6
  Download,
7
7
  Feather,
8
8
  FileClockIcon,
9
- LogOut,
10
9
  Settings2,
11
10
  } from 'lucide-react';
12
11
  import { useRouter } from 'next/navigation';
@@ -28,15 +27,11 @@ export const useCategory = () => {
28
27
  const { canInstall, install } = usePWAInstall();
29
28
  const { t } = useTranslation(['common', 'setting', 'auth']);
30
29
  const { showCloudPromotion, hideDocs } = useServerConfigStore(featureFlagsSelectors);
31
- const [isLogin, isLoginWithAuth, isLoginWithClerk, enableAuth, signOut, isLoginWithNextAuth] =
32
- useUserStore((s) => [
33
- authSelectors.isLogin(s),
34
- authSelectors.isLoginWithAuth(s),
35
- authSelectors.isLoginWithClerk(s),
36
- authSelectors.enabledAuth(s),
37
- s.logout,
38
- authSelectors.isLoginWithNextAuth(s),
39
- ]);
30
+ const [isLogin, isLoginWithAuth, enableAuth] = useUserStore((s) => [
31
+ authSelectors.isLogin(s),
32
+ authSelectors.isLoginWithAuth(s),
33
+ authSelectors.enabledAuth(s),
34
+ ]);
40
35
 
41
36
  const profile: CellProps[] = [
42
37
  {
@@ -121,20 +116,11 @@ export const useCategory = () => {
121
116
  },
122
117
  ].filter(Boolean) as CellProps[];
123
118
 
124
- const nextAuthSignOut: CellProps[] = [
125
- {
126
- icon: LogOut,
127
- key: 'nextauthSignout',
128
- label: t('auth:signout'),
129
- onClick: signOut,
130
- },
131
- ];
132
-
133
119
  const mainItems = [
134
120
  {
135
121
  type: 'divider',
136
122
  },
137
- ...(isLoginWithClerk ? profile : []),
123
+ ...(!enableAuth || (enableAuth && isLoginWithAuth) ? profile : []),
138
124
  ...(enableAuth ? (isLoginWithAuth ? settings : []) : settingsWithoutAuth),
139
125
  /* ↓ cloud slot ↓ */
140
126
 
@@ -142,7 +128,6 @@ export const useCategory = () => {
142
128
  ...(canInstall ? pwa : []),
143
129
  ...(isLogin && !isServerMode ? data : []),
144
130
  ...(!hideDocs ? helps : []),
145
- ...(enableAuth && isLoginWithNextAuth ? nextAuthSignOut : []),
146
131
  ].filter(Boolean) as CellProps[];
147
132
 
148
133
  return mainItems;
@@ -1,43 +1,60 @@
1
1
  'use client';
2
2
 
3
- import { LogOut, ShieldCheck, UserCircle } from 'lucide-react';
3
+ import { ChartColumnBigIcon, LogOut, ShieldCheck, UserCircle } from 'lucide-react';
4
4
  import { useRouter } from 'next/navigation';
5
5
  import { memo } from 'react';
6
6
  import { useTranslation } from 'react-i18next';
7
7
 
8
8
  import Cell, { CellProps } from '@/components/Cell';
9
+ import { isDeprecatedEdition } from '@/const/version';
10
+ import { ProfileTabs } from '@/store/global/initialState';
9
11
  import { useUserStore } from '@/store/user';
12
+ import { authSelectors } from '@/store/user/slices/auth/selectors';
10
13
 
11
14
  const Category = memo(() => {
15
+ const [isLogin, enableAuth, isLoginWithClerk, signOut] = useUserStore((s) => [
16
+ authSelectors.isLogin(s),
17
+ authSelectors.enabledAuth(s),
18
+ authSelectors.isLoginWithClerk(s),
19
+ s.logout,
20
+ ]);
12
21
  const router = useRouter();
13
22
  const { t } = useTranslation('auth');
14
- const signOut = useUserStore((s) => s.logout);
15
23
  const items: CellProps[] = [
16
24
  {
17
25
  icon: UserCircle,
18
- key: 'profile',
19
- label: t('profile'),
26
+ key: ProfileTabs.Profile,
27
+ label: t('tab.profile'),
20
28
  onClick: () => router.push('/profile'),
21
29
  },
22
- {
23
- icon: ShieldCheck,
24
- key: 'security',
25
- label: t('security'),
26
- onClick: () => router.push('/profile/security'),
27
- },
28
- {
29
- type: 'divider',
30
- },
31
- {
32
- icon: LogOut,
33
- key: 'logout',
34
- label: t('signout', { ns: 'auth' }),
35
- onClick: () => {
36
- signOut();
37
- router.push('/login');
30
+ enableAuth &&
31
+ isLoginWithClerk && {
32
+ icon: ShieldCheck,
33
+ key: ProfileTabs.Security,
34
+ label: t('tab.security'),
35
+ onClick: () => router.push('/profile/security'),
38
36
  },
37
+ !isDeprecatedEdition && {
38
+ icon: ChartColumnBigIcon,
39
+ key: ProfileTabs.Stats,
40
+ label: t('tab.stats'),
41
+ onClick: () => router.push('/profile/stats'),
39
42
  },
40
- ];
43
+ enableAuth &&
44
+ isLogin && {
45
+ type: 'divider',
46
+ },
47
+ enableAuth &&
48
+ isLogin && {
49
+ icon: LogOut,
50
+ key: 'logout',
51
+ label: t('signout', { ns: 'auth' }),
52
+ onClick: () => {
53
+ signOut();
54
+ router.push('/login');
55
+ },
56
+ },
57
+ ].filter(Boolean) as CellProps[];
41
58
 
42
59
  return items?.map((item, index) => <Cell key={item.key || index} {...item} />);
43
60
  });
@@ -1,13 +1,10 @@
1
- import { notFound } from 'next/navigation';
2
1
  import { PropsWithChildren } from 'react';
3
2
 
4
3
  import MobileContentLayout from '@/components/server/MobileNavLayout';
5
- import { enableClerk } from '@/const/auth';
6
4
 
7
5
  import Header from './features/Header';
8
6
 
9
7
  const Layout = ({ children }: PropsWithChildren) => {
10
- if (!enableClerk) return notFound();
11
8
  return <MobileContentLayout header={<Header />}>{children}</MobileContentLayout>;
12
9
  };
13
10
 
@@ -7,10 +7,10 @@ import { isMobileDevice } from '@/utils/server/responsive';
7
7
  import Category from './features/Category';
8
8
 
9
9
  export const generateMetadata = async () => {
10
- const { t } = await translation('clerk');
10
+ const { t } = await translation('auth');
11
11
  return metadataModule.generate({
12
- description: t('userProfile.navbar.title'),
13
- title: t('userProfile.navbar.description'),
12
+ description: t('header.desc'),
13
+ title: t('header.title'),
14
14
  url: '/me/profile',
15
15
  });
16
16
  };
@@ -1,3 +1,3 @@
1
- import CircleLoading from '@/components/Loading/BrandTextLoading';
1
+ import Loading from '@/components/Loading/BrandTextLoading';
2
2
 
3
- export default () => <CircleLoading />;
3
+ export default () => <Loading />;
@@ -1,9 +1,3 @@
1
- import { Center } from 'react-layout-kit';
1
+ import Loading from '@/components/Loading/BrandTextLoading';
2
2
 
3
- import CircleLoading from '@/components/Loading/BrandTextLoading';
4
-
5
- export default () => (
6
- <Center height={'90vh'} width={'100%'}>
7
- <CircleLoading />
8
- </Center>
9
- );
3
+ export default () => <Loading />;
@@ -1,3 +1,3 @@
1
- import CircleLoading from '@/components/Loading/BrandTextLoading';
1
+ import Loading from '@/components/Loading/BrandTextLoading';
2
2
 
3
- export default () => <CircleLoading />;
3
+ export default () => <Loading />;
@@ -0,0 +1,53 @@
1
+ 'use client';
2
+
3
+ import { Form, type ItemGroup } from '@lobehub/ui';
4
+ import { memo } from 'react';
5
+ import { useTranslation } from 'react-i18next';
6
+
7
+ import { FORM_STYLE } from '@/const/layoutTokens';
8
+ import AvatarWithUpload from '@/features/AvatarWithUpload';
9
+ import UserAvatar from '@/features/User/UserAvatar';
10
+ import { useUserStore } from '@/store/user';
11
+ import { authSelectors, userProfileSelectors } from '@/store/user/selectors';
12
+
13
+ type SettingItemGroup = ItemGroup;
14
+
15
+ const Client = memo<{ mobile?: boolean }>(() => {
16
+ const [isLoginWithNextAuth] = useUserStore((s) => [authSelectors.isLoginWithNextAuth(s)]);
17
+ const [enableAuth, nickname, username, userProfile] = useUserStore((s) => [
18
+ s.enableAuth(),
19
+ userProfileSelectors.nickName(s),
20
+ userProfileSelectors.username(s),
21
+ userProfileSelectors.userProfile(s),
22
+ ]);
23
+
24
+ const [form] = Form.useForm();
25
+ const { t } = useTranslation('auth');
26
+
27
+ const profile: SettingItemGroup = {
28
+ children: [
29
+ {
30
+ children: enableAuth && isLoginWithNextAuth ? <UserAvatar /> : <AvatarWithUpload />,
31
+ label: t('profile.avatar'),
32
+ minWidth: undefined,
33
+ },
34
+ {
35
+ children: nickname || username,
36
+ label: t('profile.username'),
37
+ minWidth: undefined,
38
+ },
39
+ {
40
+ children: userProfile?.email || '--',
41
+ hidden: !isLoginWithNextAuth || !userProfile?.email,
42
+ label: t('profile.email'),
43
+ minWidth: undefined,
44
+ },
45
+ ],
46
+ title: t('tab.profile'),
47
+ };
48
+ return (
49
+ <Form form={form} items={[profile]} itemsType={'group'} variant={'pure'} {...FORM_STYLE} />
50
+ );
51
+ });
52
+
53
+ export default Client;
@@ -0,0 +1,38 @@
1
+ import { Skeleton } from 'antd';
2
+ import dynamic from 'next/dynamic';
3
+
4
+ import { enableClerk } from '@/const/auth';
5
+ import { metadataModule } from '@/server/metadata';
6
+ import { translation } from '@/server/translation';
7
+ import { isMobileDevice } from '@/utils/server/responsive';
8
+
9
+ import Client from '../Client';
10
+
11
+ // 为了兼容 ClerkProfile, 需要使用 [[...slug]]
12
+
13
+ const ClerkProfile = dynamic(() => import('../../features/ClerkProfile'), {
14
+ loading: () => (
15
+ <div style={{ flex: 1 }}>
16
+ <Skeleton paragraph={{ rows: 8 }} title={false} />
17
+ </div>
18
+ ),
19
+ });
20
+
21
+ export const generateMetadata = async () => {
22
+ const { t } = await translation('auth');
23
+ return metadataModule.generate({
24
+ description: t('header.desc'),
25
+ title: t('tab.profile'),
26
+ url: '/profile',
27
+ });
28
+ };
29
+
30
+ const Page = async () => {
31
+ const mobile = await isMobileDevice();
32
+
33
+ if (enableClerk) return <ClerkProfile mobile={mobile} />;
34
+
35
+ return <Client mobile={mobile} />;
36
+ };
37
+
38
+ export default Page;
@@ -0,0 +1,9 @@
1
+ import CategoryContent from './features/CategoryContent';
2
+
3
+ const Category = () => {
4
+ return <CategoryContent />;
5
+ };
6
+
7
+ Category.displayName = 'SettingCategory';
8
+
9
+ export default Category;
@@ -0,0 +1,38 @@
1
+ 'use client';
2
+
3
+ import { memo } from 'react';
4
+ import urlJoin from 'url-join';
5
+
6
+ import Menu from '@/components/Menu';
7
+ import { useActiveSettingsKey } from '@/hooks/useActiveTabKey';
8
+ import { useQuery } from '@/hooks/useQuery';
9
+ import { useQueryRoute } from '@/hooks/useQueryRoute';
10
+ import { ProfileTabs, SettingsTabs } from '@/store/global/initialState';
11
+
12
+ import { useCategory } from '../../hooks/useCategory';
13
+
14
+ const CategoryContent = memo<{ modal?: boolean }>(({ modal }) => {
15
+ const activeTab = useActiveSettingsKey();
16
+ const { tab = SettingsTabs.Common } = useQuery();
17
+ const cateItems = useCategory();
18
+ const router = useQueryRoute();
19
+
20
+ return (
21
+ <Menu
22
+ items={cateItems}
23
+ onClick={({ key }) => {
24
+ const activeKey = key === ProfileTabs.Profile ? '/' : key;
25
+ if (modal) {
26
+ router.replace('/profile/modal', { query: { tab: activeKey } });
27
+ } else {
28
+ router.push(urlJoin('/profile', activeKey));
29
+ }
30
+ }}
31
+ selectable
32
+ selectedKeys={[modal ? tab : (activeTab as any)]}
33
+ variant={'compact'}
34
+ />
35
+ );
36
+ });
37
+
38
+ export default CategoryContent;
@@ -0,0 +1,85 @@
1
+ 'use client';
2
+
3
+ import { ActionIcon, ChatHeader, ChatHeaderTitle } from '@lobehub/ui';
4
+ import { Drawer, type DrawerProps } from 'antd';
5
+ import { createStyles } from 'antd-style';
6
+ import { Menu } from 'lucide-react';
7
+ import { ReactNode, memo, useState } from 'react';
8
+ import { Flexbox } from 'react-layout-kit';
9
+
10
+ import BrandWatermark from '@/components/BrandWatermark';
11
+
12
+ const useStyles = createStyles(({ token, css }) => ({
13
+ container: css`
14
+ position: relative;
15
+ flex: none;
16
+ height: 54px;
17
+ background: ${token.colorBgContainer};
18
+ `,
19
+ title: css`
20
+ font-size: 18px;
21
+ font-weight: 700;
22
+ line-height: 1.2;
23
+ `,
24
+ }));
25
+
26
+ interface HeaderProps extends Pick<DrawerProps, 'getContainer'> {
27
+ children: ReactNode;
28
+ title: ReactNode;
29
+ }
30
+
31
+ const Header = memo<HeaderProps>(({ children, getContainer, title }) => {
32
+ const [open, setOpen] = useState(false);
33
+ const { styles, theme } = useStyles();
34
+
35
+ return (
36
+ <>
37
+ <ChatHeader
38
+ className={styles.container}
39
+ left={
40
+ <ChatHeaderTitle
41
+ title={
42
+ <Flexbox align={'center'} className={styles.title} gap={4} horizontal>
43
+ <ActionIcon
44
+ color={theme.colorText}
45
+ icon={Menu}
46
+ onClick={() => setOpen(true)}
47
+ size={{ blockSize: 32, fontSize: 18 }}
48
+ />
49
+ {title}
50
+ </Flexbox>
51
+ }
52
+ />
53
+ }
54
+ />
55
+ <Drawer
56
+ bodyStyle={{
57
+ display: 'flex',
58
+ flexDirection: 'column',
59
+ gap: 20,
60
+ justifyContent: 'space-between',
61
+ padding: 16,
62
+ }}
63
+ getContainer={getContainer}
64
+ headerStyle={{ display: 'none' }}
65
+ maskStyle={{ background: 'transparent' }}
66
+ onClick={() => setOpen(false)}
67
+ onClose={() => setOpen(false)}
68
+ open={open}
69
+ placement={'left'}
70
+ rootStyle={{ position: 'absolute' }}
71
+ style={{
72
+ background: theme.colorBgContainer,
73
+ borderRight: `1px solid ${theme.colorSplit}`,
74
+ }}
75
+ width={260}
76
+ zIndex={10}
77
+ >
78
+ {children}
79
+ <BrandWatermark paddingInline={12} />
80
+ </Drawer>
81
+ </>
82
+ );
83
+ });
84
+
85
+ export default Header;
@@ -0,0 +1,42 @@
1
+ 'use client';
2
+
3
+ import { createStyles } from 'antd-style';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { Flexbox, FlexboxProps } from 'react-layout-kit';
6
+
7
+ import BrandWatermark from '@/components/BrandWatermark';
8
+ import PanelTitle from '@/components/PanelTitle';
9
+
10
+ const useStyles = createStyles(({ token, css }) => ({
11
+ container: css`
12
+ padding-block: 0 16px;
13
+ padding-inline: 12px;
14
+ background: ${token.colorBgContainer};
15
+ border-inline-end: 1px solid ${token.colorBorder};
16
+ `,
17
+ }));
18
+
19
+ interface SidebarLayoutProps extends FlexboxProps {
20
+ desc?: string;
21
+ title?: string;
22
+ }
23
+
24
+ const SidebarLayout = ({ children, className, title, desc, ...rest }: SidebarLayoutProps) => {
25
+ const { cx, styles } = useStyles();
26
+ const { t } = useTranslation('auth');
27
+ return (
28
+ <Flexbox
29
+ className={cx(styles.container, className)}
30
+ flex={'none'}
31
+ gap={20}
32
+ width={280}
33
+ {...rest}
34
+ >
35
+ <PanelTitle desc={desc || t('header.desc')} title={title || t('header.title')} />
36
+ {children}
37
+ <BrandWatermark paddingInline={12} />
38
+ </Flexbox>
39
+ );
40
+ };
41
+
42
+ export default SidebarLayout;
@@ -0,0 +1,48 @@
1
+ 'use client';
2
+
3
+ import { useResponsive } from 'antd-style';
4
+ import { memo, useRef } from 'react';
5
+ import { useTranslation } from 'react-i18next';
6
+ import { Flexbox } from 'react-layout-kit';
7
+
8
+ import InitClientDB from '@/features/InitClientDB';
9
+ import Footer from '@/features/Setting/Footer';
10
+ import SettingContainer from '@/features/Setting/SettingContainer';
11
+ import { useActiveProfileKey } from '@/hooks/useActiveTabKey';
12
+
13
+ import { LayoutProps } from '../type';
14
+ import Header from './Header';
15
+ import SideBar from './SideBar';
16
+
17
+ const Layout = memo<LayoutProps>(({ children, category }) => {
18
+ const ref = useRef<any>(null);
19
+ const { md = true } = useResponsive();
20
+ const { t } = useTranslation('auth');
21
+ const activeKey = useActiveProfileKey();
22
+
23
+ return (
24
+ <>
25
+ <Flexbox
26
+ height={'100%'}
27
+ horizontal={md}
28
+ ref={ref}
29
+ style={{ position: 'relative' }}
30
+ width={'100%'}
31
+ >
32
+ {md ? (
33
+ <SideBar>{category}</SideBar>
34
+ ) : (
35
+ <Header getContainer={() => ref.current} title={<>{t(`tab.${activeKey}`)}</>}>
36
+ {category}
37
+ </Header>
38
+ )}
39
+ <SettingContainer addonAfter={<Footer />}>{children}</SettingContainer>
40
+ </Flexbox>
41
+ <InitClientDB />
42
+ </>
43
+ );
44
+ });
45
+
46
+ Layout.displayName = 'DesktopProfileLayout';
47
+
48
+ export default Layout;
@@ -1,22 +1,40 @@
1
1
  'use client';
2
2
 
3
3
  import { MobileNavBar, MobileNavBarTitle } from '@lobehub/ui';
4
- import { usePathname, useRouter } from 'next/navigation';
4
+ import { useRouter } from 'next/navigation';
5
5
  import { memo } from 'react';
6
6
  import { useTranslation } from 'react-i18next';
7
+ import { Flexbox } from 'react-layout-kit';
7
8
 
9
+ import { useActiveProfileKey } from '@/hooks/useActiveTabKey';
8
10
  import { mobileHeaderSticky } from '@/styles/mobileHeader';
9
11
 
12
+ import ShareButton from '../../stats/features/ShareButton';
13
+
10
14
  const Header = memo(() => {
11
15
  const { t } = useTranslation('auth');
12
16
 
13
17
  const router = useRouter();
14
- const pathname = usePathname();
15
- const isSecurity = pathname.startsWith('/prifile/security');
18
+ const activeSettingsKey = useActiveProfileKey();
19
+ const isStats = activeSettingsKey === 'stats';
20
+
21
+ const handleBackClick = () => {
22
+ router.push('/me/profile');
23
+ };
24
+
16
25
  return (
17
26
  <MobileNavBar
18
- center={<MobileNavBarTitle title={t(isSecurity ? 'security' : 'profile')} />}
19
- onBackClick={() => router.push('/me/profile')}
27
+ center={
28
+ <MobileNavBarTitle
29
+ title={
30
+ <Flexbox align={'center'} gap={8} horizontal>
31
+ <span style={{ lineHeight: 1.2 }}> {t(`tab.${activeSettingsKey}`)}</span>
32
+ </Flexbox>
33
+ }
34
+ />
35
+ }
36
+ onBackClick={handleBackClick}
37
+ right={isStats ? <ShareButton mobile /> : undefined}
20
38
  showBackButton
21
39
  style={mobileHeaderSticky}
22
40
  />
@@ -1,16 +1,23 @@
1
- import { PropsWithChildren } from 'react';
1
+ import MobileContentLayout from '@/components/server/MobileNavLayout';
2
+ import InitClientDB from '@/features/InitClientDB';
3
+ import Footer from '@/features/Setting/Footer';
2
4
 
5
+ import { LayoutProps } from '../type';
3
6
  import Header from './Header';
4
7
 
5
- const Layout = ({ children }: PropsWithChildren) => {
8
+ const Layout = ({ children }: LayoutProps) => {
6
9
  return (
7
10
  <>
8
- <Header />
9
- {children}
11
+ <MobileContentLayout header={<Header />}>
12
+ {children}
13
+ <div style={{ flex: 1 }} />
14
+ <Footer />
15
+ </MobileContentLayout>
16
+ <InitClientDB />
10
17
  </>
11
18
  );
12
19
  };
13
20
 
14
- Layout.displayName = 'ProfileMobileLayout';
21
+ Layout.displayName = 'MobileProfileLayout';
15
22
 
16
23
  export default Layout;
@@ -0,0 +1,6 @@
1
+ import { ReactNode } from 'react';
2
+
3
+ export interface LayoutProps {
4
+ category: ReactNode;
5
+ children: ReactNode;
6
+ }
@@ -0,0 +1,5 @@
1
+ 'use client';
2
+
3
+ import dynamic from 'next/dynamic';
4
+
5
+ export default dynamic(() => import('@/components/Error'));