mirror of
https://github.com/cssgunc/compass.git
synced 2025-04-09 14:00:15 -04:00
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:
parent
596f648f31
commit
fdbf4ffa40
|
@ -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:
|
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.
|
- [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.
|
- [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!
|
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ export default function RootLayout({
|
||||||
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
|
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
|
||||||
const [user, setUser] = useState<User>();
|
const [user, setUser] = useState<User>();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function getUser() {
|
async function getUser() {
|
||||||
|
@ -35,6 +36,7 @@ export default function RootLayout({
|
||||||
);
|
);
|
||||||
|
|
||||||
setUser(await userData.json());
|
setUser(await userData.json());
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
getUser();
|
getUser();
|
||||||
|
@ -50,6 +52,7 @@ export default function RootLayout({
|
||||||
setIsSidebarOpen={setIsSidebarOpen}
|
setIsSidebarOpen={setIsSidebarOpen}
|
||||||
isSidebarOpen={isSidebarOpen}
|
isSidebarOpen={isSidebarOpen}
|
||||||
isAdmin={user.role === Role.ADMIN}
|
isAdmin={user.role === Role.ADMIN}
|
||||||
|
loading={loading}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={`flex-1 transition duration-300 ease-in-out ${
|
className={`flex-1 transition duration-300 ease-in-out ${
|
||||||
|
|
67
compass/components/RootLayout.tsx
Normal file
67
compass/components/RootLayout.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
19
compass/components/Sidebar/LoadingIcon.module.css
Normal file
19
compass/components/Sidebar/LoadingIcon.module.css
Normal 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 */
|
||||||
|
}
|
||||||
|
}
|
14
compass/components/Sidebar/LoadingIcon.tsx
Normal file
14
compass/components/Sidebar/LoadingIcon.tsx
Normal 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;
|
|
@ -1,4 +1,4 @@
|
||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
import {
|
import {
|
||||||
HomeIcon,
|
HomeIcon,
|
||||||
ChevronDoubleLeftIcon,
|
ChevronDoubleLeftIcon,
|
||||||
|
@ -9,7 +9,9 @@ import {
|
||||||
LockClosedIcon,
|
LockClosedIcon,
|
||||||
} from "@heroicons/react/24/solid";
|
} from "@heroicons/react/24/solid";
|
||||||
import { SidebarItem } from "./SidebarItem";
|
import { SidebarItem } from "./SidebarItem";
|
||||||
|
import styles from "./LoadingIcon.module.css";
|
||||||
import { UserProfile } from "../resource/UserProfile";
|
import { UserProfile } from "../resource/UserProfile";
|
||||||
|
import LoadingIcon from "./LoadingIcon";
|
||||||
|
|
||||||
interface SidebarProps {
|
interface SidebarProps {
|
||||||
setIsSidebarOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
setIsSidebarOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
@ -17,6 +19,7 @@ interface SidebarProps {
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
|
loading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Sidebar: React.FC<SidebarProps> = ({
|
const Sidebar: React.FC<SidebarProps> = ({
|
||||||
|
@ -25,7 +28,9 @@ const Sidebar: React.FC<SidebarProps> = ({
|
||||||
name,
|
name,
|
||||||
email,
|
email,
|
||||||
isAdmin: admin,
|
isAdmin: admin,
|
||||||
|
loading,
|
||||||
}) => {
|
}) => {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Button to open the sidebar. */}
|
{/* Button to open the sidebar. */}
|
||||||
|
@ -62,11 +67,22 @@ const Sidebar: React.FC<SidebarProps> = ({
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col space-y-8">
|
{/* Loading indicator*/}
|
||||||
{/* user + logout button */}
|
{isLoading && (
|
||||||
<div className="flex items-center p-4 space-x-2 border border-gray-200 rounded-md ">
|
<div className="fixed top-2 left-2">
|
||||||
<UserProfile name={name} email={email} />
|
<LoadingIcon />
|
||||||
</div>
|
</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 */}
|
{/* navigation menu */}
|
||||||
<div className="flex flex-col space-y-2">
|
<div className="flex flex-col space-y-2">
|
||||||
<h4 className="text-xs font-semibold text-gray-500">
|
<h4 className="text-xs font-semibold text-gray-500">
|
||||||
|
@ -79,6 +95,7 @@ const Sidebar: React.FC<SidebarProps> = ({
|
||||||
text="Admin"
|
text="Admin"
|
||||||
active={true}
|
active={true}
|
||||||
redirect="/admin"
|
redirect="/admin"
|
||||||
|
onClick={setIsLoading}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -87,24 +104,28 @@ const Sidebar: React.FC<SidebarProps> = ({
|
||||||
text="Home"
|
text="Home"
|
||||||
active={true}
|
active={true}
|
||||||
redirect="/home"
|
redirect="/home"
|
||||||
|
onClick={setIsLoading}
|
||||||
/>
|
/>
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
icon={<BookmarkIcon />}
|
icon={<BookmarkIcon />}
|
||||||
text="Resources"
|
text="Resources"
|
||||||
active={true}
|
active={true}
|
||||||
redirect="/resource"
|
redirect="/resource"
|
||||||
|
onClick={setIsLoading}
|
||||||
/>
|
/>
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
icon={<ClipboardIcon />}
|
icon={<ClipboardIcon />}
|
||||||
text="Services"
|
text="Services"
|
||||||
active={true}
|
active={true}
|
||||||
redirect="/service"
|
redirect="/service"
|
||||||
|
onClick={setIsLoading}
|
||||||
/>
|
/>
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
icon={<BookOpenIcon />}
|
icon={<BookOpenIcon />}
|
||||||
text="Training Manuals"
|
text="Training Manuals"
|
||||||
active={true}
|
active={true}
|
||||||
redirect="/training-manuals"
|
redirect="/training-manuals"
|
||||||
|
onClick={setIsLoading}
|
||||||
/>
|
/>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,6 +5,7 @@ interface SidebarItemProps {
|
||||||
text: string;
|
text: string;
|
||||||
active: boolean;
|
active: boolean;
|
||||||
redirect: string;
|
redirect: string;
|
||||||
|
onClick: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SidebarItem: React.FC<SidebarItemProps> = ({
|
export const SidebarItem: React.FC<SidebarItemProps> = ({
|
||||||
|
@ -12,9 +13,11 @@ export const SidebarItem: React.FC<SidebarItemProps> = ({
|
||||||
text,
|
text,
|
||||||
active,
|
active,
|
||||||
redirect,
|
redirect,
|
||||||
|
onClick,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
|
onClick={() => onClick(true)}
|
||||||
href={redirect}
|
href={redirect}
|
||||||
className={
|
className={
|
||||||
active
|
active
|
||||||
|
|
22
compass/components/auth/LoggingOut.tsx
Normal file
22
compass/components/auth/LoggingOut.tsx
Normal 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;
|
|
@ -1,15 +1,21 @@
|
||||||
|
import { useState } from "react";
|
||||||
import { signOut } from "@/app/auth/actions";
|
import { signOut } from "@/app/auth/actions";
|
||||||
|
|
||||||
interface UserProfileProps {
|
interface UserProfileProps {
|
||||||
name: string;
|
name: string;
|
||||||
email: 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();
|
await signOut();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const UserProfile = ({ name, email }: UserProfileProps) => {
|
export const UserProfile = ({ name, email, setLoading }: UserProfileProps) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-start space-y-2">
|
<div className="flex flex-col items-start space-y-2">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
|
@ -19,7 +25,7 @@ export const UserProfile = ({ name, email }: UserProfileProps) => {
|
||||||
<span className="text-xs text-gray-500">{email}</span>
|
<span className="text-xs text-gray-500">{email}</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={handleClick}
|
onClick={(event) => handleClick(event, setLoading)}
|
||||||
className="text-red-600 font-semibold text-xs hover:underline mt-1"
|
className="text-red-600 font-semibold text-xs hover:underline mt-1"
|
||||||
>
|
>
|
||||||
Sign out
|
Sign out
|
||||||
|
|
Loading…
Reference in New Issue
Block a user