@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.
Files changed (33) hide show
  1. package/.github/workflows/publish.yml +4 -0
  2. package/dist/components/UserListComponent.js +81 -71
  3. package/dist/i18n/notificationsTranslations.js +17 -0
  4. package/dist/i18n/onboardingTranslations.js +17 -0
  5. package/dist/index.js +14 -0
  6. package/dist/notifications/NotificationSettings.js +148 -0
  7. package/dist/notifications/api.js +41 -0
  8. package/dist/notifications/serviceWorker/sw.js +49 -0
  9. package/dist/onboarding/OnboardingProvider.js +133 -0
  10. package/dist/onboarding/OnboardingWizard.js +47 -0
  11. package/dist/onboarding/api.js +10 -0
  12. package/dist/onboarding/stepSelection.js +18 -0
  13. package/dist/onboarding/steps/BrowserPushStep.js +49 -0
  14. package/dist/onboarding/steps/CompleteNameStep.js +37 -0
  15. package/dist/onboarding/steps/CookieConsentStep.js +31 -0
  16. package/package.json +7 -4
  17. package/src/components/UserListComponent.jsx +196 -122
  18. package/src/i18n/notificationsTranslations.ts +17 -0
  19. package/src/i18n/onboardingTranslations.ts +17 -0
  20. package/src/index.js +22 -0
  21. package/src/notifications/NotificationSettings.jsx +189 -0
  22. package/src/notifications/api.js +49 -0
  23. package/src/notifications/serviceWorker/sw.js +50 -0
  24. package/src/onboarding/OnboardingProvider.jsx +153 -0
  25. package/src/onboarding/OnboardingWizard.jsx +66 -0
  26. package/src/onboarding/api.js +13 -0
  27. package/src/onboarding/stepSelection.js +16 -0
  28. package/src/onboarding/steps/BrowserPushStep.jsx +63 -0
  29. package/src/onboarding/steps/CompleteNameStep.jsx +46 -0
  30. package/src/onboarding/steps/CookieConsentStep.jsx +39 -0
  31. package/tests/NotificationSettings.test.jsx +82 -0
  32. package/tests/notificationsApi.test.js +41 -0
  33. 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
- valueGetter: (_value, row) => getUserDisplayName(row),
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: (_value, row) => Boolean(row?.profile?.is_new || row?.is_new),
293
- renderCell: (params) => renderBooleanStatusIcon(
294
- Boolean(params.row?.profile?.is_new || params.row?.is_new),
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: (_value, row) => Boolean(row?.successful_login ?? row?.last_login),
311
- renderCell: (params) => renderBooleanStatusIcon(
312
- Boolean(params.row?.successful_login ?? params.row?.last_login),
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
- valueGetter: (_value, row) => row?.role || 'none',
327
- renderCell: (params) => {
328
- const user = params.row;
329
- return (
330
- <FormControl size="small" fullWidth sx={controlSx} disabled={!canEdit(user)}>
331
- <Select
332
- value={user.role || 'none'}
333
- onChange={(event) => handleChangeRole(user.id, event.target.value)}
334
- variant="outlined"
335
- >
336
- {roles.map((role) => <MenuItem key={role} value={role}>{role}</MenuItem>)}
337
- </Select>
338
- </FormControl>
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: (_value, row) => getExtraColumnSortValue(column, row),
354
- renderCell: (params) =>
373
+ valueGetter: (row) => getExtraColumnSortValue(column, row),
374
+ renderCell: (row) =>
355
375
  column.renderCell({
356
- user: params.row,
357
- canEdit: canEdit(params.row),
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
- filterable: false,
379
- disableColumnMenu: true,
380
- renderCell: (params) => {
381
- const user = params.row;
382
-
383
- return (
384
- <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, alignItems: 'center', py: 0.5 }}>
385
- {visibleRowActions.map((action) => {
386
- const actionId = `${action.key}:${user.id}`;
387
- const isBusy = Boolean(rowActionLoading[actionId]);
388
- const isDisabled = typeof action.disabled === 'function'
389
- ? action.disabled({
390
- user,
391
- canEdit: canEdit(user),
392
- currentUser,
393
- extraContext,
394
- t,
395
- })
396
- : false;
397
-
398
- return (
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
- onClick={() => runRowAction(action, user)}
404
- disabled={isBusy || isDisabled}
437
+ color="error"
438
+ startIcon={<DeleteIcon />}
439
+ onClick={() => handleDelete(row.id)}
440
+ disabled={!canDelete(row)}
405
441
  sx={actionButtonSx}
406
442
  >
407
- {typeof action.label === 'function'
408
- ? action.label({ user, t, currentUser, canEdit: canEdit(user) })
409
- : action.label}
443
+ {t('Common.DELETE', 'Delete')}
410
444
  </Button>
411
- );
412
- })}
413
-
414
- {showDeleteAction && (
415
- <Tooltip title={t('Common.DELETE', 'Delete')}>
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%', minHeight: 520 }}>
480
- <DataGrid
481
- rows={filteredUsers}
482
- columns={columns}
483
- disableRowSelectionOnClick
484
- showToolbar
485
- getRowHeight={() => 'auto'}
486
- pageSizeOptions={[10, 25, 50, 100]}
487
- initialState={{
488
- sorting: { sortModel: [{ field: 'email', sort: 'asc' }] },
489
- pagination: { paginationModel: { pageSize: 25, page: 0 } },
490
- }}
491
- localeText={{
492
- noRowsLabel: t('UserList.NO_USERS', 'No users found.'),
493
- toolbarQuickFilterPlaceholder: t('UserList.SEARCH_PLACEHOLDER', 'Search users...'),
494
- }}
495
- slotProps={{
496
- toolbar: {
497
- showQuickFilter: true,
498
- quickFilterProps: { debounceMs: 300 },
499
- },
500
- }}
501
- sx={{
502
- border: 1,
503
- borderColor: 'divider',
504
- '& .MuiDataGrid-cell': {
505
- display: 'flex',
506
- alignItems: 'flex-start',
507
- py: 1,
508
- },
509
- '& .MuiDataGrid-columnHeaders': {
510
- bgcolor: 'action.hover',
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';