mirror of
https://github.com/cssgunc/compass.git
synced 2025-04-09 14:00:15 -04:00
Add loading animations and authentication handling
This commit is contained in:
parent
e13100e0f7
commit
f0fabead01
|
@ -15,7 +15,7 @@ openapi_tags = {
|
||||||
# TODO: Add security using HTTP Bearer Tokens
|
# TODO: Add security using HTTP Bearer Tokens
|
||||||
# TODO: Enable authorization by passing user uuid to API
|
# TODO: Enable authorization by passing user uuid to API
|
||||||
# TODO: Create custom exceptions
|
# TODO: Create custom exceptions
|
||||||
@api.get("all", response_model=List[User], tags=["Users"])
|
@api.get("/all", response_model=List[User], tags=["Users"])
|
||||||
def get_all(user_id: str, user_svc: UserService = Depends()):
|
def get_all(user_id: str, user_svc: UserService = Depends()):
|
||||||
subject = user_svc.get_user_by_uuid(user_id)
|
subject = user_svc.get_user_by_uuid(user_id)
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,11 @@
|
||||||
import Sidebar from "@/components/resource/Sidebar";
|
import Sidebar from "@/components/resource/Sidebar";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { ChevronDoubleRightIcon } from "@heroicons/react/24/outline";
|
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({
|
export default function RootLayout({
|
||||||
children,
|
children,
|
||||||
|
@ -10,33 +15,86 @@ export default function RootLayout({
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
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 (
|
return (
|
||||||
<div className="flex-row">
|
<div className="flex-row">
|
||||||
{/* button to open sidebar */}
|
{user ? (
|
||||||
<button
|
<div>
|
||||||
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
|
{/* button to open sidebar */}
|
||||||
className={`fixed z-20 p-2 text-gray-500 hover:text-gray-800 left-0`}
|
<button
|
||||||
aria-label={"Open sidebar"}
|
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
|
!isSidebarOpen && (
|
||||||
}
|
<ChevronDoubleRightIcon className="h-5 w-5" />
|
||||||
</button>
|
) // Icon for closing the sidebar
|
||||||
{/* sidebar */}
|
}
|
||||||
<div
|
</button>
|
||||||
className={`absolute inset-y-0 left-0 transform ${isSidebarOpen ? "translate-x-0" : "-translate-x-full"} w-64 transition duration-300 ease-in-out`}
|
{/* sidebar */}
|
||||||
>
|
<div
|
||||||
<Sidebar setIsSidebarOpen={setIsSidebarOpen} />
|
className={`absolute inset-y-0 left-0 transform ${
|
||||||
</div>
|
isSidebarOpen
|
||||||
{/* page ui */}
|
? "translate-x-0"
|
||||||
<div
|
: "-translate-x-full"
|
||||||
className={`flex-1 transition duration-300 ease-in-out ${isSidebarOpen ? "ml-64" : "ml-0"}`}
|
} w-64 transition duration-300 ease-in-out`}
|
||||||
>
|
>
|
||||||
{children}
|
<Sidebar
|
||||||
</div>
|
setIsSidebarOpen={setIsSidebarOpen}
|
||||||
|
name={user.username}
|
||||||
|
email={user.email}
|
||||||
|
admin={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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,15 +2,43 @@
|
||||||
|
|
||||||
import { PageLayout } from "@/components/PageLayout";
|
import { PageLayout } from "@/components/PageLayout";
|
||||||
import { Table } from "@/components/Table/Index";
|
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 { UsersIcon } from "@heroicons/react/24/solid";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
export default function Page() {
|
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 (
|
return (
|
||||||
<div className="min-h-screen flex flex-col">
|
<div className="min-h-screen flex flex-col">
|
||||||
{/* icon + title */}
|
{/* icon + title */}
|
||||||
<PageLayout title="Users" icon={<UsersIcon />}>
|
<PageLayout title="Users" icon={<UsersIcon />}>
|
||||||
<Table />
|
<Table users={users} />
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
14
compass/app/api/user/all/route.ts
Normal file
14
compass/app/api/user/all/route.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export async function GET(request: Request) {
|
||||||
|
const apiEndpoint = `${process.env.NEXT_PUBLIC_API_HOST}/api/user/all`;
|
||||||
|
|
||||||
|
console.log(apiEndpoint);
|
||||||
|
|
||||||
|
const { searchParams } = new URL(request.url);
|
||||||
|
const uuid = searchParams.get("uuid");
|
||||||
|
|
||||||
|
const data = await fetch(`${apiEndpoint}?user_id=${uuid}`);
|
||||||
|
|
||||||
|
return NextResponse.json(await data.json(), { status: data.status });
|
||||||
|
}
|
|
@ -1,6 +1,5 @@
|
||||||
// pages/index.tsx
|
// pages/index.tsx
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Button from "@/components/Button";
|
import Button from "@/components/Button";
|
||||||
import Input from "@/components/Input";
|
import Input from "@/components/Input";
|
||||||
import InlineLink from "@/components/InlineLink";
|
import InlineLink from "@/components/InlineLink";
|
||||||
|
@ -14,30 +13,26 @@ import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const supabase = createClient();
|
|
||||||
|
|
||||||
async function checkUser() {
|
|
||||||
const { data } = await supabase.auth.getUser();
|
|
||||||
|
|
||||||
if (data.user) {
|
|
||||||
router.push("/resource");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
checkUser();
|
|
||||||
}, [router]);
|
|
||||||
|
|
||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [emailError, setEmailError] = useState("");
|
const [emailError, setEmailError] = useState("");
|
||||||
const [passwordError, setPasswordError] = useState("");
|
const [passwordError, setPasswordError] = useState("");
|
||||||
const [loginError, setLoginError] = useState("");
|
const [loginError, setLoginError] = useState("");
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const supabase = createClient();
|
||||||
|
async function checkUser() {
|
||||||
|
const { data } = await supabase.auth.getUser();
|
||||||
|
if (data.user) {
|
||||||
|
router.push("/resource");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
checkUser();
|
||||||
|
}, [router]);
|
||||||
|
|
||||||
const handleEmailChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleEmailChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setEmail(event.currentTarget.value);
|
setEmail(event.currentTarget.value);
|
||||||
setEmail;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePasswordChange = (
|
const handlePasswordChange = (
|
||||||
|
@ -51,28 +46,28 @@ export default function Page() {
|
||||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
|
||||||
if (email.trim().length === 0) {
|
if (email.trim().length === 0) {
|
||||||
console.log(email);
|
|
||||||
setEmailError("Please enter your email.");
|
setEmailError("Please enter your email.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!emailRegex.test(email)) {
|
if (!emailRegex.test(email)) {
|
||||||
setEmailError("Please enter a valid email address.");
|
setEmailError("Please enter a valid email address.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setEmailError("");
|
setEmailError("");
|
||||||
|
|
||||||
if (password.trim().length === 0) {
|
if (password.trim().length === 0) {
|
||||||
console.log(password);
|
|
||||||
setPasswordError("Please enter your password.");
|
setPasswordError("Please enter your password.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setPasswordError("");
|
setPasswordError("");
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
const error = await login(email, password);
|
const error = await login(email, password);
|
||||||
setLoginError(error);
|
setIsLoading(false);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
setLoginError(error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -83,13 +78,11 @@ export default function Page() {
|
||||||
width={100}
|
width={100}
|
||||||
height={91}
|
height={91}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<h1 className="font-bold text-2xl text-purple-800">Login</h1>
|
<h1 className="font-bold text-2xl text-purple-800">Login</h1>
|
||||||
|
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<Input
|
<Input
|
||||||
type="email"
|
type="email"
|
||||||
valid={emailError == ""}
|
valid={emailError === ""}
|
||||||
title="Email"
|
title="Email"
|
||||||
placeholder="Enter Email"
|
placeholder="Enter Email"
|
||||||
onChange={handleEmailChange}
|
onChange={handleEmailChange}
|
||||||
|
@ -97,24 +90,28 @@ export default function Page() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{emailError && <ErrorBanner heading={emailError} />}
|
{emailError && <ErrorBanner heading={emailError} />}
|
||||||
|
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
title="Password"
|
title="Password"
|
||||||
placeholder="Enter Password"
|
placeholder="Enter Password"
|
||||||
valid={passwordError == ""}
|
valid={passwordError === ""}
|
||||||
onChange={handlePasswordChange}
|
onChange={handlePasswordChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{passwordError && <ErrorBanner heading={passwordError} />}
|
{passwordError && <ErrorBanner heading={passwordError} />}
|
||||||
|
|
||||||
<div className="flex flex-col items-left space-y-4">
|
<div className="flex flex-col items-left space-y-4">
|
||||||
<InlineLink href="/auth/forgot_password">
|
<InlineLink href="/auth/forgot_password">
|
||||||
Forgot password?
|
Forgot password?
|
||||||
</InlineLink>
|
</InlineLink>
|
||||||
<Button onClick={handleClick}>Login</Button>
|
<Button onClick={handleClick} disabled={isLoading}>
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
{isLoading && (
|
||||||
|
<div className="w-4 h-4 border-2 border-white border-t-purple-500 rounded-full animate-spin mr-2"></div>
|
||||||
|
)}
|
||||||
|
{isLoading ? "Logging in..." : "Login"}
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{loginError && <ErrorBanner heading={loginError} />}
|
{loginError && <ErrorBanner heading={loginError} />}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,4 +1,9 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
import "../styles/globals.css";
|
import "../styles/globals.css";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { createClient } from "@/utils/supabase/client";
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
// Layouts must accept a children prop.
|
// Layouts must accept a children prop.
|
||||||
|
|
|
@ -6,4 +6,6 @@ export default function Page() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
router.push("/auth/login");
|
router.push("/auth/login");
|
||||||
|
|
||||||
|
return <h1>GO TO LOGIN PAGE (/auth/login)</h1>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,8 +4,10 @@ import React, { useState } from "react";
|
||||||
import { ChevronDoubleRightIcon } from "@heroicons/react/24/outline";
|
import { ChevronDoubleRightIcon } from "@heroicons/react/24/outline";
|
||||||
import { createClient } from "@/utils/supabase/client";
|
import { createClient } from "@/utils/supabase/client";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { User } from "@supabase/supabase-js";
|
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
import User, { Role } from "@/utils/models/User";
|
||||||
|
import Loading from "@/components/auth/Loading";
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
|
@ -14,9 +16,11 @@ export default function RootLayout({
|
||||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||||
const [user, setUser] = useState<User>();
|
const [user, setUser] = useState<User>();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const supabase = createClient();
|
|
||||||
async function getUser() {
|
async function getUser() {
|
||||||
|
const supabase = createClient();
|
||||||
|
|
||||||
const { data, error } = await supabase.auth.getUser();
|
const { data, error } = await supabase.auth.getUser();
|
||||||
|
|
||||||
console.log(data, error);
|
console.log(data, error);
|
||||||
|
@ -26,48 +30,60 @@ export default function RootLayout({
|
||||||
router.push("auth/login");
|
router.push("auth/login");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setUser(data.user);
|
|
||||||
const userData = await fetch(
|
const userData = await fetch(
|
||||||
`${process.env.NEXT_PUBLIC_HOST}/api/user?uuid=${data.user.id}`
|
`${process.env.NEXT_PUBLIC_HOST}/api/user?uuid=${data.user.id}`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
setUser(await userData.json());
|
||||||
}
|
}
|
||||||
|
|
||||||
getUser();
|
getUser();
|
||||||
}, [router]);
|
}, [router]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-row">
|
<div className="flex-row">
|
||||||
{/* button to open sidebar */}
|
{user ? (
|
||||||
<button
|
<div>
|
||||||
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
|
{/* button to open sidebar */}
|
||||||
className={`fixed z-20 p-2 text-gray-500 hover:text-gray-800 left-0`}
|
<button
|
||||||
aria-label={"Open sidebar"}
|
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
|
!isSidebarOpen && (
|
||||||
}
|
<ChevronDoubleRightIcon className="h-5 w-5" />
|
||||||
</button>
|
) // Icon for closing the sidebar
|
||||||
{/* sidebar */}
|
}
|
||||||
<div
|
</button>
|
||||||
className={`absolute inset-y-0 left-0 transform ${
|
{/* sidebar */}
|
||||||
isSidebarOpen ? "translate-x-0" : "-translate-x-full"
|
<div
|
||||||
} w-64 transition duration-300 ease-in-out`}
|
className={`absolute inset-y-0 left-0 transform ${
|
||||||
>
|
isSidebarOpen
|
||||||
<Sidebar
|
? "translate-x-0"
|
||||||
name={""}
|
: "-translate-x-full"
|
||||||
email={(user && user.email) ?? "No email found!"}
|
} w-64 transition duration-300 ease-in-out`}
|
||||||
setIsSidebarOpen={setIsSidebarOpen}
|
>
|
||||||
/>
|
<Sidebar
|
||||||
</div>
|
name={user.username}
|
||||||
{/* page ui */}
|
email={user.email}
|
||||||
<div
|
setIsSidebarOpen={setIsSidebarOpen}
|
||||||
className={`flex-1 transition duration-300 ease-in-out ${
|
admin={user.role === Role.ADMIN}
|
||||||
isSidebarOpen ? "ml-64" : "ml-0"
|
/>
|
||||||
}`}
|
</div>
|
||||||
>
|
{/* page ui */}
|
||||||
{children}
|
<div
|
||||||
</div>
|
className={`flex-1 transition duration-300 ease-in-out ${
|
||||||
|
isSidebarOpen ? "ml-64" : "ml-0"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Loading />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { FunctionComponent, ReactNode } from "react";
|
||||||
type ButtonProps = {
|
type ButtonProps = {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||||
type?: "button" | "submit" | "reset"; // specify possible values for type
|
type?: "button" | "submit" | "reset";
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -13,11 +13,11 @@ const Button: FunctionComponent<ButtonProps> = ({
|
||||||
disabled,
|
disabled,
|
||||||
onClick,
|
onClick,
|
||||||
}) => {
|
}) => {
|
||||||
const buttonClassName = `inline-block rounded border ${
|
const buttonClassName = `inline-flex items-center justify-center rounded border ${
|
||||||
disabled
|
disabled
|
||||||
? "bg-gray-400 text-gray-600 cursor-not-allowed"
|
? "bg-gray-400 text-gray-600 cursor-not-allowed"
|
||||||
: "border-purple-600 bg-purple-600 text-white hover:bg-transparent hover:text-purple-600 focus:outline-none focus:ring active:text-purple-500"
|
: "border-purple-600 bg-purple-600 text-white hover:bg-transparent hover:text-purple-600 focus:outline-none focus:ring active:text-purple-500"
|
||||||
} px-4 py-1 text-md font-semibold w-20 h-10 text-center`;
|
} px-4 py-2 text-md font-semibold w-full sm:w-auto`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
@ -26,7 +26,9 @@ const Button: FunctionComponent<ButtonProps> = ({
|
||||||
type={type}
|
type={type}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
{children}
|
<div className="flex items-center justify-center space-x-2">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// for showcasing to compass
|
// for showcasing to compass
|
||||||
|
|
||||||
import usersImport from "./users.json";
|
import users from "./users.json";
|
||||||
import {
|
import {
|
||||||
Cell,
|
Cell,
|
||||||
ColumnDef,
|
ColumnDef,
|
||||||
|
@ -32,22 +32,7 @@ import {
|
||||||
} from "@heroicons/react/24/solid";
|
} from "@heroicons/react/24/solid";
|
||||||
import TagsInput from "../TagsInput/Index";
|
import TagsInput from "../TagsInput/Index";
|
||||||
import { rankItem } from "@tanstack/match-sorter-utils";
|
import { rankItem } from "@tanstack/match-sorter-utils";
|
||||||
import { TableCell } from "./TableCell";
|
import User from "@/utils/models/User";
|
||||||
import { PrimaryTableCell } from "./PrimaryTableCell";
|
|
||||||
|
|
||||||
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;
|
|
||||||
visible: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
// For search
|
// For search
|
||||||
const fuzzyFilter = (
|
const fuzzyFilter = (
|
||||||
|
@ -66,17 +51,17 @@ const fuzzyFilter = (
|
||||||
return itemRank.passed;
|
return itemRank.passed;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Table = () => {
|
export const Table = ({ users }: { users: User[] }) => {
|
||||||
const columnHelper = createColumnHelper<User>();
|
const columnHelper = createColumnHelper<User>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const sortedUsers = [...usersExample].sort((a, b) =>
|
const sortedUsers = [...users].sort((a, b) =>
|
||||||
a.visible === b.visible ? 0 : a.visible ? -1 : 1
|
a.visible === b.visible ? 0 : a.visible ? -1 : 1
|
||||||
);
|
);
|
||||||
setData(sortedUsers);
|
setData(sortedUsers);
|
||||||
}, []);
|
}, [users]);
|
||||||
|
|
||||||
const deleteUser = (userId) => {
|
const deleteUser = (userId: number) => {
|
||||||
console.log(data);
|
console.log(data);
|
||||||
setData((currentData) =>
|
setData((currentData) =>
|
||||||
currentData.filter((user) => user.id !== userId)
|
currentData.filter((user) => user.id !== userId)
|
||||||
|
@ -188,10 +173,10 @@ export const Table = () => {
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
const [data, setData] = useState<User[]>([...usersExample]);
|
const [data, setData] = useState<User[]>([...users]);
|
||||||
|
|
||||||
const addUser = () => {
|
const addUser = () => {
|
||||||
setData([...data, {}]);
|
setData([...data]);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Searching
|
// Searching
|
||||||
|
|
43
compass/components/auth/Loading.module.css
Normal file
43
compass/components/auth/Loading.module.css
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
/* components/Loading.module.css */
|
||||||
|
.loadingOverlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(255, 255, 255, 0.8);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loadingContent {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loadingTitle {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #5b21b6;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loadingSpinner {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border: 4px solid #5b21b6;
|
||||||
|
border-top: 4px solid #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin: 2rem auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
22
compass/components/auth/Loading.tsx
Normal file
22
compass/components/auth/Loading.tsx
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
// components/Loading.js
|
||||||
|
import styles from "./Loading.module.css";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
|
const Loading = () => {
|
||||||
|
return (
|
||||||
|
<div className={styles.loadingOverlay}>
|
||||||
|
<div className={styles.loadingContent}>
|
||||||
|
<Image
|
||||||
|
src="/logo.png"
|
||||||
|
alt="Compass Center logo."
|
||||||
|
width={100}
|
||||||
|
height={91}
|
||||||
|
/>
|
||||||
|
<h1 className={styles.loadingTitle}>Loading...</h1>
|
||||||
|
<div className={styles.loadingSpinner}></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Loading;
|
|
@ -13,9 +13,15 @@ interface SidebarProps {
|
||||||
setIsSidebarOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
setIsSidebarOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
admin: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Sidebar: React.FC<SidebarProps> = ({ setIsSidebarOpen, name, email }) => {
|
const Sidebar: React.FC<SidebarProps> = ({
|
||||||
|
setIsSidebarOpen,
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
admin,
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="w-64 h-full border border-gray-200 bg-gray-50 px-4">
|
<div className="w-64 h-full border border-gray-200 bg-gray-50 px-4">
|
||||||
{/* button to close sidebar */}
|
{/* button to close sidebar */}
|
||||||
|
@ -39,12 +45,38 @@ const Sidebar: React.FC<SidebarProps> = ({ setIsSidebarOpen, name, email }) => {
|
||||||
Pages
|
Pages
|
||||||
</h4>
|
</h4>
|
||||||
<nav className="flex flex-col">
|
<nav className="flex flex-col">
|
||||||
<SidebarItem icon={<HomeIcon />} text="Home" />
|
{admin && (
|
||||||
<SidebarItem icon={<BookmarkIcon />} text="Resources" />
|
<SidebarItem
|
||||||
<SidebarItem icon={<ClipboardIcon />} text="Services" />
|
icon={<HomeIcon />}
|
||||||
|
text="Admin"
|
||||||
|
active={true}
|
||||||
|
redirect="/admin"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<SidebarItem
|
||||||
|
icon={<HomeIcon />}
|
||||||
|
text="Home"
|
||||||
|
active={true}
|
||||||
|
redirect="/resource"
|
||||||
|
/>
|
||||||
|
<SidebarItem
|
||||||
|
icon={<BookmarkIcon />}
|
||||||
|
text="Resources"
|
||||||
|
active={true}
|
||||||
|
redirect="/resource"
|
||||||
|
/>
|
||||||
|
<SidebarItem
|
||||||
|
icon={<ClipboardIcon />}
|
||||||
|
text="Services"
|
||||||
|
active={true}
|
||||||
|
redirect="/service"
|
||||||
|
/>
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
icon={<BookOpenIcon />}
|
icon={<BookOpenIcon />}
|
||||||
text="Training Manuals"
|
text="Training Manuals"
|
||||||
|
active={true}
|
||||||
|
redirect="/training-manuals"
|
||||||
/>
|
/>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,27 +1,31 @@
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
interface SidebarItemProps {
|
interface SidebarItemProps {
|
||||||
icon: React.ReactElement;
|
icon: React.ReactElement;
|
||||||
text: string;
|
text: string;
|
||||||
active: boolean;
|
active: boolean;
|
||||||
|
redirect: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SidebarItem: React.FC<SidebarItemProps> = ({
|
export const SidebarItem: React.FC<SidebarItemProps> = ({
|
||||||
icon,
|
icon,
|
||||||
text,
|
text,
|
||||||
active,
|
active,
|
||||||
|
redirect,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<a
|
<Link
|
||||||
href="#"
|
href={redirect}
|
||||||
className={
|
className={
|
||||||
active
|
active
|
||||||
? "flex items-center p-2 space-x-2 bg-gray-200 rounded-md"
|
? "flex items-center p-2 my-1 space-x-2 bg-gray-200 rounded-md"
|
||||||
: "flex items-center p-2 space-x-2 hover:bg-gray-200 rounded-md"
|
: "flex items-center p-2 my-1 space-x-2 hover:bg-gray-200 rounded-md"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span className="h-5 text-gray-500 w-5">{icon}</span>
|
<span className="h-5 text-gray-500 w-5">{icon}</span>
|
||||||
<span className="flex-grow font-medium text-xs text-gray-500">
|
<span className="flex-grow font-medium text-xs text-gray-500">
|
||||||
{text}
|
{text}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</Link>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -20,4 +20,5 @@ export default interface User {
|
||||||
program: Program[];
|
program: Program[];
|
||||||
role: Role;
|
role: Role;
|
||||||
created_at: Date;
|
created_at: Date;
|
||||||
|
visible: boolean;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user