diff --git a/.env.example b/.env.example index a950c7e..849027d 100644 --- a/.env.example +++ b/.env.example @@ -13,7 +13,7 @@ show_babble = true show_online = true # Environment -debug = true +debug = false # Email smtp_host = smtp.foobar.com diff --git a/public/css/admin.css b/public/css/admin.css index 7202e9b..c871959 100644 --- a/public/css/admin.css +++ b/public/css/admin.css @@ -1,5 +1,11 @@ :root { - --font-size: 12px; + --font-size: 16px; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; } html { @@ -7,12 +13,26 @@ html { font-family: sans-serif; } +body { + padding: 2rem; + color: rgb(108, 108, 108); + background-color: rgb(245, 245, 245); +} + +h1, h2, h3, h4, h5 { + color: rgb(30, 30, 30); +} + div#admin-container { max-width: 1280px; margin: 0 auto; padding: 1rem; } +header { + margin-bottom: 2rem; +} + main { display: flex; gap: 2rem; @@ -32,13 +52,16 @@ table { } a { - color: #663300; + color: #015df7; text-decoration: none; - font-weight: bold; -} -a:hover { - color: #330000; + cursor: pointer; + + &:hover { + color: hsl(218, 99%, 29%); + text-decoration: underline; + } } + .small { font: 10px verdana; } @@ -59,8 +82,7 @@ a:hover { footer { display: flex; justify-content: space-around; - border: solid 1px black; - background-color: #eeeeee; font-size: 0.8rem; padding: 0.5rem; + margin: 2rem 0; } diff --git a/public/index.php b/public/index.php index 6e1cdd0..8da4764 100644 --- a/public/index.php +++ b/public/index.php @@ -17,7 +17,7 @@ $r->post('/move', 'Explore\move'); $r->get('/spell/:id', 'healspells'); $r->get('/character', 'show_character_info'); $r->get('/character/:id', 'show_character_info'); -$r->get('/showmap', 'showmap'); +$r->get('/showmap', 'show_map'); $r->form('/babblebox', 'babblebox'); $r->get('/babblebox/messages', 'babblebox_messages'); @@ -40,19 +40,7 @@ $r->get('/ninja', function() { $l = $r->lookup($_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI']); if (is_int($l)) exit("Error: $l"); -$content = $l['handler'](...$l['params'] ?? []); -if (is_htmx() && $uri[0] !== 'babblebox') { - header('HX-Push-Url: '.$_SERVER['REQUEST_URI']); - $content .= ''.page_title().''; - $content .= Render\debug_db_info(); - if (env('debug', false)) $content .= Render\debug_query_log(); - - if ($GLOBALS['state']['user-state-changed'] ?? false) { - $content .= Render\right_nav(); - $content .= Render\left_nav(); - } -} -echo $content; +echo render_response($uri, $l['handler'](...$l['params'] ?? [])); exit; /** @@ -96,7 +84,10 @@ function show_character_info(int $id = 0): string return render('layouts/minimal', ['content' => $showchar, 'title' => $user->username.' Information']); } -function showmap() +/** + * Show the user their position on the current world map. Only works with a game size of 250 and the default towns 😅 + */ +function show_map() { $pos = sprintf( '
', @@ -111,7 +102,7 @@ function showmap() } /** - * ... + * Handle a POST request to send a new babblebox message. */ function babblebox() { diff --git a/src/actions/admin.php b/src/actions/admin.php index a7eb045..776fc24 100644 --- a/src/actions/admin.php +++ b/src/actions/admin.php @@ -8,16 +8,16 @@ use Router; function register_routes(Router $r): Router { - if (user('authlevel') === 1) { + if (user()->authlevel === 1) { $r->get('/admin', 'Admin\donothing'); $r->form('/admin/main', 'Admin\primary'); $r->get('/admin/items', 'Admin\items'); - $r->form('/admin/items/:id', 'Admin\edititem'); + $r->form('/admin/items/:id', 'Admin\edit_item'); $r->get('/admin/drops', 'Admin\drops'); - $r->form('/admin/drops/:id', 'Admin\editdrop'); + $r->form('/admin/drops/:id', 'Admin\edit_drop'); $r->get('/admin/towns', 'Admin\towns'); $r->form('/admin/towns/:id', 'Admin\edittown'); @@ -39,18 +39,19 @@ function register_routes(Router $r): Router return $r; } -function donothing() +/** + * Home page for the admin panel. + */ +function donothing(): string { $page = <<
Please note that the control panel has been created mostly as a shortcut for certain individual settings. It is meant for use primarily with editing one thing at a time. If you need to completely replace an entire table (say, to replace all stock monsters with your own new ones), it is suggested that you use a more in-depth - database tool such as phpMyAdmin. Also, you may want - to have a copy of the Dragon Knight development kit, available from the - Dragon Knight homepage. + database tool such as SQLite Browser.

