diff --git a/frontend/README.md b/frontend/README.md index 28253b60..9bd24f1d 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -1,6 +1,6 @@ # fAIr Frontend -This project is a frontend web application built using **React 18**, **TypeScript**, and **Vite**. The app leverages modern libraries such as **@hotosm/ui**, **Shoelace**, and **Framer Motion** for UI components, and **React Router** for client-side routing. +This project is a frontend web application built using **React 18**, **TypeScript**, and **Vite**. The app leverages modern libraries such as **@hotosm/ui**, and **Shoelace** for UI components, and **React Router** for client-side routing. ## Table of Contents @@ -60,6 +60,7 @@ Here's an overview of the folder structure: │ ├── app/ # Contains the application routes and providers. │ ├── assets/ # Static assets specific to the app (images, icons, etc.). │ ├── components/ # Reusable components and layouts. +| |── features/ # Contains the main features of the application. │ ├── hook/ # Reusable hooks. │ ├── styles/ # Global styles. │ ├── utils/ # Utility functions, application content and constants. diff --git a/frontend/src/app/routes/models/index.tsx b/frontend/src/app/routes/models/index.tsx index 787873ac..b5b7f749 100644 --- a/frontend/src/app/routes/models/index.tsx +++ b/frontend/src/app/routes/models/index.tsx @@ -21,7 +21,7 @@ import { import Pagination, { PAGE_LIMIT, } from "@/features/models/components/pagination"; -import { buildDateFilterQueryString } from "@/utils"; +import { APP_CONTENT, buildDateFilterQueryString } from "@/utils"; import { PageHeader } from "@/features/models/components/"; import { dateFilters } from "@/features/models/components/filters/date-range-filter"; import { ORDERING_FIELDS } from "@/features/models/components/filters/ordering-filter"; @@ -68,6 +68,7 @@ const ClearFilters = ({ return (
{canClearAllFilters ? ( + // @ts-expect-error bad type definition @@ -90,7 +91,9 @@ const SetMapToggle = ({ className={`${isMobile ? "inline-flex md:hidden" : "hidden md:inline-flex"} items-center gap-x-4`} role="button" > -

Map View

+

+ {APP_CONTENT.models.modelsList.filtersSection.mapViewToggleText} +

{ ) : (
-

{data?.count} models

+

+ {data?.count}{" "} + { + APP_CONTENT.models.modelsList.sortingAndPaginationSection + .modelCountSuffix + } +

diff --git a/frontend/src/app/routes/models/model-details.tsx b/frontend/src/app/routes/models/model-details.tsx index 1a05eb3f..f7860d80 100644 --- a/frontend/src/app/routes/models/model-details.tsx +++ b/frontend/src/app/routes/models/model-details.tsx @@ -14,7 +14,7 @@ import { import { ModelDetailsSkeleton } from "@/features/models/components/skeletons"; import { useModelDetails } from "@/features/models/hooks/use-models"; import { useDialog } from "@/hooks/use-dialog"; -import { APPLICATION_ROUTES } from "@/utils"; +import { APP_CONTENT, APPLICATION_ROUTES } from "@/utils"; import { useEffect } from "react"; import { useNavigate, useParams } from "react-router-dom"; @@ -61,7 +61,9 @@ export const ModelDetailsPage = () => { openModelFilesDialog={openModelFilesDialog} openTrainingAreaDialog={openDialog} /> - + {
{/* mobile */} - +
{ : APP_CONTENT.pageNotFound.messages.pageNotFound}

-

+

404 {/* Icon */} diff --git a/frontend/src/components/errors/fallback.tsx b/frontend/src/components/errors/fallback.tsx index 4839a84c..38117886 100644 --- a/frontend/src/components/errors/fallback.tsx +++ b/frontend/src/components/errors/fallback.tsx @@ -16,7 +16,7 @@ const MainErrorFallback = () => { diff --git a/frontend/src/components/errors/under-construction.tsx b/frontend/src/components/errors/under-construction.tsx index 2fbaba87..85caf879 100644 --- a/frontend/src/components/errors/under-construction.tsx +++ b/frontend/src/components/errors/under-construction.tsx @@ -13,7 +13,7 @@ const PageUnderConstruction = () => { diff --git a/frontend/src/components/index.ts b/frontend/src/components/index.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/frontend/src/components/layouts/root-layout.tsx b/frontend/src/components/layouts/root-layout.tsx index 98c65377..101fd8fa 100644 --- a/frontend/src/components/layouts/root-layout.tsx +++ b/frontend/src/components/layouts/root-layout.tsx @@ -5,7 +5,7 @@ import { useEffect } from "react"; const RootLayout = () => { const { pathname } = useLocation(); - // Scroll to top on new page. + // Scroll to top on page switch. useEffect(() => { window.scrollTo(0, 0); }, [pathname]); diff --git a/frontend/src/components/ui/animated-beam/animated-beam.tsx b/frontend/src/components/ui/animated-beam/animated-beam.tsx index 250ec597..94133808 100644 --- a/frontend/src/components/ui/animated-beam/animated-beam.tsx +++ b/frontend/src/components/ui/animated-beam/animated-beam.tsx @@ -5,9 +5,9 @@ import { RefObject, useEffect, useId, useState } from "react"; import { motion } from "framer-motion"; import { cn } from "@/utils"; -export interface AnimatedBeamProps { +type AnimatedBeamProps = { className?: string; - containerRef: RefObject; // Container ref + containerRef: RefObject; fromRef: RefObject; toRef: RefObject; curvature?: number; @@ -23,9 +23,9 @@ export interface AnimatedBeamProps { startYOffset?: number; endXOffset?: number; endYOffset?: number; -} +}; -export const AnimatedBeam: React.FC = ({ +const AnimatedBeam: React.FC = ({ className, containerRef, fromRef, @@ -187,3 +187,5 @@ export const AnimatedBeam: React.FC = ({ ); }; + +export default AnimatedBeam; diff --git a/frontend/src/components/ui/animated-beam/index.ts b/frontend/src/components/ui/animated-beam/index.ts index 175de46f..4d0e659f 100644 --- a/frontend/src/components/ui/animated-beam/index.ts +++ b/frontend/src/components/ui/animated-beam/index.ts @@ -1 +1 @@ -export * from "./animated-beam"; +export { default as AnimatedBeam } from "./animated-beam"; diff --git a/frontend/src/components/ui/badge/badge.tsx b/frontend/src/components/ui/badge/badge.tsx index f9865ab6..b558a367 100644 --- a/frontend/src/components/ui/badge/badge.tsx +++ b/frontend/src/components/ui/badge/badge.tsx @@ -1,4 +1,5 @@ import { TBadgeVariants } from "@/types"; +import { cn } from "@/utils"; type BadgeProps = { variant: TBadgeVariants; @@ -24,7 +25,9 @@ const Badge: React.FC = ({ }; return (

@@ -122,7 +127,7 @@ const DropDown: React.FC = ({ diff --git a/frontend/src/components/ui/footer/footer.module.css b/frontend/src/components/ui/footer/footer.module.css deleted file mode 100644 index e69de29b..00000000 diff --git a/frontend/src/components/ui/form/date-picker/date-picker.tsx b/frontend/src/components/ui/form/date-picker/date-picker.tsx index 7d3581da..65260095 100644 --- a/frontend/src/components/ui/form/date-picker/date-picker.tsx +++ b/frontend/src/components/ui/form/date-picker/date-picker.tsx @@ -1,5 +1,6 @@ import { Button } from "@/components/ui/button"; import Input from "@/components/ui/form/input/input"; +import { cn } from "@/utils"; type DateRangePickerProps = { startDate: string; @@ -24,7 +25,9 @@ const DateRangePicker: React.FC = ({
{/* Native date pickers */}
= ({ {/* Only show when there is at least an input in either of the dates */} {(startDate || endDate) && (
+ {/* @ts-expect-error bad type definition */} diff --git a/frontend/src/components/ui/form/input/input.tsx b/frontend/src/components/ui/form/input/input.tsx index e235fd91..358fd97c 100644 --- a/frontend/src/components/ui/form/input/input.tsx +++ b/frontend/src/components/ui/form/input/input.tsx @@ -1,11 +1,11 @@ import { SlInput } from "@shoelace-style/shoelace/dist/react"; import styles from "./input.module.css"; import { CalenderIcon } from "@/components/ui/icons"; -import { useBrowserType } from "@/hooks/use-browser-type"; +import useBrowserType from "@/hooks/use-browser-type"; import { useRef } from "react"; type InputProps = { - handleInput: (arg: any) => void; + handleInput: (arg: React.ChangeEvent) => void; value: string; className?: string; placeholder?: string; @@ -40,6 +40,7 @@ const Input: React.FC = ({ return ( = ({ checked={checked} disabled={disabled} onSlChange={handleSwitchChange} - className={`${styles.customSwitch} ${checked && styles.checked}`} + className={cn(`${styles.customSwitch} ${checked && styles.checked}`)} > ); }; diff --git a/frontend/src/components/ui/header/navbar.tsx b/frontend/src/components/ui/header/navbar.tsx index 7672f0af..18b54e00 100644 --- a/frontend/src/components/ui/header/navbar.tsx +++ b/frontend/src/components/ui/header/navbar.tsx @@ -38,7 +38,7 @@ const NavBar = () => {
- +
{isAuthenticated ? ( @@ -131,10 +131,10 @@ const navLinks: TNavBarLinks = [ type NavBarLinksProps = { className: string; - setOpen?:(arg:boolean)=>void + setOpen?: (arg: boolean) => void; }; -const NavBarLinks: React.FC = ({ className,setOpen }) => { +const NavBarLinks: React.FC = ({ className, setOpen }) => { const location = useLocation(); return ( @@ -142,9 +142,9 @@ const NavBarLinks: React.FC = ({ className,setOpen }) => { {navLinks.map((link, id) => (
  • { + onClick={() => { //close the drawer after navigating to a new page on mobile - setOpen && setOpen(false) + setOpen && setOpen(false); }} className={`${styles.navLinkItem} ${location.pathname.includes(link.href) && styles.activeLink}`} > diff --git a/frontend/src/components/ui/image/image.tsx b/frontend/src/components/ui/image/image.tsx index f5e783db..0bfc4bcb 100644 --- a/frontend/src/components/ui/image/image.tsx +++ b/frontend/src/components/ui/image/image.tsx @@ -1,3 +1,4 @@ +import { cn } from "@/utils"; import { useEffect, useState } from "react"; type ImageProps = { @@ -46,7 +47,7 @@ const Image: React.FC = ({ title={title || alt} width={width} height={height} - className={` ${className} ${isLoading ? "hidden" : ""}`} + className={cn(`${className} ${isLoading ? "hidden" : ""}`)} onLoad={handleLoad} onError={handleError} /> diff --git a/frontend/src/components/ui/index.ts b/frontend/src/components/ui/index.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/frontend/src/components/ui/link/link.tsx b/frontend/src/components/ui/link/link.tsx index 9d729e08..eb27fc7f 100644 --- a/frontend/src/components/ui/link/link.tsx +++ b/frontend/src/components/ui/link/link.tsx @@ -1,3 +1,4 @@ +import { cn } from "@/utils"; import styles from "./link.module.css"; import { Link as ReactRouterLink } from "react-router-dom"; @@ -34,7 +35,7 @@ const Link: React.FC = ({ ) : ( {children} diff --git a/frontend/src/components/ui/spinner/spinner.tsx b/frontend/src/components/ui/spinner/spinner.tsx index 5663d1c5..50654eb7 100644 --- a/frontend/src/components/ui/spinner/spinner.tsx +++ b/frontend/src/components/ui/spinner/spinner.tsx @@ -1,7 +1,7 @@ import SlSpinner from "@shoelace-style/shoelace/dist/react/spinner/index.js"; type SpinnerProps = { - style?: object; + style?: Record; }; const Spinner: React.FC = ({ style }) => ( diff --git a/frontend/src/features/models/api/update-trainings.ts b/frontend/src/features/models/api/update-trainings.ts index d9ab02eb..e89ed732 100644 --- a/frontend/src/features/models/api/update-trainings.ts +++ b/frontend/src/features/models/api/update-trainings.ts @@ -1,8 +1,8 @@ import { API_ENDPOINTS, apiClient, MutationConfig } from "@/services"; -import { useTrainingHistory } from "../hooks/use-training"; -import { useModelDetails } from "../hooks/use-models"; +import { useTrainingHistory } from "@/features/models/hooks/use-training"; +import { useModelDetails } from "@/features/models/hooks/use-models"; import { useMutation } from "@tanstack/react-query"; -import { PAGE_LIMIT } from "../components/pagination"; +import { PAGE_LIMIT } from "@/features/models/components/pagination"; export const updateTraining = (trainingId: number) => { return apiClient.post(`${API_ENDPOINTS.UPDATE_TRAINING(trainingId)}`); diff --git a/frontend/src/features/models/components/dialogs/mobile-filters-dialog.tsx b/frontend/src/features/models/components/dialogs/mobile-filters-dialog.tsx index f81fc047..2d064c7f 100644 --- a/frontend/src/features/models/components/dialogs/mobile-filters-dialog.tsx +++ b/frontend/src/features/models/components/dialogs/mobile-filters-dialog.tsx @@ -1,5 +1,5 @@ import { Dialog } from "@/components/ui/dialog"; -import { useDevice } from "@/hooks/use-device"; +import useDevice from "@/hooks/use-device"; import { CategoryFilter, DateRangeFilter, diff --git a/frontend/src/features/models/components/dialogs/model-files-dialog.tsx b/frontend/src/features/models/components/dialogs/model-files-dialog.tsx index c5d96714..f4962c83 100644 --- a/frontend/src/features/models/components/dialogs/model-files-dialog.tsx +++ b/frontend/src/features/models/components/dialogs/model-files-dialog.tsx @@ -1,6 +1,7 @@ import { Dialog } from "@/components/ui/dialog"; import DirectoryTree from "@/features/models/components/directory-tree"; -import { useDevice } from "@/hooks/use-device"; +import useDevice from "@/hooks/use-device"; +import { APP_CONTENT } from "@/utils"; type TrainingAreaDialogProps = { isOpened: boolean; @@ -20,7 +21,7 @@ const ModelFilesDialog: React.FC = ({ diff --git a/frontend/src/features/models/components/dialogs/training-area-dialog.tsx b/frontend/src/features/models/components/dialogs/training-area-dialog.tsx index 0c41230b..d032fb9a 100644 --- a/frontend/src/features/models/components/dialogs/training-area-dialog.tsx +++ b/frontend/src/features/models/components/dialogs/training-area-dialog.tsx @@ -1,6 +1,7 @@ import { Dialog } from "@/components/ui/dialog"; -import { useDevice } from "@/hooks/use-device"; +import useDevice from "@/hooks/use-device"; import { MapComponent } from "@/features/models/components/map"; +import { cn } from "@/utils"; type TrainingAreaDialogProps = { isOpened: boolean; @@ -20,7 +21,7 @@ const TrainingAreaDialog: React.FC = ({ label={"Training Area"} size={isMobile ? "extra-large" : "large"} > -
    +
    undefined} />
    diff --git a/frontend/src/features/models/components/dialogs/training-details-dialog.tsx b/frontend/src/features/models/components/dialogs/training-details-dialog.tsx index 70e6926c..a507ca02 100644 --- a/frontend/src/features/models/components/dialogs/training-details-dialog.tsx +++ b/frontend/src/features/models/components/dialogs/training-details-dialog.tsx @@ -1,6 +1,6 @@ import { Dialog } from "@/components/ui/dialog"; -import { useDevice } from "@/hooks/use-device"; -import ModelProperties from "../model-details-properties"; +import useDevice from "@/hooks/use-device"; +import ModelProperties from "@/features/models/components/model-details-properties"; type TrainingDetailsDialogProps = { isOpened: boolean; diff --git a/frontend/src/features/models/components/directory-tree.tsx b/frontend/src/features/models/components/directory-tree.tsx index dad80a10..12470ef0 100644 --- a/frontend/src/features/models/components/directory-tree.tsx +++ b/frontend/src/features/models/components/directory-tree.tsx @@ -1,7 +1,7 @@ import { DirectoryIcon, FileIcon } from "@/components/ui/icons"; import SlFormatBytes from "@shoelace-style/shoelace/dist/react/format-bytes/index.js"; import { useState, useEffect } from "react"; -import { truncateString } from "@/utils"; +import { APP_CONTENT, truncateString } from "@/utils"; import { useQueryClient } from "@tanstack/react-query"; import { SlTree, @@ -222,14 +222,19 @@ const DirectoryTree: React.FC = ({ }; if (isLoading) return ; - if (hasError) return
    Error loading directories.
    ; + if (hasError) + return ( +
    {APP_CONTENT.models.modelsDetailsCard.modelFilesDialog.error}.
    + ); return ( //@ts-expect-error bad type definition - Root Directory + + {APP_CONTENT.models.modelsDetailsCard.modelFilesDialog.rootDirectory} + {directoryTree && renderTreeItems(directoryTree)} diff --git a/frontend/src/features/models/components/filters/ordering-filter.tsx b/frontend/src/features/models/components/filters/ordering-filter.tsx index c9f1d323..a0ad4ecb 100644 --- a/frontend/src/features/models/components/filters/ordering-filter.tsx +++ b/frontend/src/features/models/components/filters/ordering-filter.tsx @@ -4,6 +4,7 @@ import { DropdownMenuItem } from "@/components/ui/dropdown/dropdown"; import { CheckboxGroup } from "@/components/ui/form"; import { useDropdownMenu } from "@/hooks/use-dropdown-menu"; import { TQueryParams } from "@/types"; +import { APP_CONTENT } from "@/utils"; export const ORDERING_FIELDS: DropdownMenuItem[] = [ { @@ -65,7 +66,12 @@ const OrderingFilter: React.FC = ({ )?.value } triggerComponent={ -

    Sort by

    +

    + { + APP_CONTENT.models.modelsList.sortingAndPaginationSection + .sortingTitle + } +

    } >
    diff --git a/frontend/src/features/models/components/filters/search-filter.tsx b/frontend/src/features/models/components/filters/search-filter.tsx index 8cab8f51..96da33e8 100644 --- a/frontend/src/features/models/components/filters/search-filter.tsx +++ b/frontend/src/features/models/components/filters/search-filter.tsx @@ -1,6 +1,7 @@ import { SEARCH_PARAMS } from "@/app/routes/models"; import { Input } from "@/components/ui/form"; import { SearchIcon } from "@/components/ui/icons"; +import { APP_CONTENT } from "@/utils"; import { useCallback } from "react"; type SearchFilterProps = { @@ -25,7 +26,9 @@ const SearchFilter: React.FC = ({ updateQuery, query }) => {
    diff --git a/frontend/src/features/models/components/header.tsx b/frontend/src/features/models/components/header.tsx index c7e5bdde..d2761787 100644 --- a/frontend/src/features/models/components/header.tsx +++ b/frontend/src/features/models/components/header.tsx @@ -1,6 +1,6 @@ import { ButtonWithIcon } from "@/components/ui/button"; import { AddIcon } from "@/components/ui/icons"; -import { APPLICATION_ROUTES } from "@/utils"; +import { APP_CONTENT, APPLICATION_ROUTES } from "@/utils"; import { useNavigate } from "react-router-dom"; const PageHeader = () => { @@ -9,20 +9,18 @@ const PageHeader = () => {

    - fAIr AI models + {APP_CONTENT.models.modelsList.pageTitle}

    - Each model is trained using one of the training datasets. Published - models can be used to find mappable features in imagery that is - similar to the training areas that dataset comes from. + {APP_CONTENT.models.modelsList.description}

    navigate(APPLICATION_ROUTES.CREATE_NEW_MODEL)} />
    diff --git a/frontend/src/features/models/components/map/zoom-controls.tsx b/frontend/src/features/models/components/map/zoom-controls.tsx index 853dc002..9ce5ec33 100644 --- a/frontend/src/features/models/components/map/zoom-controls.tsx +++ b/frontend/src/features/models/components/map/zoom-controls.tsx @@ -1,3 +1,4 @@ +import { cn } from "@/utils"; import { Map } from "maplibre-gl"; import { useCallback, useEffect, useState } from "react"; @@ -11,7 +12,9 @@ const ZoomButton = ({ svgPath: string; }) => (
    {/* accuracy */}
    -

    Accuracy:

    +

    + {APP_CONTENT.models.modelsList.modelCard.accuracy} +

    {roundNumber(model.accuracy)} %

    @@ -54,7 +56,7 @@ const ModelCard: React.FC = ({ model }) => { {model.user.username}

    - Last Modified:{" "} + {APP_CONTENT.models.modelsList.modelCard.lastModified}{" "} {extractDatePart(model.last_modified)} diff --git a/frontend/src/features/models/components/model-details-info.tsx b/frontend/src/features/models/components/model-details-info.tsx index 39a4bfa9..bc406e34 100644 --- a/frontend/src/features/models/components/model-details-info.tsx +++ b/frontend/src/features/models/components/model-details-info.tsx @@ -1,11 +1,11 @@ import { ButtonWithIcon } from "@/components/ui/button"; import { DirectoryIcon, MapIcon } from "@/components/ui/icons"; -import { formatDate, truncateString } from "@/utils"; -import ModelDetailItem from "./model-detail-item"; -import ModelDetailsSection from "./model-details-section"; +import { APP_CONTENT, formatDate, truncateString } from "@/utils"; +import ModelDetailItem from "@/features/models/components/model-detail-item"; +import ModelDetailsSection from "@/features/models/components/model-details-section"; import ChevronDownIcon from "@/components/ui/icons/chevron-down"; import { Divider } from "@/components/ui/divider"; -import ModelFeedbacks from "./model-feedbacks"; +import ModelFeedbacks from "@/features/models/components/model-feedbacks"; const ModelDetailsInfo = ({ data, @@ -20,7 +20,9 @@ const ModelDetailsInfo = ({

    -

    Model ID: {data?.id}

    +

    + {APP_CONTENT.models.modelsDetailsCard.modelId} {data?.id} +

    - {data?.description ?? "Model description is not available."} + {data?.description ?? + APP_CONTENT.models.modelsDetailsCard + .modelDescriptionNotAvailable}

    -

    View Training Area

    +

    {APP_CONTENT.models.modelsDetailsCard.viewTrainingArea}

    @@ -57,23 +61,28 @@ const ModelDetailsInfo = ({
    - +

    - Training ID: + + {APP_CONTENT.models.modelsDetailsCard.trainingId}{" "} + Training_{data?.published_training}

    = ({ ) : ( {value ?? "N/A"} @@ -110,50 +113,86 @@ const ModelProperties: React.FC = ({ return (
    + - {/* Animate the status when it's in progress. */} {isTrainingDetailsDialog && ( )} @@ -209,7 +248,7 @@ const FailedTrainingTraceBack = ({ taskId }: { taskId: string }) => { role="button" className="flex items-center gap-x-2" > -

    Logs

    +

    {APP_CONTENT.models.modelsDetailsCard.trainingInfoDialog.logs}

    {showLogs && } diff --git a/frontend/src/features/models/components/model-feedbacks.tsx b/frontend/src/features/models/components/model-feedbacks.tsx index 43715b5d..ad3655fa 100644 --- a/frontend/src/features/models/components/model-feedbacks.tsx +++ b/frontend/src/features/models/components/model-feedbacks.tsx @@ -1,6 +1,7 @@ import { ButtonWithIcon } from "@/components/ui/button"; import { ChatbubbleIcon } from "@/components/ui/icons"; -import { useTrainingFeedbacks } from "../hooks/use-training"; +import { useTrainingFeedbacks } from "@/features/models/hooks/use-training"; +import { APP_CONTENT } from "@/utils"; const ModelFeedbacks = ({ trainingId }: { trainingId: number }) => { const { data, isLoading, isError } = useTrainingFeedbacks(trainingId); @@ -13,11 +14,13 @@ const ModelFeedbacks = ({ trainingId }: { trainingId: number }) => { <>

    {isError ? "N/A" : data?.count} - Feedbacks + + {APP_CONTENT.models.modelsDetailsCard.feedbacks} +

    = ({ }; return ( -
    +

    {_offset + 1} -{" "} diff --git a/frontend/src/features/models/components/training-history-table.tsx b/frontend/src/features/models/components/training-history-table.tsx index 7b90f262..027ad8f2 100644 --- a/frontend/src/features/models/components/training-history-table.tsx +++ b/frontend/src/features/models/components/training-history-table.tsx @@ -1,7 +1,8 @@ -import { useTrainingHistory } from "../hooks/use-training"; +import { useTrainingHistory } from "@/features/models/hooks/use-training"; import { DataTable } from "@/components/ui/data-table"; import { TBadgeVariants, TTrainingDetails } from "@/types"; import { + APP_CONTENT, formatDate, formatDuration, roundNumber, @@ -9,8 +10,8 @@ import { } from "@/utils"; import { ColumnDef, SortingState } from "@tanstack/react-table"; import { useState } from "react"; -import { SortableHeader } from "./table-header"; -import { TableSkeleton } from "./skeletons"; +import { SortableHeader } from "@/features/models/components/table-header"; +import { TableSkeleton } from "@/features/models/components/skeletons"; import { DropDown } from "@/components/ui/dropdown"; import { useDropdownMenu } from "@/hooks/use-dropdown-menu"; import { useAuth } from "@/app/providers/auth-provider"; @@ -18,10 +19,12 @@ import { Badge } from "@/components/ui/badge"; import CheckIcon from "@/components/ui/icons/check-icon"; import { ElipsisIcon, InfoIcon } from "@/components/ui/icons"; import { useDialog } from "@/hooks/use-dialog"; -import { TrainingDetailsDialog } from "./dialogs"; -import { useUpdateTraining } from "../api/update-trainings"; +import { TrainingDetailsDialog } from "@/features/models/components/dialogs"; +import { useUpdateTraining } from "@/features/models/api/update-trainings"; import { useToast } from "@/app/providers/toast-provider"; -import Pagination, { PAGE_LIMIT } from "./pagination"; +import Pagination, { + PAGE_LIMIT, +} from "@/features/models/components/pagination"; type TrainingHistoryTableProps = { modelId: string; @@ -42,7 +45,9 @@ const columnDefinitions = ( header: ({ column }) => , }, { - header: "Epochs / Batch Size", + header: + APP_CONTENT.models.modelsDetailsCard.trainingHistoryTableHeader + .epochAndBatchSize, accessorFn: (row) => `${row.epochs}/${row.batch_size}`, cell: (row) => ( {row.getValue() as string} @@ -50,20 +55,24 @@ const columnDefinitions = ( }, { accessorKey: "started_at", - header: "Started At", + header: + APP_CONTENT.models.modelsDetailsCard.trainingHistoryTableHeader.startedAt, cell: ({ row }) => { return {formatDate(row.getValue("started_at"))}; }, }, { accessorKey: "user.username", - header: "Submitted by", + header: + APP_CONTENT.models.modelsDetailsCard.trainingHistoryTableHeader + .sumittedBy, cell: ({ row }) => { return {truncateString(row.original.user.username)}; }, }, { - header: "Duration", + header: + APP_CONTENT.models.modelsDetailsCard.trainingHistoryTableHeader.duration, accessorFn: (row) => formatDuration(new Date(row.started_at), new Date(row.finished_at)), cell: (row) => ( @@ -72,7 +81,8 @@ const columnDefinitions = ( }, { accessorKey: "input_contact_spacing", - header: "DS Size", + header: + APP_CONTENT.models.modelsDetailsCard.trainingHistoryTableHeader.dsSize, cell: ({ row }) => { return {row.getValue("input_contact_spacing") ?? 0}; }, @@ -80,7 +90,13 @@ const columnDefinitions = ( { accessorKey: "accuracy", header: ({ column }) => ( - + ), cell: ({ row }) => { return ( @@ -93,7 +109,8 @@ const columnDefinitions = ( }, }, { - header: "Status", + header: + APP_CONTENT.models.modelsDetailsCard.trainingHistoryTableHeader.status, accessorKey: "status", cell: (row) => { const statusToVariant: Record = { @@ -117,8 +134,9 @@ const columnDefinitions = ( }, }, { - header: "In Use", - // accessorFn: row => row.freeze_layers, + header: + APP_CONTENT.models.modelsDetailsCard.trainingHistoryTableHeader.inUse, + cell: ({ row }) => { return ( @@ -134,8 +152,10 @@ const columnDefinitions = ( ...(modelOwner !== authUsername ? [ { - header: "Info", - // accessorFn: (row: TTrainingDetails) => row.multimasks, + header: + APP_CONTENT.models.modelsDetailsCard.trainingHistoryTableHeader + .info, + cell: ({ row }: { row: any }) => { return ( row.model, + header: + APP_CONTENT.models.modelsDetailsCard.trainingHistoryTableHeader + .action, + cell: ({ row }: { row: any }) => { const { dropdownIsOpened, onDropdownHide, onDropdownShow } = useDropdownMenu(); @@ -245,7 +267,14 @@ const TrainingHistoryTable: React.FC = ({ />

    -

    {data?.count} Training History

    +

    + {" "} + {data?.count}{" "} + { + APP_CONTENT.models.modelsDetailsCard.trainingHistoryTableHeader + .trainingHistoryCount + } +

    { +/** + * Custom hook to detect if the current browser is Google Chrome. + * + * @returns { isChrome: boolean } - An object containing a boolean value indicating if the browser is Chrome. + * + */ +const useBrowserType = (): { isChrome: boolean } => { const isChrome = useMemo(() => { return ( /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor) @@ -9,3 +15,5 @@ export const useBrowserType = (): { isChrome: boolean } => { return { isChrome }; }; + +export default useBrowserType; diff --git a/frontend/src/hooks/use-debounce.ts b/frontend/src/hooks/use-debounce.ts index 8177e801..9304a316 100644 --- a/frontend/src/hooks/use-debounce.ts +++ b/frontend/src/hooks/use-debounce.ts @@ -1,6 +1,17 @@ import { useState, useEffect } from "react"; -function useDebounce(value: string, delay: number): string { +/** + * Custom hook that debounces a value. This hook delays updating the value + * until after a specified delay period has passed without changes. + * + * @param value - The value that will be debounced. + * @param delay - The debounce delay in milliseconds. + * + * @returns {string} debouncedValue - The debounced value, updated after the delay. + * + */ + +const useDebounce = (value: string, delay: number): string => { const [debouncedValue, setDebouncedValue] = useState(value); useEffect(() => { @@ -13,6 +24,6 @@ function useDebounce(value: string, delay: number): string { }, [value, delay]); return debouncedValue; -} +}; export default useDebounce; diff --git a/frontend/src/hooks/use-device.ts b/frontend/src/hooks/use-device.ts index cffa0335..937ec09f 100644 --- a/frontend/src/hooks/use-device.ts +++ b/frontend/src/hooks/use-device.ts @@ -1,6 +1,15 @@ import { useEffect, useState } from "react"; -export const useDevice = () => { +/** + * Custom hook to detect whether the current device is mobile based on the window width. + * + * This hook tracks the window width and returns `true` if the width is less than 768 pixels, + * indicating a mobile device, and `false` otherwise. It dynamically updates as the window is resized. + * + * @returns {boolean} isMobile - A boolean indicating whether the current device is considered mobile. + * + */ +const useDevice = () => { const [isMobile, setIsMobile] = useState(false); const handleResize = () => { @@ -18,3 +27,5 @@ export const useDevice = () => { return isMobile; }; + +export default useDevice; diff --git a/frontend/src/hooks/use-dialog.ts b/frontend/src/hooks/use-dialog.ts index db36234a..d50048b4 100644 --- a/frontend/src/hooks/use-dialog.ts +++ b/frontend/src/hooks/use-dialog.ts @@ -1,5 +1,14 @@ import { useCallback, useState } from "react"; +/** + * Custom hook to manage the state of a dialog (open/close). + * + * @returns {Object} + * - `isOpened: boolean`: A boolean indicating if the dialog is currently open. + * - `toggle: () => void`: A function to toggle the dialog's state (open/close). + * - `closeDialog: () => void`: A function to explicitly close the dialog. + * - `openDialog: () => void`: A function to explicitly open the dialog. + */ export const useDialog = () => { const [isOpened, setIsOpened] = useState(false); diff --git a/frontend/src/hooks/use-dropdown-menu.ts b/frontend/src/hooks/use-dropdown-menu.ts index 0ccad661..05be6267 100644 --- a/frontend/src/hooks/use-dropdown-menu.ts +++ b/frontend/src/hooks/use-dropdown-menu.ts @@ -1,10 +1,17 @@ import { useCallback, useMemo, useState } from "react"; /** - * This hook is to be used to handle the dropdown menu element events. - * @returns Object + * Custom hook to manage the visibility state of a dropdown menu. + * + * This hook provides a simple way to control the opening and closing of a dropdown + * menu, offering functions to show and hide the dropdown, and a memoized value + * to track its current visibility state. + * + * @returns {Object} + * - `onDropdownShow: () => void`: Function to show/open the dropdown menu. + * - `onDropdownHide: () => void`: Function to hide/close the dropdown menu. + * - `dropdownIsOpened: boolean`: A memoized boolean that represents whether the dropdown is currently opened. */ - export const useDropdownMenu = () => { const [isOpened, setIsOpened] = useState(false); const onDropdownShow = useCallback(() => { diff --git a/frontend/src/hooks/use-login.ts b/frontend/src/hooks/use-login.ts index fc340a71..aa66cf3c 100644 --- a/frontend/src/hooks/use-login.ts +++ b/frontend/src/hooks/use-login.ts @@ -6,8 +6,17 @@ import { useState } from "react"; import { useToast } from "@/app/providers/toast-provider"; /** - * This hook is to be used to handle the login button click event. It encapsulate the actions that's necessary to be performed when the login button is clicked. - * @returns Promise + * Custom hook to handle the login button click event. + * + * This hook encapsulates the actions that need to be performed when the login button is clicked, + * such as starting the OAuth login flow, handling the loading state, and showing error notifications. + * It also stores the current page's path in session storage so that the user can be redirected back + * after successful authentication. + * + * @returns {Object} + * - `loading: boolean`: Indicates whether the login process is in progress. + * - `handleLogin: () => Promise`: Function to handle the login button click event, initiating the OAuth flow. + * */ export const useLogin = () => { const location = useLocation(); diff --git a/frontend/src/hooks/use-storage.ts b/frontend/src/hooks/use-storage.ts index c3c5c6ec..99aface8 100644 --- a/frontend/src/hooks/use-storage.ts +++ b/frontend/src/hooks/use-storage.ts @@ -1,6 +1,13 @@ /** - * This hook is used to manage the retrieval, creation and deletion of objects in the localstorage. - * @returns getValue(), setValue(), removeValue(). + * Custom hook to interact with the browser's localStorage. + * + * This hook provides utility functions to get, set, and remove items from the localStorage. + * It wraps the localStorage methods with error handling to avoid potential exceptions. + * + * @returns {Object} + * - `getValue: (key: string) => string | undefined`: Retrieves the stored value for the provided key. + * - `setValue: (key: string, value: string) => void`: Saves a value for the provided key in localStorage. + * - `removeValue: (key: string) => void`: Removes the value associated with the provided key from localStorage */ export const useLocalStorage = () => { const getValue = (key: string): string | undefined => { @@ -33,8 +40,15 @@ export const useLocalStorage = () => { }; /** - * This hook is used to manage the retrieval, creation and deletion of objects in the session storage. - * @returns getValue(), setValue(), removeValue(). + * Custom hook to interact with the browser's sessionStorage. + * + * This hook provides utility functions to get, set, and remove items from the sessionStorage. + * It wraps the sessionStorage methods with error handling to avoid potential exceptions. + * + * @returns {Object} + * - `getValue: (key: string) => string | undefined`: Retrieves the stored value for the provided key. + * - `setValue: (key: string, value: string) => void`: Saves a value for the provided key in sessionStorage. + * - `removeValue: (key: string) => void`: Removes the value associated with the provided key from sessionStorage */ export const useSessionStorage = () => { const getValue = (key: string): string | undefined => { diff --git a/frontend/src/services/api-client.ts b/frontend/src/services/api-client.ts index bf552d8a..dc7d7ebf 100644 --- a/frontend/src/services/api-client.ts +++ b/frontend/src/services/api-client.ts @@ -33,7 +33,6 @@ apiClient.interceptors.response.use( console.error("Unauthorized, logging out..."); localStorage.removeItem(HOT_FAIR_LOCAL_STORAGE_ACCESS_TOKEN_KEY); } - // should we handle more errors here? return Promise.reject(error); }, ); diff --git a/frontend/src/services/react-query.ts b/frontend/src/services/react-query.ts index 94f1bb2e..111aad14 100644 --- a/frontend/src/services/react-query.ts +++ b/frontend/src/services/react-query.ts @@ -2,7 +2,6 @@ import { UseMutationOptions, DefaultOptions } from "@tanstack/react-query"; export const queryConfig = { queries: { - // throwOnError: true, refetchOnWindowFocus: false, retry: false, staleTime: 1000 * 60, diff --git a/frontend/src/styles/index.css b/frontend/src/styles/index.css index 0d3a50e0..992efcf2 100644 --- a/frontend/src/styles/index.css +++ b/frontend/src/styles/index.css @@ -51,7 +51,7 @@ body { /* 80px */ } -/* Overriding the toast design to match figma design */ +/* Overriding the toast design style begins */ sl-alert::part(close-button__base), sl-alert::part(close-button__base):hover { @@ -93,7 +93,9 @@ sl-alert.success::part(base) { border-color: var(--hot-fair-toast-success-color); } -/* Matomo placement */ +/* Overriding the toast design styles ends*/ + +/* Matomo placement style begins */ .hot-matomo { position: fixed; @@ -102,7 +104,9 @@ sl-alert.success::part(base) { width: 100vw; } -/* Toast stack placement */ +/* Matomo placement style ends */ + +/* Toast stack placement style begins */ .sl-toast-stack { left: 0; @@ -111,7 +115,9 @@ sl-alert.success::part(base) { bottom: 0; } -/* Icon styles */ +/* Toast stack placement style ends */ + +/* Icon styles begins */ @layer components { .icon { @apply inline-block h-4 w-4; @@ -121,3 +127,5 @@ sl-alert.success::part(base) { @apply inline-block h-6 w-6; } } + +/* Icon styles ends */ diff --git a/frontend/src/types/api.ts b/frontend/src/types/api.ts index e0d0cec3..48c168fc 100644 --- a/frontend/src/types/api.ts +++ b/frontend/src/types/api.ts @@ -2,7 +2,7 @@ * This file contains the different types/schema for the API responses from the backend. */ -// Auth and User types +// Auth and User API response types export type TLogin = { login_url: string; @@ -31,7 +31,7 @@ type TOSMUser = { username: string; }; -// Model types +// Models API response types export type TModel = { id: string; name: string; @@ -67,7 +67,7 @@ export type TModelDetails = TModel & { description: string; }; -// Training types +// Training API response types export type TTrainingDetails = { id: number; @@ -98,7 +98,7 @@ export type TTrainingStatus = { status: "PENDING"; traceback: string; }; -// Training workspace +// Training workspace API response types export type TrainingWorkspace = { dir: Record>; @@ -111,7 +111,7 @@ export type TTrainingFeedbacks = { results: FeatureCollection; }; -// Centroid types +// Centroid API response types export type Geometry = { type: diff --git a/frontend/src/types/common.ts b/frontend/src/types/common.ts index 28e74d2a..7b52a2a6 100644 --- a/frontend/src/types/common.ts +++ b/frontend/src/types/common.ts @@ -14,3 +14,12 @@ export type DateFilter = { export type TQueryParams = Record; export type TBadgeVariants = "green" | "red" | "yellow" | "blue" | "default"; + +export type ButtonVariant = + | "primary" + | "secondary" + | "tertiary" + | "default" + | "dark"; + +export type ButtonSize = "large" | "medium" | "small"; diff --git a/frontend/src/utils/content.ts b/frontend/src/utils/content.ts index 6d16e943..db315208 100644 --- a/frontend/src/utils/content.ts +++ b/frontend/src/utils/content.ts @@ -188,4 +188,97 @@ export const APP_CONTENT = { loginSuccess: "Login successful.", logoutSuccess: "Logout successful.", }, + models: { + modelsList: { + pageTitle: "fAIr AI models", + description: `Each model is trained using one of the training datasets. Published models can be used to find mappable features in imagery that is similar to the training areas that dataset comes from.`, + ctaButton: "Create Model", + filtersSection: { + searchPlaceHolder: "Search", + mapViewToggleText: "Map View", + }, + sortingAndPaginationSection: { + modelCountSuffix: "models", + sortingTitle: "Sort by", + }, + modelCard: { + accuracy: "Accuracy:", + lastModified: "Last Modified:", + }, + }, + modelsDetailsCard: { + modelId: "Model ID", + detailsSectionTitle: "Details", + createdBy: "Created By", + createdOn: "Created On", + lastModified: "Last Modified", + trainingId: "Training ID:", + propertiesSectionTitle: "Properties", + trainingHistorySectionTitle: "Training History", + submitTrainingRequest: "Submit a training request", + feedbacks: " Feedbacks", + startMapping: "Start Mapping", + modelDescriptionNotAvailable: "Model description is not available.", + viewTrainingArea: "View Training Area", + viewFeedbacks: "View Feedbacks", + modelFiles: "Model Files", + properties: { + zoomLevels: { + title: "Zoom Levels", + tooltip: "", + }, + epochs: { + title: "Epochs", + tooltip: "", + }, + contactSpacing: { + title: "Contact Spacing", + tooltip: "", + }, + currentDatasetSize: { + title: "Current Dataset Size", + tooltip: "", + }, + sourceImage: { + title: "Source Image (TMS)", + tooltip: "", + }, + batchSize: { + title: "Batch Size", + tooltip: "", + }, + accuracy: { + title: "Accuracy", + tooltip: "", + }, + boundaryWidth: { + title: "Boundary Width", + tooltip: "", + }, + }, + trainingHistoryTableHeader: { + trainingHistoryCount: "Training History", + id: "ID", + epochAndBatchSize: "Epochs / Batch Size", + startedAt: "Started At", + sumittedBy: "Submitted by", + duration: "Duration", + dsSize: "DS Size", + accuracy: "Accuracy (%)", + status: "Status", + info: "Info", + action: "Action", + inUse: "In Use", + }, + modelFilesDialog: { + rootDirectory: "Root Directory", + dialogTitle: "Model Files", + error: "Error loading directories.", + }, + trainingInfoDialog: { + status: "Status", + logs: "Logs", + }, + }, + }, }; diff --git a/frontend/src/utils/date-utils.ts b/frontend/src/utils/date-utils.ts index 4afdf054..75218df4 100644 --- a/frontend/src/utils/date-utils.ts +++ b/frontend/src/utils/date-utils.ts @@ -1,9 +1,41 @@ import { DateFilter } from "@/types"; +/** + * Extracts the date part from an ISO 8601 date-time string. + * + * This function takes an ISO date-time string (e.g., "2024-01-01T12:00:00Z") + * and splits it at the "T" character to isolate the date portion. + * It returns the date part in the format "YYYY-MM-DD". + * + * @param {string} isoString - The ISO date-time string to extract the date from. + * @returns {string} - The extracted date part in "YYYY-MM-DD" format. + */ export const extractDatePart = (isoString: string) => { return isoString.split("T")[0]; }; +/** + * Constructs a query string object for filtering records based on date range. + * + * This function takes an optional date filter and two optional date strings + * (startDate and endDate) and builds a record of query parameters to be used + * in an API request. The resulting query parameters will use the API value of + * the selected filter along with `__gte` (greater than or equal) and `__lte` + * (less than or equal) suffixes for the respective dates. + * + * - If `startDate` is provided, it adds a query parameter for the lower bound. + * - If `endDate` is provided, it adds a query parameter for the upper bound. + * - If neither date is provided, an empty object is returned. + * + * @param {DateFilter} [selectedFilter] - The filter object containing the API value. + * @param {string} [startDate] - The starting date string in ISO format. + * @param {string} [endDate] - The ending date string in ISO format. + * @returns {Record} - An object containing the query parameters for filtering. + * + * Example usage: + * const query = buildDateFilterQueryString(selectedFilter, '2024-01-01', '2024-12-31'); + * Output: { "filterField__gte": "2024-01-01", "filterField__lte": "2024-12-31" } + */ export const buildDateFilterQueryString = ( selectedFilter?: DateFilter, startDate?: string, @@ -33,6 +65,15 @@ export const formatDate = (isoString: string): string => { return `${day}/${month}/${year}, ${hours}:${minutes}:${seconds}`; }; +/** + * Formats the duration between two Date objects (startDate and endDate) into a human-readable string. + * + * The function calculates the absolute difference between the two dates and converts it into hours, minutes, and seconds. + * It then formats the duration into a string such as "Xhr Y Mins Z Secs" depending on which time units are present. + * @param {Date} startDate - The starting date and time. + * @param {Date} endDate - The ending date and time. + * @returns {string} - The formatted duration string (e.g., "2hr 15 Mins 30 Secs"). + */ export const formatDuration = (startDate: Date, endDate: Date): string => { const diff = Math.abs(endDate.getTime() - startDate.getTime()); diff --git a/frontend/src/utils/index.ts b/frontend/src/utils/index.ts index a2feb02d..80451aa3 100644 --- a/frontend/src/utils/index.ts +++ b/frontend/src/utils/index.ts @@ -1,7 +1,6 @@ export * from "./cn"; export * from "./constants"; export * from "./content"; -export * from "./types"; export * from "./date-utils"; export * from "./number-utils"; export * from "./string-utils"; diff --git a/frontend/src/utils/number-utils.ts b/frontend/src/utils/number-utils.ts index 0371e5d8..0d03b7aa 100644 --- a/frontend/src/utils/number-utils.ts +++ b/frontend/src/utils/number-utils.ts @@ -1,3 +1,14 @@ +/** + * Rounds a number to a specified number of decimal places. + * + * This function takes a number and rounds it to a defined number of decimal places, + * returning it as a string. By default, it rounds to two decimal places, but this can + * be adjusted by providing a different value for the `round` parameter. + * + * @param {number} num - The number to be rounded. + * @param {number} [round=2] - The number of decimal places to round to (default is 2). + * @returns {string} - The rounded number as a string. + */ export const roundNumber = (num: number, round: number = 2) => { return num.toFixed(round) ?? 0; }; diff --git a/frontend/src/utils/string-utils.ts b/frontend/src/utils/string-utils.ts index 3e90ed0d..e774cca8 100644 --- a/frontend/src/utils/string-utils.ts +++ b/frontend/src/utils/string-utils.ts @@ -1,3 +1,14 @@ +/** + * Truncates a string to a specified maximum length, appending ellipsis if truncated. + * + * This function takes a string and a maximum length, and if the string exceeds + * the specified length, it truncates the string and appends "..." to indicate that + * it has been shortened. The default maximum length is set to 30 characters. + * + * @param {string} [string] - The string to be truncated (optional). + * @param {number} [maxLength=30] - The maximum length for the string (default is 30). + * @returns {string | undefined} - The truncated string with ellipsis, or the original string if within limit. + */ export const truncateString = (string?: string, maxLength: number = 30) => { if (string && string.length > maxLength) { return `${string.slice(0, maxLength - 3)}...`; diff --git a/frontend/src/utils/types.ts b/frontend/src/utils/types.ts deleted file mode 100644 index 2cd802de..00000000 --- a/frontend/src/utils/types.ts +++ /dev/null @@ -1 +0,0 @@ -export interface IconProps extends React.SVGProps {}