@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/components/keys/KeyBrowser.vue +41 -11
- package/components/keys/KeyCampaignCommentItem.vue +303 -0
- package/components/keys/KeyCampaignComments.vue +355 -0
- package/components/keys/KeyCampaignDashboardCard.vue +2 -2
- package/components/keys/KeyCampaignFeedback.vue +250 -0
- package/components/keys/KeyRevealModal.vue +2 -2
- package/locales/de.json +45 -3
- package/locales/en.json +45 -3
- package/locales/es.json +45 -3
- package/locales/pt-BR.json +45 -3
- package/locales/ro.json +45 -3
- package/package.json +1 -1
- package/pages/key-campaigns/[slug].vue +86 -34
- package/pages/key-campaigns/index.vue +72 -1
- package/pages/key-campaigns/key-materials.vue +2 -2
- package/pages/key-campaigns/redeem-key-approved.vue +30 -24
- package/pages/key-campaigns/redeem-key.vue +58 -14
- package/pages/keys/index.vue +1 -1
- package/pages/presskit/game/[slug].vue +33 -3
- package/services/campaignService.ts +149 -2
- package/utils/embargo.ts +47 -18
- package/utils/navigation.ts +26 -0
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.
|
|
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
|
|
10
|
-
* (how the studio communicates the embargo everywhere else) and,
|
|
11
|
-
* viewer is somewhere else, the same instant on their own clock — so
|
|
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(
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
59
|
+
display: MaybeEmbargo,
|
|
31
60
|
locale: string,
|
|
32
61
|
): string {
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
92
|
-
timezone: string | null | undefined,
|
|
121
|
+
display: MaybeEmbargo,
|
|
93
122
|
locale: string,
|
|
94
123
|
): string {
|
|
95
|
-
|
|
96
|
-
|
|
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
|
+
}
|