Frontend loading indicator foster (#47)

* initial layout component but in sidebar only

* loading for sign out

* Add loading functionality for changing pages

---------

Co-authored-by: emmalynf <efoster@unc.edu>
This commit is contained in:
Prajwal Moharana 2024-12-15 22:48:15 -05:00 committed by GitHub
parent 596f648f31
commit fdbf4ffa40
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 165 additions and 10 deletions

View File

@ -24,8 +24,8 @@ This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-opti
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!

View File

@ -15,6 +15,7 @@ export default function RootLayout({
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
const [user, setUser] = useState<User>();
const router = useRouter();
const [loading, setLoading] = useState(true);
useEffect(() => {
async function getUser() {
@ -35,6 +36,7 @@ export default function RootLayout({
);
setUser(await userData.json());
setLoading(false);
}
getUser();
@ -50,6 +52,7 @@ export default function RootLayout({
setIsSidebarOpen={setIsSidebarOpen}
isSidebarOpen={isSidebarOpen}
isAdmin={user.role === Role.ADMIN}
loading={loading}
/>
<div
className={`flex-1 transition duration-300 ease-in-out ${

View File

@ -0,0 +1,67 @@
"use client";
import Sidebar from "@/components/Sidebar/Sidebar";
import React, { useState, useEffect } from "react";
import { createClient } from "@/utils/supabase/client";
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(true);
const [user, setUser] = useState<User | null>(null); // Initialize user as null
const router = useRouter();
useEffect(() => {
async function getUser() {
const supabase = createClient();
const { data, error } = await supabase.auth.getUser();
if (error || !data?.user) {
console.log("User not logged in or error fetching user");
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();
setUser(user); // Set user data after fetching
}
getUser();
}, [router]);
if (!user) {
return <Loading />; // Show loading screen while the user is being fetched
}
return (
<div className="flex">
{/* Sidebar is shared across all pages */}
<Sidebar
setIsSidebarOpen={setIsSidebarOpen}
isSidebarOpen={isSidebarOpen}
name={user.username}
email={user.email}
isAdmin={user.role === Role.ADMIN}
/>
{/* Page content */}
<div
className={`flex-1 transition duration-300 ease-in-out ${
isSidebarOpen ? "ml-64" : "ml-0"
}`}
>
{children} {/* Render page-specific content here */}
</div>
</div>
);
}

View File

@ -0,0 +1,19 @@
/* components/LoadingIcon.module.css */
.loader {
width: 24px; /* Larger for better visibility */
height: 24px;
border: 4px solid #5b21b6; /* Primary color */
border-top: 4px solid #ffffff; /* Contrasting color */
border-radius: 50%;
animation: spin 1s linear infinite; /* Smooth continuous spin */
margin-bottom: 20px;
}
@keyframes spin {
0% {
transform: rotate(0deg); /* Start position */
}
100% {
transform: rotate(360deg); /* Full rotation */
}
}

View File

@ -0,0 +1,14 @@
// components/Loading.js
import styles from "./LoadingIcon.module.css";
const LoadingIcon = () => {
return (
<div className={styles.loadingOverlay}>
<div className={styles.loadingContent}>
<div className={styles.loader}></div>
</div>
</div>
);
};
export default LoadingIcon;

View File

@ -1,4 +1,4 @@
import React from "react";
import React, { useState } from "react";
import {
HomeIcon,
ChevronDoubleLeftIcon,
@ -9,7 +9,9 @@ import {
LockClosedIcon,
} from "@heroicons/react/24/solid";
import { SidebarItem } from "./SidebarItem";
import styles from "./LoadingIcon.module.css";
import { UserProfile } from "../resource/UserProfile";
import LoadingIcon from "./LoadingIcon";
interface SidebarProps {
setIsSidebarOpen: React.Dispatch<React.SetStateAction<boolean>>;
@ -17,6 +19,7 @@ interface SidebarProps {
name: string;
email: string;
isAdmin: boolean;
loading: boolean;
}
const Sidebar: React.FC<SidebarProps> = ({
@ -25,7 +28,9 @@ const Sidebar: React.FC<SidebarProps> = ({
name,
email,
isAdmin: admin,
loading,
}) => {
const [isLoading, setIsLoading] = useState(false);
return (
<>
{/* Button to open the sidebar. */}
@ -62,11 +67,22 @@ const Sidebar: React.FC<SidebarProps> = ({
</button>
</div>
<div className="flex flex-col space-y-8">
{/* user + logout button */}
<div className="flex items-center p-4 space-x-2 border border-gray-200 rounded-md ">
<UserProfile name={name} email={email} />
{/* Loading indicator*/}
{isLoading && (
<div className="fixed top-2 left-2">
<LoadingIcon />
</div>
)}
<div className="flex flex-col space-y-8">
<div className="flex items-center p-4 space-x-2 border rounded-md">
<UserProfile
name={name}
email={email}
setLoading={setIsLoading}
/>
</div>
{/* navigation menu */}
<div className="flex flex-col space-y-2">
<h4 className="text-xs font-semibold text-gray-500">
@ -79,6 +95,7 @@ const Sidebar: React.FC<SidebarProps> = ({
text="Admin"
active={true}
redirect="/admin"
onClick={setIsLoading}
/>
)}
@ -87,24 +104,28 @@ const Sidebar: React.FC<SidebarProps> = ({
text="Home"
active={true}
redirect="/home"
onClick={setIsLoading}
/>
<SidebarItem
icon={<BookmarkIcon />}
text="Resources"
active={true}
redirect="/resource"
onClick={setIsLoading}
/>
<SidebarItem
icon={<ClipboardIcon />}
text="Services"
active={true}
redirect="/service"
onClick={setIsLoading}
/>
<SidebarItem
icon={<BookOpenIcon />}
text="Training Manuals"
active={true}
redirect="/training-manuals"
onClick={setIsLoading}
/>
</nav>
</div>

View File

@ -5,6 +5,7 @@ interface SidebarItemProps {
text: string;
active: boolean;
redirect: string;
onClick: React.Dispatch<React.SetStateAction<boolean>>;
}
export const SidebarItem: React.FC<SidebarItemProps> = ({
@ -12,9 +13,11 @@ export const SidebarItem: React.FC<SidebarItemProps> = ({
text,
active,
redirect,
onClick,
}) => {
return (
<Link
onClick={() => onClick(true)}
href={redirect}
className={
active

View File

@ -0,0 +1,22 @@
// components/LoggingOut.js
import styles from "./Loading.module.css";
import Image from "next/image";
const LoggingOut = () => {
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}>Signing out...</h1>
<div className={styles.loadingSpinner}></div>
</div>
</div>
);
};
export default LoggingOut;

View File

@ -1,15 +1,21 @@
import { useState } from "react";
import { signOut } from "@/app/auth/actions";
interface UserProfileProps {
name: string;
email: string;
setLoading: React.Dispatch<React.SetStateAction<boolean>>;
}
const handleClick = async (event: React.MouseEvent<HTMLButtonElement>) => {
const handleClick = async (
event: React.MouseEvent<HTMLButtonElement>,
setLoading: React.Dispatch<React.SetStateAction<boolean>>
) => {
setLoading(true);
await signOut();
};
export const UserProfile = ({ name, email }: UserProfileProps) => {
export const UserProfile = ({ name, email, setLoading }: UserProfileProps) => {
return (
<div className="flex flex-col items-start space-y-2">
<div className="flex flex-col">
@ -19,7 +25,7 @@ export const UserProfile = ({ name, email }: UserProfileProps) => {
<span className="text-xs text-gray-500">{email}</span>
</div>
<button
onClick={handleClick}
onClick={(event) => handleClick(event, setLoading)}
className="text-red-600 font-semibold text-xs hover:underline mt-1"
>
Sign out