Implement snoozing items

This commit is contained in:
thepaperpilot 2024-04-29 21:54:54 -05:00
parent 6530de4bf2
commit bfd117ff20
12 changed files with 461 additions and 55 deletions

BIN
bun.lockb

Binary file not shown.

View file

@ -12,5 +12,6 @@
<div id="app" class="dark bg-gray-100"></div> <div id="app" class="dark bg-gray-100"></div>
<div id="modal-root" class="dark"></div> <div id="modal-root" class="dark"></div>
<script type="module" src="/src/main.ts"></script> <script type="module" src="/src/main.ts"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/flowbite/2.3.0/datepicker.min.js"></script>
</body> </body>
</html> </html>

View file

@ -12,6 +12,7 @@
"@fortawesome/fontawesome-svg-core": "^6.5.1", "@fortawesome/fontawesome-svg-core": "^6.5.1",
"@fortawesome/free-solid-svg-icons": "^6.5.1", "@fortawesome/free-solid-svg-icons": "^6.5.1",
"@fortawesome/vue-fontawesome": "latest-3", "@fortawesome/vue-fontawesome": "latest-3",
"@vitejs/plugin-vue-jsx": "^3.1.0",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.19",
"flowbite": "^2.3.0", "flowbite": "^2.3.0",
"postcss": "^8.4.38", "postcss": "^8.4.38",

View file

@ -1,10 +1,10 @@
<template> <template>
<span class="text-xl mx-4 group"> <div class="text-xl mx-4 m-2 group">
{{ name }} {{ name }}
<RouterLink :to="`/settings/category/${id}`"> <RouterLink :to="`/settings/category/${id}`">
<FontAwesomeIcon :icon="faGear" class="hidden group-hover:inline" /> <FontAwesomeIcon :icon="faGear" class="hidden group-hover:inline" />
</RouterLink> </RouterLink>
</span> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">

View file

