Skip to content

Commit 14c6424

Browse files
author
marcselman
committed
Improve attendee display by including names and photos
Refactors the attendee handling logic to store and display attendee names and photo URLs directly, eliminating redundant API calls and ensuring accurate user information. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 9b278ad5-3a45-420e-ba83-0847baf9924f Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: cd7c97d3-ebf1-460b-bd16-aa248609e22d Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/04133160-d2cd-42c9-82f0-4e08d800b951/9b278ad5-3a45-420e-ba83-0847baf9924f/szOT2Og Replit-Helium-Checkpoint-Created: true
1 parent 578245a commit 14c6424

File tree

9 files changed

+48
-50
lines changed

9 files changed

+48
-50
lines changed

client/src/components/HeroSection.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Button } from "@/components/ui/button";
22
import { Calendar, Users } from "lucide-react";
33
import { useMemo } from "react";
4-
import { isEmailInList } from "@/lib/email-utils";
4+
import { isEmailInAttendees } from "@/lib/email-utils";
55
import type { ForumEdition, Session } from "@shared/schema";
66

77
import backdrop1 from "@assets/backdrops/RL_09185.jpg";
@@ -33,7 +33,7 @@ export function HeroSection({ edition, sessions, userEmail }: HeroSectionProps)
3333
);
3434

3535
const userRegistrations = userEmail
36-
? sessions.filter((s) => isEmailInList(userEmail, s.attendees)).length
36+
? sessions.filter((s) => isEmailInAttendees(userEmail, s.attendees)).length
3737
: 0;
3838

