diff --git a/.env.example b/.env.example index f68998c0..5c25e5ca 100644 --- a/.env.example +++ b/.env.example @@ -19,7 +19,7 @@ # ============================================================================= APP_NAME=GRIT -APP_BASE_URL=https://grit.social +APP_BASE_URL=http://localhost:5173 # ===================== # Node environment diff --git a/apps/backend/prisma/migrations/20260319093739_add_display_name/migration.sql b/apps/backend/prisma/migrations/20260319093739_add_display_name/migration.sql new file mode 100644 index 00000000..ad103029 --- /dev/null +++ b/apps/backend/prisma/migrations/20260319093739_add_display_name/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "User" ADD COLUMN "displayName" TEXT; diff --git a/apps/backend/prisma/migrations/20260319171659_backfill_display_names/migration.sql b/apps/backend/prisma/migrations/20260319171659_backfill_display_names/migration.sql new file mode 100644 index 00000000..4ee0eb41 --- /dev/null +++ b/apps/backend/prisma/migrations/20260319171659_backfill_display_names/migration.sql @@ -0,0 +1,6 @@ +-- Lowercase any names that weren't stored correctly before normalization was enforced +UPDATE "User" SET "name" = LOWER("name") WHERE "name" != LOWER("name"); + +-- Backfill displayName for users who existed before the displayName column was added +-- (or were created via the broken Google OAuth path that generated mismatched nanoids) +UPDATE "User" SET "displayName" = "name" WHERE "displayName" IS NULL; diff --git a/apps/backend/prisma/schema.prisma b/apps/backend/prisma/schema.prisma index 94056946..548bce96 100644 --- a/apps/backend/prisma/schema.prisma +++ b/apps/backend/prisma/schema.prisma @@ -13,6 +13,7 @@ model User { email String @unique password String? name String @unique + displayName String? avatarKey String? bio String? city String? diff --git a/apps/backend/prisma/seed.ts b/apps/backend/prisma/seed.ts index bc4cafe2..b7b60203 100644 --- a/apps/backend/prisma/seed.ts +++ b/apps/backend/prisma/seed.ts @@ -108,8 +108,8 @@ async function uploadToBucket(bucketName: string, localFilePath: string, origina async function main() { console.log('--- Seeding database...'); - const TEST_RECORD_COUNT = 30; - const DEFAULT_TEST_PASSWORD = 'password123'; + const TEST_RECORD_COUNT = 1000; + const DEFAULT_TEST_PASSWORD = 'Password123'; const AVATAR_BUCKET = 'user-avatars'; const EVENT_BUCKET = 'event-images'; @@ -123,20 +123,35 @@ async function main() { console.log('--- Seeding Users...'); const coreUsers = [ - { email: 'admin@example.com', name: 'Admin', password: 'admin123', image: null, isAdmin: true }, + { + email: 'admin@example.com', + name: 'admin', + displayName: 'Admin', + password: 'admin123', + image: null, + isAdmin: true, + }, { email: 'alice@example.com', - name: 'Alice', + name: 'alice', + displayName: 'Alice', password: DEFAULT_TEST_PASSWORD, image: 'avatar-1.jpg', }, { email: 'bob@example.com', - name: 'Bob', + name: 'bob', + displayName: 'Bob', password: DEFAULT_TEST_PASSWORD, image: 'avatar-2.jpg', }, - { email: 'cindy@example.com', name: 'Cindy', password: DEFAULT_TEST_PASSWORD, image: null }, + { + email: 'cindy@example.com', + name: 'cindy', + displayName: 'Cindy', + password: DEFAULT_TEST_PASSWORD, + image: null, + }, ]; // Process Core Users (Since they need avatars and specific upserts) @@ -144,10 +159,16 @@ async function main() { const hashedPassword = await bcrypt.hash(u.password, 10); const user = await prisma.user.upsert({ where: { email: u.email }, - update: { name: u.name, password: hashedPassword, isConfirmed: true }, + update: { + name: u.name, + displayName: u.displayName, + password: hashedPassword, + isConfirmed: true, + }, create: { email: u.email, name: u.name, + displayName: u.displayName, password: hashedPassword, isConfirmed: true, isAdmin: u.isAdmin ?? false, @@ -178,7 +199,8 @@ async function main() { for (let i = 1; i <= TEST_RECORD_COUNT; i++) { testUsersData.push({ email: `test${String(i)}@example.com`, - name: `TestUser${String(i)}`, + name: `testuser${String(i)}`, + displayName: `TestUser${String(i)}`, password: testHashedPassword, // use pre-calculated hash to save time isConfirmed: true, isAdmin: false, diff --git a/apps/backend/src/auth/auth.schema.ts b/apps/backend/src/auth/auth.schema.ts index 38f76104..9c40968c 100644 --- a/apps/backend/src/auth/auth.schema.ts +++ b/apps/backend/src/auth/auth.schema.ts @@ -6,6 +6,7 @@ export const ResAuthMeSchema = z.object({ id: z.number().int().positive(), email: z.email(), name: z.string().nullable(), + displayName: z.string().nullable().optional(), avatarKey: z.string().nullable().optional(), isConfirmed: z.boolean().default(false), isAdmin: z.boolean().default(false), diff --git a/apps/backend/src/auth/auth.service.ts b/apps/backend/src/auth/auth.service.ts index bcdaa649..9a6da64a 100644 --- a/apps/backend/src/auth/auth.service.ts +++ b/apps/backend/src/auth/auth.service.ts @@ -27,7 +27,13 @@ export class AuthService { // Logic for verifying user credentials async validateUser(loginDto: LoginInput): Promise { - const user = await this.userService.userGetByEmail(loginDto.email); + // Determine if the input is an email or username + const isEmail = loginDto.emailOrUsername.includes('@'); + + // Query by email or username accordingly + const user = isEmail + ? await this.userService.userGetByEmail(loginDto.emailOrUsername) + : await this.userService.userGetByName(loginDto.emailOrUsername); if (!user?.password) { throw new UnauthorizedException('Invalid email or password'); @@ -45,6 +51,7 @@ export class AuthService { id: user.id, email: user.email, name: user.name, + displayName: user.displayName, avatarKey: user.avatarKey, isConfirmed: user.isConfirmed, isAdmin: user.isAdmin, @@ -69,6 +76,7 @@ export class AuthService { id: user.id, email: user.email, name: user.name, + displayName: user.displayName, avatarKey: user.avatarKey, isConfirmed: user.isConfirmed, isAdmin: user.isAdmin, @@ -92,6 +100,8 @@ export class AuthService { while (attempts < maxAttempts) { try { + const nanoId = this.generateNanoId(); + const baseName = `${firstName}-${nanoId}`; const user = await this.prisma.user.upsert({ where: { email }, update: { @@ -100,7 +110,8 @@ export class AuthService { }, create: { email, - name: `${firstName}-${this.generateNanoId()}`, + name: baseName.toLowerCase(), + displayName: baseName, googleId: providerId, isConfirmed: true, password: null, diff --git a/apps/backend/src/auth/tests/auth.e2e-spec.ts b/apps/backend/src/auth/tests/auth.e2e-spec.ts index 111b1517..e729e55e 100644 --- a/apps/backend/src/auth/tests/auth.e2e-spec.ts +++ b/apps/backend/src/auth/tests/auth.e2e-spec.ts @@ -90,7 +90,7 @@ describe('Auth E2E', () => { it('returns user info and accessToken upon successful login', async () => { const res = await request(app.getHttpServer()) .post('/auth/login') - .send({ email: user.email, password: 'Password123' }) + .send({ emailOrUsername: user.email, password: 'Password123' }) .expect(201); expect(res.body).toHaveProperty('accessToken'); @@ -100,7 +100,7 @@ describe('Auth E2E', () => { it('returns 401 for unauthorized access (wrong password)', async () => { const res = await request(app.getHttpServer()) .post('/auth/login') - .send({ email: user.email, password: 'WrongPassword123' }) + .send({ emailOrUsername: user.email, password: 'WrongPassword123' }) .expect(401); expect(res.body.message).toBe('Invalid email or password'); }); @@ -108,7 +108,7 @@ describe('Auth E2E', () => { it('returns 401 for unauthorized access (wrong email)', async () => { const res = await request(app.getHttpServer()) .post('/auth/login') - .send({ email: 'wrong@email.com', password: 'Password123' }) + .send({ emailOrUsername: 'wrong@email.com', password: 'Password123' }) .expect(401); expect(res.body.message).toBe('Invalid email or password'); @@ -127,6 +127,7 @@ describe('Auth E2E', () => { expect(res.body).toStrictEqual({ avatarKey: user.avatarKey, + displayName: user.displayName ?? null, email: user.email, id: user.id, isAdmin: user.isAdmin, diff --git a/apps/backend/src/chat/chat.service.ts b/apps/backend/src/chat/chat.service.ts index 70b7abe4..c09b1556 100644 --- a/apps/backend/src/chat/chat.service.ts +++ b/apps/backend/src/chat/chat.service.ts @@ -50,6 +50,7 @@ export class ChatService { select: { id: true, name: true, + displayName: true, avatarKey: true, }, }, diff --git a/apps/backend/src/event/event.service.ts b/apps/backend/src/event/event.service.ts index b4087555..085cc45d 100644 --- a/apps/backend/src/event/event.service.ts +++ b/apps/backend/src/event/event.service.ts @@ -130,6 +130,7 @@ export class EventService { select: { id: true, name: true, + displayName: true, avatarKey: true, }, }, @@ -182,7 +183,9 @@ export class EventService { location: true, files: true, attendees: { - select: { user: { select: { id: true, name: true, avatarKey: true } } }, + select: { + user: { select: { id: true, name: true, displayName: true, avatarKey: true } }, + }, }, conversation: { select: { id: true } }, invites: { @@ -301,6 +304,7 @@ export class EventService { select: { id: true, name: true, + displayName: true, avatarKey: true, }, }, @@ -356,6 +360,7 @@ export class EventService { select: { id: true, name: true, + displayName: true, avatarKey: true, }, }, @@ -415,6 +420,7 @@ export class EventService { select: { id: true, name: true, + displayName: true, avatarKey: true, }, }, @@ -468,6 +474,7 @@ export class EventService { select: { id: true, name: true, + displayName: true, avatarKey: true, }, }, diff --git a/apps/backend/src/friends/friends.service.ts b/apps/backend/src/friends/friends.service.ts index f4c89ae9..9542dc1e 100644 --- a/apps/backend/src/friends/friends.service.ts +++ b/apps/backend/src/friends/friends.service.ts @@ -67,8 +67,8 @@ export class FriendsService { receiverId, }, include: { - requester: { select: { id: true, name: true, avatarKey: true } }, - receiver: { select: { id: true, name: true, avatarKey: true } }, + requester: { select: { id: true, name: true, displayName: true, avatarKey: true } }, + receiver: { select: { id: true, name: true, displayName: true, avatarKey: true } }, }, }); @@ -84,8 +84,8 @@ export class FriendsService { orderBy: [{ createdAt: 'desc' }, { id: 'asc' }], take: limit + 1, include: { - requester: { select: { id: true, name: true, avatarKey: true } }, - receiver: { select: { id: true, name: true, avatarKey: true } }, + requester: { select: { id: true, name: true, displayName: true, avatarKey: true } }, + receiver: { select: { id: true, name: true, displayName: true, avatarKey: true } }, }, }); @@ -115,8 +115,8 @@ export class FriendsService { orderBy: [{ createdAt: 'desc' }, { id: 'asc' }], take: limit + 1, include: { - requester: { select: { id: true, name: true, avatarKey: true } }, - receiver: { select: { id: true, name: true, avatarKey: true } }, + requester: { select: { id: true, name: true, displayName: true, avatarKey: true } }, + receiver: { select: { id: true, name: true, displayName: true, avatarKey: true } }, }, }); @@ -148,7 +148,7 @@ export class FriendsService { orderBy: [{ friend: { name: 'asc' } }, { id: 'asc' }], take: limit + 1, include: { - friend: { select: { id: true, name: true, avatarKey: true } }, + friend: { select: { id: true, name: true, displayName: true, avatarKey: true } }, }, }), // Count all friends for this user (ignoring the pagination cursor) @@ -188,8 +188,8 @@ export class FriendsService { const friendRequest = await this.prisma.friendRequest.findFirst({ where: { id: id }, include: { - requester: { select: { id: true, name: true, avatarKey: true } }, - receiver: { select: { id: true, name: true, avatarKey: true } }, + requester: { select: { id: true, name: true, displayName: true, avatarKey: true } }, + receiver: { select: { id: true, name: true, displayName: true, avatarKey: true } }, }, }); @@ -208,7 +208,9 @@ export class FriendsService { userId: userId, friendId: friendRequest.requesterId, }, - include: { friend: { select: { id: true, name: true, avatarKey: true } } }, + include: { + friend: { select: { id: true, name: true, displayName: true, avatarKey: true } }, + }, }), this.prisma.friends.create({ data: { @@ -230,8 +232,8 @@ export class FriendsService { const friendRequest = await this.prisma.friendRequest.findFirst({ where: { id: id }, include: { - requester: { select: { id: true, name: true, avatarKey: true } }, - receiver: { select: { id: true, name: true, avatarKey: true } }, + requester: { select: { id: true, name: true, displayName: true, avatarKey: true } }, + receiver: { select: { id: true, name: true, displayName: true, avatarKey: true } }, }, }); @@ -247,8 +249,8 @@ export class FriendsService { return await this.prisma.friendRequest.delete({ where: { id }, include: { - requester: { select: { id: true, name: true, avatarKey: true } }, - receiver: { select: { id: true, name: true, avatarKey: true } }, + requester: { select: { id: true, name: true, displayName: true, avatarKey: true } }, + receiver: { select: { id: true, name: true, displayName: true, avatarKey: true } }, }, }); } @@ -258,8 +260,8 @@ export class FriendsService { const friendRequest = await this.prisma.friendRequest.findFirst({ where: { id: id }, include: { - requester: { select: { id: true, name: true, avatarKey: true } }, - receiver: { select: { id: true, name: true, avatarKey: true } }, + requester: { select: { id: true, name: true, displayName: true, avatarKey: true } }, + receiver: { select: { id: true, name: true, displayName: true, avatarKey: true } }, }, }); @@ -275,8 +277,8 @@ export class FriendsService { return await this.prisma.friendRequest.delete({ where: { id }, include: { - requester: { select: { id: true, name: true, avatarKey: true } }, - receiver: { select: { id: true, name: true, avatarKey: true } }, + requester: { select: { id: true, name: true, displayName: true, avatarKey: true } }, + receiver: { select: { id: true, name: true, displayName: true, avatarKey: true } }, }, }); } @@ -292,7 +294,7 @@ export class FriendsService { userId: userId, friendId: friendId, }, - include: { friend: { select: { id: true, name: true, avatarKey: true } } }, + include: { friend: { select: { id: true, name: true, displayName: true, avatarKey: true } } }, }); if (!friendship) throw new BadRequestException('Friendship does not exist.'); diff --git a/apps/backend/src/invites/invites.service.ts b/apps/backend/src/invites/invites.service.ts index 7be20524..d19ad9df 100644 --- a/apps/backend/src/invites/invites.service.ts +++ b/apps/backend/src/invites/invites.service.ts @@ -77,8 +77,8 @@ export class InvitesService { }, include: { event: { select: { id: true, title: true, imageKey: true } }, - sender: { select: { id: true, name: true, avatarKey: true } }, - receiver: { select: { id: true, name: true, avatarKey: true } }, + sender: { select: { id: true, name: true, displayName: true, avatarKey: true } }, + receiver: { select: { id: true, name: true, displayName: true, avatarKey: true } }, }, }); @@ -191,8 +191,8 @@ export class InvitesService { where: { id }, include: { event: { select: { id: true, title: true, imageKey: true, isPublic: true } }, - sender: { select: { id: true, name: true, avatarKey: true } }, - receiver: { select: { id: true, name: true, avatarKey: true } }, + sender: { select: { id: true, name: true, displayName: true, avatarKey: true } }, + receiver: { select: { id: true, name: true, displayName: true, avatarKey: true } }, }, }); @@ -243,8 +243,8 @@ export class InvitesService { where: { id }, include: { event: { select: { id: true, title: true, imageKey: true, isPublic: true } }, - sender: { select: { id: true, name: true, avatarKey: true } }, - receiver: { select: { id: true, name: true, avatarKey: true } }, + sender: { select: { id: true, name: true, displayName: true, avatarKey: true } }, + receiver: { select: { id: true, name: true, displayName: true, avatarKey: true } }, }, }); } @@ -254,8 +254,8 @@ export class InvitesService { where: { id }, include: { event: { select: { id: true, title: true, imageKey: true, isPublic: true } }, - sender: { select: { id: true, name: true, avatarKey: true } }, - receiver: { select: { id: true, name: true, avatarKey: true } }, + sender: { select: { id: true, name: true, displayName: true, avatarKey: true } }, + receiver: { select: { id: true, name: true, displayName: true, avatarKey: true } }, }, }); } @@ -274,8 +274,8 @@ export class InvitesService { where: { id }, include: { event: { select: { id: true, title: true, imageKey: true, isPublic: true } }, - sender: { select: { id: true, name: true, avatarKey: true } }, - receiver: { select: { id: true, name: true, avatarKey: true } }, + sender: { select: { id: true, name: true, displayName: true, avatarKey: true } }, + receiver: { select: { id: true, name: true, displayName: true, avatarKey: true } }, }, }); } @@ -292,8 +292,8 @@ export class InvitesService { where: { receiverId: userId, ...cursorFilter }, include: { event: { select: { id: true, title: true, imageKey: true } }, - sender: { select: { id: true, name: true, avatarKey: true } }, - receiver: { select: { id: true, name: true, avatarKey: true } }, + sender: { select: { id: true, name: true, displayName: true, avatarKey: true } }, + receiver: { select: { id: true, name: true, displayName: true, avatarKey: true } }, }, orderBy: [{ createdAt: 'desc' }, { id: 'asc' }], take: limit + 1, @@ -342,8 +342,8 @@ export class InvitesService { }, include: { event: { select: { id: true, title: true, imageKey: true } }, - sender: { select: { id: true, name: true, avatarKey: true } }, - receiver: { select: { id: true, name: true, avatarKey: true } }, + sender: { select: { id: true, name: true, displayName: true, avatarKey: true } }, + receiver: { select: { id: true, name: true, displayName: true, avatarKey: true } }, }, orderBy: [{ createdAt: 'desc' }, { id: 'asc' }], take: limit + 1, diff --git a/apps/backend/src/user/user.service.ts b/apps/backend/src/user/user.service.ts index 09712f0d..3419927d 100644 --- a/apps/backend/src/user/user.service.ts +++ b/apps/backend/src/user/user.service.ts @@ -62,6 +62,7 @@ export class UserService { data: slicedData.map((user) => ({ id: user.id, name: user.name, + displayName: user.displayName, avatarKey: user.avatarKey, bio: user.bio, city: user.city, @@ -146,11 +147,50 @@ export class UserService { }; } + async userGetByName(name: string) { + // Normalize to lowercase for case-insensitive lookup (uses DB index!) + const normalizedName = name.toLowerCase(); + + const user = await this.prisma.user.findUnique({ + where: { name: normalizedName }, + include: { + attending: { + include: { + event: { + include: { location: true }, + }, + }, + }, + }, + }); + + if (!user) { + return null; + } + + return { + ...user, + createdAt: user.createdAt.toISOString(), + attending: user.attending.map((a) => ({ + id: a.event.id, + title: a.event.title, + slug: a.event.slug, + startAt: a.event.startAt.toISOString(), + isOrganizer: a.event.authorId === user.id, + imageKey: a.event.imageKey, + location: a.event.location, + })), + }; + } + async userPost(data: ReqUserPostDto): Promise { const token = randomBytes(32).toString('hex'); + // Normalize username to lowercase for case-insensitive uniqueness + const normalizedName = data.name.toLowerCase(); + // A hard-coded check (since we use the name 'Unknown' for deleted users). - if (data.name.toUpperCase() === 'UNKNOWN') { + if (normalizedName === 'unknown') { throw new ConflictException('Name unknown is reserved for deleted users'); } @@ -163,9 +203,9 @@ export class UserService { throw new ConflictException('Email already in use'); } - // Check if name already exists - const existingName = await this.prisma.user.findFirst({ - where: { name: data.name }, + // Check if name already exists (case-insensitive via exact match on normalized value) + const existingName = await this.prisma.user.findUnique({ + where: { name: normalizedName }, }); if (existingName) { @@ -174,7 +214,8 @@ export class UserService { const user = await this.prisma.user.create({ data: { - name: data.name, + name: normalizedName, + displayName: data.name, // Store original casing for display email: data.email, password: await bcrypt.hash(data.password, 10), isConfirmed: false, @@ -382,18 +423,21 @@ export class UserService { const newData: Prisma.UserUpdateInput = {}; if (data.name !== undefined) { - if (data.name.toUpperCase() === 'UNKNOWN') - throw new ConflictException('Username already taken'); + // Normalize username to lowercase + const normalizedName = data.name.toLowerCase(); + + if (normalizedName === 'unknown') throw new ConflictException('Username already taken'); - // Check if name already exists + // Check if name already exists (case-insensitive via normalized value) const existingName = await tx.user.findFirst({ - where: { name: data.name, id: { not: userId } }, + where: { name: normalizedName, id: { not: userId } }, }); if (existingName) { throw new ConflictException('Username already taken'); } - newData.name = data.name; + newData.name = normalizedName; + newData.displayName = data.name; // Store original casing for display } if (data.bio !== undefined) newData.bio = data.bio; if (data.city !== undefined) newData.city = data.city; @@ -555,6 +599,7 @@ export class UserService { select: { id: true, name: true, + displayName: true, avatarKey: true, }, }, @@ -609,6 +654,7 @@ export class UserService { select: { id: true, name: true, + displayName: true, avatarKey: true, }, }, @@ -707,6 +753,7 @@ export class UserService { select: { id: true, name: true, + displayName: true, avatarKey: true, createdAt: true, bio: true, @@ -745,6 +792,7 @@ export class UserService { return { id: user.id, name: user.name, + displayName: user.displayName, avatarKey: user.avatarKey, createdAt: user.createdAt.toISOString(), bio: null, @@ -758,6 +806,7 @@ export class UserService { return { id: user.id, name: user.name, + displayName: user.displayName, avatarKey: user.avatarKey, createdAt: user.createdAt.toISOString(), bio: user.bio, diff --git a/apps/frontend/src/components/layout/Navbar.tsx b/apps/frontend/src/components/layout/Navbar.tsx index 6b969de2..386ac3ef 100644 --- a/apps/frontend/src/components/layout/Navbar.tsx +++ b/apps/frontend/src/components/layout/Navbar.tsx @@ -41,7 +41,7 @@ export function Navbar() { const isLoggedIn = useAuthStore((s) => !!s.token); const user = useCurrentUserStore((s) => s.user); const isAvatarTransitioning = useCurrentUserStore((s) => s.isAvatarTransitioning); - const displayName = user?.name ?? user?.email ?? 'User'; + const displayName = user?.displayName ?? user?.name ?? user?.email ?? 'User'; const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const [searchOpen, setSearchOpen] = useState(false); diff --git a/apps/frontend/src/components/ui/user-avatar.tsx b/apps/frontend/src/components/ui/user-avatar.tsx index b84d146a..4d2c17ec 100644 --- a/apps/frontend/src/components/ui/user-avatar.tsx +++ b/apps/frontend/src/components/ui/user-avatar.tsx @@ -23,6 +23,7 @@ type AvatarSize = keyof typeof SIZE_CLASSES; interface UserAvatarUser { id?: number | null; name?: string | null; + displayName?: string | null; email?: string | null; avatarKey?: string | null; } @@ -70,7 +71,7 @@ export function UserAvatar({ seed = user.name ?? user.email ?? 'user'; } - const displayName = user.name ?? user.email ?? undefined; + const displayName = user.displayName ?? user.name ?? user.email ?? undefined; return ( diff --git a/apps/frontend/src/components/ui/userCard.tsx b/apps/frontend/src/components/ui/userCard.tsx index e65a4749..8d366150 100644 --- a/apps/frontend/src/components/ui/userCard.tsx +++ b/apps/frontend/src/components/ui/userCard.tsx @@ -4,13 +4,14 @@ import { UserAvatar } from '@/components/ui/user-avatar'; interface UserCardUser { id: number; name: string; + displayName?: string | null; avatarKey?: string | null; createdAt?: string; onlineStatus?: boolean | null | undefined; } export const UserCard = ({ user, actions }: { user: UserCardUser; actions?: React.ReactNode }) => { - const displayName = user.name ?? 'User'; + const displayName = user.displayName ?? user.name ?? 'User'; return ( <> diff --git a/apps/frontend/src/features/chat/ChatBubble.tsx b/apps/frontend/src/features/chat/ChatBubble.tsx index 796733b7..9e2478dc 100644 --- a/apps/frontend/src/features/chat/ChatBubble.tsx +++ b/apps/frontend/src/features/chat/ChatBubble.tsx @@ -29,7 +29,12 @@ const renderMessageWithLinks = (text: string) => { export const ChatBubble = ({ message }: { message: ResChatMessage }) => { const currentUser = useCurrentUserStore((s) => s.user); - const author = message.author ?? { id: null, name: 'Unknown', avatarKey: null }; + const author = message.author ?? { + id: null, + name: 'Unknown', + displayName: 'Unknown', + avatarKey: null, + }; const isFromCurrentUser = currentUser?.id === author.id; const align = isFromCurrentUser ? 'justify-end' : 'justify-start'; const isDeletedUser = author.id === null; @@ -53,10 +58,10 @@ export const ChatBubble = ({ message }: { message: ResChatMessage }) => { {!isFromCurrentUser && (
{isDeletedUser ? ( - {author.name} + {author.displayName ?? author.name} ) : ( - {author.name} + {author.displayName ?? author.name} )}
diff --git a/apps/frontend/src/features/search/GlobalSearch.tsx b/apps/frontend/src/features/search/GlobalSearch.tsx index a053e8b0..2960ef0e 100644 --- a/apps/frontend/src/features/search/GlobalSearch.tsx +++ b/apps/frontend/src/features/search/GlobalSearch.tsx @@ -244,7 +244,7 @@ export function GlobalSearch({ open, onOpenChange }: GlobalSearchProps) { }} > - {user.name} + {user.displayName ?? user.name} {location ? ( {location} diff --git a/apps/frontend/src/pages/events/EventPage.tsx b/apps/frontend/src/pages/events/EventPage.tsx index cfb41c0d..62cc60c2 100644 --- a/apps/frontend/src/pages/events/EventPage.tsx +++ b/apps/frontend/src/pages/events/EventPage.tsx @@ -207,7 +207,7 @@ export const EventPage = () => { className="group min-w-0 max-w-full flex items-center gap-1.5 text-left cursor-pointer" > - {event.author.name} + {event.author.displayName ?? event.author.name} ) : ( diff --git a/apps/frontend/src/pages/events/components/EventCard.tsx b/apps/frontend/src/pages/events/components/EventCard.tsx index 517fd1eb..45dd58ff 100644 --- a/apps/frontend/src/pages/events/components/EventCard.tsx +++ b/apps/frontend/src/pages/events/components/EventCard.tsx @@ -66,7 +66,7 @@ export function EventCard({ event, friendsIds }: EventCardProps) { await userService.attendEvent(event.id); setIsAttending(true); setCountAttending((prev) => prev + 1); - toast.info('You’re going to "' + event.title + '".'); + toast.success('You’re going to "' + event.title + '".'); } catch (error) { toast.error('Something went wrong:' + String(error)); } finally { diff --git a/apps/frontend/src/pages/events/components/EventPageActions.tsx b/apps/frontend/src/pages/events/components/EventPageActions.tsx index 30dd74e1..865f4572 100644 --- a/apps/frontend/src/pages/events/components/EventPageActions.tsx +++ b/apps/frontend/src/pages/events/components/EventPageActions.tsx @@ -73,9 +73,10 @@ export const EventPageActions = ({ // Only filter when there's a search query, otherwise show all loaded friends const friendsToShow = searchQuery - ? invitableFriends.filter((friendship) => - friendship.friend.name.toLowerCase().includes(searchQuery.toLowerCase()) - ) + ? invitableFriends.filter((friendship) => { + const displayName = friendship.friend.displayName ?? friendship.friend.name; + return displayName.toLowerCase().includes(searchQuery.toLowerCase()); + }) : invitableFriends; const isSearching = searchQuery.trim().length > 0; @@ -266,7 +267,9 @@ export const EventPageActions = ({ )} > - {friendship.friend.name} + + {friendship.friend.displayName ?? friendship.friend.name} +