@ -1,53 +1,84 @@
<template> <template>
<div v-if="Object.keys(sourceItem.threads).length === 1 && '0' in sourceItem.threads" <div v-if="Object.keys(sourceItem.threads).length === 1 && '0' in sourceItem.threads" class="relative">
class="p-4 border-2 -mt-2px cursor-pointer bg-gray-200 border-gray-800" :class="isSelected(0) ? 'selected shadow-md scale-105' : ''" @click="emits('select', 0)"> <div :style="style()" class="p-4 border-2 -mt-2px cursor-pointer bg-gray-200 border-gray-800 select-none relative z-20" :class="isSelected(0) ? 'selected shadow-md -mx-2' : ''" @click="select(0)" @mousedown="e => startDrag(e)" @touchstart="e => startDrag(e)">
<div class="flex items-center"> <div class="flex items-center">
<Avatar v-if="getContact(0)" v-bind="getContact(0)!" class="mr-2" /> <Avatar v-if="getContact(0)" v-bind="getContact(0)!" class="mr-2" />
<span class="bg-gray-300 px-2 py-1 rounded-2xl block overflow-hidden text-nowrap text-ellipsis">{{ item.threads[0].preview }}</span> <span class="bg-gray-300 px-2 py-1 rounded-2xl block overflow-hidden text-nowrap text-ellipsis">{{ item.threads[0].preview }}</span>
<span class="grow" /> <span class="grow" />
<span v-if="item.threads[0].count > 1" class="inline-flex items-center justify-center w-3 h-3 p-3 ms-3 text-sm font-medium text-blue-800 bg-blue-100 rounded-full dark:bg-blue-900 dark:text-blue-300">{{ item.threads[0].count }}</span> <span v-if="item.threads[0].count > 1" class="inline-flex items-center justify-center w-3 h-3 p-3 ms-3 text-sm font-medium text-blue-800 bg-blue-100 rounded-full dark:bg-blue-900 dark:text-blue-300">{{ item.threads[0].count }}</span>
</div> </div>
<div class="flex -mb-2 text-gray-500 text-sm font-normal">
<span class="grow">{{ sourceItem.title }}</span>
<span>{{ source.name }}</span>
</div>
</div>
<template v-else>
<div class="p-4 border-2 -mt-2px bg-gray-200 border-gray-800">
<div class="flex -mb-2 text-gray-500 text-sm font-normal"> <div class="flex -mb-2 text-gray-500 text-sm font-normal">
<span class="grow">{{ sourceItem.title }}</span> <span class="grow">{{ sourceItem.title }}</span>
<span>{{ source.name }}</span> <span>{{ source.name }}</span>
</div> </div>
</div> </div>
<div v-for="(thread, id) in item.threads" class="p-4 border-2 -mt-2px cursor-pointer bg-slate-300 border-gray-800" :class="isSelected(id) ? 'selected shadow-md -mx-2 thread' : ''" @click="emits('select', parseInt(id as unknown as string))"> <component :is="actionIndicators" />
<div class="flex items-center"> </div>
<Avatar v-if="getContact(id)" v-bind="getContact(id)!" class="mr-2" /> <template v-else>
<span class="bg-gray-100 px-2 py-1 rounded-2xl block overflow-hidden text-nowrap text-ellipsis">{{ thread.preview }}</span> <div class="relative">
<span class="grow" /> <div class="p-4 border-2 -mt-2px bg-gray-200 border-gray-800 select-none relative z-20" :style="style()" @mousemove="drag" @mousedown="e => startDrag(e)" @mouseup="endDrag" @touchmove="drag" @touchstart="e => startDrag(e)" @touchend="endDrag">
<span v-if="thread.count > 1" class="inline-flex items-center justify-center w-3 h-3 p-3 ms-3 text-sm font-medium text-blue-800 bg-blue-100 rounded-full dark:bg-blue-900 dark:text-blue-300">{{ thread.count }}</span> <div class="flex -mb-2 text-gray-500 text-sm font-normal">
<span class="grow">{{ sourceItem.title }}</span>
<span>{{ source.name }}</span>
</div>
</div> </div>
<div class="flex -mb-2 text-gray-500 text-sm font-normal"> <component :is="actionIndicators" />
<span class="grow">{{ sourceItem.threads[id].title }}</span> </div>
<div v-for="(thread, id) in activeThreads" class="relative">
<div class="p-4 border-2 -mt-2px cursor-pointer bg-slate-300 border-gray-800 select-none relative z-20" :class="isSelected(id) ? 'selected shadow-md -mx-2 thread' : ''" :style="style(parseInt(id as unknown as string))" @click="select(parseInt(id as unknown as string))" @mousemove="drag" @mousedown="e => startDrag(e, id as unknown as string)" @mouseup="endDrag" @touchmove="drag" @touchstart="e => startDrag(e, id as unknown as string)" @touchend="endDrag">
<div class="flex items-center">
<Avatar v-if="getContact(id)" v-bind="getContact(id)!" class="mr-2" />
<span class="bg-gray-100 px-2 py-1 rounded-2xl block overflow-hidden text-nowrap text-ellipsis">{{ thread.preview }}</span>
<span class="grow" />
<span v-if="thread.count > 1" class="inline-flex items-center justify-center w-3 h-3 p-3 ms-3 text-sm font-medium text-blue-800 bg-blue-100 rounded-full dark:bg-blue-900 dark:text-blue-300">{{ thread.count }}</span>
</div>
<div class="flex -mb-2 text-gray-500 text-sm font-normal">
<span class="grow">{{ sourceItem.threads[id].title }}</span>
</div>
</div> </div>
<component :is="actionIndicators" />
</div> </div>
</template> </template>
</template> </template>
<script setup lang="ts"> <script setup lang="tsx">
import { computed } from 'vue'; import { computed, ref } from 'vue';
import type { Item, ThreadRef } from "../state"; import type { Item, ItemThread, ThreadRef } from "../state";
import { sources } from '../state'; import { items, sources } from '../state';
import Avatar from './Avatar.vue'; import Avatar from './Avatar.vue';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import { faCheck, faClockRotateLeft } from '@fortawesome/free-solid-svg-icons';
const props = defineProps<{ const props = defineProps<{
item: Item, item: Item;
selectedThread?: ThreadRef selectedThread?: ThreadRef;
showSnoozed?: boolean;
}>(); }>();
const emits = defineEmits<{ const emits = defineEmits<{
select: [thread: number]; select: [thread: number];
snoozeItem: [thread: number];
deselect: [];
}>(); }>();
const activeThreads = computed(() => Object.keys(props.item.threads).reduce((acc, curr) => {
const thread = props.item.threads[curr as unknown as number];
if ((thread.snoozedUntil == null || Date.now() >= thread.snoozedUntil) !== (props.showSnoozed === true)) {
acc[curr as unknown as number] = thread;
}
return acc;
}, {} as Record<number, ItemThread>));
const dragging = ref(false);
const dragged = ref(false);
const draggingX = ref(0);
const initialMouseX = ref(0);
const draggingIndex = ref<number>();
const style = (id?: number) =>
dragging.value && (id === draggingIndex.value || draggingIndex.value == null) ?
`transform: translateX(${Math.max(-50, Math.min(50, draggingX.value))}px)` :
'';
const source = computed(() => sources.value[props.item.source]); const source = computed(() => sources.value[props.item.source]);
const sourceItem = computed(() => source.value.items[props.item.sourceItem]); const sourceItem = computed(() => source.value.items[props.item.sourceItem]);
@ -71,16 +102,102 @@ function isSelected(thread: number | string) {
} }
return true; return true;
} }
function startDrag(e: MouseEvent | TouchEvent, index?: string) {
dragging.value = true;
dragged.value = false;
draggingX.value = 0;
draggingIndex.value = index == undefined ? undefined : parseInt(index);
initialMouseX.value = "touches" in e ? e.touches[0].clientX : e.clientX;
window.onmousemove = drag;
window.ontouchmove = drag;
window.onmouseup = endDrag;
window.ontouchend = endDrag;
window.onpointerleave = endDrag;
}
function drag(e: MouseEvent | TouchEvent) {
if (dragging.value) {
draggingX.value = ("touches" in e ? e.touches[0].clientX : e.clientX) - initialMouseX.value;
if (Math.abs(draggingX.value) > 10) {
dragged.value = true;
}
}
}
function endDrag(e: MouseEvent | TouchEvent) {
dragging.value = false;
if (dragged.value) {
e.stopPropagation();
}
if (draggingX.value <= -50) {
if (draggingIndex.value == null) {
deleteItem();
} else {
delete props.item.threads[draggingIndex.value];
if (Object.keys(props.item.threads).length === 0) {
deleteItem();
} else if (
props.selectedThread?.source === props.item.source &&
props.selectedThread.sourceItem === props.item.sourceItem &&
props.selectedThread.thread === draggingIndex.value) {
emits("deselect");
}
}
} else if (draggingX.value >= 50) {
emits('snoozeItem', draggingIndex.value ?? -1);
}
window.onmousemove = null;
window.ontouchmove = null;
window.onmouseup = null;
window.ontouchend = null;
window.onpointerleave = null;
}
function select(thread: number) {
if (!dragged.value) {
emits("select", thread);
}
}
function deleteItem() {
const index = items.value.findIndex(item => item.source === props.item.source && item.sourceItem === props.item.sourceItem);
const newItems = items.value.slice();
newItems.splice(index, 1);
items.value = newItems;
if (
props.selectedThread?.source === props.item.source &&
props.selectedThread.sourceItem === props.item.sourceItem) {
emits("deselect");
}
}
const actionIndicators = () => <div class={{
absolute: true,
"top-0": true,
"left-0": true,
"right-0": true,
"bottom-0": true,
"p-2": true,
"pointer-events-none": true,
"z-10": true,
"bg-lime-400": draggingX.value <= -50,
"bg-orange-400": draggingX.value >= 50,
"bg-slate-200": Math.abs(draggingX.value) < 50
}}>
<FontAwesomeIcon icon={faCheck} class="float-right h-full w-8" />
<FontAwesomeIcon icon={faClockRotateLeft} class="float-left h-full w-8" />
</div>
</script> </script>
<style scoped> <style scoped>
.-mt-2px { .-mt-2px {
margin-top: -2px; margin-top: -2px;
} }
.selected:not(:first-child) { :not(:first-child) > .selected {
margin-top: 1rem; margin-top: 1rem;
} }
.selected:not(:last-child) { :not(:last-child) > .selected {
margin-bottom: 1rem; margin-bottom: 1rem;
} }
</style> </style>

