From 8a11c176aa03c16b42b742165cfe339f46427da1 Mon Sep 17 00:00:00 2001 From: GitLab Deploy Bot Date: Sun, 9 Nov 2025 14:21:58 +0700 Subject: [PATCH 01/36] build docker via gitlab --- .gitlab-ci.yml | 164 +++++++++++++++++++++++++------------------- Dockerfile | 24 +++++++ docker-compose.yaml | 39 +++++++++++ 3 files changed, 158 insertions(+), 69 deletions(-) create mode 100644 Dockerfile create mode 100644 docker-compose.yaml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index efda72f0..d9db48d3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,76 +1,102 @@ -stages: [notify] +stages: + - build + - cleanup + - deploy -# --- Notify when MR is opened/updated --- -notify_discord_mr: - stage: notify - image: alpine:3.20 - rules: - - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' +variables: + DOCKER_DRIVER: overlay2 + IMAGE_NAME: "${CI_REGISTRY_IMAGE}/web-lti" + DEPLOY_ENV: development + KEEP_IMAGES: 3 + BUILD_MODE: static + +# ===================================================== +# ๐Ÿ”‘ AUTH TO REGISTRY +# ===================================================== +before_script: + - echo "๐Ÿ” Logging in to GitLab Container Registry..." + - echo "$GITLAB_TOKEN" | docker login -u "$GITLAB_USER" --password-stdin "$CI_REGISTRY" + +# ===================================================== +# ๐Ÿงฑ BUILD IMAGE +# ===================================================== +build-image: + stage: build + image: docker:27.0.2 + services: + - docker:dind variables: - WEBHOOK_URL: $DISCORD_WEBHOOK_URL - before_script: - - apk add --no-cache curl jq - script: | - MR_URL="${CI_PROJECT_URL}/-/merge_requests/${CI_MERGE_REQUEST_IID}" + DOCKER_TLS_CERTDIR: "" + script: + - echo "๐Ÿš€ Building Docker image for ${DEPLOY_ENV} branch..." - jq -n \ - --arg repo "$CI_PROJECT_PATH" \ - --arg mr "#${CI_MERGE_REQUEST_IID}" \ - --arg url "$MR_URL" \ - --arg requestor "${GITLAB_USER_LOGIN:-$GITLAB_USER_NAME}" \ - --arg source "$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME" \ - --arg target "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" \ - --arg title "$CI_MERGE_REQUEST_TITLE" \ - '{ - username: "CI Bot - FE", - embeds: [{ - title: "๐Ÿ“ฃ [LTI WEB CLIENT] Merge Request Opened/Updated", - description: ($mr + " in " + $repo), - url: $url, - color: 3447003, - fields: [ - {name: "Author", value: $requestor, inline: true}, - {name: "Source โ†’ Target", value: ($source + " โ†’ " + $target), inline: true}, - {name: "Title", value: $title} - ] - }] - }' \ - | curl -sS -H "Content-Type: application/json" -d @- "$WEBHOOK_URL" + # Tag format: web-lti:development_ + - export TAG="${DEPLOY_ENV}_${CI_COMMIT_SHORT_SHA}" -# --- Notify when MR is merged --- -notify_discord_merge: - stage: notify + - echo "๐Ÿงฑ Tagging image as: $IMAGE_NAME:$TAG" + - docker build \ + --build-arg NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \ + --build-arg NEXT_PUBLIC_LTI_API_START_URL=$NEXT_PUBLIC_LTI_API_START_URL \ + --build-arg NEXT_PUBLIC_LTI_CLIENT_ID=$NEXT_PUBLIC_LTI_CLIENT_ID \ + --build-arg BUILD_MODE=$BUILD_MODE \ + -t "$IMAGE_NAME:$TAG" \ + -t "$IMAGE_NAME:$DEPLOY_ENV" . + + - echo "๐Ÿ“ฆ Pushing images to registry..." + - docker push "$IMAGE_NAME:$TAG" + - docker push "$IMAGE_NAME:$DEPLOY_ENV" + only: + - development + +# ===================================================== +# ๐Ÿงน CLEANUP OLD IMAGES (KEEP 3) +# ===================================================== +cleanup-registry: + stage: cleanup image: alpine:3.20 - rules: - # Only run for merge request pipelines that are in merged state - - if: '$CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_STATE == "merged"' - variables: - WEBHOOK_URL: $DISCORD_WEBHOOK_URL - before_script: + script: - apk add --no-cache curl jq - script: | - MR_URL="${CI_PROJECT_URL}/-/merge_requests/${CI_MERGE_REQUEST_IID}" + - echo "๐Ÿงน Cleaning up old images (keeping ${KEEP_IMAGES})..." - jq -n \ - --arg repo "$CI_PROJECT_PATH" \ - --arg mr "#${CI_MERGE_REQUEST_IID}" \ - --arg url "$MR_URL" \ - --arg requestor "${CI_MERGE_REQUEST_AUTHOR}" \ - --arg source "$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME" \ - --arg target "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" \ - --arg title "$CI_MERGE_REQUEST_TITLE" \ - '{ - username: "CI Bot - FE", - embeds: [{ - title: "โœ… [LTI WEB CLIENT] Merge Request Merged", - description: ($mr + " has been merged into " + $repo), - url: $url, - color: 3066993, - fields: [ - {name: "Author", value: $requestor, inline: true}, - {name: "Source โ†’ Target", value: ($source + " โ†’ " + $target), inline: true}, - {name: "Title", value: $title} - ] - }] - }' \ - | curl -sS -H "Content-Type: application/json" -d @- "$WEBHOOK_URL" + - TOKEN=$(curl --silent --request POST --header "Content-Type: application/json" \ + --data "{\"login\": \"$GITLAB_USER\", \"password\": \"$GITLAB_TOKEN\"}" \ + "${CI_REGISTRY}/jwt/auth" | jq -r '.token') + + - ALL_TAGS=$(curl --silent --header "Authorization: Bearer $TOKEN" \ + "${CI_REGISTRY}/v2/${CI_PROJECT_PATH}/web-lti/tags/list" \ + | jq -r '.tags | sort | reverse | .['$KEEP_IMAGES':] | @sh' | tr -d "'") + + - | + for tag in $ALL_TAGS; do + echo "๐Ÿ—‘๏ธ Deleting old image tag: $tag" + DIGEST=$(curl --silent -H "Authorization: Bearer $TOKEN" \ + "${CI_REGISTRY}/v2/${CI_PROJECT_PATH}/web-lti/manifests/$tag" | jq -r '.config.digest') + curl --silent -X DELETE -H "Authorization: Bearer $TOKEN" \ + "${CI_REGISTRY}/v2/${CI_PROJECT_PATH}/web-lti/manifests/${DIGEST}" + done + only: + - development + when: always + +# ===================================================== +# ๐Ÿš€ DEPLOY TO SERVER (VIA SSH) +# ===================================================== +deploy: + stage: deploy + image: alpine:3.20 + before_script: + - apk add --no-cache openssh + - mkdir -p ~/.ssh + - echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa + - chmod 600 ~/.ssh/id_rsa + - ssh-keyscan -H "$SERVER_IP" >> ~/.ssh/known_hosts + script: + - echo "๐Ÿš€ Deploying $IMAGE_NAME:$DEPLOY_ENV to $SERVER_USER@$SERVER_IP" + - ssh $SERVER_USER@$SERVER_IP " + docker login -u '$GITLAB_USER' -p '$GITLAB_TOKEN' $CI_REGISTRY && + docker pull $IMAGE_NAME:$DEPLOY_ENV && + docker compose -f /home/devops/docker/deployment/development/compose/docker-compose.web-lti.yaml up -d dev-web-lti && + docker image prune -f + " + only: + - development \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..26f41276 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +FROM node:20-alpine + +RUN apk add --no-cache git bash build-base curl + +WORKDIR /app + +COPY package*.json ./ +RUN npm ci + +COPY . . + +# Buat config agar Next tahu output: export +RUN echo "const config = { output: 'export', images: { unoptimized: true } }; export default config;" > next.config.mjs + +# Build project (Next.js 15 otomatis static export) +RUN NEXT_DISABLE_TURBOPACK=1 npx next build + +# Pastikan folder static tersedia untuk URL _next/static +RUN mkdir -p .next/server/app/_next && \ + cp -r .next/static .next/server/app/_next/static && \ + cp -r public/assets .next/server/app/ + +EXPOSE 3000 +CMD ["npx", "serve", ".next/server/app", "-l", "3000"] \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 00000000..8d658170 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,39 @@ +version: "3.9" + +services: + dev-web-lti: + container_name: dev-web-lti + build: + context: . + dockerfile: Dockerfile + ports: + - "3002:3000" + env_file: + - .env + environment: + NODE_ENV: production + APP_ENV: production + networks: + - dev-lti-network + restart: always + deploy: + resources: + limits: + cpus: "3.0" + memory: 3G + reservations: + cpus: "1.0" + memory: 512M + extra_hosts: + - "host.docker.internal:host-gateway" + # Optional: aktifkan healthcheck jika punya endpoint + # healthcheck: + # test: ["CMD-SHELL", "curl -fsS http://localhost:3000/api/healthz || exit 1"] + # interval: 10s + # timeout: 3s + # retries: 10 + # start_period: 15s + +networks: + dev-lti-network: + external: true \ No newline at end of file From 52e8fb4a3bfd1e82be1d24081ecb83e1e31f6b19 Mon Sep 17 00:00:00 2001 From: GitLab Deploy Bot Date: Sun, 9 Nov 2025 14:44:58 +0700 Subject: [PATCH 02/36] build with tag docker --- .gitlab-ci.yml | 81 +++++++++++++++++++++++--------------------------- Dockerfile | 48 +++++++++++++++++++++++++----- 2 files changed, 77 insertions(+), 52 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d9db48d3..0bbd68bb 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -10,9 +10,6 @@ variables: KEEP_IMAGES: 3 BUILD_MODE: static -# ===================================================== -# ๐Ÿ”‘ AUTH TO REGISTRY -# ===================================================== before_script: - echo "๐Ÿ” Logging in to GitLab Container Registry..." - echo "$GITLAB_TOKEN" | docker login -u "$GITLAB_USER" --password-stdin "$CI_REGISTRY" @@ -27,24 +24,21 @@ build-image: - docker:dind variables: DOCKER_TLS_CERTDIR: "" - script: - - echo "๐Ÿš€ Building Docker image for ${DEPLOY_ENV} branch..." + script: | + echo "๐Ÿš€ Building Docker image for ${DEPLOY_ENV} branch..." + export TAG="${DEPLOY_ENV}_${CI_COMMIT_SHORT_SHA}" + echo "๐Ÿงฑ Tagging image as: $IMAGE_NAME:$TAG" - # Tag format: web-lti:development_ - - export TAG="${DEPLOY_ENV}_${CI_COMMIT_SHORT_SHA}" + docker build \ + --build-arg NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \ + --build-arg NEXT_PUBLIC_SSO_LOGIN_URL=$NEXT_PUBLIC_SSO_LOGIN_URL \ + --build-arg BUILD_MODE=$BUILD_MODE \ + -t "$IMAGE_NAME:$TAG" \ + -t "$IMAGE_NAME:$DEPLOY_ENV" . - - echo "๐Ÿงฑ Tagging image as: $IMAGE_NAME:$TAG" - - docker build \ - --build-arg NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \ - --build-arg NEXT_PUBLIC_LTI_API_START_URL=$NEXT_PUBLIC_LTI_API_START_URL \ - --build-arg NEXT_PUBLIC_LTI_CLIENT_ID=$NEXT_PUBLIC_LTI_CLIENT_ID \ - --build-arg BUILD_MODE=$BUILD_MODE \ - -t "$IMAGE_NAME:$TAG" \ - -t "$IMAGE_NAME:$DEPLOY_ENV" . - - - echo "๐Ÿ“ฆ Pushing images to registry..." - - docker push "$IMAGE_NAME:$TAG" - - docker push "$IMAGE_NAME:$DEPLOY_ENV" + echo "๐Ÿ“ฆ Pushing images to registry..." + docker push "$IMAGE_NAME:$TAG" + docker push "$IMAGE_NAME:$DEPLOY_ENV" only: - development @@ -54,26 +48,25 @@ build-image: cleanup-registry: stage: cleanup image: alpine:3.20 - script: - - apk add --no-cache curl jq - - echo "๐Ÿงน Cleaning up old images (keeping ${KEEP_IMAGES})..." + script: | + apk add --no-cache curl jq + echo "๐Ÿงน Cleaning up old images (keeping ${KEEP_IMAGES})..." - - TOKEN=$(curl --silent --request POST --header "Content-Type: application/json" \ - --data "{\"login\": \"$GITLAB_USER\", \"password\": \"$GITLAB_TOKEN\"}" \ - "${CI_REGISTRY}/jwt/auth" | jq -r '.token') + TOKEN=$(curl --silent --request POST --header "Content-Type: application/json" \ + --data "{\"login\": \"$GITLAB_USER\", \"password\": \"$GITLAB_TOKEN\"}" \ + "${CI_REGISTRY}/jwt/auth" | jq -r '.token') - - ALL_TAGS=$(curl --silent --header "Authorization: Bearer $TOKEN" \ - "${CI_REGISTRY}/v2/${CI_PROJECT_PATH}/web-lti/tags/list" \ - | jq -r '.tags | sort | reverse | .['$KEEP_IMAGES':] | @sh' | tr -d "'") + ALL_TAGS=$(curl --silent --header "Authorization: Bearer $TOKEN" \ + "${CI_REGISTRY}/v2/${CI_PROJECT_PATH}/web-lti/tags/list" \ + | jq -r ".tags | sort | reverse | .[${KEEP_IMAGES}:]" | jq -r '.[]') - - | - for tag in $ALL_TAGS; do - echo "๐Ÿ—‘๏ธ Deleting old image tag: $tag" - DIGEST=$(curl --silent -H "Authorization: Bearer $TOKEN" \ - "${CI_REGISTRY}/v2/${CI_PROJECT_PATH}/web-lti/manifests/$tag" | jq -r '.config.digest') - curl --silent -X DELETE -H "Authorization: Bearer $TOKEN" \ - "${CI_REGISTRY}/v2/${CI_PROJECT_PATH}/web-lti/manifests/${DIGEST}" - done + for tag in $ALL_TAGS; do + echo "๐Ÿ—‘๏ธ Deleting old image tag: $tag" + DIGEST=$(curl --silent -H "Authorization: Bearer $TOKEN" \ + "${CI_REGISTRY}/v2/${CI_PROJECT_PATH}/web-lti/manifests/$tag" | jq -r '.config.digest') + curl --silent -X DELETE -H "Authorization: Bearer $TOKEN" \ + "${CI_REGISTRY}/v2/${CI_PROJECT_PATH}/web-lti/manifests/${DIGEST}" || true + done only: - development when: always @@ -90,13 +83,13 @@ deploy: - echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa - chmod 600 ~/.ssh/id_rsa - ssh-keyscan -H "$SERVER_IP" >> ~/.ssh/known_hosts - script: - - echo "๐Ÿš€ Deploying $IMAGE_NAME:$DEPLOY_ENV to $SERVER_USER@$SERVER_IP" - - ssh $SERVER_USER@$SERVER_IP " - docker login -u '$GITLAB_USER' -p '$GITLAB_TOKEN' $CI_REGISTRY && - docker pull $IMAGE_NAME:$DEPLOY_ENV && - docker compose -f /home/devops/docker/deployment/development/compose/docker-compose.web-lti.yaml up -d dev-web-lti && - docker image prune -f - " + script: | + echo "๐Ÿš€ Deploying $IMAGE_NAME:$DEPLOY_ENV to $SERVER_USER@$SERVER_IP" + ssh $SERVER_USER@$SERVER_IP " + docker login -u '$GITLAB_USER' -p '$GITLAB_TOKEN' $CI_REGISTRY && + docker pull $IMAGE_NAME:$DEPLOY_ENV && + docker compose -f /home/devops/docker/deployment/development/compose/docker-compose.web-lti.yaml up -d dev-web-lti && + docker image prune -f + " only: - development \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 26f41276..a7273724 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,24 +1,56 @@ -FROM node:20-alpine +# ============================================================ +# ๐Ÿ—๏ธ Stage 1 โ€” Builder +# ============================================================ +FROM node:20-alpine AS builder +# Install hanya yang diperlukan untuk build RUN apk add --no-cache git bash build-base curl WORKDIR /app +# Copy dependency list terlebih dahulu agar cache efektif COPY package*.json ./ -RUN npm ci +# Gunakan npm ci (lebih cepat, konsisten) +RUN npm ci --omit=dev + +# Copy source code terakhir COPY . . -# Buat config agar Next tahu output: export +# Buat config agar Next tahu mode static export RUN echo "const config = { output: 'export', images: { unoptimized: true } }; export default config;" > next.config.mjs -# Build project (Next.js 15 otomatis static export) -RUN NEXT_DISABLE_TURBOPACK=1 npx next build +# Build Next.js tanpa Turbopack, lalu hapus cache npm +ENV NEXT_DISABLE_TURBOPACK=1 +RUN npx next build && npm cache clean --force -# Pastikan folder static tersedia untuk URL _next/static +# Tambahkan cache folder _next agar bisa dilayani oleh server RUN mkdir -p .next/server/app/_next && \ cp -r .next/static .next/server/app/_next/static && \ - cp -r public/assets .next/server/app/ + cp -r public/assets .next/server/app/ || true + +# ============================================================ +# ๐Ÿงฑ Stage 2 โ€” Runtime (super ringan) +# ============================================================ +FROM node:20-alpine AS runtime + +# Install hanya 1 dependency ringan untuk serving static file +RUN npm install -g serve && apk add --no-cache tini + +WORKDIR /app + +# Copy hasil build dari stage sebelumnya +COPY --from=builder /app/.next/server/app ./server +COPY --from=builder /app/.next/server/app/_next ./server/_next +COPY --from=builder /app/public ./public + +# Set environment minimal +ENV NODE_ENV=production +ENV PORT=3000 EXPOSE 3000 -CMD ["npx", "serve", ".next/server/app", "-l", "3000"] \ No newline at end of file + +# Jalankan lewat tini untuk handle signal & memory leak +ENTRYPOINT ["/sbin/tini", "--"] + +CMD ["serve", "-s", "server", "-l", "3000"] \ No newline at end of file From 29ff1bb50a6d5200e5090dd62f9a077c69b91700 Mon Sep 17 00:00:00 2001 From: GitLab Deploy Bot Date: Sun, 9 Nov 2025 14:53:49 +0700 Subject: [PATCH 03/36] edit .gitlab-ci --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 0bbd68bb..c2392f1a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -12,7 +12,7 @@ variables: before_script: - echo "๐Ÿ” Logging in to GitLab Container Registry..." - - echo "$GITLAB_TOKEN" | docker login -u "$GITLAB_USER" --password-stdin "$CI_REGISTRY" + - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY" # ===================================================== # ๐Ÿงฑ BUILD IMAGE From 66b6579f275e5ca55ec82352db10f910726ca12a Mon Sep 17 00:00:00 2001 From: GitLab Deploy Bot Date: Sun, 9 Nov 2025 15:01:10 +0700 Subject: [PATCH 04/36] edit .gitlab-ci --- .gitlab-ci.yml | 113 ++++++++++++++----------------------------------- Dockerfile | 32 ++++++-------- 2 files changed, 45 insertions(+), 100 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c2392f1a..9b13bb02 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,95 +1,44 @@ stages: - build - - cleanup - - deploy variables: + # ๐Ÿ”ง Aktifkan Docker BuildKit (build lebih cepat & caching layer) + DOCKER_BUILDKIT: "1" + COMPOSE_DOCKER_CLI_BUILD: "1" DOCKER_DRIVER: overlay2 - IMAGE_NAME: "${CI_REGISTRY_IMAGE}/web-lti" - DEPLOY_ENV: development - KEEP_IMAGES: 3 - BUILD_MODE: static -before_script: - - echo "๐Ÿ” Logging in to GitLab Container Registry..." - - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY" + # ๐Ÿง  Nama image (pakai commit short SHA) + IMAGE_NAME: "$CI_REGISTRY_IMAGE/web-lti:development_${CI_COMMIT_SHORT_SHA}" + + # Cache npm (disimpan antar pipeline) + NPM_CACHE_DIR: "$CI_PROJECT_DIR/.npm" + +cache: + key: npm-cache + paths: + - .npm/ -# ===================================================== -# ๐Ÿงฑ BUILD IMAGE -# ===================================================== build-image: stage: build - image: docker:27.0.2 + image: docker:27.0.3 services: - docker:dind - variables: - DOCKER_TLS_CERTDIR: "" - script: | - echo "๐Ÿš€ Building Docker image for ${DEPLOY_ENV} branch..." - export TAG="${DEPLOY_ENV}_${CI_COMMIT_SHORT_SHA}" - echo "๐Ÿงฑ Tagging image as: $IMAGE_NAME:$TAG" - docker build \ - --build-arg NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \ - --build-arg NEXT_PUBLIC_SSO_LOGIN_URL=$NEXT_PUBLIC_SSO_LOGIN_URL \ - --build-arg BUILD_MODE=$BUILD_MODE \ - -t "$IMAGE_NAME:$TAG" \ - -t "$IMAGE_NAME:$DEPLOY_ENV" . - - echo "๐Ÿ“ฆ Pushing images to registry..." - docker push "$IMAGE_NAME:$TAG" - docker push "$IMAGE_NAME:$DEPLOY_ENV" - only: - - development - -# ===================================================== -# ๐Ÿงน CLEANUP OLD IMAGES (KEEP 3) -# ===================================================== -cleanup-registry: - stage: cleanup - image: alpine:3.20 - script: | - apk add --no-cache curl jq - echo "๐Ÿงน Cleaning up old images (keeping ${KEEP_IMAGES})..." - - TOKEN=$(curl --silent --request POST --header "Content-Type: application/json" \ - --data "{\"login\": \"$GITLAB_USER\", \"password\": \"$GITLAB_TOKEN\"}" \ - "${CI_REGISTRY}/jwt/auth" | jq -r '.token') - - ALL_TAGS=$(curl --silent --header "Authorization: Bearer $TOKEN" \ - "${CI_REGISTRY}/v2/${CI_PROJECT_PATH}/web-lti/tags/list" \ - | jq -r ".tags | sort | reverse | .[${KEEP_IMAGES}:]" | jq -r '.[]') - - for tag in $ALL_TAGS; do - echo "๐Ÿ—‘๏ธ Deleting old image tag: $tag" - DIGEST=$(curl --silent -H "Authorization: Bearer $TOKEN" \ - "${CI_REGISTRY}/v2/${CI_PROJECT_PATH}/web-lti/manifests/$tag" | jq -r '.config.digest') - curl --silent -X DELETE -H "Authorization: Bearer $TOKEN" \ - "${CI_REGISTRY}/v2/${CI_PROJECT_PATH}/web-lti/manifests/${DIGEST}" || true - done - only: - - development - when: always - -# ===================================================== -# ๐Ÿš€ DEPLOY TO SERVER (VIA SSH) -# ===================================================== -deploy: - stage: deploy - image: alpine:3.20 before_script: - - apk add --no-cache openssh - - mkdir -p ~/.ssh - - echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa - - chmod 600 ~/.ssh/id_rsa - - ssh-keyscan -H "$SERVER_IP" >> ~/.ssh/known_hosts - script: | - echo "๐Ÿš€ Deploying $IMAGE_NAME:$DEPLOY_ENV to $SERVER_USER@$SERVER_IP" - ssh $SERVER_USER@$SERVER_IP " - docker login -u '$GITLAB_USER' -p '$GITLAB_TOKEN' $CI_REGISTRY && - docker pull $IMAGE_NAME:$DEPLOY_ENV && - docker compose -f /home/devops/docker/deployment/development/compose/docker-compose.web-lti.yaml up -d dev-web-lti && - docker image prune -f - " - only: - - development \ No newline at end of file + - echo "๐Ÿ” Logging in to GitLab Container Registry..." + - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY" + + script: + - echo "๐Ÿšง Building optimized Docker image..." + - docker build --build-arg BUILDKIT_INLINE_CACHE=1 --cache-from $CI_REGISTRY_IMAGE/web-lti:latest -t "$IMAGE_NAME" . + - docker push "$IMAGE_NAME" + + # ๐Ÿงน Keep only last 3 images (hapus yang lama) + - echo "๐Ÿงน Cleaning old images..." + - docker image prune -af --filter "until=72h" + + after_script: + - echo "โœ… Build complete: $IMAGE_NAME" + + rules: + - if: '$CI_COMMIT_BRANCH == "development"' \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index a7273724..f625ce73 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,56 +1,52 @@ # ============================================================ -# ๐Ÿ—๏ธ Stage 1 โ€” Builder +# ๐Ÿ—๏ธ Stage 1 โ€” Builder # ============================================================ FROM node:20-alpine AS builder -# Install hanya yang diperlukan untuk build +# Install dependensi dasar RUN apk add --no-cache git bash build-base curl WORKDIR /app -# Copy dependency list terlebih dahulu agar cache efektif +# Copy dependencies terlebih dahulu agar cache efisien COPY package*.json ./ -# Gunakan npm ci (lebih cepat, konsisten) -RUN npm ci --omit=dev +# Pastikan npm up to date agar mendukung flag terbaru +RUN npm install -g npm@11 && npm --version -# Copy source code terakhir +# Install dependency tanpa devDependencies (aman di semua npm versi) +RUN npm ci --only=production + +# Copy seluruh source COPY . . -# Buat config agar Next tahu mode static export +# Buat konfigurasi output Next.js RUN echo "const config = { output: 'export', images: { unoptimized: true } }; export default config;" > next.config.mjs -# Build Next.js tanpa Turbopack, lalu hapus cache npm +# Build project (disable Turbopack agar tidak makan RAM) ENV NEXT_DISABLE_TURBOPACK=1 RUN npx next build && npm cache clean --force -# Tambahkan cache folder _next agar bisa dilayani oleh server +# Siapkan folder static untuk serve RUN mkdir -p .next/server/app/_next && \ cp -r .next/static .next/server/app/_next/static && \ cp -r public/assets .next/server/app/ || true # ============================================================ -# ๐Ÿงฑ Stage 2 โ€” Runtime (super ringan) +# ๐Ÿงฑ Stage 2 โ€” Runtime # ============================================================ FROM node:20-alpine AS runtime -# Install hanya 1 dependency ringan untuk serving static file -RUN npm install -g serve && apk add --no-cache tini +RUN apk add --no-cache tini && npm install -g serve WORKDIR /app -# Copy hasil build dari stage sebelumnya COPY --from=builder /app/.next/server/app ./server -COPY --from=builder /app/.next/server/app/_next ./server/_next COPY --from=builder /app/public ./public -# Set environment minimal ENV NODE_ENV=production ENV PORT=3000 EXPOSE 3000 - -# Jalankan lewat tini untuk handle signal & memory leak ENTRYPOINT ["/sbin/tini", "--"] - CMD ["serve", "-s", "server", "-l", "3000"] \ No newline at end of file From a9620246c0277c76674fdb0adbbea58a04001dc0 Mon Sep 17 00:00:00 2001 From: kris Date: Sun, 9 Nov 2025 08:05:11 +0000 Subject: [PATCH 05/36] Update .gitlab-ci.yml file --- .gitlab-ci.yml | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 9b13bb02..b7ad3839 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -2,15 +2,10 @@ stages: - build variables: - # ๐Ÿ”ง Aktifkan Docker BuildKit (build lebih cepat & caching layer) DOCKER_BUILDKIT: "1" COMPOSE_DOCKER_CLI_BUILD: "1" DOCKER_DRIVER: overlay2 - - # ๐Ÿง  Nama image (pakai commit short SHA) IMAGE_NAME: "$CI_REGISTRY_IMAGE/web-lti:development_${CI_COMMIT_SHORT_SHA}" - - # Cache npm (disimpan antar pipeline) NPM_CACHE_DIR: "$CI_PROJECT_DIR/.npm" cache: @@ -25,20 +20,17 @@ build-image: - docker:dind before_script: - - echo "๐Ÿ” Logging in to GitLab Container Registry..." + - echo "Logging in to GitLab Container Registry..." - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY" script: - - echo "๐Ÿšง Building optimized Docker image..." + - echo "Building optimized Docker image..." - docker build --build-arg BUILDKIT_INLINE_CACHE=1 --cache-from $CI_REGISTRY_IMAGE/web-lti:latest -t "$IMAGE_NAME" . - docker push "$IMAGE_NAME" - - # ๐Ÿงน Keep only last 3 images (hapus yang lama) - - echo "๐Ÿงน Cleaning old images..." + - echo "Cleaning old images (keep last 3)..." - docker image prune -af --filter "until=72h" - after_script: - - echo "โœ… Build complete: $IMAGE_NAME" + after_script: "echo 'Build complete: $IMAGE_NAME'" rules: - if: '$CI_COMMIT_BRANCH == "development"' \ No newline at end of file From d3cc38aed52a938a818e42079844351f0b9e3ce1 Mon Sep 17 00:00:00 2001 From: GitLab Deploy Bot Date: Sun, 9 Nov 2025 15:15:26 +0700 Subject: [PATCH 06/36] edit Dockerfile --- Dockerfile | 45 +++++++++------------------------------------ 1 file changed, 9 insertions(+), 36 deletions(-) diff --git a/Dockerfile b/Dockerfile index f625ce73..f9a51e76 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,52 +1,25 @@ -# ============================================================ -# ๐Ÿ—๏ธ Stage 1 โ€” Builder -# ============================================================ -FROM node:20-alpine AS builder +FROM node:20-alpine -# Install dependensi dasar RUN apk add --no-cache git bash build-base curl WORKDIR /app -# Copy dependencies terlebih dahulu agar cache efisien COPY package*.json ./ +RUN npm ci -# Pastikan npm up to date agar mendukung flag terbaru -RUN npm install -g npm@11 && npm --version - -# Install dependency tanpa devDependencies (aman di semua npm versi) -RUN npm ci --only=production - -# Copy seluruh source COPY . . -# Buat konfigurasi output Next.js +# Buat config agar Next tahu output: export RUN echo "const config = { output: 'export', images: { unoptimized: true } }; export default config;" > next.config.mjs -# Build project (disable Turbopack agar tidak makan RAM) -ENV NEXT_DISABLE_TURBOPACK=1 -RUN npx next build && npm cache clean --force +# Build project (Next.js 15 otomatis static export) +RUN NEXT_DISABLE_TURBOPACK=1 npx next build -# Siapkan folder static untuk serve +# Pastikan folder static tersedia untuk URL _next/static RUN mkdir -p .next/server/app/_next && \ cp -r .next/static .next/server/app/_next/static && \ - cp -r public/assets .next/server/app/ || true - -# ============================================================ -# ๐Ÿงฑ Stage 2 โ€” Runtime -# ============================================================ -FROM node:20-alpine AS runtime - -RUN apk add --no-cache tini && npm install -g serve - -WORKDIR /app - -COPY --from=builder /app/.next/server/app ./server -COPY --from=builder /app/public ./public - -ENV NODE_ENV=production -ENV PORT=3000 + RUN cp -r public/* .next/server/app/ EXPOSE 3000 -ENTRYPOINT ["/sbin/tini", "--"] -CMD ["serve", "-s", "server", "-l", "3000"] \ No newline at end of file + +CMD ["npx", "serve", ".next/server/app", "-l", "3000"] \ No newline at end of file From 73d2de6dfbffd861ef63561b6c1f1aa7e4cf05ad Mon Sep 17 00:00:00 2001 From: GitLab Deploy Bot Date: Sun, 9 Nov 2025 15:21:15 +0700 Subject: [PATCH 07/36] edit Dockerfile --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index f9a51e76..752dbe35 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,7 +18,7 @@ RUN NEXT_DISABLE_TURBOPACK=1 npx next build # Pastikan folder static tersedia untuk URL _next/static RUN mkdir -p .next/server/app/_next && \ cp -r .next/static .next/server/app/_next/static && \ - RUN cp -r public/* .next/server/app/ + cp -r public/* .next/server/app/ EXPOSE 3000 From f126e976fdfa5d096ad711e2720e844186a9a7af Mon Sep 17 00:00:00 2001 From: kris Date: Sun, 9 Nov 2025 08:34:51 +0000 Subject: [PATCH 08/36] Update .gitlab-ci.yml file --- .gitlab-ci.yml | 41 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b7ad3839..603dc72d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,5 +1,6 @@ stages: - build + - deploy variables: DOCKER_BUILDKIT: "1" @@ -13,6 +14,9 @@ cache: paths: - .npm/ +# ========================= +# ๐Ÿ—๏ธ BUILD STAGE +# ========================= build-image: stage: build image: docker:27.0.3 @@ -25,12 +29,43 @@ build-image: script: - echo "Building optimized Docker image..." - - docker build --build-arg BUILDKIT_INLINE_CACHE=1 --cache-from $CI_REGISTRY_IMAGE/web-lti:latest -t "$IMAGE_NAME" . + - docker build --build-arg BUILDKIT_INLINE_CACHE=1 --cache-from "$CI_REGISTRY_IMAGE/web-lti:latest" -t "$IMAGE_NAME" . - docker push "$IMAGE_NAME" - - echo "Cleaning old images (keep last 3)..." + - echo "Cleaning old images..." - docker image prune -af --filter "until=72h" - after_script: "echo 'Build complete: $IMAGE_NAME'" + rules: + - if: '$CI_COMMIT_BRANCH == "development"' + +# ========================= +# ๐Ÿš€ DEPLOY STAGE +# ========================= +deploy-dev: + stage: deploy + image: alpine:3.20 + + before_script: + - echo "Installing dependencies..." + - apk add --no-cache openssh curl + - mkdir -p ~/.ssh + - echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa + - chmod 600 ~/.ssh/id_rsa + - eval $(ssh-agent -s) + - ssh-add ~/.ssh/id_rsa + - ssh-keyscan -H "$SERVER_IP" >> ~/.ssh/known_hosts + + script: + - echo "Deploying image to $SERVER_USER@$SERVER_IP" + - > + ssh -o StrictHostKeyChecking=no "$SERVER_USER@$SERVER_IP" " + docker pull $CI_REGISTRY_IMAGE/web-lti:development_${CI_COMMIT_SHORT_SHA} && + docker stop dev-web-lti || true && + docker rm dev-web-lti || true && + docker run -d --name dev-web-lti \ + --network dev-lti-network \ + -p 3002:3000 \ + $CI_REGISTRY_IMAGE/web-lti:development_${CI_COMMIT_SHORT_SHA} + " rules: - if: '$CI_COMMIT_BRANCH == "development"' \ No newline at end of file From b62427c5f450135f17fff9588e65e7fb79db63fd Mon Sep 17 00:00:00 2001 From: GitLab Deploy Bot Date: Sun, 9 Nov 2025 16:08:22 +0700 Subject: [PATCH 09/36] update Dockerfile --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 752dbe35..a3a2e197 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,7 +15,7 @@ RUN echo "const config = { output: 'export', images: { unoptimized: true } }; ex # Build project (Next.js 15 otomatis static export) RUN NEXT_DISABLE_TURBOPACK=1 npx next build -# Pastikan folder static tersedia untuk URL _next/static +# Copy static assets dan hasil build agar bisa diakses RUN mkdir -p .next/server/app/_next && \ cp -r .next/static .next/server/app/_next/static && \ cp -r public/* .next/server/app/ From 32f202d814595340ac362afd6c18149c19adffe8 Mon Sep 17 00:00:00 2001 From: kris Date: Sun, 9 Nov 2025 09:23:32 +0000 Subject: [PATCH 10/36] Update .gitlab-ci.yml file --- .gitlab-ci.yml | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 603dc72d..4291765e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,12 +3,25 @@ stages: - deploy variables: + # ๐Ÿ”ง Docker BuildKit lebih cepat DOCKER_BUILDKIT: "1" COMPOSE_DOCKER_CLI_BUILD: "1" DOCKER_DRIVER: overlay2 + + # ๐Ÿง  Tag image berdasarkan commit IMAGE_NAME: "$CI_REGISTRY_IMAGE/web-lti:development_${CI_COMMIT_SHORT_SHA}" + + # ๐Ÿ“ฆ Cache npm antar pipeline NPM_CACHE_DIR: "$CI_PROJECT_DIR/.npm" + # ๐ŸŒ Environment default untuk Next.js + NODE_ENV: "production" + HUSKY_SKIP_INSTALL: "1" + + # โš™๏ธ Inject ENV dari GitLab CI Variables ke Dockerfile + NEXT_PUBLIC_API_BASE_URL: "${NEXT_PUBLIC_API_BASE_URL}" + NEXT_PUBLIC_LTI_API_START_URL: "${NEXT_PUBLIC_LTI_API_START_URL}" + cache: key: npm-cache paths: @@ -24,14 +37,22 @@ build-image: - docker:dind before_script: - - echo "Logging in to GitLab Container Registry..." + - echo "๐Ÿ” Logging in to GitLab Container Registry..." - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY" script: - - echo "Building optimized Docker image..." - - docker build --build-arg BUILDKIT_INLINE_CACHE=1 --cache-from "$CI_REGISTRY_IMAGE/web-lti:latest" -t "$IMAGE_NAME" . + - echo "๐Ÿšง Building optimized Docker image..." + - > + docker build + --build-arg NEXT_PUBLIC_API_BASE_URL="$NEXT_PUBLIC_API_BASE_URL" + --build-arg NEXT_PUBLIC_LTI_API_START_URL="$NEXT_PUBLIC_LTI_API_START_URL" + --build-arg NODE_ENV="$NODE_ENV" + --build-arg HUSKY_SKIP_INSTALL="$HUSKY_SKIP_INSTALL" + --build-arg BUILDKIT_INLINE_CACHE=1 + --cache-from "$CI_REGISTRY_IMAGE/web-lti:latest" + -t "$IMAGE_NAME" . - docker push "$IMAGE_NAME" - - echo "Cleaning old images..." + - echo "๐Ÿงน Cleaning old images..." - docker image prune -af --filter "until=72h" rules: @@ -45,7 +66,7 @@ deploy-dev: image: alpine:3.20 before_script: - - echo "Installing dependencies..." + - echo "๐Ÿ“ฆ Installing dependencies..." - apk add --no-cache openssh curl - mkdir -p ~/.ssh - echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa @@ -55,7 +76,7 @@ deploy-dev: - ssh-keyscan -H "$SERVER_IP" >> ~/.ssh/known_hosts script: - - echo "Deploying image to $SERVER_USER@$SERVER_IP" + - echo "๐Ÿš€ Deploying image to $SERVER_USER@$SERVER_IP" - > ssh -o StrictHostKeyChecking=no "$SERVER_USER@$SERVER_IP" " docker pull $CI_REGISTRY_IMAGE/web-lti:development_${CI_COMMIT_SHORT_SHA} && From e7592eb221131a65d97040a2ce31ca602e810fcd Mon Sep 17 00:00:00 2001 From: kris Date: Sun, 9 Nov 2025 09:48:13 +0000 Subject: [PATCH 11/36] Update .gitlab-ci.yml file --- .gitlab-ci.yml | 63 +++++++++++++------------------------------------- 1 file changed, 16 insertions(+), 47 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 4291765e..8ee1e5f4 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,33 +3,16 @@ stages: - deploy variables: - # ๐Ÿ”ง Docker BuildKit lebih cepat DOCKER_BUILDKIT: "1" COMPOSE_DOCKER_CLI_BUILD: "1" DOCKER_DRIVER: overlay2 - - # ๐Ÿง  Tag image berdasarkan commit + BUILDKIT_PROGRESS: plain IMAGE_NAME: "$CI_REGISTRY_IMAGE/web-lti:development_${CI_COMMIT_SHORT_SHA}" - - # ๐Ÿ“ฆ Cache npm antar pipeline - NPM_CACHE_DIR: "$CI_PROJECT_DIR/.npm" - - # ๐ŸŒ Environment default untuk Next.js NODE_ENV: "production" HUSKY_SKIP_INSTALL: "1" - - # โš™๏ธ Inject ENV dari GitLab CI Variables ke Dockerfile NEXT_PUBLIC_API_BASE_URL: "${NEXT_PUBLIC_API_BASE_URL}" NEXT_PUBLIC_LTI_API_START_URL: "${NEXT_PUBLIC_LTI_API_START_URL}" -cache: - key: npm-cache - paths: - - .npm/ - -# ========================= -# ๐Ÿ—๏ธ BUILD STAGE -# ========================= build-image: stage: build image: docker:27.0.3 @@ -37,56 +20,42 @@ build-image: - docker:dind before_script: - - echo "๐Ÿ” Logging in to GitLab Container Registry..." + - echo "Login to registry" - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY" script: - - echo "๐Ÿšง Building optimized Docker image..." - - > - docker build - --build-arg NEXT_PUBLIC_API_BASE_URL="$NEXT_PUBLIC_API_BASE_URL" - --build-arg NEXT_PUBLIC_LTI_API_START_URL="$NEXT_PUBLIC_LTI_API_START_URL" - --build-arg NODE_ENV="$NODE_ENV" - --build-arg HUSKY_SKIP_INSTALL="$HUSKY_SKIP_INSTALL" - --build-arg BUILDKIT_INLINE_CACHE=1 - --cache-from "$CI_REGISTRY_IMAGE/web-lti:latest" - -t "$IMAGE_NAME" . + - docker build \ + --build-arg NEXT_PUBLIC_API_BASE_URL="$NEXT_PUBLIC_API_BASE_URL" \ + --build-arg NEXT_PUBLIC_LTI_API_START_URL="$NEXT_PUBLIC_LTI_API_START_URL" \ + --build-arg NODE_ENV="$NODE_ENV" \ + --build-arg HUSKY_SKIP_INSTALL="$HUSKY_SKIP_INSTALL" \ + --cache-from "$CI_REGISTRY_IMAGE/web-lti:latest" \ + -t "$IMAGE_NAME" . - docker push "$IMAGE_NAME" - - echo "๐Ÿงน Cleaning old images..." - docker image prune -af --filter "until=72h" + after_script: "echo 'Build complete: $IMAGE_NAME' && docker system prune -af || true && docker volume prune -f || true" + rules: - if: '$CI_COMMIT_BRANCH == "development"' -# ========================= -# ๐Ÿš€ DEPLOY STAGE -# ========================= deploy-dev: stage: deploy image: alpine:3.20 before_script: - - echo "๐Ÿ“ฆ Installing dependencies..." - apk add --no-cache openssh curl - mkdir -p ~/.ssh - - echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa + - echo \"$SSH_PRIVATE_KEY\" > ~/.ssh/id_rsa - chmod 600 ~/.ssh/id_rsa - eval $(ssh-agent -s) - ssh-add ~/.ssh/id_rsa - - ssh-keyscan -H "$SERVER_IP" >> ~/.ssh/known_hosts + - ssh-keyscan -H \"$SERVER_IP\" >> ~/.ssh/known_hosts script: - - echo "๐Ÿš€ Deploying image to $SERVER_USER@$SERVER_IP" - - > - ssh -o StrictHostKeyChecking=no "$SERVER_USER@$SERVER_IP" " - docker pull $CI_REGISTRY_IMAGE/web-lti:development_${CI_COMMIT_SHORT_SHA} && - docker stop dev-web-lti || true && - docker rm dev-web-lti || true && - docker run -d --name dev-web-lti \ - --network dev-lti-network \ - -p 3002:3000 \ - $CI_REGISTRY_IMAGE/web-lti:development_${CI_COMMIT_SHORT_SHA} - " + - ssh -o StrictHostKeyChecking=no \"$SERVER_USER@$SERVER_IP\" \"docker stop dev-web-lti || true && docker rm dev-web-lti || true && docker pull $CI_REGISTRY_IMAGE/web-lti:development_${CI_COMMIT_SHORT_SHA} && docker run -d --name dev-web-lti --network dev-lti-network -p 3002:3000 $CI_REGISTRY_IMAGE/web-lti:development_${CI_COMMIT_SHORT_SHA}\" + + after_script: "echo 'Deploy finished for $IMAGE_NAME'" rules: - if: '$CI_COMMIT_BRANCH == "development"' \ No newline at end of file From f14adc46d389a650164bac415c734a700bd8d8c1 Mon Sep 17 00:00:00 2001 From: kris Date: Sun, 9 Nov 2025 09:50:29 +0000 Subject: [PATCH 12/36] Update .gitlab-ci.yml file --- .gitlab-ci.yml | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8ee1e5f4..d161aff3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -24,15 +24,16 @@ build-image: - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY" script: - - docker build \ - --build-arg NEXT_PUBLIC_API_BASE_URL="$NEXT_PUBLIC_API_BASE_URL" \ - --build-arg NEXT_PUBLIC_LTI_API_START_URL="$NEXT_PUBLIC_LTI_API_START_URL" \ - --build-arg NODE_ENV="$NODE_ENV" \ - --build-arg HUSKY_SKIP_INSTALL="$HUSKY_SKIP_INSTALL" \ - --cache-from "$CI_REGISTRY_IMAGE/web-lti:latest" \ - -t "$IMAGE_NAME" . - - docker push "$IMAGE_NAME" - - docker image prune -af --filter "until=72h" + - | + docker build \ + --build-arg NEXT_PUBLIC_API_BASE_URL="$NEXT_PUBLIC_API_BASE_URL" \ + --build-arg NEXT_PUBLIC_LTI_API_START_URL="$NEXT_PUBLIC_LTI_API_START_URL" \ + --build-arg NODE_ENV="$NODE_ENV" \ + --build-arg HUSKY_SKIP_INSTALL="$HUSKY_SKIP_INSTALL" \ + --cache-from "$CI_REGISTRY_IMAGE/web-lti:latest" \ + -t "$IMAGE_NAME" . + - docker push "$IMAGE_NAME" + - docker image prune -af --filter "until=72h" after_script: "echo 'Build complete: $IMAGE_NAME' && docker system prune -af || true && docker volume prune -f || true" From 773aa2dbb1df3566237f27deabbda3615c577d71 Mon Sep 17 00:00:00 2001 From: kris Date: Sun, 9 Nov 2025 10:10:19 +0000 Subject: [PATCH 13/36] Update .gitlab-ci.yml file --- .gitlab-ci.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d161aff3..d5145dfc 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -45,13 +45,13 @@ deploy-dev: image: alpine:3.20 before_script: - - apk add --no-cache openssh curl - - mkdir -p ~/.ssh - - echo \"$SSH_PRIVATE_KEY\" > ~/.ssh/id_rsa - - chmod 600 ~/.ssh/id_rsa - - eval $(ssh-agent -s) - - ssh-add ~/.ssh/id_rsa - - ssh-keyscan -H \"$SERVER_IP\" >> ~/.ssh/known_hosts + - apk add --no-cache openssh curl + - mkdir -p ~/.ssh + - printf "%b" "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa + - chmod 600 ~/.ssh/id_rsa + - eval $(ssh-agent -s) + - ssh-add ~/.ssh/id_rsa + - ssh-keyscan -H "$SERVER_IP" >> ~/.ssh/known_hosts script: - ssh -o StrictHostKeyChecking=no \"$SERVER_USER@$SERVER_IP\" \"docker stop dev-web-lti || true && docker rm dev-web-lti || true && docker pull $CI_REGISTRY_IMAGE/web-lti:development_${CI_COMMIT_SHORT_SHA} && docker run -d --name dev-web-lti --network dev-lti-network -p 3002:3000 $CI_REGISTRY_IMAGE/web-lti:development_${CI_COMMIT_SHORT_SHA}\" From 13d57c206bb5c9e861f0493096fc67a6703b60f5 Mon Sep 17 00:00:00 2001 From: kris Date: Sun, 9 Nov 2025 10:21:06 +0000 Subject: [PATCH 14/36] Update .gitlab-ci.yml file --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d5145dfc..f829f049 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -47,7 +47,7 @@ deploy-dev: before_script: - apk add --no-cache openssh curl - mkdir -p ~/.ssh - - printf "%b" "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa + - echo "$SSH_PRIVATE_KEY" | base64 -d > ~/.ssh/id_rsa - chmod 600 ~/.ssh/id_rsa - eval $(ssh-agent -s) - ssh-add ~/.ssh/id_rsa From f00e77201841a4788024dd422c096bee2ebd6909 Mon Sep 17 00:00:00 2001 From: kris Date: Tue, 11 Nov 2025 06:07:24 +0000 Subject: [PATCH 15/36] Update .gitlab-ci.yml file --- .gitlab-ci.yml | 187 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 136 insertions(+), 51 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f829f049..345f305f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,62 +1,147 @@ stages: - build - deploy - -variables: - DOCKER_BUILDKIT: "1" - COMPOSE_DOCKER_CLI_BUILD: "1" - DOCKER_DRIVER: overlay2 - BUILDKIT_PROGRESS: plain - IMAGE_NAME: "$CI_REGISTRY_IMAGE/web-lti:development_${CI_COMMIT_SHORT_SHA}" - NODE_ENV: "production" - HUSKY_SKIP_INSTALL: "1" - NEXT_PUBLIC_API_BASE_URL: "${NEXT_PUBLIC_API_BASE_URL}" - NEXT_PUBLIC_LTI_API_START_URL: "${NEXT_PUBLIC_LTI_API_START_URL}" - -build-image: + +.build_template: &build_template stage: build - image: docker:27.0.3 - services: - - docker:dind - - before_script: - - echo "Login to registry" - - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY" - + image: node:20-alpine + cache: + key: npm-cache + paths: + - node_modules/ + variables: + NPM_CONFIG_PRODUCTION: "false" + NODE_ENV: "" script: - - | - docker build \ - --build-arg NEXT_PUBLIC_API_BASE_URL="$NEXT_PUBLIC_API_BASE_URL" \ - --build-arg NEXT_PUBLIC_LTI_API_START_URL="$NEXT_PUBLIC_LTI_API_START_URL" \ - --build-arg NODE_ENV="$NODE_ENV" \ - --build-arg HUSKY_SKIP_INSTALL="$HUSKY_SKIP_INSTALL" \ - --cache-from "$CI_REGISTRY_IMAGE/web-lti:latest" \ - -t "$IMAGE_NAME" . - - docker push "$IMAGE_NAME" - - docker image prune -af --filter "until=72h" + - echo "Installing dependencies..." + - npm ci --no-audit --no-fund + - echo "Building Next.js static export..." + - npx next build + artifacts: + name: "out-$CI_COMMIT_SHORT_SHA" + paths: + - out/ + expire_in: 1 week - after_script: "echo 'Build complete: $IMAGE_NAME' && docker system prune -af || true && docker volume prune -f || true" +.deploy_template: &deploy_template + stage: deploy + image: + name: amazon/aws-cli:latest + entrypoint: ["/bin/sh", "-c"] + script: + - set -e + - aws --version + - echo "Cleaning up newline characters in AWS credentials..." + - export AWS_ACCESS_KEY_ID=$(echo $AWS_ACCESS_KEY_ID | tr -d '\r\n') + - export AWS_SECRET_ACCESS_KEY=$(echo $AWS_SECRET_ACCESS_KEY | tr -d '\r\n') + - echo "Deploying to s3://$S3_BUCKET in region $AWS_REGION" + - aws s3api head-bucket --bucket "$S3_BUCKET" --region "$AWS_REGION" || aws s3api create-bucket --bucket "$S3_BUCKET" --region "$AWS_REGION" --create-bucket-configuration LocationConstraint="$AWS_REGION" + - aws s3 sync ./out "s3://$S3_BUCKET" --delete --region "$AWS_REGION" --endpoint-url "https://s3.ap-southeast-3.amazonaws.com" + # CloudFront invalidation + - | + STATUS="success" + if [ -n "$CLOUDFRONT_DISTRIBUTION_ID" ]; then + echo "Invalidating CloudFront cache..." + if ! aws cloudfront create-invalidation --distribution-id "$CLOUDFRONT_DISTRIBUTION_ID" --paths "/*"; then + echo "CloudFront invalidation failed." + STATUS="failed" + fi + else + echo "No CloudFront distribution specified โ€” skipping invalidation" + fi + + # Notifikasi Discord + - | + RUN_URL="${CI_PROJECT_URL}/-/pipelines/${CI_PIPELINE_ID}" + + if [ "$CI_COMMIT_BRANCH" = "development" ]; then + ENVIRONMENT_NAME="WEB-LTI-DEV" + elif [ "$CI_COMMIT_BRANCH" = "master" ]; then + ENVIRONMENT_NAME="WEB-LTI-PROD" + else + ENVIRONMENT_NAME="UNKNOWN" + fi + + if [ "$STATUS" = "success" ]; then + COLOR=3066993 + TITLE="โœ… Deployment ${ENVIRONMENT_NAME} Succeeded" + DESC="Deployment job on branch \`${CI_COMMIT_REF_NAME}\` completed successfully." + else + COLOR=15158332 + TITLE="โŒ Deployment ${ENVIRONMENT_NAME} Failed" + DESC="Deployment job on branch \`${CI_COMMIT_REF_NAME}\` encountered issues." + fi + + jq -n \ + --arg title "$TITLE" \ + --arg desc "$DESC" \ + --arg color "$COLOR" \ + --arg repo "$CI_PROJECT_PATH" \ + --arg actor "$GITLAB_USER_LOGIN" \ + --arg commit "$CI_COMMIT_SHA" \ + --arg run_url "$RUN_URL" \ + '{ + username: "CI Bot - LTI WEB", + embeds: [{ + title: $title, + description: $desc, + color: ($color|tonumber), + fields: [ + {name: "Repository", value: $repo, inline: true}, + {name: "Actor", value: $actor, inline: true}, + {name: "Commit", value: $commit, inline: false}, + {name: "Pipeline", value: ("[Open run](" + $run_url + ")"), inline: false} + ] + }] + }' > payload.json + + curl -sS -H "Content-Type: application/json" -d @payload.json "$DISCORD_WEBHOOK_URL" + +# ====== DEVELOPMENT (Branch development) ====== +build:dev: + <<: *build_template rules: - if: '$CI_COMMIT_BRANCH == "development"' + environment: + name: development + variables: + NEXT_PUBLIC_API_BASE_URL: "https://dev-api-lti.mbugroup.id" + NEXT_PUBLIC_SSO_LOGIN_URL: "https://dev-api-sso.mbugroup.id" -deploy-dev: - stage: deploy - image: alpine:3.20 - - before_script: - - apk add --no-cache openssh curl - - mkdir -p ~/.ssh - - echo "$SSH_PRIVATE_KEY" | base64 -d > ~/.ssh/id_rsa - - chmod 600 ~/.ssh/id_rsa - - eval $(ssh-agent -s) - - ssh-add ~/.ssh/id_rsa - - ssh-keyscan -H "$SERVER_IP" >> ~/.ssh/known_hosts - - script: - - ssh -o StrictHostKeyChecking=no \"$SERVER_USER@$SERVER_IP\" \"docker stop dev-web-lti || true && docker rm dev-web-lti || true && docker pull $CI_REGISTRY_IMAGE/web-lti:development_${CI_COMMIT_SHORT_SHA} && docker run -d --name dev-web-lti --network dev-lti-network -p 3002:3000 $CI_REGISTRY_IMAGE/web-lti:development_${CI_COMMIT_SHORT_SHA}\" - - after_script: "echo 'Deploy finished for $IMAGE_NAME'" - +deploy:dev: + <<: *deploy_template + needs: ["build:dev"] rules: - - if: '$CI_COMMIT_BRANCH == "development"' \ No newline at end of file + - if: '$CI_COMMIT_BRANCH == "development"' + variables: + S3_BUCKET: "dev-lti-erp.mbugroup.id" + AWS_REGION: "ap-southeast-3" + CLOUDFRONT_DISTRIBUTION_ID: "E1Z8XTA8XF1GIV" + environment: + name: development + url: https://dev-lti-erp.mbugroup.id + +# ====== PRODUCTION ====== +# build:production: +# <<: *build_template +# rules: +# # pilih salah satu: pakai branch master ATAU pakai tags rilis +# - if: '$CI_COMMIT_BRANCH == "master"' +# # - if: '$CI_COMMIT_TAG' # kalau mau rilis via tag, uncomment ini dan hapus baris di atas +# environment: +# name: production + +# deploy:production: +# <<: *deploy_template +# needs: ["build:production"] +# rules: +# - if: '$CI_COMMIT_BRANCH == "master"' +# # - if: '$CI_COMMIT_TAG' # selaras dengan rule di build:production +# variables: +# S3_BUCKET: "lti-erp.mbugroup.id" +# CLOUDFRONT_DISTRIBUTION_ID: "ddfd" +# environment: +# name: production + # url: https://royalgoldcapital.com + From 4422b7391af22f35cb59a818422610c2bd83b6d4 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 12 Nov 2025 13:04:09 +0700 Subject: [PATCH 16/36] chore(FE-149): install react-day-picker --- package-lock.json | 44 ++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 45 insertions(+) diff --git a/package-lock.json b/package-lock.json index 33b7c640..2cac4bc7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "moment": "^2.30.1", "next": "15.5.3", "react": "19.1.0", + "react-day-picker": "^9.11.1", "react-dom": "19.1.0", "react-hot-toast": "^2.6.0", "react-number-format": "^5.4.4", @@ -196,6 +197,12 @@ "node": ">=6.9.0" } }, + "node_modules/@date-fns/tz": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.4.1.tgz", + "integrity": "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==", + "license": "MIT" + }, "node_modules/@emnapi/core": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.6.0.tgz", @@ -2873,6 +2880,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/date-fns-jalali": { + "version": "4.1.0-0", + "resolved": "https://registry.npmjs.org/date-fns-jalali/-/date-fns-jalali-4.1.0-0.tgz", + "integrity": "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -5749,6 +5772,27 @@ "node": ">=0.10.0" } }, + "node_modules/react-day-picker": { + "version": "9.11.1", + "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.11.1.tgz", + "integrity": "sha512-l3ub6o8NlchqIjPKrRFUCkTUEq6KwemQlfv3XZzzwpUeGwmDJ+0u0Upmt38hJyd7D/vn2dQoOoLV/qAp0o3uUw==", + "license": "MIT", + "dependencies": { + "@date-fns/tz": "^1.4.1", + "date-fns": "^4.1.0", + "date-fns-jalali": "^4.1.0-0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/gpbl" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/react-dom": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", diff --git a/package.json b/package.json index 10fe9598..033c2963 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "moment": "^2.30.1", "next": "15.5.3", "react": "19.1.0", + "react-day-picker": "^9.11.1", "react-dom": "19.1.0", "react-hot-toast": "^2.6.0", "react-number-format": "^5.4.4", From 4bbf6fd7f8f2e71a19d4a8e00f98de936822f414 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 12 Nov 2025 13:10:18 +0700 Subject: [PATCH 17/36] feat(FE-149): integrate transfer to laying detail page to API --- .../transfer-to-laying/detail/page.tsx | 100 +----------------- 1 file changed, 4 insertions(+), 96 deletions(-) diff --git a/src/app/production/transfer-to-laying/detail/page.tsx b/src/app/production/transfer-to-laying/detail/page.tsx index de5426c8..9ff6ed5e 100644 --- a/src/app/production/transfer-to-laying/detail/page.tsx +++ b/src/app/production/transfer-to-laying/detail/page.tsx @@ -8,91 +8,6 @@ import TransferToLayingForm from '@/components/pages/production/transfer-to-layi import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; -import { TransferToLaying } from '@/types/api/production/transfer-to-laying'; - -// TODO: delete dummy data -const DUMMY_TRANSFER_TO_LAYING_DETAIL: TransferToLaying = { - id: 1, - transfer_date: '2025-10-14', - flock_source: { - id: 1, - name: 'Flock asal test', - }, - flock_destination: { - id: 2, - name: 'Flock tujuan destination', - }, - quantity: 10, - kandangs: [ - { - kandang: { - id: 1, - name: 'Kandang test', - status: 'ACTIVE', - location: { - id: 1, - name: 'test location', - address: 'test address 1', - area: { id: 1, name: 'test area 1' }, - }, - pic: { - id: 1, - id_user: 2, - email: 'test@gmail.com', - name: 'test', - }, - created_user: { - id: 1, - id_user: 2, - email: 'test@gmail.com', - name: 'test', - }, - created_at: '14-10-2025', - updated_at: '14-10-2025', - }, - quantity: 8, - }, - { - kandang: { - id: 1, - name: 'Kandang test 2', - status: 'ACTIVE', - location: { - id: 1, - name: 'test location', - address: 'test address 1', - area: { id: 1, name: 'test area 1' }, - }, - pic: { - id: 1, - id_user: 2, - email: 'test@gmail.com', - name: 'test', - }, - created_user: { - id: 1, - id_user: 2, - email: 'test@gmail.com', - name: 'test', - }, - created_at: '14-10-2025', - updated_at: '14-10-2025', - }, - quantity: 2, - }, - ], - reason: 'Test alasan', - - created_user: { - id: 1, - id_user: 2, - email: 'test@gmail.com', - name: 'test', - }, - created_at: '14-10-2025', - updated_at: '14-10-2025', -}; - const TransferToLayingDetail = () => { const router = useRouter(); const searchParams = useSearchParams(); @@ -114,11 +29,9 @@ const TransferToLayingDetail = () => { ); } - // TODO: remove dummy data and integrate with real API if ( !isLoadingTransferToLaying && - (!transferToLaying || - (isResponseError(transferToLaying) && !DUMMY_TRANSFER_TO_LAYING_DETAIL)) + (!transferToLaying || isResponseError(transferToLaying)) ) { router.replace('/404'); return; @@ -129,18 +42,13 @@ const TransferToLayingDetail = () => { {isLoadingTransferToLaying && ( )} - {/* {!isLoadingTransferToLaying && isResponseSuccess(transferToLaying) && ( + + {!isLoadingTransferToLaying && isResponseSuccess(transferToLaying) && ( - )} */} - - {/* TODO: remove this dummy data and integrate to real API */} - + )} ); }; From 963377199f7b6610ff042c36145d1b55ef41f5c0 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 12 Nov 2025 13:11:30 +0700 Subject: [PATCH 18/36] feat(FE-149): integrate transfer to laying edit page to API --- .../transfer-to-laying/detail/edit/page.tsx | 109 ++---------------- 1 file changed, 12 insertions(+), 97 deletions(-) diff --git a/src/app/production/transfer-to-laying/detail/edit/page.tsx b/src/app/production/transfer-to-laying/detail/edit/page.tsx index 9003dbba..d5498e08 100644 --- a/src/app/production/transfer-to-laying/detail/edit/page.tsx +++ b/src/app/production/transfer-to-laying/detail/edit/page.tsx @@ -8,91 +8,6 @@ import TransferToLayingForm from '@/components/pages/production/transfer-to-layi import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; -import { TransferToLaying } from '@/types/api/production/transfer-to-laying'; - -// TODO: delete dummy data -const DUMMY_TRANSFER_TO_LAYING_EDIT: TransferToLaying = { - id: 1, - transfer_date: '2025-10-14', - flock_source: { - id: 1, - name: 'Flock asal test', - }, - flock_destination: { - id: 2, - name: 'Flock tujuan destination', - }, - quantity: 10, - kandangs: [ - { - kandang: { - id: 1, - name: 'Kandang test', - status: 'ACTIVE', - location: { - id: 1, - name: 'test location', - address: 'test address 1', - area: { id: 1, name: 'test area 1' }, - }, - pic: { - id: 1, - id_user: 2, - email: 'test@gmail.com', - name: 'test', - }, - created_user: { - id: 1, - id_user: 2, - email: 'test@gmail.com', - name: 'test', - }, - created_at: '14-10-2025', - updated_at: '14-10-2025', - }, - quantity: 8, - }, - { - kandang: { - id: 1, - name: 'Kandang test 2', - status: 'ACTIVE', - location: { - id: 1, - name: 'test location', - address: 'test address 1', - area: { id: 1, name: 'test area 1' }, - }, - pic: { - id: 1, - id_user: 2, - email: 'test@gmail.com', - name: 'test', - }, - created_user: { - id: 1, - id_user: 2, - email: 'test@gmail.com', - name: 'test', - }, - created_at: '14-10-2025', - updated_at: '14-10-2025', - }, - quantity: 2, - }, - ], - reason: 'Test alasan', - - created_user: { - id: 1, - id_user: 2, - email: 'test@gmail.com', - name: 'test', - }, - created_at: '14-10-2025', - updated_at: '14-10-2025', -}; - const TransferToLayingEdit = () => { const router = useRouter(); const searchParams = useSearchParams(); @@ -114,33 +29,33 @@ const TransferToLayingEdit = () => { ); } - // TODO: remove dummy data and integrate with real API if ( !isLoadingTransferToLaying && - (!transferToLaying || - (isResponseError(transferToLaying) && !DUMMY_TRANSFER_TO_LAYING_EDIT)) + (!transferToLaying || isResponseError(transferToLaying)) ) { router.replace('/404'); return; } + if ( + isResponseSuccess(transferToLaying) && + transferToLaying.data.approval.step_number === 2 + ) { + router.replace('/production/transfer-to-laying'); + return; + } + return (
{isLoadingTransferToLaying && ( )} - {/* {!isLoadingTransferToLaying && isResponseSuccess(transferToLaying) && ( + {!isLoadingTransferToLaying && isResponseSuccess(transferToLaying) && ( - )} */} - - {/* TODO: remove this dummy data and integrate to real API */} - + )}
); }; From 03b16248e568d733f10d0cec5d1c956a18f9e788 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 12 Nov 2025 13:15:47 +0700 Subject: [PATCH 19/36] chore: update Modal component --- src/components/Modal.tsx | 60 +++++++++++++++++++++++++--------------- 1 file changed, 37 insertions(+), 23 deletions(-) diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx index a84c1827..a242b1e4 100644 --- a/src/components/Modal.tsx +++ b/src/components/Modal.tsx @@ -1,6 +1,13 @@ 'use client'; -import { ReactNode, RefObject, useCallback, useRef, useState } from 'react'; +import { + ReactNode, + RefObject, + useCallback, + useEffect, + useRef, + useState, +} from 'react'; import { cn } from '@/lib/helper'; export const useModal = () => { @@ -8,31 +15,34 @@ export const useModal = () => { const [open, setOpen] = useState(false); const openModal = useCallback(() => { + if (!ref.current) return; + ref.current.show(); setOpen(true); - - ref.current?.showModal(); }, []); const closeModal = useCallback(() => { + if (!ref.current) return; + ref.current.close(); setOpen(false); - ref.current?.close(); }, []); const toggle = useCallback(() => { - if (open) { - closeModal(); - } else { - openModal(); - } + open ? closeModal() : openModal(); }, [open, closeModal, openModal]); - if (ref.current) { - ref.current.addEventListener('close', () => { - closeModal(); - }); - } + useEffect(() => { + const dialog = ref.current; + if (!dialog) return; - return { ref, open, setOpen, openModal, closeModal, toggle } as const; + const handleClose = () => setOpen(false); + dialog.addEventListener('close', handleClose); + + return () => { + dialog.removeEventListener('close', handleClose); + }; + }, []); + + return { ref, open, openModal, closeModal, toggle } as const; }; interface ModalProps { @@ -46,15 +56,19 @@ interface ModalProps { } const Modal = ({ ref, children, closeOnBackdrop, className }: ModalProps) => { - return ( - -
{children}
+ const handleBackdropClick = (e: React.MouseEvent) => { + if (closeOnBackdrop && e.target === ref.current) { + ref.current?.close(); + } + }; - {closeOnBackdrop && ( -
- -
- )} + return ( + +
{children}
); }; From 2959295bfa283d55e151d58a93f11a9a5eadb8a4 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 12 Nov 2025 13:18:26 +0700 Subject: [PATCH 20/36] chore: add enableRowSelection prop --- src/components/Table.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/components/Table.tsx b/src/components/Table.tsx index d3498e33..b02dd3b5 100644 --- a/src/components/Table.tsx +++ b/src/components/Table.tsx @@ -13,6 +13,7 @@ import { FilterFn, SortingState, OnChangeFn, + Row, } from '@tanstack/react-table'; import { rankItem } from '@tanstack/match-sorter-utils'; import { Icon } from '@iconify/react'; @@ -50,6 +51,7 @@ export interface TableProps { manualSorting?: boolean; rowSelection?: Record; setRowSelection?: OnChangeFn>; + enableRowSelection?: boolean | ((row: Row) => boolean); } const DUMMY_SKELETON_DATA = [{}, {}, {}, {}, {}]; @@ -90,6 +92,7 @@ const Table = ({ manualSorting = false, rowSelection, setRowSelection, + enableRowSelection, }: TableProps) => { const isServerSideTable = totalItems !== undefined && @@ -150,6 +153,10 @@ const Table = ({ tableOptions.getRowId = (row) => (row as { id: string }).id; } + if (enableRowSelection !== undefined) { + tableOptions.enableRowSelection = enableRowSelection; + } + const table = useReactTable(tableOptions); const { setPageSize } = table; From 73ab5703db23b5e009557a9c11bc6af1a5e1856b Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 12 Nov 2025 13:19:00 +0700 Subject: [PATCH 21/36] chore: update DateInput component --- src/components/input/DateInput.tsx | 282 ++++++++++++++++++++++++----- 1 file changed, 241 insertions(+), 41 deletions(-) diff --git a/src/components/input/DateInput.tsx b/src/components/input/DateInput.tsx index 6e2f1d77..7317b038 100644 --- a/src/components/input/DateInput.tsx +++ b/src/components/input/DateInput.tsx @@ -1,14 +1,23 @@ 'use client'; -import { ChangeEventHandler, FocusEventHandler, ReactNode } from 'react'; - -import { cn } from '@/lib/helper'; +import { + ChangeEventHandler, + FocusEventHandler, + useEffect, + useState, +} from 'react'; +import { cn, formatDate } from '@/lib/helper'; +import Modal, { useModal } from '@/components/Modal'; +import { DateRange, DayPicker, Matcher } from 'react-day-picker'; +import 'react-day-picker/dist/style.css'; +import Button from '@/components/Button'; +import { Icon } from '@iconify/react'; export interface DateInputProps { label?: string; bottomLabel?: string; name: string; - value?: string; + value?: string | { from?: string; to?: string }; placeholder?: string; min?: string; max?: string; @@ -24,9 +33,8 @@ export interface DateInputProps { readOnly?: boolean; required?: boolean; isLoading?: boolean; + isRange?: boolean; errorMessage?: string; - startAdornment?: ReactNode; - endAdornment?: ReactNode; onChange?: ChangeEventHandler; onBlur?: FocusEventHandler; } @@ -36,22 +44,144 @@ const DateInput = ({ bottomLabel, name, value, - placeholder, + placeholder = 'dd/mm/yyyy', min, max, className, - isError, - isValid, - errorMessage, - startAdornment, - endAdornment, + isError: externalError, + isValid: externalValid, + errorMessage: externalErrorMessage, disabled = false, required = false, onChange, onBlur, readOnly = false, isLoading = false, + isRange = false, }: DateInputProps) => { + const [internalError, setInternalError] = useState(null); + const [selected, setSelected] = useState(); + const [selectedRange, setSelectedRange] = useState<{ + from?: Date; + to?: Date; + }>({}); + const [displayValue, setDisplayValue] = useState(''); + + const minDate = min + ? new Date(min.split('/').reverse().join('-')) + : undefined; + const maxDate = max + ? new Date(max.split('/').reverse().join('-')) + : undefined; + + const calendarModal = useModal(); + + // --- Sync value props --- + useEffect(() => { + if (!value) return; + if (isRange && typeof value === 'object') { + const from = value.from ? new Date(value.from) : undefined; + const to = value.to ? new Date(value.to) : undefined; + setSelectedRange({ from, to }); + setDisplayValue( + `${from ? formatDate(from, 'DD/MM/YYYY') : ''} ${ + to ? '- ' + formatDate(to, 'DD/MM/YYYY') : '' + }` + ); + } else if (typeof value === 'string') { + const iso = value.includes('/') + ? value.split('/').reverse().join('-') + : value; + const date = new Date(iso); + setSelected(date); + setDisplayValue(formatDate(iso, 'DD/MM/YYYY')); + } + }, [value, isRange]); + + const handleClick = (e: React.MouseEvent) => { + e.preventDefault(); + if (!disabled && !readOnly) calendarModal.openModal(); + }; + + const handleBlur: FocusEventHandler = (e) => { + onBlur?.(e); + }; + + const handleSelectSingle = (selectedDate?: Date) => { + if (!selectedDate) return; + if (minDate && selectedDate < minDate) { + setInternalError(`Tanggal tidak boleh sebelum ${min}`); + return; + } + if (maxDate && selectedDate > maxDate) { + setInternalError(`Tanggal tidak boleh setelah ${max}`); + return; + } + setInternalError(null); + setSelected(selectedDate); + const formattedDisplay = formatDate(selectedDate, 'DD/MM/YYYY'); + const formattedISO = formatDate(selectedDate, 'YYYY-MM-DD'); + setDisplayValue(formattedDisplay); + + const syntheticEvent = { + target: { name, value: formattedISO }, + } as unknown as React.ChangeEvent; + onChange?.(syntheticEvent); + calendarModal.closeModal(); + }; + + const handleSelectRange = (range?: { from?: Date; to?: Date }) => { + if (!range) return; + setSelectedRange(range); + + const fromStr = range.from ? formatDate(range.from, 'DD/MM/YYYY') : ''; + const toStr = range.to ? formatDate(range.to, 'DD/MM/YYYY') : ''; + setDisplayValue(`${fromStr}${toStr ? ' - ' + toStr : ''}`); + + // Jika kedua tanggal sudah terpilih + if (range.from && range.to) { + if (minDate && range.from < minDate) { + setInternalError(`Tanggal mulai tidak boleh sebelum ${min}`); + return; + } + if (maxDate && range.to > maxDate) { + setInternalError(`Tanggal akhir tidak boleh setelah ${max}`); + return; + } + + setInternalError(null); + const syntheticEvent = { + target: { + name, + value: { + from: formatDate(range.from, 'YYYY-MM-DD'), + to: formatDate(range.to, 'YYYY-MM-DD'), + }, + }, + } as unknown as React.ChangeEvent; + onChange?.(syntheticEvent); + } + }; + + const handleResetDate = () => { + setSelected(undefined); + setSelectedRange({}); + setDisplayValue(''); + const syntheticEvent = { + target: { name, value: isRange ? { from: '', to: '' } : '' }, + } as unknown as React.ChangeEvent; + onChange?.(syntheticEvent); + calendarModal.closeModal(); + }; + + const handleSaveDate = () => { + if (internalError) return; + calendarModal.closeModal(); + }; + + const finalIsError = externalError || !!internalError; + const finalErrorMessage = internalError || externalErrorMessage; + return (
{label} {required && ( - <> - {' '} - - * - - + + * + )} )}
- {startAdornment && startAdornment} - - {(isLoading || endAdornment) && ( + {isLoading && (
- {isLoading && } - {endAdornment && endAdornment} +
)} + + handleClick(e as unknown as React.MouseEvent) + } + />
- {!isError && bottomLabel && ( + {!finalIsError && bottomLabel && (

{bottomLabel}

)} - {isError && errorMessage && ( -

{errorMessage}

+ {finalIsError && finalErrorMessage && ( +

{finalErrorMessage}

)} + + + {isRange ? ( + {displayValue}
} + disabled={ + [ + minDate ? { before: minDate } : undefined, + maxDate ? { after: maxDate } : undefined, + ].filter(Boolean) as Matcher[] + } + /> + ) : ( + + )} +
+ {isRange && ( + + Tekan dua kali untuk memilih tanggal awal + + )} + +
+ + {isRange && ( + + )} +
+
+ ); }; From c9092f36e35d1e8507e677eb550d5b94faadca2e Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 12 Nov 2025 13:23:20 +0700 Subject: [PATCH 22/36] chore: add children prop --- src/components/modal/ConfirmationModal.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/modal/ConfirmationModal.tsx b/src/components/modal/ConfirmationModal.tsx index 04c221e6..683345f5 100644 --- a/src/components/modal/ConfirmationModal.tsx +++ b/src/components/modal/ConfirmationModal.tsx @@ -9,7 +9,7 @@ import Button from '@/components/Button'; import { cn } from '@/lib/helper'; import { Color } from '@/types/theme'; -interface ConfirmationModalProps { +export interface ConfirmationModalProps { ref: RefObject; type?: 'info' | 'success' | 'error'; text?: string; @@ -30,6 +30,7 @@ interface ConfirmationModalProps { modal?: string; modalBox?: string; }; + children?: React.ReactNode; } const ConfirmationModal = ({ @@ -40,6 +41,7 @@ const ConfirmationModal = ({ primaryButton, secondaryButton, className, + children, }: ConfirmationModalProps) => { const closeModalHandler = () => { ref.current?.close(); @@ -90,6 +92,8 @@ const ConfirmationModal = ({ {text ?? 'Apakah anda yakin ingin melakukan hal ini?'}

+ {children &&
{children}
} +
{secondaryButton && secondaryButton.text && (