134 lines
5.0 KiB
TypeScript
134 lines
5.0 KiB
TypeScript
import { apiFetch } from "@/app/lib/api-client";
|
|
import { avatarForName } from "@/app/data/avatars";
|
|
import { FALLBACK_PROFILES, PROFILE_GRADIENTS } from "./constants";
|
|
import type { DatingProfile, MatchIntent, ProfileRecord, SwipeResult } from "./types";
|
|
|
|
export function mapProfile(record: ProfileRecord, index: number): DatingProfile {
|
|
const name = record.name || "数字游民";
|
|
const initial = name.slice(0, 1);
|
|
return {
|
|
id: record.id,
|
|
name,
|
|
age: Number(record.age || 26 + (index % 8)),
|
|
location: record.location || "远程",
|
|
citySlug: record.citySlug || "",
|
|
gender: record.gender || "",
|
|
single: record.single || "",
|
|
tags: Array.isArray(record.tags) ? record.tags : ["数字游民", "远程工作者"],
|
|
bio: record.bio || "正在完善个人资料,期待结识同城和同路的远程工作伙伴。",
|
|
lookingFor: Array.isArray(record.lookingFor) ? record.lookingFor : ["friends", "explore"],
|
|
gradient: PROFILE_GRADIENTS[index % PROFILE_GRADIENTS.length],
|
|
initial,
|
|
avatarColor: "bg-[#ff4d4f]",
|
|
photo: record.photo || avatarForName(name),
|
|
likedAt: record.likedAt,
|
|
matchedAt: record.matchedAt,
|
|
intent: record.intent,
|
|
connectionId: record.connectionId,
|
|
conversationId: record.conversationId,
|
|
};
|
|
}
|
|
|
|
export function filterFallbackProfiles(
|
|
intent: MatchIntent,
|
|
city: string,
|
|
advanced?: { gender: string; single: string }
|
|
): DatingProfile[] {
|
|
const cityQuery = city.trim().toLowerCase();
|
|
return FALLBACK_PROFILES.filter((profile) => {
|
|
if (!profile.lookingFor.includes(intent)) return false;
|
|
if (!cityQuery || cityQuery === "all") {
|
|
// continue
|
|
} else if (
|
|
profile.citySlug.toLowerCase() !== cityQuery &&
|
|
!profile.location.toLowerCase().includes(cityQuery)
|
|
) {
|
|
return false;
|
|
}
|
|
if (advanced?.gender && advanced.gender !== "all" && profile.gender !== advanced.gender) {
|
|
return false;
|
|
}
|
|
if (advanced?.single && advanced.single !== "all" && profile.single !== advanced.single) {
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
}
|
|
|
|
export function formatTimeAgo(value?: string, locale = "zh"): string {
|
|
if (!value) return locale === "zh" ? "刚刚活跃" : "Active now";
|
|
const date = new Date(value);
|
|
if (Number.isNaN(date.getTime())) return locale === "zh" ? "刚刚活跃" : "Active now";
|
|
const diff = Date.now() - date.getTime();
|
|
const minutes = Math.floor(diff / 60000);
|
|
if (minutes < 1) return locale === "zh" ? "刚刚" : "Just now";
|
|
if (minutes < 60) return locale === "zh" ? `${minutes} 分钟前` : `${minutes}m ago`;
|
|
const hours = Math.floor(minutes / 60);
|
|
if (hours < 24) return locale === "zh" ? `${hours} 小时前` : `${hours}h ago`;
|
|
const days = Math.floor(hours / 24);
|
|
if (days < 7) return locale === "zh" ? `${days} 天前` : `${days}d ago`;
|
|
const weeks = Math.floor(days / 7);
|
|
if (weeks < 5) return locale === "zh" ? `${weeks} 周前` : `${weeks}w ago`;
|
|
const months = Math.floor(days / 30);
|
|
return locale === "zh" ? `${Math.max(months, 1)} 月前` : `${Math.max(months, 1)}mo ago`;
|
|
}
|
|
|
|
export interface MatchQuota {
|
|
vip: boolean;
|
|
limit: number;
|
|
used: number;
|
|
remaining: number;
|
|
}
|
|
|
|
export async function fetchCandidates(
|
|
intent: MatchIntent,
|
|
city: string,
|
|
advanced?: { gender: string; single: string }
|
|
): Promise<DatingProfile[]> {
|
|
const params = new URLSearchParams({ intent });
|
|
if (city && city !== "all") params.set("city", city);
|
|
if (advanced?.gender && advanced.gender !== "all") params.set("gender", advanced.gender);
|
|
if (advanced?.single && advanced.single !== "all") params.set("single", advanced.single);
|
|
params.set("exclude_swiped", "true");
|
|
const data = await apiFetch<{ items: ProfileRecord[] }>(`/api/matches/candidates?${params}`);
|
|
return (data.items || []).map((item, index) => mapProfile(item, index));
|
|
}
|
|
|
|
export async function fetchQuota(): Promise<MatchQuota> {
|
|
const data = await apiFetch<MatchQuota & { ok?: boolean }>("/api/matches/quota");
|
|
return {
|
|
vip: Boolean(data.vip),
|
|
limit: Number(data.limit || 25),
|
|
used: Number(data.used || 0),
|
|
remaining: Number(data.remaining || 0),
|
|
};
|
|
}
|
|
|
|
export async function undoSwipe(): Promise<{ profileId?: string }> {
|
|
return apiFetch<{ ok: boolean; profileId?: string }>("/api/matches/swipes/undo", {
|
|
method: "POST",
|
|
body: JSON.stringify({}),
|
|
});
|
|
}
|
|
|
|
export async function fetchLikes(): Promise<DatingProfile[]> {
|
|
const data = await apiFetch<{ items: ProfileRecord[] }>("/api/matches/likes");
|
|
return (data.items || []).map((item, index) => mapProfile(item, index));
|
|
}
|
|
|
|
export async function fetchMutualMatches(): Promise<DatingProfile[]> {
|
|
const data = await apiFetch<{ items: ProfileRecord[] }>("/api/matches/mutual");
|
|
return (data.items || []).map((item, index) => mapProfile(item, index));
|
|
}
|
|
|
|
export async function saveSwipe(
|
|
profileId: string,
|
|
action: "like" | "dislike",
|
|
intent: MatchIntent
|
|
): Promise<SwipeResult> {
|
|
return apiFetch<SwipeResult>("/api/matches/swipes", {
|
|
method: "POST",
|
|
body: JSON.stringify({ profileId, action, intent }),
|
|
});
|
|
}
|