diff --git a/frontend/content/compiled-locales/en.json b/frontend/content/compiled-locales/en.json index 481bf7ecc..46ad482a1 100644 --- a/frontend/content/compiled-locales/en.json +++ b/frontend/content/compiled-locales/en.json @@ -276,7 +276,7 @@ "ClassNavigation.sessionsTab": [ { "type": 0, - "value": "Sessions" + "value": "Lessons" } ], "ClassNavigation.studentsTab": [ @@ -1080,19 +1080,19 @@ "SessionList.columns.actions": [ { "type": 0, - "value": "Actions" + "value": "Quick Action" } ], "SessionList.columns.finishedAt": [ { "type": 0, - "value": "Finished at" + "value": "End Date" } ], "SessionList.columns.startedAt": [ { "type": 0, - "value": "Started at" + "value": "Start Date" } ], "SessionList.columns.tags": [ @@ -1110,7 +1110,7 @@ "SessionList.copySessionLink": [ { "type": 0, - "value": "Copy Session Link" + "value": "Share" } ], "SessionList.deleteConfirmation.body": [ @@ -1985,4 +1985,4 @@ "value": "${path} must be a valid UUID" } ] -} \ No newline at end of file +} diff --git a/frontend/content/locales/en.json b/frontend/content/locales/en.json index bf75781ef..757a93c27 100644 --- a/frontend/content/locales/en.json +++ b/frontend/content/locales/en.json @@ -135,7 +135,7 @@ "message": "Class Details" }, "ClassNavigation.sessionsTab": { - "message": "Sessions" + "message": "Lessons" }, "ClassNavigation.studentsTab": { "message": "Students" @@ -508,13 +508,13 @@ "message": "Analysis - {title}" }, "SessionList.columns.actions": { - "message": "Actions" + "message": "Quick Action" }, "SessionList.columns.finishedAt": { - "message": "Finished at" + "message": "End Date" }, "SessionList.columns.startedAt": { - "message": "Started at" + "message": "Start Date" }, "SessionList.columns.tags": { "message": "Tags" @@ -523,7 +523,7 @@ "message": "Title" }, "SessionList.copySessionLink": { - "message": "Copy Session Link" + "message": "Share" }, "SessionList.deleteConfirmation.body": { "message": "Are you sure you want to delete this session?" diff --git a/frontend/package.json b/frontend/package.json index 3697d4f65..762f11d19 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,6 +16,7 @@ "i18n:compile": "formatjs compile-folder --ast --format crowdin content/locales content/compiled-locales", "i18n:fill": "node ./scripts/fill-locales.mjs", "i18n:update": "yarn i18n:extract && yarn i18n:fill && yarn i18n:compile", + "chakra:update": "npx @chakra-ui/cli typegen ./src/components/ui/Theme.ts", "test": "yarn test:jest && yarn test:playwright", "test:cov:enable": "shx cp .babelrc.disabled .babelrc", "test:cov:disable": "shx rm .babelrc", diff --git a/frontend/src/api/collimator/hooks/sessions/useAllClassSessions.ts b/frontend/src/api/collimator/hooks/sessions/useAllClassSessions.ts index 76a9f5bf3..e7e39f664 100644 --- a/frontend/src/api/collimator/hooks/sessions/useAllClassSessions.ts +++ b/frontend/src/api/collimator/hooks/sessions/useAllClassSessions.ts @@ -1,11 +1,5 @@ import useSWR from "swr"; -import { LazyTableResult, LazyTableState } from "@/components/DataTable"; -import { - ApiResponse, - fromDtos, - getSwrParamererizedKey, - transformToLazyTableResult, -} from "../helpers"; +import { ApiResponse, fromDtos, getSwrParamererizedKey } from "../helpers"; import { getSessionsControllerFindAllV0Url, sessionsControllerFindAllV0, @@ -38,21 +32,3 @@ export const useAllClassSessions = ( () => fetchByClassIdAndTransform(authOptions, classId, params), ); }; - -export const useAllClassSessionsLazyTable = ( - classId: number, - _state: LazyTableState, -): ApiResponse, Error> => { - const authOptions = useAuthenticationOptions(); - - return useSWR( - getSwrParamererizedKey( - (_params?: undefined) => getSessionsControllerFindAllV0Url(classId), - undefined, - ), - () => - fetchByClassIdAndTransform(authOptions, classId).then( - transformToLazyTableResult, - ), - ); -}; diff --git a/frontend/src/components/Breadcrumbs.tsx b/frontend/src/components/Breadcrumbs.tsx index b747d3d36..85130fc59 100644 --- a/frontend/src/components/Breadcrumbs.tsx +++ b/frontend/src/components/Breadcrumbs.tsx @@ -1,6 +1,6 @@ import { Breadcrumb, HStack, Icon } from "@chakra-ui/react"; import { FormattedMessage } from "react-intl"; -import { Children, Fragment, isValidElement } from "react"; +import React from "react"; import { LuHouse } from "react-icons/lu"; import BreadcrumbItem from "./BreadcrumbItem"; @@ -16,21 +16,7 @@ const Breadcrumbs = ({ children }: BreadcrumbsProps) => ( - - {Children.map(children, (child, index) => { - if (!isValidElement(child)) { - return null; - } - - const childKey = child.key ?? `breadcrumb-${index}`; - - return ( - - - {child} - - ); - })} + {children} diff --git a/frontend/src/components/Button.tsx b/frontend/src/components/Button.tsx index 7ae8e7472..1ef17a6a3 100644 --- a/frontend/src/components/Button.tsx +++ b/frontend/src/components/Button.tsx @@ -13,21 +13,11 @@ import { useCallback, useState, useRef, - useMemo, MouseEvent as MouseEventReact, - DetailedHTMLProps, - ButtonHTMLAttributes, + ComponentProps, } from "react"; -import { isNonNull } from "@/utilities/is-non-null"; -import { ButtonVariant } from "@/components/ui/recipes/buttons/Button.recipe"; -export type ButtonProps = Omit< - DetailedHTMLProps, HTMLButtonElement>, - "variant" -> & { - variant?: ButtonVariant; - active?: boolean; -}; +export type ButtonProps = ComponentProps; const ButtonContent = chakra(HStack, { base: { @@ -38,8 +28,6 @@ const ButtonContent = chakra(HStack, { const Button = ({ onClick: onClickFn, children, - variant, - active, ...buttonProps }: ButtonProps) => { const [isLoading, setIsLoading] = useState(false); @@ -84,22 +72,8 @@ const Button = ({ [onClickFn, showPromiseResult], ); - const className = useMemo( - () => - [active ? "active" : null, buttonProps.className ?? null] - .filter(isNonNull) - .join(" "), - [buttonProps.className, active], - ); - return ( - + {children} {isLoading && } @@ -118,5 +92,4 @@ const Button = ({ ); }; -export { ButtonVariant }; export default Button; diff --git a/frontend/src/components/ChakraDataTable.tsx b/frontend/src/components/ChakraDataTable.tsx index 9bdfc954d..92860ea89 100644 --- a/frontend/src/components/ChakraDataTable.tsx +++ b/frontend/src/components/ChakraDataTable.tsx @@ -81,19 +81,15 @@ interface ChakraDataTableProps { data: T[]; columns: ColumnDef[]; isLoading?: boolean; - onRowClick?: (row: T) => void; + onRowClick?: ( + row: T, + e: React.MouseEvent, + ) => void; features?: DataTableFeatures; variant?: "outline" | "line"; includeSearchBar?: boolean; } -const TableWrapper = chakra("div", { - base: { - marginBottom: "lg", - marginTop: "4xl", - }, -}); - const InputWrapper = chakra("div", { base: { marginBottom: "lg", @@ -101,42 +97,6 @@ const InputWrapper = chakra("div", { }, }); -const ColumnHeader = chakra(Table.ColumnHeader, { - base: { - fontWeight: "semiBold", - color: "fgTertiary", - _hover: { - cursor: "pointer", - }, - "&:last-child": { - width: "auto", - textAlign: "right", - }, - }, -}); - -const TableRow = chakra(Table.Row, { - base: { - _hover: { - cursor: "pointer", - }, - }, -}); - -const TableHeader = chakra(Table.Header, { - base: { - backgroundColor: "gray.200", - }, -}); - -const TableRoot = chakra(Table.Root, { - base: { - width: "100%", - tableLayout: "fixed", - fontSize: "lg", - }, -}); - const HeaderContent = chakra("div", { base: { display: "flex", @@ -157,25 +117,7 @@ const TableContainer = chakra("div", { base: { display: "flex", flexDirection: "column", - gap: "md", - }, -}); - -const TableCell = chakra(Table.Cell, { - base: { - borderBottomWidth: "thin !important", - borderBottomStyle: "solid", - borderBottomColor: "gray.600", - - "&:first-child": { - fontWeight: "semiBold", - }, - - "&:last-child": { - width: "auto", - textAlign: "right", - whiteSpace: "nowrap", - }, + gap: "sm", }, }); @@ -507,92 +449,94 @@ export const ChakraDataTable = ({ }; return ( - - - {features?.columnFiltering && includeSearchBar && ( - - - - table.getColumn(filterColumn)?.setFilterValue(e.target.value) - } - placeholder={intl.formatMessage(messages.filterByPlaceholder)} - variety={InputVariety.Search} - /> - - - {features.columnFiltering.columns.length > 1 && ( - - {features.columnFiltering.columns.map((col) => ( - setFilterColumn(col.accessorKey)} - > - {col.label} - - ))} - - )} - - )} - - - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - -
- {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext(), - )} -
- {features?.sorting && header.column.getCanSort() && ( - - )} -
-
- ))} -
- ))} -
- - - {table.getRowModel().rows.map((row) => ( - onRowClick?.(row.original)}> - {row.getVisibleCells().map((cell) => ( - {cellWrapper({ cell })} - ))} - - ))} - -
- - {features?.pagination && table.getPageCount() > 1 && ( - table.setPageIndex(pageIndex)} - /> - )} -
-
+ + {features?.columnFiltering && includeSearchBar && ( + + + + table.getColumn(filterColumn)?.setFilterValue(e.target.value) + } + placeholder={intl.formatMessage(messages.filterByPlaceholder)} + variety={InputVariety.Search} + /> + + + {features.columnFiltering.columns.length > 1 && ( + + {features.columnFiltering.columns.map((col) => ( + setFilterColumn(col.accessorKey)} + > + {col.label} + + ))} + + )} + + )} + + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + +
+ {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} +
+ {features?.sorting && header.column.getCanSort() && ( + + )} +
+
+ ))} +
+ ))} +
+ + + {table.getRowModel().rows.map((row) => ( + onRowClick?.(row.original, e)} + > + {row.getVisibleCells().map((cell) => ( + {cellWrapper({ cell })} + ))} + + ))} + +
+ + {features?.pagination && table.getPageCount() > 1 && ( + table.setPageIndex(pageIndex)} + /> + )} +
); }; diff --git a/frontend/src/components/CrtNavigation.tsx b/frontend/src/components/CrtNavigation.tsx index d5778656c..990f6ee5b 100644 --- a/frontend/src/components/CrtNavigation.tsx +++ b/frontend/src/components/CrtNavigation.tsx @@ -5,7 +5,10 @@ import { LuListTodo, LuUsers, LuBook, + LuBookMarked, } from "react-icons/lu"; +import { Breadcrumb } from "@chakra-ui/react"; +import { Fragment } from "react"; import { ExistingClass } from "@/api/collimator/models/classes/existing-class"; import { ExistingUser } from "@/api/collimator/models/users/existing-user"; import { ExistingClassExtended } from "@/api/collimator/models/classes/existing-class-extended"; @@ -87,35 +90,48 @@ const CrtNavigation = ({ {breadcrumb && ( <> {user && ( - }> - {user.name ?? user.email} - + <> + + }> + {user.name ?? user.email} + + )} {klass && ( - } - > - {klass.name} - + <> + + } + > + {klass.name} + + )} {task && ( - } - > - {task.title} - + <> + + } + > + {task.title} + + )} {lessonId && ( - }> - {lessonName} - + <> + + }> + {lessonName} + + )} {breadcrumbItems?.map((item, index) => ( - - {item.children} - + + + {item.children} + ))} )} diff --git a/frontend/src/components/Pagination.tsx b/frontend/src/components/Pagination.tsx index 4e620e0df..880ff11fc 100644 --- a/frontend/src/components/Pagination.tsx +++ b/frontend/src/components/Pagination.tsx @@ -1,4 +1,4 @@ -import { ButtonGroup, IconButton, Pagination, chakra } from "@chakra-ui/react"; +import { ButtonGroup, Pagination, chakra, IconButton } from "@chakra-ui/react"; import { LuChevronLeft, LuChevronRight } from "react-icons/lu"; import { useIntl, defineMessages } from "react-intl"; @@ -19,48 +19,6 @@ const PaginationWrapper = chakra("div", { }, }); -const PageButton = chakra(IconButton, { - base: { - borderRadius: "sm", - _hover: { - backgroundColor: "gray.300", - }, - }, - variants: { - state: { - inactive: { - backgroundColor: "gray.200", - color: "black", - _hover: { - backgroundColor: "black", - color: "white", - }, - }, - active: { - backgroundColor: "black", - color: "white", - _hover: { - backgroundColor: "gray.600", - }, - }, - }, - }, - defaultVariants: { - state: "inactive", - }, -}); - -const NavigationButton = chakra(IconButton, { - base: { - backgroundColor: "transparent", - color: "gray.600", - _hover: { - color: "black", - backgroundColor: "transparent", - }, - }, -}); - const messages = defineMessages({ paginationPrevious: { id: "datatable.pagination.previous", @@ -94,32 +52,31 @@ export const DataTablePagination = ({ > - - + { - const isCurrentPage = page.value === currentPage; - return ( - - {page.value} - - ); - }} + render={(page) => ( + + {page.value} + + )} /> - - + diff --git a/frontend/src/components/TabNavigation.tsx b/frontend/src/components/TabNavigation.tsx index 163efc62b..e53929744 100644 --- a/frontend/src/components/TabNavigation.tsx +++ b/frontend/src/components/TabNavigation.tsx @@ -1,8 +1,8 @@ import Link from "next/link"; import { useRouter } from "next/router"; -import { Tabs } from "@chakra-ui/react"; +import { Breadcrumb, Icon, Tabs } from "@chakra-ui/react"; import { IntlShape, useIntl } from "react-intl"; -import { ReactNode, useContext } from "react"; +import { Fragment, ReactNode, useContext } from "react"; import styled from "@emotion/styled"; import { AuthenticationContext, @@ -70,15 +70,14 @@ const TabNavigation = ({ const activeItems = navigationTabs.filter((tab) => tab.isActive); if (breadcrumb) { - return ( - <> - {activeItems.map((item) => ( - - {item.title(intl, tabTitleArguments as T)} - - ))} - - ); + return activeItems.map((item) => ( + + + + {item.title(intl, tabTitleArguments as T)} + + + )); } const activeValue = activeItems[0]?.url || ""; @@ -94,6 +93,7 @@ const TabNavigation = ({ {navigationTabs.map((tab) => ( + {tab.icon && {tab.icon}} {tab.title(intl, tabTitleArguments as T)} diff --git a/frontend/src/components/class/ClassForm.tsx b/frontend/src/components/class/ClassForm.tsx index 93c83e1cf..998cc78d7 100644 --- a/frontend/src/components/class/ClassForm.tsx +++ b/frontend/src/components/class/ClassForm.tsx @@ -11,7 +11,7 @@ import { import { useYupSchema } from "@/hooks/useYupSchema"; import { useYupResolver } from "@/hooks/useYupResolver"; import { useAllUsers } from "@/api/collimator/hooks/users/useAllUsers"; -import Input, { InputVariant } from "../form/Input"; +import Input from "../form/Input"; import SwrContent from "../SwrContent"; import FormContainer from "../form/FormContainer"; import FormGrid from "../form/FormGrid"; @@ -107,7 +107,7 @@ const ClassForm = ({ label={messages.name} {...register("name")} data-testid="name" - variant={InputVariant.inputForm} + variant="inputForm" invalid={!!errors.name} errorText={errors.name?.message} /> diff --git a/frontend/src/components/class/ClassList.tsx b/frontend/src/components/class/ClassList.tsx index fe867d12c..8e753fdd9 100644 --- a/frontend/src/components/class/ClassList.tsx +++ b/frontend/src/components/class/ClassList.tsx @@ -2,18 +2,20 @@ import { defineMessages, useIntl } from "react-intl"; import { useRouter } from "next/router"; import { ColumnDef } from "@tanstack/react-table"; import { MdAdd } from "react-icons/md"; -import { Icon, HStack, chakra } from "@chakra-ui/react"; +import { Icon, HStack, chakra, Text } from "@chakra-ui/react"; import { LuChevronRight } from "react-icons/lu"; import { useAllClasses } from "@/api/collimator/hooks/classes/useAllClasses"; import { ExistingClassWithTeacher } from "@/api/collimator/models/classes/existing-class-with-teacher"; import { ColumnType } from "@/types/tanstack-types"; +import { isClickOnRow } from "@/utilities/table"; import SwrContent from "../SwrContent"; import { ChakraDataTable } from "../ChakraDataTable"; -import Button, { ButtonVariant } from "../Button"; +import Button from "../Button"; const ClassListWrapper = chakra("div", { base: { - marginTop: "2xl", + marginTop: "md", + marginBottom: "md", }, }); @@ -59,9 +61,14 @@ const ClassList = () => { accessorKey: "name", header: intl.formatMessage(messages.nameColumn), cell: (info) => ( - + {info.row.original.name} - + ), meta: { columnType: ColumnType.text, @@ -91,7 +98,7 @@ const ClassList = () => { router.push(`/class/${info.row.original.id}/detail`); }} data-testid={`class-${info.row.original.id}-details-button`} - variant={ButtonVariant.detail} + variant="detail" > @@ -112,8 +119,10 @@ const ClassList = () => { data={data} columns={columns} isLoading={isLoading} - onRowClick={(row) => { - router.push(`/class/${row.id}/detail`); + onRowClick={(row, e) => { + if (isClickOnRow(e)) { + router.push(`/class/${row.id}/detail`); + } }} features={{ sorting: true, @@ -133,7 +142,7 @@ const ClassList = () => { )} - - - - } - > - { - e.stopPropagation(); - setSessionIdToDelete(rowData.id); - setShowDeleteConfirmationModal(true); - }} - testId={`session-${rowData.id}-delete-button`} - > - {intl.formatMessage(TableMessages.delete)} - - {klass && - "userId" in authenticationContext && - klass.teacher.id === authenticationContext.userId && ( - { - const fingerprint = - await authenticationContext.keyPair.getPublicKeyFingerprint(); + return canGetSessionLink ? ( + + ) : ( + + ); + }, + [intl, authenticationContext, klass], + ); - navigator.clipboard.writeText( - `${window.location.origin}/class/${classId}/session/${rowData.id}/join?key=${fingerprint}`, - ); - }} - testId={`session-${rowData.id}-copy-session-link-button`} - > - {intl.formatMessage(messages.copySessionLink)} - - )} - - - - ), - [classId, intl, router, authenticationContext, klass], + const sharingTypeTemplate = useCallback( + (rowData: ExistingSession) => + rowData.isAnonymous + ? intl.formatMessage(messages.sharingTypeColumnAnonymous) + : intl.formatMessage(messages.sharingTypeColumnPrivate), + [intl], ); + const columns: ColumnDef[] = [ + { + accessorKey: "id", + header: intl.formatMessage(messages.idColumn), + enableSorting: false, + size: 32, + }, + { + accessorKey: "title", + header: intl.formatMessage(messages.titleColumn), + cell: (info) => ( + + {info.row.original.title} + + ), + meta: { + columnType: ColumnType.text, + }, + }, + { + accessorKey: "startedAt", + header: intl.formatMessage(messages.startedAtColumn), + enableSorting: false, + cell: (info) => startedAtTemplate(info.row.original), + meta: { + columnType: ColumnType.text, + }, + }, + { + accessorKey: "isAnonymous", + header: intl.formatMessage(messages.sharingTypeColumn), + enableSorting: false, + cell: (info) => sharingTypeTemplate(info.row.original), + meta: { + columnType: ColumnType.text, + }, + }, + { + id: "actions", + header: intl.formatMessage(messages.actionsColumn), + enableSorting: false, + cell: (info) => actionsTemplate(info.row.original), + meta: { + columnType: ColumnType.icon, + }, + }, + { + id: "details", + header: "", + enableSorting: false, + cell: (info) => ( + + ), + meta: { + columnType: ColumnType.icon, + }, + }, + ]; + return ( { errors={[error, klassError]} > {([data]) => ( - { - if (isClickOnRow(e)) - router.push( - `/class/${classId}/session/${(e.data as ExistingSession).id}/progress`, - ); - }} - > - - - - - - router.push(`/class/${classId}/session/create`) - } - data-testid="session-create-button" - > - - - - - } - /> + { + if (isClickOnRow(e)) { + router.push(`/class/${classId}/session/${row.id}/progress`); } - /> - + }} + features={{ + sorting: true, + columnFiltering: { + columns: [ + { + accessorKey: "title", + label: intl.formatMessage(messages.titleColumn), + }, + ], + }, + pagination: { + pageSize: 4, + }, + }} + /> )} - deleteSession(classId, sessionIdToDelete) - : undefined - } - isDangerous - messages={{ - title: messages.deleteConfirmationTitle, - body: messages.deleteConfirmationBody, - confirmButton: messages.deleteConfirmationConfirm, - }} - /> + ); }; diff --git a/frontend/src/components/task/TaskTable.tsx b/frontend/src/components/task/TaskTable.tsx index ce5363985..5d42bf054 100644 --- a/frontend/src/components/task/TaskTable.tsx +++ b/frontend/src/components/task/TaskTable.tsx @@ -5,7 +5,7 @@ import { useRouter } from "next/router"; import { ColumnDef } from "@tanstack/react-table"; import { MdAdd } from "react-icons/md"; import { FaRegTrashAlt } from "react-icons/fa"; -import { Icon, HStack } from "@chakra-ui/react"; +import { Icon, HStack, Text } from "@chakra-ui/react"; import { LuChevronRight } from "react-icons/lu"; import { ColumnType } from "@/types/tanstack-types"; import { useAllTasks } from "@/api/collimator/hooks/tasks/useAllTasks"; @@ -15,7 +15,7 @@ import { capitalizeString } from "@/utilities/strings"; import SwrContent from "../SwrContent"; import ConfirmationModal from "../modals/ConfirmationModal"; import { ChakraDataTable } from "../ChakraDataTable"; -import Button, { ButtonVariant } from "../Button"; +import Button from "../Button"; const TaskTableWrapper = styled.div` margin: 1rem 0; @@ -89,9 +89,14 @@ const TaskTable = () => { accessorKey: "title", header: intl.formatMessage(messages.titleColumn), cell: (info) => ( - + {info.row.original.title} - + ), meta: { columnType: ColumnType.text, @@ -124,7 +129,7 @@ const TaskTable = () => { setShowDeleteConfirmationModal(true); }} data-testid={`task-${info.row.original.id}-delete-button`} - variant={ButtonVariant.detail} + variant="detail" > @@ -147,7 +152,7 @@ const TaskTable = () => { router.push(`/task/${info.row.original.id}/detail`); }} data-testid={`task-${info.row.original.id}-details-button`} - variant={ButtonVariant.detail} + variant="detail" > @@ -201,7 +206,7 @@ const TaskTable = () => { }} />