Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,15 @@
"loadMore": "Load more",
"loading": "Loading...",
"disclaimerLabel": "Disclaimer:",
"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."
"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.",
"tagOptions": {
"fullTime": "Full-time",
"partTime": "Part-time",
"remote": "Remote",
"contract": "Contract",
"freelance": "Freelance",
"internship": "Internship"
}
},
"auth": {
"signIn": "Sign In",
Expand Down
10 changes: 9 additions & 1 deletion messages/mm.json
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,15 @@
"loadMore": "ထပ်ကြည့်ရန်",
"loading": "ဖွင့်နေသည်...",
"disclaimerLabel": "သတိပေးချက်:",
"disclaimerText": "ဤအလုပ်ခေါ်စာသည် Community မှ တင်သွင်းထားခြင်းဖြစ်ပြီး အပြည့်အဝ စစ်ဆေးအတည်ပြုထားခြင်း မရှိသေးပါ။ လျှောက်ထားမည်ဆိုပါက ကုမ္ပဏီ၏ တရားဝင် website နှင့် ပေးထားသော အချက်အလက်များကို ကိုယ်တိုင် စစ်ဆေးအတည်ပြုပါ။"
"disclaimerText": "ဤအလုပ်ခေါ်စာသည် Community မှ တင်သွင်းထားခြင်းဖြစ်ပြီး အပြည့်အဝ စစ်ဆေးအတည်ပြုထားခြင်း မရှိသေးပါ။ လျှောက်ထားမည်ဆိုပါက ကုမ္ပဏီ၏ တရားဝင် website နှင့် ပေးထားသော အချက်အလက်များကို ကိုယ်တိုင် စစ်ဆေးအတည်ပြုပါ။",
"tagOptions": {
"fullTime": "အချိန်ပြည့်",
"partTime": "အချိန်ပိုင်း",
"remote": "အဝေးမှ",
"contract": "စာချုပ်",
"freelance": "လွတ်လပ်",
"internship": "အလုပ်သင်"
}
},
"auth": {
"signIn": "ဝင်ရောက်ရန်",
Expand Down
9 changes: 2 additions & 7 deletions src/app/jobs/edit/JobEditClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { publishJobPost, unpublishJobPost } from "@/lib/firebase/firestore-jobs"
import { useTranslations } from "next-intl";
import { useLanguage } from "@/hooks/useLanguage";
import { khitHaungg } from "@/fonts/fonts";
import JobTypeSelect from "@/components/Ui/JobTypeSelect/JobTypeSelect";

const INPUT_CLASS = cn(
"w-full px-4 py-3 rounded-xl text-sm",
Expand Down Expand Up @@ -223,13 +224,7 @@ function JobEditForm({ postId }: { postId: string }) {
<Tag className="w-3 h-3 inline mr-1" />
{t("formTag")}
</label>
<input
type="text"
value={tag}
onChange={(e) => setTag(e.target.value)}
placeholder={t("formTagPlaceholder")}
className={INPUT_CLASS}
/>
<JobTypeSelect value={tag} onChange={setTag} />
</div>

{/* Skills */}
Expand Down
79 changes: 57 additions & 22 deletions src/app/jobs/write/JobWriteClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@ import { useJobEditor } from "@/hooks/jobs/useJobEditor";
import { useTranslations } from "next-intl";
import { useLanguage } from "@/hooks/useLanguage";
import { khitHaungg } from "@/fonts/fonts";
import JobTypeSelect from "@/components/Ui/JobTypeSelect/JobTypeSelect";

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

function TagInput({
Expand Down Expand Up @@ -103,7 +104,7 @@ function JobWriteForm() {
(state: SerializedEditorState) => {
setDescription(state);
},
[setDescription]
[setDescription],
);

const handleSave = async () => {
Expand Down Expand Up @@ -138,7 +139,12 @@ function JobWriteForm() {
<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]">
<Briefcase className="w-4.5 h-4.5 text-prism-cyan" />
</div>
<h1 className={cn("text-xl font-semibold font-display text-white tracking-tight", mmFont)}>
<h1
className={cn(
"text-xl font-semibold font-display text-white tracking-tight",
mmFont,
)}
>
{t("postNewJob")}
</h1>
</div>
Expand All @@ -153,7 +159,12 @@ function JobWriteForm() {
>
{/* Position */}
<div>
<label className={cn("block text-xs font-mono text-zinc-500 uppercase tracking-wider mb-2", mmFont)}>
<label
className={cn(
"block text-xs font-mono text-zinc-500 uppercase tracking-wider mb-2",
mmFont,
)}
>
{t("formPosition")}
</label>
<input
Expand All @@ -167,22 +178,26 @@ function JobWriteForm() {

{/* Tag (Job Type) */}
<div>
<label className={cn("block text-xs font-mono text-zinc-500 uppercase tracking-wider mb-2", mmFont)}>
<label
className={cn(
"block text-xs font-mono text-zinc-500 uppercase tracking-wider mb-2",
mmFont,
)}
>
<Tag className="w-3 h-3 inline mr-1" />
{t("formTag")}
</label>
<input
type="text"
value={tag}
onChange={(e) => setTag(e.target.value)}
placeholder={t("formTagPlaceholder")}
className={INPUT_CLASS}
/>
<JobTypeSelect value={tag} onChange={setTag} />
</div>

{/* Skills */}
<div>
<label className={cn("block text-xs font-mono text-zinc-500 uppercase tracking-wider mb-2", mmFont)}>
<label
className={cn(
"block text-xs font-mono text-zinc-500 uppercase tracking-wider mb-2",
mmFont,
)}
>
{t("formSkills")}
</label>
<TagInput
Expand All @@ -195,7 +210,12 @@ function JobWriteForm() {

{/* Office Email */}
<div>
<label className={cn("block text-xs font-mono text-zinc-500 uppercase tracking-wider mb-2", mmFont)}>
<label
className={cn(
"block text-xs font-mono text-zinc-500 uppercase tracking-wider mb-2",
mmFont,
)}
>
<Mail className="w-3 h-3 inline mr-1" />
{t("formEmail")}
</label>
Expand All @@ -210,7 +230,12 @@ function JobWriteForm() {

{/* Expiration Date */}
<div>
<label className={cn("block text-xs font-mono text-zinc-500 uppercase tracking-wider mb-2", mmFont)}>
<label
className={cn(
"block text-xs font-mono text-zinc-500 uppercase tracking-wider mb-2",
mmFont,
)}
>
<CalendarClock className="w-3 h-3 inline mr-1" />
{t("formExpiredAt")}
</label>
Expand All @@ -219,14 +244,24 @@ function JobWriteForm() {
onChange={setExpiredAt}
placeholder={t("formExpiredAtPlaceholder")}
/>
<p className={cn("text-[11px] text-zinc-600 mt-1.5 font-mono", mmFont)}>
<p
className={cn(
"text-[11px] text-zinc-600 mt-1.5 font-mono",
mmFont,
)}
>
{t("formExpiredAtHint")}
</p>
</div>

{/* Description (Rich Text) */}
<div>
<label className={cn("block text-xs font-mono text-zinc-500 uppercase tracking-wider mb-2", mmFont)}>
<label
className={cn(
"block text-xs font-mono text-zinc-500 uppercase tracking-wider mb-2",
mmFont,
)}
>
{t("formDescription")}
</label>
<ContentEditor
Expand All @@ -237,9 +272,7 @@ function JobWriteForm() {
</div>

{/* Error */}
{error && (
<p className="text-sm text-prism-rose">{error}</p>
)}
{error && <p className="text-sm text-prism-rose">{error}</p>}

{/* Actions */}
<div className="flex items-center gap-3 pt-4">
Expand All @@ -251,11 +284,13 @@ function JobWriteForm() {
"flex items-center gap-2 px-6 py-3 rounded-xl text-sm font-medium",
"bg-prism-cyan text-white",
"hover:bg-prism-cyan/90 transition-colors duration-200",
"disabled:opacity-40 disabled:cursor-not-allowed"
"disabled:opacity-40 disabled:cursor-not-allowed",
)}
>
<Save className="w-4 h-4" />
<span className={mmFont}>{saving ? t("saving") : t("saveDraft")}</span>
<span className={mmFont}>
{saving ? t("saving") : t("saveDraft")}
</span>
</button>
</div>
</motion.div>
Expand Down
149 changes: 149 additions & 0 deletions src/components/Ui/JobTypeSelect/JobTypeSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
"use client";

import { useState, useRef, useEffect } from "react";
import { cn } from "@/utils";
import { ChevronDown, X } from "lucide-react";
import { motion, AnimatePresence } from "motion/react";
import { useTranslations } from "next-intl";

const JOB_TYPE_KEYS = [
"fullTime",
"partTime",
"remote",
"contract",
"freelance",
"internship",
] as const;

interface JobTypeSelectProps {
value: string;
onChange: (value: string) => void;
placeholder?: string;
className?: string;
}

export default function JobTypeSelect({
value,
onChange,
placeholder,
className,
}: JobTypeSelectProps) {
const t = useTranslations("jobs");
const [open, setOpen] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);

const options = JOB_TYPE_KEYS.map((key) => ({
key,
label: t(`tagOptions.${key}`),
}));

const filteredOptions = options.filter((opt) =>
opt.label.toLowerCase().includes(value.toLowerCase()),
);

// Close dropdown when clicking outside
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (
containerRef.current &&
!containerRef.current.contains(e.target as Node)
) {
setOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);

const handleSelect = (label: string) => {
onChange(label);
setOpen(false);
};

const handleInputChange = (newValue: string) => {
onChange(newValue);
setOpen(true);
};

const handleClear = (e: React.MouseEvent) => {
e.stopPropagation();
onChange("");
setOpen(false);
};

return (
<div ref={containerRef} className={cn("relative w-full", className)}>
<div
className={cn(
"w-full flex items-center gap-2 px-4 py-3 rounded-xl text-sm",
"bg-white/[0.04] border border-white/[0.08]",
"hover:border-white/[0.12] transition-all duration-200",
"focus-within:outline-none focus-within:border-prism-cyan/40 focus-within:ring-1 focus-within:ring-prism-cyan/20",
open && "border-prism-cyan/40 ring-1 ring-prism-cyan/20",
)}
>
<input
type="text"
value={value}
onChange={(e) => handleInputChange(e.target.value)}
onFocus={() => setOpen(true)}
placeholder={placeholder || t("formTagPlaceholder")}
className="flex-1 bg-transparent text-zinc-200 placeholder:text-zinc-600 focus:outline-none"
/>
{value && (
<button
type="button"
onClick={handleClear}
className="p-0.5 rounded-md hover:bg-white/[0.08] text-zinc-500 hover:text-zinc-300 transition-colors"
>
<X className="w-3.5 h-3.5" />
</button>
)}
<ChevronDown
className={cn(
"w-4 h-4 text-zinc-600 transition-transform duration-200 shrink-0",
open && "transform rotate-180",
)}
/>
</div>

<AnimatePresence>
{open && filteredOptions.length > 0 && (
<motion.div
initial={{ opacity: 0, y: 6, scale: 0.97 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 6, scale: 0.97 }}
transition={{ duration: 0.15, ease: [0.22, 1, 0.36, 1] }}
className={cn(
"absolute top-full left-0 right-0 z-50 mt-2",
"rounded-2xl overflow-hidden",
"bg-surface/95 backdrop-blur-2xl",
"border border-white/[0.08]",
"shadow-[0_16px_48px_rgba(0,0,0,0.5),0_0_1px_rgba(255,255,255,0.05)]",
"py-2",
)}
>
<div className="h-[1px] w-full bg-gradient-to-r from-transparent via-prism-cyan/30 to-transparent mb-2" />
<div className="max-h-64 overflow-y-auto px-1">
{filteredOptions.map((option) => (
<button
key={option.key}
type="button"
onClick={() => handleSelect(option.label)}
className={cn(
"w-full px-3 py-2.5 text-sm text-left transition-colors duration-150 rounded-lg",
value === option.label
? "bg-prism-cyan/20 text-prism-cyan font-medium"
: "text-zinc-300 hover:bg-white/[0.06] hover:text-zinc-100",
)}
>
{option.label}
</button>
))}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
}
Loading