Compare commits
4 Commits
96a0c5c4b0
...
1684ac2743
Author | SHA1 | Date | |
---|---|---|---|
1684ac2743 | |||
da74d1bf82 | |||
16a191341b | |||
946bc68946 |
@ -18,6 +18,7 @@
|
|||||||
"@nextui-org/system": "^2.4.5",
|
"@nextui-org/system": "^2.4.5",
|
||||||
"@nextui-org/theme": "^2.4.4",
|
"@nextui-org/theme": "^2.4.4",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
|
"axios-hooks": "^5.1.0",
|
||||||
"framer-motion": "^11.15.0",
|
"framer-motion": "^11.15.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
@ -27,6 +28,7 @@
|
|||||||
"react": "19.0.0-rc-66855b96-20241106",
|
"react": "19.0.0-rc-66855b96-20241106",
|
||||||
"react-dom": "19.0.0-rc-66855b96-20241106",
|
"react-dom": "19.0.0-rc-66855b96-20241106",
|
||||||
"react-icons": "^5.4.0",
|
"react-icons": "^5.4.0",
|
||||||
|
"swr": "^2.3.0",
|
||||||
"zustand": "^5.0.2"
|
"zustand": "^5.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
119
src/app/components/Header/contents.tsx
Normal file
119
src/app/components/Header/contents.tsx
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
"use client";
|
||||||
|
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";
|
||||||
|
import { useUserStore } from "@/app/stores/userStore";
|
||||||
|
import { useClassStore } from "@/app/stores/classStore";
|
||||||
|
|
||||||
|
export const HeaderContent = ({ user }: { user?: User }) => {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const { classes, fetchClass } = useClassStore();
|
||||||
|
const {
|
||||||
|
currentClassId: selectedClassId,
|
||||||
|
setCurrentClassId: setSelectedClassId,
|
||||||
|
} = useUserStore();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchClass();
|
||||||
|
}, [fetchClass]);
|
||||||
|
|
||||||
|
const selectedClass = classes.find((Class) => Class.id === selectedClassId);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedClass && classes.length > 0)
|
||||||
|
setSelectedClassId(classes[0].id);
|
||||||
|
}, [selectedClass, classes, setSelectedClassId]);
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
const inputSelectedClass = classes.find(
|
||||||
|
(Class) => Class.id === selectedId,
|
||||||
|
);
|
||||||
|
if (inputSelectedClass)
|
||||||
|
setSelectedClassId(inputSelectedClass.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{classes.map((Class) => (
|
||||||
|
<AutocompleteItem key={Class.id}>
|
||||||
|
{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={user?.name ? getInitials(user.name) : ""}
|
||||||
|
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,126 +1,9 @@
|
|||||||
"use client";
|
import { getServerSession } from "next-auth";
|
||||||
import { User } from "@/app/types/next-auth";
|
import { authOptions } from "@/authOptions";
|
||||||
import {
|
import { HeaderContent } from "./contents";
|
||||||
Autocomplete,
|
|
||||||
AutocompleteItem,
|
|
||||||
Avatar,
|
|
||||||
Button,
|
|
||||||
Dropdown,
|
|
||||||
DropdownItem,
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownTrigger,
|
|
||||||
Navbar,
|
|
||||||
NavbarBrand,
|
|
||||||
NavbarContent,
|
|
||||||
NavbarItem,
|
|
||||||
} from "@nextui-org/react";
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { ThemeSwitcher } from "../ThemeSwitcher/ThemeSwitcher";
|
|
||||||
import { useClassStore } from "@/app/stores/classStore";
|
|
||||||
import { useEffect, useRef } from "react";
|
|
||||||
|
|
||||||
const getInitials = (name: string) => {
|
export const Header = async () => {
|
||||||
if (!name) return "";
|
const session = await getServerSession(authOptions);
|
||||||
|
|
||||||
const nameParts = name.split(" ");
|
return <HeaderContent user={session?.user} />;
|
||||||
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 listClassesRef = useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
const initials = user?.name ? getInitials(user.name) : "";
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchClass().then(() => {
|
|
||||||
if (selectedClass) setSelectedClass(classes[0]);
|
|
||||||
});
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Navbar className="mb-2">
|
|
||||||
<NavbarBrand>
|
|
||||||
<p className="font-bold text-inherit">Toogether</p>
|
|
||||||
</NavbarBrand>
|
|
||||||
|
|
||||||
<NavbarContent as="div" justify="center">
|
|
||||||
<Autocomplete
|
|
||||||
ref={listClassesRef}
|
|
||||||
size="sm"
|
|
||||||
label="Select an class"
|
|
||||||
value={selectedClass?.name}
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
@ -4,7 +4,7 @@ import { RoomCard } from "./Card";
|
|||||||
import { Room } from "./Room";
|
import { Room } from "./Room";
|
||||||
import { SkeletonRoomCard } from "./SkeletonRoomCard";
|
import { SkeletonRoomCard } from "./SkeletonRoomCard";
|
||||||
|
|
||||||
export const RoomList = ({ rooms }: { rooms: Room[] }) => {
|
export const RoomList = ({ rooms }: { rooms: Room[] | null }) => {
|
||||||
const scrollContainerRef = useRef<HTMLUListElement>(null);
|
const scrollContainerRef = useRef<HTMLUListElement>(null);
|
||||||
|
|
||||||
const handleWheel = (event: WheelEvent) => {
|
const handleWheel = (event: WheelEvent) => {
|
||||||
@ -47,12 +47,18 @@ export const RoomList = ({ rooms }: { rooms: Room[] }) => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
if (rooms?.length == 0) return (
|
||||||
|
<div className="flex gap-2 rounded-xl bg-default-100 p-4">
|
||||||
|
<p className="text-default-500 text-center w-full">No rooms found</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul
|
<ul
|
||||||
ref={scrollContainerRef}
|
ref={scrollContainerRef}
|
||||||
className="flex gap-2 overflow-x-auto scrollbar-hide rounded-xl bg-default-100 p-1"
|
className="flex gap-2 overflow-x-auto scrollbar-hide rounded-xl bg-default-100 p-1"
|
||||||
>
|
>
|
||||||
{rooms.length > 0 ? (
|
{rooms ? (
|
||||||
rooms.map((room) => (
|
rooms.map((room) => (
|
||||||
<li key={room.id}>
|
<li key={room.id}>
|
||||||
<RoomCard
|
<RoomCard
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { useRoomStore } from "@/app/stores/roomStore";
|
import { useRoomStore } from "@/app/stores/roomStore";
|
||||||
|
import { useUserStore } from "@/app/stores/userStore";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { RoomList } from "./List";
|
import { RoomList } from "./List";
|
||||||
import { useClassStore } from "@/app/stores/classStore";
|
|
||||||
|
|
||||||
export const RoomTable = () => {
|
export const RoomTable = () => {
|
||||||
const { fetchRooms, actual, future, past } = useRoomStore();
|
const { fetchRooms, actual, future, past } = useRoomStore();
|
||||||
const { selectedClass } = useClassStore();
|
const { currentClassId: selectedClassId } = useUserStore();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchRooms();
|
if (selectedClassId) fetchRooms();
|
||||||
}, [fetchRooms, selectedClass]);
|
}, [fetchRooms, selectedClassId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
|
@ -10,7 +10,7 @@ export const Sidebar = () => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="fixed top-0 left-0 w-64 h-screen transition-transform -translate-x-full sm:translate-x-0">
|
<aside className="fixed top-0 left-0 w-64 h-screen">
|
||||||
<div className="flex flex-col gap-2 h-full px-3 py-4 overflow-y-auto bg-foreground-100">
|
<div className="flex flex-col gap-2 h-full px-3 py-4 overflow-y-auto bg-foreground-100">
|
||||||
<ul className="font-medium gap-2">
|
<ul className="font-medium gap-2">
|
||||||
<li>
|
<li>
|
||||||
|
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 { authOptions } from "@/authOptions";
|
||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import { Header } from "../components/Header";
|
import { HeaderContent } from "../components/Header/contents";
|
||||||
|
|
||||||
export default async function Layout({
|
export default async function Layout({
|
||||||
children,
|
children,
|
||||||
@ -11,7 +11,7 @@ export default async function Layout({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Header user={session?.user} />
|
<HeaderContent user={session?.user} />
|
||||||
|
|
||||||
<main>{children}</main>
|
<main>{children}</main>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
import { authOptions } from "@/authOptions";
|
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
import { getServerSession } from "next-auth";
|
|
||||||
import { Header } from "./components/Header";
|
import { Header } from "./components/Header";
|
||||||
import { RoomTable } from "./components/Room/Table";
|
import { RoomTable } from "./components/Room/Table";
|
||||||
|
|
||||||
@ -11,11 +9,9 @@ export const metadata: Metadata = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default async function HomePage() {
|
export default async function HomePage() {
|
||||||
const session = await getServerSession(authOptions);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header user={session?.user} />
|
<Header />
|
||||||
<main className="flex flex-col gap-8 p-4">
|
<main className="flex flex-col gap-8 p-4">
|
||||||
<RoomTable />
|
<RoomTable />
|
||||||
</main>
|
</main>
|
||||||
|
6
src/app/services/class.service.ts
Normal file
6
src/app/services/class.service.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { API_URLS } from "../constants/apiUrl.constant";
|
||||||
|
import { axiosInstance } from "../lib/axios";
|
||||||
|
|
||||||
|
const getAll = () => axiosInstance.get<Class[]>(API_URLS.class.all);
|
||||||
|
|
||||||
|
export const classService = { getAll };
|
@ -1,24 +1,19 @@
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { axiosInstance } from "../lib/axios";
|
import { classService } from "../services/class.service";
|
||||||
|
|
||||||
type Class = { id: string; name: string; createdAt: string };
|
|
||||||
|
|
||||||
type ClassStoreState = {
|
type ClassStoreState = {
|
||||||
classes: Class[];
|
classes: Class[];
|
||||||
selectedClass: Class | undefined | null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type ClassStoreActions = {
|
type ClassStoreActions = {
|
||||||
_setClass: (classes: Class[]) => void;
|
_setClass: (classes: Class[]) => void;
|
||||||
fetchClass: () => Promise<void>;
|
fetchClass: () => Promise<Class[]>;
|
||||||
setSelectedClass: (selectedClass: Class | undefined | null) => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type ClassStore = ClassStoreState & ClassStoreActions;
|
type ClassStore = ClassStoreState & ClassStoreActions;
|
||||||
|
|
||||||
const defaultState: ClassStoreState = {
|
const defaultState: ClassStoreState = {
|
||||||
classes: [],
|
classes: [],
|
||||||
selectedClass: null,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useClassStore = create<ClassStore>()((set) => ({
|
export const useClassStore = create<ClassStore>()((set) => ({
|
||||||
@ -28,13 +23,9 @@ export const useClassStore = create<ClassStore>()((set) => ({
|
|||||||
classes: classes,
|
classes: classes,
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
setSelectedClass: (selectedClass: Class | undefined | null) => {
|
|
||||||
set(() => ({
|
|
||||||
selectedClass: selectedClass,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
fetchClass: async () => {
|
fetchClass: async () => {
|
||||||
const classResponse = await axiosInstance.get<Class[]>("/@me/class");
|
const classResponse = await classService.getAll();
|
||||||
useClassStore.getState()._setClass(classResponse.data);
|
useClassStore.getState()._setClass(classResponse.data);
|
||||||
|
return classResponse.data;
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
@ -2,12 +2,12 @@ import moment from "moment";
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { Room } from "../components/Room/Room";
|
import { Room } from "../components/Room/Room";
|
||||||
import { axiosInstance } from "../lib/axios";
|
import { axiosInstance } from "../lib/axios";
|
||||||
import { useClassStore } from "./classStore";
|
import { useUserStore } from "./userStore";
|
||||||
|
|
||||||
type RoomStoreState = {
|
type RoomStoreState = {
|
||||||
future: Room[];
|
future: Room[] | null;
|
||||||
actual: Room[];
|
actual: Room[] | null;
|
||||||
past: Room[];
|
past: Room[] | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type RoomStoreActions = {
|
type RoomStoreActions = {
|
||||||
@ -18,9 +18,9 @@ type RoomStoreActions = {
|
|||||||
type RoomStore = RoomStoreState & RoomStoreActions;
|
type RoomStore = RoomStoreState & RoomStoreActions;
|
||||||
|
|
||||||
const defaultState: RoomStoreState = {
|
const defaultState: RoomStoreState = {
|
||||||
future: [],
|
future: null,
|
||||||
actual: [],
|
actual: null,
|
||||||
past: [],
|
past: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useRoomStore = create<RoomStore>()((set) => ({
|
export const useRoomStore = create<RoomStore>()((set) => ({
|
||||||
@ -38,11 +38,11 @@ export const useRoomStore = create<RoomStore>()((set) => ({
|
|||||||
set({ future, actual, past });
|
set({ future, actual, past });
|
||||||
},
|
},
|
||||||
fetchRooms: () => {
|
fetchRooms: () => {
|
||||||
const classSelected = useClassStore.getState().selectedClass;
|
const selectedClassId = useUserStore.getState().currentClassId;
|
||||||
if (!classSelected) return;
|
if (!selectedClassId) return;
|
||||||
|
|
||||||
axiosInstance
|
axiosInstance
|
||||||
.get<Room[]>(`/@me/class/${classSelected.id}/rooms`)
|
.get<Room[]>(`/@me/class/${selectedClassId}/rooms`)
|
||||||
.then((classes) => {
|
.then((classes) => {
|
||||||
useRoomStore.getState()._setRooms(classes.data);
|
useRoomStore.getState()._setRooms(classes.data);
|
||||||
});
|
});
|
||||||
|
31
src/app/stores/userStore.ts
Normal file
31
src/app/stores/userStore.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { create } from "zustand";
|
||||||
|
import { persist, createJSONStorage } from "zustand/middleware";
|
||||||
|
|
||||||
|
type UserStoreState = {
|
||||||
|
currentClassId: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type UserStoreActions = {
|
||||||
|
setCurrentClassId: (classId: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type UserStore = UserStoreState & UserStoreActions;
|
||||||
|
|
||||||
|
const defaultState: UserStoreState = {
|
||||||
|
currentClassId: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUserStore = create<UserStore>()(
|
||||||
|
persist(
|
||||||
|
(set) => ({
|
||||||
|
...defaultState,
|
||||||
|
setCurrentClassId: (classId: string) => {
|
||||||
|
set({ currentClassId: classId });
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: "userStore",
|
||||||
|
storage: createJSONStorage(() => localStorage),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
5
src/app/types/class.d.ts
vendored
Normal file
5
src/app/types/class.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
interface Class {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
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;
|
||||||
|
};
|
34
yarn.lock
34
yarn.lock
@ -7,7 +7,7 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz#7bf68b20c0a350f936915fcae06f58e32007ce30"
|
resolved "https://registry.yarnpkg.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz#7bf68b20c0a350f936915fcae06f58e32007ce30"
|
||||||
integrity sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==
|
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"
|
version "7.26.0"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.0.tgz#8600c2f595f277c60815256418b85356a65173c1"
|
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.0.tgz#8600c2f595f277c60815256418b85356a65173c1"
|
||||||
integrity sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==
|
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"
|
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.10.2.tgz#85228e3e1d8b8532a27659b332e39b7fa0e022df"
|
||||||
integrity sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w==
|
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:
|
axios@^1.7.9:
|
||||||
version "1.7.9"
|
version "1.7.9"
|
||||||
resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.9.tgz#d7d071380c132a24accda1b2cfc1535b79ec650a"
|
resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.9.tgz#d7d071380c132a24accda1b2cfc1535b79ec650a"
|
||||||
@ -2883,6 +2892,11 @@ delayed-stream@~1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
|
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
|
||||||
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
|
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
|
||||||
|
|
||||||
|
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==
|
||||||
|
|
||||||
detect-libc@^2.0.3:
|
detect-libc@^2.0.3:
|
||||||
version "2.0.3"
|
version "2.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700"
|
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700"
|
||||||
@ -4038,6 +4052,11 @@ lru-cache@^10.2.0:
|
|||||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119"
|
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119"
|
||||||
integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==
|
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:
|
lru-cache@^6.0.0:
|
||||||
version "6.0.0"
|
version "6.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
|
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
|
||||||
@ -4898,6 +4917,14 @@ supports-preserve-symlinks-flag@^1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
|
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
|
||||||
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
|
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
|
||||||
|
|
||||||
|
swr@^2.3.0:
|
||||||
|
version "2.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/swr/-/swr-2.3.0.tgz#66fa45023efd4199f4e7ce608c255709a135943d"
|
||||||
|
integrity sha512-NyZ76wA4yElZWBHzSgEJc28a0u6QZvhb6w0azeL2k7+Q1gAzVK+IqQYXhVOC/mzi+HZIozrZvBVeSeOZNR2bqA==
|
||||||
|
dependencies:
|
||||||
|
dequal "^2.0.3"
|
||||||
|
use-sync-external-store "^1.4.0"
|
||||||
|
|
||||||
tailwind-merge@^1.14.0:
|
tailwind-merge@^1.14.0:
|
||||||
version "1.14.0"
|
version "1.14.0"
|
||||||
resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-1.14.0.tgz#e677f55d864edc6794562c63f5001f45093cdb8b"
|
resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-1.14.0.tgz#e677f55d864edc6794562c63f5001f45093cdb8b"
|
||||||
@ -5100,6 +5127,11 @@ use-latest@^1.2.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
use-isomorphic-layout-effect "^1.1.1"
|
use-isomorphic-layout-effect "^1.1.1"
|
||||||
|
|
||||||
|
use-sync-external-store@^1.4.0:
|
||||||
|
version "1.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz#adbc795d8eeb47029963016cefdf89dc799fcebc"
|
||||||
|
integrity sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==
|
||||||
|
|
||||||
util-deprecate@^1.0.2:
|
util-deprecate@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
||||||
|
Loading…
Reference in New Issue
Block a user