chore: remove scripts moved to infracloud

This commit is contained in:
GoHorse Deploy 2026-03-06 13:36:52 -03:00
parent 2d10101394
commit a40c0d8016
8 changed files with 500 additions and 525 deletions

View file

@ -19,6 +19,7 @@ import {
} from "@/components/ui/select"
import { Textarea } from "@/components/ui/textarea"
import { adminCompaniesApi, jobsApi, type AdminCompany, type CreateJobPayload } from "@/lib/api"
import { useTranslation } from "@/lib/i18n"
type ApplicationChannel = "email" | "url" | "phone"
@ -41,6 +42,7 @@ const DESCRIPTION_MIN_LENGTH = 20
export default function DashboardNewJobPage() {
const router = useRouter()
const { t } = useTranslation()
const [loading, setLoading] = useState(false)
const [loadingCompanies, setLoadingCompanies] = useState(true)
const [companies, setCompanies] = useState<AdminCompany[]>([])
@ -69,7 +71,6 @@ export default function DashboardNewJobPage() {
status: "draft",
})
// Location autocomplete state
const [apiCountries, setApiCountries] = useState<ApiCountry[]>([])
const [locationIds, setLocationIds] = useState<{ cityId: number | null; regionId: number | null }>({ cityId: null, regionId: null })
const [locationResults, setLocationResults] = useState<LocationResult[]>([])
@ -77,15 +78,13 @@ export default function DashboardNewJobPage() {
const [showLocationDropdown, setShowLocationDropdown] = useState(false)
const locationRef = useRef<HTMLDivElement>(null)
// Load companies
useEffect(() => {
adminCompaniesApi.list(undefined, 1, 100)
.then((data) => setCompanies(data.data ?? []))
.catch(() => toast.error("Falha ao carregar empresas"))
.catch(() => toast.error(t("admin.jobs.create.messages.loadCompaniesError")))
.finally(() => setLoadingCompanies(false))
}, [])
}, [t])
// Load countries for location autocomplete
useEffect(() => {
const apiBase = process.env.NEXT_PUBLIC_API_URL || ""
fetch(`${apiBase}/api/v1/locations/countries`)
@ -146,9 +145,9 @@ export default function DashboardNewJobPage() {
if (!canSubmit) {
const descriptionLength = formData.description.trim().length
if (descriptionLength < DESCRIPTION_MIN_LENGTH) {
toast.error(`Descricao da vaga deve ter no minimo ${DESCRIPTION_MIN_LENGTH} caracteres`)
toast.error(t("admin.jobs.create.validation.descriptionMin", { min: DESCRIPTION_MIN_LENGTH }))
} else {
toast.error("Preencha os campos obrigatorios")
toast.error(t("admin.jobs.create.validation.requiredFields"))
}
return
}
@ -183,12 +182,12 @@ export default function DashboardNewJobPage() {
}
await jobsApi.create(payload)
toast.success("Vaga cadastrada com sucesso!")
toast.success(t("admin.jobs.create.messages.createSuccess"))
setTimeout(() => {
router.push("/dashboard/jobs")
}, 700)
} catch (error: any) {
toast.error(error.message || "Falha ao cadastrar vaga")
toast.error(error.message || t("admin.jobs.create.messages.createError"))
} finally {
setLoading(false)
}
@ -197,36 +196,37 @@ export default function DashboardNewJobPage() {
return (
<div className="space-y-8">
<div>
<h1 className="text-3xl font-bold text-foreground">Nova vaga</h1>
<p className="text-muted-foreground mt-1">Preencha os dados da vaga. Os campos marcados com * são obrigatórios.</p>
<h1 className="text-3xl font-bold text-foreground">{t("admin.jobs.create.title")}</h1>
<p className="mt-1 text-muted-foreground">
{t("admin.jobs.create.subtitle")}
</p>
</div>
<form onSubmit={handleSubmit} className="space-y-8">
{/* Empresa e Status */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<PlusCircle className="h-5 w-5" /> Empresa e status
<PlusCircle className="h-5 w-5" /> {t("admin.jobs.create.sections.companyStatus")}
</CardTitle>
</CardHeader>
<CardContent className="grid md:grid-cols-4 gap-6">
<div className="md:col-span-3 space-y-1.5">
<Label>Empresa *</Label>
<CardContent className="grid gap-6 md:grid-cols-4">
<div className="space-y-1.5 md:col-span-3">
<Label>{t("admin.jobs.create.fields.company")} *</Label>
{loadingCompanies ? (
<div className="h-10 px-3 flex items-center text-sm text-muted-foreground border rounded-md">
<Loader2 className="h-4 w-4 mr-2 animate-spin" /> Carregando...
<div className="flex h-10 items-center rounded-md border px-3 text-sm text-muted-foreground">
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> {t("admin.jobs.create.common.loading")}
</div>
) : (
<Select value={formData.companyId} onValueChange={(v) => set("companyId", v)}>
<SelectTrigger>
<SelectValue placeholder="Selecione uma empresa" />
<SelectValue placeholder={t("admin.jobs.create.placeholders.company")} />
</SelectTrigger>
<SelectContent>
{companies.length === 0 ? (
<SelectItem value="__none" disabled>Nenhuma empresa disponível</SelectItem>
<SelectItem value="__none" disabled>{t("admin.jobs.create.empty.companies")}</SelectItem>
) : (
companies.map((c) => (
<SelectItem key={c.id} value={c.id}>{c.name}</SelectItem>
companies.map((company) => (
<SelectItem key={company.id} value={company.id}>{company.name}</SelectItem>
))
)}
</SelectContent>
@ -235,104 +235,111 @@ export default function DashboardNewJobPage() {
</div>
<div className="space-y-1.5">
<Label>Status</Label>
<Label>{t("admin.jobs.create.fields.status")}</Label>
<Select value={formData.status} onValueChange={(v) => set("status", v)}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectTrigger>
<SelectValue placeholder={t("admin.jobs.create.placeholders.select")} />
</SelectTrigger>
<SelectContent>
<SelectItem value="draft">Rascunho</SelectItem>
<SelectItem value="review">Em revisão</SelectItem>
<SelectItem value="open">Aberta</SelectItem>
<SelectItem value="paused">Pausada</SelectItem>
<SelectItem value="closed">Encerrada</SelectItem>
<SelectItem value="draft">{t("admin.jobs.create.options.status.draft")}</SelectItem>
<SelectItem value="review">{t("admin.jobs.create.options.status.review")}</SelectItem>
<SelectItem value="open">{t("admin.jobs.create.options.status.open")}</SelectItem>
<SelectItem value="paused">{t("admin.jobs.create.options.status.paused")}</SelectItem>
<SelectItem value="closed">{t("admin.jobs.create.options.status.closed")}</SelectItem>
</SelectContent>
</Select>
</div>
</CardContent>
</Card>
{/* Dados da vaga */}
<Card>
<CardHeader>
<CardTitle>Dados da vaga</CardTitle>
<CardTitle>{t("admin.jobs.create.sections.jobData")}</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-1.5">
<Label htmlFor="title">Título da vaga *</Label>
<Label htmlFor="title">{t("admin.jobs.create.fields.jobTitle")} *</Label>
<Input
id="title"
maxLength={255}
placeholder="Ex: Desenvolvedor(a) Full Stack Sênior"
placeholder={t("admin.jobs.create.placeholders.jobTitle")}
value={formData.title}
onChange={(e) => set("title", e.target.value)}
/>
<p className="text-xs text-muted-foreground mt-1">{formData.title.length}/255 caracteres</p>
<p className="mt-1 text-xs text-muted-foreground">
{t("admin.jobs.create.help.titleLength", { current: formData.title.length, max: 255 })}
</p>
</div>
<div className="space-y-1.5">
<Label htmlFor="description">Descrição da vaga *</Label>
<Label htmlFor="description">{t("admin.jobs.create.fields.description")} *</Label>
<Textarea
id="description"
rows={4}
className="resize-y"
placeholder="Descreva responsabilidades, requisitos e diferenciais..."
placeholder={t("admin.jobs.create.placeholders.description")}
value={formData.description}
onChange={(e) => set("description", e.target.value)}
/>
<p className="text-xs text-muted-foreground mt-1">
Minimo de {DESCRIPTION_MIN_LENGTH} caracteres ({formData.description.trim().length}/{DESCRIPTION_MIN_LENGTH})
<p className="mt-1 text-xs text-muted-foreground">
{t("admin.jobs.create.help.descriptionLength", {
min: DESCRIPTION_MIN_LENGTH,
current: formData.description.trim().length,
})}
</p>
</div>
<div className="md:w-1/3 space-y-1.5">
<Label>Idioma da descrição</Label>
<div className="space-y-1.5 md:w-1/3">
<Label>{t("admin.jobs.create.fields.descriptionLanguage")}</Label>
<Select value={formData.languageLevel} onValueChange={(v) => set("languageLevel", v)}>
<SelectTrigger><SelectValue placeholder="Selecione" /></SelectTrigger>
<SelectTrigger>
<SelectValue placeholder={t("admin.jobs.create.placeholders.select")} />
</SelectTrigger>
<SelectContent>
<SelectItem value="pt">Português</SelectItem>
<SelectItem value="en">English</SelectItem>
<SelectItem value="es">Español</SelectItem>
<SelectItem value="ja"></SelectItem>
<SelectItem value="pt">{t("admin.jobs.create.options.descriptionLanguage.pt")}</SelectItem>
<SelectItem value="en">{t("admin.jobs.create.options.descriptionLanguage.en")}</SelectItem>
<SelectItem value="es">{t("admin.jobs.create.options.descriptionLanguage.es")}</SelectItem>
<SelectItem value="ja">{t("admin.jobs.create.options.descriptionLanguage.ja")}</SelectItem>
</SelectContent>
</Select>
</div>
</CardContent>
</Card>
{/* Localização */}
<Card>
<CardHeader>
<CardTitle>Localização</CardTitle>
<CardTitle>{t("admin.jobs.create.sections.location")}</CardTitle>
</CardHeader>
<CardContent className="grid md:grid-cols-4 gap-6 items-end">
<CardContent className="grid items-end gap-6 md:grid-cols-4">
<div className="space-y-1.5">
<Label>País</Label>
<Label>{t("admin.jobs.create.fields.country")}</Label>
<Select value={formData.country} onValueChange={(v) => {
set("country", v)
set("location", "")
setLocationIds({ cityId: null, regionId: null })
}}>
<SelectTrigger><SelectValue placeholder="Selecione" /></SelectTrigger>
<SelectTrigger>
<SelectValue placeholder={t("admin.jobs.create.placeholders.select")} />
</SelectTrigger>
<SelectContent>
{apiCountries.length > 0
? apiCountries.map((c) => (
<SelectItem key={c.id} value={c.iso2}>{c.name}</SelectItem>
? apiCountries.map((country) => (
<SelectItem key={country.id} value={country.iso2}>{country.name}</SelectItem>
))
: ["US", "BR", "PT", "ES", "GB", "DE", "FR", "JP"].map((iso) => (
<SelectItem key={iso} value={iso}>{iso}</SelectItem>
))
}
))}
</SelectContent>
</Select>
</div>
<div className="md:col-span-2 space-y-1.5">
<Label>Cidade / Estado</Label>
<div className="space-y-1.5 md:col-span-2">
<Label>{t("admin.jobs.create.fields.cityState")}</Label>
<div ref={locationRef} className="relative">
<Input
placeholder={formData.country ? "Digite para buscar..." : "Selecione um país primeiro"}
placeholder={formData.country
? t("admin.jobs.create.placeholders.locationSearch")
: t("admin.jobs.create.placeholders.selectCountryFirst")}
value={formData.location}
disabled={!formData.country}
autoComplete="off"
@ -350,7 +357,7 @@ export default function DashboardNewJobPage() {
</div>
)}
{showLocationDropdown && locationResults.length > 0 && (
<div className="absolute z-50 mt-1 w-full rounded-md border bg-white shadow-md max-h-60 overflow-y-auto">
<div className="absolute z-50 mt-1 max-h-60 w-full overflow-y-auto rounded-md border bg-white shadow-md">
{locationResults.map((result) => (
<button
key={`${result.type}-${result.id}`}
@ -372,11 +379,11 @@ export default function DashboardNewJobPage() {
<span>
<span className="text-sm font-medium">{result.name}</span>
{result.region_name && (
<span className="ml-1 text-xs text-muted-foreground"> {result.region_name}</span>
<span className="ml-1 text-xs text-muted-foreground"> - {result.region_name}</span>
)}
</span>
<span className={`rounded px-1.5 py-0.5 text-xs ${result.type === "city" ? "bg-blue-50 text-blue-600" : "bg-emerald-50 text-emerald-600"}`}>
{result.type}
{t(`admin.jobs.create.location.resultType.${result.type}`)}
</span>
</button>
))}
@ -386,108 +393,106 @@ export default function DashboardNewJobPage() {
</div>
<div className="space-y-1.5">
<Label>Modo de trabalho</Label>
<Label>{t("admin.jobs.create.fields.workMode")}</Label>
<Select value={formData.workMode} onValueChange={(v) => set("workMode", v)}>
<SelectTrigger><SelectValue placeholder="Selecione" /></SelectTrigger>
<SelectTrigger>
<SelectValue placeholder={t("admin.jobs.create.placeholders.select")} />
</SelectTrigger>
<SelectContent>
<SelectItem value="onsite">Presencial</SelectItem>
<SelectItem value="hybrid">Híbrido</SelectItem>
<SelectItem value="remote">Remoto</SelectItem>
<SelectItem value="onsite">{t("admin.jobs.create.options.workMode.onsite")}</SelectItem>
<SelectItem value="hybrid">{t("admin.jobs.create.options.workMode.hybrid")}</SelectItem>
<SelectItem value="remote">{t("admin.jobs.create.options.workMode.remote")}</SelectItem>
</SelectContent>
</Select>
</div>
{/* Empty div for grid alignment to push checkbox down */}
<div className="md:col-span-1 hidden md:block"></div>
<div className="hidden md:block md:col-span-1" />
<div className="md:col-span-3 flex items-center gap-2 pt-2 md:pt-0 pb-2">
<div className="flex items-center gap-2 pb-2 pt-2 md:col-span-3 md:pt-0">
<Checkbox
id="visaSupport"
checked={formData.visaSupport}
onCheckedChange={(v) => set("visaSupport", v === true)}
/>
<Label htmlFor="visaSupport" className="font-normal cursor-pointer">Oferece suporte de visto</Label>
<Label htmlFor="visaSupport" className="cursor-pointer font-normal">
{t("admin.jobs.create.fields.visaSupport")}
</Label>
</div>
</CardContent>
</Card>
{/* Contrato e Salário */}
<Card>
<CardHeader>
<CardTitle>Contrato e salário</CardTitle>
<CardTitle>{t("admin.jobs.create.sections.contractSalary")}</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="grid md:grid-cols-2 gap-6">
<div className="grid gap-6 md:grid-cols-2">
<div className="space-y-1.5">
<Label>Tipo de contrato</Label>
<Label>{t("admin.jobs.create.fields.employmentType")}</Label>
<Select value={formData.employmentType} onValueChange={(v) => set("employmentType", v)}>
<SelectTrigger><SelectValue placeholder="Selecione" /></SelectTrigger>
<SelectTrigger>
<SelectValue placeholder={t("admin.jobs.create.placeholders.select")} />
</SelectTrigger>
<SelectContent>
<SelectItem value="permanent">Permanente</SelectItem>
<SelectItem value="full-time">Tempo integral</SelectItem>
<SelectItem value="part-time">Meio período</SelectItem>
<SelectItem value="contract">Contrato (PJ)</SelectItem>
<SelectItem value="dispatch">Terceirizado</SelectItem>
<SelectItem value="temporary">Temporário</SelectItem>
<SelectItem value="training">Estágio/Trainee</SelectItem>
<SelectItem value="voluntary">Voluntário</SelectItem>
<SelectItem value="permanent">{t("admin.jobs.create.options.employmentType.permanent")}</SelectItem>
<SelectItem value="full-time">{t("admin.jobs.create.options.employmentType.fullTime")}</SelectItem>
<SelectItem value="part-time">{t("admin.jobs.create.options.employmentType.partTime")}</SelectItem>
<SelectItem value="contract">{t("admin.jobs.create.options.employmentType.contract")}</SelectItem>
<SelectItem value="dispatch">{t("admin.jobs.create.options.employmentType.dispatch")}</SelectItem>
<SelectItem value="temporary">{t("admin.jobs.create.options.employmentType.temporary")}</SelectItem>
<SelectItem value="training">{t("admin.jobs.create.options.employmentType.training")}</SelectItem>
<SelectItem value="voluntary">{t("admin.jobs.create.options.employmentType.voluntary")}</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-1.5">
<Label htmlFor="workingHours">Jornada de trabalho</Label>
<Label htmlFor="workingHours">{t("admin.jobs.create.fields.workingHours")}</Label>
<Input
id="workingHours"
placeholder="Ex: 9h às 18h, 40h/semana"
placeholder={t("admin.jobs.create.placeholders.workingHours")}
value={formData.workingHours}
onChange={(e) => set("workingHours", e.target.value)}
/>
</div>
</div>
<div className="grid md:grid-cols-4 gap-6">
<div className="grid gap-6 md:grid-cols-4">
<div className="space-y-1.5">
<Label>Moeda</Label>
<Label>{t("admin.jobs.create.fields.currency")}</Label>
<Select value={formData.currency} onValueChange={(v) => set("currency", v)}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="BRL">BRL R$</SelectItem>
<SelectItem value="USD">USD $</SelectItem>
<SelectItem value="EUR">EUR </SelectItem>
<SelectItem value="GBP">GBP £</SelectItem>
<SelectItem value="JPY">JPY ¥</SelectItem>
<SelectItem value="CNY">CNY ¥</SelectItem>
<SelectItem value="AED">AED د.إ</SelectItem>
<SelectItem value="CAD">CAD $</SelectItem>
<SelectItem value="AUD">AUD $</SelectItem>
<SelectItem value="CHF">CHF Fr</SelectItem>
<SelectItem value="BRL">BRL - R$</SelectItem>
<SelectItem value="USD">USD - $</SelectItem>
<SelectItem value="EUR">EUR - EUR</SelectItem>
<SelectItem value="GBP">GBP - GBP</SelectItem>
<SelectItem value="JPY">JPY - JPY</SelectItem>
<SelectItem value="CNY">CNY - CNY</SelectItem>
<SelectItem value="AED">AED - AED</SelectItem>
<SelectItem value="CAD">CAD - $</SelectItem>
<SelectItem value="AUD">AUD - $</SelectItem>
<SelectItem value="CHF">CHF - CHF</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-1.5">
<Label>Período</Label>
<Label>{t("admin.jobs.create.fields.salaryPeriod")}</Label>
<Select value={formData.salaryType} onValueChange={(v) => set("salaryType", v)}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="hourly">Por hora</SelectItem>
<SelectItem value="daily">Por dia</SelectItem>
<SelectItem value="weekly">Por semana</SelectItem>
<SelectItem value="monthly">Por mês</SelectItem>
<SelectItem value="yearly">Por ano</SelectItem>
<SelectItem value="hourly">{t("admin.jobs.create.options.salaryType.hourly")}</SelectItem>
<SelectItem value="daily">{t("admin.jobs.create.options.salaryType.daily")}</SelectItem>
<SelectItem value="weekly">{t("admin.jobs.create.options.salaryType.weekly")}</SelectItem>
<SelectItem value="monthly">{t("admin.jobs.create.options.salaryType.monthly")}</SelectItem>
<SelectItem value="yearly">{t("admin.jobs.create.options.salaryType.yearly")}</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-1.5">
<Label htmlFor="salaryMin">Salário mínimo</Label>
<Label htmlFor="salaryMin">{t("admin.jobs.create.fields.salaryMin")}</Label>
<Input
id="salaryMin"
type="number"
@ -499,8 +504,7 @@ export default function DashboardNewJobPage() {
</div>
<div className="space-y-1.5">
<Label htmlFor="salaryMax">Salário máximo</Label>
<Label htmlFor="salaryMax">{t("admin.jobs.create.fields.salaryMax")}</Label>
<Input
id="salaryMax"
type="number"
@ -518,50 +522,48 @@ export default function DashboardNewJobPage() {
checked={formData.salaryNegotiable}
onCheckedChange={(v) => set("salaryNegotiable", v === true)}
/>
<Label htmlFor="salaryNegotiable" className="font-normal cursor-pointer">Salário negociável</Label>
<Label htmlFor="salaryNegotiable" className="cursor-pointer font-normal">
{t("admin.jobs.create.fields.salaryNegotiable")}
</Label>
</div>
</CardContent>
</Card>
{/* Candidaturas */}
<Card>
<CardHeader>
<CardTitle>Candidaturas</CardTitle>
<CardDescription>Como os candidatos devem se candidatar à vaga.</CardDescription>
<CardTitle>{t("admin.jobs.create.sections.applications")}</CardTitle>
<CardDescription>{t("admin.jobs.create.application.description")}</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="grid md:grid-cols-2 gap-6">
<div className="grid gap-6 md:grid-cols-2">
<div className="space-y-1.5">
<Label>Canal de candidatura</Label>
<Label>{t("admin.jobs.create.fields.applicationChannel")}</Label>
<Select value={formData.applicationChannel} onValueChange={(v) => set("applicationChannel", v as ApplicationChannel)}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="email">E-mail</SelectItem>
<SelectItem value="url">Link externo</SelectItem>
<SelectItem value="phone">Telefone</SelectItem>
<SelectItem value="email">{t("admin.jobs.create.options.applicationChannel.email")}</SelectItem>
<SelectItem value="url">{t("admin.jobs.create.options.applicationChannel.url")}</SelectItem>
<SelectItem value="phone">{t("admin.jobs.create.options.applicationChannel.phone")}</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-1.5">
<Label>Currículo</Label>
<Label>{t("admin.jobs.create.fields.resumeRequirement")}</Label>
<Select value={formData.resumeRequirement} onValueChange={(v) => set("resumeRequirement", v)}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="required">Obrigatório</SelectItem>
<SelectItem value="optional">Opcional</SelectItem>
<SelectItem value="none">Não solicitado</SelectItem>
<SelectItem value="required">{t("admin.jobs.create.options.resumeRequirement.required")}</SelectItem>
<SelectItem value="optional">{t("admin.jobs.create.options.resumeRequirement.optional")}</SelectItem>
<SelectItem value="none">{t("admin.jobs.create.options.resumeRequirement.none")}</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{formData.applicationChannel === "email" && (
<div className="space-y-1.5 mt-2">
<Label htmlFor="appEmail">E-mail para candidatura</Label>
<div className="mt-2 space-y-1.5">
<Label htmlFor="appEmail">{t("admin.jobs.create.fields.applicationEmail")}</Label>
<Input
id="appEmail"
type="email"
@ -573,9 +575,8 @@ export default function DashboardNewJobPage() {
)}
{formData.applicationChannel === "url" && (
<div className="space-y-1.5 mt-2">
<Label htmlFor="appUrl">Link externo (HTTPS)</Label>
<div className="mt-2 space-y-1.5">
<Label htmlFor="appUrl">{t("admin.jobs.create.fields.applicationUrl")}</Label>
<Input
id="appUrl"
placeholder="https://empresa.com/carreiras"
@ -586,9 +587,8 @@ export default function DashboardNewJobPage() {
)}
{formData.applicationChannel === "phone" && (
<div className="space-y-1.5 mt-2">
<Label htmlFor="appPhone">Telefone (com DDI)</Label>
<div className="mt-2 space-y-1.5">
<Label htmlFor="appPhone">{t("admin.jobs.create.fields.applicationPhone")}</Label>
<Input
id="appPhone"
placeholder="+55 11 99999-8888"
@ -601,8 +601,8 @@ export default function DashboardNewJobPage() {
</Card>
<Button type="submit" disabled={loading || !canSubmit} size="lg">
{loading && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
Criar vaga
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{t("admin.jobs.create.submit")}
</Button>
</form>
</div>

View file

@ -1142,6 +1142,125 @@
"cancel": "Cancel",
"save": "Save Changes"
},
"create": {
"title": "New job",
"subtitle": "Fill in the job details. Fields marked with * are required.",
"sections": {
"companyStatus": "Company and status",
"jobData": "Job details",
"location": "Location",
"contractSalary": "Contract and salary",
"applications": "Applications"
},
"common": {
"loading": "Loading..."
},
"fields": {
"company": "Company",
"status": "Status",
"jobTitle": "Job title",
"description": "Job description",
"descriptionLanguage": "Description language",
"country": "Country",
"cityState": "City / State",
"workMode": "Work mode",
"visaSupport": "Offers visa support",
"employmentType": "Employment type",
"workingHours": "Working hours",
"currency": "Currency",
"salaryPeriod": "Salary period",
"salaryMin": "Minimum salary",
"salaryMax": "Maximum salary",
"salaryNegotiable": "Negotiable salary",
"applicationChannel": "Application channel",
"resumeRequirement": "Resume",
"applicationEmail": "Application email",
"applicationUrl": "External link (HTTPS)",
"applicationPhone": "Phone (with country code)"
},
"placeholders": {
"company": "Select a company",
"select": "Select",
"jobTitle": "Ex: Senior Full Stack Developer",
"description": "Describe responsibilities, requirements, and differentiators...",
"locationSearch": "Type to search...",
"selectCountryFirst": "Select a country first",
"workingHours": "Ex: 9am to 6pm, 40h/week"
},
"help": {
"titleLength": "{current}/255 characters",
"descriptionLength": "Minimum {min} characters ({current}/{min})"
},
"empty": {
"companies": "No companies available"
},
"validation": {
"descriptionMin": "Job description must be at least {min} characters",
"requiredFields": "Fill in the required fields"
},
"messages": {
"loadCompaniesError": "Failed to load companies",
"createSuccess": "Job created successfully!",
"createError": "Failed to create job"
},
"application": {
"description": "How candidates should apply for this job."
},
"location": {
"resultType": {
"city": "city",
"state": "state"
}
},
"options": {
"status": {
"draft": "Draft",
"review": "In review",
"open": "Open",
"paused": "Paused",
"closed": "Closed"
},
"descriptionLanguage": {
"pt": "Portuguese",
"en": "English",
"es": "Spanish",
"ja": "Japanese"
},
"workMode": {
"onsite": "On-site",
"hybrid": "Hybrid",
"remote": "Remote"
},
"employmentType": {
"permanent": "Permanent",
"fullTime": "Full-time",
"partTime": "Part-time",
"contract": "Contract",
"dispatch": "Outsourced",
"temporary": "Temporary",
"training": "Internship / Trainee",
"voluntary": "Volunteer"
},
"salaryType": {
"hourly": "Per hour",
"daily": "Per day",
"weekly": "Per week",
"monthly": "Per month",
"yearly": "Per year"
},
"applicationChannel": {
"email": "Email",
"url": "External link",
"phone": "Phone"
},
"resumeRequirement": {
"required": "Required",
"optional": "Optional",
"none": "Not requested"
}
},
"submit": "Create job"
},
"deleteConfirm": "Are you sure you want to delete this job?",
"deleteError": "Failed to delete job",
"updateError": "Failed to update job"

View file

@ -1006,6 +1006,125 @@
"cancel": "Cancelar",
"save": "Guardar Cambios"
},
"create": {
"title": "Nuevo empleo",
"subtitle": "Completa los datos del empleo. Los campos marcados con * son obligatorios.",
"sections": {
"companyStatus": "Empresa y estado",
"jobData": "Datos del empleo",
"location": "Ubicacion",
"contractSalary": "Contrato y salario",
"applications": "Postulaciones"
},
"common": {
"loading": "Cargando..."
},
"fields": {
"company": "Empresa",
"status": "Estado",
"jobTitle": "Titulo del empleo",
"description": "Descripcion del empleo",
"descriptionLanguage": "Idioma de la descripcion",
"country": "Pais",
"cityState": "Ciudad / Estado",
"workMode": "Modalidad de trabajo",
"visaSupport": "Ofrece apoyo de visa",
"employmentType": "Tipo de contrato",
"workingHours": "Jornada laboral",
"currency": "Moneda",
"salaryPeriod": "Periodo salarial",
"salaryMin": "Salario minimo",
"salaryMax": "Salario maximo",
"salaryNegotiable": "Salario negociable",
"applicationChannel": "Canal de postulacion",
"resumeRequirement": "Curriculum",
"applicationEmail": "Correo para postulacion",
"applicationUrl": "Enlace externo (HTTPS)",
"applicationPhone": "Telefono (con codigo internacional)"
},
"placeholders": {
"company": "Seleccione una empresa",
"select": "Seleccione",
"jobTitle": "Ej: Desarrollador(a) Full Stack Senior",
"description": "Describe responsabilidades, requisitos y diferenciales...",
"locationSearch": "Escriba para buscar...",
"selectCountryFirst": "Seleccione un pais primero",
"workingHours": "Ej: 9h a 18h, 40h/semana"
},
"help": {
"titleLength": "{current}/255 caracteres",
"descriptionLength": "Minimo de {min} caracteres ({current}/{min})"
},
"empty": {
"companies": "No hay empresas disponibles"
},
"validation": {
"descriptionMin": "La descripcion del empleo debe tener al menos {min} caracteres",
"requiredFields": "Complete los campos obligatorios"
},
"messages": {
"loadCompaniesError": "Error al cargar empresas",
"createSuccess": "Empleo creado con exito!",
"createError": "Error al crear el empleo"
},
"application": {
"description": "Como deben postularse los candidatos a este empleo."
},
"location": {
"resultType": {
"city": "ciudad",
"state": "estado"
}
},
"options": {
"status": {
"draft": "Borrador",
"review": "En revision",
"open": "Abierta",
"paused": "Pausada",
"closed": "Cerrada"
},
"descriptionLanguage": {
"pt": "Portugues",
"en": "English",
"es": "Espanol",
"ja": "Japanese"
},
"workMode": {
"onsite": "Presencial",
"hybrid": "Hibrido",
"remote": "Remoto"
},
"employmentType": {
"permanent": "Permanente",
"fullTime": "Tiempo completo",
"partTime": "Medio tiempo",
"contract": "Contrato",
"dispatch": "Tercerizado",
"temporary": "Temporal",
"training": "Pasantia / Trainee",
"voluntary": "Voluntario"
},
"salaryType": {
"hourly": "Por hora",
"daily": "Por dia",
"weekly": "Por semana",
"monthly": "Por mes",
"yearly": "Por ano"
},
"applicationChannel": {
"email": "Correo",
"url": "Enlace externo",
"phone": "Telefono"
},
"resumeRequirement": {
"required": "Obligatorio",
"optional": "Opcional",
"none": "No solicitado"
}
},
"submit": "Crear empleo"
},
"deleteConfirm": "¿Está seguro de que desea eliminar este empleo?",
"deleteError": "Error al eliminar el empleo",
"updateError": "Error al actualizar el empleo"

View file

@ -1183,6 +1183,125 @@
"cancel": "Cancelar",
"save": "Salvar Alterações"
},
"create": {
"title": "Nova vaga",
"subtitle": "Preencha os dados da vaga. Os campos marcados com * sao obrigatorios.",
"sections": {
"companyStatus": "Empresa e status",
"jobData": "Dados da vaga",
"location": "Localizacao",
"contractSalary": "Contrato e salario",
"applications": "Candidaturas"
},
"common": {
"loading": "Carregando..."
},
"fields": {
"company": "Empresa",
"status": "Status",
"jobTitle": "Titulo da vaga",
"description": "Descricao da vaga",
"descriptionLanguage": "Idioma da descricao",
"country": "Pais",
"cityState": "Cidade / Estado",
"workMode": "Modo de trabalho",
"visaSupport": "Oferece suporte de visto",
"employmentType": "Tipo de contrato",
"workingHours": "Jornada de trabalho",
"currency": "Moeda",
"salaryPeriod": "Periodo",
"salaryMin": "Salario minimo",
"salaryMax": "Salario maximo",
"salaryNegotiable": "Salario negociavel",
"applicationChannel": "Canal de candidatura",
"resumeRequirement": "Curriculo",
"applicationEmail": "E-mail para candidatura",
"applicationUrl": "Link externo (HTTPS)",
"applicationPhone": "Telefone (com DDI)"
},
"placeholders": {
"company": "Selecione uma empresa",
"select": "Selecione",
"jobTitle": "Ex: Desenvolvedor(a) Full Stack Senior",
"description": "Descreva responsabilidades, requisitos e diferenciais...",
"locationSearch": "Digite para buscar...",
"selectCountryFirst": "Selecione um pais primeiro",
"workingHours": "Ex: 9h as 18h, 40h/semana"
},
"help": {
"titleLength": "{current}/255 caracteres",
"descriptionLength": "Minimo de {min} caracteres ({current}/{min})"
},
"empty": {
"companies": "Nenhuma empresa disponivel"
},
"validation": {
"descriptionMin": "Descricao da vaga deve ter no minimo {min} caracteres",
"requiredFields": "Preencha os campos obrigatorios"
},
"messages": {
"loadCompaniesError": "Falha ao carregar empresas",
"createSuccess": "Vaga cadastrada com sucesso!",
"createError": "Falha ao cadastrar vaga"
},
"application": {
"description": "Como os candidatos devem se candidatar a vaga."
},
"location": {
"resultType": {
"city": "cidade",
"state": "estado"
}
},
"options": {
"status": {
"draft": "Rascunho",
"review": "Em revisao",
"open": "Aberta",
"paused": "Pausada",
"closed": "Encerrada"
},
"descriptionLanguage": {
"pt": "Portugues",
"en": "English",
"es": "Espanol",
"ja": "Japanese"
},
"workMode": {
"onsite": "Presencial",
"hybrid": "Hibrido",
"remote": "Remoto"
},
"employmentType": {
"permanent": "Permanente",
"fullTime": "Tempo integral",
"partTime": "Meio periodo",
"contract": "Contrato (PJ)",
"dispatch": "Terceirizado",
"temporary": "Temporario",
"training": "Estagio/Trainee",
"voluntary": "Voluntario"
},
"salaryType": {
"hourly": "Por hora",
"daily": "Por dia",
"weekly": "Por semana",
"monthly": "Por mes",
"yearly": "Por ano"
},
"applicationChannel": {
"email": "E-mail",
"url": "Link externo",
"phone": "Telefone"
},
"resumeRequirement": {
"required": "Obrigatorio",
"optional": "Opcional",
"none": "Nao solicitado"
}
},
"submit": "Criar vaga"
},
"deleteConfirm": "Tem certeza que deseja excluir esta vaga?",
"deleteError": "Falha ao excluir vaga",
"updateError": "Falha ao atualizar vaga"

View file

@ -1,28 +0,0 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCc9suDobwJwCGJ
VFvga1BxKkmmGOxoF8zNibv6l33/SCFEBb5fFaenxvYotEGWUw0fed4zIcX3s6hA
q2yLr3nIygpLpcOfzpzPxas49P17NA3Chvo3k/0eGkBD6PHM1s62qPP+fKEZtwlS
q1WaFxfc949iqJAQvW6w/7WgMZDineq3IzhVVUAFdw3icZru97hCjPDU/v3eFTS7
kvGrDYGAHZXzylu3Er9ifKYHdKxOrWFmGaSsPsYMdKNxWFk+Z38NVUnwSH3TEiV/
S4e33tTkdMmNpY+6e9Cigb09RnOalj5lPjFGA9nTHMJxpsHvSKu8vMBr+OZ4CM3U
RH7MUX01AgMBAAECggEAMKxdFo/4MePY4m984B4W/0iYNv/iizLaKOBtoLsKcLeK
zT+ktXKPHzlUyvF+pyFQ3/JYA24VKAcXhRpDWhuLfcadI7Ee9PbKbKmEu3BJDEPr
gmd9vu9Ond+RDx30oUr5Je5FXySBhmpaYz7LGDHSDgzcc0EHD5HWed+JkEfegE7w
Mvt9KK41mGdaQwiPHS43uzZhQJEqybP3i/6SUnV2CntOhutxLlPk2rpHnns0p/St
Dvlcv61vduIaej4IFBrpSwTE45pvIfkvNZx0pJapM1jZhe8F/2T7GtXDkoFQveo1
3YB1aadpCx7u28IzQTwBZVwqhCpi2a5+qVYUT0AU3wKBgQDYYUxQUBiUn6bXoAsx
JTozoX0K50cX2d8LVY1OUuhpRXbztS2XXtyfeoAQtEWoT3UO7vjEedWswfo2j+N3
ZIXig7Vyj/LN5lZyCwWYn4S4inESjKlzi4Pv8D4F+Fkgg0WsVgzbTa4P7faHnDNn
eEHdyJ/ZQ8+XYxBpSAE8ecWQlwKBgQC5tGbfzh77REsv1h6b87vulrGHc+OBITTU
YFu1YfXpvbXx9geRfNLDtUhUis6vgfcQV6sxZVf78UdlqiTBebRLpcvoBlHV/MPZ
T3TsZH1vXwiitOsBIFzKkn8xdjuN6mN5lLjI6KkYeVoULYiUNbiZ+Wi7PXBPnc5I
jBO5EayOEwKBgQDU2pnso24avhatJKX92WYwphpQoISCBPPxvV38/3fbHtdOFBte
PZYAV8wlIoEnecpoP1J+TG+Su1r9U3xq1XsTAYd7w/kQ7RZ6pzcBFWLE+oMSwUZs
AIFwhb8ttklOv3PJfPi2vuqMhwUuD81NarI4jwQYASnz/SKGvqtgp1VezwKBgDoL
DOx+/GgE3ItDHaYY9HCKYUq5Ci7eNij7RS7YQ4ifZzMNdygeH7JUAxuJlzh8IsDU
5gk2Z92zeGFqYLqoU5YhaC5Ja2K68mwFzcHlVt9skMJqUdm0R8x5JZBMKCkfTaA+
v9LsBY5Ev8b2xG2urNhTgEyl02jPJh6+yZtazthJAoGAHRIX/W0IlyaLno7WzAwM
lSsNfJpTvZmkri0UOGXM2YaKuQZ652t6EBDtfM7O16eV3KNBblt1LjItz/S8kiFi
Q8tGluO27Hn5/auixJjlcZnzoUXrEjAra8lmgAo41Dm0icDpLUzhixZ0qS8d6Yfp
RIT1IoWSuu2fvOOvqezq6bg=
-----END PRIVATE KEY-----

View file

@ -1,12 +0,0 @@
dokku dokku@localhost:gohorsejobs (fetch)
dokku dokku@localhost:gohorsejobs (push)
dokku-frontend dokku@localhost:gohorse-frontend (fetch)
dokku-frontend dokku@localhost:gohorse-frontend (push)
dokku-restore dokku@localhost:gohorse-frontend (fetch)
dokku-restore dokku@localhost:gohorse-frontend (push)
forgejo git@pipe.gohorsejobs.com:bohessefm/gohorsejobs.git (fetch)
forgejo git@pipe.gohorsejobs.com:bohessefm/gohorsejobs.git (push)
origin git@github.com:rede5/gohorsejobs.git (fetch)
origin git@github.com:rede5/gohorsejobs.git (push)
pipe https://pipe.gohorsejobs.com/bohessefm/gohorsejobs.git (fetch)
pipe https://pipe.gohorsejobs.com/bohessefm/gohorsejobs.git (push)

View file

@ -1 +0,0 @@
LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2UUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktjd2dnU2pBZ0VBQW9JQkFRQ2M5c3VEb2J3SndDR0oKVkZ2Z2ExQnhLa21tR094b0Y4ek5pYnY2bDMzL1NDRkVCYjVmRmFlbnh2WW90RUdXVXcwZmVkNHpJY1gzczZoQQpxMnlMcjNuSXlncExwY09menB6UHhhczQ5UDE3TkEzQ2h2bzNrLzBlR2tCRDZQSE0xczYycVBQK2ZLRVp0d2xTCnExV2FGeGZjOTQ5aXFKQVF2VzZ3LzdXZ01aRGluZXEzSXpoVlZVQUZkdzNpY1pydTk3aENqUERVL3YzZUZUUzcKa3ZHckRZR0FIWlh6eWx1M0VyOWlmS1lIZEt4T3JXRm1HYVNzUHNZTWRLTnhXRmsrWjM4TlZVbndTSDNURWlWLwpTNGUzM3RUa2RNbU5wWSs2ZTlDaWdiMDlSbk9hbGo1bFBqRkdBOW5USE1KeHBzSHZTS3U4dk1CcitPWjRDTTNVClJIN01VWDAxQWdNQkFBRUNnZ0VBTUt4ZEZvLzRNZVBZNG05ODRCNFcvMGlZTnYvaWl6TGFLT0J0b0xzS2NMZUsKelQra3RYS1BIemxVeXZGK3B5RlEzL0pZQTI0VktBY1hoUnBEV2h1TGZjYWRJN0VlOVBiS2JLbUV1M0JKREVQcgpnbWQ5dnU5T25kK1JEeDMwb1VyNUplNUZYeVNCaG1wYVl6N0xHREhTRGd6Y2MwRUhENUhXZWQrSmtFZmVnRTd3Ck12dDlLSzQxbUdkYVF3aVBIUzQzdXpaaFFKRXF5YlAzaS82U1VuVjJDbnRPaHV0eExsUGsycnBIbm5zMHAvU3QKRHZsY3Y2MXZkdUlhZWo0SUZCcnBTd1RFNDVwdklma3ZOWngwcEphcE0xalpoZThGLzJUN0d0WERrb0ZRdmVvMQozWUIxYWFkcEN4N3UyOEl6UVR3QlpWd3FoQ3BpMmE1K3FWWVVUMEFVM3dLQmdRRFlZVXhRVUJpVW42YlhvQXN4CkpUb3pvWDBLNTBjWDJkOExWWTFPVXVocFJYYnp0UzJYWHR5ZmVvQVF0RVdvVDNVTzd2akVlZFdzd2ZvMmorTjMKWklYaWc3VnlqL0xONWxaeUN3V1luNFM0aW5FU2pLbHppNFB2OEQ0RitGa2dnMFdzVmd6YlRhNFA3ZmFIbkRObgplRUhkeUovWlE4K1hZeEJwU0FFOGVjV1Fsd0tCZ1FDNXRHYmZ6aDc3UkVzdjFoNmI4N3Z1bHJHSGMrT0JJVFRVCllGdTFZZlhwdmJYeDlnZVJmTkxEdFVoVWlzNnZnZmNRVjZzeFpWZjc4VWRscWlUQmViUkxwY3ZvQmxIVi9NUFoKVDNUc1pIMXZYd2lpdE9zQklGektrbjh4ZGp1TjZtTjVsTGpJNktrWWVWb1VMWWlVTmJpWitXaTdQWEJQbmM1SQpqQk81RWF5T0V3S0JnUURVMnBuc28yNGF2aGF0SktYOTJXWXdwaHBRb0lTQ0JQUHh2VjM4LzNmYkh0ZE9GQnRlClBaWUFWOHdsSW9FbmVjcG9QMUorVEcrU3UxcjlVM3hxMVhzVEFZZDd3L2tRN1JaNnB6Y0JGV0xFK29NU3dVWnMKQUlGd2hiOHR0a2xPdjNQSmZQaTJ2dXFNaHdVdUQ4MU5hckk0andRWUFTbnovU0tHdnF0Z3AxVmV6d0tCZ0RvTApET3grL0dnRTNJdERIYVlZOUhDS1lVcTVDaTdlTmlqN1JTN1lRNGlmWnpNTmR5Z2VIN0pVQXh1Smx6aDhJc0RVCjVnazJaOTJ6ZUdGcVlMcW9VNVloYUM1SmEySzY4bXdGemNIbFZ0OXNrTUpxVWRtMFI4eDVKWkJNS0NrZlRhQSsKdjlMc0JZNUV2OGIyeEcydXJOaFRnRXlsMDJqUEpoNit5WnRhenRoSkFvR0FIUklYL1cwSWx5YUxubzdXekF3TQpsU3NOZkpwVHZabWtyaTBVT0dYTTJZYUt1UVo2NTJ0NkVCRHRmTTdPMTZlVjNLTkJibHQxTGpJdHovUzhraUZpClE4dEdsdU8yN0huNS9hdWl4SmpsY1puem9VWHJFakFyYThsbWdBbzQxRG0waWNEcExVemhpeFowcVM4ZDZZZnAKUklUMUlvV1N1dTJmdk9PdnFlenE2Ymc9Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K

341
start.sh
View file

@ -1,341 +0,0 @@
#!/bin/bash
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
# Function to kill background processes on exit
cleanup() {
echo -e "\n${YELLOW}🛑 Stopping services...${NC}"
kill $(jobs -p) 2>/dev/null
exit
}
trap cleanup SIGINT SIGTERM
# Header
clear
echo -e "${CYAN}"
echo "╔══════════════════════════════════════════════╗"
echo "║ 🐴 GoHorse Jobs - Dev Server ║"
echo "╚══════════════════════════════════════════════╝"
echo -e "${NC}"
# Interactive menu
echo -e "${GREEN}Select an option:${NC}\n"
echo -e " ${CYAN}── Services ──${NC}"
echo -e " ${YELLOW}1)${NC} 🚀 Start (Frontend + Backend)"
echo -e " ${YELLOW}2)${NC} 🌱 Start with Seed (Reset DB + Seed + Start)"
echo -e " ${YELLOW}3)${NC} 📦 Start All (Frontend + Backend + Backoffice)"
echo -e ""
echo -e " ${CYAN}── Database ──${NC}"
echo -e " ${YELLOW}4)${NC} 🗄️ Run Migrations"
echo -e " ${YELLOW}5)${NC} 🌿 Seed Only (append data)"
echo -e " ${YELLOW}6)${NC} 🔄 Seed Reset (drop all + seed fresh)"
echo -e ""
echo -e " ${CYAN}── Testing ──${NC}"
echo -e " ${YELLOW}7)${NC} 🧪 Run Tests (Backend E2E)"
echo -e " ${YELLOW}0)${NC} ❌ Exit"
echo -e ""
echo -e " ${CYAN}── Fast Options ──${NC}"
echo -e " ${YELLOW}8)${NC} ⚡ Seed Reset LITE (skip 153k cities)"
echo -e " ${YELLOW}9)${NC} 🔬 Run All Tests (Backend + Frontend)"
echo -e ""
echo -e " ${CYAN}── Docker/Deploy ──${NC}"
echo -e " ${YELLOW}a)${NC} 🐳 Build Docker Images"
echo -e " ${YELLOW}b)${NC} 🚀 Build & Push to Forgejo"
echo ""
read -p "Enter option [0-9,a,b]: " choice
case $choice in
1)
echo -e "\n${GREEN}🚀 Starting Development Environment...${NC}\n"
# Backend
echo -e "${BLUE}🔹 Checking Backend...${NC}"
cd backend && go mod tidy
if command -v swag &> /dev/null; then
echo -e "${BLUE}🔹 Generating Swagger Docs...${NC}"
swag init -g cmd/api/main.go --parseDependency --parseInternal 2>/dev/null
fi
cd ..
# Frontend deps
if [ ! -d "frontend/node_modules" ]; then
echo -e "${BLUE}🔹 Installing Frontend Dependencies...${NC}"
cd frontend && npm install && cd ..
fi
# Start services
echo -e "${BLUE}🔹 Starting Backend on port 8521...${NC}"
(cd backend && go run cmd/api/main.go) &
BACKEND_PID=$!
sleep 2
echo -e "${BLUE}🔹 Starting Frontend on port 8963...${NC}"
(cd frontend && npm run dev -- -p 8963) &
FRONTEND_PID=$!
echo -e "\n${GREEN}✅ Services running:${NC}"
echo -e " ${CYAN}Backend:${NC} http://localhost:8521"
echo -e " ${CYAN}Frontend:${NC} http://localhost:8963"
echo -e " ${CYAN}Swagger:${NC} http://localhost:8521/swagger/index.html"
echo ""
wait $BACKEND_PID $FRONTEND_PID
;;
2)
echo -e "\n${GREEN}🌱 Starting with Database Reset & Seed...${NC}\n"
# Backend prep
echo -e "${BLUE}🔹 Checking Backend...${NC}"
cd backend && go mod tidy
if command -v swag &> /dev/null; then
swag init -g cmd/api/main.go --parseDependency --parseInternal 2>/dev/null
fi
cd ..
# Seeder deps
cd seeder-api
if [ ! -d "node_modules" ]; then
echo -e "${BLUE}🔹 Installing Seeder Dependencies...${NC}"
npm install
fi
echo -e "${YELLOW}🔹 Resetting Database...${NC}"
npm run seed:reset
cd ..
# Start backend
echo -e "${BLUE}🔹 Starting Backend...${NC}"
(cd backend && go run cmd/api/main.go) &
BACKEND_PID=$!
echo -e "${YELLOW}⏳ Waiting for Backend...${NC}"
sleep 5
# Seed
echo -e "${BLUE}🔹 Seeding Database (30 companies, 990 jobs)...${NC}"
cd seeder-api && npm run seed && cd ..
# Frontend
if [ ! -d "frontend/node_modules" ]; then
cd frontend && npm install && cd ..
fi
echo -e "${BLUE}🔹 Starting Frontend...${NC}"
(cd frontend && npm run dev -- -p 8963) &
FRONTEND_PID=$!
echo -e "\n${GREEN}✅ Services running with fresh data!${NC}"
echo -e " ${CYAN}Backend:${NC} http://localhost:8521"
echo -e " ${CYAN}Frontend:${NC} http://localhost:8963"
echo ""
wait $BACKEND_PID $FRONTEND_PID
;;
3)
echo -e "\n${GREEN}📦 Starting All Services (Including Backoffice)...${NC}\n"
# Backend
cd backend && go mod tidy && cd ..
# Dependencies
[ ! -d "frontend/node_modules" ] && (cd frontend && npm install && cd ..)
[ ! -d "backoffice/node_modules" ] && (cd backoffice && npm install && cd ..)
echo -e "${BLUE}🔹 Starting Backend on port 8521...${NC}"
(cd backend && go run cmd/api/main.go) &
sleep 2
echo -e "${BLUE}🔹 Starting Frontend on port 8963...${NC}"
(cd frontend && npm run dev -- -p 8963) &
echo -e "${BLUE}🔹 Starting Backoffice on port 3001...${NC}"
(cd backoffice && npm run start:dev) &
echo -e "\n${GREEN}✅ All services running:${NC}"
echo -e " ${CYAN}Backend:${NC} http://localhost:8521"
echo -e " ${CYAN}Frontend:${NC} http://localhost:8963"
echo -e " ${CYAN}Backoffice:${NC} http://localhost:3001"
echo -e " ${CYAN}Swagger:${NC} http://localhost:3001/api/docs"
echo ""
wait
;;
4)
echo -e "\n${GREEN}🗄️ Running Migrations...${NC}\n"
cd seeder-api
[ ! -d "node_modules" ] && npm install
npm run migrate
;;
5)
echo -e "\n${GREEN}🌿 Seeding Database Only...${NC}\n"
cd seeder-api
[ ! -d "node_modules" ] && npm install
npm run seed
echo -e "\n${GREEN}✅ Seeding completed!${NC}"
;;
6)
echo -e "\n${GREEN}🔄 Resetting Database & Seeding Fresh...${NC}\n"
cd seeder-api
[ ! -d "node_modules" ] && npm install
echo -e "${YELLOW}⚠️ This will DROP all tables and recreate!${NC}"
read -p "Are you sure? [y/N]: " confirm
if [[ $confirm == [yY] || $confirm == [yY][eE][sS] ]]; then
echo -e "\n${BLUE}🔹 Step 1/3: Dropping all tables...${NC}"
npm run seed:reset
echo -e "\n${BLUE}🔹 Step 2/3: Running migrations...${NC}"
npm run migrate
echo -e "\n${BLUE}🔹 Step 3/3: Seeding data...${NC}"
npm run seed
echo -e "\n${GREEN}✅ Database fully reset and seeded!${NC}"
else
echo -e "${YELLOW}Cancelled.${NC}"
fi
;;
7)
echo -e "\n${GREEN}🧪 Running Backend E2E Tests...${NC}\n"
cd backend && go test -tags=e2e -v ./tests/e2e/... 2>&1
echo -e "\n${GREEN}✅ Tests completed!${NC}"
;;
8)
echo -e "\n${GREEN}⚡ Fast Reset - Seed LITE (no cities)...${NC}\n"
cd seeder-api
[ ! -d "node_modules" ] && npm install
echo -e "${YELLOW}⚠️ This will DROP all tables and recreate (WITHOUT 153k cities)${NC}"
read -p "Are you sure? [y/N]: " confirm
if [[ $confirm == [yY] || $confirm == [yY][eE][sS] ]]; then
echo -e "\n${BLUE}🔹 Step 1/3: Dropping all tables...${NC}"
npm run seed:reset
echo -e "\n${BLUE}🔹 Step 2/3: Running migrations...${NC}"
npm run migrate
echo -e "\n${BLUE}🔹 Step 3/3: Seeding data (LITE - no cities)...${NC}"
npm run seed:lite
echo -e "\n${GREEN}✅ Database reset (LITE) completed! Cities skipped for speed.${NC}"
else
echo -e "${YELLOW}Cancelled.${NC}"
fi
;;
9)
echo -e "\n${GREEN}🔬 Running All Tests...${NC}\n"
echo -e "${BLUE}🔹 Backend Unit Tests...${NC}"
cd backend && go test -v ./... -count=1 2>&1 | tail -20
BACKEND_RESULT=$?
cd ..
echo -e "\n${BLUE}🔹 Backend E2E Tests...${NC}"
cd backend && go test -tags=e2e -v ./tests/e2e/... 2>&1
E2E_RESULT=$?
cd ..
if [ -d "frontend/node_modules" ]; then
echo -e "\n${BLUE}🔹 Frontend Tests...${NC}"
cd frontend && npm test -- --passWithNoTests 2>&1
FRONTEND_RESULT=$?
cd ..
else
echo -e "${YELLOW}⚠️ Frontend node_modules not found, skipping frontend tests${NC}"
FRONTEND_RESULT=0
fi
echo -e "\n${GREEN}═══════════════════════════════════════${NC}"
if [ $BACKEND_RESULT -eq 0 ] && [ $E2E_RESULT -eq 0 ] && [ $FRONTEND_RESULT -eq 0 ]; then
echo -e "${GREEN}✅ All tests passed!${NC}"
else
echo -e "${RED}❌ Some tests failed${NC}"
fi
;;
0)
echo -e "${YELLOW}Bye! 👋${NC}"
exit 0
;;
*)
echo -e "${RED}Invalid option. Exiting.${NC}"
exit 1
;;
a)
echo -e "\n${GREEN}🐳 Building Docker Images...${NC}\n"
REGISTRY="forgejo-gru.rede5.com.br/rede5"
echo -e "${BLUE}🔹 Building Backend...${NC}"
podman build -t $REGISTRY/gohorsejobs-backend:latest -f backend/Dockerfile backend/
echo -e "\n${BLUE}🔹 Building Frontend...${NC}"
podman build -t $REGISTRY/gohorsejobs-frontend:latest -f frontend/Dockerfile frontend/
echo -e "\n${BLUE}🔹 Building Seeder...${NC}"
podman build -t $REGISTRY/gohorsejobs-seeder:latest -f seeder-api/Dockerfile seeder-api/
echo -e "\n${GREEN}✅ All images built successfully!${NC}"
echo -e " ${CYAN}Backend:${NC} $REGISTRY/gohorsejobs-backend:latest"
echo -e " ${CYAN}Frontend:${NC} $REGISTRY/gohorsejobs-frontend:latest"
echo -e " ${CYAN}Seeder:${NC} $REGISTRY/gohorsejobs-seeder:latest"
;;
b)
echo -e "\n${GREEN}🚀 Building & Pushing to Forgejo Registry...${NC}\n"
REGISTRY="forgejo-gru.rede5.com.br/rede5"
# Check if logged in
echo -e "${BLUE}🔹 Checking registry login...${NC}"
if ! podman login --get-login $REGISTRY 2>/dev/null; then
echo -e "${YELLOW}⚠️ Not logged in. Please enter your credentials:${NC}"
read -p "Username: " REGISTRY_USER
read -s -p "Password/Token: " REGISTRY_PASS
echo ""
podman login forgejo-gru.rede5.com.br -u "$REGISTRY_USER" -p "$REGISTRY_PASS"
fi
echo -e "\n${BLUE}🔹 Building Backend...${NC}"
podman build -t $REGISTRY/gohorsejobs-backend:latest -f backend/Dockerfile backend/
echo -e "\n${BLUE}🔹 Building Frontend...${NC}"
podman build -t $REGISTRY/gohorsejobs-frontend:latest -f frontend/Dockerfile frontend/
echo -e "\n${BLUE}🔹 Building Seeder...${NC}"
podman build -t $REGISTRY/gohorsejobs-seeder:latest -f seeder-api/Dockerfile seeder-api/
echo -e "\n${BLUE}🔹 Pushing Backend...${NC}"
podman push $REGISTRY/gohorsejobs-backend:latest
echo -e "\n${BLUE}🔹 Pushing Frontend...${NC}"
podman push $REGISTRY/gohorsejobs-frontend:latest
echo -e "\n${BLUE}🔹 Pushing Seeder...${NC}"
podman push $REGISTRY/gohorsejobs-seeder:latest
echo -e "\n${GREEN}✅ All images built and pushed!${NC}"
echo -e " ${CYAN}Registry:${NC} $REGISTRY"
echo -e " ${CYAN}Images:${NC} gohorsejobs-backend, gohorsejobs-frontend, gohorsejobs-seeder"
;;
esac