Initial commit
This commit is contained in:
100
web/src/hooks/useAuth.tsx
Executable file
100
web/src/hooks/useAuth.tsx
Executable file
@@ -0,0 +1,100 @@
|
||||
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
||||
|
||||
interface AuthContextType {
|
||||
isAuthenticated: boolean;
|
||||
isLoading: boolean;
|
||||
login: (username: string, password: string) => Promise<boolean>;
|
||||
logout: () => void;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | null>(null);
|
||||
|
||||
const TOKEN_KEY = 'feeld_auth_token';
|
||||
|
||||
// API URL for auth endpoints
|
||||
const AUTH_API = window.location.port === '5173' || window.location.port === '3000'
|
||||
? 'http://localhost:3001/api/auth'
|
||||
: '/api/auth';
|
||||
|
||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
// Check if already authenticated on mount
|
||||
useEffect(() => {
|
||||
const checkAuth = async () => {
|
||||
const token = localStorage.getItem(TOKEN_KEY);
|
||||
if (!token) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${AUTH_API}/verify`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
setIsAuthenticated(true);
|
||||
} else {
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
}
|
||||
} catch (e) {
|
||||
// Server not available, allow access if token exists (offline mode)
|
||||
setIsAuthenticated(true);
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
checkAuth();
|
||||
}, []);
|
||||
|
||||
const login = async (username: string, password: string): Promise<boolean> => {
|
||||
try {
|
||||
const response = await fetch(`${AUTH_API}/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
localStorage.setItem(TOKEN_KEY, data.token);
|
||||
setIsAuthenticated(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (e) {
|
||||
console.error('Login failed:', e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
const token = localStorage.getItem(TOKEN_KEY);
|
||||
if (token) {
|
||||
fetch(`${AUTH_API}/logout`, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
}).catch(err => console.error('Logout request failed:', err));
|
||||
}
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
setIsAuthenticated(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ isAuthenticated, isLoading, login, logout }}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useAuth() {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context) {
|
||||
throw new Error('useAuth must be used within an AuthProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
121
web/src/hooks/useDislikedProfiles.ts
Normal file
121
web/src/hooks/useDislikedProfiles.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useMutation } from '@apollo/client/react';
|
||||
import { PROFILE_DISLIKE_MUTATION } from '../api/operations/mutations';
|
||||
import * as dataSync from '../api/dataSync';
|
||||
|
||||
export interface DislikedProfile {
|
||||
id: string;
|
||||
imaginaryName?: string;
|
||||
age?: number;
|
||||
gender?: string;
|
||||
sexuality?: string;
|
||||
photos?: any[];
|
||||
dislikedAt: string;
|
||||
}
|
||||
|
||||
export function useDislikedProfiles() {
|
||||
const [dislikedProfiles, setDislikedProfiles] = useState<DislikedProfile[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const [profileDislike] = useMutation(PROFILE_DISLIKE_MUTATION);
|
||||
|
||||
// Load from storage on mount
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const profiles = await dataSync.getDislikedProfiles();
|
||||
setDislikedProfiles(profiles);
|
||||
} catch (e) {
|
||||
console.error('Failed to load disliked profiles:', e);
|
||||
// Fall back to localStorage
|
||||
const stored = localStorage.getItem('feeld_disliked_profiles');
|
||||
if (stored) {
|
||||
try {
|
||||
setDislikedProfiles(JSON.parse(stored));
|
||||
} catch (parseError) {
|
||||
console.error('Failed to parse disliked profiles:', parseError);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
load();
|
||||
}, []);
|
||||
|
||||
const dislikeProfile = useCallback(async (profile: {
|
||||
id: string;
|
||||
imaginaryName?: string;
|
||||
age?: number;
|
||||
gender?: string;
|
||||
sexuality?: string;
|
||||
photos?: any[];
|
||||
}) => {
|
||||
// Don't dislike duplicates
|
||||
if (dislikedProfiles.some(p => p.id === profile.id)) {
|
||||
console.log('Profile already disliked:', profile.imaginaryName);
|
||||
return { success: true, alreadyDisliked: true };
|
||||
}
|
||||
|
||||
try {
|
||||
// Call the GraphQL mutation
|
||||
const result = await profileDislike({
|
||||
variables: { targetProfileId: profile.id },
|
||||
});
|
||||
|
||||
console.log('Dislike result:', result.data?.profileDislike);
|
||||
|
||||
if (result.data?.profileDislike === 'SENT') {
|
||||
const newProfile: DislikedProfile = {
|
||||
...profile,
|
||||
dislikedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// Optimistic update
|
||||
setDislikedProfiles(prev => [newProfile, ...prev]);
|
||||
|
||||
// Sync to storage
|
||||
await dataSync.addDislikedProfile(profile);
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
return { success: false, error: 'Dislike not sent' };
|
||||
} catch (error) {
|
||||
console.error('Failed to dislike profile:', error);
|
||||
return { success: false, error };
|
||||
}
|
||||
}, [dislikedProfiles, profileDislike]);
|
||||
|
||||
const removeDislikedProfile = useCallback(async (id: string) => {
|
||||
// Optimistic update
|
||||
setDislikedProfiles(prev => prev.filter(p => p.id !== id));
|
||||
|
||||
// Sync to storage
|
||||
await dataSync.removeDislikedProfile(id);
|
||||
}, []);
|
||||
|
||||
const clearAllDisliked = useCallback(async () => {
|
||||
setDislikedProfiles([]);
|
||||
await dataSync.clearAllDislikedProfiles();
|
||||
}, []);
|
||||
|
||||
const isDisliked = useCallback((id: string) => {
|
||||
return dislikedProfiles.some(p => p.id === id);
|
||||
}, [dislikedProfiles]);
|
||||
|
||||
const getDislikedProfileIds = useCallback(() => {
|
||||
return dislikedProfiles.map(p => p.id);
|
||||
}, [dislikedProfiles]);
|
||||
|
||||
return {
|
||||
dislikedProfiles,
|
||||
loading,
|
||||
dislikeProfile,
|
||||
removeDislikedProfile,
|
||||
clearAllDisliked,
|
||||
isDisliked,
|
||||
getDislikedProfileIds,
|
||||
};
|
||||
}
|
||||
93
web/src/hooks/useLikedProfiles.ts
Executable file
93
web/src/hooks/useLikedProfiles.ts
Executable file
@@ -0,0 +1,93 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import * as dataSync from '../api/dataSync';
|
||||
|
||||
const STORAGE_KEY = 'feeld_liked_profiles';
|
||||
|
||||
export interface LikedProfile {
|
||||
id: string;
|
||||
name?: string;
|
||||
likedAt: number;
|
||||
}
|
||||
|
||||
export function useLikedProfiles() {
|
||||
const [likedProfiles, setLikedProfiles] = useState<LikedProfile[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// Load from storage on mount (tries server first, falls back to localStorage)
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const profiles = await dataSync.getLikedProfiles();
|
||||
setLikedProfiles(profiles);
|
||||
} catch (e) {
|
||||
console.error('Failed to load liked profiles:', e);
|
||||
// Fall back to localStorage
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
if (stored) {
|
||||
try {
|
||||
setLikedProfiles(JSON.parse(stored));
|
||||
} catch (e) {
|
||||
console.error('Failed to parse liked profiles:', e);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
load();
|
||||
}, []);
|
||||
|
||||
const addLikedProfile = async (id: string, name?: string) => {
|
||||
// Don't add duplicates
|
||||
if (likedProfiles.some(p => p.id === id)) return;
|
||||
|
||||
const newProfile: LikedProfile = {
|
||||
id,
|
||||
name,
|
||||
likedAt: Date.now(),
|
||||
};
|
||||
|
||||
// Optimistic update
|
||||
setLikedProfiles(prev => [newProfile, ...prev]);
|
||||
|
||||
// Sync to storage
|
||||
await dataSync.addLikedProfile(id, name);
|
||||
};
|
||||
|
||||
const removeLikedProfile = async (id: string) => {
|
||||
// Optimistic update
|
||||
setLikedProfiles(prev => prev.filter(p => p.id !== id));
|
||||
|
||||
// Sync to storage
|
||||
await dataSync.removeLikedProfile(id);
|
||||
};
|
||||
|
||||
const clearAllLiked = async () => {
|
||||
setLikedProfiles([]);
|
||||
await dataSync.setData('liked_profiles', []);
|
||||
};
|
||||
|
||||
const isLiked = (id: string) => {
|
||||
return likedProfiles.some(p => p.id === id);
|
||||
};
|
||||
|
||||
const getLikedProfileIds = () => {
|
||||
return likedProfiles.map(p => p.id);
|
||||
};
|
||||
|
||||
return {
|
||||
likedProfiles,
|
||||
loading,
|
||||
addLikedProfile,
|
||||
removeLikedProfile,
|
||||
clearAllLiked,
|
||||
isLiked,
|
||||
getLikedProfileIds,
|
||||
};
|
||||
}
|
||||
|
||||
// Standalone function for use outside React components
|
||||
export async function addLikedProfileToStorage(id: string, name?: string) {
|
||||
await dataSync.addLikedProfile(id, name);
|
||||
}
|
||||
199
web/src/hooks/useLocation.tsx
Executable file
199
web/src/hooks/useLocation.tsx
Executable file
@@ -0,0 +1,199 @@
|
||||
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
||||
import * as dataSync from '../api/dataSync';
|
||||
|
||||
// UUID generator that works in non-secure contexts (HTTP)
|
||||
function generateUUID(): string {
|
||||
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||||
const r = (Math.random() * 16) | 0;
|
||||
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
export interface SavedLocation {
|
||||
id: string;
|
||||
name: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
}
|
||||
|
||||
export interface LocationState {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
interface LocationContextType {
|
||||
location: LocationState | null;
|
||||
savedLocations: SavedLocation[];
|
||||
setLocation: (location: LocationState | null) => void;
|
||||
saveLocation: (name: string, lat: number, lng: number) => void;
|
||||
deleteLocation: (id: string) => void;
|
||||
clearLocation: () => void;
|
||||
}
|
||||
|
||||
const LocationContext = createContext<LocationContextType | null>(null);
|
||||
|
||||
const STORAGE_KEY = 'feeld_locations';
|
||||
const CURRENT_LOCATION_KEY = 'feeld_current_location';
|
||||
|
||||
export function LocationProvider({ children }: { children: ReactNode }) {
|
||||
const [location, setLocationState] = useState<LocationState | null>(() => {
|
||||
const saved = localStorage.getItem(CURRENT_LOCATION_KEY);
|
||||
return saved ? JSON.parse(saved) : null;
|
||||
});
|
||||
|
||||
const [savedLocations, setSavedLocations] = useState<SavedLocation[]>(() => {
|
||||
const saved = localStorage.getItem(STORAGE_KEY);
|
||||
return saved ? JSON.parse(saved) : [];
|
||||
});
|
||||
|
||||
const [hasSyncedFromServer, setHasSyncedFromServer] = useState(false);
|
||||
|
||||
// Fetch from server on mount and merge with localStorage
|
||||
useEffect(() => {
|
||||
const syncFromServer = async () => {
|
||||
try {
|
||||
const serverData = await dataSync.getAllFromServer();
|
||||
if (serverData) {
|
||||
// Merge saved locations from server
|
||||
if (serverData.savedLocations && serverData.savedLocations.length > 0) {
|
||||
setSavedLocations(prev => {
|
||||
const merged = [...serverData.savedLocations];
|
||||
for (const local of prev) {
|
||||
if (!merged.some((s: SavedLocation) => s.id === local.id)) {
|
||||
merged.push(local);
|
||||
}
|
||||
}
|
||||
// Update localStorage with merged data
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(merged));
|
||||
return merged;
|
||||
});
|
||||
}
|
||||
|
||||
// Restore current location if we don't have one
|
||||
if (serverData.currentLocation && !location) {
|
||||
setLocationState(serverData.currentLocation);
|
||||
localStorage.setItem(CURRENT_LOCATION_KEY, JSON.stringify(serverData.currentLocation));
|
||||
}
|
||||
|
||||
// Restore custom location if we don't have one
|
||||
if (serverData.customLocation && !location) {
|
||||
setLocationState(serverData.customLocation);
|
||||
localStorage.setItem(CURRENT_LOCATION_KEY, JSON.stringify(serverData.customLocation));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to sync locations from server:', e);
|
||||
} finally {
|
||||
setHasSyncedFromServer(true);
|
||||
}
|
||||
};
|
||||
|
||||
syncFromServer();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (location) {
|
||||
localStorage.setItem(CURRENT_LOCATION_KEY, JSON.stringify(location));
|
||||
dataSync.setData('currentLocation', location);
|
||||
} else {
|
||||
localStorage.removeItem(CURRENT_LOCATION_KEY);
|
||||
}
|
||||
}, [location]);
|
||||
|
||||
// Only sync to server after initial load from server is complete
|
||||
useEffect(() => {
|
||||
if (!hasSyncedFromServer) return;
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(savedLocations));
|
||||
// Sync to server
|
||||
dataSync.setData('savedLocations', savedLocations);
|
||||
// Also sync to dedicated saved-locations endpoint for rotation cron
|
||||
fetch('/api/saved-locations', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ locations: savedLocations }),
|
||||
}).catch(() => {}); // Best-effort
|
||||
}, [savedLocations, hasSyncedFromServer]);
|
||||
|
||||
const setLocation = (loc: LocationState | null) => {
|
||||
setLocationState(loc);
|
||||
// Sync to server
|
||||
if (loc) {
|
||||
dataSync.setData('customLocation', loc);
|
||||
}
|
||||
};
|
||||
|
||||
const saveLocation = (name: string, latitude: number, longitude: number) => {
|
||||
const newLocation: SavedLocation = {
|
||||
id: generateUUID(),
|
||||
name,
|
||||
latitude,
|
||||
longitude,
|
||||
};
|
||||
setSavedLocations((prev) => [...prev, newLocation]);
|
||||
};
|
||||
|
||||
const deleteLocation = (id: string) => {
|
||||
setSavedLocations((prev) => prev.filter((loc) => loc.id !== id));
|
||||
};
|
||||
|
||||
const clearLocation = () => {
|
||||
setLocationState(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<LocationContext.Provider
|
||||
value={{
|
||||
location,
|
||||
savedLocations,
|
||||
setLocation,
|
||||
saveLocation,
|
||||
deleteLocation,
|
||||
clearLocation,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</LocationContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useLocation() {
|
||||
const context = useContext(LocationContext);
|
||||
if (!context) {
|
||||
throw new Error('useLocation must be used within a LocationProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
// Geocoding helper using OpenStreetMap Nominatim (free, no API key needed)
|
||||
export async function geocodeAddress(address: string): Promise<{ lat: number; lng: number; displayName: string } | null> {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(address)}&limit=1`,
|
||||
{
|
||||
headers: {
|
||||
'User-Agent': 'FeeldWebApp/1.0',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.length > 0) {
|
||||
return {
|
||||
lat: parseFloat(data[0].lat),
|
||||
lng: parseFloat(data[0].lon),
|
||||
displayName: data[0].display_name,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Geocoding error:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
237
web/src/hooks/useSentPings.ts
Executable file
237
web/src/hooks/useSentPings.ts
Executable file
@@ -0,0 +1,237 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useMutation, useQuery } from '@apollo/client/react';
|
||||
import * as dataSync from '../api/dataSync';
|
||||
import { PROFILE_PING_MUTATION } from '../api/operations/mutations';
|
||||
import { ACCOUNT_STATUS_QUERY, PROFILE_QUERY } from '../api/operations/queries';
|
||||
import { apolloClient } from '../api/client';
|
||||
|
||||
// Base sent ping type (from storage)
|
||||
export type SentPing = {
|
||||
targetProfileId: string;
|
||||
targetName?: string;
|
||||
message?: string;
|
||||
sentAt: number;
|
||||
status: 'SENT' | 'MATCHED' | 'EXPIRED';
|
||||
};
|
||||
|
||||
// Profile data fetched from API
|
||||
export type ProfileData = {
|
||||
id: string;
|
||||
imaginaryName?: string;
|
||||
age?: number;
|
||||
gender?: string;
|
||||
sexuality?: string;
|
||||
bio?: string;
|
||||
desires?: string[];
|
||||
connectionGoals?: string[];
|
||||
verificationStatus?: string;
|
||||
isMajestic?: boolean;
|
||||
distance?: { km: number; mi: number };
|
||||
photos?: Array<{
|
||||
id: string;
|
||||
pictureUrls?: { small?: string; medium?: string; large?: string };
|
||||
}>;
|
||||
interactionStatus?: {
|
||||
mine?: string;
|
||||
theirs?: string;
|
||||
message?: string;
|
||||
};
|
||||
};
|
||||
|
||||
// Enriched ping with profile data
|
||||
export interface EnrichedSentPing extends SentPing {
|
||||
profile?: ProfileData;
|
||||
profileLoading?: boolean;
|
||||
profileError?: string;
|
||||
}
|
||||
|
||||
export function useSentPings() {
|
||||
const [sentPings, setSentPings] = useState<SentPing[]>([]);
|
||||
const [enrichedPings, setEnrichedPings] = useState<EnrichedSentPing[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [profilesLoading, setProfilesLoading] = useState(false);
|
||||
|
||||
// Query for available pings count
|
||||
const { data: accountData, refetch: refetchAccount } = useQuery(ACCOUNT_STATUS_QUERY, {
|
||||
fetchPolicy: 'cache-and-network',
|
||||
});
|
||||
|
||||
// Always report 2 pings available — bypass Feeld's client-side limit
|
||||
const availablePings = Math.max(accountData?.account?.availablePings ?? 2, 2);
|
||||
|
||||
// ProfilePing mutation
|
||||
const [profilePingMutation, { loading: sendingPing }] = useMutation(PROFILE_PING_MUTATION);
|
||||
|
||||
// Fetch profile data for a single ping
|
||||
const fetchProfileForPing = useCallback(async (ping: SentPing): Promise<EnrichedSentPing> => {
|
||||
try {
|
||||
const result = await apolloClient.query({
|
||||
query: PROFILE_QUERY,
|
||||
variables: { profileId: ping.targetProfileId },
|
||||
fetchPolicy: 'cache-first',
|
||||
});
|
||||
|
||||
const profile = result.data?.profile;
|
||||
if (profile) {
|
||||
return {
|
||||
...ping,
|
||||
profile,
|
||||
targetName: profile.imaginaryName || ping.targetName,
|
||||
};
|
||||
}
|
||||
return { ...ping, profileError: 'Profile not found' };
|
||||
} catch (e: any) {
|
||||
console.error(`Failed to fetch profile ${ping.targetProfileId}:`, e);
|
||||
return { ...ping, profileError: e.message || 'Failed to load profile' };
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Load sent pings from storage on mount
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const pings = await dataSync.getSentPings();
|
||||
setSentPings(pings);
|
||||
// Initialize enriched pings with loading state
|
||||
setEnrichedPings(pings.map(p => ({ ...p, profileLoading: true })));
|
||||
} catch (e) {
|
||||
console.error('Failed to load sent pings:', e);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
load();
|
||||
}, []);
|
||||
|
||||
// Fetch profile data for all pings when sentPings changes
|
||||
useEffect(() => {
|
||||
if (sentPings.length === 0) {
|
||||
setEnrichedPings([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchAllProfiles = async () => {
|
||||
setProfilesLoading(true);
|
||||
try {
|
||||
const enriched = await Promise.all(
|
||||
sentPings.map(ping => fetchProfileForPing(ping))
|
||||
);
|
||||
setEnrichedPings(enriched);
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch profiles:', e);
|
||||
} finally {
|
||||
setProfilesLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchAllProfiles();
|
||||
}, [sentPings, fetchProfileForPing]);
|
||||
|
||||
const sendPing = async (
|
||||
targetProfileId: string,
|
||||
targetName?: string,
|
||||
message?: string
|
||||
): Promise<{ success: boolean; error?: string; status?: string }> => {
|
||||
// Check if user has available pings
|
||||
if (availablePings <= 0) {
|
||||
return { success: false, error: 'No pings available. Purchase more pings to continue.' };
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await profilePingMutation({
|
||||
variables: {
|
||||
targetProfileId,
|
||||
message,
|
||||
overrideInappropriate: false,
|
||||
},
|
||||
});
|
||||
|
||||
const pingResult = result.data?.profilePing;
|
||||
|
||||
if (pingResult?.status === 'SENT') {
|
||||
// Save to local storage
|
||||
await dataSync.addSentPing(targetProfileId, targetName, message);
|
||||
|
||||
// Optimistic update
|
||||
const newPing: SentPing = {
|
||||
targetProfileId,
|
||||
targetName,
|
||||
message,
|
||||
sentAt: Date.now(),
|
||||
status: 'SENT',
|
||||
};
|
||||
setSentPings(prev => [newPing, ...prev]);
|
||||
|
||||
// Refetch account to update availablePings
|
||||
refetchAccount();
|
||||
|
||||
return { success: true, status: pingResult.status };
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: `Ping failed with status: ${pingResult?.status || 'unknown'}`,
|
||||
status: pingResult?.status,
|
||||
};
|
||||
} catch (e: any) {
|
||||
console.error('Failed to send ping:', e);
|
||||
return { success: false, error: e.message || 'Failed to send ping' };
|
||||
}
|
||||
};
|
||||
|
||||
const removePing = async (targetProfileId: string) => {
|
||||
setSentPings(prev => prev.filter(p => p.targetProfileId !== targetProfileId));
|
||||
await dataSync.removeSentPing(targetProfileId);
|
||||
};
|
||||
|
||||
const updatePingStatus = async (targetProfileId: string, status: 'SENT' | 'MATCHED' | 'EXPIRED') => {
|
||||
setSentPings(prev =>
|
||||
prev.map(p => (p.targetProfileId === targetProfileId ? { ...p, status } : p))
|
||||
);
|
||||
await dataSync.updateSentPingStatus(targetProfileId, status);
|
||||
};
|
||||
|
||||
const hasPinged = (targetProfileId: string) => {
|
||||
return sentPings.some(p => p.targetProfileId === targetProfileId);
|
||||
};
|
||||
|
||||
const clearAllPings = async () => {
|
||||
setSentPings([]);
|
||||
setEnrichedPings([]);
|
||||
await dataSync.clearAllSentPings();
|
||||
};
|
||||
|
||||
// Refresh profile data for all pings
|
||||
const refreshProfiles = useCallback(async () => {
|
||||
if (sentPings.length === 0) return;
|
||||
|
||||
setProfilesLoading(true);
|
||||
try {
|
||||
const enriched = await Promise.all(
|
||||
sentPings.map(ping => fetchProfileForPing(ping))
|
||||
);
|
||||
setEnrichedPings(enriched);
|
||||
} catch (e) {
|
||||
console.error('Failed to refresh profiles:', e);
|
||||
} finally {
|
||||
setProfilesLoading(false);
|
||||
}
|
||||
}, [sentPings, fetchProfileForPing]);
|
||||
|
||||
return {
|
||||
sentPings: enrichedPings, // Return enriched pings instead of raw pings
|
||||
rawPings: sentPings,
|
||||
loading,
|
||||
profilesLoading,
|
||||
sendingPing,
|
||||
availablePings,
|
||||
sendPing,
|
||||
removePing,
|
||||
updatePingStatus,
|
||||
hasPinged,
|
||||
clearAllPings,
|
||||
refreshProfiles,
|
||||
refetchAccount,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user