View file

@ -1,9 +1,12 @@
<template> <template>
<div class="mb-4 item-group"> <div class="mb-4 item-group relative">
<Item v-for="item in items" <Item v-for="item in items"
:item="item" :item="item"
:selected-thread="selectedThread" :selected-thread="selectedThread"
@select="thread => emits('selectItem', item.source, item.sourceItem, thread)" /> :show-snoozed="showSnoozed"
@select="thread => emits('selectItem', item.source, item.sourceItem, thread)"
@snoozeItem="thread => emits('snoozeItem', item.source, item.sourceItem, thread)"
@deselect="emits('deselect')" />
</div> </div>
</template> </template>
@ -12,11 +15,14 @@ import type { Item as StateItem, ThreadRef } from "../state";
import Item from "./Item.vue"; import Item from "./Item.vue";
const emits = defineEmits<{ const emits = defineEmits<{
selectItem: [source: number, sourceItem: number, thread: number] selectItem: [source: number, sourceItem: number, thread: number];
snoozeItem: [source: number, sourceItem: number, thread: number];
deselect: [];
}>(); }>();
defineProps<{ defineProps<{
items: StateItem[], items: StateItem[];
selectedThread?: ThreadRef selectedThread?: ThreadRef;
showSnoozed?: boolean;
}>(); }>();
</script> </script>

