This document describes the internal APIs and data structures used in the PayeTonGréviste Strike fund discovery app.
⚠️ Important: This project boycotts Google services in solidarity with the strike movement. We do not use Google Analytics, Google Fonts, or any other Google services.
interface Profile {
id: string; // Unique identifier
name: string; // Display name
age: number; // Age
bio: string; // Bio text
photoUrl: string; // Profile photo URL
location: { lat: number; lon: number }; // Geographic coordinates
strikeFund: {
// Connected Strike fund
url: string; // Fund donation URL
title: string; // Fund title
};
}interface StrikeFund {
id: string; // Unique identifier
title: string; // Fund title
description: string; // Fund description
url: string; // Donation URL
category: FundCategory; // Fund category
status: FundStatus; // Fund status
targetAmount: number; // Target amount (€)
currentAmount: number; // Current amount raised (€)
urgency: UrgencyLevel; // Urgency level
location: string; // Fund location
organizer: string; // Fund organizer
createdAt: Date; // Creation date
expiresAt: Date | null; // Expiration date
isVerified: boolean; // Verification status
tags: string[]; // Fund tags
imageUrl: string; // Fund image URL
impactStories: ImpactStory[]; // Impact stories
}type FundCategory =
| 'transport' // Transport workers
| 'education' // Teachers, students
| 'health' // Healthcare workers
| 'environment' // Environmental causes
| 'social' // Social workers
| 'culture' // Cultural workers
| 'food' // Food service workers
| 'retail' // Retail workers
| 'tech' // Tech workers
| 'other'; // Other causestype FundStatus =
| 'active' // Fund is actively raising money
| 'paused' // Fund is temporarily paused
| 'completed' // Fund has reached its goal
| 'expired' // Fund has expired
| 'cancelled'; // Fund was cancelledtype UrgencyLevel =
| 'low' // No immediate urgency
| 'medium' // Some urgency
| 'high' // High urgency
| 'critical'; // Critical urgencyThe app uses Zustand for state management. Access the store with:
import { useAppStore } from './store';
// Get the entire store
const store = useAppStore();
// Get specific state
const { profiles, likedIds, chats } = useAppStore();type AppState = {
// Data
profiles: Profile[]; // All profiles
likedIds: Set<string>; // Liked profile IDs
passedIds: Set<string>; // Passed profile IDs
chats: Record<string, ChatMessage[]>; // Chat messages by profile ID
currentUser: Profile | null; // Current user profile
// Actions
likeProfile: (id: string) => void; // Like a profile
passProfile: (id: string) => void; // Pass on a profile
setProfiles: (profiles: Profile[]) => void; // Set all profiles
ensureChatFor: (id: string) => void; // Ensure chat exists
addUserMessage: (id: string, text: string) => void; // Add user message
upsertProfile: (profile: Profile) => void; // Create/update profile
removeProfile: (id: string) => void; // Remove profile
updateUserProfile: (profile: Profile) => void; // Update user profile
deleteUserProfile: () => void; // Delete user profile
isProfileComplete: () => boolean; // Check profile completeness
};Likes a profile and creates a chat with Strike fund information.
const { likeProfile } = useAppStore();
// Like a profile
likeProfile('profile-123');Passes on a profile (adds to passed set).
const { passProfile } = useAppStore();
// Pass on a profile
passProfile('profile-123');Sets all profiles in the store.
const { setProfiles } = useAppStore();
// Set profiles
setProfiles(mockProfiles);Ensures a chat exists for a profile with default Strike fund message.
const { ensureChatFor } = useAppStore();
// Ensure chat exists
ensureChatFor('profile-123');Adds a user message to a chat.
const { addUserMessage } = useAppStore();
// Add user message
addUserMessage('profile-123', 'Hello!');import {
getActiveFunds,
getFundsByCategory,
getFundsByUrgency,
getFundsByLocation,
searchFunds,
getFundById,
getFundsSortedByPriority,
getFundProgress,
isFundUrgent,
getUrgentFunds,
getFundStats,
} from './lib/strikeFunds';Returns all active Strike funds.
const activeFunds = getActiveFunds();
// Returns: StrikeFund[]Returns funds filtered by category.
const transportFunds = getFundsByCategory('transport');
// Returns: StrikeFund[]Returns funds filtered by urgency level.
const urgentFunds = getFundsByUrgency('critical');
// Returns: StrikeFund[]Returns funds filtered by location.
const parisFunds = getFundsByLocation('Paris');
// Returns: StrikeFund[]Searches funds by title, description, tags, or organizer.
const results = searchFunds('transport');
// Returns: StrikeFund[]Returns a specific fund by ID.
const fund = getFundById('fund-001');
// Returns: StrikeFund | undefinedReturns funds sorted by urgency and progress.
const prioritizedFunds = getFundsSortedByPriority();
// Returns: StrikeFund[] (sorted)Returns fund progress as percentage.
const progress = getFundProgress(fund);
// Returns: number (0-100)Checks if fund is urgent.
const urgent = isFundUrgent(fund);
// Returns: booleanReturns all urgent funds.
const urgentFunds = getUrgentFunds();
// Returns: StrikeFund[]Returns fund statistics.
const stats = getFundStats();
// Returns: {
// totalFunds: number,
// totalTarget: number,
// totalRaised: number,
// averageProgress: number,
// urgentFunds: number,
// categories: number
// }interface SwipeDeckProps {
onCreateProfile: () => void // Callback for profile creation
}
// Usage
<SwipeDeck onCreateProfile={handleCreateProfile} />interface CardProps {
profile: Profile // Profile to display
onSwipe: (direction: 'left' | 'right' | 'up') => void // Swipe callback
style?: React.CSSProperties // Custom styles
}
// Usage
<Card
profile={profile}
onSwipe={handleSwipe}
style={{ zIndex: 10 }}
/>interface ToastProps {
toasts: Toast[] // Array of toasts
onClose: (id: string) => void // Close callback
}
interface Toast {
id: string // Toast ID
message: string // Toast message
type: 'success' | 'error' | 'info' | 'warning' // Toast type
duration?: number // Duration in ms
}
// Usage
<ToastContainer toasts={toasts} onClose={removeToast} />interface OnboardingFlowProps {
onComplete: () => void // Completion callback
}
// Usage
<OnboardingFlow onComplete={handleOnboardingComplete} />import { haversineKm, getCurrentLocation } from './lib/geo';
// Calculate distance between two points
const distance = haversineKm(lat1, lon1, lat2, lon2);
// Returns: number (distance in km)
// Get current location
const location = await getCurrentLocation();
// Returns: { lat: number, lon: number } | nullimport { useToast } from './components/Toast';
const { showSuccess, showError, showInfo, showWarning } = useToast();
// Show success toast
showSuccess('Success!', 'Operation completed successfully');
// Show error toast
showError('Error!', 'Something went wrong');
// Show info toast
showInfo('Info', 'Here is some information');
// Show warning toast
showWarning('Warning!', 'Please be careful');The app handles swipe gestures through Framer Motion:
// Swipe direction types
type SwipeDirection = 'left' | 'right' | 'up';
// Swipe event handler
const handleSwipe = (direction: SwipeDirection) => {
switch (direction) {
case 'left':
passProfile(currentProfile.id);
break;
case 'right':
likeProfile(currentProfile.id);
break;
case 'up':
showProfileDetails(currentProfile);
break;
}
};// Navigation routes
const routes = {
'/': 'DiscoverPage',
'/matches': 'MatchesPage',
'/activist': 'ActivistSetupPage',
};
// Navigation handler
const navigate = useNavigate();
navigate('/matches');# Development Configuration
VITE_APP_TITLE=PayeTonGréviste
VITE_APP_VERSION=0.0.0
VITE_APP_ENV=development
# API Configuration
VITE_API_BASE_URL=http://localhost:3001
VITE_API_TIMEOUT=5000
# Feature Flags
VITE_ENABLE_GEOLOCATION=true
VITE_ENABLE_ANALYTICS=false
VITE_ENABLE_DEBUG_MODE=true
# Strike Fund Configuration
VITE_DEFAULT_FUND_CATEGORIES=transport,education,health,environment
VITE_MAX_PROFILES_PER_LOAD=10{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
}
}import { render, screen, fireEvent } from '@testing-library/react'
import { Card } from './Card'
test('renders profile information', () => {
const profile = mockProfiles[0]
render(<Card profile={profile} onSwipe={jest.fn()} />)
expect(screen.getByText(profile.name)).toBeInTheDocument()
expect(screen.getByText(profile.bio)).toBeInTheDocument()
})import { useAppStore } from './store';
test('likes profile and creates chat', () => {
const { likeProfile, chats } = useAppStore.getState();
likeProfile('profile-1');
expect(chats['profile-1']).toBeDefined();
expect(chats['profile-1'][0].from).toBe('bot');
});import { useAppStore } from './store'
import { getActiveFunds } from './lib/strikeFunds'
function CreateProfile() {
const { upsertProfile } = useAppStore()
const funds = getActiveFunds()
const handleCreateProfile = (profileData) => {
// Select a random fund
const randomFund = funds[Math.floor(Math.random() * funds.length)]
const profile = {
id: generateId(),
name: profileData.name,
age: profileData.age,
bio: profileData.bio,
photoUrl: profileData.photoUrl,
location: profileData.location,
strikeFund: {
url: randomFund.url,
title: randomFund.title
}
}
upsertProfile(profile)
}
return (
<form onSubmit={handleCreateProfile}>
{/* Form fields */}
</form>
)
}import { getFundsByCategory, getUrgentFunds, searchFunds } from './lib/strikeFunds'
function FundDiscovery() {
const [category, setCategory] = useState<FundCategory>('transport')
const [searchQuery, setSearchQuery] = useState('')
const funds = useMemo(() => {
if (searchQuery) {
return searchFunds(searchQuery)
}
return getFundsByCategory(category)
}, [category, searchQuery])
const urgentFunds = getUrgentFunds()
return (
<div>
<input
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search funds..."
/>
<select value={category} onChange={(e) => setCategory(e.target.value)}>
<option value="transport">Transport</option>
<option value="education">Education</option>
<option value="health">Health</option>
</select>
{urgentFunds.length > 0 && (
<div className="urgent-funds">
<h3>Urgent Funds</h3>
{urgentFunds.map(fund => (
<FundCard key={fund.id} fund={fund} />
))}
</div>
)}
<div className="funds-list">
{funds.map(fund => (
<FundCard key={fund.id} fund={fund} />
))}
</div>
</div>
)
}This API documentation should help developers understand how to work with the PayeTonGréviste codebase. For more specific examples or questions, please refer to the source code or open an issue.