diff options
| author | moderndevslulw <moderndevslulw@alright.party> | 2025-07-06 02:06:25 +0500 |
|---|---|---|
| committer | moderndevslulw <moderndevslulw@alright.party> | 2025-07-06 02:06:25 +0500 |
| commit | 65ef7bc6c9a18e7421468d0853d0c67369c01f97 (patch) | |
| tree | aeca097ec1d647daed354660957931062e582f0d | |
| parent | bb920b117c33c8f78331aa1aea0470efece356a5 (diff) | |
feat: channel pages
| -rw-r--r-- | lib/partials.php | 5 | ||||
| -rw-r--r-- | lib/utils.php | 21 | ||||
| -rw-r--r-- | public/channels/index.php | 372 | ||||
| -rw-r--r-- | public/static/img/icons/door_in.png | bin | 0 -> 940 bytes | |||
| -rw-r--r-- | public/static/img/icons/door_out.png | bin | 0 -> 932 bytes | |||
| -rw-r--r-- | public/static/style.css | 143 |
6 files changed, 539 insertions, 2 deletions
diff --git a/lib/partials.php b/lib/partials.php index 0fec678..f957cb5 100644 --- a/lib/partials.php +++ b/lib/partials.php @@ -1,4 +1,6 @@ <?php +include_once $_SERVER['DOCUMENT_ROOT'] . '/../config.php'; + function html_navigation_bar() { echo '' ?> @@ -11,6 +13,9 @@ function html_navigation_bar() <section class="row gap-16"> <a href="/">home</a> <a href="/wiki.php">wiki</a> + <?php if (SHOW_CHANNEL_LIST): ?> + <a href="/channels/">channels</a> + <?php endif; ?> </section> </header> <?php ; diff --git a/lib/utils.php b/lib/utils.php new file mode 100644 index 0000000..6de3dfb --- /dev/null +++ b/lib/utils.php @@ -0,0 +1,21 @@ +<?php +function format_timestamp($timestamp) +{ + $days = (int) floor($timestamp / (60.0 * 60.0 * 24.0)); + $years = (int) floor($days / 365); + $hours = (int) floor(round($timestamp / (60 * 60)) % 24); + $minutes = (int) floor(round($timestamp % (60 * 60)) / 60); + $seconds = (int) floor($timestamp % 60); + + if ($years == 0 && $days == 0 && $hours == 0 && $minutes == 0) { + return "$seconds second" . ($seconds > 1 ? "s" : ""); + } else if ($years == 0 && $days == 0 && $hours == 0) { + return "$minutes minute" . ($minutes > 1 ? "s" : ""); + } else if ($years == 0 && $days == 0) { + return "$hours hour" . ($hours > 1 ? "s" : ""); + } else if ($years == 0) { + return "$days day" . ($days > 1 ? "s" : ""); + } else { + return "$years year" . ($years > 1 ? "s" : ""); + } +}
\ No newline at end of file diff --git a/public/channels/index.php b/public/channels/index.php new file mode 100644 index 0000000..b32b63b --- /dev/null +++ b/public/channels/index.php @@ -0,0 +1,372 @@ +<?php +include_once $_SERVER['DOCUMENT_ROOT'] . '/../lib/partials.php'; +include_once $_SERVER['DOCUMENT_ROOT'] . '/../lib/utils.php'; +include_once $_SERVER['DOCUMENT_ROOT'] . '/../config.php'; + +function get_twitch_users(string $ids): array +{ + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, "https://api.twitch.tv/helix/users?id=$ids"); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Authorization: Bearer ' . TWITCH_AUTHORIZATION_TOKEN, + 'Client-Id: ' . TWITCH_CLIENT_ID + ]); + curl_setopt($ch, CURLOPT_HEADER, 0); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "GET"); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 30); + + $response = curl_exec($ch); + $response = json_decode($response, true); + + curl_close($ch); + + return $response['data'] ?? []; +} + +$channel = null; +$channels = []; + +$db = new PDO(DB_URL, DB_USER, DB_PASS); + +if (isset($_GET['alias_id'])) { + $stmt = $db->prepare('SELECT c.*, cp.*, array_to_json(cp.features) as features + FROM channels c + LEFT JOIN channel_preferences cp ON cp.id = c.id + WHERE c.alias_id = ? + '); + $stmt->execute([$_GET['alias_id']]); + $channel = $stmt->fetch(PDO::FETCH_ASSOC) ?: null; + + if (!isset($channel)) { + http_response_code(404); + exit; + } + + $channel['features'] = json_decode($channel['features'] ?: '[]', true); + + // fetching custom commands + $stmt = $db->prepare('SELECT *, array_to_json(messages) as messages + FROM custom_commands + WHERE channel_id = ? + ORDER BY created_at DESC + '); + $stmt->execute([$channel['id']]); + $channel['commands'] = $stmt->fetchAll(PDO::FETCH_ASSOC); + foreach ($channel['commands'] as &$c) { + $c['messages'] = json_decode($c['messages'], true); + } + unset($c); + + // fetching timers + $stmt = $db->prepare('SELECT *, array_to_json(messages) as messages + FROM timers + WHERE channel_id = ? + '); + $stmt->execute([$channel['id']]); + $channel['timers'] = $stmt->fetchAll(PDO::FETCH_ASSOC); + foreach ($channel['timers'] as &$c) { + $c['messages'] = json_decode($c['messages'], true); + } + unset($c); + + // fetching events + $stmt = $db->prepare('SELECT e.*, array_to_json(e.flags) as flags, COUNT(es.id) as subscription_count + FROM events e + LEFT JOIN event_subscriptions es ON es.event_id = e.id + WHERE e.channel_id = ? + GROUP BY e.id + ORDER BY subscription_count DESC + '); + $stmt->execute([$channel['id']]); + $channel['events'] = $stmt->fetchAll(PDO::FETCH_ASSOC); + + // fetching event names + $twitch_types = ['live', 'offline', 'title', 'category']; + $channel_ids = []; + foreach ($channel['events'] as &$e) { + $e['flags'] = json_decode($e['flags'], true); + if (in_array($e['event_type'], $twitch_types)) { + array_push($channel_ids, $e['target_alias_id']); + } + } + unset($e); + if (!empty($channel_ids)) { + $event_users = get_twitch_users(implode('&id=', $channel_ids)); + foreach ($event_users as $user) { + foreach ($channel['events'] as &$e) { + if ($e['target_alias_id'] == $user['id']) { + $e['name'] = $user['login']; + } + } + unset($e); + } + } + + // translating features + foreach ($channel['features'] as &$f) { + if ($f == "notify_7tv_updates") { + $f = "7TV Updates"; + } + } + unset($f); + + if ($response = get_twitch_users($channel['alias_id'])) { + $user = $response[0]; + $channel['pfp'] = $user['profile_image_url']; + $channel['description'] = $user['description'] ?: null; + } + + $has_content = !empty($channel['events']) || !empty($channel['commands']) || !empty($channel['timers']); +} else if (!SHOW_CHANNEL_LIST) { + http_response_code(403); + exit; +} else { + $stmt = $db->query('SELECT alias_id, alias_name FROM channels WHERE opt_outed_at IS NULL ORDER BY joined_at DESC'); + $stmt->execute(); + + $channels = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: []; +} + +// fetching user pfps +if (!empty($channels)) { + $channel_ids = []; + foreach ($channels as $c) { + array_push($channel_ids, $c['alias_id']); + } + + $response = get_twitch_users(implode('&id=', $channel_ids)); + + if (!empty($response)) { + foreach ($response as $c) { + $index = array_search($c['id'], array_column($channels, 'alias_id')); + $channels[$index]['pfp'] = $c['profile_image_url']; + } + } +} +?> + +<html> + +<head> + <title>Channels - The Tinybot Project</title> + <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"> + <link rel="stylesheet" href="/static/style.css"> + <link rel="shortcut icon" href="/static/favicon.ico" type="image/x-icon"> +</head> + +<body> + <main> + <?php html_navigation_bar() ?> + <content class="gap-16"> + <?php if (isset($channel)): ?> + <section class="card row p-16"> + <?php if (isset($channel['pfp'])): ?> + <div class="icon"> + <img src="<?= $channel['pfp'] ?>" alt=""> + </div> + <?php endif; ?> + <div class="column gap-8 grow"> + <h1>About <?= $channel['alias_name'] ?></h1> + <?php if (isset($channel['description'])): ?> + <p><i><?= $channel['description'] ?></i></p> + <?php endif; ?> + <div class="row gap-16 font-small"> + <p>Language: <img src="/static/img/icons/flags/<?= $channel['language'] ?>.png" alt=""> + <?= ucfirst($channel['language']) ?></p> + <p>Prefix: <b><?= $channel['prefix'] ?></b></p> + </div> + <?php if (!empty($channel['features'])): ?> + <div class="row gap-16 font-small"> + <p>Features: <b><?= implode(', ', $channel['features']) ?></b></p> + </div> + <?php endif; ?> + </div> + <div class="column gap-8"> + <p><img src="/static/img/icons/door_in.png" alt=""> Joined + <?= format_timestamp(time() - strtotime($channel['joined_at'])) ?> ago + </p> + <?php if (isset($channel['opt_outed_at'])): ?> + <p class="red box text-center"><img src="/static/img/icons/door_out.png" alt=""> Opted out!</p> + <?php endif; ?> + </div> + </section> + + <!-- CHANNEL CONTENT --> + <?php if ($has_content): ?> + <section class="box" id="channel-content"> + <div class="tabs row gap-8" id="channel-content-tabs" style="display: none"> + <?php if (!empty($channel['events'])): ?> + <button class="tab" id="events-button">Events</button> + <?php endif; ?> + <?php if (!empty($channel['commands'])): ?> + + <button class="tab" id="commands-button">Custom commands</button> + <?php endif; ?> + + <?php if (!empty($channel['timers'])): ?> + + <button class="tab" id="timers-button">Timers</button> + <?php endif; ?> + + </div> + <div class="content"> + <?php if (!empty($channel['events'])): ?> + <!-- Events --> + <div class="tab-content column" id="events"> + <h2>Events</h2> + <hr> + <table> + <tr> + <th>Name</th> + <th>Type</th> + <th>Message</th> + <th>Flags</th> + <th>Subscribers</th> + </tr> + <?php foreach ($channel['events'] as $e): ?> + <tr> + <td> + <?php if (in_array($e['event_type'], $twitch_types)): ?> + <a href="https://twitch.tv/<?= $e['name'] ?>"><?= $e['name'] ?></a> + <?php elseif ($e['event_type'] == 'github'): ?> + <a + href="https://github.com/<?= $e['custom_alias_id'] ?>"><?= $e['custom_alias_id'] ?></a> + <?php else: ?> + <?= $e['custom_alias_id'] ?> + <?php endif; ?> + </td> + <td><?= $e['event_type'] ?></td> + <td><?= $e['message'] ?></td> + <td> + <?= empty($e['flags']) ? '-' : implode(', ', $e['flags']) ?> + </td> + <td><?= $e['subscription_count'] ?></td> + </tr> + <?php endforeach; ?> + </table> + </div> + <?php endif; ?> + + <?php if (!empty($channel['commands'])): ?> + <!-- Commands --> + <div class="tab-content column" id="commands"> + <h2>Custom commands</h2> + <hr> + <table> + <tr> + <th>Name</th> + <th>Messages</th> + <th>Last executed</th> + </tr> + <?php foreach ($channel['commands'] as $c): ?> + <tr> + <td><?= $c['name'] ?></td> + <td class="column"> + <?php foreach ($c['messages'] as $m): ?> + <p><?= $m ?></p> + <?php endforeach; ?> + </td> + <td><?= isset($c['last_executed_at']) ? format_timestamp(time() - strtotime($c['last_executed_at'])) . ' ago' : '-' ?> + </td> + </tr> + <?php endforeach; ?> + </table> + </div> + <?php endif; ?> + + <?php if (!empty($channel['timers'])): ?> + <!-- Timers --> + <div class="tab-content column" id="timers"> + <h2>Timers</h2> + <hr> + <table> + <tr> + <th>Name</th> + <th>Messages</th> + <th>Interval</th> + <th>Last executed</th> + </tr> + <?php foreach ($channel['timers'] as $t): ?> + <tr> + <td><?= $t['name'] ?></td> + <td class="column"> + <?php foreach ($t['messages'] as $m): ?> + <p><?= $m ?></p> + <?php endforeach; ?> + </td> + <td><?= format_timestamp($t['interval_sec']) ?></td> + <td><?= format_timestamp(time() - strtotime($t['last_executed_at'])) ?> ago</td> + </tr> + <?php endforeach; ?> + </table> + </div> + <?php endif; ?> + </div> + </section> + <?php else: ?> + <p class="text-center"><i>Nothing found...</i></p> + <?php endif; ?> + <?php elseif (empty($channels)): ?> + <section class="column justify-center align-center background-colorful p-24"> + <h1>No one has joined yet... ;(</h1> + <a href="/!join">Be the first one!</a> + </section> + <?php else: ?> + <section class="column justify-center align-center background-colorful p-24"> + <h1><?= count($channels) ?> channels have already joined us</h1> + <a href="/!join">Check out the !join command to participate!</a> + </section> + <section class="grid-4 gap-16"> + <?php foreach ($channels as $c): ?> + <a href="/channels/?alias_id=<?= $c['alias_id'] ?>" class="card justify-center align-center"> + <div class="icon"> + <img src="<?= $c['pfp'] ?? '/static/img/default_avatar.webp' ?>" alt=""> + </div> + <p><b><?= $c['alias_name'] ?></b></p> + </a> + <?php endforeach; ?> + </section> + <?php endif; ?> + </content> + </main> + + <?php html_footer() ?> +</body> + +<?php if (isset($channel) && $has_content): ?> + <script> + for (const elem of document.querySelectorAll('#channel-content h2, #channel-content hr')) { + elem.remove(); + } + + document.getElementById('channel-content-tabs').style.display = 'flex'; + + const tabButtons = document.querySelectorAll('#channel-content-tabs button'); + const tabContent = document.querySelectorAll('.tab-content'); + + function enableTab(name) { + tabButtons.forEach(x => { + if (x.getAttribute('id') == name + '-button') { + x.setAttribute('disabled', ''); + } else { + x.removeAttribute('disabled'); + } + }); + + tabContent.forEach(x => { + x.style.display = x.getAttribute('id') == name ? 'flex' : 'none'; + }); + } + + enableTab(tabButtons[0].getAttribute('id').split('-')[0]); + + for (const tabButton of tabButtons) { + const id = tabButton.getAttribute('id'); + const name = id.split('-')[0]; + tabButton.addEventListener('click', () => enableTab(name)); + } + </script> +<?php endif; ?> + +</html>
\ No newline at end of file diff --git a/public/static/img/icons/door_in.png b/public/static/img/icons/door_in.png Binary files differnew file mode 100644 index 0000000..d2ee9c6 --- /dev/null +++ b/public/static/img/icons/door_in.png diff --git a/public/static/img/icons/door_out.png b/public/static/img/icons/door_out.png Binary files differnew file mode 100644 index 0000000..466ea2c --- /dev/null +++ b/public/static/img/icons/door_out.png diff --git a/public/static/style.css b/public/static/style.css index 739e76c..2b6dcdd 100644 --- a/public/static/style.css +++ b/public/static/style.css @@ -2,8 +2,13 @@ --primary-color: #6ee7b7; --secondary-color: #59c59a; - --box-background: #9bbdad; - --box-border: #92b3a3; + --table-border: #d4d4d4; + + --box-background: #eef5f2; + --box-border: #77ac90; + --box-tab-background: #a2f3cb; + --box-tab-background-disabled: #8ad8b1; + --box-tab-foreground: #000000; --promo-button-background: linear-gradient(0deg, #ff9028 0% 50%, #ffb224 50% 100%); --promo-button-background-hover: #ff9028; @@ -12,6 +17,10 @@ --wiki-content-background: #fff; --wiki-sidebar-background: #fefefe; + + --card-background: #eee; + --card-border: #bdbdbd; + --card-background-hover: #e4e4e4; } * { @@ -94,6 +103,34 @@ button:hover { background: var(--promo-button-background-hover); } +table { + border-collapse: collapse; +} + +table tr { + border-bottom: 1px solid var(--table-border); +} + +table td, +table th { + border-right: 1px solid var(--table-border); +} + +table td:last-child, +table th:last-child { + border-right: none; +} + +table td, +table th { + padding: 4px; + text-align: left; +} + +table tr:last-child { + border-bottom: none; +} + .img-gradient { position: relative; display: inline-block; @@ -315,6 +352,87 @@ div:has(.wiki-content) { } /** +--- CHANNELS +*/ + +.card { + display: flex; + flex-direction: column; + padding: 16px; + gap: 16px; + background: var(--card-background); + border: 1px solid var(--card-border); + text-decoration: none; +} + +a.card:hover { + color: unset; + background: var(--card-background-hover); +} + +.card .icon { + border: 1px solid #000; +} + +.card .icon img { + width: 128px; + height: auto; +} + +.card p { + text-decoration: none; +} + +/** +--- BOX +*/ + +.box, +.box>.content { + padding: 4px; + background: var(--box-background); + border: 1px solid var(--box-border); +} + +.box:has(.tabs) { + background: none; + border: none; + padding: 0; +} + +.box>.tabs { + display: flex; + flex-direction: row; + align-items: end; + gap: 4px; +} + +.tab { + background: var(--box-tab-background); + color: var(--box-tab-foreground); + text-shadow: none; + border: 1px solid var(--box-border); + border-radius: 0; + padding: 2px 8px; +} + +.tabs>.tab { + background: var(--box-tab-background-disabled); + border-bottom: none; +} + +.tabs>.tab:disabled { + background: var(--box-tab-background); + padding: 4px 10px; + margin-bottom: 0; +} + +.box.red { + background: #ff6464; + border-color: maroon; +} + +/** --- SHORTCUTS */ @@ -336,11 +454,20 @@ div:has(.wiki-content) { flex-direction: row; } +.grow { + flex-grow: 1; +} + .grid-2 { display: grid; grid-template-columns: auto auto; } +.grid-4 { + display: grid; + grid-template-columns: auto auto auto auto; +} + .align-center { align-items: center; } @@ -361,6 +488,10 @@ div:has(.wiki-content) { text-align: right; } +.text-center { + text-align: center; +} + .gap-64 { gap: 64px; } @@ -377,6 +508,10 @@ div:has(.wiki-content) { gap: 8px; } +.p-24 { + padding: 24px; +} + .p-16 { padding: 16px; } @@ -387,4 +522,8 @@ div:has(.wiki-content) { .border { border: 0.25px solid rgba(133, 133, 133, 0.25); +} + +.font-small { + font-size: 12px; }
\ No newline at end of file |
