Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ All notable changes to this project will be documented in this file.
## [unreleased]
### New
- Add option to delete orphaned votes
- Add vote indicator for locked options

### Changes
- Make vote cell focusable
Expand All @@ -16,7 +17,8 @@ All notable changes to this project will be documented in this file.

### Fixes
- Force list view mode initially on mobile viewports
- Fix some viual issues of the vote page
- Fix some visual issues of the vote page
- Bring back indicator for confirmed options after closing the poll

## [8.1.4] - 2025-07-15
### Fixes
Expand Down
2 changes: 2 additions & 0 deletions src/assets/scss/vars.scss
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@
--color-polls-background-maybe: rgba(var(--color-warning-rgb), 0.1);
--container-background-light: rgba(var(--color-info-rgb), 0.1);
--cap-width: 49rem;
--shadow-height: 0.7rem;
--shadow-height-inverted: -0.7rem;
}
156 changes: 88 additions & 68 deletions src/components/Base/modules/StickyDiv.vue
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ const style = computed(() => {
})

const stickyClass = computed(() => ({
container: true,
'sticky-top': stickyTop,
'sticky-left': stickyLeft,
'sticky-bottom-shadow': activateBottomShadow,
Expand All @@ -59,93 +58,114 @@ const stickyClass = computed(() => ({
</script>

<template>
<div :class="stickyClass" :style="style">
<slot name="default">
<div class="inner"></div>
</slot>
<div :class="['sticky-div', stickyClass]" :style="style">
<div class="top-left-corner"></div>
<div class="top"></div>
<div class="top-right-corner"></div>

<div class="right"></div>

<div class="bottom-right-corner"></div>
<div class="bottom"></div>
<div class="bottom-left-corner"></div>

<div class="left"></div>

<div class="stage outer" :style="style">
<slot name="default">
<div class="inner"></div>
</slot>
</div>
</div>
</template>

<style lang="scss" scoped>
.container {
--shadow-height: 10px;
.sticky-div {
display: grid;
grid-template-columns: auto 1fr auto;
grid-template-rows: auto 1fr auto;
grid-template-areas:
'top-left-corner top top-right-corner'
'left center right'
'bottom-left-corner bottom bottom-right-corner';
}

.inner {
.stage {
padding: 0.3rem;
grid-area: center;
width: 100%;
height: 100%;
background-color: var(--color-main-background);
}

.top-left-corner {
grid-area: top-left-corner;
height: 0;
width: 0;
}

.top {
grid-area: top;
height: 0;
}

.top-right-corner {
grid-area: top-right-corner;
height: 0;
width: 0;
}

.right {
grid-area: right;
width: 0;
}

.bottom-right-corner {
grid-area: bottom-right-corner;
height: 0;
width: 0;
}

.bottom {
grid-area: bottom;
height: 0;
}

.bottom-left-corner {
grid-area: bottom-left-corner;
height: 0;
width: 0;
}

.left {
grid-area: left;
width: 0;
}

.sticky-left {
position: sticky;
left: 0;
}

.sticky-top {
--shadow-height: 10px;
position: sticky;
top: 0;
padding-bottom: 0px;
padding-bottom: var(--shadow-height);

&::after {
content: '';
position: absolute;
bottom: 0;
left: -1px;
right: 0;
height: 0;
background: linear-gradient(
to bottom,
rgba(var(--color-box-shadow-rgb), 0.3),
rgba(var(--color-box-shadow-rgb), 0)
);
transition:
all var(--animation-slow) linear,
border 1ms;
}

&.sticky-bottom-shadow {
border-top: 0;
padding-bottom: var(--shadow-height);
margin-bottom: 0;
&::after {
height: var(--shadow-height);
}
}
}

/* TODO: Implement sticky right shadow
An Alternative could be using a grid instead of ::after
to be able to position multiple shadows in all directions */

/*
padding-right: var(--shadow-height);

&::after {
content: '';
position: absolute;
right: 0;
top: -1px;
bottom: 0;
width: 0;
background: linear-gradient(
to right,
rgba(var(--color-box-shadow-rgb), 0.3),
rgba(var(--color-box-shadow-rgb), 0)
);
transition:
all var(--animation-slow) linear,
border 1ms;
.bottom-right-corner,
.bottom,
.bottom-left-corner {
background: linear-gradient(
to bottom,
rgba(var(--color-box-shadow-rgb), 0.3),
rgba(var(--color-box-shadow-rgb), 0)
);

transition:
all var(--animation-slow) linear,
border 1ms;
.sticky-bottom-shadow & {
height: var(--shadow-height);
}

&.sticky-right-shadow {
border-right: 0;
padding-right: var(--shadow-height);
margin-right: 0;
&::after {
width: var(--shadow-height);
}
} */
}
</style>
8 changes: 7 additions & 1 deletion src/components/Options/Counter.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@
<script setup lang="ts">
import YesCounterIcon from 'vue-material-design-icons/AccountCheck.vue'
import MaybeCounterIcon from 'vue-material-design-icons/AccountCheckOutline.vue'
import CheckboxMarkedOutlinedIcon from 'vue-material-design-icons/CheckboxMarkedOutline.vue'
import { Option } from '../../Types/index.ts'
import { usePollStore } from '../../stores/poll.ts'

const pollStore = usePollStore()
interface Props {
option: Option
showMaybe?: boolean
Expand All @@ -17,7 +20,10 @@ const { option, showMaybe = false } = defineProps<Props>()
</script>

<template>
<div class="counter">
<div v-if="option.confirmed && pollStore.status.isExpired" class="counter">
<CheckboxMarkedOutlinedIcon :size="20" />
</div>
<div v-else class="counter">
<div class="yes">
<YesCounterIcon
fill-color="var(--color-polls-foreground-yes)"
Expand Down
11 changes: 11 additions & 0 deletions src/components/Options/OptionItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,17 @@ const pollStore = usePollStore()
grid-template-areas: 'drag option owner actions';
position: relative;
padding: 8px 0;
background-color: var(--color-main-background);
.confirmed & {
background-color: var(--color-polls-background-yes);
border-radius: var(--border-radius-container);
border: 2px solid var(--color-success-text);
}
.list-view .confirmed & {
padding-left: 0.5rem;
left: -0.5rem;
padding-right: 0.5rem;
}
}

.grid-area-drag-icon {
Expand Down
62 changes: 30 additions & 32 deletions src/components/VoteTable/VoteButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,64 +5,62 @@

<script setup lang="ts">
import { computed } from 'vue'
import { AxiosError } from '@nextcloud/axios'

import { t } from '@nextcloud/l10n'
import { showSuccess, showError } from '@nextcloud/dialogs'

import VoteIndicator from './VoteIndicator.vue'
import { usePollStore } from '../../stores/poll.ts'
import { useVotesStore } from '../../stores/votes.ts'
import { Answer, useVotesStore } from '../../stores/votes.ts'
import { Option, User } from '../../Types/index.ts'

import { t } from '@nextcloud/l10n'
import VoteIndicator from './VoteIndicator.vue'
import { AxiosError } from '@nextcloud/axios'

interface Props {
option: Option
user: User
}

export type richAnswer = {
name: Answer
translated: string
}

const richAnswers: { [key in Answer]: richAnswer } = {
yes: { name: 'yes', translated: t('polls', 'Yes') },
maybe: { name: 'maybe', translated: t('polls', 'Maybe') },
no: { name: 'no', translated: t('polls', 'No') },
'': { name: '', translated: t('polls', 'No answer') },
}

const { option, user } = defineProps<Props>()

const pollStore = usePollStore()
const votesStore = useVotesStore()

const thisVote = computed(() =>
const vote = computed(() =>
votesStore.getVote({
option,
user,
}),
)

const iconAnswer = computed(() => {
if (['no', ''].includes(thisVote.value.answer)) {
return pollStore.isClosed && option.confirmed ? 'no' : ''
const nextAnswer = computed<richAnswer>(() => {
if (['no', ''].includes(vote.value.answer)) {
return richAnswers.yes
}
return thisVote.value.answer
})

const nextAnswer = computed(() => {
if (pollStore.answerSequence.indexOf(thisVote.value.answer) < 0) {
return pollStore.answerSequence[1]
if (vote.value.answer === 'yes' && pollStore.configuration.allowMaybe) {
return richAnswers.maybe
}
return pollStore.answerSequence[
(pollStore.answerSequence.indexOf(thisVote.value.answer) + 1)
% pollStore.answerSequence.length
]
})
const nextAnswerTranslated = computed(() => {
if (nextAnswer.value === 'yes') {
return t('polls', 'Yes')
}
if (nextAnswer.value === 'maybe') {
return t('polls', 'Maybe')
}
return t('polls', 'No')

return pollStore.configuration.useNo ? richAnswers.no : richAnswers['']
})

async function setVote() {
try {
await votesStore.set({
option,
setTo: nextAnswer.value,
setTo: nextAnswer.value.name,
})
showSuccess(t('polls', 'Vote saved'), { timeout: 2000 })
} catch (error) {
Expand All @@ -78,15 +76,15 @@ async function setVote() {
<template>
<button
class="vote-button active"
:class="[thisVote.answer]"
:class="[vote.answer]"
:aria-label="
t('polls', 'Vote {nextAnswer} for {option}', {
t('polls', 'Click to vote with {nextAnswer} for option {option}', {
option: option.text,
nextAnswer: nextAnswerTranslated,
nextAnswer: nextAnswer.translated,
})
"
@click="setVote()">
<VoteIndicator :answer="iconAnswer" />
<VoteIndicator :answer="vote.answer" />
</button>
</template>

Expand Down
Loading