@allbluecn/admin-app 0.1.0

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 (276) hide show
  1. package/package.json +86 -0
  2. package/src/client.tsx +31 -0
  3. package/src/components/admin/admin-user-menu.tsx +90 -0
  4. package/src/components/admin/developer-log.tsx +356 -0
  5. package/src/components/ai-provider/model-list-editor.tsx +147 -0
  6. package/src/components/ai-provider/provider-form.tsx +434 -0
  7. package/src/components/ai-provider/provider-icon.tsx +43 -0
  8. package/src/components/ai-provider/provider-logo.tsx +87 -0
  9. package/src/components/ai-provider/provider-table.tsx +258 -0
  10. package/src/components/auth/admin-login-form.tsx +214 -0
  11. package/src/components/auth/auth-brand-panel.tsx +60 -0
  12. package/src/components/auth/auth-header.tsx +38 -0
  13. package/src/components/auth/auth-layout.tsx +66 -0
  14. package/src/components/auth/auth-providers.tsx +34 -0
  15. package/src/components/auth/icon-input.tsx +46 -0
  16. package/src/components/auth/oauth-section.tsx +51 -0
  17. package/src/components/auth/password-input.tsx +67 -0
  18. package/src/components/common/page-header.tsx +42 -0
  19. package/src/components/common/placeholder-page.tsx +42 -0
  20. package/src/components/component/component-form-dialog.tsx +348 -0
  21. package/src/components/component/component-import-dialog.tsx +235 -0
  22. package/src/components/component/component-table.tsx +153 -0
  23. package/src/components/data-table/data-table-pagination.tsx +114 -0
  24. package/src/components/data-table/data-table.tsx +218 -0
  25. package/src/components/data-table/table-detail-layout.tsx +99 -0
  26. package/src/components/dependency/changelog-dialog.tsx +108 -0
  27. package/src/components/dependency/columns.tsx +232 -0
  28. package/src/components/dependency/dependency-table.tsx +368 -0
  29. package/src/components/dependency/edit-dialog.tsx +200 -0
  30. package/src/components/knowledge-public/bookmark-form-dialog.tsx +215 -0
  31. package/src/components/knowledge-public/compliance-form-dialog.tsx +308 -0
  32. package/src/components/knowledge-public/legal-form-dialog.tsx +315 -0
  33. package/src/components/layout/sidebar-02/app-sidebar.tsx +160 -0
  34. package/src/components/layout/sidebar-02/index.tsx +33 -0
  35. package/src/components/layout/sidebar-02/logo.tsx +40 -0
  36. package/src/components/layout/sidebar-02/nav-main.tsx +153 -0
  37. package/src/components/layout/sidebar-02/nav-notifications.tsx +78 -0
  38. package/src/components/layout/sidebar-02/team-switcher.tsx +106 -0
  39. package/src/components/magicui/animated-theme-toggler.tsx +272 -0
  40. package/src/components/magicui/rainbow-button.tsx +78 -0
  41. package/src/components/market/columns.tsx +151 -0
  42. package/src/components/market/market-app-form.tsx +237 -0
  43. package/src/components/market/market-app-list.tsx +156 -0
  44. package/src/components/monitoring/error-list.tsx +143 -0
  45. package/src/components/monitoring/health-cards.tsx +152 -0
  46. package/src/components/monitoring/metrics-stats.tsx +184 -0
  47. package/src/components/monitoring/request-log-table.tsx +179 -0
  48. package/src/components/providers/theme-toggle.tsx +59 -0
  49. package/src/components/settings/avatar/avatar-config.ts +46 -0
  50. package/src/components/ui/alert-dialog.tsx +216 -0
  51. package/src/components/ui/alert.tsx +76 -0
  52. package/src/components/ui/avatar.tsx +129 -0
  53. package/src/components/ui/badge.tsx +66 -0
  54. package/src/components/ui/button.tsx +84 -0
  55. package/src/components/ui/card.tsx +120 -0
  56. package/src/components/ui/checkbox.tsx +50 -0
  57. package/src/components/ui/collapsible.tsx +50 -0
  58. package/src/components/ui/dialog.tsx +185 -0
  59. package/src/components/ui/dropdown-menu.tsx +286 -0
  60. package/src/components/ui/field.tsx +255 -0
  61. package/src/components/ui/input-group.tsx +171 -0
  62. package/src/components/ui/input.tsx +36 -0
  63. package/src/components/ui/label.tsx +41 -0
  64. package/src/components/ui/pagination.tsx +146 -0
  65. package/src/components/ui/select.tsx +209 -0
  66. package/src/components/ui/separator.tsx +45 -0
  67. package/src/components/ui/sheet.tsx +164 -0
  68. package/src/components/ui/sidebar.tsx +719 -0
  69. package/src/components/ui/skeleton.tsx +30 -0
  70. package/src/components/ui/sonner.tsx +37 -0
  71. package/src/components/ui/switch.tsx +48 -0
  72. package/src/components/ui/table.tsx +133 -0
  73. package/src/components/ui/textarea.tsx +35 -0
  74. package/src/components/ui/tooltip.tsx +74 -0
  75. package/src/components/user/columns.tsx +153 -0
  76. package/src/components/user/settings-activity-log.tsx +358 -0
  77. package/src/components/user/settings-profile/constants.ts +42 -0
  78. package/src/components/user/settings-profile/types.ts +47 -0
  79. package/src/components/user/settings-profile.tsx +664 -0
  80. package/src/components/user/user-create-dialog.tsx +137 -0
  81. package/src/components/user/user-filters.tsx +117 -0
  82. package/src/hooks/use-mobile.ts +36 -0
  83. package/src/instrument.client.ts +34 -0
  84. package/src/lib/anyhub/capabilities.ts +59 -0
  85. package/src/lib/anyhub/types.ts +43 -0
  86. package/src/lib/auth-client.ts +83 -0
  87. package/src/lib/auth-utils.ts +24 -0
  88. package/src/lib/auth.config.ts +264 -0
  89. package/src/lib/avatar.ts +41 -0
  90. package/src/lib/i18n/client.ts +32 -0
  91. package/src/lib/i18n/config.ts +40 -0
  92. package/src/lib/i18n/detect.ts +43 -0
  93. package/src/lib/i18n/locales/en/admin-ai.json +128 -0
  94. package/src/lib/i18n/locales/en/admin-billing.json +56 -0
  95. package/src/lib/i18n/locales/en/admin-users.json +88 -0
  96. package/src/lib/i18n/locales/en/common.json +877 -0
  97. package/src/lib/i18n/locales/en/enterprise.json +208 -0
  98. package/src/lib/i18n/locales/zh/admin-ai.json +128 -0
  99. package/src/lib/i18n/locales/zh/admin-billing.json +56 -0
  100. package/src/lib/i18n/locales/zh/admin-users.json +88 -0
  101. package/src/lib/i18n/locales/zh/common.json +877 -0
  102. package/src/lib/i18n/locales/zh/enterprise.json +208 -0
  103. package/src/lib/i18n/server.ts +18 -0
  104. package/src/lib/i18n/types.ts +5 -0
  105. package/src/lib/monitoring-benchmarks.ts +85 -0
  106. package/src/lib/monitoring-descriptions.ts +78 -0
  107. package/src/lib/prisma.ts +18 -0
  108. package/src/lib/router-context.ts +38 -0
  109. package/src/lib/session.ts +38 -0
  110. package/src/lib/utils.ts +23 -0
  111. package/src/providers/index.tsx +45 -0
  112. package/src/routeTree.gen.ts +1199 -0
  113. package/src/router.tsx +51 -0
  114. package/src/routes/__root.tsx +170 -0
  115. package/src/routes/_nav/ai-providers/$id/edit/route.lazy.tsx +74 -0
  116. package/src/routes/_nav/ai-providers/$id/edit/route.tsx +35 -0
  117. package/src/routes/_nav/ai-providers/index/route.lazy.tsx +33 -0
  118. package/src/routes/_nav/ai-providers/new/route.lazy.tsx +31 -0
  119. package/src/routes/_nav/ai-providers/new/route.tsx +20 -0
  120. package/src/routes/_nav/ai-providers/route.lazy.tsx +26 -0
  121. package/src/routes/_nav/ai-providers/route.tsx +31 -0
  122. package/src/routes/_nav/apps/route.lazy.tsx +30 -0
  123. package/src/routes/_nav/apps/route.tsx +20 -0
  124. package/src/routes/_nav/audit/route.lazy.tsx +100 -0
  125. package/src/routes/_nav/audit/route.tsx +25 -0
  126. package/src/routes/_nav/billing/invoices/route.lazy.tsx +86 -0
  127. package/src/routes/_nav/billing/invoices/route.tsx +25 -0
  128. package/src/routes/_nav/billing/route.lazy.tsx +48 -0
  129. package/src/routes/_nav/billing/route.tsx +27 -0
  130. package/src/routes/_nav/billing/subscriptions/$subscriptionId/route.lazy.tsx +160 -0
  131. package/src/routes/_nav/billing/subscriptions/$subscriptionId/route.tsx +25 -0
  132. package/src/routes/_nav/billing/subscriptions/route.lazy.tsx +113 -0
  133. package/src/routes/_nav/billing/subscriptions/route.tsx +28 -0
  134. package/src/routes/_nav/compliance/route.lazy.tsx +184 -0
  135. package/src/routes/_nav/compliance/route.tsx +25 -0
  136. package/src/routes/_nav/components/route.lazy.tsx +184 -0
  137. package/src/routes/_nav/components/route.tsx +40 -0
  138. package/src/routes/_nav/dashboard/route.lazy.tsx +33 -0
  139. package/src/routes/_nav/dashboard/route.tsx +20 -0
  140. package/src/routes/_nav/dependencies/route.lazy.tsx +66 -0
  141. package/src/routes/_nav/dependencies/route.tsx +32 -0
  142. package/src/routes/_nav/developer-logs/route.lazy.tsx +6 -0
  143. package/src/routes/_nav/developer-logs/route.tsx +14 -0
  144. package/src/routes/_nav/docs/route.lazy.tsx +30 -0
  145. package/src/routes/_nav/docs/route.tsx +20 -0
  146. package/src/routes/_nav/knowledge-public/bookmarks/route.lazy.tsx +355 -0
  147. package/src/routes/_nav/knowledge-public/bookmarks/route.tsx +40 -0
  148. package/src/routes/_nav/knowledge-public/compliance/route.lazy.tsx +245 -0
  149. package/src/routes/_nav/knowledge-public/compliance/route.tsx +40 -0
  150. package/src/routes/_nav/knowledge-public/legal/route.lazy.tsx +242 -0
  151. package/src/routes/_nav/knowledge-public/legal/route.tsx +40 -0
  152. package/src/routes/_nav/licenses/$licenseId/route.lazy.tsx +235 -0
  153. package/src/routes/_nav/licenses/$licenseId/route.tsx +25 -0
  154. package/src/routes/_nav/licenses/new/route.lazy.tsx +237 -0
  155. package/src/routes/_nav/licenses/route.lazy.tsx +143 -0
  156. package/src/routes/_nav/licenses/route.tsx +25 -0
  157. package/src/routes/_nav/market/route.lazy.tsx +31 -0
  158. package/src/routes/_nav/market/route.tsx +20 -0
  159. package/src/routes/_nav/monitoring/ai-usage/route.lazy.tsx +305 -0
  160. package/src/routes/_nav/monitoring/ai-usage/route.tsx +3 -0
  161. package/src/routes/_nav/monitoring/errors/route.lazy.tsx +111 -0
  162. package/src/routes/_nav/monitoring/health/route.lazy.tsx +260 -0
  163. package/src/routes/_nav/monitoring/logs/route.lazy.tsx +165 -0
  164. package/src/routes/_nav/monitoring/metrics/route.lazy.tsx +240 -0
  165. package/src/routes/_nav/monitoring/overview/route.lazy.tsx +138 -0
  166. package/src/routes/_nav/monitoring/route.lazy.tsx +68 -0
  167. package/src/routes/_nav/monitoring/route.tsx +20 -0
  168. package/src/routes/_nav/monitoring/sentry/route.lazy.tsx +399 -0
  169. package/src/routes/_nav/monitoring/sentry/route.tsx +20 -0
  170. package/src/routes/_nav/permissions/route.lazy.tsx +504 -0
  171. package/src/routes/_nav/permissions/route.tsx +26 -0
  172. package/src/routes/_nav/route.lazy.tsx +31 -0
  173. package/src/routes/_nav/route.tsx +34 -0
  174. package/src/routes/_nav/settings/route.lazy.tsx +30 -0
  175. package/src/routes/_nav/settings/route.tsx +20 -0
  176. package/src/routes/_nav/sso/route.lazy.tsx +145 -0
  177. package/src/routes/_nav/sso/route.tsx +25 -0
  178. package/src/routes/_nav/users/$userId/route.lazy.tsx +432 -0
  179. package/src/routes/_nav/users/$userId/route.tsx +27 -0
  180. package/src/routes/_nav/users/-components/users-table.tsx +38 -0
  181. package/src/routes/_nav/users/route.lazy.tsx +74 -0
  182. package/src/routes/_nav/users/route.tsx +43 -0
  183. package/src/routes/api/compliance-download/$jobId.ts +35 -0
  184. package/src/routes/api/observability/errors.ts +41 -0
  185. package/src/routes/api/observability/health.ts +30 -0
  186. package/src/routes/api.auth.$.ts +31 -0
  187. package/src/routes/api.sentry-test.ts +33 -0
  188. package/src/routes/index/route.lazy.tsx +22 -0
  189. package/src/routes/index/route.tsx +24 -0
  190. package/src/routes/login/route.lazy.tsx +46 -0
  191. package/src/routes/login/route.tsx +28 -0
  192. package/src/routes/sentry-tunnel.ts +61 -0
  193. package/src/routes/setup/route.lazy.tsx +94 -0
  194. package/src/routes/setup/route.tsx +28 -0
  195. package/src/server/__tests__/admin-ai-alerts.test.ts +113 -0
  196. package/src/server/__tests__/licenses-issue.test.ts +98 -0
  197. package/src/server/admin-ai-alerts.ts +177 -0
  198. package/src/server/admin-ai-usage.ts +134 -0
  199. package/src/server/auth-helpers.ts +51 -0
  200. package/src/server/entitlements.ts +24 -0
  201. package/src/server/knowledge-public/canonical-id.ts +55 -0
  202. package/src/server/knowledge-public/github-sync.ts +86 -0
  203. package/src/server/license-issue.ts +78 -0
  204. package/src/server/middlewares/request.ts +26 -0
  205. package/src/server/observability/debug-middleware.ts +39 -0
  206. package/src/server/observability/error-reporter.ts +99 -0
  207. package/src/server/observability/health-check.ts +140 -0
  208. package/src/server/observability/index.ts +33 -0
  209. package/src/server/observability/logger.ts +49 -0
  210. package/src/server/observability/metrics.ts +105 -0
  211. package/src/server/observability/request-middleware.ts +125 -0
  212. package/src/server/observability/timing.ts +42 -0
  213. package/src/server/pro-services.ts +32 -0
  214. package/src/server/serverFns/admin-billing.ts +94 -0
  215. package/src/server/serverFns/ai-provider/create.ts +92 -0
  216. package/src/server/serverFns/ai-provider/delete.ts +42 -0
  217. package/src/server/serverFns/ai-provider/get.ts +35 -0
  218. package/src/server/serverFns/ai-provider/list.ts +92 -0
  219. package/src/server/serverFns/ai-provider/update.ts +73 -0
  220. package/src/server/serverFns/anyhub/market.ts +64 -0
  221. package/src/server/serverFns/audit.ts +57 -0
  222. package/src/server/serverFns/auth/session.ts +26 -0
  223. package/src/server/serverFns/auth/setup.ts +73 -0
  224. package/src/server/serverFns/compliance.ts +85 -0
  225. package/src/server/serverFns/components/import.ts +197 -0
  226. package/src/server/serverFns/components/list.ts +51 -0
  227. package/src/server/serverFns/components/mutations.ts +84 -0
  228. package/src/server/serverFns/dependency/helpers.ts +245 -0
  229. package/src/server/serverFns/dependency/list.ts +127 -0
  230. package/src/server/serverFns/dependency/sync.ts +123 -0
  231. package/src/server/serverFns/dependency/update.ts +51 -0
  232. package/src/server/serverFns/developer-log-auto-generate.ts +176 -0
  233. package/src/server/serverFns/developer-logs.ts +132 -0
  234. package/src/server/serverFns/knowledge-public/bookmarks/create.ts +53 -0
  235. package/src/server/serverFns/knowledge-public/bookmarks/delete.ts +31 -0
  236. package/src/server/serverFns/knowledge-public/bookmarks/detail.ts +32 -0
  237. package/src/server/serverFns/knowledge-public/bookmarks/list.ts +66 -0
  238. package/src/server/serverFns/knowledge-public/bookmarks/sync-all-github.ts +56 -0
  239. package/src/server/serverFns/knowledge-public/bookmarks/update.ts +50 -0
  240. package/src/server/serverFns/knowledge-public/compliance/create.ts +58 -0
  241. package/src/server/serverFns/knowledge-public/compliance/delete.ts +31 -0
  242. package/src/server/serverFns/knowledge-public/compliance/detail.ts +32 -0
  243. package/src/server/serverFns/knowledge-public/compliance/list.ts +59 -0
  244. package/src/server/serverFns/knowledge-public/compliance/update.ts +53 -0
  245. package/src/server/serverFns/knowledge-public/legal/create.ts +62 -0
  246. package/src/server/serverFns/knowledge-public/legal/delete.ts +31 -0
  247. package/src/server/serverFns/knowledge-public/legal/detail.ts +32 -0
  248. package/src/server/serverFns/knowledge-public/legal/list.ts +60 -0
  249. package/src/server/serverFns/knowledge-public/legal/update.ts +55 -0
  250. package/src/server/serverFns/licenses.ts +92 -0
  251. package/src/server/serverFns/locale.ts +10 -0
  252. package/src/server/serverFns/monitoring/check-ai-alerts.ts +13 -0
  253. package/src/server/serverFns/monitoring/get-ai-usage-overview.ts +25 -0
  254. package/src/server/serverFns/monitoring/get-errors.ts +43 -0
  255. package/src/server/serverFns/monitoring/get-health-settings.ts +35 -0
  256. package/src/server/serverFns/monitoring/get-health.ts +55 -0
  257. package/src/server/serverFns/monitoring/get-metrics.ts +46 -0
  258. package/src/server/serverFns/monitoring/get-request-logs.ts +66 -0
  259. package/src/server/serverFns/monitoring/report-error.ts +55 -0
  260. package/src/server/serverFns/monitoring/reset.ts +70 -0
  261. package/src/server/serverFns/permissions.ts +214 -0
  262. package/src/server/serverFns/sentry/config.ts +88 -0
  263. package/src/server/serverFns/sso-config.ts +83 -0
  264. package/src/server/serverFns/user/create.ts +40 -0
  265. package/src/server/serverFns/user/delete.ts +29 -0
  266. package/src/server/serverFns/user/detail.ts +45 -0
  267. package/src/server/serverFns/user/list.ts +56 -0
  268. package/src/server/serverFns/user/reset-password.ts +28 -0
  269. package/src/server/serverFns/user/update-role.ts +26 -0
  270. package/src/server/serverFns/user/update-status.ts +26 -0
  271. package/src/server.ts +27 -0
  272. package/src/start.ts +85 -0
  273. package/src/styles/_keyframe-animations.scss +91 -0
  274. package/src/styles/_variables.scss +296 -0
  275. package/src/styles/globals.css +200 -0
  276. package/src/types/start-authjs.d.ts +27 -0
