Skip to content

Commit da19591

Browse files
committed
feat: add Member page with detailed view and navbar search
1 parent a0ddbbd commit da19591

File tree

5 files changed

+390
-13
lines changed

5 files changed

+390
-13
lines changed

frontend/src/api/members.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type {
33
AllMembersFilter,
44
Member,
55
MemberWithContact,
6+
MemberEventTeam,
67
} from "@/dto/members";
78

89
export const getAllMembers = (filters?: AllMembersFilter) =>
@@ -11,3 +12,9 @@ export const getAllMembers = (filters?: AllMembersFilter) =>
1112
});
1213

1314
export const getMe = () => instance.get<MemberWithContact>("/me");
15+
16+
export const getMemberById = (id: string) =>
17+
instance.get<Member>(`/members/${id}`);
18+
19+
export const getMemberParticipations = (id: string) =>
20+
instance.get<MemberEventTeam[]>(`/members/${id}/participations`);

frontend/src/components/CompanyOrSpeakerAutocompleteWithDialog.vue renamed to frontend/src/components/CompanyOrSpeakerOrMemberAutocompleteWithDialog.vue

Lines changed: 93 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -151,17 +151,50 @@
151151
</button>
152152
</div>
153153

154+
<!-- Members section -->
155+
<div v-if="filteredMembers.length > 0">
156+
<div
157+
class="px-3 py-2 text-xs font-semibold text-gray-500 bg-gray-50 border-b"
158+
>
159+
Members
160+
</div>
161+
<button
162+
v-for="member in filteredMembers"
163+
:key="`member-${member.id}`"
164+
type="button"
165+
:class="[
166+
'w-full text-left px-3 py-2 border-b border-gray-100 last:border-b-0 flex items-center gap-3',
167+
getItemIndex(member) === highlightedIndex
168+
? 'bg-blue-50 border-blue-200'
169+
: 'hover:bg-gray-50',
170+
]"
171+
@click="selectMember(member)"
172+
>
173+
<Image
174+
:src="member.img"
175+
:alt="member.name"
176+
class="w-8 h-8 rounded object-cover border flex-shrink-0"
177+
/>
178+
<div class="flex-1 min-w-0">
179+
<div class="font-medium text-gray-900 truncate">
180+
{{ member.name }}
181+
</div>
182+
</div>
183+
</button>
184+
</div>
185+
154186
<!-- No results -->
155187
<div
156188
v-if="
157189
!isLoading &&
158190
filteredCompanies.length === 0 &&
159191
filteredSpeakers.length === 0 &&
192+
filteredMembers.length === 0 &&
160193
searchTerm.trim()
161194
"
162195
class="p-3 text-gray-500 text-center"
163196
>
164-
No companies or speakers found
197+
No companies, speakers or members found
165198
</div>
166199

167200
<!-- Welcome message when no search term -->
@@ -170,11 +203,12 @@
170203
!isLoading &&
171204
!searchTerm.trim() &&
172205
filteredCompanies.length === 0 &&
173-
filteredSpeakers.length === 0
206+
filteredSpeakers.length === 0 &&
207+
filteredMembers.length === 0
174208
"
175209
class="p-3 text-gray-500 text-center"
176210
>
177-
Start typing to search companies and speakers...
211+
Start typing to search companies, speakers and members...
178212
</div>
179213

