@local-civics/mgmt-ui 0.1.184 → 0.1.186

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/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as React from 'react';
2
- import { useState } from 'react';
2
+ import { useState, useMemo } from 'react';
3
3
  import { createStyles, Group, Avatar, Text, Box, Badge as Badge$1, ThemeIcon, Collapse, Navbar as Navbar$1, Center, Image, Code, Burger, ScrollArea, Modal, Loader, Container, Button, Title, UnstyledButton, Card, SimpleGrid, Tabs as Tabs$1, Stack as Stack$4, Grid, Table as Table$k, ActionIcon, LoadingOverlay, Select, Autocomplete, Drawer, Divider, TextInput, Tooltip, Paper, Overlay, Anchor, Menu, Checkbox, createEmotionCache, MantineProvider, AppShell } from '@mantine/core';
4
- import { IconChevronRight, IconChevronLeft, IconVideo, IconSwitchHorizontal, IconLogout, IconHome2, IconGauge, IconCategory2, IconRoute, IconAlbum, IconLambda, IconBuilding, IconBatteryEco, IconBooks, IconBackpack, IconClipboardCopy, IconTableExport, IconArrowLeft, IconPlaylistAdd, IconCheck, IconTrash, IconDownload, IconX, IconCloudUpload, IconInfoCircle, IconColorSwatch, IconPointer, IconChevronDown, IconScribble, IconSchool, IconPodium, IconBriefcase, IconPresentation, IconNews, IconTools, IconBrandInstagram, IconBrandLinkedin, IconBrandFacebook } from '@tabler/icons';
4
+ import { IconChevronRight, IconChevronLeft, IconVideo, IconSwitchHorizontal, IconLogout, IconHome2, IconGauge, IconCategory2, IconRoute, IconAlbum, IconLambda, IconBuilding, IconBatteryEco, IconBooks, IconBackpack, IconClipboardCopy, IconTableExport, IconArrowLeft, IconPlaylistAdd, IconCheck, IconTrash, IconDownload, IconX, IconCloudUpload, IconChevronUp, IconChevronDown, IconSelector, IconInfoCircle, IconColorSwatch, IconPointer, IconScribble, IconSchool, IconPodium, IconBriefcase, IconPresentation, IconNews, IconTools, IconBrandInstagram, IconBrandLinkedin, IconBrandFacebook } from '@tabler/icons';
5
5
  import { Link } from 'react-router-dom';
6
6
  import { showNotification, NotificationsProvider } from '@mantine/notifications';
7
7
  export { showNotification, updateNotification } from '@mantine/notifications';
@@ -42,7 +42,7 @@ var __objRest$2 = (source, exclude) => {
42
42
  }
43
43
  return target;
44
44
  };
