Skip to content
Open
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
11 changes: 2 additions & 9 deletions src/components/Proposal/ProposalDateItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import moment from '@nextcloud/moment'
// icons
import ItemIcon from 'vue-material-design-icons/Calendar'
import DestroyIcon from 'vue-material-design-icons/Close'
import { getTimezoneOffset } from '@/services/timezoneOffsetService'

export default {
name: 'ProposalDateItem',
Expand Down Expand Up @@ -57,15 +58,7 @@ export default {
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
53 changes: 16 additions & 37 deletions src/components/Proposal/ProposalResponseMatrix.vue
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ import NcAvatar from '@nextcloud/vue/components/NcAvatar'
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'

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

Expand All @@ -201,17 +201,21 @@ export default {
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 +226,13 @@ export default {

},

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 +255,8 @@ export default {
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
52 changes: 52 additions & 0 deletions src/services/timezoneOffsetService.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

const dtfCache = new Map()

export function getTimezoneOffset(proposalDate, timezoneId) {
try {
const date = new Date(proposalDate)
if (isNaN(date)) {
return null
}

let dtf = dtfCache.get(timezoneId)
if (!dtf) {
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 {
return null
}
}
Loading