media
This commit is contained in:
6
api.ts
6
api.ts
@@ -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`,
|
||||||
|
|||||||
@@ -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
31
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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.");
|
||||||
|
|||||||
223
src/components/user-profile/AccountDetails.tsx
Normal file
223
src/components/user-profile/AccountDetails.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
73
src/components/user-profile/Media.tsx
Normal file
73
src/components/user-profile/Media.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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
21
src/interface/media.ts
Normal 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;
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
|
|||||||
10
src/service/mediaService.ts
Normal file
10
src/service/mediaService.ts
Normal 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;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user