Also, you should be aware that certain portions of the DK code are dependent on the formatting of certain database results (for example, the special attributes on item drops). While I have attempted to point these out @@ -58,18 +59,22 @@ function donothing() because mistakes in the database content may result in script errors or your game breaking completely. HTML; - display_admin($page, "Admin Home"); + page_title('Admin'); + return \Render\content($page, 'layouts/admin'); } -function primary() +/** + * Main settings that get written to .env + */ +function primary(): string { - if (isset($_POST["submit"])) { + if (is_post()) { $form = validate($_POST, [ - 'gamename' => ['alphanum-spaces', 'length:1-20'], + 'gamename' => ['alphanum-spaces'], 'gamesize' => ['int', 'min:5'], - 'class1name' => ['alpha-spaces', 'length:1-18'], - 'class2name' => ['alpha-spaces', 'length:1-18'], - 'class3name' => ['alpha-spaces', 'length:1-18'], + 'class1name' => ['alpha-spaces'], + 'class2name' => ['alpha-spaces'], + 'class3name' => ['alpha-spaces'], 'gameopen' => ['bool'], 'verifyemail' => ['bool'], 'shownews' => ['bool'], @@ -81,203 +86,153 @@ function primary() $form = $form['data']; if (($form['gamesize'] % 5) != 0) exit('Map size must be divisible by five.'); - db()->query('UPDATE control SET gamename=?, gamesize=?, class1name=?, class2name=?, class3name=?, gameopen=?, verifyemail=?, gameurl=?, adminemail=?, shownews=?, showonline=?, showbabble=? WHERE id=1;', [ - $form['gamename'], $form['gamesize'], $form['class1name'], $form['class2name'], $form['class3name'], $form['gameopen'], $form['verifyemail'], $form['gameurl'], $form['adminemail'], $form['shownews'], $form['showonline'], $form['showbabble'] - ]); + // @todo + // write changes to .env - display_admin("Settings updated.", "Main Settings"); + $page = 'Main settings updated.'; } else { - $errorlist = ul_from_validate_errors($form['errors']); - display_admin("Errors:
$errorlist

Please go back and try again.", "Main Settings"); + $error_list = ul_from_validate_errors($form['errors']); + $page = <<Errors:
+
{$error_list}

+ Please go back and try again. + HTML; } - } + } else { + $page = render('admin/main_settings'); + } - global $controlrow; - - $page = <<Main Settings
- These options control several major settings for the overall game engine.

-
- - - - - - - - - - - - - -
Game Open:
Close the game if you are upgrading or working on settings and don't want to cause odd errors for end-users. Closing the game will completely halt all activity.
Game Name:
Default is "Dragon Knight". Change this if you want to change to call your game something different.
Game URL:
Please specify the full URL to your game installation ("https://www.dragonknight.com/"). This gets used in the registration email sent to users. If you leave this field blank or incorrect, users may not be able to register correctly.
Admin Email:
Please specify your email address. This gets used when the game has to send an email to users.
Map Size:
Default is 250. This is the size of each map quadrant. Note that monster levels increase every 5 spaces, so you should ensure that you have at least (map size / 5) monster levels total, otherwise there will be parts of the map without any monsters, or some monsters won't ever get used. Ex: with a map size of 250, you should have 50 monster levels total.
Email Verification:
Make users verify their email address for added security.
Show News:
Toggle display of the Latest News box in towns.
Show Who's Online:
Toggle display of the Who's Online box in towns.
Show Babblebox:
Toggle display of the Babble Box in towns.
Class 1 Name:
Class 2 Name:
Class 3 Name:
- -
- HTML; - - if ($controlrow["verifyemail"] == 0) { $controlrow["selectverify0"] = "selected=\"selected\" "; } else { $controlrow["selectverify0"] = ""; } - if ($controlrow["verifyemail"] == 1) { $controlrow["selectverify1"] = "selected=\"selected\" "; } else { $controlrow["selectverify1"] = ""; } - if ($controlrow["shownews"] == 0) { $controlrow["selectnews0"] = "selected=\"selected\" "; } else { $controlrow["selectnews0"] = ""; } - if ($controlrow["shownews"] == 1) { $controlrow["selectnews1"] = "selected=\"selected\" "; } else { $controlrow["selectnews1"] = ""; } - if ($controlrow["showonline"] == 0) { $controlrow["selectonline0"] = "selected=\"selected\" "; } else { $controlrow["selectonline0"] = ""; } - if ($controlrow["showonline"] == 1) { $controlrow["selectonline1"] = "selected=\"selected\" "; } else { $controlrow["selectonline1"] = ""; } - if ($controlrow["showbabble"] == 0) { $controlrow["selectbabble0"] = "selected=\"selected\" "; } else { $controlrow["selectbabble0"] = ""; } - if ($controlrow["showbabble"] == 1) { $controlrow["selectbabble1"] = "selected=\"selected\" "; } else { $controlrow["selectbabble1"] = ""; } - if ($controlrow["gameopen"] == 1) { $controlrow["open1select"] = "selected=\"selected\" "; } else { $controlrow["open1select"] = ""; } - if ($controlrow["gameopen"] == 0) { $controlrow["open0select"] = "selected=\"selected\" "; } else { $controlrow["open0select"] = ""; } - - display_admin(parse($page, $controlrow), "Main Settings"); + page_title('Admin: Main Settings'); + return \Render\content($page, 'layouts/admin'); } -function items() +/** + * Show the full list of items that can be edited. + */ +function items(): string { $items = db()->query('SELECT id, name FROM items ORDER BY id;'); - $page = "Edit Items
Click an item's name to edit it.

\n"; + $page = "

Edit Items

Click an item's name to edit it.

\n"; $hasItems = false; while ($row = $items->fetchArray(SQLITE3_BOTH)) { $hasItems = true; - $page .= "\n"; + $page .= << + + + + HTML; } if (!$hasItems) $page .= "\n"; - display_admin($page . "
".$row["id"]."".$row["name"]."
{$row["id"]}{$row["name"]}
No items found.
", "Edit Items"); + + page_title('Admin: Items'); + return \Render\content($page . '', 'layouts/admin'); } -function edititem($id) +/** + * Shows the form for editing an item via GET, processes edits via POST + */ +function edit_item(int $id): string { - if (isset($_POST["submit"])) { - $errors = []; - $n = trim($_POST['name'] ?? ''); - $bc = (int) trim($_POST['buycost'] ?? 0); - $a = (int) trim($_POST['attribute'] ?? 0); - $s = trim($_POST['special'] ?? 'X'); - - if (empty($n)) $errors[] = "Name is required."; - if (!is_int($bc) || !($bc >= 0)) $errors[] = 'Cost must be a number greater than or equal to 0.'; - if (!is_int($a)) $errors[] = 'Attribute must be a number.'; - - if (count($errors) === 0) { - db()->query('UPDATE items SET name=?, type=?, buycost=?, attribute=?, special=? WHERE id=?;', [ - $n, $_POST['type'] ?? 0, $bc, $a, $s, $id - ]); - display_admin("Item updated.","Edit Items"); - } else { - $errorlist = implode('
', $errors); - display_admin("Errors:
$errorlist

Please go back and try again.", "Edit Items"); - } - } - $item = get_item($id); - $page = <<Edit Items

-
- - - - - - - -
ID:{{id}}
Name:
Type:
Cost: gold
Attribute:
How much the item adds to total attackpower (weapons) or defensepower (armor/shields).
Special:
Should be either a special code or X to disable. Edit this field very carefully because mistakes to formatting or field names can create problems in the game.
- -
- Special Codes:
- Special codes can be added in the item's Special field to give it extra user attributes. Special codes are in the format attribute,value. Attribute can be any database field from the Users table - however, it is suggested that you only use the ones from the list below, otherwise things can get freaky. Value may be any positive or negative whole number. For example, if you want a weapon to give an additional 50 max hit points, the special code would be maxhp,50.

- Suggested user fields for special codes:
- maxhp - max hit points
- maxmp - max magic points
- maxtp - max travel points
- goldbonus - gold bonus, in percent
- expbonus - experience bonus, in percent
- strength - strength (which also adds to attackpower)
- dexterity - dexterity (which also adds to defensepower)
- attackpower - total attack power
- defensepower - total defense power - HTML; + if (is_post()) { + $form = validate($_POST, [ + 'name' => [], + 'type' => ['int', 'in:1,2,3'], + 'buycost' => ['int', 'min:0'], + 'attribute' => ['int', 'min:0'], + 'special' => ['default:X'] + ]); - if ($item["type"] == 1) { $item["type1select"] = "selected=\"selected\" "; } else { $item["type1select"] = ""; } - if ($item["type"] == 2) { $item["type2select"] = "selected=\"selected\" "; } else { $item["type2select"] = ""; } - if ($item["type"] == 3) { $item["type3select"] = "selected=\"selected\" "; } else { $item["type3select"] = ""; } + if ($form['valid']) { + $f = $form['data']; + db()->query('UPDATE items SET name=?, type=?, buycost=?, attribute=?, special=? WHERE id=?;', [ + $f['name'], $f['type'], $f['buycost'], $f['attribute'], $f['special'], $id + ]); + $page = ''.$item['name'].' updated.'; + } else { + $error_list = ul_from_validate_errors($form['errors']); + $page = <<Errors:
+
{$error_list}

+ Please go back and try again. + HTML; + } + } else { + $page = render('admin/edit_item', ['item' => $item]); + } - display_admin(parse($page, $item), "Edit Items"); + page_title('Admin: Editing '.$item['name']); + return \Render\content($page, 'layouts/admin'); } +/** + * Show the full list of drops that can be edited + */ function drops() { - $page = "Edit Drops
Click an item's name to edit it.

\n"; + $page = "

Edit Drops

Click an item's name to edit it.

\n"; $drops = db()->query('SELECT id, name FROM drops ORDER BY id;'); $has_drops = false; while ($row = $drops->fetchArray(SQLITE3_ASSOC)) { $has_drops = true; - $page .= "\n"; + $page .= << + + + + HTML; } if (!$has_drops) { $page .= "\n"; } - display_admin($page . "
".$row["id"]."".$row["name"]."
{$row["id"]}{$row["name"]}
No drops found.
", "Edit Drops"); + page_title('Admin: Drops'); + return \Render\content($page . '', 'layouts/admin'); } -function editdrop($id) +/** + * Show the form to edit drops via GET, process those edits via POST + */ +function edit_drop(int $id): string { - if (isset($_POST["submit"])) { - $errors = []; - - $n = trim($_POST['name'] ?? ''); - $ml = (int) trim($_POST['mlevel'] ?? 0); - $a = trim($_POST['attribute1'] ?? 'X'); - $a2 = trim($_POST['attribute2'] ?? 'X'); - - if (empty($n)) $errors[] = "Name is required."; - if (!is_int($ml) || $ml < 1) $errors[] = "Monster level is required, and must be higher than 0."; - if (empty($a) || $a === 'X') $errors[] = 'First attribute is required.'; - if (empty($a2)) $a2 = 'X'; - - if (count($errors) === 0) { - db()->query('UPDATE drops SET name=?, mlevel=?, attribute1=?, attribute2=? WHERE id=?;', [ - $n, $ml, $a, $a2, $id - ]); - display_admin("Item updated.","Edit Drops"); - } else { - $errorlist = implode('
', $errors); - display_admin("Errors:
$errorlist

Please go back and try again.", "Edit Drops"); - } - } - $drop = get_drop($id); - $page = <<Edit Drops

-
- - - - - - -
ID:{{id}}
Name:
Monster Level:
Minimum monster level that will drop this item.
Attribute 1:
Must be a special code. First attribute cannot be disabled. Edit this field very carefully because mistakes to formatting or field names can create problems in the game.
Attribute 2:
Should be either a special code or X to disable. Edit this field very carefully because mistakes to formatting or field names can create problems in the game.
- -
- Special Codes:
- Special codes are used in the two attribute fields to give the item properties. The first attribute field must contain a special code, but the second one may be left empty ("X") if you wish. Special codes are in the format attribute,value. Attribute can be any database field from the Users table - however, it is suggested that you only use the ones from the list below, otherwise things can get freaky. Value may be any positive or negative whole number. For example, if you want a weapon to give an additional 50 max hit points, the special code would be maxhp,50.

- Suggested user fields for special codes:
- maxhp - max hit points
- maxmp - max magic points
- maxtp - max travel points
- goldbonus - gold bonus, in percent
- expbonus - experience bonus, in percent
- strength - strength (which also adds to attackpower)
- dexterity - dexterity (which also adds to defensepower)
- attackpower - total attack power
- defensepower - total defense power - HTML; + if (is_post()) { + $form = validate($_POST, [ + 'name' => [], + 'mlevel' => ['int', 'min:1'], + 'attribute1' => [], + 'attribute2' => ['default:X'], + ]); - display_admin(parse($page, $drop), "Edit Drops"); + if ($form['valid']) { + db()->query('UPDATE drops SET name=?, mlevel=?, attribute1=?, attribute2=? WHERE id=?;', [ + $form['data']['name'], $form['data']['mlevel'], $form['data']['attribute1'], $form['data']['attribute2'], $id + ]); + $page = ''.$form['data']['name'].' updated.'; + } else { + $error_list = ul_from_validate_errors($form['errors']); + $page = <<Errors:
+
{$error_list}

+ Please go back and try again. + HTML; + } + } else { + $page = render('admin/edit_drop', ['drop' => $drop]); + } + + page_title('Admin: Editing '.$drop['name']); + return \Render\content($page, 'layouts/admin'); } +/** + * Generate the list of towns that can be edited. + */ function towns() { $page = "Edit Towns
Click an town's name to edit it.

\n"; diff --git a/src/lib.php b/src/lib.php index baf8800..4825697 100644 --- a/src/lib.php +++ b/src/lib.php @@ -82,11 +82,7 @@ function display_admin($content, $title) { echo render('layouts/admin', [ "title" => $title, - "content" => $content, - "totaltime" => round(microtime(true) - START, 4), - "numqueries" => db()->count, - "version" => VERSION, - "build" => BUILD + "content" => $content ]); exit; @@ -511,3 +507,29 @@ function page_title(string $new_title = ''): string if ($new_title) return $GLOBALS['state']['new-page-title'] = $new_title; return $GLOBALS['state']['new-page-title'] ?? env('game_name'); } + +/** + * Render the response for the browser based on the request context. The main point is to seperate the handling + * of HTMX responses from normal responses. + */ +function render_response(array $uri, string $content): string +{ + if (!is_htmx() || $uri[0] === 'babblebox') return $content; + + header('HX-Push-Url: '.$_SERVER['REQUEST_URI']); + + $content .= ''.page_title().''; + + $content .= Render\debug_db_info(); + + if (env('debug', false)) { + $content .= Render\debug_query_log(); + } + + if ($GLOBALS['state']['user-state-changed'] ?? false) { + $content .= Render\right_nav(); + $content .= Render\left_nav(); + } + + return $content; +} diff --git a/src/models/user.php b/src/models/user.php index f448a2c..8356f7d 100644 --- a/src/models/user.php +++ b/src/models/user.php @@ -43,6 +43,7 @@ class User extends Model */ public function update_online_time(): void { + if ($this->onlinetime && strtotime($this->onlinetime) > strtotime('-9 minutes')) return; db()->query('UPDATE users SET onlinetime=CURRENT_TIMESTAMP WHERE id=?;', [$this->id]); } diff --git a/src/render.php b/src/render.php index 3814394..ac63bfb 100644 --- a/src/render.php +++ b/src/render.php @@ -9,12 +9,12 @@ namespace Render; /** * Prepare content for final render. If the request is HTMX-based, will return just the content passed to it. Otherwise - * it will render() onto layouts/primary with some additional bits. + * it will render() onto $layout with some additional bits. */ -function content(string $content): string +function content(string $content, string $layout = 'layouts/primary'): string { if (is_htmx()) return $content; - return render('layouts/primary', ['content' => $content]); + return render($layout, ['content' => $content]); } function debug_db_info(): string { diff --git a/templates/admin/edit_drop.php b/templates/admin/edit_drop.php new file mode 100644 index 0000000..6946753 --- /dev/null +++ b/templates/admin/edit_drop.php @@ -0,0 +1,49 @@ +

Editing

+ +
+ + + + + + + + + + + + + + +
ID:
Name:
Monster Level: +
+ Minimum monster level that will drop this item. +
Attribute 1: +
+ Must be a special code. First attribute cannot be disabled. Edit this field very + carefully because mistakes to formatting or field names can create problems in the game. +
Attribute 2: +
+ Should be either a special code or X to + disable. Edit this field very carefully because mistakes to formatting or field names can create + problems in the game. +
+ + + + + +
+ +

Special Codes

+Special codes are used in the two attribute fields to give the item properties. The first attribute field must contain a special code, but the second one may be left empty ("X") if you wish. Special codes are in the format attribute,value. Attribute can be any database field from the Users table - however, it is suggested that you only use the ones from the list below, otherwise things can get freaky. Value may be any positive or negative whole number. For example, if you want a weapon to give an additional 50 max hit points, the special code would be maxhp,50.

+Suggested user fields for special codes:
+maxhp - max hit points
+maxmp - max magic points
+maxtp - max travel points
+goldbonus - gold bonus, in percent
+expbonus - experience bonus, in percent
+strength - strength (which also adds to attackpower)
+dexterity - dexterity (which also adds to defensepower)
+attackpower - total attack power
+defensepower - total defense power diff --git a/templates/admin/edit_item.php b/templates/admin/edit_item.php new file mode 100644 index 0000000..708fee5 --- /dev/null +++ b/templates/admin/edit_item.php @@ -0,0 +1,55 @@ +

Edit

+
+ + + + + + + + + + + + + + + + + + + + + + +
ID:
Name:
Type:
Cost: gold
Attribute: +
+ How much the item adds to total attackpower (weapons) or defensepower (armor/shields). +
Special: +
+ Should be either a special code or X to disable. Edit + this field very carefully because mistakes to formatting or field names can create problems in the game. +
+ + + +
+ +
+ +

Special Codes

+Special codes can be added in the item's Special field to give it extra user attributes. Special codes are in the format attribute,value. Attribute can be any database field from the Users table - however, it is suggested that you only use the ones from the list below, otherwise things can get freaky. Value may be any positive or negative whole number. For example, if you want a weapon to give an additional 50 max hit points, the special code would be maxhp,50.

+Suggested user fields for special codes:
+maxhp - max hit points
+maxmp - max magic points
+maxtp - max travel points
+goldbonus - gold bonus, in percent
+expbonus - experience bonus, in percent
+strength - strength (which also adds to attackpower)
+dexterity - dexterity (which also adds to defensepower)
+attackpower - total attack power
+defensepower - total defense power diff --git a/templates/admin/main_settings.php b/templates/admin/main_settings.php new file mode 100644 index 0000000..6351c23 --- /dev/null +++ b/templates/admin/main_settings.php @@ -0,0 +1,110 @@ +

Main Settings

+

These options control several major settings for the game engine.

+

Note that these particular settings are written to the .env file in the root directory, and not the database.

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Game Open: +
+ Close the game if you are upgrading or working on settings and don't want to + cause odd errors for end-users. Closing the game will completely halt all activity. +
Game Name: +
+ Change this if you want to change to call your game something different. +
Game URL: +
+ Please specify the full URL to your game installation + ("https://www.dragonknight.com/"). This gets used in the registration email sent to users. If + you leave this field blank or incorrect, users may not be able to register correctly. +
Admin Email: +
+ Please specify your email address. This gets used when the game has to send an + email to users. +
Map Size: +
+ + Default is 250. This is the size of each map quadrant. Note that monster + levels increase every 5 spaces, so you should ensure that you have at least (map size / 5) + monster levels total, otherwise there will be parts of the map without any monsters, or some + monsters won't ever get used. Ex: with a map size of 250, you should have 50 monster levels total. + +
Email Verification: +
+ Make users verify their email address for added security. +
Show News: +
+ Toggle display of the Latest News box in towns. +
Show Who's Online: +
+ Toggle display of the Who's Online box in towns. +
Show Babblebox: +
+ Toggle display of the Babble Box in towns. +
Class 1 Name:
Class 2 Name:
Class 3 Name:
+ + + +
diff --git a/templates/layouts/admin.php b/templates/layouts/admin.php index 061d76f..024c19d 100644 --- a/templates/layouts/admin.php +++ b/templates/layouts/admin.php @@ -3,34 +3,36 @@ - <?= $title ?> + <?= page_title() ?> +
+
+

+

Admin

+
-
+
@@ -38,9 +40,11 @@ + +