diff --git a/app/(app)/jobs/create/_client.tsx b/app/(app)/jobs/create/_client.tsx index f2a7bf63..3490be12 100644 --- a/app/(app)/jobs/create/_client.tsx +++ b/app/(app)/jobs/create/_client.tsx @@ -23,12 +23,20 @@ import { import { Strong, Text } from "@/components/ui-components/text"; import { Textarea } from "@/components/ui-components/textarea"; import { saveJobsInput, saveJobsSchema } from "@/schema/job"; +import { api } from "@/server/trpc/react"; import { FEATURE_FLAGS, isFlagEnabled } from "@/utils/flags"; +import { uploadFile } from "@/utils/s3helpers"; import { zodResolver } from "@hookform/resolvers/zod"; import Image from "next/image"; import { notFound } from "next/navigation"; import React, { useRef, useState } from "react"; import { Controller, SubmitHandler, useForm } from "react-hook-form"; +import { toast } from "sonner"; + +type CompanyLogo = { + status: "success" | "error" | "loading" | "idle"; + url: string | null; +}; export default function Content() { const { @@ -49,11 +57,65 @@ export default function Content() { relocation: false, visa_sponsorship: false, jobType: "full-time", + companyLogoUrl: "", }, }); const flagEnabled = isFlagEnabled(FEATURE_FLAGS.JOBS); const fileInputRef = useRef(null); - const [imgUrl, setImgUrl] = useState(null); + const [logoUrl, setLogoUrl] = useState({ + status: "idle", + url: "", + }); + const { mutate: getUploadUrl } = api.jobs.getUploadUrl.useMutation(); + + const uploadToUrl = async (signedUrl: string, file: File) => { + setLogoUrl({ status: "loading", url: "" }); + + if (!file) { + setLogoUrl({ status: "error", url: "" }); + toast.error("Invalid file upload."); + return; + } + + const response = await uploadFile(signedUrl, file); + const { fileLocation } = response; + + //TODO: Add url to Company logo in the database + + return fileLocation; + }; + + const logoChange = async (e: React.ChangeEvent) => { + if (e.target.files && e.target.files.length > 0) { + const file = e.target.files[0]; + const { size, type } = file; + + await getUploadUrl( + { size, type }, + { + onError(error) { + if (error) return toast.error(error.message); + return toast.error( + "Something went wrong uploading the logo, please retry.", + ); + }, + async onSuccess(signedUrl) { + const url = await uploadToUrl(signedUrl, file); + if (!url) { + return toast.error( + "Something went wrong uploading the logo, please retry.", + ); + } + setLogoUrl({ status: "success", url }); + toast.success( + "Company Logo successfully set. This may take a few minutes to update around the site.", + ); + }, + }, + ); + } + }; + const onSubmit: SubmitHandler = (values) => { console.log(values); }; @@ -76,7 +138,7 @@ export default function Content() {
Company Logo {}} + onChange={logoChange} className="hidden" ref={fileInputRef} /> + JPG, GIF or PNG. 1MB max. diff --git a/package-lock.json b/package-lock.json index 358ab319..29e80cf4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19162,4 +19162,4 @@ } } } -} +} \ No newline at end of file diff --git a/schema/job.ts b/schema/job.ts index 4167f7ba..aab4863d 100644 --- a/schema/job.ts +++ b/schema/job.ts @@ -5,6 +5,7 @@ export const saveJobsSchema = z.object({ .string() .min(1, "Company name should contain atleast 1 character") .max(50, "Company name should contain atmost 50 characters"), + companyLogoUrl: z.string().url().or(z.literal("")), jobTitle: z .string() .min(3, "Job title should contain atleast 3 character") @@ -29,4 +30,13 @@ export const saveJobsSchema = z.object({ jobType: z.enum(["full-time", "part-time", "freelancer", "other"]), }); +export const uploadCompanyLogoUrlSchema = z.object({ + type: z.string(), + size: z.number(), +}); + +export const updateCompanyLogoUrlSchema = z.object({ + url: z.string().url(), +}); + export type saveJobsInput = z.TypeOf; diff --git a/server/api/router/index.ts b/server/api/router/index.ts index d7274a63..b0d3d614 100644 --- a/server/api/router/index.ts +++ b/server/api/router/index.ts @@ -7,6 +7,7 @@ import { notificationRouter } from "./notification"; import { adminRouter } from "./admin"; import { reportRouter } from "./report"; import { tagRouter } from "./tag"; +import { jobsRouter } from "./jobs"; export const appRouter = createTRPCRouter({ post: postRouter, @@ -16,6 +17,7 @@ export const appRouter = createTRPCRouter({ admin: adminRouter, report: reportRouter, tag: tagRouter, + jobs: jobsRouter, }); // export type definition of API diff --git a/server/api/router/jobs.ts b/server/api/router/jobs.ts new file mode 100644 index 00000000..3e275283 --- /dev/null +++ b/server/api/router/jobs.ts @@ -0,0 +1,37 @@ +import { getUploadUrl } from "@/app/actions/getUploadUrl"; +import { createTRPCRouter, protectedProcedure } from "../trpc"; +import { uploadCompanyLogoUrlSchema } from "@/schema/job"; +import { TRPCError } from "@trpc/server"; +import { getPresignedUrl } from "@/server/common/getPresignedUrl"; + +export const jobsRouter = createTRPCRouter({ + getUploadUrl: protectedProcedure + .input(uploadCompanyLogoUrlSchema) + .mutation(async ({ ctx, input }) => { + const { size, type } = input; + const extension = type.split("/")[1]; + + const acceptedFormats = ["jpg", "jpeg", "gif", "png", "webp"]; + + if (!acceptedFormats.includes(extension)) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: `Invalid file. Accepted file formats: ${acceptedFormats.join(", ")}.`, + }); + } + + if (size > 1048576) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Maximum file size 1MB", + }); + } + + const response = await getPresignedUrl(type, size, { + kind: "companyLogo", + userId: ctx.session.user.id, + }); + + return response; + }), +}); diff --git a/server/common/getPresignedUrl.ts b/server/common/getPresignedUrl.ts index e38d6349..fb0c851c 100644 --- a/server/common/getPresignedUrl.ts +++ b/server/common/getPresignedUrl.ts @@ -21,6 +21,9 @@ function getKey( case "uploads": if (!config.userId) throw new Error("Invalid userId provided"); return `uploads/${config.userId}/${nanoid(16)}.${extension}`; + case "companyLogo": + if (!config.userId) throw new Error("Invalid userId provided"); + return `cl/${config.userId}/${nanoid(16)}.${extension}`; default: throw new Error("Invalid folder provided"); }