Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 46 additions & 3 deletions src/api/api_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -38,12 +41,30 @@ export type AuthSystemFields<T = never> = {

// 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
}
Expand All @@ -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
Expand Down Expand Up @@ -86,6 +114,7 @@ export type ResumeRecord = {
employment_type?: ResumeEmploymentTypeOptions
experience?: RecordIdString[]
full_name?: string
img?: string
phone_number?: string
salary?: number
skills?: string
Expand All @@ -97,9 +126,11 @@ export enum UsersRoleOptions {
}
export type UsersRecord = {
avatar?: string
chats?: RecordIdString[]
company?: RecordIdString
messages?: RecordIdString[]
resume?: RecordIdString
role?: UsersRoleOptions
role: UsersRoleOptions
}

export enum VacancyExperienceOptions {
Expand Down Expand Up @@ -129,12 +160,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<Texpand = unknown> = Required<ApplicationsRecord> & BaseSystemFields<Texpand>
export type ChatsResponse<Texpand = unknown> = Required<ChatsRecord> & BaseSystemFields<Texpand>
export type CompanyResponse<Texpand = unknown> = Required<CompanyRecord> & BaseSystemFields<Texpand>
export type ExperienceResponse<Texpand = unknown> = Required<ExperienceRecord> & BaseSystemFields<Texpand>
export type MessagesResponse<Texpand = unknown> = Required<MessagesRecord> & BaseSystemFields<Texpand>
export type ResponseResponse<Texpand = unknown> = Required<ResponseRecord> & BaseSystemFields<Texpand>
export type ResumeResponse<Texpand = unknown> = Required<ResumeRecord> & BaseSystemFields<Texpand>
export type UsersResponse<Texpand = unknown> = Required<UsersRecord> & AuthSystemFields<Texpand>
Expand All @@ -143,17 +177,23 @@ export type VacancyResponse<Texpand = unknown> = Required<VacancyRecord> & 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
vacancy: VacancyRecord
}

export type CollectionResponses = {
applications: ApplicationsResponse
chats: ChatsResponse
company: CompanyResponse
experience: ExperienceResponse
messages: MessagesResponse
response: ResponseResponse
resume: ResumeResponse
users: UsersResponse
Expand All @@ -164,8 +204,11 @@ export type CollectionResponses = {
// https://github.com/pocketbase/js-sdk#specify-typescript-definitions

export type TypedPocketBase = PocketBase & {
collection(idOrName: 'applications'): RecordService<ApplicationsResponse>
collection(idOrName: 'chats'): RecordService<ChatsResponse>
collection(idOrName: 'company'): RecordService<CompanyResponse>
collection(idOrName: 'experience'): RecordService<ExperienceResponse>
collection(idOrName: 'messages'): RecordService<MessagesResponse>
collection(idOrName: 'response'): RecordService<ResponseResponse>
collection(idOrName: 'resume'): RecordService<ResumeResponse>
collection(idOrName: 'users'): RecordService<UsersResponse>
Expand Down
12 changes: 10 additions & 2 deletions src/api/auth.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use server";
import { pocketbase } from "./pocketbase";
import { UsersRoleOptions } from "./api_types";
import { AuthSystemFields, UsersRoleOptions } from "./api_types";
import { AuthModel, RecordModel } from "pocketbase";
import { cookies } from "next/headers";

Expand Down Expand Up @@ -60,7 +60,15 @@ export const isLoggedIn = async () => {
};

export const getUser = async () => {
return pocketbase().authStore.model as Promise<RecordModel>;
return pocketbase().authStore.model as Promise<AuthSystemFields>;
};

export const getUserById = async (userId: string) => {
const { items, totalItems } = await pocketbase()
.collection("users")
.getList(1, 1, { filter: `id = "${userId}"` });

return totalItems > 0 ? items[0] : null;
};

export const existsUser = async (email: string) => {
Expand Down
13 changes: 13 additions & 0 deletions src/api/resume.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { UserExpandResume } from "@/types/resume";
import { ResumeRecord } from "./api_types";
import { pocketbase } from "./pocketbase";
import { error } from "console";

export const getResume = async (id: string) => {
const pb = pocketbase();
Expand All @@ -19,3 +22,13 @@ export const hasResume = async (userId: string) => {
});
return resumes.totalItems > 0;
};

export const resumeById = async (userId: string) => {
const pb = pocketbase();
const { expand }: UserExpandResume = await pb
.collection("users")
.getFirstListItem(`id = "${userId}" && resume != ""`, {
expand: "resume",
});
return expand.resume;
};
120 changes: 67 additions & 53 deletions src/app/(with-nav)/resume/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,50 @@
import { getUser, getUserById } from "@/api/auth";
import { getResume, hasResume, resumeById } from "@/api/resume";
import BackButton from "@/components/BackButton";
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 { GoTriangleLeft, GoTriangleRight } from "react-icons/go";

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 resumePage = async ({ params }: { params: { id: string } }) => {
const user = await getUserById(params.id);
if (!user) {
return (
<ErrorPage text={`Пользоваетеля с id = ${params.id} не существует`} />
);
}

const resumeCheck = await hasResume(params.id);
if (!resumeCheck) {
return <ErrorPage text={`У пользоваетеля ${user.id} нет резюме`} />;
}

const resume = await resumeById(params.id);

const data = {
fullName: resume.full_name,
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: resume.education_levels,
placesOfStudy: [resume.education],
skills: resume.skills,
expectedSalary: resume.salary,
typeOfEmployment: resume.employment_type,
img: "/placeholder-user_2.jpg",
aboutMyself: resume.about,
phone: resume.phone_number,
email: resume.email,
};

const resumePage = () => {
return (
<div className="mx-auto p-6">
<div className="rounded-xl">
Expand Down Expand Up @@ -112,18 +125,6 @@ const resumePage = () => {
{"Name: "}
</p>
<p className="text-white text-2xl ml-2">{` ${data.fullName}`}</p>
{/* <GoTriangleLeft
color="white"
size={24}
className="mt-1"
/> */}
</div>
<div className="flex">
<p className="text-white text-xl font-semibold">
{"Age: "}
</p>
<p className="text-white text-xl ml-2">{data.age}</p>
{/* <GoTriangleLeft color="white" size={24} /> */}
</div>
</div>
</div>
Expand All @@ -135,9 +136,6 @@ const resumePage = () => {
height={400}
className="absolute bottom-0 right-5 "
/>
<div className="absolute bottom-1 left-6 flex">
<p className="text-md text-gray-300 font-medium text-center">{`Suitable vacancies: ${data.suitable} • Viewed: ${data.viewed}`}</p>
</div>
</div>
</CardHeader>
<div>
Expand Down Expand Up @@ -209,12 +207,14 @@ const resumePage = () => {
</svg>
</h2>
<div className="flex gap-2">
{data.skills.length > 0 ? (
data.skills.map((skill, index) => (
<div key={skill + index}>
<Badge>{skill}</Badge>
</div>
))
{data.skills && data.skills.length > 0 ? (
data.skills.split(",").map((item, index) => {
return (
<div key={index}>
<Badge>{item}</Badge>
</div>
);
})
) : (
<p>No skills added</p>
)}
Expand All @@ -239,7 +239,7 @@ const resumePage = () => {
<path d="M6 12.5V16a6 3 0 0 0 12 0v-3.5" />
</svg>
</h2>
{data.education.length > 0 ? (
{resume.education && resume.education.length > 0 ? (
<Card className="mb-4">
<CardContent className="flex flex-col gap-2 p-4">
<Label className="text-gray-500">Education degree</Label>
Expand All @@ -250,7 +250,7 @@ const resumePage = () => {
<div className="flex gap-2">
{data.placesOfStudy.length > 0 ? (
data.placesOfStudy.map((place, index) => (
<div key={place + index}>
<div key={place || "" + index}>
<p>{`${index + 1}. ${place}`}</p>
</div>
))
Expand Down Expand Up @@ -293,10 +293,14 @@ const resumePage = () => {
<p>{data.expectedSalary}</p>
<Label className="text-gray-500">Type of Employment</Label>
<p>{data.typeOfEmployment}</p>
<div className="mt-4">
<Label className="text-gray-500">About Myself</Label>
<p>{data.aboutMyself}</p>
</div>
{data.aboutMyself && (
<div className="mt-4">
<Label className="text-gray-500">About Myself</Label>
<div
dangerouslySetInnerHTML={{ __html: data.aboutMyself }}
/>
</div>
)}
</CardContent>
</Card>
</section>
Expand Down Expand Up @@ -337,4 +341,14 @@ const resumePage = () => {
);
};

const ErrorPage = ({ text }: { text: string }) => {
return (
<>
<div className="flex items-center justify-center">
<h1 className="text-lg font-bold">{text}</h1> <BackButton />
</div>
</>
);
};

export default resumePage;
21 changes: 17 additions & 4 deletions src/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,32 @@
import { NextResponse } from "next/server";
import { type NextRequest } from "next/server";
import { isLoggedIn } from "./api/auth";
import { getUser, isLoggedIn } from "./api/auth";
import { hasResume } from "./api/resume";

export const config = {
matcher: ["/resume/create", "/resume/create/:path*"],
matcher: ["/resume/:path*"],
};

export async function middleware(request: NextRequest) {
const auth = await isLoggedIn();
const from = request.nextUrl.pathname;

if (!auth) {
const from = request.nextUrl.pathname;
if (from === "/resume/create" && !auth) {
const url = new URL("/auth/sign-in", request.url);
url.searchParams.set("from", from);

return NextResponse.redirect(url);
} else if (!auth) {
NextResponse.next();
}

const user = await getUser();
const resumeCheck = await hasResume(user.id);

if (from === `/resume/${user.id}` && !resumeCheck) {
const url = new URL("/resume/create", request.url);
url.searchParams.set("from", from);

return NextResponse.redirect(url);
}

Expand Down
Loading