Skip to content

Commit cf90fa0

Browse files
Merge pull request #13 from Eric-Zhang-Developer/feature/middlware
Feature/middlware - MVP Completion
2 parents 90c5491 + 2e11aa6 commit cf90fa0

File tree

10 files changed

+587
-231
lines changed

10 files changed

+587
-231
lines changed

package-lock.json

Lines changed: 230 additions & 205 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,22 @@
1414
"next": "15.2.3",
1515
"react": "^19.0.0",
1616
"react-dom": "^19.0.0",
17-
"react-dropzone": "^14.3.8"
17+
"react-dropzone": "^14.3.8",
18+
"zod": "^3.25.67"
1819
},
1920
"devDependencies": {
2021
"@eslint/eslintrc": "^3",
2122
"@tailwindcss/postcss": "^4",
2223
"@testing-library/jest-dom": "^6.6.3",
2324
"@testing-library/react": "^16.3.0",
25+
"@testing-library/user-event": "^14.6.1",
2426
"@types/jest": "^30.0.0",
2527
"@types/node": "^20",
2628
"@types/react": "^19",
2729
"@types/react-dom": "^19",
2830
"eslint": "^9",
2931
"eslint-config-next": "15.2.3",
30-
"jest": "^30.0.2",
32+
"jest": "^30.0.3",
3133
"jest-environment-jsdom": "^30.0.2",
3234
"tailwindcss": "^4",
3335
"typescript": "^5"
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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+
}
54+
]
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
{
2+
"relationships_following": [
3+
{
4+
"title": "",
5+
"media_list_data": [
6+
7+
],
8+
"string_list_data": [
9+
{
10+
"href": "https://www.instagram.com/friend_alice",
11+
"value": "friend_alice",
12+
"timestamp": 1748627976
13+
}
14+
]
15+
},
16+
{
17+
"title": "",
18+
"media_list_data": [
19+
20+
],
21+
"string_list_data": [
22+
{
23+
"href": "https://www.instagram.com/friend_bob",
24+
"value": "friend_bob",
25+
"timestamp": 1748365568
26+
}
27+
]
28+
},
29+
{
30+
"title": "",
31+
"media_list_data": [
32+
33+
],
34+
"string_list_data": [
35+
{
36+
"href": "https://www.instagram.com/friend_charlie",
37+
"value": "friend_charlie",
38+
"timestamp": 1748029146
39+
}
40+
]
41+
},
42+
{
43+
"title": "",
44+
"media_list_data": [
45+
46+
],
47+
"string_list_data": [
48+
{
49+
"href": "https://www.instagram.com/ex_friend_dorthy",
50+
"value": "ex_friend_dorthy",
51+
"timestamp": 1737667668
52+
}
53+
]
54+
},
55+
{
56+
"title": "",
57+
"media_list_data": [
58+
59+
],
60+
"string_list_data": [
61+
{
62+
"href": "https://www.instagram.com/NASA",
63+
"value": "NASA",
64+
"timestamp": 1727667668
65+
}
66+
]
67+
}
68+
]
69+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
{
2+
"person": {
3+
"name": {
4+
"first": "John",
5+
"last": "Doe"
6+
},
7+
"age": 35,
8+
"isStudent": false,
9+
"email": "[email protected]",
10+
"address": {
11+
"street": "123 Main St",
12+
"city": "Anytown",
13+
"state": "CA",
14+
"postalCode": "12345"
15+
},
16+
"phoneNumbers": [
17+
{
18+
"type": "home",
19+
"number": "555-1234"
20+
},
21+
{
22+
"type": "work",
23+
"number": "555-5678"
24+
}
25+
],
26+
"favoriteFoods": [
27+
"Pizza",
28+
"Sushi",
29+
"Tacos"
30+
],
31+
"account": {
32+
"id": "acc-123456789",
33+
"status": "active",
34+
"lastLogin": "2023-10-27T10:00:00Z",
35+
"preferences": {
36+
"theme": "dark",
37+
"notifications": {
38+
"email": true,
39+
"sms": false,
40+
"push": true
41+
}
42+
}
43+
},
44+
"metadata": null
45+
},
46+
"products": [
47+
{
48+
"id": "prod-001",
49+
"name": "Super Widget",
50+
"description": "A high-quality widget with many features.",
51+
"price": 29.99,
52+
"inStock": true,
53+
"tags": ["electronics", "gadgets", "widgets"],
54+
"dimensions": {
55+
"length": 5.5,
56+
"width": 3.0,
57+
"height": 1.2
58+
}
59+
},
60+
{
61+
"id": "prod-002",
62+
"name": "Mega Gizmo",
63+
"description": "An even bigger and better gizmo.",
64+
"price": 79.50,
65+
"inStock": false,
66+
"tags": ["electronics", "gizmos", "deals"],
67+
"dimensions": {
68+
"length": 8.0,
69+
"width": 6.5,
70+
"height": 2.5
71+
}
72+
}
73+
],
74+
"apiVersion": "1.2.3",
75+
"requestStatus": {
76+
"code": 200,
77+
"message": "OK"
78+
}
79+
}

src/app/__tests__/page.test.tsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { render, screen, waitFor } from "@testing-library/react";
2+
import userEvent from "@testing-library/user-event";
3+
import Home from "../page";
4+
5+
import mockFollowers from "./__fixtures__/followers.json";
6+
import mockFollowing from "./__fixtures__/following.json";
7+
8+
describe("User Flow", () => {
9+
it("should show the users that don't follow me back and not show mutuals when I drop my following and followers files", async () => {
10+
// Assemble
11+
render(<Home></Home>);
12+
const user = userEvent.setup();
13+
const followers = new File([JSON.stringify(mockFollowers)], "followers.json", {
14+
type: "application/json",
15+
});
16+
const following = new File([JSON.stringify(mockFollowing)], "following.json", {
17+
type: "application/json",
18+
});
19+
20+
// Act
21+
const fileInput = screen.getByTestId("file-input");
22+
await user.upload(fileInput, [followers, following]);
23+
24+
await waitFor(() => {
25+
expect(screen.queryByTestId("file-input")).not.toBeInTheDocument();
26+
});
27+
28+
// Assert
29+
const nonFollowerFriend = await screen.findByRole("listitem", { name: /ex_friend_dorthy/i });
30+
expect(nonFollowerFriend).toBeInTheDocument();
31+
32+
const nonFollowerOrganization = await screen.findByRole("listitem", { name: /NASA/i });
33+
expect(nonFollowerOrganization).toBeInTheDocument();
34+
35+
const mutualFollowerItem = screen.queryByRole("listitem", { name: /friend_alice/i });
36+
expect(mutualFollowerItem).not.toBeInTheDocument();
37+
});
38+
});

src/app/page.tsx

Lines changed: 80 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,60 @@
11
"use client";
22

3+
import { useEffect, useState } from "react";
4+
import { FollowerListSchema, FollowingListSchema } from "@/lib/types";
35
import { Github, Upload } from "lucide-react";
46
import Link from "next/link";
5-
7+
import Dropzone from "react-dropzone";
8+
import ExtractNamesFromJson from "@/lib/extractNamesFromJson";
9+
import Compare from "@/lib/comparison";
610
export default function Home() {
11+
const [followers, setFollowers] = useState<string[]>([]);
12+
const [following, setFollowing] = useState<string[]>([]);
13+
const [userDifference, setUserDifference] = useState<string[]>([]);
14+
15+
const [hasProcessedFollowers, setHasProcessedFollowers] = useState(false);
16+
const [hasProcessedFollowing, setHasProcessedFollowing] = useState(false);
17+
const [hasProcessedDifference, setHasProcessedDifference] = useState(false);
18+
19+
function onDrop(acceptedFiles: File[]) {
20+
acceptedFiles.forEach((file) => {
21+
const reader = new FileReader();
22+
23+
reader.onabort = () => console.log("file reading was aborted");
24+
reader.onerror = () => console.log("file reading has failed");
25+
reader.onload = () => {
26+
try {
27+
const input = JSON.parse(reader.result as string);
28+
29+
const followingResult = FollowingListSchema.safeParse(input);
30+
if (followingResult.success) {
31+
setFollowing(ExtractNamesFromJson(followingResult.data.relationships_following));
32+
setHasProcessedFollowing(true);
33+
return;
34+
}
35+
36+
const followerResult = FollowerListSchema.safeParse(input);
37+
if (followerResult.success) {
38+
setFollowers(ExtractNamesFromJson(followerResult.data));
39+
setHasProcessedFollowers(true);
40+
return;
41+
}
42+
} catch (error) {
43+
console.error("File is not a valid followers or following JSON:", error);
44+
}
45+
};
46+
reader.readAsText(file);
47+
});
48+
}
49+
50+
// Calculate Diff
51+
useEffect(() => {
52+
if (hasProcessedFollowers && hasProcessedFollowing) {
53+
setUserDifference(Compare(following, followers));
54+
setHasProcessedDifference(true);
55+
}
56+
}, [hasProcessedFollowers, hasProcessedFollowing, following, followers]);
57+
758
return (
859
<div className="mx-auto container p-10">
960
<main className="flex items-center justify-center flex-col gap-4 px-12">
@@ -13,13 +64,34 @@ export default function Home() {
1364
right here in your browser and are never uploaded anywhere.
1465
</h2>
1566

16-
{/* Drop Zone */}
17-
<div className="border-2 px-1/2 py-30 flex flex-col gap-8 items-center justify-center w-1/2 border-dashed rounded-3xl mt-12 text-lg hover:cursor-pointer">
18-
<Upload size={50}></Upload>
19-
Drag & drop your followers.json & following.json files here
20-
<span>or</span>
21-
<button className="border-1 px-4 py-2 text-lg rounded-xl hover:cursor-pointer transition hover:scale-105">Select Files</button>
22-
</div>
67+
{!hasProcessedDifference ? (
68+
// Drop Zone
69+
<Dropzone onDrop={onDrop} accept={{ "application/json": [".json"] }}>
70+
{({ getRootProps, getInputProps }) => (
71+
<div
72+
{...getRootProps()}
73+
className="border-2 px-1/2 py-30 flex flex-col gap-8 items-center justify-center w-1/2 border-dashed rounded-3xl mt-12 text-lg hover:cursor-pointer"
74+
>
75+
<input {...getInputProps()} data-testid="file-input"></input>
76+
<Upload size={50}></Upload>
77+
Drag & drop your followers.json & following.json files here
78+
<span>or</span>
79+
<button className="border-1 px-4 py-2 text-lg rounded-xl hover:cursor-pointer transition hover:scale-105">
80+
Select Files
81+
</button>
82+
</div>
83+
)}
84+
</Dropzone>
85+
) : (
86+
<section>
87+
<p className="text-2xl mb-6">Processed!</p>
88+
<ol>
89+
{userDifference.map((userName) => (
90+
<li key={userName} aria-label={userName}>{userName}</li>
91+
))}
92+
</ol>
93+
</section>
94+
)}
2395

2496
<Link href="/tutorial" className="underline">
2597
{" "}

src/lib/extractNamesFromJson.ts

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,6 @@
1-
// The reason for these interfaces is so that typescript neatly throws an error when a user
2-
// adds a json that is not a followers.json or following.json format
1+
import { FollowingList, FollowerList } from "./types";
32

4-
interface ExpectedJSON {
5-
title: string;
6-
media_list_data: unknown[];
7-
string_list_data: stringListData[];
8-
}
9-
10-
interface stringListData{
11-
href: string;
12-
value: string;
13-
timestamp: number;
14-
}
15-
16-
export default function ExtractNamesFromJson(json: ExpectedJSON[]) {
3+
export default function ExtractNamesFromJson(json: FollowingList | FollowerList) {
174
const usernames = json.map(user => user.string_list_data[0].value);
185
return usernames;
196
}

0 commit comments

Comments
 (0)