Skip to content

Commit 9576999

Browse files
committed
refactor: moving from dropdownmenu to dedicated chatlistitemdropdown component
Motivated by the need to create a confirmation when deleting a chat
1 parent d4576be commit 9576999

File tree

8 files changed

+163
-216
lines changed

8 files changed

+163
-216
lines changed
File renamed without changes.

app/components/Chat/ChatListItem.vue renamed to app/components/Chat/ChatList/ChatListItem.vue

Lines changed: 7 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script setup lang="ts">
2-
import { dbMarkChatDeleted, dbUpdateChat } from "~/utils/db/local";
2+
import { dbUpdateChat } from "~/utils/db/local";
33
const props = defineProps<{
44
chat: ChatState;
55
}>();
@@ -108,72 +108,11 @@ const chatStore = useChatStore();
108108
/>
109109
</button>
110110
</div>
111-
<DropDownMenu v-else>
112-
<DropDownMenuButton>
113-
<Icon
114-
name="lucide:more-horizontal"
115-
:class="[
116-
'scale-125 chat-list-item-menu-button',
117-
isHovered
118-
? 'text-(--main-color) chat-list-item-menu-button-hover'
119-
: chatStore.currentChatId === chat.id
120-
? 'text-(--bg-color)'
121-
: 'text-(--sub-alt-color)',
122-
]"
123-
/>
124-
</DropDownMenuButton>
125-
<DropDownMenuList>
126-
<DropDownMenuItem
127-
@click="
128-
() => {
129-
const newPinned = !chat.pinned;
130-
chatStore.updateChatMetadata(chat.id, {
131-
pinned: newPinned,
132-
});
133-
dbUpdateChat(chat.id, { pinned: newPinned });
134-
}
135-
"
136-
>
137-
<Icon
138-
v-if="chat.pinned"
139-
name="lucide:pin-off"
140-
class="text-(--main-color) scale-125"
141-
/>
142-
<Icon
143-
v-else
144-
name="lucide:pin"
145-
class="text-(--main-color) scale-125"
146-
/>
147-
{{ chat.pinned ? "Unpin" : "Pin" }}
148-
</DropDownMenuItem>
149-
<DropDownMenuItem
150-
@click="
151-
() => {
152-
isRenaming = true;
153-
nextTick(() => {
154-
inputRef?.focus();
155-
});
156-
}
157-
"
158-
>
159-
<Icon name="lucide:edit" class="text-(--main-color) scale-125" />
160-
Rename
161-
</DropDownMenuItem>
162-
<DropDownMenuItem
163-
@click="
164-
() => {
165-
chatStore.deleteChat(chat.id);
166-
dbMarkChatDeleted(chat.id);
167-
if (chatStore.currentChatId === chat.id) {
168-
navigateTo('/chat');
169-
}
170-
}
171-
"
172-
>
173-
<Icon name="lucide:trash" class="text-(--error-color) scale-125" />
174-
Trash
175-
</DropDownMenuItem>
176-
</DropDownMenuList>
177-
</DropDownMenu>
111+
<ChatListItemDropdown
112+
v-else
113+
:chat="chat"
114+
:is-hovered="isHovered"
115+
@rename="isRenaming = true"
116+
/>
178117
</div>
179118
</template>
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
<script setup lang="ts">
2+
import { dbUpdateChat } from "~/utils/db/local";
3+
4+
const props = defineProps<{
5+
isHovered: boolean;
6+
chat: ChatState;
7+
}>();
8+
const emit = defineEmits(["rename"]);
9+
10+
const dropdownVisible = ref(false);
11+
const dropdownTrigger = ref<HTMLElement | null>(null);
12+
const dropdownMenu = ref<HTMLElement | null>(null);
13+
const dropdownStyle = ref<Record<string, string>>({});
14+
15+
function toggleDropdown() {
16+
dropdownVisible.value = !dropdownVisible.value;
17+
if (dropdownVisible.value) {
18+
nextTick(() => {
19+
setDropdownPosition();
20+
window.addEventListener("scroll", updateDropdownPosition, true);
21+
window.addEventListener("resize", updateDropdownPosition);
22+
document.addEventListener("click", onClickOutside);
23+
});
24+
} else {
25+
window.removeEventListener("scroll", updateDropdownPosition, true);
26+
window.removeEventListener("resize", updateDropdownPosition);
27+
document.removeEventListener("click", onClickOutside);
28+
}
29+
}
30+
31+
function setDropdownPosition() {
32+
if (dropdownTrigger.value) {
33+
const rect = dropdownTrigger.value.getBoundingClientRect();
34+
dropdownStyle.value = {
35+
position: "fixed",
36+
top: `${rect.bottom}px`,
37+
left: `${rect.left}px`,
38+
zIndex: "9999",
39+
};
40+
}
41+
}
42+
43+
function updateDropdownPosition() {
44+
if (dropdownVisible.value) {
45+
setDropdownPosition();
46+
}
47+
}
48+
49+
function onClickOutside(e: MouseEvent) {
50+
if (
51+
dropdownMenu.value &&
52+
!dropdownMenu.value.contains(e.target as Node) &&
53+
dropdownTrigger.value &&
54+
!dropdownTrigger.value.contains(e.target as Node)
55+
) {
56+
dropdownVisible.value = false;
57+
window.removeEventListener("scroll", updateDropdownPosition, true);
58+
window.removeEventListener("resize", updateDropdownPosition);
59+
document.removeEventListener("click", onClickOutside);
60+
}
61+
}
62+
63+
onBeforeUnmount(() => {
64+
window.removeEventListener("scroll", updateDropdownPosition, true);
65+
window.removeEventListener("resize", updateDropdownPosition);
66+
document.removeEventListener("click", onClickOutside);
67+
});
68+
69+
const isDeleting = ref(false);
70+
71+
const chatStore = useChatStore();
72+
</script>
73+
74+
<template>
75+
<div class="relative">
76+
<div
77+
ref="dropdownTrigger"
78+
class="flex items-center cursor-pointer"
79+
:class="{
80+
'opacity-0': !props.isHovered && !dropdownVisible,
81+
'opacity-100': props.isHovered || dropdownVisible,
82+
}"
83+
@mousedown.prevent.stop="toggleDropdown"
84+
@dblclick.prevent.stop
85+
>
86+
<Icon name="lucide:more-horizontal" class="scale-125" />
87+
</div>
88+
89+
<!-- popup -->
90+
<teleport to="body">
91+
<div
92+
v-if="dropdownVisible"
93+
ref="dropdownMenu"
94+
class="fixed z-[9999] border border-(--sub-color) bg-(--bg-color) rounded-lg shadow-lg"
95+
:style="dropdownStyle"
96+
>
97+
<div
98+
class="w-full flex items-center gap-2 p-2 m-0! text-left cursor-pointer hover:opacity-60 h-[40px]"
99+
@click="
100+
() => {
101+
chatStore.updateChatMetadata(props.chat.id, {
102+
pinned: !props.chat.pinned,
103+
});
104+
dbUpdateChat(props.chat.id, { pinned: !props.chat.pinned });
105+
}
106+
"
107+
>
108+
<Icon
109+
:name="props.chat.pinned ? 'lucide:pin-off' : 'lucide:pin'"
110+
class="scale-125"
111+
/>
112+
{{ props.chat.pinned ? "Unpin" : "Pin" }}
113+
</div>
114+
<div
115+
class="w-full flex items-center gap-2 p-2 m-0! text-left cursor-pointer hover:opacity-60 h-[40px]"
116+
@click="emit('rename')"
117+
>
118+
<Icon name="lucide:edit" class="scale-125" />
119+
Rename
120+
</div>
121+
<div
122+
v-if="!isDeleting"
123+
class="w-full flex items-center gap-2 p-2 m-0! text-left cursor-pointer hover:opacity-60 h-[40px]"
124+
@click.stop.prevent="
125+
() => {
126+
isDeleting = true;
127+
}
128+
"
129+
>
130+
<Icon name="lucide:trash" class="scale-125 text-(--error-color)" />
131+
Delete
132+
</div>
133+
<div v-else class="w-full flex">
134+
<div
135+
class="flex items-center justify-center bg-(--error-color) cursor-pointer grow p-1 rounded m-1 mr-0.5 h-[32px]"
136+
@click.stop.prevent="
137+
() => {
138+
chatStore.deleteChat(props.chat.id);
139+
dbUpdateChat(props.chat.id, { deleted: true });
140+
isDeleting = false;
141+
}
142+
"
143+
>
144+
<Icon name="lucide:trash-2" class="scale-125 text-(--bg-color)" />
145+
</div>
146+
<div
147+
class="flex items-center justify-center bg-(--main-color) cursor-pointer grow-3 p-1 rounded m-1 ml-0.5"
148+
@click.stop.prevent="isDeleting = false"
149+
>
150+
<Icon name="lucide:x" class="scale-125 text-(--bg-color)" />
151+
</div>
152+
</div>
153+
</div>
154+
</teleport>
155+
</div>
156+
</template>

app/components/DropDownMenu/DropDownMenu.vue

Lines changed: 0 additions & 50 deletions
This file was deleted.

app/components/DropDownMenu/DropDownMenuButton.vue

Lines changed: 0 additions & 21 deletions
This file was deleted.

app/components/DropDownMenu/DropDownMenuItem.vue

Lines changed: 0 additions & 9 deletions
This file was deleted.

app/components/DropDownMenu/DropDownMenuList.vue

Lines changed: 0 additions & 68 deletions
This file was deleted.

0 commit comments

Comments
 (0)