Merge in UI updates #19

Merged
derek merged 12 commits from working into main 2025-08-05 21:05:24 -07:00
26 changed files with 495 additions and 288 deletions
+15 -9
View File
@@ -17,16 +17,14 @@ Server:
JobCleanupService: JobCleanupService:
Need to update notification email Need to update notification email
CompanyEmailVerify:
Need to update notification email
Client: Client:
jobs/new: jobs/editor:
Job Listing Skills exists but isn't implimented in the UI Job Listing Skills exists but isn't implimented in the UI
Tab doesnt do anything Tab doesnt do anything
Want to add completed job listing preview at end of carosel Want to add completed job listing preview at end of carosel
Edit employees not implimented yet
Jobs/editor:
Jobs/editor w/ Querystring JobID=# is not implimented yet
Edit employees not implimented yet
Resume: Resume:
Resume builder minimal user input [ Dont allow AI input ] Resume builder minimal user input [ Dont allow AI input ]
@@ -38,9 +36,17 @@ Client:
Allow users to look up jobs and apply [ Boost visibility | Completely manual ] Allow users to look up jobs and apply [ Boost visibility | Completely manual ]
Mark ghost listings to allow users to be informed and put companies on blast Mark ghost listings to allow users to be informed and put companies on blast
CompanyConnect: company/editor:
need to lookup company before making a new one Need to lookup company before making a new one
Tab key does nothing
Format phone number for database
Check DataType's for email and phone.
Setup QueryParam's for Edit and New
Edit employees not implimented yet
Company:
No employees for table yet
database: database:
Add Applied Jobs Table Add Applied Jobs Table
+1
View File
@@ -129,6 +129,7 @@ CREATE TABLE IF NOT EXISTS `Company` (
`Name` varchar(100) DEFAULT NULL, `Name` varchar(100) DEFAULT NULL,
`Email` varchar(255) DEFAULT NULL, `Email` varchar(255) DEFAULT NULL,
`EmailVerified` boolean DEFAULT 0, `EmailVerified` boolean DEFAULT 0,
`EmailToken` char(36) DEFAULT NULL,
`WebsiteURL` varchar(255) DEFAULT NULL, `WebsiteURL` varchar(255) DEFAULT NULL,
`Logo` mediumblob DEFAULT NULL, `Logo` mediumblob DEFAULT NULL,
`JobsClosedSuccessful` int DEFAULT 0, `JobsClosedSuccessful` int DEFAULT 0,
+3 -3
View File
@@ -1,8 +1,8 @@
<div class="top-bar"> <div class="top-bar">
<div class="top-bar-buttons"> <div class="top-bar-buttons">
<a #jobsLink class="nav-button" routerLink="/jobs">JOB BOARD</a> <a class="nav-button" routerLink="/jobs" routerLinkActive="active">JOB BOARD</a>
<a #resumesLink class="nav-button" routerLink="/resumes">RESUMES</a> <a class="nav-button" routerLink="/resumes" routerLinkActive="active">RESUMES</a>
<a #companiesLink class="nav-button" routerLink="/company">COMPANIES</a> <a class="nav-button" routerLink="/company" routerLinkActive="active">COMPANIES</a>
</div> </div>
<a class="top-bar-logo" routerLink=""> <a class="top-bar-logo" routerLink="">
<img class="top-bar-logo" style="margin: 0;" src="img/logo-full.png" /> <img class="top-bar-logo" style="margin: 0;" src="img/logo-full.png" />
+3 -4
View File
@@ -6,9 +6,8 @@ import { PrivacyComponent } from './pages/legal/privacy/privacy.component';
import { JobsComponent } from './pages/main/jobs/jobs.component'; import { JobsComponent } from './pages/main/jobs/jobs.component';
import { ResumesComponent } from './pages/main/resumes/resumes.component'; import { ResumesComponent } from './pages/main/resumes/resumes.component';
import { JobEditorComponent } from './pages/main/jobs/editor/jobeditor.component'; import { JobEditorComponent } from './pages/main/jobs/editor/jobeditor.component';
import { CompanyConnectComponent } from './pages/main/company/connect/companyconnect.component'; import { CompanyEditorComponent } from './pages/main/company/editor/editor.component';
import { JobViewerComponent } from './pages/main/jobs/viewer/jobviewer.component'; import { JobViewerComponent } from './pages/main/jobs/viewer/jobviewer.component';
import { CompanyJobsComponent } from './pages/main/company/jobs/jobs.component';
import { CompanyComponent } from './pages/main/company/company.component'; import { CompanyComponent } from './pages/main/company/company.component';
export const routes: Routes = [ export const routes: Routes = [
@@ -26,11 +25,11 @@ export const routes: Routes = [
// Company // Company
{ path: "company", component: CompanyComponent }, { path: "company", component: CompanyComponent },
{ path: "company/connect", component: CompanyConnectComponent }, { path: "company/editor", component: CompanyEditorComponent },
{ path: "company/jobs", component: CompanyJobsComponent },
// Legal // Legal
{ path: "about", component: AboutComponent }, { path: "about", component: AboutComponent },
{ path: "contact", component: ContactComponent }, { path: "contact", component: ContactComponent },
{ path: "privacy", component: PrivacyComponent } { path: "privacy", component: PrivacyComponent }
] ]
-13
View File
@@ -13,10 +13,6 @@ import { isDevMode } from '@angular/core';
}) })
export class App { export class App {
@ViewChild('companiesLink') companiesLink!: ElementRef<HTMLAnchorElement>;
@ViewChild('jobsLink') jobLink!: ElementRef<HTMLAnchorElement>;
@ViewChild('resumesLink') resumeLink!: ElementRef<HTMLAnchorElement>;
devMode: boolean = false; devMode: boolean = false;
constructor( private http: HttpClient, public auth: Authentication, private router: Router, private route: ActivatedRoute, private location: Location){ constructor( private http: HttpClient, public auth: Authentication, private router: Router, private route: ActivatedRoute, private location: Location){
@@ -48,13 +44,4 @@ export class App {
}); });
} }
ngAfterViewInit(){
let ViewLinks = [ this.companiesLink, this.resumeLink, this.jobLink ];
ViewLinks.forEach(link => {
if (new URL(link.nativeElement.href).pathname === new URL(window.location.href).pathname){
link.nativeElement.classList.add("active");
}
});
}
} }
@@ -39,6 +39,96 @@ button {
justify-content: center; justify-content: center;
} }
.content-edit {
position: absolute;
right: 20px;
}
.center-item img { .center-item img {
width: 300px; width: 300px;
}
.content-name {
width: 300px;
text-align: center;
font-size: 30px;
}
.content-name h1 {
margin: 0;
}
.content-link {
display: flex;
width: 300px;
justify-content: center;
}
.content-link a {
text-decoration: none;
color: var(--Mistox-White);
margin-top: auto;
}
.content-desc {
border: solid 1px red;
border-radius: 5px;
padding: 20px;
margin: 0 100px;
margin-bottom: 50px;
}
.content-desc h1 {
margin: 0;
font-size: 20px;
color: #ddd;
}
.content-button {
display: flex;
justify-content: center;
}
.content-button span {
align-content: center;
}
.split-frame {
display: flex;
width: 100%;
}
.half-frame {
width: 50%;
border-right: solid 1px var(--Mistox-Black);
border-left: solid 1px var(--Mistox-Black);
}
.half-frame h2 {
text-align: center;
}
.job-tile {
display: flex;
background-color: var(--Mistox-Black);
justify-content: end;
align-items: center;
border-radius: 10px;
margin: 0 5px;
margin-bottom: 10px;
}
.center-text {
display: flex;
flex: 1;
justify-content: center;
}
.job-tile h1 {
margin: 0;
}
.job-tile button {
color: white;
border-color: white;
} }
@@ -1,25 +1,50 @@
<div class="top-bar"> <div class="top-bar">
<button *ngFor="let company of Employers" (click)="changeSelectedCompany(company.company.id!)">{{ company.company.name.toUpperCase() }}</button> <button *ngFor="let company of Employers" (click)="changeSelectedCompany(company.company.id!)">{{ company.company.name.toUpperCase() }}</button>
<button routerLink="/company/connect" >CONNECT A COMPANY</button> <button routerLink="/company/editor" >CONNECT A COMPANY</button>
</div> </div>
<div class="content-frame"> <div class="content-frame">
<div *ngIf="Comp != null"> <div *ngIf="Comp != null">
<button class="content-edit" style="color: #fff; border-color: #fff;" routerLink="/company/editor" [queryParams]="{ CompanyID: Comp.id }" >EDIT COMPANY</button>
<div class="center-item"> <div class="center-item">
<div><a [href]="'mailto:' + Comp.email" >{{ Comp.email }}</a></div> <a [href]="Comp.websiteURL">
<div><h1>{{ Comp.name }}</h1></div> <img [src]="Comp.logo" />
<div><a [href]="Comp.websiteURL">{{ Comp.websiteURL }}</a></div> </a>
</div> </div>
<div class="center-item"> <div class="center-item">
<img [src]="Comp.logo" /> <div class="content-link"><a [href]="'mailto:' + Comp.email" >{{ Comp.email }}</a></div>
<div class="content-name"><h1>{{ Comp.name }}</h1></div>
<div class="content-link"><a [href]="'tel:' + Comp.phone">{{ Comp.phone }}</a></div>
</div>
<div class="center-item">
<h1>{{ Comp.city }}, {{ Comp.stateOrRegion }} {{ Comp.postalCode }}</h1>
</div>
<div class="content-desc">
<h1 *ngFor="let line of Desc">{{ line }}</h1>
</div>
<div class="content-button" *ngIf="Comp.emailVerified">
<button style="color: #fff; border-color: #fff;" routerLink="/jobs/editor" [queryParams]="{ CompanyID: Comp.id }" >POST JOB</button>
</div>
<div class="content-button" *ngIf="!Comp.emailVerified">
<button style="color: #fff; border-color: #fff;" routerLink="/" [queryParams]="{ CompanyID: Comp.id }" >VERIFY EMAIL</button>
<span>You must verify your company email before you can post job listings.</span>
</div>
<hr />
<div class="split-frame">
<div class="half-frame">
<h2>Active Job Listings</h2>
<div class="job-tile" *ngFor="let listing of List">
<div class="center-text">
<h1>{{ listing.title }}</h1>
</div>
<button [routerLink]="['/jobs/viewer']" [queryParams]="{ JobID: listing.id }" >VIEW LISTING</button>
<button [routerLink]="['/jobs/editor']" [queryParams]="{ JobID: listing.id }" >EDIT LISTING</button>
<button (click)="RemoveJobListing(listing.id!)">DELETE LISTING</button>
</div>
</div>
<div class="half-frame">
<h2>Employees</h2>
</div>
</div> </div>
<h1>{{ Comp.emailVerified }}</h1>
<h1>{{ Comp.phone }}</h1>
<h1>{{ Comp.postalCode }}</h1>
<h1>{{ Comp.country }}</h1>
<h1>{{ Comp.stateOrRegion }}</h1>
<h1>{{ Comp.city }}</h1>
<h1>{{ Comp.description }}</h1>
<button routerLink="/company/jobs" [queryParams]="{ CompanyID: Comp.id }" >ACTIVE JOB LISTINGS</button>
</div> </div>
</div> </div>
@@ -6,6 +6,7 @@ import { Title } from '@angular/platform-browser';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Authentication } from 'app/services/Authentication'; import { Authentication } from 'app/services/Authentication';
import { Company, Employee } from 'app/models/Company'; import { Company, Employee } from 'app/models/Company';
import { JobListing } from 'app/models/JobListing';
@Component({ @Component({
selector: 'main-company', selector: 'main-company',
@@ -17,13 +18,22 @@ export class CompanyComponent {
public ErrorMsg: string = ""; public ErrorMsg: string = "";
public Employers: Employee[] = []; public Employers: Employee[] = [];
public Comp: Company | null = null; public Comp: Company | null = null;
public Desc: string[] = [];
public List: JobListing[] = [];
constructor( private http: HttpClient, private router: Router, private route: ActivatedRoute, private title: Title, public auth: Authentication ) { constructor( private http: HttpClient, private router: Router, private route: ActivatedRoute, private title: Title, public auth: Authentication ) {
this.title.setTitle("Companies | BoredCareers"); this.title.setTitle("Companies | BoredCareers");
http.get<Employee[]>("api/employee/").subscribe({ http.get<Employee[]>("api/employee/").subscribe({
next: data => { next: data => {
this.Employers = data; this.Employers = data;
if (data[0] != null){
if (data[0].company.id !== null){
this.changeSelectedCompany(data[0].company.id);
}
}
}, },
error: err => { error: err => {
this.ErrorMsg = err.error; this.ErrorMsg = err.error;
@@ -36,6 +46,27 @@ export class CompanyComponent {
this.http.get<Company>("api/company?CompanyID=" + companyID).subscribe({ this.http.get<Company>("api/company?CompanyID=" + companyID).subscribe({
next: data => { next: data => {
this.Comp = data; this.Comp = data;
this.Desc = data.description.split("\n");
},
error: err => {
this.ErrorMsg = err.error;
}
});
this.http.get<JobListing[]>("api/joblisting/company?CompanyID=" + companyID).subscribe({
next: data => {
this.List = data;
},
error: err => {
this.ErrorMsg = err.error;
}
});
}
RemoveJobListing( JobListingID: number ){
this.http.delete("api/joblisting?JobListingID=" + JobListingID).subscribe({
next: data => {
window.location.reload();
}, },
error: err => { error: err => {
this.ErrorMsg = err.error; this.ErrorMsg = err.error;
@@ -8,12 +8,12 @@ import { Authentication } from 'app/services/Authentication';
import { Company } from 'app/models/Company'; import { Company } from 'app/models/Company';
@Component({ @Component({
selector: 'main-company-connect', selector: 'main-company-editor',
templateUrl: './companyconnect.component.html', templateUrl: './editor.component.html',
styleUrls: [ './companyconnect.component.css' ], styleUrls: [ './editor.component.css' ],
imports: [ FormsModule, CommonModule, RouterModule ] imports: [ FormsModule, CommonModule, RouterModule ]
}) })
export class CompanyConnectComponent { export class CompanyEditorComponent {
@ViewChildren('step') formSteps!: QueryList<ElementRef<HTMLDivElement>>; @ViewChildren('step') formSteps!: QueryList<ElementRef<HTMLDivElement>>;
currentStep: number = 0; currentStep: number = 0;
@@ -23,7 +23,10 @@ export class CompanyConnectComponent {
MaxFileMB: number = 3; MaxFileMB: number = 3;
constructor( private http: HttpClient, private router: Router, private route: ActivatedRoute, private title: Title, public auth: Authentication ) { constructor( private http: HttpClient, private router: Router, private route: ActivatedRoute, private title: Title, public auth: Authentication ) {
this.title.setTitle("Company - Connect | BoredCareers"); this.title.setTitle("Company - Editor | BoredCareers");
// Query param CompanyID -> Edit
// Query param null -> New
}; };
ngAfterViewInit(){ ngAfterViewInit(){
@@ -35,7 +38,7 @@ export class CompanyConnectComponent {
@HostListener('window:keydown', ['$event']) @HostListener('window:keydown', ['$event'])
handleGlobalKeyDown(event: KeyboardEvent){ handleGlobalKeyDown(event: KeyboardEvent){
if (event.key === 'Tab'){ if ( event.key === 'Tab' ){
event.preventDefault(); event.preventDefault();
} }
} }
@@ -1,87 +0,0 @@
button {
width: 150px;
border-radius: 5px;
margin: 10px;
text-align: center;
padding: 15px 0;
transition: .5s;
background-color: #0000;
border: 1px solid var(--Mistox-White);
color: var(--Mistox-White);
text-decoration: none;
}
button:hover {
background-color: #00000044;
color: var(--Mistox-Light);
}
.full-width {
display: block;
width: 100%;
column-count: 2;
}
.tile-frame {
display: grid;
grid-template-columns: repeat(4, 1fr);
column-gap: 20px;
padding: 20px;
width: calc(100% - 40px);
}
.tile{
background-color: var(--Mistox-Dark);
color: var(--Mistox-White);
break-inside: avoid;
padding: 20px;
border-radius: 20px;
margin-bottom: 20px;
}
.jobs-frame {
width: 100%;
background-color: #8888;
border-top: 2px solid black;
}
.post-job-frame {
display: flex;
justify-content: center;
padding: 10px 0;
}
.tile-title {
text-align: center;
border-bottom: 1px solid;
}
.tile-title h1 {
font-size: 40px;
margin: 5px 0;
}
.tile-title h2 {
font-size: 14px;
}
.tile-split {
columns: 2;
text-align: center;
padding: 10px 0;
}
.tile-split h1 {
margin: 0;
}
.tile-button {
display: flex;
width: 100%;
justify-content: center;
}
.post-job-frame button {
border-color: var(--Mistox-Black);
color: var(--Mistox-Black);
}
@@ -1,23 +0,0 @@
<div class="post-job-frame">
<button [routerLink]="['/jobs/editor']">POST JOB</button>
</div>
<div *ngIf="auth.isLoggedIn" class="jobs-frame">
<div class="posted-jobs-frame" *ngFor="let cur of MyJobListings">
<div class="tile">
<h1>{{ cur.title }}</h1>
<h1>{{ cur.jobType }}</h1>
<h1>Is Remote: {{ cur.remote }}</h1>
<h1>{{ cur.salaryMin }}</h1>
<h1>{{ cur.salaryMax }}</h1>
<h1>{{ cur.city }}</h1>
<h1>{{ cur.stateOrRegion }}</h1>
<h1>{{ cur.country }}</h1>
<h1>{{ cur.postalCode }}</h1>
<h1>Posted: {{ cur.createdTime }}</h1>
<h1>Modified: {{ cur.modifiedTime }}</h1>
</div>
<button [routerLink]="['/jobs/editor']" [queryParams]="{ JobID: cur.id }" >EDIT</button>
<button (click)="RemoveJobListing(cur.id!)">DELETE</button>
</div>
</div>
@@ -1,52 +0,0 @@
import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { FormsModule } from '@angular/forms';
import { Router, ActivatedRoute, RouterModule } from '@angular/router';
import { Title } from '@angular/platform-browser';
import { CommonModule } from '@angular/common';
import { JobListing } from 'app/models/JobListing';
import { Authentication } from 'app/services/Authentication';
@Component({
selector: 'main-company-jobs',
templateUrl: './jobs.component.html',
styleUrls: [ './jobs.component.css' ],
imports: [ FormsModule, CommonModule, RouterModule ]
})
export class CompanyJobsComponent {
public MyJobListings: JobListing[] = [];
public ErrorMsg: string = "";
constructor( private http: HttpClient, private router: Router, private route: ActivatedRoute, private title: Title, public auth: Authentication ) {
this.title.setTitle("Company - Jobs | BoredCareers");
this.route.queryParams.subscribe(params => {
const companyID = params['CompanyID'];
if (companyID){
http.get<JobListing[]>("api/joblisting/company?CompanyID=" + companyID).subscribe({
next: data => {
this.MyJobListings = data;
},
error: err => {
this.ErrorMsg = err.error;
}
});
}else{
router.navigate(["/company"]);
}
});
};
RemoveJobListing( JobListingID: number ){
this.http.delete("api/joblisting?JobListingID=" + JobListingID).subscribe({
next: data => {
window.location.reload();
},
error: err => {
this.ErrorMsg = err.error;
}
});
}
}
@@ -1,33 +1,14 @@
<div class="title-text"> <div class="title-text">
<h1>POST A NEW JOB</h1> <h1>POST A NEW JOB</h1>
</div> </div>
<form (ngSubmit)="PostJobListing(newListing)"> <form (ngSubmit)="SubmitForm(Listing)">
<!-- Attach To Company -->
<div #step class="sub-frame">
<div class="center">
<div class="content-frame">
<label>For What Company</label>
<select name="company" [(ngModel)]="selectedCompany">
<option *ngFor="let cur of employeeOfList" [ngValue]="cur.company">{{ cur.company.name }}</option>
</select>
<button type="button" (click)="nextStep()">Next</button>
</div>
<div class="footer-frame">
<span>
Choose the company you want the listing to be created under.
</span>
<button [routerLink]="['/company/connect']">CONNECT A NEW COMPANY</button>
</div>
</div>
</div>
<!-- Title --> <!-- Title -->
<div #step class="sub-frame"> <div #step class="sub-frame">
<div class="center"> <div class="center">
<div class="content-frame"> <div class="content-frame">
<label>Job Title</label> <label>Job Title</label>
<input name="title" [(ngModel)]="newListing.title" type="text" /> <input name="title" [(ngModel)]="Listing.title" type="text" />
<button type="button" (click)="prevStep()">Back</button> <button type="button" (click)="prevStep()">Back</button>
<button type="button" (click)="nextStep()">Next</button> <button type="button" (click)="nextStep()">Next</button>
</div> </div>
@@ -40,7 +21,7 @@
<div class="content-frame split"> <div class="content-frame split">
<div class="half-frame"> <div class="half-frame">
<label>Job Type</label> <label>Job Type</label>
<select name="jobType" [(ngModel)]="newListing.jobType"> <select name="jobType" [(ngModel)]="Listing.jobType">
<option value="Full-time">Full-time</option> <option value="Full-time">Full-time</option>
<option value="Part-time">Part-time</option> <option value="Part-time">Part-time</option>
<option value="Contract">Contract</option> <option value="Contract">Contract</option>
@@ -50,7 +31,7 @@
</div> </div>
<div class="half-frame"> <div class="half-frame">
<label>Remote Position</label> <label>Remote Position</label>
<input name="remote" [(ngModel)]="newListing.remote" type="checkbox" /> <input name="remote" [(ngModel)]="Listing.remote" type="checkbox" />
</div> </div>
<button type="button" (click)="prevStep()">Back</button> <button type="button" (click)="prevStep()">Back</button>
<button type="button" (click)="nextStep()">Next</button> <button type="button" (click)="nextStep()">Next</button>
@@ -59,28 +40,28 @@
</div> </div>
<!-- Location --> <!-- Location -->
<div #step *ngIf="!newListing.remote" class="sub-frame"> <div #step *ngIf="!Listing.remote" class="sub-frame">
<div class="center"> <div class="center">
<h2>Job Location</h2> <h2>Job Location</h2>
<div> <div>
<div class="content-frame split" style="border-radius: 10px 10px 0 0;"> <div class="content-frame split" style="border-radius: 10px 10px 0 0;">
<div class="half-frame"> <div class="half-frame">
<label>City</label> <label>City</label>
<input name="city" [(ngModel)]="newListing.city" type="text" /> <input name="city" [(ngModel)]="Listing.city" type="text" />
</div> </div>
<div class="half-frame"> <div class="half-frame">
<label>2 Letter State/Region</label> <label>2 Letter State/Region</label>
<input name="stateOrRegion" maxlength="2" minlength="2" [(ngModel)]="newListing.stateOrRegion" type="text" /> <input name="stateOrRegion" maxlength="2" minlength="2" [(ngModel)]="Listing.stateOrRegion" type="text" />
</div> </div>
</div> </div>
<div class="content-frame split" style="border-radius: 0 0 10px 10px;"> <div class="content-frame split" style="border-radius: 0 0 10px 10px;">
<div class="half-frame"> <div class="half-frame">
<label>2 Letter Country</label> <label>2 Letter Country</label>
<input name="country" maxlength="2" minlength="2" [(ngModel)]="newListing.country" type="text" /> <input name="country" maxlength="2" minlength="2" [(ngModel)]="Listing.country" type="text" />
</div> </div>
<div class="half-frame"> <div class="half-frame">
<label>Postal Code</label> <label>Postal Code</label>
<input name="postalCode" [(ngModel)]="newListing.postalCode" type="text" /> <input name="postalCode" [(ngModel)]="Listing.postalCode" type="text" />
</div> </div>
</div> </div>
<button type="button" (click)="prevStep()">Back</button> <button type="button" (click)="prevStep()">Back</button>
@@ -96,11 +77,11 @@
<div class="content-frame split"> <div class="content-frame split">
<div class="half-frame"> <div class="half-frame">
<label>Minimum Salary</label> <label>Minimum Salary</label>
<input name="salaryMin" [(ngModel)]="newListing.salaryMin" type="number" /> <input name="salaryMin" [(ngModel)]="Listing.salaryMin" type="number" />
</div> </div>
<div class="half-frame"> <div class="half-frame">
<label>Maximum Salary</label> <label>Maximum Salary</label>
<input name="salaryMax" [(ngModel)]="newListing.salaryMax" type="number" /> <input name="salaryMax" [(ngModel)]="Listing.salaryMax" type="number" />
</div> </div>
<button type="button" (click)="prevStep()">Back</button> <button type="button" (click)="prevStep()">Back</button>
<button type="button" (click)="nextStep()">Next</button> <button type="button" (click)="nextStep()">Next</button>
@@ -113,7 +94,7 @@
<div class="center"> <div class="center">
<div class="content-frame"> <div class="content-frame">
<label>Description</label> <label>Description</label>
<textarea name="description" [(ngModel)]="newListing.description" type="text"></textarea> <textarea name="description" [(ngModel)]="Listing.description" type="text"></textarea>
<button type="button" (click)="prevStep()">Back</button> <button type="button" (click)="prevStep()">Back</button>
<button type="button" (click)="nextStep()">Next</button> <button type="button" (click)="nextStep()">Next</button>
</div> </div>
@@ -15,31 +15,46 @@ import { Company, Employee } from 'app/models/Company';
imports: [ FormsModule, CommonModule, RouterModule ] imports: [ FormsModule, CommonModule, RouterModule ]
}) })
export class JobEditorComponent { export class JobEditorComponent {
public ErrorMsg: string = "";
@ViewChildren('step') formSteps!: QueryList<ElementRef<HTMLDivElement>>; @ViewChildren('step') formSteps!: QueryList<ElementRef<HTMLDivElement>>;
currentStep: number = 0; currentStep: number = 0;
public employeeOfList: Employee[] = []; public Listing: JobListing = new JobListing();
public selectedCompany: Company = new Company;
public newListing: JobListing = new JobListing(); public mode: string = "";
public ErrorMsg: string = ""; public modeID: number = 0;
constructor( private http: HttpClient, private router: Router, private route: ActivatedRoute, private title: Title, public auth: Authentication ) { constructor( private http: HttpClient, private router: Router, private route: ActivatedRoute, private title: Title, public auth: Authentication ) {
this.title.setTitle("Jobs - Editor | BoredCareers"); this.title.setTitle("Jobs - Editor | BoredCareers");
this.http.get<Employee[]>("api/employee").subscribe({ this.route.queryParams.subscribe(params => {
next: empOf => { const CompanyID = params['CompanyID'] ? +params['CompanyID'] : null;
if (empOf.length === 0){ const JobID = params['JobID'] ? +params['JobID'] : null;
router.navigate(["company/connect"]); if (CompanyID !== null && JobID !== null){
} this.router.navigate([""]);
this.employeeOfList = empOf; }else if (CompanyID !== null ){
}, this.mode = "new";
error: err => { this.modeID = CompanyID;
this.ErrorMsg = err.error; }else if(JobID !== null){
this.mode = "edit";
this.modeID = JobID;
}else if (CompanyID === null && JobID === null){
this.router.navigate([""]);
}
if (this.mode === "edit") {
this.http.get<JobListing>("api/joblisting/" + JobID).subscribe({
next: data => {
this.Listing = data;
},
error: err => {
this.ErrorMsg = err.error;
}
});
} }
}); });
}; }
ngAfterViewInit(){ ngAfterViewInit(){
this.formSteps.changes.subscribe(() => { this.formSteps.changes.subscribe(() => {
@@ -70,8 +85,8 @@ export class JobEditorComponent {
this.updateUI(); this.updateUI();
} }
PostJobListing(jobListing: JobListing){ PostNewJob(jobListing: JobListing){
jobListing.companyID = this.selectedCompany.id!; jobListing.companyID = this.modeID;
this.http.post("api/joblisting", jobListing).subscribe({ this.http.post("api/joblisting", jobListing).subscribe({
next: data => { next: data => {
this.router.navigate([""]); this.router.navigate([""]);
@@ -82,4 +97,23 @@ export class JobEditorComponent {
}); });
} }
PostEditJob(jobListing: JobListing){
this.http.post("api/joblisting", jobListing).subscribe({
next: data => {
this.router.navigate([""]);
},
error: err => {
this.ErrorMsg = err.error;
}
});
}
SubmitForm(job: JobListing){
if (this.mode === "new"){
this.PostNewJob(job);
}else if (this.mode === "edit"){
this.PostEditJob(job);
}
}
} }
@@ -3,7 +3,7 @@
<div class="tile"> <div class="tile">
<div class="tile-title"> <div class="tile-title">
<h1>{{ cur.title }}</h1> <h1>{{ cur.title }}</h1>
<h2>${{ cur.salaryMax }} - ${{ cur.salaryMin }}</h2> <h2>${{ cur.salaryMin }} - ${{ cur.salaryMax }}</h2>
</div> </div>
<div class="tile-split"> <div class="tile-split">
<h1>{{ cur.jobType }}</h1> <h1>{{ cur.jobType }}</h1>
@@ -1,11 +1,91 @@
.job-frame { .company-details {
background-color: #5c3030;
} }
.job-warning { .company-details::after {
content: "";
display: block;
height: 50px;
clear: both;
}
.content-frame {
background-color: #3c3c3c;
width: calc(100% - 40px);
height: calc(100vh - 400px);
border-radius: 20px;
margin: 10px;
overflow: scroll;
padding: 10px;
color: var(--Mistox-White);
}
.center-item {
display: flex;
width: 100%;
justify-content: center;
}
.content-edit {
position: absolute;
right: 20px;
}
.center-item img {
width: 300px;
}
.content-name {
width: 300px;
text-align: center;
font-size: 30px;
}
.content-name h1 {
margin: 0;
}
.content-link {
display: flex;
width: 300px;
justify-content: center;
}
.content-link a {
text-decoration: none;
color: var(--Mistox-White);
margin-top: auto;
}
.content-desc {
border: solid 1px red;
border-radius: 5px;
padding: 20px;
margin: 0 100px;
}
.content-desc h1 {
margin: 0;
font-size: 20px;
}
.content-button {
display: flex;
justify-content: center;
}
.content-button span {
align-content: center;
} }
.job-details { .job-details {
background-color: #3c3c3c;
}
.job-timestamp {
width: 100%;
}
.job-timestamp h1 {
margin: 0;
} }
@@ -1,21 +1,27 @@
<div class="job-frame"> <div class="job-frame">
<div class="company-details" *ngIf="jobsCompany != null" > <div class="company-details" *ngIf="jobsCompany != null" >
<h1>{{ jobsCompany.name }}</h1> <div class="center-item">
<a [href]="jobsCompany.websiteURL">
<h1>{{ jobsCompany.email }}</h1> <img [src]="jobsCompany.logo" />
<h1>{{ jobsCompany.websiteURL }}</h1> </a>
</div>
<h1>{{ jobsCompany.logo }}</h1> <div class="center-item">
<h1>{{ jobsCompany.phone }}</h1> <div class="content-link"><a [href]="'mailto:' + jobsCompany.email" >{{ jobsCompany.email }}</a></div>
<div class="content-name"><h1>{{ jobsCompany.name }}</h1></div>
<h1>{{ jobsCompany.city }}</h1> <div class="content-link"><a [href]="'tel:' + jobsCompany.phone">{{ jobsCompany.phone }}</a></div>
<h1>{{ jobsCompany.stateOrRegion }}</h1> </div>
<h1>{{ jobsCompany.country }}</h1> <div class="center-item">
<h1>{{ jobsCompany.postalCode }}</h1> <h1>{{ jobsCompany.city }}, {{ jobsCompany.stateOrRegion }} {{ jobsCompany.postalCode }}</h1>
</div>
<h1>{{ jobsCompany.description }}</h1> <div class="content-desc">
</div> <h1 *ngFor="let line of jobsCompany.description.split('\n')">{{ line }}</h1>
</div>
</div>
<div class="job-details" *ngIf="selectedJob != null" > <div class="job-details" *ngIf="selectedJob != null" >
<div class="job-timestamp">
<h1>Opened: {{ selectedJob.createdTime }}</h1>
<h1>Modified: {{ selectedJob.modifiedTime }}</h1>
</div>
<div class="job-warning" *ngIf="selectedJob.isDeleted" > <div class="job-warning" *ngIf="selectedJob.isDeleted" >
<h2>THIS JOB POSTING IS CLOSED</h2> <h2>THIS JOB POSTING IS CLOSED</h2>
@@ -35,8 +41,5 @@
<h1>{{ selectedJob.postalCode }}</h1> <h1>{{ selectedJob.postalCode }}</h1>
<h1>{{ selectedJob.description }}</h1> <h1>{{ selectedJob.description }}</h1>
<h1>{{ selectedJob.createdTime }}</h1>
<h1>{{ selectedJob.modifiedTime }}</h1>
</div> </div>
</div> </div>
+59 -2
View File
@@ -2,19 +2,25 @@ using Microsoft.AspNetCore.Mvc;
using BoredCareers.Services.DatabaseService; using BoredCareers.Services.DatabaseService;
using BoredCareers.Entities; using BoredCareers.Entities;
using System.Web.Http; using System.Web.Http;
using BoredCareers.Services;
namespace BoredCareers.Controllers { namespace BoredCareers.Controllers {
[ApiController] [ApiController]
[Route("api/company")] [Route("api/company")]
public class CompanyController : MistoxControllerBase { public class CompanyController : MistoxControllerBase {
public CompanyController(DatabaseService db) : base(db) {} EmailService _emailContext;
public CompanyController(DatabaseService db, EmailService emailContext) : base(db) {
_emailContext = emailContext;
}
[HttpGet] [HttpGet]
public async Task<IActionResult> GetCompany(int CompanyID) { public async Task<IActionResult> GetCompany(int CompanyID) {
if (isLoggedIn()) { if (isLoggedIn()) {
Company? company = await _databaseService.GetCompany(CompanyID); Company? company = await _databaseService.GetCompany(CompanyID);
if (company != null) { if (company != null) {
company.EmailToken = "";
return Ok(company); return Ok(company);
} }
return NotFound("Company doesn't exist"); return NotFound("Company doesn't exist");
@@ -59,6 +65,57 @@ namespace BoredCareers.Controllers {
return NotFound("Not logged in"); return NotFound("Not logged in");
} }
} [HttpGet("sendverifyemail")]
public async Task<ActionResult<string>> SendVerify([FromQuery] int CompanyID) {
try {
string key = "v" + CompanyID;
// Stop from sending multiple emails quickly
if (_emailContext._SentEmails.ContainsKey(key)) {
DateTime PreviousSentTime = _emailContext._SentEmails.GetValueOrDefault(key);
if (PreviousSentTime.AddMinutes(5) > DateTime.Now) {
return NotFound("Cannot sent another verify email until 5 minutes has elapsed");
} else {
_emailContext._SentEmails.Remove(key);
}
}
Company? test = await _databaseService.GetCompany(CompanyID);
if (test != null) {
test.EmailToken = Guid.NewGuid().ToString();
await _databaseService.SetCompany(test);
string EmailContents = EmailService.CompanyVerifyEmailSubject;
EmailContents = Substitue(EmailContents, "@CompanyName", test.Name);
EmailContents = Substitue(EmailContents, "@ID", CompanyID.ToString());
EmailContents = Substitue(EmailContents, "@VerifyPassword", test.EmailToken);
string result = _emailContext.Send(test.Email, EmailService.CompanyVerifyEmailSubject, EmailContents);
_emailContext._SentEmails.Add(key, DateTime.Now);
return Redirect("/");
}
return NotFound("Account not found");
} catch (Exception) {
return NotFound("An internal server error has occured");
}
}
[HttpGet("verifyemail")]
public async Task<ActionResult<bool>> VerifyEmail([FromQuery] int CompanyID, [FromQuery] string EmailToken) {
try {
Company? test = await _databaseService.GetCompany(CompanyID);
if (test != null) {
if (test.EmailToken == EmailToken) {
test.EmailToken = "";
test.EmailVerified = true;
await _databaseService.SetCompany(test);
return Redirect("/");
}
return BadRequest("The token isn't valid");
}
return BadRequest("Account not found"); ;
} catch {
return BadRequest("An internal server error has occured");
}
}
}
} }
+1
View File
@@ -5,6 +5,7 @@ namespace BoredCareers.Entities {
public string Name { get; set; } = ""; public string Name { get; set; } = "";
public string Email { get; set; } = ""; public string Email { get; set; } = "";
public bool EmailVerified { get; set; } = false; public bool EmailVerified { get; set; } = false;
public string EmailToken { get; set; } = "";
public string WebsiteURL { get; set; } = ""; public string WebsiteURL { get; set; } = "";
public string Logo { get; set; } = ""; public string Logo { get; set; } = "";
public int JobsClosedSuccessful { get; set; } public int JobsClosedSuccessful { get; set; }
@@ -11,6 +11,17 @@ namespace BoredCareers.Services.TimerService {
_em = em; _em = em;
} }
public string Substitue(string message, string subString, string Replacement) {
for (int i = 0; i < (message.Length - subString.Length); i++) {
if (message.Substring(i, subString.Length) == subString) {
string before = message.Substring(0, i);
string after = message.Substring(i + subString.Length);
return before + Replacement + after;
}
}
return message;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken) { protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
while (!stoppingToken.IsCancellationRequested) { while (!stoppingToken.IsCancellationRequested) {
try { try {
@@ -37,7 +48,10 @@ namespace BoredCareers.Services.TimerService {
string[] emails = await _db.GetApplicationResponseEmailFromJobListing(listing.JobListingID); string[] emails = await _db.GetApplicationResponseEmailFromJobListing(listing.JobListingID);
foreach (string email in emails) { foreach (string email in emails) {
// Send Notify Email // Send Notify Email
_em.Send(email, EmailService.JobAutoClosedSubject, EmailService.JobAutoClosedEmail); string emailbody = EmailService.JobAutoClosedBody;
//Substitue(emailbody, "@job", listing.JobListingID);
_em.Send(email, EmailService.JobAutoClosedSubject, emailbody);
} }
} }
@@ -26,6 +26,7 @@ namespace BoredCareers.Services.DatabaseService {
string _name = reader.GetString("Name"); string _name = reader.GetString("Name");
string _email = reader.GetString("Email"); string _email = reader.GetString("Email");
bool _emailVerified = reader.GetBoolean("EmailVerified"); bool _emailVerified = reader.GetBoolean("EmailVerified");
string _emailtoken = reader.GetString("EmailToken");
string _websiteurl = reader.GetString("WebsiteURL"); string _websiteurl = reader.GetString("WebsiteURL");
string _logo = Encoding.UTF8.GetString((byte[])reader["Logo"]); string _logo = Encoding.UTF8.GetString((byte[])reader["Logo"]);
int _jobsclosedsuccessful = reader.GetInt32("JobsClosedSuccessful"); int _jobsclosedsuccessful = reader.GetInt32("JobsClosedSuccessful");
@@ -42,6 +43,7 @@ namespace BoredCareers.Services.DatabaseService {
Name = _name, Name = _name,
Email = _email, Email = _email,
EmailVerified = _emailVerified, EmailVerified = _emailVerified,
EmailToken = _emailtoken,
WebsiteURL = _websiteurl, WebsiteURL = _websiteurl,
Logo = _logo, Logo = _logo,
JobsAutoClosed = _jobsautoclosed, JobsAutoClosed = _jobsautoclosed,
@@ -64,13 +66,14 @@ namespace BoredCareers.Services.DatabaseService {
await connection.OpenAsync(); await connection.OpenAsync();
string command = @" string command = @"
INSERT INTO Company INSERT INTO Company
(ID,Name,Email,EmailVerified,WebsiteURL,Logo,JobsClosedSuccessful,JobsAutoClosed,Phone,PostalCode,Country,StateOrRegion,City,Description) (ID,Name,Email,EmailVerified,EmailToken,WebsiteURL,Logo,JobsClosedSuccessful,JobsAutoClosed,Phone,PostalCode,Country,StateOrRegion,City,Description)
VALUES VALUES
(@ID,@Name,@Email,@EmailVerified,@WebsiteURL,@Logo,@JobsClosedSuccessful,@JobsAutoClosed,@Phone,@PostalCode,@Country,@StateOrRegion,@City,@Description) (@ID,@Name,@Email,@EmailVerified,@EmailToken,@WebsiteURL,@Logo,@JobsClosedSuccessful,@JobsAutoClosed,@Phone,@PostalCode,@Country,@StateOrRegion,@City,@Description)
ON DUPLICATE KEY UPDATE ON DUPLICATE KEY UPDATE
Name = @Name, Name = @Name,
Email = @Email, Email = @Email,
EmailVerified = @EmailVerified, EmailVerified = @EmailVerified,
EmailToken = @EmailToken,
WebsiteURL = @WebsiteURL, WebsiteURL = @WebsiteURL,
Logo = @Logo, Logo = @Logo,
JobsClosedSuccessful = @JobsClosedSuccessful, JobsClosedSuccessful = @JobsClosedSuccessful,
@@ -90,6 +93,7 @@ namespace BoredCareers.Services.DatabaseService {
cmd.Parameters.AddWithValue("@Name", company.Name); cmd.Parameters.AddWithValue("@Name", company.Name);
cmd.Parameters.AddWithValue("@Email", company.Email); cmd.Parameters.AddWithValue("@Email", company.Email);
cmd.Parameters.AddWithValue("@EmailVerified", company.EmailVerified); cmd.Parameters.AddWithValue("@EmailVerified", company.EmailVerified);
cmd.Parameters.AddWithValue("@EmailToken", company.EmailToken);
cmd.Parameters.AddWithValue("@WebsiteURL", company.WebsiteURL); cmd.Parameters.AddWithValue("@WebsiteURL", company.WebsiteURL);
cmd.Parameters.AddWithValue("@Logo", Encoding.UTF8.GetBytes(company.Logo)); cmd.Parameters.AddWithValue("@Logo", Encoding.UTF8.GetBytes(company.Logo));
cmd.Parameters.AddWithValue("@JobsClosedSuccessful", company.JobsClosedSuccessful); cmd.Parameters.AddWithValue("@JobsClosedSuccessful", company.JobsClosedSuccessful);
@@ -73,7 +73,8 @@ namespace BoredCareers.Services.DatabaseService {
string command = @" string command = @"
SELECT * SELECT *
FROM JobListing FROM JobListing
WHERE CompanyID = @CompanyID; WHERE IsDeleted = FALSE
AND CompanyID = @CompanyID;
"; ";
MySqlCommand cmd = new MySqlCommand(command, connection); MySqlCommand cmd = new MySqlCommand(command, connection);
+52
View File
@@ -0,0 +1,52 @@
namespace BoredCareers.Services {
public partial class EmailService {
// @UserName
// @VerifyPassword
// https://mistox.com/api/account/verifyemail?UserName=@UserName&Guid=@VerifyPassword
public static string CompanyVerifyEmailSubject = "Verify Your Email Address";
public static string CompanyVerifyEmailBody = @"
<!DOCTYPE html>
<html lang=""en"">
<head>
<meta charset=""UTF-8"">
<meta name=""viewport"" content=""width=device-width, initial-scale=1.0"">
<title>Verify Your Email</title>
</head>
<body style=""font-family: Arial, sans-serif; background-color: #f4f4f4; margin: 0; padding: 0;"">
<table role=""presentation"" style=""width: 100%; background-color: #f4f4f4; padding: 20px 0;"">
<tr>
<td>
<table role=""presentation"" style=""max-width: 600px; width: 100%; background-color: #ffffff; margin: 0 auto; border-radius: 8px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);"">
<tr>
<td style=""padding: 20px; text-align: center; background-color: #4CAF50; color: #ffffff; border-top-left-radius: 8px; border-top-right-radius: 8px;"">
<h2>Verify Email Request</h2>
</td>
</tr>
<tr>
<td style=""padding: 20px; text-align: left; font-size: 16px; color: #333333;"">
<p>Hi @CompanyName,</p>
<p>Thank you for making an account with us:</p>
<p>In order to start using your account we need to verify your email address by clicking the link below:</p>
<p style=""text-align: center;"">
<a href=""https://boredcareers.com/api/company/verifyemail?CompanyID=@ID&EmailToken=@VerifyPassword"" style=""background-color: #4CAF50; color: #ffffff; text-decoration: none; padding: 15px 25px; font-size: 16px; border-radius: 5px; display: inline-block;"">Verify Email</a>
</p>
<p>If you didn't create an account please ignore this email.</p>
<p>Best regards</p>
</td>
</tr>
<tr>
<td style=""padding: 10px; text-align: center; background-color: #f4f4f4; color: #888888; font-size: 12px; border-bottom-left-radius: 8px; border-bottom-right-radius: 8px;"">
<p>If you have any questions, feel free to <a href=""mailto:webmaster@mistox.com"" style=""color: #4CAF50; text-decoration: none;"">contact support</a>.</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
";
}
}
@@ -6,7 +6,7 @@ namespace BoredCareers.Services {
// https://mistox.com/api/account/verifyemail?UserName=@UserName&Guid=@VerifyPassword // https://mistox.com/api/account/verifyemail?UserName=@UserName&Guid=@VerifyPassword
public static string JobAutoClosedSubject = "Verify Your Email Address"; public static string JobAutoClosedSubject = "Verify Your Email Address";
public static string JobAutoClosedEmail = @" public static string JobAutoClosedBody = @"
<!DOCTYPE html> <!DOCTYPE html>
<html lang=""en""> <html lang=""en"">
<head> <head>