more work

This commit is contained in:
Sky Johnson 2024-10-05 14:23:32 -05:00
parent 2263134e55
commit 36d8f3405f
29 changed files with 470 additions and 365 deletions

View File

@ -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); }

Binary file not shown.

Binary file not shown.

View File

@ -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
View 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

View File

@ -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;
}
}

View File

@ -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;
}
box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.1);
font-size: 1rem;
/*
.radio-block {
& > input[type="radio"] {
display: none;
&::placeholder {
color: rgba(255, 255, 255, 0.7);
}
& > label {
cursor: pointer;
display: inline-block;
border: none;
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;
&:hover {
background-color: rgba(0, 0, 0, 0.3);
}
&:focus {
background-color: rgba(0, 0, 0, 0.5);
border-color: rgba(0, 0, 0, 0.8);
}
&.error {
background-color: rgba(255, 43, 43, 0.2);
&: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(255, 43, 43, 0.3);
}
& > .badge {
background-color: #444c55;
color: white;
&:focus {
background-color: rgba(255, 43, 43, 0.3);
border-color: rgba(255, 43, 43, 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;
& > .badge {
background-color: #f7f8fa;
color: #111111;
}
}
/* 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;
}
/* 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;
}

View File

@ -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) {

View File

@ -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();

View File

@ -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\">";
}

View File

@ -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']);

View File

@ -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');
}

View File

@ -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)
{
$query = "SELECT 1 FROM $table WHERE $column = :v LIMIT 1";
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;
}

View File

@ -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);
}

View File

@ -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)
{
$_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);
// 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')]);
}
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>";

View File

@ -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) {

View File

@ -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) {
}

View File

@ -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;

View File

@ -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;

View File

@ -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
View 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);
}

View File

@ -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) {

View File

@ -3,5 +3,6 @@
<label for="char_<?= $id ?>">
<?= $char['name'] ?>
<span class="badge"><?= $char['level'] ?></span>
<span class="selected">Currently Playing</span>
</label>
</div>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>
<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 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; ?>