Commit 8d514056 authored by Lê Bảo Hồng Đức's avatar Lê Bảo Hồng Đức

fix

parent cff4d2fb
...@@ -116,6 +116,9 @@ importers: ...@@ -116,6 +116,9 @@ importers:
html-react-parser: html-react-parser:
specifier: ^5.2.7 specifier: ^5.2.7
version: 5.2.7(@types/react@19.2.2)(react@19.2.0) version: 5.2.7(@types/react@19.2.2)(react@19.2.0)
jodit-react:
specifier: ^5.3.21
version: 5.3.21(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
lodash-es: lodash-es:
specifier: ^4.17.21 specifier: ^4.17.21
version: 4.17.21 version: 4.17.21
...@@ -595,78 +598,92 @@ packages: ...@@ -595,78 +598,92 @@ packages:
resolution: {integrity: sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ==} resolution: {integrity: sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-arm@1.2.3': '@img/sharp-libvips-linux-arm@1.2.3':
resolution: {integrity: sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA==} resolution: {integrity: sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA==}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-ppc64@1.2.3': '@img/sharp-libvips-linux-ppc64@1.2.3':
resolution: {integrity: sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg==} resolution: {integrity: sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg==}
cpu: [ppc64] cpu: [ppc64]
os: [linux] os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-s390x@1.2.3': '@img/sharp-libvips-linux-s390x@1.2.3':
resolution: {integrity: sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w==} resolution: {integrity: sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w==}
cpu: [s390x] cpu: [s390x]
os: [linux] os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-x64@1.2.3': '@img/sharp-libvips-linux-x64@1.2.3':
resolution: {integrity: sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg==} resolution: {integrity: sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@img/sharp-libvips-linuxmusl-arm64@1.2.3': '@img/sharp-libvips-linuxmusl-arm64@1.2.3':
resolution: {integrity: sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw==} resolution: {integrity: sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@img/sharp-libvips-linuxmusl-x64@1.2.3': '@img/sharp-libvips-linuxmusl-x64@1.2.3':
resolution: {integrity: sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g==} resolution: {integrity: sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@img/sharp-linux-arm64@0.34.4': '@img/sharp-linux-arm64@0.34.4':
resolution: {integrity: sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ==} resolution: {integrity: sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@img/sharp-linux-arm@0.34.4': '@img/sharp-linux-arm@0.34.4':
resolution: {integrity: sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA==} resolution: {integrity: sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
libc: [glibc]
'@img/sharp-linux-ppc64@0.34.4': '@img/sharp-linux-ppc64@0.34.4':
resolution: {integrity: sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ==} resolution: {integrity: sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ppc64] cpu: [ppc64]
os: [linux] os: [linux]
libc: [glibc]
'@img/sharp-linux-s390x@0.34.4': '@img/sharp-linux-s390x@0.34.4':
resolution: {integrity: sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw==} resolution: {integrity: sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x] cpu: [s390x]
os: [linux] os: [linux]
libc: [glibc]
'@img/sharp-linux-x64@0.34.4': '@img/sharp-linux-x64@0.34.4':
resolution: {integrity: sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A==} resolution: {integrity: sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@img/sharp-linuxmusl-arm64@0.34.4': '@img/sharp-linuxmusl-arm64@0.34.4':
resolution: {integrity: sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA==} resolution: {integrity: sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@img/sharp-linuxmusl-x64@0.34.4': '@img/sharp-linuxmusl-x64@0.34.4':
resolution: {integrity: sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg==} resolution: {integrity: sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@img/sharp-wasm32@0.34.4': '@img/sharp-wasm32@0.34.4':
resolution: {integrity: sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA==} resolution: {integrity: sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA==}
...@@ -754,24 +771,28 @@ packages: ...@@ -754,24 +771,28 @@ packages:
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@next/swc-linux-arm64-musl@16.0.10': '@next/swc-linux-arm64-musl@16.0.10':
resolution: {integrity: sha512-llA+hiDTrYvyWI21Z0L1GiXwjQaanPVQQwru5peOgtooeJ8qx3tlqRV2P7uH2pKQaUfHxI/WVarvI5oYgGxaTw==} resolution: {integrity: sha512-llA+hiDTrYvyWI21Z0L1GiXwjQaanPVQQwru5peOgtooeJ8qx3tlqRV2P7uH2pKQaUfHxI/WVarvI5oYgGxaTw==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@next/swc-linux-x64-gnu@16.0.10': '@next/swc-linux-x64-gnu@16.0.10':
resolution: {integrity: sha512-AK2q5H0+a9nsXbeZ3FZdMtbtu9jxW4R/NgzZ6+lrTm3d6Zb7jYrWcgjcpM1k8uuqlSy4xIyPR2YiuUr+wXsavA==} resolution: {integrity: sha512-AK2q5H0+a9nsXbeZ3FZdMtbtu9jxW4R/NgzZ6+lrTm3d6Zb7jYrWcgjcpM1k8uuqlSy4xIyPR2YiuUr+wXsavA==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@next/swc-linux-x64-musl@16.0.10': '@next/swc-linux-x64-musl@16.0.10':
resolution: {integrity: sha512-1TDG9PDKivNw5550S111gsO4RGennLVl9cipPhtkXIFVwo31YZ73nEbLjNC8qG3SgTz/QZyYyaFYMeY4BKZR/g==} resolution: {integrity: sha512-1TDG9PDKivNw5550S111gsO4RGennLVl9cipPhtkXIFVwo31YZ73nEbLjNC8qG3SgTz/QZyYyaFYMeY4BKZR/g==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@next/swc-win32-arm64-msvc@16.0.10': '@next/swc-win32-arm64-msvc@16.0.10':
resolution: {integrity: sha512-aEZIS4Hh32xdJQbHz121pyuVZniSNoqDVx1yIr2hy+ZwJGipeqnMZBJHyMxv2tiuAXGx6/xpTcQJ6btIiBjgmg==} resolution: {integrity: sha512-aEZIS4Hh32xdJQbHz121pyuVZniSNoqDVx1yIr2hy+ZwJGipeqnMZBJHyMxv2tiuAXGx6/xpTcQJ6btIiBjgmg==}
...@@ -1585,24 +1606,28 @@ packages: ...@@ -1585,24 +1606,28 @@ packages:
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-arm64-musl@4.1.16': '@tailwindcss/oxide-linux-arm64-musl@4.1.16':
resolution: {integrity: sha512-H81UXMa9hJhWhaAUca6bU2wm5RRFpuHImrwXBUvPbYb+3jo32I9VIwpOX6hms0fPmA6f2pGVlybO6qU8pF4fzQ==} resolution: {integrity: sha512-H81UXMa9hJhWhaAUca6bU2wm5RRFpuHImrwXBUvPbYb+3jo32I9VIwpOX6hms0fPmA6f2pGVlybO6qU8pF4fzQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@tailwindcss/oxide-linux-x64-gnu@4.1.16': '@tailwindcss/oxide-linux-x64-gnu@4.1.16':
resolution: {integrity: sha512-ZGHQxDtFC2/ruo7t99Qo2TTIvOERULPl5l0K1g0oK6b5PGqjYMga+FcY1wIUnrUxY56h28FxybtDEla+ICOyew==} resolution: {integrity: sha512-ZGHQxDtFC2/ruo7t99Qo2TTIvOERULPl5l0K1g0oK6b5PGqjYMga+FcY1wIUnrUxY56h28FxybtDEla+ICOyew==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-x64-musl@4.1.16': '@tailwindcss/oxide-linux-x64-musl@4.1.16':
resolution: {integrity: sha512-Oi1tAaa0rcKf1Og9MzKeINZzMLPbhxvm7rno5/zuP1WYmpiG0bEHq4AcRUiG2165/WUzvxkW4XDYCscZWbTLZw==} resolution: {integrity: sha512-Oi1tAaa0rcKf1Og9MzKeINZzMLPbhxvm7rno5/zuP1WYmpiG0bEHq4AcRUiG2165/WUzvxkW4XDYCscZWbTLZw==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@tailwindcss/oxide-wasm32-wasi@4.1.16': '@tailwindcss/oxide-wasm32-wasi@4.1.16':
resolution: {integrity: sha512-B01u/b8LteGRwucIBmCQ07FVXLzImWESAIMcUU6nvFt/tYsQ6IHz8DmZ5KtvmwxD+iTYBtM1xwoGXswnlu9v0Q==} resolution: {integrity: sha512-B01u/b8LteGRwucIBmCQ07FVXLzImWESAIMcUU6nvFt/tYsQ6IHz8DmZ5KtvmwxD+iTYBtM1xwoGXswnlu9v0Q==}
...@@ -1795,41 +1820,49 @@ packages: ...@@ -1795,41 +1820,49 @@ packages:
resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-arm64-musl@1.11.1': '@unrs/resolver-binding-linux-arm64-musl@1.11.1':
resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1':
resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==}
cpu: [ppc64] cpu: [ppc64]
os: [linux] os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1':
resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-riscv64-musl@1.11.1': '@unrs/resolver-binding-linux-riscv64-musl@1.11.1':
resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
libc: [musl]
'@unrs/resolver-binding-linux-s390x-gnu@1.11.1': '@unrs/resolver-binding-linux-s390x-gnu@1.11.1':
resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==}
cpu: [s390x] cpu: [s390x]
os: [linux] os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-x64-gnu@1.11.1': '@unrs/resolver-binding-linux-x64-gnu@1.11.1':
resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-x64-musl@1.11.1': '@unrs/resolver-binding-linux-x64-musl@1.11.1':
resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@unrs/resolver-binding-wasm32-wasi@1.11.1': '@unrs/resolver-binding-wasm32-wasi@1.11.1':
resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==}
...@@ -2781,6 +2814,15 @@ packages: ...@@ -2781,6 +2814,15 @@ packages:
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
hasBin: true hasBin: true
jodit-react@5.3.21:
resolution: {integrity: sha512-dSFVKkrDVbhVwKDjuFMJ3HhPdqeEz/Yz5MhJf9v9B3Gg29CnelKCZ00h6MxXzlhglF3qvtvUTc2HSKrSB15khw==}
peerDependencies:
react: ~0.14 || ^15 || ^16 || ^17 || ^18 || ^19
react-dom: ~0.14 || ^15 || ^16 || ^17 || ^18 || ^19
jodit@4.12.2:
resolution: {integrity: sha512-SoZAH2YvL8JxPmL4muQJPbbF27rFKVzFQOiCRabjtSQcLVghm+XpIm5t9dXq+fCA4d1Z2O+8x/sORPVdLI4zbg==}
js-tokens@4.0.0: js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
...@@ -2893,24 +2935,28 @@ packages: ...@@ -2893,24 +2935,28 @@ packages:
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
lightningcss-linux-arm64-musl@1.30.2: lightningcss-linux-arm64-musl@1.30.2:
resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
lightningcss-linux-x64-gnu@1.30.2: lightningcss-linux-x64-gnu@1.30.2:
resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
lightningcss-linux-x64-musl@1.30.2: lightningcss-linux-x64-musl@1.30.2:
resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
lightningcss-win32-arm64-msvc@1.30.2: lightningcss-win32-arm64-msvc@1.30.2:
resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==}
...@@ -6772,6 +6818,14 @@ snapshots: ...@@ -6772,6 +6818,14 @@ snapshots:
jiti@2.6.1: {} jiti@2.6.1: {}
jodit-react@5.3.21(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
dependencies:
jodit: 4.12.2
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
jodit@4.12.2: {}
js-tokens@4.0.0: {} js-tokens@4.0.0: {}
js-yaml@4.1.0: js-yaml@4.1.0:
......
"use client"; "use client";
import * as React from "react"; import * as React from "react";
import { Checkbox } from "@/components/ui/checkbox";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Dialog, Dialog,
...@@ -22,9 +21,8 @@ import { ...@@ -22,9 +21,8 @@ import {
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { import {
headerArticleCategoryOptions, type HeaderCategoryTreeItem,
HeaderCategoryTreeItem, type HeaderCategoryType,
HeaderCategoryType,
toSlug, toSlug,
} from "@/mockdata/header-config"; } from "@/mockdata/header-config";
...@@ -38,7 +36,7 @@ export interface HeaderCategoryFormValues { ...@@ -38,7 +36,7 @@ export interface HeaderCategoryFormValues {
parent_id: string; parent_id: string;
type: HeaderCategoryType; type: HeaderCategoryType;
description: string; description: string;
category_ids: string[]; tagsearch: string;
} }
interface HeaderCategoryFormDialogProps { interface HeaderCategoryFormDialogProps {
...@@ -56,7 +54,6 @@ const TYPE_OPTIONS: Array<{ value: HeaderCategoryType; label: string }> = [ ...@@ -56,7 +54,6 @@ const TYPE_OPTIONS: Array<{ value: HeaderCategoryType; label: string }> = [
{ value: "category", label: "Danh mục" }, { value: "category", label: "Danh mục" },
{ value: "page", label: "Bài viết trang" }, { value: "page", label: "Bài viết trang" },
{ value: "news", label: "Tin tức" }, { value: "news", label: "Tin tức" },
{ value: "image", label: "Ảnh" },
]; ];
const fieldClassName = const fieldClassName =
...@@ -67,7 +64,8 @@ const selectTriggerClassName = ...@@ -67,7 +64,8 @@ const selectTriggerClassName =
const selectContentClassName = "border-[#063e8e]/15 bg-white text-gray-700"; const selectContentClassName = "border-[#063e8e]/15 bg-white text-gray-700";
const selectItemClassName = "text-gray-700 focus:bg-[#063e8e]/10 focus:text-[#063e8e]"; const selectItemClassName =
"text-gray-700 focus:bg-[#063e8e]/10 focus:text-[#063e8e]";
export function HeaderCategoryFormDialog({ export function HeaderCategoryFormDialog({
mode, mode,
...@@ -99,6 +97,11 @@ export function HeaderCategoryFormDialog({ ...@@ -99,6 +97,11 @@ export function HeaderCategoryFormDialog({
})); }));
}; };
const searchTags = values.tagsearch
.split(",")
.map((item) => item.trim())
.filter(Boolean);
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-h-[90vh] max-w-3xl overflow-y-auto border-[#063e8e]/15 bg-white text-gray-700 shadow-xl"> <DialogContent className="max-h-[90vh] max-w-3xl overflow-y-auto border-[#063e8e]/15 bg-white text-gray-700 shadow-xl">
...@@ -111,7 +114,7 @@ export function HeaderCategoryFormDialog({ ...@@ -111,7 +114,7 @@ export function HeaderCategoryFormDialog({
<div className="grid grid-cols-1 gap-4 py-2 md:grid-cols-2"> <div className="grid grid-cols-1 gap-4 py-2 md:grid-cols-2">
<div> <div>
<Label className="mb-1.5 block text-gray-700">Tên danh mục *</Label> <Label className="mb-1.5 block text-gray-700">Tên danh mục <span className="text-red-600">*</span></Label>
<Input <Input
value={values.name} value={values.name}
onChange={(event) => handleNameChange(event.target.value)} onChange={(event) => handleNameChange(event.target.value)}
...@@ -121,7 +124,7 @@ export function HeaderCategoryFormDialog({ ...@@ -121,7 +124,7 @@ export function HeaderCategoryFormDialog({
</div> </div>
<div> <div>
<Label className="mb-1.5 block text-gray-700">Thể loại *</Label> <Label className="mb-1.5 block text-gray-700">Thể loại <span className="text-red-600">*</span></Label>
<Select <Select
value={values.type} value={values.type}
onValueChange={(value) => onValueChange={(value) =>
...@@ -182,7 +185,7 @@ export function HeaderCategoryFormDialog({ ...@@ -182,7 +185,7 @@ export function HeaderCategoryFormDialog({
</div> </div>
<div> <div>
<Label className="mb-1.5 block text-gray-700">Thứ tự</Label> <Label className="mb-1.5 block text-gray-700">Thứ tự <span className="text-red-600">*</span></Label>
<Input <Input
type="number" type="number"
min="0" min="0"
...@@ -194,7 +197,7 @@ export function HeaderCategoryFormDialog({ ...@@ -194,7 +197,7 @@ export function HeaderCategoryFormDialog({
</div> </div>
<div> <div>
<Label className="mb-1.5 block text-gray-700">Slug</Label> <Label className="mb-1.5 block text-gray-700">Slug <span className="text-red-600">*</span></Label>
<Input <Input
value={values.slug} value={values.slug}
onChange={(event) => setField("slug", event.target.value)} onChange={(event) => setField("slug", event.target.value)}
...@@ -214,33 +217,28 @@ export function HeaderCategoryFormDialog({ ...@@ -214,33 +217,28 @@ export function HeaderCategoryFormDialog({
/> />
</div> </div>
{values.type === "news" ? ( {mode === "edit" && values.type === "news" ? (
<div className="md:col-span-2"> <div className="md:col-span-2">
<Label className="mb-1.5 block text-gray-700">Thể loại bài viết</Label> <Label className="mb-1.5 block text-gray-700">Tag tìm kiếm</Label>
<div className="grid grid-cols-1 gap-2 rounded-lg border border-[#063e8e]/15 p-4 md:grid-cols-2"> <Textarea
{headerArticleCategoryOptions.map((category) => ( rows={3}
<label value={values.tagsearch}
key={category.id} onChange={(event) => setField("tagsearch", event.target.value)}
className="flex items-center gap-3 rounded-md border border-[#063e8e]/10 px-3 py-2" placeholder="Nhập tag tìm kiếm, ngăn cách bằng dấu phẩy"
> className={fieldClassName}
<Checkbox
checked={values.category_ids.includes(category.id)}
onCheckedChange={(checked) => {
if (checked) {
setField("category_ids", [...values.category_ids, category.id]);
return;
}
setField(
"category_ids",
values.category_ids.filter((id) => id !== category.id),
);
}}
/> />
<span className="text-sm text-gray-700">{category.name}</span> {searchTags.length > 0 ? (
</label> <div className="mt-3 flex flex-wrap gap-2">
{searchTags.map((item) => (
<span
key={item}
className="inline-flex items-center rounded-full border border-[#063e8e]/15 bg-[#063e8e]/[0.04] px-3 py-1 text-sm text-gray-700"
>
{item}
</span>
))} ))}
</div> </div>
) : null}
</div> </div>
) : null} ) : null}
</div> </div>
...@@ -253,7 +251,10 @@ export function HeaderCategoryFormDialog({ ...@@ -253,7 +251,10 @@ export function HeaderCategoryFormDialog({
> >
Hủy Hủy
</Button> </Button>
<Button className="bg-[#063e8e] text-white hover:bg-[#063e8e]/90" onClick={onSubmit}> <Button
className="bg-[#063e8e] text-white hover:bg-[#063e8e]/90"
onClick={onSubmit}
>
{mode === "create" ? "Lưu danh mục" : "Cập nhật danh mục"} {mode === "create" ? "Lưu danh mục" : "Cập nhật danh mục"}
</Button> </Button>
</DialogFooter> </DialogFooter>
......
...@@ -7,7 +7,6 @@ import { ...@@ -7,7 +7,6 @@ import {
ChevronRight, ChevronRight,
Edit, Edit,
ExternalLink, ExternalLink,
FileImage,
FileText, FileText,
FolderTree, FolderTree,
MoreHorizontal, MoreHorizontal,
...@@ -33,7 +32,10 @@ import { ...@@ -33,7 +32,10 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "@/components/ui/table"; } from "@/components/ui/table";
import { HeaderCategoryTreeItem, getHeaderCategoryTypeLabel } from "@/mockdata/header-config"; import {
type HeaderCategoryTreeItem,
getHeaderCategoryTypeLabel,
} from "@/mockdata/header-config";
export type HeaderCategoryFlatRow = HeaderCategoryTreeItem & { export type HeaderCategoryFlatRow = HeaderCategoryTreeItem & {
depth: number; depth: number;
...@@ -69,9 +71,8 @@ function getDisplaySortOrder(item: HeaderCategoryFlatRow, rows: HeaderCategoryFl ...@@ -69,9 +71,8 @@ function getDisplaySortOrder(item: HeaderCategoryFlatRow, rows: HeaderCategoryFl
function getTypeIcon(type: HeaderCategoryTreeItem["type"]) { function getTypeIcon(type: HeaderCategoryTreeItem["type"]) {
switch (type) { switch (type) {
case "news": case "news":
case "page":
return <FileText className="h-4 w-4 text-[#063e8e]" />; return <FileText className="h-4 w-4 text-[#063e8e]" />;
case "image":
return <FileImage className="h-4 w-4 text-[#063e8e]" />;
default: default:
return <FolderTree className="h-4 w-4 text-[#063e8e]" />; return <FolderTree className="h-4 w-4 text-[#063e8e]" />;
} }
...@@ -160,9 +161,7 @@ export function HeaderCategoryTable({ ...@@ -160,9 +161,7 @@ export function HeaderCategoryTable({
const hasChildren = rows.some((entry) => entry.parentId === item.id); const hasChildren = rows.some((entry) => entry.parentId === item.id);
const isExpanded = expanded[item.id] ?? true; const isExpanded = expanded[item.id] ?? true;
const canCreateChild = !item.parent_id && item.type === "category"; const canCreateChild = !item.parent_id && item.type === "category";
const canManagePosts = const canManagePosts = item.type === "page" || item.type === "news";
item.type === "page" || item.type === "news" || item.type === "image";
const createContentLabel = item.type === "image" ? "Thêm ảnh" : "Thêm bài viết";
return ( return (
<TableRow <TableRow
...@@ -191,11 +190,13 @@ export function HeaderCategoryTable({ ...@@ -191,11 +190,13 @@ export function HeaderCategoryTable({
<div className="truncate font-medium text-black">{item.name}</div> <div className="truncate font-medium text-black">{item.name}</div>
</div> </div>
</TableCell> </TableCell>
<TableCell className="w-[180px] text-center"> <TableCell className="w-[180px] text-center">
<Badge variant="outline" className="border-[#063e8e]/25 text-[#063e8e]"> <Badge variant="outline" className="border-[#063e8e]/25 text-[#063e8e]">
{getHeaderCategoryTypeLabel(item.type)} {getHeaderCategoryTypeLabel(item.type)}
</Badge> </Badge>
</TableCell> </TableCell>
<TableCell className="w-[140px] text-center font-medium text-black"> <TableCell className="w-[140px] text-center font-medium text-black">
<span <span
className={ className={
...@@ -207,6 +208,7 @@ export function HeaderCategoryTable({ ...@@ -207,6 +208,7 @@ export function HeaderCategoryTable({
{getDisplaySortOrder(item, rows)} {getDisplaySortOrder(item, rows)}
</span> </span>
</TableCell> </TableCell>
<TableCell className="w-[280px] text-sm text-gray-700"> <TableCell className="w-[280px] text-sm text-gray-700">
<div className="mx-auto flex max-w-[220px] items-center justify-center gap-2"> <div className="mx-auto flex max-w-[220px] items-center justify-center gap-2">
<span className="block max-w-[180px] truncate"> <span className="block max-w-[180px] truncate">
...@@ -217,6 +219,7 @@ export function HeaderCategoryTable({ ...@@ -217,6 +219,7 @@ export function HeaderCategoryTable({
) : null} ) : null}
</div> </div>
</TableCell> </TableCell>
<TableCell className="w-[120px] text-center"> <TableCell className="w-[120px] text-center">
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
...@@ -241,9 +244,9 @@ export function HeaderCategoryTable({ ...@@ -241,9 +244,9 @@ export function HeaderCategoryTable({
asChild asChild
className="text-gray-700 focus:text-[#063e8e]" className="text-gray-700 focus:text-[#063e8e]"
> >
<Link href={`/admin/header-config/${item.id}/posts/new`}> <Link href={`/admin/header-config/${item.id}/posts`}>
<Plus className="mr-2 h-4 w-4" /> <FileText className="mr-2 h-4 w-4" />
{createContentLabel} Quản lý bài viết
</Link> </Link>
</DropdownMenuItem> </DropdownMenuItem>
) : null} ) : null}
......
...@@ -29,7 +29,7 @@ const EMPTY_HEADER_CATEGORY_FORM: HeaderCategoryFormValues = { ...@@ -29,7 +29,7 @@ const EMPTY_HEADER_CATEGORY_FORM: HeaderCategoryFormValues = {
parent_id: '', parent_id: '',
type: 'page', type: 'page',
description: '', description: '',
category_ids: [], tagsearch: '',
}; };
function toFormValues(item?: HeaderCategoryItem | null): HeaderCategoryFormValues { function toFormValues(item?: HeaderCategoryItem | null): HeaderCategoryFormValues {
...@@ -43,10 +43,26 @@ function toFormValues(item?: HeaderCategoryItem | null): HeaderCategoryFormValue ...@@ -43,10 +43,26 @@ function toFormValues(item?: HeaderCategoryItem | null): HeaderCategoryFormValue
parent_id: item.parent_id ?? '', parent_id: item.parent_id ?? '',
type: item.type, type: item.type,
description: item.description ?? '', description: item.description ?? '',
category_ids: item.category_ids ?? [], tagsearch: (item.tagsearch_values ?? []).join(', '),
}; };
} }
function parseTagsearch(value: string) {
const seen = new Set<string>();
return value
.split(',')
.map((item) => item.trim())
.filter((item) => {
if (!item) return false;
const key = item.toLowerCase();
if (seen.has(key)) return false;
seen.add(key);
return true;
});
}
function getInitialHeaderConfig() { function getInitialHeaderConfig() {
if (typeof window === 'undefined') { if (typeof window === 'undefined') {
return getHeaderCategorySeed(); return getHeaderCategorySeed();
...@@ -126,7 +142,8 @@ function useHeaderConfigModule() { ...@@ -126,7 +142,8 @@ function useHeaderConfigModule() {
is_article: values.type === 'news', is_article: values.type === 'news',
parent_id: values.parent_id || null, parent_id: values.parent_id || null,
level: 1, level: 1,
category_ids: values.type === 'news' ? values.category_ids : [], category_ids: [],
tagsearch_values: values.type === 'news' ? parseTagsearch(values.tagsearch) : [],
description: values.description.trim(), description: values.description.trim(),
created_at: new Date().toISOString(), created_at: new Date().toISOString(),
updated_at: new Date().toISOString(), updated_at: new Date().toISOString(),
...@@ -150,7 +167,8 @@ function useHeaderConfigModule() { ...@@ -150,7 +167,8 @@ function useHeaderConfigModule() {
type: values.type, type: values.type,
is_article: values.type === 'news', is_article: values.type === 'news',
parent_id: values.parent_id || null, parent_id: values.parent_id || null,
category_ids: values.type === 'news' ? values.category_ids : [], category_ids: [],
tagsearch_values: values.type === 'news' ? parseTagsearch(values.tagsearch) : [],
description: values.description.trim(), description: values.description.trim(),
updated_at: new Date().toISOString(), updated_at: new Date().toISOString(),
}); });
......
This diff is collapsed.
This diff is collapsed.
...@@ -12,6 +12,7 @@ interface AdminTableLayoutProps { ...@@ -12,6 +12,7 @@ interface AdminTableLayoutProps {
actionIcon?: React.ReactNode; actionIcon?: React.ReactNode;
actionDisabled?: boolean; actionDisabled?: boolean;
children: React.ReactNode; children: React.ReactNode;
filters?: React.ReactNode;
onSearchChange: (value: string) => void; onSearchChange: (value: string) => void;
onActionClick?: () => void; onActionClick?: () => void;
} }
...@@ -23,18 +24,22 @@ export function AdminTableLayout({ ...@@ -23,18 +24,22 @@ export function AdminTableLayout({
actionIcon, actionIcon,
actionDisabled = false, actionDisabled = false,
children, children,
filters,
onSearchChange, onSearchChange,
onActionClick, onActionClick,
}: AdminTableLayoutProps) { }: AdminTableLayoutProps) {
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="flex flex-1 flex-col gap-3 lg:flex-row lg:items-center">
<Input <Input
value={searchValue} value={searchValue}
placeholder={searchPlaceholder} placeholder={searchPlaceholder}
onChange={(event) => onSearchChange(event.target.value)} onChange={(event) => onSearchChange(event.target.value)}
className="max-w-sm border-[#063e8e]/15 bg-white text-gray-700 placeholder:text-gray-700" className="max-w-sm border-[#063e8e]/15 bg-white text-gray-700 placeholder:text-gray-700"
/> />
{filters}
</div>
{actionLabel ? ( {actionLabel ? (
<Button <Button
......
"use client";
import * as React from "react";
import { ImagePlus, Search, Upload, X } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { SafeNextImage } from "@/components/admin/safe-next-image";
import {
AdminMediaItem,
createAdminMediaId,
persistAdminMediaItems,
readAdminMediaItems,
} from "@/mockdata/admin-news";
import { cn } from "@/lib/utils";
interface AdminImagePickerProps {
open: boolean;
selectedId?: string | null;
onOpenChange: (open: boolean) => void;
onSelect: (item: AdminMediaItem) => void;
}
function formatFileSize(size: number) {
if (!size) return "Ảnh hệ thống";
if (size < 1024) return `${size} B`;
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`;
return `${(size / (1024 * 1024)).toFixed(1)} MB`;
}
export function AdminImagePicker({
open,
selectedId,
onOpenChange,
onSelect,
}: AdminImagePickerProps) {
const inputRef = React.useRef<HTMLInputElement | null>(null);
const [search, setSearch] = React.useState("");
const [items, setItems] = React.useState<AdminMediaItem[]>([]);
React.useEffect(() => {
if (!open) return;
setItems(readAdminMediaItems());
}, [open]);
const visibleItems = React.useMemo(() => {
const keyword = search.trim().toLowerCase();
if (!keyword) return items;
return items.filter((item) => {
return (
item.name.toLowerCase().includes(keyword) ||
item.alt.toLowerCase().includes(keyword) ||
item.url.toLowerCase().includes(keyword)
);
});
}, [items, search]);
const handleUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = () => {
const nextItem: AdminMediaItem = {
id: createAdminMediaId(),
name: file.name,
alt: file.name.replace(/\.[^.]+$/, ""),
url: typeof reader.result === "string" ? reader.result : "/img-error.png",
mime: file.type || "image/*",
size: file.size,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
source: "upload",
};
const nextItems = [nextItem, ...items];
setItems(nextItems);
persistAdminMediaItems(nextItems);
onSelect(nextItem);
onOpenChange(false);
};
reader.readAsDataURL(file);
event.target.value = "";
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-h-[88vh] max-w-5xl overflow-hidden border-[#063e8e]/15 bg-white p-0">
<DialogHeader className="border-b border-[#063e8e]/10 px-6 py-5">
<div className="flex items-start justify-between gap-4">
<div>
<DialogTitle className="text-xl font-semibold text-black">
Thư viện hình ảnh
</DialogTitle>
<DialogDescription className="mt-1 text-sm text-gray-700">
Chọn ảnh có sẵn hoặc tải thêm ảnh mới cho bài viết.
</DialogDescription>
</div>
</div>
</DialogHeader>
<div className="flex flex-col gap-4 border-b border-[#063e8e]/10 px-6 py-4 lg:flex-row lg:items-center lg:justify-between">
<div className="relative w-full lg:max-w-sm">
<Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-700" />
<Input
value={search}
onChange={(event) => setSearch(event.target.value)}
placeholder="Tìm kiếm hình ảnh..."
className="border-[#063e8e]/15 bg-white pl-9 text-gray-700 placeholder:text-gray-700"
/>
</div>
<div className="flex items-center gap-3">
<input
ref={inputRef}
type="file"
accept="image/*"
className="hidden"
onChange={handleUpload}
/>
<Button
type="button"
onClick={() => inputRef.current?.click()}
className="bg-[#063e8e] text-white hover:bg-[#063e8e]/90"
>
<Upload className="mr-2 h-4 w-4" />
Tải hình ảnh
</Button>
</div>
</div>
<div className="max-h-[60vh] overflow-y-auto px-6 py-6">
{visibleItems.length === 0 ? (
<div className="flex min-h-[240px] flex-col items-center justify-center rounded-2xl border border-dashed border-[#063e8e]/15 bg-[#063e8e]/[0.03] px-6 text-center">
<ImagePlus className="mb-3 h-10 w-10 text-[#063e8e]" />
<p className="text-base font-medium text-black">Chưa có hình ảnh phù hợp</p>
<p className="mt-1 text-sm text-gray-700">
Hãy thử từ khóa khác hoặc tải thêm hình ảnh vào thư viện.
</p>
</div>
) : (
<div className="grid grid-cols-2 gap-4 md:grid-cols-3 xl:grid-cols-4">
{visibleItems.map((item) => (
<button
key={item.id}
type="button"
onClick={() => {
onSelect(item);
onOpenChange(false);
}}
className={cn(
"group overflow-hidden rounded-2xl border bg-white text-left transition-all",
item.id === selectedId
? "border-[#063e8e] shadow-[0_0_0_2px_rgba(6,62,142,0.12)]"
: "border-[#063e8e]/10 hover:border-[#063e8e]/40 hover:shadow-sm",
)}
>
<div className="relative aspect-[4/3] overflow-hidden bg-[#063e8e]/[0.04]">
<SafeNextImage
src={item.url}
alt={item.alt || item.name}
fill
className="object-cover transition duration-300 group-hover:scale-[1.02]"
/>
{item.id === selectedId ? (
<div className="absolute right-3 top-3 rounded-full bg-[#063e8e] px-2 py-1 text-xs font-medium text-white">
Đã chọn
</div>
) : null}
</div>
<div className="space-y-1 px-4 py-3">
<p className="line-clamp-1 text-sm font-medium text-black">{item.name}</p>
<div className="flex items-center justify-between gap-2 text-xs text-gray-700">
<span>{formatFileSize(item.size)}</span>
<span>{item.source === "upload" ? "Tải lên" : "Hệ thống"}</span>
</div>
</div>
</button>
))}
</div>
)}
</div>
<div className="flex justify-end border-t border-[#063e8e]/10 px-6 py-4">
<Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
className="border-[#063e8e]/15 text-gray-700 hover:bg-[#063e8e]/[0.04]"
>
<X className="mr-2 h-4 w-4" />
Đóng
</Button>
</div>
</DialogContent>
</Dialog>
);
}
This diff is collapsed.
This diff is collapsed.
"use client";
import dynamic from "next/dynamic";
import { useMemo, useRef } from "react";
import type { JoditEditorProps } from "jodit-react";
const JoditEditor = dynamic(() => import("jodit-react"), {
ssr: false,
loading: () => (
<div className="flex min-h-[260px] items-center justify-center rounded-2xl border border-[#063e8e]/15 bg-white text-sm text-gray-500">
Đang tải trình soạn thảo...
</div>
),
});
interface AdminRichTextEditorProps {
value: string;
onChange: (value: string) => void;
placeholder?: string;
className?: string;
minHeight?: number;
readOnly?: boolean;
}
export function AdminRichTextEditor({
value,
onChange,
placeholder = "Nhập nội dung...",
className = "",
minHeight = 260,
readOnly = false,
}: AdminRichTextEditorProps) {
const editor = useRef(null);
const config: JoditEditorProps["config"] = useMemo(
() => ({
readonly: readOnly,
placeholder,
minHeight,
language: "vi",
toolbarButtonSize: "middle",
uploader: {
insertImageAsBase64URI: true,
},
buttons: [
"bold",
"italic",
"underline",
"strikethrough",
"|",
"ul",
"ol",
"|",
"outdent",
"indent",
"|",
"font",
"fontsize",
"brush",
"paragraph",
"|",
"image",
"table",
"link",
"|",
"align",
"undo",
"redo",
"|",
"hr",
"eraser",
"copyformat",
"|",
"symbol",
"fullsize",
],
buttonsXS: [
"bold",
"italic",
"|",
"ul",
"ol",
"|",
"image",
"link",
"table",
"|",
"align",
"|",
"undo",
"redo",
"|",
"dots",
],
askBeforePasteHTML: false,
askBeforePasteFromWord: false,
defaultActionOnPaste: "insert_as_html",
enter: "p",
showPlaceholder: false,
}),
[minHeight, placeholder, readOnly],
);
return (
<div className={className}>
<style jsx global>{`
.admin-rich-text-editor .jodit-container {
border-radius: 1rem;
border: 1px solid rgba(6, 62, 142, 0.15);
overflow: hidden;
background: #ffffff;
}
.admin-rich-text-editor .jodit-toolbar__box {
border-bottom: 1px solid rgba(6, 62, 142, 0.12);
background: rgba(6, 62, 142, 0.04);
padding: 10px;
}
.admin-rich-text-editor .jodit-workplace {
min-height: ${minHeight}px;
}
.admin-rich-text-editor .jodit-wysiwyg {
min-height: ${minHeight}px;
padding: 16px 18px;
color: #111827;
font-size: 14px;
line-height: 1.8;
}
.admin-rich-text-editor .jodit-wysiwyg p {
margin-bottom: 1em;
}
.admin-rich-text-editor .jodit-wysiwyg img {
max-width: 100%;
height: auto;
border-radius: 0.75rem;
}
.admin-rich-text-editor .jodit-placeholder {
color: #374151 !important;
}
`}</style>
<div className="admin-rich-text-editor">
<JoditEditor
ref={editor}
value={value}
config={config}
onBlur={(nextContent) => onChange(nextContent)}
onChange={() => undefined}
/>
</div>
</div>
);
}
"use client";
import Image, { type ImageProps } from "next/image";
import * as React from "react";
interface SafeNextImageProps extends Omit<ImageProps, "src"> {
src?: string | null;
fallbackSrc?: string;
}
export function SafeNextImage({
src,
alt,
fallbackSrc = "/img-error.png",
...props
}: SafeNextImageProps) {
const [currentSrc, setCurrentSrc] = React.useState(src || fallbackSrc);
const [hasFailed, setHasFailed] = React.useState(false);
React.useEffect(() => {
setCurrentSrc(src || fallbackSrc);
setHasFailed(false);
}, [fallbackSrc, src]);
return (
<Image
{...props}
src={currentSrc}
alt={alt}
onError={() => {
if (hasFailed || currentSrc === fallbackSrc) return;
setHasFailed(true);
setCurrentSrc(fallbackSrc);
}}
unoptimized
/>
);
}
...@@ -28,17 +28,17 @@ type NavItem = { ...@@ -28,17 +28,17 @@ type NavItem = {
}; };
const navigation: NavItem[] = [ const navigation: NavItem[] = [
{ name: 'Dashboard', href: '/admin/dashboard', icon: BarChart3 }, // { name: 'Dashboard', href: '/admin/dashboard', icon: BarChart3 },
{ name: 'Cấu hình danh mục', href: '/admin/header-config', icon: Layers }, { name: 'Cấu hình danh mục', href: '/admin/header-config', icon: Layers },
{ name: 'Quản lý bài viết', href: '/admin/news', icon: Newspaper }, { name: 'Quản lý bài viết', href: '/admin/news', icon: Newspaper },
{ name: 'Quản lý hội viên', href: '/admin/members', icon: Users }, // { name: 'Quản lý hội viên', href: '/admin/members', icon: Users },
{ name: 'Quản lý đối tác', href: '/admin/partners', icon: Building2 }, // { name: 'Quản lý đối tác', href: '/admin/partners', icon: Building2 },
{ name: 'Email thông tin', href: '/admin/emails', icon: Mail }, // { name: 'Email thông tin', href: '/admin/emails', icon: Mail },
{ // {
name: 'Thiết lập', // name: 'Thiết lập',
icon: Settings, // icon: Settings,
children: [{ name: 'Thông tin website', href: '/admin/website-config' }], // children: [{ name: 'Thông tin website', href: '/admin/website-config' }],
}, // },
]; ];
export function AdminSidebar() { export function AdminSidebar() {
......
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment