@mundogamernetwork/shared-ui 1.1.96 → 1.2.1
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/playtest/PlaytestBrief.vue +152 -0
- package/components/playtest/PlaytestPendingConfirmation.vue +84 -0
- package/components/playtest/PlaytestResponseFlow.vue +371 -0
- package/components/playtest/StarRatingInput.vue +105 -0
- package/components/playtest/accessibility/CriterionRow.vue +167 -0
- package/components/playtest/baseline/BaselineSectionStep.vue +106 -0
- package/components/playtest/compliance/ChecklistItemRow.vue +200 -0
- package/components/playtest/concept-poll/EmojiReactionVote.vue +145 -0
- package/components/playtest/concept-poll/FiveSecondTestVote.vue +197 -0
- package/components/playtest/concept-poll/HeadToHeadVote.vue +98 -0
- package/components/playtest/concept-poll/MultiSelectVote.vue +104 -0
- package/components/playtest/concept-poll/RankedChoiceVote.vue +169 -0
- package/components/playtest/concept-poll/SingleSelectVote.vue +98 -0
- package/components/playtest/concept-poll/StarRatingVote.vue +110 -0
- package/components/playtest/concept-poll/VoteReasonField.vue +74 -0
- package/components/playtest/lqa/FileIssueForm.vue +181 -0
- package/components/playtest/lqa/IssueList.vue +99 -0
- package/components/playtest/lqa/LocaleSelector.vue +73 -0
- package/components/playtest/moderated/JoinCallPanel.vue +135 -0
- package/components/playtest/moderated/SessionCard.vue +146 -0
- package/composables/usePlaytestResponse.ts +446 -0
- package/nuxt.config.ts +42 -4
- package/package.json +10 -3
- package/services/playtestTesterService.ts +416 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Join-call panel for a scheduled/completed moderated session — surfaces
|
|
3
|
+
// Meeting::meeting_link (eager-loaded server-side via
|
|
4
|
+
// PlaytestModeratedSessionRepository::findWithRelations(), confirmed this
|
|
5
|
+
// session), plus the confirm/no-show/cancel actions. NoShow is deliberately
|
|
6
|
+
// NOT exposed here as a tester self-action — markNoShow() is a
|
|
7
|
+
// moderator-facing action in the backend's own doc comments; kept in the
|
|
8
|
+
// service for completeness (per the task list) but this panel only wires
|
|
9
|
+
// confirm/cancel for the tester's own use.
|
|
10
|
+
import { computed } from "vue";
|
|
11
|
+
import type { PlaytestModeratedSession } from "../../../services/playtestTesterService";
|
|
12
|
+
|
|
13
|
+
const props = defineProps<{
|
|
14
|
+
session: PlaytestModeratedSession | null;
|
|
15
|
+
acting?: boolean;
|
|
16
|
+
}>();
|
|
17
|
+
|
|
18
|
+
const emit = defineEmits<{
|
|
19
|
+
(e: "confirm"): void
|
|
20
|
+
(e: "cancel"): void
|
|
21
|
+
}>();
|
|
22
|
+
|
|
23
|
+
const canJoin = computed(() => props.session?.status === "scheduled" && !!props.session?.meeting?.meeting_link);
|
|
24
|
+
const canConfirm = computed(() => props.session?.status === "scheduled" && !props.session?.tester_confirmed_at);
|
|
25
|
+
const canCancel = computed(() => props.session?.status === "scheduled");
|
|
26
|
+
|
|
27
|
+
const scheduledAt = computed(() => (props.session?.scheduled_at ? new Date(props.session.scheduled_at) : null));
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<template>
|
|
31
|
+
<div class="join-call-panel">
|
|
32
|
+
<template v-if="session">
|
|
33
|
+
<h3 class="join-call-panel__title">{{ session.campaign?.title ?? $t("playtest.moderated.session_title", "Moderated session") }}</h3>
|
|
34
|
+
|
|
35
|
+
<p class="join-call-panel__time">{{ scheduledAt?.toLocaleString() }}</p>
|
|
36
|
+
|
|
37
|
+
<div v-if="session.tester_confirmed_at" class="join-call-panel__confirmed-badge">
|
|
38
|
+
<i class="fa-solid fa-circle-check" />
|
|
39
|
+
{{ $t("playtest.moderated.you_confirmed", "You've confirmed attendance") }}
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<a
|
|
43
|
+
v-if="canJoin"
|
|
44
|
+
:href="session.meeting!.meeting_link!"
|
|
45
|
+
target="_blank"
|
|
46
|
+
rel="noopener"
|
|
47
|
+
class="btn primary join-call-panel__join"
|
|
48
|
+
>
|
|
49
|
+
{{ $t("playtest.moderated.join_call", "Join call") }}
|
|
50
|
+
</a>
|
|
51
|
+
<p v-else-if="session.status === 'scheduled'" class="join-call-panel__no-link">
|
|
52
|
+
{{ $t("playtest.moderated.link_pending", "The call link will appear here closer to the session.") }}
|
|
53
|
+
</p>
|
|
54
|
+
|
|
55
|
+
<div class="join-call-panel__actions">
|
|
56
|
+
<button
|
|
57
|
+
v-if="canConfirm"
|
|
58
|
+
type="button"
|
|
59
|
+
class="btn secondary"
|
|
60
|
+
:disabled="acting"
|
|
61
|
+
@click="emit('confirm')"
|
|
62
|
+
>
|
|
63
|
+
{{ $t("playtest.moderated.confirm_attendance", "Confirm attendance") }}
|
|
64
|
+
</button>
|
|
65
|
+
<button
|
|
66
|
+
v-if="canCancel"
|
|
67
|
+
type="button"
|
|
68
|
+
class="btn secondary join-call-panel__cancel"
|
|
69
|
+
:disabled="acting"
|
|
70
|
+
@click="emit('cancel')"
|
|
71
|
+
>
|
|
72
|
+
{{ $t("playtest.moderated.cancel_session", "Cancel") }}
|
|
73
|
+
</button>
|
|
74
|
+
</div>
|
|
75
|
+
</template>
|
|
76
|
+
|
|
77
|
+
<p v-else class="join-call-panel__empty">
|
|
78
|
+
{{ $t("playtest.moderated.no_session", "No session selected.") }}
|
|
79
|
+
</p>
|
|
80
|
+
</div>
|
|
81
|
+
</template>
|
|
82
|
+
|
|
83
|
+
<style scoped lang="scss">
|
|
84
|
+
.join-call-panel {
|
|
85
|
+
padding: 20px;
|
|
86
|
+
border: 1px solid var(--button-secondary-default-bg, #2a2a2a);
|
|
87
|
+
display: flex;
|
|
88
|
+
flex-direction: column;
|
|
89
|
+
gap: 12px;
|
|
90
|
+
|
|
91
|
+
&__title {
|
|
92
|
+
font-size: 16px;
|
|
93
|
+
font-weight: 700;
|
|
94
|
+
color: var(--card-article-title, #fff);
|
|
95
|
+
margin: 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
&__time {
|
|
99
|
+
font-size: 13px;
|
|
100
|
+
color: var(--secondary-info-fg, #aaa);
|
|
101
|
+
margin: 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
&__confirmed-badge {
|
|
105
|
+
display: flex;
|
|
106
|
+
align-items: center;
|
|
107
|
+
gap: 6px;
|
|
108
|
+
font-size: 12px;
|
|
109
|
+
color: #8ac53d;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
&__join {
|
|
113
|
+
width: fit-content;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
&__no-link {
|
|
117
|
+
font-size: 12px;
|
|
118
|
+
color: var(--secondary-info-fg, #888);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
&__actions {
|
|
122
|
+
display: flex;
|
|
123
|
+
gap: 8px;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
&__cancel {
|
|
127
|
+
color: var(--danger, #e53935);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
&__empty {
|
|
131
|
+
font-size: 13px;
|
|
132
|
+
color: var(--secondary-info-fg, #888);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
</style>
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Card for a single moderated slot (claimable) or an already-booked session
|
|
3
|
+
// (status-aware actions: confirm/cancel/no-show). Dual-purpose by design —
|
|
4
|
+
// consuming pages (slot browse vs. my-sessions list) pass whichever shape
|
|
5
|
+
// they have; `variant` picks the right action set.
|
|
6
|
+
import { computed } from "vue";
|
|
7
|
+
import type { PlaytestModeratedSlot, PlaytestModeratedSession } from "../../../services/playtestTesterService";
|
|
8
|
+
|
|
9
|
+
const props = defineProps<{
|
|
10
|
+
slot?: PlaytestModeratedSlot;
|
|
11
|
+
session?: PlaytestModeratedSession;
|
|
12
|
+
variant: "slot" | "session";
|
|
13
|
+
claiming?: boolean;
|
|
14
|
+
acting?: boolean;
|
|
15
|
+
}>();
|
|
16
|
+
|
|
17
|
+
const emit = defineEmits<{
|
|
18
|
+
(e: "claim", slotId: number): void
|
|
19
|
+
(e: "confirm", sessionId: number): void
|
|
20
|
+
(e: "cancel", sessionId: number): void
|
|
21
|
+
}>();
|
|
22
|
+
|
|
23
|
+
const startsAt = computed(() => {
|
|
24
|
+
const raw = props.variant === "slot" ? props.slot?.starts_at : props.session?.scheduled_at;
|
|
25
|
+
return raw ? new Date(raw) : null;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const formattedDate = computed(() => startsAt.value?.toLocaleString() ?? "");
|
|
29
|
+
|
|
30
|
+
const durationMinutes = computed(() =>
|
|
31
|
+
props.variant === "slot" ? props.slot?.duration_minutes : props.session?.duration_minutes
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const statusLabel = computed(() => props.session?.status ?? null);
|
|
35
|
+
|
|
36
|
+
const canConfirm = computed(() =>
|
|
37
|
+
props.variant === "session" &&
|
|
38
|
+
props.session?.status === "scheduled" &&
|
|
39
|
+
!props.session?.tester_confirmed_at
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const canCancel = computed(() => props.variant === "session" && props.session?.status === "scheduled");
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<template>
|
|
46
|
+
<div class="session-card">
|
|
47
|
+
<div class="session-card__header">
|
|
48
|
+
<span class="session-card__date">{{ formattedDate }}</span>
|
|
49
|
+
<span v-if="durationMinutes" class="session-card__duration">{{ durationMinutes }} min</span>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<div v-if="statusLabel" class="session-card__status" :class="`session-card__status--${statusLabel}`">
|
|
53
|
+
{{ statusLabel.replace('_', ' ') }}
|
|
54
|
+
<span v-if="session?.tester_confirmed_at" class="session-card__confirmed">
|
|
55
|
+
· {{ $t("playtest.moderated.confirmed", "confirmed") }}
|
|
56
|
+
</span>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<div class="session-card__actions">
|
|
60
|
+
<button
|
|
61
|
+
v-if="variant === 'slot'"
|
|
62
|
+
type="button"
|
|
63
|
+
class="btn primary"
|
|
64
|
+
:disabled="claiming"
|
|
65
|
+
@click="emit('claim', slot!.id)"
|
|
66
|
+
>
|
|
67
|
+
{{ $t("playtest.moderated.claim_slot", "Claim this slot") }}
|
|
68
|
+
</button>
|
|
69
|
+
|
|
70
|
+
<template v-else>
|
|
71
|
+
<button
|
|
72
|
+
v-if="canConfirm"
|
|
73
|
+
type="button"
|
|
74
|
+
class="btn primary"
|
|
75
|
+
:disabled="acting"
|
|
76
|
+
@click="emit('confirm', session!.id)"
|
|
77
|
+
>
|
|
78
|
+
{{ $t("playtest.moderated.confirm_attendance", "Confirm attendance") }}
|
|
79
|
+
</button>
|
|
80
|
+
<button
|
|
81
|
+
v-if="canCancel"
|
|
82
|
+
type="button"
|
|
83
|
+
class="btn secondary"
|
|
84
|
+
:disabled="acting"
|
|
85
|
+
@click="emit('cancel', session!.id)"
|
|
86
|
+
>
|
|
87
|
+
{{ $t("playtest.moderated.cancel_session", "Cancel") }}
|
|
88
|
+
</button>
|
|
89
|
+
</template>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
</template>
|
|
93
|
+
|
|
94
|
+
<style scoped lang="scss">
|
|
95
|
+
.session-card {
|
|
96
|
+
padding: 14px 16px;
|
|
97
|
+
background: var(--bg-app-badge, #1a1a1a);
|
|
98
|
+
border: 1px solid var(--button-secondary-default-bg, #2a2a2a);
|
|
99
|
+
display: flex;
|
|
100
|
+
flex-direction: column;
|
|
101
|
+
gap: 10px;
|
|
102
|
+
|
|
103
|
+
&__header {
|
|
104
|
+
display: flex;
|
|
105
|
+
align-items: baseline;
|
|
106
|
+
justify-content: space-between;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
&__date {
|
|
110
|
+
font-size: 14px;
|
|
111
|
+
font-weight: 600;
|
|
112
|
+
color: var(--text-primary, #fff);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
&__duration {
|
|
116
|
+
font-size: 12px;
|
|
117
|
+
color: var(--secondary-info-fg, #aaa);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
&__status {
|
|
121
|
+
display: inline-flex;
|
|
122
|
+
width: fit-content;
|
|
123
|
+
font-size: 11px;
|
|
124
|
+
font-weight: 700;
|
|
125
|
+
text-transform: uppercase;
|
|
126
|
+
padding: 2px 8px;
|
|
127
|
+
color: var(--secondary-info-fg, #aaa);
|
|
128
|
+
border: 1px solid var(--border-color, rgba(255, 255, 255, 0.2));
|
|
129
|
+
|
|
130
|
+
&--scheduled { color: var(--primary, #D297FF); border-color: var(--primary, #D297FF); }
|
|
131
|
+
&--completed { color: #8ac53d; border-color: #8ac53d; }
|
|
132
|
+
&--no_show,
|
|
133
|
+
&--cancelled { color: var(--danger, #e53935); border-color: var(--danger, #e53935); }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
&__confirmed {
|
|
137
|
+
text-transform: none;
|
|
138
|
+
font-weight: 400;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
&__actions {
|
|
142
|
+
display: flex;
|
|
143
|
+
gap: 8px;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
</style>
|