Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/frontend/account/settings-index.tsx
2209 views
1
/*
2
* This file is part of CoCalc: Copyright © 2025 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
// cSpell:ignore payg
7
8
import { Card, Divider, Flex } from "antd";
9
import { defineMessages, useIntl } from "react-intl";
10
11
import { redux, useTypedRedux } from "@cocalc/frontend/app-framework";
12
import { Icon } from "@cocalc/frontend/components";
13
import AIAvatar from "@cocalc/frontend/components/ai-avatar";
14
import { cloudFilesystemsEnabled } from "@cocalc/frontend/compute";
15
import { labels } from "@cocalc/frontend/i18n";
16
import { COLORS } from "@cocalc/util/theme";
17
import {
18
VALID_PREFERENCES_SUB_TYPES,
19
type NavigatePath,
20
type PreferencesSubTabType,
21
} from "@cocalc/util/types/settings";
22
import { APPEARANCE_ICON_NAME } from "./account-preferences-appearance";
23
import { COMMUNICATION_ICON_NAME } from "./account-preferences-communication";
24
import { EDITOR_ICON_NAME } from "./account-preferences-editor";
25
import { KEYBOARD_ICON_NAME } from "./account-preferences-keyboard";
26
import { OTHER_ICON_NAME } from "./account-preferences-other";
27
import { ACCOUNT_PROFILE_ICON_NAME } from "./account-preferences-profile";
28
import { KEYS_ICON_NAME } from "./account-preferences-security";
29
30
const MESSAGES = defineMessages({
31
title: {
32
id: "account.settings.overview.title",
33
defaultMessage: "Settings Overview",
34
},
35
profile: {
36
id: "account.settings.overview.profile",
37
defaultMessage:
38
"Manage your personal information, avatar, and account details.",
39
},
40
appearance: {
41
id: "account.settings.overview.appearance",
42
defaultMessage:
43
"Customize the visual experience via color themes, {dark_mode}, language, and visual settings.",
44
},
45
editor: {
46
id: "account.settings.overview.editor",
47
defaultMessage:
48
"Customize code editor behavior, indentation, and content options.",
49
},
50
keyboard: {
51
id: "account.settings.overview.keyboard",
52
defaultMessage: "Keyboard shortcuts.",
53
},
54
ai: {
55
id: "account.settings.overview.ai",
56
defaultMessage: "Configure AI assistant settings and integrations.",
57
},
58
communication: {
59
id: "account.settings.overview.communication",
60
defaultMessage: "Notification preferences and communication settings.",
61
},
62
keys: {
63
id: "account.settings.overview.keys",
64
defaultMessage: "Manage API keys and setup SSH keys.",
65
},
66
other: {
67
id: "account.settings.overview.other",
68
defaultMessage: "Miscellaneous settings and options.",
69
},
70
billing: {
71
id: "account.settings.overview.billing",
72
defaultMessage: "Manage subscriptions, licenses, and billing information.",
73
},
74
subscriptions: {
75
id: "account.settings.overview.subscriptions",
76
defaultMessage: "View and manage your active subscriptions.",
77
},
78
licenses: {
79
id: "account.settings.overview.licenses",
80
defaultMessage: "Manage software licenses and access permissions.",
81
},
82
payg: {
83
id: "account.settings.overview.payg",
84
defaultMessage: "Configure pay-as-you-go usage and billing.",
85
},
86
purchases: {
87
id: "account.settings.overview.purchases",
88
defaultMessage: "View purchase history and receipts.",
89
},
90
payments: {
91
id: "account.settings.overview.payments",
92
defaultMessage: "Manage payment methods and transaction history.",
93
},
94
statements: {
95
id: "account.settings.overview.statements",
96
defaultMessage: "View detailed billing statements and invoices.",
97
},
98
files: {
99
id: "account.settings.overview.files",
100
defaultMessage: "Manage published files and public sharing.",
101
},
102
cloud: {
103
id: "account.settings.overview.cloud",
104
defaultMessage: "Manage cloud file system storage.",
105
},
106
support: {
107
id: "account.settings.overview.support",
108
defaultMessage: "Access support tickets and help resources.",
109
},
110
});
111
112
const CARD_PROPS = {
113
size: "small" as const,
114
hoverable: true,
115
style: { width: 300, minWidth: 250 },
116
} as const;
117
118
// TODO: highlighting a few cards is a good idea, but better to do this later...
119
const HIGHLIGHTED_CARD_PROPS = CARD_PROPS; // = {
120
// ...CARD_PROPS,
121
// style: {
122
// ...CARD_PROPS.style,
123
// border: `2px solid ${COLORS.BLUE_LLL}`,
124
// },
125
// } as const;
126
127
const FLEX_PROPS = {
128
wrap: true as const,
129
gap: "15px",
130
style: { marginBottom: "40px" },
131
} as const;
132
133
export function SettingsOverview() {
134
const intl = useIntl();
135
const is_commercial = useTypedRedux("customize", "is_commercial");
136
137
function handleNavigate(path: NavigatePath) {
138
// Use the same navigation pattern as the account page
139
const segments = path.split("/").filter(Boolean);
140
if (segments[0] === "settings") {
141
if (segments[1] === "profile") {
142
redux.getActions("account").setState({
143
active_page: "profile",
144
active_sub_tab: undefined,
145
});
146
redux.getActions("account").push_state(`/profile`);
147
} else if (segments[1] === "preferences" && segments[2]) {
148
// Handle preferences sub-tabs
149
const subTab = segments[2] as PreferencesSubTabType;
150
if (VALID_PREFERENCES_SUB_TYPES.includes(subTab)) {
151
const subTabKey = `preferences-${subTab}` as const;
152
redux.getActions("account").setState({
153
active_page: "preferences",
154
active_sub_tab: subTabKey,
155
});
156
redux.getActions("account").push_state(`/preferences/${subTab}`);
157
}
158
} else {
159
// Handle other settings pages
160
redux.getActions("account").set_active_tab(segments[1]);
161
redux.getActions("account").push_state(`/${segments[1]}`);
162
}
163
}
164
}
165
166
return (
167
<div style={{ padding: "20px" }}>
168
<Flex {...FLEX_PROPS}>
169
<Card
170
{...HIGHLIGHTED_CARD_PROPS}
171
onClick={() => handleNavigate("settings/profile")}
172
>
173
<Card.Meta
174
avatar={<Icon name={ACCOUNT_PROFILE_ICON_NAME} />}
175
title={intl.formatMessage(labels.profile)}
176
description={intl.formatMessage(MESSAGES.profile)}
177
/>
178
</Card>
179
<Card
180
{...HIGHLIGHTED_CARD_PROPS}
181
onClick={() => handleNavigate("settings/preferences/appearance")}
182
>
183
<Card.Meta
184
avatar={<Icon name={APPEARANCE_ICON_NAME} />}
185
title={intl.formatMessage(labels.appearance)}
186
description={intl.formatMessage(MESSAGES.appearance, {
187
dark_mode: (
188
<span
189
style={{
190
backgroundColor: COLORS.GRAY_DD,
191
color: COLORS.GRAY_LLL,
192
paddingLeft: "3px",
193
paddingRight: "3px",
194
}}
195
>
196
dark mode
197
</span>
198
),
199
})}
200
/>
201
</Card>
202
<Card
203
{...CARD_PROPS}
204
onClick={() => handleNavigate("settings/preferences/editor")}
205
>
206
<Card.Meta
207
avatar={<Icon name={EDITOR_ICON_NAME} />}
208
title={intl.formatMessage(labels.editor)}
209
description={intl.formatMessage(MESSAGES.editor)}
210
/>
211
</Card>
212
<Card
213
{...CARD_PROPS}
214
onClick={() => handleNavigate("settings/preferences/keyboard")}
215
>
216
<Card.Meta
217
avatar={<Icon name={KEYBOARD_ICON_NAME} />}
218
title={intl.formatMessage(labels.keyboard)}
219
description={intl.formatMessage(MESSAGES.keyboard)}
220
/>
221
</Card>
222
<Card
223
{...CARD_PROPS}
224
onClick={() => handleNavigate("settings/preferences/ai")}
225
>
226
<Card.Meta
227
avatar={<AIAvatar size={24} />}
228
title={intl.formatMessage(labels.ai)}
229
description={intl.formatMessage(MESSAGES.ai)}
230
/>
231
</Card>
232
<Card
233
{...CARD_PROPS}
234
onClick={() => handleNavigate("settings/preferences/communication")}
235
>
236
<Card.Meta
237
avatar={<Icon name={COMMUNICATION_ICON_NAME} />}
238
title={intl.formatMessage(labels.communication)}
239
description={intl.formatMessage(MESSAGES.communication)}
240
/>
241
</Card>
242
<Card
243
{...CARD_PROPS}
244
onClick={() => handleNavigate("settings/preferences/keys")}
245
>
246
<Card.Meta
247
avatar={<Icon name={KEYS_ICON_NAME} />}
248
title={intl.formatMessage(labels.ssh_and_api_keys)}
249
description={intl.formatMessage(MESSAGES.keys)}
250
/>
251
</Card>
252
<Card
253
{...CARD_PROPS}
254
onClick={() => handleNavigate("settings/preferences/other")}
255
>
256
<Card.Meta
257
avatar={<Icon name={OTHER_ICON_NAME} />}
258
title={intl.formatMessage(labels.other)}
259
description={intl.formatMessage(MESSAGES.other)}
260
/>
261
</Card>
262
</Flex>
263
264
{is_commercial && (
265
<>
266
<Divider plain>
267
<Icon name="money-check" /> {intl.formatMessage(labels.billing)}
268
</Divider>
269
<Flex {...FLEX_PROPS}>
270
<Card
271
{...HIGHLIGHTED_CARD_PROPS}
272
onClick={() => handleNavigate("settings/subscriptions")}
273
>
274
<Card.Meta
275
avatar={<Icon name="calendar" />}
276
title={intl.formatMessage(labels.subscriptions)}
277
description={intl.formatMessage(MESSAGES.subscriptions)}
278
/>
279
</Card>
280
<Card
281
{...HIGHLIGHTED_CARD_PROPS}
282
onClick={() => handleNavigate("settings/licenses")}
283
>
284
<Card.Meta
285
avatar={<Icon name="key" />}
286
title={intl.formatMessage(labels.licenses)}
287
description={intl.formatMessage(MESSAGES.licenses)}
288
/>
289
</Card>
290
<Card
291
{...CARD_PROPS}
292
onClick={() => handleNavigate("settings/payg")}
293
>
294
<Card.Meta
295
avatar={<Icon name="line-chart" />}
296
title={intl.formatMessage(labels.pay_as_you_go)}
297
description={intl.formatMessage(MESSAGES.payg)}
298
/>
299
</Card>
300
<Card
301
{...CARD_PROPS}
302
onClick={() => handleNavigate("settings/purchases")}
303
>
304
<Card.Meta
305
avatar={<Icon name="money-check" />}
306
title={intl.formatMessage(labels.purchases)}
307
description={intl.formatMessage(MESSAGES.purchases)}
308
/>
309
</Card>
310
<Card
311
{...CARD_PROPS}
312
onClick={() => handleNavigate("settings/payments")}
313
>
314
<Card.Meta
315
avatar={<Icon name="credit-card" />}
316
title={intl.formatMessage(labels.payments)}
317
description={intl.formatMessage(MESSAGES.payments)}
318
/>
319
</Card>
320
<Card
321
{...CARD_PROPS}
322
onClick={() => handleNavigate("settings/statements")}
323
>
324
<Card.Meta
325
avatar={<Icon name="calendar-week" />}
326
title={intl.formatMessage(labels.statements)}
327
description={intl.formatMessage(MESSAGES.statements)}
328
/>
329
</Card>
330
</Flex>
331
</>
332
)}
333
334
<Divider plain>
335
<Icon name="files" /> {intl.formatMessage(labels.files)}
336
</Divider>
337
<Flex {...FLEX_PROPS}>
338
<Card
339
{...CARD_PROPS}
340
onClick={() => handleNavigate("settings/public-files")}
341
>
342
<Card.Meta
343
avatar={<Icon name="share-square" />}
344
title={intl.formatMessage(labels.published_files)}
345
description={intl.formatMessage(MESSAGES.files)}
346
/>
347
</Card>
348
349
{cloudFilesystemsEnabled() && (
350
<Card
351
{...CARD_PROPS}
352
onClick={() => handleNavigate("settings/cloud-filesystems")}
353
>
354
<Card.Meta
355
avatar={<Icon name="server" />}
356
title={intl.formatMessage(labels.cloud_file_system)}
357
description={intl.formatMessage(MESSAGES.cloud)}
358
/>
359
</Card>
360
)}
361
</Flex>
362
363
{is_commercial && (
364
<>
365
<Divider plain>
366
<Icon name="medkit" /> {intl.formatMessage(labels.support)}
367
</Divider>
368
<Flex {...FLEX_PROPS}>
369
<Card
370
{...CARD_PROPS}
371
onClick={() => handleNavigate("settings/support")}
372
>
373
<Card.Meta
374
avatar={<Icon name="medkit" />}
375
title={intl.formatMessage(labels.support)}
376
description={intl.formatMessage(MESSAGES.support)}
377
/>
378
</Card>
379
</Flex>
380
</>
381
)}
382
</div>
383
);
384
}
385
386