Replace all display() calls with new Render\content call, which accounts for HTMX

This commit is contained in:
Sky Johnson 2024-12-19 12:53:51 -06:00
parent d45d3f74e5
commit d47e1c5744
14 changed files with 96 additions and 141 deletions

View File

@ -68,7 +68,7 @@ function index(): string
redirect('/fight'); redirect('/fight');
} }
return is_htmx() ? $page : display($page, ''); return Render\content($page);
} }
/** /**

View File

@ -27,7 +27,7 @@ function move() {
$form = validate($_POST, ['direction' => ['in:north,west,east,south']]); $form = validate($_POST, ['direction' => ['in:north,west,east,south']]);
if (!$form['valid']) { if (!$form['valid']) {
$errors = ul_from_validate_errors($form['errors']); $errors = ul_from_validate_errors($form['errors']);
return is_htmx() ? $errors : display($errors, 'Move Error'); return \Render\content($errors);
} }
// Current game state // Current game state

View File

@ -120,22 +120,15 @@ function fight()
// Spell action // Spell action
if (isset($_POST["spell"])) { if (isset($_POST["spell"])) {
$pickedspell = $_POST["userspell"]; $pickedspell = $_POST["userspell"];
if ($pickedspell == 0) { if ($pickedspell == 0) return \Render\content('You must select a spell first. Please go back and try again.');
return display("You must select a spell first. Please go back and try again.", "Error");
die();
}
$newspellrow = get_spell($pickedspell); $newspellrow = get_spell($pickedspell);
$spell = in_array($pickedspell, explode(',', user()->spells)); $spell = in_array($pickedspell, explode(',', user()->spells));
if (!$spell) { if (!$spell) return \Render\content('You have not yet learned this spell. Please go back and try again.');
return display("You have not yet learned this spell. Please go back and try again.", "Error");
die();
}
if (user()->currentmp < $newspellrow["mp"]) { if (user()->currentmp < $newspellrow["mp"]) {
return display("You do not have enough Magic Points to cast this spell. Please go back and try again.", "Error"); return \Render\content('You do not have enough Magic Points to cast this spell. Please go back and try again.');
die();
} }
// Spell type handling (similar to original function) // Spell type handling (similar to original function)
@ -184,7 +177,7 @@ function fight()
// Finalize page and display it // Finalize page and display it
$page = render('fight', ['page' => $page]); $page = render('fight', ['page' => $page]);
return is_htmx() ? $page : display($page, "Fighting"); return \Render\content($page);
} }
function victory() function victory()
@ -258,7 +251,7 @@ function victory()
user()->save(); user()->save();
page_title($title); page_title($title);
return is_htmx() ? $page : display($page, $title); return \Render\content($page);
} }
function drop() function drop()
@ -272,7 +265,7 @@ function drop()
if ($slot == 0) { if ($slot == 0) {
$page = 'Please go back and select an inventory slot to continue.'; $page = 'Please go back and select an inventory slot to continue.';
return is_htmx() ? $page : display($page, ''); return \Render\content($page);
} }
$slotstr = 'slot'.$slot.'id'; $slotstr = 'slot'.$slot.'id';
@ -329,7 +322,7 @@ function drop()
user()->save(); user()->save();
$page = 'The item has been equipped. You can now continue <a href="/" hx-get="/" hx-target="#middle">exploring</a>.'; $page = 'The item has been equipped. You can now continue <a href="/" hx-get="/" hx-target="#middle">exploring</a>.';
return is_htmx() ? $page : display($page, "Item Drop"); return \Render\content($page);
} }
$attributearray = array("maxhp"=>"Max HP", $attributearray = array("maxhp"=>"Max HP",
@ -359,7 +352,7 @@ function drop()
$page .= "<form action=\"/drop\" method=\"post\"><select name=\"slot\"><option value=\"0\">Choose One</option><option value=\"1\">Slot 1: ".user()->slot1name."</option><option value=\"2\">Slot 2: ".user()->slot2name."</option><option value=\"3\">Slot 3: ".user()->slot3name."</option></select> <input type=\"submit\" name=\"submit\" value=\"Submit\" /></form>"; $page .= "<form action=\"/drop\" method=\"post\"><select name=\"slot\"><option value=\"0\">Choose One</option><option value=\"1\">Slot 1: ".user()->slot1name."</option><option value=\"2\">Slot 2: ".user()->slot2name."</option><option value=\"3\">Slot 3: ".user()->slot3name."</option></select> <input type=\"submit\" name=\"submit\" value=\"Submit\" /></form>";
$page .= "You may also choose to just continue <a href=\"/\" hx-get=\"/\" hx-target=\"#middle\">exploring</a> and give up this item."; $page .= "You may also choose to just continue <a href=\"/\" hx-get=\"/\" hx-target=\"#middle\">exploring</a> and give up this item.";
return is_htmx() ? $page : display($page, "Item Drop"); return \Render\content($page);
} }
@ -371,7 +364,7 @@ function dead()
to continue your journey.<br><br> to continue your journey.<br><br>
You may now continue back to <a href="/" hx-get="/" hx-target="#middle">town</a>, and we hope you fair better next time. You may now continue back to <a href="/" hx-get="/" hx-target="#middle">town</a>, and we hope you fair better next time.
HTML; HTML;
return is_htmx() ? $page : display($page, 'You Died'); return \Render\content($page);
} }
function handleMonsterTurn(&$userrow, $monsterrow) function handleMonsterTurn(&$userrow, $monsterrow)

View File

@ -56,7 +56,7 @@ function donothing($start = 0)
$page .= '</table></td></tr></table>'; $page .= '</table></td></tr></table>';
page_title('Forum'); page_title('Forum');
return is_htmx() ? $page : display($page); return \Render\content($page);
} }
function showthread($id, $start) function showthread($id, $start)
@ -72,7 +72,7 @@ function showthread($id, $start)
$page .= "<table width=\"100%\"><tr><td><b>Reply To This Thread:</b><br><form action=\"/forum/reply\" method=\"post\" hx-post=\"/forum/reply\" hx-target=\"#middle\"><input type=\"hidden\" name=\"parent\" value=\"$id\" /><input type=\"hidden\" name=\"title\" value=\"Re: ".$title["title"]."\" /><textarea name=\"content\" rows=\"7\" cols=\"40\"></textarea><br><input type=\"submit\" name=\"submit\" value=\"Submit\" /> <input type=\"reset\" name=\"reset\" value=\"Reset\" /></form></td></tr></table>"; $page .= "<table width=\"100%\"><tr><td><b>Reply To This Thread:</b><br><form action=\"/forum/reply\" method=\"post\" hx-post=\"/forum/reply\" hx-target=\"#middle\"><input type=\"hidden\" name=\"parent\" value=\"$id\" /><input type=\"hidden\" name=\"title\" value=\"Re: ".$title["title"]."\" /><textarea name=\"content\" rows=\"7\" cols=\"40\"></textarea><br><input type=\"submit\" name=\"submit\" value=\"Submit\" /> <input type=\"reset\" name=\"reset\" value=\"Reset\" /></form></td></tr></table>";
page_title('Forum: '.$title['title']); page_title('Forum: '.$title['title']);
return is_htmx() ? $page : display($page); return \Render\content($page);
} }
function reply() function reply()
@ -112,5 +112,5 @@ function newthread()
$page = "<table width=\"100%\"><tr><td><b>Make A New Post:</b><br><br/ ><form action=\"/forum/new\" method=\"post\" hx-post=\"/forum/new\" hx-target=\"#middle\">Title:<br><input type=\"text\" name=\"title\" size=\"50\" maxlength=\"50\" /><br><br>Message:<br><textarea name=\"content\" rows=\"7\" cols=\"40\"></textarea><br><br><input type=\"submit\" name=\"submit\" value=\"Submit\" /> <input type=\"reset\" name=\"reset\" value=\"Reset\" /></form></td></tr></table>"; $page = "<table width=\"100%\"><tr><td><b>Make A New Post:</b><br><br/ ><form action=\"/forum/new\" method=\"post\" hx-post=\"/forum/new\" hx-target=\"#middle\">Title:<br><input type=\"text\" name=\"title\" size=\"50\" maxlength=\"50\" /><br><br>Message:<br><textarea name=\"content\" rows=\"7\" cols=\"40\"></textarea><br><br><input type=\"submit\" name=\"submit\" value=\"Submit\" /> <input type=\"reset\" name=\"reset\" value=\"Reset\" /></form></td></tr></table>";
page_title('Form: New Thread'); page_title('Form: New Thread');
return is_htmx() ? $page : display($page); return \Render\content($page);
} }

View File

@ -30,5 +30,5 @@ function healspells(int $id): string
} }
page_title('Casting '.$spell['name']); page_title('Casting '.$spell['name']);
return is_htmx() ? $page : display($page, ''); return \Render\content($page);
} }

View File

@ -79,9 +79,6 @@ function inn()
$town = get_town_by_xy(user()->longitude, user()->latitude); $town = get_town_by_xy(user()->longitude, user()->latitude);
if ($town === false) { exit('Cheat attempt detected.<br><br>Get a life, loser.'); } if ($town === false) { exit('Cheat attempt detected.<br><br>Get a life, loser.'); }
$htmx = is_htmx();
page_title($town['name'] . ' Inn');
if (user()->gold < $town['innprice']) { if (user()->gold < $town['innprice']) {
$page = <<<HTML $page = <<<HTML
You do not have enough gold to stay at this Inn tonight. <br><br> You do not have enough gold to stay at this Inn tonight. <br><br>
@ -107,7 +104,8 @@ function inn()
HTML; HTML;
} }
return $htmx ? $page : display($page, $town['name'] . ' Inn'); page_title($town['name'] . ' Inn');
return \Render\content($page);
} }
/** /**
@ -120,8 +118,6 @@ function shop()
$town = get_town_by_xy(user()->longitude, user()->latitude); $town = get_town_by_xy(user()->longitude, user()->latitude);
if ($town === false) exit('Cheat attempt detected.<br><br>Get a life, loser.'); if ($town === false) exit('Cheat attempt detected.<br><br>Get a life, loser.');
$htmx = is_htmx();
page_title($town['name'] . ' Shop');
$page = <<<HTML $page = <<<HTML
Buying weapons will increase your Attack Power. Buying armor and shields will increase your Defense Power.<br><br> Buying weapons will increase your Attack Power. Buying armor and shields will increase your Defense Power.<br><br>
@ -161,7 +157,8 @@ function shop()
If you've changed your mind, you may also return back to <a hx-get="/" hx-target="#middle">town</a>. If you've changed your mind, you may also return back to <a hx-get="/" hx-target="#middle">town</a>.
HTML; HTML;
return $htmx ? $page : display($page, $town['name'] . ' Shop'); page_title($town['name'] . ' Shop');
return \Render\content($page);
} }
/** /**
@ -192,7 +189,7 @@ function buy(int $id)
if (!isset($type_mapping[$item["type"]])) { // should never happen if (!isset($type_mapping[$item["type"]])) { // should never happen
$page = 'Error! Invalid item type...<br>'.var_dump($item); $page = 'Error! Invalid item type...<br>'.var_dump($item);
return is_htmx() ? $page : display($page, ''); return \Render\content($page, '');
} }
// Retrieve current equipped item or create a default // Retrieve current equipped item or create a default
@ -276,7 +273,7 @@ function buy(int $id)
} }
page_title('Buying '.$item['name']); page_title('Buying '.$item['name']);
return is_htmx() ? $page : display($page, 'Buying '.$item['name']); return \Render\content($page);
} }
/** /**
@ -322,7 +319,7 @@ function maps()
HTML; HTML;
page_title('Maps'); page_title('Maps');
return is_htmx() ? $page : display($page, ''); return \Render\content($page);
} }
function buy_map(int $id): string function buy_map(int $id): string
@ -357,7 +354,7 @@ function buy_map(int $id): string
} }
page_title('Buying '.$town['name'].' Map'); page_title('Buying '.$town['name'].' Map');
return is_htmx() ? $page : display($page, ''); return \Render\content($page);
} }
/** /**
@ -403,5 +400,5 @@ function travelto(int $id, bool $use_points = true): string
} }
page_title('Travelling to '.$town['name']); page_title('Travelling to '.$town['name']);
return is_htmx() ? $page : display($page, ''); return \Render\content($page);
} }

View File

@ -10,7 +10,7 @@ function register_routes(Router $r): Router
$r->get('/logout', 'Users\logout'); $r->get('/logout', 'Users\logout');
$r->form('/register', 'Users\register'); $r->form('/register', 'Users\register');
$r->form('/lostpassword', 'Users\lostpassword'); $r->form('/lostpassword', 'Users\lostpassword');
$r->form('/changepassword', 'Users\changepassword'); $r->post('/changepassword', 'Users\changepassword');
$r->form('/verify', 'Users\verify'); $r->form('/verify', 'Users\verify');
$r->form('/settings', 'Users\settings'); $r->form('/settings', 'Users\settings');
return $r; return $r;
@ -38,7 +38,8 @@ function login()
redirect('/'); redirect('/');
} }
return display(render('login'), 'Log In', true, false, false); page_title('Login');
return \Render\content(render('login'));
} }
/** /**
@ -99,7 +100,8 @@ function register()
$page = render('register', ['controlrow' => $controlrow]); $page = render('register', ['controlrow' => $controlrow]);
} }
return display($page, 'Register', true, false, false); page_title('Register');
return \Render\content($page);
} }
function verify() function verify()
@ -114,10 +116,10 @@ function verify()
db()->query("UPDATE users SET verify='g2g' WHERE username=?;", [$u]); db()->query("UPDATE users SET verify='g2g' WHERE username=?;", [$u]);
return display("Your account was verified successfully.<br><br>You may now continue to the <a href=\"/login\">Login Page</a> and start playing the game.<br><br>Thanks for playing!","Verify Email",false,false,false); return \Render\content("Your account was verified successfully.<br><br>You may now continue to the <a href=\"/login\">Login Page</a> and start playing the game.<br><br>Thanks for playing!");
} }
return display(render('verify'), "Verify Email", true, false, false); return \Render\content(render('verify'));
} }
function lostpassword() function lostpassword()
@ -133,17 +135,19 @@ function lostpassword()
db()->query('UPDATE users SET password=? WHERE email=?;', [$hashed, $e]); db()->query('UPDATE users SET password=? WHERE email=?;', [$hashed, $e]);
if (sendpassemail($e, $newpass)) { if (sendpassemail($e, $newpass)) {
return display("Your new password was emailed to the address you provided.<br><br>Once you receive it, you may <a href=\"/login\">Log In</a> and continue playing.<br><br>Thank you.","Lost Password",false,false,false); return \Render\content("Your new password was emailed to the address you provided.<br><br>Once you receive it, you may <a href=\"/login\">Log In</a> and continue playing.<br><br>Thank you.");
} else { } else {
return display("There was an error sending your new password.<br><br>Please check with the game administrator for more information.<br><br>We apologize for the inconvience.","Lost Password",false,false,false); return \Render\content("There was an error sending your new password.<br><br>Please check with the game administrator for more information.<br><br>We apologize for the inconvience.");
} }
} }
return display(render('lostpassword'), "Lost Password", true, false, false); return \Render\content(render('lostpassword'));
} }
function changepassword() function changepassword()
{ {
global $auth;
if (isset($_POST["submit"])) { if (isset($_POST["submit"])) {
$u = trim($_POST['username'] ?? ''); $u = trim($_POST['username'] ?? '');
$p = $_POST['password'] ?? ''; $p = $_POST['password'] ?? '';
@ -167,12 +171,10 @@ function changepassword()
$realnewpass = password_hash($np, PASSWORD_ARGON2ID); $realnewpass = password_hash($np, PASSWORD_ARGON2ID);
db()->query('UPDATE users SET password=? WHERE username=?;', [$realnewpass, $u]); db()->query('UPDATE users SET password=? WHERE username=?;', [$realnewpass, $u]);
set_cookie('dkgame', '', -3600); $auth->logout();
return display("Your password was changed successfully.<br><br>You have been logged out of the game to avoid errors.<br><br>Please <a href=\"/login\">log back in</a> to continue playing.","Change Password",false,false,false); return \Render\content("Your password was changed successfully.<br><br>You have been logged out of the game to avoid errors.<br><br>Please <a href=\"/login\">log back in</a> to continue playing.");
} }
return display(render('changepassword'), "Change Password", true, false, false);
} }
function settings() function settings()
@ -188,10 +190,10 @@ function settings()
user()->save(); user()->save();
$alert = '<div class="alert">Settings updated</div>'; $alert = '<div class="alert">Settings updated</div>';
return display($alert . render('settings'), "Account Settings"); return \Render\content($alert . render('settings'));
} }
return display(render('settings'), "Account Settings"); return \Render\content(render('settings'));
} }
function sendpassemail($emailaddress, $password) function sendpassemail($emailaddress, $password)

View File

@ -31,7 +31,8 @@ if (!file_exists('../.installed') && $uri[0] !== 'install') {
$controlrow = get_control_row(); $controlrow = get_control_row();
if (!$controlrow["gameopen"]) { if (!$controlrow["gameopen"]) {
display("The game is currently closed for maintanence. Please check back later.", "Game Closed"); echo Render\content('The game is currently closed for maintanence. Please check back later.');
exit;
} }
$auth = new Auth; $auth = new Auth;

View File

@ -31,22 +31,13 @@ function redirect(string $location): void
} }
/** /**
* Return the path to a view file. * Render a view with the given data. Can be used redundantly within the template.
*/
function template(string $name): string
{
return "../templates/$name.php";
}
/**
* Render a view with the given data. Looks for `$path_to_base_view` through `template()`. Can be used redundantly
* within the template.
*/ */
function render(string $path_to_base_view, array $data = []): string|false function render(string $path_to_base_view, array $data = []): string|false
{ {
ob_start(); ob_start();
extract($data); extract($data);
require template($path_to_base_view); require "../templates/$name.php";
return ob_get_clean(); return ob_get_clean();
} }
@ -102,56 +93,11 @@ function display_admin($content, $title)
} }
/** /**
* Finalize page and output to browser. * Determine what game skin to use. If a user is logged in then it uses their setting, otherwise defaults to 0 (retro).
*/ */
function display($content, $title = '', bool $topnav = true, bool $leftnav = true, bool $rightnav = true): string function game_skin(): int
{ {
global $controlrow; return user() !== false ? user()->game_skin : 0;
$game_skin = user() !== false ? user()->game_skin : 0;
return render('layouts/primary', [
"dkgamename" => $controlrow["gamename"],
"content" => $content,
"game_skin" => $game_skin,
"topnav" => $topnav ? Render\header_links() : ''
]);
}
function checkcookies()
{
$row = false;
if (isset($_COOKIE["dkgame"])) {
// COOKIE FORMAT:
// {ID} {USERNAME} {PASSWORDHASH} {REMEMBERME}
$theuser = explode(" ",$_COOKIE["dkgame"]);
$query = db()->query('SELECT * FROM users WHERE id = ? AND username = ? AND password = ? LIMIT 1;', [$theuser[0], $theuser[1], $theuser[2]]);
if ($query === false) {
set_cookie('dkgame', '', -3600);
die("Invalid cookie data. Please log in again.");
}
$row = $query->fetchArray(SQLITE3_ASSOC);
set_cookie('dkgame', implode(" ", $theuser), (int) $theuser[3] === 1 ? time() + 31536000 : 0);
db()->query('UPDATE users SET onlinetime = CURRENT_TIMESTAMP WHERE id = ?;', [$theuser[0]]);
}
return $row;
}
/**
* Set a cookie with secure and HTTP-only flags.
*/
function set_cookie($name, $value, $expires)
{
setcookie($name, $value, [
'expires' => $expires,
'path' => '/',
'domain' => '', // Defaults to the current domain
'secure' => true, // Ensure the cookie is only sent over HTTPS
'httponly' => true, // Prevent access to cookie via JavaScript
'samesite' => 'Strict' // Enforce SameSite=Strict
]);
} }
/** /**

View File

@ -7,18 +7,20 @@ namespace Render;
to HTMX/AJAX for more fluid gameplay. to HTMX/AJAX for more fluid gameplay.
*/ */
function header_links(): string /**
* 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.
*/
function content(string $content): string
{ {
if (user() !== false) { if (is_htmx()) return $content;
$links = "<a href='/logout'><img src='/img/button_logout.gif' alt='Log Out' title='Log Out'></a>";
} else {
$links = <<<HTML
<a href='/login'><img src='/img/button_login.gif' alt='Log In' title='Log In'></a>
<a href='/register'><img src='/img/button_register.gif' alt='Register' title='Register'></a>
HTML;
}
return $links .= "<a href='/help'><img src='/img/button_help.gif' alt='Help' title='Help'></a>"; global $controlrow;
return render('layouts/primary', [
"dkgamename" => $controlrow["gamename"],
"content" => $content
]);
} }
function debug_db_info(): string { function debug_db_info(): string {

View File

@ -1,10 +0,0 @@
<form action="/changepassword" method="post">
<table width="100%">
<tr><td colspan="2">Use the form below to change your password. All fields are required. New passwords must be 10 alphanumeric characters or less.</td></tr>
<tr><td width="20%">Username:</td><td><input type="text" name="username" size="30" maxlength="30" /></td></tr>
<tr><td>Old Password:</td><td><input type="password" name="password" /></td></tr>
<tr><td>New Password:</td><td><input type="password" name="new_password" /></td></tr>
<tr><td>Verify New Password:</td><td><input type="password" name="new_password2" /><br><br><br></td></tr>
<tr><td colspan="2"><input type="submit" name="submit" value="Submit"> <input type="reset" name="reset" value="Reset"></td></tr>
</table>
</form>

View File

@ -20,11 +20,19 @@
} }
</script> </script>
</head> </head>
<body class="skin-<?= $game_skin ?>"> <body class="skin-<?= game_skin() ?>">
<div id="game-container"> <div id="game-container">
<header> <header>
<a href="/"><img id="logo" src="/img/logo.gif" alt="<?= $dkgamename ?>" title="<?= $dkgamename ?>"></a> <a href="/"><img id="logo" src="/img/logo.gif" alt="<?= $dkgamename ?>" title="<?= $dkgamename ?>"></a>
<nav><?= $topnav ?></nav> <nav>
<?php if (user() !== false): ?>
<a href='/logout'><img src='/img/button_logout.gif' alt='Log Out' title='Log Out'></a>
<?php else: ?>
<a href='/login'><img src='/img/button_login.gif' alt='Log In' title='Log In'></a>
<a href='/register'><img src='/img/button_register.gif' alt='Register' title='Register'></a>
<?php endif; ?>
<a href="/help" hx-boost='/help'><img src='/img/button_help.gif' alt='Help' title='Help'></a>
</nav>
</header> </header>
<main> <main>

