@micha.bigler/ui-core-micha 2.4.5 → 2.6.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.
- package/.github/workflows/publish.yml +4 -0
- package/dist/components/UserListComponent.js +81 -71
- package/dist/i18n/notificationsTranslations.js +17 -0
- package/dist/i18n/onboardingTranslations.js +17 -0
- package/dist/index.js +14 -0
- package/dist/notifications/NotificationSettings.js +148 -0
- package/dist/notifications/api.js +41 -0
- package/dist/notifications/serviceWorker/sw.js +49 -0
- package/dist/onboarding/OnboardingProvider.js +133 -0
- package/dist/onboarding/OnboardingWizard.js +47 -0
- package/dist/onboarding/api.js +10 -0
- package/dist/onboarding/stepSelection.js +18 -0
- package/dist/onboarding/steps/BrowserPushStep.js +49 -0
- package/dist/onboarding/steps/CompleteNameStep.js +37 -0
- package/dist/onboarding/steps/CookieConsentStep.js +31 -0
- package/package.json +7 -4
- package/src/components/UserListComponent.jsx +196 -122
- package/src/i18n/notificationsTranslations.ts +17 -0
- package/src/i18n/onboardingTranslations.ts +17 -0
- package/src/index.js +22 -0
- package/src/notifications/NotificationSettings.jsx +189 -0
- package/src/notifications/api.js +49 -0
- package/src/notifications/serviceWorker/sw.js +50 -0
- package/src/onboarding/OnboardingProvider.jsx +153 -0
- package/src/onboarding/OnboardingWizard.jsx +66 -0
- package/src/onboarding/api.js +13 -0
- package/src/onboarding/stepSelection.js +16 -0
- package/src/onboarding/steps/BrowserPushStep.jsx +63 -0
- package/src/onboarding/steps/CompleteNameStep.jsx +46 -0
- package/src/onboarding/steps/CookieConsentStep.jsx +39 -0
- package/tests/NotificationSettings.test.jsx +82 -0
- package/tests/notificationsApi.test.js +41 -0
- package/tests/stepSelection.test.js +53 -0
|
@@ -10,8 +10,15 @@ import {
|
|
|
10
10
|
CircularProgress,
|
|
11
11
|
Alert,
|
|
12
12
|
TextField,
|
|
13
|
+
Table,
|
|
14
|
+
TableBody,
|
|
15
|
+
TableCell,
|
|
16
|
+
TableContainer,
|
|
17
|
+
TableHead,
|
|
18
|
+
TableRow,
|
|
19
|
+
TableSortLabel,
|
|
20
|
+
TablePagination,
|
|
13
21
|
} from '@mui/material';
|
|
14
|
-
import { DataGrid } from '@mui/x-data-grid';
|
|
15
22
|
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
|
16
23
|
import CancelIcon from '@mui/icons-material/Cancel';
|
|
17
24
|
import DeleteIcon from '@mui/icons-material/Delete';
|
|
@@ -20,7 +27,7 @@ import { fetchUsersList, deleteUser, updateUserRole } from '../auth/authApi';
|
|
|
20
27
|
|
|
21
28
|
const DEFAULT_ROLES = ['none', 'student', 'teacher', 'admin'];
|
|
22
29
|
|
|
23
|
-
export function UserListComponent({
|
|
30
|
+
export function UserListComponent({
|
|
24
31
|
roles = DEFAULT_ROLES,
|
|
25
32
|
currentUser,
|
|
26
33
|
extraColumns = [],
|
|
@@ -42,6 +49,10 @@ export function UserListComponent({
|
|
|
42
49
|
const [error, setError] = useState(null);
|
|
43
50
|
const [rowActionLoading, setRowActionLoading] = useState({});
|
|
44
51
|
const [searchQuery, setSearchQuery] = useState('');
|
|
52
|
+
const [sortField, setSortField] = useState('email');
|
|
53
|
+
const [sortDir, setSortDir] = useState('asc');
|
|
54
|
+
const [page, setPage] = useState(0);
|
|
55
|
+
const [pageSize, setPageSize] = useState(25);
|
|
45
56
|
const controlSx = { minWidth: 140 };
|
|
46
57
|
const actionButtonSx = { textTransform: 'none', minWidth: 90 };
|
|
47
58
|
|
|
@@ -69,6 +80,10 @@ export function UserListComponent({
|
|
|
69
80
|
loadUsers();
|
|
70
81
|
}, [loadUsers, refreshTrigger]);
|
|
71
82
|
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
setPage(0);
|
|
85
|
+
}, [searchQuery]);
|
|
86
|
+
|
|
72
87
|
const handleDelete = async (userId) => {
|
|
73
88
|
if (!window.confirm(t('UserList.DELETE_CONFIRM', 'Are you sure you want to delete this user?'))) return;
|
|
74
89
|
try {
|
|
@@ -106,14 +121,14 @@ export function UserListComponent({
|
|
|
106
121
|
const defaultCanEdit = (targetUser) => {
|
|
107
122
|
if (!currentUser) return false;
|
|
108
123
|
if (currentUser.is_superuser) return true;
|
|
109
|
-
|
|
124
|
+
|
|
110
125
|
const myRole = currentUser.role || 'none';
|
|
111
126
|
const targetRole = targetUser.role || 'none';
|
|
112
127
|
|
|
113
128
|
if (myRole === 'admin') return true;
|
|
114
|
-
|
|
129
|
+
|
|
115
130
|
if (myRole === 'teacher') {
|
|
116
|
-
if (targetUser.id === currentUser.id) return false;
|
|
131
|
+
if (targetUser.id === currentUser.id) return false;
|
|
117
132
|
if (['teacher', 'admin', 'supervisor'].includes(targetRole)) return false;
|
|
118
133
|
return true;
|
|
119
134
|
}
|
|
@@ -267,16 +282,23 @@ export function UserListComponent({
|
|
|
267
282
|
field: 'email',
|
|
268
283
|
headerName: t('Auth.EMAIL_LABEL', 'Email'),
|
|
269
284
|
minWidth: 220,
|
|
270
|
-
maxWidth: 340,
|
|
271
285
|
flex: 1,
|
|
286
|
+
sortable: true,
|
|
287
|
+
align: 'left',
|
|
288
|
+
headerAlign: 'left',
|
|
289
|
+
valueGetter: (row) => row?.email ?? '',
|
|
290
|
+
renderCell: (row) => row.email,
|
|
272
291
|
},
|
|
273
292
|
{
|
|
274
293
|
field: 'name',
|
|
275
294
|
headerName: t('Profile.NAME_LABEL', 'Name'),
|
|
276
295
|
minWidth: 180,
|
|
277
|
-
maxWidth: 260,
|
|
278
296
|
flex: 0.9,
|
|
279
|
-
|
|
297
|
+
sortable: true,
|
|
298
|
+
align: 'left',
|
|
299
|
+
headerAlign: 'left',
|
|
300
|
+
valueGetter: (row) => getUserDisplayName(row),
|
|
301
|
+
renderCell: (row) => getUserDisplayName(row),
|
|
280
302
|
},
|
|
281
303
|
];
|
|
282
304
|
|
|
@@ -285,13 +307,13 @@ export function UserListComponent({
|
|
|
285
307
|
field: 'is_new',
|
|
286
308
|
headerName: t('UserList.NEW', 'New'),
|
|
287
309
|
minWidth: 90,
|
|
288
|
-
maxWidth: 110,
|
|
289
310
|
flex: 0.35,
|
|
311
|
+
sortable: true,
|
|
290
312
|
align: 'center',
|
|
291
313
|
headerAlign: 'center',
|
|
292
|
-
valueGetter: (
|
|
293
|
-
renderCell: (
|
|
294
|
-
Boolean(
|
|
314
|
+
valueGetter: (row) => Boolean(row?.profile?.is_new || row?.is_new),
|
|
315
|
+
renderCell: (row) => renderBooleanStatusIcon(
|
|
316
|
+
Boolean(row?.profile?.is_new || row?.is_new),
|
|
295
317
|
t('Common.YES', 'Yes'),
|
|
296
318
|
t('Common.NO', 'No'),
|
|
297
319
|
),
|
|
@@ -303,13 +325,13 @@ export function UserListComponent({
|
|
|
303
325
|
field: 'successful_login',
|
|
304
326
|
headerName: t('UserList.SUCCESSFUL_LOGIN', 'Successful Login'),
|
|
305
327
|
minWidth: 120,
|
|
306
|
-
maxWidth: 150,
|
|
307
328
|
flex: 0.4,
|
|
329
|
+
sortable: true,
|
|
308
330
|
align: 'center',
|
|
309
331
|
headerAlign: 'center',
|
|
310
|
-
valueGetter: (
|
|
311
|
-
renderCell: (
|
|
312
|
-
Boolean(
|
|
332
|
+
valueGetter: (row) => Boolean(row?.successful_login ?? row?.last_login),
|
|
333
|
+
renderCell: (row) => renderBooleanStatusIcon(
|
|
334
|
+
Boolean(row?.successful_login ?? row?.last_login),
|
|
313
335
|
t('Common.YES', 'Yes'),
|
|
314
336
|
t('Common.NO', 'No'),
|
|
315
337
|
),
|
|
@@ -321,23 +343,22 @@ export function UserListComponent({
|
|
|
321
343
|
field: 'role',
|
|
322
344
|
headerName: t('UserList.ROLE', 'Role'),
|
|
323
345
|
minWidth: 180,
|
|
324
|
-
maxWidth: 240,
|
|
325
346
|
flex: 0.7,
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
</
|
|
338
|
-
</
|
|
339
|
-
|
|
340
|
-
|
|
347
|
+
sortable: true,
|
|
348
|
+
align: 'left',
|
|
349
|
+
headerAlign: 'left',
|
|
350
|
+
valueGetter: (row) => row?.role || 'none',
|
|
351
|
+
renderCell: (row) => (
|
|
352
|
+
<FormControl size="small" fullWidth sx={controlSx} disabled={!canEdit(row)}>
|
|
353
|
+
<Select
|
|
354
|
+
value={row.role || 'none'}
|
|
355
|
+
onChange={(event) => handleChangeRole(row.id, event.target.value)}
|
|
356
|
+
variant="outlined"
|
|
357
|
+
>
|
|
358
|
+
{roles.map((role) => <MenuItem key={role} value={role}>{role}</MenuItem>)}
|
|
359
|
+
</Select>
|
|
360
|
+
</FormControl>
|
|
361
|
+
),
|
|
341
362
|
});
|
|
342
363
|
}
|
|
343
364
|
|
|
@@ -345,16 +366,15 @@ export function UserListComponent({
|
|
|
345
366
|
field: `extra:${column.key}`,
|
|
346
367
|
headerName: typeof column.label === 'function' ? column.label(listContext) : column.label,
|
|
347
368
|
minWidth: Number(column.minWidth) || 180,
|
|
348
|
-
maxWidth: Number(column.maxWidth) || 320,
|
|
349
369
|
flex: Number(column.flex) || 0.9,
|
|
350
370
|
sortable: column.sortable !== false,
|
|
351
371
|
align: column.align || 'left',
|
|
352
372
|
headerAlign: column.align || 'left',
|
|
353
|
-
valueGetter: (
|
|
354
|
-
renderCell: (
|
|
373
|
+
valueGetter: (row) => getExtraColumnSortValue(column, row),
|
|
374
|
+
renderCell: (row) =>
|
|
355
375
|
column.renderCell({
|
|
356
|
-
user:
|
|
357
|
-
canEdit: canEdit(
|
|
376
|
+
user: row,
|
|
377
|
+
canEdit: canEdit(row),
|
|
358
378
|
currentUser,
|
|
359
379
|
extraContext,
|
|
360
380
|
t,
|
|
@@ -372,65 +392,61 @@ export function UserListComponent({
|
|
|
372
392
|
field: 'actions',
|
|
373
393
|
headerName: t('Common.ACTIONS', 'Actions'),
|
|
374
394
|
minWidth: Math.max(220, 110 + (visibleRowActions.length + (showDeleteAction ? 1 : 0)) * 110),
|
|
375
|
-
maxWidth: Math.max(360, 120 + (visibleRowActions.length + (showDeleteAction ? 1 : 0)) * 120),
|
|
376
395
|
flex: 1.1,
|
|
377
396
|
sortable: false,
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
397
|
+
align: 'left',
|
|
398
|
+
headerAlign: 'left',
|
|
399
|
+
valueGetter: null,
|
|
400
|
+
renderCell: (row) => (
|
|
401
|
+
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, alignItems: 'center', py: 0.5 }}>
|
|
402
|
+
{visibleRowActions.map((action) => {
|
|
403
|
+
const actionId = `${action.key}:${row.id}`;
|
|
404
|
+
const isBusy = Boolean(rowActionLoading[actionId]);
|
|
405
|
+
const isDisabled = typeof action.disabled === 'function'
|
|
406
|
+
? action.disabled({
|
|
407
|
+
user: row,
|
|
408
|
+
canEdit: canEdit(row),
|
|
409
|
+
currentUser,
|
|
410
|
+
extraContext,
|
|
411
|
+
t,
|
|
412
|
+
})
|
|
413
|
+
: false;
|
|
414
|
+
|
|
415
|
+
return (
|
|
416
|
+
<Button
|
|
417
|
+
key={`${action.key}-${row.id}`}
|
|
418
|
+
size="small"
|
|
419
|
+
variant="outlined"
|
|
420
|
+
onClick={() => runRowAction(action, row)}
|
|
421
|
+
disabled={isBusy || isDisabled}
|
|
422
|
+
sx={actionButtonSx}
|
|
423
|
+
>
|
|
424
|
+
{typeof action.label === 'function'
|
|
425
|
+
? action.label({ user: row, t, currentUser, canEdit: canEdit(row) })
|
|
426
|
+
: action.label}
|
|
427
|
+
</Button>
|
|
428
|
+
);
|
|
429
|
+
})}
|
|
430
|
+
|
|
431
|
+
{showDeleteAction && (
|
|
432
|
+
<Tooltip title={t('Common.DELETE', 'Delete')}>
|
|
433
|
+
<span>
|
|
399
434
|
<Button
|
|
400
|
-
key={`${action.key}-${user.id}`}
|
|
401
435
|
size="small"
|
|
402
436
|
variant="outlined"
|
|
403
|
-
|
|
404
|
-
|
|
437
|
+
color="error"
|
|
438
|
+
startIcon={<DeleteIcon />}
|
|
439
|
+
onClick={() => handleDelete(row.id)}
|
|
440
|
+
disabled={!canDelete(row)}
|
|
405
441
|
sx={actionButtonSx}
|
|
406
442
|
>
|
|
407
|
-
{
|
|
408
|
-
? action.label({ user, t, currentUser, canEdit: canEdit(user) })
|
|
409
|
-
: action.label}
|
|
443
|
+
{t('Common.DELETE', 'Delete')}
|
|
410
444
|
</Button>
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
<span>
|
|
417
|
-
<Button
|
|
418
|
-
size="small"
|
|
419
|
-
variant="outlined"
|
|
420
|
-
color="error"
|
|
421
|
-
startIcon={<DeleteIcon />}
|
|
422
|
-
onClick={() => handleDelete(user.id)}
|
|
423
|
-
disabled={!canDelete(user)}
|
|
424
|
-
sx={actionButtonSx}
|
|
425
|
-
>
|
|
426
|
-
{t('Common.DELETE', 'Delete')}
|
|
427
|
-
</Button>
|
|
428
|
-
</span>
|
|
429
|
-
</Tooltip>
|
|
430
|
-
)}
|
|
431
|
-
</Box>
|
|
432
|
-
);
|
|
433
|
-
},
|
|
445
|
+
</span>
|
|
446
|
+
</Tooltip>
|
|
447
|
+
)}
|
|
448
|
+
</Box>
|
|
449
|
+
),
|
|
434
450
|
};
|
|
435
451
|
|
|
436
452
|
return [...baseColumns, ...mappedExtraColumns, actionColumn];
|
|
@@ -452,6 +468,31 @@ export function UserListComponent({
|
|
|
452
468
|
canDelete,
|
|
453
469
|
]);
|
|
454
470
|
|
|
471
|
+
const sortedUsers = useMemo(() => {
|
|
472
|
+
const col = columns.find((c) => c.field === sortField);
|
|
473
|
+
return [...filteredUsers].sort((a, b) => {
|
|
474
|
+
const aVal = col?.valueGetter ? col.valueGetter(a) : (a[sortField] ?? '');
|
|
475
|
+
const bVal = col?.valueGetter ? col.valueGetter(b) : (b[sortField] ?? '');
|
|
476
|
+
const cmp = String(aVal ?? '').localeCompare(String(bVal ?? ''), undefined, { numeric: true, sensitivity: 'base' });
|
|
477
|
+
return sortDir === 'asc' ? cmp : -cmp;
|
|
478
|
+
});
|
|
479
|
+
}, [filteredUsers, sortField, sortDir, columns]);
|
|
480
|
+
|
|
481
|
+
const pagedUsers = useMemo(
|
|
482
|
+
() => sortedUsers.slice(page * pageSize, page * pageSize + pageSize),
|
|
483
|
+
[sortedUsers, page, pageSize],
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
const handleSortClick = (field) => {
|
|
487
|
+
if (sortField === field) {
|
|
488
|
+
setSortDir((d) => (d === 'asc' ? 'desc' : 'asc'));
|
|
489
|
+
} else {
|
|
490
|
+
setSortField(field);
|
|
491
|
+
setSortDir('asc');
|
|
492
|
+
}
|
|
493
|
+
setPage(0);
|
|
494
|
+
};
|
|
495
|
+
|
|
455
496
|
return (
|
|
456
497
|
<Box>
|
|
457
498
|
<Typography variant="h6" gutterBottom>{t('UserList.TITLE', 'All Users')}</Typography>
|
|
@@ -466,7 +507,7 @@ export function UserListComponent({
|
|
|
466
507
|
onChange={(event) => setSearchQuery(event.target.value)}
|
|
467
508
|
/>
|
|
468
509
|
</Box>
|
|
469
|
-
|
|
510
|
+
|
|
470
511
|
{error && <Alert severity="error" sx={{ mb: 2 }}>{t(error)}</Alert>}
|
|
471
512
|
|
|
472
513
|
{loading && (
|
|
@@ -476,40 +517,73 @@ export function UserListComponent({
|
|
|
476
517
|
)}
|
|
477
518
|
|
|
478
519
|
{!loading && (
|
|
479
|
-
<Box sx={{ width: '100%'
|
|
480
|
-
<
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
520
|
+
<Box sx={{ width: '100%' }}>
|
|
521
|
+
<TableContainer sx={{ border: 1, borderColor: 'divider', borderRadius: 1 }}>
|
|
522
|
+
<Table size="small">
|
|
523
|
+
<TableHead sx={{ bgcolor: 'action.hover' }}>
|
|
524
|
+
<TableRow>
|
|
525
|
+
{columns.map((col) => (
|
|
526
|
+
<TableCell
|
|
527
|
+
key={col.field}
|
|
528
|
+
align={col.headerAlign || col.align || 'left'}
|
|
529
|
+
sortDirection={sortField === col.field ? sortDir : false}
|
|
530
|
+
sx={{ minWidth: col.minWidth, fontWeight: 600, whiteSpace: 'nowrap' }}
|
|
531
|
+
>
|
|
532
|
+
{col.sortable !== false ? (
|
|
533
|
+
<TableSortLabel
|
|
534
|
+
active={sortField === col.field}
|
|
535
|
+
direction={sortField === col.field ? sortDir : 'asc'}
|
|
536
|
+
onClick={() => handleSortClick(col.field)}
|
|
537
|
+
>
|
|
538
|
+
{col.headerName}
|
|
539
|
+
</TableSortLabel>
|
|
540
|
+
) : (
|
|
541
|
+
col.headerName
|
|
542
|
+
)}
|
|
543
|
+
</TableCell>
|
|
544
|
+
))}
|
|
545
|
+
</TableRow>
|
|
546
|
+
</TableHead>
|
|
547
|
+
<TableBody>
|
|
548
|
+
{pagedUsers.length === 0 ? (
|
|
549
|
+
<TableRow>
|
|
550
|
+
<TableCell
|
|
551
|
+
colSpan={columns.length}
|
|
552
|
+
align="center"
|
|
553
|
+
sx={{ py: 4, color: 'text.secondary' }}
|
|
554
|
+
>
|
|
555
|
+
{t('UserList.NO_USERS', 'No users found.')}
|
|
556
|
+
</TableCell>
|
|
557
|
+
</TableRow>
|
|
558
|
+
) : (
|
|
559
|
+
pagedUsers.map((row) => (
|
|
560
|
+
<TableRow key={row.id} hover>
|
|
561
|
+
{columns.map((col) => (
|
|
562
|
+
<TableCell
|
|
563
|
+
key={col.field}
|
|
564
|
+
align={col.align || 'left'}
|
|
565
|
+
sx={{ verticalAlign: 'top', py: 1 }}
|
|
566
|
+
>
|
|
567
|
+
{col.renderCell(row)}
|
|
568
|
+
</TableCell>
|
|
569
|
+
))}
|
|
570
|
+
</TableRow>
|
|
571
|
+
))
|
|
572
|
+
)}
|
|
573
|
+
</TableBody>
|
|
574
|
+
</Table>
|
|
575
|
+
</TableContainer>
|
|
576
|
+
<TablePagination
|
|
577
|
+
component="div"
|
|
578
|
+
count={sortedUsers.length}
|
|
579
|
+
page={page}
|
|
580
|
+
onPageChange={(_, newPage) => setPage(newPage)}
|
|
581
|
+
rowsPerPage={pageSize}
|
|
582
|
+
onRowsPerPageChange={(event) => {
|
|
583
|
+
setPageSize(Number(event.target.value));
|
|
584
|
+
setPage(0);
|
|
512
585
|
}}
|
|
586
|
+
rowsPerPageOptions={[10, 25, 50, 100]}
|
|
513
587
|
/>
|
|
514
588
|
</Box>
|
|
515
589
|
)}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const notificationsTranslations = {
|
|
2
|
+
'NotificationSettings.TITLE': { de: 'Benachrichtigungen', fr: 'Notifications', en: 'Notifications', sw: 'Arifa' },
|
|
3
|
+
'NotificationSettings.SUBTITLE': { de: 'Lege fest, wie du informiert werden möchtest.', fr: 'Choisissez comment vous souhaitez être informé.', en: 'Choose how you want to be notified.', sw: 'Chagua jinsi unavyotaka kuarifiwa.' },
|
|
4
|
+
'NotificationSettings.LOAD_ERROR': { de: 'Benachrichtigungseinstellungen konnten nicht geladen werden.', fr: 'Impossible de charger les paramètres de notification.', en: 'Notification settings could not be loaded.', sw: 'Mipangilio ya arifa haikuweza kupakiwa.' },
|
|
5
|
+
'NotificationSettings.SAVE_ERROR': { de: 'Benachrichtigungseinstellungen konnten nicht gespeichert werden.', fr: 'Impossible d’enregistrer les paramètres de notification.', en: 'Notification settings could not be saved.', sw: 'Mipangilio ya arifa haikuweza kuhifadhiwa.' },
|
|
6
|
+
'NotificationSettings.EMAIL_LABEL': { de: 'E-Mail-Benachrichtigungen', fr: 'Notifications par e-mail', en: 'Email notifications', sw: 'Arifa za barua pepe' },
|
|
7
|
+
'NotificationSettings.EMAIL_HINT': { de: 'Erhalte wichtige Informationen per E-Mail.', fr: 'Recevez les informations importantes par e-mail.', en: 'Receive important information by email.', sw: 'Pokea taarifa muhimu kwa barua pepe.' },
|
|
8
|
+
'NotificationSettings.PUSH_LABEL': { de: 'Push-Benachrichtigungen', fr: 'Notifications push', en: 'Push notifications', sw: 'Arifa za push' },
|
|
9
|
+
'NotificationSettings.PUSH_HINT': { de: 'Aktiviere Benachrichtigungen für dieses Gerät.', fr: 'Activez les notifications pour cet appareil.', en: 'Enable notifications for this device.', sw: 'Washa arifa kwa kifaa hiki.' },
|
|
10
|
+
'NotificationSettings.PUSH_ENABLE': { de: 'Push aktivieren', fr: 'Activer les notifications push', en: 'Enable push', sw: 'Washa push' },
|
|
11
|
+
'NotificationSettings.PUSH_DISABLE': { de: 'Push auf diesem Gerät deaktivieren', fr: 'Désactiver les notifications push sur cet appareil', en: 'Disable push on this device', sw: 'Zima push kwenye kifaa hiki' },
|
|
12
|
+
'NotificationSettings.PUSH_DENIED': { de: 'Die Browserberechtigung für Push-Benachrichtigungen wurde verweigert.', fr: 'L’autorisation du navigateur pour les notifications push a été refusée.', en: 'Browser permission for push notifications was denied.', sw: 'Ruhusa ya kivinjari kwa arifa za push imekataliwa.' },
|
|
13
|
+
'NotificationSettings.PUSH_ERROR': { de: 'Push-Benachrichtigungen konnten nicht aktualisiert werden.', fr: 'Impossible de mettre à jour les notifications push.', en: 'Push notifications could not be updated.', sw: 'Arifa za push hazikuweza kusasishwa.' },
|
|
14
|
+
'NotificationSettings.PUSH_CONFLICT': { de: 'Dieses Gerät ist bereits mit einem anderen Konto für Push-Benachrichtigungen verbunden.', fr: 'Cet appareil est déjà associé à un autre compte pour les notifications push.', en: 'This device is already associated with another account for push notifications.', sw: 'Kifaa hiki tayari kimeunganishwa na akaunti nyingine kwa arifa za push.' },
|
|
15
|
+
'NotificationSettings.PUSH_NOT_SUPPORTED': { de: 'Push-Benachrichtigungen werden von diesem Browser nicht unterstützt.', fr: 'Les notifications push ne sont pas prises en charge par ce navigateur.', en: 'Push notifications are not supported by this browser.', sw: 'Arifa za push hazitumiki na kivinjari hiki.' },
|
|
16
|
+
'NotificationSettings.IOS_HINT': { de: 'Füge diese App zuerst zum Home-Bildschirm hinzu, um Push-Benachrichtigungen zu aktivieren.', fr: 'Ajoutez d’abord cette application à l’écran d’accueil pour activer les notifications push.', en: 'Add this app to your home screen first to enable push notifications.', sw: 'Ongeza programu hii kwenye skrini yako ya mwanzo kwanza ili kuwasha arifa za push.' },
|
|
17
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const onboardingTranslations = {
|
|
2
|
+
'Onboarding.SETUP': { de: 'Einrichtung', fr: 'Configuration', en: 'Setup', sw: 'Usanidi' },
|
|
3
|
+
'Onboarding.STEP_COUNTER': { de: 'Schritt {{current}} von {{total}}', fr: 'Étape {{current}} sur {{total}}', en: 'Step {{current}} of {{total}}', sw: 'Hatua ya {{current}} kati ya {{total}}' },
|
|
4
|
+
'Onboarding.COOKIE_CONSENT_TITLE': { de: 'Cookie-Einstellungen', fr: 'Préférences des cookies', en: 'Cookie preferences', sw: 'Mapendeleo ya vidakuzi' },
|
|
5
|
+
'Onboarding.COOKIE_CONSENT_BODY': { de: 'Diese App verwendet optionale Cookies, um deine Einstellungen und Präferenzen zu speichern. Möchtest du diese aktivieren?', fr: 'Cette application utilise des cookies facultatifs pour enregistrer vos réglages et préférences. Souhaitez-vous les activer ?', en: 'This app uses optional cookies to save your settings and preferences. Would you like to enable them?', sw: 'Programu hii hutumia vidakuzi vya hiari kuhifadhi mipangilio na mapendeleo yako. Je, ungependa kuviwasha?' },
|
|
6
|
+
'Onboarding.COOKIE_CONSENT_ACCEPT': { de: 'Akzeptieren', fr: 'Accepter', en: 'Accept', sw: 'Kubali' },
|
|
7
|
+
'Onboarding.COOKIE_CONSENT_ERROR': { de: 'Die Cookie-Einstellung konnte nicht gespeichert werden. Bitte versuche es erneut.', fr: 'Impossible d’enregistrer votre préférence de cookies. Veuillez réessayer.', en: 'Your cookie preference could not be saved. Please try again.', sw: 'Upendeleo wako wa vidakuzi haukuweza kuhifadhiwa. Tafadhali jaribu tena.' },
|
|
8
|
+
'Onboarding.COMPLETE_NAME_TITLE': { de: 'Dein Name', fr: 'Votre nom', en: 'Your name', sw: 'Jina lako' },
|
|
9
|
+
'Onboarding.COMPLETE_NAME_BODY': { de: 'Bitte gib deinen Vor- und Nachnamen an, damit andere dich erkennen.', fr: 'Veuillez indiquer votre prénom et votre nom afin que les autres puissent vous reconnaître.', en: 'Please provide your first and last name so others can recognise you.', sw: 'Tafadhali weka jina lako la kwanza na la mwisho ili wengine wakutambue.' },
|
|
10
|
+
'Onboarding.FIRST_NAME_LABEL': { de: 'Vorname', fr: 'Prénom', en: 'First name', sw: 'Jina la kwanza' },
|
|
11
|
+
'Onboarding.LAST_NAME_LABEL': { de: 'Nachname', fr: 'Nom', en: 'Last name', sw: 'Jina la mwisho' },
|
|
12
|
+
'Onboarding.SAVE': { de: 'Speichern', fr: 'Enregistrer', en: 'Save', sw: 'Hifadhi' },
|
|
13
|
+
'Onboarding.COMPLETE_NAME_ERROR': { de: 'Der Name konnte nicht gespeichert werden. Bitte versuche es erneut.', fr: 'Impossible d’enregistrer votre nom. Veuillez réessayer.', en: 'Your name could not be saved. Please try again.', sw: 'Jina lako halikuweza kuhifadhiwa. Tafadhali jaribu tena.' },
|
|
14
|
+
'Onboarding.BROWSER_PUSH_TITLE': { de: 'Push-Benachrichtigungen', fr: 'Notifications push', en: 'Push notifications', sw: 'Arifa za push' },
|
|
15
|
+
'Onboarding.BROWSER_PUSH_BODY': { de: 'Erhalte Benachrichtigungen direkt im Browser, auch wenn die App nicht geöffnet ist.', fr: 'Recevez des notifications directement dans votre navigateur, même lorsque l’application est fermée.', en: 'Receive notifications directly in your browser, even when the app is not open.', sw: 'Pokea arifa moja kwa moja kwenye kivinjari chako, hata programu haijafunguliwa.' },
|
|
16
|
+
'Onboarding.SKIP': { de: 'Überspringen', fr: 'Passer', en: 'Skip', sw: 'Ruka' },
|
|
17
|
+
};
|
package/src/index.js
CHANGED
|
@@ -41,3 +41,25 @@ export { QrSignupManager } from './components/QrSignupManager';
|
|
|
41
41
|
|
|
42
42
|
// --- 6. Translations ---
|
|
43
43
|
export { authTranslations } from './i18n/authTranslations';
|
|
44
|
+
|
|
45
|
+
// --- 7. Notifications ---
|
|
46
|
+
export { NotificationSettings } from './notifications/NotificationSettings';
|
|
47
|
+
export * from './notifications/api';
|
|
48
|
+
|
|
49
|
+
// --- 8. Onboarding ---
|
|
50
|
+
export * from './onboarding/api';
|
|
51
|
+
export { selectActiveSteps } from './onboarding/stepSelection';
|
|
52
|
+
export {
|
|
53
|
+
OnboardingContext,
|
|
54
|
+
OnboardingProvider,
|
|
55
|
+
UNIVERSAL_STEP_DESCRIPTORS,
|
|
56
|
+
useOnboarding,
|
|
57
|
+
} from './onboarding/OnboardingProvider';
|
|
58
|
+
export { OnboardingWizard } from './onboarding/OnboardingWizard';
|
|
59
|
+
export { CookieConsentStep } from './onboarding/steps/CookieConsentStep';
|
|
60
|
+
export { CompleteNameStep } from './onboarding/steps/CompleteNameStep';
|
|
61
|
+
export { BrowserPushStep } from './onboarding/steps/BrowserPushStep';
|
|
62
|
+
|
|
63
|
+
// --- 9. Translations ---
|
|
64
|
+
export { notificationsTranslations } from './i18n/notificationsTranslations';
|
|
65
|
+
export { onboardingTranslations } from './i18n/onboardingTranslations';
|