Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IPK-194-Implement-Active-User #49

Merged
merged 1 commit into from
Apr 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/app/dashboard/bookings/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import React from "react";

function Bookings() {
return <div className="flex flex-col items-center justify-start p-8 px-6">Settings</div>;
}

export default Bookings;
100 changes: 100 additions & 0 deletions src/app/dashboard/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
"use client";
import Image from "next/image";
import { Button } from "@/components/ui/button";
import { LuBookCopy, LuBuilding, LuGauge, LuLayoutDashboard, LuSettings, LuUser2 } from "react-icons/lu";
import React, { useEffect, useState } from "react";
import type { User } from "@/types";
import { getAccessToken, getUser } from "@/lib/actions";
import Link from "next/link";

export default function DashboardLayout({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>();
const [isAdmin, setIsAdmin] = useState(user?.role === "ADMIN");

useEffect(() => {
const fetchUser = async () => {
try {
const accessToken = await getAccessToken();
setUser(await getUser(accessToken));
} catch (error) {
setIsAdmin(false);
console.error("Failed to fetch user", error);
}
};
fetchUser().catch(console.error);
}, []);

useEffect(() => {
setIsAdmin(user?.role === "ADMIN");
}, [user]);
return (
<div className="flex w-full flex-col gap-5 pl-8 md:flex-row">
<aside className="hidden lg:block">
<div className="sticky top-8 flex h-[calc(100vh-64px)] flex-row">
<div className="flex h-full w-[80px] flex-col items-center justify-start">
<div className="absolute mt-10 flex h-16 w-[80px] items-center justify-start rounded-l-md bg-subtle shadow-lg">
<div className="ml-[8px] h-[50%] w-[1px] bg-primary"></div>
</div>
<div className="mt-12 size-12 rounded-md">
<Image width={48} height={48} src="/landingLogo.png" alt="logo" />
</div>
<div className="mt-14 flex flex-col gap-y-6">
<div className="size-12 rounded-md bg-secondary"></div>
<div className="size-12 rounded-md bg-secondary"></div>
<div className="size-12 rounded-md bg-secondary"></div>
<div className="size-12 rounded-md bg-secondary"></div>
</div>
</div>
<div className="flex h-full w-[230px] flex-col items-center justify-start rounded-[20px] border-2 border-primary">
<h1 className="mt-8 text-xl font-bold">MeetMate</h1>
<div className="mt-[35%] flex flex-col items-center justify-start gap-y-4">
<header className="relative left-[-40%] mb-[-6px] text-xs text-muted-foreground">Tools</header>
<Link href={"/dashboard"}>
<Button className="w-[168px] justify-start text-foreground" variant="default">
<LuLayoutDashboard className="mx-2" size={18} />
Dashboard
</Button>
</Link>
<Link href={"/dashboard/bookings"}>
<Button className="w-[168px] justify-start text-muted-foreground" variant="ghost">
<LuBookCopy className="mx-2" size={18} />
Bookings
</Button>
</Link>
<Link href={"/dashboard/settings"}>
<Button className="w-[168px] justify-start text-muted-foreground" variant="ghost">
<LuSettings className="mx-2" size={18} />
Settings
</Button>
</Link>

{isAdmin && (
<React.Fragment>
<header className="relative left-[-40%] mb-[-6px] mt-6 text-xs text-muted-foreground">Admin</header>
<Button className="w-[168px] justify-start text-muted-foreground" variant="ghost">
<LuBuilding className="mx-2" size={18} />
Companies
</Button>
<Button className="w-[168px] justify-start text-muted-foreground" variant="ghost">
<LuUser2 className="mx-2" size={18} />
Users
</Button>
<Button disabled className="w-[168px] justify-start text-muted-foreground" variant="ghost">
<LuGauge className="mx-2" size={18} />
Analytics
</Button>
</React.Fragment>
)}
</div>
</div>
</div>
</aside>
<main className="mr-8 mt-8 min-h-[calc(100vh-64px)] w-full rounded-[20px] border-2 border-border">
{children}
<footer className="flex h-8 w-full items-center justify-start rounded-b-[20px] bg-primary">
<p className="pl-4 text-sm font-medium text-background">MeetMate</p>
</footer>
</main>
</div>
);
}
166 changes: 54 additions & 112 deletions src/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
"use client";
import { Button } from "@/components/ui/button";
import Image from "next/image";
import { Input } from "@/components/ui/input";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import React, { useEffect, useState } from "react";
import { LuLayoutDashboard, LuBookCopy, LuSettings, LuGauge, LuUser2, LuBuilding } from "react-icons/lu";
import { deleteToken, getUser } from "@/lib/actions";
import { deleteToken, getAccessToken, getUser } from "@/lib/actions";
import {
DropdownMenu,
DropdownMenuContent,
Expand All @@ -14,132 +12,76 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger
} from "@/components/ui/dropdown-menu";
import { type User } from "@/types";
import { extractNameInitials } from "@/lib/utils";

function Dashboard() {
const [isAdmin, setIsAdmin] = useState(false);
const [user, setUser] = useState<User | null>();

useEffect(() => {
const fetchUser = async () => {
try {
const accessToken = await getAccessToken();
setUser(await getUser(accessToken));
} catch (error) {
console.error("Failed to fetch user", error);
}
};
fetchUser().catch(console.error);
}, []);

const logout = async () => {
try {
await deleteToken();
window.location.reload();
} catch (error) {
console.error("Logout failed", error);
throw error;
}
};

useEffect(() => {
const checkAdmin = async () => {
const user = await getUser();
setIsAdmin(user?.role === "ADMIN");
};
checkAdmin().catch(() => {
setIsAdmin(false);
});
}, []);
return (
<div className="flex w-full flex-col gap-5 pl-8 md:flex-row">
<aside className="hidden lg:block">
<div className="sticky top-8 flex h-[calc(100vh-64px)] flex-row">
<div className="flex h-full w-[80px] flex-col items-center justify-start">
<div className="absolute mt-10 flex h-16 w-[80px] items-center justify-start rounded-l-md bg-subtle shadow-lg">
<div className="ml-[8px] h-[50%] w-[1px] bg-primary"></div>
</div>
<div className="mt-12 size-12 rounded-md">
<Image width={48} height={48} src="/landingLogo.png" alt="logo" />
</div>
<div className="mt-14 flex flex-col gap-y-6">
<div className="size-12 rounded-md bg-secondary"></div>
<div className="size-12 rounded-md bg-secondary"></div>
<div className="size-12 rounded-md bg-secondary"></div>
<div className="size-12 rounded-md bg-secondary"></div>
</div>
</div>
<div className="flex h-full w-[230px] flex-col items-center justify-start rounded-[20px] border-2 border-primary">
<h1 className="mt-8 text-xl font-bold">MeetMate</h1>
<div className="mt-[35%] flex flex-col items-center justify-start gap-y-4">
<header className="relative left-[-40%] mb-[-6px] text-xs text-muted-foreground">Tools</header>
<Button className="w-[168px] justify-start text-foreground" variant="default">
<LuLayoutDashboard className="mx-2" size={18} />
Dashboard
</Button>
<Button className="w-[168px] justify-start text-muted-foreground" variant="ghost">
<LuBookCopy className="mx-2" size={18} />
Bookings
<div className="flex flex-col items-center justify-start p-8 px-6">
<header className="flex w-full flex-row items-center justify-between">
<h1 className="m-4 font-medium text-muted-foreground md:text-2xl">Welcome back, {user?.name}</h1>
<div className="flex items-center gap-x-6">
<Input className="w-[320px]" placeholder="Search"></Input>
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild className={"mr-4"}>
<Button variant="ghost" className="relative size-8 rounded-full">
<Avatar className="size-10">
<AvatarFallback className={"bg-primary"}>{extractNameInitials(user?.name)}</AvatarFallback>
</Avatar>
</Button>
<Button className="w-[168px] justify-start text-muted-foreground" variant="ghost">
<LuSettings className="mx-2" size={18} />
Settings
</Button>

{isAdmin && (
<React.Fragment>
<header className="relative left-[-40%] mb-[-6px] mt-6 text-xs text-muted-foreground">Admin</header>
<Button className="w-[168px] justify-start text-muted-foreground" variant="ghost">
<LuBuilding className="mx-2" size={18} />
Companies
</Button>
<Button className="w-[168px] justify-start text-muted-foreground" variant="ghost">
<LuUser2 className="mx-2" size={18} />
Users
</Button>
<Button disabled className="w-[168px] justify-start text-muted-foreground" variant="ghost">
<LuGauge className="mx-2" size={18} />
Analytics
</Button>
</React.Fragment>
)}
</div>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align={"end"} className={"w-56 border-border"}>
<DropdownMenuLabel className="font-normal">
<div className="flex flex-col space-y-1">
<p className="text-sm font-medium leading-none">{user?.name}</p>
<p className="text-xs leading-none text-muted-foreground">{user?.email}</p>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem>Settings</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={logout} className={"text-red-500"}>
Log out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</aside>
<main className="mr-8 mt-8 min-h-screen w-full rounded-[20px] border-2 border-border">
<div className="flex flex-col items-center justify-start p-8 px-6">
<header className="flex w-full flex-row items-center justify-between">
<h1 className="m-4 font-medium text-muted-foreground md:text-2xl">Welcome back, Tim</h1>
<div className="flex items-center gap-x-6">
<Input className="w-[320px]" placeholder="Search"></Input>
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild className={"mr-4"}>
<Button variant="ghost" className="relative size-8 rounded-full">
<Avatar className="size-10">
<AvatarFallback className={"bg-primary"}>TL</AvatarFallback>
</Avatar>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align={"end"} className={"w-56 border-border"}>
<DropdownMenuLabel className="font-normal">
<div className="flex flex-col space-y-1">
<p className="text-sm font-medium leading-none">shadcn</p>
<p className="text-xs leading-none text-muted-foreground">[email protected]</p>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem>Settings</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={logout} className={"text-red-500"}>
Log out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</header>
</header>

<div className="mt-8 flex h-[200px] w-full flex-col items-start justify-center gap-2 rounded-[20px] bg-primary pl-12">
<h2 className="text-3xl font-semibold">MeetMate Dashboard</h2>
<p className="text-sm">Create your appointments in minutes</p>
<Button className="mt-2" variant={"secondary"}>
Book now
</Button>
</div>
<div className="mt-8 flex h-[200px] w-full flex-col items-start justify-center gap-2 rounded-[20px] bg-primary pl-12">
<h2 className="text-3xl font-semibold">MeetMate Dashboard</h2>
<p className="text-sm">Create your appointments in minutes</p>
<Button className="mt-2" variant={"secondary"}>
Book now
</Button>
</div>

<div className="mt-8 flex h-[600px] w-full rounded-[20px] bg-subtle"></div>
<div className="mt-8 flex h-[600px] w-full rounded-[20px] bg-subtle"></div>

<div className="h-[50vh]"></div>
</div>
<footer className="flex h-8 w-full items-center justify-start rounded-b-[20px] bg-primary">
<p className="pl-4 text-sm font-medium text-background">MeetMate</p>
</footer>
</main>
<div className="h-[50vh]"></div>
</div>
);
}
Expand Down
5 changes: 5 additions & 0 deletions src/app/dashboard/settings/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
function Settings() {
return <div className="flex flex-col items-center justify-start p-8 px-6">Settings</div>;
}

export default Settings;
3 changes: 3 additions & 0 deletions src/app/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ const LoginForm = () => {
variant: "default",
className: "border-emerald-300"
});
setTimeout(() => {
router.push("/dashboard");
}, 1500);
}
}, [formState, toast, dispatch]);

