mirror of
https://github.com/cssgunc/compass.git
synced 2025-04-06 20:50:17 -04:00
Merge branch 'mel-admin-GEN-102-tag' into varun-admin-GEN-59-page
This commit is contained in:
commit
5df525577a
BIN
.DS_Store → backend/.DS_Store
vendored
BIN
.DS_Store → backend/.DS_Store
vendored
Binary file not shown.
37
compass/app/admin/layout.tsx
Normal file
37
compass/app/admin/layout.tsx
Normal file
|
@ -0,0 +1,37 @@
|
|||
"use client"
|
||||
|
||||
import Sidebar from '@/components/resource/Sidebar';
|
||||
import React, { useState } from 'react';
|
||||
import { ChevronDoubleRightIcon } from '@heroicons/react/24/outline';
|
||||
|
||||
export default function RootLayout({
|
||||
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="flex-row">
|
||||
{/* button to open sidebar */}
|
||||
<button
|
||||
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
|
||||
className={`fixed z-20 p-2 text-gray-500 hover:text-gray-800 left-0`}
|
||||
aria-label={'Open sidebar'}
|
||||
>
|
||||
{!isSidebarOpen &&
|
||||
<ChevronDoubleRightIcon className="h-5 w-5" /> // Icon for closing the sidebar
|
||||
}
|
||||
</button>
|
||||
{/* sidebar */}
|
||||
<div className={`absolute inset-y-0 left-0 transform ${isSidebarOpen ? 'translate-x-0' : '-translate-x-full'} w-64 transition duration-300 ease-in-out`}>
|
||||
<Sidebar setIsSidebarOpen={setIsSidebarOpen} />
|
||||
</div>
|
||||
{/* page ui */}
|
||||
<div className={`flex-1 transition duration-300 ease-in-out ${isSidebarOpen ? 'ml-64' : 'ml-0'}`}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
22
compass/app/admin/page.tsx
Normal file
22
compass/app/admin/page.tsx
Normal file
|
@ -0,0 +1,22 @@
|
|||
"use client";
|
||||
|
||||
import { PageLayout } from "@/components/PageLayout";
|
||||
import { Table } from "@/components/Table/Index";
|
||||
|
||||
import { UsersIcon } from "@heroicons/react/24/solid";
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export default function Page() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
{/* icon + title */}
|
||||
<PageLayout title="Users" icon={<UsersIcon />}>
|
||||
<Table />
|
||||
</PageLayout>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,86 +0,0 @@
|
|||
// pages/index.tsx
|
||||
"use client";
|
||||
|
||||
import Button from '@/components/Button';
|
||||
import Input from '@/components/Input'
|
||||
import InlineLink from '@/components/InlineLink';
|
||||
import Paper from '@/components/auth/Paper';
|
||||
// import { Metadata } from 'next'
|
||||
import Image from 'next/image';
|
||||
import {ChangeEvent, useState} from "react";
|
||||
|
||||
// export const metadata: Metadata = {
|
||||
// title: 'Login',
|
||||
// }
|
||||
|
||||
export default function Page() {
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const handleEmailChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setEmail(event.currentTarget.value);
|
||||
console.log("email " + email);
|
||||
}
|
||||
|
||||
const handlePasswordChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setPassword(event.currentTarget.value);
|
||||
console.log("password " + password)
|
||||
}
|
||||
|
||||
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault();
|
||||
// Priority: Incorrect combo > Missing email > Missing password
|
||||
|
||||
if (password.trim().length === 0) {
|
||||
setError("Please enter your password.")
|
||||
}
|
||||
// This shouldn't happen, <input type="email"> already provides validation, but just in case.
|
||||
if (email.trim().length === 0) {
|
||||
setError("Please enter your email.")
|
||||
}
|
||||
// Placeholder for incorrect email + password combo.
|
||||
if (email === "incorrect@gmail.com" && password) {
|
||||
setError("Incorrect password.")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<Paper>
|
||||
<form className="mb-0 m-auto mt-6 space-y-4 rounded-lg p-4 shadow-lg sm:p-6 lg:p-8 bg-white max-w-xl">
|
||||
<Image
|
||||
src="/logo.png"
|
||||
alt='Compass Center logo.'
|
||||
width={100}
|
||||
height={91}
|
||||
/>
|
||||
<h1 className='font-bold text-xl text-purple-800'>Login</h1>
|
||||
<div className="mb-4">
|
||||
<Input type='email' title="Email" placeholder="janedoe@gmail.com" onChange={handleEmailChange} />
|
||||
</div>
|
||||
<div className="mb-6">
|
||||
<Input type='password' title="Password" onChange={handlePasswordChange} />
|
||||
</div>
|
||||
<div className="flex flex-col items-left space-y-4">
|
||||
<InlineLink href="/forgot_password">
|
||||
Forgot password?
|
||||
</InlineLink>
|
||||
<Button onClick={handleClick}>
|
||||
Login
|
||||
</Button>
|
||||
<div className="text-center text-red-600" hidden={!error}>
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
<p className="text-center mt-6 text-gray-500 text-xs">
|
||||
© 2024 Compass Center
|
||||
</p>
|
||||
</Paper>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,40 +0,0 @@
|
|||
// pages/index.tsx
|
||||
"use client";
|
||||
import Image from 'next/image';
|
||||
|
||||
|
||||
|
||||
import Drawer from '@/components/page/Drawer';
|
||||
// import { Metadata } from 'next'
|
||||
import {ChangeEvent, useState} from "react";
|
||||
|
||||
// export const metadata: Metadata = {
|
||||
// title: 'Login',
|
||||
// }
|
||||
|
||||
export default function Page() {
|
||||
const [pageContent, setPageContent] = useState("")
|
||||
|
||||
const handleDrawerContentChange = (newContent) => {
|
||||
setPageContent(newContent);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
<div className="pt-16 px-8 pb-4 flex-grow">
|
||||
<div className="mb-4 flex items-center space-x-4">
|
||||
<Image
|
||||
src="/logo.png" // Ensure the path to your logo is correct
|
||||
alt="Compass Center logo"
|
||||
width={25}
|
||||
height={25}
|
||||
// If you are using TypeScript and Next.js Image component, ensure you have set up 'next/image' in your 'next.config.js' for static import
|
||||
/>
|
||||
<h1 className="font-bold text-2xl text-purple-800">Untitled Page</h1>
|
||||
</div>
|
||||
<Drawer title="Sidebar Component" editableContent={pageContent} onSave={handleDrawerContentChange}>{pageContent}</Drawer>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -9,7 +9,7 @@ export default function Page() {
|
|||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
{/* icon + title */}
|
||||
<div className="pt-16 px-8 pb-4 flex-grow">
|
||||
<div className="pt-16 px-8 pb-4 flex-row">
|
||||
<div className="mb-4 flex items-center space-x-4">
|
||||
<Image
|
||||
src="/logo.png"
|
||||
|
|
23
compass/components/PageLayout.tsx
Normal file
23
compass/components/PageLayout.tsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
interface PageLayoutProps {
|
||||
icon: React.ReactElement;
|
||||
title: string;
|
||||
children: React.ReactElement
|
||||
}
|
||||
|
||||
export const PageLayout: React.FC<PageLayoutProps> = ({ icon, title, children }) => {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
{/* icon + title */}
|
||||
<div className="pt-16 px-8 pb-4 flex-row">
|
||||
<div className="mb-4 flex items-center space-x-4">
|
||||
<span className="w-6 h-6 text-purple-200">{icon}</span>
|
||||
<h1 className='font-bold text-2xl text-purple-800'>{title}</h1>
|
||||
</div>
|
||||
</div>
|
||||
{/* data */}
|
||||
<div className="px-8 py-8">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
45
compass/components/Table.tsx
Normal file
45
compass/components/Table.tsx
Normal file
|
@ -0,0 +1,45 @@
|
|||
import { Component } from "react"
|
||||
|
||||
// interface TableHeader {
|
||||
// title: string,
|
||||
// type: string
|
||||
// }
|
||||
|
||||
// interface TableRow {
|
||||
// [key: string]: any,
|
||||
// }
|
||||
interface TableProps {
|
||||
headersData: string[];
|
||||
data: { [key: string]: any }[];
|
||||
}
|
||||
|
||||
const Table: React.FC<TableProps> = ({ headersData, data }) => {
|
||||
const headers = headersData.map((header, i) => {
|
||||
return <th key={"header-" + i} >{header}</th>
|
||||
})
|
||||
|
||||
console.log(data);
|
||||
|
||||
const rows = data.map((item) => {
|
||||
const row = headersData.map(key => {
|
||||
return <td key={"item-" + item.id + key}>{item[key]}</td>
|
||||
});
|
||||
return <tr key={"item-" + item.id}>{row}</tr>
|
||||
})
|
||||
|
||||
return (<>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
{headers}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows}
|
||||
</tbody>
|
||||
</table>
|
||||
</>);
|
||||
|
||||
}
|
||||
|
||||
export default Table
|
113
compass/components/Table/Index.tsx
Normal file
113
compass/components/Table/Index.tsx
Normal file
|
@ -0,0 +1,113 @@
|
|||
// for showcasing to compass
|
||||
|
||||
import usersImport from "./users.json";
|
||||
import {
|
||||
ColumnDef,
|
||||
createColumnHelper,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import { useState } from "react";
|
||||
import { RowOptionMenu } from "./RowOptionMenu";
|
||||
import { RowOpenAction } from "./RowOpenAction";
|
||||
import { TableAction } from "./TableAction";
|
||||
import { AtSymbolIcon, Bars2Icon } from "@heroicons/react/24/solid";
|
||||
import TagsInput from "../TagsInput/Index";
|
||||
|
||||
const usersExample = usersImport as unknown as User[];
|
||||
|
||||
type User = {
|
||||
id: number;
|
||||
created_at: any;
|
||||
username: string;
|
||||
role: "administrator" | "employee" | "volunteer";
|
||||
email: string;
|
||||
program: "domestic" | "economic" | "community";
|
||||
experience: number;
|
||||
group?: string;
|
||||
};
|
||||
|
||||
|
||||
|
||||
export const Table = () => {
|
||||
const columnHelper = createColumnHelper<User>();
|
||||
const columns = [
|
||||
columnHelper.display({
|
||||
id: "options",
|
||||
cell: props => <RowOptionMenu />
|
||||
}),
|
||||
columnHelper.accessor("username", {
|
||||
header: () => <><Bars2Icon className="inline align-top h-4" /> Username</>,
|
||||
cell: (info) => <RowOpenAction title={info.getValue()} />,
|
||||
}),
|
||||
columnHelper.accessor("role", {
|
||||
cell: (info) => <TagsInput presetValue={info.getValue() }presetOptions={["administrator","volunteer","employee"]} />,
|
||||
}),
|
||||
columnHelper.accessor("email", {
|
||||
header: () => <><AtSymbolIcon className="inline align-top h-4" /> Email</>,
|
||||
cell: (info) => info.renderValue(),
|
||||
}),
|
||||
columnHelper.accessor("program", {
|
||||
cell: (info) => info.renderValue(),
|
||||
}),
|
||||
];
|
||||
|
||||
const [data, setData] = useState<User[]>([...usersExample]);
|
||||
|
||||
const table = useReactTable({
|
||||
columns,
|
||||
data,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row justify-end">
|
||||
<TableAction />
|
||||
</div>
|
||||
<table className="w-full text-xs text-left rtl:text-right">
|
||||
<thead className="text-xs text-gray-500 capitalize">
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<tr key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header, i) => (
|
||||
<th
|
||||
scope="col"
|
||||
className={
|
||||
"p-2 border-gray-200 border-y font-medium "
|
||||
+ ((1 < i && i < columns.length - 1) ? "border-x" : "")
|
||||
}
|
||||
key={header.id}
|
||||
>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
<tbody className="">
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<tr className="text-gray-800 border-y lowercase hover:bg-gray-50" key={row.id}>
|
||||
{row.getVisibleCells().map((cell, i) => (
|
||||
<td
|
||||
className={
|
||||
"p-2 "
|
||||
+ ((1 < i && i < columns.length - 1) ? "border-x" : "")
|
||||
+ ((i === 0) ? "text-center px-0" : "")
|
||||
}
|
||||
key={cell.id}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
}
|
21
compass/components/Table/RowOpenAction.tsx
Normal file
21
compass/components/Table/RowOpenAction.tsx
Normal file
|
@ -0,0 +1,21 @@
|
|||
import Drawer from "@/components/page/Drawer";
|
||||
import {ChangeEvent, useState} from "react";
|
||||
|
||||
export const RowOpenAction = ({ title }) => {
|
||||
const [pageContent, setPageContent] = useState("")
|
||||
|
||||
const handleDrawerContentChange = (newContent) => {
|
||||
setPageContent(newContent);
|
||||
};
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className="font-semibold group flex flex-row items-center justify-between pr-2">
|
||||
{title}
|
||||
<span >
|
||||
<Drawer title="My Drawer Title" editableContent={pageContent} onSave={handleDrawerContentChange}>{pageContent}</Drawer>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
26
compass/components/Table/RowOptionMenu.tsx
Normal file
26
compass/components/Table/RowOptionMenu.tsx
Normal file
|
@ -0,0 +1,26 @@
|
|||
//delete, duplicate, open
|
||||
import { TrashIcon, DocumentDuplicateIcon, ArrowUpRightIcon, EllipsisVerticalIcon } from "@heroicons/react/24/solid";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
|
||||
|
||||
export const RowOptionMenu = () => {
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const openMenu = () => setMenuOpen(true);
|
||||
const closeMenu = () => setMenuOpen(false);
|
||||
|
||||
|
||||
// TODO: Hide menu if clicked elsewhere
|
||||
|
||||
return (
|
||||
<>
|
||||
<button className="align-center" onClick={() => setMenuOpen(!menuOpen)}><EllipsisVerticalIcon className="h-4"/></button>
|
||||
<div
|
||||
className={"absolute text-left bg-white w-auto p-1 rounded [&>*]:p-1 [&>*]:px-5 [&>*]:rounded" + (!menuOpen ? " invisible" : "")}
|
||||
>
|
||||
<div className="hover:bg-gray-100"><TrashIcon className="inline h-4"/> Delete</div>
|
||||
<div className="hover:bg-gray-100"><DocumentDuplicateIcon className="inline h-4"/> Duplicate</div>
|
||||
<div className="hover:bg-gray-100"><ArrowUpRightIcon className="inline h-4"/> Open</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
36
compass/components/Table/TableAction.tsx
Normal file
36
compass/components/Table/TableAction.tsx
Normal file
|
@ -0,0 +1,36 @@
|
|||
import { MagnifyingGlassIcon } from "@heroicons/react/24/solid";
|
||||
import { useRef, useState } from "react";
|
||||
|
||||
export const TableAction = () => {
|
||||
const searchInput = useRef<HTMLInputElement>(null);
|
||||
const [searchActive, setSearchActive] = useState(false);
|
||||
const activateSearch = () => {
|
||||
setSearchActive(true);
|
||||
if (searchInput.current === null) { return; }
|
||||
searchInput.current.focus();
|
||||
searchInput.current.addEventListener("focusout", () => {
|
||||
if (searchInput.current?.value.trim() === "") {
|
||||
searchInput.current.value = "";
|
||||
deactivateSearch();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const deactivateSearch = () => setSearchActive(false);
|
||||
|
||||
return (
|
||||
<div className="w-auto flex flex-row gap-x-0.5 items-center justify-between text-xs font-medium text-gray-500 p-2">
|
||||
<span className="p-1 rounded hover:bg-gray-100">Filter</span>
|
||||
<span className="p-1 rounded hover:bg-gray-100">Sort</span>
|
||||
<span className="p-1 rounded hover:bg-gray-100" onClick={() => activateSearch()}>
|
||||
<MagnifyingGlassIcon className="w-4 h-4 inline" />
|
||||
</span>
|
||||
<input
|
||||
ref={searchInput}
|
||||
className={"outline-none transition-all duration-300 " + (searchActive ? "w-48" : "w-0")}
|
||||
type="text"
|
||||
name="search"
|
||||
placeholder="Type to search..." />
|
||||
</div>
|
||||
);
|
||||
};
|
1
compass/components/Table/users.json
Normal file
1
compass/components/Table/users.json
Normal file
|
@ -0,0 +1 @@
|
|||
[{"id":0,"created_at":1711482132230,"username":"Bo_Pfeffer","role":"ADMIN","email":"Bo.Pfeffer@gmail.com","program":"DOMESTIC","experience":2,"group":""},{"id":1,"created_at":1711482132231,"username":"Marianna_Heathcote76","role":"ADMIN","email":"Marianna_Heathcote14@yahoo.com","program":"DOMESTIC","experience":1,"group":""},{"id":2,"created_at":1711482132231,"username":"Queenie_Schroeder","role":"VOLUNTEER","email":"Queenie_Schroeder@yahoo.com","program":"COMMUNITY","experience":5,"group":""},{"id":3,"created_at":1711482132231,"username":"Arne.Bode","role":"VOLUNTEER","email":"Arne.Bode@hotmail.com","program":"DOMESTIC","experience":3,"group":""},{"id":4,"created_at":1711482132231,"username":"Maia.Zulauf9","role":"ADMIN","email":"Maia_Zulauf@gmail.com","program":"DOMESTIC","experience":5,"group":""},{"id":5,"created_at":1711482132231,"username":"River_Bauch","role":"EMPLOYEE","email":"River.Bauch@yahoo.com","program":"ECONOMIC","experience":2,"group":""},{"id":6,"created_at":1711482132231,"username":"Virgil.Hilll","role":"VOLUNTEER","email":"Virgil.Hilll@yahoo.com","program":"ECONOMIC","experience":3,"group":""},{"id":7,"created_at":1711482132231,"username":"Bridget_Cartwright","role":"ADMIN","email":"Bridget_Cartwright@yahoo.com","program":"ECONOMIC","experience":3,"group":""},{"id":8,"created_at":1711482132231,"username":"Glennie_Keebler64","role":"EMPLOYEE","email":"Glennie_Keebler60@yahoo.com","program":"DOMESTIC","experience":2,"group":""},{"id":9,"created_at":1711482132232,"username":"Orin.Jenkins53","role":"EMPLOYEE","email":"Orin.Jenkins@gmail.com","program":"ECONOMIC","experience":1,"group":""},{"id":10,"created_at":1711482132232,"username":"Zachery.Rosenbaum","role":"ADMIN","email":"Zachery.Rosenbaum@hotmail.com","program":"COMMUNITY","experience":3,"group":""},{"id":11,"created_at":1711482132232,"username":"Phoebe.Ziemann","role":"EMPLOYEE","email":"Phoebe_Ziemann92@gmail.com","program":"COMMUNITY","experience":2,"group":""},{"id":12,"created_at":1711482132232,"username":"Bradford_Conroy53","role":"VOLUNTEER","email":"Bradford_Conroy94@hotmail.com","program":"COMMUNITY","experience":2,"group":""},{"id":13,"created_at":1711482132232,"username":"Florine_Strosin55","role":"VOLUNTEER","email":"Florine.Strosin29@hotmail.com","program":"ECONOMIC","experience":1,"group":""},{"id":14,"created_at":1711482132232,"username":"Constance.Doyle59","role":"EMPLOYEE","email":"Constance_Doyle@hotmail.com","program":"DOMESTIC","experience":3,"group":""},{"id":15,"created_at":1711482132232,"username":"Chauncey_Lockman","role":"ADMIN","email":"Chauncey_Lockman@yahoo.com","program":"DOMESTIC","experience":5,"group":""},{"id":16,"created_at":1711482132232,"username":"Esther_Wuckert-Larson26","role":"EMPLOYEE","email":"Esther_Wuckert-Larson@gmail.com","program":"ECONOMIC","experience":0,"group":""},{"id":17,"created_at":1711482132232,"username":"Jewel.Kunde","role":"VOLUNTEER","email":"Jewel_Kunde29@gmail.com","program":"ECONOMIC","experience":5,"group":""},{"id":18,"created_at":1711482132232,"username":"Hildegard_Parker92","role":"ADMIN","email":"Hildegard_Parker74@yahoo.com","program":"ECONOMIC","experience":2,"group":""},{"id":19,"created_at":1711482132232,"username":"Jordane.Lakin2","role":"ADMIN","email":"Jordane_Lakin@hotmail.com","program":"COMMUNITY","experience":1,"group":""}]
|
10
compass/components/TagsInput/CreateNewTagAction.tsx
Normal file
10
compass/components/TagsInput/CreateNewTagAction.tsx
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { Tag } from "./Tag"
|
||||
|
||||
export const CreateNewTagAction = ({ input }) => {
|
||||
return (
|
||||
<div className="flex flex-row space-x-2 hover:bg-gray-100 rounded-md py-2 p-2 items-center">
|
||||
<p className="capitalize">Create</p>
|
||||
<Tag active={false} onDelete={null} >{input}</Tag>
|
||||
</div>
|
||||
)
|
||||
}
|
49
compass/components/TagsInput/DropdownAction.tsx
Normal file
49
compass/components/TagsInput/DropdownAction.tsx
Normal file
|
@ -0,0 +1,49 @@
|
|||
import { EllipsisHorizontalIcon, TrashIcon } from "@heroicons/react/24/solid";
|
||||
import { useState } from "react";
|
||||
|
||||
export const DropdownAction = ({ tag, handleDeleteTag, handleEditTag }) => {
|
||||
const [isVisible, setVisible] = useState(false);
|
||||
const [inputValue, setInputValue] = useState(tag);
|
||||
|
||||
|
||||
const editTagOption = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleEditTag(tag, inputValue)
|
||||
setVisible(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setInputValue(e.target.value);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
<EllipsisHorizontalIcon
|
||||
className="w-5 text-gray-500"
|
||||
onClick={() => setVisible(!isVisible)}
|
||||
/>
|
||||
{isVisible && (
|
||||
<div className="absolute flex flex-col justify-start z-50 rounded-md bg-white border border-gray-200 shadow p-2 space-y-2">
|
||||
<input
|
||||
type="text"
|
||||
value={inputValue}
|
||||
onChange={handleInputChange}
|
||||
onKeyDown={editTagOption}
|
||||
autoFocus
|
||||
className="bg-gray-50 text-2xs focus:outline-none rounded-md font-normal text-gray-800 p-1 border-2 focus:border-blue-200"
|
||||
/>
|
||||
<button onClick={() => {
|
||||
handleDeleteTag(inputValue)
|
||||
setVisible(false)
|
||||
}} className="justify-start flex flex-row space-x-4 hover:bg-gray-100 rounded-md items-center p-2 px-2">
|
||||
<TrashIcon className="w-3 h-3" />
|
||||
<p>Delete</p>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
122
compass/components/TagsInput/Index.tsx
Normal file
122
compass/components/TagsInput/Index.tsx
Normal file
|
@ -0,0 +1,122 @@
|
|||
import React, { useState } from "react";
|
||||
import "tailwindcss/tailwind.css";
|
||||
import { TagsArray } from "./TagsArray";
|
||||
import { TagDropdown } from "./TagDropdown";
|
||||
import { CreateNewTagAction } from "./CreateNewTagAction";
|
||||
|
||||
interface TagsInputProps {
|
||||
presetOptions: string[];
|
||||
}
|
||||
|
||||
const TagsInput: React.FC<TagsInputProps> = ({
|
||||
presetValue,
|
||||
presetOptions,
|
||||
}) => {
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
const [cellSelected, setCellSelected] = useState(false);
|
||||
const [tags, setTags] = useState<Set<string>>(
|
||||
new Set(presetValue ? [presetValue] : [])
|
||||
);
|
||||
const [options, setOptions] = useState<Set<string>>(new Set(presetOptions));
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setInputValue(e.target.value);
|
||||
};
|
||||
|
||||
const handleAddTag = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "Enter" && inputValue.trim()) {
|
||||
setTags((prevTags) => new Set(prevTags).add(inputValue));
|
||||
setOptions((prevOptions) => new Set(prevOptions).add(inputValue));
|
||||
setInputValue("");
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectTag = (tagToAdd: string) => {
|
||||
if (!tags.has(tagToAdd)) {
|
||||
// Corrected syntax for checking if a Set contains an item
|
||||
setTags((prevTags) => new Set(prevTags).add(tagToAdd));
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteTag = (tagToDelete: string) => {
|
||||
setTags((prevTags) => {
|
||||
const updatedTags = new Set(prevTags);
|
||||
updatedTags.delete(tagToDelete);
|
||||
return updatedTags;
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteTagOption = (tagToDelete: string) => {
|
||||
setOptions((prevOptions) => {
|
||||
const updatedOptions = new Set(prevOptions);
|
||||
updatedOptions.delete(tagToDelete);
|
||||
return updatedOptions;
|
||||
});
|
||||
if (tags.has(tagToDelete)) {
|
||||
handleDeleteTag(tagToDelete);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditTag = (oldTag: string, newTag: string) => {
|
||||
if (oldTag !== newTag) {
|
||||
setTags((prevTags) => {
|
||||
const tagsArray = Array.from(prevTags);
|
||||
const oldTagIndex = tagsArray.indexOf(oldTag);
|
||||
if (oldTagIndex !== -1) {
|
||||
tagsArray.splice(oldTagIndex, 1, newTag);
|
||||
}
|
||||
return new Set(tagsArray);
|
||||
});
|
||||
|
||||
setOptions((prevOptions) => {
|
||||
const optionsArray = Array.from(prevOptions);
|
||||
const oldTagIndex = optionsArray.indexOf(oldTag);
|
||||
if (oldTagIndex !== -1) {
|
||||
optionsArray.splice(oldTagIndex, 1, newTag);
|
||||
}
|
||||
return new Set(optionsArray);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className="cursor-pointer" onClick={() => setCellSelected(true)}>
|
||||
{!cellSelected ? (
|
||||
<TagsArray handleDelete={handleDeleteTag} tags={tags} />
|
||||
) : (
|
||||
<div className="absolute w-64 z-50 -ml-3 -mt-7">
|
||||
<div className="rounded-md border border-gray-200 shadow">
|
||||
<div className="flex flex-wrap rounded-t-md items-center gap-2 bg-gray-50 p-2">
|
||||
<TagsArray handleDelete={handleDeleteTag} active tags={tags} />
|
||||
<input
|
||||
type="text"
|
||||
value={inputValue}
|
||||
placeholder="Search for an option..."
|
||||
onChange={handleInputChange}
|
||||
onKeyDown={handleAddTag}
|
||||
className="focus:outline-none bg-transparent"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<div className="flex rounded-b-md bg-white flex-col border-t border-gray-100 text-2xs font-medium text-gray-500 p-2">
|
||||
<p className="capitalize">Select an option or create one</p>
|
||||
<TagDropdown
|
||||
handleDeleteTag={handleDeleteTagOption}
|
||||
handleEditTag={handleEditTag}
|
||||
handleAdd={handleSelectTag}
|
||||
tags={options}
|
||||
/>
|
||||
{inputValue.length > 0 && (
|
||||
<CreateNewTagAction input={inputValue} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TagsInput;
|
0
compass/components/TagsInput/Input.tsx
Normal file
0
compass/components/TagsInput/Input.tsx
Normal file
15
compass/components/TagsInput/Tag.tsx
Normal file
15
compass/components/TagsInput/Tag.tsx
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { XMarkIcon } from "@heroicons/react/24/solid";
|
||||
|
||||
export const Tag = ({ children, handleDelete, active = false }) => {
|
||||
|
||||
return (
|
||||
<span className={`font-normal bg-cyan-100 text-gray-800 flex flex-row p-1 px-2 rounded-lg`}>
|
||||
{children}
|
||||
{active && handleDelete && (
|
||||
<button onClick={() => handleDelete(children)}>
|
||||
<XMarkIcon className={`ml-1 w-3 text-cyan-500`} />
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
};
|
16
compass/components/TagsInput/TagDropdown.tsx
Normal file
16
compass/components/TagsInput/TagDropdown.tsx
Normal file
|
@ -0,0 +1,16 @@
|
|||
|
||||
import { Tag } from "./Tag";
|
||||
import { DropdownAction } from "./DropdownAction";
|
||||
|
||||
export const TagDropdown = ({ tags, handleEditTag,handleDeleteTag,handleAdd }) => {
|
||||
return (
|
||||
<div className="z-50 flex flex-col space-y-2 mt-2">
|
||||
{Array.from(tags).map((tag, index) => (
|
||||
<div key={index} className="items-center rounded-md p-1 flex flex-row justify-between hover:bg-gray-100">
|
||||
<button onClick={() => handleAdd(tag)}><Tag >{tag}</Tag></button>
|
||||
<DropdownAction handleDeleteTag={handleDeleteTag} handleEditTag={handleEditTag} tag={tag} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
16
compass/components/TagsInput/TagsArray.tsx
Normal file
16
compass/components/TagsInput/TagsArray.tsx
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { Tag } from "./Tag"
|
||||
|
||||
export const TagsArray = ({ tags, handleDelete, active = false }) => {
|
||||
|
||||
return(
|
||||
<div className="flex flex-wrap gap-2 items-center">
|
||||
{
|
||||
Array.from(tags).map((tag) => {
|
||||
return (
|
||||
<Tag handleDelete={handleDelete} active={active}>{tag}</Tag>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -31,7 +31,7 @@ const Sidebar: React.FC<SidebarProps> = ({ setIsSidebarOpen }) => {
|
|||
<div className="flex flex-col space-y-2">
|
||||
<h4 className="text-xs font-semibold text-gray-500">Pages</h4>
|
||||
<nav className="flex flex-col">
|
||||
<SidebarItem icon={<HomeIcon />} text="Home" />
|
||||
<SidebarItem active={true} icon={<HomeIcon />} text="Home" />
|
||||
<SidebarItem icon={<BookmarkIcon />} text="Resources" />
|
||||
<SidebarItem icon={<ClipboardIcon />} text="Services" />
|
||||
<SidebarItem icon={<BookOpenIcon />} text="Training Manuals" />
|
||||
|
|
|
@ -2,11 +2,12 @@
|
|||
interface SidebarItemProps {
|
||||
icon: React.ReactElement;
|
||||
text: string;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
export const SidebarItem: React.FC<SidebarItemProps> = ({ icon, text }) => {
|
||||
export const SidebarItem: React.FC<SidebarItemProps> = ({ icon, text, active }) => {
|
||||
return (
|
||||
<a href="#" className="flex items-center p-2 space-x-2 hover:bg-gray-200 rounded-md">
|
||||
<a href="#" className={active ? "flex items-center p-2 space-x-2 bg-gray-200 rounded-md" : "flex items-center p-2 space-x-2 hover:bg-gray-200 rounded-md"}>
|
||||
<span className="h-5 text-gray-500 w-5">
|
||||
{icon}
|
||||
</span>
|
||||
|
|
8763
compass/package-lock.json
generated
8763
compass/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
@ -1,28 +1,30 @@
|
|||
{
|
||||
"name": "compass",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@heroicons/react": "^2.1.1",
|
||||
"next": "13.5.6",
|
||||
"react": "^18",
|
||||
"react-dom": "^18"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"autoprefixer": "^10",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "13.5.6",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"name": "compass",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@heroicons/react": "^2.1.1",
|
||||
"@tanstack/react-table": "^8.15.0",
|
||||
"next": "13.5.6",
|
||||
"react": "^18",
|
||||
"react-dom": "^18"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"autoprefixer": "^10",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "13.5.6",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user