Skip to content

Commit 825c3b0

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

8 files changed

Lines changed: 408 additions & 2 deletions
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
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, 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+
27+
interface Props {
28+
group: TaskGroup
29+
first: boolean
30+
options: CustomOption[]
31+
project: Project
32+
tutorial: Tutorial
33+
tutorialTasks: TutorialTileTask[]
34+
tasks: TileTask[] | undefined
35+
}
36+
37+
const taskOffset = ref(0)
38+
const projectInfoRef = useTemplateRef('projectInfo')
39+
40+
const props = defineProps<Props>()
41+
42+
const logMappingStarted = inject<(projectType: string) => void>('logMappingStarted')
43+
const saveResults =
44+
inject<(results: Record<string, number[]>, startTime: string) => void>('saveResults')
45+
const arrowKeys = ref(true)
46+
const startTime = shallowRef<string>()
47+
const instruction = computed(() =>
48+
isDefined(props.project.projectInstruction)
49+
? props.project.projectInstruction
50+
: t('validateProject.doesTheShapeOutline', { feature: props.project.lookFor }),
51+
)
52+
53+
const emit = defineEmits<{ created: [] }>()
54+
const results = ref<Record<string, number[]>>({})
55+
const subGridSizeExponent = computed(() => {
56+
const subGridSizeToExponentMapping: Record<string, number> = {
57+
'2x2': 1,
58+
'4x4': 2,
59+
'8x8': 3,
60+
}
61+
62+
if ('subGridSize' in props.project && typeof props.project.subGridSize === 'string') {
63+
return subGridSizeToExponentMapping[props.project.subGridSize] ?? 1;
64+
}
65+
66+
return 1;
67+
})
68+
69+
const processedTasks = computed(() => {
70+
const tasks = props.tasks?.length ? props.tasks : buildTasks(props.project, props.group)
71+
72+
const sortedTasks = tasks.sort((a, b) => (a.taskId > b.taskId ? 1 : -1))
73+
74+
return sortedTasks
75+
})
76+
77+
watchEffect(() => {
78+
results.value = listToMap(
79+
processedTasks.value,
80+
({ taskId }) => taskId,
81+
() => Array.from(new Array((2 ** subGridSizeExponent.value) ** 2).keys()).map(
82+
// FIXME: default value should be the first option value
83+
// () => props.options[0].value,
84+
() => 0,
85+
),
86+
)
87+
88+
/*
89+
selectedTasks.value = listToMap(
90+
processedTasks.value,
91+
({ taskId }) => taskId,
92+
() => false,
93+
)
94+
*/
95+
startTime.value = new Date().toISOString()
96+
})
97+
98+
const optionMapping = computed(() =>
99+
listToMap(
100+
props.options,
101+
({ value }) => value,
102+
)
103+
)
104+
const nextOptionMapping = computed(() =>
105+
listToMap(
106+
props.options,
107+
({ value }) => value,
108+
(_, __, index) => {
109+
if (index === props.options.length - 1) {
110+
return props.options[0].value
111+
}
112+
113+
return props.options[index + 1].value
114+
},
115+
),
116+
)
117+
118+
119+
const currentTask = computed(() => processedTasks.value?.[taskOffset.value] ?? {})
120+
const currentTaskValue = computed(() => results.value[currentTask.value.taskId]);
121+
122+
onMounted(() => {
123+
logMappingStarted?.(props.project.projectType)
124+
emit('created')
125+
})
126+
127+
const isLastTask = computed(() => processedTasks.value.length - 1 === taskOffset.value)
128+
const currentTaskAnswered = computed(() => isDefined(results.value[currentTask.value.taskId]))
129+
130+
function handleBack() {
131+
if (taskOffset.value > 0) {
132+
taskOffset.value = taskOffset.value - 1
133+
}
134+
}
135+
136+
function handleForward() {
137+
if (taskOffset.value < processedTasks.value.length - 1) {
138+
taskOffset.value = taskOffset.value + 1
139+
}
140+
}
141+
142+
function handleTaskValueChange(taskId: string, newValue: number[]) {
143+
results.value[taskId] = newValue;
144+
}
145+
146+
</script>
147+
148+
<template>
149+
<ProjectHeader :mission="instruction">
150+
<ProjectInfo
151+
ref="projectInfo"
152+
:first="first"
153+
:informationPages="createInformationPages(props.tutorial, props.project, createFallbackInformationPages)"
154+
:manualUrl="project?.manualUrl"
155+
@toggle-dialog="arrowKeys = !arrowKeys"
156+
>
157+
<template #instructions>
158+
<LocateFeaturesProjectInstructions :instruction="instruction" :options="props.options" />
159+
</template>
160+
<template #tutorial>
161+
<LocateFeaturesProjectTutorial
162+
:tutorial="tutorial"
163+
:tasks="tutorialTasks"
164+
:options="options"
165+
@tutorialComplete="projectInfoRef?.toggleDialog"
166+
/>
167+
</template>
168+
</ProjectInfo>
169+
</ProjectHeader>
170+
<v-container class="ma-0 pa-0 container">
171+
<LocateFeaturesProjectTask
172+
:v-if="currentTask"
173+
:task="currentTask"
174+
:subGridSizeExponent="subGridSizeExponent"
175+
:value="currentTaskValue"
176+
:optionMapping="optionMapping"
177+
:nextOptionMapping="nextOptionMapping"
178+
@onValueChange="handleTaskValueChange"
179+
/>
180+
</v-container>
181+
<v-toolbar color="white" density="compact" extension-height="20" extended>
182+
<v-spacer />
183+
<v-btn
184+
:title="$t('findProject.moveLeft')"
185+
icon="mdi-chevron-left"
186+
color="secondary"
187+
:disabled="taskOffset <= 0"
188+
@click="handleBack"
189+
v-shortkey.once="[arrowKeys ? 'arrowleft' : '']"
190+
@shortkey="handleBack"
191+
/>
192+
<v-btn
193+
v-if="isDefined(startTime)"
194+
:title="$t('projectView.saveResults')"
195+
icon="mdi-content-save"
196+
color="primary"
197+
:disabled="!isLastTask"
198+
@click="saveResults?.(results, startTime)"
199+
/>
200+
<v-btn
201+
:title="$t('findProject.moveRight')"
202+
icon="mdi-chevron-right"
203+
color="secondary"
204+
:disabled="!currentTaskAnswered || isLastTask"
205+
@click="handleForward"
206+
v-shortkey.once="[arrowKeys ? 'arrowright' : '']"
207+
@shortkey="handleForward"
208+
/>
209+
<v-spacer />
210+
<template #extension>
211+
<TaskProgress :progress="taskOffset + 1" :total="processedTasks.length" />
212+
</template>
213+
</v-toolbar>
214+
</template>
215+
216+
<style scoped>
217+
.container {
218+
height: calc(100vh - 390px);
219+
}
220+
</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>
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
<script lang="ts" setup>
2+
import type { CustomOption, TileTask } from '@/utils/types'
3+
import ImageTile from './ImageTile.vue';
4+
import { computed } from 'vue';
5+
6+
interface Props {
7+
task: TileTask
8+
subGridSizeExponent: number;
9+
optionMapping: Record<string, CustomOption>;
10+
nextOptionMapping: Record<number, number>;
11+
value: number[];
12+
}
13+
14+
const props = defineProps<Props>()
15+
16+
const emit = defineEmits<{
17+
onValueChange: [taskId: TileTask['taskId'], newResults: number[]]
18+
}>()
19+
20+
const gridSize = 2 ** props.subGridSizeExponent;
21+
const overlayGrids = Array.from(new Array(gridSize ** 2).keys());
22+
23+
const overlayGridsStyle = computed(() => ({
24+
gridTemplateColumns: `repeat(${gridSize}, 1fr)`,
25+
gridTemplateRows: `repeat(${gridSize}, 1fr)`,
26+
}))
27+
28+
function getNewValue(oldValue: number) {
29+
return props.nextOptionMapping[oldValue];
30+
}
31+
32+
function handleClick(gridIndex: number) {
33+
const newValue = [...props.value]
34+
newValue[gridIndex] = getNewValue(props.value[gridIndex])
35+
emit('onValueChange', props.task.taskId, newValue)
36+
}
37+
38+
const overlayGridStyle = computed(() => (
39+
props.value.map((subGridValue) => {
40+
const selectedOption = props.optionMapping[subGridValue];
41+
42+
return {
43+
backgroundColor: selectedOption.iconColor,
44+
}
45+
})
46+
))
47+
48+
console.info(overlayGridStyle.value);
49+
50+
</script>
51+
52+
<template>
53+
<v-container>
54+
<v-row justify="center">
55+
<div class="task-container">
56+
<ImageTile
57+
:url="task.url"
58+
:spinner="true"
59+
/>
60+
<div
61+
class="overlay-grids"
62+
:style="overlayGridsStyle"
63+
>
64+
<div
65+
v-for="gridIndex in overlayGrids"
66+
v-bind:key="gridIndex"
67+
class="overlay-grid"
68+
:style="overlayGridStyle[gridIndex]"
69+
@click="handleClick(gridIndex)"
70+
/>
71+
</div>
72+
</div>
73+
</v-row>
74+
</v-container>
75+
</template>
76+
77+
<style scoped>
78+
.task-container {
79+
aspect-ratio: 1;
80+
width: 100%;
81+
height: 100%;
82+
max-width: 512px;
83+
position: relative;
84+
isolation: isolate;
85+
86+
.overlay-grids {
87+
display: grid;
88+
position: absolute;
89+
left: 0;
90+
top: 0;
91+
width: 100%;
92+
height: 100%;
93+
94+
.overlay-grid {
95+
position: relative;
96+
border: 1pt solid rgba(255, 255, 255, .3);
97+
opacity: .3;
98+
99+
&:hover {
100+
cursor: pointer;
101+
background-color: rgba(255, 255, 255, .1);
102+
}
103+
}
104+
}
105+
}
106+
</style>

0 commit comments

Comments
 (0)