application page
This commit is contained in:
38
src/app/(admin)/(others-pages)/profile/applications/page.tsx
Normal file
38
src/app/(admin)/(others-pages)/profile/applications/page.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
"use client";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
import { RootState } from "@/store/store";
|
||||||
|
import { SafeHTMLRenderer } from "@/components/ui/parse/SafeHTMLRenderer";
|
||||||
|
|
||||||
|
export default function ApplicationDetailPage() {
|
||||||
|
const application = useSelector((state: RootState) => state.user.selectedApplication);
|
||||||
|
|
||||||
|
if (!application) {
|
||||||
|
return <div className="p-10 text-center">Đang tải hoặc không có dữ liệu...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6 bg-white dark:bg-gray-900 rounded-xl shadow">
|
||||||
|
<h2 className="text-2xl font-bold mb-6 border-b pb-2">Chi tiết Application</h2>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-semibold text-gray-400 uppercase">Loại yêu cầu:</label>
|
||||||
|
<p className="text-lg font-medium">{application.verify_type}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-semibold text-gray-400 uppercase mb-3 block">
|
||||||
|
Nội dung hiển thị (CV):
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{/* SỬ DỤNG Ở ĐÂY */}
|
||||||
|
<div className="border border-gray-100 rounded-lg p-2 bg-gray-50">
|
||||||
|
<SafeHTMLRenderer html={application.content} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Các phần khác như Media... */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,18 +1,20 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import AccountDetails from "@/components/user-profile/AccountDetails";
|
import AccountDetails from "@/components/user-profile/AccountDetails";
|
||||||
|
import ApplicationList from "@/components/user-profile/ApplicationList";
|
||||||
import MediaCard from "@/components/user-profile/Media";
|
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 { 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 { apiGetCurrentUserApplications, apiGetCurrentUserMedia } from "@/service/userService";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
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 [mediaData, setMediaData] = useState<MediaDto | null>(null);
|
||||||
|
const [applications, setApplications] = useState<any[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -20,6 +22,12 @@ export default function Profile() {
|
|||||||
try {
|
try {
|
||||||
const userData = await apiGetCurrentUser();
|
const userData = await apiGetCurrentUser();
|
||||||
const mediaResponse = await apiGetCurrentUserMedia();
|
const mediaResponse = await apiGetCurrentUserMedia();
|
||||||
|
const userApplications = await apiGetCurrentUserApplications();
|
||||||
|
// console.log("User Applications:", userApplications);
|
||||||
|
|
||||||
|
if (userApplications?.data) {
|
||||||
|
setApplications(userApplications.data);
|
||||||
|
}
|
||||||
|
|
||||||
setMediaData(mediaResponse);
|
setMediaData(mediaResponse);
|
||||||
setUser(userData);
|
setUser(userData);
|
||||||
@@ -42,6 +50,7 @@ export default function Profile() {
|
|||||||
<UserMetaCard data={user ?? {}} />
|
<UserMetaCard data={user ?? {}} />
|
||||||
<UserInfoCard data={{ ...user, openEdit: true }} />
|
<UserInfoCard data={{ ...user, openEdit: true }} />
|
||||||
{(mediaData?.data?.length ?? 0) > 0 && <MediaCard data={mediaData ?? {}} />}
|
{(mediaData?.data?.length ?? 0) > 0 && <MediaCard data={mediaData ?? {}} />}
|
||||||
|
<ApplicationList applications={applications} />
|
||||||
<AccountDetails data={user ?? {}} />
|
<AccountDetails data={user ?? {}} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -255,13 +255,13 @@ export default function ApplicationTable({
|
|||||||
<TableCell className="px-5 py-4 text-gray-600 text-theme-sm dark:text-gray-400">
|
<TableCell className="px-5 py-4 text-gray-600 text-theme-sm dark:text-gray-400">
|
||||||
{app.reviewer?.display_name || "-"}
|
{app.reviewer?.display_name || "-"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="group relative px-5 pb-4 text-start text-theme-xs text-gray-500 dark:text-gray-400 max-w-50">
|
<TableCell className="group relative px-5 pb-4 text-start text-theme-xs text-gray-500 dark:text-gray-400 max-w-50 min-w-50">
|
||||||
<div className="truncate">{app.review_note || "-"}</div>
|
<div className="truncate">{app.review_note || "-"}</div>
|
||||||
|
|
||||||
{app.review_note && (
|
{app.review_note && (
|
||||||
<div className="invisible group-hover:visible absolute z-50 bottom-full left-1/2 -translate-x-1/2 mb-1 w-max max-w-75 p-2 backdrop-blur-sm text-black text-xs rounded-lg shadow-xl wrap-break-words whitespace-normal">
|
<div className="invisible group-hover:visible absolute z-50 bottom-full left-1/2 -translate-x-1/2 w-max max-w-75 p-2 backdrop-blur-xs text-black text-xs rounded-lg shadow-xl wrap-break-words whitespace-normal">
|
||||||
{app.review_note}
|
{app.review_note}
|
||||||
{/* <div className="absolute top-full left-1/2 -translate-x-1/2 border-4 border-transparent border-t-gray-600"></div> */}
|
<div className="absolute top-full left-1/2 border-6 border-transparent border-t-white"></div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|||||||
27
src/components/ui/parse/SafeHTMLRenderer.tsx
Normal file
27
src/components/ui/parse/SafeHTMLRenderer.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
"use client";
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
// Component này đóng vai trò như một "vùng an toàn"
|
||||||
|
export function SafeHTMLRenderer({ html }: { html: string }) {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (containerRef.current) {
|
||||||
|
// 1. Giải mã HTML Entities (Biến < thành <, " thành ", ...)
|
||||||
|
const txt = document.createElement("textarea");
|
||||||
|
txt.innerHTML = html;
|
||||||
|
let decoded = txt.value;
|
||||||
|
|
||||||
|
// 2. Xử lý xóa thẻ <pre> bọc ngoài nếu API trả về dư thừa
|
||||||
|
decoded = decoded.replace(/<pre[^>]*>/g, "").replace(/<\/pre>/g, "");
|
||||||
|
|
||||||
|
// 3. Khởi tạo Shadow DOM (nếu chưa có)
|
||||||
|
const shadowRoot = containerRef.current.shadowRoot || containerRef.current.attachShadow({ mode: "open" });
|
||||||
|
|
||||||
|
// 4. Render nội dung vào trong vùng cô lập
|
||||||
|
shadowRoot.innerHTML = decoded;
|
||||||
|
}
|
||||||
|
}, [html]);
|
||||||
|
|
||||||
|
return <div ref={containerRef} className="w-full h-auto" />;
|
||||||
|
}
|
||||||
32
src/components/user-profile/ApplicationList.tsx
Normal file
32
src/components/user-profile/ApplicationList.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { setSelectedApplication } from "@/store/features/userSlice";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useDispatch } from "react-redux";
|
||||||
|
|
||||||
|
export default function ApplicationList({ applications }: { applications: any[] }) {
|
||||||
|
const router = useRouter();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const handleViewDetail = (app: any) => {
|
||||||
|
dispatch(setSelectedApplication(app));
|
||||||
|
router.push(`/profile/applications`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{applications.map((app) => (
|
||||||
|
<div
|
||||||
|
key={app.id}
|
||||||
|
onClick={() => handleViewDetail(app)}
|
||||||
|
className="p-4 border rounded-lg cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 transition"
|
||||||
|
>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span>Loại: {app.verify_type}</span>
|
||||||
|
<span className="text-blue-500 font-medium">Xem chi tiết →</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,13 +1,25 @@
|
|||||||
import { UserData } from '@/interface/user';
|
import { UserData } from '@/interface/user';
|
||||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
// Hàm helper để đọc dữ liệu an toàn từ storage
|
||||||
|
const getStoredApplication = () => {
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
const saved = sessionStorage.getItem('selected_application');
|
||||||
|
return saved ? JSON.parse(saved) : null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
interface UserState {
|
interface UserState {
|
||||||
data: UserData | null;
|
data: UserData | null;
|
||||||
isAuthenticated: boolean;
|
isAuthenticated: boolean;
|
||||||
|
selectedApplication: any | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: UserState = {
|
const initialState: UserState = {
|
||||||
data: null,
|
data: null,
|
||||||
isAuthenticated: false,
|
isAuthenticated: false,
|
||||||
|
selectedApplication: getStoredApplication(), // Khởi tạo từ storage
|
||||||
};
|
};
|
||||||
|
|
||||||
const userSlice = createSlice({
|
const userSlice = createSlice({
|
||||||
@@ -18,12 +30,21 @@ const userSlice = createSlice({
|
|||||||
state.data = action.payload;
|
state.data = action.payload;
|
||||||
state.isAuthenticated = true;
|
state.isAuthenticated = true;
|
||||||
},
|
},
|
||||||
clearUserData: (state) => {
|
setSelectedApplication: (state, action: PayloadAction<any>) => {
|
||||||
state.data = null;
|
state.selectedApplication = action.payload;
|
||||||
state.isAuthenticated = false;
|
// Lưu vào sessionStorage để khi reload trang không bị mất
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
sessionStorage.setItem('selected_application', JSON.stringify(action.payload));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
clearSelectedApplication: (state) => {
|
||||||
|
state.selectedApplication = null;
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
sessionStorage.removeItem('selected_application');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { setUserData, clearUserData } = userSlice.actions;
|
export const { setUserData, setSelectedApplication, clearSelectedApplication } = userSlice.actions;
|
||||||
export default userSlice.reducer;
|
export default userSlice.reducer;
|
||||||
Reference in New Issue
Block a user