@mundogamernetwork/shared-ui 1.8.22 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/utils/embargo.ts CHANGED
@@ -3,20 +3,49 @@
3
3
  *
4
4
  * An embargo is a day AND a time: "August 12" alone is not something a creator
5
5
  * can comply with — publishing at 09:00 or at 23:00 on that day are very
6
- * different acts. The API stores `embargo_date` as an instant (UTC) plus the
7
- * `embargo_timezone` the campaign creator confirmed it in.
6
+ * different acts.
8
7
  *
9
- * Both matter to the creator, so both are shown: the campaign's own wall-clock
10
- * (how the studio communicates the embargo everywhere else) and, when the
11
- * viewer is somewhere else, the same instant on their own clock — so nobody has
12
- * to do timezone arithmetic to know when they may publish.
8
+ * Both clocks matter to the creator, so both are shown: the campaign's own
9
+ * wall-clock (how the studio communicates the embargo everywhere else) and,
10
+ * when the viewer is somewhere else, the same instant on their own clock — so
11
+ * nobody has to do timezone arithmetic to know when they may publish.
12
+ *
13
+ * The *instant and the zone* come from the API's `embargo_display` block rather
14
+ * than from the raw `embargo_date`/`embargo_timezone` columns, so every client
15
+ * agrees on which moment is being described — the mobile app cannot name a zone
16
+ * itself (Dart ships no timezone database) and reads the server's rendering.
17
+ * `campaign_zone` there is already resolved: an unusable stored value has been
18
+ * collapsed to UTC server-side instead of reaching the formatter.
19
+ *
20
+ * The *formatting* stays here on purpose. Rendering the server's pre-built
21
+ * string would freeze the text in whatever locale the payload was fetched
22
+ * under, so switching language would leave a stale line until a refetch;
23
+ * Intl re-runs on every locale change.
13
24
  */
14
25
 
26
+ /** The API's `embargo_display` block. Null/absent means: no embargo. */
27
+ export interface EmbargoDisplay {
28
+ utc: string
29
+ campaign_zone: string
30
+ campaign_zone_label?: string | null
31
+ campaign_offset?: string | null
32
+ campaign_wall_clock?: string | null
33
+ locale?: string | null
34
+ }
35
+
36
+ type MaybeEmbargo = EmbargoDisplay | null | undefined
37
+
38
+ /** The embargo instant, or null when there is none / it is unparseable. */
39
+ function instantOf(display: MaybeEmbargo): Date | null {
40
+ if (!display?.utc) return null
41
+ const date = new Date(display.utc)
42
+ return isNaN(date.getTime()) ? null : date
43
+ }
44
+
15
45
  /** True while the embargo is set and has not lifted yet. */
16
- export function isEmbargoActive(dateString: string | null | undefined): boolean {
17
- if (!dateString) return false
18
- const date = new Date(dateString)
19
- return !isNaN(date.getTime()) && date.getTime() > Date.now()
46
+ export function isEmbargoActive(display: MaybeEmbargo): boolean {
47
+ const date = instantOf(display)
48
+ return date !== null && date.getTime() > Date.now()
20
49
  }
21
50
 
22
51
  /**
@@ -27,11 +56,12 @@ export function isEmbargoActive(dateString: string | null | undefined): boolean
27
56
  * Returns "" once the embargo has lifted (or when there is none).
28
57
  */
29
58
  export function formatEmbargoCountdown(
30
- dateString: string | null | undefined,
59
+ display: MaybeEmbargo,
31
60
  locale: string,
32
61
  ): string {
33
- if (!isEmbargoActive(dateString)) return ""
34
- const diffMs = new Date(dateString as string).getTime() - Date.now()
62
+ const date = instantOf(display)
63
+ if (!date || date.getTime() <= Date.now()) return ""
64
+ const diffMs = date.getTime() - Date.now()
35
65
 
36
66
  const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" })
37
67
  const minutes = Math.round(diffMs / 60000)
@@ -88,14 +118,13 @@ function dayIn(date: Date, timeZone?: string): string {
88
118
  * Returns "" for a missing/invalid date so callers can `v-if` on it.
89
119
  */
90
120
  export function formatEmbargoDateTime(
91
- dateString: string | null | undefined,
92
- timezone: string | null | undefined,
121
+ display: MaybeEmbargo,
93
122
  locale: string,
94
123
  ): string {
95
- if (!dateString) return ""
96
- const date = new Date(dateString)
97
- if (isNaN(date.getTime())) return ""
124
+ const date = instantOf(display)
125
+ if (!date) return ""
98
126
 
127
+ const timezone = display?.campaign_zone
99
128
  const viewerZone = Intl.DateTimeFormat().resolvedOptions().timeZone
100
129
 
101
130
  if (!timezone || timezone === viewerZone) {
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Go back without falling out of the app.
3
+ *
4
+ * `window.history.length > 1` is the wrong test: it counts the whole tab
5
+ * session, so a user who landed here from a Google result has length 2 and
6
+ * `history.back()` returns them to Google. Same for an email link, a Discord
7
+ * link, or anything else external.
8
+ *
9
+ * Vue Router records the previous in-app entry in `history.state.back`. It is
10
+ * only set when the navigation happened inside the SPA, which is exactly the
11
+ * condition under which going back is safe. Anything else falls through to an
12
+ * explicit route.
13
+ */
14
+ export function goBackWithin(fallbackPath: string): void {
15
+ if (typeof window === "undefined") return
16
+
17
+ const router = useRouter()
18
+ const previous = (router.options.history.state as Record<string, unknown> | undefined)?.back
19
+
20
+ if (typeof previous === "string" && previous.length > 0) {
21
+ router.back()
22
+ return
23
+ }
24
+
25
+ navigateTo(fallbackPath)
26
+ }