Skip to content

Commit

Permalink
πŸš€βœ¨ Add Filters Feature to rva.rip
Browse files Browse the repository at this point in the history
πŸ—“οΈ Add RVA4Palestine Events to the Calendar
🎨 Expanded Utility of event_sources.json with appConfig section
πŸ—ƒοΈ Expanded Tags and Their Interconnectedness with the Filter
πŸ’„ Finished Filters Modal, including Custom Check Marks

More work has to be done to add cookies to the Filter to save between sessions, and to add an explainer (?) which shows next to tags which may be potentially confusing to users that'll open a popup that explains the meaning of a tag or tag category, but that's work for the future. For now this has been too much work not to push out!
  • Loading branch information
NatVIII authored Aug 27, 2024
2 parents 9cbc1de + be82938 commit 80ce557
Show file tree
Hide file tree
Showing 22 changed files with 1,062 additions and 852 deletions.
1,092 changes: 624 additions & 468 deletions assets/event_sources.json

Large diffs are not rendered by default.

316 changes: 95 additions & 221 deletions components/App.vue

Large diffs are not rendered by default.

19 changes: 0 additions & 19 deletions components/CityFilterItem.vue

This file was deleted.

15 changes: 0 additions & 15 deletions components/CountyFilterItem.vue

This file was deleted.

