├── .husky
├── commit-msg
└── pre-commit
├── .eslintrc.json
├── envs
├── .env.staging.sample
├── .env.development.sample
└── .env.production.sample
├── src
├── app
│ ├── favicon.ico
│ ├── fonts
│ │ ├── GeistVF.woff
│ │ └── GeistMonoVF.woff
│ ├── (public)
│ │ ├── contact
│ │ │ └── page.tsx
│ │ └── page.tsx
│ ├── guard
│ │ └── ex
│ │ │ └── page.tsx
│ ├── README.md
│ └── layout.tsx
├── shared
│ ├── path.ts
│ ├── README.md
│ └── toolkit
│ │ ├── hooks.ts
│ │ ├── store.ts
│ │ └── slice
│ │ └── authorized_slice.ts
├── services
│ ├── api
│ │ └── main
│ │ │ ├── endpoint.ts
│ │ │ ├── interceptor.ts
│ │ │ └── call.ts
│ └── README.md
├── types
│ ├── screen_public.types.ts
│ ├── README.md
│ └── response.ts
├── interfaces
│ ├── screens
│ │ ├── screen_private
│ │ │ └── main.tsx
│ │ ├── README.md
│ │ └── screen_public
│ │ │ └── main.tsx
│ ├── layouts
│ │ ├── README.md
│ │ ├── public_layout.tsx
│ │ └── main_layout.tsx
│ └── components
│ │ ├── README.md
│ │ ├── ui
│ │ ├── input.tsx
│ │ ├── label.tsx
│ │ ├── checkbox.tsx
│ │ ├── popover.tsx
│ │ ├── radio-group.tsx
│ │ ├── button.tsx
│ │ └── calendar.tsx
│ │ └── form-input.tsx
├── lib
│ └── utils.ts
├── configs
│ ├── environment.ts
│ └── README.md
├── utils
│ ├── README.md
│ ├── use_router.tsx
│ ├── use_icon.tsx
│ └── use_theme.tsx
├── styles
│ ├── README.md
│ ├── color.ts
│ └── globals.css
├── modules
│ ├── README.md
│ ├── providers
│ │ ├── redux_provider.tsx
│ │ └── theme_provider.tsx
│ └── cookies.ts
└── middleware.ts
├── public
├── shutter-click.wav
├── vercel.svg
├── window.svg
├── file.svg
├── globe.svg
└── next.svg
├── .dockerignore
├── postcss.config.mjs
├── next.config.ts
├── docker
├── staging
│ ├── compose.yaml
│ └── Dockerfile
├── production
│ ├── compose.yaml
│ └── Dockerfile
└── development
│ ├── compose.yaml
│ └── Dockerfile
├── commitlint.config.cts
├── components.json
├── biome.json
├── .gitignore
├── tsconfig.json
├── .github
├── FUNDING.yml
└── workflows
│ ├── cicd.yaml
│ └── codeql.yml
├── Makefile
├── tailwind.config.ts
├── package.json
├── cmd
├── setup-manual.sh
└── setup-auto.sh
└── README.md
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | pnpm dlx commitlint --edit $1
2 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["next/core-web-vitals", "next/typescript"]
3 | }
4 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | pnpm run lint:biome
2 | pnpm run check:biome
3 | pnpm run format:biome
--------------------------------------------------------------------------------
/envs/.env.staging.sample:
--------------------------------------------------------------------------------
1 | NEXT_PUBLIC_API_URL=https://api-staging.com
2 | NEXT_PUBLIC_MODE=staging
--------------------------------------------------------------------------------
/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ItPohgero/next-architecture/HEAD/src/app/favicon.ico
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/public/shutter-click.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ItPohgero/next-architecture/HEAD/public/shutter-click.wav
--------------------------------------------------------------------------------
/src/app/fonts/GeistVF.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ItPohgero/next-architecture/HEAD/src/app/fonts/GeistVF.woff
--------------------------------------------------------------------------------
/src/shared/path.ts:
--------------------------------------------------------------------------------
1 | export const PATH = {
2 | NOT_FOUND: "/404",
3 | HOME: "/",
4 | PRIVATE: "/guard/ex",
5 | };
6 |
--------------------------------------------------------------------------------
/src/app/fonts/GeistMonoVF.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ItPohgero/next-architecture/HEAD/src/app/fonts/GeistMonoVF.woff
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | Dockerfile
2 | .dockerignore
3 | node_modules
4 | npm-debug.log
5 | README.md
6 | .next
7 | docker
8 | .git
9 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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/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/types/screen_public.types.ts:
--------------------------------------------------------------------------------
1 | type Inputs = {
2 | username: string;
3 | password: string;
4 | expiresInMins: number;
5 | };
6 |
7 | export type { Inputs };
8 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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 |
--------------------------------------------------------------------------------
/public/window.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/file.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/public/globe.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
82 |
83 | );
84 | };
85 |
86 | export default ScreenPublic;
87 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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)
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------