Skip to content
Open
Show file tree
Hide file tree
Changes from 12 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
13 changes: 3 additions & 10 deletions src/components/Proposal/ProposalDateItem.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!--
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->

Expand All @@ -21,9 +21,10 @@

<script lang="ts">
import type { ProposalDateInterface } from '@/types/proposals/proposalInterfaces'
import { getTimezoneOffset } from '@/services/timezoneOffsetService'

Check failure on line 24 in src/components/Proposal/ProposalDateItem.vue

View workflow job for this annotation

GitHub Actions / NPM lint

Missed spacing between "@/types/proposals/proposalInterfaces" and "@/services/timezoneOffsetService"

// types, object and stores
import { t } from '@nextcloud/l10n'

Check failure on line 27 in src/components/Proposal/ProposalDateItem.vue

View workflow job for this annotation

GitHub Actions / NPM lint

Expected "@nextcloud/l10n" (external) to come before "@/services/timezoneOffsetService" (unknown)
import moment from '@nextcloud/moment'
// icons
import ItemIcon from 'vue-material-design-icons/Calendar'
Expand Down Expand Up @@ -57,15 +58,7 @@
return ''
}
// Get the timezone offset in minutes
let timezoneOffset = 0
try {
const now = new Date()
const utcDate = new Date(now.toLocaleString('en-US', { timeZone: 'UTC' }))
const targetDate = new Date(now.toLocaleString('en-US', { timeZone: this.timezoneId }))
timezoneOffset = ((utcDate.getTime() - targetDate.getTime()) / (1000 * 60)) * -1
} catch (e) {
timezoneOffset = 0
}
const timezoneOffset = getTimezoneOffset(this.proposalDate.date, this.timezoneId)
const m = moment(this.proposalDate.date).utcOffset(timezoneOffset)
// Examples: "Mon, Jul 8, 2:30 PM" (en), "Mon, 8 Jul, 14:30" (en-GB), "Mo, 8. Jul, 14:30" (de)
return m.format('dddd, MMMM D, LT')
Expand Down
56 changes: 18 additions & 38 deletions src/components/Proposal/ProposalResponseMatrix.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!--
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->

Expand Down Expand Up @@ -137,8 +137,10 @@
import NcButton from '@nextcloud/vue/components/NcButton'
import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
import { Proposal, ProposalResponse } from '@/models/proposals/proposals'
import { getTimezoneOffset } from '@/services/timezoneOffsetService'
import { ProposalDateVote } from '@/types/proposals/proposalEnums'


Check failure on line 143 in src/components/Proposal/ProposalResponseMatrix.vue

View workflow job for this annotation

GitHub Actions / NPM lint

More than 1 blank line not allowed
export default {
name: 'ProposalResponseMatrix',

Expand Down Expand Up @@ -182,7 +184,6 @@
data() {
return {
ProposalDateVote,
timezoneOffset: 0,
}
},

Expand All @@ -201,17 +202,21 @@
const groups = {}
dates.forEach((d) => {
// Apply timezone offset for grouping by day
const key = d.date ? moment(d.date).utcOffset(this.timezoneOffset).format('yyyy-MM-dd') : 'invalid'
const offset = getTimezoneOffset(d.date.toISOString(), this.timezoneId)
const key = moment(d.date).utcOffset(offset).format('YYYY-MM-DD')
if (!groups[key]) {
groups[key] = []
}
groups[key].push(d)
})
return Object.entries(groups).map(([key, grp]: [string, ProposalDate[]]) => ({
key,
label: moment(grp[0].date).utcOffset(this.timezoneOffset).format('dddd, MMMM Do'),
dates: grp,
}))
return Object.entries(groups).map(([key, grp]: [string, ProposalDate[]]) => {
const offset = getTimezoneOffset(grp[0].date.toISOString(), this.timezoneId)
return {
key,
label: moment(grp[0].date).utcOffset(offset).format('dddd, MMMM Do'),
dates: grp,
}
})
},

columnCount() {
Expand All @@ -222,39 +227,13 @@

},

