Skip to content

Commit 44fe52d

Browse files
committed
Implement My CV page (with upload, update, delete and download cv services)
1 parent f57111f commit 44fe52d

File tree

10 files changed

+1094
-253
lines changed

10 files changed

+1094
-253
lines changed

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@
1414
"@types/react-dom": "18.2.7",
1515
"autoprefixer": "10.4.15",
1616
"eslint": "8.47.0",
17-
"eslint-config-next": "13.4.17",
18-
"next": "13.4.17",
17+
"eslint-config-next": "^14.1.0",
18+
"next": "^14.1.0",
1919
"next-auth": "^4.23.1",
2020
"postcss": "8.4.28",
21-
"react": "18.2.0",
22-
"react-dom": "18.2.0",
21+
"react": "^18.2.0",
22+
"react-dom": "^18.2.0",
2323
"react-qr-code": "^2.0.12",
2424
"tailwindcss": "3.3.3",
2525
"typescript": "5.1.6"

src/app/layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export default async function RootLayout({
3636
</div>
3737
{session && (
3838
<div className="h-[10%]">
39-
<BottomNavbar />
39+
{/* <BottomNavbar /> */}
4040
</div>
4141
)}
4242
</div>

src/app/my-cv/MyCVClient.tsx

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
"use client";
2+
3+
import Link from "next/link";
4+
import { useRef } from "react";
5+
6+
interface MyCVClientProps {
7+
cvInfo: CVInfo;
8+
cvDownloadLink: string;
9+
handleCVUpload: (formData: FormData) => Promise<void>;
10+
handleCVDelete: () => Promise<void>;
11+
}
12+
13+
export default function MyCVClient({
14+
cvInfo,
15+
cvDownloadLink,
16+
handleCVUpload,
17+
handleCVDelete,
18+
}: MyCVClientProps) {
19+
20+
const fileInputRef = useRef<HTMLInputElement | null>(null);
21+
const fileFormRef = useRef<HTMLFormElement | null>(null);
22+
23+
const cvIsPresent = Object.entries(cvInfo).length == 0 ? false : true;
24+
25+
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
26+
e.preventDefault();
27+
28+
if (e.target.files && e.target.files.length > 0) {
29+
const file = e.target.files[0];
30+
31+
if (file.type !== "application/pdf") {
32+
// TODO: provide feedback to user (e.g. an alert)
33+
return;
34+
}
35+
36+
if (file.size > 2097152) {
37+
// TODO: provide feedback to user (e.g. an alert)
38+
return;
39+
}
40+
41+
fileFormRef.current?.submit();
42+
}
43+
};
44+
45+
const handleFileSelect = () => {
46+
fileInputRef.current?.click();
47+
};
48+
49+
return (
50+
<div className="mt-4 py-10 px-5 bg-white text-dark-blue text-md font-medium border-4 border-dark-blue rounded-md flex flex-col items-center">
51+
{/* extension box */}
52+
<div className="border-2 border-dark-blue rounded-md w-24 h-24 flex items-center justify-center">
53+
{cvIsPresent ? (
54+
<p className="text-[#FF0000]">{cvInfo.extension}</p>
55+
) : (
56+
<p>-</p>
57+
)}
58+
</div>
59+
60+
{/* cv filename/download link */}
61+
{cvIsPresent ? (
62+
<Link href={cvDownloadLink} target="blank">
63+
<p className="mt-2 underline">{cvInfo.name}</p>
64+
</Link>
65+
) : (
66+
<p className="mt-2 text-gray">No CV uploaded yet</p>
67+
)}
68+
69+
{/* select/update button */}
70+
<form action={handleCVUpload} ref={fileFormRef}>
71+
<input
72+
name="cv"
73+
type="file"
74+
ref={fileInputRef}
75+
style={{ display: "none" }}
76+
onChange={handleFileChange}
77+
/>
78+
<button className="btn-blue mt-6" onClick={handleFileSelect}>
79+
{cvIsPresent ? "Update" : "Select File"}
80+
</button>
81+
</form>
82+
83+
{/* delete button */}
84+
{cvIsPresent && (
85+
<form action={handleCVDelete}>
86+
<button type="submit" className="btn-red mt-2">
87+
Delete
88+
</button>
89+
</form>
90+
)}
91+
92+
{/* go back button */}
93+
<Link href="/">
94+
<button className="btn-blue-outline mt-2">Go Back</button>
95+
</Link>
96+
</div>
97+
);
98+
}

src/app/my-cv/page.tsx

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import Image from "next/image";
2+
import { redirect } from "next/navigation";
3+
import { getServerSession } from "next-auth";
4+
5+
import MyCVClient from "./MyCVClient";
6+
import cvIcon from "@/assets/icons/cv.png";
7+
import UserSignOut from "@/components/UserSignOut";
8+
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
9+
import { UserService } from "@/services/UserService";
10+
11+
export default async function UploadCV() {
12+
const session = await getServerSession(authOptions);
13+
if (!session) redirect("/login");
14+
15+
const user: User = await UserService.getMe(session.cannonToken);
16+
if (!user) return <UserSignOut />;
17+
18+
const cvInfo: CVInfo = await UserService.getCVInfo(session.cannonToken);
19+
const cvDownloadLink = `${process.env.CANNON_URL}/files/me/download?access_token=${session.cannonToken}`;
20+
21+
const handleCVUpload = async (formData: FormData) => {
22+
"use server";
23+
24+
const cv: File | null = formData.get("cv") as unknown as File;
25+
if (cv == null) return; // TODO: provide feedback to user (e.g. an alert)
26+
27+
const success = await UserService.uploadCV(session.cannonToken, cv);
28+
if (!success) return; // TODO: provide feedback to user (e.g. an alert)
29+
};
30+
31+
const handleCVDelete = async () => {
32+
"use server";
33+
const success = await UserService.deleteCV(session.cannonToken);
34+
if (!success) return; // TODO: provide feedback to user (e.g. an alert)
35+
};
36+
37+
return (
38+
<div className="w-full h-full flex">
39+
<div className="w-[90%] sm:w-[400px] m-auto text-center">
40+
<div className="page-header bg-light-purple">
41+
<p>My CV</p>
42+
<Image src={cvIcon} alt="CV Icon" className="w-10" />
43+
</div>
44+
<MyCVClient
45+
cvInfo={cvInfo}
46+
cvDownloadLink={cvDownloadLink}
47+
handleCVUpload={handleCVUpload}
48+
handleCVDelete={handleCVDelete}
49+
/>
50+
</div>
51+
</div>
52+
);
53+
}

src/assets/icons/cv.png

1.64 KB
Loading

src/services/UserService.ts

Lines changed: 107 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,118 @@
1+
import { revalidateTag } from "next/cache";
2+
13
export const UserService = (() => {
24
const usersEndpoint = process.env.CANNON_URL + "/users";
5+
const filesEndpoint = process.env.CANNON_URL + "/files";
36

47
const getMe = async (cannonToken: string) => {
5-
const resp = await fetch(usersEndpoint + "/me", {
6-
headers: {
7-
"Content-Type": "application/json",
8-
Authorization: `Bearer ${cannonToken}`,
9-
},
10-
next: {
11-
revalidate: 300, // 5 mins
12-
tags: ["modified-me"],
13-
},
14-
});
15-
16-
if (resp.ok) return resp.json();
8+
try {
9+
const resp = await fetch(usersEndpoint + "/me", {
10+
headers: {
11+
"Content-Type": "application/json",
12+
Authorization: `Bearer ${cannonToken}`,
13+
},
14+
next: {
15+
revalidate: 300, // 5 mins
16+
tags: ["modified-me"],
17+
},
18+
});
19+
20+
if (resp.ok) return resp.json();
21+
22+
} catch (error) {
23+
console.error(error);
24+
}
1725
return null;
1826
};
1927

2028
const demoteMe = async (cannonToken: string) => {
21-
const resp = await fetch(usersEndpoint + "/me", {
22-
method: "PUT",
23-
headers: {
24-
"Content-Type": "application/json",
25-
Authorization: `Bearer ${cannonToken}`,
26-
},
27-
body: JSON.stringify({ role: "user" }),
28-
});
29-
30-
if (resp.ok) return true;
31-
return false;
29+
let success = false;
30+
31+
try {
32+
const resp = await fetch(usersEndpoint + "/me", {
33+
method: "PUT",
34+
headers: {
35+
"Content-Type": "application/json",
36+
Authorization: `Bearer ${cannonToken}`,
37+
},
38+
body: JSON.stringify({ role: "user" }),
39+
});
40+
41+
if (resp.ok) success = true;
42+
43+
} catch (error) {
44+
console.error(error);
45+
}
46+
return success;
47+
};
48+
49+
const getCVInfo = async (cannonToken: string) => {
50+
try {
51+
const resp = await fetch(filesEndpoint + "/me", {
52+
method: "GET",
53+
headers: {
54+
Authorization: `Bearer ${cannonToken}`,
55+
},
56+
next: {
57+
revalidate: 86400, // 1 day
58+
tags: ["modified-cv"],
59+
},
60+
});
61+
if (resp.ok) return resp.json();
62+
63+
} catch (error) {
64+
console.log(error);
65+
}
66+
return null;
67+
};
68+
69+
const uploadCV = async (cannonToken: string, cv: File) => {
70+
let success = false;
71+
72+
try {
73+
let formData = new FormData();
74+
formData.append("file", cv, cv.name);
75+
76+
const resp = await fetch(filesEndpoint + "/me", {
77+
method: "POST",
78+
headers: {
79+
Authorization: `Bearer ${cannonToken}`,
80+
},
81+
body: formData,
82+
});
83+
84+
if (resp.ok) {
85+
revalidateTag("modified-cv");
86+
success = true;
87+
}
88+
89+
} catch (error) {
90+
console.error(error);
91+
}
92+
return success;
93+
};
94+
95+
const deleteCV = async (cannonToken: string) => {
96+
let success = false;
97+
98+
try {
99+
const resp = await fetch(filesEndpoint + "/me", {
100+
method: "DELETE",
101+
headers: {
102+
Authorization: `Bearer ${cannonToken}`,
103+
},
104+
});
105+
106+
if (resp.ok) {
107+
revalidateTag("modified-cv");
108+
success = true;
109+
}
110+
111+
} catch (error) {
112+
console.log(error);
113+
}
114+
return success;
32115
};
33116

34-
return { getMe, demoteMe };
117+
return { getMe, demoteMe, getCVInfo, uploadCV, deleteCV };
35118
})();

src/styles/globals.css

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,18 @@
1313
@apply rounded-full bg-white text-black border-dark-blue font-semibold flex justify-center items-center gap-2;
1414
}
1515

