Skip to content

Commit 9a0dc9e

Browse files
committed
Skills Action added
1. User can now add skills on portfolio by going to /edit/skills route 2. User can also update skill name, level and image, action to delete skill is also added
1 parent 0d07648 commit 9a0dc9e

14 files changed

+272
-67
lines changed

.env.sample

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ CLOUDFLARE_ACCOUNT_ID=''
33
CLOUDFLARE_ACCESS_ID=''
44
CLOUDFLARE_ACCESS_KEY=''
55
CLOUDFLARE_R2_BUCKET_NAME=''
6-
AUTH_SECRET=""
6+
R2_PUBLIC_URI=''
7+
AUTH_SECRET='' # Added by `npx auth`. Read more: https://cli.authjs.dev
78
AUTH_GITHUB_ID=''
89
AUTH_GITHUB_SECRET=''

src/actions/skillActions.ts

+46-11
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,57 @@
11
"use server";
2-
import SkillModel, { Skill } from "@/model/skill.model";
3-
import S3, { r2_bucket_name } from "@/lib/cloudflareR2Connect";
4-
import { ChangeEvent, FormEvent } from "react";
5-
import { randomUUID } from "crypto";
6-
import axios from "axios";
2+
import SkillModel from "@/model/skill.model";
3+
import { revalidatePath } from "next/cache";
74