View file

@ -0,0 +1,122 @@
<template>
<Modal :model-value="snoozing" @update:model-value="emits('close')">
<template v-slot:header><div class="text-3xl">Snooze "{{ threadTitle }}"</div></template>
<template v-slot:body>
<div class="flex">
<div class="justify-between flex flex-col grow">
<button class="w-full p-2 border-slate-200 border-2" v-if="new Date().getHours() < 19" @click="snoozeUntil(laterToday)">Later Today<div class="text-slate-400">{{ displayDate(laterToday) }}</div></button>
<button class="w-full p-2 border-slate-200 border-2" v-if="new Date().getDay() < 5" @click="snoozeUntil(tomorrow)">Tomorrow<div class="text-slate-400">{{ displayDate(tomorrow) }}</div></button>
<button class="w-full p-2 border-slate-200 border-2" v-if="new Date().getDay() < 4" @click="snoozeUntil(laterThisWeek)">Later This Week<div class="text-slate-400">{{ displayDate(laterThisWeek) }}</div></button>
<button class="w-full p-2 border-slate-200 border-2" v-if="![0,6].includes(new Date().getDay())" @click="snoozeUntil(weekend)">Weekend<div class="text-slate-400">{{ displayDate(weekend) }}</div></button>
<button class="w-full p-2 border-slate-200 border-2" @click="snoozeUntil(nextWeek)">Next Week<div class="text-slate-400">{{ displayDate(nextWeek) }}</div></button>
</div>
<div class="ml-2 h-full">
<div ref="datepickerRef" inline-datepicker datepicker-title="Custom" class="w-full"/>
</div>
</div>
</template>
</Modal>
</template>
<script setup lang="ts">
import { computed, onMounted, ref, toRef } from 'vue';
import { ThreadRef, items, sources } from '../state';
import Modal from './Modal.vue';
import { initFlowbite } from 'flowbite';
const datepickerRef = ref();
onMounted(() => {
initFlowbite();
datepickerRef.value.addEventListener("changeDate", (e: CustomEvent) => {
snoozeUntil(e.detail.date as Date);
});
});
const props = defineProps<{
snoozing: boolean;
threadRef?: ThreadRef;
}>();
const emits = defineEmits<{
close: [];
}>();
const threadTitle = computed(() => {
const selected = props.threadRef;
if (selected == null) {
return "";
}
const thread = selected.thread === -1 ? 0 : selected.thread;
return sources.value[selected.source].items[selected.sourceItem].threads[thread].title;
});
const laterToday = toRef(() => {
const date = new Date();
date.setHours(19, 0, 0, 0);
return date;
});
const tomorrow = toRef(() => {
const date = new Date();
date.setDate(date.getDate() + 1);
date.setHours(8, 0, 0, 0);
return date;
});
const laterThisWeek = toRef(() => {
const date = new Date();
date.setDate(date.getDate() + 2);
date.setHours(8, 0, 0, 0);
return date;
});
const weekend = toRef(() => {
const date = new Date();
date.setDate(date.getDate() + (12 - date.getDay()) % 7 + 1);
date.setHours(8, 0, 0, 0);
return date;
});
const nextWeek = toRef(() => {
const date = new Date();
date.setDate(date.getDate() + (7 - date.getDay()) % 7 + 1);
date.setHours(8, 0, 0, 0);
return date;
});
const DAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
function displayDate(date: Date) {
const halfDay = date.getHours() >= 12 ? 'PM' : 'AM';
const actualHour = (date.getHours() % 12) + 1;
const time = `${actualHour}:${padLeft(date.getMinutes().toString())} ${halfDay}`;
if (date.getDate() === new Date().getDate()) {
return time;
}
const dayOfWeek = DAY_NAMES[date.getDay()];
return dayOfWeek + " " + time;
}
function padLeft(string: string, delim= "0", length = 2) {
return new Array(length - string.length).fill(delim).join() + string;
}
function snoozeUntil(date: Date) {
const selected = props.threadRef;
if (selected == null) {
return;
}
const newItems = items.value.slice();
const item = newItems.find(item => item.source === selected.source && item.sourceItem === selected.sourceItem);
if (item == null) {
return;
}
if (selected.thread === -1) {
Object.values(item.threads).forEach(thread => thread.snoozedUntil = date.getTime());
} else {
item.threads[selected.thread].snoozedUntil = date.getTime();
}
items.value = newItems;
emits('close');
}
</script>