16+
.page-header {
17+
@apply rounded-md px-5 py-3 text-xl font-semibold flex justify-between items-center text-xl;
18+
}
19+
20+
.btn-blue {
21+
@apply bg-dark-blue text-white border-2 border-dark-blue w-44 py-1 rounded-md text-[16px] font-semibold;
22+
}
23+
24+
.btn-blue-outline {
25+
@apply bg-white text-dark-blue border-2 border-dark-blue w-44 py-1 rounded-md text-[16px] font-semibold
26+
}
27+
1628
.baseNav {
1729
width: 100%;
1830
height: 60px/* 80px */;

src/types/globals.d.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,15 @@ type Company = {
6262
advertisementLvl: string;
6363
img: string;
6464
};
65+
66+
67+
type CVInfo = {
68+
id: string,
69+
user: string,
70+
name: string,
71+
kind: string,
72+
extension: string,
73+
updated: string,
74+
created: string,
75+
downloadLink: string
76+
}

tailwind.config.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@ const config: Config = {
1515
green: "#74C48A",
1616
purple: "#B17EC9",
1717
greenAC: "#338E2B",
18-
red: "#C02727",
18+
red: "#E43E3E",
1919
"dark-blue": "#083D77",
20+
"light-purple": "#5B58C4",
21+
gray: "#B4B4B4"
2022
},
2123
},
2224
plugins: [],

0 commit comments

Comments
 (0)