feat: implement class selection in header and add API service for fetching classes
This commit is contained in:
parent
16a191341b
commit
da74d1bf82
@ -18,6 +18,7 @@
|
||||
"@nextui-org/system": "^2.4.5",
|
||||
"@nextui-org/theme": "^2.4.4",
|
||||
"axios": "^1.7.9",
|
||||
"axios-hooks": "^5.1.0",
|
||||
"framer-motion": "^11.15.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"moment": "^2.30.1",
|
||||
|
111
src/app/components/Header/contents.tsx
Normal file
111
src/app/components/Header/contents.tsx
Normal file
@ -0,0 +1,111 @@
|
||||
"use client";
|
||||
import { useClassStore } from "@/app/stores/classStore";
|
||||
import { User } from "@/app/types/next-auth";
|
||||
import { getInitials } from "@/app/utils/initial";
|
||||
import {
|
||||
Autocomplete,
|
||||
AutocompleteItem,
|
||||
Avatar,
|
||||
Button,
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
DropdownMenu,
|
||||
DropdownTrigger,
|
||||
Navbar,
|
||||
NavbarBrand,
|
||||
NavbarContent,
|
||||
NavbarItem,
|
||||
} from "@nextui-org/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
import { ThemeSwitcher } from "../ThemeSwitcher/ThemeSwitcher";
|
||||
|
||||
export const HeaderContent = ({ user }: { user?: User }) => {
|
||||
const router = useRouter();
|
||||
const { classes, selectedClass, setSelectedClass, fetchClass } =
|
||||
useClassStore();
|
||||
|
||||
const initials = user?.name ? getInitials(user.name) : "";
|
||||
|
||||
useEffect(() => {
|
||||
fetchClass().then((classesFetched) => {
|
||||
setSelectedClass(classesFetched[0]);
|
||||
});
|
||||
}, [fetchClass, setSelectedClass]);
|
||||
|
||||
return (
|
||||
<Navbar className="mb-2">
|
||||
<NavbarBrand>
|
||||
<p className="font-bold text-inherit">Toogether</p>
|
||||
</NavbarBrand>
|
||||
|
||||
<NavbarContent as="div" justify="center">
|
||||
<Autocomplete
|
||||
size="sm"
|
||||
label="Select an class"
|
||||
value={selectedClass?.name}
|
||||
selectedKey={selectedClass?.id}
|
||||
onSelectionChange={(selectedId) => {
|
||||
console.log(selectedId);
|
||||
const inputSelectedClass = classes.find(
|
||||
(Class) => Class.id === selectedId,
|
||||
);
|
||||
setSelectedClass(inputSelectedClass);
|
||||
}}
|
||||
>
|
||||
{classes.map((Class) => (
|
||||
<AutocompleteItem key={Class.id} isSelected>
|
||||
{Class.name}
|
||||
</AutocompleteItem>
|
||||
))}
|
||||
</Autocomplete>
|
||||
</NavbarContent>
|
||||
|
||||
<NavbarContent as="div" justify="end">
|
||||
{user?.roles.includes("admin") ? (
|
||||
<NavbarItem>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="flat"
|
||||
className="min-w-0"
|
||||
onPress={() => router.push("/admin")}
|
||||
>
|
||||
🔧
|
||||
</Button>
|
||||
</NavbarItem>
|
||||
) : null}
|
||||
|
||||
<NavbarItem>
|
||||
<ThemeSwitcher />
|
||||
</NavbarItem>
|
||||
|
||||
<Dropdown placement="bottom-end">
|
||||
<DropdownTrigger>
|
||||
<Avatar
|
||||
isBordered
|
||||
as="button"
|
||||
className="transition-transform"
|
||||
color="secondary"
|
||||
name={initials}
|
||||
size="sm"
|
||||
/>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu aria-label="Profile Actions" variant="flat">
|
||||
<DropdownItem key="profile" className="h-14 gap-2">
|
||||
<p>Signed in as</p>
|
||||
<p className="font-semibold">{user?.name}</p>
|
||||
</DropdownItem>
|
||||
<DropdownItem key="settings">Settings</DropdownItem>
|
||||
<DropdownItem
|
||||
key="logout"
|
||||
color="danger"
|
||||
href="/auth/logout"
|
||||
>
|
||||
Logout
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</NavbarContent>
|
||||
</Navbar>
|
||||
);
|
||||
};
|
@ -1,124 +1,9 @@
|
||||
"use client";
|
||||
import { useClassStore } from "@/app/stores/classStore";
|
||||
import { User } from "@/app/types/next-auth";
|
||||
import {
|
||||
Autocomplete,
|
||||
AutocompleteItem,
|
||||
Avatar,
|
||||
Button,
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
DropdownMenu,
|
||||
DropdownTrigger,
|
||||
Navbar,
|
||||
NavbarBrand,
|
||||
NavbarContent,
|
||||
NavbarItem,
|
||||
} from "@nextui-org/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
import { ThemeSwitcher } from "../ThemeSwitcher/ThemeSwitcher";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@/authOptions";
|
||||
import { HeaderContent } from "./contents";
|
||||
|
||||
const getInitials = (name: string) => {
|
||||
if (!name) return "";
|
||||
export const Header = async () => {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
const nameParts = name.split(" ");
|
||||
if (nameParts.length === 1) {
|
||||
return name;
|
||||
}
|
||||
|
||||
const firstInitial = nameParts[0]?.[0] || "";
|
||||
const secondInitial = nameParts[1]?.[0] || nameParts[0]?.[1] || "";
|
||||
|
||||
return firstInitial + secondInitial;
|
||||
};
|
||||
|
||||
export const Header = ({ user }: { user?: User }) => {
|
||||
const router = useRouter();
|
||||
const { classes, selectedClass, setSelectedClass, fetchClass } =
|
||||
useClassStore();
|
||||
|
||||
const initials = user?.name ? getInitials(user.name) : "";
|
||||
|
||||
useEffect(() => {
|
||||
fetchClass().then((classesFetched) => {
|
||||
setSelectedClass(classesFetched[0]);
|
||||
});
|
||||
}, [fetchClass, setSelectedClass]);
|
||||
|
||||
return (
|
||||
<Navbar className="mb-2">
|
||||
<NavbarBrand>
|
||||
<p className="font-bold text-inherit">Toogether</p>
|
||||
</NavbarBrand>
|
||||
|
||||
<NavbarContent as="div" justify="center">
|
||||
<Autocomplete
|
||||
size="sm"
|
||||
label="Select an class"
|
||||
value={selectedClass?.name}
|
||||
selectedKey={selectedClass?.id}
|
||||
onSelectionChange={(selectedId) => {
|
||||
console.log(selectedId);
|
||||
const inputSelectedClass = classes.find(
|
||||
(Class) => Class.id === selectedId,
|
||||
);
|
||||
setSelectedClass(inputSelectedClass);
|
||||
}}
|
||||
>
|
||||
{classes.map((Class) => (
|
||||
<AutocompleteItem key={Class.id} isSelected>
|
||||
{Class.name}
|
||||
</AutocompleteItem>
|
||||
))}
|
||||
</Autocomplete>
|
||||
</NavbarContent>
|
||||
|
||||
<NavbarContent as="div" justify="end">
|
||||
{user?.roles.includes("admin") ? (
|
||||
<NavbarItem>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="flat"
|
||||
className="min-w-0"
|
||||
onPress={() => router.push("/admin")}
|
||||
>
|
||||
🔧
|
||||
</Button>
|
||||
</NavbarItem>
|
||||
) : null}
|
||||
|
||||
<NavbarItem>
|
||||
<ThemeSwitcher />
|
||||
</NavbarItem>
|
||||
|
||||
<Dropdown placement="bottom-end">
|
||||
<DropdownTrigger>
|
||||
<Avatar
|
||||
isBordered
|
||||
as="button"
|
||||
className="transition-transform"
|
||||
color="secondary"
|
||||
name={initials}
|
||||
size="sm"
|
||||
/>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu aria-label="Profile Actions" variant="flat">
|
||||
<DropdownItem key="profile" className="h-14 gap-2">
|
||||
<p>Signed in as</p>
|
||||
<p className="font-semibold">{user?.name}</p>
|
||||
</DropdownItem>
|
||||
<DropdownItem key="settings">Settings</DropdownItem>
|
||||
<DropdownItem
|
||||
key="logout"
|
||||
color="danger"
|
||||
href="/auth/logout"
|
||||
>
|
||||
Logout
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</NavbarContent>
|
||||
</Navbar>
|
||||
);
|
||||
return <HeaderContent user={session?.user} />;
|
||||
};
|
||||
|
7
src/app/constants/apiUrl.constant.ts
Normal file
7
src/app/constants/apiUrl.constant.ts
Normal file
@ -0,0 +1,7 @@
|
||||
const NEXT_PUBLIC_API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
export const API_URLS = {
|
||||
class: {
|
||||
all: `${NEXT_PUBLIC_API_URL}/class`,
|
||||
},
|
||||
};
|
@ -1,6 +1,6 @@
|
||||
import { authOptions } from "@/authOptions";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { Header } from "../components/Header";
|
||||
import { HeaderContent } from "../components/Header/contents";
|
||||
|
||||
export default async function Layout({
|
||||
children,
|
||||
@ -11,7 +11,7 @@ export default async function Layout({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Header user={session?.user} />
|
||||
<HeaderContent user={session?.user} />
|
||||
|
||||
<main>{children}</main>
|
||||
</div>
|
||||
|
@ -1,6 +1,4 @@
|
||||
import { authOptions } from "@/authOptions";
|
||||
import { Metadata } from "next";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { Header } from "./components/Header";
|
||||
import { RoomTable } from "./components/Room/Table";
|
||||
|
||||
@ -11,11 +9,9 @@ export const metadata: Metadata = {
|
||||
};
|
||||
|
||||
export default async function HomePage() {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header user={session?.user} />
|
||||
<Header />
|
||||
<main className="flex flex-col gap-8 p-4">
|
||||
<RoomTable />
|
||||
</main>
|
||||
|
7
src/app/services/class.service.ts
Normal file
7
src/app/services/class.service.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { API_URLS } from "../constants/apiUrl.constant";
|
||||
import { axiosInstance } from "../lib/axios";
|
||||
import { Class } from "../stores/classStore";
|
||||
|
||||
const getAll = () => axiosInstance.get<Class[]>(API_URLS.class.all);
|
||||
|
||||
export const classService = { getAll };
|
@ -1,7 +1,7 @@
|
||||
import { create } from "zustand";
|
||||
import { axiosInstance } from "../lib/axios";
|
||||
import { classService } from "../services/class.service";
|
||||
|
||||
type Class = { id: string; name: string; createdAt: string };
|
||||
export type Class = { id: string; name: string; createdAt: string };
|
||||
|
||||
type ClassStoreState = {
|
||||
classes: Class[];
|
||||
@ -34,7 +34,7 @@ export const useClassStore = create<ClassStore>()((set) => ({
|
||||
}));
|
||||
},
|
||||
fetchClass: async () => {
|
||||
const classResponse = await axiosInstance.get<Class[]>("/@me/class");
|
||||
const classResponse = await classService.getAll();
|
||||
useClassStore.getState()._setClass(classResponse.data);
|
||||
return classResponse.data;
|
||||
},
|
||||
|
13
src/app/utils/initial.ts
Normal file
13
src/app/utils/initial.ts
Normal file
@ -0,0 +1,13 @@
|
||||
export const getInitials = (name: string) => {
|
||||
if (!name) return "";
|
||||
|
||||
const nameParts = name.split(" ");
|
||||
if (nameParts.length === 1) {
|
||||
return name;
|
||||
}
|
||||
|
||||
const firstInitial = nameParts[0]?.[0] || "";
|
||||
const secondInitial = nameParts[1]?.[0] || nameParts[0]?.[1] || "";
|
||||
|
||||
return firstInitial + secondInitial;
|
||||
};
|
18
yarn.lock
18
yarn.lock
@ -7,7 +7,7 @@
|
||||
resolved "https://registry.yarnpkg.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz#7bf68b20c0a350f936915fcae06f58e32007ce30"
|
||||
integrity sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==
|
||||
|
||||
"@babel/runtime@^7.20.13":
|
||||
"@babel/runtime@7.26.0", "@babel/runtime@^7.20.13":
|
||||
version "7.26.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.0.tgz#8600c2f595f277c60815256418b85356a65173c1"
|
||||
integrity sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==
|
||||
@ -2598,6 +2598,15 @@ axe-core@^4.10.0:
|
||||
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.10.2.tgz#85228e3e1d8b8532a27659b332e39b7fa0e022df"
|
||||
integrity sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w==
|
||||
|
||||
axios-hooks@^5.1.0:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/axios-hooks/-/axios-hooks-5.1.0.tgz#f9c9e2b9c1418e66a8986624aafb5dbf90d17aff"
|
||||
integrity sha512-tRTll4vPMJ30pLY2uivHJuBXM0nXFKzLWgnSaLzJEHduCdf6d9B/IHENybYcXJ8AngPUYEpkgWNGLKGuWzN+Jw==
|
||||
dependencies:
|
||||
"@babel/runtime" "7.26.0"
|
||||
dequal "2.0.3"
|
||||
lru-cache "^11.0.0"
|
||||
|
||||
axios@^1.7.9:
|
||||
version "1.7.9"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.9.tgz#d7d071380c132a24accda1b2cfc1535b79ec650a"
|
||||
@ -2883,7 +2892,7 @@ delayed-stream@~1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
|
||||
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
|
||||
|
||||
dequal@^2.0.3:
|
||||
dequal@2.0.3, dequal@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be"
|
||||
integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==
|
||||
@ -4043,6 +4052,11 @@ lru-cache@^10.2.0:
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119"
|
||||
integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==
|
||||
|
||||
lru-cache@^11.0.0:
|
||||
version "11.0.2"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-11.0.2.tgz#fbd8e7cf8211f5e7e5d91905c415a3f55755ca39"
|
||||
integrity sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==
|
||||
|
||||
lru-cache@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
|
||||
|
Loading…
Reference in New Issue
Block a user