Compare commits
356 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
9223122e66 | |
|
|
9e286ffd06 | |
|
|
18d6f32536 | |
|
|
9c2b3612c6 | |
|
|
6bd83237c4 | |
|
|
61df236e13 | |
|
|
7f9166b866 | |
|
|
2876e4eb30 | |
|
|
292cde6ed0 | |
|
|
3c5e644668 | |
|
|
d80adaa133 | |
|
|
eefa43a5b0 | |
|
|
277f1cc805 | |
|
|
f357a4e8ce | |
|
|
bb91379058 | |
|
|
740d73d689 | |
|
|
76c4ed9238 | |
|
|
44c6cc6d9d | |
|
|
2b8f7b724e | |
|
|
10694547d1 | |
|
|
e5e32496ab | |
|
|
78d1fc0d93 | |
|
|
c99e1a5c7d | |
|
|
274fd022e1 | |
|
|
933a672280 | |
|
|
ce7c343808 | |
|
|
cba94cf0a9 | |
|
|
7c721dcc96 | |
|
|
78ddd461df | |
|
|
ec2d7eb203 | |
|
|
ad5d048e78 | |
|
|
5614b5ada5 | |
|
|
f1b59ee537 | |
|
|
0ad1acee09 | |
|
|
2d09af2e8b | |
|
|
edafc223db | |
|
|
933cdb1100 | |
|
|
280ea508e9 | |
|
|
1a0d53bb11 | |
|
|
be1c799629 | |
|
|
3d23d75e05 | |
|
|
1ee379df71 | |
|
|
9a1b41e90b | |
|
|
fcfa24b06c | |
|
|
cf91ea33ec | |
|
|
1cc7786dee | |
|
|
f1ecebfb0f | |
|
|
76b7b4b86e | |
|
|
3698996c0c | |
|
|
3349450cb9 | |
|
|
80d07d358f | |
|
|
f9417b63ae | |
|
|
8f494739c6 | |
|
|
dd94afc81d | |
|
|
4c32b76ea3 | |
|
|
d21d377e17 | |
|
|
f90c9f73e1 | |
|
|
3c14fd2358 | |
|
|
6951ccd137 | |
|
|
cd822a7b5d | |
|
|
68b9439f23 | |
|
|
6afd7d1183 | |
|
|
1f7b912b52 | |
|
|
ae61d19a61 | |
|
|
3b0bf64e39 | |
|
|
0c5a7f86bb | |
|
|
15a395ccd2 | |
|
|
c71532cdd5 | |
|
|
eaa9001c98 | |
|
|
a08841b44c | |
|
|
e5704c6bec | |
|
|
5b6ffd0863 | |
|
|
eaa1628fde | |
|
|
dbabd36687 | |
|
|
618e5f4edb | |
|
|
5c7b8b7a34 | |
|
|
78518f038e | |
|
|
9bbbf4e45e | |
|
|
d730cc9cf9 | |
|
|
30235c240a | |
|
|
f7b1b79c08 | |
|
|
a69d3afa72 | |
|
|
f5802ff248 | |
|
|
16b5bb9fa0 | |
|
|
6b72b3e606 | |
|
|
abb5e0370f | |
|
|
6a76f2c0c2 | |
|
|
2f7bccfea9 | |
|
|
83cf910a34 | |
|
|
587f221fe1 | |
|
|
c662f3c866 | |
|
|
0d1f8041a9 | |
|
|
0757ec6519 | |
|
|
ecf4a64b03 | |
|
|
a60e661a6e | |
|
|
942b00521c | |
|
|
fd6ffda5a3 | |
|
|
d7e4200f3c | |
|
|
c6e0e123c3 | |
|
|
92811802cc | |
|
|
838b492768 | |
|
|
3e920e9108 | |
|
|
405455ea1f | |
|
|
38897c1cc1 | |
|
|
ff44a3b837 | |
|
|
4a5bce9c24 | |
|
|
5343e67f7d | |
|
|
399eef865f | |
|
|
92e300a0cf | |
|
|
b163084dfe | |
|
|
745c0b72f2 | |
|
|
c65afe8ced | |
|
|
94535fa64e | |
|
|
a96ab8590a | |
|
|
5e82ee8085 | |
|
|
b34a3d684b | |
|
|
d647025a42 | |
|
|
c661f8bbd2 | |
|
|
6a89dd3683 | |
|
|
20f7b19471 | |
|
|
8b5b6aa64e | |
|
|
49a257cab4 | |
|
|
b7e4258773 | |
|
|
683c4cc465 | |
|
|
9cf5498c88 | |
|
|
f50d37f4de | |
|
|
b72e145227 | |
|
|
73c56e764d | |
|
|
6fc2dd70af | |
|
|
d9475cc0a9 | |
|
|
d471035246 | |
|
|
0d15974117 | |
|
|
6230460148 | |
|
|
6688345b64 | |
|
|
6991c6d9b9 | |
|
|
037f3f374c | |
|
|
fdb2a161f6 | |
|
|
82e8dfafc9 | |
|
|
e43ede341f | |
|
|
bffe869f32 | |
|
|
2c7b46bcc3 | |
|
|
397b30aa75 | |
|
|
78497bfcb1 | |
|
|
92f8a4b030 | |
|
|
3d4860c7a5 | |
|
|
b6054fb3f8 | |
|
|
b7a2b445a9 | |
|
|
c25acbf4c5 | |
|
|
d0336e9ca0 | |
|
|
4f27b0f332 | |
|
|
53af10753c | |
|
|
5fdfccaeed | |
|
|
e1bc9ae6d5 | |
|
|
a4846c7c43 | |
|
|
26bbe4c003 | |
|
|
ce6ca9e853 | |
|
|
aee612f3f5 | |
|
|
affe1bce94 | |
|
|
a11257ef4e | |
|
|
e873af1f56 | |
|
|
4b69d1cc17 | |
|
|
bc8676d859 | |
|
|
7595a5126d | |
|
|
3fee36390d | |
|
|
902f8de516 | |
|
|
df2546bc08 | |
|
|
e60b452780 | |
|
|
66b592513e | |
|
|
e5541c3b35 | |
|
|
690f046bb3 | |
|
|
4dd34f7144 | |
|
|
bbed0c29c1 | |
|
|
8ba5499378 | |
|
|
5cdde1a75b | |
|
|
0b40c99ce9 | |
|
|
d9fcd2ec32 | |
|
|
f21a5ae389 | |
|
|
285bcad1d8 | |
|
|
e5a2fc2bf1 | |
|
|
bb982482c8 | |
|
|
5c3eb2aefa | |
|
|
3feb406715 | |
|
|
82bf3a8709 | |
|
|
0c866f9363 | |
|
|
7a4e7cbefe | |
|
|
9fc973d151 | |
|
|
78239c5f9d | |
|
|
c12dbf1596 | |
|
|
24a5e2a5c1 | |
|
|
5ae245c123 | |
|
|
71f0b3b52e | |
|
|
bd5d87c7ce | |
|
|
fb976f01bb | |
|
|
7e1ffe2d2f | |
|
|
cd7f7aec86 | |
|
|
8b77700af8 | |
|
|
b891591e7a | |
|
|
aa7fc8c6d4 | |
|
|
000945c33e | |
|
|
eae930552a | |
|
|
1cbfd35a76 | |
|
|
74cae6134b | |
|
|
3bf251b53c | |
|
|
74265cb797 | |
|
|
54633d8936 | |
|
|
c1aec7f316 | |
|
|
4508f5db37 | |
|
|
ab8f0c1086 | |
|
|
ac6f100047 | |
|
|
035f33250e | |
|
|
16c469f72c | |
|
|
a1bcadec86 | |
|
|
6fa5a05624 | |
|
|
cfc47a00a8 | |
|
|
5bc8099e11 | |
|
|
895cef6c75 | |
|
|
a6683014c2 | |
|
|
f572c8110d | |
|
|
d27806ba40 | |
|
|
9dd9f621ce | |
|
|
b7dce8c13c | |
|
|
12668edd3d | |
|
|
e1ac7cc688 | |
|
|
b20610369f | |
|
|
5c340a05a2 | |
|
|
f151ccecba | |
|
|
f5477357e6 | |
|
|
2f7461be46 | |
|
|
76f529a5c9 | |
|
|
4f20e4be57 | |
|
|
232c9f72b5 | |
|
|
65d1aaf3fe | |
|
|
1e4d324963 | |
|
|
4def21dcb1 | |
|
|
5216bcd8a5 | |
|
|
c4c854a37a | |
|
|
66cfb09a46 | |
|
|
6590743d90 | |
|
|
2d6ef04f45 | |
|
|
f878ab405e | |
|
|
f434ba21ff | |
|
|
450c54db11 | |
|
|
8feeaf7702 | |
|
|
c4c3d54a70 | |
|
|
2597abd40b | |
|
|
cfd89ea3f8 | |
|
|
373063e23a | |
|
|
96a3fd91cf | |
|
|
538376a687 | |
|
|
64729c9339 | |
|
|
13dcd5cd26 | |
|
|
0873cef3ce | |
|
|
2c18d422d5 | |
|
|
8645f913bf | |
|
|
98025cbab1 | |
|
|
53f2ebab41 | |
|
|
a9bba5ced6 | |
|
|
549eec65cc | |
|
|
98de288be5 | |
|
|
969eceab14 | |
|
|
b96f538184 | |
|
|
80e426570e | |
|
|
6650eb4a95 | |
|
|
a10a2108df | |
|
|
9563ad5987 | |
|
|
994d2a57d0 | |
|
|
ae1eba7beb | |
|
|
5807e389b4 | |
|
|
504101b91c | |
|
|
e0bb77e7af | |
|
|
933bbc402b | |
|
|
481cf1a2b4 | |
|
|
eeff4cc059 | |
|
|
f42f6b8388 | |
|
|
803960cf62 | |
|
|
3f8ff614f8 | |
|
|
b03da630ba | |
|
|
055c39f8d8 | |
|
|
d4d094ec95 | |
|
|
6c5bb50661 | |
|
|
fd021426d1 | |
|
|
968f2642cc | |
|
|
978c8b364f | |
|
|
00ddca22b9 | |
|
|
2e75d679b3 | |
|
|
9f32d3dde5 | |
|
|
008c8e61a8 | |
|
|
38c5b513ff | |
|
|
1ef498179c | |
|
|
377c22e083 | |
|
|
b8827ba8a8 | |
|
|
cabbd2d1da | |
|
|
e6a093bbaa | |
|
|
1fb792402d | |
|
|
4675b997f4 | |
|
|
518cdbffd3 | |
|
|
78a4a08d4a | |
|
|
76998748b1 | |
|
|
80169209a2 | |
|
|
ced4fae0da | |
|
|
6c7b32fcc3 | |
|
|
1c0e920b5d | |
|
|
249de7b595 | |
|
|
317c5ebd92 | |
|
|
ddc2484cc3 | |
|
|
aea083c0ea | |
|
|
4250c2ec78 | |
|
|
dab5958ac2 | |
|
|
538d941976 | |
|
|
a996c2623d | |
|
|
cd80bd07cb | |
|
|
e280a68635 | |
|
|
68c251c1f7 | |
|
|
2602e831e3 | |
|
|
e047b97e58 | |
|
|
7186ecf568 | |
|
|
1982679255 | |
|
|
bee5dcadb6 | |
|
|
47402ea655 | |
|
|
426a9e5c39 | |
|
|
92af4a4f6d | |
|
|
1bb6363a69 | |
|
|
d5303c4919 | |
|
|
0d12b105fb | |
|
|
d5fdc5b4af | |
|
|
381918d85a | |
|
|
fe4ca9052a | |
|
|
11eef893f4 | |
|
|
2bf469b721 | |
|
|
1f85dccc83 | |
|
|
e365aa8007 | |
|
|
19f1a59feb | |
|
|
576b129cc9 | |
|
|
836a45f364 | |
|
|
9dc4b64066 | |
|
|
0f9c070af1 | |
|
|
9345345e02 | |
|
|
fbd5586ae6 | |
|
|
3c6358fa62 | |
|
|
82a4233fb1 | |
|
|
69bed8b61b | |
|
|
6f6dea7def | |
|
|
2e87fb3b14 | |
|
|
3fa6376f2f | |
|
|
a98d16008c | |
|
|
81a4aaee88 | |
|
|
af7f5b3226 | |
|
|
ecb869ff51 | |
|
|
85e72aa17a | |
|
|
b14cc43bbe | |
|
|
484ebd87d3 | |
|
|
21d06c0e8a | |
|
|
6490679901 | |
|
|
92bba180ef | |
|
|
19849e6088 | |
|
|
89375f00b1 |
|
|
@ -0,0 +1,45 @@
|
|||
kind: pipeline
|
||||
type: ssh
|
||||
name: mediahub-fe-build-deploy
|
||||
|
||||
server:
|
||||
host:
|
||||
from_secret: ssh_host
|
||||
user:
|
||||
from_secret: ssh_user
|
||||
ssh_key:
|
||||
from_secret: ssh_key
|
||||
|
||||
steps:
|
||||
- name: prepare repo
|
||||
when:
|
||||
branch:
|
||||
- dev-sabda-v2
|
||||
- main
|
||||
- prod
|
||||
commands:
|
||||
- rm -rf /opt/build/mediahub-fe
|
||||
- mkdir -p /opt/build
|
||||
- cd /opt/build
|
||||
- git clone http://38.47.180.165:3000/mediahub/mediahub-fe.git
|
||||
|
||||
- name: build image
|
||||
when:
|
||||
branch:
|
||||
- dev-sabda-v2
|
||||
- prod
|
||||
commands:
|
||||
- docker login 38.47.180.165:3000 -u administrator -p HarborDockerImageRep0
|
||||
- cd /opt/build/mediahub-fe
|
||||
- docker build -t 38.47.180.165:3000/mediahub/mediahub-fe:$DRONE_BRANCH .
|
||||
- docker push 38.47.180.165:3000/mediahub/mediahub-fe:$DRONE_BRANCH
|
||||
|
||||
- name: deploy
|
||||
when:
|
||||
branch:
|
||||
- prod
|
||||
commands:
|
||||
- docker pull 38.47.180.165:3000/mediahub/mediahub-fe:$DRONE_BRANCH
|
||||
- docker stop new-mediahub-fe || true
|
||||
- docker rm new-mediahub-fe || true
|
||||
- docker run -dt -p 4200:3000 --restart always --name new-mediahub-fe 38.47.180.165:3000/mediahub/mediahub-fe:$DRONE_BRANCH
|
||||
5
.env
5
.env
|
|
@ -1,2 +1,3 @@
|
|||
NEXT_PUBLIC_API=https://netidhub.com/api
|
||||
NEXT_PUBLIC=https://netidhub.com
|
||||
NEXT_PUBLIC_API=https://new.netidhub.com/api
|
||||
NEXT_PUBLIC=https://new.netidhub.com
|
||||
NEXT_PUBLIC_TINYMCE_API_KEY=bhteuja26yz5p0aubxry9b95hs33amgn65kjv5km0fd5iuev
|
||||
|
|
@ -1,29 +1,30 @@
|
|||
stages:
|
||||
- build
|
||||
- deploy
|
||||
|
||||
|
||||
build-dev:
|
||||
stage: build
|
||||
when: on_success
|
||||
only:
|
||||
- main
|
||||
image: docker:stable
|
||||
- dev-landing-v2
|
||||
image:
|
||||
name: docker:25.0.3-cli
|
||||
services:
|
||||
- name: docker:dind
|
||||
command: ["--insecure-registry=103.82.242.92:8900"]
|
||||
- name: docker:25.0.3-dind
|
||||
command: ["--insecure-registry=38.47.185.86:8900"]
|
||||
script:
|
||||
- docker logout
|
||||
- docker login -u $DEPLOY_USERNAME -p $DEPLOY_TOKEN 103.82.242.92:8900
|
||||
- docker build -t 103.82.242.92:8900/mediahub/new-mediahub-fe:dev .
|
||||
- docker push 103.82.242.92:8900/mediahub/new-mediahub-fe:dev
|
||||
- docker login -u $DEPLOY_USERNAME -p $DEPLOY_TOKEN 38.47.185.86:8900
|
||||
- docker build -t 38.47.185.86:8900/mediahub/new-mediahub-fe:dev .
|
||||
- docker push 38.47.185.86:8900/mediahub/new-mediahub-fe:dev
|
||||
|
||||
auto-deploy:
|
||||
stage: deploy
|
||||
when: on_success
|
||||
only:
|
||||
- main
|
||||
- dev-landing-v2
|
||||
image: curlimages/curl:latest
|
||||
services:
|
||||
- docker:dind
|
||||
script:
|
||||
- curl --user admin:$JENKINS_PWD http://38.47.180.165:8080/job/auto-deploy-new-mediahub-fe/build?token=autodeploynewmediahub
|
||||
- curl --user admin:$JENKINS_PWD http://38.47.185.86:8080/job/auto-deploy-new-mediahub-fe/build?token=autodeploynewmediahub
|
||||
|
|
|
|||
44
Dockerfile
44
Dockerfile
|
|
@ -10,8 +10,9 @@ RUN npm install -g pnpm
|
|||
# Membuat direktori aplikasi dan mengatur sebagai working directory
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
|
||||
# Menyalin file penting terlebih dahulu untuk caching
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
COPY package.json ./
|
||||
|
||||
# Menyalin direktori ckeditor5 jika diperlukan
|
||||
COPY vendor/ckeditor5 ./vendor/ckeditor5
|
||||
|
|
@ -24,10 +25,49 @@ RUN pnpm install
|
|||
COPY . .
|
||||
|
||||
# Build aplikasi
|
||||
RUN NODE_OPTIONS="--max-old-space-size=4096" pnpm next build
|
||||
RUN NODE_OPTIONS="--max-old-space-size=4096" pnpm run build
|
||||
|
||||
# Expose port untuk server
|
||||
EXPOSE 3000
|
||||
|
||||
# Perintah untuk menjalankan aplikasi
|
||||
CMD ["pnpm", "run", "start"]
|
||||
|
||||
|
||||
# # Gunakan base image Node.js Alpine yang ringan
|
||||
# FROM node:23.5.0-alpine
|
||||
|
||||
# # Atur environment
|
||||
# ENV PORT=3000
|
||||
# ENV NODE_ENV=production
|
||||
# ENV NODE_OPTIONS="--max-old-space-size=4096"
|
||||
|
||||
# # Install dependencies global
|
||||
# RUN npm install -g pnpm pm2
|
||||
|
||||
# # Set working directory
|
||||
# WORKDIR /usr/src/app
|
||||
|
||||
# # Salin file penting untuk caching dependencies
|
||||
# COPY package.json pnpm-lock.yaml* ./
|
||||
|
||||
# # Salin vendor jika diperlukan (ckeditor misalnya)
|
||||
# COPY vendor/ckeditor5 ./vendor/ckeditor5
|
||||
|
||||
# # Install dependencies
|
||||
# RUN pnpm install --frozen-lockfile
|
||||
|
||||
# # Salin semua source code
|
||||
# COPY . .
|
||||
|
||||
# # Salin ecosystem config
|
||||
# COPY ecosystem.config.js ./
|
||||
|
||||
# # Build Next.js
|
||||
# RUN pnpm run build
|
||||
|
||||
# # Expose port
|
||||
# EXPOSE 3000
|
||||
|
||||
# # Jalankan Next.js dalam mode cluster
|
||||
# CMD ["pm2-runtime", "start", "ecosystem.config.js"]
|
||||
|
|
@ -0,0 +1,167 @@
|
|||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import { AuthPage } from '../app/[locale]/auth/page';
|
||||
import { useEmailValidation } from '../hooks/use-auth';
|
||||
|
||||
// Mock the hooks
|
||||
jest.mock('../hooks/use-auth', () => ({
|
||||
useAuth: () => ({
|
||||
login: jest.fn(),
|
||||
logout: jest.fn(),
|
||||
refreshToken: jest.fn(),
|
||||
isAuthenticated: false,
|
||||
user: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
}),
|
||||
useEmailValidation: jest.fn(),
|
||||
useEmailSetup: () => ({
|
||||
setupEmail: jest.fn(),
|
||||
loading: false,
|
||||
error: null,
|
||||
}),
|
||||
useOTPVerification: () => ({
|
||||
verifyOTP: jest.fn(),
|
||||
loading: false,
|
||||
error: null,
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock the components
|
||||
jest.mock('../components/auth/auth-layout', () => ({
|
||||
AuthLayout: ({ children }: { children: React.ReactNode }) => <div data-testid="auth-layout">{children}</div>,
|
||||
}));
|
||||
|
||||
jest.mock('../components/auth/login-form', () => ({
|
||||
LoginForm: ({ onSuccess, onError }: any) => (
|
||||
<div data-testid="login-form">
|
||||
<button onClick={() => onSuccess({ username: 'testuser', password: 'testpass' })}>
|
||||
Login Success
|
||||
</button>
|
||||
<button onClick={() => onError('Login failed')}>
|
||||
Login Error
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('../components/auth/email-setup-form', () => ({
|
||||
EmailSetupForm: ({ onSuccess, onError, onBack }: any) => (
|
||||
<div data-testid="email-setup-form">
|
||||
<button onClick={() => onSuccess()}>Email Setup Success</button>
|
||||
<button onClick={() => onError('Email setup failed')}>Email Setup Error</button>
|
||||
<button onClick={() => onBack()}>Back</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('../components/auth/otp-form', () => ({
|
||||
OTPForm: ({ onSuccess, onError, onResend }: any) => (
|
||||
<div data-testid="otp-form">
|
||||
<button onClick={() => onSuccess()}>OTP Success</button>
|
||||
<button onClick={() => onError('OTP failed')}>OTP Error</button>
|
||||
<button onClick={() => onResend()}>Resend OTP</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock toast
|
||||
jest.mock('sonner', () => ({
|
||||
toast: {
|
||||
error: jest.fn(),
|
||||
success: jest.fn(),
|
||||
info: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('Auth Flow', () => {
|
||||
const mockValidateEmail = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(useEmailValidation as jest.Mock).mockReturnValue({
|
||||
validateEmail: mockValidateEmail,
|
||||
loading: false,
|
||||
error: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('should start with login form', () => {
|
||||
render(<AuthPage params={{ locale: 'en' }} />);
|
||||
expect(screen.getByTestId('login-form')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should transition to email setup when validation returns "setup"', async () => {
|
||||
mockValidateEmail.mockResolvedValue('setup');
|
||||
|
||||
render(<AuthPage params={{ locale: 'en' }} />);
|
||||
|
||||
// Click login success to trigger email validation
|
||||
fireEvent.click(screen.getByText('Login Success'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('email-setup-form')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should transition to OTP when validation returns "otp"', async () => {
|
||||
mockValidateEmail.mockResolvedValue('otp');
|
||||
|
||||
render(<AuthPage params={{ locale: 'en' }} />);
|
||||
|
||||
// Click login success to trigger email validation
|
||||
fireEvent.click(screen.getByText('Login Success'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('otp-form')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should stay on login when validation returns "success"', async () => {
|
||||
mockValidateEmail.mockResolvedValue('success');
|
||||
|
||||
render(<AuthPage params={{ locale: 'en' }} />);
|
||||
|
||||
// Click login success to trigger email validation
|
||||
fireEvent.click(screen.getByText('Login Success'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('login-form')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should transition from email setup to OTP', async () => {
|
||||
mockValidateEmail.mockResolvedValue('setup');
|
||||
|
||||
render(<AuthPage params={{ locale: 'en' }} />);
|
||||
|
||||
// First, go to email setup
|
||||
fireEvent.click(screen.getByText('Login Success'));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('email-setup-form')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Then go to OTP
|
||||
fireEvent.click(screen.getByText('Email Setup Success'));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('otp-form')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should go back from email setup to login', async () => {
|
||||
mockValidateEmail.mockResolvedValue('setup');
|
||||
|
||||
render(<AuthPage params={{ locale: 'en' }} />);
|
||||
|
||||
// First, go to email setup
|
||||
fireEvent.click(screen.getByText('Login Success'));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('email-setup-form')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Then go back to login
|
||||
fireEvent.click(screen.getByText('Back'));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('login-form')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,264 @@
|
|||
import React from "react";
|
||||
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
||||
import { LoginForm } from "@/components/auth/login-form";
|
||||
import { useAuth, useEmailValidation } from "@/hooks/use-auth";
|
||||
|
||||
// Mock the hooks
|
||||
jest.mock("@/hooks/use-auth");
|
||||
jest.mock("@/service/landing/landing", () => ({
|
||||
listRole: jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
data: [
|
||||
{ id: 1, name: "Admin" },
|
||||
{ id: 2, name: "User" },
|
||||
],
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock next-intl
|
||||
jest.mock("next-intl", () => ({
|
||||
useTranslations: () => (key: string, options?: any) => {
|
||||
const defaults = {
|
||||
logInPlease: "Log In Please",
|
||||
acc: "Acc",
|
||||
register: "Register",
|
||||
password: "Password",
|
||||
rememberMe: "Remember Me",
|
||||
forgotPass: "Forgot Pass",
|
||||
next: "Next",
|
||||
categoryReg: "Category Reg",
|
||||
selectOne: "Select One",
|
||||
signIn: "Sign In",
|
||||
};
|
||||
return options?.defaultValue || defaults[key] || key;
|
||||
},
|
||||
}));
|
||||
|
||||
const mockUseAuth = useAuth as jest.MockedFunction<typeof useAuth>;
|
||||
const mockUseEmailValidation = useEmailValidation as jest.MockedFunction<typeof useEmailValidation>;
|
||||
|
||||
describe("LoginForm", () => {
|
||||
const mockLogin = jest.fn();
|
||||
const mockValidateEmail = jest.fn();
|
||||
const mockOnSuccess = jest.fn();
|
||||
const mockOnError = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
mockUseAuth.mockReturnValue({
|
||||
login: mockLogin,
|
||||
logout: jest.fn(),
|
||||
refreshToken: jest.fn(),
|
||||
isAuthenticated: false,
|
||||
user: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
});
|
||||
|
||||
mockUseEmailValidation.mockReturnValue({
|
||||
validateEmail: mockValidateEmail,
|
||||
loading: false,
|
||||
error: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("renders login form with all required fields", () => {
|
||||
render(
|
||||
<LoginForm
|
||||
onSuccess={mockOnSuccess}
|
||||
onError={mockOnError}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText("Log In Please")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(/username/i)).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(/password/i)).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: /selanjutnya/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows validation errors for invalid input", async () => {
|
||||
render(
|
||||
<LoginForm
|
||||
onSuccess={mockOnSuccess}
|
||||
onError={mockOnError}
|
||||
/>
|
||||
);
|
||||
|
||||
const submitButton = screen.getByRole("button", { name: /selanjutnya/i });
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Username is required")).toBeInTheDocument();
|
||||
expect(screen.getByText("Password is required")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("handles successful form submission", async () => {
|
||||
mockValidateEmail.mockResolvedValue("success");
|
||||
|
||||
render(
|
||||
<LoginForm
|
||||
onSuccess={mockOnSuccess}
|
||||
onError={mockOnError}
|
||||
/>
|
||||
);
|
||||
|
||||
const usernameInput = screen.getByLabelText(/username/i);
|
||||
const passwordInput = screen.getByLabelText(/password/i);
|
||||
const submitButton = screen.getByRole("button", { name: /selanjutnya/i });
|
||||
|
||||
fireEvent.change(usernameInput, { target: { value: "testuser" } });
|
||||
fireEvent.change(passwordInput, { target: { value: "password123" } });
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockValidateEmail).toHaveBeenCalledWith({
|
||||
username: "testuser",
|
||||
password: "password123",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("handles email validation step", async () => {
|
||||
mockValidateEmail.mockResolvedValue("setup");
|
||||
|
||||
render(
|
||||
<LoginForm
|
||||
onSuccess={mockOnSuccess}
|
||||
onError={mockOnError}
|
||||
/>
|
||||
);
|
||||
|
||||
const usernameInput = screen.getByLabelText(/username/i);
|
||||
const passwordInput = screen.getByLabelText(/password/i);
|
||||
const submitButton = screen.getByRole("button", { name: /selanjutnya/i });
|
||||
|
||||
fireEvent.change(usernameInput, { target: { value: "testuser" } });
|
||||
fireEvent.change(passwordInput, { target: { value: "password123" } });
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockOnError).toHaveBeenCalledWith("Email setup required");
|
||||
});
|
||||
});
|
||||
|
||||
it("handles OTP step", async () => {
|
||||
mockValidateEmail.mockResolvedValue("otp");
|
||||
|
||||
render(
|
||||
<LoginForm
|
||||
onSuccess={mockOnSuccess}
|
||||
onError={mockOnError}
|
||||
/>
|
||||
);
|
||||
|
||||
const usernameInput = screen.getByLabelText(/username/i);
|
||||
const passwordInput = screen.getByLabelText(/password/i);
|
||||
const submitButton = screen.getByRole("button", { name: /selanjutnya/i });
|
||||
|
||||
fireEvent.change(usernameInput, { target: { value: "testuser" } });
|
||||
fireEvent.change(passwordInput, { target: { value: "password123" } });
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockOnError).toHaveBeenCalledWith("OTP verification required");
|
||||
});
|
||||
});
|
||||
|
||||
it("toggles password visibility", () => {
|
||||
render(
|
||||
<LoginForm
|
||||
onSuccess={mockOnSuccess}
|
||||
onError={mockOnError}
|
||||
/>
|
||||
);
|
||||
|
||||
const passwordInput = screen.getByLabelText(/password/i);
|
||||
const toggleButton = screen.getByLabelText(/show password/i);
|
||||
|
||||
expect(passwordInput).toHaveAttribute("type", "password");
|
||||
|
||||
fireEvent.click(toggleButton);
|
||||
expect(passwordInput).toHaveAttribute("type", "text");
|
||||
expect(screen.getByLabelText(/hide password/i)).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(toggleButton);
|
||||
expect(passwordInput).toHaveAttribute("type", "password");
|
||||
});
|
||||
|
||||
it("handles remember me checkbox", () => {
|
||||
render(
|
||||
<LoginForm
|
||||
onSuccess={mockOnSuccess}
|
||||
onError={mockOnError}
|
||||
/>
|
||||
);
|
||||
|
||||
const rememberMeCheckbox = screen.getByRole("checkbox", { name: /remember me/i });
|
||||
|
||||
expect(rememberMeCheckbox).toBeChecked();
|
||||
|
||||
fireEvent.click(rememberMeCheckbox);
|
||||
expect(rememberMeCheckbox).not.toBeChecked();
|
||||
});
|
||||
|
||||
it("opens registration dialog", () => {
|
||||
render(
|
||||
<LoginForm
|
||||
onSuccess={mockOnSuccess}
|
||||
onError={mockOnError}
|
||||
/>
|
||||
);
|
||||
|
||||
const registerLink = screen.getByText("Register");
|
||||
fireEvent.click(registerLink);
|
||||
|
||||
expect(screen.getByText("Category Reg")).toBeInTheDocument();
|
||||
expect(screen.getByText("Select One")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Admin")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("User")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("handles loading state", async () => {
|
||||
mockUseEmailValidation.mockReturnValue({
|
||||
validateEmail: mockValidateEmail,
|
||||
loading: true,
|
||||
error: null,
|
||||
});
|
||||
|
||||
render(
|
||||
<LoginForm
|
||||
onSuccess={mockOnSuccess}
|
||||
onError={mockOnError}
|
||||
/>
|
||||
);
|
||||
|
||||
const submitButton = screen.getByRole("button", { name: /processing/i });
|
||||
expect(submitButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it("handles error state", async () => {
|
||||
mockValidateEmail.mockRejectedValue(new Error("Validation failed"));
|
||||
|
||||
render(
|
||||
<LoginForm
|
||||
onSuccess={mockOnSuccess}
|
||||
onError={mockOnError}
|
||||
/>
|
||||
);
|
||||
|
||||
const usernameInput = screen.getByLabelText(/username/i);
|
||||
const passwordInput = screen.getByLabelText(/password/i);
|
||||
const submitButton = screen.getByRole("button", { name: /selanjutnya/i });
|
||||
|
||||
fireEvent.change(usernameInput, { target: { value: "testuser" } });
|
||||
fireEvent.change(passwordInput, { target: { value: "password123" } });
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockOnError).toHaveBeenCalledWith("Validation failed");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
describe('Testing Setup', () => {
|
||||
it('should work correctly', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
|
||||
it('should have testing library matchers', () => {
|
||||
const element = document.createElement('div');
|
||||
element.textContent = 'Hello World';
|
||||
expect(element).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
'use server'
|
||||
import { redirect } from "next/navigation";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { postMessage } from "@/app/[locale]/(protected)/app/chat/utils";
|
||||
|
||||
|
||||
export const postMessageAction = async (id: string, message: string,) => {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import { useRouter } from "next/navigation";
|
|||
import { deleteUser } from "@/service/management-user/management-user";
|
||||
import { stringify } from "querystring";
|
||||
|
||||
const columns: ColumnDef<any>[] = [
|
||||
const getColumns = ({ onRefresh }: { onRefresh: () => void }): ColumnDef<any>[] => [
|
||||
{
|
||||
accessorKey: "no",
|
||||
header: "No",
|
||||
|
|
@ -30,11 +30,13 @@ const columns: ColumnDef<any>[] = [
|
|||
header: "Nama",
|
||||
cell: ({ row }) => <span>{row.getValue("fullname")}</span>,
|
||||
},
|
||||
|
||||
{
|
||||
accessorKey: "address",
|
||||
header: "Wilayah",
|
||||
cell: ({ row }) => <span>MABES</span>,
|
||||
cell: () => <span>MABES</span>,
|
||||
},
|
||||
|
||||
{
|
||||
accessorKey: "userRolePlacements",
|
||||
header: "Posisi",
|
||||
|
|
@ -52,6 +54,7 @@ const columns: ColumnDef<any>[] = [
|
|||
return <span>{posisi}</span>;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
accessorKey: "role.name",
|
||||
header: "Bidang Keahlian",
|
||||
|
|
@ -81,72 +84,77 @@ const columns: ColumnDef<any>[] = [
|
|||
|
||||
{
|
||||
id: "actions",
|
||||
accessorKey: "action",
|
||||
header: "Actions",
|
||||
enableHiding: false,
|
||||
cell: ({ row }) => {
|
||||
const { toast } = useToast();
|
||||
const MySwal = withReactContent(Swal);
|
||||
const router = useRouter();
|
||||
|
||||
const doDelete = async (id: number) => {
|
||||
Swal.fire({
|
||||
title: "Menghapus user...",
|
||||
text: "Mohon tunggu",
|
||||
allowOutsideClick: false,
|
||||
didOpen: () => Swal.showLoading(),
|
||||
});
|
||||
|
||||
const response = await deleteUser(id);
|
||||
|
||||
Swal.close();
|
||||
|
||||
if (response?.error) {
|
||||
toast({
|
||||
title: stringify(response?.message),
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
toast({
|
||||
title: "Success delete",
|
||||
});
|
||||
|
||||
router.push("?dataChange=true");
|
||||
toast({ title: "Berhasil menghapus user" });
|
||||
|
||||
// ⬅️ INI YANG PENTING → REFRESH TABLE TANPA RELOAD
|
||||
onRefresh();
|
||||
};
|
||||
|
||||
const handleDelete = (id: number) => {
|
||||
MySwal.fire({
|
||||
title: "Apakah anda ingin menghapus data user?",
|
||||
title: "Hapus user ini?",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: "#dc3545",
|
||||
confirmButtonText: "Iya",
|
||||
cancelButtonText: "Tidak",
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
doDelete(id);
|
||||
}
|
||||
}).then((res) => {
|
||||
if (res.isConfirmed) doDelete(id);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
className="bg-transparent ring-offset-transparent hover:bg-transparent hover:ring-0 hover:ring-transparent"
|
||||
>
|
||||
<MoreVertical className="h-4 w-4 text-default-800" />
|
||||
<Button size="icon" variant="ghost">
|
||||
<MoreVertical className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="p-0" align="end">
|
||||
<Link href={`/admin/add-experts/detail/${row?.original?.id}`}>
|
||||
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none">
|
||||
<Eye className="w-4 h-4 me-1.5" />
|
||||
View
|
||||
<DropdownMenuContent align="end">
|
||||
|
||||
<Link href={`/admin/add-experts/detail/${row.original.id}`}>
|
||||
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none cursor-pointer">
|
||||
<Eye className="w-4 h-4 me-1.5" /> View
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
|
||||
<Link href={`/admin/add-experts/update/${row?.original?.id}`}>
|
||||
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none">
|
||||
<SquarePen className="w-4 h-4 me-1.5" />
|
||||
Edit
|
||||
<Link href={`/admin/add-experts/update/${row.original.id}`}>
|
||||
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none cursor-pointer">
|
||||
<SquarePen className="w-4 h-4 me-1.5" /> Edit
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleDelete(row.original.userKeycloakId)}
|
||||
className="p-2 border-b text-destructive bg-destructive/30 focus:bg-destructive focus:text-destructive-foreground rounded-none"
|
||||
className="text-red-600 cursor-pointer hover:bg-red-300"
|
||||
>
|
||||
<Trash2 className="w-4 h-4 me-1.5" />
|
||||
Delete
|
||||
<Trash2 className="w-4 h-4 me-1.5" /> Delete
|
||||
</DropdownMenuItem>
|
||||
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
|
|
@ -154,4 +162,4 @@ const columns: ColumnDef<any>[] = [
|
|||
},
|
||||
];
|
||||
|
||||
export default columns;
|
||||
export default getColumns;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
import * as React from "react";
|
||||
import {
|
||||
ColumnDef,
|
||||
ColumnFiltersState,
|
||||
PaginationState,
|
||||
SortingState,
|
||||
|
|
@ -15,7 +14,6 @@ import {
|
|||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
|
|
@ -25,7 +23,6 @@ import {
|
|||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { UserIcon } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
|
|
@ -35,43 +32,14 @@ import {
|
|||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { InputGroup, InputGroupText } from "@/components/ui/input-group";
|
||||
import { paginationBlog } from "@/service/blog/blog";
|
||||
import { ticketingPagination } from "@/service/ticketing/ticketing";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import TablePagination from "@/components/table/table-pagination";
|
||||
import columns from "./column";
|
||||
import { getPlanningPagination } from "@/service/agenda-setting/agenda-setting";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { listDataMedia } from "@/service/broadcast/broadcast";
|
||||
// import columns from "./column";
|
||||
import getColumns from "./column";
|
||||
import { listEnableCategory } from "@/service/content/content";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { close, loading } from "@/config/swal";
|
||||
import { Link } from "@/i18n/routing";
|
||||
import { listDataExperts } from "@/service/experts/experts";
|
||||
|
||||
const dummyData = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Prof. Dr. Ravi",
|
||||
region: "Nasional",
|
||||
skills: "Komunikasi",
|
||||
experience: "Akademisi",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Prof. Dr. Novan",
|
||||
region: "DKI Jakarta",
|
||||
skills: "Hukum",
|
||||
experience: "Akademisi + Praktisi",
|
||||
},
|
||||
];
|
||||
|
||||
const AddExpertTable = () => {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
|
@ -97,7 +65,8 @@ const AddExpertTable = () => {
|
|||
const [limit, setLimit] = React.useState(10);
|
||||
const table = useReactTable({
|
||||
data: dataTable,
|
||||
columns,
|
||||
// columns,
|
||||
columns: getColumns({ onRefresh: fetchData }),
|
||||
onSortingChange: setSorting,
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
|
|
@ -201,7 +170,7 @@ const AddExpertTable = () => {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="w-full overflow-x-auto bg-white p-4 rounded-sm space-y-3">
|
||||
<div className="w-full overflow-x-auto bg-white dark:bg-black p-4 rounded-sm space-y-3">
|
||||
<div className="flex justify-between mb-10 items-center">
|
||||
<p className="text-xl font-medium text-default-900">Tenaga Ahli</p>
|
||||
<Link href="/admin/add-experts/create">
|
||||
|
|
@ -283,7 +252,11 @@ const AddExpertTable = () => {
|
|||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
<TableCell
|
||||
// colSpan={columns.length}
|
||||
colSpan={table.getAllLeafColumns().length}
|
||||
className="h-24 text-center"
|
||||
>
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
|
|
|||
|
|
@ -35,32 +35,61 @@ import {
|
|||
import { error, loading } from "@/config/swal";
|
||||
import { Eye, EyeOff } from "lucide-react";
|
||||
|
||||
// const FormSchema = z.object({
|
||||
// name: z.string({
|
||||
// required_error: "Required",
|
||||
// }),
|
||||
// username: z.string({
|
||||
// required_error: "Required",
|
||||
// }),
|
||||
// password: z.string({
|
||||
// required_error: "Required",
|
||||
// }),
|
||||
// phoneNumber: z.string({
|
||||
// required_error: "Required",
|
||||
// }),
|
||||
// email: z.string({
|
||||
// required_error: "Required",
|
||||
// }),
|
||||
// skills: z.string({
|
||||
// required_error: "Required",
|
||||
// }),
|
||||
// experiences: z.string({
|
||||
// required_error: "Required",
|
||||
// }),
|
||||
// company: z.string({
|
||||
// required_error: "Required",
|
||||
// }),
|
||||
// });
|
||||
|
||||
const FormSchema = z.object({
|
||||
name: z.string({
|
||||
required_error: "Required",
|
||||
}),
|
||||
username: z.string({
|
||||
required_error: "Required",
|
||||
}),
|
||||
password: z.string({
|
||||
required_error: "Required",
|
||||
}),
|
||||
phoneNumber: z.string({
|
||||
required_error: "Required",
|
||||
}),
|
||||
email: z.string({
|
||||
required_error: "Required",
|
||||
}),
|
||||
skills: z.string({
|
||||
required_error: "Required",
|
||||
}),
|
||||
experiences: z.string({
|
||||
required_error: "Required",
|
||||
}),
|
||||
company: z.string({
|
||||
required_error: "Required",
|
||||
}),
|
||||
name: z.string({ required_error: "Required" }),
|
||||
username: z
|
||||
.string({ required_error: "Required" })
|
||||
.refine((val) => !/\s/.test(val), {
|
||||
message: "Username tidak boleh mengandung spasi",
|
||||
}),
|
||||
// .transform((val) => val.toLowerCase()),
|
||||
|
||||
password: z
|
||||
.string({ required_error: "Required" })
|
||||
.min(8, "Minimal 8 karakter")
|
||||
.regex(/[A-Z]/, "Harus mengandung huruf besar (A-Z)")
|
||||
.regex(/[0-9]/, "Harus mengandung angka (0-9)")
|
||||
.regex(/[^A-Za-z0-9]/, "Harus mengandung karakter spesial (!@#$%^&*)"),
|
||||
|
||||
// confirmPassword: z.string({ required_error: "Required" }),
|
||||
|
||||
phoneNumber: z.string({ required_error: "Required" }),
|
||||
email: z.string({ required_error: "Required" }),
|
||||
skills: z.string({ required_error: "Required" }),
|
||||
experiences: z.string({ required_error: "Required" }),
|
||||
company: z.string({ required_error: "Required" }),
|
||||
});
|
||||
// .refine((data) => data.password === data.confirmPassword, {
|
||||
// path: ["confirmPassword"],
|
||||
// message: "Konfirmasi password tidak sama",
|
||||
// });
|
||||
|
||||
export type Placements = {
|
||||
index: number;
|
||||
|
|
@ -74,6 +103,7 @@ export default function AddExpertForm() {
|
|||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
});
|
||||
const [passwordStrength, setPasswordStrength] = useState("");
|
||||
const [incrementId, setIncrementId] = useState(1);
|
||||
const [placementRows, setPlacementRows] = useState<Placements[]>([
|
||||
{ index: 0, roleId: "", userLevelId: 0 },
|
||||
|
|
@ -135,7 +165,7 @@ export default function AddExpertForm() {
|
|||
};
|
||||
|
||||
loading();
|
||||
|
||||
|
||||
// check availability first
|
||||
var placementArr: any[] = [];
|
||||
placementRows.forEach((row: any) => {
|
||||
|
|
@ -261,6 +291,19 @@ export default function AddExpertForm() {
|
|||
}
|
||||
};
|
||||
|
||||
const computeStrength = (password: string) => {
|
||||
let score = 0;
|
||||
if (password.length >= 8) score++;
|
||||
if (/[A-Z]/.test(password)) score++;
|
||||
if (/[0-9]/.test(password)) score++;
|
||||
if (/[^A-Za-z0-9]/.test(password)) score++;
|
||||
|
||||
if (score <= 1) return "weak";
|
||||
if (score === 2 || score === 3) return "medium";
|
||||
if (score === 4) return "strong";
|
||||
return "";
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SiteBreadcrumb />
|
||||
|
|
@ -268,7 +311,7 @@ export default function AddExpertForm() {
|
|||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-3 bg-white rounded-sm p-4"
|
||||
className="space-y-3 bg-white dark:bg-black rounded-sm p-4"
|
||||
>
|
||||
<p className="fonnt-semibold">Campaign</p>
|
||||
<FormField
|
||||
|
|
@ -288,6 +331,39 @@ export default function AddExpertForm() {
|
|||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Username (huruf kecil, tanpa spasi)</FormLabel>
|
||||
|
||||
<Input
|
||||
type="text"
|
||||
value={field.value}
|
||||
placeholder="masukkan username"
|
||||
onChange={(e) => {
|
||||
let value = e.target.value;
|
||||
|
||||
// Hapus spasi otomatis
|
||||
value = value.replace(/\s+/g, "");
|
||||
|
||||
// Jadikan lowercase otomatis
|
||||
value = value.toLowerCase();
|
||||
|
||||
field.onChange(value);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Info tambahan */}
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Username otomatis menjadi huruf kecil tanpa spasi.
|
||||
</p>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* <FormField
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={({ field }) => (
|
||||
|
|
@ -303,7 +379,7 @@ export default function AddExpertForm() {
|
|||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
/> */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="phoneNumber"
|
||||
|
|
@ -349,6 +425,69 @@ export default function AddExpertForm() {
|
|||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<div className="relative">
|
||||
<Input
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={field.value}
|
||||
placeholder="Masukkan Password"
|
||||
onChange={(e) => {
|
||||
field.onChange(e.target.value);
|
||||
setPasswordStrength(computeStrength(e.target.value));
|
||||
}}
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-default-500"
|
||||
>
|
||||
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
|
||||
</button>
|
||||
</div>
|
||||
<FormLabel className="text-gray-400 text-[12px]">
|
||||
Password harus memiliki minimal 8 karakter, special karakter,
|
||||
angka dan huruf kapital
|
||||
</FormLabel>
|
||||
|
||||
{/* Strength meter */}
|
||||
{field.value && (
|
||||
<div className="mt-2">
|
||||
<div
|
||||
className={`h-2 rounded transition-all ${
|
||||
passwordStrength === "weak"
|
||||
? "bg-red-500 w-1/4"
|
||||
: passwordStrength === "medium"
|
||||
? "bg-yellow-500 w-2/4"
|
||||
: "bg-green-500 w-full"
|
||||
}`}
|
||||
/>
|
||||
|
||||
<p
|
||||
className={`text-xs mt-1 ${
|
||||
passwordStrength === "weak"
|
||||
? "text-red-500"
|
||||
: passwordStrength === "medium"
|
||||
? "text-yellow-600"
|
||||
: "text-green-600"
|
||||
}`}
|
||||
>
|
||||
{passwordStrength === "weak" && "Weak Password"}
|
||||
{passwordStrength === "medium" && "Medium Password"}
|
||||
{passwordStrength === "strong" && "Strong Password"}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* <FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
|
|
@ -373,7 +512,7 @@ export default function AddExpertForm() {
|
|||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
/> */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="skills"
|
||||
|
|
|
|||
|
|
@ -37,32 +37,43 @@ import { Eye, EyeOff } from "lucide-react";
|
|||
import { useParams } from "next/navigation";
|
||||
|
||||
const FormSchema = z.object({
|
||||
name: z.string({
|
||||
required_error: "Required",
|
||||
}),
|
||||
username: z.string({
|
||||
required_error: "Required",
|
||||
}),
|
||||
password: z.string({
|
||||
required_error: "Required",
|
||||
}),
|
||||
phoneNumber: z.string({
|
||||
required_error: "Required",
|
||||
}),
|
||||
email: z.string({
|
||||
required_error: "Required",
|
||||
}),
|
||||
skills: z.string({
|
||||
required_error: "Required",
|
||||
}),
|
||||
experiences: z.string({
|
||||
required_error: "Required",
|
||||
}),
|
||||
company: z.string({
|
||||
required_error: "Required",
|
||||
}),
|
||||
name: z.string().optional(),
|
||||
username: z.string().optional(),
|
||||
password: z.string().optional(),
|
||||
phoneNumber: z.string().optional(),
|
||||
email: z.string().optional(),
|
||||
skills: z.string().optional(),
|
||||
experiences: z.string().optional(),
|
||||
company: z.string().optional(),
|
||||
});
|
||||
|
||||
// const FormSchema = z.object({
|
||||
// name: z.string({
|
||||
// required_error: "Required",
|
||||
// }),
|
||||
// username: z.string({
|
||||
// required_error: "Required",
|
||||
// }),
|
||||
// password: z.string({
|
||||
// required_error: "Required",
|
||||
// }),
|
||||
// phoneNumber: z.string({
|
||||
// required_error: "Required",
|
||||
// }),
|
||||
// email: z.string({
|
||||
// required_error: "Required",
|
||||
// }),
|
||||
// skills: z.string({
|
||||
// required_error: "Required",
|
||||
// }),
|
||||
// experiences: z.string({
|
||||
// required_error: "Required",
|
||||
// }),
|
||||
// company: z.string({
|
||||
// required_error: "Required",
|
||||
// }),
|
||||
// });
|
||||
|
||||
export type Placements = {
|
||||
index: number;
|
||||
roleId?: string;
|
||||
|
|
@ -96,6 +107,10 @@ interface Detail {
|
|||
createdAt: string;
|
||||
};
|
||||
};
|
||||
userRolePlacements?: {
|
||||
roleId: number;
|
||||
userLevelId: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
export default function UpdateExpertForm() {
|
||||
|
|
@ -149,6 +164,39 @@ export default function UpdateExpertForm() {
|
|||
initState();
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!detail) return;
|
||||
|
||||
// Isi semua form field
|
||||
form.reset({
|
||||
name: detail.fullname || "",
|
||||
username: detail.username || "",
|
||||
phoneNumber: detail.phoneNumber || "",
|
||||
email: detail.email || "",
|
||||
password: "",
|
||||
skills: detail?.userProfilesAdditional?.userCompetency?.id
|
||||
? String(detail.userProfilesAdditional.userCompetency.id)
|
||||
: "",
|
||||
experiences: detail?.userProfilesAdditional?.userExperienceId
|
||||
? String(detail.userProfilesAdditional.userExperienceId)
|
||||
: "",
|
||||
company: detail?.userProfilesAdditional?.companyName || "",
|
||||
});
|
||||
|
||||
// 🔥 Masukkan posisi existing
|
||||
if (detail.userRolePlacements && detail.userRolePlacements.length > 0) {
|
||||
const mapped = detail.userRolePlacements.map(
|
||||
(item: any, idx: number) => ({
|
||||
index: idx,
|
||||
roleId: String(item.roleId),
|
||||
userLevelId: Number(item.userLevelId),
|
||||
})
|
||||
);
|
||||
|
||||
setPlacementRows(mapped);
|
||||
}
|
||||
}, [detail]);
|
||||
|
||||
if (!detail) return <div>Loading...</div>;
|
||||
|
||||
const togglePasswordType = () => {
|
||||
|
|
@ -189,18 +237,35 @@ export default function UpdateExpertForm() {
|
|||
|
||||
const dataReq = {
|
||||
id: detail?.id,
|
||||
firstName: data.name,
|
||||
username: data.username,
|
||||
email: data.email,
|
||||
password: data.password,
|
||||
firstName: data.name || detail.fullname,
|
||||
username: data.username || detail.username,
|
||||
email: data.email || detail.email,
|
||||
password: data.password || undefined,
|
||||
address: "",
|
||||
roleId: "EXP-ID",
|
||||
phoneNumber: data.phoneNumber,
|
||||
userCompetencyId: data.skills,
|
||||
userExperienceId: data.experiences,
|
||||
companyName: data.company,
|
||||
phoneNumber: data.phoneNumber || detail.phoneNumber,
|
||||
userCompetencyId:
|
||||
data.skills || detail.userProfilesAdditional?.userCompetency?.id,
|
||||
userExperienceId:
|
||||
data.experiences || detail.userProfilesAdditional?.userExperienceId,
|
||||
companyName: data.company || detail.userProfilesAdditional?.companyName,
|
||||
isAdmin: true,
|
||||
};
|
||||
|
||||
// const dataReq = {
|
||||
// id: detail?.id,
|
||||
// firstName: data.name,
|
||||
// username: data.username,
|
||||
// email: data.email,
|
||||
// password: data.password,
|
||||
// address: "",
|
||||
// roleId: "EXP-ID",
|
||||
// phoneNumber: data.phoneNumber,
|
||||
// userCompetencyId: data.skills,
|
||||
// userExperienceId: data.experiences,
|
||||
// companyName: data.company,
|
||||
// };
|
||||
|
||||
loading();
|
||||
const res = await saveUserInternal(dataReq);
|
||||
const resData = res?.data?.data;
|
||||
|
|
@ -322,10 +387,15 @@ export default function UpdateExpertForm() {
|
|||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Nama Lengkap</FormLabel>
|
||||
<Input
|
||||
{/* <Input
|
||||
defaultValue={detail?.fullname}
|
||||
placeholder="Masukkan Nama Lengkap"
|
||||
onChange={field.onChange}
|
||||
/> */}
|
||||
<Input
|
||||
{...field}
|
||||
defaultValue={detail?.fullname}
|
||||
placeholder="Masukkan Nama Lengkap"
|
||||
/>
|
||||
|
||||
<FormMessage />
|
||||
|
|
@ -333,18 +403,24 @@ export default function UpdateExpertForm() {
|
|||
)}
|
||||
/>
|
||||
<FormField
|
||||
disabled
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Username</FormLabel>
|
||||
<Input
|
||||
{/* <Input
|
||||
type="text"
|
||||
defaultValue={detail?.username}
|
||||
placeholder="Masukkan"
|
||||
onChange={field.onChange}
|
||||
/> */}
|
||||
<Input
|
||||
{...field}
|
||||
type="text"
|
||||
defaultValue={detail?.username}
|
||||
placeholder="Masukkan"
|
||||
/>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
|
|
@ -355,11 +431,17 @@ export default function UpdateExpertForm() {
|
|||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>No. HP</FormLabel>
|
||||
<Input
|
||||
{/* <Input
|
||||
type="number"
|
||||
defaultValue={detail?.phoneNumber}
|
||||
placeholder="Masukkan No.Hp"
|
||||
onChange={field.onChange}
|
||||
/> */}
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
defaultValue={detail?.phoneNumber}
|
||||
placeholder="Masukkan"
|
||||
/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
|
@ -371,17 +453,46 @@ export default function UpdateExpertForm() {
|
|||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<Input
|
||||
{/* <Input
|
||||
type="email"
|
||||
defaultValue={detail?.email}
|
||||
placeholder="Masukkan email"
|
||||
onChange={field.onChange}
|
||||
/> */}
|
||||
<Input
|
||||
{...field}
|
||||
type="email"
|
||||
defaultValue={detail?.email}
|
||||
placeholder="Masukkan email"
|
||||
/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
{/* <FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password (Opsional)</FormLabel>
|
||||
<div className="relative">
|
||||
<Input
|
||||
{...field}
|
||||
type={showPassword ? "text" : "password"}
|
||||
placeholder="Kosongkan jika tidak ingin mengubah password"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2"
|
||||
>
|
||||
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
|
||||
</button>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/> */}
|
||||
{/* <FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
|
|
@ -406,7 +517,7 @@ export default function UpdateExpertForm() {
|
|||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
/> */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="skills"
|
||||
|
|
@ -481,12 +592,21 @@ export default function UpdateExpertForm() {
|
|||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Nama Institusi/Perusahaan</FormLabel>
|
||||
<Input
|
||||
{/* <Input
|
||||
type="text"
|
||||
value={detail?.userProfilesAdditional?.companyName || ""}
|
||||
placeholder="Nama Institusi/Perusahaan"
|
||||
onChange={field.onChange}
|
||||
/> */}
|
||||
<Input
|
||||
{...field}
|
||||
type="text"
|
||||
defaultValue={
|
||||
detail?.userProfilesAdditional?.companyName || ""
|
||||
}
|
||||
placeholder="Nama Institusi/Perusahaan"
|
||||
/>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
|
|
@ -497,6 +617,7 @@ export default function UpdateExpertForm() {
|
|||
{placementRows?.map((row: any) => (
|
||||
<div key={row.index} className="flex items-center gap-2 my-2">
|
||||
<Select
|
||||
value={row.roleId}
|
||||
onValueChange={(e) =>
|
||||
handleSelectionChange(row.index, "roleId", e)
|
||||
}
|
||||
|
|
@ -533,6 +654,7 @@ export default function UpdateExpertForm() {
|
|||
</SelectContent>
|
||||
</Select> */}
|
||||
<Select
|
||||
value={row.userLevelId}
|
||||
onValueChange={(e) =>
|
||||
handleSelectionChange(row.index, "userLevelId", e)
|
||||
}
|
||||
|
|
@ -562,7 +684,7 @@ export default function UpdateExpertForm() {
|
|||
type="button"
|
||||
size="md"
|
||||
onClick={handleAddRow}
|
||||
disabled={placementRows.length >= 2} // optional: disable button if already 1 row added
|
||||
disabled={placementRows.length >= 2}
|
||||
>
|
||||
Tambah
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -133,8 +133,8 @@ export default function ContentManagement() {
|
|||
<SiteBreadcrumb />
|
||||
<div className="flex flex-col gap-3">
|
||||
<Accordion id="polri" type="single" collapsible className="w-full">
|
||||
<AccordionItem value="item-1" className="bg-white w-full">
|
||||
<AccordionTrigger className="bg-white">
|
||||
<AccordionItem value="item-1" className="bg-white dark:bg-black w-full">
|
||||
<AccordionTrigger className="bg-white dark:bg-black">
|
||||
KONTEN YANG DISIMPAN OLEH PENGGUNA POLRI INDONESIA
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
|
|
@ -161,8 +161,8 @@ export default function ContentManagement() {
|
|||
</AccordionItem>
|
||||
</Accordion>
|
||||
<Accordion id="2" type="single" collapsible className="w-full">
|
||||
<AccordionItem value="item-2" className="bg-white w-full">
|
||||
<AccordionTrigger className="bg-white">
|
||||
<AccordionItem value="item-2" className="bg-white dark:bg-black w-full">
|
||||
<AccordionTrigger className="bg-white dark:bg-black">
|
||||
PENAMBAHAN JUMLAH PENGGUNA JURNALIS INDONESIA
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
|
|
@ -189,8 +189,8 @@ export default function ContentManagement() {
|
|||
</AccordionItem>
|
||||
</Accordion>
|
||||
<Accordion id="3" type="single" collapsible className="w-full">
|
||||
<AccordionItem value="item-1" className="bg-white w-full">
|
||||
<AccordionTrigger className="bg-white">
|
||||
<AccordionItem value="item-1" className="bg-white dark:bg-black w-full">
|
||||
<AccordionTrigger className="bg-white dark:bg-black">
|
||||
KONTEN YANG DISIMPAN OLEH PENGGUNA JURNALIS INTERNASIONAL
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
|
|
@ -217,8 +217,8 @@ export default function ContentManagement() {
|
|||
</AccordionItem>
|
||||
</Accordion>
|
||||
<Accordion id="4" type="single" collapsible className="w-full">
|
||||
<AccordionItem value="item-1" className="bg-white w-full">
|
||||
<AccordionTrigger className="bg-white">
|
||||
<AccordionItem value="item-1" className="bg-white dark:bg-black w-full">
|
||||
<AccordionTrigger className="bg-white dark:bg-black">
|
||||
PENAMBAHAN JUMLAH PENGGUNA POLRI INDONESIA
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
|
|
@ -245,8 +245,8 @@ export default function ContentManagement() {
|
|||
</AccordionItem>
|
||||
</Accordion>
|
||||
<Accordion id="5" type="single" collapsible className="w-full">
|
||||
<AccordionItem value="item-1" className="bg-white w-full">
|
||||
<AccordionTrigger className="bg-white">
|
||||
<AccordionItem value="item-1" className="bg-white dark:bg-black w-full">
|
||||
<AccordionTrigger className="bg-white dark:bg-black">
|
||||
PENAMBAHAN JUMLAH PENGGUNA JURNALIS INDONESIA
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
|
|
@ -273,8 +273,8 @@ export default function ContentManagement() {
|
|||
</AccordionItem>
|
||||
</Accordion>
|
||||
<Accordion id="6" type="single" collapsible className="w-full">
|
||||
<AccordionItem value="item-1" className="bg-white w-full">
|
||||
<AccordionTrigger className="bg-white">
|
||||
<AccordionItem value="item-1" className="bg-white dark:bg-black w-full">
|
||||
<AccordionTrigger className="bg-white dark:bg-black">
|
||||
PENAMBAHAN JUMLAH PENGGUNA JURNALIS INTERNASIONAL
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
|
|
|
|||
|
|
@ -88,8 +88,8 @@ export default function EmergencyIssue() {
|
|||
collapsible
|
||||
className="w-full"
|
||||
>
|
||||
<AccordionItem value="item-1" className="bg-white w-full">
|
||||
<AccordionTrigger className="bg-white">
|
||||
<AccordionItem value="item-1" className="bg-white dark:bg-black w-full">
|
||||
<AccordionTrigger className="bg-white dark:bg-black">
|
||||
ANALISA BERKAITAN DENGAN AKUN PELAPOR{" "}
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
|
|
|
|||
|
|
@ -89,8 +89,8 @@ export default function FeedbackCenter() {
|
|||
collapsible
|
||||
className="w-full"
|
||||
>
|
||||
<AccordionItem value="item-1" className="bg-white w-full">
|
||||
<AccordionTrigger className="bg-white">
|
||||
<AccordionItem value="item-1" className="bg-white dark:bg-black w-full">
|
||||
<AccordionTrigger className="bg-white dark:bg-black">
|
||||
TICKET PADA FEEDBACK CENTER{" "}
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
|
|
|
|||
|
|
@ -114,8 +114,8 @@ export default function ContentManagement() {
|
|||
collapsible
|
||||
className="w-full"
|
||||
>
|
||||
<AccordionItem value="item-1" className="bg-white w-full">
|
||||
<AccordionTrigger className="bg-white">
|
||||
<AccordionItem value="item-1" className="bg-white dark:bg-black w-full">
|
||||
<AccordionTrigger className="bg-white dark:bg-black">
|
||||
PUBLISH JADWAL PRESS CONFERENCE TERBANYAK
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
|
|
@ -147,8 +147,8 @@ export default function ContentManagement() {
|
|||
collapsible
|
||||
className="w-full"
|
||||
>
|
||||
<AccordionItem value="item-1" className="bg-white w-full">
|
||||
<AccordionTrigger className="bg-white">
|
||||
<AccordionItem value="item-1" className="bg-white dark:bg-black w-full">
|
||||
<AccordionTrigger className="bg-white dark:bg-black">
|
||||
JUMLAH PRODUKSI KONTEN UNTUK KATEGORI PRESS CONFERENCE
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
|
|
@ -180,8 +180,8 @@ export default function ContentManagement() {
|
|||
collapsible
|
||||
className="w-full"
|
||||
>
|
||||
<AccordionItem value="item-1" className="bg-white w-full">
|
||||
<AccordionTrigger className="bg-white">
|
||||
<AccordionItem value="item-1" className="bg-white dark:bg-black w-full">
|
||||
<AccordionTrigger className="bg-white dark:bg-black">
|
||||
TINGKAT INTERAKSI KONTEN UNTUK KATEGORI PRESS CONFERENCE
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
|
|
@ -213,8 +213,8 @@ export default function ContentManagement() {
|
|||
collapsible
|
||||
className="w-full"
|
||||
>
|
||||
<AccordionItem value="item-1" className="bg-white w-full">
|
||||
<AccordionTrigger className="bg-white">
|
||||
<AccordionItem value="item-1" className="bg-white dark:bg-black w-full">
|
||||
<AccordionTrigger className="bg-white dark:bg-black">
|
||||
AKTIFITAS MEDIA BERKAITAN DENGAN PERS RILIS
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
|
|
|
|||
|
|
@ -30,34 +30,27 @@ const columns: ColumnDef<any>[] = [
|
|||
accessorKey: "accountName",
|
||||
header: "Nama",
|
||||
cell: ({ row }) => (
|
||||
<span className="normal-case">{row.getValue("accountName")}</span>
|
||||
<span className="normal-case">{row.original.mediaBlastAccountName}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "accountType",
|
||||
header: "Tipe Akun",
|
||||
cell: ({ row }) => (
|
||||
<span className="normal-case">{row.getValue("accountType")}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "accountCategory",
|
||||
header: "Kategory",
|
||||
cell: ({ row }) => (
|
||||
<span className="uppercase">{row.getValue("accountCategory")}</span>
|
||||
<span className="normal-case">{row.original.mediaBlastAccountType}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "emailAddress",
|
||||
header: "Email",
|
||||
cell: ({ row }) => (
|
||||
<span className="normal-case">{row.getValue("emailAddress")}</span>
|
||||
<span className="normal-case">{row.original.mediaBlastAccountEmail}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "whatsappNumber",
|
||||
header: "Whatsapp",
|
||||
cell: ({ row }) => <span>{row.getValue("whatsappNumber")}</span>,
|
||||
cell: ({ row }) => <span>{row.original.mediaBlastAccountPhone}</span>,
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
|
|
@ -113,7 +106,7 @@ const columns: ColumnDef<any>[] = [
|
|||
<DropdownMenuContent className="p-0" align="end">
|
||||
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none">
|
||||
<Link
|
||||
href={`/admin/broadcast/campaign-list/account-list/edit/${row.original.id}`}
|
||||
href={`/admin/broadcast/campaign-list/account-list/edit/${row.original.mediaBlastAccountId}`}
|
||||
>
|
||||
Edit
|
||||
</Link>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
import * as React from "react";
|
||||
import {
|
||||
ColumnDef,
|
||||
ColumnFiltersState,
|
||||
PaginationState,
|
||||
SortingState,
|
||||
|
|
@ -15,7 +14,6 @@ import {
|
|||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
|
|
@ -24,25 +22,64 @@ import {
|
|||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { UserIcon } from "lucide-react";
|
||||
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import TablePagination from "@/components/table/table-pagination";
|
||||
import columns from "./column";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
DialogClose,
|
||||
} from "@/components/ui/dialog";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select as UISelect,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { getMediaBlastAccountPage } from "@/service/broadcast/broadcast";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { close, loading } from "@/config/swal";
|
||||
import { Icon } from "@iconify/react";
|
||||
import { useParams, useSearchParams } from "next/navigation";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { X } from "lucide-react";
|
||||
import ReactSelect from "react-select";
|
||||
|
||||
import columns from "./column";
|
||||
import TablePagination from "@/components/table/table-pagination";
|
||||
import {
|
||||
getMediaBlastCampaignAccountList,
|
||||
deleteMediaBlastCampaignAccount,
|
||||
saveMediaBlastCampaignAccountBulk,
|
||||
} from "@/service/broadcast/broadcast";
|
||||
import {
|
||||
AdministrationUserList,
|
||||
getUserListAll,
|
||||
} from "@/service/management-user/management-user";
|
||||
import { close, loading, error, success, successCallback } from "@/config/swal";
|
||||
import { Link } from "@/i18n/routing";
|
||||
import { Icon } from "@iconify/react/dist/iconify.js";
|
||||
|
||||
// Mock data for available accounts - replace with actual API call
|
||||
const availableAccounts = [
|
||||
{ id: "1", accountName: "Account 1", category: "polri" },
|
||||
{ id: "2", accountName: "Account 2", category: "jurnalis" },
|
||||
{ id: "3", accountName: "Account 3", category: "umum" },
|
||||
{ id: "4", accountName: "Account 4", category: "ksp" },
|
||||
{ id: "5", accountName: "Account 5", category: "polri" },
|
||||
];
|
||||
|
||||
const AccountListTable = () => {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const searchParams = useSearchParams();
|
||||
const campaignId = params?.id as string;
|
||||
|
||||
const [dataTable, setDataTable] = React.useState<any[]>([]);
|
||||
const [totalData, setTotalData] = React.useState<number>(1);
|
||||
const [sorting, setSorting] = React.useState<SortingState>([]);
|
||||
|
|
@ -56,10 +93,19 @@ const AccountListTable = () => {
|
|||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
const [page, setPage] = React.useState(1);
|
||||
const [totalPage, setTotalPage] = React.useState(1);
|
||||
const [filtered, setFiltered] = React.useState<string[]>([]);
|
||||
|
||||
// --- state utk Dialog Pilih Akun ---
|
||||
const [isDialogOpen, setIsDialogOpen] = React.useState(false);
|
||||
const [accountCategory, setAccountCategory] = React.useState<string>("");
|
||||
const [selectedAccount, setSelectedAccount] = React.useState<any[]>([]);
|
||||
const [selectedCategory, setSelectedCategory] = React.useState<string>("");
|
||||
const [availableAccountsList, setAvailableAccountsList] =
|
||||
React.useState<any[]>(availableAccounts);
|
||||
const [usersList, setUsersList] = React.useState<any[]>([]);
|
||||
|
||||
const table = useReactTable({
|
||||
data: dataTable,
|
||||
columns,
|
||||
|
|
@ -83,24 +129,24 @@ const AccountListTable = () => {
|
|||
|
||||
React.useEffect(() => {
|
||||
const pageFromUrl = searchParams?.get("page");
|
||||
if (pageFromUrl) {
|
||||
setPage(Number(pageFromUrl));
|
||||
}
|
||||
if (pageFromUrl) setPage(Number(pageFromUrl));
|
||||
}, [searchParams]);
|
||||
|
||||
React.useEffect(() => {
|
||||
fetchData();
|
||||
}, [page]);
|
||||
}, [page, filtered]);
|
||||
|
||||
async function fetchData() {
|
||||
try {
|
||||
loading();
|
||||
const res = await getMediaBlastAccountPage(
|
||||
const res = await getMediaBlastCampaignAccountList(
|
||||
page - 1,
|
||||
filtered ? filtered.join(",") : ""
|
||||
filtered ? filtered.join(",") : "",
|
||||
campaignId
|
||||
);
|
||||
|
||||
const data = res?.data?.data;
|
||||
const contentData = data?.content;
|
||||
const contentData = data?.content || [];
|
||||
contentData.forEach((item: any, index: number) => {
|
||||
item.no = (page - 1) * 10 + index + 1;
|
||||
});
|
||||
|
|
@ -109,42 +155,349 @@ const AccountListTable = () => {
|
|||
setTotalData(data?.totalElements);
|
||||
setTotalPage(data?.totalPages);
|
||||
close();
|
||||
} catch (error) {
|
||||
console.error("Error fetching tasks:", error);
|
||||
} catch (err) {
|
||||
console.error("Error fetching tasks:", err);
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
// --- API helpers ---
|
||||
async function doDeleteAccount(id: string) {
|
||||
loading();
|
||||
const response = await deleteMediaBlastCampaignAccount(id);
|
||||
close();
|
||||
if (response?.error) {
|
||||
error(response.message);
|
||||
return;
|
||||
}
|
||||
fetchData();
|
||||
}
|
||||
|
||||
async function saveCampaignAccount() {
|
||||
try {
|
||||
loading();
|
||||
|
||||
if (accountCategory === "all-account") {
|
||||
// Handle all accounts - send only campaignId and category "all"
|
||||
const request = {
|
||||
mediaBlastCampaignId: campaignId,
|
||||
mediaBlastAccountCategory: "all",
|
||||
};
|
||||
const response = await saveMediaBlastCampaignAccountBulk(request);
|
||||
if (response?.error) {
|
||||
error(response.message);
|
||||
return;
|
||||
}
|
||||
} else if (accountCategory === "kategori" && selectedCategory) {
|
||||
// Handle category selection - send campaignId and role-based category
|
||||
let roleId = "";
|
||||
switch (selectedCategory) {
|
||||
case "umum":
|
||||
roleId = "5";
|
||||
break;
|
||||
case "jurnalis":
|
||||
roleId = "6";
|
||||
break;
|
||||
case "polri":
|
||||
roleId = "7";
|
||||
break;
|
||||
case "ksp":
|
||||
roleId = "8";
|
||||
break;
|
||||
default:
|
||||
roleId = "5";
|
||||
}
|
||||
|
||||
const request = {
|
||||
mediaBlastCampaignId: campaignId,
|
||||
mediaBlastAccountCategory: `role-${roleId}`,
|
||||
};
|
||||
const response = await saveMediaBlastCampaignAccountBulk(request);
|
||||
if (response?.error) {
|
||||
error(response.message);
|
||||
return;
|
||||
}
|
||||
} else if (accountCategory === "custom") {
|
||||
// Handle custom selection - send campaignId and selected user IDs
|
||||
const request = {
|
||||
mediaBlastCampaignId: campaignId,
|
||||
mediaBlastAccountIds: selectedAccount.map((acc) => acc.id),
|
||||
};
|
||||
const response = await saveMediaBlastCampaignAccountBulk(request);
|
||||
if (response?.error) {
|
||||
error(response.message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
close();
|
||||
successCallback("Akun berhasil ditambahkan ke campaign!");
|
||||
resetDialogState();
|
||||
fetchData();
|
||||
} catch (err) {
|
||||
close();
|
||||
error("Terjadi kesalahan saat menyimpan akun");
|
||||
}
|
||||
}
|
||||
|
||||
const resetDialogState = () => {
|
||||
setAccountCategory("");
|
||||
setSelectedAccount([]);
|
||||
setSelectedCategory("");
|
||||
setUsersList([]);
|
||||
setIsDialogOpen(false);
|
||||
};
|
||||
|
||||
const fetchUsersList = async () => {
|
||||
try {
|
||||
loading();
|
||||
const response = await getUserListAll();
|
||||
|
||||
if (response?.data?.data?.content) {
|
||||
setUsersList(response.data.data.content);
|
||||
}
|
||||
close();
|
||||
} catch (err) {
|
||||
close();
|
||||
error("Terjadi kesalahan saat mengambil daftar user");
|
||||
}
|
||||
};
|
||||
|
||||
const handleFilter = (id: string, checked: boolean) => {
|
||||
let temp = [...filtered];
|
||||
if (checked) {
|
||||
temp = [...temp, id];
|
||||
} else {
|
||||
temp = temp.filter((a) => a !== id);
|
||||
}
|
||||
if (checked) temp = [...temp, id];
|
||||
else temp = temp.filter((a) => a !== id);
|
||||
setFiltered(temp);
|
||||
console.log("sss", temp);
|
||||
};
|
||||
|
||||
const removeSelectedAccount = (accountId: string) => {
|
||||
setSelectedAccount(selectedAccount.filter((acc) => acc.id !== accountId));
|
||||
};
|
||||
|
||||
const getFilteredAccounts = () => {
|
||||
if (accountCategory === "kategori" && selectedCategory) {
|
||||
return availableAccountsList.filter(
|
||||
(acc) => acc.category === selectedCategory
|
||||
);
|
||||
}
|
||||
return availableAccountsList;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full overflow-x-auto bg-white p-4 rounded-sm space-y-3">
|
||||
<div className="flex justify-between mb-10 items-center">
|
||||
<div className="w-full overflow-x-auto bg-white dark:bg-black p-4 rounded-sm space-y-3">
|
||||
<div className="flex justify-between mb-3 items-center">
|
||||
<p className="text-xl font-medium text-default-900">Daftar Akun</p>
|
||||
<div className="flex flex-row gap-3">
|
||||
<Link href="/admin/broadcast/campaign-list/account-list/create">
|
||||
<Button color="primary" size="md" className="text-sm">
|
||||
<Icon icon="tdesign:user-add-filled" />
|
||||
Tambah Akun
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/admin/broadcast/campaign-list/import">
|
||||
<Button color="success" size="md" className="text-sm">
|
||||
<UserIcon />
|
||||
Import Akun
|
||||
</Button>
|
||||
</Link>
|
||||
{/* === Dialog Pilih Akun === */}
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button size="sm" className="text-sm">
|
||||
<Icon icon="tdesign:user-add-filled" className="mr-2" />
|
||||
Pilih Akun
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent
|
||||
size="md"
|
||||
className="max-w-xl max-h-[80vh] overflow-y-auto"
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Pilih Akun Untuk Campaign Ini</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 my-3">
|
||||
<RadioGroup
|
||||
value={accountCategory}
|
||||
onValueChange={(val) => {
|
||||
setAccountCategory(val);
|
||||
setSelectedAccount([]);
|
||||
setSelectedCategory("");
|
||||
if (val === "custom") {
|
||||
fetchUsersList();
|
||||
}
|
||||
}}
|
||||
className="flex space-x-6"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="all-account" id="all-account" />
|
||||
<Label htmlFor="all-account">Semua Akun</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="kategori" id="kategori" />
|
||||
<Label htmlFor="kategori">Kategori</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="custom" id="custom" />
|
||||
<Label htmlFor="custom">Kustom</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
|
||||
{/* Category Selection */}
|
||||
{accountCategory === "kategori" && (
|
||||
<div className="space-y-2">
|
||||
<Label>Pilih Kategori:</Label>
|
||||
<UISelect onValueChange={(val) => setSelectedCategory(val)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Pilih kategori" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="umum">Umum</SelectItem>
|
||||
<SelectItem value="polri">Polri</SelectItem>
|
||||
<SelectItem value="ksp">KSP</SelectItem>
|
||||
<SelectItem value="jurnalis">Jurnalis</SelectItem>
|
||||
</SelectContent>
|
||||
</UISelect>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Custom Account Selection */}
|
||||
{accountCategory === "custom" && (
|
||||
<div className="space-y-3">
|
||||
<Label>Pilih User:</Label>
|
||||
<ReactSelect
|
||||
isMulti
|
||||
options={usersList.map((user: any) => ({
|
||||
value: user.id,
|
||||
label: `${user.fullname} (${user.role?.name})`,
|
||||
user: user,
|
||||
}))}
|
||||
value={selectedAccount.map((acc: any) => ({
|
||||
value: acc.id,
|
||||
label: `${acc.fullname} (${acc.role?.name})`,
|
||||
user: acc,
|
||||
}))}
|
||||
onChange={(selectedOptions: any) => {
|
||||
const selectedUsers = selectedOptions
|
||||
? selectedOptions.map((option: any) => option.user)
|
||||
: [];
|
||||
setSelectedAccount(selectedUsers);
|
||||
}}
|
||||
placeholder="Cari dan pilih user..."
|
||||
noOptionsMessage={() => "Tidak ada user ditemukan"}
|
||||
loadingMessage={() => "Memuat..."}
|
||||
isSearchable={true}
|
||||
isClearable={true}
|
||||
className="react-select"
|
||||
classNamePrefix="select"
|
||||
/>
|
||||
|
||||
{/* Selected Accounts Display */}
|
||||
{selectedAccount.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<Label>User Terpilih ({selectedAccount.length}):</Label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedAccount.map((acc) => (
|
||||
<Badge
|
||||
key={acc.id}
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
{acc.fullname}
|
||||
<X
|
||||
className="h-3 w-3 cursor-pointer"
|
||||
onClick={() => removeSelectedAccount(acc.id)}
|
||||
/>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* All Accounts Info */}
|
||||
{accountCategory === "all-account" && (
|
||||
<div className="p-3 bg-blue-50 rounded-md">
|
||||
<p className="text-sm text-blue-700">
|
||||
Semua akun akan ditambahkan ke campaign ini.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Category Accounts Info */}
|
||||
{accountCategory === "kategori" && selectedCategory && (
|
||||
<div className="p-3 bg-green-50 rounded-md">
|
||||
<p className="text-sm text-green-700">
|
||||
Semua akun dengan role "{selectedCategory.toUpperCase()}"
|
||||
akan ditambahkan.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Custom Selection Info */}
|
||||
{accountCategory === "custom" && (
|
||||
<div className="p-3 bg-purple-50 rounded-md">
|
||||
<p className="text-sm text-purple-700">
|
||||
{selectedAccount.length} user terpilih akan ditambahkan ke
|
||||
campaign ini.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
onClick={saveCampaignAccount}
|
||||
disabled={
|
||||
!accountCategory ||
|
||||
(accountCategory === "custom" &&
|
||||
selectedAccount.length < 1) ||
|
||||
(accountCategory === "kategori" && !selectedCategory)
|
||||
}
|
||||
>
|
||||
Simpan
|
||||
</Button>
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline" onClick={resetDialogState}>
|
||||
Batal
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
|
||||
{/* === Filter Akun === */}
|
||||
<div className="flex flex-row justify-end">
|
||||
{/* <div className="flex flex-row gap-4">
|
||||
<Link href="/admin/broadcast/campaign-list/account-list/create">
|
||||
<Button variant="default" className="bg-[#3f37c9] gap-2">
|
||||
<span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="22"
|
||||
height="22"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M17 13h-4v4h-2v-4H7v-2h4V7h2v4h4m2-8H5c-1.11 0-2 .89-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
Tambahkan Akun
|
||||
</Button>
|
||||
</Link>
|
||||
<Button variant="default" className="bg-[#3f37c9] gap-2">
|
||||
<span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g fill="none">
|
||||
<path d="m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z" />
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 2v6.5a1.5 1.5 0 0 0 1.5 1.5H20v10a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-1h3.414l-1.121 1.121a1 1 0 1 0 1.414 1.415l2.829-2.829a1 1 0 0 0 0-1.414l-2.829-2.828a1 1 0 1 0-1.414 1.414L7.414 17H4V4a2 2 0 0 1 2-2zM4 17v2H3a1 1 0 1 1 0-2zM14 2.043a2 2 0 0 1 1 .543L19.414 7a2 2 0 0 1 .543 1H14z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</span>
|
||||
Import Akun
|
||||
</Button>
|
||||
</div> */}
|
||||
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button size="md" variant="outline">
|
||||
|
|
@ -163,65 +516,28 @@ const AccountListTable = () => {
|
|||
</a>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 overflow-auto max-h-[300px] text-xs custom-scrollbar-table">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="accepted"
|
||||
checked={filtered.includes("polri")}
|
||||
onCheckedChange={(e) => handleFilter("polri", Boolean(e))}
|
||||
/>
|
||||
<label
|
||||
htmlFor="accepted"
|
||||
className="text-xs font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
POLRI
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="accepted"
|
||||
checked={filtered.includes("jurnalis")}
|
||||
onCheckedChange={(e) =>
|
||||
handleFilter("jurnalis", Boolean(e))
|
||||
}
|
||||
/>
|
||||
<label
|
||||
htmlFor="accepted"
|
||||
className="text-xs font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
JURNALIS
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="accepted"
|
||||
checked={filtered.includes("umum")}
|
||||
onCheckedChange={(e) => handleFilter("umum", Boolean(e))}
|
||||
/>
|
||||
<label
|
||||
htmlFor="accepted"
|
||||
className="text-xs font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
UMUM
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="accepted"
|
||||
checked={filtered.includes("ksp")}
|
||||
onCheckedChange={(e) => handleFilter("ksp", Boolean(e))}
|
||||
/>
|
||||
<label
|
||||
htmlFor="accepted"
|
||||
className="text-xs font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
KSP
|
||||
</label>
|
||||
</div>
|
||||
{["polri", "jurnalis", "umum", "ksp"].map((cat) => (
|
||||
<div key={cat} className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id={cat}
|
||||
checked={filtered.includes(cat)}
|
||||
onCheckedChange={(e) => handleFilter(cat, Boolean(e))}
|
||||
/>
|
||||
<label
|
||||
htmlFor={cat}
|
||||
className="text-xs font-medium leading-none"
|
||||
>
|
||||
{cat.toUpperCase()}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
{/* === Table Data === */}
|
||||
<Table className="overflow-hidden">
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
|
|
@ -263,6 +579,7 @@ const AccountListTable = () => {
|
|||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<TablePagination
|
||||
table={table}
|
||||
totalData={totalData}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
|||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
|
|
@ -14,55 +13,80 @@ import {
|
|||
} from "@/components/ui/form";
|
||||
import withReactContent from "sweetalert2-react-content";
|
||||
import Swal from "sweetalert2";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { format } from "date-fns";
|
||||
import { CalendarIcon } from "lucide-react";
|
||||
import { Calendar } from "@/components/ui/calendar";
|
||||
import { getOnlyDate } from "@/utils/globals";
|
||||
import {
|
||||
getMediaBlastCampaignPage,
|
||||
saveMediaBlastAccount,
|
||||
saveMediaBlastCampaign,
|
||||
} from "@/service/broadcast/broadcast";
|
||||
import { error } from "@/config/swal";
|
||||
import { useRouter } from "@/i18n/routing";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
// ----------------------------
|
||||
// ZOD SCHEMA (dinamis)
|
||||
// ----------------------------
|
||||
|
||||
const FormSchema = z.object({
|
||||
name: z.string({
|
||||
required_error: "Required",
|
||||
}),
|
||||
name: z.string({ required_error: "Required" }),
|
||||
|
||||
accountType: z
|
||||
.array(z.string())
|
||||
.refine((value) => value.some((item) => item), {
|
||||
message: "Required",
|
||||
}),
|
||||
accountCategory: z.enum(["polri", "jurnalis", "umumu", "ksp"], {
|
||||
required_error: "Required",
|
||||
}),
|
||||
email: z.string({
|
||||
required_error: "Required",
|
||||
}),
|
||||
whatsapp: z.string({
|
||||
required_error: "Required",
|
||||
}),
|
||||
});
|
||||
.min(1, "Pilih minimal satu tipe akun"),
|
||||
|
||||
email: z.string().optional(),
|
||||
whatsapp: z.string().optional(),
|
||||
|
||||
campaignId: z.string({ required_error: "Required" }),
|
||||
}).refine(
|
||||
(data) => {
|
||||
if (data.accountType.includes("email") && !data.email) return false;
|
||||
return true;
|
||||
},
|
||||
{ message: "Email wajib diisi", path: ["email"] }
|
||||
).refine(
|
||||
(data) => {
|
||||
if (data.accountType.includes("wa") && !data.whatsapp) return false;
|
||||
return true;
|
||||
},
|
||||
{ message: "Whatsapp wajib diisi", path: ["whatsapp"] }
|
||||
);
|
||||
|
||||
|
||||
// ----------------------------
|
||||
// COMPONENT
|
||||
// ----------------------------
|
||||
|
||||
export default function CreateAccountForBroadcast() {
|
||||
const MySwal = withReactContent(Swal);
|
||||
const router = useRouter();
|
||||
|
||||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
defaultValues: { accountType: [] },
|
||||
defaultValues: {
|
||||
accountType: [],
|
||||
email: "",
|
||||
whatsapp: "",
|
||||
},
|
||||
});
|
||||
|
||||
const selectedTypes = form.watch("accountType");
|
||||
const [campaigns, setCampaigns] = useState<any[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchCampaignList();
|
||||
}, []);
|
||||
|
||||
async function fetchCampaignList() {
|
||||
try {
|
||||
const res = await getMediaBlastCampaignPage(0);
|
||||
setCampaigns(res?.data?.data?.content ?? []);
|
||||
} catch (e) {
|
||||
console.log("Error fetch campaign:", e);
|
||||
}
|
||||
}
|
||||
|
||||
const onSubmit = async (data: z.infer<typeof FormSchema>) => {
|
||||
MySwal.fire({
|
||||
title: "Simpan Data",
|
||||
|
|
@ -85,10 +109,8 @@ export default function CreateAccountForBroadcast() {
|
|||
icon: "success",
|
||||
confirmButtonColor: "#3085d6",
|
||||
confirmButtonText: "OK",
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
router.push("/admin/broadcast/campaign-list/account-list");
|
||||
}
|
||||
}).then(() => {
|
||||
router.push("/admin/broadcast/campaign-list/account-list");
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -96,20 +118,21 @@ export default function CreateAccountForBroadcast() {
|
|||
const reqData = {
|
||||
accountName: data.name,
|
||||
accountType: data.accountType.join(","),
|
||||
accountCategory: data.accountCategory,
|
||||
emailAddress: data.email,
|
||||
whatsappNumber: data.whatsapp,
|
||||
emailAddress: data.email ?? "",
|
||||
whatsappNumber: data.whatsapp ?? "",
|
||||
campaignId: data.campaignId,
|
||||
};
|
||||
console.log("data", data);
|
||||
|
||||
console.log("REQ:", reqData);
|
||||
|
||||
const response = await saveMediaBlastAccount(reqData);
|
||||
if (response?.error) {
|
||||
error(response.message);
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
successSubmit();
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SiteBreadcrumb />
|
||||
|
|
@ -118,7 +141,9 @@ export default function CreateAccountForBroadcast() {
|
|||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-3 bg-white rounded-sm p-4"
|
||||
>
|
||||
<p className="fonnt-semibold">Account</p>
|
||||
<p className="font-semibold">Account</p>
|
||||
|
||||
{/* NAMA */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
|
|
@ -130,172 +155,125 @@ export default function CreateAccountForBroadcast() {
|
|||
placeholder="Masukkan nama"
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* CHECKBOX TIPE AKUN */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="accountType"
|
||||
render={() => (
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Tipe Akun</FormLabel>
|
||||
<div className="flex flex-row gap-2">
|
||||
{" "}
|
||||
<FormField
|
||||
key="wa"
|
||||
control={form.control}
|
||||
name="accountType"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem
|
||||
key="wa"
|
||||
className="flex flex-row items-start space-x-3 space-y-0"
|
||||
>
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value?.includes("wa")}
|
||||
onCheckedChange={(checked) => {
|
||||
return checked
|
||||
? field.onChange([...field.value, "wa"])
|
||||
: field.onChange(
|
||||
field.value?.filter(
|
||||
(value) => value !== "wa"
|
||||
)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">
|
||||
Whatsapp
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<FormField
|
||||
key="email"
|
||||
control={form.control}
|
||||
name="accountType"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem
|
||||
key="email"
|
||||
className="flex flex-row items-start space-x-3 space-y-0"
|
||||
>
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value?.includes("email")}
|
||||
onCheckedChange={(checked) => {
|
||||
return checked
|
||||
? field.onChange([...field.value, "email"])
|
||||
: field.onChange(
|
||||
field.value?.filter(
|
||||
(value) => value !== "email"
|
||||
)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">Email</FormLabel>
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row gap-4">
|
||||
{/* WA */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
checked={field.value.includes("wa")}
|
||||
onCheckedChange={(checked) =>
|
||||
checked
|
||||
? field.onChange([...field.value, "wa"])
|
||||
: field.onChange(field.value.filter((v) => v !== "wa"))
|
||||
}
|
||||
/>
|
||||
<label>Whatsapp</label>
|
||||
</div>
|
||||
|
||||
{/* EMAIL */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
checked={field.value.includes("email")}
|
||||
onCheckedChange={(checked) =>
|
||||
checked
|
||||
? field.onChange([...field.value, "email"])
|
||||
: field.onChange(
|
||||
field.value.filter((v) => v !== "email")
|
||||
)
|
||||
}
|
||||
/>
|
||||
<label>Email</label>
|
||||
</div>
|
||||
</div>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* FORM WHATSAPP */}
|
||||
{selectedTypes.includes("wa") && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="whatsapp"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Whatsapp</FormLabel>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Masukkan nomor Whatsapp"
|
||||
{...field}
|
||||
/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* FORM EMAIL */}
|
||||
{selectedTypes.includes("email") && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="Masukkan email"
|
||||
{...field}
|
||||
/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* CAMPAIGN */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="accountCategory"
|
||||
name="campaignId"
|
||||
render={({ field }) => (
|
||||
<FormItem className="space-y-3">
|
||||
<FormLabel>Kategori</FormLabel>
|
||||
<FormItem>
|
||||
<FormLabel>Campaign</FormLabel>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
className="flex flex-row gap-2"
|
||||
<select
|
||||
className="w-full border rounded-md p-2 text-sm"
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="polri" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">POLRI</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="jurnalis" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">JURNALIS</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="umum" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">UMUM</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="ksp" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">KSP</FormLabel>
|
||||
</FormItem>
|
||||
</RadioGroup>
|
||||
<option value="" className="text-slate-400">
|
||||
Pilih campaign
|
||||
</option>
|
||||
|
||||
{campaigns.map((c: any) => (
|
||||
<option key={c.id} value={c.id}>
|
||||
{c.title || `Campaign ${c.id}`}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Nama</FormLabel>
|
||||
<Input
|
||||
type="email"
|
||||
value={field.value}
|
||||
placeholder="Masukkan email"
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="whatsapp"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Nama</FormLabel>
|
||||
<Input
|
||||
type="number"
|
||||
value={field.value}
|
||||
placeholder="Masukkan whatsapp"
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* BUTTON */}
|
||||
<div className="flex flex-row gap-2 mt-4 pt-4">
|
||||
<Button
|
||||
size="md"
|
||||
type="button"
|
||||
variant="outline"
|
||||
color="destructive"
|
||||
className="text-xs"
|
||||
>
|
||||
<Button type="button" variant="outline" color="destructive">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button size="md" type="submit" color="primary" className="text-xs">
|
||||
<Button type="submit" color="primary">
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -304,3 +282,380 @@ export default function CreateAccountForBroadcast() {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// "use client";
|
||||
// import SiteBreadcrumb from "@/components/site-breadcrumb";
|
||||
// import { z } from "zod";
|
||||
// import { useForm } from "react-hook-form";
|
||||
// import { zodResolver } from "@hookform/resolvers/zod";
|
||||
// import {
|
||||
// Form,
|
||||
// FormControl,
|
||||
// FormField,
|
||||
// FormItem,
|
||||
// FormLabel,
|
||||
// FormMessage,
|
||||
// } from "@/components/ui/form";
|
||||
// import withReactContent from "sweetalert2-react-content";
|
||||
// import Swal from "sweetalert2";
|
||||
// import { Input } from "@/components/ui/input";
|
||||
// import { Button } from "@/components/ui/button";
|
||||
// import {
|
||||
// getMediaBlastCampaignPage,
|
||||
// saveMediaBlastAccount,
|
||||
// } from "@/service/broadcast/broadcast";
|
||||
// import { error } from "@/config/swal";
|
||||
// import { useRouter } from "@/i18n/routing";
|
||||
// import { Checkbox } from "@/components/ui/checkbox";
|
||||
// import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
// import { useEffect, useState } from "react";
|
||||
|
||||
// // const FormSchema = z.object({
|
||||
// // name: z.string({
|
||||
// // required_error: "Required",
|
||||
// // }),
|
||||
// // accountType: z
|
||||
// // .array(z.string())
|
||||
// // .refine((value) => value.some((item) => item), {
|
||||
// // message: "Required",
|
||||
// // }),
|
||||
// // accountCategory: z.enum(["polri", "jurnalis", "umum", "ksp"], {
|
||||
// // required_error: "Required",
|
||||
// // }),
|
||||
// // email: z.string({
|
||||
// // required_error: "Required",
|
||||
// // }),
|
||||
// // whatsapp: z.string({
|
||||
// // required_error: "Required",
|
||||
// // }),
|
||||
// // campaignId: z.string({ required_error: "Required" }),
|
||||
// // });
|
||||
// const FormSchema = z
|
||||
// .object({
|
||||
// name: z.string().min(1, "Required"),
|
||||
|
||||
// accountType: z.array(z.string()).refine((value) => value.length > 0, {
|
||||
// message: "Pilih minimal satu tipe akun",
|
||||
// }),
|
||||
|
||||
// accountCategory: z.enum(["polri", "jurnalis", "umum", "ksp"], {
|
||||
// required_error: "Required",
|
||||
// }),
|
||||
|
||||
// email: z.string().optional(),
|
||||
// whatsapp: z.string().optional(),
|
||||
|
||||
// campaignId: z.string().min(1, "Required"),
|
||||
// })
|
||||
// .refine(
|
||||
// (data) => {
|
||||
// if (data.accountType.includes("email")) {
|
||||
// return !!data.email && data.email.trim() !== "";
|
||||
// }
|
||||
// return true;
|
||||
// },
|
||||
// { path: ["email"], message: "Email wajib diisi" }
|
||||
// )
|
||||
// .refine(
|
||||
// (data) => {
|
||||
// if (data.accountType.includes("wa")) {
|
||||
// return !!data.whatsapp && data.whatsapp.trim() !== "";
|
||||
// }
|
||||
// return true;
|
||||
// },
|
||||
// { path: ["whatsapp"], message: "Whatsapp wajib diisi" }
|
||||
// );
|
||||
|
||||
// export default function CreateAccountForBroadcast() {
|
||||
// const MySwal = withReactContent(Swal);
|
||||
// const router = useRouter();
|
||||
// const form = useForm<z.infer<typeof FormSchema>>({
|
||||
// resolver: zodResolver(FormSchema),
|
||||
// defaultValues: { accountType: [] },
|
||||
// });
|
||||
// const selectedTypes = form.watch("accountType");
|
||||
// const [campaigns, setCampaigns] = useState<any[]>([]);
|
||||
|
||||
// useEffect(() => {
|
||||
// fetchCampaignList();
|
||||
// }, []);
|
||||
|
||||
// async function fetchCampaignList() {
|
||||
// try {
|
||||
// const res = await getMediaBlastCampaignPage(0);
|
||||
// setCampaigns(res?.data?.data?.content ?? []);
|
||||
// } catch (e) {
|
||||
// console.log("Error fetch campaign:", e);
|
||||
// }
|
||||
// }
|
||||
|
||||
// const onSubmit = async (data: z.infer<typeof FormSchema>) => {
|
||||
// MySwal.fire({
|
||||
// title: "Simpan Data",
|
||||
// text: "Apakah Anda yakin ingin menyimpan data ini?",
|
||||
// icon: "warning",
|
||||
// showCancelButton: true,
|
||||
// cancelButtonColor: "#d33",
|
||||
// confirmButtonColor: "#3085d6",
|
||||
// confirmButtonText: "Simpan",
|
||||
// }).then((result) => {
|
||||
// if (result.isConfirmed) {
|
||||
// save(data);
|
||||
// }
|
||||
// });
|
||||
// };
|
||||
|
||||
// function successSubmit() {
|
||||
// MySwal.fire({
|
||||
// title: "Sukses",
|
||||
// icon: "success",
|
||||
// confirmButtonColor: "#3085d6",
|
||||
// confirmButtonText: "OK",
|
||||
// }).then((result) => {
|
||||
// if (result.isConfirmed) {
|
||||
// router.push("/admin/broadcast/campaign-list/account-list");
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
// const save = async (data: z.infer<typeof FormSchema>) => {
|
||||
// const reqData = {
|
||||
// accountName: data.name,
|
||||
// accountType: data.accountType.join(","),
|
||||
// accountCategory: data.accountCategory,
|
||||
// emailAddress: data.email ?? "",
|
||||
// whatsappNumber: data.whatsapp ?? "",
|
||||
// campaignId: data.campaignId,
|
||||
// };
|
||||
// console.log("data", data);
|
||||
|
||||
// const response = await saveMediaBlastAccount(reqData);
|
||||
// if (response?.error) {
|
||||
// error(response.message);
|
||||
// return false;
|
||||
// }
|
||||
|
||||
// successSubmit();
|
||||
// };
|
||||
// return (
|
||||
// <div>
|
||||
// <SiteBreadcrumb />
|
||||
// <Form {...form}>
|
||||
// <form
|
||||
// onSubmit={form.handleSubmit(onSubmit)}
|
||||
// className="space-y-3 bg-white rounded-sm p-4"
|
||||
// >
|
||||
// <p className="fonnt-semibold">Account</p>
|
||||
// <FormField
|
||||
// control={form.control}
|
||||
// name="name"
|
||||
// render={({ field }) => (
|
||||
// <FormItem>
|
||||
// <FormLabel>Nama</FormLabel>
|
||||
// <Input
|
||||
// value={field.value}
|
||||
// placeholder="Masukkan nama"
|
||||
// onChange={field.onChange}
|
||||
// />
|
||||
|
||||
// <FormMessage />
|
||||
// </FormItem>
|
||||
// )}
|
||||
// />
|
||||
// <FormField
|
||||
// control={form.control}
|
||||
// name="accountType"
|
||||
// render={() => (
|
||||
// <FormItem>
|
||||
// <FormLabel>Tipe Akun</FormLabel>
|
||||
// <div className="flex flex-row gap-2">
|
||||
// {" "}
|
||||
// <FormField
|
||||
// key="wa"
|
||||
// control={form.control}
|
||||
// name="accountType"
|
||||
// render={({ field }) => {
|
||||
// return (
|
||||
// <FormItem
|
||||
// key="wa"
|
||||
// className="flex flex-row items-start space-x-3 space-y-0"
|
||||
// >
|
||||
// <FormControl>
|
||||
// <Checkbox
|
||||
// checked={field.value?.includes("wa")}
|
||||
// onCheckedChange={(checked) => {
|
||||
// return checked
|
||||
// ? field.onChange([...field.value, "wa"])
|
||||
// : field.onChange(
|
||||
// field.value?.filter(
|
||||
// (value) => value !== "wa"
|
||||
// )
|
||||
// );
|
||||
// }}
|
||||
// />
|
||||
// </FormControl>
|
||||
// <FormLabel className="font-normal">
|
||||
// Whatsapp
|
||||
// </FormLabel>
|
||||
// </FormItem>
|
||||
// );
|
||||
// }}
|
||||
// />
|
||||
// <FormField
|
||||
// key="email"
|
||||
// control={form.control}
|
||||
// name="accountType"
|
||||
// render={({ field }) => {
|
||||
// return (
|
||||
// <FormItem
|
||||
// key="email"
|
||||
// className="flex flex-row items-start space-x-3 space-y-0"
|
||||
// >
|
||||
// <FormControl>
|
||||
// <Checkbox
|
||||
// checked={field.value?.includes("email")}
|
||||
// onCheckedChange={(checked) => {
|
||||
// return checked
|
||||
// ? field.onChange([...field.value, "email"])
|
||||
// : field.onChange(
|
||||
// field.value?.filter(
|
||||
// (value) => value !== "email"
|
||||
// )
|
||||
// );
|
||||
// }}
|
||||
// />
|
||||
// </FormControl>
|
||||
// <FormLabel className="font-normal">Email</FormLabel>
|
||||
// </FormItem>
|
||||
// );
|
||||
// }}
|
||||
// />
|
||||
// </div>
|
||||
|
||||
// <FormMessage />
|
||||
// </FormItem>
|
||||
// )}
|
||||
// />
|
||||
// {/* <FormField
|
||||
// control={form.control}
|
||||
// name="accountCategory"
|
||||
// render={({ field }) => (
|
||||
// <FormItem className="space-y-3">
|
||||
// <FormLabel>Kategori</FormLabel>
|
||||
// <FormControl>
|
||||
// <RadioGroup
|
||||
// onValueChange={field.onChange}
|
||||
// defaultValue={field.value}
|
||||
// className="flex flex-row gap-2"
|
||||
// >
|
||||
// <FormItem className="flex items-center space-x-3 space-y-0">
|
||||
// <FormControl>
|
||||
// <RadioGroupItem value="polri" />
|
||||
// </FormControl>
|
||||
// <FormLabel className="font-normal">POLRI</FormLabel>
|
||||
// </FormItem>
|
||||
// <FormItem className="flex items-center space-x-3 space-y-0">
|
||||
// <FormControl>
|
||||
// <RadioGroupItem value="jurnalis" />
|
||||
// </FormControl>
|
||||
// <FormLabel className="font-normal">JURNALIS</FormLabel>
|
||||
// </FormItem>
|
||||
// <FormItem className="flex items-center space-x-3 space-y-0">
|
||||
// <FormControl>
|
||||
// <RadioGroupItem value="umum" />
|
||||
// </FormControl>
|
||||
// <FormLabel className="font-normal">UMUM</FormLabel>
|
||||
// </FormItem>
|
||||
// <FormItem className="flex items-center space-x-3 space-y-0">
|
||||
// <FormControl>
|
||||
// <RadioGroupItem value="ksp" />
|
||||
// </FormControl>
|
||||
// <FormLabel className="font-normal">KSP</FormLabel>
|
||||
// </FormItem>
|
||||
// </RadioGroup>
|
||||
// </FormControl>
|
||||
// <FormMessage />
|
||||
// </FormItem>
|
||||
// )}
|
||||
// /> */}
|
||||
// <FormField
|
||||
// control={form.control}
|
||||
// name="email"
|
||||
// render={({ field }) => (
|
||||
// <FormItem>
|
||||
// <FormLabel>Email</FormLabel>
|
||||
// <Input
|
||||
// type="email"
|
||||
// value={field.value}
|
||||
// placeholder="Masukkan email"
|
||||
// onChange={field.onChange}
|
||||
// />
|
||||
|
||||
// <FormMessage />
|
||||
// </FormItem>
|
||||
// )}
|
||||
// />
|
||||
// <FormField
|
||||
// control={form.control}
|
||||
// name="whatsapp"
|
||||
// render={({ field }) => (
|
||||
// <FormItem>
|
||||
// <FormLabel>Nama</FormLabel>
|
||||
// <Input
|
||||
// type="number"
|
||||
// value={field.value}
|
||||
// placeholder="Masukkan whatsapp"
|
||||
// onChange={field.onChange}
|
||||
// />
|
||||
|
||||
// <FormMessage />
|
||||
// </FormItem>
|
||||
// )}
|
||||
// />
|
||||
// <FormField
|
||||
// control={form.control}
|
||||
// name="campaignId"
|
||||
// render={({ field }) => (
|
||||
// <FormItem>
|
||||
// <FormLabel>Campaign</FormLabel>
|
||||
// <FormControl>
|
||||
// <select
|
||||
// className="w-full border rounded-md p-2"
|
||||
// value={field.value}
|
||||
// onChange={field.onChange}
|
||||
// >
|
||||
// <option value="" className="text-slate-400">
|
||||
// Pilih campaign
|
||||
// </option>
|
||||
|
||||
// {campaigns.map((c: any) => (
|
||||
// <option key={c.id} value={c.id}>
|
||||
// {c.title || `Campaign ${c.id}`}
|
||||
// </option>
|
||||
// ))}
|
||||
// </select>
|
||||
// </FormControl>
|
||||
// <FormMessage />
|
||||
// </FormItem>
|
||||
// )}
|
||||
// />
|
||||
// <div className="flex flex-row gap-2 mt-4 pt-4">
|
||||
// <Button
|
||||
// size="md"
|
||||
// type="button"
|
||||
// variant="outline"
|
||||
// color="destructive"
|
||||
// className="text-xs"
|
||||
// >
|
||||
// Cancel
|
||||
// </Button>
|
||||
// <Button size="md" type="submit" color="primary" className="text-xs">
|
||||
// Submit
|
||||
// </Button>
|
||||
// </div>
|
||||
// </form>
|
||||
// </Form>
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -37,46 +37,74 @@ import { Checkbox } from "@/components/ui/checkbox";
|
|||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
import {
|
||||
getUserById,
|
||||
saveUserInternal,
|
||||
} from "@/service/management-user/management-user";
|
||||
|
||||
// const FormSchema = z.object({
|
||||
// fullname: z.string({
|
||||
// required_error: "Required",
|
||||
// }),
|
||||
// // accountType: z
|
||||
// // .array(z.string())
|
||||
// // .refine((value) => value.some((item) => item), {
|
||||
// // message: "Required",
|
||||
// // }),
|
||||
// // accountCategory: z.enum(["polri", "jurnalis", "umum", "ksp"], {
|
||||
// // required_error: "Required",
|
||||
// // }),
|
||||
// email: z.string({
|
||||
// required_error: "Required",
|
||||
// }),
|
||||
// phoneNumber: z.string({
|
||||
// required_error: "Required",
|
||||
// }),
|
||||
// });
|
||||
|
||||
const FormSchema = z.object({
|
||||
name: z.string({
|
||||
required_error: "Required",
|
||||
}),
|
||||
accountType: z
|
||||
.array(z.string())
|
||||
.refine((value) => value.some((item) => item), {
|
||||
message: "Required",
|
||||
}),
|
||||
accountCategory: z.enum(["polri", "jurnalis", "umumu", "ksp"], {
|
||||
required_error: "Required",
|
||||
}),
|
||||
email: z.string({
|
||||
required_error: "Required",
|
||||
}),
|
||||
whatsapp: z.string({
|
||||
required_error: "Required",
|
||||
}),
|
||||
fullname: z.string({ required_error: "Required" }),
|
||||
email: z.string({ required_error: "Required" }),
|
||||
phoneNumber: z.string({ required_error: "Required" }),
|
||||
username: z.string().optional(),
|
||||
role: z.string().optional(),
|
||||
level: z.string().optional(),
|
||||
nrp: z.string().optional(),
|
||||
address: z.string().optional(),
|
||||
password: z.string().optional(),
|
||||
confirmPassword: z.string().optional(),
|
||||
});
|
||||
|
||||
export default function EditAccountForBroadcast() {
|
||||
const id = useParams()?.id;
|
||||
|
||||
const MySwal = withReactContent(Swal);
|
||||
const router = useRouter();
|
||||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
defaultValues: { accountType: [] },
|
||||
// defaultValues: { accountType: [] },
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
async function getDetailData() {
|
||||
const response = await getMediaBlastAccount(String(id));
|
||||
const response = await getUserById(String(id));
|
||||
const details = response?.data?.data;
|
||||
console.log("new", details);
|
||||
form.setValue("name", details.accountName);
|
||||
form.setValue("email", details?.emailAddress);
|
||||
form.setValue("whatsapp", details?.whatsappNumber);
|
||||
form.setValue("accountCategory", details?.accountCategory);
|
||||
form.setValue("accountType", details?.accountType.split(","));
|
||||
console.log("Response full:", response);
|
||||
form.setValue("fullname", details?.fullname);
|
||||
form.setValue("username", details?.username);
|
||||
form.setValue("phoneNumber", details?.phoneNumber);
|
||||
// form.setValue("nrp", details?.memberIdentity);
|
||||
// form.setValue("address", details?.address);
|
||||
form.setValue("email", details?.email);
|
||||
form.setValue("role", details?.role?.code);
|
||||
// form.setValue("level", String(details?.userLevelId));
|
||||
// form.setValue("name", details?.accountName);
|
||||
// form.setValue("fullname", details?.fullname);
|
||||
// form.setValue("email", details?.email);
|
||||
// form.setValue("phoneNumber", details?.phoneNumber);
|
||||
// form.setValue("whatsapp", details?.whatsappNumber);
|
||||
// form.setValue("accountCategory", details?.accountCategory);
|
||||
// form.setValue("accountType", details?.accountType.split(","));
|
||||
}
|
||||
|
||||
getDetailData();
|
||||
|
|
@ -106,23 +134,32 @@ export default function EditAccountForBroadcast() {
|
|||
confirmButtonText: "OK",
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
router.push("/admin/broadcast/campaign-list/account-list");
|
||||
router.push("/admin/broadcast/campaign-list");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const save = async (data: z.infer<typeof FormSchema>) => {
|
||||
const reqData = {
|
||||
id: String(id),
|
||||
accountName: data.name,
|
||||
accountType: data.accountType.join(","),
|
||||
accountCategory: data.accountCategory,
|
||||
emailAddress: data.email,
|
||||
whatsappNumber: data.whatsapp,
|
||||
id: Number(id),
|
||||
// accountName: data.fullname,
|
||||
// accountType: data.accountType.join(","),
|
||||
// accountCategory: data.accountCategory,
|
||||
// emailAddress: data.email,
|
||||
phoneNumber: data.phoneNumber,
|
||||
firstName: data.fullname,
|
||||
username: data.username,
|
||||
roleId: data.role,
|
||||
// userLevelId: Number(data.level),
|
||||
// memberIdentity: data.nrp,
|
||||
// address: data.address,
|
||||
email: data.email,
|
||||
isDefault: false,
|
||||
isAdmin: true,
|
||||
};
|
||||
console.log("data", data);
|
||||
|
||||
const response = await saveMediaBlastAccount(reqData);
|
||||
const response = await saveUserInternal(reqData);
|
||||
if (response?.error) {
|
||||
error(response.message);
|
||||
return false;
|
||||
|
|
@ -141,7 +178,7 @@ export default function EditAccountForBroadcast() {
|
|||
<p className="fonnt-semibold">Account</p>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
name="fullname"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Nama</FormLabel>
|
||||
|
|
@ -155,7 +192,7 @@ export default function EditAccountForBroadcast() {
|
|||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
{/* <FormField
|
||||
control={form.control}
|
||||
name="accountType"
|
||||
render={() => (
|
||||
|
|
@ -227,8 +264,8 @@ export default function EditAccountForBroadcast() {
|
|||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
/> */}
|
||||
{/* <FormField
|
||||
control={form.control}
|
||||
name="accountCategory"
|
||||
render={({ field }) => (
|
||||
|
|
@ -269,13 +306,13 @@ export default function EditAccountForBroadcast() {
|
|||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
/> */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Nama</FormLabel>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<Input
|
||||
type="email"
|
||||
value={field.value}
|
||||
|
|
@ -289,14 +326,14 @@ export default function EditAccountForBroadcast() {
|
|||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="whatsapp"
|
||||
name="phoneNumber"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Nama</FormLabel>
|
||||
<FormLabel>Nomor Whatsapp</FormLabel>
|
||||
<Input
|
||||
type="number"
|
||||
value={field.value}
|
||||
placeholder="Masukkan whatsapp"
|
||||
placeholder="Masukkan nomor whatsapp"
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -11,8 +11,11 @@ import {
|
|||
} from "@/components/ui/dropdown-menu";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
||||
import { Link, useRouter } from "@/i18n/routing";
|
||||
import { close, error, loading, success } from "@/config/swal";
|
||||
import { deleteMediaBlastCampaign } from "@/service/broadcast/broadcast";
|
||||
import Swal from "sweetalert2";
|
||||
import withReactContent from "sweetalert2-react-content";
|
||||
|
||||
const columns: ColumnDef<any>[] = [
|
||||
{
|
||||
|
|
@ -58,10 +61,47 @@ const columns: ColumnDef<any>[] = [
|
|||
|
||||
{
|
||||
id: "actions",
|
||||
accessorKey: "action",
|
||||
header: "Actions",
|
||||
enableHiding: false,
|
||||
cell: ({ row }) => {
|
||||
cell: ({ row, onDeleteSuccess }: any) => {
|
||||
const MySwal = withReactContent(Swal);
|
||||
|
||||
const handleDelete = (id: number) => {
|
||||
MySwal.fire({
|
||||
title: "Apakah anda ingin menghapus data?",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: "#dc3545",
|
||||
confirmButtonText: "Iya",
|
||||
cancelButtonText: "Tidak",
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
doDeleteAccount(id);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
async function doDeleteAccount(id: number) {
|
||||
loading();
|
||||
const response = await deleteMediaBlastCampaign(id);
|
||||
close();
|
||||
|
||||
if (response.error) {
|
||||
error(response.message);
|
||||
return false;
|
||||
}
|
||||
console.log("Delete response:", response);
|
||||
|
||||
MySwal.fire({
|
||||
icon: "success",
|
||||
title: "Berhasil!",
|
||||
text: "Data berhasil dihapus.",
|
||||
confirmButtonColor: "#3085d6",
|
||||
timer: 2000,
|
||||
timerProgressBar: true,
|
||||
});
|
||||
// ✅ panggil callback dari parent
|
||||
onDeleteSuccess?.(id);
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
|
|
@ -73,28 +113,113 @@ const columns: ColumnDef<any>[] = [
|
|||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="p-0" align="end">
|
||||
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none">
|
||||
<Link
|
||||
href={`/admin/broadcast/campaign-list/detail/${row.original.id}`}
|
||||
>
|
||||
<Link
|
||||
href={`/admin/broadcast/campaign-list/detail/${row.original.id}`}
|
||||
>
|
||||
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none cursor-pointer">
|
||||
Detail
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none">
|
||||
<Link
|
||||
href={`//admin/broadcast/campaign-list/edit/${row.original.id}`}
|
||||
>
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<Link
|
||||
href={`//admin/broadcast/campaign-list/edit/${row.original.id}`}
|
||||
>
|
||||
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none cursor-pointer">
|
||||
Edit
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none">
|
||||
<a>Delete</a>
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleDelete(row.original.id)}
|
||||
className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none cursor-pointer"
|
||||
>
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
// {
|
||||
// id: "actions",
|
||||
// accessorKey: "action",
|
||||
// header: "Actions",
|
||||
// enableHiding: false,
|
||||
// cell: ({ row, onDeleteSuccess }: any) => {
|
||||
// const MySwal = withReactContent(Swal);
|
||||
|
||||
// const handleDelete = (id: any) => {
|
||||
// MySwal.fire({
|
||||
// title: "Apakah anda ingin menghapus data?",
|
||||
// showCancelButton: true,
|
||||
// confirmButtonColor: "#dc3545",
|
||||
// confirmButtonText: "Iya",
|
||||
// cancelButtonText: "Tidak",
|
||||
// }).then((result: any) => {
|
||||
// if (result.isConfirmed) {
|
||||
// doDeleteAccount(id);
|
||||
// }
|
||||
// });
|
||||
// };
|
||||
|
||||
// async function doDeleteAccount(id: any) {
|
||||
// loading();
|
||||
// const response = await deleteMediaBlastCampaign(id);
|
||||
// close();
|
||||
|
||||
// if (response.error) {
|
||||
// error(response.message);
|
||||
// return false;
|
||||
// }
|
||||
// console.log("Delete response:", response);
|
||||
|
||||
// MySwal.fire({
|
||||
// icon: "success",
|
||||
// title: "Berhasil!",
|
||||
// text: "Data berhasil dihapus.",
|
||||
// confirmButtonColor: "#3085d6",
|
||||
// timer: 2000,
|
||||
// timerProgressBar: true,
|
||||
// });
|
||||
// // ✅ langsung hapus dari state
|
||||
// onDeleteSuccess?.(id);
|
||||
// }
|
||||
|
||||
// return (
|
||||
// <DropdownMenu>
|
||||
// <DropdownMenuTrigger asChild>
|
||||
// <Button
|
||||
// size="icon"
|
||||
// className="bg-transparent ring-offset-transparent hover:bg-transparent hover:ring-0 hover:ring-transparent"
|
||||
// >
|
||||
// <MoreVertical className="h-4 w-4 text-default-800" />
|
||||
// </Button>
|
||||
// </DropdownMenuTrigger>
|
||||
// <DropdownMenuContent className="p-0" align="end">
|
||||
// <Link
|
||||
// href={`/admin/broadcast/campaign-list/detail/${row.original.id}`}
|
||||
// >
|
||||
// <DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none cursor-pointer">
|
||||
// Detail
|
||||
// </DropdownMenuItem>
|
||||
// </Link>
|
||||
// <Link
|
||||
// href={`//admin/broadcast/campaign-list/edit/${row.original.id}`}
|
||||
// >
|
||||
// <DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none cursor-pointer">
|
||||
// Edit
|
||||
// </DropdownMenuItem>
|
||||
// </Link>
|
||||
// <DropdownMenuItem
|
||||
// onClick={() => handleDelete(row.original.id)}
|
||||
// className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none cursor-pointer"
|
||||
// >
|
||||
// Delete
|
||||
// </DropdownMenuItem>
|
||||
// </DropdownMenuContent>
|
||||
// </DropdownMenu>
|
||||
// );
|
||||
// },
|
||||
// },
|
||||
];
|
||||
|
||||
export default columns;
|
||||
|
|
|
|||
|
|
@ -89,6 +89,11 @@ const CampaignListTable = () => {
|
|||
|
||||
const [page, setPage] = React.useState(1);
|
||||
const [totalPage, setTotalPage] = React.useState(1);
|
||||
|
||||
function handleDeleteSuccess(id: number) {
|
||||
setDataTable((prev) => prev.filter((item) => item.id !== id));
|
||||
}
|
||||
|
||||
const table = useReactTable({
|
||||
data: dataTable,
|
||||
columns,
|
||||
|
|
@ -143,16 +148,16 @@ const CampaignListTable = () => {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="w-full overflow-x-auto bg-white p-4 rounded-sm space-y-3">
|
||||
<div className="w-full overflow-x-auto bg-white dark:bg-black p-4 rounded-sm space-y-3">
|
||||
<div className="flex justify-between mb-10 items-center">
|
||||
<p className="text-xl font-medium text-default-900">Daftar Campaign</p>
|
||||
<div className="flex flex-row gap-2">
|
||||
<Link href="/admin/broadcast/campaign-list/account-list">
|
||||
{/* <Link href="/admin/broadcast/campaign-list/account-list">
|
||||
<Button color="primary" size="md" className="text-sm">
|
||||
<UserIcon />
|
||||
Daftar Akun
|
||||
</Button>
|
||||
</Link>
|
||||
</Link> */}
|
||||
<Link href="/admin/broadcast/campaign-list/create">
|
||||
<Button color="primary" size="md" className="text-sm">
|
||||
<NewCampaignIcon size={23} />
|
||||
|
|
@ -189,7 +194,14 @@ const CampaignListTable = () => {
|
|||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
{/* {flexRender(cell.column.columnDef.cell, cell.getContext())} */}
|
||||
{flexRender(cell.column.columnDef.cell, {
|
||||
...cell.getContext(),
|
||||
onDeleteSuccess: (id: number) => {
|
||||
// setDataTable((prev) => prev.filter((item) => item.id !== id)
|
||||
fetchData()
|
||||
},
|
||||
})}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ export default function CreateCampaign() {
|
|||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-3 bg-white rounded-sm p-4"
|
||||
className="space-y-3 bg-white dark:bg-black rounded-sm p-4"
|
||||
>
|
||||
<p className="fonnt-semibold">Campaign</p>
|
||||
<FormField
|
||||
|
|
|
|||
|
|
@ -0,0 +1,406 @@
|
|||
"use client";
|
||||
|
||||
import "react-datepicker/dist/react-datepicker.css";
|
||||
import { Icon } from "@iconify/react";
|
||||
import Link from "next/link";
|
||||
import { useRouter, usePathname } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useMediaQuery } from "react-responsive";
|
||||
import { close, loading } from "@/config/swal";
|
||||
import ReactDatePicker from "react-datepicker";
|
||||
import { getOnlyDate } from "@/utils/globals";
|
||||
import AccountListTable from "../../account-list/component/table";
|
||||
import {
|
||||
ColumnDef,
|
||||
ColumnFiltersState,
|
||||
PaginationState,
|
||||
SortingState,
|
||||
VisibilityState,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getFilteredRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import TablePagination from "@/components/table/table-pagination";
|
||||
import {
|
||||
getMediaBlastCampaignById,
|
||||
getMediaBlastBroadcastList,
|
||||
} from "@/service/broadcast/broadcast";
|
||||
|
||||
// Types
|
||||
interface CampaignData {
|
||||
id: string;
|
||||
no: number;
|
||||
mediaBlastCampaignId: string;
|
||||
mediaBlastCampaign: {
|
||||
title: string;
|
||||
};
|
||||
subject: string;
|
||||
type: string;
|
||||
status: string;
|
||||
sendDate: string;
|
||||
}
|
||||
|
||||
interface PaginatedResponse {
|
||||
content: CampaignData[];
|
||||
totalPages: number;
|
||||
totalElements: number;
|
||||
}
|
||||
|
||||
interface PageProps {
|
||||
params: {
|
||||
id: string;
|
||||
locale: string;
|
||||
};
|
||||
searchParams: {
|
||||
page?: string;
|
||||
size?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default function BroadcastCampaignDetail({
|
||||
params,
|
||||
searchParams,
|
||||
}: PageProps) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const { id, locale } = params;
|
||||
const [getData, setGetData] = useState<CampaignData[]>([]);
|
||||
const [totalPage, setTotalPage] = useState<number>(0);
|
||||
const [totalData, setTotalData] = useState<number>(0);
|
||||
const [activeTab, setActiveTab] = useState<
|
||||
"sent" | "schedule" | "account-list"
|
||||
>("sent");
|
||||
const { page, size } = searchParams;
|
||||
const [calenderState, setCalenderState] = useState<boolean>(false);
|
||||
const [typeFilter, setTypeFilter] = useState<string>("email");
|
||||
const [dateRange, setDateRange] = useState<[Date, Date]>([
|
||||
new Date(),
|
||||
new Date(),
|
||||
]);
|
||||
const [startDate, endDate] = dateRange;
|
||||
const [startDateString, setStartDateString] = useState<string | undefined>();
|
||||
const [endDateString, setEndDateString] = useState<string | undefined>();
|
||||
// Table state
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
||||
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
|
||||
const [rowSelection, setRowSelection] = useState({});
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: parseInt(size || "10"),
|
||||
});
|
||||
const pages = page ? parseInt(page) - 1 : 0;
|
||||
const currentPage = page ? parseInt(page) : 1;
|
||||
const pageSize = parseInt(size || "10");
|
||||
|
||||
const isFHD = useMediaQuery({
|
||||
minWidth: 1920,
|
||||
});
|
||||
|
||||
const setCurrentPage = (pageNumber: number) => {
|
||||
const params = new URLSearchParams(searchParams);
|
||||
params.set("page", pageNumber.toString());
|
||||
router.push(`${pathname}?${params.toString()}`);
|
||||
};
|
||||
|
||||
async function getListPaginationData() {
|
||||
loading();
|
||||
console.log("Type : ", typeFilter);
|
||||
console.log("Date : ", startDateString, endDateString);
|
||||
|
||||
try {
|
||||
const res = await getMediaBlastBroadcastList(
|
||||
pages,
|
||||
activeTab === "schedule",
|
||||
startDateString || "",
|
||||
endDateString || "",
|
||||
typeFilter,
|
||||
id
|
||||
);
|
||||
|
||||
close();
|
||||
if (res?.data?.data) {
|
||||
setupData(res.data.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getListPaginationData();
|
||||
}, [
|
||||
currentPage,
|
||||
pageSize,
|
||||
activeTab,
|
||||
endDateString,
|
||||
startDateString,
|
||||
typeFilter,
|
||||
]);
|
||||
|
||||
function setupData(rawData: PaginatedResponse) {
|
||||
console.log("raw", rawData);
|
||||
if (rawData !== undefined) {
|
||||
const dataContent = rawData?.content;
|
||||
const data: CampaignData[] = [];
|
||||
|
||||
dataContent.forEach((element, i) => {
|
||||
element.no = (currentPage - 1) * pageSize + i + 1;
|
||||
data.push(element);
|
||||
});
|
||||
|
||||
setGetData(data);
|
||||
setTotalPage(rawData?.totalPages);
|
||||
setTotalData(rawData?.totalElements);
|
||||
}
|
||||
}
|
||||
|
||||
const columns: ColumnDef<CampaignData>[] = [
|
||||
{
|
||||
accessorKey: "no",
|
||||
header: "No",
|
||||
cell: ({ row }) => <span>{row.getValue("no")}</span>,
|
||||
},
|
||||
{
|
||||
accessorKey: "mediaBlastCampaign.title",
|
||||
header: "Campaign",
|
||||
cell: ({ row }) => (
|
||||
<Link
|
||||
href={`/${locale}/admin/broadcast/campaign-list/detail/${row.original.mediaBlastCampaignId}`}
|
||||
className="text-dark"
|
||||
>
|
||||
<span className="font-weight-bold">
|
||||
{row.original.mediaBlastCampaign?.title}
|
||||
</span>
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "subject",
|
||||
header: "Judul",
|
||||
cell: ({ row }) => (
|
||||
<Link
|
||||
href={`/${locale}/admin/broadcast/content/detail/${row.original.id}`}
|
||||
className="text-dark"
|
||||
>
|
||||
<span className="font-weight-bold">{row.getValue("subject")}</span>
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "type",
|
||||
header: "Tipe",
|
||||
cell: ({ row }) => (
|
||||
<div className="text-right text-black">{row.getValue("type")}</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "status",
|
||||
header: "Status",
|
||||
cell: ({ row }) => (
|
||||
<div className="text-right text-black">{row.getValue("status")}</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "sendDate",
|
||||
header: "Tanggal & Waktu",
|
||||
cell: ({ row }) => (
|
||||
<div className="text-black">{row.getValue("sendDate")}</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const table = useReactTable({
|
||||
data: getData,
|
||||
columns,
|
||||
onSortingChange: setSorting,
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
onColumnVisibilityChange: setColumnVisibility,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
onPaginationChange: setPagination,
|
||||
state: {
|
||||
sorting,
|
||||
columnFilters,
|
||||
columnVisibility,
|
||||
rowSelection,
|
||||
pagination,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
function initState() {
|
||||
if (startDate != null && endDate != null) {
|
||||
setStartDateString(getOnlyDate(startDate));
|
||||
setEndDateString(getOnlyDate(endDate));
|
||||
}
|
||||
}
|
||||
console.log("date range", dateRange);
|
||||
initState();
|
||||
}, [calenderState, startDate, endDate]);
|
||||
|
||||
const handleTypeFilter = (type: string) => {
|
||||
setTypeFilter(type);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-black container-fluid rounded ">
|
||||
<div className="mt-1 p-4">
|
||||
<div className="flex flex-row gap-1 border-2 rounded-md w-fit mb-4">
|
||||
<Button
|
||||
onClick={() => setActiveTab("sent")}
|
||||
size="md"
|
||||
className={`hover:text-white ${
|
||||
activeTab === "sent"
|
||||
? "bg-indigo-600 text-white "
|
||||
: "bg-white text-black "
|
||||
}`}
|
||||
>
|
||||
Sent
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setActiveTab("schedule")}
|
||||
size="md"
|
||||
className={`hover:text-white ${
|
||||
activeTab === "schedule"
|
||||
? "bg-indigo-600 text-white "
|
||||
: "bg-white text-black "
|
||||
}`}
|
||||
>
|
||||
Schedule
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setActiveTab("account-list")}
|
||||
size="md"
|
||||
className={`hover:text-white ${
|
||||
activeTab === "account-list"
|
||||
? "bg-indigo-600 text-white "
|
||||
: "bg-white text-black "
|
||||
}`}
|
||||
>
|
||||
List Akun
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{activeTab === "account-list" ? (
|
||||
<AccountListTable />
|
||||
) : (
|
||||
<>
|
||||
<div className="broadcast-filter flex flex-column gap-3 mb-4">
|
||||
<div className="flex flex-row gap-1 border-2 rounded-md w-fit h-fit">
|
||||
<Button
|
||||
onClick={() => handleTypeFilter("email")}
|
||||
className={`hover:text-white ${
|
||||
typeFilter === "email"
|
||||
? "bg-black text-white "
|
||||
: "bg-white text-black "
|
||||
}`}
|
||||
size="sm"
|
||||
>
|
||||
Email Blast
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => handleTypeFilter("wa")}
|
||||
className={`hover:text-white ${
|
||||
typeFilter === "wa"
|
||||
? "bg-black text-white "
|
||||
: "bg-white text-black "
|
||||
}`}
|
||||
size="sm"
|
||||
>
|
||||
WhatsApp Blast
|
||||
</Button>
|
||||
</div>
|
||||
<div className="dashboard-date-picker">
|
||||
<div className="mx-6 my-1">
|
||||
<ReactDatePicker
|
||||
selectsRange
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
onChange={(update) => {
|
||||
setDateRange(update as [Date, Date]);
|
||||
}}
|
||||
placeholderText="Pilih Tanggal"
|
||||
onCalendarClose={() => setCalenderState(!calenderState)}
|
||||
className="form-control rounded-pill"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full overflow-x-auto">
|
||||
<Table className="overflow-hidden mt-3">
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id} className="bg-default-200">
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableHead key={header.id}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
className="h-[75px]"
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={columns.length}
|
||||
className="h-24 text-center"
|
||||
>
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<TablePagination
|
||||
table={table}
|
||||
totalData={totalData}
|
||||
totalPage={totalPage}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -130,7 +130,7 @@ export default function EditCampaign() {
|
|||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-3 bg-white rounded-sm p-4"
|
||||
className="space-y-3 bg-white dark:bg-black rounded-sm p-4"
|
||||
>
|
||||
<p className="fonnt-semibold">Campaign</p>
|
||||
<FormField
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
import DetailContentBlast from "@/components/form/broadcast/content-blast--detail-form";
|
||||
import SiteBreadcrumb from "@/components/site-breadcrumb";
|
||||
|
||||
export default function DetailEmailBlast() {
|
||||
return (
|
||||
<div>
|
||||
<SiteBreadcrumb />
|
||||
<DetailContentBlast />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ export default function CreateEmailBlast() {
|
|||
return (
|
||||
<div>
|
||||
<SiteBreadcrumb />
|
||||
<ContentBlast type="email" />
|
||||
<ContentBlast />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -85,23 +85,21 @@ const columns: ColumnDef<any>[] = [
|
|||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="p-0" align="end">
|
||||
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none">
|
||||
<Link
|
||||
href={`/contributor/content/image/detail/${row.original.id}`}
|
||||
>
|
||||
<Link href={`/contributor/content/image/detail/${row.original.id}`}>
|
||||
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none cursor-pointer">
|
||||
Detail
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none">
|
||||
<Link href={`/admin/broadcast/email/${row.original.id}`}>
|
||||
Email Blast
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none">
|
||||
<Link href={`/admin/broadcast/whatsapp/${row.original.id}`}>
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<Link href={`/admin/broadcast/create/${row.original.id}`}>
|
||||
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none cursor-pointer">
|
||||
Email & Whatsapp Blast
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
{/* <Link href={`/admin/broadcast/whatsapp/${row.original.id}`}>
|
||||
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none cursor-pointer">
|
||||
Whatsapp Blast
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuItem>
|
||||
</Link> */}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
|
|
@ -204,7 +204,7 @@ const BroadcastEmailTable = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="w-full overflow-x-auto bg-white p-4 rounded-sm space-y-3">
|
||||
<div className="w-full overflow-x-auto bg-white dark:bg-black p-4 rounded-sm space-y-3">
|
||||
<div className="flex justify-between ">
|
||||
<Link href="/admin/broadcast/campaign-list" className="mr-3">
|
||||
<Button color="primary" size="md" className="text-sm">
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
import SiteBreadcrumb from "@/components/site-breadcrumb";
|
||||
import BroadcastTable from "./email/component/table";
|
||||
import BroadcastTable from "./create/component/table";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
|
||||
import EscalationTable from "../../shared/communication/escalation/components/escalation-table";
|
||||
|
|
@ -8,7 +8,7 @@ import InternalTable from "../../shared/communication/internal/components/intern
|
|||
import { useState } from "react";
|
||||
import { Link } from "@/i18n/routing";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import BroadcastEmailTable from "./email/component/table";
|
||||
import BroadcastEmailTable from "./create/component/table";
|
||||
import BroadcastWhatsAppTable from "./whatsapp/component/table";
|
||||
|
||||
export default function AdminBroadcast() {
|
||||
|
|
@ -16,33 +16,7 @@ export default function AdminBroadcast() {
|
|||
return (
|
||||
<div>
|
||||
<SiteBreadcrumb />
|
||||
<div className="w-full overflow-x-auto bg-white p-4 rounded-sm space-y-3">
|
||||
<div className="flex flex-row gap-1 border-2 rounded-md w-fit mb-5">
|
||||
<Button
|
||||
rounded="md"
|
||||
onClick={() => setTab("Email Blast")}
|
||||
className={` hover:text-white
|
||||
${
|
||||
tab === "Email Blast"
|
||||
? "bg-black text-white "
|
||||
: "bg-white text-black "
|
||||
}`}
|
||||
>
|
||||
Email Blast
|
||||
</Button>
|
||||
<Button
|
||||
rounded="md"
|
||||
onClick={() => setTab("WhatsApp Blast")}
|
||||
className={` hover:text-white
|
||||
${
|
||||
tab === "WhatsApp Blast"
|
||||
? "bg-black text-white "
|
||||
: "bg-white text-black "
|
||||
}`}
|
||||
>
|
||||
WhatsApp Blast
|
||||
</Button>
|
||||
</div>
|
||||
<div className="w-full overflow-x-auto bg-white dark:bg-black p-4 rounded-sm space-y-3">
|
||||
{tab === "Email Blast" && <BroadcastEmailTable />}
|
||||
{tab === "WhatsApp Blast" && <BroadcastWhatsAppTable />}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ export default function CreateWABlast() {
|
|||
return (
|
||||
<div>
|
||||
<SiteBreadcrumb />
|
||||
<ContentBlast type="wa" />
|
||||
{/* <ContentBlast /> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,23 +80,21 @@ const columns: ColumnDef<any>[] = [
|
|||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="p-0" align="end">
|
||||
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none">
|
||||
<Link
|
||||
href={`/contributor/content/image/detail/${row.original.id}`}
|
||||
>
|
||||
<Link href={`/contributor/content/image/detail/${row.original.id}`}>
|
||||
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none cursor-pointer">
|
||||
Detail
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none">
|
||||
<Link href={`/admin/broadcast/email/${row.original.id}`}>
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<Link href={`/admin/broadcast/email/${row.original.id}`}>
|
||||
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none cursor-pointer">
|
||||
Email Blast
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none">
|
||||
<Link href={`/admin/broadcast/whatsapp/${row.original.id}`}>
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<Link href={`/admin/broadcast/whatsapp/${row.original.id}`}>
|
||||
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none cursor-pointer">
|
||||
Whatsapp Blast
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -76,9 +76,6 @@ export default function EditUserForm() {
|
|||
},
|
||||
});
|
||||
|
||||
const passwordVal = form.watch("password");
|
||||
const confPasswordVal = form.watch("confirmPassword");
|
||||
|
||||
useEffect(() => {
|
||||
initData();
|
||||
}, []);
|
||||
|
|
|
|||
|
|
@ -353,8 +353,8 @@ export default function EditUserForm() {
|
|||
<PasswordChecklist
|
||||
rules={["minLength", "specialChar", "number", "capital", "match"]}
|
||||
minLength={8}
|
||||
value={passwordVal}
|
||||
valueAgain={confPasswordVal}
|
||||
value={passwordVal || ""}
|
||||
valueAgain={confPasswordVal || ""}
|
||||
onChange={(isValid) => {
|
||||
form.setValue("isValidPassword", isValid);
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,9 @@
|
|||
|
||||
import SiteBreadcrumb from "@/components/site-breadcrumb";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Check, ChevronsUpDown } from "lucide-react";
|
||||
import { Check, ChevronsUpDown, Eye, EyeOff } from "lucide-react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
import { cn, getCookiesDecrypt } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
|
|
@ -174,7 +173,8 @@ export default function CreateUserForm() {
|
|||
const MySwal = withReactContent(Swal);
|
||||
const levelName = getCookiesDecrypt("ulnae");
|
||||
const [roleList, setRoleList] = useState<RoleData[]>([]);
|
||||
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||
const [userEducations, setUserEducations] = useState<any>();
|
||||
const [userSchools, setUserSchools] = useState<any>();
|
||||
const [userCompetencies, setUserCompetencies] = useState<any>();
|
||||
|
|
@ -289,7 +289,17 @@ export default function CreateUserForm() {
|
|||
};
|
||||
|
||||
if (data.role == "OPT-ID") {
|
||||
req.handledSocialMedia = data?.sns ? data.sns.join(",") : "";
|
||||
// req.handledSocialMedia = data?.sns ? data.sns.join(",") : "";
|
||||
if (data.role == "OPT-ID") {
|
||||
let snsValue = data?.sns ? data.sns.join(",") : "";
|
||||
|
||||
// ✅ Jika hanya 1 value → tambahkan koma agar backend tidak error
|
||||
if (data?.sns && data.sns.length === 1) {
|
||||
snsValue = snsValue + ",";
|
||||
}
|
||||
|
||||
req.handledSocialMedia = snsValue;
|
||||
}
|
||||
}
|
||||
|
||||
if (data.role == "KUR-ID") {
|
||||
|
|
@ -342,7 +352,7 @@ export default function CreateUserForm() {
|
|||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-6 bg-white p-10 w-full"
|
||||
className="space-y-6 bg-white dark:bg-black p-10 w-full"
|
||||
>
|
||||
<p className="text-xl">Data Pengelola Media Hub</p>
|
||||
<FormField
|
||||
|
|
@ -694,18 +704,26 @@ export default function CreateUserForm() {
|
|||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Masukkan kata sandi"
|
||||
{...field}
|
||||
className="w-1/2"
|
||||
/>
|
||||
<div className="relative w-1/2">
|
||||
<Input
|
||||
type={showPassword ? "text" : "password"}
|
||||
placeholder="Masukkan kata sandi"
|
||||
{...field}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 text-gray-500"
|
||||
>
|
||||
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
|
||||
</button>
|
||||
</div>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="confirmPassword"
|
||||
|
|
@ -713,23 +731,37 @@ export default function CreateUserForm() {
|
|||
<FormItem>
|
||||
<FormLabel>Konfirmasi Kata Sandi</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Masukkan kata sandi"
|
||||
{...field}
|
||||
className="w-1/2"
|
||||
/>
|
||||
<div className="relative w-1/2">
|
||||
<Input
|
||||
type={showConfirmPassword ? "text" : "password"}
|
||||
placeholder="Masukkan kata sandi"
|
||||
{...field}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setShowConfirmPassword(!showConfirmPassword)
|
||||
}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 text-gray-500"
|
||||
>
|
||||
{showConfirmPassword ? (
|
||||
<EyeOff size={18} />
|
||||
) : (
|
||||
<Eye size={18} />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<PasswordChecklist
|
||||
rules={["minLength", "specialChar", "number", "capital", "match"]}
|
||||
minLength={8}
|
||||
value={passwordVal}
|
||||
valueAgain={confPasswordVal}
|
||||
value={passwordVal || ""}
|
||||
valueAgain={confPasswordVal || ""}
|
||||
onChange={(isValid) => {
|
||||
form.setValue("isValidPassword", isValid);
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -191,8 +191,6 @@ export default function DetailUserForm() {
|
|||
},
|
||||
});
|
||||
|
||||
const passwordVal = form.watch("password");
|
||||
const confPasswordVal = form.watch("confirmPassword");
|
||||
const selectedRole = form.watch("role");
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -221,7 +221,6 @@ export default function EditUserForm() {
|
|||
form.setValue("level", String(res?.userLevelId));
|
||||
} else {
|
||||
initFetch();
|
||||
console.log("sadad", res?.role?.code);
|
||||
form.setValue("fullname", res?.fullname);
|
||||
form.setValue("username", res?.username);
|
||||
form.setValue("phoneNumber", res?.phoneNumber);
|
||||
|
|
@ -756,8 +755,8 @@ export default function EditUserForm() {
|
|||
<PasswordChecklist
|
||||
rules={["minLength", "specialChar", "number", "capital", "match"]}
|
||||
minLength={8}
|
||||
value={passwordVal}
|
||||
valueAgain={confPasswordVal}
|
||||
value={passwordVal || ""}
|
||||
valueAgain={confPasswordVal || ""}
|
||||
onChange={(isValid) => {
|
||||
form.setValue("isValidPassword", isValid);
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export default function ManagementUser() {
|
|||
<ManagementUserVisualization />
|
||||
</section>
|
||||
|
||||
<section className="flex flex-col gap-2 bg-white rounded-lg p-3 mt-5">
|
||||
<section className="flex flex-col gap-2 bg-white dark:bg-black rounded-lg p-3 mt-5">
|
||||
<div className="flex justify-between py-3">
|
||||
<p className="text-lg">
|
||||
Data User {isInternal ? "Internal" : "Eksternal"}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ import {
|
|||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Collapsible, CollapsibleContent } from "@/components/ui/collapsible";
|
||||
import { validateMediaLink } from "@/service/media-tracking/media-tracking";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
const columns: ColumnDef<any>[] = [
|
||||
{
|
||||
|
|
@ -52,12 +54,132 @@ const columns: ColumnDef<any>[] = [
|
|||
<span className="normal-case">{row.getValue("title")}</span>
|
||||
),
|
||||
},
|
||||
// {
|
||||
// accessorKey: "link",
|
||||
// header: "Link Berita",
|
||||
// cell: ({ row }) => (
|
||||
// <span className="normal-case">{row.getValue("link")}</span>
|
||||
// ),
|
||||
// },
|
||||
{
|
||||
accessorKey: "link",
|
||||
header: "Link Berita",
|
||||
cell: ({ row }) => (
|
||||
<span className="normal-case">{row.getValue("link")}</span>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const link = row.getValue<string>("link");
|
||||
|
||||
if (!link) {
|
||||
return <span className="text-muted-foreground">-</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 underline hover:text-blue-800 break-all"
|
||||
>
|
||||
{link}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "validation",
|
||||
header: "Validasi",
|
||||
cell: ({ row, table }) => {
|
||||
const original = row.original;
|
||||
|
||||
// const isValid = original.isValid;
|
||||
const isRelevant = original.isRelevant;
|
||||
const link = original.link;
|
||||
|
||||
const updateRow = (data: Partial<any>) => {
|
||||
table.options.meta?.updateData(row.index, data);
|
||||
};
|
||||
|
||||
const handleValid = async () => {
|
||||
try {
|
||||
await validateMediaLink(original.id, true);
|
||||
updateRow({
|
||||
isRelevant: true,
|
||||
});
|
||||
table.options.meta?.refetchData?.();
|
||||
} catch (err: any) {
|
||||
toast.error(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInvalid = async () => {
|
||||
try {
|
||||
await validateMediaLink(original.id, false);
|
||||
|
||||
updateRow({
|
||||
isRelevant: false,
|
||||
});
|
||||
table.options.meta?.refetchData?.();
|
||||
} catch (err: any) {
|
||||
toast.error(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
if (!link) {
|
||||
return <span className="text-muted-foreground">-</span>;
|
||||
}
|
||||
|
||||
if (isRelevant === true) {
|
||||
return (
|
||||
<Button
|
||||
size="sm"
|
||||
className="bg-green-600 hover:bg-green-700"
|
||||
disabled
|
||||
>
|
||||
Relevan
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={handleValid}
|
||||
className="flex items-center"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M18.7 7.2c-.4-.4-1-.4-1.4 0l-7.5 7.5l-3.1-3.1c-.4-.4-1-.4-1.4 0s-.4 1 0 1.4l3.8 3.8c.2.2.4.3.7.3s.5-.1.7-.3l8.2-8.2c.4-.4.4-1 0-1.4"
|
||||
/>
|
||||
</svg>
|
||||
Relevan
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={handleInvalid} className="flex text-center items-center justify-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.5"
|
||||
d="M6.758 17.243L12.001 12m5.243-5.243L12 12m0 0L6.758 6.757M12.001 12l5.243 5.243"
|
||||
/>
|
||||
</svg>
|
||||
Tidak Relevan
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -116,6 +116,18 @@ const NewsDetailTable = () => {
|
|||
onColumnVisibilityChange: setColumnVisibility,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
onPaginationChange: setPagination,
|
||||
meta: {
|
||||
updateData: (rowIndex: number, value: Partial<any>) => {
|
||||
setDataTable((old) =>
|
||||
old.map((row, index) =>
|
||||
index === rowIndex ? { ...row, ...value } : row
|
||||
)
|
||||
);
|
||||
},
|
||||
refetchData: () => {
|
||||
fetchData();
|
||||
},
|
||||
},
|
||||
state: {
|
||||
sorting,
|
||||
columnFilters,
|
||||
|
|
@ -154,7 +166,7 @@ const NewsDetailTable = () => {
|
|||
pageIndex: 0,
|
||||
pageSize: Number(showData),
|
||||
});
|
||||
}, [page, showData]);
|
||||
}, [page, showData, id]);
|
||||
|
||||
async function fetchData() {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,15 @@
|
|||
import * as React from "react";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
|
||||
import { Eye, MoreVertical, SquarePen, Trash2 } from "lucide-react";
|
||||
import { exportMediaTrackingToExcel } from "@/utils/export-media-tracking";
|
||||
import { loading, close } from "@/config/swal";
|
||||
import { error } from "@/lib/swal";
|
||||
import {
|
||||
DownloadIcon,
|
||||
Eye,
|
||||
MoreVertical,
|
||||
SquarePen,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
DropdownMenu,
|
||||
|
|
@ -45,19 +53,101 @@ const columns: ColumnDef<any>[] = [
|
|||
cell: ({ row }) => <span>{row.getValue("title")}</span>,
|
||||
},
|
||||
{
|
||||
accessorKey: "link",
|
||||
header: "Jumlah Amplifikasi",
|
||||
cell: ({ row }) => <span>{row.getValue("link")}</span>,
|
||||
accessorKey: "resultTotal",
|
||||
header: () => <div className="text-center w-full">Total Artikel</div>,
|
||||
cell: ({ row }) => {
|
||||
const value = row.getValue("resultTotal") as number | string | null;
|
||||
|
||||
const finalValue =
|
||||
value === null || value === undefined || value === ""
|
||||
? 0
|
||||
: Number(value);
|
||||
|
||||
return <div className="text-center w-full">{finalValue}</div>;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "status",
|
||||
header: "Status",
|
||||
cell: ({ row }) => <span>{row.getValue("status")}</span>,
|
||||
accessorKey: "amplification",
|
||||
header: () => <div className="text-center w-full">Jumlah Amplifikasi</div>,
|
||||
cell: ({ row }) => {
|
||||
const raw = row.getValue("amplification") as string | null;
|
||||
|
||||
let total = 0;
|
||||
let invalidTotal = 0;
|
||||
|
||||
if (raw && typeof raw === "string") {
|
||||
const parts = raw.split("/").map((v) => v.trim());
|
||||
total = Number(parts[0]) || 0;
|
||||
invalidTotal = Number(parts[1]) || 0;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="text-center w-full font-medium">
|
||||
{total}
|
||||
<span className="text-muted-foreground">/{invalidTotal}</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
// {
|
||||
// accessorKey: "status",
|
||||
// header: "Status",
|
||||
// cell: ({ row }) => <span>{row.getValue("status")}</span>,
|
||||
// },
|
||||
// {
|
||||
// accessorKey: "isProcessing",
|
||||
// header: () => <div className="text-center">Status</div>,
|
||||
// cell: ({ row }) => {
|
||||
// const raw = row.getValue("isProcessing");
|
||||
// var status = "Sedang Diproses"
|
||||
// if (Boolean(raw) == true) {
|
||||
// status = "Selesai Diproses";
|
||||
// }
|
||||
// return <div className="text-center">{status}</div>;
|
||||
// },
|
||||
// },
|
||||
{
|
||||
accessorKey: "date",
|
||||
header: "Tanggal Penarikan",
|
||||
cell: ({ row }) => <span>{row.getValue("date")}</span>,
|
||||
accessorKey: "isProcessing",
|
||||
header: () => <div className="text-center">Status</div>,
|
||||
cell: ({ row }) => {
|
||||
const raw = Boolean(row.getValue("isProcessing"));
|
||||
const statusText = raw ? "Sedang Diproses" : "Sudah Selesai";
|
||||
const colorClass = raw
|
||||
? "bg-yellow-100 text-yellow-700 border border-yellow-300"
|
||||
: "bg-green-100 text-green-700 border border-green-300";
|
||||
|
||||
return (
|
||||
<div className="text-center">
|
||||
<span
|
||||
className={`px-2 py-1 rounded text-xs font-medium inline-block ${colorClass}`}
|
||||
>
|
||||
{statusText}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
accessorKey: "createdAt",
|
||||
header: () => <div className="text-center">Tanggal Penarikan</div>,
|
||||
cell: ({ row }) => {
|
||||
const raw = row.getValue("createdAt");
|
||||
if (!raw || typeof raw !== "string")
|
||||
return <div className="text-center">-</div>;
|
||||
|
||||
const date = new Date(raw);
|
||||
if (isNaN(date.getTime())) return <div className="text-center">-</div>;
|
||||
|
||||
const formatted = date.toLocaleDateString("id-ID", {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
});
|
||||
|
||||
return <div className="text-center">{formatted}</div>;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
|
|
@ -78,13 +168,31 @@ const columns: ColumnDef<any>[] = [
|
|||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="p-0" align="end">
|
||||
<Link href={`/admin/media-tracking/detail/${row.original.id}`}>
|
||||
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none">
|
||||
<DropdownMenuItem className="p-2 border-b cursor-pointer text-default-700 group focus:bg-default focus:text-primary-foreground items-center rounded-none">
|
||||
<Eye className="w-4 h-4 me-1.5" />
|
||||
View
|
||||
View
|
||||
{row.original.mediaUpload.fileType.secondaryName &&
|
||||
row.original.mediaUpload.fileType.secondaryName.toLowerCase()}
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<DropdownMenuItem
|
||||
className="p-2 border-b cursor-pointer text-default-700 group rounded-none focus:bg-default focus:text-primary-foreground "
|
||||
onClick={async () => {
|
||||
try {
|
||||
loading();
|
||||
await exportMediaTrackingToExcel({
|
||||
mediaTrackingId: row.original.id,
|
||||
});
|
||||
close();
|
||||
} catch (e: any) {
|
||||
close();
|
||||
error(e.message || "Gagal export data");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DownloadIcon className="w-4 h-4 me-1.5" />
|
||||
<p>Download</p>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -257,7 +257,7 @@ const ResultTable = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="w-full overflow-x-auto bg-white p-4 rounded-sm space-y-3 border ">
|
||||
<div className="w-full overflow-x-auto bg-white dark:bg-black p-4 rounded-sm space-y-3 border ">
|
||||
<div className="flex flex-col sm:flex-row lg:flex-row justify-end sm:items-center md:items-center lg:items-center">
|
||||
<div className=" flex flex-row justify-end items-center gap-3">
|
||||
<div className="flex items-center">
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ import { PaginationState } from "@tanstack/react-table";
|
|||
import page from "../page";
|
||||
import CustomPagination from "@/components/table/custom-pagination";
|
||||
import { close, loading } from "@/config/swal";
|
||||
import withReactContent from "sweetalert2-react-content";
|
||||
import Swal from "sweetalert2";
|
||||
|
||||
export default function TrackingBeritaCard() {
|
||||
const [search, setSearch] = useState("");
|
||||
|
|
@ -30,6 +32,7 @@ export default function TrackingBeritaCard() {
|
|||
const [page, setPage] = useState(1);
|
||||
const [totalPage, setTotalPage] = useState(1);
|
||||
const [showData, setShowData] = useState("6");
|
||||
const MySwal = withReactContent(Swal);
|
||||
|
||||
useEffect(() => {
|
||||
initFecth();
|
||||
|
|
@ -37,7 +40,7 @@ export default function TrackingBeritaCard() {
|
|||
|
||||
const initFecth = async () => {
|
||||
loading();
|
||||
const response = await listDataTracking(showData, page - 1);
|
||||
const response = await listDataTracking(Number(showData), page - 1, search);
|
||||
const data = response?.data?.data;
|
||||
const newData = data?.content;
|
||||
setTotalPage(data?.totalPages || 1);
|
||||
|
|
@ -53,17 +56,25 @@ export default function TrackingBeritaCard() {
|
|||
setContent(response?.data?.data?.content || []);
|
||||
};
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const handleInputChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
setSearch(value);
|
||||
|
||||
if (value.trim() === "") {
|
||||
initFecth();
|
||||
} else {
|
||||
fecthAll(value);
|
||||
}
|
||||
const response = await listDataTracking(Number(showData), 0, value);
|
||||
setContent(response?.data?.data?.content || []);
|
||||
};
|
||||
|
||||
// const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
// const value = e.target.value;
|
||||
// setSearch(value);
|
||||
|
||||
// if (value.trim() === "") {
|
||||
// initFecth();
|
||||
// } else {
|
||||
// fecthAll(value);
|
||||
// }
|
||||
// };
|
||||
|
||||
const handleSelect = (id: number) => {
|
||||
setSelectedItems((prev) =>
|
||||
prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]
|
||||
|
|
@ -72,30 +83,143 @@ export default function TrackingBeritaCard() {
|
|||
|
||||
const doSave = async () => {
|
||||
if (selectedItems.length === 0) {
|
||||
toast("Pilih minimal 1 berita untuk disimpan.");
|
||||
MySwal.fire(
|
||||
"Peringatan",
|
||||
"Pilih minimal 1 berita untuk disimpan.",
|
||||
"warning"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const promises = selectedItems.map((id) =>
|
||||
mediaTrackingSave({
|
||||
loading();
|
||||
|
||||
const promises = selectedItems.map(async (id) => {
|
||||
const res = await mediaTrackingSave({
|
||||
mediaUploadId: id,
|
||||
duration: 24,
|
||||
scrapingPeriod: 3,
|
||||
})
|
||||
);
|
||||
await Promise.all(promises);
|
||||
});
|
||||
|
||||
toast("Berhasil Menambahkan", {
|
||||
description: "",
|
||||
// cek pesan API
|
||||
if (!res?.data?.success) {
|
||||
throw new Error(
|
||||
res?.data?.message ||
|
||||
"Limit media tracking per hari sudah tercapai. Maksimal 5 tracking per hari."
|
||||
);
|
||||
}
|
||||
|
||||
return res;
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
close();
|
||||
|
||||
await MySwal.fire({
|
||||
icon: "success",
|
||||
title: "Berhasil!",
|
||||
text: "Tracking berita berhasil ditambahkan.",
|
||||
confirmButtonColor: "#2563eb",
|
||||
});
|
||||
|
||||
setSelectedItems([]);
|
||||
initFecth();
|
||||
} catch (err: any) {
|
||||
error(err?.message || "Gagal menyimpan data.");
|
||||
close();
|
||||
MySwal.fire({
|
||||
icon: "error",
|
||||
title: "Gagal!",
|
||||
text: err?.message || "Terjadi kesalahan saat menyimpan data.",
|
||||
confirmButtonColor: "#dc2626",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// const doSave = async () => {
|
||||
// if (selectedItems.length === 0) {
|
||||
// toast("Pilih minimal 1 berita untuk disimpan.");
|
||||
// return;
|
||||
// }
|
||||
|
||||
// try {
|
||||
// const promises = selectedItems.map((id) =>
|
||||
// mediaTrackingSave({
|
||||
// mediaUploadId: id,
|
||||
// duration: 24,
|
||||
// scrapingPeriod: 3,
|
||||
// })
|
||||
// );
|
||||
// await Promise.all(promises);
|
||||
|
||||
// toast("Berhasil Menambahkan", {
|
||||
// description: "",
|
||||
// });
|
||||
// setSelectedItems([]);
|
||||
// initFecth();
|
||||
// } catch (err: any) {
|
||||
// error(err?.message || "Gagal menyimpan data.");
|
||||
// }
|
||||
// };
|
||||
|
||||
// const doSave = async () => {
|
||||
// if (selectedItems.length === 0) {
|
||||
// MySwal.fire(
|
||||
// "Peringatan",
|
||||
// "Pilih minimal 1 berita untuk disimpan.",
|
||||
// "warning"
|
||||
// );
|
||||
// return;
|
||||
// }
|
||||
|
||||
// try {
|
||||
// loading();
|
||||
|
||||
// const promises = selectedItems.map((id) =>
|
||||
// mediaTrackingSave({
|
||||
// mediaUploadId: id,
|
||||
// duration: 24,
|
||||
// scrapingPeriod: 3,
|
||||
// })
|
||||
// );
|
||||
// await Promise.all(promises);
|
||||
|
||||
// close();
|
||||
|
||||
// await MySwal.fire({
|
||||
// icon: "success",
|
||||
// title: "Berhasil!",
|
||||
// text: "Tracking berita berhasil ditambahkan.",
|
||||
// confirmButtonColor: "#2563eb",
|
||||
// });
|
||||
|
||||
// setSelectedItems([]);
|
||||
// initFecth();
|
||||
// } catch (err: any) {
|
||||
// close();
|
||||
// MySwal.fire({
|
||||
// icon: "error",
|
||||
// title: "Gagal!",
|
||||
// text: err?.message || "Terjadi kesalahan saat menyimpan data.",
|
||||
// confirmButtonColor: "#dc2626",
|
||||
// });
|
||||
// }
|
||||
// };
|
||||
|
||||
const slugify = (text: string) => {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/(^-|-$)+/g, "");
|
||||
};
|
||||
|
||||
const goToDetail = (item: any) => {
|
||||
const type = item.type || "image";
|
||||
const slug = slugify(item.title || "");
|
||||
const url = `/in/${type}/detail/${item.id}-${slug}`;
|
||||
|
||||
window.location.href = url;
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<div className="p-4 space-y-4">
|
||||
|
|
@ -141,7 +265,7 @@ export default function TrackingBeritaCard() {
|
|||
<div className="text-sm text-blue-600 font-medium">
|
||||
{selectedItems.length} Item Terpilih{" "}
|
||||
<span className="text-black">
|
||||
/ Tracking Berita tersisa {29 - selectedItems.length}
|
||||
/ Tracking Berita tersisa {5 - selectedItems.length}
|
||||
</span>
|
||||
</div>
|
||||
<Button className="bg-blue-600 text-white" onClick={doSave}>
|
||||
|
|
@ -151,6 +275,48 @@ export default function TrackingBeritaCard() {
|
|||
)}
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||
{content?.length > 0 &&
|
||||
content.map((item: any) => (
|
||||
<Card
|
||||
key={item.id}
|
||||
className="relative overflow-hidden shadow-sm border rounded-lg"
|
||||
>
|
||||
{/* KLIK GAMBAR = CHECKLIST */}
|
||||
<div
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleSelect(item.id)}
|
||||
>
|
||||
<img
|
||||
src={item.thumbnailLink}
|
||||
alt={item.title}
|
||||
className="w-full h-[300px] object-cover"
|
||||
/>
|
||||
|
||||
{/* CHECKBOX */}
|
||||
<div className="absolute top-2 left-2">
|
||||
<div className="w-5 h-5 border-2 border-white bg-white rounded-sm flex items-center justify-center">
|
||||
{selectedItems.includes(item.id) && (
|
||||
<div className="w-3 h-3 bg-blue-600 rounded-sm" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* KLIK JUDUL = DETAIL */}
|
||||
<p
|
||||
className="p-2 text-sm font-medium hover:underline cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
goToDetail(item);
|
||||
}}
|
||||
>
|
||||
{item.title}
|
||||
</p>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||
{content?.length > 1 &&
|
||||
content.map((item: any) => (
|
||||
<Card
|
||||
|
|
@ -175,7 +341,7 @@ export default function TrackingBeritaCard() {
|
|||
</p>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div> */}
|
||||
<div className="mt-3">
|
||||
{content && content?.length > 0 ? (
|
||||
<CustomPagination
|
||||
|
|
|
|||
|
|
@ -58,16 +58,6 @@ const columns: ColumnDef<any>[] = [
|
|||
<span>{formatDateToIndonesian(row.getValue("createdAt"))}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "isStaticBanner",
|
||||
header: "Static Banner",
|
||||
cell: ({ row }) => (
|
||||
<StaticToogle
|
||||
id={row.original.id}
|
||||
initChecked={row.original.isStaticBanner}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "statusName",
|
||||
header: "Status Banner",
|
||||
|
|
@ -75,7 +65,6 @@ const columns: ColumnDef<any>[] = [
|
|||
<StatusToogle id={row.original.id} initChecked={row.original.isBanner} />
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
id: "actions",
|
||||
accessorKey: "action",
|
||||
|
|
|
|||
|
|
@ -94,9 +94,26 @@ const ContentListBanner = () => {
|
|||
const [selectedItems, setSelectedItems] = React.useState<number[]>([]);
|
||||
const [page, setPage] = React.useState(1);
|
||||
const [totalPage, setTotalPage] = React.useState(1);
|
||||
const [searchQuery, setSearchQuery] = React.useState("");
|
||||
const [bannerCount, setBannerCount] = React.useState<number>(0);
|
||||
|
||||
let typingTimer: any;
|
||||
const doneTypingInterval = 1500;
|
||||
React.useEffect(() => {
|
||||
fetchBannerCount();
|
||||
}, []);
|
||||
|
||||
async function fetchBannerCount() {
|
||||
try {
|
||||
const res = await listDataMedia(0, "100", "", "", "");
|
||||
const banners = res?.data?.data?.content?.filter(
|
||||
(item: any) => item.isBanner
|
||||
);
|
||||
setBannerCount(banners?.length || 0);
|
||||
|
||||
setBannerCount(data?.length || 0);
|
||||
} catch (error) {
|
||||
console.error("Error fetching banner count:", error);
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyUp = () => {
|
||||
clearTimeout(typingTimer);
|
||||
|
|
@ -108,6 +125,21 @@ const ContentListBanner = () => {
|
|||
typingTimer = setTimeout(doneTyping, doneTypingInterval);
|
||||
};
|
||||
|
||||
let typingTimer: NodeJS.Timeout;
|
||||
const doneTypingInterval = 1500;
|
||||
|
||||
const handleTyping = () => {
|
||||
clearTimeout(typingTimer);
|
||||
typingTimer = setTimeout(() => {
|
||||
setPage(1);
|
||||
fetchData();
|
||||
}, doneTypingInterval);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
fetchData();
|
||||
}, [categoryFilter, statusFilter]);
|
||||
|
||||
async function doneTyping() {
|
||||
fetchData();
|
||||
}
|
||||
|
|
@ -133,10 +165,11 @@ const ContentListBanner = () => {
|
|||
const res = await listDataMedia(
|
||||
page - 1,
|
||||
showData,
|
||||
"",
|
||||
searchQuery,
|
||||
categoryFilter?.sort().join(","),
|
||||
statusFilter?.sort().join(",")
|
||||
);
|
||||
|
||||
const data = res?.data?.data;
|
||||
const contentData = data?.content;
|
||||
contentData.forEach((item: any, index: number) => {
|
||||
|
|
@ -204,11 +237,26 @@ const ContentListBanner = () => {
|
|||
|
||||
const handleBanner = async (ids: number[]) => {
|
||||
try {
|
||||
await Promise.all(ids.map((id) => setBanner(id, true)));
|
||||
toast({
|
||||
title: "Sukses",
|
||||
description: `${ids.length} item berhasil dijadikan banner.`,
|
||||
});
|
||||
// const res = await Promise.all(ids.map((id) => setBanner(id, true)));
|
||||
|
||||
for (const element of ids) {
|
||||
loading();
|
||||
const res = await setBanner(element, true);
|
||||
close();
|
||||
if (res?.error) {
|
||||
toast({
|
||||
title: "Gagal",
|
||||
description:
|
||||
"Banner sudah melebihi batas maksimum (4 konten). Silahkan di disable banner Lainnya.",
|
||||
variant: "destructive",
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
title: "Sukses",
|
||||
description: `item berhasil dijadikan banner.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: "Gagal",
|
||||
|
|
@ -224,10 +272,19 @@ const ContentListBanner = () => {
|
|||
<Input
|
||||
type="text"
|
||||
placeholder="Search"
|
||||
onKeyUp={handleKeyUp}
|
||||
onKeyDown={handleKeyDown}
|
||||
value={searchQuery}
|
||||
onChange={(e) => {
|
||||
setSearchQuery(e.target.value);
|
||||
handleTyping();
|
||||
}}
|
||||
className="max-w-[300px]"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
fetchData();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* <div className="flex flex-row gap-2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
|
|
@ -366,7 +423,7 @@ const ContentListBanner = () => {
|
|||
checked={selectedItems.length === data.length}
|
||||
onCheckedChange={handleSelectAll}
|
||||
/>
|
||||
<span>Pilih Semua</span>
|
||||
<span className="text-black dark:text-white">Pilih Semua</span>
|
||||
</div>
|
||||
{selectedItems.length > 0 && (
|
||||
<Button color="primary" onClick={() => handleBanner(selectedItems)}>
|
||||
|
|
@ -393,9 +450,12 @@ const ContentListBanner = () => {
|
|||
alt={item.title}
|
||||
className="w-full h-48 object-cover"
|
||||
/>
|
||||
<div className="p-3">
|
||||
<h4 className="font-semibold text-sm truncate">{item.title}</h4>
|
||||
</div>
|
||||
<Link
|
||||
href={`/contributor/content/image/detail/${item?.id}`}
|
||||
className="p-3"
|
||||
>
|
||||
<h4 className="font-semibold text-sm">{item.title}</h4>
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export default function AdminBanner() {
|
|||
return (
|
||||
<div>
|
||||
<SiteBreadcrumb />
|
||||
<div className="w-full overflow-x-auto bg-white p-4 rounded-sm space-y-3">
|
||||
<div className="w-full overflow-x-auto bg-white dark:bg-black p-4 rounded-sm space-y-3">
|
||||
<div className="flex justify-between">
|
||||
{selectedTab === "content" ? "List Media" : " List Banner"}
|
||||
|
||||
|
|
@ -28,7 +28,7 @@ export default function AdminBanner() {
|
|||
: "bg-white text-black "
|
||||
}`}
|
||||
>
|
||||
Kontent
|
||||
Konten
|
||||
</Button>
|
||||
<Button
|
||||
rounded="md"
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ const columns: ColumnDef<any>[] = [
|
|||
router.push("/admin/settings/category?dataChange=true");
|
||||
};
|
||||
return (
|
||||
<Menubar className="border-none">
|
||||
<Menubar className="border-none dark:bg-black">
|
||||
<MenubarMenu>
|
||||
<MenubarTrigger>
|
||||
<Button
|
||||
|
|
@ -124,7 +124,7 @@ const columns: ColumnDef<any>[] = [
|
|||
onClick={() => categoryDelete(row.original.id)}
|
||||
className="hover:underline cursor-pointer hover:text-destructive"
|
||||
>
|
||||
Delete
|
||||
Delete
|
||||
</a>
|
||||
</MenubarContent>
|
||||
</MenubarMenu>
|
||||
|
|
|
|||
|
|
@ -125,16 +125,16 @@ export default function CreateCategoryModal() {
|
|||
});
|
||||
|
||||
const contentType = form.watch("contentType");
|
||||
const isAllContentChecked = listContent.every((item) =>
|
||||
contentType?.includes(item.id)
|
||||
const isAllContentChecked = contentType && listContent.every((item) =>
|
||||
contentType.includes(item.id)
|
||||
);
|
||||
|
||||
const users = form.watch("selectedUser");
|
||||
const isAllUserChecked = userList.every((item) => users?.includes(item.id));
|
||||
const isAllUserChecked = users && userList.every((item) => users.includes(item.id));
|
||||
|
||||
const target = form.watch("publishTo");
|
||||
const isAllTargetChecked = publishToList.every((item) =>
|
||||
target?.includes(item.id)
|
||||
const isAllTargetChecked = target && publishToList.every((item) =>
|
||||
target.includes(item.id)
|
||||
);
|
||||
|
||||
const { getRootProps, getInputProps } = useDropzone({
|
||||
|
|
@ -240,7 +240,7 @@ export default function CreateCategoryModal() {
|
|||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button color="primary" size="md">
|
||||
{t("add-category")}
|
||||
{t("add-category", { defaultValue: "Add Category" })}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent
|
||||
|
|
@ -248,12 +248,12 @@ export default function CreateCategoryModal() {
|
|||
className="sm:h-[300px] md:h-[300px] lg:h-[500px] overflow-y-auto"
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle> {t("add-category")}</DialogTitle>
|
||||
<DialogTitle> {t("add-category", { defaultValue: "Add Category" })}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-3 bg-white rounded-sm"
|
||||
className="space-y-3 bg-white dark:bg-[#1f2937] rounded-sm"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
|
|
|
|||
|
|
@ -138,35 +138,34 @@ export default function EditCategoryModal(props: {
|
|||
maxFiles: 1,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const initFetch = async () => {
|
||||
const req = await getCategoryDetail(id);
|
||||
const data = req?.data?.data;
|
||||
console.log("dataC", data);
|
||||
form.setValue("id", String(data?.id));
|
||||
form.setValue("title", String(data?.name));
|
||||
form.setValue("description", String(data?.description));
|
||||
form.setValue("contentType", data?.mediaTypes?.split(","));
|
||||
form.setValue(
|
||||
"selectedUser",
|
||||
removeAndReturn(data?.publishedFor, [2, 3, 4])
|
||||
);
|
||||
form.setValue("publishTo", data?.publishedLocation?.split(","));
|
||||
form.setValue("file", thumbnailLink);
|
||||
// useEffect(() => {
|
||||
// initFetch();
|
||||
// }, [id]);
|
||||
|
||||
setUnitData(filterString(data?.publishedLocationLevel, "under"));
|
||||
setSatkerData(filterString(data?.publishedLocationLevel, "above"));
|
||||
};
|
||||
const initFetch = async () => {
|
||||
const req = await getCategoryDetail(id);
|
||||
const data = req?.data?.data;
|
||||
form.setValue("id", String(data?.id));
|
||||
form.setValue("title", String(data?.name));
|
||||
form.setValue("description", String(data?.description));
|
||||
form.setValue("contentType", data?.mediaTypes?.split(",") || []);
|
||||
form.setValue(
|
||||
"selectedUser",
|
||||
removeAndReturn(data?.publishedFor, [2, 3, 4])
|
||||
);
|
||||
form.setValue("publishTo", data?.publishedLocation?.split(",") || []);
|
||||
form.setValue("file", thumbnailLink);
|
||||
|
||||
initFetch();
|
||||
}, [id]);
|
||||
setUnitData(filterString(data?.publishedLocationLevel, "under"));
|
||||
setSatkerData(filterString(data?.publishedLocationLevel, "above"));
|
||||
};
|
||||
|
||||
function removeAndReturn(inputString: string, toRemove: number[]) {
|
||||
const numbers = inputString.split(",").map(Number);
|
||||
const numbers = inputString?.split(",").map(Number);
|
||||
|
||||
const filteredNumbers = numbers.filter((num) => !toRemove.includes(num));
|
||||
const filteredNumbers = numbers?.filter((num) => !toRemove?.includes(num));
|
||||
|
||||
return filteredNumbers.map(String);
|
||||
return filteredNumbers?.map(String);
|
||||
}
|
||||
|
||||
function filterString(inputString: string, type: string) {
|
||||
|
|
@ -183,17 +182,16 @@ export default function EditCategoryModal(props: {
|
|||
}
|
||||
|
||||
const contentType = form.watch("contentType");
|
||||
const isAllContentChecked = listContent.every((item) =>
|
||||
contentType?.includes(item.id)
|
||||
);
|
||||
const isAllContentChecked =
|
||||
contentType && listContent.every((item) => contentType.includes(item.id));
|
||||
|
||||
const users = form.watch("selectedUser");
|
||||
const isAllUserChecked = userList.every((item) => users?.includes(item.id));
|
||||
const isAllUserChecked =
|
||||
users && userList.every((item) => users.includes(item.id));
|
||||
|
||||
const target = form.watch("publishTo");
|
||||
const isAllTargetChecked = publishToList.every((item) =>
|
||||
target?.includes(item.id)
|
||||
);
|
||||
const isAllTargetChecked =
|
||||
target && publishToList.every((item) => target.includes(item.id));
|
||||
|
||||
useEffect(() => {
|
||||
getRoles();
|
||||
|
|
@ -213,6 +211,7 @@ export default function EditCategoryModal(props: {
|
|||
const uniqueNumbers = Array.from(new Set(numbers));
|
||||
return uniqueNumbers.join(",");
|
||||
}
|
||||
|
||||
const onSubmit = async (data: z.infer<typeof FormSchema>) => {
|
||||
const formMedia = new FormData();
|
||||
|
||||
|
|
@ -263,7 +262,9 @@ export default function EditCategoryModal(props: {
|
|||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger>
|
||||
<a
|
||||
onClick={() => setIsOpen(true)}
|
||||
onClick={() =>{
|
||||
initFetch()
|
||||
setIsOpen(true); } }
|
||||
className="hover:underline cursor-pointer"
|
||||
>
|
||||
{isDetail ? "Detail" : "Edit"}
|
||||
|
|
|
|||
|
|
@ -10,15 +10,34 @@ export default function StatusToogle(props: {
|
|||
}) {
|
||||
const { id, initValue } = props;
|
||||
const router = useRouter();
|
||||
// const publishCategory = async (id: number, status: string) => {
|
||||
// const response = await publishUnpublishCategory(id, status);
|
||||
// console.log(response);
|
||||
// if (response?.error) {
|
||||
// error(response.message);
|
||||
// return false;
|
||||
// }
|
||||
// router.push("/admin/settings/category?dataChange=true");
|
||||
// };
|
||||
const publishCategory = async (id: number, status: string) => {
|
||||
const response = await publishUnpublishCategory(id, status);
|
||||
console.log(response);
|
||||
console.log("API Response:", response);
|
||||
|
||||
// cek error interceptor
|
||||
if (response?.error) {
|
||||
error(response.message);
|
||||
error(response.message || "Terjadi kesalahan");
|
||||
return false;
|
||||
}
|
||||
|
||||
// cek flag success asli dari backend
|
||||
if (response?.data?.success === false) {
|
||||
error(response?.data?.message || "Terjadi kesalahan");
|
||||
return false;
|
||||
}
|
||||
|
||||
router.push("/admin/settings/category?dataChange=true");
|
||||
};
|
||||
|
||||
return (
|
||||
<Switch
|
||||
id={String(id)}
|
||||
|
|
|
|||
|
|
@ -28,21 +28,10 @@ import {
|
|||
import { useSearchParams } from "next/navigation";
|
||||
import TablePagination from "@/components/table/table-pagination";
|
||||
import columns from "./column";
|
||||
|
||||
import { listEnableCategory } from "@/service/content/content";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { close, loading } from "@/config/swal";
|
||||
import { Link, useRouter } from "@/i18n/routing";
|
||||
import { NewCampaignIcon } from "@/components/icon";
|
||||
import { getCategories } from "@/service/settings/settings";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import CreateCategoryModal from "./create";
|
||||
import { useTranslations } from "next-intl";
|
||||
import {
|
||||
|
|
@ -186,9 +175,9 @@ const AdminCategoryTable = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="w-full overflow-x-auto bg-white p-4 rounded-sm space-y-3">
|
||||
<div className="w-full overflow-x-auto bg-white dark:bg-black p-4 rounded-sm space-y-3">
|
||||
<div className="flex justify-between mb-10 items-center">
|
||||
<p className="text-xl font-medium text-default-900">{t("category")}</p>
|
||||
<p className="text-xl font-medium text-default-900">{t("category", { defaultValue: "Category" })}</p>
|
||||
<CreateCategoryModal />
|
||||
</div>
|
||||
<div className="flex items-end justify-end">
|
||||
|
|
|
|||
|
|
@ -68,11 +68,11 @@ export function UnitMapping(props: {
|
|||
|
||||
const unitType = form.watch("items");
|
||||
|
||||
const isAllUnitChecked = unitList.every((item) =>
|
||||
unitType?.includes(String(item.id))
|
||||
const isAllUnitChecked = unitType && unitList.every((item) =>
|
||||
unitType.includes(String(item.id))
|
||||
);
|
||||
const isAllSatkerChecked = satkerList.every((item) =>
|
||||
unitType?.includes(String(item.id))
|
||||
const isAllSatkerChecked = unitType && satkerList.every((item) =>
|
||||
unitType.includes(String(item.id))
|
||||
);
|
||||
|
||||
const setupUnit = (data: UnitType[]) => {
|
||||
|
|
|
|||
|
|
@ -91,8 +91,8 @@ export default function CreateFAQModal() {
|
|||
});
|
||||
|
||||
const target = form.watch("publishTo");
|
||||
const isAllTargetChecked = publishToList.every((item) =>
|
||||
target?.includes(item.id)
|
||||
const isAllTargetChecked = target && publishToList.every((item) =>
|
||||
target.includes(item.id)
|
||||
);
|
||||
|
||||
const onSubmit = async (data: z.infer<typeof FormSchema>) => {
|
||||
|
|
@ -121,12 +121,12 @@ export default function CreateFAQModal() {
|
|||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button color="primary" size="md">
|
||||
{t("add")} FAQ
|
||||
{t("add", { defaultValue: "Add" })} FAQ
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent size="md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("add")} FAQ</DialogTitle>
|
||||
<DialogTitle>{t("add", { defaultValue: "Add" })} FAQ</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form
|
||||
|
|
|
|||
|
|
@ -111,12 +111,12 @@ export default function CreateFAQModal() {
|
|||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button color="primary" size="md">
|
||||
{t("add")} Feedback
|
||||
{t("add", { defaultValue: "Add" })} Feedback
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent size="md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("add")} Feedback</DialogTitle>
|
||||
<DialogTitle>{t("add", { defaultValue: "Add" })} Feedback</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import * as React from "react";
|
|||
import { ColumnDef } from "@tanstack/react-table";
|
||||
|
||||
import { Eye, MoreVertical, SquarePen, Trash2 } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn, getCookiesDecrypt } from "@/lib/utils";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
|
|
@ -78,7 +78,7 @@ const columns: ColumnDef<any>[] = [
|
|||
enableHiding: false,
|
||||
cell: ({ row }) => {
|
||||
const MySwal = withReactContent(Swal);
|
||||
|
||||
const levelNumber = getCookiesDecrypt("ulne");
|
||||
async function doDelete(id: any) {
|
||||
// loading();
|
||||
const data = {
|
||||
|
|
@ -132,7 +132,7 @@ const columns: ColumnDef<any>[] = [
|
|||
<MoreVertical className="h-4 w-4 text-default-800" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="p-0" align="end">
|
||||
{/* <DropdownMenuContent className="p-0" align="end">
|
||||
<Link href={`/admin/settings/iklan/detail/${row.original.id}`}>
|
||||
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none">
|
||||
<Eye className="w-4 h-4 me-1.5" />
|
||||
|
|
@ -152,6 +152,33 @@ const columns: ColumnDef<any>[] = [
|
|||
<Trash2 className="w-4 h-4 me-1.5" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent> */}
|
||||
<DropdownMenuContent className="p-0" align="end">
|
||||
<Link href={`/admin/settings/iklan/detail/${row.original.id}`}>
|
||||
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none">
|
||||
<Eye className="w-4 h-4 me-1.5" />
|
||||
View
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
|
||||
{levelNumber === "1" && (
|
||||
<>
|
||||
<Link href={`/admin/settings/iklan/update/${row.original.id}`}>
|
||||
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none">
|
||||
<SquarePen className="w-4 h-4 me-1.5" />
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleDeleteAdvertisements(row.original.id)}
|
||||
className="p-2 border-b text-destructive bg-destructive/30 focus:bg-destructive focus:text-destructive-foreground rounded-none"
|
||||
>
|
||||
<Trash2 className="w-4 h-4 me-1.5" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ import {
|
|||
UploadIcon,
|
||||
UserIcon,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn, getCookiesDecrypt } from "@/lib/utils";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
|
|
@ -53,7 +53,7 @@ import { InputGroup, InputGroupText } from "@/components/ui/input-group";
|
|||
import { paginationBlog } from "@/service/blog/blog";
|
||||
import { ticketingPagination } from "@/service/ticketing/ticketing";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import TablePagination from "@/components/table/table-pagination";
|
||||
import columns from "./column";
|
||||
import { getPlanningPagination } from "@/service/agenda-setting/agenda-setting";
|
||||
|
|
@ -69,7 +69,7 @@ import {
|
|||
import { listEnableCategory } from "@/service/content/content";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { close, loading } from "@/config/swal";
|
||||
import { Link } from "@/i18n/routing";
|
||||
import { Link, useRouter } from "@/i18n/routing";
|
||||
import { TambahIklanModal } from "@/components/form/setting/form-add-iklan";
|
||||
|
||||
const AdvertisementsList = () => {
|
||||
|
|
@ -94,6 +94,9 @@ const AdvertisementsList = () => {
|
|||
const [statusFilter, setStatusFilter] = React.useState<number[]>([]);
|
||||
const [page, setPage] = React.useState(1);
|
||||
const [totalPage, setTotalPage] = React.useState(1);
|
||||
const roleId = getCookiesDecrypt("urie");
|
||||
const levelNumber = getCookiesDecrypt("ulne");
|
||||
const userLevelId = getCookiesDecrypt("ulie");
|
||||
const table = useReactTable({
|
||||
data: dataTable,
|
||||
columns,
|
||||
|
|
@ -150,11 +153,7 @@ const AdvertisementsList = () => {
|
|||
async function fetchData() {
|
||||
try {
|
||||
loading();
|
||||
const res = await listDataAdvertisements(
|
||||
page - 1,
|
||||
showData,
|
||||
"",
|
||||
);
|
||||
const res = await listDataAdvertisements(page - 1, showData, "");
|
||||
const data = res?.data?.data;
|
||||
const contentData = data?.content;
|
||||
contentData.forEach((item: any, index: number) => {
|
||||
|
|
@ -207,14 +206,27 @@ const AdvertisementsList = () => {
|
|||
return (
|
||||
<>
|
||||
<div>
|
||||
<div className="flex-none">
|
||||
<Link href={"/admin/settings/iklan/create"}>
|
||||
<Button color="primary" className="text-white" size="md">
|
||||
<UploadIcon size={18} className="mr-2" />
|
||||
Tambah Iklan
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
{levelNumber === "1" && (
|
||||
<div className="flex-none">
|
||||
<Link href={"/admin/settings/iklan/create"}>
|
||||
<Button
|
||||
disabled={dataTable.length == 4}
|
||||
color="primary"
|
||||
className="text-white"
|
||||
size="md"
|
||||
>
|
||||
<UploadIcon size={18} className="mr-2" />
|
||||
Tambah Iklan
|
||||
</Button>
|
||||
</Link>
|
||||
{dataTable.length == 4 && (
|
||||
<p className="text-sm text-red-400 pt-1">
|
||||
Jumlah Iklan Sudah Maksimal (4)
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* <TambahIklanModal /> */}
|
||||
</div>
|
||||
<div className="flex justify-between ">
|
||||
|
|
|
|||
|
|
@ -58,24 +58,13 @@ const columns: ColumnDef<any>[] = [
|
|||
<span>{formatDateToIndonesian(row.getValue("createdAt"))}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "isStaticBanner",
|
||||
header: "Static Banner",
|
||||
cell: ({ row }) => (
|
||||
<StaticToogle
|
||||
id={row.original.id}
|
||||
initChecked={row.original.isStaticBanner}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "statusName",
|
||||
header: "Status Banner",
|
||||
header: "Status Pop Up",
|
||||
cell: ({ row }) => (
|
||||
<StatusToogle id={row.original.id} initChecked={row.original.isBanner} />
|
||||
<StatusToogle id={row.original.id} initChecked={row.original.isInterstitial} />
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
id: "actions",
|
||||
accessorKey: "action",
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import { useSearchParams } from "next/navigation";
|
|||
import { close, loading } from "@/config/swal";
|
||||
import { Link, useRouter } from "@/i18n/routing";
|
||||
import columns from "./popup-column";
|
||||
import { listBanner, listStaticBanner } from "@/service/settings/settings";
|
||||
import { getListPopUp, listBanner, listStaticBanner } from "@/service/settings/settings";
|
||||
import { listDataPopUp } from "@/service/broadcast/broadcast";
|
||||
|
||||
const PopUpListTable = () => {
|
||||
|
|
@ -84,27 +84,21 @@ const PopUpListTable = () => {
|
|||
|
||||
React.useEffect(() => {
|
||||
if (dataChange) {
|
||||
router.push("/admin/settings/banner");
|
||||
router.push("/admin/settings/popup");
|
||||
}
|
||||
getListBanner();
|
||||
getPopUp();
|
||||
}, [dataChange]);
|
||||
|
||||
React.useEffect(() => {
|
||||
getListBanner();
|
||||
getPopUp();
|
||||
// getListStaticBanner();
|
||||
}, [page, showData]);
|
||||
|
||||
async function getListBanner() {
|
||||
async function getPopUp() {
|
||||
loading();
|
||||
let temp: any;
|
||||
|
||||
const response = await listDataPopUp(
|
||||
page - 1,
|
||||
showData,
|
||||
"",
|
||||
categoryFilter?.sort().join(","),
|
||||
statusFilter?.sort().join(",")
|
||||
);
|
||||
const response = await getListPopUp();
|
||||
const data = response?.data?.data?.content;
|
||||
console.log("banner", data);
|
||||
setGetData(data);
|
||||
|
|
@ -114,47 +108,49 @@ const PopUpListTable = () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Table className="overflow-hidden mt-10">
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id} className="bg-default-200">
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableHead key={header.id}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
className="h-[75px]"
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
{table &&
|
||||
<Table className="overflow-hidden mt-10">
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id} className="bg-default-200">
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableHead key={header.id}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table?.getRowModel()?.rows?.length ? (
|
||||
table?.getRowModel()?.rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
className="h-[75px]"
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { Switch } from "@/components/ui/switch";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import { useRouter } from "@/i18n/routing";
|
||||
import { setBanner } from "@/service/settings/settings";
|
||||
import { setBanner, setPopUp } from "@/service/settings/settings";
|
||||
|
||||
export default function StatusToogle(props: {
|
||||
id: number;
|
||||
|
|
@ -12,7 +12,7 @@ export default function StatusToogle(props: {
|
|||
const router = useRouter();
|
||||
|
||||
const disableBanner = async () => {
|
||||
const response = await setBanner(id, false);
|
||||
const response = await setPopUp(id, false);
|
||||
|
||||
if (response?.error) {
|
||||
toast({
|
||||
|
|
@ -25,7 +25,7 @@ export default function StatusToogle(props: {
|
|||
toast({
|
||||
title: "Success ",
|
||||
});
|
||||
router.push("/admin/settings/banner?dataChange=true");
|
||||
router.push("/admin/settings/popup?dataChange=true");
|
||||
};
|
||||
return (
|
||||
<Switch
|
||||
|
|
|
|||
|
|
@ -94,9 +94,10 @@ const ContentListPopUp = () => {
|
|||
const [selectedItems, setSelectedItems] = React.useState<number[]>([]);
|
||||
const [page, setPage] = React.useState(1);
|
||||
const [totalPage, setTotalPage] = React.useState(1);
|
||||
const [searchQuery, setSearchQuery] = React.useState("");
|
||||
|
||||
let typingTimer: any;
|
||||
const doneTypingInterval = 1500;
|
||||
let typingTimer: NodeJS.Timeout;
|
||||
const doneTypingInterval = 2000;
|
||||
|
||||
const handleKeyUp = () => {
|
||||
clearTimeout(typingTimer);
|
||||
|
|
@ -105,9 +106,16 @@ const ContentListPopUp = () => {
|
|||
|
||||
const handleKeyDown = () => {
|
||||
clearTimeout(typingTimer);
|
||||
typingTimer = setTimeout(doneTyping, doneTypingInterval);
|
||||
typingTimer = setTimeout(() => {
|
||||
setPage(1);
|
||||
fetchData();
|
||||
}, doneTypingInterval);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
fetchData();
|
||||
}, [categoryFilter, statusFilter]);
|
||||
|
||||
async function doneTyping() {
|
||||
fetchData();
|
||||
}
|
||||
|
|
@ -127,13 +135,40 @@ const ContentListPopUp = () => {
|
|||
});
|
||||
}, [page, showData]);
|
||||
|
||||
// async function fetchData() {
|
||||
// try {
|
||||
// loading();
|
||||
// const res = await listDataPopUp(
|
||||
// page - 1,
|
||||
// showData,
|
||||
// "",
|
||||
// categoryFilter?.sort().join(","),
|
||||
// statusFilter?.sort().join(",")
|
||||
// );
|
||||
// const data = res?.data?.data;
|
||||
// const contentData = data?.content;
|
||||
// contentData.forEach((item: any, index: number) => {
|
||||
// item.no = (page - 1) * Number(showData) + index + 1;
|
||||
// });
|
||||
|
||||
// console.log("contentData : ", data);
|
||||
|
||||
// setData(contentData);
|
||||
// setTotalData(data?.totalElements);
|
||||
// setTotalPage(data?.totalPages);
|
||||
// close();
|
||||
// } catch (error) {
|
||||
// console.error("Error fetching tasks:", error);
|
||||
// }
|
||||
// }
|
||||
|
||||
async function fetchData() {
|
||||
try {
|
||||
loading();
|
||||
const res = await listDataPopUp(
|
||||
const res = await listDataMedia(
|
||||
page - 1,
|
||||
showData,
|
||||
"",
|
||||
searchQuery, // <-- gunakan nilai pencarian
|
||||
categoryFilter?.sort().join(","),
|
||||
statusFilter?.sort().join(",")
|
||||
);
|
||||
|
|
@ -202,13 +237,44 @@ const ContentListPopUp = () => {
|
|||
|
||||
const { toast } = useToast();
|
||||
|
||||
const handleBanner = async (ids: number[]) => {
|
||||
// const handlePopUp = async (ids: number[]) => {
|
||||
// try {
|
||||
// await Promise.all(ids.map((id) => setPopUp(id, true)));
|
||||
// toast({
|
||||
// title: "Sukses",
|
||||
// description: `${ids.length} item berhasil dijadikan Popup.`,
|
||||
// });
|
||||
// } catch (err) {
|
||||
// toast({
|
||||
// title: "Gagal",
|
||||
// description: "Terjadi kesalahan saat menjadikan Popup.",
|
||||
// variant: "destructive",
|
||||
// });
|
||||
// }
|
||||
// };
|
||||
|
||||
const handlePopUp = async (ids: number[]) => {
|
||||
try {
|
||||
await Promise.all(ids.map((id) => setPopUp(id, true)));
|
||||
toast({
|
||||
title: "Sukses",
|
||||
description: `${ids.length} item berhasil dijadikan banner.`,
|
||||
});
|
||||
// const res = await Promise.all(ids.map((id) => setBanner(id, true)));
|
||||
|
||||
for (const element of ids) {
|
||||
loading();
|
||||
const res = await setPopUp(element, true);
|
||||
close();
|
||||
if (res?.error) {
|
||||
toast({
|
||||
title: "Gagal",
|
||||
description:
|
||||
"Banner sudah melebihi batas maksimum (4 konten). Silahkan di disable popup Lainnya.",
|
||||
variant: "destructive",
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
title: "Sukses",
|
||||
description: `item berhasil dijadikan banner.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: "Gagal",
|
||||
|
|
@ -224,10 +290,19 @@ const ContentListPopUp = () => {
|
|||
<Input
|
||||
type="text"
|
||||
placeholder="Search"
|
||||
onKeyUp={handleKeyUp}
|
||||
onKeyDown={handleKeyDown}
|
||||
value={searchQuery}
|
||||
onChange={(e) => {
|
||||
setSearchQuery(e.target.value);
|
||||
handleKeyDown();
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
fetchData();
|
||||
}
|
||||
}}
|
||||
className="max-w-[300px]"
|
||||
/>
|
||||
|
||||
{/* <div className="flex flex-row gap-2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
|
|
@ -369,7 +444,7 @@ const ContentListPopUp = () => {
|
|||
<span>Pilih Semua</span>
|
||||
</div>
|
||||
{selectedItems.length > 0 && (
|
||||
<Button color="primary" onClick={() => handleBanner(selectedItems)}>
|
||||
<Button color="primary" onClick={() => handlePopUp(selectedItems)}>
|
||||
Jadikan PopUp
|
||||
</Button>
|
||||
)}
|
||||
|
|
@ -393,9 +468,12 @@ const ContentListPopUp = () => {
|
|||
alt={item.title}
|
||||
className="w-full h-48 object-cover"
|
||||
/>
|
||||
<div className="p-3">
|
||||
<h4 className="font-semibold text-sm truncate">{item.title}</h4>
|
||||
</div>
|
||||
<Link
|
||||
href={`/contributor/content/image/detail/${item?.id}`}
|
||||
className="p-3"
|
||||
>
|
||||
<h4 className="font-semibold text-sm">{item.title}</h4>
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ export default function AdminPopup() {
|
|||
: "bg-white text-black "
|
||||
}`}
|
||||
>
|
||||
Kontent
|
||||
Konten
|
||||
</Button>
|
||||
<Button
|
||||
rounded="md"
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import {
|
|||
} from "@/components/ui/form";
|
||||
import { close, error, loading } from "@/config/swal";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import JoditEditor from "jodit-react";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { getPrivacy, savePrivacy } from "@/service/settings/settings";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
|
|
@ -104,16 +104,6 @@ export default function AdminPrivacyPolicy() {
|
|||
<FormItem>
|
||||
<FormLabel>Konten</FormLabel>
|
||||
<FormControl>
|
||||
{/* <JoditEditor
|
||||
ref={editor}
|
||||
value={field.value}
|
||||
config={{
|
||||
height: 400, // Tinggi editor dalam piksel
|
||||
}}
|
||||
className="dark:text-black"
|
||||
onChange={field.onChange}
|
||||
|
||||
/> */}
|
||||
<CustomEditor
|
||||
onChange={field.onChange}
|
||||
initialData={field.value}
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ const useTableColumns = () => {
|
|||
{
|
||||
id: "actions",
|
||||
accessorKey: "action",
|
||||
header: t("action"),
|
||||
header: t("action", { defaultValue: "Action" }),
|
||||
enableHiding: false,
|
||||
cell: ({ row }) => {
|
||||
const MySwal = withReactContent(Swal);
|
||||
|
|
|
|||
|
|
@ -27,22 +27,10 @@ import {
|
|||
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import TablePagination from "@/components/table/table-pagination";
|
||||
import columns from "./column";
|
||||
|
||||
import { listEnableCategory } from "@/service/content/content";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { close, loading } from "@/config/swal";
|
||||
import { Link, useRouter } from "@/i18n/routing";
|
||||
import { NewCampaignIcon } from "@/components/icon";
|
||||
import { getCategories } from "@/service/settings/settings";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { useTranslations } from "next-intl";
|
||||
import {
|
||||
Popover,
|
||||
|
|
@ -188,7 +176,7 @@ const AdminSettingTrackingTable = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="w-full overflow-x-auto bg-white p-4 rounded-sm space-y-3">
|
||||
<div className="w-full overflow-x-auto bg-white dark:bg-black p-4 rounded-sm space-y-3">
|
||||
<div className="flex items-end justify-between">
|
||||
{/* <CreateSettingTracking /> */}
|
||||
<div className="flex-none">
|
||||
|
|
|
|||
|
|
@ -109,12 +109,12 @@ export default function CreateTagModal() {
|
|||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button color="primary" size="md">
|
||||
{t("add-tags")}
|
||||
{t("add-tags", { defaultValue: "Add Tags" })}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent size="md">
|
||||
<DialogHeader>
|
||||
<DialogTitle> {t("add-tags")}</DialogTitle>
|
||||
<DialogTitle> {t("add-tags", { defaultValue: "Add Tags" })}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ const AdminTagTable = () => {
|
|||
return (
|
||||
<div className="w-full overflow-x-auto bg-white p-4 rounded-sm space-y-3">
|
||||
<div className="flex justify-between mb-10 items-center">
|
||||
<p className="text-xl font-medium text-default-900">{t("tags")}</p>
|
||||
<p className="text-xl font-medium text-default-900">{t("tags", { defaultValue: "Tags" })}</p>
|
||||
<CreateFAQModal />
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -54,22 +54,7 @@ import { Badge } from "@/components/ui/badge";
|
|||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import TablePagination from "@/components/table/table-pagination";
|
||||
import columns from "./column";
|
||||
import { getPlanningPagination } from "@/service/agenda-setting/agenda-setting";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import {
|
||||
getMediaBlastCampaignPage,
|
||||
listDataMedia,
|
||||
} from "@/service/broadcast/broadcast";
|
||||
import { listEnableCategory } from "@/service/content/content";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { close, loading } from "@/config/swal";
|
||||
import { Link } from "@/i18n/routing";
|
||||
import { NewCampaignIcon } from "@/components/icon";
|
||||
import search from "../../../app/chat/components/search";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
|
|
@ -79,7 +64,6 @@ import {
|
|||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import {
|
||||
Bar,
|
||||
BarChart,
|
||||
|
|
@ -206,7 +190,7 @@ const SurveyListTable = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="w-full overflow-x-auto bg-white p-4 rounded-sm space-y-3">
|
||||
<div className="w-full overflow-x-auto bg-white dark:bg-black p-4 rounded-sm space-y-3">
|
||||
<div className="flex-1 text-xl font-medium text-default-900">Survey</div>
|
||||
<div className="flex flex-row gap-2 items-center justify-between">
|
||||
<div className="w-full md:w-[200px] lg:w-[300px] px-2">
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ export default function AdminSurvey() {
|
|||
return (
|
||||
<div>
|
||||
<SiteBreadcrumb />
|
||||
<div className="w-full overflow-x-auto bg-white p-4 rounded-sm space-y-3">
|
||||
<div className="w-full overflow-x-auto bg-white p-4 rounded-sm space-y-3">
|
||||
<SurveyListTable />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,243 +0,0 @@
|
|||
"use client";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import FullCalendar from "@fullcalendar/react"; // must go before plugins
|
||||
import dayGridPlugin from "@fullcalendar/daygrid";
|
||||
import timeGridPlugin from "@fullcalendar/timegrid";
|
||||
import interactionPlugin, { Draggable } from "@fullcalendar/interaction";
|
||||
import listPlugin from "@fullcalendar/list";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import ExternalDraggingevent from "./dragging-events";
|
||||
import { Calendar } from "@/components/ui/calendar";
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import { Plus } from "lucide-react";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { CalendarEvent, CalendarCategory } from "./data"
|
||||
import {
|
||||
EventContentArg,
|
||||
} from '@fullcalendar/core'
|
||||
import EventModal from "./event-modal";
|
||||
import { useTranslations } from "next-intl";
|
||||
const wait = () => new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
interface CalendarViewProps {
|
||||
events: CalendarEvent[];
|
||||
categories: CalendarCategory[];
|
||||
|
||||
|
||||
}
|
||||
|
||||
const CalendarView = ({ events, categories }: CalendarViewProps) => {
|
||||
const [selectedCategory, setSelectedCategory] = useState<string[] | null>(null);
|
||||
const [selectedEventDate, setSelectedEventDate] = useState<Date | null>(null);
|
||||
const [selectedEvent, setSelectedEvent] = useState<CalendarEvent | null>(null);
|
||||
const [draggableInitialized, setDraggableInitialized] = useState<boolean>(false);
|
||||
const t = useTranslations("CalendarApp")
|
||||
// event canvas state
|
||||
const [sheetOpen, setSheetOpen] = useState<boolean>(false);
|
||||
const [date, setDate] = React.useState<Date>(new Date());
|
||||
|
||||
const [dragEvents] = useState([
|
||||
{ title: "New Event Planning", id: "101", tag: "business" },
|
||||
{ title: "Meeting", id: "102", tag: "meeting" },
|
||||
{ title: "Generating Reports", id: "103", tag: "holiday" },
|
||||
{ title: "Create New theme", id: "104", tag: "etc" },
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedCategory(categories?.map((c) => c.value));
|
||||
}, [events, categories]);
|
||||
|
||||
useEffect(() => {
|
||||
const draggableEl = document.getElementById("external-events");
|
||||
|
||||
const initDraggable = () => {
|
||||
if (draggableEl) {
|
||||
new Draggable(draggableEl, {
|
||||
itemSelector: ".fc-event",
|
||||
eventData: function (eventEl) {
|
||||
let title = eventEl.getAttribute("title");
|
||||
let id = eventEl.getAttribute("data");
|
||||
let event = dragEvents.find((e) => e.id === id);
|
||||
let tag = event ? event.tag : "";
|
||||
return {
|
||||
title: title,
|
||||
id: id,
|
||||
extendedProps: {
|
||||
calendar: tag,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (dragEvents.length > 0) {
|
||||
initDraggable();
|
||||
}
|
||||
|
||||
return () => {
|
||||
draggableEl?.removeEventListener("mousedown", initDraggable);
|
||||
};
|
||||
}, [dragEvents]);
|
||||
// event click
|
||||
const handleEventClick = (arg: any) => {
|
||||
setSelectedEventDate(null);
|
||||
setSheetOpen(true);
|
||||
setSelectedEvent(arg);
|
||||
wait().then(() => (document.body.style.pointerEvents = "auto"));
|
||||
};
|
||||
// handle close modal
|
||||
const handleCloseModal = () => {
|
||||
setSheetOpen(false);
|
||||
setSelectedEvent(null);
|
||||
setSelectedEventDate(null);
|
||||
};
|
||||
const handleDateClick = (arg: any) => {
|
||||
setSheetOpen(true);
|
||||
setSelectedEventDate(arg);
|
||||
setSelectedEvent(null);
|
||||
wait().then(() => (document.body.style.pointerEvents = "auto"));
|
||||
};
|
||||
|
||||
const handleCategorySelection = (category: string) => {
|
||||
if (selectedCategory && selectedCategory.includes(category)) {
|
||||
setSelectedCategory(selectedCategory.filter((c) => c !== category));
|
||||
} else {
|
||||
setSelectedCategory([...selectedCategory || [], category]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClassName = (arg: EventContentArg) => {
|
||||
|
||||
if (arg.event.extendedProps.calendar === "holiday") {
|
||||
return "destructive";
|
||||
}
|
||||
else if (arg.event.extendedProps.calendar === "business") {
|
||||
return "primary";
|
||||
} else if (arg.event.extendedProps.calendar === "personal") {
|
||||
return "success";
|
||||
} else if (arg.event.extendedProps.calendar === "family") {
|
||||
return "info";
|
||||
} else if (arg.event.extendedProps.calendar === "etc") {
|
||||
return "info";
|
||||
} else if (arg.event.extendedProps.calendar === "meeting") {
|
||||
return "warning";
|
||||
}
|
||||
else {
|
||||
return "primary";
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
const filteredEvents = events?.filter((event) =>
|
||||
selectedCategory?.includes(event.extendedProps.calendar)
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-cols-12 gap-6 divide-x divide-border">
|
||||
<Card className="col-span-12 lg:col-span-4 2xl:col-span-3 pb-5">
|
||||
<CardContent className="p-0">
|
||||
<CardHeader className="border-none mb-2 pt-5">
|
||||
<Button
|
||||
onClick={handleDateClick}
|
||||
className="dark:bg-background dark:text-foreground"
|
||||
>
|
||||
<Plus className="w-4 h-4 me-1" />
|
||||
{t("addEvent")}
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<div className="px-3">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={date}
|
||||
onSelect={(s) => {
|
||||
handleDateClick(s);
|
||||
}}
|
||||
className="rounded-md border w-full p-0 border-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="external-events" className=" space-y-1.5 mt-6 px-4">
|
||||
<p className="text-sm font-medium text-default-700 mb-3">
|
||||
{t("shortDesc")}
|
||||
</p>
|
||||
{dragEvents.map((event) => (
|
||||
<ExternalDraggingevent key={event.id} event={event} />
|
||||
))}
|
||||
</div>
|
||||
<div className="py-4 text-default-800 font-semibold text-xs uppercase mt-4 mb-2 px-4">
|
||||
{t("filter")}
|
||||
</div>
|
||||
<ul className="space-y-3 px-4">
|
||||
<li className=" flex gap-3">
|
||||
<Checkbox
|
||||
checked={selectedCategory?.length === categories?.length}
|
||||
onClick={() => {
|
||||
if (selectedCategory?.length === categories?.length) {
|
||||
setSelectedCategory([]);
|
||||
} else {
|
||||
setSelectedCategory(categories.map((c) => c.value));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Label>All</Label>
|
||||
</li>
|
||||
{categories?.map((category) => (
|
||||
<li className="flex gap-3 " key={category.value}>
|
||||
<Checkbox
|
||||
className={category.className}
|
||||
id={category.label}
|
||||
checked={selectedCategory?.includes(category.value)}
|
||||
onClick={() => handleCategorySelection(category.value)}
|
||||
/>
|
||||
<Label htmlFor={category.label}>{category.label}</Label>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="col-span-12 lg:col-span-8 2xl:col-span-9 pt-5">
|
||||
<CardContent className="dashcode-app-calendar">
|
||||
<FullCalendar
|
||||
plugins={[
|
||||
dayGridPlugin,
|
||||
timeGridPlugin,
|
||||
interactionPlugin,
|
||||
listPlugin,
|
||||
]}
|
||||
headerToolbar={{
|
||||
left: "prev,next today",
|
||||
center: "title",
|
||||
right: "dayGridMonth,timeGridWeek,timeGridDay,listWeek",
|
||||
}}
|
||||
events={filteredEvents}
|
||||
editable={true}
|
||||
rerenderDelay={10}
|
||||
eventDurationEditable={false}
|
||||
selectable={true}
|
||||
selectMirror={true}
|
||||
droppable={true}
|
||||
dayMaxEvents={2}
|
||||
weekends={true}
|
||||
eventClassNames={handleClassName}
|
||||
dateClick={handleDateClick}
|
||||
eventClick={handleEventClick}
|
||||
initialView="dayGridMonth"
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<EventModal
|
||||
open={sheetOpen}
|
||||
onClose={handleCloseModal}
|
||||
categories={categories}
|
||||
event={selectedEvent}
|
||||
selectedDate={selectedEventDate}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CalendarView;
|
||||
|
|
@ -1,155 +0,0 @@
|
|||
import { faker } from "@faker-js/faker";
|
||||
|
||||
const date = new Date();
|
||||
const prevDay = new Date().getDate() - 1;
|
||||
const nextDay = new Date(new Date().getTime() + 24 * 60 * 60 * 1000);
|
||||
|
||||
// prettier-ignore
|
||||
const nextMonth = date.getMonth() === 11 ? new Date(date.getFullYear() + 1, 0, 1) : new Date(date.getFullYear(), date.getMonth() + 1, 1)
|
||||
// prettier-ignore
|
||||
const prevMonth = date.getMonth() === 11 ? new Date(date.getFullYear() - 1, 0, 1) : new Date(date.getFullYear(), date.getMonth() - 1, 1)
|
||||
export const calendarEvents = [
|
||||
{
|
||||
id: faker.string.uuid() ,
|
||||
title: "All Day Event",
|
||||
start: date,
|
||||
end: nextDay,
|
||||
allDay: false,
|
||||
//className: "warning",
|
||||
extendedProps: {
|
||||
calendar: "business",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: faker.string.uuid(),
|
||||
title: "Meeting With Client",
|
||||
start: new Date(date.getFullYear(), date.getMonth() + 1, -11),
|
||||
end: new Date(date.getFullYear(), date.getMonth() + 1, -10),
|
||||
allDay: true,
|
||||
//className: "success",
|
||||
extendedProps: {
|
||||
calendar: "personal",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: faker.string.uuid(),
|
||||
title: "Lunch",
|
||||
allDay: true,
|
||||
start: new Date(date.getFullYear(), date.getMonth() + 1, -9),
|
||||
end: new Date(date.getFullYear(), date.getMonth() + 1, -7),
|
||||
// className: "info",
|
||||
extendedProps: {
|
||||
calendar: "family",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: faker.string.uuid(),
|
||||
title: "Birthday Party",
|
||||
start: new Date(date.getFullYear(), date.getMonth() + 1, -11),
|
||||
end: new Date(date.getFullYear(), date.getMonth() + 1, -10),
|
||||
allDay: true,
|
||||
//className: "primary",
|
||||
extendedProps: {
|
||||
calendar: "meeting",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: faker.string.uuid(),
|
||||
title: "Birthday Party",
|
||||
start: new Date(date.getFullYear(), date.getMonth() + 1, -13),
|
||||
end: new Date(date.getFullYear(), date.getMonth() + 1, -12),
|
||||
allDay: true,
|
||||
// className: "danger",
|
||||
extendedProps: {
|
||||
calendar: "holiday",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: faker.string.uuid(),
|
||||
title: "Monthly Meeting",
|
||||
start: nextMonth,
|
||||
end: nextMonth,
|
||||
allDay: true,
|
||||
//className: "primary",
|
||||
extendedProps: {
|
||||
calendar: "business",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const calendarCategories = [
|
||||
{
|
||||
label: "Business",
|
||||
value: "business",
|
||||
activeClass: "ring-primary-500 bg-primary-500",
|
||||
className: "group-hover:border-blue-500",
|
||||
},
|
||||
{
|
||||
label: "Personal",
|
||||
value: "personal",
|
||||
activeClass: "ring-success-500 bg-success-500",
|
||||
className: " group-hover:border-green-500",
|
||||
},
|
||||
{
|
||||
label: "Holiday",
|
||||
value: "holiday",
|
||||
activeClass: "ring-danger-500 bg-danger-500",
|
||||
className: " group-hover:border-red-500",
|
||||
},
|
||||
{
|
||||
label: "Family",
|
||||
value: "family",
|
||||
activeClass: "ring-info-500 bg-info-500",
|
||||
className: " group-hover:border-cyan-500",
|
||||
},
|
||||
{
|
||||
label: "Meeting",
|
||||
value: "meeting",
|
||||
activeClass: "ring-warning-500 bg-warning-500",
|
||||
className: " group-hover:border-yellow-500",
|
||||
},
|
||||
{
|
||||
label: "Etc",
|
||||
value: "etc",
|
||||
activeClass: "ring-info-500 bg-info-500",
|
||||
className: " group-hover:border-cyan-500",
|
||||
}
|
||||
];
|
||||
|
||||
export const categories = [
|
||||
{
|
||||
label: "Business",
|
||||
value: "business",
|
||||
className: "data-[state=checked]:bg-primary data-[state=checked]:ring-primary",
|
||||
},
|
||||
{
|
||||
label: "Personal",
|
||||
value: "personal",
|
||||
|
||||
className: "data-[state=checked]:bg-success data-[state=checked]:ring-success",
|
||||
},
|
||||
{
|
||||
label: "Holiday",
|
||||
value: "holiday",
|
||||
className: "data-[state=checked]:bg-destructive data-[state=checked]:ring-destructive ",
|
||||
},
|
||||
{
|
||||
label: "Family",
|
||||
value: "family",
|
||||
className: "data-[state=checked]:bg-info data-[state=checked]:ring-info ",
|
||||
},
|
||||
{
|
||||
label: "Meeting",
|
||||
value: "meeting",
|
||||
className: "data-[state=checked]:bg-warning data-[state=checked]:ring-warning",
|
||||
},
|
||||
{
|
||||
label: "Etc",
|
||||
value: "etc",
|
||||
className: "data-[state=checked]:bg-info data-[state=checked]:ring-info",
|
||||
}
|
||||
];
|
||||
|
||||
export type CalendarEvent = (typeof calendarEvents)[number]
|
||||
export type CalendarCategory = (typeof calendarCategories)[number]
|
||||
export type Category = (typeof categories)[number]
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
import { cn } from "@/lib/utils";
|
||||
const ExternalDraggingevent = ({ event }: any) => {
|
||||
const { title, id, tag } = event;
|
||||
|
||||
return (
|
||||
<div
|
||||
title={title}
|
||||
data-id={id}
|
||||
className="fc-event px-4 py-1.5 bg-default-100 dark:bg-default-300 rounded text-sm flex items-center gap-2 shadow-sm cursor-move" >
|
||||
<span
|
||||
className={cn("h-2 w-2 rounded-full block", {
|
||||
"bg-primary": tag === "business",
|
||||
"bg-warning": tag === "meeting",
|
||||
"bg-destructive": tag === "holiday",
|
||||
"bg-info": tag === "etc",
|
||||
})}
|
||||
></span>
|
||||
<span className="text-sm font-medium text-default-900">{title}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExternalDraggingevent;
|
||||
|
|
@ -1,285 +0,0 @@
|
|||
"use client";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { format } from "date-fns";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Calendar } from "@/components/ui/calendar";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
import { Loader2, CalendarIcon } from "lucide-react";
|
||||
import DeleteConfirmationDialog from "@/components/delete-confirmation-dialog";
|
||||
import { CalendarCategory } from "./data";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
const schema = z.object({
|
||||
title: z.string().min(3, { message: "Required" }),
|
||||
});
|
||||
|
||||
const EventModal = ({
|
||||
open,
|
||||
onClose,
|
||||
categories,
|
||||
event,
|
||||
selectedDate,
|
||||
}: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
categories: any;
|
||||
event: any;
|
||||
selectedDate: any;
|
||||
}) => {
|
||||
const [startDate, setStartDate] = useState<Date>(new Date());
|
||||
const [endDate, setEndDate] = useState<Date>(new Date());
|
||||
const [isPending, startTransition] = React.useTransition();
|
||||
const [calendarProps, setCalendarProps] = React.useState<any>(
|
||||
categories[0].value
|
||||
);
|
||||
// delete modal state
|
||||
const [deleteModalOpen, setDeleteModalOpen] = useState<boolean>(false);
|
||||
const [eventIdToDelete, setEventIdToDelete] = useState<string | null>(null);
|
||||
|
||||
const {
|
||||
register,
|
||||
control,
|
||||
reset,
|
||||
setValue,
|
||||
formState: { errors },
|
||||
handleSubmit,
|
||||
} = useForm({
|
||||
resolver: zodResolver(schema),
|
||||
mode: "all",
|
||||
});
|
||||
|
||||
const onSubmit = (data: any) => {
|
||||
startTransition(() => {
|
||||
if (!event) {
|
||||
data.start = startDate;
|
||||
data.end = endDate;
|
||||
data.allDay = false;
|
||||
data.extendedProps = {
|
||||
calendar: calendarProps,
|
||||
};
|
||||
}
|
||||
if (event) {
|
||||
}
|
||||
});
|
||||
};
|
||||
useEffect(() => {
|
||||
if (selectedDate) {
|
||||
setStartDate(selectedDate.date);
|
||||
setEndDate(selectedDate.date);
|
||||
}
|
||||
if (event) {
|
||||
setStartDate(event?.event?.start);
|
||||
setEndDate(event?.event?.end);
|
||||
const eventCalendar = event?.event?.extendedProps?.calendar;
|
||||
if (eventCalendar) {
|
||||
setCalendarProps(eventCalendar);
|
||||
} else {
|
||||
setCalendarProps(categories[0].value);
|
||||
}
|
||||
}
|
||||
setValue("title", event?.event?.title || "");
|
||||
}, [event, selectedDate, open, categories, setValue]);
|
||||
|
||||
const onDeleteEventAction = async () => {
|
||||
try {
|
||||
} catch (error) {}
|
||||
};
|
||||
|
||||
const handleOpenDeleteModal = (eventId: string) => {
|
||||
setEventIdToDelete(eventId);
|
||||
setDeleteModalOpen(true);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DeleteConfirmationDialog
|
||||
open={deleteModalOpen}
|
||||
onClose={() => setDeleteModalOpen(false)}
|
||||
onConfirm={onDeleteEventAction}
|
||||
defaultToast={false}
|
||||
/>
|
||||
<Dialog open={open} onOpenChange={onClose}>
|
||||
<DialogContent onPointerDownOutside={onClose}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{event ? "Edit Event" : "Create Event"} {event?.title}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="mt-6 h-full">
|
||||
<form className="h-full" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="space-y-4 pb-5 ">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="title">Event Name</Label>
|
||||
<Input
|
||||
id="title"
|
||||
type="text"
|
||||
placeholder="Enter Event Name"
|
||||
{...register("title")}
|
||||
/>
|
||||
{errors?.title?.message && (
|
||||
<div className="text-destructive text-sm">
|
||||
{typeof errors?.title?.message === "string"
|
||||
? errors?.title?.message
|
||||
: JSON.stringify(errors?.title?.message)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="startDate">Start Date </Label>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="md"
|
||||
className={cn(
|
||||
"w-full justify-between text-left font-normal border-default-200 text-default-600 md:px-4",
|
||||
!startDate && "text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{startDate ? (
|
||||
format(startDate, "PP")
|
||||
) : (
|
||||
<span>Pick a date</span>
|
||||
)}
|
||||
<CalendarIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0">
|
||||
<Controller
|
||||
name="startDate"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={startDate}
|
||||
onSelect={(date) => setStartDate(date as Date)}
|
||||
initialFocus
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="endDate">End Date</Label>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="md"
|
||||
className={cn(
|
||||
"w-full justify-between text-left font-normal border-default-200 text-default-600 md:px-4",
|
||||
!endDate && "text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{endDate ? (
|
||||
format(endDate, "PP")
|
||||
) : (
|
||||
<span>Pick a date</span>
|
||||
)}
|
||||
<CalendarIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0">
|
||||
<Controller
|
||||
name="endDate"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={endDate}
|
||||
onSelect={(date) => setEndDate(date as Date)}
|
||||
initialFocus
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="calendarProps">Label </Label>
|
||||
<Controller
|
||||
name="calendarProps"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
value={calendarProps}
|
||||
onValueChange={(data) => setCalendarProps(data)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Label" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{categories.map((category: CalendarCategory) => (
|
||||
<SelectItem
|
||||
value={category.value}
|
||||
key={category.value}
|
||||
>
|
||||
{category.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2 mt-10">
|
||||
<Button type="submit" disabled={isPending} className="flex-1">
|
||||
{isPending ? (
|
||||
<>
|
||||
<Loader2 className="me-2 h-4 w-4 animate-spin" />
|
||||
{event ? "Updating..." : "Adding..."}
|
||||
</>
|
||||
) : event ? (
|
||||
"Update Event"
|
||||
) : (
|
||||
"Add Event"
|
||||
)}
|
||||
</Button>
|
||||
{event && (
|
||||
<Button
|
||||
type="button"
|
||||
color="destructive"
|
||||
onClick={() => handleOpenDeleteModal(event?.event?.id)}
|
||||
className="flex-1"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventModal;
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
import { getEvents, getCategories } from "./utils";
|
||||
import { Category } from "./data"
|
||||
import CalendarView from "./calender-view";
|
||||
|
||||
|
||||
|
||||
const CalenderPage = async () => {
|
||||
const events = await getEvents();
|
||||
const categories = await getCategories();
|
||||
const formattedCategories = categories.map((category: Category) => ({
|
||||
...category,
|
||||
activeClass: "",
|
||||
}));
|
||||
return (
|
||||
<div>
|
||||
<CalendarView events={events} categories={formattedCategories} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CalenderPage;
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
import { calendarEvents, categories } from "./data";
|
||||
|
||||
// get events
|
||||
export const getEvents = async () => {
|
||||
return calendarEvents;
|
||||
};
|
||||
|
||||
// get categories
|
||||
export const getCategories = async () => {
|
||||
return categories;
|
||||
}
|
||||
|
|
@ -1,123 +0,0 @@
|
|||
"use client";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Icon } from "@/components/ui/icon";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { useMediaQuery } from "@/hooks/use-media-query";
|
||||
import { Contact, ProfileUser } from "@/app/api/chat/data";
|
||||
import { useChatConfig } from "@/hooks/use-chat";
|
||||
|
||||
const ChatHeader = ({ contact }: { contact: any }) => {
|
||||
let active = true;
|
||||
const isLg = useMediaQuery("(max-width: 1024px)");
|
||||
|
||||
const [chatConfig, setChatConfig] = useChatConfig()
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<div className="flex-1 flex gap-3 items-center">
|
||||
{isLg && (
|
||||
<Button size="icon" variant='ghost' color="secondary" onClick={() => setChatConfig({
|
||||
...chatConfig,
|
||||
isOpen: true
|
||||
})}>
|
||||
<Icon icon="heroicons-outline:menu-alt-1" className="w-5 h-5" />
|
||||
</Button>
|
||||
)}
|
||||
<div className="relative inline-block">
|
||||
<Avatar className="border-none shadow-none bg-transparent hover:bg-transparent">
|
||||
<AvatarImage src={contact?.avatar?.src} alt="" />
|
||||
<AvatarFallback>{contact?.fullName?.slice(0, 2)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<Badge
|
||||
className=" h-2 w-2 p-0 ring-1 ring-border ring-offset-[1px] absolute top-2 -end-0.5"
|
||||
color={active ? "success" : "secondary"}
|
||||
></Badge>
|
||||
</div>
|
||||
<div className="hidden lg:block">
|
||||
<div className="text-default-800 text-sm font-medium mb-0.5 ">
|
||||
<span className="relative">{contact?.fullName}</span>
|
||||
</div>
|
||||
<div className="text-default-600 text-xs font-normal">
|
||||
{active ? "Active Now" : "Offline"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-none flex gap-2">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
className="rounded-full bg-default-100 text-default-900 hover:bg-default-100 hover:ring-0 hover:ring-transparent"
|
||||
>
|
||||
<Icon icon="heroicons-outline:phone" className="text-xl" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" align="end">
|
||||
<p>Start a voice call</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
className="rounded-full bg-default-100 text-default-900 hover:bg-default-100 hover:ring-0 hover:ring-transparent"
|
||||
>
|
||||
<Icon icon="heroicons-outline:video-camera" className="text-xl" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" align="end">
|
||||
<p>Start a video call</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
{!isLg && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
onClick={() => setChatConfig({ ...chatConfig, showInfo: !chatConfig.showInfo })}
|
||||
type="button"
|
||||
size="icon"
|
||||
className={cn(
|
||||
"rounded-full bg-default-100 text-primary hover:bg-default-100 hover:ring-0 hover:ring-transparent",
|
||||
{
|
||||
"text-default-900": !chatConfig.showInfo,
|
||||
}
|
||||
)}
|
||||
|
||||
>
|
||||
<span className="text-xl ">
|
||||
{chatConfig.showInfo ? (
|
||||
<Icon icon="heroicons-outline:dots-vertical" className="text-xl" />
|
||||
|
||||
) : (
|
||||
<Icon icon="heroicons-outline:dots-horizontal" className="text-xl" />
|
||||
)}
|
||||
</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" align="end">
|
||||
<p>Conversation information</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatHeader;
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { useChatConfig } from '@/hooks/use-chat';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
|
||||
const InfoWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
const [chatConfig] = useChatConfig();
|
||||
if (!chatConfig.showInfo) return null
|
||||
return (
|
||||
<Card className='w-[285px]'>
|
||||
<ScrollArea className='h-full'>
|
||||
<CardContent className='p-0'> {children}</CardContent>
|
||||
</ScrollArea>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default InfoWrapper
|
||||
|
|
@ -1,143 +0,0 @@
|
|||
"use client";
|
||||
import React, { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Icon } from "@/components/ui/icon";
|
||||
import { Annoyed, SendHorizontal } from "lucide-react";
|
||||
|
||||
import data from "@emoji-mart/data";
|
||||
import Picker from "@emoji-mart/react";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipArrow,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { postMessageAction } from "@/action/app-actions";
|
||||
import { useTheme } from "next-themes";
|
||||
|
||||
const MessageFooter = () => {
|
||||
const { theme: mode } = useTheme();
|
||||
const [message, setMessage] = useState("");
|
||||
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setMessage(e.target.value);
|
||||
e.target.style.height = "auto"; // Reset the height to auto to adjust
|
||||
e.target.style.height = `${e.target.scrollHeight - 15}px`;
|
||||
};
|
||||
|
||||
const handleSelectEmoji = (emoji: any) => {
|
||||
setMessage(message + emoji.native);
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
if (!message) return;
|
||||
const data: any = {
|
||||
message,
|
||||
};
|
||||
|
||||
await postMessageAction("55fe838e-9a09-4caf-a591-559803309ef1", "sfsfsf");
|
||||
setMessage("");
|
||||
|
||||
|
||||
};
|
||||
return (
|
||||
<>
|
||||
|
||||
|
||||
<div
|
||||
className="w-full flex items-end gap-1 lg:gap-4 lg:px-4 relative px-2 "
|
||||
>
|
||||
<div className="flex-none flex gap-1 absolute md:static top-0 left-1.5 z-10 ">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
className="rounded-full hover:ring-0 hover:ring-transparent bg-default-100 hover:bg-default-100 hover:text-default-900 text-default-900">
|
||||
<Icon icon="heroicons-outline:link" className="w-5 h-5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Add link</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
className="rounded-full hover:ring-0 hover:ring-transparent bg-default-100 hover:bg-default-100 hover:text-default-900 text-default-900">
|
||||
<Annoyed className="w-6 h-6 text-default" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent side="top" align="start" className="w-fit p-0 shadow-none border-none bottom-0 rtl:left-5 ltr:-left-[110px]">
|
||||
<Picker
|
||||
data={data}
|
||||
onEmojiSelect={handleSelectEmoji}
|
||||
theme={mode === "dark" ? "dark" : "light"}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="flex gap-1 relative">
|
||||
<textarea
|
||||
value={message}
|
||||
onChange={handleChange}
|
||||
placeholder="Type your message..."
|
||||
className="bg-background focus:outline-none rounded-xl break-words ps-8 md:ps-3 px-3 flex-1 h-10 pt-2 p-1 pr-8 no-scrollbar "
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSubmit(e as any);
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
minHeight: "40px",
|
||||
maxHeight: "120px",
|
||||
overflowY: "auto",
|
||||
resize: "none",
|
||||
}}
|
||||
/>
|
||||
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
type="submit"
|
||||
className="rounded-full hover:ring-0 hover:ring-transparent bg-default-100 hover:bg-default-100 hover:text-default-900 text-default-900"
|
||||
>
|
||||
<Icon
|
||||
icon="heroicons-outline:paper-airplane"
|
||||
className="transform rotate-[60deg] w-5 h-5"
|
||||
/>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent align="end">
|
||||
<p>Send Message</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MessageFooter;
|
||||
|
|
@ -1,241 +0,0 @@
|
|||
|
||||
import { getChatsByContactId, getProfileUser } from '../utils'
|
||||
import { Icon } from "@/components/ui/icon";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Card, CardContent, CardHeader, CardFooter } from "@/components/ui/card";
|
||||
import Image from 'next/image';
|
||||
import { redirect } from '@/components/navigation';
|
||||
import MessageFooter from './components/message-footer';
|
||||
import ChatHeader from './components/chat-header';
|
||||
import InfoWrapper from './components/info-wrapper';
|
||||
import { MoreHorizontal } from 'lucide-react';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
|
||||
const socials = [
|
||||
{
|
||||
name: "facebook",
|
||||
icon: "bi:facebook",
|
||||
link: "#",
|
||||
},
|
||||
{
|
||||
name: "twitter",
|
||||
link: "#",
|
||||
icon: "bi:twitter",
|
||||
},
|
||||
{
|
||||
name: "instagram",
|
||||
link: "#",
|
||||
icon: "bi:instagram",
|
||||
},
|
||||
];
|
||||
|
||||
const ChatPageSingle = async ({ params: { id } }: { params: { id: string }; }) => {
|
||||
|
||||
const { chat, contact } = await getChatsByContactId(id)
|
||||
const profile = await getProfileUser()
|
||||
|
||||
if (!contact) {
|
||||
redirect('/app/chat')
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="h-full flex flex-col flex-1 ">
|
||||
<CardHeader className="flex-none mb-0 border-b border-default-200 py-5">
|
||||
<ChatHeader contact={contact} />
|
||||
</CardHeader>
|
||||
<CardContent className=" relative flex-1 overflow-y-auto no-scrollbar">
|
||||
{chat && chat?.chat?.length > 0 ? (
|
||||
chat?.chat?.map(({ senderId, message, }, index) => (
|
||||
<div className="block " key={index}>
|
||||
{senderId === "e2c1a571-5f7e-4f56-9020-13f98b0eaba2" ? (
|
||||
<>
|
||||
<div className="flex gap-2 items-start justify-end group w-full mb-4">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="opacity-0 invisible group-hover:opacity-100 group-hover:visible ">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<span className="w-7 h-7 rounded-full bg-default-200 flex items-center justify-center">
|
||||
<MoreHorizontal />
|
||||
</span>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-fit"
|
||||
align="end"
|
||||
side="top"
|
||||
>
|
||||
<DropdownMenuItem>Remove</DropdownMenuItem>
|
||||
<DropdownMenuItem>Forward</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<div className="whitespace-pre-wrap break-all">
|
||||
<div className="bg-default-100 text-default-900 text-sm p-3 font-normal rounded-md ">
|
||||
{message}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="font-normal text-xs text-default-400 dark:text-default-600 text-start mt-1"> 2:40 pm </div>
|
||||
</div>
|
||||
<div className="flex-none self-end -translate-y-5">
|
||||
<div className="h-8 w-8 rounded-full ">
|
||||
<Image
|
||||
src={profile?.avatar}
|
||||
alt=""
|
||||
className="block w-full h-full object-cover rounded-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex gap-2 items-start group mb-4">
|
||||
<div className="flex-none self-end -translate-y-5">
|
||||
<div className="h-8 w-8 rounded-full">
|
||||
<Image
|
||||
src={contact?.avatar || `/images/users/user-5.jpg`}
|
||||
alt=""
|
||||
className="block w-full h-full object-cover rounded-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col gap-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="whitespace-pre-wrap break-all relative z-[1]">
|
||||
<div className="bg-default-100 text-default-900 text-sm p-3 font-normal rounded-md ">
|
||||
{message}
|
||||
</div>
|
||||
</div>
|
||||
<div className="opacity-0 invisible group-hover:opacity-100 group-hover:visible ">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<span className="w-7 h-7 rounded-full bg-default-200 flex items-center justify-center">
|
||||
<MoreHorizontal />
|
||||
</span>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-fit"
|
||||
align="end"
|
||||
side="top"
|
||||
>
|
||||
<DropdownMenuItem>Remove</DropdownMenuItem>
|
||||
<DropdownMenuItem>Forward</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
<div className="font-normal text-xs text-default-400 dark:text-default-600 text-start mt-1"> 2:40 pm </div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="text-center absolute start-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">
|
||||
<Icon icon="typcn:messages" className="h-20 w-20 text-default-300 mx-auto" />
|
||||
<div className="mt-4 text-lg font-medium text-default-500">No messages </div>
|
||||
<div className="mt-1 text-sm font-medium text-default-400">{`don't worry, just take a deep breath & say "Hello"`}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</CardContent>
|
||||
<CardFooter className="flex-none flex-col px-0 py-4 border-t border-border">
|
||||
<MessageFooter />
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
<InfoWrapper>
|
||||
<h4 className="text-xl text-default-900 font-medium mb-8 px-6 mt-6">
|
||||
About
|
||||
</h4>
|
||||
|
||||
<div className='flex flex-col items-center px-6'>
|
||||
<Avatar className="h-24 w-24 border-none shadow-none bg-transparent hover:bg-transparent">
|
||||
<AvatarImage src={contact?.avatar?.src || `/images/users/user-5.jpg`} alt="" />
|
||||
<AvatarFallback>{contact?.fullName?.slice(0, 2)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="text-center mt-4 ">
|
||||
<h5 className="text-base text-default-600 font-medium mb-1">
|
||||
{contact?.fullName}
|
||||
</h5>
|
||||
<h6 className="text-xs text-default-600 font-normal">
|
||||
{contact?.role}
|
||||
</h6>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul className="mt-5 px-6 space-y-4 border-b border-default-200 pb-5 ">
|
||||
<li className="flex justify-between text-sm text-default-600 ">
|
||||
<div className="flex gap-2 items-start ">
|
||||
<Icon
|
||||
icon="heroicons-outline:location-marker"
|
||||
className="text-base"
|
||||
/>
|
||||
<span>Location</span>
|
||||
</div>
|
||||
<div className="font-medium">Bangladesh</div>
|
||||
</li>
|
||||
<li className="flex justify-between text-sm text-default-600 ">
|
||||
<div className="flex gap-2 items-start">
|
||||
<Icon icon="heroicons-outline:user" className="text-base" />
|
||||
<span>Members since</span>
|
||||
</div>
|
||||
<div className="font-medium">Oct 2021</div>
|
||||
</li>
|
||||
<li className="flex justify-between text-sm text-default-600 ">
|
||||
<div className="flex gap-2 items-start ">
|
||||
<Icon icon="heroicons-outline:translate" className="text-base" />
|
||||
<span>Language</span>
|
||||
</div>
|
||||
<div className="font-medium">English</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul className="mt-5 px-6 space-y-4 border-b border-default-200 pb-5 ">
|
||||
{socials?.map((slink, sindex) => (
|
||||
<li
|
||||
key={sindex}
|
||||
className="text-sm text-default-600"
|
||||
>
|
||||
<button className="flex gap-2">
|
||||
<Icon icon={slink.icon} className="text-base" />
|
||||
<span className="capitalize font-normal text-default-600">
|
||||
{slink.name}
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<h4 className="py-4 text-sm px-6 text-default-500 font-medium">
|
||||
Shared documents
|
||||
</h4>
|
||||
<div className="grid grid-cols-3 gap-2 px-6">
|
||||
{
|
||||
["/images/chat/sd1.png", "/images/chat/sd2.png", "/images/chat/sd3.png", "/images/chat/sd4.png", "/images/chat/sd5.png", "/images/chat/sd6.png"].map((image, index) => (
|
||||
<Image
|
||||
key={`image-${index}`}
|
||||
src={image}
|
||||
alt=""
|
||||
width={200}
|
||||
height={100}
|
||||
className='w-full h-12 object-cover rounded-md'
|
||||
/>
|
||||
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</InfoWrapper>
|
||||
</>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
export default ChatPageSingle
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
'use client'
|
||||
import { useMediaQuery } from '@/hooks/use-media-query';
|
||||
import React from 'react'
|
||||
import { useChatConfig } from '@/hooks/use-chat';
|
||||
|
||||
const ChatWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
const [chatConfig, setChatConfig] = useChatConfig();
|
||||
const { isOpen } = chatConfig
|
||||
const isTablet = useMediaQuery("(min-width: 1024px)");
|
||||
return (
|
||||
<div className=' app-height flex gap-5 relative'>
|
||||
{!isTablet && isOpen && (
|
||||
<div
|
||||
onClick={() => setChatConfig({ ...chatConfig, isOpen: false })}
|
||||
className="overlay bg-default-900 dark:bg-default-900 dark:bg-opacity-60 bg-opacity-60 backdrop-filter
|
||||
backdrop-blur-sm absolute w-full flex-1 inset-0 z-20 rounded-md"
|
||||
|
||||
></div>
|
||||
)}
|
||||
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChatWrapper
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
"use client";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { useMediaQuery } from "@/hooks/use-media-query";
|
||||
import { Icon } from "@/components/ui/icon";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useChatConfig } from "@/hooks/use-chat";
|
||||
import { useTranslations } from "next-intl";
|
||||
const Blank = () => {
|
||||
const isLg = useMediaQuery("(max-width: 1024px)");
|
||||
const [chatConfig, setChatConfig] = useChatConfig()
|
||||
const t = useTranslations("ChatApp");
|
||||
return (
|
||||
<Card className="flex-1 h-full">
|
||||
<CardContent className="h-full flex justify-center items-center">
|
||||
<div className="text-center flex flex-col items-center">
|
||||
<Icon icon="uiw:message" className="text-7xl text-default-300" />
|
||||
<div className="mt-4 text-lg font-medium text-default-500">
|
||||
{t("blankMessageTitle")}
|
||||
</div>
|
||||
<p className="mt-1 text-sm font-medium text-default-400">
|
||||
{t("blankMessageDesc")}
|
||||
</p>
|
||||
{isLg && (
|
||||
<Button className="mt-2" onClick={() => setChatConfig({ ...chatConfig, isOpen: true })}>
|
||||
Start Conversation
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default Blank;
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
|
||||
'use client'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Icon } from "@/components/ui/icon";
|
||||
import { type Contact as ContactType, type Chat as ChatType } from "../utils";
|
||||
import { Link, usePathname } from "@/components/navigation";
|
||||
import { useChatConfig } from "@/hooks/use-chat";
|
||||
|
||||
|
||||
const ContactList = ({ contact }: {
|
||||
contact: ContactType,
|
||||
|
||||
}) => {
|
||||
const { avatar, id, fullName, status, about, unreadmessage, date } =
|
||||
contact;
|
||||
|
||||
const pathname = usePathname();
|
||||
const [chatConfig, setChatConfig] = useChatConfig()
|
||||
return (
|
||||
<Link
|
||||
onClick={() => setChatConfig({
|
||||
...chatConfig,
|
||||
isOpen: false
|
||||
})}
|
||||
href={`/app/chat/${id}`} className={cn(
|
||||
" gap-4 py-2 lg:py-2.5 px-3 border-l-2 border-transparent hover:bg-default-100 cursor-pointer flex ",
|
||||
{
|
||||
"lg:bg-default-100 ": `/app/chat/${id}` === pathname
|
||||
}
|
||||
)} >
|
||||
|
||||
|
||||
<div className="flex-1 flex items-center gap-3 ">
|
||||
<div className="relative inline-block ">
|
||||
<Avatar className="border-none bg-transparent hover:bg-transparent">
|
||||
<AvatarImage src={avatar.src} />
|
||||
<AvatarFallback className="uppercase">
|
||||
{fullName.slice(0, 2)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<Badge
|
||||
className=" h-2 w-2 p-0 ring-1 ring-border ring-offset-[1px] items-center justify-center absolute top-2 -end-[3px]"
|
||||
color={status === "online" ? "success" : "secondary"}
|
||||
></Badge>
|
||||
</div>
|
||||
<div className="block">
|
||||
<div className="truncate max-w-[120px]">
|
||||
<span className="text-sm text-default-900 font-medium">
|
||||
{" "}
|
||||
{fullName}
|
||||
</span>
|
||||
</div>
|
||||
<div className="truncate max-w-[120px]">
|
||||
<span className=" text-xs text-default-700 ">
|
||||
{about}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-none flex-col items-end gap-2 hidden lg:flex">
|
||||
<span className="text-xs text-default-600 text-end uppercase">
|
||||
{date}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
"h-[14px] w-[14px] flex items-center justify-center bg-default-400 rounded-full text-default-foreground text-[10px] font-medium",
|
||||
{
|
||||
"bg-[#FFC155]": unreadmessage > 0,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{unreadmessage === 0 ? (
|
||||
<Icon icon="uil:check" className="text-sm" />
|
||||
) : (
|
||||
unreadmessage
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContactList;
|
||||
|
|
@ -1,116 +0,0 @@
|
|||
'use client'
|
||||
|
||||
import { Icon } from "@/components/ui/icon"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { useChatConfig } from "@/hooks/use-chat";
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
|
||||
const MyProfile = () => {
|
||||
const [chatConfig, setChatConfig] = useChatConfig();
|
||||
let status = "active";
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between gap-1">
|
||||
<div className="flex gap-3">
|
||||
<div className="flex-none">
|
||||
<Avatar>
|
||||
<AvatarImage src="/images/users/user-1.jpg" />
|
||||
<AvatarFallback>SC</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
<div className="flex-1 text-start">
|
||||
<div className="text-default-800 text-sm font-medium mb-1">
|
||||
Jane Cooper
|
||||
<span className="bg-success inline-block h-2.5 w-2.5 rounded-full ms-3"></span>
|
||||
</div>
|
||||
<div className=" text-default-500 text-xs font-normal">
|
||||
Available
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
size="icon"
|
||||
className="w-8 h-8"
|
||||
color="secondary"
|
||||
rounded="full"
|
||||
onClick={() => setChatConfig({ ...chatConfig, showProfile: true })}
|
||||
>
|
||||
<Icon icon="heroicons-outline:dots-horizontal" className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className={cn('absolute bg-card rounded-md h-full start-0 top-0 bottom-0 w-full z-50', {
|
||||
'hidden -start-full': !chatConfig.showProfile
|
||||
})}>
|
||||
<ScrollArea className="h-full">
|
||||
<div className="p-6">
|
||||
<div className="flex justify-end">
|
||||
<Button size="icon" color="secondary" className="w-8 h-8" rounded="full" onClick={() => setChatConfig({ ...chatConfig, showProfile: false })}>
|
||||
<Icon icon="heroicons-outline:x" className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<div className="relative">
|
||||
<Avatar className="h-16 w-16 border border-default-200 p-1 bg-transparent hover:bg-transparent ">
|
||||
<AvatarImage src="/images/users/user-1.jpg" className="rounded-full" />
|
||||
<AvatarFallback>SC</AvatarFallback>
|
||||
</Avatar>
|
||||
<span
|
||||
className={cn("absolute top-3 -end-[3px] h-3 w-3 rounded-full bg-success border border-primary-foreground",
|
||||
{
|
||||
"bg-success": status === "active",
|
||||
"bg-warning": status === "away",
|
||||
"bg-destructive": status === "busy",
|
||||
"bg-secondary": status === "offline",
|
||||
}
|
||||
)}
|
||||
></span>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-default-800 text-sm font-medium mb-0.5">
|
||||
Jane Cooper
|
||||
</div>
|
||||
<div className=" text-default-500 text-xs font-normal">
|
||||
Admin
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="my-8">
|
||||
<Label htmlFor="bio" className="mb-2 block text-default-900"> About </Label>
|
||||
<Textarea id="bio" placeholder="About your self" />
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="status" className="block mb-3 text-default-700">Status</Label>
|
||||
<RadioGroup defaultValue="comfortable">
|
||||
<div className="flex items-center gap-2">
|
||||
<RadioGroupItem value="default" id="active" color="success" />
|
||||
<Label htmlFor="active">Active</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<RadioGroupItem value="busy" id="busy" color="destructive" />
|
||||
<Label htmlFor="busy">Do Not Disturb</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<RadioGroupItem value="away" id="away" color="warning" />
|
||||
<Label htmlFor="away">Away</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<RadioGroupItem value="offline" id="offline" color="warning" />
|
||||
<Label htmlFor="offline">Offline</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
<Button className="mt-7">Logout</Button>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MyProfile;
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
import { Input } from '@/components/ui/input';
|
||||
import React from 'react';
|
||||
import {Search as SearchIcon} from "lucide-react"
|
||||
const Search = () => {
|
||||
return (
|
||||
<div className='relative'>
|
||||
<SearchIcon className='absolute top-1/2 -translate-y-1/2 start-6 w-4 h-4 text-default-600' />
|
||||
<Input placeholder='Search...' className='dark:bg-transparent rounded-none border-l-0 border-r-0 dark:border-default-300 focus:border-default-200 ps-12 text-lg font-normal' size="lg"/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Search;
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
import { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Media Hub | POLRI",
|
||||
description: "Media Hub merupakan situs resmi milik Divisi Humas Polri di mana di dalamnya berisi konten-konten yang dapat diakses secara gratis oleh Internal Polri, Jurnalis, Masyarakat Umum, dan KSP.",
|
||||
};
|
||||
const Layout = ({ children }: { children: React.ReactNode }) => {
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
} from "@/components/ui/card";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import ContactList from "./components/contact-list";
|
||||
import { getContacts } from './utils';
|
||||
import MyProfile from './components/my-profile';
|
||||
import Search from './components/search';
|
||||
import ChatSidebarWrapper from './sidebar-wrapper';
|
||||
import ChatWrapper from './chat-wrapper';
|
||||
|
||||
|
||||
|
||||
const layout = async ({ children }: { children: React.ReactNode }) => {
|
||||
|
||||
const contacts = await getContacts()
|
||||
return (
|
||||
<ChatWrapper>
|
||||
<ChatSidebarWrapper>
|
||||
<Card className="h-full pb-0 ">
|
||||
<CardHeader className="border-none pb-3">
|
||||
<MyProfile />
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0 px-0 h-full ">
|
||||
<ScrollArea className="lg:h-[calc(100%-62px)] h-[calc(100%-80px)] ">
|
||||
<div className='sticky top-0 z-10 bg-card'>
|
||||
<Search />
|
||||
</div>
|
||||
{
|
||||
contacts?.map((contact) => {
|
||||
return (
|
||||
<ContactList
|
||||
key={contact.id}
|
||||
contact={contact}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</ChatSidebarWrapper>
|
||||
<div className='flex-1 h-full flex gap-5'>
|
||||
{children}
|
||||
</div>
|
||||
</ChatWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export default layout
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
import Blank from "./components/blank-chat"
|
||||
const ChatPage = async () => {
|
||||
|
||||
return (
|
||||
<Blank />
|
||||
)
|
||||
}
|
||||
|
||||
export default ChatPage
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
'use client'
|
||||
import { useMediaQuery } from '@/hooks/use-media-query'
|
||||
import { cn } from '@/lib/utils'
|
||||
import React from 'react'
|
||||
import { useChatConfig } from '@/hooks/use-chat'
|
||||
const ChatSidebarWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
const [chatConfig] = useChatConfig()
|
||||
const { isOpen } = chatConfig
|
||||
const isTablet = useMediaQuery("(min-width: 1024px)");
|
||||
if (!isTablet) {
|
||||
return (
|
||||
<div className={cn('absolute h-full start-0 w-[240px] z-50 ', {
|
||||
'-start-full': !isOpen
|
||||
})}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className='relative w-[320px] h-full '>{children}</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChatSidebarWrapper
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
import { chats, contacts, profileUser } from '@/app/api/chat/data'
|
||||
import { baseURL } from '@/config'
|
||||
|
||||
|
||||
export const getContacts = async () => {
|
||||
return contacts
|
||||
}
|
||||
|
||||
// get chats by contact id
|
||||
export const getChatsByContactId = async (contactId: string) => {
|
||||
|
||||
const chat = chats.find(chat => chat.id === contactId)
|
||||
const contact = contacts.find(contact => contact.id === contactId)
|
||||
return {
|
||||
chat,
|
||||
contact
|
||||
}
|
||||
}
|
||||
// get contact by id
|
||||
export const getContactById = async (contactId: string) => {
|
||||
return contacts.find(contact => contact.id === contactId)
|
||||
}
|
||||
|
||||
|
||||
// get profile user
|
||||
export const getProfileUser = async () => {
|
||||
return profileUser
|
||||
}
|
||||
export type Chat = typeof chats[number];
|
||||
export type Contact = typeof contacts[number];
|
||||
export type ProfileUser = typeof profileUser;
|
||||
|
||||
// post message
|
||||
export const postMessage = async (id: string, data: any) => {
|
||||
const res = await fetch(`${baseURL}/chat/${id}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error(`Error creating task`);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
'use client'
|
||||
import {
|
||||
MoveLeft,
|
||||
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useRouter } from '@/components/navigation';
|
||||
const GoBack = () => {
|
||||
const router = useRouter()
|
||||
return (
|
||||
<Button
|
||||
onClick={router.back}
|
||||
size="icon"
|
||||
className="rounded-full bg-default-100 hover:text-default-50 hover:outline-0 hover:outline-offset-0 hover:border-0 hover:ring-0 text-default-600 hover:ring-offset-0 p-4"
|
||||
>
|
||||
<MoveLeft className=" h-5 w-5" />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export default GoBack
|
||||
|
|
@ -1,186 +0,0 @@
|
|||
import { Link } from '@/i18n/routing';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardHeader, CardContent } from "@/components/ui/card";
|
||||
import {
|
||||
Box,
|
||||
Ellipsis,
|
||||
LogOutIcon,
|
||||
Printer,
|
||||
Star,
|
||||
} from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { getMailById } from "../utils";
|
||||
import { Alert } from "@/components/ui/alert";
|
||||
import GoBack from "./components/go-back"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip"
|
||||
import { Icon } from "@/components/ui/icon";
|
||||
const MailDetails = async ({ params: { id } }: { params: { id: string }; }) => {
|
||||
const mail = await getMailById(id)
|
||||
if (!mail) {
|
||||
return <Alert color="destructive"> Mail not found</Alert>
|
||||
}
|
||||
return (
|
||||
<Card className=" h-full overflow-auto">
|
||||
<CardHeader className="flex flex-row items-center justify-between p-4 border-b border-solid">
|
||||
<GoBack />
|
||||
<div className="flex gap-3">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
className="rounded-full bg-default-100 hover:text-default-50 hover:outline-0 hover:outline-offset-0 hover:border-0 hover:ring-0 text-default-600 hover:ring-offset-0 p-4"
|
||||
>
|
||||
<LogOutIcon className=" h-5 w-5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Forward</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
className="rounded-full bg-default-100 hover:text-default-50 hover:outline-0 hover:outline-offset-0 hover:border-0 hover:ring-0 text-default-600 hover:ring-offset-0 p-4"
|
||||
>
|
||||
<Star className=" h-5 w-5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Favourite</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
className="rounded-full bg-default-100 hover:text-default-50 hover:outline-0 hover:outline-offset-0 hover:border-0 hover:ring-0 text-default-600 hover:ring-offset-0 p-4"
|
||||
>
|
||||
<Box className=" h-5 w-5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Archive</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
className="rounded-full bg-default-100 hover:text-default-50 hover:outline-0 hover:outline-offset-0 hover:border-0 hover:ring-0 text-default-600 hover:ring-offset-0 p-4"
|
||||
>
|
||||
<Printer className=" h-5 w-5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Print</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
className="rounded-full bg-default-100 hover:text-default-50 hover:outline-0 hover:outline-offset-0 hover:border-0 hover:ring-0 text-default-600 hover:ring-offset-0 p-4"
|
||||
>
|
||||
<Ellipsis className=" h-5 w-5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Actions</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent >
|
||||
<p className="text-lg font-semibold text-default-800 mt-6">
|
||||
Pay bills & win up to 600$ Cashback!
|
||||
</p>
|
||||
<div className="flex items-center mt-4 gap-4">
|
||||
<Avatar className=" h-8 w-8">
|
||||
<AvatarImage
|
||||
src="https://images.unsplash.com/photo-1531427186611-ecfd6d936c79?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=687&q=80"
|
||||
alt="Avatar image"
|
||||
/>
|
||||
<AvatarFallback>JD</AvatarFallback>
|
||||
</Avatar>
|
||||
<p className="text-sm text-default-700 font-semibold">John Doe</p>
|
||||
</div>
|
||||
<div className="my-4 border-b border-solid border-default-200 pb-6 space-y-4 text-default-600 text-base">
|
||||
<p>Hi Jane Cooper,</p>
|
||||
<p>
|
||||
Jornalists call this critical, introductory section the Lede, and
|
||||
when bridge properly executed, it is that carries your reader from
|
||||
an headine try at attention-grabbing to the body of your blog post,
|
||||
if you want to get it right on of these 10 clever ways to omen your
|
||||
next blog
|
||||
</p>
|
||||
<p>
|
||||
posr with a bang With resrpect, i must disagree with Mr.Zinsser. We
|
||||
all know the most part of important part of any article is the
|
||||
title.Without a compelleing title, your reader will not even get to
|
||||
the first sentence.After the title, however, the first few sentences
|
||||
of your article are certainly the most important part.
|
||||
</p>
|
||||
<div>
|
||||
<p>Best regards,</p>
|
||||
<p>Esther Howard</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="py-5 flex gap-5 flex-wrap items-center border-b border-solid border-default-200">
|
||||
<div className="flex flex-col items-center">
|
||||
<Image
|
||||
className="w-[150px] h-[95px]"
|
||||
width={500}
|
||||
height={300}
|
||||
src="/images/all-img/inbox-1.png"
|
||||
alt="mail"
|
||||
/>
|
||||
<Link className="text-primary mt-1" href="/">Download</Link>
|
||||
</div>
|
||||
<div className="flex flex-col items-center">
|
||||
<Image
|
||||
className="w-[150px] h-[95px]"
|
||||
width={500}
|
||||
height={300}
|
||||
src="/images/all-img/inbox-2.png"
|
||||
alt="mail"
|
||||
/>
|
||||
<Link className="text-primary mt-1" href="/">Download</Link>
|
||||
</div>
|
||||
<div className="flex flex-col items-center">
|
||||
<Image
|
||||
className="w-[150px] h-[95px]"
|
||||
width={500}
|
||||
height={300}
|
||||
src="/images/all-img/inbox-3.png"
|
||||
alt="mail"
|
||||
/>
|
||||
<Link className="text-primary mt-1" href="/">Download</Link>
|
||||
</div>
|
||||
</div>
|
||||
<Button color="secondary" variant="soft" size="md" className="mt-4">
|
||||
<Icon icon="heroicons:chat-bubble-left-right" className="h-5 w-5 me-3" /> Reply
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default MailDetails;
|
||||
|
|
@ -1,121 +0,0 @@
|
|||
'use client'
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import Select, { MultiValue } from 'react-select'
|
||||
import { Plus } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { useForm, SubmitHandler, Controller } from "react-hook-form"
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
|
||||
|
||||
interface Option {
|
||||
value: string;
|
||||
label: string;
|
||||
image?: string;
|
||||
}
|
||||
|
||||
type Inputs = {
|
||||
subject: string;
|
||||
description: string;
|
||||
mailTo: MultiValue<Option>;
|
||||
}
|
||||
|
||||
const mailToOptions: Option[] = [
|
||||
{ value: "mahedi", label: "Mahedi Amin", image: "/images/avatar/av-1.svg" },
|
||||
{ value: "sovo", label: "Sovo Haldar", image: "/images/avatar/av-2.svg" },
|
||||
{ value: "rakibul", label: "Rakibul Islam", image: "/images/avatar/av-3.svg" },
|
||||
{ value: "pritom", label: "Pritom Miha", image: "/images/avatar/av-4.svg" },
|
||||
];
|
||||
|
||||
|
||||
const Compose = () => {
|
||||
const t = useTranslations("EmailApp");
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
const [value, setValue] = useState<string>('Hello World');
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
control
|
||||
} = useForm<Inputs>();
|
||||
const onSubmit: SubmitHandler<Inputs> = (data) => {
|
||||
setOpen(false)
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<Button
|
||||
fullWidth
|
||||
size="lg"
|
||||
onClick={() => setOpen(true)}
|
||||
className="dark:bg-background dark:ring-background dark:text-foreground"
|
||||
>
|
||||
<Plus className="w-6 h-6 me-1.5" />
|
||||
{t("compose")}
|
||||
</Button>
|
||||
|
||||
<DialogContent>
|
||||
<DialogHeader className="mb-6">
|
||||
<DialogTitle> {t("composeEmail")}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-3.5">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="assignee">To</Label>
|
||||
<Controller
|
||||
name="mailTo"
|
||||
control={control}
|
||||
defaultValue={[]}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
className="react-select"
|
||||
classNamePrefix="select"
|
||||
{...field}
|
||||
options={mailToOptions}
|
||||
isMulti
|
||||
onChange={(selectedOption) => field.onChange(selectedOption)}
|
||||
getOptionLabel={(option) => (
|
||||
<div className="flex items-center">
|
||||
<Image width={40} height={40} src={option.image as string} alt={option.label} className="w-8 h-8 rounded-full me-2" />
|
||||
<span className="text-sm font-medium">{option.label}</span>
|
||||
</div>
|
||||
) as unknown as string}
|
||||
placeholder="Select..."
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="subject">Subject</Label>
|
||||
<Input
|
||||
id="subject"
|
||||
placeholder="Type Subject..."
|
||||
{...register("subject")}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="description">Description</Label>
|
||||
<Textarea placeholder="Hello world" id="description" {...register("description")}/>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button>Submit</Button>
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default Compose;
|
||||
|
|
@ -1,213 +0,0 @@
|
|||
|
||||
export const mails = [
|
||||
{
|
||||
id: "fe3aa1aa-c89d-4539-8301-8bf18bd80e87",
|
||||
image: [
|
||||
{
|
||||
image: "/images/avatar/avatar-4.png",
|
||||
label: "Mahedi Amin",
|
||||
value: "mahedi",
|
||||
},
|
||||
],
|
||||
title: "laboriosam mollitia et enim quasi adipisci quia provident illum",
|
||||
isDone: false,
|
||||
name: "Ester Howard",
|
||||
isfav: false,
|
||||
time: "12.00 pm",
|
||||
isTrash: false,
|
||||
category: [
|
||||
{
|
||||
value: "team",
|
||||
label: "team",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "9d7135af-2c69-44f6-a1b5-e7c42126e1e9",
|
||||
image: [
|
||||
{
|
||||
image: "/images/avatar/avatar-2.png",
|
||||
label: "Rakibul Islam",
|
||||
value: "rakibul",
|
||||
},
|
||||
],
|
||||
title:
|
||||
"Amet minim mollit non deserunt ullamco est sit aliqua dolor do amet sint.",
|
||||
isDone: false,
|
||||
name: "Ester Howard",
|
||||
isfav: true,
|
||||
time: "12.00 pm",
|
||||
isTrash: false,
|
||||
category: [
|
||||
{
|
||||
value: "low",
|
||||
label: "low",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "fff45fba-157c-4a9a-a839-480e4c94f927",
|
||||
image: [
|
||||
{
|
||||
image: "/images/avatar/avatar-1.png",
|
||||
label: "Rakibul Islam",
|
||||
value: "rakibul",
|
||||
},
|
||||
],
|
||||
title:
|
||||
"Amet minim mollit non deserunt ullamco est sit aliqua dolor do amet sint.",
|
||||
isDone: true,
|
||||
isfav: true,
|
||||
time: "12.00 pm",
|
||||
name: "Ester Howard",
|
||||
isTrash: false,
|
||||
category: [
|
||||
{
|
||||
value: "medium",
|
||||
label: "medium",
|
||||
},
|
||||
{
|
||||
value: "low",
|
||||
label: "low",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "4c5bd598-f951-4a4e-981d-b1f0af0cc917",
|
||||
image: [
|
||||
{
|
||||
image: "/images/avatar/avatar-3.png",
|
||||
label: "Mahedi Amin",
|
||||
value: "mahedi",
|
||||
},
|
||||
],
|
||||
title: "illo expedita consequatur quia in",
|
||||
isDone: false,
|
||||
name: "Ester Howard",
|
||||
isfav: false,
|
||||
time: "12.00 pm",
|
||||
isTrash: false,
|
||||
category: [
|
||||
{
|
||||
value: "high",
|
||||
label: "high",
|
||||
},
|
||||
{
|
||||
value: "low",
|
||||
label: "low",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "bc1ad0b1-d6f8-42e1-9689-21c8f2814396",
|
||||
image: [
|
||||
{
|
||||
image: "/images/avatar/avatar-5.png",
|
||||
label: "Rakibul Islam",
|
||||
value: "rakibul",
|
||||
},
|
||||
],
|
||||
title: "illo expedita consequatur quia in",
|
||||
isDone: false,
|
||||
name: "Ester Howard",
|
||||
isfav: false,
|
||||
time: "12.00 pm",
|
||||
isTrash: false,
|
||||
category: [
|
||||
{
|
||||
value: "update",
|
||||
label: "update",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "6f4eabf5-e549-4fd1-a24f-87093ebd31be",
|
||||
image: [
|
||||
{
|
||||
image: "/images/avatar/avatar-5.png",
|
||||
label: "Rakibul Islam",
|
||||
value: "rakibul",
|
||||
},
|
||||
],
|
||||
title: "illo expedita consequatur quia in",
|
||||
isDone: false,
|
||||
name: "Ester Howard",
|
||||
isfav: false,
|
||||
time: "12.00 pm",
|
||||
isTrash: false,
|
||||
category: [
|
||||
{
|
||||
value: "update",
|
||||
label: "update",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "ed6df190-9141-4048-81d0-1ac067f37e46",
|
||||
image: [
|
||||
{
|
||||
image: "/images/avatar/avatar-5.png",
|
||||
label: "Rakibul Islam",
|
||||
value: "rakibul",
|
||||
},
|
||||
],
|
||||
title: "illo expedita consequatur quia in",
|
||||
isDone: false,
|
||||
name: "Ester Howard",
|
||||
isfav: false,
|
||||
time: "12.00 pm",
|
||||
isTrash: false,
|
||||
category: [
|
||||
{
|
||||
value: "update",
|
||||
label: "update",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "a296f702-a7ed-4127-a42a-121669b02b1a",
|
||||
image: [
|
||||
{
|
||||
image: "/images/avatar/avatar-5.png",
|
||||
label: "Rakibul Islam",
|
||||
value: "rakibul",
|
||||
},
|
||||
],
|
||||
title: "illo expedita consequatur quia in",
|
||||
isDone: false,
|
||||
name: "Ester Howard",
|
||||
isfav: false,
|
||||
time: "12.00 pm",
|
||||
isTrash: false,
|
||||
category: [
|
||||
{
|
||||
value: "update",
|
||||
label: "update",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "6d4be8c8-a382-4d4a-8df5-a2234e69b57f",
|
||||
image: [
|
||||
{
|
||||
image: "/images/avatar/avatar-5.png",
|
||||
label: "Rakibul Islam",
|
||||
value: "rakibul",
|
||||
},
|
||||
],
|
||||
title: "illo expedita consequatur quia in",
|
||||
isDone: false,
|
||||
name: "Ester Howard",
|
||||
isfav: false,
|
||||
time: "12.00 pm",
|
||||
isTrash: false,
|
||||
category: [
|
||||
{
|
||||
value: "update",
|
||||
label: "update",
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
export type Mail = (typeof mails)[number];
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
import { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Media Hub | POLRI",
|
||||
description: "Media Hub merupakan situs resmi milik Divisi Humas Polri di mana di dalamnya berisi konten-konten yang dapat diakses secara gratis oleh Internal Polri, Jurnalis, Masyarakat Umum, dan KSP.",
|
||||
};
|
||||
const Layout = ({ children }: { children: React.ReactNode }) => {
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
|
|
@ -1,92 +0,0 @@
|
|||
import { Metadata } from "next";
|
||||
import MailWrapper from "./mail-wrapper";
|
||||
import MailSidebarWrapper from "./sidebar-wrapper";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import Compose from "./compose";
|
||||
import Nav from "@/components/nav";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
export const metadata: Metadata = {
|
||||
title: "Mail",
|
||||
description: "Mail Application",
|
||||
};
|
||||
const Layout = ({ children }: { children: React.ReactNode }) => {
|
||||
|
||||
return <MailWrapper>
|
||||
<div className="flex gap-5 h-full ">
|
||||
<MailSidebarWrapper>
|
||||
<Card className=" h-full ">
|
||||
<CardContent className=" h-full space-y-2 pt-6 px-0">
|
||||
<div className="px-5">
|
||||
<Compose />
|
||||
</div>
|
||||
<ScrollArea className="h-[calc(100%-30px)]">
|
||||
<Nav
|
||||
links={[
|
||||
{
|
||||
title: "Inbox",
|
||||
icon: "uil:image-v",
|
||||
active: true,
|
||||
},
|
||||
{
|
||||
title: "Starred",
|
||||
icon: "heroicons:star",
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
title: "Sent",
|
||||
icon: "heroicons:paper-airplane",
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
title: "Drafts",
|
||||
icon: "heroicons:pencil-square",
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
title: "Spam",
|
||||
icon: "heroicons:information-circle",
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
title: "Trash",
|
||||
icon: "heroicons:trash",
|
||||
active: false,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<div className="space-y-2 mt-3">
|
||||
<p className="text-sm font-medium text-default-900 px-5">TAGS</p>
|
||||
<Nav
|
||||
dotStyle
|
||||
links={[
|
||||
{
|
||||
title: "personal",
|
||||
active: true,
|
||||
},
|
||||
{
|
||||
title: "social",
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
title: "promotions",
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
title: "business",
|
||||
active: false,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</MailSidebarWrapper>
|
||||
<div className="flex-1 w-full h-full">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</MailWrapper>;
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
import { Input } from "@/components/ui/input";
|
||||
import { MoreVertical, Search } from "lucide-react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Icon } from "@/components/ui/icon";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import ToggleMailSidebar from "./toggle-mail-sidebar";
|
||||
|
||||
const MailHeader = () => {
|
||||
const actions = [
|
||||
{
|
||||
name: "Reset Sort",
|
||||
icon: "heroicons-outline:sort-ascending",
|
||||
},
|
||||
{
|
||||
name: "Sort A-Z ",
|
||||
icon: "heroicons-outline:sort-ascending",
|
||||
},
|
||||
{
|
||||
name: "Sort Z-A ",
|
||||
icon: "heroicons-outline:sort-descending",
|
||||
},
|
||||
];
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<div className="flex-1 flex items-center gap-1">
|
||||
<ToggleMailSidebar />
|
||||
<Checkbox className="w-4 h-4 text-default-400 dark:bg-default-300" />
|
||||
<Input
|
||||
placeholder="Search Mail"
|
||||
className="max-w-[180px] border-none font-medium dark:bg-transparent"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-none">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button size="icon" className="w-8 h-8 rounded-full bg-default-100">
|
||||
<MoreVertical className="w-5 h-5 text-default hover:text-default-foreground" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
className="p-0 rounded-md overflow-hidden"
|
||||
>
|
||||
{actions.map((action, index) => (
|
||||
<DropdownMenuItem
|
||||
key={`action-${index}`}
|
||||
className="flex items-center gap-1.5 p-2 border-b text-default-600 group focus:bg-default focus:text-primary-foreground rounded-none group"
|
||||
>
|
||||
<Icon
|
||||
icon={action.icon}
|
||||
className="group-hover:text-primary-foreground w-4 h-4"
|
||||
/>
|
||||
<span className="text-default-700 group-hover:text-primary-foreground">
|
||||
{action.name}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MailHeader;
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
'use client'
|
||||
import { useMediaQuery } from '@/hooks/use-media-query';
|
||||
import { useMailConfig } from '@/hooks/use-mail'
|
||||
import React from 'react'
|
||||
|
||||
const MailWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
const [mailConfig, setMailConfig] = useMailConfig();
|
||||
const { isOpen } = mailConfig
|
||||
const isTablet = useMediaQuery("(min-width: 1024px)");
|
||||
return (
|
||||
<div className=' relative app-height'>
|
||||
{!isTablet && isOpen && (
|
||||
<div
|
||||
onClick={() => setMailConfig({ ...mailConfig, isOpen: false })}
|
||||
className="overlay bg-default-900 dark:bg-default-900 dark:bg-opacity-60 bg-opacity-60 backdrop-filter
|
||||
backdrop-blur-sm absolute w-full flex-1 inset-0 z-20 rounded-md"
|
||||
|
||||
></div>
|
||||
)}
|
||||
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MailWrapper
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
"use client";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Icon } from "@/components/ui/icon";
|
||||
import { Avatar, AvatarImage } from "@/components/ui/avatar";
|
||||
import { AvatarFallback } from "@radix-ui/react-avatar";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import {ReactNode,useState} from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Trash2 } from "lucide-react";
|
||||
import DeleteConfirmationDialog from "@/components/delete-confirmation-dialog";
|
||||
import { Link } from "@/components/navigation"
|
||||
const MailItem = ({ mail }: { mail: any }) => {
|
||||
const { image, title, isfav, time, name, id } = mail;
|
||||
const [open, setOpen] = useState<boolean>(false)
|
||||
return (
|
||||
<>
|
||||
<DeleteConfirmationDialog
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
/>
|
||||
<Link href={`/app/email/${id}`} className=" group block border-b border-default-200 dark:border-default-300 last:border-none" >
|
||||
<div className="flex items-center gap-4 group-hover:bg-default-50 last:border-none px-6 py-4 translate-y-0">
|
||||
<div>
|
||||
<Checkbox className="mt-0.5 dark:bg-default-300" />
|
||||
</div>
|
||||
<div className="ms-1">
|
||||
{isfav ? (
|
||||
<Icon
|
||||
icon="heroicons:star-20-solid"
|
||||
className="text-xl cursor-pointer text-[#FFCE30]"
|
||||
/>
|
||||
) : (
|
||||
<Icon
|
||||
icon="heroicons:star"
|
||||
className="text-xl cursor-pointer text-default-400"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="-space-x-1.5 rtl:space-x-reverse">
|
||||
{image.map(
|
||||
(
|
||||
item: {
|
||||
label: ReactNode;
|
||||
image: string;
|
||||
},
|
||||
index: any
|
||||
) => (
|
||||
<TooltipProvider key={`avatar-${index}`}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Avatar className="h-7 w-7 border-none shadow-none bg-transparent">
|
||||
<AvatarImage src={item.image} />
|
||||
<AvatarFallback>SA</AvatarFallback>
|
||||
</Avatar>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{item.label}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 overflow-hidden text-sm text-default-600 truncate">
|
||||
<span className="me-3">{name}</span>
|
||||
<span className="text-sm text-default-600 truncate max-w-56">{title}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<p className="text-default-900 text-sm">{time}</p>
|
||||
<div className="group-hover:bg-default-100 hidden group-hover:flex absolute group-hover:transition-all duration-300 right-0 top-0 h-full items-center ">
|
||||
<Button
|
||||
className="bg-transparent ring-transparent hover:bg-transparent hover:ring-0 hover:ring-offset-0 hover:ring-transparent w-28 border-transparent"
|
||||
size="icon"
|
||||
onClick={(event) => {
|
||||
setOpen(true);
|
||||
event.preventDefault();
|
||||
}}
|
||||
>
|
||||
<Trash2 className="text-default-900 w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MailItem;
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import MailItem from "./mail";
|
||||
import MailHeader from "./mail-header";
|
||||
import { getMail } from "./utils";
|
||||
const EmailApp = async () => {
|
||||
const mails = await getMail()
|
||||
return (
|
||||
<Card className="h-full rounded-md">
|
||||
<CardHeader className="border-b border-default-200 sticky top-0 z-10 bg-card py-4">
|
||||
<MailHeader />
|
||||
</CardHeader>
|
||||
<CardContent className="p-0 h-full">
|
||||
<div className="h-[calc(100%-80px)] overflow-y-auto no-scrollbar">
|
||||
{mails.map((mail, index) => (
|
||||
<MailItem key={`mail-${index}`} mail={mail} />
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmailApp;
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
'use client'
|
||||
import { useMediaQuery } from '@/hooks/use-media-query'
|
||||
import { useMailConfig } from '@/hooks/use-mail'
|
||||
import { cn } from '@/lib/utils'
|
||||
import React from 'react'
|
||||
const MailSidebarWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
const [mailConfig] = useMailConfig()
|
||||
const { isOpen } = mailConfig
|
||||
const isTablet = useMediaQuery("(min-width: 1024px)");
|
||||
if (!isTablet) {
|
||||
return (
|
||||
<div className={cn('absolute h-full start-0 w-[240px] z-50 ', {
|
||||
'-start-full': !isOpen
|
||||
})}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className='flex-none md:w-[270px] w-[200px] '>{children}</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MailSidebarWrapper
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
"use client";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useMediaQuery } from "@/hooks/use-media-query";
|
||||
import { useMailConfig } from "@/hooks/use-mail";
|
||||
import { Menu } from "lucide-react";
|
||||
import React from "react";
|
||||
|
||||
const ToggleMailSidebar = () => {
|
||||
const isTablet = useMediaQuery("(min-width: 1024px)");
|
||||
const [mailConfig, setMailConfig] = useMailConfig();
|
||||
if (isTablet) return null;
|
||||
|
||||
return (
|
||||
<Button
|
||||
size="icon"
|
||||
color="secondary"
|
||||
onClick={() =>
|
||||
setMailConfig({ ...mailConfig, isOpen: !mailConfig.isOpen })
|
||||
}
|
||||
>
|
||||
<Menu className=" h-5 w-5" />
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToggleMailSidebar;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue