This commit is contained in:
2026-04-07 16:20:18 +07:00
parent 91f3b16634
commit e3065e1bf5
16 changed files with 442 additions and 195 deletions

6
api.ts
View File

@@ -1,10 +1,14 @@
export const API_URL_ROOT = process.env.NEXT_PUBLIC_API_URL_ROOT || ""; export const API_URL_ROOT = process.env.NEXT_PUBLIC_API_URL_ROOT || "";
export const URL_MEDIA = "https://cdn.kain.id.vn/history-app/"
export const API = { export const API = {
User : { User : {
CURRENT: `${API_URL_ROOT}/users/current`, CURRENT: `${API_URL_ROOT}/users/current`,
MEDIA: `${API_URL_ROOT}/users/current/media`, MEDIA: `${API_URL_ROOT}/users/current/media`,
Update: (Id: number | string) => `${API_URL_ROOT}/users/${Id}`, Update: (Id: number | string) => `${API_URL_ROOT}/users/${Id}`,
CHANGE_PASSWORD: (Id: number | string) => `${API_URL_ROOT}/users/${Id}/password`,
},
Media:{
PRESIGNED: `${API_URL_ROOT}/media/presigned`
}, },
Auth : { Auth : {
SIGNUP: `${API_URL_ROOT}/auth/signup`, SIGNUP: `${API_URL_ROOT}/auth/signup`,

View File

@@ -2,6 +2,17 @@ import type { NextConfig } from "next";
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
/* config options here */ /* config options here */
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'lh3.googleusercontent.com',
port: '',
pathname: '/**',
},
],
},
webpack(config) { webpack(config) {
config.module.rules.push({ config.module.rules.push({
test: /\.svg$/, test: /\.svg$/,
@@ -18,7 +29,6 @@ const nextConfig: NextConfig = {
}, },
}, },
}, },
}; };
export default nextConfig; export default nextConfig;

31
package-lock.json generated
View File

@@ -33,7 +33,8 @@
"react-redux": "^9.2.0", "react-redux": "^9.2.0",
"sonner": "^2.0.7", "sonner": "^2.0.7",
"swiper": "^11.2.10", "swiper": "^11.2.10",
"tailwind-merge": "^2.6.0" "tailwind-merge": "^2.6.0",
"yet-another-react-lightbox": "^3.30.1"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.3.1", "@eslint/eslintrc": "^3.3.1",
@@ -3524,7 +3525,7 @@
"version": "19.2.3", "version": "19.2.3",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"dev": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
"@types/react": "^19.2.0" "@types/react": "^19.2.0"
@@ -9466,6 +9467,32 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/yet-another-react-lightbox": {
"version": "3.30.1",
"resolved": "https://registry.npmjs.org/yet-another-react-lightbox/-/yet-another-react-lightbox-3.30.1.tgz",
"integrity": "sha512-VYy9UZbBtHkuU6FnABF9G9UCAr56TPUcb3uFXHpgMd+FhqvPQDTbfSmwsax3cdMtSc/udu5ycfHz8jwFeWMP3g==",
"license": "MIT",
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/igordanchenko"
},
"peerDependencies": {
"@types/react": "^16 || ^17 || ^18 || ^19",
"@types/react-dom": "^16 || ^17 || ^18 || ^19",
"react": "^16.8.0 || ^17 || ^18 || ^19",
"react-dom": "^16.8.0 || ^17 || ^18 || ^19"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/yocto-queue": { "node_modules/yocto-queue": {
"version": "0.1.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",

View File

@@ -34,7 +34,8 @@
"react-redux": "^9.2.0", "react-redux": "^9.2.0",
"sonner": "^2.0.7", "sonner": "^2.0.7",
"swiper": "^11.2.10", "swiper": "^11.2.10",
"tailwind-merge": "^2.6.0" "tailwind-merge": "^2.6.0",
"yet-another-react-lightbox": "^3.30.1"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.3.1", "@eslint/eslintrc": "^3.3.1",

View File

@@ -1,27 +1,28 @@
"use client"; "use client";
import UserAddressCard from "@/components/user-profile/UserAddressCard"; import AccountDetails from "@/components/user-profile/AccountDetails";
import MediaCard from "@/components/user-profile/Media";
import UserInfoCard from "@/components/user-profile/UserInfoCard"; import UserInfoCard from "@/components/user-profile/UserInfoCard";
import UserMetaCard from "@/components/user-profile/UserMetaCard"; import UserMetaCard from "@/components/user-profile/UserMetaCard";
import { MediaDto } from "@/interface/media";
import { UserMetaCardProps } from "@/interface/user"; import { UserMetaCardProps } from "@/interface/user";
import { apiGetCurrentUser } from "@/service/auth"; import { apiGetCurrentUser } from "@/service/auth";
import { apiGetCurrentUserMedia } from "@/service/userService"; import { apiGetCurrentUserMedia } from "@/service/userService";
import { RootState } from "@/store/store";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useSelector } from "react-redux";
export default function Profile() { export default function Profile() {
const [user, setUser] = useState<UserMetaCardProps | null>(null); const [user, setUser] = useState<UserMetaCardProps | null>(null);
const [mediaData, setMediaData] = useState<MediaDto | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
useEffect(() => { useEffect(() => {
const fetchUser = async () => { const fetchUser = async () => {
try { try {
const result = await apiGetCurrentUser(); const userData = await apiGetCurrentUser();
const mediaResult = await apiGetCurrentUserMedia(); const mediaResponse = await apiGetCurrentUserMedia(); // Giả sử hàm này return {status, data, message}
console.log("Current User:", result);
console.log("User Media:", mediaResult); setMediaData(mediaResponse);
setUser(result); setUser(userData);
} catch (err) { } catch (err) {
console.error("Lỗi:", err); console.error("Lỗi:", err);
} finally { } finally {
@@ -40,7 +41,8 @@ export default function Profile() {
<div className="space-y-6"> <div className="space-y-6">
<UserMetaCard data={user ?? {}} /> <UserMetaCard data={user ?? {}} />
<UserInfoCard data={user ?? {}} /> <UserInfoCard data={user ?? {}} />
<UserAddressCard /> <AccountDetails data={user ?? {}} />
<MediaCard data={mediaData ?? {}} />
</div> </div>
</div> </div>
</div> </div>

View File

@@ -73,7 +73,7 @@ export default function SignInForm() {
console.log("Current User Data:", data); console.log("Current User Data:", data);
if (data?.data) { if (data?.data) {
dispatch(setUserData(data.data)); dispatch(setUserData(data.data));
// router.push("/profile"); router.push("/");
} }
} else { } else {
toast.error("Email hoặc mật khẩu không đúng."); toast.error("Email hoặc mật khẩu không đúng.");

View File

@@ -0,0 +1,223 @@
"use client";
import React, { useState, useEffect } from "react";
import { useModal } from "../../hooks/useModal";
import { Modal } from "../ui/modal";
import Button from "../ui/button/Button";
import Input from "../form/input/InputField";
import Label from "../form/Label";
import { UserMetaCardProps } from "@/interface/user";
import { useRouter } from "next/navigation";
import { EyeCloseIcon, EyeIcon } from "@/icons";
import { apiChangePassword } from "@/service/auth";
import { toast } from "sonner";
export default function AccountDetails({ data }: { data: UserMetaCardProps }) {
const router = useRouter();
const { isOpen, openModal, closeModal } = useModal();
const [formValues, setFormValues] = useState({
old_password: "",
new_password: "",
confirm_password: "",
});
const [showOldPass, setShowOldPass] = useState(false);
const [error, setError] = useState("");
useEffect(() => {
if (!isOpen) {
setFormValues({
old_password: "",
new_password: "",
confirm_password: "",
});
setShowOldPass(false);
setError("");
}
}, [isOpen]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormValues((prev) => ({ ...prev, [name]: value }));
};
const handleSave = async (e: React.FormEvent) => {
e.preventDefault();
setError("");
if (formValues.new_password !== formValues.confirm_password) {
setError("Mật khẩu mới và xác nhận mật khẩu không khớp.");
return;
}
if (formValues.new_password.length < 8) {
setError("Mật khẩu mới phải có ít nhất 8 ký tự.");
return;
}
try {
const userId = data?.data?.id;
if (userId) {
const payload = {
old_password: formValues.old_password,
new_password: formValues.new_password,
};
await apiChangePassword(userId, payload as any);
closeModal();
toast.success("Cập nhật thành công!");
router.refresh();
}
} catch (err: any) {
setError(
`${err?.response?.data?.message}, Cập nhật thất bại, vui lòng kiểm tra lại thông tin. Mật khẩu tối thiểu 8 ký tự, 1 in hoa, 1 số và 1 ký tự đặc biệt.` ||
"Failed to update password. Please check your current password.",
);
toast.error(err?.response?.data?.message || "Cập nhật thất bại, vui lòng kiểm tra lại thông tin!");
}
};
return (
<>
<div className="p-5 border border-gray-200 rounded-2xl dark:border-gray-800 lg:p-6">
<div className="flex flex-col gap-6 lg:flex-row lg:items-start lg:justify-between">
<div>
<h4 className="text-lg font-semibold text-gray-800 dark:text-white/90 lg:mb-6">
Account Details
</h4>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2 lg:gap-7 2xl:gap-x-32">
<div>
<p className="mb-2 text-xs text-gray-500 dark:text-gray-400">
Email Address
</p>
<p className="text-sm font-medium text-gray-800 dark:text-white/90">
{data?.data?.email || "example@mail.com"}
</p>
</div>
<div>
<p className="mb-2 text-xs text-gray-500 dark:text-gray-400">
Password
</p>
<p className="text-sm font-medium text-gray-800 dark:text-white/90">
</p>
</div>
</div>
</div>
<button
onClick={openModal}
className="flex items-center justify-center gap-2 rounded-full border border-gray-300 bg-white px-4 py-3 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 lg:inline-flex lg:w-auto"
>
<svg
className="fill-current"
width="18"
height="18"
viewBox="0 0 18 18"
>
<path d="M15.0911 2.78206C14.2125 1.90338 12.7878 1.90338 11.9092 2.78206L4.57524 10.116C4.26682 10.4244 4.0547 10.8158 3.96468 11.2426L3.31231 14.3352C3.25997 14.5833 3.33653 14.841 3.51583 15.0203C3.69512 15.1996 3.95286 15.2761 4.20096 15.2238L7.29355 14.5714C7.72031 14.4814 8.11172 14.2693 8.42013 13.9609L15.7541 6.62695C16.6327 5.74827 16.6327 4.32365 15.7541 3.44497L15.0911 2.78206ZM12.9698 3.84272C13.2627 3.54982 13.7376 3.54982 14.0305 3.84272L14.6934 4.50563C14.9863 4.79852 14.9863 5.2734 14.6934 5.56629L14.044 6.21573L12.3204 4.49215L12.9698 3.84272ZM11.2597 5.55281L5.6359 11.1766C5.53309 11.2794 5.46238 11.4099 5.43238 11.5522L5.01758 13.5185L6.98394 13.1037C7.1262 13.0737 7.25666 13.003 7.35947 12.9002L12.9833 7.27639L11.2597 5.55281Z" />
</svg>
Edit
</button>
</div>
</div>
{/* Modal Chỉnh sửa */}
<Modal isOpen={isOpen} onClose={closeModal} className="max-w-[700px] m-4">
<div className="relative w-full p-4 overflow-y-auto bg-white no-scrollbar rounded-3xl dark:bg-gray-900 lg:p-11">
<div className="px-2 pr-14">
<h4 className="mb-2 text-2xl font-semibold text-gray-800 dark:text-white/90">
Update Password
</h4>
<p className="mb-6 text-sm text-gray-500 dark:text-gray-400">
For security, please enter your current password to set a new one.
</p>
</div>
<form className="flex flex-col" onSubmit={handleSave}>
<div className="px-2 overflow-y-auto custom-scrollbar">
<div className="grid grid-cols-1 gap-x-6 gap-y-5 lg:grid-cols-2">
{/* 1. Email (Disabled) */}
<div className="col-span-2">
<Label>Email Address</Label>
<Input
type="email"
defaultValue={data?.data?.email || ""}
disabled
className="bg-gray-100 dark:bg-gray-800 cursor-not-allowed"
/>
</div>
{/* 2. Current Password - CÓ NÚT HIỆN MẬT KHẨU */}
<div className="col-span-2 relative">
<Label>Current Password</Label>
<div className="relative">
<Input
type={showOldPass ? "text" : "password"}
name="old_password"
placeholder="Enter current password"
defaultValue={formValues.old_password}
onChange={handleChange}
/>
<button
type="button"
onClick={() => setShowOldPass(!showOldPass)}
className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
{showOldPass ? (
<EyeCloseIcon className="w-5 h-5" />
) : (
<EyeIcon className="w-5 h-5" />
)}
</button>
</div>
</div>
{/* 3. New Password - LUÔN ẨN */}
<div className="col-span-2 lg:col-span-1">
<Label>New Password</Label>
<Input
type="text"
name="new_password"
placeholder="Enter new password"
defaultValue={formValues.new_password}
onChange={handleChange}
/>
</div>
{/* 4. Confirm New Password - LUÔN ẨN */}
<div className="col-span-2 lg:col-span-1">
<Label>Confirm New Password</Label>
<Input
type="text"
name="confirm_password"
placeholder="Re-type new password"
defaultValue={formValues.confirm_password}
onChange={handleChange}
/>
</div>
</div>
{error && <p className="mt-4 text-sm text-red-500">{error}</p>}
</div>
<div className="flex items-center gap-3 px-2 mt-6 lg:justify-end">
<Button
size="sm"
variant="outline"
type="button"
onClick={closeModal}
>
Close
</Button>
<Button size="sm" type="submit">
Save Changes
</Button>
</div>
</form>
</div>
</Modal>
</>
);
}

View File

@@ -0,0 +1,73 @@
"use client";
import { useState } from "react";
import Lightbox from "yet-another-react-lightbox";
import Zoom from "yet-another-react-lightbox/plugins/zoom";
import Captions from "yet-another-react-lightbox/plugins/captions";
import "yet-another-react-lightbox/styles.css";
import "yet-another-react-lightbox/plugins/captions.css";
import { MediaDto } from "@/interface/media";
import { URL_MEDIA } from "../../../api";
export default function MediaCard({ data }: { data: MediaDto }) {
const [index, setIndex] = useState(-1);
const listMedia = data?.data || [];
const slides = listMedia.map((item) => ({
src: `${URL_MEDIA}${item.storage_key}`,
title: item.original_name,
description: `Size: ${(item.size / 1024).toFixed(2)} KB - Type: ${item.mime_type}`,
}));
return (
<div className="w-full">
<h3 className="mb-4 font-semibold">Media Assets</h3>
<div className="flex flex-row gap-4 overflow-x-auto pb-4 scrollbar-hide">
{listMedia.map((item, idx) => (
<div
key={item.id}
onClick={() => setIndex(idx)}
className="group relative min-w-[150px] h-[150px] cursor-pointer overflow-hidden rounded-lg border border-gray-200"
>
<img
src={`${URL_MEDIA}${item.storage_key}`}
alt={item.original_name}
className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-110"
/>
<div className="absolute inset-0 flex items-end bg-black/40 p-2 opacity-0 transition-opacity duration-300 group-hover:opacity-100">
<p className="w-full truncate text-xs text-white">
{item.original_name}
</p>
</div>
</div>
))}
</div>
<Lightbox
index={index}
open={index >= 0}
close={() => setIndex(-1)}
slides={slides}
plugins={[Zoom, Captions]}
zoom={{
maxZoomPixelRatio: 10,
zoomInMultiplier: 2,
doubleTapDelay: 300,
doubleClickDelay: 300,
doubleClickMaxStops: 2,
keyboardMoveDistance: 50,
wheelZoomDistanceFactor: 100,
pinchZoomDistanceFactor: 100,
}}
animation={{ zoom: 200 }}
styles={{
root: {
zIndex: 999999999,
"--yarl__color_backdrop": "rgba(0, 0, 0, 0.85)",
},
}}
/>
</div>
);
}

View File

@@ -1,140 +0,0 @@
"use client";
import React, { useState } from "react";
import { useModal } from "../../hooks/useModal";
import { Modal } from "../ui/modal";
import Button from "../ui/button/Button";
import Input from "../form/input/InputField";
import Label from "../form/Label";
import { Profile } from "@/interface/user";
export default function UserAddressCard() {
const { isOpen, openModal, closeModal } = useModal();
const [formData, setFormData] = useState<Profile>({
display_name: "",
phone: "",
bio: "",
location: "",
website: "",
// Thêm các field khác nếu cần
});
useEffect(() => {
if (data?.data?.profile) {
setFormData({
display_name: data.data.profile.display_name || "",
phone: data.data.profile.phone || "",
bio: data.data.profile.bio || "",
location: data.data.profile.location || "",
website: data.data.profile.website || "",
});
}
}, [data, isOpen]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData((prev) => ({
...prev,
[name]: value,
}));
};
return (
<>
<div className="p-5 border border-gray-200 rounded-2xl dark:border-gray-800 lg:p-6">
<div className="flex flex-col gap-6 lg:flex-row lg:items-start lg:justify-between">
<div>
<h4 className="text-lg font-semibold text-red-600 dark:text-white/90 lg:mb-6">
Account details
</h4>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2 lg:gap-7 2xl:gap-x-32">
<div>
<p className="mb-2 text-xs leading-normal text-gray-500 dark:text-gray-400">
Username
</p>
<p className="text-sm font-medium text-gray-800 dark:text-white/90">
United States
</p>
</div>
<div>
<p className="mb-2 text-xs leading-normal text-gray-500 dark:text-gray-400">
City/State
</p>
<p className="text-sm font-medium text-gray-800 dark:text-white/90">
Phoenix, Arizona, United States.
</p>
</div>
</div>
</div>
<button
onClick={openModal}
className="flex w-full items-center justify-center gap-2 rounded-full border border-gray-300 bg-white px-4 py-3 text-sm font-medium text-red-600 shadow-theme-xs hover:bg-gray-50 hover:text-red-800 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] dark:hover:text-gray-200 lg:inline-flex lg:w-auto"
>
<svg
className="fill-current"
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M15.0911 2.78206C14.2125 1.90338 12.7878 1.90338 11.9092 2.78206L4.57524 10.116C4.26682 10.4244 4.0547 10.8158 3.96468 11.2426L3.31231 14.3352C3.25997 14.5833 3.33653 14.841 3.51583 15.0203C3.69512 15.1996 3.95286 15.2761 4.20096 15.2238L7.29355 14.5714C7.72031 14.4814 8.11172 14.2693 8.42013 13.9609L15.7541 6.62695C16.6327 5.74827 16.6327 4.32365 15.7541 3.44497L15.0911 2.78206ZM12.9698 3.84272C13.2627 3.54982 13.7376 3.54982 14.0305 3.84272L14.6934 4.50563C14.9863 4.79852 14.9863 5.2734 14.6934 5.56629L14.044 6.21573L12.3204 4.49215L12.9698 3.84272ZM11.2597 5.55281L5.6359 11.1766C5.53309 11.2794 5.46238 11.4099 5.43238 11.5522L5.01758 13.5185L6.98394 13.1037C7.1262 13.0737 7.25666 13.003 7.35947 12.9002L12.9833 7.27639L11.2597 5.55281Z"
fill=""
/>
</svg>
Edit
</button>
</div>
</div>
<Modal isOpen={isOpen} onClose={closeModal} className="max-w-[700px] m-4">
<div className="relative w-full p-4 overflow-y-auto bg-white no-scrollbar rounded-3xl dark:bg-gray-900 lg:p-11">
<div className="px-2 pr-14">
<h4 className="mb-2 text-2xl font-semibold text-gray-800 dark:text-white/90">
Edit Address
</h4>
<p className="mb-6 text-sm text-gray-500 dark:text-gray-400 lg:mb-7">
Update your details to keep your profile up-to-date.
</p>
</div>
<form className="flex flex-col">
<div className="px-2 overflow-y-auto custom-scrollbar">
<div className="grid grid-cols-1 gap-x-6 gap-y-5 lg:grid-cols-2">
<div>
<Label>Country</Label>
<Input type="text" defaultValue="United States" />
</div>
<div>
<Label>City/State</Label>
<Input type="text" defaultValue="Arizona, United States." />
</div>
<div>
<Label>Postal Code</Label>
<Input type="text" defaultValue="ERT 2489" />
</div>
<div>
<Label>TAX ID</Label>
<Input type="text" defaultValue="AS4568384" />
</div>
</div>
</div>
<div className="flex items-center gap-3 px-2 mt-6 lg:justify-end">
<Button size="sm" variant="outline" onClick={closeModal}>
Close
</Button>
<Button size="sm" onClick={handleSave}>
Save Changes
</Button>
</div>
</form>
</div>
</Modal>
</>
);
}

View File

@@ -14,14 +14,13 @@ import Link from "next/link";
export default function UserInfoCard({ data }: { data: UserMetaCardProps }) { export default function UserInfoCard({ data }: { data: UserMetaCardProps }) {
const router = useRouter(); const router = useRouter();
const { isOpen, openModal, closeModal } = useModal(); const { isOpen, openModal, closeModal } = useModal();
// 1. Khởi tạo state cho form
const [formData, setFormData] = useState<Profile>({ const [formData, setFormData] = useState<Profile>({
display_name: "", display_name: "",
phone: "", phone: "",
bio: "", bio: "",
location: "", location: "",
website: "", website: "",
// Thêm các field khác nếu cần avatar_url: "",
}); });
useEffect(() => { useEffect(() => {
@@ -32,6 +31,7 @@ export default function UserInfoCard({ data }: { data: UserMetaCardProps }) {
bio: data.data.profile.bio || "", bio: data.data.profile.bio || "",
location: data.data.profile.location || "", location: data.data.profile.location || "",
website: data.data.profile.website || "", website: data.data.profile.website || "",
avatar_url: data.data.profile.avatar_url || "",
}); });
} }
}, [data, isOpen]); }, [data, isOpen]);
@@ -65,6 +65,14 @@ export default function UserInfoCard({ data }: { data: UserMetaCardProps }) {
</h4> </h4>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2 lg:gap-7 2xl:gap-x-32"> <div className="grid grid-cols-1 gap-4 lg:grid-cols-2 lg:gap-7 2xl:gap-x-32">
{/* <div>
<p className="mb-2 text-xs leading-normal text-gray-500 dark:text-gray-400">
Avatar
</p>
<p className="text-sm font-medium text-gray-800 dark:text-white/90">
{data.data?.profile?.avatar_url || "avatar_url"}
</p>
</div> */}
<div> <div>
<p className="mb-2 text-xs leading-normal text-gray-500 dark:text-gray-400"> <p className="mb-2 text-xs leading-normal text-gray-500 dark:text-gray-400">
Full Name Full Name
@@ -164,6 +172,15 @@ export default function UserInfoCard({ data }: { data: UserMetaCardProps }) {
</h5> </h5>
<div className="grid grid-cols-1 gap-x-6 gap-y-5 lg:grid-cols-2"> <div className="grid grid-cols-1 gap-x-6 gap-y-5 lg:grid-cols-2">
<div className="col-span-2">
<Label>Avatar</Label>
<Input
type="text"
name="avatar_url"
defaultValue={formData.avatar_url}
onChange={handleChange}
/>
</div>
<div className="col-span-2"> <div className="col-span-2">
<Label>Display Name</Label> <Label>Display Name</Label>
<Input <Input

View File

@@ -1,20 +1,15 @@
"use client"; "use client";
import React from "react"; // import React from "react";
import { useModal } from "../../hooks/useModal"; // import { useModal } from "../../hooks/useModal";
import { Modal } from "../ui/modal"; // import { Modal } from "../ui/modal";
import Button from "../ui/button/Button"; // import Button from "../ui/button/Button";
import Input from "../form/input/InputField"; // import Input from "../form/input/InputField";
import Label from "../form/Label"; // import Label from "../form/Label";
import Image from "next/image"; import Image from "next/image";
import { UserMetaCardProps } from "@/interface/user"; import { UserMetaCardProps } from "@/interface/user";
export default function UserMetaCard({ data }: { data: UserMetaCardProps }) { export default function UserMetaCard({ data }: { data: UserMetaCardProps }) {
const { isOpen, openModal, closeModal } = useModal(); // const { isOpen, openModal, closeModal } = useModal();
const handleSave = () => {
console.log("Saving changes...");
closeModal();
};
console.log("UserMetaCard data:", data.data?.profile?.display_name);
return ( return (
<> <>
<div className="p-5 border border-gray-200 rounded-2xl dark:border-gray-800 lg:p-6"> <div className="p-5 border border-gray-200 rounded-2xl dark:border-gray-800 lg:p-6">
@@ -33,6 +28,11 @@ export default function UserMetaCard({ data }: { data: UserMetaCardProps }) {
{data.data?.profile?.display_name || "Full Name"} {data.data?.profile?.display_name || "Full Name"}
</h4> </h4>
<div className="flex flex-col items-center gap-1 text-center xl:flex-row xl:gap-3 xl:text-left"> <div className="flex flex-col items-center gap-1 text-center xl:flex-row xl:gap-3 xl:text-left">
<p className="text-sm text-blue-500 dark:text-gray-400">
{data.data?.roles?.map((role) => role.name).join(", ") ||
"No roles available"}
</p>
<div className="hidden h-3.5 w-px bg-gray-300 dark:bg-gray-700 xl:block"></div>
<p className="text-sm text-gray-500 dark:text-gray-400"> <p className="text-sm text-gray-500 dark:text-gray-400">
{data.data?.profile?.bio || "No bio available"} {data.data?.profile?.bio || "No bio available"}
</p> </p>
@@ -40,11 +40,6 @@ export default function UserMetaCard({ data }: { data: UserMetaCardProps }) {
<p className="text-sm text-gray-500 dark:text-gray-400"> <p className="text-sm text-gray-500 dark:text-gray-400">
{data.data?.profile?.location || "user location"} {data.data?.profile?.location || "user location"}
</p> </p>
<div className="hidden h-3.5 w-px bg-gray-300 dark:bg-gray-700 xl:block"></div>
<p className="text-sm text-blue-500 dark:text-gray-400">
{data.data?.roles?.map((role) => role.name).join(", ") ||
"No roles available"}
</p>
</div> </div>
</div> </div>
{/* <div className="flex items-center order-2 gap-2 grow xl:order-3 xl:justify-end"> {/* <div className="flex items-center order-2 gap-2 grow xl:order-3 xl:justify-end">

View File

@@ -50,7 +50,7 @@ api.interceptors.response.use(
} catch (refreshErr) { } catch (refreshErr) {
processQueue(refreshErr) processQueue(refreshErr)
// window.location.href = "/login" window.location.href = "/signin"
return Promise.reject(refreshErr) return Promise.reject(refreshErr)
} finally { } finally {

21
src/interface/media.ts Normal file
View File

@@ -0,0 +1,21 @@
interface item{
file_metadata:string;
id:string;
mime_type:string;
original_name:string;
size:number;
updated_at:string;
user_id:string;
storage_key:string;
}
export interface MediaDto {
data?: item[];
message?:string;
status?:boolean;
}
export interface payloadPresignedMedia {
filename:string;
contentType:string;
}

View File

@@ -374,7 +374,7 @@ const AppSidebar: React.FC = () => {
</div> </div>
</div> </div>
</nav> </nav>
{isExpanded || isHovered || isMobileOpen ? <SidebarWidget /> : null} {/* {isExpanded || isHovered || isMobileOpen ? <SidebarWidget /> : null} */}
</div> </div>
</aside> </aside>
); );

View File

@@ -31,3 +31,7 @@ export const apiGetCurrentUser = async () => {
return response?.data; return response?.data;
}; };
export const apiChangePassword = async (id:string,payload: any) => {
const response = await api.patch(API.User.CHANGE_PASSWORD(id), payload);
return response?.data;
};

View File

@@ -0,0 +1,10 @@
import api from "@/config/config";
import { API } from "../../api";
import { payloadPresignedMedia } from "@/interface/media";
export const apiGetCurrentUserMedia = async (payload: payloadPresignedMedia) => {
const response = await api.get(API.Media.PRESIGNED, {
params: payload,
});
return response?.data;
};