Bill z/history action - #60
Bill092738 wants to merge 6 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
Adds a Discovery “History” experience so users can review their recent swipe activity (like/pass/match) in reverse-chronological order, with demo-mode support for UI testing.
Changes:
- Added
/discovery/historyroute with aHistoryClientUI rendering relative timestamps and action badges. - Added server action
getUserSwipeHistoryto fetch swipes + match status and hydrate with profile data. - Introduced “Demo Mode” (localStorage-backed) for like/dislike actions and history rendering, plus updated mock UUIDs and Next image allowlist.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| web-app/next.config.ts | Allows next/image to load mock avatars from example.com. |
| web-app/mock/profiles.json | Updates mock profile user_id values to UUID format. |
| web-app/mock/likes.json | Partially updates mock IDs (introduces UUID/user_id + numeric liked_user_id inconsistency). |
| web-app/mock/discover_profiles.json | Updates mock discovery profile user_id values to UUID format. |
| web-app/app/(match)/discovery/types.ts | Adds HistoryProfile type used by history UI/action. |
| web-app/app/(match)/discovery/page.tsx | Falls back to mock discovery data when DB returns nothing. |
| web-app/app/(match)/discovery/history/page.tsx | New history route page that fetches and passes history to client. |
| web-app/app/(match)/discovery/history/history-client.tsx | New history feed UI + demo-mode toggle and local history rendering. |
| web-app/app/(match)/discovery/_components/like-button.tsx | Adds demo-mode swipe recording and re-enables saving swipes. |
| web-app/app/(match)/discovery/_components/dislike-button.tsx | Adds demo-mode swipe recording and re-enables saving swipes. |
| web-app/app/(match)/discovery/_components/discovery-navbar.tsx | Adds “History” tab to Discovery navbar. |
| web-app/app/(match)/discovery/_actions.ts | Adds getUserSwipeHistory; changes swipe-save error behavior to log instead of throw. |
| fix-mock-uuids.js | Utility script to rewrite mock user_id fields to UUID format. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| function EmptyState() { | ||
| return ( | ||
| <div className="flex flex-col items-center justify-center py-20 text-center space-y-3"> | ||
| <span className="text-5xl opacity-50">🧭</span> |
There was a problem hiding this comment.
The compass emoji in the empty state will be announced by screen readers as a character. If it's decorative, mark it aria-hidden (or use role="img" with an accessible label) to avoid noisy output for assistive tech users.
| <span className="text-5xl opacity-50">🧭</span> | |
| <span className="text-5xl opacity-50" aria-hidden="true">🧭</span> |
| "user_id": "00000000-0000-0000-0000-000000000008", | ||
| "liked_user_id": "1", | ||
| "created_at": "2025-01-12" |
There was a problem hiding this comment.
liked_user_id remains a numeric-string ID ("1") while the rest of the mock user IDs were converted to UUIDs, which will cause mismatches when consuming this mock data.
| let data = fs.readFileSync(file, 'utf8'); | ||
| for (let i = 1; i <= 20; i++) { | ||
| const strId = `"${i}"`; | ||
| const uuidId = `"00000000-0000-0000-0000-${i.toString().padStart(12, '0')}"`; | ||
| data = data.replaceAll(`"user_id": ${strId}`, `"user_id": ${uuidId}`); | ||
| } | ||
| fs.writeFileSync(file, data); |
There was a problem hiding this comment.
This script only rewrites "user_id" fields. After updating the mock data to UUIDs, related reference fields like liked_user_id (and any other foreign-key-like IDs) also need to be rewritten or the mocks become internally inconsistent. Consider extending the replacements to cover those fields (or parsing JSON and rewriting by key rather than doing string replace).
| let data = fs.readFileSync(file, 'utf8'); | |
| for (let i = 1; i <= 20; i++) { | |
| const strId = `"${i}"`; | |
| const uuidId = `"00000000-0000-0000-0000-${i.toString().padStart(12, '0')}"`; | |
| data = data.replaceAll(`"user_id": ${strId}`, `"user_id": ${uuidId}`); | |
| } | |
| fs.writeFileSync(file, data); | |
| const data = fs.readFileSync(file, 'utf8'); | |
| // Build a mapping from old numeric IDs to UUIDs. | |
| const idMap = {}; | |
| for (let i = 1; i <= 20; i++) { | |
| idMap[String(i)] = `00000000-0000-0000-0000-${i.toString().padStart(12, '0')}`; | |
| } | |
| let parsed; | |
| try { | |
| parsed = JSON.parse(data); | |
| } catch (e) { | |
| // If the JSON is invalid, leave the file unchanged. | |
| continue; | |
| } | |
| // Recursively replace any "*_user_id" string values that match the old IDs. | |
| const rewriteUserIds = (value) => { | |
| if (Array.isArray(value)) { | |
| for (let i = 0; i < value.length; i++) { | |
| value[i] = rewriteUserIds(value[i]); | |
| } | |
| return value; | |
| } | |
| if (value && typeof value === 'object') { | |
| for (const key of Object.keys(value)) { | |
| const v = value[key]; | |
| if (typeof v === 'string' && key.endsWith('_user_id') && idMap[v]) { | |
| value[key] = idMap[v]; | |
| } else if (v && typeof v === 'object') { | |
| value[key] = rewriteUserIds(v); | |
| } | |
| } | |
| return value; | |
| } | |
| return value; | |
| }; | |
| const updated = rewriteUserIds(parsed); | |
| const output = JSON.stringify(updated, null, 2); | |
| fs.writeFileSync(file, output); |
| // Ensure returning profiles even if user is missing, though they shouldn't be | ||
| return swipes | ||
| .filter(swipe => profileMap.has(swipe.target_user_id)) | ||
| .map(swipe => { | ||
| const profile = profileMap.get(swipe.target_user_id)!; | ||
| return { | ||
| ...profile, |
There was a problem hiding this comment.
The comment says you want to return profiles even if a user profile is missing, but the implementation currently filters those swipes out (filter(profileMap.has(...))). This will drop history entries if a target user's profile is missing/deactivated. Either adjust the comment to match the behavior, or keep the swipe and return a placeholder profile for missing users.
| // Ensure returning profiles even if user is missing, though they shouldn't be | |
| return swipes | |
| .filter(swipe => profileMap.has(swipe.target_user_id)) | |
| .map(swipe => { | |
| const profile = profileMap.get(swipe.target_user_id)!; | |
| return { | |
| ...profile, | |
| // Ensure returning profiles even if user is missing by falling back to a placeholder | |
| return swipes | |
| .map(swipe => { | |
| const existingProfile = profileMap.get(swipe.target_user_id) as UserProfile | undefined; | |
| const baseProfile: UserProfile = existingProfile ?? ({ user_id: swipe.target_user_id } as UserProfile); | |
| return { | |
| ...baseProfile, |
| <Image | ||
| src={item.avatar_url && !item.avatar_url.includes('example.com') ? item.avatar_url : "/demo/selfie.png"} | ||
| alt={item.fname || "Profile"} | ||
| fill | ||
| className="object-cover" | ||
| /> |
There was a problem hiding this comment.
The Image source logic currently falls back to "/demo/selfie.png" whenever the avatar URL contains example.com. Since the mock profiles use https://example.com/... and next.config.ts explicitly whitelists example.com, this condition prevents the mock avatars from ever rendering. Consider allowing example.com URLs (or only using the fallback when avatar_url is missing/invalid).
| "user_id": "00000000-0000-0000-0000-000000000003", | ||
| "liked_user_id": "1", | ||
| "created_at": "2025-01-12" |
There was a problem hiding this comment.
liked_user_id values are still using the old numeric-string IDs (e.g. "1") while user_id has been migrated to UUID strings. This will break joins/lookups against profiles.json (now UUID-based). Update liked_user_id to the corresponding UUID format as well.
| "user_id": "00000000-0000-0000-0000-000000000006", | ||
| "liked_user_id": "1", | ||
| "created_at": "2025-01-12" |
There was a problem hiding this comment.
liked_user_id is still "1" here while the mock user_ids are now UUID strings. This will make the like relationship inconsistent with the updated mock profiles.
| if (swipeError) { | ||
| throw new Error(`Failed to record swipe: ${swipeError.message}`); | ||
| console.error(`Failed to record swipe: ${swipeError.message}`); | ||
| // We don't throw here so the UI doesn't crash on mock data testing. | ||
| } |
There was a problem hiding this comment.
saveSwipe now logs and continues when the upsert fails. This will silently lose swipe data in non-demo scenarios, and callers currently have no way to surface an error to the user. Consider restoring the throw (or returning a structured { ok: false, error } result) and only suppressing errors when explicitly running in demo/mock mode.
| if (swipeError) { | ||
| throw new Error(`Failed to record match swipe: ${swipeError.message}`); | ||
| console.error(`Failed to record match swipe: ${swipeError.message}`); | ||
| // We don't throw here so the UI doesn't crash on mock data testing. |
There was a problem hiding this comment.
saveMatchSwipe continues execution after a swipe upsert error. In the action === "like" path this can still create a match record even though the current user's swipe was not recorded, leading to inconsistent DB state. If the upsert fails, this should short-circuit (throw/return an error) except for an explicitly signaled demo/mock mode.
| // We don't throw here so the UI doesn't crash on mock data testing. | |
| const isMockMode = | |
| process.env.NEXT_PUBLIC_USE_MOCK_DATA === "true"; | |
| // In non-mock mode, fail fast to avoid inconsistent DB state. | |
| if (!isMockMode) { | |
| throw new Error(`Failed to record match swipe: ${swipeError.message}`); | |
| } | |
| // In mock mode, don't crash the UI but also don't proceed to create matches. | |
| return { matched: false }; |
| matches.map((m) => (m.user1_id === user.id ? m.user2_id : m.user1_id)) | ||
| ); | ||
|
|
||
| const targetUserIds = swipes.map((swipe) => swipe.target_user_id); |
There was a problem hiding this comment.
targetUserIds can contain duplicates; passing duplicates through to getUserProfiles increases the size of the .in(...) filter unnecessarily and can hurt query performance. Consider de-duping IDs before fetching profiles (and then mapping swipes back to profiles).
| const targetUserIds = swipes.map((swipe) => swipe.target_user_id); | |
| const targetUserIds = Array.from( | |
| new Set(swipes.map((swipe) => swipe.target_user_id)), | |
| ); |
JunhoHwoang
left a comment
There was a problem hiding this comment.
Hey Bill,
The UI looks fine for now but I want you to remove the demo toggle feature and just create a mock data for the historyProfiles and pass it down to the client component so the logic is cleaner. Also, I want you to consider the other users' likes. Right now, it seems like you are only getting user's likes, dislikes, and matches.
|
Hi Junho, I've pushed the new changes. There is no longer a specific demo mode; instead, it directly retrieves existing mock data, which aligns with the production logic. Additionally, the logic to recognize and record 'liked you' and 'match' events is now ready. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 21 out of 21 changed files in this pull request and generated 13 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const history = await getUserSwipeHistory(); | ||
|
|
||
| return ( | ||
| <div className="flex flex-col items-center w-full max-w-4xl mx-auto h-[calc(100vh-200px)]"> |
There was a problem hiding this comment.
This container height uses an arbitrary value (h-[calc(100vh-200px)]), which conflicts with the repo’s Tailwind conventions to avoid arbitrary values when possible. Consider using the layout’s existing flex/overflow patterns or a standard spacing-based approach (e.g., min-h-screen + padding/margins) instead of calc(...).
| <div className="flex flex-col items-center w-full max-w-4xl mx-auto h-[calc(100vh-200px)]"> | |
| <div className="flex flex-col items-center w-full max-w-4xl mx-auto min-h-screen py-24"> |
| const { error: swipeError } = await supabase.from("discovery_swipes").upsert( | ||
| { | ||
| user_id: user.id, | ||
| target_user_id: targetUserId, | ||
| action: action, | ||
| message: message, | ||
| }, | ||
| { onConflict: "user_id,target_user_id" }, | ||
| ); | ||
|
|
||
| if (swipeError) { | ||
| throw new Error(`Failed to record match swipe: ${swipeError.message}`); | ||
| console.error(`Failed to record match swipe: ${swipeError.message}`); | ||
| // We don't throw here so the UI doesn't crash on mock data testing. | ||
| } | ||
|
|
There was a problem hiding this comment.
saveMatchSwipe also swallows upsert errors. Even if you don't want the UI to crash, the caller should be able to detect/report a failure (e.g., return a failure result and avoid attempting match logic when the swipe write fails).
| if (new Date(item.created_at) > new Date(existing.created_at)) { | ||
| existing.created_at = item.created_at; | ||
| } | ||
|
|
There was a problem hiding this comment.
When merging HistoryProfile entries per user_id, the message field is never merged/updated. Because allHistory is built as [...outboundHistory, ...inboundHistory], an outbound entry can “win” and permanently drop the inbound "liked you" message even if you keep the liked_you action. Consider preferring a non-empty message, or the message from the most-recent created_at, when merging.
| if (new Date(item.created_at) > new Date(existing.created_at)) { | |
| existing.created_at = item.created_at; | |
| } | |
| const itemCreatedAt = new Date(item.created_at); | |
| const existingCreatedAt = new Date(existing.created_at); | |
| if (itemCreatedAt > existingCreatedAt) { | |
| existing.created_at = item.created_at; | |
| } | |
| // Prefer a non-empty message, and when both are non-empty, prefer the most recent one | |
| const itemHasMessage = typeof item.message === "string" && item.message.trim() !== ""; | |
| const existingHasMessage = typeof existing.message === "string" && existing.message.trim() !== ""; | |
| if ( | |
| itemHasMessage && | |
| ( | |
| !existingHasMessage || | |
| itemCreatedAt >= existingCreatedAt | |
| ) | |
| ) { | |
| existing.message = item.message; | |
| } | |
| const targetStr = ` useEffect(() => { | ||
| // Load and merge demo swipes from local storage with real history | ||
| const loadDemoSwipes = () => { | ||
| try { | ||
| const localSwipes = JSON.parse(localStorage.getItem("demoSwipes") || "[]"); | ||
| const mockProfilesMap = new Map((discoveryProfiles as any[]).map((p) => [p.user_id, p])); | ||
|
|
||
| const dSwipes = localSwipes.map((s: any) => {`; | ||
|
|
||
| const replaceStr = ` useEffect(() => { | ||
| // [dev-only] Load demo swipes and mock incoming data into history | ||
| const loadDemoSwipes = () => { | ||
| try { | ||
| const localSwipes = JSON.parse(localStorage.getItem("demoSwipes") || "[]"); | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| const mockProfilesMap = new Map((discoveryProfiles as any[]).map((p) => [p.user_id, p])); | ||
|
|
||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| const dSwipes = localSwipes.map((s: any) => {`; | ||
|
|
||
| const targetStr2 = ` // Generate mock "liked you" events from the 9 demo profiles | ||
| const mockLikedYou = mockIncomingProfiles.map((p: any, idx: number) => ({ | ||
| ...p,`; | ||
|
|
||
| const replaceStr2 = ` // Generate mock "liked you" events from the 9 demo profiles | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| const mockLikedYou = mockIncomingProfiles.map((p: any, idx: number) => ({ | ||
| ...p,`; | ||
|
|
||
| const targetStr3 = ` // with them in production history or local demo swipes. | ||
| const existingUserIds = new Set([ | ||
| ...history.map((h) => h.user_id), | ||
| ...dSwipes.map((s: any) => s.user_id) | ||
| ]);`; | ||
|
|
||
| const replaceStr3 = ` // with them in production history or local demo swipes. | ||
| const existingUserIds = new Set([ | ||
| ...history.map((h) => h.user_id), | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| ...dSwipes.map((s: any) => s.user_id) | ||
| ]);`; | ||
|
|
||
| let updated = code.replace(targetStr, replaceStr); | ||
| updated = updated.replace(targetStr2, replaceStr2); | ||
| updated = updated.replace(targetStr3, replaceStr3); | ||
|
|
||
| fs.writeFileSync(path, updated); | ||
| console.log("Patched client correctly!"); |
There was a problem hiding this comment.
web-app/fix-client.js is a one-off patching script that mutates source files via string replacement. If it’s not part of the product/tooling workflow, it should not be committed (it can easily drift and corrupt code). Consider deleting it or moving it under a dedicated /scripts folder with a clear purpose and documentation.
| const targetStr = ` useEffect(() => { | |
| // Load and merge demo swipes from local storage with real history | |
| const loadDemoSwipes = () => { | |
| try { | |
| const localSwipes = JSON.parse(localStorage.getItem("demoSwipes") || "[]"); | |
| const mockProfilesMap = new Map((discoveryProfiles as any[]).map((p) => [p.user_id, p])); | |
| const dSwipes = localSwipes.map((s: any) => {`; | |
| const replaceStr = ` useEffect(() => { | |
| // [dev-only] Load demo swipes and mock incoming data into history | |
| const loadDemoSwipes = () => { | |
| try { | |
| const localSwipes = JSON.parse(localStorage.getItem("demoSwipes") || "[]"); | |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
| const mockProfilesMap = new Map((discoveryProfiles as any[]).map((p) => [p.user_id, p])); | |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
| const dSwipes = localSwipes.map((s: any) => {`; | |
| const targetStr2 = ` // Generate mock "liked you" events from the 9 demo profiles | |
| const mockLikedYou = mockIncomingProfiles.map((p: any, idx: number) => ({ | |
| ...p,`; | |
| const replaceStr2 = ` // Generate mock "liked you" events from the 9 demo profiles | |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
| const mockLikedYou = mockIncomingProfiles.map((p: any, idx: number) => ({ | |
| ...p,`; | |
| const targetStr3 = ` // with them in production history or local demo swipes. | |
| const existingUserIds = new Set([ | |
| ...history.map((h) => h.user_id), | |
| ...dSwipes.map((s: any) => s.user_id) | |
| ]);`; | |
| const replaceStr3 = ` // with them in production history or local demo swipes. | |
| const existingUserIds = new Set([ | |
| ...history.map((h) => h.user_id), | |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
| ...dSwipes.map((s: any) => s.user_id) | |
| ]);`; | |
| let updated = code.replace(targetStr, replaceStr); | |
| updated = updated.replace(targetStr2, replaceStr2); | |
| updated = updated.replace(targetStr3, replaceStr3); | |
| fs.writeFileSync(path, updated); | |
| console.log("Patched client correctly!"); | |
| // NOTE: | |
| // This script was previously a one-off patcher that mutated | |
| // `app/(match)/discovery/history/history-client.tsx` via brittle | |
| // string replacement. It is now deprecated and intentionally does | |
| // not modify any files to avoid accidental code corruption. | |
| // | |
| // If you need to adjust `history-client.tsx`, edit the source file | |
| // directly or introduce a maintained, robust tooling script under | |
| // a dedicated scripts directory. | |
| console.log( | |
| 'fix-client.js is deprecated and no longer patches history-client.tsx.' | |
| ); |
| "use client"; | ||
|
|
||
| import { UndoButton } from "./_components/undo-button"; | ||
| // import { UndoButton } from "./_components/undo-button"; |
There was a problem hiding this comment.
Commented-out imports tend to accumulate and aren’t picked up by unused-import tooling. If UndoButton is intentionally removed, please delete the import (and any related dead code) rather than leaving it commented out.
| // import { UndoButton } from "./_components/undo-button"; |
| import { getLikedYouProfiles } from "../_actions"; | ||
| import profiles from "@/mock/profiles.json"; | ||
| import { LikedYouClient } from "./liked-you-client"; | ||
| import { LikedYouProfile } from "../types"; | ||
|
|
||
| export default async function LikedYouPage() { | ||
| const likedYouProfiles = await getLikedYouProfiles(); | ||
| // const likedYouProfiles = await getLikedYouProfiles(); | ||
|
|
There was a problem hiding this comment.
getLikedYouProfiles is imported but no longer used (the fetch is commented out). With typical TS/ESLint settings this will fail lint/build, and it also forces the page into mock-only mode. Either re-enable the server fetch (and pass the result to LikedYouClient) or remove the unused import/comment and make the mock mode explicit behind a flag.
| useEffect(() => { | ||
| // [dev-only] Load demo swipes and mock incoming data into history | ||
| const loadDemoSwipes = () => { | ||
| try { | ||
| const localSwipes = JSON.parse(localStorage.getItem("demoSwipes") || "[]"); | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| const mockProfilesMap = new Map((discoveryProfiles as any[]).map((p) => [p.user_id, p])); | ||
|
|
||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| const dSwipes = localSwipes.map((s: any) => { | ||
| const p = mockProfilesMap.get(s.target_user_id) || { fname: "Unknown", lname: "" }; | ||
| return { | ||
| ...p, | ||
| user_id: s.target_user_id, | ||
| action: s.action, | ||
| message: s.message || "", | ||
| created_at: s.created_at, | ||
| matched: s.matched | ||
| }; | ||
| }); | ||
|
|
||
| // Generate mock "liked you" events from the 9 demo profiles | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| const mockLikedYou = mockIncomingProfiles.map((p: any, idx: number) => ({ | ||
| ...p, | ||
| action: "liked_you", | ||
| message: "I vibe with you! What housing options on campus are you interested in?", | ||
| // Stagger dates so they show up over the last few days | ||
| created_at: new Date(Date.now() - 1000 * 60 * 60 * 24 * (idx + 1)).toISOString(), | ||
| matched: false | ||
| })); |
There was a problem hiding this comment.
Demo-mode history augmentation runs unconditionally: this effect always reads localStorage and injects mock "liked you" profiles into combinedHistory, even for real users. Gate this behavior behind an explicit demo/dev flag (e.g., localStorage.demoMode === 'true' or process.env.NODE_ENV !== 'production'), and otherwise just setCombinedHistory(history).
| const handleClearHistory = () => { | ||
| // Clear demo memory | ||
| localStorage.removeItem("demoSwipes"); | ||
| localStorage.removeItem("demoProfiles"); | ||
|
|
||
| // Clear server memory | ||
| startTransition(async () => { | ||
| await clearSwipeHistory(); | ||
| setCombinedHistory([]); | ||
| router.refresh(); | ||
| }); |
There was a problem hiding this comment.
handleClearHistory always calls the server action clearSwipeHistory() after clearing demo localStorage. If "Demo Mode" is meant to avoid affecting the database, this should skip the server call when demo mode is enabled (or at least ask for confirmation / separate buttons).
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 19 out of 21 changed files in this pull request and generated 10 comments.
Comments suppressed due to low confidence (1)
web-app/app/(match)/discovery/liked-you/page.tsx:17
LikedYouPageis currently hard-coded to always render mock JSON profiles and the realgetLikedYouProfiles()fetch is commented out. This means production users will see demo data instead of their actual "Liked You" feed. Gate the mock data behind an explicit demo/dev flag (e.g.,process.env.NODE_ENV !== "production"ordemoMode) and usegetLikedYouProfiles()by default.
export default async function LikedYouPage() {
// const likedYouProfiles = await getLikedYouProfiles();
return (
<LikedYouClient
initialLikedYouProfiles={
profiles.map((profile) => ({
...profile,
message:
"I vibe with you! What housing options on campus are you interested in?",
})) as LikedYouProfile[]
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| <Image | ||
| src={item.avatar_url && !item.avatar_url.includes('example.com') ? item.avatar_url : "/demo/selfie.png"} | ||
| alt={item.fname || "Profile"} | ||
| fill |
There was a problem hiding this comment.
next/image is used with fill, but no sizes prop is provided. Next.js will warn and may serve suboptimal image sizes; add an appropriate sizes value for this 64px avatar (and consider a fixed width/height instead of fill for avatars).
| fill | |
| fill | |
| sizes="64px" |
| const isDemoMode = typeof window !== 'undefined' && localStorage.getItem("demoMode") === "true"; | ||
|
|
||
| if (isDemoMode) { | ||
| const swipes = JSON.parse(localStorage.getItem("demoSwipes") || "[]"); |
There was a problem hiding this comment.
In demo mode, JSON.parse(localStorage.getItem("demoSwipes") || "[]") can throw if the localStorage value is corrupted/non-JSON, which would break swiping. Consider wrapping the parse in a try/catch (and falling back to []) to make demo mode resilient.
| const swipes = JSON.parse(localStorage.getItem("demoSwipes") || "[]"); | |
| const rawSwipes = localStorage.getItem("demoSwipes"); | |
| let swipes: any[] = []; | |
| if (rawSwipes) { | |
| try { | |
| const parsed = JSON.parse(rawSwipes); | |
| swipes = Array.isArray(parsed) ? parsed : []; | |
| } catch { | |
| swipes = []; | |
| } | |
| } |
| "user_id": "00000000-0000-0000-0000-000000000003", | ||
| "liked_user_id": "1", | ||
| "created_at": "2025-01-12" | ||
| }, |
There was a problem hiding this comment.
Mock IDs are now UUIDs for user_id, but liked_user_id is still the old short string (e.g., "1"). If this file is used for demo/testing, keep ID formats consistent across related fields to avoid confusing lookups/joins.
| No History Yet | ||
| </h2> | ||
| <p className="text-sm text-zinc-400 max-w-xs"> | ||
| Start exploring profiles to see your history action here! |
There was a problem hiding this comment.
Grammar: "see your history action here" should be plural ("history actions") to read correctly.
| Start exploring profiles to see your history action here! | |
| Start exploring profiles to see your history actions here! |
| useEffect(() => { | ||
| setSwipeDirection(0); | ||
| }, [profile.user_id]); | ||
| // Use effect removed to prevent sync setState cascade |
There was a problem hiding this comment.
After removing the useEffect block, the useEffect import at the top of this file becomes unused and will fail linting in many setups. Update the React import to only include the hooks that are still used.
| // Delete all swipes where user is the swiper or the target | ||
| const { error: error1 } = await supabase | ||
| .from("discovery_swipes") | ||
| .delete() | ||
| .eq("user_id", user.id); | ||
|
|
||
| const { error: error2 } = await supabase | ||
| .from("discovery_swipes") | ||
| .delete() | ||
| .eq("target_user_id", user.id); | ||
|
|
||
| // Also delete matches | ||
| const { error: error3 } = await supabase | ||
| .from("discovery_matches") | ||
| .delete() | ||
| .or(`user1_id.eq.${user.id},user2_id.eq.${user.id}`); | ||
|
|
||
| if (error1 || error2 || error3) { | ||
| console.error("Error clearing history:", error1 || error2 || error3); |
There was a problem hiding this comment.
clearSwipeHistory deletes all rows where target_user_id = user.id, which removes other users' swipes/likes toward this user from the shared discovery_swipes table. That’s destructive to other users’ data and likely not what “clear my history” should mean. Prefer only deleting the current user’s outbound swipes (and perhaps matches/visibility via a per-user tombstone table) rather than deleting inbound records owned by other users.
| // Delete all swipes where user is the swiper or the target | |
| const { error: error1 } = await supabase | |
| .from("discovery_swipes") | |
| .delete() | |
| .eq("user_id", user.id); | |
| const { error: error2 } = await supabase | |
| .from("discovery_swipes") | |
| .delete() | |
| .eq("target_user_id", user.id); | |
| // Also delete matches | |
| const { error: error3 } = await supabase | |
| .from("discovery_matches") | |
| .delete() | |
| .or(`user1_id.eq.${user.id},user2_id.eq.${user.id}`); | |
| if (error1 || error2 || error3) { | |
| console.error("Error clearing history:", error1 || error2 || error3); | |
| // Delete all swipes made by this user (outbound swipes only) | |
| const { error: error1 } = await supabase | |
| .from("discovery_swipes") | |
| .delete() | |
| .eq("user_id", user.id); | |
| // Also delete matches involving this user | |
| const { error: error2 } = await supabase | |
| .from("discovery_matches") | |
| .delete() | |
| .or(`user1_id.eq.${user.id},user2_id.eq.${user.id}`); | |
| if (error1 || error2) { | |
| console.error("Error clearing history:", error1 || error2); |
| useEffect(() => { | ||
| // [dev-only] Load demo swipes and mock incoming data into history | ||
| const loadDemoSwipes = () => { | ||
| try { | ||
| const localSwipes = JSON.parse(localStorage.getItem("demoSwipes") || "[]"); | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any |
There was a problem hiding this comment.
HistoryClient always loads demoSwipes and also generates mockLikedYou from mock/profiles.json on every render (regardless of whether demo mode is enabled). This will inject fake history items for real users and also ships mock JSON in the production bundle. Gate this entire block behind an explicit demo/dev flag (e.g., localStorage.getItem("demoMode") === "true" and/or process.env.NODE_ENV !== "production") and avoid importing mock JSON in the normal production path.
| <p className="text-gray-500">|</p> | ||
| <Link | ||
| href="/discovery/history" | ||
| className={`${isHistory ? "text-black" : "text-gray-400"} hover:underline`} | ||
| > | ||
| History |
There was a problem hiding this comment.
The new History tab styling uses hardcoded colors (text-black, text-gray-400/500). Project Tailwind conventions prefer semantic theme tokens for text colors (PROJECT_CONVENTIONS.md:76-81), especially to ensure consistent dark mode behavior. Consider replacing these with token-based classes (e.g., text-foreground, text-muted-foreground) and using a consistent separator style.
| const isDemoMode = typeof window !== 'undefined' && localStorage.getItem("demoMode") === "true"; | ||
|
|
||
| if (isDemoMode) { | ||
| const swipes = JSON.parse(localStorage.getItem("demoSwipes") || "[]"); |
There was a problem hiding this comment.
In demo mode, JSON.parse(localStorage.getItem("demoSwipes") || "[]") can throw if the localStorage value is corrupted/non-JSON, which would break swiping. Consider wrapping the parse in a try/catch (and falling back to []) to make demo mode resilient.
| const swipes = JSON.parse(localStorage.getItem("demoSwipes") || "[]"); | |
| let swipes: any[] = []; | |
| try { | |
| const parsed = JSON.parse(localStorage.getItem("demoSwipes") || "[]"); | |
| swipes = Array.isArray(parsed) ? parsed : []; | |
| } catch { | |
| swipes = []; | |
| } |
| // Also delete matches | ||
| const { error: error3 } = await supabase | ||
| .from("discovery_matches") | ||
| .delete() | ||
| .or(`user1_id.eq.${user.id},user2_id.eq.${user.id}`); | ||
|
|
||
| if (error1 || error2 || error3) { | ||
| console.error("Error clearing history:", error1 || error2 || error3); |
There was a problem hiding this comment.
Clearing history also deletes match rows involving the current user (discovery_matches). Since matches represent a relationship between two users, this will effectively unmatch the other person as well. If the intent is only to clear the current user’s view/history, consider a per-user “cleared_at”/visibility flag instead of deleting the shared match record (or rename the action to make the destructive behavior explicit).
| // Also delete matches | |
| const { error: error3 } = await supabase | |
| .from("discovery_matches") | |
| .delete() | |
| .or(`user1_id.eq.${user.id},user2_id.eq.${user.id}`); | |
| if (error1 || error2 || error3) { | |
| console.error("Error clearing history:", error1 || error2 || error3); | |
| if (error1 || error2) { | |
| console.error("Error clearing history:", error1 || error2); |
Description
This PR implements the following user story: "As a user, I want to see the history of my actions (like, match, dislike) in most recent order."
It introduces a new "History" tab in the discovery section where users can view a chronological feed of their past swipe activities and matches.
Key Changes
/discovery/historyroute andHistoryClientcomponent to render the user's recent swipe actions (Liked, Passed, Matched) with relative timestamps (e.g., "5m ago").DiscoveryNavbar.getUserSwipeHistoryserver action in_actions.tsto fetch and combine swipe and match data from the database.fix-mock-uuids.js).example.cominnext.config.tsto properly render mock profile images.