45
- const useStyles$t = createStyles((theme) => ({
45
+ const useStyles$u = createStyles((theme) => ({
46
46
  user: {
47
47
  display: "block",
48
48
  width: "100%",
@@ -53,7 +53,7 @@ const useStyles$t = createStyles((theme) => ({
53
53
  }));
54
54
  function UserButton(_a) {
55
55
  var _b = _a, { image, name, email, icon } = _b, others = __objRest$2(_b, ["image", "name", "email", "icon"]);
56
- const { classes } = useStyles$t();
56
+ const { classes } = useStyles$u();
57
57
  return /* @__PURE__ */ React.createElement(Group, __spreadValues$9({ className: classes.user }, others), /* @__PURE__ */ React.createElement(Avatar, { src: image, radius: "xl" }), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }, /* @__PURE__ */ React.createElement(Text, { size: "sm", weight: 500 }, name), /* @__PURE__ */ React.createElement(Text, { color: "dimmed", size: "xs" }, email)));
58
58
  }
59
59
 
@@ -64,7 +64,7 @@ const compact = (num) => {
64
64
  }).format(num || 0);
65
65
  };
66
66
 
67
- const useStyles$s = createStyles((theme, _params, getRef) => {
67
+ const useStyles$t = createStyles((theme, _params, getRef) => {
68
68
  const icon = getRef("icon");
69
69
  return {
70
70
  control: {
@@ -117,7 +117,7 @@ const useStyles$s = createStyles((theme, _params, getRef) => {
117
117
  };
118
118
  });
119
119
  function LinksGroup({ icon: Icon, href, label, initiallyOpened, links, active, notifications }) {
120
- const { classes, theme, cx } = useStyles$s();
120
+ const { classes, theme, cx } = useStyles$t();
121
121
  const hasLinks = Array.isArray(links) && links.length > 0;
122
122
  const hasActiveLinks = Array.isArray(links) && links.map((l) => !!active && active === `${label}/${l.label}`).reduce((a, b) => a || b, false);
123
123
  const [opened, setOpened] = useState(initiallyOpened || hasActiveLinks || false);
@@ -181,7 +181,7 @@ var __spreadValues$8 = (a, b) => {
181
181
  return a;
182
182
  };
183
183
  var __spreadProps$5 = (a, b) => __defProps$5(a, __getOwnPropDescs$5(b));
184
- const useStyles$r = createStyles((theme, _params, getRef) => {
184
+ const useStyles$s = createStyles((theme, _params, getRef) => {
185
185
  const icon = getRef("icon");
186
186
  return {
187
187
  navbar: {
@@ -281,7 +281,7 @@ const TRIAL_PAGES = [
281
281
  "Badges"
282
282
  ];
283
283
  function Navbar(props) {
284
- const { classes, cx } = useStyles$r();
284
+ const { classes, cx } = useStyles$s();
285
285
  const [burgerOpen, setBurgerOpen] = React.useState(false);
286
286
  const toggle = () => setBurgerOpen(!burgerOpen);
287
287
  const links = data.map((item) => {
@@ -324,7 +324,7 @@ function Navbar(props) {
324
324
  } }, /* @__PURE__ */ React.createElement(IconLogout, { className: classes.linkIcon, stroke: 1.5 }), /* @__PURE__ */ React.createElement("span", null, "Logout"))))));
325
325
  }
326
326
 
327
- const useStyles$q = createStyles((theme) => ({
327
+ const useStyles$r = createStyles((theme) => ({
328
328
  inner: {
329
329
  paddingTop: theme.spacing.xl,
330
330
  paddingBottom: theme.spacing.xl * 4
@@ -360,7 +360,7 @@ const useStyles$q = createStyles((theme) => ({
360
360
  }
361
361
  }));
362
362
  const GettingStarted = (props) => {
363
- const { classes } = useStyles$q();
363
+ const { classes } = useStyles$r();
364
364
  return /* @__PURE__ */ React.createElement(
365
365
  Modal,
366
366
  {
@@ -374,7 +374,7 @@ const GettingStarted = (props) => {
374
374
  );
375
375
  };
376
376
 
377
- const useStyles$p = createStyles((theme) => ({
377
+ const useStyles$q = createStyles((theme) => ({
378
378
  title: {
379
379
  fontSize: 34,
380
380
  fontWeight: 900,
@@ -415,7 +415,7 @@ const useStyles$p = createStyles((theme) => ({
415
415
  }
416
416
  }));
417
417
  const SwitchAccount = (props) => {
418
- const { classes, theme } = useStyles$p();
418
+ const { classes, theme } = useStyles$q();
419
419
  const options = props.accounts.map((a) => {
420
420
  return /* @__PURE__ */ React.createElement(UnstyledButton, { onClick: () => props.onClick && props.onClick(a.accountId), key: a.accountId, p: theme.spacing.md }, /* @__PURE__ */ React.createElement(Card, { withBorder: true, shadow: "md", radius: "md", className: classes.card, p: "xl" }, a.isAdmin && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(IconBatteryEco, { size: 50, stroke: 2, color: theme.fn.primaryColor() })), a.isGroupAdmin && !a.isAdmin && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(IconBooks, { size: 50, stroke: 2, color: theme.fn.primaryColor() })), !a.isAdmin && !a.isGroupAdmin && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(IconBackpack, { size: 50, stroke: 2, color: theme.fn.primaryColor() })), /* @__PURE__ */ React.createElement(Text, { size: "lg", weight: 500, className: classes.cardTitle, mt: "md" }, a.name), /* @__PURE__ */ React.createElement(Text, { size: "sm", color: "dimmed", mt: "sm" }, a.isAdmin ? "Admin" : a.isGroupAdmin ? "Educator" : "Student")));
421
421
  });
@@ -432,7 +432,7 @@ const SwitchAccount = (props) => {
432
432
  );
433
433
  };
434
434
 
435
- const useStyles$o = createStyles((theme) => ({
435
+ const useStyles$p = createStyles((theme) => ({
436
436
  root: {
437
437
  display: "flex",
438
438
  backgroundImage: `linear-gradient(-60deg, ${theme.colors[theme.primaryColor][4]} 0%, ${theme.colors[theme.primaryColor][7]} 100%)`,
@@ -476,7 +476,7 @@ const useStyles$o = createStyles((theme) => ({
476
476
  }
477
477
  }));
478
478
  const StatsGroup = ({ data, footer }) => {
479
- const { classes } = useStyles$o();
479
+ const { classes } = useStyles$p();
480
480
  const stats = data.map((stat) => {
481
481
  const value = (() => {
482
482
  if (stat.unit === "%") {
@@ -496,7 +496,7 @@ const Tabs = (props) => {
496
496
  return /* @__PURE__ */ React.createElement(Tabs$1, { value: props.value, onTabChange: props.onChange }, /* @__PURE__ */ React.createElement(Tabs$1.List, null, tabs));
497
497
  };
498
498
 
499
- const useStyles$n = createStyles((theme) => ({
499
+ const useStyles$o = createStyles((theme) => ({
500
500
  button: {
501
501
  borderTopRightRadius: 0,
502
502
  borderBottomRightRadius: 0,
@@ -511,7 +511,7 @@ const useStyles$n = createStyles((theme) => ({
511
511
  }
512
512
  }));
513
513
  const SplitButton$4 = (props) => {
514
- const { classes, theme } = useStyles$n();
514
+ const { classes, theme } = useStyles$o();
515
515
  theme.colors[theme.primaryColor][theme.colorScheme === "dark" ? 5 : 6];
516
516
  return /* @__PURE__ */ React.createElement(Stack$4, { spacing: "sm" }, /* @__PURE__ */ React.createElement(
517
517
  Button,
@@ -539,7 +539,7 @@ const SplitButton$4 = (props) => {
539
539
  ));
540
540
  };
541
541
 
542
- const useStyles$m = createStyles((theme) => ({
542
+ const useStyles$n = createStyles((theme) => ({
543
543
  wrapper: {
544
544
  display: "flex",
545
545
  alignItems: "center",
@@ -590,7 +590,7 @@ const useStyles$m = createStyles((theme) => ({
590
590
  }
591
591
  }));
592
592
  const PlaceholderBanner = (props) => {
593
- const { classes } = useStyles$m();
593
+ const { classes } = useStyles$n();
594
594
  const title = props.title || "Nothing to display";
595
595
  const description = props.description || "We don't have anything to show you here just yet. Add data, check back later, or adjust your search.";
596
596
  return /* @__PURE__ */ React.createElement("div", { className: classes.wrapper }, /* @__PURE__ */ React.createElement("div", { className: classes.body }, /* @__PURE__ */ React.createElement(Title, { className: classes.title }, props.loading ? "Loading..." : title), /* @__PURE__ */ React.createElement(Text, { size: "sm", color: "dimmed" }, props.loading ? "Hold on, we're loading your data." : description)), /* @__PURE__ */ React.createElement(Image, { src: `https://cdn.localcivics.io/illustrations/${props.icon}.svg`, className: classes.image }));
@@ -662,7 +662,7 @@ function Table$i(props) {
662
662
  return /* @__PURE__ */ React.createElement(ScrollArea.Autosize, { maxHeight: 600 }, /* @__PURE__ */ React.createElement(Table$k, { verticalSpacing: "sm", sx: { minWidth: 700 }, highlightOnHover: true, striped: true }, /* @__PURE__ */ React.createElement("thead", null, /* @__PURE__ */ React.createElement("tr", null, /* @__PURE__ */ React.createElement("th", null, "Lesson Name"), /* @__PURE__ */ React.createElement("th", null, "Lesson Completion"))), /* @__PURE__ */ React.createElement("tbody", null, rows)));
663
663
  }
664
664
 
665
- const useStyles$l = createStyles((theme) => ({
665
+ const useStyles$m = createStyles((theme) => ({
666
666
  title: {
667
667
  fontSize: 34,
668
668
  fontWeight: 900,
@@ -675,7 +675,7 @@ const useStyles$l = createStyles((theme) => ({
675
675
  }
676
676
  }));
677
677
  const Badge = (props) => {
678
- const { classes } = useStyles$l();
678
+ const { classes } = useStyles$m();
679
679
  const [tab, setTab] = useState("lessons");
680
680
  const numberOfStudents = props.students.length;
681
681
  const numberOfBadges = numberOfStudents > 0 ? props.students.filter((u) => u.isComplete).length : 0;
@@ -773,7 +773,7 @@ function Table$h(props) {
773
773
  return /* @__PURE__ */ React.createElement(ScrollArea.Autosize, { maxHeight: 600 }, /* @__PURE__ */ React.createElement(Table$k, { horizontalSpacing: 0, verticalSpacing: 0, sx: { minWidth: 700 } }, /* @__PURE__ */ React.createElement("tbody", null, rows)));
774
774
  }
775
775
 
776
- const useStyles$k = createStyles((theme) => ({
776
+ const useStyles$l = createStyles((theme) => ({
777
777
  title: {
778
778
  fontSize: 34,
779
779
  fontWeight: 900,
@@ -786,7 +786,7 @@ const useStyles$k = createStyles((theme) => ({
786
786
  }
787
787
  }));
788
788
  const Badges = (props) => {
789
- const { classes } = useStyles$k();
789
+ const { classes } = useStyles$l();
790
790
  return /* @__PURE__ */ React.createElement(Container, { size: "lg", py: "xl" }, /* @__PURE__ */ React.createElement(Stack$4, { spacing: "md" }, /* @__PURE__ */ React.createElement(Grid, null, /* @__PURE__ */ React.createElement(Grid.Col, { sm: "auto" }, /* @__PURE__ */ React.createElement(Badge$1, { variant: "filled", size: "lg" }, "Badges"), /* @__PURE__ */ React.createElement(Title, { order: 2, className: classes.title, mt: "md" }, "Badges and micro-credentials"), /* @__PURE__ */ React.createElement(Text, { color: "dimmed", className: classes.description, mt: "sm" }, "Key milestones that reflect skill development, micro-credentials, or academic progress"))), /* @__PURE__ */ React.createElement(
791
791
  Autocomplete,
792
792
  {
@@ -1067,7 +1067,7 @@ var __spreadValues$7 = (a, b) => {
1067
1067
  return a;
1068
1068
  };
1069
1069
  var __spreadProps$4 = (a, b) => __defProps$4(a, __getOwnPropDescs$4(b));
1070
- const useStyles$j = createStyles((theme) => ({
1070
+ const useStyles$k = createStyles((theme) => ({
1071
1071
  title: {
1072
1072
  fontSize: 34,
1073
1073
  fontWeight: 900,
@@ -1100,7 +1100,7 @@ const useStyles$j = createStyles((theme) => ({
1100
1100
  }
1101
1101
  }));
1102
1102
  const Class = (props) => {
1103
- const { classes } = useStyles$j();
1103
+ const { classes } = useStyles$k();
1104
1104
  const form = useForm({
1105
1105
  initialValues: {
1106
1106
  classId: "",
@@ -1201,7 +1201,7 @@ const Class = (props) => {
1201
1201
  ))))));
1202
1202
  };
1203
1203
  const DropzoneButton$1 = (props) => {
1204
- const { classes, theme } = useStyles$j();
1204
+ const { classes, theme } = useStyles$k();
1205
1205
  const openRef = React.useRef(null);
1206
1206
  const [loading, setLoading] = React.useState(false);
1207
1207
  const onDrop = React.useCallback((acceptedFiles) => {
@@ -1247,7 +1247,63 @@ const DropzoneButton$1 = (props) => {
1247
1247
  } }, "Select file"));
1248
1248
  };
1249
1249
 
1250
+ function useSortableData(items, config = { key: "", direction: null }) {
1251
+ const [sortConfig, setSortConfig] = useState(config);
1252
+ const sortedItems = useMemo(() => {
1253
+ let sortableItems = [...items];
1254
+ if (sortConfig.direction !== null) {
1255
+ sortableItems.sort((a, b) => {
1256
+ const aValue = a[sortConfig.key];
1257
+ const bValue = b[sortConfig.key];
1258
+ if (aValue === bValue)
1259
+ return 0;
1260
+ if (aValue === null || aValue === void 0)
1261
+ return 1;
1262
+ if (bValue === null || bValue === void 0)
1263
+ return -1;
1264
+ if (typeof aValue === "number" && typeof bValue === "number") {
1265
+ return sortConfig.direction === "asc" ? aValue - bValue : bValue - aValue;
1266
+ }
1267
+ if (typeof aValue === "boolean" && typeof bValue === "boolean") {
1268
+ return sortConfig.direction === "asc" ? aValue === bValue ? 0 : aValue ? 1 : -1 : aValue === bValue ? 0 : aValue ? -1 : 1;
1269
+ }
1270
+ if (aValue instanceof Date && bValue instanceof Date) {
1271
+ return sortConfig.direction === "asc" ? aValue.getTime() - bValue.getTime() : bValue.getTime() - aValue.getTime();
1272
+ }
1273
+ return sortConfig.direction === "asc" ? String(aValue).localeCompare(String(bValue)) : String(bValue).localeCompare(String(aValue));
1274
+ });
1275
+ }
1276
+ return sortableItems;
1277
+ }, [items, sortConfig]);
1278
+ const requestSort = (key) => {
1279
+ let direction = "asc";
1280
+ if (sortConfig.key === key && sortConfig.direction === "asc") {
1281
+ direction = "desc";
1282
+ } else if (sortConfig.key === key && sortConfig.direction === "desc") {
1283
+ direction = null;
1284
+ }
1285
+ setSortConfig({ key, direction });
1286
+ };
1287
+ return { items: sortedItems, requestSort, sortConfig };
1288
+ }
1289
+
1290
+ const useStyles$j = createStyles((theme) => ({
1291
+ th: { padding: "0 !important" },
1292
+ control: {
1293
+ width: "100%",
1294
+ padding: `${theme.spacing.xs}px ${theme.spacing.md}px`,
1295
+ "&:hover": {
1296
+ backgroundColor: theme.colorScheme === "dark" ? theme.colors.dark[6] : theme.colors.gray[0]
1297
+ }
1298
+ }
1299
+ }));
1300
+ function Th({ children, reversed, sorted, onSort }) {
1301
+ const { classes } = useStyles$j();
1302
+ const Icon = sorted ? reversed ? IconChevronUp : IconChevronDown : IconSelector;
1303
+ return /* @__PURE__ */ React.createElement("th", { className: classes.th }, /* @__PURE__ */ React.createElement(UnstyledButton, { onClick: onSort, className: classes.control }, /* @__PURE__ */ React.createElement(Group, { position: "apart" }, /* @__PURE__ */ React.createElement(Text, { weight: 500, size: "sm" }, children), /* @__PURE__ */ React.createElement(Center, null, /* @__PURE__ */ React.createElement(Icon, { size: 14, stroke: 1.5 })))));
1304
+ }
1250
1305
  function Table$a(props) {
1306
+ const { items: sortedItems, requestSort, sortConfig } = useSortableData(props.items);
1251
1307
  if (props.items.length === 0) {
1252
1308
  return /* @__PURE__ */ React.createElement(
1253
1309
  PlaceholderBanner,
@@ -1267,8 +1323,32 @@ function Table$a(props) {
1267
1323
  confirmProps: { color: "red" },
1268
1324
  onConfirm: () => props.onDeleteClass(group)
1269
1325
  });
1270
- const rows = props.items.map((row) => /* @__PURE__ */ React.createElement("tr", { key: row.classId }, /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement(UnstyledButton, { component: Link, to: row.href }, /* @__PURE__ */ React.createElement(Text, { size: 14 }, row.name))), /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement(Text, { size: 14 }, row.description)), /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement(Text, { size: 14 }, row.numberOfStudents || 0)), /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement(Group, { noWrap: true, spacing: 0, position: "right" }, /* @__PURE__ */ React.createElement(ActionIcon, { color: "red" }, /* @__PURE__ */ React.createElement(IconTrash, { onClick: () => openDeleteModal(row), size: 16, stroke: 1.5 }))))));
1271
- return /* @__PURE__ */ React.createElement(ScrollArea.Autosize, { maxHeight: 600 }, /* @__PURE__ */ React.createElement(Table$k, { verticalSpacing: 20, sx: { minWidth: 700 }, highlightOnHover: true, striped: true }, /* @__PURE__ */ React.createElement("thead", null, /* @__PURE__ */ React.createElement("tr", null, /* @__PURE__ */ React.createElement("th", null, "Class Name"), /* @__PURE__ */ React.createElement("th", null, "Description"), /* @__PURE__ */ React.createElement("th", null, "# of Students"), /* @__PURE__ */ React.createElement("th", null))), /* @__PURE__ */ React.createElement("tbody", null, rows)));
1326
+ const rows = sortedItems.map((row) => /* @__PURE__ */ React.createElement("tr", { key: row.classId }, /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement(UnstyledButton, { component: Link, to: row.href }, /* @__PURE__ */ React.createElement(Text, { size: 14 }, row.name))), /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement(Text, { size: 14 }, row.description)), /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement(Text, { size: 14 }, row.numberOfStudents || 0)), /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement(Group, { noWrap: true, spacing: 0, position: "right" }, /* @__PURE__ */ React.createElement(ActionIcon, { color: "red" }, /* @__PURE__ */ React.createElement(IconTrash, { onClick: () => openDeleteModal(row), size: 16, stroke: 1.5 }))))));
1327
+ return /* @__PURE__ */ React.createElement(ScrollArea.Autosize, { maxHeight: 600 }, /* @__PURE__ */ React.createElement(Table$k, { verticalSpacing: 20, sx: { minWidth: 700 }, highlightOnHover: true, striped: true }, /* @__PURE__ */ React.createElement("thead", null, /* @__PURE__ */ React.createElement("tr", null, /* @__PURE__ */ React.createElement(
1328
+ Th,
1329
+ {
1330
+ sorted: sortConfig.key === "name",
1331
+ reversed: sortConfig.direction === "desc",
1332
+ onSort: () => requestSort("name")
1333
+ },
1334
+ "Class Name"
1335
+ ), /* @__PURE__ */ React.createElement(
1336
+ Th,
1337
+ {
1338
+ sorted: sortConfig.key === "description",
1339
+ reversed: sortConfig.direction === "desc",
1340
+ onSort: () => requestSort("description")
1341
+ },
1342
+ "Description"
1343
+ ), /* @__PURE__ */ React.createElement(
1344
+ Th,
1345
+ {
1346
+ sorted: sortConfig.key === "numberOfStudents",
1347
+ reversed: sortConfig.direction === "desc",
1348
+ onSort: () => requestSort("numberOfStudents")
1349
+ },
1350
+ "# of Students"
1351
+ ), /* @__PURE__ */ React.createElement("th", null))), /* @__PURE__ */ React.createElement("tbody", null, rows)));
1272
1352
  }
1273
1353
 
1274
1354
  var __defProp$6 = Object.defineProperty;