From 72fb4f2536c0fe2f1c2a11e72e67c800163ee3c1 Mon Sep 17 00:00:00 2001 From: Derek Holloway Date: Mon, 1 Sep 2025 13:31:06 -0700 Subject: [PATCH 1/7] Add location filtering based on ZIP + Country --- database/mistox.sql | 5 +- src/Server/Controllers/LocationController.cs | 28 ++++++ src/Server/Entities/Location.cs | 10 ++ .../Services/DatabaseService/Location.cs | 95 +++++++++++++++++++ 4 files changed, 134 insertions(+), 4 deletions(-) create mode 100644 src/Server/Controllers/LocationController.cs create mode 100644 src/Server/Entities/Location.cs create mode 100644 src/Server/Services/DatabaseService/Location.cs diff --git a/database/mistox.sql b/database/mistox.sql index 59032b3..2d08131 100755 --- a/database/mistox.sql +++ b/database/mistox.sql @@ -206,10 +206,7 @@ FIELDS TERMINATED BY '\t' ENCLOSED BY '"' LINES TERMINATED BY '\n'; -CREATE INDEX idx_country_code ON PostalCodes(CountryCode); -CREATE INDEX idx_postal_code ON PostalCodes(PostalCode); -CREATE INDEX idx_latitude ON PostalCodes(Latitude); -CREATE INDEX idx_longitude ON PostalCodes(Longitude); +CREATE INDEX idx_postal_country ON PostalCodes (City, PostalCode, CountryCode, Latitude, Longitude); -- Application Section diff --git a/src/Server/Controllers/LocationController.cs b/src/Server/Controllers/LocationController.cs new file mode 100644 index 0000000..e855975 --- /dev/null +++ b/src/Server/Controllers/LocationController.cs @@ -0,0 +1,28 @@ +using Microsoft.AspNetCore.Mvc; +using BoredCareers.Services.DatabaseService; +using BoredCareers.Entities; +using BoredCareers.Services; + +namespace BoredCareers.Controllers { + [ApiController] + [Route("api/location")] + public class LocationController : MistoxControllerBase { + + public LocationController(DatabaseService db, EmailService emailContext) : base(db) {} + + [HttpGet] + public async Task GetCompany(string PostalCode, string CountryCode, float MaxDistanceKm) { + if (isLoggedIn()) { + + Location? MyZIP = await _databaseService.GetLocation(PostalCode, CountryCode); + if (MyZIP != null) { + Location[] places = await _databaseService.GetNearbyLocations(MyZIP.Latitude, MyZIP.Longitude, MyZIP.CountryCode, MaxDistanceKm); + return Ok(places.ToArray()); + } + return NotFound("Postal + CountryCode not found"); + } + return NotFound("Not logged in"); + } + + } +} diff --git a/src/Server/Entities/Location.cs b/src/Server/Entities/Location.cs new file mode 100644 index 0000000..0fa6711 --- /dev/null +++ b/src/Server/Entities/Location.cs @@ -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; } + } +} \ No newline at end of file diff --git a/src/Server/Services/DatabaseService/Location.cs b/src/Server/Services/DatabaseService/Location.cs new file mode 100644 index 0000000..467d00b --- /dev/null +++ b/src/Server/Services/DatabaseService/Location.cs @@ -0,0 +1,95 @@ +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 GetLocation(string PostalCode, string CountryCode) { + using (MySqlConnection connection = GetConnection()) { + await connection.OpenAsync(); + string command = @" + SELECT PostalCode, CountryCode, Latitude, Longitude, City + FROM PostalCodes + WHERE PostalCode = @PostalCode + AND CountryCode = @CountryCode; + "; + + MySqlCommand cmd = new MySqlCommand(command, connection); + cmd.Parameters.AddWithValue("@PostalCode", PostalCode); + cmd.Parameters.AddWithValue("@CountryCode", CountryCode); + + 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"); + + return new Location() { + City = _city, + PostalCode = _postalCode, + CountryCode = _countryCode, + Latitude = _latitude, + Longitude = _longitude + }; + } + } + } + return null; + } + + public async Task GetNearbyLocations(float Latitude, float Longitude, string CountryCode, float MaxDistanceKm) { + List closePostalCodes = new List(); + using (MySqlConnection connection = GetConnection()) { + await connection.OpenAsync(); + string command = @" + SELECT PostalCode, CountryCode, Latitude, Longitude, City, ( + 6371 * acos( + cos(radians(@Latitude)) * + cos(radians(Latitude)) * + cos(radians(Longitude) - radians(@Longitude)) + + sin(radians(@Latitude)) * + sin(radians(Latitude)) + ) + ) AS distance_km + FROM PostalCodes + WHERE countrycode = @CountryCode + HAVING distance_km <= @MaxDistanceKm + ORDER BY distance_km; + "; + + MySqlCommand cmd = new MySqlCommand(command, connection); + cmd.Parameters.AddWithValue("@Latitude", Latitude); + cmd.Parameters.AddWithValue("@Longitude", Longitude); + 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(); + } + + } +} -- 2.52.0 From 73c1e1ce98a3ff112af1fa5789e68d41e8b7248a Mon Sep 17 00:00:00 2001 From: Derek Holloway Date: Tue, 2 Sep 2025 22:31:05 -0700 Subject: [PATCH 2/7] Start work on filtering logic --- ToDo.yaml | 13 ++++ database/mistox.sql | 4 +- src/Client/src/app/models/JobFilter.ts | 11 +++ .../src/app/pages/jobs/jobs.component.html | 36 ++++++++++ .../src/app/pages/jobs/jobs.component.ts | 41 ++++++++++- src/Client/src/app/services/Validation.ts | 35 +++++++--- .../Controllers/JobListingController.cs | 22 +++++- src/Server/Controllers/LocationController.cs | 12 +--- .../Services/DatabaseService/JobListing.cs | 70 ++++++++++++++++--- .../Services/DatabaseService/Location.cs | 69 ++++++------------ 10 files changed, 230 insertions(+), 83 deletions(-) create mode 100644 src/Client/src/app/models/JobFilter.ts diff --git a/ToDo.yaml b/ToDo.yaml index 883180f..80a2eb7 100755 --- a/ToDo.yaml +++ b/ToDo.yaml @@ -15,6 +15,19 @@ Server: Emails: 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: jobs/editor: Want to add completed job listing preview at end of carosel diff --git a/database/mistox.sql b/database/mistox.sql index 2d08131..068b5b0 100755 --- a/database/mistox.sql +++ b/database/mistox.sql @@ -193,8 +193,8 @@ CREATE TABLE IF NOT EXISTS `PostalCodes` ( `StateCode` varchar(20), `County` varchar(100), `CountyCode` varchar(20), - `Admin` varchar(100), - `AdminCode` varchar(20), + `Community` varchar(100), + `CommunityCode` varchar(20), `Latitude` float, `Longitude` float, `Accuracy` varchar(2) diff --git a/src/Client/src/app/models/JobFilter.ts b/src/Client/src/app/models/JobFilter.ts new file mode 100644 index 0000000..7ccd085 --- /dev/null +++ b/src/Client/src/app/models/JobFilter.ts @@ -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; +} \ No newline at end of file diff --git a/src/Client/src/app/pages/jobs/jobs.component.html b/src/Client/src/app/pages/jobs/jobs.component.html index cdfd17c..60fd90c 100644 --- a/src/Client/src/app/pages/jobs/jobs.component.html +++ b/src/Client/src/app/pages/jobs/jobs.component.html @@ -1,3 +1,35 @@ + +
+
+

Country

+ +
+
+

Postal Code

+ +
+
+

Distance

+ +
+
+

Job Type

+ +
+
+

Remote

+ +
+
+

Minimum Salary

+ +
+
+

Maximum Salary

+ +
+
+
@for (cur of JobListingPage; track cur.id){ @@ -21,4 +53,8 @@
} +
+

Jobs Per Page

+ +
\ No newline at end of file diff --git a/src/Client/src/app/pages/jobs/jobs.component.ts b/src/Client/src/app/pages/jobs/jobs.component.ts index 827bd86..04b71d3 100644 --- a/src/Client/src/app/pages/jobs/jobs.component.ts +++ b/src/Client/src/app/pages/jobs/jobs.component.ts @@ -6,6 +6,7 @@ import { Title } from '@angular/platform-browser'; import { CommonModule } from '@angular/common'; import { JobListing } from 'app/models/JobListing'; import { Authentication } from 'app/services/Authentication'; +import { JobFilter } from 'app/models/JobFilter'; @Component({ selector: 'main-jobs', @@ -15,7 +16,7 @@ import { Authentication } from 'app/services/Authentication'; }) export class JobsComponent { - public MyJobListings: JobListing[] = []; + public currentFilter: JobFilter; public JobListingPage: JobListing[] = []; public ErrorMsg: string = ""; @@ -23,10 +24,46 @@ export class JobsComponent { constructor( private http: HttpClient, private router: Router, private route: ActivatedRoute, private title: Title, public auth: Authentication ) { this.title.setTitle("Jobs | BoredCareers"); + this.currentFilter = new JobFilter(); }; ngOnInit(){ - this.http.get("api/joblisting?PageQuantity=" + 10 + "&Page=" + 1).subscribe({ + this.http.get("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 != 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(queryBuilder).subscribe({ next: data => { this.JobListingPage = data; }, diff --git a/src/Client/src/app/services/Validation.ts b/src/Client/src/app/services/Validation.ts index 0887ad9..ddec9c9 100644 --- a/src/Client/src/app/services/Validation.ts +++ b/src/Client/src/app/services/Validation.ts @@ -74,16 +74,31 @@ export class Validation { ///////// 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; + 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; + } + } + + ///////// 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']; } -} } \ No newline at end of file diff --git a/src/Server/Controllers/JobListingController.cs b/src/Server/Controllers/JobListingController.cs index 18e90c3..647050c 100644 --- a/src/Server/Controllers/JobListingController.cs +++ b/src/Server/Controllers/JobListingController.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Mvc; using BoredCareers.Services.DatabaseService; using BoredCareers.Entities; +using Org.BouncyCastle.Tls; namespace BoredCareers.Controllers { [ApiController] @@ -31,8 +32,25 @@ namespace BoredCareers.Controllers { } [HttpGet] - public async Task GetJobListings(int Page = 1, int PageQuantity = 25) { - JobListing[] jobListings = await _databaseService.GetJobListingPage(Page, PageQuantity); + public async Task GetJobListings(string? CC, string? PC, float? D, string? JT, bool? R, int? SMI, int? SMA, int Page = 1, int PageQuantity = 25 ) { + + // Get all relevant postal codes + List PostalCodes = new List(); + 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); } diff --git a/src/Server/Controllers/LocationController.cs b/src/Server/Controllers/LocationController.cs index e855975..90ab731 100644 --- a/src/Server/Controllers/LocationController.cs +++ b/src/Server/Controllers/LocationController.cs @@ -1,25 +1,19 @@ using Microsoft.AspNetCore.Mvc; using BoredCareers.Services.DatabaseService; using BoredCareers.Entities; -using BoredCareers.Services; namespace BoredCareers.Controllers { [ApiController] [Route("api/location")] public class LocationController : MistoxControllerBase { - public LocationController(DatabaseService db, EmailService emailContext) : base(db) {} + public LocationController(DatabaseService db) : base(db) {} [HttpGet] public async Task GetCompany(string PostalCode, string CountryCode, float MaxDistanceKm) { if (isLoggedIn()) { - - Location? MyZIP = await _databaseService.GetLocation(PostalCode, CountryCode); - if (MyZIP != null) { - Location[] places = await _databaseService.GetNearbyLocations(MyZIP.Latitude, MyZIP.Longitude, MyZIP.CountryCode, MaxDistanceKm); - return Ok(places.ToArray()); - } - return NotFound("Postal + CountryCode not found"); + Location[] places = await _databaseService.GetNearbyLocations(PostalCode, CountryCode, MaxDistanceKm); + return Ok(places.ToArray()); } return NotFound("Not logged in"); } diff --git a/src/Server/Services/DatabaseService/JobListing.cs b/src/Server/Services/DatabaseService/JobListing.cs index 8dad416..28029ca 100644 --- a/src/Server/Services/DatabaseService/JobListing.cs +++ b/src/Server/Services/DatabaseService/JobListing.cs @@ -7,21 +7,73 @@ using System.Data.Common; namespace BoredCareers.Services.DatabaseService { public partial class DatabaseService { - public async Task GetJobListingPage(int PageNumber, int CountPerPage) { + public async Task GetJobListingPage(int PageNumber, int CountPerPage, string[]? PostalCodes = null, string? CountryCode = null, string? JobType = null, bool? Remote = null, int? SalaryMin = null, int? SalaryMax = null ) { List joblistings = new List(); using (MySqlConnection connection = GetConnection()) { 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 Filters = new List(); + List Parameters = new List(); + List ParameterName = new List(); + + 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.ToString()); + ParameterName.Add("@Remote"); + } + + if (SalaryMin != null) { + Filters.Add("SalaryMin"); + Parameters.Add(SalaryMin.Value.ToString()); + ParameterName.Add("@SalaryMin"); + } + + if (SalaryMax != null) { + Filters.Add("SalaryMax"); + Parameters.Add(SalaryMax.Value.ToString()); + 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("@PageNumber", (PageNumber - 1) * CountPerPage); + for (int i = 0; i < Filters.Count; i++) { + cmd.Parameters.AddWithValue( ParameterName[i], Parameters[i] ); + } using (DbDataReader reader = await cmd.ExecuteReaderAsync()) { while (await reader.ReadAsync()) { diff --git a/src/Server/Services/DatabaseService/Location.cs b/src/Server/Services/DatabaseService/Location.cs index 467d00b..16efe83 100644 --- a/src/Server/Services/DatabaseService/Location.cs +++ b/src/Server/Services/DatabaseService/Location.cs @@ -7,64 +7,35 @@ using System.Text; namespace BoredCareers.Services.DatabaseService { public partial class DatabaseService { - public async Task GetLocation(string PostalCode, string CountryCode) { - using (MySqlConnection connection = GetConnection()) { - await connection.OpenAsync(); - string command = @" - SELECT PostalCode, CountryCode, Latitude, Longitude, City - FROM PostalCodes - WHERE PostalCode = @PostalCode - AND CountryCode = @CountryCode; - "; - - MySqlCommand cmd = new MySqlCommand(command, connection); - cmd.Parameters.AddWithValue("@PostalCode", PostalCode); - cmd.Parameters.AddWithValue("@CountryCode", CountryCode); - - 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"); - - return new Location() { - City = _city, - PostalCode = _postalCode, - CountryCode = _countryCode, - Latitude = _latitude, - Longitude = _longitude - }; - } - } - } - return null; - } - - public async Task GetNearbyLocations(float Latitude, float Longitude, string CountryCode, float MaxDistanceKm) { + public async Task GetNearbyLocations(string PostalCode, string CountryCode, float MaxDistanceKm) { List closePostalCodes = new List(); using (MySqlConnection connection = GetConnection()) { await connection.OpenAsync(); string command = @" - SELECT PostalCode, CountryCode, Latitude, Longitude, City, ( - 6371 * acos( - cos(radians(@Latitude)) * - cos(radians(Latitude)) * - cos(radians(Longitude) - radians(@Longitude)) + - sin(radians(@Latitude)) * - sin(radians(Latitude)) - ) - ) AS distance_km - FROM PostalCodes - WHERE countrycode = @CountryCode + 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("@Latitude", Latitude); - cmd.Parameters.AddWithValue("@Longitude", Longitude); + cmd.Parameters.AddWithValue("@PostalCode", PostalCode); cmd.Parameters.AddWithValue("@CountryCode", CountryCode); cmd.Parameters.AddWithValue("@MaxDistanceKm", MaxDistanceKm); -- 2.52.0 From 9052db6c3842b55e15fba690cfe12ad286026572 Mon Sep 17 00:00:00 2001 From: Derek Holloway Date: Thu, 4 Sep 2025 19:42:13 -0700 Subject: [PATCH 3/7] Impliment dark mode --- .../app/pages/company/company.component.css | 69 +++++++++++-------- .../app/pages/company/company.component.html | 26 +++---- src/Client/src/styles.css | 43 +++++++----- 3 files changed, 80 insertions(+), 58 deletions(-) diff --git a/src/Client/src/app/pages/company/company.component.css b/src/Client/src/app/pages/company/company.component.css index 8eecdb8..d7264b8 100644 --- a/src/Client/src/app/pages/company/company.component.css +++ b/src/Client/src/app/pages/company/company.component.css @@ -1,36 +1,56 @@ -button { +.primary-button { height: 45px; border-radius: 5px; margin: 10px; text-align: center; padding: 15px 20px; transition: 0.5s; - background-color: #00000000; - border: 1px solid var(--Mistox-Black); - color: var(--Mistox-Black); + background-color: var(--mistox-button-primary); + border: 1px solid var(--mistox-button-primary); + color: var(--mistox-button-text); text-decoration: none; font: inherit; } - button:hover { - background-color: #00000044; - color: var(--Mistox-Light); + .primary-button:hover { + background-color: var(--mistox-button-primary-click); + } + +.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 { - width: 100%; - height: 60px; + 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); } .content-frame { - background-color: #3c3c3c; - width: calc(100% - 40px); - height: calc(100vh - 400px); + max-width: 1800px; border-radius: 20px; - margin: 10px; - overflow: scroll; padding: 10px; - color: var(--Mistox-White); + margin: 0 auto; } .center-item { @@ -40,7 +60,7 @@ button { } .content-edit { - position: absolute; + position: relative; right: 20px; } @@ -66,22 +86,23 @@ button { .content-link a { text-decoration: none; - color: var(--Mistox-White); margin-top: auto; } .content-desc { - border: solid 1px red; + border: solid 1px var(--mistox-border); border-radius: 5px; padding: 20px; margin: 0 100px; + margin-bottom: 0px; margin-bottom: 50px; + background-color: var(--mistox-bg-medium); + color: var(--mistox-text); } .content-desc h1 { margin: 0; font-size: 20px; - color: #ddd; } .content-button { @@ -100,8 +121,6 @@ button { .half-frame { width: 50%; - border-right: solid 1px var(--Mistox-Black); - border-left: solid 1px var(--Mistox-Black); } .half-frame h2 { @@ -110,7 +129,8 @@ button { .job-tile { display: flex; - background-color: var(--Mistox-Black); + background-color: var(--mistox-bg-medium); + border: solid 1px var(--mistox-border-dark); justify-content: end; align-items: center; border-radius: 10px; @@ -126,9 +146,4 @@ button { .job-tile h1 { margin: 0; -} - -.job-tile button { - color: white; - border-color: white; } \ No newline at end of file diff --git a/src/Client/src/app/pages/company/company.component.html b/src/Client/src/app/pages/company/company.component.html index 456916f..3387cc5 100644 --- a/src/Client/src/app/pages/company/company.component.html +++ b/src/Client/src/app/pages/company/company.component.html @@ -1,13 +1,13 @@
@for(company of Employers; track company.accountID){ - + } - +
@if(Comp != null){
- +
@@ -31,11 +31,11 @@
@if (Comp.emailVerified){
- +
} @else {
- VERIFY EMAIL> + VERIFY EMAIL> You must verify your company email before you can post job listings.
} @@ -46,21 +46,21 @@

{{ listing.title }}

- - - - + + + +
}
@if (Comp.emailVerified){
- +
} @else {
- VERIFY EMAIL> + VERIFY EMAIL> You must verify your company email before you can post job listings.
} @@ -72,9 +72,9 @@

{{ listing.accountName }}

@if (listing.accountID != auth.loggedInUser.id){ - + } @else { - + }
} diff --git a/src/Client/src/styles.css b/src/Client/src/styles.css index 81dd7be..d9bb939 100644 --- a/src/Client/src/styles.css +++ b/src/Client/src/styles.css @@ -12,6 +12,8 @@ --mistox-border: oklch(0.6 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-click: oklch(0.3 0.13 264); @@ -27,28 +29,34 @@ font-family: Arial, Helvetica, sans-serif; } -dark-mode { - --mistox-bg-dark: oklch(0.1 0.065 264); - --mistox-bg-medium: oklch(0.15 0.065 264); - --mistox-bg-light: oklch(0.2 0.065 264); +@media (prefers-color-scheme: dark) { + :root { + --mistox-bg-dark: oklch(0.1 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-sub: oklch(0.76 0.1 264); + --mistox-text: oklch(0.96 0.1 264); + --mistox-text-sub: oklch(0.76 0.1 264); - --mistox-border-light: oklch(0.5 0.13 264); - --mistox-border: oklch(0.4 0.13 264); - --mistox-border-dark: oklch(0.3 0.13 264); + --mistox-border-light: oklch(0.5 0.13 264); + --mistox-border: oklch(0.4 0.13 264); + --mistox-border-dark: oklch(0.3 0.13 264); - --mistox-button-primary: oklch(0.76 0.13 264); - --mistox-button-secondary: oklch(0.76 0.13 84); + --mistox-button-text: oklch(0 0.00011 271.152); - --mistox-alert-danger: oklch(0.7 0.13 30); - --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-button-primary: oklch(0.76 0.13 264); + --mistox-button-secondary: oklch(0.76 0.13 84); - --mistox-shadow: 0px 2px 2px oklch(0 0 0 / 0.2), 0px 4px 4px oklch(0 0 0 / 0.1); - font-family: Arial, Helvetica, sans-serif; + --mistox-alert-danger: oklch(0.7 0.13 30); + --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 { @@ -56,6 +64,5 @@ html { } 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); } \ No newline at end of file -- 2.52.0 From 8c5ae6a2412434975c4351aea6c8fb38eedb8f5d Mon Sep 17 00:00:00 2001 From: Derek Holloway Date: Thu, 4 Sep 2025 19:42:27 -0700 Subject: [PATCH 4/7] Make filter a top bar --- .../src/app/pages/jobs/jobs.component.css | 28 +++++++++++++++++++ .../src/app/pages/jobs/jobs.component.html | 24 ++++++++-------- .../src/app/pages/jobs/jobs.component.ts | 4 +++ 3 files changed, 45 insertions(+), 11 deletions(-) diff --git a/src/Client/src/app/pages/jobs/jobs.component.css b/src/Client/src/app/pages/jobs/jobs.component.css index 4bbf54f..edfca32 100644 --- a/src/Client/src/app/pages/jobs/jobs.component.css +++ b/src/Client/src/app/pages/jobs/jobs.component.css @@ -14,6 +14,34 @@ button { 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 { display: block; width: 100%; diff --git a/src/Client/src/app/pages/jobs/jobs.component.html b/src/Client/src/app/pages/jobs/jobs.component.html index 60fd90c..d57768b 100644 --- a/src/Client/src/app/pages/jobs/jobs.component.html +++ b/src/Client/src/app/pages/jobs/jobs.component.html @@ -1,30 +1,32 @@ -
-
+
+

Country

-
+

Postal Code

-
-

Distance

- -
-
+ @if (currentFilter.CountryCode != null && currentFilter.CountryCode !== "" && currentFilter.PostalCode != null && currentFilter.PostalCode !== ""){ +
+

Distance

+ +
+ } +

Job Type

-
+

Remote

-
+

Minimum Salary

-
+

Maximum Salary

diff --git a/src/Client/src/app/pages/jobs/jobs.component.ts b/src/Client/src/app/pages/jobs/jobs.component.ts index 04b71d3..b677b2a 100644 --- a/src/Client/src/app/pages/jobs/jobs.component.ts +++ b/src/Client/src/app/pages/jobs/jobs.component.ts @@ -41,6 +41,10 @@ export class JobsComponent { 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; } -- 2.52.0 From d9bd4355c7e2e5ff1ee999bca2f4de52edb3ace2 Mon Sep 17 00:00:00 2001 From: Derek Holloway Date: Thu, 4 Sep 2025 20:13:55 -0700 Subject: [PATCH 5/7] Start to organize the page layout better --- .../pages/jobs/viewer/jobviewer.component.css | 4 +- .../jobs/viewer/jobviewer.component.html | 59 ++++++++++--------- 2 files changed, 33 insertions(+), 30 deletions(-) diff --git a/src/Client/src/app/pages/jobs/viewer/jobviewer.component.css b/src/Client/src/app/pages/jobs/viewer/jobviewer.component.css index 1ce3c33..3644a9c 100644 --- a/src/Client/src/app/pages/jobs/viewer/jobviewer.component.css +++ b/src/Client/src/app/pages/jobs/viewer/jobviewer.component.css @@ -1,5 +1,5 @@ .company-details { - background-color: #5c3030; + background-color: var(--mistox-bg-light); } .company-details::after { @@ -82,7 +82,7 @@ } .job-details { - background-color: #3c3c3c; + padding-bottom: 40px; } .job-timestamp { diff --git a/src/Client/src/app/pages/jobs/viewer/jobviewer.component.html b/src/Client/src/app/pages/jobs/viewer/jobviewer.component.html index b0f8daf..cba83bb 100644 --- a/src/Client/src/app/pages/jobs/viewer/jobviewer.component.html +++ b/src/Client/src/app/pages/jobs/viewer/jobviewer.component.html @@ -1,26 +1,5 @@
- @if (jobsCompany != null){ -
-
- - - -
-
- -

{{ jobsCompany.name }}

- -
-
-

{{ jobsCompany.city }}, {{ jobsCompany.stateOrRegion }} {{ jobsCompany.postalCode }}

-
-
- @for(line of jobsCompany.description.split('\n'); track line.length){ -

{{ line }}

- } -
-
- } + @if (selectedJob != null) {
@if (selectedJob.isDeleted){ @@ -32,6 +11,12 @@

{{ selectedJob.title }}

+
+

Job Description:

+
+

{{ selectedJob.description }}

+
+

Job Details:

@@ -78,12 +63,6 @@ Opened: {{ selectedJob.createdTime }} | Last Updated: {{ selectedJob.modifiedTime }}
-
-

Job Description:

-
-

{{ selectedJob.description }}

-
-
@@ -93,4 +72,28 @@
} + + @if (jobsCompany != null){ +
+
+ + + +
+
+ +

{{ jobsCompany.name }}

+ +
+
+

{{ jobsCompany.city }}, {{ jobsCompany.stateOrRegion }} {{ jobsCompany.postalCode }}

+
+
+ @for(line of jobsCompany.description.split('\n'); track line.length){ +

{{ line }}

+ } +
+
+ } +
\ No newline at end of file -- 2.52.0 From eb439e58dbc26fac4f63210b7f6370076fb2408a Mon Sep 17 00:00:00 2001 From: Derek Holloway Date: Thu, 4 Sep 2025 20:14:05 -0700 Subject: [PATCH 6/7] Fix bad filtering --- .../Controllers/JobListingController.cs | 1 - .../Services/DatabaseService/JobListing.cs | 24 +++++++++---------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/Server/Controllers/JobListingController.cs b/src/Server/Controllers/JobListingController.cs index 647050c..d4f4ddf 100644 --- a/src/Server/Controllers/JobListingController.cs +++ b/src/Server/Controllers/JobListingController.cs @@ -1,7 +1,6 @@ using Microsoft.AspNetCore.Mvc; using BoredCareers.Services.DatabaseService; using BoredCareers.Entities; -using Org.BouncyCastle.Tls; namespace BoredCareers.Controllers { [ApiController] diff --git a/src/Server/Services/DatabaseService/JobListing.cs b/src/Server/Services/DatabaseService/JobListing.cs index 28029ca..5acab3d 100644 --- a/src/Server/Services/DatabaseService/JobListing.cs +++ b/src/Server/Services/DatabaseService/JobListing.cs @@ -17,45 +17,45 @@ namespace BoredCareers.Services.DatabaseService { string limit = " LIMIT @PageSize OFFSET @PageNumber;"; List Filters = new List(); - List Parameters = new List(); + List Parameters = new List(); List ParameterName = new List(); if (PostalCodes != null) { for (int i = 0; i < PostalCodes.Length; i++) { Filters.Add("PostalCode"); Parameters.Add(PostalCodes[i]); - ParameterName.Add("@PostalCode" + i); + ParameterName.Add(" = @PostalCode" + i); } } if (CountryCode != null) { Filters.Add("Country"); Parameters.Add(CountryCode); - ParameterName.Add("@CountryCode"); + ParameterName.Add(" = @CountryCode"); } if (JobType != null) { Filters.Add("JobType"); Parameters.Add(JobType); - ParameterName.Add("@JobType"); + ParameterName.Add(" = @JobType"); } if (Remote != null) { Filters.Add("Remote"); - Parameters.Add(Remote.Value.ToString()); - ParameterName.Add("@Remote"); + Parameters.Add(Remote.Value); + ParameterName.Add(" = @Remote"); } if (SalaryMin != null) { Filters.Add("SalaryMin"); - Parameters.Add(SalaryMin.Value.ToString()); - ParameterName.Add("@SalaryMin"); + Parameters.Add(SalaryMin.Value); + ParameterName.Add(" >= @SalaryMin"); } if (SalaryMax != null) { Filters.Add("SalaryMax"); - Parameters.Add(SalaryMax.Value.ToString()); - ParameterName.Add("@SalaryMax"); + Parameters.Add(SalaryMax.Value); + ParameterName.Add(" <= @SalaryMax"); } string filter = " WHERE IsDeleted = False"; @@ -65,14 +65,14 @@ namespace BoredCareers.Services.DatabaseService { } else { filter += " AND "; } - filter += Filters[i] + " = " + ParameterName[i]; + filter += Filters[i] + ParameterName[i]; } MySqlCommand cmd = new MySqlCommand(select + filter + order + limit, connection); cmd.Parameters.AddWithValue("@PageSize", CountPerPage); cmd.Parameters.AddWithValue("@PageNumber", (PageNumber - 1) * CountPerPage); for (int i = 0; i < Filters.Count; i++) { - cmd.Parameters.AddWithValue( ParameterName[i], Parameters[i] ); + cmd.Parameters.AddWithValue( ParameterName[i].Split(' ').Last(), Parameters[i] ); } using (DbDataReader reader = await cmd.ExecuteReaderAsync()) { -- 2.52.0 From edee872881b9f9bb991fe83ee06937d86c2bfa92 Mon Sep 17 00:00:00 2001 From: Derek Holloway Date: Thu, 4 Sep 2025 20:14:22 -0700 Subject: [PATCH 7/7] Update todo --- ToDo.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ToDo.yaml b/ToDo.yaml index 80a2eb7..93ccd47 100755 --- a/ToDo.yaml +++ b/ToDo.yaml @@ -42,6 +42,8 @@ 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 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: CSS is broken -- 2.52.0