├── .dockerignore ├── .eslintrc.json ├── .github ├── FUNDING.yml └── workflows │ ├── cicd.yaml │ └── codeql.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── Makefile ├── README.md ├── biome.json ├── cmd ├── setup-auto.sh └── setup-manual.sh ├── commitlint.config.cts ├── components.json ├── docker ├── development │ ├── Dockerfile │ └── compose.yaml ├── production │ ├── Dockerfile │ └── compose.yaml └── staging │ ├── Dockerfile │ └── compose.yaml ├── envs ├── .env.development.sample ├── .env.production.sample └── .env.staging.sample ├── next.config.ts ├── package.json ├── pnpm-lock.yaml ├── postcss.config.mjs ├── public ├── file.svg ├── globe.svg ├── next.svg ├── shutter-click.wav ├── vercel.svg └── window.svg ├── src ├── app │ ├── (public) │ │ ├── contact │ │ │ └── page.tsx │ │ └── page.tsx │ ├── README.md │ ├── favicon.ico │ ├── fonts │ │ ├── GeistMonoVF.woff │ │ └── GeistVF.woff │ ├── guard │ │ └── ex │ │ │ └── page.tsx │ └── layout.tsx ├── configs │ ├── README.md │ └── environment.ts ├── interfaces │ ├── components │ │ ├── README.md │ │ ├── form-input.tsx │ │ └── ui │ │ │ ├── button.tsx │ │ │ ├── calendar.tsx │ │ │ ├── checkbox.tsx │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ ├── popover.tsx │ │ │ └── radio-group.tsx │ ├── layouts │ │ ├── README.md │ │ ├── main_layout.tsx │ │ └── public_layout.tsx │ └── screens │ │ ├── README.md │ │ ├── screen_private │ │ └── main.tsx │ │ └── screen_public │ │ └── main.tsx ├── lib │ └── utils.ts ├── middleware.ts ├── modules │ ├── README.md │ ├── cookies.ts │ └── providers │ │ ├── redux_provider.tsx │ │ └── theme_provider.tsx ├── services │ ├── README.md │ └── api │ │ └── main │ │ ├── call.ts │ │ ├── endpoint.ts │ │ └── interceptor.ts ├── shared │ ├── README.md │ ├── path.ts │ └── toolkit │ │ ├── hooks.ts │ │ ├── slice │ │ └── authorized_slice.ts │ │ └── store.ts ├── styles │ ├── README.md │ ├── color.ts │ └── globals.css ├── types │ ├── README.md │ ├── response.ts │ └── screen_public.types.ts └── utils │ ├── README.md │ ├── use_icon.tsx │ ├── use_router.tsx │ └── use_theme.tsx ├── tailwind.config.ts └── tsconfig.json /.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | .dockerignore 3 | node_modules 4 | npm-debug.log 5 | README.md 6 | .next 7 | docker 8 | .git 9 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next/typescript"] 3 | } 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: ['itpohgero.com', 'mataraman.dev']# Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /.github/workflows/cicd.yaml: -------------------------------------------------------------------------------- 1 | name: CI/CD Pipeline 2 | 3 | on: 4 | push: 5 | branches: 6 | - development 7 | - staging 8 | - production 9 | 10 | jobs: 11 | # Job untuk Development 12 | build-development: 13 | name: Build and Deploy Development 14 | runs-on: ubuntu-latest 15 | if: github.ref_name == 'development' 16 | steps: 17 | # https://github.com/marketplace/actions/checkout 18 | - name: Checkout Code 19 | uses: actions/checkout@v4.2.2 20 | 21 | # https://github.com/marketplace/actions/webfactory-ssh-agent 22 | - name: Setup SSH 23 | uses: webfactory/ssh-agent@v0.9.0 24 | with: 25 | ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} 26 | 27 | - name: Add Known Hosts 28 | run: | 29 | mkdir -p ~/.ssh 30 | ssh-keyscan -H ${{ secrets.DROPLET_IP }} >> ~/.ssh/known_hosts 31 | 32 | - name: Build and Deploy to Development Droplet 33 | run: | 34 | ssh ${{ secrets.SERVER_USERNAME }}@${{ secrets.DROPLET_IP }} << 'EOF' 35 | set -e 36 | cd ${{ secrets.PATH_APP }} 37 | git pull origin development 38 | make build-development 39 | make start-development 40 | EOF 41 | 42 | # Job untuk Staging 43 | build-staging: 44 | name: Build and Deploy Staging 45 | runs-on: ubuntu-latest 46 | needs: build-development 47 | if: github.ref_name == 'staging' 48 | steps: 49 | # https://github.com/marketplace/actions/checkout 50 | - name: Checkout Code 51 | uses: actions/checkout@v4.2.2 52 | 53 | # https://github.com/marketplace/actions/webfactory-ssh-agent 54 | - name: Setup SSH 55 | uses: webfactory/ssh-agent@v0.9.0 56 | with: 57 | ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} 58 | 59 | - name: Add Known Hosts 60 | run: | 61 | mkdir -p ~/.ssh 62 | ssh-keyscan -H ${{ secrets.DROPLET_IP }} >> ~/.ssh/known_hosts 63 | 64 | - name: Build and Deploy to Staging Droplet 65 | run: | 66 | ssh ${{ secrets.SERVER_USERNAME }}@${{ secrets.DROPLET_IP }} << 'EOF' 67 | set -e 68 | cd ${{ secrets.PATH_APP }} 69 | git pull origin staging 70 | make build-staging 71 | make start-staging 72 | EOF 73 | 74 | # Job untuk Production 75 | build-production: 76 | name: Build and Deploy Production 77 | runs-on: ubuntu-latest 78 | needs: build-staging 79 | if: github.ref_name == 'production' 80 | steps: 81 | # https://github.com/marketplace/actions/checkout 82 | - name: Checkout Code 83 | uses: actions/checkout@v4.2.2 84 | 85 | # https://github.com/marketplace/actions/webfactory-ssh-agent 86 | - name: Setup SSH 87 | uses: webfactory/ssh-agent@v0.9.0 88 | with: 89 | ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} 90 | 91 | - name: Add Known Hosts 92 | run: | 93 | mkdir -p ~/.ssh 94 | ssh-keyscan -H ${{ secrets.DROPLET_IP }} >> ~/.ssh/known_hosts 95 | 96 | - name: Build and Deploy to Production Droplet 97 | run: | 98 | ssh ${{ secrets.SERVER_USERNAME }}@${{ secrets.DROPLET_IP }} << 'EOF' 99 | set -e 100 | cd ${{ secrets.PATH_APP }} 101 | git pull origin production 102 | make build-production 103 | make start-production 104 | EOF 105 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL Advanced" 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | pull_request: 18 | branches: [ "main" ] 19 | schedule: 20 | - cron: '26 1 * * 1' 21 | 22 | jobs: 23 | analyze: 24 | name: Analyze (${{ matrix.language }}) 25 | # Runner size impacts CodeQL analysis time. To learn more, please see: 26 | # - https://gh.io/recommended-hardware-resources-for-running-codeql 27 | # - https://gh.io/supported-runners-and-hardware-resources 28 | # - https://gh.io/using-larger-runners (GitHub.com only) 29 | # Consider using larger runners or machines with greater resources for possible analysis time improvements. 30 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} 31 | permissions: 32 | # required for all workflows 33 | security-events: write 34 | 35 | # required to fetch internal or private CodeQL packs 36 | packages: read 37 | 38 | # only required for workflows in private repositories 39 | actions: read 40 | contents: read 41 | 42 | strategy: 43 | fail-fast: false 44 | matrix: 45 | include: 46 | - language: javascript-typescript 47 | build-mode: none 48 | # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' 49 | # Use `c-cpp` to analyze code written in C, C++ or both 50 | # Use 'java-kotlin' to analyze code written in Java, Kotlin or both 51 | # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both 52 | # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, 53 | # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. 54 | # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how 55 | # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages 56 | steps: 57 | - name: Checkout repository 58 | uses: actions/checkout@v4 59 | 60 | # Initializes the CodeQL tools for scanning. 61 | - name: Initialize CodeQL 62 | uses: github/codeql-action/init@v3 63 | with: 64 | languages: ${{ matrix.language }} 65 | build-mode: ${{ matrix.build-mode }} 66 | # If you wish to specify custom queries, you can do so here or in a config file. 67 | # By default, queries listed here will override any specified in a config file. 68 | # Prefix the list here with "+" to use these queries and those in the config file. 69 | 70 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 71 | # queries: security-extended,security-and-quality 72 | 73 | # If the analyze step fails for one of the languages you are analyzing with 74 | # "We were unable to automatically build your code", modify the matrix above 75 | # to set the build mode to "manual" for that language. Then modify this step 76 | # to build your code. 77 | # ℹ️ Command-line programs to run using the OS shell. 78 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 79 | - if: matrix.build-mode == 'manual' 80 | shell: bash 81 | run: | 82 | echo 'If you are using a "manual" build mode for one or more of the' \ 83 | 'languages you are analyzing, replace this with the commands to build' \ 84 | 'your code, for example:' 85 | echo ' make bootstrap' 86 | echo ' make release' 87 | exit 1 88 | 89 | - name: Perform CodeQL Analysis 90 | uses: github/codeql-action/analyze@v3 91 | with: 92 | category: "/language:${{matrix.language}}" 93 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | 32 | # env files (can opt-in for committing if needed) 33 | # .env* 34 | 35 | # vercel 36 | .vercel 37 | 38 | # typescript 39 | *.tsbuildinfo 40 | next-env.d.ts 41 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | pnpm dlx commitlint --edit $1 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | pnpm run lint:biome 2 | pnpm run check:biome 3 | pnpm run format:biome -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build-development 2 | build-development: ## Build the development docker image. 3 | docker compose -f docker/development/compose.yaml build 4 | 5 | .PHONY: start-development 6 | start-development: ## Start the development docker container. 7 | docker compose -f docker/development/compose.yaml up -d 8 | 9 | .PHONY: stop-development 10 | stop-development: ## Stop the development docker container. 11 | docker compose -f docker/development/compose.yaml down 12 | 13 | .PHONY: build-staging 14 | build-staging: ## Build the staging docker image. 15 | docker compose -f docker/staging/compose.yaml build 16 | 17 | .PHONY: start-staging 18 | start-staging: ## Start the staging docker container. 19 | docker compose -f docker/staging/compose.yaml up -d 20 | 21 | .PHONY: stop-staging 22 | stop-staging: ## Stop the staging docker container. 23 | docker compose -f docker/staging/compose.yaml down 24 | 25 | .PHONY: build-production 26 | build-production: ## Build the production docker image. 27 | docker compose -f docker/production/compose.yaml build 28 | 29 | .PHONY: start-production 30 | start-production: ## Start the production docker container. 31 | docker compose -f docker/production/compose.yaml up -d 32 | 33 | .PHONY: stop-production 34 | stop-production: ## Stop the production docker container. 35 | docker compose -f docker/production/compose.yaml down 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Next Architecture Project Overview 2 | 3 | **Created with ❤️ by [Wahyu Agus Arifin](https://github.com/itpohgero)** 4 | 5 | This project leverages a modern stack to enhance development efficiency, code consistency, and performance. It is built using powerful libraries and tools for optimal experience in managing state, handling forms, and formatting code. 6 | 7 | --- 8 | 9 | ### Tech Stack & Tools Included 10 | 11 | 1. **Biome** 12 | A blazing-fast formatter for JavaScript, TypeScript, JSX, TSX, JSON, CSS, and GraphQL files. It provides 97% compatibility with Prettier, offering consistent formatting while saving CI and developer time. 13 | 14 | 2. **Redux Toolkit** 15 | A powerful library for state management that simplifies setup and helps manage complex state across the app. Redux Toolkit provides optimized patterns for managing state in a scalable and maintainable way. 16 | 17 | 3. **React Hook Form** 18 | Lightweight and performant library for form validation in React. It reduces re-renders and provides simple API for handling complex form use cases. 19 | 20 | 4. **Shadcn UI** 21 | A component library for building beautiful and accessible user interfaces in React. It provides a set of pre-built components that are easy to use and customize. 22 | 23 | 5. **Day.js** 24 | A small and fast library for handling dates and times in JavaScript. It offers an immutable API similar to Moment.js, with support for plugins and internationalization. 25 | 26 | 6. **Jose** 27 | A JavaScript library for cryptographic operations, useful for JSON Web Tokens (JWT) and handling cryptography securely in the app. 28 | 29 | 7. **Lodash** 30 | A utility library that provides helpful functions for common programming tasks, improving readability and efficiency. 31 | 32 | 8. **Iconify** 33 | A comprehensive icon framework that offers thousands of icons in a single package, allowing for easy use and customization. 34 | 35 | 9. **Axios** 36 | A popular library for making HTTP requests, providing promise-based methods for handling API requests and responses. 37 | 38 | 10. **Next Themes** 39 | A library for handling dark mode and light mode in Next.js applications, making it easy to toggle between themes. 40 | 41 | 11. **Next.js Toploader** 42 | A library for loading content in Next.js applications, providing a loading indicator while content is being fetched. 43 | 44 | 12. **dotenv-cli** 45 | A CLI tool for loading environment variables from .env files. (pnpm add -d dotenv-cli) 46 | 47 | 13. **obfuscated** 48 | The obfuscated command is used to obfuscate the static files in the Next.js app. It uses the JavaScript Obfuscator library to obfuscate the code and reduce the size of the files. 49 | 14. **Husky** 50 | A package manager for Git hooks, allowing you to configure and run scripts during various Git lifecycle events. 51 | 52 | 15. **Commitlint** 53 | A package for linting commit messages, ensuring that they follow a specific format and conventions. 54 | 55 | 16. **Docker Multi-Stage Build and Environment** 56 | A project that uses Docker for building and deploying multiple environments for development, staging, and production. 57 | 58 | 17. **Makefile** 59 | A project that uses Makefile for building and deploying multiple environments for development, staging, and production. 60 | 61 | 18. **File Setup (Auto and Manual)** 62 | A project that sets up SSH keys for deployment. 63 | 64 | 19. **Tailwind CSS** 65 | A CSS framework that provides a set of utility classes for building responsive and mobile-first websites. 66 | 67 | --- 68 | 69 | ### Getting Started 70 | 71 | To run this project locally: 72 | 73 | 1. Clone this repository: ```git clone ``` 74 | 2. Make file in folder envs (.env.development, .env.production, .env.staging) 75 | 2. Install dependencies with pnpm: ```pnpm install``` 76 | 3. Run the development server: ```pnpm run dev``` 77 | 78 | 79 | ## Using Docker and Makefile 80 | 81 | ### Development environment - for doing testing 82 | 83 | ``` 84 | make build-development 85 | make start-development 86 | ``` 87 | 88 | Open http://localhost:3001 89 | 90 | ### Staging environment - for doing UAT testing 91 | 92 | ``` 93 | make build-staging 94 | make start-staging 95 | ``` 96 | 97 | Open http://localhost:3002 98 | 99 | ### Production environment - for users 100 | 101 | ``` 102 | make build-production 103 | make start-production 104 | ``` 105 | 106 | Open http://localhost:3003 107 | 108 | 109 | Credit to [Wahyu Agus Arifin](https://github.com/itpohgero) 110 | - itpohgero@gmail.com 111 | - ig : @wahyuagusarifin 112 | - linkedin : [wahyuagusarifin](https://id.linkedin.com/in/wahyu-agus-arifin-8a6992215) -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", 3 | "vcs": { 4 | "enabled": false, 5 | "clientKind": "git", 6 | "useIgnoreFile": false 7 | }, 8 | "files": { 9 | "ignoreUnknown": false, 10 | "ignore": ["node_modules", ".next"] 11 | }, 12 | "formatter": { 13 | "enabled": true, 14 | "indentStyle": "tab" 15 | }, 16 | "organizeImports": { 17 | "enabled": true 18 | }, 19 | "linter": { 20 | "enabled": true, 21 | "rules": { 22 | "recommended": true 23 | } 24 | }, 25 | "javascript": { 26 | "formatter": { 27 | "quoteStyle": "double" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /cmd/setup-auto.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # ======================================== 3 | # Script Setup SSH untuk Deployment 4 | # Cara Penggunaan: 5 | # chmod +x setup-auto.sh 6 | # ./setup-auto.sh 7 | # ======================================== 8 | 9 | # Menghentikan script jika terjadi error 10 | set -e 11 | 12 | # 1. Deteksi USERNAME otomatis 13 | USER=$(whoami) 14 | 15 | # 2. Deteksi IP Publik otomatis (gunakan layanan eksternal) 16 | SERVER_IP=$(curl -s ifconfig.me) 17 | 18 | # 3. Pastikan email ada sebagai inputan 19 | if [ -z "$EMAIL" ]; then 20 | read -p "Masukkan email untuk SSH key: " EMAIL 21 | fi 22 | 23 | # 4. Cek apakah kunci SSH sudah ada 24 | if [ ! -f "$HOME/.ssh/id_ed25519" ]; then 25 | echo "Membuat kunci SSH baru..." 26 | 27 | # Membuat kunci SSH baru tanpa passphrase 28 | ssh-keygen -t ed25519 -C "$EMAIL" -f "$HOME/.ssh/id_ed25519" -q -N "" 29 | echo "Kunci SSH berhasil dibuat di $HOME/.ssh/id_ed25519" 30 | else 31 | echo "Kunci SSH sudah ada di $HOME/.ssh/id_ed25519" 32 | fi 33 | 34 | # 5. Pastikan direktori .ssh ada di server remote dan atur izin 35 | echo "Menyiapkan direktori .ssh di server remote..." 36 | ssh -o StrictHostKeyChecking=no "$USER@$SERVER_IP" <<'EOF' 37 | mkdir -p ~/.ssh 38 | chmod 700 ~/.ssh 39 | EOF 40 | 41 | # 6. Menambahkan kunci publik ke authorized_keys di server remote 42 | echo "Menambahkan kunci publik ke server remote..." 43 | ssh-copy-id -i "$HOME/.ssh/id_ed25519.pub" "$USER@$SERVER_IP" || { 44 | echo "Gagal menyalin kunci. Periksa apakah SSH dapat diakses atau kunci sudah ada." 45 | exit 1 46 | } 47 | 48 | # 7. Verifikasi koneksi SSH tanpa password 49 | echo "Verifikasi koneksi SSH tanpa password..." 50 | if ssh -o BatchMode=yes "$USER@$SERVER_IP" "echo 'Koneksi SSH berhasil tanpa password.'"; then 51 | echo "Koneksi SSH berhasil tanpa password." 52 | else 53 | echo "Verifikasi koneksi SSH gagal. Periksa kembali konfigurasi." 54 | exit 1 55 | fi 56 | 57 | # 8. Menampilkan kunci privat untuk disalin ke GitHub Secrets 58 | echo "===== Salin kunci berikut ke GitHub Secrets (SSH_PRIVATE_KEY) =====" 59 | cat "$HOME/.ssh/id_ed25519" 60 | echo "===================================================================" 61 | 62 | # 9. Menampilkan IP server untuk GitHub Secrets (DROPLET_IP) 63 | echo "===== Salin IP berikut ke GitHub Secrets (DROPLET_IP) =====" 64 | echo "$SERVER_IP" 65 | echo "==========================================================" 66 | 67 | # 10. Menampilkan username server untuk GitHub Secrets (SERVER_USERNAME) 68 | echo "===== Salin username berikut ke GitHub Secrets (SERVER_USERNAME) =====" 69 | echo "$USER" 70 | echo "=====================================================================" 71 | -------------------------------------------------------------------------------- /cmd/setup-manual.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # ======================================== 3 | # Script Setup SSH untuk Deployment 4 | # Cara Penggunaan: 5 | # chmod +x setup-manual.sh 6 | # SERVER_IP="123.45.6.789" USER="your_username" EMAIL="your_email@example.com" ./setup-manual.sh 7 | # ======================================== 8 | 9 | # Menghentikan script jika terjadi error 10 | set -e 11 | 12 | # 1. Cek apakah kunci SSH sudah ada 13 | if [ ! -f "$HOME/.ssh/id_ed25519" ]; then 14 | echo "Membuat kunci SSH baru..." 15 | 16 | # Membuat kunci SSH baru tanpa passphrase 17 | ssh-keygen -t ed25519 -C "$EMAIL" -f "$HOME/.ssh/id_ed25519" -q -N "" 18 | echo "Kunci SSH berhasil dibuat di $HOME/.ssh/id_ed25519" 19 | else 20 | echo "Kunci SSH sudah ada di $HOME/.ssh/id_ed25519" 21 | fi 22 | 23 | # 2. Pastikan direktori .ssh ada di server remote dan atur izin 24 | echo "Menyiapkan direktori .ssh di server remote..." 25 | ssh -o StrictHostKeyChecking=no "$USER@$SERVER_IP" <<'EOF' 26 | mkdir -p ~/.ssh 27 | chmod 700 ~/.ssh 28 | EOF 29 | 30 | # 3. Menambahkan kunci publik ke authorized_keys di server remote 31 | echo "Menambahkan kunci publik ke server remote..." 32 | ssh-copy-id -i "$HOME/.ssh/id_ed25519.pub" "$USER@$SERVER_IP" || { 33 | echo "Gagal menyalin kunci. Periksa apakah SSH dapat diakses atau apakah kunci sudah ada." 34 | exit 1 35 | } 36 | 37 | # 4. Verifikasi koneksi SSH tanpa password 38 | echo "Verifikasi koneksi SSH tanpa password..." 39 | if ssh -o BatchMode=yes "$USER@$SERVER_IP" "echo 'Koneksi SSH berhasil tanpa password.'"; then 40 | echo "Koneksi SSH berhasil tanpa password." 41 | else 42 | echo "Verifikasi koneksi SSH gagal. Periksa kembali konfigurasi." 43 | exit 1 44 | fi 45 | 46 | # 5. Menampilkan kunci privat untuk disalin ke GitHub Secrets 47 | echo "===== Salin kunci berikut ke GitHub Secrets (SSH_PRIVATE_KEY) =====" 48 | cat "$HOME/.ssh/id_ed25519" 49 | echo "===================================================================" 50 | 51 | # 6. Menampilkan IP server untuk GitHub Secrets (DROPLET_IP) 52 | echo "===== Salin IP berikut ke GitHub Secrets (DROPLET_IP) =====" 53 | echo "$SERVER_IP" 54 | echo "==========================================================" 55 | 56 | # 7. Menampilkan username server untuk GitHub Secrets (SERVER_USERNAME) 57 | echo "===== Salin username berikut ke GitHub Secrets (SERVER_USERNAME) =====" 58 | echo "$USER" 59 | echo "=====================================================================" 60 | -------------------------------------------------------------------------------- /commitlint.config.cts: -------------------------------------------------------------------------------- 1 | export default { 2 | extends: ["@commitlint/config-conventional"], 3 | rules: { 4 | "type-enum": [ 5 | 2, 6 | "always", 7 | ["feat", "fix", "docs", "style", "refactor", "test", "chore"], 8 | ], 9 | "subject-empty": [2, "never"], 10 | "type-empty": [2, "never"], 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/styles/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/interfaces/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/interfaces/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } 22 | -------------------------------------------------------------------------------- /docker/development/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker.io/docker/dockerfile:1 2 | 3 | FROM node:18-alpine AS base 4 | 5 | # 1. Install dependencies only when needed 6 | FROM base AS deps 7 | # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. 8 | RUN apk add --no-cache libc6-compat 9 | 10 | WORKDIR /app 11 | 12 | # Install dependencies based on the preferred package manager 13 | COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* .npmrc* ./ 14 | RUN \ 15 | if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ 16 | elif [ -f package-lock.json ]; then npm ci; \ 17 | elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i; \ 18 | else echo "Lockfile not found." && exit 1; \ 19 | fi 20 | 21 | # 2. Rebuild the source code only when needed 22 | FROM base AS builder 23 | WORKDIR /app 24 | COPY --from=deps /app/node_modules ./node_modules 25 | COPY . . 26 | # This will do the trick, use the corresponding env file for each environment. 27 | COPY /envs/.env.development.sample .env.production 28 | RUN npm run build 29 | 30 | # 3. Production image, copy all the files and run next 31 | FROM base AS runner 32 | WORKDIR /app 33 | 34 | ENV NODE_ENV=production 35 | 36 | RUN addgroup -g 1001 -S nodejs 37 | RUN adduser -S nextjs -u 1001 38 | 39 | COPY --from=builder /app/public ./public 40 | 41 | # Automatically leverage output traces to reduce image size 42 | # https://nextjs.org/docs/advanced-features/output-file-tracing 43 | COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ 44 | COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static 45 | 46 | 47 | USER nextjs 48 | 49 | EXPOSE 3000 50 | 51 | ENV PORT=3000 52 | 53 | CMD HOSTNAME="0.0.0.0" node server.js 54 | -------------------------------------------------------------------------------- /docker/development/compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | next-architecture-development: 3 | build: 4 | context: ../../ 5 | dockerfile: docker/development/Dockerfile 6 | image: next-architecture-development 7 | ports: 8 | - "3001:3000" 9 | -------------------------------------------------------------------------------- /docker/production/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker.io/docker/dockerfile:1 2 | 3 | FROM node:18-alpine AS base 4 | 5 | # 1. Install dependencies only when needed 6 | FROM base AS deps 7 | # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. 8 | RUN apk add --no-cache libc6-compat 9 | 10 | WORKDIR /app 11 | 12 | # Install dependencies based on the preferred package manager 13 | COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* .npmrc* ./ 14 | RUN \ 15 | if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ 16 | elif [ -f package-lock.json ]; then npm ci; \ 17 | elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i; \ 18 | else echo "Lockfile not found." && exit 1; \ 19 | fi 20 | 21 | 22 | # 2. Rebuild the source code only when needed 23 | FROM base AS builder 24 | WORKDIR /app 25 | COPY --from=deps /app/node_modules ./node_modules 26 | COPY . . 27 | # This will do the trick, use the corresponding env file for each environment. 28 | COPY /envs/.env.production.sample .env.production 29 | RUN npm run build 30 | 31 | # 3. Production image, copy all the files and run next 32 | FROM base AS runner 33 | WORKDIR /app 34 | 35 | ENV NODE_ENV=production 36 | 37 | RUN addgroup -g 1001 -S nodejs 38 | RUN adduser -S nextjs -u 1001 39 | 40 | COPY --from=builder /app/public ./public 41 | 42 | # Automatically leverage output traces to reduce image size 43 | # https://nextjs.org/docs/advanced-features/output-file-tracing 44 | COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ 45 | COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static 46 | 47 | 48 | USER nextjs 49 | 50 | EXPOSE 3000 51 | 52 | ENV PORT=3000 53 | 54 | CMD HOSTNAME="0.0.0.0" node server.js 55 | -------------------------------------------------------------------------------- /docker/production/compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | next-architecture-production: 3 | build: 4 | context: ../../ 5 | dockerfile: docker/production/Dockerfile 6 | image: next-architecture-production 7 | ports: 8 | - "3003:3000" 9 | -------------------------------------------------------------------------------- /docker/staging/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker.io/docker/dockerfile:1 2 | 3 | FROM node:18-alpine AS base 4 | 5 | # 1. Install dependencies only when needed 6 | FROM base AS deps 7 | # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. 8 | RUN apk add --no-cache libc6-compat 9 | 10 | WORKDIR /app 11 | 12 | # Install dependencies based on the preferred package manager 13 | COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* .npmrc* ./ 14 | RUN \ 15 | if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ 16 | elif [ -f package-lock.json ]; then npm ci; \ 17 | elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i; \ 18 | else echo "Lockfile not found." && exit 1; \ 19 | fi 20 | 21 | 22 | # 2. Rebuild the source code only when needed 23 | FROM base AS builder 24 | WORKDIR /app 25 | COPY --from=deps /app/node_modules ./node_modules 26 | COPY . . 27 | # This will do the trick, use the corresponding env file for each environment. 28 | COPY /envs/.env.staging.sample .env.production 29 | RUN npm run build 30 | 31 | # 3. Production image, copy all the files and run next 32 | FROM base AS runner 33 | WORKDIR /app 34 | 35 | ENV NODE_ENV=production 36 | 37 | RUN addgroup -g 1001 -S nodejs 38 | RUN adduser -S nextjs -u 1001 39 | 40 | COPY --from=builder /app/public ./public 41 | 42 | # Automatically leverage output traces to reduce image size 43 | # https://nextjs.org/docs/advanced-features/output-file-tracing 44 | COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ 45 | COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static 46 | 47 | 48 | USER nextjs 49 | 50 | EXPOSE 3000 51 | 52 | ENV PORT=3000 53 | 54 | CMD HOSTNAME="0.0.0.0" node server.js 55 | -------------------------------------------------------------------------------- /docker/staging/compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | next-architecture-staging: 3 | build: 4 | context: ../../ 5 | dockerfile: docker/staging/Dockerfile 6 | image: next-architecture-staging 7 | ports: 8 | - "3002:3000" 9 | -------------------------------------------------------------------------------- /envs/.env.development.sample: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_API_URL=https://api-development.com 2 | NEXT_PUBLIC_MODE=development -------------------------------------------------------------------------------- /envs/.env.production.sample: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_API_URL=https://api-production.com 2 | NEXT_PUBLIC_MODE=production -------------------------------------------------------------------------------- /envs/.env.staging.sample: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_API_URL=https://api-staging.com 2 | NEXT_PUBLIC_MODE=staging -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | output: "standalone", 5 | /* config options here */ 6 | }; 7 | 8 | export default nextConfig; 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-architecture", 3 | "version": "0.1.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "next dev", 8 | "start": "next start", 9 | "build": "next build", 10 | "lint": "next lint", 11 | "obfuscate:static": "javascript-obfuscator .next/static --output .next/static --compact true --self-defending true --target 'browser'", 12 | "lint:biome": "pnpm exec biome lint --write .", 13 | "check:biome": "pnpm exec biome check --write .", 14 | "format:biome": "pnpm exec biome format --write .", 15 | "prepare": "husky" 16 | }, 17 | "dependencies": { 18 | "@iconify/react": "^5.0.2", 19 | "@radix-ui/react-checkbox": "^1.1.2", 20 | "@radix-ui/react-label": "^2.1.0", 21 | "@radix-ui/react-popover": "^1.1.2", 22 | "@radix-ui/react-radio-group": "^1.2.1", 23 | "@radix-ui/react-slot": "^1.1.0", 24 | "@reduxjs/toolkit": "^2.4.0", 25 | "@types/lodash": "^4.17.13", 26 | "axios": "^1.7.8", 27 | "class-variance-authority": "^0.7.1", 28 | "clsx": "^2.1.1", 29 | "date-fns": "^3.6.0", 30 | "dayjs": "^1.11.13", 31 | "jose": "^5.9.6", 32 | "lodash": "^4.17.21", 33 | "lucide-react": "^0.456.0", 34 | "next": "15.0.3", 35 | "next-themes": "^0.4.3", 36 | "nextjs-toploader": "^3.7.15", 37 | "react": "18.3.1", 38 | "react-day-picker": "8.10.1", 39 | "react-dom": "18.3.1", 40 | "react-hook-form": "^7.53.2", 41 | "react-if": "^4.1.5", 42 | "react-redux": "^9.1.2", 43 | "tailwind-merge": "^2.5.5", 44 | "tailwindcss-animate": "^1.0.7" 45 | }, 46 | "devDependencies": { 47 | "@biomejs/biome": "1.9.4", 48 | "@commitlint/cli": "^19.6.0", 49 | "@commitlint/config-conventional": "^19.6.0", 50 | "@types/node": "^20.17.9", 51 | "@types/react": "^18.3.12", 52 | "@types/react-dom": "^18.3.1", 53 | "dotenv-cli": "^7.4.4", 54 | "eslint": "^9.16.0", 55 | "eslint-config-next": "15.0.3", 56 | "husky": "^9.1.7", 57 | "javascript-obfuscator": "^4.1.1", 58 | "postcss": "^8.4.49", 59 | "tailwindcss": "^3.4.15", 60 | "typescript": "^5.7.2" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/shutter-click.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItPohgero/next-architecture/4404541eff61ab9316d9fad245a83aee06bba37e/public/shutter-click.wav -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/(public)/contact/page.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Page = () => { 4 | return
Page
; 5 | }; 6 | 7 | export default Page; 8 | -------------------------------------------------------------------------------- /src/app/(public)/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import ScreenPublic from "@/interfaces/screens/screen_public/main"; 3 | import React from "react"; 4 | 5 | const Page = () => { 6 | return ; 7 | }; 8 | 9 | export default Page; 10 | -------------------------------------------------------------------------------- /src/app/README.md: -------------------------------------------------------------------------------- 1 | # app 2 | 3 | Folder `app` dalam proyek ini berfungsi sebagai tempat penyimpanan konfigurasi dan pengaturan routing aplikasi, termasuk penentuan jalur dan pengelolaan tampilan untuk setiap halaman. 4 | 5 | Author: Wahyu Agus Arifin 6 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItPohgero/next-architecture/4404541eff61ab9316d9fad245a83aee06bba37e/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItPohgero/next-architecture/4404541eff61ab9316d9fad245a83aee06bba37e/src/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /src/app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItPohgero/next-architecture/4404541eff61ab9316d9fad245a83aee06bba37e/src/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /src/app/guard/ex/page.tsx: -------------------------------------------------------------------------------- 1 | import ScreenPrivate from "@/interfaces/screens/screen_private/main"; 2 | import React from "react"; 3 | 4 | const Page = () => ; 5 | 6 | export default Page; 7 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import localFont from "next/font/local"; 3 | import "@/styles/globals.css"; 4 | import MainLayout from "@/interfaces/layouts/main_layout"; 5 | import { ProviderReduxToolkit } from "@/modules/providers/redux_provider"; 6 | import ProviderTheme from "@/modules/providers/theme_provider"; 7 | import { Color } from "@/styles/color"; 8 | import NextTopLoader from "nextjs-toploader"; 9 | 10 | const geistSans = localFont({ 11 | src: "./fonts/GeistVF.woff", 12 | variable: "--font-geist-sans", 13 | weight: "100 900", 14 | }); 15 | const geistMono = localFont({ 16 | src: "./fonts/GeistMonoVF.woff", 17 | variable: "--font-geist-mono", 18 | weight: "100 900", 19 | }); 20 | 21 | export const metadata: Metadata = { 22 | title: "Create Next App", 23 | description: "Generated by create next app", 24 | }; 25 | 26 | export default function RootLayout({ 27 | children, 28 | }: Readonly<{ 29 | children: React.ReactNode; 30 | }>) { 31 | return ( 32 | 33 | 36 | 37 | 38 | 39 | 40 |
{children}
41 |
42 |
43 |
44 | 45 | 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /src/configs/README.md: -------------------------------------------------------------------------------- 1 | # configs 2 | 3 | Folder `configs` dalam proyek ini berfungsi sebagai tempat penyimpanan file konfigurasi aplikasi, seperti pengaturan environment, koneksi database, atau konfigurasi layanan eksternal yang digunakan di seluruh aplikasi. 4 | 5 | Author: Wahyu Agus Arifin 6 | -------------------------------------------------------------------------------- /src/configs/environment.ts: -------------------------------------------------------------------------------- 1 | export const ENV = { 2 | MODE: process.env.NEXT_PUBLIC_MODE, 3 | TOKEN_KEY: "token", 4 | JWT_SCREET: "screet_jwt", 5 | URI: { 6 | BASE_URL: "https://dummyjson.com/", 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /src/interfaces/components/README.md: -------------------------------------------------------------------------------- 1 | # components 2 | 3 | Folder `components` dalam proyek ini berfungsi sebagai tempat penyimpanan komponen-komponen UI kecil yang digunakan untuk membangun tampilan aplikasi, seperti tombol, input, dan form, yang dapat digunakan kembali di seluruh aplikasi. 4 | 5 | Author: Wahyu Agus Arifin 6 | -------------------------------------------------------------------------------- /src/interfaces/components/form-input.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | import { format } from "date-fns"; 3 | import { debounce } from "lodash"; 4 | import { CalendarIcon } from "lucide-react"; 5 | import React, { type ReactNode, useEffect, useMemo, useState } from "react"; 6 | import { 7 | type Control, 8 | Controller, 9 | type ControllerRenderProps, 10 | type FieldErrors, 11 | type FieldPathValue, 12 | type FieldValues, 13 | type Path, 14 | type RegisterOptions, 15 | } from "react-hook-form"; 16 | import { Button } from "./ui/button"; 17 | import { Calendar } from "./ui/calendar"; 18 | import { Checkbox } from "./ui/checkbox"; 19 | import { Input } from "./ui/input"; 20 | import { Label } from "./ui/label"; 21 | import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"; 22 | import { RadioGroup, RadioGroupItem } from "./ui/radio-group"; 23 | 24 | export type OptionType = { label: string; value: string | number }; 25 | 26 | type CustomInputProps< 27 | TFieldValues extends FieldValues, 28 | TName extends Path, 29 | > = { 30 | disabled?: boolean; 31 | name: TName; 32 | control: Control; 33 | rules?: Omit< 34 | RegisterOptions, 35 | "valueAsNumber" | "valueAsDate" | "setValueAs" | "disabled" 36 | >; 37 | label?: string; 38 | type: "text" | "password" | "number" | "date" | "checkbox" | "radio"; 39 | options?: OptionType[]; 40 | placeholder?: string; 41 | defaultValue?: FieldPathValue; 42 | errors?: FieldErrors; 43 | manualSearch?: boolean; 44 | onSearch?: (query: string) => void; 45 | callbackSelect?: ({ 46 | label, 47 | value, 48 | }: { label: string; value: string | number }) => void; 49 | maxLength?: number; 50 | prefix?: ReactNode; 51 | suffix?: ReactNode; 52 | }; 53 | 54 | const FormInput = < 55 | TFieldValues extends FieldValues, 56 | TName extends Path, 57 | >({ 58 | disabled, 59 | name, 60 | control, 61 | rules, 62 | label, 63 | type, 64 | options, 65 | placeholder, 66 | defaultValue, 67 | errors, 68 | manualSearch, 69 | onSearch, 70 | maxLength, 71 | }: CustomInputProps) => { 72 | const [searchInput, setData] = useState(""); 73 | 74 | const debouncedOnSearch = useMemo( 75 | () => 76 | debounce((input: string) => { 77 | if (onSearch) onSearch(input.length >= 3 ? input : ""); 78 | }, 300), 79 | [onSearch], 80 | ); 81 | 82 | useEffect(() => { 83 | setData(""); 84 | if (manualSearch) debouncedOnSearch(searchInput); 85 | return () => debouncedOnSearch.cancel(); 86 | }, [searchInput, manualSearch, debouncedOnSearch]); 87 | 88 | const RenderInput = (field: ControllerRenderProps) => { 89 | switch (type) { 90 | case "text": 91 | return ( 92 | field.onChange(e.target.value)} 98 | /> 99 | ); 100 | case "password": 101 | return ( 102 | field.onChange(e.target.value)} 107 | /> 108 | ); 109 | case "number": 110 | return ( 111 | field.onChange(value)} 117 | style={{ width: "100%" }} 118 | /> 119 | ); 120 | case "date": 121 | return ( 122 | 123 | 124 | 138 | 139 | 140 | 146 | 147 | 148 | ); 149 | case "checkbox": 150 | return ( 151 | field.onChange(e.target)} 155 | > 156 | {label} 157 | 158 | ); 159 | case "radio": 160 | return ( 161 | field.onChange(e.target)} 164 | value={field.value} 165 | defaultValue="option-one" 166 | > 167 | {options?.map((option) => ( 168 |
169 | 173 | 174 |
175 | ))} 176 |
177 | ); 178 | default: 179 | return ( 180 | field.onChange(e.target.value)} 184 | /> 185 | ); 186 | } 187 | }; 188 | 189 | return ( 190 |
191 | {type !== "checkbox" && label && ( 192 | 201 | )} 202 |
203 | RenderInput(field)} 209 | /> 210 | {errors?.[name] && ( 211 |

212 | {errors[name]?.message as string} 213 |

214 | )} 215 |
216 | ); 217 | }; 218 | 219 | export default FormInput; 220 | -------------------------------------------------------------------------------- /src/interfaces/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import { Slot } from "@radix-ui/react-slot"; 2 | import { type VariantProps, cva } from "class-variance-authority"; 3 | import * as React from "react"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2", 25 | sm: "h-8 rounded-md px-3 text-xs", 26 | lg: "h-10 rounded-md px-8", 27 | icon: "h-9 w-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | }, 35 | ); 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean; 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button"; 46 | return ( 47 | 52 | ); 53 | }, 54 | ); 55 | Button.displayName = "Button"; 56 | 57 | export { Button, buttonVariants }; 58 | -------------------------------------------------------------------------------- /src/interfaces/components/ui/calendar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ChevronLeft, ChevronRight } from "lucide-react"; 4 | import type * as React from "react"; 5 | import { DayPicker } from "react-day-picker"; 6 | 7 | import { buttonVariants } from "@/interfaces/components/ui/button"; 8 | import { cn } from "@/lib/utils"; 9 | 10 | export type CalendarProps = React.ComponentProps; 11 | 12 | function Calendar({ 13 | className, 14 | classNames, 15 | showOutsideDays = true, 16 | ...props 17 | }: CalendarProps) { 18 | return ( 19 | .day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md" 43 | : "[&:has([aria-selected])]:rounded-md", 44 | ), 45 | day: cn( 46 | buttonVariants({ variant: "ghost" }), 47 | "h-8 w-8 p-0 font-normal aria-selected:opacity-100", 48 | ), 49 | day_range_start: "day-range-start", 50 | day_range_end: "day-range-end", 51 | day_selected: 52 | "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground", 53 | day_today: "bg-accent text-accent-foreground", 54 | day_outside: 55 | "day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground", 56 | day_disabled: "text-muted-foreground opacity-50", 57 | day_range_middle: 58 | "aria-selected:bg-accent aria-selected:text-accent-foreground", 59 | day_hidden: "invisible", 60 | ...classNames, 61 | }} 62 | components={{ 63 | IconLeft: () => , 64 | IconRight: () => , 65 | }} 66 | {...props} 67 | /> 68 | ); 69 | } 70 | Calendar.displayName = "Calendar"; 71 | 72 | export { Calendar }; 73 | -------------------------------------------------------------------------------- /src/interfaces/components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; 4 | import { Check } from "lucide-react"; 5 | import * as React from "react"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | const Checkbox = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, ...props }, ref) => ( 13 | 21 | 24 | 25 | 26 | 27 | )); 28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName; 29 | 30 | export { Checkbox }; 31 | -------------------------------------------------------------------------------- /src/interfaces/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | const Input = React.forwardRef>( 6 | ({ className, type, ...props }, ref) => { 7 | return ( 8 | 17 | ); 18 | }, 19 | ); 20 | Input.displayName = "Input"; 21 | 22 | export { Input }; 23 | -------------------------------------------------------------------------------- /src/interfaces/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as LabelPrimitive from "@radix-ui/react-label"; 4 | import { type VariantProps, cva } from "class-variance-authority"; 5 | import * as React from "react"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", 11 | ); 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )); 24 | Label.displayName = LabelPrimitive.Root.displayName; 25 | 26 | export { Label }; 27 | -------------------------------------------------------------------------------- /src/interfaces/components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as PopoverPrimitive from "@radix-ui/react-popover"; 4 | import * as React from "react"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const Popover = PopoverPrimitive.Root; 9 | 10 | const PopoverTrigger = PopoverPrimitive.Trigger; 11 | 12 | const PopoverAnchor = PopoverPrimitive.Anchor; 13 | 14 | const PopoverContent = React.forwardRef< 15 | React.ElementRef, 16 | React.ComponentPropsWithoutRef 17 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 18 | 19 | 29 | 30 | )); 31 | PopoverContent.displayName = PopoverPrimitive.Content.displayName; 32 | 33 | export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }; 34 | -------------------------------------------------------------------------------- /src/interfaces/components/ui/radio-group.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"; 4 | import { Circle } from "lucide-react"; 5 | import * as React from "react"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | const RadioGroup = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, ...props }, ref) => { 13 | return ( 14 | 19 | ); 20 | }); 21 | RadioGroup.displayName = RadioGroupPrimitive.Root.displayName; 22 | 23 | const RadioGroupItem = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => { 27 | return ( 28 | 36 | 37 | 38 | 39 | 40 | ); 41 | }); 42 | RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName; 43 | 44 | export { RadioGroup, RadioGroupItem }; 45 | -------------------------------------------------------------------------------- /src/interfaces/layouts/README.md: -------------------------------------------------------------------------------- 1 | # layouts 2 | 3 | Folder `layouts` dalam proyek ini berfungsi sebagai tempat penyimpanan komponen-komponen tata letak (layout) yang mengatur struktur dasar halaman, seperti header, footer, dan sidebar, yang digunakan kembali di berbagai bagian aplikasi. 4 | 5 | Author: Wahyu Agus Arifin 6 | -------------------------------------------------------------------------------- /src/interfaces/layouts/main_layout.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { 3 | type FC, 4 | Fragment, 5 | type PropsWithChildren, 6 | useEffect, 7 | useState, 8 | } from "react"; 9 | 10 | const MainLayout: FC = ({ children }) => { 11 | const [mounted, setMounted] = useState(false); 12 | 13 | useEffect(() => { 14 | setMounted(true); 15 | }, []); 16 | 17 | if (!mounted) { 18 | return children; 19 | } 20 | 21 | return ( 22 | 23 |
{children}
24 |
25 | ); 26 | }; 27 | 28 | export default MainLayout; 29 | -------------------------------------------------------------------------------- /src/interfaces/layouts/public_layout.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { type FC, Fragment, type PropsWithChildren } from "react"; 3 | 4 | const PublicLayout: FC = ({ children }) => { 5 | return ( 6 | 7 |
{children}
8 |
9 | ); 10 | }; 11 | 12 | export default PublicLayout; 13 | -------------------------------------------------------------------------------- /src/interfaces/screens/README.md: -------------------------------------------------------------------------------- 1 | # screens 2 | 3 | Folder `screens` dalam proyek ini berfungsi sebagai tempat penyimpanan komponen-komponen tampilan utama (screen) yang mewakili halaman-halaman atau tampilan besar dalam aplikasi. 4 | 5 | Author: Wahyu Agus Arifin 6 | -------------------------------------------------------------------------------- /src/interfaces/screens/screen_private/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const ScreenPrivate = () => { 4 | return
ScreenPrivate
; 5 | }; 6 | 7 | export default ScreenPrivate; 8 | -------------------------------------------------------------------------------- /src/interfaces/screens/screen_public/main.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { ENV } from "@/configs/environment"; 3 | import FormInput from "@/interfaces/components/form-input"; 4 | import { Button } from "@/interfaces/components/ui/button"; 5 | import { createCookies } from "@/modules/cookies"; 6 | import { post } from "@/services/api/main/call"; 7 | import { MAIN_ENDPOINT } from "@/services/api/main/endpoint"; 8 | import { PATH } from "@/shared/path"; 9 | import type { Inputs } from "@/types/screen_public.types"; 10 | import UseTheme from "@/utils/use_theme"; 11 | import { useRouter } from "next/navigation"; 12 | import React, { Fragment } from "react"; 13 | import { useForm } from "react-hook-form"; 14 | 15 | const ScreenPublic = () => { 16 | const router = useRouter(); 17 | const { 18 | control, 19 | handleSubmit, 20 | formState: { errors }, 21 | } = useForm({ 22 | defaultValues: { 23 | expiresInMins: 100, 24 | username: "emilys", 25 | password: "emilyspass", 26 | }, 27 | }); 28 | const [loading, setLoading] = React.useState(false); 29 | const onSubmit = handleSubmit(async (data) => { 30 | try { 31 | setLoading(true); 32 | const { Kind, OK, StatusCode } = await post( 33 | MAIN_ENDPOINT.Auth.Login, 34 | data, 35 | ); 36 | console.log({ OK, StatusCode }); 37 | if (!OK) { 38 | throw new Error(); 39 | } 40 | const resp = Kind as { accessToken: string }; 41 | await createCookies({ 42 | name: ENV.TOKEN_KEY, 43 | data: resp.accessToken, 44 | }); 45 | router.push(PATH.PRIVATE); 46 | } catch (error) { 47 | console.log({ error }); 48 | } 49 | }); 50 | return ( 51 | 52 |
53 |
54 |
55 |
env : {ENV.MODE}
56 |
57 | 58 |
59 | 67 | 75 |
76 | 79 | 80 |
81 |
82 | 83 | ); 84 | }; 85 | 86 | export default ScreenPublic; 87 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /src/middleware.ts: -------------------------------------------------------------------------------- 1 | // import { jwtVerify } from "jose"; 2 | import { NextResponse } from "next/server"; 3 | import type { NextRequest } from "next/server"; 4 | import { ENV } from "./configs/environment"; 5 | import { PATH } from "./shared/path"; 6 | 7 | const TOKEN_KEY = ENV.TOKEN_KEY; 8 | // const JWT_SECRET = ENV.JWT_SCREET; 9 | 10 | export async function middleware(request: NextRequest) { 11 | const token = request.cookies.get(TOKEN_KEY); 12 | console.log({ token }); 13 | 14 | // Cek apakah user sudah berada di halaman "not found" untuk menghindari redirect loop 15 | if (request.nextUrl.pathname === PATH.NOT_FOUND) { 16 | return NextResponse.next(); 17 | } 18 | 19 | // Jika token tidak ada, tampilkan halaman "not found" tanpa redirect loop 20 | if (!token) { 21 | return NextResponse.rewrite(new URL(PATH.NOT_FOUND, request.url)); 22 | } 23 | 24 | try { 25 | // Verifikasi token JWT 26 | // const secret = new TextEncoder().encode(JWT_SECRET); 27 | // const { payload } = await jwtVerify(token.value, secret, { 28 | // algorithms: ["HS256"], 29 | // }); 30 | 31 | // Cek apakah token sudah kedaluwarsa 32 | // const currentTime = Math.floor(Date.now() / 1000); 33 | // if (payload.exp && payload.exp < currentTime) { 34 | // request.cookies.delete(TOKEN_KEY); 35 | // return NextResponse.rewrite(new URL(PATH.NOT_FOUND, request.url)); 36 | // } 37 | 38 | // Jika token valid, lanjutkan ke halaman tujuan 39 | return NextResponse.next(); 40 | } catch { 41 | // Hapus token jika verifikasi gagal dan tampilkan halaman "not found" 42 | request.cookies.delete(TOKEN_KEY); 43 | return NextResponse.rewrite(new URL(PATH.NOT_FOUND, request.url)); 44 | } 45 | } 46 | 47 | export const config = { 48 | matcher: ["/guard/:path*", "/contact"], 49 | }; 50 | -------------------------------------------------------------------------------- /src/modules/README.md: -------------------------------------------------------------------------------- 1 | # modules 2 | 3 | Folder `modules` dalam proyek ini berfungsi sebagai tempat penyimpanan bagian-bagian terpisah dari aplikasi yang memiliki fungsionalitas tertentu, seperti modul untuk otentikasi, pengelolaan pengguna, atau fitur lainnya yang terisolasi. 4 | 5 | Author: Wahyu Agus Arifin 6 | -------------------------------------------------------------------------------- /src/modules/cookies.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import type { ENV } from "@/configs/environment"; 4 | import { cookies } from "next/headers"; 5 | 6 | type NameCookies = typeof ENV.TOKEN_KEY | "g_token" | "bg" | "text" | "style"; 7 | interface CookiesProps { 8 | name: NameCookies; 9 | data: string; 10 | } 11 | 12 | export async function createCookies(props: CookiesProps) { 13 | (await cookies()).set(props?.name, props?.data, { secure: true }); 14 | } 15 | 16 | export async function getCookies(name: CookiesProps["name"]) { 17 | return (await cookies()).get(name); 18 | } 19 | 20 | export async function removeCookies(name: CookiesProps["name"]) { 21 | (await cookies()).delete(name); 22 | } 23 | -------------------------------------------------------------------------------- /src/modules/providers/redux_provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { store } from "@/shared/toolkit/store"; 3 | import { Provider } from "react-redux"; 4 | 5 | export function ProviderReduxToolkit({ 6 | children, 7 | }: { children: React.ReactNode }) { 8 | return {children}; 9 | } 10 | -------------------------------------------------------------------------------- /src/modules/providers/theme_provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { ThemeProvider } from "next-themes"; 3 | import React, { 4 | type FC, 5 | type PropsWithChildren, 6 | useEffect, 7 | useState, 8 | } from "react"; 9 | 10 | const ProviderTheme: FC = ({ children }) => { 11 | const [mounted, setMounted] = useState(false); 12 | 13 | useEffect(() => { 14 | setMounted(true); 15 | }, []); 16 | 17 | if (!mounted) { 18 | return children; 19 | } 20 | 21 | return ( 22 | 23 | {children} 24 | 25 | ); 26 | }; 27 | 28 | export default ProviderTheme; 29 | -------------------------------------------------------------------------------- /src/services/README.md: -------------------------------------------------------------------------------- 1 | # services 2 | 3 | Folder `services` dalam proyek ini berfungsi sebagai tempat penyimpanan logika bisnis atau fungsi-fungsi yang berhubungan dengan komunikasi eksternal, seperti API, database, atau layanan pihak ketiga. 4 | 5 | Author: Wahyu Agus Arifin 6 | -------------------------------------------------------------------------------- /src/services/api/main/call.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { ENV } from "@/configs/environment"; 4 | import { removeCookies } from "@/modules/cookies"; 5 | import type { AxiosError, AxiosResponse } from "axios"; 6 | import api from "./interceptor"; 7 | 8 | interface Status { 9 | Code: number; 10 | Message: string; 11 | } 12 | 13 | interface ApiResponse { 14 | Results: T; 15 | Status: Status; 16 | } 17 | 18 | interface ErrorResponse { 19 | Message: string; 20 | } 21 | 22 | interface Res { 23 | OK: boolean; 24 | Kind: T | ApiResponse | ErrorResponse; 25 | StatusCode: number; 26 | } 27 | 28 | export async function get( 29 | url: string, 30 | params?: Record, 31 | ): Promise> { 32 | try { 33 | const response: AxiosResponse = await api.get(url, { params }); 34 | return { 35 | OK: true, 36 | StatusCode: response.status, 37 | Kind: response.data, 38 | }; 39 | } catch (error: unknown) { 40 | return handleAxiosError(error); 41 | } 42 | } 43 | 44 | export async function post( 45 | url: string, 46 | data: Record, 47 | ): Promise> { 48 | try { 49 | const response: AxiosResponse = await api.post(url, data); 50 | return { 51 | OK: true, 52 | StatusCode: response.status, 53 | Kind: response.data, 54 | }; 55 | } catch (error: unknown) { 56 | return handleAxiosError(error); 57 | } 58 | } 59 | 60 | export async function put( 61 | url: string, 62 | data: Record, 63 | ): Promise> { 64 | try { 65 | const response: AxiosResponse = await api.put(url, data); 66 | return { 67 | OK: true, 68 | StatusCode: response.status, 69 | Kind: response.data, 70 | }; 71 | } catch (error: unknown) { 72 | return handleAxiosError(error); 73 | } 74 | } 75 | 76 | export async function patch( 77 | url: string, 78 | data: Record, 79 | ): Promise> { 80 | try { 81 | const response: AxiosResponse = await api.patch(url, data); 82 | return { 83 | OK: true, 84 | StatusCode: response.status, 85 | Kind: response.data, 86 | }; 87 | } catch (error: unknown) { 88 | return handleAxiosError(error); 89 | } 90 | } 91 | 92 | export async function del(url: string): Promise> { 93 | try { 94 | const response: AxiosResponse = await api.delete(url); 95 | return { 96 | OK: true, 97 | StatusCode: response.status, 98 | Kind: response.data, 99 | }; 100 | } catch (error: unknown) { 101 | return handleAxiosError(error); 102 | } 103 | } 104 | 105 | export async function upload( 106 | url: string, 107 | formData: FormData, 108 | ): Promise> { 109 | try { 110 | const response: AxiosResponse = await api.post(url, formData, { 111 | headers: { 112 | "Content-Type": "multipart/form-data", 113 | }, 114 | }); 115 | return { 116 | OK: true, 117 | StatusCode: response.status, 118 | Kind: response.data, 119 | }; 120 | } catch (error: unknown) { 121 | return handleAxiosError(error); 122 | } 123 | } 124 | 125 | function handleAxiosError(error: unknown): Res { 126 | if (error && typeof error === "object" && "isAxiosError" in error) { 127 | const axiosError = error as AxiosError; 128 | const StatusCode = axiosError.response?.status || 500; 129 | 130 | if (axiosError.response) { 131 | if (StatusCode === 401) { 132 | removeCookies(ENV.TOKEN_KEY); 133 | } 134 | return { 135 | OK: false, 136 | StatusCode, 137 | Kind: (axiosError.response.data as ErrorResponse) || { 138 | Message: "Unknown error", 139 | }, 140 | }; 141 | } 142 | } 143 | 144 | return { 145 | OK: false, 146 | StatusCode: 500, 147 | Kind: { Message: error instanceof Error ? error.message : "Unknown error" }, 148 | }; 149 | } 150 | -------------------------------------------------------------------------------- /src/services/api/main/endpoint.ts: -------------------------------------------------------------------------------- 1 | export const MAIN_ENDPOINT = { 2 | Auth: { 3 | Login: "/auth/login", 4 | CurrentUser: "/auth/me", 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /src/services/api/main/interceptor.ts: -------------------------------------------------------------------------------- 1 | import { ENV } from "@/configs/environment"; 2 | import axios from "axios"; 3 | 4 | const baseURL = ENV.URI.BASE_URL; 5 | const isServer = typeof window === "undefined"; 6 | 7 | const api = axios.create({ 8 | baseURL, 9 | headers: { 10 | "Content-Type": "application/json", 11 | }, 12 | }); 13 | api.interceptors.request.use(async (config) => { 14 | if (isServer) { 15 | const { cookies } = await import("next/headers"); 16 | const token = (await cookies()).get(ENV.TOKEN_KEY)?.value; 17 | 18 | if (token) { 19 | config.headers.Authorization = `Bearer ${token}`; 20 | } 21 | } else { 22 | const token = document.cookie.replace( 23 | /(?:(?:^|.*;\s*)token\s*=\s*([^;]*).*$)|^.*$/, 24 | "$1", 25 | ); 26 | if (token) { 27 | config.headers.Authorization = `Bearer ${token}`; 28 | } 29 | } 30 | 31 | return config; 32 | }); 33 | 34 | export default api; 35 | -------------------------------------------------------------------------------- /src/shared/README.md: -------------------------------------------------------------------------------- 1 | # shared 2 | 3 | Folder `shared` dalam proyek ini berfungsi sebagai tempat penyimpanan komponen, utilitas, atau modul yang digunakan secara bersama di berbagai bagian aplikasi untuk memastikan kode yang dapat digunakan kembali dan konsisten. 4 | 5 | Author: Wahyu Agus Arifin 6 | -------------------------------------------------------------------------------- /src/shared/path.ts: -------------------------------------------------------------------------------- 1 | export const PATH = { 2 | NOT_FOUND: "/404", 3 | HOME: "/", 4 | PRIVATE: "/guard/ex", 5 | }; 6 | -------------------------------------------------------------------------------- /src/shared/toolkit/hooks.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type TypedUseSelectorHook, 3 | useDispatch, 4 | useSelector, 5 | } from "react-redux"; 6 | import type { AppDispatch, RootState } from "./store"; 7 | 8 | export const useAppDispatch = () => useDispatch(); 9 | export const useAppSelector: TypedUseSelectorHook = useSelector; 10 | -------------------------------------------------------------------------------- /src/shared/toolkit/slice/authorized_slice.ts: -------------------------------------------------------------------------------- 1 | import { type PayloadAction, createSlice } from "@reduxjs/toolkit"; 2 | 3 | export type AuthorizedState = { 4 | Status: boolean; 5 | }; 6 | const initialState = { 7 | Status: false, 8 | } as AuthorizedState; 9 | 10 | export const authorized = createSlice({ 11 | name: "authorized", 12 | initialState, 13 | reducers: { 14 | reset: () => initialState, 15 | changeAuthorized: ( 16 | state: AuthorizedState, 17 | action: PayloadAction, 18 | ) => { 19 | const { Status } = action.payload; 20 | state.Status = Status !== undefined ? Status : state.Status; 21 | }, 22 | }, 23 | }); 24 | export const { changeAuthorized, reset } = authorized.actions; 25 | export default authorized.reducer; 26 | -------------------------------------------------------------------------------- /src/shared/toolkit/store.ts: -------------------------------------------------------------------------------- 1 | import { configureStore } from "@reduxjs/toolkit"; 2 | import authorized from "./slice/authorized_slice"; 3 | 4 | export const store = configureStore({ 5 | reducer: { 6 | authorized, 7 | }, 8 | devTools: process.env.NODE_ENV !== "production", 9 | }); 10 | 11 | export type RootState = ReturnType; 12 | export type AppDispatch = typeof store.dispatch; 13 | -------------------------------------------------------------------------------- /src/styles/README.md: -------------------------------------------------------------------------------- 1 | # styles 2 | 3 | Folder `styles` dalam proyek ini berfungsi sebagai tempat penyimpanan file-file terkait styling, seperti CSS, SASS, atau file tema, yang digunakan untuk mendefinisikan tampilan aplikasi. 4 | 5 | Author: Wahyu Agus Arifin 6 | -------------------------------------------------------------------------------- /src/styles/color.ts: -------------------------------------------------------------------------------- 1 | export const Color = { 2 | Main: { 3 | 10: "#d3edd1", 4 | 20: "#b5e2b3", 5 | 30: "#90d38d", 6 | 40: "#6bc466", 7 | 50: "#46b640", 8 | Base: "#21a71a", 9 | 60: "#1c8b16", 10 | 70: "#166f11", 11 | 80: "#11540d", 12 | 90: "#0b3809", 13 | 100: "#072105", 14 | }, 15 | Border: { 16 | 10: "#f0f9f0", 17 | 20: "#e6f5e6", 18 | 30: "#daf1d9", 19 | 40: "#ceeccc", 20 | 50: "#c1e7c0", 21 | Base: "#b5e2b3", 22 | 60: "#97bc95", 23 | 70: "#799777", 24 | 80: "#5b715a", 25 | 90: "#3c4b3c", 26 | 100: "#242d24", 27 | }, 28 | Dark: { 29 | 10: "#fafafa", 30 | 20: "#f5f5f5", 31 | 30: "#e5e5e5", 32 | 40: "#d4d4d4", 33 | 50: "#a3a3a3", 34 | Base: "#737373", 35 | 60: "#525252", 36 | 70: "#404040", 37 | 80: "#262626", 38 | 90: "#171717", 39 | 100: "#0a0a0a", 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | font-family: Arial, Helvetica, sans-serif; 7 | } 8 | 9 | @layer base { 10 | :root { 11 | --background: 0 0% 100%; 12 | --foreground: 0 0% 3.9%; 13 | --card: 0 0% 100%; 14 | --card-foreground: 0 0% 3.9%; 15 | --popover: 0 0% 100%; 16 | --popover-foreground: 0 0% 3.9%; 17 | --primary: 0 0% 9%; 18 | --primary-foreground: 0 0% 98%; 19 | --secondary: 0 0% 96.1%; 20 | --secondary-foreground: 0 0% 9%; 21 | --muted: 0 0% 96.1%; 22 | --muted-foreground: 0 0% 45.1%; 23 | --accent: 0 0% 96.1%; 24 | --accent-foreground: 0 0% 9%; 25 | --destructive: 0 84.2% 60.2%; 26 | --destructive-foreground: 0 0% 98%; 27 | --border: 0 0% 89.8%; 28 | --input: 0 0% 89.8%; 29 | --ring: 0 0% 3.9%; 30 | --chart-1: 12 76% 61%; 31 | --chart-2: 173 58% 39%; 32 | --chart-3: 197 37% 24%; 33 | --chart-4: 43 74% 66%; 34 | --chart-5: 27 87% 67%; 35 | --radius: 0.5rem; 36 | } 37 | .dark { 38 | --background: 0 0% 3.9%; 39 | --foreground: 0 0% 98%; 40 | --card: 0 0% 3.9%; 41 | --card-foreground: 0 0% 98%; 42 | --popover: 0 0% 3.9%; 43 | --popover-foreground: 0 0% 98%; 44 | --primary: 0 0% 98%; 45 | --primary-foreground: 0 0% 9%; 46 | --secondary: 0 0% 14.9%; 47 | --secondary-foreground: 0 0% 98%; 48 | --muted: 0 0% 14.9%; 49 | --muted-foreground: 0 0% 63.9%; 50 | --accent: 0 0% 14.9%; 51 | --accent-foreground: 0 0% 98%; 52 | --destructive: 0 62.8% 30.6%; 53 | --destructive-foreground: 0 0% 98%; 54 | --border: 0 0% 14.9%; 55 | --input: 0 0% 14.9%; 56 | --ring: 0 0% 83.1%; 57 | --chart-1: 220 70% 50%; 58 | --chart-2: 160 60% 45%; 59 | --chart-3: 30 80% 55%; 60 | --chart-4: 280 65% 60%; 61 | --chart-5: 340 75% 55%; 62 | } 63 | } 64 | 65 | @layer base { 66 | * { 67 | @apply border-border; 68 | } 69 | body { 70 | @apply bg-background text-foreground; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/types/README.md: -------------------------------------------------------------------------------- 1 | # types 2 | 3 | Folder `types` dalam proyek ini berfungsi sebagai tempat penyimpanan definisi tipe yang digunakan di seluruh aplikasi untuk memastikan konsistensi dan tipe yang kuat. 4 | 5 | Author: Wahyu Agus Arifin 6 | -------------------------------------------------------------------------------- /src/types/response.ts: -------------------------------------------------------------------------------- 1 | export type PaginationType = { 2 | Total: number; 3 | Limit: number; 4 | PageCurrent: number; 5 | PageTotal: number; 6 | }; 7 | 8 | export type ResponseMeta = { 9 | Message: string; 10 | Results: { 11 | Status: boolean; 12 | Data: T; 13 | Pagination?: PaginationType; 14 | }; 15 | }; 16 | -------------------------------------------------------------------------------- /src/types/screen_public.types.ts: -------------------------------------------------------------------------------- 1 | type Inputs = { 2 | username: string; 3 | password: string; 4 | expiresInMins: number; 5 | }; 6 | 7 | export type { Inputs }; 8 | -------------------------------------------------------------------------------- /src/utils/README.md: -------------------------------------------------------------------------------- 1 | # utils 2 | 3 | Folder `utils` dalam proyek ini berfungsi sebagai tempat penyimpanan fungsi-fungsi utilitas yang dirancang untuk memudahkan pengembangan aplikasi. 4 | 5 | 6 | Author : Wahyu Agus Arifin -------------------------------------------------------------------------------- /src/utils/use_icon.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Color } from "@/styles/color"; 3 | import { Icon } from "@iconify/react"; 4 | import { useTheme } from "next-themes"; 5 | import type React from "react"; 6 | import { useEffect, useState } from "react"; 7 | 8 | type IconType = "loading" | "home" | "settings"; 9 | type SizeType = "small" | "medium" | "large"; 10 | 11 | interface AppIconProps { 12 | icon: IconType; 13 | size?: SizeType; 14 | color?: string; 15 | darkColor?: string; 16 | } 17 | 18 | const iconMapping: { [key in IconType]: string } = { 19 | loading: "line-md:loading-twotone-loop", 20 | home: "mdi:home", 21 | settings: "mdi:cog", 22 | }; 23 | 24 | const sizeMapping: { [key in SizeType]: string } = { 25 | small: "14px", 26 | medium: "24px", 27 | large: "32px", 28 | }; 29 | const AppIcon: React.FC = ({ 30 | icon, 31 | size = "medium", 32 | color = Color.Main[70], 33 | darkColor = Color.Dark[40], 34 | }) => { 35 | const { theme, systemTheme } = useTheme(); 36 | const [currentTheme, setCurrentTheme] = useState( 37 | undefined, 38 | ); 39 | useEffect(() => { 40 | if (theme === "system") { 41 | setCurrentTheme(systemTheme); 42 | } else { 43 | setCurrentTheme(theme); 44 | } 45 | }, [theme, systemTheme]); 46 | 47 | const iconSize = sizeMapping[size]; 48 | const iconColor = currentTheme === "dark" ? darkColor : color; 49 | return ( 50 | 56 | ); 57 | }; 58 | 59 | export default AppIcon; 60 | -------------------------------------------------------------------------------- /src/utils/use_router.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { usePathname } from "next/navigation"; 3 | import { useRouter as Router } from "nextjs-toploader/app"; 4 | import { useEffect, useState } from "react"; 5 | 6 | export const useRouter = () => { 7 | const router = Router(); 8 | const pathname = usePathname(); 9 | const [currentPath, setCurrentPath] = useState(pathname); 10 | 11 | useEffect(() => { 12 | const playSound = () => { 13 | const audio = new Audio("/shutter-click.wav"); 14 | audio.play(); 15 | }; 16 | if (currentPath !== pathname) { 17 | playSound(); 18 | setCurrentPath(pathname); 19 | } 20 | }, [pathname, currentPath]); 21 | return router; 22 | }; 23 | -------------------------------------------------------------------------------- /src/utils/use_theme.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Icon } from "@iconify/react"; 3 | import { useTheme as THEME } from "next-themes"; 4 | import { Case, Switch } from "react-if"; 5 | 6 | const UseTheme = () => { 7 | const { theme = "light", setTheme } = THEME(); 8 | return ( 9 |
10 | 11 | 12 | 23 | 24 | 25 | 33 | 34 | 35 | 43 | 44 | 45 |
46 | ); 47 | }; 48 | 49 | export default UseTheme; 50 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | export default { 4 | content: [ 5 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}", 8 | "./src/interfaces/**/*.{js,ts,jsx,tsx,mdx}", 9 | "./src/utils/**/*.{js,ts,jsx,tsx,mdx}", 10 | ], 11 | theme: { 12 | extend: { 13 | colors: { 14 | background: "hsl(var(--background))", 15 | foreground: "hsl(var(--foreground))", 16 | card: { 17 | DEFAULT: "hsl(var(--card))", 18 | foreground: "hsl(var(--card-foreground))", 19 | }, 20 | popover: { 21 | DEFAULT: "hsl(var(--popover))", 22 | foreground: "hsl(var(--popover-foreground))", 23 | }, 24 | primary: { 25 | DEFAULT: "hsl(var(--primary))", 26 | foreground: "hsl(var(--primary-foreground))", 27 | }, 28 | secondary: { 29 | DEFAULT: "hsl(var(--secondary))", 30 | foreground: "hsl(var(--secondary-foreground))", 31 | }, 32 | muted: { 33 | DEFAULT: "hsl(var(--muted))", 34 | foreground: "hsl(var(--muted-foreground))", 35 | }, 36 | accent: { 37 | DEFAULT: "hsl(var(--accent))", 38 | foreground: "hsl(var(--accent-foreground))", 39 | }, 40 | destructive: { 41 | DEFAULT: "hsl(var(--destructive))", 42 | foreground: "hsl(var(--destructive-foreground))", 43 | }, 44 | border: "hsl(var(--border))", 45 | input: "hsl(var(--input))", 46 | ring: "hsl(var(--ring))", 47 | chart: { 48 | "1": "hsl(var(--chart-1))", 49 | "2": "hsl(var(--chart-2))", 50 | "3": "hsl(var(--chart-3))", 51 | "4": "hsl(var(--chart-4))", 52 | "5": "hsl(var(--chart-5))", 53 | }, 54 | }, 55 | borderRadius: { 56 | lg: "var(--radius)", 57 | md: "calc(var(--radius) - 2px)", 58 | sm: "calc(var(--radius) - 4px)", 59 | }, 60 | }, 61 | }, 62 | plugins: [require("tailwindcss-animate")], 63 | darkMode: ["class"], 64 | } satisfies Config; 65 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | --------------------------------------------------------------------------------