Compare commits

7 Commits

Author SHA1 Message Date
derek bf6137fd9c Start work on mobile UI 2025-10-15 19:55:19 -07:00
derek 9ee4a83759 Fix the number per page 2025-10-15 17:04:56 -07:00
derek 0f557a5bc7 Cleanup apply process 2025-10-15 17:04:33 -07:00
derek 1de00480cc Start work for dropdown filtering 2025-10-13 20:54:20 -07:00
derek 436ae6d543 Query List for Countries 2025-10-13 20:54:05 -07:00
derek 50a9678669 Merge branch 'working' of https://git.mistox.net/derek/boredcareers into working 2025-09-23 20:56:33 -07:00
derek 096d2583f4 Update todo 2025-09-23 20:56:32 -07:00
14 changed files with 217 additions and 77 deletions
+2 -5
View File
@@ -9,16 +9,13 @@ Server:
Update the company rating
JobCleanupService:
Need to update notification email to show what job and to follow theme of website
Need to update notification email to show what job was autoclosed and to follow theme of website | base off company email verify
CompanyEmailVerify:
Need to update notification email to follow theme of website
Create page to notify cx that their work email has been verified
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
Need to work on job filtering more
Validation:
Alot of the validation is only taking place client side.
+20 -3
View File
@@ -247,6 +247,7 @@
"resolved": "https://registry.npmjs.org/@angular/common/-/common-20.0.3.tgz",
"integrity": "sha512-HqqVqaj+xzByWJOIrONVRkpvM6mRuGmC+m9wKixhc9f+xXsymVTBR6xg+G/RwyYP2NuC5chxIZbaJTz2Hj+6+g==",
"license": "MIT",
"peer": true,
"dependencies": {
"tslib": "^2.3.0"
},
@@ -263,6 +264,7 @@
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-20.0.3.tgz",
"integrity": "sha512-CShPNvqqV5Cleyho8CKtcFlt7l2thHPUdXZPtKHH3Zf43KojvJbJksZLBz6ZbyoQdgxNMYSfbh4h0UbSGtPOzQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"tslib": "^2.3.0"
},
@@ -276,6 +278,7 @@
"integrity": "sha512-u+fYnx1sRrwL0fd8kaAD2LqJjfe/Zj7zyOv0A3Ue7r8jzdNsPU8MWr/QyBaWlqSpPEpR+kD3xmDvRT9ra9RTBA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/core": "7.27.4",
"@jridgewell/sourcemap-codec": "^1.4.14",
@@ -417,6 +420,7 @@
"resolved": "https://registry.npmjs.org/@angular/core/-/core-20.0.3.tgz",
"integrity": "sha512-kB6w1bQgClfmkTbWJeD3vSLqX0e3uSaJD6KJ7XXT1IEaqUs4J+mKRKHQyxpJlpdUb7R+jDaHSM/vrVF15/L2rA==",
"license": "MIT",
"peer": true,
"dependencies": {
"tslib": "^2.3.0"
},
@@ -460,6 +464,7 @@
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-20.0.3.tgz",
"integrity": "sha512-cba0bibw9dJ8b+a2a8mwkiq5/HPiakY9P5OiJEVefN+2V/K9CND/pW+KIbW0/P6KhSSDQ29xgcGRseVtkjYLmg==",
"license": "MIT",
"peer": true,
"dependencies": {
"tslib": "^2.3.0"
},
@@ -526,6 +531,7 @@
"integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.27.1",
@@ -1417,6 +1423,7 @@
"integrity": "sha512-5AOrZPf2/GxZ+SDRZ5WFplCA2TAQgK3OYrXCYmJL5NaTu4ECcoWFlfUZuw7Es++6Njv7iu/8vpYJhuzxUH76Vg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@inquirer/checkbox": "^4.1.6",
"@inquirer/confirm": "^5.1.10",
@@ -3286,6 +3293,7 @@
"integrity": "sha512-MX4Zioh39chHlDJbKmEgydJDS3tspMP/lnQC67G3SWsTnb9NeYVWOjkxpOSy4oMfPs4StcWHwBrvUb4ybfnuaw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~7.8.0"
}
@@ -3601,6 +3609,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001718",
"electron-to-chromium": "^1.5.160",
@@ -5542,7 +5551,8 @@
"resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.7.1.tgz",
"integrity": "sha512-QnurrtpKsPoixxG2R3d1xP0St/2kcX5oTZyDyQJMY+Vzi/HUlu1kGm+2V8Tz+9lV991leB1l0xcsyz40s9xOOw==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/js-tokens": {
"version": "4.0.0",
@@ -5634,6 +5644,7 @@
"integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@colors/colors": "1.5.0",
"body-parser": "^1.19.0",
@@ -7483,6 +7494,7 @@
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"tslib": "^2.1.0"
}
@@ -7518,6 +7530,7 @@
"integrity": "sha512-sF6TWQqjFvr4JILXzG4ucGOLELkESHL+I5QJhh7CNaE+Yge0SI+ehCatsXhJ7ymU1hAFcIS3/PBpjdIbXoyVbg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"chokidar": "^4.0.0",
"immutable": "^5.0.2",
@@ -8272,7 +8285,8 @@
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
"license": "0BSD",
"peer": true
},
"node_modules/tuf-js": {
"version": "3.0.1",
@@ -8322,6 +8336,7 @@
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -8488,6 +8503,7 @@
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
@@ -8925,7 +8941,8 @@
"version": "0.15.1",
"resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz",
"integrity": "sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==",
"license": "MIT"
"license": "MIT",
"peer": true
}
}
}
+6
View File
@@ -11,3 +11,9 @@ export class Application {
public notes: string = "";
public trackUUID: string = crypto.randomUUID();
}
export class ApplicationRequest {
public resumeID: number = 0;
public jobListingID: number = 0;
public responseEmail: string = "";
}
@@ -15,11 +15,10 @@ button {
}
.top-bar {
display: flex;
flex-wrap: wrap;
break-inside: avoid;
padding: 20px;
border-radius: 20px;
width: 400px;
margin: 20px;
background-color: var(--mistox-bg-medium);
border: 1px solid var(--mistox-border);
@@ -31,11 +30,15 @@ button {
display: flex;
height: 40px;
align-items: center;
width: 100%;
}
.top-bar-sub h1 {
font-size: 20px;
}
.top-bar-sub :nth-child(1) {
margin: 0;
font-size: 20px;
width: 100%;
}
.top-bar-sub :nth-child(2) {
@@ -44,6 +47,7 @@ button {
margin-top: 0;
padding: initial;
height: 20px;
width: 100%;
}
.full-width {
@@ -120,6 +124,9 @@ button {
}
.jobs-per-page {
position: absolute;
bottom: 100px;
right: 0;
display: flex;
justify-content: end;
padding: 20px;
@@ -132,6 +139,10 @@ button {
font-size: 20px;
}
.jobs-per-page select {
width: 100px;
}
@media (max-width: 1920px) {
.tile-frame{
grid-template-columns: repeat(3, 1fr);
@@ -1,43 +1,70 @@
<!-- Filter Bar -->
<div class="top-bar">
<div class="tile-frame">
<!-- Filter -->
<div class="tile">
<div class="top-bar-sub">
<div>
<h1>Country</h1>
<input name="CountryCode" [(ngModel)]="currentFilter.CountryCode" (ngModelChange)="checkUpdate()" type="text" />
</div>
<select name="CountryCode" [(ngModel)]="currentFilter.CountryCode" type="text">
@for(cur of countryOptions; track cur){
<option [value]="cur">{{cur}}</option>
}
</select>
</div>
<div class="top-bar-sub">
<div>
<h1>Postal Code</h1>
<input name="PostalCode" [(ngModel)]="currentFilter.PostalCode" (ngModelChange)="checkUpdate()" type="text" />
</div>
<input name="PostalCode" [(ngModel)]="currentFilter.PostalCode" type="text" />
</div>
@if (currentFilter.CountryCode != null && currentFilter.CountryCode !== "" && currentFilter.PostalCode != null && currentFilter.PostalCode !== ""){
<div class="top-bar-sub">
<div>
<h1>Distance</h1>
<input name="Distance" [(ngModel)]="currentFilter.Distance" type="number" />
</div>
<select name="Distance" [(ngModel)]="currentFilter.Distance" type="number">
@for(cur of distanceOptions; track cur){
<option [value]="cur">{{cur}}</option>
}
</select>
</div>
}
<div class="top-bar-sub">
<div>
<h1>Job Type</h1>
<input name="CountryCode" [(ngModel)]="currentFilter.JobType" type="text" />
</div>
<div class="top-bar-sub">
<h1>Remote</h1>
<input name="Remote" [(ngModel)]="currentFilter.Remote" type="checkbox" />
</div>
<select name="JobType" [(ngModel)]="currentFilter.JobType" type="text">
@for(cur of jobTypeOptions; track cur){
<option [value]="cur">{{cur}}</option>
}
</select>
</div>
<div class="top-bar-sub">
<div>
<h1>Minimum Salary</h1>
</div>
<input name="SalaryMin" [(ngModel)]="currentFilter.SalaryMin" type="number" />
</div>
<div class="top-bar-sub">
<div>
<h1>Maximum Salary</h1>
</div>
<input name="SalaryMax" [(ngModel)]="currentFilter.SalaryMax" type="number" />
</div>
<div class="top-bar-sub">
<div>
<h1>Remote</h1>
</div>
<input name="Remote" [(ngModel)]="currentFilter.Remote" type="checkbox" />
</div>
<div class="top-bar-sub">
<h1></h1>
<input name="ApplyButton" style="height: 25px;" value="APPLY" type="button" (click)="reloadFilters()" />
</div>
</div>
</div>
<!-- Avaliable Jobs -->
<div class="tile-frame">
<!-- Avaliable Jobs -->
@for (cur of JobListingPage; track cur.id){
<div class="tile">
<div class="tile-title">
@@ -65,5 +92,10 @@
<div class="jobs-per-page">
<h1>Jobs Per Page</h1>
<input name="JobsPerPage" [(ngModel)]="currentFilter.JobsPerPage" (ngModelChange)="reloadFilters()" type="number" />
<select name="JobsPerPage" [(ngModel)]="currentFilter.JobsPerPage" (ngModelChange)="reloadFilters()" type="number">
@for(cur of itemsPerPageOptions; track cur){
<option [value]="cur">{{cur}}</option>
}
</select>
</div>
@@ -16,6 +16,11 @@ import { JobFilter } from 'app/models/JobFilter';
})
export class JobsComponent {
public distanceOptions = [10, 25, 50, 100];
public itemsPerPageOptions = [10, 25, 50, 100];
public countryOptions: string[] = [];
public jobTypeOptions: string[] = ["", "Full-time", "Part-time", "Contract", "Temporary", "Internship"];
public currentFilter: JobFilter;
public JobListingPage: JobListing[] = [];
public ErrorMsg: string = "";
@@ -36,12 +41,17 @@ export class JobsComponent {
this.ErrorMsg = err.error;
}
});
this.http.get<string[]>("/api/location/distinct").subscribe({
next: data => {
this.countryOptions = data;
this.currentFilter.CountryCode = "US";
},
error: err => {
this.ErrorMsg = err.error;
}
checkUpdate(){
if ( this.currentFilter.PostalCode === "" || this.currentFilter.CountryCode === "" ){
this.currentFilter.Distance = null;
}
});
this.currentFilter.JobsPerPage = this.itemsPerPageOptions[0];
this.currentFilter.Distance = this.distanceOptions[0];
}
reloadFilters(){
@@ -57,7 +67,7 @@ export class JobsComponent {
if (this.currentFilter.CountryCode != null){
queryBuilder += "&CC=" + this.currentFilter.CountryCode;
}
if (this.currentFilter.Distance != null){
if (this.currentFilter.PostalCode != null && this.currentFilter.CountryCode != null){
queryBuilder += "&D=" + this.currentFilter.Distance;
}
if (this.currentFilter.JobType != null){
@@ -1,5 +1,7 @@
.company-details {
background-color: var(--mistox-bg-light);
flex: 1;
min-width: 650px;
}
.company-details::after {
@@ -81,8 +83,13 @@
align-content: center;
}
.job-frame {
display: flex;
min-height: calc(100vh - 300px);
}
.job-details {
padding-bottom: 40px;
flex: 1 1 500px;
}
.job-timestamp {
@@ -158,3 +165,27 @@
display: grid;
}
@media (max-width: 1640px) {
.skill-combo {
column-count: 1;
}
}
@media (max-width: 1500px) {
.split {
column-count: 1;
}
}
@media (max-width: 1200px) {
.job-frame {
display: block;
}
}
@@ -67,7 +67,7 @@
<div class="bottom-bar">
@for(resume of myResumes; track resume.trackUUID){
<button type="button" (click)="applyWithResume(resume)">APPLY USING RESUME: {{ resume.name }}</button>
<button type="button" (click)="applyWithResume(resume)">APPLY USING RESUME: {{ resume.title }}</button>
}
</div>
</div>
@@ -8,7 +8,7 @@ import { Authentication } from 'app/services/Authentication';
import { JobListing } from 'app/models/JobListing';
import { Company } from 'app/models/Company';
import { Resume } from 'app/models/Resume';
import { Application } from 'app/models/Application';
import { ApplicationRequest } from 'app/models/Application';
@Component({
selector: 'main-jobs-viewer',
@@ -69,18 +69,13 @@ export class JobViewerComponent {
}
applyWithResume(resume: Resume) {
var application = new Application;
if (this.auth.loggedInUser.id != null){
application.accountID = this.auth.loggedInUser.id;
}
var application = new ApplicationRequest;
if (resume.id != null){
application.resumeID = resume.id;
}
application.jobListingID = this.JobListingID;
application.hasBeenViewed = false;
application.responseEmail = resume.email;
this.http.post("api/application", application).subscribe({
@@ -28,14 +28,22 @@ namespace BoredCareers.Controllers {
}
[HttpPost]
public async Task<IActionResult> SetApplication([FromBody] Application application) {
public async Task<IActionResult> SetApplication([FromBody] ApplicationRequest appReq) {
if (isLoggedIn()) {
if (application.AccountID == getLoggedInUserID()) {
Application application = new Application() {
AccountID = getLoggedInUserID(),
DateApplied = DateTime.UtcNow,
JobListingID = appReq.JobListingID,
ResumeID = appReq.ResumeID,
ResponseEmail = appReq.ResponseEmail,
HasBeenViewed = false,
ResponseStatus = "",
Notes = "",
Rating = -1
};
await _databaseService.SetApplication(application);
return Ok();
}
return NotFound("Cannot apply for someone else");
}
return NotFound("Not logged in");
}
@@ -18,5 +18,14 @@ namespace BoredCareers.Controllers {
return NotFound("Not logged in");
}
[HttpGet("Distinct")]
public async Task<IActionResult> GetCountries() {
if (isLoggedIn()) {
string[] places = await _databaseService.GetDistinctCountries();
return Ok(places.ToArray());
}
return NotFound("Not logged in");
}
}
}
+6
View File
@@ -11,4 +11,10 @@ namespace BoredCareers.Entities {
public int Rating { get; set; }
public string Notes { get; set; } = "";
}
public class ApplicationRequest {
public int ResumeID { get; set; } // FK
public int JobListingID { get; set; } // FK
public string ResponseEmail { get; set; } = "";
}
}
@@ -62,5 +62,23 @@ namespace BoredCareers.Services.DatabaseService {
return closePostalCodes.ToArray();
}
public async Task<string[]> GetDistinctCountries() {
List<string> closePostalCodes = new List<string>();
using (MySqlConnection connection = GetConnection()) {
await connection.OpenAsync();
string command = "SELECT DISTINCT(CountryCode) FROM PostalCodes;";
MySqlCommand cmd = new MySqlCommand(command, connection);
using (DbDataReader reader = await cmd.ExecuteReaderAsync()) {
while (await reader.ReadAsync()) {
string _city = reader.GetString("CountryCode");
closePostalCodes.Add(_city);
}
}
}
return closePostalCodes.ToArray();
}
}
}
@@ -21,12 +21,12 @@ namespace BoredCareers.Services {
</div>
<div style=""padding: 20px; text-align: left; font-size: 16px; color: oklch(.76 .1 264);"">
<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>Thank you for connecting your company to our service</p>
<p>In order to start listing opportunities 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: oklch(.76 .13 84); 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>If you didn't link a company to your account please ignore this email.</p>
<p>Best regards</p>
</div>
<div style=""padding: 10px; text-align: center; background-color: oklch(.15 .065 264); color: #888888; font-size: 12px; border-bottom-left-radius: 8px; border-bottom-right-radius: 8px;"">