feat: add profile page UI & actions

This commit is contained in:
Mahen 2026-02-07 09:24:56 +07:00
parent a03c89f598
commit d2bcba7eb1
8 changed files with 145 additions and 10 deletions

View File

@ -1,7 +1,9 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
images: {
domains: ["lh3.googleusercontent.com"],
},
};
export default nextConfig;

2
package-lock.json generated
View File

@ -18,7 +18,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"dotenv": "^17.2.3",
"framer-motion": "^12.31.0",
"framer-motion": "^12.33.0",
"lucide-react": "^0.563.0",
"next": "16.1.6",
"next-auth": "^4.24.13",

View File

@ -20,7 +20,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"dotenv": "^17.2.3",
"framer-motion": "^12.31.0",
"framer-motion": "^12.33.0",
"lucide-react": "^0.563.0",
"next": "16.1.6",
"next-auth": "^4.24.13",

View File

@ -0,0 +1,24 @@
import { getServerSession } from "next-auth/next";
import { authOptions } from "../../api/auth/[...nextauth]/route";
import prisma from "@/lib/prisma";
export const getAnotherUserData = async () => {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.email) return null;
const user = await prisma.user.findUnique({
where: { email: session.user.email },
select: {
gender: true,
productReference: true,
},
});
return user;
} catch (error) {
console.error("Error fetching user data:", error);
return null;
}
};

18
src/app/profile/page.tsx Normal file
View File

@ -0,0 +1,18 @@
import { Header } from "@/src/components/dashboards/Header";
import { getAnotherUserData } from "./lib/action";
import ProfileClient from "@/src/components/dashboards/ProfileClient";
import { UserGender } from "@prisma/client";
export default async function ProfilePage() {
const user = await getAnotherUserData();
return (
<>
<Header />
<ProfileClient
gender={user?.gender as UserGender}
productReference={user?.productReference || "None"}
/>
</>
);
}

View File

@ -79,7 +79,6 @@ export default function DashboardClient() {
<Header />
<main className="container mx-auto px-4 py-8">
{/* Hero Section */}
<div
className="mb-8 rounded-2xl p-8 text-center"
style={{ background: "hsl(var(--primary))" }}

View File

@ -19,6 +19,7 @@ import {
DropdownMenuTrigger,
} from "../ui/dropdown-menu";
import { signOut, useSession } from "next-auth/react";
import Link from "next/link";
export function Header() {
const [isRefreshing, setIsRefreshing] = useState(false);
@ -34,7 +35,7 @@ export function Header() {
<header className="border-b bg-card/50 backdrop-blur-sm sticky top-0 z-50">
<div className="container mx-auto px-4 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Link href="/" className="flex items-center gap-3 cursor-pointer">
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-primary text-primary-foreground">
<BarChart3 className="h-5 w-5" />
</div>
@ -46,7 +47,7 @@ export function Header() {
Analisis Sentimen Ulasan Laptop Tokopedia
</p>
</div>
</div>
</Link>
<div className="flex items-center gap-6">
<div className="hidden items-center gap-6 text-sm md:flex">
@ -79,10 +80,12 @@ export function Header() {
align="end"
className="w-max bg-card border-border shadow-md"
>
<DropdownMenuItem className="cursor-pointer gap-2 focus:bg-secondary focus:text-primary transition-colors hover:text-primary">
<UserCircle className="h-4 w-4 text-muted-foreground" />
<span>Menu Profil</span>
</DropdownMenuItem>
<Link href="/profile">
<DropdownMenuItem className="cursor-pointer gap-2 focus:bg-secondary focus:text-primary transition-colors hover:text-primary">
<UserCircle className="h-4 w-4 text-muted-foreground" />
<span>Menu Profil</span>
</DropdownMenuItem>
</Link>
<DropdownMenuSeparator className="bg-border" />

View File

@ -0,0 +1,89 @@
"use client";
import Image from "next/image";
import { Button } from "../ui/button";
import { ArrowLeft, Pencil } from "lucide-react";
import { Separator } from "../ui/separator";
import { useSession } from "next-auth/react";
import { UserGender } from "@prisma/client";
import Link from "next/link";
import { motion } from "framer-motion";
interface ProfileClientProps {
gender?: UserGender;
productReference?: string;
}
export default function ProfileClient({
gender,
productReference,
}: ProfileClientProps) {
const session = useSession();
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.4, ease: "easeInOut" }}
className="container mx-auto px-4 py-8"
>
<Link
href="/"
className="flex items-center gap-2 text-md text-primary max-w-xl mx-auto"
>
<ArrowLeft className="w-4 h-4" />
<span>Back to Dashboard</span>
</Link>
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.4, ease: "easeOut" }}
className="mx-auto w-full max-w-xl rounded-xl border bg-background shadow-sm mt-4"
>
<div className="flex items-center justify-between gap-4 p-6">
<div className="flex items-center gap-4">
<Image
src={session?.data?.user?.image ?? "file.svg"}
alt="User Avatar"
width={80}
height={80}
className="h-14 w-14 rounded-full border object-cover"
/>
<div>
<h1 className="text-lg font-semibold leading-tight">
{session?.data?.user?.name || "Guest"}
</h1>
<p className="text-sm text-muted-foreground">
{session?.data?.user?.email || "Not logged in"}
</p>
</div>
</div>
<Button
variant="outline"
size="sm"
className="gap-2 bg-primary text-card border-none"
>
<Pencil className="h-4 w-4" />
Edit Profile
</Button>
</div>
<Separator />
<div className="grid grid-cols-1 gap-4 p-6 sm:grid-cols-2">
<div className="space-y-1">
<p className="text-sm text-muted-foreground">Gender</p>
<p className="font-medium">{gender || "Not specified"}</p>
</div>
<div className="space-y-1">
<p className="text-sm text-muted-foreground">Product Preference</p>
<p className="font-medium">{productReference || "None"}</p>
</div>
</div>
</motion.div>
</motion.div>
);
}