Skip to content

Commit 7295d50

Browse files
Merge pull request #21 from Eric-Zhang-Developer/feature/error-handling
Feature/error handling
2 parents f1a2a8e + 6ecd897 commit 7295d50

File tree

6 files changed

+197
-54
lines changed

6 files changed

+197
-54
lines changed
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
[
2+
{
3+
"title": "",
4+
"media_list_data": [
5+
6+
],
7+
"string_list_data": [
8+
{
9+
"href": "https://www.instagram.com/friend_alice",
10+
"value": "friend_alice",
11+
"timestamp": 1748307829
12+
}
13+
]
14+
},
15+
{
16+
"title": "",
17+
"media_list_data": [
18+
19+
],
20+
"string_list_data": [
21+
{
22+
"href": "https://www.instagram.com/friend_bob",
23+
"value": "friend_bob",
24+
"timestamp": 1747670084
25+
}
26+
]
27+
},
28+
{
29+
"title": "",
30+
"media_list_data": [
31+
32+
],
33+
"string_list_data": [
34+
{
35+
"href": "https://www.instagram.com/friend_charlie",
36+
"value": "friend_charlie",
37+
"timestamp": 1747261369
38+
}
39+
]
40+
},
41+
{
42+
"title": "",
43+
"media_list_data": [
44+
45+
],
46+
"string_list_data": [
47+
{
48+
"href": "https://www.instagram.com/spam_bot",
49+
"value": "spam_bot",
50+
"timestamp": 1727261369
51+
}
52+
]
53+
File renamed without changes.

src/app/__tests__/page.test.tsx

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
import { render, screen, waitFor } from "@testing-library/react";
22
import userEvent from "@testing-library/user-event";
33
import Home from "../page";
4+
// fs and path are for the malformed json so Jest does not freak out and throw a syntax error
5+
import fs from 'fs';
6+
import path from 'path';
47

58
import mockFollowers from "./__fixtures__/followers.json";
69
import mockFollowing from "./__fixtures__/following.json";
10+
import nonConformingData from "./__fixtures__/nonConformingData.json"
11+
712

813
// Since we are testing similar user flows the initial code is almost line for like the same
914

@@ -40,7 +45,7 @@ describe("User Flow", () => {
4045
});
4146

4247
// =================== //
43-
// Single File Uploads
48+
// Single File Uploads
4449
// =================== //
4550

4651
it("should display the Followers List Uploaded! banner when uploading a single proper followers.json file", async () => {
@@ -60,7 +65,7 @@ describe("User Flow", () => {
6065
expect(followersListUploaded).toBeInTheDocument();
6166
});
6267

63-
// This is almost the exact same as the previous test cause well its almost the exact same process to check for the banner.
68+
// This is almost the exact same as the previous test cause well its almost the exact same process to check for the banner.
6469
it("should display the Followers List Uploaded! banner when uploading a single proper followers.json file", async () => {
6570
// Assemble
6671
render(<Home></Home>);
@@ -77,10 +82,48 @@ describe("User Flow", () => {
7782
const followingListUploaded = await screen.findByText("Following List Uploaded!");
7883
expect(followingListUploaded).toBeInTheDocument();
7984
});
80-
});
8185

86+
// ============ //
87+
// Error States
88+
// ============ //
89+
90+
it("should an error message to the user when uploading a single nonConforming json file", async () => {
91+
// Assemble
92+
render(<Home></Home>);
93+
const user = userEvent.setup();
94+
const nonConforming = new File([JSON.stringify(nonConformingData)], "following.json", {
95+
type: "application/json",
96+
});
97+
98+
// Act
99+
const fileInput = screen.getByTestId("file-input");
100+
await user.upload(fileInput, [nonConforming]);
101+
102+
// Assert
103+
const errorFlag = await screen.findByText("Upload Failed!");
104+
expect(errorFlag).toBeInTheDocument();
105+
});
106+
107+
it("should an error message to the user when uploading a single malformed json file", async () => {
108+
// Assemble
109+
render(<Home></Home>);
110+
const user = userEvent.setup();
111+
112+
// This is the test case where we need fs and path due to syntax error
113+
const malformedFilePath = path.join(__dirname, '__fixtures__', 'malformed.json');
114+
const malformedFileContent = fs.readFileSync(malformedFilePath, 'utf-8');
115+
const malformedFile = new File([malformedFileContent], 'malformed.json', { type: 'application/json' });
116+
117+
// Act
118+
const fileInput = screen.getByTestId("file-input");
119+
await user.upload(fileInput, [malformedFile]);
120+
121+
// Assert
122+
const errorFlag = await screen.findByText("Upload Failed!");
123+
expect(errorFlag).toBeInTheDocument();
124+
});
125+
});
82126

83-
84127
describe("Reset Button", () => {
85128
it("should return me back to the drop / default section when I hit the reset button", async () => {
86129
render(<Home></Home>);

src/app/page.tsx

Lines changed: 50 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export default function Home() {
1515
const [hasProcessedFollowers, setHasProcessedFollowers] = useState(false);
1616
const [hasProcessedFollowing, setHasProcessedFollowing] = useState(false);
1717
const [hasProcessedDifference, setHasProcessedDifference] = useState(false);
18+
const [errorFlag, setErrorFlag] = useState(false);
1819

1920
// This function exists to compete the user life cycle on the page
2021
function handleReset() {
@@ -26,37 +27,49 @@ export default function Home() {
2627
setUserDifference([]);
2728
}
2829

29-
// On Drop reads files then checks them against the schemas and throws an error if broke
30-
// Extremely Robust for wrong json
31-
function onDrop(acceptedFiles: File[]) {
32-
acceptedFiles.forEach((file) => {
30+
// Helper function for onDrop
31+
const readFileAsText = (file: File): Promise<string> => {
32+
return new Promise((resolve, reject) => {
3333
const reader = new FileReader();
34+
reader.onload = () => resolve(reader.result as string);
35+
reader.onerror = () => reject(reader.error);
36+
reader.onabort = () => reject(new Error("File reading was aborted."));
37+
reader.readAsText(file);
38+
});
39+
};
3440

35-
reader.onabort = () => console.log("file reading was aborted");
36-
reader.onerror = () => console.log("file reading has failed");
37-
reader.onload = () => {
38-
try {
39-
const input = JSON.parse(reader.result as string);
41+
// On Drop reads files then checks them against the schemas and throws an error if broke
42+
// Extremely Robust for wrong json
43+
async function onDrop(acceptedFiles: File[]) {
44+
let foundAtLeastOneValidFile = false;
45+
46+
for (const file of acceptedFiles) {
47+
try {
48+
const fileContent = await readFileAsText(file);
4049

41-
const followingResult = FollowingListSchema.safeParse(input);
42-
if (followingResult.success) {
43-
setFollowing(ExtractNamesFromJson(followingResult.data.relationships_following));
44-
setHasProcessedFollowing(true);
45-
return;
46-
}
50+
const input = JSON.parse(fileContent);
4751

48-
const followerResult = FollowerListSchema.safeParse(input);
49-
if (followerResult.success) {
50-
setFollowers(ExtractNamesFromJson(followerResult.data));
51-
setHasProcessedFollowers(true);
52-
return;
53-
}
54-
} catch (error) {
55-
console.error("File is not a valid followers or following JSON:", error);
52+
const followingResult = FollowingListSchema.safeParse(input);
53+
if (followingResult.success) {
54+
setFollowing(ExtractNamesFromJson(followingResult.data.relationships_following));
55+
setHasProcessedFollowing(true);
56+
foundAtLeastOneValidFile = true;
57+
continue;
5658
}
57-
};
58-
reader.readAsText(file);
59-
});
59+
60+
const followerResult = FollowerListSchema.safeParse(input);
61+
if (followerResult.success) {
62+
setFollowers(ExtractNamesFromJson(followerResult.data));
63+
setHasProcessedFollowers(true);
64+
foundAtLeastOneValidFile = true;
65+
continue;
66+
}
67+
} catch { /* The file failed to parse, which is fine. We do nothing and let the loop continue */ }
68+
}
69+
70+
if (!foundAtLeastOneValidFile) {
71+
setErrorFlag(true);
72+
}
6073
}
6174

6275
// Calculate Diff
@@ -75,9 +88,18 @@ export default function Home() {
7588
</h1>
7689

7790
{!hasProcessedDifference ? (
78-
<HeroSection onDrop={onDrop} hasProcessedFollowers={hasProcessedFollowers} hasProcessedFollowing={hasProcessedFollowing}></HeroSection>
91+
<HeroSection
92+
onDrop={onDrop}
93+
errorFlag={errorFlag}
94+
setErrorFlag={setErrorFlag}
95+
hasProcessedFollowers={hasProcessedFollowers}
96+
hasProcessedFollowing={hasProcessedFollowing}
97+
></HeroSection>
7998
) : (
80-
<ResultsSection handleReset={handleReset} userDifference={userDifference}></ResultsSection>
99+
<ResultsSection
100+
handleReset={handleReset}
101+
userDifference={userDifference}
102+
></ResultsSection>
81103
)}
82104
</main>
83105

src/components/HeroSection.tsx

Lines changed: 45 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
11
// components/FileUploadZone.tsx
2-
import React from 'react';
2+
import React from "react";
33
import { Upload } from "lucide-react";
44
import Link from "next/link";
55
import Dropzone from "react-dropzone";
6-
import {HeroSectionProps} from "../lib/types"
6+
import { HeroSectionProps } from "../lib/types";
77

8-
export default function HeroSection({hasProcessedFollowers, hasProcessedFollowing, onDrop} : HeroSectionProps) {
8+
export default function HeroSection({
9+
hasProcessedFollowers,
10+
hasProcessedFollowing,
11+
onDrop,
12+
errorFlag,
13+
setErrorFlag,
14+
}: HeroSectionProps) {
915
return (
1016
<section className="flex flex-col container items-center gap-4">
1117
<h2 className="text-lg md:text-xl text-center">
1218
Safely see your non-followers using your official Instagram data. Your files are processed
1319
right here in your browser and are never uploaded anywhere.
1420
</h2>
15-
<Dropzone onDrop={onDrop} accept={{ "application/json": [".json"] }}>
21+
<Dropzone onDrop={onDrop} disabled={errorFlag} accept={{ "application/json": [".json"] }}>
1622
{({ getRootProps, getInputProps }) => (
1723
<div
1824
{...getRootProps()}
@@ -36,24 +42,41 @@ export default function HeroSection({hasProcessedFollowers, hasProcessedFollowin
3642
</div>
3743
)}
3844

39-
<Upload size={50}></Upload>
40-
<p className="text-center whitespace-normal break-words">
41-
Drag&nbsp;&amp;&nbsp;drop&nbsp;your
42-
<br />
43-
<span className="bg-slate-100 text-sm font-mono mx-1 px-2 py-1 rounded break-all">
44-
followers.json
45-
</span>
46-
&amp;
47-
<span className="bg-slate-100 text-sm font-mono mx-1 px-2 py-1 rounded break-all">
48-
following.json
49-
</span>
50-
<br />
51-
files&nbsp;here
52-
</p>
53-
<span>or</span>
54-
<button className="border-1 px-4 py-2 text-lg rounded-xl hover:cursor-pointer transition hover:scale-105">
55-
Select Files
56-
</button>
45+
{errorFlag ? (
46+
<section className="flex flex-col items-center gap-4 md:gap-6 text-center">
47+
<h2 className="text-3xl">Upload Failed!</h2>
48+
<p className="text-xl">
49+
The file doesn&apos;t look like a proper Instagram followers.json or following.json file
50+
</p>
51+
<button
52+
onClick={() => setErrorFlag(false)}
53+
className="border-1 px-4 py-2 text-lg rounded-xl hover:cursor-pointer transition hover:scale-105"
54+
>
55+
Try Again
56+
</button>
57+
</section>
58+
) : (
59+
<section className="flex flex-col items-center gap-4 md:gap-8">
60+
<Upload size={50}></Upload>
61+
<p className="text-center whitespace-normal break-words">
62+
Drag&nbsp;&amp;&nbsp;drop&nbsp;your
63+
<br />
64+
<span className="bg-slate-100 text-sm font-mono mx-1 px-2 py-1 rounded break-all">
65+
followers.json
66+
</span>
67+
&amp;
68+
<span className="bg-slate-100 text-sm font-mono mx-1 px-2 py-1 rounded break-all">
69+
following.json
70+
</span>
71+
<br />
72+
files&nbsp;here
73+
</p>
74+
<span>or</span>
75+
<button className="border-1 px-4 py-2 text-lg rounded-xl hover:cursor-pointer transition hover:scale-105">
76+
Select Files
77+
</button>
78+
</section>
79+
)}
5780
</div>
5881
)}
5982
</Dropzone>

src/lib/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ export interface HeroSectionProps{
4242
hasProcessedFollowers : boolean;
4343
hasProcessedFollowing : boolean;
4444
onDrop: (acceptedFiles: File[]) => void;
45+
errorFlag : boolean;
46+
setErrorFlag : (bool : boolean) => void;
4547
}
4648

4749
export interface ResultsSectionProps{

0 commit comments

Comments
 (0)