180214
<!-- Create options -->
@@ -255,6 +289,7 @@ import { ref, computed, watch, nextTick } from "vue";
255289
import { useQuery } from "@pinia/colada";
256290
import { getAllCompanies } from "@/api/companies";
257291
import { getAllSpeakers } from "@/api/speakers";
292+
import { getAllMembers } from "@/api/members";
258293
import { useEventStore } from "@/stores/event";
259294
import { Input } from "@/components/ui/input";
260295
import { Label } from "@/components/ui/label";
@@ -271,8 +306,9 @@ import {
271306
} from "@/components/ui/alert-dialog";
272307
import type { Company } from "@/dto/companies";
273308
import type { Speaker } from "@/dto/speakers";
309+
import type { Member } from "@/dto/members";
274310
275-
type SelectedItem = Company | Speaker;
311+
type SelectedItem = Company | Speaker | Member;
276312
277313
interface Props {
278314
modelValue?: string;
@@ -297,6 +333,7 @@ const props = withDefaults(defineProps<Props>(), {
297333
const emit = defineEmits<{
298334
companySelected: [value: Company];
299335
speakerSelected: [value: Speaker];
336+
memberSelected: [value: Member];
300337
"update:modelValue": [value: string];
301338
companySuccess: [companyId: string];
302339
speakerSuccess: [speakerId: string];
@@ -335,8 +372,14 @@ const { data: speakersData, isLoading: speakersLoading } = useQuery({
335372
enabled: () => !!eventStore.selectedEvent?.id,
336373
});
337374
375+
const { data: membersData, isLoading: membersLoading } = useQuery({
376+
key: () => ["members"],
377+
query: () => getAllMembers({}),
378+
enabled: () => !!eventStore.selectedEvent?.id,
379+
});
380+
338381
const isLoading = computed(
339-
() => companiesLoading.value || speakersLoading.value,
382+
() => companiesLoading.value || speakersLoading.value || membersLoading.value,
340383
);
341384
342385
const filteredCompanies = computed(() => {
@@ -377,12 +420,37 @@ const filteredSpeakers = computed(() => {
377420
.slice(0, 5); // Limit to 5 results
378421
});
379422
423+
const filteredMembers = computed(() => {
424+
if (!membersData.value?.data) return [];
425+
426+
const term = searchTerm.value.toLowerCase();
427+
428+
if (!term) {
429+
// Show recent members when no search term
430+
return membersData.value.data.slice(0, 5);
431+
}
432+
433+
return membersData.value.data
434+
.filter((member: Member) => member.name.toLowerCase().includes(term))
435+
.slice(0, 5);
436+
});
437+
380438
const results = computed(() => [
381-
...filteredCompanies.value,
382-
...filteredSpeakers.value,
439+
...filteredCompanies.value.map((company: Company) => ({
440+
...company,
441+
type: "company",
442+
})),
443+
...filteredSpeakers.value.map((speaker: Speaker) => ({
444+
...speaker,
445+
type: "speaker",
446+
})),
447+
...filteredMembers.value.map((member: Member) => ({
448+
...member,
449+
type: "member",
450+
})),
383451
]);
384452
385-
const getItemIndex = (item: Company | Speaker) => {
453+
const getItemIndex = (item: SelectedItem) => {
386454
return results.value.findIndex((result) => result.id === item.id);
387455
};
388456
@@ -396,6 +464,10 @@ const getItemImage = (item: SelectedItem) => {
396464
return item.imgs.internal || item.imgs.speaker;
397465
}
398466
}
467+
468+
// Member may have an `img` property
469+
if ((item as Member).img) return (item as Member).img;
470+
399471
return "";
400472
};
401473
@@ -440,11 +512,11 @@ const handleKeydown = (event: KeyboardEvent) => {
440512
highlightedIndex.value < results.value.length
441513
) {
442514
const selectedResult = results.value[highlightedIndex.value];
443-
if ("companyName" in selectedResult) {
444-
// It's a speaker
515+
if (selectedResult.type === "speaker") {
445516
selectSpeaker(selectedResult as Speaker);
446-
} else {
447-
// It's a company
517+
} else if (selectedResult.type === "member") {
518+
selectMember(selectedResult as Member);
519+
} else if (selectedResult.type === "company") {
448520
selectCompany(selectedResult as Company);
449521
}
450522
}
@@ -469,6 +541,15 @@ const selectSpeaker = (speaker: Speaker) => {
469541
emit("update:modelValue", speaker.name);
470542
};
471543
544+
const selectMember = (member: Member) => {
545+
selectedItem.value = member;
546+
searchTerm.value = member.name;
547+
showSuggestions.value = false;
548+
highlightedIndex.value = -1;
549+
emit("memberSelected", member);
550+
emit("update:modelValue", member.name);
551+
};
552+
472553
const clearSelection = () => {
473554
selectedItem.value = null;
474555
searchTerm.value = "";

frontend/src/components/Navbar.vue

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ import {
1515
import { useEventStore } from "@/stores/event";
1616
import { useAuthStore } from "@/stores/auth";
1717
import { useRouter } from "vue-router";
18-
import CompanyOrSpeakerAutocompleteWithDialog from "./CompanyOrSpeakerAutocompleteWithDialog.vue";
18+
import CompanyOrSpeakerAutocompleteWithDialog from "./CompanyOrSpeakerOrMemberAutocompleteWithDialog.vue";
1919
import type { Company } from "@/dto/companies";
2020
import type { Speaker } from "@/dto/speakers";
21+
import type { Member } from "@/dto/members";
2122
import { useMagicKeys } from "@vueuse/core";
2223
import Notification from "./navbar/Notification.vue";
2324
@@ -73,6 +74,9 @@ const companySelected = (company: Company) =>
7374
const speakerSelected = (speaker: Speaker) =>
7475
router.push({ name: "speaker", params: { speakerId: speaker.id } });
7576
77+
const memberSelected = (member: Member) =>
78+
router.push({ name: "member", params: { memberId: member.id } });
79+
7680
const keys = useMagicKeys();
7781
const shortcutMac = keys["meta+k"];
7882
const shortcutLinux = keys["ctrl+k"];
@@ -134,6 +138,7 @@ watch(shortcutLinux, () => {
134138
show-create
135139
@company-selected="companySelected"
136140
@speaker-selected="speakerSelected"
141+
@member-selected="memberSelected"
137142
/>
138143

139144
<!-- Desktop Navigation -->
@@ -180,6 +185,7 @@ watch(shortcutLinux, () => {
180185
placeholder="Search"
181186
@company-selected="companySelected"
182187
@speaker-selected="speakerSelected"
188+
@member-selected="memberSelected"
183189
/>
184190

185191
<div class="container mx-auto px-4">

frontend/src/router.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ const router = createRouter({
4141
name: "speaker",
4242
component: () => import("./views/Dashboard/Speakers/SpeakerView.vue"),
4343
},
44+
{
45+
path: "members/:memberId",
46+
name: "member",
47+
component: () => import("./views/Dashboard/Members/MemberView.vue"),
48+
},
4449
{
4550
path: "settings",
4651
name: "settings",

0 commit comments

Comments
 (0)