diff --git a/src/app/[lang]/DiscoveryFeed.tsx b/src/app/[lang]/DiscoveryFeed.tsx index 446ed69..6ee7c4d 100644 --- a/src/app/[lang]/DiscoveryFeed.tsx +++ b/src/app/[lang]/DiscoveryFeed.tsx @@ -8,6 +8,7 @@ type DiscoveryFeedProps = { locale: string; needsOnboarding: boolean; savedProfileIds: string[]; + currentProfileId?: string; userAvatarUrl?: string; }; @@ -17,6 +18,7 @@ export const DiscoveryFeed = async ({ locale, needsOnboarding, savedProfileIds, + currentProfileId, userAvatarUrl, }: DiscoveryFeedProps) => { let profiles: DiscoveryProfile[] = []; @@ -38,6 +40,7 @@ export const DiscoveryFeed = async ({ needsOnboarding={needsOnboarding} profiles={profiles} savedProfileIds={savedProfileIds} + currentProfileId={currentProfileId} userAvatarUrl={userAvatarUrl} /> ); diff --git a/src/app/[lang]/page.tsx b/src/app/[lang]/page.tsx index 06eb916..9631717 100644 --- a/src/app/[lang]/page.tsx +++ b/src/app/[lang]/page.tsx @@ -20,11 +20,13 @@ export default async function Home({ const user = await getCurrentUser(); let needsOnboarding = false; let savedProfileIds: string[] = []; + let currentProfileId: string | undefined; if (user) { const persistedUser = await upsertDiscordUser(user); const profile = await getProfileByUserId(persistedUser.id); needsOnboarding = !profile; + currentProfileId = profile?.profile.id; savedProfileIds = await listSavedProfileIds(persistedUser.id); } @@ -39,6 +41,7 @@ export default async function Home({ isLoggedIn={isLoggedIn} locale={lang} needsOnboarding={needsOnboarding} + currentProfileId={currentProfileId} userAvatarUrl={user?.avatarUrl} /> } @@ -49,6 +52,7 @@ export default async function Home({ locale={lang} needsOnboarding={needsOnboarding} savedProfileIds={savedProfileIds} + currentProfileId={currentProfileId} userAvatarUrl={user?.avatarUrl} /> diff --git a/src/app/[lang]/saved/SavedRouteClient.tsx b/src/app/[lang]/saved/SavedRouteClient.tsx index d3f16c8..c6e5972 100644 --- a/src/app/[lang]/saved/SavedRouteClient.tsx +++ b/src/app/[lang]/saved/SavedRouteClient.tsx @@ -13,12 +13,14 @@ import { Navbar } from '@/features/Navbar'; type SavedRouteClientProps = { locale: string; profiles: DiscoveryProfile[]; + currentProfileId?: string; userAvatarUrl?: string; }; export const SavedRouteClient = ({ locale, profiles: initialProfiles, + currentProfileId, userAvatarUrl, }: SavedRouteClientProps) => { const router = useRouter(); @@ -65,6 +67,7 @@ export const SavedRouteClient = ({ profiles={profiles} isLoggedIn savedProfileIds={profiles.map((profile) => profile.id)} + currentProfileId={currentProfileId} onSaveProfile={saveProfileRequest} onProfileUnsaved={handleProfileUnsaved} /> diff --git a/src/app/[lang]/saved/page.tsx b/src/app/[lang]/saved/page.tsx index 2e2dcf2..367becb 100644 --- a/src/app/[lang]/saved/page.tsx +++ b/src/app/[lang]/saved/page.tsx @@ -1,5 +1,5 @@ import { redirect } from 'next/navigation'; -import { listSavedProfiles, upsertDiscordUser } from '@/db'; +import { getProfileByUserId, listSavedProfiles, upsertDiscordUser } from '@/db'; import { getCurrentUser } from '@/lib/auth'; import { SavedRouteClient } from './SavedRouteClient'; @@ -16,12 +16,14 @@ export default async function SavedRoute({ } const persistedUser = await upsertDiscordUser(user); + const profile = await getProfileByUserId(persistedUser.id); const savedProfiles = await listSavedProfiles(persistedUser.id); return ( ); diff --git a/src/app/api/saved/route.ts b/src/app/api/saved/route.ts index f06c0f3..1c5ed0b 100644 --- a/src/app/api/saved/route.ts +++ b/src/app/api/saved/route.ts @@ -50,6 +50,14 @@ export const POST = async (request: Request) => { } const user = await upsertDiscordUser(currentUser); + + if (target.profile.userId === user.id) { + return NextResponse.json( + { error: 'Cannot save your own profile' }, + { status: 400 }, + ); + } + await saveProfile(user.id, profileId); return NextResponse.json({ saved: true }); diff --git a/src/db/saved.ts b/src/db/saved.ts index 7054785..541eb14 100644 --- a/src/db/saved.ts +++ b/src/db/saved.ts @@ -1,9 +1,9 @@ import 'server-only'; -import { and, desc, eq } from 'drizzle-orm'; +import { and, desc, eq, ne } from 'drizzle-orm'; import { db } from './client'; import { listPublicProfilesByIds } from './profiles'; -import { savedProfiles } from './schema'; +import { profiles, savedProfiles } from './schema'; const missingSavedProfilesStorageCodes = new Set(['42P01', '42703']); @@ -36,7 +36,8 @@ export const listSavedProfileIds = async (userId: string) => { const rows = await db .select({ profileId: savedProfiles.profileId }) .from(savedProfiles) - .where(eq(savedProfiles.userId, userId)) + .innerJoin(profiles, eq(savedProfiles.profileId, profiles.id)) + .where(and(eq(savedProfiles.userId, userId), ne(profiles.userId, userId))) .orderBy(desc(savedProfiles.createdAt)); return rows.map((row) => row.profileId); diff --git a/src/features/Discovery/DiscoveryPage.tsx b/src/features/Discovery/DiscoveryPage.tsx index 74697c7..34556ef 100644 --- a/src/features/Discovery/DiscoveryPage.tsx +++ b/src/features/Discovery/DiscoveryPage.tsx @@ -42,6 +42,7 @@ type DiscoveryPageProps = { needsOnboarding?: boolean; profiles?: DiscoveryProfile[]; savedProfileIds?: string[]; + currentProfileId?: string; userAvatarUrl?: string; }; @@ -65,6 +66,7 @@ export const DiscoveryPage = ({ needsOnboarding = false, profiles = [], savedProfileIds, + currentProfileId, userAvatarUrl, }: DiscoveryPageProps) => { const router = useRouter(); @@ -306,6 +308,7 @@ export const DiscoveryPage = ({ profiles={pageItems} isLoggedIn={isLoggedIn} savedProfileIds={savedProfileIds} + currentProfileId={currentProfileId} onSaveProfile={saveProfileRequest} emptyState={ hasActiveFilters ? undefined : t('emptyFeedDescription') diff --git a/src/features/Discovery/ProfileCard.stories.tsx b/src/features/Discovery/ProfileCard.stories.tsx index 88f283f..91aaa78 100644 --- a/src/features/Discovery/ProfileCard.stories.tsx +++ b/src/features/Discovery/ProfileCard.stories.tsx @@ -256,8 +256,12 @@ export const SaveAction: Story = { }, play: async ({ args, canvasElement }) => { const canvas = within(canvasElement); + const body = within(canvasElement.ownerDocument.body); - await userEvent.click(canvas.getByRole('button', { name: 'Save profile' })); + await userEvent.click(canvas.getByRole('button', { name: 'Card menu' })); + await userEvent.click( + await body.findByRole('button', { name: 'Save profile' }), + ); await expect(args.onToggleSave).toHaveBeenCalledWith('1'); }, }; @@ -269,9 +273,12 @@ export const Saved: Story = { }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); + const body = within(canvasElement.ownerDocument.body); + + await userEvent.click(canvas.getByRole('button', { name: 'Card menu' })); await expect( - canvas.getByRole('button', { name: 'Remove from saved' }), + await body.findByRole('button', { name: 'Remove from saved' }), ).toBeInTheDocument(); }, }; diff --git a/src/features/Discovery/ProfileCard.tsx b/src/features/Discovery/ProfileCard.tsx index bef9025..a8c7e9b 100644 --- a/src/features/Discovery/ProfileCard.tsx +++ b/src/features/Discovery/ProfileCard.tsx @@ -284,6 +284,7 @@ export const ProfileCard = ({ const handleToggleSave = () => { onToggleSave?.(profile.id); + setIsMenuOpen(false); }; const renderTag = (value: string, key?: string) => ( @@ -393,25 +394,6 @@ export const ProfileCard = ({ {profile.lastBumpRelative} )} - {!isPreview && onToggleSave && ( - - )} {!isPreview && ( @@ -439,6 +421,19 @@ export const ProfileCard = ({ > {t('viewProfile')} + {onToggleSave ? ( + + {isSaved ? t('unsaveProfile') : t('saveProfile')} + + ) : null} {(typeof navigator !== 'undefined' && typeof navigator.share === 'function') || !!onShare ? ( diff --git a/src/features/Discovery/ProfileGrid.tsx b/src/features/Discovery/ProfileGrid.tsx index 7d1aed2..2a2ccab 100644 --- a/src/features/Discovery/ProfileGrid.tsx +++ b/src/features/Discovery/ProfileGrid.tsx @@ -17,6 +17,7 @@ type ProfileGridProps = { emptyState?: ReactNode; isLoggedIn?: boolean; savedProfileIds?: string[]; + currentProfileId?: string; matchCriteria?: MatchCriteria | null; sortByMatchScore?: boolean; onCopyUsername?: (username: string, profileId: string) => void; @@ -81,6 +82,7 @@ export const ProfileGrid = ({ emptyState, isLoggedIn = false, savedProfileIds, + currentProfileId, matchCriteria = null, sortByMatchScore = false, onCopyUsername, @@ -191,26 +193,30 @@ export const ProfileGrid = ({ } }; - const renderProfileCard = ({ profile, index }: ProfileGridItem) => ( - - ); + const renderProfileCard = ({ profile, index }: ProfileGridItem) => { + const canSaveProfile = saveEnabled && profile.id !== currentProfileId; + + return ( + + ); + }; const renderProfileColumns = (columnCount: number, className: string) => (
diff --git a/src/locales/en.json b/src/locales/en.json index c669dda..4d5c77c 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -374,7 +374,7 @@ "title": "Saved profiles", "subtitle": "Profiles you saved to revisit. Private or removed profiles drop off automatically.", "emptyTitle": "No saved profiles yet", - "emptyDescription": "Use the bookmark on any profile card to keep it here for later.", + "emptyDescription": "Use the save action in any profile card menu to keep it here for later.", "emptyAction": "Browse profiles" }, "Onboarding": { diff --git a/src/locales/ja.json b/src/locales/ja.json index 9992938..6912bff 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -374,7 +374,7 @@ "title": "保存したプロフィール", "subtitle": "あとで見返すために保存したプロフィール。非公開や削除されたプロフィールは自動的に表示されなくなります。", "emptyTitle": "保存したプロフィールはまだありません", - "emptyDescription": "プロフィールカードのブックマークを押すと、ここに保存して後で見返せます。", + "emptyDescription": "プロフィールカードメニューの保存アクションを使うと、ここに保存して後で見返せます。", "emptyAction": "プロフィールを見る" }, "Onboarding": {