Skip to content

Commit

Permalink
Implement the choice helper
Browse files Browse the repository at this point in the history
  • Loading branch information
ysbrandB committed Jun 17, 2024
1 parent 003817f commit 896537f
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 34 deletions.
3 changes: 1 addition & 2 deletions app/Http/Controllers/ItemController.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ class ItemController extends Controller
*/
public function index(Request $request)
{
// return File::json(base_path('questions.json'))['name'];

$builder = Item::query()->with('attributes.attributeType');
$filters = $request->input('filters');
foreach ($filters ?? [] as $attributeCategoryId => $attributeIds) {
Expand All @@ -32,6 +30,7 @@ public function index(Request $request)
'items' => $builder->get(),
'attributeTypes' => AttributeType::with('attributes')->orderBy('created_at', 'desc')->get(),
'initialFilters' => $filters,
'questions' => json_decode(File::get(base_path('questions.json'))),
]);
}

Expand Down
23 changes: 23 additions & 0 deletions questions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"questions": [
{
"question": "What is the correct spelling?",
"description": "Choose the correct spelling out of the buttons",
"answers": [
{"text": "Paris", "filters": [0]},
{"text": "Peris", "filters": [1]},
{"text": "Baris", "filters": [2]},
{"text": "Parice", "filters": [3]}
]
},{
"question": "What is the correct spelling?",
"description": "Choose the correct spelling out of the buttons",
"answers": [
{"text":"Berlin", "filters": [4]},
{"text":"Berline", "filters": [5]},
{"text":"Berlen", "filters": [6]},
{"text":"Berlun", "filters": [7]}
]
}
]
}
59 changes: 45 additions & 14 deletions resources/js/CustomComponents/AttributeFilter.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import OpeningComponent from "@/CustomComponents/OpeningComponent.vue";
import Pill from "@/CustomComponents/Pill.vue";
import {Attribute, AttributeType} from "@/types";
import {nextTick, ref} from "vue";
import {computed, nextTick, ref} from "vue";
const props = withDefaults(defineProps<{
attributeTypes: AttributeType[],
Expand All @@ -18,24 +18,41 @@ const emit = defineEmits<{
const check = (attributeType: AttributeType, attribute: Attribute, checked: boolean) => nextTick(() => {
if (checked) {
checkedAttributes.value.set(attributeType, [...(checkedAttributes.value.get(attributeType) ?? []), attribute]);
checkedAttributesMap.value.set(attributeType, [...(checkedAttributesMap.value.get(attributeType) ?? []), attribute]);
} else {
checkedAttributes.value.set(attributeType, (checkedAttributes.value.get(attributeType) ?? []).filter((checkedAttribute) => checkedAttribute.id !== attribute.id));
checkedAttributesMap.value.set(attributeType, (checkedAttributesMap.value.get(attributeType) ?? []).filter((checkedAttribute) => checkedAttribute.id !== attribute.id));
}
//filter out the attribute type if there are no attributes
if (checkedAttributes.value.get(attributeType)?.length === 0) {
checkedAttributes.value.delete(attributeType);
if (checkedAttributesMap.value.get(attributeType)?.length === 0) {
checkedAttributesMap.value.delete(attributeType);
}
const allAttributes: Record<number, number[]> = {};
for (const [attributeType, attributes] of checkedAttributes.value.entries()) {
allAttributes[attributeType.id] = attributes.map((attribute) => attribute.id);
}
emit('update', allAttributes);
emit('update', checkedAttributes.value);
});
const addFiltersByAttributeIds = (ids: number[]) => {
ids.forEach((id) => {
addFilterByAttributeId(id);
});
}
const addFilterByAttributeId = (id: number) => {
const attribute = props.attributeTypes.flatMap((attributeType) => attributeType.attributes).find((attribute) => attribute?.id === id);
if (attribute) {
//find the corresponding attribute type
const attributeType = props.attributeTypes.find((attributeType) => attributeType.id === attribute.attribute_type_id);
if(attributeType && !checkedAttributesMap.value.has(attributeType)){
check(attributeType, attribute, true);
}
}
}
const reset = () => {
checkedAttributesMap.value.clear();
emit('update', {});
}
const checkedAttributes = ref(new Map<AttributeType, Attribute[]>());
const checkedAttributesMap = ref(new Map<AttributeType, Attribute[]>());
if (props.initialFilters) {
for (const [attributeTypeId, attributeIds] of Object.entries(props.initialFilters)) {
const attributeType = props.attributeTypes.find((attributeType) => attributeType.id === parseInt(attributeTypeId));
Expand All @@ -50,23 +67,37 @@ if (props.initialFilters) {
}
}
const checkedAttributes = computed(() => {
const allAttributes: Record<number, number[]> = {};
for (const [attributeType, attributes] of checkedAttributesMap.value.entries()) {
allAttributes[attributeType.id] = attributes.map((attribute) => attribute.id);
}
return allAttributes;
});
function capitalizeFirstLetter(string: string) {
return string[0].toUpperCase() + string.slice(1);
}
defineExpose({
reset,
addFiltersByAttributeIds,
checkedAttributes
})
</script>
<template>
<div class="w-full text-2xl text-center font-semibold mt-4">{{ capitalizeFirstLetter(title) }}</div>
<div
class="mx-2 mt-1 flex flex-wrap text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600 dark:text-white">
<template v-for="[attributeType, attributes] in checkedAttributes.entries()">
<template v-for="[attributeType, attributes] in checkedAttributesMap.entries()">
<pill
v-for="attribute in attributes" :key="attribute.id"
class="cursor-pointer" @click="check(attributeType, attribute, false)"
:color="attributeType.color">
{{ attribute.title }} <span class="ms-2 text-red-600">x</span>
</pill>
</template>
<div v-if="checkedAttributes.size===0" class="w-full p-2 text-center text-gray-500">
<div v-if="checkedAttributesMap.size===0" class="w-full p-2 text-center text-gray-500">
No {{ title }} applied
</div>
</div>
Expand All @@ -82,7 +113,7 @@ function capitalizeFirstLetter(string: string) {
class="w-full border-b border-gray-200 rounded-t-lg dark:border-gray-600">
<div class="flex items-center ps-3">
<input type="checkbox"
:checked="checkedAttributes.get(attributeType)?.some((checkedAttribute) => checkedAttribute.id === attribute.id)"
:checked="checkedAttributesMap.get(attributeType)?.some((checkedAttribute) => checkedAttribute.id === attribute.id)"
@change="check(attributeType, attribute, ($event as HTMLInputElement|any).target.checked)"
:id="attribute.id.toString()" :value="attribute.id"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-700 dark:focus:ring-offset-gray-700 focus:ring-2 dark:bg-gray-600 dark:border-gray-500">
Expand Down
89 changes: 71 additions & 18 deletions resources/js/Pages/Items/Index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,30 @@ import Card from "@/Components/Card.vue";
import AttributeFilter from "@/CustomComponents/AttributeFilter.vue";
import PrimaryButton from "@/Components/PrimaryButton.vue";
import SecondaryButton from "@/Components/SecondaryButton.vue";
import {ref, watch} from "vue";
import {computed, onMounted, Ref, ref, watch} from "vue";
import DangerButton from "@/Components/DangerButton.vue";
import Pill from "@/CustomComponents/Pill.vue";
import axios from "axios";
import ItemCard from "@/Pages/Items/ItemCard.vue";
import Modal from "@/Components/Modal.vue";
const props = defineProps<{
items: Item[],
initialSelectedItems?: Item[],
attributeTypes: AttributeType[],
initialFilters: Record<number, number[]>,
questions: {
questions: {
title: string,
description: string,
answers: {
text: string,
filters: number[]
}[]
}[],
},
}>();
const selectedItems = ref(new Set<Item>(props.initialSelectedItems??[]));
const selectedItems = ref(new Set<Item>(props.initialSelectedItems ?? []));
const reloadWithFilters = (filters: Record<number, number[]>) => {
router.reload({
data: {
Expand All @@ -34,28 +45,53 @@ watch(selectedItems.value, (selected) => {
selected: Array.from(selected).map((item: Item) => item.id)
});
})
const resetChoice = () => {
choiceOpen.value = false;
choiceIndex.value = 0;
}
const questionAnswered = (filters: number[]) => {
filter.value?.addFiltersByAttributeIds(filters);
if (choiceIndex.value < props.questions.questions.length - 1) {
choiceIndex.value++;
return;
}
resetChoice();
}
const choiceOpen = ref(false);
const choiceIndex = ref(0);
const question = computed(() => props.questions.questions[choiceIndex.value]);
const filter: Ref<typeof AttributeFilter | null> = ref(null);
onMounted(() => console.log(filter.value))
</script>

<template>
<AuthenticatedLayout>
<template #header>
<div class="flex flex-row justify-between">
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">Items</h2>
<div class="flex flex-row justify-end">
<div class="flex flex-row gap-2 justify-end">
<div class="flex flex-row gap-2" v-if="$page.props.auth.user">
<NavLink :href="route('items.create')">
New item
</NavLink>
</div>
<primary-button
@click="router.get(route('graph.index'))">
See overview
</primary-button>
<primary-button
@click="resetChoice(); filter?.reset(); choiceOpen=true">
Choice helper
</primary-button>
<pill :color="''" v-for="item in selectedItems" class="cursor-pointer bg-red-100"
@click="selectedItems.delete(item)">
{{ item.title }} <span class="ms-2 text-red-600">x</span>
</pill>
</div>
<div class="flex flex-row gap-2" v-if="$page.props.auth.user">
<NavLink :href="route('items.create')">
New item
</NavLink>
</div>
</div>
</template>

Expand All @@ -65,6 +101,7 @@ watch(selectedItems.value, (selected) => {
<card class="mt-4">
<div class="w-full text-center">
<attribute-filter
ref="filter"
@update="reloadWithFilters"
:attribute-types="attributeTypes"
:initial-filters="initialFilters"/>
Expand All @@ -73,20 +110,36 @@ watch(selectedItems.value, (selected) => {
</div>
<div class="lg:col-span-9 col-span-3">
<div class="grid lg:grid-cols-4 md:grid-cols-2 grid-cols-1 gap-4 mt-4">
<item-card :item="item" v-for="item in items">
<template #qr>
<danger-button v-if="selectedItems.has(item)" @click="selectedItems.delete(item)"
class="w-min text-sm px-0 py-0 mb-2 mr-2 self-end"> -
</danger-button>
<secondary-button v-else @click="selectedItems.add(item)"
class="w-min text-sm px-0 py-0 mb-2 mr-2 self-end"> +
</secondary-button>
</template>
</item-card>
<item-card :item="item" v-for="item in items">
<template #qr>
<danger-button v-if="selectedItems.has(item)" @click="selectedItems.delete(item)"
class="w-min text-sm px-0 py-0 mb-2 mr-2 self-end"> -
</danger-button>
<secondary-button v-else @click="selectedItems.add(item)"
class="w-min text-sm px-0 py-0 mb-2 mr-2 self-end"> +
</secondary-button>
</template>
</item-card>
</div>
</div>
</div>
</section>
<modal :show="choiceOpen" @close="resetChoice">
<div class="w-full bg-blue-100 p-8">
<div class="text-black text-2xl font-bold">Choice helper</div>
<div class="text-gray-900 text-lg mt-1">
Answer the questions to see which items fit your needs!
</div>
<card class="mt-4">
<div class="text-gray-800 dark:text-gray-200 leading-tight">{{ question.description }}</div>
<div class="w-full mt-2 mx-auto" v-for="answer in question.answers">
<primary-button @click="questionAnswered(answer.filters)">
{{ answer.text }}
</primary-button>
</div>
</card>
</div>
</modal>

</AuthenticatedLayout>
</template>

0 comments on commit 896537f

Please sign in to comment.