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:
Need to update notification email
CompanyEmailVerify:
Need to update notification email
Client:
jobs/new:
jobs/editor:
Job Listing Skills exists but isn't implimented in the UI
Tab doesnt do anything
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 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 ]
Mark ghost listings to allow users to be informed and put companies on blast
CompanyConnect:
need to lookup company before making a new one
company/editor:
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:
Add Applied Jobs Table
+1
View File
@@ -129,6 +129,7 @@ CREATE TABLE IF NOT EXISTS `Company` (
`Name` varchar(100) DEFAULT NULL,
`Email` varchar(255) DEFAULT NULL,
`EmailVerified` boolean DEFAULT 0,
`EmailToken` char(36) DEFAULT NULL,
`WebsiteURL` varchar(255) DEFAULT NULL,
`Logo` mediumblob DEFAULT NULL,
`JobsClosedSuccessful` int DEFAULT 0,
+3 -3
View File
@@ -1,8 +1,8 @@
<div class="top-bar">
<div class="top-bar-buttons">
<a #jobsLink class="nav-button" routerLink="/jobs">JOB BOARD</a>
<a #resumesLink class="nav-button" routerLink="/resumes">RESUMES</a>
<a #companiesLink class="nav-button" routerLink="/company">COMPANIES</a>
<a class="nav-button" routerLink="/jobs" routerLinkActive="active">JOB BOARD</a>
<a class="nav-button" routerLink="/resumes" routerLinkActive="active">RESUMES</a>
<a class="nav-button" routerLink="/company" routerLinkActive="active">COMPANIES</a>
</div>
<a class="top-bar-logo" routerLink="">
<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 { ResumesComponent } from './pages/main/resumes/resumes.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 { CompanyJobsComponent } from './pages/main/company/jobs/jobs.component';
import { CompanyComponent } from './pages/main/company/company.component';
export const routes: Routes = [
@@ -26,11 +25,11 @@ export const routes: Routes = [
// Company
{ path: "company", component: CompanyComponent },
{ path: "company/connect", component: CompanyConnectComponent },
{ path: "company/jobs", component: CompanyJobsComponent },
{ path: "company/editor", component: CompanyEditorComponent },
// Legal
{ path: "about", component: AboutComponent },
{ path: "contact", component: ContactComponent },
{ path: "privacy", component: PrivacyComponent }
]
-13
View File
@@ -13,10 +13,6 @@ import { isDevMode } from '@angular/core';
})
export class App {
@ViewChild('companiesLink') companiesLink!: ElementRef<HTMLAnchorElement>;
@ViewChild('jobsLink') jobLink!: ElementRef<HTMLAnchorElement>;
@ViewChild('resumesLink') resumeLink!: ElementRef<HTMLAnchorElement>;
devMode: boolean = false;
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;
}
.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;
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">
<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 class="content-frame">
<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><a [href]="'mailto:' + Comp.email" >{{ Comp.email }}</a></div>
<div><h1>{{ Comp.name }}</h1></div>
<div><a [href]="Comp.websiteURL">{{ Comp.websiteURL }}</a></div>
<a [href]="Comp.websiteURL">
<img [src]="Comp.logo" />
</a>
</div>
<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>
<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>
@@ -6,6 +6,7 @@ import { Title } from '@angular/platform-browser';
import { CommonModule } from '@angular/common';
import { Authentication } from 'app/services/Authentication';
import { Company, Employee } from 'app/models/Company';
import { JobListing } from 'app/models/JobListing';
@Component({
selector: 'main-company',
@@ -17,13 +18,22 @@ export class CompanyComponent {
public ErrorMsg: string = "";
public Employers: Employee[] = [];
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 ) {
this.title.setTitle("Companies | BoredCareers");
http.get<Employee[]>("api/employee/").subscribe({
next: data => {
this.Employers = data;
if (data[0] != null){
if (data[0].company.id !== null){
this.changeSelectedCompany(data[0].company.id);
}
}
},
error: err => {
this.ErrorMsg = err.error;
@@ -36,6 +46,27 @@ export class CompanyComponent {
this.http.get<Company>("api/company?CompanyID=" + companyID).subscribe({
next: 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 => {
this.ErrorMsg = err.error;
@@ -8,12 +8,12 @@ import { Authentication } from 'app/services/Authentication';
import { Company } from 'app/models/Company';
@Component({
selector: 'main-company-connect',
templateUrl: './companyconnect.component.html',
styleUrls: [ './companyconnect.component.css' ],
selector: 'main-company-editor',
templateUrl: './editor.component.html',
styleUrls: [ './editor.component.css' ],
imports: [ FormsModule, CommonModule, RouterModule ]
})
export class CompanyConnectComponent {
export class CompanyEditorComponent {
@ViewChildren('step') formSteps!: QueryList<ElementRef<HTMLDivElement>>;
currentStep: number = 0;
@@ -23,7 +23,10 @@ export class CompanyConnectComponent {
MaxFileMB: number = 3;
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(){
@@ -35,7 +38,7 @@ export class CompanyConnectComponent {
@HostListener('window:keydown', ['$event'])
handleGlobalKeyDown(event: KeyboardEvent){
if (event.key === 'Tab'){
if ( event.key === 'Tab' ){
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">
<h1>POST A NEW JOB</h1>
</div>
<form (ngSubmit)="PostJobListing(newListing)">
<!-- 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>
<form (ngSubmit)="SubmitForm(Listing)">
<!-- Title -->
<div #step class="sub-frame">
<div class="center">
<div class="content-frame">
<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)="nextStep()">Next</button>
</div>
@@ -40,7 +21,7 @@
<div class="content-frame split">
<div class="half-frame">
<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="Part-time">Part-time</option>
<option value="Contract">Contract</option>
@@ -50,7 +31,7 @@
</div>
<div class="half-frame">
<label>Remote Position</label>
<input name="remote" [(ngModel)]="newListing.remote" type="checkbox" />
<input name="remote" [(ngModel)]="Listing.remote" type="checkbox" />
</div>
<button type="button" (click)="prevStep()">Back</button>
<button type="button" (click)="nextStep()">Next</button>
@@ -59,28 +40,28 @@
</div>
<!-- Location -->
<div #step *ngIf="!newListing.remote" class="sub-frame">
<div #step *ngIf="!Listing.remote" class="sub-frame">
<div class="center">
<h2>Job Location</h2>
<div>
<div class="content-frame split" style="border-radius: 10px 10px 0 0;">
<div class="half-frame">
<label>City</label>
<input name="city" [(ngModel)]="newListing.city" type="text" />
<input name="city" [(ngModel)]="Listing.city" type="text" />
</div>
<div class="half-frame">
<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 class="content-frame split" style="border-radius: 0 0 10px 10px;">
<div class="half-frame">
<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 class="half-frame">
<label>Postal Code</label>
<input name="postalCode" [(ngModel)]="newListing.postalCode" type="text" />
<input name="postalCode" [(ngModel)]="Listing.postalCode" type="text" />
</div>
</div>
<button type="button" (click)="prevStep()">Back</button>
@@ -96,11 +77,11 @@
<div class="content-frame split">
<div class="half-frame">
<label>Minimum Salary</label>
<input name="salaryMin" [(ngModel)]="newListing.salaryMin" type="number" />
<input name="salaryMin" [(ngModel)]="Listing.salaryMin" type="number" />
</div>
<div class="half-frame">
<label>Maximum Salary</label>
<input name="salaryMax" [(ngModel)]="newListing.salaryMax" type="number" />
<input name="salaryMax" [(ngModel)]="Listing.salaryMax" type="number" />
</div>
<button type="button" (click)="prevStep()">Back</button>
<button type="button" (click)="nextStep()">Next</button>
@@ -113,7 +94,7 @@
<div class="center">
<div class="content-frame">
<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)="nextStep()">Next</button>
</div>
@@ -15,31 +15,46 @@ import { Company, Employee } from 'app/models/Company';
imports: [ FormsModule, CommonModule, RouterModule ]
})
export class JobEditorComponent {
public ErrorMsg: string = "";
@ViewChildren('step') formSteps!: QueryList<ElementRef<HTMLDivElement>>;
currentStep: number = 0;
public employeeOfList: Employee[] = [];
public selectedCompany: Company = new Company;
public Listing: JobListing = new JobListing();
public newListing: JobListing = new JobListing();
public ErrorMsg: string = "";
public mode: string = "";
public modeID: number = 0;
constructor( private http: HttpClient, private router: Router, private route: ActivatedRoute, private title: Title, public auth: Authentication ) {
this.title.setTitle("Jobs - Editor | BoredCareers");
this.http.get<Employee[]>("api/employee").subscribe({
next: empOf => {
if (empOf.length === 0){
router.navigate(["company/connect"]);
}
this.employeeOfList = empOf;
},
error: err => {
this.ErrorMsg = err.error;
this.route.queryParams.subscribe(params => {
const CompanyID = params['CompanyID'] ? +params['CompanyID'] : null;
const JobID = params['JobID'] ? +params['JobID'] : null;
if (CompanyID !== null && JobID !== null){
this.router.navigate([""]);
}else if (CompanyID !== null ){
this.mode = "new";
this.modeID = CompanyID;
}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(){
this.formSteps.changes.subscribe(() => {
@@ -70,8 +85,8 @@ export class JobEditorComponent {
this.updateUI();
}
PostJobListing(jobListing: JobListing){
jobListing.companyID = this.selectedCompany.id!;
PostNewJob(jobListing: JobListing){
jobListing.companyID = this.modeID;
this.http.post("api/joblisting", jobListing).subscribe({
next: data => {
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-title">
<h1>{{ cur.title }}</h1>
<h2>${{ cur.salaryMax }} - ${{ cur.salaryMin }}</h2>
<h2>${{ cur.salaryMin }} - ${{ cur.salaryMax }}</h2>
</div>
<div class="tile-split">
<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 {
background-color: #3c3c3c;
}
.job-timestamp {
width: 100%;
}
.job-timestamp h1 {
margin: 0;
}
@@ -1,21 +1,27 @@
<div class="job-frame">
<div class="company-details" *ngIf="jobsCompany != null" >
<h1>{{ jobsCompany.name }}</h1>
<h1>{{ jobsCompany.email }}</h1>
<h1>{{ jobsCompany.websiteURL }}</h1>
<h1>{{ jobsCompany.logo }}</h1>
<h1>{{ jobsCompany.phone }}</h1>
<h1>{{ jobsCompany.city }}</h1>
<h1>{{ jobsCompany.stateOrRegion }}</h1>
<h1>{{ jobsCompany.country }}</h1>
<h1>{{ jobsCompany.postalCode }}</h1>
<h1>{{ jobsCompany.description }}</h1>
</div>
<div class="center-item">
<a [href]="jobsCompany.websiteURL">
<img [src]="jobsCompany.logo" />
</a>
</div>
<div class="center-item">
<div class="content-link"><a [href]="'mailto:' + jobsCompany.email" >{{ jobsCompany.email }}</a></div>
<div class="content-name"><h1>{{ jobsCompany.name }}</h1></div>
<div class="content-link"><a [href]="'tel:' + jobsCompany.phone">{{ jobsCompany.phone }}</a></div>
</div>
<div class="center-item">
<h1>{{ jobsCompany.city }}, {{ jobsCompany.stateOrRegion }} {{ jobsCompany.postalCode }}</h1>
</div>
<div class="content-desc">
<h1 *ngFor="let line of jobsCompany.description.split('\n')">{{ line }}</h1>
</div>
</div>
<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" >
<h2>THIS JOB POSTING IS CLOSED</h2>
@@ -35,8 +41,5 @@
<h1>{{ selectedJob.postalCode }}</h1>
<h1>{{ selectedJob.description }}</h1>
<h1>{{ selectedJob.createdTime }}</h1>
<h1>{{ selectedJob.modifiedTime }}</h1>
</div>
</div>
+59 -2
View File
@@ -2,19 +2,25 @@ using Microsoft.AspNetCore.Mvc;
using BoredCareers.Services.DatabaseService;
using BoredCareers.Entities;
using System.Web.Http;
using BoredCareers.Services;
namespace BoredCareers.Controllers {
[ApiController]
[Route("api/company")]
public class CompanyController : MistoxControllerBase {
public CompanyController(DatabaseService db) : base(db) {}
EmailService _emailContext;
public CompanyController(DatabaseService db, EmailService emailContext) : base(db) {
_emailContext = emailContext;
}
[HttpGet]
public async Task<IActionResult> GetCompany(int CompanyID) {
if (isLoggedIn()) {
Company? company = await _databaseService.GetCompany(CompanyID);
if (company != null) {
company.EmailToken = "";
return Ok(company);
}
return NotFound("Company doesn't exist");
@@ -59,6 +65,57 @@ namespace BoredCareers.Controllers {
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 Email { get; set; } = "";
public bool EmailVerified { get; set; } = false;
public string EmailToken { get; set; } = "";
public string WebsiteURL { get; set; } = "";
public string Logo { get; set; } = "";
public int JobsClosedSuccessful { get; set; }
@@ -11,6 +11,17 @@ namespace BoredCareers.Services.TimerService {
_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) {
while (!stoppingToken.IsCancellationRequested) {
try {
@@ -37,7 +48,10 @@ namespace BoredCareers.Services.TimerService {
string[] emails = await _db.GetApplicationResponseEmailFromJobListing(listing.JobListingID);
foreach (string email in emails) {
// 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 _email = reader.GetString("Email");
bool _emailVerified = reader.GetBoolean("EmailVerified");
string _emailtoken = reader.GetString("EmailToken");
string _websiteurl = reader.GetString("WebsiteURL");
string _logo = Encoding.UTF8.GetString((byte[])reader["Logo"]);
int _jobsclosedsuccessful = reader.GetInt32("JobsClosedSuccessful");
@@ -42,6 +43,7 @@ namespace BoredCareers.Services.DatabaseService {
Name = _name,
Email = _email,
EmailVerified = _emailVerified,
EmailToken = _emailtoken,
WebsiteURL = _websiteurl,
Logo = _logo,
JobsAutoClosed = _jobsautoclosed,
@@ -64,13 +66,14 @@ namespace BoredCareers.Services.DatabaseService {
await connection.OpenAsync();
string command = @"
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
(@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
Name = @Name,
Email = @Email,
EmailVerified = @EmailVerified,
EmailToken = @EmailToken,
WebsiteURL = @WebsiteURL,
Logo = @Logo,
JobsClosedSuccessful = @JobsClosedSuccessful,
@@ -90,6 +93,7 @@ namespace BoredCareers.Services.DatabaseService {
cmd.Parameters.AddWithValue("@Name", company.Name);
cmd.Parameters.AddWithValue("@Email", company.Email);
cmd.Parameters.AddWithValue("@EmailVerified", company.EmailVerified);
cmd.Parameters.AddWithValue("@EmailToken", company.EmailToken);
cmd.Parameters.AddWithValue("@WebsiteURL", company.WebsiteURL);
cmd.Parameters.AddWithValue("@Logo", Encoding.UTF8.GetBytes(company.Logo));
cmd.Parameters.AddWithValue("@JobsClosedSuccessful", company.JobsClosedSuccessful);
@@ -73,7 +73,8 @@ namespace BoredCareers.Services.DatabaseService {
string command = @"
SELECT *
FROM JobListing
WHERE CompanyID = @CompanyID;
WHERE IsDeleted = FALSE
AND CompanyID = @CompanyID;
";
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
public static string JobAutoClosedSubject = "Verify Your Email Address";
public static string JobAutoClosedEmail = @"
public static string JobAutoClosedBody = @"
<!DOCTYPE html>
<html lang=""en"">
<head>