From d673876c9ff3765c45d0e3b3251e422033518720 Mon Sep 17 00:00:00 2001 From: Pete Gadomski Date: Thu, 4 Dec 2025 10:54:38 -0700 Subject: [PATCH 1/2] refactor: move things to hooks, add tests --- .gitignore | 2 +- src/app.tsx | 110 ++------------- src/hooks/document-title.ts | 12 ++ src/hooks/href-param.ts | 55 ++++++++ src/hooks/stac-filters.ts | 53 ++++++++ src/utils/stac.ts | 14 ++ tests/hooks/document-title.spec.ts | 201 +++++++++++++++++++++++++++ tests/hooks/href-param.spec.ts | 69 ++++++++++ tests/hooks/stac-filters.spec.ts | 211 +++++++++++++++++++++++++++++ 9 files changed, 630 insertions(+), 97 deletions(-) create mode 100644 src/hooks/document-title.ts create mode 100644 src/hooks/href-param.ts create mode 100644 src/hooks/stac-filters.ts create mode 100644 tests/hooks/document-title.spec.ts create mode 100644 tests/hooks/href-param.spec.ts create mode 100644 tests/hooks/stac-filters.spec.ts diff --git a/.gitignore b/.gitignore index 7781638..2d7b7c0 100644 --- a/.gitignore +++ b/.gitignore @@ -22,5 +22,5 @@ dist-ssr *.njsproj *.sln *.sw? -tests/__screenshots__/ +tests/**/__screenshots__/ codebook.toml diff --git a/src/app.tsx b/src/app.tsx index cf4b09c..fad36dd 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -1,21 +1,17 @@ -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useState } from "react"; import { Box, Container, FileUpload, useFileUpload } from "@chakra-ui/react"; import type { StacCollection, StacItem } from "stac-ts"; import { Toaster } from "./components/ui/toaster"; +import useDocumentTitle from "./hooks/document-title"; +import useHrefParam from "./hooks/href-param"; import useStacChildren from "./hooks/stac-children"; +import useStacFilters from "./hooks/stac-filters"; import useStacValue from "./hooks/stac-value"; import Map from "./layers/map"; import Overlay from "./layers/overlay"; import type { BBox2D, Color } from "./types/map"; import type { DatetimeBounds, StacValue } from "./types/stac"; -import { - isCog, - isCollectionInBbox, - isCollectionInDatetimeBounds, - isItemInBbox, - isItemInDatetimeBounds, - isVisual, -} from "./utils/stac"; +import { getCogTileHref } from "./utils/stac"; // TODO make this configurable by the user. const lineColor: Color = [207, 63, 2, 100]; @@ -23,8 +19,8 @@ const fillColor: Color = [207, 63, 2, 50]; export default function App() { // User state - const [href, setHref] = useState(getInitialHref()); const fileUpload = useFileUpload({ maxFiles: 1 }); + const { href, setHref } = useHrefParam(fileUpload); const [userCollections, setCollections] = useState(); const [userItems, setItems] = useState(); const [picked, setPicked] = useState(); @@ -54,77 +50,22 @@ export default function App() { }); const collections = collectionsLink ? userCollections : linkedCollections; const items = userItems || linkedItems; - const filteredCollections = useMemo(() => { - if (filter && collections) { - return collections.filter( - (collection) => - (!bbox || isCollectionInBbox(collection, bbox)) && - (!datetimeBounds || - isCollectionInDatetimeBounds(collection, datetimeBounds)) - ); - } else { - return undefined; - } - }, [collections, filter, bbox, datetimeBounds]); - const filteredItems = useMemo(() => { - if (filter && items) { - return items.filter( - (item) => - (!bbox || isItemInBbox(item, bbox)) && - (!datetimeBounds || isItemInDatetimeBounds(item, datetimeBounds)) - ); - } else { - return undefined; - } - }, [items, filter, bbox, datetimeBounds]); + const { filteredCollections, filteredItems } = useStacFilters({ + collections, + items, + filter, + bbox, + datetimeBounds, + }); // Effects - useEffect(() => { - function handlePopState() { - setHref(new URLSearchParams(location.search).get("href") ?? ""); - } - window.addEventListener("popstate", handlePopState); - - const href = new URLSearchParams(location.search).get("href"); - if (href) { - try { - new URL(href); - } catch { - history.pushState(null, "", location.pathname); - } - } - - return () => { - window.removeEventListener("popstate", handlePopState); - }; - }, []); - - useEffect(() => { - if (href && new URLSearchParams(location.search).get("href") != href) { - history.pushState(null, "", "?href=" + href); - } else if (href === "") { - history.pushState(null, "", location.pathname); - } - }, [href]); - - useEffect(() => { - // It should never be more than 1. - if (fileUpload.acceptedFiles.length == 1) { - setHref(fileUpload.acceptedFiles[0].name); - } - }, [fileUpload.acceptedFiles]); + useDocumentTitle(value); useEffect(() => { setPicked(undefined); setItems(undefined); setDatetimeBounds(undefined); setCogTileHref(value && getCogTileHref(value)); - - if (value && (value.title || value.id)) { - document.title = "stac-map | " + (value.title || value.id); - } else { - document.title = "stac-map"; - } }, [value]); useEffect(() => { @@ -201,26 +142,3 @@ export default function App() { ); } - -function getInitialHref() { - const href = new URLSearchParams(location.search).get("href") || ""; - try { - new URL(href); - } catch { - return undefined; - } - return href; -} - -function getCogTileHref(value: StacValue) { - let cogTileHref = undefined; - if (value.assets) { - for (const asset of Object.values(value.assets)) { - if (isCog(asset) && isVisual(asset)) { - cogTileHref = asset.href as string; - break; - } - } - } - return cogTileHref; -} diff --git a/src/hooks/document-title.ts b/src/hooks/document-title.ts new file mode 100644 index 0000000..8bb999d --- /dev/null +++ b/src/hooks/document-title.ts @@ -0,0 +1,12 @@ +import { useEffect } from "react"; +import type { StacValue } from "../types/stac"; + +export default function useDocumentTitle(value: StacValue | undefined) { + useEffect(() => { + if (value && (value.title || value.id)) { + document.title = "stac-map | " + (value.title || value.id); + } else { + document.title = "stac-map"; + } + }, [value]); +} diff --git a/src/hooks/href-param.ts b/src/hooks/href-param.ts new file mode 100644 index 0000000..b71d222 --- /dev/null +++ b/src/hooks/href-param.ts @@ -0,0 +1,55 @@ +import { useEffect, useState } from "react"; +import type { UseFileUploadReturn } from "@chakra-ui/react"; + +function getInitialHref(): string | undefined { + const href = new URLSearchParams(location.search).get("href") || ""; + try { + new URL(href); + } catch { + return undefined; + } + return href; +} + +export default function useHrefParam(fileUpload: UseFileUploadReturn) { + const [href, setHref] = useState(getInitialHref()); + + // Sync href with URL params + useEffect(() => { + if (href && new URLSearchParams(location.search).get("href") != href) { + history.pushState(null, "", "?href=" + href); + } else if (href === "") { + history.pushState(null, "", location.pathname); + } + }, [href]); + + // Handle browser back/forward + useEffect(() => { + function handlePopState() { + setHref(new URLSearchParams(location.search).get("href") ?? ""); + } + window.addEventListener("popstate", handlePopState); + + const href = new URLSearchParams(location.search).get("href"); + if (href) { + try { + new URL(href); + } catch { + history.pushState(null, "", location.pathname); + } + } + + return () => { + window.removeEventListener("popstate", handlePopState); + }; + }, []); + + // Handle file uploads + useEffect(() => { + if (fileUpload.acceptedFiles.length == 1) { + setHref(fileUpload.acceptedFiles[0].name); + } + }, [fileUpload.acceptedFiles]); + + return { href, setHref }; +} diff --git a/src/hooks/stac-filters.ts b/src/hooks/stac-filters.ts new file mode 100644 index 0000000..3bfdba2 --- /dev/null +++ b/src/hooks/stac-filters.ts @@ -0,0 +1,53 @@ +import { useMemo } from "react"; +import type { StacCollection, StacItem } from "stac-ts"; +import type { BBox2D } from "../types/map"; +import type { DatetimeBounds } from "../types/stac"; +import { + isCollectionInBbox, + isCollectionInDatetimeBounds, + isItemInBbox, + isItemInDatetimeBounds, +} from "../utils/stac"; + +interface UseStacFiltersProps { + collections?: StacCollection[]; + items?: StacItem[]; + filter: boolean; + bbox?: BBox2D; + datetimeBounds?: DatetimeBounds; +} + +export default function useStacFilters({ + collections, + items, + filter, + bbox, + datetimeBounds, +}: UseStacFiltersProps) { + const filteredCollections = useMemo(() => { + if (filter && collections) { + return collections.filter( + (collection) => + (!bbox || isCollectionInBbox(collection, bbox)) && + (!datetimeBounds || + isCollectionInDatetimeBounds(collection, datetimeBounds)) + ); + } else { + return undefined; + } + }, [collections, filter, bbox, datetimeBounds]); + + const filteredItems = useMemo(() => { + if (filter && items) { + return items.filter( + (item) => + (!bbox || isItemInBbox(item, bbox)) && + (!datetimeBounds || isItemInDatetimeBounds(item, datetimeBounds)) + ); + } else { + return undefined; + } + }, [items, filter, bbox, datetimeBounds]); + + return { filteredCollections, filteredItems }; +} diff --git a/src/utils/stac.ts b/src/utils/stac.ts index 57ed0b6..e8f241e 100644 --- a/src/utils/stac.ts +++ b/src/utils/stac.ts @@ -267,3 +267,17 @@ export function isVisual(asset: StacAsset) { } return false; } + +export function getCogTileHref(value: StacValue): string | undefined { + if (!value.assets) { + return undefined; + } + + for (const asset of Object.values(value.assets)) { + if (isCog(asset) && isVisual(asset)) { + return asset.href as string; + } + } + + return undefined; +} diff --git a/tests/hooks/document-title.spec.ts b/tests/hooks/document-title.spec.ts new file mode 100644 index 0000000..fda04f8 --- /dev/null +++ b/tests/hooks/document-title.spec.ts @@ -0,0 +1,201 @@ +import type { StacCatalog, StacCollection, StacItem } from "stac-ts"; +import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import type { StacItemCollection, StacValue } from "../../src/types/stac"; + +describe("useDocumentTitle logic", () => { + let originalTitle: string; + + beforeEach(() => { + if (typeof document !== "undefined") { + originalTitle = document.title; + } + }); + + afterEach(() => { + if (typeof document !== "undefined") { + document.title = originalTitle; + } + }); + + function setDocumentTitle(value: StacValue | undefined) { + if (value && (value.title || value.id)) { + document.title = "stac-map | " + (value.title || value.id); + } else { + document.title = "stac-map"; + } + } + + test("should set default title when value is undefined", () => { + setDocumentTitle(undefined); + + expect(document.title).toBe("stac-map"); + }); + + test("should set title with catalog title", () => { + const catalog: StacCatalog = { + id: "test-catalog", + type: "Catalog", + title: "Test Catalog", + description: "A test catalog", + stac_version: "1.0.0", + links: [], + }; + + setDocumentTitle(catalog); + + expect(document.title).toBe("stac-map | Test Catalog"); + }); + + test("should set title with catalog id when title is missing", () => { + const catalog: StacCatalog = { + id: "test-catalog", + type: "Catalog", + description: "A test catalog", + stac_version: "1.0.0", + links: [], + }; + + setDocumentTitle(catalog); + + expect(document.title).toBe("stac-map | test-catalog"); + }); + + test("should set title with collection title", () => { + const collection: StacCollection = { + id: "test-collection", + type: "Collection", + title: "Test Collection", + description: "A test collection", + stac_version: "1.0.0", + license: "MIT", + extent: { + spatial: { + bbox: [[-180, -90, 180, 90]], + }, + temporal: { + interval: [[null, null]], + }, + }, + links: [], + }; + + setDocumentTitle(collection); + + expect(document.title).toBe("stac-map | Test Collection"); + }); + + test("should set title with item title", () => { + const item: StacItem = { + id: "test-item", + type: "Feature", + title: "Test Item", + stac_version: "1.0.0", + geometry: { + type: "Point", + coordinates: [0, 0], + }, + properties: { + datetime: "2020-01-01T00:00:00Z", + }, + links: [], + assets: {}, + }; + + setDocumentTitle(item); + + expect(document.title).toBe("stac-map | Test Item"); + }); + + test("should set title with item collection title", () => { + const itemCollection: StacItemCollection = { + type: "FeatureCollection", + title: "Test Item Collection", + features: [], + }; + + setDocumentTitle(itemCollection); + + expect(document.title).toBe("stac-map | Test Item Collection"); + }); + + test("should set title with item collection id when title is missing", () => { + const itemCollection: StacItemCollection = { + type: "FeatureCollection", + id: "test-item-collection", + features: [], + }; + + setDocumentTitle(itemCollection); + + expect(document.title).toBe("stac-map | test-item-collection"); + }); + + test("should prefer title over id when both are present", () => { + const catalog: StacCatalog = { + id: "catalog-id", + type: "Catalog", + title: "Catalog Title", + description: "A test catalog", + stac_version: "1.0.0", + links: [], + }; + + setDocumentTitle(catalog); + + expect(document.title).toBe("stac-map | Catalog Title"); + }); + + test("should handle value without title or id", () => { + const itemCollection: StacItemCollection = { + type: "FeatureCollection", + features: [], + }; + + setDocumentTitle(itemCollection); + + expect(document.title).toBe("stac-map"); + }); + + test("should update title when value changes", () => { + const catalog1: StacCatalog = { + id: "catalog1", + type: "Catalog", + title: "First Catalog", + description: "First catalog", + stac_version: "1.0.0", + links: [], + }; + + const catalog2: StacCatalog = { + id: "catalog2", + type: "Catalog", + title: "Second Catalog", + description: "Second catalog", + stac_version: "1.0.0", + links: [], + }; + + setDocumentTitle(catalog1); + expect(document.title).toBe("stac-map | First Catalog"); + + setDocumentTitle(catalog2); + expect(document.title).toBe("stac-map | Second Catalog"); + }); + + test("should reset to default title when value becomes undefined", () => { + const catalog: StacCatalog = { + id: "test-catalog", + type: "Catalog", + title: "Test Catalog", + description: "A test catalog", + stac_version: "1.0.0", + links: [], + }; + + setDocumentTitle(catalog); + expect(document.title).toBe("stac-map | Test Catalog"); + + setDocumentTitle(undefined); + expect(document.title).toBe("stac-map"); + }); +}); diff --git a/tests/hooks/href-param.spec.ts b/tests/hooks/href-param.spec.ts new file mode 100644 index 0000000..b396383 --- /dev/null +++ b/tests/hooks/href-param.spec.ts @@ -0,0 +1,69 @@ +import { beforeEach, describe, expect, test } from "vitest"; + +// Test the getInitialHref logic by importing and testing the hook behavior +describe("useHrefParam", () => { + beforeEach(() => { + // Reset URL to clean state + if (typeof window !== "undefined") { + history.replaceState({}, "", "/"); + } + }); + + test("should parse valid URL from href parameter", () => { + const href = "https://example.com/catalog.json"; + const url = new URL(`http://localhost/?href=${href}`); + const params = new URLSearchParams(url.search); + const parsedHref = params.get("href") || ""; + + let isValid = false; + try { + new URL(parsedHref); + isValid = true; + } catch { + isValid = false; + } + + expect(isValid).toBe(true); + expect(parsedHref).toBe(href); + }); + + test("should reject invalid URLs in href parameter", () => { + const href = "not-a-valid-url"; + const url = new URL(`http://localhost/?href=${href}`); + const params = new URLSearchParams(url.search); + const parsedHref = params.get("href") || ""; + + let isValid = false; + try { + new URL(parsedHref); + isValid = true; + } catch { + isValid = false; + } + + expect(isValid).toBe(false); + }); + + test("should handle empty href parameter", () => { + const url = new URL(`http://localhost/`); + const params = new URLSearchParams(url.search); + const parsedHref = params.get("href") || ""; + + expect(parsedHref).toBe(""); + }); + + test("should construct proper query string with href", () => { + const href = "https://example.com/catalog.json"; + const queryString = "?href=" + href; + + expect(queryString).toBe("?href=https://example.com/catalog.json"); + }); + + test("should handle file upload state", () => { + const mockFile = { name: "test.json" }; + const acceptedFiles = [mockFile]; + + expect(acceptedFiles.length).toBe(1); + expect(acceptedFiles[0].name).toBe("test.json"); + }); +}); diff --git a/tests/hooks/stac-filters.spec.ts b/tests/hooks/stac-filters.spec.ts new file mode 100644 index 0000000..179ce61 --- /dev/null +++ b/tests/hooks/stac-filters.spec.ts @@ -0,0 +1,211 @@ +import type { StacCollection, StacItem } from "stac-ts"; +import { describe, expect, test } from "vitest"; +import type { BBox2D } from "../../src/types/map"; +import type { DatetimeBounds } from "../../src/types/stac"; +import { + isCollectionInBbox, + isCollectionInDatetimeBounds, + isItemInBbox, + isItemInDatetimeBounds, +} from "../../src/utils/stac"; + +describe("useStacFilters logic", () => { + const mockCollection1: StacCollection = { + id: "collection1", + type: "Collection", + stac_version: "1.0.0", + description: "Test collection 1", + license: "MIT", + extent: { + spatial: { + bbox: [[-180, -90, 180, 90]], + }, + temporal: { + interval: [["2020-01-01T00:00:00Z", "2020-12-31T23:59:59Z"]], + }, + }, + links: [], + }; + + const mockCollection2: StacCollection = { + id: "collection2", + type: "Collection", + stac_version: "1.0.0", + description: "Test collection 2", + license: "MIT", + extent: { + spatial: { + bbox: [[0, 0, 10, 10]], + }, + temporal: { + interval: [["2021-01-01T00:00:00Z", "2021-12-31T23:59:59Z"]], + }, + }, + links: [], + }; + + const mockItem1: StacItem = { + id: "item1", + type: "Feature", + stac_version: "1.0.0", + geometry: { + type: "Point", + coordinates: [0, 0], + }, + bbox: [-1, -1, 1, 1], + properties: { + datetime: "2020-06-15T00:00:00Z", + }, + links: [], + assets: {}, + }; + + const mockItem2: StacItem = { + id: "item2", + type: "Feature", + stac_version: "1.0.0", + geometry: { + type: "Point", + coordinates: [50, 50], + }, + bbox: [49, 49, 51, 51], + properties: { + datetime: "2021-06-15T00:00:00Z", + }, + links: [], + assets: {}, + }; + + test("should filter collections by bbox", () => { + const bbox: BBox2D = [0, 0, 10, 10]; + + const filtered = [mockCollection1, mockCollection2].filter((collection) => + isCollectionInBbox(collection, bbox) + ); + + // collection1 has global bbox, collection2 overlaps with the filter bbox + expect(filtered.length).toBe(1); + expect(filtered.map((c) => c.id)).toContain("collection2"); + }); + + test("should filter collections by datetime bounds", () => { + const datetimeBounds: DatetimeBounds = { + start: new Date("2020-01-01"), + end: new Date("2020-12-31"), + }; + + const filtered = [mockCollection1, mockCollection2].filter((collection) => + isCollectionInDatetimeBounds(collection, datetimeBounds) + ); + + expect(filtered.length).toBe(1); + expect(filtered[0].id).toBe("collection1"); + }); + + test("should filter collections by both bbox and datetime bounds", () => { + const bbox: BBox2D = [0, 0, 10, 10]; + const datetimeBounds: DatetimeBounds = { + start: new Date("2021-01-01"), + end: new Date("2021-12-31"), + }; + + const filtered = [mockCollection1, mockCollection2].filter( + (collection) => + isCollectionInBbox(collection, bbox) && + isCollectionInDatetimeBounds(collection, datetimeBounds) + ); + + expect(filtered.length).toBe(1); + expect(filtered[0].id).toBe("collection2"); + }); + + test("should filter items by bbox", () => { + const bbox: BBox2D = [-5, -5, 5, 5]; + + const filtered = [mockItem1, mockItem2].filter((item) => + isItemInBbox(item, bbox) + ); + + expect(filtered.length).toBe(1); + expect(filtered[0].id).toBe("item1"); + }); + + test("should filter items by datetime bounds", () => { + const datetimeBounds: DatetimeBounds = { + start: new Date("2020-01-01"), + end: new Date("2020-12-31"), + }; + + const filtered = [mockItem1, mockItem2].filter((item) => + isItemInDatetimeBounds(item, datetimeBounds) + ); + + expect(filtered.length).toBe(1); + expect(filtered[0].id).toBe("item1"); + }); + + test("should filter items by both bbox and datetime bounds", () => { + const bbox: BBox2D = [-5, -5, 5, 5]; + const datetimeBounds: DatetimeBounds = { + start: new Date("2020-01-01"), + end: new Date("2020-12-31"), + }; + + const filtered = [mockItem1, mockItem2].filter( + (item) => + isItemInBbox(item, bbox) && isItemInDatetimeBounds(item, datetimeBounds) + ); + + expect(filtered.length).toBe(1); + expect(filtered[0].id).toBe("item1"); + }); + + test("should return empty array when no items match filters", () => { + const bbox: BBox2D = [100, 100, 110, 110]; + + const filtered = [mockItem1, mockItem2].filter((item) => + isItemInBbox(item, bbox) + ); + + expect(filtered.length).toBe(0); + }); + + test("should handle global bbox (360 degrees wide)", () => { + const globalBbox: BBox2D = [-180, -90, 180, 90]; + + const filteredCollections = [mockCollection1, mockCollection2].filter( + (collection) => isCollectionInBbox(collection, globalBbox) + ); + + const filteredItems = [mockItem1, mockItem2].filter((item) => + isItemInBbox(item, globalBbox) + ); + + // Global bbox should include all collections and items + expect(filteredCollections.length).toBe(2); + expect(filteredItems.length).toBe(2); + }); + + test("should not filter when filter is false (logic test)", () => { + const filter = false; + const collections = [mockCollection1, mockCollection2]; + + const result = filter ? collections.filter(() => true) : undefined; + + expect(result).toBeUndefined(); + }); + + test("should filter when filter is true (logic test)", () => { + const filter = true; + const collections = [mockCollection1, mockCollection2]; + const bbox: BBox2D = [0, 0, 10, 10]; + + const result = filter + ? collections.filter((c) => isCollectionInBbox(c, bbox)) + : undefined; + + expect(result).toBeDefined(); + expect(result?.length).toBe(1); + expect(result?.[0].id).toBe("collection2"); + }); +}); From 115e55a4c07cc29cc76a58b60371d9794cf473e0 Mon Sep 17 00:00:00 2001 From: Pete Gadomski Date: Wed, 10 Dec 2025 17:38:26 -0700 Subject: [PATCH 2/2] fix: use onFileChange --- src/app.tsx | 11 +++++++++-- src/hooks/href-param.ts | 10 +--------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/app.tsx b/src/app.tsx index fad36dd..c44bbbb 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -19,8 +19,15 @@ const fillColor: Color = [207, 63, 2, 50]; export default function App() { // User state - const fileUpload = useFileUpload({ maxFiles: 1 }); - const { href, setHref } = useHrefParam(fileUpload); + const { href, setHref } = useHrefParam(); + const fileUpload = useFileUpload({ + maxFiles: 1, + onFileChange: (details) => { + if (details.acceptedFiles.length === 1) { + setHref(details.acceptedFiles[0].name); + } + }, + }); const [userCollections, setCollections] = useState(); const [userItems, setItems] = useState(); const [picked, setPicked] = useState(); diff --git a/src/hooks/href-param.ts b/src/hooks/href-param.ts index b71d222..c7fa69c 100644 --- a/src/hooks/href-param.ts +++ b/src/hooks/href-param.ts @@ -1,5 +1,4 @@ import { useEffect, useState } from "react"; -import type { UseFileUploadReturn } from "@chakra-ui/react"; function getInitialHref(): string | undefined { const href = new URLSearchParams(location.search).get("href") || ""; @@ -11,7 +10,7 @@ function getInitialHref(): string | undefined { return href; } -export default function useHrefParam(fileUpload: UseFileUploadReturn) { +export default function useHrefParam() { const [href, setHref] = useState(getInitialHref()); // Sync href with URL params @@ -44,12 +43,5 @@ export default function useHrefParam(fileUpload: UseFileUploadReturn) { }; }, []); - // Handle file uploads - useEffect(() => { - if (fileUpload.acceptedFiles.length == 1) { - setHref(fileUpload.acceptedFiles[0].name); - } - }, [fileUpload.acceptedFiles]); - return { href, setHref }; }