Skip to content

Commit dbf49b0

Browse files
committed
feat: implementa layout do usuário logado
1 parent f7971ac commit dbf49b0

23 files changed

Lines changed: 626 additions & 216 deletions

File tree

frontend/src/app/AppRoutes.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { useAuth } from "@/domains/auth/application/AuthContext";
2+
import { AuthenticatedLayout } from "@/app/AuthenticatedLayout";
23
import AuthCallbackPage from "@/domains/auth/presentation/pages/AuthCallbackPage";
34
import LoginPage from "@/domains/auth/presentation/pages/LoginPage";
45
import RegisterPage from "@/domains/auth/presentation/pages/RegisterPage";
5-
import DashboardPage from "@/domains/jobs/presentation/pages/DashboardPage";
6+
import JobsPage from "@/domains/jobs/presentation/pages/JobsPage";
67
import LandingPage from "@/domains/marketing/presentation/pages/LandingPage";
78
import NotFound from "@/app/NotFound";
89
import Loading from "@/shared/ui/Loading";
@@ -18,7 +19,7 @@ function ProtectedRoute({ children }: { children: React.ReactNode }) {
1819
function PublicRoute({ children }: { children: React.ReactNode }) {
1920
const { user, isLoading } = useAuth();
2021
if (isLoading) return <Loading />;
21-
if (user) return <Navigate to="/app" replace />;
22+
if (user) return <Navigate to="/home" replace />;
2223
return <>{children}</>;
2324
}
2425

@@ -27,13 +28,17 @@ export function AppRoutes() {
2728
<Routes>
2829
<Route path="/" element={<LandingPage />} />
2930
<Route
30-
path="/dashboard"
3131
element={
3232
<ProtectedRoute>
33-
<DashboardPage />
33+
<AuthenticatedLayout />
3434
</ProtectedRoute>
3535
}
36-
/>
36+
>
37+
<Route path="/home" element={<JobsPage />} />
38+
<Route path="/dashboard" element={<NotFound />} />
39+
<Route path="/vagas" element={<JobsPage />} />
40+
<Route path="/mentoria" element={<NotFound />} />
41+
</Route>
3742
<Route
3843
path="/login"
3944
element={
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { useAuth } from "@/domains/auth/application/AuthContext";
2+
import { useTheme } from "@/shared/hooks/useTheme";
3+
import { ThemeToggle } from "@/shared/ui/theme-toggle";
4+
import { Bell, Mail } from "lucide-react";
5+
import { useLocation } from "react-router-dom";
6+
7+
const routeTitles: Array<{ path: string; title: string; exact?: boolean }> = [
8+
{ path: "/home", title: "Início", exact: true },
9+
{ path: "/vagas", title: "Vagas" },
10+
{ path: "/mentoria", title: "Mentoria" },
11+
];
12+
13+
function getHeaderTitle(pathname: string) {
14+
const matchedRoute = routeTitles.find((route) => {
15+
if (pathname === route.path) return true;
16+
if (route.exact) return false;
17+
return pathname.startsWith(`${route.path}/`);
18+
});
19+
20+
return matchedRoute?.title ?? "Início";
21+
}
22+
23+
function getInitials(value: string) {
24+
const normalized = value.trim();
25+
if (!normalized) return "U";
26+
27+
const [first = "", second = ""] = normalized
28+
.replace(/@.*/, "")
29+
.split(/\s|[._-]/)
30+
.filter(Boolean);
31+
32+
return `${first[0] ?? ""}${second[0] ?? first[1] ?? ""}`.toUpperCase();
33+
}
34+
35+
export function AuthenticatedHeader() {
36+
const { user } = useAuth();
37+
const { resolvedTheme, toggleTheme } = useTheme();
38+
const location = useLocation();
39+
40+
const displayName =
41+
user?.name || user?.displayName || user?.email || "Usuário";
42+
const title = getHeaderTitle(location.pathname);
43+
const initials = getInitials(displayName);
44+
45+
return (
46+
<header className="flex h-[103px] shrink-0 items-center justify-between border-b border-border bg-card px-6 text-card-foreground shadow-[0_8px_18px_rgba(0,0,0,0.04)] dark:shadow-[0_8px_24px_rgba(0,0,0,0.22)] md:px-[43px]">
47+
<h1 className="truncate text-[32px] font-bold leading-[1.5] text-foreground">
48+
{title}
49+
</h1>
50+
51+
<div className="flex shrink-0 items-center gap-[17px]">
52+
<ThemeToggle theme={resolvedTheme} onToggle={toggleTheme} />
53+
<button
54+
type="button"
55+
className="flex size-6 items-center justify-center text-foreground transition-colors hover:text-primary"
56+
aria-label="Mensagens"
57+
>
58+
<Mail className="size-4" strokeWidth={2.4} />
59+
</button>
60+
<button
61+
type="button"
62+
className="flex size-6 items-center justify-center text-foreground transition-colors hover:text-primary"
63+
aria-label="Notificações"
64+
>
65+
<Bell className="size-4" strokeWidth={2.4} />
66+
</button>
67+
<div
68+
className="flex size-8 items-center justify-center rounded-full bg-[#8fb3a0] text-base font-semibold leading-6 text-primary dark:bg-primary/30 dark:text-primary-foreground"
69+
aria-label={displayName}
70+
title={displayName}
71+
>
72+
{initials}
73+
</div>
74+
</div>
75+
</header>
76+
);
77+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { AuthenticatedHeader } from "@/app/AuthenticatedHeader";
2+
import { AuthenticatedSidebar } from "@/app/AuthenticatedSidebar";
3+
import { Outlet } from "react-router-dom";
4+
5+
export function AuthenticatedLayout() {
6+
return (
7+
<div className="flex h-screen overflow-hidden bg-background text-foreground">
8+
<AuthenticatedSidebar />
9+
10+
<div className="flex min-w-0 flex-1 flex-col">
11+
<AuthenticatedHeader />
12+
13+
<main className="min-h-0 flex-1 overflow-y-auto overflow-x-hidden bg-background">
14+
<Outlet />
15+
</main>
16+
</div>
17+
</div>
18+
);
19+
}
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import { useAuth } from "@/domains/auth/application/AuthContext";
2+
import { cn } from "@/shared/lib/utils";
3+
import {
4+
BriefcaseBusiness,
5+
ChartNoAxesColumn,
6+
Headphones,
7+
Home,
8+
LogOut,
9+
UserRound,
10+
UsersRound,
11+
} from "lucide-react";
12+
import type { LucideIcon } from "lucide-react";
13+
import { Link, useLocation, useNavigate } from "react-router-dom";
14+
15+
interface NavigationItem {
16+
label: string;
17+
to: string;
18+
icon: LucideIcon;
19+
activePaths: string[];
20+
exact?: boolean;
21+
showChevron?: boolean;
22+
}
23+
24+
const navigationItems: NavigationItem[] = [
25+
{
26+
label: "Início",
27+
to: "/home",
28+
icon: Home,
29+
activePaths: ["/home"],
30+
exact: true,
31+
},
32+
{
33+
label: "Dashboard",
34+
to: "/dashboard",
35+
icon: ChartNoAxesColumn,
36+
activePaths: ["/dashboard"],
37+
},
38+
{
39+
label: "Vagas",
40+
to: "/vagas",
41+
icon: BriefcaseBusiness,
42+
activePaths: ["/vagas"],
43+
},
44+
{
45+
label: "Mentoria",
46+
to: "/mentoria",
47+
icon: UsersRound,
48+
activePaths: ["/mentoria"],
49+
},
50+
];
51+
52+
const sidebarItemClasses =
53+
"flex h-10 items-center gap-3 rounded-[5px] border text-sm tracking-[0.1px] transition-colors";
54+
55+
const inactiveSidebarItemClasses =
56+
"border-border/70 bg-card text-card-foreground hover:border-primary/35 hover:bg-muted dark:border-border/60 dark:bg-muted/15 dark:hover:bg-muted/35";
57+
58+
const activeSidebarItemClasses =
59+
"border-primary bg-primary font-bold text-primary-foreground shadow-sm shadow-primary/20";
60+
61+
function isRouteActive(pathname: string, item: NavigationItem) {
62+
return item.activePaths.some((activePath) => {
63+
if (pathname === activePath) return true;
64+
if (item.exact) return false;
65+
return pathname.startsWith(`${activePath}/`);
66+
});
67+
}
68+
69+
function BrandLogo() {
70+
return (
71+
<p className="select-none whitespace-nowrap text-[32px] font-semibold leading-none text-foreground">
72+
<span className="text-[#8ecaff]">&lt;</span>
73+
<span>Cand</span>
74+
<span className="text-[#fea50b]">!</span>
75+
<span>Date</span>
76+
<span className="text-[#955dfc]">!</span>
77+
<span className="text-[#8ecaff]">&gt;</span>
78+
</p>
79+
);
80+
}
81+
82+
export function AuthenticatedSidebar() {
83+
const { logout } = useAuth();
84+
const navigate = useNavigate();
85+
const location = useLocation();
86+
87+
const handleLogout = async () => {
88+
await logout();
89+
navigate("/login", { replace: true });
90+
};
91+
92+
return (
93+
<aside
94+
className="flex h-screen w-[249px] shrink-0 flex-col border-r border-border bg-card text-card-foreground shadow-[0_0_20px_rgba(0,0,0,0.04)] dark:shadow-[0_0_24px_rgba(0,0,0,0.22)]"
95+
aria-label="Menu lateral"
96+
>
97+
<div className="flex h-[103px] shrink-0 items-center px-[10px] pt-1">
98+
<BrandLogo />
99+
</div>
100+
101+
<nav
102+
className="flex flex-1 flex-col gap-[22px] border-t border-dashed border-border/70 px-[13px] pt-[28px]"
103+
aria-label="Navegação principal"
104+
>
105+
{navigationItems.map((item) => {
106+
const Icon = item.icon;
107+
const isActive = isRouteActive(location.pathname, item);
108+
109+
return (
110+
<Link
111+
key={item.label}
112+
to={item.to}
113+
aria-current={isActive ? "page" : undefined}
114+
className={cn(
115+
sidebarItemClasses,
116+
"px-[13px]",
117+
isActive
118+
? activeSidebarItemClasses
119+
: inactiveSidebarItemClasses,
120+
)}
121+
>
122+
<Icon
123+
className="h-6 w-6 shrink-0"
124+
strokeWidth={isActive ? 2.5 : 2.25}
125+
/>
126+
<span className="min-w-0 flex-1 truncate">{item.label}</span>
127+
</Link>
128+
);
129+
})}
130+
</nav>
131+
132+
<div className="flex shrink-0 flex-col gap-[22px] border-t border-dashed border-border/70 px-[13px] pb-[29px] pt-[28px]">
133+
<button
134+
type="button"
135+
className={cn(
136+
sidebarItemClasses,
137+
inactiveSidebarItemClasses,
138+
"px-[8px] font-normal",
139+
)}
140+
>
141+
<UserRound className="h-6 w-6 shrink-0" strokeWidth={2.25} />
142+
<span>Perfil</span>
143+
</button>
144+
<button
145+
type="button"
146+
className={cn(
147+
sidebarItemClasses,
148+
inactiveSidebarItemClasses,
149+
"px-[8px] font-normal",
150+
)}
151+
>
152+
<Headphones className="h-6 w-6 shrink-0" strokeWidth={2.25} />
153+
<span>Ajuda</span>
154+
</button>
155+
<button
156+
type="button"
157+
className={cn(
158+
sidebarItemClasses,
159+
inactiveSidebarItemClasses,
160+
"px-[8px] font-normal",
161+
)}
162+
onClick={handleLogout}
163+
>
164+
<LogOut className="h-6 w-6 shrink-0" strokeWidth={2.25} />
165+
<span>Sair</span>
166+
</button>
167+
</div>
168+
</aside>
169+
);
170+
}

frontend/src/domains/auth/domain/auth.types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export interface User {
22
id: string;
33
email: string;
44
name?: string;
5+
displayName?: string | null;
56
}
67

78
export interface LoginCredentials {

frontend/src/domains/auth/presentation/components/LoginFormPanel.tsx

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
/* eslint-disable @typescript-eslint/no-explicit-any */
22
import {
3-
login,
43
getGoogleAuthUrl,
54
getGithubAuthUrl,
65
getLinkedinAuthUrl,
76
} from "@/domains/auth/infrastructure/authApi";
7+
import { useAuth } from "@/domains/auth/application/AuthContext";
88
import { Image } from "@unpic/react";
99
import { motion } from "framer-motion";
1010
import { ArrowLeft, Eye, EyeOff } from "lucide-react";
@@ -55,6 +55,7 @@ function StarsBackground() {
5555

5656
export default function RightSide() {
5757
const navigate = useNavigate();
58+
const { login } = useAuth();
5859
const [showPassword, setShowPassword] = useState(false);
5960
const [email, setEmail] = useState("");
6061
const [password, setPassword] = useState("");
@@ -132,13 +133,8 @@ export default function RightSide() {
132133
if (isValid) {
133134
setIsLoading(true);
134135
try {
135-
const result = await login({ email, password });
136-
137-
if (result.token) {
138-
localStorage.setItem("token", result.token);
139-
}
140-
141-
navigate("/dashboard", { replace: true });
136+
await login({ email, password });
137+
navigate("/home", { replace: true });
142138
} catch (error: any) {
143139
setApiError(
144140
error.message || "Erro ao fazer login. Verifique suas credenciais.",

frontend/src/domains/auth/presentation/pages/AuthCallbackPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export default function AuthCallback() {
1919
if (isLoading || !hasRefreshed.current) return;
2020

2121
if (user) {
22-
navigate("/app", { replace: true });
22+
navigate("/home", { replace: true });
2323
} else {
2424
navigate("/login?error=oauth_failed", { replace: true });
2525
}

0 commit comments

Comments
 (0)