watch: {
timezoneId(newZone) {
if (newZone) {
this.timezoneOffset = this.calculateTimezoneOffset(newZone)
}
},
},

created() {
if (this.timezoneId) {
this.timezoneOffset = this.calculateTimezoneOffset(this.timezoneId)
}
},

methods: {
t,

calculateTimezoneOffset(timezoneId) {
// Get the timezone offset in minutes
try {
const now = new Date()
const utcDate = new Date(now.toLocaleString('en-US', { timeZone: 'UTC' }))
const targetDate = new Date(now.toLocaleString('en-US', { timeZone: timezoneId }))
return ((utcDate.getTime() - targetDate.getTime()) / (1000 * 60)) * -1
} catch (e) {
// Fallback to UTC if timezone is invalid
return 0
}
},

dateTimeSpan(date) {
const startDate = moment(date).utcOffset(this.timezoneOffset)
const endDate = moment(date).utcOffset(this.timezoneOffset).add(this.proposal.duration, 'minutes')
const offset = getTimezoneOffset(date.toISOString(), this.timezoneId)
const startDate = moment(date).utcOffset(offset)
const endDate = moment(date).utcOffset(offset).add(this.proposal.duration, 'minutes')

const startTime = startDate.format('LT')
const endTime = endDate.format('LT')
Expand All @@ -277,7 +256,8 @@
return ''
}
// Apply timezone offset and format very compact: "7/8 2PM"
const adjustedDate = moment(date).utcOffset(this.timezoneOffset)
const offset = getTimezoneOffset(date.toISOString(), this.timezoneId)
const adjustedDate = moment(date).utcOffset(offset)
return adjustedDate.format('M/D LT').replace(':00', '').replace(' ', ' ')
},

Expand Down
57 changes: 57 additions & 0 deletions src/services/timezoneOffsetService.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

// SPDX-SnippetBegin
// SPDX-SnippetCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
// SPDX-License-Identifier: AGPL-3.0-or-later

const dtfCache = new Map()

export function getTimezoneOffset(proposalDate, timezoneId) {
try {

Check failure on line 13 in src/services/timezoneOffsetService.js

View workflow job for this annotation

GitHub Actions / NPM lint

Expected indentation of 1 tab but found 4 spaces
const date = new Date(proposalDate)

Check failure on line 14 in src/services/timezoneOffsetService.js

View workflow job for this annotation

GitHub Actions / NPM lint

Expected indentation of 2 tabs but found 8 spaces
if (isNaN(date)) {

Check failure on line 15 in src/services/timezoneOffsetService.js

View workflow job for this annotation

GitHub Actions / NPM lint

Expected indentation of 2 tabs but found 8 spaces
return null

Check failure on line 16 in src/services/timezoneOffsetService.js

View workflow job for this annotation

GitHub Actions / NPM lint

Expected indentation of 3 tabs but found 12 spaces
}

Check failure on line 17 in src/services/timezoneOffsetService.js

View workflow job for this annotation

GitHub Actions / NPM lint

Expected indentation of 2 tabs but found 8 spaces

let dtf = dtfCache.get(timezoneId)

Check failure on line 19 in src/services/timezoneOffsetService.js

View workflow job for this annotation

GitHub Actions / NPM lint

Expected indentation of 2 tabs but found 8 spaces
if (!dtf) {

Check failure on line 20 in src/services/timezoneOffsetService.js

View workflow job for this annotation

GitHub Actions / NPM lint

Expected indentation of 2 tabs but found 8 spaces
dtf = new Intl.DateTimeFormat('en-US', {
timeZone: timezoneId,
hour12: false,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
})
dtfCache.set(timezoneId, dtf)
}

const parts = dtf.formatToParts(date)
const values = {}

for (const { type, value } of parts) {
if (type !== 'literal') {
values[type] = value
}
}

const asUTC = Date.UTC(
Number(values.year),
Number(values.month) - 1,
Number(values.day),
Number(values.hour),
Number(values.minute),
Number(values.second),
)

return Math.floor((asUTC - date.getTime()) / 60000)
} catch (error) {
return null
}
}
// SPDX-SnippetEnd
Loading