working #37
@@ -27,6 +27,7 @@ Client:
|
|||||||
Job Board:
|
Job Board:
|
||||||
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
|
||||||
|
Dont allow users to apply to the same job more than once
|
||||||
|
|
||||||
resume/viewer:
|
resume/viewer:
|
||||||
CSS is broken
|
CSS is broken
|
||||||
@@ -50,5 +51,11 @@ Client:
|
|||||||
Need to impliment Add employee
|
Need to impliment Add employee
|
||||||
Need to impliment Remove employee
|
Need to impliment Remove employee
|
||||||
|
|
||||||
|
AI Resume Rating:
|
||||||
|
Allow companies to determine if the resume looks AI -> add rating
|
||||||
|
|
||||||
|
Translations:
|
||||||
|
Finish adding en strings to the Translations json
|
||||||
|
|
||||||
database:
|
database:
|
||||||
Add Applied Jobs Table
|
Add Applied Jobs Table
|
||||||
@@ -4,5 +4,6 @@ ENV MYSQL_DATABASE=boredcareers
|
|||||||
ENV MYSQL_ROOT_PASSWORD=90pa8pav89h4g08hads
|
ENV MYSQL_ROOT_PASSWORD=90pa8pav89h4g08hads
|
||||||
|
|
||||||
ADD mistox.sql /docker-entrypoint-initdb.d
|
ADD mistox.sql /docker-entrypoint-initdb.d
|
||||||
|
ADD postalcodes.csv /var/lib/mysql-files/postalcodes.csv
|
||||||
|
|
||||||
EXPOSE 3306
|
EXPOSE 3306
|
||||||
|
|||||||
@@ -183,6 +183,29 @@ CREATE TABLE IF NOT EXISTS `JobListingSkill` (
|
|||||||
FOREIGN KEY (`JobListingID`) REFERENCES `JobListing`(`ID`) ON DELETE CASCADE
|
FOREIGN KEY (`JobListingID`) REFERENCES `JobListing`(`ID`) ON DELETE CASCADE
|
||||||
) AUTO_INCREMENT=1;
|
) AUTO_INCREMENT=1;
|
||||||
|
|
||||||
|
-- Postal Codes Section
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `PostalCodes` (
|
||||||
|
`country code` char(2),
|
||||||
|
`postal code` varchar(20),
|
||||||
|
`place name` varchar(180),
|
||||||
|
`state` varchar(100),
|
||||||
|
`state code` varchar(20),
|
||||||
|
`city` varchar(100),
|
||||||
|
`admin code2` varchar(20),
|
||||||
|
`admin name3` varchar(100),
|
||||||
|
`admin code3` varchar(20),
|
||||||
|
`latitude` float,
|
||||||
|
`longitude` float,
|
||||||
|
`accuracy` varchar(2)
|
||||||
|
);
|
||||||
|
|
||||||
|
LOAD DATA INFILE '/var/lib/mysql-files/postalcodes.csv'
|
||||||
|
INTO TABLE PostalCodes
|
||||||
|
FIELDS TERMINATED BY '\t'
|
||||||
|
ENCLOSED BY '"'
|
||||||
|
LINES TERMINATED BY '\n';
|
||||||
|
|
||||||
-- Application Section
|
-- Application Section
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS `JobApplication` (
|
CREATE TABLE IF NOT EXISTS `JobApplication` (
|
||||||
|
|||||||
+1826595
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
|||||||
|
Postal codes is pulled from https://download.geonames.org/export/zip/
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
"assets": [
|
"assets": [
|
||||||
{
|
{
|
||||||
"glob": "**/*",
|
"glob": "**/*",
|
||||||
"input": "public"
|
"input": "src/assets"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Authentication } from './services/Authentication';
|
|||||||
import { CommonModule, Location } from '@angular/common';
|
import { CommonModule, Location } from '@angular/common';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { isDevMode } from '@angular/core';
|
import { isDevMode } from '@angular/core';
|
||||||
|
import { TranslationService } from './services/translations';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
@@ -16,7 +17,8 @@ export class App {
|
|||||||
devMode: boolean = false;
|
devMode: boolean = false;
|
||||||
loginToken: string | null = null;
|
loginToken: string | null = null;
|
||||||
|
|
||||||
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, public strings: TranslationService){
|
||||||
|
strings.setLanguage("en");
|
||||||
this.devMode = isDevMode();
|
this.devMode = isDevMode();
|
||||||
this.route.queryParams.subscribe(params => {
|
this.route.queryParams.subscribe(params => {
|
||||||
this.loginToken = params['LoginToken'];
|
this.loginToken = params['LoginToken'];
|
||||||
|
|||||||
@@ -7,9 +7,9 @@
|
|||||||
<h1>{{ application.rating }}</h1>
|
<h1>{{ application.rating }}</h1>
|
||||||
<h1>{{ application.responseStatus }}</h1>
|
<h1>{{ application.responseStatus }}</h1>
|
||||||
|
|
||||||
<h1>Date Applied: </h1><h1>{{ application.dateApplied }}</h1>
|
<h1>{{ strings.translate("application/viewer", "s Date Applied") }}</h1><h1>{{ application.dateApplied }}</h1>
|
||||||
|
|
||||||
<button type="button" (click)="viewResume(application)" >VIEW RESUME</button>
|
<button type="button" (click)="viewResume(application)" >{{ strings.translate("application/viewer", "b View Resume") }}</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 { Application } from 'app/models/Application';
|
import { Application } from 'app/models/Application';
|
||||||
|
import { TranslationService } from 'app/services/translations';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'App-Viewer',
|
selector: 'App-Viewer',
|
||||||
@@ -18,7 +19,7 @@ export class AppViewerComponent {
|
|||||||
|
|
||||||
public List: Application[] = [];
|
public List: Application[] = [];
|
||||||
|
|
||||||
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, public strings: TranslationService ) {
|
||||||
this.title.setTitle("Applications | BoredCareers");
|
this.title.setTitle("Applications | BoredCareers");
|
||||||
if (!auth.isLoggedIn){
|
if (!auth.isLoggedIn){
|
||||||
router.navigate(["/"]);
|
router.navigate(["/"]);
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
<div class="center-text">
|
<div class="center-text">
|
||||||
<h1>{{ listing.title }}</h1>
|
<h1>{{ listing.title }}</h1>
|
||||||
</div>
|
</div>
|
||||||
<button [routerLink]="['/application/viewer']" [queryParams]="{ JobID: listing.id }" >VIEW LISTING</button>
|
<button [routerLink]="['/application/viewer']" [queryParams]="{ JobID: listing.id }" >VIEW Applicants</button>
|
||||||
<button [routerLink]="['/jobs/viewer']" [queryParams]="{ JobID: listing.id }" >VIEW LISTING</button>
|
<button [routerLink]="['/jobs/viewer']" [queryParams]="{ JobID: listing.id }" >VIEW LISTING</button>
|
||||||
<button [routerLink]="['/jobs/editor']" [queryParams]="{ JobID: listing.id }" >EDIT LISTING</button>
|
<button [routerLink]="['/jobs/editor']" [queryParams]="{ JobID: listing.id }" >EDIT LISTING</button>
|
||||||
<button (click)="RemoveJobListing(listing.id!)">DELETE LISTING</button>
|
<button (click)="RemoveJobListing(listing.id!)">DELETE LISTING</button>
|
||||||
|
|||||||
@@ -108,4 +108,14 @@ form {
|
|||||||
background-color: var(--Mistox-Dark)\);
|
background-color: var(--Mistox-Dark)\);
|
||||||
height: 40px;
|
height: 40px;
|
||||||
break-inside: avoid;
|
break-inside: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-window {
|
||||||
|
position: absolute;
|
||||||
|
left: calc(50% - 400px);
|
||||||
|
width: 800px;
|
||||||
|
text-align: center;
|
||||||
|
top: 600px;
|
||||||
|
background-color: red;
|
||||||
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
<div class="center">
|
<div class="center">
|
||||||
<div class="content-frame">
|
<div class="content-frame">
|
||||||
<label>Company Website URL</label>
|
<label>Company Website URL</label>
|
||||||
<input class="input-field" name="url" [(ngModel)]="newListing.websiteURL" type="text" placeholder="https://mistox.com/" />
|
<input class="input-field" name="url" [(ngModel)]="newListing.websiteURL" type="text" (blur)="validateURL(newListing.websiteURL)" placeholder="https://mistox.com/" />
|
||||||
<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>
|
||||||
@@ -167,4 +167,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
@if (ErrorMsg != ""){
|
||||||
|
<div class="error-window">
|
||||||
|
<h1>{{ ErrorMsg }}</h1>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
@@ -62,9 +62,12 @@ export class CompanyEditorComponent {
|
|||||||
|
|
||||||
validateEmail(input: string){
|
validateEmail(input: string){
|
||||||
let result = this.validator.validateEmail(input);
|
let result = this.validator.validateEmail(input);
|
||||||
if (result[0]){
|
this.newListing.email = result[1];
|
||||||
this.newListing.email = result[1];
|
}
|
||||||
}
|
|
||||||
|
validateURL(input: string){
|
||||||
|
let result = this.validator.validateURL(input);
|
||||||
|
this.newListing.websiteURL = result[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
@HostListener('window:keydown', ['$event'])
|
@HostListener('window:keydown', ['$event'])
|
||||||
@@ -146,6 +149,13 @@ export class CompanyEditorComponent {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var urlIsValid = this.validator.validateURL(company.websiteURL);
|
||||||
|
if (urlIsValid[0] == false){
|
||||||
|
this.ErrorMsg = urlIsValid[1];
|
||||||
|
this.focusFrame(1, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.isNullOrEmpty(company.logo)){
|
if (this.isNullOrEmpty(company.logo)){
|
||||||
this.ErrorMsg = "Logo is blank";
|
this.ErrorMsg = "Logo is blank";
|
||||||
this.focusFrame(2, 0);
|
this.focusFrame(2, 0);
|
||||||
@@ -158,8 +168,9 @@ export class CompanyEditorComponent {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.validator.validateEmail(company.email)[0]){
|
var emailIsValid = this.validator.validateEmail(company.email);
|
||||||
this.ErrorMsg = "Email is invalid";
|
if (emailIsValid[0] == false){
|
||||||
|
this.ErrorMsg = emailIsValid[1];
|
||||||
this.focusFrame(3, 0);
|
this.focusFrame(3, 0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -170,7 +181,7 @@ export class CompanyEditorComponent {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.validator.validatePhoneNumber(company.phone)[0]){
|
if (this.validator.validatePhoneNumber(company.phone)[0] == false){
|
||||||
this.ErrorMsg = "Phone number is invalid";
|
this.ErrorMsg = "Phone number is invalid";
|
||||||
this.focusFrame(3, 1);
|
this.focusFrame(3, 1);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<div class="center-frame center-text border">
|
<div class="center-frame center-text border">
|
||||||
<h1>Bored Careers</h1>
|
<h1>Bored Careers</h1>
|
||||||
<h2>The Anti-AI Job Board</h2>
|
<h2>{{ strings.translate("home", "s subTitle") }}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="content-frame border">
|
<div class="content-frame border">
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { FormsModule } from '@angular/forms';
|
|||||||
import { Router, ActivatedRoute } from '@angular/router';
|
import { Router, ActivatedRoute } from '@angular/router';
|
||||||
import { Title } from '@angular/platform-browser';
|
import { Title } from '@angular/platform-browser';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { TranslationService } from 'app/services/translations';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'main-home',
|
selector: 'main-home',
|
||||||
@@ -13,8 +15,12 @@ import { CommonModule } from '@angular/common';
|
|||||||
})
|
})
|
||||||
export class HomeComponent {
|
export class HomeComponent {
|
||||||
|
|
||||||
constructor( private http: HttpClient, private router: Router, private route: ActivatedRoute, private title: Title ) {
|
Loaded$: Observable<boolean>;
|
||||||
|
|
||||||
|
constructor( private http: HttpClient, private router: Router, private route: ActivatedRoute, private title: Title, public strings: TranslationService ) {
|
||||||
this.title.setTitle("Home | BoredCareers");
|
this.title.setTitle("Home | BoredCareers");
|
||||||
|
this.Loaded$ = this.strings.changed$;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -58,10 +58,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.content-desc {
|
.content-desc {
|
||||||
border: solid 1px red;
|
|
||||||
border-radius: 5px;
|
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
border-radius: 20px;
|
||||||
margin: 0 100px;
|
margin: 0 100px;
|
||||||
|
background-color: var(--mistox-bg-medium);
|
||||||
|
border: 1px solid var(--mistox-border);
|
||||||
|
box-shadow: var(--mistox-shadow);
|
||||||
|
color: var(--mistox-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-desc h1 {
|
.content-desc h1 {
|
||||||
@@ -83,9 +86,75 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.job-timestamp {
|
.job-timestamp {
|
||||||
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
justify-content: end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.job-timestamp h1 {
|
.job-warning {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 40px;
|
||||||
|
width: 100%;
|
||||||
|
background-color: #f00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.split {
|
||||||
|
column-count: 2;
|
||||||
|
margin: 0 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nobreak {
|
||||||
|
break-inside: avoid;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
background-color: var(--mistox-bg-medium);
|
||||||
|
border: 1px solid var(--mistox-border);
|
||||||
|
box-shadow: var(--mistox-shadow);
|
||||||
|
color: var(--mistox-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-block {
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
background-color: var(--mistox-bg-light);
|
||||||
|
border: 1px solid var(--mistox-border);
|
||||||
|
box-shadow: var(--mistox-shadow);
|
||||||
|
color: var(--mistox-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-block h1 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-block h2 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-box {
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
background-color: var(--mistox-bg-light);
|
||||||
|
border: 1px solid var(--mistox-border);
|
||||||
|
box-shadow: var(--mistox-shadow);
|
||||||
|
color: var(--mistox-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-box h1 {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-combo {
|
||||||
|
column-count: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-bar {
|
||||||
|
display: grid;
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -23,46 +23,72 @@
|
|||||||
}
|
}
|
||||||
@if (selectedJob != null) {
|
@if (selectedJob != null) {
|
||||||
<div class="job-details">
|
<div class="job-details">
|
||||||
<div class="job-timestamp">
|
|
||||||
<h1>Opened: {{ selectedJob.createdTime }}</h1>
|
|
||||||
<h1>Modified: {{ selectedJob.modifiedTime }}</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (selectedJob.isDeleted){
|
@if (selectedJob.isDeleted){
|
||||||
<div class="job-warning">
|
<div class="job-warning">
|
||||||
<h2>THIS JOB POSTING IS CLOSED</h2>
|
<h2>THIS JOB POSTING IS CLOSED</h2>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
<div class="center-item">
|
||||||
|
<h1>{{ selectedJob.title }}</h1>
|
||||||
|
</div>
|
||||||
|
<div class="split">
|
||||||
|
<div class="nobreak">
|
||||||
|
<h1>Job Details:</h1>
|
||||||
|
<div class="detail-block">
|
||||||
|
<h1>Job Type</h1>
|
||||||
|
<h2>{{ selectedJob.jobType }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-block">
|
||||||
|
<h1>In Office:</h1>
|
||||||
|
@if (selectedJob.remote){
|
||||||
|
<h2>Remote</h2>
|
||||||
|
} @else {
|
||||||
|
<h2>On-Site</h2>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
<h1>{{ selectedJob.title }}</h1>
|
<div class="detail-block">
|
||||||
|
<h1>Pay Range:</h1>
|
||||||
<h1>{{ selectedJob.jobType }}</h1>
|
<h2>{{ selectedJob.salaryMin }} - {{ selectedJob.salaryMax }}</h2>
|
||||||
<h1>{{ selectedJob.remote }}</h1>
|
</div>
|
||||||
|
|
||||||
<h1>{{ selectedJob.salaryMin }}</h1>
|
@if (!selectedJob.remote){
|
||||||
<h1>{{ selectedJob.salaryMax }}</h1>
|
<div class="detail-block">
|
||||||
|
<h1>Location:</h1>
|
||||||
<h1>{{ selectedJob.city }}</h1>
|
<h2>{{ selectedJob.city }}, {{ selectedJob.stateOrRegion }} {{ selectedJob.country }} {{ selectedJob.postalCode }}</h2>
|
||||||
<h1>{{ selectedJob.stateOrRegion }}</h1>
|
|
||||||
<h1>{{ selectedJob.country }}</h1>
|
|
||||||
<h1>{{ selectedJob.postalCode }}</h1>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h1>Required Skills</h1>
|
|
||||||
@for(skill of selectedJob.skills; track skill.trackUUID){
|
|
||||||
<div>
|
|
||||||
<h1>{{ skill.name }}</h1>
|
|
||||||
<h1>{{ skill.description }}</h1>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<div class="detail-block">
|
||||||
|
<h1>Required Skills:</h1>
|
||||||
|
@for(skill of selectedJob.skills; track skill.trackUUID){
|
||||||
|
<div class="skill-combo">
|
||||||
|
<div class="nobreak">
|
||||||
|
<h1>{{ skill.name }}</h1>
|
||||||
|
</div>
|
||||||
|
<div class="nobreak">
|
||||||
|
<h2>{{ skill.description }}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="job-timestamp">
|
||||||
|
<span>Opened: {{ selectedJob.createdTime }} | Last Updated: {{ selectedJob.modifiedTime }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="nobreak">
|
||||||
|
<h1>Job Description: </h1>
|
||||||
|
<div class="description-box">
|
||||||
|
<h1>{{ selectedJob.description }}</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1>{{ selectedJob.description }}</h1>
|
<div class="bottom-bar">
|
||||||
<div>
|
|
||||||
@for(resume of myResumes; track resume.trackUUID){
|
@for(resume of myResumes; track resume.trackUUID){
|
||||||
<div>
|
<button type="button" (click)="applyWithResume(resume)">APPLY USING RESUME: {{ resume.name }}</button>
|
||||||
<button type="button" (click)="applyWithResume(resume)">APPLY USING RESUME: {{ resume.name }}</button>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<h1>Public: </h1><input [name]="'active' + resume.trackUUID" [(ngModel)]="resume.isActive" type="checkbox" />
|
<h1>Public: </h1><input [name]="'active' + resume.trackUUID" [(ngModel)]="resume.isActive" type="checkbox" />
|
||||||
<h1>Is Veteran: </h1><input [name]="'veteran' + resume.military?.trackUUID" type="checkbox" [checked]="resume.military !== null" (change)="onVeteranChange($event)" />
|
<h1>Is Veteran: </h1><input [name]="'veteran' + resume.military?.trackUUID" type="checkbox" [checked]="resume.military !== null" (change)="onVeteranChange($event)" />
|
||||||
<button type="button" (click)="PrintResume()">Print</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="paper">
|
<div class="paper">
|
||||||
@@ -140,7 +139,7 @@
|
|||||||
<div class="resume-sub-section flex-two-row">
|
<div class="resume-sub-section flex-two-row">
|
||||||
<div>
|
<div>
|
||||||
<input [name]="'certname' + cert.trackUUID" [(ngModel)]="cert.name" type="text" placeholder="Certification Name" />
|
<input [name]="'certname' + cert.trackUUID" [(ngModel)]="cert.name" type="text" placeholder="Certification Name" />
|
||||||
<input [name]="'certverificationURL' + cert.trackUUID" [(ngModel)]="cert.verificationURL" type="text" placeholder="Verification URL" />
|
<input [name]="'certverificationURL' + cert.trackUUID" [(ngModel)]="cert.verificationURL" type="text" (blur)="validateCertURL(cert)" placeholder="Verification URL" />
|
||||||
</div>
|
</div>
|
||||||
<textarea [name]="'certdescription' + cert.trackUUID" [(ngModel)]="cert.description" placeholder="Description"></textarea>
|
<textarea [name]="'certdescription' + cert.trackUUID" [(ngModel)]="cert.description" placeholder="Description"></textarea>
|
||||||
</div>
|
</div>
|
||||||
@@ -156,7 +155,7 @@
|
|||||||
<div class="resume-sub-section flex-two-row">
|
<div class="resume-sub-section flex-two-row">
|
||||||
<div>
|
<div>
|
||||||
<input [name]="'projname' + proj.trackUUID" [(ngModel)]="proj.name" type="text" placeholder="Project Name" />
|
<input [name]="'projname' + proj.trackUUID" [(ngModel)]="proj.name" type="text" placeholder="Project Name" />
|
||||||
<input [name]="'projurl' + proj.trackUUID" [(ngModel)]="proj.url" type="text" placeholder="Reference URL" />
|
<input [name]="'projurl' + proj.trackUUID" [(ngModel)]="proj.url" type="text" (blur)="validateProjectURL(proj)" placeholder="Reference URL" />
|
||||||
</div>
|
</div>
|
||||||
<textarea [name]="'projdescription' + proj.trackUUID" [(ngModel)]="proj.description" placeholder="Description"></textarea>
|
<textarea [name]="'projdescription' + proj.trackUUID" [(ngModel)]="proj.description" placeholder="Description"></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -90,32 +90,22 @@ export class ResumesEditorComponent {
|
|||||||
|
|
||||||
validatePhone(input: string){
|
validatePhone(input: string){
|
||||||
let result = this.validator.validatePhoneNumber(input);
|
let result = this.validator.validatePhoneNumber(input);
|
||||||
if (result[0]){
|
this.resume.phoneNumber = result[1];
|
||||||
this.resume.phoneNumber = result[1];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
validateEmail(input: string){
|
validateEmail(input: string){
|
||||||
let result = this.validator.validateEmail(input);
|
let result = this.validator.validateEmail(input);
|
||||||
if (result[0]){
|
this.resume.email = result[1];
|
||||||
this.resume.email = result[1];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PrintResume(){
|
validateCertURL(input: ResumeCertification){
|
||||||
const divToPrint = document.getElementsByClassName("paper")[0];
|
let result = this.validator.validateURL(input.verificationURL);
|
||||||
|
input.verificationURL = result[1];
|
||||||
|
}
|
||||||
|
|
||||||
const printContents = divToPrint.innerHTML;
|
validateProjectURL(input: ResumeProject){
|
||||||
const originalContents = document.body.innerHTML; // Store original body content
|
let result = this.validator.validateURL(input.url);
|
||||||
|
input.url = result[1];
|
||||||
// Temporarily replace the body content with the div's content
|
|
||||||
document.body.innerHTML = printContents;
|
|
||||||
|
|
||||||
// Trigger the print dialog
|
|
||||||
window.print();
|
|
||||||
|
|
||||||
// Restore the original body content
|
|
||||||
document.body.innerHTML = originalContents;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addExperience(){
|
addExperience(){
|
||||||
|
|||||||
@@ -25,11 +25,65 @@ export class Validation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
emailRegex: RegExp = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/;
|
emailRegex: RegExp = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
||||||
validateEmail(input: string): [boolean, string] {
|
validateEmail(input: string): [boolean, string] {
|
||||||
const corrected = input.trim().toLowerCase();
|
const trimmed = input.trim();
|
||||||
|
const [local, domain] = trimmed.split('@');
|
||||||
|
const corrected = domain ? `${local}@${domain.toLowerCase()}` : trimmed;
|
||||||
|
|
||||||
|
// Extra checks to avoid invalid formats
|
||||||
|
if ( corrected.includes('..') || corrected.startsWith('.') || corrected.endsWith('.') ) {
|
||||||
|
return [false, corrected];
|
||||||
|
}
|
||||||
|
|
||||||
const success = this.emailRegex.test(corrected);
|
const success = this.emailRegex.test(corrected);
|
||||||
return [success, corrected];
|
return [success, corrected];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validateURL(input: string): [boolean, string] {
|
||||||
|
const testUrl = /^https?:\/\//i.test(input) ? input : "http://" + input;
|
||||||
|
var urlToParse = new URL(testUrl);
|
||||||
|
try {
|
||||||
|
|
||||||
|
if (urlToParse.protocol !== 'http:' && urlToParse.protocol !== 'https:') {
|
||||||
|
return [false, "bad protocol"];
|
||||||
|
}
|
||||||
|
|
||||||
|
const hostname = urlToParse.hostname.toLowerCase();
|
||||||
|
if (hostname === 'localhost' || hostname.endsWith('.localhost')) {
|
||||||
|
return [false, "localhost not allowed"];
|
||||||
|
}
|
||||||
|
|
||||||
|
const privateIpRegex = /^(127\.|10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[0-1])\.)/;
|
||||||
|
if (privateIpRegex.test(hostname)) {
|
||||||
|
return [false, "internal ipv4 not allowed"];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hostname.includes(':')) {
|
||||||
|
if (this.isPrivateIPv6(hostname)) {
|
||||||
|
return [false, "internal ipv6 not allowed"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [true, urlToParse.href.substring(0, urlToParse.href.length - 1)];
|
||||||
|
} catch (e) {
|
||||||
|
return [false, "validation error has occurred"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///////// HELPER FUNCTIONS /////////
|
||||||
|
|
||||||
|
isPrivateIPv6(ip: string): boolean {
|
||||||
|
try {
|
||||||
|
const normalized = ip.replace(/^\[|\]$/g, '').toLowerCase();
|
||||||
|
if (normalized === '::1') return true;
|
||||||
|
if (normalized.startsWith('fc') || normalized.startsWith('fd')) return true;
|
||||||
|
const first4 = normalized.slice(0, 4);
|
||||||
|
if (first4 >= 'fe80' && first4 <= 'febf') return true;
|
||||||
|
return false;
|
||||||
|
} catch {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
|
||||||
|
import { BehaviorSubject, firstValueFrom, Observable } from 'rxjs';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class TranslationService {
|
||||||
|
|
||||||
|
private _translations: any = {};
|
||||||
|
private _translationsLoaded: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||||
|
|
||||||
|
constructor(private http: HttpClient) {}
|
||||||
|
|
||||||
|
// Change language dynamically
|
||||||
|
setLanguage(lang: string) {
|
||||||
|
this.http.get(`/lang/${lang}.json`).subscribe({
|
||||||
|
next: data => {
|
||||||
|
this._translations = data;
|
||||||
|
this._translationsLoaded.next(true);
|
||||||
|
},
|
||||||
|
error: err => {
|
||||||
|
this._translations = [];
|
||||||
|
this._translationsLoaded.next(true);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Observable for checking if language is loaded
|
||||||
|
get changed$(): Observable<boolean> {
|
||||||
|
return this._translationsLoaded.asObservable();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a specific translation
|
||||||
|
translate(page: string, key: string): string {
|
||||||
|
return this._translations[page][key] || key;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"home": {
|
||||||
|
"s subTitle": "The Anti AI Job Board",
|
||||||
|
"emailPlaceholder": "Enter your email",
|
||||||
|
"passwordPlaceholder": "Enter your password",
|
||||||
|
"loginButton": "Login"
|
||||||
|
},
|
||||||
|
"application/viewer": {
|
||||||
|
"s Date Applied": "Date Applied: ",
|
||||||
|
"b View Resume": "VIEW RESUME"
|
||||||
|
},
|
||||||
|
"profile": {
|
||||||
|
"pageTitle": "Your Profile",
|
||||||
|
"editButton": "Edit Profile"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user