From 35f3c109568732e270554699f7467a839c0ec30a Mon Sep 17 00:00:00 2001 From: rocky_balboa <168601600+jsR1der@users.noreply.github.com> Date: Sun, 6 Oct 2024 18:52:12 +0300 Subject: [PATCH] aplication controller, ui to submit application --- backend/src/app.module.ts | 6 +- backend/src/models/application.model.ts | 4 ++ .../applications.controller.spec.ts | 20 +++++++ .../applications/applications.controller.ts | 58 +++++++++++++++++++ .../applications/applications.module.ts | 13 +++++ .../applications/applications.service.spec.ts | 18 ++++++ .../applications/applications.service.ts | 46 +++++++++++++++ .../dto/create-application.dto.ts | 5 ++ .../dto/update-application.dto.ts | 4 ++ .../entities/application.entity.ts | 13 +++++ frontend/src/components/form/Form.tsx | 2 +- frontend/src/components/job/Job.tsx | 7 ++- frontend/src/components/jobs/Jobs.tsx | 2 +- frontend/src/components/upload/Upload.tsx | 10 +--- frontend/src/models/application.model.ts | 4 ++ frontend/src/pages/details/Details.tsx | 52 ++++++++++++++--- frontend/src/router/Router.tsx | 2 +- .../api/application/application.service.ts | 10 ++++ 18 files changed, 254 insertions(+), 22 deletions(-) create mode 100644 backend/src/models/application.model.ts create mode 100644 backend/src/resources/applications/applications.controller.spec.ts create mode 100644 backend/src/resources/applications/applications.controller.ts create mode 100644 backend/src/resources/applications/applications.module.ts create mode 100644 backend/src/resources/applications/applications.service.spec.ts create mode 100644 backend/src/resources/applications/applications.service.ts create mode 100644 backend/src/resources/applications/dto/create-application.dto.ts create mode 100644 backend/src/resources/applications/dto/update-application.dto.ts create mode 100644 backend/src/resources/applications/entities/application.entity.ts create mode 100644 frontend/src/models/application.model.ts create mode 100644 frontend/src/services/api/application/application.service.ts diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index a1d4286..102ba90 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -4,12 +4,13 @@ import { AppService } from './app.service'; import { TypeOrmModule } from '@nestjs/typeorm'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { TestUserEntity } from './resources/users/entities/testUser.entity'; -import { UserEntity } from './resources/users/entities/user.entity'; import { CompaniesModule } from './resources/companies/companies.module'; import { CompanyEntity } from './resources/companies/entities/company.entity'; import { UsersModule } from './resources/users/users.module'; import { JobsModule } from './resources/jobs/jobs.module'; import { JobEntity } from './resources/jobs/entities/job.entity'; +import { ApplicationEntity } from './resources/applications/entities/application.entity'; +import { ApplicationsModule } from './resources/applications/applications.module'; @Module({ imports: [ @@ -24,13 +25,14 @@ import { JobEntity } from './resources/jobs/entities/job.entity'; port: configService.get('DBPORT'), password: configService.get('PGPASSWORD'), username: configService.get('PGUSER'), - entities: [TestUserEntity, CompanyEntity, JobEntity], + entities: [TestUserEntity, CompanyEntity, JobEntity, ApplicationEntity], database: configService.get('PGDATABASE'), synchronize: configService.get('synchronize'), logging: configService.get('logging'), ssl: configService.get('ssl'), }), }), + ApplicationsModule, UsersModule, JobsModule, CompaniesModule, diff --git a/backend/src/models/application.model.ts b/backend/src/models/application.model.ts new file mode 100644 index 0000000..d8f86f9 --- /dev/null +++ b/backend/src/models/application.model.ts @@ -0,0 +1,4 @@ +export interface ApplicationModel { + cv: File; + letter: string; +} diff --git a/backend/src/resources/applications/applications.controller.spec.ts b/backend/src/resources/applications/applications.controller.spec.ts new file mode 100644 index 0000000..11c5a23 --- /dev/null +++ b/backend/src/resources/applications/applications.controller.spec.ts @@ -0,0 +1,20 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ApplicationsController } from './applications.controller'; +import { ApplicationsService } from './applications.service'; + +describe('ApplicationsController', () => { + let controller: ApplicationsController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [ApplicationsController], + providers: [ApplicationsService], + }).compile(); + + controller = module.get(ApplicationsController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/backend/src/resources/applications/applications.controller.ts b/backend/src/resources/applications/applications.controller.ts new file mode 100644 index 0000000..414d06c --- /dev/null +++ b/backend/src/resources/applications/applications.controller.ts @@ -0,0 +1,58 @@ +import { + Body, + Controller, + Delete, + Get, + Ip, + Param, + Patch, + Post, + UploadedFile, + UseInterceptors, +} from '@nestjs/common'; +import { ApplicationsService } from './applications.service'; +import { CreateApplicationDto } from './dto/create-application.dto'; +import { UpdateApplicationDto } from './dto/update-application.dto'; +import { FileInterceptor } from '@nestjs/platform-express'; + +@Controller('applications') +export class ApplicationsController { + constructor(private readonly applicationsService: ApplicationsService) {} + + @Post() + @UseInterceptors(FileInterceptor('cv')) + create( + @Body() createApplicationDto: CreateApplicationDto, + @UploadedFile() cv: Express.Multer.File, + @Ip() ip: string, + ) { + return this.applicationsService.saveApplication({ + ...createApplicationDto, + cv, + ip, + }); + } + + @Get() + findAll() { + return this.applicationsService.findAll(); + } + + @Get('canSubmit') + public canSubmit(@Ip() ip: string) { + return this.applicationsService.allowedToSubmit(ip); + } + + @Patch(':id') + update( + @Param('id') id: string, + @Body() updateApplicationDto: UpdateApplicationDto, + ) { + return this.applicationsService.update(+id, updateApplicationDto); + } + + @Delete(':id') + remove(@Param('id') id: string) { + return this.applicationsService.remove(+id); + } +} diff --git a/backend/src/resources/applications/applications.module.ts b/backend/src/resources/applications/applications.module.ts new file mode 100644 index 0000000..ab5d323 --- /dev/null +++ b/backend/src/resources/applications/applications.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { ApplicationsService } from './applications.service'; +import { ApplicationsController } from './applications.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { ApplicationEntity } from './entities/application.entity'; +import { S3Service } from '../../services/s3.service'; + +@Module({ + imports: [TypeOrmModule.forFeature([ApplicationEntity])], + controllers: [ApplicationsController], + providers: [ApplicationsService, S3Service], +}) +export class ApplicationsModule {} diff --git a/backend/src/resources/applications/applications.service.spec.ts b/backend/src/resources/applications/applications.service.spec.ts new file mode 100644 index 0000000..85d27f4 --- /dev/null +++ b/backend/src/resources/applications/applications.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ApplicationsService } from './applications.service'; + +describe('ApplicationsService', () => { + let service: ApplicationsService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ApplicationsService], + }).compile(); + + service = module.get(ApplicationsService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/backend/src/resources/applications/applications.service.ts b/backend/src/resources/applications/applications.service.ts new file mode 100644 index 0000000..e7da696 --- /dev/null +++ b/backend/src/resources/applications/applications.service.ts @@ -0,0 +1,46 @@ +import { Injectable } from '@nestjs/common'; +import { CreateApplicationDto } from './dto/create-application.dto'; +import { UpdateApplicationDto } from './dto/update-application.dto'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { ApplicationEntity } from './entities/application.entity'; +import { S3Service } from '../../services/s3.service'; + +@Injectable() +export class ApplicationsService { + constructor( + @InjectRepository(ApplicationEntity) + private readonly applicationRepository: Repository, + private readonly s3Service: S3Service, + ) {} + + public async saveApplication( + createApplicationDto: CreateApplicationDto, + ): Promise { + const cvUrl = await this.s3Service.uploadFile(createApplicationDto.cv); + const applicationToSave = this.applicationRepository.create({ + ...createApplicationDto, + cvUrl, + }); + const application = + await this.applicationRepository.save(applicationToSave); + return !!application.id; + } + + findAll() { + return `This action returns all applications`; + } + + public async allowedToSubmit(ip: string): Promise { + const record = await this.applicationRepository.findOneBy({ ip }); + return !Boolean(record); + } + + update(id: number, updateApplicationDto: UpdateApplicationDto) { + return `This action updates a #${id} application`; + } + + remove(id: number) { + return `This action removes a #${id} application`; + } +} diff --git a/backend/src/resources/applications/dto/create-application.dto.ts b/backend/src/resources/applications/dto/create-application.dto.ts new file mode 100644 index 0000000..017087a --- /dev/null +++ b/backend/src/resources/applications/dto/create-application.dto.ts @@ -0,0 +1,5 @@ +export class CreateApplicationDto { + cv: Express.Multer.File; + letter: string; + ip?: string; +} diff --git a/backend/src/resources/applications/dto/update-application.dto.ts b/backend/src/resources/applications/dto/update-application.dto.ts new file mode 100644 index 0000000..b25743b --- /dev/null +++ b/backend/src/resources/applications/dto/update-application.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { CreateApplicationDto } from './create-application.dto'; + +export class UpdateApplicationDto extends PartialType(CreateApplicationDto) {} diff --git a/backend/src/resources/applications/entities/application.entity.ts b/backend/src/resources/applications/entities/application.entity.ts new file mode 100644 index 0000000..56224af --- /dev/null +++ b/backend/src/resources/applications/entities/application.entity.ts @@ -0,0 +1,13 @@ +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; + +@Entity({ name: 'applications' }) +export class ApplicationEntity { + @PrimaryGeneratedColumn() + id: number; + @Column({ length: 255, nullable: false }) + ip: string; + @Column({ length: 255, nullable: false }) + cvUrl: string; + @Column({ type: 'varchar', nullable: false }) + letter: string; +} diff --git a/frontend/src/components/form/Form.tsx b/frontend/src/components/form/Form.tsx index 88bf572..3a27275 100644 --- a/frontend/src/components/form/Form.tsx +++ b/frontend/src/components/form/Form.tsx @@ -105,7 +105,7 @@ function Form() { - + diff --git a/frontend/src/components/job/Job.tsx b/frontend/src/components/job/Job.tsx index 19681ea..3990b64 100644 --- a/frontend/src/components/job/Job.tsx +++ b/frontend/src/components/job/Job.tsx @@ -1,10 +1,15 @@ import './Job.scss'; import {JobModel} from "../../models/jobModel.ts"; +import {useNavigate} from "react-router-dom"; function Job(props: { job: JobModel }) { + const navigate = useNavigate(); + const showDetails = () => { + navigate(`jobs/${props.job.id}`, {state: props.job}) + } return
{/**/} -

{props.job.jobTitle}

+

{props.job.jobTitle}

{props.job.fork}

diff --git a/frontend/src/components/jobs/Jobs.tsx b/frontend/src/components/jobs/Jobs.tsx index f85012c..a6e5a52 100644 --- a/frontend/src/components/jobs/Jobs.tsx +++ b/frontend/src/components/jobs/Jobs.tsx @@ -34,7 +34,7 @@ function Jobs() { } return <> -

Existing Clients

+

Existing Jobs

{data.items.map(job => ())}
diff --git a/frontend/src/components/upload/Upload.tsx b/frontend/src/components/upload/Upload.tsx index 14d90db..679500a 100644 --- a/frontend/src/components/upload/Upload.tsx +++ b/frontend/src/components/upload/Upload.tsx @@ -1,12 +1,8 @@ import './Upload.scss' import {ChangeEvent, MouseEventHandler, useRef, useState} from "react"; -import {FieldError, UseFormRegisterReturn} from "react-hook-form"; -import {handleErrorMessage} from "../../utils/forms.ts"; function Upload(props: { - config: UseFormRegisterReturn, onChange: (e: ChangeEvent) => void, - error: FieldError | undefined }) { const [image, setImage] = useState(null) const inputRef = useRef(null) @@ -20,7 +16,7 @@ function Upload(props: { if (target.files?.length) { setImage(target.files[0]) } else { - throw Error('Fuck you!!!') + throw Error('bad error!!!') } }) input?.click() @@ -32,12 +28,12 @@ function Upload(props: {
inputRef.current = e} - accept={'image/jpg,image/jpeg'} type="file" + accept={'application/pdf'} type="file" hidden={true}/>
{image ? image.name : 'Upload your photo'}
-

{handleErrorMessage(props.error)}

+ {/*

{handleErrorMessage(props.error)}

*/}
} diff --git a/frontend/src/models/application.model.ts b/frontend/src/models/application.model.ts new file mode 100644 index 0000000..9138bb3 --- /dev/null +++ b/frontend/src/models/application.model.ts @@ -0,0 +1,4 @@ +export interface ApplicationModel { + cv: File | null; + letter: string; +} \ No newline at end of file diff --git a/frontend/src/pages/details/Details.tsx b/frontend/src/pages/details/Details.tsx index 4978cb6..12a170d 100644 --- a/frontend/src/pages/details/Details.tsx +++ b/frontend/src/pages/details/Details.tsx @@ -1,15 +1,49 @@ -// import styles from './Details.module.scss'; -import {useParams} from 'react-router-dom'; -import {useAuth0} from "@auth0/auth0-react"; -import {useEffect} from "react"; +import {useLocation, useParams} from 'react-router-dom'; +import Upload from "../../components/upload/Upload.tsx"; +import {ChangeEvent, useEffect, useState} from "react"; +import {TextareaAutosize} from "@mui/material"; +import {ApplicationModel} from "../../models/application.model.ts"; +import {hasSubmitted, submitApplication} from "../../services/api/application/application.service.ts"; // import {useAsyncErrorBoundary} from "../errors/asyncErrorBoundary/UseAsyncErrorBoundary.ts"; export const Details = () => { - const {isAuthenticated} = useAuth0() + const [form, setForm] = useState({cv: null, letter: ''}) + const [canSubmit, setCanSubmit] = useState(true) useEffect(() => { - console.log(isAuthenticated) - }, [isAuthenticated]); - // const catchAsync = useAsyncErrorBoundary(); + hasSubmitted().then(res => setCanSubmit(res.data)).catch(console.log) + }, []) const {id} = useParams(); - return
{id}
+ const {state: job} = useLocation(); + const sendApplication = () => { + if (form.cv && form.letter.length) { + submitApplication(form).then(() => setCanSubmit(false)).catch(console.log) + } + } + + const onUploadChange = (event: ChangeEvent) => { + const target = event.target; + setForm({...form, cv: target.files![0]}) + } + + const onCoverLetterChange = (event: ChangeEvent) => { + setForm({...form, letter: event.target.value}) + } + return
+
{job.companyName}
+
{job.jobTitle}
+
{job.description}
+
Salary: {job.fork}
+
Views: {job.views}
+
Applications {job.applications_sent}
+
create date {job.created_at}
+ + + {canSubmit ? + + : +
You already sent an application
+ } +
}; diff --git a/frontend/src/router/Router.tsx b/frontend/src/router/Router.tsx index 1807d29..d4f1042 100644 --- a/frontend/src/router/Router.tsx +++ b/frontend/src/router/Router.tsx @@ -12,7 +12,7 @@ export const Router = () => { - + wait...
}>}> diff --git a/frontend/src/services/api/application/application.service.ts b/frontend/src/services/api/application/application.service.ts new file mode 100644 index 0000000..ef22ee5 --- /dev/null +++ b/frontend/src/services/api/application/application.service.ts @@ -0,0 +1,10 @@ +import {apiService} from "../api-base.service.ts"; +import {ApplicationModel} from "../../../models/application.model.ts"; + +export const submitApplication = async (application: ApplicationModel) => { + return await apiService.instance.post('applications', application, {headers: {"Content-Type": "multipart/form-data"}}); +} + +export const hasSubmitted = async () => { + return await apiService.instance.get('applications/canSubmit') +}