@@ -0,0 +1,358 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // AllBlue - 理想之海
3
+ // Copyright (C) 2026 AllBlue Contributors
4
+ //
5
+ // This program is free software: you can redistribute it and/or modify
6
+ // it under the terms of the GNU Affero General Public License as published by
7
+ // the Free Software Foundation, either version 3 of the License, or
8
+ // (at your option) any later version.
9
+ //
10
+ // This program is distributed in the hope that it will be useful,
11
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ // GNU Affero General Public License for more details.
14
+ //
15
+ // You should have received a copy of the GNU Affero General Public License
16
+ // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ import {
19
+ Calendar,
20
+ Download,
21
+ Filter,
22
+ Loader2,
23
+ MapPin,
24
+ Search,
25
+ } from "lucide-react";
26
+ import { useState } from "react";
27
+ import { useTranslation } from 'react-i18next'
28
+ import { cn } from "@/lib/utils";
29
+ import { Badge } from "@/components/ui/badge";
30
+ import { Button } from "@/components/ui/button";
31
+ import {
32
+ Card,
33
+ CardContent,
34
+ CardDescription,
35
+ CardHeader,
36
+ CardTitle,
37
+ } from "@/components/ui/card";
38
+ import { FieldLabel } from "@/components/ui/field";
39
+ import {
40
+ InputGroup,
41
+ InputGroupInput,
42
+ } from "@/components/ui/input-group";
43
+ import {
44
+ Select,
45
+ SelectContent,
46
+ SelectItem,
47
+ SelectTrigger,
48
+ SelectValue,
49
+ } from "@/components/ui/select";
50
+ import { Separator } from "@/components/ui/separator";
51
+
52
+ export interface ActivityLogEntry {
53
+ id: string;
54
+ action: string;
55
+ type:
56
+ | "login"
57
+ | "logout"
58
+ | "password_change"
59
+ | "profile_update"
60
+ | "settings_change"
61
+ | "export"
62
+ | "delete"
63
+ | "create"
64
+ | "update";
65
+ description: string;
66
+ ipAddress: string;
67
+ location: string;
68
+ device: string;
69
+ timestamp: Date;
70
+ status: "success" | "failed";
71
+ /** 失败原因(仅 status === "failed" 时显示,位于日期右侧,dot 分隔) */
72
+ reason?: string;
73
+ }
74
+
75
+ export interface SettingsActivityLogProps {
76
+ entries?: ActivityLogEntry[];
77
+ onExport?: (filters: {
78
+ dateRange?: { start: Date; end: Date };
79
+ type?: string;
80
+ search?: string;
81
+ }) => Promise<void>;
82
+ className?: string;
83
+ title?: string;
84
+ description?: string;
85
+ }
86
+
87
+ const actionTypes = [
88
+ { value: "all", key: "all" },
89
+ { value: "login", key: "login" },
90
+ { value: "logout", key: "logout" },
91
+ { value: "password_change", key: "password_change" },
92
+ { value: "profile_update", key: "profile_update" },
93
+ { value: "settings_change", key: "settings_change" },
94
+ { value: "export", key: "export" },
95
+ { value: "delete", key: "delete" },
96
+ ] as const
97
+
98
+ function formatRelativeTime(t: (k: string, p?: Record<string, number>) => string, date: Date): string {
99
+ const now = Date.now();
100
+ const diff = now - date.getTime();
101
+ const minutes = Math.floor(diff / 60_000);
102
+ const hours = Math.floor(minutes / 60);
103
+ const days = Math.floor(hours / 24);
104
+
105
+ if (minutes < 1) return t("activityLog.justNow");
106
+ if (minutes < 60) return t("common.timeAgo.minutes", { count: minutes });
107
+ if (hours < 24) return t("common.timeAgo.hours", { count: hours });
108
+ if (days < 7) return t("common.timeAgo.days", { count: days });
109
+ return new Intl.DateTimeFormat("zh-CN", {
110
+ month: "long",
111
+ day: "numeric",
112
+ year: "numeric",
113
+ hour: "numeric",
114
+ minute: "2-digit",
115
+ }).format(date);
116
+ }
117
+
118
+ export default function SettingsActivityLog({
119
+ entries = [],
120
+ onExport,
121
+ className,
122
+ title,
123
+ description,
124
+ }: SettingsActivityLogProps) {
125
+ const { t } = useTranslation('common')
126
+ const displayTitle = title ?? t('activityLog.title')
127
+ const displayDescription = description ?? t('activityLog.description')
128
+ const [searchQuery, setSearchQuery] = useState("");
129
+ const [selectedType, setSelectedType] = useState("all");
130
+ const [dateRange, setDateRange] = useState({
131
+ start: "",
132
+ end: "",
133
+ });
134
+ const [isExporting, setIsExporting] = useState(false);
135
+
136
+ const filteredEntries = entries.filter((entry) => {
137
+ if (selectedType !== "all" && entry.type !== selectedType) return false;
138
+ if (
139
+ searchQuery &&
140
+ !entry.description.toLowerCase().includes(searchQuery.toLowerCase()) &&
141
+ !entry.ipAddress.includes(searchQuery) &&
142
+ !entry.location.toLowerCase().includes(searchQuery.toLowerCase())
143
+ )
144
+ return false;
145
+ return true;
146
+ });
147
+
148
+ const handleExport = async () => {
149
+ setIsExporting(true);
150
+ try {
151
+ const exportRange =
152
+ dateRange.start && dateRange.end
153
+ ? {
154
+ start: new Date(dateRange.start),
155
+ end: new Date(dateRange.end),
156
+ }
157
+ : undefined;
158
+
159
+ await onExport?.({
160
+ dateRange: exportRange,
161
+ type: selectedType !== "all" ? selectedType : undefined,
162
+ search: searchQuery || undefined,
163
+ });
164
+ } finally {
165
+ setIsExporting(false);
166
+ }
167
+ };
168
+
169
+ return (
170
+ <Card className={cn("w-full shadow-xs", className)}>
171
+ <CardHeader>
172
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
173
+ <div className="flex min-w-0 flex-1 flex-col gap-2">
174
+ <CardTitle className="wrap-break-word">{displayTitle}</CardTitle>
175
+ <CardDescription className="wrap-break-word">
176
+ {displayDescription}
177
+ </CardDescription>
178
+ </div>
179
+ </div>
180
+ </CardHeader>
181
+ <CardContent>
182
+ <div className="flex flex-col gap-6">
183
+ <div className="grid gap-4 md:grid-cols-2">
184
+ <div className="flex flex-col gap-4 rounded-lg border p-4">
185
+ <div className="flex items-center gap-2">
186
+ <div className="flex size-8 items-center justify-center rounded-lg bg-muted">
187
+ <Search className="size-4" />
188
+ </div>
189
+ <FieldLabel className="mb-0">{t('activityLog.search')}</FieldLabel>
190
+ </div>
191
+ <InputGroup>
192
+ <InputGroupInput
193
+ onChange={(e) => setSearchQuery(e.target.value)}
194
+ placeholder={t('activityLog.searchPlaceholder')}
195
+ value={searchQuery}
196
+ />
197
+ </InputGroup>
198
+ </div>
199
+
200
+ <div className="flex flex-col gap-4 rounded-lg border p-4">
201
+ <div className="flex items-center gap-2">
202
+ <div className="flex size-8 items-center justify-center rounded-lg bg-muted">
203
+ <Filter className="size-4" />
204
+ </div>
205
+ <FieldLabel className="mb-0" htmlFor="action-type">
206
+ {t('activityLog.actionType')}
207
+ </FieldLabel>
208
+ </div>
209
+ <Select onValueChange={setSelectedType} value={selectedType}>
210
+ <SelectTrigger id="action-type">
211
+ <SelectValue />
212
+ </SelectTrigger>
213
+ <SelectContent>
214
+ {actionTypes.map((type) => (
215
+ <SelectItem key={type.value} value={type.value}>
216
+ {t(`activityLog.actionTypes.${type.key}`)}
217
+ </SelectItem>
218
+ ))}
219
+ </SelectContent>
220
+ </Select>
221
+ </div>
222
+
223
+ <div className="flex flex-col gap-4 rounded-lg border p-4">
224
+ <div className="flex items-center gap-2">
225
+ <div className="flex size-8 items-center justify-center rounded-lg bg-muted">
226
+ <Calendar className="size-4" />
227
+ </div>
228
+ <FieldLabel className="mb-0" htmlFor="date-start">
229
+ {t('activityLog.startDate')}
230
+ </FieldLabel>
231
+ </div>
232
+ <InputGroup>
233
+ <InputGroupInput
234
+ id="date-start"
235
+ onChange={(e) =>
236
+ setDateRange((prev) => ({ ...prev, start: e.target.value }))
237
+ }
238
+ type="date"
239
+ value={dateRange.start}
240
+ />
241
+ </InputGroup>
242
+ </div>
243
+
244
+ <div className="flex flex-col gap-4 rounded-lg border p-4">
245
+ <div className="flex items-center gap-2">
246
+ <div className="flex size-8 items-center justify-center rounded-lg bg-muted">
247
+ <Calendar className="size-4" />
248
+ </div>
249
+ <FieldLabel className="mb-0" htmlFor="date-end">
250
+ {t('activityLog.endDate')}
251
+ </FieldLabel>
252
+ </div>
253
+ <InputGroup>
254
+ <InputGroupInput
255
+ id="date-end"
256
+ onChange={(e) =>
257
+ setDateRange((prev) => ({ ...prev, end: e.target.value }))
258
+ }
259
+ type="date"
260
+ value={dateRange.end}
261
+ />
262
+ </InputGroup>
263
+ </div>
264
+ </div>
265
+
266
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
267
+ <p className="text-muted-foreground text-sm">
268
+ {t('activityLog.total', { count: filteredEntries.length })}
269
+ </p>
270
+ <Button
271
+ className="w-full sm:w-auto"
272
+ disabled={isExporting}
273
+ onClick={handleExport}
274
+ type="button"
275
+ variant="outline"
276
+ >
277
+ {isExporting ? (
278
+ <>
279
+ <Loader2 className="size-4 animate-spin" />
280
+ {t('activityLog.exporting')}
281
+ </>
282
+ ) : (
283
+ <>
284
+ <Download className="size-4" />
285
+ {t('activityLog.exportLog')}
286
+ </>
287
+ )}
288
+ </Button>
289
+ </div>
290
+
291
+ <Separator />
292
+
293
+ {filteredEntries.length === 0 ? (
294
+ <div className="flex flex-col items-center justify-center gap-4 py-12 text-center">
295
+ <div className="flex size-12 items-center justify-center rounded-full bg-muted">
296
+ <Search className="size-6 text-muted-foreground" />
297
+ </div>
298
+ <div className="flex flex-col gap-2">
299
+ <p className="font-medium text-sm">{t('activityLog.empty')}</p>
300
+ <p className="text-muted-foreground text-sm">
301
+ {searchQuery || selectedType !== "all"
302
+ ? t('activityLog.emptyHint.filtered')
303
+ : t('activityLog.emptyHint.default')}
304
+ </p>
305
+ </div>
306
+ </div>
307
+ ) : (
308
+ <div className="flex flex-col gap-3">
309
+ {filteredEntries.map((entry) => (
310
+ <div
311
+ className="flex items-start gap-3 rounded-lg border p-4"
312
+ key={entry.id}
313
+ >
314
+ <div className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-muted">
315
+ <Calendar className="size-4" />
316
+ </div>
317
+ <div className="flex min-w-0 flex-1 flex-col gap-2">
318
+ <div className="flex flex-wrap items-center gap-2">
319
+ <span className="font-medium text-sm">
320
+ {entry.description}
321
+ </span>
322
+ <Badge
323
+ className="text-xs"
324
+ variant={
325
+ entry.status === "success" ? "default" : "destructive"
326
+ }
327
+ >
328
+ {entry.status === "success" ? t('activityLog.status.success') : t('activityLog.status.failed')}
329
+ </Badge>
330
+ </div>
331
+ <div className="flex flex-wrap items-center gap-2 text-muted-foreground text-xs">
332
+ <span className="flex items-center gap-1">
333
+ <MapPin className="size-3" />
334
+ {entry.location || t('activityLog.unknown')}
335
+ </span>
336
+ <span>•</span>
337
+ <span>{entry.ipAddress || t('activityLog.unknown')}</span>
338
+ <span>•</span>
339
+ <span>{entry.device || t('activityLog.unknown')}</span>
340
+ <span>•</span>
341
+ <span>{formatRelativeTime(t, entry.timestamp)}</span>
342
+ {entry.status === "failed" && entry.reason && (
343
+ <>
344
+ <span>•</span>
345
+ <span className="text-destructive">{entry.reason}</span>
346
+ </>
347
+ )}
348
+ </div>
349
+ </div>
350
+ </div>
351
+ ))}
352
+ </div>
353
+ )}
354
+ </div>
355
+ </CardContent>
356
+ </Card>
357
+ );
358
+ }
@@ -0,0 +1,42 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // AllBlue - 理想之海
3
+ // Copyright (C) 2026 AllBlue Contributors
4
+ //
5
+ // This program is free software: you can redistribute it and/or modify
6
+ // it under the terms of the GNU Affero General Public License as published by
7
+ // the Free Software Foundation, either version 3 of the License, or
8
+ // (at your option) any later version.
9
+ //
10
+ // This program is distributed in the hope that it will be useful,
11
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ // GNU Affero General Public License for more details.
14
+ //
15
+ // You should have received a copy of the GNU Affero General Public License
16
+ // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ export const defaultSocialPlatforms = [
19
+ {
20
+ id: "twitter",
21
+ label: "Twitter/X",
22
+ placeholder: "https://x.com/preetsuthar17",
23
+ },
24
+ {
25
+ id: "github",
26
+ label: "GitHub",
27
+ placeholder: "https://github.com/preetsuthar17",
28
+ },
29
+ {
30
+ id: "linkedin",
31
+ label: "LinkedIn",
32
+ placeholder: "https://linkedin.com/in/preetsuthar17",
33
+ },
34
+ { id: "website", label: "Website", placeholder: "https://preetsuthar.me" },
35
+ ]
36
+
37
+ export const socialPlatformLabels: Record<string, string> = {
38
+ twitter: "Twitter/X",
39
+ github: "GitHub",
40
+ linkedin: "LinkedIn",
41
+ website: "Website",
42
+ }
@@ -0,0 +1,47 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // AllBlue - 理想之海
3
+ // Copyright (C) 2026 AllBlue Contributors
4
+ //
5
+ // This program is free software: you can redistribute it and/or modify
6
+ // it under the terms of the GNU Affero General Public License as published by
7
+ // the Free Software Foundation, either version 3 of the License, or
8
+ // (at your option) any later version.
9
+ //
10
+ // This program is distributed in the hope that it will be useful,
11
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ // GNU Affero General Public License for more details.
14
+ //
15
+ // You should have received a copy of the GNU Affero General Public License
16
+ // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ import type { ReactNode } from "react";
19
+
20
+ export interface SocialLink {
21
+ platform: string;
22
+ url: string;
23
+ }
24
+
25
+ export interface ProfileData {
26
+ name: string;
27
+ email: string;
28
+ bio?: string;
29
+ location?: string;
30
+ website?: string;
31
+ avatar?: string;
32
+ socialLinks?: SocialLink[];
33
+ }
34
+
35
+ export interface SettingsProfileProps {
36
+ profile?: ProfileData;
37
+ onSave?: (data: ProfileData) => Promise<void>;
38
+ onEmailChange?: (newEmail: string, currentPassword: string) => Promise<void>;
39
+ onAvatarUpload?: (file: File) => Promise<string>;
40
+ onAvatarRemove?: () => Promise<void>;
41
+ className?: string;
42
+ showEmailVerification?: boolean;
43
+ readOnly?: boolean;
44
+ title?: string;
45
+ description?: string;
46
+ actions?: ReactNode;
47
+ }