From 68ec026891edd5b4b7a67cc5cfa46925eda2e165 Mon Sep 17 00:00:00 2001 From: Anuar Kapbasov Date: Tue, 5 Nov 2024 00:56:25 +0500 Subject: [PATCH 1/7] add create resume functions --- src/api/resume.ts | 32 +++++++++ src/app/(with-nav)/resume/create/page.tsx | 87 ++++++++++++++++++++--- src/lib/formValidationSchemas.ts | 16 ++--- 3 files changed, 113 insertions(+), 22 deletions(-) diff --git a/src/api/resume.ts b/src/api/resume.ts index 6bb86c7..e32abae 100644 --- a/src/api/resume.ts +++ b/src/api/resume.ts @@ -1,3 +1,4 @@ +"use server"; import { pocketbase } from "./pocketbase"; export const getResume = async (id: string) => { @@ -8,10 +9,41 @@ export const getResume = async (id: string) => { export const createResume = async (resume: any) => { const pb = pocketbase(); + const data = await pb.collection("resume").create(resume); return data; }; +export const createExperience = async (experiences: any) => { + const pb = pocketbase(); + + const experiencePromises = experiences.map(async (experience: any) => { + return await pb.collection("experience").create(experience); + }); + + const results = await Promise.all(experiencePromises); + return results; +}; + +export const createSingleExperience = async ( + id: string, + company: string, + description: string, + startDate: string, + endDate: string +) => { + const pb = pocketbase(); + const experience = { + id, + company, + description, + startDate, + endDate, + }; + await pb.collection("experience").create(experience); +}; + + export const hasResume = async (userId: string) => { const pb = pocketbase(); const resumes = await pb.collection("users").getList(1, 1, { diff --git a/src/app/(with-nav)/resume/create/page.tsx b/src/app/(with-nav)/resume/create/page.tsx index ffb1a6b..77c3812 100644 --- a/src/app/(with-nav)/resume/create/page.tsx +++ b/src/app/(with-nav)/resume/create/page.tsx @@ -16,6 +16,7 @@ import { Textarea } from "@/components/ui/textarea"; import Image from "next/image"; import ExperienceForm from "@/components/ExperienceForm"; import { Experience } from "@/types/resume"; +import { createExperience, createResume } from "@/api/resume"; const ResumeForm = () => { const { @@ -33,7 +34,62 @@ const ResumeForm = () => { const [skillInput, setSkillInput] = useState(""); const [experience, setExperience] = useState([]); - const onSubmit = (data: ResumeCreationSchema) => {}; + const fileInput = document.getElementById("file-input") as HTMLInputElement; + + const onSubmit = async (data: ResumeCreationSchema) => { + console.log("resume data: ", data); + console.log("experience data: ", experience); + try { + // const formattedData = (resume: any) => ({ + // full_name: resume.fullName, + // skills: resume.skills.join(", "), + // salary: resume.expectedSalary, + // city: resume.city, + // education_levels: resume.education, + // education: resume.placesOfStudy.join(", "), + // employment_type: resume.typeOfEmployment, + // email: resume.email, + // phone_number: resume.phone, + // about: resume.aboutMyself, + // img: resume.img, + // }); + const formattedData = (resume: any) => { + const formData = new FormData(); + formData.append("full_name", resume.fullName); + formData.append("skills", resume.skills.join(", ")); + formData.append("salary", resume.expectedSalary); + formData.append("city", resume.city); + formData.append("education_levels", resume.education); + formData.append("education", resume.placesOfStudy.join(", ")); + formData.append("employment_type", resume.typeOfEmployment); + formData.append("about", resume.aboutMyself); + + fileInput.addEventListener("change", function () { + if (fileInput.files && fileInput.files.length > 0) { + const file = fileInput.files[0]; + formData.append("img", file); + } + }); + + return formData; + }; + + const resume = await createResume(formattedData(data)); + console.log("submitted resume data: ", resume); + const experiences = experience.map((exp) => ({ + resume: resume.id, + company_name: exp.company, + description: exp.jobDescription, + start_date: exp.startDate, + end_date: exp.endDate, + })); + const experienceData = await createExperience(experiences); + console.log("submitted experience data: ", experienceData); + console.log("test: ", experiences); + } catch (error) { + console.log(error); + } + }; const submitWorkExperience = (e: React.FormEvent) => { e.preventDefault(); @@ -44,7 +100,7 @@ const ResumeForm = () => { }; const convertToExperienceList = (obj: any): Experience[] => { - /* + /* we have have object with entries workExperience..company workExperience..endDate @@ -105,9 +161,18 @@ const ResumeForm = () => { setImagePreview(reader.result as string); }; reader.readAsDataURL(file); + console.log("file", file); } }; + // const handleUploadedFile = (event: React.ChangeEvent) => { + // const file = event?.target?.files?.[0]; + + // const urlImage = URL.createObjectURL(file: any); + + // setPreview(urlImage); + // }; + const triggerFileSelect = () => { document.getElementById("file-input")?.click(); }; @@ -404,10 +469,11 @@ const ResumeForm = () => { className="ring-1 ring-gray-300 p-2 rounded-md text-sm w-full" {...register("education")} > - - - - + + + + + {errors.typeOfEmployment?.message && (

@@ -528,10 +594,11 @@ const ResumeForm = () => { className="ring-[1px] ring-gray-300 p-2 rounded-md text-sm w-full" {...register("typeOfEmployment")} > - - - - + + + + + {errors.typeOfEmployment?.message && (

diff --git a/src/lib/formValidationSchemas.ts b/src/lib/formValidationSchemas.ts index 9beaa88..2c2a34c 100644 --- a/src/lib/formValidationSchemas.ts +++ b/src/lib/formValidationSchemas.ts @@ -48,7 +48,7 @@ export const signInSchema = z.object({ export type SignUpSchema = z.infer; -const MAX_FILE_SIZE = 20000000; +const MAX_FILE_SIZE = 200000000; const ACCEPTED_IMAGE_TYPES = ["image/jpeg", "image/jpg", "image/png", "image/webp"]; export const resumeCreationSchema = z.object({ @@ -88,7 +88,7 @@ export const resumeCreationSchema = z.object({ .optional() .default([]), - education: z.string().nonempty({ message: "Education is required!" }), + education: z.enum(["bachelor", "masters", "high school", "doctorate", "college"], {errorMap: () => ({ message: "Please select a valid education level" })}), placesOfStudy: z.array(z.string()).optional().default([]), skills: z.array(z.string()).optional().default([]), expectedSalary: z.preprocess((value) => Number(value), z @@ -96,21 +96,13 @@ export const resumeCreationSchema = z.object({ .positive({ message: "Salary must be a positive number" }) ), typeOfEmployment: z.enum( - ["Full-time", "Part-time", "Freelance", "Contract"], + ["full_time", "part_time", "project", "internship", "volutury"], { errorMap: () => ({ message: "Please select a valid type of employment" }), } ), img: z - .any() - .refine((file) => file?.size <= MAX_FILE_SIZE, { - message: `Max file size is${MAX_FILE_SIZE / 1000000}MB`, - }) - .refine( - (file) => ACCEPTED_IMAGE_TYPES.includes(file?.type), - "Only .jpg, .jpeg, .png and .webp formats are supported." - ) - .optional(), + .any(), aboutMyself: z .string() .min(10, { message: "About myself must be at least 10 characters" }), From 15e7638c9abb4b1c7e3fc149a621fedae1da9e60 Mon Sep 17 00:00:00 2001 From: Anuar Kapbasov Date: Tue, 5 Nov 2024 11:49:01 +0500 Subject: [PATCH 2/7] add user to resume relation func --- src/api/resume.ts | 8 +++++ src/app/(with-nav)/resume/create/page.tsx | 44 ++++++++++++++++++----- 2 files changed, 43 insertions(+), 9 deletions(-) diff --git a/src/api/resume.ts b/src/api/resume.ts index e32abae..3ee07db 100644 --- a/src/api/resume.ts +++ b/src/api/resume.ts @@ -7,6 +7,14 @@ export const getResume = async (id: string) => { return resume; }; +export const userToResume = async (resume: string) => { + const pb = pocketbase(); + const user = await pb.collection("users").update(resume, { + resume: resume, + }); + return user; +}; + export const createResume = async (resume: any) => { const pb = pocketbase(); diff --git a/src/app/(with-nav)/resume/create/page.tsx b/src/app/(with-nav)/resume/create/page.tsx index 77c3812..5392f56 100644 --- a/src/app/(with-nav)/resume/create/page.tsx +++ b/src/app/(with-nav)/resume/create/page.tsx @@ -16,7 +16,8 @@ import { Textarea } from "@/components/ui/textarea"; import Image from "next/image"; import ExperienceForm from "@/components/ExperienceForm"; import { Experience } from "@/types/resume"; -import { createExperience, createResume } from "@/api/resume"; +import { createExperience, createResume, userToResume } from "@/api/resume"; +import { useRouter } from "next/navigation"; const ResumeForm = () => { const { @@ -26,6 +27,7 @@ const ResumeForm = () => { } = useForm({ resolver: zodResolver(resumeCreationSchema), }); + const router = useRouter(); const [imagePreview, setImagePreview] = useState(null); const [options, setOptions] = useState([]); @@ -35,6 +37,7 @@ const ResumeForm = () => { const [experience, setExperience] = useState([]); const fileInput = document.getElementById("file-input") as HTMLInputElement; + const formData = new FormData(); const onSubmit = async (data: ResumeCreationSchema) => { console.log("resume data: ", data); @@ -54,7 +57,6 @@ const ResumeForm = () => { // img: resume.img, // }); const formattedData = (resume: any) => { - const formData = new FormData(); formData.append("full_name", resume.fullName); formData.append("skills", resume.skills.join(", ")); formData.append("salary", resume.expectedSalary); @@ -63,13 +65,13 @@ const ResumeForm = () => { formData.append("education", resume.placesOfStudy.join(", ")); formData.append("employment_type", resume.typeOfEmployment); formData.append("about", resume.aboutMyself); - - fileInput.addEventListener("change", function () { - if (fileInput.files && fileInput.files.length > 0) { - const file = fileInput.files[0]; - formData.append("img", file); - } - }); + formData.append("img", resume.img.files[0]); + // fileInput.addEventListener("change", function () { + // if (fileInput.files && fileInput.files.length > 0) { + // const file = fileInput.files[0]; + // formData.append("img", file); + // } + // }); return formData; }; @@ -84,13 +86,37 @@ const ResumeForm = () => { end_date: exp.endDate, })); const experienceData = await createExperience(experiences); + const userData = await userToResume(resume.id); console.log("submitted experience data: ", experienceData); console.log("test: ", experiences); + // router.back(); } catch (error) { console.log(error); } }; + // async function imageToBlob(imageFile: File): Promise { + // return new Promise((resolve, reject) => { + // const reader = new FileReader(); + + // reader.onloadend = () => { + // const result = reader.result; + // if (result instanceof ArrayBuffer) { + // resolve(new Blob([result])); + // } else if (result) { + // resolve(new Blob([new Uint8Array(result as ArrayBufferLike)])); + // } else { + // reject(new Error("Ошибка при чтении файла изображения.")); + // } + // }; + + // reader.onerror = () => { + // reject(new Error("Ошибка при чтении файла изображения.")); + // }; + + // reader.readAsArrayBuffer(imageFile); + // }); + // } const submitWorkExperience = (e: React.FormEvent) => { e.preventDefault(); const formData = new FormData(e.currentTarget); From 92baf4a0633b44dea2087da7b6322c4f649ddbac Mon Sep 17 00:00:00 2001 From: Anuar Kapbasov Date: Tue, 5 Nov 2024 16:17:32 +0500 Subject: [PATCH 3/7] resume basic functionality --- package-lock.json | 9 -------- package.json | 1 - src/api/resume.ts | 6 +++--- src/app/(with-nav)/resume/[id]/page.tsx | 1 - src/app/(with-nav)/resume/create/page.tsx | 14 +++++++++++-- src/lib/formValidationSchemas.ts | 25 ++++++++++++++--------- 6 files changed, 30 insertions(+), 26 deletions(-) diff --git a/package-lock.json b/package-lock.json index c3870f2..85fe533 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,7 +30,6 @@ "react": "^18", "react-dom": "^18", "react-hook-form": "^7.53.0", - "react-icons": "^5.3.0", "sonner": "^1.5.0", "tailwind-merge": "^2.5.2", "tailwindcss-animate": "^1.0.7", @@ -3282,14 +3281,6 @@ "react": "^16.8.0 || ^17 || ^18 || ^19" } }, - "node_modules/react-icons": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.3.0.tgz", - "integrity": "sha512-DnUk8aFbTyQPSkCfF8dbX6kQjXA9DktMeJqfjrg6cK9vwQVMxmcA3BfP4QoiztVmEHtwlTgLFsPuH2NskKT6eg==", - "peerDependencies": { - "react": "*" - } - }, "node_modules/react-remove-scroll": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.0.tgz", diff --git a/package.json b/package.json index 67ce366..45c17bd 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,6 @@ "react": "^18", "react-dom": "^18", "react-hook-form": "^7.53.0", - "react-icons": "^5.3.0", "sonner": "^1.5.0", "tailwind-merge": "^2.5.2", "tailwindcss-animate": "^1.0.7", diff --git a/src/api/resume.ts b/src/api/resume.ts index 3ee07db..3dcb151 100644 --- a/src/api/resume.ts +++ b/src/api/resume.ts @@ -7,12 +7,12 @@ export const getResume = async (id: string) => { return resume; }; -export const userToResume = async (resume: string) => { +export const userToResume = async (userId: string, resume: string) => { const pb = pocketbase(); - const user = await pb.collection("users").update(resume, { + const updatedUser = await pb.collection("users").update(userId, { resume: resume, }); - return user; + return updatedUser; }; export const createResume = async (resume: any) => { diff --git a/src/app/(with-nav)/resume/[id]/page.tsx b/src/app/(with-nav)/resume/[id]/page.tsx index 5b1daa9..05426c4 100644 --- a/src/app/(with-nav)/resume/[id]/page.tsx +++ b/src/app/(with-nav)/resume/[id]/page.tsx @@ -3,7 +3,6 @@ import { Card, CardContent, CardHeader } from "@/components/ui/card"; import { Label } from "@/components/ui/label"; import Image from "next/image"; import React from "react"; -import { GoTriangleLeft, GoTriangleRight } from "react-icons/go"; const data = { fullName: "John Doe", diff --git a/src/app/(with-nav)/resume/create/page.tsx b/src/app/(with-nav)/resume/create/page.tsx index 5392f56..ab54372 100644 --- a/src/app/(with-nav)/resume/create/page.tsx +++ b/src/app/(with-nav)/resume/create/page.tsx @@ -18,6 +18,7 @@ import ExperienceForm from "@/components/ExperienceForm"; import { Experience } from "@/types/resume"; import { createExperience, createResume, userToResume } from "@/api/resume"; import { useRouter } from "next/navigation"; +import { getUser } from "@/api/auth"; const ResumeForm = () => { const { @@ -65,7 +66,12 @@ const ResumeForm = () => { formData.append("education", resume.placesOfStudy.join(", ")); formData.append("employment_type", resume.typeOfEmployment); formData.append("about", resume.aboutMyself); - formData.append("img", resume.img.files[0]); + // formData.append("img", resume.img.files[0]); + + if (resume.img) { + formData.append("img", resume.img.files[0]); + } + // fileInput.addEventListener("change", function () { // if (fileInput.files && fileInput.files.length > 0) { // const file = fileInput.files[0]; @@ -86,9 +92,13 @@ const ResumeForm = () => { end_date: exp.endDate, })); const experienceData = await createExperience(experiences); - const userData = await userToResume(resume.id); + const user = await getUser(); + console.log("user: ", user.id); + const userData = await userToResume(user.id, resume.id); console.log("submitted experience data: ", experienceData); console.log("test: ", experiences); + // const from = searchParams.get("from") || "/"; + // router.push(from); // router.back(); } catch (error) { console.log(error); diff --git a/src/lib/formValidationSchemas.ts b/src/lib/formValidationSchemas.ts index 742582c..cbc40fe 100644 --- a/src/lib/formValidationSchemas.ts +++ b/src/lib/formValidationSchemas.ts @@ -82,7 +82,11 @@ export const resumeCreationSchema = z.object({ .optional() .default([]), - education: z.string().nonempty({ message: "Education is required!" }), + education: z.enum(["bachelor", "masters", "high_school", "doctorate","college"], + { + errorMap: () => ({message: "Please select a valud type of employment"}) + } + ), placesOfStudy: z.array(z.string()).optional().default([]), skills: z.array(z.string()).optional().default([]), expectedSalary: z.preprocess((value) => Number(value), z @@ -90,21 +94,22 @@ export const resumeCreationSchema = z.object({ .positive({ message: "Salary must be a positive number" }) ), typeOfEmployment: z.enum( - ["Full-time", "Part-time", "Freelance", "Contract"], + ["full_time", "part_time", "project", "voluntary", "internship"], { errorMap: () => ({ message: "Please select a valid type of employment" }), } ), img: z .any() - .refine((file) => file?.size <= MAX_FILE_SIZE, { - message: `Max file size is${MAX_FILE_SIZE / 1000000}MB`, - }) - .refine( - (file) => ACCEPTED_IMAGE_TYPES.includes(file?.type), - "Only .jpg, .jpeg, .png and .webp formats are supported." - ) - .optional(), + // .refine((file) => file?.size <= MAX_FILE_SIZE, { + // message: `Max file size is${MAX_FILE_SIZE / 1000000}MB`, + // }) + // .refine( + // (file) => ACCEPTED_IMAGE_TYPES.includes(file?.type), + // "Only .jpg, .jpeg, .png and .webp formats are supported." + // ) + // .optional() + , aboutMyself: z .string() .min(10, { message: "About myself must be at least 10 characters" }), From d8adf7a6e178ccdece2f276f812d946d5b4c19c6 Mon Sep 17 00:00:00 2001 From: Anuar Kapbasov Date: Tue, 5 Nov 2024 16:37:45 +0500 Subject: [PATCH 4/7] some comments --- src/api/resume.ts | 7 ++++++ src/app/(with-nav)/resume/create/page.tsx | 27 ++++++++++++++++++----- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/api/resume.ts b/src/api/resume.ts index 3dcb151..41c34c5 100644 --- a/src/api/resume.ts +++ b/src/api/resume.ts @@ -1,4 +1,5 @@ "use server"; +import { permanentRedirect } from "next/navigation"; import { pocketbase } from "./pocketbase"; export const getResume = async (id: string) => { @@ -15,6 +16,12 @@ export const userToResume = async (userId: string, resume: string) => { return updatedUser; }; +export const resumeRedirect = async (resumeId: string) => { + const pb = pocketbase(); + if (resumeId) { + permanentRedirect(`/resume/${resumeId}`); + } +}; export const createResume = async (resume: any) => { const pb = pocketbase(); diff --git a/src/app/(with-nav)/resume/create/page.tsx b/src/app/(with-nav)/resume/create/page.tsx index ab54372..76d9efb 100644 --- a/src/app/(with-nav)/resume/create/page.tsx +++ b/src/app/(with-nav)/resume/create/page.tsx @@ -8,7 +8,7 @@ import { resumeCreationSchema, ResumeCreationSchema, } from "@/lib/formValidationSchemas"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Card, CardContent, CardHeader } from "@/components/ui/card"; @@ -16,8 +16,13 @@ import { Textarea } from "@/components/ui/textarea"; import Image from "next/image"; import ExperienceForm from "@/components/ExperienceForm"; import { Experience } from "@/types/resume"; -import { createExperience, createResume, userToResume } from "@/api/resume"; -import { useRouter } from "next/navigation"; +import { + createExperience, + createResume, + resumeRedirect, + userToResume, +} from "@/api/resume"; +import { redirect, useRouter } from "next/navigation"; import { getUser } from "@/api/auth"; const ResumeForm = () => { @@ -36,10 +41,20 @@ const ResumeForm = () => { const [skills, setSkills] = useState([]); const [skillInput, setSkillInput] = useState(""); const [experience, setExperience] = useState([]); + const [user, setUser] = useState(null); const fileInput = document.getElementById("file-input") as HTMLInputElement; const formData = new FormData(); + // TODO: добавить рабочий редирект для резюме + useEffect(() => { + const fetchUser = async () => { + const user = await getUser(); + // await resumeRedirect(user.resume_id); + }; + fetchUser(); + }, []); + const onSubmit = async (data: ResumeCreationSchema) => { console.log("resume data: ", data); console.log("experience data: ", experience); @@ -68,6 +83,7 @@ const ResumeForm = () => { formData.append("about", resume.aboutMyself); // formData.append("img", resume.img.files[0]); + //TODO: сделать добавление картинки к резюме if (resume.img) { formData.append("img", resume.img.files[0]); } @@ -92,11 +108,12 @@ const ResumeForm = () => { end_date: exp.endDate, })); const experienceData = await createExperience(experiences); - const user = await getUser(); + // const user = await getUser(); console.log("user: ", user.id); const userData = await userToResume(user.id, resume.id); console.log("submitted experience data: ", experienceData); console.log("test: ", experiences); + // Перенос на предыдущую страницу // const from = searchParams.get("from") || "/"; // router.push(from); // router.back(); @@ -201,6 +218,7 @@ const ResumeForm = () => { } }; + // ХУЙНЯ // const handleUploadedFile = (event: React.ChangeEvent) => { // const file = event?.target?.files?.[0]; @@ -212,7 +230,6 @@ const ResumeForm = () => { const triggerFileSelect = () => { document.getElementById("file-input")?.click(); }; - return (

Date: Tue, 5 Nov 2024 23:49:51 +0500 Subject: [PATCH 5/7] add resume page db data --- src/api/api_types.ts | 50 +++++- src/api/resume.ts | 18 +- src/app/(with-nav)/resume/[id]/page.tsx | 201 ++++++++++++++++------ src/app/(with-nav)/resume/create/page.tsx | 31 ++-- 4 files changed, 234 insertions(+), 66 deletions(-) diff --git a/src/api/api_types.ts b/src/api/api_types.ts index bdfa719..4379c62 100644 --- a/src/api/api_types.ts +++ b/src/api/api_types.ts @@ -6,8 +6,11 @@ import type PocketBase from 'pocketbase' import type { RecordService } from 'pocketbase' export enum Collections { + Applications = "applications", + Chats = "chats", Company = "company", Experience = "experience", + Messages = "messages", Response = "response", Resume = "resume", Users = "users", @@ -38,12 +41,30 @@ export type AuthSystemFields = { // Record types for each collection +export enum ApplicationsStatusOptions { + "created" = "created", + "accepted" = "accepted", + "archived" = "archived", +} +export type ApplicationsRecord = { + resume: RecordIdString + status: ApplicationsStatusOptions + vacancy: RecordIdString +} + +export type ChatsRecord = { + is_group?: boolean + title?: string + users?: RecordIdString[] +} + export type CompanyRecord = { description?: HTMLString email?: string field?: string name?: string phone?: string + user?: RecordIdString vacansies?: RecordIdString[] website?: string } @@ -52,10 +73,17 @@ export type ExperienceRecord = { company_name?: string description?: HTMLString end_date?: IsoDateString - field?: RecordIdString + resume?: RecordIdString start_date?: IsoDateString } +export type MessagesRecord = { + attachments?: string[] + chat?: RecordIdString + text: string + user?: RecordIdString +} + export type ResponseRecord = { resume?: RecordIdString vacancy?: RecordIdString @@ -79,6 +107,7 @@ export enum ResumeEmploymentTypeOptions { } export type ResumeRecord = { about?: HTMLString + age?: number city?: string education?: string education_levels?: ResumeEducationLevelsOptions @@ -86,6 +115,7 @@ export type ResumeRecord = { employment_type?: ResumeEmploymentTypeOptions experience?: RecordIdString[] full_name?: string + img?: string phone_number?: string salary?: number skills?: string @@ -97,9 +127,11 @@ export enum UsersRoleOptions { } export type UsersRecord = { avatar?: string + chats?: RecordIdString[] company?: RecordIdString + messages?: RecordIdString[] resume?: RecordIdString - role?: UsersRoleOptions + role: UsersRoleOptions } export enum VacancyExperienceOptions { @@ -129,12 +161,15 @@ export type VacancyRecord = { publication_date?: IsoDateString remote?: boolean skills?: string - title?: string + title: string } // Response types include system fields and match responses from the PocketBase API +export type ApplicationsResponse = Required & BaseSystemFields +export type ChatsResponse = Required & BaseSystemFields export type CompanyResponse = Required & BaseSystemFields export type ExperienceResponse = Required & BaseSystemFields +export type MessagesResponse = Required & BaseSystemFields export type ResponseResponse = Required & BaseSystemFields export type ResumeResponse = Required & BaseSystemFields export type UsersResponse = Required & AuthSystemFields @@ -143,8 +178,11 @@ export type VacancyResponse = Required & BaseS // Types containing all Records and Responses, useful for creating typing helper functions export type CollectionRecords = { + applications: ApplicationsRecord + chats: ChatsRecord company: CompanyRecord experience: ExperienceRecord + messages: MessagesRecord response: ResponseRecord resume: ResumeRecord users: UsersRecord @@ -152,8 +190,11 @@ export type CollectionRecords = { } export type CollectionResponses = { + applications: ApplicationsResponse + chats: ChatsResponse company: CompanyResponse experience: ExperienceResponse + messages: MessagesResponse response: ResponseResponse resume: ResumeResponse users: UsersResponse @@ -164,8 +205,11 @@ export type CollectionResponses = { // https://github.com/pocketbase/js-sdk#specify-typescript-definitions export type TypedPocketBase = PocketBase & { + collection(idOrName: 'applications'): RecordService + collection(idOrName: 'chats'): RecordService collection(idOrName: 'company'): RecordService collection(idOrName: 'experience'): RecordService + collection(idOrName: 'messages'): RecordService collection(idOrName: 'response'): RecordService collection(idOrName: 'resume'): RecordService collection(idOrName: 'users'): RecordService diff --git a/src/api/resume.ts b/src/api/resume.ts index 41c34c5..4264bb8 100644 --- a/src/api/resume.ts +++ b/src/api/resume.ts @@ -4,10 +4,26 @@ import { pocketbase } from "./pocketbase"; export const getResume = async (id: string) => { const pb = pocketbase(); - const resume = await pb.collection("resume").getOne(id); + const resume = await pb.collection("resume").getOne(id, { + expand: "experience", + }); return resume; }; +export const getUserByResume = async (id: string) => { + const pb = pocketbase(); + const user = await pb.collection("users").getFirstListItem(`resume="${id}"`); + return user; +} + +export const getExperience = async (id: string) => { + const pb = pocketbase(); + const experience = await pb.collection("experience").getFullList({ + filter: `resume="${id}"`, + }); + return experience; +} + export const userToResume = async (userId: string, resume: string) => { const pb = pocketbase(); const updatedUser = await pb.collection("users").update(userId, { diff --git a/src/app/(with-nav)/resume/[id]/page.tsx b/src/app/(with-nav)/resume/[id]/page.tsx index 05426c4..0e93340 100644 --- a/src/app/(with-nav)/resume/[id]/page.tsx +++ b/src/app/(with-nav)/resume/[id]/page.tsx @@ -1,36 +1,128 @@ +"use client"; +import { getUser } from "@/api/auth"; +import { getExperience, getResume, getUserByResume } from "@/api/resume"; import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardHeader } from "@/components/ui/card"; import { Label } from "@/components/ui/label"; import Image from "next/image"; -import React from "react"; +import { useParams, useRouter } from "next/navigation"; +import React, { use, useEffect, useState } from "react"; -const data = { - fullName: "John Doe", - age: 25, - workExperience: [ - { - company: "Tech Company", - startDate: "2020-01-15", - endDate: "2022-05-20", - jobDescription: - "Worked as a full-stack developer, building web applications using Remix.", - }, - ], - education: "Bachelor of Computer Science", - placesOfStudy: ["MIT", "Community College"], - skills: ["JavaScript", "React", "Remix"], - expectedSalary: 50000, - typeOfEmployment: "Full-time", - img: "/placeholder-user_2.jpg", - aboutMyself: - "I am a passionate developer with a strong interest in web technologies and software development.", - phone: "1234567890", - email: "example@mail.com", - viewed: 123, - suitable: 123, -}; +// const data = { +// fullName: "John Doe", +// age: 25, +// workExperience: [ +// { +// company: "Tech Company", +// startDate: "2020-01-15", +// endDate: "2022-05-20", +// jobDescription: +// "Worked as a full-stack developer, building web applications using Remix.", +// }, +// ], +// education: "Bachelor of Computer Science", +// placesOfStudy: ["MIT", "Community College"], +// skills: ["JavaScript", "React", "Remix"], +// expectedSalary: 50000, +// typeOfEmployment: "Full-time", +// img: "/placeholder-user_2.jpg", +// aboutMyself: +// "I am a passionate developer with a strong interest in web technologies and software development.", +// phone: "1234567890", +// email: "example@mail.com", +// viewed: 123, +// suitable: 123, +// }; + +interface Resume { + id: string; + fullName: string; + age: number | string; + skills: string; + expectedSalary: number | string; + city: string; + education: string; + placesOfStudy: string; + typeOfEmployment: string; + email: string; + phoneNumber: string; + aboutMyself: string; +} + +// interface Experience { +// workExperience: Array<{ +// id: string; +// company: string; +// jobDescription: string; +// startDate: string; +// endDate: string; +// }>; +// } + +interface Experience { + workExperience: Array<{ + id: string; + company: string; + jobDescription: string; + startDate: string; + endDate: string; + }>; +} + +interface User { + id: string; + email: string; +} const resumePage = () => { + const [data, setData] = useState(); + const [user, setUser] = useState(); + const [workExperience, setWorkExperience] = useState(); + const router = useRouter(); + const id = useParams<{ id: string }>().id; + + useEffect(() => { + const fetchResume = async () => { + const resumeResponse = await getResume(id); + const resume: Resume = { + id: resumeResponse.id, + fullName: resumeResponse.full_name, + age: resumeResponse.age, + skills: resumeResponse.skills, + expectedSalary: resumeResponse.salary, + city: resumeResponse.city, + education: resumeResponse.education_levels, + placesOfStudy: resumeResponse.education, + typeOfEmployment: resumeResponse.employment_type, + email: resumeResponse.email, + phoneNumber: resumeResponse.phone_number, + aboutMyself: resumeResponse.about, + // workExperience: resumeResponse.experience.map((exp: any) => ({ + // company: exp.company, + // jobDescription: exp.job_description, + // startDate: exp.start_date, + // endDate: exp.end_date, + // })), + }; + + const experienceResponse = await getExperience(resume.id); + const experience = experienceResponse.map((exp: any) => ({ + id: exp.id, + company: exp.company_name, + jobDescription: exp.description, + startDate: exp.start_date, + endDate: exp.end_date, + })); + setWorkExperience({ workExperience: experience }); + console.log("workExperience", workExperience); + const user = await getUserByResume(resume.id); + console.log("user", user); + console.log("data", resume); + setUser(user); + setData(resume); + }; + fetchResume(); + }, [id]); return (
@@ -75,7 +167,7 @@ const resumePage = () => {
Profile picture {

{"Name: "}

-

{` ${data.fullName}`}

+

{` ${data?.fullName}`}

{/* {

{"Age: "}

-

{data.age}

+

{data?.age}

{/* */}
@@ -134,9 +226,10 @@ const resumePage = () => { height={400} className="absolute bottom-0 right-5 " /> + {/* TODO: Add stats

{`Suitable vacancies: ${data.suitable} • Viewed: ${data.viewed}`}

-
+
*/}
@@ -160,8 +253,9 @@ const resumePage = () => { - {data.workExperience.length > 0 ? ( - data.workExperience.map((job, index) => ( + {Array.isArray(workExperience?.workExperience) && + workExperience.workExperience.length > 0 ? ( + workExperience?.workExperience.map((job, index) => (
@@ -208,12 +302,14 @@ const resumePage = () => {
- {data.skills.length > 0 ? ( - data.skills.map((skill, index) => ( -
- {skill} -
- )) + {data?.skills && data.skills.split(",").length > 0 ? ( + data?.skills + .split(",") + .map((skill: string, index: number) => ( +
+ {skill} +
+ )) ) : (

No skills added

)} @@ -238,21 +334,24 @@ const resumePage = () => { - {data.education.length > 0 ? ( + {data?.education && data?.education.length > 0 ? ( -

{data.education}

+

{data?.education}

- {data.placesOfStudy.length > 0 ? ( - data.placesOfStudy.map((place, index) => ( -
-

{`${index + 1}. ${place}`}

-
- )) + {data?.placesOfStudy && + data?.placesOfStudy.split(", ").length > 0 ? ( + data?.placesOfStudy + .split(", ") + .map((place, index) => ( +
+

{`${index + 1}. ${place}`}

+
+ )) ) : (

No places added

)} @@ -289,12 +388,12 @@ const resumePage = () => { -

{data.expectedSalary}

+

{data?.expectedSalary}

-

{data.typeOfEmployment}

+

{data?.typeOfEmployment}

-

{data.aboutMyself}

+

{data?.aboutMyself}

@@ -323,9 +422,9 @@ const resumePage = () => { -

{data.phone}

+

{user?.phone}

-

{data.email}

+

{user?.email}

diff --git a/src/app/(with-nav)/resume/create/page.tsx b/src/app/(with-nav)/resume/create/page.tsx index 76d9efb..0c9ce24 100644 --- a/src/app/(with-nav)/resume/create/page.tsx +++ b/src/app/(with-nav)/resume/create/page.tsx @@ -43,18 +43,25 @@ const ResumeForm = () => { const [experience, setExperience] = useState([]); const [user, setUser] = useState(null); - const fileInput = document.getElementById("file-input") as HTMLInputElement; - const formData = new FormData(); + // const fileInput = document.getElementById("file-input") as HTMLInputElement; // TODO: добавить рабочий редирект для резюме - useEffect(() => { - const fetchUser = async () => { - const user = await getUser(); - // await resumeRedirect(user.resume_id); - }; - fetchUser(); - }, []); - + // useEffect(() => { + // const fetchUser = async () => { + // const user = await getUser(); + // await resumeRedirect(user.resume_id, router); + // }; + // fetchUser(); + // }, [router]); + // useEffect(() => { + // const redirectUser = async () => { + // const user = await getUser(); + // if (user.resume_id) { + // router.push(`/resume/${user.resume_id}`); + // } + // }; + // redirectUser(); + // }, [router]); const onSubmit = async (data: ResumeCreationSchema) => { console.log("resume data: ", data); console.log("experience data: ", experience); @@ -73,6 +80,7 @@ const ResumeForm = () => { // img: resume.img, // }); const formattedData = (resume: any) => { + const formData = new FormData(); formData.append("full_name", resume.fullName); formData.append("skills", resume.skills.join(", ")); formData.append("salary", resume.expectedSalary); @@ -85,7 +93,7 @@ const ResumeForm = () => { //TODO: сделать добавление картинки к резюме if (resume.img) { - formData.append("img", resume.img.files[0]); + formData.append("img", resume.img[0]); } // fileInput.addEventListener("change", function () { @@ -230,6 +238,7 @@ const ResumeForm = () => { const triggerFileSelect = () => { document.getElementById("file-input")?.click(); }; + // encType="multipart/form-data" return (
Date: Wed, 6 Nov 2024 00:01:45 +0500 Subject: [PATCH 6/7] sorry sorry sorry sorry sorry --- src/app/(with-nav)/resume/[id]/page.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/(with-nav)/resume/[id]/page.tsx b/src/app/(with-nav)/resume/[id]/page.tsx index 0e93340..2cb5d73 100644 --- a/src/app/(with-nav)/resume/[id]/page.tsx +++ b/src/app/(with-nav)/resume/[id]/page.tsx @@ -421,8 +421,8 @@ const resumePage = () => { - -

{user?.phone}

+ {/* +

{user?.phone}

*/}

{user?.email}

From bf8c2b18f1e3bd9de5976663ee3af7fe26f2ef11 Mon Sep 17 00:00:00 2001 From: Anuar Kapbasov Date: Thu, 7 Nov 2024 00:59:27 +0500 Subject: [PATCH 7/7] add image upload --- next.config.mjs | 2 +- src/api/resume.ts | 14 ++++++++++++++ src/app/(with-nav)/resume/[id]/page.tsx | 14 +++++++++++--- src/app/(with-nav)/resume/create/page.tsx | 21 +++++++++++++-------- src/app/globals.css | 10 ++++++++++ src/lib/formValidationSchemas.ts | 4 ++-- 6 files changed, 51 insertions(+), 14 deletions(-) diff --git a/next.config.mjs b/next.config.mjs index 4678774..a58e239 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,4 +1,4 @@ /** @type {import('next').NextConfig} */ const nextConfig = {}; -export default nextConfig; +export default nextConfig; \ No newline at end of file diff --git a/src/api/resume.ts b/src/api/resume.ts index 4264bb8..a1b52b0 100644 --- a/src/api/resume.ts +++ b/src/api/resume.ts @@ -24,6 +24,20 @@ export const getExperience = async (id: string) => { return experience; } +export const getImgUrl = async (id: string) => { + try { + const pb = pocketbase(); + const record = await pb.collection('resume').getOne(id); + const firstFilename = record.img[0]; + const url = pb.files.getUrl(record, firstFilename); + // const url = pocketbase.records.getFileUrl(record, record.imageField); + return url; + } catch (error) { + console.log(error); + return null; + } +} + export const userToResume = async (userId: string, resume: string) => { const pb = pocketbase(); const updatedUser = await pb.collection("users").update(userId, { diff --git a/src/app/(with-nav)/resume/[id]/page.tsx b/src/app/(with-nav)/resume/[id]/page.tsx index 2cb5d73..777497f 100644 --- a/src/app/(with-nav)/resume/[id]/page.tsx +++ b/src/app/(with-nav)/resume/[id]/page.tsx @@ -1,6 +1,11 @@ "use client"; import { getUser } from "@/api/auth"; -import { getExperience, getResume, getUserByResume } from "@/api/resume"; +import { + getExperience, + getImgUrl, + getResume, + getUserByResume, +} from "@/api/resume"; import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardHeader } from "@/components/ui/card"; import { Label } from "@/components/ui/label"; @@ -45,6 +50,7 @@ interface Resume { placesOfStudy: string; typeOfEmployment: string; email: string; + img: any; phoneNumber: string; aboutMyself: string; } @@ -84,6 +90,7 @@ const resumePage = () => { useEffect(() => { const fetchResume = async () => { const resumeResponse = await getResume(id); + const avatar = await getImgUrl(resumeResponse.id); const resume: Resume = { id: resumeResponse.id, fullName: resumeResponse.full_name, @@ -95,6 +102,7 @@ const resumePage = () => { placesOfStudy: resumeResponse.education, typeOfEmployment: resumeResponse.employment_type, email: resumeResponse.email, + img: avatar, phoneNumber: resumeResponse.phone_number, aboutMyself: resumeResponse.about, // workExperience: resumeResponse.experience.map((exp: any) => ({ @@ -167,7 +175,7 @@ const resumePage = () => {
Profile picture { alt="" width={400} height={400} - className="absolute bottom-0 right-5 " + className="absolute bottom-0 right-5 img-nondragable" /> {/* TODO: Add stats
diff --git a/src/app/(with-nav)/resume/create/page.tsx b/src/app/(with-nav)/resume/create/page.tsx index 0c9ce24..e3d992d 100644 --- a/src/app/(with-nav)/resume/create/page.tsx +++ b/src/app/(with-nav)/resume/create/page.tsx @@ -22,18 +22,20 @@ import { resumeRedirect, userToResume, } from "@/api/resume"; -import { redirect, useRouter } from "next/navigation"; +import { redirect, useRouter, useSearchParams } from "next/navigation"; import { getUser } from "@/api/auth"; const ResumeForm = () => { const { register, handleSubmit, + setValue, formState: { errors }, } = useForm({ resolver: zodResolver(resumeCreationSchema), }); const router = useRouter(); + const searchParams = useSearchParams(); const [imagePreview, setImagePreview] = useState(null); const [options, setOptions] = useState([]); @@ -82,6 +84,7 @@ const ResumeForm = () => { const formattedData = (resume: any) => { const formData = new FormData(); formData.append("full_name", resume.fullName); + formData.append("age", resume.age); formData.append("skills", resume.skills.join(", ")); formData.append("salary", resume.expectedSalary); formData.append("city", resume.city); @@ -93,7 +96,7 @@ const ResumeForm = () => { //TODO: сделать добавление картинки к резюме if (resume.img) { - formData.append("img", resume.img[0]); + formData.append("img", resume.img); } // fileInput.addEventListener("change", function () { @@ -102,7 +105,7 @@ const ResumeForm = () => { // formData.append("img", file); // } // }); - + console.log(formData.get("img")); return formData; }; @@ -116,14 +119,14 @@ const ResumeForm = () => { end_date: exp.endDate, })); const experienceData = await createExperience(experiences); - // const user = await getUser(); - console.log("user: ", user.id); + const user = await getUser(); + // console.log("user: ", user.id); const userData = await userToResume(user.id, resume.id); console.log("submitted experience data: ", experienceData); console.log("test: ", experiences); // Перенос на предыдущую страницу - // const from = searchParams.get("from") || "/"; - // router.push(from); + const from = searchParams.get("from") || "/"; + router.push(from); // router.back(); } catch (error) { console.log(error); @@ -222,6 +225,7 @@ const ResumeForm = () => { setImagePreview(reader.result as string); }; reader.readAsDataURL(file); + setValue("img", file); console.log("file", file); } }; @@ -244,6 +248,7 @@ const ResumeForm = () => {
@@ -377,7 +382,7 @@ const ResumeForm = () => { alt="" width={200} height={200} - className="absolute bottom-0 right-5 " + className="absolute bottom-0 right-5 img-nondragable" priority={true} />
diff --git a/src/app/globals.css b/src/app/globals.css index 1127985..11a8e7d 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -104,4 +104,14 @@ input:-webkit-autofill:active{ body { @apply bg-background text-foreground; } +} + +@layer components { + .img-nondragable { + -webkit-user-drag: none; + -khtml-user-drag: none; + -moz-user-drag: none; + -o-user-drag: none; + user-drag: none; + } } \ No newline at end of file diff --git a/src/lib/formValidationSchemas.ts b/src/lib/formValidationSchemas.ts index cbc40fe..04188ae 100644 --- a/src/lib/formValidationSchemas.ts +++ b/src/lib/formValidationSchemas.ts @@ -82,7 +82,7 @@ export const resumeCreationSchema = z.object({ .optional() .default([]), - education: z.enum(["bachelor", "masters", "high_school", "doctorate","college"], + education: z.enum(["bachelor", "masters", "high school", "doctorate","college"], { errorMap: () => ({message: "Please select a valud type of employment"}) } @@ -100,7 +100,7 @@ export const resumeCreationSchema = z.object({ } ), img: z - .any() + .instanceof(File).optional() // .refine((file) => file?.size <= MAX_FILE_SIZE, { // message: `Max file size is${MAX_FILE_SIZE / 1000000}MB`, // })