diff --git a/.gitignore b/.gitignore index b37a254..b0b3503 100644 --- a/.gitignore +++ b/.gitignore @@ -43,4 +43,6 @@ next-env.d.ts **/public/worker-*.js **/public/sw.js.map **/public/workbox-*.js.map -**/public/worker-*.js.map \ No newline at end of file +**/public/worker-*.js.map + +package-lock.json \ No newline at end of file diff --git a/app/actions/posts/jobs.ts b/app/actions/posts/jobs.ts index 82275a8..6eeb713 100644 --- a/app/actions/posts/jobs.ts +++ b/app/actions/posts/jobs.ts @@ -3,6 +3,15 @@ import { createJobSchema, createJobSchemaType } from "@/schema/jobs"; import { CheckUser } from "../users/checkUser"; import prisma from "@/db"; +import { v2 as cloudinary } from "cloudinary"; +import streamifier from "streamifier"; +import { File } from "buffer"; + +cloudinary.config({ + cloud_name: process.env.CLOUDINARY_CLOUD_NAME, + api_key: process.env.CLOUDINARY_API_KEY, + api_secret: process.env.CLOUDINARY_API_SECRET, +}); // Create new Jobs @@ -19,6 +28,8 @@ export async function CreateJob(postdata: createJobSchemaType) { data: { apply_link: postdata.apply_link, company: postdata.company, + company_logo: postdata.company_logo, + company_website: postdata.company_website, experience_level: postdata.experience_level, job_type: postdata.job_type, location: postdata.location, @@ -69,6 +80,8 @@ export async function GetAllPost() { id: true, apply_link: true, company: true, + company_logo: true, + company_website: true, experience_level: true, job_type: true, location: true, @@ -122,6 +135,8 @@ export async function GetPostByAuthorId(authorId: string) { id: true, apply_link: true, company: true, + company_logo: true, + company_website: true, experience_level: true, job_type: true, location: true, @@ -202,3 +217,55 @@ export async function DestroyPost(postId: string, authorId: string) { }; } } + +//Upload Image + +export async function UploadImage(data: FormData) { + try { + const file = data.get("image"); + + if (!file || !(file instanceof File)) { + return { + status: 401, + message: "File is not provided", + }; + } + //Converting the file instance to buffer + const bufferArr = await file.arrayBuffer(); + const buffer = Buffer.from(bufferArr); + + const uploadToCloud = new Promise((resolve, reject) => { + const cld = cloudinary.uploader.upload_stream( + { + use_filename: true, + folder: "jobjunction", + overwrite: false, + }, + (err: any, res: any) => { + if (err) { + reject(err); + } + resolve(res); + } + ); + + streamifier.createReadStream(buffer).pipe(cld); + }); + + const uploadedImageObject = await uploadToCloud; + + return { + status: 200, + message: "File uploaded successfully", + public_id: uploadedImageObject.public_id, + secure_url: uploadedImageObject.secure_url, + }; + } catch (error) { + return { + status: 200, + message: "File unot ploaded", + public_id: null, + secure_url: null, + }; + } +} diff --git a/components/Admin/Users/Users.tsx b/components/Admin/Users/Users.tsx index d83ee86..e59a525 100644 --- a/components/Admin/Users/Users.tsx +++ b/components/Admin/Users/Users.tsx @@ -13,10 +13,6 @@ export default function AllUser() { const session = useSession(); const [users, setUsers] = useState([]); const [loading, setLoading] = useState(false); - const [error, setError] = useState({ - status: false, - message: "", - }); useEffect(() => { const getAllUsers = async () => { @@ -27,17 +23,7 @@ export default function AllUser() { if (response.status !== 200) throw new Error(response.message); setUsers(response.data); } catch (error) { - setError({ - status: true, - message: (error as Error).message, - }); - - setTimeout(() => { - setError({ - status: true, - message: (error as Error).message, - }); - }, 1500); + toast((error as Error).message); } finally { setLoading(false); } @@ -51,8 +37,6 @@ export default function AllUser() { } return (
- {error.status ? toast(error.message) : ""} -
{loading ? (
diff --git a/components/Auth/Signin.tsx b/components/Auth/Signin.tsx index d75145e..c7cb065 100644 --- a/components/Auth/Signin.tsx +++ b/components/Auth/Signin.tsx @@ -26,11 +26,9 @@ export default function SigninForm() { }); const [submitting, setSubmitting] = useState(false); const [passwordClick, setPasswordClick] = useState(false); - const [error, setError] = useState(null); async function onSubmit(data: any) { setSubmitting(true); - setError(null); try { const res = await signIn("credentials", { username: data.username, @@ -39,18 +37,12 @@ export default function SigninForm() { }); if (res?.error) { - setError(true); - setTimeout(() => { - setError(false); - }, 2000); + toast("Username / Password mismatched"); } else { window.location.href = res?.url || "/jobs"; } - } catch (error: any) { - setError(true); - setTimeout(() => { - setError(false); - }, 2000); + } catch (error) { + toast("Username / Password mismatched"); } finally { setSubmitting(false); reset(); @@ -60,8 +52,6 @@ export default function SigninForm() { return (
- {error && toast("Username / Password mismatched")} -
diff --git a/components/Auth/Signup.tsx b/components/Auth/Signup.tsx index 22561f4..9ae0a2f 100644 --- a/components/Auth/Signup.tsx +++ b/components/Auth/Signup.tsx @@ -42,14 +42,6 @@ export default function SignupForm() { resolver: zodResolver(signupSchema), }); - const [error, setError] = useState<{ - status: boolean; - message: string; - }>({ - status: false, - message: "", - }); - const [success, setSuccess] = useState(false); const [passwordClick, setPasswordClick] = useState(false); const [submitting, setSubmitting] = useState(false); @@ -58,24 +50,16 @@ export default function SignupForm() { try { const response = await CreateUser(data); if (response.status !== 200) throw new Error(response.message); - setSuccess(true); - setTimeout(() => { - setSuccess(false); - }, 2000); + toast("Account created Successfully.", { + duration: 2000, + }); router.push("/signin"); setCurrentIndex(0); - } catch (error: any) { - setError({ - status: true, - message: error.message, + } catch (error) { + toast((error as Error).message, { + duration: 2000, }); - setTimeout(() => { - setError({ - status: false, - message: "", - }); - }, 2000); } finally { setSubmitting(false); reset(); @@ -114,16 +98,6 @@ export default function SignupForm() { return (
- {success && - toast("Account created Successfully.", { - duration: 2000, - })} - - {error.status && - toast(error.message, { - duration: 2000, - })} -
diff --git a/components/Job/Create/CreateForm.tsx b/components/Job/Create/CreateForm.tsx index eb4860c..44672e5 100644 --- a/components/Job/Create/CreateForm.tsx +++ b/components/Job/Create/CreateForm.tsx @@ -1,13 +1,12 @@ "use client"; -import { CreateJob } from "@/app/actions/posts/jobs"; +import { CreateJob, UploadImage } from "@/app/actions/posts/jobs"; import { Button } from "@/components/ui/button"; import { createJobSchema, createJobSchemaType } from "@/schema/jobs"; import { zodResolver } from "@hookform/resolvers/zod"; -import { Building, IndianRupee, Link } from "lucide-react"; -import { useSession } from "next-auth/react"; +import { Building, IndianRupee, Link, Upload } from "lucide-react"; import { useRouter } from "next/navigation"; -import { useEffect, useState } from "react"; +import { ChangeEvent, useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { FaSpinner } from "react-icons/fa"; import { PiOfficeChair } from "react-icons/pi"; @@ -37,7 +36,6 @@ export default function CreateForm() { resolver: zodResolver(createJobSchema), }); - const session: any = useSession(); const router = useRouter(); const watchSalaryToggle = watch("salary_disclosed"); @@ -46,41 +44,42 @@ export default function CreateForm() { setValue("salary_disclosed", true); //To make undefined to true intially }, []); - const [success, setSuccess] = useState(false); - const [error, setError] = useState<{ - status: boolean; - message: string; - }>({ - status: false, - message: "", - }); const [loading, setLoading] = useState(false); + const [logo, setLogo] = useState(null); + + async function handleLogoChange(e: ChangeEvent) { + let selectedFile = e.target.files ? e.target.files[0] : null; + if (selectedFile === undefined) return; //if user opens the model to select the file but came back without choosing an thing so holding prev val + setLogo(selectedFile); + } + + async function handleImageUpload() { + const formData = new FormData(); + if (logo) { + formData.append("image", logo); + } + const imageUrl = logo + ? await UploadImage(formData) + : { + status: 200, + message: "File unot ploaded", + public_id: null, + secure_url: "/Images/jj-logo.png", + }; // handling the logo "null" condition + return imageUrl; + } - async function onSubmit(data: any) { + async function onSubmit(data: createJobSchemaType) { setLoading(true); try { + const uploadedImageUrl = await handleImageUpload(); + data.company_logo = uploadedImageUrl.secure_url; const response = await CreateJob(data); - if (response.status !== 200) throw new Error(response.message); - - setSuccess(true); - setTimeout(() => { - setSuccess(false); - }, 1500); - + toast("Successfully created"); router.push("/jobs"); } catch (error) { - setError({ - status: true, - message: (error as Error).message, - }); - - setTimeout(() => { - setError({ - status: false, - message: "", - }); - }, 1500); + toast((error as Error).message); } finally { reset(); setLoading(false); @@ -90,10 +89,6 @@ export default function CreateForm() { return (
- {success ? toast("Successfully created") : ""} - - {error.status ? toast(error.message) : ""} - router.push("/jobs")} @@ -157,6 +152,41 @@ export default function CreateForm() { )}
+
+ + + + +
+ +
+ {errors.company_logo?.message && ( +

+ {errors.company_logo.message.toString()} +

+ )} +
+
diff --git a/components/Job/Jobs.tsx b/components/Job/Jobs.tsx index 1e7da6d..2b823ba 100644 --- a/components/Job/Jobs.tsx +++ b/components/Job/Jobs.tsx @@ -16,7 +16,6 @@ import { export default function AllJobsComp() { const [allJobs, setAllJobs] = useRecoilState(allJobListings); const [loading, setLoading] = useRecoilState(universalLoader); - const [error, setError] = useRecoilState(universalError); const errorNoPost = useRecoilValue(joblistingError); useEffect(() => { @@ -27,17 +26,7 @@ export default function AllJobsComp() { if (response.status !== 200) throw new Error(response.message); setAllJobs(response.data); } catch (error) { - setError({ - status: false, - message: (error as Error).message, - }); - - setTimeout(() => { - setError({ - status: true, - message: "", - }); - }, 1500); + toast((error as Error).message || "Error Occured"); } finally { setLoading(false); } @@ -46,10 +35,6 @@ export default function AllJobsComp() { getAllJobs(); }, []); - { - error.status ? toast(error.message || "Error Occured") : ""; - } - return ( <> {errorNoPost ? ( @@ -74,6 +59,8 @@ export default function AllJobsComp() { author={e.author} position={e.position} company={e.company} + company_logo={e.company_logo} + company_website={e.company_website} role_description={e.role_description} job_type={e.job_type} location={e.location} diff --git a/components/Job/MoreDialog.tsx b/components/Job/MoreDialog.tsx index 935f1b4..75992e1 100644 --- a/components/Job/MoreDialog.tsx +++ b/components/Job/MoreDialog.tsx @@ -1,7 +1,7 @@ "use client"; import { Bookmark, Trash2 } from "lucide-react"; -import { useEffect, useState } from "react"; +import { useEffect } from "react"; import { CheckForBookmark, @@ -22,17 +22,6 @@ export default function MoreOptionDialog({ authorId: string; }) { const [bookmarked, setBookmarked] = useRecoilState(bookmarkedPosts(postId)); - const [showBookmarkToast, setShowBookmarkToast] = useState({ - status: false, - message: "", - }); - - const [deleted, setDeleted] = useState(false); - const [deleteError, setDeleteError] = useState({ - status: false, - message: "", - }); - const session: any = useSession(); const router = useRouter(); @@ -44,23 +33,10 @@ export default function MoreOptionDialog({ ); if (response.status !== 200) throw new Error(response.message); setBookmarked(true); - setShowBookmarkToast({ - status: true, - message: response.message, - }); + toast(response.message); } catch (error) { setBookmarked(false); - setShowBookmarkToast({ - status: true, - message: (error as Error).message, - }); - } finally { - setTimeout(() => { - setShowBookmarkToast({ - status: false, - message: "", - }); - }, 100); + toast((error as Error).message); } } @@ -69,24 +45,10 @@ export default function MoreOptionDialog({ const response = await DestroyPost(postId, session.data.user.id); if (response.status !== 201) throw new Error(response.message); - setDeleted(true); - setTimeout(() => { - setDeleted(false); - }, 1000); - + toast("Deleted Successfully"); router.refresh(); } catch (error) { - setDeleteError({ - status: true, - message: (error as Error).message, - }); - setTimeout(() => { - setDeleteError({ - status: false, - message: "", - }); - }, 1000); - } finally { + toast((error as Error).message); } } @@ -106,17 +68,13 @@ export default function MoreOptionDialog({ return ( <> - {showBookmarkToast.status ? toast(showBookmarkToast.message) : null} - {deleted && toast("Deleted Successfully")} - {deleteError.status && toast(deleteError.message)} -
- {session.data?.user.id === authorId || + {/* {session.data?.user.id === authorId || session.data?.user.role === "ADMIN" ? (
handlePostDelete()}>
- ) : null} + ) : null} */}
handleBookmarkClick()}> -
-
-

- {position} -

- {author.role === "ADMIN" ? ( - - ) : null} +
+
+ Post-image
-
-

{company} |

-

- Posted{" "} - {diff > 0 - ? diff < 1 - ? "today" - : diff > 1 && diff < 2 - ? "yesterday" - : diff > 2 && diff < 6 - ? `${Math.ceil(diff)} days ago` - : `${Math.ceil(Math.ceil(diff) / 7)} weeks ago` - : "Invalid posting date"} - | -

- - feat: @{author.username} - +
+
+

+ {position} +

+ {author.role === "ADMIN" ? ( + + ) : null} +
+
+

{company} |

+

+ Posted{" "} + {diff > 0 + ? diff < 1 + ? "today" + : diff > 1 && diff < 2 + ? "yesterday" + : diff > 2 && diff < 6 + ? `${Math.ceil(diff)} days ago` + : `${Math.ceil(Math.ceil(diff) / 7)} weeks ago` + : "Invalid posting date"} +

+
@@ -83,6 +90,11 @@ export default function JobCard({

{location}

+
+ +

{experience_level}

+
+

{salary_disclosed && salary_min && salary_max ? ` ₹ ${Math.round(salary_min / 1000)}k - ₹ ${Math.round( @@ -95,16 +107,21 @@ export default function JobCard({ {/* third section */}

-
- -

{experience_level}

-
+ + ft: @{author.username.slice(0, 15)}.. + ( Randomstring.generate(8) @@ -65,10 +65,7 @@ export default function DeleteComp() { setModalOpen(false); signOut(); } catch (error) { - setError(true); - setTimeout(() => { - setError(false); - }, 1500); + toast("Error Occured"); } finally { setLoading(false); } @@ -80,7 +77,6 @@ export default function DeleteComp() { return (
- {error ? toast("Error Occured") : ""}

Delete Account

diff --git a/next.config.mjs b/next.config.mjs index d6499e3..ad496c8 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -27,6 +27,10 @@ const nextConfig = { protocol: "https", hostname: "t4.ftcdn.net", }, + { + protocol: "https", + hostname: "res.cloudinary.com", + }, ], }, }; diff --git a/package-lock.json b/package-lock.json index f6ece9d..3b56900 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,6 +45,7 @@ "@types/bcryptjs": "^2.4.6", "bcryptjs": "^2.4.3", "class-variance-authority": "^0.7.0", + "cloudinary": "^2.5.1", "clsx": "^2.1.1", "concurrently": "^9.0.1", "embla-carousel-autoplay": "^8.3.0", @@ -64,6 +65,7 @@ "react-icons": "^5.3.0", "recoil": "^0.7.7", "sonner": "^1.5.0", + "streamifier": "^0.1.1", "tailwind-merge": "^2.4.0", "tailwindcss-animate": "^1.0.7", "zod": "^3.23.8" @@ -76,6 +78,7 @@ "@types/react": "^18", "@types/react-dom": "^18", "@types/recoil": "^0.0.9", + "@types/streamifier": "^0.1.2", "eslint": "^8", "eslint-config-next": "14.2.5", "postcss": "^8", @@ -4085,6 +4088,15 @@ "@types/node": "*" } }, + "node_modules/@types/streamifier": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@types/streamifier/-/streamifier-0.1.2.tgz", + "integrity": "sha512-W53GwxY5Tunz3JUoP1YWDidrAadenSZKOBKMRl6RO3yThOFd03w7yx3HypI8ABGuUCGDHCHUml/5e45FWRt1TQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/trusted-types": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", @@ -5117,6 +5129,18 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/cloudinary": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/cloudinary/-/cloudinary-2.5.1.tgz", + "integrity": "sha512-CNg6uU53Hl4FEVynkTGpt5bQEAQWDHi3H+Sm62FzKf5uQHipSN2v7qVqS8GRVqeb0T1WNV+22+75DOJeRXYeSQ==", + "dependencies": { + "lodash": "^4.17.21", + "q": "^1.5.1" + }, + "engines": { + "node": ">=9" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -8904,6 +8928,16 @@ "node": ">=6" } }, + "node_modules/q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", + "deprecated": "You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.\n\n(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)", + "engines": { + "node": ">=0.6.0", + "teleport": ">=0.2.0" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -9645,6 +9679,14 @@ "node": ">= 0.4" } }, + "node_modules/streamifier": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/streamifier/-/streamifier-0.1.1.tgz", + "integrity": "sha512-zDgl+muIlWzXNsXeyUfOk9dChMjlpkq0DRsxujtYPgyJ676yQ8jEm6zzaaWHFDg5BNcLuif0eD2MTyJdZqXpdg==", + "engines": { + "node": ">=0.10" + } + }, "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", diff --git a/package.json b/package.json index 3d5618f..e449ef7 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "@types/bcryptjs": "^2.4.6", "bcryptjs": "^2.4.3", "class-variance-authority": "^0.7.0", + "cloudinary": "^2.5.1", "clsx": "^2.1.1", "concurrently": "^9.0.1", "embla-carousel-autoplay": "^8.3.0", @@ -71,6 +72,7 @@ "react-icons": "^5.3.0", "recoil": "^0.7.7", "sonner": "^1.5.0", + "streamifier": "^0.1.1", "tailwind-merge": "^2.4.0", "tailwindcss-animate": "^1.0.7", "zod": "^3.23.8" @@ -83,6 +85,7 @@ "@types/react": "^18", "@types/react-dom": "^18", "@types/recoil": "^0.0.9", + "@types/streamifier": "^0.1.2", "eslint": "^8", "eslint-config-next": "14.2.5", "postcss": "^8", diff --git a/prisma/migrations/20241113175522_/migration.sql b/prisma/migrations/20241113175522_/migration.sql new file mode 100644 index 0000000..6f08049 --- /dev/null +++ b/prisma/migrations/20241113175522_/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - Made the column `company_logo` on table `Post` required. This step will fail if there are existing NULL values in that column. + +*/ +-- AlterTable +ALTER TABLE "Post" ALTER COLUMN "company_logo" SET NOT NULL; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 98bfb21..20137a5 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -32,7 +32,7 @@ model Post { authorId String apply_link String company String - company_logo String? + company_logo String company_website String? experience_level String job_type String diff --git a/prisma/seed.ts b/prisma/seed.ts index 7f55cb9..802f710 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -123,6 +123,7 @@ async function insertPosts() { authorId: e.authorId, apply_link: e.apply_link, company: e.company, + company_logo: "/Images/jj-logo.png", experience_level: e.experience_level, job_type: e.job_type, location: e.location, diff --git a/schema/jobs.ts b/schema/jobs.ts index eadfad5..be5aff4 100644 --- a/schema/jobs.ts +++ b/schema/jobs.ts @@ -28,9 +28,7 @@ export const createJobSchema = z .string({ message: "Company name is required" }) .min(2, { message: "Extend it little" }), - company_logo: z - .string({ message: "Company's logo must be string" }) - .optional(), + company_logo: z.union([z.any().optional(), z.string().optional()]), company_website: z .string() .optional() diff --git a/types/types.ts b/types/types.ts index 94c4cf1..57c114d 100644 --- a/types/types.ts +++ b/types/types.ts @@ -68,6 +68,8 @@ export type JobLisitingType = { id: string; apply_link: string; company: string; + company_logo: string; + company_website: string | null; experience_level: string; job_type: string; location: string;