feat: add profile page UI & actions
This commit is contained in:
parent
a03c89f598
commit
d2bcba7eb1
|
|
@ -1,7 +1,9 @@
|
||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
/* config options here */
|
images: {
|
||||||
|
domains: ["lh3.googleusercontent.com"],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"framer-motion": "^12.31.0",
|
"framer-motion": "^12.33.0",
|
||||||
"lucide-react": "^0.563.0",
|
"lucide-react": "^0.563.0",
|
||||||
"next": "16.1.6",
|
"next": "16.1.6",
|
||||||
"next-auth": "^4.24.13",
|
"next-auth": "^4.24.13",
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"framer-motion": "^12.31.0",
|
"framer-motion": "^12.33.0",
|
||||||
"lucide-react": "^0.563.0",
|
"lucide-react": "^0.563.0",
|
||||||
"next": "16.1.6",
|
"next": "16.1.6",
|
||||||
"next-auth": "^4.24.13",
|
"next-auth": "^4.24.13",
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -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"}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -79,7 +79,6 @@ export default function DashboardClient() {
|
||||||
<Header />
|
<Header />
|
||||||
|
|
||||||
<main className="container mx-auto px-4 py-8">
|
<main className="container mx-auto px-4 py-8">
|
||||||
{/* Hero Section */}
|
|
||||||
<div
|
<div
|
||||||
className="mb-8 rounded-2xl p-8 text-center"
|
className="mb-8 rounded-2xl p-8 text-center"
|
||||||
style={{ background: "hsl(var(--primary))" }}
|
style={{ background: "hsl(var(--primary))" }}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import {
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "../ui/dropdown-menu";
|
} from "../ui/dropdown-menu";
|
||||||
import { signOut, useSession } from "next-auth/react";
|
import { signOut, useSession } from "next-auth/react";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
export function Header() {
|
export function Header() {
|
||||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
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">
|
<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="container mx-auto px-4 py-4">
|
||||||
<div className="flex items-center justify-between">
|
<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">
|
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-primary text-primary-foreground">
|
||||||
<BarChart3 className="h-5 w-5" />
|
<BarChart3 className="h-5 w-5" />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -46,7 +47,7 @@ export function Header() {
|
||||||
Analisis Sentimen Ulasan Laptop Tokopedia
|
Analisis Sentimen Ulasan Laptop Tokopedia
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Link>
|
||||||
|
|
||||||
<div className="flex items-center gap-6">
|
<div className="flex items-center gap-6">
|
||||||
<div className="hidden items-center gap-6 text-sm md:flex">
|
<div className="hidden items-center gap-6 text-sm md:flex">
|
||||||
|
|
@ -79,10 +80,12 @@ export function Header() {
|
||||||
align="end"
|
align="end"
|
||||||
className="w-max bg-card border-border shadow-md"
|
className="w-max bg-card border-border shadow-md"
|
||||||
>
|
>
|
||||||
|
<Link href="/profile">
|
||||||
<DropdownMenuItem className="cursor-pointer gap-2 focus:bg-secondary focus:text-primary transition-colors hover:text-primary">
|
<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" />
|
<UserCircle className="h-4 w-4 text-muted-foreground" />
|
||||||
<span>Menu Profil</span>
|
<span>Menu Profil</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
</Link>
|
||||||
|
|
||||||
<DropdownMenuSeparator className="bg-border" />
|
<DropdownMenuSeparator className="bg-border" />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue