more work
This commit is contained in:
parent
2263134e55
commit
36d8f3405f
18
color.php
18
color.php
|
@ -4,12 +4,12 @@
|
|||
* A collection of functions to colorize the output of the terminal.
|
||||
*/
|
||||
|
||||
function c(string $c, string $s): string { return $c . $s . "\033[0m"; }
|
||||
function black(string $s): string { return c("\033[30m", $s); }
|
||||
function red(string $s): string { return c("\033[31m", $s); }
|
||||
function green(string $s): string { return c("\033[32m", $s); }
|
||||
function yellow(string $s): string { return c("\033[33m", $s); }
|
||||
function blue(string $s): string { return c("\033[34m", $s); }
|
||||
function magenta(string $s): string { return c("\033[35m", $s); }
|
||||
function cyan(string $s): string { return c("\033[36m", $s); }
|
||||
function white(string $s): string { return c("\033[37m", $s); }
|
||||
function c($c, $s) { return $c . $s . "\033[0m"; }
|
||||
function black($s) { return c("\033[30m", $s); }
|
||||
function red($s) { return c("\033[31m", $s); }
|
||||
function green($s) { return c("\033[32m", $s); }
|
||||
function yellow($s) { return c("\033[33m", $s); }
|
||||
function blue($s) { return c("\033[34m", $s); }
|
||||
function magenta($s) { return c("\033[35m", $s); }
|
||||
function cyan($s) { return c("\033[36m", $s); }
|
||||
function white($s) { return c("\033[37m", $s); }
|
||||
|
|
BIN
database/auth.db
BIN
database/auth.db
Binary file not shown.
BIN
database/live.db
BIN
database/live.db
Binary file not shown.
|
@ -15,7 +15,7 @@ const BPS = 'blueprints.db';
|
|||
/**
|
||||
* Echo a string with a newline.
|
||||
*/
|
||||
function eln(string $string): void
|
||||
function eln($string)
|
||||
{
|
||||
echo $string . PHP_EOL;
|
||||
}
|
||||
|
@ -63,7 +63,7 @@ if ($database === AUTH || $database === 'reset') {
|
|||
password TEXT NOT NULL,
|
||||
auth INT NOT NULL DEFAULT 0,
|
||||
char_id INTEGER NOT NULL DEFAULT 0,
|
||||
char_slots INTEGER NOT NULL DEFAULT 3,
|
||||
char_slots INTEGER NOT NULL DEFAULT 300,
|
||||
created DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
last_login DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)');
|
||||
|
@ -656,7 +656,7 @@ if ($database === LIVE || $database === 'reset') {
|
|||
if ($database !== 'reset') exit(0);
|
||||
}
|
||||
|
||||
function created_or_error(bool $result, string $table): void
|
||||
function created_or_error($result, $table)
|
||||
{
|
||||
if ($result === false) {
|
||||
eln(red('Failed to create table: ') . $table);
|
||||
|
|
56
docs/captcha.md
Normal file
56
docs/captcha.md
Normal file
|
@ -0,0 +1,56 @@
|
|||
Here is some example code for implementing a CAPTCHA, using the gd extension to create a server-rendered, randomized image.
|
||||
|
||||
```php
|
||||
<?php
|
||||
// Start the session to store the CAPTCHA answer
|
||||
session_start();
|
||||
|
||||
// Define the image dimensions
|
||||
$width = 200;
|
||||
$height = 60;
|
||||
|
||||
// Create the image resource
|
||||
$image = imagecreatetruecolor($width, $height);
|
||||
|
||||
// Define colors
|
||||
$bg_color = imagecolorallocate($image, 255, 255, 255); // White background
|
||||
$text_color = imagecolorallocate($image, 0, 0, 0); // Black text
|
||||
$noise_color = imagecolorallocate($image, 100, 100, 100); // Gray for noise
|
||||
|
||||
// Fill the background
|
||||
imagefill($image, 0, 0, $bg_color);
|
||||
|
||||
// Generate a random math equation (e.g., 3 + 5)
|
||||
$num1 = rand(1, 9);
|
||||
$num2 = rand(1, 9);
|
||||
$operator = rand(0, 1) ? '+' : '-'; // Randomly choose addition or subtraction
|
||||
$equation = "$num1 $operator $num2 = ?";
|
||||
$answer = $operator == '+' ? ($num1 + $num2) : ($num1 - $num2);
|
||||
|
||||
// Store the answer in the session
|
||||
$_SESSION['captcha'] = $answer;
|
||||
|
||||
// Add noise to the image (random dots)
|
||||
for ($i = 0; $i < 1000; $i++) {
|
||||
imagesetpixel($image, rand(0, $width), rand(0, $height), $noise_color);
|
||||
}
|
||||
|
||||
// Set the path to the ZXX font
|
||||
$font = __DIR__ . '/fonts/ZXX-Regular.ttf'; // Path to the ZXX font file
|
||||
|
||||
// Add the math equation using ZXX font
|
||||
$font_size = 30;
|
||||
$x = 20;
|
||||
$y = 40;
|
||||
imagettftext($image, $font_size, rand(-10, 10), $x, $y, $text_color, $font, $equation);
|
||||
|
||||
// Output the image as PNG
|
||||
header('Content-Type: image/png');
|
||||
imagepng($image);
|
||||
|
||||
// Free the image resource
|
||||
imagedestroy($image);
|
||||
?>
|
||||
```
|
||||
|
||||
https://github.com/kawaiidesune/zxx
|
|
@ -136,7 +136,19 @@ aside#left {
|
|||
|
||||
&:hover, &.active {
|
||||
color: white;
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: #444c55;
|
||||
color: #ffffff;
|
||||
background-image: linear-gradient(rgba(255, 255, 255, 0.15), rgba(0, 0, 0, 0.1));
|
||||
border: 1px solid;
|
||||
border-color: #3D444C #2F353B #2C3137;
|
||||
box-shadow: 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -182,6 +194,7 @@ span.badge {
|
|||
color: #111111;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.1rem 0.25rem;
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1) inset;
|
||||
|
||||
&.dark {
|
||||
background-color: #444c55;
|
||||
|
@ -343,3 +356,14 @@ h1:has(.badge), h2:has(.badge), h3:has(.badge), h4:has(.badge), h5:has(.badge),
|
|||
border-color: #b3b3b3;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: #4C0515;
|
||||
text-decoration: none;
|
||||
transition: color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: #6C0515;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,99 +3,51 @@
|
|||
outline: none;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 34px;
|
||||
padding: 6px 12px;
|
||||
font-size: 14px;
|
||||
line-height: 1.42857143;
|
||||
color: #555555;
|
||||
background-color: #fff;
|
||||
background-image: none;
|
||||
border: 1px solid #ccc;
|
||||
padding: 0.5rem;
|
||||
color: white;
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
||||
-webkit-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
|
||||
-o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
|
||||
-webkit-transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s;
|
||||
transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s;
|
||||
transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
|
||||
transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s, -webkit-box-shadow ease-in-out .15s;
|
||||
}
|
||||
|
||||
/*
|
||||
.radio-block {
|
||||
& > input[type="radio"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& > label {
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
border: none;
|
||||
box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.1);
|
||||
font-size: 1rem;
|
||||
background: #f7f8fa linear-gradient(rgba(255, 255, 255, 0), rgba(0, 0, 0, 0.1));
|
||||
box-shadow: 0 1px 0 1px rgba(255, 255, 255, 0.3) inset, 0 0 0 1px #adb2bb inset;
|
||||
color: #111111;
|
||||
padding: 0.5rem 1rem 0.5rem;
|
||||
text-align: center;
|
||||
border-radius: 3px;
|
||||
user-select: none;
|
||||
text-decoration: none;
|
||||
transition: opacity 0.1s ease, background-color 0.1s ease, color 0.1s ease, background 0.1s ease;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
|
||||
&::placeholder {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #e0e0e0;
|
||||
background-image: linear-gradient(rgba(255, 255, 255, 0), rgba(0, 0, 0, 0.1));
|
||||
box-shadow: 0 1px 0 1px rgba(255, 255, 255, 0.3) inset, 0 0 0 1px #adb2bb inset;
|
||||
color: rgba(0, 0, 0, 0.8);
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
& > .badge {
|
||||
background-color: #444c55;
|
||||
color: white;
|
||||
}
|
||||
&:focus {
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
border-color: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
&.active > label {
|
||||
background-color: #444c55;
|
||||
color: #ffffff;
|
||||
background-image: linear-gradient(rgba(255, 255, 255, 0.15), rgba(0, 0, 0, 0.1));
|
||||
border: 1px solid;
|
||||
border-color: #3D444C #2F353B #2C3137;
|
||||
box-shadow: 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset;
|
||||
&.error {
|
||||
background-color: rgba(255, 43, 43, 0.2);
|
||||
|
||||
& > .badge {
|
||||
background-color: #f7f8fa;
|
||||
color: #111111;
|
||||
}
|
||||
&:hover {
|
||||
background-color: rgba(255, 43, 43, 0.3);
|
||||
}
|
||||
|
||||
/* When the radio button is checked, change the background color of the label
|
||||
& > input[type="radio"]:checked + label {
|
||||
background-color: #5a9bd4;
|
||||
background-image: linear-gradient(rgba(255, 255, 255, 0.15), rgba(60, 100, 150, 0.1));
|
||||
box-shadow: 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset;
|
||||
border: 1px solid;
|
||||
border-color: #4a8ab0 #3a7a9c #2a6a88;
|
||||
&:focus {
|
||||
background-color: rgba(255, 43, 43, 0.3);
|
||||
border-color: rgba(255, 43, 43, 0.8);
|
||||
}
|
||||
|
||||
/* When the radio button is disabled, show a normal cursor
|
||||
& > input[type="radio"]:disabled + label {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
.radio-block-2 {
|
||||
& > input[type="radio"] {
|
||||
display: none;
|
||||
}
|
||||
.form.group {
|
||||
margin-bottom: 1rem;
|
||||
|
||||
& > label {
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
& > .form.control:not(:last-child) {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -113,28 +65,48 @@
|
|||
width: 100%;
|
||||
border-radius: 0.15rem;
|
||||
cursor: pointer;
|
||||
transition: color, background-color 0.2s ease;
|
||||
transition: color, background-color, border-color, background-image 0.2s ease;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid transparent;
|
||||
background-image: linear-gradient(rgba(255, 255, 255, 0), rgba(0, 0, 0, 0));
|
||||
|
||||
&:hover {
|
||||
background-color: black;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
color: white;
|
||||
}
|
||||
|
||||
& > .badge {
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
& > span.selected {
|
||||
display: none;
|
||||
margin-left: auto;
|
||||
color: #a6e3a1;
|
||||
}
|
||||
}
|
||||
|
||||
&.active > label {
|
||||
background-color: black;
|
||||
color: white;
|
||||
background-color: #444c55;
|
||||
color: #ffffff;
|
||||
background-image: linear-gradient(rgba(255, 255, 255, 0.15), rgba(0, 0, 0, 0.1));
|
||||
border: 1px solid;
|
||||
border-color: #3D444C #2F353B #2C3137;
|
||||
box-shadow: 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset;
|
||||
|
||||
& > span.selected {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
/* When the radio button is checked, change the background color of the label */
|
||||
& > input[type="radio"]:checked + label {
|
||||
background-color: #f4cc67;
|
||||
background-image: linear-gradient(rgba(255, 255, 255, 0.15), rgba(0, 0, 0, 0.1));
|
||||
box-shadow: 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset;
|
||||
color: #111111;
|
||||
border: 1px solid;
|
||||
border-color: #C59F43 #AA8326 #957321;
|
||||
}
|
||||
|
||||
/* When the radio button is disabled, show a normal cursor */
|
||||
|
@ -142,3 +114,8 @@
|
|||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
/* If there is no character selected, hide the buttons */
|
||||
.character-select:not(:has(input[type="radio"]:checked)) > .buttons {
|
||||
display: none;
|
||||
}
|
||||
|
|
12
src/auth.php
12
src/auth.php
|
@ -3,7 +3,7 @@
|
|||
/**
|
||||
* Checks if the given username already exists.
|
||||
*/
|
||||
function auth_username_exists(string $username): bool
|
||||
function auth_username_exists($username)
|
||||
{
|
||||
return db_exists(db_auth(), 'users', 'username', $username);
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ function auth_username_exists(string $username): bool
|
|||
/**
|
||||
* Checks if the given email already exists.
|
||||
*/
|
||||
function auth_email_exists(string $email): bool
|
||||
function auth_email_exists($email)
|
||||
{
|
||||
return db_exists(db_auth(), 'users', 'email', $email);
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ function auth_email_exists(string $email): bool
|
|||
* Check for a user session. If $_SESSION['user'] already exists, return early. If not, check for a remember me
|
||||
* cookie. If a remember me cookie exists, validate the session and set $_SESSION['user'].
|
||||
*/
|
||||
function auth_check(): bool
|
||||
function auth_check()
|
||||
{
|
||||
if (isset($_SESSION['user'])) return true;
|
||||
|
||||
|
@ -42,7 +42,7 @@ function auth_check(): bool
|
|||
* Ensure a user is logged in, or redirect to the login page. This will also check for a remember me cookie and
|
||||
* populate the $_SESSION['user'] array.
|
||||
*/
|
||||
function auth_only(): void
|
||||
function auth_only()
|
||||
{
|
||||
if (!auth_check()) redirect('/auth/login');
|
||||
}
|
||||
|
@ -50,7 +50,7 @@ function auth_only(): void
|
|||
/**
|
||||
* If there is a user logged in, redirect to the home page. Used for when we have a guest-only page.
|
||||
*/
|
||||
function guest_only(): void
|
||||
function guest_only()
|
||||
{
|
||||
if (auth_check()) redirect('/');
|
||||
}
|
||||
|
@ -59,7 +59,7 @@ function guest_only(): void
|
|||
* Ensure the user has a character selected. If they have no character, redirect to the character creation page. Otherwise,
|
||||
* select the first character attached to the user.
|
||||
*/
|
||||
function must_have_character(): void
|
||||
function must_have_character()
|
||||
{
|
||||
// If there is a character selected, make sure the session is up to date.
|
||||
if ($_SESSION['user']['char_id'] !== 0) {
|
||||
|
|
|
@ -11,6 +11,7 @@ require_once SRC . '/database.php';
|
|||
require_once SRC . '/auth.php';
|
||||
require_once SRC . '/router.php';
|
||||
require_once SRC . '/components.php';
|
||||
require_once SRC . '/render.php';
|
||||
|
||||
// Database models
|
||||
require_once SRC . '/models/user.php';
|
||||
|
@ -39,5 +40,8 @@ csrf();
|
|||
// Have a global counter for queries
|
||||
$GLOBALS['queries'] = 0;
|
||||
|
||||
// Set the default page layout
|
||||
page_layout('basic');
|
||||
|
||||
// Run auth_check to see if we're logged in, since it populates the user data in SESSION
|
||||
auth_check();
|
||||
|
|
|
@ -8,7 +8,7 @@ const nav_tabs = [
|
|||
/**
|
||||
* Render the logout button's form.
|
||||
*/
|
||||
function c_logout_button(): string
|
||||
function c_logout_button()
|
||||
{
|
||||
return render('components/logout_button');
|
||||
}
|
||||
|
@ -17,24 +17,31 @@ function c_logout_button(): string
|
|||
* Render the character bar. Relies on there being a character in the session. Without one, this will return an empty
|
||||
* string.
|
||||
*/
|
||||
function c_char_bar(): string
|
||||
function c_char_bar()
|
||||
{
|
||||
if (char() === false) return '';
|
||||
return render('components/char_bar', ['char' => char()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the left sidebar navigation menu. Provide the active tab to highlight it.
|
||||
* Render the left sidebar navigation menu. Provide the active tab to highlight it. Will retrieve the current
|
||||
* tab from the GLOBALS.
|
||||
*/
|
||||
function c_left_nav(int $activeTab): string
|
||||
function c_left_nav()
|
||||
{
|
||||
return render('components/left_nav', ['activeTab' => $activeTab]);
|
||||
$tab = match ($GLOBALS['active_nav_tab'] ?? '') {
|
||||
'home' => 0,
|
||||
'chars' => 1,
|
||||
default => 0
|
||||
};
|
||||
|
||||
return render('components/left_nav', ['ant' => $tab]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the debug query log.
|
||||
*/
|
||||
function c_debug_query_log(): string
|
||||
function c_debug_query_log()
|
||||
{
|
||||
return render('components/debug_query_log');
|
||||
}
|
||||
|
@ -42,7 +49,7 @@ function c_debug_query_log(): string
|
|||
/**
|
||||
* Render the character select radio buttons.
|
||||
*/
|
||||
function c_char_select_box(int $id, array $char): string
|
||||
function c_char_select_box($id, array $char)
|
||||
{
|
||||
return render('components/char_select_box', ['id' => $id, 'char' => $char]);
|
||||
}
|
||||
|
@ -50,15 +57,34 @@ function c_char_select_box(int $id, array $char): string
|
|||
/**
|
||||
* Render an alert with a given type and message.
|
||||
*/
|
||||
function c_alert(string $t, string $m): string
|
||||
function c_alert($t, $m)
|
||||
{
|
||||
return "<div class=\"alert $t\">$m</div>";
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a form field.
|
||||
* Renders a danger alert with form errors, if there are any. Add an optional placement id.
|
||||
*/
|
||||
function c_form_field(string $type, string $name, string $id, string $placeholder, bool $required = false): string
|
||||
function c_form_errors($placement = '')
|
||||
{
|
||||
return render('components/form_field', ['type' => $type, 'name' => $name, 'id' => $id, 'placeholder' => $placeholder, 'required' => $required]);
|
||||
$errors = $GLOBALS[($placement !== '' ? "form-errors-$placement" : 'form-errors')] ?? false;
|
||||
if ($errors === false) return '';
|
||||
$html = '';
|
||||
foreach ($errors as $field)
|
||||
foreach ($field as $message) $html .= "<p>$message</p>";
|
||||
return c_alert('danger', $html);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a text form field. This component will automatically add the error class if there are errors for this field,
|
||||
* depending on the form-errors GLOBAL. Pass an optional form ID to target a specific form GLOBAL.
|
||||
*/
|
||||
function c_form_field($type, $name, $placeholder, $required = false, $autocomplete = "off", $formId = '')
|
||||
{
|
||||
$errors = $GLOBALS[($formId !== '' ? "form-errors-$formId" : 'form-errors')] ?? false;
|
||||
$html = "<input type=\"$type\" name=\"$name\" id=\"$name\" placeholder=\"$placeholder\"";
|
||||
if ($required) $html .= ' required';
|
||||
if (isset($_POST[$name]) && $type !== 'password') $html .= ' value="' . $_POST[$name] . '"';
|
||||
$html .= $errors !== false && !empty($errors[$name]) ? ' class="form control error"' : ' class="form control"';
|
||||
return $html . " autocomplete=\"$autocomplete\">";
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
/**
|
||||
* Displays the registration page.
|
||||
*/
|
||||
function auth_controller_register_get(): void
|
||||
function auth_controller_register_get()
|
||||
{
|
||||
guest_only();
|
||||
echo render('layouts/basic', ['view' => 'pages/auth/register']);
|
||||
|
@ -12,20 +12,16 @@ function auth_controller_register_get(): void
|
|||
/**
|
||||
* Handles the registration form submission.
|
||||
*/
|
||||
function auth_controller_register_post(): void
|
||||
function auth_controller_register_post()
|
||||
{
|
||||
guest_only();
|
||||
csrf_ensure();
|
||||
|
||||
$errors = [];
|
||||
|
||||
$u = $_POST['username'] ?? '';
|
||||
$e = $_POST['email'] ?? '';
|
||||
$p = $_POST['password'] ?? '';
|
||||
|
||||
// Trim the input.
|
||||
$u = trim($u);
|
||||
$e = trim($e);
|
||||
$u = trim($_POST['u'] ?? '');
|
||||
$e = trim($_POST['e'] ?? '');
|
||||
$p = $_POST['p'] ?? '';
|
||||
|
||||
/*
|
||||
A username is required.
|
||||
|
@ -54,12 +50,6 @@ function auth_controller_register_post(): void
|
|||
$errors['p'][] = 'Password is required and must be at least 6 characters long.';
|
||||
}
|
||||
|
||||
// If there are errors at this point, send them to the page with errors flashed.
|
||||
if (!empty($errors)) {
|
||||
flash('alert-registration', ['errors', $errors]);
|
||||
redirect('/auth/register');
|
||||
}
|
||||
|
||||
/*
|
||||
A username must be unique.
|
||||
*/
|
||||
|
@ -76,8 +66,9 @@ function auth_controller_register_post(): void
|
|||
|
||||
// If there are errors at this point, send them to the page with errors flashed.
|
||||
if (!empty($errors)) {
|
||||
flash('alert-registration', ['errors', $errors]);
|
||||
redirect('/auth/register');
|
||||
$GLOBALS['form-errors'] = $errors;
|
||||
echo page('auth/register');
|
||||
exit;
|
||||
}
|
||||
|
||||
$user = user_create($u, $e, $p);
|
||||
|
@ -91,7 +82,7 @@ function auth_controller_register_post(): void
|
|||
/**
|
||||
* Displays the login page.
|
||||
*/
|
||||
function auth_controller_login_get(): void
|
||||
function auth_controller_login_get()
|
||||
{
|
||||
guest_only();
|
||||
echo render('layouts/basic', ['view' => 'pages/auth/login']);
|
||||
|
@ -100,48 +91,35 @@ function auth_controller_login_get(): void
|
|||
/**
|
||||
* Handles the login form submission.
|
||||
*/
|
||||
function auth_controller_login_post(): void
|
||||
function auth_controller_login_post()
|
||||
{
|
||||
guest_only();
|
||||
csrf_ensure();
|
||||
|
||||
$errors = [];
|
||||
|
||||
$u = $_POST['username'] ?? '';
|
||||
$p = $_POST['password'] ?? '';
|
||||
$u = trim($_POST['u'] ?? '');
|
||||
$p = $_POST['p'] ?? '';
|
||||
|
||||
// Trim the input.
|
||||
$u = trim($u);
|
||||
|
||||
/*
|
||||
A username is required.
|
||||
*/
|
||||
if (empty($u)) {
|
||||
$errors['u'][] = 'Username is required.';
|
||||
}
|
||||
|
||||
/*
|
||||
A password is required.
|
||||
*/
|
||||
if (empty($p)) {
|
||||
$errors['p'][] = 'Password is required.';
|
||||
}
|
||||
if (empty($u)) $errors['u'][] = 'Username is required.';
|
||||
if (empty($p)) $errors['p'][] = 'Password is required.';
|
||||
|
||||
// If there are errors at this point, send them to the page with errors flashed.
|
||||
if (!empty($errors)) {
|
||||
flash('errors', $errors);
|
||||
redirect('/auth/login');
|
||||
$GLOBALS['form-errors'] = $errors;
|
||||
echo render('layouts/basic', ['view' => 'pages/auth/login']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$user = user_find($u);
|
||||
if ($user === false || !password_verify($p, $user['password'])) {
|
||||
$errors['u'][] = 'Invalid username or password.';
|
||||
flash('errors', $errors);
|
||||
redirect('/auth/login');
|
||||
$errors['x'][] = 'Invalid username or password.';
|
||||
$GLOBALS['form-errors'] = $errors;
|
||||
echo render('layouts/basic', ['view' => 'pages/auth/login']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$_SESSION['user'] = $user;
|
||||
change_user_character($user['char_id']);
|
||||
|
||||
if ($_POST['remember'] ?? false) {
|
||||
$token = token();
|
||||
|
@ -155,13 +133,19 @@ function auth_controller_login_post(): void
|
|||
set_cookie('remember_me', $token, $expires);
|
||||
}
|
||||
|
||||
if (char_count($_SESSION['user']['id']) === 0) {
|
||||
redirect('/character/create-first');
|
||||
} elseif (!change_user_character($_SESSION['user']['char_id'])) {
|
||||
router_error(999);
|
||||
}
|
||||
|
||||
redirect('/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs the user out.
|
||||
*/
|
||||
function auth_controller_logout_post(): void
|
||||
function auth_controller_logout_post()
|
||||
{
|
||||
csrf_ensure();
|
||||
session_delete($_SESSION['user']['id']);
|
||||
|
|
|
@ -3,24 +3,22 @@
|
|||
/**
|
||||
* Display a list of characters for the currently logged in user.
|
||||
*/
|
||||
function char_controller_list_get(): void
|
||||
function char_controller_list_get()
|
||||
{
|
||||
auth_only();
|
||||
must_have_character();
|
||||
auth_only(); must_have_character();
|
||||
|
||||
$chars = char_list(user('id'));
|
||||
|
||||
echo render('layouts/basic', ['view' => 'pages/chars/list', 'chars' => $chars, 'activeTab' => nav_tabs['chars']]);
|
||||
$GLOBALS['active_nav_tab'] = 'chars';
|
||||
echo page('chars/list', ['chars' => char_list(user('id'))]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an action from the character list page.
|
||||
*/
|
||||
function char_controller_list_post(): void
|
||||
function char_controller_list_post()
|
||||
{
|
||||
auth_only();
|
||||
must_have_character();
|
||||
csrf_ensure();
|
||||
auth_only(); must_have_character(); csrf_ensure();
|
||||
|
||||
$GLOBALS['active_nav_tab'] = 'chars';
|
||||
|
||||
$char_id = (int) ($_POST['char_id'] ?? 0);
|
||||
$action = $_POST['action'] ?? '';
|
||||
|
@ -28,6 +26,12 @@ function char_controller_list_post(): void
|
|||
// If the character ID is not a number, or the action is not a string, return a 400.
|
||||
if (!is_numeric($char_id) || !is_string($action)) router_error(400);
|
||||
|
||||
// If the character ID is 0, return to the list.
|
||||
if ($char_id === 0) {
|
||||
flash('alert_character_list_1', ['', 'No character selected.']);
|
||||
redirect('/characters');
|
||||
}
|
||||
|
||||
// If the action is not one of the allowed actions, return a 400.
|
||||
if (!in_array($action, ['select', 'delete'])) router_error(400);
|
||||
|
||||
|
@ -35,33 +39,25 @@ function char_controller_list_post(): void
|
|||
if ($action === 'select') {
|
||||
// If the character ID is the current character, do nothing.
|
||||
if ($char_id === $_SESSION['user']['char_id'] || $char_id === 0) {
|
||||
flash('info', 'You are already using that character.');
|
||||
flash('alert_character_list_1', ['info', 'You are already using <b>' . char('name') . '</b>.']);
|
||||
redirect('/characters');
|
||||
}
|
||||
|
||||
// Make sure the character ID is valid.
|
||||
if (char_exists($char_id) === false) throw new Exception('Invalid character ID. (acccp)');
|
||||
|
||||
// Make sure the user owns the character.
|
||||
// Ensure the character ID is valid and belongs to the user.
|
||||
if (!char_belongs_to_user($char_id, $_SESSION['user']['id'])) router_error(999);
|
||||
|
||||
change_user_character($char_id);
|
||||
|
||||
flash('success', 'Switched to character ' . char('name') . '!');
|
||||
flash('alert_character_list_1', ['success', 'Switched to character <b>' . char('name') . '</b>!']);
|
||||
}
|
||||
|
||||
// If the action is to delete a character, move to the confirmation page.
|
||||
if ($action === 'delete') {
|
||||
// Make sure the character ID is valid.
|
||||
if (char_exists($char_id) === false) throw new Exception('Invalid character ID. (accdp)');
|
||||
|
||||
// Make sure the user owns the character.
|
||||
// Ensure the character ID is valid and belongs to the user.
|
||||
if (!char_belongs_to_user($char_id, $_SESSION['user']['id'])) router_error(999);
|
||||
|
||||
$char = char_find($char_id);
|
||||
|
||||
echo render('layouts/basic', ['view' => 'pages/chars/delete', 'char' => $char, 'activeTab' => nav_tabs['chars']]);
|
||||
return;
|
||||
echo page('chars/delete', ['char' => char_find($char_id)]);
|
||||
exit;
|
||||
}
|
||||
|
||||
redirect('/characters');
|
||||
|
@ -70,29 +66,23 @@ function char_controller_list_post(): void
|
|||
/**
|
||||
* Delete a character for the currently logged in user.
|
||||
*/
|
||||
function char_controller_delete_post(): void
|
||||
function char_controller_delete_post()
|
||||
{
|
||||
auth_only();
|
||||
must_have_character();
|
||||
csrf_ensure();
|
||||
auth_only(); must_have_character(); csrf_ensure();
|
||||
|
||||
$char_id = (int) ($_POST['char_id'] ?? 0);
|
||||
$name = $_POST['name'] ?? '';
|
||||
|
||||
// If the character ID is not a number, return a 400.
|
||||
if (!is_numeric($char_id)) router_error(400);
|
||||
|
||||
// Make sure the character ID is valid.
|
||||
if (char_exists($char_id) === false) throw new Exception('Invalid character ID. (acddp)');
|
||||
|
||||
// Make sure the user owns the character.
|
||||
// Ensure the character ID is valid and belongs to the user.
|
||||
if (!char_belongs_to_user($char_id, $_SESSION['user']['id'])) router_error(999);
|
||||
|
||||
$char = char_find($char_id);
|
||||
|
||||
// Confirm the name matches the name of the character.
|
||||
if ($char['name'] !== $_POST['name']) {
|
||||
flash('error', 'Failed to delete character. Name confirmation did not match.');
|
||||
// Confirm the name matches the name of the character. CASE SENSITIVE.
|
||||
if ($char['name'] !== trim($_POST['n'] ?? '')) {
|
||||
flash('alert_character_list_1', ['danger', 'Failed to delete <b>' . $char['name'] . '</b>. Name confirmation did not match.']);
|
||||
redirect('/characters');
|
||||
}
|
||||
|
||||
|
@ -105,37 +95,37 @@ function char_controller_delete_post(): void
|
|||
if (count($chars) > 0) change_user_character($chars[0]['id']);
|
||||
}
|
||||
|
||||
flash('error', 'Character ' . $char['name'] . ' deleted.');
|
||||
flash('alert_character_list_1', ['danger', 'Character <b>' . $char['name'] . '</b> deleted.']);
|
||||
redirect('/characters');
|
||||
}
|
||||
|
||||
/**
|
||||
* Form to create your first character.
|
||||
*/
|
||||
function char_controller_create_first_get(): void
|
||||
function char_controller_create_first_get()
|
||||
{
|
||||
auth_only();
|
||||
|
||||
$GLOBALS['active_nav_tab'] = 'chars';
|
||||
|
||||
// If the user already has a character, redirect them to the main page.
|
||||
if (char_count(user('id')) > 0) redirect('/');
|
||||
|
||||
echo render('layouts/basic', ['view' => 'pages/chars/first', 'activeTab' => nav_tabs['chars']]);
|
||||
echo page('chars/first');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a character for the currently logged in user.
|
||||
*/
|
||||
function char_controller_create_post(): void
|
||||
function char_controller_create_post()
|
||||
{
|
||||
auth_only();
|
||||
csrf_ensure();
|
||||
auth_only(); csrf_ensure();
|
||||
|
||||
$GLOBALS['active_nav_tab'] = 'chars';
|
||||
|
||||
$errors = [];
|
||||
|
||||
$name = $_POST['name'] ?? '';
|
||||
|
||||
// Trim the input.
|
||||
$name = trim($name);
|
||||
$name = trim($_POST['n'] ?? '');
|
||||
|
||||
/*
|
||||
A name is required.
|
||||
|
@ -143,18 +133,27 @@ function char_controller_create_post(): void
|
|||
A name must contain only alphanumeric characters and spaces.
|
||||
*/
|
||||
if (empty($name) || strlen($name) < 3 || strlen($name) > 18 || !ctype_alnum(str_replace(' ', '', $name))) {
|
||||
$errors['name'][] = 'Name is required and must be between 3 and 18 characters long and contain only alphanumeric characters and spaces.';
|
||||
$errors['n'][] = 'Name is required and must be between 3 and 18 characters long and contain only alphanumeric characters and spaces.';
|
||||
}
|
||||
|
||||
/*
|
||||
A character's name must be unique.
|
||||
*/
|
||||
if (char_name_exists($name)) $errors['name'][] = 'Name is already taken.';
|
||||
if (char_name_exists($name)) $errors['n'][] = 'Name is already taken.';
|
||||
|
||||
// If there are errors at this point, send them to the page with errors flashed.
|
||||
if (!empty($errors)) {
|
||||
flash('alert-cl2', ['errors', $errors]);
|
||||
redirect('/characters');
|
||||
$GLOBALS['form-errors-create-character'] = $errors;
|
||||
|
||||
if (isset($_POST['first']) && $_POST['first'] === 'true') {
|
||||
// If this is the first character, return to the first character creation page.
|
||||
echo page('chars/first');
|
||||
exit;
|
||||
} else {
|
||||
// If this is not the first character, return to the character list page.
|
||||
echo page('chars/list', ['chars' => char_list(user('id'))]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Create the character
|
||||
|
@ -168,7 +167,7 @@ function char_controller_create_post(): void
|
|||
// Set the character as the user's selected character
|
||||
change_user_character($char);
|
||||
|
||||
flash('alert_character_list_1', ['success', 'Character ' . $name . ' created!']);
|
||||
flash('alert_character_list_1', ['success', 'Character <b>' . $name . '</b> created!']);
|
||||
redirect('/characters');
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
/**
|
||||
* Open a connection to a database.
|
||||
*/
|
||||
function db_open(string $path): SQLite3
|
||||
function db_open($path): SQLite3
|
||||
{
|
||||
$db = new SQLite3($path);
|
||||
|
||||
|
@ -55,11 +55,10 @@ function db_blueprints(): SQLite3
|
|||
* Take a SQLite3 database connection, a query string, and an array of parameters. Prepare the query and
|
||||
* bind the parameters with proper type casting. Then execute the query and return the result.
|
||||
*/
|
||||
function db_query(SQLite3 $db, string $query, array $params = []): SQLite3Result|false
|
||||
function db_query(SQLite3 $db, $query, array $params = [])
|
||||
{
|
||||
$stmt = $db->prepare($query);
|
||||
if (!empty($params)) foreach ($params as $key => $value) $stmt->bindValue($key, $value, getSQLiteType($value));
|
||||
$GLOBALS['queries']++;
|
||||
db_log($query);
|
||||
return $stmt->execute();
|
||||
}
|
||||
|
@ -67,9 +66,8 @@ function db_query(SQLite3 $db, string $query, array $params = []): SQLite3Result
|
|||
/**
|
||||
* Take a SQLite3 database connection and a query string. Execute the query and return the result.
|
||||
*/
|
||||
function db_exec(SQLite3 $db, string $query): bool
|
||||
function db_exec(SQLite3 $db, $query)
|
||||
{
|
||||
$GLOBALS['queries']++;
|
||||
db_log($query);
|
||||
return $db->exec($query);
|
||||
}
|
||||
|
@ -78,18 +76,22 @@ function db_exec(SQLite3 $db, string $query): bool
|
|||
* Take a SQLite3 database connection, a column name, and a value. Execute a COUNT query to see if the value
|
||||
* exists in the column. Return true if the value exists, false otherwise.
|
||||
*/
|
||||
function db_exists(SQLite3 $db, string $table, string $column, mixed $value): bool
|
||||
function db_exists(SQLite3 $db, $table, $column, $value, $caseInsensitive = true)
|
||||
{
|
||||
if ($caseInsensitive) {
|
||||
$query = "SELECT 1 FROM $table WHERE $column = :v COLLATE NOCASE LIMIT 1";
|
||||
} else {
|
||||
$query = "SELECT 1 FROM $table WHERE $column = :v LIMIT 1";
|
||||
}
|
||||
|
||||
$result = db_query($db, $query, [':v' => $value]);
|
||||
db_log($query);
|
||||
return $result->fetchArray(SQLITE3_NUM) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the appropriate SQLite type casting for the value.
|
||||
*/
|
||||
function getSQLiteType(mixed $value): int
|
||||
function getSQLiteType($value): int
|
||||
{
|
||||
return match (true) {
|
||||
is_int($value) => SQLITE3_INTEGER,
|
||||
|
@ -102,7 +104,8 @@ function getSQLiteType(mixed $value): int
|
|||
/**
|
||||
* Log the given query string to the db debug log.
|
||||
*/
|
||||
function db_log(string $query): void
|
||||
function db_log($query)
|
||||
{
|
||||
$GLOBALS['queries']++;
|
||||
if (env('debug', false)) $GLOBALS['query_log'][] = $query;
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
/**
|
||||
* Load the environment variables from the .env file.
|
||||
*/
|
||||
function env_load(string $filePath): void
|
||||
function env_load($filePath)
|
||||
{
|
||||
if (!file_exists($filePath)) throw new Exception("The .env file does not exist. (el)");
|
||||
|
||||
|
@ -34,7 +34,7 @@ function env_load(string $filePath): void
|
|||
/**
|
||||
* Retrieve an environment variable.
|
||||
*/
|
||||
function env(string $key, mixed $default = null): mixed
|
||||
function env($key, $default = null)
|
||||
{
|
||||
return $_ENV[$key] ?? $_SERVER[$key] ?? (getenv($key) ?: $default);
|
||||
}
|
||||
|
|
|
@ -1,28 +1,9 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Return the path to a view file.
|
||||
*/
|
||||
function template(string $name): string
|
||||
{
|
||||
return __DIR__ . "/../templates/$name.php";
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a view with the given data. Looks for `$view` through `template()`.
|
||||
*/
|
||||
function render(string $pathToBaseView, array $data = []): string|false
|
||||
{
|
||||
ob_start();
|
||||
extract($data);
|
||||
require template($pathToBaseView);
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a pretty dope token.
|
||||
*/
|
||||
function token(int $length = 32): string
|
||||
function token($length = 32)
|
||||
{
|
||||
return bin2hex(random_bytes($length));
|
||||
}
|
||||
|
@ -30,7 +11,7 @@ function token(int $length = 32): string
|
|||
/**
|
||||
* Redirect to a new location.
|
||||
*/
|
||||
function redirect(string $location): void
|
||||
function redirect($location)
|
||||
{
|
||||
header("Location: $location");
|
||||
exit;
|
||||
|
@ -39,17 +20,25 @@ function redirect(string $location): void
|
|||
/**
|
||||
* Flash data to the session, or retrieve an existing flash value. Returns false if the key does not exist.
|
||||
*/
|
||||
function flash(string $key, mixed $value = ''): mixed
|
||||
function flash($key, $value = '')
|
||||
{
|
||||
if ($value === '') return $_SESSION["flash_$key"] ?? false;
|
||||
$_SESSION["flash_$key"] = $value;
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear a specific flash message.
|
||||
*/
|
||||
function unflash($key)
|
||||
{
|
||||
unset($_SESSION["flash_$key"]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all flash messages.
|
||||
*/
|
||||
function clear_flashes(): void
|
||||
function clear_flashes()
|
||||
{
|
||||
foreach ($_SESSION as $key => $_) {
|
||||
if (str_starts_with($key, 'flash_')) unset($_SESSION[$key]);
|
||||
|
@ -59,7 +48,7 @@ function clear_flashes(): void
|
|||
/**
|
||||
* Create a CSRF token.
|
||||
*/
|
||||
function csrf(): string
|
||||
function csrf()
|
||||
{
|
||||
if (empty($_SESSION['csrf'])) $_SESSION['csrf'] = token();
|
||||
return $_SESSION['csrf'];
|
||||
|
@ -68,7 +57,7 @@ function csrf(): string
|
|||
/**
|
||||
* Verify a CSRF token.
|
||||
*/
|
||||
function csrf_verify(string $token): bool
|
||||
function csrf_verify($token)
|
||||
{
|
||||
if (hash_equals($_SESSION['csrf'] ?? '', $token)) {
|
||||
$_SESSION['csrf'] = token();
|
||||
|
@ -81,7 +70,7 @@ function csrf_verify(string $token): bool
|
|||
/**
|
||||
* Create a hidden input field for CSRF tokens.
|
||||
*/
|
||||
function csrf_field(): string
|
||||
function csrf_field()
|
||||
{
|
||||
return '<input type="hidden" name="csrf" value="' . csrf() . '">';
|
||||
}
|
||||
|
@ -89,7 +78,7 @@ function csrf_field(): string
|
|||
/**
|
||||
* Kill the current request with a 418 error, if $_POST['csrf'] is invalid.
|
||||
*/
|
||||
function csrf_ensure(): void
|
||||
function csrf_ensure()
|
||||
{
|
||||
if (!csrf_verify($_POST['csrf'] ?? '')) router_error(418);
|
||||
}
|
||||
|
@ -97,7 +86,7 @@ function csrf_ensure(): void
|
|||
/**
|
||||
* Set a cookie with secure and HTTP-only flags.
|
||||
*/
|
||||
function set_cookie(string $name, string $value, int $expires): void
|
||||
function set_cookie($name, $value, $expires)
|
||||
{
|
||||
setcookie($name, $value, [
|
||||
'expires' => $expires,
|
||||
|
@ -112,7 +101,7 @@ function set_cookie(string $name, string $value, int $expires): void
|
|||
/**
|
||||
* Get the current user's array from SESSION if it exists. Specify a key to get a specific value.
|
||||
*/
|
||||
function user(string $field = ''): mixed
|
||||
function user($field = '')
|
||||
{
|
||||
if (empty($_SESSION['user'])) return false;
|
||||
if ($field === '') return $_SESSION['user'];
|
||||
|
@ -131,7 +120,7 @@ function user_selected_char(): int
|
|||
* If the current user has a selected char and the data is in the session, retrieve either the full array of data
|
||||
* or a specific field. If there is no character data, populate it.
|
||||
*/
|
||||
function char(string $field = ''): mixed
|
||||
function char($field = '')
|
||||
{
|
||||
// If there is no user, return false
|
||||
if (empty($_SESSION['user'])) return false;
|
||||
|
@ -149,19 +138,28 @@ function char(string $field = ''): mixed
|
|||
}
|
||||
|
||||
/**
|
||||
* Shorthand to update the user's selected character.
|
||||
* Shorthand to update the user's selected character. Returns true on success, false on failure. Database
|
||||
* is updated if the character ID is different from the current session.
|
||||
*/
|
||||
function change_user_character(int $char_id): void
|
||||
function change_user_character($char_id)
|
||||
{
|
||||
// If the character does not exist, return false
|
||||
if (($char = char_find($char_id)) === false) return false;
|
||||
$GLOBALS['char'] = $char;
|
||||
|
||||
// If the character ID is different, update the session and database
|
||||
if ($_SESSION['user']['char_id'] !== $char_id) {
|
||||
$_SESSION['user']['char_id'] = $char_id;
|
||||
db_query(db_auth(), "UPDATE users SET char_id = :c WHERE id = :u", [':c' => $char_id, ':u' => user('id')]);
|
||||
$GLOBALS['char'] = char_find($char_id);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a percent between two ints, rounded to the nearest whole number or return 0.
|
||||
*/
|
||||
function percent(int $num, int $denom, int $precision = 4): int
|
||||
function percent($num, $denom, $precision = 4): int
|
||||
{
|
||||
if ($denom === 0) return 0;
|
||||
$p = ($num / $denom) * 100;
|
||||
|
@ -173,7 +171,7 @@ function percent(int $num, int $denom, int $precision = 4): int
|
|||
* the data is up to date with every request without having to query the database every use within, for example, a
|
||||
* template. Will return false if the field does not exist, or the entire wallet array if no field is specified.
|
||||
*/
|
||||
function wallet(string $field = ''): array|int|false
|
||||
function wallet($field = ''): array|int
|
||||
{
|
||||
if (empty($GLOBALS['wallet'])) {
|
||||
$GLOBALS['wallet'] = db_query(
|
||||
|
@ -190,7 +188,7 @@ function wallet(string $field = ''): array|int|false
|
|||
/**
|
||||
* Format an array of strings to a ul element.
|
||||
*/
|
||||
function array_to_ul(array $array): string
|
||||
function array_to_ul(array $array)
|
||||
{
|
||||
$html = '';
|
||||
foreach ($array as $item) $html .= "<li>$item</li>";
|
||||
|
|
|
@ -24,7 +24,7 @@ const currently = [
|
|||
* of overrides to set additional fields. A character's name must be unique, but this function does not check for
|
||||
* that. Returns the created character's ID.
|
||||
*/
|
||||
function char_create(int $user_id, string $name, array $overrides = []): int
|
||||
function char_create($user_id, $name, array $overrides = []): int
|
||||
{
|
||||
// Prep the data and merge in any overrides
|
||||
$data = ['user_id' => $user_id, 'name' => $name];
|
||||
|
@ -49,7 +49,7 @@ function char_create(int $user_id, string $name, array $overrides = []): int
|
|||
* Create a character's location record. A character's location is where they are in the game world. A character can only be
|
||||
* in one location at a time. Can define a starting location for the character. Default state is 'Exploring'.
|
||||
*/
|
||||
function char_location_create(int $char_id, int $x = 0, int $y = 0, int $currently = 0): void
|
||||
function char_location_create($char_id, $x = 0, $y = 0, $currently = 0)
|
||||
{
|
||||
if (db_query(db_live(), "INSERT INTO char_locations (char_id, x, y, currently) VALUES (:p, :x, :y, :c)", [
|
||||
':p' => $char_id,
|
||||
|
@ -65,7 +65,7 @@ function char_location_create(int $char_id, int $x = 0, int $y = 0, int $current
|
|||
* Create the character's gear table. A character's gear is where they store their equipped items.
|
||||
* @TODO: implement initial gear
|
||||
*/
|
||||
function char_gear_create(int $char_id, array $initialGear = []): void
|
||||
function char_gear_create($char_id, array $initialGear = [])
|
||||
{
|
||||
if (db_query(db_live(), "INSERT INTO char_gear (char_id) VALUES (:p)", [':p' => $char_id]) === false) {
|
||||
throw new Exception('Failed to create character gear. (cgc)');
|
||||
|
@ -73,19 +73,18 @@ function char_gear_create(int $char_id, array $initialGear = []): void
|
|||
}
|
||||
|
||||
/**
|
||||
* Get a charcter by their ID. Returns the character's data as an associative array.
|
||||
* Get a charcter by their ID. Returns the character's data as an associative array, or false if not found.
|
||||
*/
|
||||
function char_find(int $char_id): array
|
||||
function char_find($char_id)
|
||||
{
|
||||
$char = db_query(db_live(), "SELECT * FROM characters WHERE id = :id", [':id' => $char_id])->fetchArray(SQLITE3_ASSOC);
|
||||
if ($char === false) throw new Exception('Character not found. (cf)');
|
||||
return $char;
|
||||
return $char === false ? false : $char;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the number of characters associated with an account ID.
|
||||
*/
|
||||
function char_count(int $user_id): int
|
||||
function char_count($user_id): int
|
||||
{
|
||||
$count = db_query(db_live(), "SELECT COUNT(*) FROM characters WHERE user_id = :u", [':u' => $user_id])->fetchArray(SQLITE3_NUM);
|
||||
if ($count === false) throw new Exception('Failed to count characters. (cc)');
|
||||
|
@ -95,7 +94,7 @@ function char_count(int $user_id): int
|
|||
/**
|
||||
* Get a an array of id => [name, level] for all characters associated with an account ID.
|
||||
*/
|
||||
function char_list(int $user_id): array
|
||||
function char_list($user_id): array
|
||||
{
|
||||
$stmt = db_query(db_live(), "SELECT id, name, level FROM characters WHERE user_id = :u", [':u' => $user_id]);
|
||||
if ($stmt === false) throw new Exception('Failed to list characters. (cl)');
|
||||
|
@ -111,7 +110,7 @@ function char_list(int $user_id): array
|
|||
/**
|
||||
* Get a character's location info by their character ID. Returns the location's data as an associative array.
|
||||
*/
|
||||
function char_get_location(int $char_id): array
|
||||
function char_get_location($char_id): array
|
||||
{
|
||||
// Get the location
|
||||
$location = db_query(db_live(), "SELECT * FROM char_locations WHERE char_id = :p", [':p' => $char_id])->fetchArray(SQLITE3_ASSOC);
|
||||
|
@ -122,7 +121,7 @@ function char_get_location(int $char_id): array
|
|||
/**
|
||||
* See if a character name exists.
|
||||
*/
|
||||
function char_name_exists(string $name): bool
|
||||
function char_name_exists($name)
|
||||
{
|
||||
return db_exists(db_live(), 'characters', 'name', $name);
|
||||
}
|
||||
|
@ -130,25 +129,31 @@ function char_name_exists(string $name): bool
|
|||
/**
|
||||
* Checks whether a character exists at a certain ID.
|
||||
*/
|
||||
function char_exists(int $char_id): bool
|
||||
function char_exists($char_id)
|
||||
{
|
||||
return db_exists(db_live(), 'characters', 'id', $char_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* See if the given character belongs to the given user.
|
||||
* See if the given character belongs to the given user. Returns false if the character does not belong to the user,
|
||||
* or if the character does not exist. Returns true if the character belongs to the user. Generally this function
|
||||
* shouldn't return false, as it should be called after the character's existence is confirmed. If it does return false,
|
||||
* it is likely due to user interference.
|
||||
*/
|
||||
function char_belongs_to_user(int $char_id, int $user_id): bool
|
||||
function char_belongs_to_user($char_id, $user_id)
|
||||
{
|
||||
$char = db_query(db_live(), "SELECT user_id FROM characters WHERE id = :p", [':p' => $char_id])->fetchArray(SQLITE3_ASSOC);
|
||||
if ($char === false) throw new Exception('Character not found. (cbtu)');
|
||||
return $char['user_id'] === $user_id;
|
||||
$char = db_query(
|
||||
db_live(),
|
||||
"SELECT 1 FROM characters WHERE id = :p AND user_id = :u",
|
||||
[':p' => $char_id, ':u' => $user_id]
|
||||
)->fetchArray(SQLITE3_ASSOC);
|
||||
return $char !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a character by their ID. This will delete all associated data tables as well.
|
||||
*/
|
||||
function char_delete(int $char_id): void
|
||||
function char_delete($char_id)
|
||||
{
|
||||
// Delete the character
|
||||
if (db_query(db_live(), "DELETE FROM characters WHERE id = :p", [':p' => $char_id]) === false) {
|
||||
|
|
|
@ -61,6 +61,6 @@ const item_qualities = [
|
|||
/**
|
||||
* Create an item
|
||||
*/
|
||||
function create_item(string $name, array $type, array $opts) {
|
||||
function create_item($name, array $type, array $opts) {
|
||||
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
/**
|
||||
* Create a session for a user with a token and expiration date. Returns the token on success, or false on failure.
|
||||
*/
|
||||
function session_create(int $userId, int $expires): string|false
|
||||
function session_create($userId, $expires)
|
||||
{
|
||||
$token = token();
|
||||
$result = db_query(db_auth(), "INSERT INTO sessions (token, user_id, expires) VALUES (:t, :u, :e)", [
|
||||
|
@ -18,7 +18,7 @@ function session_create(int $userId, int $expires): string|false
|
|||
/**
|
||||
* Find a session by token.
|
||||
*/
|
||||
function session_find(string $token): array|false
|
||||
function session_find($token)
|
||||
{
|
||||
$result = db_query(db_auth(), "SELECT * FROM sessions WHERE token = :t", [':t' => $token]);
|
||||
$session = $result->fetchArray(SQLITE3_ASSOC);
|
||||
|
@ -30,7 +30,7 @@ function session_find(string $token): array|false
|
|||
/**
|
||||
* Delete sessions by user id.
|
||||
*/
|
||||
function session_delete(int $userId): SQLite3Result|false
|
||||
function session_delete($userId)
|
||||
{
|
||||
return db_query(db_auth(), "DELETE FROM sessions WHERE user_id = :u", [':u' => $userId]);
|
||||
}
|
||||
|
@ -38,7 +38,7 @@ function session_delete(int $userId): SQLite3Result|false
|
|||
/**
|
||||
* Validate a session by token and expiration date. If expired, the session is deleted and false is returned.
|
||||
*/
|
||||
function session_validate(string $token): bool
|
||||
function session_validate($token)
|
||||
{
|
||||
$session = session_find($token);
|
||||
if (!$session) return false;
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
/**
|
||||
* Create a token for a user. Returns the token on success, or false on failure.
|
||||
*/
|
||||
function token_create(int $userId): string|false
|
||||
function token_create($userId)
|
||||
{
|
||||
$token = token();
|
||||
$result = db_query(db_auth(), "INSERT INTO tokens (token, user_id) VALUES (:t, :u)", [
|
||||
|
@ -17,7 +17,7 @@ function token_create(int $userId): string|false
|
|||
/**
|
||||
* Find a token by token.
|
||||
*/
|
||||
function token_find(string $token): array|false
|
||||
function token_find($token)
|
||||
{
|
||||
$result = db_query(db_auth(), "SELECT * FROM tokens WHERE token = :t", [':t' => $token]);
|
||||
$token = $result->fetchArray(SQLITE3_ASSOC);
|
||||
|
@ -29,7 +29,7 @@ function token_find(string $token): array|false
|
|||
/**
|
||||
* Delete a token by token.
|
||||
*/
|
||||
function token_delete(string $token): SQLite3Result|false
|
||||
function token_delete($token)
|
||||
{
|
||||
return db_query(db_auth(), "DELETE FROM tokens WHERE token = :t", [':t' => $token]);
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ function token_delete(string $token): SQLite3Result|false
|
|||
/**
|
||||
* Validate a token by token and created date. Tokens are invalid if older than 7 days.
|
||||
*/
|
||||
function token_validate(string $token): bool
|
||||
function token_validate($token)
|
||||
{
|
||||
$token = token_find($token);
|
||||
if (!$token) return false;
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
/**
|
||||
* Find a user by username, email, or id.
|
||||
*/
|
||||
function user_find(string|int $user): array|false
|
||||
function user_find($user)
|
||||
{
|
||||
$result = db_query(db_auth(), "SELECT * FROM users WHERE username = :u OR email = :u OR id = :u", [':u' => $user]);
|
||||
$user = $result->fetchArray(SQLITE3_ASSOC);
|
||||
|
@ -18,7 +18,7 @@ function user_find(string|int $user): array|false
|
|||
* also up to the caller to validate password strength. This function will hash the password with the PASSWORD_ARGON2ID
|
||||
* algorithm.
|
||||
*/
|
||||
function user_create(string $username, string $email, string $password, int $auth = 0): SQLite3Result|false
|
||||
function user_create($username, $email, $password, $auth = 0)
|
||||
{
|
||||
return db_query(db_auth(), "INSERT INTO users (username, email, password, auth) VALUES (:u, :e, :p, :a)", [
|
||||
':u' => $username,
|
||||
|
@ -31,7 +31,7 @@ function user_create(string $username, string $email, string $password, int $aut
|
|||
/**
|
||||
* Delete a user by username, email, or id.
|
||||
*/
|
||||
function user_delete(string|int $user): SQLite3Result|false
|
||||
function user_delete($user)
|
||||
{
|
||||
return db_query(db_auth(), "DELETE FROM users WHERE username = :u OR email = :u OR id = :u", [':u' => $user]);
|
||||
}
|
||||
|
@ -40,7 +40,7 @@ function user_delete(string|int $user): SQLite3Result|false
|
|||
* Creates an account wallet. Can optionally specify the starting balances of the wallet. Returns the created wallet's
|
||||
* ID. If a currency is set to -1, the starting_silver or starting_star_gems fields from the env will be used.
|
||||
*/
|
||||
function wallet_create(int $user_id, int $silver = -1, int $starGems = -1): void
|
||||
function wallet_create($user_id, $silver = -1, $starGems = -1)
|
||||
{
|
||||
if (db_query(db_live(), "INSERT INTO wallets (user_id, silver, stargem) VALUES (:u, :s, :sg)", [
|
||||
':u' => $user_id,
|
||||
|
|
37
src/render.php
Normal file
37
src/render.php
Normal file
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Return the path to a view file.
|
||||
*/
|
||||
function template($name)
|
||||
{
|
||||
return __DIR__ . "/../templates/$name.php";
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a view with the given data. Looks for `$view` through `template()`.
|
||||
*/
|
||||
function render($pathToBaseView, $data = [])
|
||||
{
|
||||
ob_start();
|
||||
extract($data);
|
||||
require template($pathToBaseView);
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set/retrieve the current page layout in/from GLOBALS.
|
||||
*/
|
||||
function page_layout($layout = '')
|
||||
{
|
||||
if ($layout !== '') $GLOBALS['page-layout'] = $layout;
|
||||
return $GLOBALS['page-layout'] ?? 'basic';
|
||||
}
|
||||
|
||||
/**
|
||||
* Shorthand to render a page with the current layout.
|
||||
*/
|
||||
function page($view, array $data = [])
|
||||
{
|
||||
return render("layouts/" . page_layout(), ['view' => "pages/$view"] + $data);
|
||||
}
|
|
@ -7,7 +7,7 @@
|
|||
* Example:
|
||||
* `router_add($routes, 'GET', '/posts/:id', function($id) { echo "Viewing post $id"; });`
|
||||
*/
|
||||
function router_add(array &$routes, string $method, string $route, callable $handler): void
|
||||
function router_add(array &$routes, $method, $route, callable $handler)
|
||||
{
|
||||
// Expand the route into segments and make dynamic segments into a common placeholder
|
||||
$segments = array_map(function($segment) {
|
||||
|
@ -33,7 +33,7 @@ function router_add(array &$routes, string $method, string $route, callable $han
|
|||
*
|
||||
* @return array ['code', 'handler', 'params']
|
||||
*/
|
||||
function router_lookup(array $routes, string $method, string $uri): array
|
||||
function router_lookup(array $routes, $method, $uri): array
|
||||
{
|
||||
// node is a reference to our current location in the node tree
|
||||
$node = $routes;
|
||||
|
@ -76,7 +76,7 @@ function router_lookup(array $routes, string $method, string $uri): array
|
|||
/**
|
||||
* Register a GET route
|
||||
*/
|
||||
function router_get(array &$routes, string $route, callable $handler): void
|
||||
function router_get(array &$routes, $route, callable $handler)
|
||||
{
|
||||
router_add($routes, 'GET', $route, $handler);
|
||||
}
|
||||
|
@ -84,7 +84,7 @@ function router_get(array &$routes, string $route, callable $handler): void
|
|||
/**
|
||||
* Register a POST route
|
||||
*/
|
||||
function router_post(array &$routes, string $route, callable $handler): void
|
||||
function router_post(array &$routes, $route, callable $handler)
|
||||
{
|
||||
router_add($routes, 'POST', $route, $handler);
|
||||
}
|
||||
|
@ -92,7 +92,7 @@ function router_post(array &$routes, string $route, callable $handler): void
|
|||
/**
|
||||
* Register a PUT route
|
||||
*/
|
||||
function router_put(array &$routes, string $route, callable $handler): void
|
||||
function router_put(array &$routes, $route, callable $handler)
|
||||
{
|
||||
router_add($routes, 'PUT', $route, $handler);
|
||||
}
|
||||
|
@ -100,7 +100,7 @@ function router_put(array &$routes, string $route, callable $handler): void
|
|||
/**
|
||||
* Register a DELETE route
|
||||
*/
|
||||
function router_delete(array &$routes, string $route, callable $handler): void
|
||||
function router_delete(array &$routes, $route, callable $handler)
|
||||
{
|
||||
router_add($routes, 'DELETE', $route, $handler);
|
||||
}
|
||||
|
@ -108,7 +108,7 @@ function router_delete(array &$routes, string $route, callable $handler): void
|
|||
/**
|
||||
* Register a PATCH route
|
||||
*/
|
||||
function router_patch(array &$routes, string $route, callable $handler): void
|
||||
function router_patch(array &$routes, $route, callable $handler)
|
||||
{
|
||||
router_add($routes, 'PATCH', $route, $handler);
|
||||
}
|
||||
|
@ -116,7 +116,7 @@ function router_patch(array &$routes, string $route, callable $handler): void
|
|||
/**
|
||||
* Handle a router error by setting the response code and echoing an error message
|
||||
*/
|
||||
function router_error(int $code): void
|
||||
function router_error($code)
|
||||
{
|
||||
http_response_code($code);
|
||||
echo match ($code) {
|
||||
|
|
|
@ -3,5 +3,6 @@
|
|||
<label for="char_<?= $id ?>">
|
||||
<?= $char['name'] ?>
|
||||
<span class="badge"><?= $char['level'] ?></span>
|
||||
<span class="selected">Currently Playing</span>
|
||||
</label>
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<div id="nav" class="box">
|
||||
<a href="/" class="<?= $activeTab === 0 ? 'active' : '' ?>">Home</a>
|
||||
<a href="/characters" class="<?= $activeTab === 1 ? 'active' : '' ?>">Characters</a>
|
||||
<a href="/" class="<?= $ant === 0 ? 'active' : '' ?>">Home</a>
|
||||
<a href="/characters" class="<?= $ant === 1 ? 'active' : '' ?>">Characters</a>
|
||||
</div>
|
||||
|
|
|
@ -1,29 +1,26 @@
|
|||
<?php
|
||||
$errors = flash('errors');
|
||||
if ($errors !== false) {
|
||||
foreach ($errors as $error) {
|
||||
foreach ($error as $message) {
|
||||
echo "<p>$message</p>";
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="container-960">
|
||||
<h1 class="my-4">Login</h1>
|
||||
<h1 class="mb-2">Login</h1>
|
||||
|
||||
<p class="mb-4">
|
||||
Welcome back, adventurer! Your journey continues here. Login to your account and pick up where you left off.
|
||||
</p>
|
||||
|
||||
<?= c_form_errors() ?>
|
||||
|
||||
<form action="/auth/login" method="post">
|
||||
<?= csrf_field() ?>
|
||||
|
||||
<input class="form control mb-1" type="text" name="username" placeholder="Username">
|
||||
<input class="form control mb-4" type="password" name="password" placeholder="Password">
|
||||
|
||||
<div class="mb-4">
|
||||
<input type="checkbox" name="remember" id="remember"> <label for="remember">remember me</label>
|
||||
<div class="form group">
|
||||
<?= c_form_field('text', 'u', 'Username or Email') ?>
|
||||
<?= c_form_field('password', 'p', 'Password') ?>
|
||||
</div>
|
||||
|
||||
<button class="ui button primary" type="submit">Login</button>
|
||||
<a href="/auth/register" class="ui button secondary">Register</a>
|
||||
<div class="mt-2 mb-4">
|
||||
<input type="checkbox" name="remember" id="remember"> <label for="remember">Remember Me</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="ui button primary mb-4">Login</button>
|
||||
<a href="/auth/register">New adventurer? Start here!</a>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,25 +1,28 @@
|
|||
<?php
|
||||
$errors = flash('errors');
|
||||
if ($errors !== false) {
|
||||
foreach ($errors as $error) {
|
||||
foreach ($error as $message) {
|
||||
echo "<p>$message</p>";
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
<div class="container-960" autocomplete="off">
|
||||
<h1 class="mb-2">Register</h1>
|
||||
|
||||
<p class="mb-2">
|
||||
So you want to be an adventurer, eh? Fantastic choice! There is glory and treasure to be found at the end
|
||||
of this road; and danger too! If you're strong of will enough, you can become something more.
|
||||
</p>
|
||||
|
||||
<p class="mb-4">
|
||||
Start your journey here. Register an account and begin the never-ending quest for power and riches.
|
||||
</p>
|
||||
|
||||
<?= c_form_errors() ?>
|
||||
|
||||
<div class="container-960">
|
||||
<h1 class="my-4">Register</h1>
|
||||
<form action="/auth/register" method="post">
|
||||
<?= csrf_field() ?>
|
||||
|
||||
<input type="text" name="username" placeholder="Username" class="form control mb-1">
|
||||
<input type="text" name="email" placeholder="Email" class="form control mb-1">
|
||||
<input type="password" name="password" placeholder="Password" class="form control mb-4">
|
||||
<div class="form group">
|
||||
<?= c_form_field('text', 'u', 'Username') ?>
|
||||
<?= c_form_field('text', 'e', 'Email') ?>
|
||||
<?= c_form_field('password', 'p', 'Password', autocomplete: "new-password") ?>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="ui button primary">Register</button>
|
||||
<a href="/auth/login" class="ui button secondary">Login</a>
|
||||
<button type="submit" class="ui button primary mr-4">Register</button>
|
||||
<a href="/auth/login">Already an adventurer? Login!</a>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -15,8 +15,8 @@
|
|||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="char_id" value="<?= $char['id'] ?>">
|
||||
|
||||
<label for="name">Type <b><?= $char['name'] ?></b> below.</label>
|
||||
<input id="name" class="form control mb-2" type="text" name="name" placeholder="Character Name">
|
||||
<label for="n">Type <b><?= $char['name'] ?></b> below.</label>
|
||||
<?= c_form_field('text', 'n', 'Character Name') ?>
|
||||
|
||||
<button class="ui button danger" type="submit">Delete</button>
|
||||
<a class="ui button" href="/characters">Back</a>
|
||||
|
|
|
@ -1,14 +1,3 @@
|
|||
<?php
|
||||
$errors = flash('errors');
|
||||
if ($errors !== false) {
|
||||
foreach ($errors as $error) {
|
||||
foreach ($error as $message) {
|
||||
echo "<p>$message</p>";
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="container-960">
|
||||
<h1 class="my-4">Create Your First Character</h1>
|
||||
|
||||
|
@ -16,13 +5,18 @@
|
|||
|
||||
<p class="mb-4">
|
||||
Before you can begin your adventure, you need to make your first character. Pick a name below. You
|
||||
can create multiple characters later, and there are no classes; feel free to experiment!
|
||||
can create multiple characters later, and there are no classes. Feel free to experiment!
|
||||
</p>
|
||||
|
||||
<?= c_form_errors('create-character') ?>
|
||||
|
||||
<form action="/character/create" method="post">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="first" value="true">
|
||||
|
||||
<input class="form control mb-2" type="text" name="name" placeholder="Character Name">
|
||||
<div class="form group">
|
||||
<?= c_form_field('text', 'n', 'Character Name', formId: 'create-character') ?>
|
||||
</div>
|
||||
|
||||
<button class="ui button primary" type="submit">Create</button>
|
||||
</form>
|
||||
|
|
|
@ -1,20 +1,21 @@
|
|||
<section>
|
||||
<?php
|
||||
if (($f = flash('alert_character_list_1')) !== false) echo c_alert($f[0], $f[1]);
|
||||
?>
|
||||
|
||||
<h1>Characters <span class="badge"><?= count($chars) . '/' . user('char_slots') ?></span></h1>
|
||||
<?php
|
||||
if (count($chars) > 0): ?>
|
||||
if (($f = flash('alert_character_list_1')) !== false) echo c_alert($f[0], $f[1]);
|
||||
|
||||
if (count($chars) > 0):
|
||||
?>
|
||||
<form action="/characters" method="post">
|
||||
<input type="hidden" name="csrf" value="<?= csrf() ?>">
|
||||
|
||||
<div class="my-4 character-select">
|
||||
<div class="my-2 character-select">
|
||||
<?php foreach ($chars as $id => $char) echo c_char_select_box($id, $char); ?>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 buttons">
|
||||
<button type="submit" class="ui button primary" name="action" value="select">Select</button>
|
||||
<button type="submit" class="ui button danger" name="action" value="delete">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<?php else: ?>
|
||||
<!-- Should never see this particular message. If you have, there's a bug. -->
|
||||
|
@ -22,13 +23,8 @@
|
|||
<?php endif; ?>
|
||||
</section>
|
||||
|
||||
<?php if (user('char_slots') > count($chars)): ?>
|
||||
<section>
|
||||
<?php
|
||||
if (($f = flash('alert_character_list_2')) !== false) echo c_alert($f[0], $f[1]);
|
||||
if (($f = flash('errors_create_character')) !== false) echo c_alert('danger', array_to_ul($f));
|
||||
?>
|
||||
|
||||
<?php if (user('char_slots') > count($chars)): ?>
|
||||
<h2>Create a new character</h2>
|
||||
<?php $num_slots_left = user('char_slots') - count($chars); ?>
|
||||
<p>You have <b><?= $num_slots_left ?> <?= $num_slots_left === 1 ? 'slot' : 'slots' ?></b> left.</p>
|
||||
|
@ -36,10 +32,11 @@
|
|||
<input type="hidden" name="csrf" value="<?= csrf() ?>">
|
||||
|
||||
<div class="my-4">
|
||||
<input class="form control" type="text" name="name" id="name" placeholder="Character name" required>
|
||||
<?= c_form_errors('create-character') ?>
|
||||
<?= c_form_field('text', 'n', 'Character Name', formId: 'create-character') ?>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="ui button secondary">Create</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
|
Loading…
Reference in New Issue
Block a user