fix(profil-module): QC improvements based on QC-PROFIL-MODULE.md
- Fix fetch method inconsistency (convert to ApiFetch) - programInovasi: findUnique, delete, update methods - mediaSosial: findUnique, delete, update methods - Add loading state to findUnique operations - Fix iconUrl validation (make optional instead of required) - Add DOMPurify for HTML sanitization (XSS protection) - program-inovasi page.tsx (list & detail) - Remove console.log in production (use dev-only logging) - Install dompurify and @types/dompurify Security: Prevent XSS attacks by sanitizing HTML content Consistency: Use ApiFetch for all API operations UX: Proper loading states for better user feedback Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
@@ -113,6 +113,7 @@
|
|||||||
"@playwright/test": "^1.58.2",
|
"@playwright/test": "^1.58.2",
|
||||||
"@testing-library/jest-dom": "^6.9.1",
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
"@types/cli-progress": "^3.11.6",
|
"@types/cli-progress": "^3.11.6",
|
||||||
|
"@types/dompurify": "^3.2.0",
|
||||||
"@types/jsonwebtoken": "^9.0.10",
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
|
|||||||
@@ -55,10 +55,15 @@ const programInovasi = proxy({
|
|||||||
programInovasi.findMany.load();
|
programInovasi.findMany.load();
|
||||||
return toast.success("Sukses menambahkan");
|
return toast.success("Sukses menambahkan");
|
||||||
}
|
}
|
||||||
console.log(res);
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.log(res);
|
||||||
|
}
|
||||||
return toast.error("failed create");
|
return toast.error("failed create");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log((error as Error).message);
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.error("Create error:", error);
|
||||||
|
}
|
||||||
|
toast.error("Gagal menambahkan data");
|
||||||
} finally {
|
} finally {
|
||||||
programInovasi.create.loading = false;
|
programInovasi.create.loading = false;
|
||||||
}
|
}
|
||||||
@@ -91,13 +96,17 @@ const programInovasi = proxy({
|
|||||||
programInovasi.findMany.total = res.data.total || 0;
|
programInovasi.findMany.total = res.data.total || 0;
|
||||||
programInovasi.findMany.totalPages = res.data.totalPages || 1;
|
programInovasi.findMany.totalPages = res.data.totalPages || 1;
|
||||||
} else {
|
} else {
|
||||||
console.error("Failed to load pegawai:", res.data?.message);
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.error("Failed to load pegawai:", res.data?.message);
|
||||||
|
}
|
||||||
programInovasi.findMany.data = [];
|
programInovasi.findMany.data = [];
|
||||||
programInovasi.findMany.total = 0;
|
programInovasi.findMany.total = 0;
|
||||||
programInovasi.findMany.totalPages = 1;
|
programInovasi.findMany.totalPages = 1;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading pegawai:", error);
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.error("Error loading pegawai:", error);
|
||||||
|
}
|
||||||
programInovasi.findMany.data = [];
|
programInovasi.findMany.data = [];
|
||||||
programInovasi.findMany.total = 0;
|
programInovasi.findMany.total = 0;
|
||||||
programInovasi.findMany.totalPages = 1;
|
programInovasi.findMany.totalPages = 1;
|
||||||
@@ -112,19 +121,25 @@ const programInovasi = proxy({
|
|||||||
image: true;
|
image: true;
|
||||||
};
|
};
|
||||||
}> | null,
|
}> | null,
|
||||||
|
loading: false,
|
||||||
async load(id: string) {
|
async load(id: string) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/landingpage/programinovasi/${id}`);
|
programInovasi.findUnique.loading = true;
|
||||||
if (res.ok) {
|
const res = await (ApiFetch.api.landingpage.programinovasi as any)[id].get();
|
||||||
const data = await res.json();
|
if (res.data?.success) {
|
||||||
programInovasi.findUnique.data = data.data ?? null;
|
programInovasi.findUnique.data = res.data.data ?? null;
|
||||||
|
return res.data.data;
|
||||||
} else {
|
} else {
|
||||||
console.error("Failed to fetch program inovasi:", res.statusText);
|
toast.error(res.data?.message || "Gagal memuat data program inovasi");
|
||||||
programInovasi.findUnique.data = null;
|
programInovasi.findUnique.data = null;
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching program inovasi:", error);
|
console.error("Error fetching program inovasi:", error);
|
||||||
programInovasi.findUnique.data = null;
|
programInovasi.findUnique.data = null;
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
programInovasi.findUnique.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -135,27 +150,18 @@ const programInovasi = proxy({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
programInovasi.delete.loading = true;
|
programInovasi.delete.loading = true;
|
||||||
|
const res = await (ApiFetch.api.landingpage.programinovasi as any)["del"][id].delete();
|
||||||
|
|
||||||
const response = await fetch(
|
if (res.data?.success) {
|
||||||
`/api/landingpage/programinovasi/del/${id}`,
|
toast.success(res.data.message || "Program inovasi berhasil dihapus");
|
||||||
{
|
await programInovasi.findMany.load();
|
||||||
method: "DELETE",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
if (response.ok && result?.success) {
|
|
||||||
toast.success(result.message || "Program inovasi berhasil dihapus");
|
|
||||||
await programInovasi.findMany.load(); // refresh list
|
|
||||||
} else {
|
} else {
|
||||||
toast.error(result?.message || "Gagal menghapus program inovasi");
|
toast.error(res.data?.message || "Gagal menghapus program inovasi");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Gagal delete:", error);
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.error("Gagal delete:", error);
|
||||||
|
}
|
||||||
toast.error("Terjadi kesalahan saat menghapus program inovasi");
|
toast.error("Terjadi kesalahan saat menghapus program inovasi");
|
||||||
} finally {
|
} finally {
|
||||||
programInovasi.delete.loading = false;
|
programInovasi.delete.loading = false;
|
||||||
@@ -174,20 +180,11 @@ const programInovasi = proxy({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/landingpage/programinovasi/${id}`, {
|
programInovasi.update.loading = true;
|
||||||
method: "GET",
|
const res = await (ApiFetch.api.landingpage.programinovasi as any)[id].get();
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await response.json();
|
if (res.data?.success) {
|
||||||
|
const data = res.data.data;
|
||||||
if (result?.success) {
|
|
||||||
const data = result.data;
|
|
||||||
this.id = data.id;
|
this.id = data.id;
|
||||||
this.form = {
|
this.form = {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
@@ -197,13 +194,15 @@ const programInovasi = proxy({
|
|||||||
};
|
};
|
||||||
return data;
|
return data;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
toast.error(res.data?.message || "Gagal mengambil data program inovasi");
|
||||||
result?.message || "Gagal mengambil data program inovasi"
|
return null;
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error((error as Error).message);
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.error("Error loading program inovasi:", error);
|
||||||
|
}
|
||||||
toast.error("Terjadi kesalahan saat mengambil data program inovasi");
|
toast.error("Terjadi kesalahan saat mengambil data program inovasi");
|
||||||
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
programInovasi.update.loading = false;
|
programInovasi.update.loading = false;
|
||||||
}
|
}
|
||||||
@@ -221,41 +220,25 @@ const programInovasi = proxy({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
programInovasi.update.loading = true;
|
programInovasi.update.loading = true;
|
||||||
|
const res = await (ApiFetch.api.landingpage.programinovasi as any)[this.id].put({
|
||||||
|
name: this.form.name,
|
||||||
|
description: this.form.description,
|
||||||
|
imageId: this.form.imageId,
|
||||||
|
link: this.form.link,
|
||||||
|
});
|
||||||
|
|
||||||
const response = await fetch(
|
if (res.data?.success) {
|
||||||
`/api/landingpage/programinovasi/${this.id}`,
|
|
||||||
{
|
|
||||||
method: "PUT",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
name: this.form.name,
|
|
||||||
description: this.form.description,
|
|
||||||
imageId: this.form.imageId,
|
|
||||||
link: this.form.link,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorData = await response.json().catch(() => ({}));
|
|
||||||
throw new Error(
|
|
||||||
errorData.message || `HTTP error! status: ${response.status}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
toast.success("Berhasil update program inovasi");
|
toast.success("Berhasil update program inovasi");
|
||||||
await programInovasi.findMany.load(); // refresh list
|
await programInovasi.findMany.load();
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(result.message || "Gagal update program inovasi");
|
toast.error(res.data?.message || "Gagal update program inovasi");
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating program inovasi:", error);
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.error("Error updating program inovasi:", error);
|
||||||
|
}
|
||||||
toast.error(
|
toast.error(
|
||||||
error instanceof Error
|
error instanceof Error
|
||||||
? error.message
|
? error.message
|
||||||
@@ -443,7 +426,7 @@ const pejabatDesa = proxy({
|
|||||||
const templateMediaSosial = z.object({
|
const templateMediaSosial = z.object({
|
||||||
name: z.string().min(3, "Nama minimal 3 karakter"),
|
name: z.string().min(3, "Nama minimal 3 karakter"),
|
||||||
imageId: z.string().nullable().optional(),
|
imageId: z.string().nullable().optional(),
|
||||||
iconUrl: z.string().min(3, "Icon URL minimal 3 karakter"),
|
iconUrl: z.string().optional(), // ✅ Optional - tidak selalu required
|
||||||
icon: z.string().nullable().optional(),
|
icon: z.string().nullable().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -484,10 +467,15 @@ const mediaSosial = proxy({
|
|||||||
mediaSosial.findMany.load();
|
mediaSosial.findMany.load();
|
||||||
return toast.success("Sukses menambahkan");
|
return toast.success("Sukses menambahkan");
|
||||||
}
|
}
|
||||||
console.log(res);
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.log(res);
|
||||||
|
}
|
||||||
return toast.error("failed create");
|
return toast.error("failed create");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log((error as Error).message);
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.log((error as Error).message);
|
||||||
|
}
|
||||||
|
toast.error("Gagal menambahkan data");
|
||||||
} finally {
|
} finally {
|
||||||
mediaSosial.create.loading = false;
|
mediaSosial.create.loading = false;
|
||||||
}
|
}
|
||||||
@@ -518,13 +506,17 @@ const mediaSosial = proxy({
|
|||||||
mediaSosial.findMany.total = res.data.total || 0;
|
mediaSosial.findMany.total = res.data.total || 0;
|
||||||
mediaSosial.findMany.totalPages = res.data.totalPages || 1;
|
mediaSosial.findMany.totalPages = res.data.totalPages || 1;
|
||||||
} else {
|
} else {
|
||||||
console.error("Failed to load media sosial:", res.data?.message);
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.error("Failed to load media sosial:", res.data?.message);
|
||||||
|
}
|
||||||
mediaSosial.findMany.data = [];
|
mediaSosial.findMany.data = [];
|
||||||
mediaSosial.findMany.total = 0;
|
mediaSosial.findMany.total = 0;
|
||||||
mediaSosial.findMany.totalPages = 1;
|
mediaSosial.findMany.totalPages = 1;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading media sosial:", error);
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.error("Error loading media sosial:", error);
|
||||||
|
}
|
||||||
mediaSosial.findMany.data = [];
|
mediaSosial.findMany.data = [];
|
||||||
mediaSosial.findMany.total = 0;
|
mediaSosial.findMany.total = 0;
|
||||||
mediaSosial.findMany.totalPages = 1;
|
mediaSosial.findMany.totalPages = 1;
|
||||||
@@ -539,25 +531,32 @@ const mediaSosial = proxy({
|
|||||||
image: true;
|
image: true;
|
||||||
};
|
};
|
||||||
}> | null,
|
}> | null,
|
||||||
|
loading: false,
|
||||||
async load(id: string) {
|
async load(id: string) {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
toast.warn("ID tidak valid");
|
toast.warn("ID tidak valid");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
mediaSosial.update.loading = true;
|
mediaSosial.findUnique.loading = true;
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/landingpage/mediasosial/${id}`);
|
const res = await (ApiFetch.api.landingpage.mediasosial as any)[id].get();
|
||||||
if (res.ok) {
|
if (res.data?.success) {
|
||||||
const data = await res.json();
|
mediaSosial.findUnique.data = res.data.data ?? null;
|
||||||
mediaSosial.findUnique.data = data.data ?? null;
|
return res.data.data;
|
||||||
} else {
|
} else {
|
||||||
console.error("Failed to fetch media sosial:", res.statusText);
|
toast.error(res.data?.message || "Gagal memuat data media sosial");
|
||||||
mediaSosial.findUnique.data = null;
|
mediaSosial.findUnique.data = null;
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching media sosial:", error);
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.error("Error fetching media sosial:", error);
|
||||||
|
}
|
||||||
mediaSosial.findUnique.data = null;
|
mediaSosial.findUnique.data = null;
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
mediaSosial.findUnique.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -568,24 +567,18 @@ const mediaSosial = proxy({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
mediaSosial.delete.loading = true;
|
mediaSosial.delete.loading = true;
|
||||||
|
const res = await (ApiFetch.api.landingpage.mediasosial as any)["del"][id].delete();
|
||||||
|
|
||||||
const response = await fetch(`/api/landingpage/mediasosial/del/${id}`, {
|
if (res.data?.success) {
|
||||||
method: "DELETE",
|
toast.success(res.data.message || "Media Sosial berhasil dihapus");
|
||||||
headers: {
|
await mediaSosial.findMany.load();
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
if (response.ok && result?.success) {
|
|
||||||
toast.success(result.message || "Media Sosial berhasil dihapus");
|
|
||||||
await mediaSosial.findMany.load(); // refresh list
|
|
||||||
} else {
|
} else {
|
||||||
toast.error(result?.message || "Gagal menghapus media sosial");
|
toast.error(res.data?.message || "Gagal menghapus media sosial");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Gagal delete:", error);
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.error("Gagal delete:", error);
|
||||||
|
}
|
||||||
toast.error("Terjadi kesalahan saat menghapus media sosial");
|
toast.error("Terjadi kesalahan saat menghapus media sosial");
|
||||||
} finally {
|
} finally {
|
||||||
mediaSosial.delete.loading = false;
|
mediaSosial.delete.loading = false;
|
||||||
@@ -603,43 +596,32 @@ const mediaSosial = proxy({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
mediaSosial.update.loading = true; // ✅ Tambahkan ini di awal
|
mediaSosial.update.loading = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/landingpage/mediasosial/${id}`, {
|
const res = await (ApiFetch.api.landingpage.mediasosial as any)[id].get();
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (res.data?.success) {
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
const data = res.data.data;
|
||||||
}
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
if (result?.success) {
|
|
||||||
const data = result.data;
|
|
||||||
this.id = data.id;
|
this.id = data.id;
|
||||||
this.form = {
|
this.form = {
|
||||||
name: data.name || "",
|
name: data.name || "",
|
||||||
imageId: data.imageId || null,
|
imageId: data.imageId || null,
|
||||||
iconUrl: data.iconUrl || "",
|
iconUrl: data.iconUrl || "",
|
||||||
icon: data.icon || null,
|
icon: data.icon || null,
|
||||||
|
|
||||||
};
|
};
|
||||||
return data;
|
return data;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
toast.error(res.data?.message || "Gagal mengambil data media sosial");
|
||||||
result?.message || "Gagal mengambil data media sosial"
|
return null;
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error((error as Error).message);
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.error("Error loading media sosial:", error);
|
||||||
|
}
|
||||||
toast.error("Terjadi kesalahan saat mengambil data media sosial");
|
toast.error("Terjadi kesalahan saat mengambil data media sosial");
|
||||||
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
mediaSosial.update.loading = false; // ✅ Supaya berhenti loading walau error
|
mediaSosial.update.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -655,41 +637,25 @@ const mediaSosial = proxy({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
mediaSosial.update.loading = true;
|
mediaSosial.update.loading = true;
|
||||||
|
const res = await (ApiFetch.api.landingpage.mediasosial as any)[this.id].put({
|
||||||
|
name: this.form.name,
|
||||||
|
imageId: this.form.imageId,
|
||||||
|
iconUrl: this.form.iconUrl,
|
||||||
|
icon: this.form.icon,
|
||||||
|
});
|
||||||
|
|
||||||
const response = await fetch(
|
if (res.data?.success) {
|
||||||
`/api/landingpage/mediasosial/${this.id}`,
|
|
||||||
{
|
|
||||||
method: "PUT",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
name: this.form.name,
|
|
||||||
imageId: this.form.imageId,
|
|
||||||
iconUrl: this.form.iconUrl,
|
|
||||||
icon: this.form.icon,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorData = await response.json().catch(() => ({}));
|
|
||||||
throw new Error(
|
|
||||||
errorData.message || `HTTP error! status: ${response.status}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
toast.success("Berhasil update media sosial");
|
toast.success("Berhasil update media sosial");
|
||||||
await mediaSosial.findMany.load(); // refresh list
|
await mediaSosial.findMany.load();
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(result.message || "Gagal update media sosial");
|
toast.error(res.data?.message || "Gagal update media sosial");
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating media sosial:", error);
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.error("Error updating media sosial:", error);
|
||||||
|
}
|
||||||
toast.error(
|
toast.error(
|
||||||
error instanceof Error
|
error instanceof Error
|
||||||
? error.message
|
? error.message
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
|
|||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
import DOMPurify from 'dompurify';
|
||||||
|
|
||||||
function DetailProgramInovasi() {
|
function DetailProgramInovasi() {
|
||||||
const stateProgramInovasi = useProxy(profileLandingPageState.programInovasi)
|
const stateProgramInovasi = useProxy(profileLandingPageState.programInovasi)
|
||||||
@@ -85,7 +86,7 @@ function DetailProgramInovasi() {
|
|||||||
<Box>
|
<Box>
|
||||||
<Text fz="lg" fw="bold">Deskripsi</Text>
|
<Text fz="lg" fw="bold">Deskripsi</Text>
|
||||||
<Box pl={5}>
|
<Box pl={5}>
|
||||||
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data.description || '-' }}></Text>
|
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(data.description || '-') }}></Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
|
|||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
import DOMPurify from 'dompurify';
|
||||||
import HeaderSearch from '../../../_com/header';
|
import HeaderSearch from '../../../_com/header';
|
||||||
import profileLandingPageState from '../../../_state/landing-page/profile';
|
import profileLandingPageState from '../../../_state/landing-page/profile';
|
||||||
|
|
||||||
@@ -90,7 +91,7 @@ function ListProgramInovasi({ search }: { search: string }) {
|
|||||||
<Text fw={500}>{item.name}</Text>
|
<Text fw={500}>{item.name}</Text>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd style={{ maxWidth: 250 }}>
|
<TableTd style={{ maxWidth: 250 }}>
|
||||||
<Text fz="sm" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.description || '-' }}></Text>
|
<Text fz="sm" lineClamp={1} dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(item.description || '-') }}></Text>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd style={{ maxWidth: 250 }}>
|
<TableTd style={{ maxWidth: 250 }}>
|
||||||
<Tooltip label="Buka tautan program" position="top" withArrow>
|
<Tooltip label="Buka tautan program" position="top" withArrow>
|
||||||
@@ -144,7 +145,7 @@ function ListProgramInovasi({ search }: { search: string }) {
|
|||||||
{/* Description */}
|
{/* Description */}
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz="sm" fw={600} lh={1.4}>Deskripsi</Text>
|
<Text fz="sm" fw={600} lh={1.4}>Deskripsi</Text>
|
||||||
<Text dangerouslySetInnerHTML={{ __html: item.description || '-' }} fz="sm" c="gray.7" lineClamp={2} />
|
<Text dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(item.description || '-') }} fz="sm" c="gray.7" lineClamp={2} />
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Link */}
|
{/* Link */}
|
||||||
|
|||||||
Reference in New Issue
Block a user