working #40

Merged
derek merged 8 commits from working into main 2025-09-04 20:15:04 -07:00
17 changed files with 454 additions and 117 deletions
+15
View File
@@ -15,6 +15,19 @@ Server:
Emails: Emails:
Make emails follow theme of website better Make emails follow theme of website better
JobListingController:
Dont refresh on every filter edit
Line 63 is terrible and need to be fixed in the JobListing DB JobListingController
bools and numbers are getting strigified which breaks the mysql parameters
Validation:
Alot of the validation is only taking place client side.
Need to validate all inputs before processing
Phone number
Email Address
City, CountryCode, PostalCode
When applying to a job, The server doesnt make sure that the company only fields are not modified
Client: Client:
jobs/editor: jobs/editor:
Want to add completed job listing preview at end of carosel Want to add completed job listing preview at end of carosel
@@ -29,6 +42,8 @@ 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
Dont allow users to apply to the same job more than once Dont allow users to apply to the same job more than once
Need to add 'job field' to job for better filtering
Need to add 'clear filter button'
resume/viewer: resume/viewer:
CSS is broken CSS is broken
+3 -6
View File
@@ -193,8 +193,8 @@ CREATE TABLE IF NOT EXISTS `PostalCodes` (
`StateCode` varchar(20), `StateCode` varchar(20),
`County` varchar(100), `County` varchar(100),
`CountyCode` varchar(20), `CountyCode` varchar(20),
`Admin` varchar(100), `Community` varchar(100),
`AdminCode` varchar(20), `CommunityCode` varchar(20),
`Latitude` float, `Latitude` float,
`Longitude` float, `Longitude` float,
`Accuracy` varchar(2) `Accuracy` varchar(2)
@@ -206,10 +206,7 @@ FIELDS TERMINATED BY '\t'
ENCLOSED BY '"' ENCLOSED BY '"'
LINES TERMINATED BY '\n'; LINES TERMINATED BY '\n';
CREATE INDEX idx_country_code ON PostalCodes(CountryCode); CREATE INDEX idx_postal_country ON PostalCodes (City, PostalCode, CountryCode, Latitude, Longitude);
CREATE INDEX idx_postal_code ON PostalCodes(PostalCode);
CREATE INDEX idx_latitude ON PostalCodes(Latitude);
CREATE INDEX idx_longitude ON PostalCodes(Longitude);
-- Application Section -- Application Section
+11
View File
@@ -0,0 +1,11 @@
export class JobFilter {
public JobsPerPage: number = 20;
public CurrentPage: number = 1;
public CountryCode: string | null = null;
public PostalCode: string | null = null;
public Distance: number | null = null;
public JobType: string | null = null;
public Remote: boolean | null = null;
public SalaryMin: number | null = null;
public SalaryMax: number | null = null;
}
@@ -1,36 +1,56 @@
button { .primary-button {
height: 45px; height: 45px;
border-radius: 5px; border-radius: 5px;
margin: 10px; margin: 10px;
text-align: center; text-align: center;
padding: 15px 20px; padding: 15px 20px;
transition: 0.5s; transition: 0.5s;
background-color: #00000000; background-color: var(--mistox-button-primary);
border: 1px solid var(--Mistox-Black); border: 1px solid var(--mistox-button-primary);
color: var(--Mistox-Black); color: var(--mistox-button-text);
text-decoration: none; text-decoration: none;
font: inherit; font: inherit;
} }
button:hover { .primary-button:hover {
background-color: #00000044; background-color: var(--mistox-button-primary-click);
color: var(--Mistox-Light); }
.secondary-button {
height: 45px;
border-radius: 5px;
margin: 10px;
text-align: center;
padding: 15px 20px;
transition: 0.5s;
background-color: var(--mistox-button-secondary);
border: 1px solid var(--mistox-button-secondary);
color: var(--mistox-button-text);
text-decoration: none;
font: inherit;
}
.secondary-button:hover {
background-color: var(--mistox-button-secondary-click);
} }
.top-bar { .top-bar {
width: 100%; display: flex;
height: 60px; break-inside: avoid;
padding: 20px;
border-radius: 20px;
margin: 20px;
background-color: var(--mistox-bg-medium);
border: 1px solid var(--mistox-border);
box-shadow: var(--mistox-shadow);
color: var(--mistox-text);
} }
.content-frame { .content-frame {
background-color: #3c3c3c; max-width: 1800px;
width: calc(100% - 40px);
height: calc(100vh - 400px);
border-radius: 20px; border-radius: 20px;
margin: 10px;
overflow: scroll;
padding: 10px; padding: 10px;
color: var(--Mistox-White); margin: 0 auto;
} }
.center-item { .center-item {
@@ -40,7 +60,7 @@ button {
} }
.content-edit { .content-edit {
position: absolute; position: relative;
right: 20px; right: 20px;
} }
@@ -66,22 +86,23 @@ button {
.content-link a { .content-link a {
text-decoration: none; text-decoration: none;
color: var(--Mistox-White);
margin-top: auto; margin-top: auto;
} }
.content-desc { .content-desc {
border: solid 1px red; border: solid 1px var(--mistox-border);
border-radius: 5px; border-radius: 5px;
padding: 20px; padding: 20px;
margin: 0 100px; margin: 0 100px;
margin-bottom: 0px;
margin-bottom: 50px; margin-bottom: 50px;
background-color: var(--mistox-bg-medium);
color: var(--mistox-text);
} }
.content-desc h1 { .content-desc h1 {
margin: 0; margin: 0;
font-size: 20px; font-size: 20px;
color: #ddd;
} }
.content-button { .content-button {
@@ -100,8 +121,6 @@ button {
.half-frame { .half-frame {
width: 50%; width: 50%;
border-right: solid 1px var(--Mistox-Black);
border-left: solid 1px var(--Mistox-Black);
} }
.half-frame h2 { .half-frame h2 {
@@ -110,7 +129,8 @@ button {
.job-tile { .job-tile {
display: flex; display: flex;
background-color: var(--Mistox-Black); background-color: var(--mistox-bg-medium);
border: solid 1px var(--mistox-border-dark);
justify-content: end; justify-content: end;
align-items: center; align-items: center;
border-radius: 10px; border-radius: 10px;
@@ -127,8 +147,3 @@ button {
.job-tile h1 { .job-tile h1 {
margin: 0; margin: 0;
} }
.job-tile button {
color: white;
border-color: white;
}
@@ -1,13 +1,13 @@
<div class="top-bar"> <div class="top-bar">
@for(company of Employers; track company.accountID){ @for(company of Employers; track company.accountID){
<button (click)="changeSelectedCompany(company.company.id!)">{{ company.company.name.toUpperCase() }}</button> <button class="secondary-button" (click)="changeSelectedCompany(company.company.id!)">{{ company.company.name.toUpperCase() }}</button>
} }
<button routerLink="/company/editor" >CONNECT A COMPANY</button> <button class="primary-button" routerLink="/company/editor" >CONNECT A COMPANY</button>
</div> </div>
<div class="content-frame"> <div class="content-frame">
@if(Comp != null){ @if(Comp != null){
<div> <div>
<button class="content-edit" style="color: #fff; border-color: #fff;" routerLink="/company/editor" [queryParams]="{ CompanyID: Comp.id }" >EDIT COMPANY</button> <button class="primary-button content-edit" routerLink="/company/editor" [queryParams]="{ CompanyID: Comp.id }" >EDIT COMPANY</button>
<div class="center-item"> <div class="center-item">
<a [href]="Comp.websiteURL"> <a [href]="Comp.websiteURL">
<img [src]="Comp.logo" /> <img [src]="Comp.logo" />
@@ -31,11 +31,11 @@
<div class="half-frame"> <div class="half-frame">
@if (Comp.emailVerified){ @if (Comp.emailVerified){
<div class="content-button"> <div class="content-button">
<button style="color: #fff; border-color: #fff;" routerLink="/jobs/editor" [queryParams]="{ CompanyID: Comp.id }" >POST JOB</button> <button class="primary-button" routerLink="/jobs/editor" [queryParams]="{ CompanyID: Comp.id }" >POST JOB</button>
</div> </div>
} @else { } @else {
<div class="content-button"> <div class="content-button">
<a style="color: #fff; border-color: #fff;" [href]="'/api/company/sendverifyemail?CompanyID=' + Comp.id" >VERIFY EMAIL></a> <a class="primary-button" [href]="'/api/company/sendverifyemail?CompanyID=' + Comp.id" >VERIFY EMAIL></a>
<span>You must verify your company email before you can post job listings.</span> <span>You must verify your company email before you can post job listings.</span>
</div> </div>
} }
@@ -46,21 +46,21 @@
<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 Applicants</button> <button class="secondary-button" [routerLink]="['/application/viewer']" [queryParams]="{ JobID: listing.id }" >VIEW Applicants</button>
<button [routerLink]="['/jobs/viewer']" [queryParams]="{ JobID: listing.id }" >VIEW LISTING</button> <button class="secondary-button" [routerLink]="['/jobs/viewer']" [queryParams]="{ JobID: listing.id }" >VIEW LISTING</button>
<button [routerLink]="['/jobs/editor']" [queryParams]="{ JobID: listing.id }" >EDIT LISTING</button> <button class="secondary-button" [routerLink]="['/jobs/editor']" [queryParams]="{ JobID: listing.id }" >EDIT LISTING</button>
<button (click)="RemoveJobListing(listing.id!)">DELETE LISTING</button> <button class="secondary-button" (click)="RemoveJobListing(listing.id!)">DELETE LISTING</button>
</div> </div>
} }
</div> </div>
<div class="half-frame"> <div class="half-frame">
@if (Comp.emailVerified){ @if (Comp.emailVerified){
<div class="content-button"> <div class="content-button">
<button style="color: #fff; border-color: #fff;" routerLink="/jobs/editor" [queryParams]="{ CompanyID: Comp.id }" >ADD EMPLOYEE</button> <button class="primary-button" routerLink="/jobs/editor" [queryParams]="{ CompanyID: Comp.id }" >ADD EMPLOYEE</button>
</div> </div>
} @else { } @else {
<div class="content-button"> <div class="content-button">
<a style="color: #fff; border-color: #fff;" [href]="'/api/company/sendverifyemail?CompanyID=' + Comp.id" >VERIFY EMAIL></a> <a class="primary-button" [href]="'/api/company/sendverifyemail?CompanyID=' + Comp.id" >VERIFY EMAIL></a>
<span>You must verify your company email before you can post job listings.</span> <span>You must verify your company email before you can post job listings.</span>
</div> </div>
} }
@@ -72,9 +72,9 @@
<h1>{{ listing.accountName }}</h1> <h1>{{ listing.accountName }}</h1>
</div> </div>
@if (listing.accountID != auth.loggedInUser.id){ @if (listing.accountID != auth.loggedInUser.id){
<button (click)="RemoveJobListing(listing.id!)">Remove</button> <button class="secondary-button" (click)="RemoveJobListing(listing.id!)">Remove</button>
} @else { } @else {
<button disabled>SELF</button> <button class="secondary-button" disabled>SELF</button>
} }
</div> </div>
} }
@@ -14,6 +14,34 @@ button {
background-color: var(--mistox-button-primary-click); background-color: var(--mistox-button-primary-click);
} }
.top-bar {
display: flex;
break-inside: avoid;
padding: 20px;
border-radius: 20px;
margin: 20px;
background-color: var(--mistox-bg-medium);
border: 1px solid var(--mistox-border);
box-shadow: var(--mistox-shadow);
color: var(--mistox-text);
}
.top-bar-sub {
display: flex;
height: 20px;
}
.top-bar-sub :nth-child(1) {
margin: 0;
font-size: 20px;
}
.top-bar-sub :nth-child(2) {
margin-right: 50px;
margin-left: 5px;
height: 15px;
}
.full-width { .full-width {
display: block; display: block;
width: 100%; width: 100%;
@@ -1,3 +1,37 @@
<!-- Filter Bar -->
<div class="top-bar">
<div class="top-bar-sub">
<h1>Country</h1>
<input name="CountryCode" [(ngModel)]="currentFilter.CountryCode" (ngModelChange)="reloadFilters()" type="text" />
</div>
<div class="top-bar-sub">
<h1>Postal Code</h1>
<input name="PostalCode" [(ngModel)]="currentFilter.PostalCode" (ngModelChange)="reloadFilters()" type="text" />
</div>
@if (currentFilter.CountryCode != null && currentFilter.CountryCode !== "" && currentFilter.PostalCode != null && currentFilter.PostalCode !== ""){
<div class="top-bar-sub">
<h1>Distance</h1>
<input name="Distance" [(ngModel)]="currentFilter.Distance" (ngModelChange)="reloadFilters()" type="number" />
</div>
}
<div class="top-bar-sub">
<h1>Job Type</h1>
<input name="CountryCode" [(ngModel)]="currentFilter.JobType" (ngModelChange)="reloadFilters()" type="text" />
</div>
<div class="top-bar-sub">
<h1>Remote</h1>
<input name="Remote" [(ngModel)]="currentFilter.Remote" (ngModelChange)="reloadFilters()" type="checkbox" />
</div>
<div class="top-bar-sub">
<h1>Minimum Salary</h1>
<input name="SalaryMin" [(ngModel)]="currentFilter.SalaryMin" (ngModelChange)="reloadFilters()" type="number" />
</div>
<div class="top-bar-sub">
<h1>Maximum Salary</h1>
<input name="SalaryMax" [(ngModel)]="currentFilter.SalaryMax" (ngModelChange)="reloadFilters()" type="number" />
</div>
</div>
<!-- Avaliable Jobs --> <!-- Avaliable Jobs -->
<div class="tile-frame"> <div class="tile-frame">
@for (cur of JobListingPage; track cur.id){ @for (cur of JobListingPage; track cur.id){
@@ -21,4 +55,8 @@
</div> </div>
</div> </div>
} }
<div>
<h1>Jobs Per Page</h1>
<input name="JobsPerPage" [(ngModel)]="currentFilter.JobsPerPage" (ngModelChange)="reloadFilters()" type="number" />
</div>
</div> </div>
@@ -6,6 +6,7 @@ import { Title } from '@angular/platform-browser';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { JobListing } from 'app/models/JobListing'; import { JobListing } from 'app/models/JobListing';
import { Authentication } from 'app/services/Authentication'; import { Authentication } from 'app/services/Authentication';
import { JobFilter } from 'app/models/JobFilter';
@Component({ @Component({
selector: 'main-jobs', selector: 'main-jobs',
@@ -15,7 +16,7 @@ import { Authentication } from 'app/services/Authentication';
}) })
export class JobsComponent { export class JobsComponent {
public MyJobListings: JobListing[] = []; public currentFilter: JobFilter;
public JobListingPage: JobListing[] = []; public JobListingPage: JobListing[] = [];
public ErrorMsg: string = ""; public ErrorMsg: string = "";
@@ -23,10 +24,50 @@ export class JobsComponent {
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 | BoredCareers"); this.title.setTitle("Jobs | BoredCareers");
this.currentFilter = new JobFilter();
}; };
ngOnInit(){ ngOnInit(){
this.http.get<JobListing[]>("api/joblisting?PageQuantity=" + 10 + "&Page=" + 1).subscribe({ this.http.get<JobListing[]>("api/joblisting?PageQuantity=" + this.currentFilter.JobsPerPage + "&Page=" + this.currentFilter.CurrentPage).subscribe({
next: data => {
this.JobListingPage = data;
},
error: err => {
this.ErrorMsg = err.error;
}
});
}
reloadFilters(){
var queryBuilder = "api/joblisting?PageQuantity=" + this.currentFilter.JobsPerPage + "&Page=" + this.currentFilter.CurrentPage;
if ( this.currentFilter.PostalCode === "" || this.currentFilter.CountryCode === "" ){
this.currentFilter.Distance = null;
}
if (this.currentFilter.PostalCode != null){
queryBuilder += "&PC=" + this.currentFilter.PostalCode;
}
if (this.currentFilter.CountryCode != null){
queryBuilder += "&CC=" + this.currentFilter.CountryCode;
}
if (this.currentFilter.Distance != null){
queryBuilder += "&D=" + this.currentFilter.Distance;
}
if (this.currentFilter.JobType != null){
queryBuilder += "&JT=" + this.currentFilter.JobType;
}
if (this.currentFilter.Remote != null){
queryBuilder += "&R=" + this.currentFilter.Remote;
}
if (this.currentFilter.SalaryMin != null){
queryBuilder += "&SMI=" + this.currentFilter.SalaryMin;
}
if (this.currentFilter.SalaryMax != null){
queryBuilder += "&SMA=" + this.currentFilter.SalaryMax;
}
this.http.get<JobListing[]>(queryBuilder).subscribe({
next: data => { next: data => {
this.JobListingPage = data; this.JobListingPage = data;
}, },
@@ -1,5 +1,5 @@
.company-details { .company-details {
background-color: #5c3030; background-color: var(--mistox-bg-light);
} }
.company-details::after { .company-details::after {
@@ -82,7 +82,7 @@
} }
.job-details { .job-details {
background-color: #3c3c3c; padding-bottom: 40px;
} }
.job-timestamp { .job-timestamp {
@@ -1,26 +1,5 @@
<div class="job-frame"> <div class="job-frame">
@if (jobsCompany != null){
<div class="company-details">
<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">
@for(line of jobsCompany.description.split('\n'); track line.length){
<h1>{{ line }}</h1>
}
</div>
</div>
}
@if (selectedJob != null) { @if (selectedJob != null) {
<div class="job-details"> <div class="job-details">
@if (selectedJob.isDeleted){ @if (selectedJob.isDeleted){
@@ -32,6 +11,12 @@
<h1>{{ selectedJob.title }}</h1> <h1>{{ selectedJob.title }}</h1>
</div> </div>
<div class="split"> <div class="split">
<div class="nobreak">
<h1>Job Description: </h1>
<div class="description-box">
<h1>{{ selectedJob.description }}</h1>
</div>
</div>
<div class="nobreak"> <div class="nobreak">
<h1>Job Details:</h1> <h1>Job Details:</h1>
<div class="detail-block"> <div class="detail-block">
@@ -78,12 +63,6 @@
<span>Opened: {{ selectedJob.createdTime }} | Last Updated: {{ selectedJob.modifiedTime }}</span> <span>Opened: {{ selectedJob.createdTime }} | Last Updated: {{ selectedJob.modifiedTime }}</span>
</div> </div>
</div> </div>
<div class="nobreak">
<h1>Job Description: </h1>
<div class="description-box">
<h1>{{ selectedJob.description }}</h1>
</div>
</div>
</div> </div>
<div class="bottom-bar"> <div class="bottom-bar">
@@ -93,4 +72,28 @@
</div> </div>
</div> </div>
} }
@if (jobsCompany != null){
<div class="company-details">
<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">
@for(line of jobsCompany.description.split('\n'); track line.length){
<h1>{{ line }}</h1>
}
</div>
</div>
}
</div> </div>
+25 -10
View File
@@ -74,16 +74,31 @@ export class Validation {
///////// HELPER FUNCTIONS ///////// ///////// HELPER FUNCTIONS /////////
isPrivateIPv6(ip: string): boolean { isPrivateIPv6(ip: string): boolean {
try { try {
const normalized = ip.replace(/^\[|\]$/g, '').toLowerCase(); const normalized = ip.replace(/^\[|\]$/g, '').toLowerCase();
if (normalized === '::1') return true; if (normalized === '::1') return true;
if (normalized.startsWith('fc') || normalized.startsWith('fd')) return true; if (normalized.startsWith('fc') || normalized.startsWith('fd')) return true;
const first4 = normalized.slice(0, 4); const first4 = normalized.slice(0, 4);
if (first4 >= 'fe80' && first4 <= 'febf') return true; if (first4 >= 'fe80' && first4 <= 'febf') return true;
return false; return false;
} catch { } catch {
return true; return true;
}
}
///////// GETTERS /////////
get ValidCountries(): string[] {
return ['AD', 'AE', 'AI', 'AL', 'AR', 'AS', 'AT', 'AU', 'AX', 'AZ', 'BD', 'BE', 'BG',
'BM', 'BR', 'BY', 'CA', 'CC', 'CH', 'CL', 'CN', 'CO', 'CR', 'CX', 'CY', 'CZ',
'DE', 'DK', 'DO', 'DZ', 'EC', 'EE', 'ES', 'FI', 'FK', 'FM', 'FO', 'FR', 'GB',
'GF', 'GG', 'GI', 'GL', 'GP', 'GS', 'GT', 'GU', 'HK', 'HM', 'HN', 'HR', 'HT',
'HU', 'ID', 'IE', 'IM', 'IN', 'IO', 'IS', 'IT', 'JE', 'JP', 'KE', 'KR', 'LI',
'LK', 'LT', 'LU', 'LV', 'MA', 'MC', 'MD', 'MH', 'MK', 'MO', 'MP', 'MQ', 'MT',
'MW', 'MX', 'MY', 'NC', 'NF', 'NL', 'NO', 'NR', 'NU', 'NZ', 'PA', 'PE', 'PF',
'PH', 'PK', 'PL', 'PM', 'PN', 'PR', 'PT', 'PW', 'RE', 'RO', 'RS', 'RU', 'SE',
'SG', 'SI', 'SJ', 'SK', 'SM', 'TC', 'TH', 'TR', 'UA', 'US', 'UY', 'VA', 'VI',
'WF', 'WS', 'YT', 'ZA'];
} }
}
} }
+25 -18
View File
@@ -12,6 +12,8 @@
--mistox-border: oklch(0.6 0.13 264); --mistox-border: oklch(0.6 0.13 264);
--mistox-border-dark: oklch(0.7 0.13 264); --mistox-border-dark: oklch(0.7 0.13 264);
--mistox-button-text: oklch(1 0.00011 271.152);
--mistox-button-primary: oklch(0.4 0.13 264); --mistox-button-primary: oklch(0.4 0.13 264);
--mistox-button-primary-click: oklch(0.3 0.13 264); --mistox-button-primary-click: oklch(0.3 0.13 264);
@@ -27,28 +29,34 @@
font-family: Arial, Helvetica, sans-serif; font-family: Arial, Helvetica, sans-serif;
} }
dark-mode { @media (prefers-color-scheme: dark) {
--mistox-bg-dark: oklch(0.1 0.065 264); :root {
--mistox-bg-medium: oklch(0.15 0.065 264); --mistox-bg-dark: oklch(0.1 0.065 264);
--mistox-bg-light: oklch(0.2 0.065 264); --mistox-bg-medium: oklch(0.15 0.065 264);
--mistox-bg-light: oklch(0.2 0.065 264);
--mistox-text: oklch(0.96 0.1 264); --mistox-text: oklch(0.96 0.1 264);
--mistox-text-sub: oklch(0.76 0.1 264); --mistox-text-sub: oklch(0.76 0.1 264);
--mistox-border-light: oklch(0.5 0.13 264); --mistox-border-light: oklch(0.5 0.13 264);
--mistox-border: oklch(0.4 0.13 264); --mistox-border: oklch(0.4 0.13 264);
--mistox-border-dark: oklch(0.3 0.13 264); --mistox-border-dark: oklch(0.3 0.13 264);
--mistox-button-primary: oklch(0.76 0.13 264); --mistox-button-text: oklch(0 0.00011 271.152);
--mistox-button-secondary: oklch(0.76 0.13 84);
--mistox-alert-danger: oklch(0.7 0.13 30); --mistox-button-primary: oklch(0.76 0.13 264);
--mistox-alert-warning: oklch(0.7 0.13 100); --mistox-button-secondary: oklch(0.76 0.13 84);
--mistox-alert-success: oklch(0.7 0.13 160);
--mistox-alert-info: oklch(0.7 0.13 260);
--mistox-shadow: 0px 2px 2px oklch(0 0 0 / 0.2), 0px 4px 4px oklch(0 0 0 / 0.1); --mistox-alert-danger: oklch(0.7 0.13 30);
font-family: Arial, Helvetica, sans-serif; --mistox-alert-warning: oklch(0.7 0.13 100);
--mistox-alert-success: oklch(0.7 0.13 160);
--mistox-alert-info: oklch(0.7 0.13 260);
--mistox-shadow: 0px 2px 2px oklch(0 0 0 / 0.2), 0px 4px 4px oklch(0 0 0 / 0.1);
color: #fff;
font-family: Arial, Helvetica, sans-serif;
}
} }
html { html {
@@ -56,6 +64,5 @@ html {
} }
body { body {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='28' height='49' viewBox='0 0 28 49'%3E%3Cg fill-rule='evenodd'%3E%3Cg id='hexagons' fill='%23999999' fill-opacity='0.2' fill-rule='nonzero'%3E%3Cpath d='M13.99 9.25l13 7.5v15l-13 7.5L1 31.75v-15l12.99-7.5zM3 17.9v12.7l10.99 6.34 11-6.35V17.9l-11-6.34L3 17.9zM0 15l12.98-7.5V0h-2v6.35L0 12.69v2.3zm0 18.5L12.98 41v8h-2v-6.85L0 35.81v-2.3zM15 0v7.5L27.99 15H28v-2.31h-.01L17 6.35V0h-2zm0 49v-8l12.99-7.5H28v2.31h-.01L17 42.15V49h-2z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
background-color: var(--mistox-bg-dark); background-color: var(--mistox-bg-dark);
} }
+19 -2
View File
@@ -31,8 +31,25 @@ namespace BoredCareers.Controllers {
} }
[HttpGet] [HttpGet]
public async Task<IActionResult> GetJobListings(int Page = 1, int PageQuantity = 25) { public async Task<IActionResult> GetJobListings(string? CC, string? PC, float? D, string? JT, bool? R, int? SMI, int? SMA, int Page = 1, int PageQuantity = 25 ) {
JobListing[] jobListings = await _databaseService.GetJobListingPage(Page, PageQuantity);
// Get all relevant postal codes
List<string> PostalCodes = new List<string>();
if (PC != null && CC != null && D != null) {
Location[] nearby = await _databaseService.GetNearbyLocations(PC, CC, D.Value);
foreach (Location cur in nearby) {
PostalCodes.Add(cur.PostalCode);
}
} else if (PC != null) {
PostalCodes.Add(PC);
}
string[]? pc = null;
if (PostalCodes.Count > 0) {
pc = PostalCodes.ToArray();
}
JobListing[] jobListings = await _databaseService.GetJobListingPage(Page, PageQuantity, pc, CC, JT, R, SMI, SMA );
return Ok(jobListings); return Ok(jobListings);
} }
@@ -0,0 +1,22 @@
using Microsoft.AspNetCore.Mvc;
using BoredCareers.Services.DatabaseService;
using BoredCareers.Entities;
namespace BoredCareers.Controllers {
[ApiController]
[Route("api/location")]
public class LocationController : MistoxControllerBase {
public LocationController(DatabaseService db) : base(db) {}
[HttpGet]
public async Task<IActionResult> GetCompany(string PostalCode, string CountryCode, float MaxDistanceKm) {
if (isLoggedIn()) {
Location[] places = await _databaseService.GetNearbyLocations(PostalCode, CountryCode, MaxDistanceKm);
return Ok(places.ToArray());
}
return NotFound("Not logged in");
}
}
}
+10
View File
@@ -0,0 +1,10 @@
namespace BoredCareers.Entities {
public class Location {
public string City { get; set; } = "";
public string PostalCode { get; set; } = "";
public string CountryCode { get; set; } = "";
public float Latitude { get; set; }
public float Longitude { get; set; }
public float DistanceKM { get; set; }
}
}
@@ -7,21 +7,73 @@ using System.Data.Common;
namespace BoredCareers.Services.DatabaseService { namespace BoredCareers.Services.DatabaseService {
public partial class DatabaseService { public partial class DatabaseService {
public async Task<JobListing[]> GetJobListingPage(int PageNumber, int CountPerPage) { public async Task<JobListing[]> GetJobListingPage(int PageNumber, int CountPerPage, string[]? PostalCodes = null, string? CountryCode = null, string? JobType = null, bool? Remote = null, int? SalaryMin = null, int? SalaryMax = null ) {
List<JobListing> joblistings = new List<JobListing>(); List<JobListing> joblistings = new List<JobListing>();
using (MySqlConnection connection = GetConnection()) { using (MySqlConnection connection = GetConnection()) {
await connection.OpenAsync(); await connection.OpenAsync();
string command = @"
SELECT *
FROM JobListing
WHERE IsDeleted = FALSE
ORDER BY CreatedTime DESC
LIMIT @PageSize OFFSET @PageNumber;
";
MySqlCommand cmd = new MySqlCommand(command, connection); string select = "SELECT * FROM JobListing";
string order = " ORDER BY CreatedTime DESC";
string limit = " LIMIT @PageSize OFFSET @PageNumber;";
List<string> Filters = new List<string>();
List<object> Parameters = new List<object>();
List<string> ParameterName = new List<string>();
if (PostalCodes != null) {
for (int i = 0; i < PostalCodes.Length; i++) {
Filters.Add("PostalCode");
Parameters.Add(PostalCodes[i]);
ParameterName.Add(" = @PostalCode" + i);
}
}
if (CountryCode != null) {
Filters.Add("Country");
Parameters.Add(CountryCode);
ParameterName.Add(" = @CountryCode");
}
if (JobType != null) {
Filters.Add("JobType");
Parameters.Add(JobType);
ParameterName.Add(" = @JobType");
}
if (Remote != null) {
Filters.Add("Remote");
Parameters.Add(Remote.Value);
ParameterName.Add(" = @Remote");
}
if (SalaryMin != null) {
Filters.Add("SalaryMin");
Parameters.Add(SalaryMin.Value);
ParameterName.Add(" >= @SalaryMin");
}
if (SalaryMax != null) {
Filters.Add("SalaryMax");
Parameters.Add(SalaryMax.Value);
ParameterName.Add(" <= @SalaryMax");
}
string filter = " WHERE IsDeleted = False";
for (int i = 0; i < Filters.Count; i++) {
if (Filters[i] == "PostalCode" && i != 0) {
filter += " OR ";
} else {
filter += " AND ";
}
filter += Filters[i] + ParameterName[i];
}
MySqlCommand cmd = new MySqlCommand(select + filter + order + limit, connection);
cmd.Parameters.AddWithValue("@PageSize", CountPerPage); cmd.Parameters.AddWithValue("@PageSize", CountPerPage);
cmd.Parameters.AddWithValue("@PageNumber", (PageNumber - 1) * CountPerPage); cmd.Parameters.AddWithValue("@PageNumber", (PageNumber - 1) * CountPerPage);
for (int i = 0; i < Filters.Count; i++) {
cmd.Parameters.AddWithValue( ParameterName[i].Split(' ').Last(), Parameters[i] );
}
using (DbDataReader reader = await cmd.ExecuteReaderAsync()) { using (DbDataReader reader = await cmd.ExecuteReaderAsync()) {
while (await reader.ReadAsync()) { while (await reader.ReadAsync()) {
@@ -0,0 +1,66 @@
using BoredCareers.Entities;
using MySql.Data.MySqlClient;
using System.Data;
using System.Data.Common;
using System.Text;
namespace BoredCareers.Services.DatabaseService {
public partial class DatabaseService {
public async Task<Location[]> GetNearbyLocations(string PostalCode, string CountryCode, float MaxDistanceKm) {
List<Location> closePostalCodes = new List<Location>();
using (MySqlConnection connection = GetConnection()) {
await connection.OpenAsync();
string command = @"
SELECT
pc2.PostalCode,
pc2.CountryCode,
pc2.Latitude,
pc2.Longitude,
pc2.City,
(
6371 * acos(
cos(radians(pc1.Latitude)) *
cos(radians(pc2.Latitude)) *
cos(radians(pc2.Longitude) - radians(pc1.Longitude)) +
sin(radians(pc1.Latitude)) *
sin(radians(pc2.Latitude))
)
) AS distance_km
FROM PostalCodes pc1
JOIN PostalCodes pc2 ON pc2.CountryCode = 'us'
WHERE pc1.PostalCode = @PostalCode AND pc1.CountryCode = @CountryCode
HAVING distance_km <= @MaxDistanceKm
ORDER BY distance_km;
";
MySqlCommand cmd = new MySqlCommand(command, connection);
cmd.Parameters.AddWithValue("@PostalCode", PostalCode);
cmd.Parameters.AddWithValue("@CountryCode", CountryCode);
cmd.Parameters.AddWithValue("@MaxDistanceKm", MaxDistanceKm);
using (DbDataReader reader = await cmd.ExecuteReaderAsync()) {
while (await reader.ReadAsync()) {
string _city = reader.GetString("City");
string _postalCode = reader.GetString("PostalCode");
string _countryCode = reader.GetString("CountryCode");
float _latitude = reader.GetFloat("Latitude");
float _longitude = reader.GetFloat("Longitude");
float _distanceKm = reader.GetFloat("distance_km");
closePostalCodes.Add(new Location() {
City = _city,
PostalCode = _postalCode,
CountryCode = _countryCode,
Latitude = _latitude,
Longitude = _longitude,
DistanceKM = _distanceKm
});
}
}
}
return closePostalCodes.ToArray();
}
}
}