| @@ -0,0 +1,22 @@ | |||
| 1 | + | <h1><?= t('twofa.title') ?></h1> | |
| 2 | + | <p><?= t('twofa.security') ?> <b><?php echo ($status) ? t('common.enabled') : t('common.disabled'); ?></b>.</p> | |
| 3 | + | ||
| 4 | + | <?php if ($status === false): ?> | |
| 5 | + | <p><strong><?= t('twofa.login_to_activate') ?></strong></p> | |
| 6 | + | <?php else: ?> | |
| 7 | + | <form method="post" data-confirm="<?= htmlspecialchars(t('twofa.disable_confirm'), ENT_QUOTES, 'UTF-8') ?>"> | |
| 8 | + | <button type="submit" name="disable_2fa" value="1"><?= t('twofa.disable_btn') ?> <?= t('twofa.title') ?></button> | |
| 9 | + | </form> | |
| 10 | + | <?php endif; ?> | |
| 11 | + | ||
| 12 | + | <img | |
| 13 | + | src="<?php echo TokenAuth6238::getBarCodeUrl($user_data['name'], $_SERVER["HTTP_HOST"], $query['znote_secret'], preg_replace('/\s+/', '', $config['site_title'])); ?>" | |
| 14 | + | alt="<?= t('twofa.qr_alt', ['title' => t('twofa.title')]) ?>" | |
| 15 | + | /> | |
| 16 | + | ||
| 17 | + | <h2><?= t('twofa.howto') ?></h2> | |
| 18 | + | <ol> | |
| 19 | + | <li><?= t('twofa.step_download', ['authy' => '<strong>Authy</strong> (<a target="_BLANK" href="https://play.google.com/store/apps/details?id=com.authy.authy">Android</a>), (<a target="_BLANK" href="https://itunes.apple.com/us/app/authy/id494168017">iPhone</a>)', 'google' => '<strong>' . t('twofa.google') . '</strong> (<a target="_BLANK" href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2">Android</a>), (<a target="_BLANK" href="https://itunes.apple.com/us/app/google-authenticator/id388497605">iPhone</a>)']) ?></li> | |
| 20 | + | <li><?= t('twofa.step_scan') ?></li> | |
| 21 | + | <li><?= t('twofa.step_login', ['logout_link' => '<a href="logout.php">' . t('nav.logout') . '</a>', 'title' => t('twofa.title')]) ?></li> | |
| 22 | + | </ol> |
| @@ -0,0 +1,8 @@ | |||
| 1 | + | <h1><?= t('twofa.incompatible') ?></h1> | |
| 2 | + | <p><?= t('twofa.incompatible2') ?> <?= t('twofa.title') ?>.<br> | |
| 3 | + | TFS 1.2 or higher is required to run the legacy in-game two-factor authentication, grab it | |
| 4 | + | <a href="https://github.com/otland/forgottenserver/releases" target="_BLANK">here</a>. | |
| 5 | + | <?php if ($twofa2Enabled): ?> | |
| 6 | + | <br>You can still use the website's own two-factor authentication below - it does not depend on the game engine. | |
| 7 | + | <?php endif; ?> | |
| 8 | + | </p> |
| @@ -0,0 +1,8 @@ | |||
| 1 | + | <?php | |
| 2 | + | ?> | |
| 3 | + | <div class="znx-acct"> | |
| 4 | + | ||
| 5 | + | <div class="znx-acct-head"><?= t_default('voting.title', 'Vote for Us') ?></div> | |
| 6 | + | <div class="znx-acct-info"><?= $votingMessage ?></div> | |
| 7 | + | ||
| 8 | + | </div><!-- .znx-acct --> |
| @@ -0,0 +1,46 @@ | |||
| 1 | + | <div class="well myaccount_widget widget"> | |
| 2 | + | <div class="header"> | |
| 3 | + | <?= t('widget.admin.title') ?> | |
| 4 | + | </div> | |
| 5 | + | <div class="body"> | |
| 6 | + | <ul class="linkbuttons"> | |
| 7 | + | <li> | |
| 8 | + | <a href="admin/index.php"><i class="fa fa-sliders"></i> <?= t('widget.admin.panel') ?></a> | |
| 9 | + | </li> | |
| 10 | + | <?php | |
| 11 | + | // Everything that used to be a separate admin_*.php link now lives | |
| 12 | + | // inside the panel. Only the counter is worth surfacing out here. | |
| 13 | + | $new = 0; | |
| 14 | + | $cache = new Cache('engine/cache/asideFeedbackCount'); | |
| 15 | + | if ($cache->hasExpired()) { | |
| 16 | + | $cat = 4; // Category ID for feedback section | |
| 17 | + | $threads = db()->fetchAll("SELECT `id`, `player_id` FROM `znote_forum_threads` WHERE `forum_id` = ? AND `closed` = 0;", [$cat]); | |
| 18 | + | if ($threads !== false) { | |
| 19 | + | $staffs = db()->fetchAll("SELECT `id` FROM `players` WHERE `group_id` > 1;"); | |
| 20 | + | ||
| 21 | + | foreach($threads as $thread) { | |
| 22 | + | $response = false; | |
| 23 | + | $posts = db()->fetchAll("SELECT `id`, `player_id` FROM `znote_forum_posts` WHERE `thread_id` = ?;", [$thread['id']]); | |
| 24 | + | if ($posts !== false) { | |
| 25 | + | foreach($posts as $post) { | |
| 26 | + | foreach ($staffs as $staff) { | |
| 27 | + | if ($post['player_id'] == $staff['id']) $response = true; | |
| 28 | + | } | |
| 29 | + | } | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | if (!$response) $new++; | |
| 33 | + | } | |
| 34 | + | } | |
| 35 | + | $cache->setContent($new); | |
| 36 | + | $cache->save(); | |
| 37 | + | } else { | |
| 38 | + | $new = $cache->load(); | |
| 39 | + | } | |
| 40 | + | ?> | |
| 41 | + | <li> | |
| 42 | + | <a href='forum.php?cat=4'><?= t('widget.admin.feedback', ['count' => $new]) ?></a> | |
| 43 | + | </li> | |
| 44 | + | </ul> | |
| 45 | + | </div> | |
| 46 | + | </div> |
| @@ -0,0 +1,66 @@ | |||
| 1 | + | <div class="well search_widget" id="searchContainer"> | |
| 2 | + | <div class="header"> | |
| 3 | + | <?= t('widget.search.title') ?> | |
| 4 | + | </div> | |
| 5 | + | <div class="body"> | |
| 6 | + | <div class="relative"> | |
| 7 | + | <div id="name_suggestion"></div> | |
| 8 | + | </div> | |
| 9 | + | <form class="searchForm" action="characterprofile.php" method="get"> | |
| 10 | + | <label for="src_name"><?= t('widget.search.label') ?> </label><input autocomplete="off" type="text" name="name" id="src_name" class="search" placeholder="<?= t('widget.search.placeholder') ?>"> | |
| 11 | + | </form> | |
| 12 | + | <?php | |
| 13 | + | $cache = new Cache('engine/cache/characterNames'); | |
| 14 | + | if ($cache->hasExpired()) { | |
| 15 | + | $names_sql = db()->fetchAll('SELECT `name` FROM `players` ORDER BY `name` ASC;'); | |
| 16 | + | $names = array(); | |
| 17 | + | if ($names_sql !== false): foreach ($names_sql as $name) { | |
| 18 | + | $names[] = $name['name']; | |
| 19 | + | } endif; | |
| 20 | + | $cache->setContent($names); | |
| 21 | + | $cache->save(); | |
| 22 | + | } else { | |
| 23 | + | $names = $cache->load(); | |
| 24 | + | } | |
| 25 | + | ?> | |
| 26 | + | <script type="text/javascript"> | |
| 27 | + | window.searchNames = <?php echo json_encode($names); ?>; | |
| 28 | + | document.addEventListener('DOMContentLoaded', function () { | |
| 29 | + | var input = document.getElementById('src_name'); | |
| 30 | + | var suggestion = document.getElementById('name_suggestion'); | |
| 31 | + | if (!input || !suggestion || !Array.isArray(window.searchNames) || window.searchNames.length === 0) { | |
| 32 | + | return; | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | input.addEventListener('keyup', function () { | |
| 36 | + | suggestion.innerHTML = ''; | |
| 37 | + | var search = input.value.toLowerCase(); | |
| 38 | + | var results = []; | |
| 39 | + | ||
| 40 | + | if (search.length > 0) { | |
| 41 | + | for (var i = 0; i < window.searchNames.length && results.length < 10; i += 1) { | |
| 42 | + | if (String(window.searchNames[i]).toLowerCase().indexOf(search) > -1) { | |
| 43 | + | results.push(window.searchNames[i]); | |
| 44 | + | } | |
| 45 | + | } | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | if (results.length > 0) { | |
| 49 | + | results.forEach(function (name) { | |
| 50 | + | var row = document.createElement('div'); | |
| 51 | + | var link = document.createElement('a'); | |
| 52 | + | row.className = 'sname'; | |
| 53 | + | link.href = 'characterprofile.php?name=' + encodeURIComponent(name); | |
| 54 | + | link.textContent = name; | |
| 55 | + | row.appendChild(link); | |
| 56 | + | suggestion.appendChild(row); | |
| 57 | + | }); | |
| 58 | + | suggestion.classList.add('show'); | |
| 59 | + | } else { | |
| 60 | + | suggestion.classList.remove('show'); | |
| 61 | + | } | |
| 62 | + | }); | |
| 63 | + | }); | |
| 64 | + | </script> | |
| 65 | + | </div> | |
| 66 | + | </div> |
| @@ -0,0 +1,21 @@ | |||
| 1 | + | <div class="well widget"> | |
| 2 | + | <div class="header"> | |
| 3 | + | <?= t('widget.highscores.title') ?> | |
| 4 | + | </div> | |
| 5 | + | <div class="body"> | |
| 6 | + | <form action="highscores.php" method="get"> | |
| 7 | + | <select name="type"> | |
| 8 | + | <option value="7"><?= t('skill.experience') ?></option> | |
| 9 | + | <option value="5"><?= t('skill.shielding') ?></option> | |
| 10 | + | <option value="3"><?= t('skill.axe') ?></option> | |
| 11 | + | <option value="2"><?= t('skill.sword') ?></option> | |
| 12 | + | <option value="1"><?= t('skill.club') ?></option> | |
| 13 | + | <option value="4"><?= t('skill.distance') ?></option> | |
| 14 | + | <option value="9"><?= t('skill.fist') ?></option> | |
| 15 | + | <option value="6"><?= t('skill.fishing') ?></option> | |
| 16 | + | <option value="8"><?= t('skill.magic') ?></option> | |
| 17 | + | </select> | |
| 18 | + | <input type="submit" value="<?= t('widget.highscores.submit') ?>"> | |
| 19 | + | </form> | |
| 20 | + | </div> | |
| 21 | + | </div> |
| @@ -0,0 +1,16 @@ | |||
| 1 | + | <div class="well widget"> | |
| 2 | + | <div class="header"> | |
| 3 | + | <?= t('widget.houses.title') ?> | |
| 4 | + | </div> | |
| 5 | + | <div class="body"> | |
| 6 | + | <form action="houses.php" method="get"> | |
| 7 | + | <select name="id"> | |
| 8 | + | <?php | |
| 9 | + | foreach ($config['towns'] as $id => $name) | |
| 10 | + | echo '<option value="'. $id .'">'. $name .'</option>'; | |
| 11 | + | ?> | |
| 12 | + | </select> | |
| 13 | + | <input type="submit" value="<?= t('widget.houses.submit') ?>"> | |
| 14 | + | </form> | |
| 15 | + | </div> | |
| 16 | + | </div> |
| @@ -0,0 +1,8 @@ | |||
| 1 | + | <?php | |
| 2 | + | if (function_exists('translate_selector')) { | |
| 3 | + | $markup = translate_selector(); | |
| 4 | + | if ($markup !== '') { | |
| 5 | + | echo $markup . translate_selector_assets(); | |
| 6 | + | translate_selector_rendered(true); | |
| 7 | + | } | |
| 8 | + | } |
| @@ -0,0 +1,31 @@ | |||
| 1 | + | <div class="well loginContainer widget" id="loginContainer"> | |
| 2 | + | <div class="header"> | |
| 3 | + | <?= t('widget.login.title') ?> | |
| 4 | + | </div> | |
| 5 | + | <div class="body"> | |
| 6 | + | <form class="loginForm" action="login.php" method="post"> | |
| 7 | + | <div class="well"> | |
| 8 | + | <label for="login_username"><?= t('widget.login.username') ?></label> <input type="text" name="username" id="login_username"> | |
| 9 | + | </div> | |
| 10 | + | <div class="well"> | |
| 11 | + | <label for="login_password"><?= t('widget.login.password') ?></label> <input type="password" name="password" id="login_password"> | |
| 12 | + | </div> | |
| 13 | + | <?php if ($config['twoFactorAuthenticator']): ?> | |
| 14 | + | <div class="well"> | |
| 15 | + | <label for="login_authcode"><?= t('widget.login.token') ?></label> <input type="password" name="authcode"> | |
| 16 | + | </div> | |
| 17 | + | <?php endif; ?> | |
| 18 | + | <div class="well"> | |
| 19 | + | <input type="submit" value="<?= t('widget.login.submit') ?>" class="submitButton"> | |
| 20 | + | </div> | |
| 21 | + | <?php | |
| 22 | + | /* Form file */ | |
| 23 | + | Token::create(); | |
| 24 | + | ?> | |
| 25 | + | <center> | |
| 26 | + | <h3><a href="register.php"><?= t('widget.login.new_account') ?></a></h3> | |
| 27 | + | <p><?= t('widget.login.lost') ?></p> | |
| 28 | + | </center> | |
| 29 | + | </form> | |
| 30 | + | </div> | |
| 31 | + | </div> |
| @@ -0,0 +1,24 @@ | |||
| 1 | + | <div class="well myaccount_widget widget" id="loginContainer"> | |
| 2 | + | <div class="header"> | |
| 3 | + | <?= t('widget.account.welcome', ['name' => $user_data['name'] ?? '']) ?> | |
| 4 | + | </div> | |
| 5 | + | <div class="body"> | |
| 6 | + | <ul class="linkbuttons"> | |
| 7 | + | <li> | |
| 8 | + | <a href='myaccount.php'><?= t('widget.account.my_account') ?></a> | |
| 9 | + | </li> | |
| 10 | + | <li> | |
| 11 | + | <a href='createcharacter.php'><?= t('widget.account.create_character') ?></a> | |
| 12 | + | </li> | |
| 13 | + | <li> | |
| 14 | + | <a href='changepassword.php'><?= t('widget.account.change_password') ?></a> | |
| 15 | + | </li> | |
| 16 | + | <li> | |
| 17 | + | <a href='settings.php'><?= t('widget.account.settings') ?></a> | |
| 18 | + | </li> | |
| 19 | + | <li> | |
| 20 | + | <a href='logout.php'><?= t('widget.account.logout') ?></a> | |
| 21 | + | </li> | |
| 22 | + | </ul> | |
| 23 | + | </div> | |
| 24 | + | </div> |
| @@ -0,0 +1,63 @@ | |||
| 1 | + | <div class="well widget"> | |
| 2 | + | <div class="header"> | |
| 3 | + | <?= t('widget.powergamers.title') ?> | |
| 4 | + | </div> | |
| 5 | + | <div class="body"> | |
| 6 | + | <table> | |
| 7 | + | <?php | |
| 8 | + | $cache = new Cache('engine/cache/widget_powergamers'); | |
| 9 | + | if ($cache->hasExpired()) { | |
| 10 | + | $players = db()->fetchAll(" | |
| 11 | + | SELECT | |
| 12 | + | `h`.`player_id`, | |
| 13 | + | `p`.`name`, | |
| 14 | + | `p`.`level`, | |
| 15 | + | CAST(`p`.`experience` as signed) - CAST(`f`.`experience` as signed) AS `diff_experience` | |
| 16 | + | FROM ( | |
| 17 | + | SELECT | |
| 18 | + | `i`.`player_id`, | |
| 19 | + | IFNULL(`o`.`id`, `i`.`id`) AS `from_id` | |
| 20 | + | FROM `player_history_skill` AS `i` | |
| 21 | + | LEFT JOIN ( | |
| 22 | + | SELECT | |
| 23 | + | `x`.`player_id`, | |
| 24 | + | MAX(`x`.`id`) AS `id` | |
| 25 | + | FROM `player_history_skill` AS `x` | |
| 26 | + | WHERE | |
| 27 | + | `x`.`lastlogout` < UNIX_TIMESTAMP() - 7 * 24 * 60 * 60 | |
| 28 | + | GROUP BY | |
| 29 | + | `x`.`player_id` | |
| 30 | + | ) AS `o` | |
| 31 | + | ON `i`.`player_id` = `o`.`player_id` | |
| 32 | + | WHERE | |
| 33 | + | `i`.`lastlogout` >= UNIX_TIMESTAMP() - 7 * 24 * 60 * 60 | |
| 34 | + | GROUP BY | |
| 35 | + | `i`.`player_id` | |
| 36 | + | ) AS `h` | |
| 37 | + | INNER JOIN `player_history_skill` AS `f` | |
| 38 | + | ON `h`.`from_id` = `f`.`id` | |
| 39 | + | INNER JOIN `players` AS `p` | |
| 40 | + | ON `h`.`player_id` = `p`.`id` | |
| 41 | + | WHERE CAST(`p`.`experience` as signed) - CAST(`f`.`experience` as signed) > 0 | |
| 42 | + | ORDER BY CAST(`p`.`experience` as signed) - CAST(`f`.`experience` as signed) DESC | |
| 43 | + | LIMIT 5 | |
| 44 | + | "); | |
| 45 | + | ||
| 46 | + | $cache->setContent($players); | |
| 47 | + | $cache->save(); | |
| 48 | + | } else { | |
| 49 | + | $players = $cache->load(); | |
| 50 | + | } | |
| 51 | + | ||
| 52 | + | if ($players) { | |
| 53 | + | foreach($players as $count => $player) { | |
| 54 | + | $nr = $count+1; | |
| 55 | + | $kexp = $player['diff_experience'] / 1000; | |
| 56 | + | $kexp = number_format($kexp, 0, '', ' '); | |
| 57 | + | echo "<tr><td>{$nr}</td><td><a href='characterprofile.php?name={$player['name']}'>{$player['name']}</a> ({$player['level']}) <span style='float: right;font-size:14px;'>{$kexp} K exp</span></td></tr>"; | |
| 58 | + | } | |
| 59 | + | } | |
| 60 | + | ?> | |
| 61 | + | </table> | |
| 62 | + | </div> | |
| 63 | + | </div> |
| @@ -0,0 +1,27 @@ | |||
| 1 | + | <?php | |
| 2 | + | $cache = new Cache('engine/cache/asideServerInfo'); | |
| 3 | + | if ($cache->hasExpired()) { | |
| 4 | + | $asideServerInfo = db()->fetchOne(" | |
| 5 | + | SELECT | |
| 6 | + | (SELECT COUNT(`id`) FROM `accounts`) as `accounts`, | |
| 7 | + | (SELECT COUNT(`id`) FROM `players`) as `players`, | |
| 8 | + | (SELECT COUNT(`player_id`) FROM `players_online`) as `online` | |
| 9 | + | "); | |
| 10 | + | $cache->setContent($asideServerInfo); | |
| 11 | + | $cache->save(); | |
| 12 | + | } else { | |
| 13 | + | $asideServerInfo = $cache->load(); | |
| 14 | + | } | |
| 15 | + | ?> | |
| 16 | + | <div class="well widget"> | |
| 17 | + | <div class="header"> | |
| 18 | + | <?= t('widget.serverinfo.title') ?> | |
| 19 | + | </div> | |
| 20 | + | <div class="body"> | |
| 21 | + | <ul> | |
| 22 | + | <li><a href="onlinelist.php"><?= t('widget.serverinfo.online', ['count' => $asideServerInfo['online'] ?? 0]) ?></a></li> | |
| 23 | + | <li><?= t('widget.serverinfo.accounts', ['count' => $asideServerInfo['accounts'] ?? 0]) ?></li> | |
| 24 | + | <li><?= t('widget.serverinfo.players', ['count' => $asideServerInfo['players'] ?? 0]) ?></li> | |
| 25 | + | </ul> | |
| 26 | + | </div> | |
| 27 | + | </div> |
| @@ -0,0 +1,31 @@ | |||
| 1 | + | <div class="well widget"> | |
| 2 | + | <div class="header"> | |
| 3 | + | <?= t('widget.topplayers.title') ?> | |
| 4 | + | </div> | |
| 5 | + | <div class="body"> | |
| 6 | + | <table> | |
| 7 | + | <?php | |
| 8 | + | $cache = new Cache('engine/cache/topPlayer'); | |
| 9 | + | if ($cache->hasExpired()) { | |
| 10 | + | $players = db()->fetchAll(" | |
| 11 | + | SELECT `name`, `level`, `experience` | |
| 12 | + | FROM `players` WHERE `group_id` < ? | |
| 13 | + | ORDER BY `level` DESC, `experience` DESC LIMIT 5; | |
| 14 | + | ", [$config['highscore']['ignoreGroupId']]); | |
| 15 | + | ||
| 16 | + | $cache->setContent($players); | |
| 17 | + | $cache->save(); | |
| 18 | + | } else { | |
| 19 | + | $players = $cache->load(); | |
| 20 | + | } | |
| 21 | + | ||
| 22 | + | if ($players) { | |
| 23 | + | foreach($players as $count => $player) { | |
| 24 | + | $nr = $count+1; | |
| 25 | + | echo "<tr><td>{$nr}</td><td><a href='characterprofile.php?name={$player['name']}'>{$player['name']}</a> ({$player['level']}).</td></tr>"; | |
| 26 | + | } | |
| 27 | + | } | |
| 28 | + | ?> | |
| 29 | + | </table> | |
| 30 | + | </div> | |
| 31 | + | </div> |
| @@ -0,0 +1,11 @@ | |||
| 1 | + | <div class="well vote_widget" id="loginContainer"> | |
| 2 | + | <div class="header"> | |
| 3 | + | <?= t('widget.vote.title') ?> | |
| 4 | + | </div> | |
| 5 | + | <div class="body"> | |
| 6 | + | <form type="submit" action="voting.php" method="GET"> | |
| 7 | + | <?= t('widget.vote.text') ?> | |
| 8 | + | <input type="submit" value="<?= t('widget.vote.submit') ?>"> | |
| 9 | + | </form> | |
| 10 | + | </div> | |
| 11 | + | </div> |
| @@ -0,0 +1,307 @@ | |||
| 1 | + | # Making a ZnoteX theme | |
| 2 | + | ||
| 3 | + | A theme is one folder in `layouts/`. It holds **HTML, CSS, JS and images only** — | |
| 4 | + | never database queries, never business logic. The root pages keep doing the work; | |
| 5 | + | your theme decides how the result looks. | |
| 6 | + | ||
| 7 | + | Nothing outside your folder needs to change. You never touch `engine/`, | |
| 8 | + | the root `.php` files, or `config.php`. | |
| 9 | + | ||
| 10 | + | --- | |
| 11 | + | ||
| 12 | + | ## Quick start | |
| 13 | + | ||
| 14 | + | 1. Copy `layouts/_example/` and rename it, e.g. `layouts/oldhell/`. | |
| 15 | + | Folder names may only contain `a-z`, `0-9`, `-` and `_`. | |
| 16 | + | 2. Edit `theme.json`. | |
| 17 | + | 3. Open the admin panel → **Layout** → **Activate**. | |
| 18 | + | ||
| 19 | + | That is the whole install. Your theme appears in the panel the moment the folder | |
| 20 | + | exists; there is no registry to edit. | |
| 21 | + | ||
| 22 | + | Declare compatibility independently from the theme's own version: | |
| 23 | + | ||
| 24 | + | ```json | |
| 25 | + | { | |
| 26 | + | "name": "My Theme", | |
| 27 | + | "version": "1.0.0", | |
| 28 | + | "requires": { | |
| 29 | + | "znotex": ">=2.0.0 <3.0.0", | |
| 30 | + | "php": ">=8.1", | |
| 31 | + | "api": "^1.0" | |
| 32 | + | } | |
| 33 | + | } | |
| 34 | + | ``` | |
| 35 | + | ||
| 36 | + | An incompatible theme remains installed and visible in the panel, but ZnoteX | |
| 37 | + | will not activate it. The theme version does not need to match a plugin version. | |
| 38 | + | ||
| 39 | + | --- | |
| 40 | + | ||
| 41 | + | ## What a theme folder can contain | |
| 42 | + | ||
| 43 | + | ``` | |
| 44 | + | layouts/yourtheme/ | |
| 45 | + | theme.json name, author, version, description, update [recommended] | |
| 46 | + | screenshot.png thumbnail in the admin panel [optional] | |
| 47 | + | ||
| 48 | + | shells/ | |
| 49 | + | default.php the page frame [REQUIRED] | |
| 50 | + | wide.php any other frame you want [optional] | |
| 51 | + | ||
| 52 | + | views/ | |
| 53 | + | index.php the middle block of index.php [optional] | |
| 54 | + | highscores.php the middle block of highscores.php [optional] | |
| 55 | + | ||
| 56 | + | pages/ | |
| 57 | + | wiki.php a page YOUR theme adds to the site [optional] | |
| 58 | + | ||
| 59 | + | assets/ | |
| 60 | + | css/style.css your stylesheet | |
| 61 | + | js/theme.js your scripts | |
| 62 | + | img/... your images | |
| 63 | + | ||
| 64 | + | menu.php only if your shell calls theme_menu() [optional] | |
| 65 | + | aside.php only if your shell calls theme_sidebar() [optional] | |
| 66 | + | widgets/ only if your shell calls widget() [optional] | |
| 67 | + | ``` | |
| 68 | + | ||
| 69 | + | **`shells/default.php` is the only required file.** Everything else falls back | |
| 70 | + | to `layouts/default/`. A theme made of one shell and one stylesheet already | |
| 71 | + | redresses the entire site. | |
| 72 | + | ||
| 73 | + | --- | |
| 74 | + | ||
| 75 | + | ## The shell | |
| 76 | + | ||
| 77 | + | The frame every page renders inside. Plain HTML plus a few one-liners: | |
| 78 | + | ||
| 79 | + | ```php | |
| 80 | + | <!DOCTYPE html> | |
| 81 | + | <html lang="en"> | |
| 82 | + | <head> | |
| 83 | + | <meta charset="utf-8"> | |
| 84 | + | <title><?= theme_title() ?></title> | |
| 85 | + | <link rel="stylesheet" href="<?= theme_asset('css/style.css') ?>"> | |
| 86 | + | </head> | |
| 87 | + | <body class="<?= theme_body_class() ?>"> | |
| 88 | + | ||
| 89 | + | <header class="my-header"> | |
| 90 | + | <nav> | |
| 91 | + | <a href="index.php">Home</a> | |
| 92 | + | <a href="highscores.php">Highscores</a> | |
| 93 | + | </nav> | |
| 94 | + | </header> | |
| 95 | + | ||
| 96 | + | <main> | |
| 97 | + | <?php theme_content(); ?> | |
| 98 | + | </main> | |
| 99 | + | ||
| 100 | + | <footer>© <?= theme_title() ?></footer> | |
| 101 | + | </body> | |
| 102 | + | </html> | |
| 103 | + | ``` | |
| 104 | + | ||
| 105 | + | `theme_content()` is the only line you cannot remove — it is where the page goes. | |
| 106 | + | Everything else is yours to move, delete or rewrite. | |
| 107 | + | ||
| 108 | + | ### Functions available in a shell, view or page | |
| 109 | + | ||
| 110 | + | | Call | Does | | |
| 111 | + | | --- | --- | | |
| 112 | + | | `theme_content()` | prints the page body — **required, once, in the shell** | | |
| 113 | + | | `theme_title()` | site title from `config.php`, already escaped | | |
| 114 | + | | `theme_body_class()` | `"theme-yourtheme page_highscores"` | | |
| 115 | + | | `theme_asset('css/style.css')` | URL of a file in your `assets/` | | |
| 116 | + | | `theme_menu()` | includes your `menu.php` | | |
| 117 | + | | `theme_sidebar()` | includes your `aside.php` | | |
| 118 | + | | `widget('login')` | includes one file from your `widgets/` | | |
| 119 | + | | `theme_shell('wide')` | render this page in `shells/wide.php` instead | | |
| 120 | + | | `$config` | everything from `config.php` | | |
| 121 | + | | `user_logged_in()`, `is_admin($user_data)` | session state | | |
| 122 | + | ||
| 123 | + | For stable access to configuration, settings, cache, database and hooks, use | |
| 124 | + | `$api = znote_theme_api()`. The same object exposes `asset()`, | |
| 125 | + | `themeOption()`, `apiVersion()` and `znoteVersion()`. | |
| 126 | + | ||
| 127 | + | Write your menu directly in the shell if you prefer — `theme_menu()` exists only | |
| 128 | + | if you want it. Nothing is imposed. | |
| 129 | + | ||
| 130 | + | --- | |
| 131 | + | ||
| 132 | + | ## Child themes — building on another theme | |
| 133 | + | ||
| 134 | + | Name a parent in `theme.json` and your theme only has to ship what it changes: | |
| 135 | + | ||
| 136 | + | ```json | |
| 137 | + | { | |
| 138 | + | "name": "Exodus Dark", | |
| 139 | + | "parent": "default" | |
| 140 | + | } | |
| 141 | + | ``` | |
| 142 | + | ||
| 143 | + | Files are then looked up **child → parent → default**. A stylesheet and two | |
| 144 | + | views on top of a full parent is a complete, working theme — and a fix in the | |
| 145 | + | parent reaches every child without touching them. | |
| 146 | + | ||
| 147 | + | `layouts/_childexample/` is exactly that: one stylesheet, nothing else. | |
| 148 | + | ||
| 149 | + | Parents can themselves have parents, up to 8 levels. A cycle or a missing | |
| 150 | + | parent is ignored rather than fatal — the chain just falls through to | |
| 151 | + | `default`, so a typo degrades the look instead of taking the site down. | |
| 152 | + | ||
| 153 | + | ### One rule that matters | |
| 154 | + | ||
| 155 | + | Inside a shell, use **`theme_include()`**, never `theme_path()`: | |
| 156 | + | ||
| 157 | + | ```php | |
| 158 | + | <?php theme_include('parts/head.php'); ?> // correct | |
| 159 | + | <?php include theme_path() . '/parts/head.php'; ?> // breaks children | |
| 160 | + | ``` | |
| 161 | + | ||
| 162 | + | `theme_path()` points at the active theme only. A child that does not ship | |
| 163 | + | `parts/head.php` would include nothing and render without its frame. | |
| 164 | + | `theme_include()` walks the chain. Pass variables as a second argument, since | |
| 165 | + | an include from inside a function cannot see the caller's locals: | |
| 166 | + | ||
| 167 | + | ```php | |
| 168 | + | <?php theme_include('parts/box.php', ['title' => $title]); ?> | |
| 169 | + | ``` | |
| 170 | + | ||
| 171 | + | --- | |
| 172 | + | ||
| 173 | + | ## Views — restyling an existing page | |
| 174 | + | ||
| 175 | + | A view is the middle block of one root page. The page's logic has already run, | |
| 176 | + | so every variable it prepared is yours to use. | |
| 177 | + | ||
| 178 | + | `layouts/yourtheme/views/highscores.php`: | |
| 179 | + | ||
| 180 | + | ```php | |
| 181 | + | <div class="my-panel"> | |
| 182 | + | <h1>Ranking for <?= skillName($type) ?></h1> | |
| 183 | + | ||
| 184 | + | <table class="table table-striped"> | |
| 185 | + | <tr class="yellow"><td>#</td><td>Name</td><td>Level</td></tr> | |
| 186 | + | <?php foreach ($players as $player): ?> | |
| 187 | + | <tr> | |
| 188 | + | <td><?= (int)$player['rank'] ?></td> | |
| 189 | + | <td><a href="characterprofile.php?name=<?= urlencode($player['name']) ?>"> | |
| 190 | + | <?= htmlspecialchars($player['name'], ENT_QUOTES, 'UTF-8') ?> | |
| 191 | + | </a></td> | |
| 192 | + | <td><?= (int)$player['level'] ?></td> | |
| 193 | + | </tr> | |
| 194 | + | <?php endforeach; ?> | |
| 195 | + | </table> | |
| 196 | + | </div> | |
| 197 | + | ``` | |
| 198 | + | ||
| 199 | + | **Only write the views you actually want to change.** Any page without a view of | |
| 200 | + | its own uses the default theme's markup, inside your shell, styled by your CSS. | |
| 201 | + | ||
| 202 | + | To find out which variables a page gives you, open the root file — e.g. | |
| 203 | + | `highscores.php` — and read the logic above `view('highscores')`. | |
| 204 | + | ||
| 205 | + | --- | |
| 206 | + | ||
| 207 | + | ## Pages — adding pages of your own | |
| 208 | + | ||
| 209 | + | Drop a file in `pages/` and it is live. No registration. | |
| 210 | + | ||
| 211 | + | `layouts/yourtheme/pages/wiki.php` → **`page.php?p=wiki`** | |
| 212 | + | ||
| 213 | + | ```php | |
| 214 | + | <h1>Wiki</h1> | |
| 215 | + | <p>Anything you want.</p> | |
| 216 | + | ``` | |
| 217 | + | ||
| 218 | + | It renders inside your shell like every other page, and gets the body class | |
| 219 | + | `page_wiki` so you can target it from CSS. | |
| 220 | + | ||
| 221 | + | Pretty URLs, if you want them, in `.htaccess`: | |
| 222 | + | ||
| 223 | + | ```apache | |
| 224 | + | RewriteRule ^([a-z0-9_-]+)\.html$ page.php?p=$1 [L,QSA] | |
| 225 | + | ``` | |
| 226 | + | ||
| 227 | + | --- | |
| 228 | + | ||
| 229 | + | ## Several frames in one theme | |
| 230 | + | ||
| 231 | + | Some pages need a different structure — a landing page with no sidebar, a | |
| 232 | + | full-width page. Add another shell and ask for it from the view or page: | |
| 233 | + | ||
| 234 | + | ```php | |
| 235 | + | <?php theme_shell('wide'); ?> | |
| 236 | + | <div class="hero">...</div> | |
| 237 | + | ``` | |
| 238 | + | ||
| 239 | + | `shells/wide.php` is a complete frame, just like `default.php`. | |
| 240 | + | ||
| 241 | + | --- | |
| 242 | + | ||
| 243 | + | ## The CSS contract | |
| 244 | + | ||
| 245 | + | This is the part people miss. | |
| 246 | + | ||
| 247 | + | The root pages emit some markup themselves, with class names your theme does not | |
| 248 | + | control. **Style these or those pages render unstyled.** The full list: | |
| 249 | + | ||
| 250 | + | | Class | Where | | |
| 251 | + | | --- | --- | | |
| 252 | + | | `table`, `table-striped`, `table-hover`, `tbl-hover` | every listing page | | |
| 253 | + | | `tr.yellow` | table header rows — Znote does not use `<th>` | | |
| 254 | + | | `znoteTable`, `ThreadTable` | forum and helpdesk | | |
| 255 | + | | `btn`, `btn-primary`, `btn-success`, `btn-warning`, `btn-danger`, `btn-info` | every form | | |
| 256 | + | | `form-control` | inputs | | |
| 257 | + | | `special` | highlighted rows | | |
| 258 | + | | `txt`, `zheadline`, `bighr` | text helpers | | |
| 259 | + | | `outfitColumn` | outfit images in listings | | |
| 260 | + | | `span12`, `show`, `wtf`, `nav_link` | odds and ends | | |
| 261 | + | ||
| 262 | + | `layouts/_example/assets/css/style.css` styles all of them and is annotated — | |
| 263 | + | copy that section as your starting point. | |
| 264 | + | ||
| 265 | + | If your shell calls `theme_sidebar()` or `widget()`, you also need `.well`, | |
| 266 | + | `.widget` and `.header`. | |
| 267 | + | ||
| 268 | + | --- | |
| 269 | + | ||
| 270 | + | ## Rules | |
| 271 | + | ||
| 272 | + | - **No logic in a theme.** No `mysql_insert`, no `UPDATE`. Reading data for a | |
| 273 | + | page of your own is fine; that is what `pages/` is for. | |
| 274 | + | - **Escape anything from the database**: `htmlspecialchars($x, ENT_QUOTES, 'UTF-8')`. | |
| 275 | + | - **Never edit `layouts/_example/`.** It is the reference every theme is copied | |
| 276 | + | from. Copy it, do not modify it. | |
| 277 | + | - **Never edit `layouts/default/`** either, unless you mean to change the fallback | |
| 278 | + | for every theme on the site. | |
| 279 | + | - **Reference your files through `theme_asset()`**, not with a hardcoded path. | |
| 280 | + | It keeps working when your folder is renamed, and falls back to the default | |
| 281 | + | theme when a file is missing. | |
| 282 | + | ||
| 283 | + | Shared vendor files that are not part of any theme live in `assets/` | |
| 284 | + | (`assets/fontawesome/`, `assets/js/jquery.js`). The admin panel uses them too, | |
| 285 | + | which is why they are not inside a theme. | |
| 286 | + | ||
| 287 | + | --- | |
| 288 | + | ||
| 289 | + | ## Troubleshooting | |
| 290 | + | ||
| 291 | + | **The site is unstyled.** Your shell is probably not loading your CSS. Check | |
| 292 | + | `theme_asset('css/style.css')` and that the file is at | |
| 293 | + | `layouts/yourtheme/assets/css/style.css`. | |
| 294 | + | ||
| 295 | + | **A page is blank.** Look in the PHP error log for `[ZnoteX theme]`. A view that | |
| 296 | + | fails to resolve is logged there. | |
| 297 | + | ||
| 298 | + | **A page renders but has no frame.** `shells/default.php` is missing or has a | |
| 299 | + | parse error. The admin panel refuses to activate a theme without it, but it can | |
| 300 | + | break after activation. | |
| 301 | + | ||
| 302 | + | **My change does nothing.** Confirm which theme is active: admin panel → | |
| 303 | + | Layout. The active one is marked. The setting lives in the `znote_config` | |
| 304 | + | table, not in `config.php`. | |
| 305 | + | ||
| 306 | + | **The admin panel looks unchanged.** That is intentional. The panel has its own | |
| 307 | + | styling in `admin/assets/` and is identical for every theme. |
| @@ -0,0 +1,19 @@ | |||
| 1 | + | /* --------------------------------------------------------------------------- | |
| 2 | + | Child theme example - the ONLY file this theme ships. | |
| 3 | + | ||
| 4 | + | theme.json names "default" as the parent. Because the chain is searched | |
| 5 | + | child -> parent -> default, this file wins wherever the parent asks for | |
| 6 | + | css/style.css, and everything else - shells, parts, views, pages, images, | |
| 7 | + | scripts - still comes from the parent. | |
| 8 | + | ||
| 9 | + | Point "parent" at any installed theme, adjust the import path below to match | |
| 10 | + | where that theme keeps its stylesheet, and you have a variant of it that | |
| 11 | + | survives the parent being updated underneath you. | |
| 12 | + | ||
| 13 | + | Import the parent's sheet first, then override only what you want. | |
| 14 | + | --------------------------------------------------------------------------- */ | |
| 15 | + | ||
| 16 | + | @import url("../../../default/assets/css/style.css"); | |
| 17 | + | ||
| 18 | + | /* Example override: shift the whole palette. Delete this and start yours. */ | |
| 19 | + | body { filter: hue-rotate(200deg); } |
| @@ -0,0 +1,12 @@ | |||
| 1 | + | { | |
| 2 | + | "name": "Child Example", | |
| 3 | + | "author": "Alex - opengamescommunity.com", | |
| 4 | + | "version": "1.0.0", | |
| 5 | + | "requires": { | |
| 6 | + | "znotex": ">=2.0.0 <3.0.0", | |
| 7 | + | "php": ">=8.1", | |
| 8 | + | "api": "^1.0" | |
| 9 | + | }, | |
| 10 | + | "parent": "default", | |
| 11 | + | "description": "Shows what a child theme is: it ships one stylesheet and inherits everything else from its parent. Change \"parent\" to any installed theme and copy this folder to base a variant on it." | |
| 12 | + | } |
| @@ -0,0 +1,181 @@ | |||
| 1 | + | /* =========================================================================== | |
| 2 | + | Example theme | |
| 3 | + | --------------------------------------------------------------------------- | |
| 4 | + | Deliberately plain. It is a starting point, not a design. | |
| 5 | + | ||
| 6 | + | The bottom half of this file is the CSS CONTRACT: the classes the root | |
| 7 | + | pages emit in their own markup. Style those and every page of the site is | |
| 8 | + | readable, even the ones you never wrote a view for. Skip them and those | |
| 9 | + | pages render unstyled. | |
| 10 | + | =========================================================================== */ | |
| 11 | + | ||
| 12 | + | :root { | |
| 13 | + | --ex-bg: #14171d; | |
| 14 | + | --ex-panel: #1d222b; | |
| 15 | + | --ex-line: #2c333f; | |
| 16 | + | --ex-text: #dfe4ec; | |
| 17 | + | --ex-muted: #93a0b2; | |
| 18 | + | --ex-accent: #d1a233; | |
| 19 | + | --ex-radius: 6px; | |
| 20 | + | } | |
| 21 | + | ||
| 22 | + | * { box-sizing: border-box; } | |
| 23 | + | ||
| 24 | + | body { | |
| 25 | + | margin: 0; | |
| 26 | + | background: var(--ex-bg); | |
| 27 | + | color: var(--ex-text); | |
| 28 | + | font: 15px/1.6 system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif; | |
| 29 | + | } | |
| 30 | + | ||
| 31 | + | a { color: var(--ex-accent); text-decoration: none; } | |
| 32 | + | a:hover { text-decoration: underline; } | |
| 33 | + | ||
| 34 | + | img { max-width: 100%; height: auto; } | |
| 35 | + | ||
| 36 | + | /* --------------------------------------------------------------- Chrome -- | |
| 37 | + | These belong to this theme's shell (shells/default.php). Rename them | |
| 38 | + | freely - nothing outside your theme refers to them. */ | |
| 39 | + | ||
| 40 | + | .ex-header { | |
| 41 | + | display: flex; | |
| 42 | + | flex-wrap: wrap; | |
| 43 | + | align-items: center; | |
| 44 | + | gap: 10px 26px; | |
| 45 | + | padding: 14px 24px; | |
| 46 | + | background: var(--ex-panel); | |
| 47 | + | border-bottom: 2px solid var(--ex-accent); | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | .ex-logo { | |
| 51 | + | font-size: 20px; | |
| 52 | + | font-weight: 700; | |
| 53 | + | color: #fff; | |
| 54 | + | } | |
| 55 | + | .ex-logo:hover { text-decoration: none; } | |
| 56 | + | ||
| 57 | + | .ex-nav { display: flex; flex-wrap: wrap; gap: 18px; } | |
| 58 | + | .ex-nav a { color: var(--ex-muted); font-size: 14px; } | |
| 59 | + | .ex-nav a:hover { color: var(--ex-accent); text-decoration: none; } | |
| 60 | + | ||
| 61 | + | .ex-main { | |
| 62 | + | max-width: 1100px; | |
| 63 | + | margin: 26px auto; | |
| 64 | + | padding: 0 20px; | |
| 65 | + | } | |
| 66 | + | ||
| 67 | + | .ex-footer { | |
| 68 | + | max-width: 1100px; | |
| 69 | + | margin: 40px auto 24px; | |
| 70 | + | padding: 18px 20px 0; | |
| 71 | + | border-top: 1px solid var(--ex-line); | |
| 72 | + | color: var(--ex-muted); | |
| 73 | + | font-size: 13px; | |
| 74 | + | } | |
| 75 | + | ||
| 76 | + | /* ============================================================ CSS CONTRACT | |
| 77 | + | Emitted by the root pages themselves, not by your theme. Every theme has | |
| 78 | + | to style these or those pages look broken. This is the whole list. | |
| 79 | + | ======================================================================== */ | |
| 80 | + | ||
| 81 | + | h1, h2, h3 { line-height: 1.25; margin: 1.4em 0 .6em; } | |
| 82 | + | h1 { font-size: 26px; } | |
| 83 | + | h2 { font-size: 20px; } | |
| 84 | + | ||
| 85 | + | /* Tables - highscores, guilds, deaths, online list, forum, ... */ | |
| 86 | + | table, | |
| 87 | + | .table, | |
| 88 | + | .znoteTable, | |
| 89 | + | .ThreadTable { | |
| 90 | + | width: 100%; | |
| 91 | + | border-collapse: collapse; | |
| 92 | + | margin: 14px 0; | |
| 93 | + | background: var(--ex-panel); | |
| 94 | + | border-radius: var(--ex-radius); | |
| 95 | + | overflow: hidden; | |
| 96 | + | } | |
| 97 | + | ||
| 98 | + | table td, table th { | |
| 99 | + | padding: 9px 12px; | |
| 100 | + | border-bottom: 1px solid var(--ex-line); | |
| 101 | + | text-align: left; | |
| 102 | + | } | |
| 103 | + | ||
| 104 | + | /* Header rows. Znote marks them with .yellow, not with <th>. */ | |
| 105 | + | tr.yellow td, | |
| 106 | + | tr.yellow th, | |
| 107 | + | .yellow { | |
| 108 | + | background: #262d38; | |
| 109 | + | color: #fff; | |
| 110 | + | font-weight: 700; | |
| 111 | + | } | |
| 112 | + | ||
| 113 | + | .table-striped tbody tr:nth-child(odd) td { background: rgba(255, 255, 255, .02); } | |
| 114 | + | .table-hover tbody tr:hover td, | |
| 115 | + | .tbl-hover tr:hover td { background: rgba(209, 162, 51, .08); } | |
| 116 | + | ||
| 117 | + | tr.special td { font-weight: 600; } | |
| 118 | + | ||
| 119 | + | /* Buttons and form controls */ | |
| 120 | + | .btn, | |
| 121 | + | button, | |
| 122 | + | input[type="submit"], | |
| 123 | + | input[type="button"] { | |
| 124 | + | display: inline-block; | |
| 125 | + | padding: 8px 16px; | |
| 126 | + | border: 0; | |
| 127 | + | border-radius: var(--ex-radius); | |
| 128 | + | background: var(--ex-accent); | |
| 129 | + | color: #14171d; | |
| 130 | + | font: inherit; | |
| 131 | + | font-weight: 600; | |
| 132 | + | cursor: pointer; | |
| 133 | + | } | |
| 134 | + | .btn:hover { filter: brightness(1.1); text-decoration: none; } | |
| 135 | + | ||
| 136 | + | .btn-primary { background: #3c8dbc; color: #fff; } | |
| 137 | + | .btn-success { background: #2ea36c; color: #fff; } | |
| 138 | + | .btn-warning { background: #e6a532; color: #14171d; } | |
| 139 | + | .btn-danger { background: #e05563; color: #fff; } | |
| 140 | + | .btn-info { background: #17a2b8; color: #fff; } | |
| 141 | + | ||
| 142 | + | input[type="text"], | |
| 143 | + | input[type="password"], | |
| 144 | + | input[type="number"], | |
| 145 | + | input[type="email"], | |
| 146 | + | select, | |
| 147 | + | textarea, | |
| 148 | + | .form-control { | |
| 149 | + | padding: 8px 11px; | |
| 150 | + | border: 1px solid var(--ex-line); | |
| 151 | + | border-radius: var(--ex-radius); | |
| 152 | + | background: #12161c; | |
| 153 | + | color: var(--ex-text); | |
| 154 | + | font: inherit; | |
| 155 | + | max-width: 100%; | |
| 156 | + | } | |
| 157 | + | ||
| 158 | + | textarea { min-height: 120px; } | |
| 159 | + | .forumReply { width: 100%; } | |
| 160 | + | ||
| 161 | + | /* Odds and ends the pages use */ | |
| 162 | + | .txt { color: var(--ex-muted); } | |
| 163 | + | .zheadline { font-size: 22px; font-weight: 700; margin: 1.2em 0 .5em; } | |
| 164 | + | .bighr { border: 0; border-top: 1px solid var(--ex-line); margin: 24px 0; } | |
| 165 | + | .outfitColumn { width: 64px; text-align: center; } | |
| 166 | + | .span12 { width: 100%; } | |
| 167 | + | .show { display: block; } | |
| 168 | + | .wtf { overflow-x: auto; } | |
| 169 | + | .nav_link { display: inline-block; padding: 4px 8px; } | |
| 170 | + | ||
| 171 | + | /* The sidebar widgets, if your shell calls theme_sidebar() or widget(). | |
| 172 | + | This theme's shell does not, so these are here only for reference. */ | |
| 173 | + | .well { background: var(--ex-panel); border-radius: var(--ex-radius); padding: 14px; margin-bottom: 16px; } | |
| 174 | + | .widget { margin-bottom: 16px; } | |
| 175 | + | .header { font-weight: 700; margin-bottom: 8px; } | |
| 176 | + | ||
| 177 | + | @media (max-width: 720px) { | |
| 178 | + | .ex-header { flex-direction: column; align-items: flex-start; } | |
| 179 | + | .ex-main { padding: 0 14px; } | |
| 180 | + | table { font-size: 14px; } | |
| 181 | + | } |
| @@ -0,0 +1,66 @@ | |||
| 1 | + | <?php | |
| 2 | + | /** | |
| 3 | + | * Title: Example page | |
| 4 | + | * | |
| 5 | + | * A page this theme adds to the site. It does not exist in ZnoteX - the theme | |
| 6 | + | * invents it. Nothing to register anywhere: dropping this file in pages/ makes | |
| 7 | + | * it live at page.php?p=example, wrapped in this theme's shell. | |
| 8 | + | * | |
| 9 | + | * Delete it, rename it, add ten more. The filename is the URL. | |
| 10 | + | */ | |
| 11 | + | ?> | |
| 12 | + | <h1>An extra page</h1> | |
| 13 | + | ||
| 14 | + | <p> | |
| 15 | + | This file is <code>layouts/_example/pages/example.php</code> and it is | |
| 16 | + | reachable at <code>page.php?p=example</code>. | |
| 17 | + | </p> | |
| 18 | + | ||
| 19 | + | <p> | |
| 20 | + | Write plain HTML. You have the whole engine available if you want it: | |
| 21 | + | <code>$config</code>, <code>user_logged_in()</code>, | |
| 22 | + | <code>db()->fetchAll($sql, $params)</code>, and every function in | |
| 23 | + | <code>engine/function/</code>. | |
| 24 | + | </p> | |
| 25 | + | ||
| 26 | + | <h2>Example: pull something out of the database</h2> | |
| 27 | + | ||
| 28 | + | <?php | |
| 29 | + | $players = db()->fetchAll(" | |
| 30 | + | SELECT `name`, `level`, `vocation` | |
| 31 | + | FROM `players` | |
| 32 | + | ORDER BY `level` DESC | |
| 33 | + | LIMIT 5; | |
| 34 | + | "); | |
| 35 | + | ?> | |
| 36 | + | ||
| 37 | + | <?php if (is_array($players) && $players): ?> | |
| 38 | + | <table class="table table-striped"> | |
| 39 | + | <tr class="yellow"> | |
| 40 | + | <td>Name</td> | |
| 41 | + | <td>Vocation</td> | |
| 42 | + | <td>Level</td> | |
| 43 | + | </tr> | |
| 44 | + | <?php foreach ($players as $player): ?> | |
| 45 | + | <tr> | |
| 46 | + | <td> | |
| 47 | + | <a href="characterprofile.php?name=<?= urlencode((string)$player['name']) ?>"> | |
| 48 | + | <?= htmlspecialchars((string)$player['name'], ENT_QUOTES, 'UTF-8') ?> | |
| 49 | + | </a> | |
| 50 | + | </td> | |
| 51 | + | <td><?= htmlspecialchars(vocation_id_to_name((int)$player['vocation']), ENT_QUOTES, 'UTF-8') ?></td> | |
| 52 | + | <td><?= (int)$player['level'] ?></td> | |
| 53 | + | </tr> | |
| 54 | + | <?php endforeach; ?> | |
| 55 | + | </table> | |
| 56 | + | <?php else: ?> | |
| 57 | + | <p>No characters yet.</p> | |
| 58 | + | <?php endif; ?> | |
| 59 | + | ||
| 60 | + | <h2>Want a different frame for this page?</h2> | |
| 61 | + | ||
| 62 | + | <p> | |
| 63 | + | Add <code>shells/wide.php</code> to your theme and put | |
| 64 | + | <code><?php theme_shell('wide'); ?></code> at the top of this file. | |
| 65 | + | The page then renders inside that frame instead of the default one. | |
| 66 | + | </p> |
| @@ -0,0 +1,66 @@ | |||
| 1 | + | <?php | |
| 2 | + | /** | |
| 3 | + | * The shell: the frame every page of this theme is rendered inside. | |
| 4 | + | * | |
| 5 | + | * This is plain HTML. The only line you cannot remove is theme_content(), | |
| 6 | + | * which is where the page itself goes. Everything else is yours - move it, | |
| 7 | + | * delete it, rewrite it. | |
| 8 | + | * | |
| 9 | + | * Available to you here: | |
| 10 | + | * | |
| 11 | + | * theme_title() site title, already escaped | |
| 12 | + | * theme_body_class() "theme-yourtheme page_highscores" | |
| 13 | + | * theme_asset('css/x.css') URL of a file in this theme's assets/ folder | |
| 14 | + | * theme_content() the page body [required] | |
| 15 | + | * theme_menu() includes this theme's menu.php [optional] | |
| 16 | + | * theme_sidebar() includes this theme's aside.php [optional] | |
| 17 | + | * widget('login') one widget from widgets/ [optional] | |
| 18 | + | * $config everything from config.php | |
| 19 | + | * user_logged_in() true/false | |
| 20 | + | * | |
| 21 | + | * This theme writes its menu directly in the markup below rather than calling | |
| 22 | + | * theme_menu(), to show that nothing is imposed on you. | |
| 23 | + | */ | |
| 24 | + | ?> | |
| 25 | + | <!DOCTYPE html> | |
| 26 | + | <html lang="en" dir="ltr"> | |
| 27 | + | <head> | |
| 28 | + | <meta charset="utf-8"> | |
| 29 | + | <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| 30 | + | <title><?= theme_title() ?></title> | |
| 31 | + | ||
| 32 | + | <link rel="stylesheet" href="<?= theme_asset('css/style.css') ?>"> | |
| 33 | + | <link rel="stylesheet" href="assets/fontawesome/css/font-awesome.min.css"> | |
| 34 | + | </head> | |
| 35 | + | <body class="<?= theme_body_class() ?>"> | |
| 36 | + | ||
| 37 | + | <header class="ex-header"> | |
| 38 | + | <a class="ex-logo" href="index.php"><?= theme_title() ?></a> | |
| 39 | + | ||
| 40 | + | <nav class="ex-nav"> | |
| 41 | + | <a href="index.php">Home</a> | |
| 42 | + | <a href="highscores.php">Highscores</a> | |
| 43 | + | <a href="onlinelist.php">Online</a> | |
| 44 | + | <a href="guilds.php">Guilds</a> | |
| 45 | + | <a href="shop.php">Shop</a> | |
| 46 | + | <a href="page.php?p=example">Example page</a> | |
| 47 | + | <?php if (user_logged_in()): ?> | |
| 48 | + | <a href="myaccount.php">My account</a> | |
| 49 | + | <?php else: ?> | |
| 50 | + | <a href="login.php">Login</a> | |
| 51 | + | <?php endif; ?> | |
| 52 | + | </nav> | |
| 53 | + | </header> | |
| 54 | + | ||
| 55 | + | <main class="ex-main"> | |
| 56 | + | <?php theme_content(); ?> | |
| 57 | + | </main> | |
| 58 | + | ||
| 59 | + | <footer class="ex-footer"> | |
| 60 | + | © <?= theme_title() ?> · Powered by | |
| 61 | + | <a href="credits.php">ZnoteX</a> · | |
| 62 | + | <?= elapsedTime() ?>s | |
| 63 | + | </footer> | |
| 64 | + | ||
| 65 | + | </body> | |
| 66 | + | </html> |
| @@ -0,0 +1,12 @@ | |||
| 1 | + | { | |
| 2 | + | "name": "Example", | |
| 3 | + | "author": "Alex - opengamescommunity.com", | |
| 4 | + | "version": "1.0.0", | |
| 5 | + | "requires": { | |
| 6 | + | "znotex": ">=2.0.0 <3.0.0", | |
| 7 | + | "php": ">=8.1", | |
| 8 | + | "api": "^1.0" | |
| 9 | + | }, | |
| 10 | + | "description": "Starter theme. Copy this folder, rename it, and edit. Never edit this one - it is the reference other themes are copied from.", | |
| 11 | + | "url": "https://opengamescommunity.com" | |
| 12 | + | } |
| @@ -0,0 +1,38 @@ | |||
| 1 | + | <?php | |
| 2 | + | /** | |
| 3 | + | * View for the front page (index.php). | |
| 4 | + | * | |
| 5 | + | * A view is the middle block of one root page - the part between the header | |
| 6 | + | * and the footer. The page's logic already ran: every variable it prepared is | |
| 7 | + | * available here. | |
| 8 | + | * | |
| 9 | + | * This theme only ships this one view. Every other page of the site falls back | |
| 10 | + | * to layouts/default/views/, wrapped in THIS theme's shell and styled by THIS | |
| 11 | + | * theme's CSS. Add views/highscores.php the day you want to restyle that page, | |
| 12 | + | * and not before. | |
| 13 | + | */ | |
| 14 | + | ?> | |
| 15 | + | <h1>Welcome to <?= theme_title() ?></h1> | |
| 16 | + | ||
| 17 | + | <p> | |
| 18 | + | You are looking at <code>layouts/_example/views/index.php</code>. | |
| 19 | + | The rest of the site is running on the default theme's views, dressed by | |
| 20 | + | this theme. | |
| 21 | + | </p> | |
| 22 | + | ||
| 23 | + | <?php | |
| 24 | + | $news = fetchAllNews(); | |
| 25 | + | if (is_array($news)): | |
| 26 | + | foreach (array_slice($news, 0, (int)($config['news_per_page'] ?? 5)) as $article): ?> | |
| 27 | + | <article class="well"> | |
| 28 | + | <h2><?= htmlspecialchars((string)$article['title'], ENT_QUOTES, 'UTF-8') ?></h2> | |
| 29 | + | <p class="txt"> | |
| 30 | + | <?= htmlspecialchars(getClock((int)$article['date'], true), ENT_QUOTES, 'UTF-8') ?> | |
| 31 | + | by <?= htmlspecialchars((string)($article['name'] ?? ''), ENT_QUOTES, 'UTF-8') ?> | |
| 32 | + | </p> | |
| 33 | + | <div><?= $article['text'] ?></div> | |
| 34 | + | </article> | |
| 35 | + | <?php endforeach; | |
| 36 | + | else: ?> | |
| 37 | + | <p>No news posted yet. Write one from the admin panel.</p> | |
| 38 | + | <?php endif; ?> |
| @@ -0,0 +1,22 @@ | |||
| 1 | + | MIT License | |
| 2 | + | ||
| 3 | + | Copyright (c) 2019 Stefan André Brannfjell | |
| 4 | + | Copyright (c) 2026 Alex / Open Games Community | |
| 5 | + | ||
| 6 | + | Permission is hereby granted, free of charge, to any person obtaining a copy | |
| 7 | + | of this software and associated documentation files (the "Software"), to deal | |
| 8 | + | in the Software without restriction, including without limitation the rights | |
| 9 | + | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| 10 | + | copies of the Software, and to permit persons to whom the Software is | |
| 11 | + | furnished to do so, subject to the following conditions: | |
| 12 | + | ||
| 13 | + | The above copyright notice and this permission notice shall be included in all | |
| 14 | + | copies or substantial portions of the Software. | |
| 15 | + | ||
| 16 | + | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| 17 | + | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 18 | + | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| 19 | + | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 20 | + | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| 21 | + | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
| 22 | + | SOFTWARE. |