summaryrefslogtreecommitdiff
path: root/public
diff options
context:
space:
mode:
authormoderndevslulw <moderndevslulw@alright.party>2025-07-06 02:06:25 +0500
committermoderndevslulw <moderndevslulw@alright.party>2025-07-06 02:06:25 +0500
commit65ef7bc6c9a18e7421468d0853d0c67369c01f97 (patch)
treeaeca097ec1d647daed354660957931062e582f0d /public
parentbb920b117c33c8f78331aa1aea0470efece356a5 (diff)
feat: channel pages
Diffstat (limited to 'public')
-rw-r--r--public/channels/index.php372
-rw-r--r--public/static/img/icons/door_in.pngbin0 -> 940 bytes
-rw-r--r--public/static/img/icons/door_out.pngbin0 -> 932 bytes
-rw-r--r--public/static/style.css143
4 files changed, 513 insertions, 2 deletions
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... &semi;&lpar;</h1>
+ <a href="/!join">Be the first one&excl;</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 &excl;join command to participate&excl;</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
new file mode 100644
index 0000000..d2ee9c6
--- /dev/null
+++ b/public/static/img/icons/door_in.png
Binary files differ
diff --git a/public/static/img/icons/door_out.png b/public/static/img/icons/door_out.png
new file mode 100644
index 0000000..466ea2c
--- /dev/null
+++ b/public/static/img/icons/door_out.png
Binary files differ
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