Browse Source

Replaced moment.js with Javascript date.

Thanks to xet7 !
Lauri Ojansivu 5 days ago
parent
commit
cb6afe67a7

+ 29 - 15
client/components/cards/cardCustomFields.js

@@ -1,6 +1,25 @@
-import moment from 'moment/min/moment-with-locales';
 import { TAPi18n } from '/imports/i18n';
 import { DatePicker } from '/client/lib/datepicker';
+import { 
+  formatDateTime, 
+  formatDate, 
+  formatTime, 
+  getISOWeek, 
+  isValidDate, 
+  isBefore, 
+  isAfter, 
+  isSame, 
+  add, 
+  subtract, 
+  startOf, 
+  endOf, 
+  format, 
+  parseDate, 
+  now, 
+  createDate, 
+  fromNow, 
+  calendar 
+} from '/imports/lib/dateUtils';
 import Cards from '/models/cards';
 import { CustomFieldStringTemplate } from '/client/lib/customFields'
 
@@ -134,18 +153,18 @@ CardCustomField.register('cardCustomField');
     super.onCreated();
     const self = this;
     self.date = ReactiveVar();
-    self.now = ReactiveVar(moment());
+    self.now = ReactiveVar(now());
     window.setInterval(() => {
-      self.now.set(moment());
+      self.now.set(now());
     }, 60000);
 
     self.autorun(() => {
-      self.date.set(moment(self.data().value));
+      self.date.set(new Date(self.data().value));
     });
   }
 
   showWeek() {
-    return this.date.get().week().toString();
+    return getISOWeek(this.date.get()).toString();
   }
 
   showWeekOfYear() {
@@ -153,12 +172,7 @@ CardCustomField.register('cardCustomField');
   }
 
   showDate() {
-    // this will start working once mquandalle:moment
-    // is updated to at least moment.js 2.10.5
-    // until then, the date is displayed in the "L" format
-    return this.date.get().calendar(null, {
-      sameElse: 'llll',
-    });
+    return calendar(this.date.get());
   }
 
   showISODate() {
@@ -167,8 +181,8 @@ CardCustomField.register('cardCustomField');
 
   classes() {
     if (
-      this.date.get().isBefore(this.now.get(), 'minute') &&
-      this.now.get().isBefore(this.data().value)
+      isBefore(this.date.get(), this.now.get(), 'minute') &&
+      isBefore(this.now.get(), this.data().value, 'minute')
     ) {
       return 'current';
     }
@@ -176,7 +190,7 @@ CardCustomField.register('cardCustomField');
   }
 
   showTitle() {
-    return `${TAPi18n.__('card-start-on')} ${this.date.get().format('LLLL')}`;
+    return `${TAPi18n.__('card-start-on')} ${this.date.get().toLocaleString()}`;
   }
 
   events() {
@@ -195,7 +209,7 @@ CardCustomField.register('cardCustomField');
     const self = this;
     self.card = Utils.getCurrentCard();
     self.customFieldId = this.data()._id;
-    this.data().value && this.date.set(moment(this.data().value));
+    this.data().value && this.date.set(new Date(this.data().value));
   }
 
   _storeDate(date) {

+ 50 - 36
client/components/cards/cardDate.js

@@ -1,17 +1,36 @@
-import moment from 'moment/min/moment-with-locales';
 import { TAPi18n } from '/imports/i18n';
 import { DatePicker } from '/client/lib/datepicker';
+import { 
+  formatDateTime, 
+  formatDate, 
+  formatTime, 
+  getISOWeek, 
+  isValidDate, 
+  isBefore, 
+  isAfter, 
+  isSame, 
+  add, 
+  subtract, 
+  startOf, 
+  endOf, 
+  format, 
+  parseDate, 
+  now, 
+  createDate, 
+  fromNow, 
+  calendar 
+} from '/imports/lib/dateUtils';
 
 // editCardReceivedDatePopup
 (class extends DatePicker {
   onCreated() {
-    super.onCreated(moment().format('YYYY-MM-DD HH:mm'));
+    super.onCreated(formatDateTime(now()));
     this.data().getReceived() &&
-      this.date.set(moment(this.data().getReceived()));
+      this.date.set(new Date(this.data().getReceived()));
   }
 
   _storeDate(date) {
-    this.card.setReceived(moment(date).format('YYYY-MM-DD HH:mm'));
+    this.card.setReceived(formatDateTime(date));
   }
 
   _deleteDate() {
@@ -22,8 +41,8 @@ import { DatePicker } from '/client/lib/datepicker';
 // editCardStartDatePopup
 (class extends DatePicker {
   onCreated() {
-    super.onCreated(moment().format('YYYY-MM-DD HH:mm'));
-    this.data().getStart() && this.date.set(moment(this.data().getStart()));
+    super.onCreated(formatDateTime(now()));
+    this.data().getStart() && this.date.set(new Date(this.data().getStart()));
   }
 
   onRendered() {
@@ -37,7 +56,7 @@ import { DatePicker } from '/client/lib/datepicker';
   }
 
   _storeDate(date) {
-    this.card.setStart(moment(date).format('YYYY-MM-DD HH:mm'));
+    this.card.setStart(formatDateTime(date));
   }
 
   _deleteDate() {
@@ -49,7 +68,7 @@ import { DatePicker } from '/client/lib/datepicker';
 (class extends DatePicker {
   onCreated() {
     super.onCreated('1970-01-01 17:00:00');
-    this.data().getDue() && this.date.set(moment(this.data().getDue()));
+    this.data().getDue() && this.date.set(new Date(this.data().getDue()));
   }
 
   onRendered() {
@@ -60,7 +79,7 @@ import { DatePicker } from '/client/lib/datepicker';
   }
 
   _storeDate(date) {
-    this.card.setDue(moment(date).format('YYYY-MM-DD HH:mm'));
+    this.card.setDue(formatDateTime(date));
   }
 
   _deleteDate() {
@@ -71,8 +90,8 @@ import { DatePicker } from '/client/lib/datepicker';
 // editCardEndDatePopup
 (class extends DatePicker {
   onCreated() {
-    super.onCreated(moment().format('YYYY-MM-DD HH:mm'));
-    this.data().getEnd() && this.date.set(moment(this.data().getEnd()));
+    super.onCreated(formatDateTime(now()));
+    this.data().getEnd() && this.date.set(new Date(this.data().getEnd()));
   }
 
   onRendered() {
@@ -83,7 +102,7 @@ import { DatePicker } from '/client/lib/datepicker';
   }
 
   _storeDate(date) {
-    this.card.setEnd(moment(date).format('YYYY-MM-DD HH:mm'));
+    this.card.setEnd(formatDateTime(date));
   }
 
   _deleteDate() {
@@ -100,14 +119,14 @@ const CardDate = BlazeComponent.extendComponent({
   onCreated() {
     const self = this;
     self.date = ReactiveVar();
-    self.now = ReactiveVar(moment());
+    self.now = ReactiveVar(now());
     window.setInterval(() => {
-      self.now.set(moment());
+      self.now.set(now());
     }, 60000);
   },
 
   showWeek() {
-    return this.date.get().week().toString();
+    return getISOWeek(this.date.get()).toString();
   },
 
   showWeekOfYear() {
@@ -115,12 +134,7 @@ const CardDate = BlazeComponent.extendComponent({
   },
 
   showDate() {
-    // this will start working once mquandalle:moment
-    // is updated to at least moment.js 2.10.5
-    // until then, the date is displayed in the "L" format
-    return this.date.get().calendar(null, {
-      sameElse: 'llll',
-    });
+    return calendar(this.date.get());
   },
 
   showISODate() {
@@ -133,7 +147,7 @@ class CardReceivedDate extends CardDate {
     super.onCreated();
     const self = this;
     self.autorun(() => {
-      self.date.set(moment(self.data().getReceived()));
+      self.date.set(new Date(self.data().getReceived()));
     });
   }
 
@@ -173,7 +187,7 @@ class CardStartDate extends CardDate {
     super.onCreated();
     const self = this;
     self.autorun(() => {
-      self.date.set(moment(self.data().getStart()));
+      self.date.set(new Date(self.data().getStart()));
     });
   }
 
@@ -208,7 +222,7 @@ class CardDueDate extends CardDate {
     super.onCreated();
     const self = this;
     self.autorun(() => {
-      self.date.set(moment(self.data().getDue()));
+      self.date.set(new Date(self.data().getDue()));
     });
   }
 
@@ -244,7 +258,7 @@ class CardEndDate extends CardDate {
     super.onCreated();
     const self = this;
     self.autorun(() => {
-      self.date.set(moment(self.data().getEnd()));
+      self.date.set(new Date(self.data().getEnd()));
     });
   }
 
@@ -279,12 +293,12 @@ class CardCustomFieldDate extends CardDate {
     super.onCreated();
     const self = this;
     self.autorun(() => {
-      self.date.set(moment(self.data().value));
+      self.date.set(new Date(self.data().value));
     });
   }
 
   showWeek() {
-    return this.date.get().week().toString();
+    return getISOWeek(this.date.get()).toString();
   }
 
   showWeekOfYear() {
@@ -316,31 +330,31 @@ CardCustomFieldDate.register('cardCustomFieldDate');
 
 (class extends CardReceivedDate {
   showDate() {
-    return this.date.get().format('L');
+    return format(this.date.get(), 'L');
   }
 }.register('minicardReceivedDate'));
 
 (class extends CardStartDate {
   showDate() {
-    return this.date.get().format('YYYY-MM-DD HH:mm');
+    return format(this.date.get(), 'YYYY-MM-DD HH:mm');
   }
 }.register('minicardStartDate'));
 
 (class extends CardDueDate {
   showDate() {
-    return this.date.get().format('YYYY-MM-DD HH:mm');
+    return format(this.date.get(), 'YYYY-MM-DD HH:mm');
   }
 }.register('minicardDueDate'));
 
 (class extends CardEndDate {
   showDate() {
-    return this.date.get().format('YYYY-MM-DD HH:mm');
+    return format(this.date.get(), 'YYYY-MM-DD HH:mm');
   }
 }.register('minicardEndDate'));
 
 (class extends CardCustomFieldDate {
   showDate() {
-    return this.date.get().format('L');
+    return format(this.date.get(), 'L');
   }
 }.register('minicardCustomFieldDate'));
 
@@ -349,7 +363,7 @@ class VoteEndDate extends CardDate {
     super.onCreated();
     const self = this;
     self.autorun(() => {
-      self.date.set(moment(self.data().getVoteEnd()));
+      self.date.set(new Date(self.data().getVoteEnd()));
     });
   }
   classes() {
@@ -357,10 +371,10 @@ class VoteEndDate extends CardDate {
     return classes;
   }
   showDate() {
-    return this.date.get().format('L LT');
+    return format(this.date.get(), 'L') + ' ' + format(this.date.get(), 'HH:mm');
   }
   showTitle() {
-    return `${TAPi18n.__('card-end-on')} ${this.date.get().format('LLLL')}`;
+    return `${TAPi18n.__('card-end-on')} ${this.date.get().toLocaleString()}`;
   }
 
   events() {
@@ -376,7 +390,7 @@ class PokerEndDate extends CardDate {
     super.onCreated();
     const self = this;
     self.autorun(() => {
-      self.date.set(moment(self.data().getPokerEnd()));
+      self.date.set(new Date(self.data().getPokerEnd()));
     });
   }
   classes() {

+ 72 - 55
client/components/cards/cardDetails.js

@@ -1,7 +1,26 @@
 import { ReactiveCache } from '/imports/reactiveCache';
-import moment from 'moment/min/moment-with-locales';
 import { TAPi18n } from '/imports/i18n';
 import { DatePicker } from '/client/lib/datepicker';
+import { 
+  formatDateTime, 
+  formatDate, 
+  formatTime, 
+  getISOWeek, 
+  isValidDate, 
+  isBefore, 
+  isAfter, 
+  isSame, 
+  add, 
+  subtract, 
+  startOf, 
+  endOf, 
+  format, 
+  parseDate, 
+  now, 
+  createDate, 
+  fromNow, 
+  calendar 
+} from '/imports/lib/dateUtils';
 import Cards from '/models/cards';
 import Boards from '/models/boards';
 import Checklists from '/models/checklists';
@@ -455,7 +474,7 @@ BlazeComponent.extendComponent({
         'click .js-poker-finish'(e) {
           if ($(e.target).hasClass('js-poker-finish')) {
             e.preventDefault();
-            const now = moment().format('YYYY-MM-DD HH:mm');
+            const now = formatDateTime(new Date());
             this.data().setPokerEnd(now);
           }
         },
@@ -1106,8 +1125,8 @@ BlazeComponent.extendComponent({
 // editVoteEndDatePopup
 (class extends DatePicker {
   onCreated() {
-    super.onCreated(moment().format('YYYY-MM-DD HH:mm'));
-    this.data().getVoteEnd() && this.date.set(moment(this.data().getVoteEnd()));
+    super.onCreated(formatDateTime(now()));
+    this.data().getVoteEnd() && this.date.set(new Date(this.data().getVoteEnd()));
   }
   events() {
     return [
@@ -1118,12 +1137,12 @@ BlazeComponent.extendComponent({
           // if no time was given, init with 12:00
           const time =
             evt.target.time.value ||
-            moment(new Date().setHours(12, 0, 0)).format('LT');
+            formatTime(new Date().setHours(12, 0, 0));
 
           const dateString = `${evt.target.date.value} ${time}`;
 
           /*
-          const newDate = moment(dateString, 'L LT', true);
+          const newDate = parseDate(dateString, ['L LT'], true);
           if (newDate.isValid()) {
             // if active vote -  store it
             if (this.currentData().getVoteQuestion()) {
@@ -1137,28 +1156,27 @@ BlazeComponent.extendComponent({
 
           */
 
-          // Try to parse different date formats of all languages.
-          // This code is same for vote and planning poker.
-          const usaDate = moment(dateString, 'L LT', true);
-          const euroAmDate = moment(dateString, 'DD.MM.YYYY LT', true);
-          const euro24hDate = moment(dateString, 'DD.MM.YYYY HH.mm', true);
-          const eurodotDate = moment(dateString, 'DD.MM.YYYY HH:mm', true);
-          const minusDate = moment(dateString, 'YYYY-MM-DD HH:mm', true);
-          const slashDate = moment(dateString, 'DD/MM/YYYY HH.mm', true);
-          const dotDate = moment(dateString, 'DD/MM/YYYY HH:mm', true);
-          const brezhonegDate = moment(dateString, 'DD/MM/YYYY h[e]mm A', true);
-          const hrvatskiDate = moment(dateString, 'DD. MM. YYYY H:mm', true);
-          const latviaDate = moment(dateString, 'YYYY.MM.DD. H:mm', true);
-          const nederlandsDate = moment(dateString, 'DD-MM-YYYY HH:mm', true);
-          // greekDate does not work: el Greek Ελληνικά ,
-          // it has date format DD/MM/YYYY h:mm MM like 20/06/2021 11:15 MM
-          // where MM is maybe some text like AM/PM ?
-          // Also some other languages that have non-ascii characters in dates
-          // do not work.
-          const greekDate = moment(dateString, 'DD/MM/YYYY h:mm A', true);
-          const macedonianDate = moment(dateString, 'D.MM.YYYY H:mm', true);
-
-          if (usaDate.isValid()) {
+          // Try to parse different date formats using native Date parsing
+          const formats = [
+            'YYYY-MM-DD HH:mm',
+            'MM/DD/YYYY HH:mm',
+            'DD.MM.YYYY HH:mm',
+            'DD/MM/YYYY HH:mm',
+            'DD-MM-YYYY HH:mm'
+          ];
+          
+          let parsedDate = null;
+          for (const format of formats) {
+            parsedDate = parseDate(dateString, [format], true);
+            if (parsedDate) break;
+          }
+          
+          // Fallback to native Date parsing
+          if (!parsedDate) {
+            parsedDate = new Date(dateString);
+          }
+
+          if (isValidDate(parsedDate)) {
             // if active poker -  store it
             if (this.currentData().getPokerQuestion()) {
               this._storeDate(usaDate.toDate());
@@ -1337,9 +1355,9 @@ BlazeComponent.extendComponent({
 // editPokerEndDatePopup
 (class extends DatePicker {
   onCreated() {
-    super.onCreated(moment().format('YYYY-MM-DD HH:mm'));
+    super.onCreated(formatDateTime(now()));
     this.data().getPokerEnd() &&
-      this.date.set(moment(this.data().getPokerEnd()));
+      this.date.set(new Date(this.data().getPokerEnd()));
   }
 
   /*
@@ -1357,7 +1375,7 @@ BlazeComponent.extendComponent({
     return moment.localeData().longDateFormat('LT');
   }
 
-  const newDate = moment(dateString, dateformat() + ' ' + timeformat(), true);
+  const newDate = parseDate(dateString, [dateformat() + ' ' + timeformat()], true);
   */
 
   events() {
@@ -1369,7 +1387,7 @@ BlazeComponent.extendComponent({
           // if no time was given, init with 12:00
           const time =
             evt.target.time.value ||
-            moment(new Date().setHours(12, 0, 0)).format('LT');
+            formatTime(new Date().setHours(12, 0, 0));
 
           const dateString = `${evt.target.date.value} ${time}`;
 
@@ -1380,7 +1398,7 @@ BlazeComponent.extendComponent({
           Maybe client/components/lib/datepicker.jade could have hidden input field for
           datepicker format that could be used to detect date format?
 
-          const newDate = moment(dateString, dateformat() + ' ' + timeformat(), true);
+          const newDate = parseDate(dateString, [dateformat() + ' ' + timeformat()], true);
 
           if (newDate.isValid()) {
             // if active poker -  store it
@@ -1393,28 +1411,27 @@ BlazeComponent.extendComponent({
             }
           */
 
-          // Try to parse different date formats of all languages.
-          // This code is same for vote and planning poker.
-          const usaDate = moment(dateString, 'L LT', true);
-          const euroAmDate = moment(dateString, 'DD.MM.YYYY LT', true);
-          const euro24hDate = moment(dateString, 'DD.MM.YYYY HH.mm', true);
-          const eurodotDate = moment(dateString, 'DD.MM.YYYY HH:mm', true);
-          const minusDate = moment(dateString, 'YYYY-MM-DD HH:mm', true);
-          const slashDate = moment(dateString, 'DD/MM/YYYY HH.mm', true);
-          const dotDate = moment(dateString, 'DD/MM/YYYY HH:mm', true);
-          const brezhonegDate = moment(dateString, 'DD/MM/YYYY h[e]mm A', true);
-          const hrvatskiDate = moment(dateString, 'DD. MM. YYYY H:mm', true);
-          const latviaDate = moment(dateString, 'YYYY.MM.DD. H:mm', true);
-          const nederlandsDate = moment(dateString, 'DD-MM-YYYY HH:mm', true);
-          // greekDate does not work: el Greek Ελληνικά ,
-          // it has date format DD/MM/YYYY h:mm MM like 20/06/2021 11:15 MM
-          // where MM is maybe some text like AM/PM ?
-          // Also some other languages that have non-ascii characters in dates
-          // do not work.
-          const greekDate = moment(dateString, 'DD/MM/YYYY h:mm A', true);
-          const macedonianDate = moment(dateString, 'D.MM.YYYY H:mm', true);
-
-          if (usaDate.isValid()) {
+          // Try to parse different date formats using native Date parsing
+          const formats = [
+            'YYYY-MM-DD HH:mm',
+            'MM/DD/YYYY HH:mm',
+            'DD.MM.YYYY HH:mm',
+            'DD/MM/YYYY HH:mm',
+            'DD-MM-YYYY HH:mm'
+          ];
+          
+          let parsedDate = null;
+          for (const format of formats) {
+            parsedDate = parseDate(dateString, [format], true);
+            if (parsedDate) break;
+          }
+          
+          // Fallback to native Date parsing
+          if (!parsedDate) {
+            parsedDate = new Date(dateString);
+          }
+
+          if (isValidDate(parsedDate)) {
             // if active poker -  store it
             if (this.currentData().getPokerQuestion()) {
               this._storeDate(usaDate.toDate());

+ 21 - 2
client/config/blazeHelpers.js

@@ -1,7 +1,26 @@
 import { ReactiveCache } from '/imports/reactiveCache';
 import { Blaze } from 'meteor/blaze';
 import { Session } from 'meteor/session';
-import moment from 'moment/min/moment-with-locales';
+import { 
+  formatDateTime, 
+  formatDate, 
+  formatTime, 
+  getISOWeek, 
+  isValidDate, 
+  isBefore, 
+  isAfter, 
+  isSame, 
+  add, 
+  subtract, 
+  startOf, 
+  endOf, 
+  format, 
+  parseDate, 
+  now, 
+  createDate, 
+  fromNow, 
+  calendar 
+} from '/imports/lib/dateUtils';
 
 Blaze.registerHelper('currentBoard', () => {
   const ret = Utils.getCurrentBoard();
@@ -47,7 +66,7 @@ Blaze.registerHelper('isTouchScreenOrShowDesktopDragHandles', () =>
 Blaze.registerHelper('moment', (...args) => {
   args.pop(); // hash
   const [date, format] = args;
-  return moment(date).format(format ?? 'LLLL');
+  return format(new Date(date), format ?? 'LLLL');
 });
 
 Blaze.registerHelper('canModifyCard', () =>

+ 40 - 23
client/lib/datepicker.js

@@ -1,12 +1,29 @@
 import { ReactiveCache } from '/imports/reactiveCache';
 import { TAPi18n } from '/imports/i18n';
-import moment from 'moment/min/moment-with-locales';
+import { 
+  formatDateTime, 
+  formatDate, 
+  formatTime, 
+  getISOWeek, 
+  isValidDate, 
+  isBefore, 
+  isAfter, 
+  isSame, 
+  add, 
+  subtract, 
+  startOf, 
+  endOf, 
+  format, 
+  parseDate, 
+  now, 
+  createDate, 
+  fromNow, 
+  calendar 
+} from '/imports/lib/dateUtils';
 
-// Helper function to replace HH with H for 24 hours format, because H allows also single-digit hours
+// Helper function to get time format for 24 hours
 function adjustedTimeFormat() {
-  return moment
-    .localeData()
-    .longDateFormat('LT');
+  return 'HH:mm';
 }
 
 //   .replace(/HH/i, 'H');
@@ -19,7 +36,7 @@ export class DatePicker extends BlazeComponent {
   onCreated(defaultTime = '1970-01-01 08:00:00') {
     this.error = new ReactiveVar('');
     this.card = this.data();
-    this.date = new ReactiveVar(moment.invalid());
+    this.date = new ReactiveVar(new Date('invalid'));
     this.defaultTime = defaultTime;
   }
 
@@ -34,35 +51,35 @@ export class DatePicker extends BlazeComponent {
 
   onRendered() {
     // Set initial values for native HTML inputs
-    if (this.date.get().isValid()) {
+    if (isValidDate(this.date.get())) {
       const dateInput = this.find('#date');
       const timeInput = this.find('#time');
       
       if (dateInput) {
-        dateInput.value = this.date.get().format('YYYY-MM-DD');
+        dateInput.value = formatDate(this.date.get());
       }
       if (timeInput && !timeInput.value && this.defaultTime) {
-        const defaultMoment = moment(this.defaultTime);
-        timeInput.value = defaultMoment.format('HH:mm');
-      } else if (timeInput && this.date.get().isValid()) {
-        timeInput.value = this.date.get().format('HH:mm');
+        const defaultDate = new Date(this.defaultTime);
+        timeInput.value = formatTime(defaultDate);
+      } else if (timeInput && isValidDate(this.date.get())) {
+        timeInput.value = formatTime(this.date.get());
       }
     }
   }
 
   showDate() {
-    if (this.date.get().isValid()) return this.date.get().format('YYYY-MM-DD');
+    if (isValidDate(this.date.get())) return formatDate(this.date.get());
     return '';
   }
   showTime() {
-    if (this.date.get().isValid()) return this.date.get().format('HH:mm');
+    if (isValidDate(this.date.get())) return formatTime(this.date.get());
     return '';
   }
   dateFormat() {
-    return moment.localeData().longDateFormat('L');
+    return 'L';
   }
   timeFormat() {
-    return moment.localeData().longDateFormat('LT');
+    return 'LT';
   }
 
   events() {
@@ -72,8 +89,8 @@ export class DatePicker extends BlazeComponent {
           // Native HTML date input validation
           const dateValue = this.find('#date').value;
           if (dateValue) {
-            const dateMoment = moment(dateValue, 'YYYY-MM-DD', true);
-            if (dateMoment.isValid()) {
+            const dateObj = new Date(dateValue);
+            if (isValidDate(dateObj)) {
               this.error.set('');
             } else {
               this.error.set('invalid-date');
@@ -84,8 +101,8 @@ export class DatePicker extends BlazeComponent {
           // Native HTML time input validation
           const timeValue = this.find('#time').value;
           if (timeValue) {
-            const timeMoment = moment(timeValue, 'HH:mm', true);
-            if (timeMoment.isValid()) {
+            const timeObj = new Date(`1970-01-01T${timeValue}`);
+            if (isValidDate(timeObj)) {
               this.error.set('');
             } else {
               this.error.set('invalid-time');
@@ -104,14 +121,14 @@ export class DatePicker extends BlazeComponent {
             return;
           }
 
-          const newCompleteDate = moment(`${dateValue} ${timeValue}`, 'YYYY-MM-DD HH:mm', true);
+          const newCompleteDate = new Date(`${dateValue}T${timeValue}`);
           
-          if (!newCompleteDate.isValid()) {
+          if (!isValidDate(newCompleteDate)) {
             this.error.set('invalid');
             return;
           }
 
-          this._storeDate(newCompleteDate.toDate());
+          this._storeDate(newCompleteDate);
           Popup.back();
         },
         'click .js-delete-date'(evt) {

+ 31 - 29
client/lib/filter.js

@@ -1,5 +1,24 @@
 import { ReactiveCache } from '/imports/reactiveCache';
-import moment from 'moment/min/moment-with-locales';
+import { 
+  formatDateTime, 
+  formatDate, 
+  formatTime, 
+  getISOWeek, 
+  isValidDate, 
+  isBefore, 
+  isAfter, 
+  isSame, 
+  add, 
+  subtract, 
+  startOf, 
+  endOf, 
+  format, 
+  parseDate, 
+  now, 
+  createDate, 
+  fromNow, 
+  calendar 
+} from '/imports/lib/dateUtils';
 
 // Filtered view manager
 // We define local filter objects for each different type of field (SetFilter,
@@ -30,7 +49,7 @@ class DateFilter {
       this.reset();
       return;
     }
-    this._filter = { $lte: moment().toDate() };
+    this._filter = { $lte: now() };
     this._updateState('past');
   }
 
@@ -72,13 +91,8 @@ class DateFilter {
       return;
     }
 
-    var startDay = moment()
-        .startOf('day')
-        .toDate(),
-      endDay = moment()
-        .endOf('day')
-        .add(offset, 'day')
-        .toDate();
+    var startDay = startOf(now(), 'day'),
+      endDay = endOf(add(now(), offset, 'day'), 'day');
 
     if (offset >= 0) {
       this._filter = { $gte: startDay, $lte: endDay };
@@ -112,33 +126,21 @@ class DateFilter {
     const weekStartDay = currentUser ? currentUser.getStartDayOfWeek() : 1;
 
     if (week === 'this') {
-      // Moments are mutable so they must be cloned before modification
-      var WeekStart = moment()
-        .startOf('day')
-        .startOf('week')
-        .add(weekStartDay, 'days');
-      var WeekEnd = WeekStart
-        .clone()
-        .add(6, 'days')
-        .endOf('day');
+      // Create week start and end dates
+      var WeekStart = startOf(add(startOf(now(), 'week'), weekStartDay, 'days'), 'day');
+      var WeekEnd = endOf(add(WeekStart, 6, 'days'), 'day');
 
       this._updateState('thisweek');
     } else if (week === 'next') {
-      // Moments are mutable so they must be cloned before modification
-      var WeekStart = moment()
-        .startOf('day')
-        .startOf('week')
-        .add(weekStartDay + 7, 'days');
-      var WeekEnd = WeekStart
-        .clone()
-        .add(6, 'days')
-        .endOf('day');
+      // Create next week start and end dates
+      var WeekStart = startOf(add(startOf(now(), 'week'), weekStartDay + 7, 'days'), 'day');
+      var WeekEnd = endOf(add(WeekStart, 6, 'days'), 'day');
 
      this._updateState('nextweek');
     }
 
-    var startDate = WeekStart.toDate();
-    var endDate = WeekEnd.toDate();
+    var startDate = WeekStart;
+    var endDate = WeekEnd;
 
     if (offset >= 0) {
       this._filter = { $gte: startDate, $lte: endDate };

+ 39 - 23
config/query-classes.js

@@ -1,5 +1,24 @@
-import moment from 'moment/min/moment-with-locales';
 import { TAPi18n } from '/imports/i18n';
+import { 
+  formatDateTime, 
+  formatDate, 
+  formatTime, 
+  getISOWeek, 
+  isValidDate, 
+  isBefore, 
+  isAfter, 
+  isSame, 
+  add, 
+  subtract, 
+  startOf, 
+  endOf, 
+  format, 
+  parseDate, 
+  now, 
+  createDate, 
+  fromNow, 
+  calendar 
+} from '/imports/lib/dateUtils';
 import {
   OPERATOR_ASSIGNEE,
   OPERATOR_BOARD,
@@ -421,43 +440,44 @@ export class Query {
                 switch (duration) {
                   case PREDICATE_WEEK:
                     // eslint-disable-next-line no-case-declarations
-                    const week = moment().week();
+                    const week = getISOWeek(now());
                     if (week === 52) {
-                      date = moment(1, 'W');
-                      date.set('year', date.year() + 1);
+                      date = new Date(now().getFullYear() + 1, 0, 1); // January 1st of next year
                     } else {
-                      date = moment(week + 1, 'W');
+                      // Calculate the date for the next week
+                      const currentDate = now();
+                      const daysToAdd = (week + 1) * 7 - (currentDate.getDay() + 6) % 7;
+                      date = add(currentDate, daysToAdd, 'days');
                     }
                     break;
                   case PREDICATE_MONTH:
                     // eslint-disable-next-line no-case-declarations
-                    const month = moment().month();
-                    // .month() is zero indexed
+                    const month = now().getMonth();
+                    // .getMonth() is zero indexed
                     if (month === 11) {
-                      date = moment(1, 'M');
-                      date.set('year', date.year() + 1);
+                      date = new Date(now().getFullYear() + 1, 0, 1); // January 1st of next year
                     } else {
-                      date = moment(month + 2, 'M');
+                      date = new Date(now().getFullYear(), month + 1, 1); // First day of next month
                     }
                     break;
                   case PREDICATE_QUARTER:
                     // eslint-disable-next-line no-case-declarations
-                    const quarter = moment().quarter();
+                    const quarter = Math.floor(now().getMonth() / 3) + 1;
                     if (quarter === 4) {
-                      date = moment(1, 'Q');
-                      date.set('year', date.year() + 1);
+                      date = new Date(now().getFullYear() + 1, 0, 1); // January 1st of next year
                     } else {
-                      date = moment(quarter + 1, 'Q');
+                      const nextQuarterMonth = quarter * 3; // 3, 6, 9 for quarters 2, 3, 4
+                      date = new Date(now().getFullYear(), nextQuarterMonth, 1); // First day of next quarter
                     }
                     break;
                   case PREDICATE_YEAR:
-                    date = moment(moment().year() + 1, 'YYYY');
+                    date = new Date(now().getFullYear() + 1, 0, 1); // January 1st of next year
                     break;
                 }
                 if (date) {
                   value = {
                     operator: '$lt',
-                    value: date.format('YYYY-MM-DD'),
+                    value: formatDate(date),
                   };
                 }
               } else if (
@@ -466,7 +486,7 @@ export class Query {
               ) {
                 value = {
                   operator: '$lt',
-                  value: moment().format('YYYY-MM-DD'),
+                  value: formatDate(now()),
                 };
               } else {
                 this.addError(OPERATOR_DUE, {
@@ -478,16 +498,12 @@ export class Query {
             } else if (operator === OPERATOR_DUE) {
               value = {
                 operator: '$lt',
-                value: moment(moment().format('YYYY-MM-DD'))
-                  .add(days + 1, duration ? duration : 'days')
-                  .format(),
+                value: formatDate(add(add(now(), 1, 'days'), days + 1, duration ? duration : 'days')),
               };
             } else {
               value = {
                 operator: '$gte',
-                value: moment(moment().format('YYYY-MM-DD'))
-                  .subtract(days, duration ? duration : 'days')
-                  .format(),
+                value: formatDate(subtract(now(), days, duration ? duration : 'days')),
               };
             }
           } else if (operator === OPERATOR_SORT) {

+ 0 - 1
imports/i18n/index.js

@@ -1,6 +1,5 @@
 import { TAPi18n } from './tap';
 import './accounts';
-import './moment';
 
 if (Meteor.isClient) {
   import './blaze';

+ 2 - 10
imports/i18n/moment.js

@@ -1,13 +1,5 @@
 import { Tracker } from 'meteor/tracker';
-import moment from 'moment/min/moment-with-locales';
 import { TAPi18n } from './tap';
 
-// Reactively adjust Moment.js translations
-Tracker.autorun(() => {
-  const language = TAPi18n.getLanguage();
-  try {
-    moment.locale(language);
-  } catch (err) {
-    console.error(err);
-  }
-});
+// Note: moment.js has been removed and replaced with native JavaScript Date functions
+// Locale handling is now done through native Date.toLocaleString() methods

+ 492 - 0
imports/lib/dateUtils.js

@@ -0,0 +1,492 @@
+/**
+ * Date utility functions to replace moment.js with native JavaScript Date
+ */
+
+/**
+ * Format a date to YYYY-MM-DD HH:mm format
+ * @param {Date|string} date - Date to format
+ * @returns {string} Formatted date string
+ */
+export function formatDateTime(date) {
+  const d = new Date(date);
+  if (isNaN(d.getTime())) return '';
+  
+  const year = d.getFullYear();
+  const month = String(d.getMonth() + 1).padStart(2, '0');
+  const day = String(d.getDate()).padStart(2, '0');
+  const hours = String(d.getHours()).padStart(2, '0');
+  const minutes = String(d.getMinutes()).padStart(2, '0');
+  
+  return `${year}-${month}-${day} ${hours}:${minutes}`;
+}
+
+/**
+ * Format a date to YYYY-MM-DD format
+ * @param {Date|string} date - Date to format
+ * @returns {string} Formatted date string
+ */
+export function formatDate(date) {
+  const d = new Date(date);
+  if (isNaN(d.getTime())) return '';
+  
+  const year = d.getFullYear();
+  const month = String(d.getMonth() + 1).padStart(2, '0');
+  const day = String(d.getDate()).padStart(2, '0');
+  
+  return `${year}-${month}-${day}`;
+}
+
+/**
+ * Format a time to HH:mm format
+ * @param {Date|string} date - Date to format
+ * @returns {string} Formatted time string
+ */
+export function formatTime(date) {
+  const d = new Date(date);
+  if (isNaN(d.getTime())) return '';
+  
+  const hours = String(d.getHours()).padStart(2, '0');
+  const minutes = String(d.getMinutes()).padStart(2, '0');
+  
+  return `${hours}:${minutes}`;
+}
+
+/**
+ * Get ISO week number (ISO 8601)
+ * @param {Date|string} date - Date to get week number for
+ * @returns {number} ISO week number
+ */
+export function getISOWeek(date) {
+  const d = new Date(date);
+  if (isNaN(d.getTime())) return 0;
+  
+  // Set to nearest Thursday: current date + 4 - current day number
+  // Make Sunday's day number 7
+  const target = new Date(d);
+  const dayNr = (d.getDay() + 6) % 7;
+  target.setDate(target.getDate() - dayNr + 3);
+  
+  // ISO week date weeks start on monday, so correct the day number
+  const firstThursday = target.valueOf();
+  target.setMonth(0, 1);
+  if (target.getDay() !== 4) {
+    target.setMonth(0, 1 + ((4 - target.getDay()) + 7) % 7);
+  }
+  
+  return 1 + Math.ceil((firstThursday - target) / 604800000); // 604800000 = 7 * 24 * 3600 * 1000
+}
+
+/**
+ * Check if a date is valid
+ * @param {Date|string} date - Date to check
+ * @returns {boolean} True if date is valid
+ */
+export function isValidDate(date) {
+  const d = new Date(date);
+  return !isNaN(d.getTime());
+}
+
+/**
+ * Check if a date is before another date
+ * @param {Date|string} date1 - First date
+ * @param {Date|string} date2 - Second date
+ * @param {string} unit - Unit of comparison ('minute', 'hour', 'day', etc.)
+ * @returns {boolean} True if date1 is before date2
+ */
+export function isBefore(date1, date2, unit = 'millisecond') {
+  const d1 = new Date(date1);
+  const d2 = new Date(date2);
+  
+  if (isNaN(d1.getTime()) || isNaN(d2.getTime())) return false;
+  
+  switch (unit) {
+    case 'year':
+      return d1.getFullYear() < d2.getFullYear();
+    case 'month':
+      return d1.getFullYear() < d2.getFullYear() || 
+             (d1.getFullYear() === d2.getFullYear() && d1.getMonth() < d2.getMonth());
+    case 'day':
+      return d1.getFullYear() < d2.getFullYear() || 
+             (d1.getFullYear() === d2.getFullYear() && d1.getMonth() < d2.getMonth()) ||
+             (d1.getFullYear() === d2.getFullYear() && d1.getMonth() === d2.getMonth() && d1.getDate() < d2.getDate());
+    case 'hour':
+      return d1.getTime() < d2.getTime() && Math.floor(d1.getTime() / (1000 * 60 * 60)) < Math.floor(d2.getTime() / (1000 * 60 * 60));
+    case 'minute':
+      return d1.getTime() < d2.getTime() && Math.floor(d1.getTime() / (1000 * 60)) < Math.floor(d2.getTime() / (1000 * 60));
+    default:
+      return d1.getTime() < d2.getTime();
+  }
+}
+
+/**
+ * Check if a date is after another date
+ * @param {Date|string} date1 - First date
+ * @param {Date|string} date2 - Second date
+ * @param {string} unit - Unit of comparison ('minute', 'hour', 'day', etc.)
+ * @returns {boolean} True if date1 is after date2
+ */
+export function isAfter(date1, date2, unit = 'millisecond') {
+  return isBefore(date2, date1, unit);
+}
+
+/**
+ * Check if a date is the same as another date
+ * @param {Date|string} date1 - First date
+ * @param {Date|string} date2 - Second date
+ * @param {string} unit - Unit of comparison ('minute', 'hour', 'day', etc.)
+ * @returns {boolean} True if dates are the same
+ */
+export function isSame(date1, date2, unit = 'millisecond') {
+  const d1 = new Date(date1);
+  const d2 = new Date(date2);
+  
+  if (isNaN(d1.getTime()) || isNaN(d2.getTime())) return false;
+  
+  switch (unit) {
+    case 'year':
+      return d1.getFullYear() === d2.getFullYear();
+    case 'month':
+      return d1.getFullYear() === d2.getFullYear() && d1.getMonth() === d2.getMonth();
+    case 'day':
+      return d1.getFullYear() === d2.getFullYear() && d1.getMonth() === d2.getMonth() && d1.getDate() === d2.getDate();
+    case 'hour':
+      return Math.floor(d1.getTime() / (1000 * 60 * 60)) === Math.floor(d2.getTime() / (1000 * 60 * 60));
+    case 'minute':
+      return Math.floor(d1.getTime() / (1000 * 60)) === Math.floor(d2.getTime() / (1000 * 60));
+    default:
+      return d1.getTime() === d2.getTime();
+  }
+}
+
+/**
+ * Add time to a date
+ * @param {Date|string} date - Base date
+ * @param {number} amount - Amount to add
+ * @param {string} unit - Unit ('years', 'months', 'days', 'hours', 'minutes', 'seconds')
+ * @returns {Date} New date
+ */
+export function add(date, amount, unit) {
+  const d = new Date(date);
+  if (isNaN(d.getTime())) return new Date();
+  
+  switch (unit) {
+    case 'years':
+      d.setFullYear(d.getFullYear() + amount);
+      break;
+    case 'months':
+      d.setMonth(d.getMonth() + amount);
+      break;
+    case 'days':
+      d.setDate(d.getDate() + amount);
+      break;
+    case 'hours':
+      d.setHours(d.getHours() + amount);
+      break;
+    case 'minutes':
+      d.setMinutes(d.getMinutes() + amount);
+      break;
+    case 'seconds':
+      d.setSeconds(d.getSeconds() + amount);
+      break;
+    default:
+      d.setTime(d.getTime() + amount);
+  }
+  
+  return d;
+}
+
+/**
+ * Subtract time from a date
+ * @param {Date|string} date - Base date
+ * @param {number} amount - Amount to subtract
+ * @param {string} unit - Unit ('years', 'months', 'days', 'hours', 'minutes', 'seconds')
+ * @returns {Date} New date
+ */
+export function subtract(date, amount, unit) {
+  return add(date, -amount, unit);
+}
+
+/**
+ * Get start of a time unit
+ * @param {Date|string} date - Base date
+ * @param {string} unit - Unit ('year', 'month', 'day', 'hour', 'minute', 'second')
+ * @returns {Date} Start of unit
+ */
+export function startOf(date, unit) {
+  const d = new Date(date);
+  if (isNaN(d.getTime())) return new Date();
+  
+  switch (unit) {
+    case 'year':
+      d.setMonth(0, 1);
+      d.setHours(0, 0, 0, 0);
+      break;
+    case 'month':
+      d.setDate(1);
+      d.setHours(0, 0, 0, 0);
+      break;
+    case 'day':
+      d.setHours(0, 0, 0, 0);
+      break;
+    case 'hour':
+      d.setMinutes(0, 0, 0);
+      break;
+    case 'minute':
+      d.setSeconds(0, 0);
+      break;
+    case 'second':
+      d.setMilliseconds(0);
+      break;
+  }
+  
+  return d;
+}
+
+/**
+ * Get end of a time unit
+ * @param {Date|string} date - Base date
+ * @param {string} unit - Unit ('year', 'month', 'day', 'hour', 'minute', 'second')
+ * @returns {Date} End of unit
+ */
+export function endOf(date, unit) {
+  const d = new Date(date);
+  if (isNaN(d.getTime())) return new Date();
+  
+  switch (unit) {
+    case 'year':
+      d.setMonth(11, 31);
+      d.setHours(23, 59, 59, 999);
+      break;
+    case 'month':
+      d.setMonth(d.getMonth() + 1, 0);
+      d.setHours(23, 59, 59, 999);
+      break;
+    case 'day':
+      d.setHours(23, 59, 59, 999);
+      break;
+    case 'hour':
+      d.setMinutes(59, 59, 999);
+      break;
+    case 'minute':
+      d.setSeconds(59, 999);
+      break;
+    case 'second':
+      d.setMilliseconds(999);
+      break;
+  }
+  
+  return d;
+}
+
+/**
+ * Format date for display with locale
+ * @param {Date|string} date - Date to format
+ * @param {string} format - Format string (simplified)
+ * @returns {string} Formatted date string
+ */
+export function format(date, format = 'L') {
+  const d = new Date(date);
+  if (isNaN(d.getTime())) return '';
+  
+  const year = d.getFullYear();
+  const month = String(d.getMonth() + 1).padStart(2, '0');
+  const day = String(d.getDate()).padStart(2, '0');
+  const hours = String(d.getHours()).padStart(2, '0');
+  const minutes = String(d.getMinutes()).padStart(2, '0');
+  const seconds = String(d.getSeconds()).padStart(2, '0');
+  
+  switch (format) {
+    case 'L':
+      return `${month}/${day}/${year}`;
+    case 'LL':
+      return d.toLocaleDateString();
+    case 'LLL':
+      return d.toLocaleString();
+    case 'llll':
+      return d.toLocaleString();
+    case 'YYYY-MM-DD':
+      return `${year}-${month}-${day}`;
+    case 'YYYY-MM-DD HH:mm':
+      return `${year}-${month}-${day} ${hours}:${minutes}`;
+    case 'HH:mm':
+      return `${hours}:${minutes}`;
+    default:
+      return d.toLocaleString();
+  }
+}
+
+/**
+ * Parse a date string with multiple formats
+ * @param {string} dateString - Date string to parse
+ * @param {string[]} formats - Array of format strings to try
+ * @param {boolean} strict - Whether to use strict parsing
+ * @returns {Date|null} Parsed date or null if invalid
+ */
+export function parseDate(dateString, formats = [], strict = true) {
+  if (!dateString) return null;
+  
+  // Try native Date parsing first
+  const nativeDate = new Date(dateString);
+  if (!isNaN(nativeDate.getTime())) {
+    return nativeDate;
+  }
+  
+  // Try common formats
+  const commonFormats = [
+    'YYYY-MM-DD HH:mm',
+    'YYYY-MM-DD',
+    'MM/DD/YYYY HH:mm',
+    'MM/DD/YYYY',
+    'DD.MM.YYYY HH:mm',
+    'DD.MM.YYYY',
+    'DD/MM/YYYY HH:mm',
+    'DD/MM/YYYY',
+    'DD-MM-YYYY HH:mm',
+    'DD-MM-YYYY'
+  ];
+  
+  const allFormats = [...formats, ...commonFormats];
+  
+  for (const format of allFormats) {
+    const parsed = parseWithFormat(dateString, format);
+    if (parsed && isValidDate(parsed)) {
+      return parsed;
+    }
+  }
+  
+  return null;
+}
+
+/**
+ * Parse date with specific format
+ * @param {string} dateString - Date string to parse
+ * @param {string} format - Format string
+ * @returns {Date|null} Parsed date or null
+ */
+function parseWithFormat(dateString, format) {
+  // Simple format parsing - can be extended as needed
+  const formatMap = {
+    'YYYY': '\\d{4}',
+    'MM': '\\d{2}',
+    'DD': '\\d{2}',
+    'HH': '\\d{2}',
+    'mm': '\\d{2}',
+    'ss': '\\d{2}'
+  };
+  
+  let regex = format;
+  for (const [key, value] of Object.entries(formatMap)) {
+    regex = regex.replace(new RegExp(key, 'g'), `(${value})`);
+  }
+  
+  const match = dateString.match(new RegExp(regex));
+  if (!match) return null;
+  
+  const groups = match.slice(1);
+  let year, month, day, hour = 0, minute = 0, second = 0;
+  
+  let groupIndex = 0;
+  for (let i = 0; i < format.length; i++) {
+    if (format[i] === 'Y' && format[i + 1] === 'Y' && format[i + 2] === 'Y' && format[i + 3] === 'Y') {
+      year = parseInt(groups[groupIndex++]);
+      i += 3;
+    } else if (format[i] === 'M' && format[i + 1] === 'M') {
+      month = parseInt(groups[groupIndex++]) - 1;
+      i += 1;
+    } else if (format[i] === 'D' && format[i + 1] === 'D') {
+      day = parseInt(groups[groupIndex++]);
+      i += 1;
+    } else if (format[i] === 'H' && format[i + 1] === 'H') {
+      hour = parseInt(groups[groupIndex++]);
+      i += 1;
+    } else if (format[i] === 'm' && format[i + 1] === 'm') {
+      minute = parseInt(groups[groupIndex++]);
+      i += 1;
+    } else if (format[i] === 's' && format[i + 1] === 's') {
+      second = parseInt(groups[groupIndex++]);
+      i += 1;
+    }
+  }
+  
+  if (year === undefined || month === undefined || day === undefined) {
+    return null;
+  }
+  
+  return new Date(year, month, day, hour, minute, second);
+}
+
+/**
+ * Get current date and time
+ * @returns {Date} Current date
+ */
+export function now() {
+  return new Date();
+}
+
+/**
+ * Create a date from components
+ * @param {number} year - Year
+ * @param {number} month - Month (0-based)
+ * @param {number} day - Day
+ * @param {number} hour - Hour (optional)
+ * @param {number} minute - Minute (optional)
+ * @param {number} second - Second (optional)
+ * @returns {Date} Created date
+ */
+export function createDate(year, month, day, hour = 0, minute = 0, second = 0) {
+  return new Date(year, month, day, hour, minute, second);
+}
+
+/**
+ * Get relative time string (e.g., "2 hours ago")
+ * @param {Date|string} date - Date to compare
+ * @param {Date|string} now - Current date (optional)
+ * @returns {string} Relative time string
+ */
+export function fromNow(date, now = new Date()) {
+  const d = new Date(date);
+  const n = new Date(now);
+  
+  if (isNaN(d.getTime()) || isNaN(n.getTime())) return '';
+  
+  const diffMs = n.getTime() - d.getTime();
+  const diffSeconds = Math.floor(diffMs / 1000);
+  const diffMinutes = Math.floor(diffSeconds / 60);
+  const diffHours = Math.floor(diffMinutes / 60);
+  const diffDays = Math.floor(diffHours / 24);
+  const diffWeeks = Math.floor(diffDays / 7);
+  const diffMonths = Math.floor(diffDays / 30);
+  const diffYears = Math.floor(diffDays / 365);
+  
+  if (diffSeconds < 60) return 'a few seconds ago';
+  if (diffMinutes < 60) return `${diffMinutes} minute${diffMinutes !== 1 ? 's' : ''} ago`;
+  if (diffHours < 24) return `${diffHours} hour${diffHours !== 1 ? 's' : ''} ago`;
+  if (diffDays < 7) return `${diffDays} day${diffDays !== 1 ? 's' : ''} ago`;
+  if (diffWeeks < 4) return `${diffWeeks} week${diffWeeks !== 1 ? 's' : ''} ago`;
+  if (diffMonths < 12) return `${diffMonths} month${diffMonths !== 1 ? 's' : ''} ago`;
+  return `${diffYears} year${diffYears !== 1 ? 's' : ''} ago`;
+}
+
+/**
+ * Get calendar format (e.g., "Today", "Yesterday", "Tomorrow")
+ * @param {Date|string} date - Date to format
+ * @param {Date|string} now - Current date (optional)
+ * @returns {string} Calendar format string
+ */
+export function calendar(date, now = new Date()) {
+  const d = new Date(date);
+  const n = new Date(now);
+  
+  if (isNaN(d.getTime()) || isNaN(n.getTime())) return format(d);
+  
+  const diffMs = d.getTime() - n.getTime();
+  const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
+  
+  if (diffDays === 0) return 'Today';
+  if (diffDays === 1) return 'Tomorrow';
+  if (diffDays === -1) return 'Yesterday';
+  if (diffDays > 1 && diffDays < 7) return `In ${diffDays} days`;
+  if (diffDays < -1 && diffDays > -7) return `${Math.abs(diffDays)} days ago`;
+  
+  return format(d, 'L');
+}

+ 25 - 8
models/cards.js

@@ -1,5 +1,24 @@
 import { ReactiveCache, ReactiveMiniMongoIndex } from '/imports/reactiveCache';
-import moment from 'moment/min/moment-with-locales';
+import { 
+  formatDateTime, 
+  formatDate, 
+  formatTime, 
+  getISOWeek, 
+  isValidDate, 
+  isBefore, 
+  isAfter, 
+  isSame, 
+  add, 
+  subtract, 
+  startOf, 
+  endOf, 
+  format, 
+  parseDate, 
+  now, 
+  createDate, 
+  fromNow, 
+  calendar 
+} from '/imports/lib/dateUtils';
 import {
   ALLOWED_COLORS,
   TYPE_CARD,
@@ -1492,8 +1511,8 @@ Cards.helpers({
   expiredVote() {
     let end = this.getVoteEnd();
     if (end) {
-      end = moment(end);
-      return end.isBefore(new Date());
+      end = new Date(end);
+      return isBefore(end, new Date());
     }
     return false;
   },
@@ -1586,8 +1605,8 @@ Cards.helpers({
   expiredPoker() {
     let end = this.getPokerEnd();
     if (end) {
-      end = moment(end);
-      return end.isBefore(new Date());
+      end = new Date(end);
+      return isBefore(end, new Date());
     }
     return false;
   },
@@ -3201,9 +3220,7 @@ if (Meteor.isServer) {
         // change list modifiedAt, when user modified the key values in
         // timingaction array, if it's endAt, put the modifiedAt of list
         // back to one year ago for sorting purpose
-        const modifiedAt = moment()
-          .subtract(1, 'year')
-          .toISOString();
+        const modifiedAt = add(now(), -1, 'year').toISOString();
         const boardId = list.boardId;
         Lists.direct.update(
           {

+ 27 - 8
models/exporter.js

@@ -1,7 +1,26 @@
 import { ReactiveCache } from '/imports/reactiveCache';
-import moment from 'moment/min/moment-with-locales';
 const Papa = require('papaparse');
 import { TAPi18n } from '/imports/i18n';
+import { 
+  formatDateTime, 
+  formatDate, 
+  formatTime, 
+  getISOWeek, 
+  isValidDate, 
+  isBefore, 
+  isAfter, 
+  isSame, 
+  add, 
+  subtract, 
+  startOf, 
+  endOf, 
+  format, 
+  parseDate, 
+  now, 
+  createDate, 
+  fromNow, 
+  calendar 
+} from '/imports/lib/dateUtils';
 
 //const stringify = require('csv-stringify');
 
@@ -302,15 +321,15 @@ export class Exporter {
         labels = `${labels + label.name}-${label.color} `;
       });
       currentRow.push(labels.trim());
-      currentRow.push(card.startAt ? moment(card.startAt).format() : ' ');
-      currentRow.push(card.dueAt ? moment(card.dueAt).format() : ' ');
-      currentRow.push(card.endAt ? moment(card.endAt).format() : ' ');
+      currentRow.push(card.startAt ? new Date(card.startAt).toISOString() : ' ');
+      currentRow.push(card.dueAt ? new Date(card.dueAt).toISOString() : ' ');
+      currentRow.push(card.endAt ? new Date(card.endAt).toISOString() : ' ');
       currentRow.push(card.isOvertime ? 'true' : 'false');
       currentRow.push(card.spentTime);
-      currentRow.push(card.createdAt ? moment(card.createdAt).format() : ' ');
-      currentRow.push(card.modifiedAt ? moment(card.modifiedAt).format() : ' ');
+      currentRow.push(card.createdAt ? new Date(card.createdAt).toISOString() : ' ');
+      currentRow.push(card.modifiedAt ? new Date(card.modifiedAt).toISOString() : ' ');
       currentRow.push(
-        card.dateLastActivity ? moment(card.dateLastActivity).format() : ' ',
+        card.dateLastActivity ? new Date(card.dateLastActivity).toISOString() : ' ',
       );
       if (card.vote && card.vote.question !== '') {
         let positiveVoters = '';
@@ -343,7 +362,7 @@ export class Exporter {
         if (field.value !== null) {
           if (customFieldMap[field._id].type === 'date') {
             customFieldValuesToPush[customFieldMap[field._id].position] =
-              moment(field.value).format();
+              new Date(field.value).toISOString();
           } else if (customFieldMap[field._id].type === 'dropdown') {
             const dropdownOptions = result.customFields.find(
               ({ _id }) => _id === field._id,

+ 22 - 2
models/server/ExporterCardPDF.js

@@ -1,6 +1,26 @@
 import { ReactiveCache } from '/imports/reactiveCache';
 // exporter maybe is broken since Gridfs introduced, add fs and path
 import { createWorkbook } from './createWorkbook';
+import { 
+  formatDateTime, 
+  formatDate, 
+  formatTime, 
+  getISOWeek, 
+  isValidDate, 
+  isBefore, 
+  isAfter, 
+  isSame, 
+  add, 
+  subtract, 
+  startOf, 
+  endOf, 
+  format, 
+  parseDate, 
+  now, 
+  createDate, 
+  fromNow, 
+  calendar 
+} from '/imports/lib/dateUtils';
 
 class ExporterCardPDF {
   constructor(boardId) {
@@ -353,8 +373,8 @@ class ExporterCardPDF {
         //add data +8 hours
         function addTZhours(jdate) {
           const curdate = new Date(jdate);
-          const checkCorrectDate = moment(curdate);
-          if (checkCorrectDate.isValid()) {
+          const checkCorrectDate = new Date(curdate);
+          if (isValidDate(checkCorrectDate)) {
             return curdate;
           } else {
             return ' ';

+ 22 - 3
models/server/ExporterExcel.js

@@ -1,7 +1,26 @@
 import { ReactiveCache } from '/imports/reactiveCache';
-import moment from 'moment/min/moment-with-locales';
 import { TAPi18n } from '/imports/i18n';
 import { createWorkbook } from './createWorkbook';
+import { 
+  formatDateTime, 
+  formatDate, 
+  formatTime, 
+  getISOWeek, 
+  isValidDate, 
+  isBefore, 
+  isAfter, 
+  isSame, 
+  add, 
+  subtract, 
+  startOf, 
+  endOf, 
+  format, 
+  parseDate, 
+  now, 
+  createDate, 
+  fromNow, 
+  calendar 
+} from '/imports/lib/dateUtils';
 
 // exporter maybe is broken since Gridfs introduced, add fs and path
 
@@ -379,8 +398,8 @@ class ExporterExcel {
     function addTZhours(jdate) {
       if (!jdate) { return ' '; }
       const curdate = new Date(jdate);
-      const checkCorrectDate = moment(curdate);
-      if (checkCorrectDate.isValid()) {
+      const checkCorrectDate = new Date(curdate);
+      if (isValidDate(checkCorrectDate)) {
         return curdate;
       } else {
         return ' ';

+ 21 - 2
models/trelloCreator.js

@@ -1,10 +1,29 @@
 import { ReactiveCache } from '/imports/reactiveCache';
-import moment from 'moment/min/moment-with-locales';
 import { TAPi18n } from '/imports/i18n';
+import { 
+  formatDateTime, 
+  formatDate, 
+  formatTime, 
+  getISOWeek, 
+  isValidDate, 
+  isBefore, 
+  isAfter, 
+  isSame, 
+  add, 
+  subtract, 
+  startOf, 
+  endOf, 
+  format, 
+  parseDate, 
+  now, 
+  createDate, 
+  fromNow, 
+  calendar 
+} from '/imports/lib/dateUtils';
 
 const DateString = Match.Where(function(dateAsString) {
   check(dateAsString, String);
-  return moment(dateAsString, moment.ISO_8601).isValid();
+  return isValidDate(new Date(dateAsString));
 });
 
 export class TrelloCreator {

+ 21 - 2
models/wekanCreator.js

@@ -1,9 +1,28 @@
 import { ReactiveCache } from '/imports/reactiveCache';
-import moment from 'moment/min/moment-with-locales';
+import { 
+  formatDateTime, 
+  formatDate, 
+  formatTime, 
+  getISOWeek, 
+  isValidDate, 
+  isBefore, 
+  isAfter, 
+  isSame, 
+  add, 
+  subtract, 
+  startOf, 
+  endOf, 
+  format, 
+  parseDate, 
+  now, 
+  createDate, 
+  fromNow, 
+  calendar 
+} from '/imports/lib/dateUtils';
 
 const DateString = Match.Where(function(dateAsString) {
   check(dateAsString, String);
-  return moment(dateAsString, moment.ISO_8601).isValid();
+  return isValidDate(new Date(dateAsString));
 });
 
 export class WekanCreator {

+ 0 - 1
package.json

@@ -47,7 +47,6 @@
     "markdown-it-mathjax3": "^4.3.2",
     "meteor-accounts-t9n": "^2.6.0",
     "meteor-node-stubs": "^1.2.24",
-    "moment": "^2.30.1",
     "os": "^0.1.2",
     "papaparse": "^5.5.3",
     "pretty-ms": "^7.0.1",

+ 21 - 4
server/publications/cards.js

@@ -1,7 +1,26 @@
 import { ReactiveCache } from '/imports/reactiveCache';
-import moment from 'moment/min/moment-with-locales';
 import escapeForRegex from 'escape-string-regexp';
 import Users from '../../models/users';
+import { 
+  formatDateTime, 
+  formatDate, 
+  formatTime, 
+  getISOWeek, 
+  isValidDate, 
+  isBefore, 
+  isAfter, 
+  isSame, 
+  add, 
+  subtract, 
+  startOf, 
+  endOf, 
+  format, 
+  parseDate, 
+  now, 
+  createDate, 
+  fromNow, 
+  calendar 
+} from '/imports/lib/dateUtils';
 import Boards from '../../models/boards';
 import Lists from '../../models/lists';
 import Swimlanes from '../../models/swimlanes';
@@ -730,9 +749,7 @@ function findCards(sessionId, query) {
     userId,
     modifiedAt: {
       $lt: new Date(
-        moment()
-          .subtract(1, 'day')
-          .format(),
+        subtract(now(), 1, 'day').toISOString(),
       ),
     },
   });