tangled
alpha
login
or
join now
byarielm.fyi
/
atlast
16
fork
atom
ATlast — you'll never need to find your favorites on another platform again. Find your favs in the ATmosphere.
atproto
16
fork
atom
overview
issues
1
pulls
pipelines
remove redundant file upload section
byarielm.fyi
4 months ago
e3123227
afbe1de2
+24
-276
3 changed files
expand all
collapse all
unified
split
src
components
FileUploadZone.tsx
lib
fileParser.ts
pages
Home.tsx
-55
src/components/FileUploadZone.tsx
···
1
1
-
import { Upload } from "lucide-react";
2
2
-
import { RefObject } from "react";
3
3
-
4
4
-
interface FileUploadZoneProps {
5
5
-
onFileChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
6
6
-
fileInputRef?: RefObject<HTMLInputElement | null>;
7
7
-
}
8
8
-
9
9
-
export default function FileUploadZone({ onFileChange, fileInputRef }: FileUploadZoneProps) {
10
10
-
return (
11
11
-
<div>
12
12
-
<div className="border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-xl p-6 text-center hover:border-blue-400 dark:hover:border-blue-500 focus-within:border-blue-400 dark:focus-within:border-blue-500 transition-colors">
13
13
-
<Upload className="w-12 h-12 text-gray-400 dark:text-gray-500 mx-auto mb-3" aria-hidden="true" />
14
14
-
<p className="text-lg font-medium text-gray-700 dark:text-gray-300 mb-1">Choose File</p>
15
15
-
<p className="text-sm text-gray-500 dark:text-gray-300 mb-3">TikTok Following.txt, Instagram HTML/JSON, or ZIP export</p>
16
16
-
17
17
-
<input
18
18
-
id="file-upload"
19
19
-
ref={fileInputRef}
20
20
-
type="file"
21
21
-
accept=".txt,.json,.html,.zip"
22
22
-
onChange={onFileChange}
23
23
-
className="sr-only"
24
24
-
aria-label="Upload following data file"
25
25
-
/>
26
26
-
27
27
-
<label
28
28
-
htmlFor="file-upload"
29
29
-
className="inline-block bg-blue-500 hover:bg-blue-600 text-white px-6 py-2 rounded-lg font-medium transition-colors cursor-pointer focus-within:ring-2 focus-within:ring-blue-400 focus-within:ring-offset-2"
30
30
-
tabIndex={0}
31
31
-
onKeyDown={(e) => {
32
32
-
if (e.key === 'Enter' || e.key === ' ') {
33
33
-
e.preventDefault();
34
34
-
document.getElementById('file-upload')?.click();
35
35
-
}
36
36
-
}}
37
37
-
>
38
38
-
Browse Files
39
39
-
</label>
40
40
-
</div>
41
41
-
42
42
-
<div className="mt-4 p-4 bg-blue-50 dark:bg-blue-900/20 rounded-xl">
43
43
-
<p className="text-sm text-blue-900 dark:text-blue-300">
44
44
-
💡 <strong>How to get your data:</strong>
45
45
-
</p>
46
46
-
<p className="text-sm text-blue-900 dark:text-blue-300 mt-2">
47
47
-
<strong>TikTok:</strong> Profile → Settings → Account → Download your data → Upload Following.txt
48
48
-
</p>
49
49
-
<p className="text-sm text-blue-900 dark:text-blue-300 mt-1">
50
50
-
<strong>Instagram:</strong> Profile → Settings → Accounts Center → Your information and permissions → Download your information → Upload following.html
51
51
-
</p>
52
52
-
</div>
53
53
-
</div>
54
54
-
);
55
55
-
}
-216
src/lib/fileParser.ts
···
1
1
-
import JSZip from "jszip";
2
2
-
import type { TikTokUser } from '../types';
3
3
-
4
4
-
export class FileParseError extends Error {
5
5
-
constructor(message: string) {
6
6
-
super(message);
7
7
-
this.name = 'FileParseError';
8
8
-
}
9
9
-
}
10
10
-
11
11
-
export const fileParser = {
12
12
-
async parseJsonFile(jsonText: string): Promise<TikTokUser[]> {
13
13
-
const users: TikTokUser[] = [];
14
14
-
const jsonData = JSON.parse(jsonText);
15
15
-
16
16
-
const followingArray = jsonData?.["Your Activity"]?.["Following"]?.["Following"];
17
17
-
18
18
-
if (!followingArray || !Array.isArray(followingArray)) {
19
19
-
throw new FileParseError(
20
20
-
"Could not find following data in JSON. Expected path: Your Activity > Following > Following"
21
21
-
);
22
22
-
}
23
23
-
24
24
-
for (const entry of followingArray) {
25
25
-
users.push({
26
26
-
username: entry.UserName,
27
27
-
date: entry.Date || "",
28
28
-
});
29
29
-
}
30
30
-
31
31
-
return users;
32
32
-
},
33
33
-
34
34
-
parseTxtFile(text: string): TikTokUser[] {
35
35
-
const users: TikTokUser[] = [];
36
36
-
const entries = text.split("\n\n").map((b) => b.trim()).filter(Boolean);
37
37
-
38
38
-
for (const entry of entries) {
39
39
-
const userMatch = entry.match(/Username:\s*(.+)/);
40
40
-
if (userMatch) {
41
41
-
users.push({ username: userMatch[1].trim(), date: "" });
42
42
-
}
43
43
-
}
44
44
-
45
45
-
return users;
46
46
-
},
47
47
-
48
48
-
async parseInstagramHtmlFile(htmlText: string): Promise<TikTokUser[]> {
49
49
-
const users: TikTokUser[] = [];
50
50
-
51
51
-
// Parse the HTML
52
52
-
const parser = new DOMParser();
53
53
-
const doc = parser.parseFromString(htmlText, 'text/html');
54
54
-
55
55
-
// Instagram following data is in divs with class "_a6-g uiBoxWhite noborder"
56
56
-
// The username is in an h2 with class "_a6-h _a6-i"
57
57
-
const userDivs = doc.querySelectorAll('div.pam._3-95._2ph-._a6-g.uiBoxWhite.noborder');
58
58
-
59
59
-
userDivs.forEach((div) => {
60
60
-
const h2 = div.querySelector('h2._3-95._2pim._a6-h._a6-i');
61
61
-
const dateDiv = div.querySelector('div._a6-p > div > div:nth-child(2)');
62
62
-
63
63
-
if (h2) {
64
64
-
const username = h2.textContent?.trim();
65
65
-
const date = dateDiv?.textContent?.trim() || '';
66
66
-
67
67
-
if (username) {
68
68
-
users.push({
69
69
-
username: username,
70
70
-
date: date
71
71
-
});
72
72
-
}
73
73
-
}
74
74
-
});
75
75
-
76
76
-
return users;
77
77
-
},
78
78
-
79
79
-
async parseInstagramJsonFile(jsonText: string): Promise<TikTokUser[]> {
80
80
-
const users: TikTokUser[] = [];
81
81
-
const jsonData = JSON.parse(jsonText);
82
82
-
83
83
-
// Instagram JSON exports can have different structures
84
84
-
// Try the most common structure first
85
85
-
let followingArray = jsonData?.relationships_following;
86
86
-
87
87
-
if (!followingArray && jsonData?.following) {
88
88
-
followingArray = jsonData.following;
89
89
-
}
90
90
-
91
91
-
if (!Array.isArray(followingArray)) {
92
92
-
throw new FileParseError(
93
93
-
"Could not find following data in Instagram JSON file"
94
94
-
);
95
95
-
}
96
96
-
97
97
-
for (const entry of followingArray) {
98
98
-
const username = entry.string_list_data?.[0]?.value || entry.username || entry.handle;
99
99
-
const timestamp = entry.string_list_data?.[0]?.timestamp || entry.timestamp;
100
100
-
101
101
-
if (username) {
102
102
-
users.push({
103
103
-
username: username,
104
104
-
date: timestamp ? new Date(timestamp * 1000).toISOString() : ''
105
105
-
});
106
106
-
}
107
107
-
}
108
108
-
109
109
-
return users;
110
110
-
},
111
111
-
112
112
-
async parseZipFile(file: File): Promise<TikTokUser[]> {
113
113
-
const zip = await JSZip.loadAsync(file);
114
114
-
115
115
-
// Try TikTok first
116
116
-
const followingFile =
117
117
-
zip.file("TikTok/Profile and Settings/Following.txt") ||
118
118
-
zip.file("Profile and Settings/Following.txt") ||
119
119
-
zip.files[
120
120
-
Object.keys(zip.files).find(
121
121
-
(path) => path.endsWith("Following.txt") && path.includes("Profile")
122
122
-
) || ""
123
123
-
];
124
124
-
125
125
-
if (followingFile) {
126
126
-
const followingText = await followingFile.async("string");
127
127
-
return this.parseTxtFile(followingText);
128
128
-
}
129
129
-
130
130
-
// Try Instagram HTML
131
131
-
const instagramFollowingHtml = Object.values(zip.files).find(
132
132
-
(f) => f.name.includes("following") && f.name.endsWith(".html") && !f.dir
133
133
-
);
134
134
-
135
135
-
if (instagramFollowingHtml) {
136
136
-
const htmlText = await instagramFollowingHtml.async("string");
137
137
-
return this.parseInstagramHtmlFile(htmlText);
138
138
-
}
139
139
-
140
140
-
// Try Instagram JSON
141
141
-
const instagramJsonFile = Object.values(zip.files).find(
142
142
-
(f) => (f.name.includes("following") || f.name.includes("connections")) &&
143
143
-
f.name.endsWith(".json") && !f.dir
144
144
-
);
145
145
-
146
146
-
if (instagramJsonFile) {
147
147
-
const jsonText = await instagramJsonFile.async("string");
148
148
-
return this.parseInstagramJsonFile(jsonText);
149
149
-
}
150
150
-
151
151
-
// If no specific file found, look for any JSON at the top level
152
152
-
const jsonFileEntry = Object.values(zip.files).find(
153
153
-
(f) => f.name.endsWith(".json") && !f.dir
154
154
-
);
155
155
-
156
156
-
if (!jsonFileEntry) {
157
157
-
throw new FileParseError(
158
158
-
"Could not find following data in the ZIP archive. Please ensure it contains Instagram's following.html or connections.json file."
159
159
-
);
160
160
-
}
161
161
-
162
162
-
const jsonText = await jsonFileEntry.async("string");
163
163
-
164
164
-
// Try Instagram JSON format first
165
165
-
try {
166
166
-
return this.parseInstagramJsonFile(jsonText);
167
167
-
} catch {
168
168
-
// Fall back to TikTok JSON format
169
169
-
return this.parseJsonFile(jsonText);
170
170
-
}
171
171
-
},
172
172
-
173
173
-
async parseFile(file: File): Promise<TikTokUser[]> {
174
174
-
let users: TikTokUser[];
175
175
-
let sourceFile = file.name;
176
176
-
177
177
-
if (file.name.endsWith(".json")) {
178
178
-
users = await this.parseJsonFile(await file.text());
179
179
-
} else if (file.name.endsWith(".txt")) {
180
180
-
users = await this.parseTxtFile(await file.text());
181
181
-
} else if (file.name.endsWith(".zip")) {
182
182
-
users = await this.parseZipFile(file);
183
183
-
// Determine which file was actually used from the ZIP
184
184
-
const zip = await JSZip.loadAsync(file);
185
185
-
const followingFile = zip.file("TikTok/Profile and Settings/Following.txt") ||
186
186
-
zip.file("Profile and Settings/Following.txt") ||
187
187
-
zip.files[Object.keys(zip.files).find(path => path.endsWith("Following.txt") && path.includes("Profile")) || ""];
188
188
-
189
189
-
if (followingFile) {
190
190
-
sourceFile = `${file.name} (TikTok Following.txt)`;
191
191
-
} else {
192
192
-
const instagramFollowingHtml = Object.values(zip.files).find(
193
193
-
(f) => f.name.includes("following") && f.name.endsWith(".html") && !f.dir
194
194
-
);
195
195
-
if (instagramFollowingHtml) {
196
196
-
sourceFile = `${file.name} (Instagram ${instagramFollowingHtml.name})`;
197
197
-
} else {
198
198
-
const instagramJsonFile = Object.values(zip.files).find(
199
199
-
(f) => (f.name.includes("following") || f.name.includes("connections")) &&
200
200
-
f.name.endsWith(".json") && !f.dir
201
201
-
);
202
202
-
if (instagramJsonFile) {
203
203
-
sourceFile = `${file.name} (Instagram ${instagramJsonFile.name})`;
204
204
-
}
205
205
-
}
206
206
-
}
207
207
-
} else if (file.name.endsWith(".html")) {
208
208
-
users = await this.parseInstagramHtmlFile(await file.text());
209
209
-
} else {
210
210
-
throw new FileParseError("Please upload a .txt, .json, .html, or .zip file");
211
211
-
}
212
212
-
213
213
-
console.log(`Parsed ${users.length} users from: ${sourceFile}`);
214
214
-
return users;
215
215
-
}
216
216
-
};
+24
-5
src/pages/Home.tsx
···
2
2
import { useState, useEffect, useRef } from "react";
3
3
import AppHeader from "../components/AppHeader";
4
4
import PlatformSelector from "../components/PlatformSelector";
5
5
-
import FileUploadZone from "../components/FileUploadZone";
6
5
import { apiClient } from "../lib/apiClient";
7
6
import type { Upload as UploadType } from "../types";
8
7
···
94
93
</h2>
95
94
</div>
96
95
<p className="text-gray-600 dark:text-gray-400 mb-6">
97
97
-
Upload your exported data from any platform to find matches on the ATmosphere
96
96
+
Click a platform below to upload your exported data and find matches on the ATmosphere
98
97
</p>
99
98
100
99
<PlatformSelector onPlatformSelect={handlePlatformSelect} />
101
101
-
<FileUploadZone
102
102
-
onFileChange={(e) => onFileUpload(e, selectedPlatform || 'tiktok')}
103
103
-
fileInputRef={fileInputRef} />
100
100
+
101
101
+
{/* Hidden file input */}
102
102
+
<input
103
103
+
id="file-upload"
104
104
+
ref={fileInputRef}
105
105
+
type="file"
106
106
+
accept=".txt,.json,.html,.zip"
107
107
+
onChange={(e) => onFileUpload(e, selectedPlatform || 'tiktok')}
108
108
+
className="sr-only"
109
109
+
aria-label="Upload following data file"
110
110
+
/>
111
111
+
112
112
+
<div className="mt-4 p-4 bg-blue-50 dark:bg-blue-900/20 rounded-xl">
113
113
+
<p className="text-sm text-blue-900 dark:text-blue-300">
114
114
+
💡 <strong>How to get your data:</strong>
115
115
+
</p>
116
116
+
<p className="text-sm text-blue-900 dark:text-blue-300 mt-2">
117
117
+
<strong>TikTok:</strong> Profile → Settings → Account → Download your data → Upload Following.txt
118
118
+
</p>
119
119
+
<p className="text-sm text-blue-900 dark:text-blue-300 mt-1">
120
120
+
<strong>Instagram:</strong> Profile → Settings → Accounts Center → Your information and permissions → Download your information → Upload following.html
121
121
+
</p>
122
122
+
</div>
104
123
</div>
105
124
106
125
{/* Upload History Section */}