219 changes: 109 additions & 110 deletions components/FilterModal.vue
Original file line number Diff line number Diff line change
@@ -1,126 +1,125 @@
<script setup lang="ts">
import { VueFinalModal } from 'vue-final-modal'
const props = defineProps<{
title?: string
allCallback: (newIsEnabled: boolean) => void
cityCallback: (newIsEnabled: boolean, city: string) => void
countyCallback: (newIsEnabled: boolean, county: string) => void
}>()
const emit = defineEmits<{
(e: 'confirm'): void
}>()
// There's a lot of redundancy with the cities and counties between this component and the App component.
// Try to reduce it in the future.
import { ref, inject, computed } from 'vue';
import { VueFinalModal } from 'vue-final-modal';
import TopicFilterItem from './TopicFilterItem.vue';
import TagFilterItem from './TagFilterItem.vue';
import eventSourcesJSON from '@/assets/event_sources.json';
// Use composable.
const isAllCitiesInMarinCountyEnabled = useIsAllCitiesInMarinCountyEnabled();
const isSanFranciscoEnabled = useIsSanFranciscoEnabled();
const isOthersInSFSanMateoCountyEnabled = useIsOthersInSFSanMateoCountyEnabled();
const isOaklandEnabled = useIsOaklandEnabled();
const isBerkeleyEnabled = useIsBerkeleyEnabled();
const isOthersInAlamedaCountyEnabled = useIsOthersInAlamedaCountyEnabled();
const isSanJoseEnabled = useIsSanJoseEnabled();
const isOthersInSantaClaraCountyEnabled = useIsOthersInSantaClaraCountyEnabled();
const isSantaCruzEnabled = useIsSantaCruzEnabled();
const isOthersInSantaCruzCountyEnabled = useIsOthersInSantaCruzCountyEnabled();
const tags = inject('tags'); //Grabs the 'tags' array, which features all tags and whether they're hidden, from App.vue
const countyIdToCitiesRefs = {
[MARIN_COUNTY_ID]: [isAllCitiesInMarinCountyEnabled],
[SF_SAN_MATEO_COUNTY_ID]: [isSanFranciscoEnabled, isOthersInSFSanMateoCountyEnabled],
[ALAMEDA_COUNTY_ID]: [isOaklandEnabled, isBerkeleyEnabled, isOthersInAlamedaCountyEnabled],
[SANTA_CLARA_COUNTY_ID]: [isSanJoseEnabled, isOthersInSantaClaraCountyEnabled],
[SANTA_CRUZ_COUNTY_ID]: [isSantaCruzEnabled, isOthersInSantaCruzCountyEnabled],
};
// This function is needed because, since the CityFilterItem component is nested within this component,
// the changes to the state from the App component will not cause a re-render of the CityFilterItem components.
const updateIsCityEnabledLocally = (newIsEnabled, county) => {
countyIdToCitiesRefs[county].forEach((city) => {
city.value = newIsEnabled;
});
function getTagVisibility(tagName) {
const tag = tags.value.find(t => t.name === tagName);
return tag ? tag.isVisible : false;
}
const countyWrapper = (newIsEnabled: boolean, county: string) => {
updateIsCityEnabledLocally(newIsEnabled, county);
props.countyCallback(newIsEnabled, county);
function updateTagVisibility(tagName, visibility) {
const tag = tags.value.find(t => t.name === tagName);
if (tag) {
tag.isVisible = visibility;
}
}
const allWrapper = (newIsEnabled: boolean) => {
updateAllIsCityEnabledLocally(newIsEnabled);
props.allCallback(newIsEnabled);
function setVisibilityForGroup(tagsInGroup, visibility) {
tagsInGroup.forEach(tagName => {
const tag = tags.value.find(t => t.name === tagName.name);
if (tag) {
tag.isVisible = visibility;
}
});
}
const updateAllIsCityEnabledLocally = (newIsEnabled) => {
Object.values(countyIdToCitiesRefs).forEach((cities) => {
cities.forEach((city) => {
city.value = newIsEnabled;
});
});
}
const { enableEventSource, disableEventSource } = defineProps<{
enableEventSource: (name: string) => void;
disableEventSource: (name: string) => void;
}>();
</script>
<template>
<VueFinalModal class="popper-box-wrapper" content-class="popper-box-inner" overlay-transition="vfm-fade"
content-transition="vfm-fade">
<CountyFilterItem :label="ALL_ID" @on-yes="allWrapper(true)" @on-no="allWrapper(false)">
</CountyFilterItem>
const emit = defineEmits<{
(e: 'confirm'): void;
}>();
<CountyFilterItem :label="MARIN_COUNTY_ID"
@on-yes="countyWrapper(true, MARIN_COUNTY_ID); (() => { useIsAllCitiesInMarinCountyEnabled.value = true })()"
@on-no="countyWrapper(false, MARIN_COUNTY_ID); ">
<CityFilterItem v-model="isAllCitiesInMarinCountyEnabled" :label="ALL_CITIES_IN_MARIN_COUNTY_ID"
@on-input="cityCallback($event.target.checked, ALL_CITIES_IN_MARIN_COUNTY_ID)">
</CityFilterItem>
</CountyFilterItem>
// Development environment flag
const isDevelopment = process.env.NODE_ENV === 'development';
<CountyFilterItem :label="SF_SAN_MATEO_COUNTY_ID" @on-yes="countyWrapper(true, SF_SAN_MATEO_COUNTY_ID)"
@on-no="countyWrapper(false, SF_SAN_MATEO_COUNTY_ID)">
<CityFilterItem v-model="isSanFranciscoEnabled" :label="SAN_FRANCISCO_ID"
@on-input="cityCallback($event.target.checked, SAN_FRANCISCO_ID)">
</CityFilterItem>
<CityFilterItem v-model="isOthersInSFSanMateoCountyEnabled" :label="OTHERS_IN_SF_SAN_MATEO_COUNTY_ID"
@on-input="cityCallback($event.target.checked, OTHERS_IN_SF_SAN_MATEO_COUNTY_ID)">
</CityFilterItem>
</CountyFilterItem>
// Accessing tags from the imported JSON
const tagsHeader = ref(eventSourcesJSON.appConfig.tagsHeader);
const tagsHidden = ref(eventSourcesJSON.appConfig.tagsHidden);
const tagsToShow = ref(eventSourcesJSON.appConfig.tagsToShow);
<CountyFilterItem :label="ALAMEDA_COUNTY_ID" @on-yes="countyWrapper(true, ALAMEDA_COUNTY_ID)"
@on-no="countyWrapper(false, ALAMEDA_COUNTY_ID)">
<CityFilterItem v-model="isOaklandEnabled" :label="OAKLAND_ID"
@on-input="cityCallback($event.target.checked, OAKLAND_ID)">
</CityFilterItem>
<CityFilterItem v-model="isBerkeleyEnabled" :label="BERKELEY_ID"
@on-input="cityCallback($event.target.checked, BERKELEY_ID)">
</CityFilterItem>
<CityFilterItem v-model="isOthersInAlamedaCountyEnabled" :label="OTHERS_IN_ALAMEDA_COUNTY_ID"
@on-input="cityCallback($event.target.checked, OTHERS_IN_ALAMEDA_COUNTY_ID)">
</CityFilterItem>
</CountyFilterItem>
// Computed property to flatten tagsToShow into a 1D array of strings, for the purpose of debugging to make sure there's no tags missing
const tagsAllShown = computed(() => {
let flattened = [];
for (let i = 0; i < tagsToShow.value.length; i++) {
if (tagsToShow.value[i].length === 1) {
flattened.push(tagsToShow.value[i][0].map(tag => tag.name));
} else {
flattened.push(...tagsToShow.value[i].slice(1).map(tag => tag.name)); // Skip the first element (label) and add rest
}
}
flattened.push(...tagsHeader.value.map(tag => tag.name)); //Add the tagsHeader values to the array, ensuring that they're not left out from the list of ALL TAGS SHOWN
flattened.push(...tagsHidden.value); //Add the tagsHidden values to the array, ensuring that they're not left out from the list of ALL TAGS SHOWN
return flattened;
});
<CountyFilterItem :label="SANTA_CLARA_COUNTY_ID" @on-yes="countyWrapper(true, SANTA_CLARA_COUNTY_ID)"
@on-no="countyWrapper(false, SANTA_CLARA_COUNTY_ID)">
<CityFilterItem v-model="isSanJoseEnabled" :label="SAN_JOSE_ID"
@on-input="cityCallback($event.target.checked, SAN_JOSE_ID)">
</CityFilterItem>
<CityFilterItem v-model="isOthersInSantaClaraCountyEnabled" :label="OTHERS_IN_SANTA_CLARA_COUNTY_ID"
@on-input="cityCallback($event.target.checked, OTHERS_IN_SANTA_CLARA_COUNTY_ID)">
</CityFilterItem>
</CountyFilterItem>
//
const tagsNotShown = computed(() => { //A 1D array of tags that aren't featured in tagsAllShown but ARE featured in tags; used to show any tags that maybe should be visible in the UI or should be wiped from event_sources.json altogether.
const shownSet = new Set(tagsAllShown.value);
return tags.value.filter(tag => !shownSet.has(tag.name));
});
<CountyFilterItem :label="SANTA_CRUZ_COUNTY_ID" @on-yes="countyWrapper(true, SANTA_CRUZ_COUNTY_ID)"
@on-no="countyWrapper(false, SANTA_CRUZ_COUNTY_ID)">
<CityFilterItem v-model="isSantaCruzEnabled" :label="SANTA_CRUZ_ID"
@on-input="cityCallback($event.target.checked, SANTA_CRUZ_ID)">
</CityFilterItem>
<CityFilterItem v-model="isOthersInSantaCruzCountyEnabled" :label="OTHERS_IN_SANTA_CRUZ_COUNTY_ID"
@on-input="cityCallback($event.target.checked, OTHERS_IN_SANTA_CRUZ_COUNTY_ID)">
</CityFilterItem>
</CountyFilterItem>
<div class="bottom">
function handleEventSourceChange(tag: string, isEnabled: boolean) {
if (isEnabled) {
enableEventSource(tag);
} else {
disableEventSource(tag);
}
}
// Example function to toggle visibility
function toggleTagVisibility(tagName: string) {
const tag = tags.value.find(t => t.name === tagName);
if (tag) {
tag.isVisible = !tag.isVisible;
}
}
<button @click="emit('confirm')">
Done
</button>
</div>
</VueFinalModal>
</template>
</script>

<template>
<VueFinalModal class="popper-box-wrapper" content-class="popper-box-inner" overlay-transition="vfm-fade" content-transition="vfm-fade">
<span class="event-headers">
Event Purpose
</span>
<div class="county-header">
<TagFilterItem v-for="tag in tagsHeader" :key="tag.name" class="tag-group" :label="tag.fullName" :modelValue="getTagVisibility(tag.name)" @update:modelValue="updateTagVisibility(tag.name, $event)">
</TagFilterItem>
</div>
<span class="event-headers">
Event Type
</span>
<div v-for="group in tagsToShow" :key="group[0] || group" class="tag-group">
<template v-if="Array.isArray(group)">
<TopicFilterItem class="tag-header" :label="group[0].fullName" @checkAll="setVisibilityForGroup(group.slice(1), true)" @uncheckAll="setVisibilityForGroup(group.slice(1), false)">
<TagFilterItem v-for="tag in group.slice(1)" :key="tag.name" :label="tag.fullName" :modelValue="getTagVisibility(tag.name)" @update:modelValue="updateTagVisibility(tag.name, $event)">
</TagFilterItem>
</TopicFilterItem>
</template>
<template v-else>
<TagFilterItem class="tag-sub-item" :label="group.fullName" :modelValue="getTagVisibility(group.name)" @update:modelValue="updateTagVisibility(group.name, $event)">
</TagFilterItem>
</template>
</div>
<span v-if="isDevelopment">
<span class="event-headers">
Tags Not Shown
</span>
<div v-for="tag in tagsNotShown" :key="tag">
<input type="checkbox"
:checked="getTagVisibility(tag.name)"
@change="updateTagVisibility(tag.name, $event.target.checked)" /> {{ tag.name }}
</div>
</span>
<div class="bottom filterButton">
<button @click="emit('confirm')">Apply</button>
</div>
</VueFinalModal>
</template>
20 changes: 20 additions & 0 deletions components/TagFilterItem.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<script setup>
const props = defineProps({
label: String,
modelValue: Boolean,
isVisible: Boolean
});
const emit = defineEmits(['update:modelValue']);
// Emitting visibility changes if needed
function updateVisibility(checked) {
emit('update:modelValue', checked);
}
</script>

<template>
<label class="city-header" :class="{ 'is-hidden': !props.isVisible }">
<input type="checkbox" :checked="props.modelValue" @change="updateVisibility($event.target.checked)">
{{ props.label }}
</label>
</template>
26 changes: 26 additions & 0 deletions components/TopicFilterItem.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<script setup lang="ts">
const props = defineProps({
label: String
});
const emit = defineEmits(['checkAll', 'uncheckAll']);
function checkAllTags() {
emit('checkAll');
}
function uncheckAllTags() {
emit('uncheckAll');
}
</script>

<template>
<div class="county-header">
<span>
<button class="county-input" @click="checkAllTags">+</button>
<button class="county-input" @click="uncheckAllTags">-</button>
{{ label }}
</span>
<slot></slot>
</div>
</template>
Loading

1 comment on commit 80ce557

@vercel
Copy link

@vercel vercel bot commented on 80ce557 Aug 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

rva-rip – ./

rva-rip-git-main-my-team-66f1c945.vercel.app
rva.rip
www.rva.rip
rva-rip-my-team-66f1c945.vercel.app

Please sign in to comment.