├── src
├── vite-env.d.ts
├── index.css
├── types
│ ├── accountType.ts
│ └── userType.ts
├── page.tsx
├── main.tsx
├── components
│ ├── Logo.tsx
│ ├── SearchUser.tsx
│ ├── Footer.tsx
│ ├── Navbar.tsx
│ ├── ToogleNightMode.tsx
│ ├── EditUser.tsx
│ ├── FilterUser.tsx
│ ├── AddUser.tsx
│ └── UserList.tsx
├── middleware
│ └── ProtectedRoute.tsx
├── controllers
│ ├── AccountController.ts
│ └── UserControllers.ts
├── hooks
│ └── useTheme.tsx
├── Layout.tsx
├── assets
│ └── react.svg
└── pages
│ ├── SettingsProfile.tsx
│ └── Login.tsx
├── bun.lockb
├── postcss.config.js
├── tsconfig.json
├── vite.config.ts
├── tailwind.config.js
├── .gitignore
├── index.html
├── tsconfig.node.json
├── tsconfig.app.json
├── eslint.config.js
├── package.json
├── public
└── vite.svg
└── README.md
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/bun.lockb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rendy278/RenLiblary/HEAD/bun.lockb
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/src/types/accountType.ts:
--------------------------------------------------------------------------------
1 | export interface Account {
2 | id: number;
3 | username: string;
4 | email: string;
5 | password: string;
6 | }
7 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "references": [
4 | { "path": "./tsconfig.app.json" },
5 | { "path": "./tsconfig.node.json" }
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 |
4 | // https://vite.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | })
8 |
--------------------------------------------------------------------------------
/src/types/userType.ts:
--------------------------------------------------------------------------------
1 | export interface User {
2 | id: number;
3 | name: string;
4 | address: string;
5 | phone: number;
6 | bookTitle: string;
7 | loadDate: string;
8 | returnDate: string;
9 | }
10 |
--------------------------------------------------------------------------------
/src/page.tsx:
--------------------------------------------------------------------------------
1 | import UserList from "./components/UserList";
2 |
3 | const App = () => {
4 | return (
5 |
6 |
7 |
8 | );
9 | };
10 |
11 | export default App;
12 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
4 | darkMode: "class",
5 | theme: {
6 | extend: {},
7 | },
8 | plugins: [],
9 | };
10 |
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import { StrictMode } from "react";
2 | import { createRoot } from "react-dom/client";
3 | import "./index.css";
4 | import Layout from "./Layout.tsx"; // Layout utama yang mengatur navigasi
5 |
6 | createRoot(document.getElementById("root")!).render(
7 |
8 |
9 |
10 | );
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/src/components/Logo.tsx:
--------------------------------------------------------------------------------
1 | import { BiBook } from "react-icons/bi";
2 | import { Link } from "react-router-dom";
3 |
4 | const Logo = () => {
5 | return (
6 |
7 |
8 |
RenLiblary
9 |
10 | );
11 | };
12 |
13 | export default Logo;
14 |
--------------------------------------------------------------------------------
/src/middleware/ProtectedRoute.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from "react";
2 | import { Navigate } from "react-router-dom";
3 |
4 | const ProtectedRoute = ({ children }: { children: ReactNode }) => {
5 | const isLoggedIn = localStorage.getItem("isLoggedIn") === "true";
6 | return isLoggedIn ? children : ;
7 | };
8 |
9 | export default ProtectedRoute;
10 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Crud Local Storange Test Intern
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/components/SearchUser.tsx:
--------------------------------------------------------------------------------
1 | interface SearchUserProps {
2 | query: string;
3 | onSearch: (e: React.ChangeEvent) => void;
4 | }
5 |
6 | const SearchUser: React.FC = ({ query, onSearch }) => {
7 | return (
8 |
9 |
16 |
17 | );
18 | };
19 |
20 | export default SearchUser;
21 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4 | "target": "ES2022",
5 | "lib": ["ES2023"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "isolatedModules": true,
13 | "moduleDetection": "force",
14 | "noEmit": true,
15 |
16 | /* Linting */
17 | "strict": true,
18 | "noUnusedLocals": true,
19 | "noUnusedParameters": true,
20 | "noFallthroughCasesInSwitch": true,
21 | "noUncheckedSideEffectImports": true
22 | },
23 | "include": ["vite.config.ts"]
24 | }
25 |
--------------------------------------------------------------------------------
/src/controllers/AccountController.ts:
--------------------------------------------------------------------------------
1 | import { Account } from "../types/accountType";
2 |
3 | const defaultAccount: Account = {
4 | id: 1,
5 | username: "Rendy Yoshizawa",
6 | email: "rendyyoshizawa@gmail.com",
7 | password: "rendy123",
8 | };
9 |
10 | export const getAccount = (): Account => {
11 | const account = localStorage.getItem("account");
12 | return account ? JSON.parse(account) : defaultAccount;
13 | };
14 |
15 | const saveAccount = (account: Account): void => {
16 | localStorage.setItem("account", JSON.stringify(account));
17 | };
18 |
19 | export const editAccount = (updatedAccount: Partial): void => {
20 | const account = getAccount();
21 | const newAccount = { ...account, ...updatedAccount };
22 | saveAccount(newAccount);
23 | };
24 |
--------------------------------------------------------------------------------
/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4 | "target": "ES2020",
5 | "useDefineForClassFields": true,
6 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
7 | "module": "ESNext",
8 | "skipLibCheck": true,
9 |
10 | /* Bundler mode */
11 | "moduleResolution": "bundler",
12 | "allowImportingTsExtensions": true,
13 | "isolatedModules": true,
14 | "moduleDetection": "force",
15 | "noEmit": true,
16 | "jsx": "react-jsx",
17 |
18 | /* Linting */
19 | "strict": true,
20 | "noUnusedLocals": true,
21 | "noUnusedParameters": true,
22 | "noFallthroughCasesInSwitch": true,
23 | "noUncheckedSideEffectImports": true
24 | },
25 | "include": ["src"]
26 | }
27 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | import js from '@eslint/js'
2 | import globals from 'globals'
3 | import reactHooks from 'eslint-plugin-react-hooks'
4 | import reactRefresh from 'eslint-plugin-react-refresh'
5 | import tseslint from 'typescript-eslint'
6 |
7 | export default tseslint.config(
8 | { ignores: ['dist'] },
9 | {
10 | extends: [js.configs.recommended, ...tseslint.configs.recommended],
11 | files: ['**/*.{ts,tsx}'],
12 | languageOptions: {
13 | ecmaVersion: 2020,
14 | globals: globals.browser,
15 | },
16 | plugins: {
17 | 'react-hooks': reactHooks,
18 | 'react-refresh': reactRefresh,
19 | },
20 | rules: {
21 | ...reactHooks.configs.recommended.rules,
22 | 'react-refresh/only-export-components': [
23 | 'warn',
24 | { allowConstantExport: true },
25 | ],
26 | },
27 | },
28 | )
29 |
--------------------------------------------------------------------------------
/src/components/Footer.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Logo from "./Logo";
3 |
4 | const Footer: React.FC = () => {
5 | const currentYear = new Date().getFullYear();
6 |
7 | return (
8 |
21 | );
22 | };
23 |
24 | export default Footer;
25 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "testinternpaid",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc -b && vite build",
9 | "lint": "eslint .",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "react": "^18.3.1",
14 | "react-dom": "^18.3.1",
15 | "react-icons": "^5.4.0",
16 | "react-router-dom": "^7.1.1"
17 | },
18 | "devDependencies": {
19 | "@eslint/js": "^9.17.0",
20 | "@types/react": "^18.3.18",
21 | "@types/react-dom": "^18.3.5",
22 | "@vitejs/plugin-react": "^4.3.4",
23 | "autoprefixer": "^10.4.20",
24 | "eslint": "^9.17.0",
25 | "eslint-plugin-react-hooks": "^5.0.0",
26 | "eslint-plugin-react-refresh": "^0.4.16",
27 | "globals": "^15.14.0",
28 | "postcss": "^8.4.49",
29 | "tailwindcss": "^3.4.17",
30 | "typescript": "~5.6.2",
31 | "typescript-eslint": "^8.18.2",
32 | "vite": "^6.0.5"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/hooks/useTheme.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | createContext,
3 | useState,
4 | useEffect,
5 | useContext,
6 | ReactNode,
7 | } from "react";
8 |
9 | interface ThemeContextProps {
10 | isNightMode: boolean;
11 | toggleNightMode: () => void;
12 | }
13 |
14 | const ThemeContext = createContext(undefined);
15 |
16 | export const useTheme = () => {
17 | const context = useContext(ThemeContext);
18 | if (!context) {
19 | throw new Error("useTheme must be used within a ThemeProvider");
20 | }
21 | return context;
22 | };
23 |
24 | interface ThemeProviderProps {
25 | children: ReactNode;
26 | }
27 |
28 | export const ThemeProvider = ({ children }: ThemeProviderProps) => {
29 | const [isNightMode, setIsNightMode] = useState(() => {
30 | const storedValue = localStorage.getItem("isNightMode");
31 | return storedValue ? JSON.parse(storedValue) : false;
32 | });
33 |
34 | useEffect(() => {
35 | localStorage.setItem("isNightMode", JSON.stringify(isNightMode));
36 | document.documentElement.classList.toggle("dark", isNightMode);
37 | }, [isNightMode]);
38 |
39 | const toggleNightMode = () => {
40 | setIsNightMode((prevMode) => !prevMode);
41 | };
42 |
43 | return (
44 |
45 | {children}
46 |
47 | );
48 | };
49 |
--------------------------------------------------------------------------------
/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/controllers/UserControllers.ts:
--------------------------------------------------------------------------------
1 | import { User } from "../types/userType";
2 |
3 | const getUsers = (): User[] => {
4 | const users = localStorage.getItem("users");
5 | return users ? JSON.parse(users) : [];
6 | };
7 |
8 | export const addUser = (user: User): void => {
9 | const users = getUsers();
10 | users.push(user);
11 | localStorage.setItem("users", JSON.stringify(users));
12 | };
13 |
14 | export const editUser = (updatedUser: User): void => {
15 | const users = getUsers().map((user) =>
16 | user.id === updatedUser.id ? updatedUser : user
17 | );
18 | localStorage.setItem("users", JSON.stringify(users));
19 | };
20 |
21 | export const deleteUser = (id: number): void => {
22 | const users = getUsers().filter((user) => user.id !== id);
23 | localStorage.setItem("users", JSON.stringify(users));
24 | };
25 |
26 | export const searchUsers = (query: string): User[] => {
27 | const users = getUsers();
28 | return users.filter(
29 | (user) =>
30 | user.name.toLowerCase().includes(query.toLowerCase()) ||
31 | user.address.toLowerCase().includes(query.toLowerCase()) ||
32 | user.bookTitle.toLowerCase().includes(query.toLowerCase())
33 | );
34 | };
35 |
36 | export const filterUsersByDate = (
37 | startDate: string,
38 | endDate: string
39 | ): User[] => {
40 | const users = getUsers();
41 | const start = startDate ? new Date(startDate) : null;
42 | const end = endDate ? new Date(endDate) : null;
43 |
44 | return users.filter((user) => {
45 | const loadDate = new Date(user.loadDate);
46 | const returnDate = new Date(user.returnDate);
47 |
48 | const isInRange =
49 | (start ? loadDate >= start : true) && (end ? returnDate <= end : true);
50 | return isInRange;
51 | });
52 | };
53 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React + TypeScript + Vite
2 |
3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4 |
5 | Currently, two official plugins are available:
6 |
7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9 |
10 | ## Expanding the ESLint configuration
11 |
12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
13 |
14 | - Configure the top-level `parserOptions` property like this:
15 |
16 | ```js
17 | export default tseslint.config({
18 | languageOptions: {
19 | // other options...
20 | parserOptions: {
21 | project: ['./tsconfig.node.json', './tsconfig.app.json'],
22 | tsconfigRootDir: import.meta.dirname,
23 | },
24 | },
25 | })
26 | ```
27 |
28 | - Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
29 | - Optionally add `...tseslint.configs.stylisticTypeChecked`
30 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
31 |
32 | ```js
33 | // eslint.config.js
34 | import react from 'eslint-plugin-react'
35 |
36 | export default tseslint.config({
37 | // Set the react version
38 | settings: { react: { version: '18.3' } },
39 | plugins: {
40 | // Add the react plugin
41 | react,
42 | },
43 | rules: {
44 | // other rules...
45 | // Enable its recommended rules
46 | ...react.configs.recommended.rules,
47 | ...react.configs['jsx-runtime'].rules,
48 | },
49 | })
50 | ```
51 |
--------------------------------------------------------------------------------
/src/Layout.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import Footer from "./components/Footer";
3 | import Navbar from "./components/Navbar";
4 | import {
5 | createBrowserRouter,
6 | createRoutesFromElements,
7 | Route,
8 | RouterProvider,
9 | Navigate,
10 | Outlet,
11 | } from "react-router-dom";
12 | import Login from "./pages/Login";
13 | import App from "./page";
14 | import ProtectedRoute from "./middleware/ProtectedRoute";
15 | import { ThemeProvider } from "./hooks/useTheme";
16 | import SettingsProfile from "./pages/SettingsProfile";
17 |
18 | const MainLayout = ({ handleLogout }: { handleLogout: () => void }) => (
19 | <>
20 |
21 |
22 |
23 | >
24 | );
25 |
26 | const Layout = () => {
27 | const [isLoggedIn, setIsLoggedIn] = useState(false);
28 |
29 | useEffect(() => {
30 | const loggedIn = localStorage.getItem("isLoggedIn") === "true";
31 | setIsLoggedIn(loggedIn);
32 | }, []);
33 |
34 | const handleLogout = () => {
35 | localStorage.removeItem("isLoggedIn");
36 | setIsLoggedIn(false);
37 | };
38 |
39 | const router = createBrowserRouter(
40 | createRoutesFromElements(
41 |
42 |
46 |
47 |
48 | }
49 | >
50 | } />
51 | } />
52 |
53 |
54 | : }
57 | />
58 |
59 |
63 | 404 - Halaman Tidak Ditemukan
64 | Halaman yang Anda cari tidak tersedia.
65 |
66 | }
67 | />
68 |
69 | )
70 | );
71 |
72 | return (
73 |
74 |
75 |
76 |
77 |
78 | );
79 | };
80 |
81 | export default Layout;
82 |
--------------------------------------------------------------------------------
/src/components/Navbar.tsx:
--------------------------------------------------------------------------------
1 | import { useRef, useEffect, useState } from "react";
2 | import { AiOutlineUser } from "react-icons/ai";
3 | import ToggleNightMode from "../components/ToogleNightMode";
4 | import Logo from "./Logo";
5 | import { useNavigate } from "react-router-dom";
6 |
7 | const Navbar = ({ handleLogout }: { handleLogout: () => void }) => {
8 | const navigate = useNavigate();
9 | const userDropdownRef = useRef(null);
10 | const [userDropdownOpen, setUserDropdownOpen] = useState(false);
11 | const [user, setUser] = useState({
12 | userName: "",
13 | userEmail: "",
14 | userPassword: "",
15 | });
16 |
17 | useEffect(() => {
18 | const storedAccount = localStorage.getItem("account");
19 | if (storedAccount) {
20 | const account = JSON.parse(storedAccount);
21 | setUser({
22 | userName: account.username,
23 | userEmail: account.email,
24 | userPassword: account.password,
25 | });
26 | }
27 | }, []);
28 |
29 | useEffect(() => {
30 | const handleOutsideClick = (event: MouseEvent) => {
31 | if (
32 | userDropdownRef.current &&
33 | !userDropdownRef.current.contains(event.target as Node)
34 | ) {
35 | setUserDropdownOpen(false);
36 | }
37 | };
38 |
39 | document.addEventListener("mousedown", handleOutsideClick);
40 |
41 | return () => {
42 | document.removeEventListener("mousedown", handleOutsideClick);
43 | };
44 | }, []);
45 |
46 | const toggleUserDropdown = () => {
47 | setUserDropdownOpen((prev) => !prev);
48 | };
49 |
50 | const handleNavigate = () => {
51 | // Navigasi ke halaman "/settings"
52 | navigate("/settings");
53 | };
54 |
55 | return (
56 |
100 | );
101 | };
102 |
103 | export default Navbar;
104 |
--------------------------------------------------------------------------------
/src/assets/react.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/ToogleNightMode.tsx:
--------------------------------------------------------------------------------
1 | import { FaMoon, FaSun } from "react-icons/fa";
2 | import { IoIosArrowDown } from "react-icons/io";
3 | import { useTheme } from "../hooks/useTheme";
4 | import { useState, useEffect, useRef } from "react";
5 | import { GrSystem } from "react-icons/gr";
6 |
7 | // Define your mode options with icons
8 | const modeOptions = [
9 | { mode: "system", label: "System", icon: },
10 | {
11 | mode: "dark",
12 | label: "Dark",
13 | icon: ,
14 | },
15 | {
16 | mode: "light",
17 | label: "Light",
18 | icon: ,
19 | },
20 | ];
21 |
22 | type ModeType = "system" | "dark" | "light";
23 |
24 | const ToggleNightMode = () => {
25 | const { isNightMode, toggleNightMode } = useTheme();
26 | const [mode, setMode] = useState(() => {
27 | const savedMode = localStorage.getItem("themeMode") as ModeType;
28 | return savedMode || "system";
29 | });
30 |
31 | const [isDropdownOpen, setIsDropdownOpen] = useState(false);
32 | const dropdownRef = useRef(null);
33 |
34 | useEffect(() => {
35 | localStorage.setItem("themeMode", mode);
36 |
37 | if (mode === "system") {
38 | const isSystemDark = window.matchMedia(
39 | "(prefers-color-scheme: dark)"
40 | ).matches;
41 | if (isSystemDark) {
42 | if (!isNightMode) toggleNightMode();
43 | } else {
44 | if (isNightMode) toggleNightMode();
45 | }
46 | } else if (mode === "dark" && !isNightMode) {
47 | toggleNightMode();
48 | } else if (mode === "light" && isNightMode) {
49 | toggleNightMode();
50 | }
51 | }, [mode, isNightMode, toggleNightMode]);
52 |
53 | useEffect(() => {
54 | const handleOutsideClick = (event: MouseEvent) => {
55 | if (
56 | dropdownRef.current &&
57 | !dropdownRef.current.contains(event.target as Node)
58 | ) {
59 | setIsDropdownOpen(false);
60 | }
61 | };
62 | document.addEventListener("click", handleOutsideClick);
63 | return () => {
64 | document.removeEventListener("click", handleOutsideClick);
65 | };
66 | }, []);
67 |
68 | const handleModeChange = (newMode: ModeType) => {
69 | setMode(newMode);
70 | setIsDropdownOpen(false);
71 | };
72 |
73 | const selectedOption = modeOptions.find((option) => option.mode === mode);
74 |
75 | return (
76 |
77 |
setIsDropdownOpen((prev) => !prev)}
80 | >
81 |
82 |
83 | {selectedOption?.icon}
84 | {selectedOption?.label}
85 |
86 |
91 |
92 |
93 |
94 | {isDropdownOpen && (
95 |
113 | )}
114 |
115 | );
116 | };
117 |
118 | export default ToggleNightMode;
119 |
--------------------------------------------------------------------------------
/src/pages/SettingsProfile.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import { getAccount, editAccount } from "../controllers/AccountController";
3 | import { AiFillEye, AiFillEyeInvisible } from "react-icons/ai";
4 |
5 | const SettingsProfile = () => {
6 | const [formData, setFormData] = useState({
7 | username: "",
8 | email: "",
9 | password: "",
10 | });
11 | const [message, setMessage] = useState("");
12 | const [showPassword, setShowPassword] = useState(false);
13 |
14 | useEffect(() => {
15 | const account = getAccount();
16 | setFormData({
17 | username: account.username,
18 | email: account.email,
19 | password: account.password,
20 | });
21 | }, []);
22 |
23 | const handleInputChange = (e: React.ChangeEvent) => {
24 | const { name, value } = e.target;
25 | setFormData((prevState) => ({
26 | ...prevState,
27 | [name]: value,
28 | }));
29 | };
30 |
31 | const handleSubmit = (e: React.FormEvent) => {
32 | e.preventDefault();
33 | editAccount(formData);
34 | setMessage("Akun berhasil diperbarui!");
35 | setTimeout(() => setMessage(""), 3000);
36 | };
37 |
38 | return (
39 |
40 |
41 |
42 |
Account Settings
43 |
44 |
119 |
120 |
121 | );
122 | };
123 |
124 | export default SettingsProfile;
125 |
--------------------------------------------------------------------------------
/src/pages/Login.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { useNavigate } from "react-router-dom";
3 | import { AiFillEye, AiFillEyeInvisible } from "react-icons/ai";
4 | import { getAccount } from "../controllers/AccountController";
5 | import Logo from "../components/Logo";
6 |
7 | type LoginState = {
8 | email: string;
9 | password: string;
10 | message: string;
11 | showPassword: boolean;
12 | };
13 |
14 | const Login = () => {
15 | const [loginState, setLoginState] = useState({
16 | email: "",
17 | password: "",
18 | message: "",
19 | showPassword: false,
20 | });
21 |
22 | const navigate = useNavigate();
23 |
24 | const handleChange = (e: React.ChangeEvent) => {
25 | const { name, value } = e.target;
26 | setLoginState((prevState) => ({
27 | ...prevState,
28 | [name]: value,
29 | }));
30 | };
31 |
32 | const togglePasswordVisibility = () => {
33 | setLoginState((prevState) => ({
34 | ...prevState,
35 | showPassword: !prevState.showPassword,
36 | }));
37 | };
38 |
39 | const handleSubmit = (e: React.FormEvent) => {
40 | e.preventDefault();
41 | const storedAccount = getAccount();
42 |
43 | if (
44 | loginState.email === storedAccount.email &&
45 | loginState.password === storedAccount.password
46 | ) {
47 | localStorage.setItem("isLoggedIn", "true");
48 | setLoginState((prevState) => ({ ...prevState, message: "" }));
49 | navigate("/");
50 | } else {
51 | setLoginState((prevState) => ({
52 | ...prevState,
53 | message: "Email atau password salah",
54 | }));
55 | }
56 | };
57 |
58 | return (
59 |
60 |
61 |
62 |
63 |
64 |
Login
65 |
66 |
128 |
129 |
130 |
131 | );
132 | };
133 |
134 | export default Login;
135 |
--------------------------------------------------------------------------------
/src/components/EditUser.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { User } from "../types/userType";
3 |
4 | interface EditUserProps {
5 | isModal: boolean;
6 | setModal: (value: boolean) => void;
7 | user: User;
8 | onEditUser: (user: User) => void;
9 | }
10 |
11 | const EditUser = ({ isModal, setModal, user, onEditUser }: EditUserProps) => {
12 | const [formData, setFormData] = useState({
13 | name: user.name,
14 | address: user.address,
15 | phone: user.phone,
16 | bookTitle: user.bookTitle,
17 | loadDate: user.loadDate,
18 | returnDate: user.returnDate,
19 | });
20 |
21 | const [errors, setErrors] = useState({
22 | name: "",
23 | address: "",
24 | phone: "",
25 | bookTitle: "",
26 | loadDate: "",
27 | returnDate: "",
28 | });
29 |
30 | const handleChange = (field: keyof typeof formData, value: string) => {
31 | setFormData({ ...formData, [field]: value });
32 | if (value.trim() !== "") {
33 | setErrors({ ...errors, [field]: "" });
34 | }
35 | };
36 |
37 | const validateForm = () => {
38 | const newErrors = {
39 | name: formData.name.trim() ? "" : "Name is required",
40 | address: formData.address.trim() ? "" : "Address is required",
41 | phone: formData.phone > 0 ? "" : "Phone is required",
42 | bookTitle: formData.bookTitle.trim() ? "" : "Book title is required",
43 | loadDate: formData.loadDate ? "" : "Load date is required",
44 | returnDate: formData.returnDate ? "" : "Return date is required",
45 | };
46 | setErrors(newErrors);
47 | return Object.values(newErrors).every((error) => error === "");
48 | };
49 |
50 | const handleSubmit = (e: React.FormEvent) => {
51 | e.preventDefault();
52 | if (validateForm()) {
53 | const updatedUser: User = {
54 | ...user,
55 | ...formData,
56 | };
57 | onEditUser(updatedUser);
58 | setModal(false);
59 | }
60 | };
61 |
62 | return (
63 |
64 | {isModal && (
65 |
66 |
67 |
Edit User
68 |
141 |
142 |
143 | )}
144 |
145 | );
146 | };
147 |
148 | export default EditUser;
149 |
--------------------------------------------------------------------------------
/src/components/FilterUser.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { BsCalendar } from "react-icons/bs";
3 |
4 | interface FilterUserProps {
5 | onApplyFilter: (startDate: string, endDate: string, type: string) => void;
6 | }
7 |
8 | const FilterUser: React.FC = ({ onApplyFilter }) => {
9 | const [startDate, setStartDate] = useState("");
10 | const [endDate, setEndDate] = useState("");
11 | const [filterType, setFilterType] = useState("");
12 | const [error, setError] = useState("");
13 | const [isModalOpen, setIsModalOpen] = useState(false);
14 |
15 | const validateInputs = (): boolean => {
16 | if (filterType === "both" && (!startDate || !endDate)) {
17 | setError("Please select both start and end dates.");
18 | return false;
19 | }
20 |
21 | if (filterType === "loadDate" && !startDate) {
22 | setError("Please select a load date.");
23 | return false;
24 | }
25 |
26 | if (filterType === "returnDate" && !endDate) {
27 | setError("Please select a return date.");
28 | return false;
29 | }
30 |
31 | if (startDate && endDate && new Date(startDate) > new Date(endDate)) {
32 | setError("Start date cannot be later than end date.");
33 | return false;
34 | }
35 |
36 | setError("");
37 | return true;
38 | };
39 |
40 | const handleDateFilter = () => {
41 | if (validateInputs()) {
42 | console.log("Validation passed. Applying filter...");
43 | onApplyFilter(startDate, endDate, filterType);
44 | setIsModalOpen(false);
45 | }
46 | };
47 |
48 | const handleAllData = () => {
49 | setStartDate("");
50 | setEndDate("");
51 | onApplyFilter("", "", "");
52 | };
53 |
54 | const isApplyButtonDisabled = (): boolean => {
55 | if (filterType === "") {
56 | return false;
57 | }
58 |
59 | if (filterType === "both" && (!startDate || !endDate)) return true;
60 |
61 | if (filterType === "loadDate" && !startDate) return true;
62 |
63 | if (filterType === "returnDate" && !endDate) return true;
64 |
65 | if (startDate && endDate && new Date(startDate) > new Date(endDate))
66 | return true;
67 |
68 | return false;
69 | };
70 |
71 | return (
72 |
73 |
86 |
87 | {isModalOpen && (
88 |
89 |
90 |
91 |
107 |
108 |
109 | {(filterType === "loadDate" || filterType === "both") && (
110 |
111 |
114 | setStartDate(e.target.value)}
120 | />
121 |
122 | )}
123 |
124 | {(filterType === "returnDate" || filterType === "both") && (
125 |
126 |
129 | setEndDate(e.target.value)}
135 | />
136 |
137 | )}
138 |
139 | {error &&
{error}
}
140 |
141 |
151 |
152 | )}
153 |
154 | );
155 | };
156 |
157 | export default FilterUser;
158 |
--------------------------------------------------------------------------------
/src/components/AddUser.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { User } from "../types/userType";
3 |
4 | interface AddUserProps {
5 | isModal: boolean;
6 | setModal: (value: boolean) => void;
7 | onAddUser: (user: User) => void;
8 | }
9 |
10 | const AddUser = ({ isModal, setModal, onAddUser }: AddUserProps) => {
11 | const [formData, setFormData] = useState({
12 | name: "",
13 | address: "",
14 | phone: "",
15 | bookTitle: "",
16 | loadDate: "",
17 | returnDate: "",
18 | });
19 |
20 | const [errors, setErrors] = useState({
21 | name: "",
22 | address: "",
23 | phone: "",
24 | bookTitle: "",
25 | loadDate: "",
26 | returnDate: "",
27 | });
28 |
29 | const validateForm = () => {
30 | const newErrors: typeof errors = {
31 | name: formData.name ? "" : "Name is required.",
32 | address: formData.address ? "" : "Address is required.",
33 | phone: formData.phone ? "" : "Phone is required.",
34 | bookTitle: formData.bookTitle ? "" : "Book Title is required.",
35 | loadDate: formData.loadDate ? "" : "Load Date is required.",
36 | returnDate: formData.returnDate ? "" : "Return Date is required.",
37 | };
38 |
39 | setErrors(newErrors);
40 | return Object.values(newErrors).every((error) => error === "");
41 | };
42 |
43 | const handleChange = (field: keyof typeof formData, value: string) => {
44 | setFormData({ ...formData, [field]: value });
45 | if (errors[field]) {
46 | setErrors({ ...errors, [field]: "" });
47 | }
48 | };
49 |
50 | const handleSubmit = (e: React.FormEvent) => {
51 | e.preventDefault();
52 | if (!validateForm()) return;
53 |
54 | const newUser: User = {
55 | id: Date.now(),
56 | ...formData,
57 | phone: parseInt(formData.phone, 10),
58 | };
59 |
60 | onAddUser(newUser);
61 | setFormData({
62 | name: "",
63 | address: "",
64 | phone: "",
65 | bookTitle: "",
66 | loadDate: "",
67 | returnDate: "",
68 | });
69 | setModal(false);
70 | };
71 |
72 | return (
73 |
74 | {isModal && (
75 |
76 |
77 |
Add New User
78 |
155 |
156 |
157 | )}
158 |
159 | );
160 | };
161 |
162 | export default AddUser;
163 |
--------------------------------------------------------------------------------
/src/components/UserList.tsx:
--------------------------------------------------------------------------------
1 | // UserList.tsx
2 | import React, { useEffect, useState } from "react";
3 | import { useSearchParams } from "react-router-dom";
4 | import AddUser from "./AddUser";
5 | import SearchUser from "./SearchUser";
6 | import EditUser from "./EditUser";
7 | import FilterUser from "./FilterUser";
8 | import { User } from "../types/userType";
9 | import {
10 | addUser,
11 | editUser,
12 | deleteUser,
13 | searchUsers,
14 | filterUsersByDate,
15 | } from "../controllers/UserControllers";
16 | import { FaArrowLeft, FaArrowRight } from "react-icons/fa";
17 |
18 | const UserList: React.FC = () => {
19 | const [searchParams, setSearchParams] = useSearchParams();
20 | const [query, setQuery] = useState(searchParams.get("query") || "");
21 | const [page, setPage] = useState(Number(searchParams.get("page")) || 1);
22 | const [startDate, setStartDate] = useState(
23 | searchParams.get("startDate") || ""
24 | );
25 | const [endDate, setEndDate] = useState(searchParams.get("endDate") || "");
26 | const [users, setUsers] = useState([]);
27 | const [editingUser, setEditingUser] = useState(null);
28 | const [showAddModal, setShowAddModal] = useState(false);
29 |
30 | useEffect(() => {
31 | const updateUserList = () => {
32 | const filteredUsers = filterUsersByDate(startDate, endDate);
33 | const searchedUsers = searchUsers(query);
34 | const combinedUsers = filteredUsers.filter((user) =>
35 | searchedUsers.some((searchedUser) => searchedUser.id === user.id)
36 | );
37 | setUsers(combinedUsers);
38 | };
39 | updateUserList();
40 | }, [query, startDate, endDate]);
41 |
42 | useEffect(() => {
43 | const params: Record = { query, page: page.toString() };
44 | if (startDate) params.startDate = startDate;
45 | if (endDate) params.endDate = endDate;
46 | setSearchParams(params);
47 | }, [query, page, startDate, endDate]);
48 |
49 | const handleSearch = (e: React.ChangeEvent) => {
50 | setQuery(e.target.value);
51 | setPage(1);
52 | };
53 |
54 | const handleAddUser = (user: User) => {
55 | try {
56 | addUser(user);
57 | setShowAddModal(false);
58 | setUsers((prev) => [...prev, user]);
59 | } catch (error) {
60 | console.error("Failed to add user", error);
61 | }
62 | };
63 |
64 | const handleEditUser = (user: User) => {
65 | try {
66 | editUser(user);
67 | setEditingUser(null);
68 | setUsers((prev) =>
69 | prev.map((existingUser) =>
70 | existingUser.id === user.id ? user : existingUser
71 | )
72 | );
73 | } catch (error) {
74 | console.error("Failed to edit user", error);
75 | }
76 | };
77 |
78 | const handleDeleteUser = (id: number) => {
79 | try {
80 | deleteUser(id);
81 | setUsers((prev) => prev.filter((user) => user.id !== id));
82 | } catch (error) {
83 | console.error("Failed to delete user", error);
84 | }
85 | };
86 |
87 | const handleDateFilter = (start: string, end: string) => {
88 | setStartDate(start);
89 | setEndDate(end);
90 | setPage(1);
91 | };
92 |
93 | const handlePageChange = (newPage: number) => {
94 | const totalPages = Math.ceil(users.length / 5);
95 | if (newPage > 0 && newPage <= totalPages) {
96 | setPage(newPage);
97 | }
98 | };
99 |
100 | const paginatedUsers = users.slice((page - 1) * 5, page * 5);
101 | const totalPages = Math.ceil(users.length / 5);
102 |
103 | return (
104 |
105 | {showAddModal && (
106 |
setShowAddModal(false)}
109 | onAddUser={handleAddUser}
110 | />
111 | )}
112 |
113 | {editingUser && (
114 | setEditingUser(null)}
117 | user={editingUser}
118 | onEditUser={handleEditUser}
119 | />
120 | )}
121 |
122 |
123 |
129 |
130 |
131 |
132 |
133 |
134 | {paginatedUsers.length ? (
135 |
136 |
137 |
138 |
139 | | No |
140 | Name |
141 | Address |
142 | Phone |
143 | Book Title |
144 | Load Date |
145 | Return Date |
146 | Actions |
147 |
148 |
149 |
150 | {paginatedUsers.map((user, index) => (
151 |
152 | |
153 | {(page - 1) * 5 + index + 1}
154 | |
155 | {user.name} |
156 | {user.address} |
157 | {user.phone} |
158 |
159 | {user.bookTitle}
160 | |
161 |
162 | {user.loadDate}
163 | |
164 |
165 | {user.returnDate}
166 | |
167 |
168 |
174 |
180 | |
181 |
182 | ))}
183 |
184 |
185 |
186 | {totalPages > 1 && (
187 |
188 |
195 |
{page}
196 |
203 |
204 | )}
205 |
206 | ) : (
207 |
208 |
Data Not Found
209 |
210 | )}
211 |
212 | );
213 | };
214 |
215 | export default UserList;
216 |
--------------------------------------------------------------------------------