3939
const formatDate = (dateStr: string) => {

client/src/components/SessionCard.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Button } from "@/components/ui/button";
44
import { Badge } from "@/components/ui/badge";
55
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
66
import { Clock, MapPin, Users, Check, Utensils } from "lucide-react";
7-
import { isEmailInList, isSpeaker } from "@/lib/email-utils";
7+
import { isEmailInAttendees, isSpeaker } from "@/lib/email-utils";
88
import { getInitials } from "@/lib/utils";
99
import type { Session } from "@shared/schema";
1010
import foodDrinkBg from "@assets/image_1768474260490.png";
@@ -44,7 +44,7 @@ export function SessionCard({
4444
onUnregister,
4545
isPending = false,
4646
}: SessionCardProps) {
47-
const isRegistered = userEmail ? isEmailInList(userEmail, session.attendees) : false;
47+
const isRegistered = userEmail ? isEmailInAttendees(userEmail, session.attendees) : false;
4848
const isUserSpeaker = userEmail ? isSpeaker(userEmail, session.speakers) : false;
4949

5050
const formatTime = (dateStr: string) => {

client/src/components/SessionTimeline.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Button } from "@/components/ui/button";
33
import { Badge } from "@/components/ui/badge";
44
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
55
import { Clock, MapPin, Users, Check, Utensils } from "lucide-react";
6-
import { isEmailInList, isSpeaker } from "@/lib/email-utils";
6+
import { isEmailInAttendees, isSpeaker } from "@/lib/email-utils";
77
import { getInitials } from "@/lib/utils";
88
import type { Session } from "@shared/schema";
99
import foodDrinkBg from "@assets/image_1768474260490.png";
@@ -81,7 +81,7 @@ export function SessionTimeline({
8181
<div className="flex-1 space-y-3 pt-1">
8282
{slotSessions.map((session) => {
8383
const isRegistered = userEmail
84-
? isEmailInList(userEmail, session.attendees)
84+
? isEmailInAttendees(userEmail, session.attendees)
8585
: false;
8686
const isUserSpeaker = userEmail
8787
? isSpeaker(userEmail, session.speakers)

client/src/lib/email-utils.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
import type { Speaker } from "@shared/schema";
1+
import type { Speaker, Attendee } from "@shared/schema";
22

33
function getLocalPart(email: string): string {
44
const parts = email.toLowerCase().split("@");
55
return parts.length === 2 ? parts[0] : email.toLowerCase();
66
}
77

8-
export function isEmailInList(email: string, list: string[]): boolean {
8+
export function isEmailInAttendees(email: string, attendees: Attendee[]): boolean {
99
const userLocalPart = getLocalPart(email);
10-
return list.some((listEmail) => getLocalPart(listEmail) === userLocalPart);
10+
return attendees.some((attendee) => getLocalPart(attendee.email) === userLocalPart);
1111
}
1212

1313
export function isSpeaker(email: string, speakers: Speaker[]): boolean {

client/src/pages/Home.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
AlertDialogTitle,
2121
} from "@/components/ui/alert-dialog";
2222
import { Utensils } from "lucide-react";
23-
import { isEmailInList } from "@/lib/email-utils";
23+
import { isEmailInAttendees } from "@/lib/email-utils";
2424
import { findOverlappingSessions } from "@/lib/session-utils";
2525
import { OverlapWarningBanner } from "@/components/OverlapWarningBanner";
2626
import type { ForumData, Session } from "@shared/schema";
@@ -126,7 +126,7 @@ export default function Home() {
126126
if (!newSession) return null;
127127

128128
const userRegisteredSessions = allSessions.filter(
129-
s => s.id !== newSessionId && isEmailInList(userEmail, s.attendees)
129+
s => s.id !== newSessionId && isEmailInAttendees(userEmail, s.attendees)
130130
);
131131

132132
if (userRegisteredSessions.length === 0) return null;
@@ -136,7 +136,7 @@ export default function Home() {
136136

137137
const foodDrinkSessions = allSessions.filter(s =>
138138
(s.categories || []).some(c => c.toLowerCase() === "eten & drinken") &&
139-
!isEmailInList(userEmail, s.attendees) &&
139+
!isEmailInAttendees(userEmail, s.attendees) &&
140140
s.id !== newSessionId
141141
);
142142

@@ -217,7 +217,7 @@ export default function Home() {
217217

218218
const registeredSessions = useMemo(() => {
219219
if (!sessions || !user?.email) return [];
220-
return sessions.filter((s) => isEmailInList(user.email, s.attendees));
220+
return sessions.filter((s) => isEmailInAttendees(user.email, s.attendees));
221221
}, [sessions, user?.email]);
222222

223223
const overlapPairs = useMemo(() => {

client/src/pages/MySessions.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { EmptyState } from "@/components/EmptyState";
88
import { SessionGridSkeleton } from "@/components/LoadingState";
99
import { Button } from "@/components/ui/button";
1010
import { useToast } from "@/hooks/use-toast";
11-
import { isEmailInList } from "@/lib/email-utils";
11+
import { isEmailInAttendees } from "@/lib/email-utils";
1212
import { findOverlappingSessions } from "@/lib/session-utils";
1313
import { OverlapWarningBanner } from "@/components/OverlapWarningBanner";
1414
import { ArrowLeft, Calendar } from "lucide-react";
@@ -50,7 +50,7 @@ export default function MySessions() {
5050
if (!data?.sessions || !user?.email) return [];
5151

5252
return data.sessions
53-
.filter((session) => isEmailInList(user.email, session.attendees))
53+
.filter((session) => isEmailInAttendees(user.email, session.attendees))
5454
.sort((a, b) => new Date(a.startTime).getTime() - new Date(b.startTime).getTime());
5555
}, [data?.sessions, user?.email]);
5656

client/src/pages/SessionDetail.tsx

Lines changed: 13 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -11,31 +11,16 @@ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
1111
import { useToast } from "@/hooks/use-toast";
1212
import { useUser } from "@/context/UserContext";
1313
import { apiRequest, queryClient } from "@/lib/queryClient";
14-
import { isEmailInList, isSpeaker } from "@/lib/email-utils";
14+
import { isEmailInAttendees, isSpeaker } from "@/lib/email-utils";
1515
import { findOverlapsWithSession } from "@/lib/session-utils";
1616
import { formatDisplayName } from "@/lib/name-utils";
1717
import { getInitials } from "@/lib/utils";
18-
import type { Session, ForumData } from "@shared/schema";
18+
import type { Session, ForumData, Attendee } from "@shared/schema";
1919

20-
interface UserInfo {
21-
displayName: string;
22-
email: string;
23-
}
24-
25-
function AttendeeItem({ email, index }: { email: string; index: number }) {
26-
const { data: userInfo, isLoading } = useQuery<UserInfo>({
27-
queryKey: ["/api/users", email],
28-
queryFn: async () => {
29-
const res = await fetch(`/api/users/${encodeURIComponent(email)}`);
30-
if (!res.ok) throw new Error("User not found");
31-
return res.json();
32-
},
33-
staleTime: 1000 * 60 * 60,
34-
});
35-
36-
const displayName = formatDisplayName(userInfo?.displayName || email.split("@")[0]);
37-
const initials = displayName.split(/[\s]+/).filter(Boolean).slice(0, 2).map((w: string) => w[0]).join("").toUpperCase() || email.slice(0, 2).toUpperCase();
38-
const photoUrl = `/api/users/${encodeURIComponent(email)}/photo`;
20+
function AttendeeItem({ attendee, index }: { attendee: Attendee; index: number }) {
21+
const displayName = formatDisplayName(attendee.name);
22+
const initials = displayName.split(/[\s]+/).filter(Boolean).slice(0, 2).map((w: string) => w[0]).join("").toUpperCase() || attendee.email.slice(0, 2).toUpperCase();
23+
const photoUrl = attendee.photoUrl || `/api/users/${encodeURIComponent(attendee.email)}/photo`;
3924

4025
return (
4126
<div className="flex items-center gap-3" data-testid={`attendee-${index}`}>
@@ -45,13 +30,9 @@ function AttendeeItem({ email, index }: { email: string; index: number }) {
4530
{initials}
4631
</AvatarFallback>
4732
</Avatar>
48-
{isLoading ? (
49-
<Skeleton className="h-4 w-32" />
50-
) : (
51-
<span className="text-sm truncate" data-testid={`text-attendee-name-${index}`}>
52-
{displayName}
53-
</span>
54-
)}
33+
<span className="text-sm truncate" data-testid={`text-attendee-name-${index}`}>
34+
{displayName}
35+
</span>
5536
</div>
5637
);
5738
}
@@ -109,7 +90,7 @@ export default function SessionDetail() {
10990

11091
const registeredSessions = useMemo(() => {
11192
if (!forumData?.sessions || !user?.email) return [];
112-
return forumData.sessions.filter((s) => isEmailInList(user.email, s.attendees));
93+
return forumData.sessions.filter((s) => isEmailInAttendees(user.email, s.attendees));
11394
}, [forumData?.sessions, user?.email]);
11495

11596
const overlappingSessions = useMemo(() => {
@@ -222,7 +203,7 @@ export default function SessionDetail() {
222203
);
223204
}
224205

225-
const isRegistered = user?.email ? isEmailInList(user.email, session.attendees) : false;
206+
const isRegistered = user?.email ? isEmailInAttendees(user.email, session.attendees) : false;
226207
const isUserSpeaker = user?.email ? isSpeaker(user.email, session.speakers) : false;
227208
const isPending = registerMutation.isPending || unregisterMutation.isPending;
228209

@@ -416,8 +397,8 @@ export default function SessionDetail() {
416397
Deelnemers ({session.capacity ? `${session.attendees.length}/${session.capacity}` : session.attendees.length})
417398
</h3>
418399
<div className="space-y-3">
419-
{session.attendees.map((email, index) => (
420-
<AttendeeItem key={email} email={email} index={index} />
400+
{session.attendees.map((attendee, index) => (
401+
<AttendeeItem key={attendee.email} attendee={attendee} index={index} />
421402
))}
422403
</div>
423404
</CardContent>

server/microsoft-graph.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -595,7 +595,11 @@ export class MicrosoftGraphService {
595595
a.status.response === "tentativelyAccepted"
596596
)
597597
.filter((a) => a.type.toLowerCase() !== "required")
598-
.map((a) => a.emailAddress.address);
598+
.map((a) => ({
599+
name: formatDisplayName(a.emailAddress.name || a.emailAddress.address.split("@")[0]),
600+
email: a.emailAddress.address,
601+
photoUrl: `/api/users/${encodeURIComponent(a.emailAddress.address)}/photo`,
602+
}));
599603

600604
return {
601605
id: event.id,
@@ -670,7 +674,11 @@ export class MicrosoftGraphService {
670674
a.status.response === "tentativelyAccepted"
671675
)
672676
.filter((a) => a.type.toLowerCase() !== "required")
673-
.map((a) => a.emailAddress.address);
677+
.map((a) => ({
678+
name: formatDisplayName(a.emailAddress.name || a.emailAddress.address.split("@")[0]),
679+
email: a.emailAddress.address,
680+
photoUrl: `/api/users/${encodeURIComponent(a.emailAddress.address)}/photo`,
681+
}));
674682

675683
return {
676684
id: event.id,

shared/schema.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,15 @@ export const speakerSchema = z.object({
99

1010
export type Speaker = z.infer<typeof speakerSchema>;
1111

12+
// Attendee schema - includes name from Outlook invitation
13+
export const attendeeSchema = z.object({
14+
name: z.string(),
15+
email: z.string(),
16+
photoUrl: z.string().optional(),
17+
});
18+
19+
export type Attendee = z.infer<typeof attendeeSchema>;
20+
1221
// Session schema - categories come directly from Outlook
1322
export const sessionSchema = z.object({
1423
id: z.string(),
@@ -21,7 +30,7 @@ export const sessionSchema = z.object({
2130
endTime: z.string(), // ISO datetime
2231
room: z.string(),
2332
speakers: z.array(speakerSchema), // Array of speakers (required attendees)
24-
attendees: z.array(z.string()), // Array of email addresses
33+
attendees: z.array(attendeeSchema), // Array of attendees with name and email
2534
capacity: z.number().optional(), // Maximum number of attendees (from back-matter)
2635
});
2736

0 commit comments

Comments
 (0)