Skip to content

Commit 80a3c36

Browse files
authored
Some minor UI improvements (#287)
- Add missing keyboard arrow selection - Fix embed hover border highlighting - Prevent Osano button scroll behavior - Add validation error when upstream commitment value is greater than open source - Add validation error when salary max range is less than salary min - Add loading spinner to search members/projects - Fix stats legend on jobs daily chart Signed-off-by: Cintia Sánchez García <[email protected]>
1 parent 909f38a commit 80a3c36

File tree

11 files changed

+244
-18
lines changed

11 files changed

+244
-18
lines changed

gitjobs-server/static/css/styles.src.css

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,15 @@ select.aligned-right option {
282282
/* End select styles */
283283

284284

285+
/* Input styles */
286+
287+
input[type="range"]:focus-visible {
288+
outline: 0;
289+
}
290+
291+
/* End input styles */
292+
293+
285294
/* Icons */
286295

287296
.svg-icon {
@@ -776,19 +785,9 @@ select.aligned-right option {
776785
}
777786

778787
.osano-cm-widget {
779-
height: 30px !important;
780-
width: 30px !important;
781-
right: 30px !important;
782-
bottom: 30px !important;
783-
position: absolute !important;
784788
display: none;
785789
}
786790

787-
/* Osano widget is only visible on pages with footer */
788-
body:has(footer) .osano-cm-widget {
789-
display: block;
790-
}
791-
792791
.osano-cm-widget svg {
793792
width: 30px !important;
794793
height: 30px !important;
Lines changed: 14 additions & 0 deletions
Loading

gitjobs-server/static/js/common/multiselect.js

Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { html } from "/static/vendor/js/lit-all.v3.2.1.min.js";
1+
import { html, createRef, ref } from "/static/vendor/js/lit-all.v3.2.1.min.js";
22
import { unnormalize } from "/static/js/common/common.js";
33
import { LitWrapper } from "/static/js/common/lit-wrapper.js";
44
import { getBenefits, getSkills } from "/static/js/common/data.js";
@@ -19,6 +19,7 @@ export class MultiSelect extends LitWrapper {
1919
* @property {string[]} visibleOptions - Filtered options based on search
2020
* @property {boolean} visibleDropdown - Dropdown visibility state
2121
* @property {string} legend - Helper text below input
22+
* @property {number|null} activeIndex - Index of currently highlighted option for keyboard navigation
2223
*/
2324
static properties = {
2425
name: { type: String },
@@ -29,8 +30,12 @@ export class MultiSelect extends LitWrapper {
2930
visibleOptions: { type: Array },
3031
visibleDropdown: { type: Boolean },
3132
legend: { type: String },
33+
activeIndex: { type: Number | null },
3234
};
3335

36+
/** @type {import('lit').Ref<HTMLInputElement>} Reference to input element */
37+
inputRef = createRef();
38+
3439
constructor() {
3540
super();
3641
this.name = "name";
@@ -41,6 +46,7 @@ export class MultiSelect extends LitWrapper {
4146
this.visibleOptions = [];
4247
this.visibleDropdown = false;
4348
this.legend = undefined;
49+
this.activeIndex = null;
4450
}
4551

4652
connectedCallback() {
@@ -66,6 +72,7 @@ export class MultiSelect extends LitWrapper {
6672
} else {
6773
this.visibleOptions = this.options;
6874
}
75+
this.activeIndex = null;
6976
}
7077

7178
/**
@@ -95,6 +102,7 @@ export class MultiSelect extends LitWrapper {
95102
_handleClickOutside = (event) => {
96103
if (!this.contains(event.target)) {
97104
this.visibleDropdown = false;
105+
this.activeIndex = null;
98106
}
99107
};
100108

@@ -117,6 +125,58 @@ export class MultiSelect extends LitWrapper {
117125
this.selected = this.selected.filter((selectedOption) => selectedOption !== option);
118126
}
119127

128+
/**
129+
* Highlights suggestion item for keyboard navigation.
130+
* @param {'up'|'down'} direction - Navigation direction
131+
* @private
132+
*/
133+
_highlightItem(direction) {
134+
if (this.visibleOptions && this.visibleOptions.length > 0) {
135+
if (this.activeIndex === null) {
136+
this.activeIndex = direction === "down" ? 0 : this.visibleOptions.length - 1;
137+
} else {
138+
let newIndex = direction === "down" ? this.activeIndex + 1 : this.activeIndex - 1;
139+
if (newIndex >= this.visibleOptions.length) {
140+
newIndex = 0;
141+
}
142+
if (newIndex < 0) {
143+
newIndex = this.visibleOptions.length - 1;
144+
}
145+
this.activeIndex = newIndex;
146+
}
147+
}
148+
}
149+
150+
/**
151+
* Handles keyboard navigation and selection.
152+
* @param {KeyboardEvent} event - Keyboard event
153+
* @private
154+
*/
155+
_handleKeyDown(event) {
156+
switch (event.key) {
157+
// Highlight the next item in the list
158+
case "ArrowDown":
159+
this._highlightItem("down");
160+
break;
161+
// Highlight the previous item in the list
162+
case "ArrowUp":
163+
this._highlightItem("up");
164+
break;
165+
// Select the highlighted item
166+
case "Enter":
167+
event.preventDefault();
168+
if (this.activeIndex !== null && this.visibleOptions) {
169+
const activeItem = this.visibleOptions[this.activeIndex];
170+
if (activeItem) {
171+
this._onClickOption(activeItem);
172+
}
173+
}
174+
break;
175+
default:
176+
break;
177+
}
178+
}
179+
120180
/**
121181
* Adds an option to selected list.
122182
* @param {string} option - Option to add, or uses entered value if empty
@@ -126,7 +186,12 @@ export class MultiSelect extends LitWrapper {
126186
this.selected.push(option || this.enteredValue);
127187
this.enteredValue = "";
128188
this.visibleDropdown = false;
189+
this.activeIndex = null;
129190
this._filterOptions();
191+
const input = this.inputRef.value;
192+
if (input) {
193+
input.blur(); // Remove focus from input after selection
194+
}
130195
}
131196

132197
render() {
@@ -157,7 +222,9 @@ export class MultiSelect extends LitWrapper {
157222
</span> `,
158223
)}
159224
<input
225+
${ref(this.inputRef)}
160226
type="text"
227+
@keydown="${this._handleKeyDown}"
161228
@input=${this._onInputChange}
162229
@focus=${() => (this.visibleDropdown = true)}
163230
.value="${this.enteredValue}"
@@ -179,14 +246,20 @@ export class MultiSelect extends LitWrapper {
179246
}`}
180247
>
181248
<ul class="text-sm text-stone-700 overflow-x-auto max-h-[150px]">
182-
${this.visibleOptions.map((option) => {
249+
${this.visibleOptions.map((option, index) => {
183250
const isSelected = this.selected.includes(option);
184-
return html`<li>
251+
return html`<li
252+
class="group ${this.activeIndex === index ? "active" : ""}"
253+
data-index="${index}"
254+
>
185255
<button
186256
@click=${() => this._onClickOption(option)}
257+
@mouseover=${() => (this.activeIndex = index)}
187258
type="button"
188259
class=${`${
189-
isSelected ? "bg-stone-100 opacity-50" : "cursor-pointer hover:bg-stone-100"
260+
isSelected
261+
? "bg-stone-100 opacity-50"
262+
: "cursor-pointer hover:bg-stone-100 group-[.active]:bg-stone-100"
190263
} capitalize block w-full text-left px-4 py-2`}
191264
?disabled="${isSelected}"
192265
>

gitjobs-server/static/js/dashboard/common/dashboard-search.js

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export class DashboardSearch extends LitWrapper {
1818
* @property {boolean} visibleDropdown - Dropdown visibility state
1919
* @property {number|null} activeIndex - Active suggestion index
2020
* @property {string} selectedFoundation - Selected foundation filter
21+
* @property {boolean} isLoading - Loading state for suggestions
2122
*/
2223
static properties = {
2324
type: { type: String },
@@ -28,6 +29,7 @@ export class DashboardSearch extends LitWrapper {
2829
visibleDropdown: { type: Boolean },
2930
activeIndex: { type: Number | null },
3031
selectedFoundation: { type: String },
32+
isLoading: { type: Boolean },
3133
};
3234

3335
/** @type {string} Default foundation when none selected */
@@ -44,6 +46,7 @@ export class DashboardSearch extends LitWrapper {
4446
this.visibleDropdown = false;
4547
this.activeIndex = null;
4648
this.selectedFoundation = this.defaultFoundation;
49+
this.isLoading = false;
4750
}
4851

4952
connectedCallback() {
@@ -60,7 +63,7 @@ export class DashboardSearch extends LitWrapper {
6063
* Fetches projects or members from server based on search criteria.
6164
* @private
6265
*/
63-
async _getProjects() {
66+
async _getItems() {
6467
const url = `${this.type === "members" ? "/dashboard/members/search?member=" : "/projects/search?project="}${encodeURIComponent(this.enteredValue)}&foundation=${this.selectedFoundation}`;
6568
try {
6669
const response = await fetch(url);
@@ -74,6 +77,7 @@ export class DashboardSearch extends LitWrapper {
7477
// TODO: Implement error handling
7578
} finally {
7679
this.visibleDropdown = true;
80+
this.isLoading = false;
7781
}
7882
}
7983

@@ -101,7 +105,8 @@ export class DashboardSearch extends LitWrapper {
101105
*/
102106
_filterOptions() {
103107
if (this.enteredValue.length > 2) {
104-
debounce(this._getProjects(this.enteredValue), 300);
108+
this.isLoading = true;
109+
debounce(this._getItems(this.enteredValue), 300);
105110
} else {
106111
this.visibleOptions = [];
107112
this.visibleDropdown = false;
@@ -265,6 +270,29 @@ export class DashboardSearch extends LitWrapper {
265270
<div class="svg-icon size-5 bg-stone-400 hover:bg-stone-700 icon-close"></div>
266271
</button>
267272
</div>
273+
${this.isLoading
274+
? html`<div class="absolute end-7 top-1">
275+
<div role="status">
276+
<svg
277+
aria-hidden="true"
278+
class="inline size-5 text-stone-200 animate-spin fill-primary-600"
279+
viewBox="0 0 100 101"
280+
fill="none"
281+
xmlns="http://www.w3.org/2000/svg"
282+
>
283+
<path
284+
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
285+
fill="currentColor"
286+
/>
287+
<path
288+
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
289+
fill="currentFill"
290+
/>
291+
</svg>
292+
<span class="sr-only">Loading...</span>
293+
</div>
294+
</div>`
295+
: ""}
268296
<div class="absolute z-10 start-0 end-0">
269297
<div
270298
class="${!this.visibleDropdown

gitjobs-server/static/js/dashboard/employer/jobs.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,30 @@ export const checkSalaryBeforeSubmit = () => {
4040
salaryField.removeAttribute("required");
4141
salaryMinField.removeAttribute("required");
4242
salaryMaxField.removeAttribute("required");
43+
salaryMaxField.setCustomValidity(""); // Clear any previous error
4344

4445
if (selectedSalaryType.id === "range") {
4546
// Range salary: clear exact value, set requirements for range fields
4647
salaryField.value = "";
4748

4849
if (salaryMinField.value !== "" || salaryMaxField.value !== "") {
50+
// If min and max are set, validate that max is not less than min
51+
if (
52+
salaryMaxField.value &&
53+
salaryMinField.value &&
54+
parseInt(salaryMaxField.value) < parseInt(salaryMinField.value)
55+
) {
56+
salaryMaxField.setCustomValidity("Maximum salary cannot be less than minimum salary.");
57+
58+
// Clear error when user interacts with fields
59+
salaryMaxField.addEventListener("input", () => {
60+
salaryMaxField.setCustomValidity(""); // Clear error on input
61+
});
62+
salaryMinField.addEventListener("input", () => {
63+
salaryMaxField.setCustomValidity(""); // Clear error on input
64+
});
65+
}
66+
4967
salaryMinField.setAttribute("required", "required");
5068
salaryMaxField.setAttribute("required", "required");
5169
salaryPeriodField.setAttribute("required", "required");
@@ -62,6 +80,33 @@ export const checkSalaryBeforeSubmit = () => {
6280
salaryCurrencyField.setAttribute("required", "required");
6381
}
6482
}
83+
84+
const jobsForm = document.getElementById("jobs-form");
85+
jobsForm.reportValidity(); // Trigger validation on the form
86+
};
87+
88+
/**
89+
* Validates open source and upstream commitment values.
90+
* Ensures that upstream commitment is not greater than open source value.
91+
*/
92+
export const checkOpenSourceValues = () => {
93+
const openSource = document.querySelector('input[name="open_source"]');
94+
const upstreamCommitment = document.querySelector('input[name="upstream_commitment"]');
95+
96+
// Ensure both fields are present before proceeding
97+
if (!openSource || !upstreamCommitment) {
98+
return;
99+
}
100+
101+
// Clear any previous custom validity messages
102+
upstreamCommitment.setCustomValidity("");
103+
104+
if (openSource.value && upstreamCommitment.value) {
105+
// If both fields are filled, validate that upstream commitment is not greater than open source
106+
if (parseInt(upstreamCommitment.value) > parseInt(openSource.value)) {
107+
upstreamCommitment.setCustomValidity("Upstream commitment cannot be greater than open source value.");
108+
}
109+
}
65110
};
66111

67112
/**

gitjobs-server/static/js/jobboard/stats.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -567,7 +567,7 @@ const renderBarDailyChart = (data, max, min) => {
567567
...getBarStatsOptions().tooltip,
568568
formatter: (params) => {
569569
const chartdate = echarts.time.format(params.data[0], "{dd} {MMM}'{yy}");
570-
return `${chartdate}<br />Jobs: ${prettifyNumber(params.data[1])}`;
570+
return `${chartdate}<br />Views: ${prettifyNumber(params.data[1])}`;
571571
},
572572
},
573573
xAxis: {

gitjobs-server/templates/base.html

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,28 @@
66
<meta name="description"
77
content="GitJobs is an open source job board focused on open source job opportunities.">
88
<meta name="keywords" content="community, organization, jobs, job">
9+
10+
{#- OG tags #}
911
<meta property="og:type" content="website">
1012
<meta property="og:title" content="GitJobs">
13+
<meta property="og:url" content="https://gitjobs.dev/">
1114
<meta property="og:description"
1215
content="GitJobs is an open source job board focused on open source job opportunities.">
1316
<meta property="og:image"
1417
content="https://gitjobs.dev/static/images/index/gitjobs.png">
18+
{#- End OG tags #}
19+
20+
{#- Twitter tags #}
21+
<meta name="twitter:card" content="summary_large_image">
22+
<meta property="twitter:domain" content="gitjobs.dev">
23+
<meta property="twitter:url" content="https://gitjobs.dev">
24+
<meta name="twitter:title" content="GitJobs">
25+
<meta name="twitter:description"
26+
content="GitJobs is an open source job board focused on open source job opportunities.">
27+
<meta name="twitter:image"
28+
content="https://gitjobs.dev/static/images/index/gitjobs.png">
29+
{#- End Twitter tags #}
30+
1531
<link rel="icon"
1632
href="https://gitjobs.dev/static/images/index/favicon.ico"
1733
sizes="any">

0 commit comments

Comments
 (0)