Expand Down
6 changes: 5 additions & 1 deletion src/lib/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export async function getUser(accessToken?: string): Promise<User | null> {
if (response.ok) {
return (await response.json()) as User;
} else {
console.log(response.status);
console.log("Error getting User", response.status);
return null;
}
} catch (error) {
Expand All @@ -106,6 +106,10 @@ export async function getUser(accessToken?: string): Promise<User | null> {
}
}

export async function getAccessToken() {
return cookies().get("accessToken")?.value;
}

type LoginFormState = {
message: string;
errors: Record<keyof { email: string; password: string }, string> | undefined;
Expand Down
20 changes: 17 additions & 3 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
return twMerge(clsx(inputs));
}

export function extractNameInitials(name: string | undefined) {
if (name === undefined) {
return "";
}
const parts = name.trim().split(/\s+/);
let initials = parts[0][0];

if (parts.length > 1) {
initials += parts[parts.length - 1][0];
}

return initials.toUpperCase();
}
1 change: 0 additions & 1 deletion src/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ export async function middleware(req: NextRequest) {
if (refreshToken !== undefined || refreshToken !== "") {
const newAccessToken = await refreshAccessToken(refreshToken);
if (newAccessToken !== undefined) {
console.log(newAccessToken);
response.cookies.set("accessToken", newAccessToken[0], { httpOnly: true, secure: true, sameSite: "strict" });
response.cookies.set("expires_at", newAccessToken[1], { httpOnly: true, secure: true, sameSite: "strict" });
user = await getUser(newAccessToken[0]);
Expand Down
Loading