+16
.env.example
+16
.env.example
···
1
+
# Copy below .env.mock for mock mode (frontend only); remove the rest
2
+
VITE_LOCAL_MOCK=true
3
+
VITE_ENABLE_OAUTH=false
4
+
VITE_ENABLE_DATABASE=false
5
+
6
+
# Copy this to .env for full local development and update marked fields
7
+
VITE_LOCAL_MOCK=false
8
+
VITE_API_BASE=/.netlify/functions
9
+
URL=http://127.0.0.1:8888
10
+
DEPLOY_URL=http://127.0.0.1:8888
11
+
DEPLOY_PRIME_URL=http://127.0.0.1:8888
12
+
CONTEXT=dev
13
+
14
+
# Update these
15
+
NETLIFY_DATABASE_URL=postgresql://user:password@host/database
16
+
OAUTH_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nYOUR_KEY_HERE\n-----END PRIVATE KEY-----"
+253
CONTRIBUTING.md
+253
CONTRIBUTING.md
···
1
+
# Contributing to ATlast
2
+
3
+
Thank you for your interest in contributing! This guide will help you get started with local development.
4
+
5
+
## Two Development Modes
6
+
7
+
We support two development modes:
8
+
9
+
🎨 **Mock Mode** (No backend required)
10
+
**Best for:** Frontend development, UI/UX work, design changes
11
+
12
+
🔧 **Full Mode** (Complete backend)
13
+
**Best for:** Backend development, API work, OAuth testing, database changes
14
+
15
+
**Requirements:**
16
+
- PostgreSQL database (local or Neon)
17
+
- OAuth keys
18
+
- Environment configuration
19
+
20
+
---
21
+
22
+
## Mock Mode Starting Guide
23
+
24
+
Perfect for frontend contributors who want to jump in quickly!
25
+
26
+
1. Clone and Install
27
+
```bash
28
+
git clone <repo-url>
29
+
cd atlast
30
+
npm install
31
+
```
32
+
33
+
2. Create .env.local
34
+
```bash
35
+
# .env.mock
36
+
VITE_LOCAL_MOCK=true
37
+
VITE_ENABLE_OAUTH=false
38
+
VITE_ENABLE_DATABASE=false
39
+
```
40
+
41
+
3. Start Development
42
+
```bash
43
+
npm run dev:mock
44
+
```
45
+
46
+
4. Open Your Browser
47
+
Go to `http://localhost:5173`
48
+
49
+
5. "Login" with Mock User
50
+
Enter any handle - it will create a mock session.
51
+
52
+
6. Upload Test Data
53
+
Upload your TikTok or Instagram data file. The mock API will generate fake matches for testing the UI.
54
+
55
+
---
56
+
57
+
## Full Mode Starting Guide
58
+
59
+
For contributors working on backend features, OAuth, or database operations.
60
+
61
+
### Prerequisites
62
+
63
+
- Node.js 18+
64
+
- PostgreSQL (or Neon account)
65
+
- OpenSSL (for key generation)
66
+
67
+
1. Clone and Install
68
+
```bash
69
+
git clone <repo-url>
70
+
cd atlast
71
+
npm install
72
+
npm install -g netlify-cli
73
+
```
74
+
75
+
2. Database Setup
76
+
77
+
**Option A: Neon (Recommended)**
78
+
1. Create account at https://neon.tech
79
+
2. Create project "atlast-dev"
80
+
3. Copy connection string
81
+
82
+
**Option B: Local PostgreSQL**
83
+
```bash
84
+
# macOS
85
+
brew install postgresql@15
86
+
brew services start postgresql@15
87
+
createdb atlast_dev
88
+
89
+
# Ubuntu
90
+
sudo apt install postgresql
91
+
sudo systemctl start postgresql
92
+
sudo -u postgres createdb atlast_dev
93
+
```
94
+
95
+
3. Generate OAuth Keys
96
+
```bash
97
+
# Generate private key
98
+
openssl ecparam -name prime256v1 -genkey -noout -out private-key.pem
99
+
100
+
# Extract public key
101
+
openssl ec -in private-key.pem -pubout -out public-key.pem
102
+
103
+
# View private key (copy for .env)
104
+
cat private-key.pem
105
+
```
106
+
107
+
4. Extract Public Key JWK
108
+
```bash
109
+
node -e "
110
+
const fs = require('fs');
111
+
const jose = require('jose');
112
+
const pem = fs.readFileSync('public-key.pem', 'utf8');
113
+
jose.importSPKI(pem, 'ES256').then(key => {
114
+
return jose.exportJWK(key);
115
+
}).then(jwk => {
116
+
console.log(JSON.stringify(jwk, null, 2));
117
+
});
118
+
"
119
+
```
120
+
121
+
5. Update netlify/functions/jwks.ts
122
+
123
+
Replace `PUBLIC_JWK` with the output from step 4.
124
+
125
+
6. Create .env
126
+
127
+
```bash
128
+
VITE_LOCAL_MOCK=false
129
+
VITE_API_BASE=/.netlify/functions
130
+
131
+
# Database (choose one)
132
+
NETLIFY_DATABASE_URL=postgresql://user:pass@host/db # Neon
133
+
# NETLIFY_DATABASE_URL=postgresql://localhost/atlast_dev # Local
134
+
135
+
# OAuth (paste your private key)
136
+
OAUTH_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nYOUR_KEY_HERE\n-----END PRIVATE KEY-----"
137
+
138
+
# Local URLs (MUST use 127.0.0.1 for OAuth)
139
+
URL=http://127.0.0.1:8888
140
+
DEPLOY_URL=http://127.0.0.1:8888
141
+
DEPLOY_PRIME_URL=http://127.0.0.1:8888
142
+
CONTEXT=dev
143
+
```
144
+
145
+
7. Initialize Database
146
+
```bash
147
+
npm run init-db
148
+
```
149
+
150
+
8. Start Development Server
151
+
```bash
152
+
npm run dev:full
153
+
```
154
+
155
+
9. Test OAuth
156
+
157
+
1. Open `http://127.0.0.1:8888` (NOT localhost)
158
+
2. Enter your real Bluesky handle
159
+
3. Authorize the app
160
+
4. You should be redirected back and logged in
161
+
162
+
---
163
+
164
+
## Project Structure
165
+
166
+
```
167
+
atlast/
168
+
├── src/
169
+
│ ├── components/ # React components
170
+
│ ├── pages/ # Page components
171
+
│ ├── hooks/ # Custom hooks
172
+
│ ├── lib/
173
+
│ │ ├── apiClient/ # API client (real + mock)
174
+
│ │ ├── platforms/ # File parsers
175
+
│ │ └── config.ts # Environment config
176
+
│ └── types/ # TypeScript types
177
+
├── netlify/
178
+
│ └── functions/ # Backend API
179
+
├── scripts/ # Build scripts
180
+
└── test-data/ # Sample upload files (git-ignored)
181
+
```
182
+
183
+
---
184
+
185
+
## Task Workflows
186
+
187
+
### Adding a New Social Platform
188
+
189
+
1. Create `src/lib/platforms/yourplatform.ts`
190
+
2. Implement parser following `tiktok.ts` or `instagram.ts`
191
+
3. Register in `src/lib/platforms/registry.ts`
192
+
4. Update `src/constants/platforms.ts`
193
+
5. Test with real data file
194
+
195
+
### Adding a New API Endpoint
196
+
197
+
1. Create `netlify/functions/your-endpoint.ts`
198
+
2. Add authentication check (copy from existing)
199
+
3. Update `src/lib/apiClient/realApiClient.ts`
200
+
4. Update `src/lib/apiClient/mockApiClient.ts`
201
+
5. Use in components via `apiClient.yourMethod()`
202
+
203
+
### Styling Changes
204
+
205
+
- Use Tailwind utility classes
206
+
- Follow dark mode pattern: `class="bg-white dark:bg-gray-800"`
207
+
- Test in both light and dark modes
208
+
- Mobile-first responsive design
209
+
- Check accessibility (if implemented) is retained
210
+
211
+
---
212
+
213
+
## Submitting Changes
214
+
215
+
### Before Submitting
216
+
217
+
- [ ] Test in mock mode: `npm run dev:mock`
218
+
- [ ] Test in full mode (if backend changes): `npm run dev:full`
219
+
- [ ] Check both light and dark themes
220
+
- [ ] Test mobile responsiveness
221
+
- [ ] No console errors
222
+
- [ ] Code follows existing patterns
223
+
224
+
### Pull Request Process
225
+
226
+
1. Fork the repository
227
+
2. Create a feature branch: `git checkout -b feature/your-feature`
228
+
3. Make your changes
229
+
4. Commit with clear messages
230
+
5. Push to your fork
231
+
6. Open a Pull Request
232
+
233
+
### PR Description Should Include
234
+
235
+
- What changes were made
236
+
- Why these changes are needed
237
+
- Screenshots (for UI changes)
238
+
- Testing steps
239
+
- Related issues
240
+
241
+
---
242
+
243
+
## Resources
244
+
245
+
- [AT Protocol Docs](https://atproto.com)
246
+
- [Bluesky API](https://docs.bsky.app)
247
+
- [React Documentation](https://react.dev)
248
+
- [Tailwind CSS](https://tailwindcss.com)
249
+
- [Netlify Functions](https://docs.netlify.com/functions/overview)
250
+
251
+
---
252
+
253
+
Thank you for contributing to ATlast!
+4
-2
package.json
+4
-2
package.json
···
5
5
"version": "0.0.1",
6
6
"type": "module",
7
7
"scripts": {
8
-
"dev": "vite",
8
+
"dev": "netlify dev",
9
+
"dev:mock": "vite --mode mock",
10
+
"dev:full": "netlify dev",
9
11
"build": "vite build",
10
-
"preview": "vite preview"
12
+
"init-db": "tsx scripts/init-local-db.ts"
11
13
},
12
14
"dependencies": {
13
15
"@atcute/identity": "^1.1.0",
+1
-1
src/lib/apiClient.ts
src/lib/apiClient/realApiClient.ts
+1
-1
src/lib/apiClient.ts
src/lib/apiClient/realApiClient.ts
···
1
-
import type { AtprotoSession, BatchSearchResult, BatchFollowResult, SaveResultsResponse, SearchResult } from '../types';
1
+
import type { AtprotoSession, BatchSearchResult, BatchFollowResult, SaveResultsResponse, SearchResult } from '../../types';
2
2
3
3
// Client-side cache with TTL
4
4
interface CacheEntry<T> {
+11
src/lib/apiClient/index.ts
+11
src/lib/apiClient/index.ts
···
1
+
import { isLocalMockMode } from '../config';
2
+
3
+
// Import both clients
4
+
import { apiClient as realApiClient } from './realApiClient';
5
+
import { mockApiClient } from './mockApiClient';
6
+
7
+
// Export the appropriate client
8
+
export const apiClient = isLocalMockMode() ? mockApiClient : realApiClient;
9
+
10
+
// Also export both for explicit usage
11
+
export { realApiClient, mockApiClient };
+181
src/lib/apiClient/mockApiClient.ts
+181
src/lib/apiClient/mockApiClient.ts
···
1
+
import type {
2
+
AtprotoSession,
3
+
BatchSearchResult,
4
+
BatchFollowResult,
5
+
SearchResult,
6
+
SaveResultsResponse
7
+
} from '../../types';
8
+
9
+
// Mock user data for testing
10
+
const MOCK_SESSION: AtprotoSession = {
11
+
did: 'did:plc:mock123',
12
+
handle: 'developer.bsky.social',
13
+
displayName: 'Local Developer',
14
+
avatar: undefined,
15
+
description: 'Testing ATlast locally'
16
+
};
17
+
18
+
// Generate mock Bluesky matches
19
+
function generateMockMatches(username: string): any[] {
20
+
const numMatches = Math.random() < 0.7 ? Math.floor(Math.random() * 3) + 1 : 0;
21
+
22
+
return Array.from({ length: numMatches }, (_, i) => ({
23
+
did: `did:plc:mock${username}${i}`,
24
+
handle: `${username}.bsky.social`,
25
+
displayName: username.charAt(0).toUpperCase() + username.slice(1),
26
+
avatar: `https://api.dicebear.com/7.x/avataaars/svg?seed=${username}${i}`,
27
+
matchScore: 100 - (i * 20),
28
+
description: `Mock profile for ${username}`,
29
+
postCount: Math.floor(Math.random() * 1000),
30
+
followerCount: Math.floor(Math.random() * 5000),
31
+
}));
32
+
}
33
+
34
+
// Simulate network delay
35
+
const delay = (ms: number = 500) => new Promise(resolve => setTimeout(resolve, ms));
36
+
37
+
export const mockApiClient = {
38
+
async startOAuth(handle: string): Promise<{ url: string }> {
39
+
await delay(300);
40
+
console.log('[MOCK] Starting OAuth for:', handle);
41
+
// In mock mode, just return to home immediately
42
+
return { url: window.location.origin + '/?session=mock' };
43
+
},
44
+
45
+
async getSession(): Promise<AtprotoSession> {
46
+
await delay(200);
47
+
console.log('[MOCK] Getting session');
48
+
49
+
// Check if user has "logged in" via mock OAuth
50
+
const params = new URLSearchParams(window.location.search);
51
+
if (params.get('session') === 'mock') {
52
+
return MOCK_SESSION;
53
+
}
54
+
55
+
// Check localStorage for mock session
56
+
const mockSession = localStorage.getItem('mock_session');
57
+
if (mockSession) {
58
+
return JSON.parse(mockSession);
59
+
}
60
+
61
+
throw new Error('No mock session');
62
+
},
63
+
64
+
async logout(): Promise<void> {
65
+
await delay(200);
66
+
console.log('[MOCK] Logging out');
67
+
localStorage.removeItem('mock_session');
68
+
localStorage.removeItem('mock_uploads');
69
+
},
70
+
71
+
async getUploads(): Promise<{ uploads: any[] }> {
72
+
await delay(300);
73
+
console.log('[MOCK] Getting uploads');
74
+
75
+
const mockUploads = localStorage.getItem('mock_uploads');
76
+
if (mockUploads) {
77
+
return { uploads: JSON.parse(mockUploads) };
78
+
}
79
+
80
+
return { uploads: [] };
81
+
},
82
+
83
+
async getUploadDetails(uploadId: string, page: number = 1, pageSize: number = 50): Promise<{
84
+
results: SearchResult[];
85
+
pagination?: any;
86
+
}> {
87
+
await delay(500);
88
+
console.log('[MOCK] Getting upload details:', uploadId);
89
+
90
+
const mockData = localStorage.getItem(`mock_upload_${uploadId}`);
91
+
if (mockData) {
92
+
const results = JSON.parse(mockData);
93
+
return { results };
94
+
}
95
+
96
+
return { results: [] };
97
+
},
98
+
99
+
async getAllUploadDetails(uploadId: string): Promise<{ results: SearchResult[] }> {
100
+
return this.getUploadDetails(uploadId);
101
+
},
102
+
103
+
async batchSearchActors(usernames: string[]): Promise<{ results: BatchSearchResult[] }> {
104
+
await delay(800); // Simulate API delay
105
+
console.log('[MOCK] Searching for:', usernames);
106
+
107
+
const results: BatchSearchResult[] = usernames.map(username => ({
108
+
username,
109
+
actors: generateMockMatches(username),
110
+
error: undefined
111
+
}));
112
+
113
+
return { results };
114
+
},
115
+
116
+
async batchFollowUsers(dids: string[]): Promise<{
117
+
success: boolean;
118
+
total: number;
119
+
succeeded: number;
120
+
failed: number;
121
+
results: BatchFollowResult[];
122
+
}> {
123
+
await delay(1000);
124
+
console.log('[MOCK] Following users:', dids);
125
+
126
+
const results: BatchFollowResult[] = dids.map(did => ({
127
+
did,
128
+
success: true,
129
+
error: null
130
+
}));
131
+
132
+
return {
133
+
success: true,
134
+
total: dids.length,
135
+
succeeded: dids.length,
136
+
failed: 0,
137
+
results
138
+
};
139
+
},
140
+
141
+
async saveResults(
142
+
uploadId: string,
143
+
sourcePlatform: string,
144
+
results: SearchResult[]
145
+
): Promise<SaveResultsResponse> {
146
+
await delay(500);
147
+
console.log('[MOCK] Saving results:', { uploadId, sourcePlatform, count: results.length });
148
+
149
+
// Save to localStorage
150
+
localStorage.setItem(`mock_upload_${uploadId}`, JSON.stringify(results));
151
+
152
+
// Add to uploads list
153
+
const uploads = JSON.parse(localStorage.getItem('mock_uploads') || '[]');
154
+
const matchedUsers = results.filter(r => r.atprotoMatches.length > 0).length;
155
+
156
+
uploads.unshift({
157
+
uploadId,
158
+
sourcePlatform,
159
+
createdAt: new Date().toISOString(),
160
+
totalUsers: results.length,
161
+
matchedUsers,
162
+
unmatchedUsers: results.length - matchedUsers
163
+
});
164
+
165
+
localStorage.setItem('mock_uploads', JSON.stringify(uploads));
166
+
167
+
return {
168
+
success: true,
169
+
uploadId,
170
+
totalUsers: results.length,
171
+
matchedUsers,
172
+
unmatchedUsers: results.length - matchedUsers
173
+
};
174
+
},
175
+
176
+
cache: {
177
+
clear: () => console.log('[MOCK] Cache cleared'),
178
+
invalidate: (key: string) => console.log('[MOCK] Cache invalidated:', key),
179
+
invalidatePattern: (pattern: string) => console.log('[MOCK] Cache pattern invalidated:', pattern),
180
+
}
181
+
}
+19
src/lib/config.ts
+19
src/lib/config.ts
···
1
+
export const ENV = {
2
+
// Detect if we're in local mock mode
3
+
IS_LOCAL_MOCK: import.meta.env.VITE_LOCAL_MOCK === 'true',
4
+
5
+
// API base URL
6
+
API_BASE: import.meta.env.VITE_API_BASE || '/.netlify/functions',
7
+
8
+
// Feature flags
9
+
ENABLE_OAUTH: import.meta.env.VITE_ENABLE_OAUTH !== 'false',
10
+
ENABLE_DATABASE: import.meta.env.VITE_ENABLE_DATABASE !== 'false',
11
+
} as const;
12
+
13
+
export function isLocalMockMode(): boolean {
14
+
return ENV.IS_LOCAL_MOCK;
15
+
}
16
+
17
+
export function getApiUrl(endpoint: string): string {
18
+
return `${ENV.API_BASE}/${endpoint}`;
19
+
}