Skip to content

Commit 2ae1f63

Browse files
authored
feat: profile page (#211)
* mobile page layout * update styling * page layout completed * remove typo * updated styling * profile page styling updated * text subtle * update styling * format * swagger * add ts resolve alias + remove unsused code * lint
1 parent bacf1fe commit 2ae1f63

File tree

6 files changed

+173
-21
lines changed

6 files changed

+173
-21
lines changed

backend/docs/swagger.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ definitions:
6666
type: integer
6767
type: array
6868
limit:
69+
minimum: 0
6970
type: integer
7071
type: object
7172
GenerateRequestInput:

clients/mobile/components/tasks/active-filter-chips.tsx

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
import Feather from "@expo/vector-icons/Feather";
22
import { Pressable, Text, View } from "react-native";
33

4-
interface FilterItem {
5-
label: string;
6-
value: string;
7-
}
8-
94
interface ActiveFilterChipsProps {
105
filters: { label: string; value: string }[];
116
onRemoveFilter: (value: string) => void;

clients/mobile/eslint.config.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,13 @@ module.exports = defineConfig([
77
{
88
ignores: ["dist/*"],
99
},
10+
{
11+
settings: {
12+
"import/resolver": {
13+
typescript: {
14+
project: "./tsconfig.json",
15+
},
16+
},
17+
},
18+
},
1019
]);

clients/web/src/components/Sidebar.tsx

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ function NavLink({
1919
to={to}
2020
className={`flex items-center gap-3 rounded-lg px-3 py-2 text-sm transition-colors ${
2121
isActive
22-
? "bg-primary/10 font-semibold text-gray-900"
22+
? "bg-bg-selected font-semibold text-subtle"
2323
: "text-gray-500 hover:bg-gray-100 hover:text-gray-700"
2424
}`}
2525
>
@@ -29,6 +29,33 @@ function NavLink({
2929
);
3030
}
3131

32+
function ProfileLink({ displayName }: { displayName: string | undefined }) {
33+
const pathname = useRouterState({ select: (s) => s.location.pathname });
34+
const isActive = pathname === "/profile";
35+
return (
36+
<Link
37+
to="/profile"
38+
className={`flex items-center gap-3 rounded-lg p-3 transition-colors ${
39+
isActive ? "bg-bg-selected" : "hover:bg-bg-selected"
40+
}`}
41+
>
42+
<UserButton
43+
appearance={{
44+
elements: {
45+
avatarBox: "size-10",
46+
},
47+
}}
48+
/>
49+
<div className="min-w-0 flex-1">
50+
<p className="truncate text-sm font-semibold text-text-subtle">
51+
{displayName || "User"}
52+
</p>
53+
<p className="truncate text-xs text-primary">Hotel Chain</p>
54+
</div>
55+
</Link>
56+
);
57+
}
58+
3259
export function Sidebar() {
3360
const { user } = useUser();
3461

@@ -69,21 +96,7 @@ export function Sidebar() {
6996
Settings
7097
</NavLink>
7198
<LogoutButton />
72-
<div className="flex items-center gap-3 pt-4">
73-
<UserButton
74-
appearance={{
75-
elements: {
76-
avatarBox: "size-10",
77-
},
78-
}}
79-
/>
80-
<div className="min-w-0 flex-1">
81-
<p className="truncate text-sm font-medium text-gray-700">
82-
{displayName || "User"}
83-
</p>
84-
<p className="truncate text-xs text-gray-500">Hotel Chain</p>
85-
</div>
86-
</div>
99+
<ProfileLink displayName={displayName} />
87100
</div>
88101
</aside>
89102
);

clients/web/src/routeTree.gen.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { Route as IndexRouteImport } from './routes/index'
1616
import { Route as ProtectedTestApiRouteImport } from './routes/_protected/test-api'
1717
import { Route as ProtectedSettingsRouteImport } from './routes/_protected/settings'
1818
import { Route as ProtectedRoomsRouteImport } from './routes/_protected/rooms'
19+
import { Route as ProtectedProfileRouteImport } from './routes/_protected/profile'
1920
import { Route as ProtectedHomeRouteImport } from './routes/_protected/home'
2021
import { Route as ProtectedRoomsIndexRouteImport } from './routes/_protected/rooms.index'
2122
import { Route as ProtectedGuestsIndexRouteImport } from './routes/_protected/guests.index'
@@ -55,6 +56,11 @@ const ProtectedRoomsRoute = ProtectedRoomsRouteImport.update({
5556
path: '/rooms',
5657
getParentRoute: () => ProtectedRoute,
5758
} as any)
59+
const ProtectedProfileRoute = ProtectedProfileRouteImport.update({
60+
id: '/profile',
61+
path: '/profile',
62+
getParentRoute: () => ProtectedRoute,
63+
} as any)
5864
const ProtectedHomeRoute = ProtectedHomeRouteImport.update({
5965
id: '/home',
6066
path: '/home',
@@ -81,6 +87,7 @@ export interface FileRoutesByFullPath {
8187
'/sign-in': typeof SignInRoute
8288
'/sign-up': typeof SignUpRoute
8389
'/home': typeof ProtectedHomeRoute
90+
'/profile': typeof ProtectedProfileRoute
8491
'/rooms': typeof ProtectedRoomsRouteWithChildren
8592
'/settings': typeof ProtectedSettingsRoute
8693
'/test-api': typeof ProtectedTestApiRoute
@@ -93,6 +100,7 @@ export interface FileRoutesByTo {
93100
'/sign-in': typeof SignInRoute
94101
'/sign-up': typeof SignUpRoute
95102
'/home': typeof ProtectedHomeRoute
103+
'/profile': typeof ProtectedProfileRoute
96104
'/settings': typeof ProtectedSettingsRoute
97105
'/test-api': typeof ProtectedTestApiRoute
98106
'/guests/$guestId': typeof ProtectedGuestsGuestIdRoute
@@ -106,6 +114,7 @@ export interface FileRoutesById {
106114
'/sign-in': typeof SignInRoute
107115
'/sign-up': typeof SignUpRoute
108116
'/_protected/home': typeof ProtectedHomeRoute
117+
'/_protected/profile': typeof ProtectedProfileRoute
109118
'/_protected/rooms': typeof ProtectedRoomsRouteWithChildren
110119
'/_protected/settings': typeof ProtectedSettingsRoute
111120
'/_protected/test-api': typeof ProtectedTestApiRoute
@@ -120,6 +129,7 @@ export interface FileRouteTypes {
120129
| '/sign-in'
121130
| '/sign-up'
122131
| '/home'
132+
| '/profile'
123133
| '/rooms'
124134
| '/settings'
125135
| '/test-api'
@@ -132,6 +142,7 @@ export interface FileRouteTypes {
132142
| '/sign-in'
133143
| '/sign-up'
134144
| '/home'
145+
| '/profile'
135146
| '/settings'
136147
| '/test-api'
137148
| '/guests/$guestId'
@@ -144,6 +155,7 @@ export interface FileRouteTypes {
144155
| '/sign-in'
145156
| '/sign-up'
146157
| '/_protected/home'
158+
| '/_protected/profile'
147159
| '/_protected/rooms'
148160
| '/_protected/settings'
149161
| '/_protected/test-api'
@@ -210,6 +222,13 @@ declare module '@tanstack/react-router' {
210222
preLoaderRoute: typeof ProtectedRoomsRouteImport
211223
parentRoute: typeof ProtectedRoute
212224
}
225+
'/_protected/profile': {
226+
id: '/_protected/profile'
227+
path: '/profile'
228+
fullPath: '/profile'
229+
preLoaderRoute: typeof ProtectedProfileRouteImport
230+
parentRoute: typeof ProtectedRoute
231+
}
213232
'/_protected/home': {
214233
id: '/_protected/home'
215234
path: '/home'
@@ -255,6 +274,7 @@ const ProtectedRoomsRouteWithChildren = ProtectedRoomsRoute._addFileChildren(
255274

256275
interface ProtectedRouteChildren {
257276
ProtectedHomeRoute: typeof ProtectedHomeRoute
277+
ProtectedProfileRoute: typeof ProtectedProfileRoute
258278
ProtectedRoomsRoute: typeof ProtectedRoomsRouteWithChildren
259279
ProtectedSettingsRoute: typeof ProtectedSettingsRoute
260280
ProtectedTestApiRoute: typeof ProtectedTestApiRoute
@@ -264,6 +284,7 @@ interface ProtectedRouteChildren {
264284

265285
const ProtectedRouteChildren: ProtectedRouteChildren = {
266286
ProtectedHomeRoute: ProtectedHomeRoute,
287+
ProtectedProfileRoute: ProtectedProfileRoute,
267288
ProtectedRoomsRoute: ProtectedRoomsRouteWithChildren,
268289
ProtectedSettingsRoute: ProtectedSettingsRoute,
269290
ProtectedTestApiRoute: ProtectedTestApiRoute,
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { createFileRoute } from "@tanstack/react-router";
2+
import { useUser } from "@clerk/clerk-react";
3+
4+
export const Route = createFileRoute("/_protected/profile")({
5+
component: ProfilePage,
6+
});
7+
8+
function DetailRow({ label, value }: { label: string; value: string }) {
9+
return (
10+
<div className="grid grid-cols-[40%_1fr] items-center py-3">
11+
<p className="text-sm font-medium text-text-default">{label}</p>
12+
<p className="text-sm text-text-subtle">{value}</p>
13+
</div>
14+
);
15+
}
16+
17+
function ProfileInfoCard({
18+
displayName,
19+
email,
20+
phone,
21+
}: {
22+
displayName: string;
23+
email: string;
24+
phone: string;
25+
}) {
26+
return (
27+
<section className="h-56 rounded-lg border border-stroke-subtle bg-white p-6">
28+
<div className="divide-y divide-stroke-subtle">
29+
<DetailRow label="Government Name" value={displayName} />
30+
<DetailRow label="Date of Birth" value="—" />
31+
<DetailRow label="Phone" value={phone || "—"} />
32+
<DetailRow label="Email" value={email || "—"} />
33+
</div>
34+
</section>
35+
);
36+
}
37+
38+
function NotesFromManagerCard() {
39+
return (
40+
<section className="h-56 rounded-lg border border-stroke-subtle bg-white p-6">
41+
<h2 className="text-sm font-medium text-text-default">
42+
Notes from Manager
43+
</h2>
44+
</section>
45+
);
46+
}
47+
48+
function ProfilePage() {
49+
const { user } = useUser();
50+
51+
const displayName =
52+
user?.fullName ||
53+
[user?.firstName, user?.lastName].filter(Boolean).join(" ") ||
54+
"User";
55+
56+
const email = user?.primaryEmailAddress?.emailAddress ?? "";
57+
const phone = user?.primaryPhoneNumber?.phoneNumber ?? "";
58+
59+
return (
60+
<main className="flex h-screen flex-col overflow-hidden">
61+
{/* Page header */}
62+
<header className="flex-row gap-1.5 border-b border-stroke-subtle px-10 py-5">
63+
<h1 className="text-2xl font-semibold text-text-default">Profile</h1>
64+
<p className="text-sm font-medium text-text-subtle">
65+
Overview of profile
66+
</p>
67+
</header>
68+
69+
{/* Scrollable content */}
70+
<div className="flex flex-1 flex-col overflow-y-auto px-6 py-6">
71+
{/* User hero */}
72+
<div className="flex items-center gap-11 mb-6 pl-4">
73+
{user?.imageUrl ? (
74+
<img
75+
src={user.imageUrl}
76+
alt={displayName}
77+
className="size-30 rounded-full object-cover"
78+
/>
79+
) : (
80+
<div className="flex size-30 items-center justify-center rounded-full border-2 border-text-default bg-gray-100">
81+
<span className="text-[40px] font-semibold text-text-default">
82+
{displayName.charAt(0)}
83+
</span>
84+
</div>
85+
)}
86+
<div>
87+
<h2 className="text-[40px] font-bold text-text-default leading-none">
88+
{user?.firstName}
89+
</h2>
90+
<h2 className="text-[40px] font-bold text-text-default leading-none">
91+
{user?.lastName}
92+
</h2>
93+
<p className="pt-1 text-base font-bold text-primary">Hotel Chain</p>
94+
</div>
95+
</div>
96+
97+
{/* Info + Notes row */}
98+
<div className="flex flex-1 gap-4">
99+
<div className="flex-1">
100+
<ProfileInfoCard
101+
displayName={displayName}
102+
email={email}
103+
phone={phone}
104+
/>
105+
</div>
106+
<div className="flex-1">
107+
<NotesFromManagerCard />
108+
</div>
109+
</div>
110+
</div>
111+
</main>
112+
);
113+
}

0 commit comments

Comments
 (0)