diff options
| author | Murtaza Sultani <sultani@data-quest.de> | 2025-07-16 10:40:55 +0200 |
|---|---|---|
| committer | Murtaza Sultani <sultani@data-quest.de> | 2025-07-16 10:40:55 +0200 |
| commit | 3cc68c76d26e56bdb1bb59ada2c7490208a79ccf (patch) | |
| tree | 4aeb939a2f1b14813b180634a78f50ba6666a674 | |
| parent | c0c4d45c3ded5a45941a263eb063334cb8a2e98e (diff) | |
Resolve "Forum3: Man kann nicht mehr sehen, wer genau einen Beitrag auf einen Beitrag reagiert hat"
Closes #5687 and #5731
Merge request studip/studip!4359
22 files changed, 397 insertions, 137 deletions
diff --git a/resources/assets/stylesheets/scss/forum.scss b/resources/assets/stylesheets/scss/forum.scss index 9c5efc6..4b309a4 100644 --- a/resources/assets/stylesheets/scss/forum.scss +++ b/resources/assets/stylesheets/scss/forum.scss @@ -60,6 +60,11 @@ $card-max-width: 300px; .subscription-button { background-color: transparent; border-color: transparent; + justify-content: end; + padding: 0; + &:hover { + color: var(--color--highlight); + } } } @@ -216,37 +221,6 @@ $card-max-width: 300px; } } - .icon-button { - display: flex; - align-items: center; - justify-content: center; - background: white; - border: 1px solid var(--color--highlight); - color: var(--color--highlight); - border-radius: 5px; - padding: 7px; - cursor: pointer; - - &.--with-label { - padding-left: 10px; - padding-right: 10px; - } - - &:hover { - background-color: $content-color-10; - } - - &:disabled { - border-color: var(--color--button-inactive-border); - opacity: 0.5; - cursor: not-allowed; - } - - .label { - margin-left: 5px; - } - } - button.style-less { background: no-repeat; border: none; @@ -254,23 +228,21 @@ $card-max-width: 300px; } .button { - &.--with-icon { - display: inline-flex; - gap: 10px; + &--icon-only { + min-width: unset; + margin: 0; + padding: 6px; + display: flex; align-items: center; + justify-content: center; } - .icon-hover { - display: none; - } - - &:hover { - .icon-default { - display: none; - } - .icon-hover { - display: inline-block; - } + &--icon-label { + margin: 0; + display: flex; + align-items: center; + justify-content: center; + gap: 10px; } } @@ -329,26 +301,21 @@ $card-max-width: 300px; align-items: center; } - h3 { - margin: 0; - font-size: 16px; - font-weight: normal; - } - p { margin-top: 5px; color: var(--color--font-secondary); } } - .discussion-overview { - h3 { - margin-top: 0; - font-size: 16px; - font-weight: normal; - } + .topic-title, + .category-title, + .discussion-title { + font-size: 16px; + } + .discussion-overview { p { + margin-top: 5px; color: var(--color--font-secondary); } @@ -455,15 +422,20 @@ $card-max-width: 300px; z-index: 1; } + &:hover { + .topic-card__title { + color: var(--color--highlight-hover); + text-decoration: underline; + } + } + &__title { - margin-top: 0; - margin-bottom: 10px; - font-size: 16px; - font-weight: normal; + color: var(--color--highlight); } p { - opacity: 60%; + margin-top: 5px; + color: var(--color--font-secondary); } &__content { @@ -621,7 +593,12 @@ $card-max-width: 300px; align-items: center; justify-content: center; - &__create-button { + &__button-group { + display: inline-flex; + align-items: center; + } + + &__add-reaction { display: inline-flex; cursor: pointer; background: none; @@ -648,6 +625,11 @@ $card-max-width: 300px; } } + &__show-reactions { + border: none; + background: none; + } + &__container { position: absolute; top: -50px; @@ -776,6 +758,14 @@ $card-max-width: 300px; } } } + + .post-form__footer { + margin-top: 20px; + display: flex; + justify-content: center; + align-items: center; + gap: 15px; + } } .user-avatar-dropdown { @@ -1104,10 +1094,10 @@ $card-max-width: 300px; border-color: $base-color-60; } } + } - .html-emoji { - font-family: apple color emoji, segoe ui emoji, notocoloremoji, segoe ui symbol, android emoji, emojisymbols, emojione mozilla; - } + .emoji-icon { + font-family: apple color emoji, segoe ui emoji, notocoloremoji, segoe ui symbol, android emoji, emojisymbols, emojione mozilla; } .forum-subscriptions-dropdown { @@ -1176,9 +1166,8 @@ $card-max-width: 300px; .subscription-button { white-space: nowrap; + min-width: unset; display: inline-flex; - gap: 6px; - align-items: center; } } @@ -1488,6 +1477,95 @@ $card-max-width: 300px; } } + .tab { + &__buttons { + display: flex; + gap: 10px; + border-bottom: 2px solid var(--color--divider); + } + + &__button { + label { + position: relative; + transition: color 0.3s ease; + color: var(--color--highlight); + font-weight: bold; + padding: 6px 12px; + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 5px; + + &:hover { + color: var(--color--highlight-hover); + + &::after { + background-color: var(--color--focus); + } + } + + &:hover, + &.is-checked { + &::after { + content: ''; + position: absolute; + left: 0; + bottom: -2px; + width: 100%; + height: 2px; + } + } + + &.is-checked { + &::after { + background-color: var(--color--highlight); + } + } + } + + input[type="radio"] { + position: absolute; + opacity: 0; + width: 0; + height: 0; + pointer-events: none; + + &:focus + label { + outline: 2px solid var(--color--focus); + outline-offset: 2px; + border-radius: 2px; + } + } + + } + + &__content { + margin-top: 15px; + } + } + + .post-reactions-dialog { + .user-avatar-dropdown { + .dropdown__content { + right: auto; + left: 0; + } + } + } + + .user-reaction { + position: relative; + width: 35px; + height: 35px; + + .emoji-icon { + position: absolute; + bottom: 0; + right: 0; + z-index: 1; + } + } + /* vue Transition --start-- */ diff --git a/resources/assets/stylesheets/scss/personal-notifications.scss b/resources/assets/stylesheets/scss/personal-notifications.scss index 8d80fbb..1e63488 100644 --- a/resources/assets/stylesheets/scss/personal-notifications.scss +++ b/resources/assets/stylesheets/scss/personal-notifications.scss @@ -187,7 +187,7 @@ display: block; padding: 0; &:hover { - color: var(--color--highlight-hover); + color: var(--color--highlight-hover); text-decoration: none; } } @@ -224,7 +224,7 @@ .item:hover .options.hidden { visibility: visible; } } - .html-emoji { + .emoji-icon { font-family: apple color emoji, segoe ui emoji, notocoloremoji, segoe ui symbol, android emoji, emojisymbols, emojione mozilla; font-size: 20px; margin-right: 10px; diff --git a/resources/assets/stylesheets/studip.scss b/resources/assets/stylesheets/studip.scss index d923f64..080ca40 100644 --- a/resources/assets/stylesheets/studip.scss +++ b/resources/assets/stylesheets/studip.scss @@ -779,6 +779,7 @@ input.allow-plaintext-toggle { z-index: 1; right: 10px; top: 10px; + padding: 0; display: flex; align-items: center; justify-content: center; diff --git a/resources/vue/apps/forum/categories/Index.vue b/resources/vue/apps/forum/categories/Index.vue index 51646ba..abb98c0 100644 --- a/resources/vue/apps/forum/categories/Index.vue +++ b/resources/vue/apps/forum/categories/Index.vue @@ -127,14 +127,14 @@ const swapCategory = (categoryId, step) => { v-if="forumConfig.tileLayout" @click="forumConfig.toggleForumLayout()" type="button" - :title="$gettext('Tabellarische Ansicht')" class="icon-button"> + :title="$gettext('Tabellarische Ansicht')" class="button button--icon-only"> <StudipIcon shape="view-list" :size="20" /> </button> <button v-else @click="forumConfig.toggleForumLayout()" type="button" - :title="$gettext('Kachelansicht')" class="icon-button"> + :title="$gettext('Kachelansicht')" class="button button--icon-only"> <StudipIcon shape="view-wall" :size="20" /> </button> <div aria-live="polite" class="sr-only" role="status">{{ toggleLayoutMessage }}</div> diff --git a/resources/vue/apps/forum/categories/Show.vue b/resources/vue/apps/forum/categories/Show.vue index 2445646..4c340cd 100644 --- a/resources/vue/apps/forum/categories/Show.vue +++ b/resources/vue/apps/forum/categories/Show.vue @@ -95,7 +95,7 @@ onMounted(async () => { @click="forumConfig.toggleForumLayout()" type="button" :title="$gettext('Tabellarische Ansicht')" - class="icon-button"> + class="button button--icon-only"> <StudipIcon shape="view-list" :size="20" /> </button> <button @@ -103,7 +103,7 @@ onMounted(async () => { @click="forumConfig.toggleForumLayout()" type="button" :title="$gettext('Kachelansicht')" - class="icon-button"> + class="button button--icon-only"> <StudipIcon shape="view-wall" :size="20" /> </button> <div aria-live="polite" class="sr-only" role="status">{{ toggleLayoutMessage }}</div> diff --git a/resources/vue/apps/forum/discussions/Show.vue b/resources/vue/apps/forum/discussions/Show.vue index ed5043c..9343047 100644 --- a/resources/vue/apps/forum/discussions/Show.vue +++ b/resources/vue/apps/forum/discussions/Show.vue @@ -91,7 +91,7 @@ const fetchPostings = async () => { `forum-discussions/${props.discussion.discussion_id}/postings`, { data: { - include: 'author,opengraph-urls,posting,reactions,reactions.user&fields[users]=id', + include: 'author,opengraph-urls,posting,reactions,reactions.user&fields[users]=id,username,formatted-name', page: { offset } } } @@ -194,7 +194,7 @@ onMounted(async () => { </em> <StudipIcon shape="lock-locked2" :size="20" role="inactive" /> </div> - <button v-if="canEditDiscussion" @click="editDiscussion(discussion.discussion_id)" type="button" :title="$gettext('Diskussion bearbeiten')" class="icon-button"> + <button v-if="canEditDiscussion" @click="editDiscussion(discussion.discussion_id)" type="button" :title="$gettext('Diskussion bearbeiten')" class="button button--icon-only"> <StudipIcon shape="edit" :size="20" /> </button> <SubscriptionDropdown @@ -243,7 +243,7 @@ onMounted(async () => { <a v-if="!discussion.closed_at" href="#new-post" - class="button --with-icon m-0" + class="button button--icon-label" role="button" :title="$gettext('Antworten')" :aria-label="$gettext('Antworten')" @@ -252,8 +252,7 @@ onMounted(async () => { }" @click="postCreateForm = true" > - <StudipIcon shape="reply" :size="20" class="icon-default" aria-hidden="true" /> - <StudipIcon shape="reply" :size="20" class="icon-hover" role="info_alt" aria-hidden="true" /> + <StudipIcon shape="reply" :size="20" aria-hidden="true" /> {{ $gettext('Antworten') }} </a> </div> @@ -296,7 +295,7 @@ onMounted(async () => { <a v-if="!discussion.closed_at" href="#new-post" - class="button --with-icon m-0" + class="button button--icon-label" role="button" :title="$gettext('Antworten')" :aria-label="$gettext('Antworten')" @@ -305,8 +304,7 @@ onMounted(async () => { }" @click="postCreateForm = true" > - <StudipIcon shape="reply" :size="20" class="icon-default" aria-hidden="true" /> - <StudipIcon shape="reply" :size="20" class="icon-hover" role="info_alt" aria-hidden="true" /> + <StudipIcon shape="reply" :size="20" aria-hidden="true" /> {{ $gettext('Antworten') }} </a> </div> diff --git a/resources/vue/apps/forum/search/Index.vue b/resources/vue/apps/forum/search/Index.vue index 9e7c36a..2b5c4bb 100644 --- a/resources/vue/apps/forum/search/Index.vue +++ b/resources/vue/apps/forum/search/Index.vue @@ -146,14 +146,13 @@ onMounted(() => { </div> <button type="submit" - class="button m-0 --with-icon" + class="button button--icon-label" :title="$gettext('Suchen')" > - <StudipIcon shape="search" :size="20" class="icon-default" aria-hidden="true" /> - <StudipIcon shape="search" :size="20" class="icon-hover" role="info_alt" aria-hidden="true" /> + <StudipIcon shape="search" :size="20" aria-hidden="true" /> {{ $gettext('Suchen') }} </button> - <button @click="resetSearchForm" type="button" class="icon-button" :title="$gettext('Zurücksetzen')"> + <button @click="resetSearchForm" type="button" class="button button--icon-only" :title="$gettext('Zurücksetzen')"> <StudipIcon shape="decline" :size="20" /> </button> </div> diff --git a/resources/vue/apps/forum/topics/Index.vue b/resources/vue/apps/forum/topics/Index.vue index 9d70dc7..9df0a78 100644 --- a/resources/vue/apps/forum/topics/Index.vue +++ b/resources/vue/apps/forum/topics/Index.vue @@ -69,7 +69,7 @@ onMounted(async () => { @click="forumConfig.toggleForumLayout();" :title="$gettext('Tabellarische Ansicht')" type="button" - class="icon-button"> + class="button button--icon-only"> <StudipIcon shape="view-list" :size="20" /> </button> <button @@ -77,7 +77,7 @@ onMounted(async () => { @click="forumConfig.toggleForumLayout();" :title="$gettext('Kachelansicht')" type="button" - class="icon-button"> + class="button button--icon-only"> <StudipIcon shape="view-wall" :size="20" /> </button> <div aria-live="polite" class="sr-only" role="status">{{ toggleLayoutMessage }}</div> diff --git a/resources/vue/components/forum/EmptyForum.vue b/resources/vue/components/forum/EmptyForum.vue index bc7d25d..8a2c40c 100644 --- a/resources/vue/components/forum/EmptyForum.vue +++ b/resources/vue/components/forum/EmptyForum.vue @@ -24,14 +24,12 @@ const emptyForumIllustration = `${STUDIP.ASSETS_URL}images/forum/forum-keyvisual </p> <div class="buttons-container"> - <button type="button" class="button --with-icon"> - <StudipIcon shape="lightbulb" :size="20" class="icon-default" aria-hidden="true" /> - <StudipIcon shape="lightbulb" :size="20" class="icon-hover" role="info_alt" aria-hidden="true" /> + <button type="button" class="button button--icon-label"> + <StudipIcon shape="lightbulb" :size="20" aria-hidden="true" /> {{ $gettext('Tour ansehen') }} </button> - <a :href="getDiscussionCreateURL()" data-dialog="width=900;height=700" class="button --with-icon"> - <StudipIcon shape="add" :size="20" class="icon-default" aria-hidden="true" /> - <StudipIcon shape="add" :size="20" class="icon-hover" role="info_alt" aria-hidden="true" /> + <a :href="getDiscussionCreateURL()" data-dialog="width=900;height=700" class="button button--icon-label"> + <StudipIcon shape="add" :size="20" aria-hidden="true" /> {{ $gettext('Eine Diskussion starten') }} </a> </div> diff --git a/resources/vue/components/forum/SubscriptionDropdown.vue b/resources/vue/components/forum/SubscriptionDropdown.vue index c641011..422c0ae 100644 --- a/resources/vue/components/forum/SubscriptionDropdown.vue +++ b/resources/vue/components/forum/SubscriptionDropdown.vue @@ -128,7 +128,13 @@ const subscribe = async (notification_type = 'all') => { <template> <Dropdown class="forum-subscriptions-dropdown" v-model="isOpen" :title="title"> <template #trigger> - <button class="icon-button subscription-button" type="button" @click="isOpen = !isOpen" :title="title"> + <button + type="button" + :title="title" + class="button subscription-button" + :class="subscriptionButtonLabel ? 'button--icon-label' : 'button--icon-only'" + @click="isOpen = !isOpen" + > <span v-if="subscriptionButtonLabel"> {{ subscriptionButtonLabel }} </span> diff --git a/resources/vue/components/forum/categories/CategoryItem.vue b/resources/vue/components/forum/categories/CategoryItem.vue index 55685a7..0035ad1 100644 --- a/resources/vue/components/forum/categories/CategoryItem.vue +++ b/resources/vue/components/forum/categories/CategoryItem.vue @@ -74,7 +74,7 @@ const swapCategory = event => { class="title-with-actions__link" :href="getCategoryURL(category.id)" :title="$gettext('Zur Kategorie')"> - <h3 class="line-clamp-2">{{ category.name }}</h3> + <span class="category-title line-clamp-2">{{ category.name }}</span> <span v-if="category.meta.postings_count > category.meta.user_read_index" class="unread-items-badge" @@ -185,9 +185,9 @@ const swapCategory = event => { <div class="topic-card__body"> <div class="flex space-between"> <div class="flex items-start gap-10"> - <h3 class="topic-card__title line-clamp-2"> + <span class="topic-card__title category-title line-clamp-2"> {{ category.name }} - </h3> + </span> <span v-if="category.meta.postings_count > category.meta.user_read_index" diff --git a/resources/vue/components/forum/categories/Create.vue b/resources/vue/components/forum/categories/Create.vue index fa79752..a7b47df 100644 --- a/resources/vue/components/forum/categories/Create.vue +++ b/resources/vue/components/forum/categories/Create.vue @@ -18,7 +18,8 @@ defineProps({ data-dialog="size=700" :title="$gettext('Neue Kategorie anlegen')" :aria-label="$gettext('Neue Kategorie anlegen')" - class="icon-button" + class="button" + :class="label ? 'button--icon-label' : 'button--icon-only'" role="button" > <StudipIcon shape="add" :size="20" aria-hidden="true" /> diff --git a/resources/vue/components/forum/discussions/Create.vue b/resources/vue/components/forum/discussions/Create.vue index 1d88d78..ee76c72 100644 --- a/resources/vue/components/forum/discussions/Create.vue +++ b/resources/vue/components/forum/discussions/Create.vue @@ -20,8 +20,8 @@ const discussionCreateURL = computed(() => { :href="discussionCreateURL" :title="$gettext('Neue Diskussion starten')" data-dialog="width=900;height=750" - type="button" - class="icon-button"> + role="button" + class="button button--icon-only"> <StudipIcon shape="add" :size="20" aria-hidden="true" /> </a> </template> diff --git a/resources/vue/components/forum/discussions/DiscussionIndex.vue b/resources/vue/components/forum/discussions/DiscussionIndex.vue index 86fdf8a..2369888 100644 --- a/resources/vue/components/forum/discussions/DiscussionIndex.vue +++ b/resources/vue/components/forum/discussions/DiscussionIndex.vue @@ -167,7 +167,7 @@ onMounted(() => { class="title-with-actions__link" :href="getDiscussionURL(discussion.id, {redirect})" :title="$gettext('Zur Diskussion')"> - <h3 class="title-with-actions_title line-clamp-2 m-0">{{ discussion.title }}</h3> + <span class="title-with-actions_title discussion-title line-clamp-2 m-0">{{ discussion.title }}</span> <span v-if="redirect !== 'recent' && discussion.meta.postings_count > discussion.meta.user_read_index" class="unread-items-badge" diff --git a/resources/vue/components/forum/posts/Post.vue b/resources/vue/components/forum/posts/Post.vue index f954b94..28e70d2 100644 --- a/resources/vue/components/forum/posts/Post.vue +++ b/resources/vue/components/forum/posts/Post.vue @@ -203,17 +203,17 @@ const removePostHighlight = id => { <div class="inline-flex items-center gap-40"> <div v-if="!discussion.closed_at" class="inline-flex items-center gap-10"> <template v-if="post.author?.id === auth_user.id"> - <button :disabled="editPost === post.id" @click="editPost = post.id" type="button" class="icon-button" :title="$gettext('Beitrag bearbeiten')" :aria-label="$gettext('Beitrag bearbeiten')"> + <button :disabled="editPost === post.id" @click="editPost = post.id" type="button" class="button button--icon-only" :title="$gettext('Beitrag bearbeiten')" :aria-label="$gettext('Beitrag bearbeiten')"> <StudipIcon shape="edit" :size="20" aria-hidden="true" /> </button> - <button @click="deletePost(post)" type="button" class="icon-button" :title="$gettext('Beitrag löschen')" :aria-label="$gettext('Beitrag löschen')"> + <button @click="deletePost(post)" type="button" class="button button--icon-only" :title="$gettext('Beitrag löschen')" :aria-label="$gettext('Beitrag löschen')"> <StudipIcon shape="trash" :size="20" aria-hidden="true" /> </button> </template> - <button type="button" @click="forwardPost(post)" class="icon-button" :title="$gettext('Beitrage weiterleiten')" :aria-label="$gettext('Beitrage weiterleiten')"> + <button type="button" @click="forwardPost(post)" class="button button--icon-only" :title="$gettext('Beitrage weiterleiten')" :aria-label="$gettext('Beitrage weiterleiten')"> <StudipIcon shape="export" :size="20" aria-hidden="true" /> </button> - <button :disabled="postCreateForm" @click="addReply(post)" type="button" class="icon-button" :title="$gettext('Zitieren und antworten')" :aria-label="$gettext('Zitieren und Antworten')"> + <button :disabled="postCreateForm" @click="addReply(post)" type="button" class="button button--icon-only" :title="$gettext('Zitieren und antworten')" :aria-label="$gettext('Zitieren und Antworten')"> <StudipIcon shape="quote" :size="20" aria-hidden="true" /> </button> </div> diff --git a/resources/vue/components/forum/posts/PostCreateForm.vue b/resources/vue/components/forum/posts/PostCreateForm.vue index 095413a..00daeba 100644 --- a/resources/vue/components/forum/posts/PostCreateForm.vue +++ b/resources/vue/components/forum/posts/PostCreateForm.vue @@ -133,27 +133,25 @@ const storePost = async () => { <div v-if="forumConfig.anonymousPost" class="mt-10"> <StudipSwitch name="anonymous" v-model="anonymous" :label="$gettext('Anonym')" /> </div> - <div class="flex items-center gap-10"> + <div class="post-form__footer"> <button type="submit" :disabled="isLoading || !content" - class="button --with-icon" + class="button button--icon-label" :title="$gettext('Antworten')" :aria-label="$gettext('Antworten')" > - <StudipIcon shape="reply" :size="20" class="icon-default" aria-hidden="true" /> - <StudipIcon shape="reply" :size="20" class="icon-hover" role="info_alt" aria-hidden="true" /> + <StudipIcon shape="reply" :size="20" aria-hidden="true" /> {{ $gettext('Antworten') }} </button> <button type="button" - class="button --with-icon" + class="button button--icon-label" :title="$gettext('Abbrechen')" :aria-label="$gettext('Abbrechen')" @click="$emit('canceled')" > - <StudipIcon shape="decline" :size="20" class="icon-default" aria-hidden="true"/> - <StudipIcon shape="decline" :size="20" class="icon-hover" role="info_alt" aria-hidden="true"/> + <StudipIcon shape="decline" :size="20" aria-hidden="true"/> {{ $gettext('Abbrechen') }} </button> </div> diff --git a/resources/vue/components/forum/posts/PostEditForm.vue b/resources/vue/components/forum/posts/PostEditForm.vue index 9c50f90..5c9c311 100644 --- a/resources/vue/components/forum/posts/PostEditForm.vue +++ b/resources/vue/components/forum/posts/PostEditForm.vue @@ -80,26 +80,24 @@ onUnmounted(() => { <div v-if="forumConfig.anonymousPost" class="mt-10"> <StudipSwitch name="anonymous" v-model="anonymous" :label="$gettext('Anonym')" /> </div> - <div class="flex items-center gap-10"> + <div class="post-form__footer"> <button type="submit" :disabled="isLoading || !content" - class="button --with-icon" + class="button button--icon-label" :value="$gettext('Speichern')" :title="$gettext('Speichern')" > - <StudipIcon shape="reply" :size="20" class="icon-default" aria-hidden="true" /> - <StudipIcon shape="reply" :size="20" class="icon-hover" role="info_alt" aria-hidden="true" /> + <StudipIcon shape="reply" :size="20" aria-hidden="true" /> {{ $gettext('Speichern') }} </button> <button type="button" - class="button --with-icon" + class="button button--icon-label" :title="$gettext('Abbrechen')" @click="$emit('canceled')" > - <StudipIcon shape="decline" :size="20" class="icon-default" aria-hidden="true"/> - <StudipIcon shape="decline" :size="20" class="icon-hover" role="info_alt" aria-hidden="true"/> + <StudipIcon shape="decline" :size="20" aria-hidden="true"/> {{ $gettext('Abbrechen') }} </button> </div> diff --git a/resources/vue/components/forum/posts/PostReactionShow.vue b/resources/vue/components/forum/posts/PostReactionShow.vue new file mode 100644 index 0000000..a15df91 --- /dev/null +++ b/resources/vue/components/forum/posts/PostReactionShow.vue @@ -0,0 +1,111 @@ +<script setup> +import {$gettext} from "../../../../assets/javascripts/lib/gettext"; +import StudipDateTime from "../../StudipDateTime.vue"; +import UserAvatarDropdown from "../UserAvatarDropdown.vue"; +import {REACTION_ICONS} from "./reactions"; +import {userProfileURL} from "../helpers/urls"; +import {computed, onMounted} from "vue"; +import {useSortable} from "../../../composables/useSortable"; + +const props = defineProps({ + reactions: { + type: Array, + required: true + }, + emoji: { + type: String, + default: 'all' + } +}); + +const computedReactions = computed(() => { + if (props.emoji === 'all') { + return props.reactions; + } + + return props.reactions.filter(({ emoji }) => emoji === props.emoji); +}); + +const { + sortedData: sortedReactions, + sortBy, + getSortClass, + getAriaSortString, + getAriaSortLabel +} = useSortable(computedReactions); + +onMounted(() => { + sortBy('mkdate', 'desc'); +}); +</script> + +<template> + <table class="default forum-table --posts-reactors"> + <colgroup> + <col style="width: 50px;"> + <col> + <col> + </colgroup> + <thead> + <tr class="sortable"> + <th></th> + <th + :class="getSortClass('user.formatted_name')" + :aria-sort="getAriaSortString('user.formatted_name')" + :aria-label="getAriaSortLabel('user.formatted_name', $gettext('Name'))" + > + <a + href="#" + @click.prevent="sortBy('user.formatted_name')" + :title="$gettext('Nach Name sortieren')"> + {{ $gettext('Name') }} + </a> + </th> + <th + :class="getSortClass('mkdate')" + :aria-sort="getAriaSortString('mkdate')" + :aria-label="getAriaSortLabel('mkdate', $gettext('Datum'))" + > + <a + href="#" + @click.prevent="sortBy('mkdate')" + :title="$gettext('Nach Datum sortieren')"> + {{ $gettext('Datum') }} + </a> + </th> + </tr> + </thead> + <tbody> + <tr v-for="(reaction, index) in sortedReactions" :key="index"> + <td> + <div class="user-reaction"> + <UserAvatarDropdown + size="30px" + :user="{ + id: reaction.user.id, + username: reaction.user.username, + name: reaction.user.formatted_name, + avatar_url: reaction.user.meta.avatar.medium + }" + /> + <span class="emoji-icon" v-html="REACTION_ICONS[reaction.emoji].icon"></span> + <span class="sr-only">{{ emoji }}</span> + </div> + </td> + <td> + <a + :href="userProfileURL(reaction.user.username)" + :title="$gettext('Zum Profil')" + :aria-label="$gettext('Zum Profil von %{name}', { name: reaction.user.formatted_name })" + class="author-name" + > + {{ reaction.user.formatted_name }} + </a> + </td> + <td> + <StudipDateTime :iso="reaction.mkdate" :relative="true" /> + </td> + </tr> + </tbody> + </table> +</template> diff --git a/resources/vue/components/forum/posts/PostReactions.vue b/resources/vue/components/forum/posts/PostReactions.vue index 816ed15..49d6025 100644 --- a/resources/vue/components/forum/posts/PostReactions.vue +++ b/resources/vue/components/forum/posts/PostReactions.vue @@ -1,5 +1,5 @@ <script setup> -import {computed, ref, useTemplateRef} from "vue"; +import {computed, reactive, ref, useTemplateRef} from "vue"; import {REACTION_ICONS} from "./reactions"; import {$gettext} from "@/assets/javascripts/lib/gettext"; import {numberFormatter} from "../../../../assets/javascripts/lib/number_formatter"; @@ -7,6 +7,8 @@ import useDetectOutsideClick from "../../../composables/useDetectOutsideClick"; import {useForumPost} from "../../../store/pinia/forum/ForumPost"; import {deserializeJSONAPIResponse} from "../../../../assets/javascripts/lib/jsonapiUtils"; import StudipIcon from "../../StudipIcon.vue"; +import PostReactionShow from "./PostReactionShow.vue"; +import StudipDialog from "../../StudipDialog.vue"; const forumDiscussionPost = useForumPost(); const props = defineProps({ @@ -20,6 +22,7 @@ const props = defineProps({ } }); + const showReactions = ref(false); const reactionStatusMessage = ref(null); @@ -87,6 +90,11 @@ const findUserReaction = (emoji, reactions = props.reactions) => reactions.find( const reactionCreate = useTemplateRef('reactionCreate'); useDetectOutsideClick(reactionCreate, () => showReactions.value = false); + +const reactionShowDialog = reactive({ + isOpen: false, + emoji: 'all' +}); </script> <template> @@ -104,21 +112,31 @@ useDetectOutsideClick(reactionCreate, () => showReactions.value = false); :title="findUserReaction(emoji, reaction) ? $gettext('Reaktion zurücknehmen') : $gettext('Reaktion hinzufügen')" :aria-label="findUserReaction(emoji, reaction) ? $gettext('Reaktion zurücknehmen') : $gettext('Reaktion hinzufügen')" @click="toggleReaction(emoji, reaction)"> - <span class="html-emoji" v-html="REACTION_ICONS[emoji].icon"></span> + <span class="emoji-icon" v-html="REACTION_ICONS[emoji].icon"></span> <span>{{ numberFormatter(reaction.length, 1) }}</span> </button> </template> </template> <div ref="reactionCreate" class="post-reactions"> - <button - class="post-reactions__create-button" - type="button" - :title="$gettext('Reagieren')" - :aria-label="$gettext('Reagieren')" - @click="showReactions = !showReactions"> - <StudipIcon shape="add-reaction" class="add-reaction-icon" :size="18" /> - <p>{{ numberFormatter(reactions.length, 1) }}</p> - </button> + <div class="post-reactions__button-group"> + <button + type="button" + class="post-reactions__add-reaction" + :title="$gettext('Reagieren')" + :aria-label="$gettext('Reagieren')" + @click="showReactions = !showReactions"> + <StudipIcon shape="add-reaction" class="add-reaction-icon" :size="18" /> + </button> + <button + v-if="reactions.length" + type="button" + class="post-reactions__show-reactions" + :title="$gettext('Reaktionen anzeigen')" + :aria-label="$gettext('%{count} Reaktionen anzeigen', { count: reactions.length })" + @click="reactionShowDialog.isOpen = true"> + {{ numberFormatter(reactions.length, 1) }} + </button> + </div> <Transition name="fade"> <div v-if="showReactions" class="post-reactions__container"> <template v-for="(emoji, index) in REACTION_ICONS" :key="index"> @@ -131,11 +149,64 @@ useDetectOutsideClick(reactionCreate, () => showReactions.value = false); :aria-label="$gettext('Auf diesen Beitrag mit %{emojiName} reagieren', { emojiName: emoji.value })" @click="toggleReaction(emoji.value)" > - <span class="html-emoji" v-html="emoji.icon"></span> + <span class="emoji-icon" v-html="emoji.icon"></span> </button> </template> </div> </Transition> </div> </div> + + <StudipDialog + v-if="reactionShowDialog.isOpen && reactions.length" + :title="$gettext('Reaktionen anzeigen')" + :closeText="$gettext('Schließen')" + closeClass="cancel" + height="700" + width="600" + @close="reactionShowDialog.isOpen = false" + > + <template #dialogContent> + <div class="forum"> + <div class="tab post-reactions-dialog"> + <div class="tab__buttons" role="radiogroup" :aria-label="$gettext('Emoji-Filter')"> + <div class="tab__button"> + <input + type="radio" + id="reaction-all" + name="reaction-filter" + value="all" + v-model="reactionShowDialog.emoji" + /> + <label for="reaction-all" :class="{ 'is-checked': reactionShowDialog.emoji === 'all' }"> + {{ $gettext('Alle') }} + <span>{{ numberFormatter(reactions.length, 1) }}</span> + </label> + </div> + <div + v-for="(reaction, emoji) in groupedReactions" + :key="emoji" + class="tab__button" + > + <input + type="radio" + :id="`reaction-${emoji}`" + name="reaction-filter" + :value="emoji" + v-model="reactionShowDialog.emoji" + /> + <label :for="`reaction-${emoji}`" :class="{ 'is-checked': reactionShowDialog.emoji === emoji }"> + <span class="emoji-icon" v-html="REACTION_ICONS[emoji].icon" aria-hidden="true"></span> + <span class="sr-only">{{ emoji }}</span> + <span>{{ numberFormatter(reaction.length, 1) }}</span> + </label> + </div> + </div> + <div class="tab__content"> + <PostReactionShow :reactions="reactions" :emoji="reactionShowDialog.emoji" /> + </div> + </div> + </div> + </template> + </StudipDialog> </template> diff --git a/resources/vue/components/forum/topics/CreateTopic.vue b/resources/vue/components/forum/topics/CreateTopic.vue index d6d3d5f..03b7542 100644 --- a/resources/vue/components/forum/topics/CreateTopic.vue +++ b/resources/vue/components/forum/topics/CreateTopic.vue @@ -28,7 +28,8 @@ const topicCreateURL = computed(() => { data-dialog="width=700" :title="$gettext('Neues Thema anlegen')" :aria-label="$gettext('Neues Thema anlegen')" - class="icon-button" + class="button button--icon-only" + :class="label ? 'button--icon-label' : 'button--icon-only'" role="button" > <StudipIcon shape="add" :size="20" aria-hidden="true" /> diff --git a/resources/vue/components/forum/topics/TopicItem.vue b/resources/vue/components/forum/topics/TopicItem.vue index d783524..a499941 100644 --- a/resources/vue/components/forum/topics/TopicItem.vue +++ b/resources/vue/components/forum/topics/TopicItem.vue @@ -70,7 +70,7 @@ const swapTopic = event => { <div class="title-with-actions"> <div class="title-with-actions__content"> <a class="title-with-actions__link" :href="getTopicURL(topic.id)" :title="$gettext('Zum Thema')"> - <h3 class="line-clamp-2">{{ topic.name }}</h3> + <span class="topic-title line-clamp-2">{{ topic.name }}</span> <span v-if="topic.meta.postings_count > topic.meta.user_read_index" class="unread-items-badge" @@ -173,9 +173,9 @@ const swapTopic = event => { <div class="topic-card__body"> <div class="flex space-between"> <div class="flex items-start gap-10"> - <h3 class="topic-card__title line-clamp-2"> + <span class="topic-card__title topic-title line-clamp-2"> {{ topic.name }} - </h3> + </span> <span v-if="topic.meta.postings_count > topic.meta.user_read_index" diff --git a/templates/personal_notifications/notification.php b/templates/personal_notifications/notification.php index b45fc58..20f1f9d 100644 --- a/templates/personal_notifications/notification.php +++ b/templates/personal_notifications/notification.php @@ -5,7 +5,7 @@ <? if (filter_var($notification['avatar'], FILTER_VALIDATE_URL)): ?> <div class="avatar" style="background-color: currentColor; mask: url(<?= $notification['avatar'] ?>) no-repeat center / contain;;"></div> <? else: ?> - <div class="html-emoji"> + <div class="emoji-icon"> <?= $notification['avatar'] ?> </div> <? endif ?> |
