diff --git a/frontend/src/api/members.ts b/frontend/src/api/members.ts index 74da98ad..8bd12fef 100644 --- a/frontend/src/api/members.ts +++ b/frontend/src/api/members.ts @@ -3,6 +3,7 @@ import type { AllMembersFilter, Member, MemberWithContact, + MemberEventTeam, } from "@/dto/members"; export const getAllMembers = (filters?: AllMembersFilter) => @@ -11,3 +12,9 @@ export const getAllMembers = (filters?: AllMembersFilter) => }); export const getMe = () => instance.get("/me"); + +export const getMemberById = (id: string) => + instance.get(`/members/${id}`); + +export const getMemberParticipations = (id: string) => + instance.get(`/members/${id}/participations`); diff --git a/frontend/src/components/CompanyOrSpeakerAutocompleteWithDialog.vue b/frontend/src/components/CompanyOrSpeakerOrMemberAutocompleteWithDialog.vue similarity index 84% rename from frontend/src/components/CompanyOrSpeakerAutocompleteWithDialog.vue rename to frontend/src/components/CompanyOrSpeakerOrMemberAutocompleteWithDialog.vue index 39bd1c48..61dc33d4 100644 --- a/frontend/src/components/CompanyOrSpeakerAutocompleteWithDialog.vue +++ b/frontend/src/components/CompanyOrSpeakerOrMemberAutocompleteWithDialog.vue @@ -151,17 +151,50 @@ + +
+
+ Members +
+ +
+
- No companies or speakers found + No companies, speakers or members found
@@ -170,11 +203,12 @@ !isLoading && !searchTerm.trim() && filteredCompanies.length === 0 && - filteredSpeakers.length === 0 + filteredSpeakers.length === 0 && + filteredMembers.length === 0 " class="p-3 text-gray-500 text-center" > - Start typing to search companies and speakers... + Start typing to search companies, speakers and members... @@ -255,6 +289,7 @@ import { ref, computed, watch, nextTick } from "vue"; import { useQuery } from "@pinia/colada"; import { getAllCompanies } from "@/api/companies"; import { getAllSpeakers } from "@/api/speakers"; +import { getAllMembers } from "@/api/members"; import { useEventStore } from "@/stores/event"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -271,8 +306,9 @@ import { } from "@/components/ui/alert-dialog"; import type { Company } from "@/dto/companies"; import type { Speaker } from "@/dto/speakers"; +import type { Member } from "@/dto/members"; -type SelectedItem = Company | Speaker; +type SelectedItem = Company | Speaker | Member; interface Props { modelValue?: string; @@ -297,6 +333,7 @@ const props = withDefaults(defineProps(), { const emit = defineEmits<{ companySelected: [value: Company]; speakerSelected: [value: Speaker]; + memberSelected: [value: Member]; "update:modelValue": [value: string]; companySuccess: [companyId: string]; speakerSuccess: [speakerId: string]; @@ -335,8 +372,14 @@ const { data: speakersData, isLoading: speakersLoading } = useQuery({ enabled: () => !!eventStore.selectedEvent?.id, }); +const { data: membersData, isLoading: membersLoading } = useQuery({ + key: () => ["members"], + query: () => getAllMembers({}), + enabled: () => !!eventStore.selectedEvent?.id, +}); + const isLoading = computed( - () => companiesLoading.value || speakersLoading.value, + () => companiesLoading.value || speakersLoading.value || membersLoading.value, ); const filteredCompanies = computed(() => { @@ -377,12 +420,37 @@ const filteredSpeakers = computed(() => { .slice(0, 5); // Limit to 5 results }); +const filteredMembers = computed(() => { + if (!membersData.value?.data) return []; + + const term = searchTerm.value.toLowerCase(); + + if (!term) { + // Show recent members when no search term + return membersData.value.data.slice(0, 5); + } + + return membersData.value.data + .filter((member: Member) => member.name.toLowerCase().includes(term)) + .slice(0, 5); +}); + const results = computed(() => [ - ...filteredCompanies.value, - ...filteredSpeakers.value, + ...filteredCompanies.value.map((company: Company) => ({ + ...company, + type: "company", + })), + ...filteredSpeakers.value.map((speaker: Speaker) => ({ + ...speaker, + type: "speaker", + })), + ...filteredMembers.value.map((member: Member) => ({ + ...member, + type: "member", + })), ]); -const getItemIndex = (item: Company | Speaker) => { +const getItemIndex = (item: SelectedItem) => { return results.value.findIndex((result) => result.id === item.id); }; @@ -396,6 +464,10 @@ const getItemImage = (item: SelectedItem) => { return item.imgs.internal || item.imgs.speaker; } } + + // Member may have an `img` property + if ((item as Member).img) return (item as Member).img; + return ""; }; @@ -440,11 +512,11 @@ const handleKeydown = (event: KeyboardEvent) => { highlightedIndex.value < results.value.length ) { const selectedResult = results.value[highlightedIndex.value]; - if ("companyName" in selectedResult) { - // It's a speaker + if (selectedResult.type === "speaker") { selectSpeaker(selectedResult as Speaker); - } else { - // It's a company + } else if (selectedResult.type === "member") { + selectMember(selectedResult as Member); + } else if (selectedResult.type === "company") { selectCompany(selectedResult as Company); } } @@ -469,6 +541,15 @@ const selectSpeaker = (speaker: Speaker) => { emit("update:modelValue", speaker.name); }; +const selectMember = (member: Member) => { + selectedItem.value = member; + searchTerm.value = member.name; + showSuggestions.value = false; + highlightedIndex.value = -1; + emit("memberSelected", member); + emit("update:modelValue", member.name); +}; + const clearSelection = () => { selectedItem.value = null; searchTerm.value = ""; diff --git a/frontend/src/components/Navbar.vue b/frontend/src/components/Navbar.vue index 7ae3c2d2..7c20e5c7 100644 --- a/frontend/src/components/Navbar.vue +++ b/frontend/src/components/Navbar.vue @@ -15,9 +15,10 @@ import { import { useEventStore } from "@/stores/event"; import { useAuthStore } from "@/stores/auth"; import { useRouter } from "vue-router"; -import CompanyOrSpeakerAutocompleteWithDialog from "./CompanyOrSpeakerAutocompleteWithDialog.vue"; +import CompanyOrSpeakerAutocompleteWithDialog from "./CompanyOrSpeakerOrMemberAutocompleteWithDialog.vue"; import type { Company } from "@/dto/companies"; import type { Speaker } from "@/dto/speakers"; +import type { Member } from "@/dto/members"; import { useMagicKeys } from "@vueuse/core"; import Notification from "./navbar/Notification.vue"; @@ -73,6 +74,9 @@ const companySelected = (company: Company) => const speakerSelected = (speaker: Speaker) => router.push({ name: "speaker", params: { speakerId: speaker.id } }); +const memberSelected = (member: Member) => + router.push({ name: "member", params: { memberId: member.id } }); + const keys = useMagicKeys(); const shortcutMac = keys["meta+k"]; const shortcutLinux = keys["ctrl+k"]; @@ -134,6 +138,7 @@ watch(shortcutLinux, () => { show-create @company-selected="companySelected" @speaker-selected="speakerSelected" + @member-selected="memberSelected" /> @@ -180,6 +185,7 @@ watch(shortcutLinux, () => { placeholder="Search" @company-selected="companySelected" @speaker-selected="speakerSelected" + @member-selected="memberSelected" />
diff --git a/frontend/src/components/companies/MembersCompanies.vue b/frontend/src/components/companies/MembersCompanies.vue index 62c3305a..a76e72d3 100644 --- a/frontend/src/components/companies/MembersCompanies.vue +++ b/frontend/src/components/companies/MembersCompanies.vue @@ -26,7 +26,12 @@