View file

@ -0,0 +1,102 @@
<template>
<div class="flex h-full">
<div class="grow basis-6/12 p-4 overflow-x-hidden relative">
<div class="text-3xl mb-4">Snoozed</div>
<div v-if="snoozedCategories.length === 0" class="absolute center text-9xl"></div>
<div v-for="category in snoozedCategories">
<CategoryHeader v-bind="category" />
<ItemGroup
:items="category.snoozedItems"
:selected-thread="showThread ? selectedItem : undefined"
:show-snoozed="true"
@select-item="selectItem"
@snooze-item="snoozeItem"
@deselect="close" />
</div>
</div>
<div class="hidden xl:flex bg-slate-200 ml-0 h-screen flex-col overflow-hidden" :class="showThread ? 'p-4 basis-6/12' : 'basis-0'">
<PanelTitle :title="threadTitle" />
<Thread :source="selectedItem?.source ?? 0" :sourceItem="selectedItem?.sourceItem ?? 0" :thread="selectedItem?.thread ?? 0" base-url="/snoozed" />
</div>
<Modal :model-value="showThread" class="block xl:hidden" @update:model-value="close">
<template v-slot:header><div class="text-3xl">{{ threadTitle }}</div></template>
<template v-slot:body><Thread :source="selectedItem?.source ?? 0" :sourceItem="selectedItem?.sourceItem ?? 0" :thread="selectedItem?.thread ?? 0" base-url="/snoozed" /></template>
</Modal>
<SnoozeModal :snoozing="snoozing" :threadRef="snoozingItem" @close="stopSnoozing" />
</div>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { ThreadRef, sources, snoozedCategories } from '../../state';
import CategoryHeader from "../CategoryHeader.vue";
import ItemGroup from "../ItemGroup.vue";
import Modal from '../Modal.vue';
import PanelTitle from '../PanelTitle.vue';
import SnoozeModal from '../SnoozeModal.vue';
import Thread from '../Thread.vue';
const route = useRoute();
const router = useRouter();
const showThread = ref(false);
const selectedItem = ref<ThreadRef | undefined>();
const snoozingItem = ref<ThreadRef | undefined>();
const snoozing = ref(false);
watch([() => route.params.source, () => route.params.sourceItem, () => route.params.thread], ([source, sourceItem, thread]) => {
const sourceInt = parseInt(Array.isArray(source) ? source[0] : source);
const sourceItemInt = parseInt(Array.isArray(sourceItem) ? sourceItem[0] : sourceItem);
const threadInt = parseInt(Array.isArray(thread) ? thread[0] : thread);
if (!isNaN(sourceInt) && !isNaN(sourceItemInt) && !isNaN(threadInt)) {
selectedItem.value = { source: sourceInt, sourceItem: sourceItemInt, thread: threadInt };
showThread.value = true;
} else {
// Intentionally leave selectedItem so animations look right
showThread.value = false;
}
}, { immediate: true });
const threadTitle = computed(() => {
const selected = selectedItem.value;
if (selected == null) {
return "";
}
return sources.value[selected.source].items[selected.sourceItem].threads[selected.thread].title;
});
function selectItem(source: number, sourceItem: number, thread: number) {
const p = route.params;
const sourceInt = parseInt(Array.isArray(p.source) ? p.source[0] : p.source);
const sourceItemInt = parseInt(Array.isArray(p.sourceItem) ? p.sourceItem[0] : p.sourceItem);
const threadInt = parseInt(Array.isArray(p.thread) ? p.thread[0] : p.thread);
if (source === sourceInt && sourceItem === sourceItemInt && thread === threadInt) {
close();
return false;
}
router.push(`/snoozed/${source}/${sourceItem}/${thread}`);
return true;
}
function snoozeItem(source: number, sourceItem: number, thread: number) {
snoozingItem.value = { source, sourceItem, thread };
snoozing.value = true;
}
function close() {
console.log("closing thread")
router.push("/snoozed");
snoozing.value = false;
}
function stopSnoozing() {
if (snoozingItem.value?.source === selectedItem.value?.source &&
snoozingItem.value?.sourceItem === selectedItem.value?.sourceItem &&
(snoozingItem.value?.thread === -1 || snoozingItem.value?.thread === selectedItem.value?.thread)
) {
close();
} else {
snoozing.value = false;
}
}
</script>

