diff --git a/resources/js/bootstrap/components.js b/resources/js/bootstrap/components.js index 8aec2d92c0e..56d5043f072 100644 --- a/resources/js/bootstrap/components.js +++ b/resources/js/bootstrap/components.js @@ -37,6 +37,7 @@ import AssetContainerList from '../components/AssetContainerList.vue'; import AddonList from '../components/AddonList.vue'; import AddonDetails from '../components/AddonDetails.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..e52e38848c2 100644 --- a/resources/js/components/DateFormatter.js +++ b/resources/js/components/DateFormatter.js @@ -35,12 +35,66 @@ export default class DateFormatter { toString() { try { + if (this.#options.relative) return this.#formatRelative(); + return Intl.DateTimeFormat(this.locale, this.#options).format(this.#date); } catch (e) { return 'Invalid Date'; } } + #formatRelative() { + let specificity = this.#options.relative; + specificity = !specificity || specificity === true ? 'year' : specificity; + + 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 rtf = new Intl.RelativeTimeFormat(this.locale, { numeric: 'auto' }); + + // Always show seconds if less than a minute + if (seconds < 60) return rtf.format(-Math.sign(diff) * seconds, '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'); + } + + return new DateFormatter(this.#date, this.#options.fallback || 'datetime').toString(); + } + static format(date, options) { return new DateFormatter(date, options).toString(); } diff --git a/resources/js/components/forms/FormWidget.vue b/resources/js/components/forms/FormWidget.vue new file mode 100644 index 00000000000..706cf2394ba --- /dev/null +++ b/resources/js/components/forms/FormWidget.vue @@ -0,0 +1,82 @@ + + + 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({