diff --git a/__tests__/toolkitAdd.test.tsx b/__tests__/toolkitAdd.test.tsx index 247adcbc..1cb90229 100644 --- a/__tests__/toolkitAdd.test.tsx +++ b/__tests__/toolkitAdd.test.tsx @@ -3,23 +3,12 @@ import { screen, fireEvent, waitFor, - act, } from "@testing-library/react"; -import AddToolPage from "@/app/toolkit/add-tool/page"; +import Inputs from "@/app/toolkit/add-tool/components/AddToolInputs"; // Adjust the path if needed import { AddToolProvider } from "@/context/AddToolContext"; -import { validateUrl } from "@/lib/utils/validateUrl"; +import AddToolPage from "@/app/toolkit/add-tool/page"; import { useDatabase } from "@/context/DatabaseContext"; - -jest.mock("next/navigation", () => ({ - useRouter: jest.fn(() => ({ - push: jest.fn(), - back: jest.fn(), - forward: jest.fn(), - refresh: jest.fn(), - prefetch: jest.fn(), - })), - usePathname: jest.fn(() => "/addTool"), -})); +import { validateUrl } from "@/lib/utils/validateUrl"; jest.mock("@/lib/utils/validateUrl", () => ({ validateUrl: jest.fn(), @@ -29,6 +18,46 @@ jest.mock("@/context/DatabaseContext", () => ({ useDatabase: jest.fn(), })); +jest.mock("next/navigation", () => ({ + useRouter: jest.fn(() => ({ + push: jest.fn(), + })), +})); + +describe("Inputs Component", () => { + const mockDatabase = { + addToDb: jest.fn(), + addCategories: jest.fn(), + getFromDb: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + (useDatabase as jest.Mock).mockReturnValue(mockDatabase); + (validateUrl as jest.Mock).mockImplementation(() => ({ + isValid: true, + url: "https://test.com", + })); + }); + + it("renders all form components", () => { + render( + + + + ); + + expect(screen.getByText("Name")).toBeInTheDocument(); + expect(screen.getByText("Tags")).toBeInTheDocument(); + expect(screen.getByText("Description")).toBeInTheDocument(); + expect(screen.getByText("Image URL")).toBeInTheDocument(); + expect(screen.getByText("Link")).toBeInTheDocument(); + expect( + screen.getByRole("button", { name: "Add Tool" }) + ).toBeInTheDocument(); + }); +}); + describe("AddToolInputs Component", () => { const mockDatabase = { getFromDb: jest.fn(), @@ -64,7 +93,7 @@ describe("AddToolInputs Component", () => { ).toBeInTheDocument(); }); - describe("AddToolTags Component", () => { +describe("AddToolTags Component", () => { it("renders existing categories", async () => { mockDatabase.getFromDb.mockResolvedValue([ { name: "Category 1" }, @@ -119,74 +148,4 @@ describe("AddToolInputs Component", () => { fireEvent.change(nameInput, { target: { value: "Test Tool" } }); expect(nameInput.value).toBe("Test Tool"); }); - - it("validates URLs correctly", async () => { - mockDatabase.getFromDb.mockResolvedValue([{ name: "Category 1" }]); - - (validateUrl as jest.Mock).mockImplementationOnce(() => ({ - isValid: false, - error: "Invalid URL", - })); - - render( - - - - ); - - await waitFor(() => { - expect(screen.getByText("Category 1")).toBeInTheDocument(); - }); - fireEvent.click(screen.getByText("Category 1")); - - const infoUrlInput = screen.getByRole("textbox", { name: "Link" }); - fireEvent.change(infoUrlInput, { target: { value: "invalid-url" } }); - - const submitButton = screen.getByRole("button", { name: "Add Tool" }); - await act(async () => { - fireEvent.click(submitButton); - }); - - await waitFor(() => { - expect(screen.getByText("Invalid URL")).toBeInTheDocument(); - }); - }); - - it("inserts data into the database", async () => { - mockDatabase.getFromDb.mockResolvedValue([{ name: "Category 1" }]); - - render( - - - - ); - - await waitFor(() => { - expect(screen.getByText("Category 1")).toBeInTheDocument(); - }); - fireEvent.click(screen.getByText("Category 1")); - - const inputs = screen.getAllByRole("textbox"); - const nameInput = inputs[0] as HTMLInputElement; - const infoUrlInput = screen.getByRole("textbox", { name: "Link" }); - - fireEvent.change(nameInput, { target: { value: "Test Tool" } }); - fireEvent.change(infoUrlInput, { target: { value: "https://test.com" } }); - - const submitButton = screen.getByRole("button", { name: "Add Tool" }); - - await act(async () => { - fireEvent.click(submitButton); - }); - - await waitFor(() => { - expect(mockDatabase.addToDb).toHaveBeenCalledWith( - "toolkit_items", - expect.objectContaining({ - name: "Test Tool", - infoUrl: "https://test.com", - }) - ); - }); - }); }); diff --git a/src/app/needs/info/page.tsx b/src/app/needs/info/page.tsx index 532f3b07..8fd30bc5 100644 --- a/src/app/needs/info/page.tsx +++ b/src/app/needs/info/page.tsx @@ -63,7 +63,7 @@ export default function NeedsInfoPage() { Learn More Here diff --git a/src/app/toolkit/add-tool/components/AddToolInputs.tsx b/src/app/toolkit/add-tool/components/AddToolInputs.tsx index 66a9d651..0bb4b7c3 100644 --- a/src/app/toolkit/add-tool/components/AddToolInputs.tsx +++ b/src/app/toolkit/add-tool/components/AddToolInputs.tsx @@ -25,12 +25,18 @@ export default function Inputs() { const [categoryErrorModal, setCategoryErrorModal] = useState(false); const [infoUrlErrorModal, setInfoUrlErrorModal] = useState(false); const [imageUrlErrorModal, setImageUrlErrorModal] = useState(false); + const [nameErrorModalOpen, setNameErrorModalOpen] = useState(false); const [submitErrorModal, setSubmitErrorModal] = useState(false); const [submitErrorMessage, setSubmitErrorMessage] = useState(""); function SubmitButton() { const handleSubmit = async () => { console.log(`Validating form with state: ${JSON.stringify(formState)}`); + + if (!formState.name || formState.name.trim() === "") { + setNameErrorModalOpen(true); + return; + } if (formState.categories.length === 0) { setCategoryErrorModal(true); @@ -133,11 +139,21 @@ export default function Inputs() { }} /> + {/* Modal for missing name */} + setNameErrorModalOpen(false), + }} + /> + { setSaveUnusedCategory(true); setUnusedCategoryModalOpen(false); @@ -153,7 +169,7 @@ export default function Inputs() { /> ); diff --git a/src/app/toolkit/add-tool/components/AddToolTags.tsx b/src/app/toolkit/add-tool/components/AddToolTags.tsx index bf74aaa0..a7fa4e68 100644 --- a/src/app/toolkit/add-tool/components/AddToolTags.tsx +++ b/src/app/toolkit/add-tool/components/AddToolTags.tsx @@ -99,7 +99,7 @@ export default function AddTags() { @@ -108,8 +108,8 @@ export default function AddTags() { cat.name)); } else { setCategories([]); + } }; fetchCategories(); @@ -59,7 +61,7 @@ export default function CategoriesBar({ diff --git a/src/app/toolkit/components/SortableItem.tsx b/src/app/toolkit/components/SortableItem.tsx index ebb61610..4e5ccf5f 100644 --- a/src/app/toolkit/components/SortableItem.tsx +++ b/src/app/toolkit/components/SortableItem.tsx @@ -114,8 +114,9 @@ export default function SortableItem({ title="Following this link will leave the app. Do you want to continue?" forwardButton={{ action: () => { - const link = item.infoUrl; // Replace with your link - window.open(link, "_blank"); // Opens in a new tab + const link = item.infoUrl; + window.open(link, "_blank"); + setIsLinkModalOpen(false); }, label: "Yes", }} diff --git a/src/app/toolkit/components/ToolList.tsx b/src/app/toolkit/components/ToolList.tsx index 3b950999..20872990 100644 --- a/src/app/toolkit/components/ToolList.tsx +++ b/src/app/toolkit/components/ToolList.tsx @@ -25,13 +25,12 @@ export interface ToolkitComponentData { export default function ToolkitList() { const database = useDatabase(); const [mainData, setMainData] = useState([]); - const [displayedData, setDisplayedData] = useState( - [] - ); + const [displayedData, setDisplayedData] = useState([]); const [searchQuery, setSearchQuery] = useState(""); + const [dragOrder, setDragOrder] = useState([]); const { selectedCategories } = useToolkit(); - // Fetch data from the database + useEffect(() => { const fetchData = async () => { try { @@ -42,6 +41,7 @@ export default function ToolkitList() { ); setMainData(toolkitData); setDisplayedData(toolkitData); + setDragOrder(toolkitData.map((item) => item.id)); } else { console.log("No items found in toolkit_items collection."); } @@ -53,28 +53,29 @@ export default function ToolkitList() { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + // Toggle item `checked` state const handleToggle = async (id: string) => { - setMainData((prevData) => - prevData.map((item) => - item.id === id ? { ...item, checked: !item.checked } : item - ) + const updatedData = mainData.map((item) => + item.id === id ? { ...item, checked: !item.checked } : item ); - setDisplayedData((prevData) => - prevData.map((item) => - item.id === id ? { ...item, checked: !item.checked } : item - ) - ); - - // Find the item to update - const updatedItem = mainData.find((item) => item.id === id); - if (updatedItem) { - try { - // Update in database - await database.updateDocument("toolkit_items", id, "checked", !updatedItem.checked ); - } catch (error) { - console.error("Error updating checked status in database:", error); + + setMainData(updatedData); + + // `displayedData` + const orderedData = dragOrder.map((id) => + updatedData.find((item) => item.id === id) + ).filter(Boolean) as ToolkitComponentData[]; + + setDisplayedData(orderedData); + + try { + const updatedItem = updatedData.find((item) => item.id === id); + if (updatedItem) { + await database.updateDocument("toolkit_items", id, "checked", updatedItem.checked); } + } catch (error) { + console.error("Error updating checked status in database:", error); } }; @@ -83,7 +84,15 @@ export default function ToolkitList() { try { await database.deleteFromDb("toolkit_items", id); setMainData((prevData) => prevData.filter((item) => item.id !== id)); - setDisplayedData((prevData) => prevData.filter((item) => item.id !== id)); + // setDisplayedData((prevData) => prevData.filter((item) => item.id !== id)); + setDragOrder((prevOrder) => prevOrder.filter((itemId) => itemId !== id)); + + const orderedData = dragOrder + .filter((itemId) => itemId !== id) + .map((id) => mainData.find((item) => item.id === id)) + .filter(Boolean) as ToolkitComponentData[]; + + setDisplayedData(orderedData); } catch (error) { console.error("Error in handleDelete:", error); } @@ -95,30 +104,46 @@ export default function ToolkitList() { if (!destination) return; - const reorderedData = [...displayedData]; - const [movedItem] = reorderedData.splice(source.index, 1); - reorderedData.splice(destination.index, 0, movedItem); + const newOrder = [...dragOrder]; + const [movedItem] = newOrder.splice(source.index, 1); + newOrder.splice(destination.index, 0, movedItem); + + setDragOrder(newOrder); + + // Обновляем `displayedData` в соответствии с новым порядком + const orderedData = newOrder.map((id) => + mainData.find((item) => item.id === id) + ).filter(Boolean) as ToolkitComponentData[]; + + setDisplayedData(orderedData); - setDisplayedData(reorderedData); }; // Handle search queries const handleSearch = (query: string) => { setSearchQuery(query); - if (query) { - const filtered = mainData.filter((item) => - item.name.toLowerCase().includes(query.toLowerCase()) - ); - setDisplayedData(filtered); - } else { - setDisplayedData(mainData); - } + const filtered = mainData.filter((item) => + item.name.toLowerCase().includes(query.toLowerCase()) + ); + + const orderedData = dragOrder + .map((id) => filtered.find((item) => item.id === id)) + .filter(Boolean) as ToolkitComponentData[]; + + setDisplayedData(orderedData); + }; // Clear search input const handleClearSearch = () => { setSearchQuery(""); - setDisplayedData(mainData); + // setDisplayedData(mainData); + // Restore displayedData to match dragOrder + const orderedData = dragOrder + .map((id) => mainData.find((item) => item.id === id)) + .filter(Boolean) as ToolkitComponentData[]; + + setDisplayedData(orderedData); }; // Filter data based on selected categories @@ -129,23 +154,28 @@ export default function ToolkitList() { ); }, [mainData, selectedCategories]); + // Filter data based on selected categories useEffect(() => { + let filteredData = mainData; + + if (selectedCategories.length > 0) { + filteredData = filteredData.filter((item) => + item.categories.some((cat) => selectedCategories.includes(cat)) + ); + } + if (searchQuery) { - // Filter the current displayed data (based on categories) by the search query - const searchFilteredData = ( - selectedCategories.length > 0 ? filteredData : mainData - ).filter((item) => + filteredData = filteredData.filter((item) => item.name.toLowerCase().includes(searchQuery.toLowerCase()) ); - setDisplayedData(searchFilteredData); - } else if (selectedCategories.length > 0) { - // Show filtered data by categories when no search query - setDisplayedData(filteredData); - } else { - // Show all data if no categories or search query - setDisplayedData(mainData); } - }, [filteredData, searchQuery, selectedCategories.length, mainData]); + + const orderedData = dragOrder + .map((id) => filteredData.find((item) => item.id === id)) + .filter(Boolean) as ToolkitComponentData[]; + + setDisplayedData(orderedData); + }, [filteredData, mainData, searchQuery, selectedCategories, dragOrder]); return (
diff --git a/src/app/toolkit/components/ToolkitDisplay.tsx b/src/app/toolkit/components/ToolkitDisplay.tsx index cbca839e..63c3d86f 100644 --- a/src/app/toolkit/components/ToolkitDisplay.tsx +++ b/src/app/toolkit/components/ToolkitDisplay.tsx @@ -44,8 +44,8 @@ export default function ToolkitDisplay() { { - router.back(); - }; return ( <> {/* Header */}
-
+
{/* Subheader */}

@@ -80,16 +74,15 @@ export default function CategoriesInfoPage() {

Adding your own categories can help you organise in the best way for you.

- - {/* Back Button */} + {/* Back Button
+
*/} ); diff --git a/src/app/toolkit/info/page.tsx b/src/app/toolkit/info/page.tsx index 5a1c6c30..17d3ccb3 100644 --- a/src/app/toolkit/info/page.tsx +++ b/src/app/toolkit/info/page.tsx @@ -1,89 +1,84 @@ -"use client"; -import Button from "@/ui/shared/Button"; +import Link from "next/link"; import { Header } from "@/ui/shared/Header"; -import { useState } from "react"; -import CategoriesInfoPage from "./CategoriesInfoPage"; -export default function ToolkitInfoPage() { - const [showCategories, setShowCategories] = useState(false); - if (showCategories) { - return ; - } +export default function ToolkitInfoPage() { return ( <> {/* Header */}
-
- {/* Introduction Section */} -
-

- What is this Tab About? -

-

- Toolkit tab is your personal space to create, organize, and manage a - toolkit that’s tailored to your unique needs and experiences. It’s - designed to help you quickly access the tools and resources that work - best for you, especially during moments when you feel uneasy, - impulsive, or overwhelmed. By customizing this toolkit, you can rely - on your own expertise about what helps you the most, making it a - powerful support system at your fingertips. -

-
- - {/* How It Helps Section */} -
-

- How Can This Tab Help? -

-
    -
  • - Personalized Tools:{" "} - Add descriptions, images, and links to create tools that resonate - with you. These visual and textual cues are designed to help you act - quickly when decision-making feels harder than usual. -
  • -
  • - - Guidance and Inspiration: - {" "} - Explore expert-recommended suggestions and discover tools that have - helped others. This can give you fresh ideas and reassurance that - you’re not alone in your journey. -
  • -
  • - Easy Organization:{" "} - Categorize your tools in a way that makes sense to you. Use filters - to find what you need and prioritize your most-used tools for quick - access. -
  • +
    + {/* Introduction Section */} +
    +

    + What is this Tab About? +

    +

    + Toolkit tab is your personal space to create, organize, and manage a + toolkit that’s tailored to your unique needs and experiences. It’s + designed to help you quickly access the tools and resources that + work best for you, especially during moments when you feel uneasy, + impulsive, or overwhelmed. By customizing this toolkit, you can rely + on your own expertise about what helps you the most, making it a + powerful support system at your fingertips. +

    +
    -
    -
    + {/* How It Helps Section */} +
    +

    + How Can This Tab Help? +

    +
      +
    • + + Personalized Tools: + {" "} + Add descriptions, images, and links to create tools that resonate + with you. These visual and textual cues are designed to help you + act quickly when decision-making feels harder than usual. +
    • +
    • + Adaptability:{" "} + Update your toolkit as your needs change. Add new tools, remove + what no longer works, and even create your own categories to + reflect your unique experience. +
    • +
    • + + Easy Organization: + {" "} + Categorize your tools in a way that makes sense to you. Use + filters to find what you need and prioritize your most-used tools + for quick access. +
    • +
      + + Go to Categories + +
      -
    • - Adaptability:{" "} - Update your toolkit as your needs change. Add new tools, remove what - no longer works, and even create your own categories to reflect your - unique experience. -
    • -
    • - - Reflection and Growth: - {" "} - Rate how helpful each tool is so you can refine your toolkit over - time, ensuring it stays effective and relevant. -
    • -
    +
  • + + Guidance and Inspiration (Coming soon): + {" "} + Explore suggestions and discover tools that have helped others. + This can give you fresh ideas and reassurance that you’re not + alone in your journey. +
  • +
  • + + Reflection and Growth (Coming soon): + {" "} + Rate how helpful each tool is so you can refine your toolkit over + time, ensuring it stays effective and relevant. +
  • +
+
- - + ); -} \ No newline at end of file +} diff --git a/src/lib/db/seed/toolkit.ts b/src/lib/db/seed/toolkit.ts index 881f6ed5..7df3e2e0 100644 --- a/src/lib/db/seed/toolkit.ts +++ b/src/lib/db/seed/toolkit.ts @@ -6,7 +6,7 @@ export const categories = [ { id: uuidv4(), name: "Distract", timestamp: new Date().toISOString() }, { id: uuidv4(), - name: "Change Status", + name: "Change State", timestamp: new Date().toISOString(), }, ]; @@ -35,7 +35,7 @@ export const toolkit = [ { id: uuidv4(), name: "Call a friend", - categories: ["Distract", "Change status"], + categories: ["Distract", "Change state"], checked: false, infoUrl: "https://example.com/call", imageUrl: @@ -45,7 +45,7 @@ export const toolkit = [ { id: uuidv4(), name: "Drink water", - categories: ["Distract", "Change status"], + categories: ["Distract", "Change state"], checked: false, infoUrl: "https://example.com/call", imageUrl: