Skip to content

Bill z/history action - #60

Draft
Bill092738 wants to merge 6 commits into
devfrom
BillZ/history-action
Draft

Bill092738 wants to merge 6 commits into
devfrom
BillZ/history-action

Conversation

@Bill092738

Copy link
Copy Markdown
Collaborator

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

  • New History Page: Added /discovery/history route and HistoryClient component to render the user's recent swipe actions (Liked, Passed, Matched) with relative timestamps (e.g., "5m ago").
  • Navigation Update: Added a "History" link to the DiscoveryNavbar.
  • Data Fetching: Created the getUserSwipeHistory server action in _actions.ts to fetch and combine swipe and match data from the database.
  • Demo Mode: Implemented a local storage-based "Demo Mode" in the like/dislike buttons and history client for easier UI testing without affecting the actual database.
  • Mock Data Improvements: Updated mock JSON files to use standard UUID formats instead of single-digit strings (via fix-mock-uuids.js).
  • Config Update: Whitelisted example.com in next.config.ts to properly render mock profile images.

@Bill092738
Bill092738 requested a review from JunhoHwoang as a code owner March 24, 2026 17:33
Copilot AI review requested due to automatic review settings March 24, 2026 17:33
@vercel

vercel Bot commented Mar 24, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
sp-26-web-project Ready Ready Preview, Comment Mar 25, 2026 3:32am

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/history route with a HistoryClient UI rendering relative timestamps and action badges.
  • Added server action getUserSwipeHistory to 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>

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
<span className="text-5xl opacity-50">🧭</span>
<span className="text-5xl opacity-50" aria-hidden="true">🧭</span>

Copilot uses AI. Check for mistakes.
Comment thread web-app/mock/likes.json
Comment on lines +13 to 15
"user_id": "00000000-0000-0000-0000-000000000008",
"liked_user_id": "1",
"created_at": "2025-01-12"

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread fix-mock-uuids.js
Comment on lines +10 to +16
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);

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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);

Copilot uses AI. Check for mistakes.
Comment on lines +325 to +331
// 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,

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
// 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,

Copilot uses AI. Check for mistakes.
Comment on lines +127 to +132
<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"
/>

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment thread web-app/mock/likes.json
Comment on lines +3 to 5
"user_id": "00000000-0000-0000-0000-000000000003",
"liked_user_id": "1",
"created_at": "2025-01-12"

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread web-app/mock/likes.json
Comment on lines +8 to 10
"user_id": "00000000-0000-0000-0000-000000000006",
"liked_user_id": "1",
"created_at": "2025-01-12"

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 69 to 72
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.
}

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
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.

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
// 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 };

Copilot uses AI. Check for mistakes.
matches.map((m) => (m.user1_id === user.id ? m.user2_id : m.user1_id))
);

const targetUserIds = swipes.map((swipe) => swipe.target_user_id);

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
const targetUserIds = swipes.map((swipe) => swipe.target_user_id);
const targetUserIds = Array.from(
new Set(swipes.map((swipe) => swipe.target_user_id)),
);

Copilot uses AI. Check for mistakes.

@JunhoHwoang JunhoHwoang left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@Bill092738

Copy link
Copy Markdown
Collaborator Author

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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)]">

Copilot AI Mar 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(...).

Suggested change
<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">

Copilot uses AI. Check for mistakes.
Comment on lines 114 to 128
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.
}

Copilot AI Mar 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +383 to +386
if (new Date(item.created_at) > new Date(existing.created_at)) {
existing.created_at = item.created_at;
}

Copilot AI Mar 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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;
}

Copilot uses AI. Check for mistakes.
Comment thread web-app/app/(match)/discovery/history/history-client.tsx.patch Outdated
Comment thread web-app/app/(match)/discovery/_actions.ts.patch Outdated
Comment thread web-app/fix-client.js
Comment on lines +5 to +52
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!");

Copilot AI Mar 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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.'
);

Copilot uses AI. Check for mistakes.
"use client";

import { UndoButton } from "./_components/undo-button";
// import { UndoButton } from "./_components/undo-button";

Copilot AI Mar 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
// import { UndoButton } from "./_components/undo-button";

Copilot uses AI. Check for mistakes.
Comment on lines 1 to 8
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();

Copilot AI Mar 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +51 to +81
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
}));

Copilot AI Mar 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +38 to +48
const handleClearHistory = () => {
// Clear demo memory
localStorage.removeItem("demoSwipes");
localStorage.removeItem("demoProfiles");

// Clear server memory
startTransition(async () => {
await clearSwipeHistory();
setCombinedHistory([]);
router.refresh();
});

Copilot AI Mar 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  • LikedYouPage is currently hard-coded to always render mock JSON profiles and the real getLikedYouProfiles() 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" or demoMode) and use getLikedYouProfiles() 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

Copilot AI Mar 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
fill
fill
sizes="64px"

Copilot uses AI. Check for mistakes.
const isDemoMode = typeof window !== 'undefined' && localStorage.getItem("demoMode") === "true";

if (isDemoMode) {
const swipes = JSON.parse(localStorage.getItem("demoSwipes") || "[]");

Copilot AI Mar 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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 = [];
}
}

Copilot uses AI. Check for mistakes.
Comment thread web-app/mock/likes.json
Comment on lines +3 to 6
"user_id": "00000000-0000-0000-0000-000000000003",
"liked_user_id": "1",
"created_at": "2025-01-12"
},

Copilot AI Mar 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
No History Yet
</h2>
<p className="text-sm text-zinc-400 max-w-xs">
Start exploring profiles to see your history action here!

Copilot AI Mar 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Grammar: "see your history action here" should be plural ("history actions") to read correctly.

Suggested change
Start exploring profiles to see your history action here!
Start exploring profiles to see your history actions here!

Copilot uses AI. Check for mistakes.
useEffect(() => {
setSwipeDirection(0);
}, [profile.user_id]);
// Use effect removed to prevent sync setState cascade

Copilot AI Mar 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +405 to +423
// 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);

Copilot AI Mar 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
// 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);

Copilot uses AI. Check for mistakes.
Comment on lines +51 to +56
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

Copilot AI Mar 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +28 to +33
<p className="text-gray-500">|</p>
<Link
href="/discovery/history"
className={`${isHistory ? "text-black" : "text-gray-400"} hover:underline`}
>
History

Copilot AI Mar 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
const isDemoMode = typeof window !== 'undefined' && localStorage.getItem("demoMode") === "true";

if (isDemoMode) {
const swipes = JSON.parse(localStorage.getItem("demoSwipes") || "[]");

Copilot AI Mar 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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 = [];
}

Copilot uses AI. Check for mistakes.
Comment on lines +416 to +423
// 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);

Copilot AI Mar 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
// 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);

Copilot uses AI. Check for mistakes.
@Bill092738
Bill092738 marked this pull request as draft April 8, 2026 20:09
@Bill092738 Bill092738 self-assigned this Apr 8, 2026

This branch was successfully deployed

1 active deployment
Preview 6c8ba16d Deployed Mar 25, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants