Skip to content

Commit

Permalink
aplication controller, ui to submit application
Browse files Browse the repository at this point in the history
  • Loading branch information
jsR1der committed Oct 6, 2024
1 parent 13be217 commit 35f3c10
Show file tree
Hide file tree
Showing 18 changed files with 254 additions and 22 deletions.
6 changes: 4 additions & 2 deletions backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand All @@ -24,13 +25,14 @@ import { JobEntity } from './resources/jobs/entities/job.entity';
port: configService.get<number>('DBPORT'),
password: configService.get<string>('PGPASSWORD'),
username: configService.get<string>('PGUSER'),
entities: [TestUserEntity, CompanyEntity, JobEntity],
entities: [TestUserEntity, CompanyEntity, JobEntity, ApplicationEntity],
database: configService.get<string>('PGDATABASE'),
synchronize: configService.get<boolean>('synchronize'),
logging: configService.get<boolean>('logging'),
ssl: configService.get<boolean>('ssl'),
}),
}),
ApplicationsModule,
UsersModule,
JobsModule,
CompaniesModule,
Expand Down
4 changes: 4 additions & 0 deletions backend/src/models/application.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface ApplicationModel {
cv: File;
letter: string;
}
20 changes: 20 additions & 0 deletions backend/src/resources/applications/applications.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -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>(ApplicationsController);
});

it('should be defined', () => {
expect(controller).toBeDefined();
});
});
58 changes: 58 additions & 0 deletions backend/src/resources/applications/applications.controller.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
13 changes: 13 additions & 0 deletions backend/src/resources/applications/applications.module.ts
Original file line number Diff line number Diff line change
@@ -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 {}
18 changes: 18 additions & 0 deletions backend/src/resources/applications/applications.service.spec.ts
Original file line number Diff line number Diff line change
@@ -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>(ApplicationsService);
});

it('should be defined', () => {
expect(service).toBeDefined();
});
});
46 changes: 46 additions & 0 deletions backend/src/resources/applications/applications.service.ts
Original file line number Diff line number Diff line change
@@ -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<ApplicationEntity>,
private readonly s3Service: S3Service,
) {}

public async saveApplication(
createApplicationDto: CreateApplicationDto,
): Promise<boolean> {
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<boolean> {
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`;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export class CreateApplicationDto {
cv: Express.Multer.File;
letter: string;
ip?: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/mapped-types';
import { CreateApplicationDto } from './create-application.dto';

export class UpdateApplicationDto extends PartialType(CreateApplicationDto) {}
13 changes: 13 additions & 0 deletions backend/src/resources/applications/entities/application.entity.ts
Original file line number Diff line number Diff line change
@@ -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;
}
2 changes: 1 addition & 1 deletion frontend/src/components/form/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ function Form() {
<Input config={inputConfigs.email} placeholder="Email" error={errors.email}></Input>
<Input config={inputConfigs.phone} placeholder="Phone" error={errors.phone}></Input>
<RadioInput config={inputConfigs.position_id} onChange={radioChange}></RadioInput>
<Upload config={inputConfigs.photo} onChange={fileChange} error={errors.photo}></Upload>
<Upload onChange={fileChange} ></Upload>
<Button colorClass={Color.primary} type="submit" text="Sign Up"></Button>
</form>
</section>
Expand Down
7 changes: 6 additions & 1 deletion frontend/src/components/job/Job.tsx
Original file line number Diff line number Diff line change
@@ -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 <div className="grid grid-cols-1 card-grid w-full">
{/*<Image url={props.user.photo}></Image>*/}
<p className="truncate w-full text-center name">{props.job.jobTitle}</p>
<p style={{color: 'red',cursor: 'pointer'}} className="truncate w-full text-center name" onClick={showDetails}>{props.job.jobTitle}</p>

<div className="flex flex-col w-full gap-[5px]">
<p className="truncate w-full text-center title">{props.job.fork}</p>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/jobs/Jobs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ function Jobs() {
}

return <>
<h1>Existing Clients</h1>
<h1>Existing Jobs</h1>
<div className="card-grid-list w-full">
{data.items.map(job => (<Job key={job.id} job={job}></Job>))}
</div>
Expand Down
10 changes: 3 additions & 7 deletions frontend/src/components/upload/Upload.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLInputElement>) => void,
error: FieldError | undefined
}) {
const [image, setImage] = useState<File | null>(null)
const inputRef = useRef<HTMLInputElement | null>(null)
Expand All @@ -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()
Expand All @@ -32,12 +28,12 @@ function Upload(props: {
<div className="input">
<input onChange={props.onChange}
ref={(e) => inputRef.current = e}
accept={'image/jpg,image/jpeg'} type="file"
accept={'application/pdf'} type="file"
hidden={true}/>
<button onClick={tryUpload}>Upload</button>
<div className="truncate">{image ? image.name : 'Upload your photo'}</div>
</div>
<p className="error-message">{handleErrorMessage(props.error)}</p>
{/*<p className="error-message">{handleErrorMessage(props.error)}</p>*/}
</div>

}
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/models/application.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface ApplicationModel {
cv: File | null;
letter: string;
}
52 changes: 43 additions & 9 deletions frontend/src/pages/details/Details.tsx
Original file line number Diff line number Diff line change
@@ -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<ApplicationModel>({cv: null, letter: ''})
const [canSubmit, setCanSubmit] = useState<boolean>(true)
useEffect(() => {
console.log(isAuthenticated)
}, [isAuthenticated]);
// const catchAsync = useAsyncErrorBoundary();
hasSubmitted().then(res => setCanSubmit(res.data)).catch(console.log)
}, [])
const {id} = useParams();
return <div>{id}</div>
const {state: job} = useLocation();
const sendApplication = () => {
if (form.cv && form.letter.length) {
submitApplication(form).then(() => setCanSubmit(false)).catch(console.log)
}
}

const onUploadChange = (event: ChangeEvent<HTMLInputElement>) => {
const target = event.target;
setForm({...form, cv: target.files![0]})
}

const onCoverLetterChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
setForm({...form, letter: event.target.value})
}
return <div className={"abt-center"}>
<div>{job.companyName}</div>
<div>{job.jobTitle}</div>
<div>{job.description}</div>
<div>Salary: {job.fork}</div>
<div>Views: {job.views}</div>
<div>Applications {job.applications_sent}</div>
<div>create date {job.created_at}</div>
<TextareaAutosize onChange={onCoverLetterChange}></TextareaAutosize>
<Upload onChange={onUploadChange}></Upload>
{canSubmit ?
<button onClick={sendApplication}>Apply to
possition button
</button>
:
<div>You already sent an application</div>
}
</div>
};
2 changes: 1 addition & 1 deletion frontend/src/router/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const Router = () => {
<Routes>
<Route path="/" Component={Layout}>
<Route index Component={JobsContainer}></Route>
<Route path=":id" Component={DetailsWithErrorBoundary}></Route>
<Route path="/jobs/:id" Component={DetailsWithErrorBoundary}></Route>
<Route path="settings"
element={<Suspense fallback={<div>wait...</div>}><Settings></Settings></Suspense>}></Route>
<Route path="*" Component={NotFound}></Route>
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/services/api/application/application.service.ts
Original file line number Diff line number Diff line change
@@ -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<ApplicationModel>('applications', application, {headers: {"Content-Type": "multipart/form-data"}});
}

export const hasSubmitted = async () => {
return await apiService.instance.get<boolean>('applications/canSubmit')
}

0 comments on commit 35f3c10

Please sign in to comment.