From f3a228550953d3edd1c2b5978d8f5806e0c07985 Mon Sep 17 00:00:00 2001 From: flashwave Date: Thu, 18 Apr 2019 01:59:33 +0200 Subject: [PATCH] WIP poll support. --- assets/less/forum/forum.less | 1 + assets/less/forum/poll.less | 41 +++++++++ assets/less/input/checkbox.less | 14 +++- assets/less/main.less | 6 ++ assets/typescript/Forum/Polls.ts | 56 +++++++++++++ assets/typescript/misuzu.ts | 2 + .../2019_04_16_123231_create_polls_tables.php | 83 +++++++++++++++++++ misuzu.php | 1 + public/forum/poll.php | 50 +++++++++++ public/forum/topic.php | 6 ++ src/Forum/poll.php | 83 +++++++++++++++++++ src/Forum/topic.php | 4 + src/url.php | 1 + templates/_layout/input.twig | 9 +- templates/forum/macros.twig | 46 ++++++++++ templates/forum/topic.twig | 4 +- 16 files changed, 399 insertions(+), 8 deletions(-) create mode 100644 assets/less/forum/poll.less create mode 100644 assets/typescript/Forum/Polls.ts create mode 100644 database/2019_04_16_123231_create_polls_tables.php create mode 100644 public/forum/poll.php create mode 100644 src/Forum/poll.php diff --git a/assets/less/forum/forum.less b/assets/less/forum/forum.less index 7ee20beb..7b5f0d51 100644 --- a/assets/less/forum/forum.less +++ b/assets/less/forum/forum.less @@ -4,6 +4,7 @@ @import "confirm"; @import "header"; @import "post"; +@import "poll"; @import "status"; @import "topic"; @import "topics"; diff --git a/assets/less/forum/poll.less b/assets/less/forum/poll.less new file mode 100644 index 00000000..35801b84 --- /dev/null +++ b/assets/less/forum/poll.less @@ -0,0 +1,41 @@ +.forum__poll { + margin: 2px 0; + padding: 5px; + display: flex; + flex-direction: column; + align-items: center; + + &__options { + display: flex; + flex-direction: column; + max-width: 500px; + min-width: 100%; + + @media (min-width: 400px) { + min-width: 300px; + } + } + + &__option { + padding: 2px; + } + + &__remaining, + &__expires { + line-height: 1.5em; + + &__num, + &__datetime { + font-weight: 700; + } + } + + &__buttons { + display: flex; + margin-top: 2px; + } + + &__button { + margin: 0 2px; + } +} diff --git a/assets/less/input/checkbox.less b/assets/less/input/checkbox.less index edc5cc1c..a847ccb6 100644 --- a/assets/less/input/checkbox.less +++ b/assets/less/input/checkbox.less @@ -1,7 +1,10 @@ .input__checkbox { display: inline-flex; margin: 1px 0; - cursor: pointer; + + &:not(&--disabled) { + cursor: pointer; + } &--radio &__display, &--radio &__display__icon { @@ -12,6 +15,7 @@ display: inline-block; position: absolute; z-index: -1000; + visibility: hidden; } &__display { @@ -43,8 +47,8 @@ opacity: 1; } - &:hover &__display, - &__input:focus ~ &__display { + &:not(&--disabled):hover &__display, + &:not(&--disabled) &__input:focus ~ &__display { border-color: var(--accent-colour); } @@ -52,4 +56,8 @@ display: inline-block; margin-left: 4px; } + + &--disabled { + opacity: .5; + } } diff --git a/assets/less/main.less b/assets/less/main.less index b1533521..5ca77e08 100644 --- a/assets/less/main.less +++ b/assets/less/main.less @@ -96,6 +96,12 @@ html { } } +// Just in case. +[hidden] { + display: none !important; + visibility: hidden !important; +} + // Misc @import "animations"; @import "link"; diff --git a/assets/typescript/Forum/Polls.ts b/assets/typescript/Forum/Polls.ts new file mode 100644 index 00000000..6d0c8e53 --- /dev/null +++ b/assets/typescript/Forum/Polls.ts @@ -0,0 +1,56 @@ +function forumPollsInit(): void { + const polls: NodeListOf = document.getElementsByClassName('js-forum-poll'); + + if (polls.length < 1) { + return; + } + + for (let i = 0; i < polls.length; i++) { + forumPollInit(polls[i]); + } +} + +function forumPollInit(poll: HTMLFormElement): void { + console.log(poll); + + const options: HTMLNodeListOf = poll.getElementsByClassName('input__checkbox__input'), + votesRemaining: HTMLDivElement = poll.querySelector('.js-forum-poll-remaining'), + votesRemainingCount: HTMLSpanElement = poll.querySelector('.js-forum-poll-remaining-count'), + votesRemainingPlural: HTMLSpanElement = poll.querySelector('.js-forum-poll-remaining-plural'), + maxVotes: number = parseInt(poll.dataset.pollMaxVotes); + + if (maxVotes > 1) { + let votes: number = maxVotes; + + for (let i = 0; i < options.length; i++) { + if (options[i].checked) { + if (votes < 1) { + options[i].checked = false; + } else { + votes--; + } + } + + options[i].addEventListener('change', ev => { + if (ev.target.checked) { + if (votes < 1) { + ev.target.checked = false; + ev.preventDefault(); + return; + } + + votes--; + } else { + votes++; + } + + votesRemainingCount.textContent = votes; + votesRemainingPlural.hidden = votes == 1; + }); + } + + votesRemaining.hidden = false; + votesRemainingCount.textContent = votes; + votesRemainingPlural.hidden = votes == 1; + } +} diff --git a/assets/typescript/misuzu.ts b/assets/typescript/misuzu.ts index 50121780..80980562 100644 --- a/assets/typescript/misuzu.ts +++ b/assets/typescript/misuzu.ts @@ -8,6 +8,7 @@ /// /// /// +/// declare const timeago: any; declare const hljs: any; @@ -63,6 +64,7 @@ window.addEventListener('load', () => { commentsInit(); forumPostingInit(); + forumPollsInit(); }); function loginFormUpdateAvatar(avatarElement: HTMLElement, usernameElement: HTMLInputElement, force: boolean = false): void { diff --git a/database/2019_04_16_123231_create_polls_tables.php b/database/2019_04_16_123231_create_polls_tables.php new file mode 100644 index 00000000..947a4ea2 --- /dev/null +++ b/database/2019_04_16_123231_create_polls_tables.php @@ -0,0 +1,83 @@ +exec(" + CREATE TABLE `msz_forum_polls` ( + `poll_id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `poll_max_votes` TINYINT(3) UNSIGNED NOT NULL DEFAULT '1', + `poll_expires` TIMESTAMP NULL DEFAULT NULL, + `poll_preview_results` TINYINT(3) UNSIGNED NOT NULL DEFAULT '1', + PRIMARY KEY (`poll_id`) + ) COLLATE='utf8mb4_bin' ENGINE=InnoDB + "); + + $conn->exec(" + CREATE TABLE `msz_forum_polls_options` ( + `option_id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `poll_id` INT(10) UNSIGNED NOT NULL, + `option_text` VARCHAR(255) NOT NULL COLLATE 'utf8mb4_bin', + PRIMARY KEY (`option_id`), + INDEX `polls_options_poll_foreign` (`poll_id`), + CONSTRAINT `polls_options_poll_foreign` + FOREIGN KEY (`poll_id`) + REFERENCES `msz_forum_polls` (`poll_id`) + ON UPDATE CASCADE + ON DELETE CASCADE + ) COLLATE='utf8mb4_bin' ENGINE=InnoDB + "); + + $conn->exec(" + CREATE TABLE `msz_forum_polls_answers` ( + `user_id` INT(10) UNSIGNED NOT NULL, + `poll_id` INT(10) UNSIGNED NOT NULL, + `option_id` INT(10) UNSIGNED NOT NULL, + UNIQUE INDEX `polls_answers_unique` (`user_id`, `poll_id`, `option_id`), + INDEX `polls_answers_user_foreign` (`user_id`), + INDEX `polls_answers_poll_foreign` (`poll_id`), + INDEX `polls_answers_option_foreign` (`option_id`), + CONSTRAINT `polls_answers_option_foreign` + FOREIGN KEY (`option_id`) + REFERENCES `msz_forum_polls_options` (`option_id`) + ON UPDATE CASCADE + ON DELETE CASCADE, + CONSTRAINT `polls_answers_poll_foreign` + FOREIGN KEY (`poll_id`) + REFERENCES `msz_forum_polls` (`poll_id`) + ON UPDATE CASCADE + ON DELETE CASCADE, + CONSTRAINT `polls_answers_user_foreign` + FOREIGN KEY (`user_id`) + REFERENCES `msz_users` (`user_id`) + ON UPDATE CASCADE + ON DELETE CASCADE + ) COLLATE='utf8mb4_bin' ENGINE=InnoDB + "); + + $conn->exec(" + ALTER TABLE `msz_forum_topics` + ADD COLUMN `poll_id` INT(10) UNSIGNED NULL DEFAULT NULL AFTER `user_id`, + ADD INDEX `posts_poll_id_foreign` (`poll_id`), + ADD CONSTRAINT `posts_poll_id_foreign` + FOREIGN KEY (`poll_id`) + REFERENCES `msz_forum_polls` (`poll_id`) + ON UPDATE CASCADE + ON DELETE SET NULL; + "); +} + +function migrate_down(PDO $conn): void +{ + $conn->exec(" + ALTER TABLE `msz_forum_topics` + DROP COLUMN `poll_id`, + DROP INDEX `posts_poll_id_foreign`, + DROP FOREIGN KEY `posts_poll_id_foreign`; + "); + $conn->exec("DROP TABLE `msz_forum_polls_answers`"); + $conn->exec("DROP TABLE `msz_forum_polls_options`"); + $conn->exec("DROP TABLE `msz_forum_polls`"); +} diff --git a/misuzu.php b/misuzu.php index cba24187..3bf6901f 100644 --- a/misuzu.php +++ b/misuzu.php @@ -52,6 +52,7 @@ require_once 'src/zalgo.php'; require_once 'src/Forum/forum.php'; require_once 'src/Forum/leaderboard.php'; require_once 'src/Forum/perms.php'; +require_once 'src/Forum/poll.php'; require_once 'src/Forum/post.php'; require_once 'src/Forum/topic.php'; require_once 'src/Forum/validate.php'; diff --git a/public/forum/poll.php b/public/forum/poll.php new file mode 100644 index 00000000..e50c27cc --- /dev/null +++ b/public/forum/poll.php @@ -0,0 +1,50 @@ + 0) { + echo render_info_or_json($isXHR, 'You have been banned, check your profile for more information.', 403); + return; +} +if (user_warning_check_expiration($currentUserId, MSZ_WARN_SILENCE) > 0) { + echo render_info_or_json($isXHR, 'You have been silenced, check your profile for more information.', 403); + return; +} + +header(csrf_http_header('forum_poll')); + +if (empty($_POST['polls']) || !is_array($_POST['polls'])) { + echo render_info_or_json($isXHR, "Invalid request.", 400); + return; +} + +foreach ($_POST['polls'] as $pollId => $answerId) { + if (!is_int($pollId) || !is_string($answerId) || !ctype_digit($answerId)) { + continue; + } + + $answerId = (int)$answerId; + + var_dump($pollId, $answerId); +} diff --git a/public/forum/topic.php b/public/forum/topic.php index 68ae990b..05a4a5a3 100644 --- a/public/forum/topic.php +++ b/public/forum/topic.php @@ -38,6 +38,11 @@ if (!perms_check($perms, MSZ_FORUM_PERM_VIEW_FORUM)) { return; } +if (!empty($topic['poll_id'])) { + $pollOptions = forum_poll_options($topic['poll_id']); + $pollAnswers = []; +} + $topicIsLocked = !empty($topic['topic_locked']); $topicIsArchived = !empty($topic['topic_archived']); $topicPostsTotal = (int)($topic['topic_count_posts'] + $topic['topic_count_posts_deleted']); @@ -363,4 +368,5 @@ echo tpl_render('forum.topic', [ 'topic_can_nuke_or_restore' => $canNukeOrRestore, 'topic_can_bump' => $canBumpTopic, 'topic_can_lock' => $canLockTopic, + 'topic_poll_options' => $pollOptions ?? [], ]); diff --git a/src/Forum/poll.php b/src/Forum/poll.php new file mode 100644 index 00000000..9c22cdc9 --- /dev/null +++ b/src/Forum/poll.php @@ -0,0 +1,83 @@ +bindValue('max_votes', $maxVotes); + return (int)($createPoll->execute() ? db_last_insert_id() : -1); +} + +function forum_poll_options(int $poll): array +{ + if($poll < 1) { + return []; + } + + static $polls = []; + + if(array_key_exists($poll, $polls)) { + return $polls[$poll]; + } + + $getOptions = db_prepare(' + SELECT `option_id`, `option_text` + FROM `msz_forum_polls_options` + WHERE `poll_id` = :poll + '); + $getOptions->bindValue('poll', $poll); + + return $polls[$poll] = db_fetch_all($getOptions); +} + +function forum_poll_reset_answers(int $poll): void +{ + if ($poll < 1) { + return; + } + + $resetAnswers = db_prepare(" + DELETE FROM `msz_forum_polls_answers` + WHERE `poll_id` = :poll + "); + $resetAnswers->bindValue('poll', $poll); + $resetAnswers->execute(); +} + +function forum_poll_option_add(int $poll, string $text): int +{ + if ($poll < 1 || empty($text) || strlen($text) > 0xFF) { + return -1; + } + + $addOption = db_prepare(" + INSERT INTO `msz_forum_polls_options` + (`poll_id`, `option_text`) + VALUES + (:poll, :text) + "); + $addOption->bindValue('poll', $poll); + $addOption->bindValue('text', $text); + return (int)($createPoll->execute() ? db_last_insert_id() : -1); +} + +function forum_poll_option_remove(int $option): void +{ + if ($option < 1) { + return; + } + + $removeOption = db_prepare(" + DELETE FROM `msz_forum_polls_options` + WHERE `option_id` = :option + "); + $removeOption->bindValue('option', $option); + $removeOption->execute(); +} diff --git a/src/Forum/topic.php b/src/Forum/topic.php index d46db1fe..68686582 100644 --- a/src/Forum/topic.php +++ b/src/Forum/topic.php @@ -76,6 +76,8 @@ function forum_topic_get(int $topicId, bool $allowDeleted = false): array SELECT t.`topic_id`, t.`forum_id`, t.`topic_title`, t.`topic_type`, t.`topic_locked`, t.`topic_created`, f.`forum_archived` as `topic_archived`, t.`topic_deleted`, t.`topic_bumped`, + tp.`poll_id`, tp.`poll_max_votes`, tp.`poll_expires`, tp.`poll_preview_results`, + (tp.`poll_expires` < CURRENT_TIMESTAMP) AS `poll_expired`, fp.`topic_id` as `author_post_id`, fp.`user_id` as `author_user_id`, ( SELECT COUNT(`post_id`) @@ -98,6 +100,8 @@ function forum_topic_get(int $topicId, bool $allowDeleted = false): array FROM `msz_forum_posts` WHERE `topic_id` = t.`topic_id` ) + LEFT JOIN `msz_forum_polls` AS tp + ON tp.`poll_id` = t.`poll_id` WHERE t.`topic_id` = :topic_id %s ', diff --git a/src/url.php b/src/url.php index 7e851474..801eef14 100644 --- a/src/url.php +++ b/src/url.php @@ -60,6 +60,7 @@ define('MSZ_URLS', [ 'forum-post-nuke' => ['/forum/post.php', ['p' => '', 'm' => 'nuke']], 'forum-post-quote' => ['/forum/posting.php', ['q' => '']], 'forum-post-edit' => ['/forum/posting.php', ['p' => '', 'm' => 'edit']], + 'forum-poll-vote' => ['/forum/poll.php'], 'user-list' => ['/members.php', ['r' => '', 'ss' => '', 'sd' => '', 'p' => '']], diff --git a/templates/_layout/input.twig b/templates/_layout/input.twig index 1f71e363..c36abbed 100644 --- a/templates/_layout/input.twig +++ b/templates/_layout/input.twig @@ -23,11 +23,12 @@ {% endspaceless %} {% endmacro %} -{% macro input_checkbox_raw(name, checked, class, value, radio, attributes) %} +{% macro input_checkbox_raw(name, checked, class, value, radio, attributes, disabled) %} {% spaceless %} 0 %}name="{{ name }}"{% endif %} {% if checked %}checked{% endif %} + {% if disabled %}disabled{% endif %} {% if value|length > 0 %}value="{{ value }}"{% endif %} {% for name, value in attributes|default([]) %} {{ name }}{% if value|length > 0 %}="{{ value }}"{% endif %} @@ -35,11 +36,11 @@ {% endspaceless %} {% endmacro %} -{% macro input_checkbox(name, text, checked, class, value, radio, attributes) %} +{% macro input_checkbox(name, text, checked, class, value, radio, attributes, disabled) %} {% from _self import input_checkbox_raw %} {% spaceless %} -