Skip to content

Commit c0f58a0

Browse files
✨ feat(job type field): crate and edit job type list (#254)
* ✨ feat(job type field): crate and edit job type list * 🐛 fix(restor auth guard): restor auth guard in auth client wrapper
1 parent 63f9431 commit c0f58a0

5 files changed

Lines changed: 226 additions & 31 deletions

File tree

messages/en.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,15 @@
260260
"loadMore": "Load more",
261261
"loading": "Loading...",
262262
"disclaimerLabel": "Disclaimer:",
263-
"disclaimerText": "This job post is community-submitted and has not been fully verified. Please visit the company's official website and verify the provided information before applying."
263+
"disclaimerText": "This job post is community-submitted and has not been fully verified. Please visit the company's official website and verify the provided information before applying.",
264+
"tagOptions": {
265+
"fullTime": "Full-time",
266+
"partTime": "Part-time",
267+
"remote": "Remote",
268+
"contract": "Contract",
269+
"freelance": "Freelance",
270+
"internship": "Internship"
271+
}
264272
},
265273
"auth": {
266274
"signIn": "Sign In",

messages/mm.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,15 @@
260260
"loadMore": "ထပ်ကြည့်ရန်",
261261
"loading": "ဖွင့်နေသည်...",
262262
"disclaimerLabel": "သတိပေးချက်:",
263-
"disclaimerText": "ဤအလုပ်ခေါ်စာသည် Community မှ တင်သွင်းထားခြင်းဖြစ်ပြီး အပြည့်အဝ စစ်ဆေးအတည်ပြုထားခြင်း မရှိသေးပါ။ လျှောက်ထားမည်ဆိုပါက ကုမ္ပဏီ၏ တရားဝင် website နှင့် ပေးထားသော အချက်အလက်များကို ကိုယ်တိုင် စစ်ဆေးအတည်ပြုပါ။"
263+
"disclaimerText": "ဤအလုပ်ခေါ်စာသည် Community မှ တင်သွင်းထားခြင်းဖြစ်ပြီး အပြည့်အဝ စစ်ဆေးအတည်ပြုထားခြင်း မရှိသေးပါ။ လျှောက်ထားမည်ဆိုပါက ကုမ္ပဏီ၏ တရားဝင် website နှင့် ပေးထားသော အချက်အလက်များကို ကိုယ်တိုင် စစ်ဆေးအတည်ပြုပါ။",
264+
"tagOptions": {
265+
"fullTime": "အချိန်ပြည့်",
266+
"partTime": "အချိန်ပိုင်း",
267+
"remote": "အဝေးမှ",
268+
"contract": "စာချုပ်",
269+
"freelance": "လွတ်လပ်",
270+
"internship": "အလုပ်သင်"
271+
}
264272
},
265273
"auth": {
266274
"signIn": "ဝင်ရောက်ရန်",

src/app/jobs/edit/JobEditClient.tsx

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { publishJobPost, unpublishJobPost } from "@/lib/firebase/firestore-jobs"
1616
import { useTranslations } from "next-intl";
1717
import { useLanguage } from "@/hooks/useLanguage";
1818
import { khitHaungg } from "@/fonts/fonts";
19+
import JobTypeSelect from "@/components/Ui/JobTypeSelect/JobTypeSelect";
1920

2021
const INPUT_CLASS = cn(
2122
"w-full px-4 py-3 rounded-xl text-sm",
@@ -223,13 +224,7 @@ function JobEditForm({ postId }: { postId: string }) {
223224
<Tag className="w-3 h-3 inline mr-1" />
224225
{t("formTag")}
225226
</label>
226-
<input
227-
type="text"
228-
value={tag}
229-
onChange={(e) => setTag(e.target.value)}
230-
placeholder={t("formTagPlaceholder")}
231-
className={INPUT_CLASS}
232-
/>
227+
<JobTypeSelect value={tag} onChange={setTag} />
233228
</div>
234229

235230
{/* Skills */}

src/app/jobs/write/JobWriteClient.tsx

Lines changed: 57 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,14 @@ import { useJobEditor } from "@/hooks/jobs/useJobEditor";
1313
import { useTranslations } from "next-intl";
1414
import { useLanguage } from "@/hooks/useLanguage";
1515
import { khitHaungg } from "@/fonts/fonts";
16+
import JobTypeSelect from "@/components/Ui/JobTypeSelect/JobTypeSelect";
1617

1718
const INPUT_CLASS = cn(
1819
"w-full px-4 py-3 rounded-xl text-sm",
1920
"bg-white/[0.04] border border-white/[0.08]",
2021
"text-zinc-200 placeholder:text-zinc-600",
2122
"focus:outline-none focus:border-prism-cyan/40 focus:ring-1 focus:ring-prism-cyan/20",
22-
"transition-all duration-200"
23+
"transition-all duration-200",
2324
);
2425

2526
function TagInput({
@@ -103,7 +104,7 @@ function JobWriteForm() {
103104
(state: SerializedEditorState) => {
104105
setDescription(state);
105106
},
106-
[setDescription]
107+
[setDescription],
107108
);
108109

109110
const handleSave = async () => {
@@ -138,7 +139,12 @@ function JobWriteForm() {
138139
<div className="w-9 h-9 rounded-lg flex items-center justify-center bg-gradient-to-br from-prism-cyan/20 to-prism-violet/20 border border-white/[0.08]">
139140
<Briefcase className="w-4.5 h-4.5 text-prism-cyan" />
140141
</div>
141-
<h1 className={cn("text-xl font-semibold font-display text-white tracking-tight", mmFont)}>
142+
<h1
143+
className={cn(
144+
"text-xl font-semibold font-display text-white tracking-tight",
145+
mmFont,
146+
)}
147+
>
142148
{t("postNewJob")}
143149
</h1>
144150
</div>
@@ -153,7 +159,12 @@ function JobWriteForm() {
153159
>
154160
{/* Position */}
155161
<div>
156-
<label className={cn("block text-xs font-mono text-zinc-500 uppercase tracking-wider mb-2", mmFont)}>
162+
<label
163+
className={cn(
164+
"block text-xs font-mono text-zinc-500 uppercase tracking-wider mb-2",
165+
mmFont,
166+
)}
167+
>
157168
{t("formPosition")}
158169
</label>
159170
<input
@@ -167,22 +178,26 @@ function JobWriteForm() {
167178

168179
{/* Tag (Job Type) */}
169180
<div>
170-
<label className={cn("block text-xs font-mono text-zinc-500 uppercase tracking-wider mb-2", mmFont)}>
181+
<label
182+
className={cn(
183+
"block text-xs font-mono text-zinc-500 uppercase tracking-wider mb-2",
184+
mmFont,
185+
)}
186+
>
171187
<Tag className="w-3 h-3 inline mr-1" />
172188
{t("formTag")}
173189
</label>
174-
<input
175-
type="text"
176-
value={tag}
177-
onChange={(e) => setTag(e.target.value)}
178-
placeholder={t("formTagPlaceholder")}
179-
className={INPUT_CLASS}
180-
/>
190+
<JobTypeSelect value={tag} onChange={setTag} />
181191
</div>
182192

183193
{/* Skills */}
184194
<div>
185-
<label className={cn("block text-xs font-mono text-zinc-500 uppercase tracking-wider mb-2", mmFont)}>
195+
<label
196+
className={cn(
197+
"block text-xs font-mono text-zinc-500 uppercase tracking-wider mb-2",
198+
mmFont,
199+
)}
200+
>
186201
{t("formSkills")}
187202
</label>
188203
<TagInput
@@ -195,7 +210,12 @@ function JobWriteForm() {
195210

196211
{/* Office Email */}
197212
<div>
198-
<label className={cn("block text-xs font-mono text-zinc-500 uppercase tracking-wider mb-2", mmFont)}>
213+
<label
214+
className={cn(
215+
"block text-xs font-mono text-zinc-500 uppercase tracking-wider mb-2",
216+
mmFont,
217+
)}
218+
>
199219
<Mail className="w-3 h-3 inline mr-1" />
200220
{t("formEmail")}
201221
</label>
@@ -210,7 +230,12 @@ function JobWriteForm() {
210230

211231
{/* Expiration Date */}
212232
<div>
213-
<label className={cn("block text-xs font-mono text-zinc-500 uppercase tracking-wider mb-2", mmFont)}>
233+
<label
234+
className={cn(
235+
"block text-xs font-mono text-zinc-500 uppercase tracking-wider mb-2",
236+
mmFont,
237+
)}
238+
>
214239
<CalendarClock className="w-3 h-3 inline mr-1" />
215240
{t("formExpiredAt")}
216241
</label>
@@ -219,14 +244,24 @@ function JobWriteForm() {
219244
onChange={setExpiredAt}
220245
placeholder={t("formExpiredAtPlaceholder")}
221246
/>
222-
<p className={cn("text-[11px] text-zinc-600 mt-1.5 font-mono", mmFont)}>
247+
<p
248+
className={cn(
249+
"text-[11px] text-zinc-600 mt-1.5 font-mono",
250+
mmFont,
251+
)}
252+
>
223253
{t("formExpiredAtHint")}
224254
</p>
225255
</div>
226256

227257
{/* Description (Rich Text) */}
228258
<div>
229-
<label className={cn("block text-xs font-mono text-zinc-500 uppercase tracking-wider mb-2", mmFont)}>
259+
<label
260+
className={cn(
261+
"block text-xs font-mono text-zinc-500 uppercase tracking-wider mb-2",
262+
mmFont,
263+
)}
264+
>
230265
{t("formDescription")}
231266
</label>
232267
<ContentEditor
@@ -237,9 +272,7 @@ function JobWriteForm() {
237272
</div>
238273

239274
{/* Error */}
240-
{error && (
241-
<p className="text-sm text-prism-rose">{error}</p>
242-
)}
275+
{error && <p className="text-sm text-prism-rose">{error}</p>}
243276

244277
{/* Actions */}
245278
<div className="flex items-center gap-3 pt-4">
@@ -251,11 +284,13 @@ function JobWriteForm() {
251284
"flex items-center gap-2 px-6 py-3 rounded-xl text-sm font-medium",
252285
"bg-prism-cyan text-white",
253286
"hover:bg-prism-cyan/90 transition-colors duration-200",
254-
"disabled:opacity-40 disabled:cursor-not-allowed"
287+
"disabled:opacity-40 disabled:cursor-not-allowed",
255288
)}
256289
>
257290
<Save className="w-4 h-4" />
258-
<span className={mmFont}>{saving ? t("saving") : t("saveDraft")}</span>
291+
<span className={mmFont}>
292+
{saving ? t("saving") : t("saveDraft")}
293+
</span>
259294
</button>
260295
</div>
261296
</motion.div>
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
"use client";
2+
3+
import { useState, useRef, useEffect } from "react";
4+
import { cn } from "@/utils";
5+
import { ChevronDown, X } from "lucide-react";
6+
import { motion, AnimatePresence } from "motion/react";
7+
import { useTranslations } from "next-intl";
8+
9+
const JOB_TYPE_KEYS = [
10+
"fullTime",
11+
"partTime",
12+
"remote",
13+
"contract",
14+
"freelance",
15+
"internship",
16+
] as const;
17+
18+
interface JobTypeSelectProps {
19+
value: string;
20+
onChange: (value: string) => void;
21+
placeholder?: string;
22+
className?: string;
23+
}
24+
25+
export default function JobTypeSelect({
26+
value,
27+
onChange,
28+
placeholder,
29+
className,
30+
}: JobTypeSelectProps) {
31+
const t = useTranslations("jobs");
32+
const [open, setOpen] = useState(false);
33+
const containerRef = useRef<HTMLDivElement>(null);
34+
35+
const options = JOB_TYPE_KEYS.map((key) => ({
36+
key,
37+
label: t(`tagOptions.${key}`),
38+
}));
39+
40+
const filteredOptions = options.filter((opt) =>
41+
opt.label.toLowerCase().includes(value.toLowerCase()),
42+
);
43+
44+
// Close dropdown when clicking outside
45+
useEffect(() => {
46+
const handleClickOutside = (e: MouseEvent) => {
47+
if (
48+
containerRef.current &&
49+
!containerRef.current.contains(e.target as Node)
50+
) {
51+
setOpen(false);
52+
}
53+
};
54+
document.addEventListener("mousedown", handleClickOutside);
55+
return () => document.removeEventListener("mousedown", handleClickOutside);
56+
}, []);
57+
58+
const handleSelect = (label: string) => {
59+
onChange(label);
60+
setOpen(false);
61+
};
62+
63+
const handleInputChange = (newValue: string) => {
64+
onChange(newValue);
65+
setOpen(true);
66+
};
67+
68+
const handleClear = (e: React.MouseEvent) => {
69+
e.stopPropagation();
70+
onChange("");
71+
setOpen(false);
72+
};
73+
74+
return (
75+
<div ref={containerRef} className={cn("relative w-full", className)}>
76+
<div
77+
className={cn(
78+
"w-full flex items-center gap-2 px-4 py-3 rounded-xl text-sm",
79+
"bg-white/[0.04] border border-white/[0.08]",
80+
"hover:border-white/[0.12] transition-all duration-200",
81+
"focus-within:outline-none focus-within:border-prism-cyan/40 focus-within:ring-1 focus-within:ring-prism-cyan/20",
82+
open && "border-prism-cyan/40 ring-1 ring-prism-cyan/20",
83+
)}
84+
>
85+
<input
86+
type="text"
87+
value={value}
88+
onChange={(e) => handleInputChange(e.target.value)}
89+
onFocus={() => setOpen(true)}
90+
placeholder={placeholder || t("formTagPlaceholder")}
91+
className="flex-1 bg-transparent text-zinc-200 placeholder:text-zinc-600 focus:outline-none"
92+
/>
93+
{value && (
94+
<button
95+
type="button"
96+
onClick={handleClear}
97+
className="p-0.5 rounded-md hover:bg-white/[0.08] text-zinc-500 hover:text-zinc-300 transition-colors"
98+
>
99+
<X className="w-3.5 h-3.5" />
100+
</button>
101+
)}
102+
<ChevronDown
103+
className={cn(
104+
"w-4 h-4 text-zinc-600 transition-transform duration-200 shrink-0",
105+
open && "transform rotate-180",
106+
)}
107+
/>
108+
</div>
109+
110+
<AnimatePresence>
111+
{open && filteredOptions.length > 0 && (
112+
<motion.div
113+
initial={{ opacity: 0, y: 6, scale: 0.97 }}
114+
animate={{ opacity: 1, y: 0, scale: 1 }}
115+
exit={{ opacity: 0, y: 6, scale: 0.97 }}
116+
transition={{ duration: 0.15, ease: [0.22, 1, 0.36, 1] }}
117+
className={cn(
118+
"absolute top-full left-0 right-0 z-50 mt-2",
119+
"rounded-2xl overflow-hidden",
120+
"bg-surface/95 backdrop-blur-2xl",
121+
"border border-white/[0.08]",
122+
"shadow-[0_16px_48px_rgba(0,0,0,0.5),0_0_1px_rgba(255,255,255,0.05)]",
123+
"py-2",
124+
)}
125+
>
126+
<div className="h-[1px] w-full bg-gradient-to-r from-transparent via-prism-cyan/30 to-transparent mb-2" />
127+
<div className="max-h-64 overflow-y-auto px-1">
128+
{filteredOptions.map((option) => (
129+
<button
130+
key={option.key}
131+
type="button"
132+
onClick={() => handleSelect(option.label)}
133+
className={cn(
134+
"w-full px-3 py-2.5 text-sm text-left transition-colors duration-150 rounded-lg",
135+
value === option.label
136+
? "bg-prism-cyan/20 text-prism-cyan font-medium"
137+
: "text-zinc-300 hover:bg-white/[0.06] hover:text-zinc-100",
138+
)}
139+
>
140+
{option.label}
141+
</button>
142+
))}
143+
</div>
144+
</motion.div>
145+
)}
146+
</AnimatePresence>
147+
</div>
148+
);
149+
}

0 commit comments

Comments
 (0)