From 4e5041275f612f328e0de5b40d6ee3e44759374b Mon Sep 17 00:00:00 2001 From: Abdulhakeem Abdulazeez Date: Thu, 28 Nov 2024 09:24:04 +0100 Subject: [PATCH 1/3] feat: modified and added reusable components --- app/client/.env.example | 1 + app/client/components.json | 21 ++ app/client/components.md | 99 +++++++ app/client/package.json | 11 +- app/client/public/icons/layer.svg | 6 + .../src/app/components/Button/Button.tsx | 31 ++- .../components/Input/PlacesAutocomplete.tsx | 95 +++++++ .../src/app/components/Input/SelectField.tsx | 78 ++++++ .../src/app/components/Input/TextField.tsx | 46 ++++ app/client/src/app/components/LandsList.tsx | 241 +++++++++++------- app/client/src/app/components/Modal/Modal.tsx | 36 ++- app/client/src/app/components/P/P.tsx | 54 +++- .../app/components/Table/DefaultColumn.tsx | 106 ++++++++ .../src/app/components/Table/DefaultData.tsx | 69 +++++ app/client/src/app/components/Table/Table.tsx | 239 +++++++++++++++++ app/client/src/app/globals.css | 85 +++++- .../src/components/ui/dropdown-menu.tsx | 201 +++++++++++++++ app/client/src/components/ui/table.tsx | 120 +++++++++ app/client/tailwind.config.ts | 76 +++++- 19 files changed, 1470 insertions(+), 145 deletions(-) create mode 100644 app/client/.env.example create mode 100644 app/client/components.json create mode 100644 app/client/components.md create mode 100644 app/client/public/icons/layer.svg create mode 100644 app/client/src/app/components/Input/PlacesAutocomplete.tsx create mode 100644 app/client/src/app/components/Input/SelectField.tsx create mode 100644 app/client/src/app/components/Input/TextField.tsx create mode 100644 app/client/src/app/components/Table/DefaultColumn.tsx create mode 100644 app/client/src/app/components/Table/DefaultData.tsx create mode 100644 app/client/src/app/components/Table/Table.tsx create mode 100644 app/client/src/components/ui/dropdown-menu.tsx create mode 100644 app/client/src/components/ui/table.tsx diff --git a/app/client/.env.example b/app/client/.env.example new file mode 100644 index 00000000..05aa59ad --- /dev/null +++ b/app/client/.env.example @@ -0,0 +1 @@ +NEXT_GOOGLE_PLACES_API=GIzaSyBYZUIAKIEP_kboIuNb0SK4KzlAmJc-YWo \ No newline at end of file diff --git a/app/client/components.json b/app/client/components.json new file mode 100644 index 00000000..a5777073 --- /dev/null +++ b/app/client/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "src/app/globals.css", + "baseColor": "zinc", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/app/client/components.md b/app/client/components.md new file mode 100644 index 00000000..2d276463 --- /dev/null +++ b/app/client/components.md @@ -0,0 +1,99 @@ +# Component Library Documentation + +This PR provides a set of reusable components designed for common UI tasks. Below are the details for each component and how to use them effectively. + +--- + +## Components Overview + +### 1. **`P` Component** +The `P` component is used for headings and paragraphs. + +#### Props: +- **`size`**: Controls the size of the text. +- **`weight`**: Sets the font weight. +- **`classname`**: Allows custom styles to be added. +- **`align`**: Aligns the text (e.g., left, center, right). + +Refer to the component code for the expected values for these props. + +--- + +### 2. **`Input TextField` Component** +This component is a customizable input field with extended functionality. + +#### Props: +- **HTML Input Attributes**: All standard input attributes (e.g., `type`, `placeholder`, etc.). +- **`error`**: *(Optional)* A string displayed below the field to indicate validation errors. +- **`onChange`**: A callback function to handle and receive the input field's value. +- **`classname`**: Allows custom styles to be added. + +--- + +### 3. **`Input SelectField` Component** +This component is used for rendering dropdowns. + +#### Props: +- **HTML Select Attributes**: Standard select element attributes. +- **`options`**: An array of objects defining the dropdown's content. +- **`nameKey`**: Specifies the object property to display in the dropdown. +- **`value`**: Sets the default selected value. +- **`onChange`**: A callback function to handle the selected value in the parent component. +- **`classname`**: Allows custom styles to be added. + +--- + +### 4. **`Modal` Component** +A flexible modal for displaying content overlays. + +#### Props: +- **`isOpen`**: A boolean controlling whether the modal is visible. +- **`onClose`**: A callback function to handle modal closure. +- **`size`**: Specifies the modal size. Options include: + - `sm` (small) + - `md` (medium) + - `lg` (large) + +--- + +### 5. **`Button` Component** +A versatile button component with customizable styles. + +#### Props: +- **HTML Button Attributes**: All standard button attributes. +- **`variant`**: Controls the button's color. +- **`size`**: Adjusts the button's size. +- **`type`**: Specifies the button type (e.g., `submit`, `button`). +- **`outlined`**: A boolean determining whether the button has an outlined style. +- **`onClick`**: A callback function triggered when the button is clicked. +- **`classname`**: Allows custom styles to be added. + +--- + +### 6. **`Table` Component** +This component renders a data table with filtering and searching capabilities. + +#### Props: +- **`columns`**: Defines the table's column structure. See [`./src/app/components/Table/DefaultColumn.tsx`](./src/app/components/Table/DefaultColumn.tsx) for an example. +- **`data`**: The dataset to display. Each row's properties must match the column configuration. +- **`filterData`**: An array of strings for filtering table data. +- **`searchBy`**: A string specifying the column to filter the table using the search field. +- **`filterColumn`**: Specifies the column used for filtering with `filterData`. + +--- + +### 7. **`PlacesAutocomplete` Component** +A location search input field powered by the Google Places API. + +#### Props: +- **`label`**: The label for the input field. +- **`onLocationSelect`**: A callback function returning the selected location's: + - Latitude + - Longitude + - Address + +--- + +## Usage Examples + +Refer to the individual component files for usage examples and implementation details. These components are designed to be flexible, reusable, and easy to integrate into your projects. \ No newline at end of file diff --git a/app/client/package.json b/app/client/package.json index 2bda69a5..84362e7a 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -9,15 +9,24 @@ "lint": "next lint" }, "dependencies": { + "@radix-ui/react-dropdown-menu": "^2.1.2", + "@react-google-maps/api": "^2.20.3", "@starknet-react/chains": "^3.1.0", "@starknet-react/core": "^3.6.0", + "@tanstack/react-table": "^8.20.5", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.1", "lucide-react": "^0.460.0", "next": "14.2.15", "react": "^18", "react-dom": "^18", - "starknet": "^6.11.0" + "starknet": "^6.11.0", + "tailwind-merge": "^2.5.5", + "tailwindcss-animate": "^1.0.7", + "use-places-autocomplete": "^4.0.1" }, "devDependencies": { + "@types/google.maps": "^3.58.1", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", diff --git a/app/client/public/icons/layer.svg b/app/client/public/icons/layer.svg new file mode 100644 index 00000000..cbef4a16 --- /dev/null +++ b/app/client/public/icons/layer.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/client/src/app/components/Button/Button.tsx b/app/client/src/app/components/Button/Button.tsx index 52e1d6c4..9851bce6 100644 --- a/app/client/src/app/components/Button/Button.tsx +++ b/app/client/src/app/components/Button/Button.tsx @@ -2,10 +2,11 @@ import React from "react"; interface ButtonProps extends React.PropsWithChildren { classname?: string; - variant?: "default" | "error" | "success" | "gray"; + variant?: "default" | "error" | "success" | "grey" | "primary" | "secondary"; size?: "small" | "medium" | "large" | "full"; type?: "button" | "submit" | "reset"; disabled?: boolean; + outlined?: boolean; onClick?: () => void; "aria-label"?: string; } @@ -17,17 +18,28 @@ const Button: React.FC = ({ size = "medium", type = "button", disabled = false, + outlined = false, onClick, "aria-label": ariaLabel, }) => { const getVariantStyles = () => { switch (variant) { + case "primary": + return `${ + outlined + ? "border border-primary text-primary bg-white hover:primary-outlined-gradient-bg disabled:border-grey-7 disabled:text-grey-6" + : "bg-primary hover:primary-gradient-bg text-white disabled:bg-grey-6" + }`; case "error": return "bg-red-500 hover:bg-red-600 disabled:bg-red-300"; case "success": return "bg-green-500 hover:bg-green-600 disabled:bg-green-300"; - case "gray": - return "bg-gray-500 hover:bg-gray-600 disabled:bg-gray-300"; + case "grey": + return `${ + outlined + ? "border border-grey-5 text-grey bg-white" + : "bg-grey-5 text-white" + }`; default: return "bg-[#6364d5] hover:bg-[#5353c5] disabled:bg-[#a0a0d8]"; } @@ -42,7 +54,7 @@ const Button: React.FC = ({ case "large": return "px-8 py-3 text-lg"; case "full": - return "w-full py-2 text-base"; + return "w-full py-2.5 text-base"; default: return "px-6 py-2 text-base"; } @@ -54,10 +66,13 @@ const Button: React.FC = ({ disabled={disabled} onClick={onClick} aria-label={ariaLabel} - className={`${getVariantStyles()} ${getSizeStyles()} ${classname} text-white rounded focus:outline-none focus:ring-2 focus:ring-offset-2 ${ - disabled ? "cursor-not-allowed" : "" - }`} - > + className={`${getVariantStyles()} ${getSizeStyles()} ${classname} rounded-lg font-bold focus:outline-none focus:ring-2 focus:ring-offset-2 ${ + disabled + ? `cursor-not-allowed ${ + outlined ? "disabled:border-grey-7 disabled:text-grey-6" : "disabled:bg-grey-6" + }` + : "" + }`}> {children} ); diff --git a/app/client/src/app/components/Input/PlacesAutocomplete.tsx b/app/client/src/app/components/Input/PlacesAutocomplete.tsx new file mode 100644 index 00000000..60ec8a9e --- /dev/null +++ b/app/client/src/app/components/Input/PlacesAutocomplete.tsx @@ -0,0 +1,95 @@ +import React, { useState } from "react"; +import { useLoadScript } from "@react-google-maps/api"; +import usePlacesAutocomplete, { + getGeocode, + getLatLng, +} from "use-places-autocomplete"; +import TextInput from "./TextField"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; + +interface GooglePlaceInputProps { + label?: string; + onLocationSelect: (lat: number, lng: number, address: string) => void; +} + +const PlacesAutocomplete: React.FC = ({ + label = "Location", + onLocationSelect, +}) => { + const [error, setError] = useState(null); + const { isLoaded, loadError } = useLoadScript({ + googleMapsApiKey: process.env.NEXT_GOOGLE_PLACES_API as string, + libraries: ["places"], + }); + const { + ready, + value, + setValue, + suggestions: { status, data }, + clearSuggestions, + } = usePlacesAutocomplete({ + // requestOptions: { + // Specify additional options like bounds or types if needed + // componentRestrictions: { country: "us" }, + // }, + debounce: 300, + }); + + const handleInputChange = (value: string) => { + setValue(value); + }; + + const handleSelect = async (address: string) => { + setValue(address, false); + clearSuggestions(); + + try { + const results = await getGeocode({ address }); + const { lat, lng } = await getLatLng(results[0]); + onLocationSelect(lat, lng, address); + setError(null); + } catch (error) { + console.error("Error fetching location details: ", error); + setError("Failed to get location details. Please try again."); + } + }; + + if (loadError) return
Error loading Google Maps script
; + if (!isLoaded) return
Loading Component...
+ return ( +
+ {label && ( + + )} + + + + + + {status === "OK" && + data.map(({ place_id, description }) => ( + handleSelect(description)}> + {description} + + ))} + + + {error &&

{error}

} +
+ ); +}; + +export default PlacesAutocomplete; diff --git a/app/client/src/app/components/Input/SelectField.tsx b/app/client/src/app/components/Input/SelectField.tsx new file mode 100644 index 00000000..f9c4fd6d --- /dev/null +++ b/app/client/src/app/components/Input/SelectField.tsx @@ -0,0 +1,78 @@ +import React, { useEffect, useState } from "react"; +import { ChevronDown } from "lucide-react"; +import Paragraph from "../P/P"; + +interface SelectProps + extends Omit, "onChange"> { + options: T[]; + nameKey: keyof T; + label?: string; + error?: string; + classname?: string; + onChange?: (value: string) => void; + value?: string | null | undefined; +} + +const SelectField = ({ + options, + nameKey, + label, + error, + classname = "", + onChange, + value, + ...props +}: SelectProps) => { + const [isOpen, setIsOpen] = useState(false); + const [selected, setSelected] = useState(null); + + const toggleDropdown = () => setIsOpen(!isOpen); + + const handleOptionClick = (value: string) => { + setSelected(value); + setIsOpen(false); + onChange?.(value); + }; + + useEffect(() => { + setSelected(value); + }, [value]) + + return ( +
+ + {label && ( + + {label} + {props.required && *} + + )} +
+ {selected || ""} +
+ {isOpen && ( +
+ {options.map((option, index) => ( +
handleOptionClick(String(option[nameKey]))}> + {String(option[nameKey])} +
+ ))} +
+ )} + {error &&

{error}

} +
+ ); +}; + +export default SelectField; diff --git a/app/client/src/app/components/Input/TextField.tsx b/app/client/src/app/components/Input/TextField.tsx new file mode 100644 index 00000000..f9b8b15f --- /dev/null +++ b/app/client/src/app/components/Input/TextField.tsx @@ -0,0 +1,46 @@ +import React from "react"; +import Paragraph from "../P/P"; + +interface TextInputProps extends Omit, 'onChange'> { + label?: string; + error?: string; + classname?: string; + onChange?: (value: string) => void; +} + +const TextInput: React.FC = ({ + label, + error, + classname = "", + onChange, + ...props +}) => { + const handleChange = (event: React.ChangeEvent) => { + onChange?.(event.target.value); + }; + + return ( +
+ {label && ( + + )} + + {error && {error}} + {!error &&

no error

} +
+ ); +}; + +export default TextInput; diff --git a/app/client/src/app/components/LandsList.tsx b/app/client/src/app/components/LandsList.tsx index 782da37b..809cd0c2 100644 --- a/app/client/src/app/components/LandsList.tsx +++ b/app/client/src/app/components/LandsList.tsx @@ -1,7 +1,11 @@ "use client"; import { useEffect, useState } from "react"; -// import Image from "next/image"; +import Image from "next/image"; import type { Connector } from "@starknet-react/core"; +import { X } from "lucide-react"; +import TextInput from "./Input/TextField"; +import SelectField from "./Input/SelectField"; +import Button from "./Button/Button"; import { useConnect, useDisconnect, @@ -23,6 +27,12 @@ import { import { ABI as LandRegistryABI } from "@/abis/LandRegistryAbi"; import { type Address } from "@starknet-react/chains"; +import Paragraph from "./P/P"; +import { DataTable } from "./Table/Table"; +import { TableData } from "./Table/DefaultData"; +import { columns } from "./Table/DefaultColumn"; +import Modal from "./Modal/Modal"; +import PlacesAutocomplete from "./Input/PlacesAutocomplete"; const contractAddress = "0x5a4054a1b1389dcd48b650637977280d32f1ad8b3027bc6c7eb606bf7e28bf5"; @@ -98,8 +108,21 @@ export const LandList = () => { area: 0, latitude: 0, longitude: 0, - landUse: 0, + landUse: "", }); + const [isButtonDisabled, setIsButtonDisabled] = useState(true); + + useEffect(() => { + const isDisabled = + createLandData.area <= 0 || + createLandData.landUse.trim() === "" || + createLandData.latitude === 0 || + createLandData.longitude === 0; + + setIsButtonDisabled(isDisabled); + }, [createLandData]); + + const filterData = ["all", "approved", "unapproved", "bought"]; const [refresh, setRefresh] = useState(false); @@ -145,6 +168,10 @@ export const LandList = () => { ); }; + const getLocation = (data: any) => { + console.log(data); + }; + const removeInspector = async (inspector_id: string) => { console.log(inspector_id); await contract.connect(account); @@ -167,7 +194,7 @@ export const LandList = () => { area: 0, latitude: 0, longitude: 0, - landUse: 0, + landUse: '', }); setRefresh(!refresh); setShowCreateLandModal(false); @@ -184,16 +211,14 @@ export const LandList = () => {
setShowCreateLandModal(true)} - className=" cursor-pointer text-base font-bold text-gray-700 bg-gray-200 p-2 rounded-md" - > + className=" cursor-pointer text-base font-bold text-gray-700 bg-gray-200 p-2 rounded-md"> Register Land
{ setShowAddInspectorModal(true); }} - className=" cursor-pointer text-base font-bold text-gray-700 bg-gray-200 p-2 rounded-md" - > + className=" cursor-pointer text-base font-bold text-gray-700 bg-gray-200 p-2 rounded-md"> Add Inspector
@@ -201,12 +226,15 @@ export const LandList = () => {
setRefresh(!refresh)} - className=" cursor-pointer text-base font-bold text-gray-700 bg-gray-200 p-2 rounded-md" - > + className=" cursor-pointer text-base font-bold text-gray-700 bg-gray-200 p-2 rounded-md"> Refresh List
+
+ +
+
{loadingList && (
@@ -218,8 +246,7 @@ export const LandList = () => { return (
+ className="grid grid-cols-3 gap-10 bg-gray-200 py-3 px-2 rounded-md mb-4">
Land use:{" "} { @@ -255,8 +282,7 @@ export const LandList = () => { setShowAssignInspectorModal(true); setLandToAssignInspector(land.id); }} - className="font-semibold cursor-pointer" - > + className="font-semibold cursor-pointer"> Assign Inspector
)} @@ -276,8 +302,7 @@ export const LandList = () => { onClick={() => { setShowAddInspectorModal(false); }} - className="text-center mb-3 cursor-pointer font-bold" - > + className="text-center mb-3 cursor-pointer font-bold"> Close
@@ -291,8 +316,7 @@ export const LandList = () => { />
addInspector()} - className="text-center font-semibold p-2 bg-gray-100 cursor-pointer" - > + className="text-center font-semibold p-2 bg-gray-100 cursor-pointer"> Add Inspector
@@ -306,8 +330,7 @@ export const LandList = () => { onClick={() => { setShowAssignInspectorModal(false); }} - className="text-center mb-3 cursor-pointer font-bold" - > + className="text-center mb-3 cursor-pointer font-bold"> Close
@@ -321,94 +344,116 @@ export const LandList = () => { />
assignInspector()} - className="font-semibold p-2 bg-gray-100 cursor-pointer" - > + className="font-semibold p-2 bg-gray-100 cursor-pointer"> assign
)} - {showCreateLandModal && ( -
-
-
{ - setShowCreateLandModal(false); - }} - className="text-center mb-3 cursor-pointer font-bold" - > - Close + {/* {showCreateLandModal && ( */} + setShowCreateLandModal(false)} isOpen={showCreateLandModal}> +
+ layer icon +
+ + Register New Land + + + Please enter all details to register your land + +
+ + + setCreateLandData({ + ...createLandData, + latitude: value as any, + }) + } + value={createLandData.latitude} + /> + + setCreateLandData({ + ...createLandData, + longitude: value as any, + }) + } + value={createLandData.longitude} + /> + + setCreateLandData({ + ...createLandData, + area: value as any, + }) + } + value={createLandData.area} + /> + + setCreateLandData({ + ...createLandData, + landUse: value as any, + }) + } + />
-
Land Data
-
Latitude
- - setCreateLandData({ - ...createLandData, - latitude: e.target.value as any, - }) - } - className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" - placeholder="Latitude" - /> -
Longitude
- - setCreateLandData({ - ...createLandData, - longitude: e.target.value as any, - }) - } - className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" - placeholder="Longitude" - /> -
Area
- - setCreateLandData({ - ...createLandData, - area: e.target.value as any, - }) - } - className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" - placeholder="Area" - /> -
Land use
- -
Land use
-
createLand()} - className="font-semibold p-2 text-center bg-gray-200 cursor-pointer" - > - Add Land +
+ +
-
-
-
- )} + +
); }; diff --git a/app/client/src/app/components/Modal/Modal.tsx b/app/client/src/app/components/Modal/Modal.tsx index 177ce599..35cff47d 100644 --- a/app/client/src/app/components/Modal/Modal.tsx +++ b/app/client/src/app/components/Modal/Modal.tsx @@ -4,10 +4,11 @@ import { X } from "lucide-react"; interface ModalProps { isOpen: boolean; onClose: () => void; - children: ReactNode; + children?: ReactNode; + size?: "sm" | "md" | "lg"; } -const Modal: React.FC = ({ isOpen, onClose, children }) => { +const Modal: React.FC = ({ isOpen, onClose, size, children }) => { if (!isOpen) return null; const handleBackgroundClick = (e: React.MouseEvent) => { @@ -16,19 +17,32 @@ const Modal: React.FC = ({ isOpen, onClose, children }) => { } }; + const getSizeStyles = () => { + switch (size) { + case "sm": + return "w-full max-w-[400px]"; + case "md": + return "w-full max-w-[560px]"; + case "lg": + return "w-full max-w-[630px]"; + default: + return "w-full max-w-[560px]"; + } + }; + return (
-
+ className="fixed backdrop-blur-[8px] top-0 left-0 right-0 bottom-0 bg-blue-2/70 bg-opacity-60 flex justify-center items-center z-30" + onClick={handleBackgroundClick}> +
{/* Close Button */} - + /> {/* Modal Content */} {children} diff --git a/app/client/src/app/components/P/P.tsx b/app/client/src/app/components/P/P.tsx index d3498a89..9493d8a8 100644 --- a/app/client/src/app/components/P/P.tsx +++ b/app/client/src/app/components/P/P.tsx @@ -1,33 +1,69 @@ interface ParagraphProps extends React.PropsWithChildren { classname?: string; - size?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; + size?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "xl" | "lg" | "rg" | "sm" | "xs"; + weight?: "bold" | "medium" | "semibold"; + align?: "left" | "center" | "right"; } const Paragraph: React.FC = ({ children, classname = "", size, + weight, + align, }) => { const getSizeStyles = () => { switch (size) { case "h1": - return "text-[56px] leading-[61.6px]"; + return "text-[56px]/[61.6px]"; case "h2": - return "text-[48px] leading-[52.8px]"; + return "text-[48px]/[52.8px]"; case "h3": - return "text-[40px] leading-[44px]"; + return "text-[40px]/[44px]"; case "h4": - return "text-[32px] leading-[35.2px]"; + return "text-[32px]/[35.2px]"; case "h5": - return "text-[24px] leading-[26.4px]"; + return "text-2xl/[26.4px]"; case "h6": - return "text-[20px] leading-[22px]"; - default: + return "text-xl/[22px]"; + case "xl": + return "text-xl/[28px]"; + case "lg": + return "text-lg/[25.2px]"; + case "xs": return "text-xs"; + case "sm": + return "text-sm/[19.6px]" + default: + return "text-base/[22.4px]"; } }; - return

{children}

; + const getWeightStyles = () => { + switch (weight) { + case "bold": + return "font-bold"; + case "medium": + return "font-medium"; + case "semibold": + return "font-semibold"; + default: + return ""; + } + }; + + const getTextAlign = () => { + switch (align) { + case "center": + return "text-center"; + case "right": + return "text-right"; + default: + return "text-left" + } + } + + return

{children}

; }; export default Paragraph; diff --git a/app/client/src/app/components/Table/DefaultColumn.tsx b/app/client/src/app/components/Table/DefaultColumn.tsx new file mode 100644 index 00000000..b2789714 --- /dev/null +++ b/app/client/src/app/components/Table/DefaultColumn.tsx @@ -0,0 +1,106 @@ +"use client"; + +import * as React from "react"; +import { + ColumnDef +} from "@tanstack/react-table"; +import { MoreHorizontal } from "lucide-react"; + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import Paragraph from "../P/P"; + + +export type LandData = { + id: string; + amount: number; + status: "all" | "approved" | "unapproved" | "bought"; + name: string; +}; + +export const columns: ColumnDef[] = [ + // { + // id: "select", + // header: ({ table }) => ( + // table.toggleAllPageRowsSelected(!!value)} + // aria-label="Select all" + // /> + // ), + // cell: ({ row }) => ( + // row.toggleSelected(!!value)} + // aria-label="Select row" + // /> + // ), + // enableSorting: false, + // enableHiding: false, + // }, + { + accessorKey: "status", + header: "Status", + cell: ({ row }) => ( +
{row.getValue("status")}
+ ), + }, + { + accessorKey: "name", + header: "Land Name", + cell: ({ row }) =>
{row.getValue("name")}
, + }, + { + accessorKey: "amount", + header: () => amount, + cell: ({ row }) => { + const amount = parseFloat(row.getValue("amount")); + + // Format the amount as a dollar amount + const formatted = new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + }).format(amount); + + return
{formatted}
; + }, + }, + { + id: "actions", + header: "ACTIONS", + enableHiding: false, + cell: ({ row }) => { + const payment = row.original; + + return ( + + + + + + Actions + navigator.clipboard.writeText(payment.id)}> + Copy payment ID + + + View customer + View payment details + + + ); + }, + }, +]; diff --git a/app/client/src/app/components/Table/DefaultData.tsx b/app/client/src/app/components/Table/DefaultData.tsx new file mode 100644 index 00000000..6e843a9d --- /dev/null +++ b/app/client/src/app/components/Table/DefaultData.tsx @@ -0,0 +1,69 @@ +export type Payment = { + id: string; + amount: number; + status: "approved" | "unapproved" | "bought"; + name: string; +}; + +export const TableData: Payment[] = [ + { + id: "m5gr84i9", + amount: 316, + status: "approved", + name: "Banana Island", + }, + { + id: "3u1reuv4", + amount: 242, + status: "approved", + name: "Badagry", + }, + { + id: "derv1ws0", + amount: 837, + status: "unapproved", + name: "Gbagada", + }, + { + id: "5kma53ae", + amount: 874, + status: "approved", + name: "Lekki", + }, + { + id: "bhqecj4p", + amount: 721, + status: "bought", + name: "Ikeja", + }, + { + id: "m5gr84i9", + amount: 316, + status: "approved", + name: "Surulere", + }, + { + id: "3u1reuv4", + amount: 242, + status: "approved", + name: "Badagry", + }, + { + id: "derv1ws0", + amount: 837, + status: "unapproved", + name: "Gbagada", + }, + { + id: "5kma53ae", + amount: 874, + status: "approved", + name: "Lekki", + }, + { + id: "bhqecj4p", + amount: 721, + status: "bought", + name: "Ikeja", + }, +]; diff --git a/app/client/src/app/components/Table/Table.tsx b/app/client/src/app/components/Table/Table.tsx new file mode 100644 index 00000000..3d247c3a --- /dev/null +++ b/app/client/src/app/components/Table/Table.tsx @@ -0,0 +1,239 @@ +"use client"; + +import * as React from "react"; +import { + ColumnDef, + ColumnFiltersState, + SortingState, + VisibilityState, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table"; +import { + ChevronDown, + ChevronLeft, + ChevronRight, + ChevronsLeft, + ChevronsRight, +} from "lucide-react"; +import TextInput from "../Input/TextField"; + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuTrigger, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, +} from "@/components/ui/dropdown-menu"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import SelectField from "../Input/SelectField"; + +type DataTableType = { + columns: ColumnDef[]; + data: any; + filterData?: string[]; + searchBy?: string; + filterColumn?: string; +}; + +export function DataTable({ + columns, + data, + filterData, + searchBy, + filterColumn +}: DataTableType) { + const [sorting, setSorting] = React.useState([]); + const [columnFilters, setColumnFilters] = React.useState( + [] + ); + const [columnVisibility, setColumnVisibility] = + React.useState({}); + const [rowSelection, setRowSelection] = React.useState({}); + const [filterValue, setFilter] = React.useState(""); + + const setFilterTo = (value: string) => { + if (!filterColumn) return; + setFilter(value); + if (value === "all") { + table.resetColumnFilters(); + } else { + table.getColumn(filterColumn)?.setFilterValue(value); + } + }; + + const pageSize: { count: number }[] = [ + { count: 5 }, + { count: 10 }, + { count: 20 }, + { count: 30 }, + { count: 40 }, + { count: 50 }, + ]; + + const table = useReactTable({ + data, + columns, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + onColumnVisibilityChange: setColumnVisibility, + onRowSelectionChange: setRowSelection, + state: { + sorting, + columnFilters, + columnVisibility, + rowSelection, + }, + }); + + return ( +
+
+ {searchBy && ( + + table.getColumn(searchBy)?.setFilterValue(value) + } + className="bg-grey-8 rounded-xl max-w-2xl w-full px-3.5 py-2.5" + /> + )} + {filterData && ( + + + + + + + {filterData.map((data, index) => { + return ( + + {data} + + ); + })} + + + + )} +
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ); + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + No results. + + + )} + +
+
+
+
+ table.setPageSize(Number(value))} + classname="bg-grey-8 border-none border-grey-8" + /> +
+
+
+ + + + +
+
+
+
+ ); +} diff --git a/app/client/src/app/globals.css b/app/client/src/app/globals.css index 6b717ad3..65f14711 100644 --- a/app/client/src/app/globals.css +++ b/app/client/src/app/globals.css @@ -2,20 +2,85 @@ @tailwind components; @tailwind utilities; -:root { - --background: #ffffff; - --foreground: #171717; +@layer utilities { + .primary-outlined-gradient-bg { + background: linear-gradient( + 0deg, + rgba(255, 255, 255, 0.8) 0%, + rgba(255, 255, 255, 0.8) 100% + ), + #6e62e5; + } + .primary-gradient-bg { + background: linear-gradient(0deg, rgba(0, 0, 0, 0.30) 0%, rgba(0, 0, 0, 0.30) 100%), #6E62E5; + } +} + +body { + font-family: Arial, Helvetica, sans-serif; } -@media (prefers-color-scheme: dark) { +@layer base { :root { - --background: #0a0a0a; - --foreground: #ededed; + --background: 0 0% 100%; + --foreground: 240 10% 3.9%; + --card: 0 0% 100%; + --card-foreground: 240 10% 3.9%; + --popover: 0 0% 100%; + --popover-foreground: 240 10% 3.9%; + --primary: 240 5.9% 10%; + --primary-foreground: 0 0% 98%; + --secondary: 240 4.8% 95.9%; + --secondary-foreground: 240 5.9% 10%; + --muted: 240 4.8% 95.9%; + --muted-foreground: 240 3.8% 46.1%; + --accent: 240 4.8% 95.9%; + --accent-foreground: 240 5.9% 10%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + --border: 240 5.9% 90%; + --input: 240 5.9% 90%; + --ring: 240 10% 3.9%; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + --radius: 0.5rem; + } + .dark { + --background: 240 10% 3.9%; + --foreground: 0 0% 98%; + --card: 240 10% 3.9%; + --card-foreground: 0 0% 98%; + --popover: 240 10% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 0 0% 98%; + --primary-foreground: 240 5.9% 10%; + --secondary: 240 3.7% 15.9%; + --secondary-foreground: 0 0% 98%; + --muted: 240 3.7% 15.9%; + --muted-foreground: 240 5% 64.9%; + --accent: 240 3.7% 15.9%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 240 3.7% 15.9%; + --input: 240 3.7% 15.9%; + --ring: 240 4.9% 83.9%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; } } -body { - color: var(--foreground); - background: var(--background); - font-family: Arial, Helvetica, sans-serif; +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } } diff --git a/app/client/src/components/ui/dropdown-menu.tsx b/app/client/src/components/ui/dropdown-menu.tsx new file mode 100644 index 00000000..082639fb --- /dev/null +++ b/app/client/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,201 @@ +"use client" + +import * as React from "react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { Check, ChevronRight, Circle } from "lucide-react" + +import { cn } from "@/lib/utils" + +const DropdownMenu = DropdownMenuPrimitive.Root + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger + +const DropdownMenuGroup = DropdownMenuPrimitive.Group + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal + +const DropdownMenuSub = DropdownMenuPrimitive.Sub + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + svg]:size-4 [&>svg]:shrink-0", + inset && "pl-8", + className + )} + {...props} + /> +)) +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +DropdownMenuShortcut.displayName = "DropdownMenuShortcut" + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +} diff --git a/app/client/src/components/ui/table.tsx b/app/client/src/components/ui/table.tsx new file mode 100644 index 00000000..c0df655c --- /dev/null +++ b/app/client/src/components/ui/table.tsx @@ -0,0 +1,120 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Table = React.forwardRef< + HTMLTableElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+ + +)) +Table.displayName = "Table" + +const TableHeader = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableHeader.displayName = "TableHeader" + +const TableBody = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableBody.displayName = "TableBody" + +const TableFooter = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + tr]:last:border-b-0", + className + )} + {...props} + /> +)) +TableFooter.displayName = "TableFooter" + +const TableRow = React.forwardRef< + HTMLTableRowElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableRow.displayName = "TableRow" + +const TableHead = React.forwardRef< + HTMLTableCellElement, + React.ThHTMLAttributes +>(({ className, ...props }, ref) => ( +
[role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> +)) +TableHead.displayName = "TableHead" + +const TableCell = React.forwardRef< + HTMLTableCellElement, + React.TdHTMLAttributes +>(({ className, ...props }, ref) => ( + [role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> +)) +TableCell.displayName = "TableCell" + +const TableCaption = React.forwardRef< + HTMLTableCaptionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +TableCaption.displayName = "TableCaption" + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +} diff --git a/app/client/tailwind.config.ts b/app/client/tailwind.config.ts index 021c3937..9104038e 100644 --- a/app/client/tailwind.config.ts +++ b/app/client/tailwind.config.ts @@ -1,19 +1,79 @@ import type { Config } from "tailwindcss"; const config: Config = { - content: [ + darkMode: ["class"], + content: [ "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", "./src/components/**/*.{js,ts,jsx,tsx,mdx}", "./src/app/**/*.{js,ts,jsx,tsx,mdx}", ], theme: { - extend: { - colors: { - background: "var(--background)", - foreground: "var(--foreground)", - }, - }, + extend: { + colors: { + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))' + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))' + }, + black: '#090914', + grey: '#7E8299', + info: '#2F80ED', + success: '#27AE60', + warning: '#E2B93B', + error: '#EB5757', + red: '#F1416C', + 'grey-2': '#4F4F4F', + 'grey-3': '#828282', + 'grey-4': '#D9DAE1', + 'grey-5': '#D0D5DD', + 'grey-6': '#E0E0E0', + 'grey-7': '#BDBDBD', + 'grey-8': '#F9F9F9', + 'black-2': '#101828', + 'blue-2': '#344054', + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))' + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))' + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))' + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))' + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))' + }, + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + chart: { + '1': 'hsl(var(--chart-1))', + '2': 'hsl(var(--chart-2))', + '3': 'hsl(var(--chart-3))', + '4': 'hsl(var(--chart-4))', + '5': 'hsl(var(--chart-5))' + } + }, + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)' + } + } }, - plugins: [], + plugins: [require("tailwindcss-animate")], }; export default config; From e6a2ae60536474d44f1108ebf8abcc2cdfc3f692 Mon Sep 17 00:00:00 2001 From: Abdulhakeem Abdulazeez Date: Thu, 28 Nov 2024 10:51:25 +0100 Subject: [PATCH 2/3] feat: fix build --- app/client/.eslintrc.json | 7 ++- .../src/app/components/Input/SelectField.tsx | 2 +- app/client/src/app/components/LandsList.tsx | 50 ++++++++----------- app/client/src/app/components/Sidebar.tsx | 4 +- app/client/src/app/layout.tsx | 2 +- 5 files changed, 30 insertions(+), 35 deletions(-) diff --git a/app/client/.eslintrc.json b/app/client/.eslintrc.json index 37224185..ed21dd81 100644 --- a/app/client/.eslintrc.json +++ b/app/client/.eslintrc.json @@ -1,3 +1,6 @@ { - "extends": ["next/core-web-vitals", "next/typescript"] -} + "extends": ["next/core-web-vitals", "next/typescript"], + "rules": { + "@typescript-eslint/no-explicit-any": "off" + } +} \ No newline at end of file diff --git a/app/client/src/app/components/Input/SelectField.tsx b/app/client/src/app/components/Input/SelectField.tsx index f9c4fd6d..532a92e6 100644 --- a/app/client/src/app/components/Input/SelectField.tsx +++ b/app/client/src/app/components/Input/SelectField.tsx @@ -10,7 +10,7 @@ interface SelectProps error?: string; classname?: string; onChange?: (value: string) => void; - value?: string | null | undefined; + value?: string | undefined; } const SelectField = ({ diff --git a/app/client/src/app/components/LandsList.tsx b/app/client/src/app/components/LandsList.tsx index 809cd0c2..36db8052 100644 --- a/app/client/src/app/components/LandsList.tsx +++ b/app/client/src/app/components/LandsList.tsx @@ -1,32 +1,20 @@ "use client"; import { useEffect, useState } from "react"; import Image from "next/image"; -import type { Connector } from "@starknet-react/core"; -import { X } from "lucide-react"; import TextInput from "./Input/TextField"; import SelectField from "./Input/SelectField"; import Button from "./Button/Button"; import { - useConnect, - useDisconnect, useAccount, useContract, - useSendTransaction, - useNonceForAddress, } from "@starknet-react/core"; import { - CairoOption, - CairoOptionVariant, - CairoEnum, - AbiEnum, - StarknetEnumType, - getCalldata, CairoCustomEnum, } from "starknet"; import { ABI as LandRegistryABI } from "@/abis/LandRegistryAbi"; -import { type Address } from "@starknet-react/chains"; +// import { type Address } from "@starknet-react/chains"; import Paragraph from "./P/P"; import { DataTable } from "./Table/Table"; import { TableData } from "./Table/DefaultData"; @@ -37,13 +25,13 @@ import PlacesAutocomplete from "./Input/PlacesAutocomplete"; const contractAddress = "0x5a4054a1b1389dcd48b650637977280d32f1ad8b3027bc6c7eb606bf7e28bf5"; -enum a { - Commercial, -} +// enum a { +// Commercial, +// } -type land_use = { - Commercial: any; -}; +// type land_use = { +// Commercial: any; +// }; export const LandList = () => { const LandUse = [ @@ -82,9 +70,9 @@ export const LandList = () => { ]; const { address, status, account } = useAccount(); // status --> "connected" | "disconnected" | "connecting" | "reconnecting"; - const { data, isLoading, isError, error } = useNonceForAddress({ - address: account?.address as Address, - }); + // const { data, isLoading, isError, error } = useNonceForAddress({ + // address: account?.address as Address, + // }); const { contract } = useContract({ abi: LandRegistryABI, address: contractAddress, @@ -148,15 +136,17 @@ export const LandList = () => { setLoadingList(false); } } catch (error) { + console.error(error); setLoadingList(false); } })(); - }, [status, address, refresh]); + }, [status, address, refresh, contract]); const addInspector = async () => { await contract.connect(account); const inspector_address = inspectorToAssign; const response = await contract.add_inspector(inspector_address); + console.log(response); }; const assignInspector = async () => { @@ -166,18 +156,20 @@ export const LandList = () => { landToAssignInspector, inspector_address ); + console.log(response); }; const getLocation = (data: any) => { console.log(data); }; - const removeInspector = async (inspector_id: string) => { - console.log(inspector_id); - await contract.connect(account); - const inspector_address = inspector_id; - const response = await contract.remove_inspector(inspector_address); - }; + // const removeInspector = async (inspector_id: string) => { + // console.log(inspector_id); + // await contract.connect(account); + // const inspector_address = inspector_id; + // const response = await contract.remove_inspector(inspector_address); + // console.log(response); + // }; const createLand = async () => { try { diff --git a/app/client/src/app/components/Sidebar.tsx b/app/client/src/app/components/Sidebar.tsx index e3c11453..b9e1b301 100644 --- a/app/client/src/app/components/Sidebar.tsx +++ b/app/client/src/app/components/Sidebar.tsx @@ -72,8 +72,8 @@ const Sidebar: React.FC = ({ role }) => {