Skip to content

Commit 61dee86

Browse files
committed
feat(locate-features): add screens
1 parent 9a38b1c commit 61dee86

8 files changed

Lines changed: 475 additions & 2 deletions
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
<script lang="ts" setup>
2+
import { useI18n } from 'vue-i18n'
3+
4+
const { t } = useI18n()
5+
import type {
6+
Project,
7+
TaskGroup,
8+
Tutorial,
9+
CustomOption,
10+
TutorialTileTask,
11+
TileTask,
12+
} from '@/utils/types'
13+
import { computed, inject, onMounted, ref, shallowRef, useTemplateRef, watchEffect } from 'vue'
14+
import { isDefined, isNotDefined, listToMap } from '@togglecorp/fujs'
15+
16+
import createInformationPages from '@/utils/createInformationPages'
17+
import { createFallbackInformationPages } from '@/utils/domain'
18+
import LocateFeaturesProjectTask from '@/components/LocateFeaturesProjectTask.vue'
19+
import LocateFeaturesProjectInstructions from '@/components/LocateFeaturesProjectInstructions.vue'
20+
import LocateFeaturesProjectTutorial from '@/components/LocateFeaturesProjectTutorial.vue'
21+
import buildTasks from '@/utils/buildTasks'
22+
23+
import ProjectInfo from './ProjectInfo.vue'
24+
import ProjectHeader from './ProjectHeader.vue'
25+
import TaskProgress from './TaskProgress.vue'
26+
import type { VContainer } from 'vuetify/components'
27+
28+
interface Props {
29+
group: TaskGroup
30+
first: boolean
31+
options: CustomOption[]
32+
project: Project
33+
tutorial: Tutorial
34+
tutorialTasks: TutorialTileTask[]
35+
tasks: TileTask[] | undefined
36+
}
37+
38+
const taskOffset = ref(0)
39+
const projectInfoRef = useTemplateRef('projectInfo')
40+
41+
const props = defineProps<Props>()
42+
const taskContainer = shallowRef<VContainer | null>(null)
43+
44+
const logMappingStarted = inject<(projectType: string) => void>('logMappingStarted')
45+
const saveResults =
46+
inject<(results: Record<string, number[]>, startTime: string) => void>('saveResults')
47+
const arrowKeys = ref(true)
48+
const startTime = shallowRef<string>()
49+
const instruction = computed(() =>
50+
isDefined(props.project.projectInstruction)
51+
? props.project.projectInstruction
52+
: t('validateProject.doesTheShapeOutline', { feature: props.project.lookFor }),
53+
)
54+
55+
const emit = defineEmits<{ created: [] }>()
56+
const results = ref<Record<string, number[]>>({})
57+
const tileSize = ref<number>(1);
58+
59+
const subGridSizeExponent = computed(() => {
60+
const subGridSizeToExponentMapping: Record<string, number> = {
61+
'2x2': 1,
62+
'4x4': 2,
63+
'8x8': 3,
64+
}
65+
66+
if ('subGridSize' in props.project && typeof props.project.subGridSize === 'string') {
67+
return subGridSizeToExponentMapping[props.project.subGridSize] ?? 1;
68+
}
69+
70+
return 1;
71+
})
72+
73+
const processedTasks = computed(() => {
74+
const tasks = props.tasks?.length ? props.tasks : buildTasks(props.project, props.group)
75+
76+
const sortedTasks = tasks.sort((a, b) => (a.taskId > b.taskId ? 1 : -1))
77+
78+
return sortedTasks
79+
})
80+
81+
watchEffect(() => {
82+
results.value = listToMap(
83+
processedTasks.value,
84+
({ taskId }) => taskId,
85+
() => Array.from(new Array((2 ** subGridSizeExponent.value) ** 2).keys()).map(
86+
// FIXME: default value should be the first option value
87+
// () => props.options[0].value,
88+
() => 0,
89+
),
90+
)
91+
92+
/*
93+
selectedTasks.value = listToMap(
94+
processedTasks.value,
95+
({ taskId }) => taskId,
96+
() => false,
97+
)
98+
*/
99+
startTime.value = new Date().toISOString()
100+
})
101+
102+
const optionMapping = computed(() =>
103+
listToMap(
104+
props.options,
105+
({ value }) => value,
106+
)
107+
)
108+
const nextOptionMapping = computed(() =>
109+
listToMap(
110+
props.options,
111+
({ value }) => value,
112+
(_, __, index) => {
113+
if (index === props.options.length - 1) {
114+
return props.options[0].value
115+
}
116+
117+
return props.options[index + 1].value
118+
},
119+
),
120+
)
121+
122+
123+
const currentTask = computed(() => processedTasks.value?.[taskOffset.value] ?? {})
124+
const currentTaskValue = computed(() => results.value[currentTask.value.taskId]);
125+
126+
onMounted(() => {
127+
logMappingStarted?.(props.project.projectType)
128+
emit('created')
129+
})
130+
131+
const isLastTask = computed(() => processedTasks.value.length - 1 === taskOffset.value)
132+
const currentTaskAnswered = computed(() => isDefined(results.value[currentTask.value.taskId]))
133+
134+
function handleBack() {
135+
if (taskOffset.value > 0) {
136+
taskOffset.value = taskOffset.value - 1
137+
}
138+
}
139+
140+
function handleForward() {
141+
if (taskOffset.value < processedTasks.value.length - 1) {
142+
taskOffset.value = taskOffset.value + 1
143+
}
144+
}
145+
146+
function handleTaskValueChange(taskId: string, newValue: number[]) {
147+
results.value[taskId] = newValue;
148+
}
149+
150+
function handleTaskContainerResize() {
151+
const el = taskContainer.value?.$el as HTMLDivElement | null;
152+
const bcr = el?.getBoundingClientRect();
153+
154+
if (isNotDefined(bcr)) {
155+
return undefined;
156+
}
157+
158+
const { width, height } = bcr;
159+
tileSize.value = Math.min(width, height);
160+
}
161+
162+
</script>
163+
164+
<template>
165+
<ProjectHeader :mission="instruction">
166+
<ProjectInfo
167+
ref="projectInfo"
168+
:first="first"
169+
:informationPages="createInformationPages(props.tutorial, props.project, createFallbackInformationPages)"
170+
:manualUrl="project?.manualUrl"
171+
@toggle-dialog="arrowKeys = !arrowKeys"
172+
>
173+
<template #instructions>
174+
<LocateFeaturesProjectInstructions :instruction="instruction" :options="props.options" />
175+
</template>
176+
<template #tutorial>
177+
<LocateFeaturesProjectTutorial
178+
:tutorial="tutorial"
179+
:tasks="tutorialTasks"
180+
:options="options"
181+
@tutorialComplete="projectInfoRef?.toggleDialog"
182+
/>
183+
</template>
184+
</ProjectInfo>
185+
</ProjectHeader>
186+
<v-container
187+
class="ma-0 pa-0 container"
188+
ref="taskContainer"
189+
v-resize="handleTaskContainerResize"
190+
>
191+
<LocateFeaturesProjectTask
192+
:v-if="currentTask"
193+
:task="currentTask"
194+
:subGridSizeExponent="subGridSizeExponent"
195+
:value="currentTaskValue"
196+
:optionMapping="optionMapping"
197+
:nextOptionMapping="nextOptionMapping"
198+
:tileSize="tileSize"
199+
@onValueChange="handleTaskValueChange"
200+
/>
201+
</v-container>
202+
<v-toolbar color="white" density="compact" extension-height="20" extended>
203+
<v-spacer />
204+
<v-btn
205+
:title="$t('findProject.moveLeft')"
206+
icon="mdi-chevron-left"
207+
color="secondary"
208+
:disabled="taskOffset <= 0"
209+
@click="handleBack"
210+
v-shortkey.once="[arrowKeys ? 'arrowleft' : '']"
211+
@shortkey="handleBack"
212+
/>
213+
<v-btn
214+
v-if="isDefined(startTime)"
215+
:title="$t('projectView.saveResults')"
216+
icon="mdi-content-save"
217+
color="primary"
218+
:disabled="!isLastTask"
219+
@click="saveResults?.(results, startTime)"
220+
/>
221+
<v-btn
222+
:title="$t('findProject.moveRight')"
223+
icon="mdi-chevron-right"
224+
color="secondary"
225+
:disabled="!currentTaskAnswered || isLastTask"
226+
@click="handleForward"
227+
v-shortkey.once="[arrowKeys ? 'arrowright' : '']"
228+
@shortkey="handleForward"
229+
/>
230+
<v-spacer />
231+
<template #extension>
232+
<TaskProgress :progress="taskOffset + 1" :total="processedTasks.length" />
233+
</template>
234+
</v-toolbar>
235+
</template>
236+
237+
<style scoped>
238+
.container {
239+
height: calc(100vh - 20rem);
240+
display: flex;
241+
align-items: center;
242+
justify-content: center;
243+
}
244+
</style>
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<script lang="ts" setup>
2+
import OptionButton from '@/components/OptionButton.vue'
3+
import type { CustomOption } from '@/utils/types'
4+
5+
interface Props {
6+
instruction: string
7+
options: CustomOption[]
8+
}
9+
10+
const props = defineProps<Props>()
11+
</script>
12+
13+
<template>
14+
<v-card-text>
15+
<div class="text-h6">
16+
{{ $t('projectInstructions.classifyTitle') }}
17+
</div>
18+
<div class="text-p">
19+
{{ props.instruction }} {{ $t('projectInstructions.classifyInstruction') }}.
20+
</div>
21+
<v-row v-for="(option, optionIndex) in props.options" :key="optionIndex" align="center" dense>
22+
<v-col cols="auto" class="mt-5 mr-4">
23+
<option-button :option="option" :only-icon="true" />
24+
</v-col>
25+
<v-col class="mt-5">{{
26+
[option.title, option.description].filter(Boolean).join(': ')
27+
}}</v-col>
28+
</v-row>
29+
30+
<div class="text-h6 mt-10">{{ $t('projectInstructions.useButtonsToNavigate') }}</div>
31+
<div class="text-p mt-2">
32+
<v-row class="align-center" dense>
33+
<v-col cols="auto" class="mr-4">
34+
<v-btn icon="mdi-chevron-left" color="secondary" class="mr-2" variant="text" />
35+
<v-btn icon="mdi-chevron-right" color="secondary" variant="text" />
36+
</v-col>
37+
<v-col>{{ $t('projectInstructions.move') }}</v-col>
38+
</v-row>
39+
</div>
40+
41+
<div class="text-h6 mt-10">{{ $t('projectInstructions.saveYourAnswers') }}</div>
42+
<div class="text-p mt-2">
43+
<v-row class="align-center" dense>
44+
<v-col cols="auto" class="mr-4">
45+
<v-btn icon="mdi-content-save" color="primary" variant="text" />
46+
</v-col>
47+
<v-col>{{ $t('validateProjectInstructions.seenAll') }}</v-col>
48+
</v-row>
49+
</div>
50+
<div class="text-h6 mt-10">{{ $t('projectInstructions.dontWorry') }}</div>
51+
<div class="text-p">{{ $t('projectInstructions.everyTaskIsViewedBy') }}.</div>
52+
</v-card-text>
53+
</template>
54+
55+
<style scoped></style>

0 commit comments

Comments
 (0)