From 0b22ea0191d68c6f40094f9cd25b72b00fe20807 Mon Sep 17 00:00:00 2001 From: Lorenzo Corallo <66379281+lorenzocorallo@users.noreply.github.com> Date: Wed, 12 Jul 2023 00:50:43 +0200 Subject: [PATCH] feat: add table filter by "matricola" (#109) * feat: add shacdn/ui input * feat: add table filter by "matricola" --- src/components/ui/input.tsx | 24 ++++++++ src/routes/view/viewer/Table.tsx | 96 +++++++++++++++++++++++++++++--- src/routes/view/viewer/index.tsx | 34 ++--------- src/utils/constants.ts | 2 + src/utils/strings.ts | 17 ++++++ 5 files changed, 136 insertions(+), 37 deletions(-) create mode 100644 src/components/ui/input.tsx diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx new file mode 100644 index 00000000..b761c24f --- /dev/null +++ b/src/components/ui/input.tsx @@ -0,0 +1,24 @@ +import * as React from "react" + +import { cn } from "@/utils/ui.ts" + +export type InputProps = React.InputHTMLAttributes + +const Input = React.forwardRef( + ({ className, type, ...props }, ref) => { + return ( + + ) + } +) +Input.displayName = "Input" + +export { Input } diff --git a/src/routes/view/viewer/Table.tsx b/src/routes/view/viewer/Table.tsx index 7e4425c8..7fa28d7e 100644 --- a/src/routes/view/viewer/Table.tsx +++ b/src/routes/view/viewer/Table.tsx @@ -1,10 +1,15 @@ -import { useState } from "react" -import { Table as TableType } from "@tanstack/react-table" +import { useMemo, useState } from "react" +import { + ColumnFiltersState, + Row, + Table as TableType +} from "@tanstack/react-table" import { MdOutlineKeyboardDoubleArrowLeft as DoubleArrowLeft, MdKeyboardDoubleArrowRight as DoubleArrowRight, MdKeyboardArrowLeft as ArrowLeft, - MdKeyboardArrowRight as ArrowRight + MdKeyboardArrowRight as ArrowRight, + MdDownload } from "react-icons/md" import { CellContext, @@ -14,6 +19,7 @@ import { flexRender, getCoreRowModel, getPaginationRowModel, + getFilteredRowModel, useReactTable } from "@tanstack/react-table" @@ -35,10 +41,16 @@ import { } from "@/components/ui/select" import School from "@/utils/types/data/School" import StudentResult from "@/utils/types/data/parsed/Ranking/StudentResult" +import { Input } from "@/components/ui/input" +import MeritTable from "@/utils/types/data/parsed/Ranking/MeritTable" +import CourseTable from "@/utils/types/data/parsed/Ranking/CourseTable" +import Store from "@/utils/data/store" +import { sha256 } from "@/utils/strings" interface TableProps extends React.HTMLAttributes { school: School - rows: StudentResult[] + csvFilename: string + table: MeritTable | CourseTable isGlobalRanking?: boolean } @@ -102,7 +114,9 @@ const headerBorder = ( return "border-x" } -export default function Table({ rows }: TableProps) { +export default function Table({ table: pTable, csvFilename }: TableProps) { + const { rows } = pTable + const csv = useMemo(() => (pTable ? Store.tableToCsv(pTable) : ""), [pTable]) const has = makeHas(rows) const columns = getColumns(rows) const [columnVisibility, setColumnVisibility] = @@ -112,21 +126,60 @@ export default function Table({ rows }: TableProps) { pageIndex: 0 }) + const [idFilter, setIdFilter] = useState("") + const [columnFilters, setColumnFilters] = useState([ + { + id: "matricola-hash", + value: idFilter + } + ]) + + const handleIdFilterChange = async (v: string) => { + setIdFilter(v) + const hashed = v === "" ? "" : await sha256(v) + setColumnFilters([ + { + id: "matricola-hash", + value: hashed + } + ]) + } + const table = useReactTable({ data: rows, columns, state: { columnVisibility, - pagination + pagination, + columnFilters }, getCoreRowModel: getCoreRowModel(), getPaginationRowModel: getPaginationRowModel(), + getFilteredRowModel: getFilteredRowModel(), onColumnVisibilityChange: setColumnVisibility, + onColumnFiltersChange: setColumnFilters, onPaginationChange: setPagination }) return ( -
+
+
+ {has.id && ( + handleIdFilterChange(e.target.value)} + /> + )} + +
@@ -368,7 +421,20 @@ function getColumns(rows: StudentResult[]): ColumnDef[] { { accessorKey: "id", header: "Matricola hash", - cell: CellFns.displayHash + id: "matricola-hash", + cell: CellFns.displayHash, + enableColumnFilter: true, + enableGlobalFilter: true, + filterFn: ( + row: Row, + _: string, + filterValue: string + ): boolean => { + if (!filterValue) return true + return ( + row.original.id?.slice(0, 10) === filterValue.slice(0, 10) ?? true + ) + } }, { accessorKey: "birthDate", @@ -388,3 +454,17 @@ function calculateRowSpan(header: Header): number { return 1 } + +function downloadCsv(csv: string, filename: string) { + const blob = new Blob([csv], { type: "text/csv" }) + const url = window.URL.createObjectURL(blob) + + const a = document.createElement("a") + a.href = url + a.download = (filename ?? "data") + ".csv" + a.click() + + a.remove() + + window.URL.revokeObjectURL(url) +} diff --git a/src/routes/view/viewer/index.tsx b/src/routes/view/viewer/index.tsx index 87a22e30..82e45cb5 100644 --- a/src/routes/view/viewer/index.tsx +++ b/src/routes/view/viewer/index.tsx @@ -1,5 +1,4 @@ import { useContext, useEffect, useMemo, useState } from "react" -import { MdDownload } from "react-icons/md" import { ErrorComponent, Navigate, Route, useNavigate } from "@tanstack/router" import MobileContext from "@/contexts/MobileContext" import School from "@/utils/types/data/School.ts" @@ -7,7 +6,6 @@ import CourseTable from "@/utils/types/data/parsed/Ranking/CourseTable.ts" import { PhaseLink } from "@/utils/types/data/parsed/Index/RankingFile.ts" import { ABS_ORDER } from "@/utils/constants.ts" import Store from "@/utils/data/store.ts" -import { Button } from "@/components/ui/button" import Spinner from "@/components/custom-ui/Spinner.tsx" import Page from "@/components/custom-ui/Page.tsx" import { rootRoute } from "@/routes/root.tsx" @@ -90,7 +88,6 @@ export const viewerRoute = new Route({ () => store.getTable(selectedCourse, selectedLocation), [selectedCourse, selectedLocation, store] ) - const csv = useMemo(() => (table ? Store.tableToCsv(table) : ""), [table]) useEffect(() => { if (!table) return @@ -140,24 +137,17 @@ export const viewerRoute = new Route({ /> )}
-
- -
{selectedPhase?.name === ranking.phase ? (
{table ? ( - +
) : (

Nessun dato disponibile

)} @@ -172,17 +162,3 @@ export const viewerRoute = new Route({ ) } }) - -function downloadCsv(csv: string, filename: string) { - const blob = new Blob([csv], { type: "text/csv" }) - const url = window.URL.createObjectURL(blob) - - const a = document.createElement("a") - a.href = url - a.download = (filename ?? "data") + ".csv" - a.click() - - a.remove() - - window.URL.revokeObjectURL(url) -} diff --git a/src/utils/constants.ts b/src/utils/constants.ts index d76cbd0e..228c1acc 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -15,3 +15,5 @@ export const SCHOOLS = [ export const ALERT_LEVELS = ["error", "warning", "success", "info"] as const export const ABS_ORDER = "absorder" as const + +export const SALT = "saltPoliNetwork" as const diff --git a/src/utils/strings.ts b/src/utils/strings.ts index 60e539a5..fb0e1f87 100644 --- a/src/utils/strings.ts +++ b/src/utils/strings.ts @@ -1,3 +1,5 @@ +import { SALT } from "./constants" + export function capitalizeWords(str: string): string { const words = str.split(/\b/) const capitalizedWords = words.map(word => { @@ -12,3 +14,18 @@ export function containsOnlyNumbers(input?: string): boolean { if (!input) return false return /^[0-9]+$/.test(input) } + +export async function sha256(input: string, salt = SALT): Promise { + // encode as UTF-8 + const msgBuffer = new TextEncoder().encode(input + salt) + + // hash the input + const hashBuffer = await crypto.subtle.digest("SHA-256", msgBuffer) + + // convert ArrayBuffer to Array + const hashArray = Array.from(new Uint8Array(hashBuffer)) + + // convert bytes to hex string + const hashHex = hashArray.map(b => ("00" + b.toString(16)).slice(-2)).join("") + return hashHex +}