feat(member): implement member profile details page and routing
All checks were successful
Deploy Flowstate to VPS / deploy (push) Successful in 47s
All checks were successful
Deploy Flowstate to VPS / deploy (push) Successful in 47s
This commit is contained in:
parent
86d1c8859d
commit
a457ad19da
5 changed files with 172 additions and 16 deletions
|
|
@ -5,7 +5,9 @@
|
|||
|
||||
"use client";
|
||||
|
||||
import { GoBackButton } from "@/app/components/ui/buttons/GoBackButton";
|
||||
import { UsersRound } from "lucide-react";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
/**
|
||||
* Renders the members page header featuring title details, workspace subtitle, and an indicator icon.
|
||||
|
|
@ -13,18 +15,27 @@ import { UsersRound } from "lucide-react";
|
|||
* @returns {JSX.Element} The rendered members header component.
|
||||
*/
|
||||
export default function MemberHeader() {
|
||||
const pathname = usePathname();
|
||||
const isRootPage = pathname === "/member";
|
||||
|
||||
return (
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 text-primary font-medium text-xs uppercase tracking-wider mb-1">
|
||||
<UsersRound size={14} />
|
||||
Team
|
||||
<>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 text-primary font-medium text-xs uppercase tracking-wider mb-1">
|
||||
<UsersRound size={14} />
|
||||
Team
|
||||
</div>
|
||||
<h1 className="text-3xl font-extrabold tracking-tight">Member</h1>
|
||||
<p className="text-sm text-foreground-muted mt-1">
|
||||
Manage your team members and collaborate efficiently.
|
||||
</p>
|
||||
</div>
|
||||
<h1 className="text-3xl font-extrabold tracking-tight">Member</h1>
|
||||
<p className="text-sm text-foreground-muted mt-1">
|
||||
Manage your team members and collaborate efficiently.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isRootPage && (
|
||||
<GoBackButton fallbackUrl="/member" label="Member List" />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
96
app/(app)/member/[id]/MemberProfileCard.tsx
Normal file
96
app/(app)/member/[id]/MemberProfileCard.tsx
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
/**
|
||||
* @file MemberProfileCard.tsx
|
||||
* @description Client component rendering detailed profile information and activity metrics for a specific team member.
|
||||
*/
|
||||
|
||||
"use client";
|
||||
|
||||
import { getFullName, getInitials, formatTimeAgo } from "@/utils/user";
|
||||
import { Mail, Clock, ShieldCheck } from "lucide-react";
|
||||
import { UserListItem } from "@/types/user";
|
||||
|
||||
/**
|
||||
* Properties for the MemberProfileCard component.
|
||||
*
|
||||
* @interface MemberProfileCardProps
|
||||
* @property {UserListItem} member - The user item containing detailed profile data, activity status, and credentials.
|
||||
*/
|
||||
interface MemberProfileCardProps {
|
||||
member: UserListItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a detailed profile card for a team member, including user avatar initials, online status indicators,
|
||||
* contact information, and activity metrics.
|
||||
*
|
||||
* @param {MemberProfileCardProps} props - The component props.
|
||||
* @returns {JSX.Element} The rendered member profile card component.
|
||||
*/
|
||||
export default function MemberProfileCard({ member }: MemberProfileCardProps) {
|
||||
const stats = [
|
||||
{
|
||||
label: "Last Activity",
|
||||
value: formatTimeAgo(member.lastLogin),
|
||||
icon: Clock,
|
||||
color: "text-blue-400 bg-blue-500/10 border-blue-500/20",
|
||||
},
|
||||
{
|
||||
label: "Account Status",
|
||||
value: "Active - Member",
|
||||
icon: ShieldCheck,
|
||||
color: "text-emerald-400 bg-emerald-500/10 border-emerald-500/20",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="relative bg-card/40 border border-border/80 rounded-3xl p-4 sm:p-6 shadow-sm overflow-hidden">
|
||||
{/* Accent Bar */}
|
||||
<div
|
||||
className={`absolute left-0 top-0 bottom-0 w-1.5 transition-colors ${member.isOnline ? "bg-emerald-500" : "bg-slate-600"}`}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6 pl-2">
|
||||
<div className="flex items-center gap-5">
|
||||
<div
|
||||
className={`w-16 h-16 shrink-0 rounded-2xl flex items-center justify-center text-xl font-black tracking-wider text-white shadow-xl border-2 border-card ring-2 ring-border/50 ${member.color}`}
|
||||
style={{ textShadow: "0 1px 2px rgba(0, 0, 0, 0.8)" }}
|
||||
>
|
||||
{getInitials(member.firstName, member.lastName)}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-2xl font-bold tracking-tight text-foreground">
|
||||
{getFullName(member.firstName, member.lastName)}
|
||||
</h1>
|
||||
<div className="flex items-center gap-2 text-foreground-muted font-mono text-xs sm:text-sm">
|
||||
<Mail size={14} className="shrink-0 text-foreground-muted/70" />
|
||||
<span className="truncate">{member.email}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-4 pt-4 border-t border-border/60 pl-2">
|
||||
{stats.map((stat) => (
|
||||
<div
|
||||
key={stat.label}
|
||||
className="flex items-center gap-4 p-4 rounded-2xl bg-card/30 border border-border/60 hover:border-primary/40 transition-colors"
|
||||
>
|
||||
<div
|
||||
className={`w-12 h-12 shrink-0 rounded-xl border flex items-center justify-center ${stat.color}`}
|
||||
>
|
||||
<stat.icon size={22} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5 min-w-0">
|
||||
<p className="text-[11px] uppercase tracking-wider text-foreground-muted font-mono font-semibold">
|
||||
{stat.label}
|
||||
</p>
|
||||
<p className="text-sm font-semibold text-foreground truncate">
|
||||
{stat.value}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
43
app/(app)/member/[id]/page.tsx
Normal file
43
app/(app)/member/[id]/page.tsx
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
/**
|
||||
* @file member/[id]/page.tsx
|
||||
* @description Server component rendering the detailed profile page for a specific member by fetching their user profile data.
|
||||
*/
|
||||
|
||||
import { UserService } from "@/services/user.service";
|
||||
import { notFound } from "next/navigation";
|
||||
import MemberHeader from "../MemberHeader";
|
||||
import MemberProfileCard from "./MemberProfileCard";
|
||||
|
||||
/**
|
||||
* Properties for the MemberDetailPage component.
|
||||
*
|
||||
* @interface MemberDetailPageProps
|
||||
* @property {Promise<{ id: string }>} params - Promise containing route context parameters with the target member ID.
|
||||
*/
|
||||
interface MemberDetailPageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the member detail page by resolving the route parameter, fetching the user profile by ID,
|
||||
* and displaying their header and profile card or triggering a 404 page if the member is not found.
|
||||
*
|
||||
* @async
|
||||
* @param {MemberDetailPageProps} props - The component props.
|
||||
* @returns {Promise<JSX.Element>} The rendered member detail page component.
|
||||
*/
|
||||
export default async function MemberDetailPage({
|
||||
params,
|
||||
}: MemberDetailPageProps) {
|
||||
const { id } = await params;
|
||||
const user = await UserService.findProfileById(id);
|
||||
|
||||
if (!user) notFound();
|
||||
|
||||
return (
|
||||
<div className="space-y-6 max-w-4xl mx-auto pb-12">
|
||||
<MemberHeader />
|
||||
<MemberProfileCard member={user} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ import {
|
|||
} from "@/utils/user";
|
||||
import { Mail } from "lucide-react";
|
||||
import { UserListItem } from "@/types/user";
|
||||
import Link from "next/link";
|
||||
|
||||
/**
|
||||
* Properties for the MemberItem component.
|
||||
|
|
@ -32,7 +33,10 @@ interface MemberItemProps {
|
|||
*/
|
||||
export default function MemberItem({ member }: MemberItemProps) {
|
||||
return (
|
||||
<div className="group relative flex flex-col sm:flex-row sm:items-center justify-between gap-4 p-4 bg-card/40 border border-border/80 rounded-2xl hover:border-primary/40 hover:bg-card/70 transition-all duration-200 shadow-sm overflow-hidden">
|
||||
<Link
|
||||
href={`/member/${member.id}`}
|
||||
className="group relative flex flex-col sm:flex-row sm:items-center justify-between gap-4 p-4 bg-card/40 border border-border/80 rounded-2xl hover:border-primary/40 hover:bg-card/70 transition-all duration-200 shadow-sm overflow-hidden"
|
||||
>
|
||||
{/* Dynamic colored accent stripe */}
|
||||
<div
|
||||
className={`absolute left-0 top-0 bottom-0 w-1 transition-colors ${
|
||||
|
|
@ -90,6 +94,6 @@ export default function MemberItem({ member }: MemberItemProps) {
|
|||
{formatTimeAgo(member.lastLogin)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,24 +5,26 @@
|
|||
|
||||
import { db } from "@/db";
|
||||
import { usersTable } from "@/db/schema";
|
||||
import { eq, desc, sql } from "drizzle-orm";
|
||||
import { eq, sql } from "drizzle-orm";
|
||||
|
||||
/**
|
||||
* Service class for handling user operations and database interactions.
|
||||
*/
|
||||
export class UserService {
|
||||
/**
|
||||
* Retrieves specific profile information (firstName, lastName, color, online status, last login) for a user by ID.
|
||||
* Retrieves specific profile information for a user by ID.
|
||||
*
|
||||
* @async
|
||||
* @param {string} userId - The unique identifier of the user.
|
||||
* @returns {Promise<{ firstName: string; lastName: string; color: string; isOnline: boolean; lastLogin: Date } | null>} The user profile data or null if not found.
|
||||
* @returns {Promise<{ id: string; firstName: string; lastName: string; email: string; color: string; isOnline: boolean; lastLogin: Date } | null>} The user profile data or null if not found.
|
||||
*/
|
||||
static async findProfileById(userId: string) {
|
||||
const [user] = await db
|
||||
.select({
|
||||
id: usersTable.id,
|
||||
firstName: usersTable.firstName,
|
||||
lastName: usersTable.lastName,
|
||||
email: usersTable.email,
|
||||
color: usersTable.color,
|
||||
isOnline: usersTable.isOnline,
|
||||
lastLogin: usersTable.lastLogin,
|
||||
|
|
@ -34,7 +36,7 @@ export class UserService {
|
|||
}
|
||||
|
||||
/**
|
||||
* Retrieves a list of all users sorted by their last login date in descending order, putting null values last.
|
||||
* Retrieves a list of all users sorted by their last login date in descending order, placing null values last.
|
||||
*
|
||||
* @async
|
||||
* @returns {Promise<Array<{ id: string; firstName: string; lastName: string; email: string; color: string; lastLogin: Date; isOnline: boolean }>>} An array of user list items.
|
||||
|
|
|
|||
Loading…
Reference in a new issue