View File

@ -50,10 +50,9 @@
<a href="/" hx-get="/" hx-target="#middle">Home</a><br> <a href="/" hx-get="/" hx-target="#middle">Home</a><br>
<a href="/forum" hx-get="/forum" hx-target="#middle">Forum</a><br> <a href="/forum" hx-get="/forum" hx-target="#middle">Forum</a><br>
<a href="/settings">Settings</a><br> <a href="/settings">Settings</a><br>
<a href="/changepassword">Change Password</a><br>
<a href="/logout">Log Out</a><br>
<?php if (user()->authlevel === 1): ?> <?php if (user()->authlevel === 1): ?>
<a href="/admin">Admin</a><br> <a href="/admin">Admin</a><br>
<?php endif; ?> <?php endif; ?>
<a href="/help">Help</a> <a href="/help">Help</a><br>
<a href="/logout">Log Out</a>
</section> </section>

View File

@ -1,12 +1,29 @@
<h1>Account Settings</h1> <h1>Account Settings</h1>
<p>Here you can change some basic settings for your account.</p> <p>Here you can change some basic settings for your account.</p>
<form action="/settings" method="post"> <section>
<label for="game_skin">Game Skin</label> <h2>Game Skin</h2>
<select id="game_skin" name="game_skin"> <form action="/settings" method="post">
<option value="0">Default</option> <select name="game_skin">
<option value="1">Snowstorm</option> <option value="0">Default</option>
</select> <option value="1">Snowstorm</option>
</select>
<button type="submit">Save</button>
</form>
</section>
<section>
<h2>Change Password</h2>
<form action="/changepassword" method="post">
<table width="100%">
<tr><td colspan="2">Use the form below to change your password. All fields are required. New passwords must be 10 alphanumeric characters or less.</td></tr>
<tr><td>Old Password:</td><td><input type="password" name="password"></td></tr>
<tr><td>New Password:</td><td><input type="password" name="new_password"></td></tr>
<tr><td>Verify New Password:</td><td><input type="password" name="confirm_new_password"><br><br><br></td></tr>
<tr><td colspan="2"><input type="submit" name="submit" value="Submit"></td></tr>
</table>
</form>
</section>
<button type="submit">Save</button>
</form>