8-
export async function addSkill(
9-
formData: FormData,
10-
Key: string
11-
) {
5+
export async function addSkill(formData: FormData, Key: string) {
126
const skillName = formData.get("skill-name");
137
const profLevel = formData.get("prof-level");
14-
8+
const img = Key;
159
const newSkill = new SkillModel({
1610
name: skillName,
17-
img_url: `${process.env.R2_PUBLIC_URI}${Key}`,
11+
img_url: `${process.env.R2_PUBLIC_URI}${img}`,
1812
level: Number(profLevel),
13+
img: `${img}`,
1914
});
2015

16+
console.log(newSkill);
17+
2118
await newSkill.save();
19+
revalidatePath("/edit/skills");
20+
return;
21+
}
22+
23+
export async function updateSkill(
24+
formData: FormData,
25+
id: string,
26+
Key?: string
27+
) {
28+
const skillName = formData.get("skill-name");
29+
const profLevel = formData.get("prof-level");
30+
31+
const skill = await SkillModel.findById(id);
32+
if (!skill) {
33+
// console.log("error", id);
34+
return;
35+
}
36+
skill.name = skillName as string;
37+
skill.level = Number(profLevel);
38+
if (Key) {
39+
skill.img_url = `${process.env.R2_PUBLIC_URI}${Key}`;
40+
skill.img = Key;
41+
}
42+
await skill.save();
43+
revalidatePath("/edit/skills");
44+
return;
45+
}
46+
47+
export async function deleteSkill(id: string) {
48+
try {
49+
console.log(id)
50+
await SkillModel.findByIdAndDelete(id);
51+
revalidatePath("/edit/skills");
52+
return;
53+
} catch (err) {
54+
console.error("Error Deleteing Skill", err);
55+
return;
56+
}
2257
}
+73-16
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,80 @@
1+
import { auth } from "@/auth";
12
import S3, { r2_bucket_name } from "@/lib/cloudflareR2Connect";
23
import { ApiResponse } from "@/types/ApiResponse";
34
import { randomUUID } from "crypto";
5+
import { Session } from "next-auth";
46
import { NextRequest, NextResponse } from "next/server";
57

6-
export const GET = async (req: NextRequest) => {
7-
const fileType = req.nextUrl.searchParams.get("fileType") as string;
8-
const ext = fileType.split("/")[1];
9-
const Key = `${randomUUID()}.${ext}`;
10-
const params = {
11-
Bucket: r2_bucket_name,
12-
Key,
13-
ContentType: fileType,
14-
};
15-
const signedUrl = await S3.getSignedUrl("putObject", params);
8+
export const GET = auth(async (req: NextRequest & { auth: Session | null }) => {
169
const res: ApiResponse = {
17-
success: true,
18-
message: "OK",
19-
signedUrl,
20-
Key,
10+
success: false,
11+
message: "",
2112
};
22-
return NextResponse.json(res);
23-
};
13+
if (req.auth) {
14+
const fileType = req.nextUrl.searchParams.get("fileType") as string;
15+
const existingKey = req.nextUrl.searchParams.get("key") as string;
16+
const action = req.nextUrl.searchParams.get("action") as string;
17+
switch (action) {
18+
case "put":
19+
try {
20+
const ext = fileType.split("/")[1];
21+
if (!ext) {
22+
res.message = "Invalid File Type";
23+
throw new Error("Invalid File Type");
24+
}
25+
const Key = existingKey ? existingKey : `${randomUUID()}.${ext}`;
26+
const params = {
27+
Bucket: r2_bucket_name,
28+
Key,
29+
ContentType: fileType,
30+
};
31+
const signedUrl = await S3.getSignedUrl("putObject", params);
32+
res.success = true;
33+
res.message = "OK";
34+
res.signedUrl = signedUrl;
35+
res.Key = Key;
36+
return NextResponse.json(res, { status: 200 });
37+
} catch (error) {
38+
res.success = false;
39+
console.error("PresignedURL Error", error);
40+
res.message =
41+
res.message == ""
42+
? "Something Went Wrong, Check Server Logs for More Details"
43+
: res.message;
44+
return NextResponse.json(res, { status: 500 });
45+
}
46+
case "delete":
47+
try {
48+
if (!existingKey) {
49+
res.message = "Invalid Key";
50+
throw new Error("Invalid Key");
51+
}
52+
const params = {
53+
Bucket: r2_bucket_name,
54+
Key: existingKey,
55+
};
56+
const signedUrl = await S3.getSignedUrl("deleteObject", params);
57+
res.success = true;
58+
res.message = "OK";
59+
res.signedUrl = signedUrl;
60+
return NextResponse.json(res, { status: 200 });
61+
} catch (error) {
62+
res.success = false;
63+
console.error("PresignedURL Error", error);
64+
res.message =
65+
res.message == ""
66+
? "Something Went Wrong, Check Server Logs for More Details"
67+
: res.message;
68+
return NextResponse.json(res, { status: 500 });
69+
}
70+
default:
71+
res.success = false;
72+
res.message = res.message == "" ? "Invalid Request" : res.message;
73+
return NextResponse.json(res, { status: 500 });
74+
}
75+
} else {
76+
res.success = false;
77+
res.message = " Unauthorized";
78+
return NextResponse.json(res, { status: 401 });
79+
}
80+
});

src/app/edit/layout.tsx

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { auth } from "@/auth";
22
import { SignInButton } from "@/components/signInButton";
3+
import { SignOutButton } from "@/components/signOutButton";
34
import Link from "next/link";
45
import { ReactNode } from "react";
56

@@ -19,8 +20,8 @@ async function Layout({ children }: Readonly<{ children: ReactNode }>) {
1920
);
2021
return (
2122
<main className="wide-page flex flex-col md:flex-row gap-4">
22-
<div className="md:min-w-40 my-0 md:my-4 h-fit rounded-md border dark:border-zinc-700 dark:bg-zinc-800">
23-
<ul className="px-4 py-3 flex flex-wrap sm:flex-nowrap md:flex-col justify-start md:justify-center text-center md:text-start items-start gap-8 md:gap-3">
23+
<div className="md:min-w-40 my-0 md:my-4 h-fit flex flex-col gap-4">
24+
<ul className="px-4 py-3 flex flex-wrap sm:flex-nowrap md:flex-col justify-start md:justify-center text-center md:text-start items-start gap-8 md:gap-3 rounded-md border dark:border-zinc-700 dark:bg-zinc-800">
2425
<li className="w-fit sm:w-full cursor-pointer text-zinc-300 hover:text-zinc-200 font-light hover:scale-[1.02] transition-all duration-150 ease-in-out">
2526
<Link href="/edit/blogs">Blogs</Link>
2627
</li>
@@ -34,6 +35,7 @@ async function Layout({ children }: Readonly<{ children: ReactNode }>) {
3435
<Link href="/edit/resume">Resume</Link>
3536
</li>
3637
</ul>
38+
<SignOutButton/>
3739
</div>
3840
{children}
3941
</main>

src/app/edit/skills/page.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ async function EditSkill() {
1717
);
1818

1919
return (
20-
<div className="my-0 md:my-4 flex flex-col gap-6 w-full">
20+
<div className="my-0 md:my-4 flex flex-col gap-6 w-full md:max-h-[45rem] z-[-1] md:z-auto">
2121
<SkillDialog type="add" />
2222
<SkillTable/>
2323
</div>

src/components/header.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,15 @@ export default async function Header() {
2424
<div className="flex items-center gap-5">
2525
<nav className="hidden sm:block">
2626
<ul className="flex gap-5 items-center">
27-
<li>
27+
{/* <li>
2828
<PageLink href="/projects">Projects</PageLink>
2929
</li>
3030
<li>
3131
<PageLink href="/blog">Blog</PageLink>
3232
</li>
3333
<li>
3434
<PageLink href="/resume">Resume</PageLink>
35-
</li>
35+
</li> */}
3636
{session?.user && (
3737
<li>
3838
<PageLink href="/edit">Edit</PageLink>

src/components/iconText.tsx

+11-2
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,17 @@ function IconText({
1212
children?: ReactNode;
1313
}) {
1414
return (
15-
<div className="flex justify-center items-center gap-2 py-2 px-2 bg-zinc-200 dark:bg-zinc-800 rounded border border-zinc-300 dark:border-zinc-700 ">
16-
{src && <Image src={src} alt={alt ? alt : ""} width={20} height={20} priority/>}
15+
<div className="flex justify-center items-center gap-2 py-1 px-2 bg-zinc-200 dark:bg-zinc-800 rounded-md border border-zinc-300 dark:border-zinc-700 ">
16+
{src && (
17+
<Image
18+
src={src}
19+
alt={alt ? alt : ""}
20+
width={22}
21+
height={22}
22+
priority
23+
className="h-auto"
24+
/>
25+
)}
1726
<div className="text-sm text-zinc-700 dark:text-zinc-200 cursor-default">
1827
{children}
1928
</div>

src/components/signOutButton.tsx

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { signOut } from "@/auth";
2+
import { GoSignOut } from "react-icons/go";
3+
import { Button } from "./ui/button";
4+
5+
export function SignOutButton() {
6+
return (
7+
<form
8+
action={async () => {
9+
"use server";
10+
await signOut();
11+
}}
12+
>
13+
<Button type="submit" variant="secondary" className="flex items-center gap-2" >
14+
<span> Logout</span>
15+
<GoSignOut size={20}/>
16+
</Button>
17+
</form>
18+
);
19+
}
+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"use client";
2+
3+
import { Skill } from "@/model/skill.model";
4+
import { Button } from "../ui/button";
5+
import { MdDelete } from "react-icons/md";
6+
import axios from "axios";
7+
8+
function SkillDeleteButton({
9+
skill,
10+
deleteSkill,
11+
}: {
12+
skill: Skill;
13+
deleteSkill: (id: string) => void;
14+
}) {
15+
async function handleDelete(skill: Skill) {
16+
const key = skill.img;
17+
if (key) {
18+
axios
19+
.get(`/api/image/get-presigned-data?action=delete&key=${key}`)
20+
.then(async ({ data }) => {
21+
// console.log(data);
22+
await axios.delete(data.signedUrl);
23+
});
24+
}
25+
await deleteSkill(skill._id as string);
26+
return;
27+
}
28+
return (
29+
<Button
30+
variant="ghost"
31+
className="rounded-full"
32+
size="icon"
33+
onClick={async () => handleDelete(skill)}
34+
>
35+
<MdDelete size={24} />
36+
</Button>
37+
);
38+
}
39+
40+
export default SkillDeleteButton;

src/components/skill/skillDialog.tsx

+12-4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { Button } from "@/components/ui/button";
1010
import SkillForm from "@/components/skill/skillForm";
1111
import { Skill } from "@/model/skill.model";
1212
import { MdEdit } from "react-icons/md";
13+
import { addSkill, updateSkill } from "@/actions/skillActions";
1314

1415
function SkillDialog({ type, skill }: { type: "edit" | "add"; skill?: Skill }) {
1516
return (
@@ -19,9 +20,11 @@ function SkillDialog({ type, skill }: { type: "edit" | "add"; skill?: Skill }) {
1920
<Button variant="default" className="mt-2 max-w-[8rem]">
2021
Add Skill
2122
</Button>
22-
):(<Button variant="ghost" className="rounded-full" size="icon">
23-
<MdEdit size={24}/>
24-
</Button>)}
23+
) : (
24+
<Button variant="ghost" className="rounded-full" size="icon">
25+
<MdEdit size={24} />
26+
</Button>
27+
)}
2528
</DialogTrigger>
2629
<DialogContent className="w-full h-[15rem] md:w-[30rem] md:h-auto">
2730
<DialogHeader>
@@ -33,7 +36,12 @@ function SkillDialog({ type, skill }: { type: "edit" | "add"; skill?: Skill }) {
3336
when you're done
3437
</DialogDescription>
3538
</DialogHeader>
36-
<SkillForm skill={type == "edit" ? skill : undefined} type={type}/>
39+
<SkillForm
40+
skill={type == "edit" ? skill : undefined}
41+
type={type}
42+
addSkill={addSkill}
43+
updateSkill={type == "edit" ? updateSkill : undefined}
44+
/>
3745
</DialogContent>
3846
</Dialog>
3947
);

0 commit comments

Comments
 (0)