Intialize resource, service, and training-manual routes

This commit is contained in:
pmoharana-cmd 2024-04-24 19:36:28 -04:00
parent 150dde1b0a
commit dfed92816d
14 changed files with 701 additions and 61 deletions

View File

@ -28,7 +28,7 @@ export default function RootLayout({
if (error) {
console.log("Accessed admin page but not logged in");
router.push("auth/login");
router.push("/auth/login");
return;
}
@ -42,7 +42,7 @@ export default function RootLayout({
console.log(
`Accessed admin page but incorrect permissions: ${user.username} ${user.role}`
);
router.push("auth/login");
router.push("/auth/login");
return;
}

View File

@ -21,8 +21,8 @@ export async function login(email: string, password: string) {
const supabaseUser = await supabase.auth.getUser();
if (!supabaseUser.data.user) {
revalidatePath("/resource", "layout");
redirect("/resource");
revalidatePath("/home", "layout");
redirect("/home");
}
const apiData = await fetch(
@ -37,8 +37,8 @@ export async function login(email: string, password: string) {
redirect("/admin");
}
revalidatePath("/resource", "layout");
redirect("/resource");
revalidatePath("/home", "layout");
redirect("/home");
}
export async function signOut() {
@ -53,6 +53,6 @@ export async function signOut() {
await supabase.auth.signOut();
revalidatePath("/resource", "layout");
revalidatePath("/auth/login", "layout");
redirect("/auth/login");
}

View File

@ -25,7 +25,7 @@ export default function Page() {
async function checkUser() {
const { data } = await supabase.auth.getUser();
if (data.user) {
router.push("/resource");
router.push("/home");
}
}
checkUser();

View File

@ -0,0 +1,89 @@
"use client";
import Sidebar from "@/components/Sidebar/Sidebar";
import React, { useState } from "react";
import { ChevronDoubleRightIcon } from "@heroicons/react/24/outline";
import { createClient } from "@/utils/supabase/client";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
import User, { Role } from "@/utils/models/User";
import Loading from "@/components/auth/Loading";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const [user, setUser] = useState<User>();
const router = useRouter();
useEffect(() => {
async function getUser() {
const supabase = createClient();
const { data, error } = await supabase.auth.getUser();
console.log(data, error);
if (error) {
console.log("Accessed home page but not logged in");
router.push("/auth/login");
return;
}
const userData = await fetch(
`${process.env.NEXT_PUBLIC_HOST}/api/user?uuid=${data.user.id}`
);
setUser(await userData.json());
}
getUser();
}, [router]);
return (
<div className="flex-row">
{user ? (
<div>
{/* 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
name={user.username}
email={user.email}
setIsSidebarOpen={setIsSidebarOpen}
isAdmin={user.role === Role.ADMIN}
/>
</div>
{/* page ui */}
<div
className={`flex-1 transition duration-300 ease-in-out ${
isSidebarOpen ? "ml-64" : "ml-0"
}`}
>
{children}
</div>
</div>
) : (
<Loading />
)}
</div>
);
}

61
compass/app/home/page.tsx Normal file
View File

@ -0,0 +1,61 @@
"use client";
import Callout from "@/components/resource/Callout";
import Card from "@/components/resource/Card";
import { LandingSearchBar } from "@/components/resource/LandingSearchBar";
import {
BookOpenIcon,
BookmarkIcon,
ClipboardIcon,
} from "@heroicons/react/24/solid";
import Image from "next/image";
import Link from "next/link";
export default function Page() {
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">
<Image
src="/logo.png"
alt="Compass Center logo."
width={25}
height={25}
/>
<h1 className="font-bold text-2xl text-purple-800">
Compass Center Advocate Landing Page
</h1>
</div>
<Callout>
Welcome! Below you will find a list of resources for the
Compass Center&apos;s trained advocates. These materials
serve to virtually provide a collection of advocacy,
resource, and hotline manuals and information.
<b>
{" "}
If you are an advocate looking for the contact
information of a particular Compass Center employee,
please directly contact your staff back-up or the person
in charge of your training.
</b>
</Callout>
</div>
<div className="p-8 flex-grow border-t border-gray-200 bg-gray-50">
{/* link to different pages */}
<div className="grid grid-cols-3 gap-6 pb-6">
<Link href="/resource">
<Card icon={<BookmarkIcon />} text="Resources" />
</Link>
<Link href="/service">
<Card icon={<ClipboardIcon />} text="Services" />
</Link>
<Link href="/training-manual">
<Card icon={<BookOpenIcon />} text="Training Manuals" />
</Link>
</div>
{/* search bar */}
<LandingSearchBar />
</div>
</div>
);
}

View File

@ -1,10 +1,11 @@
"use client";
import Sidebar from "@/components/Sidebar/Sidebar";
import React, { useState } from "react";
import { ChevronDoubleRightIcon } from "@heroicons/react/24/outline";
import { createClient } from "@/utils/supabase/client";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import User, { Role } from "@/utils/models/User";
import Loading from "@/components/auth/Loading";
@ -14,8 +15,8 @@ export default function RootLayout({
children: React.ReactNode;
}) {
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const [user, setUser] = useState<User>();
const router = useRouter();
const [user, setUser] = useState<User>();
useEffect(() => {
async function getUser() {
@ -27,7 +28,7 @@ export default function RootLayout({
if (error) {
console.log("Accessed resource page but not logged in");
router.push("auth/login");
router.push("/auth/login");
return;
}
@ -35,7 +36,9 @@ export default function RootLayout({
`${process.env.NEXT_PUBLIC_HOST}/api/user?uuid=${data.user.id}`
);
setUser(await userData.json());
const user: User = await userData.json();
setUser(user);
}
getUser();
@ -66,9 +69,9 @@ export default function RootLayout({
} w-64 transition duration-300 ease-in-out`}
>
<Sidebar
setIsSidebarOpen={setIsSidebarOpen}
name={user.username}
email={user.email}
setIsSidebarOpen={setIsSidebarOpen}
isAdmin={user.role === Role.ADMIN}
/>
</div>

View File

@ -1,54 +1,45 @@
"use client";
import Callout from "@/components/resource/Callout";
import Card from "@/components/resource/Card";
import { LandingSearchBar } from "@/components/resource/LandingSearchBar";
import {
BookOpenIcon,
BookmarkIcon,
ClipboardIcon,
} from "@heroicons/react/24/solid";
import Image from "next/image";
import { PageLayout } from "@/components/PageLayout";
import { Table } from "@/components/Table/Index";
import User from "@/utils/models/User";
import { createClient } from "@/utils/supabase/client";
import { UsersIcon } from "@heroicons/react/24/solid";
import { useEffect, useState } from "react";
export default function Page() {
const [users, setUsers] = useState<User[]>([]);
useEffect(() => {
async function getUser() {
const supabase = createClient();
const { data, error } = await supabase.auth.getUser();
if (error) {
console.log("Accessed admin page but not logged in");
return;
}
const userListData = await fetch(
`${process.env.NEXT_PUBLIC_HOST}/api/user/all?uuid=${data.user.id}`
);
const users: User[] = await userListData.json();
setUsers(users);
}
getUser();
}, []);
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">
<Image
src="/logo.png"
alt="Compass Center logo."
width={25}
height={25}
/>
<h1 className="font-bold text-2xl text-purple-800">
Compass Center Advocate Landing Page
</h1>
</div>
<Callout>
Welcome! Below you will find a list of resources for the
Compass Center&apos;s trained advocates. These materials
serve to virtually provide a collection of advocacy,
resource, and hotline manuals and information.
<b>
{" "}
If you are an advocate looking for the contact
information of a particular Compass Center employee,
please directly contact your staff back-up or the person
in charge of your training.
</b>
</Callout>
</div>
<div className="p-8 flex-grow border-t border-gray-200 bg-gray-50">
{/* link to different pages */}
<div className="grid grid-cols-3 gap-6 pb-6">
<Card icon={<BookmarkIcon />} text="Resources" />
<Card icon={<ClipboardIcon />} text="Services" />
<Card icon={<BookOpenIcon />} text="Training Manuals" />
</div>
{/* search bar */}
<LandingSearchBar />
</div>
<PageLayout title="Users" icon={<UsersIcon />}>
<Table users={users} />
</PageLayout>
</div>
);
}

View File

@ -0,0 +1,100 @@
"use client";
import Sidebar from "@/components/Sidebar/Sidebar";
import React, { useState } from "react";
import { ChevronDoubleRightIcon } from "@heroicons/react/24/outline";
import { createClient } from "@/utils/supabase/client";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import User, { Role } from "@/utils/models/User";
import Loading from "@/components/auth/Loading";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const router = useRouter();
const [user, setUser] = useState<User>();
useEffect(() => {
async function getUser() {
const supabase = createClient();
const { data, error } = await supabase.auth.getUser();
console.log(data, error);
if (error) {
console.log("Accessed admin page but not logged in");
router.push("/auth/login");
return;
}
const userData = await fetch(
`${process.env.NEXT_PUBLIC_HOST}/api/user?uuid=${data.user.id}`
);
const user: User = await userData.json();
if (user.role !== Role.ADMIN) {
console.log(
`Accessed admin page but incorrect permissions: ${user.username} ${user.role}`
);
router.push("/auth/login");
return;
}
setUser(user);
}
getUser();
}, [router]);
return (
<div className="flex-row">
{user ? (
<div>
{/* 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}
name={user.username}
email={user.email}
isAdmin={user.role === Role.ADMIN}
/>
</div>
{/* page ui */}
<div
className={`flex-1 transition duration-300 ease-in-out ${
isSidebarOpen ? "ml-64" : "ml-0"
}`}
>
{children}
</div>
</div>
) : (
<Loading />
)}
</div>
);
}

View File

@ -0,0 +1,45 @@
"use client";
import { PageLayout } from "@/components/PageLayout";
import { Table } from "@/components/Table/Index";
import User from "@/utils/models/User";
import { createClient } from "@/utils/supabase/client";
import { UsersIcon } from "@heroicons/react/24/solid";
import { useEffect, useState } from "react";
export default function Page() {
const [users, setUsers] = useState<User[]>([]);
useEffect(() => {
async function getUser() {
const supabase = createClient();
const { data, error } = await supabase.auth.getUser();
if (error) {
console.log("Accessed admin page but not logged in");
return;
}
const userListData = await fetch(
`${process.env.NEXT_PUBLIC_HOST}/api/user/all?uuid=${data.user.id}`
);
const users: User[] = await userListData.json();
setUsers(users);
}
getUser();
}, []);
return (
<div className="min-h-screen flex flex-col">
{/* icon + title */}
<PageLayout title="Users" icon={<UsersIcon />}>
<Table users={users} />
</PageLayout>
</div>
);
}

View File

@ -0,0 +1,40 @@
// page.tsx
import React from "react";
import Head from "next/head";
import Link from "next/link";
const ComingSoonPage: React.FC = () => {
return (
<>
<Head>
<title>Training Manuals - Coming Soon</title>
<meta
name="description"
content="Our training manuals page is coming soon. Stay tuned for updates!"
/>
</Head>
<div className="min-h-screen bg-gradient-to-r from-purple-600 to-blue-500 flex items-center justify-center">
<div className="text-center">
<h1 className="text-4xl font-bold text-white mb-4">
Training Manuals
</h1>
<p className="text-xl text-white mb-8">
Our training manuals page is under construction.
</p>
<p className="text-lg text-white">
Stay tuned for updates!
</p>
<div className="mt-8">
<Link href="/home">
<button className="bg-white text-purple-600 font-semibold py-2 px-4 rounded-full shadow-md hover:bg-purple-100 transition duration-300">
Notify Me
</button>
</Link>
</div>
</div>
</div>
</>
);
};
export default ComingSoonPage;

View File

@ -5,6 +5,7 @@ import {
BookmarkIcon,
ClipboardIcon,
BookOpenIcon,
LockClosedIcon,
} from "@heroicons/react/24/solid";
import { SidebarItem } from "./SidebarItem";
import { UserProfile } from "../resource/UserProfile";
@ -47,7 +48,7 @@ const Sidebar: React.FC<SidebarProps> = ({
<nav className="flex flex-col">
{admin && (
<SidebarItem
icon={<HomeIcon />}
icon={<LockClosedIcon />}
text="Admin"
active={true}
redirect="/admin"
@ -58,7 +59,7 @@ const Sidebar: React.FC<SidebarProps> = ({
icon={<HomeIcon />}
text="Home"
active={true}
redirect="/resource"
redirect="/home"
/>
<SidebarItem
icon={<BookmarkIcon />}

View File

@ -0,0 +1,306 @@
// for showcasing to compass
import users from "./users.json";
import {
Cell,
ColumnDef,
Row,
createColumnHelper,
flexRender,
getCoreRowModel,
getFilteredRowModel,
sortingFns,
useReactTable,
} from "@tanstack/react-table";
import {
ChangeEvent,
useState,
useEffect,
FunctionComponent,
useRef,
ChangeEventHandler,
Key,
} from "react";
import { RowOptionMenu } from "./RowOptionMenu";
import { RowOpenAction } from "./RowOpenAction";
import { TableAction } from "./TableAction";
import {
AtSymbolIcon,
Bars2Icon,
ArrowDownCircleIcon,
PlusIcon,
} from "@heroicons/react/24/solid";
import TagsInput from "../TagsInput/Index";
import { rankItem } from "@tanstack/match-sorter-utils";
import User from "@/utils/models/User";
// For search
const fuzzyFilter = (
row: Row<any>,
columnId: string,
value: any,
addMeta: (meta: any) => void
) => {
// Rank the item
const itemRank = rankItem(row.getValue(columnId), value);
// Store the ranking info
addMeta(itemRank);
// Return if the item should be filtered in/out
return itemRank.passed;
};
export const Table = ({ users }: { users: User[] }) => {
const columnHelper = createColumnHelper<User>();
useEffect(() => {
const sortedUsers = [...users].sort((a, b) =>
a.visible === b.visible ? 0 : a.visible ? -1 : 1
);
setData(sortedUsers);
}, [users]);
const deleteUser = (userId: number) => {
console.log(data);
setData((currentData) =>
currentData.filter((user) => user.id !== userId)
);
};
const hideUser = (userId: number) => {
console.log(`Toggling visibility for user with ID: ${userId}`);
setData((currentData) => {
const newData = currentData
.map((user) => {
if (user.id === userId) {
return { ...user, visible: !user.visible };
}
return user;
})
.sort((a, b) =>
a.visible === b.visible ? 0 : a.visible ? -1 : 1
);
console.log(newData);
return newData;
});
};
const [presetOptions, setPresetOptions] = useState([
"administrator",
"volunteer",
"employee",
]);
const [tagColors, setTagColors] = useState(new Map());
const getTagColor = (tag: string) => {
if (!tagColors.has(tag)) {
const colors = [
"bg-cyan-100",
"bg-blue-100",
"bg-green-100",
"bg-yellow-100",
"bg-purple-100",
];
const randomColor =
colors[Math.floor(Math.random() * colors.length)];
setTagColors(new Map(tagColors).set(tag, randomColor));
}
return tagColors.get(tag);
};
const columns = [
columnHelper.display({
id: "options",
cell: (props) => (
<RowOptionMenu
onDelete={() => deleteUser(props.row.original.id)}
onHide={() => hideUser(props.row.original.id)}
/>
),
}),
columnHelper.accessor("username", {
header: () => (
<>
<Bars2Icon className="inline align-top h-4" /> Username
</>
),
cell: (info) => (
<RowOpenAction
title={info.getValue()}
rowData={info.row.original}
onRowUpdate={handleRowUpdate}
/>
),
}),
columnHelper.accessor("role", {
header: () => (
<>
<ArrowDownCircleIcon className="inline align-top h-4" />{" "}
Role
</>
),
cell: (info) => (
<TagsInput
presetValue={info.getValue()}
presetOptions={presetOptions}
setPresetOptions={setPresetOptions}
getTagColor={getTagColor}
setTagColors={setTagColors}
/>
),
}),
columnHelper.accessor("email", {
header: () => (
<>
<AtSymbolIcon className="inline align-top h-4" /> Email
</>
),
cell: (info) => (
<span className="ml-2 text-gray-500 underline hover:text-gray-400">
{info.getValue()}
</span>
),
}),
columnHelper.accessor("program", {
header: () => (
<>
<ArrowDownCircleIcon className="inline align-top h-4" />{" "}
Program
</>
),
cell: (info) => <TagsInput presetValue={info.getValue()} />,
}),
];
const [data, setData] = useState<User[]>([...users]);
const addUser = () => {
setData([...data]);
};
// Searching
const [query, setQuery] = useState("");
const handleSearchChange = (e: ChangeEvent) => {
const target = e.target as HTMLInputElement;
setQuery(String(target.value));
};
const handleCellChange = (e: ChangeEvent, key: Key) => {
const target = e.target as HTMLInputElement;
console.log(key);
};
// TODO: Filtering
// TODO: Sorting
// added this fn for editing rows
const handleRowUpdate = (updatedRow: User) => {
const dataIndex = data.findIndex((row) => row.id === updatedRow.id);
if (dataIndex !== -1) {
const updatedData = [...data];
updatedData[dataIndex] = updatedRow;
setData(updatedData);
}
};
const table = useReactTable({
columns,
data,
filterFns: {
fuzzy: fuzzyFilter,
},
state: {
globalFilter: query,
},
onGlobalFilterChange: setQuery,
globalFilterFn: fuzzyFilter,
getCoreRowModel: getCoreRowModel(),
});
const handleRowData = (row: any) => {
const rowData: any = {};
row.cells.forEach((cell: any) => {
rowData[cell.column.id] = cell.value;
});
// Use rowData object containing data from all columns for the current row
console.log(rowData);
return rowData;
};
return (
<div className="flex flex-col">
<div className="flex flex-row justify-end">
<TableAction query={query} handleChange={handleSearchChange} />
</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>
{table.getRowModel().rows.map((row) => {
// Individual row
const isUserVisible = row.original.visible;
const rowClassNames = `text-gray-800 border-y lowercase hover:bg-gray-50 ${
!isUserVisible ? "bg-gray-200 text-gray-500" : ""
}`;
return (
<tr className={rowClassNames} key={row.id}>
{row.getVisibleCells().map((cell, i) => (
<td
key={cell.id}
className={
"[&:nth-child(n+3)]:border-x relative first:text-left first:px-0 last:border-none"
}
>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</td>
))}
</tr>
);
})}
</tbody>
<tfoot>
<tr>
<td
className="p-3 border-y border-gray-200 text-gray-600 hover:bg-gray-50"
colSpan={100}
onClick={addUser}
>
<span className="flex ml-1 text-gray-500">
<PlusIcon className="inline h-4 mr-1" />
New
</span>
</td>
</tr>
</tfoot>
</table>
</div>
);
};

View File

@ -17,7 +17,9 @@ const TagsInput: React.FC<TagsInputProps> = ({
const [inputValue, setInputValue] = useState("");
const [cellSelected, setCellSelected] = useState(false);
const [tags, setTags] = useState<Set<string>>(
new Set(presetValue ? [presetValue] : [])
typeof presetValue === "string"
? new Set([presetValue])
: new Set(presetValue)
);
const [options, setOptions] = useState<Set<string>>(new Set(presetOptions));
const dropdown = useRef<HTMLDivElement>(null);

View File

@ -7,6 +7,8 @@ export interface Tags {
}
export const TagsArray = ({ tags, handleDelete, active = false }: Tags) => {
console.log(tags);
return (
<div className="flex ml-2 flex-wrap gap-2 items-center">
{Array.from(tags).map((tag, index) => {