View file

@ -1,10 +1,16 @@
<template> <template>
<div class="flex h-full"> <div class="flex h-full">
<div class="grow basis-6/12 p-4 overflow-x-hidden"> <div class="grow basis-6/12 p-4 overflow-x-hidden relative">
<div class="text-3xl mb-4">Todo</div> <div class="text-3xl mb-4">Todo</div>
<div v-if="todoCategories.length === 0" class="absolute center text-9xl"></div>
<div v-for="category in todoCategories"> <div v-for="category in todoCategories">
<CategoryHeader v-bind="category" /> <CategoryHeader v-bind="category" />
<ItemGroup :items="category.activeItems" :selected-thread="showThread ? selectedItem : undefined" @select-item="selectItem" /> <ItemGroup
:items="category.activeItems"
:selected-thread="showThread ? selectedItem : undefined"
@select-item="selectItem"
@snooze-item="snoozeItem"
@deselect="close" />
</div> </div>
</div> </div>
<div class="hidden xl:flex bg-slate-200 ml-0 h-screen flex-col overflow-hidden" :class="showThread ? 'p-4 basis-6/12' : 'basis-0'"> <div class="hidden xl:flex bg-slate-200 ml-0 h-screen flex-col overflow-hidden" :class="showThread ? 'p-4 basis-6/12' : 'basis-0'">
@ -15,24 +21,28 @@
<template v-slot:header><div class="text-3xl">{{ threadTitle }}</div></template> <template v-slot:header><div class="text-3xl">{{ threadTitle }}</div></template>
<template v-slot:body><Thread :source="selectedItem?.source ?? 0" :sourceItem="selectedItem?.sourceItem ?? 0" :thread="selectedItem?.thread ?? 0" base-url="/todo" /></template> <template v-slot:body><Thread :source="selectedItem?.source ?? 0" :sourceItem="selectedItem?.sourceItem ?? 0" :thread="selectedItem?.thread ?? 0" base-url="/todo" /></template>
</Modal> </Modal>
<SnoozeModal :snoozing="snoozing" :threadRef="snoozingItem" @close="stopSnoozing" />
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { ThreadRef, sources, todoCategories } from '../state'; import { ThreadRef, sources, todoCategories } from '../../state';
import CategoryHeader from "./CategoryHeader.vue"; import CategoryHeader from "../CategoryHeader.vue";
import ItemGroup from "./ItemGroup.vue"; import ItemGroup from "../ItemGroup.vue";
import Modal from './Modal.vue'; import Modal from '../Modal.vue';
import PanelTitle from './PanelTitle.vue'; import PanelTitle from '../PanelTitle.vue';
import Thread from './Thread.vue'; import SnoozeModal from '../SnoozeModal.vue';
import Thread from '../Thread.vue';
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const showThread = ref(false); const showThread = ref(false);
const selectedItem = ref<ThreadRef | undefined>(); const selectedItem = ref<ThreadRef | undefined>();
const snoozingItem = ref<ThreadRef | undefined>();
const snoozing = ref(false);
watch([() => route.params.source, () => route.params.sourceItem, () => route.params.thread], ([source, sourceItem, thread]) => { watch([() => route.params.source, () => route.params.sourceItem, () => route.params.thread], ([source, sourceItem, thread]) => {
const sourceInt = parseInt(Array.isArray(source) ? source[0] : source); const sourceInt = parseInt(Array.isArray(source) ? source[0] : source);
const sourceItemInt = parseInt(Array.isArray(sourceItem) ? sourceItem[0] : sourceItem); const sourceItemInt = parseInt(Array.isArray(sourceItem) ? sourceItem[0] : sourceItem);
@ -61,13 +71,31 @@ function selectItem(source: number, sourceItem: number, thread: number) {
const threadInt = parseInt(Array.isArray(p.thread) ? p.thread[0] : p.thread); const threadInt = parseInt(Array.isArray(p.thread) ? p.thread[0] : p.thread);
if (source === sourceInt && sourceItem === sourceItemInt && thread === threadInt) { if (source === sourceInt && sourceItem === sourceItemInt && thread === threadInt) {
close(); close();
return; return false;
} }
router.push(`/todo/${source}/${sourceItem}/${thread}`); router.push(`/todo/${source}/${sourceItem}/${thread}`);
return true;
}
function snoozeItem(source: number, sourceItem: number, thread: number) {
snoozingItem.value = { source, sourceItem, thread };
snoozing.value = true;
} }
function close() { function close() {
console.log("closing thread") console.log("closing thread")
router.push("/todo"); router.push("/todo");
snoozing.value = false;
}
function stopSnoozing() {
if (snoozingItem.value?.source === selectedItem.value?.source &&
snoozingItem.value?.sourceItem === selectedItem.value?.sourceItem &&
(snoozingItem.value?.thread === -1 || snoozingItem.value?.thread === selectedItem.value?.thread)
) {
close();
} else {
snoozing.value = false;
}
} }
</script> </script>

View file

@ -3,11 +3,13 @@ import { createMemoryHistory, createRouter } from "vue-router";
import type { RouteRecordRaw } from "vue-router"; import type { RouteRecordRaw } from "vue-router";
import "./style.css"; import "./style.css";
import App from "./App.vue"; import App from "./App.vue";
import Todo from "./components/Todo.vue"; import Todo from "./components/pages/Todo.vue";
import Snoozed from "./components/pages/Snoozed.vue";
const routes: RouteRecordRaw[] = [ const routes: RouteRecordRaw[] = [
{ path: '/', component: Todo, name: 'home' }, { path: '/', component: Todo, name: 'home' },
{ path: '/todo/:source(\\d+)?/:sourceItem(\\d+)?/:thread(\\d+)?', component: Todo, name: 'todo' }, { path: '/todo/:source(\\d+)?/:sourceItem(\\d+)?/:thread(\\d+)?', component: Todo, name: 'todo' },
{ path: '/snoozed/:source(\\d+)?/:sourceItem(\\d+)?/:thread(\\d+)?', component: Snoozed, name: 'snoozed' },
]; ];
const router = createRouter({ const router = createRouter({

View file

@ -86,6 +86,7 @@ export interface Category {
name: string; name: string;
priority: Priority; priority: Priority;
activeItems: Item[]; activeItems: Item[];
snoozedItems: Item[];
} }
export interface ThreadRef { export interface ThreadRef {
@ -231,19 +232,22 @@ export const items = ref<Item[]>([
} }
]); ]);
export const unsortedItems = ref<Item[]>([]); export const unsortedItems = ref<Item[]>([]);
export const unsortedItemsSnoozed = ref<Item[]>([]);
export const rules = ref<{ category?: string; rules: Rule[] }[]>([]); export const rules = ref<{ category?: string; rules: Rule[] }[]>([]);
export const categories = ref<Category[]>([ export const categories = ref<Category[]>([
{ {
id: 0, id: 0,
name: "Urgent", name: "Urgent",
priority: "urgent", priority: "urgent",
activeItems: [] activeItems: [],
snoozedItems: []
}, },
{ {
id: 1, id: 1,
name: "DMs", name: "DMs",
priority: "notify", priority: "notify",
activeItems: [] activeItems: [],
snoozedItems: []
} }
]); ]);
export const favorites = ref<ThreadRef[]>([ export const favorites = ref<ThreadRef[]>([
@ -260,35 +264,54 @@ export const todoItems = computed(() =>
todoCategories.value todoCategories.value
.reduce((acc, curr) => mergeSortedLists(acc, curr.activeItems), [] as Item[]) .reduce((acc, curr) => mergeSortedLists(acc, curr.activeItems), [] as Item[])
); );
export const todoCategories = computed(() => categories.value.filter(c => ["urgent", "notify", "todo"].includes(c.priority))); export const todoCategories = computed(() => categories.value.filter(c => ["urgent", "notify", "todo"].includes(c.priority) && Object.keys(c.activeItems).length > 0));
export const snoozedCategories = computed(() => categories.value.filter(c => Object.keys(c.snoozedItems).length > 0));
watch( watch(
items, items,
items => { items => {
const mappedItems: Record<number, Item[]> = {}; const mappedItems: Record<number, Item[]> = {};
const mappedItemsSnoozed: Record<number, Item[]> = {};
const unsorted: Item[] = []; const unsorted: Item[] = [];
const unsortedSnoozed: Item[] = [];
items.forEach(item => { items.forEach(item => {
let includeInUnsorted = false; let includeInUnsorted = false;
let includeInUnsortedSnoozed = false;
const includeInCategories = new Set<number>(); const includeInCategories = new Set<number>();
const includeInCategoriesSnoozed = new Set<number>();
Object.values(item.threads).forEach(t => { Object.values(item.threads).forEach(t => {
if (t.category == null) { if (t.snoozedUntil != null && Date.now() < t.snoozedUntil) {
includeInUnsorted = true; if (t.category == null) {
includeInUnsortedSnoozed = true;
} else {
includeInCategoriesSnoozed.add(t.category);
}
} else { } else {
includeInCategories.add(t.category); if (t.category == null) {
includeInUnsorted = true;
} else {
includeInCategories.add(t.category);
}
} }
}); });
if (includeInUnsorted) { if (includeInUnsorted) {
unsorted.push(item); unsorted.push(item);
} }
if (includeInUnsortedSnoozed) {
unsortedSnoozed.push(item);
}
includeInCategories.forEach(cat => mappedItems[cat] = [...(mappedItems[cat] ?? []), item]); includeInCategories.forEach(cat => mappedItems[cat] = [...(mappedItems[cat] ?? []), item]);
includeInCategoriesSnoozed.forEach(cat => mappedItemsSnoozed[cat] = [...(mappedItemsSnoozed[cat] ?? []), item]);
}); });
categories.value.forEach(cat => { categories.value.forEach(cat => {
// Noting here that activeItems must be sorted by most recently updated // Noting here that activeItems must be sorted by most recently updated
cat.activeItems = mappedItems[cat.id]?.sort((a, b) => a.updatedAt - b.updatedAt) ?? []; cat.activeItems = mappedItems[cat.id]?.sort((a, b) => a.updatedAt - b.updatedAt) ?? [];
cat.snoozedItems = mappedItemsSnoozed[cat.id]?.sort((a, b) => a.updatedAt - b.updatedAt) ?? [];
}); });
unsortedItems.value = unsorted; unsortedItems.value = unsorted;
unsortedItemsSnoozed.value = unsortedSnoozed;
}, },
{ immediate: true } { immediate: true }
); );

View file

@ -1,8 +1,12 @@
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [vue()], plugins: [
vue(),
vueJsx()
],
base: "/" base: "/"
}) })