From 16a248e06ea25b2202a9d78032f2d12b0f9ba53c Mon Sep 17 00:00:00 2001 From: Jack McDade Date: Wed, 16 Apr 2025 10:52:16 -0400 Subject: [PATCH 1/7] Convert form widget to Vue and add a relative preset to the DateFormatter --- resources/js/bootstrap/components.js | 4 +- resources/js/components/DateFormatter.js | 83 ++++++++++++++++++-- resources/js/components/forms/FormWidget.vue | 72 +++++++++++++++++ resources/js/components/ui/Widget.vue | 2 +- resources/views/forms/widget.blade.php | 51 ++---------- src/Forms/Widget.php | 1 + 6 files changed, 161 insertions(+), 52 deletions(-) create mode 100644 resources/js/components/forms/FormWidget.vue diff --git a/resources/js/bootstrap/components.js b/resources/js/bootstrap/components.js index d5d7c3f77f3..56d5043f072 100644 --- a/resources/js/bootstrap/components.js +++ b/resources/js/bootstrap/components.js @@ -36,7 +36,8 @@ import TermListing from '../components/terms/Listing.vue'; import AssetContainerList from '../components/AssetContainerList.vue'; import AddonList from '../components/AddonList.vue'; import AddonDetails from '../components/AddonDetails.vue'; -import CollectionWidget from '../components/entries/Widget.vue'; +import CollectionWidget from '../components/entries/CollectionWidget.vue'; +import FormWidget from '../components/forms/FormWidget.vue'; import SvgIcon from '../components/SvgIcon.vue'; import FileIcon from '../components/FileIcon.vue'; import LoadingGraphic from '../components/LoadingGraphic.vue'; @@ -115,6 +116,7 @@ export default function registerGlobalComponents(app) { // Widgets app.component('collection-widget', CollectionWidget); + app.component('form-widget', FormWidget); // Reusable app.component('svg-icon', SvgIcon); diff --git a/resources/js/components/DateFormatter.js b/resources/js/components/DateFormatter.js index fc848de01c0..738f66ef2f0 100644 --- a/resources/js/components/DateFormatter.js +++ b/resources/js/components/DateFormatter.js @@ -2,13 +2,15 @@ export default class DateFormatter { #date; #options; #locale = navigator.language; - #presets = { + static presets = { datetime: { year: 'numeric', month: 'numeric', day: 'numeric', - hour: 'numeric', - minute: 'numeric', + hour: '2-digit', + minute: '2-digit', + hour12: false, + formatMatcher: 'basic', }, date: { year: 'numeric', @@ -18,6 +20,11 @@ export default class DateFormatter { time: { timeStyle: 'short', }, + relative: { + numeric: 'auto', + style: 'long', + specificity: 'day', + }, }; constructor(date, options) { @@ -35,12 +42,78 @@ export default class DateFormatter { toString() { try { + if (this.#options === 'relative' || (this.#options.specificity && this.#options.numeric === 'auto')) { + return this.#formatRelative(); + } return Intl.DateTimeFormat(this.locale, this.#options).format(this.#date); } catch (e) { return 'Invalid Date'; } } + #formatRelative() { + const now = new Date(); + const diff = now - this.#date; + const seconds = Math.abs(Math.floor(diff / 1000)); + const minutes = Math.abs(Math.floor(seconds / 60)); + const hours = Math.abs(Math.floor(minutes / 60)); + const days = Math.abs(Math.floor(hours / 24)); + const weeks = Math.abs(Math.floor(days / 7)); + const months = Math.abs(Math.floor(days / 30)); + const years = Math.abs(Math.floor(days / 365)); + + const specificity = this.#options.specificity || DateFormatter.presets.relative.specificity; + const rtf = new Intl.RelativeTimeFormat(this.locale, DateFormatter.presets.relative); + + // Always show seconds if less than a minute + if (seconds < 60) return rtf.format(-Math.sign(diff), 'second'); + + // Show minutes if less than an hour and specificity allows + if (minutes < 60 && ['minute', 'hour', 'day', 'week', 'month', 'year'].includes(specificity)) { + return rtf.format(-Math.sign(diff) * minutes, 'minute'); + } + + // Show hours if less than a day and specificity allows + if (hours < 24 && ['hour', 'day', 'week', 'month', 'year'].includes(specificity)) { + return rtf.format(-Math.sign(diff) * hours, 'hour'); + } + + // Show days if less than a week and specificity allows + if (days < 7 && ['day', 'week', 'month', 'year'].includes(specificity)) { + return rtf.format(-Math.sign(diff) * days, 'day'); + } + + // Show weeks if less than a month and specificity allows + if (weeks < 4 && ['week', 'month', 'year'].includes(specificity)) { + return rtf.format(-Math.sign(diff) * weeks, 'week'); + } + + // Show months if less than a year and specificity allows + if (months < 12 && ['month', 'year'].includes(specificity)) { + return rtf.format(-Math.sign(diff) * months, 'month'); + } + + // Show years if specificity allows + if (specificity === 'year') { + return rtf.format(-Math.sign(diff) * years, 'year'); + } + + // Custom format for datetime without comma + const date = new Intl.DateTimeFormat(this.locale, { + year: 'numeric', + month: 'numeric', + day: 'numeric', + }).format(this.#date); + + const time = new Intl.DateTimeFormat(this.locale, { + hour: '2-digit', + minute: '2-digit', + hour12: false, + }).format(this.#date); + + return `${date} ${time}`; + } + static format(date, options) { return new DateFormatter(date, options).toString(); } @@ -65,9 +138,9 @@ export default class DateFormatter { if (!options) options = 'datetime'; if (typeof options === 'string') { - if (!this.#presets[options]) throw new Error(`Invalid date format: ${options}`); + if (!DateFormatter.presets[options]) throw new Error(`Invalid date format: ${options}`); - return this.#presets[options]; + return DateFormatter.presets[options]; } return options; diff --git a/resources/js/components/forms/FormWidget.vue b/resources/js/components/forms/FormWidget.vue new file mode 100644 index 00000000000..b9606e52b97 --- /dev/null +++ b/resources/js/components/forms/FormWidget.vue @@ -0,0 +1,72 @@ + + + diff --git a/resources/js/components/ui/Widget.vue b/resources/js/components/ui/Widget.vue index 75ef231aa62..25a747fa5c2 100644 --- a/resources/js/components/ui/Widget.vue +++ b/resources/js/components/ui/Widget.vue @@ -10,7 +10,7 @@ defineProps({