├── .gitignore ├── README.md ├── app ├── controllers │ ├── PagesController.php │ └── UsersController.php ├── models │ ├── Project.php │ └── User.php ├── routes.php └── views │ ├── about.view.php │ ├── contact.view.php │ ├── error.view.php │ ├── index.view.php │ ├── partials │ ├── footer.php │ ├── header.php │ └── nav.php │ ├── user.view.php │ └── users.view.php ├── composer.json ├── config.php.example ├── core ├── App.php ├── Request.php ├── Router.php ├── bootstrap.php ├── database │ ├── Connection.php │ ├── Model.php │ └── QueryBuilder.php ├── helpers.php └── logger │ ├── LogToDatabase.php │ ├── LogToFile.php │ └── Logger.php ├── logs └── .gitignore ├── people.sql ├── public ├── .htaccess ├── css │ ├── bootstrap.min.css │ └── main.css ├── favicon.ico ├── img │ └── logo.png ├── index.php └── js │ ├── bootstrap.min.js │ ├── dark-toggle.js │ ├── jquery.min.js │ ├── main.js │ └── popper.min.js └── tests ├── Feature └── FeatureTest.php ├── TestCase.php └── Unit └── UnitTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | vendor/** 3 | config.php 4 | .idea/** 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple-PHP-MVC-Framework 2 | This is a simple PHP MVC framework. Made in 2017, it uses Composer and is designed to be very simple to use. Inspired by Laravel. 3 | 4 | # Requirements 5 | * PHP >= 7.3.0 6 | * MySQL >= 5.6.10 7 | * Composer 8 | 9 | # How to Install 10 | Note: It is recommended that you install LAMP with my LAMP install script to ensure that you have all of the requirements. 11 | 12 | My LAMP install script is located at 13 | 14 | https://github.com/kenstuddy/Deploy-LAMP 15 | 16 | Clone the repository 17 | 18 | ``` 19 | git clone https://github.com/kenstuddy/Simple-PHP-MVC-Framework 20 | ``` 21 | 22 | Change to the repository directory 23 | 24 | ``` 25 | cd Simple-PHP-MVC-Framework 26 | ``` 27 | 28 | Run composer to install any PHP dependencies 29 | 30 | ``` 31 | composer install 32 | ``` 33 | 34 | Change to the public directory 35 | 36 | ``` 37 | cd public 38 | ``` 39 | 40 | Start the PHP server (0.0.0.0 is the default route, this makes PHP listen on all IPv4 interfaces) 41 | 42 | ``` 43 | php -S 0.0.0.0:8000 44 | ``` 45 | 46 | Visit the IP address (127.0.0.1 if you are running Linux natively, or the IP address of your VM/VPS/etc) http://127.0.0.1:8000 in your web browser. 47 | 48 | To run the included unit tests, make sure you are still in the public directory, and then type the following command 49 | 50 | ``` 51 | ../vendor/bin/phpunit ../tests 52 | ``` 53 | -------------------------------------------------------------------------------- /app/controllers/PagesController.php: -------------------------------------------------------------------------------- 1 | 40 | -------------------------------------------------------------------------------- /app/controllers/UsersController.php: -------------------------------------------------------------------------------- 1 | count(); 22 | $paginationConfig = App::Config()['pagination']; 23 | $limit = $paginationConfig['per_page'] ?? 5; 24 | $page = $vars['page'] ?? 1; 25 | $offset = ($page - 1) * $limit; 26 | $users = $user->where([['user_id', '>', '0']], $limit, $offset)->get(); 27 | return view('users', compact('users', 'count', 'page', 'limit')); 28 | } 29 | 30 | /* 31 | * This function selects a the user from the users database and then grabs the user view to display them. 32 | */ 33 | public function show($vars) 34 | { 35 | //Here we use the Query Builder to get the user: 36 | /*$user = App::DB()->selectAllWhere('users', [ 37 | ['user_id', '=', $vars['id']], 38 | ]); 39 | */ 40 | 41 | //Here we use the ORM to get the user: 42 | $user = new User(); 43 | $foundUser = $user->find($vars['id']); 44 | $user = $foundUser ? $foundUser->get() : []; 45 | 46 | if (empty($user)) { 47 | redirect('users'); 48 | } 49 | return view('user', compact('user')); 50 | } 51 | 52 | /* 53 | * This function inserts a new user into our database using array notation. 54 | */ 55 | public function store() 56 | { 57 | App::DB()->insert('users', [ 58 | 'name' => $_POST['name'] 59 | ]); 60 | $paginationConfig = App::Config()['pagination']; 61 | if ($paginationConfig['show_latest_page_on_add']) { 62 | $totalRecords = App::DB()->count('users'); 63 | $recordsPerPage = $paginationConfig['per_page'] ?? 5; 64 | $lastPage = ceil($totalRecords / $recordsPerPage); 65 | return redirect('users/' . $lastPage); 66 | } else { 67 | return redirect('users'); 68 | } 69 | } 70 | 71 | /* 72 | * This function updates a user from our database using array notation. 73 | */ 74 | public function update($vars) 75 | { 76 | App::DB()->updateWhere('users', [ 77 | 'name' => $_POST['name'] 78 | ], [ 79 | ['user_id', '=', $vars['id']] 80 | ]); 81 | return redirect('user/' . $vars['id']); 82 | } 83 | 84 | /* 85 | * This function deletes a user from our database. 86 | */ 87 | public function delete($vars) 88 | { 89 | App::DB()->deleteWhere('users', [ 90 | ['user_id', '=', $vars['id']] 91 | ]); 92 | $paginationConfig = App::Config()['pagination']; 93 | if ($paginationConfig['show_latest_page_on_delete']) { 94 | $currentPage = $_GET['page'] ?? 1; 95 | $recordsPerPage = $paginationConfig['per_page'] ?? 5; 96 | $totalRecordsAfterDeletion = App::DB()->count('users'); 97 | $lastPageAfterDeletion = max(ceil($totalRecordsAfterDeletion / $recordsPerPage), 1); 98 | if ($currentPage > $lastPageAfterDeletion) { 99 | $redirectPage = $lastPageAfterDeletion; 100 | } else { 101 | $redirectPage = $currentPage; 102 | } 103 | return redirect('users/' . $redirectPage); 104 | } else { 105 | return redirect('users'); 106 | } 107 | } 108 | } 109 | 110 | ?> 111 | -------------------------------------------------------------------------------- /app/models/Project.php: -------------------------------------------------------------------------------- 1 | 20 | -------------------------------------------------------------------------------- /app/models/User.php: -------------------------------------------------------------------------------- 1 | 21 | -------------------------------------------------------------------------------- /app/routes.php: -------------------------------------------------------------------------------- 1 | getArray([ 6 | '' => 'PagesController@home', 7 | 'about' => 'PagesController@about', 8 | 'contact' => 'PagesController@contact', 9 | 'users' => 'UsersController@index', 10 | 'users/{page}' => 'UsersController@index', 11 | 'user/{id}' => 'UsersController@show', 12 | 'user/delete/{id}' => 'UsersController@delete' 13 | ]); 14 | $router->postArray([ 15 | 'users' => 'UsersController@store', 16 | 'user/update/{id}' => 'UsersController@update', 17 | ]); 18 | } 19 | else { 20 | $router->get('', 'PagesController@home'); 21 | $router->get('about', 'PagesController@about'); 22 | $router->get('contact', 'PagesController@contact'); 23 | 24 | $router->get('users', 'UsersController@index'); 25 | $router->get('users/{page}', 'UsersController@index'); 26 | $router->get('user/{id}', 'UsersController@show'); 27 | $router->get('user/delete/{id}', 'UsersController@delete'); 28 | $router->post('users', 'UsersController@store'); 29 | $router->post('user/update/{id}', 'UsersController@update'); 30 | } 31 | 32 | ?> 33 | -------------------------------------------------------------------------------- /app/views/about.view.php: -------------------------------------------------------------------------------- 1 | 2 |

About

3 |

Simple PHP MVC Framework is a framework created by Ken Studdy in 2017. It is inspired by Laravel, and has a query builder, ORM, pagination, a lightweight templating system, and more. You can find the source code for it here.

4 | 5 | -------------------------------------------------------------------------------- /app/views/contact.view.php: -------------------------------------------------------------------------------- 1 | 2 |

Contact

3 |

If you'd like to contact , please feel free to use my contact form on or email me at

4 | 5 | -------------------------------------------------------------------------------- /app/views/error.view.php: -------------------------------------------------------------------------------- 1 | 2 |

An Error Has Occurred

3 |

You have encountered an error. Please click here to go to the home page.

4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/views/index.view.php: -------------------------------------------------------------------------------- 1 | 2 |

Home Page

3 |

Welcome to the Simple PHP MVC Framework! Simple PHP MVC Framework is a fast PHP MVC framework designed to allow for easy creation of powerful websites and web applications. It is inspired by Laravel, and is designed to be easy to use.

4 | 5 | 6 | -------------------------------------------------------------------------------- /app/views/partials/footer.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/views/partials/header.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Simple PHP MVC Framework 8 | 9 | 10 | 11 | 12 | 13 | > 14 | 15 |
16 | 17 | -------------------------------------------------------------------------------- /app/views/partials/nav.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/user.view.php: -------------------------------------------------------------------------------- 1 | 2 |

Update name ?>'s name:

3 |
4 |
5 | 6 | 7 |
8 |
9 |

Viewing user name ?>

10 | 13 | 14 | -------------------------------------------------------------------------------- /app/views/users.view.php: -------------------------------------------------------------------------------- 1 | 2 |

Submit A Name:

3 |
4 |
5 | 6 | 7 |
8 |
9 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "autoload": { 3 | "classmap": [ 4 | "./" 5 | ] 6 | }, 7 | "require-dev": { 8 | "phpunit/phpunit": "^9" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /config.php.example: -------------------------------------------------------------------------------- 1 | [ 8 | 'name' => 'people', 9 | 'username' => 'person', 10 | 'password' => 'password', 11 | 'connection' => 'mysql:host=127.0.0.1', 12 | 'options' => [ 13 | PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, 14 | PDO::ATTR_CASE => PDO::CASE_NATURAL 15 | ] 16 | ], 17 | 'options' => [ 18 | 'debug' => true, 19 | 'production' => false, 20 | 'array_routing' => false 21 | ], 22 | 'pagination' => [ 23 | 'per_page' => 5, 24 | 'show_first_last' => true, 25 | 'show_latest_page_on_add' => true, 26 | 'show_latest_page_on_delete' => true, 27 | ] 28 | ] 29 | ?> 30 | -------------------------------------------------------------------------------- /core/App.php: -------------------------------------------------------------------------------- 1 | info($data); 41 | } 42 | 43 | public static function logError($data, Logger $logger = null): bool 44 | { 45 | $logger = $logger ?: new LogToFile(); 46 | return $logger->error($data); 47 | } 48 | } 49 | 50 | ?> 51 | -------------------------------------------------------------------------------- /core/Request.php: -------------------------------------------------------------------------------- 1 | 22 | -------------------------------------------------------------------------------- /core/Router.php: -------------------------------------------------------------------------------- 1 | [], 12 | 'POST' => [] 13 | 14 | ]; 15 | /* 16 | * This function loads the routes from a file. In this framework, the routes are stored in app/routes.php. 17 | */ 18 | public static function load($file) 19 | { 20 | $router = new static; 21 | 22 | require $file; 23 | 24 | return $router; 25 | 26 | } 27 | 28 | /* 29 | * This function gets the GET route based on the URI and passes it off to the controller. 30 | */ 31 | public function get($uri, $controller) 32 | { 33 | $this->routes['GET'][$uri] = $controller; 34 | } 35 | /* 36 | * This function gets the POST route based on the URI and passes it off to the controller. 37 | */ 38 | public function post($uri, $controller) 39 | { 40 | $this->routes['POST'][$uri] = $controller; 41 | } 42 | /* 43 | * This function using array notation routing gets the GET routes. PHP does not support function overloading (also known as method overloading in OOP), so we cannot name this function get even though it has a different number of parameters than the get function used for routing without array notation. 44 | */ 45 | public function getArray($routes) 46 | { 47 | $this->routes['GET'] = $routes; 48 | } 49 | /* 50 | * This function using array notation routing gets the POST routes. PHP does not support function overloading (also known as method overloading in OOP), so we cannot name this function post even though it has a different number of parameters than the post function used for routing without array notation. 51 | */ 52 | public function postArray($routes) 53 | { 54 | $this->routes['POST'] = $routes; 55 | } 56 | 57 | /* 58 | * This function directs the user to the route based on the request type. 59 | */ 60 | public function direct($uri, $requestType) 61 | { 62 | if (array_key_exists($uri, $this->routes[$requestType])) { 63 | return $this->callAction( 64 | ...explode('@', $this->routes[$requestType][$uri]) 65 | ); 66 | } 67 | 68 | foreach ($this->routes[$requestType] as $key => $value) { 69 | $pattern = preg_replace('#\(/\)#', '/?', $key); 70 | $pattern = "@^" . preg_replace('/{([\w\-]+)}/', '(?<$1>[\w\-]+)', $pattern) . "$@D"; 71 | preg_match($pattern, $uri, $matches); 72 | array_shift($matches); 73 | if ($matches) { 74 | $action = explode('@', $value); 75 | return $this->callAction($action[0], $action[1], $matches); 76 | } 77 | } 78 | 79 | throw new Exception('No route defined for this URI.'); 80 | } 81 | /* 82 | * This function calls the controller for an action. 83 | */ 84 | protected function callAction($controller, $action, $vars = []) 85 | { 86 | $controller = "App\\Controllers\\{$controller}"; 87 | 88 | $controller = new $controller; 89 | 90 | if (!method_exists($controller, $action)) 91 | { 92 | throw new Exception("{$controller} does not respond to the {$action} action."); 93 | } 94 | 95 | return $controller->$action($vars); 96 | } 97 | } 98 | 99 | 100 | ?> 101 | -------------------------------------------------------------------------------- /core/bootstrap.php: -------------------------------------------------------------------------------- 1 | 22 | -------------------------------------------------------------------------------- /core/database/Connection.php: -------------------------------------------------------------------------------- 1 | $e->getMessage()]); 25 | } 26 | return view('error'); 27 | } 28 | } 29 | } 30 | 31 | 32 | 33 | ?> 34 | -------------------------------------------------------------------------------- /core/database/Model.php: -------------------------------------------------------------------------------- 1 | setClassName(get_class($this))->getSql(); 63 | } 64 | 65 | /** 66 | * This method finds one or more rows in the database based off of ID and binds it to the Model, or returns null if no rows are found. 67 | * @param $id 68 | * @return $this 69 | * @throws Exception 70 | */ 71 | public function find($id): ?Model 72 | { 73 | $this->cols = App::DB()->setClassName(get_class($this))->describe(static::$table); 74 | $this->rows = App::DB()->setClassName(get_class($this))->selectAllWhere(static::$table, [[$this->cols[0]->Field, '=', $id]]); 75 | return !empty($this->rows) ? $this : null; 76 | } 77 | 78 | /** 79 | * This method finds one or more rows in the database based off of ID and binds it to the Model, or throws an exception if no rows are found. 80 | * @param $id 81 | * @return $this 82 | * @throws Exception 83 | */ 84 | public function findOrFail($id): Model 85 | { 86 | $this->cols = App::DB()->setClassName(get_class($this))->describe(static::$table); 87 | $this->rows = App::DB()->setClassName(get_class($this))->selectAllWhere(static::$table, [[$this->cols[0]->Field, '=', $id]]); 88 | if (!empty($this->rows)) { 89 | return $this; 90 | } 91 | throw new RuntimeException("ModelNotFoundException"); 92 | } 93 | 94 | /** 95 | * This method finds one or more rows matching specific criteria in the database and binds it to the Model, then returns the Model. 96 | * @param $where 97 | * @return $this 98 | * @throws Exception 99 | */ 100 | public function where($where, $limit = "", $offset = ""): Model 101 | { 102 | $this->cols = App::DB()->setClassName(get_class($this))->describe(static::$table); 103 | $this->rows = App::DB()->setClassName(get_class($this))->selectAllWhere(static::$table, $where, $limit, $offset); 104 | return $this; 105 | } 106 | 107 | /** 108 | * This method returns the count of the rows for a database query. 109 | * @param $where 110 | * @return int|bool 111 | * @throws Exception 112 | */ 113 | public function count($where = "") 114 | { 115 | if (!empty($where)) { 116 | return App::DB()->setClassName(get_class($this))->countWhere(static::$table, $where); 117 | } 118 | return App::DB()->setClassName(get_class($this))->count(static::$table); 119 | } 120 | 121 | /** 122 | * This method adds the row to the database and binds it to the model. 123 | * @param $columns 124 | * @return $this 125 | * @throws Exception 126 | */ 127 | public function add($columns): Model 128 | { 129 | $this->id = App::DB()->insert(static::$table, $columns); 130 | $this->cols = App::DB()->setClassName(get_class($this))->describe(static::$table); 131 | $this->rows = App::DB()->setClassName(get_class($this))->selectAllWhere(static::$table, [[$this->cols[0]->Field, '=', $this->id]]); 132 | return $this; 133 | } 134 | 135 | /** 136 | * This method updates one or more rows in the database. 137 | * @param $parameters 138 | * @return int 139 | * @throws Exception 140 | */ 141 | public function update($parameters): int 142 | { 143 | return App::DB()->update(static::$table, $parameters); 144 | } 145 | 146 | /** 147 | * This method updates one or more rows in the database matching specific criteria. 148 | * @param $parameters 149 | * @param $where 150 | * @return int 151 | * @throws Exception 152 | */ 153 | public function updateWhere($parameters, $where): int 154 | { 155 | return App::DB()->updateWhere(static::$table, $parameters, $where); 156 | } 157 | 158 | /** 159 | * This method deletes one or more rows from the database. 160 | * @return int 161 | * @throws Exception 162 | */ 163 | public function delete(): int 164 | { 165 | return App::DB()->delete(static::$table); 166 | } 167 | 168 | /** 169 | * This method deletes one or more rows from the database matching specific criteria. 170 | * @param $where 171 | * @return int 172 | * @throws Exception 173 | */ 174 | public function deleteWhere($where): int 175 | { 176 | return App::DB()->deleteWhere(static::$table, $where); 177 | } 178 | 179 | /** 180 | * This method updates one or more rows in the database. 181 | * @return $this 182 | * @throws Exception 183 | */ 184 | public function save(): Model 185 | { 186 | $this->cols = App::DB()->setClassName(get_class($this))->describe(static::$table); 187 | $newValues = []; 188 | foreach ($this->cols as $col) { 189 | $newValues[$col->Field] = $this->{$col->Field}; 190 | } 191 | App::DB()->updateWhere(static::$table, $newValues, [[$this->cols[0]->Field, '=', $this->{$this->cols[0]->Field}]]); 192 | return $this; 193 | } 194 | 195 | /** 196 | * This static method returns and binds one or more rows in the database to the model. 197 | * @return Model[]|false 198 | * @throws Exception 199 | */ 200 | public static function all() 201 | { 202 | return App::DB()->setClassName(static::class)->selectAll(static::$table); 203 | } 204 | 205 | /** 206 | * This method fetches all of the rows for the Model. 207 | * @return Model[] 208 | */ 209 | public function get(): array 210 | { 211 | return $this->rows; 212 | } 213 | 214 | /** 215 | * This method fetches all of the columns for the Model. 216 | * This returns the columns if they're cached, otherwise they are fetched again first. 217 | * @return array 218 | */ 219 | public function describe(): array 220 | { 221 | if (!$this->cols) { 222 | $this->cols = App::DB()->setClassName(get_class($this))->describe(static::$table); 223 | } 224 | return $this->cols; 225 | } 226 | 227 | /** 228 | * This method fetches the first row for the Model. 229 | * @return Model|null 230 | */ 231 | public function first(): ?Model 232 | { 233 | return $this->rows[0] ?? null; 234 | } 235 | 236 | /** 237 | * This method fetches the first row for the Model or throws an exception if a row is not found. 238 | * @return Model 239 | * @throws Exception 240 | */ 241 | public function firstOrFail(): Model 242 | { 243 | if (!empty($this->rows[0])) { 244 | return $this->rows[0]; 245 | } 246 | throw new RuntimeException("ModelNotFoundException"); 247 | } 248 | 249 | /** 250 | * This method returns the primary key's value for the Model, or null if it doesn't have one. 251 | * @return string|null 252 | * @throws Exception 253 | */ 254 | public function id(): ?string 255 | { 256 | if (!$this->cols) { 257 | $this->cols = App::DB()->setClassName(get_class($this))->describe(static::$table); 258 | } 259 | return $this->{$this->cols[0]->Field} ?? null; 260 | } 261 | 262 | /** 263 | * This method returns the primary key's name for the Model, or null if it doesn't have one. 264 | * @return string|null 265 | * @throws Exception 266 | */ 267 | public function primary(): ?string 268 | { 269 | if (!$this->cols) { 270 | $this->cols = App::DB()->setClassName(get_class($this))->describe(static::$table); 271 | } 272 | return $this->cols[0]->Field ?? null; 273 | } 274 | } -------------------------------------------------------------------------------- /core/database/QueryBuilder.php: -------------------------------------------------------------------------------- 1 | pdo = $pdo; 38 | } 39 | 40 | /** 41 | * This method returns the PDO instance. 42 | * @return PDO 43 | */ 44 | public function getPdo(): PDO 45 | { 46 | return $this->pdo; 47 | } 48 | 49 | /** 50 | * This method returns the last set SQL query. 51 | * 52 | */ 53 | public function getSql(): string 54 | { 55 | return $this->sql; 56 | } 57 | 58 | /** 59 | * This method sets the class name to bind the Model to. 60 | * @param mixed $class_name 61 | * @return QueryBuilder 62 | */ 63 | public function setClassName($class_name): QueryBuilder 64 | { 65 | $this->class_name = $class_name; 66 | return $this; 67 | } 68 | 69 | /** 70 | * This method selects all of the rows from a table in a database. 71 | * @param string $table 72 | * @param string $limit 73 | * @param string $offset 74 | * @return array|false 75 | * @throws Exception 76 | */ 77 | public function selectAll(string $table, $limit = "", $offset = "") 78 | { 79 | return $this->select($table, "*", $limit, $offset); 80 | } 81 | 82 | /** 83 | * This method selects rows from a table in a database where one or more conditions are matched. 84 | * @param string $table 85 | * @param $where 86 | * @param string $limit 87 | * @param string $offset 88 | * @return array|false 89 | * @throws Exception 90 | */ 91 | public function selectAllWhere(string $table, $where, $limit = "", $offset = "") 92 | { 93 | return $this->selectWhere($table, "*", $where, $limit, $offset); 94 | } 95 | 96 | /** 97 | * This method returns the number of rows in a table. 98 | * @param string $table 99 | * @return int|bool 100 | * @throws Exception 101 | */ 102 | public function count(string $table) 103 | { 104 | $this->sql = "SELECT COUNT(*) FROM {$table}"; 105 | try { 106 | $statement = $this->pdo->prepare($this->sql); 107 | $statement->execute(); 108 | return $statement->fetchColumn(); 109 | } catch (PDOException $e) { 110 | $this->handlePDOException($e); 111 | } 112 | return false; 113 | } 114 | 115 | /** 116 | * This method returns the number of rows in a table where one or more conditions are matched. 117 | * @param string $table 118 | * @param $where 119 | * @param string $columns 120 | * @return int|bool 121 | * @throws Exception 122 | */ 123 | public function countWhere(string $table, $where) 124 | { 125 | $where = $this->prepareWhere($where); 126 | $mapped_wheres = $this->prepareMappedWheres($where); 127 | $where = array_column($where, 3); 128 | $this->sql = "SELECT COUNT(*) FROM {$table} WHERE {$mapped_wheres}"; 129 | try { 130 | $statement = $this->pdo->prepare($this->sql); 131 | $statement->execute($where); 132 | return $statement->fetchColumn(); 133 | } catch (PDOException $e) { 134 | $this->handlePDOException($e); 135 | } 136 | return false; 137 | } 138 | 139 | /** 140 | * This method selects rows from a table in a database. 141 | * @param string $table 142 | * @param string $columns 143 | * @param string $limit 144 | * @param string $offset 145 | * @return array|false 146 | * @throws Exception 147 | */ 148 | public function select(string $table, string $columns, $limit = "", $offset = "") 149 | { 150 | $limit = $this->prepareLimit($limit); 151 | $offset = $this->prepareOffset($offset); 152 | $this->sql = "SELECT {$columns} FROM {$table} {$limit} {$offset}"; 153 | try { 154 | $statement = $this->pdo->prepare($this->sql); 155 | $statement->execute(); 156 | return $statement->fetchAll(PDO::FETCH_CLASS, $this->class_name ?: "stdClass"); 157 | } catch (PDOException $e) { 158 | $this->handlePDOException($e); 159 | } 160 | return false; 161 | } 162 | 163 | /** 164 | * This method selects rows from a table in a database where one or more conditions are matched. 165 | * @param string $table 166 | * @param string $columns 167 | * @param $where 168 | * @param string $limit 169 | * @param string $offset 170 | * @return array|false 171 | * @throws Exception 172 | */ 173 | public function selectWhere(string $table, string $columns, $where, $limit = "", $offset = "") 174 | { 175 | $limit = $this->prepareLimit($limit); 176 | $offset = $this->prepareOffset($offset); 177 | $where = $this->prepareWhere($where); 178 | $mapped_wheres = $this->prepareMappedWheres($where); 179 | $where = array_column($where, 3); 180 | $this->sql = "SELECT {$columns} FROM {$table} WHERE {$mapped_wheres} {$limit} {$offset}"; 181 | try { 182 | $statement = $this->pdo->prepare($this->sql); 183 | $statement->execute($where); 184 | return $statement->fetchAll(PDO::FETCH_CLASS, $this->class_name ?: "stdClass"); 185 | } catch (PDOException $e) { 186 | $this->handlePDOException($e); 187 | } 188 | return false; 189 | } 190 | 191 | /** 192 | * This method deletes rows from a table in a database. 193 | * @param string $table 194 | * @param string $limit 195 | * @return int 196 | * @throws Exception 197 | */ 198 | public function delete(string $table, $limit = ""): int 199 | { 200 | $limit = $this->prepareLimit($limit); 201 | $this->sql = "DELETE FROM {$table} {$limit}"; 202 | try { 203 | $statement = $this->pdo->prepare($this->sql); 204 | $statement->execute(); 205 | return $statement->rowCount(); 206 | } catch (PDOException $e) { 207 | $this->handlePDOException($e); 208 | } 209 | return 0; 210 | } 211 | 212 | 213 | /** 214 | * This method deletes rows from a table in a database where one or more conditions are matched. 215 | * @param string $table 216 | * @param $where 217 | * @param string $limit 218 | * @return int 219 | * @throws Exception 220 | */ 221 | public function deleteWhere(string $table, $where, $limit = ""): int 222 | { 223 | $limit = $this->prepareLimit($limit); 224 | $where = $this->prepareWhere($where); 225 | $mapped_wheres = $this->prepareMappedWheres($where); 226 | $where = array_column($where, 3); 227 | $this->sql = "DELETE FROM {$table} WHERE {$mapped_wheres} {$limit}"; 228 | try { 229 | $statement = $this->pdo->prepare($this->sql); 230 | $statement->execute($where); 231 | return $statement->rowCount(); 232 | } catch (PDOException $e) { 233 | $this->handlePDOException($e); 234 | } 235 | return 0; 236 | } 237 | 238 | /** 239 | * This method inserts data into a table in a database. 240 | * @param string $table 241 | * @param $parameters 242 | * @return string 243 | * @throws Exception 244 | */ 245 | public function insert(string $table, $parameters): string 246 | { 247 | $names = $this->prepareCommaSeparatedColumnNames($parameters); 248 | $values = $this->prepareCommaSeparatedColumnValues($parameters); 249 | $this->sql = sprintf( 250 | 'INSERT INTO %s (%s) VALUES (%s)', 251 | $table, 252 | $names, 253 | $values 254 | ); 255 | try { 256 | $statement = $this->pdo->prepare($this->sql); 257 | $statement->execute($parameters); 258 | return $this->pdo->lastInsertId(); 259 | } catch (PDOException $e) { 260 | $this->handlePDOException($e); 261 | } 262 | return ""; 263 | } 264 | 265 | /** 266 | * This method updates data in a table in a database. 267 | * @param string $table 268 | * @param $parameters 269 | * @param string $limit 270 | * @return int 271 | * @throws Exception 272 | */ 273 | public function update(string $table, $parameters, $limit = ""): int 274 | { 275 | $limit = $this->prepareLimit($limit); 276 | $set = $this->prepareNamed($parameters); 277 | $this->sql = sprintf( 278 | 'UPDATE %s SET %s %s', 279 | $table, 280 | $set, 281 | $limit 282 | ); 283 | try { 284 | $statement = $this->pdo->prepare($this->sql); 285 | $statement->execute($parameters); 286 | return $statement->rowCount(); 287 | } catch (PDOException $e) { 288 | $this->handlePDOException($e); 289 | } 290 | return 0; 291 | } 292 | 293 | /** 294 | * This method updates data in a table in a database where one or more conditions are matched. 295 | * @param string $table 296 | * @param $parameters 297 | * @param $where 298 | * @param string $limit 299 | * @return int 300 | * @throws Exception 301 | */ 302 | public function updateWhere(string $table, $parameters, $where, $limit = ""): int 303 | { 304 | $limit = $this->prepareLimit($limit); 305 | $set = $this->prepareUnnamed($parameters); 306 | $parameters = $this->prepareParameters($parameters); 307 | $where = $this->prepareWhere($where); 308 | $mapped_wheres = $this->prepareMappedWheres($where); 309 | $where = array_column($where, 3); 310 | $this->sql = sprintf( 311 | 'UPDATE %s SET %s WHERE %s %s', 312 | $table, 313 | $set, 314 | $mapped_wheres, 315 | $limit 316 | ); 317 | try { 318 | $statement = $this->pdo->prepare($this->sql); 319 | $statement->execute(array_merge($parameters, $where)); 320 | return $statement->rowCount(); 321 | } catch (PDOException $e) { 322 | $this->handlePDOException($e); 323 | } 324 | return 0; 325 | } 326 | 327 | /** 328 | * This method selects all of the rows from a table in a database. 329 | * @param string $table 330 | * @return array|int 331 | * @throws Exception 332 | */ 333 | public function describe(string $table) 334 | { 335 | $this->sql = "DESCRIBE {$table}"; 336 | try { 337 | $statement = $this->pdo->prepare($this->sql); 338 | $statement->execute(); 339 | return $statement->fetchAll(PDO::FETCH_CLASS, $this->class_name ?: "stdClass"); 340 | } catch (PDOException $e) { 341 | $this->handlePDOException($e); 342 | } 343 | return 0; 344 | } 345 | 346 | /** 347 | * This method executes raw SQL against a database. 348 | * @param string $sql 349 | * @param array $parameters 350 | * @return array|int 351 | * @throws Exception 352 | */ 353 | public function raw(string $sql, array $parameters = []) 354 | { 355 | try { 356 | $this->sql = $sql; 357 | $statement = $this->pdo->prepare($sql); 358 | $statement->execute($parameters); 359 | $output = $statement->rowCount(); 360 | if (stripos($sql, "SELECT") === 0) { 361 | $output = $statement->fetchAll(PDO::FETCH_CLASS, $this->class_name ?: "stdClass"); 362 | } 363 | return $output; 364 | } catch (PDOException $e) { 365 | $this->handlePDOException($e); 366 | } 367 | return 0; 368 | } 369 | 370 | /** 371 | * This method prepares the where clause array for the query builder. 372 | * @param $where 373 | * @return mixed 374 | */ 375 | private function prepareWhere($where) 376 | { 377 | $array = $where; 378 | foreach ($where as $key => $value) { 379 | if (count($value) < 4) { 380 | array_unshift($array[$key], 0); 381 | } 382 | } 383 | return $array; 384 | } 385 | 386 | /** 387 | * This method prepares the limit statement for the query builder. 388 | * @param $limit 389 | * @return string 390 | */ 391 | private function prepareLimit($limit): string 392 | { 393 | return (!empty($limit) ? " LIMIT " . $limit : ""); 394 | } 395 | 396 | /** 397 | * This method prepares the offset statement for the query builder. 398 | * @param $offset 399 | * @return string 400 | */ 401 | private function prepareOffset($offset): string 402 | { 403 | return (!empty($offset) ? " OFFSET " . $offset : ""); 404 | } 405 | 406 | /** 407 | * This method prepares the comma separated names for the query builder. 408 | * @param $parameters 409 | * @return string 410 | */ 411 | private function prepareCommaSeparatedColumnNames($parameters): string 412 | { 413 | return implode(', ', array_keys($parameters)); 414 | } 415 | 416 | /** 417 | * This method prepares the comma separated values for the query builder. 418 | * @param $parameters 419 | * @return string 420 | */ 421 | private function prepareCommaSeparatedColumnValues($parameters): string 422 | { 423 | return ':' . implode(', :', array_keys($parameters)); 424 | } 425 | 426 | /** 427 | * This method prepares the mapped wheres. 428 | * @param $where 429 | * @return string 430 | */ 431 | private function prepareMappedWheres($where): string 432 | { 433 | $mapped_wheres = ''; 434 | foreach ($where as $clause) { 435 | $modifier = $mapped_wheres === '' ? '' : $clause[0]; 436 | $mapped_wheres .= " {$modifier} {$clause[1]} {$clause[2]} ?"; 437 | } 438 | return $mapped_wheres; 439 | } 440 | 441 | /** 442 | * This method prepares the unnamed columns. 443 | * @param $parameters 444 | * @return string 445 | */ 446 | private function prepareUnnamed($parameters): string 447 | { 448 | return implode(', ', array_map( 449 | static function ($property) { 450 | return "{$property} = ?"; 451 | }, 452 | array_keys($parameters) 453 | )); 454 | } 455 | 456 | /** 457 | * This method prepares the named columns. 458 | * @param $parameters 459 | * @return string 460 | */ 461 | private function prepareNamed($parameters): string 462 | { 463 | return implode(', ', array_map( 464 | static function ($property) { 465 | return "{$property} = :{$property}"; 466 | }, 467 | array_keys($parameters) 468 | )); 469 | } 470 | 471 | /** 472 | * This method prepares the parameters with numeric keys. 473 | * @param $parameters 474 | * @param int $counter 475 | * @return mixed 476 | */ 477 | private function prepareParameters($parameters, $counter = 1) 478 | { 479 | foreach ($array = $parameters as $key => $value) { 480 | unset($parameters[$key]); 481 | $parameters[$counter] = $value; 482 | $counter++; 483 | } 484 | return $parameters; 485 | } 486 | 487 | /** 488 | * This method binds values from an array to the PDOStatement. 489 | * @param PDOStatement $PDOStatement 490 | * @param $array 491 | * @param int $counter 492 | */ 493 | private function prepareBindings(PDOStatement $PDOStatement, $array, $counter = 1): void 494 | { 495 | foreach ($array as $key => $value) { 496 | $PDOStatement->bindParam($counter, $value); 497 | $counter++; 498 | } 499 | } 500 | 501 | /** 502 | * This method handles PDO exceptions. 503 | * @param PDOException $e 504 | * @return mixed 505 | * @throws Exception 506 | */ 507 | private function handlePDOException(PDOException $e) 508 | { 509 | App::logError('There was a PDO Exception. Details: ' . $e); 510 | if (App::get('config')['options']['debug']) { 511 | return view('error', ['error' => $e->getMessage()]); 512 | } 513 | return view('error'); 514 | } 515 | } 516 | 517 | ?> -------------------------------------------------------------------------------- /core/helpers.php: -------------------------------------------------------------------------------- 1 | "; 36 | var_dump($value); 37 | echo ""; 38 | } 39 | 40 | /* 41 | * This function is used for generating pagination links. 42 | */ 43 | function paginate($table, $page, $limit, $count) 44 | { 45 | $totalPages = ceil($count / $limit); 46 | $offset = ($page - 1) * $limit; 47 | $output = ""; 48 | 49 | $showFirstLast = App::Config()['pagination']['show_first_last']; 50 | 51 | if ($showFirstLast && $page > 1) { 52 | $output .= "First "; 53 | } 54 | 55 | if ($page > 1) { 56 | $prev = $page - 1; 57 | $output .= "Prev "; 58 | } 59 | 60 | $output .= " Page $page "; 61 | 62 | if ($count > ($offset + $limit)) { 63 | $next = $page + 1; 64 | $output .= "Next "; 65 | } 66 | 67 | if ($showFirstLast && $page < $totalPages) { 68 | $output .= "Last"; 69 | } 70 | 71 | $output .= ""; 72 | return $output; 73 | } 74 | 75 | /* 76 | * This function displays a session variable's value if it exists. 77 | */ 78 | function session($name) { 79 | return $_SESSION[$name] ?? ""; 80 | } 81 | 82 | /* 83 | * This function displays a session variable's value and unsets it if it exists. 84 | */ 85 | function session_once($name) { 86 | if (isset($_SESSION[$name])) { 87 | $value = $_SESSION[$name]; 88 | unset($_SESSION[$name]); 89 | return $value; 90 | } 91 | return ""; 92 | } 93 | 94 | /* 95 | * This function enables displaying of errors in the web browser. 96 | */ 97 | function display_errors() 98 | { 99 | ini_set('display_errors', 1); 100 | ini_set('display_startup_errors', 1); 101 | error_reporting(E_ALL); 102 | } 103 | 104 | ?> 105 | -------------------------------------------------------------------------------- /core/logger/LogToDatabase.php: -------------------------------------------------------------------------------- 1 | log($data, "info.log"); 13 | } 14 | 15 | public function error($data): bool 16 | { 17 | return $this->log($data, "error.log"); 18 | } 19 | 20 | private function log($data, $filename = "log.log"): bool 21 | { 22 | if (!file_exists("../logs/") && (!mkdir("../logs/", 0777, true) && !is_dir($filename))) { 23 | throw new RuntimeException(sprintf('Folder "%s" was not created', $filename)); 24 | } 25 | return file_put_contents("../logs/" . $filename, date("Y-m-d h:i:sa") . " " . $data . "\n", FILE_APPEND); 26 | } 27 | } -------------------------------------------------------------------------------- /core/logger/Logger.php: -------------------------------------------------------------------------------- 1 | 2 | RewriteEngine On 3 | 4 | RewriteCond %{REQUEST_FILENAME} !-d 5 | RewriteCond %{REQUEST_URI} (.+)/$ 6 | RewriteRule ^ %1 [L,R=301] 7 | 8 | RewriteCond %{REQUEST_FILENAME} !-d 9 | RewriteCond %{REQUEST_FILENAME} !-f 10 | RewriteRule ^ index.php [L] 11 | 12 | -------------------------------------------------------------------------------- /public/css/main.css: -------------------------------------------------------------------------------- 1 | .text-white-75 { 2 | color: rgba(255, 255, 255, .75) !important; 3 | } 4 | 5 | .home-paragraph { 6 | line-height: 2.2; 7 | } 8 | 9 | .home-logo { 10 | margin-top:60px; 11 | border-radius: 30px; 12 | display: block; 13 | margin-left: auto; 14 | margin-right: auto; 15 | width: 50%; 16 | } 17 | 18 | .navbar-toggler { 19 | margin-bottom: 5px; 20 | } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kenstuddy/Simple-PHP-MVC-Framework/3b4a243b69e6970d98df65c4147c5c768edddb0b/public/favicon.ico -------------------------------------------------------------------------------- /public/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kenstuddy/Simple-PHP-MVC-Framework/3b4a243b69e6970d98df65c4147c5c768edddb0b/public/img/logo.png -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | direct(Request::uri(), Request::method()); 20 | 21 | 22 | ?> 23 | -------------------------------------------------------------------------------- /public/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v5.1.3 (https://getbootstrap.com/) 3 | * Copyright 2011-2021 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) 5 | */ 6 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e(require("@popperjs/core")):"function"==typeof define&&define.amd?define(["@popperjs/core"],e):(t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap=e(t.Popper)}(this,(function(t){"use strict";function e(t){if(t&&t.__esModule)return t;const e=Object.create(null);if(t)for(const i in t)if("default"!==i){const s=Object.getOwnPropertyDescriptor(t,i);Object.defineProperty(e,i,s.get?s:{enumerable:!0,get:()=>t[i]})}return e.default=t,Object.freeze(e)}const i=e(t),s="transitionend",n=t=>{let e=t.getAttribute("data-bs-target");if(!e||"#"===e){let i=t.getAttribute("href");if(!i||!i.includes("#")&&!i.startsWith("."))return null;i.includes("#")&&!i.startsWith("#")&&(i=`#${i.split("#")[1]}`),e=i&&"#"!==i?i.trim():null}return e},o=t=>{const e=n(t);return e&&document.querySelector(e)?e:null},r=t=>{const e=n(t);return e?document.querySelector(e):null},a=t=>{t.dispatchEvent(new Event(s))},l=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),c=t=>l(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?document.querySelector(t):null,h=(t,e,i)=>{Object.keys(i).forEach((s=>{const n=i[s],o=e[s],r=o&&l(o)?"element":null==(a=o)?`${a}`:{}.toString.call(a).match(/\s([a-z]+)/i)[1].toLowerCase();var a;if(!new RegExp(n).test(r))throw new TypeError(`${t.toUpperCase()}: Option "${s}" provided type "${r}" but expected type "${n}".`)}))},d=t=>!(!l(t)||0===t.getClientRects().length)&&"visible"===getComputedStyle(t).getPropertyValue("visibility"),u=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),g=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?g(t.parentNode):null},_=()=>{},f=t=>{t.offsetHeight},p=()=>{const{jQuery:t}=window;return t&&!document.body.hasAttribute("data-bs-no-jquery")?t:null},m=[],b=()=>"rtl"===document.documentElement.dir,v=t=>{var e;e=()=>{const e=p();if(e){const i=t.NAME,s=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=s,t.jQueryInterface)}},"loading"===document.readyState?(m.length||document.addEventListener("DOMContentLoaded",(()=>{m.forEach((t=>t()))})),m.push(e)):e()},y=t=>{"function"==typeof t&&t()},E=(t,e,i=!0)=>{if(!i)return void y(t);const n=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const s=Number.parseFloat(e),n=Number.parseFloat(i);return s||n?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(e)+5;let o=!1;const r=({target:i})=>{i===e&&(o=!0,e.removeEventListener(s,r),y(t))};e.addEventListener(s,r),setTimeout((()=>{o||a(e)}),n)},w=(t,e,i,s)=>{let n=t.indexOf(e);if(-1===n)return t[!i&&s?t.length-1:0];const o=t.length;return n+=i?1:-1,s&&(n=(n+o)%o),t[Math.max(0,Math.min(n,o-1))]},A=/[^.]*(?=\..*)\.|.*/,T=/\..*/,C=/::\d+$/,k={};let L=1;const S={mouseenter:"mouseover",mouseleave:"mouseout"},O=/^(mouseenter|mouseleave)/i,N=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function D(t,e){return e&&`${e}::${L++}`||t.uidEvent||L++}function I(t){const e=D(t);return t.uidEvent=e,k[e]=k[e]||{},k[e]}function P(t,e,i=null){const s=Object.keys(t);for(let n=0,o=s.length;nfunction(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};s?s=t(s):i=t(i)}const[o,r,a]=x(e,i,s),l=I(t),c=l[a]||(l[a]={}),h=P(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&n);const d=D(r,e.replace(A,"")),u=o?function(t,e,i){return function s(n){const o=t.querySelectorAll(e);for(let{target:r}=n;r&&r!==this;r=r.parentNode)for(let a=o.length;a--;)if(o[a]===r)return n.delegateTarget=r,s.oneOff&&$.off(t,n.type,e,i),i.apply(r,[n]);return null}}(t,i,s):function(t,e){return function i(s){return s.delegateTarget=t,i.oneOff&&$.off(t,s.type,e),e.apply(t,[s])}}(t,i);u.delegationSelector=o?i:null,u.originalHandler=r,u.oneOff=n,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function j(t,e,i,s,n){const o=P(e[i],s,n);o&&(t.removeEventListener(i,o,Boolean(n)),delete e[i][o.uidEvent])}function H(t){return t=t.replace(T,""),S[t]||t}const $={on(t,e,i,s){M(t,e,i,s,!1)},one(t,e,i,s){M(t,e,i,s,!0)},off(t,e,i,s){if("string"!=typeof e||!t)return;const[n,o,r]=x(e,i,s),a=r!==e,l=I(t),c=e.startsWith(".");if(void 0!==o){if(!l||!l[r])return;return void j(t,l,r,o,n?i:null)}c&&Object.keys(l).forEach((i=>{!function(t,e,i,s){const n=e[i]||{};Object.keys(n).forEach((o=>{if(o.includes(s)){const s=n[o];j(t,e,i,s.originalHandler,s.delegationSelector)}}))}(t,l,i,e.slice(1))}));const h=l[r]||{};Object.keys(h).forEach((i=>{const s=i.replace(C,"");if(!a||e.includes(s)){const e=h[i];j(t,l,r,e.originalHandler,e.delegationSelector)}}))},trigger(t,e,i){if("string"!=typeof e||!t)return null;const s=p(),n=H(e),o=e!==n,r=N.has(n);let a,l=!0,c=!0,h=!1,d=null;return o&&s&&(a=s.Event(e,i),s(t).trigger(a),l=!a.isPropagationStopped(),c=!a.isImmediatePropagationStopped(),h=a.isDefaultPrevented()),r?(d=document.createEvent("HTMLEvents"),d.initEvent(n,l,!0)):d=new CustomEvent(e,{bubbles:l,cancelable:!0}),void 0!==i&&Object.keys(i).forEach((t=>{Object.defineProperty(d,t,{get:()=>i[t]})})),h&&d.preventDefault(),c&&t.dispatchEvent(d),d.defaultPrevented&&void 0!==a&&a.preventDefault(),d}},B=new Map,z={set(t,e,i){B.has(t)||B.set(t,new Map);const s=B.get(t);s.has(e)||0===s.size?s.set(e,i):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(s.keys())[0]}.`)},get:(t,e)=>B.has(t)&&B.get(t).get(e)||null,remove(t,e){if(!B.has(t))return;const i=B.get(t);i.delete(e),0===i.size&&B.delete(t)}};class R{constructor(t){(t=c(t))&&(this._element=t,z.set(this._element,this.constructor.DATA_KEY,this))}dispose(){z.remove(this._element,this.constructor.DATA_KEY),$.off(this._element,this.constructor.EVENT_KEY),Object.getOwnPropertyNames(this).forEach((t=>{this[t]=null}))}_queueCallback(t,e,i=!0){E(t,e,i)}static getInstance(t){return z.get(c(t),this.DATA_KEY)}static getOrCreateInstance(t,e={}){return this.getInstance(t)||new this(t,"object"==typeof e?e:null)}static get VERSION(){return"5.1.3"}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}}const F=(t,e="hide")=>{const i=`click.dismiss${t.EVENT_KEY}`,s=t.NAME;$.on(document,i,`[data-bs-dismiss="${s}"]`,(function(i){if(["A","AREA"].includes(this.tagName)&&i.preventDefault(),u(this))return;const n=r(this)||this.closest(`.${s}`);t.getOrCreateInstance(n)[e]()}))};class q extends R{static get NAME(){return"alert"}close(){if($.trigger(this._element,"close.bs.alert").defaultPrevented)return;this._element.classList.remove("show");const t=this._element.classList.contains("fade");this._queueCallback((()=>this._destroyElement()),this._element,t)}_destroyElement(){this._element.remove(),$.trigger(this._element,"closed.bs.alert"),this.dispose()}static jQueryInterface(t){return this.each((function(){const e=q.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}F(q,"close"),v(q);const W='[data-bs-toggle="button"]';class U extends R{static get NAME(){return"button"}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}static jQueryInterface(t){return this.each((function(){const e=U.getOrCreateInstance(this);"toggle"===t&&e[t]()}))}}function K(t){return"true"===t||"false"!==t&&(t===Number(t).toString()?Number(t):""===t||"null"===t?null:t)}function V(t){return t.replace(/[A-Z]/g,(t=>`-${t.toLowerCase()}`))}$.on(document,"click.bs.button.data-api",W,(t=>{t.preventDefault();const e=t.target.closest(W);U.getOrCreateInstance(e).toggle()})),v(U);const X={setDataAttribute(t,e,i){t.setAttribute(`data-bs-${V(e)}`,i)},removeDataAttribute(t,e){t.removeAttribute(`data-bs-${V(e)}`)},getDataAttributes(t){if(!t)return{};const e={};return Object.keys(t.dataset).filter((t=>t.startsWith("bs"))).forEach((i=>{let s=i.replace(/^bs/,"");s=s.charAt(0).toLowerCase()+s.slice(1,s.length),e[s]=K(t.dataset[i])})),e},getDataAttribute:(t,e)=>K(t.getAttribute(`data-bs-${V(e)}`)),offset(t){const e=t.getBoundingClientRect();return{top:e.top+window.pageYOffset,left:e.left+window.pageXOffset}},position:t=>({top:t.offsetTop,left:t.offsetLeft})},Y={find:(t,e=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(e,t)),findOne:(t,e=document.documentElement)=>Element.prototype.querySelector.call(e,t),children:(t,e)=>[].concat(...t.children).filter((t=>t.matches(e))),parents(t,e){const i=[];let s=t.parentNode;for(;s&&s.nodeType===Node.ELEMENT_NODE&&3!==s.nodeType;)s.matches(e)&&i.push(s),s=s.parentNode;return i},prev(t,e){let i=t.previousElementSibling;for(;i;){if(i.matches(e))return[i];i=i.previousElementSibling}return[]},next(t,e){let i=t.nextElementSibling;for(;i;){if(i.matches(e))return[i];i=i.nextElementSibling}return[]},focusableChildren(t){const e=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map((t=>`${t}:not([tabindex^="-"])`)).join(", ");return this.find(e,t).filter((t=>!u(t)&&d(t)))}},Q="carousel",G={interval:5e3,keyboard:!0,slide:!1,pause:"hover",wrap:!0,touch:!0},Z={interval:"(number|boolean)",keyboard:"boolean",slide:"(boolean|string)",pause:"(string|boolean)",wrap:"boolean",touch:"boolean"},J="next",tt="prev",et="left",it="right",st={ArrowLeft:it,ArrowRight:et},nt="slid.bs.carousel",ot="active",rt=".active.carousel-item";class at extends R{constructor(t,e){super(t),this._items=null,this._interval=null,this._activeElement=null,this._isPaused=!1,this._isSliding=!1,this.touchTimeout=null,this.touchStartX=0,this.touchDeltaX=0,this._config=this._getConfig(e),this._indicatorsElement=Y.findOne(".carousel-indicators",this._element),this._touchSupported="ontouchstart"in document.documentElement||navigator.maxTouchPoints>0,this._pointerEvent=Boolean(window.PointerEvent),this._addEventListeners()}static get Default(){return G}static get NAME(){return Q}next(){this._slide(J)}nextWhenVisible(){!document.hidden&&d(this._element)&&this.next()}prev(){this._slide(tt)}pause(t){t||(this._isPaused=!0),Y.findOne(".carousel-item-next, .carousel-item-prev",this._element)&&(a(this._element),this.cycle(!0)),clearInterval(this._interval),this._interval=null}cycle(t){t||(this._isPaused=!1),this._interval&&(clearInterval(this._interval),this._interval=null),this._config&&this._config.interval&&!this._isPaused&&(this._updateInterval(),this._interval=setInterval((document.visibilityState?this.nextWhenVisible:this.next).bind(this),this._config.interval))}to(t){this._activeElement=Y.findOne(rt,this._element);const e=this._getItemIndex(this._activeElement);if(t>this._items.length-1||t<0)return;if(this._isSliding)return void $.one(this._element,nt,(()=>this.to(t)));if(e===t)return this.pause(),void this.cycle();const i=t>e?J:tt;this._slide(i,this._items[t])}_getConfig(t){return t={...G,...X.getDataAttributes(this._element),..."object"==typeof t?t:{}},h(Q,t,Z),t}_handleSwipe(){const t=Math.abs(this.touchDeltaX);if(t<=40)return;const e=t/this.touchDeltaX;this.touchDeltaX=0,e&&this._slide(e>0?it:et)}_addEventListeners(){this._config.keyboard&&$.on(this._element,"keydown.bs.carousel",(t=>this._keydown(t))),"hover"===this._config.pause&&($.on(this._element,"mouseenter.bs.carousel",(t=>this.pause(t))),$.on(this._element,"mouseleave.bs.carousel",(t=>this.cycle(t)))),this._config.touch&&this._touchSupported&&this._addTouchEventListeners()}_addTouchEventListeners(){const t=t=>this._pointerEvent&&("pen"===t.pointerType||"touch"===t.pointerType),e=e=>{t(e)?this.touchStartX=e.clientX:this._pointerEvent||(this.touchStartX=e.touches[0].clientX)},i=t=>{this.touchDeltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this.touchStartX},s=e=>{t(e)&&(this.touchDeltaX=e.clientX-this.touchStartX),this._handleSwipe(),"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout((t=>this.cycle(t)),500+this._config.interval))};Y.find(".carousel-item img",this._element).forEach((t=>{$.on(t,"dragstart.bs.carousel",(t=>t.preventDefault()))})),this._pointerEvent?($.on(this._element,"pointerdown.bs.carousel",(t=>e(t))),$.on(this._element,"pointerup.bs.carousel",(t=>s(t))),this._element.classList.add("pointer-event")):($.on(this._element,"touchstart.bs.carousel",(t=>e(t))),$.on(this._element,"touchmove.bs.carousel",(t=>i(t))),$.on(this._element,"touchend.bs.carousel",(t=>s(t))))}_keydown(t){if(/input|textarea/i.test(t.target.tagName))return;const e=st[t.key];e&&(t.preventDefault(),this._slide(e))}_getItemIndex(t){return this._items=t&&t.parentNode?Y.find(".carousel-item",t.parentNode):[],this._items.indexOf(t)}_getItemByOrder(t,e){const i=t===J;return w(this._items,e,i,this._config.wrap)}_triggerSlideEvent(t,e){const i=this._getItemIndex(t),s=this._getItemIndex(Y.findOne(rt,this._element));return $.trigger(this._element,"slide.bs.carousel",{relatedTarget:t,direction:e,from:s,to:i})}_setActiveIndicatorElement(t){if(this._indicatorsElement){const e=Y.findOne(".active",this._indicatorsElement);e.classList.remove(ot),e.removeAttribute("aria-current");const i=Y.find("[data-bs-target]",this._indicatorsElement);for(let e=0;e{$.trigger(this._element,nt,{relatedTarget:o,direction:d,from:n,to:r})};if(this._element.classList.contains("slide")){o.classList.add(h),f(o),s.classList.add(c),o.classList.add(c);const t=()=>{o.classList.remove(c,h),o.classList.add(ot),s.classList.remove(ot,h,c),this._isSliding=!1,setTimeout(u,0)};this._queueCallback(t,s,!0)}else s.classList.remove(ot),o.classList.add(ot),this._isSliding=!1,u();a&&this.cycle()}_directionToOrder(t){return[it,et].includes(t)?b()?t===et?tt:J:t===et?J:tt:t}_orderToDirection(t){return[J,tt].includes(t)?b()?t===tt?et:it:t===tt?it:et:t}static carouselInterface(t,e){const i=at.getOrCreateInstance(t,e);let{_config:s}=i;"object"==typeof e&&(s={...s,...e});const n="string"==typeof e?e:s.slide;if("number"==typeof e)i.to(e);else if("string"==typeof n){if(void 0===i[n])throw new TypeError(`No method named "${n}"`);i[n]()}else s.interval&&s.ride&&(i.pause(),i.cycle())}static jQueryInterface(t){return this.each((function(){at.carouselInterface(this,t)}))}static dataApiClickHandler(t){const e=r(this);if(!e||!e.classList.contains("carousel"))return;const i={...X.getDataAttributes(e),...X.getDataAttributes(this)},s=this.getAttribute("data-bs-slide-to");s&&(i.interval=!1),at.carouselInterface(e,i),s&&at.getInstance(e).to(s),t.preventDefault()}}$.on(document,"click.bs.carousel.data-api","[data-bs-slide], [data-bs-slide-to]",at.dataApiClickHandler),$.on(window,"load.bs.carousel.data-api",(()=>{const t=Y.find('[data-bs-ride="carousel"]');for(let e=0,i=t.length;et===this._element));null!==s&&n.length&&(this._selector=s,this._triggerArray.push(e))}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return ct}static get NAME(){return lt}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let t,e=[];if(this._config.parent){const t=Y.find(ft,this._config.parent);e=Y.find(".collapse.show, .collapse.collapsing",this._config.parent).filter((e=>!t.includes(e)))}const i=Y.findOne(this._selector);if(e.length){const s=e.find((t=>i!==t));if(t=s?mt.getInstance(s):null,t&&t._isTransitioning)return}if($.trigger(this._element,"show.bs.collapse").defaultPrevented)return;e.forEach((e=>{i!==e&&mt.getOrCreateInstance(e,{toggle:!1}).hide(),t||z.set(e,"bs.collapse",null)}));const s=this._getDimension();this._element.classList.remove(ut),this._element.classList.add(gt),this._element.style[s]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const n=`scroll${s[0].toUpperCase()+s.slice(1)}`;this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(gt),this._element.classList.add(ut,dt),this._element.style[s]="",$.trigger(this._element,"shown.bs.collapse")}),this._element,!0),this._element.style[s]=`${this._element[n]}px`}hide(){if(this._isTransitioning||!this._isShown())return;if($.trigger(this._element,"hide.bs.collapse").defaultPrevented)return;const t=this._getDimension();this._element.style[t]=`${this._element.getBoundingClientRect()[t]}px`,f(this._element),this._element.classList.add(gt),this._element.classList.remove(ut,dt);const e=this._triggerArray.length;for(let t=0;t{this._isTransitioning=!1,this._element.classList.remove(gt),this._element.classList.add(ut),$.trigger(this._element,"hidden.bs.collapse")}),this._element,!0)}_isShown(t=this._element){return t.classList.contains(dt)}_getConfig(t){return(t={...ct,...X.getDataAttributes(this._element),...t}).toggle=Boolean(t.toggle),t.parent=c(t.parent),h(lt,t,ht),t}_getDimension(){return this._element.classList.contains("collapse-horizontal")?"width":"height"}_initializeChildren(){if(!this._config.parent)return;const t=Y.find(ft,this._config.parent);Y.find(pt,this._config.parent).filter((e=>!t.includes(e))).forEach((t=>{const e=r(t);e&&this._addAriaAndCollapsedClass([t],this._isShown(e))}))}_addAriaAndCollapsedClass(t,e){t.length&&t.forEach((t=>{e?t.classList.remove(_t):t.classList.add(_t),t.setAttribute("aria-expanded",e)}))}static jQueryInterface(t){return this.each((function(){const e={};"string"==typeof t&&/show|hide/.test(t)&&(e.toggle=!1);const i=mt.getOrCreateInstance(this,e);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t]()}}))}}$.on(document,"click.bs.collapse.data-api",pt,(function(t){("A"===t.target.tagName||t.delegateTarget&&"A"===t.delegateTarget.tagName)&&t.preventDefault();const e=o(this);Y.find(e).forEach((t=>{mt.getOrCreateInstance(t,{toggle:!1}).toggle()}))})),v(mt);const bt="dropdown",vt="Escape",yt="Space",Et="ArrowUp",wt="ArrowDown",At=new RegExp("ArrowUp|ArrowDown|Escape"),Tt="click.bs.dropdown.data-api",Ct="keydown.bs.dropdown.data-api",kt="show",Lt='[data-bs-toggle="dropdown"]',St=".dropdown-menu",Ot=b()?"top-end":"top-start",Nt=b()?"top-start":"top-end",Dt=b()?"bottom-end":"bottom-start",It=b()?"bottom-start":"bottom-end",Pt=b()?"left-start":"right-start",xt=b()?"right-start":"left-start",Mt={offset:[0,2],boundary:"clippingParents",reference:"toggle",display:"dynamic",popperConfig:null,autoClose:!0},jt={offset:"(array|string|function)",boundary:"(string|element)",reference:"(string|element|object)",display:"string",popperConfig:"(null|object|function)",autoClose:"(boolean|string)"};class Ht extends R{constructor(t,e){super(t),this._popper=null,this._config=this._getConfig(e),this._menu=this._getMenuElement(),this._inNavbar=this._detectNavbar()}static get Default(){return Mt}static get DefaultType(){return jt}static get NAME(){return bt}toggle(){return this._isShown()?this.hide():this.show()}show(){if(u(this._element)||this._isShown(this._menu))return;const t={relatedTarget:this._element};if($.trigger(this._element,"show.bs.dropdown",t).defaultPrevented)return;const e=Ht.getParentFromElement(this._element);this._inNavbar?X.setDataAttribute(this._menu,"popper","none"):this._createPopper(e),"ontouchstart"in document.documentElement&&!e.closest(".navbar-nav")&&[].concat(...document.body.children).forEach((t=>$.on(t,"mouseover",_))),this._element.focus(),this._element.setAttribute("aria-expanded",!0),this._menu.classList.add(kt),this._element.classList.add(kt),$.trigger(this._element,"shown.bs.dropdown",t)}hide(){if(u(this._element)||!this._isShown(this._menu))return;const t={relatedTarget:this._element};this._completeHide(t)}dispose(){this._popper&&this._popper.destroy(),super.dispose()}update(){this._inNavbar=this._detectNavbar(),this._popper&&this._popper.update()}_completeHide(t){$.trigger(this._element,"hide.bs.dropdown",t).defaultPrevented||("ontouchstart"in document.documentElement&&[].concat(...document.body.children).forEach((t=>$.off(t,"mouseover",_))),this._popper&&this._popper.destroy(),this._menu.classList.remove(kt),this._element.classList.remove(kt),this._element.setAttribute("aria-expanded","false"),X.removeDataAttribute(this._menu,"popper"),$.trigger(this._element,"hidden.bs.dropdown",t))}_getConfig(t){if(t={...this.constructor.Default,...X.getDataAttributes(this._element),...t},h(bt,t,this.constructor.DefaultType),"object"==typeof t.reference&&!l(t.reference)&&"function"!=typeof t.reference.getBoundingClientRect)throw new TypeError(`${bt.toUpperCase()}: Option "reference" provided type "object" without a required "getBoundingClientRect" method.`);return t}_createPopper(t){if(void 0===i)throw new TypeError("Bootstrap's dropdowns require Popper (https://popper.js.org)");let e=this._element;"parent"===this._config.reference?e=t:l(this._config.reference)?e=c(this._config.reference):"object"==typeof this._config.reference&&(e=this._config.reference);const s=this._getPopperConfig(),n=s.modifiers.find((t=>"applyStyles"===t.name&&!1===t.enabled));this._popper=i.createPopper(e,this._menu,s),n&&X.setDataAttribute(this._menu,"popper","static")}_isShown(t=this._element){return t.classList.contains(kt)}_getMenuElement(){return Y.next(this._element,St)[0]}_getPlacement(){const t=this._element.parentNode;if(t.classList.contains("dropend"))return Pt;if(t.classList.contains("dropstart"))return xt;const e="end"===getComputedStyle(this._menu).getPropertyValue("--bs-position").trim();return t.classList.contains("dropup")?e?Nt:Ot:e?It:Dt}_detectNavbar(){return null!==this._element.closest(".navbar")}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(){const t={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return"static"===this._config.display&&(t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,..."function"==typeof this._config.popperConfig?this._config.popperConfig(t):this._config.popperConfig}}_selectMenuItem({key:t,target:e}){const i=Y.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",this._menu).filter(d);i.length&&w(i,e,t===wt,!i.includes(e)).focus()}static jQueryInterface(t){return this.each((function(){const e=Ht.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}static clearMenus(t){if(t&&(2===t.button||"keyup"===t.type&&"Tab"!==t.key))return;const e=Y.find(Lt);for(let i=0,s=e.length;ie+t)),this._setElementAttributes($t,"paddingRight",(e=>e+t)),this._setElementAttributes(Bt,"marginRight",(e=>e-t))}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(t,e,i){const s=this.getWidth();this._applyManipulationCallback(t,(t=>{if(t!==this._element&&window.innerWidth>t.clientWidth+s)return;this._saveInitialAttribute(t,e);const n=window.getComputedStyle(t)[e];t.style[e]=`${i(Number.parseFloat(n))}px`}))}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,"paddingRight"),this._resetElementAttributes($t,"paddingRight"),this._resetElementAttributes(Bt,"marginRight")}_saveInitialAttribute(t,e){const i=t.style[e];i&&X.setDataAttribute(t,e,i)}_resetElementAttributes(t,e){this._applyManipulationCallback(t,(t=>{const i=X.getDataAttribute(t,e);void 0===i?t.style.removeProperty(e):(X.removeDataAttribute(t,e),t.style[e]=i)}))}_applyManipulationCallback(t,e){l(t)?e(t):Y.find(t,this._element).forEach(e)}isOverflowing(){return this.getWidth()>0}}const Rt={className:"modal-backdrop",isVisible:!0,isAnimated:!1,rootElement:"body",clickCallback:null},Ft={className:"string",isVisible:"boolean",isAnimated:"boolean",rootElement:"(element|string)",clickCallback:"(function|null)"},qt="show",Wt="mousedown.bs.backdrop";class Ut{constructor(t){this._config=this._getConfig(t),this._isAppended=!1,this._element=null}show(t){this._config.isVisible?(this._append(),this._config.isAnimated&&f(this._getElement()),this._getElement().classList.add(qt),this._emulateAnimation((()=>{y(t)}))):y(t)}hide(t){this._config.isVisible?(this._getElement().classList.remove(qt),this._emulateAnimation((()=>{this.dispose(),y(t)}))):y(t)}_getElement(){if(!this._element){const t=document.createElement("div");t.className=this._config.className,this._config.isAnimated&&t.classList.add("fade"),this._element=t}return this._element}_getConfig(t){return(t={...Rt,..."object"==typeof t?t:{}}).rootElement=c(t.rootElement),h("backdrop",t,Ft),t}_append(){this._isAppended||(this._config.rootElement.append(this._getElement()),$.on(this._getElement(),Wt,(()=>{y(this._config.clickCallback)})),this._isAppended=!0)}dispose(){this._isAppended&&($.off(this._element,Wt),this._element.remove(),this._isAppended=!1)}_emulateAnimation(t){E(t,this._getElement(),this._config.isAnimated)}}const Kt={trapElement:null,autofocus:!0},Vt={trapElement:"element",autofocus:"boolean"},Xt=".bs.focustrap",Yt="backward";class Qt{constructor(t){this._config=this._getConfig(t),this._isActive=!1,this._lastTabNavDirection=null}activate(){const{trapElement:t,autofocus:e}=this._config;this._isActive||(e&&t.focus(),$.off(document,Xt),$.on(document,"focusin.bs.focustrap",(t=>this._handleFocusin(t))),$.on(document,"keydown.tab.bs.focustrap",(t=>this._handleKeydown(t))),this._isActive=!0)}deactivate(){this._isActive&&(this._isActive=!1,$.off(document,Xt))}_handleFocusin(t){const{target:e}=t,{trapElement:i}=this._config;if(e===document||e===i||i.contains(e))return;const s=Y.focusableChildren(i);0===s.length?i.focus():this._lastTabNavDirection===Yt?s[s.length-1].focus():s[0].focus()}_handleKeydown(t){"Tab"===t.key&&(this._lastTabNavDirection=t.shiftKey?Yt:"forward")}_getConfig(t){return t={...Kt,..."object"==typeof t?t:{}},h("focustrap",t,Vt),t}}const Gt="modal",Zt="Escape",Jt={backdrop:!0,keyboard:!0,focus:!0},te={backdrop:"(boolean|string)",keyboard:"boolean",focus:"boolean"},ee="hidden.bs.modal",ie="show.bs.modal",se="resize.bs.modal",ne="click.dismiss.bs.modal",oe="keydown.dismiss.bs.modal",re="mousedown.dismiss.bs.modal",ae="modal-open",le="show",ce="modal-static";class he extends R{constructor(t,e){super(t),this._config=this._getConfig(e),this._dialog=Y.findOne(".modal-dialog",this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._ignoreBackdropClick=!1,this._isTransitioning=!1,this._scrollBar=new zt}static get Default(){return Jt}static get NAME(){return Gt}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||this._isTransitioning||$.trigger(this._element,ie,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._isAnimated()&&(this._isTransitioning=!0),this._scrollBar.hide(),document.body.classList.add(ae),this._adjustDialog(),this._setEscapeEvent(),this._setResizeEvent(),$.on(this._dialog,re,(()=>{$.one(this._element,"mouseup.dismiss.bs.modal",(t=>{t.target===this._element&&(this._ignoreBackdropClick=!0)}))})),this._showBackdrop((()=>this._showElement(t))))}hide(){if(!this._isShown||this._isTransitioning)return;if($.trigger(this._element,"hide.bs.modal").defaultPrevented)return;this._isShown=!1;const t=this._isAnimated();t&&(this._isTransitioning=!0),this._setEscapeEvent(),this._setResizeEvent(),this._focustrap.deactivate(),this._element.classList.remove(le),$.off(this._element,ne),$.off(this._dialog,re),this._queueCallback((()=>this._hideModal()),this._element,t)}dispose(){[window,this._dialog].forEach((t=>$.off(t,".bs.modal"))),this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new Ut({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new Qt({trapElement:this._element})}_getConfig(t){return t={...Jt,...X.getDataAttributes(this._element),..."object"==typeof t?t:{}},h(Gt,t,te),t}_showElement(t){const e=this._isAnimated(),i=Y.findOne(".modal-body",this._dialog);this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE||document.body.append(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0,i&&(i.scrollTop=0),e&&f(this._element),this._element.classList.add(le),this._queueCallback((()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,$.trigger(this._element,"shown.bs.modal",{relatedTarget:t})}),this._dialog,e)}_setEscapeEvent(){this._isShown?$.on(this._element,oe,(t=>{this._config.keyboard&&t.key===Zt?(t.preventDefault(),this.hide()):this._config.keyboard||t.key!==Zt||this._triggerBackdropTransition()})):$.off(this._element,oe)}_setResizeEvent(){this._isShown?$.on(window,se,(()=>this._adjustDialog())):$.off(window,se)}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._backdrop.hide((()=>{document.body.classList.remove(ae),this._resetAdjustments(),this._scrollBar.reset(),$.trigger(this._element,ee)}))}_showBackdrop(t){$.on(this._element,ne,(t=>{this._ignoreBackdropClick?this._ignoreBackdropClick=!1:t.target===t.currentTarget&&(!0===this._config.backdrop?this.hide():"static"===this._config.backdrop&&this._triggerBackdropTransition())})),this._backdrop.show(t)}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if($.trigger(this._element,"hidePrevented.bs.modal").defaultPrevented)return;const{classList:t,scrollHeight:e,style:i}=this._element,s=e>document.documentElement.clientHeight;!s&&"hidden"===i.overflowY||t.contains(ce)||(s||(i.overflowY="hidden"),t.add(ce),this._queueCallback((()=>{t.remove(ce),s||this._queueCallback((()=>{i.overflowY=""}),this._dialog)}),this._dialog),this._element.focus())}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._scrollBar.getWidth(),i=e>0;(!i&&t&&!b()||i&&!t&&b())&&(this._element.style.paddingLeft=`${e}px`),(i&&!t&&!b()||!i&&t&&b())&&(this._element.style.paddingRight=`${e}px`)}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(t,e){return this.each((function(){const i=he.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t](e)}}))}}$.on(document,"click.bs.modal.data-api",'[data-bs-toggle="modal"]',(function(t){const e=r(this);["A","AREA"].includes(this.tagName)&&t.preventDefault(),$.one(e,ie,(t=>{t.defaultPrevented||$.one(e,ee,(()=>{d(this)&&this.focus()}))}));const i=Y.findOne(".modal.show");i&&he.getInstance(i).hide(),he.getOrCreateInstance(e).toggle(this)})),F(he),v(he);const de="offcanvas",ue={backdrop:!0,keyboard:!0,scroll:!1},ge={backdrop:"boolean",keyboard:"boolean",scroll:"boolean"},_e="show",fe=".offcanvas.show",pe="hidden.bs.offcanvas";class me extends R{constructor(t,e){super(t),this._config=this._getConfig(e),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._addEventListeners()}static get NAME(){return de}static get Default(){return ue}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||$.trigger(this._element,"show.bs.offcanvas",{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._element.style.visibility="visible",this._backdrop.show(),this._config.scroll||(new zt).hide(),this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add(_e),this._queueCallback((()=>{this._config.scroll||this._focustrap.activate(),$.trigger(this._element,"shown.bs.offcanvas",{relatedTarget:t})}),this._element,!0))}hide(){this._isShown&&($.trigger(this._element,"hide.bs.offcanvas").defaultPrevented||(this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.remove(_e),this._backdrop.hide(),this._queueCallback((()=>{this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._element.style.visibility="hidden",this._config.scroll||(new zt).reset(),$.trigger(this._element,pe)}),this._element,!0)))}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_getConfig(t){return t={...ue,...X.getDataAttributes(this._element),..."object"==typeof t?t:{}},h(de,t,ge),t}_initializeBackDrop(){return new Ut({className:"offcanvas-backdrop",isVisible:this._config.backdrop,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:()=>this.hide()})}_initializeFocusTrap(){return new Qt({trapElement:this._element})}_addEventListeners(){$.on(this._element,"keydown.dismiss.bs.offcanvas",(t=>{this._config.keyboard&&"Escape"===t.key&&this.hide()}))}static jQueryInterface(t){return this.each((function(){const e=me.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}$.on(document,"click.bs.offcanvas.data-api",'[data-bs-toggle="offcanvas"]',(function(t){const e=r(this);if(["A","AREA"].includes(this.tagName)&&t.preventDefault(),u(this))return;$.one(e,pe,(()=>{d(this)&&this.focus()}));const i=Y.findOne(fe);i&&i!==e&&me.getInstance(i).hide(),me.getOrCreateInstance(e).toggle(this)})),$.on(window,"load.bs.offcanvas.data-api",(()=>Y.find(fe).forEach((t=>me.getOrCreateInstance(t).show())))),F(me),v(me);const be=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),ve=/^(?:(?:https?|mailto|ftp|tel|file|sms):|[^#&/:?]*(?:[#/?]|$))/i,ye=/^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[\d+/a-z]+=*$/i,Ee=(t,e)=>{const i=t.nodeName.toLowerCase();if(e.includes(i))return!be.has(i)||Boolean(ve.test(t.nodeValue)||ye.test(t.nodeValue));const s=e.filter((t=>t instanceof RegExp));for(let t=0,e=s.length;t{Ee(t,r)||i.removeAttribute(t.nodeName)}))}return s.body.innerHTML}const Ae="tooltip",Te=new Set(["sanitize","allowList","sanitizeFn"]),Ce={animation:"boolean",template:"string",title:"(string|element|function)",trigger:"string",delay:"(number|object)",html:"boolean",selector:"(string|boolean)",placement:"(string|function)",offset:"(array|string|function)",container:"(string|element|boolean)",fallbackPlacements:"array",boundary:"(string|element)",customClass:"(string|function)",sanitize:"boolean",sanitizeFn:"(null|function)",allowList:"object",popperConfig:"(null|object|function)"},ke={AUTO:"auto",TOP:"top",RIGHT:b()?"left":"right",BOTTOM:"bottom",LEFT:b()?"right":"left"},Le={animation:!0,template:'',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:[0,0],container:!1,fallbackPlacements:["top","right","bottom","left"],boundary:"clippingParents",customClass:"",sanitize:!0,sanitizeFn:null,allowList:{"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],div:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},popperConfig:null},Se={HIDE:"hide.bs.tooltip",HIDDEN:"hidden.bs.tooltip",SHOW:"show.bs.tooltip",SHOWN:"shown.bs.tooltip",INSERTED:"inserted.bs.tooltip",CLICK:"click.bs.tooltip",FOCUSIN:"focusin.bs.tooltip",FOCUSOUT:"focusout.bs.tooltip",MOUSEENTER:"mouseenter.bs.tooltip",MOUSELEAVE:"mouseleave.bs.tooltip"},Oe="fade",Ne="show",De="show",Ie="out",Pe=".tooltip-inner",xe=".modal",Me="hide.bs.modal",je="hover",He="focus";class $e extends R{constructor(t,e){if(void 0===i)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");super(t),this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this._config=this._getConfig(e),this.tip=null,this._setListeners()}static get Default(){return Le}static get NAME(){return Ae}static get Event(){return Se}static get DefaultType(){return Ce}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(t){if(this._isEnabled)if(t){const e=this._initializeOnDelegatedTarget(t);e._activeTrigger.click=!e._activeTrigger.click,e._isWithActiveTrigger()?e._enter(null,e):e._leave(null,e)}else{if(this.getTipElement().classList.contains(Ne))return void this._leave(null,this);this._enter(null,this)}}dispose(){clearTimeout(this._timeout),$.off(this._element.closest(xe),Me,this._hideModalHandler),this.tip&&this.tip.remove(),this._disposePopper(),super.dispose()}show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this.isWithContent()||!this._isEnabled)return;const t=$.trigger(this._element,this.constructor.Event.SHOW),e=g(this._element),s=null===e?this._element.ownerDocument.documentElement.contains(this._element):e.contains(this._element);if(t.defaultPrevented||!s)return;"tooltip"===this.constructor.NAME&&this.tip&&this.getTitle()!==this.tip.querySelector(Pe).innerHTML&&(this._disposePopper(),this.tip.remove(),this.tip=null);const n=this.getTipElement(),o=(t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t));return t})(this.constructor.NAME);n.setAttribute("id",o),this._element.setAttribute("aria-describedby",o),this._config.animation&&n.classList.add(Oe);const r="function"==typeof this._config.placement?this._config.placement.call(this,n,this._element):this._config.placement,a=this._getAttachment(r);this._addAttachmentClass(a);const{container:l}=this._config;z.set(n,this.constructor.DATA_KEY,this),this._element.ownerDocument.documentElement.contains(this.tip)||(l.append(n),$.trigger(this._element,this.constructor.Event.INSERTED)),this._popper?this._popper.update():this._popper=i.createPopper(this._element,n,this._getPopperConfig(a)),n.classList.add(Ne);const c=this._resolvePossibleFunction(this._config.customClass);c&&n.classList.add(...c.split(" ")),"ontouchstart"in document.documentElement&&[].concat(...document.body.children).forEach((t=>{$.on(t,"mouseover",_)}));const h=this.tip.classList.contains(Oe);this._queueCallback((()=>{const t=this._hoverState;this._hoverState=null,$.trigger(this._element,this.constructor.Event.SHOWN),t===Ie&&this._leave(null,this)}),this.tip,h)}hide(){if(!this._popper)return;const t=this.getTipElement();if($.trigger(this._element,this.constructor.Event.HIDE).defaultPrevented)return;t.classList.remove(Ne),"ontouchstart"in document.documentElement&&[].concat(...document.body.children).forEach((t=>$.off(t,"mouseover",_))),this._activeTrigger.click=!1,this._activeTrigger.focus=!1,this._activeTrigger.hover=!1;const e=this.tip.classList.contains(Oe);this._queueCallback((()=>{this._isWithActiveTrigger()||(this._hoverState!==De&&t.remove(),this._cleanTipClass(),this._element.removeAttribute("aria-describedby"),$.trigger(this._element,this.constructor.Event.HIDDEN),this._disposePopper())}),this.tip,e),this._hoverState=""}update(){null!==this._popper&&this._popper.update()}isWithContent(){return Boolean(this.getTitle())}getTipElement(){if(this.tip)return this.tip;const t=document.createElement("div");t.innerHTML=this._config.template;const e=t.children[0];return this.setContent(e),e.classList.remove(Oe,Ne),this.tip=e,this.tip}setContent(t){this._sanitizeAndSetContent(t,this.getTitle(),Pe)}_sanitizeAndSetContent(t,e,i){const s=Y.findOne(i,t);e||!s?this.setElementContent(s,e):s.remove()}setElementContent(t,e){if(null!==t)return l(e)?(e=c(e),void(this._config.html?e.parentNode!==t&&(t.innerHTML="",t.append(e)):t.textContent=e.textContent)):void(this._config.html?(this._config.sanitize&&(e=we(e,this._config.allowList,this._config.sanitizeFn)),t.innerHTML=e):t.textContent=e)}getTitle(){const t=this._element.getAttribute("data-bs-original-title")||this._config.title;return this._resolvePossibleFunction(t)}updateAttachment(t){return"right"===t?"end":"left"===t?"start":t}_initializeOnDelegatedTarget(t,e){return e||this.constructor.getOrCreateInstance(t.delegateTarget,this._getDelegateConfig())}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_resolvePossibleFunction(t){return"function"==typeof t?t.call(this._element):t}_getPopperConfig(t){const e={placement:t,modifiers:[{name:"flip",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"onChange",enabled:!0,phase:"afterWrite",fn:t=>this._handlePopperPlacementChange(t)}],onFirstUpdate:t=>{t.options.placement!==t.placement&&this._handlePopperPlacementChange(t)}};return{...e,..."function"==typeof this._config.popperConfig?this._config.popperConfig(e):this._config.popperConfig}}_addAttachmentClass(t){this.getTipElement().classList.add(`${this._getBasicClassPrefix()}-${this.updateAttachment(t)}`)}_getAttachment(t){return ke[t.toUpperCase()]}_setListeners(){this._config.trigger.split(" ").forEach((t=>{if("click"===t)$.on(this._element,this.constructor.Event.CLICK,this._config.selector,(t=>this.toggle(t)));else if("manual"!==t){const e=t===je?this.constructor.Event.MOUSEENTER:this.constructor.Event.FOCUSIN,i=t===je?this.constructor.Event.MOUSELEAVE:this.constructor.Event.FOCUSOUT;$.on(this._element,e,this._config.selector,(t=>this._enter(t))),$.on(this._element,i,this._config.selector,(t=>this._leave(t)))}})),this._hideModalHandler=()=>{this._element&&this.hide()},$.on(this._element.closest(xe),Me,this._hideModalHandler),this._config.selector?this._config={...this._config,trigger:"manual",selector:""}:this._fixTitle()}_fixTitle(){const t=this._element.getAttribute("title"),e=typeof this._element.getAttribute("data-bs-original-title");(t||"string"!==e)&&(this._element.setAttribute("data-bs-original-title",t||""),!t||this._element.getAttribute("aria-label")||this._element.textContent||this._element.setAttribute("aria-label",t),this._element.setAttribute("title",""))}_enter(t,e){e=this._initializeOnDelegatedTarget(t,e),t&&(e._activeTrigger["focusin"===t.type?He:je]=!0),e.getTipElement().classList.contains(Ne)||e._hoverState===De?e._hoverState=De:(clearTimeout(e._timeout),e._hoverState=De,e._config.delay&&e._config.delay.show?e._timeout=setTimeout((()=>{e._hoverState===De&&e.show()}),e._config.delay.show):e.show())}_leave(t,e){e=this._initializeOnDelegatedTarget(t,e),t&&(e._activeTrigger["focusout"===t.type?He:je]=e._element.contains(t.relatedTarget)),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState=Ie,e._config.delay&&e._config.delay.hide?e._timeout=setTimeout((()=>{e._hoverState===Ie&&e.hide()}),e._config.delay.hide):e.hide())}_isWithActiveTrigger(){for(const t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1}_getConfig(t){const e=X.getDataAttributes(this._element);return Object.keys(e).forEach((t=>{Te.has(t)&&delete e[t]})),(t={...this.constructor.Default,...e,..."object"==typeof t&&t?t:{}}).container=!1===t.container?document.body:c(t.container),"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),h(Ae,t,this.constructor.DefaultType),t.sanitize&&(t.template=we(t.template,t.allowList,t.sanitizeFn)),t}_getDelegateConfig(){const t={};for(const e in this._config)this.constructor.Default[e]!==this._config[e]&&(t[e]=this._config[e]);return t}_cleanTipClass(){const t=this.getTipElement(),e=new RegExp(`(^|\\s)${this._getBasicClassPrefix()}\\S+`,"g"),i=t.getAttribute("class").match(e);null!==i&&i.length>0&&i.map((t=>t.trim())).forEach((e=>t.classList.remove(e)))}_getBasicClassPrefix(){return"bs-tooltip"}_handlePopperPlacementChange(t){const{state:e}=t;e&&(this.tip=e.elements.popper,this._cleanTipClass(),this._addAttachmentClass(this._getAttachment(e.placement)))}_disposePopper(){this._popper&&(this._popper.destroy(),this._popper=null)}static jQueryInterface(t){return this.each((function(){const e=$e.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}v($e);const Be={...$e.Default,placement:"right",offset:[0,8],trigger:"click",content:"",template:''},ze={...$e.DefaultType,content:"(string|element|function)"},Re={HIDE:"hide.bs.popover",HIDDEN:"hidden.bs.popover",SHOW:"show.bs.popover",SHOWN:"shown.bs.popover",INSERTED:"inserted.bs.popover",CLICK:"click.bs.popover",FOCUSIN:"focusin.bs.popover",FOCUSOUT:"focusout.bs.popover",MOUSEENTER:"mouseenter.bs.popover",MOUSELEAVE:"mouseleave.bs.popover"};class Fe extends $e{static get Default(){return Be}static get NAME(){return"popover"}static get Event(){return Re}static get DefaultType(){return ze}isWithContent(){return this.getTitle()||this._getContent()}setContent(t){this._sanitizeAndSetContent(t,this.getTitle(),".popover-header"),this._sanitizeAndSetContent(t,this._getContent(),".popover-body")}_getContent(){return this._resolvePossibleFunction(this._config.content)}_getBasicClassPrefix(){return"bs-popover"}static jQueryInterface(t){return this.each((function(){const e=Fe.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}v(Fe);const qe="scrollspy",We={offset:10,method:"auto",target:""},Ue={offset:"number",method:"string",target:"(string|element)"},Ke="active",Ve=".nav-link, .list-group-item, .dropdown-item",Xe="position";class Ye extends R{constructor(t,e){super(t),this._scrollElement="BODY"===this._element.tagName?window:this._element,this._config=this._getConfig(e),this._offsets=[],this._targets=[],this._activeTarget=null,this._scrollHeight=0,$.on(this._scrollElement,"scroll.bs.scrollspy",(()=>this._process())),this.refresh(),this._process()}static get Default(){return We}static get NAME(){return qe}refresh(){const t=this._scrollElement===this._scrollElement.window?"offset":Xe,e="auto"===this._config.method?t:this._config.method,i=e===Xe?this._getScrollTop():0;this._offsets=[],this._targets=[],this._scrollHeight=this._getScrollHeight(),Y.find(Ve,this._config.target).map((t=>{const s=o(t),n=s?Y.findOne(s):null;if(n){const t=n.getBoundingClientRect();if(t.width||t.height)return[X[e](n).top+i,s]}return null})).filter((t=>t)).sort(((t,e)=>t[0]-e[0])).forEach((t=>{this._offsets.push(t[0]),this._targets.push(t[1])}))}dispose(){$.off(this._scrollElement,".bs.scrollspy"),super.dispose()}_getConfig(t){return(t={...We,...X.getDataAttributes(this._element),..."object"==typeof t&&t?t:{}}).target=c(t.target)||document.documentElement,h(qe,t,Ue),t}_getScrollTop(){return this._scrollElement===window?this._scrollElement.pageYOffset:this._scrollElement.scrollTop}_getScrollHeight(){return this._scrollElement.scrollHeight||Math.max(document.body.scrollHeight,document.documentElement.scrollHeight)}_getOffsetHeight(){return this._scrollElement===window?window.innerHeight:this._scrollElement.getBoundingClientRect().height}_process(){const t=this._getScrollTop()+this._config.offset,e=this._getScrollHeight(),i=this._config.offset+e-this._getOffsetHeight();if(this._scrollHeight!==e&&this.refresh(),t>=i){const t=this._targets[this._targets.length-1];this._activeTarget!==t&&this._activate(t)}else{if(this._activeTarget&&t0)return this._activeTarget=null,void this._clear();for(let e=this._offsets.length;e--;)this._activeTarget!==this._targets[e]&&t>=this._offsets[e]&&(void 0===this._offsets[e+1]||t`${e}[data-bs-target="${t}"],${e}[href="${t}"]`)),i=Y.findOne(e.join(","),this._config.target);i.classList.add(Ke),i.classList.contains("dropdown-item")?Y.findOne(".dropdown-toggle",i.closest(".dropdown")).classList.add(Ke):Y.parents(i,".nav, .list-group").forEach((t=>{Y.prev(t,".nav-link, .list-group-item").forEach((t=>t.classList.add(Ke))),Y.prev(t,".nav-item").forEach((t=>{Y.children(t,".nav-link").forEach((t=>t.classList.add(Ke)))}))})),$.trigger(this._scrollElement,"activate.bs.scrollspy",{relatedTarget:t})}_clear(){Y.find(Ve,this._config.target).filter((t=>t.classList.contains(Ke))).forEach((t=>t.classList.remove(Ke)))}static jQueryInterface(t){return this.each((function(){const e=Ye.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}$.on(window,"load.bs.scrollspy.data-api",(()=>{Y.find('[data-bs-spy="scroll"]').forEach((t=>new Ye(t)))})),v(Ye);const Qe="active",Ge="fade",Ze="show",Je=".active",ti=":scope > li > .active";class ei extends R{static get NAME(){return"tab"}show(){if(this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE&&this._element.classList.contains(Qe))return;let t;const e=r(this._element),i=this._element.closest(".nav, .list-group");if(i){const e="UL"===i.nodeName||"OL"===i.nodeName?ti:Je;t=Y.find(e,i),t=t[t.length-1]}const s=t?$.trigger(t,"hide.bs.tab",{relatedTarget:this._element}):null;if($.trigger(this._element,"show.bs.tab",{relatedTarget:t}).defaultPrevented||null!==s&&s.defaultPrevented)return;this._activate(this._element,i);const n=()=>{$.trigger(t,"hidden.bs.tab",{relatedTarget:this._element}),$.trigger(this._element,"shown.bs.tab",{relatedTarget:t})};e?this._activate(e,e.parentNode,n):n()}_activate(t,e,i){const s=(!e||"UL"!==e.nodeName&&"OL"!==e.nodeName?Y.children(e,Je):Y.find(ti,e))[0],n=i&&s&&s.classList.contains(Ge),o=()=>this._transitionComplete(t,s,i);s&&n?(s.classList.remove(Ze),this._queueCallback(o,t,!0)):o()}_transitionComplete(t,e,i){if(e){e.classList.remove(Qe);const t=Y.findOne(":scope > .dropdown-menu .active",e.parentNode);t&&t.classList.remove(Qe),"tab"===e.getAttribute("role")&&e.setAttribute("aria-selected",!1)}t.classList.add(Qe),"tab"===t.getAttribute("role")&&t.setAttribute("aria-selected",!0),f(t),t.classList.contains(Ge)&&t.classList.add(Ze);let s=t.parentNode;if(s&&"LI"===s.nodeName&&(s=s.parentNode),s&&s.classList.contains("dropdown-menu")){const e=t.closest(".dropdown");e&&Y.find(".dropdown-toggle",e).forEach((t=>t.classList.add(Qe))),t.setAttribute("aria-expanded",!0)}i&&i()}static jQueryInterface(t){return this.each((function(){const e=ei.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}$.on(document,"click.bs.tab.data-api",'[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',(function(t){["A","AREA"].includes(this.tagName)&&t.preventDefault(),u(this)||ei.getOrCreateInstance(this).show()})),v(ei);const ii="toast",si="hide",ni="show",oi="showing",ri={animation:"boolean",autohide:"boolean",delay:"number"},ai={animation:!0,autohide:!0,delay:5e3};class li extends R{constructor(t,e){super(t),this._config=this._getConfig(e),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get DefaultType(){return ri}static get Default(){return ai}static get NAME(){return ii}show(){$.trigger(this._element,"show.bs.toast").defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove(si),f(this._element),this._element.classList.add(ni),this._element.classList.add(oi),this._queueCallback((()=>{this._element.classList.remove(oi),$.trigger(this._element,"shown.bs.toast"),this._maybeScheduleHide()}),this._element,this._config.animation))}hide(){this._element.classList.contains(ni)&&($.trigger(this._element,"hide.bs.toast").defaultPrevented||(this._element.classList.add(oi),this._queueCallback((()=>{this._element.classList.add(si),this._element.classList.remove(oi),this._element.classList.remove(ni),$.trigger(this._element,"hidden.bs.toast")}),this._element,this._config.animation)))}dispose(){this._clearTimeout(),this._element.classList.contains(ni)&&this._element.classList.remove(ni),super.dispose()}_getConfig(t){return t={...ai,...X.getDataAttributes(this._element),..."object"==typeof t&&t?t:{}},h(ii,t,this.constructor.DefaultType),t}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout((()=>{this.hide()}),this._config.delay)))}_onInteraction(t,e){switch(t.type){case"mouseover":case"mouseout":this._hasMouseInteraction=e;break;case"focusin":case"focusout":this._hasKeyboardInteraction=e}if(e)return void this._clearTimeout();const i=t.relatedTarget;this._element===i||this._element.contains(i)||this._maybeScheduleHide()}_setListeners(){$.on(this._element,"mouseover.bs.toast",(t=>this._onInteraction(t,!0))),$.on(this._element,"mouseout.bs.toast",(t=>this._onInteraction(t,!1))),$.on(this._element,"focusin.bs.toast",(t=>this._onInteraction(t,!0))),$.on(this._element,"focusout.bs.toast",(t=>this._onInteraction(t,!1)))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each((function(){const e=li.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}return F(li),v(li),{Alert:q,Button:U,Carousel:at,Collapse:mt,Dropdown:Ht,Modal:he,Offcanvas:me,Popover:Fe,ScrollSpy:Ye,Tab:ei,Toast:li,Tooltip:$e}})); 7 | //# sourceMappingURL=bootstrap.min.js.map -------------------------------------------------------------------------------- /public/js/dark-toggle.js: -------------------------------------------------------------------------------- 1 | toggleDarkMode = () => { 2 | if ((document.getElementById('darkmode').value = !document.getElementById('darkmode').value) === true) { 3 | enableDarkMode(); 4 | localStorage.setItem("darkmode", "true"); 5 | document.cookie = "darkmode=true;max-age=31540000;path=/"; 6 | } else { 7 | disableDarkMode(); 8 | localStorage.setItem("darkmode", "false"); 9 | document.cookie = "darkmode=false;max-age=31540000;path=/"; 10 | } 11 | } 12 | 13 | loadDarkMode = () => { 14 | if (localStorage.getItem("darkmode") === "true") { 15 | document.getElementById('darkmode').value = true; 16 | enableDarkMode(); 17 | } else { 18 | document.getElementById('darkmode').value = false; 19 | disableDarkMode(); 20 | } 21 | } 22 | 23 | enableDarkMode = () => { 24 | document.querySelectorAll(".bg-white").forEach((it) => { 25 | it.classList.remove('bg-white'); 26 | it.classList.add('bg-dark'); 27 | it.classList.add('text-white-75'); 28 | }); 29 | document.querySelectorAll(".text-primary").forEach((it) => { 30 | it.classList.remove('text-primary'); 31 | it.classList.add('text-light'); 32 | }); 33 | document.querySelectorAll(".text-dark").forEach((it) => { 34 | it.classList.remove('text-dark'); 35 | it.classList.add('text-white-75'); 36 | }); 37 | document.querySelectorAll(".navbar-light").forEach((it) => { 38 | it.classList.remove('navbar-light'); 39 | it.classList.add('navbar-dark'); 40 | }); 41 | } 42 | 43 | disableDarkMode = () => { 44 | document.querySelectorAll(".bg-dark").forEach((it) => { 45 | it.classList.remove('bg-dark'); 46 | it.classList.add('bg-white'); 47 | }); 48 | document.querySelectorAll(".text-light").forEach((it) => { 49 | it.classList.remove('text-light'); 50 | it.classList.add('text-primary'); 51 | }); 52 | document.querySelectorAll(".text-white-75").forEach((it) => { 53 | it.classList.remove('text-white-75'); 54 | it.classList.add('text-dark'); 55 | }); 56 | document.querySelectorAll(".navbar-dark").forEach((it) => { 57 | it.classList.remove('navbar-dark'); 58 | it.classList.add('navbar-light'); 59 | }); 60 | } 61 | -------------------------------------------------------------------------------- /public/js/main.js: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------- /public/js/popper.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) Federico Zivolo 2019 3 | Distributed under the MIT License (license terms are at http://opensource.org/licenses/MIT). 4 | */(function(e,t){'object'==typeof exports&&'undefined'!=typeof module?module.exports=t():'function'==typeof define&&define.amd?define(t):e.Popper=t()})(this,function(){'use strict';function e(e){return e&&'[object Function]'==={}.toString.call(e)}function t(e,t){if(1!==e.nodeType)return[];var o=e.ownerDocument.defaultView,n=o.getComputedStyle(e,null);return t?n[t]:n}function o(e){return'HTML'===e.nodeName?e:e.parentNode||e.host}function n(e){if(!e)return document.body;switch(e.nodeName){case'HTML':case'BODY':return e.ownerDocument.body;case'#document':return e.body;}var i=t(e),r=i.overflow,p=i.overflowX,s=i.overflowY;return /(auto|scroll|overlay)/.test(r+s+p)?e:n(o(e))}function i(e){return e&&e.referenceNode?e.referenceNode:e}function r(e){return 11===e?re:10===e?pe:re||pe}function p(e){if(!e)return document.documentElement;for(var o=r(10)?document.body:null,n=e.offsetParent||null;n===o&&e.nextElementSibling;)n=(e=e.nextElementSibling).offsetParent;var i=n&&n.nodeName;return i&&'BODY'!==i&&'HTML'!==i?-1!==['TH','TD','TABLE'].indexOf(n.nodeName)&&'static'===t(n,'position')?p(n):n:e?e.ownerDocument.documentElement:document.documentElement}function s(e){var t=e.nodeName;return'BODY'!==t&&('HTML'===t||p(e.firstElementChild)===e)}function d(e){return null===e.parentNode?e:d(e.parentNode)}function a(e,t){if(!e||!e.nodeType||!t||!t.nodeType)return document.documentElement;var o=e.compareDocumentPosition(t)&Node.DOCUMENT_POSITION_FOLLOWING,n=o?e:t,i=o?t:e,r=document.createRange();r.setStart(n,0),r.setEnd(i,0);var l=r.commonAncestorContainer;if(e!==l&&t!==l||n.contains(i))return s(l)?l:p(l);var f=d(e);return f.host?a(f.host,t):a(e,d(t).host)}function l(e){var t=1=o.clientWidth&&n>=o.clientHeight}),l=0a[e]&&!t.escapeWithReference&&(n=Q(f[o],a[e]-('right'===e?f.width:f.height))),ae({},o,n)}};return l.forEach(function(e){var t=-1===['left','top'].indexOf(e)?'secondary':'primary';f=le({},f,m[t](e))}),e.offsets.popper=f,e},priority:['left','right','top','bottom'],padding:5,boundariesElement:'scrollParent'},keepTogether:{order:400,enabled:!0,fn:function(e){var t=e.offsets,o=t.popper,n=t.reference,i=e.placement.split('-')[0],r=Z,p=-1!==['top','bottom'].indexOf(i),s=p?'right':'bottom',d=p?'left':'top',a=p?'width':'height';return o[s]r(n[s])&&(e.offsets.popper[d]=r(n[s])),e}},arrow:{order:500,enabled:!0,fn:function(e,o){var n;if(!K(e.instance.modifiers,'arrow','keepTogether'))return e;var i=o.element;if('string'==typeof i){if(i=e.instance.popper.querySelector(i),!i)return e;}else if(!e.instance.popper.contains(i))return console.warn('WARNING: `arrow.element` must be child of its popper element!'),e;var r=e.placement.split('-')[0],p=e.offsets,s=p.popper,d=p.reference,a=-1!==['left','right'].indexOf(r),l=a?'height':'width',f=a?'Top':'Left',m=f.toLowerCase(),h=a?'left':'top',c=a?'bottom':'right',u=S(i)[l];d[c]-us[c]&&(e.offsets.popper[m]+=d[m]+u-s[c]),e.offsets.popper=g(e.offsets.popper);var b=d[m]+d[l]/2-u/2,w=t(e.instance.popper),y=parseFloat(w['margin'+f],10),E=parseFloat(w['border'+f+'Width'],10),v=b-e.offsets.popper[m]-y-E;return v=ee(Q(s[l]-u,v),0),e.arrowElement=i,e.offsets.arrow=(n={},ae(n,m,$(v)),ae(n,h,''),n),e},element:'[x-arrow]'},flip:{order:600,enabled:!0,fn:function(e,t){if(W(e.instance.modifiers,'inner'))return e;if(e.flipped&&e.placement===e.originalPlacement)return e;var o=v(e.instance.popper,e.instance.reference,t.padding,t.boundariesElement,e.positionFixed),n=e.placement.split('-')[0],i=T(n),r=e.placement.split('-')[1]||'',p=[];switch(t.behavior){case ce.FLIP:p=[n,i];break;case ce.CLOCKWISE:p=G(n);break;case ce.COUNTERCLOCKWISE:p=G(n,!0);break;default:p=t.behavior;}return p.forEach(function(s,d){if(n!==s||p.length===d+1)return e;n=e.placement.split('-')[0],i=T(n);var a=e.offsets.popper,l=e.offsets.reference,f=Z,m='left'===n&&f(a.right)>f(l.left)||'right'===n&&f(a.left)f(l.top)||'bottom'===n&&f(a.top)f(o.right),g=f(a.top)f(o.bottom),b='left'===n&&h||'right'===n&&c||'top'===n&&g||'bottom'===n&&u,w=-1!==['top','bottom'].indexOf(n),y=!!t.flipVariations&&(w&&'start'===r&&h||w&&'end'===r&&c||!w&&'start'===r&&g||!w&&'end'===r&&u),E=!!t.flipVariationsByContent&&(w&&'start'===r&&c||w&&'end'===r&&h||!w&&'start'===r&&u||!w&&'end'===r&&g),v=y||E;(m||b||v)&&(e.flipped=!0,(m||b)&&(n=p[d+1]),v&&(r=z(r)),e.placement=n+(r?'-'+r:''),e.offsets.popper=le({},e.offsets.popper,C(e.instance.popper,e.offsets.reference,e.placement)),e=P(e.instance.modifiers,e,'flip'))}),e},behavior:'flip',padding:5,boundariesElement:'viewport',flipVariations:!1,flipVariationsByContent:!1},inner:{order:700,enabled:!1,fn:function(e){var t=e.placement,o=t.split('-')[0],n=e.offsets,i=n.popper,r=n.reference,p=-1!==['left','right'].indexOf(o),s=-1===['top','left'].indexOf(o);return i[p?'left':'top']=r[o]-(s?i[p?'width':'height']:0),e.placement=T(t),e.offsets.popper=g(i),e}},hide:{order:800,enabled:!0,fn:function(e){if(!K(e.instance.modifiers,'hide','preventOverflow'))return e;var t=e.offsets.reference,o=D(e.instance.modifiers,function(e){return'preventOverflow'===e.name}).boundaries;if(t.bottomo.right||t.top>o.bottom||t.rightwindow.devicePixelRatio||!fe),c='bottom'===o?'top':'bottom',g='right'===n?'left':'right',b=B('transform');if(d='bottom'==c?'HTML'===l.nodeName?-l.clientHeight+h.bottom:-f.height+h.bottom:h.top,s='right'==g?'HTML'===l.nodeName?-l.clientWidth+h.right:-f.width+h.right:h.left,a&&b)m[b]='translate3d('+s+'px, '+d+'px, 0)',m[c]=0,m[g]=0,m.willChange='transform';else{var w='bottom'==c?-1:1,y='right'==g?-1:1;m[c]=d*w,m[g]=s*y,m.willChange=c+', '+g}var E={"x-placement":e.placement};return e.attributes=le({},E,e.attributes),e.styles=le({},m,e.styles),e.arrowStyles=le({},e.offsets.arrow,e.arrowStyles),e},gpuAcceleration:!0,x:'bottom',y:'right'},applyStyle:{order:900,enabled:!0,fn:function(e){return V(e.instance.popper,e.styles),j(e.instance.popper,e.attributes),e.arrowElement&&Object.keys(e.arrowStyles).length&&V(e.arrowElement,e.arrowStyles),e},onLoad:function(e,t,o,n,i){var r=L(i,t,e,o.positionFixed),p=O(o.placement,r,t,e,o.modifiers.flip.boundariesElement,o.modifiers.flip.padding);return t.setAttribute('x-placement',p),V(t,{position:o.positionFixed?'fixed':'absolute'}),o},gpuAcceleration:void 0}}},ge}); 5 | //# sourceMappingURL=popper.min.js.map 6 | -------------------------------------------------------------------------------- /tests/Feature/FeatureTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(true); 15 | } 16 | } 17 | 18 | ?> -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | assertNotEmpty(App::get('config')); 16 | } 17 | 18 | /** @test */ 19 | public function database_is_not_empty() 20 | { 21 | $this->assertNotEmpty(App::DB()); 22 | } 23 | 24 | /** @test */ 25 | public function users_store() 26 | { 27 | $testUser = App::DB()->insert('users', [ 28 | 'name' => 'TestUser' 29 | ]); 30 | $secondUser = App::DB()->insert('users', [ 31 | 'name' => 'SecondUser' 32 | ]); 33 | $this->assertNotEmpty($testUser); 34 | $this->assertNotEmpty($secondUser); 35 | } 36 | 37 | /** @test */ 38 | public function users_index() 39 | { 40 | $users = App::DB()->selectAll('users'); 41 | $this->assertNotEmpty($users); 42 | } 43 | 44 | /** @test */ 45 | public function users_limit() 46 | { 47 | $count = 2; 48 | $users = App::DB()->selectAll('users', $count); 49 | //echo App::DB()->getSql(); 50 | $this->assertCount($count, $users); 51 | } 52 | 53 | /** @test */ 54 | public function user_show() 55 | { 56 | $user = App::DB()->selectAllWhere('users', [ 57 | ['name', '=', 'TestUser'], 58 | ]); 59 | //echo App::DB()->getSql(); 60 | $this->assertNotEmpty($user); 61 | } 62 | 63 | /** @test */ 64 | public function user_update() 65 | { 66 | $user = App::DB()->updateWhere('users', [ 67 | 'name' => 'FirstUser' 68 | ], [ 69 | ['name', '=', 'TestUser'] 70 | ]); 71 | $this->assertNotEmpty($user); 72 | $renamedUser = App::DB()->selectAllWhere('users', [ 73 | ['name', '=', 'FirstUser'], 74 | ]); 75 | $this->assertEquals($renamedUser[0]->name, 'FirstUser'); 76 | } 77 | 78 | /** @test */ 79 | public function users_delete() 80 | { 81 | $firstUser = App::DB()->deleteWhere('users', [ 82 | ['name', '=', 'FirstUser'], 83 | ]); 84 | $this->assertNotEmpty($firstUser); 85 | $firstDeletedUser = App::DB()->selectAllWhere('users', [ 86 | ['name', '=', 'FirstUser'], 87 | ]); 88 | $this->assertEmpty($firstDeletedUser); 89 | $secondUser = App::DB()->deleteWhere('users', [ 90 | ['name', '=', 'SecondUser'], 91 | ]); 92 | $this->assertNotEmpty($secondUser); 93 | $secondDeletedUser = App::DB()->selectAllWhere('users', [ 94 | ['name', '=', 'SecondUser'], 95 | ]); 96 | $this->assertEmpty($secondDeletedUser); 97 | } 98 | 99 | /** @test */ 100 | public function user_model_add() 101 | { 102 | $user = new User(); 103 | $user = $user->add(['name' => 'TestUser']); 104 | $this->assertEquals($user->first()->name, 'TestUser'); 105 | $user = $user->where([['name', '=', 'TestUser']])->first(); 106 | $this->assertEquals($user->name, 'TestUser'); 107 | } 108 | 109 | /** @test */ 110 | public function user_model_index() 111 | { 112 | $users = User::all(); 113 | $this->assertNotEmpty($users); 114 | } 115 | 116 | /** @test */ 117 | public function user_model_first_or_fail() 118 | { 119 | $user = new User(); 120 | $user->where([['name', '=', 'TestUser']])->firstOrFail(); 121 | $this->assertNotEmpty($user); 122 | } 123 | 124 | /** @test */ 125 | public function user_model_find() 126 | { 127 | $user = new User(); 128 | $user = $user->add(['name' => 'NewUserHere']); 129 | $newUser = $user->find($user->first()->id()); 130 | $this->assertNotEmpty($newUser); 131 | $newUser->deleteWhere([[$user->first()->primary(), '=', $user->first()->id()]]); 132 | $user = $user->find(-1); 133 | $this->assertNull($user); 134 | } 135 | 136 | /** @test */ 137 | public function user_model_find_or_fail() 138 | { 139 | $user = new User(); 140 | $this->expectExceptionMessage("ModelNotFoundException"); 141 | $user->findOrFail(-1); 142 | } 143 | 144 | /** @test */ 145 | public function users_raw_query() 146 | { 147 | $unnamedUsers = App::DB()->raw('SELECT * FROM users WHERE user_id > ?', [0]); 148 | $this->assertNotEmpty(count($unnamedUsers)); 149 | $namedUsers = App::DB()->raw('SELECT * FROM users WHERE user_id > :user_id', ['user_id' => 0]); 150 | $this->assertNotEmpty(count($namedUsers)); 151 | $newUser = App::DB()->raw('INSERT INTO users(name) VALUES (?)', ['TestingUser']); 152 | $this->assertNotEmpty($newUser); 153 | $deleteUser = App::DB()->raw('DELETE FROM users WHERE name = :name', ['name' => 'TestingUser']); 154 | $this->assertNotEmpty($deleteUser); 155 | $deletedUser = App::DB()->raw('SELECT * FROM users WHERE name = ?', ['TestingUser']); 156 | $this->assertEmpty(count($deletedUser)); 157 | } 158 | 159 | /** @test */ 160 | public function user_model_update() 161 | { 162 | $user = new User(); 163 | $user->updateWhere(['name' => 'SomeUser'], [['name', '=', 'TestUser']]); 164 | $user = $user->where([['name', '=', 'SomeUser']])->first(); 165 | $this->assertEquals($user->name, 'SomeUser'); 166 | } 167 | 168 | /** @test */ 169 | public function user_model_save() 170 | { 171 | $user = new User(); 172 | $user = $user->where([['name', '=', 'SomeUser']])->first(); 173 | $this->assertEquals($user->name, 'SomeUser'); 174 | $user->name = 'ThisUser'; 175 | $user->save(); 176 | $this->assertEquals($user->name, 'ThisUser'); 177 | } 178 | 179 | /** @test */ 180 | public function user_model_delete() 181 | { 182 | $user = new User(); 183 | $user->deleteWhere([['name', '=', 'ThisUser']]); 184 | $deletedUser = $user->where([['name', '=', 'ThisUser']])->first(); 185 | $this->assertEmpty($deletedUser); 186 | } 187 | 188 | /** @test */ 189 | public function project_model_add() 190 | { 191 | $project = new Project(); 192 | $project = $project->add(['name' => 'TestProject']); 193 | //echo $project->getSql(); 194 | $this->assertEquals($project->first()->name, 'TestProject'); 195 | $project = $project->where([['name', '=', 'TestProject']])->first(); 196 | //echo $project->getSql(); 197 | $this->assertEquals($project->name, 'TestProject'); 198 | 199 | } 200 | 201 | /** @test */ 202 | public function project_model_save() 203 | { 204 | $project = new Project(); 205 | $foundProject = $project->where([['name', '=', 'TestProject']])->first(); 206 | //dd($project->describe()); 207 | //dd($foundProject->describe()); 208 | $this->assertEquals($foundProject->name, 'TestProject'); 209 | $foundProject->name = 'SomeProject'; 210 | $foundProject->save(); 211 | $this->assertEquals($project->first()->name, 'SomeProject'); 212 | } 213 | 214 | /** @test */ 215 | public function project_model_index() 216 | { 217 | $projects = Project::all(); 218 | $this->assertNotEmpty($projects); 219 | } 220 | 221 | /** @test */ 222 | public function projects_raw_query() 223 | { 224 | $unnamedProjects = App::DB()->raw('SELECT * FROM projects WHERE project_id > ?', [0]); 225 | //echo App::DB()->getSql(); 226 | $this->assertNotEmpty(count($unnamedProjects)); 227 | $namedProjects = App::DB()->raw('SELECT * FROM projects WHERE project_id > :project_id', ['project_id' => 0]); 228 | //echo App::DB()->getSql(); 229 | $this->assertNotEmpty(count($namedProjects)); 230 | $newProject = App::DB()->raw('INSERT INTO projects(name) VALUES (?)', ['TestingProject']); 231 | $this->assertNotEmpty($newProject); 232 | $deleteProject = App::DB()->raw('DELETE FROM projects WHERE name = :name', ['name' => 'TestingProject']); 233 | //echo App::DB()->getSql(); 234 | $this->assertNotEmpty($deleteProject); 235 | $deletedProject = App::DB()->raw('SELECT * FROM projects WHERE name = ?', ['TestingProject']); 236 | $this->assertEmpty(count($deletedProject)); 237 | } 238 | 239 | /** @test */ 240 | public function project_model_delete() 241 | { 242 | $project = new Project(); 243 | $project->deleteWhere([['name', '=', 'TestProject']]); 244 | $deletedProject = $project->where([['name', '=', 'TestProject']])->get(); 245 | $this->assertEmpty($deletedProject); 246 | } 247 | 248 | /** @test */ 249 | public function users_model_paginate() 250 | { 251 | $user = new User(); 252 | for ($i = 0; $i < 5; $i++) { 253 | $user->add(['name' => 'TestUser']); 254 | } 255 | $num = $user->count(); 256 | $this->assertNotEmpty($num); 257 | $numMany = $user->count([['user_id', '>', '3']]); 258 | $this->assertNotEmpty($numMany); 259 | $page = 1; 260 | $limit = 2; 261 | $pageOneUser = new User(); 262 | $pageOneUsers = $pageOneUser->where([['user_id', '>', '0']], $limit, ($page - 1) * $limit)->get(); 263 | //echo $pageOneUsers->getSql(); 264 | $this->assertNotNull($pageOneUsers); 265 | $this->assertCount(2, $pageOneUsers); 266 | $page = 2; 267 | $pageTwoUser = new User(); 268 | $pageTwoUsers = $pageTwoUser->where([['user_id', '>', '0']], $limit, ($page - 1) * $limit)->get(); 269 | $this->assertNotNull($pageTwoUsers); 270 | $this->assertCount(2, $pageTwoUsers); 271 | $this->assertNotEquals($pageTwoUsers[0]->user_id, $pageOneUsers[0]->user_id); 272 | //echo $pageTwoUsers->getSql(); 273 | $user->deleteWhere([['name', '=', 'TestUser']]); 274 | $deletedUsers = $user->where([['name', '=', 'TestUser']])->get(); 275 | $this->assertEmpty($deletedUsers); 276 | } 277 | } 278 | 279 | ?> --------------------------------------------------------------------------------